From a6fd05d00a3f48bb6f5bc6dc022991402d451832 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Sun, 12 Feb 2023 21:01:06 +0000 Subject: [PATCH 01/88] chore: update method doc --- pyneuroml/plot/PlotMorphology.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 7ec04e6c..d3bbfc75 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -176,7 +176,10 @@ def plot_2D( square: bool = False, plot_type: str = "Detailed", ): - """Plot cell morphology in 2D. + """Plot cell morphologies in 2D. + + If a file with a network containing multiple cells is provided, it will + plot all the cells. This uses matplotlib to plot the morphology in 2D. From 7bebd473e9765fd428ab6ea9c160691b01d9037e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 14:21:37 +0000 Subject: [PATCH 02/88] feat: add segment group schematic plotter --- pyneuroml/plot/PlotMorphology.py | 217 ++++++++++++++++++++++++++++- tests/plot/test_morphology_plot.py | 50 ++++++- 2 files changed, 265 insertions(+), 2 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index d3bbfc75..3b7cbca2 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -19,9 +19,11 @@ from matplotlib_scalebar.scalebar import ScaleBar import plotly.graph_objects as go -from pyneuroml.pynml import read_neuroml2_file +from pyneuroml.pynml import read_neuroml2_file, get_next_hex_color from pyneuroml.utils.cli import build_namespace from pyneuroml.utils import extract_position_info +from neuroml import (Segment, SegmentGroup, Cell) +from neuroml.neuro_lex_ids import neuro_lex_ids logger = logging.getLogger(__name__) @@ -664,5 +666,218 @@ def plot_interactive_3D( logger.info("Saved image to %s of plot: %s" % (save_to_file, title)) +def plot_2D_schematic( + cell: Cell, + segment_groups: list[SegmentGroup], + plane2d: str = "xy", + min_width: float = DEFAULTS["minwidth"], + verbose: bool = False, + square: bool = False, + nogui: bool = False, + save_to_file: typing.Optional[str] = None, +) -> None: + """Plot a 2D schematic of the provided segment groups. + + This plots each segment group as a straight line between its first and last + segment. + + :param cell: cell to plot + :type cell: Cell + :param segment_groups: list of unbranched segment groups to plot + :type segment_groups: list(SegmentGroup) + :param plane2d: what plane to plot (xy/yx/yz/zy/zx/xz) + :type plane2d: str + :param min_width: minimum width for segments (useful for visualising very + thin segments): default 0.8um + :type min_width: float + :param verbose: show extra information (default: False) + :type verbose: bool + :param square: scale axes so that image is approximately square + :type square: bool + :param nogui: do not show matplotlib GUI (default: false) + :type nogui: bool + :param save_to_file: optional filename to save generated morphology to + :type save_to_file: str + :returns: None + + """ + title = f"2D schematic of segment groups from {cell.id}" + ord_segs = cell.get_ordered_segments_in_groups( + segment_groups, check_parentage=False + ) + + fig, ax = plt.subplots(1, 1) # noqa + plt.get_current_fig_manager().set_window_title(title) + + ax.set_aspect("equal") + + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.yaxis.set_ticks_position("left") + ax.xaxis.set_ticks_position("bottom") + + if plane2d == "xy": + ax.set_xlabel("x (μm)") + ax.set_ylabel("y (μm)") + elif plane2d == "yx": + ax.set_xlabel("y (μm)") + ax.set_ylabel("x (μm)") + elif plane2d == "xz": + ax.set_xlabel("x (μm)") + ax.set_ylabel("z (μm)") + elif plane2d == "zx": + ax.set_xlabel("z (μm)") + ax.set_ylabel("x (μm)") + elif plane2d == "yz": + ax.set_xlabel("y (μm)") + ax.set_ylabel("z (μm)") + elif plane2d == "zy": + ax.set_xlabel("z (μm)") + ax.set_ylabel("y (μm)") + else: + logger.error(f"Invalid value for plane: {plane2d}") + sys.exit(-1) + + max_xaxis = -1 * float("inf") + min_xaxis = float("inf") + width = 1 + + for sg, segs in ord_segs.items(): + + sgobj = cell.get_segment_group(sg) + if sgobj.neuro_lex_id != neuro_lex_ids["section"]: + raise ValueError(f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment") + + # get proximal and distal points + first_seg = (segs[0]) # type: Segment + last_seg = (segs[-1]) # type: Segment + + # unique color for each segment group + color = get_next_hex_color() + + if plane2d == "xy": + min_xaxis, max_xaxis = add_line( + ax, + [first_seg.proximal.x, last_seg.distal.x], + [first_seg.proximal.y, last_seg.distal.y], + width, + color, + min_xaxis, + max_xaxis, + ) + elif plane2d == "yx": + min_xaxis, max_xaxis = add_line( + ax, + [first_seg.proximal.y, last_seg.distal.y], + [first_seg.proximal.x, last_seg.distal.x], + width, + color, + min_xaxis, + max_xaxis, + ) + elif plane2d == "xz": + min_xaxis, max_xaxis = add_line( + ax, + [first_seg.proximal.x, last_seg.distal.x], + [first_seg.proximal.z, last_seg.distal.z], + width, + color, + min_xaxis, + max_xaxis, + ) + elif plane2d == "zx": + min_xaxis, max_xaxis = add_line( + ax, + [first_seg.proximal.z, last_seg.distal.z], + [first_seg.proximal.x, last_seg.distal.x], + width, + color, + min_xaxis, + max_xaxis, + ) + elif plane2d == "yz": + min_xaxis, max_xaxis = add_line( + ax, + [first_seg.proximal.y, last_seg.distal.y], + [first_seg.proximal.z, last_seg.distal.z], + width, + color, + min_xaxis, + max_xaxis, + ) + elif plane2d == "zy": + min_xaxis, max_xaxis = add_line( + ax, + [first_seg.proximal.z, last_seg.distal.z], + [first_seg.proximal.y, last_seg.distal.y], + width, + color, + min_xaxis, + max_xaxis, + ) + else: + raise Exception(f"Invalid value for plane: {plane2d}") + + if verbose: + print("Extent x: %s -> %s" % (min_xaxis, max_xaxis)) + + # add a scalebar + # ax = fig.add_axes([0, 0, 1, 1]) + sc_val = 50 + if max_xaxis - min_xaxis < 100: + sc_val = 5 + if max_xaxis - min_xaxis < 10: + sc_val = 1 + scalebar1 = ScaleBar( + 0.001, + units="mm", + dimension="si-length", + scale_loc="top", + location="lower right", + fixed_value=sc_val, + fixed_units="um", + box_alpha=0.8, + ) + ax.add_artist(scalebar1) + + plt.autoscale() + xl = plt.xlim() + yl = plt.ylim() + if verbose: + print("Auto limits - x: %s , y: %s" % (xl, yl)) + + small = 0.1 + if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point + plt.xlim([-100, 100]) + plt.ylim([-100, 100]) + elif xl[1] - xl[0] < small: + d_10 = (yl[1] - yl[0]) / 10 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d_10, m + d_10]) + elif yl[1] - yl[0] < small: + d_10 = (xl[1] - xl[0]) / 10 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d_10, m + d_10]) + + if square: + if xl[1] - xl[0] > yl[1] - yl[0]: + d2 = (xl[1] - xl[0]) / 2 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d2, m + d2]) + + if xl[1] - xl[0] < yl[1] - yl[0]: + d2 = (yl[1] - yl[0]) / 2 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d2, m + d2]) + + if save_to_file: + abs_file = os.path.abspath(save_to_file) + plt.savefig(abs_file, dpi=200, bbox_inches="tight") + print(f"Saved image on plane {plane2d} to {abs_file} of plot: {title}") + + if not nogui: + plt.show() + + if __name__ == "__main__": main() diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 6fe196ec..3b22a1e1 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -11,7 +11,10 @@ import logging import pathlib as pl -from pyneuroml.plot.PlotMorphology import plot_2D, plot_interactive_3D +import neuroml +from pyneuroml.plot.PlotMorphology import (plot_2D, plot_interactive_3D, + plot_2D_schematic) +from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase logger = logging.getLogger(__name__) @@ -73,3 +76,48 @@ def test_3d_plotter(self): self.assertIsFile(filename) pl.Path(filename).unlink() + + def test_2d_schematic_plotter(self): + """Test plot_2D_schematic function.""" + nml_file = "tests/plot/Cell_497232312.cell.nml" + olm_file = "tests/plot/test.cell.nml" + + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + ofile = pl.Path(nml_file).name + + olm_doc = read_neuroml2_file(olm_file) + olm_cell = olm_doc.cells[0] # type: neuroml.Cell + olm_ofile = pl.Path(olm_file).name + + for plane in ["xy", "yz", "xz"]: + # olm cell + filename = f"test_schematic_plot_2d_{olm_ofile.replace('.', '_', 100)}_{plane}.png" + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + plot_2D_schematic( + olm_cell, segment_groups=["soma_0", "dendrite_0", "axon_0"], + nogui=False, plane2d=plane, save_to_file=filename + ) + + # more complex cell + filename = f"test_schematic_plot_2d_{ofile.replace('.', '_', 100)}_{plane}.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + sgs = cell.get_segment_groups_by_substring("apic_") + sgs_1 = cell.get_segment_groups_by_substring("dend_") + sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) + plot_2D_schematic( + cell, segment_groups=sgs_ids, + nogui=False, plane2d=plane, save_to_file=filename + ) + + self.assertIsFile(filename) + pl.Path(filename).unlink() From 0029f38acd75fba92b4143cd12efe7fd40b83b81 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 15:59:53 +0000 Subject: [PATCH 03/88] feat(schematic-plot): enable addition of segment group ids as labels --- pyneuroml/plot/PlotMorphology.py | 61 ++++++++++++++++++++++++++++-- pyneuroml/utils/plot.py | 38 +++++++++++++++++++ tests/plot/test_morphology_plot.py | 2 +- 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 pyneuroml/utils/plot.py diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 3b7cbca2..4bb5c7ad 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -22,7 +22,8 @@ from pyneuroml.pynml import read_neuroml2_file, get_next_hex_color from pyneuroml.utils.cli import build_namespace from pyneuroml.utils import extract_position_info -from neuroml import (Segment, SegmentGroup, Cell) +from pyneuroml.utils.plot import add_text_to_2D_plot +from neuroml import (SegmentGroup, Cell) from neuroml.neuro_lex_ids import neuro_lex_ids @@ -669,6 +670,7 @@ def plot_interactive_3D( def plot_2D_schematic( cell: Cell, segment_groups: list[SegmentGroup], + labels: bool = False, plane2d: str = "xy", min_width: float = DEFAULTS["minwidth"], verbose: bool = False, @@ -682,9 +684,11 @@ def plot_2D_schematic( segment. :param cell: cell to plot - :type cell: Cell + :type cell: neuroml.Cell :param segment_groups: list of unbranched segment groups to plot :type segment_groups: list(SegmentGroup) + :param labels: toggle labelling of segment groups + :type labels: bool :param plane2d: what plane to plot (xy/yx/yz/zy/zx/xz) :type plane2d: str :param min_width: minimum width for segments (useful for visualising very @@ -742,9 +746,9 @@ def plot_2D_schematic( min_xaxis = float("inf") width = 1 - for sg, segs in ord_segs.items(): + for sgid, segs in ord_segs.items(): - sgobj = cell.get_segment_group(sg) + sgobj = cell.get_segment_group(sgid) if sgobj.neuro_lex_id != neuro_lex_ids["section"]: raise ValueError(f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment") @@ -765,6 +769,15 @@ def plot_2D_schematic( min_xaxis, max_xaxis, ) + if labels: + add_text_to_2D_plot( + ax, + [first_seg.proximal.x, last_seg.distal.x], + [first_seg.proximal.y, last_seg.distal.y], + color=color, + text=sgid + ) + elif plane2d == "yx": min_xaxis, max_xaxis = add_line( ax, @@ -775,6 +788,14 @@ def plot_2D_schematic( min_xaxis, max_xaxis, ) + if labels: + add_text_to_2D_plot( + ax, + [first_seg.proximal.y, last_seg.distal.y], + [first_seg.proximal.x, last_seg.distal.x], + color=color, + text=sgid + ) elif plane2d == "xz": min_xaxis, max_xaxis = add_line( ax, @@ -785,6 +806,14 @@ def plot_2D_schematic( min_xaxis, max_xaxis, ) + if labels: + add_text_to_2D_plot( + ax, + [first_seg.proximal.x, last_seg.distal.x], + [first_seg.proximal.z, last_seg.distal.z], + color=color, + text=sgid + ) elif plane2d == "zx": min_xaxis, max_xaxis = add_line( ax, @@ -795,6 +824,14 @@ def plot_2D_schematic( min_xaxis, max_xaxis, ) + if labels: + add_text_to_2D_plot( + ax, + [first_seg.proximal.z, last_seg.distal.z], + [first_seg.proximal.x, last_seg.distal.x], + color=color, + text=sgid + ) elif plane2d == "yz": min_xaxis, max_xaxis = add_line( ax, @@ -805,6 +842,14 @@ def plot_2D_schematic( min_xaxis, max_xaxis, ) + if labels: + add_text_to_2D_plot( + ax, + [first_seg.proximal.y, last_seg.distal.y], + [first_seg.proximal.z, last_seg.distal.z], + color=color, + text=sgid + ) elif plane2d == "zy": min_xaxis, max_xaxis = add_line( ax, @@ -815,6 +860,14 @@ def plot_2D_schematic( min_xaxis, max_xaxis, ) + if labels: + add_text_to_2D_plot( + ax, + [first_seg.proximal.z, last_seg.distal.z], + [first_seg.proximal.y, last_seg.distal.y], + color=color, + text=sgid + ) else: raise Exception(f"Invalid value for plane: {plane2d}") diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py new file mode 100644 index 00000000..65e70663 --- /dev/null +++ b/pyneuroml/utils/plot.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +""" +Common utils to help with plotting + +File: pyneuroml/utils/plot.py + +Copyright 2023 NeuroML contributors +""" + +from matplotlib.axes import Axes +import numpy + + +def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str): + """Add text to a matplotlib plot between two points, center aligned + horizontally, and bottom aligned vertically + + :param ax: matplotlib axis object + :type ax: Axes + :param xv: start and end coordinates in one axis + :type xv: list[x1, x2] + :param yv: start and end coordinates in second axix + :type yv: list[y1, y2] + :param color: color of text + :type color: str + :param text: text to write + :type text: str + + """ + angle = int(numpy.rad2deg(numpy.arctan2((yv[1] - yv[0]), (xv[1] - xv[0])))) + if angle > 90: + angle -= 180 + elif angle < -90: + angle += 180 + + ax.text((xv[0] + xv[1]) / 2, (yv[0] + yv[1]) / 2, text, color=color, + horizontalalignment="center", verticalalignment="bottom", + rotation_mode="default", rotation=angle) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 3b22a1e1..373eca30 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -116,7 +116,7 @@ def test_2d_schematic_plotter(self): sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) plot_2D_schematic( cell, segment_groups=sgs_ids, - nogui=False, plane2d=plane, save_to_file=filename + nogui=False, plane2d=plane, save_to_file=filename, labels=True ) self.assertIsFile(filename) From 4b202c2d110d71132f8a4cdeb0b053afce52c146 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:04:29 +0000 Subject: [PATCH 04/88] feat(utils)!: move `get_next_hex_color` to separate utils module BREAKING CHANGE: `pynml.get_next_hex_color` -> `utils.plot.get_next_hex_color` --- pyneuroml/analysis/__init__.py | 3 ++- pyneuroml/lems/LEMSSimulation.py | 2 +- pyneuroml/lems/__init__.py | 3 ++- pyneuroml/plot/PlotMorphology.py | 4 ++-- pyneuroml/pynml.py | 16 ---------------- pyneuroml/utils/plot.py | 18 ++++++++++++++++++ 6 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pyneuroml/analysis/__init__.py b/pyneuroml/analysis/__init__.py index 38768419..22ca9bbb 100644 --- a/pyneuroml/analysis/__init__.py +++ b/pyneuroml/analysis/__init__.py @@ -7,6 +7,7 @@ from pyneuroml import pynml from pyneuroml.lems.LEMSSimulation import LEMSSimulation from pyneuroml.lems import generate_lems_file_for_neuroml +from pyneuroml.utils.plot import get_next_hex_color import neuroml as nml from pyelectro.analysis import max_min from pyelectro.analysis import mean_spike_frequency @@ -240,7 +241,7 @@ def generate_current_vs_frequency_curve( for i in range(number_cells): ref = "v_cell%i" % i quantity = "%s[%i]/v" % (pop.id, i) - ls.add_line_to_display(disp0, ref, quantity, "1mV", pynml.get_next_hex_color()) + ls.add_line_to_display(disp0, ref, quantity, "1mV", get_next_hex_color()) ls.add_column_to_output_file(of0, ref, quantity) lems_file_name = ls.save_to_file() diff --git a/pyneuroml/lems/LEMSSimulation.py b/pyneuroml/lems/LEMSSimulation.py index b1bad303..77d12e43 100644 --- a/pyneuroml/lems/LEMSSimulation.py +++ b/pyneuroml/lems/LEMSSimulation.py @@ -13,7 +13,7 @@ from neuroml import __version__ as libnml_ver from pyneuroml.pynml import read_neuroml2_file from pyneuroml.pynml import read_lems_file -from pyneuroml.pynml import get_next_hex_color +from pyneuroml.utils.plot import get_next_hex_color logger = logging.getLogger(__name__) diff --git a/pyneuroml/lems/__init__.py b/pyneuroml/lems/__init__.py index bc99316e..1ca81496 100644 --- a/pyneuroml/lems/__init__.py +++ b/pyneuroml/lems/__init__.py @@ -4,7 +4,8 @@ import shutil import os import logging -from pyneuroml.pynml import read_neuroml2_file, get_next_hex_color +from pyneuroml.pynml import read_neuroml2_file +from pyneuroml.utils.plot import get_next_hex_color import random import neuroml diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 4bb5c7ad..35ea06ec 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -19,10 +19,10 @@ from matplotlib_scalebar.scalebar import ScaleBar import plotly.graph_objects as go -from pyneuroml.pynml import read_neuroml2_file, get_next_hex_color +from pyneuroml.pynml import read_neuroml2_file from pyneuroml.utils.cli import build_namespace from pyneuroml.utils import extract_position_info -from pyneuroml.utils.plot import add_text_to_2D_plot +from pyneuroml.utils.plot import add_text_to_2D_plot, get_next_hex_color from neuroml import (SegmentGroup, Cell) from neuroml.neuro_lex_ids import neuro_lex_ids diff --git a/pyneuroml/pynml.py b/pyneuroml/pynml.py index 6477c621..271eb22b 100644 --- a/pyneuroml/pynml.py +++ b/pyneuroml/pynml.py @@ -1929,22 +1929,6 @@ def reload_saved_data( return traces -def get_next_hex_color(my_random: typing.Optional[random.Random] = None) -> str: - """Get a new randomly generated HEX colour code. - - You may pass a random.Random instance that you may be used. Otherwise the - default Python random generator will be used. - - :param my_random: a random.Random object - :type my_random: random.Random - :returns: HEX colour code - """ - if my_random is not None: - return "#%06x" % my_random.randint(0, 0xFFFFFF) - else: - return "#%06x" % random.randint(0, 0xFFFFFF) - - def confirm_file_exists(filename: str) -> None: """Check if a file exists, exit if it does not. diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 65e70663..3fadf1e9 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -9,6 +9,8 @@ from matplotlib.axes import Axes import numpy +import typing +import random def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str): @@ -36,3 +38,19 @@ def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str): ax.text((xv[0] + xv[1]) / 2, (yv[0] + yv[1]) / 2, text, color=color, horizontalalignment="center", verticalalignment="bottom", rotation_mode="default", rotation=angle) + + +def get_next_hex_color(my_random: typing.Optional[random.Random] = None) -> str: + """Get a new randomly generated HEX colour code. + + You may pass a random.Random instance that you may be used. Otherwise the + default Python random generator will be used. + + :param my_random: a random.Random object + :type my_random: random.Random + :returns: HEX colour code + """ + if my_random is not None: + return "#%06x" % my_random.randint(0, 0xFFFFFF) + else: + return "#%06x" % random.randint(0, 0xFFFFFF) From fbc0ef5c2e476dd44ec5bfb9f738a0a86a9a47cd Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:06:57 +0000 Subject: [PATCH 05/88] feat: bump MAJOR version number for API breaking change --- pyneuroml/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/__init__.py b/pyneuroml/__init__.py index 99f16415..b765b4e0 100644 --- a/pyneuroml/__init__.py +++ b/pyneuroml/__init__.py @@ -1,6 +1,6 @@ import logging -__version__ = "0.7.6" +__version__ = "1.0.0" JNEUROML_VERSION = "0.12.1" From eb243dabd981080c7b32188acf7c66b05e8ac94c Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:13:16 +0000 Subject: [PATCH 06/88] fix(plot): correct type annotation --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 35ea06ec..a9682648 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -669,7 +669,7 @@ def plot_interactive_3D( def plot_2D_schematic( cell: Cell, - segment_groups: list[SegmentGroup], + segment_groups: typing.List[SegmentGroup], labels: bool = False, plane2d: str = "xy", min_width: float = DEFAULTS["minwidth"], From 07ab95a7a539a1b1b7b8c6c846d6c82fc5704251 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:23:18 +0000 Subject: [PATCH 07/88] chore(test): turn off GUI for morph plot tests --- tests/plot/test_morphology_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 373eca30..98f71736 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -100,7 +100,7 @@ def test_2d_schematic_plotter(self): plot_2D_schematic( olm_cell, segment_groups=["soma_0", "dendrite_0", "axon_0"], - nogui=False, plane2d=plane, save_to_file=filename + nogui=True, plane2d=plane, save_to_file=filename ) # more complex cell @@ -116,7 +116,7 @@ def test_2d_schematic_plotter(self): sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) plot_2D_schematic( cell, segment_groups=sgs_ids, - nogui=False, plane2d=plane, save_to_file=filename, labels=True + nogui=True, plane2d=plane, save_to_file=filename, labels=True ) self.assertIsFile(filename) From 64ffeac26bd2f51ded851ed3bce3c8107ba2e073 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:35:13 +0000 Subject: [PATCH 08/88] chore(ci): remove duplicate pytest invocation `pytest` is called in `test-ghactions.sh` already --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f99ce792..8b29368f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,6 @@ jobs: - name: Run tests run: | - pytest --cov=pyneuroml -m "not localonly" . pynml -h ./test-ghactions.sh -neuron From ff5585b80045810dda39f94e71a8aab5ec92f491 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:38:43 +0000 Subject: [PATCH 09/88] test: run `nrnivmodl` before running morph export tests Required to correctly load the hoc files by NEURON --- test-ghactions.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test-ghactions.sh b/test-ghactions.sh index 796ba357..c7274cd0 100755 --- a/test-ghactions.sh +++ b/test-ghactions.sh @@ -132,6 +132,7 @@ if [ "$run_neuron_examples" == true ]; then echo "################################################" echo "## Try exporting morphologies to NeuroML from NEURON" + nrnivmodl # Export NeuroML v1 from NEURON example python export_neuroml1.py From 5328d3fa72213a06bd8e4edfb4336f0cac073d27 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 13 Feb 2023 16:53:28 +0000 Subject: [PATCH 10/88] wip: curtain plots [skip-ci] --- pyneuroml/plot/PlotMorphology.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index a9682648..1fd2907c 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -932,5 +932,38 @@ def plot_2D_schematic( plt.show() +def plot_segment_groups_curtain_plots( + cell: Cell, + segment_groups: typing.List[SegmentGroup], + labels: bool = False, + plane2d: str = "xy", + verbose: bool = False, + nogui: bool = False, + save_to_file: typing.Optional[str] = None, +) -> None: + """Plot curtain plots of provided segment groups. + + + :param cell: cell to plot + :type cell: neuroml.Cell + :param segment_groups: list of unbranched segment groups to plot + :type segment_groups: list(SegmentGroup) + :param labels: toggle labelling of segment groups + :type labels: bool + :param plane2d: what plane to plot (xy/yx/yz/zy/zx/xz) + :type plane2d: str + :param verbose: show extra information (default: False) + :type verbose: bool + :param square: scale axes so that image is approximately square + :type square: bool + :param nogui: do not show matplotlib GUI (default: false) + :type nogui: bool + :param save_to_file: optional filename to save generated morphology to + :type save_to_file: str + :returns: None + """ + pass + + if __name__ == "__main__": main() From f608b067c4fa57edd6478617b226e23f7302d43e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 12:36:12 +0000 Subject: [PATCH 11/88] fix(plot-morph): only create scalebar at end of plotting --- pyneuroml/plot/PlotMorphology.py | 90 ++++++++++++++++---------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 1fd2907c..94c562f2 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -874,54 +874,54 @@ def plot_2D_schematic( if verbose: print("Extent x: %s -> %s" % (min_xaxis, max_xaxis)) - # add a scalebar - # ax = fig.add_axes([0, 0, 1, 1]) - sc_val = 50 - if max_xaxis - min_xaxis < 100: - sc_val = 5 - if max_xaxis - min_xaxis < 10: - sc_val = 1 - scalebar1 = ScaleBar( - 0.001, - units="mm", - dimension="si-length", - scale_loc="top", - location="lower right", - fixed_value=sc_val, - fixed_units="um", - box_alpha=0.8, - ) - ax.add_artist(scalebar1) - - plt.autoscale() - xl = plt.xlim() - yl = plt.ylim() - if verbose: - print("Auto limits - x: %s , y: %s" % (xl, yl)) + # add a scalebar + # ax = fig.add_axes([0, 0, 1, 1]) + sc_val = 50 + if max_xaxis - min_xaxis < 100: + sc_val = 5 + if max_xaxis - min_xaxis < 10: + sc_val = 1 + scalebar1 = ScaleBar( + 0.001, + units="mm", + dimension="si-length", + scale_loc="top", + location="lower right", + fixed_value=sc_val, + fixed_units="um", + box_alpha=0.8, + ) + ax.add_artist(scalebar1) - small = 0.1 - if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point - plt.xlim([-100, 100]) - plt.ylim([-100, 100]) - elif xl[1] - xl[0] < small: - d_10 = (yl[1] - yl[0]) / 10 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d_10, m + d_10]) - elif yl[1] - yl[0] < small: - d_10 = (xl[1] - xl[0]) / 10 + plt.autoscale() + xl = plt.xlim() + yl = plt.ylim() + if verbose: + print("Auto limits - x: %s , y: %s" % (xl, yl)) + + small = 0.1 + if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point + plt.xlim([-100, 100]) + plt.ylim([-100, 100]) + elif xl[1] - xl[0] < small: + d_10 = (yl[1] - yl[0]) / 10 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d_10, m + d_10]) + elif yl[1] - yl[0] < small: + d_10 = (xl[1] - xl[0]) / 10 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d_10, m + d_10]) + + if square: + if xl[1] - xl[0] > yl[1] - yl[0]: + d2 = (xl[1] - xl[0]) / 2 m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d_10, m + d_10]) - - if square: - if xl[1] - xl[0] > yl[1] - yl[0]: - d2 = (xl[1] - xl[0]) / 2 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d2, m + d2]) + plt.ylim([m - d2, m + d2]) - if xl[1] - xl[0] < yl[1] - yl[0]: - d2 = (yl[1] - yl[0]) / 2 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d2, m + d2]) + if xl[1] - xl[0] < yl[1] - yl[0]: + d2 = (yl[1] - yl[0]) / 2 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d2, m + d2]) if save_to_file: abs_file = os.path.abspath(save_to_file) From 1895d53e4825f4f0186033f53d2f3b5896ddaf81 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 12:42:38 +0000 Subject: [PATCH 12/88] feat(plot-morph): add scalebar etc. at end of plotting and not in each iteration of the loop CC @pgleeson --- pyneuroml/plot/PlotMorphology.py | 92 ++++++++++++++++---------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 94c562f2..87fcd527 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -476,54 +476,54 @@ def plot_2D( if verbose: print("Extent x: %s -> %s" % (min_xaxis, max_xaxis)) - # add a scalebar - # ax = fig.add_axes([0, 0, 1, 1]) - sc_val = 50 - if max_xaxis - min_xaxis < 100: - sc_val = 5 - if max_xaxis - min_xaxis < 10: - sc_val = 1 - scalebar1 = ScaleBar( - 0.001, - units="mm", - dimension="si-length", - scale_loc="top", - location="lower right", - fixed_value=sc_val, - fixed_units="um", - box_alpha=0.8, - ) - ax.add_artist(scalebar1) + # add a scalebar + # ax = fig.add_axes([0, 0, 1, 1]) + sc_val = 50 + if max_xaxis - min_xaxis < 100: + sc_val = 5 + if max_xaxis - min_xaxis < 10: + sc_val = 1 + scalebar1 = ScaleBar( + 0.001, + units="mm", + dimension="si-length", + scale_loc="top", + location="lower right", + fixed_value=sc_val, + fixed_units="um", + box_alpha=0.8, + ) + ax.add_artist(scalebar1) - plt.autoscale() - xl = plt.xlim() - yl = plt.ylim() - if verbose: - print("Auto limits - x: %s , y: %s" % (xl, yl)) - - small = 0.1 - if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point - plt.xlim([-100, 100]) - plt.ylim([-100, 100]) - elif xl[1] - xl[0] < small: - d_10 = (yl[1] - yl[0]) / 10 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d_10, m + d_10]) - elif yl[1] - yl[0] < small: - d_10 = (xl[1] - xl[0]) / 10 + plt.autoscale() + xl = plt.xlim() + yl = plt.ylim() + if verbose: + print("Auto limits - x: %s , y: %s" % (xl, yl)) + + small = 0.1 + if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point + plt.xlim([-100, 100]) + plt.ylim([-100, 100]) + elif xl[1] - xl[0] < small: + d_10 = (yl[1] - yl[0]) / 10 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d_10, m + d_10]) + elif yl[1] - yl[0] < small: + d_10 = (xl[1] - xl[0]) / 10 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d_10, m + d_10]) + + if square: + if xl[1] - xl[0] > yl[1] - yl[0]: + d2 = (xl[1] - xl[0]) / 2 m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d_10, m + d_10]) - - if square: - if xl[1] - xl[0] > yl[1] - yl[0]: - d2 = (xl[1] - xl[0]) / 2 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d2, m + d2]) - - if xl[1] - xl[0] < yl[1] - yl[0]: - d2 = (yl[1] - yl[0]) / 2 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d2, m + d2]) + plt.ylim([m - d2, m + d2]) + + if xl[1] - xl[0] < yl[1] - yl[0]: + d2 = (yl[1] - yl[0]) / 2 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d2, m + d2]) if save_to_file: abs_file = os.path.abspath(save_to_file) From 1da15502b59eea734ae7054b726618ea1eabaed4 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 14:23:16 +0000 Subject: [PATCH 13/88] feat(curtain-plots): add method to create curtain plots of a list of segment groups --- pyneuroml/plot/PlotMorphology.py | 101 +++++++++++++++++++++++++++++-- pyneuroml/utils/plot.py | 19 +++++- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 87fcd527..398ba299 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -11,6 +11,7 @@ import argparse import os import sys +import random import typing import logging @@ -22,7 +23,8 @@ from pyneuroml.pynml import read_neuroml2_file from pyneuroml.utils.cli import build_namespace from pyneuroml.utils import extract_position_info -from pyneuroml.utils.plot import add_text_to_2D_plot, get_next_hex_color +from pyneuroml.utils.plot import (add_text_to_2D_plot, get_next_hex_color, + add_box_to_plot) from neuroml import (SegmentGroup, Cell) from neuroml.neuro_lex_ids import neuro_lex_ids @@ -936,10 +938,11 @@ def plot_segment_groups_curtain_plots( cell: Cell, segment_groups: typing.List[SegmentGroup], labels: bool = False, - plane2d: str = "xy", verbose: bool = False, nogui: bool = False, save_to_file: typing.Optional[str] = None, + overlay_data: typing.Dict[str, typing.List[typing.Any]] = None, + width: typing.Union[float, int] = 5 ) -> None: """Plot curtain plots of provided segment groups. @@ -950,8 +953,6 @@ def plot_segment_groups_curtain_plots( :type segment_groups: list(SegmentGroup) :param labels: toggle labelling of segment groups :type labels: bool - :param plane2d: what plane to plot (xy/yx/yz/zy/zx/xz) - :type plane2d: str :param verbose: show extra information (default: False) :type verbose: bool :param square: scale axes so that image is approximately square @@ -960,9 +961,99 @@ def plot_segment_groups_curtain_plots( :type nogui: bool :param save_to_file: optional filename to save generated morphology to :type save_to_file: str + :param overlay_data: data to overlay over the curtain plots; + this must be a dictionary with segment group ids as keys, and lists of + values to overlay as values. Each list should have a value for every + segment in the segment group. + :type overlay_data: dict, keys are segment group ids, values are lists of + magnitudes to overlay on curtain plots + :param width: width of each segment group + :type width: float/int :returns: None """ - pass + # use a random number generator so that the colours are always the same + myrandom = random.Random() + myrandom.seed(122436) + + title = f"Curtain plots of segment groups from {cell.id}" + (ord_segs, cumulative_lengths) = cell.get_ordered_segments_in_groups( + segment_groups, check_parentage=False, include_cumulative_lengths=True + ) + # plot setup + fig, ax = plt.subplots(1, 1) # noqa + plt.get_current_fig_manager().set_window_title(title) + + # ax.set_aspect("equal") + + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.yaxis.set_ticks_position("left") + ax.xaxis.set_ticks_position("none") + ax.xaxis.set_ticks([]) + + ax.set_xlabel("Segment group") + ax.set_ylabel("length (μm)") + + # column counter + column = 0 + for sgid, segs in ord_segs.items(): + column += 1 + length = 0 + + cumulative_lengths_sg = cumulative_lengths[sgid] + + sgobj = cell.get_segment_group(sgid) + if sgobj.neuro_lex_id != neuro_lex_ids["section"]: + raise ValueError(f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment") + + for seg_num in range(0, len(segs)): + seg = segs[seg_num] + cumulative_len = cumulative_lengths_sg[seg_num] + + add_box_to_plot( + ax, + [column * width - width * 0.25, -1 * length], + height=cumulative_len, + width=width * .5, + color=get_next_hex_color(myrandom), + ) + + length += cumulative_len + + add_text_to_2D_plot( + ax, + [column * width - width / 2, column * width + width / 2], + [10, 10], + color="black", + text=sgid + ) + + plt.autoscale() + xl = plt.xlim() + yl = plt.ylim() + if verbose: + print("Auto limits - x: %s , y: %s" % (xl, yl)) + + small = width + if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point + plt.xlim([-100, 100]) + plt.ylim([-100, 100]) + elif xl[1] - xl[0] < small: + d_10 = (yl[1] - yl[0]) / 10 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d_10, m + d_10]) + elif yl[1] - yl[0] < small: + d_10 = (xl[1] - xl[0]) / 10 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d_10, m + d_10]) + + if save_to_file: + abs_file = os.path.abspath(save_to_file) + plt.savefig(abs_file, dpi=200, bbox_inches="tight") + print(f"Saved image to {abs_file} of plot: {title}") + + if not nogui: + plt.show() if __name__ == "__main__": diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 3fadf1e9..01df91fe 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -7,10 +7,11 @@ Copyright 2023 NeuroML contributors """ -from matplotlib.axes import Axes import numpy import typing import random +from matplotlib.axes import Axes +from matplotlib.patches import Rectangle def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str): @@ -54,3 +55,19 @@ def get_next_hex_color(my_random: typing.Optional[random.Random] = None) -> str: return "#%06x" % my_random.randint(0, 0xFFFFFF) else: return "#%06x" % random.randint(0, 0xFFFFFF) + + +def add_box_to_plot(ax, xy, height, width, color): + """Add a box to a matplotlib plot, between points `xv[0], yv[0]` and + `xv[1], yv[1]`, of `width` and `color` + + :param ax: TODO + :param xv: TODO + :param height: TODO + :param width: TODO + :param color: TODO + :returns: TODO + + """ + ax.add_patch(Rectangle(xy, width, height, edgecolor=color, facecolor=color, + fill=True)) From c8eb0a14bca04360b8f1a6ea3597ebe95b1307fa Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 14:23:44 +0000 Subject: [PATCH 14/88] test(curtain-plot): add test --- tests/plot/test_morphology_plot.py | 32 ++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 98f71736..058654c5 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -13,7 +13,8 @@ import neuroml from pyneuroml.plot.PlotMorphology import (plot_2D, plot_interactive_3D, - plot_2D_schematic) + plot_2D_schematic, + plot_segment_groups_curtain_plots) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -116,8 +117,35 @@ def test_2d_schematic_plotter(self): sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) plot_2D_schematic( cell, segment_groups=sgs_ids, - nogui=True, plane2d=plane, save_to_file=filename, labels=True + nogui=False, plane2d=plane, save_to_file=filename, labels=True ) self.assertIsFile(filename) pl.Path(filename).unlink() + + def test_plot_segment_groups_curtain_plots(self): + """Test plot_segment_groups_curtain_plots function.""" + nml_file = "tests/plot/Cell_497232312.cell.nml" + + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + ofile = pl.Path(nml_file).name + + # more complex cell + filename = f"test_curtain_plot_2d_{ofile.replace('.', '_', 100)}.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + sgs = cell.get_segment_groups_by_substring("apic_") + # sgs_1 = cell.get_segment_groups_by_substring("dend_") + sgs_ids = list(sgs.keys()) # + list(sgs_1.keys()) + plot_segment_groups_curtain_plots( + cell, segment_groups=sgs_ids[0:20], + nogui=True, save_to_file=filename, labels=True + ) + + self.assertIsFile(filename) + pl.Path(filename).unlink() From 0fec0a0a191eaba789635572d56d7f37d78225f4 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 16:12:53 +0000 Subject: [PATCH 15/88] feat(plot-utils): extend text adder method to allow setting alignment --- pyneuroml/utils/plot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 01df91fe..7033a08f 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -14,9 +14,9 @@ from matplotlib.patches import Rectangle -def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str): - """Add text to a matplotlib plot between two points, center aligned - horizontally, and bottom aligned vertically +def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str, + horizontal="center", vertical="bottom"): + """Add text to a matplotlib plot between two points :param ax: matplotlib axis object :type ax: Axes @@ -37,7 +37,7 @@ def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str): angle += 180 ax.text((xv[0] + xv[1]) / 2, (yv[0] + yv[1]) / 2, text, color=color, - horizontalalignment="center", verticalalignment="bottom", + horizontalalignment=horizontal, verticalalignment=vertical, rotation_mode="default", rotation=angle) From ca1b3161985b40b65a82391dd417cd19042d24aa Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 16:13:33 +0000 Subject: [PATCH 16/88] feat(plotting): add method to generate curtain plots of lists of segment groups and overlay them with some data --- pyneuroml/plot/PlotMorphology.py | 86 ++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 398ba299..b75f05e2 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -16,6 +16,8 @@ import typing import logging +import numpy +import matplotlib from matplotlib import pyplot as plt from matplotlib_scalebar.scalebar import ScaleBar import plotly.graph_objects as go @@ -942,7 +944,9 @@ def plot_segment_groups_curtain_plots( nogui: bool = False, save_to_file: typing.Optional[str] = None, overlay_data: typing.Dict[str, typing.List[typing.Any]] = None, - width: typing.Union[float, int] = 5 + overlay_data_label: str = "", + width: typing.Union[float, int] = 4, + colormap_name:str = 'viridis' ) -> None: """Plot curtain plots of provided segment groups. @@ -961,15 +965,27 @@ def plot_segment_groups_curtain_plots( :type nogui: bool :param save_to_file: optional filename to save generated morphology to :type save_to_file: str - :param overlay_data: data to overlay over the curtain plots; + :param overlay_data: data to overlay over the curtain plots; this must be a dictionary with segment group ids as keys, and lists of values to overlay as values. Each list should have a value for every segment in the segment group. :type overlay_data: dict, keys are segment group ids, values are lists of magnitudes to overlay on curtain plots + :param overlay_data_label: label of data being overlaid + :type overlay_data_label: str :param width: width of each segment group :type width: float/int + :param colormap_name: name of matplotlib colourmap to use for data overlay + See: + https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.colormaps + Note: random colours are used for each segment if no data is to be overlaid + :type colormap_name: str :returns: None + + :raises ValueError: if keys in `overlay_data` do not match + ids of segment groups of `segment_groups` + :raises ValueError: if number of items for each key in `overlay_data` does + not match the number of segments in the corresponding segment group """ # use a random number generator so that the colours are always the same myrandom = random.Random() @@ -979,11 +995,40 @@ def plot_segment_groups_curtain_plots( (ord_segs, cumulative_lengths) = cell.get_ordered_segments_in_groups( segment_groups, check_parentage=False, include_cumulative_lengths=True ) + # plot setup fig, ax = plt.subplots(1, 1) # noqa plt.get_current_fig_manager().set_window_title(title) - # ax.set_aspect("equal") + # overlaying data related checks + data_max = -1 * float("inf") + data_min = float("inf") + acolormap = None + norm = None + if overlay_data: + if (set(overlay_data.keys()) != set(ord_segs.keys())): + raise ValueError( + "Keys of overlay_data and ord_segs must match." + ) + for key in overlay_data.keys(): + if len(overlay_data[key]) != len(ord_segs[key]): + raise ValueError( + f"Number of values for key {key} does not match in overlay_data({len(overlay_data[key])}) and the segment group ({len(ord_segs[key])})" + ) + + # since lists are of different lengths, one cannot use `numpy.max` + # on all the values directly + this_max = numpy.max(list(overlay_data[key])) + this_min = numpy.min(list(overlay_data[key])) + if this_max > data_max: + data_max = this_max + if this_min < data_min: + data_min = this_min + + acolormap = matplotlib.colormaps[colormap_name] + norm = matplotlib.colors.Normalize(vmin=data_min, vmax=data_max) + fig.colorbar(matplotlib.cm.ScalarMappable(norm=norm, cmap=acolormap), + label=overlay_data_label) ax.spines["right"].set_visible(False) ax.spines["bottom"].set_visible(False) @@ -1010,22 +1055,31 @@ def plot_segment_groups_curtain_plots( seg = segs[seg_num] cumulative_len = cumulative_lengths_sg[seg_num] + if overlay_data and acolormap and norm: + color = acolormap(norm(overlay_data[sgid][seg_num])) + else: + color = get_next_hex_color(myrandom) + + logger.debug(f"color is {color}") + add_box_to_plot( ax, - [column * width - width * 0.25, -1 * length], + [column * width - width * 0.10, -1 * length], height=cumulative_len, - width=width * .5, - color=get_next_hex_color(myrandom), + width=width * .8, + color=color ) length += cumulative_len add_text_to_2D_plot( ax, - [column * width - width / 2, column * width + width / 2], - [10, 10], + [column * width + width / 2, column * width + width / 2], + [50, 100], color="black", - text=sgid + text=sgid, + vertical="bottom", + horizontal="center" ) plt.autoscale() @@ -1034,18 +1088,8 @@ def plot_segment_groups_curtain_plots( if verbose: print("Auto limits - x: %s , y: %s" % (xl, yl)) - small = width - if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point - plt.xlim([-100, 100]) - plt.ylim([-100, 100]) - elif xl[1] - xl[0] < small: - d_10 = (yl[1] - yl[0]) / 10 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d_10, m + d_10]) - elif yl[1] - yl[0] < small: - d_10 = (xl[1] - xl[0]) / 10 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d_10, m + d_10]) + plt.ylim(top=0) + ax.set_yticklabels(abs(ax.get_yticks())) if save_to_file: abs_file = os.path.abspath(save_to_file) From a8e30f91f806886df4c5fa53cd5fe0a6f8056a90 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 16:14:00 +0000 Subject: [PATCH 17/88] test(curtain-plots): add tests for data overlaid curtain plots --- tests/plot/test_morphology_plot.py | 41 ++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 058654c5..c1e7c0b4 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -11,6 +11,7 @@ import logging import pathlib as pl +import numpy import neuroml from pyneuroml.plot.PlotMorphology import (plot_2D, plot_interactive_3D, plot_2D_schematic, @@ -143,8 +144,44 @@ def test_plot_segment_groups_curtain_plots(self): # sgs_1 = cell.get_segment_groups_by_substring("dend_") sgs_ids = list(sgs.keys()) # + list(sgs_1.keys()) plot_segment_groups_curtain_plots( - cell, segment_groups=sgs_ids[0:20], - nogui=True, save_to_file=filename, labels=True + cell, segment_groups=sgs_ids[0:50], + nogui=False, save_to_file=filename, labels=True + ) + + self.assertIsFile(filename) + pl.Path(filename).unlink() + + def test_plot_segment_groups_curtain_plots_with_data(self): + """Test plot_segment_groups_curtain_plots function with data overlay.""" + nml_file = "tests/plot/Cell_497232312.cell.nml" + + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + ofile = pl.Path(nml_file).name + + # more complex cell + filename = f"test_curtain_plot_2d_{ofile.replace('.', '_', 100)}_withdata.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + sgs = cell.get_segment_groups_by_substring("apic_") + sgs_1 = cell.get_segment_groups_by_substring("dend_") + sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) + data_dict = {} + + nsgs = 50 + + for sg_id in sgs_ids[0:nsgs]: + lensgs = len(cell.get_all_segments_in_group(sg_id)) + data_dict[sg_id] = numpy.random.random_integers(0, 100, lensgs) + + plot_segment_groups_curtain_plots( + cell, segment_groups=sgs_ids[0:nsgs], + nogui=False, save_to_file=filename, labels=True, + overlay_data=data_dict, overlay_data_label="Random values (0, 100)", width=4 ) self.assertIsFile(filename) From fb93a0590e99b1636d1405be5a21d18f502a1733 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Feb 2023 16:52:07 +0000 Subject: [PATCH 18/88] chore: turn off gui in plotting tests --- tests/plot/test_morphology_plot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index c1e7c0b4..a30cce8f 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -118,7 +118,7 @@ def test_2d_schematic_plotter(self): sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) plot_2D_schematic( cell, segment_groups=sgs_ids, - nogui=False, plane2d=plane, save_to_file=filename, labels=True + nogui=True, plane2d=plane, save_to_file=filename, labels=True ) self.assertIsFile(filename) @@ -145,7 +145,7 @@ def test_plot_segment_groups_curtain_plots(self): sgs_ids = list(sgs.keys()) # + list(sgs_1.keys()) plot_segment_groups_curtain_plots( cell, segment_groups=sgs_ids[0:50], - nogui=False, save_to_file=filename, labels=True + nogui=True, save_to_file=filename, labels=True ) self.assertIsFile(filename) @@ -180,7 +180,7 @@ def test_plot_segment_groups_curtain_plots_with_data(self): plot_segment_groups_curtain_plots( cell, segment_groups=sgs_ids[0:nsgs], - nogui=False, save_to_file=filename, labels=True, + nogui=True, save_to_file=filename, labels=True, overlay_data=data_dict, overlay_data_label="Random values (0, 100)", width=4 ) From 2f0320ec82386b30cf1601f9ca8b394848d9d948 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 14:07:06 +0000 Subject: [PATCH 19/88] refactor(plotting): move util methods to different module --- pyneuroml/plot/PlotMorphology.py | 843 ++++++++++++++--------------- pyneuroml/utils/plot.py | 242 ++++++++- tests/plot/test_morphology_plot.py | 43 +- 3 files changed, 661 insertions(+), 467 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index b75f05e2..f6dbcee4 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -19,20 +19,23 @@ import numpy import matplotlib from matplotlib import pyplot as plt -from matplotlib_scalebar.scalebar import ScaleBar import plotly.graph_objects as go from pyneuroml.pynml import read_neuroml2_file from pyneuroml.utils.cli import build_namespace from pyneuroml.utils import extract_position_info -from pyneuroml.utils.plot import (add_text_to_2D_plot, get_next_hex_color, - add_box_to_plot) +from pyneuroml.utils.plot import (add_text_to_matplotlib_2D_plot, get_next_hex_color, + add_box_to_matplotlib_2D_plot, + get_new_matplotlib_morph_plot, + autoscale_matplotlib_plot, + add_scalebar_to_matplotlib_plot, + add_line_to_matplotlib_2D_plot) from neuroml import (SegmentGroup, Cell) from neuroml.neuro_lex_ids import neuro_lex_ids logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) DEFAULTS = { @@ -145,34 +148,6 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): ) -########################################################################################## -# Taken from https://stackoverflow.com/questions/19394505/expand-the-line-with-specified-width-in-data-unit -from matplotlib.lines import Line2D - - -class LineDataUnits(Line2D): - def __init__(self, *args, **kwargs): - _lw_data = kwargs.pop("linewidth", 1) - super().__init__(*args, **kwargs) - self._lw_data = _lw_data - - def _get_lw(self): - if self.axes is not None: - ppd = 72.0 / self.axes.figure.dpi - trans = self.axes.transData.transform - return ((trans((1, self._lw_data)) - trans((0, 0))) * ppd)[1] - else: - return 1 - - def _set_lw(self, lw): - self._lw_data = lw - - _linewidth = property(_get_lw, _set_lw) - - -########################################################################################## - - def plot_2D( nml_file: str, plane2d: str = "xy", @@ -182,6 +157,7 @@ def plot_2D( save_to_file: typing.Optional[str] = None, square: bool = False, plot_type: str = "Detailed", + title: typing.Optional[str] = None ): """Plot cell morphologies in 2D. @@ -212,6 +188,8 @@ def plot_2D( - Schematic: only plot each unbranched segment group as a straight line, not following each segment :type plot_type: str + :param title: title of plot + :type title: str """ if plot_type not in ["Detailed", "Constant", "Schematic"]: @@ -236,7 +214,8 @@ def plot_2D( pop_id_vs_radii, ) = extract_position_info(nml_model, verbose) - title = "2D plot of %s from %s" % (nml_model.networks[0].id, nml_file) + if title is None: + title = "2D plot of %s from %s" % (nml_model.networks[0].id, nml_file) if verbose: logger.debug(f"positions: {positions}") @@ -245,40 +224,8 @@ def plot_2D( logger.debug(f"pop_id_vs_color: {pop_id_vs_color}") logger.debug(f"pop_id_vs_radii: {pop_id_vs_radii}") - fig, ax = plt.subplots(1, 1) # noqa - plt.get_current_fig_manager().set_window_title(title) - - ax.set_aspect("equal") - - ax.spines["right"].set_visible(False) - ax.spines["top"].set_visible(False) - ax.yaxis.set_ticks_position("left") - ax.xaxis.set_ticks_position("bottom") - - if plane2d == "xy": - ax.set_xlabel("x (μm)") - ax.set_ylabel("y (μm)") - elif plane2d == "yx": - ax.set_xlabel("y (μm)") - ax.set_ylabel("x (μm)") - elif plane2d == "xz": - ax.set_xlabel("x (μm)") - ax.set_ylabel("z (μm)") - elif plane2d == "zx": - ax.set_xlabel("z (μm)") - ax.set_ylabel("x (μm)") - elif plane2d == "yz": - ax.set_xlabel("y (μm)") - ax.set_ylabel("z (μm)") - elif plane2d == "zy": - ax.set_xlabel("z (μm)") - ax.set_ylabel("y (μm)") - else: - logger.error(f"Invalid value for plane: {plane2d}") - sys.exit(-1) - - max_xaxis = -1 * float("inf") - min_xaxis = float("inf") + fig, ax = get_new_matplotlib_morph_plot(title, plane2d) + axis_min_max = [float("inf"), -1 * float("inf")] for pop_id in pop_id_vs_cell: cell = pop_id_vs_cell[pop_id] @@ -286,248 +233,27 @@ def plot_2D( for cell_index in pos_pop: pos = pos_pop[cell_index] - - try: - soma_segs = cell.get_all_segments_in_group("soma_group") - except: - soma_segs = [] - try: - dend_segs = cell.get_all_segments_in_group("dendrite_group") - except: - dend_segs = [] - try: - axon_segs = cell.get_all_segments_in_group("axon_group") - except: - axon_segs = [] - - if cell is None: - - radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 - color = "b" - if pop_id in pop_id_vs_color: - color = pop_id_vs_color[pop_id] - - if plane2d == "xy": - min_xaxis, max_xaxis = add_line( - ax, - [pos[0], pos[0]], - [pos[1], pos[1]], - radius, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "yx": - min_xaxis, max_xaxis = add_line( - ax, - [pos[1], pos[1]], - [pos[0], pos[0]], - radius, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "xz": - min_xaxis, max_xaxis = add_line( - ax, - [pos[0], pos[0]], - [pos[2], pos[2]], - radius, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "zx": - min_xaxis, max_xaxis = add_line( - ax, - [pos[2], pos[2]], - [pos[0], pos[0]], - radius, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "yz": - min_xaxis, max_xaxis = add_line( - ax, - [pos[1], pos[1]], - [pos[2], pos[2]], - radius, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "zy": - min_xaxis, max_xaxis = add_line( - ax, - [pos[2], pos[2]], - [pos[1], pos[1]], - radius, - color, - min_xaxis, - max_xaxis, - ) - else: - raise Exception(f"Invalid value for plane: {plane2d}") - + radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 + color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None + + if plot_type == "Schematic": + plot_2D_schematic(offset=pos, cell=cell, segment_groups=None, labels=True, + plane2d=plane2d, min_width=min_width, + verbose=verbose, fig=fig, ax=ax, scalebar=False, + nogui=True, autoscale=False, square=False) else: - - for seg in cell.morphology.segments: - p = cell.get_actual_proximal(seg.id) - d = seg.distal - width = (p.diameter + d.diameter) / 2 - - if width < min_width: - width = min_width - - if plot_type == "Constant": - width = min_width - - color = "b" - if pop_id in pop_id_vs_color: - color = pop_id_vs_color[pop_id] - else: - if seg.id in soma_segs: - color = "g" - if seg.id in axon_segs: - color = "r" - - spherical = ( - p.x == d.x - and p.y == d.y - and p.z == d.z - and p.diameter == d.diameter - ) - - if verbose: - print( - "\nSeg %s, id: %s%s has proximal: %s, distal: %s (width: %s, min_width: %s), color: %s" - % ( - seg.name, - seg.id, - " (spherical)" if spherical else "", - p, - d, - width, - min_width, - str(color), - ) - ) - - if plane2d == "xy": - min_xaxis, max_xaxis = add_line( - ax, - [pos[0] + p.x, pos[0] + d.x], - [pos[1] + p.y, pos[1] + d.y], - width, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "yx": - min_xaxis, max_xaxis = add_line( - ax, - [pos[1] + p.y, pos[1] + d.y], - [pos[0] + p.x, pos[0] + d.x], - width, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "xz": - min_xaxis, max_xaxis = add_line( - ax, - [pos[0] + p.x, pos[0] + d.x], - [pos[2] + p.z, pos[2] + d.z], - width, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "zx": - min_xaxis, max_xaxis = add_line( - ax, - [pos[2] + p.z, pos[2] + d.z], - [pos[0] + p.x, pos[0] + d.x], - width, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "yz": - min_xaxis, max_xaxis = add_line( - ax, - [pos[1] + p.y, pos[1] + d.y], - [pos[2] + p.z, pos[2] + d.z], - width, - color, - min_xaxis, - max_xaxis, - ) - elif plane2d == "zy": - min_xaxis, max_xaxis = add_line( - ax, - [pos[2] + p.z, pos[2] + d.z], - [pos[1] + p.y, pos[1] + d.y], - width, - color, - min_xaxis, - max_xaxis, - ) - else: - raise Exception(f"Invalid value for plane: {plane2d}") - - if verbose: - print("Extent x: %s -> %s" % (min_xaxis, max_xaxis)) - - # add a scalebar - # ax = fig.add_axes([0, 0, 1, 1]) - sc_val = 50 - if max_xaxis - min_xaxis < 100: - sc_val = 5 - if max_xaxis - min_xaxis < 10: - sc_val = 1 - scalebar1 = ScaleBar( - 0.001, - units="mm", - dimension="si-length", - scale_loc="top", - location="lower right", - fixed_value=sc_val, - fixed_units="um", - box_alpha=0.8, - ) - ax.add_artist(scalebar1) - - plt.autoscale() - xl = plt.xlim() - yl = plt.ylim() - if verbose: - print("Auto limits - x: %s , y: %s" % (xl, yl)) - - small = 0.1 - if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point - plt.xlim([-100, 100]) - plt.ylim([-100, 100]) - elif xl[1] - xl[0] < small: - d_10 = (yl[1] - yl[0]) / 10 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d_10, m + d_10]) - elif yl[1] - yl[0] < small: - d_10 = (xl[1] - xl[0]) / 10 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d_10, m + d_10]) - - if square: - if xl[1] - xl[0] > yl[1] - yl[0]: - d2 = (xl[1] - xl[0]) / 2 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d2, m + d2]) - - if xl[1] - xl[0] < yl[1] - yl[0]: - d2 = (yl[1] - yl[0]) / 2 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d2, m + d2]) + plot_2D_cell_morphology(offset=pos, cell=cell, plane2d=plane2d, + color=color, soma_radius=radius, + plot_type=plot_type, verbose=verbose, + fig=fig, + ax=ax, min_width=min_width, + axis_min_max=axis_min_max, + scalebar=False, + nogui=True, autoscale=False, + square=False) + + add_scalebar_to_matplotlib_plot(axis_min_max, ax) + autoscale_matplotlib_plot(verbose, square) if save_to_file: abs_file = os.path.abspath(save_to_file) @@ -536,29 +262,8 @@ def plot_2D( if not nogui: plt.show() - - -def add_line(ax, xv, yv, width, color, min_xaxis, max_xaxis): - - if ( - abs(xv[0] - xv[1]) < 0.01 and abs(yv[0] - yv[1]) < 0.01 - ): # looking at the cylinder from the top, OR a sphere, so draw a circle - xv[1] = xv[1] + width / 1000.0 - yv[1] = yv[1] + width / 1000.0 - - ax.add_line( - LineDataUnits(xv, yv, linewidth=width, solid_capstyle="round", color=color) - ) - - ax.add_line( - LineDataUnits(xv, yv, linewidth=width, solid_capstyle="butt", color=color) - ) - - min_xaxis = min(min_xaxis, xv[0]) - min_xaxis = min(min_xaxis, xv[1]) - max_xaxis = max(max_xaxis, xv[0]) - max_xaxis = max(max_xaxis, xv[1]) - return min_xaxis, max_xaxis + else: + plt.close() def plot_interactive_3D( @@ -671,9 +376,276 @@ def plot_interactive_3D( logger.info("Saved image to %s of plot: %s" % (save_to_file, title)) +def plot_2D_cell_morphology( + offset: typing.List[float] = [0, 0], + cell: Cell = None, + plane2d: str = "xy", + color: typing.Optional[str] = None, + soma_radius: float = 0., + title: str = "", + verbose: bool = False, + fig: matplotlib.figure.Figure = None, + ax: matplotlib.axes.Axes = None, + min_width: float = DEFAULTS['minwidth'], + axis_min_max: typing.List = [float("inf"), -1 * float("inf")], + scalebar: bool = False, + nogui: bool = True, + autoscale: bool = True, + square: bool = True, + plot_type: str = "Detailed", + save_to_file: typing.Optional[str] = None, +): + """Plot the detailed 2D morphology of a cell in provided plane + + :param offset: offset for cell + :type offset: [float, float] + :param cell: cell to plot + :type cell: neuroml.Cell + :param plane2d: plane to plot on + :type plane2d: str + :param color: color to use for all segments + :type color: str + :param soma_radius: radius of soma (uses min_width if provided) + :type soma_radius: float + :param fig: a matplotlib.figure.Figure object to use + :type fig: matplotlib.figure.Figure + :param ax: a matplotlib.axes.Axes object to use + :type ax: matplotlib.axes.Axes + :param min_width: minimum width for segments (useful for visualising very + thin segments): default 0.8um + :type min_width: float + :param axis_min_max: min, max value of axes + :type axis_min_max: [float, float] + :param title: title of plot + :type title: str + :param verbose: show extra information (default: False) + :type verbose: bool + :param nogui: do not show matplotlib GUI (default: false) + :type nogui: bool + :param save_to_file: optional filename to save generated morphology to + :type save_to_file: str + :param square: scale axes so that image is approximately square + :type square: bool + :param autoscale: toggle autoscaling + :type autoscale: bool + :param scalebar: toggle scalebar + :type scalebar: bool + + :raises: ValueError if `cell` is None + + """ + if cell is None: + raise ValueError("No cell provided") + + try: + soma_segs = cell.get_all_segments_in_group("soma_group") + except Exception: + soma_segs = [] + try: + dend_segs = cell.get_all_segments_in_group("dendrite_group") + except Exception: + dend_segs = [] + try: + axon_segs = cell.get_all_segments_in_group("axon_group") + except Exception: + axon_segs = [] + + new_fig = False + if fig is None: + fig, ax = get_new_matplotlib_morph_plot(title) + new_fig = True + + # random default color + cell_color = get_next_hex_color() + if cell is None: + + if soma_radius is None: + soma_radius = 10 + + if plot_type == "Constant": + soma_radius = min_width + + if plane2d == "xy": + add_line_to_matplotlib_2D_plot( + ax, + [offset[0], offset[0]], + [offset[1], offset[1]], + soma_radius, + cell_color if color is None else color, + axis_min_max + ) + elif plane2d == "yx": + add_line_to_matplotlib_2D_plot( + ax, + [offset[1], offset[1]], + [offset[0], offset[0]], + soma_radius, + cell_color if color is None else color, + axis_min_max + ) + elif plane2d == "xz": + add_line_to_matplotlib_2D_plot( + ax, + [offset[0], offset[0]], + [offset[2], offset[2]], + soma_radius, + cell_color if color is None else color, + axis_min_max + ) + elif plane2d == "zx": + add_line_to_matplotlib_2D_plot( + ax, + [offset[2], offset[2]], + [offset[0], offset[0]], + soma_radius, + cell_color if color is None else color, + axis_min_max + ) + elif plane2d == "yz": + add_line_to_matplotlib_2D_plot( + ax, + [offset[1], offset[1]], + [offset[2], offset[2]], + soma_radius, + cell_color if color is None else color, + axis_min_max + ) + elif plane2d == "zy": + add_line_to_matplotlib_2D_plot( + ax, + [offset[2], offset[2]], + [offset[1], offset[1]], + soma_radius, + cell_color if color is None else color, + axis_min_max + ) + else: + raise Exception(f"Invalid value for plane: {plane2d}") + + else: + + for seg in cell.morphology.segments: + p = cell.get_actual_proximal(seg.id) + d = seg.distal + width = (p.diameter + d.diameter) / 2 + + if width < min_width: + width = min_width + + if plot_type == "Constant": + width = min_width + + seg_color = "b" + if seg.id in soma_segs: + seg_color = "g" + elif seg.id in axon_segs: + seg_color = "r" + + spherical = ( + p.x == d.x + and p.y == d.y + and p.z == d.z + and p.diameter == d.diameter + ) + + if verbose: + print( + "\nSeg %s, id: %s%s has proximal: %s, distal: %s (width: %s, min_width: %s), color: %s" + % ( + seg.name, + seg.id, + " (spherical)" if spherical else "", + p, + d, + width, + min_width, + str(seg_color), + ) + ) + + if plane2d == "xy": + add_line_to_matplotlib_2D_plot( + ax, + [offset[0] + p.x, offset[0] + d.x], + [offset[1] + p.y, offset[1] + d.y], + width, + seg_color if color is None else color, + axis_min_max + ) + elif plane2d == "yx": + add_line_to_matplotlib_2D_plot( + ax, + [offset[1] + p.y, offset[1] + d.y], + [offset[0] + p.x, offset[0] + d.x], + width, + seg_color if color is None else color, + axis_min_max + ) + elif plane2d == "xz": + add_line_to_matplotlib_2D_plot( + ax, + [offset[0] + p.x, offset[0] + d.x], + [offset[2] + p.z, offset[2] + d.z], + width, + seg_color if color is None else color, + axis_min_max + ) + elif plane2d == "zx": + add_line_to_matplotlib_2D_plot( + ax, + [offset[2] + p.z, offset[2] + d.z], + [offset[0] + p.x, offset[0] + d.x], + width, + seg_color if color is None else color, + axis_min_max + ) + elif plane2d == "yz": + add_line_to_matplotlib_2D_plot( + ax, + [offset[1] + p.y, offset[1] + d.y], + [offset[2] + p.z, offset[2] + d.z], + width, + seg_color if color is None else color, + axis_min_max + ) + elif plane2d == "zy": + add_line_to_matplotlib_2D_plot( + ax, + [offset[2] + p.z, offset[2] + d.z], + [offset[1] + p.y, offset[1] + d.y], + width, + seg_color if color is None else color, + axis_min_max + ) + else: + raise Exception(f"Invalid value for plane: {plane2d}") + + if verbose: + print("Extent x: %s -> %s" % (axis_min_max[0], axis_min_max[1])) + + if scalebar: + add_scalebar_to_matplotlib_plot(axis_min_max, ax) + if autoscale: + autoscale_matplotlib_plot(verbose, square) + + if save_to_file: + abs_file = os.path.abspath(save_to_file) + plt.savefig(abs_file, dpi=200, bbox_inches="tight") + print(f"Saved image on plane {plane2d} to {abs_file} of plot: {title}") + + if not nogui: + plt.show() + else: + # only close if a new fig was created + # if a figure was passed in, the caller needs to close it. + if new_fig: + plt.close() + + def plot_2D_schematic( cell: Cell, - segment_groups: typing.List[SegmentGroup], + segment_groups: typing.Optional[typing.List[SegmentGroup]], + offset: typing.List[float] = [0, 0], labels: bool = False, plane2d: str = "xy", min_width: float = DEFAULTS["minwidth"], @@ -681,12 +653,19 @@ def plot_2D_schematic( square: bool = False, nogui: bool = False, save_to_file: typing.Optional[str] = None, + scalebar: bool = True, + autoscale: bool = True, + fig: matplotlib.figure.Figure = None, + ax: matplotlib.axes.Axes = None, + title: str = "" ) -> None: """Plot a 2D schematic of the provided segment groups. This plots each segment group as a straight line between its first and last segment. + :param offset: offset for cell + :type offset: [float, float] :param cell: cell to plot :type cell: neuroml.Cell :param segment_groups: list of unbranched segment groups to plot @@ -706,23 +685,39 @@ def plot_2D_schematic( :type nogui: bool :param save_to_file: optional filename to save generated morphology to :type save_to_file: str - :returns: None + :param fig: a matplotlib.figure.Figure object to use + :type fig: matplotlib.figure.Figure + :param ax: a matplotlib.axes.Axes object to use + :type ax: matplotlib.axes.Axes + :param title: title of plot + :type title: str + :param square: scale axes so that image is approximately square + :type square: bool + :param autoscale: toggle autoscaling + :type autoscale: bool + :param scalebar: toggle scalebar + :type scalebar: bool """ - title = f"2D schematic of segment groups from {cell.id}" + if title == "": + title = f"2D schematic of segment groups from {cell.id}" + + # if no segment groups are given, do them all + if segment_groups is None: + segment_groups = [] + for sg in cell.morphology.segment_groups: + if sg.neuro_lex_id == neuro_lex_ids["section"]: + segment_groups.append(sg.id) + ord_segs = cell.get_ordered_segments_in_groups( segment_groups, check_parentage=False ) - fig, ax = plt.subplots(1, 1) # noqa - plt.get_current_fig_manager().set_window_title(title) - - ax.set_aspect("equal") - - ax.spines["right"].set_visible(False) - ax.spines["top"].set_visible(False) - ax.yaxis.set_ticks_position("left") - ax.xaxis.set_ticks_position("bottom") + new_fig = False + if fig is None: + logger.debug("No figure provided, creating new fig and ax") + fig, ax = get_new_matplotlib_morph_plot(title, plane2d) + new_fig = True if plane2d == "xy": ax.set_xlabel("x (μm)") @@ -746,8 +741,9 @@ def plot_2D_schematic( logger.error(f"Invalid value for plane: {plane2d}") sys.exit(-1) - max_xaxis = -1 * float("inf") - min_xaxis = float("inf") + # use a mutable object so it can be passed as an argument to methods, using + # float (immuatable) variables requires us to return these from all methods + axis_min_max = [float("inf"), -1 * float("inf")] width = 1 for sgid, segs in ord_segs.items(): @@ -764,111 +760,105 @@ def plot_2D_schematic( color = get_next_hex_color() if plane2d == "xy": - min_xaxis, max_xaxis = add_line( + add_line_to_matplotlib_2D_plot( ax, - [first_seg.proximal.x, last_seg.distal.x], - [first_seg.proximal.y, last_seg.distal.y], + [offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], + [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], width, color, - min_xaxis, - max_xaxis, + axis_min_max ) if labels: - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, - [first_seg.proximal.x, last_seg.distal.x], - [first_seg.proximal.y, last_seg.distal.y], + [offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], + [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], color=color, text=sgid ) elif plane2d == "yx": - min_xaxis, max_xaxis = add_line( + add_line_to_matplotlib_2D_plot( ax, - [first_seg.proximal.y, last_seg.distal.y], - [first_seg.proximal.x, last_seg.distal.x], + [offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], + [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], width, color, - min_xaxis, - max_xaxis, + axis_min_max ) if labels: - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, - [first_seg.proximal.y, last_seg.distal.y], - [first_seg.proximal.x, last_seg.distal.x], + [offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], + [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], color=color, text=sgid ) elif plane2d == "xz": - min_xaxis, max_xaxis = add_line( + add_line_to_matplotlib_2D_plot( ax, - [first_seg.proximal.x, last_seg.distal.x], - [first_seg.proximal.z, last_seg.distal.z], + [offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], + [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], width, color, - min_xaxis, - max_xaxis, + axis_min_max ) if labels: - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, - [first_seg.proximal.x, last_seg.distal.x], - [first_seg.proximal.z, last_seg.distal.z], + [offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], + [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], color=color, text=sgid ) elif plane2d == "zx": - min_xaxis, max_xaxis = add_line( + add_line_to_matplotlib_2D_plot( ax, - [first_seg.proximal.z, last_seg.distal.z], - [first_seg.proximal.x, last_seg.distal.x], + [offset[0] + first_seg.proximal.z, offset[0] + last_seg.distal.z], + [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], width, color, - min_xaxis, - max_xaxis, + axis_min_max ) if labels: - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, - [first_seg.proximal.z, last_seg.distal.z], - [first_seg.proximal.x, last_seg.distal.x], + [offset[0] + first_seg.proximal.z, offset[0] + last_seg.distal.z], + [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], color=color, text=sgid ) elif plane2d == "yz": - min_xaxis, max_xaxis = add_line( + add_line_to_matplotlib_2D_plot( ax, - [first_seg.proximal.y, last_seg.distal.y], - [first_seg.proximal.z, last_seg.distal.z], + [offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], + [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], width, color, - min_xaxis, - max_xaxis, + axis_min_max ) if labels: - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, - [first_seg.proximal.y, last_seg.distal.y], - [first_seg.proximal.z, last_seg.distal.z], + [offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], + [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], color=color, text=sgid ) elif plane2d == "zy": - min_xaxis, max_xaxis = add_line( + add_line_to_matplotlib_2D_plot( ax, - [first_seg.proximal.z, last_seg.distal.z], - [first_seg.proximal.y, last_seg.distal.y], + [offset[0] + first_seg.proximal.z, offset[0] + last_seg.distal.z], + [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], width, color, - min_xaxis, - max_xaxis, + axis_min_max ) if labels: - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, - [first_seg.proximal.z, last_seg.distal.z], - [first_seg.proximal.y, last_seg.distal.y], + [offset[0] + first_seg.proximal.z, offset[0] + last_seg.distal.z], + [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], color=color, text=sgid ) @@ -876,56 +866,12 @@ def plot_2D_schematic( raise Exception(f"Invalid value for plane: {plane2d}") if verbose: - print("Extent x: %s -> %s" % (min_xaxis, max_xaxis)) - - # add a scalebar - # ax = fig.add_axes([0, 0, 1, 1]) - sc_val = 50 - if max_xaxis - min_xaxis < 100: - sc_val = 5 - if max_xaxis - min_xaxis < 10: - sc_val = 1 - scalebar1 = ScaleBar( - 0.001, - units="mm", - dimension="si-length", - scale_loc="top", - location="lower right", - fixed_value=sc_val, - fixed_units="um", - box_alpha=0.8, - ) - ax.add_artist(scalebar1) + print("Extent x: %s -> %s" % (axis_min_max[0], axis_min_max[1])) - plt.autoscale() - xl = plt.xlim() - yl = plt.ylim() - if verbose: - print("Auto limits - x: %s , y: %s" % (xl, yl)) - - small = 0.1 - if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point - plt.xlim([-100, 100]) - plt.ylim([-100, 100]) - elif xl[1] - xl[0] < small: - d_10 = (yl[1] - yl[0]) / 10 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d_10, m + d_10]) - elif yl[1] - yl[0] < small: - d_10 = (xl[1] - xl[0]) / 10 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d_10, m + d_10]) - - if square: - if xl[1] - xl[0] > yl[1] - yl[0]: - d2 = (xl[1] - xl[0]) / 2 - m = yl[0] + (yl[1] - yl[0]) / 2.0 - plt.ylim([m - d2, m + d2]) - - if xl[1] - xl[0] < yl[1] - yl[0]: - d2 = (yl[1] - yl[0]) / 2 - m = xl[0] + (xl[1] - xl[0]) / 2.0 - plt.xlim([m - d2, m + d2]) + if scalebar: + add_scalebar_to_matplotlib_plot(axis_min_max, ax) + if autoscale: + autoscale_matplotlib_plot(verbose, square) if save_to_file: abs_file = os.path.abspath(save_to_file) @@ -934,6 +880,11 @@ def plot_2D_schematic( if not nogui: plt.show() + # only close if a new fig was created + # if a figure was passed in, the caller needs to close it. + else: + if new_fig: + plt.close() def plot_segment_groups_curtain_plots( @@ -946,11 +897,10 @@ def plot_segment_groups_curtain_plots( overlay_data: typing.Dict[str, typing.List[typing.Any]] = None, overlay_data_label: str = "", width: typing.Union[float, int] = 4, - colormap_name:str = 'viridis' + colormap_name: str = 'viridis' ) -> None: """Plot curtain plots of provided segment groups. - :param cell: cell to plot :type cell: neuroml.Cell :param segment_groups: list of unbranched segment groups to plot @@ -1062,7 +1012,7 @@ def plot_segment_groups_curtain_plots( logger.debug(f"color is {color}") - add_box_to_plot( + add_box_to_matplotlib_2D_plot( ax, [column * width - width * 0.10, -1 * length], height=cumulative_len, @@ -1072,7 +1022,7 @@ def plot_segment_groups_curtain_plots( length += cumulative_len - add_text_to_2D_plot( + add_text_to_matplotlib_2D_plot( ax, [column * width + width / 2, column * width + width / 2], [50, 100], @@ -1098,6 +1048,7 @@ def plot_segment_groups_curtain_plots( if not nogui: plt.show() + plt.close() if __name__ == "__main__": diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 7033a08f..42717eda 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -7,17 +7,31 @@ Copyright 2023 NeuroML contributors """ +import logging import numpy import typing import random +import matplotlib +from matplotlib import pyplot as plt +from matplotlib.lines import Line2D from matplotlib.axes import Axes from matplotlib.patches import Rectangle +from matplotlib_scalebar.scalebar import ScaleBar -def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str, - horizontal="center", vertical="bottom"): +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +def add_text_to_matplotlib_2D_plot( + ax: Axes, xv, yv, color, text: str, horizontal="center", vertical="bottom" +): """Add text to a matplotlib plot between two points + Wrapper around matplotlib.axes.Axes.text. + + Rotates the text label to ensure it is at the same angle as the line. + :param ax: matplotlib axis object :type ax: Axes :param xv: start and end coordinates in one axis @@ -36,9 +50,17 @@ def add_text_to_2D_plot(ax: Axes, xv, yv, color, text: str, elif angle < -90: angle += 180 - ax.text((xv[0] + xv[1]) / 2, (yv[0] + yv[1]) / 2, text, color=color, - horizontalalignment=horizontal, verticalalignment=vertical, - rotation_mode="default", rotation=angle) + ax.text( + (xv[0] + xv[1]) / 2, + (yv[0] + yv[1]) / 2, + text, + color=color, + horizontalalignment=horizontal, + verticalalignment=vertical, + rotation_mode="default", + rotation=angle, + clip_on=True, + ) def get_next_hex_color(my_random: typing.Optional[random.Random] = None) -> str: @@ -57,17 +79,205 @@ def get_next_hex_color(my_random: typing.Optional[random.Random] = None) -> str: return "#%06x" % random.randint(0, 0xFFFFFF) -def add_box_to_plot(ax, xy, height, width, color): - """Add a box to a matplotlib plot, between points `xv[0], yv[0]` and - `xv[1], yv[1]`, of `width` and `color` +def add_box_to_matplotlib_2D_plot(ax, xy, height, width, color): + """Add a box to a matplotlib plot, at xy of `height`, `width` and `color`. + + :param ax: matplotlib.axes.Axes object + :type ax: matplotlob.axes.Axis + :param xy: bottom left corner of box + :type xy: typing.List[float] + :param height: height of box + :type height: float + :param width: width of box + :type width: float + :param color: color of box for edge, face, fill + :type color: str + :returns: None + + """ + ax.add_patch( + Rectangle(xy, width, height, edgecolor=color, facecolor=color, fill=True) + ) + + +def get_new_matplotlib_morph_plot( + title: str = "", plane2d: str = "xy" +) -> typing.Tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]: + """Get a new 2D matplotlib plot for morphology related plots. + + :param title: title of plot + :type title: str + :param plane2d: plane to use + :type plane: str + :returns: new [matplotlib.figure.Figure, matplotlib.axes.Axes] + :rtype: [matplotlib.figure.Figure, matplotlib.axes.Axes] + """ + fig, ax = plt.subplots(1, 1) # noqa + plt.get_current_fig_manager().set_window_title(title) + + ax.set_aspect("equal") + + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.yaxis.set_ticks_position("left") + ax.xaxis.set_ticks_position("bottom") + + if plane2d == "xy": + ax.set_xlabel("x (μm)") + ax.set_ylabel("y (μm)") + elif plane2d == "yx": + ax.set_xlabel("y (μm)") + ax.set_ylabel("x (μm)") + elif plane2d == "xz": + ax.set_xlabel("x (μm)") + ax.set_ylabel("z (μm)") + elif plane2d == "zx": + ax.set_xlabel("z (μm)") + ax.set_ylabel("x (μm)") + elif plane2d == "yz": + ax.set_xlabel("y (μm)") + ax.set_ylabel("z (μm)") + elif plane2d == "zy": + ax.set_xlabel("z (μm)") + ax.set_ylabel("y (μm)") + else: + raise ValueError(f"Invalid value for plane: {plane2d}") + + return fig, ax + + +class LineDataUnits(Line2D): + """New Line class for making lines with specific widthS + + Reference: + https://stackoverflow.com/questions/19394505/expand-the-line-with-specified-width-in-data-unit + """ + + def __init__(self, *args, **kwargs): + _lw_data = kwargs.pop("linewidth", 1) + super().__init__(*args, **kwargs) + self._lw_data = _lw_data + + def _get_lw(self): + if self.axes is not None: + ppd = 72.0 / self.axes.figure.dpi + trans = self.axes.transData.transform + return ((trans((1, self._lw_data)) - trans((0, 0))) * ppd)[1] + else: + return 1 + + def _set_lw(self, lw): + self._lw_data = lw + + _linewidth = property(_get_lw, _set_lw) + + +def autoscale_matplotlib_plot(verbose: bool = False, square: bool = True) -> None: + """Autoscale the current matplotlib plot + + :param verbose: toggle verbosity + :type verbose: bool + :param square: toggle squaring of plot + :type square: bool + :returns: None + + """ + plt.autoscale() + xl = plt.xlim() + yl = plt.ylim() + if verbose: + print("Auto limits - x: %s , y: %s" % (xl, yl)) + + small = 0.1 + if xl[1] - xl[0] < small and yl[1] - yl[0] < small: # i.e. only a point + plt.xlim([-100, 100]) + plt.ylim([-100, 100]) + elif xl[1] - xl[0] < small: + d_10 = (yl[1] - yl[0]) / 10 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d_10, m + d_10]) + elif yl[1] - yl[0] < small: + d_10 = (xl[1] - xl[0]) / 10 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d_10, m + d_10]) + + if square: + if xl[1] - xl[0] > yl[1] - yl[0]: + d2 = (xl[1] - xl[0]) / 2 + m = yl[0] + (yl[1] - yl[0]) / 2.0 + plt.ylim([m - d2, m + d2]) + + if xl[1] - xl[0] < yl[1] - yl[0]: + d2 = (yl[1] - yl[0]) / 2 + m = xl[0] + (xl[1] - xl[0]) / 2.0 + plt.xlim([m - d2, m + d2]) - :param ax: TODO - :param xv: TODO - :param height: TODO - :param width: TODO - :param color: TODO - :returns: TODO + +def add_scalebar_to_matplotlib_plot(axis_min_max, ax): + """Add a scalebar to matplotlib plots. + + The scalebar is of magnitude 50 by default, but if the difference between + max and min vals is less than 100, it's reduced to 5, and if the difference + is less than 10, it's reduced to 1. + + :param axis_min_max: minimum, maximum value in plot + :type axis_min_max: [float, float] + :param ax: axis to plot scalebar at + :type ax: matplotlib.axes.Axes + :returns: None """ - ax.add_patch(Rectangle(xy, width, height, edgecolor=color, facecolor=color, - fill=True)) + # add a scalebar + # ax = fig.add_axes([0, 0, 1, 1]) + sc_val = 50 + if axis_min_max[1] - axis_min_max[0] < 100: + sc_val = 5 + if axis_min_max[1] - axis_min_max[0] < 10: + sc_val = 1 + scalebar1 = ScaleBar( + 0.001, + units="mm", + dimension="si-length", + scale_loc="top", + location="lower right", + fixed_value=sc_val, + fixed_units="um", + box_alpha=0.8, + ) + ax.add_artist(scalebar1) + + +def add_line_to_matplotlib_2D_plot(ax, xv, yv, width, color, axis_min_max): + """Add a line to a matplotlib plot + + :param ax: matplotlib.axes.Axes object + :type ax: matplotlib.axes.Axes + :param xv: x values + :type xv: [float, float] + :param yv: y values + :type yv: [float, float] + :param width: width of line + :type width: float + :param color: color of line + :type color: str + :param axis_min_max: min, max value of axis + :type axis_min_max: [float, float]""" + + if ( + abs(xv[0] - xv[1]) < 0.01 and abs(yv[0] - yv[1]) < 0.01 + ): # looking at the cylinder from the top, OR a sphere, so draw a circle + xv[1] = xv[1] + width / 1000.0 + yv[1] = yv[1] + width / 1000.0 + + ax.add_line( + LineDataUnits(xv, yv, linewidth=width, solid_capstyle="round", color=color) + ) + + ax.add_line( + LineDataUnits(xv, yv, linewidth=width, solid_capstyle="butt", color=color) + ) + + axis_min_max[0] = min(axis_min_max[0], xv[0]) + axis_min_max[0] = min(axis_min_max[0], xv[1]) + axis_min_max[1] = max(axis_min_max[1], xv[0]) + axis_min_max[1] = max(axis_min_max[1], xv[1]) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index a30cce8f..1b720579 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -62,6 +62,42 @@ def test_2d_plotter_network(self): self.assertIsFile(filename) pl.Path(filename).unlink() + def test_2d_constant_plotter_network(self): + """Test plot_2D_schematic function with a network of a few cells.""" + nml_file = "tests/plot/L23-example/TestNetwork.net.nml" + ofile = pl.Path(nml_file).name + for plane in ["xy", "yz", "xz"]: + filename = f"test_morphology_plot_2d_{ofile.replace('.', '_', 100)}_{plane}_constant.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + plot_2D(nml_file, nogui=True, plane2d=plane, + save_to_file=filename, plot_type="Constant") + + self.assertIsFile(filename) + pl.Path(filename).unlink() + + def test_2d_schematic_plotter_network(self): + """Test plot_2D_schematic function with a network of a few cells.""" + nml_file = "tests/plot/L23-example/TestNetwork.net.nml" + ofile = pl.Path(nml_file).name + for plane in ["xy", "yz", "xz"]: + filename = f"test_morphology_plot_2d_{ofile.replace('.', '_', 100)}_{plane}_schematic.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + plot_2D(nml_file, nogui=True, plane2d=plane, + save_to_file=filename, plot_type="Schematic") + + self.assertIsFile(filename) + pl.Path(filename).unlink() + def test_3d_plotter(self): """Test plot_interactive_3D function.""" nml_files = ["tests/plot/Cell_497232312.cell.nml", "tests/plot/test.cell.nml"] @@ -113,11 +149,8 @@ def test_2d_schematic_plotter(self): except FileNotFoundError: pass - sgs = cell.get_segment_groups_by_substring("apic_") - sgs_1 = cell.get_segment_groups_by_substring("dend_") - sgs_ids = list(sgs.keys()) + list(sgs_1.keys()) plot_2D_schematic( - cell, segment_groups=sgs_ids, + cell, segment_groups=None, nogui=True, plane2d=plane, save_to_file=filename, labels=True ) @@ -176,7 +209,7 @@ def test_plot_segment_groups_curtain_plots_with_data(self): for sg_id in sgs_ids[0:nsgs]: lensgs = len(cell.get_all_segments_in_group(sg_id)) - data_dict[sg_id] = numpy.random.random_integers(0, 100, lensgs) + data_dict[sg_id] = numpy.random.randint(0, 101, lensgs) plot_segment_groups_curtain_plots( cell, segment_groups=sgs_ids[0:nsgs], From 29ed49c723d6341855f66e5c97477b34492f1c7a Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 14:10:55 +0000 Subject: [PATCH 20/88] chore(morph-plot): return logging level to INFO [ci-skip] --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index f6dbcee4..66ede31d 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -35,7 +35,7 @@ logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) DEFAULTS = { From 4c3f444435586f203255cdcfefeba362e4ac727f Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 14:13:46 +0000 Subject: [PATCH 21/88] chore(plot): format with black [ci-skip] --- pyneuroml/plot/PlotMorphology.py | 180 +++++++++++++++++------------ tests/plot/test_morphology_plot.py | 72 +++++++++--- 2 files changed, 158 insertions(+), 94 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 66ede31d..ba857c5d 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -24,13 +24,16 @@ from pyneuroml.pynml import read_neuroml2_file from pyneuroml.utils.cli import build_namespace from pyneuroml.utils import extract_position_info -from pyneuroml.utils.plot import (add_text_to_matplotlib_2D_plot, get_next_hex_color, - add_box_to_matplotlib_2D_plot, - get_new_matplotlib_morph_plot, - autoscale_matplotlib_plot, - add_scalebar_to_matplotlib_plot, - add_line_to_matplotlib_2D_plot) -from neuroml import (SegmentGroup, Cell) +from pyneuroml.utils.plot import ( + add_text_to_matplotlib_2D_plot, + get_next_hex_color, + add_box_to_matplotlib_2D_plot, + get_new_matplotlib_morph_plot, + autoscale_matplotlib_plot, + add_scalebar_to_matplotlib_plot, + add_line_to_matplotlib_2D_plot, +) +from neuroml import SegmentGroup, Cell from neuroml.neuro_lex_ids import neuro_lex_ids @@ -46,7 +49,7 @@ "plane2d": "xy", "minwidth": 0.8, "square": False, - "plotType": "Detailed" + "plotType": "Detailed", } @@ -143,8 +146,14 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): plot_interactive_3D(a.nml_file, a.minwidth, a.v, a.nogui, a.save_to_file) else: plot_2D( - a.nml_file, a.plane2d, a.minwidth, a.v, a.nogui, a.save_to_file, - a.square, a.plot_type + a.nml_file, + a.plane2d, + a.minwidth, + a.v, + a.nogui, + a.save_to_file, + a.square, + a.plot_type, ) @@ -157,7 +166,7 @@ def plot_2D( save_to_file: typing.Optional[str] = None, square: bool = False, plot_type: str = "Detailed", - title: typing.Optional[str] = None + title: typing.Optional[str] = None, ): """Plot cell morphologies in 2D. @@ -193,7 +202,9 @@ def plot_2D( """ if plot_type not in ["Detailed", "Constant", "Schematic"]: - raise ValueError("plot_type must be one of 'Detailed', 'Constant', or 'Schematic'") + raise ValueError( + "plot_type must be one of 'Detailed', 'Constant', or 'Schematic'" + ) if verbose: print("Plotting %s" % nml_file) @@ -237,20 +248,39 @@ def plot_2D( color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None if plot_type == "Schematic": - plot_2D_schematic(offset=pos, cell=cell, segment_groups=None, labels=True, - plane2d=plane2d, min_width=min_width, - verbose=verbose, fig=fig, ax=ax, scalebar=False, - nogui=True, autoscale=False, square=False) + plot_2D_schematic( + offset=pos, + cell=cell, + segment_groups=None, + labels=True, + plane2d=plane2d, + min_width=min_width, + verbose=verbose, + fig=fig, + ax=ax, + scalebar=False, + nogui=True, + autoscale=False, + square=False, + ) else: - plot_2D_cell_morphology(offset=pos, cell=cell, plane2d=plane2d, - color=color, soma_radius=radius, - plot_type=plot_type, verbose=verbose, - fig=fig, - ax=ax, min_width=min_width, - axis_min_max=axis_min_max, - scalebar=False, - nogui=True, autoscale=False, - square=False) + plot_2D_cell_morphology( + offset=pos, + cell=cell, + plane2d=plane2d, + color=color, + soma_radius=radius, + plot_type=plot_type, + verbose=verbose, + fig=fig, + ax=ax, + min_width=min_width, + axis_min_max=axis_min_max, + scalebar=False, + nogui=True, + autoscale=False, + square=False, + ) add_scalebar_to_matplotlib_plot(axis_min_max, ax) autoscale_matplotlib_plot(verbose, square) @@ -272,7 +302,7 @@ def plot_interactive_3D( verbose: bool = False, nogui: bool = False, save_to_file: typing.Optional[str] = None, - plot_type: str = "Detailed" + plot_type: str = "Detailed", ): """Plot NeuroML2 cell morphology interactively using Plot.ly @@ -303,7 +333,9 @@ def plot_interactive_3D( :type plot_type: str """ if plot_type not in ["Detailed", "Constant", "Schematic"]: - raise ValueError("plot_type must be one of 'Detailed', 'Constant', or 'Schematic'") + raise ValueError( + "plot_type must be one of 'Detailed', 'Constant', or 'Schematic'" + ) nml_model = read_neuroml2_file(nml_file) @@ -381,12 +413,12 @@ def plot_2D_cell_morphology( cell: Cell = None, plane2d: str = "xy", color: typing.Optional[str] = None, - soma_radius: float = 0., + soma_radius: float = 0.0, title: str = "", verbose: bool = False, fig: matplotlib.figure.Figure = None, ax: matplotlib.axes.Axes = None, - min_width: float = DEFAULTS['minwidth'], + min_width: float = DEFAULTS["minwidth"], axis_min_max: typing.List = [float("inf"), -1 * float("inf")], scalebar: bool = False, nogui: bool = True, @@ -458,7 +490,6 @@ def plot_2D_cell_morphology( # random default color cell_color = get_next_hex_color() if cell is None: - if soma_radius is None: soma_radius = 10 @@ -472,7 +503,7 @@ def plot_2D_cell_morphology( [offset[1], offset[1]], soma_radius, cell_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "yx": add_line_to_matplotlib_2D_plot( @@ -481,7 +512,7 @@ def plot_2D_cell_morphology( [offset[0], offset[0]], soma_radius, cell_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "xz": add_line_to_matplotlib_2D_plot( @@ -490,7 +521,7 @@ def plot_2D_cell_morphology( [offset[2], offset[2]], soma_radius, cell_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "zx": add_line_to_matplotlib_2D_plot( @@ -499,7 +530,7 @@ def plot_2D_cell_morphology( [offset[0], offset[0]], soma_radius, cell_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "yz": add_line_to_matplotlib_2D_plot( @@ -508,7 +539,7 @@ def plot_2D_cell_morphology( [offset[2], offset[2]], soma_radius, cell_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "zy": add_line_to_matplotlib_2D_plot( @@ -517,13 +548,12 @@ def plot_2D_cell_morphology( [offset[1], offset[1]], soma_radius, cell_color if color is None else color, - axis_min_max + axis_min_max, ) else: raise Exception(f"Invalid value for plane: {plane2d}") else: - for seg in cell.morphology.segments: p = cell.get_actual_proximal(seg.id) d = seg.distal @@ -542,10 +572,7 @@ def plot_2D_cell_morphology( seg_color = "r" spherical = ( - p.x == d.x - and p.y == d.y - and p.z == d.z - and p.diameter == d.diameter + p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter ) if verbose: @@ -570,7 +597,7 @@ def plot_2D_cell_morphology( [offset[1] + p.y, offset[1] + d.y], width, seg_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "yx": add_line_to_matplotlib_2D_plot( @@ -579,7 +606,7 @@ def plot_2D_cell_morphology( [offset[0] + p.x, offset[0] + d.x], width, seg_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "xz": add_line_to_matplotlib_2D_plot( @@ -588,7 +615,7 @@ def plot_2D_cell_morphology( [offset[2] + p.z, offset[2] + d.z], width, seg_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "zx": add_line_to_matplotlib_2D_plot( @@ -597,7 +624,7 @@ def plot_2D_cell_morphology( [offset[0] + p.x, offset[0] + d.x], width, seg_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "yz": add_line_to_matplotlib_2D_plot( @@ -606,7 +633,7 @@ def plot_2D_cell_morphology( [offset[2] + p.z, offset[2] + d.z], width, seg_color if color is None else color, - axis_min_max + axis_min_max, ) elif plane2d == "zy": add_line_to_matplotlib_2D_plot( @@ -615,7 +642,7 @@ def plot_2D_cell_morphology( [offset[1] + p.y, offset[1] + d.y], width, seg_color if color is None else color, - axis_min_max + axis_min_max, ) else: raise Exception(f"Invalid value for plane: {plane2d}") @@ -657,7 +684,7 @@ def plot_2D_schematic( autoscale: bool = True, fig: matplotlib.figure.Figure = None, ax: matplotlib.axes.Axes = None, - title: str = "" + title: str = "", ) -> None: """Plot a 2D schematic of the provided segment groups. @@ -747,14 +774,15 @@ def plot_2D_schematic( width = 1 for sgid, segs in ord_segs.items(): - sgobj = cell.get_segment_group(sgid) if sgobj.neuro_lex_id != neuro_lex_ids["section"]: - raise ValueError(f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment") + raise ValueError( + f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment" + ) # get proximal and distal points - first_seg = (segs[0]) # type: Segment - last_seg = (segs[-1]) # type: Segment + first_seg = segs[0] # type: Segment + last_seg = segs[-1] # type: Segment # unique color for each segment group color = get_next_hex_color() @@ -766,7 +794,7 @@ def plot_2D_schematic( [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], width, color, - axis_min_max + axis_min_max, ) if labels: add_text_to_matplotlib_2D_plot( @@ -774,7 +802,7 @@ def plot_2D_schematic( [offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], color=color, - text=sgid + text=sgid, ) elif plane2d == "yx": @@ -784,7 +812,7 @@ def plot_2D_schematic( [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], width, color, - axis_min_max + axis_min_max, ) if labels: add_text_to_matplotlib_2D_plot( @@ -792,7 +820,7 @@ def plot_2D_schematic( [offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], color=color, - text=sgid + text=sgid, ) elif plane2d == "xz": add_line_to_matplotlib_2D_plot( @@ -801,7 +829,7 @@ def plot_2D_schematic( [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], width, color, - axis_min_max + axis_min_max, ) if labels: add_text_to_matplotlib_2D_plot( @@ -809,7 +837,7 @@ def plot_2D_schematic( [offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], color=color, - text=sgid + text=sgid, ) elif plane2d == "zx": add_line_to_matplotlib_2D_plot( @@ -818,7 +846,7 @@ def plot_2D_schematic( [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], width, color, - axis_min_max + axis_min_max, ) if labels: add_text_to_matplotlib_2D_plot( @@ -826,7 +854,7 @@ def plot_2D_schematic( [offset[0] + first_seg.proximal.z, offset[0] + last_seg.distal.z], [offset[1] + first_seg.proximal.x, offset[1] + last_seg.distal.x], color=color, - text=sgid + text=sgid, ) elif plane2d == "yz": add_line_to_matplotlib_2D_plot( @@ -835,7 +863,7 @@ def plot_2D_schematic( [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], width, color, - axis_min_max + axis_min_max, ) if labels: add_text_to_matplotlib_2D_plot( @@ -843,7 +871,7 @@ def plot_2D_schematic( [offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], [offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], color=color, - text=sgid + text=sgid, ) elif plane2d == "zy": add_line_to_matplotlib_2D_plot( @@ -852,7 +880,7 @@ def plot_2D_schematic( [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], width, color, - axis_min_max + axis_min_max, ) if labels: add_text_to_matplotlib_2D_plot( @@ -860,7 +888,7 @@ def plot_2D_schematic( [offset[0] + first_seg.proximal.z, offset[0] + last_seg.distal.z], [offset[1] + first_seg.proximal.y, offset[1] + last_seg.distal.y], color=color, - text=sgid + text=sgid, ) else: raise Exception(f"Invalid value for plane: {plane2d}") @@ -897,7 +925,7 @@ def plot_segment_groups_curtain_plots( overlay_data: typing.Dict[str, typing.List[typing.Any]] = None, overlay_data_label: str = "", width: typing.Union[float, int] = 4, - colormap_name: str = 'viridis' + colormap_name: str = "viridis", ) -> None: """Plot curtain plots of provided segment groups. @@ -956,10 +984,8 @@ def plot_segment_groups_curtain_plots( acolormap = None norm = None if overlay_data: - if (set(overlay_data.keys()) != set(ord_segs.keys())): - raise ValueError( - "Keys of overlay_data and ord_segs must match." - ) + if set(overlay_data.keys()) != set(ord_segs.keys()): + raise ValueError("Keys of overlay_data and ord_segs must match.") for key in overlay_data.keys(): if len(overlay_data[key]) != len(ord_segs[key]): raise ValueError( @@ -977,8 +1003,10 @@ def plot_segment_groups_curtain_plots( acolormap = matplotlib.colormaps[colormap_name] norm = matplotlib.colors.Normalize(vmin=data_min, vmax=data_max) - fig.colorbar(matplotlib.cm.ScalarMappable(norm=norm, cmap=acolormap), - label=overlay_data_label) + fig.colorbar( + matplotlib.cm.ScalarMappable(norm=norm, cmap=acolormap), + label=overlay_data_label, + ) ax.spines["right"].set_visible(False) ax.spines["bottom"].set_visible(False) @@ -999,7 +1027,9 @@ def plot_segment_groups_curtain_plots( sgobj = cell.get_segment_group(sgid) if sgobj.neuro_lex_id != neuro_lex_ids["section"]: - raise ValueError(f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment") + raise ValueError( + f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment" + ) for seg_num in range(0, len(segs)): seg = segs[seg_num] @@ -1016,8 +1046,8 @@ def plot_segment_groups_curtain_plots( ax, [column * width - width * 0.10, -1 * length], height=cumulative_len, - width=width * .8, - color=color + width=width * 0.8, + color=color, ) length += cumulative_len @@ -1029,7 +1059,7 @@ def plot_segment_groups_curtain_plots( color="black", text=sgid, vertical="bottom", - horizontal="center" + horizontal="center", ) plt.autoscale() diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 1b720579..5a73db67 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -13,9 +13,12 @@ import numpy import neuroml -from pyneuroml.plot.PlotMorphology import (plot_2D, plot_interactive_3D, - plot_2D_schematic, - plot_segment_groups_curtain_plots) +from pyneuroml.plot.PlotMorphology import ( + plot_2D, + plot_interactive_3D, + plot_2D_schematic, + plot_segment_groups_curtain_plots, +) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -50,7 +53,9 @@ def test_2d_plotter_network(self): nml_file = "tests/plot/L23-example/TestNetwork.net.nml" ofile = pl.Path(nml_file).name for plane in ["xy", "yz", "xz"]: - filename = f"test_morphology_plot_2d_{ofile.replace('.', '_', 100)}_{plane}.png" + filename = ( + f"test_morphology_plot_2d_{ofile.replace('.', '_', 100)}_{plane}.png" + ) # remove the file first try: pl.Path(filename).unlink() @@ -74,8 +79,13 @@ def test_2d_constant_plotter_network(self): except FileNotFoundError: pass - plot_2D(nml_file, nogui=True, plane2d=plane, - save_to_file=filename, plot_type="Constant") + plot_2D( + nml_file, + nogui=True, + plane2d=plane, + save_to_file=filename, + plot_type="Constant", + ) self.assertIsFile(filename) pl.Path(filename).unlink() @@ -92,8 +102,13 @@ def test_2d_schematic_plotter_network(self): except FileNotFoundError: pass - plot_2D(nml_file, nogui=True, plane2d=plane, - save_to_file=filename, plot_type="Schematic") + plot_2D( + nml_file, + nogui=True, + plane2d=plane, + save_to_file=filename, + plot_type="Schematic", + ) self.assertIsFile(filename) pl.Path(filename).unlink() @@ -130,19 +145,26 @@ def test_2d_schematic_plotter(self): for plane in ["xy", "yz", "xz"]: # olm cell - filename = f"test_schematic_plot_2d_{olm_ofile.replace('.', '_', 100)}_{plane}.png" + filename = ( + f"test_schematic_plot_2d_{olm_ofile.replace('.', '_', 100)}_{plane}.png" + ) try: pl.Path(filename).unlink() except FileNotFoundError: pass plot_2D_schematic( - olm_cell, segment_groups=["soma_0", "dendrite_0", "axon_0"], - nogui=True, plane2d=plane, save_to_file=filename + olm_cell, + segment_groups=["soma_0", "dendrite_0", "axon_0"], + nogui=True, + plane2d=plane, + save_to_file=filename, ) # more complex cell - filename = f"test_schematic_plot_2d_{ofile.replace('.', '_', 100)}_{plane}.png" + filename = ( + f"test_schematic_plot_2d_{ofile.replace('.', '_', 100)}_{plane}.png" + ) # remove the file first try: pl.Path(filename).unlink() @@ -150,8 +172,12 @@ def test_2d_schematic_plotter(self): pass plot_2D_schematic( - cell, segment_groups=None, - nogui=True, plane2d=plane, save_to_file=filename, labels=True + cell, + segment_groups=None, + nogui=True, + plane2d=plane, + save_to_file=filename, + labels=True, ) self.assertIsFile(filename) @@ -177,8 +203,11 @@ def test_plot_segment_groups_curtain_plots(self): # sgs_1 = cell.get_segment_groups_by_substring("dend_") sgs_ids = list(sgs.keys()) # + list(sgs_1.keys()) plot_segment_groups_curtain_plots( - cell, segment_groups=sgs_ids[0:50], - nogui=True, save_to_file=filename, labels=True + cell, + segment_groups=sgs_ids[0:50], + nogui=True, + save_to_file=filename, + labels=True, ) self.assertIsFile(filename) @@ -212,9 +241,14 @@ def test_plot_segment_groups_curtain_plots_with_data(self): data_dict[sg_id] = numpy.random.randint(0, 101, lensgs) plot_segment_groups_curtain_plots( - cell, segment_groups=sgs_ids[0:nsgs], - nogui=True, save_to_file=filename, labels=True, - overlay_data=data_dict, overlay_data_label="Random values (0, 100)", width=4 + cell, + segment_groups=sgs_ids[0:nsgs], + nogui=True, + save_to_file=filename, + labels=True, + overlay_data=data_dict, + overlay_data_label="Random values (0, 100)", + width=4, ) self.assertIsFile(filename) From 8a9d6b1a3a098a7c7a8e311d9afa7ed375dcfb5f Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 14:48:21 +0000 Subject: [PATCH 22/88] chore(plotting): change default argument value for consistency [ci skip] --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index ba857c5d..49719c85 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -423,7 +423,7 @@ def plot_2D_cell_morphology( scalebar: bool = False, nogui: bool = True, autoscale: bool = True, - square: bool = True, + square: bool = False, plot_type: str = "Detailed", save_to_file: typing.Optional[str] = None, ): From 31c38b2e8aa40ba4687c2e515d4a646f096c815e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 15:16:09 +0000 Subject: [PATCH 23/88] feat(curtain-plots): allow setting title --- pyneuroml/plot/PlotMorphology.py | 26 +++++++++++++++----------- pyneuroml/utils/plot.py | 13 +++++++++++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 49719c85..e6cb225b 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -926,6 +926,7 @@ def plot_segment_groups_curtain_plots( overlay_data_label: str = "", width: typing.Union[float, int] = 4, colormap_name: str = "viridis", + title: str = "SegmentGroup", ) -> None: """Plot curtain plots of provided segment groups. @@ -958,6 +959,8 @@ def plot_segment_groups_curtain_plots( https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.colormaps Note: random colours are used for each segment if no data is to be overlaid :type colormap_name: str + :param title: plot title, displayed at bottom + :type title: str :returns: None :raises ValueError: if keys in `overlay_data` do not match @@ -969,7 +972,6 @@ def plot_segment_groups_curtain_plots( myrandom = random.Random() myrandom.seed(122436) - title = f"Curtain plots of segment groups from {cell.id}" (ord_segs, cumulative_lengths) = cell.get_ordered_segments_in_groups( segment_groups, check_parentage=False, include_cumulative_lengths=True ) @@ -1014,7 +1016,7 @@ def plot_segment_groups_curtain_plots( ax.xaxis.set_ticks_position("none") ax.xaxis.set_ticks([]) - ax.set_xlabel("Segment group") + ax.set_xlabel(title) ax.set_ylabel("length (μm)") # column counter @@ -1052,15 +1054,17 @@ def plot_segment_groups_curtain_plots( length += cumulative_len - add_text_to_matplotlib_2D_plot( - ax, - [column * width + width / 2, column * width + width / 2], - [50, 100], - color="black", - text=sgid, - vertical="bottom", - horizontal="center", - ) + if labels: + add_text_to_matplotlib_2D_plot( + ax, + [column * width + width / 2, column * width + width / 2], + [50, 100], + color="black", + text=sgid, + vertical="bottom", + horizontal="center", + clip_on=False, + ) plt.autoscale() xl = plt.xlim() diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 42717eda..64284140 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -24,7 +24,14 @@ def add_text_to_matplotlib_2D_plot( - ax: Axes, xv, yv, color, text: str, horizontal="center", vertical="bottom" + ax: matplotlib.axes.Axes, + xv: typing.List[float], + yv: typing.List[float], + color: str, + text: str, + horizontal: str = "center", + vertical: str = "bottom", + clip_on: bool = True, ): """Add text to a matplotlib plot between two points @@ -42,6 +49,8 @@ def add_text_to_matplotlib_2D_plot( :type color: str :param text: text to write :type text: str + :param clip_on: toggle clip_on (if False, text will also be shown outside plot) + :type clip_on: bool """ angle = int(numpy.rad2deg(numpy.arctan2((yv[1] - yv[0]), (xv[1] - xv[0])))) @@ -59,7 +68,7 @@ def add_text_to_matplotlib_2D_plot( verticalalignment=vertical, rotation_mode="default", rotation=angle, - clip_on=True, + clip_on=clip_on, ) From bbd6baf6ffa2d00a6bb7caf8ffc27417e7337efd Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 22:31:30 +0000 Subject: [PATCH 24/88] feat(curtain-plot): allow setting explicit min and max limits --- pyneuroml/plot/PlotMorphology.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index e6cb225b..9b77db82 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -927,6 +927,8 @@ def plot_segment_groups_curtain_plots( width: typing.Union[float, int] = 4, colormap_name: str = "viridis", title: str = "SegmentGroup", + datamin: typing.Optional[float] = None, + datamax: typing.Optional[float] = None ) -> None: """Plot curtain plots of provided segment groups. @@ -961,6 +963,10 @@ def plot_segment_groups_curtain_plots( :type colormap_name: str :param title: plot title, displayed at bottom :type title: str + :param datamin: min limits of data (useful to compare different plots) + :type datamin: float + :param datamax: max limits of data (useful to compare different plots) + :type datamax: float :returns: None :raises ValueError: if keys in `overlay_data` do not match From 5875fe98cfbe37dc41453aff47827d8819c52cdf Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 22:31:47 +0000 Subject: [PATCH 25/88] feat(curtain-plot): improve error text --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 9b77db82..692b3ef9 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -993,7 +993,7 @@ def plot_segment_groups_curtain_plots( norm = None if overlay_data: if set(overlay_data.keys()) != set(ord_segs.keys()): - raise ValueError("Keys of overlay_data and ord_segs must match.") + raise ValueError(f"Keys of overlay_data ({overlay_data.keys()}) and ord_segs ({ord_segs.keys()})must match.") for key in overlay_data.keys(): if len(overlay_data[key]) != len(ord_segs[key]): raise ValueError( From b7491a87d65905733f7227c02a701317e106ce1e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Feb 2023 22:34:36 +0000 Subject: [PATCH 26/88] feat(curtain-plot): complete min/max implementation --- pyneuroml/plot/PlotMorphology.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 692b3ef9..fdab27ed 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1009,6 +1009,11 @@ def plot_segment_groups_curtain_plots( if this_min < data_min: data_min = this_min + if datamin is not None: + data_min = datamin + if datamax is not None: + data_max = datamax + acolormap = matplotlib.colormaps[colormap_name] norm = matplotlib.colors.Normalize(vmin=data_min, vmax=data_max) fig.colorbar( From 29ff2efbf29c4a7113fddd80a58c21e1e29c868d Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 16 Feb 2023 08:33:21 +0000 Subject: [PATCH 27/88] feat(curtain-plots): close plots if not to be shown Otherwise plots remain open and consume memory. matplotlib warns: ``` .../pyneuroml/plot/PlotMorphology.py:986: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). ``` --- pyneuroml/plot/PlotMorphology.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index fdab27ed..a52d9b9a 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1093,6 +1093,7 @@ def plot_segment_groups_curtain_plots( if not nogui: plt.show() + else: plt.close() From 917f594f50de8be68254b87b928fe0700aff81de Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 16 Feb 2023 10:25:14 +0000 Subject: [PATCH 28/88] feat(plotting): add explicit option to close `plt` plot --- pyneuroml/plot/Plot.py | 14 ++++++++++--- pyneuroml/plot/PlotMorphology.py | 36 +++++++++++++++++--------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/pyneuroml/plot/Plot.py b/pyneuroml/plot/Plot.py index 15bfd8ae..a8b8f480 100644 --- a/pyneuroml/plot/Plot.py +++ b/pyneuroml/plot/Plot.py @@ -45,7 +45,8 @@ def generate_plot( save_figure_to: typing.Optional[str] = None, title_above_plot: bool = False, verbose: bool = False, -) -> matplotlib.axes.Axes: + close_plot: bool = False, +) -> typing.Optional[matplotlib.axes.Axes]: """Utility function to generate plots using the Matplotlib library. This function can be used to generate graphs with multiple plot lines. @@ -127,7 +128,9 @@ def generate_plot( :type title_above_plot: boolean :param verbose: enable/disable verbose logging (default: False) :type verbose: boolean - :returns: matplotlib.axes.Axes object + :param close_plot: call pyplot.close() to close plot after plotting + :type close_plot: bool + :returns: matplotlib.axes.Axes object if plot is not closed, else None """ logger.info("Generating plot: %s" % (title)) @@ -239,7 +242,12 @@ def generate_plot( if show_plot_already: plt.show() - return ax + if close_plot: + plt.close() + else: + return ax + + return None def generate_interactive_plot( diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index a52d9b9a..96416f6b 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -167,6 +167,7 @@ def plot_2D( square: bool = False, plot_type: str = "Detailed", title: typing.Optional[str] = None, + close_plot: bool = False ): """Plot cell morphologies in 2D. @@ -199,6 +200,8 @@ def plot_2D( :type plot_type: str :param title: title of plot :type title: str + :param close_plot: call pyplot.close() to close plot after plotting + :type close_plot: bool """ if plot_type not in ["Detailed", "Constant", "Schematic"]: @@ -292,7 +295,7 @@ def plot_2D( if not nogui: plt.show() - else: + if close_plot: plt.close() @@ -426,6 +429,7 @@ def plot_2D_cell_morphology( square: bool = False, plot_type: str = "Detailed", save_to_file: typing.Optional[str] = None, + close_plot: bool = True ): """Plot the detailed 2D morphology of a cell in provided plane @@ -462,6 +466,8 @@ def plot_2D_cell_morphology( :type autoscale: bool :param scalebar: toggle scalebar :type scalebar: bool + :param close_plot: call pyplot.close() to close plot after plotting + :type close_plot: bool :raises: ValueError if `cell` is None @@ -482,10 +488,8 @@ def plot_2D_cell_morphology( except Exception: axon_segs = [] - new_fig = False if fig is None: fig, ax = get_new_matplotlib_morph_plot(title) - new_fig = True # random default color cell_color = get_next_hex_color() @@ -662,11 +666,8 @@ def plot_2D_cell_morphology( if not nogui: plt.show() - else: - # only close if a new fig was created - # if a figure was passed in, the caller needs to close it. - if new_fig: - plt.close() + if close_plot: + plt.close() def plot_2D_schematic( @@ -685,6 +686,7 @@ def plot_2D_schematic( fig: matplotlib.figure.Figure = None, ax: matplotlib.axes.Axes = None, title: str = "", + close_plot: bool = False, ) -> None: """Plot a 2D schematic of the provided segment groups. @@ -724,6 +726,8 @@ def plot_2D_schematic( :type autoscale: bool :param scalebar: toggle scalebar :type scalebar: bool + :param close_plot: call pyplot.close() to close plot after plotting + :type close_plot: bool """ if title == "": @@ -740,11 +744,9 @@ def plot_2D_schematic( segment_groups, check_parentage=False ) - new_fig = False if fig is None: logger.debug("No figure provided, creating new fig and ax") fig, ax = get_new_matplotlib_morph_plot(title, plane2d) - new_fig = True if plane2d == "xy": ax.set_xlabel("x (μm)") @@ -908,11 +910,8 @@ def plot_2D_schematic( if not nogui: plt.show() - # only close if a new fig was created - # if a figure was passed in, the caller needs to close it. - else: - if new_fig: - plt.close() + if close_plot: + plt.close() def plot_segment_groups_curtain_plots( @@ -928,7 +927,8 @@ def plot_segment_groups_curtain_plots( colormap_name: str = "viridis", title: str = "SegmentGroup", datamin: typing.Optional[float] = None, - datamax: typing.Optional[float] = None + datamax: typing.Optional[float] = None, + close_plot: bool = False ) -> None: """Plot curtain plots of provided segment groups. @@ -967,6 +967,8 @@ def plot_segment_groups_curtain_plots( :type datamin: float :param datamax: max limits of data (useful to compare different plots) :type datamax: float + :param close_plot: call pyplot.close() to close plot after plotting + :type close_plot: bool :returns: None :raises ValueError: if keys in `overlay_data` do not match @@ -1093,7 +1095,7 @@ def plot_segment_groups_curtain_plots( if not nogui: plt.show() - else: + if close_plot: plt.close() From a91a575adf16b8dd8d24685c4ba9fee6e85bf5c4 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 16 Feb 2023 12:39:02 +0000 Subject: [PATCH 29/88] fix(plotting): do not close plot by default and add info to inform user that plots are being closed --- pyneuroml/plot/Plot.py | 1 + pyneuroml/plot/PlotMorphology.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyneuroml/plot/Plot.py b/pyneuroml/plot/Plot.py index a8b8f480..0e84a88f 100644 --- a/pyneuroml/plot/Plot.py +++ b/pyneuroml/plot/Plot.py @@ -243,6 +243,7 @@ def generate_plot( plt.show() if close_plot: + logger.info("Closing plot") plt.close() else: return ax diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 96416f6b..dff9af98 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -296,6 +296,7 @@ def plot_2D( if not nogui: plt.show() if close_plot: + logger.info("Closing plot") plt.close() @@ -429,7 +430,7 @@ def plot_2D_cell_morphology( square: bool = False, plot_type: str = "Detailed", save_to_file: typing.Optional[str] = None, - close_plot: bool = True + close_plot: bool = False ): """Plot the detailed 2D morphology of a cell in provided plane @@ -667,6 +668,7 @@ def plot_2D_cell_morphology( if not nogui: plt.show() if close_plot: + logger.info("closing plot") plt.close() @@ -911,6 +913,7 @@ def plot_2D_schematic( if not nogui: plt.show() if close_plot: + logger.info("closing plot") plt.close() @@ -1096,6 +1099,7 @@ def plot_segment_groups_curtain_plots( if not nogui: plt.show() if close_plot: + logger.info("Closing plot") plt.close() From c75f0320bc0208089ccd2a6322e14251d3edd9da Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 16 Feb 2023 17:17:08 +0000 Subject: [PATCH 30/88] feat(morph-plot): extend to allow overlaying data on plot --- pyneuroml/plot/PlotMorphology.py | 65 +++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index dff9af98..3ac25c80 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -430,7 +430,12 @@ def plot_2D_cell_morphology( square: bool = False, plot_type: str = "Detailed", save_to_file: typing.Optional[str] = None, - close_plot: bool = False + close_plot: bool = False, + overlay_data: typing.Dict[int, float] = None, + overlay_data_label: typing.Optional[str] = None, + datamin: typing.Optional[float] = None, + datamax: typing.Optional[float] = None, + colormap_name: str = 'viridis' ): """Plot the detailed 2D morphology of a cell in provided plane @@ -469,6 +474,22 @@ def plot_2D_cell_morphology( :type scalebar: bool :param close_plot: call pyplot.close() to close plot after plotting :type close_plot: bool + :param overlay_data: data to overlay over the morphology + this must be a dictionary with segment ids as keys, the single value to + overlay as values + :type overlay_data: dict, keys are segment ids, values are magnitudes to + overlay on curtain plots + :param overlay_data_label: label of data being overlaid + :type overlay_data_label: str + :param colormap_name: name of matplotlib colourmap to use for data overlay + See: + https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.colormaps + Note: random colours are used for each segment if no data is to be overlaid + :type colormap_name: str + :param datamin: min limits of data (useful to compare different plots) + :type datamin: float + :param datamax: max limits of data (useful to compare different plots) + :type datamax: float :raises: ValueError if `cell` is None @@ -492,6 +513,31 @@ def plot_2D_cell_morphology( if fig is None: fig, ax = get_new_matplotlib_morph_plot(title) + # overlaying data + data_max = -1 * float("inf") + data_min = float("inf") + acolormap = None + norm = None + if overlay_data: + this_max = numpy.max(list(overlay_data.values())) + this_min = numpy.min(list(overlay_data.values())) + if this_max > data_max: + data_max = this_max + if this_min < data_min: + data_min = this_min + + if datamin is not None: + data_min = datamin + if datamax is not None: + data_max = datamax + + acolormap = matplotlib.colormaps[colormap_name] + norm = matplotlib.colors.Normalize(vmin=data_min, vmax=data_max) + fig.colorbar( + matplotlib.cm.ScalarMappable(norm=norm, cmap=acolormap), + label=overlay_data_label, + ) + # random default color cell_color = get_next_hex_color() if cell is None: @@ -570,12 +616,19 @@ def plot_2D_cell_morphology( if plot_type == "Constant": width = min_width - seg_color = "b" - if seg.id in soma_segs: - seg_color = "g" - elif seg.id in axon_segs: - seg_color = "r" + if overlay_data and acolormap and norm: + try: + seg_color = acolormap(norm(overlay_data[seg.id])) + except KeyError: + seg_color = "black" + else: + seg_color = "b" + if seg.id in soma_segs: + seg_color = "g" + elif seg.id in axon_segs: + seg_color = "r" + logger.debug(f"color is {color}") spherical = ( p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter ) From 705b58e6e1cc5d40aa7e61d55ae58ecdb9eed22e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 16 Feb 2023 17:17:22 +0000 Subject: [PATCH 31/88] test(morph-plot): add test for morph plotter with data overlaid --- tests/plot/test_morphology_plot.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 5a73db67..a58689f0 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -15,6 +15,7 @@ import neuroml from pyneuroml.plot.PlotMorphology import ( plot_2D, + plot_2D_cell_morphology, plot_interactive_3D, plot_2D_schematic, plot_segment_groups_curtain_plots, @@ -48,6 +49,33 @@ def test_2d_plotter(self): self.assertIsFile(filename) pl.Path(filename).unlink() + def test_2d_morphology_plotter_data_overlay(self): + """Test plot_2D_cell_morphology method with data.""" + nml_files = ["tests/plot/Cell_497232312.cell.nml"] + for nml_file in nml_files: + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + ofile = pl.Path(nml_file).name + plane = "xy" + filename = f"test_morphology_plot_2d_{ofile.replace('.', '_', 100)}_{plane}_with_data.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + segs = cell.get_all_segments_in_group("all") + values = (list(numpy.random.randint(50, 101, 1800)) + list(numpy.random.randint(0, 51, len(segs) - 1800))) + data_dict = dict(zip(segs, values)) + + plot_2D_cell_morphology(cell=cell, nogui=False, plane2d=plane, + save_to_file=filename, + overlay_data=data_dict, + overlay_data_label="Test") + + self.assertIsFile(filename) + pl.Path(filename).unlink() + def test_2d_plotter_network(self): """Test plot_2D function with a network of a few cells.""" nml_file = "tests/plot/L23-example/TestNetwork.net.nml" From 58655dfa0262a5f5638e70fcf898ed2d2a1900d1 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 17 Feb 2023 15:59:56 +0000 Subject: [PATCH 32/88] refactor(plot): split out point neuron plotting --- pyneuroml/plot/PlotMorphology.py | 417 ++++++++++++++++++------------- 1 file changed, 249 insertions(+), 168 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 3ac25c80..85ea2279 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -169,12 +169,14 @@ def plot_2D( title: typing.Optional[str] = None, close_plot: bool = False ): - """Plot cell morphologies in 2D. + """Plot cells in a 2D plane. If a file with a network containing multiple cells is provided, it will - plot all the cells. + plot all the cells. For detailed neuroml.Cell* types, it will plot their + complete morphology. For point neurons, we only plot the points (locations) + where they are. - This uses matplotlib to plot the morphology in 2D. + This method uses matplotlib. :param nml_file: path to NeuroML cell file :type nml_file: str @@ -192,11 +194,15 @@ def plot_2D( :param square: scale axes so that image is approximately square :type square: bool :param plot_type: type of plot, one of: - - Detailed: show detailed morphology taking into account each segment's + + - "Detailed": show detailed morphology taking into account each segment's width - - Constant: show morphology, but use constant line widths - - Schematic: only plot each unbranched segment group as a straight + - "Constant": show morphology, but use constant line widths + - "Schematic": only plot each unbranched segment group as a straight line, not following each segment + + This is only applicable for neuroml.Cell cells (ones with some + morphology) :type plot_type: str :param title: title of plot :type title: str @@ -250,40 +256,50 @@ def plot_2D( radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None - if plot_type == "Schematic": - plot_2D_schematic( - offset=pos, - cell=cell, - segment_groups=None, - labels=True, - plane2d=plane2d, - min_width=min_width, - verbose=verbose, - fig=fig, - ax=ax, - scalebar=False, - nogui=True, - autoscale=False, - square=False, - ) + if cell is None: + plot_2D_point_cells(offset=pos, plane2d=plane2d, + color=color, + soma_radius=radius, + verbose=verbose, + ax=ax, + fig=fig, + autoscale=False, + scalebar=False, + nogui=True,) else: - plot_2D_cell_morphology( - offset=pos, - cell=cell, - plane2d=plane2d, - color=color, - soma_radius=radius, - plot_type=plot_type, - verbose=verbose, - fig=fig, - ax=ax, - min_width=min_width, - axis_min_max=axis_min_max, - scalebar=False, - nogui=True, - autoscale=False, - square=False, - ) + if plot_type == "Schematic": + plot_2D_schematic( + offset=pos, + cell=cell, + segment_groups=None, + labels=True, + plane2d=plane2d, + min_width=min_width, + verbose=verbose, + fig=fig, + ax=ax, + scalebar=False, + nogui=True, + autoscale=False, + square=False, + ) + else: + plot_2D_cell_morphology( + offset=pos, + cell=cell, + plane2d=plane2d, + color=color, + plot_type=plot_type, + verbose=verbose, + fig=fig, + ax=ax, + min_width=min_width, + axis_min_max=axis_min_max, + scalebar=False, + nogui=True, + autoscale=False, + square=False, + ) add_scalebar_to_matplotlib_plot(axis_min_max, ax) autoscale_matplotlib_plot(verbose, square) @@ -417,7 +433,6 @@ def plot_2D_cell_morphology( cell: Cell = None, plane2d: str = "xy", color: typing.Optional[str] = None, - soma_radius: float = 0.0, title: str = "", verbose: bool = False, fig: matplotlib.figure.Figure = None, @@ -447,8 +462,6 @@ def plot_2D_cell_morphology( :type plane2d: str :param color: color to use for all segments :type color: str - :param soma_radius: radius of soma (uses min_width if provided) - :type soma_radius: float :param fig: a matplotlib.figure.Figure object to use :type fig: matplotlib.figure.Figure :param ax: a matplotlib.axes.Axes object to use @@ -495,7 +508,7 @@ def plot_2D_cell_morphology( """ if cell is None: - raise ValueError("No cell provided") + raise ValueError("No cell provided. If you would like to plot a network of point neurons, consider using `plot_2D_point_cells` instead") try: soma_segs = cell.get_all_segments_in_group("soma_group") @@ -539,174 +552,242 @@ def plot_2D_cell_morphology( ) # random default color - cell_color = get_next_hex_color() - if cell is None: - if soma_radius is None: - soma_radius = 10 + for seg in cell.morphology.segments: + p = cell.get_actual_proximal(seg.id) + d = seg.distal + width = (p.diameter + d.diameter) / 2 + + if width < min_width: + width = min_width if plot_type == "Constant": - soma_radius = min_width + width = min_width + + if overlay_data and acolormap and norm: + try: + seg_color = acolormap(norm(overlay_data[seg.id])) + except KeyError: + seg_color = "black" + else: + seg_color = "b" + if seg.id in soma_segs: + seg_color = "g" + elif seg.id in axon_segs: + seg_color = "r" + + spherical = ( + p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter + ) + + if verbose: + logger.info( + "\nSeg %s, id: %s%s has proximal: %s, distal: %s (width: %s, min_width: %s), color: %s" + % ( + seg.name, + seg.id, + " (spherical)" if spherical else "", + p, + d, + width, + min_width, + str(seg_color), + ) + ) if plane2d == "xy": add_line_to_matplotlib_2D_plot( ax, - [offset[0], offset[0]], - [offset[1], offset[1]], - soma_radius, - cell_color if color is None else color, + [offset[0] + p.x, offset[0] + d.x], + [offset[1] + p.y, offset[1] + d.y], + width, + seg_color if color is None else color, axis_min_max, ) elif plane2d == "yx": add_line_to_matplotlib_2D_plot( ax, - [offset[1], offset[1]], - [offset[0], offset[0]], - soma_radius, - cell_color if color is None else color, + [offset[1] + p.y, offset[1] + d.y], + [offset[0] + p.x, offset[0] + d.x], + width, + seg_color if color is None else color, axis_min_max, ) elif plane2d == "xz": add_line_to_matplotlib_2D_plot( ax, - [offset[0], offset[0]], - [offset[2], offset[2]], - soma_radius, - cell_color if color is None else color, + [offset[0] + p.x, offset[0] + d.x], + [offset[2] + p.z, offset[2] + d.z], + width, + seg_color if color is None else color, axis_min_max, ) elif plane2d == "zx": add_line_to_matplotlib_2D_plot( ax, - [offset[2], offset[2]], - [offset[0], offset[0]], - soma_radius, - cell_color if color is None else color, + [offset[2] + p.z, offset[2] + d.z], + [offset[0] + p.x, offset[0] + d.x], + width, + seg_color if color is None else color, axis_min_max, ) elif plane2d == "yz": add_line_to_matplotlib_2D_plot( ax, - [offset[1], offset[1]], - [offset[2], offset[2]], - soma_radius, - cell_color if color is None else color, + [offset[1] + p.y, offset[1] + d.y], + [offset[2] + p.z, offset[2] + d.z], + width, + seg_color if color is None else color, axis_min_max, ) elif plane2d == "zy": add_line_to_matplotlib_2D_plot( ax, - [offset[2], offset[2]], - [offset[1], offset[1]], - soma_radius, - cell_color if color is None else color, + [offset[2] + p.z, offset[2] + d.z], + [offset[1] + p.y, offset[1] + d.y], + width, + seg_color if color is None else color, axis_min_max, ) else: raise Exception(f"Invalid value for plane: {plane2d}") - else: - for seg in cell.morphology.segments: - p = cell.get_actual_proximal(seg.id) - d = seg.distal - width = (p.diameter + d.diameter) / 2 + if verbose: + print("Extent x: %s -> %s" % (axis_min_max[0], axis_min_max[1])) - if width < min_width: - width = min_width + if scalebar: + add_scalebar_to_matplotlib_plot(axis_min_max, ax) + if autoscale: + autoscale_matplotlib_plot(verbose, square) - if plot_type == "Constant": - width = min_width + if save_to_file: + abs_file = os.path.abspath(save_to_file) + plt.savefig(abs_file, dpi=200, bbox_inches="tight") + print(f"Saved image on plane {plane2d} to {abs_file} of plot: {title}") - if overlay_data and acolormap and norm: - try: - seg_color = acolormap(norm(overlay_data[seg.id])) - except KeyError: - seg_color = "black" - else: - seg_color = "b" - if seg.id in soma_segs: - seg_color = "g" - elif seg.id in axon_segs: - seg_color = "r" + if not nogui: + plt.show() + if close_plot: + logger.info("closing plot") + plt.close() - logger.debug(f"color is {color}") - spherical = ( - p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter - ) - if verbose: - print( - "\nSeg %s, id: %s%s has proximal: %s, distal: %s (width: %s, min_width: %s), color: %s" - % ( - seg.name, - seg.id, - " (spherical)" if spherical else "", - p, - d, - width, - min_width, - str(seg_color), - ) - ) +def plot_2D_point_cells( + offset: typing.List[float] = [0, 0], + plane2d: str = "xy", + color: typing.Optional[str] = None, + soma_radius: float = 0.0, + title: str = "", + verbose: bool = False, + fig: matplotlib.figure.Figure = None, + ax: matplotlib.axes.Axes = None, + min_width: float = DEFAULTS["minwidth"], + axis_min_max: typing.List = [float("inf"), -1 * float("inf")], + scalebar: bool = False, + nogui: bool = True, + autoscale: bool = True, + square: bool = False, + save_to_file: typing.Optional[str] = None, + close_plot: bool = False, +): + """Plot point cells. - if plane2d == "xy": - add_line_to_matplotlib_2D_plot( - ax, - [offset[0] + p.x, offset[0] + d.x], - [offset[1] + p.y, offset[1] + d.y], - width, - seg_color if color is None else color, - axis_min_max, - ) - elif plane2d == "yx": - add_line_to_matplotlib_2D_plot( - ax, - [offset[1] + p.y, offset[1] + d.y], - [offset[0] + p.x, offset[0] + d.x], - width, - seg_color if color is None else color, - axis_min_max, - ) - elif plane2d == "xz": - add_line_to_matplotlib_2D_plot( - ax, - [offset[0] + p.x, offset[0] + d.x], - [offset[2] + p.z, offset[2] + d.z], - width, - seg_color if color is None else color, - axis_min_max, - ) - elif plane2d == "zx": - add_line_to_matplotlib_2D_plot( - ax, - [offset[2] + p.z, offset[2] + d.z], - [offset[0] + p.x, offset[0] + d.x], - width, - seg_color if color is None else color, - axis_min_max, - ) - elif plane2d == "yz": - add_line_to_matplotlib_2D_plot( - ax, - [offset[1] + p.y, offset[1] + d.y], - [offset[2] + p.z, offset[2] + d.z], - width, - seg_color if color is None else color, - axis_min_max, - ) - elif plane2d == "zy": - add_line_to_matplotlib_2D_plot( - ax, - [offset[2] + p.z, offset[2] + d.z], - [offset[1] + p.y, offset[1] + d.y], - width, - seg_color if color is None else color, - axis_min_max, - ) - else: - raise Exception(f"Invalid value for plane: {plane2d}") + :param offset: location of cell + :type offset: [float, float] + :param plane2d: plane to plot on + :type plane2d: str + :param color: color to use for cell + :type color: str + :param soma_radius: radius of soma (uses min_width if provided) + :type soma_radius: float + :param fig: a matplotlib.figure.Figure object to use + :type fig: matplotlib.figure.Figure + :param ax: a matplotlib.axes.Axes object to use + :type ax: matplotlib.axes.Axes + :param min_width: minimum width for segments (useful for visualising very + thin segments): default 0.8um + :type min_width: float + :param axis_min_max: min, max value of axes + :type axis_min_max: [float, float] + :param title: title of plot + :type title: str + :param verbose: show extra information (default: False) + :type verbose: bool + :param nogui: do not show matplotlib GUI (default: false) + :type nogui: bool + :param save_to_file: optional filename to save generated morphology to + :type save_to_file: str + :param square: scale axes so that image is approximately square + :type square: bool + :param autoscale: toggle autoscaling + :type autoscale: bool + :param scalebar: toggle scalebar + :type scalebar: bool + :param close_plot: call pyplot.close() to close plot after plotting + :type close_plot: bool + """ + if fig is None: + fig, ax = get_new_matplotlib_morph_plot(title) - if verbose: - print("Extent x: %s -> %s" % (axis_min_max[0], axis_min_max[1])) + cell_color = get_next_hex_color() + if soma_radius is None: + soma_radius = 10 + + if plane2d == "xy": + add_line_to_matplotlib_2D_plot( + ax, + [offset[0], offset[0]], + [offset[1], offset[1]], + soma_radius, + cell_color if color is None else color, + axis_min_max, + ) + elif plane2d == "yx": + add_line_to_matplotlib_2D_plot( + ax, + [offset[1], offset[1]], + [offset[0], offset[0]], + soma_radius, + cell_color if color is None else color, + axis_min_max, + ) + elif plane2d == "xz": + add_line_to_matplotlib_2D_plot( + ax, + [offset[0], offset[0]], + [offset[2], offset[2]], + soma_radius, + cell_color if color is None else color, + axis_min_max, + ) + elif plane2d == "zx": + add_line_to_matplotlib_2D_plot( + ax, + [offset[2], offset[2]], + [offset[0], offset[0]], + soma_radius, + cell_color if color is None else color, + axis_min_max, + ) + elif plane2d == "yz": + add_line_to_matplotlib_2D_plot( + ax, + [offset[1], offset[1]], + [offset[2], offset[2]], + soma_radius, + cell_color if color is None else color, + axis_min_max, + ) + elif plane2d == "zy": + add_line_to_matplotlib_2D_plot( + ax, + [offset[2], offset[2]], + [offset[1], offset[1]], + soma_radius, + cell_color if color is None else color, + axis_min_max, + ) + else: + raise Exception(f"Invalid value for plane: {plane2d}") if scalebar: add_scalebar_to_matplotlib_plot(axis_min_max, ax) From bd82eee1d89208553f4c3de6e5756284ba876722 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 17 Feb 2023 16:00:26 +0000 Subject: [PATCH 33/88] test(plot): add test for point neuron plotter --- tests/plot/Izh2007Cells.net.nml | 97 ++++++++++++++++++++++++++++++ tests/plot/test_morphology_plot.py | 19 ++++++ 2 files changed, 116 insertions(+) create mode 100644 tests/plot/Izh2007Cells.net.nml diff --git a/tests/plot/Izh2007Cells.net.nml b/tests/plot/Izh2007Cells.net.nml new file mode 100644 index 00000000..87cb8f17 --- /dev/null +++ b/tests/plot/Izh2007Cells.net.nml @@ -0,0 +1,97 @@ + + + + + + + Regular spiking cell + + + + + Weakly adapting cell + + + + + + Strongly adapting cell + + + + + Low threshold spiking cell + + + + + + + + + + + + + A number of different Izhikevich spiking cells + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index a58689f0..0371e2cb 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -19,6 +19,7 @@ plot_interactive_3D, plot_2D_schematic, plot_segment_groups_curtain_plots, + plot_2D_point_cells ) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -31,6 +32,24 @@ class TestMorphologyPlot(BaseTestCase): """Test Plot module""" + def test_2d_point_plotter(self): + """Test plot_2D_point_cells function.""" + nml_files = ["tests/plot/Izh2007Cells.net.nml"] + for nml_file in nml_files: + ofile = pl.Path(nml_file).name + for plane in ["xy", "yz", "xz"]: + filename = f"test_morphology_plot_2d_point_{ofile.replace('.', '_', 100)}_{plane}.png" + # remove the file first + try: + pl.Path(filename).unlink() + except FileNotFoundError: + pass + + plot_2D(nml_file, nogui=False, plane2d=plane, save_to_file=filename) + + self.assertIsFile(filename) + pl.Path(filename).unlink() + def test_2d_plotter(self): """Test plot_2D function.""" nml_files = ["tests/plot/Cell_497232312.cell.nml", "tests/plot/test.cell.nml"] From 465c365c3248af27c40a25b69d55a8ac7df3f028 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 17 Feb 2023 16:30:43 +0000 Subject: [PATCH 34/88] chore(plot-morph): improve documentation --- pyneuroml/plot/PlotMorphology.py | 89 +++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 85ea2279..4a1c090c 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -167,12 +167,12 @@ def plot_2D( square: bool = False, plot_type: str = "Detailed", title: typing.Optional[str] = None, - close_plot: bool = False + close_plot: bool = False, ): """Plot cells in a 2D plane. If a file with a network containing multiple cells is provided, it will - plot all the cells. For detailed neuroml.Cell* types, it will plot their + plot all the cells. For detailed neuroml.Cell types, it will plot their complete morphology. For point neurons, we only plot the points (locations) where they are. @@ -203,6 +203,7 @@ def plot_2D( This is only applicable for neuroml.Cell cells (ones with some morphology) + :type plot_type: str :param title: title of plot :type title: str @@ -257,15 +258,18 @@ def plot_2D( color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None if cell is None: - plot_2D_point_cells(offset=pos, plane2d=plane2d, - color=color, - soma_radius=radius, - verbose=verbose, - ax=ax, - fig=fig, - autoscale=False, - scalebar=False, - nogui=True,) + plot_2D_point_cells( + offset=pos, + plane2d=plane2d, + color=color, + soma_radius=radius, + verbose=verbose, + ax=ax, + fig=fig, + autoscale=False, + scalebar=False, + nogui=True, + ) else: if plot_type == "Schematic": plot_2D_schematic( @@ -345,14 +349,14 @@ def plot_interactive_3D( :param save_to_file: optional filename to save generated morphology to :type save_to_file: str :param plot_type: type of plot, one of: + - Detailed: show detailed morphology taking into account each segment's width - Constant: show morphology, but use constant line widths - - Schematic: only plot each unbranched segment group as a straight - line, not following each segment + :type plot_type: str """ - if plot_type not in ["Detailed", "Constant", "Schematic"]: + if plot_type not in ["Detailed", "Constant"]: raise ValueError( "plot_type must be one of 'Detailed', 'Constant', or 'Schematic'" ) @@ -450,9 +454,24 @@ def plot_2D_cell_morphology( overlay_data_label: typing.Optional[str] = None, datamin: typing.Optional[float] = None, datamax: typing.Optional[float] = None, - colormap_name: str = 'viridis' + colormap_name: str = "viridis", ): - """Plot the detailed 2D morphology of a cell in provided plane + """Plot the detailed 2D morphology of a cell in provided plane. + + The method can also overlay data onto the morphology. + + .. versionadded:: 1.0.0 + + .. seealso:: + + :py:func:`plot_2D` + general function for plotting + + :py:func:`plot_2D_schematic` + for plotting only segmeng groups with their labels + + :py:func:`plot_2D_point_cells` + for plotting point cells :param offset: offset for cell :type offset: [float, float] @@ -508,7 +527,9 @@ def plot_2D_cell_morphology( """ if cell is None: - raise ValueError("No cell provided. If you would like to plot a network of point neurons, consider using `plot_2D_point_cells` instead") + raise ValueError( + "No cell provided. If you would like to plot a network of point neurons, consider using `plot_2D_point_cells` instead" + ) try: soma_segs = cell.get_all_segments_in_group("soma_group") @@ -691,6 +712,19 @@ def plot_2D_point_cells( ): """Plot point cells. + .. versionadded:: 1.0.0 + + .. seealso:: + + :py:func:`plot_2D` + general function for plotting + + :py:func:`plot_2D_schematic` + for plotting only segmeng groups with their labels + + :py:func:`plot_2D_cell_morphology` + for plotting cells with detailed morphologies + :param offset: location of cell :type offset: [float, float] :param plane2d: plane to plot on @@ -829,6 +863,19 @@ def plot_2D_schematic( This plots each segment group as a straight line between its first and last segment. + .. versionadded:: 1.0.0 + + .. seealso:: + + :py:func:`plot_2D` + general function for plotting + + :py:func:`plot_2D_point_cells` + for plotting point cells + + :py:func:`plot_2D_cell_morphology` + for plotting cells with detailed morphologies + :param offset: offset for cell :type offset: [float, float] :param cell: cell to plot @@ -1065,10 +1112,12 @@ def plot_segment_groups_curtain_plots( title: str = "SegmentGroup", datamin: typing.Optional[float] = None, datamax: typing.Optional[float] = None, - close_plot: bool = False + close_plot: bool = False, ) -> None: """Plot curtain plots of provided segment groups. + .. versionadded:: 1.0.0 + :param cell: cell to plot :type cell: neuroml.Cell :param segment_groups: list of unbranched segment groups to plot @@ -1132,7 +1181,9 @@ def plot_segment_groups_curtain_plots( norm = None if overlay_data: if set(overlay_data.keys()) != set(ord_segs.keys()): - raise ValueError(f"Keys of overlay_data ({overlay_data.keys()}) and ord_segs ({ord_segs.keys()})must match.") + raise ValueError( + f"Keys of overlay_data ({overlay_data.keys()}) and ord_segs ({ord_segs.keys()})must match." + ) for key in overlay_data.keys(): if len(overlay_data[key]) != len(ord_segs[key]): raise ValueError( From d5ee73f0826bff2da39873fd179dbd6a72962438 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 17 Feb 2023 17:10:16 +0000 Subject: [PATCH 35/88] chore(povray): document method --- pyneuroml/povray/NeuroML2ToPOVRay.py | 103 ++++++++++++++++++++------- 1 file changed, 78 insertions(+), 25 deletions(-) diff --git a/pyneuroml/povray/NeuroML2ToPOVRay.py b/pyneuroml/povray/NeuroML2ToPOVRay.py index 2c948b47..164da297 100644 --- a/pyneuroml/povray/NeuroML2ToPOVRay.py +++ b/pyneuroml/povray/NeuroML2ToPOVRay.py @@ -8,6 +8,7 @@ # This work has been funded by the Medical Research Council and Wellcome Trust +import typing import random import argparse import logging @@ -17,9 +18,9 @@ logger = logging.getLogger(__name__) -_WHITE = "<1,1,1,0.55>" -_BLACK = "<0,0,0,0.55>" -_GREY = "<0.85,0.85,0.85,0.55>" +_WHITE = "<1,1,1,0.55>" # type: str +_BLACK = "<0,0,0,0.55>" # type: str +_GREY = "<0.85,0.85,0.85,0.55>" # type: str _DUMMY_CELL = "DUMMY_CELL" @@ -49,7 +50,7 @@ "mindiam": 0, "plane": False, "segids": False, -} +} # type: typing.Dict[str, typing.Any] def process_args(): @@ -258,28 +259,80 @@ def main(): def generate_povray( - neuroml_file, - split=defaults["split"], - background=defaults["background"], - movie=defaults["movie"], - inputs=defaults["inputs"], - conns=defaults["conns"], - conn_points=defaults["conn_points"], - v=defaults["v"], - frames=defaults["frames"], - posx=defaults["posx"], - posy=defaults["posy"], - posz=defaults["posz"], - viewx=defaults["viewx"], - viewy=defaults["viewy"], - viewz=defaults["viewz"], - scalex=defaults["scalex"], - scaley=defaults["scaley"], - scalez=defaults["scalez"], - mindiam=defaults["mindiam"], - plane=defaults["plane"], - segids=defaults["segids"], + neuroml_file: str, + split: bool = defaults["split"], + background: str = defaults["background"], + movie: bool = defaults["movie"], + inputs: bool = defaults["inputs"], + conns: bool = defaults["conns"], + conn_points: bool = defaults["conn_points"], + v: bool = defaults["v"], + frames: bool = defaults["frames"], + posx: float = defaults["posx"], + posy: float = defaults["posy"], + posz: float = defaults["posz"], + viewx: float = defaults["viewx"], + viewy: float = defaults["viewy"], + viewz: float = defaults["viewz"], + scalex: float = defaults["scalex"], + scaley: float = defaults["scaley"], + scalez: float = defaults["scalez"], + mindiam: float = defaults["mindiam"], + plane: bool = defaults["plane"], + segids: bool = defaults["segids"], ): + """Generate a POVRAY image or movie file. + + Please see http://www.povray.org/documentation/ and + https://wiki.povray.org/content/Main_Page for information on installing and + using POVRAY. + + This function will generate POVRAY files that you can then run using + POVRAY. + + :param neuroml_file: path to NeuroML file containing cell/network + :type neuroml_file: str + :param split: generate separate files for cells and network + :type split: bool + :param background: background for POVRAY rendering + :type background: str + :param movie: toggle between image and movie rendering + :type movie: bool + :param inputs: show locations of inputs also + :type inputs: bool + :param conns: show connections in networks with lines + :type conns: bool + :param conn_points: show end points of connections in network + :type conn_points: bool + :param v: toggle verbose output + :type v: bool + :param frames: number of frames to use in movie + :type frames: int + :param posx: offset position in x dir (0 is centre, 1 is top) + :type posx: float + :param posy: offset position in y dir (0 is centre, 1 is top) + :type posy: float + :param posz: offset position in z dir (0 is centre, 1 is top) + :type posz: float + :param viewx: offset viewing point in x dir (0 is centre, 1 is top) + :type viewx: float + :param viewy: offset viewing point in y dir (0 is centre, 1 is top) + :type viewy: float + :param viewz: offset viewing point in z dir (0 is centre, 1 is top) + :type viewz: float + :param scalex: scale position from network in x dir + :type scalex: float + :param scaley: scale position from network in y dir + :type scaley: float + :param scalez: scale position from network in z dir + :type scalez: float + :param mindiam: minimum diameter for dendrites/axons (to improve visualisations) + :type mindiam: float + :param plane: add a 2D plane below cell/network + :type plane: bool + :param segids: toggle showing segment ids + :type segids: bool + """ xmlfile = neuroml_file pov_file_name = xmlfile From 8d62a777d830fc73c71822275f7299b502f8ac98 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 17 Feb 2023 17:15:15 +0000 Subject: [PATCH 36/88] chore(povray): add module level docstring --- pyneuroml/povray/NeuroML2ToPOVRay.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyneuroml/povray/NeuroML2ToPOVRay.py b/pyneuroml/povray/NeuroML2ToPOVRay.py index 164da297..3554d3a8 100644 --- a/pyneuroml/povray/NeuroML2ToPOVRay.py +++ b/pyneuroml/povray/NeuroML2ToPOVRay.py @@ -1,11 +1,12 @@ -# -# A file for converting NeuroML 2 files (including cells & network structure) -# into POVRay files for 3D rendering -# -# Author: Padraig Gleeson & Matteo Farinella -# -# This file has been developed as part of the neuroConstruct project -# This work has been funded by the Medical Research Council and Wellcome Trust +""" +A file for converting NeuroML 2 files (including cells & network structure) +into POVRay files for 3D rendering + +Author: Padraig Gleeson & Matteo Farinella + +This file has been developed as part of the neuroConstruct project +This work has been funded by the Medical Research Council and Wellcome Trust +""" import typing @@ -735,7 +736,6 @@ def generate_povray( net_file.write("}\n") if conns or conn_points: - projections = ( nml_doc.networks[0].projections + nml_doc.networks[0].electrical_projections From 92e47884fd7efea0ed387a2c84aba29f1f2b98d5 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 22 Feb 2023 15:13:07 +0000 Subject: [PATCH 37/88] feat(morph-plot): rename plotly morph plotter --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 4a1c090c..c6da6696 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -320,7 +320,7 @@ def plot_2D( plt.close() -def plot_interactive_3D( +def plot_interactive_3D_plotly( nml_file: str, min_width: float = 0.8, verbose: bool = False, From 1da6f3250112a5a7ef458a11a4e262570ff863a4 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 11:47:45 +0000 Subject: [PATCH 38/88] feat(3d-plotting): add initial 3D schematic plotter --- pyneuroml/plot/PlotMorphology.py | 180 ++++++++++++++++++++++++++++- pyneuroml/utils/plot.py | 42 +++++++ tests/plot/test_morphology_plot.py | 24 +++- 3 files changed, 235 insertions(+), 11 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index c6da6696..64fec259 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -16,6 +16,8 @@ import typing import logging +import napari +from vispy import app, scene import numpy import matplotlib from matplotlib import pyplot as plt @@ -26,6 +28,7 @@ from pyneuroml.utils import extract_position_info from pyneuroml.utils.plot import ( add_text_to_matplotlib_2D_plot, + add_text_to_vispy_3D_plot, get_next_hex_color, add_box_to_matplotlib_2D_plot, get_new_matplotlib_morph_plot, @@ -33,7 +36,7 @@ add_scalebar_to_matplotlib_plot, add_line_to_matplotlib_2D_plot, ) -from neuroml import SegmentGroup, Cell +from neuroml import SegmentGroup, Cell, Segment from neuroml.neuro_lex_ids import neuro_lex_ids @@ -320,7 +323,7 @@ def plot_2D( plt.close() -def plot_interactive_3D_plotly( +def plot_3D_cell_morphology_plotly( nml_file: str, min_width: float = 0.8, verbose: bool = False, @@ -846,7 +849,7 @@ def plot_2D_schematic( offset: typing.List[float] = [0, 0], labels: bool = False, plane2d: str = "xy", - min_width: float = DEFAULTS["minwidth"], + width: float = 2.0, verbose: bool = False, square: bool = False, nogui: bool = False, @@ -886,9 +889,8 @@ def plot_2D_schematic( :type labels: bool :param plane2d: what plane to plot (xy/yx/yz/zy/zx/xz) :type plane2d: str - :param min_width: minimum width for segments (useful for visualising very - thin segments): default 0.8um - :type min_width: float + :param width: width for lines + :type width: float :param verbose: show extra information (default: False) :type verbose: bool :param square: scale axes so that image is approximately square @@ -1098,6 +1100,172 @@ def plot_2D_schematic( plt.close() +def plot_3D_schematic( + cell: Cell, + segment_groups: typing.Optional[typing.List[SegmentGroup]], + offset: typing.List[float] = [0, 0, 0], + labels: bool = False, + width: float = 5., + verbose: bool = False, + square: bool = False, + nogui: bool = False, + viewer: napari.Viewer = None, + title: str = "", + canvas: scene.SceneCanvas = None +) -> None: + """Plot a 3D schematic of the provided segment groups in Napari as a new + layer.. + + This plots each segment group as a straight line between its first and last + segment. + + .. versionadded:: 1.0.0 + + .. seealso:: + + :py:func:`plot_2D_schematic` + general function for plotting + + :py:func:`plot_2D` + general function for plotting + + :py:func:`plot_2D_point_cells` + for plotting point cells + + :py:func:`plot_2D_cell_morphology` + for plotting cells with detailed morphologies + + :param offset: offset for cell + :type offset: [float, float, float] + :param cell: cell to plot + :type cell: neuroml.Cell + :param segment_groups: list of unbranched segment groups to plot + :type segment_groups: list(SegmentGroup) + :param labels: toggle labelling of segment groups + :type labels: bool + :param width: width for lines for segment groups + :type width: float + :param verbose: show extra information (default: False) + :type verbose: bool + :param viewer: a napari.Viewer object + :type viewer: napari.Viewer + :param title: title of plot + :type title: str + + """ + if title == "": + title = f"2D schematic of segment groups from {cell.id}" + + # if no segment groups are given, do them all + if segment_groups is None: + segment_groups = [] + for sg in cell.morphology.segment_groups: + if sg.neuro_lex_id == neuro_lex_ids["section"]: + segment_groups.append(sg.id) + + ord_segs = cell.get_ordered_segments_in_groups( + segment_groups, check_parentage=False + ) + + # if no canvas is defined, define a new one + if canvas is None: + # get approximate view extents + seg0 = cell.morphology.segments[0] # type: Segment + view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] + seg1 = cell.morphology.segments[-1] # type: Segment + view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] + view_extent = numpy.array(view_max) - numpy.array(view_min) + view_center = view_extent / 2 + + print(f"view maxmin is {view_min} - {view_max}") + + # https://vispy.org/gallery/scene/axes_plot.html + canvas = scene.SceneCanvas(keys='interactive', show=True, + bgcolor="black", size=(800, 600)) + grid = canvas.central_widget.add_grid(margin=10) + grid.spacing = 0 + + title_widget = scene.Label(title) + title_widget.height_max = 40 + grid.add_widget(title_widget, row=0, col=0, col_span=2) + + yaxis = scene.AxisWidget(orientation='left', + axis_label='Extent (Y)', + axis_font_size=12, + axis_label_margin=50, + tick_label_margin=5) + yaxis.width_max = 80 + grid.add_widget(yaxis, row=1, col=0) + + xaxis = scene.AxisWidget(orientation='bottom', + axis_label='Extent (X)', + axis_font_size=12, + axis_label_margin=50, + tick_label_margin=5) + + xaxis.height_max = 80 + grid.add_widget(xaxis, row=2, col=1) + + right_padding = grid.add_widget(row=1, col=2, row_span=1) + right_padding.width_max = 50 + + view = grid.add_view(row=1, col=1, border_color='white', + camera="arcball") + view.camera.set_range( + x=(view_min[0], view_max[0]), + y=(view_min[1], view_max[1]), + z=(view_min[2], view_max[2]) + ) + + xaxis.link_view(view) + yaxis.link_view(view) + + # xyz axis for orientation + # TODO improve placement + plot = scene.Line([view_center, [view_center[0] + 500, view_center[1], view_center[2]]], + parent=view.scene, color="white", + width=width) + plot = scene.Line([view_center, [view_center[1], view_center[1] + 500, view_center[2]]], + parent=view.scene, color="white", + width=width) + plot = scene.Line([view_center, [view_center[1], view_center[1], view_center[2] + 500]], + parent=view.scene, color="white", + width=2) + + for sgid, segs in ord_segs.items(): + sgobj = cell.get_segment_group(sgid) + if sgobj.neuro_lex_id != neuro_lex_ids["section"]: + raise ValueError( + f"{sgobj} does not have neuro_lex_id set to indicate it is an unbranched segment" + ) + + # get proximal and distal points + first_seg = segs[0] # type: Segment + last_seg = segs[-1] # type: Segment + first_prox = cell.get_actual_proximal(first_seg.id) + + data = numpy.array([ + [offset[0] + first_prox.x, offset[1] + first_prox.y, offset[2] + first_prox.z], + [offset[0] + last_seg.distal.x, offset[1] + last_seg.distal.y, offset[2] + last_seg.distal.z] + ]) + + color = get_next_hex_color() + plot = scene.Line(data, parent=view.scene, color=color, + width=width) + + # TODO: needs fixing to show labels + labels = False + if labels: + alabel = add_text_to_vispy_3D_plot(scene, text=f"{sgid}", + xv=[offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], + yv=[offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], + zv=[offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], + color=color) + alabel.font_size = 30 + + app.run() + + def plot_segment_groups_curtain_plots( cell: Cell, segment_groups: typing.List[SegmentGroup], diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 64284140..6eb5a56f 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -17,12 +17,54 @@ from matplotlib.axes import Axes from matplotlib.patches import Rectangle from matplotlib_scalebar.scalebar import ScaleBar +from vispy.scene import SceneCanvas logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) +def add_text_to_vispy_3D_plot( + scene: SceneCanvas, + xv: typing.List[float], + yv: typing.List[float], + zv: typing.List[float], + color: str, + text: str, +): + """Add text to a vispy plot between two points. + + Wrapper around vispy.scene.visuals.Text + + Rotates the text label to ensure it is at the same angle as the line. + + :param scene: vispy scene object + :type scene: SceneCanvas + :param xv: start and end coordinates in one axis + :type xv: list[x1, x2] + :param yv: start and end coordinates in second axis + :type yv: list[y1, y2] + :param zv: start and end coordinates in third axix + :type zv: list[z1, z2] + :param color: color of text + :type color: str + :param text: text to write + :type text: str + """ + angle = int(numpy.rad2deg(numpy.arctan2((yv[1] - yv[0]), (xv[1] - xv[0])))) + if angle > 90: + angle -= 180 + elif angle < -90: + angle += 180 + + return scene.Text( + pos=((xv[0] + xv[1]) / 2, (yv[0] + yv[1]) / 2, (zv[0] + zv[1]) / 2), + text=text, + color=color, + rotation=angle, + ) + + def add_text_to_matplotlib_2D_plot( ax: matplotlib.axes.Axes, xv: typing.List[float], diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 0371e2cb..9c2e5c29 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -16,10 +16,11 @@ from pyneuroml.plot.PlotMorphology import ( plot_2D, plot_2D_cell_morphology, - plot_interactive_3D, + plot_3D_cell_morphology_plotly, plot_2D_schematic, plot_segment_groups_curtain_plots, - plot_2D_point_cells + plot_2D_point_cells, + plot_3D_schematic ) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -160,8 +161,21 @@ def test_2d_schematic_plotter_network(self): self.assertIsFile(filename) pl.Path(filename).unlink() - def test_3d_plotter(self): - """Test plot_interactive_3D function.""" + def test_3d_schematic_plotter(self): + """Test plot_3D_schematic plotter function.""" + nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + # remove the file first + + plot_3D_schematic( + cell, + segment_groups=None, + nogui=False, + ) + + def test_3d_plotter_plotly(self): + """Test plot_3D_cell_morphology_plotly function.""" nml_files = ["tests/plot/Cell_497232312.cell.nml", "tests/plot/test.cell.nml"] for nml_file in nml_files: ofile = pl.Path(nml_file).name @@ -172,7 +186,7 @@ def test_3d_plotter(self): except FileNotFoundError: pass - plot_interactive_3D(nml_file, nogui=True, save_to_file=filename) + plot_3D_cell_morphology_plotly(nml_file, nogui=True, save_to_file=filename) self.assertIsFile(filename) pl.Path(filename).unlink() From 16c5d42eee63c25e662be5266ae46f98da648560 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 12:06:54 +0000 Subject: [PATCH 39/88] refactor(3d-schematic): split out canvas creator into util --- pyneuroml/plot/PlotMorphology.py | 62 ++----------------------- pyneuroml/utils/plot.py | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 59 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 64fec259..35831b9e 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -35,6 +35,7 @@ autoscale_matplotlib_plot, add_scalebar_to_matplotlib_plot, add_line_to_matplotlib_2D_plot, + create_new_vispy_canvas, ) from neuroml import SegmentGroup, Cell, Segment from neuroml.neuro_lex_ids import neuro_lex_ids @@ -1154,7 +1155,7 @@ def plot_3D_schematic( """ if title == "": - title = f"2D schematic of segment groups from {cell.id}" + title = f"3D schematic of segment groups from {cell.id}" # if no segment groups are given, do them all if segment_groups is None: @@ -1169,68 +1170,11 @@ def plot_3D_schematic( # if no canvas is defined, define a new one if canvas is None: - # get approximate view extents seg0 = cell.morphology.segments[0] # type: Segment view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] seg1 = cell.morphology.segments[-1] # type: Segment view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] - view_extent = numpy.array(view_max) - numpy.array(view_min) - view_center = view_extent / 2 - - print(f"view maxmin is {view_min} - {view_max}") - - # https://vispy.org/gallery/scene/axes_plot.html - canvas = scene.SceneCanvas(keys='interactive', show=True, - bgcolor="black", size=(800, 600)) - grid = canvas.central_widget.add_grid(margin=10) - grid.spacing = 0 - - title_widget = scene.Label(title) - title_widget.height_max = 40 - grid.add_widget(title_widget, row=0, col=0, col_span=2) - - yaxis = scene.AxisWidget(orientation='left', - axis_label='Extent (Y)', - axis_font_size=12, - axis_label_margin=50, - tick_label_margin=5) - yaxis.width_max = 80 - grid.add_widget(yaxis, row=1, col=0) - - xaxis = scene.AxisWidget(orientation='bottom', - axis_label='Extent (X)', - axis_font_size=12, - axis_label_margin=50, - tick_label_margin=5) - - xaxis.height_max = 80 - grid.add_widget(xaxis, row=2, col=1) - - right_padding = grid.add_widget(row=1, col=2, row_span=1) - right_padding.width_max = 50 - - view = grid.add_view(row=1, col=1, border_color='white', - camera="arcball") - view.camera.set_range( - x=(view_min[0], view_max[0]), - y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]) - ) - - xaxis.link_view(view) - yaxis.link_view(view) - - # xyz axis for orientation - # TODO improve placement - plot = scene.Line([view_center, [view_center[0] + 500, view_center[1], view_center[2]]], - parent=view.scene, color="white", - width=width) - plot = scene.Line([view_center, [view_center[1], view_center[1] + 500, view_center[2]]], - parent=view.scene, color="white", - width=width) - plot = scene.Line([view_center, [view_center[1], view_center[1], view_center[2] + 500]], - parent=view.scene, color="white", - width=2) + scene, view = create_new_vispy_canvas(view_min, view_max, title) for sgid, segs in ord_segs.items(): sgobj = cell.get_segment_group(sgid) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 6eb5a56f..bc59f34b 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -17,6 +17,7 @@ from matplotlib.axes import Axes from matplotlib.patches import Rectangle from matplotlib_scalebar.scalebar import ScaleBar +from vispy import scene from vispy.scene import SceneCanvas @@ -332,3 +333,81 @@ def add_line_to_matplotlib_2D_plot(ax, xv, yv, width, color, axis_min_max): axis_min_max[0] = min(axis_min_max[0], xv[1]) axis_min_max[1] = max(axis_min_max[1], xv[0]) axis_min_max[1] = max(axis_min_max[1], xv[1]) + + +def create_new_vispy_canvas(view_min: typing.List[float], view_max: + typing.List[float], title: str = "", axes_pos: + typing.Optional[typing.List] = None, axes_length: float = 100, + axes_width: int = 2): + """Create a new vispy scene canvas with a view and optional axes lines + + Reference: https://vispy.org/gallery/scene/axes_plot.html + + :param view_min: min view co-ordinates + :type view_min: [float, float, float] + :param view_max: max view co-ordinates + :type view_max: [float, float, float] + :param title: title of canvas + :type title: str + :param axes_pos: position to draw axes at + :type axes_pos: [float, float, float] + :param axes_length: length of axes + :type axes_length: float + :param axes_width: width of axes lines + :type axes_width: float + :returns: scene, view + """ + canvas = scene.SceneCanvas(keys='interactive', show=True, + bgcolor="black", size=(800, 600)) + grid = canvas.central_widget.add_grid(margin=10) + grid.spacing = 0 + + title_widget = scene.Label(title, color="white") + title_widget.height_max = 80 + grid.add_widget(title_widget, row=0, col=0, col_span=2) + + yaxis = scene.AxisWidget(orientation='left', + axis_label='Extent (Y)', + axis_font_size=12, + axis_label_margin=50, + tick_label_margin=5) + yaxis.width_max = 80 + grid.add_widget(yaxis, row=1, col=0) + + xaxis = scene.AxisWidget(orientation='bottom', + axis_label='Extent (X)', + axis_font_size=12, + axis_label_margin=50, + tick_label_margin=5) + + xaxis.height_max = 80 + grid.add_widget(xaxis, row=2, col=1) + + right_padding = grid.add_widget(row=1, col=2, row_span=1) + right_padding.width_max = 50 + + view = grid.add_view(row=1, col=1, border_color='white', + camera="arcball") + view.camera.set_range( + x=(view_min[0], view_max[0]), + y=(view_min[1], view_max[1]), + z=(view_min[2], view_max[2]) + ) + + xaxis.link_view(view) + yaxis.link_view(view) + + # xyz axis for orientation + # TODO improve placement + if axes_pos: + scene.Line([axes_pos, [axes_pos[0] + axes_length, axes_pos[1], axes_pos[2]]], + parent=view.scene, color="white", + width=axes_width) + scene.Line([axes_pos, [axes_pos[1], axes_pos[1] + axes_length, axes_pos[2]]], + parent=view.scene, color="white", + width=axes_width) + scene.Line([axes_pos, [axes_pos[1], axes_pos[1], axes_pos[2] + axes_length]], + parent=view.scene, color="white", + width=axes_width) + + return scene, view From 572b2bed331865bcf0f2c578d21da79c6225a0a5 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 12:15:33 +0000 Subject: [PATCH 40/88] chore(3d-schematic): pass scene and view instead of canvas --- pyneuroml/plot/PlotMorphology.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 35831b9e..c7b415cb 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1112,7 +1112,8 @@ def plot_3D_schematic( nogui: bool = False, viewer: napari.Viewer = None, title: str = "", - canvas: scene.SceneCanvas = None + current_scene: scene.SceneCanvas = None, + current_view: scene.ViewBox = None, ) -> None: """Plot a 3D schematic of the provided segment groups in Napari as a new layer.. @@ -1152,7 +1153,13 @@ def plot_3D_schematic( :type viewer: napari.Viewer :param title: title of plot :type title: str - + :param nogui: toggle if plot should be shown or not + :type nogui: bool + :param current_scene: vispy SceneCanvas to use (a new one is created if it is not + provided) + :type current_scene: SceneCanvas + :param current_view: vispy viewbox to use + :type current_view: ViewBox """ if title == "": title = f"3D schematic of segment groups from {cell.id}" @@ -1169,12 +1176,12 @@ def plot_3D_schematic( ) # if no canvas is defined, define a new one - if canvas is None: + if current_scene is None: seg0 = cell.morphology.segments[0] # type: Segment view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] seg1 = cell.morphology.segments[-1] # type: Segment view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] - scene, view = create_new_vispy_canvas(view_min, view_max, title) + current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) for sgid, segs in ord_segs.items(): sgobj = cell.get_segment_group(sgid) @@ -1194,7 +1201,7 @@ def plot_3D_schematic( ]) color = get_next_hex_color() - plot = scene.Line(data, parent=view.scene, color=color, + plot = scene.Line(data, parent=current_view.scene, color=color, width=width) # TODO: needs fixing to show labels From 337ef91de26213f0d12639c3002804086612faa1 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 12:16:46 +0000 Subject: [PATCH 41/88] feat(3d-schematic): add option to not run app immediately --- pyneuroml/plot/PlotMorphology.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index c7b415cb..2c7b19c4 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1214,7 +1214,8 @@ def plot_3D_schematic( color=color) alabel.font_size = 30 - app.run() + if not nogui: + app.run() def plot_segment_groups_curtain_plots( From af4c71117e359f4734ca94cc73164ab15ecbbed0 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 15:00:20 +0000 Subject: [PATCH 42/88] feat(plot-morph): add 3D morphology viewer --- pyneuroml/plot/PlotMorphology.py | 190 +++++++++++++++++++++++++++++ tests/plot/test_morphology_plot.py | 13 +- 2 files changed, 200 insertions(+), 3 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 2c7b19c4..f2a96924 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -696,6 +696,196 @@ def plot_2D_cell_morphology( plt.close() +def plot_3D_cell_morphology( + offset: typing.List[float] = [0, 0, 0], + cell: Cell = None, + color: typing.Optional[str] = None, + title: str = "", + verbose: bool = False, + current_scene: scene.SceneCanvas = None, + current_view: scene.ViewBox = None, + min_width: float = DEFAULTS["minwidth"], + axis_min_max: typing.List = [float("inf"), -1 * float("inf")], + nogui: bool = True, + plot_type: str = "Constant", +): + """Plot the detailed 3D morphology of a cell using vispy. + https://vispy.org/ + + .. versionadded:: 1.0.0 + + .. seealso:: + + :py:func:`plot_2D` + general function for plotting + + :py:func:`plot_2D_schematic` + for plotting only segmeng groups with their labels + + :py:func:`plot_2D_point_cells` + for plotting point cells + + :param offset: offset for cell + :type offset: [float, float] + :param cell: cell to plot + :type cell: neuroml.Cell + :param color: color to use for segments: + + - if None, each segment is given a new unique color + - if "Groups", each unbranched segment group is given a unique color, + and segments that do not belong to an unbranched segment group are in + white + - if "Default Groups", axonal segments are in red, dendritic in blue, + somatic in green, and others in white + + :type color: str + :param min_width: minimum width for segments (useful for visualising very + :type min_width: float + :param axis_min_max: min, max value of axes + :type axis_min_max: [float, float] + :param title: title of plot + :type title: str + :param verbose: show extra information (default: False) + :type verbose: bool + :param nogui: do not show image immediately + :type nogui: bool + :param current_scene: vispy SceneCanvas to use (a new one is created if it is not + provided) + :type current_scene: SceneCanvas + :param current_view: vispy viewbox to use + :type current_view: ViewBox + :param plot_type: type of plot, one of: + + - "Detailed": show detailed morphology taking into account each segment's + width. This is not performant, because a new line is required for + each segment. To only be used for cells with small numbers of + segments + - "Constant": show morphology, but use constant line widths + + This is only applicable for neuroml.Cell cells (ones with some + morphology) + + :type plot_type: str + :raises: ValueError if `cell` is None + + """ + if cell is None: + raise ValueError( + "No cell provided. If you would like to plot a network of point neurons, consider using `plot_2D_point_cells` instead" + ) + + try: + soma_segs = cell.get_all_segments_in_group("soma_group") + except Exception: + soma_segs = [] + try: + dend_segs = cell.get_all_segments_in_group("dendrite_group") + except Exception: + dend_segs = [] + try: + axon_segs = cell.get_all_segments_in_group("axon_group") + except Exception: + axon_segs = [] + + if current_scene is None or current_view is None: + seg0 = cell.morphology.segments[0] # type: Segment + view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] + seg1 = cell.morphology.segments[-1] # type: Segment + view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] + current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) + + if color == "Groups": + color_dict = {} + # if no segment groups are given, do them all + segment_groups = [] + for sg in cell.morphology.segment_groups: + if sg.neuro_lex_id == neuro_lex_ids["section"]: + segment_groups.append(sg.id) + + ord_segs = cell.get_ordered_segments_in_groups( + segment_groups, check_parentage=False + ) + + for sgs, segs in ord_segs.items(): + c = get_next_hex_color() + for s in segs: + color_dict[s.id] = c + + # for lines/segments + points = [] + toconnect = [] + colors = [] + + for seg in cell.morphology.segments: + p = cell.get_actual_proximal(seg.id) + d = seg.distal + width = (p.diameter + d.diameter) / 2 + + if width < min_width: + width = min_width + + if plot_type == "Constant": + width = min_width + + seg_color = "white" + if color is None: + seg_color = get_next_hex_color() + elif color == "Groups": + try: + seg_color = color_dict[seg.id] + except KeyError: + print(f"Unbranched segment found: {seg.id}") + if seg.id in soma_segs: + seg_color = "green" + elif seg.id in axon_segs: + seg_color = "red" + elif seg.id in dend_segs: + seg_color = "blue" + elif color == "Default Groups": + if seg.id in soma_segs: + seg_color = "green" + elif seg.id in axon_segs: + seg_color = "red" + elif seg.id in dend_segs: + seg_color = "blue" + else: + seg_color = color + + # check if for a spherical segment + if ( + p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter + ): + scene.visuals.Markers(pos=(p.x, p.y, p.z), size=p.diameter, + spherical=True, edge_color=seg_color, + face_color=seg_color) + else: + if plot_type == "Constant": + points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) + colors.append(seg_color) + points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) + colors.append(seg_color) + toconnect.append([len(points) - 2, len(points) - 1]) + # every segment plotted individually + elif plot_type == "Detailed": + points = [] + points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) + colors.append(seg_color) + points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) + colors.append(seg_color) + toconnect.append([len(points) - 2, len(points) - 1]) + scene.Line(pos=points, color=colors, + connect=numpy.array(toconnect), parent=current_view.scene, + width=width) + + if plot_type == "Constant": + scene.Line(pos=points, color=colors, + connect=numpy.array(toconnect), parent=current_view.scene, + width=width) + + if not nogui: + app.run() + + def plot_2D_point_cells( offset: typing.List[float] = [0, 0], plane2d: str = "xy", diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 9c2e5c29..bea12b01 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -20,7 +20,8 @@ plot_2D_schematic, plot_segment_groups_curtain_plots, plot_2D_point_cells, - plot_3D_schematic + plot_3D_schematic, + plot_3D_cell_morphology, ) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -166,14 +167,20 @@ def test_3d_schematic_plotter(self): nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" nml_doc = read_neuroml2_file(nml_file) cell = nml_doc.cells[0] # type: neuroml.Cell - # remove the file first - plot_3D_schematic( cell, segment_groups=None, nogui=False, ) + def test_3d_plotter_vispy(self): + """Test plot_3D_cell_morphology_vispy function.""" + nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + plot_3D_cell_morphology(cell=cell, nogui=False, min_width=4, + color="Groups") + def test_3d_plotter_plotly(self): """Test plot_3D_cell_morphology_plotly function.""" nml_files = ["tests/plot/Cell_497232312.cell.nml", "tests/plot/test.cell.nml"] From 278b9666ee0f6029c84593e974f201b291fb7288 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 18:05:06 +0000 Subject: [PATCH 43/88] feat(plot-utils): add event handling to vispy scene --- pyneuroml/utils/plot.py | 133 ++++++++++++++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 18 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index bc59f34b..bad8d0cd 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -8,6 +8,7 @@ """ import logging +import textwrap import numpy import typing import random @@ -358,56 +359,152 @@ def create_new_vispy_canvas(view_min: typing.List[float], view_max: :returns: scene, view """ canvas = scene.SceneCanvas(keys='interactive', show=True, - bgcolor="black", size=(800, 600)) + bgcolor="white", size=(800, 600)) grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 - title_widget = scene.Label(title, color="white") - title_widget.height_max = 80 + view_center = (numpy.array(view_max) - numpy.array(view_min)) / 2 + + title_widget = scene.Label(title, color="black") + title_widget.height_max = 40 grid.add_widget(title_widget, row=0, col=0, col_span=2) + console_widget = scene.Console(text_color="black", font_size=8) + console_widget.height_max = 40 + grid.add_widget(console_widget, row=3, col=1, col_span=1) + console_text = "\tKeys: 0 reset, 5 changes camera" + yaxis = scene.AxisWidget(orientation='left', axis_label='Extent (Y)', axis_font_size=12, - axis_label_margin=50, - tick_label_margin=5) + axis_label_margin=60, + axis_color='black', + tick_color='black', + tick_label_margin=5, + text_color='black') yaxis.width_max = 80 grid.add_widget(yaxis, row=1, col=0) xaxis = scene.AxisWidget(orientation='bottom', axis_label='Extent (X)', axis_font_size=12, - axis_label_margin=50, + axis_label_margin=40, + axis_color='black', + tick_color='black', + text_color='black', tick_label_margin=5) - xaxis.height_max = 80 + xaxis.height_max = 60 grid.add_widget(xaxis, row=2, col=1) - right_padding = grid.add_widget(row=1, col=2, row_span=1) + right_padding = grid.add_widget(row=0, col=2, row_span=4) right_padding.width_max = 50 - view = grid.add_view(row=1, col=1, border_color='white', - camera="arcball") - view.camera.set_range( - x=(view_min[0], view_max[0]), - y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]) - ) + bottom_padding = grid.add_widget(row=4, col=0, col_span=3) + bottom_padding.height_max = 40 + + view = grid.add_view(row=1, col=1, border_color='black') + # create cameras + # https://vispy.org/gallery/scene/flipped_axis.html + cam1 = scene.cameras.PanZoomCamera(parent=view.scene, aspect=1, + name='PanZoom') + cam1.center = [view_center[0], view_center[1]] + cam2 = scene.cameras.TurntableCamera(parent=view.scene, name='Turntable') + cam2.center = view_center + cam3 = scene.cameras.ArcballCamera(parent=view.scene, name='Arcball') + cam3.center = view_center + cam4 = scene.cameras.FlyCamera(parent=view.scene, name='Fly') + cam4.center = view_center + cam4.autoroll = False + + cams = { + cam1: cam2, + cam2: cam3, + cam3: cam4, + cam4: cam1, + } + + cam_text = { + cam1: """ + Left mouse button: pans view; right mouse button or scroll: + zooms""", + cam2: """ + Left mouse button: orbits view around center point; right + mouse button or scroll: change zoom level; Shift + left mouse button: + translate center point; Shift + right mouse button: change field of + view""", + cam3: """ + Left mouse button: orbits view around center point; right + mouse button or scroll: change zoom level; Shift + left mouse button: + translate center point; Shift + right mouse button: change field of + view""", + cam4: """ + Arrow keys/WASD to move forward/backwards/left/right; F/C to + move up and down; Space to brake; Left mouse button/I/K/J/L to + control pitch and yaw; Q/E for rolling""" + } + + for acam in cams.values(): + acam.set_range( + x=(view_min[0], view_max[0]), + y=(view_min[1], view_max[1]), + z=(view_min[2], view_max[2]) + ) + # Fly is default + + view.camera = cam4 xaxis.link_view(view) yaxis.link_view(view) + console_widget.write(console_text + f" ({view.camera.name})") + + @canvas.events.key_press.connect + def vispy_on_key_press(event): + if event.text == '5': + state = view.camera.get_state() + view.camera = cams[view.camera] + console_widget.clear() + console_widget.write(console_text + f"({view.camera.name})") + try: + console_widget.write(textwrap.dedent(cam_text[view.camera]).replace("\n", " ")) + except KeyError: + pass + # PanZoom doesn't like it + if view.camera.name != "PanZoom": + try: + view.camera.center = state['center'] + view.camera.scale_factor = state['scale_factor'] + view.camera.fov = state['fov'] + except KeyError: + pass + else: + view.camera.set_range( + x=(view_min[0], view_max[0]), + y=(view_min[1], view_max[1]), + z=(view_min[2], view_max[2]) + ) + elif event.text == '0': + for acam in cams.values(): + acam.set_range( + x=(view_min[0], view_max[0]), + y=(view_min[1], view_max[1]), + z=(view_min[2], view_max[2]) + ) + # xyz axis for orientation # TODO improve placement if axes_pos: scene.Line([axes_pos, [axes_pos[0] + axes_length, axes_pos[1], axes_pos[2]]], - parent=view.scene, color="white", + parent=view.scene, color="black", width=axes_width) scene.Line([axes_pos, [axes_pos[1], axes_pos[1] + axes_length, axes_pos[2]]], - parent=view.scene, color="white", + parent=view.scene, color="black", width=axes_width) scene.Line([axes_pos, [axes_pos[1], axes_pos[1], axes_pos[2] + axes_length]], - parent=view.scene, color="white", + parent=view.scene, color="black", width=axes_width) return scene, view + + From f938035f7d3340f7d893b2fbac670bcb73453e31 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 18:05:29 +0000 Subject: [PATCH 44/88] feat(vispy-plot): add initial vispy network plotter --- pyneuroml/plot/PlotMorphology.py | 111 +++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index f2a96924..b77e357d 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -161,6 +161,117 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): ) +def plot_interactive_3D( + nml_file: str, + min_width: float = DEFAULTS["minwidth"], + verbose: bool = False, + plot_type: str = "Constant", + title: typing.Optional[str] = None, +): + """Plot interactive plots in 3D using Vispy + + https://vispy.org + + :param nml_file: path to NeuroML cell file + :type nml_file: str + :param min_width: minimum width for segments (useful for visualising very + thin segments): default 0.8um + :type min_width: float + :param verbose: show extra information (default: False) + :type verbose: bool + :param plot_type: type of plot, one of: + + - "Detailed": show detailed morphology taking into account each segment's + width + - "Constant": show morphology, but use constant line widths + - "Schematic": only plot each unbranched segment group as a straight + line, not following each segment + + This is only applicable for neuroml.Cell cells (ones with some + morphology) + + :type plot_type: str + :param title: title of plot + :type title: str + """ + if plot_type not in ["Detailed", "Constant", "Schematic"]: + raise ValueError( + "plot_type must be one of 'Detailed', 'Constant', or 'Schematic'" + ) + + if verbose: + print("Plotting %s" % nml_file) + + nml_model = read_neuroml2_file( + nml_file, + include_includes=True, + check_validity_pre_include=False, + verbose=False, + optimized=True, + ) + + ( + cell_id_vs_cell, + pop_id_vs_cell, + positions, + pop_id_vs_color, + pop_id_vs_radii, + ) = extract_position_info(nml_model, verbose) + + if title is None: + title = "2D plot of %s from %s" % (nml_model.networks[0].id, nml_file) + + if verbose: + logger.debug(f"positions: {positions}") + logger.debug(f"pop_id_vs_cell: {pop_id_vs_cell}") + logger.debug(f"cell_id_vs_cell: {cell_id_vs_cell}") + logger.debug(f"pop_id_vs_color: {pop_id_vs_color}") + logger.debug(f"pop_id_vs_radii: {pop_id_vs_radii}") + + current_scene, current_view = create_new_vispy_canvas([0, 0, 0], [1000, 1000, 1000], title) + + for pop_id in pop_id_vs_cell: + cell = pop_id_vs_cell[pop_id] + pos_pop = positions[pop_id] + + for cell_index in pos_pop: + pos = pos_pop[cell_index] + radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 + color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None + + print(f"Plotting {cell}") + + if cell is None: + # TODO: implement 3D for point cells + pass + else: + if plot_type == "Schematic": + plot_3D_schematic( + offset=pos, + cell=cell, + segment_groups=None, + labels=True, + verbose=verbose, + current_scene=current_scene, + current_view=current_view, + nogui=True, + ) + else: + plot_3D_cell_morphology( + offset=pos, + cell=cell, + color=color, + plot_type=plot_type, + verbose=verbose, + current_scene=current_scene, + current_view=current_view, + min_width=min_width, + nogui=True, + ) + + app.run() + + def plot_2D( nml_file: str, plane2d: str = "xy", From 878d246db79fdffc2c4ad248c45c134400183c1a Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 18:06:01 +0000 Subject: [PATCH 45/88] chore(plots): improve strings and hints --- pyneuroml/plot/PlotMorphology.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index b77e357d..c8ec4b6a 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -21,6 +21,7 @@ import numpy import matplotlib from matplotlib import pyplot as plt +from matplotlib.colors import to_rgba import plotly.graph_objects as go from pyneuroml.pynml import read_neuroml2_file @@ -117,14 +118,14 @@ def process_args(): type=str, metavar="", default=None, - help="Name of the image file", + help="Name of the image file, for 2D plot", ) parser.add_argument( "-square", action="store_true", default=DEFAULTS["square"], - help="Scale axes so that image is approximately square", + help="Scale axes so that image is approximately square, for 2D plot", ) return parser.parse_args() @@ -147,7 +148,7 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): a = build_namespace(DEFAULTS, a, **kwargs) print(a) if a.interactive3d: - plot_interactive_3D(a.nml_file, a.minwidth, a.v, a.nogui, a.save_to_file) + plot_interactive_3D(a.nml_file, a.minwidth, a.v, a.plot_type) else: plot_2D( a.nml_file, @@ -1409,9 +1410,7 @@ def plot_3D_schematic( labels: bool = False, width: float = 5., verbose: bool = False, - square: bool = False, nogui: bool = False, - viewer: napari.Viewer = None, title: str = "", current_scene: scene.SceneCanvas = None, current_view: scene.ViewBox = None, @@ -1450,8 +1449,6 @@ def plot_3D_schematic( :type width: float :param verbose: show extra information (default: False) :type verbose: bool - :param viewer: a napari.Viewer object - :type viewer: napari.Viewer :param title: title of plot :type title: str :param nogui: toggle if plot should be shown or not @@ -1547,8 +1544,6 @@ def plot_segment_groups_curtain_plots( :type labels: bool :param verbose: show extra information (default: False) :type verbose: bool - :param square: scale axes so that image is approximately square - :type square: bool :param nogui: do not show matplotlib GUI (default: false) :type nogui: bool :param save_to_file: optional filename to save generated morphology to From 3e2245700ca6dad17c9482f72535cda37541bed5 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 18:06:22 +0000 Subject: [PATCH 46/88] feat(test-plots): add initial vispy network plotter --- tests/plot/test_morphology_plot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index bea12b01..aa6d08d8 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -22,6 +22,7 @@ plot_2D_point_cells, plot_3D_schematic, plot_3D_cell_morphology, + plot_interactive_3D ) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -173,6 +174,11 @@ def test_3d_schematic_plotter(self): nogui=False, ) + def test_3d_morphology_plotter_vispy_network(self): + """Test plot_3D_cell_morphology_vispy function.""" + nml_file = "tests/plot/L23-example/TestNetwork.net.nml" + plot_interactive_3D(nml_file) + def test_3d_plotter_vispy(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" From c2db36b72c0a887e53ed3b8811cf3927f02c50c7 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 23 Feb 2023 20:57:13 +0000 Subject: [PATCH 47/88] feat: add vispy to deps --- requirements-development.txt | 1 + requirements-experimental.txt | 1 + requirements.txt | 1 + setup.py | 1 + 4 files changed, 4 insertions(+) diff --git a/requirements-development.txt b/requirements-development.txt index 40308a38..538f6f5a 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -2,6 +2,7 @@ argparse airspeed>=0.5.5 matplotlib graphviz +vispy NEURON ppft diff --git a/requirements-experimental.txt b/requirements-experimental.txt index 40308a38..538f6f5a 100644 --- a/requirements-experimental.txt +++ b/requirements-experimental.txt @@ -2,6 +2,7 @@ argparse airspeed>=0.5.5 matplotlib graphviz +vispy NEURON ppft diff --git a/requirements.txt b/requirements.txt index c988c09b..11d83547 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ argparse airspeed>=0.5.5 matplotlib graphviz +vispy modelspec>=0.1.3 NEURON ppft diff --git a/setup.py b/setup.py index 623c80e5..59677e21 100644 --- a/setup.py +++ b/setup.py @@ -98,6 +98,7 @@ "hdf5": ["tables"], "analysis": ["pyelectro"], "tune": ["neurotune", "ppft"], + "vispy": ["vispy"], }, classifiers=[ "Intended Audience :: Science/Research", From 5ccf0c713f38c603a18d73f23c42923178674f40 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 12:07:28 +0000 Subject: [PATCH 48/88] fix(setup.py): add pyqt5 as default backend for vispy --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 81066782..7b665516 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ "tune": ["neurotune @ git+https://github.com/NeuralEnsemble/neurotune.git@master#egg=neurotune", "inspyred @ git+https://github.com/aarongarrett/inspyred.git@master#egg=inspyred", "ppft"], - "vispy": ["vispy"], + "vispy": ["vispy", "pyqt5"], } extras["all"] = sum(extras.values(), []), From 970875b9047127c13863bc329f06638fa9403da3 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 12:07:58 +0000 Subject: [PATCH 49/88] chore: remove unneeded imports --- pyneuroml/plot/PlotMorphology.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index c8ec4b6a..dd89bb2a 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -16,12 +16,10 @@ import typing import logging -import napari from vispy import app, scene import numpy import matplotlib from matplotlib import pyplot as plt -from matplotlib.colors import to_rgba import plotly.graph_objects as go from pyneuroml.pynml import read_neuroml2_file From 28ac70529f0c8665d79de533bc88ac09584a220d Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 12:09:07 +0000 Subject: [PATCH 50/88] chore: format with black --- pyneuroml/utils/plot.py | 115 +++++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 49 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index bad8d0cd..50f8e593 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -336,10 +336,14 @@ def add_line_to_matplotlib_2D_plot(ax, xv, yv, width, color, axis_min_max): axis_min_max[1] = max(axis_min_max[1], xv[1]) -def create_new_vispy_canvas(view_min: typing.List[float], view_max: - typing.List[float], title: str = "", axes_pos: - typing.Optional[typing.List] = None, axes_length: float = 100, - axes_width: int = 2): +def create_new_vispy_canvas( + view_min: typing.List[float], + view_max: typing.List[float], + title: str = "", + axes_pos: typing.Optional[typing.List] = None, + axes_length: float = 100, + axes_width: int = 2, +): """Create a new vispy scene canvas with a view and optional axes lines Reference: https://vispy.org/gallery/scene/axes_plot.html @@ -358,8 +362,9 @@ def create_new_vispy_canvas(view_min: typing.List[float], view_max: :type axes_width: float :returns: scene, view """ - canvas = scene.SceneCanvas(keys='interactive', show=True, - bgcolor="white", size=(800, 600)) + canvas = scene.SceneCanvas( + keys="interactive", show=True, bgcolor="white", size=(800, 600) + ) grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 @@ -374,25 +379,29 @@ def create_new_vispy_canvas(view_min: typing.List[float], view_max: grid.add_widget(console_widget, row=3, col=1, col_span=1) console_text = "\tKeys: 0 reset, 5 changes camera" - yaxis = scene.AxisWidget(orientation='left', - axis_label='Extent (Y)', - axis_font_size=12, - axis_label_margin=60, - axis_color='black', - tick_color='black', - tick_label_margin=5, - text_color='black') + yaxis = scene.AxisWidget( + orientation="left", + axis_label="Extent (Y)", + axis_font_size=12, + axis_label_margin=60, + axis_color="black", + tick_color="black", + tick_label_margin=5, + text_color="black", + ) yaxis.width_max = 80 grid.add_widget(yaxis, row=1, col=0) - xaxis = scene.AxisWidget(orientation='bottom', - axis_label='Extent (X)', - axis_font_size=12, - axis_label_margin=40, - axis_color='black', - tick_color='black', - text_color='black', - tick_label_margin=5) + xaxis = scene.AxisWidget( + orientation="bottom", + axis_label="Extent (X)", + axis_font_size=12, + axis_label_margin=40, + axis_color="black", + tick_color="black", + text_color="black", + tick_label_margin=5, + ) xaxis.height_max = 60 grid.add_widget(xaxis, row=2, col=1) @@ -403,17 +412,16 @@ def create_new_vispy_canvas(view_min: typing.List[float], view_max: bottom_padding = grid.add_widget(row=4, col=0, col_span=3) bottom_padding.height_max = 40 - view = grid.add_view(row=1, col=1, border_color='black') + view = grid.add_view(row=1, col=1, border_color="black") # create cameras # https://vispy.org/gallery/scene/flipped_axis.html - cam1 = scene.cameras.PanZoomCamera(parent=view.scene, aspect=1, - name='PanZoom') + cam1 = scene.cameras.PanZoomCamera(parent=view.scene, aspect=1, name="PanZoom") cam1.center = [view_center[0], view_center[1]] - cam2 = scene.cameras.TurntableCamera(parent=view.scene, name='Turntable') + cam2 = scene.cameras.TurntableCamera(parent=view.scene, name="Turntable") cam2.center = view_center - cam3 = scene.cameras.ArcballCamera(parent=view.scene, name='Arcball') + cam3 = scene.cameras.ArcballCamera(parent=view.scene, name="Arcball") cam3.center = view_center - cam4 = scene.cameras.FlyCamera(parent=view.scene, name='Fly') + cam4 = scene.cameras.FlyCamera(parent=view.scene, name="Fly") cam4.center = view_center cam4.autoroll = False @@ -441,14 +449,14 @@ def create_new_vispy_canvas(view_min: typing.List[float], view_max: cam4: """ Arrow keys/WASD to move forward/backwards/left/right; F/C to move up and down; Space to brake; Left mouse button/I/K/J/L to - control pitch and yaw; Q/E for rolling""" + control pitch and yaw; Q/E for rolling""", } for acam in cams.values(): acam.set_range( x=(view_min[0], view_max[0]), y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]) + z=(view_min[2], view_max[2]), ) # Fly is default @@ -461,50 +469,59 @@ def create_new_vispy_canvas(view_min: typing.List[float], view_max: @canvas.events.key_press.connect def vispy_on_key_press(event): - if event.text == '5': + if event.text == "5": state = view.camera.get_state() view.camera = cams[view.camera] console_widget.clear() console_widget.write(console_text + f"({view.camera.name})") try: - console_widget.write(textwrap.dedent(cam_text[view.camera]).replace("\n", " ")) + console_widget.write( + textwrap.dedent(cam_text[view.camera]).replace("\n", " ") + ) except KeyError: pass # PanZoom doesn't like it if view.camera.name != "PanZoom": try: - view.camera.center = state['center'] - view.camera.scale_factor = state['scale_factor'] - view.camera.fov = state['fov'] + view.camera.center = state["center"] + view.camera.scale_factor = state["scale_factor"] + view.camera.fov = state["fov"] except KeyError: pass else: view.camera.set_range( x=(view_min[0], view_max[0]), y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]) + z=(view_min[2], view_max[2]), ) - elif event.text == '0': + elif event.text == "0": for acam in cams.values(): acam.set_range( x=(view_min[0], view_max[0]), y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]) + z=(view_min[2], view_max[2]), ) # xyz axis for orientation # TODO improve placement if axes_pos: - scene.Line([axes_pos, [axes_pos[0] + axes_length, axes_pos[1], axes_pos[2]]], - parent=view.scene, color="black", - width=axes_width) - scene.Line([axes_pos, [axes_pos[1], axes_pos[1] + axes_length, axes_pos[2]]], - parent=view.scene, color="black", - width=axes_width) - scene.Line([axes_pos, [axes_pos[1], axes_pos[1], axes_pos[2] + axes_length]], - parent=view.scene, color="black", - width=axes_width) + scene.Line( + [axes_pos, [axes_pos[0] + axes_length, axes_pos[1], axes_pos[2]]], + parent=view.scene, + color="black", + width=axes_width, + ) + scene.Line( + [axes_pos, [axes_pos[1], axes_pos[1] + axes_length, axes_pos[2]]], + parent=view.scene, + color="black", + width=axes_width, + ) + scene.Line( + [axes_pos, [axes_pos[1], axes_pos[1], axes_pos[2] + axes_length]], + parent=view.scene, + color="black", + width=axes_width, + ) return scene, view - - From 654aa155cb7f7de7aab4851011553712d6a4698f Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 12:20:08 +0000 Subject: [PATCH 51/88] improvement((vispy-canvas)): better align console text --- pyneuroml/utils/plot.py | 60 ++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 50f8e593..d804b7b1 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -340,6 +340,7 @@ def create_new_vispy_canvas( view_min: typing.List[float], view_max: typing.List[float], title: str = "", + console_font_size: float = 10, axes_pos: typing.Optional[typing.List] = None, axes_length: float = 100, axes_width: int = 2, @@ -374,10 +375,13 @@ def create_new_vispy_canvas( title_widget.height_max = 40 grid.add_widget(title_widget, row=0, col=0, col_span=2) - console_widget = scene.Console(text_color="black", font_size=8) - console_widget.height_max = 40 + console_widget = scene.Console( + text_color="black", + font_size=console_font_size, + ) + console_widget.height_max = 80 grid.add_widget(console_widget, row=3, col=1, col_span=1) - console_text = "\tKeys: 0 reset, 5 changes camera" + console_text = "Controls: reset view: 0; cycle camera: 5" yaxis = scene.AxisWidget( orientation="left", @@ -410,7 +414,7 @@ def create_new_vispy_canvas( right_padding.width_max = 50 bottom_padding = grid.add_widget(row=4, col=0, col_span=3) - bottom_padding.height_max = 40 + bottom_padding.height_max = 10 view = grid.add_view(row=1, col=1, border_color="black") # create cameras @@ -433,23 +437,31 @@ def create_new_vispy_canvas( } cam_text = { - cam1: """ - Left mouse button: pans view; right mouse button or scroll: - zooms""", - cam2: """ - Left mouse button: orbits view around center point; right - mouse button or scroll: change zoom level; Shift + left mouse button: - translate center point; Shift + right mouse button: change field of - view""", - cam3: """ - Left mouse button: orbits view around center point; right - mouse button or scroll: change zoom level; Shift + left mouse button: - translate center point; Shift + right mouse button: change field of - view""", - cam4: """ - Arrow keys/WASD to move forward/backwards/left/right; F/C to - move up and down; Space to brake; Left mouse button/I/K/J/L to - control pitch and yaw; Q/E for rolling""", + cam1: textwrap.dedent( + """ + Left mouse button: pans view; right mouse button or scroll: + zooms""" + ), + cam2: textwrap.dedent( + """ + Left mouse button: orbits view around center point; right mouse + button or scroll: change zoom level; Shift + left mouse button: + translate center point; Shift + right mouse button: change field of + view""" + ), + cam3: textwrap.dedent( + """ + Left mouse button: orbits view around center point; right + mouse button or scroll: change zoom level; Shift + left mouse + button: translate center point; Shift + right mouse button: change + field of view""" + ), + cam4: textwrap.dedent( + """ + Arrow keys/WASD to move forward/backwards/left/right; F/C to move + up and down; Space to brake; Left mouse button/I/K/J/L to control + pitch and yaw; Q/E for rolling""" + ), } for acam in cams.values(): @@ -473,11 +485,9 @@ def vispy_on_key_press(event): state = view.camera.get_state() view.camera = cams[view.camera] console_widget.clear() - console_widget.write(console_text + f"({view.camera.name})") + console_widget.write(console_text + f" ({view.camera.name})") try: - console_widget.write( - textwrap.dedent(cam_text[view.camera]).replace("\n", " ") - ) + console_widget.write(cam_text[view.camera].replace("\n", " ")) except KeyError: pass # PanZoom doesn't like it From b2a40f9556eab2fc21e82e722da2d95fc77fa9a2 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 12:23:27 +0000 Subject: [PATCH 52/88] feat(vispy-canvas): set NeuroML title --- pyneuroml/utils/plot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index d804b7b1..9dec4ab8 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -353,7 +353,7 @@ def create_new_vispy_canvas( :type view_min: [float, float, float] :param view_max: max view co-ordinates :type view_max: [float, float, float] - :param title: title of canvas + :param title: title of plot :type title: str :param axes_pos: position to draw axes at :type axes_pos: [float, float, float] @@ -364,7 +364,8 @@ def create_new_vispy_canvas( :returns: scene, view """ canvas = scene.SceneCanvas( - keys="interactive", show=True, bgcolor="white", size=(800, 600) + keys="interactive", show=True, bgcolor="white", size=(800, 600), + title="NeuroML viewer (VisPy)" ) grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 From eaecbd4f4666349b378d10b50c04d9f64a3dccea Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 13:20:42 +0000 Subject: [PATCH 53/88] fix(morph-plot): correctly use min_width cli argument --- pyneuroml/plot/PlotMorphology.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index dd89bb2a..6470bcfb 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -50,9 +50,9 @@ "saveToFile": None, "interactive3d": False, "plane2d": "xy", - "minwidth": 0.8, + "minWidth": 0.8, "square": False, - "plotType": "Detailed", + "plotType": "Constant", } @@ -95,12 +95,13 @@ def process_args(): type=str, metavar="", default=DEFAULTS["plotType"], - help="Plane to plot on for 2D plot", + help="Level of detail to plot in", ) parser.add_argument( "-minWidth", - action="store_true", - default=DEFAULTS["minwidth"], + type=float, + metavar="", + default=DEFAULTS["minWidth"], help="Minimum width of lines to use", ) @@ -146,12 +147,14 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): a = build_namespace(DEFAULTS, a, **kwargs) print(a) if a.interactive3d: - plot_interactive_3D(a.nml_file, a.minwidth, a.v, a.plot_type) + plot_interactive_3D(nml_file=a.nml_file, + min_width=a.min_width, + verbose=a.v, plot_type=a.plot_type) else: plot_2D( a.nml_file, a.plane2d, - a.minwidth, + a.min_width, a.v, a.nogui, a.save_to_file, @@ -162,7 +165,7 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): def plot_interactive_3D( nml_file: str, - min_width: float = DEFAULTS["minwidth"], + min_width: float = DEFAULTS["minWidth"], verbose: bool = False, plot_type: str = "Constant", title: typing.Optional[str] = None, @@ -274,7 +277,7 @@ def plot_interactive_3D( def plot_2D( nml_file: str, plane2d: str = "xy", - min_width: float = DEFAULTS["minwidth"], + min_width: float = DEFAULTS["minWidth"], verbose: bool = False, nogui: bool = False, save_to_file: typing.Optional[str] = None, @@ -555,7 +558,7 @@ def plot_2D_cell_morphology( verbose: bool = False, fig: matplotlib.figure.Figure = None, ax: matplotlib.axes.Axes = None, - min_width: float = DEFAULTS["minwidth"], + min_width: float = DEFAULTS["minWidth"], axis_min_max: typing.List = [float("inf"), -1 * float("inf")], scalebar: bool = False, nogui: bool = True, @@ -814,7 +817,7 @@ def plot_3D_cell_morphology( verbose: bool = False, current_scene: scene.SceneCanvas = None, current_view: scene.ViewBox = None, - min_width: float = DEFAULTS["minwidth"], + min_width: float = DEFAULTS["minWidth"], axis_min_max: typing.List = [float("inf"), -1 * float("inf")], nogui: bool = True, plot_type: str = "Constant", @@ -984,7 +987,8 @@ def plot_3D_cell_morphology( colors.append(seg_color) toconnect.append([len(points) - 2, len(points) - 1]) scene.Line(pos=points, color=colors, - connect=numpy.array(toconnect), parent=current_view.scene, + connect=numpy.array(toconnect), + parent=current_view.scene, width=width) if plot_type == "Constant": @@ -1005,7 +1009,7 @@ def plot_2D_point_cells( verbose: bool = False, fig: matplotlib.figure.Figure = None, ax: matplotlib.axes.Axes = None, - min_width: float = DEFAULTS["minwidth"], + min_width: float = DEFAULTS["minWidth"], axis_min_max: typing.List = [float("inf"), -1 * float("inf")], scalebar: bool = False, nogui: bool = True, From 2634680615c93c2e1ea305f30f14f66071995355 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 13:21:03 +0000 Subject: [PATCH 54/88] chore(morph-plot): print cell id, not full cell --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 6470bcfb..b9cd00b4 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -241,7 +241,7 @@ def plot_interactive_3D( radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None - print(f"Plotting {cell}") + print(f"Plotting {cell.id}") if cell is None: # TODO: implement 3D for point cells From 1201481f0ba517a1e3de04687e0304ed737d11eb Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 13:32:01 +0000 Subject: [PATCH 55/88] feat(3d-schematic): optimise using constant width and `connect` method --- pyneuroml/plot/PlotMorphology.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index b9cd00b4..06d1c443 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1476,13 +1476,17 @@ def plot_3D_schematic( ) # if no canvas is defined, define a new one - if current_scene is None: + if current_scene is None or current_view is None: seg0 = cell.morphology.segments[0] # type: Segment view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] seg1 = cell.morphology.segments[-1] # type: Segment view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) + points = [] + toconnect = [] + colors = [] + for sgid, segs in ord_segs.items(): sgobj = cell.get_segment_group(sgid) if sgobj.neuro_lex_id != neuro_lex_ids["section"]: @@ -1495,14 +1499,11 @@ def plot_3D_schematic( last_seg = segs[-1] # type: Segment first_prox = cell.get_actual_proximal(first_seg.id) - data = numpy.array([ - [offset[0] + first_prox.x, offset[1] + first_prox.y, offset[2] + first_prox.z], - [offset[0] + last_seg.distal.x, offset[1] + last_seg.distal.y, offset[2] + last_seg.distal.z] - ]) - - color = get_next_hex_color() - plot = scene.Line(data, parent=current_view.scene, color=color, - width=width) + points.append([offset[0] + first_prox.x, offset[1] + first_prox.y, offset[2] + first_prox.z]) + points.append([offset[0] + last_seg.distal.x, offset[1] + last_seg.distal.y, offset[2] + last_seg.distal.z]) + colors.append(get_next_hex_color()) + colors.append(get_next_hex_color()) + toconnect.append([len(points) - 2, len(points) - 1]) # TODO: needs fixing to show labels labels = False @@ -1511,9 +1512,12 @@ def plot_3D_schematic( xv=[offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], yv=[offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], zv=[offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], - color=color) + color=colors[-1]) alabel.font_size = 30 + scene.Line(points, parent=current_view.scene, color=colors, width=width, + connect=numpy.array(toconnect)) + if not nogui: app.run() From 9abedf81b09784ec8d13fa0b795a87789d0d606f Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 13:35:01 +0000 Subject: [PATCH 56/88] feat(vispy-canvas): add quit event --- pyneuroml/utils/plot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 9dec4ab8..707b5285 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -512,6 +512,8 @@ def vispy_on_key_press(event): y=(view_min[1], view_max[1]), z=(view_min[2], view_max[2]), ) + elif event.text == "q": + canvas.app.quit() # xyz axis for orientation # TODO improve placement From 3a840788bf29568c204c520c36172871cf74e192 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 13:35:25 +0000 Subject: [PATCH 57/88] feat(vispy-canvas): document quit keypress --- pyneuroml/utils/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 707b5285..5453e8c4 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -382,7 +382,7 @@ def create_new_vispy_canvas( ) console_widget.height_max = 80 grid.add_widget(console_widget, row=3, col=1, col_span=1) - console_text = "Controls: reset view: 0; cycle camera: 5" + console_text = "Controls: reset view: 0; cycle camera: 5; quit: q" yaxis = scene.AxisWidget( orientation="left", From 8b37f1652936717ff1a81285d66970d85010155d Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 13:35:42 +0000 Subject: [PATCH 58/88] feat(vispy-canvas): better align console text output --- pyneuroml/utils/plot.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 5453e8c4..8890f176 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -478,7 +478,11 @@ def create_new_vispy_canvas( xaxis.link_view(view) yaxis.link_view(view) - console_widget.write(console_text + f" ({view.camera.name})") + console_widget.write(console_text) + try: + console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) + except KeyError: + console_widget.write(f"Current camera: {view.camera.name}") @canvas.events.key_press.connect def vispy_on_key_press(event): @@ -486,11 +490,11 @@ def vispy_on_key_press(event): state = view.camera.get_state() view.camera = cams[view.camera] console_widget.clear() - console_widget.write(console_text + f" ({view.camera.name})") + console_widget.write(console_text) try: - console_widget.write(cam_text[view.camera].replace("\n", " ")) + console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) except KeyError: - pass + console_widget.write(f"Current camera: {view.camera.name}") # PanZoom doesn't like it if view.camera.name != "PanZoom": try: From c7cb2ba7d69dce5dc67f862fedb2a27d261975f4 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 14:36:16 +0000 Subject: [PATCH 59/88] feat(3d-plots): update title text --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 06d1c443..b71de2fb 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -221,7 +221,7 @@ def plot_interactive_3D( ) = extract_position_info(nml_model, verbose) if title is None: - title = "2D plot of %s from %s" % (nml_model.networks[0].id, nml_file) + title = f"{nml_model.networks[0].id} from {nml_file}" if verbose: logger.debug(f"positions: {positions}") From 0f91a2d017546fcfdee694b1b674570593342c6f Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 14:41:32 +0000 Subject: [PATCH 60/88] feat(plotting): improve logging/printing --- pyneuroml/plot/PlotMorphology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index b71de2fb..39e744cd 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -202,7 +202,7 @@ def plot_interactive_3D( ) if verbose: - print("Plotting %s" % nml_file) + print(f"Plotting {nml_file}") nml_model = read_neuroml2_file( nml_file, @@ -241,7 +241,7 @@ def plot_interactive_3D( radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None - print(f"Plotting {cell.id}") + logging.info(f"Plotting {cell.id}") if cell is None: # TODO: implement 3D for point cells From ea5a495d0ff48b9d2d72d05dce65074327030b69 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 15:24:19 +0000 Subject: [PATCH 61/88] feat(plotting): remove unused method arg --- pyneuroml/plot/PlotMorphology.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 39e744cd..f8bfbe71 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -395,7 +395,6 @@ def plot_2D( segment_groups=None, labels=True, plane2d=plane2d, - min_width=min_width, verbose=verbose, fig=fig, ax=ax, @@ -1004,12 +1003,11 @@ def plot_2D_point_cells( offset: typing.List[float] = [0, 0], plane2d: str = "xy", color: typing.Optional[str] = None, - soma_radius: float = 0.0, + soma_radius: float = 10.0, title: str = "", verbose: bool = False, fig: matplotlib.figure.Figure = None, ax: matplotlib.axes.Axes = None, - min_width: float = DEFAULTS["minWidth"], axis_min_max: typing.List = [float("inf"), -1 * float("inf")], scalebar: bool = False, nogui: bool = True, @@ -1039,15 +1037,12 @@ def plot_2D_point_cells( :type plane2d: str :param color: color to use for cell :type color: str - :param soma_radius: radius of soma (uses min_width if provided) + :param soma_radius: radius of soma :type soma_radius: float :param fig: a matplotlib.figure.Figure object to use :type fig: matplotlib.figure.Figure :param ax: a matplotlib.axes.Axes object to use :type ax: matplotlib.axes.Axes - :param min_width: minimum width for segments (useful for visualising very - thin segments): default 0.8um - :type min_width: float :param axis_min_max: min, max value of axes :type axis_min_max: [float, float] :param title: title of plot @@ -1071,8 +1066,6 @@ def plot_2D_point_cells( fig, ax = get_new_matplotlib_morph_plot(title) cell_color = get_next_hex_color() - if soma_radius is None: - soma_radius = 10 if plane2d == "xy": add_line_to_matplotlib_2D_plot( From 73fb2d3e9a87ac0a39a6ab1ca353f115184204f1 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 15:24:37 +0000 Subject: [PATCH 62/88] test(plotting): disable gui --- tests/plot/test_morphology_plot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index aa6d08d8..68ac637e 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -48,7 +48,7 @@ def test_2d_point_plotter(self): except FileNotFoundError: pass - plot_2D(nml_file, nogui=False, plane2d=plane, save_to_file=filename) + plot_2D(nml_file, nogui=True, plane2d=plane, save_to_file=filename) self.assertIsFile(filename) pl.Path(filename).unlink() @@ -90,7 +90,7 @@ def test_2d_morphology_plotter_data_overlay(self): values = (list(numpy.random.randint(50, 101, 1800)) + list(numpy.random.randint(0, 51, len(segs) - 1800))) data_dict = dict(zip(segs, values)) - plot_2D_cell_morphology(cell=cell, nogui=False, plane2d=plane, + plot_2D_cell_morphology(cell=cell, nogui=True, plane2d=plane, save_to_file=filename, overlay_data=data_dict, overlay_data_label="Test") @@ -171,20 +171,20 @@ def test_3d_schematic_plotter(self): plot_3D_schematic( cell, segment_groups=None, - nogui=False, + nogui=True, ) def test_3d_morphology_plotter_vispy_network(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/TestNetwork.net.nml" - plot_interactive_3D(nml_file) + plot_interactive_3D(nml_file, min_width=1) def test_3d_plotter_vispy(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" nml_doc = read_neuroml2_file(nml_file) cell = nml_doc.cells[0] # type: neuroml.Cell - plot_3D_cell_morphology(cell=cell, nogui=False, min_width=4, + plot_3D_cell_morphology(cell=cell, nogui=True, min_width=4, color="Groups") def test_3d_plotter_plotly(self): From 844406bfb8f2a12550953b0605589ab17e7072ad Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 15:24:56 +0000 Subject: [PATCH 63/88] feat(vispy): change key for quit `q` is used by the `Fly` camera for rolling --- pyneuroml/utils/plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 8890f176..eb5e2c11 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -382,7 +382,7 @@ def create_new_vispy_canvas( ) console_widget.height_max = 80 grid.add_widget(console_widget, row=3, col=1, col_span=1) - console_text = "Controls: reset view: 0; cycle camera: 5; quit: q" + console_text = "Controls: reset view: 0; cycle camera: 5; quit: 9" yaxis = scene.AxisWidget( orientation="left", @@ -516,7 +516,7 @@ def vispy_on_key_press(event): y=(view_min[1], view_max[1]), z=(view_min[2], view_max[2]), ) - elif event.text == "q": + elif event.text == "9": canvas.app.quit() # xyz axis for orientation From 5a1a3f5bdcfe4537edea4334e9fa76dc6a2f1295 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 18:09:31 +0000 Subject: [PATCH 64/88] feat(vispy-canvas): improve camera placement and disable extra cams --- pyneuroml/utils/plot.py | 126 +++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index eb5e2c11..9558cff7 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -15,7 +15,6 @@ import matplotlib from matplotlib import pyplot as plt from matplotlib.lines import Line2D -from matplotlib.axes import Axes from matplotlib.patches import Rectangle from matplotlib_scalebar.scalebar import ScaleBar from vispy import scene @@ -370,7 +369,8 @@ def create_new_vispy_canvas( grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 - view_center = (numpy.array(view_max) - numpy.array(view_min)) / 2 + view_center = (numpy.array(view_max) + numpy.array(view_min)) / 2 + logger.debug(f"Center is {view_center}") title_widget = scene.Label(title, color="black") title_widget.height_max = 40 @@ -382,7 +382,8 @@ def create_new_vispy_canvas( ) console_widget.height_max = 80 grid.add_widget(console_widget, row=3, col=1, col_span=1) - console_text = "Controls: reset view: 0; cycle camera: 5; quit: 9" + # console_text = "Controls: reset view: 0; cycle camera: 1, 2 (fwd/bwd); quit: 9" + console_text = "Controls: reset view: 0; quit: 9" yaxis = scene.AxisWidget( orientation="left", @@ -420,22 +421,21 @@ def create_new_vispy_canvas( view = grid.add_view(row=1, col=1, border_color="black") # create cameras # https://vispy.org/gallery/scene/flipped_axis.html - cam1 = scene.cameras.PanZoomCamera(parent=view.scene, aspect=1, name="PanZoom") + cam1 = scene.cameras.PanZoomCamera(parent=view.scene, name="PanZoom") cam1.center = [view_center[0], view_center[1]] + cam2 = scene.cameras.TurntableCamera(parent=view.scene, name="Turntable") cam2.center = view_center + cam3 = scene.cameras.ArcballCamera(parent=view.scene, name="Arcball") cam3.center = view_center + cam4 = scene.cameras.FlyCamera(parent=view.scene, name="Fly") cam4.center = view_center - cam4.autoroll = False + # keep z up + cam4.autoroll = True - cams = { - cam1: cam2, - cam2: cam3, - cam3: cam4, - cam4: cam1, - } + cams = [cam4] cam_text = { cam1: textwrap.dedent( @@ -465,80 +465,76 @@ def create_new_vispy_canvas( ), } - for acam in cams.values(): + for acam in cams: + x_width = abs(view_min[0] - view_max[0]) + y_width = abs(view_min[1] - view_max[1]) + z_width = abs(view_min[2] - view_max[2]) + acam.set_range( - x=(view_min[0], view_max[0]), - y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]), + x=(view_min[0] - x_width * 0.5, view_max[0] + x_width * 0.5), + y=(view_min[1] - y_width * 0.5, view_max[1] + y_width * 0.5), + z=(view_min[2] - z_width * 0.5, view_max[2] + z_width * 0.5), ) # Fly is default + cam_index = 0 + view.camera = cams[cam_index] - view.camera = cam4 xaxis.link_view(view) yaxis.link_view(view) + console_widget.write(f"Center: {view.camera.center}") console_widget.write(console_text) - try: - console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) - except KeyError: - console_widget.write(f"Current camera: {view.camera.name}") + console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) + + if axes_pos: + points = [ + axes_pos, # origin + [axes_pos[0] + axes_length, axes_pos[1], axes_pos[2]], + [axes_pos[0], axes_pos[1] + axes_length, axes_pos[2]], + [axes_pos[0], axes_pos[1], axes_pos[2] + axes_length], + ] + scene.Line( + points, + connect=numpy.array([[0, 1], [0, 2], [0, 3]]), + parent=view.scene, + color="black", + width=axes_width, + ) @canvas.events.key_press.connect def vispy_on_key_press(event): - if event.text == "5": - state = view.camera.get_state() - view.camera = cams[view.camera] - console_widget.clear() - console_widget.write(console_text) - try: - console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) - except KeyError: - console_widget.write(f"Current camera: {view.camera.name}") - # PanZoom doesn't like it - if view.camera.name != "PanZoom": - try: - view.camera.center = state["center"] - view.camera.scale_factor = state["scale_factor"] - view.camera.fov = state["fov"] - except KeyError: - pass - else: - view.camera.set_range( - x=(view_min[0], view_max[0]), - y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]), - ) - elif event.text == "0": + state = view.camera.get_state() + nonlocal cam_index + + """ + # Disable camera cycling. The fly camera looks sufficient. + # Keeping views/ranges same when switching cameras is not simple. + # Prev + if event.text == "1": + cam_index = (cam_index - 1) % len(cams) + view.camera = cams[cam_index] + # next + elif event.text == "2": + cam_index = (cam_index + 1) % len(cams) + view.camera = cams[cam_index] + """ + # reset + if event.text == "0": for acam in cams.values(): acam.set_range( x=(view_min[0], view_max[0]), y=(view_min[1], view_max[1]), z=(view_min[2], view_max[2]), ) + # quit elif event.text == "9": canvas.app.quit() - # xyz axis for orientation - # TODO improve placement - if axes_pos: - scene.Line( - [axes_pos, [axes_pos[0] + axes_length, axes_pos[1], axes_pos[2]]], - parent=view.scene, - color="black", - width=axes_width, - ) - scene.Line( - [axes_pos, [axes_pos[1], axes_pos[1] + axes_length, axes_pos[2]]], - parent=view.scene, - color="black", - width=axes_width, - ) - scene.Line( - [axes_pos, [axes_pos[1], axes_pos[1], axes_pos[2] + axes_length]], - parent=view.scene, - color="black", - width=axes_width, - ) + view.camera.center = state["center"] + console_widget.clear() + # console_widget.write(f"Center: {view.camera.center}") + console_widget.write(console_text) + console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) return scene, view From 6964d157ff36aaf57464a98cc018c4ca6935efd2 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 18:41:50 +0000 Subject: [PATCH 65/88] feat(vispy-canvas): allow not setting min/max view ranges --- pyneuroml/utils/plot.py | 56 +++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 9558cff7..bb8370ba 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -336,8 +336,8 @@ def add_line_to_matplotlib_2D_plot(ax, xv, yv, width, color, axis_min_max): def create_new_vispy_canvas( - view_min: typing.List[float], - view_max: typing.List[float], + view_min: typing.Optional[typing.List[float]] = None, + view_max: typing.Optional[typing.List[float]] = None, title: str = "", console_font_size: float = 10, axes_pos: typing.Optional[typing.List] = None, @@ -369,9 +369,6 @@ def create_new_vispy_canvas( grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 - view_center = (numpy.array(view_max) + numpy.array(view_min)) / 2 - logger.debug(f"Center is {view_center}") - title_widget = scene.Label(title, color="black") title_widget.height_max = 40 grid.add_widget(title_widget, row=0, col=0, col_span=2) @@ -419,19 +416,16 @@ def create_new_vispy_canvas( bottom_padding.height_max = 10 view = grid.add_view(row=1, col=1, border_color="black") + # create cameras # https://vispy.org/gallery/scene/flipped_axis.html cam1 = scene.cameras.PanZoomCamera(parent=view.scene, name="PanZoom") - cam1.center = [view_center[0], view_center[1]] cam2 = scene.cameras.TurntableCamera(parent=view.scene, name="Turntable") - cam2.center = view_center cam3 = scene.cameras.ArcballCamera(parent=view.scene, name="Arcball") - cam3.center = view_center cam4 = scene.cameras.FlyCamera(parent=view.scene, name="Fly") - cam4.center = view_center # keep z up cam4.autoroll = True @@ -465,17 +459,6 @@ def create_new_vispy_canvas( ), } - for acam in cams: - x_width = abs(view_min[0] - view_max[0]) - y_width = abs(view_min[1] - view_max[1]) - z_width = abs(view_min[2] - view_max[2]) - - acam.set_range( - x=(view_min[0] - x_width * 0.5, view_max[0] + x_width * 0.5), - y=(view_min[1] - y_width * 0.5, view_max[1] + y_width * 0.5), - z=(view_min[2] - z_width * 0.5, view_max[2] + z_width * 0.5), - ) - # Fly is default cam_index = 0 view.camera = cams[cam_index] @@ -483,6 +466,28 @@ def create_new_vispy_canvas( xaxis.link_view(view) yaxis.link_view(view) + if view_min is not None and view_max is not None: + view_center = (numpy.array(view_max) + numpy.array(view_min)) / 2 + logger.debug(f"Center is {view_center}") + cam1.center = [view_center[0], view_center[1]] + cam2.center = view_center + cam3.center = view_center + cam4.center = view_center + + for acam in cams: + x_width = abs(view_min[0] - view_max[0]) + y_width = abs(view_min[1] - view_max[1]) + z_width = abs(view_min[2] - view_max[2]) + + acam.set_range( + x=(view_min[0] - x_width * 0.5, view_max[0] + x_width * 0.5), + y=(view_min[1] - y_width * 0.5, view_max[1] + y_width * 0.5), + z=(view_min[2] - z_width * 0.5, view_max[2] + z_width * 0.5), + ) + + for acam in cams: + acam.set_default_state() + console_widget.write(f"Center: {view.camera.center}") console_widget.write(console_text) console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) @@ -504,10 +509,8 @@ def create_new_vispy_canvas( @canvas.events.key_press.connect def vispy_on_key_press(event): - state = view.camera.get_state() nonlocal cam_index - """ # Disable camera cycling. The fly camera looks sufficient. # Keeping views/ranges same when switching cameras is not simple. # Prev @@ -518,20 +521,13 @@ def vispy_on_key_press(event): elif event.text == "2": cam_index = (cam_index + 1) % len(cams) view.camera = cams[cam_index] - """ # reset if event.text == "0": - for acam in cams.values(): - acam.set_range( - x=(view_min[0], view_max[0]), - y=(view_min[1], view_max[1]), - z=(view_min[2], view_max[2]), - ) + view.camera.reset() # quit elif event.text == "9": canvas.app.quit() - view.camera.center = state["center"] console_widget.clear() # console_widget.write(f"Center: {view.camera.center}") console_widget.write(console_text) From 8574fecfe056cbb3a42efc1ee7166c5841c9cb2c Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 18:42:21 +0000 Subject: [PATCH 66/88] feat(3d-plot): calculate bounding box for view ranges --- pyneuroml/plot/PlotMorphology.py | 33 ++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index f8bfbe71..a32fc709 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -230,7 +230,27 @@ def plot_interactive_3D( logger.debug(f"pop_id_vs_color: {pop_id_vs_color}") logger.debug(f"pop_id_vs_radii: {pop_id_vs_radii}") - current_scene, current_view = create_new_vispy_canvas([0, 0, 0], [1000, 1000, 1000], title) + only_pos = [] + for posdict in positions.values(): + for poss in posdict.values(): + only_pos.append(poss) + + pos_array = numpy.array(only_pos) + x_min = numpy.min(pos_array[:, 0]) + y_min = numpy.min(pos_array[:, 1]) + z_min = numpy.min(pos_array[:, 2]) + x_max = numpy.max(pos_array[:, 0]) + y_max = numpy.max(pos_array[:, 1]) + z_max = numpy.max(pos_array[:, 2]) + view_min = [x_min, y_min, z_min] + view_max = [x_max, y_max, z_max] + current_scene, current_view = create_new_vispy_canvas( + view_min, + view_max, + title + ) + + logger.debug(f"figure extents are: {view_min}, {view_max}") for pop_id in pop_id_vs_cell: cell = pop_id_vs_cell[pop_id] @@ -241,11 +261,16 @@ def plot_interactive_3D( radius = pop_id_vs_radii[pop_id] if pop_id in pop_id_vs_radii else 10 color = pop_id_vs_color[pop_id] if pop_id in pop_id_vs_color else None - logging.info(f"Plotting {cell.id}") + try: + logging.info(f"Plotting {cell.id}") + except AttributeError: + logging.info(f"Plotting a point cell at {pos}") if cell is None: - # TODO: implement 3D for point cells - pass + print(f"plotting a point cell at {pos}") + plot_3D_point_cell(offset=pos, color=color, soma_radius=radius, + current_scene=current_scene, + current_view=current_view) else: if plot_type == "Schematic": plot_3D_schematic( From d1aa0ea1e10d917223b9bbad753982b0355f9b8a Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 24 Feb 2023 19:06:21 +0000 Subject: [PATCH 67/88] feat(vispy-canvas): do not autoroll --- pyneuroml/utils/plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index bb8370ba..03ea4db9 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -426,8 +426,8 @@ def create_new_vispy_canvas( cam3 = scene.cameras.ArcballCamera(parent=view.scene, name="Arcball") cam4 = scene.cameras.FlyCamera(parent=view.scene, name="Fly") - # keep z up - cam4.autoroll = True + # do not keep z up + cam4.autoroll = False cams = [cam4] From cca38ca2f9d386b14450f33777cae057e9dbe6ba Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 2 Mar 2023 10:03:39 +0000 Subject: [PATCH 68/88] WIP: squash --- pyneuroml/plot/PlotMorphology.py | 105 ++++++++++++++++++++++------- pyneuroml/utils/plot.py | 20 ++++++ tests/plot/test_morphology_plot.py | 16 ++++- 3 files changed, 113 insertions(+), 28 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index a32fc709..dd6b003d 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -35,13 +35,14 @@ add_scalebar_to_matplotlib_plot, add_line_to_matplotlib_2D_plot, create_new_vispy_canvas, + get_cell_bound_box, ) from neuroml import SegmentGroup, Cell, Segment from neuroml.neuro_lex_ids import neuro_lex_ids logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +logger.setLevel(logging.DEBUG) DEFAULTS = { @@ -223,27 +224,45 @@ def plot_interactive_3D( if title is None: title = f"{nml_model.networks[0].id} from {nml_file}" - if verbose: - logger.debug(f"positions: {positions}") - logger.debug(f"pop_id_vs_cell: {pop_id_vs_cell}") - logger.debug(f"cell_id_vs_cell: {cell_id_vs_cell}") - logger.debug(f"pop_id_vs_color: {pop_id_vs_color}") - logger.debug(f"pop_id_vs_radii: {pop_id_vs_radii}") + logger.debug(f"positions: {positions}") + logger.debug(f"pop_id_vs_cell: {pop_id_vs_cell}") + logger.debug(f"cell_id_vs_cell: {cell_id_vs_cell}") + logger.debug(f"pop_id_vs_color: {pop_id_vs_color}") + logger.debug(f"pop_id_vs_radii: {pop_id_vs_radii}") + + if len(positions.keys()) > 1: + only_pos = [] + for posdict in positions.values(): + for poss in posdict.values(): + only_pos.append(poss) + + pos_array = numpy.array(only_pos) + center = numpy.mean(pos_array) + x_min = numpy.min(pos_array[:, 0]) + x_max = numpy.max(pos_array[:, 0]) + x_len = abs(x_max - x_min) + + y_min = numpy.min(pos_array[:, 1]) + y_max = numpy.max(pos_array[:, 1]) + y_len = abs(y_max - y_min) + + z_min = numpy.min(pos_array[:, 2]) + z_max = numpy.max(pos_array[:, 2]) + z_len = abs(z_max - z_min) + + view_min = center - numpy.array([x_len, y_len, z_len]) + view_max = center + numpy.array([x_len, y_len, z_len]) + + else: + cell = list(pop_id_vs_cell.values())[0] + if cell is not None: + view_min, view_max = get_cell_bound_box(cell) + else: + logger.debug("Got a point cell") + pos = list((list(positions.values())[0]).values())[0] + view_min = list(numpy.array(pos) - 100) + view_min = list(numpy.array(pos) + 100) - only_pos = [] - for posdict in positions.values(): - for poss in posdict.values(): - only_pos.append(poss) - - pos_array = numpy.array(only_pos) - x_min = numpy.min(pos_array[:, 0]) - y_min = numpy.min(pos_array[:, 1]) - z_min = numpy.min(pos_array[:, 2]) - x_max = numpy.max(pos_array[:, 0]) - y_max = numpy.max(pos_array[:, 1]) - z_max = numpy.max(pos_array[:, 2]) - view_min = [x_min, y_min, z_min] - view_max = [x_max, y_max, z_max] current_scene, current_view = create_new_vispy_canvas( view_min, view_max, @@ -925,10 +944,7 @@ def plot_3D_cell_morphology( axon_segs = [] if current_scene is None or current_view is None: - seg0 = cell.morphology.segments[0] # type: Segment - view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] - seg1 = cell.morphology.segments[-1] # type: Segment - view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] + view_min, view_max = get_cell_bound_box(cell) current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) if color == "Groups": @@ -1540,6 +1556,45 @@ def plot_3D_schematic( app.run() +def plot_3D_point_cell( + offset: typing.List[float] = [0, 0, 0], + color: typing.Optional[str] = None, + soma_radius: float = 500.0, + current_scene: scene.SceneCanvas = None, + current_view: scene.ViewBox = None, + title: str = "", + verbose: bool = False, + nogui: bool = True, +): + """TODO: Docstring for plot_3D_point_cells. + + :param cell: TODO + :returns: TODO + + """ + if color is None: + color = "black" + + # if no canvas is defined, define a new one + view_min = list(numpy.array(offset) - 100) + view_min = list(numpy.array(offset) + 100) + if current_scene is None or current_view is None: + current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) + + scene.visuals.Markers( + pos=numpy.array([offset]), + size=soma_radius, + antialias=0, + face_color=color, + edge_color=color, + edge_width=100, + spherical=True, + ) + + if not nogui: + app.run() + + def plot_segment_groups_curtain_plots( cell: Cell, segment_groups: typing.List[SegmentGroup], diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 03ea4db9..5ab451c0 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -19,6 +19,7 @@ from matplotlib_scalebar.scalebar import ScaleBar from vispy import scene from vispy.scene import SceneCanvas +from neuroml import Cell, Segment logger = logging.getLogger(__name__) @@ -534,3 +535,22 @@ def vispy_on_key_press(event): console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) return scene, view + + +def get_cell_bound_box(cell: Cell): + """Get a boundary box for a cell + + :param cell: TODO + :returns: TODO + + """ + seg0 = cell.morphology.segments[0] # type: Segment + ex1 = numpy.array([seg0.distal.x, seg0.distal.y, seg0.distal.z]) + seg1 = cell.morphology.segments[-1] # type: Segment + ex2 = numpy.array([seg1.distal.x, seg1.distal.y, seg1.distal.z]) + center = (ex1 + ex2) / 2 + diff = numpy.linalg.norm(ex2 - ex1) + view_min = center - diff + view_max = center + diff + + return view_min, view_max diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 68ac637e..792acfd2 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -22,7 +22,8 @@ plot_2D_point_cells, plot_3D_schematic, plot_3D_cell_morphology, - plot_interactive_3D + plot_interactive_3D, + plot_3D_point_cell ) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -53,6 +54,12 @@ def test_2d_point_plotter(self): self.assertIsFile(filename) pl.Path(filename).unlink() + def test_3d_point_plotter(self): + """Test plot_2D_point_cells function.""" + nml_files = ["tests/plot/Izh2007Cells.net.nml"] + for nml_file in nml_files: + plot_interactive_3D(nml_file) + def test_2d_plotter(self): """Test plot_2D function.""" nml_files = ["tests/plot/Cell_497232312.cell.nml", "tests/plot/test.cell.nml"] @@ -184,8 +191,11 @@ def test_3d_plotter_vispy(self): nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" nml_doc = read_neuroml2_file(nml_file) cell = nml_doc.cells[0] # type: neuroml.Cell - plot_3D_cell_morphology(cell=cell, nogui=True, min_width=4, - color="Groups") + plot_3D_cell_morphology(cell=cell, nogui=False, min_width=4, + color="Groups", verbose=True) + """ + plot_interactive_3D(nml_file, min_width=1) + """ def test_3d_plotter_plotly(self): """Test plot_3D_cell_morphology_plotly function.""" From c09c0ab415194a1aaa885111ac2f438b942b411e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 7 Mar 2023 17:23:28 +0000 Subject: [PATCH 69/88] fix(vispy-plot): fix spherical plotting --- pyneuroml/plot/PlotMorphology.py | 50 ++++++----- tests/plot/test-spherical-soma.cell.nml | 115 ++++++++++++++++++++++++ tests/plot/test_morphology_plot.py | 16 ++-- 3 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 tests/plot/test-spherical-soma.cell.nml diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index dd6b003d..28935bac 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1004,32 +1004,36 @@ def plot_3D_cell_morphology( else: seg_color = color - # check if for a spherical segment + # check if for a spherical segment, add extra spherical node if ( p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter ): - scene.visuals.Markers(pos=(p.x, p.y, p.z), size=p.diameter, - spherical=True, edge_color=seg_color, - face_color=seg_color) - else: - if plot_type == "Constant": - points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) - colors.append(seg_color) - points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) - colors.append(seg_color) - toconnect.append([len(points) - 2, len(points) - 1]) - # every segment plotted individually - elif plot_type == "Detailed": - points = [] - points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) - colors.append(seg_color) - points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) - colors.append(seg_color) - toconnect.append([len(points) - 2, len(points) - 1]) - scene.Line(pos=points, color=colors, - connect=numpy.array(toconnect), - parent=current_view.scene, - width=width) + scene.Markers(pos=numpy.array([[offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]]), + size=numpy.array([p.diameter]), + spherical=True, edge_color="white", + face_color=seg_color, edge_width=0, scaling=True, + parent=current_view.scene) + + if plot_type == "Constant": + points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) + colors.append(seg_color) + points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) + colors.append(seg_color) + toconnect.append([len(points) - 2, len(points) - 1]) + # every segment plotted individually + elif plot_type == "Detailed": + points = [] + toconnect = [] + colors = [] + points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) + colors.append(seg_color) + points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) + colors.append(seg_color) + toconnect.append([len(points) - 2, len(points) - 1]) + scene.Line(pos=points, color=colors, + connect=numpy.array(toconnect), + parent=current_view.scene, + width=width) if plot_type == "Constant": scene.Line(pos=points, color=colors, diff --git a/tests/plot/test-spherical-soma.cell.nml b/tests/plot/test-spherical-soma.cell.nml new file mode 100644 index 00000000..697ca7e9 --- /dev/null +++ b/tests/plot/test-spherical-soma.cell.nml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default soma segment group for the cell + + + + + + + Default axon segment group for the cell + + + + + + + Default dendrite segment group for the cell + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 792acfd2..450a070c 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -191,11 +191,17 @@ def test_3d_plotter_vispy(self): nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" nml_doc = read_neuroml2_file(nml_file) cell = nml_doc.cells[0] # type: neuroml.Cell - plot_3D_cell_morphology(cell=cell, nogui=False, min_width=4, - color="Groups", verbose=True) - """ - plot_interactive_3D(nml_file, min_width=1) - """ + plot_3D_cell_morphology(cell=cell, nogui=True, + color="Groups", verbose=True, + plot_type="Constant") + + # test a circular soma + nml_file = "tests/plot/test-spherical-soma.cell.nml" + nml_doc = read_neuroml2_file(nml_file) + cell = nml_doc.cells[0] # type: neuroml.Cell + plot_3D_cell_morphology(cell=cell, nogui=True, + color="Groups", verbose=True, + plot_type="Constant") def test_3d_plotter_plotly(self): """Test plot_3D_cell_morphology_plotly function.""" From 30f4f758b44508ba8451382376e0b63da2038fe3 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 7 Mar 2023 17:41:31 +0000 Subject: [PATCH 70/88] chore: add pyqt5 to reqs --- requirements-development.txt | 1 + requirements-experimental.txt | 1 + requirements.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/requirements-development.txt b/requirements-development.txt index 538f6f5a..2d3b4d3c 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -3,6 +3,7 @@ airspeed>=0.5.5 matplotlib graphviz vispy +pyqt5 NEURON ppft diff --git a/requirements-experimental.txt b/requirements-experimental.txt index 538f6f5a..2d3b4d3c 100644 --- a/requirements-experimental.txt +++ b/requirements-experimental.txt @@ -3,6 +3,7 @@ airspeed>=0.5.5 matplotlib graphviz vispy +pyqt5 NEURON ppft diff --git a/requirements.txt b/requirements.txt index 11d83547..d40a25a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ airspeed>=0.5.5 matplotlib graphviz vispy +pyqt5 modelspec>=0.1.3 NEURON ppft From d1d32b179a5eb82a99a04e4e7b36bb7615dba96b Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 7 Mar 2023 17:48:08 +0000 Subject: [PATCH 71/88] chore: reset logging level [ci skip] --- pyneuroml/plot/PlotMorphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 28935bac..ba9f322b 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -42,7 +42,7 @@ logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) DEFAULTS = { From c3453f5f28d61fa6a06880b95477d4754a06411e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 7 Mar 2023 18:04:40 +0000 Subject: [PATCH 72/88] feat(vispy): set themes In the future, we may be able to expose these values so users can set them. --- pyneuroml/utils/plot.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 5ab451c0..53aa1645 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -26,6 +26,19 @@ logger.setLevel(logging.DEBUG) +VISPY_THEME = { + "light": { + "bg": "white", + "fg": "black" + }, + "dark": { + "bg": "black", + "fg": "white" + } +} +PYNEUROML_VISPY_THEME = "light" + + def add_text_to_vispy_3D_plot( scene: SceneCanvas, xv: typing.List[float], @@ -344,6 +357,7 @@ def create_new_vispy_canvas( axes_pos: typing.Optional[typing.List] = None, axes_length: float = 100, axes_width: int = 2, + theme=PYNEUROML_VISPY_THEME, ): """Create a new vispy scene canvas with a view and optional axes lines @@ -364,18 +378,18 @@ def create_new_vispy_canvas( :returns: scene, view """ canvas = scene.SceneCanvas( - keys="interactive", show=True, bgcolor="white", size=(800, 600), + keys="interactive", show=True, bgcolor=VISPY_THEME[theme]["bg"], size=(800, 600), title="NeuroML viewer (VisPy)" ) grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 - title_widget = scene.Label(title, color="black") + title_widget = scene.Label(title, color=VISPY_THEME[theme]["fg"]) title_widget.height_max = 40 grid.add_widget(title_widget, row=0, col=0, col_span=2) console_widget = scene.Console( - text_color="black", + text_color=VISPY_THEME[theme]["fg"], font_size=console_font_size, ) console_widget.height_max = 80 @@ -388,10 +402,10 @@ def create_new_vispy_canvas( axis_label="Extent (Y)", axis_font_size=12, axis_label_margin=60, - axis_color="black", - tick_color="black", + axis_color=VISPY_THEME[theme]["fg"], + tick_color=VISPY_THEME[theme]["fg"], tick_label_margin=5, - text_color="black", + text_color=VISPY_THEME[theme]["fg"], ) yaxis.width_max = 80 grid.add_widget(yaxis, row=1, col=0) @@ -401,9 +415,9 @@ def create_new_vispy_canvas( axis_label="Extent (X)", axis_font_size=12, axis_label_margin=40, - axis_color="black", - tick_color="black", - text_color="black", + axis_color=VISPY_THEME[theme]["fg"], + tick_color=VISPY_THEME[theme]["fg"], + text_color=VISPY_THEME[theme]["fg"], tick_label_margin=5, ) @@ -416,7 +430,7 @@ def create_new_vispy_canvas( bottom_padding = grid.add_widget(row=4, col=0, col_span=3) bottom_padding.height_max = 10 - view = grid.add_view(row=1, col=1, border_color="black") + view = grid.add_view(row=1, col=1, border_color=VISPY_THEME[theme]["fg"]) # create cameras # https://vispy.org/gallery/scene/flipped_axis.html @@ -504,7 +518,7 @@ def create_new_vispy_canvas( points, connect=numpy.array([[0, 1], [0, 2], [0, 3]]), parent=view.scene, - color="black", + color=VISPY_THEME[theme]["fg"], width=axes_width, ) From b67b4fe4adb283ec5be0cfa78f3d9160e984e957 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 8 Mar 2023 07:28:40 +0000 Subject: [PATCH 73/88] wip: add labels to vispy [ci skip] --- pyneuroml/plot/PlotMorphology.py | 24 ++++++++++++++++++------ pyneuroml/utils/plot.py | 3 ++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index ba9f322b..cfd7916d 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -1515,15 +1515,14 @@ def plot_3D_schematic( # if no canvas is defined, define a new one if current_scene is None or current_view is None: - seg0 = cell.morphology.segments[0] # type: Segment - view_min = [seg0.distal.x, seg0.distal.y, seg0.distal.z] - seg1 = cell.morphology.segments[-1] # type: Segment - view_max = [seg1.distal.x, seg1.distal.y, seg1.distal.z] + view_min, view_max = get_cell_bound_box(cell) current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) points = [] toconnect = [] colors = [] + text = [] + textpoints = [] for sgid, segs in ord_segs.items(): sgobj = cell.get_segment_group(sgid) @@ -1544,17 +1543,30 @@ def plot_3D_schematic( toconnect.append([len(points) - 2, len(points) - 1]) # TODO: needs fixing to show labels - labels = False if labels: - alabel = add_text_to_vispy_3D_plot(scene, text=f"{sgid}", + text.append(f"{sgid}") + textpoints.append( + [offset[0] + (first_prox.x + last_seg.distal.x)/2, + offset[1] + (first_prox.y + last_seg.distal.y)/2, + offset[2] + (first_prox.z + last_seg.distal.z)/2, + ] + ) + """ + + alabel = add_text_to_vispy_3D_plot(current_scene=current_view.scene, text=f"{sgid}", xv=[offset[0] + first_seg.proximal.x, offset[0] + last_seg.distal.x], yv=[offset[0] + first_seg.proximal.y, offset[0] + last_seg.distal.y], zv=[offset[1] + first_seg.proximal.z, offset[1] + last_seg.distal.z], color=colors[-1]) alabel.font_size = 30 + """ scene.Line(points, parent=current_view.scene, color=colors, width=width, connect=numpy.array(toconnect)) + if labels: + print("Text rendering") + scene.Text(text, pos=textpoints, font_size=30, color="black", + parent=current_view.scene) if not nogui: app.run() diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 53aa1645..38ec6b27 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -40,7 +40,7 @@ def add_text_to_vispy_3D_plot( - scene: SceneCanvas, + current_scene: SceneCanvas, xv: typing.List[float], yv: typing.List[float], zv: typing.List[float], @@ -77,6 +77,7 @@ def add_text_to_vispy_3D_plot( text=text, color=color, rotation=angle, + parent=current_scene ) From 1a9a927aafb6117c1eda793cefeb1f526f805f57 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 12:14:11 +0000 Subject: [PATCH 74/88] fix(plotting): fix point cell 3d plotting --- pyneuroml/plot/PlotMorphology.py | 138 ++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 49 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index cfd7916d..095dbf25 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -148,9 +148,12 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): a = build_namespace(DEFAULTS, a, **kwargs) print(a) if a.interactive3d: - plot_interactive_3D(nml_file=a.nml_file, - min_width=a.min_width, - verbose=a.v, plot_type=a.plot_type) + plot_interactive_3D( + nml_file=a.nml_file, + min_width=a.min_width, + verbose=a.v, + plot_type=a.plot_type, + ) else: plot_2D( a.nml_file, @@ -237,7 +240,13 @@ def plot_interactive_3D( only_pos.append(poss) pos_array = numpy.array(only_pos) - center = numpy.mean(pos_array) + center = numpy.array( + [ + numpy.mean(pos_array[:, 0]), + numpy.mean(pos_array[:, 1]), + numpy.mean(pos_array[:, 2]), + ] + ) x_min = numpy.min(pos_array[:, 0]) x_max = numpy.max(pos_array[:, 0]) x_len = abs(x_max - x_min) @@ -252,6 +261,7 @@ def plot_interactive_3D( view_min = center - numpy.array([x_len, y_len, z_len]) view_max = center + numpy.array([x_len, y_len, z_len]) + logger.debug(f"center, view_min, max are {center}, {view_min}, {view_max}") else: cell = list(pop_id_vs_cell.values())[0] @@ -260,14 +270,10 @@ def plot_interactive_3D( else: logger.debug("Got a point cell") pos = list((list(positions.values())[0]).values())[0] - view_min = list(numpy.array(pos) - 100) - view_min = list(numpy.array(pos) + 100) + view_min = list(numpy.array(pos)) + view_min = list(numpy.array(pos)) - current_scene, current_view = create_new_vispy_canvas( - view_min, - view_max, - title - ) + current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) logger.debug(f"figure extents are: {view_min}, {view_max}") @@ -287,9 +293,13 @@ def plot_interactive_3D( if cell is None: print(f"plotting a point cell at {pos}") - plot_3D_point_cell(offset=pos, color=color, soma_radius=radius, - current_scene=current_scene, - current_view=current_view) + plot_3D_point_cell( + offset=pos, + color=color, + soma_radius=radius, + current_scene=current_scene, + current_view=current_view, + ) else: if plot_type == "Schematic": plot_3D_schematic( @@ -1005,14 +1015,17 @@ def plot_3D_cell_morphology( seg_color = color # check if for a spherical segment, add extra spherical node - if ( - p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter - ): - scene.Markers(pos=numpy.array([[offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]]), - size=numpy.array([p.diameter]), - spherical=True, edge_color="white", - face_color=seg_color, edge_width=0, scaling=True, - parent=current_view.scene) + if p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter: + scene.Markers( + pos=numpy.array([[offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]]), + size=numpy.array([p.diameter]), + spherical=True, + edge_color="white", + face_color=seg_color, + edge_width=0, + scaling=True, + parent=current_view.scene, + ) if plot_type == "Constant": points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) @@ -1030,15 +1043,22 @@ def plot_3D_cell_morphology( points.append([offset[0] + d.x, offset[1] + d.y, offset[2] + d.z]) colors.append(seg_color) toconnect.append([len(points) - 2, len(points) - 1]) - scene.Line(pos=points, color=colors, - connect=numpy.array(toconnect), - parent=current_view.scene, - width=width) + scene.Line( + pos=points, + color=colors, + connect=numpy.array(toconnect), + parent=current_view.scene, + width=width, + ) if plot_type == "Constant": - scene.Line(pos=points, color=colors, - connect=numpy.array(toconnect), parent=current_view.scene, - width=width) + scene.Line( + pos=points, + color=colors, + connect=numpy.array(toconnect), + parent=current_view.scene, + width=width, + ) if not nogui: app.run() @@ -1448,7 +1468,7 @@ def plot_3D_schematic( segment_groups: typing.Optional[typing.List[SegmentGroup]], offset: typing.List[float] = [0, 0, 0], labels: bool = False, - width: float = 5., + width: float = 5.0, verbose: bool = False, nogui: bool = False, title: str = "", @@ -1536,8 +1556,20 @@ def plot_3D_schematic( last_seg = segs[-1] # type: Segment first_prox = cell.get_actual_proximal(first_seg.id) - points.append([offset[0] + first_prox.x, offset[1] + first_prox.y, offset[2] + first_prox.z]) - points.append([offset[0] + last_seg.distal.x, offset[1] + last_seg.distal.y, offset[2] + last_seg.distal.z]) + points.append( + [ + offset[0] + first_prox.x, + offset[1] + first_prox.y, + offset[2] + first_prox.z, + ] + ) + points.append( + [ + offset[0] + last_seg.distal.x, + offset[1] + last_seg.distal.y, + offset[2] + last_seg.distal.z, + ] + ) colors.append(get_next_hex_color()) colors.append(get_next_hex_color()) toconnect.append([len(points) - 2, len(points) - 1]) @@ -1546,10 +1578,11 @@ def plot_3D_schematic( if labels: text.append(f"{sgid}") textpoints.append( - [offset[0] + (first_prox.x + last_seg.distal.x)/2, - offset[1] + (first_prox.y + last_seg.distal.y)/2, - offset[2] + (first_prox.z + last_seg.distal.z)/2, - ] + [ + offset[0] + (first_prox.x + last_seg.distal.x) / 2, + offset[1] + (first_prox.y + last_seg.distal.y) / 2, + offset[2] + (first_prox.z + last_seg.distal.z) / 2, + ] ) """ @@ -1561,12 +1594,18 @@ def plot_3D_schematic( alabel.font_size = 30 """ - scene.Line(points, parent=current_view.scene, color=colors, width=width, - connect=numpy.array(toconnect)) + scene.Line( + points, + parent=current_view.scene, + color=colors, + width=width, + connect=numpy.array(toconnect), + ) if labels: print("Text rendering") - scene.Text(text, pos=textpoints, font_size=30, color="black", - parent=current_view.scene) + scene.Text( + text, pos=textpoints, font_size=30, color="black", parent=current_view.scene + ) if not nogui: app.run() @@ -1591,20 +1630,21 @@ def plot_3D_point_cell( if color is None: color = "black" - # if no canvas is defined, define a new one - view_min = list(numpy.array(offset) - 100) - view_min = list(numpy.array(offset) + 100) if current_scene is None or current_view is None: + # if no canvas is defined, define a new one + view_min = list(numpy.array(offset) - 100) + view_max = list(numpy.array(offset) + 100) current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) - scene.visuals.Markers( + scene.Markers( pos=numpy.array([offset]), - size=soma_radius, - antialias=0, - face_color=color, - edge_color=color, - edge_width=100, + size=numpy.array([soma_radius]), spherical=True, + edge_color=color, + face_color=color, + edge_width=1, + scaling=True, + parent=current_view.scene, ) if not nogui: From 2a95f226517125e11ae2d8bc99bc2dc9256cbf08 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 12:14:54 +0000 Subject: [PATCH 75/88] improvement(plot): tweak default scene range --- pyneuroml/utils/plot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 38ec6b27..588418d2 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -495,11 +495,12 @@ def create_new_vispy_canvas( y_width = abs(view_min[1] - view_max[1]) z_width = abs(view_min[2] - view_max[2]) - acam.set_range( - x=(view_min[0] - x_width * 0.5, view_max[0] + x_width * 0.5), - y=(view_min[1] - y_width * 0.5, view_max[1] + y_width * 0.5), - z=(view_min[2] - z_width * 0.5, view_max[2] + z_width * 0.5), - ) + xrange = ((view_min[0] - x_width * 0.02, view_max[0] + x_width * 0.02) if x_width > 0 else (-100, 100)) + yrange = ((view_min[1] - y_width * 0.02, view_max[1] + y_width * 0.02) if y_width > 0 else (-100, 100)) + zrange = ((view_min[2] - z_width * 0.02, view_max[2] + z_width * 0.02) if z_width > 0 else (-100, 100)) + logger.debug(f"{xrange}, {yrange}, {zrange}") + + acam.set_range(x=xrange, y=yrange, z=zrange) for acam in cams: acam.set_default_state() From 1e0627f7ddbc2a350b7cf9e05a3f8b2dd221465b Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 12:16:12 +0000 Subject: [PATCH 76/88] chore: format with black --- pyneuroml/utils/plot.py | 47 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 588418d2..8da6954d 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -27,14 +27,8 @@ VISPY_THEME = { - "light": { - "bg": "white", - "fg": "black" - }, - "dark": { - "bg": "black", - "fg": "white" - } + "light": {"bg": "white", "fg": "black"}, + "dark": {"bg": "black", "fg": "white"}, } PYNEUROML_VISPY_THEME = "light" @@ -77,7 +71,7 @@ def add_text_to_vispy_3D_plot( text=text, color=color, rotation=angle, - parent=current_scene + parent=current_scene, ) @@ -379,8 +373,11 @@ def create_new_vispy_canvas( :returns: scene, view """ canvas = scene.SceneCanvas( - keys="interactive", show=True, bgcolor=VISPY_THEME[theme]["bg"], size=(800, 600), - title="NeuroML viewer (VisPy)" + keys="interactive", + show=True, + bgcolor=VISPY_THEME[theme]["bg"], + size=(800, 600), + title="NeuroML viewer (VisPy)", ) grid = canvas.central_widget.add_grid(margin=10) grid.spacing = 0 @@ -495,9 +492,21 @@ def create_new_vispy_canvas( y_width = abs(view_min[1] - view_max[1]) z_width = abs(view_min[2] - view_max[2]) - xrange = ((view_min[0] - x_width * 0.02, view_max[0] + x_width * 0.02) if x_width > 0 else (-100, 100)) - yrange = ((view_min[1] - y_width * 0.02, view_max[1] + y_width * 0.02) if y_width > 0 else (-100, 100)) - zrange = ((view_min[2] - z_width * 0.02, view_max[2] + z_width * 0.02) if z_width > 0 else (-100, 100)) + xrange = ( + (view_min[0] - x_width * 0.02, view_max[0] + x_width * 0.02) + if x_width > 0 + else (-100, 100) + ) + yrange = ( + (view_min[1] - y_width * 0.02, view_max[1] + y_width * 0.02) + if y_width > 0 + else (-100, 100) + ) + zrange = ( + (view_min[2] - z_width * 0.02, view_max[2] + z_width * 0.02) + if z_width > 0 + else (-100, 100) + ) logger.debug(f"{xrange}, {yrange}, {zrange}") acam.set_range(x=xrange, y=yrange, z=zrange) @@ -507,7 +516,10 @@ def create_new_vispy_canvas( console_widget.write(f"Center: {view.camera.center}") console_widget.write(console_text) - console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) + console_widget.write( + f"Current camera: {view.camera.name}: " + + cam_text[view.camera].replace("\n", " ").strip() + ) if axes_pos: points = [ @@ -548,7 +560,10 @@ def vispy_on_key_press(event): console_widget.clear() # console_widget.write(f"Center: {view.camera.center}") console_widget.write(console_text) - console_widget.write(f"Current camera: {view.camera.name}: " + cam_text[view.camera].replace("\n", " ").strip()) + console_widget.write( + f"Current camera: {view.camera.name}: " + + cam_text[view.camera].replace("\n", " ").strip() + ) return scene, view From bfa2f63d0c18de1b0015ca50ee52da6003da263e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 12:18:23 +0000 Subject: [PATCH 77/88] chore: document method --- pyneuroml/utils/plot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 8da6954d..f3ca2110 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -571,8 +571,9 @@ def vispy_on_key_press(event): def get_cell_bound_box(cell: Cell): """Get a boundary box for a cell - :param cell: TODO - :returns: TODO + :param cell: cell to get boundary box for + :type cell: neuroml.Cell + :returns: tuple (min view, max view) """ seg0 = cell.morphology.segments[0] # type: Segment From 5afbdb71ba45c78d6df0dbc51b06eea8e291e539 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 14:11:14 +0000 Subject: [PATCH 78/88] chore(vispy): make turntable camera default --- pyneuroml/utils/plot.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index f3ca2110..6c7d09fd 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) +logger.setLevel(logging.INFO) VISPY_THEME = { @@ -392,8 +392,6 @@ def create_new_vispy_canvas( ) console_widget.height_max = 80 grid.add_widget(console_widget, row=3, col=1, col_span=1) - # console_text = "Controls: reset view: 0; cycle camera: 1, 2 (fwd/bwd); quit: 9" - console_text = "Controls: reset view: 0; quit: 9" yaxis = scene.AxisWidget( orientation="left", @@ -442,7 +440,11 @@ def create_new_vispy_canvas( # do not keep z up cam4.autoroll = False - cams = [cam4] + cams = [cam4, cam2] + if len(cams) > 1: + console_text = "Controls: reset view: 0; cycle camera: 1, 2 (fwd/bwd); quit: 9" + else: + console_text = "Controls: reset view: 0; quit: 9" cam_text = { cam1: textwrap.dedent( @@ -472,8 +474,8 @@ def create_new_vispy_canvas( ), } - # Fly is default - cam_index = 0 + # Turntable is default + cam_index = 1 view.camera = cams[cam_index] xaxis.link_view(view) From 083939f0271535b0cf7a728510398d58e1cb8ef9 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:09:06 +0000 Subject: [PATCH 79/88] improvement(vispy): remove axes from canvas The view has perspective, so the axes and the values they show don't mean anything. --- pyneuroml/utils/plot.py | 50 ++++++++--------------------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 6c7d09fd..88d065bb 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -383,50 +383,20 @@ def create_new_vispy_canvas( grid.spacing = 0 title_widget = scene.Label(title, color=VISPY_THEME[theme]["fg"]) - title_widget.height_max = 40 - grid.add_widget(title_widget, row=0, col=0, col_span=2) + title_widget.height_max = 80 + grid.add_widget(title_widget, row=0, col=0, col_span=1) console_widget = scene.Console( text_color=VISPY_THEME[theme]["fg"], font_size=console_font_size, ) console_widget.height_max = 80 - grid.add_widget(console_widget, row=3, col=1, col_span=1) - - yaxis = scene.AxisWidget( - orientation="left", - axis_label="Extent (Y)", - axis_font_size=12, - axis_label_margin=60, - axis_color=VISPY_THEME[theme]["fg"], - tick_color=VISPY_THEME[theme]["fg"], - tick_label_margin=5, - text_color=VISPY_THEME[theme]["fg"], - ) - yaxis.width_max = 80 - grid.add_widget(yaxis, row=1, col=0) - - xaxis = scene.AxisWidget( - orientation="bottom", - axis_label="Extent (X)", - axis_font_size=12, - axis_label_margin=40, - axis_color=VISPY_THEME[theme]["fg"], - tick_color=VISPY_THEME[theme]["fg"], - text_color=VISPY_THEME[theme]["fg"], - tick_label_margin=5, - ) - - xaxis.height_max = 60 - grid.add_widget(xaxis, row=2, col=1) + grid.add_widget(console_widget, row=2, col=0, col_span=1) - right_padding = grid.add_widget(row=0, col=2, row_span=4) - right_padding.width_max = 50 - - bottom_padding = grid.add_widget(row=4, col=0, col_span=3) + bottom_padding = grid.add_widget(row=3, col=0, col_span=1) bottom_padding.height_max = 10 - view = grid.add_view(row=1, col=1, border_color=VISPY_THEME[theme]["fg"]) + view = grid.add_view(row=1, col=0, border_color=None) # create cameras # https://vispy.org/gallery/scene/flipped_axis.html @@ -441,10 +411,11 @@ def create_new_vispy_canvas( cam4.autoroll = False cams = [cam4, cam2] + + # console text + console_text = "Controls: reset view: 0; quit: Esc/9" if len(cams) > 1: - console_text = "Controls: reset view: 0; cycle camera: 1, 2 (fwd/bwd); quit: 9" - else: - console_text = "Controls: reset view: 0; quit: 9" + console_text += "; cycle camera: 1, 2 (fwd/bwd)" cam_text = { cam1: textwrap.dedent( @@ -478,9 +449,6 @@ def create_new_vispy_canvas( cam_index = 1 view.camera = cams[cam_index] - xaxis.link_view(view) - yaxis.link_view(view) - if view_min is not None and view_max is not None: view_center = (numpy.array(view_max) + numpy.array(view_min)) / 2 logger.debug(f"Center is {view_center}") From 40ba536411e94ddf88ad3d78843d58b74cabff9d Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:10:09 +0000 Subject: [PATCH 80/88] perf(vispy-plotting): use single `Marker` object for all spheres This is more performant than having a Marker object for each sphere. Ideally, we should also use a single Line object to plot all lines in the same way, to improve performance. A TODO. --- pyneuroml/plot/PlotMorphology.py | 109 ++++++++++++++----------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 095dbf25..a84bcd96 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -224,6 +224,14 @@ def plot_interactive_3D( pop_id_vs_radii, ) = extract_position_info(nml_model, verbose) + # Collect all markers and only plot one markers object + # this is more efficient than multiple markers, one for each point. + # TODO: also collect all line points and only use one object rather than a + # new object for each cell. + marker_sizes = [] + marker_points = [] + marker_colors = [] + if title is None: title = f"{nml_model.networks[0].id} from {nml_file}" @@ -293,13 +301,9 @@ def plot_interactive_3D( if cell is None: print(f"plotting a point cell at {pos}") - plot_3D_point_cell( - offset=pos, - color=color, - soma_radius=radius, - current_scene=current_scene, - current_view=current_view, - ) + marker_points.extend([pos]) + marker_sizes.extend([radius]) + marker_colors.extend([color]) else: if plot_type == "Schematic": plot_3D_schematic( @@ -313,7 +317,7 @@ def plot_interactive_3D( nogui=True, ) else: - plot_3D_cell_morphology( + pts, sizes, colors = plot_3D_cell_morphology( offset=pos, cell=cell, color=color, @@ -324,8 +328,23 @@ def plot_interactive_3D( min_width=min_width, nogui=True, ) - - app.run() + marker_points.extend(pts) + marker_sizes.extend(sizes) + marker_colors.extend(colors) + + if len(marker_points) > 0: + scene.Markers( + pos=numpy.array(marker_points), + size=numpy.array(marker_sizes), + spherical=True, + face_color=marker_colors, + edge_color=marker_colors, + parent=current_view.scene, + scaling=True, + antialias=0 + ) + if not nogui: + app.run() def plot_2D( @@ -978,6 +997,10 @@ def plot_3D_cell_morphology( points = [] toconnect = [] colors = [] + # for any spheres which we plot as markers at once + marker_points = [] + marker_colors = [] + marker_sizes = [] for seg in cell.morphology.segments: p = cell.get_actual_proximal(seg.id) @@ -1016,16 +1039,9 @@ def plot_3D_cell_morphology( # check if for a spherical segment, add extra spherical node if p.x == d.x and p.y == d.y and p.z == d.z and p.diameter == d.diameter: - scene.Markers( - pos=numpy.array([[offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]]), - size=numpy.array([p.diameter]), - spherical=True, - edge_color="white", - face_color=seg_color, - edge_width=0, - scaling=True, - parent=current_view.scene, - ) + marker_points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) + marker_colors.append(seg_color) + marker_sizes.append(p.diameter) if plot_type == "Constant": points.append([offset[0] + p.x, offset[1] + p.y, offset[2] + p.z]) @@ -1061,7 +1077,20 @@ def plot_3D_cell_morphology( ) if not nogui: + # markers + if len(marker_points) > 0: + scene.Markers( + pos=numpy.array(marker_points), + size=numpy.array(marker_sizes), + spherical=True, + face_color=marker_colors, + edge_color=marker_colors, + parent=current_view.scene, + scaling=True, + antialias=0 + ) app.run() + return marker_points, marker_sizes, marker_colors def plot_2D_point_cells( @@ -1611,46 +1640,6 @@ def plot_3D_schematic( app.run() -def plot_3D_point_cell( - offset: typing.List[float] = [0, 0, 0], - color: typing.Optional[str] = None, - soma_radius: float = 500.0, - current_scene: scene.SceneCanvas = None, - current_view: scene.ViewBox = None, - title: str = "", - verbose: bool = False, - nogui: bool = True, -): - """TODO: Docstring for plot_3D_point_cells. - - :param cell: TODO - :returns: TODO - - """ - if color is None: - color = "black" - - if current_scene is None or current_view is None: - # if no canvas is defined, define a new one - view_min = list(numpy.array(offset) - 100) - view_max = list(numpy.array(offset) + 100) - current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) - - scene.Markers( - pos=numpy.array([offset]), - size=numpy.array([soma_radius]), - spherical=True, - edge_color=color, - face_color=color, - edge_width=1, - scaling=True, - parent=current_view.scene, - ) - - if not nogui: - app.run() - - def plot_segment_groups_curtain_plots( cell: Cell, segment_groups: typing.List[SegmentGroup], From 52760d5cd03b05a7f9e96ec32fdcdfb347911ad2 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:11:19 +0000 Subject: [PATCH 81/88] feat(vispy): expose theme setting to all methods --- pyneuroml/plot/PlotMorphology.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index a84bcd96..ac277266 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -54,6 +54,7 @@ "minWidth": 0.8, "square": False, "plotType": "Constant", + "theme": "light" } @@ -98,6 +99,13 @@ def process_args(): default=DEFAULTS["plotType"], help="Level of detail to plot in", ) + parser.add_argument( + "-theme", + type=str, + metavar="", + default=DEFAULTS["theme"], + help="Theme to use for interactive 3d plotting", + ) parser.add_argument( "-minWidth", type=float, @@ -153,6 +161,7 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): min_width=a.min_width, verbose=a.v, plot_type=a.plot_type, + theme=a.theme ) else: plot_2D( @@ -173,6 +182,8 @@ def plot_interactive_3D( verbose: bool = False, plot_type: str = "Constant", title: typing.Optional[str] = None, + theme: str = "light", + nogui: bool = False ): """Plot interactive plots in 3D using Vispy @@ -199,6 +210,10 @@ def plot_interactive_3D( :type plot_type: str :param title: title of plot :type title: str + :param theme: theme to use (light/dark) + :type theme: str + :param nogui: toggle showing gui (for testing only) + :type nogui: bool """ if plot_type not in ["Detailed", "Constant", "Schematic"]: raise ValueError( @@ -281,7 +296,8 @@ def plot_interactive_3D( view_min = list(numpy.array(pos)) view_min = list(numpy.array(pos)) - current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) + current_scene, current_view = create_new_vispy_canvas(view_min, view_max, + title, theme=theme) logger.debug(f"figure extents are: {view_min}, {view_max}") @@ -893,6 +909,7 @@ def plot_3D_cell_morphology( axis_min_max: typing.List = [float("inf"), -1 * float("inf")], nogui: bool = True, plot_type: str = "Constant", + theme="light" ): """Plot the detailed 3D morphology of a cell using vispy. https://vispy.org/ @@ -951,6 +968,8 @@ def plot_3D_cell_morphology( morphology) :type plot_type: str + :param theme: theme to use (dark/light) + :type theme: str :raises: ValueError if `cell` is None """ @@ -974,7 +993,9 @@ def plot_3D_cell_morphology( if current_scene is None or current_view is None: view_min, view_max = get_cell_bound_box(cell) - current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) + current_scene, current_view = create_new_vispy_canvas(view_min, + view_max, title, + theme=theme) if color == "Groups": color_dict = {} @@ -1503,6 +1524,7 @@ def plot_3D_schematic( title: str = "", current_scene: scene.SceneCanvas = None, current_view: scene.ViewBox = None, + theme: str = "light" ) -> None: """Plot a 3D schematic of the provided segment groups in Napari as a new layer.. @@ -1547,6 +1569,8 @@ def plot_3D_schematic( :type current_scene: SceneCanvas :param current_view: vispy viewbox to use :type current_view: ViewBox + :param theme: theme to use (light/dark) + :type theme: str """ if title == "": title = f"3D schematic of segment groups from {cell.id}" @@ -1565,7 +1589,9 @@ def plot_3D_schematic( # if no canvas is defined, define a new one if current_scene is None or current_view is None: view_min, view_max = get_cell_bound_box(cell) - current_scene, current_view = create_new_vispy_canvas(view_min, view_max, title) + current_scene, current_view = create_new_vispy_canvas(view_min, + view_max, title, + theme=theme) points = [] toconnect = [] From 27b8dd09001a9096a4b4efcc498166090e50e59c Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:27:32 +0000 Subject: [PATCH 82/88] feat(vispy): implement rotation for Turntable camera --- pyneuroml/utils/plot.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyneuroml/utils/plot.py b/pyneuroml/utils/plot.py index 88d065bb..ebe4a72e 100644 --- a/pyneuroml/utils/plot.py +++ b/pyneuroml/utils/plot.py @@ -19,6 +19,7 @@ from matplotlib_scalebar.scalebar import ScaleBar from vispy import scene from vispy.scene import SceneCanvas +from vispy.app import Timer from neuroml import Cell, Segment @@ -428,7 +429,7 @@ def create_new_vispy_canvas( Left mouse button: orbits view around center point; right mouse button or scroll: change zoom level; Shift + left mouse button: translate center point; Shift + right mouse button: change field of - view""" + view; r/R: view rotation animation""" ), cam3: textwrap.dedent( """ @@ -506,6 +507,11 @@ def create_new_vispy_canvas( width=axes_width, ) + def vispy_rotate(self): + view.camera.orbit(azim=1, elev=0) + + rotation_timer = Timer(connect=vispy_rotate) + @canvas.events.key_press.connect def vispy_on_key_press(event): nonlocal cam_index @@ -520,8 +526,15 @@ def vispy_on_key_press(event): elif event.text == "2": cam_index = (cam_index + 1) % len(cams) view.camera = cams[cam_index] + # for turntable only: rotate animation + elif event.text == "R" or event.text == "r": + if view.camera == cam2: + if rotation_timer.running: + rotation_timer.stop() + else: + rotation_timer.start() # reset - if event.text == "0": + elif event.text == "0": view.camera.reset() # quit elif event.text == "9": From 4e71eda94d30db115ed801852c3a0a3de98826b7 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:34:53 +0000 Subject: [PATCH 83/88] chore: complete comment --- pyneuroml/plot/PlotMorphology.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index ac277266..b4fdba49 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -242,7 +242,8 @@ def plot_interactive_3D( # Collect all markers and only plot one markers object # this is more efficient than multiple markers, one for each point. # TODO: also collect all line points and only use one object rather than a - # new object for each cell. + # new object for each cell: will only work for the case where all lines + # have the same width marker_sizes = [] marker_points = [] marker_colors = [] From 9ef580e2d6ecae8a7153093f40fd8b83f0ae086b Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:42:39 +0000 Subject: [PATCH 84/88] chore: format with black --- pyneuroml/plot/PlotMorphology.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index b4fdba49..8c2dff1b 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -54,7 +54,7 @@ "minWidth": 0.8, "square": False, "plotType": "Constant", - "theme": "light" + "theme": "light", } @@ -161,7 +161,7 @@ def plot_from_console(a: typing.Optional[typing.Any] = None, **kwargs: str): min_width=a.min_width, verbose=a.v, plot_type=a.plot_type, - theme=a.theme + theme=a.theme, ) else: plot_2D( @@ -183,7 +183,7 @@ def plot_interactive_3D( plot_type: str = "Constant", title: typing.Optional[str] = None, theme: str = "light", - nogui: bool = False + nogui: bool = False, ): """Plot interactive plots in 3D using Vispy @@ -297,8 +297,9 @@ def plot_interactive_3D( view_min = list(numpy.array(pos)) view_min = list(numpy.array(pos)) - current_scene, current_view = create_new_vispy_canvas(view_min, view_max, - title, theme=theme) + current_scene, current_view = create_new_vispy_canvas( + view_min, view_max, title, theme=theme + ) logger.debug(f"figure extents are: {view_min}, {view_max}") @@ -358,7 +359,7 @@ def plot_interactive_3D( edge_color=marker_colors, parent=current_view.scene, scaling=True, - antialias=0 + antialias=0, ) if not nogui: app.run() @@ -910,7 +911,7 @@ def plot_3D_cell_morphology( axis_min_max: typing.List = [float("inf"), -1 * float("inf")], nogui: bool = True, plot_type: str = "Constant", - theme="light" + theme="light", ): """Plot the detailed 3D morphology of a cell using vispy. https://vispy.org/ @@ -994,9 +995,9 @@ def plot_3D_cell_morphology( if current_scene is None or current_view is None: view_min, view_max = get_cell_bound_box(cell) - current_scene, current_view = create_new_vispy_canvas(view_min, - view_max, title, - theme=theme) + current_scene, current_view = create_new_vispy_canvas( + view_min, view_max, title, theme=theme + ) if color == "Groups": color_dict = {} @@ -1109,7 +1110,7 @@ def plot_3D_cell_morphology( edge_color=marker_colors, parent=current_view.scene, scaling=True, - antialias=0 + antialias=0, ) app.run() return marker_points, marker_sizes, marker_colors @@ -1525,7 +1526,7 @@ def plot_3D_schematic( title: str = "", current_scene: scene.SceneCanvas = None, current_view: scene.ViewBox = None, - theme: str = "light" + theme: str = "light", ) -> None: """Plot a 3D schematic of the provided segment groups in Napari as a new layer.. @@ -1590,9 +1591,9 @@ def plot_3D_schematic( # if no canvas is defined, define a new one if current_scene is None or current_view is None: view_min, view_max = get_cell_bound_box(cell) - current_scene, current_view = create_new_vispy_canvas(view_min, - view_max, title, - theme=theme) + current_scene, current_view = create_new_vispy_canvas( + view_min, view_max, title, theme=theme + ) points = [] toconnect = [] From 8376d1f3aa9d7344192f3a9fc24c4200f65f4c1c Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 15:43:03 +0000 Subject: [PATCH 85/88] chore: format with black --- tests/plot/test_morphology_plot.py | 33 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index 450a070c..aac50c1c 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -23,7 +23,6 @@ plot_3D_schematic, plot_3D_cell_morphology, plot_interactive_3D, - plot_3D_point_cell ) from pyneuroml.pynml import read_neuroml2_file from .. import BaseTestCase @@ -58,7 +57,7 @@ def test_3d_point_plotter(self): """Test plot_2D_point_cells function.""" nml_files = ["tests/plot/Izh2007Cells.net.nml"] for nml_file in nml_files: - plot_interactive_3D(nml_file) + plot_interactive_3D(nml_file, theme="dark", nogui=True) def test_2d_plotter(self): """Test plot_2D function.""" @@ -94,13 +93,19 @@ def test_2d_morphology_plotter_data_overlay(self): pass segs = cell.get_all_segments_in_group("all") - values = (list(numpy.random.randint(50, 101, 1800)) + list(numpy.random.randint(0, 51, len(segs) - 1800))) + values = list(numpy.random.randint(50, 101, 1800)) + list( + numpy.random.randint(0, 51, len(segs) - 1800) + ) data_dict = dict(zip(segs, values)) - plot_2D_cell_morphology(cell=cell, nogui=True, plane2d=plane, - save_to_file=filename, - overlay_data=data_dict, - overlay_data_label="Test") + plot_2D_cell_morphology( + cell=cell, + nogui=True, + plane2d=plane, + save_to_file=filename, + overlay_data=data_dict, + overlay_data_label="Test", + ) self.assertIsFile(filename) pl.Path(filename).unlink() @@ -184,24 +189,24 @@ def test_3d_schematic_plotter(self): def test_3d_morphology_plotter_vispy_network(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/TestNetwork.net.nml" - plot_interactive_3D(nml_file, min_width=1) + plot_interactive_3D(nml_file, min_width=1, nogui=True, theme="dark") def test_3d_plotter_vispy(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" nml_doc = read_neuroml2_file(nml_file) cell = nml_doc.cells[0] # type: neuroml.Cell - plot_3D_cell_morphology(cell=cell, nogui=True, - color="Groups", verbose=True, - plot_type="Constant") + plot_3D_cell_morphology( + cell=cell, nogui=True, color="Groups", verbose=True, plot_type="Constant" + ) # test a circular soma nml_file = "tests/plot/test-spherical-soma.cell.nml" nml_doc = read_neuroml2_file(nml_file) cell = nml_doc.cells[0] # type: neuroml.Cell - plot_3D_cell_morphology(cell=cell, nogui=True, - color="Groups", verbose=True, - plot_type="Constant") + plot_3D_cell_morphology( + cell=cell, nogui=True, color="Groups", verbose=True, plot_type="Constant" + ) def test_3d_plotter_plotly(self): """Test plot_3D_cell_morphology_plotly function.""" From 01234a34c22753262d53f5e028aea6aa58675644 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 14 Mar 2023 17:20:21 +0000 Subject: [PATCH 86/88] test: mark vispy tests to disable them on GitHub CI --- tests/plot/test_morphology_plot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/plot/test_morphology_plot.py b/tests/plot/test_morphology_plot.py index aac50c1c..c7ba0330 100644 --- a/tests/plot/test_morphology_plot.py +++ b/tests/plot/test_morphology_plot.py @@ -11,6 +11,7 @@ import logging import pathlib as pl +import pytest import numpy import neuroml from pyneuroml.plot.PlotMorphology import ( @@ -53,6 +54,7 @@ def test_2d_point_plotter(self): self.assertIsFile(filename) pl.Path(filename).unlink() + @pytest.mark.localonly def test_3d_point_plotter(self): """Test plot_2D_point_cells function.""" nml_files = ["tests/plot/Izh2007Cells.net.nml"] @@ -175,6 +177,7 @@ def test_2d_schematic_plotter_network(self): self.assertIsFile(filename) pl.Path(filename).unlink() + @pytest.mark.localonly def test_3d_schematic_plotter(self): """Test plot_3D_schematic plotter function.""" nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" @@ -186,11 +189,13 @@ def test_3d_schematic_plotter(self): nogui=True, ) + @pytest.mark.localonly def test_3d_morphology_plotter_vispy_network(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/TestNetwork.net.nml" plot_interactive_3D(nml_file, min_width=1, nogui=True, theme="dark") + @pytest.mark.localonly def test_3d_plotter_vispy(self): """Test plot_3D_cell_morphology_vispy function.""" nml_file = "tests/plot/L23-example/HL23PYR.cell.nml" From 1d1c63d52872232149b27fc549e9024ce4ac4ef2 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Mar 2023 13:08:07 +0000 Subject: [PATCH 87/88] improvement(3d-plotting): remove edge for spheres --- pyneuroml/plot/PlotMorphology.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 8c2dff1b..6d539a89 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -357,6 +357,7 @@ def plot_interactive_3D( spherical=True, face_color=marker_colors, edge_color=marker_colors, + edge_width=0, parent=current_view.scene, scaling=True, antialias=0, @@ -1108,6 +1109,7 @@ def plot_3D_cell_morphology( spherical=True, face_color=marker_colors, edge_color=marker_colors, + edge_width=0, parent=current_view.scene, scaling=True, antialias=0, From d1c8e42b1b546dd65ef1a9b660007d35ab268781 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 15 Mar 2023 13:08:22 +0000 Subject: [PATCH 88/88] chore: remove print statement --- pyneuroml/plot/PlotMorphology.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyneuroml/plot/PlotMorphology.py b/pyneuroml/plot/PlotMorphology.py index 6d539a89..7d3a17e9 100644 --- a/pyneuroml/plot/PlotMorphology.py +++ b/pyneuroml/plot/PlotMorphology.py @@ -318,7 +318,6 @@ def plot_interactive_3D( logging.info(f"Plotting a point cell at {pos}") if cell is None: - print(f"plotting a point cell at {pos}") marker_points.extend([pos]) marker_sizes.extend([radius]) marker_colors.extend([color])