Skip to content

swc_io

Source code in navis/io/swc_io.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class SwcReader(base.BaseReader):
    def __init__(
        self,
        connector_labels: Optional[Dict[str, Union[str, int]]] = None,
        soma_label: Union[str, int] = 1,
        delimiter: str = DEFAULT_DELIMITER,
        precision: int = DEFAULT_PRECISION,
        read_meta: bool = False,
        fmt: str = DEFAULT_FMT,
        errors: str = "raise",
        attrs: Optional[Dict[str, Any]] = None,
    ):
        if not fmt.endswith(".swc"):
            raise ValueError('`fmt` must end with ".swc"')

        super().__init__(
            fmt=fmt, attrs=attrs, file_ext=".swc", errors=errors, name_fallback="SWC"
        )
        self.connector_labels = connector_labels or dict()
        self.soma_label = soma_label
        self.delimiter = delimiter
        self.read_meta = read_meta

        int_, float_ = base.parse_precision(precision)
        self._dtypes = {
            "node_id": int_,
            "parent_id": int_,
            "label": "category",
            "x": float_,
            "y": float_,
            "z": float_,
            "radius": float_,
        }

    @base.handle_errors
    def read_buffer(
        self, f: IO, attrs: Optional[Dict[str, Any]] = None
    ) -> "core.TreeNeuron":
        """Read buffer into a TreeNeuron.

        Parameters
        ----------
        f :         IO
                    Readable buffer (if bytes, interpreted as utf-8).
        attrs :     dict | None
                    Arbitrary attributes to include in the TreeNeuron.

        Returns
        -------
        core.TreeNeuron
        """
        if isinstance(f, HTTPResponse):
            f = io.StringIO(f.data.decode())

        if isinstance(f.read(0), bytes):
            f = io.TextIOWrapper(f, encoding="utf-8")

        header_rows = read_header_rows(f)
        try:
            nodes = pd.read_csv(
                f,
                delimiter=self.delimiter,
                skipinitialspace=True,
                skiprows=len(header_rows),
                comment=COMMENT,
                header=None,
                na_values=NA_VALUES,
            )
            if len(nodes.columns) < len(NODE_COLUMNS):
                raise ValueError("Not enough columns in SWC file.")
            elif len(nodes.columns) > len(NODE_COLUMNS):
                logger.warning(
                    f"Found {len(nodes.columns)} instead of the expected 7 "
                    "columns in SWC file. Assuming additional columns are "
                    "custom properties. You can silence this warning by setting "
                    "`navis.set_loggers('ERROR')`."
                )
                nodes.columns = (
                    list(NODE_COLUMNS) + nodes.columns[len(NODE_COLUMNS) :].tolist()
                )
            else:
                nodes.columns = NODE_COLUMNS
        except pd.errors.EmptyDataError:
            # If file is totally empty, return an empty neuron
            # Note that the TreeNeuron will still complain but it's a better
            # error message
            nodes = pd.DataFrame(columns=NODE_COLUMNS)

        # Check for row with JSON-formatted meta data
        # Expected format '# Meta: {"id": "12345"}'
        if self.read_meta:
            meta_row = [r for r in header_rows if r.lower().startswith("# meta:")]
            if meta_row:
                meta_data = json.loads(meta_row[0][7:].strip())
                attrs = base.merge_dicts(meta_data, attrs)

        return self.read_dataframe(
            nodes, base.merge_dicts({"swc_header": "\n".join(header_rows)}, attrs)
        )

    @base.handle_errors
    def read_dataframe(
        self, nodes: pd.DataFrame, attrs: Optional[Dict[str, Any]] = None
    ) -> "core.TreeNeuron":
        """Convert a SWC-like DataFrame into a TreeNeuron.

        Parameters
        ----------
        nodes :     pandas.DataFrame
        attrs :     dict or None
                    Arbitrary attributes to include in the TreeNeuron.

        Returns
        -------
        core.TreeNeuron
        """
        n = core.TreeNeuron(
            sanitise_nodes(nodes.astype(self._dtypes, errors="ignore", copy=False)),
            connectors=self._extract_connectors(nodes),
        )

        if self.soma_label is not None:
            is_soma_node = n.nodes.label.values == self.soma_label
            if any(is_soma_node):
                n.soma = n.nodes.node_id.values[is_soma_node][0]

        attrs = self._make_attributes({"name": "SWC", "origin": "DataFrame"}, attrs)

        # SWC is special - we do not want to register it
        n.swc_header = attrs.pop("swc_header", "")

        # Try adding properties one-by-one. If one fails, we'll keep track of it
        # in the `.meta` attribute
        meta = {}
        for k, v in attrs.items():
            try:
                n._register_attr(k, v)
            except (AttributeError, ValueError, TypeError):
                meta[k] = v

        if meta:
            n.meta = meta

        return n

    def _extract_connectors(self, nodes: pd.DataFrame) -> Optional[pd.DataFrame]:
        """Infer outgoing/incoming connectors from node labels.

        Parameters
        ----------
        nodes :     pd.DataFrame

        Returns
        -------
        Optional[pd.DataFrame]
                    With columns `["node_id", "x", "y", "z", "connector_id", "type"]`
        """
        if not self.connector_labels:
            return None

        to_concat = [
            pd.DataFrame([], columns=["node_id", "connector_id", "type", "x", "y", "z"])
        ]
        for name, val in self.connector_labels.items():
            cn = nodes[nodes.label == val][["node_id", "x", "y", "z"]].copy()
            cn["connector_id"] = None
            cn["type"] = name
            to_concat.append(cn)

        return pd.concat(to_concat, axis=0)

Read buffer into a TreeNeuron.

PARAMETER DESCRIPTION
f
    Readable buffer (if bytes, interpreted as utf-8).

TYPE: IO

attrs
    Arbitrary attributes to include in the TreeNeuron.

TYPE: dict | None DEFAULT: None

RETURNS DESCRIPTION
core.TreeNeuron
Source code in navis/io/swc_io.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
@base.handle_errors
def read_buffer(
    self, f: IO, attrs: Optional[Dict[str, Any]] = None
) -> "core.TreeNeuron":
    """Read buffer into a TreeNeuron.

    Parameters
    ----------
    f :         IO
                Readable buffer (if bytes, interpreted as utf-8).
    attrs :     dict | None
                Arbitrary attributes to include in the TreeNeuron.

    Returns
    -------
    core.TreeNeuron
    """
    if isinstance(f, HTTPResponse):
        f = io.StringIO(f.data.decode())

    if isinstance(f.read(0), bytes):
        f = io.TextIOWrapper(f, encoding="utf-8")

    header_rows = read_header_rows(f)
    try:
        nodes = pd.read_csv(
            f,
            delimiter=self.delimiter,
            skipinitialspace=True,
            skiprows=len(header_rows),
            comment=COMMENT,
            header=None,
            na_values=NA_VALUES,
        )
        if len(nodes.columns) < len(NODE_COLUMNS):
            raise ValueError("Not enough columns in SWC file.")
        elif len(nodes.columns) > len(NODE_COLUMNS):
            logger.warning(
                f"Found {len(nodes.columns)} instead of the expected 7 "
                "columns in SWC file. Assuming additional columns are "
                "custom properties. You can silence this warning by setting "
                "`navis.set_loggers('ERROR')`."
            )
            nodes.columns = (
                list(NODE_COLUMNS) + nodes.columns[len(NODE_COLUMNS) :].tolist()
            )
        else:
            nodes.columns = NODE_COLUMNS
    except pd.errors.EmptyDataError:
        # If file is totally empty, return an empty neuron
        # Note that the TreeNeuron will still complain but it's a better
        # error message
        nodes = pd.DataFrame(columns=NODE_COLUMNS)

    # Check for row with JSON-formatted meta data
    # Expected format '# Meta: {"id": "12345"}'
    if self.read_meta:
        meta_row = [r for r in header_rows if r.lower().startswith("# meta:")]
        if meta_row:
            meta_data = json.loads(meta_row[0][7:].strip())
            attrs = base.merge_dicts(meta_data, attrs)

    return self.read_dataframe(
        nodes, base.merge_dicts({"swc_header": "\n".join(header_rows)}, attrs)
    )

Convert a SWC-like DataFrame into a TreeNeuron.

PARAMETER DESCRIPTION
nodes

TYPE: pandas.DataFrame

attrs
    Arbitrary attributes to include in the TreeNeuron.

TYPE: dict or None DEFAULT: None

RETURNS DESCRIPTION
core.TreeNeuron
Source code in navis/io/swc_io.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
@base.handle_errors
def read_dataframe(
    self, nodes: pd.DataFrame, attrs: Optional[Dict[str, Any]] = None
) -> "core.TreeNeuron":
    """Convert a SWC-like DataFrame into a TreeNeuron.

    Parameters
    ----------
    nodes :     pandas.DataFrame
    attrs :     dict or None
                Arbitrary attributes to include in the TreeNeuron.

    Returns
    -------
    core.TreeNeuron
    """
    n = core.TreeNeuron(
        sanitise_nodes(nodes.astype(self._dtypes, errors="ignore", copy=False)),
        connectors=self._extract_connectors(nodes),
    )

    if self.soma_label is not None:
        is_soma_node = n.nodes.label.values == self.soma_label
        if any(is_soma_node):
            n.soma = n.nodes.node_id.values[is_soma_node][0]

    attrs = self._make_attributes({"name": "SWC", "origin": "DataFrame"}, attrs)

    # SWC is special - we do not want to register it
    n.swc_header = attrs.pop("swc_header", "")

    # Try adding properties one-by-one. If one fails, we'll keep track of it
    # in the `.meta` attribute
    meta = {}
    for k, v in attrs.items():
        try:
            n._register_attr(k, v)
        except (AttributeError, ValueError, TypeError):
            meta[k] = v

    if meta:
        n.meta = meta

    return n

Generate a node table compliant with the SWC format.

Follows the format specified here.

PARAMETER DESCRIPTION
x
            Dotprops will be turned from points + vectors to
            individual segments.

TYPE: TreeNeuron | Dotprops

labels
            Node labels. Can be::

            str : column name in node table
            dict: must be of format {node_id: 'label', ...}.
            bool: if True, will generate automatic labels, if False all nodes have label "0".

TYPE: str | dict | bool DEFAULT: None

export_connectors
            If True, will label nodes with pre- ("7") and
            postsynapse ("8"). Because only one label can be given
            this might drop synapses (i.e. in case of multiple
            pre- or postsynapses on a single node)! `labels`
            must be `True` for this to have any effect.

TYPE: bool DEFAULT: False

return_node_map
            If True, will return a dictionary mapping the old node
            ID to the new reindexed node IDs in the file.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SWC table : pandas.DataFrame
node map : dict

Only if return_node_map=True.

Source code in navis/io/swc_io.py
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
def make_swc_table(
    x: Union["core.TreeNeuron", "core.Dotprops"],
    labels: Union[str, dict, bool] = None,
    export_connectors: bool = False,
    return_node_map: bool = False,
) -> pd.DataFrame:
    """Generate a node table compliant with the SWC format.

    Follows the format specified
    [here](http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html).

    Parameters
    ----------
    x :                 TreeNeuron | Dotprops
                        Dotprops will be turned from points + vectors to
                        individual segments.
    labels :            str | dict | bool, optional
                        Node labels. Can be::

                        str : column name in node table
                        dict: must be of format {node_id: 'label', ...}.
                        bool: if True, will generate automatic labels, if False all nodes have label "0".

    export_connectors : bool, optional
                        If True, will label nodes with pre- ("7") and
                        postsynapse ("8"). Because only one label can be given
                        this might drop synapses (i.e. in case of multiple
                        pre- or postsynapses on a single node)! `labels`
                        must be `True` for this to have any effect.
    return_node_map :   bool
                        If True, will return a dictionary mapping the old node
                        ID to the new reindexed node IDs in the file.

    Returns
    -------
    SWC table :         pandas.DataFrame
    node map :          dict
                        Only if `return_node_map=True`.

    """
    if isinstance(x, core.Dotprops):
        x = x.to_skeleton()

    # Work on a copy
    swc = x.nodes.copy()

    # Add labels
    swc["label"] = 0
    if isinstance(labels, dict):
        swc["label"] = swc.index.map(labels)
    elif isinstance(labels, str):
        swc["label"] = swc[labels]
    elif labels:
        # Add end/branch labels
        swc.loc[swc.type == "branch", "label"] = 5
        swc.loc[swc.type == "end", "label"] = 6
        # Add soma label
        if not isinstance(x.soma, type(None)):
            soma = utils.make_iterable(x.soma)
            swc.loc[swc.node_id.isin(soma), "label"] = 1
        if export_connectors:
            # Add synapse label
            pre_ids = x.presynapses.node_id.values
            post_ids = x.postsynapses.node_id.values
            swc.loc[swc.node_id.isin(pre_ids), "label"] = 7
            swc.loc[swc.node_id.isin(post_ids), "label"] = 8

    # Sort such that the parent is always before the child
    swc.sort_values("parent_id", ascending=True, inplace=True)

    # Reset index
    swc.reset_index(drop=True, inplace=True)

    # Generate mapping
    new_ids = dict(zip(swc.node_id.values, swc.index.values + 1))

    swc["node_id"] = swc.node_id.map(new_ids)
    # Lambda prevents potential issue with missing parents
    swc["parent_id"] = swc.parent_id.map(lambda x: new_ids.get(x, -1))

    # Get things in order
    swc = swc[["node_id", "label", "x", "y", "z", "radius", "parent_id"]]

    # Make sure radius has no `None`
    swc["radius"] = swc.radius.fillna(0)

    # Adjust column titles
    swc.columns = ["PointNo", "Label", "X", "Y", "Z", "Radius", "Parent"]

    if return_node_map:
        return swc, new_ids

    return swc
Source code in navis/io/swc_io.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def read_header_rows(f: TextIO):
    f"""Read {COMMENT}-prefixed lines from the start of a buffer,
    then seek back to the start of the buffer.

    Parameters
    ----------
    f : io.TextIO

    Returns
    -------
    list : List of strings
    """
    out = []
    for line in f:
        if not line.startswith(COMMENT):
            break
        out.append(line)

    f.seek(0)
    return out

Check that nodes dataframe is non-empty and is not missing any data.

PARAMETER DESCRIPTION
nodes

TYPE: pandas.DataFrame

RETURNS DESCRIPTION
pandas.DataFrame
Source code in navis/io/swc_io.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def sanitise_nodes(nodes: pd.DataFrame, allow_empty=True) -> pd.DataFrame:
    """Check that nodes dataframe is non-empty and is not missing any data.

    Parameters
    ----------
    nodes : pandas.DataFrame

    Returns
    -------
    pandas.DataFrame
    """
    if not allow_empty and nodes.empty:
        raise ValueError("No data found in SWC.")

    is_na = nodes[["node_id", "parent_id", "x", "y", "z"]].isna().any(axis=1)

    if is_na.any():
        # Remove nodes with missing data
        nodes = nodes.loc[~is_na.any(axis=1)]

        # Because we removed nodes, we'll have to run a more complicated root
        # detection
        nodes.loc[~nodes.parent_id.isin(nodes.node_id), "parent_id"] = -1

    return nodes