Source code for dendrotweaks.morphology.io.reader

# SPDX-FileCopyrightText: 2025 Poirazi Lab <dendrotweaks@dendrites.gr>
# SPDX-License-Identifier: MPL-2.0

import pandas as pd

SWC_IDS_TO_DOMAINS = {
    0: 'undefined',
    1: 'soma',
    2: 'axon',
    3: 'dend',
    31: 'basal',
    4: 'apic',
    41: 'trunk',
    42: 'tuft',
    43: 'oblique',
    6: 'neurite',
    7: 'glia',
    8: 'reduced',
}

DOMAINS_TO_COLORS = {
    'undefined': '#7F7F7F',
    'soma': '#E69F00',
    'axon': '#F0E442',
    'dend': '#019E73',
    'basal': '#31A354',
    'apic': '#0072B2',
    'trunk': '#56B4E9',
    'tuft': '#A55194',
    'oblique': '#8C564B',
    'neurite': '#D55E00',
    'glia': '#D62728',
    'reduced': '#E377C2',
}

DEFAULT_IDS = list(SWC_IDS_TO_DOMAINS.keys())

[docs] class SWCReader(): """ Reads an SWC file and returns a DataFrame. """ def __init__(self): pass
[docs] def read_file(self, path_to_swc_file: str) -> pd.DataFrame: """ Read the SWC file and return a DataFrame enriched with domain & color metadata. """ with open(path_to_swc_file, 'r') as f: raw_lines = f.readlines() # 1. Clean up lines lines = self._clean_lines(raw_lines) # 2. Read SWC table df = self._read_swc_table(path_to_swc_file) unique_type_ids = df['Type'].unique() # 3. Extract data from header (if available) domain_map, color_map = self._build_mappings(lines, unique_type_ids) # 4. Add domain and color columns df.insert(df.columns.get_loc("Type") + 1, "Domain", df["Type"].map(domain_map)) df.insert(df.columns.get_loc("Domain") + 1, "Color", df["Domain"].map(color_map)) return df
[docs] @staticmethod def rename_domain(df, type_idx: int, new_domain_name: str, new_color: str): """ Rename a domain in the DataFrame by changing the domain name and color for a given type index. """ df.loc[df['Type'] == type_idx, 'Domain'] = new_domain_name df.loc[df['Type'] == type_idx, 'Color'] = new_color
[docs] @staticmethod def replace_domain(df, old_type_idx: int, new_type_idx: int): """ Replace a domain in the DataFrame by changing the type index, domain name, and color. Parameters ---------- df : pd.DataFrame The DataFrame containing the SWC data (generated by read_file). old_type_idx : int The type index of the domain to be replaced. new_type_idx : int The type index of the new domain. """ new_domain_name = df.loc[df['Type'] == new_type_idx, 'Domain'].iloc[0] new_color = df.loc[df['Type'] == new_type_idx, 'Color'].iloc[0] df.loc[df['Type'] == old_type_idx, 'Type'] = new_type_idx df.loc[df['Type'] == new_type_idx, 'Domain'] = new_domain_name df.loc[df['Type'] == new_type_idx, 'Color'] = new_color
[docs] @staticmethod def remove_domain(df, type_idx: int): """ Remove a domain from the DataFrame by dropping all rows with the given type index. """ df.drop(df[df['Type'] == type_idx].index, inplace=True)
@staticmethod def _clean_lines(lines): """Strip whitespace, collapse multiple spaces, drop empty lines.""" return [' '.join(line.split()) for line in lines if line.strip()] @staticmethod def _read_swc_header(lines): """ Extract domain and color mappings from SWC header. """ domain_map = {} color_map = {} domain_line = next((l for l in lines if l.startswith("# DOMAIN_NAMES")), None) color_line = next((l for l in lines if l.startswith("# DOMAIN_COLORS")), None) if domain_line: items = domain_line.replace("# DOMAIN_NAMES", "").strip().split() for item in items: t, dom = item.split(":", 1) domain_map[int(t)] = dom if color_line: items = color_line.replace("# DOMAIN_COLORS", "").strip().split() for item in items: t, col = item.split(":", 1) domain_name = domain_map.get(int(t)) color_map[domain_name] = col return domain_map, color_map def _build_mappings(self, lines, unique_type_ids): """ Build domain and color mappings from SWC header or use defaults. """ domain_map = SWC_IDS_TO_DOMAINS.copy() color_map = DOMAINS_TO_COLORS.copy() # Extract and apply header mappings header_domains, header_colors = self._read_swc_header(lines) domain_map.update(header_domains) color_map.update(header_colors) # Handle missing type IDs (reduced and custom domains) for t in unique_type_ids: if t in domain_map: continue domain_map[t] = f'custom_{t}' color_map[f'custom_{t}'] = '#17BECF' return domain_map, color_map @staticmethod def _read_swc_table(path): """Read the numeric part of an SWC file into a DataFrame.""" df = pd.read_csv( path, sep=' ', header=None, comment='#', names=['Index', 'Type', 'X', 'Y', 'Z', 'R', 'Parent'], index_col=False, dtype={ 'Index': int, 'Type': int, 'X': float, 'Y': float, 'Z': float, 'R': float, 'Parent': int } ) # Fix zero-radii case: if (df['R'] == 0).all(): df['R'] = 1.0 if df['Index'].duplicated().any(): raise ValueError("The SWC file contains duplicate node ids.") return df
[docs] @staticmethod def plot_raw_data(df, ax, **kwargs): """ Plot the raw data from the SWC file using the DataFrame. Parameters ---------- df : pd.DataFrame The DataFrame containing the SWC data (generated by read_file). ax : matplotlib.pyplot.Axes The axes to plot on. """ for t in df['Type'].unique(): mask = df['Type'] == t color = df[mask]['Color'].iloc[0] if pd.isna(color): color = 'black' ax.scatter(xs=df[mask]['X'], ys=df[mask]['Y'], zs=df[mask]['Z'], c=color, label=f'Type {t}', **kwargs) ax.legend()