limnode.py

limnode.py

The limnode.py file location depends on the installation type:

%userprofile%\AppData\Local\Programs\NIS-Express\Python\Lib\site-packages\limnode.py
C:\Program Files\NIS-Express\Python\Lib\site-packages\limnode.py
Imports and type definitions
from __future__ import annotations
from dataclasses import dataclass, field, asdict
from types import NoneType
from typing import Any, Callable, cast, Protocol, Sequence, TypeAlias, TypeGuard,

import enum, subprocess, re, os, sys

class ArrayLike(Protocol):
    ndim: int
    shape: tuple[int, ...]
    def __array__(self, dtype=None): ...

CellData: TypeAlias = int|float|str|None
ColData: TypeAlias = list[int|None]|list[float|None]|list[str|None]|ArrayLike
MultiColData: TypeAlias = ColData|list[ColData]|ArrayLike
SafeColSpec: TypeAlias = (
    list[str] | list[int]
    | re.Pattern
    | Callable[[str, dict[str, Any]], bool]
    | None
)
class LimTableLike(Protocol):
    ...

class MplFigureLike(Protocol):
    def print_png(*args, **kwargs) -> None: ...
    def savefig(*args, **kwargs) -> None: ...

def _is_str_list(x: object) -> TypeGuard[list[str]]:
    return isinstance(x, list) and all(isinstance(i, str) for i in x)

def _is_int_list(x: object) -> TypeGuard[list[str]]:
    return isinstance(x, list) and all(isinstance(i, str) for i in x)

def _is_list_list(x: object) -> TypeGuard[list[list]]:
    return isinstance(x, list) and all(isinstance(i, list) for i in x)

def _is_1d_array_list(x: object) -> TypeGuard[list[ArrayLike]]:
    return (
        isinstance(x, list)
        and all((getattr(i, '__array__', False) and i.ndim == 1) for i in x)
    )

def _is_1d_array(x: object) -> TypeGuard[ArrayLike]:
    return getattr(x, '__array__', False) and cast(ArrayLike, x).ndim == 1

def _is_2d_array(x: object) -> TypeGuard[ArrayLike]:
    return getattr(x, '__array__', False) and cast(ArrayLike, x).ndim == 2

class ExperimentLoopType(enum.IntEnum):
    loopTypeUnknown  = 0
    loopTypePlate    = 1
    loopTypeWell     = 2
    loopTypeXYSite   = 3
    loopTypeTLapse   = 4
    loopTypeZStack   = 5
    loopTypeCount    = 6
    loopTypeSlide    = 7
    loopTypeRegion   = 8
    loopTypeTemporary= 9

AnyColorDef: TypeAlias = int|str|tuple[int, int, int]

Common base types

@dataclass(frozen=True, kw_only=True)
class Node:
    name: str
    uuid: str
    defaultName: str

@dataclass(frozen=True, kw_only=True)
class LoopDef:
    name: str
    type: ExperimentLoopType
    index: int

@dataclass(frozen=True, kw_only=True)
class ParameterDef:
    parentNode: Node
    parName: str
    parUuid: str
    parIsInput: bool
    loopDefList: list[LoopDef]

    def loopNames(
        self,
        loop_type: ExperimentLoopType|list[ExperimentLoopType]|None = None) -> list[str]:
        ...

    def format_coordinates(
        self,
        coordinates: tuple[int],
        *,
        sep: str = '_',
        one_based_indexes: bool = True,
        leading_zeros: int = 0
        ) -> str:
        ...

@dataclass(frozen=True, kw_only=True)
class Metadata:
    componentCount: int
    bitsPerComponent: int
    size: tuple[int, int, int]
    calibration: tuple[float, float, float]
    alignment: int
    isRGB: bool

    @property
    def width(self) -> int:
        return self.size[0]

    @property
    def height(self) -> int:
        return self.size[1]

    @property
    def depth(self) -> int:
        return self.size[2]

    @property
    def calibrationXY(self) -> float:
        return self.calibration[0]

    @property
    def calibrationZ(self) -> float:
        return self.calibration[2]

    @property
    def calibrated(self) -> tuple[bool, ...]:
        return (0 < self.calibration[0], 0 < self.calibration[1], 0 < self.calibration[2])
    @property
    def units(self) -> tuple[str, ...]:
        return tuple("\xB5m" if cal else "px" for cal in self.calibrated)

    def withDifferentSize(
        self,
        size: tuple[int, int, int],
        calibration: tuple[float, float, float]|None = None
        ) -> Metadata:
        ...

    def copyMetadataFrom(self, other: Metadata) -> Metadata:
        ...

UserParTuple: TypeAlias = tuple[int|float|str, ...]
LoopDefs: TypeAlias =  list[LoopDef]

The output(...) function

AnyInDef: TypeAlias = InputChannelDef|InputBinaryDef|InputTableDef|None
AnyOutDef: TypeAlias = OutputChannelDef|OutputBinaryDef|OutputTableDef
InDefTuple: TypeAlias = tuple[AnyInDef, ...]
OutDefTuple: TypeAlias = tuple[AnyOutDef, ...]

def output(inp: limnode.InDefTuple,
           out: limnode.OutDefTuple,
           par: limnode.UserParTuple) -> None:
    pass
@dataclass(frozen=True, kw_only=True)
class InputChannelDef(ParameterDef, Metadata):
    channelName: str
    channelColor: int

@dataclass(frozen=True, kw_only=True)
class OutputChannelDef(ParameterDef, Metadata):
    assignedParName: str|None = None
    channelName: str|None = None
    channelColor: int|None = None

    def assign(self, param: InputChannelDef) -> OutputChannelDef:
        ...

    def makeNewMono(self, name: str, color: AnyColorDef) -> OutputChannelDef:
        ...

    def makeNewRgb(self, name: str|None = None) -> OutputChannelDef:
        ...

    def makeFloat(self) -> OutputChannelDef:
        ...

    def makeUInt16(self) -> OutputChannelDef:
        ...

    def makeUInt8(self) -> OutputChannelDef:
        ...

    def copyFrom(self, other: OutputChannelDef) -> OutputChannelDef:
        ...

@dataclass(frozen=True, kw_only=True)
class InputBinaryDef(ParameterDef, Metadata):
    binaryName: str
    binaryColor: int

@dataclass(frozen=True, kw_only=True)
class OutputBinaryDef(ParameterDef, Metadata):
    assignedParName: str|None = None
    binaryName: str|None = None
    binaryColor: int|None = None

    def assign(self, param: InputBinaryDef) -> OutputBinaryDef:
        ...

    def makeNew(self, name: str, color: AnyColorDef) -> OutputBinaryDef:
        ...

    def makeInt32(self) -> OutputBinaryDef:
        ...

    def makeUInt8(self) -> OutputBinaryDef:
        ...

    def copyFrom(self, other: OutputBinaryDef) -> OutputBinaryDef:
        ...

@dataclass(frozen=True, kw_only=True)
class InputTableDef(ParameterDef):
    tableDef: LimTableLike

    def accumulatedOver(self) -> list[str]:
        return self.tableDef.tableMetadata.get('accumulatedLoops', [])

@dataclass(frozen=True, kw_only=True)
class OutputTableDef(ParameterDef):
    assignedParName: str
    tableDef: LimTableLike

    def assign(self, param: InputTableDef) -> OutputTableDef:
        ...

    # new empty table (optional InputTableDef to copy accumulated loops)
    def makeEmpty(self, name: str,
                  param: InputTableDef|None = None) -> OutputTableDef:
        ...

    # optional InputTableDef to copy columns from or if None add just loopCols
    def makeNew(self, name: str,
                param: InputTableDef|None = None) -> OutputTableDef:
        ...

    # to make a result - Graph, (optional InputTableDef to copy accumulated loops)
    def makeResult(self, name: str,
                   param: InputTableDef|None = None) -> OutputTableDef:
        ...

    # to add all necessary loop columns
    def withLoopCols(self, *,
                     newColNames: list[str]|None = None) -> OutputTableDef:
        ...

    # to add Entity and ObjectID columns
    def withObjectCols(self, *,
                       newColNames: list[str]|None = None) -> OutputTableDef:
        ...

    # to add Entity and 3D ObjectID columns
    def withObject3dCols(self, *,
                         newColNames: list[str]|None = None) -> OutputTableDef:
        ...

    # and arbitrary data columns based on type
    def withIntCol(self,
                   title: str,
                   unit: str|None = None,
                   feature:str|None = None,
                   id: str|None = None) -> OutputTableDef:
        ...

    def withFloatCol(self,
                     title: str,
                     unit: str|None = None,
                     feature:str|None = None,
                     id: str|None = None) -> OutputTableDef:
        ...

    def withStringCol(self,
                      title: str,
                      unit: str|None = None,
                      feature:str|None = None,
                      id: str|None = None) -> OutputTableDef:
        ...

    # set accumulated table manually (prefer referencing input table in makeEmpty)
    def setAccumulatedOver(self, *args) -> OutputTableDef:
        ...

    # convenience for the above
    def setAccumulatedOverZStack(self) -> OutputTableDef:
        ...

    def setAccumulatedOverTimeLapse(self) -> OutputTableDef:
        ...

    def setAccumulatedOverMultiPoint(self) -> OutputTableDef:
        ...

    def setAccumulatedOverAll(self) -> OutputTableDef:
        ...

    def _withColumn(self,
        decltype: str,
        title: str,
        unit: str|None = None,
        feature: str|None = None,
        id: str|None = None,
        meta: dict[str, Any]|None = None) -> OutputTableDef:
            ...

    def copyFrom(self, other: OutputTableDef) -> OutputTableDef:
        ...

The run(...) function

AnyInData: TypeAlias = InputChannelData|InputBinaryData|InputTableData|None
AnyOutData: TypeAlias = OutputChannelData|OutputBinaryData|OutputTableData
InDataTuple: TypeAlias = tuple[AnyInData, ...]
OutDataTuple: TypeAlias = tuple[AnyOutData, ...]

def run(inp: limnode.InDataTuple,
        out: limnode.OutDataTuple,
        par: limnode.UserParTuple,
        ctx: limnode.RunContext) -> None:
    pass
@dataclass(frozen=True, kw_only=True)
class ArrayData:
    data: ArrayLike|None

    # internal method to set an array in shared memory for out-proc
    # for setting data use data[:]
    def _setData(self, new_data: ArrayLike|None) -> None:
        ...

@dataclass(frozen=True, kw_only=True)
class RecordedData:
    recdata: LimTableLike|None

    def _copyRecordedDataFrom(self, other: RecordedData) -> None:
        ...


@dataclass(frozen=True, kw_only=True)
class InputChannelData(ParameterDef, Metadata, ArrayData, RecordedData):
    pass

@dataclass(frozen=True, kw_only=True)
class OutputChannelData(ParameterDef, Metadata, ArrayData, RecordedData):
    def copyRecordedDataFrom(self, other: RecordedData) -> None:
        ...


@dataclass(frozen=True, kw_only=True)
class InputBinaryData(ParameterDef, Metadata, ArrayData, RecordedData):
    pass

@dataclass(frozen=True, kw_only=True)
class OutputBinaryData(ParameterDef, Metadata, ArrayData, RecordedData):
    def copyRecordedDataFrom(self, other: RecordedData) -> None:
        ...

@dataclass(frozen=True, kw_only=True)
class InputTableData(ParameterDef, RecordedData):
    data: LimTableLike

    def colArray(self,
                 cols: str|int|SafeColSpec = None
                 ) -> ArrayLike|list[ArrayLike]:
        """Return numpy ndarray or a list there of for specified columns.

        If cols is scalar ndarray is returned otherwise a list is returned instead.

        Column specifier:
        - column name, ID or index returns single column
        - list of column names, IDs or indexes returns list of columns (must be present)
        - None return all columns
        - re.Pattern return columns where fullmatch succeeded on column ID or Title
        - Callable[[str, dict], bool] returns columns where callable
          returns True for (ID, Metadata)

        When column names, IDs or indexes are specified explicitly in a list they must
        be present otherwise and AssertError is raised.

        Args:
            cols: col name, id or index or a list there of or a Pattern or Callable

        Returns:
            Single 1D numpy ndarray or a list thereof.

        Raises:
            ValueError: if a column explicitly specified (by name, id or index)
                        is not found
            TypeError: if cols are not of requested type
        """
        ...

    def dataFrame(self,
                  *,
                  use_col_ids: bool = False,
                  no_hidden: bool = False,
                  no_loops: bool = False,
                  no_loop_indexes: bool = False,
                  no_bin: bool = False,
                  no_bin_id: bool = False,
                  no_bin_entity: bool = False,
                  no_well: bool = False,
                  no_file: bool = False,
                  excl: SafeColSpec = None,
                  incl: SafeColSpec = None) -> "pd.DataFrame":
        """Return Pandas DataFrame with all columns except as specified.

        By default all columns are returned.
        If no_* or excl kwarg is present, matching columns are removed.
        If incl is present, matching columns are added (after exclusion)

        Args:
            use_col_ids: columns are indexed by column ids and not by column titles
            no_*: exclude specific system column/hidden columns
            excl: exclude any column
            incl: include any column

        Returns:
            Pandas DataFrame.

        Raises:
            AssertError: if a column explicitly specified column (by name, id or index)
                         is not found
        """
        ...


    def metaDataFrame(self) -> "pd.DataFrame":
        ...

@dataclass(frozen=True, kw_only=True)
class OutputTableData(ParameterDef, RecordedData):
    data: LimTableLike|None

    def withColDataFrom(self,
                        *args: Sequence[InputTableData|"pd.DataFrame"|"pd.Series"],
                        remaining_cols: list[str]|None = None
                        ) -> OutputTableData:
        ...

    def withColData(self,
                    cols: str|int|SafeColSpec,
                    data: MultiColData
                    ) -> OutputTableData:
        """Fill table with column data and return self.

        Column specifier:
        - column name, ID or index returns single column
        - list of column names, IDs or indexes returns list of columns (must be present)
        - None return all columns
        - re.Pattern return columns where fullmatch succeeded on column ID or Title
        - Callable[[str, dict], bool] returns columns where callable returns
          True for (ID, Metadata)

        The data must have same length as cols.

        When column names, IDs or indexes are specified explicitly in a list they must
        be present otherwise and AssertError is raised.

        Args:
            cols: col name, id or index or a list there of or a Pattern or Callable
            data: list of values, 1D nd array or a list of these or 2D nd array

        Returns:
            self

        Raises:
            ValueError: if a column explicitly specified column (by name, id or index)
                        is not found
            TypeError: if cols or data are not of requested type
        """
        ...

    def dataFrame(self, *, use_col_ids: bool = False) -> "pd.DataFrame":
        """Return Pandas DataFrame with all columns.

        Args:
            use_col_ids: columns are indexed by column ids and not by column titles

        Returns:
            Pandas DataFrame.
        """
        ...

    def withDataFrame(self,
                      df: "pd.DataFrame",
                      cols: str|int|SafeColSpec = None
                      ) -> OutputTableData:
        """Fill table with Pandas DataFrame.

        Column specifier:
        - column name, ID or index returns single column
        - list of column names, IDs or indexes returns list of columns (must be present)
        - None return all columns
        - re.Pattern return columns where fullmatch succeeded on column ID or Title
        - Callable[[str, dict], bool] returns columns where callable
          returns True for (ID, Metadata)

        When column names, IDs or indexes are specified explicitly in a list they must
        be present otherwise and AssertError is raised.

        Args:
            df: Pandas DataFrame
            cols: col name, id or index or a list there of or a Pattern or Callable

        Returns:
            self

        Raises:
            ValueError: if a column explicitly specified (by name, id or index)
                        is not found
            TypeError: if cols or data are not of requested type
        """
        ...

    def copyRecordedDataFrom(self, other: RecordedData) -> None:
        ...

    def copyFrom(self, other: RecordedData) -> None:
        ...

    def withMplImage(self, loopCoordinates: tuple[int],
                     plt: MplFigureLike|tuple[MplFigureLike,MplFigureLike],
                     *, name: str|None = None, iconres: str|None = None,
                     objectFit: str|None = None, tableRowVisibility: str|None = None,
                     savefig_kwargs: dict = { 'format': 'png' }
                     ) -> OutputTableData:
        ...

    def _makeResult(self,
                    data: LimTableLike,
                    jsClassName: str,
                    state: dict[str, Any] = {}) -> OutputTableData:
        ...

The build(...) function

UserParTuple: TypeAlias = tuple[int|float|str, ...]
LoopDefs: TypeAlias = list[LoopDef]

def build(par: limnode.UserParTuple, loops: limnode.LoopDefs) -> limnode.Program|None:
    return None
@dataclass
class Program:
    allLoopDefs: list[LoopDef]
    overLoops: list[str] = field(default_factory=list, init=False)

    def overZStack(self) -> Program:
        ...

    def overTimeLapse(self) -> Program:
        ...

    def overMultiPoint(self) -> Program:
        ...

    def overAll(self) -> Program:
        ...

@dataclass
class ReduceProgram(Program):
    pass

@dataclass
class TwoPassProgram(Program):
    pass

@dataclass(kw_only=True)
class RunContext:
    inpFilename: str
    outFilename: str
    inpParameterCoordinates: tuple[tuple[int]]
    outCoordinates: tuple[int]
    programLoopsOver: list[str]|None          # loop names
    programCoordinateIndexes: tuple[int]|None # inner loops: indexes of loops that must
                                              # be handled in Python (not by GA3)
                                              # e.g. Z-Stack loop
    programPass: int
    finalCall: bool
    tempPath: str

    @property
    def shouldAbort(self) -> bool:
        ...

AnyColorDef: TypeAlias = int|str|tuple[int, int, int]

def _parse_color(color: AnyColorDef) -> int:
    if type(color) == int:
        return color & 0xFFFFFF
    elif type(color) == str and len(color) == 7 and color[0] == '#':
        return int(color[1:], 16)
    elif type(color) == tuple:
        assert 3 <= len(color), "Unexpected length of argument 'color'"
        return (color[0] << 16) + (color[1] << 8) + color[2]
    else:
        return 0

Utility functions


# labels to NIS binaries separated background pixel
def separateLabeledImage(src: ArrayLike) -> ArrayLike:
    ...

# labels to NIS binaries separated background voxls
def separateLabeledImage3d(src: ArrayLike) -> ArrayLike:
    ...