Skip to content

Clip v_mem to min_v_mem before generating the layer output (fixes #236) - #336

Draft
tritsystem wants to merge 1 commit into
synsense:developfrom
tritsystem:fix/min-v-mem-clips-output
Draft

tritsystem wants to merge 1 commit into
synsense:developfrom
tritsystem:fix/min-v-mem-clips-output

Conversation

@tritsystem

Copy link
Copy Markdown

Checklist before requesting a review

  • Tests for the changes have been added (for bug fixes/features)
  • Docs have been added / updated (for bug fixes / features)
  • I have performed a self-review of my code
  • Will this be part of a product update? If yes, please write one line about this on docs/about/release_notes.md

Docs: no user-facing doc change — the min_v_mem docstring ("Lower bound for membrane potential v_mem, clipped at every time step") already describes the intended behaviour; this PR just makes the code match it. Release notes are maintained per the tag-based release process, happy to add a line if you'd like one.


Fixes #236.

  • What kind of change does this PR introduce?

Bug fix.

  • What is the current behavior?

When a LIF or IAF layer is built without a spike function (spike_fn=None) — which is also exactly what the public ExpLeak / ExpLeakSqueeze layers do internally — the layer output is the membrane potential v_mem. When min_v_mem is set, the stored state is clipped to that floor but the returned output is not, because in functional/lif.py::lif_forward_single the clip runs after v_mem has already been cloned into the output tensor:

if spike_fn:
    ...
else:
    spikes = state["v_mem"].clone()   # output copied here (pre-clip)
if min_v_mem is not None:
    state["v_mem"] = relu(state["v_mem"] - min_v_mem) + min_v_mem   # too late for `spikes`
return spikes, state

Reproduction (develop @ 904eb89):

LIF(tau_mem=30., min_v_mem=-0.5, spike_fn=None) : output min = -2.47   state v_mem min = -0.50   -> 68/72 outputs below the floor
ExpLeak(tau_mem=30., min_v_mem=-0.5)            : output min = -2.47   state v_mem min = -0.50   -> 68/72 below the floor
IAF(min_v_mem=-0.5, spike_fn=None)             : output min = -2.49                            -> 68/72 below the floor

The existing test_min_v_mem does not catch this: it uses a spiking LIF and only asserts on layer.v_mem, never on the returned output.

  • What is the new behavior (if this is a feature change)?

v_mem is clipped to min_v_mem before the spike / no-spike branch, so the no-spike-fn output respects the floor. On the spiking branch the clip is also re-applied after reset_fn so the stored state on that path is byte-for-byte identical to before for every documented configuration (min_v_mem is None by default and always <= 0 < spike_threshold when set). min_v_mem is a lower bound and a spike is triggered by v_mem crossing the upper spike_threshold, so clipping earlier cannot change which neurons spike — a new test asserts the spike train is bit-identical with and without the floor on a spiking input.

After the fix, every path above reports output min = -0.5000, 0/72 below the floor.

ALIF is not affected: its forward unconditionally dereferences spike_fn.required_states, so it cannot be run with spike_fn=None, and its output is always spikes, never v_mem.

  • Does this PR introduce a breaking change?

No. Default min_v_mem is None (no-op). For spiking layers the stored state and spike output are unchanged. The only behavioural change is that non-spiking layers (ExpLeak, LIF/IAF with spike_fn=None) now return outputs >= min_v_mem, which is what the min_v_mem docstring already promises.

  • Other information:

Diff: sinabs/layers/functional/lif.py +19 / −4 (mostly the moved block + comments), tests/test_layers/test_lif.py +41.

New tests in tests/test_layers/test_lif.py:

  • test_min_v_mem_clips_the_output_without_spike_fnExpLeak / LIF(spike_fn=None) / IAF(spike_fn=None) output >= min_v_mem. Fails on develop (output -1.48 fell below min_v_mem=-0.5), passes here.
  • test_min_v_mem_does_not_change_the_spiking_path — spike train bit-identical with and without min_v_mem on a spiking input.

black --check sinabs/ tests/ → 135 files unchanged. pytest tests/ (non-hardware modules) → 197 → 199 passed, no regressions.

Before (develop, #236 present)

before

After (this branch)

after

In lif_forward_single the min_v_mem clip ran *after* v_mem had already
been cloned into the output on the no-spike-function branch, so ExpLeak
and LIF/IAF built with spike_fn=None returned outputs below min_v_mem
even though the stored state was correctly clipped (synsense#236).

Move the clip ahead of the spike / no-spike branch, and re-apply it
after reset_fn on the spiking branch so the stored state on that path
is unchanged for every documented configuration (min_v_mem is None by
default and always <= 0 < spike_threshold when set). min_v_mem is a
lower bound and spikes are triggered by v_mem crossing the upper
spike_threshold, so clipping earlier cannot change which neurons spike.

Adds tests/test_layers/test_lif.py::test_min_v_mem_clips_the_output_without_spike_fn
(fails on develop) and ::test_min_v_mem_does_not_change_the_spiking_path.

Fixes synsense#236
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LIF and IAF classes can output values below min_v_mem when not spiking

1 participant