neu_mark.rules

The @rule decorator and RuleSet: declaring how annotation strings become facets.

A plain rule is any callable taking one row and returning a value, a sequence, or None; neu_mark.explore accepts those. A Rule is that callable plus its declarations (see rule()), which neu_mark.segprops.build() needs in order to know which rules mint tags, which exclude a body, and what each facet is called. Explore with plain callables; publish with a RuleSet.

A rules module is dataset content (which prefixes are neuropils, which suffixes mark completeness), kept with the body lists and loaded by path with from_module(). Nothing is built in. The module names its label column once (LABEL = "instance"); a rule that declares no needs reads that column, and test, remainder and explain fill it.

>>> RULES = RuleSet([side, column], label="instance")
>>> RULES["side"].test("Tm2_A2(L)")
'L'
>>> RULES.explain("LN_C5(L)_NCL")
{'side': 'L', ...}
>>> RULES.remainder("LN_C5(L)_NCL")         # what no rule accounted for
'LN'

Attributes

Classes

Rule

One named facet derived from a row. Callable exactly like the function it wraps.

RuleSet

An ordered collection of rules, plus the reports for iterating on them.

Functions

from_module(path, *[, variable]) → RuleSet

Load a rules module by file path.

provenance(ruleset, *[, embed, max_embed_bytes]) → dict

A record of the rules that produced an output.

rule([func, needs, multi, tag, prefix, name, ...])

Declare a function as a rule. Usable bare (@rule) or called (@rule(...)).

Module Contents

class neu_mark.rules.Rule(func: Callable, *, name: str | None = None, needs: Sequence[str] | None = None, multi: bool = False, tag: bool = True, prefix: str | None = None, description: str | None = None, drop: bool = False, consumes: Iterable[str] | Callable | None = None)

One named facet derived from a row. Callable exactly like the function it wraps.

bound(label: str | None) → Rule

A copy reading label as the label column; this rule is left as it is.

consumed(row) → tuple[str, ...]

The tokens of the source string this rule accounts for.

A rule that does not read the label column consumes nothing, so a rule returning the curated type cannot remove the token a cell-type remainder wants to keep.

remainder(_value: str | None = None, /, **fields) → str | None

The label string with this rule’s tokens removed, rejoined with _.

Removal is by token (the strings are _- and parenthesis-delimited), not by substring, so side’s "L" does not eat the L of LC10.

scalar(row) → Any

The rule’s single value, or None. For the single-valued kinds: drop reasons and the reserved label rule, where a tuple would only be unwrapped.

set_kwargs(**kwargs) → Self

Bind keyword arguments of the rule’s function, in place. Returns the rule.

test(_value: str | None = None, /, **fields) → Any

Evaluate on one synthetic row; rule.test("Tm2_A2(L)") fills the label column.

Returns the rule’s own return value. Missing fields arrive as None, as in a run.

values(row) → tuple

The rule’s output as a tuple, validated against multi.

description
drop = False
func
label: str | None = None
multi = False
name
property needs: tuple[str, ...]

The columns the rule reads: those it declares, else the label column.

prefix
tag = True
class neu_mark.rules.RuleSet(rules: Iterable[Rule] | Mapping[str, Callable], *, keep: Sequence[str] | None = None, source: str | None = None, label: str | None = None)

An ordered collection of rules, plus the reports for iterating on them.

apply(frame: pandas.DataFrame) → pandas.DataFrame

Every rule over every row: one column per rule, values as tuples.

as_mapping() → dict[str, Callable]

{name: callable}, for the neu_mark.explore functions.

consumed(_value: str | None = None, /, **fields) → dict[str, tuple]

{rule: tokens} for every rule that accounted for part of the string.

coverage(frame: pandas.DataFrame, *, top: int = 3) → pandas.DataFrame

Per-rule coverage, with each rule’s declared facts alongside.

describe() → pandas.DataFrame

What every rule declares, one row per rule in evaluation order. (coverage() reports what they did to a frame.)

dropping(names: Iterable[str]) → RuleSet

This set with the named rules turned into drop rules, keeping their own test.

Each is replaced in place by a rule with the same function and needs, so the body it fires on is excluded rather than tagged. An unknown name is refused.

explain(_value: str | None = None, /, **fields) → dict

Every rule’s output for one synthetic row: a scalar for single-valued rules, a tuple for multi-valued ones.

override(other: RuleSet | Iterable[Rule] | Mapping[str, Callable]) → RuleSet

This set with other’s rules replacing same-named ones, others appended.

A replacement keeps the position of the rule it replaces, so evaluation order is unchanged. (__init__ refuses duplicate names; overriding is how a dataset module redefines a builtin.)

remainder(_value: str | None = None, /, **fields) → str | None

The string with every token any rule accounted for removed, so a cell-type parse need not repeat the rules’ vocabularies.

The removed tokens are one union, so the result does not depend on rule order. The result is a residue, not a cell type: often a bare neuropil name for an unidentified body.

require(frame: pandas.DataFrame) → None

Check every rule’s needs against a frame, naming the rule that is unmet.

unparsed(frame: pandas.DataFrame, name: str, *, column: str | None = None) → pandas.DataFrame

The strings one rule did not fire on, ranked by bodies. The iteration loop. column defaults to the label column.

property drops: list[Rule]

The rules that exclude a body. Evaluated first, so an excluded body does not count toward other rules’ coverage.

keep
label = None
property names: list[str]
rules
source = None
property tagged: list[Rule]

The rules that mint tags, in declaration order.

neu_mark.rules.from_module(path: str | pathlib.Path, *, variable: str = 'RULES') → RuleSet

Load a rules module by file path.

Uses the module’s RULES if it defines one; otherwise every @rule at module level, in declaration order. The module’s LABEL names its label column (see RuleSet); a RULES that names a different one is refused.

neu_mark.rules.provenance(ruleset: RuleSet, *, embed: bool = True, max_embed_bytes: int = MAX_EMBED_BYTES) → dict

A record of the rules that produced an output.

Each rules source file is embedded verbatim with its sha256, so the record does not depend on the file still existing; git commit, remote and dirty flag are added if the file is in a work tree. Files are found from each rule’s __code__.co_filename, so helpers imported from sibling files are included; files inside neu_mark are not.

neu_mark.rules.rule(func: Callable | None = None, /, *, needs: Sequence[str] | None = None, multi: bool = False, tag: bool = True, prefix: str | None = None, name: str | None = None, description: str | None = None, drop: bool = False, consumes: Iterable[str] | Callable | None = None)

Declare a function as a rule. Usable bare (@rule) or called (@rule(...)).

  • needs: the columns the rule reads, checked up front. Left out, the rule reads the label column of the RuleSet it is bound into.

  • multi: whether the facet can have several values. A single-valued rule that returns several raises, which catches re.findall used where re.search was meant.

  • prefix: the tag namespace, defaulting to the rule’s name (glomerulus:VA1v); prefix="" mints a bare tag.

  • drop: the rule excludes the body it fires on, returning the reason.

  • consumes: the tokens it accounts for, for RuleSet.remainder().

  • tag=False: the rule produces a property, not a tag (see RESERVED).

The docstring’s first line becomes the description shown in reports and in the published tag_descriptions. A rule may read a column an earlier rule produced.

neu_mark.rules.MAX_EMBED_BYTES = 262144
neu_mark.rules.RESERVED = ('label', 'description')
neu_mark.rules.logger