diff --git a/.gitignore b/.gitignore index e2d89bd7..ce6f5c11 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,9 @@ extensions/cmake-build-debug BrainPyExamples/ BrainModels/ book/ -docs/examples +# Maintained examples are source; only their generated outputs are ignored. +/docs/examples/**/generated/ +/docs/examples/**/artifacts/ docs/apis/jaxsetting.rst docs/quickstart/data examples/recurrent_neural_network/neurogym diff --git a/AGENTS.md b/AGENTS.md index d23d64e5..41725eaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,12 +11,7 @@ Biologically detailed brain cell modeling in BrainX. 5. Every correction: reflect on the mistake, plan to avoid repeating it. 6. All updates must be happened on the worktree branch, not main. 7. Use `brainstate.random` instead of `jax.random` directly for all random number generation. -8. **All durable prose lives under `docs/`; never leave a stray `.md` inside `braincell/`.** Two homes, each with a filename rule: - - `docs/specs/YYYY-MM-DD-.md` — the spec and plan for one change, written *before* implementation. The date prefix is the creation date, so the directory reads chronologically. - - `docs/design/.md` — durable design notes, invariants, and architecture maps that outlive any single change. Group a multi-document topic in its own subdirectory (`docs/design/network/`). - - Name a file for what it documents, not where the code happens to sit: `io-swc-reader-invariants.md`, never `README.md` or `notes.md`. Give it an `# H1` that matches. - - Explicit exception: an unstable experiment directory under `examples/experimental/` may contain one local `README.md` that maps the files, status, commands, and historical context for that directory. It must not define public API or duplicate durable design/results prose; link to `docs/` and generated artifacts for those. +8. **Check design, implementation, and relevant examples before each commit.** Review only the scope of that commit across `docs/design/`, implementation and tests, and the actual related examples (including root `examples/`). Routine development does not require synchronizing these three after every edit or task. `docs/examples/` is not a mandatory parallel maintenance destination. `docs/specs/` is a chronological historical archive, not the current contract or a required pre-implementation deliverable. Follow [Design, code, and examples](#design-code-and-examples) below. 9. Tests should >90% coverage, but focus on meaningful tests that cover edge cases and critical paths, not just trivial lines. 10. Co-locate tests with the code under test: each module `foo.py` has its tests in a sibling `foo_test.py` (suffix style — never a separate `tests/` directory, never the `test_*.py` prefix). See [Testing](#testing) for the full rule. 11. **Never drive a model with a bare Python `for`/`while` loop when it runs repeatedly.** Python loops execute op-by-op (dispatch overhead, no fusion) and trace fresh each step; the `brainstate.transform` primitives lower the whole loop into one compiled XLA program, tracing the body only once. Pick by shape of the work: @@ -30,18 +25,53 @@ Biologically detailed brain cell modeling in BrainX. 13. **Every tracked `.py` file opens with the Apache-2.0 license header — add it when you create the file.** It goes at the very top, above the module docstring, below only a shebang or PEP 263 encoding line. See [License header](#license-header) for the verbatim block. +## Design, code, and examples + +All paths below are relative to the repository root (`/home/swl/braincell` in the current workspace). + +| Location | Maintained responsibility | +| --- | --- | +| `docs/design/` | Current design, interface contracts, module discussions, and implementation direction. | +| `braincell/` | Implementation and co-located tests. | +| Actual related examples, including root `examples/` | Usage examples relevant to the commit, kept at their existing locations. No duplicate or migration to `docs/examples/` is required. | + +Before each commit, review the relevant design, implementation and tests, and actual related examples once for the scope being committed. Routine development may leave these temporarily out of sync; no cross-document synchronization check is required after each edit or task. At the commit check, update affected interfaces, behavior descriptions, and example usage as needed. No-impact files need no edits. For behavior changes being committed, run the relevant code tests and affected examples; report what was checked and any unverified gaps. A discovered mismatch must be fixed within scope or recorded in the module document with a concrete follow-up; do not claim full consistency while a gap remains. This is a review requirement, not a new automatic Git hook. + +Design may lead implementation. Clearly distinguish implemented, partially implemented, planned, and research content. Executable examples must use implemented interfaces; proposed syntax belongs in explicitly marked design discussions. A documentation-only proposal does not require implementing the feature or adding an example of an unavailable API. + +### Module documents and project progress + +Human contributors start with [CONTRIBUTING.md](CONTRIBUTING.md) and the +[Developer Guide](docs/developer/index.rst). Developer pages explain contribution +steps and link to Design for module contracts, formulas, and architecture. + +See the [Repository organization guide](docs/repository.md) for directory responsibilities, +content placement, and the proposed layout awaiting migration confirmation. + +Follow [Design documentation rules](docs/design/AGENTS.md) when writing or updating `docs/design/`. +That directory-level guide owns writing style, module layout, document roles, and task status definitions. +Keep durable design prose under `docs/design/`, with implementation references linking to the relevant document. + +### History and deferred documentation + +- `docs/specs/YYYY-MM-DD-.md` preserves historical decisions and change records in creation-date order. Add a record when the history is useful; no new spec is required before every implementation. +- Current decisions and active plans live in `docs/design/`. Do not continually rewrite old specs to match new interfaces or use historical requirements to override current module documents. Old paths in historical records can remain historical references. +- Other documentation trees under `docs/`, including `docs/examples/`, are not mandatory parallel maintenance destinations. Select examples by their relevance to the commit, not by a required directory. Do not create duplicate examples, migrate them into `docs/examples/`, or bulk-refresh unrelated documentation or examples unless requested. This does not claim that all existing files are already consistent. +- Example-specific import progress, comparison settings, and validation work belong alongside the relevant examples (for example, `examples/neuron_compare/cerebellum-import-progress.md`). Reusable API and architecture contracts stay under `docs/design/` and are linked from the example record. +- The existing exception for an unstable `examples/experimental/` directory remains: one local `README.md` may map files, commands, status, and history, but must link to durable design/results prose under `docs/` rather than define public API or duplicate it. + ## Quick Reference ```bash # Install (dev) -pip install -e ".[testing]" +pip install -e ".[dev]" # Run tests (tests are co-located with source code) pytest braincell/ # Pre-commit pre-commit install -pre-commit run --all +pre-commit run --all-files ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f13bd604..5e58a919 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,206 +1,21 @@ -# Contributing to `braincell` +# 参与贡献 -Thank you for contributing to `braincell`. +BrainCell 接受问题报告、错误修复、新模型、文档和示例贡献。 -This project provides biologically detailed brain cell modeling tools built on top of JAX and the BrainX ecosystem. Contributions are welcome across code, tests, documentation, examples, bug reports, and design discussions. +- **报告问题**:在 [Issues](https://github.com/chaobrain/braincell/issues) 提供最小复现、预期与实际结果,以及环境版本。 +- **提出功能或设计改进**:先查看对应 [模块 TODO](docs/design/TODO.md),在 Issue 或 PR 中说明使用场景和方案取舍。 +- **提交修改**:按照 [贡献流程](docs/developer/contributing.md) 配置环境、修改代码、验证结果并提交 PR。 -By participating in this project, you agree to follow the guidance in [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). +详细指南: -## Table of contents - -- [Ways to contribute](#ways-to-contribute) -- [Development setup](#development-setup) -- [Running tests](#running-tests) -- [Code style](#code-style) -- [Project conventions](#project-conventions) -- [Building documentation](#building-documentation) -- [Pull requests](#pull-requests) -- [Reporting bugs and security issues](#reporting-bugs-and-security-issues) -- [License](#license) - -## Ways to contribute - -You can help by: - -- reporting bugs or unclear behavior -- proposing new features or API improvements -- improving documentation and tutorials -- adding tests for uncovered behavior -- contributing bug fixes or new functionality -- improving examples under `examples/` - -If you are planning a larger change, open an issue first so the scope and API impact can be discussed before implementation. - -`docs/design/TODO.md` is the project design document. It tracks the architectural intent and the current implementation state of every subsystem, using `[x]` shipped / `[~]` partial / `[ ]` planned markers. Read the relevant section before starting substantial work, and update it when your change moves a subsystem forward. - -## Development setup - -`braincell` requires Python 3.11 or newer. Continuous integration currently tests Python 3.13 on Linux, macOS, and Windows, and a nightly matrix exercises JAX 0.8.0 through the latest release. - -Create an isolated environment and install the package in editable mode with the development extras: - -```bash -python -m venv .venv -source .venv/bin/activate -python -m pip install --upgrade pip setuptools -python -m pip install -e ".[dev]" -``` - -On Windows PowerShell, activate the environment with: - -```powershell -.venv\Scripts\Activate.ps1 -``` - -Dependency groups are declared in `pyproject.toml` under `[project.optional-dependencies]`: - -| Extra | Contents | +| 要做什么 | 阅读入口 | | --- | --- | -| `vis` | `matplotlib`, `networkx`, `pyvista`, `plotly` — 2D and 3D visualization backends | -| `io` | `requests` — the NeuroMorpho.Org client | -| `all` | `vis` + `io`; everything a user-facing install might need | -| `testing` | `all` + `pytest`, `pytest-benchmark`, `hypothesis`, `absl-py` | -| `doc` | `all` + the Sphinx toolchain | -| `dev` | `testing` + `doc` + `pre-commit` | -| `cpu` / `cuda12` / `cuda13` / `tpu` | the matching JAX backend build; pick exactly one | - -The `requirements*.txt` files are thin pointers to these extras and exist only for tooling that expects them (CI and Read the Docs). Add new dependencies to `pyproject.toml`, never to the `requirements*.txt` files. - -Finally, install the git hooks: - -```bash -pre-commit install -``` - -## Running tests - -Run the test suite from the repository root: - -```bash -pytest braincell/ -``` - -Test discovery is configured in `pyproject.toml` under `[tool.pytest]`; it points at the `braincell` package and collects only `*_test.py` modules. - -To run a single module or a single test: - -```bash -pytest braincell/io/swc/reader_test.py -pytest braincell/io/swc/reader_test.py::SwcReaderTest::test_single_point_soma_expands_to_three_points_and_connects_at_midpoint -``` - -With coverage (configuration lives in `[tool.coverage]`): - -```bash -pytest braincell/ --cov=braincell --cov-report=term-missing -``` - -On Windows, CI disables the fault handler. If you hit platform-specific issues locally, this is the closest CI-equivalent command: - -```bash -pytest braincell/ -p no:faulthandler -``` - -Some suites skip themselves when an optional dependency is absent — `pytest-benchmark` for the performance baselines, `hypothesis` for the layout property tests, and `pyvista` / `plotly` for the 3D backends. Install the `testing` extra to run them. - -When you change behavior, add or update tests in the same area of the codebase. - -## Code style - -Formatting and linting are handled by [ruff](https://docs.astral.sh/ruff/), configured in `pyproject.toml` under `[tool.ruff]` and run through pre-commit: - -```bash -pre-commit run --all-files -``` - -The line length is 120. Quote style is set to `preserve`, so the existing mix of single and double quotes is left alone — do not reformat strings gratuitously. - -The lint rule set is currently minimal on purpose. `[tool.ruff]`'s `lint.ignore` list enumerates the rules the tree still violates, each with its violation count and the reason it is deferred rather than fixed. If your change cleans up one of those categories, delete the corresponding entry in the same pull request. - -## Project conventions - -These are the conventions that reviewers will look for. `AGENTS.md` holds the full version; the essentials are: - -**Units are mandatory.** Every public API that takes a physical quantity routes through `normalize_param()` in `braincell/_misc.py`, which rejects bare numerics with `TypeError`. Accept `python number / numpy array / jax array * brainunit unit`, store canonical SI internally, and hand values back with units attached — never raw floats. - -```python -import brainunit as u - -v_rest = -65.0 * u.mV # correct -v_rest = -65.0 # rejected with TypeError -``` - -**Tests are co-located and suffix-named.** Every module `foo.py` has its tests in a sibling `foo_test.py`. Never a separate `tests/` directory, and never the `test_*.py` prefix — `python_files` is set to `*_test.py` only, so a misnamed file is silently never run. Shared test helpers that are not themselves tests go in a leading-underscore module such as `_testing.py`. - -**Docstrings are NumPy-style.** See `AGENTS.md` for the canonical section order. Examples must be `.. code-block:: python` blocks that are doctest-compatible and self-contained. - -**Use `brainstate.random`,** not `jax.random` directly. - -**Do not drive a model with a bare Python loop.** Use `brainstate.transform.for_loop` / `scan` (or the `checkpointed_` variants under autograd) so the loop lowers into a single compiled XLA program. - -**Keep optional dependencies lazy.** `matplotlib`, `pyvista`, and `plotly` must be imported inside the backend that uses them, gated on `importlib.util.find_spec`, so that `import braincell` stays cheap. - -**Support JAX >= 0.8.0.** Prefer feature or shape detection over hard version checks. - -Write a spec under `docs/specs` before implementing a substantial change, so the design is available for reference during review. - -## Building documentation - -The documentation lives in `docs/` and uses Sphinx. - -```bash -python -m pip install -e ".[doc]" -cd docs -make html -``` - -On Windows: - -```powershell -cd docs -.\make.bat html -``` - -If you regenerate notebooks that contain interactive `vis3d(...)` output, also install the PyVista HTML export dependencies before running and saving those notebooks: - -```bash -python -m pip install ipywidgets trame trame-vtk trame-vuetify "jupyterlab>=3" -``` - -Documentation includes Markdown, reStructuredText, and notebooks. Notebook execution is disabled in the Sphinx configuration, so documentation changes should focus on content correctness and importability. For static interactive PyVista output, use `vis3d(notebook=True, jupyter_backend="html")`; the PyVista backend exports raw iframe HTML for this backend so the published docs page does not need to load the Jupyter widget manager. - -## Pull requests - -Before opening a pull request: - -1. make sure your branch is based on the latest target branch state -2. run `pre-commit run --all-files` -3. run the relevant tests locally -4. update documentation, examples, `docs/design/TODO.md`, or `changelog.md` if your change is user-facing -5. review the pull request template in `.github/PULL_REQUEST_TEMPLATE.md` - -When opening a pull request, include: - -- a clear description of the problem and the change -- links to any related issues -- the local test commands you ran -- notes about API changes, breaking changes, or follow-up work - -Keep pull requests small and focused. Please keep the following in mind: - -- keep public APIs and examples stable unless the change intentionally updates them -- avoid adding new dependencies unless they are clearly justified -- preserve compatibility with the supported Python versions declared in `pyproject.toml` - -Draft pull requests are welcome for early feedback. - -## Reporting bugs and security issues - -- For general bugs, feature requests, and usability issues, open a GitHub issue: -- For security-related concerns, **do not open a public issue** — see [SECURITY.md](SECURITY.md) for the private reporting channels. - -Please include enough detail to reproduce the problem: operating system, Python version, package versions, a minimal example, and the observed error or unexpected behavior. +| 找到相关代码与设计 | [代码与设计导航](docs/developer/project_layout.md) | +| 编写和运行测试 | [测试指南](docs/developer/testing.md) | +| 添加通道、离子、突触或积分器 | [扩展指南](docs/developer/extending.md) | +| 排查开发环境与构建问题 | [开发排错](docs/developer/troubleshooting.md) | -## License +Developer 说明贡献步骤,Design 维护模块的接口、架构和方案。仓库规则见 [AGENTS.md](AGENTS.md)。 -By contributing to this repository, you agree that your contributions will be distributed under the same license as the project — Apache License 2.0. See [LICENSE](LICENSE). +参与协作请遵守 [行为准则](CODE_OF_CONDUCT.md)。安全问题通过 [安全报告渠道](SECURITY.md) 私下提交。 +贡献采用项目的 [Apache-2.0 许可证](LICENSE)。 diff --git a/braincell/__init__.py b/braincell/__init__.py index 503af042..6bcf3a15 100644 --- a/braincell/__init__.py +++ b/braincell/__init__.py @@ -27,7 +27,7 @@ hidden_state, state_grouping, ) -from . import quad, mech, channel, synapse, ion, filter, morph, trainable +from . import quad, mech, channel, synapse, ion, filter, morph, reduction, trainable from ._base_channel import ( Channel, IonInfo, @@ -78,6 +78,20 @@ VoltageCrossingSource, ) from .network.recording import EventSeries, RecordingSchema, RecordingSpec, SampleBlock, observe +from .reduction import ( + EventAccumulatorReduction, + PayloadAccumulatorReduction, + ReductionContext, + ReductionInputGroup, + ReductionInputGroupSchema, + ReductionInputs, + ReductionModel, + ReductionOutput, + ReductionSynapse, + ReductionView, + ReductionViewCollection, + SynapticKernelAccumulatorReduction, +) from ._version import ( __version__, __version_info__, @@ -133,6 +147,7 @@ "DiffEqModule", "DiffEqSingleState", "DiffEqState", + "EventAccumulatorReduction", "EventSequence", "EventSeries", "EventSource", @@ -155,9 +170,19 @@ "NetworkResult", "Node", "NodeTree", + "PayloadAccumulatorReduction", "PointPlacement", "RecordingSchema", "RecordingSpec", + "ReductionContext", + "ReductionInputGroup", + "ReductionInputGroupSchema", + "ReductionInputs", + "ReductionModel", + "ReductionOutput", + "ReductionSynapse", + "ReductionView", + "ReductionViewCollection", "RunResult", "SampleBlock", "SineClamp", @@ -165,6 +190,7 @@ "Soma", "Synapse", "SynapseView", + "SynapticKernelAccumulatorReduction", "VoltageCrossingSource", "__version__", "__version_info__", @@ -180,6 +206,7 @@ "network", "observe", "quad", + "reduction", "state", "state_grouping", "synapse", diff --git a/braincell/__init___test.py b/braincell/__init___test.py index 107ef526..edbe3a07 100644 --- a/braincell/__init___test.py +++ b/braincell/__init___test.py @@ -41,6 +41,7 @@ "morph", "network", "quad", + "reduction", "synapse", "trainable", "vis", diff --git a/braincell/_base_channel.py b/braincell/_base_channel.py index b90cf89d..d173d898 100644 --- a/braincell/_base_channel.py +++ b/braincell/_base_channel.py @@ -33,9 +33,9 @@ import brainunit as u from braincell._typing import ArrayLike, Size -from braincell._parameter_schema import RuntimeParameterState +from braincell._parameter_schema import RuntimeParameterState, constructor_parameters from ._misc import TreeNode -from .mech import NoEventInput, ParameterSpec, StateSpec +from .mech import NoEventInput, StateSpec from .quad.protocol import DiffEqModule, DiffEqSingleState, IndependentIntegration __all__ = ["IonChannel", "IonInfo", "Channel", "Synapse"] @@ -337,7 +337,6 @@ def current(self, V, *args): __module__ = 'braincell' - parameters: Mapping[str, ParameterSpec] = {} states: Mapping[str, StateSpec] = {} def __getattribute__(self, name: str): @@ -355,8 +354,8 @@ def __setattr__(self, name: str, value) -> None: class Synapse(IonChannel): """Base class for vectorized runtime point-synapse mechanisms. - Subclasses declare physical parameters and differential states through - explicit schemas; any further public value is a plain property, resolved + Subclasses declare physical parameters in their constructors and states + through explicit schemas; any further public value is a property, resolved by name at the call site. ``current()`` always returns an inward-positive total point current. Discrete input is passed directly to :meth:`apply_events`; event buffers are owned by runtime routing. @@ -364,25 +363,56 @@ class Synapse(IonChannel): __module__ = 'braincell' - parameters: Mapping[str, ParameterSpec] = {} states: Mapping[str, StateSpec] = {} event_input = NoEventInput() - def __init__(self, size: Size, name: Optional[str] = None, **parameters): + def __init__(self, size: Size, name: Optional[str] = None): super().__init__(size=size, name=name) - unknown = tuple(sorted(set(parameters).difference(self.parameters))) - if unknown: - raise TypeError(f"Unknown {type(self).__name__} parameters: {unknown!r}.") - for field, spec in self.parameters.items(): - value = parameters.get(field, spec.default) + if getattr(type(self), "parameters", None): + raise TypeError("Synapse.parameters is retired; declare parameters in an explicit __init__ signature.") + + def __getattribute__(self, name): + value = super().__getattribute__(name) + return value.dense_value() if isinstance(value, RuntimeParameterState) else value + + def __setattr__(self, name, value): + current = vars(self).get(name) + if isinstance(current, RuntimeParameterState) and not isinstance(value, RuntimeParameterState): + current.value = value + return + super().__setattr__(name, value) + + @classmethod + def parameter_info(cls): + """Return physical field metadata inferred from the constructor. + + Returns + ------- + dict + Parameter names mapped to default-value and unit metadata. + """ + from braincell._parameter_schema import SignatureParameterSpec + + return { + name: SignatureParameterSpec(param.default) + for name, param in constructor_parameters(cls).items() + if name not in {"size", "name"} + } + + def _init_parameters(self, **parameters): + schema = self.parameter_info() + for field, value in parameters.items(): value = braintools.init.param(value, self.varshape, allow_none=False) - spec.validate(value, field) + schema[field].validate(value, field) setattr(self, field, value) - self.validate_parameters() + # A parent constructor can initialize its fields before a subclass adds + # its own signature fields with another _init_parameters() call. + if all(hasattr(self, field) for field in schema): + self.validate_parameters() def validate_parameters(self) -> None: """Validate relations involving more than one parameter.""" - self.validate_parameter_values({field: getattr(self, field) for field in self.parameters}) + self.validate_parameter_values({field: getattr(self, field) for field in self.parameter_info()}) @classmethod def validate_parameter_values(cls, parameters: Mapping[str, object]) -> None: diff --git a/braincell/_base_channel_test.py b/braincell/_base_channel_test.py index 2f837be3..66a8e8bf 100644 --- a/braincell/_base_channel_test.py +++ b/braincell/_base_channel_test.py @@ -23,6 +23,40 @@ class BaseChannelExportTest(unittest.TestCase): + def test_synapse_legacy_schema_requires_migration(self): + from braincell import Synapse + from braincell.mech import ParameterSpec + + class Legacy(Synapse): + parameters = {"gain": ParameterSpec(1.0)} + + with self.assertRaisesRegex(TypeError, "explicit __init__"): + Legacy(1) + + def test_synapse_signatures_forward_inheritance_and_required_fields(self): + import brainunit as u + import numpy as np + from braincell import Synapse + from braincell.synapse import ExpSyn + + class Extended(ExpSyn): + def __init__(self, size, gain=1.0, **kwargs): + super().__init__(size, **kwargs) + self._init_parameters(gain=gain) + + self.assertEqual(set(Extended.parameter_info()), {"tau", "e", "gain"}) + self.assertEqual(float(Extended(1).gain), 1.0) + + class Required(Synapse): + def __init__(self, size, reversal): + super().__init__(size) + self._init_parameters(reversal=reversal) + + node = Required(2, -70.0 * u.mV) + np.testing.assert_allclose(node.reversal.to_decimal(u.mV), [-70.0, -70.0]) + node = ExpSyn(2, tau=lambda shape: np.full(shape, 3.0) * u.ms) + np.testing.assert_allclose(node.tau.to_decimal(u.ms), [3.0, 3.0]) + def test_public_namespace_reexports_this_module(self) -> None: import braincell import braincell._base_channel as channel_mod diff --git a/braincell/_base_ion.py b/braincell/_base_ion.py index b2026ef7..12686304 100644 --- a/braincell/_base_ion.py +++ b/braincell/_base_ion.py @@ -34,6 +34,7 @@ from brainstate.mixin import _JointGenericAlias from braincell._typing import Size +from braincell._parameter_schema import RuntimeParameterState from ._base_channel import Channel, IonChannel, IonInfo from ._base_neuron import HHTypedNeuron from ._misc import ( @@ -261,6 +262,17 @@ class Ion(IonChannel, Container): _container_name = 'channels' root_type = HHTypedNeuron + def __getattribute__(self, name: str): + value = super().__getattribute__(name) + return value.dense_value() if isinstance(value, RuntimeParameterState) else value + + def __setattr__(self, name: str, value) -> None: + current = vars(self).get(name) + if isinstance(current, RuntimeParameterState) and not isinstance(value, RuntimeParameterState): + current.value = value + return + super().__setattr__(name, value) + def __init__(self, size: Size, name: Optional[str] = None, **channels) -> None: super().__init__(size, name=name) self.channels: Dict[str, Channel] = dict() diff --git a/braincell/_base_ion_test.py b/braincell/_base_ion_test.py index abbfe588..8dde2ad5 100644 --- a/braincell/_base_ion_test.py +++ b/braincell/_base_ion_test.py @@ -516,5 +516,19 @@ def test_the_mixed_pool_is_freed_without_the_cyclic_collector(self) -> None: gc.enable() +class IonRuntimeParameterTest(unittest.TestCase): + def test_attribute_assignment_preserves_runtime_parameter_identity(self): + from braincell._parameter_schema import RuntimeParameterState + from braincell.ion import SodiumFixed + + ion = SodiumFixed(size=2) + parameter = RuntimeParameterState(50 * u.mV, full_shape=(2,)) + ion.E = parameter + self.assertTrue(u.math.allclose(ion.E, 50 * u.mV)) + ion.E = 45 * u.mV + self.assertIs(vars(ion)["E"], parameter) + self.assertTrue(u.math.allclose(ion.E, 45 * u.mV)) + + if __name__ == "__main__": unittest.main() diff --git a/braincell/_base_neuron.py b/braincell/_base_neuron.py index 1a56439a..5fbff1e1 100644 --- a/braincell/_base_neuron.py +++ b/braincell/_base_neuron.py @@ -61,6 +61,16 @@ def _zero_spike_like(V): return u.math.zeros_like(u.get_magnitude(V)) +def _threshold_crossing(last_V, next_V, threshold, spk_fun, *, direction="rising"): + """Detect arrival at a threshold with matching forward/JVP/VJP rules.""" + sign = 1.0 if direction == "rising" else -1.0 + denom = _cast_like(20.0 * u.mV, next_V) + threshold = _cast_like(threshold, next_V) + old = sign * (last_V - threshold) / denom + new = sign * (next_V - threshold) / denom + return spk_fun(new) * (1.0 - spk_fun(old)) + + class HHTypedNeuron(brainpy.state.Dynamics, Container, DiffEqModule): """Base class for Hodgkin-Huxley typed neuronal membrane dynamics. @@ -262,6 +272,4 @@ def get_spike(self, last_V, next_V): product of rising- and falling-crossing terms produces a non-zero value only when ``last_V < V_th <= next_V``. """ - denom = _cast_like(20.0 * u.mV, next_V) - V_th = _cast_like(self.V_th, next_V) - return self.spk_fun((next_V - V_th) / denom) * self.spk_fun((V_th - last_V) / denom) + return _threshold_crossing(last_V, next_V, self.V_th, self.spk_fun) diff --git a/braincell/_base_neuron_test.py b/braincell/_base_neuron_test.py index 2ae0c1ca..8ddecc00 100644 --- a/braincell/_base_neuron_test.py +++ b/braincell/_base_neuron_test.py @@ -58,6 +58,14 @@ def test_module_does_not_import_base_ion(self) -> None: class HHTypedNeuronGetSpikeTest(unittest.TestCase): """ARCH-04: get_spike lives on the shared base, not on each subclass.""" + def test_arrival_boundary_does_not_repeat_at_threshold(self): + from braincell._single_compartment.base import SingleCompartment + + sc = SingleCompartment(size=1, V_th=0.0 * u.mV) + old, new = np.meshgrid([-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]) + actual = sc.get_spike(jnp.asarray(old) * u.mV, jnp.asarray(new) * u.mV) + np.testing.assert_array_equal(actual, (old < 0) & (new >= 0)) + def test_get_spike_is_method_on_base(self) -> None: self.assertTrue(hasattr(HHTypedNeuron, "get_spike")) self.assertTrue(callable(HHTypedNeuron.get_spike)) diff --git a/braincell/_compute/__init___test.py b/braincell/_compute/__init___test.py index 835c2c6a..7e54609d 100644 --- a/braincell/_compute/__init___test.py +++ b/braincell/_compute/__init___test.py @@ -472,7 +472,7 @@ def test_graph_matches_the_declared_layering(self) -> None: "__init__": set(), "bindings": {"ions", "layouts", "parameters"}, "bridge": set(), - "ions": {"layouts"}, + "ions": {"layouts", "parameters"}, "layouts": {"parameters"}, "parameters": set(), "scheduling": set(), diff --git a/braincell/_compute/bindings.py b/braincell/_compute/bindings.py index 6ae5a7b4..320319df 100644 --- a/braincell/_compute/bindings.py +++ b/braincell/_compute/bindings.py @@ -73,6 +73,7 @@ import brainunit as u import jax.numpy as jnp import numpy as np +import brainstate from braincell._base_channel import Channel from braincell.channel._base import Markov @@ -354,7 +355,7 @@ def _instantiate_runtime_node( # The runtime class's declared parameters are the same schema state.py used to # write these buffers, so select against it rather than inferring which buffers # are constructor arguments from a leading-underscore naming convention. - declared = set(runtime_cls.parameters) + declared = set(runtime_cls.parameter_info()) parameter_names = tuple( var_name for layout_id, var_name in state_buffers @@ -369,7 +370,17 @@ def _instantiate_runtime_node( for var_name in parameter_names } size = (int(layout.n_active),) - node = runtime_cls(size=size, name=mechanism.synapse_type, **params) + node = runtime_cls( + size=size, + name=mechanism.synapse_type, + **{ + key: value.dense_value() if isinstance(value, RuntimeParameterState) else value + for key, value in params.items() + }, + ) + for key, value in params.items(): + if isinstance(value, RuntimeParameterState) and not isinstance(getattr(node, key), brainstate.nn.Param): + setattr(node, key, value) return node, (), None if layout.target != "density" or layout.layout != "dense": @@ -386,7 +397,7 @@ def _instantiate_runtime_node( size = next(iter(params.values())).shape else: size = pop_size + (layout.spatial_axis_len,) - node = runtime_cls(size=size, **params) + node = runtime_cls(size=size, **_constructor_parameter_values(params)) _attach_runtime_parameter_states(node, params) bound_ions, current_owner_specs = _resolve_channel_runtime_bindings( runtime_cls=runtime_cls, @@ -463,6 +474,17 @@ def _install_merged_channel_nodes( ) if len(owner_specs) != 1: continue + if any((layout.id, name) not in state_buffers for name in mechanism.params): + # Non-numeric constructor arguments cannot be scattered across CVs. + continue + static_configuration = [] + for name in density_parameter_names(mechanism): + buffer = state_buffers[(layout.id, name)] + raw = buffer.value if isinstance(buffer, RuntimeParameterState) else buffer + if not isinstance(raw, u.Quantity): + array = np.asarray(raw) + if array.dtype.kind == "b": + static_configuration.append((name, array.shape, array.tobytes())) key = ( runtime_cls, mechanism.instance_name, @@ -471,6 +493,7 @@ def _install_merged_channel_nodes( mechanism.substeps, tuple(ion_key for ion_key, _ in bound_ions), owner_specs, + tuple(static_configuration), ) groups.setdefault(key, []).append((layout, mechanism, runtime_cls, bound_ions, owner_specs)) @@ -489,7 +512,7 @@ def _install_merged_channel_nodes( state_buffers=state_buffers, ) size = pop_size + (n_cv,) - node = runtime_cls(size=size, **params) + node = runtime_cls(size=size, **_constructor_parameter_values(params)) _attach_runtime_parameter_states(node, params) cv_mask = np.zeros((n_cv,), dtype=bool) for layout, *_ in merge_items: @@ -554,17 +577,19 @@ def _merged_channel_constructor_params( state_buffers: dict[tuple[int, str], np.ndarray], ) -> dict[str, object]: all_param_names = [] - for _layout, mechanism, *_ in items: - for name in density_parameter_names(mechanism): + for layout, mechanism, *_ in items: + for buffer_layout_id, name in state_buffers: + if buffer_layout_id != layout.id: + continue if name not in all_param_names: all_param_names.append(name) - params = {} + params = { + name: value for name, value in items[0][1].params.items() if name not in density_parameter_names(items[0][1]) + } full_shape = pop_size + (n_cv,) for var_name in all_param_names: - value_items = [ - (layout, mechanism) for layout, mechanism, *_ in items if var_name in density_parameter_names(mechanism) - ] + value_items = [(layout, mechanism) for layout, mechanism, *_ in items if (layout.id, var_name) in state_buffers] if not value_items: continue first_layout, _first_mechanism = value_items[0] @@ -936,7 +961,7 @@ def _sync_runtime_node_param(runtime: CellRuntimeState, *, layout_id: int, var_n layout = runtime.layouts[int(layout_id)] kind = layout.kind if kind.startswith("ion:"): - _sync_runtime_ion(runtime, layout_id=int(layout_id)) + _sync_runtime_ion(runtime, layout_id=int(layout_id), var_name=var_name) return merged_groups = runtime.merged_channel_layout_groups or {} merged_layout_ids = merged_groups.get(int(layout_id)) @@ -946,6 +971,7 @@ def _sync_runtime_node_param(runtime: CellRuntimeState, *, layout_id: int, var_n layout_ids=merged_layout_ids, var_name=str(var_name), ) + new_value = _preserve_parameter_coercion(node, var_name, new_value) setattr(node, var_name, new_value) node._on_param_updated(var_name, new_value) return @@ -954,10 +980,20 @@ def _sync_runtime_node_param(runtime: CellRuntimeState, *, layout_id: int, var_n var_name=var_name, state_buffers=runtime.state_buffers, ) + new_value = _preserve_parameter_coercion(node, var_name, new_value) setattr(node, var_name, new_value) node._on_param_updated(var_name, new_value) +def _preserve_parameter_coercion(node, name, value): + """Keep scalar bool/int constructor conversions on subsequent writes.""" + current = vars(node).get(name) + if type(current) in (bool, int): + raw = value.value if isinstance(value, RuntimeParameterState) else value + return type(current)(raw) + return value + + def _merged_channel_param_value( runtime: CellRuntimeState, *, @@ -1022,16 +1058,41 @@ def _runtime_constructor_params( if mechanism.category != "channel": return {} return { - var_name: _runtime_param_value(layout=layout, var_name=var_name, state_buffers=state_buffers) - for var_name in density_parameter_names(mechanism) + **dict(mechanism.params), + **{ + var_name: _runtime_param_value(layout=layout, var_name=var_name, state_buffers=state_buffers) + for buffer_layout_id, var_name in state_buffers + if buffer_layout_id == layout.id + }, } +def _constructor_parameter_values(params: dict[str, object]) -> dict[str, object]: + """Pass arrays so a scalar result identifies an actual constructor coercion.""" + result = dict(params) + for name, value in params.items(): + if isinstance(value, RuntimeParameterState): + array = u.math.asarray(value.value) + if not isinstance(array, u.Quantity): + if array.dtype.kind == "b": + concrete = np.asarray(array) + if concrete.size and np.all(concrete == concrete.flat[0]): + array = array.reshape(-1)[0] + result[name] = array + return result + + def _attach_runtime_parameter_states(node: object, params: dict[str, object]) -> None: """Restore schema parameter states unwrapped by ``braintools.init.param``.""" for name, value in params.items(): if isinstance(value, RuntimeParameterState): - setattr(node, name, value) + # Retain explicit constructor coercions and forced subclass settings. + current = getattr(node, name, None) + if isinstance(current, (bool, int)): + value.value = current + value.axis = "uniform" + else: + setattr(node, name, value) def _is_root_level_runtime_node(kind: str) -> bool: diff --git a/braincell/_compute/ions.py b/braincell/_compute/ions.py index ae89c1b5..90f86b13 100644 --- a/braincell/_compute/ions.py +++ b/braincell/_compute/ions.py @@ -13,56 +13,33 @@ # limitations under the License. # ============================================================================== -"""Runtime ion instantiation, parameter normalization, and synchronization. - -This module owns everything that turns a cell's ``Density`` ion declarations -into live runtime ion objects, and everything that keeps those objects in step -with the state buffers afterwards: - -- :func:`_build_runtime_ions` — the entry point. Collects the ion declarations - spread across mechanism layouts, instantiates one runtime ion per instance - name, fills in a placeholder ion for every family - :func:`braincell.ion.build_placeholder_ions` supplies that was never - declared, and returns a 5-tuple of the ion map, the alias map that lets - channels look an ion up by instance name, family key, or class name, the - family and class candidate maps, and the per-layout runtime-ion nodes. -- :func:`_collect_runtime_ion_instances`, :func:`_build_ion_alias_map`, - :func:`ion_species_key`, :func:`_runtime_ion_species_key` — the grouping and - naming rules, including the conflict check that rejects an instance name - reused across two ion classes. -- :func:`_supported_ion_runtime_params`, :func:`_ion_runtime_attr_name`, - :func:`_normalize_ion_runtime_param_value` — introspection of a runtime ion - class's constructor and the small amount of renaming/unwrapping needed to - read a param back off an instance. -- :func:`_ion_param_broadcast` and :func:`_ion_param_scatter` — the rectangular - buffer algebra. A baseline param is broadcast onto the full CV shape once, - then each declaration layout scatters its own CV rows into it, so the - common rectangular path needs no Python loop over per-CV - :class:`brainunit.Quantity` boxes. -- :func:`_sync_runtime_ion` — the post-compilation counterpart, rebuilding one - runtime ion's params from the current state buffers when a buffer is written. - -Ion construction happens before channels are bound, so this module depends only -on :mod:`braincell._compute.layouts` (for the ``MechanismLayout`` record and the -constant-quantity helper), on ``braincell.mech``, and on ``braincell.ion`` — -including its private :mod:`braincell.ion._base` module, for the runtime ion -base classes. It imports nothing from :mod:`braincell._compute.bindings` or -:mod:`braincell._compute.state`, which both sit above it in the layer stack. +"""Build and synchronize runtime ions from signature-derived density parameters. + +One runtime ion owns each named pool across its declaration layouts. Numeric +parameters use persistent runtime states and differentiable regional scatters. +Initializers combine explicit regional values with live model defaults; their +states are distinct from the differential species created at initialization. + +This module sits below bindings/state and does not import their runtime code. """ from __future__ import annotations import functools -import inspect from typing import TYPE_CHECKING import brainunit as u +import braintools +import brainstate +import jax +import jax.numpy as jnp import numpy as np from braincell.ion import build_placeholder_ions from braincell.ion._base import DynamicNernstIon, InitNernstIon -from braincell.mech import Density, get_registry -from .layouts import MechanismLayout, _constant_quantity_value +from braincell.mech import Density, Params, get_registry +from .layouts import MechanismLayout +from .parameters import RuntimeParameterState, _density_signature, parameter_state_value if TYPE_CHECKING: from .state import CellRuntimeState @@ -259,18 +236,7 @@ def _runtime_ion_species_key(cls: type) -> str: @functools.lru_cache(maxsize=None) def _supported_ion_runtime_params(cls: type) -> tuple[str, ...]: - signature = inspect.signature(cls.__init__) - supported: list[str] = [] - excluded = {"solver", "substeps", "species_initializers"} - for name, parameter in signature.parameters.items(): - if name in {"self", "size", "name"}: - continue - if name in excluded: - continue - if parameter.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): - continue - supported.append(name) - return tuple(supported) + return tuple(name for name in _density_signature(cls) if name not in {"size", "name"}) def _ion_runtime_attr_name(cls: type, param_name: str) -> str: @@ -279,19 +245,6 @@ def _ion_runtime_attr_name(cls: type, param_name: str) -> str: return param_name -def _normalize_ion_runtime_param_value(cls: type, param_name: str, value: object) -> object: - if param_name == "Ci_initializer" and issubclass(cls, DynamicNernstIon): - if isinstance(value, u.Quantity): - return value - inner = getattr(value, "value", None) - if isinstance(inner, u.Quantity): - return inner - constant_quantity = _constant_quantity_value(value) - if constant_quantity is not None: - return constant_quantity - return value - - def _instantiate_runtime_ion_instance( *, instance_name: str, @@ -304,11 +257,9 @@ def _instantiate_runtime_ion_instance( ) -> object: """Build one runtime ion instance from its density declaration layouts. - Start from a baseline ion and replace per-CV params where each - declaration layout requests them. Each layout's buffer is scattered - into the accumulated array by :func:`_ion_param_scatter`, which uses - ``np.put_along_axis`` on the Quantity mantissa — no Python loops on - per-point Quantity boxes. + Merge each layout's numeric buffers with JAX scatters. Initializer + overrides retain a separate mask so unselected points continue to + evaluate the model's live defaults. """ supported_params = _supported_ion_runtime_params(runtime_cls) unsupported_params: dict[int, set[str]] = {} @@ -324,175 +275,97 @@ def _instantiate_runtime_ion_instance( ) full_size = pop_size + (n_cv,) - baseline_ion = runtime_cls(size=full_size) - full_param_values: dict[str, object] = {} - for param_name in supported_params: - baseline_value = _normalize_ion_runtime_param_value( - runtime_cls, - param_name, - getattr(baseline_ion, _ion_runtime_attr_name(runtime_cls, param_name)), - ) - full_param_values[param_name] = _ion_param_broadcast(baseline_value, shape=full_size) - + baseline = runtime_cls(size=full_size) + fields = tuple( + dict.fromkeys(field for layout in layouts for layout_id, field in state_buffers if layout_id == layout.id) + ) + params = {} for layout, declaration in zip(layouts, declarations): - point_index = np.asarray(layout.source_cv_ids, dtype=np.int32) - for param_name in declaration.params.keys(): - buffer = state_buffers[(layout.id, param_name)] - full_param_values[param_name] = _ion_param_scatter( - runtime_cls=runtime_cls, - param_name=param_name, - target=full_param_values[param_name], - buffer=buffer, - point_index=point_index, - ) + for name, value in declaration.params.items(): + if name not in fields: + if name in params and Params({name: params[name]}) != Params({name: value}): + raise ValueError(f"Ion {instance_name!r} requires uniform constructor configuration {name!r}.") + params[name] = value + values = {} + for name in fields: + raw = getattr(baseline, _ion_runtime_attr_name(runtime_cls, name)) + if callable(raw): + raw = braintools.init.param(raw, full_size) + value = u.math.broadcast_to(raw, full_size) + for layout in layouts: + if (layout.id, name) in state_buffers: + value = _scatter_numeric(value, state_buffers[(layout.id, name)], layout.source_cv_ids) + values[name] = value + if not name.endswith("_initializer"): + if not isinstance(value, u.Quantity): + concrete = np.asarray(value) + if np.all(concrete == concrete.flat[0]): + value = value.reshape(-1)[0] + params[name] = value + node = runtime_cls(size=full_size, name=instance_name, **params) + node._runtime_ion_parameters = {} + node._runtime_ion_initial_masks = {} + for name, value in values.items(): + attr = _ion_runtime_attr_name(runtime_cls, name) + current = getattr(node, attr) + state = RuntimeParameterState(value, axis="row", full_shape=full_size) + node._runtime_ion_parameters[name] = state + if name.endswith("_initializer"): + mask = np.zeros(full_size, dtype=bool) + for layout in layouts: + buffer = state_buffers.get((layout.id, name)) + if buffer is not None: + mask |= buffer.initial_override_mask + mask_state = brainstate.LongTermState(jnp.asarray(mask)) + node._runtime_ion_initial_masks[name] = mask_state + setattr(node, attr, state if np.all(mask) else _regional_initializer(current, state, mask_state)) + elif type(current) in (bool, int): + state.value = u.math.full(full_size, current) + for layout in layouts: + buffer = state_buffers.get((layout.id, name)) + if isinstance(buffer, RuntimeParameterState): + buffer.value = current + buffer.axis = "uniform" + else: + state.value = u.math.broadcast_to(current, full_size) + setattr(node, attr, state) + return node - runtime_ion_instance = runtime_cls(size=full_size, name=instance_name, **full_param_values) - _restore_shaped_species_initializers(runtime_ion_instance, full_param_values) - return runtime_ion_instance +def _uniform_scalar(value, name): + array = u.math.asarray(value) + if not isinstance(array, jax.core.Tracer): + concrete = np.asarray(array) + if not np.all(concrete == concrete.flat[0]): + raise ValueError(f"Ion constructor configuration {name!r} must be uniform.") + return array.reshape(-1)[0] -def _restore_shaped_species_initializers(runtime_ion_instance: object, full_param_values: dict[str, object]) -> None: - if "Ci_initializer" not in full_param_values: - return - species_initializers = getattr(runtime_ion_instance, "species_initializers", None) - if not isinstance(species_initializers, dict) or "Ci" not in species_initializers: - return - shaped_ci = full_param_values["Ci_initializer"] - if not isinstance(shaped_ci, (u.Quantity, np.ndarray)): - cainull = getattr(runtime_ion_instance, "cainull", None) - if isinstance(cainull, (u.Quantity, np.ndarray)): - shaped_ci = cainull - # ``Ci_initializer`` is a property over this same dict entry, so writing - # the dict is the whole update -- assigning both wrote it twice. - species_initializers["Ci"] = shaped_ci - - -def _ion_param_broadcast(value: object, *, shape: tuple[int, ...]) -> object: - """Broadcast an ion baseline value onto ``shape``. - - Handles three cases: already-shaped Quantity pass-through, scalar - Quantity broadcast, and plain numeric / object fallbacks. Returns - a buffer that :func:`_ion_param_scatter` copies and updates via - ``np.put_along_axis``. - """ - if isinstance(value, u.Quantity): - raw = value.mantissa if hasattr(value, "mantissa") else value.to_decimal(value.unit) - mantissa = np.asarray(raw, dtype=np.float64) - if mantissa.shape == shape: - return u.Quantity(mantissa.copy(), value.unit) - if mantissa.ndim == 0 or mantissa.shape == (): - return u.Quantity(np.full(shape, float(mantissa), dtype=np.float64), value.unit) - raise ValueError(f"Cannot broadcast ion baseline value with shape {mantissa.shape!r} onto shape {shape!r}.") - # Plain numeric baseline (e.g., valence): broadcast as numpy array. - if isinstance(value, (np.ndarray,)) or isinstance(value, (int, float)): - arr = np.asarray(value) - if arr.shape == shape: - return arr.copy() - if arr.ndim == 0: - return np.broadcast_to(arr, shape).copy() - if hasattr(value, "shape") and not callable(value): - arr = np.asarray(value) - if arr.shape == shape: - return arr.copy() - if arr.ndim == 0: - return np.broadcast_to(arr, shape).copy() - # Callable / opaque baseline: keep as tuple of length shape[0]. - n = int(np.prod(shape, dtype=int)) if shape else 1 - return tuple(value for _ in range(n)) - - -def _ion_param_scatter( - *, - runtime_cls: type, - param_name: str, - target: object, - buffer: object, - point_index: np.ndarray, -) -> object: - """Scatter the values of one sparse ion-layout buffer into ``target``. - - For ``Ci_initializer`` on :class:`DynamicNernstIon` (which may hold a - State-wrapped callable), box the ``target``/``buffer`` values into an - object-dtype array with a per-CV Python loop, then scatter that - array via ``np.put_along_axis`` like the other branches. Rectangular - Quantity and ndarray buffers scatter directly via ``np.put_along_axis`` - onto a copy of ``target``, with unit coercion for Quantity buffers. - """ - if isinstance(target, u.Quantity) and isinstance(buffer, u.Quantity): - target_unit = target.unit - src_mantissa = np.asarray(buffer.mantissa, dtype=np.float64) - target_mantissa = np.asarray(target.mantissa, dtype=np.float64) - # Layout buffers and runtime ions both end with the CV axis. - # Any leading axes are homogeneous-population dimensions. - if src_mantissa.shape[-1:] == point_index.shape: - src = src_mantissa - else: - src = np.take(src_mantissa, point_index, axis=-1) - incoming = np.asarray(u.Quantity(src, buffer.unit).to_decimal(target_unit), dtype=np.float64) - new_mantissa = target_mantissa.copy() - np.put_along_axis( - new_mantissa, - np.reshape(point_index, (1,) * (new_mantissa.ndim - 1) + point_index.shape), - incoming, - axis=-1, - ) - return u.Quantity(new_mantissa, target_unit) - - if isinstance(target, tuple): - if isinstance(buffer, u.Quantity): - flat = [u.Quantity(value, buffer.unit) for value in np.asarray(buffer.mantissa, dtype=object).reshape(-1)] - src_arr = np.empty(len(flat), dtype=object) - for index, value in enumerate(flat): - src_arr[index] = value - src_arr = src_arr.reshape(buffer.mantissa.shape) - elif isinstance(buffer, tuple): - src_arr = np.asarray(buffer, dtype=object).reshape(np.asarray(buffer, dtype=object).shape) - else: - src_arr = np.asarray(buffer, dtype=object) - src_arr = src_arr if src_arr.shape[-1:] == point_index.shape else np.take(src_arr, point_index, axis=-1) - leading_shape = src_arr.shape[:-1] - leading_size = int(np.prod(leading_shape, dtype=int)) if leading_shape else 1 - target_flat = np.empty(len(target), dtype=object) - for index, value in enumerate(target): - target_flat[index] = value - target_arr = target_flat.reshape(leading_shape + (len(target) // leading_size,)) - np.put_along_axis( - target_arr, - np.reshape(point_index, (1,) * (target_arr.ndim - 1) + point_index.shape), - src_arr, - axis=-1, - ) - return tuple(target_arr.reshape(-1).tolist()) - - if isinstance(target, np.ndarray): - new_target = target.copy() - if isinstance(buffer, u.Quantity): - src = np.asarray(buffer.mantissa) - elif isinstance(buffer, np.ndarray): - src = buffer - else: - raise TypeError(f"Cannot scatter non-array buffer into numpy target for ion param {param_name!r}.") - src = src if src.shape[-1:] == point_index.shape else np.take(src, point_index, axis=-1) - np.put_along_axis( - new_target, - np.reshape(point_index, (1,) * (new_target.ndim - 1) + point_index.shape), - src, - axis=-1, - ) - return new_target - raise TypeError( - f"Unsupported target/buffer combination for ion param {param_name!r}: " - f"target={type(target).__name__}, buffer={type(buffer).__name__}." - ) +def _scatter_numeric(target, buffer, point_ids): + value = parameter_state_value(buffer) + unit = target.unit if isinstance(target, u.Quantity) else None + raw = value.to_decimal(unit) if unit is not None else value + old = target.mantissa if unit is not None else target + ids = jnp.asarray(point_ids, dtype=jnp.int32) + if raw.shape[-1:] != ids.shape: + raw = jnp.take(raw, ids, axis=-1) + result = jnp.asarray(old, dtype=jnp.result_type(old, raw)).at[..., ids].set(raw) + return u.Quantity(result, unit) if unit is not None else result + +def _regional_initializer(default, state, mask): + def initialize(shape): + fallback = braintools.init.param(default, shape) + return u.math.where(mask.value, state.dense_value(), fallback) -def _sync_runtime_ion(runtime: CellRuntimeState, *, layout_id: int) -> None: + return initialize + + +def _sync_runtime_ion(runtime: CellRuntimeState, *, layout_id: int, var_name: str | None = None) -> None: """Rebuild the runtime ion's per-point params from state buffers. - Uses :func:`_ion_param_scatter`, which vectorises via - ``np.put_along_axis`` on Quantity mantissas instead of Python - per-index loops. + Update persistent parameter states so repeated compiled calls observe + new values without retaining tracers in ordinary Python attributes. """ mechanism = runtime.layout_mechanisms[int(layout_id)] if not isinstance(mechanism, Density) or mechanism.category != "ion": @@ -500,36 +373,23 @@ def _sync_runtime_ion(runtime: CellRuntimeState, *, layout_id: int) -> None: instance_name = mechanism.instance_name ion = runtime.ions[instance_name] ion_cls = type(ion) - supported_params = _supported_ion_runtime_params(ion_cls) - - full_values: dict[str, object] = {} - for param_name in supported_params: - baseline = _normalize_ion_runtime_param_value( - ion_cls, - param_name, - getattr(ion, _ion_runtime_attr_name(ion_cls, param_name)), - ) - full_values[param_name] = _ion_param_broadcast(baseline, shape=runtime.pop_size + (runtime.n_cv,)) - - for candidate in runtime.layouts: - candidate_mechanism = runtime.layout_mechanisms[candidate.id] - if candidate.target != "density": - continue - if not isinstance(candidate_mechanism, Density) or candidate_mechanism.category != "ion": - continue - if candidate_mechanism.instance_name != instance_name: - continue - for param_name in candidate_mechanism.params.keys(): - buffer = runtime.state_buffers[(candidate.id, param_name)] - full_values[param_name] = _ion_param_scatter( - runtime_cls=ion_cls, - param_name=param_name, - target=full_values[param_name], - buffer=buffer, - point_index=np.asarray(candidate.source_cv_ids, dtype=np.int32), - ) - - for param_name, value in full_values.items(): - setattr(ion, _ion_runtime_attr_name(ion_cls, param_name), value) + states = ion._runtime_ion_parameters + for param_name in states if var_name is None else (var_name,): + state = states[param_name] + value = state.dense_value() + for candidate in runtime.layouts: + candidate_mechanism = runtime.layout_mechanisms[candidate.id] + if not isinstance(candidate_mechanism, Density) or candidate_mechanism.category != "ion": + continue + if candidate_mechanism.instance_name != instance_name: + continue + buffer = runtime.state_buffers.get((candidate.id, param_name)) + if buffer is not None: + value = _scatter_numeric(value, buffer, candidate.source_cv_ids) + attr = _ion_runtime_attr_name(ion_cls, param_name) + current = vars(ion).get(attr) + if type(current) in (bool, int): + setattr(ion, attr, type(current)(_uniform_scalar(value, param_name))) + state.value = value if isinstance(ion, InitNernstIon): ion._update_reversal() diff --git a/braincell/_compute/ions_test.py b/braincell/_compute/ions_test.py index 11ad9d9a..93d4fb72 100644 --- a/braincell/_compute/ions_test.py +++ b/braincell/_compute/ions_test.py @@ -19,6 +19,7 @@ import braintools import brainunit as u +import brainstate import numpy as np import braincell @@ -30,6 +31,53 @@ class RuntimeIonTest(unittest.TestCase): """Default, named, dynamic, and kinetic ion behaviour in the cell runtime.""" + def setUp(self): + # These legacy reference assertions require twelve decimal places. + # Trainable-manager tests separately exercise the default float32 path. + self.enterContext(brainstate.environ.context(precision=64)) + + def test_explicit_species_dictionary_is_preserved(self): + cell = Cell(_build_tree()) + cell.paint( + BranchSlice(branch_index=[0, 1], prox=0, dist=1), + braincell.mech.Ion("CdpStC_NoCAM_MA2020_GoC", name="pool", species_initializers={"Buff2": 7 * u.mM}), + ) + cell.init_state() + self.assertTrue(u.math.allclose(cell.get_ion("pool").Buff2.value, 7 * u.mM)) + cell.ions["pool"].set(Buffnull2=100 * u.mM) + cell.reset_state() + self.assertTrue(u.math.allclose(cell.get_ion("pool").Buff2.value, 7 * u.mM)) + + def test_conflicting_pool_configuration_is_rejected(self): + cell = Cell(_build_tree()) + for branch, value in ((0, 7), (1, 8)): + cell.paint( + BranchSlice(branch_index=branch, prox=0, dist=1), + braincell.mech.Ion( + "CdpStC_NoCAM_MA2020_GoC", name="pool", species_initializers={"Buff2": value * u.mM} + ), + ) + with self.assertRaisesRegex(ValueError, "uniform constructor configuration"): + cell.init_state() + + def test_equivalent_initializer_dictionaries_ignore_key_order(self): + cell = Cell(_build_tree()) + for branch, initializers in ( + (0, {"Buff2": 7 * u.mM, "PV": 0.08 * u.mM}), + (1, {"PV": 0.08 * u.mM, "Buff2": 7 * u.mM}), + ): + cell.paint( + BranchSlice(branch_index=branch, prox=0, dist=1), + braincell.mech.Ion( + "CdpStC_NoCAM_MA2020_GoC", + name="pool", + Nannuli=10.0 + branch, + species_initializers=initializers, + ), + ) + cell.init_state() + self.assertTrue(u.math.allclose(cell.get_ion("pool").Buff2.value, 7 * u.mM)) + def test_default_ions_are_available_with_global_shape(self) -> None: import braincell diff --git a/braincell/_compute/parameters.py b/braincell/_compute/parameters.py index 63fc765c..55f2defe 100644 --- a/braincell/_compute/parameters.py +++ b/braincell/_compute/parameters.py @@ -13,17 +13,21 @@ # limitations under the License. # ============================================================================== -"""Schema-aware density parameter storage.""" +"""Signature-derived Channel/Ion metadata and compact parameter storage.""" from __future__ import annotations from collections.abc import Mapping +import inspect +import functools +import braintools import brainunit as u import jax.numpy as jnp import numpy as np -from braincell._parameter_schema import ParameterSpec, RuntimeParameterState +from braincell._parameter_schema import ParameterSpec, RuntimeParameterState, constructor_parameters +from braincell._parameter_schema import SignatureParameterSpec as _SignatureParameterSpec from braincell.mech import Density, get_registry __all__ = [ @@ -39,15 +43,72 @@ def density_parameter_schema(mechanism: Density) -> Mapping[str, ParameterSpec]: - """Return the explicit runtime parameter schema for one density declaration.""" + """Return Channel/Ion signature metadata or an existing density schema.""" runtime_cls = get_registry().get(mechanism.category, mechanism.class_name) + if mechanism.category in {"channel", "ion"}: + result = {} + for name, parameter in _density_signature(runtime_cls).items(): + default = parameter.default + if mechanism.category == "ion": + default = _ion_default(runtime_cls, name, default) + if default is inspect.Parameter.empty or default is None or callable(default): + default = mechanism.params.get(name, default) + result[name] = _SignatureParameterSpec(default) + return result schema = getattr(runtime_cls, "parameters", {}) return schema if isinstance(schema, Mapping) else {} +def _density_signature(runtime_cls) -> dict[str, inspect.Parameter]: + return constructor_parameters(runtime_cls) + + +def _ion_default(runtime_cls, name, default): + if name in {"size", "name", "solver", "species_initializers"}: + return default + if default is not None and not isinstance(default, braintools.init.Initialization): + return default + node = _ion_default_node(runtime_cls) + attr = "_Ci_initializer" if name == "Ci_initializer" and hasattr(node, "_Ci_initializer") else name + value = getattr(node, attr, default) + if callable(value): + value = value((1,)) + if tuple(getattr(value, "shape", ())) == (1,): + value = value[0] + return value + + +@functools.lru_cache(maxsize=None) +def _ion_default_node(runtime_cls): + return runtime_cls(size=(1,)) + + +def _is_buffer_value(value: object) -> bool: + """Distinguish numeric storage from ordinary constructor configuration.""" + if callable(value) and value is not inspect.Parameter.empty: + return True + try: + return np.asarray(u.get_mantissa(value)).dtype.kind in "biufc" + except (TypeError, ValueError): + return False + + def density_parameter_names(mechanism: Density) -> tuple[str, ...]: - """Return schema fields for migrated mechanisms and explicit fields otherwise.""" + """Return fields needing numeric runtime storage, not a trainability whitelist.""" schema = density_parameter_schema(mechanism) + if mechanism.category in {"channel", "ion"}: + names = dict.fromkeys((*schema, *mechanism.params)) + return tuple( + name + for name in names + if _is_buffer_value( + mechanism.params[name] + if name in mechanism.params and mechanism.params[name] is not None + else schema[name].default + if name in schema + else None + ) + ) return tuple(schema) if schema else tuple(mechanism.params) @@ -59,9 +120,14 @@ def density_parameter_spec(mechanism: Density, name: str) -> ParameterSpec | Non def density_parameter_value(mechanism: Density, name: str) -> object: """Resolve an explicit declaration value or its schema default.""" if name in mechanism.params: - return mechanism.params[name] + value = mechanism.params[name] + if mechanism.category == "ion" and isinstance(value, braintools.init.Initialization): + value = value((1,)) + return value[0] if tuple(getattr(value, "shape", ())) == (1,) else value + if value is not None or mechanism.category != "ion": + return value spec = density_parameter_spec(mechanism, name) - if spec is None: + if spec is None or spec.default is inspect.Parameter.empty: raise KeyError(f"Mechanism has no parameter {name!r}.") return spec.default @@ -151,12 +217,15 @@ def set_parameter_row( if not isinstance(value, u.Quantity): raise TypeError(f"Density parameter requires a Quantity compatible with {unit}.") replacement = value.to_decimal(unit) - mantissa = jnp.asarray(current.to_decimal(unit)).at[population_index, point_id].set(replacement) + dtype = jnp.result_type(current.mantissa, replacement) + mantissa = jnp.asarray(current.to_decimal(unit), dtype=dtype).at[population_index, point_id].set(replacement) state.value = u.Quantity(mantissa, unit) else: if isinstance(value, u.Quantity): raise TypeError("Dimensionless density parameter cannot be assigned a Quantity.") - state.value = jnp.asarray(current).at[population_index, point_id].set(value) + state.value = ( + jnp.asarray(current, dtype=jnp.result_type(current, value)).at[population_index, point_id].set(value) + ) state.axis = "row" diff --git a/braincell/_compute/parameters_test.py b/braincell/_compute/parameters_test.py index 52565012..c5011980 100644 --- a/braincell/_compute/parameters_test.py +++ b/braincell/_compute/parameters_test.py @@ -20,8 +20,56 @@ import brainunit as u import numpy as np -from braincell._compute.parameters import make_runtime_parameter_state, set_parameter_row +from braincell._compute.parameters import ( + density_parameter_schema, + density_parameter_value, + make_runtime_parameter_state, + set_parameter_row, +) from braincell._parameter_schema import ParameterSpec +from braincell.mech import Channel, Ion, get_registry + + +class SignatureParameterTest(unittest.TestCase): + def test_registered_ion_numeric_defaults_are_valid(self): + from braincell._compute.parameters import density_parameter_names + + for class_name in get_registry().names("ion"): + if class_name.startswith("_"): + continue + mechanism = Ion(class_name) + schema = density_parameter_schema(mechanism) + for field in density_parameter_names(mechanism): + with self.subTest(ion=class_name, field=field): + value = density_parameter_value(mechanism, field) + make_runtime_parameter_state(value, full_shape=(1, 2), spec=schema[field], name=field) + + def test_previously_unclassified_channel_has_signature_defaults(self): + mechanism = Channel("Na_TM1991") + schema = density_parameter_schema(mechanism) + self.assertIn("V_sh", schema) + self.assertIn("name", schema) + self.assertNotIn("arbitrary_keyword", schema) + self.assertEqual(density_parameter_value(mechanism, "V_sh"), schema["V_sh"].default) + + def test_forwarded_signature_exposes_parent_defaults(self): + schema = density_parameter_schema(Channel("Ca_ZH2019_IO_Frozen")) + self.assertIn("mMidV", schema) + self.assertIn("freeze_m_inf", schema) + + def test_registered_channel_numeric_defaults_are_valid(self): + from braincell._compute.parameters import density_parameter_names + + for class_name in get_registry().names("channel"): + if class_name.startswith("_"): + continue + mechanism = Channel(class_name) + schema = density_parameter_schema(mechanism) + for field in density_parameter_names(mechanism): + with self.subTest(channel=class_name, field=field): + value = density_parameter_value(mechanism, field) + if not callable(value): + make_runtime_parameter_state(value, full_shape=(1, 2), spec=schema[field], name=field) class RuntimeParameterStateTest(unittest.TestCase): diff --git a/braincell/_compute/state.py b/braincell/_compute/state.py index 5b0e2c07..60c16292 100644 --- a/braincell/_compute/state.py +++ b/braincell/_compute/state.py @@ -353,8 +353,10 @@ def register( f"Unsupported event input {type(event_input).__name__!r} for {mechanism.synapse_type!r}." ) logical_ids = np.asarray(synapse_ids, dtype=np.int64) - for var_name in tuple(runtime_cls.parameters): - state_buffers[(layout_spec.id, var_name)] = synapse_store.parameter_column(logical_ids, var_name) + for var_name in runtime_cls.parameter_info(): + state_buffers[(layout_spec.id, var_name)] = RuntimeParameterState( + synapse_store.parameter_column(logical_ids, var_name), axis="row", full_shape=(len(point_ids),) + ) state_shapes[(layout_spec.id, var_name)] = (len(point_ids),) synapse_store.bind_runtime(mechanism.synapse_type, layout_spec.id, logical_ids) continue @@ -423,6 +425,14 @@ def register( shape=shape, ) + _allocate_extra_density_parameters( + cell=cell, + layouts=layouts, + layout_mechanisms=layout_mechanisms, + state_buffers=state_buffers, + state_shapes=state_shapes, + pop_size=pop_size, + ) _apply_density_parameter_overrides( cell=cell, layouts=tuple(layouts), @@ -734,6 +744,77 @@ def evaluate_point_clamps(self, *, t, point_ids=None) -> object: return u.Quantity(point_current_decimal, u.nA) +def _allocate_extra_density_parameters( + *, + cell, + layouts, + layout_mechanisms, + state_buffers, + state_shapes, + pop_size, +) -> None: + """Allocate explicitly supplied fields with no numeric signature default.""" + supplied = {} + for (category, owner, population, cv, field), value in cell._density_parameter_overrides.items(): + if category in {"channel", "ion"}: + supplied.setdefault((category, owner, field), {})[(population, cv)] = value + for binding in cell.trainables.bindings(): + values = binding._evaluate() + rows = supplied.setdefault((binding._rows[0].category, binding.target_owner, binding.target_field), {}) + for index, row in enumerate(binding._rows): + rows[(row.population_index, row.cv_id)] = values[index] + + for layout in layouts: + mechanism = layout_mechanisms[layout.id] + if not isinstance(mechanism, Density) or mechanism.category not in {"channel", "ion"}: + continue + if mechanism.category == "ion": + for (layout_id, field), state in state_buffers.items(): + if ( + layout_id == layout.id + and field.endswith("_initializer") + and isinstance(state, RuntimeParameterState) + ): + mask = np.zeros(state.full_shape, dtype=bool) + if mechanism.params.get(field) is not None: + mask[..., list(layout.source_cv_ids)] = True + for population, cv in supplied.get(("ion", mechanism.instance_name, field), {}): + if cv in layout.source_cv_ids: + mask[population, cv] = True + state.initial_override_mask = mask + for (category, owner, field), rows in supplied.items(): + key = (layout.id, field) + if category != mechanism.category or owner != mechanism.instance_name or key in state_buffers: + continue + expected = {(population, cv) for population in range(int(np.prod(pop_size))) for cv in layout.source_cv_ids} + if not expected.intersection(rows): + continue + if not expected.issubset(rows): + raise ValueError( + f"Channel {owner!r}.{field} has no numeric default; supply a value for every active row." + ) + full_shape = pop_size + (len(cell.cvs),) + first = rows[min(expected)] + state = make_runtime_parameter_state( + first, + full_shape=full_shape, + spec=density_parameter_spec(mechanism, field), + name=field, + point_mask=layout.cv_mask, + ) + for population, cv in sorted(expected): + set_parameter_row( + state, + population_index=population, + point_id=cv, + population_size=int(np.prod(pop_size)), + point_size=len(cell.cvs), + value=rows[(population, cv)], + ) + state_buffers[key] = state + state_shapes[key] = full_shape + + def _apply_density_parameter_overrides( *, cell: "Cell", diff --git a/braincell/_compute/state_test.py b/braincell/_compute/state_test.py index 90b4b9be..9f77bae3 100644 --- a/braincell/_compute/state_test.py +++ b/braincell/_compute/state_test.py @@ -188,7 +188,7 @@ def test_sample_probe_reads_mechanism_and_total_ion_current(self) -> None: "K_Kv_test", g_max=0.1 * (u.mS / u.cm**2), v12=25.0 * u.mV, - q=9.0, + q=9.0 * u.mV, ), ) cell.place( diff --git a/braincell/_multi_compartment/cell.py b/braincell/_multi_compartment/cell.py index 04b28bbd..4bdf3172 100644 --- a/braincell/_multi_compartment/cell.py +++ b/braincell/_multi_compartment/cell.py @@ -86,7 +86,9 @@ ) from braincell.filter import LocsetBatch, LocsetExpr, LocsetMask, RegionExpr, RegionMask, at from braincell.network.event import EventOutputCollection, _CellSpikeSource -from braincell.network.recording import RecordingSpec, compile_recording +from braincell.network.recording import RecordingSpec, compile_recording, recording_is_active +from braincell.reduction import ReductionOutput, ReductionViewCollection +from braincell.reduction.runtime import ReductionInputRuntime, build_reduction_input_runtime from braincell.morph.morphology import Morphology, clone_morpho from braincell.quad import get_integrator, ind_exp_euler_step from braincell.quad._exp_euler import _ind_exp_euler_step_selected @@ -603,6 +605,20 @@ def event_outputs(self) -> EventOutputCollection: """Return named live event outputs for selected population members.""" return self._cell.event_outputs.for_population(self._population_indices) + @property + def reductions(self) -> ReductionViewCollection: + """Return registered reduction models over selected population members.""" + if self._scope.spatially_restricted: + raise RuntimeError("Reduction parameters can only be selected by population, not by morphology location.") + return ReductionViewCollection(self._cell, self._population_indices) + + @property + def outputs(self) -> Mapping[str, object]: + """Return initialized model outputs gathered over selected members.""" + if self._scope.spatially_restricted: + raise RuntimeError("Model outputs can only be selected by population, not by morphology location.") + return self._cell._selected_outputs(self._population_indices) + @property def V_init(self): """Return effective initial voltages for selected cells.""" @@ -639,6 +655,14 @@ def V(self): def spike(self): """Return initialized spike values gathered over selected cells.""" self._cell._raise_if_not_initialized("CellView.spike") + if self._cell._uses_reduction: + return _select_model_output( + self._cell.spike.value, + population_indices=self._population_indices, + population_size=self._cell._population_size, + batch_size=self._cell._runtime_batch_size, + detailed=False, + ) return _select_population_value( self._cell.spike.value, population_indices=self._population_indices, @@ -820,6 +844,8 @@ def __init__( self._V_th = V_th self._V_th_declaration = V_th + self._V_th_parameter = None + self._next_detector_id = 0 self._V_init = V_init self._V_init_materialized = None self._population_parameter_overrides: dict[str, dict[int, object]] = { @@ -847,6 +873,12 @@ def __init__( self._run_loop_cache: dict[tuple[object, ...], object] = {} self._runtime: CellRuntimeState | None = None + self._reduction_models: dict[str, object] = {} + self._selected_model_name = "detailed" + self._reduction_input_runtime: ReductionInputRuntime | None = None + self._reduction_output_states: dict[str, brainstate.State] = {} + self._pending_reduction_inputs = None + self._runtime_batch_size: int | None = None self._runtime_cvs_cache: tuple[RuntimeCVView, ...] | None = None self._runtime_nodes_cache: tuple[RuntimeNodeView, ...] | None = None self._synapse_store_cache: _SynapseStore | None = None @@ -935,7 +967,7 @@ def place_rules(self) -> tuple[PlaceRule, ...]: @property def V_th(self): - return self._V_th + return self._V_th if self._V_th_parameter is None else self._V_th_parameter.value @V_th.setter def V_th(self, value) -> None: @@ -944,6 +976,11 @@ def V_th(self, value) -> None: # ``_initialized`` is still False at that point. After # ``init_state`` completes, the guard rejects further assignment. self._raise_if_initialized("assign V_th") + if self._V_th_parameter is not None: + from braincell.trainable._targets import require_unbound + + require_unbound(self, "threshold", "V_th", range(self._population_size * self.n_compartment), "threshold") + self._V_th_parameter.value = bridge.fill_like(self.varshape, value) self._V_th = value if hasattr(self, "_population_parameter_overrides"): self._population_parameter_overrides["V_th"].clear() @@ -1078,6 +1115,11 @@ def _set_selected_population_parameters(self, population_indices, parameters) -> if unknown: raise KeyError(f"CellView.set() does not support parameters {sorted(unknown)!r}.") indices = tuple(int(index) for index in population_indices) + if "V_th" in parameters and self._V_th_parameter is not None: + from braincell.trainable._targets import require_unbound + + logical = {i * self.n_compartment + cv for i in indices for cv in range(self.n_compartment)} + require_unbound(self, "threshold", "V_th", logical, "threshold") for name, value in parameters.items(): overrides = self._population_parameter_overrides[name] if value is None: @@ -1097,10 +1139,12 @@ def _set_selected_population_parameters(self, population_indices, parameters) -> overrides[index] = item if name == "V_init": self._V_init_materialized = None + elif self._V_th_parameter is not None: + self._V_th_parameter.value = self._materialize_population_parameter("V_th") def _materialize_population_parameter(self, name: str): if name == "V_th": - value = bridge.fill_like(self.varshape, self._V_th) + value = bridge.fill_like(self.varshape, self.V_th) elif name == "V_init": initializer = self._V_init if initializer is None: @@ -1118,7 +1162,7 @@ def _materialize_population_parameter(self, name: str): def _selected_population_parameter(self, name: str, population_indices): if name == "V_th" and self._initialized: - values = self._V_th + values = self.V_th elif name == "V_init" and self._V_init_materialized is not None: values = self._V_init_materialized else: @@ -1357,6 +1401,43 @@ def event_outputs(self) -> EventOutputCollection: """Return named live event-output ports for this cell population.""" return EventOutputCollection(self) + @property + def reductions(self) -> ReductionViewCollection: + """Return all Cell-local registered reduction models.""" + return ReductionViewCollection(self, tuple(range(self._population_size))) + + @property + def outputs(self) -> Mapping[str, object]: + """Return the selected model's initialized raw outputs.""" + return self._selected_outputs(tuple(range(self._population_size))) + + def add_reduction(self, name: str, model): + """Register one interchangeable reduced model under a Cell-local name.""" + self._raise_if_initialized("add a reduction model") + if not isinstance(name, str) or not name: + raise ValueError("Reduction model name must be a non-empty string.") + if name == "detailed": + raise ValueError("Reduction model name 'detailed' is reserved for the full Cell model.") + if name in self._reduction_models: + raise ValueError(f"Cell already has a reduction model named {name!r}.") + required = ("init_state", "update", "reset_state", "reset") + missing = tuple(method for method in required if not callable(getattr(model, method, None))) + if missing: + raise TypeError(f"Reduction model {type(model).__name__!r} is missing callable methods {missing!r}.") + self._reduction_models[name] = model + self._run_loop_cache.clear() + return self.reductions[name] + + def use_model(self, name: str = "detailed") -> "Cell": + """Select the detailed model or one registered reduction for the next initialization.""" + self._raise_if_initialized("select a Cell execution model") + if name != "detailed" and name not in self._reduction_models: + raise KeyError(f"Unknown Cell model {name!r}; available reductions: {tuple(self._reduction_models)!r}.") + self._selected_model_name = name + self._run_loop_cache.clear() + self._compiled_recording_cache.clear() + return self + def _get_spike_event_source(self) -> _CellSpikeSource: if self._spike_event_source_cache is None: self._spike_event_source_cache = _CellSpikeSource(self) @@ -1419,10 +1500,79 @@ def _compiled_recordings(self, dt) -> tuple: key = (dt_ms, tuple(self._recording_specs)) cached = self._compiled_recording_cache.get(key) if cached is None: - cached = tuple(compile_recording(self, spec, dt=dt) for spec in self._recording_specs.values()) + cached = tuple( + compile_recording(self, spec, dt=dt) + for spec in self._recording_specs.values() + if recording_is_active(self, spec) + ) self._compiled_recording_cache[key] = cached return cached + @property + def _uses_reduction(self) -> bool: + return self._selected_model_name != "detailed" + + def _selected_outputs(self, population_indices: tuple[int, ...]) -> Mapping[str, object]: + if not self._initialized: + return MappingProxyType({}) + if not self._uses_reduction: + values = {"voltage": self.V.value} + else: + values = {name: state.value for name, state in self._reduction_output_states.items()} + selected = { + name: _select_model_output( + value, + population_indices=population_indices, + population_size=self._population_size, + batch_size=self._runtime_batch_size, + detailed=not self._uses_reduction, + ) + for name, value in values.items() + } + return MappingProxyType(selected) + + def _validate_reduction_output(self, output) -> ReductionOutput: + if not isinstance(output, ReductionOutput): + raise TypeError( + f"Reduction model {self._selected_model_name!r} must return ReductionOutput, " + f"got {type(output).__name__!s}." + ) + prefix = ((self._runtime_batch_size,) if self._runtime_batch_size is not None else ()) + self.pop_size + event_shape = tuple(getattr(output.event, "shape", ())) + if event_shape != prefix: + raise ValueError(f"Reduction event shape must be {prefix!r} for this Cell, got {event_shape!r}.") + for name, value in output.values.items(): + shape = tuple(getattr(value, "shape", ())) + if shape[: len(prefix)] != prefix: + raise ValueError( + f"Reduction output {name!r} must start with Cell runtime shape {prefix!r}, got {shape!r}." + ) + return output + + def _publish_reduction_output(self, output, *, initialize: bool = False) -> None: + output = self._validate_reduction_output(output) + if initialize: + self._reduction_output_states = { + name: brainstate.ShortTermState(value) for name, value in output.values.items() + } + self.spike = brainstate.ShortTermState(output.event) + return + if tuple(output.values) != tuple(self._reduction_output_states): + raise ValueError( + "Reduction output names must remain unchanged after init_state(); " + f"expected {tuple(self._reduction_output_states)!r}, got {tuple(output.values)!r}." + ) + for name, value in output.values.items(): + current = self._reduction_output_states[name].value + expected = tuple(current.shape) + actual = tuple(getattr(value, "shape", ())) + if actual != expected: + raise ValueError(f"Reduction output {name!r} changed shape from {expected!r} to {actual!r}.") + _require_same_value_type(current, value, name=f"Reduction output {name!r}") + self._reduction_output_states[name].value = value + _require_same_value_type(self.spike.value, output.event, name="Reduction event") + self.spike.value = output.event + def get_point_placement(self, placement_id: int): """Return one static point placement by its stable id.""" if isinstance(placement_id, bool) or not isinstance(placement_id, (int, np.integer)): @@ -1451,6 +1601,15 @@ def init_state(self, batch_size=None) -> None: self._morpho = morpho self._invalidate_discretization_cache() _ = self._discretization + if batch_size is not None: + if isinstance(batch_size, bool) or not isinstance(batch_size, (int, np.integer)): + raise TypeError("Cell init_state() batch_size must be an integer or None.") + if int(batch_size) < 1: + raise ValueError("Cell init_state() batch_size must be >= 1.") + self._runtime_batch_size = None if batch_size is None else int(batch_size) + if self._uses_reduction: + self._init_reduction_state(batch_size=batch_size) + return self._runtime = CellRuntimeState.from_cell(self) # Save scalar V_th declaration before the vector overwrite below. @@ -1514,6 +1673,18 @@ def init_state(self, batch_size=None) -> None: self._runtime_cvs_cache = self._build_runtime_cv_views() self._runtime_nodes_cache = self._build_runtime_node_views() + def _init_reduction_state(self, *, batch_size=None) -> None: + """Allocate only packed synapse inputs plus the selected reduced model.""" + self._in_size = self.pop_size + self._out_size = self.pop_size + self._reduction_input_runtime = build_reduction_input_runtime(self) + model = self._reduction_models[self._selected_model_name] + output = model.init_state(self._reduction_input_runtime.context, batch_size=batch_size) + self._publish_reduction_output(output, initialize=True) + self._pending_reduction_inputs = self._reduction_input_runtime.take_inputs() + self._current_time_state.value = 0.0 * u.ms + self._initialized = True + def reset(self) -> None: """Drop runtime and per-step state; return to DECLARING. @@ -1535,6 +1706,9 @@ def reset(self) -> None: self.connections.clear_runtime() + if self._uses_reduction: + self._reduction_models[self._selected_model_name].reset() + for name in ("_in_size", "_out_size", "ion_channels", "C"): if hasattr(self, name): delattr(self, name) @@ -1554,6 +1728,10 @@ def reset(self) -> None: self._current_time_state.value = 0.0 * u.ms self._runtime = None + self._reduction_input_runtime = None + self._reduction_output_states = {} + self._pending_reduction_inputs = None + self._runtime_batch_size = None self.trainables.runtime_reset() self._runtime_cvs_cache = None self._runtime_nodes_cache = None @@ -1573,11 +1751,15 @@ def reset(self) -> None: @property def runtime(self) -> CellRuntimeState: self._raise_if_not_initialized("runtime") + if self._uses_reduction: + raise RuntimeError("Detailed Cell runtime is unavailable while a reduction model is selected.") return self._runtime @property def n_point(self) -> int: self._raise_if_not_initialized("n_point") + if self._uses_reduction: + raise RuntimeError("Detailed point runtime is unavailable while a reduction model is selected.") return self._runtime.n_point @property @@ -2097,12 +2279,17 @@ def _begin_step(self): delivered; it does not integrate continuous synapse dynamics. """ self._raise_if_not_initialized("_begin_step()") + if self._uses_reduction: + self._pending_reduction_inputs = self._reduction_input_runtime.take_inputs() + return point_V = self._point_voltage_for_mechanisms(self.V.value) self._apply_runtime_synapse_events(point_V) def _prepare_step_clamps(self, *, t=None, dt=None) -> None: """Sample all current clamps at the main-step midpoint and cache them.""" self._raise_if_not_initialized("_prepare_step_clamps()") + if self._uses_reduction: + return if len(self._get_clamp_store().id) == 0: return step_t = self._resolve_t() if t is None else t @@ -2160,10 +2347,15 @@ def _update_dynamics(self): """ self._raise_if_not_initialized("_update_dynamics()") - last_V = self.V.value - self._event_previous_V.value = last_V if brainstate.environ.get("dt", None) is None: raise ValueError("Cell.update(...) requires brainstate.environ['dt'] to be set.") + if self._uses_reduction: + output = self._reduction_models[self._selected_model_name].update(self._pending_reduction_inputs) + self._publish_reduction_output(output) + return self.spike.value + + last_V = self.V.value + self._event_previous_V.value = last_V with jax.named_scope("braincell:cell_update:solver"): self.solver(self) @@ -2190,6 +2382,13 @@ def _prepare_next_synapse_inputs(self, *, t=None): delivery layer before this preparation phase. """ self._raise_if_not_initialized("_prepare_next_synapse_inputs()") + if self._uses_reduction: + if t is None: + self._prepare_runtime_synapse_inputs(None) + else: + with brainstate.environ.context(t=t): + self._prepare_runtime_synapse_inputs(None) + return point_V = self._point_voltage_for_mechanisms(self.V.value) if t is None: self._prepare_runtime_synapse_inputs(point_V) @@ -2248,10 +2447,10 @@ def _prepare_runtime_synapse_inputs(self, point_V): _ = point_V self._raise_if_not_initialized("_prepare_runtime_synapse_inputs()") t = self._resolve_t() - for layout, _ in self._runtime.iter_synapse_layouts(): - if layout.id not in self._runtime.event_buffers: + for layout in self._event_layouts(): + if layout.id not in self._event_runtime().event_buffers: continue - total_drive = self._runtime.get_event_buffer(layout.id) + total_drive = self._event_runtime().get_event_buffer(layout.id) contact_drive = self._evaluate_contact_inputs( layout, t=t, @@ -2263,7 +2462,7 @@ def _prepare_runtime_synapse_inputs(self, point_V): layout, total_drive, ) - self._runtime.event_buffers[layout.id].value = total_drive + self._set_event_buffer(layout.id, total_drive) def _evaluate_contact_inputs(self, layout, *, t, template, scheduled_only=True): """Return weighted Connection arrivals addressed to one synapse layout.""" @@ -2303,12 +2502,13 @@ def _apply_direct_live_connection_events(self) -> None: if dt is None: raise ValueError("Live Connection delivery requires brainstate.environ['dt'].") t = self._resolve_t() - layouts = tuple(self._runtime.iter_synapse_layouts()) + layouts = self._event_layouts() drives = {} - for layout, _ in layouts: - if layout.id not in self._runtime.event_buffers: + event_runtime = self._event_runtime() + for layout in layouts: + if layout.id not in event_runtime.event_buffers: continue - drives[layout.id] = _zeros_like_event_template(self._runtime.get_event_buffer(layout.id)) + drives[layout.id] = _zeros_like_event_template(event_runtime.get_event_buffer(layout.id)) synapse_store = self._get_synapse_store() for connection in live_connections: @@ -2317,21 +2517,25 @@ def _apply_direct_live_connection_events(self) -> None: layout_id = synapse_store.layout_id(synapse_type) if layout_id not in drives: continue - template = self._runtime.get_event_buffer(layout_id) + template = event_runtime.get_event_buffer(layout_id) contribution = counts * u.math.asarray(_connection_event_weight(template, connection.weight)) local_indices = synapse_store.runtime_rows(connection.synapse_id).astype(np.int32) drives[layout_id] = drives[layout_id].at[local_indices].add(contribution) - point_v = self._cv_to_point(self.V.value) - for layout, synapse in layouts: + point_v = None if self._uses_reduction else self._cv_to_point(self.V.value) + for layout in layouts: if layout.id not in drives: continue - template = self._runtime.get_event_buffer(layout.id) + template = event_runtime.get_event_buffer(layout.id) drive = _rewrap_event_template(template, drives[layout.id]) self._apply_synapse_layout_event_drive(layout.id, drive, point_v=point_v) def _apply_synapse_layout_event_drive(self, layout_id: int, drive, *, point_v=None) -> None: """Apply one already-aggregated boundary payload to a runtime layout.""" + if self._uses_reduction: + current = self._reduction_input_runtime.get_event_buffer(layout_id) + self._set_event_buffer(layout_id, current + _coerce_drive_like(drive, current)) + return layout = self._runtime.layouts[int(layout_id)] synapse = self._runtime.get_runtime_node(layout.id) if point_v is None: @@ -2339,6 +2543,33 @@ def _apply_synapse_layout_event_drive(self, layout_id: int, drive, *, point_v=No args = (layout.gather_points(point_v),) synapse.apply_events(drive, *args) + def _event_runtime(self): + """Return the detailed or reduced owner of packed event buffers.""" + return self._reduction_input_runtime if self._uses_reduction else self._runtime + + def _event_layouts(self): + """Return executable synapse input layouts without exposing runtime nodes.""" + if self._uses_reduction: + return self._reduction_input_runtime.layouts + return tuple(layout for layout, _ in self._runtime.iter_synapse_layouts()) + + def _event_layout(self, layout_id: int): + """Return one input layout by its stable runtime id.""" + for layout in self._event_layouts(): + if int(layout.id) == int(layout_id): + return layout + raise KeyError(f"Unknown synapse event layout id {layout_id!r}.") + + def _set_event_buffer(self, layout_id: int, value) -> None: + self._event_runtime().event_buffers[int(layout_id)].value = value + + def _write_event_arrival(self, layout_id: int, arrival) -> None: + """Merge one Network arrival with any event staged for this boundary.""" + if self._uses_reduction: + current = self._reduction_input_runtime.get_event_buffer(layout_id) + arrival = current + _coerce_drive_like(arrival, current) + self._set_event_buffer(layout_id, arrival) + def _evaluate_bound_synapse_inputs(self, layout, template): drive = u.math.zeros_like(template) if layout.synapse_index is None: @@ -2419,6 +2650,20 @@ def reset_state(self, batch_size=None) -> None: """ self._raise_if_network_owned("reset_state()") self._raise_if_not_initialized("reset_state()") + if self._uses_reduction: + requested_batch = None if batch_size is None else int(batch_size) + if requested_batch != self._runtime_batch_size: + raise ValueError( + "Reduced Cell reset_state() must preserve the init_state() batch size; " + f"expected {self._runtime_batch_size!r}, got {requested_batch!r}." + ) + self.connections.reset_runtime() + self._reduction_input_runtime.clear_event_buffers() + output = self._reduction_models[self._selected_model_name].reset_state(batch_size=batch_size) + self._publish_reduction_output(output) + self._pending_reduction_inputs = self._reduction_input_runtime.take_inputs() + self._current_time_state.value = 0.0 * u.ms + return if self.trainables.bindings(): self.trainables.materialize() self.connections.reset_runtime() @@ -2446,6 +2691,8 @@ def reset_state(self, batch_size=None) -> None: @property def layouts(self): self._raise_if_not_initialized("layouts") + if self._uses_reduction: + raise RuntimeError("Detailed mechanism layouts are unavailable while a reduction model is selected.") return self._runtime.layouts @property @@ -2500,10 +2747,14 @@ def get_ion(self, name): def sample_probe(self, name: str): self._raise_if_not_initialized("sample_probe()") + if self._uses_reduction: + raise KeyError(f"Detailed probe {name!r} is inactive while a reduction model is selected.") return probes.sample_probe(self, name) def sample_probes(self) -> dict[str, object]: self._raise_if_not_initialized("sample_probes()") + if self._uses_reduction: + return {} return probes.sample_probes(self) def mech_table(self) -> MechanismObjectTable: @@ -2520,6 +2771,8 @@ def mech_table(self) -> MechanismObjectTable: If :meth:`init_state` has not been called. """ self._raise_if_not_initialized("mech_table()") + if self._uses_reduction: + raise RuntimeError("Detailed mechanism inspection is unavailable while a reduction model is selected.") return build_mechanism_object_table(self._runtime, self.cvs) # ------------------------------------------------------------------ @@ -2538,7 +2791,7 @@ def run(self, *, dt, duration): raise RuntimeError(f"Cell belongs to Network {owner_name!r}; run it through Network {owner_name!r}.") if not self._initialized: self.init_state() - elif self.trainables.bindings(): + elif not self._uses_reduction and self.trainables.bindings(): self.trainables.materialize() return run_module.run(self, dt=dt, duration=duration) @@ -2669,6 +2922,38 @@ def _select_population_value(value, *, population_indices: tuple[int, ...], popu return value[tuple(index)] +def _select_model_output( + value, + *, + population_indices: tuple[int, ...], + population_size: int, + batch_size: int | None, + detailed: bool, +): + """Gather the explicit population axis from a detailed or reduced output.""" + shape = tuple(getattr(value, "shape", ())) + if not shape: + return value + axis = (1 if batch_size is not None else 0) if not detailed else len(shape) - 2 + if shape[axis] != population_size: + raise RuntimeError(f"Cell output population axis has size {shape[axis]!r}; expected {population_size!r}.") + index = [slice(None)] * len(shape) + index[axis] = np.asarray(population_indices, dtype=np.int32) + return value[tuple(index)] + + +def _require_same_value_type(previous, current, *, name: str) -> None: + """Require one reduced output to preserve dtype and exact unit.""" + previous_unit = u.get_unit(previous) + current_unit = u.get_unit(current) + if previous_unit != current_unit: + raise TypeError(f"{name} changed unit from {previous_unit!r} to {current_unit!r}.") + previous_dtype = jnp.asarray(u.get_magnitude(previous)).dtype + current_dtype = jnp.asarray(u.get_magnitude(current)).dtype + if previous_dtype != current_dtype: + raise TypeError(f"{name} changed dtype from {previous_dtype!r} to {current_dtype!r}.") + + def _select_packed_population_value(value, *, owners: np.ndarray, population_indices: tuple[int, ...]): """Gather packed point-instance rows owned by selected population members.""" shape = tuple(getattr(value, "shape", ())) diff --git a/braincell/_multi_compartment/density_views.py b/braincell/_multi_compartment/density_views.py index 3fc511ac..aacd8f29 100644 --- a/braincell/_multi_compartment/density_views.py +++ b/braincell/_multi_compartment/density_views.py @@ -21,6 +21,7 @@ from dataclasses import dataclass import brainstate +import braintools import brainunit as u import jax.numpy as jnp import numpy as np @@ -147,7 +148,7 @@ def trainable(self, **fields): return self def parameter_info(self): - """Return the declared physical parameter schema for this owner.""" + """Return parameter metadata inferred from Channel/Ion constructors.""" self._require_one_owner("inspect parameters") if not self._rows: return {} @@ -167,13 +168,22 @@ def _row_value(self, row: _DensityRow, field: str): schema = density_parameter_schema(row.mechanism) if field not in row.mechanism.params and field not in schema: raise KeyError(f"{row.category.title()} {row.name!r} has no declared parameter {field!r}.") + if row.category == "ion" and field.endswith("_initializer") and row.mechanism.params.get(field) is None: + return self._initial_default(row, field) value = density_parameter_value(row.mechanism, field) return value(self._cell.cv_contexts[row.cv_id]) if callable(value) else value layout = _runtime_layout(self._cell, row) runtime = self._cell.runtime point_value = None - if runtime.has_layout_value(layout.id, field): + if row.category == "ion" and field.endswith("_initializer"): + from braincell._compute.ions import _ion_runtime_attr_name + + node = runtime.get_runtime_node(layout.id) + value = getattr(node, _ion_runtime_attr_name(type(node), field)) + value = value(node.varshape) if callable(value) else value + point_value = _take_point(value, row.cv_id, self._cell.n_cv) + elif runtime.has_layout_value(layout.id, field): point_value = _take_point(runtime.get_state(layout.id, field), row.cv_id, self._cell.n_cv) else: node = runtime.get_runtime_node(layout.id) @@ -185,6 +195,25 @@ def _row_value(self, row: _DensityRow, field: str): point_value = _take_point(point_value, row.cv_id, self._cell.n_cv) return _take_population(point_value, row.population_index, self._cell._population_size) + def _initial_default(self, row, field): + """Resolve a default initializer against this row's current parameters.""" + from braincell._compute.ions import _ion_runtime_attr_name + + params = dict(row.mechanism.params) + for (category, owner, population, cv, name), value in self._cell._density_parameter_overrides.items(): + if (category, owner, population, cv) == (row.category, row.name, row.population_index, row.cv_id): + params[name] = value + for name, value in params.items(): + if callable(value) and not isinstance(value, braintools.init.Initialization): + value = value(self._cell.cv_contexts[row.cv_id]) + value = _take_point(value, row.cv_id, self._cell.n_cv) + params[name] = _take_population(value, row.population_index, self._cell._population_size) + cls = get_registry().get("ion", row.mechanism.class_name) + node = cls(size=(1,), **params) + value = getattr(node, _ion_runtime_attr_name(cls, field)) + value = braintools.init.param(value, (1,)) + return value[0] if tuple(getattr(value, "shape", ())) == (1,) else value + def _set_row_value(self, row: _DensityRow, field: str, value) -> None: if self._cell.trainables.owns( category=row.category, @@ -221,6 +250,10 @@ def _set_row_value(self, row: _DensityRow, field: str, value) -> None: point_size=self._cell.n_cv, value=value, ) + if row.category == "ion" and field.endswith("_initializer"): + node = runtime.get_runtime_node(layout.id) + mask = node._runtime_ion_initial_masks[field] + mask.value = mask.value.at[row.population_index, row.cv_id].set(True) runtime.set_state(layout.id, field, updated) self._cell._run_loop_cache.clear() @@ -342,6 +375,7 @@ def _set_population_point(buffer, *, population_index, point_id, population_size mantissa = jnp.asarray(buffer) if mantissa.shape[-1] != point_size: raise ValueError("Density parameter buffer does not expose the point axis.") + mantissa = mantissa.astype(jnp.result_type(mantissa, decimal)) if mantissa.ndim >= 2 and mantissa.shape[0] == population_size: mantissa = mantissa.at[population_index, point_id].set(decimal) else: diff --git a/braincell/_multi_compartment/density_views_test.py b/braincell/_multi_compartment/density_views_test.py index c57dd65b..8ac46fbd 100644 --- a/braincell/_multi_compartment/density_views_test.py +++ b/braincell/_multi_compartment/density_views_test.py @@ -49,10 +49,10 @@ def test_schema_default_is_visible_and_settable_before_init(self) -> None: leak.set(E=-65.0 * u.mV) self.assertTrue(u.math.allclose(leak.E, -65.0 * u.mV)) - def test_parameter_info_uses_migrated_schema(self) -> None: + def test_parameter_info_uses_constructor_signature(self) -> None: cell = _cell() cell.paint(BranchSlice([0, 1], 0.0, 1.0), braincell.mech.Channel("IL", name="leak")) - self.assertEqual(tuple(cell.channels["leak"].parameter_info()), ("g_max", "E")) + self.assertEqual(tuple(cell.channels["leak"].parameter_info()), ("size", "g_max", "E", "name")) def test_same_owner_across_disjoint_cvs_has_one_view(self) -> None: cell = _cell() diff --git a/braincell/_multi_compartment/run.py b/braincell/_multi_compartment/run.py index 70928747..9a784909 100644 --- a/braincell/_multi_compartment/run.py +++ b/braincell/_multi_compartment/run.py @@ -128,7 +128,7 @@ def run(rcell: "Cell", *, dt, duration) -> RunResult: rcell.connections.prepare_runtime(dt) compiled_recordings = rcell._compiled_recordings(dt) - ordered_probe_names = tuple(sorted(probes.probe_names(rcell))) + ordered_probe_names = () if rcell._uses_reduction else tuple(sorted(probes.probe_names(rcell))) schedule_issues = _differentiable_schedule_issues(compiled_recordings, dt=dt, n_steps=n_steps) with brainstate.environ.context(dt=dt): @@ -252,7 +252,11 @@ def _step(t): with jax.named_scope("braincell:cell_run:prepare_clamps"): rcell._prepare_step_clamps(t=t, dt=brainstate.environ.get_dt()) with jax.named_scope("braincell:cell_run:sample_recordings"): - recording_snapshot = tuple(item.sample() for item in compiled_recordings) + pre_recording_snapshot = { + index: item.sample() + for index, item in enumerate(compiled_recordings) + if item.phase == "pre" + } with jax.named_scope("braincell:cell_run:begin_step"): rcell._begin_step() with jax.named_scope("braincell:cell_run:update_dynamics"): @@ -262,6 +266,11 @@ def _step(t): rcell._apply_direct_live_connection_events() with jax.named_scope("braincell:cell_run:prepare_next_synapse_inputs"): rcell._prepare_next_synapse_inputs(t=t + brainstate.environ.get_dt()) + with jax.named_scope("braincell:cell_run:sample_outputs"): + recording_snapshot = tuple( + pre_recording_snapshot[index] if item.phase == "pre" else item.sample() + for index, item in enumerate(compiled_recordings) + ) # Placed Probe objects keep their legacy post-step sampling # contract during the transition to layout-free recordings. with jax.named_scope("braincell:cell_run:sample_legacy_probes"): diff --git a/braincell/_multi_compartment/synapses.py b/braincell/_multi_compartment/synapses.py index fed82396..7ff64940 100644 --- a/braincell/_multi_compartment/synapses.py +++ b/braincell/_multi_compartment/synapses.py @@ -126,7 +126,7 @@ def _build_parameter_columns(self) -> None: for raw_type in dict.fromkeys(self.synapse_type.tolist()): synapse_type = str(raw_type) runtime_cls = get_registry().get("synapse", synapse_type) - schema = dict(runtime_cls.parameters) + schema = runtime_cls.parameter_info() rows = np.flatnonzero(self.synapse_type == synapse_type).astype(np.int64) self._type_rows[synapse_type] = rows for local_row, store_row in enumerate(rows.tolist()): @@ -256,7 +256,7 @@ def set_parameters(self, logical_ids: np.ndarray, updates: dict[str, object]) -> local_rows = np.asarray([self._type_local_by_id[int(item)] for item in ids.tolist()], dtype=np.int64) columns = dict(self.parameter_columns[synapse_type]) for parameter, values in updates.items(): - spec = runtime_cls.parameters[parameter] + spec = runtime_cls.parameter_info()[parameter] spec.validate(values, parameter) columns[parameter] = _set_vector_items(columns[parameter], local_rows, values) runtime_cls.validate_parameter_values(columns) @@ -514,6 +514,34 @@ def get(self, field: str): value = value.value return _take_last_axis(value, self._store.runtime_rows(self._logical_ids)) + def trainable(self, **fields): + """Bind parameter sources to selected logical synapses. + + Parameters + ---------- + **fields + Constructor parameter names mapped to trainable sources. + + Returns + ------- + SynapseView + This selection. + """ + from braincell.trainable._targets import register_synapse + + register_synapse(self, fields) + return self + + def parameter_info(self): + """Return signature-derived parameter metadata for this synapse type. + + Returns + ------- + dict + Parameter metadata keyed by constructor field. + """ + return get_registry().get("synapse", self._require_homogeneous_type()).parameter_info() + def set(self, **parameters: object) -> "SynapseView": """Set model parameters before or after runtime materialization. @@ -535,6 +563,10 @@ def set(self, **parameters: object) -> "SynapseView": If a value has an incompatible shape or unit. """ synapse_type = self._require_homogeneous_type() + from braincell.trainable._targets import require_unbound + + for field in parameters: + require_unbound(self._cell, "synapse", synapse_type, self.id, field) valid = self._parameter_names(synapse_type) normalized_updates = {} for parameter, value in parameters.items(): @@ -573,7 +605,7 @@ def set(self, **parameters: object) -> "SynapseView": for parameter, normalized in normalized_updates.items(): proposed[parameter] = _set_last_axis(proposed[parameter], rows, normalized) for parameter in normalized_updates: - type(node).parameters[parameter].validate(proposed[parameter], parameter) + type(node).parameter_info()[parameter].validate(proposed[parameter], parameter) type(node).validate_parameter_values(proposed) for parameter, updated in proposed.items(): @@ -627,7 +659,7 @@ def set_state(self, **states: object) -> "SynapseView": def _parameter_names(self, synapse_type: str) -> set[str]: runtime_cls = get_registry().get("synapse", str(synapse_type)) - return set(runtime_cls.parameters) + return set(runtime_cls.parameter_info()) def _require_homogeneous_type(self) -> str: types = tuple(dict.fromkeys(str(item) for item in self.synapse_type.tolist())) diff --git a/braincell/_parameter_schema.py b/braincell/_parameter_schema.py index 1d59cd66..db91facd 100644 --- a/braincell/_parameter_schema.py +++ b/braincell/_parameter_schema.py @@ -17,6 +17,8 @@ from __future__ import annotations +import inspect + import brainstate import brainunit as u @@ -25,6 +27,32 @@ __all__ = ["ParameterSpec", "RuntimeParameterState", "StateSpec", "positive"] +def constructor_parameters(runtime_cls): + """Inspect explicit constructor fields, including forwarded inheritance.""" + parameters = tuple(inspect.signature(runtime_cls.__init__).parameters.values())[1:] + result = {} + if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in parameters): + parent = next((cls for cls in runtime_cls.__mro__[1:] if "__init__" in vars(cls)), None) + if parent is not None and parent is not object: + result.update(constructor_parameters(parent)) + result.update( + (p.name, p) + for p in parameters + if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) + ) + return result + + +class SignatureParameterSpec(ParameterSpec): + """Infer a missing unit prototype from the supplied constructor value.""" + + def validate(self, value: object, name: str) -> None: + default = self.default + if default is inspect.Parameter.empty or default is None or callable(default): + default = value + ParameterSpec(default).validate(value, name) + + class RuntimeParameterState(brainstate.LongTermState): """Store a materialized physical parameter outside the optimizer tree.""" @@ -83,3 +111,18 @@ def mantissa(self): def __getitem__(self, index): return self.dense_value()[index] + + def to_decimal(self, unit): + """Return the current physical mantissa in the requested unit. + + Parameters + ---------- + unit : brainunit.Unit + Unit compatible with the stored physical quantity. + + Returns + ------- + array + Converted mantissa without copying the optimizer parameter. + """ + return self.dense_value().to_decimal(unit) diff --git a/braincell/channel/_base.py b/braincell/channel/_base.py index 19456e01..295f27ba 100644 --- a/braincell/channel/_base.py +++ b/braincell/channel/_base.py @@ -196,6 +196,10 @@ def cached_q10_factor(owner, slot: str, q10, temp, temp_ref): ``_on_param_updated``, so a runtime temperature change replaces ``owner.temp`` with a new object and invalidates the memo automatically. + Traced inputs bypass the memo, and traced outputs are never retained on + the owner. This also applies when concrete inputs produce a traced result + inside JIT, so a later trace cannot accidentally reuse an escaped tracer. + Parameters ---------- owner : object @@ -212,11 +216,15 @@ def cached_q10_factor(owner, slot: str, q10, temp, temp_ref): The dimensionless Q10 factor. """ key = (q10, temp, temp_ref) + if any(isinstance(leaf, jax.core.Tracer) for leaf in jax.tree.leaves(key)): + return q10_factor(q10, temp, temp_ref) cached = getattr(owner, slot, None) if cached is not None and all(a is b for a, b in zip(cached[0], key)): return cached[1] value = q10_factor(q10, temp, temp_ref) - setattr(owner, slot, (key, value)) + # Even concrete inputs can produce a tracer when called inside a JIT trace. + if not any(isinstance(leaf, jax.core.Tracer) for leaf in jax.tree.leaves(value)): + setattr(owner, slot, (key, value)) return value diff --git a/braincell/channel/_base_test.py b/braincell/channel/_base_test.py index 0dce2eae..2707b48b 100644 --- a/braincell/channel/_base_test.py +++ b/braincell/channel/_base_test.py @@ -404,6 +404,28 @@ def test_cached_q10_factor_recomputes_when_a_parameter_is_rebound(self) -> None: self.assertIsNot(second, first) self.assertTrue(u.math.allclose(second, q10_factor(3.0, ch.temp, ref), atol=1e-12)) + def test_cached_q10_does_not_leak_traced_inputs_or_outputs(self) -> None: + ch = _ExampleHHInfTau(size=1) + ref = u.celsius2kelvin(22.0) + + def evaluate(offset): + return cached_q10_factor(ch, "_traced_memo", 3.0, ref + offset * u.kelvin, ref) + + with jax.checking_leaks(): + result = jax.jit(jax.value_and_grad(evaluate))(10.0) + self.assertAlmostEqual(float(result[0]), 3.0, places=5) + self.assertGreater(float(result[1]), 0.0) + self.assertFalse(hasattr(ch, "_traced_memo")) + + concrete_q10 = jnp.asarray(3.0) + + def concrete_inputs(): + return cached_q10_factor(ch, "_concrete_memo", concrete_q10, ref, ref) + + with jax.checking_leaks(): + self.assertAlmostEqual(float(jax.jit(concrete_inputs)()), 1.0) + self.assertFalse(hasattr(ch, "_concrete_memo")) + def test_freeze_gradient_keeps_the_value_and_drops_the_derivative(self) -> None: frozen = freeze_gradient(jnp.array([-60.0, -40.0]) * u.mV) self.assertEqual(u.get_unit(frozen), u.mV) diff --git a/braincell/channel/leaky.py b/braincell/channel/leaky.py index f2ed2898..810e7297 100644 --- a/braincell/channel/leaky.py +++ b/braincell/channel/leaky.py @@ -28,7 +28,7 @@ from braincell._base_channel import Channel from braincell._base_neuron import HHTypedNeuron from braincell._typing import Initializer, Size -from braincell.mech import ParameterSpec, register_channel +from braincell.mech import register_channel __all__ = [ 'LeakageChannel', @@ -125,10 +125,6 @@ class IL(LeakageChannel): __module__ = 'braincell.channel' root_type = HHTypedNeuron - parameters = { - "g_max": ParameterSpec(default=_IL_G_MAX_DEFAULT), - "E": ParameterSpec(default=_IL_E_DEFAULT), - } states = {} def __init__( diff --git a/braincell/channel/potassium.py b/braincell/channel/potassium.py index c4418bfa..741446ab 100644 --- a/braincell/channel/potassium.py +++ b/braincell/channel/potassium.py @@ -26,7 +26,7 @@ from braincell._typing import ArrayLike, Initializer, Size from braincell.channel._base import Gate, HH, OhmicHH, cached_q10_factor, is_disabled from braincell.ion import Potassium -from braincell.mech import ParameterSpec, StateSpec, register_channel +from braincell.mech import StateSpec, register_channel #: Hoisted so the identity-keyed memo in ``cached_q10_factor`` can hit: #: a freshly built ``celsius2kelvin`` result would miss on every call. @@ -463,13 +463,6 @@ class K_HH1952(OhmicHH): __module__ = "braincell.channel" root_type = Potassium gates = (Gate("p", power=4, q10="q10", temp_ref="temp_ref"),) - parameters = { - "g_max": ParameterSpec(_K_HH1952_G_MAX_DEFAULT), - "temp": ParameterSpec(_K_HH1952_TEMP_DEFAULT), - "q10": ParameterSpec(_K_HH1952_Q10_DEFAULT), - "temp_ref": ParameterSpec(_K_HH1952_TEMP_REF_DEFAULT), - "V_sh": ParameterSpec(_K_HH1952_V_SH_DEFAULT), - } states = {"p": StateSpec()} def __init__( diff --git a/braincell/channel/sodium.py b/braincell/channel/sodium.py index e94fa4a5..03178c8d 100644 --- a/braincell/channel/sodium.py +++ b/braincell/channel/sodium.py @@ -26,7 +26,7 @@ from braincell._typing import ArrayLike, Initializer, Size from braincell.channel._base import Gate, OhmicHH, OhmicMarkov, is_disabled, q10_factor from braincell.ion import Sodium -from braincell.mech import ParameterSpec, StateSpec, register_channel +from braincell.mech import StateSpec, register_channel __all__ = [ "Na_Ba2002", @@ -360,13 +360,6 @@ class Na_HH1952(OhmicHH): Gate("p", power=3, q10="q10", temp_ref="temp_ref"), Gate("q", q10="q10", temp_ref="temp_ref"), ) - parameters = { - "g_max": ParameterSpec(_NA_HH1952_G_MAX_DEFAULT), - "temp": ParameterSpec(_NA_HH1952_TEMP_DEFAULT), - "q10": ParameterSpec(_NA_HH1952_Q10_DEFAULT), - "temp_ref": ParameterSpec(_NA_HH1952_TEMP_REF_DEFAULT), - "V_sh": ParameterSpec(_NA_HH1952_V_SH_DEFAULT), - } states = {"p": StateSpec(), "q": StateSpec()} def __init__( @@ -876,7 +869,6 @@ def __init__( super().__init__(size=size, name=name, solver=solver, substeps=substeps) self.temp = braintools.init.param(temp, self.varshape, allow_none=False) - self.phi = q10_factor(3, self.temp, u.celsius2kelvin(22.0)) self.g_max = braintools.init.param(g_max, self.varshape, allow_none=False) self.Con = 0.005 @@ -903,6 +895,11 @@ def __init__( self.alfac = (self.Oon / self.Con) ** (1 / 4) self.btfac = (self.Ooff / self.Coff) ** (1 / 4) + @property + def phi(self): + """Return the rate factor at the current temperature.""" + return q10_factor(3, self.temp, u.celsius2kelvin(22.0)) + f01 = lambda self, V: 4 * self.alpha * u.math.exp((V / u.mV) / self.x1) * self.phi f02 = lambda self, V: 3 * self.alpha * u.math.exp((V / u.mV) / self.x1) * self.phi f03 = lambda self, V: 2 * self.alpha * u.math.exp((V / u.mV) / self.x1) * self.phi @@ -1307,7 +1304,6 @@ def __init__( solver=solver, substeps=substeps, ) - self.phi = q10_factor(2.7, self.temp, u.celsius2kelvin(22.0)) self.gateCurrent = braintools.init.param(gateCurrent, self.varshape, allow_none=False) self.Oon = 2.3 self.epsilon = 1e-12 @@ -1316,6 +1312,11 @@ def __init__( self.e0 = 1.60217646e-19 * u.coulomb self.alfac = (self.Oon / self.Con) ** (1 / 4) + @property + def phi(self): + """Return this variant's rate factor at the current temperature.""" + return q10_factor(2.7, self.temp, u.celsius2kelvin(22.0)) + def current(self, V, Na: IonInfo): conductive = super().current(V, Na) if is_disabled(self.gateCurrent): @@ -1575,7 +1576,6 @@ def __init__( self.temp = braintools.init.param(temp, self.varshape, allow_none=False) self.g_max = braintools.init.param(g_max, self.varshape, allow_none=False) - self.phi = q10_factor(3, self.temp, u.celsius2kelvin(20.0)) self.Aalfa = 353.91 self.Valfa = 13.99 @@ -1599,6 +1599,11 @@ def init_state(self, V, Na: IonInfo, batch_size: int = None): super().init_state(V, Na, batch_size=batch_size) self.reset_steady_state(V, Na, batch_size=batch_size) + @property + def phi(self): + """Return the rate factor at the current temperature.""" + return q10_factor(3, self.temp, u.celsius2kelvin(20.0)) + alfa = lambda self, V: self.phi * self.Aalfa * u.math.exp((V / u.mV) / self.Valfa) beta = lambda self, V: self.phi * self.Abeta * u.math.exp(-(V / u.mV) / self.Vbeta) teta = lambda self, V: self.phi * self.Ateta * u.math.exp(-(V / u.mV) / self.Vteta) @@ -1799,7 +1804,6 @@ def __init__( self.temp = braintools.init.param(temp, self.varshape, allow_none=False) self.g_max = braintools.init.param(g_max, self.varshape, allow_none=False) - self.phi = q10_factor(3, self.temp, u.celsius2kelvin(20.0)) self.Aalfa = 353.91 self.Valfa = 13.99 @@ -1823,6 +1827,11 @@ def __init__( self.c = 20.0 self.d = 0.075 + @property + def phi(self): + """Return the rate factor at the current temperature.""" + return q10_factor(3, self.temp, u.celsius2kelvin(20.0)) + Lon = lambda self, V: self.phi * self.ALon Loff = lambda self, V: self.phi * self.ALoff diff --git a/braincell/channel/sodium_test.py b/braincell/channel/sodium_test.py index b7bc9ba9..4de2bbd8 100644 --- a/braincell/channel/sodium_test.py +++ b/braincell/channel/sodium_test.py @@ -19,6 +19,7 @@ import brainstate import brainunit as u +import jax import jax.numpy as jnp import pytest @@ -47,6 +48,35 @@ ) +class TemperatureDerivedPhiTest(unittest.TestCase): + def test_all_derived_phi_variants_follow_temperature_and_gradient(self): + variants = ( + (Nav1p6_MA2020_GoC, 3.0, 22.0), + (Nav1p6_MA2024_PC, 3.0, 22.0), + (Nav1p6_MA2025_BC, 3.0, 22.0), + (Nav1p6_RI2021_SC, 3.0, 22.0), + (Nav1p1_MA2025_BC, 2.7, 22.0), + (Nav1p1_RI2021_SC, 2.7, 22.0), + (Nav_MA2020_GrC, 3.0, 20.0), + (NaFHF_MA2020_GrC, 3.0, 20.0), + ) + for cls, q10, ref in variants: + with self.subTest(channel=cls.__name__): + ch = cls(1, temp=u.celsius2kelvin(ref)) + ch.temp = u.celsius2kelvin(ref + 10.0) + self.assertTrue(u.math.allclose(ch.phi, q10)) + + def evaluate(offset): + ch.temp = u.celsius2kelvin(ref) + offset * u.kelvin + return u.math.sum(ch.phi) + + derivative = jax.grad(evaluate)(10.0) + self.assertTrue(u.math.allclose(derivative, q10 * jnp.log(q10) / 10.0)) + epsilon = 1e-3 + finite_difference = (evaluate(10.0 + epsilon) - evaluate(10.0 - epsilon)) / (2 * epsilon) + self.assertTrue(u.math.allclose(derivative, finite_difference, rtol=1e-7)) + + @pytest.fixture(autouse=True) def _float64(): """Run this module's tests at 64-bit precision. diff --git a/braincell/io/_geometry.py b/braincell/io/_geometry.py index fe31499f..cc6f7a94 100644 --- a/braincell/io/_geometry.py +++ b/braincell/io/_geometry.py @@ -112,7 +112,7 @@ def should_copy_attach_point( with the attachment but carries a different radius still gets the copied point, so the branch boundary keeps its radius jump as a zero-length first segment. That behaviour is an invariant recorded in - ``docs/design/io-swc-reader-invariants.md``. + ``docs/design/io/current/swc-reader-invariants.md``. * The ASC reader passes ``False``. Neurolucida traces repeat the parent terminal coordinate routinely, and NEURON's ``read_nlcda3.hoc`` suppresses the duplicate on coincident xyz regardless of diameter. diff --git a/braincell/io/swc/reader.py b/braincell/io/swc/reader.py index 9a71ca5e..925ce077 100644 --- a/braincell/io/swc/reader.py +++ b/braincell/io/swc/reader.py @@ -31,7 +31,7 @@ - ``braincell/io/swc/swc_test.py`` - ``braincell/_discretization/lower_test.py`` - ``examples/neuron_compare/cable/tests/`` -- ``docs/design/io-swc-reader-invariants.md`` +- ``docs/design/io/current/swc-reader-invariants.md`` """ from dataclasses import dataclass, field diff --git a/braincell/ion/_base.py b/braincell/ion/_base.py index 287b53a1..ec1cbc92 100644 --- a/braincell/ion/_base.py +++ b/braincell/ion/_base.py @@ -456,11 +456,16 @@ def _init_nernst_ion(self, *, Ci=None, Co=None, temp=None, valence=None): allow_none=False, ) self.temp = braintools.init.param(temp, self.varshape, allow_none=False) - self.E = None + self._E = brainstate.LongTermState(None) + + @property + def E(self): + """Return the reversal stored at the last initialization or refresh.""" + return self._E.value def _update_reversal(self): """Recompute and store ``E`` from the current ``Ci/Co/temp/valence``.""" - self.E = _nernst_of(self) + self._E.value = _nernst_of(self) def _ion_init_state_hook(self, V, batch_size: int = None): """Refresh the stored Nernst reversal during ion initialization.""" @@ -852,8 +857,15 @@ def _resolve_species_initializers(self, *, Ci_initializer, species_initializers) raise ValueError(f"{type(self).__name__} only accepts differential-species overrides; got {invalid_names}.") defaults = self._default_species_initializers(Ci_initializer) - defaults.update(overrides) - return {name: self._as_initializer(value) for name, value in defaults.items()} + if Ci_initializer is not None: + overrides.setdefault("Ci", Ci_initializer) + # Keep the recipe, not construction-time equilibrium concentrations. + return { + name: self._as_initializer(overrides[name]) + if name in overrides + else (lambda shape, name=name: u.math.broadcast_to(self._default_species_initializers(None)[name], shape)) + for name in defaults + } def _default_species_initializers(self, Ci_initializer) -> dict[str, Any]: """Return this ion's resting species initializers, before overrides. @@ -966,11 +978,9 @@ def _require_diam_arc_mean(self): def _seed_geometry(self): """Derive and cache the diameter-dependent shell factors. - ``dsq``, ``dsqvol``, and ``parea`` are fixed once the - compartment layer has written ``diam_arc_mean``, but the - reaction networks above them read ``dsqvol`` 39 to 97 times - within a single ``compute_derivative`` -- each read previously - re-deriving the same eight array operations. + ``dsq`` and ``parea`` depend only on fixed compartment geometry. + ``dsqvol`` additionally reads the live ``Nannuli`` parameter and + is deliberately not stored in this cache. The cache records the diameter it was built from, so rebinding ``diam_arc_mean`` reseeds on the next read rather than serving a @@ -979,7 +989,6 @@ def _seed_geometry(self): """ diam_arc_mean = self._require_diam_arc_mean() self._dsq = diam_arc_mean * diam_arc_mean - self._dsqvol = self._dsq * self.vrat self._parea = u.math.pi * diam_arc_mean self._geometry_source = diam_arc_mean @@ -993,16 +1002,11 @@ def _geometry(self, name: str): def vrat(self): """Outermost-shell volume ratio implied by ``Nannuli``. - Depends only on ``Nannuli``, which is fixed at construction, so - unlike the other three factors this one is available before the + Recomputed from the current ``Nannuli``; available before the compartment layer attaches ``diam_arc_mean``. """ - cached = getattr(self, "_vrat", None) - if cached is None: - dr2 = 0.25 / (self.Nannuli - 1.0) - cached = u.math.pi * (0.5 - (dr2 / 2.0)) * 2.0 * dr2 - self._vrat = cached - return cached + dr2 = 0.25 / (self.Nannuli - 1.0) + return u.math.pi * (0.5 - (dr2 / 2.0)) * 2.0 * dr2 @property def dsq(self): @@ -1012,7 +1016,7 @@ def dsq(self): @property def dsqvol(self): """Combined ``dsq * vrat`` volume factor for the outermost shell.""" - return self._geometry("_dsqvol") + return self.dsq * self.vrat @property def parea(self): @@ -1149,7 +1153,7 @@ def _species_value(self, spec, batch_size: int = None): :class:`braincell.Cell`, the grouped hidden-state rank contract. Broadcasting here keeps one species set homogeneous from the start. """ - init = self.owner.species_initializers.get(spec.name, spec.init) + init = _unwrap(self.owner.species_initializers.get(spec.name, spec.init)) value = braintools.init.param(init, self.owner.varshape, batch_size) target = tuple(self.owner.varshape) if batch_size is not None: diff --git a/braincell/ion/_base_test.py b/braincell/ion/_base_test.py index 8d259b6a..8907ac20 100644 --- a/braincell/ion/_base_test.py +++ b/braincell/ion/_base_test.py @@ -697,5 +697,31 @@ def test_all_three_templates_agree_on_the_same_inputs(self) -> None: self.assertTrue(u.math.allclose(init_nernst.E, expected, atol=1e-9 * u.mV)) +class DerivedInitialValueTest(unittest.TestCase): + def test_shell_volume_tracks_annulus_parameter(self): + from braincell.ion import CdpStC_NoCAM_MA2020_GoC + + ion = CdpStC_NoCAM_MA2020_GoC(size=1) + ion.diam_arc_mean = 2 * u.um + first = ion.dsqvol + ion.Nannuli = 20.0 + dr2 = 0.25 / 19.0 + expected = 4 * u.um**2 * u.math.pi * (0.5 - dr2 / 2) * 2 * dr2 + self.assertFalse(u.math.allclose(first, expected)) + self.assertTrue(u.math.allclose(ion.dsqvol, expected)) + + def test_buffer_default_recomputed_but_explicit_override_preserved(self): + from braincell.ion import CdpStC_NoCAM_MA2020_GoC + + for explicit in (False, True): + ion = CdpStC_NoCAM_MA2020_GoC(size=1, species_initializers={"Buff2": 7 * u.mM} if explicit else None) + ion.diam_arc_mean = 2 * u.um + ion.init_state(-65 * u.mV) + first = ion.Buff2.value + ion.Buffnull2 = 2 * ion.Buffnull2 + ion.reset_state(-65 * u.mV) + self.assertTrue(u.math.allclose(ion.Buff2.value, first if explicit else 2 * first)) + + if __name__ == "__main__": unittest.main() diff --git a/braincell/ion/calcium.py b/braincell/ion/calcium.py index 492bffa6..8f5cc9c3 100644 --- a/braincell/ion/calcium.py +++ b/braincell/ion/calcium.py @@ -3779,7 +3779,7 @@ def __init__( ): super().__init__(size, name=name, **channels) if Ci_initializer is None: - Ci_initializer = braintools.init.Constant(caiBase) + Ci_initializer = lambda shape: u.math.broadcast_to(self.caiBase, shape) self._init_dynamic_nernst_ion( Co=Co, temp=temp, @@ -3948,7 +3948,7 @@ def __init__( ): super().__init__(size, name=name, **channels) if Ci_initializer is None: - Ci_initializer = braintools.init.Constant(caliBase) + Ci_initializer = lambda shape: u.math.broadcast_to(self.caliBase, shape) self._init_dynamic_nernst_ion( Co=Co, temp=temp, diff --git a/braincell/morph/__init___test.py b/braincell/morph/__init___test.py index 66e5ae22..49256852 100644 --- a/braincell/morph/__init___test.py +++ b/braincell/morph/__init___test.py @@ -33,7 +33,7 @@ ``cannot import name 'RegionMask' from partially initialized module 'braincell.filter'``, because ``vis`` imports ``filter`` on the way up. -``docs/design/morph-layering-invariants.md`` records why each edge exists +``docs/design/morph/current/layering-invariants.md`` records why each edge exists and the measured failure for each one. The two checks below turn the invariant into a targeted failure so a future editor learns *why* rather than bisecting a traceback that does not mention their edit. @@ -150,7 +150,7 @@ def test_no_module_scope_import_reaches_upward(self) -> None: "braincell.morph is imported before braincell.io / .vis / .filter exist, so a " "module-scope import of one of them makes `import braincell` raise ImportError -- " "often from a package you did not touch. Move it into the method body. See " - "docs/design/morph-layering-invariants.md.", + "docs/design/morph/current/layering-invariants.md.", ) diff --git a/braincell/network/connection.py b/braincell/network/connection.py index e48b86a9..0c2be9f9 100644 --- a/braincell/network/connection.py +++ b/braincell/network/connection.py @@ -26,6 +26,7 @@ import numpy as np from braincell._misc import require_name as _require_name, scalar_decimal +from braincell._parameter_schema import RuntimeParameterState from braincell._multi_compartment.synapses import SynapseView, _cell_label from .pairing import ( PairingContext, @@ -88,6 +89,21 @@ class _ConnectionCall: live_delay_steps: np.ndarray | None = None live_dt_ms: float | None = None + def __post_init__(self): + if self.weight is not None: + object.__setattr__(self, "weight", RuntimeParameterState(self.weight)) + + def __getattribute__(self, name): + value = object.__getattribute__(self, name) + return value.value if name == "weight" and isinstance(value, RuntimeParameterState) else value + + def __setattr__(self, name, value): + state = vars(self).get(name) + if name == "weight" and isinstance(state, RuntimeParameterState): + state.value = value + else: + object.__setattr__(self, name, value) + class _ConnectionStore: """Cell-owned SoA columns for all direct event-routing rows.""" @@ -405,10 +421,31 @@ def for_population(self, population_indices) -> "ConnectionView": selected = np.asarray(tuple(int(index) for index in population_indices), dtype=np.int64) return ConnectionView(self._store, self._active_ids[np.isin(self.synapse.population_index, selected)]) + def trainable(self, **fields): + """Bind weight sources to selected contacts; delay remains static. + + Parameters + ---------- + **fields + ``weight`` mapped to a trainable parameter source. + + Returns + ------- + ConnectionView + This selection. + """ + from braincell.trainable._targets import register_connection + + register_connection(self, fields) + return self + def set(self, *, weight=_UNSET, delay=_UNSET) -> "ConnectionView": """Update selected routing rows before Cell initialization.""" self.cell._raise_if_initialized("modify Connection") if weight is not _UNSET: + from braincell.trainable._targets import require_unbound + + require_unbound(self.cell, "connection", "weight", self.id, "weight") self._require_homogeneous_synapse_type("modify weight") normalized = _normalize_weight(self.synapse, weight, count=len(self), omitted=False) current = self._store.weight_for(self._active_ids) @@ -837,8 +874,8 @@ def _stack_values(values): first = values[0] if isinstance(first, u.Quantity): unit = first.unit - return u.Quantity(np.asarray([value.to_decimal(unit) for value in values]), unit) - return np.asarray(values) + return u.Quantity(jnp.stack([jnp.asarray(value.to_decimal(unit)) for value in values]), unit) + return jnp.stack([jnp.asarray(value) for value in values]) def _split_values(value, count: int): diff --git a/braincell/network/core.py b/braincell/network/core.py index b22d65c8..1b46bc31 100644 --- a/braincell/network/core.py +++ b/braincell/network/core.py @@ -53,6 +53,7 @@ class Population: "size", "ids", "event_outputs", + "outputs", "synapses", "connections", "metadata", @@ -98,6 +99,13 @@ def event_outputs(self): port = "spike" if self.kind == "netstim" else "event" return MappingProxyType({port: self.model.view}) + @property + def outputs(self): + """Return raw outputs exposed by a Cell population.""" + if self.kind != "cell": + raise TypeError(f"Population {self.name!r} owns {self.kind}, not a Cell with raw outputs.") + return self.model.outputs + @property def synapses(self): """Return logical synapses owned by a Cell population.""" diff --git a/braincell/network/delivery.py b/braincell/network/delivery.py index 97f03d1f..2e78a961 100644 --- a/braincell/network/delivery.py +++ b/braincell/network/delivery.py @@ -34,6 +34,7 @@ import brainstate import brainunit as u +import jax import jax.numpy as jnp import numpy as np @@ -71,6 +72,16 @@ class DeliveryBlock: pre_index: np.ndarray flat_target_index: np.ndarray weight: object + weight_indices: object = None + + def __getattribute__(self, name): + if name == "weight": + source = object.__getattribute__(self, "source") + if getattr(source, "weight_getter", None) is not None: + indices = object.__getattribute__(self, "weight_indices") + value = source.weight + return value if indices is None else value[indices] + return object.__getattribute__(self, name) @dataclass(frozen=True) @@ -170,6 +181,7 @@ def delivery_blocks( pre_index=block.pre_index[contact_indices], flat_target_index=block.synapse_index[contact_indices].astype(np.int32, copy=False), weight=slice_weight(block.weight, contact_indices), + weight_indices=contact_indices, ) ) return tuple(delivery) @@ -350,7 +362,7 @@ def write_arrivals( ) post_population, layout_id = key cell = populations[post_population].cell - cell.runtime.event_buffers[layout_id].value = arrival + cell._write_event_arrival(layout_id, arrival) def enqueue_future_events( @@ -535,7 +547,9 @@ def make_delivery_op( The returned callable captures static sparse indices as JAX arrays. The scatter path computes ``pre_spike[pre_index] * weight`` and accumulates it into ``flat_target_index``. The ``brainevent`` path uses ``brainevent.coomv`` - with the same sparse topology. + with the same sparse topology. Its exact bilinear derivative uses scatter + operations so batching weight and event tangents does not depend on the + backend's sparse-primitive batching rules. """ target_size = int(block.source.n_active) pre_index = jnp.asarray(block.pre_index, dtype=jnp.int32) @@ -546,11 +560,11 @@ def make_delivery_op( except Exception: # pragma: no cover backend = "scatter" if backend == "brainevent" and hasattr(brainevent, "coomv"): - data = block.weight - def _op(pre_spike): + @jax.custom_jvp + def _coomv(weight, pre_spike): return brainevent.coomv( - data, + weight, pre_index, flat_target_index, pre_spike, @@ -559,6 +573,24 @@ def _op(pre_spike): backend=brainevent_backend, ) + def _scatter(weight, pre_spike): + contact_event = pre_spike[pre_index] * weight + return jnp.zeros((target_size,), dtype=contact_event.dtype).at[flat_target_index].add(contact_event) + + @_coomv.defjvp + def _coomv_jvp(primals, tangents): + weight, event = primals + dweight, devent = tangents + # d(W z) = dW z + W dz, including repeated sparse destinations. + return _coomv(weight, event), _scatter(dweight, event) + _scatter(weight, devent) + + def _op(pre_spike): + weight, weight_unit = u.split_mantissa_unit(block.weight) + event, event_unit = u.split_mantissa_unit(pre_spike) + dtype = jnp.result_type(weight, event, float) + result = _coomv(jnp.asarray(weight, dtype=dtype), jnp.asarray(event, dtype=dtype)) + return u.maybe_decimal(result * weight_unit * event_unit) + return _op def _op(pre_spike): diff --git a/braincell/network/delivery_test.py b/braincell/network/delivery_test.py index 03429add..dedee4f8 100644 --- a/braincell/network/delivery_test.py +++ b/braincell/network/delivery_test.py @@ -20,8 +20,12 @@ via :meth:`Network.run`.""" import unittest +from types import ModuleType, SimpleNamespace +from unittest.mock import patch import brainunit as u +import jax +import jax.numpy as jnp import numpy as np @@ -53,18 +57,53 @@ def test_a_plain_payload_keeps_its_dtype(self) -> None: class DeliveryTest(unittest.TestCase): - def test_event_backend_brainevent_requires_coomv(self) -> None: - import braincell.network.delivery as delivery + def test_brainevent_batched_weight_and_event_derivatives(self) -> None: + from braincell.network.delivery import DeliveryBlock, make_delivery_op try: import brainevent - except Exception: - return - if hasattr(brainevent, "coomv"): - return + except ImportError: + self.skipTest("brainevent is unavailable") + if not hasattr(brainevent, "coomv"): + self.skipTest("brainevent.coomv is unavailable") + + for weights in (jnp.asarray([0.2, 0.3, 0.4, 0.5]), jnp.asarray([0.2])): + for unit in (u.UNITLESS, u.uS): + with self.subTest(weights=weights.shape, unit=unit): + source = SimpleNamespace(n_active=2) + + def evaluate(weight, event, backend): + block = DeliveryBlock( + source, 0, np.asarray([0, 1, 0, 1]), np.asarray([0, 0, 1, 1]), weight * unit + ) + result = make_delivery_op(block, pre_size=2, backend=backend)(event) + self.assertEqual(u.get_unit(result), unit) + return u.get_mantissa(result) + + event = jnp.asarray([1.0, 0.5]) + directions = jnp.eye(weights.size + event.size) + results = [] + for backend in ("scatter", "brainevent"): + call = lambda w, e: evaluate(w, e, backend) + primal, linear = jax.linearize(call, weights, event) + tangent = jax.jit(jax.vmap(linear))( + directions[:, : weights.size], directions[:, weights.size :] + ) + reverse = jax.jit(jax.grad(lambda w, e: call(w, e).sum(), argnums=(0, 1)))(weights, event) + bool_event = jax.jit(lambda w: call(w, jnp.asarray([True, False])))(weights) + bool_gradient = jax.jit(jax.grad(lambda w: call(w, jnp.asarray([True, False])).sum()))(weights) + results.append((primal, tangent, reverse, bool_event, bool_gradient)) + for expected, actual in zip(jax.tree.leaves(results[0]), jax.tree.leaves(results[1])): + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-7) + + def test_event_backend_selection_without_coomv(self) -> None: + import braincell.network.delivery as delivery - with self.assertRaisesRegex(RuntimeError, "brainevent.coomv"): - delivery.resolve_event_backend("brainevent") + with patch.dict("sys.modules", {"brainevent": ModuleType("brainevent")}): + self.assertEqual(delivery.resolve_event_backend("auto"), "scatter") + self.assertEqual(delivery.resolve_event_backend("scatter"), "scatter") + with self.assertRaisesRegex(RuntimeError, "brainevent.coomv"): + delivery.resolve_event_backend("brainevent") if __name__ == "__main__": diff --git a/braincell/network/engine.py b/braincell/network/engine.py index e7ddb36d..a1414588 100644 --- a/braincell/network/engine.py +++ b/braincell/network/engine.py @@ -92,6 +92,10 @@ def __init__(self, name: str | None = None, *, seed: int = 0) -> None: self._cell_lifecycle_active = False self._initialized = False self._source_current_time = 0.0 * u.ms + from braincell.trainable._network import NetworkTrainables + + self.trainables = NetworkTrainables(self) + self._prepared_run = None def _raise_if_initialized(self, action: str) -> None: if self._initialized: @@ -184,10 +188,6 @@ def add_population(self, name: str, model, **metadata) -> Population: if any(existing.model is model for existing in self.populations.values()): raise ValueError("The same model object cannot be registered as more than one Network population.") if population.kind == "cell": - if model.trainables.bindings(): - raise NotImplementedError( - "Network aggregation of trainable Cell parameters is deferred; add an unbound Cell." - ) model._bind_network_owner(self) elif population.kind == "netstim": model._bind_network_seed(self.seed, population.name) @@ -383,6 +383,7 @@ def run( self.init_state() if not self._cell_populations(): return self._run_scheduled_sources_only(dt=dt, duration=duration) + self.trainables.materialize() setup_key = self._run_setup_cache_key( dt=dt, delay_quantization=delay_quantization, @@ -473,6 +474,78 @@ def run( dt=dt, ) + def prepare_run(self, *, dt, delay_quantization="nearest", event_backend="auto", brainevent_backend="jax_raw"): + """Prepare static routing and queues outside a differentiated rollout. + + Parameters + ---------- + dt : brainunit.Quantity + Fixed integration step. + delay_quantization : str, optional + Existing delay quantization policy; delays are never trainable. + event_backend : str, optional + Event delivery backend, as in :meth:`run`. + brainevent_backend : str or None, optional + Backend for the sparse event operator. + + Returns + ------- + Network + This initialized and prepared network. + """ + validate_time_quantity(dt, name="dt", prefix="Network.prepare_run") + event_backend = normalize_event_backend(event_backend) + self.init_state() + if not self._cell_populations(): + raise ValueError("Network.prepare_run requires at least one Cell population.") + key = self._run_setup_cache_key( + dt=dt, + delay_quantization=delay_quantization, + event_backend=event_backend, + brainevent_backend=brainevent_backend, + ) + if self._runtime_config is not None and self._runtime_config != key[2:]: + raise RuntimeError("Network runtime configuration is fixed after the first run.") + self._runtime_config = key[2:] + setup = self._run_setup( + dt=dt, + delay_quantization=delay_quantization, + event_backend=event_backend, + brainevent_backend=brainevent_backend, + ) + self._common_start_time(setup.ordered_population_names) + cached = self._network_run_loop(setup=setup, setup_key=key, dt=dt, n_steps=1) + self.trainables.materialize() + self._prepared_run = (setup, cached, dt) + return self + + def update(self): + """Advance a prepared network by one differentiable time step. + + Returns + ------- + dict + Floating spike arrays keyed by Cell population name. Continuous + observations remain available on each Cell's runtime states. + + Raises + ------ + RuntimeError + If :meth:`prepare_run` has not been called outside tracing. + + Notes + ----- + Compose with ``brainstate.transform.for_loop`` or ``scan``. No event + tables or host-side recording conversions are built here. + """ + if self._prepared_run is None: + raise RuntimeError("Call Network.prepare_run() before update().") + setup, cached, dt = self._prepared_run + self.trainables.materialize() + first = self.populations[setup.ordered_population_names[0]].cell + cached.runner(first.current_time, u.math.zeros((1,)) * dt) + return {name: self.populations[name].cell.spike.value for name in setup.ordered_population_names} + def _run_setup( self, *, @@ -524,7 +597,7 @@ def _run_setup( # runs real gathers outside jit just to discard everything but the # names. probe_names() reads the same ordering off the layouts. probe_names = { - name: tuple(sorted(_probes.probe_names(population.cell))) + name: (() if population.cell._uses_reduction else tuple(sorted(_probes.probe_names(population.cell)))) for name, population in self._cell_populations().items() } compiled_recordings = { @@ -557,7 +630,7 @@ def _run_setup_cache_key( ) -> tuple: dt_ms = scalar_decimal(dt, u.ms) runtime_ids = tuple( - (name, id(population.cell.runtime), population.size) + (name, id(population.cell._event_runtime()), population.size) for name, population in self._cell_populations().items() ) return ( @@ -626,11 +699,14 @@ def _step(t): for name in ordered_population_names: self.populations[name].cell._prepare_step_clamps(t=t, dt=dt) with jax.named_scope("braincell:network_run:sample_recordings"): - recording_snapshots = tuple( - compiled.sample() - for name in ordered_population_names - for compiled in compiled_recordings[name] + flat_recordings = tuple( + compiled for name in ordered_population_names for compiled in compiled_recordings[name] ) + pre_recording_snapshots = { + index: compiled.sample() + for index, compiled in enumerate(flat_recordings) + if compiled.phase == "pre" + } with jax.named_scope("braincell:network_run:write_arrivals"): write_arrivals(delivery_state, populations=self.populations) with jax.named_scope("braincell:network_run:prepare_inputs"): @@ -654,6 +730,11 @@ def _step(t): snapshots = { name: self.populations[name].cell.sample_probes() for name in ordered_population_names } + with jax.named_scope("braincell:network_run:sample_outputs"): + recording_snapshots = tuple( + pre_recording_snapshots[index] if compiled.phase == "pre" else compiled.sample() + for index, compiled in enumerate(flat_recordings) + ) with jax.named_scope("braincell:network_run:record_events"): events = tuple( view.owner.current_event_count(view.source_id) diff --git a/braincell/network/engine_test.py b/braincell/network/engine_test.py index 76410443..6cfe043d 100644 --- a/braincell/network/engine_test.py +++ b/braincell/network/engine_test.py @@ -15,6 +15,7 @@ import unittest +import brainstate import brainunit as u import numpy as np @@ -25,12 +26,86 @@ class NetworkRuntimeTest(unittest.TestCase): - def test_cell_with_trainable_bindings_is_rejected_until_network_aggregation_exists(self) -> None: + def test_preparation_requires_cell_and_keeps_configuration_static(self): + net = Network("source_only") + net.add_population("input", braincell.NetStim()) + with self.assertRaisesRegex(ValueError, "at least one Cell"): + net.prepare_run(dt=0.1 * u.ms) + net = make_runtime_network() + net.prepare_run(dt=0.1 * u.ms, event_backend="scatter") + with self.assertRaisesRegex(RuntimeError, "configuration is fixed"): + net.prepare_run(dt=0.2 * u.ms, event_backend="scatter") + + def test_cell_with_trainable_bindings_is_aggregated_without_copying(self) -> None: cell = make_threshold_cell() cell.paint(AllRegion(), braincell.mech.Channel("IL", name="leak")) cell.channels["leak"].trainable(g_max=braincell.trainable.scale(name="factor")) - with self.assertRaisesRegex(NotImplementedError, "Network aggregation"): - Network("network").add_population("cell", cell) + network = Network("network") + network.add_population("cell", cell) + self.assertIs( + network.trainables.parameters().states()["cell.factor"], cell.trainables.parameters().states()["factor"] + ) + + def test_prepared_network_keeps_weight_and_threshold_gradients(self): + self._check_prepared_network_gradients("scatter") + + def test_prepared_network_keeps_weight_and_threshold_gradients_brainevent(self): + try: + import brainevent + except ImportError: + self.skipTest("brainevent is unavailable") + if not hasattr(brainevent, "coomv"): + self.skipTest("brainevent.coomv is unavailable") + self._check_prepared_network_gradients("brainevent") + + def _check_prepared_network_gradients(self, backend): + network = make_runtime_network(delay=0.2 * u.ms) + post = network.populations["post"].cell + pre = network.populations["pre"].cell + post.connections["drive"].trainable(weight=braincell.trainable.scale(name="w")) + pre.event_outputs["spike"].trainable(threshold=braincell.trainable.parameter(group_by="all", name="th")) + network.prepare_run(dt=0.1 * u.ms, event_backend=backend) + synapse = post.runtime.get_runtime_node(post.synapses["exp"]._store.layout_id("ExpSyn")) + + def observe(): + network.reset_state() + brainstate.transform.for_loop(lambda _: network.update(), np.arange(5)) + return synapse.g.value.to_decimal(u.uS).sum() + + run = brainstate.transform.jit(observe) + grad = brainstate.transform.jit( + brainstate.transform.grad(observe, grad_states=network.trainables.parameters().states()) + ) + first = run() + gradients = grad() + self.assertGreater(float(first), 0.0) + self.assertNotEqual(float(gradients["post.w"]), 0.0) + self.assertNotEqual(float(u.get_mantissa(gradients["pre.th"])), 0.0) + values = network.trainables.parameters().physical_values() + values["post.w"] = 2.0 + network.trainables.parameters().set_physical_values(values) + np.testing.assert_allclose(run(), 2 * first, rtol=1e-6) + + def test_prepare_required_and_update_matches_run(self): + network = make_runtime_network(delay=0.2 * u.ms) + with self.assertRaisesRegex(RuntimeError, "prepare_run"): + network.update() + network.prepare_run(dt=0.1 * u.ms, event_backend="scatter") + post = network.populations["post"].cell + synapse = post.runtime.get_runtime_node(post.synapses["exp"]._store.layout_id("ExpSyn")) + + def step(_): + network.update() + return synapse.g.value + + actual = brainstate.transform.for_loop(step, np.arange(5)) + reference = make_runtime_network(delay=0.2 * u.ms).run( + dt=0.1 * u.ms, duration=0.5 * u.ms, event_backend="scatter" + ) + # This fixture records before the step; update() exposes post-step state. + np.testing.assert_allclose( + actual.to_decimal(u.uS)[:-1], reference.samples["post"]["g"].values.to_decimal(u.uS)[1:], rtol=1e-6 + ) def test_cell_has_one_network_execution_owner(self) -> None: cell = make_threshold_cell() diff --git a/braincell/network/event.py b/braincell/network/event.py index 17522056..d84bc99d 100644 --- a/braincell/network/event.py +++ b/braincell/network/event.py @@ -29,6 +29,7 @@ import brainstate import brainunit as u import numpy as np +from braincell._parameter_schema import RuntimeParameterState __all__ = [ "EventSource", @@ -84,6 +85,22 @@ def __len__(self) -> int: def __getitem__(self, selector) -> "EventSourceView": return self.view[selector] + def trainable(self, **fields): + """Bind threshold parameter sources to all detector endpoints. + + Parameters + ---------- + **fields + ``threshold`` mapped to a trainable source. + + Returns + ------- + EventSource + This detector. + """ + self.view.trainable(**fields) + return self + def event_count(self, source_index, *, t, delay, dt): """Read live event counts; scheduled sources override this method.""" _ = t, dt @@ -135,6 +152,24 @@ def source_id(self) -> np.ndarray: def __len__(self) -> int: return int(self._source_ids.size) + def trainable(self, **fields): + """Bind threshold parameter sources to selected detector endpoints. + + Parameters + ---------- + **fields + ``threshold`` mapped to a trainable source. + + Returns + ------- + EventSourceView + This selection. + """ + from braincell.trainable._targets import register_detector + + register_detector(self, fields) + return self + def __getitem__(self, selector) -> "EventSourceView": selected = self._source_ids[selector] return EventSourceView(self._owner, np.asarray(selected).reshape(-1)) @@ -408,6 +443,23 @@ class VoltageCrossingSource(EventSource): Omitting ``location`` selects the root branch midpoint. Endpoint rows are ordered population-major, preserving the evaluated location order and any duplicate locations. + + Parameters + ---------- + cells : Cell or CellView + Cell endpoints whose voltages are observed. + location : location expression, optional + Morphology locations; defaults to the root midpoint. + threshold : brainunit.Quantity, optional + Explicit voltage threshold, or the owning Cell's V_th when omitted. + direction : str, optional + ``rising`` uses last < threshold <= next; ``falling`` reverses both + comparisons. Staying at equality never repeats an event. + spk_fun : callable or None, optional + Hard Heaviside forward function (one at zero) with surrogate derivative. + Defaults to the Cell's spk_fun. Direction only changes its input sign. + name : str or None, optional + Source name. """ __slots__ = ( @@ -420,6 +472,8 @@ class VoltageCrossingSource(EventSource): "_population_indices", "_location_indices", "_cv_ids", + "spk_fun", + "_training_id", ) def __init__( @@ -429,6 +483,7 @@ def __init__( location=None, threshold=_CELL_VOLTAGE_THRESHOLD, direction: str = "rising", + spk_fun=None, name: str | None = None, ) -> None: from braincell.filter import RootLocation @@ -481,8 +536,13 @@ def __init__( self.cells = root self.location = location self.direction = direction + if spk_fun is not None and not callable(spk_fun): + raise TypeError("VoltageCrossingSource.spk_fun must be callable or None.") + self.spk_fun = spk_fun self.name = name - self._threshold = normalized_threshold + self._threshold = None if uses_cell_threshold else RuntimeParameterState(normalized_threshold) + self._training_id = root._next_detector_id + root._next_detector_id += 1 self._uses_cell_threshold = uses_cell_threshold self._population_indices = np.repeat(population_indices, n_location) self._location_indices = np.tile(np.arange(n_location, dtype=np.int64), n_population) @@ -504,7 +564,7 @@ def location_index(self) -> np.ndarray: @property def threshold(self): """Return the explicit threshold, or the Cell threshold when omitted.""" - return self.cells.V_th if self._uses_cell_threshold else self._threshold + return self.cells.V_th if self._uses_cell_threshold else self._threshold.value @property def instance_name(self) -> str: @@ -523,12 +583,14 @@ def cv_id(self) -> np.ndarray: def current_event_count(self, source_index): """Return current-boundary crossing values for selected detector rows.""" self.cells._raise_if_not_initialized("read VoltageCrossingSource") + if self.cells._uses_reduction: + raise RuntimeError("VoltageCrossingSource is unavailable while its Cell uses a reduction model.") if not hasattr(self.cells, "_event_previous_V"): raise RuntimeError("Cell runtime does not expose previous voltage for threshold detection.") source_index = np.asarray(source_index, dtype=np.int64) population_index = self._population_indices[source_index] cv_id = self._cv_ids[source_index] - if self._uses_cell_threshold and self.direction == "rising": + if self._uses_cell_threshold and self.direction == "rising" and self.spk_fun is None: spike = self.cells.spike.value if len(self.cells.pop_size) == 0: return spike[cv_id] @@ -539,16 +601,24 @@ def current_event_count(self, source_index): if len(self.cells.pop_size) == 0: last = last_v[cv_id] next_value = next_v[cv_id] - threshold = self.cells.V_th[cv_id] if self._uses_cell_threshold else self._threshold[source_index] + threshold = self.cells.V_th[cv_id] if self._uses_cell_threshold else self._threshold.value[source_index] else: last = last_v[population_index, cv_id] next_value = next_v[population_index, cv_id] threshold = ( - self.cells.V_th[population_index, cv_id] if self._uses_cell_threshold else self._threshold[source_index] + self.cells.V_th[population_index, cv_id] + if self._uses_cell_threshold + else self._threshold.value[source_index] ) - if self.direction == "rising": - return (last < threshold) & (next_value >= threshold) - return (last > threshold) & (next_value <= threshold) + from braincell._base_neuron import _threshold_crossing + + return _threshold_crossing( + last, + next_value, + threshold, + self.cells.spk_fun if self.spk_fun is None else self.spk_fun, + direction=self.direction, + ) class _CellSpikeSource(EventSource): @@ -591,9 +661,9 @@ def current_event_count(self, source_index): self.cell._raise_if_not_initialized("read cell.event_outputs['spike']") source_index = np.asarray(source_index, dtype=np.int64) spike = self.cell.spike.value - if len(self.cell.pop_size) == 0: - return u.math.broadcast_to(spike[self.cv_id], source_index.shape) - return spike[source_index, self.cv_id] + if self.cell._uses_reduction: + return spike[..., source_index] + return spike[..., source_index, self.cv_id] class EventOutputCollection: diff --git a/braincell/network/event_test.py b/braincell/network/event_test.py index e4a1272f..7ec036b3 100644 --- a/braincell/network/event_test.py +++ b/braincell/network/event_test.py @@ -16,6 +16,7 @@ import unittest import brainstate +import braintools import brainunit as u import numpy as np @@ -126,6 +127,27 @@ def test_validates_shape_range_and_units(self) -> None: class EventSourceTest(unittest.TestCase): + def test_threshold_equality_directions_and_surrogate_override(self): + cell = _two_cv_population(size=1) + rising = VoltageCrossingSource(cell, threshold=0.0 * u.mV) + falling = VoltageCrossingSource(cell, threshold=0.0 * u.mV, direction="falling") + custom = VoltageCrossingSource(cell, threshold=0.0 * u.mV, spk_fun=braintools.surrogate.Sigmoid()) + cell.init_state() + for last in (-1.0, 0.0, 1.0): + for new in (-1.0, 0.0, 1.0): + cell._event_previous_V.value = np.full((1, 2), last) * u.mV + cell.V.value = np.full((1, 2), new) * u.mV + for detector, expected in ( + (rising, last < 0.0 <= new), + (falling, last > 0.0 >= new), + (custom, last < 0.0 <= new), + ): + np.testing.assert_array_equal(detector.current_event_count([0]), [float(expected)]) + cell.reset_state() + np.testing.assert_array_equal(rising.current_event_count([0]), [0.0]) + with self.assertRaises(TypeError): + VoltageCrossingSource(cell, spk_fun="not callable") + def test_event_source_view_preserves_order_and_duplicates(self) -> None: source = NetStim(size=3) view = source[[2, 0, 2]] diff --git a/braincell/network/lowering.py b/braincell/network/lowering.py index c1791919..fa998af5 100644 --- a/braincell/network/lowering.py +++ b/braincell/network/lowering.py @@ -17,7 +17,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field import brainunit as u import numpy as np @@ -43,6 +43,14 @@ class ConnectionBlock: weight: object delay_steps: np.ndarray event_source: object + weight_getter: object = field(default=None, repr=False, compare=False) + + def __getattribute__(self, name): + if name == "weight": + getter = object.__getattribute__(self, "weight_getter") + if getter is not None: + return getter() + return object.__getattribute__(self, name) def lower_direct_connections( @@ -73,7 +81,7 @@ def lower_direct_connections( raise TypeError("One direct Connection block must target exactly one synapse type.") synapse_type = str(synapse_types[0]) layout_id = post.cell._get_synapse_store().layout_id(synapse_type) - layout = post.cell.runtime.layouts[layout_id] + layout = post.cell._event_layout(layout_id) synapse_index = post.cell._get_synapse_store().runtime_rows(connection.synapse_id).astype(np.int32) delay_steps = _expand_delay_steps( connection.delay, @@ -94,6 +102,7 @@ def lower_direct_connections( weight=connection.weight, delay_steps=delay_steps, event_source=source, + weight_getter=lambda connection=connection: connection.weight, ) ) return tuple(blocks) diff --git a/braincell/network/recording.py b/braincell/network/recording.py index bea771d2..7097b6bd 100644 --- a/braincell/network/recording.py +++ b/braincell/network/recording.py @@ -49,6 +49,11 @@ class _CellStateObservable: field: str +@dataclass(frozen=True) +class _OutputObservable: + name: str + + @dataclass(frozen=True) class _MechanismStateObservable: category: str @@ -103,6 +108,11 @@ def state(self, field: str): _require_name(field, "state field") return _CellStateObservable(field) + def output(self, name: str): + """Observe one selected model output after each model update.""" + _require_name(name, "output name") + return _OutputObservable(name) + def channel(self, *, type: str | None = None, name: str | None = None): """Select density channels by one explicit type or name.""" return _MechanismObservableBuilder("channel", _one_selector(type=type, name=name)) @@ -184,9 +194,9 @@ class RecordingRow: """Static metadata for one column in a SampleBlock.""" population_index: int - cv_id: int - point_id: int - branch_id: int + cv_id: int | None + point_id: int | None + branch_id: int | None field: str unit: object | None mechanism_category: str | None = None @@ -197,6 +207,8 @@ class RecordingRow: placement_id: int | None = None clamp_ids: tuple[int, ...] = () contributor_ids: tuple[int, ...] = () + output_name: str | None = None + output_index: tuple[int, ...] = () @dataclass(frozen=True) @@ -316,7 +328,8 @@ def compile_recording(cell, spec: RecordingSpec, *, dt): schedule_start=spec.start, time_offset=(0.5 * dt if isinstance(spec.observable, _ClampCurrentObservable) else 0.0 * u.ms), ) - return _CompiledRecording(spec=spec, schema=schema, sample=sampler) + phase = "post" if isinstance(spec.observable, _OutputObservable) else "pre" + return _CompiledRecording(spec=spec, schema=schema, sample=sampler, phase=phase) @dataclass(frozen=True) @@ -324,10 +337,46 @@ class _CompiledRecording: spec: RecordingSpec schema: RecordingSchema sample: object = field(compare=False, repr=False) + phase: str = "pre" + + +def recording_is_active(cell, spec: RecordingSpec) -> bool: + """Return whether a declaration belongs to the currently selected model.""" + if isinstance(spec.observable, _OutputObservable): + return spec.observable.name in cell.outputs + return not cell._uses_reduction def _observable_rows_and_sampler(cell, spec: RecordingSpec): observable = spec.observable + if isinstance(observable, _OutputObservable): + if spec.scope.spatially_restricted: + raise ValueError("observe.output(...) only supports Cell or population-only CellView scopes.") + value = cell.outputs[observable.name] + runtime_prefix_rank = len(cell.pop_size) + (1 if cell._runtime_batch_size is not None else 0) + feature_shape = tuple(value.shape[runtime_prefix_rank:]) + rows = tuple( + RecordingRow( + population_index=int(population_index), + cv_id=None, + point_id=None, + branch_id=None, + field=observable.name, + unit=None, + output_name=observable.name, + output_index=tuple(int(item) for item in feature_index), + ) + for population_index in spec.scope.population_indices + for feature_index in np.ndindex(feature_shape) + ) + + def sample(): + selected = cell._selected_outputs(spec.scope.population_indices)[observable.name] + batch_prefix = (selected.shape[0],) if cell._runtime_batch_size is not None else () + return u.math.reshape(selected, batch_prefix + (len(rows),)) + + return rows, sample + if isinstance(observable, _CellStateObservable): if observable.field != "v": raise KeyError(f"Cell state {observable.field!r} is not recordable in v1.") diff --git a/braincell/quad/protocol.py b/braincell/quad/protocol.py index 54a4e9e0..848d4ba6 100644 --- a/braincell/quad/protocol.py +++ b/braincell/quad/protocol.py @@ -27,7 +27,7 @@ trailing compartment axis with :class:`DiffEqGroupState`. See ``docs/specs/2026-08-13-cell-hidden-group-state.md``, ``docs/specs/2026-08-14-diffeq-state-mixin-split.md``, and -``docs/design/cell.md``. +``docs/design/cell/current/architecture.md``. """ import contextlib diff --git a/braincell/reduction/__init__.py b/braincell/reduction/__init__.py new file mode 100644 index 00000000..b8fb644b --- /dev/null +++ b/braincell/reduction/__init__.py @@ -0,0 +1,48 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Interchangeable lightweight execution models for detailed Cells.""" + +from braincell.reduction.core import ( + ReductionContext, + ReductionInputGroup, + ReductionInputGroupSchema, + ReductionInputs, + ReductionModel, + ReductionOutput, + ReductionSynapse, + ReductionView, + ReductionViewCollection, +) +from braincell.reduction.toy import ( + EventAccumulatorReduction, + PayloadAccumulatorReduction, + SynapticKernelAccumulatorReduction, +) + +__all__ = [ + "EventAccumulatorReduction", + "PayloadAccumulatorReduction", + "ReductionContext", + "ReductionInputGroup", + "ReductionInputGroupSchema", + "ReductionInputs", + "ReductionModel", + "ReductionOutput", + "ReductionSynapse", + "ReductionView", + "ReductionViewCollection", + "SynapticKernelAccumulatorReduction", +] diff --git a/braincell/reduction/core.py b/braincell/reduction/core.py new file mode 100644 index 00000000..d61961cb --- /dev/null +++ b/braincell/reduction/core.py @@ -0,0 +1,264 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Public contracts for interchangeable Cell reduction models.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Iterator, Mapping +from dataclasses import dataclass, field +from types import MappingProxyType +import weakref + +import numpy as np + +__all__ = [ + "ReductionContext", + "ReductionInputGroup", + "ReductionInputGroupSchema", + "ReductionInputs", + "ReductionModel", + "ReductionOutput", + "ReductionSynapse", + "ReductionView", + "ReductionViewCollection", +] + + +@dataclass(frozen=True) +class ReductionSynapse: + """Describe one logical synapse exposed to a reduction model.""" + + id: int + synapse_index: int + population_index: int + placement_id: int + point_id: int + cv_id: int + branch_id: int + branch_x: float + name: str + synapse_type: str + parameters: Mapping[str, object] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__(self, "parameters", MappingProxyType(dict(self.parameters))) + + +@dataclass(frozen=True) +class ReductionInputGroupSchema: + """Describe one packed homogeneous synapse-payload group.""" + + layout_id: int + synapse_type: str + event_input: object + synapse_id: np.ndarray + synapse_index: np.ndarray + population_index: np.ndarray + + def __post_init__(self) -> None: + for name in ("synapse_id", "synapse_index", "population_index"): + value = np.asarray(getattr(self, name), dtype=np.int64).reshape(-1).copy() + value.flags.writeable = False + object.__setattr__(self, name, value) + + @property + def size(self) -> int: + """Return the number of packed synapse rows in this group.""" + return int(self.synapse_id.size) + + +@dataclass(frozen=True) +class ReductionInputGroup: + """Pair one static input schema with its current event payload.""" + + schema: ReductionInputGroupSchema + payload: object + + +@dataclass(frozen=True) +class ReductionInputs: + """Hold all synapse payload groups consumed by one reduced update.""" + + groups: tuple[ReductionInputGroup, ...] = () + + def __iter__(self): + return iter(self.groups) + + def __len__(self) -> int: + return len(self.groups) + + +@dataclass(frozen=True) +class ReductionOutput: + """Return named raw values and one canonical event output.""" + + values: Mapping[str, object] + event: object + + def __post_init__(self) -> None: + prepared = dict(self.values) + for name in prepared: + if not isinstance(name, str) or not name: + raise ValueError("Reduction output names must be non-empty strings.") + object.__setattr__(self, "values", MappingProxyType(prepared)) + + +@dataclass(frozen=True) +class ReductionContext: + """Provide a reduction model with the current static Cell input schema.""" + + pop_size: tuple[int, ...] + synapses: tuple[ReductionSynapse, ...] + input_groups: tuple[ReductionInputGroupSchema, ...] + fingerprint: str + _cell_ref: object = field(repr=False, compare=False) + + @classmethod + def with_cell( + cls, + cell, + *, + synapses: tuple[ReductionSynapse, ...], + input_groups: tuple[ReductionInputGroupSchema, ...], + fingerprint: str, + ) -> "ReductionContext": + """Create a context carrying a weak reference to its owner Cell.""" + return cls( + pop_size=tuple(cell.pop_size), + synapses=synapses, + input_groups=input_groups, + fingerprint=fingerprint, + _cell_ref=weakref.ref(cell), + ) + + @property + def cell(self): + """Return the owning Cell while it remains alive.""" + cell = self._cell_ref() + if cell is None: + raise RuntimeError("The Cell owning this ReductionContext no longer exists.") + return cell + + @property + def population_size(self) -> int: + """Return the flattened number of Cell population members.""" + return int(np.prod(self.pop_size, dtype=np.int64)) if self.pop_size else 1 + + +class ReductionModel(ABC): + """Define the minimal lifecycle implemented by a Cell reduction model.""" + + @abstractmethod + def init_state(self, context: ReductionContext, batch_size=None) -> ReductionOutput: + """Initialize model state for one current Cell context.""" + + @abstractmethod + def update(self, inputs: ReductionInputs) -> ReductionOutput: + """Consume current synapse payloads and advance the model once.""" + + @abstractmethod + def reset_state(self, batch_size=None) -> ReductionOutput: + """Reset dynamic state and return the model's initial output.""" + + def reset(self) -> None: + """Drop optional initialized caches while retaining model parameters.""" + + def get(self, field: str, population_indices: tuple[int, ...]): + """Return one optional population parameter for selected members.""" + raise NotImplementedError(f"{type(self).__name__} does not expose population parameters.") + + def set(self, population_indices: tuple[int, ...], **parameters) -> None: + """Set optional population parameters for selected members.""" + raise NotImplementedError(f"{type(self).__name__} does not expose population parameters.") + + +class ReductionView: + """Expose one named reduction model over selected population members.""" + + __slots__ = ("_cell", "_name", "_population_indices") + + def __init__(self, cell, name: str, population_indices: tuple[int, ...]) -> None: + self._cell = cell + self._name = name + self._population_indices = tuple(int(item) for item in population_indices) + + @property + def name(self) -> str: + """Return the Cell-local registered model name.""" + return self._name + + @property + def model(self) -> ReductionModel: + """Return the registered model object.""" + return self._cell._reduction_models[self._name] + + @property + def population_indices(self) -> tuple[int, ...]: + """Return selected root-population indices.""" + return self._population_indices + + @property + def is_selected(self) -> bool: + """Return whether this is the root Cell's selected execution model.""" + return self._cell._selected_model_name == self._name + + def get(self, field: str): + """Read a model-defined population parameter over this view.""" + if not isinstance(field, str) or not field: + raise ValueError("Reduction parameter name must be a non-empty string.") + return self.model.get(field, self._population_indices) + + def set(self, **parameters) -> "ReductionView": + """Set model-defined population parameters over this view.""" + self._cell._raise_if_initialized("set reduction parameters") + if not parameters: + return self + self.model.set(self._population_indices, **parameters) + self._cell._run_loop_cache.clear() + return self + + def __repr__(self) -> str: + return ( + f"ReductionView(name={self.name!r}, model={type(self.model).__name__}, " + f"population_indices={self.population_indices!r}, selected={self.is_selected!r})" + ) + + +class ReductionViewCollection(Mapping[str, ReductionView]): + """Provide Mapping-style access to a Cell's registered reductions.""" + + __slots__ = ("_cell", "_population_indices") + + def __init__(self, cell, population_indices: tuple[int, ...]) -> None: + self._cell = cell + self._population_indices = tuple(int(item) for item in population_indices) + + def __getitem__(self, name: str) -> ReductionView: + if name not in self._cell._reduction_models: + raise KeyError( + f"Unknown reduction model {name!r}; available models: {tuple(self._cell._reduction_models)!r}." + ) + return ReductionView(self._cell, name, self._population_indices) + + def __iter__(self) -> Iterator[str]: + return iter(self._cell._reduction_models) + + def __len__(self) -> int: + return len(self._cell._reduction_models) + + def __repr__(self) -> str: + return f"ReductionViewCollection(names={tuple(self)!r}, population_indices={self._population_indices!r})" diff --git a/braincell/reduction/runtime.py b/braincell/reduction/runtime.py new file mode 100644 index 00000000..29872d1f --- /dev/null +++ b/braincell/reduction/runtime.py @@ -0,0 +1,181 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Lightweight packed synapse-input runtime for reduced Cells.""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib + +import brainstate +import brainunit as u +import numpy as np + +from braincell.mech import NoEventInput, ScalarEventInput, TriggerEventInput, get_registry +from braincell.reduction.core import ( + ReductionContext, + ReductionInputGroup, + ReductionInputGroupSchema, + ReductionInputs, + ReductionSynapse, +) + +__all__ = ["ReductionInputLayout", "ReductionInputRuntime", "build_reduction_input_runtime"] + + +@dataclass(frozen=True) +class ReductionInputLayout: + """Describe one packed event-buffer layout needed by connection lowering.""" + + id: int + kind: str + n_active: int + placement_index: np.ndarray + synapse_index: np.ndarray + schema: ReductionInputGroupSchema + + +@dataclass +class ReductionInputRuntime: + """Own only the event buffers and schema required by a reduced Cell.""" + + layouts: tuple[ReductionInputLayout, ...] + event_buffers: dict[int, brainstate.State] + context: ReductionContext + + def get_event_buffer(self, layout_id: int): + """Return the current payload for one input layout.""" + return self.event_buffers[int(layout_id)].value + + def clear_event_buffer(self, layout_id: int) -> None: + """Set one input buffer to zero without changing its unit or dtype.""" + state = self.event_buffers[int(layout_id)] + state.value = u.math.zeros_like(state.value) + + def clear_event_buffers(self) -> None: + """Clear every packed input buffer.""" + for layout_id in self.event_buffers: + self.clear_event_buffer(layout_id) + + def take_inputs(self) -> ReductionInputs: + """Snapshot all current payloads and clear their backing buffers.""" + groups = tuple( + ReductionInputGroup(layout.schema, self.get_event_buffer(layout.id)) + for layout in self.layouts + if layout.id in self.event_buffers + ) + self.clear_event_buffers() + return ReductionInputs(groups) + + +def build_reduction_input_runtime(cell) -> ReductionInputRuntime: + """Build a packed input-only runtime from the Cell's current synapses.""" + store = cell._get_synapse_store() + population_local_index = _population_local_indices(store.population_index) + layouts = [] + event_buffers = {} + group_schemas = [] + + for layout_id, raw_type in enumerate(dict.fromkeys(store.synapse_type.tolist())): + synapse_type = str(raw_type) + logical_ids = store.id[store.synapse_type == synapse_type] + rows = store.row_indices(logical_ids) + runtime_cls = get_registry().get("synapse", synapse_type) + event_input = runtime_cls.event_input + schema = ReductionInputGroupSchema( + layout_id=layout_id, + synapse_type=synapse_type, + event_input=event_input, + synapse_id=logical_ids, + synapse_index=population_local_index[rows], + population_index=store.population_index[rows], + ) + layout = ReductionInputLayout( + id=layout_id, + kind=f"synapse:{synapse_type}", + n_active=schema.size, + placement_index=np.asarray(store.placement_id[rows], dtype=np.int64), + synapse_index=np.asarray(logical_ids, dtype=np.int64), + schema=schema, + ) + layouts.append(layout) + group_schemas.append(schema) + if isinstance(event_input, ScalarEventInput): + zero = u.Quantity(np.zeros((schema.size,), dtype=float), event_input.unit) + elif isinstance(event_input, TriggerEventInput): + zero = np.zeros((schema.size,), dtype=np.int32) + elif isinstance(event_input, NoEventInput): + continue + else: + raise TypeError(f"Unsupported event input {type(event_input).__name__!r} for {synapse_type!r}.") + event_buffers[layout_id] = brainstate.ShortTermState(zero) + store.bind_runtime(synapse_type, layout_id, logical_ids) + + synapses = tuple(_synapse_record(store, row, int(population_local_index[row])) for row in range(len(store.id))) + signature = tuple( + ( + item.population_index, + item.synapse_index, + item.point_id, + item.name, + item.synapse_type, + tuple((name, repr(value)) for name, value in item.parameters.items()), + ) + for item in synapses + ) + fingerprint = hashlib.sha256(repr(signature).encode("utf-8")).hexdigest() + context = ReductionContext.with_cell( + cell, + synapses=synapses, + input_groups=tuple(group_schemas), + fingerprint=fingerprint, + ) + return ReductionInputRuntime(tuple(layouts), event_buffers, context) + + +def _population_local_indices(population_index: np.ndarray) -> np.ndarray: + counters = {} + result = np.empty(len(population_index), dtype=np.int64) + for row, raw_owner in enumerate(np.asarray(population_index, dtype=np.int64).tolist()): + owner = int(raw_owner) + result[row] = counters.get(owner, 0) + counters[owner] = int(result[row]) + 1 + return result + + +def _synapse_record(store, row: int, synapse_index: int) -> ReductionSynapse: + synapse_type = str(store.synapse_type[row]) + parameters = { + name: _take_one(value, store._type_local_by_id[int(store.id[row])]) + for name, value in store.parameter_columns[synapse_type].items() + } + return ReductionSynapse( + id=int(store.id[row]), + synapse_index=synapse_index, + population_index=int(store.population_index[row]), + placement_id=int(store.placement_id[row]), + point_id=int(store.point_id[row]), + cv_id=int(store.cv_id[row]), + branch_id=int(store.branch_id[row]), + branch_x=float(store.branch_x[row]), + name=str(store.name[row]), + synapse_type=synapse_type, + parameters=parameters, + ) + + +def _take_one(value, index: int): + return value[int(index)] diff --git a/braincell/reduction/runtime_test.py b/braincell/reduction/runtime_test.py new file mode 100644 index 00000000..18f42050 --- /dev/null +++ b/braincell/reduction/runtime_test.py @@ -0,0 +1,364 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import unittest + +import brainstate +import brainunit as u +import jax.numpy as jnp +import numpy as np + +import braincell +from braincell._multi_compartment.selection_test import _cell +from braincell.filter import at + + +class _TickReduction(braincell.ReductionModel): + def __init__(self, *, emit=False, features=1) -> None: + self.emit = emit + self.features = features + self.state = None + self.context = None + + def init_state(self, context, batch_size=None): + self.context = context + shape = ((batch_size,) if batch_size is not None else ()) + context.pop_size + self.state = brainstate.ShortTermState(jnp.zeros(shape)) + return self._output() + + def update(self, inputs): + del inputs + self.state.value = self.state.value + 1 + return self._output() + + def reset_state(self, batch_size=None): + shape = ((batch_size,) if batch_size is not None else ()) + self.context.pop_size + self.state.value = jnp.zeros(shape) + return self._output() + + def reset(self): + self.state = None + self.context = None + + def _output(self): + value = self.state.value + if self.features > 1: + value = jnp.stack(tuple(value + index for index in range(self.features)), axis=-1) + event = ( + jnp.ones_like(self.state.value, dtype=jnp.int32) + if self.emit + else jnp.zeros_like(self.state.value, dtype=jnp.int32) + ) + return braincell.ReductionOutput({"value": value}, event) + + +class ReductionRuntimeTest(unittest.TestCase): + def test_detailed_cell_exposes_full_voltage_output(self) -> None: + cell = _cell(2) + cell.init_state() + + self.assertEqual(tuple(cell.outputs), ("voltage",)) + self.assertEqual(cell.outputs["voltage"].shape, cell.V.value.shape) + self.assertEqual(cell[1].outputs["voltage"].shape, (1, cell.n_cv)) + + def test_registration_selection_and_population_parameter_views(self) -> None: + cell = _cell(3) + view = cell.add_reduction("acc", braincell.EventAccumulatorReduction(alpha=0.5, threshold=2.0)) + + self.assertFalse(view.is_selected) + cell[1:].reductions["acc"].set(threshold=[3.0, 4.0]) + np.testing.assert_allclose(cell.reductions["acc"].get("threshold"), [2.0, 3.0, 4.0]) + self.assertIs(cell.use_model("acc"), cell) + self.assertTrue(view.is_selected) + self.assertEqual(cell.outputs, {}) + + with self.assertRaisesRegex(ValueError, "reserved"): + cell.add_reduction("detailed", _TickReduction()) + + def test_reduced_init_is_lightweight_and_outputs_support_features(self) -> None: + cell = _cell(2) + cell.add_reduction("tick", _TickReduction(features=2)) + cell.use_model("tick") + cell.init_state() + + self.assertIsNone(cell._runtime) + self.assertFalse(hasattr(cell, "V")) + self.assertEqual(cell.outputs["value"].shape, (2, 2)) + self.assertEqual(cell[1].outputs["value"].shape, (1, 2)) + with self.assertRaisesRegex(RuntimeError, "Detailed Cell runtime"): + _ = cell.runtime + + def test_batched_feature_outputs_preserve_batch_and_flatten_recording_rows(self) -> None: + cell = _cell(2) + cell.add_reduction("tick", _TickReduction(features=2)) + cell.use_model("tick") + cell.record("features", braincell.observe.output("value")) + cell.init_state(batch_size=3) + + self.assertEqual(cell.outputs["value"].shape, (3, 2, 2)) + self.assertEqual(cell[1].outputs["value"].shape, (3, 1, 2)) + result = cell.run(dt=0.1 * u.ms, duration=0.2 * u.ms) + self.assertEqual(result.samples["features"].values.shape, (2, 3, 4)) + + def test_output_recording_is_post_update_and_detailed_recording_is_omitted(self) -> None: + cell = _cell(2) + cell.soma.record("old_v", braincell.observe.state("v")) + cell[1].record("reduced", braincell.observe.output("value")) + cell.add_reduction("tick", _TickReduction(features=2)) + cell.use_model("tick") + + result = cell.run(dt=0.1 * u.ms, duration=0.2 * u.ms) + + self.assertEqual(tuple(result.samples), ("reduced",)) + np.testing.assert_allclose(result.samples["reduced"].values, [[1.0, 2.0], [2.0, 3.0]]) + rows = result.samples["reduced"].schema.rows + self.assertEqual([(row.population_index, row.output_index) for row in rows], [(1, (0,)), (1, (1,))]) + + cell.reset() + cell.use_model() + detailed = cell.run(dt=0.1 * u.ms, duration=0.1 * u.ms) + self.assertEqual(tuple(detailed.samples), ("old_v",)) + self.assertIn("voltage", cell.outputs) + + def test_reset_state_keeps_mode_and_reset_allows_new_synapses(self) -> None: + cell = _cell(1) + model = _TickReduction() + cell.add_reduction("tick", model) + cell.use_model("tick") + cell.run(dt=0.1 * u.ms, duration=0.1 * u.ms) + cell.reset_state() + + self.assertEqual(cell._selected_model_name, "tick") + np.testing.assert_allclose(cell.outputs["value"], [0.0]) + + cell.reset() + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="later")) + cell.init_state() + self.assertEqual(len(model.context.synapses), 1) + + def test_context_preserves_heterogeneous_member_local_synapse_indices(self) -> None: + cell = _cell(2) + cell[0].place(at("soma", 0.25), braincell.mech.Synapse("ExpSyn", name="first")) + cell[1].place( + braincell.filter.LocsetMask.from_columns([0, 0], [0.25, 0.75]), + braincell.mech.Synapse("ExpSyn", name="second"), + ) + model = _TickReduction() + cell.add_reduction("tick", model) + cell.use_model("tick") + cell.init_state() + + self.assertEqual(len(model.context.synapses), 3) + self.assertEqual( + [(item.population_index, item.synapse_index) for item in model.context.synapses], + [(0, 0), (1, 0), (1, 1)], + ) + + def test_quantity_threshold_parameters_are_population_specific(self) -> None: + cell = _cell(2) + model = braincell.PayloadAccumulatorReduction(threshold=0.2 * u.uS) + cell.add_reduction("payload", model) + cell[1].reductions["payload"].set(threshold=500.0 * u.nS) + + np.testing.assert_allclose( + cell.reductions["payload"].get("threshold").to_decimal(u.uS), + [0.2, 0.5], + ) + + def test_payload_accumulator_preserves_converged_event_magnitude(self) -> None: + network = braincell.Network("payload") + source = network.add_population("source", braincell.NetStim(size=2, start=0.0 * u.ms, number=1)) + cell = _cell(1) + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="input")) + cell.add_reduction("payload", braincell.PayloadAccumulatorReduction(threshold=0.4 * u.uS)) + cell.use_model("payload") + cell.record("value", braincell.observe.output("value")) + target = network.add_population("target", cell) + network.connect( + "drive", + source=source, + synapse=target.synapses["input"][[0, 0]], + weight=[0.2, 0.3] * u.uS, + ) + + result = network.run(dt=0.1 * u.ms, duration=0.2 * u.ms) + + np.testing.assert_allclose(result.samples["target"]["value"].values.to_decimal(u.uS), [[0.5], [0.0]]) + np.testing.assert_array_equal(result.events["target"]["spike"].source_id, [0]) + + def test_quantity_accumulators_validate_threshold_and_input_units(self) -> None: + with self.assertRaisesRegex(TypeError, "scalar quantity"): + braincell.PayloadAccumulatorReduction(threshold=0.5) + with self.assertRaisesRegex(ValueError, "compatible with"): + braincell.PayloadAccumulatorReduction(threshold=0.5 * u.mV) + with self.assertRaisesRegex(ValueError, "compatible with"): + braincell.SynapticKernelAccumulatorReduction(threshold=0.5 * u.uS) + + class Owner: + pop_size = (1,) + + owner = Owner() + schema = braincell.ReductionInputGroupSchema( + layout_id=0, + synapse_type="TriggerOnly", + event_input=braincell.mech.TriggerEventInput(), + synapse_id=np.asarray([0]), + synapse_index=np.asarray([0]), + population_index=np.asarray([0]), + ) + context = braincell.ReductionContext.with_cell( + owner, + synapses=(), + input_groups=(schema,), + fingerprint="trigger", + ) + with self.assertRaisesRegex(TypeError, "requires ScalarEventInput"): + braincell.PayloadAccumulatorReduction().init_state(context) + + def test_synaptic_kernel_rejects_types_without_an_area_rule(self) -> None: + class Owner: + pop_size = (1,) + + owner = Owner() + synapse = braincell.ReductionSynapse( + id=0, + synapse_index=0, + population_index=0, + placement_id=0, + point_id=0, + cv_id=0, + branch_id=0, + branch_x=0.5, + name="custom", + synapse_type="CustomSynapse", + ) + schema = braincell.ReductionInputGroupSchema( + layout_id=0, + synapse_type="CustomSynapse", + event_input=braincell.mech.ScalarEventInput(u.uS), + synapse_id=np.asarray([0]), + synapse_index=np.asarray([0]), + population_index=np.asarray([0]), + ) + context = braincell.ReductionContext.with_cell( + owner, + synapses=(synapse,), + input_groups=(schema,), + fingerprint="custom", + ) + + with self.assertRaisesRegex(TypeError, "supports only ExpSyn and Exp2Syn"): + braincell.SynapticKernelAccumulatorReduction().init_state(context) + + def test_synaptic_kernel_uses_type_specific_analytic_area(self) -> None: + network = braincell.Network("kernel") + source = network.add_population("source", braincell.NetStim(size=2, start=0.0 * u.ms, number=1)) + cell = _cell(2) + cell[0].place( + at("soma", 0.5), + braincell.mech.Synapse("ExpSyn", name="exp", tau=2.0 * u.ms), + ) + cell[1].place( + at("soma", 0.5), + braincell.mech.Synapse( + "Exp2Syn", + name="exp2", + tau1=0.5 * u.ms, + tau2=5.0 * u.ms, + ), + ) + model = braincell.SynapticKernelAccumulatorReduction(threshold=10.0 * u.uS * u.ms) + cell.add_reduction("kernel", model) + cell.use_model("kernel") + cell.record("value", braincell.observe.output("value")) + target = network.add_population("target", cell) + network.connect( + "drive_exp", + source=source.event_outputs["spike"][0], + synapse=target.synapses["exp"], + weight=0.2 * u.uS, + ) + network.connect( + "drive_exp2", + source=source.event_outputs["spike"][1], + synapse=target.synapses["exp2"], + weight=0.2 * u.uS, + ) + + result = network.run(dt=0.1 * u.ms, duration=0.2 * u.ms) + + tau1 = 0.5 + tau2 = 5.0 + t_peak = tau1 * tau2 / (tau2 - tau1) * np.log(tau2 / tau1) + factor = 1.0 / (np.exp(-t_peak / tau2) - np.exp(-t_peak / tau1)) + expected = [0.2 * 2.0, 0.2 * factor * (tau2 - tau1)] + np.testing.assert_allclose( + result.samples["target"]["value"].values[0].to_decimal(u.uS * u.ms), + expected, + rtol=1e-6, + ) + self.assertIsNone(cell._runtime) + + def test_scheduled_source_reaches_packed_inputs_and_emits_canonical_events(self) -> None: + network = braincell.Network("scheduled") + source = network.add_population("source", braincell.NetStim(size=2, start=0.0 * u.ms, number=1)) + cell = _cell(2) + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="input")) + model = braincell.EventAccumulatorReduction(threshold=0.5) + cell.add_reduction("acc", model) + cell.use_model("acc") + cell.record("value", braincell.observe.output("value")) + target = network.add_population("target", cell) + network.connect("drive", source=source, synapse=target.synapses["input"], weight=0.2 * u.uS) + + result = network.run(dt=0.1 * u.ms, duration=0.2 * u.ms) + + np.testing.assert_allclose(result.samples["target"]["value"].values, [[1.0, 1.0], [0.0, 0.0]]) + np.testing.assert_array_equal(result.events["target"]["spike"].source_id, [0, 1]) + self.assertEqual(len(model._context.input_groups), 1) + np.testing.assert_array_equal(model._context.input_groups[0].population_index, [0, 1]) + + def test_live_zero_delay_event_is_consumed_on_following_step(self) -> None: + network = braincell.Network("live") + pre_cell = _cell(1) + pre_cell.add_reduction("tick", _TickReduction(emit=True)) + pre_cell.use_model("tick") + pre = network.add_population("pre", pre_cell) + + post_cell = _cell(1) + post_cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="input")) + post_cell.add_reduction("acc", braincell.EventAccumulatorReduction(threshold=10.0)) + post_cell.use_model("acc") + post_cell.record("value", braincell.observe.output("value")) + post = network.add_population("post", post_cell) + network.connect("drive", source=pre.event_outputs["spike"], synapse=post.synapses["input"]) + + result = network.run(dt=0.1 * u.ms, duration=0.3 * u.ms) + + np.testing.assert_allclose(result.samples["post"]["value"].values[:, 0], [0.0, 1.0, 1.0]) + + def test_voltage_crossing_source_is_rejected_in_reduced_mode(self) -> None: + cell = _cell(1) + source = braincell.VoltageCrossingSource(cell.soma) + cell.add_reduction("tick", _TickReduction()) + cell.use_model("tick") + cell.init_state() + + with self.assertRaisesRegex(RuntimeError, "unavailable"): + source.current_event_count([0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/braincell/reduction/toy.py b/braincell/reduction/toy.py new file mode 100644 index 00000000..791088de --- /dev/null +++ b/braincell/reduction/toy.py @@ -0,0 +1,369 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Small reference reduction models exercising the public runtime contract.""" + +from __future__ import annotations + +from abc import abstractmethod + +import brainstate +import brainunit as u +import jax.numpy as jnp +import numpy as np + +from braincell.mech import ScalarEventInput +from braincell.reduction.core import ReductionContext, ReductionInputs, ReductionModel, ReductionOutput + +__all__ = [ + "EventAccumulatorReduction", + "PayloadAccumulatorReduction", + "SynapticKernelAccumulatorReduction", +] + + +class _PopulationParameters: + """Store scalar defaults plus sparse declaration-time population overrides.""" + + def __init__(self, **defaults) -> None: + self.defaults = dict(defaults) + self.overrides = {name: {} for name in defaults} + + def set(self, population_indices, **parameters) -> None: + unknown = set(parameters).difference(self.defaults) + if unknown: + raise KeyError(f"Unknown reduction parameters: {tuple(sorted(unknown))!r}.") + for name, value in parameters.items(): + for index, item in zip(population_indices, _selected_values(value, len(population_indices), name=name)): + self.overrides[name][int(index)] = item + + def get(self, field: str, population_indices): + if field not in self.defaults: + raise KeyError(f"Unknown reduction parameter {field!r}.") + values = [self.overrides[field].get(int(index), self.defaults[field]) for index in population_indices] + default = self.defaults[field] + if isinstance(default, u.Quantity): + unit = u.get_unit(default) + return u.Quantity(np.asarray([value.to_decimal(unit) for value in values]), unit) + return np.asarray(values) + + def materialize(self, field: str, population_size: int): + values = self.get(field, tuple(range(population_size))) + if isinstance(values, u.Quantity): + return u.Quantity(jnp.asarray(u.get_mantissa(values)), u.get_unit(values)) + return jnp.asarray(values) + + +class _AccumulatorReduction(ReductionModel): + """Share decay, threshold, reset, and population-parameter behavior.""" + + def __init__(self, *, alpha: float, threshold) -> None: + self._validate_alpha(alpha) + self._validate_threshold(threshold) + self._parameters = _PopulationParameters(alpha=float(alpha), threshold=threshold) + self._state = None + self._context = None + + def init_state(self, context: ReductionContext, batch_size=None) -> ReductionOutput: + self._prepare_context(context) + self._context = context + shape = _runtime_shape(context.pop_size, batch_size) + self._state = brainstate.ShortTermState(self._initial_value(shape)) + self._materialize_parameters() + return self._zero_output() + + def update(self, inputs: ReductionInputs) -> ReductionOutput: + self._require_initialized() + candidate = self._alpha * self._state.value + self._drive(inputs) + event = candidate > self._threshold + self._state.value = u.math.where(event, u.math.zeros_like(candidate), candidate) + return ReductionOutput(values={"value": candidate}, event=event.astype(jnp.int32)) + + def reset_state(self, batch_size=None) -> ReductionOutput: + self._require_initialized() + shape = _runtime_shape(self._context.pop_size, batch_size) + self._state.value = self._initial_value(shape) + self._materialize_parameters() + return self._zero_output() + + def reset(self) -> None: + self._state = None + self._context = None + self._clear_context() + for name in ("_alpha", "_threshold"): + if hasattr(self, name): + delattr(self, name) + + def get(self, field: str, population_indices: tuple[int, ...]): + return self._parameters.get(field, population_indices) + + def set(self, population_indices: tuple[int, ...], **parameters) -> None: + if "alpha" in parameters: + for value in _selected_values(parameters["alpha"], len(population_indices), name="alpha"): + self._validate_alpha(value) + if "threshold" in parameters: + for value in _selected_values(parameters["threshold"], len(population_indices), name="threshold"): + self._validate_threshold(value) + self._parameters.set(population_indices, **parameters) + + def _materialize_parameters(self) -> None: + population_size = self._context.population_size + self._alpha = self._parameters.materialize("alpha", population_size) + self._threshold = self._parameters.materialize("threshold", population_size) + + def _zero_output(self) -> ReductionOutput: + zero = u.math.zeros_like(self._state.value) + event = jnp.zeros_like(u.get_mantissa(zero), dtype=jnp.int32) + return ReductionOutput(values={"value": zero}, event=event) + + def _require_initialized(self) -> None: + if self._state is None or self._context is None: + raise RuntimeError(f"{type(self).__name__} requires init_state() first.") + + def _prepare_context(self, context: ReductionContext) -> None: + del context + + def _clear_context(self) -> None: + pass + + @abstractmethod + def _initial_value(self, shape: tuple[int, ...]): + raise NotImplementedError + + @abstractmethod + def _drive(self, inputs: ReductionInputs): + raise NotImplementedError + + def _validate_alpha(self, value) -> None: + _validate_alpha(value, owner=type(self).__name__) + + @abstractmethod + def _validate_threshold(self, value) -> None: + raise NotImplementedError + + +class EventAccumulatorReduction(_AccumulatorReduction): + """Accumulate nonzero synapse slots with decay and threshold-reset events.""" + + def __init__(self, *, alpha: float = 0.0, threshold: float = 1.0) -> None: + super().__init__(alpha=alpha, threshold=threshold) + + def _initial_value(self, shape: tuple[int, ...]): + return jnp.zeros(shape, dtype=float) + + def _drive(self, inputs: ReductionInputs): + return _active_synapse_count(inputs, self._state.value) + + def _validate_threshold(self, value) -> None: + _validate_numeric_threshold(value, owner=type(self).__name__) + + +class PayloadAccumulatorReduction(_AccumulatorReduction): + """Accumulate scalar synapse payloads while preserving their physical unit.""" + + def __init__(self, *, alpha: float = 0.0, threshold=1.0 * u.uS) -> None: + super().__init__(alpha=alpha, threshold=threshold) + + def _prepare_context(self, context: ReductionContext) -> None: + threshold_unit = u.get_unit(self._parameters.defaults["threshold"]) + for group in context.input_groups: + event_input = group.event_input + if not isinstance(event_input, ScalarEventInput): + raise TypeError( + f"{type(self).__name__} requires ScalarEventInput groups; " + f"synapse type {group.synapse_type!r} declares {type(event_input).__name__}." + ) + if u.get_dim(event_input.unit) != u.get_dim(threshold_unit): + raise ValueError( + f"{type(self).__name__} threshold units are incompatible with " + f"synapse type {group.synapse_type!r} payload units {event_input.unit}." + ) + + def _initial_value(self, shape: tuple[int, ...]): + threshold = self._parameters.defaults["threshold"] + return u.Quantity(jnp.zeros(shape, dtype=float), u.get_unit(threshold)) + + def _drive(self, inputs: ReductionInputs): + unit = u.get_unit(self._state.value) + return _sum_rows_by_population( + inputs, + template=u.get_mantissa(self._state.value), + row_values=lambda group: group.payload.to_decimal(unit), + unit=unit, + ) + + def _validate_threshold(self, value) -> None: + _validate_quantity_threshold( + value, + owner=type(self).__name__, + expected_unit=u.uS, + ) + + +class SynapticKernelAccumulatorReduction(_AccumulatorReduction): + """Accumulate type-aware analytic synaptic-kernel areas without synapse state.""" + + def __init__(self, *, alpha: float = 0.0, threshold=1.0 * u.uS * u.ms) -> None: + super().__init__(alpha=alpha, threshold=threshold) + self._kernel_area_ms = {} + + def _prepare_context(self, context: ReductionContext) -> None: + threshold_unit = u.get_unit(self._parameters.defaults["threshold"]) + synapses_by_id = {synapse.id: synapse for synapse in context.synapses} + kernel_area_ms = {} + for group in context.input_groups: + event_input = group.event_input + if not isinstance(event_input, ScalarEventInput): + raise TypeError( + f"{type(self).__name__} requires ScalarEventInput groups; " + f"synapse type {group.synapse_type!r} declares {type(event_input).__name__}." + ) + if u.get_dim(event_input.unit * u.ms) != u.get_dim(threshold_unit): + raise ValueError( + f"{type(self).__name__} threshold units are incompatible with " + f"synapse type {group.synapse_type!r} kernel-area units {event_input.unit * u.ms}." + ) + kernel_area_ms[group.layout_id] = jnp.asarray( + [_synaptic_kernel_area_ms(synapses_by_id[int(synapse_id)]) for synapse_id in group.synapse_id], + dtype=float, + ) + self._kernel_area_ms = kernel_area_ms + + def _clear_context(self) -> None: + self._kernel_area_ms = {} + + def _initial_value(self, shape: tuple[int, ...]): + threshold = self._parameters.defaults["threshold"] + return u.Quantity(jnp.zeros(shape, dtype=float), u.get_unit(threshold)) + + def _drive(self, inputs: ReductionInputs): + unit = u.get_unit(self._state.value) + + def kernel_area_payload(group): + area = self._kernel_area_ms[group.schema.layout_id] * u.ms + return (group.payload * area).to_decimal(unit) + + return _sum_rows_by_population( + inputs, + template=u.get_mantissa(self._state.value), + row_values=kernel_area_payload, + unit=unit, + ) + + def _validate_threshold(self, value) -> None: + _validate_quantity_threshold( + value, + owner=type(self).__name__, + expected_unit=u.uS * u.ms, + ) + + +def _active_synapse_count(inputs: ReductionInputs, template): + result = jnp.zeros_like(template) + for group in inputs: + payload = u.get_magnitude(group.payload) + active = jnp.asarray(payload != 0, dtype=result.dtype) + target_shape = tuple(result.shape[:-1]) + (group.schema.size,) + active = jnp.broadcast_to(active, target_shape) + result = result.at[..., jnp.asarray(group.schema.population_index)].add(active) + return result + + +def _sum_rows_by_population(inputs: ReductionInputs, *, template, row_values, unit): + result = jnp.zeros_like(template) + for group in inputs: + values = jnp.asarray(row_values(group), dtype=result.dtype) + target_shape = tuple(result.shape[:-1]) + (group.schema.size,) + values = jnp.broadcast_to(values, target_shape) + result = result.at[..., jnp.asarray(group.schema.population_index)].add(values) + return u.Quantity(result, unit) + + +def _synaptic_kernel_area_ms(synapse) -> float: + if synapse.synapse_type == "ExpSyn": + return _time_parameter_ms(synapse, "tau") + if synapse.synapse_type == "Exp2Syn": + tau1 = _time_parameter_ms(synapse, "tau1") + tau2 = _time_parameter_ms(synapse, "tau2") + t_peak = tau1 * tau2 / (tau2 - tau1) * np.log(tau2 / tau1) + factor = 1.0 / (np.exp(-t_peak / tau2) - np.exp(-t_peak / tau1)) + return float(factor * (tau2 - tau1)) + raise TypeError( + "SynapticKernelAccumulatorReduction supports only ExpSyn and Exp2Syn; " + f"got synapse type {synapse.synapse_type!r}." + ) + + +def _time_parameter_ms(synapse, name: str) -> float: + try: + value = synapse.parameters[name] + except KeyError as exc: + raise ValueError(f"Synapse type {synapse.synapse_type!r} is missing required parameter {name!r}.") from exc + try: + result = float(value.to_decimal(u.ms)) + except Exception as exc: + raise ValueError(f"Synapse type {synapse.synapse_type!r} parameter {name!r} must have time units.") from exc + if not np.isfinite(result) or result <= 0.0: + raise ValueError(f"Synapse type {synapse.synapse_type!r} parameter {name!r} must be finite and > 0.") + return result + + +def _runtime_shape(pop_size: tuple[int, ...], batch_size) -> tuple[int, ...]: + if len(pop_size) > 1: + raise NotImplementedError("Reference reduction models currently support scalar or one-dimensional pop_size.") + population_shape = pop_size if pop_size else (1,) + return ((int(batch_size),) if batch_size is not None else ()) + population_shape + + +def _selected_values(value, count: int, *, name: str): + if isinstance(value, u.Quantity): + shape = tuple(value.shape) + if shape == (): + return tuple(value for _ in range(count)) + if shape != (count,): + raise ValueError(f"Reduction parameter {name!r} must be scalar or shape {(count,)!r}, got {shape!r}.") + return tuple(value[index] for index in range(count)) + array = np.asarray(value) + if array.shape == (): + return tuple(array.item() for _ in range(count)) + if array.shape != (count,): + raise ValueError(f"Reduction parameter {name!r} must be scalar or shape {(count,)!r}, got {array.shape!r}.") + return tuple(array.tolist()) + + +def _validate_alpha(value, *, owner: str) -> None: + if isinstance(value, bool) or not np.isscalar(value) or not np.isfinite(value): + raise TypeError(f"{owner}.alpha must be a finite scalar.") + if float(value) < 0.0 or float(value) > 1.0: + raise ValueError(f"{owner}.alpha must be between 0 and 1 inclusive.") + + +def _validate_numeric_threshold(value, *, owner: str) -> None: + if isinstance(value, bool) or not np.isscalar(value) or not np.isfinite(value): + raise TypeError(f"{owner}.threshold must be a finite scalar.") + if float(value) < 0.0: + raise ValueError(f"{owner}.threshold must be >= 0.") + + +def _validate_quantity_threshold(value, *, owner: str, expected_unit=None) -> None: + if not isinstance(value, u.Quantity) or tuple(value.shape) != (): + raise TypeError(f"{owner}.threshold must be a scalar quantity.") + if expected_unit is not None and u.get_dim(value) != u.get_dim(expected_unit): + raise ValueError(f"{owner}.threshold must have units compatible with {expected_unit}.") + magnitude = np.asarray(u.get_mantissa(value)) + if not np.isfinite(magnitude).item(): + raise TypeError(f"{owner}.threshold must be finite.") + if float(magnitude) < 0.0: + raise ValueError(f"{owner}.threshold must be >= 0.") diff --git a/braincell/synapse/exponential.py b/braincell/synapse/exponential.py index 205025f9..0284bf57 100644 --- a/braincell/synapse/exponential.py +++ b/braincell/synapse/exponential.py @@ -25,8 +25,8 @@ from braincell._base_channel import Synapse from braincell._base_neuron import HHTypedNeuron +from braincell._typing import Initializer, Size from braincell.mech import ( - ParameterSpec, ScalarEventInput, StateSpec, positive, @@ -49,16 +49,32 @@ class ExpSyn(Synapse): - decay: ``g' = -g / tau`` - step-boundary event: ``g <- g + weighted_pre_drive`` - inward-positive point current: ``I = g * (e - V_post)`` + + Parameters + ---------- + size : brainstate.typing.Size + Packed synapse shape. + name : str or None, optional + Runtime node name. + tau : brainstate.typing.ArrayLike or callable, optional + Positive decay time constant, default 0.1 ms. + e : brainstate.typing.ArrayLike or callable, optional + Reversal voltage, default 0 mV. """ root_type = HHTypedNeuron - parameters = { - "tau": ParameterSpec(0.1 * u.ms, validator=positive), - "e": ParameterSpec(0.0 * u.mV), - } states = {"g": StateSpec(0.0 * u.uS)} event_input = ScalarEventInput(u.uS, aggregation="sum") + def __init__(self, size: Size, name: str | None = None, tau: Initializer = 0.1 * u.ms, e: Initializer = 0.0 * u.mV): + super().__init__(size=size, name=name) + self._init_parameters(tau=tau, e=e) + + @classmethod + def validate_parameter_values(cls, parameters): + """Require a positive decay time constant.""" + positive(parameters["tau"], "tau") + def apply_events(self, payload, V_post=None): _ = V_post self.event_input.validate_payload(payload) @@ -83,20 +99,39 @@ class Exp2Syn(Synapse): - conductance: ``g = B - A`` - inward-positive point current: ``I = g * (e - V_post)`` - step-boundary event: ``A <- A + weighted_pre_drive * factor`` and same for ``B`` + + Parameters + ---------- + size : brainstate.typing.Size + Packed synapse shape. + name : str or None, optional + Runtime node name. + tau1 : brainstate.typing.ArrayLike or callable, optional + Positive rise time constant, default 0.1 ms. + tau2 : brainstate.typing.ArrayLike or callable, optional + Decay time constant greater than tau1, default 10 ms. + e : brainstate.typing.ArrayLike or callable, optional + Reversal voltage, default 0 mV. """ root_type = HHTypedNeuron - parameters = { - "tau1": ParameterSpec(0.1 * u.ms, validator=positive), - "tau2": ParameterSpec(10.0 * u.ms, validator=positive), - "e": ParameterSpec(0.0 * u.mV), - } states = { "A": StateSpec(0.0 * u.uS), "B": StateSpec(0.0 * u.uS), } event_input = ScalarEventInput(u.uS, aggregation="sum") + def __init__( + self, + size: Size, + name: str | None = None, + tau1: Initializer = 0.1 * u.ms, + tau2: Initializer = 10.0 * u.ms, + e: Initializer = 0.0 * u.mV, + ): + super().__init__(size=size, name=name) + self._init_parameters(tau1=tau1, tau2=tau2, e=e) + @classmethod def validate_parameter_values(cls, parameters) -> None: """Require ``tau1 < tau2`` elementwise. @@ -115,6 +150,8 @@ def validate_parameter_values(cls, parameters) -> None: quantities, so that a caller mixing ``us`` and ``ms`` is ordered on physical duration rather than on raw magnitude. """ + positive(parameters["tau1"], "tau1") + positive(parameters["tau2"], "tau2") tau1 = np.asarray(u.Quantity(parameters["tau1"]).to_decimal(u.ms)) tau2 = np.asarray(u.Quantity(parameters["tau2"]).to_decimal(u.ms)) if np.any(tau1 >= tau2): diff --git a/braincell/synapse/exponential_test.py b/braincell/synapse/exponential_test.py index 4a36190f..c16b8b45 100644 --- a/braincell/synapse/exponential_test.py +++ b/braincell/synapse/exponential_test.py @@ -25,12 +25,15 @@ class SynapseSchemaTest(unittest.TestCase): - def test_expsyn_schema_is_the_only_parameter_source(self) -> None: + def test_expsyn_signature_is_the_only_parameter_source(self) -> None: + import inspect + model = braincell.synapse.ExpSyn - self.assertEqual(tuple(model.parameters), ("tau", "e")) + self.assertIn("tau", inspect.signature(model.__init__).parameters) + self.assertEqual(tuple(model.parameter_info()), ("tau", "e")) self.assertEqual(tuple(model.states), ("g",)) self.assertEqual(model.event_input, ScalarEventInput(u.uS, aggregation="sum")) - self.assertNotIn("weight", model.parameters) + self.assertNotIn("weight", model.parameter_info()) self.assertFalse(hasattr(model, "event_weight_unit")) self.assertFalse(hasattr(model, "current_sign")) self.assertFalse(hasattr(model, "current_units")) @@ -38,7 +41,7 @@ def test_expsyn_schema_is_the_only_parameter_source(self) -> None: def test_physical_validation_is_not_a_learning_transform(self) -> None: with self.assertRaisesRegex(ValueError, "must be > 0"): braincell.synapse.ExpSyn(1, tau=0.0 * u.ms) - field = braincell.synapse.ExpSyn.parameters["tau"] + field = braincell.synapse.ExpSyn.parameter_info()["tau"] self.assertFalse(hasattr(field, "trainable")) self.assertFalse(hasattr(field, "transform")) diff --git a/braincell/trainable/_manager.py b/braincell/trainable/_manager.py index c696fc02..42ea283f 100644 --- a/braincell/trainable/_manager.py +++ b/braincell/trainable/_manager.py @@ -44,6 +44,7 @@ class _TargetRow: population_index: int cv_id: int point_id: int + logical_id: int | None = None @dataclass(frozen=True) @@ -60,6 +61,7 @@ class ParameterBinding: baseline: object | None = None _rows: tuple[_TargetRow, ...] = field(default=(), repr=False) _evaluate: object = field(default=None, repr=False, compare=False) + _prepare_write: object = field(default=None, repr=False, compare=False) class TrainableManager(brainstate.nn.Module): @@ -91,10 +93,6 @@ def register(self, view, fields: dict[str, ParameterSource]) -> None: cell = self._cell() if cell._initialized: raise RuntimeError("trainable() must be called before Cell.init_state().") - if cell.network_owner is not None: - raise NotImplementedError( - "Trainable density parameters are Cell-local in P0 and cannot target a Network Cell." - ) if not view.rows: raise ValueError("trainable() requires a non-empty View.") owners = tuple(dict.fromkeys((row.mechanism_type, row.name) for row in view.rows)) @@ -110,6 +108,8 @@ def register(self, view, fields: dict[str, ParameterSource]) -> None: try: for target_field, source in fields.items(): self._register_one(view, target_field, source) + if hasattr(view, "_validate_bindings"): + view._validate_bindings(self._binding_list) except Exception: self.roots.clear() self.roots.update(roots_before) @@ -129,8 +129,15 @@ def materialize(self) -> None: evaluated = [(binding, binding._evaluate()) for binding in self._binding_list] pending: dict[tuple[int, str], tuple[RuntimeParameterState, object, str]] = {} + point_commits = [] for binding, values in evaluated: + if binding._prepare_write is not None: + if tuple(getattr(values, "shape", ())) != (len(binding._rows),): + raise ValueError(f"Binding {binding.name!r} must produce one value per logical row.") + point_commits.extend(binding._prepare_write(values)) + continue required_axis = _binding_axis(binding) + selection_axes = {} if tuple(getattr(values, "shape", ())) != (len(binding._rows),): raise ValueError( f"Binding {binding.name!r} returned shape {getattr(values, 'shape', ())!r}; " @@ -147,10 +154,17 @@ def materialize(self) -> None: ) pending[key] = (state, state.dense_value(), required_axis) state, full, pending_axis = pending[key] + # Equal initial values do not imply shared trainable ownership. + if layout.id not in selection_axes: + selected = np.zeros(state.full_shape, dtype=bool) + for selected_row in binding._rows: + if selected_row.cv_id in layout.source_cv_ids: + selected[selected_row.population_index, selected_row.cv_id] = True + selection_axes[layout.id] = _compact_axis(selected, state.point_mask) pending[key] = ( state, _set_row(full, row.population_index, row.cv_id, values[index]), - _join_axes(pending_axis, required_axis), + _join_axes(_join_axes(pending_axis, required_axis), selection_axes[layout.id]), ) commits = [] @@ -161,10 +175,18 @@ def materialize(self) -> None: else: axis = _join_axes(axis, required_axis) commits.append((key, state, axis, _project_axis(full, axis, state.point_mask))) + # Merge selections before committing so a later unit/shape error cannot + # leave a partially updated physical parameter vector. + point_pending = {} + for state, indices, values in point_commits: + current = point_pending.get(id(state), (state, state.value))[1] + point_pending[id(state)] = (state, _set_items(current, indices, values)) for key, state, axis, value in commits: state.value = value state.axis = axis self._target_axes[key] = axis + for state, value in point_pending.values(): + state.value = value if commits: from braincell._compute.bindings import _sync_runtime_node_param @@ -180,14 +202,11 @@ def _register_one(self, view, target_field: str, source: ParameterSource) -> Non raise ValueError("Trainable target field must be a non-empty string.") if not isinstance(source, (DirectSource, ScaleSource, ParameterizedSource)): raise TypeError(f"Field {target_field!r} expects a braincell.trainable parameter source.") - mechanism = view.rows[0].mechanism - schema = density_parameter_schema(mechanism) - if not schema: - raise NotImplementedError( - f"Channel {mechanism.class_name!r} has no trainable parameter schema in this release." - ) + point_target = hasattr(view, "_trainable_schema") + mechanism = None if point_target else view.rows[0].mechanism + schema = view._trainable_schema() if point_target else density_parameter_schema(mechanism) if target_field not in schema: - raise KeyError(f"Channel {mechanism.class_name!r} has no trainable parameter {target_field!r}.") + raise KeyError(f"{view.rows[0].mechanism_type!r} has no trainable parameter {target_field!r}.") rows = tuple( _TargetRow( @@ -197,15 +216,26 @@ def _register_one(self, view, target_field: str, source: ParameterSource) -> Non int(row.population_index), int(row.cv_id), int(row.point_id), + getattr(row, "logical_id", None), ) for row in view.rows ) - target_keys = {(row.category, row.owner, row.population_index, row.cv_id, target_field) for row in rows} + target_keys = { + ( + row.category, + row.owner, + row.population_index, + row.cv_id if row.logical_id is None else row.logical_id, + target_field, + ) + for row in rows + } overlap = target_keys.intersection(self._owned_targets) if overlap: raise ValueError(f"Trainable target rows are already bound: {tuple(sorted(overlap))!r}.") - current = tuple(view._row_value(source_row, target_field) for source_row in view.rows) + needs_current = isinstance(source, ScaleSource) or (isinstance(source, DirectSource) and source.initial is None) + current = tuple(view._row_value(source_row, target_field) for source_row in view.rows) if needs_current else () base_name = _base_name(rows, target_field, source) if isinstance(source, DirectSource): evaluate, root_names = self._prepare_direct(source, rows, current, base_name) @@ -227,18 +257,21 @@ def _register_one(self, view, target_field: str, source: ParameterSource) -> Non ) for index in range(len(rows)): schema[target_field].validate(sample[index], target_field) - unit = schema[target_field].default.unit if isinstance(schema[target_field].default, u.Quantity) else None + unit = sample.unit if isinstance(sample, u.Quantity) else None binding = ParameterBinding( name=base_name, target_owner=rows[0].owner, target_field=target_field, - row_keys=tuple((row.population_index, row.cv_id) for row in rows), + row_keys=tuple( + (row.population_index, row.cv_id if row.logical_id is None else row.logical_id) for row in rows + ), group_by=group_by, root_names=root_names, unit=unit, baseline=baseline, _rows=rows, _evaluate=evaluate, + _prepare_write=(lambda values: view._prepare_write(target_field, values)) if point_target else None, ) self._binding_list.append(binding) self._owned_targets.update(target_keys) @@ -350,18 +383,20 @@ def _cell(self): def _base_name(rows, field: str, source: ParameterSource) -> str: - fingerprint = hashlib.sha256(repr(tuple((row.population_index, row.cv_id) for row in rows)).encode()).hexdigest()[ - :8 - ] + fingerprint = hashlib.sha256( + repr( + tuple((row.population_index, row.cv_id if row.logical_id is None else row.logical_id) for row in rows) + ).encode() + ).hexdigest()[:8] role = "direct" if isinstance(source, DirectSource) else "scale" if isinstance(source, ScaleSource) else "function" - return f"channel.{rows[0].owner}.{field}.{role}.{fingerprint}" + return f"{rows[0].category}.{rows[0].owner}.{field}.{role}.{fingerprint}" def _group_indices(rows, group_by: str) -> tuple[np.ndarray, int]: keys = [] for row in rows: if group_by == "row": - key = (row.population_index, row.cv_id) + key = (row.population_index, row.cv_id if row.logical_id is None else row.logical_id) elif group_by == "population": key = row.population_index elif group_by == "cv": @@ -426,6 +461,16 @@ def _stack(values: tuple[object, ...]): return u.math.stack(values) +def _set_items(current, indices, values): + if isinstance(current, u.Quantity): + raw = values.to_decimal(current.unit) + return u.Quantity( + jnp.asarray(current.mantissa, dtype=jnp.result_type(current.mantissa, raw)).at[indices].set(raw), + current.unit, + ) + return jnp.asarray(current, dtype=jnp.result_type(current, values)).at[indices].set(values) + + def _equal_value(left: object, right: object) -> bool: if isinstance(left, u.Quantity): if not isinstance(right, u.Quantity): @@ -461,13 +506,13 @@ def _set_row(full: object, population_index: int, point_id: int, value: object): if isinstance(full, u.Quantity): if not isinstance(value, u.Quantity): raise TypeError(f"Materialized value requires a Quantity compatible with {full.unit}.") - mantissa = ( - jnp.asarray(full.to_decimal(full.unit)).at[population_index, point_id].set(value.to_decimal(full.unit)) - ) + replacement = value.to_decimal(full.unit) + dtype = jnp.result_type(full.mantissa, replacement) + mantissa = jnp.asarray(full.to_decimal(full.unit), dtype=dtype).at[population_index, point_id].set(replacement) return u.Quantity(mantissa, full.unit) if isinstance(value, u.Quantity): raise TypeError("Materialized value must be dimensionless.") - return jnp.asarray(full).at[population_index, point_id].set(value) + return jnp.asarray(full, dtype=jnp.result_type(full, value)).at[population_index, point_id].set(value) def _compact_axis(value: object, point_mask: object | None) -> str: diff --git a/braincell/trainable/_manager_test.py b/braincell/trainable/_manager_test.py index de084251..2bc41921 100644 --- a/braincell/trainable/_manager_test.py +++ b/braincell/trainable/_manager_test.py @@ -19,10 +19,18 @@ import brainstate import brainunit as u +import jax +import numpy as np import braincell from braincell._compute._testing import _build_tree -from braincell.filter import AllRegion +from braincell.filter import AllRegion, BranchSlice + + +@braincell.mech.register_channel("_SignatureRequiredLeak") +class _SignatureRequiredLeak(braincell.channel.IL): + def __init__(self, size, g_max, E=None, name=None): + super().__init__(size, g_max=g_max, E=-70.0 * u.mV if E is None else E, name=name) def _leak_cell(*, pop_size=(2,)): @@ -32,6 +40,493 @@ def _leak_cell(*, pop_size=(2,)): class TrainableManagerTest(unittest.TestCase): + def test_ion_initial_scale_uses_current_derived_default_before_init(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("CdpHVA_SU2015_DCN", name="pool", caiBase=0.001 * u.mM)) + cell.soma.ions["pool"].set(caiBase=0.002 * u.mM) + self.assertTrue(u.math.allclose(cell.ions["pool"].Ci_initializer, u.math.asarray([0.002, 0.001]) * u.mM)) + cell.ions["pool"].trainable(Ci_initializer=braincell.trainable.scale(name="initial")) + cell.init_state() + self.assertTrue(u.math.allclose(cell.get_ion("pool").Ci.value, u.math.asarray([[0.002, 0.001]]) * u.mM)) + + def test_ion_initial_parameter_survives_repeated_compiled_reset(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("CalciumDetailed", name="pool")) + cell.ions["pool"].trainable( + Ci_initializer=braincell.trainable.parameter(0.001 * u.mM, group_by="all", name="initial") + ) + cell.init_state() + + def initial_concentration(): + cell.reset_state() + return cell.get_ion("pool").Ci.value.to_decimal(u.mM).sum() + + run = brainstate.transform.jit(initial_concentration) + grad = brainstate.transform.jit( + brainstate.transform.grad(initial_concentration, grad_states=cell.trainables.parameters().states()) + ) + first = run() + self.assertGreater(float(u.get_mantissa(grad()["initial"])), 0) + cell.trainables.parameters().set_physical_values({"initial": 0.002 * u.mM}) + self.assertTrue(u.math.allclose(run(), first * 2)) + self.assertGreater(float(u.get_mantissa(grad()["initial"])), 0) + + def test_ion_signature_defaults_and_regional_gradients(self): + cell = braincell.Cell(_build_tree(), pop_size=(2,)) + cell.paint(AllRegion(), braincell.mech.Ion("SodiumInitNernst", name="pool")) + self.assertTrue(u.math.allclose(cell.ions["pool"].Ci, 10 * u.mM)) + cell[0].soma.ions["pool"].trainable(temp=braincell.trainable.parameter(group_by="all", name="temp")) + cell.init_state() + + def voltage(): + cell.reset_state() + return cell.get_ion("pool").E.to_decimal(u.mV)[0, 0] + + run = brainstate.transform.jit(voltage) + first = run() + original = cell.get_ion("pool").temp + values = cell.trainables.parameters().physical_values() + cell.trainables.parameters().set_physical_values({"temp": values["temp"] + 10 * u.kelvin}) + self.assertGreater(float(run()), float(first)) + self.assertTrue(u.math.allclose(cell.get_ion("pool").temp[1], original[1])) + gradient = brainstate.transform.grad(voltage, grad_states=cell.trainables.parameters().states())() + self.assertGreater(float(u.get_mantissa(gradient["temp"])), 0) + + def test_ion_derived_initializers_and_partial_explicit_initials(self): + cell = braincell.Cell(_build_tree(), pop_size=(2,)) + cell.paint(AllRegion(), braincell.mech.Ion("CdpHVA_SU2015_DCN", name="pool")) + cell.ions["pool"].trainable(caiBase=braincell.trainable.scale(name="base")) + cell[0].soma.ions["pool"].trainable( + Ci_initializer=braincell.trainable.parameter(0.001 * u.mM, group_by="all", name="initial") + ) + cell.init_state() + + def start(): + cell.reset_state() + return cell.get_ion("pool").Ci.value.to_decimal(u.mM) + + run = brainstate.transform.jit(start) + first = run() + cell.trainables.parameters().set_physical_values({"base": 2.0, "initial": 0.001 * u.mM}) + second = run() + self.assertTrue(u.math.allclose(first[0, 0], second[0, 0])) + self.assertTrue(u.math.allclose(first[1] * 2, second[1])) + self.assertTrue(u.math.allclose(cell.ions["pool"].Ci_initializer.to_decimal(u.mM), second.reshape(-1))) + + def test_ion_kinetic_rate_gradient_matches_finite_difference(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("ToyCaBindingKinetic_SU2015_DCN", name="pool")) + cell.ions["pool"].trainable(kf=braincell.trainable.scale(name="rate")) + cell.init_state() + + def loss(): + cell.reset_state() + ion = cell.get_ion("pool") + ion.compute_derivative(-65 * u.mV) + return ion.BC.derivative.to_decimal(u.mM / u.ms).sum() + + evaluate = brainstate.transform.jit(loss) + grad = brainstate.transform.jit( + brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states()) + ) + actual = float(grad()["rate"]) + cell.trainables.parameters().set_physical_values({"rate": 1.001}) + plus = evaluate() + cell.trainables.parameters().set_physical_values({"rate": 0.999}) + minus = evaluate() + self.assertGreater(actual, 0) + np.testing.assert_allclose(actual, (plus - minus) / 0.002, rtol=0.002) + + def test_ion_zero_gradient_and_natural_errors(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("SodiumFixed", name="pool")) + cell.ions["pool"].trainable(Ci=braincell.trainable.scale(name="concentration")) + cell.init_state() + + def loss(): + cell.reset_state() + return cell.get_ion("pool").E.to_decimal(u.mV).sum() + + gradient = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + self.assertEqual(float(gradient["concentration"]), 0) + for field, value in (("E", 1 * u.ms), ("name", "pool")): + invalid = braincell.Cell(_build_tree()) + invalid.paint(AllRegion(), braincell.mech.Ion("SodiumFixed", name="pool")) + with self.assertRaises((TypeError, ValueError)): + invalid.ions["pool"].trainable(**{field: braincell.trainable.parameter(value, group_by="all")}) + + def test_ion_post_init_set_changes_independent_initializer(self): + cell = braincell.Cell(_build_tree(), pop_size=(2,)) + cell.paint(AllRegion(), braincell.mech.Ion("CalciumDetailed", name="pool")) + cell.init_state() + cell[0].soma.ions["pool"].set(Ci_initializer=0.001 * u.mM) + cell.reset_state() + self.assertTrue(u.math.allclose(cell.get_ion("pool").Ci.value[0, 0], 0.001 * u.mM)) + self.assertTrue(u.math.allclose(cell.get_ion("pool").Ci.value[1], 2.4e-4 * u.mM)) + + def test_ion_radial_defaults_have_repeatable_compiled_gradients(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("CdpStC_NoCAM_MA2020_GoC", name="pool")) + cell.ions["pool"].trainable( + Nannuli=braincell.trainable.scale(name="shell"), + Buffnull2=braincell.trainable.scale(name="buffer"), + ) + cell.init_state() + + def loss(): + cell.reset_state() + ion = cell.get_ion("pool") + return (ion.Buff2.value.to_decimal(u.mM) * ion.dsqvol.to_decimal(u.um**2)).sum() + + run = brainstate.transform.jit(loss) + grad = brainstate.transform.jit( + brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states()) + ) + first = run() + g = grad() + self.assertLess(float(g["shell"]), 0) + self.assertGreater(float(g["buffer"]), 0) + cell.trainables.parameters().set_physical_values({"shell": 1.0, "buffer": 2.0}) + self.assertTrue(u.math.allclose(run(), first * 2)) + self.assertTrue(u.math.allclose(grad()["shell"], g["shell"] * 2)) + + def test_ion_integer_default_keeps_continuous_regional_values(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("SodiumInitNernst", name="pool")) + cell.ions["pool"].set(valence=np.array([1.5, 2.5])) + cell.ions["pool"].trainable(valence=braincell.trainable.parameter(group_by="row", name="charge")) + cell.init_state() + np.testing.assert_allclose(cell.get_ion("pool").valence, [[1.5, 2.5]]) + + def loss(): + cell.reset_state() + return cell.get_ion("pool").E.to_decimal(u.mV).sum() + + gradient = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + self.assertTrue(u.math.all(gradient["charge"] < 0)) + + def test_ion_configuration_keeps_scalar_conversion_and_natural_error(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Ion("ToyCaBindingKinetic_SU2015_DCN", name="pool")) + cell.ions["pool"].trainable(substeps=braincell.trainable.parameter(3.5, group_by="all", name="steps")) + cell.init_state() + self.assertEqual(cell.get_ion("pool").substeps, 3) + + def loss(): + cell.reset_state() + return cell.get_ion("pool").Ci.value.to_decimal(u.mM).sum() + + with self.assertRaises((jax.errors.ConcretizationTypeError, jax.errors.TracerArrayConversionError)): + brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + + def test_ion_split_layouts_keep_independent_groups(self): + for group in ("all", "row", "population", "cv"): + cell = braincell.Cell(_build_tree(), pop_size=(2,)) + cell.paint(BranchSlice(0, 0, 1), braincell.mech.Ion("SodiumFixed", name="pool", E=50 * u.mV)) + cell.paint(BranchSlice(1, 0, 1), braincell.mech.Ion("SodiumFixed", name="pool", E=55 * u.mV)) + cell.ions["pool"].trainable(E=braincell.trainable.scale(group_by=group, name="factor")) + cell.init_state() + + def loss(): + cell.reset_state() + return cell.get_ion("pool").E.to_decimal(u.mV).sum() + + gradient = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + self.assertTrue(u.math.all(gradient["factor"] > 0)) + self.assertAlmostEqual(float(u.math.sum(gradient["factor"])), 210) + before = cell.trainables.parameters().states()["factor"] + cell.reset() + cell.init_state() + self.assertIs(cell.trainables.parameters().states()["factor"], before) + + def test_ion_and_channel_share_a_root_and_parameterized_ion_profile(self): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), braincell.mech.Ion("SodiumFixed", name="pool"), braincell.mech.Channel("IL", name="leak") + ) + factor = brainstate.nn.Param(1.0) + cell.ions["pool"].trainable(E=braincell.trainable.scale(factor, name="shared")) + cell.channels["leak"].trainable(g_max=braincell.trainable.scale(factor, name="shared")) + slope = brainstate.nn.Param(1.0 * u.mM) + cell.ions["pool"].trainable( + Co=braincell.trainable.parameterized(lambda ctx, slope: 140 * u.mM + ctx.cv_id * slope, slope=slope) + ) + cell.init_state() + self.assertEqual(len(cell.trainables.parameters().states()), 2) + np.testing.assert_allclose(cell.get_ion("pool").Co.to_decimal(u.mM), [[140, 141]]) + self.assertTrue( + any(name.startswith("ion.") for name in cell.trainables.parameters().states() if name != "shared") + ) + + def test_missing_and_none_defaults_can_be_supplied_before_initialization(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Channel("_SignatureRequiredLeak", name="leak")) + with self.assertRaises(KeyError): + cell.channels["leak"].get("g_max") + cell.channels["leak"].set(g_max=0.2 * u.mS / u.cm**2) + cell.channels["leak"].trainable(E=braincell.trainable.parameter(-60.0 * u.mV, group_by="all", name="E")) + cell.init_state() + self.assertTrue(u.math.allclose(cell.channels["leak"].g_max, 0.2 * u.mS / u.cm**2)) + self.assertTrue(u.math.allclose(cell.channels["leak"].E, -60.0 * u.mV)) + + def test_floating_override_does_not_truncate_integer_default(self): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Ion("CalciumFixed", Ci=0.01 * u.mM), + braincell.mech.Channel("AHP_De1994", name="ahp"), + ) + cell.channels["ahp"].set(n=2.5) + cell.init_state() + self.assertTrue(u.math.allclose(cell.channels["ahp"].n, 2.5)) + cell.channels["ahp"].set(n=2.75) + self.assertTrue(u.math.allclose(cell.channels["ahp"].n, 2.75)) + + def test_missing_numeric_default_requires_values_on_all_active_rows(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Channel("_SignatureRequiredLeak", name="leak")) + cell.soma.channels["leak"].set(g_max=0.2 * u.mS / u.cm**2) + with self.assertRaisesRegex(ValueError, "every active row"): + cell.init_state() + + def test_required_parameter_accepts_an_explicit_trainable_initial(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Channel("_SignatureRequiredLeak", name="leak")) + cell.channels["leak"].trainable( + g_max=braincell.trainable.parameter(0.2 * u.mS / u.cm**2, group_by="all", name="g"), + ) + cell.init_state() + self.assertTrue(u.math.allclose(cell.channels["leak"].g_max, 0.2 * u.mS / u.cm**2)) + + def test_static_boolean_keeps_constructor_semantics(self): + for class_name, freeze, expected in ( + ("Ca_ZH2019_IO", False, False), + ("Ca_ZH2019_IO", True, True), + ("Ca_ZH2019_IO_Frozen", False, True), + ): + with self.subTest(channel=class_name, freeze=freeze): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Channel(class_name, name="ca", freeze_m_inf=freeze)) + cell.init_state() + node = cell.runtime.get_runtime_node(0) + self.assertIs(node.freeze_m_inf, expected) + self.assertTrue(bool(u.math.all(cell.channels["ca"].get("freeze_m_inf") == expected))) + + def test_frozen_gate_has_zero_gradient_but_unfrozen_gate_does_not(self): + for freeze in (False, True): + with self.subTest(freeze=freeze): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Channel("Ca_ZH2019_IO", name="ca", freeze_m_inf=freeze)) + cell.channels["ca"].trainable(mMidV=braincell.trainable.parameter(group_by="all", name="midpoint")) + cell.init_state() + node = cell.runtime.get_runtime_node(0) + + def loss(): + cell.trainables.materialize() + return node.current(-50.0 * u.mV).to_decimal(u.mA / u.cm**2).sum() + + gradient = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + value = float(u.get_mantissa(gradient["midpoint"])) + self.assertEqual(value == 0.0, freeze) + + def test_new_signature_parameter_has_independent_regional_gradients(self): + self._check_regional_gradients(split_layouts=False) + self._check_regional_gradients(split_layouts=True) + + def _check_regional_gradients(self, *, split_layouts): + cell = braincell.Cell(_build_tree(), pop_size=(2,)) + cell.paint( + AllRegion(), + braincell.mech.Ion("SodiumFixed", E=50.0 * u.mV), + ) + if split_layouts: + cell.paint( + BranchSlice(0, 0.0, 1.0), braincell.mech.Channel("Na_TM1991", name="na", g_max=100.0 * u.mS / u.cm**2) + ) + cell.paint( + BranchSlice(1, 0.0, 1.0), braincell.mech.Channel("Na_TM1991", name="na", g_max=120.0 * u.mS / u.cm**2) + ) + else: + cell.paint(AllRegion(), braincell.mech.Channel("Na_TM1991", name="na")) + original = cell[1].channels["na"].V_sh + cell[0].soma.channels["na"].trainable(V_sh=braincell.trainable.parameter(group_by="all", name="soma")) + cell[0].branch[1].channels["na"].trainable(V_sh=braincell.trainable.parameter(group_by="all", name="dendrite")) + cell.init_state() + node = cell.runtime.get_runtime_node(1) + if split_layouts: + self.assertIs(node, cell.runtime.get_runtime_node(2)) + + def loss(): + cell.trainables.materialize() + return node.f_p_alpha(-30.0 * u.mV, None)[0, 0] + + gradients = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + self.assertNotEqual(float(u.get_mantissa(gradients["soma"])), 0.0) + self.assertEqual(float(u.get_mantissa(gradients["dendrite"])), 0.0) + values = cell.trainables.parameters().physical_values() + cell.trainables.parameters().set_physical_values( + { + "soma": values["soma"] + 1.0 * u.mV, + "dendrite": values["dendrite"] - 2.0 * u.mV, + } + ) + cell.trainables.materialize() + self.assertTrue(u.math.allclose(cell[1].channels["na"].V_sh, original)) + self.assertTrue(u.math.allclose(cell[0].soma.channels["na"].V_sh, values["soma"] + 1.0 * u.mV)) + self.assertTrue(u.math.allclose(cell[0].branch[1].channels["na"].V_sh, values["dendrite"] - 2.0 * u.mV)) + + def test_gate_current_switch_has_zero_gradient_at_both_values(self): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Channel("Kv1p1_MA2025_BC", name="k"), + ) + cell.channels["k"].trainable(gateCurrent=braincell.trainable.parameter(group_by="all", name="switch")) + cell.init_state() + node = cell.runtime.get_runtime_node(1) + ion = braincell.IonInfo(E=-77.0 * u.mV, Ci=140.0 * u.mM, Co=5.0 * u.mM, valence=1) + + def loss(): + cell.trainables.materialize() + return node.current(-30.0 * u.mV, ion).to_decimal(u.mA / u.cm**2).sum() + + gradient = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states()) + self.assertEqual(float(gradient()["switch"]), 0.0) + disabled = brainstate.transform.jit(loss)() + cell.trainables.parameters().set_physical_values({"switch": 1.0}) + self.assertEqual(float(gradient()["switch"]), 0.0) + enabled = brainstate.transform.jit(loss)() + self.assertNotEqual(float(disabled), float(enabled)) + + def test_q10_gradient_is_zero_only_at_zero_temperature_offset(self): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), + braincell.mech.Ion("SodiumFixed", E=50.0 * u.mV), + braincell.mech.Channel("Na_TM1991", name="na"), + ) + view = cell.channels["na"] + view.set(temp=view.parameter_info()["temp_ref"].default) + view.trainable(q10=braincell.trainable.parameter(group_by="all", name="q10")) + cell.init_state() + node = cell.runtime.get_runtime_node(1) + + def loss(): + cell.trainables.materialize() + return node.gate_phi(node._iter_gates()[0]).sum() + + gradient = brainstate.transform.jit( + brainstate.transform.grad( + loss, + grad_states=cell.trainables.parameters().states(), + ) + ) + self.assertEqual(float(gradient()["q10"]), 0.0) + view.set(temp=view.parameter_info()["temp_ref"].default + 10.0 * u.kelvin) + self.assertGreater(float(gradient()["q10"]), 0.0) + + def test_integer_default_exponent_and_independent_phi_are_learnable(self): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Ion("CalciumFixed", Ci=0.01 * u.mM), + braincell.mech.Channel("AHP_De1994", name="ahp"), + ) + cell.channels["ahp"].trainable( + n=braincell.trainable.parameter(group_by="all", name="exponent"), + phi=braincell.trainable.parameter(group_by="all", name="phi"), + ) + cell.init_state() + node = cell.runtime.get_runtime_node(2) + calcium = braincell.IonInfo(Ci=0.01 * u.mM, Co=2.0 * u.mM, E=120.0 * u.mV, valence=2) + + def loss(): + cell.trainables.materialize() + return (node.phi * node.f_p_alpha(-30.0 * u.mV, None, calcium)).sum() + + gradients = brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + self.assertLess(float(gradients["exponent"]), 0.0) + self.assertGreater(float(gradients["phi"]), 0.0) + + def test_invalid_sources_use_existing_validation_and_rollback(self): + cell = _leak_cell(pop_size=(1,)) + for field, initial, errors in ( + ("name", "not-a-number", (TypeError, ValueError)), + ("g_max", 1.0, (TypeError,)), + ("E", 1.0 * u.kelvin, (ValueError,)), + ("g_max", np.ones((3, 4)) * u.mS / u.cm**2, (ValueError,)), + ("not_in_signature", 1.0, (KeyError,)), + ): + with self.subTest(field=field), self.assertRaises(errors): + cell.channels["leak"].trainable( + **{ + field: braincell.trainable.parameter(initial, group_by="all"), + } + ) + self.assertEqual(cell.trainables.bindings(), ()) + self.assertEqual(cell.trainables.parameters().states(), {}) + + def test_training_python_boolean_control_flow_errors_naturally(self): + cell = braincell.Cell(_build_tree()) + cell.paint(AllRegion(), braincell.mech.Channel("Ca_ZH2019_IO", name="ca")) + cell.channels["ca"].trainable( + freeze_m_inf=braincell.trainable.parameter(group_by="all", name="freeze"), + ) + cell.init_state() + node = cell.runtime.get_runtime_node(0) + + def loss(): + cell.trainables.materialize() + return node.current(-50.0 * u.mV).to_decimal(u.mA / u.cm**2).sum() + + with self.assertRaises((ValueError, jax.errors.TracerBoolConversionError)): + brainstate.transform.grad(loss, grad_states=cell.trainables.parameters().states())() + + def test_structural_signature_parameters_are_not_blacklisted(self): + for field in ("size", "solver", "substeps"): + with self.subTest(field=field): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), + braincell.mech.Ion("SodiumFixed", E=50.0 * u.mV), + braincell.mech.Channel("Nav1p6_MA2020_GoC", name="na"), + ) + cell.channels["na"].trainable( + **{ + field: braincell.trainable.parameter(2.0, group_by="all", name="configuration"), + } + ) + self.assertEqual(len(cell.trainables.bindings()), 1) + with self.assertRaises((TypeError, ValueError)): + cell.init_state() + + def test_derived_phi_updates_through_runtime_temperature_binding(self): + cell = braincell.Cell(_build_tree()) + cell.paint( + AllRegion(), + braincell.mech.Ion("SodiumFixed", E=50.0 * u.mV), + braincell.mech.Channel("Nav1p6_MA2020_GoC", name="na"), + ) + cell.channels["na"].trainable(temp=braincell.trainable.parameter(group_by="all", name="temp")) + cell.init_state() + node = cell.runtime.get_runtime_node(1) + + def loss(): + cell.trainables.materialize() + return node.f01(-30.0 * u.mV).sum() + + evaluate = brainstate.transform.jit(loss) + before = evaluate() + parameters = cell.trainables.parameters() + parameters.set_physical_values({"temp": u.celsius2kelvin(32.0)}) + after = evaluate() + self.assertTrue(u.math.allclose(after, 3.0 * before)) + gradient = brainstate.transform.grad(loss, grad_states=parameters.states())()["temp"] + self.assertTrue(u.math.allclose(u.get_mantissa(gradient), after * np.log(3.0) / 10.0)) + def test_grouping_produces_expected_degrees_of_freedom(self) -> None: expected = {"row": (4,), "population": (2,), "cv": (2,), "all": ()} for group_by, shape in expected.items(): @@ -252,9 +747,18 @@ def profile(ctx, a, b): ) ) - def test_legacy_channel_is_rejected_without_affecting_its_runtime(self) -> None: + def test_signature_default_can_be_set_and_trained_without_a_dictionary(self) -> None: cell = braincell.Cell(_build_tree()) - cell.paint(AllRegion(), braincell.mech.Channel("K_Leak", name="legacy")) - with self.assertRaises(NotImplementedError): - cell.channels["legacy"].trainable(g_max=braincell.trainable.parameter()) - self.assertEqual(cell.trainables.bindings(), ()) + cell.paint( + AllRegion(), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Channel("K_Leak", name="legacy"), + ) + view = cell.channels["legacy"] + self.assertIn("g_max", view.parameter_info()) + view.set(g_max=0.2 * u.mS / u.cm**2) + view.trainable(g_max=braincell.trainable.scale(name="factor")) + cell.init_state() + cell.trainables.parameters().set_physical_values({"factor": 2.0}) + cell.trainables.materialize() + self.assertTrue(u.math.allclose(view.g_max, 0.4 * u.mS / u.cm**2)) diff --git a/braincell/trainable/_network.py b/braincell/trainable/_network.py new file mode 100644 index 00000000..b70813e3 --- /dev/null +++ b/braincell/trainable/_network.py @@ -0,0 +1,92 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Network aggregation preserving Cell-local optimizer root identity.""" + +from collections.abc import Mapping +from dataclasses import replace +import weakref + +from braincell.trainable._parameters import ParameterSet + + +class _NetworkRoots(Mapping): + def __init__(self, manager): + self.manager = manager + + def __iter__(self): + return iter(self.manager._collect()[0]) + + def __len__(self): + return len(self.manager._collect()[0]) + + def __getitem__(self, key): + return self.manager._collect()[0][key] + + +class NetworkTrainables: + """Aggregate live Cell roots without introducing another parameter owner.""" + + def __init__(self, network): + self._network_ref = weakref.ref(network) + self.roots = _NetworkRoots(self) + + def _cells(self): + network = self._network_ref() + if network is None: + raise RuntimeError("Owning Network no longer exists.") + return [ + (name, population.cell) + for name, population in sorted(network.populations.items()) + if population.kind == "cell" + ] + + def _collect(self): + roots, names = {}, {} + for population, cell in self._cells(): + for local_name, root in cell.trainables.roots.items(): + if id(root) not in names: + name = f"{population}.{local_name}" + if name in roots: + raise ValueError(f"Ambiguous qualified trainable name {name!r}.") + roots[name] = root + names[id(root)] = name + return roots, names + + def parameters(self): + """Return the optimizer-facing live parameter collection.""" + return ParameterSet(self.roots) + + def bindings(self): + """Return population-qualified bindings to the original roots.""" + _, names = self._collect() + result = [] + for population, cell in self._cells(): + for binding in cell.trainables.bindings(): + result.append( + replace( + binding, + name=f"{population}.{binding.name}", + target_owner=f"{population}.{binding.target_owner}", + root_names=tuple(names[id(cell.trainables.roots[name])] for name in binding.root_names), + ) + ) + return tuple(result) + + def materialize(self): + """Refresh every initialized Cell's physical runtime parameters.""" + for _, cell in self._cells(): + if cell.trainables.bindings(): + cell.trainables.materialize() diff --git a/braincell/trainable/_network_test.py b/braincell/trainable/_network_test.py new file mode 100644 index 00000000..c67d1e00 --- /dev/null +++ b/braincell/trainable/_network_test.py @@ -0,0 +1,68 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Live Network aggregation preserves original optimizer roots.""" + +import gc +import unittest + +import braincell +import brainstate +import brainunit as u + +from braincell.filter import at +from braincell.network._testing import make_soma_tree +from braincell.trainable._network import NetworkTrainables + + +class NetworkTrainablesTest(unittest.TestCase): + def test_live_collection_shared_roots_and_bindings(self): + net = braincell.Network("shared") + parameters = net.trainables.parameters() + root = brainstate.nn.Param(1.0) + for name in ("b", "a"): + cell = braincell.Cell(make_soma_tree(), pop_size=(1,)) + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="syn")) + cell.synapses["syn"].trainable(tau=braincell.trainable.scale(root, name="factor")) + net.add_population(name, cell) + self.assertEqual(tuple(parameters.states()), ("a.factor",)) + self.assertEqual(len(net.trainables.roots), 1) + self.assertIs(net.trainables.roots["a.factor"], root) + self.assertIs(parameters.states()["a.factor"], root.val) + self.assertEqual(len(net.trainables.bindings()), 2) + self.assertTrue(all(b.root_names == ("a.factor",) for b in net.trainables.bindings())) + net.prepare_run(dt=0.1 * u.ms) + parameters.set_physical_values({"a.factor": 2.0}) + net.reset_state() + for population in net.populations.values(): + self.assertAlmostEqual(float(population.cell.synapses["syn"].tau[0] / u.ms), 0.2) + + def test_collected_owner_cannot_be_used(self): + net = braincell.Network("temporary") + manager = NetworkTrainables(net) + del net + gc.collect() + with self.assertRaisesRegex(RuntimeError, "no longer exists"): + list(manager.roots) + + def test_ambiguous_qualified_names_are_rejected(self): + net = braincell.Network("ambiguous") + for population, parameter in (("a.b", "c"), ("a", "b.c")): + cell = braincell.Cell(make_soma_tree(), pop_size=(1,)) + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="syn")) + cell.synapses["syn"].trainable(tau=braincell.trainable.scale(name=parameter)) + net.add_population(population, cell) + with self.assertRaisesRegex(ValueError, "Ambiguous"): + net.trainables.parameters().states() diff --git a/braincell/trainable/_targets.py b/braincell/trainable/_targets.py new file mode 100644 index 00000000..9cc39811 --- /dev/null +++ b/braincell/trainable/_targets.py @@ -0,0 +1,172 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Adapters from logical point targets to the common trainable registry.""" + +from dataclasses import dataclass + +import brainunit as u +import numpy as np + +from braincell._parameter_schema import ParameterSpec, RuntimeParameterState + + +@dataclass(frozen=True) +class _PointRow: + category: str + name: str + mechanism_type: str + population_index: int + cv_id: int + point_id: int + logical_id: int + + +class _PointTarget: + def __init__(self, rows, schema, read, write, validate=None): + self.rows = tuple(rows) + self._schema = schema + self._read = read + self._write = write + self._validate = validate + + def _trainable_schema(self): + return self._schema + + def _row_value(self, row, field): + return self._read(field)[self.rows.index(row)] + + def _prepare_write(self, field, values): + return self._write(field, values) + + def _validate_bindings(self, bindings): + if self._validate is not None: + self._validate(bindings) + + +def register_synapse(view, fields): + kind = view._require_homogeneous_type() + cell = view._cell + records = view._store.records(view.id) + rows = [_PointRow("synapse", kind, kind, r.population_index, r.cv_id, r.point_id, r.id) for r in records] + + def write(field, values): + store = cell._get_synapse_store() + state = cell._runtime.state_buffers[(store.layout_id(kind), field)] + return [(state, store.runtime_rows(view.id), values)] + + def validate(bindings): + from braincell.mech import get_registry + + columns = {field: view.get(field) for field in view.parameter_info()} + positions = {r.id: i for i, r in enumerate(records)} + for binding in bindings: + if binding._rows[0].category != "synapse" or binding.target_owner != kind: + continue + values = binding._evaluate() + for index, row in enumerate(binding._rows): + if row.logical_id in positions: + column = columns[binding.target_field] + column = column.at[positions[row.logical_id]].set(values[index]) + columns[binding.target_field] = column + get_registry().get("synapse", kind).validate_parameter_values(columns) + + target = _PointTarget(rows, view.parameter_info(), view.get, write, validate) + cell.trainables.register(target, fields) + + +def register_connection(view, fields): + if "delay" in fields: + raise NotImplementedError("Connection delay is static and cannot be trained.") + if set(fields).difference({"weight"}): + raise KeyError("Only Connection weight is trainable; bind threshold on its event detector.") + view._require_homogeneous_synapse_type("train weight") + current = view.weight + if current is None: + raise TypeError("Trigger-only Connection has no physical weight to train.") + ids = view.id + synapses = view.synapse + rows = [ + _PointRow("connection", "weight", "Connection", int(pop), int(cv), int(point), int(i)) + for i, pop, cv, point in zip(ids, synapses.population_index, synapses.cv_id, synapses.point_id) + ] + + def write(field, values): + commits = [] + store = view._store + call_ids = store.connect_id[store.rows(ids)] + for call_id in np.unique(call_ids): + selected = np.flatnonzero(call_ids == call_id) + call = store.call(int(call_id)) + state = vars(call)["weight"] + commits.append((state, ids[selected] - call.row_ids[0], values[selected])) + return commits + + target = _PointTarget(rows, {"weight": ParameterSpec(current[0])}, lambda field: view.weight, write) + view.cell.trainables.register(target, fields) + + +def register_detector(view, fields): + from braincell.network.event import VoltageCrossingSource, _CellSpikeSource + + source = view.owner + if not isinstance(source, (VoltageCrossingSource, _CellSpikeSource)): + raise TypeError("Only voltage event detectors have a trainable threshold.") + if set(fields).difference({"threshold"}): + raise KeyError("Only the detector threshold is trainable.") + cell = source.execution_owner + if cell._initialized: + raise RuntimeError("trainable() must be called before Cell.init_state().") + ids = view.source_id + own_threshold = isinstance(source, VoltageCrossingSource) and not source._uses_cell_threshold + if isinstance(source, VoltageCrossingSource): + populations = source._population_indices[ids] + cvs = source._cv_ids[ids] + else: + populations = ids + cvs = np.full(len(ids), source.cv_id, dtype=np.int64) + if own_threshold: + owner = f"detector.{source._training_id}" + logical = ids + state = source._threshold + indices = ids + read = lambda field: state.value[ids] + else: + owner = "V_th" + logical = populations * cell.n_compartment + cvs + if cell._V_th_parameter is None: + cell._V_th_parameter = RuntimeParameterState(cell._materialize_population_parameter("V_th")) + state = cell._V_th_parameter + indices = (populations, cvs) if len(cell.pop_size) else (cvs,) + read = lambda field: state.value[indices] + if len(set(logical.tolist())) != len(logical): + raise ValueError("A trainable detector selection cannot repeat the same threshold target.") + rows = [ + _PointRow("threshold", owner, "VoltageThreshold", int(pop), int(cv), int(cv), int(i)) + for pop, cv, i in zip(populations, cvs, logical) + ] + target = _PointTarget( + rows, {"threshold": ParameterSpec(0.0 * u.mV)}, read, lambda field, values: [(state, indices, values)] + ) + cell.trainables.register(target, fields) + + +def require_unbound(cell, category, owner, ids, field): + for binding in cell.trainables.bindings(): + if binding.target_field != field: + continue + for row in binding._rows: + if row.category == category and row.owner == owner and row.logical_id in ids: + raise RuntimeError(f"{category} {field} is trainable; update its parameter root instead.") diff --git a/braincell/trainable/_targets_test.py b/braincell/trainable/_targets_test.py new file mode 100644 index 00000000..407070e9 --- /dev/null +++ b/braincell/trainable/_targets_test.py @@ -0,0 +1,230 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Logical point parameter binding regressions.""" + +import unittest + +import brainstate +import brainunit as u +import numpy as np + +import braincell +from braincell.filter import at +from braincell.network._testing import make_soma_tree +from braincell.network.event import VoltageCrossingSource + + +def _cell(): + cell = braincell.Cell(make_soma_tree(), pop_size=(1,)) + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="syn", tau=2.0 * u.ms)) + return cell + + +class PointTargetsTest(unittest.TestCase): + def test_exp2_factor_uses_current_tau_and_matches_finite_difference(self): + cell = braincell.Cell(make_soma_tree(), pop_size=(1,)) + cell.place(at("soma", 0.5), braincell.mech.Synapse("Exp2Syn", name="syn")) + syn = cell.synapses["syn"] + syn.trainable(tau1=braincell.trainable.scale(name="factor")) + cell.init_state() + node = cell.runtime.get_runtime_node(syn._store.layout_id("Exp2Syn")) + parameters = cell.trainables.parameters() + + def observe(): + cell.reset_state() + node.apply_events(0.01 * u.uS) + return node.A.value.to_decimal(u.uS).sum() + + run = brainstate.transform.jit(observe) + derivative = brainstate.transform.jit(brainstate.transform.grad(observe, grad_states=parameters.states()))()[ + "factor" + ] + eps = 0.01 + parameters.set_physical_values({"factor": 1.0 + eps}) + plus = run() + parameters.set_physical_values({"factor": 1.0 - eps}) + minus = run() + self.assertNotEqual(float(derivative), 0.0) + np.testing.assert_allclose(derivative, (plus - minus) / (2 * eps), rtol=0.01) + + def test_failed_materialization_does_not_partially_write(self): + cell = _cell() + invalid = [False] + syn = cell.synapses["syn"] + syn.trainable( + tau=braincell.trainable.parameter(2.0 * u.ms, name="tau"), + e=braincell.trainable.parameterized(lambda ctx: 1.0 * (u.ms if invalid[0] else u.mV)), + ) + cell.init_state() + cell.trainables.parameters().set_physical_values({"tau": 4.0 * u.ms}) + invalid[0] = True + with self.assertRaises(u.UnitMismatchError): + cell.trainables.materialize() + np.testing.assert_allclose(syn.tau.to_decimal(u.ms), [2.0]) + + def test_grouping_parameterized_and_unselected_rows(self): + for group, shape in (("row", (4,)), ("population", (2,)), ("cv", ()), ("all", ())): + with self.subTest(group=group): + cell = braincell.Cell(make_soma_tree(), pop_size=(2,)) + for name in ("a", "b"): + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name=name, tau=2.0 * u.ms)) + cell.synapses.by_type("ExpSyn").trainable(tau=braincell.trainable.parameter(group_by=group, name="tau")) + self.assertEqual(cell.trainables.parameters().physical_values()["tau"].shape, shape) + cell.init_state() + np.testing.assert_allclose(cell.synapses.by_type("ExpSyn").tau.to_decimal(u.ms), 2.0) + cell = _cell() + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="other", tau=3.0 * u.ms)) + cell.synapses["syn"].trainable( + tau=braincell.trainable.parameterized(lambda ctx, factor: factor * u.ms, factor=brainstate.nn.Param(4.0)) + ) + cell.init_state() + np.testing.assert_allclose(cell.synapses["syn"].tau.to_decimal(u.ms), [4.0]) + np.testing.assert_allclose(cell.synapses["other"].tau.to_decimal(u.ms), [3.0]) + + def test_disjoint_contact_bindings_and_shared_scale(self): + cell = _cell() + first = braincell.connect("a", source=braincell.NetStim(), synapse=cell.synapses["syn"], weight=0.01 * u.uS) + second = braincell.connect("b", source=braincell.NetStim(), synapse=cell.synapses["syn"], weight=0.02 * u.uS) + root = brainstate.nn.Param(2.0) + for view in (first, second): + view.trainable(weight=braincell.trainable.scale(root, name="factor")) + self.assertEqual(len(cell.trainables.roots), 1) + with self.assertRaisesRegex(RuntimeError, "trainable"): + first.set(weight=0.03 * u.uS) + cell.init_state() + np.testing.assert_allclose(first.weight.to_decimal(u.uS), [0.02]) + np.testing.assert_allclose(second.weight.to_decimal(u.uS), [0.04]) + + def test_threshold_selection_and_alias_ownership(self): + cell = braincell.Cell(make_soma_tree(), pop_size=(2,)) + source = VoltageCrossingSource(cell) + source[0].trainable(threshold=braincell.trainable.parameter(-40.0 * u.mV, name="vth")) + with self.assertRaisesRegex(ValueError, "already bound"): + cell.event_outputs["spike"][0].trainable(threshold=braincell.trainable.parameter()) + cell[1].set(V_th=-15.0 * u.mV) + cell.init_state() + np.testing.assert_allclose(cell.V_th.to_decimal(u.mV), [[-40.0], [-15.0]]) + with self.assertRaises(RuntimeError): + source[1].trainable(threshold=braincell.trainable.parameter()) + + def test_detector_and_connection_invalid_fields(self): + cell = _cell() + source = VoltageCrossingSource(cell, threshold=-30.0 * u.mV) + for action, error in ( + (lambda: source.trainable(direction=braincell.trainable.parameter(1.0)), KeyError), + (lambda: source[[0, 0]].trainable(threshold=braincell.trainable.parameter()), ValueError), + (lambda: braincell.NetStim().trainable(threshold=braincell.trainable.parameter()), TypeError), + ): + with self.assertRaises(error): + action() + connection = braincell.connect( + "a", source=braincell.NetStim(), synapse=cell.synapses["syn"], weight=0.01 * u.uS + ) + with self.assertRaises(KeyError): + connection.trainable(threshold=braincell.trainable.parameter()) + with self.assertRaises((TypeError, ValueError)): + connection.trainable(weight=braincell.trainable.parameter(1.0 * u.ms)) + + def test_synapse_and_weight_survive_compiled_reset(self): + cell = _cell() + syn = cell.synapses["syn"] + connection = braincell.connect("input", source=braincell.NetStim(), synapse=syn, weight=0.02 * u.uS) + syn.trainable(tau=braincell.trainable.parameter(name="tau")) + connection.trainable(weight=braincell.trainable.parameter(name="weight")) + cell.init_state() + node = cell.runtime.get_runtime_node(syn._store.layout_id("ExpSyn")) + + def observe(): + cell.reset_state() + node.apply_events(connection.weight) + node.compute_derivative() + return node.g.derivative.to_decimal(u.uS / u.ms).sum() + + run = brainstate.transform.jit(observe) + grad = brainstate.transform.jit( + brainstate.transform.grad(observe, grad_states=cell.trainables.parameters().states()) + ) + np.testing.assert_allclose(run(), -0.01) + self.assertNotEqual(float(u.get_mantissa(grad()["tau"])), 0.0) + cell.trainables.parameters().set_physical_values({"tau": 4.0 * u.ms, "weight": 0.08 * u.uS}) + np.testing.assert_allclose(run(), -0.02) + self.assertNotEqual(float(u.get_mantissa(grad()["weight"])), 0.0) + + def test_colocated_synapses_have_independent_rows(self): + cell = _cell() + cell.place(at("soma", 0.5), braincell.mech.Synapse("ExpSyn", name="other", tau=3.0 * u.ms)) + cell.synapses.by_type("ExpSyn").trainable(tau=braincell.trainable.parameter(name="tau")) + cell.init_state() + self.assertEqual(cell.trainables.parameters().physical_values()["tau"].shape, (2,)) + cell.trainables.parameters().set_physical_values({"tau": np.array([5.0, 7.0]) * u.ms}) + cell.trainables.materialize() + np.testing.assert_allclose(cell.synapses.by_type("ExpSyn").tau.to_decimal(u.ms), [5.0, 7.0]) + + def test_detector_threshold_binding_and_gradients(self): + cell = _cell() + detector = VoltageCrossingSource(cell, threshold=-40.0 * u.mV) + detector.trainable(threshold=braincell.trainable.parameter(name="threshold")) + cell.init_state() + + def event(): + cell.reset_state() + cell._event_previous_V.value = np.array([[-45.0]]) * u.mV + cell.V.value = np.array([[-38.0]]) * u.mV + return detector.current_event_count([0]).sum() + + grad = brainstate.transform.jit( + brainstate.transform.grad(event, grad_states=cell.trainables.parameters().states()) + ) + self.assertNotEqual(float(u.get_mantissa(grad()["threshold"])), 0.0) + + def test_default_detector_binds_cell_threshold(self): + cell = _cell() + cell.event_outputs["spike"].trainable(threshold=braincell.trainable.parameter(-40.0 * u.mV, name="threshold")) + cell.init_state() + np.testing.assert_allclose(cell.V_th.to_decimal(u.mV), [[-40.0]]) + cell.trainables.parameters().set_physical_values({"threshold": -30.0 * u.mV}) + cell.reset_state() + np.testing.assert_allclose(cell.V_th.to_decimal(u.mV), [[-30.0]]) + + def test_static_delay_and_owned_set_are_rejected(self): + cell = _cell() + syn = cell.synapses["syn"] + connection = braincell.connect("input", source=braincell.NetStim(), synapse=syn, weight=0.02 * u.uS) + with self.assertRaisesRegex(NotImplementedError, "delay"): + connection.trainable(delay=braincell.trainable.parameter()) + syn.trainable(tau=braincell.trainable.parameter()) + with self.assertRaisesRegex(RuntimeError, "trainable"): + syn.set(tau=3.0 * u.ms) + + def test_invalid_synapse_registration_is_atomic(self): + cell = _cell() + with self.assertRaisesRegex(ValueError, "> 0"): + cell.synapses["syn"].trainable(tau=braincell.trainable.parameter(-1.0 * u.ms)) + self.assertEqual(len(cell.trainables.roots), 0) + cell.place(at("soma", 0.5), braincell.mech.Synapse("Exp2Syn", name="double")) + syn = cell.synapses["double"] + with self.assertRaisesRegex(ValueError, "tau1 < tau2"): + syn.trainable(tau1=braincell.trainable.parameter(20.0 * u.ms)) + self.assertEqual(len(cell.trainables.roots), 0) + syn.trainable(tau1=braincell.trainable.parameter(20.0 * u.ms), tau2=braincell.trainable.parameter(30.0 * u.ms)) + + def test_owned_cell_threshold_cannot_be_overwritten(self): + cell = _cell() + cell.event_outputs["spike"].trainable(threshold=braincell.trainable.parameter()) + with self.assertRaisesRegex(RuntimeError, "trainable"): + cell.V_th = -30.0 * u.mV + with self.assertRaisesRegex(RuntimeError, "trainable"): + cell[0].set(V_th=-30.0 * u.mV) diff --git a/docs/apis/vis.rst b/docs/apis/vis.rst index e05f5e12..91e0b93f 100644 --- a/docs/apis/vis.rst +++ b/docs/apis/vis.rst @@ -5,9 +5,10 @@ .. automodule:: braincell.vis ``braincell.vis`` is the visualization layer of BrainCell. It turns a -:class:`~braincell.Morphology` (or any higher-level object that carries -one) into static plots through matplotlib, interactive 3D through PyVista -or Plotly, and publication-quality exports. The module is deliberately +:class:`~braincell.Morphology` into static plots through matplotlib, +interactive 3D through PyVista or Plotly, and publication-quality exports. +Pass ``cell.morpho`` to morphology plots; use ``plot_cell_topology(cell, ...)`` +to resolve cell-level selections and runtime fields. The module is split into three layers: 1. **Scene builders** (``scene2d`` / ``scene3d``) translate a morphology @@ -23,6 +24,41 @@ imported lazily inside the backend that uses them so the base install stays small. +Start with a Cell +----------------- + +Build a morphology, inspect its CV topology, then initialize the cell to +colour runtime nodes by membrane voltage: + +.. code-block:: python + + import braincell as bc + import brainunit as u + from braincell import vis + + soma = bc.Branch.from_points( + points=[[0., 0., 0.], [10., 0., 0.]] * u.um, + radii=[5., 5.] * u.um, + type="soma", + ) + morpho = bc.Morphology.from_root(soma, name="soma") + cell = bc.Cell(morpho, cv_policy=bc.CVPerBranch(2)) + ax = vis.plot2d(cell.morpho) + vis.plot_cell_topology(cell, level="cv", layout="kamada_kawai") + cell.init_state() + voltage_ax = vis.plot_cell_topology( + cell, value="V", layout="kamada_kawai", + ) + vis.save_figure(voltage_ax, "voltage.png") + +Plotting reads current state without advancing the simulation. Replot to +show new state; reinitializing the cell is a separate model operation. +``Morphology.vis2d()`` and ``Branch.vis2d()`` are convenience wrappers. +Their ``show=True`` default calls ``matplotlib.pyplot.show()``; the function +entry points return figures or axes for explicit display and customization. +PyVista may create a notebook viewer according to its notebook settings. + + Top-level plot entry points --------------------------- @@ -64,12 +100,30 @@ cell's control volumes and colour nodes by runtime state. Pick the granularity with ``level``: ``level="node"`` (the default) - One node per runtime point — the level the solver works at, and the - only one that can show per-point state. Requires an initialized cell. + One node per runtime point, including CV midpoints and boundary points. + Requires an initialized cell. CV membrane values such as ``"V"`` are + shown at midpoints; other points receive NaN. Explicit point arrays can + colour every point. ``level="cv"`` - One node per control volume. + One node per control volume. Structure and selections can be inspected + before initialization; value rendering requires initialized state. ``level="branch"`` - One node per morphology branch. Topology only. + One node per morphology branch. Supports region coverage; rejects + locset, value, and value-colormap options. + +Cell plots accept region/locset expressions or evaluated masks. Morphology +plots take evaluated masks. ``value=`` is mutually exclusive with +``region=`` and ``locset=`` in Cell topology plots. Region and locset +highlights at node level select the owning CV's midpoint. + +Morphology ``values=`` arrays describe branches, geometric segments, or +centerline points. Cell ``value=`` arrays describe CVs or runtime nodes. +These spaces are distinct even when their array lengths happen to match. + +Use ``value="V"``, ``("ion", ion_name, field)``, +``("channel", class_name, field)``, or ``("layout_id", layout_id, field)`` +to select fields. Non-singleton population axes require selecting a member +explicitly and passing its one-dimensional array. .. autosummary:: :toctree: generated/ @@ -78,6 +132,28 @@ granularity with ``level``: plot_cell_topology +Return values and display +------------------------- + +Matplotlib plots return ``Axes``; comparison helpers return a figure and a +tuple of axes. ``plot_traces`` returns ``TracesResult`` with ``figure``, +``morpho_axes`` and ``trace_axes``. ``plot_movie`` returns ``MovieResult`` +with ``animation``, ``frames`` and ``output_path``; keep this object alive +while displaying an animation. + +Plotly returns a ``Figure``. PyVista returns a ``Plotter`` outside notebooks, +and may return a viewer or an HTML display object inside a notebook. +``return_plotter=True`` requests the raw plotter; use ``notebook=False`` +as well to avoid notebook-viewer creation. The morphology convenience +methods return their backend result even when ``return_plotter=False``. + +``save_figure`` accepts Matplotlib Axes/Figure, Plotly Figure, or PyVista +Plotter. Pass ``traces.figure`` for a trace result. Output directories must +exist. Plotly static-image export needs an image engine; PyVista vector +export depends on its ``save_graphic`` capability. Animation output uses +``plot_movie(out=...)`` and the relevant writer dependencies. + + Comparison helpers ------------------ diff --git a/docs/design/AGENTS.md b/docs/design/AGENTS.md new file mode 100644 index 00000000..5dea263a --- /dev/null +++ b/docs/design/AGENTS.md @@ -0,0 +1,211 @@ +# Design 文档规范 + +适用于 `docs/design/` 及其子目录。文档应让读者顺着具体例子理解系统、问题和设计决定。 +跨代码、设计与示例的提交前检查遵循 [仓库约定](../../AGENTS.md#design-code-and-examples)。 + +简洁是删掉重复表达,保留完整信息。Current 要让读者能正确使用接口、理解运行机制; +Proposal 要让读者能理解问题、比较方案并作出决定。两者分别遵循下方的内容规格。 + +## 通用写作规则 + +1. **开头直接说主题。** 用一小段说明当前情况、具体问题或核心行为。需要标注状态时,在开头写一次;局部未定事项放在对应问题处。 +2. **按读者的问题展开。** API 从使用流程进入接口查询;架构追踪数据与状态;Proposal 从问题和原因进入方案比较。 +3. **具体例子优先。** 接口用最小调用展示,计算用节点和方程说明,架构用关系图或数据流表达,方案用表格比较。根据内容选择形式。 +4. **正文解释含义。** 图、表、代码和公式已经表达的信息只写一次,正文补充关键原因、推论或取舍。符号、单位和必要假设随例子定义。 +5. **每段提供新信息。** 定义、结论和约束在一个位置完整说明,其他位置引用。删除同义复述和反复总结。 +6. **正文描述系统。** “本次仅整理文档”“本轮不修改代码”“此次迁移不代表……”等任务汇报放在协作回复或历史记录。 +7. **限制写具体。** 写清触发条件、实际行为和影响。例如“独立 RandomState 使用自己的随机流”。保留影响使用、推导或决定的约束,删除泛泛的防御性声明。 +8. **区分事实、建议与未知。** 事实给源码、实验或来源依据;建议给理由;未知写成具体问题。证据缺口写明缺什么,例如“端点刺激尚缺显式 RK 数值对照”。 +9. **章节与内容匹配。** 标题描述具体接口、流程或问题,如“Cell.run”“状态布局”“边界输入丢失的位置”。同类规则集中说明,差异在对应条目补充。 +10. **按信息需要控制篇幅。** 保留调用契约、必要假设、关键反例和证据。完稿逐段检查:删除后对模型、用法、推导或决定的理解没有减少,就删去。 + +## Current 规格 + +Current 描述当前实现。公共 API、内部架构和实验实现按内容分开;模块内容较多时, +分别使用 `current/api.md`、`current/architecture.md`,专题再按实际需要拆分。 +已实现功能和具体限制写在 Current,候选接口、备选设计及剩余工作链接到 Proposal。 + +### API:用法与接口契约 + +阅读顺序为“功能和入口 → 最小用法 → 按职责组织的接口 → 生命周期与关键限制”。 + +- 开头说明能完成什么任务、从哪个公开命名空间导入。最小用法包含导入、对象构造、关键调用和结果读取;后续片段可复用前面的对象,并写清依赖的上下文。 +- 概览表负责导航,覆盖的公开接口应能定位到具体说明或权威文档。主要接口给出完整调用签名,保留参数顺序、关键字限定和默认值;省略参数的片段标为调用示例。 +- 构造器、方法、函数、属性和结果对象按使用流程或职责组织。同类接口共用规则,差异单列;内部辅助函数留在架构或源码中。 + +每个接口按下表交代适用信息。简单接口可以用签名和短段落说明,参数较多时使用表格, +无需为不适用的项目创建空章节。 + +| 信息 | 必须说清什么 | +| --- | --- | +| 功能与签名 | 调用完成的操作、公开名称及完整调用形式;属性说明读写能力 | +| 参数 | 类型、默认值和语义;`None`、枚举值及参数组合有特殊含义时分别说明 | +| 数值与空间 | 物理单位、数组形状和各轴含义;支持的广播、索引及位置映射规则 | +| Callable | 回调签名、输入与返回值,以及影响使用的求值时机或上下文 | +| 返回值 | 类型与含义;复合结果说明主要字段、形状和单位;区分新对象、原对象、view 与 `None` | +| 状态影响 | 是否就地修改、修改哪些状态,何时生效;共享引用或缓存会影响使用时说明其行为 | +| 调用条件 | 初始化要求、结构何时冻结、允许的调用阶段和必要的环境上下文 | +| 重复调用 | 累积、覆盖、继续推进或重新初始化的行为;reset 对状态与结构的影响 | +| 关键错误 | 实际触发条件、异常类型及可采取的调用调整,例如互斥参数、非法形状或阶段错误 | + +- 对有歧义的行为用最小输入和可观察结果说明,例如返回形状、位置归属或重复运行的时间区间。代码块中的候选语法属于 Proposal,Current 示例使用已实现接口。 +- 公共契约对照当前导出、签名和实现核实,相关测试及实际示例提供验证入口。源码链接用于核对依据,正文保留用户理解和调用接口必需的信息。 +- 跨模块接口指定一个主要维护位置,其他文档链接到对应章节。长文档可拆成专题并保留索引,避免靠删参数、返回值或调用条件来缩短篇幅。 + +### Architecture:结构与运行机制 + +阅读顺序为“职责边界与核心对象 → 典型数据流 → 状态和执行路径 → 关键计算与限制”。 + +- 先说明模块接收什么、产出什么,以及与相邻模块的职责边界;通过模块职责表定位实现入口。 +- 沿一个典型流程追踪输入如何成为运行结果,交代核心对象的数据归属、共享关系和生命周期,以及声明、构建、运行各阶段的转换。 +- 涉及数组或空间离散时说明状态布局、各轴含义与映射关系;涉及多阶段更新时写清读取旧值还是新值、更新顺序及原因。 +- 不同执行路径分别说明适用条件、共享部分和实际行为差异,例如默认求解器与通用导数路径。选择条件和顺序会影响结果时,不能只列函数调用名。 +- 数学计算给出关键方程,随例子定义变量、单位、符号和适用假设;正文解释公式对应哪个阶段、产生什么结果。 +- 每张图只表达一种关系,注明箭头代表依赖、调用还是数据流;节点用短名称,文件和函数细节放职责表。复杂图按阶段拆开,检查渲染后是否有多余节点、拥挤标签或难以追踪的连线。 +- 实现决定说明仍然有效的理由与取舍;历史过程进入 specs,未定设计进入 Proposal。已实现限制写清触发条件与影响,公共调用细节链接 API。 + +## Proposal 规格 + +阅读顺序为“当前问题 → 最小例子 → 原因 → 方案比较 → 待决定问题”。 + +- 开头写一次状态和要解决的问题。先让读者看到现有行为及其影响,再提出改进目标;当前实现链接 Current。 +- 例子只保留理解问题所需的对象、调用或方程。计算问题列清现有项、缺项和正确关系;公式随例子定义变量、单位、符号及假设。 +- 候选方案分别说明如何解决问题、成立条件和主要代价。比较相同维度,候选 API 在代码块前明确标注。 +- 已确认决定说明采用的方案和理由;未定事项写成具体问题,标出决定所缺的证据。已确认范围与尚未确定的接口可以分别陈述。 +- 只展开当前决定依赖的问题。缓存、并发、序列化等未来细节在影响方案选择时展开;进入实施准备后补齐实施步骤和可观察的验收场景。 +- 任务状态及下一步由模块 TODO 管理,Proposal 集中保留论证、方案和决定。 + +## Reference 与结果记录 + +| 类型 | 阅读顺序 | 应保留的信息 | +| --- | --- | --- | +| Reference | 要回答的问题 → 外部方法或推导 → 对本项目的启示 | 来源与版本、关键结论、推导假设和适用条件、采用状态、关联方案或实现的双向链接 | +| 实测结果 | 实验问题与配置 → 结果 → 解释 → 复现入口 | 模型与数据、软件和硬件配置、测量口径、结果与证据范围、实际脚本或命令 | + +外部实现事实、本文推导和本项目建议分别表述。性能结论对应具体配置和测量结果, +例如注明是否计入编译、是否等待异步计算完成;复现入口链接实际示例的位置。 + +## 目录与维护 + +- 模块以 `/TODO.md` 为唯一协作入口,详细内容按主题命名,文件名与 H1 对应。 +- `proposals/` 保存未落地方案、备选方向和剩余工作;`current/` 持续描述已实现行为;`references/` 保存研究依据。按实际内容创建目录和文件。 +- 跨模块架构放在 `architecture/`。主题目录按问题归属划分,可以跨越多个 Python 包。 +- 全局 [TODO](TODO.md) 按模块维护能力、目标及主要缺口;模块 TODO 管理任务拆分、下一步和验收细节。里程碑或阻塞变化时同步对应条目。 +- 部分落地时,已实现行为进入 current,剩余问题保留在 proposal。需要保留的讨论历史按日期进入 `docs/specs/`。 +- current 区分公共接口和实验实现;实测可按实验主题放在 `current/results/`,记录配置、来源、复现入口及证据范围。全局 shipped 状态遵循下方的提交与验收口径。 +- 参考资料保留一个权威副本。引用处说明用途,参考页反向链接到相关方案或实现,并标注待采用、已采用或背景参考。 +- 目录分工、状态定义和写作要求在本文件集中维护,各模块通过链接引用。`AGENTS.md` 是规范入口,`TODO.md` 是进度入口。 + +## 全局 TODO 规格 + +全局 TODO 应让读者直接判断每个模块已经能做什么、还要实现什么、主要缺口在哪里。 + +- **按模块展开列表。** 每个模块独立成节,保留局部 TODO 入口,先列已有能力,再列推进中的能力和后续目标;不将整个模块压成表格的一行。 +- **按能力划分条目。** 一条对应一个能独立判断完成情况的功能或目标。简单模块可以少列,复杂模块按实际内容展开;具体方法、实施步骤和验收矩阵由局部文档维护。 +- **行首标记进度。** 模块和跨模块任务统一以 `- [x]`、`- [~]`、`- [ ]` 开头,导航保留普通列表。`[x]` 表示已完成;`[~]` 表示部分完成、实施中或待提交验收;`[ ]` 表示待讨论、讨论中或已确认待实施。 +- **一至两句话交代含义。** 标记后写“能力或目标名称:实际行为或预期结果。详情链接”。已完成、部分完成不在句尾重复标注;部分完成写清已有能力和缺项,其他状态保留阶段文字。 +- **链接到维护位置。** 已完成条目链接 Current;计划条目优先链接已有 Proposal,尚无 Proposal 时链接局部 TODO。条目可以直接指向具体章节,避免每层只提供泛化目录。 +- **跨模块事项写清分工。** 列出牵头模块、参与模块及关键依赖或阻塞。模块章节保留目标简述,跨模块章节集中解释协作关系。 +- **里程碑变化时同步。** 全局更新能力范围、总体状态和主要缺口;局部更新具体事项、下一步与验收依据。部分完成的全局目标可以包含已完成子项和仍在讨论的后续子项。 +- **核对历史与现状。** 整理时对照旧条目、快照和局部 TODO 检查遗漏;当前状态依据提交记录、实现及验收记录判断。过时条目按现状修正,历史快照保持原样。 + +## 事项状态 + +| 状态 | 含义 | +| --- | --- | +| 待讨论 | 问题已提出,下一步展开分析 | +| 讨论中 | 正在分析方案、证据或接口 | +| 已确认待实施 | 该事项的方案与验收边界已确定,等待实现 | +| 实施中 | 已开始实现,列出剩余工作与阻塞 | +| 部分完成 | 已有部分能力,明确仍缺的功能、运行时集成或验证 | +| 待提交验收 | 工作区实现与验证已完成,尚未满足全局已完成的提交条件 | +| 已完成 | 实现与验证已完成,链接最终说明;全局条目还需满足下方的提交验收条件 | + +状态对应具体事项。范围决定可以已经确认,同时接口设计仍在讨论;备选方案写在 proposal 中。 +实施中描述正在进行的工作,部分完成描述一个目标的能力覆盖,两者按条目要表达的层级使用。 + +### 全局完成与验收 + +全局“已完成”沿用历史快照的 shipped 口径:条目范围内的实现、相关测试、设计及适用示例已随提交验收。 +局部事项完成或 Current 出现可调用接口,不直接使全局目标升级;工作区实现与验证已齐备时标为“待提交验收”。 +仍缺功能、运行时接入或验证依据时,写出具体缺项并保留“部分完成”或对应的推进状态。 + +验收依据保存在对应 Current、结果页或历史记录,全局链接到该维护位置。 +实验能力在条目中注明实验范围;某个模型、设备或 solver 的通过结果只支持对应场景。 +历史 `[x] shipped` 对应全局已完成,`[~] partial` 对应部分完成;`[ ]` 必须结合文字区分研究讨论与已确认待实施。 + +局部 TODO 按“事项 → 状态 → 下一步 → 详情链接”组织,可以保留表格。 +全局 TODO 开头只保留简短进度口径并链接本规范,详细规则在这里维护。 + +## 完稿检查 + +| 类型 | 检查问题 | +| --- | --- | +| API | 读者能否选对入口、构造合法参数、在正确阶段调用,并理解返回值和状态变化?主要接口是否只有名称清单而缺少说明? | +| Architecture | 能否从输入追踪到输出,找到状态归属和更新时序,并解释不同执行路径为何产生不同结果? | +| Proposal | 能否复述具体问题、判断方案取舍,并指出已决定什么、下一步还缺什么证据? | +| Reference / 结果 | 结论能否追溯到来源、假设或实验配置,关联用途和复现入口是否明确? | +| TODO | 每项能力或目标是否有明确状态和详情入口?部分完成是否写出缺项?全局完成是否有提交验收依据,且与局部事项范围一致? | +| 所有文档 | 是否有重复结论、任务汇报或泛泛声明?是否因压缩篇幅删掉了必要信息?链接、锚点及图表是否有效? | + +接口说明核对源码,修改后的可运行示例按风险验证;架构图变更后检查实际渲染。 +涉及提交时,执行仓库规定的设计、实现和相关示例同步检查。 + +## 改写示例 + +### Current:从方法名到调用契约 + +原文: + +> 连续运行:`run(dt=..., duration=...)`。 + +改为以下结构。签名块用于查阅,调用片段复用已构造的独立 `cell`: + +```text +Cell.run(*, dt, duration) -> RunResult +``` + +按固定步长推进 Cell,返回本次时间段的结果。 + +| 参数 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `dt` | 正的标量时间量 | 必填 | 积分步长,例如 `0.025 * u.ms` | +| `duration` | 正的标量时间量 | 必填 | 本次运行时长,必须为 `dt` 的整数倍 | + +返回 `RunResult`,其 `time` 为形状 `(n_steps,)` 的时间数组,`traces`、`samples` 保存 +已声明的观测结果。调用会就地推进 Cell 状态和当前时间;首次调用自动初始化,后续调用继续推进。 +由 Network 管理的 Cell 通过 Network 运行,独立调用此方法会抛出 `RuntimeError`。 + +```python +import brainunit as u + +first = cell.run(dt=0.025 * u.ms, duration=1.0 * u.ms) +second = cell.run(dt=0.025 * u.ms, duration=1.0 * u.ms) +assert first.stop_time == second.start_time +``` + +这一例子的依据见 [Cell.run](../../braincell/_multi_compartment/cell.py) 和 +[RunResult 与时间检查](../../braincell/_multi_compartment/run.py)。模块中的正式接口说明由 +[Cell API](cell/current/api.md) 维护。 + +### 从声明转向模型 + +原文: + +> 本文只记录讨论方向,不定义可调用的类、方法或签名,不代表可塑性接口已经实现。 + +改为: + +> 状态:讨论中。 +> +> 可塑性分为两类:改变突触内部动力学的模型由 Synapse 实现;只更新连接权重的规则挂载在 Connection 上。下面讨论状态归属和上下游信号。 + +### 从笼统限制转向具体缺项 + +原文: + +> staggered 的支持不代表所有 solver 已支持,不保证成功 place 后都能保持完整语义。 + +改为: + +> 单 branch、3 CV 对应五个电气节点。staggered 装配三行膜电压方程和两行边界约束;当前显式路径消去边界后,遗漏了端点刺激和突触产生的反馈项。下面列出完整方程及缺失项。 diff --git a/docs/design/TODO.md b/docs/design/TODO.md index 3cac55cf..ee24a9b2 100644 --- a/docs/design/TODO.md +++ b/docs/design/TODO.md @@ -1,1310 +1,190 @@ -# BrainCell Project Design and TODO +# BrainCell Project TODO -> Status: living document. Tracks both the architectural intent of the -> `braincell` package and the current implementation state of every major -> subsystem. Status markers in this file follow: -> -> - `[x]` shipped — implemented, covered by `*_test.py`, and documented at -> its intended public or internal surface. -> - `[~]` partial — implementation exists but is missing functionality, -> tests, or runtime integration. Specific gaps are listed inline. -> - `[ ]` planned — design agreed, code not yet written. -> -> This document describes committed repository state. Experimental work in an -> uncommitted working tree does not become a shipped capability until its API, -> implementation, and tests land together. +BrainCell 用带物理单位的形态、机制和事件声明构建可微分的细胞与网络模型。 +本页按模块列出已有能力、后续目标和主要缺口;任务拆分、下一步与验收细节由各模块 TODO 维护。 +系统职责与数据流见 [系统总览](architecture/current/system-overview.md)。 ---- +## 进度口径 -## Document Navigation +- `[x]` **已完成**:沿用 shipped 口径,实现、相关测试、设计及适用示例已随提交验收。 +- `[~]` **部分完成 / 实施中 / 待提交验收**:已有能力或正在实现,具体缺项及实施、提交阶段随条目说明;待提交验收表示工作区实现与验证已完成。 +- `[ ]` **待讨论 / 讨论中 / 已确认待实施**:问题待分析、方案正在讨论,或方案和验收边界已确定、等待实现。 -- [Mission and scope](#1-mission-and-scope) -- [Top-level architecture](#2-top-level-architecture) -- [Module catalogue](#3-module-catalogue) -- [Cross-cutting concerns](#4-cross-cutting-concerns) -- [Public API contract](#6-public-api-contract) -- [End-to-end workflows](#7-end-to-end-user-workflows) +Current 描述工作区行为,条目状态按其所列范围判断。详细规则见 +[进度维护规范](AGENTS.md#全局-todo-规格);历史条目及原始验证口径见 +[整理前快照](../specs/2026-09-07-design-todo-snapshot.md)。 -Detailed network and parameter-training contracts live in -[`network/`](network/design-overview.md) and [`optim/`](optim/design-overview.md). -Those topic directories are authoritative when their details are more specific -than this project-level summary. +## 模块里程碑 -## Current Architecture Snapshot +### Architecture:系统分工与公共约定 -The committed repository currently provides: +协作入口:[Architecture TODO](architecture/TODO.md)。 -- [x] **Cerebellum channel/ion imports and tests expanded.** The channel - catalogue now includes PC MA2024 channel variants and the calcium-ion - catalogue includes concrete Cerebellum kinetic-ion imports such as - `CdpStC_*`, `CdpCAM_MA2024_PC`, and `CdpCR_MA2020_GrC`, with co-located - unit tests and NEURON-comparison notebooks under `examples/neuron_compare`. -- [x] **PC MA2024 assembly scaffold added.** `examples/neuron_compare/cell/pc_ma2024` - contains the simplified NEURON assembly, the matching BrainCell assembly, - shared parameter loading, debug variants, and `run.ipynb` for side-by-side - simulation. -- [x] **Direct multi-compartment runtime.** `Cell` owns declaration and runtime - state, is initialized with `init_state()`, and advances directly with - `run()`. The former `Cell -> RunnableCell` build boundary no longer exists. -- [x] **Population network runtime.** `braincell.network` owns population - registration, event routing, initialization and result aggregation while - synapses, connections and recordings remain owned by their target `Cell`. -- [x] **Trainable parameter mappings.** `braincell.trainable` maps selected - channel fields from direct, shared-scale or latent parameter sources into - runtime values. Optimizers, losses and training loops remain user-owned. -- [x] **SWC writing and structural round trips.** `Morphology.to_swc()` writes - branch trees through `braincell.io.swc`; focused tests cover shared branch - endpoints, soma attachments, reversed branches and validation failures. -- [x] **NEURON-style ion-current snapshot mode added.** `Cell(..., - cache_ion_total_current=True)` caches the total ion current at the start of - the staggered step, before voltage or ion state advances, so current-driven - ion mechanisms can read the same precomputed current snapshot that - NEURON-style scheduling expects. -- [x] **Frozen voltage channel variants added where needed.** Some PC calcium - channels now have `_Frozen` variants which stop differentiation through the - voltage used inside the current expression, matching the intended NEURON - semantics for those mechanisms during the comparison. -- [x] **Two ion/channel update schedules are available.** - `ion_channel_update_order="family"` restores the NEURON-like family - ordering for ion/channel updates; `"integration"` keeps the previous - BrainCell integration-oriented ordering. -- [x] **Homogeneous multi-compartment `Cell` populations now support - multi-dimensional `pop_size`.** `Cell(..., pop_size=(...))` expands - runtime state to `pop_size + (n_cv,)`, point-space runtime arrays to - `pop_size + (n_point,)`, and supports population-specific - `CurrentClamp(...)` amplitudes such as `(2,)` or `(2, 2)`-shaped - current grids. Regression coverage includes `(2,)` and `(2, 2)` - populations. -- [x] **The population axis is mandatory.** `pop_size` defaults to `1` - and an explicitly empty `pop_size=()` is rejected, so every `Cell` - hidden state is at least two-dimensional and its trailing axis always - enumerates compartments or points. That invariant is what lets `Cell` - states be `brainstate.HiddenGroupState` (`Cell.V` is a - `braincell.DiffEqGroupState`) while `SingleCompartment`, which has no - spatial axis, keeps the plain `brainstate.HiddenState`. See - `docs/specs/2026-08-13-cell-hidden-group-state.md`. -- [x] **The channel template layer validates at class-definition time.** - `HH` and `Markov` resolve and check `gates` / `pairs` in - `__init_subclass__`, so a mistyped gate name, a duplicate, a gate - defining neither (or both) rate forms, a transition naming a missing - rate method, and a `dependent_state` outside the state set are all - rejected when the class is created rather than at `reset_state()`. - `init_state` refuses to bind a gate over a non-`DiffEqState` - attribute, which used to silently replace a constructor parameter. -- [x] **Gate and transition rates carry real units.** `Gate.time_unit` - (default `u.ms`) says what a bare `f_*_tau` / `f_*_alpha` / `f_*_beta` - return means; a united return is used as given and a wrong dimension - is rejected against the gate by name. Markov transition rates accept - the same two forms against a fixed `u.ms`. Every state derivative is - asserted to be an inverse time before it reaches the integrator, - which is what catches a dimensioned `phi`. -- [x] **`OhmicHH` carries the ohmic driving force.** 63 channels that - restated `g_max * conductance_factor(...) * (E - V)` verbatim now - inherit it; a channel reading a fixed `self.E` overrides - `reversal_potential()`. GHK-flux and permeability-scaled channels - keep inheriting `HH` and writing their own `current()`. -- [x] **Gate metadata binds by attribute name.** `Gate(q10="q10")` - replaces the 75 `lambda self: self.q10` closures, which were - unpicklable and invisible to tooling. The callable form still works. -- [~] **Gate/state clipping is an explicit policy.** `Gate.clip` - defaults to `False` (NEURON does not clip HH gates, and the catalogue - is validated against those mechanisms); `Markov.clip_states` defaults - to `True`. Both project only the value fed to the conductance product - or the kinetics, never the stored state. Remaining gap: the implicit - `dependent_state` fallback still exists behind a `DeprecationWarning` - and is slated for removal. +- [x] **数据与状态分工**:形态和机制声明经 Cell 构建为运行时状态,Network 协调事件与推进,Trainable 管理参数映射。 [系统总览](architecture/current/system-overview.md) +- [~] **Python 支持覆盖**:现有 CI 主要测试 3.13,classifiers 声明 3.11 至 3.14;需要扩展测试矩阵或调整支持声明。 [版本覆盖](architecture/TODO.md#当前需要推进的事项) +- [ ] **公共接口命名**:统一声明类型与运行时基类的区分方式,并明确公共导出、内部路径及兼容策略。状态:**待讨论**。 [接口一致性](architecture/proposals/interface-consistency.md) -## 1. Mission and Scope +### Morph:形态构造与编辑 -BrainCell is a JAX-native library for **biologically detailed cell and network -modelling**. It targets the same workload as NEURON, Arbor, and BluePyOpt but -expresses models as differentiable, vectorized JAX programs so that -multi-compartment populations can be simulated, connected, batched, and -parameterized inside the broader `brain*` ecosystem (`brainstate`, -`brainunit`, `brainevent`, `braintools`, `brainpy`). +协作入口:[Morph TODO](morph/TODO.md)。 -The library owns seven concerns end-to-end: +- [x] **分支几何**:通过长度、半径或三维采样点构造 Branch,计算长度、面积和体积。 [Branch 几何](morph/current/api.md#branch-几何) +- [x] **树构建与连接**:构造 Morphology,在指定位置连接分支,保留父子关系和连接方向。 [树连接](morph/current/api.md#morphology-与连接) +- [x] **查询、统计与复制**:提供树遍历、分支视图、路径及几何指标;通过独立复制创建可修改的形态副本。 [查询与指标](morph/current/api.md#查询视图和指标) +- [ ] **子树编辑**:支持删除、拼接和替换子树,需确定分支身份、引用及连接方向的保持规则。状态:**待讨论**。 [编辑事项](morph/TODO.md#当前需要推进的事项) +- [ ] **几何变换**:支持平移、旋转、缩放和主轴对齐,并更新依赖几何的指标及派生缓存。状态:**待讨论**。 [变换事项](morph/TODO.md#当前需要推进的事项) +- [ ] **公共导出**:评估在顶层入口之外,同时从 morph 子包导出 Branch 和 Morphology 的收益与依赖影响。状态:**待讨论**。 [导出事项](morph/TODO.md#当前需要推进的事项) -1. **Morphology ingestion** — read SWC / ASC / NeuroML2, validate, cache. -2. **Geometry & discretization** — turn a morphology + a CV policy into - immutable control-volume (CV) arrays suitable for vectorized solvers. -3. **Mechanism declaration** — paint cable properties, density mechanisms, - and ion channels onto regions; place point mechanisms onto locsets. -4. **Runtime lowering** — initialize `Cell` with resolved ion species, - channel state, point-mechanism storage, and a DHS-ordered node tree. -5. **Numerical integration** — provide a registry of explicit, implicit, - exponential, and staggered step functions, including a custom DHS - voltage solver for branched cables. -6. **Network execution** — connect event sources to Cell-owned synapses, - schedule delayed delivery, and aggregate samples and sparse events. -7. **Parameterization** — expose selected physical fields through stable, - unit-aware trainable parameter mappings. +### IO:形态读写与外部数据 -Out of scope (for this iteration): a BrainCell-owned optimizer or Trainer, -plasticity learning rules, trainable topology, NEURON HOC compatibility, GUI -tools, and stand-alone NMODL execution. The previous `mech/nmodl/` research -tree has been removed; if NMODL support returns, it will be a separate codegen -design targeting the mechanism registry. +协作入口:[IO TODO](io/TODO.md)。 ---- +- [x] **SWC 读写与结构往返**:导入形态及诊断报告,处理 soma 和分支连接,并将树写回 SWC;已有共享端点、反向分支等回归。 [SWC API](io/current/api.md#swc-读取与检查) +- [x] **形态 checkpoint**:以自包含的 .bcm 格式保存和恢复形态,配有调用示例。 [Checkpoint](io/current/api.md#checkpoint) +- [x] **NeuroMorpho 检索与下载**:提供便捷加载、客户端检索、下载及缓存,返回形态和元数据。 [NeuroMorpho](io/current/neuromorpho.md) +- [~] **ASC 几何与标记覆盖**:已有树和元数据导入;spine、轮廓 soma 及多树案例仍需补齐处理与验证。 [ASC 覆盖](io/TODO.md#当前需要推进的事项) +- [~] **自动几何对照**:已有 NEURON 形态差异工具,下一步将 NeuroMorpho 指标比较整理为固定数据集、单位和容差的回归。 [对照事项](io/TODO.md#当前需要推进的事项) +- [ ] **NeuroML2 导入**:将 cell 和 segment-group 映射为 Morphology;当前 reader 仍是 stub,最小映射与验收样本待确定。状态:**待讨论**。 [导入事项](io/TODO.md#当前需要推进的事项) -## 2. Top-Level Architecture +### Filter:区域、位点与空间参数 -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ braincell.io │ -│ SWC / ASC / NeuroML2 readers · checkpoints · NeuroMorpho client │ -└─────────────────────────────┬────────────────────────────────────────┘ - │ Morphology - ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ braincell.morph │ -│ Branch (frozen) · Morphology (mutable tree) │ -└──────────────┬───────────────────────────────────┬───────────────────┘ - │ Morphology │ - ▼ ▼ -┌─────────────────────────────┐ ┌────────────────────────────────────┐ -│ braincell.filter │ │ braincell.mech │ -│ RegionExpr · LocsetExpr │ │ Mechanism · CableProperty │ -│ SelectionCache │ │ Density · Point · Junction │ -│ │ │ MechanismRegistry │ -└──────────────┬──────────────┘ └─────────────────┬──────────────────┘ - │ selection │ declarations - ▼ ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ braincell._discretization │ braincell._compute │ -│ CV/CVTree · policies │ layouts · bindings · scheduling │ -│ geometry · mechanism rules│ CellRuntimeState · bridge · table │ -├─────────────────────────────┴────────────────────────────────────────┤ -│ braincell._multi_compartment (Cell) │ -│ declaration + initialized runtime · views · run · recording │ -└──────────────────────────────┬───────────────────────────────────────┘ - │ HHTypedNeuron - ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ braincell.quad │ -│ IntegratorRegistry · explicit / implicit / exp_euler / staggered │ -│ steps · dhs_voltage_step (branched-cable Hines solver) │ -└──────────────────────────────┬───────────────────────────────────────┘ - │ DiffEqState - ▼ - brainstate / JAX execution -``` +协作入口:[Filter TODO](filter/TODO.md)。 -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ braincell.ion · braincell.channel · braincell.synapse │ -│ concrete Ion species (Na, K, Ca) · IonChannel implementations │ -│ (Na, K, Ca, Ih, K_Ca, leaky) · exponential synapse models │ -└──────────────────────────────────────────────────────────────────────┘ - (supply concrete mechanism objects consumed by mech.Density / - mech.Point declarations and installed inside braincell.Cell) -``` +- [x] **区域与集合运算**:按分支及形态标签选择区间,组合交、并、差,并缓存解析结果供 Cell 使用。 [区域 API](filter/current/api.md#区域) +- [x] **离散位点与采样批次**:选择根、末端、分叉或显式位置,支持区域内均匀及随机取点和批次组合。 [位点 API](filter/current/api.md#位点和批次) +- [x] **连续采样与空间参数**:按长度、面积、体积测度和 density 采样,空间 callable 可读取形态上下文与指标。 [连续采样](filter/current/sampling.md)、[空间参数](filter/current/spatial-callable-parameters.md) +- [ ] **半径、距离与子树区域**:扩展阈值切分、路径距离、欧氏距离及子树选择;需确定单位、边界和树修改后的缓存规则。状态:**待讨论**。 [区域扩展](filter/TODO.md#当前需要推进的事项) +- [ ] **区域锚点与固定步长取点**:实现 RegionAnchors 和 StepSamples 的区域相对位置、端点及重复值规则;两者当前均为预留表达式。状态:**待讨论**。 [位点扩展](filter/TODO.md#当前需要推进的事项) +- [ ] **随机流一致性**:让旧 RandomSamples 的 NumPy 局部流与 BrainState 随机上下文方案衔接。状态:**待讨论**。 [随机上下文](network/proposals/random-context.md) -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ braincell.vis │ -│ 2D / 3D scenes · matplotlib & PyVista backends · │ -│ region / locset / value overlays │ -└──────────────────────────────────────────────────────────────────────┘ - (consumes Branch / Morphology / Cell / RegionExpr / LocsetExpr) -``` +### Mech:机制声明与运行时契约 -The directional rule of thumb: -**`io -> morph -> {filter, mech} -> _discretization -> _compute -> Cell -> quad`**, -with `ion` / `channel` / `synapse` as peer top-level modules supplying concrete -mechanism implementations that `mech` wraps into `Density` (`Channel` / -`Ion`) and `Point` (`CurrentClamp`, `Synapse`, `Junction`, …) declarations -at paint/place time. `network` orchestrates initialized Cells and event -sources; `trainable` binds parameter sources into Cell-owned runtime fields; -`vis` reads anything from `morph` upward. Shared runtime bases live in -`_base_neuron`, `_base_ion`, and `_base_channel`. +协作入口:[Mech TODO](mech/TODO.md)。 ---- +- [x] **机制声明与注册**:将 Channel、Ion、Synapse 等模型注册为可解析声明,供 paint/place 构建实际机制。 [声明 API](mech/current/api.md) +- [x] **刺激与事件输入**:提供电流钳、电压钳和事件输入契约,将声明交给 Cell 和 Network 绑定执行。 [声明到运行时](mech/current/architecture.md) +- [~] **Junction 电耦合**:已有占位声明,仍缺 partner 身份、对称配对及电压方程中的电流贡献。 [接线缺口](mech/proposals/runtime-extensions.md) +- [ ] **参数单位诊断**:在构造或 paint 阶段定位错误参数、物理维度和声明位置,所需元数据与 Channel/Ion 共同设计。状态:**待讨论**。 [参数诊断](mech/TODO.md) +- [ ] **旧 Probe 迁移**:核对旧字段声明与现行 observe 的对应关系,确定错误校验和迁移方式。状态:**待讨论**。 [迁移事项](mech/TODO.md) +- [ ] **模型验证框架**:从现有 NEURON 比较例子提取可复用的电压钳、电流钳及误差验收流程。状态:**待讨论**。 [机制验证](mech/proposals/runtime-extensions.md#机制生成与验证) +- [ ] **NMODL 生成器**:研究以 registry 为目标的机制代码生成,先确定最小语法集和标准模板的映射。状态:**待讨论**。 [生成方向](mech/proposals/runtime-extensions.md#机制生成与验证) -## 3. Module Catalogue +### Channel:通道模板与模型目录 -Each subsection lists: **purpose · key types · public API surface · -internal dependencies · status · open work**. +协作入口:[Channel TODO](channel/TODO.md)。 -### 3.1 `braincell.morph` — morphology data model +- [x] **HH 与 Markov 模板**:统一门状态、转移、生命周期和速率单位,在类定义时检查名称、速率形式及依赖;Markov 必须显式指定 dependent_state。 [模板约束](channel/current/template-invariants.md) +- [x] **电流与裁剪策略**:提供 ohmic/GHK 驱动力、Q10 辅助函数及显式裁剪配置,复用各模型共有的计算。 [模板 API](channel/current/api.md) +- [x] **通道目录与注册**:提供钠、钾、钙、HCN、混合离子和漏通道家族,包含 PC MA2024 等导入模型及相邻测试。 [模型入口](channel/current/api.md) +- [~] **GHK 与温度审计**:模板和部分模型已采用共享驱动力及温度路径,仍需逐家族核对原模型要求与参数来源。 [审计事项](channel/TODO.md#当前需要推进的事项) +- [~] **逐模型精度与刚性验收**:在已有定向测试和模型比较基础上,补全目录级 MOD 对照与 dt/solver 收敛矩阵。 [验证事项](channel/TODO.md#当前需要推进的事项) +- [ ] **参数单位元数据**:确定参数维度的维护位置,与 Mech 的声明校验及错误定位衔接。状态:**待讨论**。 [元数据事项](channel/TODO.md#当前需要推进的事项) +- [ ] **门变量命名**:为 p/q 与模型自定义名称设计兼容路径,保留状态读写的可追溯关系。状态:**待讨论**。 [命名事项](channel/TODO.md#当前需要推进的事项) +- [ ] **氯通道**:在 Chloride 离子家族确定后,补齐相应电流与反转电位的模型。状态:**待讨论**。 [氯通道事项](channel/TODO.md#当前需要推进的事项) -- **Purpose** — owns the canonical in-memory representation of a neuron's - geometry. Splits cleanly into immutable per-branch geometry (`Branch`) - and a mutable owning tree (`Morphology`). -- **Key types** - - `Branch` (frozen dataclass) and typed subclasses `Soma`, `Dendrite`, - `Axon`, `BasalDendrite`, `ApicalDendrite`, `CustomBranch`. - Built via `Branch.from_lengths` / `Branch.from_points`. - - `branch_class_for_type(type_str)` factory used by IO readers. - - `Morphology` — mutable owning tree, root attachment, attribute-style - children (`morpho.soma.dendrite = ...`), `topo()` text rendering, - `branches`, `edges`, `branch_by_order`. - - `MorphoBranch` — node view exposing parent / children navigation. - - `MorphoEdge` — frozen, read-only directed edge between two - `MorphoBranch` nodes. - - `MorphoMetric` — frozen snapshot of `n_branches`, `total_length`, - `total_area`, `total_volume`, `max_path_distance`, - `max_euclidean_distance`, `max_branch_order`, range boxes, etc. -- **Status** - - [x] Branch geometry, area, volume, point/length constructors. - - [x] Morphology root construction, `attach`, sugar attribute API, - topology queries, `topo()` text tree. - - [x] `Morphology.from_swc` / `Morphology.from_asc` constructors. - - [x] `save_checkpoint` / `load_checkpoint` (`.bcm` self-contained - format) plus `pickle` / `copy.deepcopy` support. - - [x] `MorphoMetric` covering total length / area / volume, branch - order, path distance, Euclidean distance. - - [ ] **Tree editing primitives**: delete subtree, splice subtree, - merge two morphologies at a chosen attachment point, swap a branch - with another while preserving orientation. - - [ ] **In-place geometry transforms**: translate / rotate / scale / - align principal axis, with corresponding metric invalidation. -- **Open risks** - - Mutability of `Morphology` versus the immutability of `Branch` - (and downstream caches in `Cell`) makes accidental aliasing easy. - Tree-edit operations must follow the existing - `Morphology.clone()` discipline used by `Cell`. +### Ion:离子状态与浓度动力学 -### 3.2 `braincell.io` — file-format ingestion +协作入口:[Ion TODO](ion/TODO.md)。 -- **Purpose** — read morphologies from common neuroscience formats and - produce a `Morphology` plus a structured report describing parsing - decisions and validation issues. -- **Key types** - - `swc.SwcReader`, `SwcReadOptions`, `SwcReport`, `SwcIssue` plus - rulebook (`rules.py`) and soma reconstruction (`soma.py`). - - `asc.AscReader`, `AscReport`, `AscIssue`, `AscMetadata`. - - `neuroml2.NeuroMlReader`. - - `neuromorpho` package — three-tier NeuroMorpho.Org integration: - - Tier 1: `load_neuromorpho` (also re-exported as - `braincell.load_neuromorpho`), `fetch_neuromorpho`, and the - `Morphology.from_neuromorpho` classmethod sibling to `from_swc` / - `from_asc`. - - Tier 2: `NeuroMorphoClient` (typed `search` / `iter_search`, - `get_neuron`, `get_measurement`, `describe`, `download` with - `dry_run=True`, configurable `retries` / `backoff_base`). - - Tier 3: `NeuroMorphoCache`, `NeuroMorphoCacheLayout`, - `NeuroMorphoQuery`, `NeuroMorphoMeasurement`, `NeuroMorphoFilePlan`, - `NeuroMorphoUrls`, `NeuroMorphoCacheStatus`, - `NeuroMorphoSearchPage`, `NeuroMorphoDetail`, - `NeuroMorphoDownloadItem`, `NeuroMorphoDownloadRecord`, - `NeuroMorphoNeuron`, plus pure URL helpers - (`build_standard_swc_url`, `build_original_file_url`, - `infer_original_extension`, `plan_neuron_files`). - - Errors: `NeuroMorphoError`, `NeuroMorphoHTTPError`, - `NeuroMorphoNotFoundError`. - - `io.checkpoint` — `save_branch` / `load_branch` / - `save_morpho` / `load_morpho` and the `.bcm` single-file format. -- **Status** - - [x] SWC import + rulebook validation + report. - - [x] SWC export through `Morphology.to_swc()` / `swc.write_swc()`, - with structural round-trip coverage for branch endpoint duplication, - soma interior attachments, reversed branches, and invalid geometry. - - [~] ASC import: most Neurolucida trees, metadata, and - `Morphology.from_asc(..., return_report=True)` work; **gaps**: - spine markers, contour-only somas, and multi-tree files are still - handled minimally — see `io/asc/reader_test.py` skips. - - [ ] NeuroML2 import — reader stub exists; needs cell, segment-group, - biophysics decoding and round-trip tests. - - [x] NEURON-based diff harness via `examples/neuron_compare/morph/neuron_diff.py`. - - [x] NeuroMorpho.Org integration: Tier 1 `load_neuromorpho` / - `fetch_neuromorpho` one-liners, Tier 2 `NeuroMorphoClient` with - typed `iter_search` / `download` / retries, Tier 3 `NeuroMorphoCache` - plus pure URL helpers, full NumPy-doc docstrings, and - `Morphology.from_neuromorpho` classmethod. Notebook walkthrough at - `examples/multi_compartment/neuromorpho.ipynb` shows the full search → cache → - metric-diff loop. - - [ ] Automated metric diff against published NeuroMorpho reference - statistics promoted from the notebook into a pytest case (so the - NeuroMorpho corpus becomes a wide regression net). - - [x] Checkpoint API and `.bcm` format with notebook tutorial - (`examples/multi_compartment/morphology-checkpoint.ipynb`). - - [ ] **NMODL parsing compiler** — deferred. The previous - `mech/nmodl/` research tree has been removed from the working - copy; if NMODL support returns it will land as a codegen pass - targeting the mechanism registry (see §3.4 / M5 Phase 4). -- **Open risks** - - Format heterogeneity is the dominant source of bugs. Every reader - must produce a `Report` so user-facing tools can surface issues - instead of silently massaging geometry. +- [x] **固定值与 Nernst 模型**:提供钠、钾、钙等固定反转电位或浓度驱动的反转电位,以及共享的子通道生命周期。 [固定值与 Nernst](ion/current/api.md#固定值与-nernst-模型) +- [x] **KineticIon 反应模型**:声明物种、反应、source、factor 和守恒关系,接入 Cell 的浓度状态及电流输入。 [KineticIon](ion/current/kinetic-ion-api.md) +- [~] **动态钙浓度**:已有 Detailed、FirstOrder 和小脑动力学模型;CalciumFirstOrder 的默认 alpha/beta 缺少正确单位转换,仍不能完成对应导数路径。 [具体缺项](ion/current/api.md#动态钙浓度) +- [ ] **动态钠、钾浓度**:扩展内外浓度、泵和电流驱动模型,为活动依赖的离子积累提供状态方程。状态:**待讨论**。 [家族扩展](ion/TODO.md#当前需要推进的事项) +- [ ] **Chloride 家族**:确定固定与动态氯反转电位,以及 GABA 模型所需的浓度方程。状态:**待讨论**。 [氯离子事项](ion/TODO.md#当前需要推进的事项) +- [ ] **外部电流一致性**:逐模型核对 include_external、总电流缓存及电流到浓度导数的转换。状态:**待讨论**。 [电流审计](ion/TODO.md#当前需要推进的事项) +- [ ] **模型来源补全**:补齐共享文献表中未核实的归因和模型版本,为通道及离子对照提供依据。状态:**待讨论**。 [来源事项](ion/TODO.md#当前需要推进的事项) -### 3.3 `braincell.filter` — region & locset selection +### Cell:声明、离散与细胞运行 -- **Purpose** — declarative, composable selection of regions of a - morphology and points on it. The cell layer consumes these to map - user intent onto control volumes. -- **Key types** - - `RegionExpr` family: `BranchSlice`, `branch_in(...)` predicates for - branch metadata / topology, `branch_range(...)` for scalar branch - properties and metrics, set operations - (union / intersection / difference / complement). - - `LocsetExpr` family: root, branch points, terminals, region-driven - uniform sampling, region-driven random sampling. - - `SelectionCache` — memoizes resolved index sets for stable - Morphology objects. -- **Status** - - [x] BranchSlice, broadcasted inputs, set algebra. - - [x] Discrete predicates (type / name / branch_order / parent_id / - n_children / n_tapers / branch_id). - - [x] Continuous `branch_range(...)` with both numeric and `Quantity` - bounds. - - [x] Branch scalar metric filters: `length`, `mean_radius`, `area`, - `volume`. - - [ ] **Radius-range filter** (e.g., `radius_range(0.5*u.um, 2*u.um)`). - - [ ] **Path-distance filter** (graph distance from soma along the - tree). - - [ ] **Euclidean-distance filter** (3-D distance from a chosen - anchor point). - - [ ] **Subtree region** — everything reachable below a given branch - or locset; needs to interoperate with the planned - `Morphology` subtree-edit operations. - - [x] Locset: root, branch points, terminals. - - [x] Locset: uniform / random sampling driven by a region. - - [~] **Locset anchors and fixed-step sampling**: `RegionAnchors` and - explicit `at(branch, x)` locations are implemented; `StepSamples` - remains a reserved expression that raises `NotImplementedError`. -- **Open risks** - - The reserved distance/radius/subtree expressions must reuse the existing - morphology spatial metrics and `SelectionCache`; they must not introduce - a second geometry cache with different invalidation semantics. +协作入口:[Cell TODO](cell/TODO.md)。 -### 3.4 `braincell.mech` — mechanism declarations +- [x] **声明与空间离散**:通过 morphology、CVPolicy 和 paint/place 构建电缆、密度机制与点机制布局。 [构造与 CVPolicy](cell/current/api.md#cell-构造) +- [x] **CV 与 point 状态**:密度机制使用 CV 空间,点机制使用 point 空间;population 轴支持多维形状,并保留末尾空间轴。 [状态布局](cell/current/architecture.md#状态布局) +- [x] **生命周期与直接运行**:Cell 直接初始化、重置和按固定步长推进,支持连续运行及带时间范围的结果。 [生命周期](cell/current/api.md#生命周期)、[运行与结果](cell/current/api.md#运行与结果) +- [x] **视图、观测与状态读写**:按 population、区域和机制选择状态,读取轨迹或 buffer,并记录固定步长刺激结果。 [Views](cell/current/views.md)、[状态查询](cell/current/api.md#运行时查询) +- [x] **离子电流快照与调度**:staggered 可读取步首总离子电流,并选择 family 或 integration 的机制更新顺序。 [调度契约](cell/current/architecture.md#离子电流快照与调度) +- [~] **显式 solver 的边界输入**:显式路径已推进 CV 电压,消元时仍遗漏端点刺激和突触的等效贡献;修复方案正在讨论。 [边界输入](cell/proposals/explicit-solver-boundary-inputs.md) +- [ ] **Single 与多室统一**:从单 branch、特殊单 CV policy 和位点语义开始,使单方程 ODE 模型兼容 Cell;形态等效与积分路径仍需比较。状态:**讨论中**。 [统一提案](cell/proposals/single-multi-compartment-unification.md) +- [ ] **生命周期与查询风格**:明确 reset/reset_state 的命名,并统一缓存查询与触发构建操作的表达。状态:**待讨论**。 [接口事项](cell/TODO.md#当前需要推进的事项) -- **Purpose** — strongly-typed, purely-declarative containers used by - the `Cell` frontend. Everything here describes *what to install*, not - *how to integrate*: no `brainstate`, no JAX, no runtime state. The - concrete ion species, ion channels, and synapses live in peer - top-level modules (`braincell.ion`, `braincell.channel`, - `braincell.synapse`) and register themselves with the - `MechanismRegistry` at import time via class-level decorators; the - runtime lowering in `braincell._compute` resolves a - `Density.class_name` through the registry when it installs channels - on a cell. -- **Key files & types** - - `mech/_base.py` — `Mechanism` marker base class. Every mechanism - declaration (density or point) inherits from it, so consumers can - check `isinstance(x, Mechanism)` without having to know whether - they hold a `Density` or a `Point`. - - `mech/_registry.py` — `MechanismEntry(category, name, cls, - aliases)` frozen dataclass, `MechanismRegistry` with - `register` / `unregister` / `add_alias` / `contains` / `get` / - `entry` / `names` / `items` / `clear`, the `_REGISTRY` singleton - accessed via `get_registry()`, and the three class-level - decorators `register_channel` / `register_ion` / - `register_synapse`. Unknown-name lookups raise `KeyError` with a - `difflib`-based "did you mean ...?" suggestion (same pattern as - `braincell.quad._registry`). Three valid categories: - `"channel"`, `"ion"`, `"synapse"`. - - `mech/_params.py` — `Params(Mapping[str, Any])` frozen hashable - mapping. `__hash__` uses `frozenset(self._items.items())`, so - `Channel("IL", g_max=..., E=...)` and `Channel("IL", E=..., - g_max=...)` deduplicate into a single paint-layout group. Iteration - order is the declared order so `repr()` is stable. Accepts - `Mapping`, `(k,v)` tuples, or another `Params` in the constructor - (`Params.coerce(value)`), supports `**params` unpacking via the - `Mapping` protocol, and exposes non-mutating `with_updates(...)` / - `without(...)`. - - `mech/_density.py` — `Density(Mechanism)` abstract base plus the - concrete subclasses `Channel(Density)` and `Ion(Density)`. `Density` - is a manually-immutable `__slots__` class (not a dataclass) with a - `category: ClassVar[str]` discriminator set by each subclass - (`"channel"` / `"ion"`). The constructor accepts `class_name` as - either a string **or** a class (`braincell.channel.IL`); types are - resolved to their canonical registry name via reverse lookup. - `coverage_area_fraction` is a dedicated first-class field, not a - pseudo-parameter. `instance_name` falls back to `class_name`, - `identity = (instance_name, class_name)` drives paint-layout - grouping, and `with_params(...)` / `with_coverage(...)` / - `with_name(...)` return non-mutating copies via an internal - `object.__new__` + `object.__setattr__` bypass. `Channel` and - `Ion` collect parameters via `**params` kwargs. - - `mech/_point.py` — `Point(Mechanism)` plain base class (not a - `Union`; use `isinstance(x, Point)` in consumers) plus concrete - frozen-dataclass subclasses `CurrentClamp`, `SineClamp`, - `FunctionClamp`, `ProbeMechanism`, and `Synapse`. `CurrentClamp` - has one canonical form `(start, durations, amplitudes)` and a - `CurrentClamp(delay=..., durations=duration, amplitudes=amplitude)` classmethod - shortcut. `Synapse` is itself a frozen dataclass - (`synapse_type`, `params`, `name`); there is no separate factory - function. - - `mech/_junction.py` — `Junction(Point)` frozen dataclass for - gap-junction coupling declarations. Placeholder implementation - (`params` field only); lives in its own module so downstream - work on gap-junction state and partner wiring has a clean home. - - `mech/_cable.py` — `CableProperty` frozen dataclass - (`resting_potential`, `membrane_capacitance`, `axial_resistivity`, - `temperature`, all `brainunit` quantities; temperature defaults to - 36 °C via a `default_factory` and is coerced to kelvin in - `__post_init__`). Exposes non-mutating `with_updates(**kwargs)`. - - `mech/__init__.py` — re-exports the public surface - (`Mechanism`, `Density`, `Channel`, `Ion`, `Point`, `CurrentClamp`, - `SineClamp`, `FunctionClamp`, `ProbeMechanism`, `Synapse`, - `Junction`, `CableProperty`, `Params`, registry API). - - Co-located tests: `_base_test.py`, `_registry_test.py`, - `_params_test.py`, `_density_test.py`, `_point_test.py`, - `_junction_test.py`, `_cable_test.py`. -- **Status** - - [x] `CableProperty`, `Density` (with `Channel` / `Ion` - subclasses), and the full `Point` family (`CurrentClamp`, - `SineClamp`, `FunctionClamp`, `ProbeMechanism`, `Synapse`, - `Junction`) with `brainunit`-typed fields and co-located tests. - Everything inherits from a shared `Mechanism` marker base class. - - [x] **One type per concept.** The legacy `MechanismSpec` / - `DensityMechanism` duality and the eight `density_*` isinstance- - dispatch helpers in `spec.py` are gone. Every density declaration - is a `Density` subclass (`Channel` or `Ion`) carrying a - `category` `ClassVar`; every point declaration is a `Point` - subclass. - - [x] **Class-based `Channel` / `Ion`.** `braincell.mech.Channel` - and `braincell.mech.Ion` are real classes (not factory functions) - inheriting from `Density`. They accept the target class as either - a string name (`"IL"`) or the concrete class object - (`braincell.channel.IL`); the class form is reverse-looked-up in - the registry to produce the canonical name so aliases continue to - collapse into one identity. Top-level `braincell.Channel` / - `braincell.Ion` still point at the runtime base classes from - `_base_channel.py` / `_base_ion.py`; the declaration-layer classes are - reached via - `braincell.mech.Channel` / `braincell.mech.Ion` to avoid the - name collision. - - [x] **Mechanism registry.** `MechanismRegistry` + the - `@register_channel` / `@register_ion` / `@register_synapse` - decorators ship in `mech/_registry.py`. ~49 concrete classes in - `braincell.channel`, `braincell.ion`, and `braincell.synapse` - self-register at import time. `get_registry().get(category, - class_name)` is the single lookup path used by - `_compute/parameters.py` and `_compute/bindings.py` to resolve - `Density.class_name` into a - runtime class. Channel-to-ion binding is inferred from - `issubclass(cls.root_type, Sodium / Potassium / Calcium)`, not - from hardcoded class-name matching. Abstract base classes - (`LeakageChannel`, `SodiumChannel`, `Calcium`, …) are deliberately - **not** decorated. - - [x] **Hash-stable Params.** `Params.__hash__` uses - `frozenset(items)` so two `Channel(...)` calls with the same - parameters in different keyword order compare equal and - deduplicate into the same paint-layout group. Only `params` is - hash-insensitive; `class_name`, `name`, `category`, and - `coverage_area_fraction` remain position-sensitive. - - [x] **`coverage_area_fraction` as a first-class field** on - `Density`. The old abstraction leak where coverage was smuggled - through ordinary mechanism parameters is gone; `_discretization` - and `_compute` preserve it as geometry metadata. - - [x] **Unified `CurrentClamp`.** One canonical frozen-dataclass - form `(delay, durations, amplitudes)`. The old - `CurrentClamp(amplitude=, delay=, duration=)` compatibility form - is gone; use `CurrentClamp(delay=..., durations=duration, amplitudes=amplitude)`. - - [x] **Consumer simplification.** `_discretization/mechanism.py` and - the `_compute` layout, binding, parameter, and table modules operate - directly on the declaration types without a parallel spec hierarchy. - - [ ] **Parameter-unit validation** — `Params` currently stores - values untyped. Needs compile-time validation that each value - carries the brainunit dimension the target channel declares - (e.g. `g_max` must be in `S/cm²`, `E` in `mV`), with an error - that points at the offending `paint(...)` call. The infrastructure - for this lives on the mechanism registry: each entry can declare - the expected unit per parameter name. - - [ ] **`Junction` runtime wiring** — `Junction` currently ships - as a placeholder frozen dataclass with only a `params` field. - It needs a `partner` reference (locset or another placed - `Junction`), symmetric pair resolution in the runtime, and a - gap-junction current contribution in the voltage solve. Tracked - as the first sub-task in milestone M5 Phase 3. - - [ ] **`ProbeMechanism` variable taxonomy** — `variable` is - currently a free-form string. Promote it to a typed enum of known - probes (`"v"`, `"ina"`, `"ik"`, `"ica"`, `"cai"`, `"cao"`, - channel gate names, …) so user typos fail at declaration time - rather than silently producing empty traces. - - [ ] **Mechanism validation harness** — a structured comparison - against NEURON `.mod` reference traces for every channel in - `braincell.channel`. The previous `mech/mod_validate/` tree has - been removed from the working copy; the harness needs to be - re-introduced as a package under `braincell/mech/` (or a sibling - test package) and promoted to automated pytest cases. Tracked in - milestone M5. - - [ ] **NMODL ingestion** — deferred. If NMODL support returns it - must target the mechanism registry so generated channels land - under the standard naming convention in `braincell.channel` - rather than creating a parallel hierarchy. -- **Open risks** - - **Hash-insensitive `Params` equality** only kicks in for the - `params` field; `class_name`, `name`, `category`, and - `coverage_area_fraction` stay position-sensitive. Do not extend - the hash-insensitive treatment to other fields without first - understanding the paint-layout grouping contract in - `_discretization/mechanism.py`. - - **Class-level decorator ordering.** Registration is a side - effect of importing `braincell.channel` / `braincell.ion` / - `braincell.synapse`. If a user imports `braincell.mech` alone - (without importing the concrete modules) the registry is empty — - by design. The canonical entry points in `braincell/__init__.py` - already import all three, so normal users never see this. - - **Ion binding inference** uses - `issubclass(cls.root_type, Sodium/Potassium/Calcium)` in - `_compute/bindings.py`. New ion species must either set - `root_type` on their channels or we extend the dispatch to walk - a lookup table — do not hardcode class-name matching. - - **Name collision with runtime `Channel` / `Ion` bases.** The - declaration-layer `Channel` / `Ion` classes live under - `braincell.mech`, not at the top level of `braincell`, because - `braincell.Channel` / `braincell.Ion` already resolve to the - runtime base classes from `_base_channel.py` / `_base_ion.py`. - Tutorials and user code - should use the fully-qualified `braincell.mech.Channel` / - `braincell.mech.Ion` when declaring mechanisms on a `Cell`. - - The module is intentionally free of `brainstate` / JAX state — - keeping `mech` purely declarative makes importing `braincell.mech` - cheap and keeps the declaration frontend usable even in - environments where the numerical runtime is absent. Do not - import `brainstate`, `jax`, or any concrete channel/ion/synapse - class inside `braincell/mech/`. The one permitted dynamic - import is inside `_density._resolve_class_name`, which consults - the registry via a lazy `from ._registry import get_registry` - local import when a user passes a class object instead of a - name string. +### Quad:积分方法与电压求解 -### 3.5 `_discretization` / `_compute` / `_multi_compartment` — Cell runtime +协作入口:[Quad TODO](quad/TODO.md)。 -- **Purpose** — turn *(Morphology, CVPolicy, paint/place declarations)* - into an initialized, directly runnable `Cell(HHTypedNeuron)`: - - `braincell._discretization` owns immutable CV geometry, policies, - mechanism rules, `CVTree`, and declaration-time `NodeTree` data. - - `braincell._compute` owns runtime layouts, bindings, CV/point bridges, - scheduling, tables, and `CellRuntimeState`. - - `braincell._multi_compartment` owns `Cell`, its spatial and mechanism - views, clamps, synapses, probes, and `RunResult`. -- **Status** - - [x] `Cell(morpho, pop_size=..., cv_policy=...)`, `paint`, and `place` - form the declaration phase; declarations freeze after initialization. - - [x] `Cell.init_state()` lowers the declaration and installs runtime - state on the same object. `Cell.run(dt=..., duration=...)` advances it - directly; there is no public build phase or `RunnableCell`. - - [x] CV policies, geometry, axial-resistance partitioning, mechanism - lowering, point topology, DHS scheduling, and CV/point conversion. - - [x] Homogeneous populations with mandatory population axes and - multi-dimensional `pop_size`. - - [x] Cell, Channel, Ion, Synapse and Clamp views with Cell-owned - connection, recording, and trainable-parameter storage. - - [x] Fixed-step clamps retain their exact continuous interval at runtime; - density parameters are materialized on CVs rather than non-CV points. - - [x] NEURON-compatible ion-current snapshots and selectable - `"family"` / `"integration"` ion-channel update ordering. -- **Open risks** - - Declaration shapes and ownership must remain fixed after - `init_state()` so JIT state trees and network routing stay stable. - - Parameter materialization may change values without changing runtime - layout, topology, units, or state shape. +- [x] **积分注册与通用 ODE**:提供显式 RK、隐式和指数方法,通过统一步函数及阶段协议推进模型状态。 [积分 API](quad/current/api.md) +- [x] **Staggered 与 DHS**:按机制和电压阶段推进,使用树结构求解包含边界与分叉约束的电缆系统。 [求解架构](quad/current/architecture.md) +- [~] **精度与性能对照**:已有算法回归、收敛测试及电缆数值对照,仍缺 Mainen/Hay/L5PC 等标准模型上的统一精度、编译和计时比较。 [对照事项](quad/TODO.md) +- [~] **显式边界输入**:已有 CV 导数推进,仍需与 Cell 共同补入边界刺激和突触反馈;修复方案正在讨论。 [边界方案](cell/proposals/explicit-solver-boundary-inputs.md) +- [ ] **Single 积分路径**:在 single 统一中比较共享边界装配与独立 ODE 积分,核对等价性和 solver 一致性。状态:**讨论中**。 [路径方案](cell/proposals/single-multi-compartment-unification.md) +- [ ] **自适应步长**:基于 embedded RK 误差估计推进,同时处理事件时间和记录采样对齐。状态:**待讨论**。 [步长事项](quad/TODO.md) -### 3.6 `braincell.quad` — numerical integrators +### Synapse:突触动力学与状态 -- **Purpose** — provide a uniform registry of step functions over - `DiffEqModule` targets, plus the specialized branched-cable voltage - solver. -- **Key types** - - `IntegratorRegistry`, `IntegratorEntry`, `register_integrator`, - `get_registry`, `get_integrator`. Decorator-based registration with - canonical name, aliases, category, order, description, deprecation. - - `_RegistryDictView` exposes a read-only `all_integrators` mapping - for legacy callers. - - `DiffEqModule`, `DiffEqState`, `IndependentIntegration` — - structural protocols and helpers for step functions. - - **Explicit families**: `euler_step`, `rk2/3/4_step`, `heun2/3_step`, - `midpoint_step`, `ralston2/3/4_step`, `ssprk3_step`. - - **Implicit / mixed**: `backward_euler_step`, `implicit_euler_step`. - - **Exponential Euler**: `exp_euler_step`, `ind_exp_euler_step`. - - **Staggered**: `staggered_step` (DHS voltage solve + - `ind_exp_euler` for ion-channel state, the workhorse for full - cells). - - **Voltage solvers**: `dhs_voltage_step` (DHS branched Hines), - `dense_voltage_step`, `sparse_voltage_step`. -- **Status** - - [x] Registry, alias resolution, "did you mean ...?" suggestions. - - [x] Backwards-compatible `all_integrators` mapping view. - - [x] All explicit RK / Heun / Ralston / Midpoint / SSPRK families. - - [x] Backward Euler and implicit Euler. The six cell-only variants - (`implicit_rk4`, `implicit_exp_euler`, `cn_rk4`, `cn_exp_euler`, - `exp_exp_euler`, `splitting`) were removed: they had rotted against - several `brainstate` / `Cell` API generations and none could be - invoked successfully. `braincell/quad/_implicit_test.py` pins their - absence from the registry. - - [x] Exponential Euler (`exp_euler_step`, `ind_exp_euler_step`). - - [x] Staggered solver (`staggered_step`). - - [x] The staggered full-cell path calls - `cache_ion_total_currents(...)` when the target supports it, so - NEURON-compatible ion-current snapshot semantics can be selected at - the `Cell` level without changing the integrator API. - - [x] DHS voltage solver (`dhs_voltage_step`). - - [ ] **Adaptive timestep wrapper** that produces a registered - integrator from any embedded RK pair. - - [x] **Convergence test matrix** — pytest-driven order-of-accuracy - checks for every registered integrator on a small set of - reference ODEs (passive cable, single HH spike, two-branch Y). - - [ ] **Performance benchmarks** vs NEURON / Arbor on the standard - Mainen / Hay / L5PC cells, run nightly via `CI-daily.yml`. +协作入口:[Synapse TODO](synapse/TODO.md)。 -### 3.7 `braincell.vis` — visualization +- [x] **指数突触模型**:ExpSyn 和 Exp2Syn 提供事件驱动的电导动力学、电流计算及状态重置。 [模型 API](synapse/current/api.md) +- [x] **Cell-owned 突触状态**:突触状态由目标 Cell 持有,连接投递事件并乘权,空间布局与生命周期由 Cell 管理。 [状态与事件](synapse/current/architecture.md) +- [ ] **内部动力学可塑性**:通过新的 Synapse 模型表达释放或内部状态的可塑变化,与只更新 Connection weight 的规则区分。状态:**讨论中**。 [可塑性方案](network/proposals/connection-plasticity.md) +- [ ] **模型验证覆盖**:在已有初始化、衰减和事件测试之外,扩展事件序列、时间常数及电压驱动力的参照对照。状态:**待讨论**。 [验证事项](synapse/TODO.md) -- **Purpose** — render morphologies and cell-level data with both an - interactive 3D backend (PyVista) and a static / publication 2D - backend (matplotlib), plus a dependency-light Plotly backend for - interactive notebook 3D without VTK. -- **Key types and files** - - `scene.py` — frozen dataclass primitives (`Polyline2D`, `Polygon2D`, - `Circle2D`, `Label2D`, `BranchPolyline3D`, `BranchTypeBatch3D`), - `RenderScene2D` / `RenderScene3D` containers, `RenderRequest`, - `OverlaySpec`. - - `scene2d.py`, `scene3d.py` — scene builders that strip brainunits - (`.to_decimal(u.um)`) and translate morphology + layout into - primitive tuples. - - `plot2d.py`, `plot3d.py` — high-level user entry points. - - `backend.py` — `RenderBackend` Protocol + `BackendChooser`. - - `backend_matplotlib.py`, `backend_pyvista.py`, `backend_plotly.py` — - concrete backends with lazy optional imports. The matplotlib - backend attaches per-artist pick metadata; the PyVista backend - attaches a point→branch lookup so `enable_point_picking` can - resolve clicks. - - `hooks.py` — `VisHooks(on_pick=..., on_hover=..., on_leave=...)` - plus the `PickInfo` payload delivered to user callbacks - (backend-agnostic; wired in both matplotlib and PyVista). - - `export.py` — unified `save_figure(figure, path, dpi=..., transparent=...)` - that dispatches on matplotlib `Axes`/`Figure`, pyvista `Plotter`, - or plotly `Figure`. - - `compare.py` — generalized `compare_morphologies([m1, m2, ...])` and - `compare_values(morpho, [values_a, values_b, ...])` side-by-side - helpers built on top of `plot2d`. - - `pytest-benchmark` baselines for layout build, scene build, and - end-to-end plot2d render on 50 / 500 / 2000-branch synthetic - morphologies, skipped when `pytest-benchmark` is not installed. - Filed with the module each measures: `layout/_dispatch_test.py`, - `scene2d_test.py`, `plot2d_test.py`. - - `layout/` — 2D tree-layout engine split across - `_common.py` (shared dataclasses + tree helpers), - `_geometry.py` (pure-numeric sampling and branch construction), - `_collision.py` (spatial-hash collision scoring), - `_config.py` (`LayoutConfig` frozen dataclass, the tunable - knobs), `_cache.py` (`LayoutCache` LRU keyed on a morphology - snapshot plus the layout config), `_stem.py` / `_balloon.py` / - `_radial.py` / `_legacy.py` (layout families), and `_dispatch.py` - (`build_layout_branches_2d` entry point, cache-aware). Each file - ships with a sibling `*_test.py`. - - `compare2d.py` — side-by-side comparison of layout families on the - same morphology (legacy, specific to layout-family gallery). - - `config.py` — `VisDefaults` dataclass singleton plus - `configure_defaults` / `get_defaults` / `reset_defaults`, - `theme(**overrides)` scoped context manager, and - `PublicationTheme` / `publication_theme()` which flips both vis - defaults and matplotlib `rcParams` for LaTeX-friendly output. - - `_values.py` — colour-by-values normalisation (per-branch / - per-segment / per-centerline-point → per-point scalar arrays) - plus :mod:`brainunit` unit-label extraction. - - `movie.py` — `plot_movie` time-varying colour-by-values - animation (matplotlib `FuncAnimation` + pyvista - `Plotter.open_movie`). - - `traces.py` — `plot_traces` morphology-synchronized time-series - panels. - - `morphometry.py` — `plot_dendrogram`, `plot_topology`, - `plot_sholl`, `plot_branch_order_histogram`, and the - `compute_sholl_profile` / `ShollProfile` helpers. - - `_testing.py` — shared morphology builders, the `FakeBackend` - scene-capturing double, `VisDefaultsResetMixin`, and the - `PYTEST_BENCHMARK_AVAILABLE` plugin probe. -- **Status** - - [x] 3D rendering of `Branch` / `Morphology` with point geometry, - scene composition, PyVista backend. - - [x] 2D projected mode driven by real points. - - [x] 2D tree auto-layout. - - [x] 2D frustum auto-layout. - - [x] Stem / balloon / radial360 layout family with matplotlib - comparison output. - - [x] `OverlaySpec` plumbed end-to-end for `region` / `locset` / - `values`, with per-CV value colormaps, locset scatter markers, - and region recolor passes consumed by both backends. - - [x] `RenderRequest` uses a neutral `backend_options` mapping; - backend-specific kwargs no longer pollute the shared schema. - - [x] Backend capability registry via `supported_scene_kinds: - frozenset[str]` so a future backend can declare multi-format - support. - - [x] `plot3d(mode="skeleton")` fast-preview path (centerline-only, - no tube generation) alongside the default `"geometry"` mode. - - [x] `RenderScene2D.draw_order` honored by the matplotlib backend - (primitives sorted by draw_order → `zorder=` argument). - - [x] `braincell.vis.theme(**overrides)` context manager for scoped - style overrides; tests no longer need manual `reset_defaults()`. - - [x] Shared `vis/_testing.py` helpers and parametrized layout-family - tests covering the shared invariants across stem / balloon / - radial_360. - - [x] **`layout2d.py` refactor** into `vis/layout/` with separate - files for `_common.py`, `_dispatch.py`, `_stem.py`, `_balloon.py`, - `_radial.py`, `_legacy.py`, `_collision.py`, `_geometry.py`, - and a `_config.py` holding the `LayoutConfig` frozen dataclass - (M6 Phase 2). The legacy family now emits a `DeprecationWarning`, - the collision backend uses a 2D spatial hash, and `plot2d` - accepts `layout_config=` as an optional user knob. - - [x] **Color-by-values** for 2D and 3D scenes: accept per-branch / - per-segment / per-centerline-point scalars. The matplotlib - backend uses vectorized `LineCollection` / `PolyCollection` - (10–50× speedup on dense scenes), the PyVista backend writes - `polydata.point_data["values"]` and calls - `add_mesh(scalars=..., cmap=..., scalar_bar_args=...)`. Proper - colorbars with unit labels, plus `vmin` / `vmax` / `cmap` / - `norm` surfaced through `plot2d` / `plot3d` (M6 Phase 3). - - [x] **`plot_movie`** — time-varying values over a morphology - using matplotlib `FuncAnimation` (2D) or - `pyvista.Plotter.open_movie` (3D). The 2D path builds the scene - once and mutates the `LineCollection` / `PolyCollection` scalar - array per frame; the 3D path rewrites - `polydata.point_data["values"]` and writes one frame per - timestep. - - [x] **`plot_traces`** — stacked time-series panels at `locset` - locations, color-synced with markers on a left-hand morphology - view (optional). - - [x] **Morphometry / topology plots**: `plot_dendrogram`, - `plot_topology`, `plot_sholl` (with `compute_sholl_profile` and - `ShollProfile` helpers), `plot_branch_order_histogram`. - - [x] **Layout caching** — `LayoutCache` LRU keyed on a stable - morphology snapshot plus the `LayoutConfig` hash. The - dispatcher consults `get_default_layout_cache()` on every call; - callers can pass a scoped `cache=LayoutCache(...)` or opt out - with `use_cache=False`. - - [ ] **Visual regression tests** — the `pytest-mpl` suite was - removed in 2026-08. Its baseline directory was never committed and - CI never passed `--mpl`, so no comparison had ever run; the 12 - tests were figure constructors with no assertions. Eight duplicated - existing coverage and were dropped, four were rewritten as real - matplotlib-artist assertions in `backend_matplotlib_test.py`. See - `docs/specs/2026-08-19-vis-baselines-and-coverage-gaps.md`. - Reinstating pixel regression needs committed baselines plus a - Linux-only CI job that actually passes `--mpl`. - - [x] **Generalized comparison**: `compare_morphologies([m1, m2, ...])` - and `compare_values(morpho, [values_a, values_b, ...])` in - `vis/compare.py` (M6 Phase 4). - - [x] **Interactivity**: `VisHooks(on_pick=, on_hover=, on_leave=)` + - `PickInfo` in `vis/hooks.py`. The matplotlib backend attaches - per-artist pick metadata and wires `pick_event` / - `motion_notify_event` handlers; the PyVista backend builds a - point→branch lookup and calls `enable_point_picking` - (M6 Phase 4). - - [x] **Plotly backend**: `backend_plotly.py` renders value scenes - as `Scatter3d` traces with per-point `line.color` / `colorscale` - and a shared scalar bar; gated on - `importlib.util.find_spec("plotly")` so the base install stays - dependency-free (M6 Phase 4). - - [x] **Export polish**: unified `save_figure(figure, path, ...)` in - `vis/export.py` that dispatches on matplotlib `Axes`/`Figure`, - pyvista `Plotter`, or plotly `Figure`; `PublicationTheme` preset - plus `publication_theme()` context manager in `config.py` that - flips both vis defaults and matplotlib `rcParams` (serif font, - thicker lines, no grid, print-friendly palette) (M6 Phase 4). - - [x] **Performance baselines** via `pytest-benchmark`, co-located - with the modules they measure (`vis/layout/_dispatch_test.py`, - `vis/scene2d_test.py`, `vis/plot2d_test.py`) — layout build, scene - build, and plot2d render on 50 / 500 / 2000-branch synthetic - morphologies, skipped when the plugin is absent (M6 Phase 4). - - [x] **Narrative tutorial**: `examples/multi_compartment/vis.ipynb` — quick start, - layout gallery, styling/themes, color-by-values, overlays, movie, - trace panels, morphometry, interactivity, publication export, - comparison (M6 Phase 4). - - [x] **Sphinx autodoc wiring**: `docs/apis/vis.rst` exposes the - whole public surface (plot entry points, morphometry helpers, - comparison helpers, hooks, themes, layout engine) through - `autosummary` and is linked from `docs/index.rst` (M6 Phase 4). -- **Open risks** - - The stem layout family still holds the most bug-prone code - (heuristic collision avoidance, the multi-weight scoring - function). After the Phase 2 split it lives in `vis/layout/_stem.py` - but remains the largest file in the package. Tuning individual - scoring weights now goes through `LayoutConfig` rather than - editing module-level constants, which makes experiments safer. - - Optional dependencies (`matplotlib`, `pyvista`, `plotly`, - `pytest-benchmark`) must stay lazy-imported inside - the backend that uses them. The import-time test from §4.5 / - risk #5 should grow to assert that none of the heavy optional - deps are loaded after `import braincell.vis`. - - `VisHooks` on the matplotlib backend relies on `pick_event` and - `motion_notify_event`, which only fire with an interactive - matplotlib backend. Notebook users should pick a GUI backend - (e.g. `%matplotlib widget`) — the Agg backend used in tests - will register the handlers but never deliver events, which the - tests explicitly cover. +### Network:组网、事件与结果 -### 3.8 `braincell.ion` — ion species +协作入口:[Network TODO](network/TODO.md)。 -- **Purpose** — concrete `Ion` subclasses modelling intra/extracellular - concentration, reversal potential, and the container of ion-bearing - channels that consume the species' `IonInfo`. Lives as a peer - top-level module (not under `mech`) because the classes are runtime - objects with JAX state, not declarations. -- **Key files & types** - - `braincell/ion/_base.py` — reusable `FixedIon`, `InitNernstIon`, - `DynamicNernstIon`, and `KineticIon` lifecycle templates. - - `braincell/ion/sodium.py` — `Sodium` (abstract base with - `root_type = HHTypedNeuron`), `SodiumFixed`, and `SodiumInitNernst`. - - `braincell/ion/potassium.py` — `Potassium` abstract base and - fixed and initialized-Nernst variants. - - `braincell/ion/calcium.py` — `Calcium` base class, - fixed/initialized-Nernst variants, and two concrete dynamics models: - - `CalciumDetailed` — Destexhe et al. 1993 thin-shell model with - tunable `d`, `tau`, `C_rest`, `C0`, `T`. - - `CalciumFirstOrder` — Bazhenov et al. 1998 first-order pool - (`Ca' = α I_Ca − β Ca`). - Both expose `C` as a `DiffEqState`, compute the Nernst reversal - `E = (RT/2F) log(C0/C)` as a property, and forward - `compute_derivative` to every attached `Channel` child. - - Co-located tests: `sodium_test.py`, `potassium_test.py`, - `calcium_test.py`. -- **Status** - - [x] `SodiumFixed` / `PotassiumFixed` / `CalciumFixed` parameter - storage, container (`**channels`) attachment, and `pack_info()` - returning an `IonInfo(C, E)` tuple. - - [x] `CalciumDetailed` / `CalciumFirstOrder` with Nernst reversal - and full derivative wiring to child calcium channels. - - [x] `KineticIon`-based Cerebellum calcium-pool mechanisms imported - for the current comparison work, including `CdpStC_MA2020_GoC`, - `CdpStC_NoCAM_MA2020_GoC`, `CdpStC_CAMOnly_MA2020_GoC`, - `CdpStC_MA2025_BC`, `CdpStC_RI2021_SC`, `CdpCAM_MA2024_PC`, and - `CdpCR_MA2020_GrC`. - - [x] Co-located unit tests (~75) covering defaults, custom - parameters, callable broadcasts, `init_state` / - `reset_state` / `compute_derivative`, `pack_info`, - external-current registration, Nernst formula edge cases, and - child-channel forwarding. - - [ ] **`SodiumDetailed` / `SodiumFirstOrder`** — activity- - dependent Na⁺ accumulation (e.g., for spike-frequency adaptation - driven by a Na/K pump). Parallel to the calcium dynamics pair - and needed to reproduce several of the published cortical - models in `examples/`. - - [ ] **`PotassiumDetailed` / `PotassiumFirstOrder`** — activity- - dependent intracellular / extracellular K⁺ accumulation for - network-level effects and K-pump dynamics, with the same - Nernst-reversal property as the calcium path. - - [ ] **`Chloride` ion** (`Chloride`, `ChlorideFixed`, - `ChlorideDynamics`) in a new `braincell/ion/chloride.py` plus a - sibling `chloride_test.py`. Needed for quantitative GABAa - modelling and developmental E_Cl shifts. - - [x] **Shared ion lifecycle templates** — package-private `FixedIon`, - `InitNernstIon`, `DynamicNernstIon`, and `KineticIon` mixins own the - reusable initialization, reversal, and kinetics contracts. - - [x] **`__init__.py` hygiene** — ion and channel re-export sets are - explicit, deduplicated, and guarded by package-level re-export tests. - - [x] **Mechanism-registry plumbing** — every concrete `Ion` - subclass now self-registers via `@register_ion("CalciumFixed")` / - `@register_ion("CalciumDetailed")` / `@register_ion("CalciumFirstOrder")` / - `@register_ion("SodiumFixed")` / `@register_ion("PotassiumFixed")` - at import time, and `braincell.mech.Ion("CalciumFixed")` resolves - through the registry described in §3.4. - - [x] **Current-driven ion dynamics can use cached ion current.** - Kinetic ions that consume total calcium current can receive the - runtime snapshot created by `cache_ion_total_current=True`, matching - the NEURON-style separation between channel-current evaluation and - ion-state integration. - - [ ] **Consistent external-current registration** — audit that - every dynamics class honours `include_external=True` in its - `derivative` (the existing `CalciumDetailed.derivative` already - does; the contract must stay alive across future refactors). -- **Open risks** - - **Nernst unit trap.** Nernst factors resolve correctly only when every - term remains a `brainunit` quantity; changes to the shared ion templates - must preserve units through graph flattening and materialization. - - **Shared lifecycle contracts.** New ion families must use the common - template hooks and contract tests so child-channel reset and derivative - forwarding cannot diverge by species. - - **Test-side coupling with `braincell.channel`.** The calcium - tests instantiate `CaT_HM1992` to exercise child-channel - forwarding, so a heavy top-level import in `braincell.channel` - would drag through the ion suite. Keep the channel package - tree-shakable (see §3.9 risks). +- [x] **Population 与网络生命周期**:注册 Cell population 和事件源,协调初始化、重置与连续运行。 [Network API](network/current/api.md) +- [x] **端点配对与连接**:通过显式索引或采样配对建立具名连接,管理连接权重及异构延迟。 [配对](network/current/pairing.md)、[连接](network/current/connections.md) +- [x] **事件路由与记录**:调度延迟投递,聚合规则采样和稀疏事件,保持目标 Cell 的状态归属。 [事件架构](network/current/architecture.md)、[Recording](network/current/recording.md) +- [ ] **统一随机上下文**:用 BrainState 随机区域管理网络、source 和 pairing 的默认随机流,确定局部子流与迁移语义。状态:**讨论中**。 [随机方案](network/proposals/random-context.md) +- [ ] **Connection 权重可塑性**:根据上下游 spike 或电压维护规则状态并更新 weight,确定信号绑定及更新顺序。状态:**讨论中**。 [可塑性方案](network/proposals/connection-plasticity.md) +- [ ] **大规模事件与配对**:研究稀疏 delay slots 和分块 endpoint generators,控制静态 shape、调度开销及内存。状态:**待讨论**。 [运行时扩展](network/proposals/runtime-extensions.md) +- [ ] **可学习拓扑**:定义结构变化的状态、梯度及重编译协议。状态:**待讨论**。 [拓扑方案](network/proposals/runtime-extensions.md#i-10-trainable-topology) +- [ ] **网络 batch**:确定网络 batch 的连接、事件和状态轴语义。状态:**待讨论**。 [Batch 方案](network/proposals/runtime-extensions.md#network-batch-runtime) -### 3.9 `braincell.channel` — concrete ion channels +### Optim:参数映射与训练验证 -- **Purpose** — the library's catalogue of ready-to-use HH-style and - Markov-kinetics ion channels. Every class is a subclass of - `Channel` from `_base_channel.py` (so every instance is an `IonChannel` - that registers its gate state as `DiffEqState`s) and declares - `root_type = HHTypedNeuron`. Channels are container children of - an `Ion` species or of a `SingleCompartment` / `Cell` directly. -- **Key families** - - `sodium.py` — `Na_Ba2002`, `Na_TM1991`, `Na_HH1952`, persistent, - resurgent, and cell-specific Nav families. - - `potassium.py` — delayed rectifier, A-type, inward rectifier, Kv, - and M-current families such as `KDR_Ba2002`, `K_HH1952`, and the - MA2020/MA2024 cell-specific variants. - - `calcium.py` — T/L/HVA/LVA and Cav families, including frozen-gradient - variants used by controlled NEURON comparisons. - - `braincell/channel/leaky.py` — `LeakageChannel` base and the - passive leak `IL`. - - `hyperpolarization_activated.py`, `potassium_calcium.py`, and - `potassium_sodium.py` — HCN and mixed-ion channel families. -- **Status** - - [x] Concrete channel families use current-free mechanism names such as - `Na_HH1952`, `K_HH1952`, `CaT_HM1992`, and `HCN_HM1992`; the removed - leading-`I` compatibility aliases are not public API. - - [x] Co-located tests cover kinetics, current sign and shape, lifecycle, - template invariants, and representative reference voltages. - - [x] Concrete classes self-register with the mechanism registry at import - time; abstract family bases are deliberately not registered. - - [x] **PC MA2024 channel set imported.** Sodium, potassium, - calcium, calcium-activated potassium, and HCN PC variants have been - added and covered by targeted tests. The calcium channel set also - includes `_Frozen` variants for the NEURON-comparison path where the - current expression must treat voltage as fixed with respect to - differentiation. - - [ ] **Parameter metadata** — each channel should declare the - unit of every user-facing parameter (`g_max` in `S/cm²`, `E` in - `mV`, time constants in `ms`, …) so that `Density.params` - validation can produce an actionable error at paint time rather - than an opaque JAX trace failure. Store the per-parameter unit - on `MechanismEntry.metadata` and consult it during - `Density.__init__`. - - [~] **GHK current formulation** — `GhkHH` and `ghk_flux` are implemented, - tested, and used by selected Cav channels; the remaining work is a - catalogue-wide audit of which published mechanisms require GHK rather - than an ohmic driving force. - - [~] **Q10 temperature scaling audit** — shared `q10_factor` and - `cached_q10_factor` helpers exist and most gates use the template path; - remaining family-specific temperature assumptions need documentation. - - [ ] **NEURON `.mod` validation** — for every channel in the - catalogue, compare voltage-clamp and current-clamp traces - against the reference `.mod` implementation within a tight - tolerance. Requires re-introducing the `mech/mod_validate/` - harness (see §3.4) and wiring it into milestone M5. - - [ ] **Chloride channels** — add a `braincell/channel/chloride.py` - module once `braincell.ion.Chloride` lands, covering the passive - leak plus GABAa-reversal-driven phasic conductance. - - [ ] **Stiff-channel integrator audit** — run the convergence matrix - over every channel to identify models that require a dedicated - integration path. - - [ ] **Gate-variable naming convention** — most channels use - `p`/`q` for activation / inactivation and a handful use bespoke - names (`m`, `h`, `n`, `s`, …). Tests already rely on the - `p`/`q` convention; unifying the rest will need a deprecation - path because downstream code reaches into `channel.p.value`. -- **Open risks** - - **Import cost.** The package has thirty-plus classes and pulls - `braintools.init`, `brainunit`, and `jax.numpy` at import time. - New families should stay in their own module so the package - remains tree-shakable, and should avoid importing numpy at - module top level beyond what is already there. - - **Cross-ion channels.** `potassium_calcium.py` channels depend - on the attached calcium pool's `C` state. Compile-time checks - that the parent `Cell` actually has a calcium ion attached would - prevent silent `KeyError` / `AttributeError` at simulate time; - this belongs on the mechanism registry in §3.4. - - **API drift vs NEURON naming.** Upstream `.mod` files use lowercase - suffixes (`ih`, `ik`, `ikdr`), while BrainCell names mechanisms by - family/model and provenance. Any validation harness - needs a stable alias table so the diff does not become a - renaming exercise every time a new channel lands. +协作入口:[Optim TODO](optim/TODO.md)。 -### 3.10 `braincell` package root — neuron base classes +- [x] **参数 source 与映射**:提供 direct、共享 scale、parameterized 映射及分组,保留物理单位、共享关系和 reset/materialization 语义。 [参数 API](optim/current/api.md) +- [x] **Channel 与 Ion 参数训练**:按构造签名发现候选参数,支持 Cell 内通道和离子参数绑定;已有单参数教学拟合及相关回归。 [支持度](optim/current/parameter-support.md)、[结果](optim/current/results/parameter-learning.md) +- [x] **训练与诊断实验**:已有固定参数 rollout 的 BPTT/RTRL、分阶段拟合、搜索、敏感度诊断及刺激设计实验,入口位于 examples/experimental。 [实验工作流](optim/current/experimental-workflows.md) +- [x] **Synapse 与 Network 参数训练**:支持突触参数、静态连接 weight、检测阈值和网络 roots 聚合,已通过 CPU 梯度及拟合验收并提交。 [接口范围](optim/current/api.md#synapse-connection-network)、[验证记录](optim/current/results/synapse-network-learning.md#提交验收) +- [~] **组合精度与性能验证**:已有多 CV、population、CPU/GPU 等单项结果;新事件网络的 GPU、多 CV 与多 population 组合及 checkpoint 比较仍缺证据。 [验证缺口](optim/proposals/roadmap.md#验证缺口) +- [ ] **可塑性参数训练**:明确动态 weight 初值、规则参数与运行中状态的关系,连接可塑性调度和梯度验收。状态:**讨论中**。 [训练提案](optim/proposals/connection-plasticity.md) +- [ ] **训练自动恢复**:在已有历史 archive 和诊断基础上,设计 plateau、SGDR、perturb 控制器及恢复协议。状态:**讨论中**。 [恢复方案](optim/proposals/training-recovery.md) +- [ ] **参数范围与公共训练协议**:确定 Cell 初值和 cable 参数的 owner,以及可复用训练协议、稳定 grouping 和持久化边界。状态:**待讨论**。 [后续方向](optim/proposals/roadmap.md) +- [ ] **Rollout 内参数更新**:定义逐步更新参数时的参数历史、状态演化及梯度含义。状态:**待讨论**。 [研究方向](optim/proposals/roadmap.md#rollout-内更新参数) -- `_base_neuron.py`, `_base_ion.py`, and `_base_channel.py` define the - runtime bases composed by concrete neurons and mechanisms. -- `_single_compartment/` owns `SingleCompartment`, the simplest concrete - neuron and a numerical sanity surface. -- `_multi_compartment/` owns the directly initialized and executed `Cell`, - its views, point-mechanism stores, probes, and `RunResult` (see §3.5). -- `_misc.py` — `normalize_param` (the brainunit gatekeeper), helpers, - decorators (`set_module_as`, `deprecation_getattr`), `Container`. -- `_typing.py` — type aliases (`Initializer`, `ArrayLike`, `T`, `DT`). +### Reduction:约化模型接入 -### 3.11 `braincell.network` — population and event runtime +协作入口:[Reduction TODO](reduction/TODO.md)。 -- **Purpose** — register Cells and event sources, connect source outputs to - Cell-owned synapses, coordinate lifecycle and delayed delivery, and - aggregate immutable sample and sparse-event results. -- **Status** - - [x] Direct `Network`, `Population`, `NetworkConnections`, and - `NetworkResult` model with no separate public build phase. - - [x] Named connection calls, explicit or sampled endpoint pairing, - heterogeneous delays, split runs, reset semantics, and cached schedules. - - [x] Static recording schemas with regular `SampleBlock` outputs and - sparse `EventSeries` outputs. - - [ ] Chunked large-N pairing, automatic sparse/dense delay queues, - post-initialization topology mutation, and network batch runtime. -- **Design authority** — [`network/design-overview.md`](network/design-overview.md) - and its linked API, architecture, issues, and implementation documents. +- [x] **可替换的 Cell 约化运行时**:通过 ReductionModel 接入 Cell 的生命周期、输入输出和网络运行,已有示例及回归。 [接入指南](reduction/current/model-integration-guide.md) +- [ ] **DBNN 数据、训练与部署**:沿用现有挂载契约,确定数据布局、训练规模、模型资产与分阶段验收。状态:**讨论中**。 [DBNN 提案](reduction/proposals/DBNN-plan.md) -### 3.12 `braincell.trainable` — parameter ownership and mapping +### Vis:形态、拓扑与结果展示 -- **Purpose** — bind optimizer-facing parameter roots to selected physical - runtime fields while preserving units, sharing semantics, and stable JAX - state trees. It does not own optimizers, losses, datasets, or training loops. -- **Status** - - [x] `ParameterSource`, `ParameterBinding`, `ParameterSet`, and - `TrainableManager`, plus direct, shared-scale, and callable latent sources. - - [x] Cell-local ChannelView mappings for the initial supported channel - families, with transactional validation and differentiable materialization. - - [ ] Ion, Synapse and Connection parameters, Network aggregation, and - broader parameter families. -- **Design authority** — [`optim/design-overview.md`](optim/design-overview.md) - and its linked API, architecture, implementation plan, and references. +协作入口:[Vis TODO](vis/TODO.md)。 ---- +- [x] **形态与空间数据绘图**:提供 2D/3D 几何和树形布局,按 branch、segment 或采样点着色,并高亮区域与位点。 [绘图 API](vis/current/api.md) +- [x] **Cell 拓扑与动态结果**:展示 branch、CV、point 拓扑、时间轨迹、多模型比较和动画。 [展示能力](vis/current/visualization.md#当前支持什么) +- [x] **交互与导出**:按后端提供拾取、图片、动画及 HTML 导出;后端之间的交互能力有明确差异。 [后端支持](vis/current/visualization.md#后端与数据) +- [~] **渲染回归**:已有布局、场景和图元断言,仍缺代表性像素基线及执行图像比较的 CI。 [验证现状](vis/current/visualization.md#验证现状) +- [ ] **迁入 BrainTools**:讨论公共可视化模块归属、数据入口、简单绘图与 GUI,以及旧调用的兼容方案。状态:**讨论中**。 [迁移提案](vis/proposals/braintools-migration.md) -## 4. Cross-Cutting Concerns +## 跨模块依赖与阻塞 -### 4.1 Units +- [ ] **单方程 ODE 兼容到 Cell**:Cell 牵头确定特殊 policy 与 place 语义,Filter 配合位点表达,Quad 比较积分路径;先确定单 branch 情形,再讨论分叉形态等效。状态:**讨论中**。 [Cell 统一方案](cell/proposals/single-multi-compartment-unification.md) +- [~] **显式积分完整处理边界输入**:Cell 牵头将 point 刺激和 Synapse 电流传入消元后的 RHS,Quad 配合边界约束及完整五行系统对照;现有完整装配位于 staggered 路径。 [边界输入问题](cell/proposals/explicit-solver-boundary-inputs.md) +- [ ] **统一随机区域**:Network 牵头,source、pairing 与 Filter 配合默认随机流和局部子流设计,使区域内自定义随机调用也能统一管理。状态:**讨论中**。 [随机上下文](network/proposals/random-context.md) +- [ ] **可塑性与参数训练**:Network 牵头明确 Connection weight 更新,Synapse 管理内部动力学,Optim 绑定规则参数及初值;需确定信号读取和更新顺序。状态:**讨论中**。 [可塑性](network/proposals/connection-plasticity.md)、[训练提案](optim/proposals/connection-plasticity.md) +- [ ] **空间编辑后的缓存一致性**:Morph 牵头定义结构和几何修订,Filter、Cell 配合选择结果与离散缓存的失效,保证编辑后重新构建使用新形态。状态:**待讨论**。 [Morph](morph/TODO.md)、[Filter](filter/TODO.md) +- [ ] **单位诊断与模型验证**:Mech 牵头建立声明诊断及验证框架,Channel/Ion 提供参数维度、模型来源和参考机制;已有模型比较需整理成可复用验收流程。状态:**待讨论**。 [Mech](mech/TODO.md)、[Channel](channel/TODO.md)、[Ion](ion/TODO.md) +- [ ] **量化 GABA 与氯反转电位**:Ion 牵头建立 Chloride 状态和反转电位,Channel、Synapse 配合电流及事件模型;当前仍缺氯离子家族。状态:**待讨论**。 [Ion](ion/TODO.md)、[Channel](channel/TODO.md) +- [~] **大规模精度与性能结论**:Optim 牵头训练组合验证,Quad 提供 solver/dt 对照,Cell、Network 配合多 CV 与多 population 场景;GPU 和 checkpoint 的组合证据尚不完整。 [Optim 验证缺口](optim/proposals/roadmap.md#验证缺口)、[Quad](quad/TODO.md) +- [~] **Python 支持范围一致**:Architecture 牵头,CI 配置与依赖维护配合;在现有 3.13 测试基础上扩展矩阵或收窄 3.11 至 3.14 的声明。 [版本覆盖](architecture/TODO.md) -`brainunit` is non-negotiable. Every public API that takes a physical -quantity routes through `_misc.normalize_param`, which **rejects bare -numerics with `TypeError`**. New modules must: +## 专题与维护入口 -- accept inputs as `python_number/np.ndarray/jax.Array * brainunit_unit`; -- store quantities in canonical SI units internally; -- expose values back to users with units attached, never raw floats. - -### 4.2 Immutability discipline - -- `Branch`, `CV`, `MorphoEdge`, `MorphoMetric`, `IntegratorEntry`, - `PaintRule`, `PlaceRule`, `CableProperty` are frozen dataclasses. -- `Morphology` is mutable and carries a monotonic `revision`. Before - initialization, `Cell` keys its discretization cache by morphology identity - and revision; structural mutation after initialization must not silently - reshape runtime state. -- `IntegratorRegistry` is the single mutable global; entries are - added at import time via decorators and never mutated afterwards. - -### 4.3 Cell declaration and initialization - -`Cell` owns both its mutable declaration and, after initialization, its JAX -runtime state. Structural declarations are accepted only before -`init_state()`: - -``` -Cell(morpho, policy) - -> cell.paint(region, density_mech) - -> cell.place(locset, point_mech) - -> cell.init_state() - -> cell.run(dt=..., duration=...) # returns RunResult -``` - -After initialization, topology-changing paint/place/connect/recording calls -are rejected. Parameter mappings may materialize new values into the existing -layout, but must not change the state-tree structure. `reset_state()` resets -runtime values without reopening the declaration phase. - -### 4.4 Testing - -- pytest with `unittest.TestCase`; tests live next to source as - `_test.py`, with no exceptions. The `*` must name a real - sibling module; the one sanctioned exception is a package-scope guard - in `/__init___test.py`. -- `conftest.py` forces `JAX_PLATFORMS=cpu` and `MPLBACKEND=Agg`. -- IO test fixtures live in `examples/multi_compartment/morpho_files/`. -- New code is expected to ship with co-located tests and to keep - per-module test runtime under a few seconds on CPU. - -### 4.5 Documentation - -- All public classes / methods / functions use **NumPy-style - docstrings** (see CLAUDE.md for the canonical template). -- Examples must be `.. code-block:: python` blocks compatible with - doctest. -- High-level narrative documentation lives under `docs/`; design - notebooks live under `examples/multi_compartment/`. - ---- - -## 5. Data-Model Summary - -| Layer | Type | Mutability | Lifetime | Owner | -|---|---|---|---|---| -| Geometry | `Branch`, `Soma`, `Dendrite`, ... | frozen | morphology lifetime | user / IO reader | -| Geometry | `Morphology` | mutable tree | until edited | user | -| Geometry view | `MorphoBranch`, `MorphoEdge` | frozen view | follows tree | `Morphology` | -| Metrics | `MorphoMetric` | frozen snapshot | recomputed on demand | `Morphology` | -| Selection | `RegionExpr`, `LocsetExpr` | frozen expression | reusable | user | -| Selection cache | `SelectionCache` | mutable | per-Morphology | filter layer | -| Mechanisms | `CableProperty`, `Density` (`Channel`, `Ion`), `Point*` (`CurrentClamp`, `Synapse`, `Junction`, …) | frozen dataclass / slots | declaration | user | -| Mechanisms | `Ion`, `Channel`, `IonChannel`, `MixIons` | hybrid (JAX state) | per-initialized Cell | `Cell` | -| Discretization | `CV` | frozen | declaration cache / initialization | `Cell` | -| Discretization | `PaintRule`, `PlaceRule` | frozen | declaration | `Cell` | -| Topology | `CVTree`, `NodeTree`, `Node`, `NodeEdge` | frozen | declaration cache / initialization | `Cell` | -| Scheduling | `NodeScheduling` | frozen | initialized runtime | `Cell` | -| Runtime | `CellRuntimeState` and mechanism stores | brainstate-managed | initialized runtime | `Cell` | -| Network | `Population`, connection/recording stores | mixed | network lifecycle | source / target owner | -| Parameters | `ParameterSet`, `ParameterBinding` | stable structure, mutable values | training lifecycle | `TrainableManager` | -| Numerics | `IntegratorEntry` | frozen | process lifetime | `IntegratorRegistry` | -| Numerics | `DiffEqState`, `IndependentIntegration` | brainstate-managed | per-step | step function | - ---- - -## 6. Public API Contract - -The list below is the *intended* stable surface. Anything not on it is -internal and may change without deprecation. - -- **Morphology layer**: `Branch`, `Soma`, `Dendrite`, `Axon`, - `BasalDendrite`, `ApicalDendrite`, `CustomBranch`, - `branch_class_for_type`, `Morphology`, `MorphoBranch`, `MorphoEdge`, - `MorphoMetric`. The `Morphology` class also exposes the - `from_swc` / `from_asc` / `from_neuromorpho` classmethod constructors. -- **External-data entry points**: `braincell.io.load_neuromorpho` and - `Morphology.from_neuromorpho`. - Tier-2 / Tier-3 NeuroMorpho.Org symbols (`NeuroMorphoClient`, - `NeuroMorphoCache`, `NeuroMorphoQuery`, `NeuroMorphoMeasurement`, - `NeuroMorphoError`, …) live under `braincell.io.neuromorpho` and - `braincell.io`. -- **Filter layer**: `RegionExpr`, `LocsetExpr`, `SelectionCache`. -- **Mechanism declaration layer** (`braincell.mech`): `Mechanism` - (marker base), `CableProperty`, `Density` (and its concrete - subclasses `Channel` / `Ion`, which accept the target as either a - string or a class object), `Point` (and its concrete subclasses - `CurrentClamp`, `SineClamp`, `FunctionClamp`, `ProbeMechanism`, - `Synapse`, `Junction`), the frozen `Params` mapping, and the - registry API (`MechanismRegistry`, `MechanismEntry`, - `get_registry`, `register_channel`, `register_ion`, - `register_synapse`). -- **Ion species** (`braincell.ion`): `Sodium`, `SodiumFixed`, - `Potassium`, `PotassiumFixed`, `Calcium`, `CalciumFixed`, - `CalciumDetailed`, `CalciumFirstOrder`. -- **Ion channels** (`braincell.channel`): concrete Na, K, Ca, leak, HCN, - calcium-activated potassium, and mixed-ion families exported by the - channel package, plus their documented template bases. -- **Synapses** (`braincell.synapse`): `ExpSyn`, `Exp2Syn` from - `synapse.exponential`. -- **Cell and discretization layer**: `Cell`, `MultiCompartment`, `CellView`, - `ChannelView`, `IonView`, `SynapseView`, `ClampView`, `RunResult`, `CV`, - `CVTree`, `CVPolicy`, `CVPerBranch`, `CVPerBranchList`, `MaxCVLen`, - `DLambda`, `CVPolicyByTypeRule`, `CompositeByTypePolicy`, `Node`, - `NodeTree`, and `PointPlacement`. Internal runtime and scheduling records - are not part of the top-level contract. -- **Network layer**: `Network`, `NetworkResult`, `NetworkConnections`, - `ConnectionView`, event-source and event-table types, recording schemas, - `SampleBlock`, `EventSeries`, `connect`, and `observe`. The deliberately - small `braincell.network.__all__` is separate from top-level convenience - exports; specialized constructors remain available from their submodules. -- **Trainable parameter layer** (`braincell.trainable`): - `ParameterSource`, `ParameterBinding`, `ParameterSet`, `TrainableManager`, - `parameter`, `parameterized`, and `scale`. -- **Numerics layer**: `register_integrator`, `get_integrator`, - `get_registry`, `IntegratorEntry`, `IntegratorRegistry`, - `all_integrators`, every `*_step` function listed in - `braincell/quad/__init__.py::__all__`, `DiffEqModule`, - `DiffEqState`, `IndependentIntegration`. -- **Neuron base**: `HHTypedNeuron`, `IonChannel`, `Ion`, `IonInfo`, - `Channel`, `MixIons`, `mix_ions`, `SingleCompartment`. -- **Visualization**: top-level `braincell.vis.plot2d` / `plot3d` - entry points (the imperative scene API stays internal until it - stabilizes). - ---- - -## 7. End-to-End User Workflows - -### 7.1 Build and inspect a morphology - -```python -import braincell -import brainunit as u - -morpho, report = braincell.Morphology.from_swc("cell.swc", return_report=True) -print(morpho.topo()) -print(morpho.metric) # MorphoMetric snapshot -soma_region = braincell.filter.branch_in("type", {"soma"}) -distal_region = braincell.filter.branch_range("length", (50 * u.um, None)) -``` - -### 7.2 Discretize and declare mechanisms - -```python -import braincell.mech as mech - -cell = braincell.Cell(morpho, cv_policy=braincell.DLambda(0.1)) - -cell.paint( - braincell.filter.AllRegion(), - mech.CableProperty( - membrane_capacitance=1.0 * (u.uF / u.cm ** 2), - axial_resistivity=100.0 * (u.ohm * u.cm), - resting_potential=-65 * u.mV, - ), -) -cell.paint(soma_region, mech.Ion("SodiumFixed")) -# mech.Channel / mech.Ion accept either a registry name string or the -# concrete class itself — both route through the mechanism registry. -cell.paint(soma_region, mech.Channel(braincell.channel.Na_Ba2002, g_max=0.12 * u.S / u.cm ** 2)) -cell.place( - braincell.filter.at("soma", 0.5), - mech.CurrentClamp(delay=10 * u.ms, durations=50 * u.ms, amplitudes=0.2 * u.nA), -) -cell.place(braincell.filter.at("soma", 0.5), mech.StateProbe(name="soma_v")) -``` - -### 7.3 Run a simulation - -```python -cell.init_state() -result = cell.run(dt=0.025 * u.ms, duration=100 * u.ms) -print(result.traces["soma_v"].shape) -``` - -`cell.init_state()` freezes structural declarations and installs runtime state -on the same `Cell`. Subsequent runtime inspection, reset, recording, and -continued runs use that initialized object. - -### 7.4 Compare two morphologies visually - -```python -braincell.vis.compare2d(morpho_a, morpho_b, layout="frustum") -``` - ---- - -## 8. External Dependencies - -| Package | Floor | Role | -|---|---|---| -| `python` | 3.11 | language; classifiers claim 3.11–3.14 (see note) | -| `jax` | recent | autodiff, vmap, jit, GPU/TPU — deliberately unpinned | -| `brainunit` | >= 0.0.8 | units (mandatory at every API boundary) | -| `brainstate` | >= 0.5.4 | stateful simulation framework | -| `brainevent` | >= 0.0.7 | sparse event / CSR ops | -| `braintools` | >= 0.1.0 | brain modeling utilities | -| `brainpy` | >= 2.7.5 | brain dynamics library | -| `numpy` | >= 2.0 | arrays | -| `scipy` | recent | scientific helpers | -| `pyvista` | optional | 3D visualization backend | -| `matplotlib` | optional | 2D visualization backend | -| `NEURON` | dev only | reference comparator under `examples/multi_compartment/` | - -This table and `[project].dependencies` in `pyproject.toml` are kept in -sync; `pyproject.toml` is the machine-readable source of truth, and the -`requirements*.txt` files are thin pointers to its extras. - -`pyproject.toml` is the source of truth for dependency floors. In particular, -`brainstate>=0.5.4` is required for current JAX compatibility and -`numpy>=2.0` reflects the tested support policy. Experimental dependencies in -an uncommitted worktree are not part of this table. - -Optional dependencies must be **lazily imported** so the base install -stays small — use `importlib.util.find_spec` plus PEP 562 -`__getattr__` for the visualization backends. - -> **Note — Python version coverage.** The `classifiers` list advertises -> 3.11 through 3.14, but `CI.yml` and `CI-daily.yml` both run a -> single-entry `python-version: ["3.13"]` matrix. Three of the four -> advertised versions are therefore untested. Either widen the CI matrix -> or narrow the classifiers. - ---- - -## 9. Glossary - -- **CV (control volume)** — atomic spatial unit produced by the - discretization layer; the array-of-CVs is what the integrator sees. -- **CV policy** — rule that turns a `Branch` into a sequence of CVs - (e.g., `DLambda(0.1)`, `MaxCVLen(10*u.um)`, `CVPerBranch(n)`). -- **Paint** — install a *distributed* mechanism (cable or density) - onto a `RegionExpr`. -- **Place** — install a *point* mechanism (clamp, probe, synapse, - gap junction) onto a `LocsetExpr`. -- **DHS** — Dependent Hines Solver: parent-pointer-driven elimination - ordering used by `dhs_voltage_step`, designed to vectorize the - classic Hines solver across batched cells. -- **Staggered step** — split integrator that solves the voltage - system implicitly (DHS) and the gating variables with exponential - Euler in alternating half-steps. -- **`.bcm` file** — BrainCell Morphology, the self-contained - checkpoint format produced by `io/checkpoint.py`. +- [小脑示例进度](../../examples/neuron_compare/cerebellum-import-progress.md):具体模型导入、PC 装配与数值比较。 +- [共享 Ion/Channel 文献表](ion/references/ion-channel-bibliography.md):模型来源、版本和归因证据。 +- [Design 规范](AGENTS.md):文档职责及进度维护;提交前检查遵循 [仓库约定](../../AGENTS.md#design-code-and-examples)。 diff --git a/docs/design/architecture/TODO.md b/docs/design/architecture/TODO.md new file mode 100644 index 00000000..c6c4d2da --- /dev/null +++ b/docs/design/architecture/TODO.md @@ -0,0 +1,24 @@ +# Architecture TODO + +Architecture 维护系统级模块关系、数据流、状态归属和跨模块接口约定。 +宏观目标见 [全局 TODO](../TODO.md),文档分工与状态定义见 [Design 规范](../AGENTS.md)。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步或待决定问题 | 文档 | +| --- | --- | --- | --- | +| 声明类型与运行时基类同名 | 待讨论 | 比较命名空间限定与显式声明名,核对示例和类型标注的迁移成本 | [命名方案](proposals/interface-consistency.md#声明与运行时类型的名称) | +| 公共导出与内部实现路径 | 待讨论 | 确定导出、支持的成员和兼容策略如何共同表达公共契约 | [导出方案](proposals/interface-consistency.md#公共导出与实现路径) | +| Python 版本覆盖 | 待讨论 | 对齐 classifiers 的 3.11 至 3.14 声明和主要测试 3.13 的 CI:扩展矩阵或调整支持范围 | [CI](../../../.github/workflows/CI.yml)、[Daily CI](../../../.github/workflows/CI-daily.yml) | + +## 当前架构 + +- [系统总览](current/system-overview.md):模块职责、依赖与数据流、Cell/Network/Trainable/Vis 示例、状态归属和执行约定。 +- [Cell 架构](../cell/current/architecture.md)、[Network 架构](../network/current/architecture.md)、[Trainable 架构](../optim/current/architecture.md):模块内部构建与运行细节。 +- [Morph 分层约束](../morph/current/layering-invariants.md)、[Vis 架构](../vis/current/visualization.md):几何层依赖及绘图数据组织。 + +## 模块议题入口 + +Cell 的 reset 和查询风格见 [Cell TODO](../cell/TODO.md),Morph 子包导出见 +[Morph TODO](../morph/TODO.md),通道命名现状见 [Channel TODO](../channel/TODO.md)。 +Vis 向 BrainTools 迁移涉及的接口与模块归属由 [Vis TODO](../vis/TODO.md) 推进。 diff --git a/docs/design/architecture/current/system-overview.md b/docs/design/architecture/current/system-overview.md new file mode 100644 index 00000000..8d7aa1c1 --- /dev/null +++ b/docs/design/architecture/current/system-overview.md @@ -0,0 +1,285 @@ +# System Overview + +BrainCell 把形态、机制和连接声明构建为可积分的 Cell,再由 Network 组织多个 Cell 的时间推进与事件投递。 +Trainable 把优化参数映射到运行时参数,Vis 把形态、离散结构和记录结果变成图像。 +这份总览沿这条流程说明模块分工、数据归属和执行约定。 + +## 模块与核心对象 + +| 模块 | 输入、产出与职责 | 实现与详细文档 | +| --- | --- | --- | +| IO | 从 SWC、ASC 等外部格式构建 Morphology,保存和恢复形态数据 | [API](../../io/current/api.md)、[NeuroMorpho](../../io/current/neuromorpho.md) | +| Morph | 用 Branch 描述几何,用 Morphology 组织树;提供视图和几何度量 | [API](../../morph/current/api.md)、[分层约束](../../morph/current/layering-invariants.md) | +| Filter | Region 选择空间区域,Locset 选择位置;表达式在形态和离散上下文中求值 | [API](../../filter/current/api.md)、[连续采样](../../filter/current/sampling.md)、[空间参数](../../filter/current/spatial-callable-parameters.md) | +| Mech | 保存电缆属性、密度机制和点机制声明;通过注册表定位运行时类 | [API](../../mech/current/api.md)、[架构](../../mech/current/architecture.md) | +| Cell | 持有声明,构建 CV 与节点树,绑定机制、状态和求解器 | [实现](../../../../braincell/_multi_compartment)、[API](../../cell/current/api.md)、[架构](../../cell/current/architecture.md) | +| Ion / Channel / Synapse | 实现离子、通道和突触的电流与内部动力学 | [Ion API](../../ion/current/api.md)、[Channel API](../../channel/current/api.md)、[Synapse API](../../synapse/current/api.md) | +| Quad | 提供积分协议、机制积分器和树形电压求解器,推进 Cell 及机制状态 | [API](../../quad/current/api.md)、[架构](../../quad/current/architecture.md) | +| Network | 注册 population,构建事件路由并协调 Cell 推进;汇总采样和事件结果 | [实现](../../../../braincell/network)、[API](../../network/current/api.md)、[架构](../../network/current/architecture.md) | +| Trainable | 注册原始参数和绑定关系,将参数变换后的物理量写入既有运行时布局 | [实现](../../../../braincell/trainable)、[API](../../optim/current/api.md)、[架构](../../optim/current/architecture.md) | +| Reduction | 在同一个 Cell 上挂载替代动力学,消费已投递的突触输入并返回约化模型输出 | [接入指南](../../reduction/current/model-integration-guide.md) | +| Vis | 读取形态、Cell 拓扑或数组,生成静态图、轨迹和动画 | [实现](../../../../braincell/vis)、[API](../../vis/current/api.md) | +| SingleCompartment 与公共基类 | 集中参数模型使用独立执行路径;与 Cell 共享 HHTypedNeuron、离子/通道基类及积分设施 | [SingleCompartment](../../../../braincell/_single_compartment/base.py)、[统一提案](../../cell/proposals/single-multi-compartment-unification.md) | + +下面表示主要依赖:实线从使用方指向提供方;虚线表示便捷方法中的延迟导入。 +图按职责合并内部模块,具体构建与绑定代码由 Cell 架构展开。 + +```mermaid +--- +config: + layout: elk +--- +flowchart LR + network[Network 执行器] --> cell[Cell] + network --> events[事件 / 记录协议] + cell --> events + cell --> build[离散构建与机制绑定] + build --> morph[Morph] + build --> filter[Filter] + build --> mech[Mech / 注册表] + filter --> morph + cell --> trainable[Trainable] + network --> trainable + cell --> reduction[Reduction] + cell --> quad[Quad] + cell --> bases[公共运行时基类] + single[SingleCompartment] --> bases + single --> quad + mechanisms[Ion / Channel / Synapse] --> bases + io[IO] --> morph + vis[Vis] --> morph + vis --> cell + morph -. 便捷读取 .-> io + morph -. 便捷绘图 .-> vis +``` + +Morphology 的文件读取与绘图快捷方法在调用时导入 IO、Vis。Vis 可以读取 Cell, +Cell 的仿真路径则不需要绘图库;Matplotlib、PyVista 等后端按需加载。 +这个约束由 [Morph 守护测试](../../../../braincell/morph/__init___test.py) 和 +[Vis 守护测试](../../../../braincell/vis/__init___test.py) 检查。 + +Network 执行器依赖 Cell,Cell 则使用 `network.event`、`network.recording` 中的协议和记录类型。 +因此 Python 包的依赖并非简单的上下分层;图中将事件与记录协议单独列出,区分它们与执行器的职责。 + +## 从声明到结果 + +下面的箭头均表示数据流。声明决定运行时结构,优化器更新原始参数后,Trainable 将新值写回同一结构。 + +```mermaid +flowchart TB + declarations[形态 / policy / paint / place] --> geometry[CV / NodeTree / 位置映射] + geometry --> runtime[机制对象 / 布局 / 状态] + connections[连接 / 记录声明] --> routing[路由 / 延迟队列 / 采样映射] + runtime --> step[Cell 积分与 Network 事件投递] + routing --> step + step --> samples[电压 / 机制状态 / 事件 / 采样结果] + samples --> plots[Vis 图像] + geometry --> plots + samples --> loss[损失与梯度] + loss --> roots[优化器更新原始参数] + roots --> materialize[Trainable 参数映射] + materialize --> runtime +``` + +### 构造 Cell 和事件输入 + +以下三个代码块按顺序运行。例子使用一个 branch、两个 CV 的被动 Cell, +在中点放置一个指数突触,并将漏电导的缩放因子注册为可训练参数。 + +```python +import braincell as bc +import brainstate +import braintools +import brainunit as u +import jax.numpy as jnp +from braincell.filter import AllRegion, RootLocation + +soma = bc.Branch.from_lengths( + lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type="soma", +) +morpho = bc.Morphology.from_root(soma, name="soma") +cell = bc.Cell(morpho, cv_policy=bc.CVPerBranch(2), V_init=-65.0 * u.mV) +cell.paint( + AllRegion(), + bc.mech.CableProperty( + membrane_capacitance=1.0 * u.uF / u.cm**2, + axial_resistivity=100.0 * u.ohm * u.cm, + resting_potential=-65.0 * u.mV, + ), + bc.mech.Channel("IL", name="leak", g_max=0.1 * u.mS / u.cm**2, E=-65.0 * u.mV), +) +cell.place(RootLocation(0.5), bc.mech.Synapse("ExpSyn", name="syn", tau=2.0 * u.ms)) +cell.loc(RootLocation(0.5)).record("v_mid", bc.observe.state("v")) +cell.channels["leak"].trainable( + g_max=bc.trainable.scale(brainstate.nn.Param(1.0), name="leak_factor"), +) + +stim = bc.NetStim(start=0.25 * u.ms, number=1, interval=10.0 * u.ms) +bc.connect("input", source=stim, synapse=cell.synapses["syn"], + weight=0.001 * u.uS, delay=0.1 * u.ms) +net = bc.Network("example") +net.add_population("post", cell) +net.add_population("input", stim) +``` + +`paint` 将密度机制应用到区域,`place` 将点机制放到位置;二者保存声明, +初始化时才实例化运行时机制。`record` 保存观测声明,采样映射复用已有空间结构。 +连接的目标突触属于 Cell;Network 注册的 population 引用这个 Cell。 + +### 运行、读取和绘图 + +```python +dt = 0.025 * u.ms +result = net.run(dt=dt, duration=1.0 * u.ms, event_backend="scatter") +block = result.samples["post"]["v_mid"] +assert cell.V.value.shape == (1, 2) +assert block.values.shape == (40, 1) + +from braincell import vis + +topology = vis.plot_cell_topology(cell, level="cv", value="V", layout="kamada_kawai") +traces = vis.plot_traces( + cell.morpho, block.time, block.values, + locset=RootLocation(0.5).evaluate(cell.morpho), layout="fan", shape="line", +) +assert topology.figure is not None +assert len(traces.trace_axes) == 1 +``` + +`Network.run` 首次调用会初始化模型;返回值覆盖本次运行区间。 +`SampleBlock.values` 的列与 `schema.rows` 对齐,这里只有一个 population 成员、一个记录位点。 +Vis 的 Cell 入口是 `vis.plot_cell_topology(cell, ...)`;轨迹入口接收时间和二维数组, +此例的数组列顺序正好对应传入的 Locset。完整签名、空间索引与返回对象见 [Vis API](../../vis/current/api.md)。 + +### 参数如何进入梯度计算 + +继续使用上面的 Network。`prepare_run` 构建单步执行器;每次求损失先重置动力学状态, +再通过 `update` 推进并直接读取电压。这里的损失是轨迹相对 -60 mV 的均方差,演示一次参数更新。 + +```python +net.prepare_run(dt=dt, event_backend="scatter") +states = net.trainables.parameters().states() +optimizer = braintools.optim.Adam(lr=0.01) +optimizer.register_trainable_weights(states) + +def loss(): + net.reset_state() + + def step(_): + net.update() + return cell.V.value.to_decimal(u.mV)[0, 0] + + voltage = brainstate.transform.for_loop(step, jnp.arange(40)) + return jnp.mean((voltage - (-60.0)) ** 2) + +gradient = brainstate.transform.grad(loss, grad_states=states, return_value=True) + +@brainstate.transform.jit +def train_step(): + gradients, value = gradient() + optimizer.update(gradients) + return value + +value = train_step() +assert bool(jnp.isfinite(value)) +``` + +Network 的参数集合聚合各 Cell 的原始 `Param`,按对象身份去重,保留原对象引用。 +优化器更新这些参数;初始化、运行入口和单步 `update` 中的 materialize 根据绑定关系计算物理量, +写入机制或连接的参数缓冲。动力学状态、原始参数和物理参数缓冲因此有不同的生命周期。 +更完整的目标轨迹拟合见 [突触学习示例](../../../../examples/multi_compartment/synapse_learning.py)。 + +## 数据归属与生命周期 + +| 数据 | 持有者与共享关系 | 何时建立或更新 | +| --- | --- | --- | +| Branch、Morphology 与空间表达式 | 用户构造;声明期 Cell 引用传入的 Morphology | 形态可在初始化前编辑;几何查询缓存按形态身份与 revision 失效 | +| policy、paint/place、连接和记录声明 | Cell 保存模型声明;连接由目标 Cell 管理,Network 汇总查询 | 初始化前添加,初始化后结构冻结 | +| CV、NodeTree、位置映射 | Cell 的离散缓存 | 声明期按需构建;初始化时在克隆后的形态上重建 | +| 布局、机制实例、参数和事件缓冲 | CellRuntimeState 及关联存储 | 初始化时绑定;固定形状下更新数值 | +| 电压、spike、时间 | Cell;机制门控、浓度、突触状态由具体机制实例持有 | 积分推进或 reset_state 重置 | +| population、事件路由和延迟队列 | Network;population 引用原 Cell 或事件源 | 注册后构建执行配置;每步投递、入队和推进 | +| 记录 schema 与结果 | Cell 声明观测;执行器产出 SampleBlock / EventSeries | 每次运行生成对应时间段的结果 | +| 原始 Param 与绑定 | Cell 的 TrainableManager;Network 提供聚合视图 | 初始化前注册绑定;优化器更新参数,materialize 写入运行时 | +| 图像和绘图布局缓存 | Vis 与返回的 Figure / Axes 等对象 | 绘图时读取当前数据;仿真推进不会自动刷新旧图 | + +`Cell.init_state()` 克隆形态、构建离散结构并绑定机制,然后初始化状态。 +`reset_state()` 保留布局和参数根,重置电压及机制等运行状态;`reset()` 丢弃运行时并恢复声明期形态引用。 +两种 reset 的调用条件见 [Cell 生命周期](../../cell/current/api.md#生命周期)。 + +独立 Cell 可以 `run`;加入 Network 后由 Network 统一推进和重置。 +Network 首次准备运行后固定步长、事件后端和延迟量化配置,后续 `run` 在同一时间线上继续。 +重复实验使用 `net.reset_state()`,参数绑定和执行配置随之保留。 + +## 状态轴与空间映射 + +| 表达 | 含义 | +| --- | --- | +| `pop_size + (n_cv,)` | 未额外 batching 的 Cell 膜电压布局;默认 `pop_size=(1,)`,上例为 `(1, 2)` | +| `pop_size + (n_point,)` | 电气节点布局,包括 CV 中心及边界/连接节点;用于点机制装配与节点电压求解 | +| 机制布局行 | 经空间选择和机制绑定生成;局部行索引通过布局映射回 CV 或 point | +| `(n_times, n_rows)` | 上例的规则采样结果;每列的 population、位置、字段和单位由 recording schema 解释 | +| Vis 的 branch / segment / centerline / CV / node 值 | 对应不同的几何或电气索引;调用时按入口指定的空间传值 | + +独立 Cell 的 `init_state(batch_size=...)` 可再增加前置 batch 轴;Network 当前拒绝该参数, +通过 Cell 的 `pop_size` 表示 population。机制与记录布局的完整轴约定见模块 API。 + +CV 是膜面积、电容和密度机制的空间单元;point 是电压方程中的节点。 +因此单 branch、单 CV 仍可包含两个端点和一个 CV 中心。SingleCompartment 的单行 ODE +没有这两行边界约束,状态轴也不自动带 Cell 的末尾 CV 轴;统一方式见 +[Single/MultiCompartment 提案](../../cell/proposals/single-multi-compartment-unification.md)。 + +膜电压满足电流平衡:`C dV/dt = I_membrane + I_injected + I_axial`。 +这里 C 是 CV 总电容,电流以流入 CV 为正;密度电流乘膜面积后才与点电流相加。 +几何面积、轴向电导及节点消元的公式见 [Cell 几何计算](../../cell/current/architecture.md#几何计算)。 + +## 时间推进与求解路径 + +Network 每步先准备外部输入与 pre 采样,再投递到期事件并更新突触输入,随后推进 Cell。 +Cell 更新之后处理即时零延迟事件、post 采样及输出事件,再将延迟事件入队并推进队列。 +事件延迟如何量化、零延迟何时生效见 [Network 执行架构](../../network/current/architecture.md), +逐步顺序可核对 [Network 执行器](../../../../braincell/network/engine.py)。 + +Cell 默认使用 staggered:电压阶段读取旧机制状态,在节点树上通过 DHS 解线性电压系统; +机制阶段使用新电压更新门控等状态。依赖离子总电流的机制按快照与调度约定读取电流。 +通用 Euler/RK 等路径通过导数协议推进,轴向算子消去边界节点后作用于 CV 电压。 +当前这条显式路径对端点点机制的反馈存在缺项,具体反例和方案见 +[边界输入提案](../../cell/proposals/explicit-solver-boundary-inputs.md)。 + +两条路径的装配、离子电流快照及更新顺序由 [Cell 求解架构](../../cell/current/architecture.md#电压与电流路径) +维护。Network 的 `run` 负责收集运行结果;可微训练使用准备好的 `update`, +在 `brainstate.transform.for_loop` 等变换中直接读取状态并构建损失。 + +Cell 也可以通过 `use_model(name)` 选择已挂载的 ReductionModel,替代详细动力学。 +这时 Network 仍管理原 Cell 的连接和事件投递,约化模型接收分组的突触输入并持有自己的动态状态; +输入 schema、输出记录及生命周期见 [约化模型接入指南](../../reduction/current/model-integration-guide.md)。 + +## 单位、参数与外部依赖 + +仿真入口的长度、时间、电压、电流等参数使用 `brainunit.Quantity`,例如 `0.025 * u.ms`。 +密度电导与突触总电导分别使用 `u.mS / u.cm**2` 和 `u.uS`,二者通过膜面积建立联系。 +无量纲参数根可以经 Trainable 的映射产生带单位物理量;梯度损失和绘图需要纯数组时, +用 `.to_decimal(unit)` 明确选定单位。Vis 也接受纯数值数组,其单位标签由调用方指定。 + +| 依赖 | 在执行流程中的作用 | +| --- | --- | +| BrainState / JAX | 状态管理、编译循环、自动微分与设备数组计算 | +| BrainUnit | 物理量运算、单位转换和量纲检查 | +| BrainTools | 参数变换、优化器、代理梯度等工具 | +| BrainEvent | 可选事件执行路径中的稀疏算子 | +| BrainPy / NumPy / SciPy | 动力学与科学计算工具 | +| Matplotlib / PyVista | Vis 的二维和三维后端,使用时加载 | + +版本约束和 extras 由 [pyproject.toml](../../../../pyproject.toml) 维护。 +Vis 迁移到 BrainTools、提供简单绘图与 GUI 的方案见 [Vis TODO](../../vis/TODO.md)。 + +## 公共入口与设计讨论 + +公开导入从 `braincell` 顶层或已公开的领域子包进入,具体导出见 +[顶层入口](../../../../braincell/__init__.py) 和各子包 `__all__`。 +实现放在 `_discretization` 等内部路径,不改变其顶层重导出对象的公开身份; +例如使用 `bc.CVPerBranch`,实现位置则供开发者查阅。 + +当前 `bc.mech.Channel` / `bc.mech.Ion` 是声明,`bc.Channel` / `bc.Ion` 是运行时基类。 +跨模块命名与公共导出约定见 [接口一致性讨论](../proposals/interface-consistency.md); +模块内部接口问题进入对应 TODO,系统级进度见 [Architecture TODO](../TODO.md)。 diff --git a/docs/design/architecture/proposals/interface-consistency.md b/docs/design/architecture/proposals/interface-consistency.md new file mode 100644 index 00000000..bac425f3 --- /dev/null +++ b/docs/design/architecture/proposals/interface-consistency.md @@ -0,0 +1,63 @@ +# 跨模块接口一致性讨论 + +状态:待讨论。当前有两类跨模块接口问题:声明类型与运行时类型同名,以及公开对象的实现路径位于内部包。 +目标是让读者从导入、类型和文档判断对象的用途与兼容约定。当前结构见 +[系统总览](../current/system-overview.md),事项进度见 [Architecture TODO](../TODO.md)。 + +## 声明与运行时类型的名称 + +当前 `bc.mech.Channel` 保存机制声明,`bc.Channel` 是运行时通道基类;Ion 也有同样的命名关系。 +例如以下两行分别用于组装模型和检查运行时类型: + +```python +import braincell as bc +import brainunit as u + +declaration = bc.mech.Channel("IL", g_max=0.1 * u.mS / u.cm**2) +assert issubclass(bc.channel.IL, bc.Channel) +``` + +限定命名空间时含义明确;同时导入两个同名类、阅读未带限定名的类型标注时,读者需要额外判断其层次。 +声明包含初始化时实例化与参数绑定等语义,不能当作运行时机制直接更新。 +依据见 [声明实现](../../../../braincell/mech/_density.py) 和 +[运行时基类](../../../../braincell/_base_channel.py)。 + +| 方案 | 调用与阅读效果 | 主要代价 | +| --- | --- | --- | +| 保留名称,统一示例使用 `bc.mech.Channel` / `bc.Channel` | 用命名空间表达声明与运行时层次,现有调用继续使用 | 裸类名和简短类型标注仍可能混淆 | +| 声明增加显式名称,如 `ChannelSpec` / `IonSpec` | 在导入后仍可直接辨认声明对象 | 需要决定是否保留旧名、迁移时长,以及 Synapse 等声明是否采用同样规则 | +| 运行时基类增加显式名称,如 `ChannelBase` / `IonBase` | 自定义机制继承关系更直观 | 影响已有子类、类型标注、导出及文档,覆盖面更大 | + +下一步先统计公共示例、扩展类和类型标注中两类对象的使用方式。 +需要决定的是:命名空间约定是否足以避免实际误用;若增加名称,哪一层值得承担迁移成本。 + +## 公共导出与实现路径 + +`bc.CV`、`bc.CVPerBranch` 等公开对象实现于 `_discretization`。 +这种安排已符合仓库的包命名约定:下划线路径属于内部实现,顶层重导出的名称是公共入口。 +例如当前合法调用为: + +```python +policy = bc.CVPerBranch(3) +``` + +需要明确的是:公开导出一个类型时,哪些成员、构造方式和返回结构属于受支持的契约。 +仅有一个类名列表,无法回答 CV 查询字段与内部缓存字段是否具有相同的兼容要求。 +依据见 [顶层导出](../../../../braincell/__init__.py) 和 +[包命名约定](../../../../AGENTS.md#note-on-package-naming)。 + +| 方案 | 契约表达 | 主要代价 | +| --- | --- | --- | +| 以显式导出和模块 API 文档共同定义 | `__all__` 表达入口,API 文档列出支持的构造、成员与返回结构 | 新增或修改导出时需要同步核对文档 | +| 再维护一份带稳定性等级的导出清单 | 可区分成熟接口、实验接口和仅供查询的结果类型 | 清单与导出、模块 API 有重复,需要自动校验避免漂移 | + +倾向先使用模块 API 作为完整契约入口。若确有需要同时发布的实验接口,再决定统一的稳定性标记。 +下一步核对顶层与子包导出对应的文档,找出尚无成员契约的公开类型,以及用户直接依赖内部路径的实例。 + +## 模块内的问题 + +| 问题 | 维护位置 | +| --- | --- | +| `reset` / `reset_state` 的用途是否应在名称上更明确,查询是否统一 property/method | [Cell TODO](../../cell/TODO.md) | +| `Morphology` / `Branch` 是否同时从 `braincell.morph` 导出 | [Morph TODO](../../morph/TODO.md) | +| 通道名称与旧别名的当前约定 | [Channel TODO](../../channel/TODO.md) | diff --git a/docs/design/cell.md b/docs/design/cell.md deleted file mode 100644 index ce5a81a9..00000000 --- a/docs/design/cell.md +++ /dev/null @@ -1,292 +0,0 @@ -# Cell 前端层规范(重写版) - -## 目标与边界 - -`cell` 现在既做前端建模,也作为运行时主对象。 - -- 入口固定为:`Cell(morpho, cv_policy=CVPerBranch())`;`braincell.MultiCompartment` - 是 `Cell` 的别名(`MultiCompartment is Cell`),只是换一个更能说明模型类型的 - 名字,与下面「不暴露中间壳对象」并不冲突 -- 自动离散得到 `CV` 集合 -- 支持 `paint` / `place` 规则积累与查询 -- 支持懒重建(改了 `cv_policy`、`paint`、`place` 后置脏) - -本层明确不做: - -- 不单独再暴露 `CellExecution` 这类中间壳对象(`MultiCompartment` 只是 `Cell` - 的别名,不是另一个对象) -- 不要求用户手动再包装一层执行对象 - ---- - -## 核心类列表(必须保留的主干) - -### `class Cell` - -主对象,持有: - -- 形态快照:`morpho` -- 离散策略:`cv_policy` -- 离散结果:`cvs` -- 原始规则:`paint_rules`、`place_rules` -- 编译缓存:layout / runtime node / ion / state buffer -- 运行时状态:`V`、`spike` - -对外接口: - -- `paint(region, *mechanisms)` -- `place(locset, *point_mech)` -- `n_cv` -- `cvs[i] -> CV` -- `init_state()` -- `reset_state()` -- `pre_integral() / compute_derivative() / post_integral() / update()` -- `layouts` -- `get_state()` / `set_state()` -- `get_point_state()` / `get_cv_state()` -- `get_ion()` - -### `class CV` - -一个 CV 对应一个 branch 区间: - -- `region = (branch_id, prox, dist)` - -拓扑信息: - -- `parent_cv` -- `children_cv` - -包含信息: - -- 本 CV 覆盖的圆台切片列表(可能含截断插值点) -- 可选 `as_branch()`:返回一个由本 CV 切片构造出的新 `Branch` - -中点属性(CV 的核心属性都定义在中点): - -- `cm` -- `ra` -- `v` -- `temp` - -派生属性: - -- `length` -- `area` -- `r_axial_prox` -- `r_axial_dist` -- `r_axial` - -机制容器: - -- `density_mech`(按机制类型索引) -- `point_mech`(按位点存放,可落在 `mid` 或边界 node,并在 `CV` 上保留位置角色) - -### `class CVPolicy` - -离散策略基类。 - -- 默认:`CVPerBranch()`,即每个 branch 1 个 CV -- 支持 `CVPerBranch(cv_per_branch=...)`:每个 branch 使用统一 `cv_per_branch` -- 支持 `MaxCVLen(max_cv_len=..., keep_odd=True)`:按 `max_cv_len` 计算每个 branch 的 CV 数量 - - 规则:先算 `n = max(1, ceil(branch_total_length / max_cv_len))` - - 若 `keep_odd=True` 且 `n` 为偶数,则提升为 `n + 1` - - 语义:默认对齐 NEURON 风格偏好奇数分段;若要严格长度上限可用 `keep_odd=False` -- 支持 `DLambda(d_lambda=..., frequency=100 * u.Hz, keep_odd=True)`:按 branch 级电长度决定 CV 数量 - - `d_lambda` 必须显式给出 - - `frequency` 默认 `100 Hz` - - `keep_odd=True` 时,自动分段数若为偶数则提升到下一个奇数 - - `Ra/cm` 不从 policy 参数读取,而是从默认 cable 和 `paint(CableProperties)` 推导 - - 支持不同 branch 使用不同 `Ra/cm` - - 若同一 branch 内 `Ra/cm` 不一致,则直接报错,要求统一该 branch 或改用其他 `cv_policy` - - `v_rest` / `temperature` 不参与 `DLambda` 的 branch 内一致性检查 - -`PaintRule` / `PlaceRule` 仍存在于实现内部,用于保存标准化后的声明,但不再作为公开主接口导出或文档重点强调。 - ---- - -## 当前代码组织(实现约定) - -- `cell/cell.py`:`Cell` 对外接口与重建编排(总控) -- `cell/cv.py`:`CV` 与 `CV` 组装逻辑 -- `cell/cv_policy.py`:`CVPolicy` 基类和各类离散策略 -- `cell/cv_geo.py`:`CVGeo` + `CVFrustum`,负责离散、几何、拓扑映射 -- `cell/cv_mech.py`:内部规则与 CV 机制应用 -- `_discretization/topology.py`:`NodeTree` 与 node graph builder -- `_compute/scheduling.py`:`NodeScheduling` 与 scheduler builder -- `cell/runtime.py`:内部编译 helper,负责 mechanism grouping、layout lowering、runtime node 与 state query - ---- - -## CV 与离散点语义 - -每个 CV 有三个几何参考点: - -- 近端点 `prox_node` -- 中点 `mid_node` -- 远端点 `dist_node` - -说明: - -- 这三个点用于后续矩阵组装视角 -- 单树中 `n` 个 CV 共有 `2n+1` 个唯一计算点(边点可共享) -- 但本层状态与膜相关计算只在 `mid_node` - -硬规则: - -- `cm/ra/v/temp` 只在中点定义 -- 膜电流、离子电流、电极电流、突触电流都归中点 -- 边点只用于后续 KCL 矩阵的一行约束,不在本层存独立膜状态 - ---- - -## 几何与属性计算规则 - -### CV 几何切片 - -`CV` 覆盖的是 branch 的一个区间 `(prox, dist)`,需要得到该区间包含的圆台切片。 - -- `lengths + radii` 输入:在 `prox` / `dist` 最多插值两次 `radius` -- `xyz + radii` 输入:在 `prox` / `dist` 最多插值两次 `xyz` 与 `radius` - -### 派生属性 - -- `length = (dist - prox) * branch.total_length` -- `area = CV 内所有圆台侧面积之和` - -### 轴向电阻 - -`Branch` 本身没有 `ra`,所以轴向电阻在 `CV` 上计算。 - -- `r_axial_prox`:CV 长度前半段轴阻 -- `r_axial_dist`:CV 长度后半段轴阻 -- `r_axial = prox + dist` - ---- - -## paint / place 语义 - -### paint - -支持覆盖: - -- 电缆属性:`cm`、`ra`、`v`、`temp` -- 密度机制:`ion`、`channel` - -规则: - -- 默认进来有 1 条全区域 `CableProperties` paint 规则 -- 规则按声明顺序覆盖(后写覆盖前写) -- 同一 `region` 的 `CableProperties` 规则只保留最后一次声明 -- `channel` 部分覆盖按膜面积比例缩放 `gmax` -- `ion` 不按体积/面积比例缩放,只做机制挂载与参数覆盖 - -### place - -点机制吸附到 CV 中点。 - -映射规则: - -- 点位于某 CV 内部:归该 CV -- 点恰好位于 CV 边界:归后一个 CV(右侧 CV) - - 例:`[0,0.5]` 与 `[0.5,1]`,`x=0.5` 归后者 -- 特殊规则:`(branch0, 1.0)` 仍归 `branch0` 的末端 CV - - 即使 `branch0` 在该点连接了 child branch,也不跳到 child 的 CV - ---- - -## 查询、编译与懒重建 - -`Cell` 层查询包含两类内容: - -- 原始规则:`paint_rules`、`place_rules` -- 离散结果:`cvs[i]` 的属性与机制视图 -- 编译结果:`layouts`、`get_state()`、`get_ion()` 等 - -懒重建触发条件: - -- 修改 `cv_policy` -- 新增或修改 `paint` -- 新增或修改 `place` - -触发后行为: - ---- - -## 当前实现进展补充 - -- `Cell.update(I_ext)` 现在按旧版 `MultiCompartment` 语义接收外部总电流,默认单位是 `u.nA` -- 若传入的是总电流,`Cell` 内部会按 CV 面积换成电流密度后再参与膜方程 -- 若传入的已经是电流密度 `u.nA / u.cm**2`,当前实现也兼容 -- `staggered` 电压求解器的热路径已尽量改为 `jnp` / `u.math`,`numpy` 仅保留在静态拓扑与编译期数据整理中 - -- 仅标记 `dirty` -- 下次查询 `n_discretization/cvs` 时重建前端离散结果 -- 下次 `init_state()` 时重新编译 runtime,并重建真实 state - -运行时约束: - -- `paint/place/cv_policy` 修改后,必须重新 `init_state()` -- `reset_state()` 可以在需要时隐式补编译 -- `update()` / `compute_derivative()` 不会隐式编译,未初始化会直接报错 - ---- - -## 不兼容与迁移约束(必须执行) - -本规范不兼容旧别名。旧名字要么改名,要么删除,不保留兼容壳。 - -需要退出主模型语义的旧对象: - -- `CVRecord` -- `CellAssembly` -- `ControlVolume` -- `Discretizer`(可作为内部 helper,但不作为用户主接口) -- 旧 `DiscretizationPolicy` 命名(迁移到 `CVPolicy`) - -文档标准以 `Cell + CV` 为唯一主干,其他均为内部实现细节。 - ---- - -## 当前进展 - -### 已完成 - -- `Cell` 已直接继承 `HHTypedNeuron` -- `Cell.init_state()` 负责 runtime 编译与 state 创建 -- `Cell` 已直接接管 `reset_state/pre_integral/compute_derivative/post_integral/update` -- `CellRuntimeState` 退化为内部编译缓存,不再作为公开主接口 -- `braincell.mech.Channel("IL")` 与 `braincell.mech.Channel("Na_HH1952")` 已能创建真实 runtime channel,并绑定到默认 `na/k/ca` -- `Cell` 已可直接查询 `layouts/get_state/get_point_state/get_cv_state/get_runtime_node/get_ion` -- `Cell.V` 的公开尺寸现在固定为 `pop_size + (n_cv,)`;`pop_size` 默认 `(1,)` 且不允许为空 -- painted density channel / ion 按 CV dense axis 创建,公开尺寸为 `pop_size + (n_cv,)`; - `node_tree.n_point` 仅用于 DHS algebraic workspace 和 sparse point mechanisms -- `Cell` 的全部 hidden state 都是 `brainstate.HiddenGroupState`(`V` 为 `braincell.DiffEqGroupState`), - 尾轴即 CV 或 sparse point-layout row;`SingleCompartment` 无空间轴,仍用普通 `DiffEqSingleState`。 - 参见 `docs/specs/2026-08-13-cell-hidden-group-state.md` -- `braincell.quad._staggered.dhs_voltage_step()` 已改为从 `node_tree` 中的调度视图提取树结构 -- `Cell(solver="staggered")` 已可直接走新的 node-tree DHS 电压求解 - -### 当前约束 - -- `density_mech` runtime 直接使用 CV domain;需要 node visualization 时才映射到 `cv_to_mid_node_id` -- 默认离子先固定为全局 `na/k/ca` -- `dense/sparse` 自动阈值切换接口已保留,但阈值策略尚未实装 -- DHS solver 会把 CV coefficient/RHS 装配到 `n_point` tree rows,只在 midpoint row 写入/读回 -- 当前 buffer 以 bridge view 为主,参数/状态先用 object array 承载,不做真实数值积分 - -### 下一步 - -- 已接入 `braincell.mech.Channel("IL", ...)` 这类简短 spec,`Cell.paint(...)` 可直接接受,旧 `DensityMechanism` 仍兼容 -- 已打通 `Channel("IL", ...) -> CellRuntimeState.runtime_nodes -> braincell.channel.IL(size=(n_cv,))` -- 当前 `set_state(layout_id, var_name, value)` 会同步更新 bridge buffer 和已注册的 `IL` runtime node 参数 -- 已增加默认全局固定 ion 容器:`runtime.ions["na" | "k" | "ca"]` -- 已打通 `Channel("Na_HH1952", ...) -> runtime.get_runtime_node(layout_id) -> runtime.get_ion("na").channels["Na"]` -- 当前 `INa_HH1952` spec 里的 `temp` 会在 runtime bridge 中转换成底层构造参数 `phi` -- 当前仍只支持 dense runtime;`k/ca` 容器已创建但还未绑定新的真实 channel -- 下一步优先做更多 ion-bound channel 映射,或者设计 `cell.ion[...]` / `cell.soma.channel[...]` 这种更直接的 facade - -### 公开面收口 - -- `braincell.cv`、`braincell.compute` 与顶层 `braincell` 只稳定导出:`Cell`、`CV`、`CVPolicy*`、`NodeTree`、`NodeScheduling`、`CellProfileReport` -- `PaintRule`、`PlaceRule`、`MechanismLayout`、`CellExecution` 不再作为稳定公开 API diff --git a/docs/design/cell/TODO.md b/docs/design/cell/TODO.md new file mode 100644 index 00000000..a5a95606 --- /dev/null +++ b/docs/design/cell/TODO.md @@ -0,0 +1,30 @@ +# Cell TODO + +Cell 的具体事项与下一步集中在这里;跨模块目标与依赖见 [全局 TODO](../TODO.md), +文档分工、状态定义和维护规则见 [Design 规范](../AGENTS.md)。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步 | 文档 | +| --- | --- | --- | --- | +| 显式 solver 的边界输入完整性 | 讨论中 | 根据五行方程及消元缺项,比较恢复边界约束与共享 point 装配的方案 | [边界输入提案](proposals/explicit-solver-boundary-inputs.md) | +| SingleCompartment 与 MultiCompartment 统一 | 讨论中 | 从单 branch、单 CV 开始,衔接现有集中参数模型与 Cell 的用法 | [统一提案](proposals/single-multi-compartment-unification.md) | +| 特殊 policy 与 single 位点表达 | 讨论中 | 确定中点校验的归属及省略 locset 的快捷方式 | [Policy 与位点](proposals/single-multi-compartment-unification.md#特殊-policy-与位点表达) | +| single 与 Cell 的状态轴差异 | 待讨论 | 对应状态读写、population、batch 及参数单位 | [状态与参数](proposals/single-multi-compartment-unification.md#状态形状与参数) | +| 分叉 morphology 的 single 表示 | 讨论中 | 比较等效圆柱体与跨 branch 单 CV 的几何、机制和位置映射 | [形态兼容](proposals/single-multi-compartment-unification.md#形态与旧接口的兼容) | +| single 专用积分路径 | 讨论中 | 先验证现有路径的等价性,再评估省去边界装配的收益和 solver 一致性 | [积分路径](proposals/single-multi-compartment-unification.md#积分路径是否需要单独实现) | +| 生命周期命名 | 待讨论 | 比较保留 reset/reset_state 与增加明确别名,保留重建结构和重置状态的语义区别 | [生命周期](current/api.md#生命周期) | +| 查询接口风格 | 待讨论 | 按读取缓存、触发构建及参数需求检查 node_tree/runtime/layouts,判断是否需要统一 property/method | [静态查询](current/api.md#静态查询)、[运行时查询](current/api.md#运行时查询) | + +## 已实现内容 + +| 内容 | 入口 | +| --- | --- | +| 构造与分段、paint/place、生命周期、运行结果、状态查询及积分协议 | [Cell API](current/api.md) | +| 模块与数据归属、离散构建、状态布局、时间推进及求解路径 | [Cell 架构](current/architecture.md) | +| 离子电流快照与更新排序 | [调度契约](current/architecture.md#离子电流快照与调度) | + +## 参考 + +- [Arbor CV Discretization](references/arbor-cv-discretization.md):跨 branch CV 表示、属性汇总与近似局限。 +- [系统总览](../architecture/current/system-overview.md):跨模块关系、数据归属与典型流程。 diff --git a/docs/design/cell/current/api.md b/docs/design/cell/current/api.md new file mode 100644 index 00000000..b5895582 --- /dev/null +++ b/docs/design/cell/current/api.md @@ -0,0 +1,391 @@ +# Cell API + +`braincell.Cell` 用 morphology、离散策略和机制声明构建电缆模型,并持有运行时状态。 +`braincell.MultiCompartment is braincell.Cell`;独立的 `SingleCompartment` 与 Cell 的兼容方向见 +[统一提案](../proposals/single-multi-compartment-unification.md)。内部计算见 [Cell 架构](architecture.md)。 + +| 任务 | 入口 | +| --- | --- | +| 构造与分段 | [Cell 构造](#cell-构造)、[CVPolicy](#cvpolicy) | +| 声明膜属性、刺激与突触 | [Paint 与 Place](#paint-与-place) | +| 初始化、重置与运行 | [生命周期](#生命周期)、[运行与结果](#运行与结果) | +| 查询几何、布局与状态 | [静态查询](#静态查询)、[运行时查询](#运行时查询) | +| 接入积分器 | [积分协议](#积分协议) | +| 空间 View、记录、训练和约化模型 | [相关接口](#相关接口) | + +## 最小用法 + +单 branch、三个 CV 的被动膜模型,在中间 CV 注入电流,并记录该处电压: + +```python +import braincell as bc +import brainunit as u +from braincell.filter import AllRegion, RootLocation + +branch = bc.Branch.from_lengths( + lengths=[60.0] * u.um, + radii=[2.0, 2.0] * u.um, + type="dendrite", +) +cell = bc.Cell( + bc.Morphology.from_root(branch), + cv_policy=bc.CVPerBranch(3), + V_init=-65.0 * u.mV, +) +cell.paint( + AllRegion(), + bc.mech.Channel("IL", g_max=0.1 * u.mS / u.cm**2, E=-65.0 * u.mV), +) +cell.place( + RootLocation(0.5), + bc.mech.CurrentClamp( + delay=0.2 * u.ms, durations=0.5 * u.ms, amplitudes=0.01 * u.nA, + ), +) +cell.loc(RootLocation(0.5)).record("v_mid", bc.observe.state("v")) +result = cell.run(dt=0.025 * u.ms, duration=1.0 * u.ms) +voltage = cell.V.value +trace = result.samples["v_mid"].values +assert voltage.shape == (1, 3) +assert result.time.shape == (40,) +assert trace.shape == (40, 1) +``` + +`voltage` 是末态电压,`trace` 的两轴分别为采样时刻和观测行;两者都保留电压单位。 +后续示例复用这里已经初始化的独立 `cell`、`result` 和导入。 + +## Cell 构造 + +完整调用签名: + +```text +Cell( + morpho, *, + pop_size=1, cv_policy=None, + V_th=0 * u.mV, V_init=None, + spk_fun=braintools.surrogate.ReluGrad(), + solver="staggered", subsolver=None, substeps=None, + cache_ion_total_current=True, ion_channel_update_order="family", + membrane_linearizer="point", name=None, +) -> Cell +``` + +| 参数 | 类型与默认值 | 含义 | +| --- | --- | --- | +| `morpho` | `Morphology`,必填 | 声明期形态;初始化时复制为运行时快照 | +| `pop_size` | 正整数或非空正整数序列,`1` | 共享形态的 population 尺寸;整数转为单元素 tuple | +| `cv_policy` | `CVPolicy or None`,`None` | `None` 选择 `CVPerBranch()` | +| `V_th` | 电压 Quantity,`0 * u.mV` | spike 检测阈值,可广播到 `pop_size + (n_cv,)` | +| `V_init` | 电压 Quantity、callable 或 `None`,`None` | `None` 读取各 CV 静息电位;数值广播到电压形状 | +| `spk_fun` | callable,`ReluGrad()` | 将阈值归一化后的无量纲电压差映射成 spike,用于阈值跨越检测及替代梯度 | +| `solver` | 注册名称或 callable,`"staggered"` | 单步积分器,Cell 调用 `solver(cell)`,由其就地推进状态 | +| `subsolver`、`substeps` | 名称/callable 与正整数,同为 `None` | 默认 backward Euler、1 子步;显式设置必须成对提供,用于 Markov channel / kinetic ion | +| `cache_ion_total_current` | bool,`True` | 为需要总电流驱动的离子模型缓存旧状态电流 | +| `ion_channel_update_order` | `"family" / "integration"`,`"family"` | 电压求解后的机制更新顺序,见 [调度](architecture.md#离子电流快照与调度) | +| `membrane_linearizer` | `"point" / "generic"`,`"point"` | 保留的线性化选择参数;当前两值没有对应两套不同的电压线性化实现 | +| `name` | `str or None`,`None` | Cell 名称 | + +构造返回处于声明阶段的 Cell,已有静态离散预览,动态 `V/spike` 在初始化时创建。 +`V_init` 为 callable 时按 `initializer(shape)` 求值,`shape=pop_size + (n_cv,)`, +应返回该形状的电压量;初始化与 `reset_state()` 都会重新求值。空间电缆参数的 +`fn(CVContext)` 是另一种回调,见 [空间参数](../../filter/current/spatial-callable-parameters.md)。 + +`V_init`、`V_th`、`cv_policy`、`solver`、`spk_fun` 和 `membrane_linearizer` +可在声明阶段赋值,初始化后赋值抛出 `RuntimeError`。错误的形态或 policy 类型抛出 +`TypeError`;非正 population 尺寸、非法枚举或不完整的 subsolver 配置会报错。 + +## CVPolicy + +策略从 `braincell` 导入,构造结果传给 `Cell(cv_policy=...)`。现有策略都按 branch 划分, +因此 `CVPerBranch(1)` 在分叉形态上产生多个 CV。 + +| 完整构造形式 | 参数与划分规则 | +| --- | --- | +| `CVPerBranch(cv_per_branch=1)` | 正整数;每个 branch 使用相同 CV 数量 | +| `CVPerBranchList(cv_per_branch)` | 正整数序列;按 `morpho.branches` 顺序指定数量,长度必须等于 branch 数 | +| `MaxCVLen(max_cv_len, keep_odd=True)` | 正的标量长度量;数量约为 `max(1, ceil(length / max_cv_len))`,考虑几何容差 | +| `DLambda(d_lambda, frequency=100 * u.Hz, keep_odd=True)` | 正的无量纲 `d_lambda` 和正的标量频率;根据 branch 电长度计算数量 | +| `CVPolicyByTypeRule(branch_types, policy)` | 非空 branch 类型字符串 tuple 与 `CVPolicy`,创建一条分派规则 | +| `CompositeByTypePolicy(rules, default_policy)` | 规则 tuple 与默认 `CVPolicy`;按 branch 类型选择策略,后匹配的规则覆盖先匹配者 | + +`keep_odd=True` 将计算所得的偶数 CV 数量提升到下一个奇数。`DLambda` 从默认电缆属性及 +`paint(CableProperty)` 读取 `Ra/cm`,要求同一 branch 内为一致标量,不同 branch 可以不同; +同 branch 内不一致或使用 callable 时会报错。静息电位与温度不参与这项检查。 + +组合策略对整个 morphology 求解各子 policy,再取对应 branch 的结果。因此子 policy +仍须适用于整个输入形态,例如 `CVPerBranchList` 仍需覆盖全部 branch。 +策略参数类型错误通常抛出 `TypeError`,非正数、长度不匹配或不一致的电缆属性抛出 `ValueError`。 + +扩展入口为抽象方法 `CVPolicy.resolve_cv_bounds(morpho, *, paint_rules=None)`, +返回按 branch 排列的 `tuple[tuple[(prox, dist), ...], ...]`,坐标沿 branch 长度归一化。 +`paint_rules` 是 Cell 传入的内部声明,供依赖电缆属性的策略使用。 +实现与分段计算见 [policy.py](../../../../braincell/_discretization/policy.py)。 + +## Paint 与 Place + +```text +Cell.paint(region, *mechanisms) -> Cell +Cell.place(locset, *mechanisms) -> Cell +``` + +两者在声明阶段累积规则、使离散缓存失效,均返回原 Cell,支持链式调用。 +初始化后调用抛出 `RuntimeError`;密度声明传给 paint,点声明传给 place。 + +### 区域声明 + +`region` 接受 `RegionExpr`,例如 `AllRegion()`;直接传入 `RegionMask` 会抛出 `TypeError`。 +`*mechanisms` 是 +`braincell.mech.CableProperty`、`Channel` 或 `Ion` 等密度声明。 + +| 声明 | 覆盖与重复声明行为 | +| --- | --- | +| `CableProperty` | 初始有一条全区域默认规则;按 CV 中点覆盖关系和声明顺序解析,同一 region 保留最后一次声明 | +| density channel | 独立保留机制声明,离散后检查 identity 冲突;部分覆盖按实际膜面积比例缩放电流 | +| ion | 挂载机制并解析参数,机制本身不按覆盖面积或体积比例缩放 | + +### 点声明 + +`*mechanisms` 接受 CurrentClamp、Synapse、Probe 等点机制声明。 +`locset` 的三种表达为: + +| 输入类型 | 位置如何应用 | +| --- | --- | +| `LocsetExpr / LocsetMask` | 所有 population 成员共享同一组位置 | +| `LocsetBatch` | 矩形、逐成员对齐的位置;要求一维 population,行数等于成员数 | +| 每个成员一个 locset 的序列 | 各成员位置数可不同;要求一维 population、序列长度匹配,当前用于 Synapse | + +place 保留输入顺序和重复位置;同一位置多次声明的突触仍是独立实例。 +placement 保留原始连续位置,分别解析所属 CV 和电气 point: + +| 放置位置 | 所属 CV | 电气 point | +| --- | --- | --- | +| CV 内部 | 包含该位置的 CV | 该 CV 中点 | +| branch 内部 CV 分界 | 右侧 CV | 右侧 CV 中点 | +| branch 端点或连接处 | 声明位置所在 branch 的首/末 CV | 对应边界节点,连接处可共享 | + +例如两个区间 `[0, 0.5]`、`[0.5, 1]` 的 `x=0.5` 归后者; +`(branch0, 1.0)` 始终归 branch0 的末端 CV,即使电气节点与子 branch 共享。 + +端点刺激和突触由 staggered/DHS 的边界行消费。当前显式 Euler/RK 使用的通用导数遗漏了 +边界输入反馈,切换 solver 会影响结果;原因见 [边界输入提案](../proposals/explicit-solver-boundary-inputs.md)。 + +## 生命周期 + +```text +Cell.init_state(batch_size=None) -> None +Cell.reset_state(batch_size=None) -> None +Cell.reset() -> None +``` + +`batch_size` 为 `None` 或正整数:`None` 使用 population 与空间轴,整数增加前置 batch 轴。 +重置已有批次时传回相同的 `batch_size`。 + +| 方法 | 前置状态 | 状态与结构变化 | +| --- | --- | --- | +| `init_state` | 声明阶段 | 复制形态、构建运行时、创建电压和机制状态;时间归零,声明冻结 | +| `reset_state` | 已初始化 | 按当前参数与初态重新设置动态状态,时间归零并清空本地待处理输入;保留声明和运行时结构 | +| `reset` | 已初始化 | 丢弃运行时与动态状态,恢复声明期形态引用;保留 paint/place 声明,返回可编辑阶段 | + +`init_state()` 重复调用、初始化前调用两种 reset,以及 Network 管理的 Cell 独立调用 +这些生命周期方法,均抛出 `RuntimeError`。由 Network 管理时使用其生命周期入口。 + +```python +cell.reset_state() +assert cell.current_time == 0.0 * u.ms +cell.reset() +cell.cv_policy = bc.CVPerBranch(5) +cell.init_state() +assert cell.V.value.shape == (1, 5) +``` + +后续示例继续使用这个五 CV、已初始化的 `cell`。 + +## 运行与结果 + +### 连续运行 + +```text +Cell.run(*, dt, duration) -> RunResult +``` + +| 参数 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `dt` | 正的标量时间 Quantity | 必填 | 固定积分步长 | +| `duration` | 正的标量时间 Quantity | 必填 | 本次运行时长,必须为 `dt` 的整数倍 | + +首次调用自动初始化,后续调用从当前状态和时间继续,返回一个新的结果对象。 +调用会就地更新电压、机制状态、spike 和当前时间。Network 管理的 Cell 独立 run 会抛出 +`RuntimeError`;不带时间单位抛出 `TypeError`,非正时间或非整数步数抛出 `ValueError`。 + +### RunResult + +从 `braincell.RunResult` 导出,由 `run` 返回。结果字段与映射只读,数组值用于后续分析。 + +| 字段 | 类型与含义 | +| --- | --- | +| `time` | 时间 Quantity,形状 `(n_steps,)`,位于 `[start_time, stop_time)` | +| `traces` | `probe_name -> values`;旧 Probe 接口的兼容结果,首轴为时间,尾轴随 probe 选择而定 | +| `samples` | `recording_name -> SampleBlock`;通过 `Cell.record` 声明的记录 | +| `start_time / stop_time / dt` | 本次时间段起止与步长,均为时间 Quantity | + +```text +RunResult.concat(parts) -> RunResult +``` + +`parts` 为非空、按时间排列的 RunResult 序列。要求时间段连续、步长相同、recording 名称和 +schema 一致,违反时抛出 `ValueError`。返回新的合并结果,输入对象保持原值; +`traces` 仅合并所有时间段共有的 probe 名称。 + +```python +first = cell.run(dt=0.025 * u.ms, duration=1.0 * u.ms) +second = cell.run(dt=0.025 * u.ms, duration=1.0 * u.ms) +joined = bc.RunResult.concat((first, second)) +assert first.stop_time == second.start_time +assert joined.time.shape == (80,) +``` + +Recording 的采样规则与 SampleBlock 见 [记录接口](../../network/current/recording.md#recording-and-results); +结果实现见 [run.py](../../../../braincell/_multi_compartment/run.py)。 + +### 单步推进 + +```text +Cell.update() -> spike_value +``` + +要求已初始化并设置环境 `dt`;缺少步长时抛出 `ValueError`。 +调用就地推进状态并返回无量纲 spike,形状与当前 `Cell.V` 相同。 +`t` 优先取环境值,未设置时读取 `cell.current_time`。 + +`update()` 不推进 `current_time`;循环驱动者负责时间。独立连续运行使用 `run()`。 +单步调用示例: + +```python +import brainstate + +@brainstate.transform.jit +def one_step(): + with brainstate.environ.context(t=cell.current_time, dt=0.025 * u.ms): + return cell.update() + +spike = one_step() +assert spike.shape == cell.V.value.shape +``` + +## 静态查询 + +下面的属性在初始化前后均可读取。离散结果按需构建,声明或形态 revision 改变后重建。 + +| 只读属性 | 返回内容 | +| --- | --- | +| `morpho` | 当前 Morphology 引用;声明期为输入对象,初始化后为快照 | +| `paint_rules / place_rules` | 标准化声明 tuple | +| `pop_size / varshape` | population shape / `pop_size + (n_cv,)`,后者不包含可选 batch 轴 | +| `n_cv` | CV 数量,整数 | +| `cvs` | 按 CV 顺序排列的不可变记录 tuple | +| `cv_midpoints` | 每个 CV 一个连续中点位置的 `LocsetMask` | +| `node_tree` | NodeTree,`node_tree.n_point` 包含中点及边界节点 | +| `point_placements` | 保留原始位点和空间索引的点机制 placement 序列 | + +CV 是单 branch 区间的静态记录,动态膜电压通过 `Cell.V` 读取: + +| CV 字段 | 含义与单位 | +| --- | --- | +| `branch_id, prox, dist, midpoint` | branch 索引及沿长度归一化的位置 | +| `region, parent_cv, children_cv` | RegionMask 及相邻 CV 索引 | +| `cm, ra, v, temp` | 比膜电容、轴向电阻率、静息电位、温度,均带相应物理单位 | +| `length, area, radius_mid, diam_mid` | 长度、膜面积、中点半径与直径 | +| `r_axial_prox, r_axial_dist, r_axial` | 两个半 CV 及整个 CV 的轴向电阻 | +| `density_mech, point_mech, point_mech_roles` | 密度与点机制声明;roles 保存原始位置和局部几何角色 | + +CV 与 point 的关系及几何计算见 [离散表示](architecture.md#cv-与-point)。 + +## 运行时查询 + +本节针对已初始化的详细电缆模型,初始化前调用抛出 `RuntimeError`。 +约化模式的状态入口见 [相关接口](#相关接口)。 + +### 电压、布局与机制对象 + +| 属性或完整调用形式 | 返回值 | +| --- | --- | +| `V / spike` | 状态对象;用 `.value` 读取当前电压 Quantity / 无量纲 spike | +| `n_point` | 运行时电气点数量 | +| `voltage_shape` | 布局中的 `pop_size + (n_cv,)`;实际批次形状读取 `V.value.shape` | +| `layouts` | `MechanismLayout` tuple,各项的 `id` 用于布局查询 | +| `get_point_layouts(point_id)` | 覆盖该电气 point 的 layout tuple | +| `get_cv_layouts(cv_id)` | 归属于该 CV 的 layout tuple | +| `get_runtime_node(layout_id)` | 原运行时机制对象,非副本 | +| `get_ion(name)` | 原离子容器;按唯一名称或无歧义的类/家族别名解析 | + +`cv_id`、`point_id` 为从 0 开始的整数,越界抛出 `IndexError`。 +未注册的 runtime node 或 ion 抛出 `KeyError`;离子别名匹配多个容器时抛出 `ValueError`。 +`get_runtime_node` 返回对象中的隐藏状态由对应机制定义,例如门控变量的 `.value`。 + +### 布局 Buffer 读写 + +```text +Cell.expected_state_shape(layout_id, var_name) -> tuple +Cell.get_state(layout_id, var_name) -> buffer_value +Cell.set_state(layout_id, var_name, value) -> None +Cell.get_point_state(point_id) -> dict +Cell.get_cv_state(cv_id) -> dict +``` + +`layout_id` 为布局 ID,`var_name` 为已注册 buffer 的字段名字符串。这里读取的是 +运行时布局 buffer,包括机制参数和 clamp 序列;任意门控状态不一定注册在其中。 +膜电压读写用 `Cell.V.value`,机制隐藏状态通过运行时机制对象访问。 + +| 操作 | 返回与修改行为 | +| --- | --- | +| `expected_state_shape` | 指定 buffer 的已登记形状 | +| `get_state` | buffer 的当前值,保留单位与布局形状 | +| `set_state` | 更新已有 buffer 并同步对应机制参数,返回 `None`;不创建新字段 | +| `get_point_state` | 新字典 `layout_id -> {var_name: point_value}` | +| `get_cv_state` | 新字典,包含该 CV 中点的点机制 buffer 与密度机制的 CV 切片 | + +未知 buffer 键抛出 `KeyError`。赋值使用与现有 buffer 相容的单位和形状; +标量可填充整个 buffer,形状不匹配抛出 `ValueError`。优先先读后写,避免猜测布局尺寸: + +```python +leak = next(layout for layout in cell.layouts if layout.target == "density") +g_max = cell.get_state(leak.id, "g_max") +assert g_max.shape == cell.expected_state_shape(leak.id, "g_max") +cell.set_state(leak.id, "g_max", 0.5 * g_max) +``` + +clamp 的 ragged 序列 buffer 还支持逐位点的时长、幅值序列,写入会更新 padding 和 mask; +常用操作通过 [ClampView](views.md#clampview) 完成。 +布局读取、赋值和离子名称解析的实现见 [state.py](../../../../braincell/_compute/state.py)。 + +## 积分协议 + +以下接口供积分器在已初始化 Cell 上调用。`update()` 和 `compute_derivative()` +均不接收 `I_ext`;外部输入通过 clamp、突触和电流输入路径汇总。 + +| 完整调用形式 | 结果与状态影响 | +| --- | --- | +| `pre_integral() -> None` | 调用非独立积分机制的预处理钩子 | +| `compute_derivative() -> None` | 写入 `V.derivative` 与非独立积分机制的导数,供积分器组合 stage | +| `post_integral() -> None` | 将 delta 输入应用到 V,并调用非独立机制的后处理钩子 | +| `compute_membrane_derivative(V)` | 当前膜电流密度除以比膜电容 | +| `compute_axial_derivative(V)` | CV 轴向算子产生的电压导数,首次调用可能构建缓存 | +| `compute_voltage_derivative(V)` | 上述两个导数之和 | + +三个电压导数方法接收与 `Cell.V.value` 形状相容的电压 Quantity, +返回相同电压空间的电压/时间 Quantity,不把结果写回膜电压。 +机制状态从当前 Cell 读取,膜输入时间取环境 `t` 或 `current_time`。 +通用导数的边界输入限制见 [电压与电流路径](architecture.md#电压与电流路径)。 + +## 相关接口 + +| 能力 | 权威说明 | +| --- | --- | +| population 与空间选择,channel/ion/synapse views | [Cell Scope and Mechanism Views](views.md#cell-scope-and-mechanism-views) | +| 事件连接 | [Synapse and Connection](../../network/current/connections.md#synapse-and-connection) | +| `Cell.record`、观测选择与采样 | [Recording and Results](../../network/current/recording.md#recording-and-results) | +| `View.trainable` 与可训练参数 | [Trainable API](../../optim/current/api.md) | +| `add_reduction(name, model)`、`use_model(name="detailed")` | [约化模型接入指南](../../reduction/current/model-integration-guide.md) | + +源码与回归入口:[Cell](../../../../braincell/_multi_compartment/cell.py)、 +[Cell 测试](../../../../braincell/_multi_compartment/cell_test.py)。 diff --git a/docs/design/cell/current/architecture.md b/docs/design/cell/current/architecture.md new file mode 100644 index 00000000..3391a0d7 --- /dev/null +++ b/docs/design/cell/current/architecture.md @@ -0,0 +1,201 @@ +# Cell 架构 + +详细电缆 Cell 将形态与机制声明转换成 CV、电气节点和机制状态,再交给 solver 推进。 +建模调用见 [Cell API](api.md),约化模型的宿主路径见 +[模型接入指南](../../reduction/current/model-integration-guide.md)。 + +| 要理解的问题 | 章节 | +| --- | --- | +| 数据由谁持有、哪些对象共享 | [模块分工](#模块分工)、[数据归属](#数据归属) | +| 声明如何成为可运行模型 | [从声明到运行时](#从声明到运行时) | +| CV、边界节点与机制状态如何对应 | [CV 与 Point](#cv-与-point)、[状态布局](#状态布局) | +| 一步积分如何消费电流并更新状态 | [电压与电流路径](#电压与电流路径)、[离子电流快照与调度](#离子电流快照与调度) | + +## 模块分工 + +下图只表示三个内部包的依赖方向:箭头从使用方指向提供方。 + +```mermaid +flowchart TD + CELL["_multi_compartment"] --> DISC["_discretization"] + CELL --> COMPUTE["_compute"] + COMPUTE --> DISC +``` + +| 模块 | 职责与主要产物 | +| --- | --- | +| [`_multi_compartment/cell.py`](../../../../braincell/_multi_compartment/cell.py) | 用户声明、生命周期与运行阶段编排;`currents.py` 汇总电流,`probes.py` 查询观测,`run.py` 执行连续推进 | +| [`_discretization`](../../../../braincell/_discretization) | `policy` 划分 branch 区间,`geometry` 计算几何,`mechanism` 解析声明,`node_build` 构造电气节点;`base.build_discretization` 汇总为 `Discretization` | +| [`_compute`](../../../../braincell/_compute) | `layouts` 定义机制空间索引,`ions/bindings` 创建并绑定运行时机制,`state` 分配状态;`bridge` 映射 CV/point,`table` 提供机制查询,`scheduling` 构造树求解顺序 | +| [`quad`](../../../../braincell/quad) | 积分协议、机制积分器与电压求解器;默认 staggered 调用 DHS 求解节点树 | + +## 数据归属 + +Cell 是声明和运行时的宿主,机制声明与实际参与积分的机制对象分别保存: + +| 数据 | 持有者 | 创建与更新时机 | +| --- | --- | --- | +| 原 morphology、policy、paint/place 规则 | Cell 声明 | 构造及声明操作;原 morphology 可由多个 Cell 引用 | +| CV、NodeTree、placement 与空间上下文 | Cell 的 Discretization 缓存 | 按声明按需重建;初始化后对应形态快照 | +| layout、运行时机制对象、参数 buffer、事件 buffer | CellRuntimeState | `from_cell` 构建;运行阶段读写已有 buffer | +| `V`、`spike`、当前时间 | Cell | 初始化创建,积分与循环驱动更新 | +| 门控、浓度、连续突触等动态状态 | 运行时 channel / ion / synapse 对象 | 机制的 init/reset 钩子创建或重置,积分器推进 | +| point 电压、轴向算子、DHS 工作数据 | Cell 与求解器缓存 | 按求解路径准备;边界电压由 DHS 约束求解 | + +布局 buffer 提供参数和声明值的空间存储,机制对象持有自身动力学状态,两者通过绑定连接。 +因此 `get_state(layout_id, name)` 查询的是注册 buffer;任意隐藏状态通过相应机制对象读取。 +公共读取与赋值入口见 [运行时查询](api.md#运行时查询)。 + +## 从声明到运行时 + +下面的箭头表示数据构建顺序,与上面的依赖图含义不同。 + +```mermaid +flowchart TD + DECL["形态、policy、paint/place"] --> DISC["CV 与 NodeTree"] + DISC --> BIND["机制布局与绑定"] + BIND --> STATE["电压与机制状态"] + STATE --> READY["已初始化的 Cell"] +``` + +以 API 页的三 CV 模型为例,`paint` 保存密度通道声明,`place` 保存刺激的原始位置。 +`record` 单独保存观测声明,不新增点机制或改变机制布局。 +静态查询通过 `build_discretization` 生成三个 CV、五个 point,以及声明到这些位置的映射。 +此时可以检查几何和 placement,运行时机制状态尚未创建。 + +`init_state()` 复制形态并重新离散,随后 `CellRuntimeState.from_cell` 分配布局与 buffer, +实例化并绑定运行时机制。Cell 将它们挂到运行时对象树,物化已注册的可训练参数,创建 +`V/spike` 等状态,再在 group-state 上下文中调用机制的初始化与重置钩子。 +离子与通道的依赖绑定因此先于状态初始化,初态读取的是已解析的参数和 CV 电压。 + +声明或形态 revision 改变会使静态缓存失效。`reset_state()` 复用运行时结构, +`reset()` 则丢弃运行时并恢复原声明期形态引用;具体调用条件见 [生命周期](api.md#生命周期)。 + +树调度由 `build_node_scheduling` 从 NodeTree 构建。DHS 静态求解数据与通用导数使用的 +CV 轴向矩阵分别按需求建立,初始化不会预先构造稠密 CV 轴向矩阵。 + +## CV 与 Point + +一个 CV 覆盖一个 branch 区间,并承载一个动态膜电压。NodeTree 另保留无膜电容的 +边界节点,用于端点和分叉处的电流平衡。例如单 branch、三个 CV: + +```text +位置 x 0 1/6 1/2 5/6 1 +角色 边界 ---- CV0 ---- CV1 ---- CV2 ---- 边界 +电压 V_p V_0 V_1 V_2 V_d +膜电容 0 C_0 C_1 C_2 0 +``` + +这是三行膜电压 ODE 加两行边界代数约束。内部 CV 分界 `x=1/3, 2/3` 不创建节点。 +一般构建规则为每个 CV 一个中点、根端点及每个 branch 的末端节点,分叉连接处共享节点。 +单 branch、单 CV 因此有三个点,但只有一个动态膜电压自由度。 + +CV 字段与单位见 [静态查询](api.md#静态查询)。当前 CV 表示限于单 branch 区间。 +跨 branch 的连通区间表示及其属性汇总见 +[Arbor 参考](../references/arbor-cv-discretization.md),候选演进见 +[两个类的统一](../proposals/single-multi-compartment-unification.md)。 +类型与构建依据为 [base.py](../../../../braincell/_discretization/base.py)、 +[node_build.py](../../../../braincell/_discretization/node_build.py)。 + +### 几何计算 + +离散保留 CV 区间内原有的几何采样点,在截断边界插值半径和可用空间坐标。 +一个 CV 可以包含多个圆台片段,直接对这些片段计算: + +$$ +L_{\mathrm{CV}}=(\mathrm{dist}-\mathrm{prox})L_{\mathrm{branch}},\qquad +A_{\mathrm{CV}}=\sum_k A_k,\qquad +R_{\mathrm{axial}}=R_{\mathrm{prox}}+R_{\mathrm{dist}}. +$$ + +$A_k$ 为第 k 段圆台侧面积;两个半区间的轴阻使用该 CV 解析出的 `ra` 和原始半径分布计算, +对应 `r_axial_prox`、`r_axial_dist`。长度、面积、轴阻分别具有长度、面积、电阻单位。 +总膜电容为 $C_{\mathrm{tot}}=\mathrm{cm}\,A_{\mathrm{CV}}$。 +计算实现见 [geometry.py](../../../../braincell/_discretization/geometry.py)。 + +## 状态布局 + +令 `P=cell.pop_size`,`N=cell.n_cv`;可选 batch 轴位于 population 轴之前。 + +| 数据 | 空间轴与作用 | +| --- | --- | +| `Cell.V` | `P + (N,)`,可增加前置 batch 轴;使用 `DiffEqGroupState` | +| density channel / ion 状态 | CV 尾轴;机制自身的状态维度由其模型定义 | +| 普通点布局 | 以活跃 point/placement 行代替 CV 轴,通过 layout 索引读取对应电压 | +| packed 突触布局 | 将不同成员的逻辑突触打包为一条行轴,由 population 与 point 索引共同映射电压 | +| point 电压和 DHS 工作区 | NodeTree 电气点轴,包含 CV 中点及边界代数节点 | + +例如 `pop_size=(2,)`、三个 CV 时,V 的形状为 `(2, 3)`;带 `batch_size=4` 时为 `(4, 2, 3)`。 +五个 point 不会使 V 变为长度五的状态。 +机制隐藏状态在 group-state 上下文中创建,空间尾轴参与积分器的状态合并。 +独立 `SingleCompartment` 的膜电压则为 `DiffEqSingleState`,没有空间尾轴。 +布局索引依据见 [MechanismLayout](../../../../braincell/_compute/layouts.py)。 + +## 电压与电流路径 + +### 单步编排与时间 + +独立 `Cell.update()` 的外层顺序为: + +1. 准备本步 clamp,应用已准备的突触事件。 +2. 调用 `solver(cell)` 推进连续状态。 +3. 清除本步离子电流快照,检测新旧电压的阈值跨越并写入 spike。 +4. 准备下一步突触输入。 + +`update()` 读取环境时间或 Cell 当前时间,本身不推进时钟。`run()` 通过编译循环管理每步 +时间,收集记录,并在时间段结束后更新 `current_time`。Network 执行时,时间与延迟事件 +到达由 Network 调度;这些 Cell 内部阶段由其调用。 + +### 两条电压路径 + +| 阶段 | 默认 staggered / DHS | 通用电压导数,供显式 Euler/RK 等使用 | +| --- | --- | --- | +| 电流求值 | `total_membrane_rate_point`:密度机制读取 CV 电压,点突触读取所在 point 电压,clamp 进入对应 point | `total_membrane_current(host, V_cv=V, t=t)`:密度机制读取 CV 电压,点机制经 CV/point 映射求值 | +| 膜电流汇总 | 中点按膜电容处理,边界绝对电流进入约束行 | CV 密度电流加中点输入,再除以比膜电容 | +| 轴向处理 | 在 NodeTree 上装配含边界行的时间离散系统 | `build_cv_axial_operator` 对纯轴向矩阵消去边界;`compute_axial_derivative` 应用所得算子 | +| 电压推进 | `dhs_voltage_step` 同时求解边界电压和 CV 电压 | `compute_voltage_derivative` 返回膜导数与轴向导数之和,由积分器推进 | +| 机制推进 | 电压求解后使用新电压更新机制 | 各积分 stage 经 `compute_derivative` 求值,独立积分机制按其协议推进 | + +通用导数在 CV 空间计算: + +$$ +\dot{\mathbf V} +=\mathbf j_{\mathrm{mid}}(\mathbf V,z,t)\oslash\mathbf c_m +-A_{\mathrm{CV}}\mathbf V. +$$ + +这里 $\mathbf j_{\mathrm{mid}}$ 为密度机制及中点输入汇总的膜电流密度,向内为正, +$\mathbf c_m$ 为比膜电容,$z$ 为当前机制状态,$\oslash$ 表示逐元素除法。 +$A_{\mathrm{CV}}$ 是含电容归一化的约化轴向算子,单位为时间的倒数;两项均为电压/时间。 +代码中的 `Cell.C` 保存比膜电容,与几何段落的总膜电容 $C_{\mathrm{tot}}$ 区分。 + +通用路径中,`bridge.cv_to_point` 只写中点电压,`point_to_cv` 只取中点贡献。 +因此端点 clamp 的电流及端点突触的电压反馈没有进入 CV 导数。 +五节点系统的完整方程、正确消元后的缺项和待验证场景见 +[边界输入提案](../proposals/explicit-solver-boundary-inputs.md)。 + +源码入口:[电流汇总](../../../../braincell/_multi_compartment/currents.py)、 +[空间映射](../../../../braincell/_compute/bridge.py)、 +[staggered 与轴向算子](../../../../braincell/quad/_staggered.py)、 +[Runge-Kutta](../../../../braincell/quad/_runge_kutta.py)。 + +## 离子电流快照与调度 + +默认 `cache_ion_total_current=True`。staggered 在电压和机制更新前,为标记 +`uses_total_current` 的离子模型计算并缓存旧状态总电流,避免读取部分更新后的通道状态; +关闭后由模型按其电流计算路径求值。 + +电压求解后,`ion_channel_update_order` 决定机制更新顺序: + +| 设置 | 顺序 | +| --- | --- | +| `"family"`,默认 | 连续突触状态,然后离子自身状态,再更新通道;各类内部按是否独立积分分组 | +| `"integration"` | 按顶层运行时节点的积分类型组织,先更新非独立状态,再调用独立更新入口 | + +两条路径均按机制选用相应积分器。staggered 的电压阶段读取旧机制状态,机制阶段读取新电压, +构成一阶分裂。此顺序属于 Cell 的调度契约;离子反应声明见 +[KineticIon](../../ion/current/kinetic-ion.md)。实现与回归入口见 +[Cell](../../../../braincell/_multi_compartment/cell.py)、 +[staggered](../../../../braincell/quad/_staggered.py)、 +[Cell 测试](../../../../braincell/_multi_compartment/cell_test.py);NEURON 比较配置见 +[小脑比较进度](../../../../examples/neuron_compare/cerebellum-import-progress.md)。 diff --git a/docs/design/cell/current/views.md b/docs/design/cell/current/views.md new file mode 100644 index 00000000..4c2a01df --- /dev/null +++ b/docs/design/cell/current/views.md @@ -0,0 +1,118 @@ +# Cell Spatial and Mechanism Views + +View 保存对同一 Cell 的选择,读写共享声明或运行时数组。Cell 构造及根对象 paint/place 见 [Cell API](api.md),观测声明见 [Recording](../../network/current/recording.md)。下方 `text` 块表示依赖已有 Cell 的查询和调用形式。 + +## 最小用法 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import AllRegion, RootLocation + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[3.0, 3.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), pop_size=2, cv_policy=bc.CVPerBranch(1)) +cell.paint(AllRegion(), bc.mech.Channel("IL", name="leak", g_max=0.1*u.mS/u.cm**2, E=-65*u.mV)) +selected = cell[1:2].channels["leak"] +selected.set(g_max=0.2*u.mS/u.cm**2) +cell[1:2].loc(RootLocation(0.5)).record("selected_v", bc.observe.state("v")) +result = cell.run(dt=0.025*u.ms, duration=0.1*u.ms) +assert result.samples["selected_v"].values.shape == (4, 1) +assert u.math.allclose(selected.get("g_max"), 0.2*u.mS/u.cm**2) +``` + +## Cell Scope and Mechanism Views + +Cell 与 CellView 使用同一套空间选择顺序: + +```text +population members -> branch name/type or region -> CV -> mechanism rows +``` + +```text +cell[[0, 2]] +cell[[0, 2]].dendrite +cell[[0, 2]].dendrite.cv[1:] + +cell.soma.channels +cell.dendrite.ions +cell[1:3].synapses +cell[1:3].connections +``` + +空间 View 只保存索引,不复制 Cell、morphology 或 runtime arrays。机制的公共身份和最小逻辑行如下。 + +| Category | Type | Name | Extra identity | Logical row | +| --- | --- | --- | --- | --- | +| Channel | runtime model,例如 `Na_HH1952` | 用户声明的 owner,例如 `nav` | - | `(population, CV, type, name)` | +| Ion | implementation,例如 `SodiumFixed` | owner,例如 `na_pool` | species,例如 `na` | `(population, CV, type, name)` | +| Synapse | runtime model,例如 `ExpSyn` | group,例如 `fast_ampa` | stable logical ID | 一个独立 Synapse instance | +| Connection | source-to-synapse routing | connect call name | stable row ID | 一行 routing | + +| View | Type selector | Name selector | Other selectors | Numeric slicing | +| --- | --- | --- | --- | --- | +| Channel | `by_type(type)` | `view[name]` | - | 不支持独立 logical row slicing | +| Ion | `by_type(type)` | `view[name]` | `by_species(species)` | 不支持独立 logical row slicing | +| Synapse | `by_type(type)` | `view[name]` | stable IDs | 支持,且保序 | +| Connection | `by_source_type(type)`, `by_synapse_type(type)` | connect/synapse name | stable row IDs | 支持,且保序 | + +```text +cell.channels.by_type("IL") +cell.channels["leak_soma"] + +cell.ions.by_species("na") +cell.ions.by_type("SodiumFixed") +cell.ions["na_pool"] + +cell.synapses.by_type("ExpSyn") +cell.synapses["fast_ampa"] +cell.synapses["fast_ampa"][[0, 2]] +``` + +Channel/Ion 的 `get(field)` 和 `set(**fields)` 要求最终 View 只包含一个 `(type, name)` owner。Synapse +`get/set` 要求同一 type,但可以跨同 type 的多个 name。View 在初始化前读取声明参数;初始化后通过 +logical-to-runtime mapping 读取 runtime parameter/state,不保存第二份数组。 + +### `CellView.place` + +```text +CellView.place(locset, *mechanisms) -> CellView +``` + +在选定的 Population members 上放置独立 point mechanism instances。根对象的完整调用条件见 +[Cell.place](api.md#paint-与-place)。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `locset` | `LocsetExpr`, `LocsetMask`, `LocsetBatch`, or sequence | required | 共享位置、矩形批量位置,或每个 member 一个可不等长的位置集合。 | +| `*mechanisms` | point mechanism declarations | required | 要放置的 point mechanisms;异质 per-cell locset 当前用于 Synapse 声明。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `CellView` | 返回当前 Population view。 | + +#### Notes + +`place` 保留输入位置顺序和重复位置。相同位置、相同 Synapse type/name 的多次放置仍是独立 logical +Synapse instances;runtime 按 Synapse type 组织 SoA storage。 + + +### `ClampView` + +`cell.clamps` 返回所有逻辑电流 clamp 的稳定 view。Clamp 没有 semantic name,字符串下标按类型选择; +类型筛选可继续使用普通位置索引: + +```text +dc = cell.clamps["CurrentClamp"] +second_dc = dc[1] +cell.clamps.by_type(braincell.CurrentClamp).record("dc_inputs") +``` + +记录结果位于 `result.samples["dc_inputs"]`;每个 logical clamp 对应一列,不跨位置求和。该 +`SampleBlock.time` 是实际刺激求值时间 `step_start + 0.5 * dt`。Solver 在整个主步内消费与 recording +相同的缓存值,包括 Runge-Kutta 的所有局部阶段。 + +机制观测的组合用法见 [Recording scopes](../../network/current/recording.md#recording-scopes)。 diff --git a/docs/design/cell/proposals/explicit-solver-boundary-inputs.md b/docs/design/cell/proposals/explicit-solver-boundary-inputs.md new file mode 100644 index 00000000..40becc8d --- /dev/null +++ b/docs/design/cell/proposals/explicit-solver-boundary-inputs.md @@ -0,0 +1,179 @@ +# Explicit Solver Boundary Inputs Proposal + +## 目前的问题 + +状态:讨论中。当前 staggered/DHS 装配了包含边界刺激和突触的边界条件;显式 Euler/RK +使用的通用导数路径把边界 point 消去后,只保留无边界输入时的 CV 轴向算子, +没有把边界电流和突触的作用同步带入剩下的电压方程。 + +因此,模型可以在边界成功 place,但切换到当前显式路径后,这些输入对膜电压的作用会遗漏。 +下面用一个 branch、3 CV、5 个节点把完整方程和缺失项列出来。问题是当前导数缺项, +提高显式方法的阶数不能补回;显式积分方法本身并不要求丢弃边界条件。 + +运行路径见 [Cell 架构](../current/architecture.md#电压与电流路径),进度见 [Cell TODO](../TODO.md)。 +下文依据源码分析和方程推导;显式 Euler/RK 的端点输入仍需数值复现与独立参考解对照。 + +## 例子:一个 branch、3 CV、5 个节点 + +取单个 branch,沿归一化位置均匀划分为 3 CV。当前 node tree 是: + +```text +node 0 1 2 3 4 +x 0 1/6 1/2 5/6 1 +role boundary CV1 mid CV2 mid CV3 mid boundary + o---a0-----o---a1-----o---a2-----o---a3-----o +``` + +节点 1、2、3 承载膜电容和动态膜电压,节点 0、4 是无膜电容的边界约束点。 +内部 CV 分界 x=1/3、2/3 不另建节点。这里的五行是 **3 行膜电压 ODE 加 2 行边界代数约束**, +不把突触、通道等机制自身的状态方程计入这五行。 + +| 符号 | 含义 | +| --- | --- | +| $V_0,\ldots,V_4$ | 五个节点的电压 | +| $C_1,C_2,C_3$ | 三个 CV 的总膜电容,即膜面积乘比膜电容 | +| $a_0,\ldots,a_3$ | 相邻节点间的轴向电导;中点之间包含相邻半 CV 的串联电阻 | +| $F_i$ | CV i 的总膜电流及中点输入,不含轴向电流和端点输入 | +| $J_0(t),J_4(t)$ | 两端 CurrentClamp 的绝对电流,向内为正 | +| $s_0(t),s_4(t)$ | 两端电导型突触的瞬时电导 | +| $E_0,E_4$ | 两端突触的反转电位 | + +所有电流统一采用向内为正。示例使用欧姆型突触 $I_{\mathrm{syn}}=s(E-V)$, +求边界电压时视该时刻的 s 为已知;s 自身仍可由突触动力学演化。 +取有限正轴向电导和非负突触电导,保证下面的端点约束可解。 + +## 完整的五行方程 + +沿节点 0 到 4 排列: + +$$ +\begin{aligned} +0 &= J_0+s_0(E_0-V_0)+a_0(V_1-V_0), &&\text{左边界}\\ +C_1\dot V_1 &= F_1+a_0(V_0-V_1)+a_1(V_2-V_1), &&\text{CV1}\\ +C_2\dot V_2 &= F_2+a_1(V_1-V_2)+a_2(V_3-V_2), &&\text{CV2}\\ +C_3\dot V_3 &= F_3+a_2(V_2-V_3)+a_3(V_4-V_3), &&\text{CV3}\\ +0 &= J_4+s_4(E_4-V_4)+a_3(V_3-V_4). &&\text{右边界} +\end{aligned} +$$ + +第一、第五行要求边界输入与流向相邻 CV 的轴向电流平衡。它们决定 $V_0,V_4$, +再通过第二、第四行的轴向项影响 $V_1,V_3$;CV2 随后通过与两侧 CV 的耦合受到影响。 + +### Staggered 为什么可以消费边界输入 + +staggered/DHS 对这五个节点构造时间离散后的方程。以左端为例,边界行可以整理为: + +$$ +(a_0+s_0)V_0-a_0V_1=J_0+s_0E_0. +$$ + +$J_0$ 进入右端项,突触同时贡献对角系数 $s_0$ 和右端项 $s_0E_0$。右端边界同理。 +当前 point-space 电流装配保留这些边界贡献,DHS 求解边界电压和 CV 电压, +因此端点刺激和突触能通过边界条件实际作用于膜节点。 + +边界行没有 $C\dot V$,属于代数约束。数值比较还需控制时间离散误差,检查步长收敛。 + +## 当前显式路径只剩下什么 + +当前 build_cv_axial_operator 只对纯轴向矩阵做边界消元,相当于在构造这个算子时采用: + +$$ +a_0(V_1-V_0)=0,\qquad a_3(V_3-V_4)=0. +$$ + +于是 $V_0=V_1$、$V_4=V_3$,两端轴向项在约化算子中消失。另一方面, +通用膜电流路径只取中点贡献,没有把端点上的 J 和突触电流补回来。 +这个例子中的电压导数因此对应以下三行: + +$$ +\begin{aligned} +C_1\dot V_1 &= F_1+a_1(V_2-V_1),\\ +C_2\dot V_2 &= F_2+a_1(V_1-V_2)+a_2(V_3-V_2),\\ +C_3\dot V_3 &= F_3+a_2(V_2-V_3). +\end{aligned} +$$ + +这三行适用于端点没有额外输入的情形。即使端点已经 place 了 CurrentClamp 或 Synapse, +当前通用路径也没有保留上面完整边界条件产生的反馈。所有消费这个导数的显式 Euler/RK +都会继承这一缺项;其他复用通用导数的 solver 也需要核查。 + +## 正确消元后少不了的两项 + +从完整的第一、第五行解出端点电压: + +$$ +V_0=\frac{a_0V_1+J_0+s_0E_0}{a_0+s_0}, +\qquad +V_4=\frac{a_3V_3+J_4+s_4E_4}{a_3+s_4}. +$$ + +代回相邻 CV 的轴向项,得到边界对 CV 的实际输入: + +$$ +\begin{aligned} +B_L &= a_0(V_0-V_1) + =\frac{a_0}{a_0+s_0}\big[J_0+s_0(E_0-V_1)\big],\\ +B_R &= a_3(V_4-V_3) + =\frac{a_3}{a_3+s_4}\big[J_4+s_4(E_4-V_3)\big]. +\end{aligned} +$$ + +所以,保留三行 ODE 时,完整形式应当是: + +$$ +\begin{aligned} +C_1\dot V_1 &= F_1+a_1(V_2-V_1)+B_L,\\ +C_2\dot V_2 &= F_2+a_1(V_1-V_2)+a_2(V_3-V_2),\\ +C_3\dot V_3 &= F_3+a_2(V_2-V_3)+B_R. +\end{aligned} +$$ + +**当前缺少的就是第一、第三行中的 $B_L,B_R$,以及与这些输入一致的端点电压。** +在代码计算 dV/dt 时,对应缺少的是 $B_L/C_1$ 和 $B_R/C_3$。 +三个动态方程足以表达这个例子,前提是消元同步保留边界输入,而非只保留无输入的轴向算子。 + +### 三种情形检查 + +| 边界配置 | 左端正确反馈 $B_L$ | 当前三行遗漏了什么 | +| --- | --- | --- | +| 无刺激、无突触:$J_0=s_0=0$ | $0$ | 本例没有边界输入缺项 | +| 仅电流刺激:$s_0=0$ | $J_0$ | 注入左边界的电流应完整进入 CV1 | +| 仅突触:$J_0=0$ | $\frac{a_0s_0}{a_0+s_0}(E_0-V_1)$ | 突触产生的等效电导作用 | + +右端分别对应 $J_4,s_4,a_3,V_3$。只有突触时,令 +$s_{\mathrm{eff},L}=a_0s_0/(a_0+s_0)$,遗漏项可展开为: + +$$ +B_L=s_{\mathrm{eff},L}E_0-s_{\mathrm{eff},L}V_1. +$$ + +因此缺少的不只是外加电流常数,还包括电压反馈项。突触和刺激同时存在时,二者通过边界 +电压共同决定反馈,不能直接把 $J_0+s_0(E_0-V_1)$ 当作正确输入搬到中点。 + +上述式子中 a 和 s 均为电导,$a/(a+s)$ 无量纲,B 与 J、F 均为绝对电流, +$B/C$ 为电压变化率。非欧姆型或电压依赖电导需要重新求相应边界约束,不能直接套用本例闭式表达式。 + +## 对应到当前源码 + +| 位置 | 与本例的对应关系 | +| --- | --- | +| [node_build](../../../../braincell/_discretization/node_build.py) | 构造三个 CV 中点和 branch 两端,共五个节点 | +| [staggered](../../../../braincell/quad/_staggered.py) | DHS 保留边界行;build_cv_axial_operator 则只约化纯轴向矩阵 | +| [currents](../../../../braincell/_multi_compartment/currents.py) | total_membrane_rate_point 保留边界绝对电流;通用路径的 _clamp_density 只填充 midpoint_ids | +| [bridge](../../../../braincell/_compute/bridge.py) | point_to_cv 只取中点;cv_to_point 只散布中点值,不求解 $V_0,V_4$ | +| [Cell](../../../../braincell/_multi_compartment/cell.py) | compute_voltage_derivative 将中点膜项和约化轴向项相加,没有补齐本例的 $B_L,B_R$ | +| [Runge-Kutta](../../../../braincell/quad/_runge_kutta.py) | 各 stage 调用上述通用导数,积分阶数不会补回遗漏的输入 | + +## 后续讨论与验收入口 + +下一步目标是补齐上述边界输入及边界电压,保持合法 placement 的原始物理语义。 +可比较在 CV 导数求值时恢复边界约束、提取与 staggered 共用的 point-space 装配, +或将两者组合。 + +后续需要明确 RK stage 状态、刺激采样与事件投递、非线性边界、边界电压观测及缓存。 +验收先覆盖本例的无输入、仅刺激、仅突触和二者同时存在,再扩展到单 CV、多分支汇聚、 +电流守恒、population/batch、reset 和分段运行。在稳定步长下与解析或独立参考解比较并检查收敛。 +场景入口见 [staggered 端点测试](../../../../braincell/quad/_staggered_test.py)。 + +[两个 Compartment 类的统一提案](single-multi-compartment-unification.md) 的中点输入限制属于集中参数模型的范围, +不能用于回避普通 cable Cell 在这里缺失的边界输入。 diff --git a/docs/design/cell/proposals/single-multi-compartment-unification.md b/docs/design/cell/proposals/single-multi-compartment-unification.md new file mode 100644 index 00000000..ebc3d53d --- /dev/null +++ b/docs/design/cell/proposals/single-multi-compartment-unification.md @@ -0,0 +1,128 @@ +# SingleCompartment 与 MultiCompartment 的统一 + +状态:讨论中。 + +## 两个类如何统一 + +目标是让 `Cell` 同时表达多房室电缆模型和现有 `SingleCompartment` 的集中参数模型。 +当前 `MultiCompartment` 是 `Cell` 的别名,`SingleCompartment` 仍是独立类, +见 [Cell API](../current/api.md)。首阶段兼容现有 SingleCompartment 的集中参数 ODE 建模方式。 + +主要差异有三个:single 使用唯一膜电压位点;Cell 需要 morphology;Cell 的部分 solver +还会装配边界约束。下面先说明方程何时等价,再讨论位点、构造和积分路径的兼容。 +进度见 [Cell TODO](../TODO.md)。 + +## 单 CV 的三行方程 + +单 branch、单 CV 的电气节点为: + +```text +proximal endpoint ---- midpoint ---- distal endpoint + V_p g_p V_m g_d V_d +``` + +中点承载总膜电容 $C_{\mathrm{tot}}$,两端是无膜电容的代数约束点。取电流向内为正, +$g_p,g_d$ 为两段轴向电导,$I_p,I_d$ 包含端点刺激和突触电流,$I_m$ 为中点输入, +$I_{\mathrm{mem}}(V_m,z)$ 为总膜电流,$z$ 表示机制状态。完整电压系统是: + +$$ +\begin{aligned} +0 &= I_p+g_p(V_m-V_p),\\ +C_{\mathrm{tot}}\dot V_m + &= I_{\mathrm{mem}}(V_m,z)+I_m + +g_p(V_p-V_m)+g_d(V_d-V_m),\\ +0 &= I_d+g_d(V_m-V_d). +\end{aligned} +$$ + +这是一行膜电压 ODE 加两行边界约束。封闭边界且所有点机制和输入位于中点时, +$I_p=I_d=0$。在有限正轴向电导下,两端电压满足 $V_p=V_m=V_d$,轴向项消失: + +$$ +C_{\mathrm{tot}}\dot V_m=I_{\mathrm{mem}}(V_m,z)+I_m. +$$ + +匹配总电容、膜电流、初态及机制更新方式后,这就是 single 的膜电压方程。 +门控变量、离子浓度和突触仍有各自的状态。普通单 CV cable 可以接受端点输入, +其额外反馈见 [边界输入提案](explicit-solver-boundary-inputs.md);集中参数模式通过位点约束 +保证使用这里的单方程形式。 + +## 特殊 Policy 与位点表达 + +候选方案是用特殊 CV policy 指定 single 模式,同时表达“只有一个 CV”和“只有一个输入位点”。 +首阶段用于单 branch,保留 branch 的几何信息及其中的多个 segment。 +普通 `CVPerBranch(1)` 表示每个 branch 一个 CV;特殊 policy 还携带集中参数模式的放置规则。 + +| 操作 | Single 模式下的候选行为 | +| --- | --- | +| 显式传入 locset | 只接受唯一 CV 中点,边界或其他位置报错 | +| 省略 locset | 默认放到该 CV 中点 | +| 刺激、突触及其他输入 | 统一作用于唯一膜电压位点 | + +以下是候选写法,`single_policy` 表示待设计的策略对象: + +```python +cell = Cell(morpho, cv_policy=single_policy) + +# 两种等价的放置写法,任选其一。 +cell.place(midpoint, mechanism) +cell.place(mechanism) +``` + +省略的是 `place(locset, mechanism)` 中的 locset。显式传入的位置仍需校验, +快捷方式和其他输入入口共同遵守中点语义。 +需要决定:由 policy 自身负责校验,还是由 Cell 读取 policy 的模式后执行校验? + +## 形态与旧接口的兼容 + +Cell 构造时需要 morphology。为了接收已有的分叉形态,可以先生成一个集中参数表示, +再复用单 branch 的 single policy: + +```text +branched morphology -> lumped properties -> equivalent cylinder -> single policy +``` + +等效圆柱体至少需要匹配总膜面积和总膜电容。对原形态各段 k: + +$$ +A_{\mathrm{eq}}=\sum_k A_k,\qquad +C_{\mathrm{eq}}=\sum_k c_{m,k}A_k,\qquad +c_{m,\mathrm{eq}}=C_{\mathrm{eq}}/A_{\mathrm{eq}}. +$$ + +膜面积采用圆柱侧面积约定时,$A_{\mathrm{eq}}=\pi dL$。面积只约束直径与长度的乘积, +还需选择 d、L,以及处理离子池所需的体积。机制汇总需要保持目标膜电流;不同动力学参数 +是否可以合并,应按机制判断。原 branch 上的 paint 区域、place 位置和初始状态也需要映射。 +这一步将空间电压差异合并为一个电压自由度,是等电位近似;单方程与 single 的等价性针对 +汇总后的模型,而原多房室模型的空间传播与局部响应需要另行评价。 + +可比较两种形态表示: + +| 方案 | 表示方式 | 待解决问题 | +| --- | --- | --- | +| 等效圆柱体预处理 | 分叉 morphology 转成一个圆柱 branch,再使用现有单 branch CV 表示 | 几何与机制汇总、原位置映射、转换入口 | +| 原 morphology 跨 branch 单 CV | 保留原始形态,由一个 CV 覆盖多个连通区间 | 扩展当前 CV 表示及离散汇总,参见 [Arbor 参考](../references/arbor-cv-discretization.md) | + +先完成单 branch 的兼容,再确定分叉形态采用哪种表示。构造接口还需衔接旧 +SingleCompartment 的参数:由用户显式调用形态转换,还是由 Cell 的初始化入口生成所需 morphology? + +### 状态形状与参数 + +现有 single 没有空间尾轴,Cell 使用 `pop_size + (1,)`。需要对应旧类的状态读写、 +population 和 batch 用法,并明确总电流与电流密度、总电容与比膜电容的转换。 +形态入口的简化和这些参数、状态兼容一起决定两个类如何统一。 + +## 积分路径是否需要单独实现 + +特殊 policy 已经让模型满足单方程条件,接下来比较是否值得为它省去边界装配: + +| 方案 | 计算方式 | 需要验证 | +| --- | --- | --- | +| 复用现有 solver | staggered 保留边界装配;通用导数使用已有轴向消元 | 无边界输入时与 single 的导数、积分顺序和轨迹是否一致,以及额外开销 | +| 专用 single 分支 | 直接推进单行膜电压 ODE,复用膜电流与机制状态更新,省去边界装配和求解 | 性能收益、状态更新一致性及新增维护成本 | + +核心问题是:能否在现有 solver 协议内部选择 single 分支,保持相同的调用和机制更新语义? +若需要独立积分实现,应明确它与通用路径共享哪些计算,以及如何维持求解器之间的一致性。 + +先用被动膜、主动通道和中点输入验证复用路径,匹配参数、初态、积分器、步长与机制更新顺序, +比较导数和轨迹,并检查初始化、reset 和批量状态。再根据边界装配的实际开销决定是否增加专用路径。 diff --git a/docs/design/cell/references/arbor-cv-discretization.md b/docs/design/cell/references/arbor-cv-discretization.md new file mode 100644 index 00000000..961fa0f2 --- /dev/null +++ b/docs/design/cell/references/arbor-cv-discretization.md @@ -0,0 +1,60 @@ +# Arbor CV Discretization + +## 用途与关联 + +Arbor v0.12.2 的跨 branch CV 如何表示、如何汇总电学属性,是 Cell 形态兼容方案的参考。 + +| 关联内容 | 用途与采用状态 | +| --- | --- | +| [SingleCompartment 与 MultiCompartment 的统一](../proposals/single-multi-compartment-unification.md) | 比较等效圆柱体预处理与原形态跨 branch 单 CV;后续形态表示待选择 | +| [Cell 架构](../current/architecture.md#cv-与-point) | 当前实现的对照:BrainCell 的 CV 仍是单 branch 区间 | +| [Cell TODO](../TODO.md) | 查看后续问题和模块协作进度 | + +## 跨 branch CV 的表示 + +Arbor 的一个 CV 可以由原始 morphology 上多个连通的 cable 区间组成。每个区间记录 +branch 及其起止位置,原始几何与拓扑仍然保留。例如下图约定子 branch 接在 branch 0 远端: + +```text +branch 0 --------+-------- branch 1 + | + +-------- branch 2 +``` + +可以用以下两种方式理解划分,区间坐标均按各自 branch 的长度归一化: + +| 划分 | CV 所覆盖的区间 | +| --- | --- | +| 整树一个 CV | CV 0: branch 0、1、2 的 `[0, 1]` | +| 中央分叉 CV | CV 0: branch 0 的 `[0, 1]`,branch 1、2 的 `[0, 0.5]` | +| 上述划分的两个远端 CV | CV 1: branch 1 的 `[0.5, 1]`;CV 2: branch 2 的 `[0.5, 1]` | + +后两行合起来是一种三个 CV 的划分,CV 0 分别连接 CV 1、CV 2;不是把不连通的片段任意 +合并。Arbor 的 `cv_policy_single` 对整个 cell 生成一个 CV,对区域则每个连通分量一个 CV。 +显式边界策略以及允许分叉位于 CV 内部的策略支持跨 branch 的划分。参见 +[Arbor CV policy 与表示](https://docs.arbor-sim.org/en/v0.12.2/cpp/morphology.html)。 + +## 电学属性如何汇总 + +按 Arbor v0.12.2 的 `fvm_cv_discretize` 和 `apply_parameters_on_cv` 实现: + +- 面积由覆盖区间的膜面积求和,总膜电容由膜电容密度对面积积分。 +- 初始电压与温度按膜面积加权平均。 +- 密度机制参数在其覆盖区域内按面积汇总,并记录覆盖区域占整个 CV 的面积比例。 +- 相邻 CV 的轴向电阻按连接路径积分。无分叉 CV 使用中点作为参考位置;分叉 CV + 使用靠近相应接口的分叉点。一个 CV 电压不要求在形态上只有一个参考位置。 +- 实现也由总面积和总长度计算等效直径 `d = A / (pi L)`,并派生 `volume = A d / 4`。 + 这是离散后的派生几何量,不是先把整棵树变成圆柱再划分 CV。 + +源码见 [Arbor v0.12.2 fvm_layout.cpp](https://github.com/arbor-sim/arbor/blob/v0.12.2/arbor/fvm_layout.cpp)。 + +## 近似与使用边界 + +整树一个 CV 把空间膜电压自由度合为一个;在通常的封闭边界、无额外电耦合条件下, +没有相邻 CV 的轴向耦合项。这种等电位近似舍弃了空间传播与局部电压差异; +非线性机制的参数平均一般会改变动力学。参见 +[Arbor 离散电压方程](https://docs.arbor-sim.org/en/v0.12.2/dev/matrix_solver.html)。 + +整树一个 CV 与恰好 N 个 CV 是不同的划分问题;后者还需确定边界选择准则, +上述 Arbor 策略没有直接提供这种通用接口。BrainCell 的候选方向见 +[形态兼容讨论](../proposals/single-multi-compartment-unification.md#形态与旧接口的兼容)。 diff --git a/docs/design/channel/TODO.md b/docs/design/channel/TODO.md new file mode 100644 index 00000000..2b9538f7 --- /dev/null +++ b/docs/design/channel/TODO.md @@ -0,0 +1,29 @@ +# Channel TODO + +通道模板与通道实现的协作入口。[全局 TODO](../TODO.md) 管理宏观进度, +[Design 规范](../AGENTS.md) 定义分类和状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步或待决定问题 | 文档 | +| --- | --- | --- | --- | +| 参数单位元数据 | 待讨论 | 对齐构造签名和 Mech 错误定位,明确元数据 owner | [API](current/api.md)、[Mech TODO](../mech/TODO.md) | +| GHK 与 Q10 审计 | 实施中 | 逐家族核对原模型驱动力及参考温度,补剩余参数来源 | [辅助函数](current/api.md#电流与辅助函数)、[文献表](../ion/references/ion-channel-bibliography.md) | +| 逐模型 MOD 与刚性检查 | 待讨论 | 电压钳和电流钳对照,并按 dt/solver 检查收敛 | [Mech 验证框架](../mech/proposals/runtime-extensions.md#机制生成与验证) | +| 氯通道 | 待讨论 | 在 Chloride 家族确定后补对应通道 | [Ion TODO](../ion/TODO.md) | +| 门变量命名 | 待讨论 | 核对 p/q 与模型自定义名称的下游使用,设计兼容路径 | [模板 API](current/api.md#hh-与-gate) | + +## 已实现内容索引 + +- [API](current/api.md):HH/Markov 声明、生命周期、单位及可运行示例。 +- [通道模板约束](current/template-invariants.md):HH、OhmicHH、Markov 的声明校验、单位和裁剪策略。 +- [空间 callable 参数](../filter/current/spatial-callable-parameters.md):跨模块能力,由 Filter 维护。 + +通道已使用 `Na_HH1952`、`K_HH1952` 等现行名称,旧 `INa_HH1952` 等别名已移除; +`IL` 是当前漏通道名称。可用名称以 [Channel 导出](../../../braincell/channel/__init__.py) 为准, +迁移记录见 [通道简化](../../specs/2026-09-02-channel-simplification.md)。 + +## 参考与示例 + +- [Ion/Channel 文献表](../ion/references/ion-channel-bibliography.md):共享来源记录,仅保留这一份;缺失归因不视为已核实。 +- [小脑导入与比较进度](../../../examples/neuron_compare/cerebellum-import-progress.md):具体模型的验证工作由示例维护。 diff --git a/docs/design/channel/current/api.md b/docs/design/channel/current/api.md new file mode 100644 index 00000000..fb03e98d --- /dev/null +++ b/docs/design/channel/current/api.md @@ -0,0 +1,187 @@ +# Channel API + +`braincell.channel` 提供现成通道和 HH、Markov 模板。通道计算膜电流密度; +`braincell.mech.Channel` 则是安装到 Cell 的声明,二者的关系见 [Mech](../../mech/current/api.md)。 + +| 任务 | 入口 | +| --- | --- | +| 使用现成模型 | [Cell 中的通道](#cell-中的通道)、[公开模型目录](../../../../braincell/channel/__init__.py) | +| 定义门控动力学 | [HH 与 Gate](#hh-与-gate) | +| 定义状态转移 | [Markov 与 Transition](#markov-与-transition) | +| 选择驱动力与温度处理 | [电流与辅助函数](#电流与辅助函数) | +| 校验和裁剪的实现依据 | [模板约束](template-invariants.md) | + +## Cell 中的通道 + +下面安装一个钠通道,显式指定离子反转电位,并读取门状态。运行时通道的最后一轴是该 owner 覆盖的 CV; +前面的轴属于 Cell population。`g_max` 是单位面积电导,电压和浓度均需物理单位。 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import AllRegion + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[5.0, 5.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), cv_policy=bc.CVPerBranch(1)) +cell.paint(AllRegion(), bc.mech.Ion("SodiumFixed", E=50.0 * u.mV)) +cell.paint(AllRegion(), bc.mech.Channel("Na_HH1952", name="na", g_max=1.0 * u.mS / u.cm**2)) +cell.record("p", bc.observe.channel(name="na").state("p")) +result = cell.run(dt=0.025 * u.ms, duration=0.1 * u.ms) +assert result.samples["p"].values.shape == (4, 1) +``` + +现成通道通过具体构造器声明参数,默认值随模型而异,完整模型签名和出处分别见 +[源码目录](../../../../braincell/channel/) 和 [共享文献表](../../ion/references/ion-channel-bibliography.md)。 +参数的空间 callable 和 CV 上的求值规则见 [Filter](../../filter/current/spatial-callable-parameters.md)。 + +## HH 与 Gate + +模板构造及共同方法的完整签名: + +```text +HH(size, name=None) +OhmicHH(size, name=None) +GhkHH(size, name=None) +Gate(name, power=1, phi=None, q10=None, temp_ref=None, time_unit=u.ms, clip=False) +init_state(V, *ions, batch_size=None) -> None +reset_state(V, *ions, batch_size=None) -> None +compute_derivative(V, *ions) -> None +conductance_factor(V, *ions) -> dimensionless array +gate_phi(gate) -> dimensionless value +current(V, *ions) -> current density +``` + +`size` 是整数或形状序列。`V` 为电压数组,`*ions` 是与 `root_type` 对应的 IonInfo,提供 `E`、`Ci`、`Co`。 +模板本身不提供具体速率,子类用类属性 `gates` 声明 Gate,并为每个门选一种速率形式: + +| 回调方法,均为 `(self, V, *ions)` | 返回值 | 方程 | +| --- | --- | --- | +| `f_m_inf`、`f_m_tau` | 无量纲稳态值、时间常数 | `dm/dt = phi * (m_inf - m) / tau` | +| `f_m_alpha`、`f_m_beta` | 两个逆时间速率 | `dm/dt = phi * (alpha * (1-m) - beta * m)` | + +回调可省略不使用的 IonInfo 位置参数;返回值应广播到门状态形状。 +带单位的 tau/rate 按实际单位使用,裸数分别解释为 `time_unit`、`1/time_unit`。 +`power` 决定电导因子 `product(gate ** power)`,不改变门方程。 + +`phi` 直接给温度倍率;或成对给 `q10`、`temp_ref`,由实例的绝对温度 `temp` 求倍率。 +这三个字段支持值、实例属性名字符串和 `(owner) -> value` 回调,动力学求值时解析。 +`phi` 与 q10 组合互斥,非法组合抛出 `ValueError`。`clip=True` 只裁剪电导求值使用的门值。 + +`init_state` 分配零值 DiffEqState;`reset_state` 写入当前电压下的稳态门值; +`compute_derivative` 写 `.derivative`,不推进 `.value`。独立使用时先 init 再 reset, +随后由积分器推进。Cell 自动管理这些调用。门名冲突、重复门、速率形式缺失或同时存在两套, +在类定义或状态绑定时报告 `ValueError`,规则见 [模板校验](template-invariants.md)。 + +这个独立示例定义一个门和固定反转电位,直接检查导数及电流: + +```python +import braincell as bc +import brainunit as u + +class DemoHH(bc.channel.OhmicHH): + root_type = bc.HHTypedNeuron + gates = (bc.channel.Gate("m", power=2),) + + def __init__(self, size=1): + super().__init__(size=size) + self.g_max = 0.1 * u.mS / u.cm**2 + self.E = 0.0 * u.mV + + def f_m_inf(self, V): + return 1.0 / (1.0 + u.math.exp(-(V + 40.0 * u.mV) / (10.0 * u.mV))) + + def f_m_tau(self, V): + return u.math.ones_like(V / u.mV) * 2.0 * u.ms + + def reversal_potential(self, V, *ions): + return self.E + +channel = DemoHH() +V = u.math.asarray([-65.0]) * u.mV +channel.init_state(V) +channel.reset_state(V) +channel.compute_derivative(V) +assert channel.m.value.shape == (1,) +assert u.math.allclose(channel.m.derivative, 0.0 / u.ms) +assert u.math.all(channel.current(V) > 0.0 * u.uA / u.cm**2) +``` + +## Markov 与 Transition + +```text +Markov(size, name=None, solver=None, substeps=None) +OhmicMarkov(size, name=None, solver=None, substeps=None) +Transition(src, dst, forward, backward=None) +init_state(V, *ions, batch_size=None) -> None +reset_state(V, *ions, batch_size=None) -> None +reset_steady_state(V, *ions, batch_size=None) -> None +state_values() -> dict[str, array] +compute_derivative(V, *ions) -> None +make_integration(*args, **kwargs) -> None +``` + +`pairs` 是 Transition 元组。`src`、`dst` 是状态名,`forward`、`backward` 是实例速率方法名; +省略 backward 表示单向反应。速率回调 `(self, V, *ions)` 返回逆时间,裸数解释为 `/ms`。 +`dependent_state` 必须明确指定一个已声明状态,否则类定义抛出 `ValueError`。 + +对 `C <-> O`,设速率为 alpha、beta,总概率 `conserve=1`: + +$$ +\dot O=\alpha C-\beta O,\qquad C=1-O. +$$ + +只积分独立状态;`state_values()` 返回包含重建依赖状态的字典。默认 reset 将独立状态置零, +`reset_to_steady_state=True` 改为求定常分布。`clip_states=True` 默认裁剪动力学求值所用的独立状态, +不覆盖储存值。`conserve` 可为值、属性名或 owner 回调。 +`solver=None`、`substeps=None` 分别采用类默认 `backward_euler`、1;substeps 必须至少为 1。 +独立积分通过环境的 `t`、`dt` 获取时间,协议见 [Quad](../../quad/current/api.md)。 + +```python +import braincell as bc +import brainunit as u + +class DemoMarkov(bc.channel.OhmicMarkov): + root_type = bc.ion.Potassium + pairs = (bc.channel.Transition("C", "O", "alpha", "beta"),) + dependent_state = "C" + open_states = ("O",) + + def __init__(self, size=1): + super().__init__(size=size) + self.g_max = 0.1 * u.mS / u.cm**2 + + def alpha(self, V, K): + return u.math.ones_like(V / u.mV) * 0.2 / u.ms + + def beta(self, V, K): + return u.math.ones_like(V / u.mV) * 0.1 / u.ms + +channel = DemoMarkov() +V = u.math.asarray([-65.0]) * u.mV +K = bc.ion.PotassiumFixed(size=1).pack_info() +channel.init_state(V, K) +channel.reset_state(V, K) +channel.compute_derivative(V, K) +states = channel.state_values() +assert u.math.allclose(states["C"] + states["O"], 1.0) +assert u.math.allclose(channel.O.derivative, 0.2 / u.ms) +``` + +## 电流与辅助函数 + +HH、Markov 只定义状态动力学,具体子类需实现 `current(V, *ions)`。 +OhmicHH 和 OhmicMarkov 共享 `g_max * conductance_factor * (E - V)`,向内为正。 +默认从第一个 IonInfo 取 E;直接附着于神经元的固定 E 通道可覆盖 `reversal_potential`。 +OhmicMarkov 的 `open_states=("O",)` 决定哪些概率相加形成电导因子。 + +| 完整签名或属性 | 行为 | +| --- | --- | +| `q10_factor(q10, temp, temp_ref)` | 返回 `q10 ** ((temp-temp_ref)/(10*u.kelvin))`;温度为绝对温度,倍率无量纲 | +| `freeze_gradient(value)` | 返回数值、形状、单位不变的值,停止经过它的梯度 | +| `ghk_flux(V, ci, co, z, temp)` | 电压、内外摩尔浓度、无量纲价态、绝对温度;返回尚未乘 permeability 的恒场通量 | +| `GhkHH.permeability()` | 默认返回 `self.g_max`,其单位需与通量相乘得到电流密度;不是 ohmic 电导单位 | +| `GhkHH.current(V, *ions)` | 使用首个 IonInfo 的 Ci/Co,以及 self.z、self.temp,计算 `-permeability * gate_factor * ghk_flux` | +| `GhkHH.freeze_drive_gradient=False` | 为 True 时仅冻结驱动力中的 V,门控的电压依赖保留 | + +GHK 在零电压附近使用解析极限分支。具体实现及测试见 +[_base.py](../../../../braincell/channel/_base.py)、[_base_test.py](../../../../braincell/channel/_base_test.py)。 diff --git a/docs/design/channel-template-invariants.md b/docs/design/channel/current/template-invariants.md similarity index 92% rename from docs/design/channel-template-invariants.md rename to docs/design/channel/current/template-invariants.md index 3335eb35..bf5ae55a 100644 --- a/docs/design/channel-template-invariants.md +++ b/docs/design/channel/current/template-invariants.md @@ -1,5 +1,8 @@ # Channel Template Invariants +入口见 [Channel TODO](../TODO.md)。模板和具体通道的来源统一记录在 +[Ion/Channel 文献表](../../ion/references/ion-channel-bibliography.md);引用记录不代表完成模型数值比较。 + `braincell/channel/_base.py` provides three declarative templates that the whole channel catalogue is built on. This note records the conventions they enforce, so a new channel can be written — and reviewed — without reverse-engineering the base class. @@ -111,14 +114,13 @@ back into range, and a state that is stored unclipped but differentiated clipped `[0, 1]` indefinitely. Clipping the conductance is a rendering decision; clipping the kinetics is a dynamics decision, and only the Markov pool needs the latter. -## `dependent_state` is effectively required +## `dependent_state` is required -One Markov state is eliminated and reconstructed as `conserve - sum(others)`. Leaving -`dependent_state` unset falls back to "the last state discovered while scanning `pairs`", which -makes a reordering of `pairs` silently change which state is eliminated. That fallback warns and is -slated for removal; +One Markov state is eliminated and reconstructed as `conserve - sum(others)`. A subclass +declaring `pairs` must name `dependent_state`; omission raises `ValueError` during class creation. +This prevents transition ordering from silently changing the eliminated state. `ChannelTemplateTest.test_shipped_markov_channels_declare_dependent_state` locks the catalogue -against regressing. +against regressing. Construction and runnable examples are in the [Channel API](api.md). ## Numeric-invariance expectation diff --git a/docs/design/filter/TODO.md b/docs/design/filter/TODO.md new file mode 100644 index 00000000..464a5e6e --- /dev/null +++ b/docs/design/filter/TODO.md @@ -0,0 +1,22 @@ +# Filter TODO + +区域、位点与空间参数选择的协作入口。[全局 TODO](../TODO.md) 管理宏观进度, +[Design 规范](../AGENTS.md) 定义分类和状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步 | 文档 | +| --- | --- | --- | --- | +| 半径与距离区域 | 待讨论 | 复用 Morph 空间指标,定义跨阈值区间切分及单位规则 | [预留类型](current/api.md#解析和缓存) | +| 子树区域 | 待讨论 | 确定根选择、连接方向及树编辑后的缓存失效 | [预留类型](current/api.md#解析和缓存)、[Morph TODO](../morph/TODO.md) | +| RegionAnchors 与 StepSamples | 待讨论 | 定义区域相对坐标与固定物理步长的端点和重复值规则 | [预留类型](current/api.md#解析和缓存) | +| 旧 RandomSamples 随机流 | 待讨论 | 将 NumPy 局部流与项目 BrainState 随机上下文方案对齐 | [当前采样](current/api.md#位点和批次)、[随机上下文](../network/proposals/random-context.md) | + +Cell single 模式的位点表达见 [Cell 统一提案](../cell/proposals/single-multi-compartment-unification.md#特殊-policy-与位点表达)。 + +## 已实现内容索引 + +- [空间 callable 参数](current/spatial-callable-parameters.md):形态上下文、metric 和 paint/sampling 参数。 +- [API](current/api.md):区域、位点、集合运算、批次与解析缓存。 +- [连续采样](current/sampling.md):几何测度、density 与 SamplingContext。 +- [Network Pairing](../network/current/pairing.md):已有端点的连接配对。 diff --git a/docs/design/filter/current/api.md b/docs/design/filter/current/api.md new file mode 100644 index 00000000..e6d8d323 --- /dev/null +++ b/docs/design/filter/current/api.md @@ -0,0 +1,106 @@ +# Filter API + +`braincell.filter` 的 RegionExpr 选择形态区间,LocsetExpr 选择离散位置。表达式在获得 Morphology 后解析; +Cell.on/paint 使用区域,Cell.loc/place 使用位置。连续概率采样见 [Sampling](sampling.md), +空间参数回调见 [Callable 参数](spatial-callable-parameters.md)。 + +## 最小用法 + +```python +import braincell as bc +import brainunit as u +from braincell import filter as f + +branch = bc.Branch.from_lengths(lengths=[60.0] * u.um, radii=[2.0, 2.0] * u.um, type="dendrite") +morpho = bc.Morphology.from_root(branch, name="dend") +region = f.branch_in("type", ["dendrite"]) & f.BranchSlice(0, 0.25, 0.75) +cache = f.SelectionCache() +mask = region.evaluate(morpho, cache) +assert mask.intervals == ((0, 0.25, 0.75),) +points = (f.at("dend", 0.25) + f.at("dend", 0.75)).evaluate(morpho, cache) +assert points.branch_id.shape == (2,) +assert len(points) == 2 +sampled = f.sample(region, number=4, seed=7).evaluate(morpho) +assert sampled.branch_x.shape == (4,) +assert ((sampled.branch_x >= 0.25) & (sampled.branch_x <= 0.75)).all() +``` + +## 区域 + +```text +AllRegion() +EmptyRegion() +BranchSlice(branch_index, prox, dist) +branch_in(property, values) -> BranchInFilter +branch_range(property, bounds, *, closed="neither") -> BranchRangeFilter +RegionExpr.evaluate(morpho, cache=None) -> RegionMask +RegionMask(intervals) +``` + +BranchSlice 的 branch_index/prox/dist 支持广播,坐标是分支弧长归一化的 `[0,1]`; +RegionMask.intervals 是 `(branch_index, prox, dist)` 元组集合。 +branch_in 选择离散属性如 type/name/branch_id/parent_id/branch_order/n_children/n_tapers。 +values 是一个或多个允许值。branch_range 用 `(lower, upper)` 筛选分支标量属性, +支持 length、mean_radius、area、volume 以及数值拓扑属性;物理量 bounds 必须使用匹配单位。 +closed 为 neither/left/right/both,分别决定开闭边界;任一 bound 为 None 表示该侧不设限。 + +区域代数 `a | b`、`a & b`、`a - b`、`a.complement()` 返回新表达式,不修改原对象; +解析时分别取区间并、交、差以及相对全部形态的补集。操作类型为 +`RegionSetOp(op, operands)`,通常用运算符构造。 + +## 位点和批次 + +```text +at(branch, x) -> AtLocation +AtLocation(branch, x) +RootLocation(x) +ForkPoints() +Terminals() +UniformSamples(region, count) +RandomSamples(region, count, seed) +LocsetExpr.evaluate(morpho, cache=None) -> LocsetMask +LocsetMask(points=(), display_names=None) +LocsetMask.from_columns(branch_id, branch_x, *, display_names=None) +LocsetBatch.from_columns(branch_id, branch_x, *, display_names=None) +``` + +branch 为分支整数索引或名称,x 为 `[0,1]` 弧长坐标。RootLocation 使用根分支; +ForkPoints 返回分叉位置,BranchPoints 是它的别名;Terminals 返回终端位置。 +UniformSamples 将所选区间按物理长度连接,在总长的 count 个等分中点取样; +RandomSamples 先按区间长度选择区间,再在区间内均匀抽样。count 为正整数,空或零长度区域抛出 ValueError。 +旧 RandomSamples 使用独立 NumPy RNG;需要显式测度、density 和保留抽样顺序时使用 [sample](sampling.md)。 + +LocsetMask.points 是 `(branch_id, branch_x)` 元组,列表示为只读 `(L,)` 数组; +LocsetBatch 列为 `(P,L)`,P 是 population 行,L 是每行位置数。两列形状必须匹配。 +普通索引返回位置子集;batch 的一行索引返回 LocsetMask。display_names 为可选对齐标签。 + +| 运算 | 顺序与重复值 | +| --- | --- | +| `a + b` | 连接两个位置序列,保留顺序和重复位置 | +| `a | b`、`a & b`、`a - b` | 集合并、交、差 | +| `a.unique()` | 按首次出现顺序去重 | + +对应表达式类型为 `LocsetConcatOp(operands)`、`LocsetSetOp(op, operands)`、 +`LocsetUniqueOp(operand)`。Cell 将几何位置映射到 CV/边界 point 的规则见 +[Cell 架构](../../cell/current/architecture.md),Locset 本身不分配电气节点。 + +## 解析和缓存 + +evaluate 需要 Morphology,错误对象抛出 TypeError;无效分支索引/名称、坐标和广播形状在解析时报错。 +`morpho.select(expr, cache=cache)` 是同一解析能力的便利入口。 +同一个 SelectionCache 可复用子表达式结果;换 morphology 或 attach 导致 revision 改变时清空缓存。 +不可哈希的表达式仍可解析,只跳过缓存。实现见 [cache.py](../../../../braincell/filter/cache.py)。 + +以下已导出类型目前只能构造,evaluate 会抛出 NotImplementedError: + +| 签名 | 尚缺能力 | +| --- | --- | +| `RadiusRangeRegion(minimum, maximum)` | 按局部半径截取区域 | +| `TreeDistanceRegion(minimum, maximum)` | 按树路径距离截取 | +| `EuclideanDistanceRegion(minimum, maximum)` | 按三维距离截取 | +| `SubtreeRegion(root_branch_index)` | 按子树选择 | +| `RegionAnchors(region, x)` | 区域相对锚点 | +| `StepSamples(region, step)` | 固定物理步长采样 | + +这些类型的实现工作见 [Filter TODO](../TODO.md)。region、locset 的行为用例分别在 +[region_test.py](../../../../braincell/filter/region_test.py)、[locset_test.py](../../../../braincell/filter/locset_test.py)。 diff --git a/docs/design/filter/current/sampling.md b/docs/design/filter/current/sampling.md new file mode 100644 index 00000000..ea11996f --- /dev/null +++ b/docs/design/filter/current/sampling.md @@ -0,0 +1,44 @@ +# Filter Continuous Sampling + +`braincell.filter.sample` 在连续形态上抽取位置,返回可供 Cell.place 消费的 LocsetExpr。区域与位点解析见 [Filter API](api.md),空间参数上下文见 [Callable 参数](spatial-callable-parameters.md)。 + +## Continuous Location Sampling + +### `braincell.filter.sample` + +```text +braincell.filter.sample( + region, + *, + number, + seed, + measure="length", + density=None, + u_resolution=1e-10, +) -> SampleLocations +``` + +创建一个延迟解析的连续随机 `LocsetExpr`。表达式在获得具体 morphology 后才生成 `branch_id` 和连续 +`branch_x`,因此可以直接传给 `Cell.place` 或 `Network.connect(locations=...)`。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `region` | `RegionExpr` | required | 连续 morphology 支持域。 | +| `number` | positive `int` | required | 样本数;保留抽样顺序和重复位置。 | +| `seed` | `int` | required | 该采样规则独立使用的显式随机种子。 | +| `measure` | `{"normalized", "length", "lateral_area", "area"}` | `"length"` | density 下方的基础几何测度。 | +| `density` | callable or `None` | `None` | 接收 `SamplingContext` 的非负、无量纲位置偏好。 | +| `u_resolution` | `float` | `1e-10` | 数值逆 CDF 的目标精度,范围为 `[1e-12, 1e-5]`。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `SampleLocations` | 延迟到 morphology 已知时解析的 locset expression。 | + +#### Probability measure + +设所选区域为 \(R\),用户 density 为 \(\rho\),`measure` 指定的几何测度为 \(\mu_m\)。对任意 +子区域 \(A\subseteq R\),一个样本落入其中的概率为: diff --git a/docs/design/filter-spatial-callable-parameters.md b/docs/design/filter/current/spatial-callable-parameters.md similarity index 100% rename from docs/design/filter-spatial-callable-parameters.md rename to docs/design/filter/current/spatial-callable-parameters.md diff --git a/docs/design/interface-map.md b/docs/design/interface-map.md deleted file mode 100644 index f210380f..00000000 --- a/docs/design/interface-map.md +++ /dev/null @@ -1,514 +0,0 @@ -# BrainCell 模块依赖与公共接口速览 - -本文基于当前仓库源码扫描整理,用于和合作者讨论接口命名与子模块分工。它不是完整 API 文档,也不替代 `README.md`、`docs/design/TODO.md` 或自动生成的 `docs/apis/*` 文档。 - -## 1. 总体结构 - -当前主要代码在 `braincell/` 下,按职责大致分为以下层次: - -```text -io -> morph -> filter - -> mech - -> _cv -> _compute -> _multi_compartment.Cell -> quad / brainstate / JAX - -ion / channel / synapse -> concrete runtime mechanisms -vis -> consumes morph / filter / Cell data for plotting and export (one-way: nothing below it imports vis at load time) -_single_compartment -> classic single-compartment HH-style neuron frontend -_base_neuron / _base_ion / _base_channel -> shared runtime base classes -``` - -### 依赖强弱 - -- 较独立的声明层:`braincell.mech`。它主要定义机制声明对象、参数容器和注册表,不直接承担 JAX runtime 状态。 -- 几何基础层:`braincell.morph`。多数上层模块依赖它;但当前实现里 `Morphology.from_*`、`vis2d`、`vis3d` 也会反向引用 `io` 和 `vis`,属于用户便利入口。 -- 选择层:`braincell.filter`。依赖 `morph`,被 CV 划分、Cell paint/place、可视化 overlay 使用。 -- CV 与 compute 层:`braincell._discretization` 和 `braincell._compute`。负责把 morphology、filter、mech 声明 lowering 到 control volumes、node tree 和 runtime layout。 -- 最强编排层:`braincell._multi_compartment.Cell`。集中依赖 `morph`、`filter`、`mech`、`_discretization`、`_compute`、`quad`。它不再依赖 `vis`。 -- 数值积分层:`braincell.quad`。提供 integrator registry 和 solver step;会依赖 shared protocol,并有部分 solver 适配 multi/single-compartment 对象。 -- 可视化层:`braincell.vis`。主要消费 `morph`、`filter`、Cell/point topology,不应作为核心计算依赖。依赖方向是单向的:`vis` 在 load time 导入 `_multi_compartment`(`vis/cell_topology.py` 顶层 `import Cell`),因此 `braincell/vis/` 之外的任何 production 模块都**不得**在 load time 导入 `braincell.vis`——`Morphology.vis2d/vis3d` 与 `Branch.vis2d/vis3d` 都在函数体内延迟导入。该不变量由 `braincell/vis/__init___test.py` 的 AST 扫描守护。 - -## 2. 模块职责与主要依赖 - -| 模块 | 当前职责 | 主要内部依赖 | 被谁依赖 | -| --- | --- | --- | --- | -| `braincell.morph` | Branch 几何、Morphology 树、metric、加载/绘图便利入口 | `_misc`、`io`、`filter`、`vis` | `filter`、`_discretization`、`_compute`、`Cell`、`io`、`vis` | -| `braincell.io` | SWC/ASC/NeuroML2/NeuroMorpho/checkpoint 输入输出 | `morph`、`io.swc`、`io.asc` | `Morphology.from_*`、用户 | -| `braincell.filter` | Region/locset 表达式与选择缓存 | `morph` | `_discretization`、`Cell`、`vis` | -| `braincell.mech` | 机制声明、参数、registry | 基本只依赖自身和 `brainunit` | `_discretization`、`_compute`、`Cell`、`channel`、`ion`、`synapse` | -| `braincell._discretization` | CV 对象、CV policy、paint/place lowering | `morph`、`filter`、`mech` | `_compute`、`Cell` | -| `braincell._compute` | point topology、runtime state、layout/table | `_discretization`、`mech`、`ion` | `Cell`、`vis.point_topology`、`vis.cell_topology` | -| `braincell._multi_compartment` | 多隔室 Cell 前端和运行时 facade | 几乎所有核心层 | 顶层 `braincell.Cell` | -| `braincell._single_compartment` | 单隔室 HH-style neuron | `_base_neuron`、`quad` | 顶层 `braincell.SingleCompartment` | -| `braincell.ion` | Na/K/Ca ion containers | `_base_ion`、`mech`、`quad.protocol` | `channel`、runtime | -| `braincell.channel` | 具体 ion channel 实现 | `_base_channel`、`ion`、`mech` | 用户、runtime registry | -| `braincell.synapse` | 具体 synapse 实现 | `mech` | 用户、runtime registry | -| `braincell.quad` | integrator registry 和 step functions | `_misc`、`quad.protocol` | single/multi-compartment runtime | -| `braincell.vis` | 2D/3D scene、backend、morphometry、traces、cell topology | `morph`、`filter`、`_discretization`、`_multi_compartment`、layout/backend | 用户、`Morphology.vis*`(延迟导入) | - -## 3. 顶层导出面 - -`braincell/__init__.py` 当前主要导出: - -- 基础协议/基类:`DiffEqState`、`DiffEqSingleState`、`DiffEqGroupState`、`DiffEqModule`、`IndependentIntegration`、`HHTypedNeuron`、`IonChannel`、`Ion`、`MixIons`、`Channel`、`IonInfo`、`mix_ions` -- morphology:`Branch`、`Soma`、`Dendrite`、`Axon`、`BasalDendrite`、`ApicalDendrite`、`CustomBranch`、`Morphology` -- multi-compartment:`Cell`、`RunResult` -- single-compartment:`SingleCompartment` -- CV policy:`CV`、`CVPolicy`、`CVPerBranch`、`MaxCVLen`、`DLambda`、`CVPolicyByTypeRule`、`CompositeByTypePolicy` -- declaration helpers:`CableProperty`、`CurrentClamp`、`FunctionClamp`、`SineClamp` -- 子包:`channel`、`ion`、`mech`、`quad`、`synapse`、`vis` - -命名注意点: - -- `braincell.Channel` 是 runtime base class;`braincell.mech.Channel` 是 Cell paint 使用的声明层对象。后续讨论接口名时要明确这两个概念是否继续同名。 -- `braincell.Ion` 同样是 runtime ion base class;`braincell.mech.Ion` 是声明层 density mechanism。 -- `braincell.morph.__all__` 当前没有导出 `Branch` 和 `Morphology`,但顶层 `braincell` 有导出;如果希望 `braincell.morph.Branch` 成为稳定公共路径,需要单独确认。 - -## 4. Morphology / Branch 接口 - -### `Branch` - -文件:`braincell/morph/branch.py` - -主要构造与属性: - -- `Branch.from_lengths(...)` -- `Branch.from_points(...)` -- `radii` -- `points` -- `n_segments` -- `length` -- `mean_radius` -- `areas` -- `area` -- `volumes` -- `volume` -- `vis2d(...)` -- `vis3d(...)` -- `save_checkpoint(path)` -- `Branch.load_checkpoint(path)` - -类型化子类: - -- `Soma` -- `Dendrite` -- `Axon` -- `BasalDendrite` -- `ApicalDendrite` -- `CustomBranch` -- `branch_class_for_type(branch_type)` - -### `Morphology` - -文件:`braincell/morph/morphology.py` - -主要构造/加载: - -- `Morphology.from_root(branch, name="soma")` -- `Morphology.from_swc(path, options=None, mode=None, return_report=False)` -- `Morphology.to_swc(path)` -- `Morphology.from_asc(path, return_report=False)` -- `Morphology.from_neuromorpho(...)` -- `Morphology.save_checkpoint(path)` -- `Morphology.load_checkpoint(path)` - -树结构与查询: - -- `root` -- `branches` -- `edges` -- `branch_by_order(order="default")` -- `branch(name=None, index=None, order=None)` -- `path_to_root(branch_index)` -- `topo()` -- `attach(parent=..., child_branch=..., child_name=None, parent_x=1.0, child_x=0.0)` -- `select(expr, cache=None)` - -metric 属性: - -- `metric` -- `has_full_point_geometry` -- `total_length` -- `mean_radius` -- `total_area` -- `total_volume` -- `n_branches` -- `n_stems` -- `n_bifurcations` -- `x_range` -- `y_range` -- `z_range` -- `max_branch_order` -- `max_euclidean_distance` -- `max_euclidean_distance_excluding_soma` -- `max_path_distance` -- `max_path_distance_excluding_soma` - -可视化便利入口: - -- `vis2d(...)` -- `vis3d(...)` - -辅助类型: - -- `MorphoBranch`:`index`、`index_by(...)`、`parent`、`children`、`n_children`、`attach(...)`,并支持 attribute-style child assignment。 -- `MorphoEdge`:parent-child attachment edge。 -- `MorphoMetric`:`from_morpho(...)`、`as_dict()`。 - -## 5. Cell / Multi-Compartment 接口 - -文件:`braincell/_multi_compartment/cell.py` - -### 构造与声明期接口 - -- `Cell(morpho, cv_policy=None, V_th=..., V_init=None, spk_fun=..., solver="staggered", name=None)` -- `morpho` -- `cv_policy` -- `paint_rules` -- `place_rules` -- `V_th` -- `V_init` -- `solver` -- `solver_name` -- `spk_fun` -- `name` -- `paint(region, *mechanisms)` -- `place(locset, *mechanisms)` - -### CV preview 与生命周期 - -- `n_cv` -- `cvs` -- `init_state(batch_size=None)` -- `reset()` -- `reset_state(batch_size=None)` -- `runtime` - -### Runtime / simulation 接口 - -- `n_point` -- `pop_size` -- `varshape` -- `n_compartment` -- `node_tree` -- `node_scheduling(max_group_size=32, algorithm="dhs")` -- `current_time` -- `pre_integral(I_ext=0.0)` -- `compute_derivative(I_ext=0.0)` -- `compute_membrane_derivative(V, I_ext=0.0)` -- `compute_axial_derivative(V)` -- `compute_voltage_derivative(V, I_ext=0.0)` -- `post_integral(I_ext=0.0)` -- `update(I_ext=None)` -- `run(dt=..., duration=...)` - -### 状态与机制查询 - -- `layouts` -- `voltage_shape` -- `get_point_layouts(point_id)` -- `get_cv_layouts(cv_id)` -- `expected_state_shape(layout_id, var_name)` -- `get_state(layout_id, var_name)` -- `set_state(layout_id, var_name, value)` -- `get_point_state(point_id)` -- `get_cv_state(cv_id)` -- `get_runtime_node(layout_id)` -- `get_ion(name)` -- `sample_probe(name)` -- `sample_probes()` -- `mech_table()` - -### Cell 可视化入口 - -`Cell` 不再暴露任何 `vis*` 方法。拓扑绘制统一走 -`braincell.vis.plot_cell_topology(cell, level=...)`(见第 13 节), -`Cell` 侧只保留把 selector 解析成 CV/point 向量的内部能力 -(`_multi_compartment/field_resolution.py`),该模块同时服务于 -`Cell.on(...)` 和 runtime ion inspection。 - -命名建议关注点: - -- `reset()` 与 `reset_state()` 语义不同:前者回到声明期,后者重置 runtime state。建议后续文档中明确命名或别名策略。 -- `node_tree` 和 `runtime` 现在都是属性风格查询接口。 - -## 6. CV / Discretization 接口 - -主要文件:`braincell/_discretization/base.py`、`braincell/_discretization/policy.py` - -公开类型: - -- `CV` -- `CVPolicy` -- `CVPerBranch` -- `MaxCVLen` -- `DLambda` -- `CVPolicyByTypeRule` -- `CompositeByTypePolicy` - -主要接口: - -- `CV.region` -- `CV.diam_mid` -- `CVPolicy.resolve_cv_bounds(morpho, paint_rules=None)` -- `build_discretization(morpho, policy=..., paint_rules=..., place_rules=...)` - -当前 CV 层更像内部 lowering 层,但 `CV` 和 policy 类已经从顶层 `braincell` 导出,属于需要稳定命名的接口。 - -## 7. Mechanism Declaration 接口 - -文件:`braincell/mech/*` - -### 基础与参数 - -- `Mechanism` -- `Params` -- `Params.keys()` -- `Params.values()` -- `Params.items()` -- `Params.get(...)` -- `Params.with_updates(...)` -- `Params.coerce(...)` - -### Cable / density mechanisms - -- `CableProperty` -- `Density` -- `Density.instance_name` -- `Density.with_coverage(...)` -- `mech.Channel(class_name, ..., ion_name=None, ion_names=None, **params)` -- `mech.Ion(class_name, ..., **params)` - -### Point mechanisms - -- `Point` -- `CurrentClamp` -- `CurrentClamp(delay=..., durations=duration, amplitudes=amplitude)` -- `SineClamp` -- `FunctionClamp` -- `StateProbe` -- `MechanismProbe` -- `CurrentProbe` -- `ProbeMechanism` -- `Synapse` -- `Synapse.instance_name` -- `Junction` - -### Registry - -- `MechanismEntry` -- `MechanismRegistry` -- `MechanismRegistry.register(...)` -- `MechanismRegistry.unregister(...)` -- `MechanismRegistry.clear()` -- `MechanismRegistry.contains(...)` -- `MechanismRegistry.get(...)` -- `MechanismRegistry.entry(...)` -- `MechanismRegistry.names(...)` -- `MechanismRegistry.items(...)` -- `get_registry()` -- `register_channel(...)` -- `register_ion(...)` -- `register_synapse(...)` - -## 8. Filter / Selection 接口 - -文件:`braincell/filter/region.py`、`braincell/filter/locset.py` - -### Region - -- `RegionMask` -- `RegionExpr` -- `RegionExpr.complement()` -- `RegionExpr.evaluate(morpho, cache=None)` -- `AllRegion` -- `EmptyRegion` -- `BranchSlice` -- `BranchInFilter` -- `BranchRangeFilter` -- `RadiusRangeRegion` -- `TreeDistanceRegion` -- `EuclideanDistanceRegion` -- `SubtreeRegion` -- `RegionSetOp` -- `branch_in(property, values)` -- `branch_range(property, bounds, closed="neither")` - -### Locset - -- `LocsetMask` -- `LocsetExpr` -- `LocsetExpr.evaluate(morpho, cache=None)` -- `AtLocation` -- `at(branch, x)` -- `RootLocation` -- `ForkPoints` (`BranchPoints` compatibility alias) -- `Terminals` -- `RegionAnchors` -- `UniformSamples` -- `RandomSamples` -- `StepSamples` -- `LocsetSetOp` - -### Cache - -- `SelectionCache` - -## 9. Runtime Base / Single-Compartment 接口 - -### Runtime base classes - -文件:`braincell/_base_neuron.py`、`braincell/_base_ion.py`、`braincell/_base_channel.py` - -- `HHTypedNeuron`:`pop_size`、`n_compartment`、`current(...)`、`pre_integral(...)`、`compute_derivative(...)`、`post_integral(...)`、`init_state(...)`、`reset_state(...)`、`add(...)`、`get_spike(...)` -- `Ion`:`external_currents`、`pre_integral(V)`、`compute_derivative(V)`、`post_integral(V)`、`current(V, include_external=False)`、`init_state(V, batch_size=None)`、`reset_state(V, batch_size=None)`、`update(V, ...)`、`register_external_current(...)`、`pack_info()`、`add(...)` -- `MixIons`:`ion_types`、`pre_integral(V)`、`compute_derivative(V)`、`post_integral(V)`、`current(V)`、`init_state(...)`、`reset_state(...)`、`update(...)`、`add(...)` -- `mix_ions(...)` -- `IonChannel`:`varshape`、`current(...)`、`pre_integral(...)`、`compute_derivative(...)`、`post_integral(...)`、`reset_state(...)`、`init_state(...)`、`update(...)` -- `IonInfo` -- `Channel` -- `Synapse` - -### `SingleCompartment` - -文件:`braincell/_single_compartment/base.py` - -- `SingleCompartment(...)` -- `pop_size` -- `n_compartment` -- `area` -- `init_state(batch_size=None)` -- `reset_state(batch_size=None)` -- `pre_integral(I_ext=...)` -- `compute_derivative(I_ext=...)` -- `post_integral(I_ext=...)` -- `update(I_ext=...)` -- `soma_spike()` - -## 10. Ion / Channel / Synapse 具体实现 - -### `braincell.ion` - -主要导出: - -- `FixedIon` -- `InitNernstIon` -- `DynamicNernstIon` -- `Calcium`、`CalciumFixed`、`CalciumInitNernst`、`CalciumDetailed`、`CalciumFirstOrder` -- `Potassium`、`PotassiumFixed`、`PotassiumInitNernst` -- `Sodium`、`SodiumFixed`、`SodiumInitNernst` -- `build_placeholder_ions(size=(1,))` - -### `braincell.channel` - -按文件分组: - -- `leaky`:`LeakageChannel`、`IL` -- `sodium`:`Na_Ba2002`、`Na_TM1991`、`Na_HH1952`、`NaF_SU2015_DCN`、`NaP_SU2015_DCN`、`Na_ZH2019_IO`、`Nav1p6_MA2020_GoC`、`Nav1p6_MA2024_PC`、`Nav1p6_MA2025_BC`、`Nav1p6_RI2021_SC`、`Nav1p1_MA2025_BC`、`Nav1p1_RI2021_SC`、`Nav_MA2020_GrC`、`NaFHF_MA2020_GrC` -- `potassium`:`KDR_Ba2002`、`K_TM1991`、`K_HH1952`、`KA1_HM1992`、`KA2_HM1992`、`KK2A_HM1992`、`KK2B_HM1992`、`KNI_Ya1989`、`K_Leak`、`KM_*`、`Kv*`、`Kir*`、`Kdr_ZH2019_IO` 等 -- `calcium`:`CaN_IS2008`、`CaT_HM1992`、`CaT_HP1992`、`CaHT_HM1992`、`CaHT_Re1993`、`CaL_IS2008`、`Cav*`、`CaHVA_*`、`Ca_ZH2019_IO` -- `potassium_calcium`:`AHP_De1994`、`Kca3p1_MA2020_GoC`、`Kca2p2_MA2020_GoC`、`Kca1p1_MA2020_GoC` -- `hyperpolarization_activated`:`HCN_HM1992`、`HCN1_*`、`HCN2_MA2020_GoC`、`HCN_SU2015_DCN`、`HCN_ZH2019_IO` -- `channel._base`:`Gate`、`Transition`、`HH`、`Markov`、`ghk_flux` - -命名注意点: - -- 当前实际类名多为 `Na_HH1952`、`KDR_Ba2002`、`CaL_IS2008`,但 README/docs 的部分示例仍使用 `INa_HH1952`、`IKDR_Ba2002`、`ICaL_IS2008` 这类旧命名。统一接口名时需要决定是否保留 alias、更新文档,或迁移到无 `I` 前缀命名。 - -### `braincell.synapse` - -- `ExpSyn` -- `Exp2Syn` - -两者都在 `synapse/exponential.py`,通过 `@register_synapse` 注册, -使用时经 `braincell.mech.Synapse("ExpSyn")` 声明。 - -## 11. IO 接口 - -主要导出: - -- `SwcReader` -- `SwcReadOptions` -- `SwcReport` -- `SwcIssue` -- `AscReader` -- `AscReport` -- `AscIssue` -- `AscMetadata` -- `AscSpineRecord` -- `NeuroMlReader` -- `NeuroMorphoClient` -- `NeuroMorphoCache` -- `NeuroMorphoQuery` -- `NeuroMorphoNeuron` -- `NeuroMorphoMeasurement` -- `NeuroMorphoSearchPage` -- `NeuroMorphoDetail` -- `NeuroMorphoUrls` -- `NeuroMorphoFilePlan` -- `NeuroMorphoDownloadItem` -- `NeuroMorphoDownloadRecord` -- `NeuroMorphoCacheStatus` -- `fetch_neuromorpho(...)` -- `load_neuromorpho(...)` -- `save_branch(...)` -- `load_branch(...)` -- `save_morpho(...)` -- `load_morpho(...)` - -## 12. Quad / Integration 接口 - -主要导出: - -- `get_integrator(method)` -- `register_integrator(...)` -- `get_registry()` -- `IntegratorEntry` -- `IntegratorRegistry` -- `all_integrators` -- explicit / RK:`euler_step`、`midpoint_step`、`rk2_step`、`heun2_step`、`ralston2_step`、`rk3_step`、`heun3_step`、`ssprk3_step`、`ralston3_step`、`rk4_step`、`ralston4_step` -- exponential:`exp_euler_step`、`ind_exp_euler_step` -- implicit:`backward_euler_step`、`implicit_euler_step` -- cable-specific:`staggered_step` -- protocol:`DiffEqState`、`DiffEqSingleState`、`DiffEqGroupState`、`DiffEqModule`、`IndependentIntegration` - -## 13. Visualization 接口 - -主要导出: - -- `plot2d(...)` -- `plot3d(...)` -- `plot_traces(...)` -- `plot_movie(...)` -- `plot_topology(...)` -- `plot_point_topology(...)` -- `plot_cell_topology(...)` — 唯一的 cell 级拓扑入口,`level="node"|"cv"|"branch"` -- `plot_dendrogram(...)` -- `plot_sholl(...)` -- `plot_branch_order_histogram(...)` -- `compare_morphologies(...)` -- `compare_values(...)` -- `save_figure(...)` -- `configure_defaults(...)` -- `get_defaults()` -- `set_defaults(...)` -- `reset_defaults()` -- `publication_theme(...)` -- `theme(...)` -- `LayoutCache` -- `LayoutConfig` -- `ValueSpec` -- `OverlaySpec` -- `VisDefaults` -- `PublicationTheme` -- `PickInfo` -- `VisHooks` - -## 14. 后续统一接口名时建议优先讨论的问题 - -1. `braincell.Channel` / `braincell.Ion` 与 `braincell.mech.Channel` / `braincell.mech.Ion` 是否继续共用名称。 -2. channel 类名是否统一使用无 `I` 前缀,例如 `Na_HH1952`,并为旧 `INa_HH1952` 提供 alias 或弃用提示。 -3. `Morphology` 是否应该在 `braincell.morph` 子包直接导出,和顶层 `braincell.Morphology` 保持一致。 -4. Cell 生命周期接口是否保留 `reset()` 与 `reset_state()` 两套名称,或增加更明确的别名。 -5. `Cell` 的查询接口是否统一 property/method 风格,例如 `node_tree`、`runtime`、`layouts`。 -6. `_discretization`、`_compute` 当前以下划线标识内部模块,但 `CV` 和 policy 已从顶层导出;需要确定哪些类型是稳定公共 API。 -7. docs 中旧结构文件如 `docs/apis/morphology.rst` 仍包含 `Section`、`Segment` 等旧名字,建议后续单独清理。 diff --git a/docs/design/io/TODO.md b/docs/design/io/TODO.md new file mode 100644 index 00000000..2bba0593 --- /dev/null +++ b/docs/design/io/TODO.md @@ -0,0 +1,19 @@ +# IO TODO + +形态读写的协作入口。[全局 TODO](../TODO.md) 管理宏观进度, +[Design 规范](../AGENTS.md) 定义分类和状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步 | 文档 | +| --- | --- | --- | --- | +| ASC 几何与标记覆盖 | 实施中 | 核对 spine、轮廓 soma、多树案例及 reader 测试的具体缺口 | [API](current/api.md#asc-与-neuroml)、[测试](../../../braincell/io/asc/reader_test.py) | +| NeuroML2 导入 | 待讨论 | 从 cell/segment-group 到 Morphology 的最小映射与 fixture 开始;read 当前为 stub | [API](current/api.md#asc-与-neuroml) | +| NeuroMorpho 指标自动对照 | 待讨论 | 从 notebook 提取固定数据集、单位转换及容差,避免在线数据变化影响回归 | [NeuroMorpho API](current/neuromorpho.md)、[示例](../../../examples/multi_compartment/neuromorpho.ipynb) | + +## 已实现内容索引 + +- [API](current/api.md):SWC、ASC、预留 NeuroML 入口、写出与 checkpoint。 +- [NeuroMorpho](current/neuromorpho.md):检索、下载、缓存与结果对象。 +- [SWC Reader 约束](current/swc-reader-invariants.md):读取流程、soma 处理、分支连接及相关测试。 +- [Morph 分层约束](../morph/current/layering-invariants.md):IO 与形态便利入口之间的依赖边界。 diff --git a/docs/design/io/current/api.md b/docs/design/io/current/api.md new file mode 100644 index 00000000..ea04fb06 --- /dev/null +++ b/docs/design/io/current/api.md @@ -0,0 +1,102 @@ +# IO API + +`braincell.io` 读取 SWC、ASC,保存 Morphology/Branch checkpoint;SWC 写出入口位于 +`braincell.io.swc.write_swc`。在线形态检索和缓存单列在 [NeuroMorpho](neuromorpho.md)。 +读取结果是 `braincell.Morphology`,几何数据模型见 [Morph API](../../morph/current/api.md)。 + +## 本地读写示例 + +以下使用仓库自带 SWC fixture,在临时目录验证写出和 checkpoint;不会访问网络。 + +```python +from pathlib import Path +from tempfile import TemporaryDirectory +import braincell as bc +from braincell.io._testing import FIXTURE_DIR + +morpho, report = bc.io.SwcReader().read(FIXTURE_DIR / "three_points_soma.swc", return_report=True) +assert morpho.n_branches > 0 +assert not report.has_errors +with TemporaryDirectory() as directory: + directory = Path(directory) + swc_path = morpho.to_swc(directory / "copy.swc") + reread = bc.Morphology.from_swc(swc_path) + checkpoint = bc.io.save_morpho(morpho, directory / "shape.bcm") + restored = bc.io.load_morpho(checkpoint) + assert reread.n_branches == morpho.n_branches + assert restored == morpho +``` + +## SWC 读取与检查 + +```text +SwcReadOptions(standardize_safe_fixes=True, unknown_type_as_custom=True, + require_root_type_soma=False, mode="neuron") +SwcReader(options=SwcReadOptions()) +SwcReader.read(path, *, return_report=False) -> Morphology | (Morphology, SwcReport) +SwcReader.check(path) -> SwcReport +Morphology.from_swc(path, *, options=None, mode=None, return_report=False) +``` + +path 为 str/PathLike,SWC 数值长度解释为微米。options 默认是每个 reader 独立构造的对象。 +mode 为 neuron 或 neuromorpho,决定 soma 重建和连接语义;选择方法及完整几何例子见 +[SWC Reader 约束](swc-reader-invariants.md)。便利入口同时传 options 和 mode 时两者必须一致,否则抛出 ValueError; +仅传 mode 时据此创建默认 options。 + +standardize_safe_fixes 决定是否应用规则允许的修复;unknown_type_as_custom 把未知类型保留为 custom; +require_root_type_soma=True 将非 soma 根作为错误。read 构造新树,return_report=True 同时返回诊断; +check 只返回检查结果。不可读取的文件和失败的格式验证分别通过文件异常或 ValueError 报告。 + +SwcReport 的 `issues` 保存 level/code/message 及行号,`has_errors` 表示存在错误; +SwcIssue 另带 node_id、fix_message、fix_applied,便于区分发现的问题和实际应用的修复。 +重复读取构造独立树,不共享后续 attach 修改。 + +## ASC 与 NeuroML + +```text +AscReader() +AscReader.read(path, return_report=False) -> Morphology | (Morphology, AscReport) +Morphology.from_asc(path, *, return_report=False) +NeuroMlReader() +NeuroMlReader.read(path) -> Morphology +``` + +ASC 读取 Neurolucida 树,并在 AscReport 中保留诊断、metadata 和 spine 记录。 +几何导入与非几何标记的区别见 [ASC reader](../../../../braincell/io/asc/reader.py) 和 +[ASC report 类型](../../../../braincell/io/asc/types.py)。轮廓 soma、多树及 spine 细节的剩余工作在 +[IO TODO](../TODO.md) 中跟踪。 + +NeuroMlReader 当前是预留入口,read 直接抛出 NotImplementedError,尚不能导入 NeuroML 文件。 + +## SWC 写出 + +```text +braincell.io.swc.write_swc(morpho, path) -> Path +Morphology.to_swc(path) -> Path +``` + +写出需要完整三维点几何;只有 lengths 的 Branch 无法直接转成 SWC 坐标。 +writer 将树展开成 SWC rows,保留分支连接、方向和 soma 附着,文件长度为微米;返回实际输出路径。 +已有路径会被写入覆盖。非法几何或不能表示的连接在验证阶段报错; +结构回读用例见 [writer_test.py](../../../../braincell/io/swc/writer_test.py)。 + +## Checkpoint + +```text +save_branch(branch, path) -> Path +load_branch(path) -> Branch +save_morpho(morpho, path) -> Path +load_morpho(path) -> Morphology +Branch.save_checkpoint(path) -> Path +Branch.load_checkpoint(path) -> Branch +Morphology.save_checkpoint(path) -> Path +Morphology.load_checkpoint(path) -> Morphology +``` + +函数从 braincell.io 导入。`.bcm` 是自包含的形态格式,保存几何、类型、连接和命名信息, +加载返回独立对象。它保存形态,不保存 Cell 电压或 Network 队列。 +保存会写入指定文件,父目录和错误文件格式处理见 +[checkpoint.py](../../../../braincell/io/checkpoint.py)。格式错误抛出 CheckpointError, +不支持的版本抛出其子类 CheckpointVersionError;文件系统异常保留原异常类型。 + +完整示例见 [morphology-checkpoint.ipynb](../../../../examples/multi_compartment/morphology-checkpoint.ipynb)。 diff --git a/docs/design/io/current/neuromorpho.md b/docs/design/io/current/neuromorpho.md new file mode 100644 index 00000000..a0b88f72 --- /dev/null +++ b/docs/design/io/current/neuromorpho.md @@ -0,0 +1,128 @@ +# NeuroMorpho API + +`braincell.io` 提供按 neuron ID 加载、检索、下载和缓存 NeuroMorpho 数据的接口。 +加载返回 Morphology,fetch/download 返回文件记录。SWC 导入模式由 [IO API](api.md) 解释。 + +## 离线可运行示例 + +以下注入仓库测试中的 HTTP 替身,展示查询结果和下载计划的读取;实际使用时省略 session 即可访问服务。 + +```python +from tempfile import TemporaryDirectory +import braincell as bc +from braincell.io.neuromorpho._testing import FakeSession, FakeResponse, sample_neuron_payload + +payload = { + "_embedded": {"neuronResources": [sample_neuron_payload()]}, + "page": {"number": 0, "size": 20, "totalPages": 1, "totalElements": 1}, +} +session = FakeSession([FakeResponse(json_data=payload)]) +with TemporaryDirectory() as directory: + client = bc.io.NeuroMorphoClient(session=session, cache_dir=directory) + page = client.search("species:mouse") + neuron = page.items[0] + plans = client.file_plan(neuron, mode="standard") + assert neuron.neuron_id == 10047 + assert len(plans) == 1 + assert plans[0].filename.endswith(".swc") + assert len(session.calls) == 1 +``` + +## 一步加载与下载 + +```text +load_neuromorpho(neuron_id, *, cache_dir=None, mode="neuromorpho", client=None, + return_report=False, overwrite=False) -> Morphology | (Morphology, SwcReport) +Morphology.from_neuromorpho(neuron_id, *, cache_dir=None, mode="neuromorpho", client=None, + return_report=False, overwrite=False) +fetch_neuromorpho(neuron_id, dest=None, *, mode="standard", overwrite=False, + client=None) -> NeuroMorphoDownloadRecord +``` + +neuron_id 是整数服务 ID;client=None 创建默认客户端。load 的 cache_dir=None 使用默认用户缓存目录, +mode 是 **SWC 导入模式**。fetch 的 dest 是输出目录,mode 则是 **文件种类** standard/original/both。 +standard 为标准 SWC,original 为归档原文件;原始文件可能是 ASC,fetch 本身不解析它。 +overwrite=False 复用已有文件,True 重新下载。return_report=True 让 load 同时返回 SWC 诊断。 +load_neuromorpho 还从 braincell 顶层导出。 + +## Client + +```text +NeuroMorphoClient(session=None, *, timeout=30.0, cache_dir=None, retries=3, backoff_base=0.5) +search(query, *, fq=None, size=20, page=0, sort="neuron_id,asc") -> NeuroMorphoSearchPage +iter_search(query, *, fq=None, size=20, limit=None, start_page=0, + sort="neuron_id,asc") -> Iterator[NeuroMorphoNeuron] +get_neuron(neuron_id) -> NeuroMorphoNeuron +get_measurement(neuron) -> NeuroMorphoMeasurement +get_urls(neuron) -> NeuroMorphoUrls +get_cache_status(neuron) -> NeuroMorphoCacheStatus +describe(neuron, *, include_measurement=True) -> NeuroMorphoDetail +file_plan(neuron, *, mode="both") -> tuple[NeuroMorphoFilePlan, ...] +download(neuron, output_dir=None, *, mode="both", overwrite=False, + dry_run=False) -> NeuroMorphoDownloadRecord +``` + +| 参数 | 含义 | +| --- | --- | +| session | requests 风格会话;None 建立默认会话,可注入测试替身 | +| timeout、retries、backoff_base | 超时秒数、重试次数和退避基数;无 brainunit 单位 | +| query、fq | 查询字符串或 NeuroMorphoQuery;fq 为额外过滤字符串列表 | +| size、page、start_page、limit | 每页数量、从 0 开始页号、迭代起点和可选总条数上限 | +| sort | 服务排序字符串;默认按 neuron_id 升序 | +| neuron | get_urls/file_plan 需要 NeuroMorphoNeuron;其余同名参数也接受整数 ID | +| output_dir | 下载目录;None 使用配置的缓存布局 | +| dry_run | 返回下载计划记录而不写目标文件;解析整数 ID 等元数据步骤仍可能访问网络 | + +search 返回一页,不隐式遍历;iter_search 按需请求后续页。get_cache_status 读取缓存状态, +describe 将 neuron、measurement、urls、cache_status 汇成一个对象。 + +## 结果与缓存 + +| 类型 | 主要字段 | +| --- | --- | +| NeuroMorphoNeuron | neuron_id、neuron_name、archive、species、brain_region、cell_type、原 payload | +| NeuroMorphoSearchPage | items、page、size、total_pages、total_elements、query_url | +| NeuroMorphoFilePlan | kind、url、filename、skip、reason | +| NeuroMorphoDownloadRecord | folder、metadata_path、download_items、measurement、download_mode、dry_run | +| NeuroMorphoDownloadItem | path、downloaded_now、kind、url、reason | +| NeuroMorphoCacheStatus | configured、folder、exists、metadata_exists、standard_exists、original_exists | +| NeuroMorphoMeasurement | length、surface、volume、path_distance 等服务数值,以及 raw/extras | + +Measurement 保留服务的数值字段,不能直接当作 brainunit Quantity;与本地几何比较时需按字段单位转换。 +独立缓存接口从 braincell.io 导入: + +```text +NeuroMorphoCache(root) +cache.list_neurons() -> tuple[int, ...] +cache.contains(neuron_id) -> bool +cache.status(neuron_id, *, neuron_name=None, original_format=None) -> NeuroMorphoCacheStatus +cache.metadata(neuron_id) -> Mapping +cache.measurement(neuron_id) -> NeuroMorphoMeasurement | None +cache.standard_swc_path(neuron_id) -> Path | None +cache.original_file_path(neuron_id) -> Path | None +cache.load(neuron_id, *, mode="neuromorpho", return_report=False) +cache.remove(neuron_id) -> bool +cache.clear() -> int +``` + +root 是本地缓存根目录。load 只解析已缓存的标准 SWC,缺少文件时失败,不隐式下载; +remove 删除一个 neuron 目录并返回是否存在,clear 删除缓存记录并返回数量。 +这些删除操作立即修改本地缓存。路径布局和元数据文件见 +[cache.py](../../../../braincell/io/neuromorpho/cache.py)。 + +```text +NeuroMorphoQuery(species=None, brain_region=None, cell_type=None, archive=None, + original_format=None, stain=None, age_classification=None, + gender=None, raw_q=(), raw_fq=()) +query.to_q() -> str +query.to_fq() -> list[str] +query.to_params() -> dict +``` + +语义字段接受字符串或字符串元组;同一字段多值用 OR,不同字段用 AND。 +raw_q/raw_fq 为原始查询子句元组。转换返回新的查询字符串/参数对象, +不发送请求;实现见 [query.py](../../../../braincell/io/neuromorpho/query.py)。 +服务和下载失败使用 NeuroMorphoError 家族;HTTP 错误为 NeuroMorphoHTTPError,404 为 NeuroMorphoNotFoundError。 +服务可用性和数据变化与本地 reader 验证是两个环节,离线测试入口为 +[client_test.py](../../../../braincell/io/neuromorpho/client_test.py)。完整交互教程见 +[neuromorpho.ipynb](../../../../examples/multi_compartment/neuromorpho.ipynb)。 diff --git a/docs/design/io-swc-reader-invariants.md b/docs/design/io/current/swc-reader-invariants.md similarity index 100% rename from docs/design/io-swc-reader-invariants.md rename to docs/design/io/current/swc-reader-invariants.md diff --git a/docs/design/ion-cerebellum-import-plan.md b/docs/design/ion-cerebellum-import-plan.md deleted file mode 100644 index 322d61cc..00000000 --- a/docs/design/ion-cerebellum-import-plan.md +++ /dev/null @@ -1,144 +0,0 @@ -# Cerebellum ion import progress - -## Current status - -- Declarative `KineticIon` is now implemented in `braincell/ion/_base.py`. -- Public kinetic pieces exist: `Factor`, `Species`, `Reaction`, `Source`, `Conserve`, `KineticIon`. -- Runtime kinetic pieces exist: `_Specs`, `_Species`, `_Conserve`, `_Flux`. -- `MechanismProbe` now supports plain-value ion/mechanism fields in addition to `brainstate.State` fields. -- Concrete Cerebellum calcium-pool ions are now imported in - `braincell/ion/calcium.py`: `CdpStC_MA2020_GoC`, - `CdpStC_NoCAM_MA2020_GoC`, `CdpStC_CAMOnly_MA2020_GoC`, - `CdpStC_MA2025_BC`, `CdpStC_RI2021_SC`, `CdpCAM_MA2024_PC`, and - `CdpCR_MA2020_GrC`. -- PC MA2024 channel imports and targeted tests have been added across - sodium, potassium, calcium, calcium-activated potassium, and HCN - channel modules. -- The PC MA2024 cell-comparison scaffold lives under - `examples/neuron_compare/cell/pc_ma2024`, with a simplified NEURON - assembly, a matching BrainCell assembly, shared parameters, debug - versions, and a side-by-side run notebook. - -## Files changed - -### Ion template and lifecycle - -- `braincell/ion/_base.py` -- `braincell/_base_ion.py` - -### Multi-compartment scheduling and runtime - -- `braincell/_multi_compartment/cell.py` -- `braincell/_compute/ions.py` -- `braincell/_compute/bindings.py` -- `braincell/quad/_staggered.py` - -### Cerebellum ion and channel imports - -- `braincell/ion/calcium.py` -- `braincell/channel/sodium.py` -- `braincell/channel/potassium.py` -- `braincell/channel/calcium.py` -- `braincell/channel/potassium_calcium.py` -- `braincell/channel/hyperpolarization_activated.py` - -### Probe behavior - -- `braincell/_multi_compartment/probes.py` - -### NEURON comparison examples - -- `examples/neuron_compare/ion/` -- `examples/neuron_compare/channel_no_conc/` -- `examples/neuron_compare/cell/pc_ma2024/` -- `examples/neuron_compare/Cerebellum_mod/` - -### Tests - -- `braincell/ion/_base_test.py` -- `braincell/_base_ion_test.py` -- `braincell/_multi_compartment/cell_test.py` -- `braincell/_multi_compartment/probes_test.py` -- `braincell/_compute/ions_test.py` -- `braincell/_compute/bindings_test.py` - -## What is now supported - -- `KineticIon` supports diffeq/algebraic species, `Conserve`, factor-based visible/scaled conversion, and resolved full species views. -- `Ci` is a reserved species name and still feeds the standard `Ion.pack_info()` path. -- Current-driven ion dynamics can optionally reuse a precomputed total-current snapshot when one is provided by the caller. -- `Cell(..., cache_ion_total_current=True)` snapshots per-ion total - current at the start of the staggered step, before voltage or ion - state advances. This restores the NEURON meaning for current-driven - calcium pools: the ion mechanism consumes the channel current from a - stable per-step snapshot instead of recomputing through partially - updated states. -- `Cell(..., ion_channel_update_order="family")` selects the - NEURON-like family schedule. `"integration"` keeps the original - BrainCell integration-oriented schedule for comparison and backwards - behavior checks. -- Same-name channel instances painted onto disjoint layouts are kept - distinct internally, so soma and dendrite can both use the same - channel class/name without one layout overwriting the other inside - `Ion.channels`. -- PC calcium channel `_Frozen` variants stop differentiation through - the voltage used by the current expression. This is a local - compatibility path for reproducing the NEURON scheduling semantics of - the imported mechanisms. - -## Scheduling semantics - -There are now two explicit post-voltage ion/channel schedules: - -- `ion_channel_update_order="family"` is the NEURON-compatibility mode. - It updates by ion family so ion dynamics and their attached channels - see the same grouping assumptions as the source MOD mechanisms. -- `ion_channel_update_order="integration"` is the original BrainCell - behavior. It follows the integration grouping used before the - NEURON-alignment work and is useful as a comparison/debug mode. - -For NEURON comparison runs that include current-driven calcium pools, use -`cache_ion_total_current=True` together with -`ion_channel_update_order="family"`. - -## What is still limited - -- `SingleCompartment` still uses its own update path and has not been extended for the new kinetic-ion template work. -- The PC MA2024 full-cell scaffold is present, but it is still a live - validation target: numerical differences against NEURON should be - tracked through the notebooks before treating it as a regression - baseline. -- Callable spatial parameter expressions are still only documented as a - future filter/paint direction; paint values currently need explicit - arrays or per-region calls. -- Not every Cerebellum MOD file has a BrainCell counterpart yet. The - current completed focus is the ion/channel subset needed by the PC - and calcium-pool comparisons. - -## Tests already run - -- Targeted scheduling/runtime tests around `cache_ion_total_current`, - same-name channel layouts, and `ion_channel_update_order`. -- Targeted ion tests for the imported `CdpStC`, `CdpCAM`, and `CdpCR` - kinetic-ion classes. -- Targeted channel tests for the PC MA2024 channel imports and the - `_Frozen` voltage-current variants. -- NEURON-vs-BrainCell comparison notebooks/scripts under - `examples/neuron_compare/ion` and `examples/neuron_compare/cell/pc_ma2024` - are being used as the external validation path. - -## Next step - -- Keep tightening the PC MA2024 comparison in - `examples/neuron_compare/cell/pc_ma2024/run.ipynb`. -- Promote the stable parts of the notebook comparisons into automated - regression tests once the expected tolerances are clear. -- Continue importing the remaining Cerebellum MOD mechanisms only after - the PC calcium-pool/channel scheduling path is stable. - -## Assumptions - -- This file is now a compressed progress record, not a full design notebook. -- Previous exploratory detail has been intentionally replaced by short status and next-step notes. -- PC MA2024 is the current end-to-end validation target for the - channel/ion scheduling work. diff --git a/docs/design/ion/TODO.md b/docs/design/ion/TODO.md new file mode 100644 index 00000000..2497defe --- /dev/null +++ b/docs/design/ion/TODO.md @@ -0,0 +1,28 @@ +# Ion TODO + +离子模板与离子实现的协作入口。[全局 TODO](../TODO.md) 管理宏观进度, +[Design 规范](../AGENTS.md) 定义分类和状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步或待决定问题 | 文档 | +| --- | --- | --- | --- | +| 文献来源与归因缺口 | 待讨论 | 按文献表中的未核实项补充来源证据,不推断已经确认 | [共享文献表](references/ion-channel-bibliography.md) | +| 钠、钾动态浓度 | 待讨论 | 定义 Detailed/FirstOrder 模型的内外浓度、泵和 current 输入,复用共享生命周期 | [Ion API](current/api.md) | +| Chloride 家族 | 待讨论 | 确定固定/动态反转电位与 GABA 模型所需浓度方程 | [生命周期模板](current/kinetic-ion-api.md) | +| 外部电流一致性 | 待讨论 | 审计各动态模型是否纳入 include_external 及缓存电流 | [电流契约](current/api.md#生命周期和电流) | +| CalciumFirstOrder 单位错误 | 待讨论 | 确定 alpha/beta 的物理单位,修复电流密度到浓度导数的转换并补对照 | [具体失败条件](current/api.md#动态钙浓度) | + +小脑机制导入、PC 数值比较及剩余 MOD 覆盖由 [示例进度](../../../examples/neuron_compare/cerebellum-import-progress.md) 管理, +不在这里复制逐模型任务表。Single 兼容范围由 [Cell 统一提案](../cell/proposals/single-multi-compartment-unification.md) 讨论。 + +## 已实现内容索引 + +- [API](current/api.md):Fixed/Nernst 家族、动态钙浓度和生命周期。 +- [KineticIon 模板 API](current/kinetic-ion-api.md):反应、source、factor、守恒及 Cell 示例。 +- [KineticIon 契约](current/kinetic-ion.md):声明、物种状态和电流输入边界。 +- [Cell 电流快照与调度](../cell/current/architecture.md#离子电流快照与调度):由 Cell 管理的运行时语义。 + +## 参考入口 + +- [Ion/Channel 文献表](references/ion-channel-bibliography.md):Ion 与 Channel 共用的来源记录。 diff --git a/docs/design/ion/current/api.md b/docs/design/ion/current/api.md new file mode 100644 index 00000000..bf79c3a7 --- /dev/null +++ b/docs/design/ion/current/api.md @@ -0,0 +1,105 @@ +# Ion API + +`braincell.ion` 提供离子容器,持有内外浓度、反转电位和对应通道。Cell 通过 +`braincell.mech.Ion` 安装离子;自定义反应系统见 [KineticIon 模板](kinetic-ion-api.md)。 + +## 最小用法 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import AllRegion + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[3.0, 3.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), cv_policy=bc.CVPerBranch(1)) +cell.paint(AllRegion(), bc.mech.Ion("CalciumDetailed", name="ca")) +cell.record("ci", bc.observe.ion(name="ca").state("Ci")) +result = cell.run(dt=0.025 * u.ms, duration=0.1 * u.ms) +ci = result.samples["ci"].values +assert ci.shape == (4, 1) +assert u.math.all(ci > 0.0 * u.mM) +``` + +## 固定值与 Nernst 模型 + +下列签名中的 `**channels` 是命名的运行时 Channel 子对象;使用 Cell 时由 paint 声明通道。 + +```text +SodiumFixed(size, E=50*u.mV, Ci=None, Co=None, valence=None, name=None, **channels) +PotassiumFixed(size, E=-95*u.mV, Ci=None, Co=None, valence=None, name=None, **channels) +CalciumFixed(size, E=120*u.mV, Ci=None, Co=None, valence=None, name=None, **channels) +SodiumInitNernst(size, temp=309.15*u.kelvin, Ci=None, Co=None, valence=None, name=None, **channels) +PotassiumInitNernst(size, temp=309.15*u.kelvin, Ci=None, Co=None, valence=None, name=None, **channels) +CalciumInitNernst(size, temp=309.15*u.kelvin, Ci=None, Co=None, valence=None, name=None, **channels) +``` + +| 参数 | 类型与含义 | +| --- | --- | +| `size` | 整数或形状序列,决定运行时离子状态形状 | +| `E` | 电压值或 shape initializer;Fixed 模型保持给定值 | +| `Ci`、`Co` | 摩尔浓度或 initializer;None 使用具体离子类的 default_Ci/default_Co | +| `valence` | 无量纲价态或 initializer;None 使用具体类 default_valence | +| `temp` | 绝对温度或 initializer,默认 36 摄氏度对应的 kelvin | +| `name` | 运行时节点名称,None 由模块命名机制处理 | + +构造参数广播到 `size`,普通 initializer 接受 shape;Cell 声明的空间 callable 先按 +[CVContext](../../filter/current/spatial-callable-parameters.md) 求值。模型间的 None 默认值见各家族源码, +不能用零替代。浓度必须为正才能使 Nernst 对数有定义,电价不能为零。 + +$$ +E=\frac{RT}{zF}\log\frac{C_o}{C_i}. +$$ + +Fixed 模型直接持有 E;InitNernst 在初始化和 reset 时计算 E,此后改浓度不自动刷新缓存。 +动态 Nernst 模型则在读取 E 时根据当前 Ci 求值。 + +## 动态钙浓度 + +完整签名中 `Constant` 来自 `braintools.init`: + +```text +CalciumDetailed(size, temp=309.15*u.kelvin, d=1*u.um, tau=5*u.ms, + C_rest=0.00024*u.mM, Co=None, + Ci_initializer=Constant(0.00024*u.mM), name=None, **channels) +CalciumFirstOrder(size, temp=309.15*u.kelvin, alpha=0.13, beta=0.075, + Co=None, Ci_initializer=Constant(0.00024*u.mM), name=None, **channels) +derivative(Ci, V, total_current=None) -> concentration / time +``` + +Detailed 使用厚度 d 的膜下薄壳,将向内钙电流密度换算为浓度流入,再以 tau 回到 C_rest: + +$$ +\dot C_i=\max\left(\frac{I_{Ca}}{2Fd},0\right)+\frac{C_{rest}-C_i}{\tau}. +$$ + +CalciumFirstOrder 当前存在单位错误:alpha、beta 是裸数,derivative 却把 `alpha * total_current` +与 `0*u.mM` 比较,缺少电流密度到浓度导数的转换。传入带单位的总电流会抛出 UnitMismatchError; +无通道时 current 返回 None,也无法计算导数。它目前可构造,但不能用默认参数完成这条动力学路径。 +源码依据见 [CalciumFirstOrder.derivative](../../../../braincell/ion/calcium.py),修复由 [Ion TODO](../TODO.md) 跟踪。 +`Ci_initializer` 接受浓度或 shape initializer;Ci 存储为 DiffEqState,`Co` 为外部浓度参数。 +`total_current=None` 使用离子容器汇总电流,传值时使用提供的快照;该输入是电流密度。 +Cell 的快照与更新顺序见 [离子调度](../../cell/current/architecture.md#离子电流快照与调度)。 + +## 生命周期和电流 + +```text +init_state(V, batch_size=None) -> None +reset_state(V, batch_size=None) -> None +compute_derivative(V) -> None +current(V, include_external=False) -> current density +pack_info() -> IonInfo +``` + +`init_state` 分配离子及子通道状态,reset 重新应用浓度初值并重置子通道; +compute_derivative 计算离子和子通道导数,不推进状态。运行时 E 是电压,Ci/Co 是浓度, +`pack_info()` 将这些当前值组成 IonInfo 供通道读取。current 汇总子通道的向内正电流, +`include_external=True` 同时纳入已注册的外部贡献。 + +独立构造的 Ion 要在给定 V 下初始化后再使用动态 Ci;Cell 自动管理这一过程。 +IonInfo 的字段是本次读取的值,不是用于替换 owner 状态的视图。 +所有状态的形状应与所属 Cell population/CV 布局一致;状态修改与参数训练分别通过 +[Cell Views](../../cell/current/views.md) 和 [Trainable](../../optim/current/api.md) 完成。 + +模板生命周期的内部扩展钩子集中在 [KineticIon 与生命周期模板](kinetic-ion-api.md)。 +现成模型的全部导出见 [ion/__init__.py](../../../../braincell/ion/__init__.py), +模型方程出处见 [共享文献表](../references/ion-channel-bibliography.md)。 diff --git a/docs/design/ion/current/kinetic-ion-api.md b/docs/design/ion/current/kinetic-ion-api.md new file mode 100644 index 00000000..956de77d --- /dev/null +++ b/docs/design/ion/current/kinetic-ion-api.md @@ -0,0 +1,106 @@ +# KineticIon 与生命周期模板 + +自定义 Ion 可复用 `braincell.ion._base` 中的生命周期 mixin。该路径是内部扩展入口, +这些类型没有从 `braincell.ion` 重导出。现成模型的公共用法见 [Ion API](api.md), +状态、守恒和电流归属见 [KineticIon 架构契约](kinetic-ion.md)。 + +## 生命周期钩子 + +具体类同时继承一个离子家族和 mixin,在自己的构造器中调用以下初始化钩子: + +```text +FixedIon._init_fixed_ion(*, Ci=None, Co=None, E=None, valence=None) +InitNernstIon._init_nernst_ion(*, Ci=None, Co=None, temp=None, valence=None) +DynamicNernstIon._init_dynamic_nernst_ion(*, Co=None, temp=None, valence=None, Ci_initializer=None) +KineticIon._init_kinetic_ion(*, Co=None, temp=None, valence=None, + species_initializers=None, solver=None, substeps=None) +``` + +FixedIon 要求 E;其余三种要求绝对温度 temp。缺少时抛出 ValueError。 +None 的 Co、Ci、valence 使用具体家族的 class defaults。DynamicNernstIon 子类实现 +`derivative(self, Ci, V, total_current=None)` 返回浓度导数。 +这些钩子物化参数并配置初始化规则,运行时分配由 Ion.init_state 执行。 + +## 反应声明 + +```text +Factor(name, value) +Species(name, init, factor=None) +Reaction(lhs, rhs, forward, backward=None) +Source(target, flux) +Conserve(species, algebraic, total) +species_values() -> dict[str, quantity] +make_integration(V, recursive_child=True) -> None +``` + +| 声明 | 参数与回调 | +| --- | --- | +| Factor | name 是唯一名称;value 是 `(owner) -> factor`,如胞质体积 | +| Species | name 是状态名;init 为可见单位的值或 `(shape) -> initial` initializer;factor 可引用 Factor 名称 | +| Reaction | lhs/rhs 为 `species_name -> 正整数化学计量`;forward/backward 是 `(owner, V, values) -> rate coefficient` | +| Source | target 是物种名;flux 为 `(owner, V, values, total_current=None) -> scaled derivative` | +| Conserve | species 为物种名元组;algebraic 为被消去的物种;total 为 `(owner, V, values) -> scaled total` | + +`values` 是可见物种量的字典。Source 回调会收到 `total_current` 关键字,即使规则不使用也应接受它。 +各声明分别放进类属性 `factors/species/reactions/sources/conserves` 元组。 +必须声明 `Ci`,它供 Nernst 电位和 IonInfo 使用;未知物种、重复定义和非法守恒引用在模板配置时拒绝。 + +若 `y_s = f_s * x_s`,其中 x 是可见浓度,f 是 Factor,则积分和守恒在 y 空间计算: + +$$ +\dot y_s=\sum_r \nu_{sr}J_r+S_s,\qquad +J_r=k_r^+\prod_s x_s^{\nu^-_{sr}}-k_r^-\prod_s x_s^{\nu^+_{sr}}. +$$ + +反应系数回调只返回 k,模板乘反应物浓度。其单位应让 J 与 scaled derivative 一致; +例如体积缩放后的二阶结合,需要 `volume / (concentration * time)`。 +Source 直接给 scaled derivative。Conserve.total 与参与物种的 scaled value 同单位。 +Factor 在一个积分步内作为既定转换量;不要在回调里推进另一套隐含状态。 + +`species_values()` 返回重建守恒物种后的可见单位字典。独立物种是 DiffEqState, +代数物种通过守恒写回;具体写回时机见 [架构契约](kinetic-ion.md)。 +默认独立积分器是 backward_euler、substeps=1,外层 dt 从 brainstate 环境获取。 +`uses_total_current=True` 请求总离子电流,来源与缓存规则同 [Ion API](api.md#动态钙浓度)。 + +## 最小结合模型与 Cell + +这个例子把 `Ci + B <-> BC` 放到一个 Cell 中,体积设为 1,守恒量为 `B + BC`。 +注册名称用于 mech.Ion 查找类;同一进程重复定义时需先注销旧的示例名称。 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import AllRegion +from braincell.ion._base import KineticIon, Species, Reaction, Conserve + +@bc.mech.register_ion("DocBindingCalcium") +class DocBindingCalcium(bc.ion.Calcium, KineticIon): + species = ( + Species("Ci", 0.1 * u.mM), + Species("B", 1.0 * u.mM), + Species("BC", 0.0 * u.mM), + ) + reactions = ( + Reaction({"Ci": 1, "B": 1}, {"BC": 1}, + lambda self, V, x: 0.2 / (u.mM * u.ms), + lambda self, V, x: 0.1 / u.ms), + ) + conserves = (Conserve(("B", "BC"), "B", lambda self, V, x: 1.0 * u.mM),) + + def __init__(self, size, name=None): + super().__init__(size=size, name=name) + self._init_kinetic_ion(temp=309.15 * u.kelvin) + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[3.0, 3.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), cv_policy=bc.CVPerBranch(1)) +cell.paint(AllRegion(), bc.mech.Ion("DocBindingCalcium", name="binding")) +cell.record("bound", bc.observe.ion(name="binding").state("BC")) +cell.record("free", bc.observe.ion(name="binding").state("B")) +result = cell.run(dt=0.025 * u.ms, duration=0.1 * u.ms) +assert result.samples["bound"].values.shape == (4, 1) +assert u.math.allclose(result.samples["bound"].values + result.samples["free"].values, 1.0 * u.mM) +``` + +模板实现和边界用例见 [_base.py](../../../../braincell/ion/_base.py)、 +[_base_test.py](../../../../braincell/ion/_base_test.py)。真实的多壳层模型与 MOD 对照由 +[小脑示例进度](../../../../examples/neuron_compare/cerebellum-import-progress.md) 管理。 diff --git a/docs/design/ion/current/kinetic-ion.md b/docs/design/ion/current/kinetic-ion.md new file mode 100644 index 00000000..804254be --- /dev/null +++ b/docs/design/ion/current/kinetic-ion.md @@ -0,0 +1,35 @@ +# KineticIon 契约 + +构造与反应声明见 [模板 API](kinetic-ion-api.md),公共模型用法见 [Ion API](api.md)。 + +状态:已有实现说明。本页从小脑导入记录中提取可复用的模板契约, +不把模型比较进度当作模板或整细胞数值等价性的验收结果。入口见 [Ion TODO](../TODO.md)。 + +## 声明与状态 + +`braincell.ion` 导出 `Factor`、`Species`、`Reaction`、`Source`、`Conserve` 和 `KineticIon`。 +模板支持微分物种、代数物种、反应与源项,以及用 `Conserve` 解析的守恒关系。 +物种表必须包含且只包含一个名为 `Ci` 的物种,它继续通过标准 `Ion.pack_info()` 提供胞内浓度。 +`Co`、温度和价态属于离子字段。 + +物种以可见单位存储和积分;factor 只在守恒求解与导数映射时转换可见量和缩放量, +不会把持久状态改成缩放后的数值。解析后的完整物种视图包含代数物种。 + +模板初始化要求显式温度,子步数至少为 1;默认 solver 为 `backward_euler`,默认子步数为 1。 +具体离子负责给出初值、反应和来源模型,不由模板统一指定生理参数。 + +## 电流输入与运行时边界 + +需要总离子电流的模型通过模板的电流输入路径求导。调用方提供快照时,模板可以复用该快照。 +快照何时产生、离子与通道如何排序属于 [Cell 调度契约](../../cell/current/architecture.md#离子电流快照与调度), +不由反应网络声明决定。 + +本页记录现有模板及详细 Cell 接入,不宣称新的 single 模式已经实现或验证。 +合并范围见 [两个 Compartment 类的统一提案](../../cell/proposals/single-multi-compartment-unification.md)。 + +## 实现与验证入口 + +- [模板实现](../../../../braincell/ion/_base.py)、[模板测试](../../../../braincell/ion/_base_test.py)。 +- [具体钙池](../../../../braincell/ion/calcium.py)、[离子运行时测试](../../../../braincell/_compute/ions_test.py)。 +- [共享文献表](../references/ion-channel-bibliography.md):来源与模型归因,不代表全部导入模型已完成比较。 +- [小脑导入与比较进度](../../../../examples/neuron_compare/cerebellum-import-progress.md):具体模型、复现入口和验证限制。 diff --git a/docs/design/ion-channel-bibliography.md b/docs/design/ion/references/ion-channel-bibliography.md similarity index 99% rename from docs/design/ion-channel-bibliography.md rename to docs/design/ion/references/ion-channel-bibliography.md index b1d3d5c1..c71e67dd 100644 --- a/docs/design/ion-channel-bibliography.md +++ b/docs/design/ion/references/ion-channel-bibliography.md @@ -1,5 +1,17 @@ # Ion and channel bibliography +## Ownership and adoption + +This is the shared citation record for [Ion](../TODO.md) and [Channel](../../channel/TODO.md), +kept in one canonical location. It supports [KineticIon](../current/kinetic-ion.md), +[channel templates](../../channel/current/template-invariants.md), and the +[Cerebellum comparison work](../../../../examples/neuron_compare/cerebellum-import-progress.md). +The inventory counts and verification steps below describe the original citation audit, +not a fresh scan performed during documentation relocation. Each attribution retains its +own verification status; a listed model or citation does not imply completed numerical validation. + +## Citation record + This file is the single source of truth for the `References` entries that will appear in the NumPy-doc docstrings of the 155 public symbols under `braincell/ion/` and `braincell/channel/`. Docstring `References` sections diff --git a/docs/design/mech/TODO.md b/docs/design/mech/TODO.md new file mode 100644 index 00000000..25758e26 --- /dev/null +++ b/docs/design/mech/TODO.md @@ -0,0 +1,13 @@ +# Mech TODO + +机制声明、注册和输入契约的协作入口。宏观依赖见 [全局 TODO](../TODO.md),文档规则见 [Design 规范](../AGENTS.md)。 + +| 事项 | 状态 | 下一步 | 详情 | +| --- | --- | --- | --- | +| 参数单位诊断 | 待讨论 | 对照当前构造签名与运行时校验,设计指向 paint 声明的错误信息 | [声明 API](current/api.md)、[Channel TODO](../channel/TODO.md) | +| Junction 运行时接线 | 待讨论 | 确定 partner 身份、对称连接与电压方程贡献 | [运行时缺口](proposals/runtime-extensions.md) | +| 旧 Probe 字段校验 | 待讨论 | 核对废弃 Probe 与现行 observe 的迁移方式,避免新增重复 taxonomy | [Recording](../network/current/recording.md) | +| MOD 数值验证框架 | 待讨论 | 从现有比较例子提取可复用的电压钳与电流钳对照 | [Channel TODO](../channel/TODO.md) | +| NMODL 生成器 | 待讨论 | 需要时以 registry 为生成目标,确定最小语法集 | [扩展方向](proposals/runtime-extensions.md) | + +当前实现:[API](current/api.md)、[架构](current/architecture.md)。 diff --git a/docs/design/mech/current/api.md b/docs/design/mech/current/api.md new file mode 100644 index 00000000..f7f75af1 --- /dev/null +++ b/docs/design/mech/current/api.md @@ -0,0 +1,103 @@ +# Mech Declaration API + +`braincell.mech` 定义 Cell.paint/place 消费的不可变声明。它不持有积分状态; +`braincell.Channel/Ion/Synapse` 是运行时基类,名称相同但导入位置不同。 + +## 声明与查询 + +```python +import braincell as bc +import brainunit as u + +leak = bc.mech.Channel("IL", name="leak", g_max=0.1 * u.mS / u.cm**2, E=-65.0 * u.mV) +partial = leak.with_coverage(0.5) +assert leak.instance_name == "leak" +assert partial.coverage_area_fraction == 0.5 +assert bc.mech.get_registry().get("channel", "IL") is bc.channel.IL +``` + +## 密度与电缆属性 + +```text +Channel(class_name, /, *, name=None, coverage_area_fraction=1.0, + ion_name=None, ion_names=None, solver=None, substeps=None, **params) +Ion(class_name, /, *, name=None, coverage_area_fraction=1.0, + solver=None, substeps=None, **params) +CableProperty(resting_potential, membrane_capacitance, axial_resistivity, + temperature=309.15*u.kelvin) +``` + +class_name 接受 registry 名称或已注册类,params 是目标运行时构造参数; +name 决定逻辑 owner 名称,None 回退为类名。coverage_area_fraction 为 `[0,1]` 的无量纲覆盖面积比例,独立于 params。 +Channel 的 ion_name 选择单离子 owner,ion_names 为多离子依赖提供名称映射;不能混淆类型与逻辑名称。 +solver/substeps 配置机制独立积分,None 使用目标模型默认值。 +CableProperty 四个字段依次为电压、单位面积电容、电阻率和绝对温度。 +安装方法、重叠检查和空间广播见 [Cell.paint](../../cell/current/api.md#paint-与-place)。 + +`with_coverage(fraction)` 返回覆盖比例不同的新声明;名称和参数不同的声明通过构造器创建。 +CableProperty.with_updates(**kwargs) 同样返回副本。原声明不变。 +`Params(data=None, /, **kwargs)` 是不可变 Mapping,参数字典顺序不影响哈希相等; +`with_updates(**kwargs)` 构造新映射,不能用它直接写运行时状态。 + +## 点机制 + +```text +Synapse(synapse_type, /, *, name=None, **params) +CurrentClamp(delay=0*u.ms, durations=1*u.ms, amplitudes=0*u.nA) +FunctionClamp(fn) +SineClamp(amplitude, frequency, phase=0.0, offset=0*u.nA, + delay=0*u.ms, duration=1*u.ms) +Junction(params=Params()) +``` + +Synapse 的 synapse_type 为 registry 中的突触模型名;参数和动态方程见 [Synapse API](../../synapse/current/api.md)。 +CurrentClamp 的 durations/amplitudes 描述连续分段刺激,形状、区间及 population 广播见 +[Cell 刺激声明](../../cell/current/api.md#paint-与-place)。FunctionClamp 的 fn 接收带单位时间,返回电流; +SineClamp 的 frequency 是频率量,phase 为弧度裸数,offset/amplitude 为电流,delay/duration 为时间。 +Clamp 在主步中点求值并缓存,观测与 solver 消费同一值,见 [ClampView](../../cell/current/views.md#clampview)。 +Junction 当前只有声明,还没有 partner 接线和电压求解贡献。 + +旧 ProbeMechanism、StateProbe、CurrentProbe、MechanismProbe 仍在导出列表,新的观测使用 +[Cell.record 与 observe](../../network/current/recording.md),不占用点机制位置。 + +## 注册与事件契约 + +```text +get_registry() -> MechanismRegistry +register_channel(name, *, aliases=()) -> class decorator +register_ion(name, *, aliases=()) -> class decorator +register_synapse(name, *, aliases=()) -> class decorator +NoEventInput() +TriggerEventInput(*, aggregation="count") +ScalarEventInput(unit, *, aggregation="sum") +ParameterSpec(default, validator=None) +StateSpec(initial=) +positive(value, name) -> None +``` + +装饰器注册类并返回原类。registry 按 channel/ion/synapse 分类,未知名称查询抛出 KeyError 并给近似建议; +重复冲突名称拒绝注册。具体查询与变更签名: + +```text +MechanismEntry(category, name, cls, aliases=()) +MechanismRegistry() +registry.register(entry) -> None +registry.unregister(category, name) -> None +registry.clear() -> None +registry.contains(category, name) -> bool +registry.get(category, name) -> type +registry.entry(category, name) -> MechanismEntry +registry.names(category=None, *, include_aliases=False) -> tuple[str, ...] +registry.items(category=None) -> tuple[tuple[str, type], ...] +``` + +category=None 查询所有分类;names 默认只列 canonical 名称,include_aliases=True 追加别名。 +entry 返回冻结的注册元数据;register/unregister/clear 就地修改所操作的 registry, +全局 registry 的变更影响后续声明解析,独立 MechanismRegistry 实例有自己的内容。 +注册冲突和别名处理的依据见 [_registry.py](../../../../braincell/mech/_registry.py)。 +事件契约决定目标是否接收事件、计数或带单位的标量累加;ScalarEventInput 的 unit 决定 Connection.weight 单位。 +StateSpec 是运行时 Synapse 状态初值声明;ParameterSpec 是旧显式 schema 辅助类型, +现行参数发现从构造签名读取,见 [Synapse 架构](../../synapse/current/architecture.md)。 + +源码入口:[density](../../../../braincell/mech/_density.py)、[point](../../../../braincell/mech/_point.py)、 +[事件契约](../../../../braincell/mech/_event_contract.py)。 diff --git a/docs/design/mech/current/architecture.md b/docs/design/mech/current/architecture.md new file mode 100644 index 00000000..b2cb883f --- /dev/null +++ b/docs/design/mech/current/architecture.md @@ -0,0 +1,27 @@ +# Mech Architecture + +Mech 将机制类型、参数和输入契约表示为声明。离散层决定覆盖哪些 CV/point,计算层解析 registry 并分配运行时对象。 + +```text +mech.Channel / Ion -> paint rules -> CV coverage -> runtime Channel / Ion +mech.Synapse -> place rules -> logical rows -> runtime Synapse +event_input -> weight validation + event buffer allocation +``` + +| 对象 | 数据与职责 | +| --- | --- | +| Density | category、class_name、name、覆盖比例、params、积分配置 | +| Point | 几何位置之外的机制声明;位置由 place rule 保存 | +| Params | 不可变参数映射,等价参数按值分组,关键字顺序不改变身份 | +| MechanismRegistry | 分类名称/别名到运行时类的单一解析入口 | +| EventInput / StateSpec | 运行时可以在构造之前读取的静态契约 | + +通道到离子家族的依赖来自 runtime class.root_type,逻辑 owner 选择由声明名称决定。 +同名密度 owner 的 CV 覆盖冲突由 Cell 检查;改变参数不能规避覆盖冲突。 +覆盖面积比例保留为几何元数据,不混进目标模型的普通参数。 + +Mech 不导入其他 braincell 包或数值运行时。具体 Channel/Ion/Synapse 模块导入时主动注册自己, +因此 registry 不需要反向导入模型来找类。事件输入和字段 schema 留在这里,使运行时基类和 Network +共享契约而不形成导入环,约束见 [Network 模块布局](../../network/current/module-layout.md)。 + +接口见 [API](api.md),完整转换阶段见 [Cell 架构](../../cell/current/architecture.md)。 diff --git a/docs/design/mech/proposals/runtime-extensions.md b/docs/design/mech/proposals/runtime-extensions.md new file mode 100644 index 00000000..e6ef3bf4 --- /dev/null +++ b/docs/design/mech/proposals/runtime-extensions.md @@ -0,0 +1,20 @@ +# Mech 运行时扩展 + +Junction 已有占位声明,但没有连接另一端的身份,也没有在电压装配中贡献跨 Cell 电流。 +NMODL 生成与统一 MOD 对照框架则需要从现有模型导入和比较脚本提取共同部分。 + +## Junction + +两个位置 a、b 的电耦合应给出成对电流 `I_a=g*(V_b-V_a)`、`I_b=-I_a`。 +下一步需决定 partner 用逻辑 placement ID 还是显式端点对表示,并确认跨 Cell 求解的同步顺序。 +只补一个 params 字段不能完成耦合,验收需包含电流守恒和两个 Cell 的同步电压对照。 + +## 机制生成与验证 + +生成器的目标是现有 registry 注册类、参数构造签名和 Channel/Ion/Synapse 模板。 +待讨论的最小范围包括单位、状态方程、事件输入以及特殊求解语句; +以一个有公开 MOD 参照的机制完成“生成、注册、放入 Cell、电压钳对照”后再扩展语法。 +比较框架从 [现有 NEURON 示例](../../../../examples/neuron_compare/) 提取,逐模型来源继续由 +[共享文献表](../../ion/references/ion-channel-bibliography.md) 维护。 + +事项状态见 [Mech TODO](../TODO.md)。 diff --git a/docs/design/module-dependency-map.md b/docs/design/module-dependency-map.md deleted file mode 100644 index 36e4fcba..00000000 --- a/docs/design/module-dependency-map.md +++ /dev/null @@ -1,208 +0,0 @@ -# `_multi_compartment` / `_discretization` / `_compute` 依赖图 - -本文只看三个目录之间和内部的关系: - -- `braincell._multi_compartment` -- `braincell._discretization` -- `braincell._compute` - -箭头含义:`A --> B` 表示 `A` 在实现上调用、导入或依赖 `B`。虚线表示 type-only 或调试辅助引用,不是主执行链路。 - -## 1. 三个包之间的主关系 - -```mermaid -flowchart LR - MC["`_multi_compartment`
Cell frontend
cell / currents / probes / run"] - CV["`_discretization`
CV + node-tree declaration
base / node_build / policy / geometry / mechanism"] - CP["`_compute`
runtime compile layer
layouts / ions / bindings / state / bridge / scheduling / table"] - - MC -->|"Cell.cvs / Cell.init_state
build_discretization"| CV - MC -->|"Cell.init_state
CellRuntimeState.from_cell(...)"| CP - CP -->|"node scheduling consumes declaration node tree"| CV -``` - -这张图里的重点: - -- `_multi_compartment.cell.Cell` 是调用方和用户入口。 -- `_discretization` 负责把 `morpho + cv_policy + paint/place rules` 变成 `tuple[CV, ...]`,并在初始化路径生成 `NodeTree`。 -- `_compute` 负责把 `Cell + CV/NodeTree declaration` 变成 runtime state、layout、runtime nodes,并为 solver 构造 scheduling。 -- `bridge.py` 现在就在 `_compute` 内部;它的 `TYPE_CHECKING` 引用指向 `braincell._compute.state.CellRuntimeState`,运行时调用方是 `_multi_compartment.cell`(最大的调用方,约 15 处)、`_compute.state`、`_multi_compartment.currents`、`_multi_compartment.probes`。 - -## 2. `_multi_compartment` 内部 - -```mermaid -flowchart TD - CELL["cell.py
Cell
paint/place/init_state/update/run"] - BRIDGE["`_compute.bridge`
CV <-> point helpers
cv_to_point, point_to_cv, ..."] - CURRENTS["currents.py
total_membrane_current"] - PROBES["probes.py
sample_probe(s)"] - RUN["run.py
RunResult / run"] - CVBASE["`_discretization.base`
CV / Node / Discretization
build_discretization"] - CVNODE["`_discretization.node_build`
build_node_tree_from_cvs"] - CVPOLICY["`_discretization.policy`
CVPolicy / CVPerBranch / ..."] - CVGEOM["`_discretization.geometry`
CVGeometryResult
build_cv_geometry"] - CVMECH["`_discretization.mechanism`
PaintRule / PlaceRule
normalize / merge"] - CPSTATE["`_compute.state`
CellRuntimeState"] - CPTABLE["`_compute.table`
MechanismObjectTable"] - CPTOPO["`_compute.scheduling`
build_node_scheduling"] - - CELL -->|"imports"| BRIDGE - CELL -->|"compute_membrane_derivative"| CURRENTS - CELL -->|"sample_probe(s)"| PROBES - CELL -->|"run(...)"| RUN - - CURRENTS -->|"V_cv -> point_V
I_point -> I_cv"| BRIDGE - PROBES -->|"point/CV conversions"| BRIDGE - - CELL -->|"cvs / init_state"| CVBASE - CELL -->|"paint/place normalization"| CVMECH - CELL -->|"policy setter/default"| CVPOLICY - CELL -->|"runtime facade"| CPSTATE - CELL -->|"mech_table"| CPTABLE - CELL -->|"node_tree"| CPTOPO -``` - -`cell.py` 是这个目录的中心文件: - -- 声明期:`paint(...)` / `place(...)` 走 `_discretization.mechanism.normalize_*` 和 `merge_*`。 -- 预览期:`cvs` 属性走 `_discretization.base.build_discretization(...)`,再取 `.cvs`。 -- 初始化:`init_state(...)` 走 `_discretization.base.build_discretization(...)`,一次拿到 `CVTree + NodeTree`,再走 `_compute.state.CellRuntimeState.from_cell(...)`。 -- 运行期:`compute_membrane_derivative(...)` 调 `currents.total_membrane_current(...)`;`run(...)` 委托给 `run.py`;probe 查询委托给 `probes.py`。 - -## 3. `_discretization` 内部 - -```mermaid -flowchart TD - BASE["base.py
CV / Node / Discretization
build_discretization(...)"] - NODEBUILD["node_build.py
build_node_tree_from_cvs"] - POLICY["policy.py
CVPolicy
CVPerBranch / MaxCVLen / DLambda"] - GEOM["geometry.py
CVGeometryResult
build_cv_geometry"] - MECHLOWER["mechanism.py
PaintRule / PlaceRule
normalize / merge"] - MORPH["external: morph
Morphology / Branch"] - FILTER["external: filter
RegionExpr / LocsetExpr"] - MECH["external: mech
CableProperty / Density / Point"] - - BASE -->|"policy.resolve_cv_bounds"| POLICY - BASE -->|"CV geometry"| CVGEOM - BASE -->|"rule lowering"| CVMECH - BASE -->|"CV -> NodeTree"| CVNODE - CVGEOM -->|"branch geometry"| MORPH - CVMECH -->|"region / locset evaluate"| FILTER - CVMECH -->|"cable + mechanisms"| MECH - POLICY -->|"branch length/type"| MORPH - POLICY -->|"CableProperty for DLambda"| MECH -``` - -`_discretization` 的主执行链很短: - -1. `Cell.cvs` 调 `build_discretization(...).cvs`。 -2. `Cell.init_state()` 内部一次构建 `CVTree + NodeTree`。 -3. `build_discretization(...)` 调 `policy.resolve_cv_bounds(...)` 决定每个 branch 的 CV 区间。 -4. `geometry.build_cv_geometry(...)` 产出静态 CV 几何。 -5. `mechanism.build_cv_mechanisms(...)` 再把 point mechanisms 按 locset 映射到对应的 `Node.point_mech`。 - -代表接口只需要记这些: - -- `CV`:`region`、`diam_mid`、`...` -- `build_discretization(...)` -- `NodeTree` / `build_node_tree_from_cvs(...)` -- `PaintRule` / `PlaceRule` -- `normalize_paint_rules(...)` / `normalize_place_rule(...)` -- `merge_paint_rules(...)` / `merge_place_rules(...)` -- `CVPolicy.resolve_cv_bounds(...)` - -## 4. `_compute` 内部 - -```mermaid -flowchart TD - LAYOUTS["layouts.py
MechanismLayout / clamp routing
state-buffer allocation"] - IONS["ions.py
runtime ion instantiation / sync"] - BINDINGS["bindings.py
channel binding
runtime node instantiation"] - STATE["state.py
CellRuntimeState"] - BRIDGE["bridge.py
CV <-> point scatter/gather helpers"] - TABLE["table.py
MechanismObjectTable"] - TOPO["scheduling.py
NodeScheduling
build_node_scheduling"] - CVBASE["external: `_discretization.base`
CV / NodeTree"] - MECH["external: mech
declarations + registry"] - ION["external: ion/channel
runtime mechanism classes"] - CELLEXT["external: `_multi_compartment.cell`
Cell"] - - IONS -->|"layout grouping"| LAYOUTS - BINDINGS -->|"runtime ion helpers"| IONS - BINDINGS -->|"layout lookup"| LAYOUTS - STATE -->|"channel/synapse binding"| BINDINGS - STATE -->|"CV/point vectors"| BRIDGE - STATE -->|"clamp/state-buffer layout"| LAYOUTS - TABLE -->|"layout/runtime lookup"| STATE - - LAYOUTS -->|"node tree declaration"| CVBASE - LAYOUTS -->|"resolve declarations"| MECH - IONS -->|"resolve declarations"| MECH - IONS -->|"instantiate runtime ions"| ION - BINDINGS -->|"resolve declarations"| MECH - BINDINGS -->|"instantiate runtime channels"| ION - STATE -->|"node tree declaration"| CVBASE - STATE -->|"resolve declarations"| MECH - TABLE -->|"declaration identity"| MECH - TOPO -->|"node tree declaration"| CVBASE - - LAYOUTS -.->|"type-only: CellRuntimeState"| STATE - IONS -.->|"type-only: CellRuntimeState"| STATE - BINDINGS -.->|"type-only: CellRuntimeState"| STATE - BRIDGE -.->|"type-only: CellRuntimeState"| STATE - STATE -.->|"type-only: Cell"| CELLEXT -``` - -`_compute` 现在没有单一中心模块,职责按依赖顺序拆成几层: - -- `layouts.py` 是最底层:`MechanismLayout` 记录、clamp routing、state buffer 分配,运行时对包内其他模块无依赖;仅在 `TYPE_CHECKING` 下引用 `state.py` 的 `CellRuntimeState` 做类型标注。 -- `ions.py` 依赖 `layouts.py`,负责 runtime ion 实例的构建与同步;同样仅在 `TYPE_CHECKING` 下引用 `state.py` 的 `CellRuntimeState`。 -- `bindings.py` 依赖 `ions.py` 和 `layouts.py`,负责 channel binding 与 runtime node 实例化;同样仅在 `TYPE_CHECKING` 下引用 `state.py` 的 `CellRuntimeState`。 -- `state.py` 依赖 `bindings.py`、`bridge.py`、`layouts.py`,把上面几层聚合成 `CellRuntimeState` 门面;对 `_multi_compartment.cell.Cell` 的引用只在 `TYPE_CHECKING` 下出现。 -- `bridge.py` 现在是 `_compute` 内部模块,提供 CV <-> point 的 scatter/gather helper,运行时对包内其他模块无依赖;同样仅在 `TYPE_CHECKING` 下引用 `state.py` 的 `CellRuntimeState`。 -- `table.py` 依赖 `state.py`,是 inspect/debug/query 层,用 runtime layout 和 declaration 生成 mechanism table。 -- `scheduling.py` 只保留 `NodeScheduling`;`NodeTree` 的真实定义在 `_discretization.base`,node 构建细节在 `_discretization.node_build`;它与本包其余模块之间没有依赖关系。 - -## 5. `Cell.init_state()` 路径 - -```mermaid -sequenceDiagram - participant Cell as _multi_compartment.cell.Cell - participant CV as _discretization.base/policy/geometry/mechanism/node_build - participant Runtime as _compute.state - participant Bridge as _compute.bridge - - Cell->>CV: internal tuple helper (morpho, policy, paint_rules, place_rules) - CV-->>Cell: CVTree, NodeTree - Cell->>Runtime: CellRuntimeState.from_cell(self) - Runtime->>Bridge: attach_runtime_ion_geometry / vector helpers - Runtime-->>Cell: CellRuntimeState -``` - -## 6. `Cell.update()` / current 路径 - -```mermaid -sequenceDiagram - participant Cell as Cell.update / derivative - participant Curr as currents.total_membrane_current - participant Bridge as bridge - participant Runtime as CellRuntimeState - participant Channel as runtime ion channels - - Cell->>Curr: total_membrane_current(V_cv, I_ext, t) - Curr->>Bridge: cv_to_point(V_cv, runtime) - Curr->>Runtime: evaluate_point_clamps(t) - Curr->>Channel: ch.current(point_V) - Curr->>Bridge: point_to_cv(I_point, runtime) - Curr-->>Cell: I_total_cv -``` - -## 7. 最小记忆版 - -- `_multi_compartment.cell` 是入口和编排层。 -- `_discretization.base.build_discretization(...)` 是静态离散入口;CV 预览通过 `.cvs` 取得。 -- `_discretization.node_build` 把 CV 变成 node tree,并承载 endpoint/midpoint point mechanism placement。 -- `_compute.scheduling` 只做 node scheduling 和兼容导出。 -- `_compute.state` 把 Cell declaration 变成 runtime state(依赖 `layouts` / `bindings` / `bridge`;`ions` 只通过 `bindings` 间接可达)。 -- `_compute.bridge` 是 CV-space 和 point-space 的转换工具,被 `_multi_compartment.cell`(最大的调用方)、`_compute.state`、`currents`、`probes` 共同使用。 -- `_compute.layouts/ions/bindings` 是真正的实现模块,按 `layouts -> ions -> bindings -> state` 的顺序逐层依赖,不是 re-export。 diff --git a/docs/design/morph/TODO.md b/docs/design/morph/TODO.md new file mode 100644 index 00000000..fafee22c --- /dev/null +++ b/docs/design/morph/TODO.md @@ -0,0 +1,18 @@ +# Morph TODO + +形态模块的协作入口。[全局 TODO](../TODO.md) 管理宏观进度, +[Design 规范](../AGENTS.md) 定义文档分工和事项状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步 | 文档 | +| --- | --- | --- | --- | +| Morphology / Branch 的子包导出 | 待讨论 | 当前使用顶层 `braincell.Morphology` / `braincell.Branch`;比较同时从 `braincell.morph` 导出的可发现性收益与维护成本,检查导入依赖 | [子包导出](../../../braincell/morph/__init__.py)、[分层约束](current/layering-invariants.md) | +| 子树编辑 | 待讨论 | 定义删除、splice、两树连接和分支替换的身份及方向保持规则 | [当前 attach API](current/api.md#morphology-与连接) | +| 几何变换 | 待讨论 | 确定平移、旋转、缩放与主轴对齐后的 revision、指标和 Cell 缓存失效 | [当前查询与缓存](current/api.md#查询视图和指标) | + +## 已实现内容索引 + +- [API](current/api.md):Branch 构造、树连接、views、指标与独立复制。 +- [Morph 分层约束](current/layering-invariants.md):依赖方向、延迟导入及测试守护。 +- [SWC Reader 约束](../io/current/swc-reader-invariants.md):由 IO 维护的形态导入语义。 diff --git a/docs/design/morph/current/api.md b/docs/design/morph/current/api.md new file mode 100644 index 00000000..2a000e50 --- /dev/null +++ b/docs/design/morph/current/api.md @@ -0,0 +1,107 @@ +# Morph API + +`braincell.Branch` 保存不可变分支几何,`braincell.Morphology` 管理可追加的树。 +两者从 braincell 顶层导入;`braincell.morph` 导出 MorphoBranch、MorphoEdge、MorphoMetric 等辅助类型。 +文件读写见 [IO](../../io/current/api.md),依赖规则见 [分层约束](layering-invariants.md)。 + +## 构造一棵树 + +```python +import copy +import braincell as bc +import brainunit as u + +soma = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[5.0, 5.0] * u.um, type="soma") +dend = bc.Branch.from_lengths(lengths=[40.0, 60.0] * u.um, radii=[2.0, 1.5, 1.0] * u.um, type="dendrite") +morpho = bc.Morphology.from_root(soma, name="soma") +child = morpho.attach(parent="soma", child_branch=dend, child_name="dend", parent_x=0.5) +assert child.parent is morpho.root +assert morpho.n_branches == 2 +assert u.math.allclose(morpho.total_length, 120.0 * u.um) +assert len(morpho.edges) == 1 +copied = copy.deepcopy(morpho) +assert copied is not morpho +print(morpho.topo()) +``` + +## Branch 几何 + +完整签名;`` 表示省略 type 时采用调用类的默认分支类型,Branch 为 custom: + +```text +Branch(lengths, radii_proximal, radii_distal, + points_proximal=None, points_distal=None, type="custom") +Branch.from_lengths(*, lengths, radii=None, radii_proximal=None, + radii_distal=None, type=) -> Branch +Branch.from_points(*, points, radii=None, radii_proximal=None, + radii_distal=None, type=) -> Branch +``` + +| 参数 | 单位和形状 | +| --- | --- | +| lengths | 长度量 `(n,)`,n 为 frustum 数 | +| points | 长度量 `(n+1, 3)`,每行 xyz,段长由相邻点计算 | +| radii | 长度量 `(n+1,)`,相邻值给出每段两端半径 | +| radii_proximal、radii_distal | 长度量 `(n,)`;与 radii 二选一,允许段间半径跳变 | +| points_proximal、points_distal | 直接构造时的逐段端点 `(n,3)`,需同时提供 | +| type | 形态语义字符串,如 soma、axon、dendrite、basal_dendrite、apical_dendrite、custom | + +缺少半径、混用两种半径形式、非有限几何、非法形状或单位会在构造时失败, +具体检查见 [branch.py](../../../../braincell/morph/branch.py)。 +`from_lengths` 提供电缆几何,但没有三维坐标;路径长度和面积仍可用,xyz 范围、欧氏距离及三维绘图需要 points。 +段长允许零以保留半径跳变的面积语义;这类几何如何导入见 [SWC 约束](../../io/current/swc-reader-invariants.md)。 + +## Morphology 与连接 + +```text +Morphology(*, root_name, root_branch) +Morphology.from_root(branch, *, name="soma") -> Morphology +Morphology.attach(*, parent, child_branch, child_name=None, + parent_x=1.0, child_x=0.0) -> MorphoBranch +MorphoBranch.attach(branch, name=None, *, parent_x=1.0, child_x=0.0) -> MorphoBranch +``` + +root_branch/child_branch 是 Branch;parent 是本树的分支名或 MorphoBranch。 +`parent_x` 为父分支归一化弧长坐标 `[0,1]`,`child_x` 为子分支连接端点 0 或 1。 +child_name=None 自动按类型分配名称,显式名称需唯一且不能占用保留属性名。 +attach 就地增加节点和边、递增 revision、失效派生缓存,返回新节点的 view。 +错误父节点、重复名称和非法连接坐标会在写入前拒绝。 + +属性式 `morpho.soma.dend = branch` 等价于在 soma 末端追加命名分支。 +已有名称不能用该语法替换几何;删除、splice、整体变换的设计由 [Morph TODO](../TODO.md) 跟踪。 +要编辑独立副本,使用 `copy.deepcopy(morpho)`;当前没有公共 `clone()` 方法。 + +## 查询、视图和指标 + +```text +Morphology.branch(*, name=None, index=None, order=None) -> MorphoBranch +Morphology.branch_by_order(*, order="default") -> tuple[MorphoBranch, ...] +Morphology.path_to_root(branch_index) -> tuple[int, ...] +Morphology.topo() -> str +Morphology.select(expr, *, cache=None) -> RegionMask | LocsetMask +MorphoBranch.index_by(*, order="default") -> int +MorphoMetric.as_dict() -> dict +``` + +branch 按 name 或 index 选一个分支,两者互斥。order 支持 default/type/depth: +分别按节点创建顺序、分支类型与名称、根路径深度排序;index 属于所选顺序。 +持久定位宜使用名称,按 type/depth 排列时追加结构可能改变索引。依据见 +[树查询实现](../../../../braincell/morph/morphology.py)。 +select 消费 RegionExpr/LocsetExpr,参数和缓存行为见 [Filter](../../filter/current/api.md)。 + +| 属性 | 返回值和归属 | +| --- | --- | +| root、branches、edges | 根 view、分支 view 元组、只读 MorphoEdge 元组,均由当前树拥有 | +| MorphoBranch.parent、children、n_children | 父 view 或 None、子 view 元组、子节点数 | +| MorphoBranch.index / branch_id、branch_order、n_tapers | 空间选择所用整数索引、拓扑层次和几何段数 | +| revision | 当前结构修订号,用于缓存失效 | +| metric | 当前统计的冻结 MorphoMetric 快照,修改树后应重新读取 | +| total_length、mean_radius | 长度量 | +| total_area、total_volume | 面积和体积量 | +| n_branches、n_stems、n_bifurcations、max_branch_order | 整数拓扑指标 | +| max_path_distance、max_path_distance_excluding_soma | 树路径距离 | +| max_euclidean_distance 及 excluding_soma 变体、x/y/z_range | 依赖完整三维坐标的距离量;缺失坐标时抛出 ValueError | + +几何存储、连接方向与导入分层见 [架构约束](layering-invariants.md)。 +读写和 checkpoint 方法统一由 [IO API](../../io/current/api.md) 维护, +vis2d/vis3d 完整调用见 [Vis API](../../vis/current/api.md)。 diff --git a/docs/design/morph-layering-invariants.md b/docs/design/morph/current/layering-invariants.md similarity index 100% rename from docs/design/morph-layering-invariants.md rename to docs/design/morph/current/layering-invariants.md diff --git a/docs/design/network/TODO.md b/docs/design/network/TODO.md new file mode 100644 index 00000000..3673f5df --- /dev/null +++ b/docs/design/network/TODO.md @@ -0,0 +1,33 @@ +# Network TODO + +Network 的协作入口。[全局 TODO](../TODO.md) 管理宏观目标与跨模块阻塞, +[Design 规范](../AGENTS.md) 定义分类和状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步或待决定问题 | 文档 | +| --- | --- | --- | --- | +| 随机上下文替代 Network seed | 讨论中 | 确定默认流、局部子流、生命周期与兼容迁移边界 | [随机上下文](proposals/random-context.md) | +| Synapse 动力学与 Connection 权重可塑性 | 讨论中 | 细化共用的挂载、信号绑定、事件输入与生命周期合同,核对状态共享及调度语义 | [可塑性](proposals/connection-plasticity.md) | +| I-09 稀疏 delay slots | 待讨论 | 比较表示、选择规则、静态 shape 与性能基准 | [运行时扩展](proposals/runtime-extensions.md#i-09-sparse-delay-slots) | +| I-10 可学习 topology | 待讨论 | 明确结构 mutation、state 与重编译协议 | [运行时扩展](proposals/runtime-extensions.md#i-10-trainable-topology) | +| I-11 大规模 endpoint generators | 待讨论 | 确定 chunking、专用生成器与内存验收 | [运行时扩展](proposals/runtime-extensions.md#i-11-scalable-endpoint-generators) | +| Network batch runtime | 待讨论 | 明确网络 batch 轴、拓扑和事件约定 | [运行时扩展](proposals/runtime-extensions.md#network-batch-runtime) | + +## 已实现内容索引 + +- [API](current/api.md):公开入口、参数合同与用法。 +- [事件与连接](current/connections.md)、[端点配对](current/pairing.md)、[记录与结果](current/recording.md):按任务查阅接口。 +- [架构](current/architecture.md):公开模型、Cell-owned storage、事件调度、生命周期和 v1 边界。 +- [模块分层](current/module-layout.md):内部职责、导入边界和命名。 + +历史编号和原测试记录见 [决定快照](../../specs/2026-09-07-network-decisions-snapshot.md)、 +[验证快照](../../specs/2026-09-07-network-verification-snapshot.md)。现行决定由 API 和架构维护。 + +## 参考入口 + +- [平台调研](references/platform-survey-2026-06.md):架构取舍及扩展背景。 +- [Connection 与 Synapse 语义](references/bmtk-netpyne-synapse-sharing.md):既有 owner 边界与候选扩展的依据。 +- [可塑性模型](references/plasticity-models.md):BrainPy 规则与释放模型、来源、横向延伸及数值例子。 + +具体采用状态见各参考页。 diff --git a/docs/design/network/api.md b/docs/design/network/api.md deleted file mode 100644 index beec68aa..00000000 --- a/docs/design/network/api.md +++ /dev/null @@ -1,1043 +0,0 @@ -# Network API - -本文是当前 Network、Synapse、Connection、连续位置采样和 Recording 接口的参考说明。最短的完整流程是: - -```python -net = braincell.Network("demo", seed=7) -stim = net.add_population("stim", braincell.NetStim(size=4)) -post = net.add_population("post", cell, layer="demo") - -post.cell.place(at("dend_b", 0.7), braincell.mech.Synapse("ExpSyn", name="ampa")) -net.connect( - "stim_to_post", - source=stim.event_outputs["spike"], - synapse=post.synapses["ampa"], - weight=0.1 * u.uS, -) - -post.cell.soma.record("v", braincell.observe.state("v")) -result = net.run(dt=0.025 * u.ms, duration=10.0 * u.ms) -``` - -## Network and Population - -### `Network` - -```python -braincell.Network(name=None, *, seed=0) -``` - -创建一个命名网络,统一管理 Population、Connection、时间、随机种子和运行时状态。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `name` | `str or None` | `None` | 可选的网络名称;非空字符串。 | -| `seed` | `int` | `0` | Network 级随机种子,用于派生未显式给定的局部随机流。 | - -#### Main attributes - -| Attribute | Meaning | -| --- | --- | -| `name` | Network 名称。 | -| `seed` | Network 级随机种子。 | -| `populations` | `population_name -> Population` 映射。 | -| `connections` | 全网 Connection 查询入口。 | - -### `Network.add_population` - -```python -Network.add_population(name, model, **metadata) -> Population -``` - -将一个已创建的模型或零参数 provider 注册为 Network Population。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `name` | `str` | required | Network 内唯一的非空 Population 名称。 | -| `model` | `Cell`, `NetStim`, `EventSequence`, or callable | required | 模型 owner,或返回其中一种模型的零参数 provider。 | -| `**metadata` | scalar or population-aligned array | - | 自定义 Population metadata;标量广播到 `size`,非标量首维必须等于 `size`。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `Population` | 已解析并由当前 Network 管理的 Population。 | - -#### Notes - -- 同一个模型对象不能注册到多个 Population。 -- metadata 不会转发给 `model`,也不会自动修改 Cell 参数。 -- metadata 名称不能覆盖 Population 的保留属性或方法。 -- Population 必须在 Network 初始化前添加。 - -```python -stim = net.add_population("stim", braincell.NetStim(size=4)) -post = net.add_population( - "post", - cell, - layer="molecular_layer", - position=positions, -) -``` - -### `Population` - -Population 是 Network 中一维模型集合的解析后句柄。正式属性、自定义 metadata 和 Cell 转发入口如下。 - -| Category | Name | Description | -| --- | --- | --- | -| identity | `name` | Network 内唯一名称。 | -| owner | `model` | 被管理的原始 `Cell`、`NetStim` 或 `EventSequence`。 | -| runtime dispatch | `kind` | Network 内部分派使用的只读类型。 | -| shape | `size` | Population 实例数。 | -| indexing | `ids` | 从 0 开始的 Population 局部索引。 | -| events | `event_outputs` | 该 Population 可提供给下游的命名事件输出。 | -| custom data | `metadata` | 自定义字段的只读映射。 | -| Cell forwarding | `cell` | Cell Population 的原始 Cell owner。 | -| Cell forwarding | `synapses` | Cell 拥有的逻辑 Synapse。 | -| Cell forwarding | `connections` | 以该 Cell 为目标的 routing rows。 | - -`event_outputs` 表示 Population 向外提供什么事件,不表示它接收了哪些上游输入。指向 Cell Population -的上游连接通过 `post.connections` 查询。 - -```python -post.layer -post.metadata["layer"] - -post.cell -post.synapses -post.connections -post.event_outputs["spike"] -``` - -### `Population.set` - -```python -Population.set(**metadata) -> Population -``` - -设置经过 Population 维度校验的自定义 metadata。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `**metadata` | scalar or population-aligned array | - | 标量广播到 `size`;数组首维必须等于 `size`。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `Population` | 当前 Population,支持链式调用。 | - -### `Population.register_event_output` - -```python -Population.register_event_output(source, *, name=None) -> EventSourceView -``` - -显式发布一个未参与 Connection、但需要出现在 `NetworkResult.events` 中的 Cell live event output。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `source` | `EventSource or EventSourceView` | required | 由该 Population 的 Cell 驱动的 live event source。 | -| `name` | `str or None` | `None` | Population 内唯一的 output 名称;省略时使用 source 自身名称。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `EventSourceView` | 完整 source owner 的注册视图,即使传入的是 source 子集。 | - -#### Notes - -Cell Population 默认提供 `event_outputs["spike"]`,它检测 `RootLocation(0.5)` 所属 CV 的 canonical -threshold crossing。额外的具名 live EventSource 首次成功用于 `Network.connect()` 时会自动发布,通常 -不需要手动调用本方法。同一个 source owner 只注册一次;同名不同 owner 会报错。 - -```python -monitor = braincell.VoltageCrossingSource( - post.cell, - location=at("dend_a", 0.4), - threshold=-20.0 * u.mV, - name="monitor", -) -post.register_event_output(monitor) -``` - -### `VoltageCrossingSource` - -```python -VoltageCrossingSource( - cells, - *, - location=None, - threshold=, - direction="rising", - name=None, -) -> VoltageCrossingSource -``` - -在 Cell 电压上声明一个或多个 live threshold detectors。它是可连接的 `EventSource`,也可以通过 -`Population.register_event_output()` 只发布到结果中。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `cells` | `Cell or CellView` | required | 提供电压和 threshold state 的 Cell owner;CellView 可选择 Population members。 | -| `location` | locset expression or mask | root midpoint | 一个或多个连续 morphology 点;重复位置保留。 | -| `threshold` | voltage quantity | omitted | 省略时逐 endpoint 使用 Cell 自身的异质 `V_th`;显式值可为 scalar、每 Cell 的 `(P,)`、每位置的 `(1,L)`、`(P,L)` 或 flat endpoint rows。 | -| `direction` | `{"rising", "falling"}` | `"rising"` | rising 为 `v_prev < threshold <= v_next`;falling 为反向 crossing。 | -| `name` | `str or None` | `None` | 额外 event output 自动注册时所需的稳定名称。 | - -#### Endpoint rows - -若选择 `P` 个 Cell members,location 解析出 `L` 个点,则 source 有 `P * L` 行,顺序为 -Population-major,再按 locset 原始顺序排列。以下只读数组把 source row 映射回模型: - -| Attribute | Meaning | -| --- | --- | -| `population_index` | endpoint 所属 Cell member。 | -| `location_index` | endpoint 在已解析 locset 中的行号。 | -| `cv_id` | 连续位置最终所属的 CV。 | - -省略 threshold 的 rising detector 与 Cell canonical spike 使用同一个 `cell.spike` 计算结果。显式 threshold -始终独立比较前后两步电压;省略 threshold 的 falling detector 也会独立使用 Cell `V_th` 比较。 - -```python -all_cv = braincell.VoltageCrossingSource( - post.cell, - location=post.cell.cv_midpoints, - name="all_cv_spikes", -) -post.register_event_output(all_cv) - -result = net.run(dt=0.025 * u.ms, duration=10.0 * u.ms) -events = result.events["post"]["all_cv_spikes"] -events.metadata["population_index"] -events.metadata["location_index"] -events.metadata["cv_id"] -``` - -不同模型的 canonical event output 如下。 - -| Population model | Canonical key | Output | -| --- | --- | --- | -| `Cell` | `"spike"` | root reference CV 的 threshold crossing。 | -| `NetStim` | `"spike"` | NetStim 生成的事件。 | -| `EventSequence` | `"event"` | 显式时间表中的事件。 | - -## Cell Scope and Mechanism Views - -Cell 与 CellView 使用同一套空间选择顺序: - -```text -population members -> branch name/type or region -> CV -> mechanism rows -``` - -```python -cell[[0, 2]] -cell[[0, 2]].dendrite -cell[[0, 2]].dendrite.cv[1:] - -cell.soma.channels -cell.dendrite.ions -cell[1:3].synapses -cell[1:3].connections -``` - -空间 View 只保存索引,不复制 Cell、morphology 或 runtime arrays。机制的公共身份和最小逻辑行如下。 - -| Category | Type | Name | Extra identity | Logical row | -| --- | --- | --- | --- | --- | -| Channel | runtime model,例如 `Na_HH1952` | 用户声明的 owner,例如 `nav` | - | `(population, CV, type, name)` | -| Ion | implementation,例如 `SodiumFixed` | owner,例如 `na_pool` | species,例如 `na` | `(population, CV, type, name)` | -| Synapse | runtime model,例如 `ExpSyn` | group,例如 `fast_ampa` | stable logical ID | 一个独立 Synapse instance | -| Connection | source-to-synapse routing | connect call name | stable row ID | 一行 routing | - -| View | Type selector | Name selector | Other selectors | Numeric slicing | -| --- | --- | --- | --- | --- | -| Channel | `by_type(type)` | `view[name]` | - | 不支持独立 logical row slicing | -| Ion | `by_type(type)` | `view[name]` | `by_species(species)` | 不支持独立 logical row slicing | -| Synapse | `by_type(type)` | `view[name]` | stable IDs | 支持,且保序 | -| Connection | `by_source_type(type)`, `by_synapse_type(type)` | connect/synapse name | stable row IDs | 支持,且保序 | - -```python -cell.channels.by_type("IL") -cell.channels["leak_soma"] - -cell.ions.by_species("na") -cell.ions.by_type("SodiumFixed") -cell.ions["na_pool"] - -cell.synapses.by_type("ExpSyn") -cell.synapses["fast_ampa"] -cell.synapses["fast_ampa"][[0, 2]] -``` - -Channel/Ion 的 `get(field)` 和 `set(**fields)` 要求最终 View 只包含一个 `(type, name)` owner。Synapse -`get/set` 要求同一 type,但可以跨同 type 的多个 name。View 在初始化前读取声明参数;初始化后通过 -logical-to-runtime mapping 读取 runtime parameter/state,不保存第二份数组。 - -### `Cell.place` - -```python -Cell.place(locset, *mechanisms) -> Cell -CellView.place(locset, *mechanisms) -> CellView -``` - -在选定的 Population members 上放置独立 point mechanism instances。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `locset` | `LocsetExpr`, `LocsetMask`, `LocsetBatch`, or sequence | required | 共享位置、矩形批量位置,或每个 member 一个可不等长的位置集合。 | -| `*mechanisms` | point mechanism declarations | required | 要放置的 point mechanisms;异质 per-cell locset 当前用于 Synapse 声明。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `Cell or CellView` | root 调用返回当前 Cell;Population view 调用返回当前 CellView。 | - -#### Notes - -`place` 保留输入位置顺序和重复位置。相同位置、相同 Synapse type/name 的多次放置仍是独立 logical -Synapse instances;runtime 按 Synapse type 组织 SoA storage。 - -## Synapse and Connection - -### `braincell.connect` - -```python -braincell.connect( - name, - *, - source, - synapse, - pairing=None, - weight=, - delay=0.0 * u.ms, -) -> ConnectionView -``` - -低层入口,用于单 Cell 或 Network 组装前,将 EventSource endpoints 绑定到已经存在的 Synapse rows。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `name` | `str` | required | 目标 Cell 内唯一的 Connection call 名称。 | -| `source` | `EventSource or EventSourceView` | required | 有序 source endpoints。 | -| `synapse` | `SynapseView` | required | 有序目标 Synapse;一次调用必须命中一个 synapse type 和一个 name。 | -| `pairing` | `PairingSpec or None` | `None` | endpoint 采样规则;省略时使用等长或 singleton 广播。 | -| `weight` | quantity or row-aligned quantity | synapse event default | 标量或每个生成 row 一个值;单位必须符合 Synapse event-input contract。 | -| `delay` | time quantity | `0.0 * u.ms` | 非负延迟;标量或每个生成 row 一个值。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `ConnectionView` | 本次创建的具体 routing rows。 | - -```python -braincell.connect( - "drive", - source=stim, - synapse=cell.synapses["ampa"], - weight=0.1 * u.uS, - delay=0.5 * u.ms, -) -``` - -### `Network.connect` - -```python -Network.connect( - name, - *, - source, - synapse, - target=None, - locations=None, - pairing=None, - weight=, - delay=0.0 * u.ms, -) -> ConnectionView -``` - -连接已注册的 source,并选择已有 Synapse 或在连接时快捷创建 Synapse。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `name` | `str` | required | 目标 Cell 内唯一的 Connection call 名称。 | -| `source` | `Population`, `EventSource`, or `EventSourceView` | required | 已注册 Population 提供的 source 或 source view。 | -| `synapse` | `SynapseView or Synapse` | required | 已有 Synapse rows,或要放置的新 Synapse 声明。 | -| `target` | `Population or CellView` | `None` | 使用 `Synapse` 时必需;已有 `SynapseView` 时禁止。 | -| `locations` | locset expression, mask, batch, or sequence | `None` | 使用 `Synapse` 时传给 `target.place` 的位置。 | -| `pairing` | `PairingSpec or None` | `None` | 仅支持已有 `SynapseView`。 | -| `weight` | quantity or row-aligned quantity | synapse event default | Connection event payload。 | -| `delay` | time quantity | `0.0 * u.ms` | 标量或 row-aligned 非负延迟。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `ConnectionView` | 创建的 routing rows;快捷创建的目标可通过 `connection.synapse` 访问。 | - -#### Notes - -- source owner 与 target Cell 必须已经注册到同一个 Network。 -- 额外具名 Cell EventSource 会在 Connection 成功后自动发布到 source Population。 -- 快捷调用是原子的;place、广播、pairing 或 endpoint 对齐失败时不会留下孤立 Synapse、Connection - 或自动注册的 event output。 - -连接已有 Synapse: - -```python -connection = net.connect( - "stim_fast", - source=stim.event_outputs["spike"][0:2], - synapse=post.synapses["fast"], - weight=0.08 * u.uS, -) -``` - -快捷创建 Synapse 并连接: - -```python -connection = net.connect( - "stim_slow", - source=stim.event_outputs["spike"][2:4], - target=post.cell[2:4], - locations=at("dend_b", 0.7), - synapse=braincell.mech.Synapse( - "Exp2Syn", - name="slow", - tau1=0.5 * u.ms, - tau2=5.0 * u.ms, - ), - weight=0.12 * u.uS, -) -``` - -### Endpoint alignment - -省略 `pairing` 时,source 和 Synapse 使用以下对齐规则。 - -| Source length | Synapse length | Result | -| --- | --- | --- | -| `C` | `C` | 按输入顺序逐行 zip,产生 `C` rows。 | -| `1` | `C` | 同一个 source 广播到全部 Synapse。 | -| `C` | `1` | 全部 source 广播到同一个 Synapse。 | -| other unequal lengths | other unequal lengths | 报错;必须显式构造重复索引或使用 `pairing`。 | - -```python -net.connect( - "pairs", - source=pre.event_outputs["spike"][[0, 0, 2]], - synapse=post.synapses["ampa"][[1, 3, 3]], -) -``` - -### Connection queries - -| Query | Meaning | -| --- | --- | -| `post.connections["stim_fast"]` | 目标 Cell 上一次具名 connect call。 | -| `post.connections.by_source_type("NetStim")` | 按 source type 筛选。 | -| `post.connections.by_synapse_type("ExpSyn")` | 按 Synapse type 筛选。 | -| `post.connections.by_synapse_name("fast")` | 按 Synapse name 筛选。 | -| `net.connections["post"]` | 目标 Population 的全部 active rows。 | -| `net.connections["post", "stim_fast"]` | 目标 Population 上一次具名 call。 | - -连接名在目标 Cell 内唯一,不同目标 Population 可以同名。`len(ConnectionView)` 是 routing rows; -`len(net.connections)` 是 active named calls;`net.connections.n_rows` 是全网 active rows。 - -## Continuous Location Sampling - -### `braincell.filter.sample` - -```python -braincell.filter.sample( - region, - *, - number, - seed, - measure="length", - density=None, - u_resolution=1e-10, -) -> SampleLocations -``` - -创建一个延迟解析的连续随机 `LocsetExpr`。表达式在获得具体 morphology 后才生成 `branch_id` 和连续 -`branch_x`,因此可以直接传给 `Cell.place` 或 `Network.connect(locations=...)`。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `region` | `RegionExpr` | required | 连续 morphology 支持域。 | -| `number` | positive `int` | required | 样本数;保留抽样顺序和重复位置。 | -| `seed` | `int` | required | 该采样规则独立使用的显式随机种子。 | -| `measure` | `{"normalized", "length", "lateral_area", "area"}` | `"length"` | density 下方的基础几何测度。 | -| `density` | callable or `None` | `None` | 接收 `SamplingContext` 的非负、无量纲位置偏好。 | -| `u_resolution` | `float` | `1e-10` | 数值逆 CDF 的目标精度,范围为 `[1e-12, 1e-5]`。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `SampleLocations` | 延迟到 morphology 已知时解析的 locset expression。 | - -#### Probability measure - -设所选区域为 \(R\),用户 density 为 \(\rho\),`measure` 指定的几何测度为 \(\mu_m\)。对任意 -子区域 \(A\subseteq R\),一个样本落入其中的概率为: - -$$ -P(X\in A) -= -\frac{\displaystyle\int_{A\cap R}\rho(x)\,\mathrm{d}\mu_m(x)} - {\displaystyle\int_R\rho(x)\,\mathrm{d}\mu_m(x)}. -$$ - -因此 `density=None` 等价于 \(\rho(x)=1\)。“均匀”指相对于所选 `measure` 均匀,并不一定对 -`branch_x`、物理长度或膜面积同时均匀。 - -在 branch \(b\) 的归一化坐标 \(x\in[0,1]\) 上,连续部分的未归一化概率密度是: - -$$ -q_b(x)=\rho\!\left(\mathrm{ctx}_b(x)\right)J_{m,b}(x), -$$ - -其中 \(J_{m,b}\) 是从 `branch_x` 到相应几何测度的 Jacobian。令 \(L_b\) 为整条 branch 的物理 -长度,\(r_b(x)\) 为局部半径,则当前四种 measure 为: - -| `measure` | Continuous Jacobian \(J_{m,b}(x)\) | Meaning | -| --- | --- | --- | -| `"normalized"` | \(1\) | 每个被选中的 `branch_x` 区间按归一化宽度贡献质量。 | -| `"length"` | \(L_b\) | 按物理弧长采样;长 branch 区间获得更大质量。 | -| `"lateral_area"` | \(2\pi r_b(x)\sqrt{L_b^2+(\mathrm{d}r_b/\mathrm{d}x)^2}\) | 按正长度圆台的侧面积采样。 | -| `"area"` | 与 `lateral_area` 相同 | 连续部分按侧面积,并额外包含零长度半径跳变的离散面积。 | - -对于 `measure="area"`,零长度 segment 上从 \(r_0\) 跳变到 \(r_1\) 的环形面积作为位于该 -`branch_x` 的离散 probability atom: - -$$ -A_k=\pi(r_0+r_1)|r_1-r_0|=\pi|r_1^2-r_0^2|. -$$ - -总归一化质量同时包含连续区间和离散 atoms: - -$$ -Z -= -\sum_c\int_{x_{c,0}}^{x_{c,1}}q_c(x)\,\mathrm{d}x -+ -\sum_k\rho\!\left(\mathrm{ctx}_k\right)A_k. -$$ - -实现先依据每个连续 component 和 atom 的质量选择 component,再在连续 component 内通过局部 CDF - -$$ -F_c(x) -= -\frac{\displaystyle\int_{x_{c,0}}^x q_c(t)\,\mathrm{d}t} - {\displaystyle\int_{x_{c,0}}^{x_{c,1}}q_c(t)\,\mathrm{d}t} -$$ - -反演 \(F_c(x)=U\), \(U\sim\mathrm{Uniform}(0,1)\),得到连续 `branch_x`。atom 被选中时直接返回 -其固定 `branch_x`。 - -#### SamplingContext - -| Field | Type/shape | Meaning | -| --- | --- | --- | -| `branch_id` | scalar integer | 当前 morphology branch ID。 | -| `branch_name` | `str` | 当前 branch 名称。 | -| `branch_type` | `str` | 当前 branch morphology type。 | -| `branch_x` | scalar or inspected array | 当前连续 branch 坐标。 | -| `radius` | length quantity | `branch_x` 处的局部半径。 | -| `path_distance_to_root` | length quantity | 到 root reference 的树路径距离。 | -| `path_distance_from_soma` | length quantity | 到全部 soma branches 的最短树路径;没有 soma 时 root branch 为零距离区域。 | -| `local_position` | `(..., 3)` length quantity | morphology-local 3-D 坐标;需要完整 3-D geometry。 | -| `position` | `(..., 3)` length quantity | 当前等于 `local_position`,为后续 world transform 保留。 | - -density 必须返回有限、非负、无量纲的 scalar,或与 `context.branch_x` 同形的数组。可通过 -`braincell.filter.metric` 统一读取 `branch_x`、`radius`、`path_distance_from_soma` 和 `position`。 - -```python -def proximal_density(ctx): - distance = braincell.filter.metric.path_distance_from_soma(ctx) - return u.math.exp(-distance / (100.0 * u.um)) - - -locations = braincell.filter.sample( - braincell.filter.branch_in("type", ["dendrite", "apical_dendrite"]), - number=200, - seed=7, - measure="area", - density=proximal_density, -) -cell.place(locations, ampa) -``` - -## Endpoint Pairing - -`pairing=` 从已有 source 和 Synapse candidate views 中生成临时局部索引,最终仍写入普通 Connection -rows,不建立第二套 topology 或 storage。它当前只接受已存在的 `SynapseView`。 - -### Strategy comparison - -| Helper | Row count | Sampling order | Typical use | -| --- | --- | --- | --- | -| `independent(number, ...)` | 固定为 `number` | source 与 Synapse 独立采样 | 已知总 Connection 数。 | -| `source_first(number, ...)` | 固定为 `number` | 先 source,后条件采样 Synapse | Synapse 偏好依赖已选 source。 | -| `synapse_first(number, ...)` | 固定为 `number` | 先 Synapse,后条件采样 source | source 偏好依赖已选 Synapse。 | -| `by_source(degree, ...)` | source degrees 之和 | 每个 source 采样其 Synapse partners | 指定出度。 | -| `by_synapse(degree, ...)` | Synapse degrees 之和 | 每个 Synapse 采样其 source partners | 指定入度。 | -| `match_degrees(source_degree, synapse_degree, ...)` | 两侧 degree 和 | 展开两侧 stubs 后随机匹配 | 同时固定两侧 degree sequence。 | - -### `braincell.network.connection.independent` - -```python -braincell.network.connection.independent( - number, - *, - source_score=None, - synapse_score=None, - source_replace=True, - synapse_replace=True, - group_by=None, - seed=None, -) -> PairingSpec -``` - -固定总 row 数,分别从 source 和 Synapse candidate pools 独立采样。 - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `number` | positive integer scalar or group-aligned array | required | 总 rows,或每个 `target_cell` group 的 rows。 | -| `source_score` | callable or `None` | `None` | source 边际非负权重。 | -| `synapse_score` | callable or `None` | `None` | Synapse 边际非负权重。 | -| `source_replace` | `bool` | `True` | source pool 是否放回采样。 | -| `synapse_replace` | `bool` | `True` | Synapse pool 是否放回采样。 | -| `group_by` | `None or "target_cell"` | `None` | 是否按 target cell 独立运行规则。 | -| `seed` | `int or None` | `None` | 显式局部 seed;给定后覆盖 Network seed 派生。 | - -### `braincell.network.connection.source_first` - -```python -braincell.network.connection.source_first( - number, - *, - source_score=None, - synapse_score=None, - source_replace=True, - replace=True, - group_by=None, - seed=None, -) -> PairingSpec -``` - -先采样 source,再让 `synapse_score(ctx)` 在已选 source 条件下为 Synapse candidates 赋权。 - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `number` | positive integer scalar or group-aligned array | required | 生成 rows 数。 | -| `source_score` | callable or `None` | `None` | 第一阶段 source 边际权重。 | -| `synapse_score` | callable or `None` | `None` | 第二阶段条件 Synapse 权重。 | -| `source_replace` | `bool` | `True` | 第一阶段 source 是否放回。 | -| `replace` | `bool` | `True` | 同一固定 source 的 Synapse partners 是否可重复。 | -| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | -| `seed` | `int or None` | `None` | 显式局部 seed。 | - -### `braincell.network.connection.synapse_first` - -```python -braincell.network.connection.synapse_first( - number, - *, - source_score=None, - synapse_score=None, - synapse_replace=True, - replace=True, - group_by=None, - seed=None, -) -> PairingSpec -``` - -先采样 Synapse,再让 `source_score(ctx)` 在已选 Synapse 条件下为 source candidates 赋权。 - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `number` | positive integer scalar or group-aligned array | required | 生成 rows 数。 | -| `synapse_score` | callable or `None` | `None` | 第一阶段 Synapse 边际权重。 | -| `source_score` | callable or `None` | `None` | 第二阶段条件 source 权重。 | -| `synapse_replace` | `bool` | `True` | 第一阶段 Synapse 是否放回。 | -| `replace` | `bool` | `True` | 同一固定 Synapse 的 source partners 是否可重复。 | -| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | -| `seed` | `int or None` | `None` | 显式局部 seed。 | - -### `braincell.network.connection.by_source` and `by_synapse` - -```python -braincell.network.connection.by_source( - degree, - *, - synapse_score=None, - replace=True, - group_by=None, - seed=None, -) -> PairingSpec - -braincell.network.connection.by_synapse( - degree, - *, - source_score=None, - replace=True, - group_by=None, - seed=None, -) -> PairingSpec -``` - -分别为每个 source 指定下游 Synapse 数,或为每个 Synapse 指定上游 source 数。 - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `degree` | non-negative integer scalar, array, or callable | required | 每个固定 endpoint 的 partner 数;callable 签名为 `(ctx, rng) -> counts`。 | -| `synapse_score` / `source_score` | callable or `None` | `None` | partner candidate 权重。 | -| `replace` | `bool` | `True` | 同一个固定 endpoint 内 partner 是否可重复。 | -| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | -| `seed` | `int or None` | `None` | 显式局部 seed。 | - -### `braincell.network.connection.match_degrees` - -```python -braincell.network.connection.match_degrees( - source_degree, - synapse_degree, - *, - group_by=None, - seed=None, -) -> PairingSpec -``` - -展开 source 与 Synapse stubs,然后随机一一配对。 - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `source_degree` | scalar, array, or degree callable | required | 每个 source 的 stub 数。 | -| `synapse_degree` | scalar, array, or degree callable | required | 每个 Synapse 的 stub 数。 | -| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | -| `seed` | `int or None` | `None` | 显式局部 seed。 | - -两侧 degree 总和必须严格相等。该策略 v1 不接受 score 或额外约束。 - -### Degree helpers - -| Signature | Distribution / meaning | -| --- | --- | -| `braincell.network.connection.degree.poisson(lam)` | Poisson degree callable。 | -| `braincell.network.connection.degree.binomial(n, p)` | Binomial degree callable。 | -| `braincell.network.connection.degree.negative_binomial(n, p)` | Negative-binomial degree callable,要求 \(p\in(0,1]\)。 | -| `braincell.network.connection.degree.empirical(values, probabilities)` | 从显式离散 PMF 采样 degree。 | - -这些 callable 使用 `brainstate.random.RandomState`,返回非负整数 counts。 - -### Score and grouping contracts - -score callable 接收 `ctx`,必须返回有限、非负、无量纲权重。权重 \(w_i\) 的归一化概率为: - -$$ -p_i=\frac{w_i}{\sum_j w_j}. -$$ - -条件采样时固定端形状为 `(B, 1)`,候选端为 `(1, K)`,score 应可广播到 `(B, K)`;边际 score -使用 `B=1`。每个被归一化的候选行至少需要一个正值。 - -| Context | Available information | -| --- | --- | -| Synapse | logical/location/CV/branch IDs、population index、radius、树路径距离、3-D position、`get(parameter)`。 | -| Source | source ID、type、name、owner、可用时的 population index、`get(field)`。 | - -默认候选 endpoints 属于一个全局池。`group_by="target_cell"` 按 Synapse `population_index` 升序分组, -每组独立执行规则后拼接。固定行数规则的 `number` 可以是 scalar,或长度等于实际分组数的一维整数数组。 - -候选 source/Synapse views 不能含重复 ID,但生成结果允许重复。`replace=False` 在边际采样中分别作用于 -对应池;条件采样只保证同一个固定 endpoint 的 partners 不重复,不保证全局 pair 唯一。生成零行会报错, -且不会修改 Connection store。 - -```python -net.connect( - "distance_conditioned", - source=pre.event_outputs["spike"], - synapse=post.synapses["ampa"], - pairing=braincell.network.connection.source_first( - 500, - synapse_score=lambda ctx: distance_kernel( - ctx.source.get("position"), - ctx.synapse.position, - ), - seed=8, - ), -) - -pairing = braincell.network.connection.by_synapse( - braincell.network.connection.degree.poisson(5.0), - source_score=lambda ctx: source_preference(ctx.source), - replace=False, - seed=9, -) -``` - -直接 `braincell.connect` 的隐式 pairing seed root 为 0。`Network.connect` 从 Network seed 与 source -Population、target Population 和 connection name 派生,与 Population 添加顺序无关。显式 pairing -seed 完全覆盖 Network seed。 - -## Recording and Results - -### `Cell.record` - -```python -Cell.record( - name, - observable, - *, - period=None, - frequency=None, - start=0.0 * u.ms, -) -> RecordingSpec -``` - -在调用它的 Cell/CellView 空间 scope 上注册一个静态 observer。Recording 不调用 `place()`,不创建 -point mechanism,也不改变 runtime layout。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `name` | `str` | required | Cell 内唯一 recording 名称。 | -| `observable` | observe descriptor | required | 由 `braincell.observe.*` 构建的观测声明。 | -| `period` | time quantity or `None` | `None` | 规则采样周期;与 `frequency` 互斥。 | -| `frequency` | frequency quantity or `None` | `None` | 规则采样频率;与 `period` 互斥。 | -| `start` | time quantity | `0.0 * u.ms` | 全局采样计划的开始时间。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `RecordingSpec` | 由 root Cell 拥有的不可变 recording 声明。 | - -`period` 和 `frequency` 都省略时每个 `dt` 采样。`period` 和 `start` 在首次 run、`dt` 已知时解析, -并必须是 `dt` 的整数倍。Recording 只能在初始化前添加。 - -### Observable constructors - -| Signature | Selector | Result rows | -| --- | --- | --- | -| `observe.state(field)` | 当前空间 scope | 每个选定 `(population, CV)` 一行。 | -| `observe.channel(type=None, name=None)` | `type`、`name` 或全部 Channel owners | 每个匹配 Channel owner 和 `(population, CV)` 一行。 | -| `observe.ion(species=None, type=None, name=None)` | `species`、`type`、`name` 或全部 Ion owners | 每个匹配 Ion owner 和 `(population, CV)` 一行。 | -| `observe.synapse(type=None, name=None, ids=None)` | `type`、`name`、stable IDs 或全部 Synapse | 每个匹配 stable Synapse ID 一行。 | -| `observe.membrane_current()` | 当前空间 scope | 每个 `(population, CV)` 的总膜电流密度一行。 | -| `observe.clamp_current(reduce="sum")` | 当前空间 scope | 每个 `(population, CV)` 的外部 clamp 电流合计。 | -| `observe.clamp_current(reduce="none")` | 当前空间 scope | 每个匹配 clamp placement 一行。 | - -Channel、Ion 和 Synapse builder 提供: - -| Signature | Meaning | -| --- | --- | -| `.state(field)` | 保留每个匹配 mechanism row 的指定 state。 | -| `.current(reduce="sum")` | 将命中 current contributors 按 `(population, CV)` 求和。 | -| `.current(reduce="none")` | 保留每个 current contributor。 | - -Connection 的 `weight` 和 `delay` 是静态 routing 参数,应通过 `ConnectionView` 查询,不属于 recording -observable。 - -### `ClampView` - -`cell.clamps` 返回所有逻辑电流 clamp 的稳定 view。Clamp 没有 semantic name,字符串下标按类型选择; -类型筛选可继续使用普通位置索引: - -```python -dc = cell.clamps["CurrentClamp"] -second_dc = dc[1] -cell.clamps.by_type(braincell.CurrentClamp).record("dc_inputs") -``` - -记录结果位于 `result.samples["dc_inputs"]`;每个 logical clamp 对应一列,不跨位置求和。该 -`SampleBlock.time` 是实际刺激求值时间 `step_start + 0.5 * dt`。Solver 在整个主步内消费与 recording -相同的缓存值,包括 Runge-Kutta 的所有局部阶段。 - -```python -cell[[0, 2]].dendrite.cv[1:].record( - "dend_v", - braincell.observe.state("v"), - period=0.1 * u.ms, -) -cell.soma.record( - "nav_p", - braincell.observe.channel(name="nav").state("p"), - frequency=10.0 * u.kHz, -) -cell.soma.record( - "sodium_current", - braincell.observe.ion(species="na").current(), -) -cell.record( - "selected_g", - braincell.observe.synapse(ids=synapse_ids).state("g"), -) -cell.soma.record( - "membrane_current", - braincell.observe.membrane_current(), - start=0.5 * u.ms, -) -``` - -### `NetworkResult` - -`Network.run` 返回不可变的 `NetworkResult`。 - -| Attribute | Structure | Meaning | -| --- | --- | --- | -| `time` | time quantity | 当前 segment 的 step times。 | -| `samples` | `population -> recording -> SampleBlock` | 新 Recording API 的规则样本。 | -| `events` | `population -> output -> EventSeries` | 稀疏 event outputs。 | -| `start_time`, `stop_time`, `dt` | time quantities | segment 边界和固定步长。 | -| `traces` | `population -> probe -> values` | legacy Probe compatibility。 | - -```python -block = result.samples["post"]["dend_v"] -block.time -block.values -block.schema.rows - -events = result.events["stim"]["spike"] -events.time -events.source_id -events.count -events.metadata -``` - -对于多位点 Cell event output,metadata 包含与 source endpoint 行对齐的只读 `population_index`、 -`location_index` 和 `cv_id`。`source_id` 索引这些映射数组,而不是直接表示 Cell ID。 - -`SampleBlock.values` 第一维是规则采样时间,最后一维与 `RecordingSchema.rows` 一一对应。每个 -`RecordingRow` 保存 population/CV/point/branch、field/unit,以及可用时的 mechanism category/type/name -和 Synapse ID。求和 current 的 `contributor_ids` 保存归约前 contributor positions。 - -### `NetworkResult.concat` - -```python -NetworkResult.concat(parts) -> NetworkResult -``` - -合并时间连续且 schema 一致的多个运行结果。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `parts` | iterable of `NetworkResult` | required | 按时间排序的连续 segments。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `NetworkResult` | 合并后的不可变结果。 | - -所有 segments 必须具有相同 `dt`、相接的时间边界,以及相同的 sample/event Population、recording names -和 recording schemas。 - -## Lifecycle and Run - -### `Network.run` - -```python -Network.run( - *, - dt, - duration, - delay_quantization="nearest", - event_backend="auto", - brainevent_backend="jax_raw", -) -> NetworkResult -``` - -以固定 `dt` 推进 Network,并返回当前时间 segment 的结果。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `dt` | positive time quantity | required | 固定仿真步长。 | -| `duration` | positive time quantity | required | 当前调用推进的持续时间,必须是 `dt` 的整数倍。 | -| `delay_quantization` | `{"nearest", "ceil", "floor", "strict"}` | `"nearest"` | 将 Connection delay 映射到整数 steps 的规则。 | -| `event_backend` | `{"auto", "scatter", "brainevent"}` | `"auto"` | event delivery backend。 | -| `brainevent_backend` | `str or None` | `"jax_raw"` | 选择 BrainEvent 的具体 backend。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `NetworkResult` | 当前半开时间区间 `[start_time, stop_time)` 的不可变结果。 | - -#### Notes - -- 第一次 `run` 隐式调用一次 `init_state`。初始化后不能添加 Population、Synapse、Connection 或 Recording。 -- 首次运行后,`dt`、delay quantization 和 event backend 固定。 -- 后续 `run` 从当前全局时间继续,并保留 Cell、Channel、Ion、Synapse 状态、threshold detector history、 - 在途 delay events、recording schedule 和 RNG 状态。 -- 因而在相同初始模型、seed、`dt` 和 runtime 配置下,连续 `run(5 ms)` 两次与一次 `run(10 ms)` - 产生相同的连续状态轨迹;区别是前者得到两个分段结果。 -- 各 segment 使用相接的半开时间区间,因此边界时间不会被重复采样。 - -```python -first = net.run(dt=0.025 * u.ms, duration=5.0 * u.ms) -second = net.run(dt=0.025 * u.ms, duration=5.0 * u.ms) - -assert first.stop_time == second.start_time -joined = braincell.NetworkResult.concat((first, second)) -``` - -### `Network.reset_state` - -```python -Network.reset_state(batch_size=None) -> Network -``` - -将已初始化 Network 的动态状态恢复到初始化基线,同时保留已经编译的 topology 和 runtime layout。 - -#### Parameters - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `batch_size` | `None` | `None` | Network batch execution 尚未实现;非 `None` 会报错。 | - -#### Returns - -| Type | Description | -| --- | --- | -| `Network` | 当前 Network,支持链式调用。 | - -`reset_state` 将全局时间重置为 `0 ms`,恢复 Cell 和 Synapse 初始化状态,并清空 delay queues。它不会调用 -`Cell.reset()`,不会返回可编辑声明阶段。 - -Network 的紧凑表示同时报告具名 connections 和实际 routing rows: - -```text -Network(name='demo', populations=2, connections=2, rows=4) -``` diff --git a/docs/design/network/current/api.md b/docs/design/network/current/api.md new file mode 100644 index 00000000..d3c348e2 --- /dev/null +++ b/docs/design/network/current/api.md @@ -0,0 +1,262 @@ +# Network API + +`braincell.Network` 注册模型、冻结拓扑并按固定步长推进。事件与连接、配对、记录分别由下列专题维护。 + +| 任务 | 文档 | +| --- | --- | +| 事件源与连接 | [Connections](connections.md) | +| 端点配对与随机规则 | [Pairing](pairing.md) | +| 观测声明与结果 | [Recording](recording.md) | +| Cell 空间与机制视图 | [Cell Views](../../cell/current/views.md) | +| 连续位置采样 | [Filter Sampling](../../filter/current/sampling.md) | +| 突触动力学 | [Synapse API](../../synapse/current/api.md) | + +内部执行见 [架构](architecture.md)。当前 `seed` 行为见 [配对随机数](pairing.md),替换方向见 [随机上下文提案](../proposals/random-context.md)。 + +## 最小用法 + +这个完整例子把两个事件源分别接到两个 Cell 的中点突触,并读取电压和稀疏事件。 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import AllRegion, RootLocation + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[5.0, 5.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), pop_size=2, cv_policy=bc.CVPerBranch(1)) +cell.paint(AllRegion(), bc.mech.Channel("IL", g_max=0.1 * u.mS / u.cm**2, E=-65.0 * u.mV)) +cell.place(RootLocation(0.5), bc.mech.Synapse("ExpSyn", name="ampa", tau=2.0 * u.ms)) +cell.loc(RootLocation(0.5)).record("v", bc.observe.state("v")) +net = bc.Network("demo", seed=7) +stim = net.add_population("stim", bc.NetStim(size=2, start=0.1 * u.ms)) +post = net.add_population("post", cell) +connections = net.connect("input", source=stim, synapse=post.synapses["ampa"], weight=0.001 * u.uS) +result = net.run(dt=0.025 * u.ms, duration=0.5 * u.ms) +assert len(connections) == 2 +assert result.samples["post"]["v"].values.shape == (20, 2) +assert result.events["stim"]["spike"].source_id.shape == (2,) +``` + +后续 text 块是签名和依赖已有模型的调用形式;可执行运行片段可复用上述 net。 + +## Network and Population + +### `Network` + +```text +braincell.Network(name=None, *, seed=0) +``` + +创建一个命名网络,统一管理 Population、Connection、时间、随机种子和运行时状态。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `str or None` | `None` | 可选的网络名称;非空字符串。 | +| `seed` | `int` | `0` | Network 级随机种子,用于派生未显式给定的局部随机流。 | + +#### Main attributes + +| Attribute | Meaning | +| --- | --- | +| `name` | Network 名称。 | +| `seed` | Network 级随机种子。 | +| `populations` | `population_name -> Population` 映射。 | +| `connections` | 全网 Connection 查询入口。 | + +### `Network.add_population` + +```text +Network.add_population(name, model, **metadata) -> Population +``` + +将一个已创建的模型或零参数 provider 注册为 Network Population。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `str` | required | Network 内唯一的非空 Population 名称。 | +| `model` | `Cell`, `NetStim`, `EventSequence`, or callable | required | 模型 owner,或返回其中一种模型的零参数 provider。 | +| `**metadata` | scalar or population-aligned array | - | 自定义 Population metadata;标量广播到 `size`,非标量首维必须等于 `size`。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `Population` | 已解析并由当前 Network 管理的 Population。 | + +#### Notes + +- 同一个模型对象不能注册到多个 Population。 +- metadata 不会转发给 `model`,也不会自动修改 Cell 参数。 +- metadata 名称不能覆盖 Population 的保留属性或方法。 +- Population 必须在 Network 初始化前添加。 + +```text +stim = net.add_population("stim", braincell.NetStim(size=4)) +post = net.add_population( + "post", + cell, + layer="molecular_layer", + position=positions, +) +``` + +### `Population` + +Population 是 Network 中一维模型集合的解析后句柄。正式属性、自定义 metadata 和 Cell 转发入口如下。 + +| Category | Name | Description | +| --- | --- | --- | +| identity | `name` | Network 内唯一名称。 | +| owner | `model` | 被管理的原始 `Cell`、`NetStim` 或 `EventSequence`。 | +| runtime dispatch | `kind` | Network 内部分派使用的只读类型。 | +| shape | `size` | Population 实例数。 | +| indexing | `ids` | 从 0 开始的 Population 局部索引。 | +| events | `event_outputs` | 该 Population 可提供给下游的命名事件输出。 | +| custom data | `metadata` | 自定义字段的只读映射。 | +| Cell forwarding | `cell` | Cell Population 的原始 Cell owner。 | +| Cell forwarding | `synapses` | Cell 拥有的逻辑 Synapse。 | +| Cell forwarding | `connections` | 以该 Cell 为目标的 routing rows。 | + +`event_outputs` 表示 Population 向外提供什么事件,不表示它接收了哪些上游输入。指向 Cell Population +的上游连接通过 `post.connections` 查询。 + +```text +post.layer +post.metadata["layer"] + +post.cell +post.synapses +post.connections +post.event_outputs["spike"] +``` + +### `Population.set` + +```text +Population.set(**metadata) -> Population +``` + +设置经过 Population 维度校验的自定义 metadata。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `**metadata` | scalar or population-aligned array | - | 标量广播到 `size`;数组首维必须等于 `size`。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `Population` | 当前 Population,支持链式调用。 | + + +## Lifecycle and Run + +### `Network.init_state` + +```text +Network.init_state(batch_size=None) -> Network +``` + +验证事件源归属并初始化各 Cell,返回当前 Network。batch_size 当前只接受 None; +初始化后结构冻结。重复调用直接返回,不重置已经运行的 Cell;重新开始轨迹使用 reset_state。 + +### 单步与训练入口 + +```text +Network.prepare_run(*, dt, delay_quantization="nearest", event_backend="auto", + brainevent_backend="jax_raw") -> Network +Network.update() -> dict[str, spike array] +``` + +prepare_run 在 tracing 外初始化并准备固定路由、队列和 dt,返回当前 Network; +参数单位和选项与 run 相同,至少需要一个 Cell population。重复调用复用固定配置,不重置动态状态; +改变已固定的 dt 或 backend 会抛出 RuntimeError。 +update 需先 prepare_run,否则抛出 RuntimeError;它物化可训参数、推进一个 dt 并更新各 Cell 时间, +返回按 Cell population 名组织的 `cell.spike.value` 浮点数组;电缆模型形状为 +`cell.pop_size + (cell.n_cv,)`,最后一轴保留全部 CV 的检测结果。Reduction 模型沿用自身事件输出形状。 +它不构造 host 侧的 recording/event 结果表,可放入 brainstate.transform.for_loop/scan。 +训练上下文见 [Trainable API](../../optim/current/api.md#synapse-connection-network)。 + +### `Network.run` + +```text +Network.run( + *, + dt, + duration, + delay_quantization="nearest", + event_backend="auto", + brainevent_backend="jax_raw", +) -> NetworkResult +``` + +以固定 `dt` 推进 Network,并返回当前时间 segment 的结果。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `dt` | positive time quantity | required | 固定仿真步长。 | +| `duration` | positive time quantity | required | 当前调用推进的持续时间,必须是 `dt` 的整数倍。 | +| `delay_quantization` | `{"nearest", "ceil", "floor", "strict"}` | `"nearest"` | 将 Connection delay 映射到整数 steps 的规则。 | +| `event_backend` | `{"auto", "scatter", "brainevent"}` | `"auto"` | event delivery backend。 | +| `brainevent_backend` | `str or None` | `"jax_raw"` | 选择 BrainEvent 的具体 backend。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `NetworkResult` | 当前半开时间区间 `[start_time, stop_time)` 的不可变结果。 | + +#### Notes + +- 第一次 `run` 隐式调用一次 `init_state`。初始化后不能添加 Population、Synapse、Connection 或 Recording。 +- 首次运行后,`dt`、delay quantization 和 event backend 固定。 +- 后续 `run` 从当前全局时间继续,并保留 Cell、Channel、Ion、Synapse 状态、threshold detector history、 + 在途 delay events、recording schedule 和 RNG 状态。 +- 因而在相同初始模型、seed、`dt` 和 runtime 配置下,连续 `run(5 ms)` 两次与一次 `run(10 ms)` + 产生相同的连续状态轨迹;区别是前者得到两个分段结果。 +- 各 segment 使用相接的半开时间区间,因此边界时间不会被重复采样。 + +```text +first = net.run(dt=0.025 * u.ms, duration=5.0 * u.ms) +second = net.run(dt=0.025 * u.ms, duration=5.0 * u.ms) + +assert first.stop_time == second.start_time +joined = braincell.NetworkResult.concat((first, second)) +``` + +### `Network.reset_state` + +```text +Network.reset_state(batch_size=None) -> Network +``` + +将已初始化 Network 的动态状态恢复到初始化基线,同时保留已经编译的 topology 和 runtime layout。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `batch_size` | `None` | `None` | Network batch execution 尚未实现;非 `None` 会报错。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `Network` | 当前 Network,支持链式调用。 | + +`reset_state` 将全局时间重置为 `0 ms`,恢复 Cell 和 Synapse 初始化状态,并清空 delay queues。它不会调用 +`Cell.reset()`,不会返回可编辑声明阶段。 + +Network 的紧凑表示同时报告具名 connections 和实际 routing rows: + +```text +Network(name='demo', populations=2, connections=2, rows=4) +``` diff --git a/docs/design/network/architecture.md b/docs/design/network/current/architecture.md similarity index 63% rename from docs/design/network/architecture.md rename to docs/design/network/current/architecture.md index 48742aff..638cfe34 100644 --- a/docs/design/network/architecture.md +++ b/docs/design/network/current/architecture.md @@ -1,7 +1,48 @@ # Network Architecture +BrainCell Network 只负责四件事:注册模型 owner、建立直接事件路由、统一初始化与推进、聚合结果。 +Cell 持有 morphology、机制声明、Synapse/Connection SoA storage 和 runtime;Network 不复制这些数据。 +Recording 同样由 Cell 声明,Network 只按 population name 收集规则 samples 与稀疏 events。 + +## 公开模型 + +```text +Network + populations[name] -> Population(Cell | NetStim | EventSequence) + connections[target_population] -> Cell-owned ConnectionView + run result -> samples[population][recording] + events[population][port] + +EventSource -- Connection(weight, delay) --> Synapse --> target Cell runtime +CellView -- RecordingSpec(observable, schedule) --> SampleBlock(schema, values) +``` + +`Synapse` 拥有 postsynaptic parameters、state 和 dynamics。`Connection` 只拥有 source routing、 +weight 和 delay。一次命名 `connect` 调用可以批量产生多行 routing;connection 数量指命名调用数, +row 数量指实际稀疏路由数。 + +Cell/CellView 先选择 population 与空间,Channel/Ion/Synapse View 再选择机制 identity。Channel 使用 +type/name,Ion 使用 species/type/name,Synapse 使用 type/name/stable IDs。Recording selector 复用同一 +identity 模型,不创建另一套机制对象。 + +## 结构固定条件 + +- source 和 target 必须属于同一 Network,初始化后拓扑冻结。 +- 连接已有 Synapse,或通过 `Network.connect` 快捷完成 place + connect。 +- source/target 等长、`1 -> N`、`N -> 1` 自动对齐;任意显式 pairs 使用重复索引后的 views。 +- `pairing=` 支持固定行数 marginal/conditional sampling、单侧 degree 和双侧 stub matching;它只生成 + 临时端点索引,不进入 Network storage,也不重新引入第二套连接对象。 +- v1 pairing 只消费已有 EventSourceView 与 SynapseView;不会从 Region 同时创建 Synapse。 +- recording 只支持静态 schema;初始化前声明,运行中不能增加或改变记录行。 +- 规则 state/current samples 与稀疏 source events 分开保存;legacy Probe 不是新接口的一部分。 +- 不支持初始化后新增/删除机制、异质 morphology 或 Network batch runtime。 + +入口见 [Network TODO](../TODO.md),开放方向见 [运行时扩展](../proposals/runtime-extensions.md)。 + ## Ownership +owner 边界的比较依据见 [BMTK/NetPyNE 语义](../references/bmtk-netpyne-synapse-sharing.md), +执行模型的调研背景见 [平台调研](../references/platform-survey-2026-06.md),调用契约见 [API](api.md)。 + Cell 是静态声明与 runtime 的 owner。一个 Cell population 内: ```text @@ -91,6 +132,22 @@ Network 只有 editable 和 initialized 两个外部状态。`init_state` 验证 初始化 Cell runtime。成功后不提供 build/deinit 或返回声明态的操作。`reset_state` 只重置动态状态、 时间、detector 和 queues。重复运行复用 setup 和 compiled loop caches。 +## 决定索引 + +既有问题编号保留用于追溯,现行说明按职责维护: + +| 编号 | 现行维护位置 | +| --- | --- | +| I-01 owner、I-02 call/row 与名称 | [Ownership](#ownership)、[Connections](connections.md) | +| I-03 事件单位与符号 | [Synapse API](../../synapse/current/api.md#方程与事件)、[Connection 参数](connections.md#braincellconnect) | +| I-04 生命周期、I-05 延迟与连续运行 | [Network API](api.md#lifecycle-and-run)、[投递](#runtime-delivery) | +| I-06 density overlap | [Cell API](../../cell/current/api.md#paint-与-place) | +| I-07 记录选择与电流归约 | [Recording](recording.md) | +| I-08 配对与随机数 | [Pairing](pairing.md) | + +原决定列表和原始验证数量保存在 [决定快照](../../../specs/2026-09-07-network-decisions-snapshot.md)、 +[验证快照](../../../specs/2026-09-07-network-verification-snapshot.md)。 + ## Mechanism views 空间选择顺序为 population -> region/locset/branch -> CV -> mechanism。Channel/Ion views 使用 diff --git a/docs/design/network/current/connections.md b/docs/design/network/current/connections.md new file mode 100644 index 00000000..4f1fba01 --- /dev/null +++ b/docs/design/network/current/connections.md @@ -0,0 +1,292 @@ +# Network Events and Connections + +事件源产生计数,Connection 把计数按 weight 和 delay 路由到 Cell 拥有的 Synapse。模型注册及运行见 [Network API](api.md),突触自身状态见 [Synapse API](../../synapse/current/api.md)。下方 `text` 块用于签名与调用结构查询。 + +## Scheduled Event Sources + +```text +NetStim(size=1, start=0*u.ms, number=1, interval=10*u.ms, noise=0.0, seed=None, name=None) +EventTable(source_index, time, event_id=None) +EventSequence(size, events, name=None) +EventSequence.from_times(times, *, name=None) -> EventSequence +``` + +NetStim 的 size 是正整数;start/interval 是标量或 `(size,)` 时间量,分别要求非负/正值。 +number 是非负整数计数,noise 是 `[0,1]` 的无量纲比例,两者同样支持每源一值。 +noise=0 产生周期事件;非零时每个间隔的一部分由指数等待时间决定。 +seed=None 的独立源使用根 0,注册进 Network 时由 Network seed 和 population 名派生; +显式 seed 保持源自己的随机流。当前行为的替代设计见 [随机上下文](../proposals/random-context.md)。 + +EventTable 是扁平事件表,source_index 为 `(E,)` 非负整数,time 为同形的非负有限时间量, +event_id 可省略自动编号,显式 ID 需唯一。EventSequence.size 定义源总数,events 为 EventTable, +越界 source_index 抛出 IndexError。from_times 接受每源一个时间 Quantity 数组,可有不同事件数,返回新源。 +这些对象在构造时形成事件日程,事件输出名称分别为 spike 和 event。 + +```python +import braincell as bc +import brainunit as u + +source = bc.EventSequence.from_times(([0.1, 0.4] * u.ms, [0.2] * u.ms)) +assert source.size == 2 +assert len(source.events) == 3 +assert source.events.source_index.tolist() == [0, 0, 1] +``` + +实现与校验见 [event.py](../../../../braincell/network/event.py)。 + +### `Population.register_event_output` + +```text +Population.register_event_output(source, *, name=None) -> EventSourceView +``` + +显式发布一个未参与 Connection、但需要出现在 `NetworkResult.events` 中的 Cell live event output。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `source` | `EventSource or EventSourceView` | required | 由该 Population 的 Cell 驱动的 live event source。 | +| `name` | `str or None` | `None` | Population 内唯一的 output 名称;省略时使用 source 自身名称。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `EventSourceView` | 完整 source owner 的注册视图,即使传入的是 source 子集。 | + +#### Notes + +Cell Population 默认提供 `event_outputs["spike"]`,它检测 `RootLocation(0.5)` 所属 CV 的 canonical +threshold crossing。额外的具名 live EventSource 首次成功用于 `Network.connect()` 时会自动发布,通常 +不需要手动调用本方法。同一个 source owner 只注册一次;同名不同 owner 会报错。 + +```text +monitor = braincell.VoltageCrossingSource( + post.cell, + location=at("dend_a", 0.4), + threshold=-20.0 * u.mV, + name="monitor", +) +post.register_event_output(monitor) +``` + + +### `VoltageCrossingSource` + +```text +VoltageCrossingSource( + cells, + *, + location=None, + threshold=, + direction="rising", + spk_fun=None, + name=None, +) -> VoltageCrossingSource +``` + +在 Cell 电压上声明一个或多个 live threshold detectors。它是可连接的 `EventSource`,也可以通过 +`Population.register_event_output()` 只发布到结果中。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `cells` | `Cell or CellView` | required | 提供电压和 threshold state 的 Cell owner;CellView 可选择 Population members。 | +| `location` | locset expression or mask | root midpoint | 一个或多个连续 morphology 点;重复位置保留。 | +| `threshold` | voltage quantity | omitted | 省略时逐 endpoint 使用 Cell 自身的异质 `V_th`;显式值可为 scalar、每 Cell 的 `(P,)`、每位置的 `(1,L)`、`(P,L)` 或 flat endpoint rows。 | +| `direction` | `{"rising", "falling"}` | `"rising"` | rising 为 `v_prev < threshold <= v_next`;falling 为反向 crossing。 | +| `spk_fun` | callable or `None` | `None` | 默认使用 Cell.spk_fun;自定义函数接收无量纲电压偏差,前向须为零点取 1 的硬阶跃,反向提供代理导数。 | +| `name` | `str or None` | `None` | 额外 event output 自动注册时所需的稳定名称。 | + +#### Endpoint rows + +若选择 `P` 个 Cell members,location 解析出 `L` 个点,则 source 有 `P * L` 行,顺序为 +Population-major,再按 locset 原始顺序排列。以下只读数组把 source row 映射回模型: + +| Attribute | Meaning | +| --- | --- | +| `population_index` | endpoint 所属 Cell member。 | +| `location_index` | endpoint 在已解析 locset 中的行号。 | +| `cv_id` | 连续位置最终所属的 CV。 | + +同时省略 threshold 和 spk_fun 的 rising detector 复用 `cell.spike`。其余情况根据前后两步 +电压计算事件;省略 threshold 时使用 Cell.V_th。到达阈值计一次,在阈值停留不重复发放。 +检测器支持初始化前调用 `trainable(threshold=source)`,规则见 +[训练接口](../../optim/current/api.md#synapse-connection-network)。 + +```text +all_cv = braincell.VoltageCrossingSource( + post.cell, + location=post.cell.cv_midpoints, + name="all_cv_spikes", +) +post.register_event_output(all_cv) + +result = net.run(dt=0.025 * u.ms, duration=10.0 * u.ms) +events = result.events["post"]["all_cv_spikes"] +events.metadata["population_index"] +events.metadata["location_index"] +events.metadata["cv_id"] +``` + +不同模型的 canonical event output 如下。 + +| Population model | Canonical key | Output | +| --- | --- | --- | +| `Cell` | `"spike"` | root reference CV 的 threshold crossing。 | +| `NetStim` | `"spike"` | NetStim 生成的事件。 | +| `EventSequence` | `"event"` | 显式时间表中的事件。 | + + +## Synapse and Connection + +### `braincell.connect` + +```text +braincell.connect( + name, + *, + source, + synapse, + pairing=None, + weight=, + delay=0.0 * u.ms, +) -> ConnectionView +``` + +低层入口,用于单 Cell 或 Network 组装前,将 EventSource endpoints 绑定到已经存在的 Synapse rows。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `str` | required | 目标 Cell 内唯一的 Connection call 名称。 | +| `source` | `EventSource or EventSourceView` | required | 有序 source endpoints。 | +| `synapse` | `SynapseView` | required | 有序目标 Synapse;一次调用必须命中一个 synapse type 和一个 name。 | +| `pairing` | `PairingSpec or None` | `None` | endpoint 采样规则;省略时使用等长或 singleton 广播。 | +| `weight` | quantity or row-aligned quantity | synapse event default | 标量或每个生成 row 一个值;单位必须符合 Synapse event-input contract。 | +| `delay` | time quantity | `0.0 * u.ms` | 非负延迟;标量或每个生成 row 一个值。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `ConnectionView` | 本次创建的具体 routing rows。 | + +```text +braincell.connect( + "drive", + source=stim, + synapse=cell.synapses["ampa"], + weight=0.1 * u.uS, + delay=0.5 * u.ms, +) +``` + +### `Network.connect` + +```text +Network.connect( + name, + *, + source, + synapse, + target=None, + locations=None, + pairing=None, + weight=, + delay=0.0 * u.ms, +) -> ConnectionView +``` + +连接已注册的 source,并选择已有 Synapse 或在连接时快捷创建 Synapse。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `str` | required | 目标 Cell 内唯一的 Connection call 名称。 | +| `source` | `Population`, `EventSource`, or `EventSourceView` | required | 已注册 Population 提供的 source 或 source view。 | +| `synapse` | `SynapseView or Synapse` | required | 已有 Synapse rows,或要放置的新 Synapse 声明。 | +| `target` | `Population or CellView` | `None` | 使用 `Synapse` 时必需;已有 `SynapseView` 时禁止。 | +| `locations` | locset expression, mask, batch, or sequence | `None` | 使用 `Synapse` 时传给 `target.place` 的位置。 | +| `pairing` | `PairingSpec or None` | `None` | 仅支持已有 `SynapseView`。 | +| `weight` | quantity or row-aligned quantity | synapse event default | Connection event payload。 | +| `delay` | time quantity | `0.0 * u.ms` | 标量或 row-aligned 非负延迟。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `ConnectionView` | 创建的 routing rows;快捷创建的目标可通过 `connection.synapse` 访问。 | + +#### Notes + +- source owner 与 target Cell 必须已经注册到同一个 Network。 +- 额外具名 Cell EventSource 会在 Connection 成功后自动发布到 source Population。 +- 快捷调用是原子的;place、广播、pairing 或 endpoint 对齐失败时不会留下孤立 Synapse、Connection + 或自动注册的 event output。 + +连接已有 Synapse: + +```text +connection = net.connect( + "stim_fast", + source=stim.event_outputs["spike"][0:2], + synapse=post.synapses["fast"], + weight=0.08 * u.uS, +) +``` + +快捷创建 Synapse 并连接: + +```text +connection = net.connect( + "stim_slow", + source=stim.event_outputs["spike"][2:4], + target=post.cell[2:4], + locations=at("dend_b", 0.7), + synapse=braincell.mech.Synapse( + "Exp2Syn", + name="slow", + tau1=0.5 * u.ms, + tau2=5.0 * u.ms, + ), + weight=0.12 * u.uS, +) +``` + +### Endpoint alignment + +省略 `pairing` 时,source 和 Synapse 使用以下对齐规则。 + +| Source length | Synapse length | Result | +| --- | --- | --- | +| `C` | `C` | 按输入顺序逐行 zip,产生 `C` rows。 | +| `1` | `C` | 同一个 source 广播到全部 Synapse。 | +| `C` | `1` | 全部 source 广播到同一个 Synapse。 | +| other unequal lengths | other unequal lengths | 报错;必须显式构造重复索引或使用 `pairing`。 | + +```text +net.connect( + "pairs", + source=pre.event_outputs["spike"][[0, 0, 2]], + synapse=post.synapses["ampa"][[1, 3, 3]], +) +``` + +### Connection queries + +| Query | Meaning | +| --- | --- | +| `post.connections["stim_fast"]` | 目标 Cell 上一次具名 connect call。 | +| `post.connections.by_source_type("NetStim")` | 按 source type 筛选。 | +| `post.connections.by_synapse_type("ExpSyn")` | 按 Synapse type 筛选。 | +| `post.connections.by_synapse_name("fast")` | 按 Synapse name 筛选。 | +| `net.connections["post"]` | 目标 Population 的全部 active rows。 | +| `net.connections["post", "stim_fast"]` | 目标 Population 上一次具名 call。 | + +连接名在目标 Cell 内唯一,不同目标 Population 可以同名。`len(ConnectionView)` 是 routing rows; +`len(net.connections)` 是 active named calls;`net.connections.n_rows` 是全网 active rows。 diff --git a/docs/design/network/current/module-layout.md b/docs/design/network/current/module-layout.md new file mode 100644 index 00000000..907e85ea --- /dev/null +++ b/docs/design/network/current/module-layout.md @@ -0,0 +1,57 @@ +# Network Module Layout + +Network 需要被 Cell 在导入过程中使用,又依赖 Cell 的连接和运行实现。 +稳定导入的关键是依赖具体子模块,并把共享声明契约放在 Mech。 +状态归属和执行流程见 [Network 架构](architecture.md)。 + +## 模块职责 + +| 模块 | 职责 | +| --- | --- | +| core | Population、NetworkResult | +| event | EventSource、EventTable、EventSequence、NetStim、VoltageCrossingSource | +| recording | RecordingSpec、RecordingSchema、SampleBlock、EventSeries、observe | +| connection | connect、ConnectionView、NetworkConnections | +| pairing | PairingSpec、score/degree 上下文与临时端点配对 | +| engine | Network 生命周期与运行 | +| lowering | 声明转为 ConnectionBlock | +| delivery | delay queue、稀疏路由与事件累加 | +| mech/_event_contract | 目标机制声明的事件输入契约 | +| mech/_synapse_schema | 目标机制的静态字段声明 | + +## 依赖约束 + +下面只画共享契约的依赖,箭头表示“使用声明”,不是完整 Python import 图: + +```mermaid +flowchart LR + Base[运行时基类] --> Contracts[Mech 契约] + Compute[状态分配] --> Contracts + Connect[连接校验] --> Contracts +``` + +EventInput 描述目标能消费何种 payload,StateSpec 描述目标状态。 +基类、状态分配和连接校验都读取它们,所以放在不导入其他 braincell 包的 Mech 中。 +具体模型导入时向 registry 注册自己,Mech 不反向导入模型。 + +core/event/recording 是 Network 内部的低层模块,但仍依赖 `_misc`、`_parameter_schema` 等基础代码; +“低层”不表示完全没有 braincell 导入。 + +## 部分初始化的父包 + +Cell 和 compute 在模块顶层导入 network.event/recording,Python 会先执行 network/__init__.py。 +此时 `_multi_compartment` 父包可能尚未完成初始化。因此从 Network eager 可达的模块, +不能使用 `from braincell._multi_compartment import Cell` 这类父包名字导入。 +依赖 `.cell`、`.synapses`、`.run` 等具体子模块可以按当前导入图解析。 + +network/__init__.py 当前直接绑定 Network、Population、NetworkResult 等导出,无延迟 `__getattr__`。 +可否 eager 导入由上面的父包约束决定,不由文件大小决定。 + +| 检查 | 保证的性质 | +| --- | --- | +| PartialParentTest | eager 可达模块不从 `_multi_compartment` 包根导入名字 | +| ImportGraphTest | 包内依赖符合显式 DAG | +| MechIsALeafTest | Mech 不导入其他 braincell 包 | + +这些检查位于 [network/__init___test.py](../../../../braincell/network/__init___test.py)。 +历史拆分和旧验收记录见 [Network 历史验证](../../../specs/2026-09-07-network-verification-snapshot.md)。 diff --git a/docs/design/network/current/pairing.md b/docs/design/network/current/pairing.md new file mode 100644 index 00000000..41a1ee88 --- /dev/null +++ b/docs/design/network/current/pairing.md @@ -0,0 +1,237 @@ +# Network Endpoint Pairing + +配对规则将已有端点候选集变成 Connection rows。先按所需行数选择策略,再定义 score 和分组;调用入口见 [Connections](connections.md)。 + +## 最小用法 + +两个源和两个已有突触,从候选池独立抽取三行连接。规则只生成路由索引,不增加 Synapse。 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import RootLocation + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[3.0, 3.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), pop_size=2, cv_policy=bc.CVPerBranch(1)) +cell.place(RootLocation(0.5), bc.mech.Synapse("ExpSyn", name="ampa")) +net = bc.Network(seed=7) +pre = net.add_population("pre", bc.NetStim(size=2)) +post = net.add_population("post", cell) +rows = net.connect("sampled", source=pre, synapse=post.synapses["ampa"], + pairing=bc.network.connection.independent(3, seed=8), weight=0.001*u.uS) +assert len(rows) == 3 +assert len(post.synapses["ampa"]) == 2 +``` + +## Endpoint Pairing + +`pairing=` 从已有 source 和 Synapse candidate views 中生成临时局部索引,最终仍写入普通 Connection +rows,不建立第二套 topology 或 storage。它当前只接受已存在的 `SynapseView`。 + +### Strategy comparison + +| Helper | Row count | Sampling order | Typical use | +| --- | --- | --- | --- | +| `independent(number, ...)` | 固定为 `number` | source 与 Synapse 独立采样 | 已知总 Connection 数。 | +| `source_first(number, ...)` | 固定为 `number` | 先 source,后条件采样 Synapse | Synapse 偏好依赖已选 source。 | +| `synapse_first(number, ...)` | 固定为 `number` | 先 Synapse,后条件采样 source | source 偏好依赖已选 Synapse。 | +| `by_source(degree, ...)` | source degrees 之和 | 每个 source 采样其 Synapse partners | 指定出度。 | +| `by_synapse(degree, ...)` | Synapse degrees 之和 | 每个 Synapse 采样其 source partners | 指定入度。 | +| `match_degrees(source_degree, synapse_degree, ...)` | 两侧 degree 和 | 展开两侧 stubs 后随机匹配 | 同时固定两侧 degree sequence。 | + +### `braincell.network.connection.independent` + +```text +braincell.network.connection.independent( + number, + *, + source_score=None, + synapse_score=None, + source_replace=True, + synapse_replace=True, + group_by=None, + seed=None, +) -> PairingSpec +``` + +固定总 row 数,分别从 source 和 Synapse candidate pools 独立采样。 + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `number` | positive integer scalar or group-aligned array | required | 总 rows,或每个 `target_cell` group 的 rows。 | +| `source_score` | callable or `None` | `None` | source 边际非负权重。 | +| `synapse_score` | callable or `None` | `None` | Synapse 边际非负权重。 | +| `source_replace` | `bool` | `True` | source pool 是否放回采样。 | +| `synapse_replace` | `bool` | `True` | Synapse pool 是否放回采样。 | +| `group_by` | `None or "target_cell"` | `None` | 是否按 target cell 独立运行规则。 | +| `seed` | `int or None` | `None` | 显式局部 seed;给定后覆盖 Network seed 派生。 | + +### `braincell.network.connection.source_first` + +```text +braincell.network.connection.source_first( + number, + *, + source_score=None, + synapse_score=None, + source_replace=True, + replace=True, + group_by=None, + seed=None, +) -> PairingSpec +``` + +先采样 source,再让 `synapse_score(ctx)` 在已选 source 条件下为 Synapse candidates 赋权。 + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `number` | positive integer scalar or group-aligned array | required | 生成 rows 数。 | +| `source_score` | callable or `None` | `None` | 第一阶段 source 边际权重。 | +| `synapse_score` | callable or `None` | `None` | 第二阶段条件 Synapse 权重。 | +| `source_replace` | `bool` | `True` | 第一阶段 source 是否放回。 | +| `replace` | `bool` | `True` | 同一固定 source 的 Synapse partners 是否可重复。 | +| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | +| `seed` | `int or None` | `None` | 显式局部 seed。 | + +### `braincell.network.connection.synapse_first` + +```text +braincell.network.connection.synapse_first( + number, + *, + source_score=None, + synapse_score=None, + synapse_replace=True, + replace=True, + group_by=None, + seed=None, +) -> PairingSpec +``` + +先采样 Synapse,再让 `source_score(ctx)` 在已选 Synapse 条件下为 source candidates 赋权。 + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `number` | positive integer scalar or group-aligned array | required | 生成 rows 数。 | +| `synapse_score` | callable or `None` | `None` | 第一阶段 Synapse 边际权重。 | +| `source_score` | callable or `None` | `None` | 第二阶段条件 source 权重。 | +| `synapse_replace` | `bool` | `True` | 第一阶段 Synapse 是否放回。 | +| `replace` | `bool` | `True` | 同一固定 Synapse 的 source partners 是否可重复。 | +| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | +| `seed` | `int or None` | `None` | 显式局部 seed。 | + +### `braincell.network.connection.by_source` and `by_synapse` + +```text +braincell.network.connection.by_source( + degree, + *, + synapse_score=None, + replace=True, + group_by=None, + seed=None, +) -> PairingSpec + +braincell.network.connection.by_synapse( + degree, + *, + source_score=None, + replace=True, + group_by=None, + seed=None, +) -> PairingSpec +``` + +分别为每个 source 指定下游 Synapse 数,或为每个 Synapse 指定上游 source 数。 + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `degree` | non-negative integer scalar, array, or callable | required | 每个固定 endpoint 的 partner 数;callable 签名为 `(ctx, rng) -> counts`。 | +| `synapse_score` / `source_score` | callable or `None` | `None` | partner candidate 权重。 | +| `replace` | `bool` | `True` | 同一个固定 endpoint 内 partner 是否可重复。 | +| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | +| `seed` | `int or None` | `None` | 显式局部 seed。 | + +### `braincell.network.connection.match_degrees` + +```text +braincell.network.connection.match_degrees( + source_degree, + synapse_degree, + *, + group_by=None, + seed=None, +) -> PairingSpec +``` + +展开 source 与 Synapse stubs,然后随机一一配对。 + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `source_degree` | scalar, array, or degree callable | required | 每个 source 的 stub 数。 | +| `synapse_degree` | scalar, array, or degree callable | required | 每个 Synapse 的 stub 数。 | +| `group_by` | `None or "target_cell"` | `None` | 可选 target-cell grouping。 | +| `seed` | `int or None` | `None` | 显式局部 seed。 | + +两侧 degree 总和必须严格相等。该策略 v1 不接受 score 或额外约束。 + +### Degree helpers + +| Signature | Distribution / meaning | +| --- | --- | +| `braincell.network.connection.degree.poisson(lam)` | Poisson degree callable。 | +| `braincell.network.connection.degree.binomial(n, p)` | Binomial degree callable。 | +| `braincell.network.connection.degree.negative_binomial(n, p)` | Negative-binomial degree callable,要求 \(p\in(0,1]\)。 | +| `braincell.network.connection.degree.empirical(values, probabilities)` | 从显式离散 PMF 采样 degree。 | + +这些 callable 使用 `brainstate.random.RandomState`,返回非负整数 counts。 + +### Score and grouping contracts + +score callable 接收 `ctx`,必须返回有限、非负、无量纲权重。权重 \(w_i\) 的归一化概率为: + +$$ +p_i=\frac{w_i}{\sum_j w_j}. +$$ + +条件采样时固定端形状为 `(B, 1)`,候选端为 `(1, K)`,score 应可广播到 `(B, K)`;边际 score +使用 `B=1`。每个被归一化的候选行至少需要一个正值。 + +| Context | Available information | +| --- | --- | +| Synapse | logical/location/CV/branch IDs、population index、radius、树路径距离、3-D position、`get(parameter)`。 | +| Source | source ID、type、name、owner、可用时的 population index、`get(field)`。 | + +默认候选 endpoints 属于一个全局池。`group_by="target_cell"` 按 Synapse `population_index` 升序分组, +每组独立执行规则后拼接。固定行数规则的 `number` 可以是 scalar,或长度等于实际分组数的一维整数数组。 + +候选 source/Synapse views 不能含重复 ID,但生成结果允许重复。`replace=False` 在边际采样中分别作用于 +对应池;条件采样只保证同一个固定 endpoint 的 partners 不重复,不保证全局 pair 唯一。生成零行会报错, +且不会修改 Connection store。 + +```text +net.connect( + "distance_conditioned", + source=pre.event_outputs["spike"], + synapse=post.synapses["ampa"], + pairing=braincell.network.connection.source_first( + 500, + synapse_score=lambda ctx: distance_kernel( + ctx.source.get("position"), + ctx.synapse.position, + ), + seed=8, + ), +) + +pairing = braincell.network.connection.by_synapse( + braincell.network.connection.degree.poisson(5.0), + source_score=lambda ctx: source_preference(ctx.source), + replace=False, + seed=9, +) +``` + +直接 `braincell.connect` 的隐式 pairing seed root 为 0。`Network.connect` 从 Network seed 与 source +Population、target Population 和 connection name 派生,与 Population 添加顺序无关。显式 pairing +seed 完全覆盖 Network seed。 diff --git a/docs/design/network/current/recording.md b/docs/design/network/current/recording.md new file mode 100644 index 00000000..f8284e64 --- /dev/null +++ b/docs/design/network/current/recording.md @@ -0,0 +1,152 @@ +# Network Recording and Results + +Cell 声明观测及采样计划,Network 按 population 汇总 samples 和 events。独立 Cell 的运行结果见 [Cell API](../../cell/current/api.md#运行与结果)。 + +## 最小用法 + +独立 Cell 使用相同的 recording 声明;Network 结果在外层增加 population 名。 + +```python +import braincell as bc +import brainunit as u +from braincell.filter import RootLocation + +branch = bc.Branch.from_lengths(lengths=[20.0] * u.um, radii=[3.0, 3.0] * u.um) +cell = bc.Cell(bc.Morphology.from_root(branch), cv_policy=bc.CVPerBranch(1)) +cell.loc(RootLocation(0.5)).record("v", bc.observe.state("v"), period=0.05*u.ms) +net = bc.Network() +net.add_population("post", cell) +result = net.run(dt=0.025*u.ms, duration=0.1*u.ms) +block = result.samples["post"]["v"] +assert block.values.shape == (2, 1) +assert len(block.schema.rows) == 1 +``` + +## Recording and Results + +### `Cell.record` + +```text +Cell.record( + name, + observable, + *, + period=None, + frequency=None, + start=0.0 * u.ms, +) -> RecordingSpec +``` + +在调用它的 Cell/CellView 空间 scope 上注册一个静态 observer。Recording 不调用 `place()`,不创建 +point mechanism,也不改变 runtime layout。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `str` | required | Cell 内唯一 recording 名称。 | +| `observable` | observe descriptor | required | 由 `braincell.observe.*` 构建的观测声明。 | +| `period` | time quantity or `None` | `None` | 规则采样周期;与 `frequency` 互斥。 | +| `frequency` | frequency quantity or `None` | `None` | 规则采样频率;与 `period` 互斥。 | +| `start` | time quantity | `0.0 * u.ms` | 全局采样计划的开始时间。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `RecordingSpec` | 由 root Cell 拥有的不可变 recording 声明。 | + +`period` 和 `frequency` 都省略时每个 `dt` 采样。`period` 和 `start` 在首次 run、`dt` 已知时解析, +并必须是 `dt` 的整数倍。Recording 只能在初始化前添加。 + +### Observable constructors + +| Signature | Selector | Result rows | +| --- | --- | --- | +| `observe.state(field)` | 当前空间 scope | 每个选定 `(population, CV)` 一行。 | +| `observe.channel(type=None, name=None)` | `type`、`name` 或全部 Channel owners | 每个匹配 Channel owner 和 `(population, CV)` 一行。 | +| `observe.ion(species=None, type=None, name=None)` | `species`、`type`、`name` 或全部 Ion owners | 每个匹配 Ion owner 和 `(population, CV)` 一行。 | +| `observe.synapse(type=None, name=None, ids=None)` | `type`、`name`、stable IDs 或全部 Synapse | 每个匹配 stable Synapse ID 一行。 | +| `observe.membrane_current()` | 当前空间 scope | 每个 `(population, CV)` 的总膜电流密度一行。 | +| `observe.clamp_current(reduce="sum")` | 当前空间 scope | 每个 `(population, CV)` 的外部 clamp 电流合计。 | +| `observe.clamp_current(reduce="none")` | 当前空间 scope | 每个匹配 clamp placement 一行。 | + +Channel、Ion 和 Synapse builder 提供: + +| Signature | Meaning | +| --- | --- | +| `.state(field)` | 保留每个匹配 mechanism row 的指定 state。 | +| `.current(reduce="sum")` | 将命中 current contributors 按 `(population, CV)` 求和。 | +| `.current(reduce="none")` | 保留每个 current contributor。 | + +Connection 的 `weight` 和 `delay` 是静态 routing 参数,应通过 `ConnectionView` 查询,不属于 recording +observable。 + +Clamp 选择与逐刺激记录见 [ClampView](../../cell/current/views.md#clampview)。 + +### Recording Scopes + +下面是已有多 population、soma/dendrite、nav 通道和所选突触 ID 的模型中的调用形式: + +```text +cell[[0, 2]].dendrite.cv[1:].record("dend_v", braincell.observe.state("v"), period=0.1*u.ms) +cell.soma.record("nav_p", braincell.observe.channel(name="nav").state("p"), frequency=10*u.kHz) +cell.soma.record("sodium_current", braincell.observe.ion(species="na").current()) +cell.record("selected_g", braincell.observe.synapse(ids=synapse_ids).state("g")) +cell.soma.record("membrane_current", braincell.observe.membrane_current(), start=0.5*u.ms) +``` + +### `NetworkResult` + +`Network.run` 返回不可变的 `NetworkResult`。 + +| Attribute | Structure | Meaning | +| --- | --- | --- | +| `time` | time quantity | 当前 segment 的 step times。 | +| `samples` | `population -> recording -> SampleBlock` | 新 Recording API 的规则样本。 | +| `events` | `population -> output -> EventSeries` | 稀疏 event outputs。 | +| `start_time`, `stop_time`, `dt` | time quantities | segment 边界和固定步长。 | +| `traces` | `population -> probe -> values` | legacy Probe compatibility。 | + +```text +block = result.samples["post"]["dend_v"] +block.time +block.values +block.schema.rows + +events = result.events["stim"]["spike"] +events.time +events.source_id +events.count +events.metadata +``` + +对于多位点 Cell event output,metadata 包含与 source endpoint 行对齐的只读 `population_index`、 +`location_index` 和 `cv_id`。`source_id` 索引这些映射数组,而不是直接表示 Cell ID。 + +`SampleBlock.values` 第一维是规则采样时间,最后一维与 `RecordingSchema.rows` 一一对应。每个 +`RecordingRow` 保存 population/CV/point/branch、field/unit,以及可用时的 mechanism category/type/name +和 Synapse ID。求和 current 的 `contributor_ids` 保存归约前 contributor positions。 + +### `NetworkResult.concat` + +```text +NetworkResult.concat(parts) -> NetworkResult +``` + +合并时间连续且 schema 一致的多个运行结果。 + +#### Parameters + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `parts` | iterable of `NetworkResult` | required | 按时间排序的连续 segments。 | + +#### Returns + +| Type | Description | +| --- | --- | +| `NetworkResult` | 合并后的不可变结果。 | + +所有 segments 必须具有相同 `dt`、相接的时间边界,以及相同的 sample/event Population、recording names +和 recording schemas。 diff --git a/docs/design/network/design-overview.md b/docs/design/network/design-overview.md deleted file mode 100644 index 7928b1fb..00000000 --- a/docs/design/network/design-overview.md +++ /dev/null @@ -1,45 +0,0 @@ -# Network Design Overview - -BrainCell Network 只负责四件事:注册模型 owner、建立直接事件路由、统一初始化与推进、聚合结果。 -Cell 持有 morphology、机制声明、Synapse/Connection SoA storage 和 runtime;Network 不复制这些数据。 -Recording 同样由 Cell 声明,Network 只按 population name 收集规则 samples 与稀疏 events。 - -## 文档导航 - -- [API](./api.md):面向用户的完整入口和示例。 -- [Architecture](./architecture.md):Cell-owned storage、事件调度和生命周期。 -- [Issues](./issues.md):已经锁定和仍开放的设计问题。 -- [Implementation plan](./implementation-plan.md):实现状态与验收项。 -- [References](./references/platform-survey-2026-06.md):其他平台的行为参考。 - -## 公开模型 - -```text -Network - populations[name] -> Population(Cell | NetStim | EventSequence) - connections[target_population] -> Cell-owned ConnectionView - run result -> samples[population][recording] + events[population][port] - -EventSource -- Connection(weight, delay) --> Synapse --> target Cell runtime -CellView -- RecordingSpec(observable, schedule) --> SampleBlock(schema, values) -``` - -`Synapse` 拥有 postsynaptic parameters、state 和 dynamics。`Connection` 只拥有 source routing、 -weight 和 delay。一次命名 `connect` 调用可以批量产生多行 routing;connection 数量指命名调用数, -row 数量指实际稀疏路由数。 - -Cell/CellView 先选择 population 与空间,Channel/Ion/Synapse View 再选择机制 identity。Channel 使用 -type/name,Ion 使用 species/type/name,Synapse 使用 type/name/stable IDs。Recording selector 复用同一 -identity 模型,不创建另一套机制对象。 - -## v1 边界 - -- source 和 target 必须属于同一 Network,初始化后拓扑冻结。 -- 连接已有 Synapse,或通过 `Network.connect` 快捷完成 place + connect。 -- source/target 等长、`1 -> N`、`N -> 1` 自动对齐;任意显式 pairs 使用重复索引后的 views。 -- `pairing=` 支持固定行数 marginal/conditional sampling、单侧 degree 和双侧 stub matching;它只生成 - 临时端点索引,不进入 Network storage,也不重新引入第二套连接对象。 -- v1 pairing 只消费已有 EventSourceView 与 SynapseView;不会从 Region 同时创建 Synapse。 -- recording 只支持静态 schema;初始化前声明,运行中不能增加或改变记录行。 -- 规则 state/current samples 与稀疏 source events 分开保存;legacy Probe 不是新接口的一部分。 -- 不支持初始化后新增/删除机制、异质 morphology 或 Network batch runtime。 diff --git a/docs/design/network/module-layout.md b/docs/design/network/module-layout.md deleted file mode 100644 index aa521f0e..00000000 --- a/docs/design/network/module-layout.md +++ /dev/null @@ -1,97 +0,0 @@ -# Network Module Layout - -本文记录 `braincell/network/` 与 `braincell/mech/` 之间的分层约束,以及 -`braincell/network/__init__.py` 为什么必须保持"轻"。改动这两个包之前先读这一页。 - -## 分层 - -```mermaid -flowchart TD - MECH["`braincell.mech`
纯 leaf:声明 + 契约
_event_contract / _synapse_schema"] - BASE["`_base_channel` / `_base_ion`
Channel / Synapse 基类"] - COMPUTE["`_compute`
layouts / state / bindings"] - MC["`_multi_compartment`
Cell / run"] - NETLEAF["`network.core`
`network.event`
`network.recording`
三个 leaf 模块"] - NETHEAVY["`network.connection` / `network.pairing`
`network.engine` / `network.lowering` / `network.delivery`"] - - BASE --> MECH - COMPUTE --> MECH - COMPUTE --> NETLEAF - MC --> MECH - MC --> NETLEAF - NETHEAVY --> MC - NETHEAVY --> MECH - NETHEAVY --> NETLEAF -``` - -关键事实:**箭头没有回边**。`mech` 不 import 任何其它 `braincell` 包, -`network` 的三个 leaf 模块顶层也不 import 任何 `braincell` 包。 - -## 为什么契约在 `mech` 而不在 `network` - -`EventInput` / `NoEventInput` / `TriggerEventInput` / `ScalarEventInput` 是 -*目标机制声明自己能消费什么事件* 的契约,不是事件源。它的调用方是 -`_base_channel.Synapse`(类属性 `event_input`)、`_compute.state`(分配 -runtime event buffer)和 `network.connection`(校验 `connect()` 的 payload)。 - -前两个位于栈底。若契约留在 `network` 包内,`_base_channel` 就要 import -`braincell.network.*`,而 Python 会先执行 `network/__init__.py` → -`connection.py` → `_multi_compartment.synapses` → 回到 `_base_channel`, -形成硬 `ImportError`。 - -`_synapse_schema`(`ParameterSpec` / `StateSpec` / `positive`) -同理:它被 `_base_channel` 和 `synapse.exponential` 共同使用,放进 -`braincell/synapse/` 会经 `synapse/__init__.py` → `exponential.py` → -`braincell._base_channel` 成环。 - -两者都是纯声明、无 runtime state,放在 `mech` 使 `mech` 成为一个真正的 leaf, -栈上任何一层都可以安全依赖它。 - -## `network/__init__.py` 与部分初始化的父包 - -`_multi_compartment.cell`、`_multi_compartment.run` 和 `_compute.layouts` 在 -**模块顶层** import `braincell.network.event` / `braincell.network.recording`。 -Python 在导入任何子模块前先执行包的 `__init__`,所以 `network/__init__.py` -是在 `braincell._multi_compartment` 尚未执行完时被拉起来的。 - -真正的不变式不是「`__init__` 必须保持轻」,而是: - -> 从 `network` 包 eager 可达的任何模块,都不能从**部分初始化**的 -> `braincell._multi_compartment` 包根 import 一个*名字*。 - -`connection.py`、`pairing.py`、`engine.py` 确实会回指 `_multi_compartment`, -但它们 import 的是*子模块*(`.synapses`、`.probes`、`.run`、`.cell`)—— -Python 对部分初始化的父包一样解析得了子模块。真正会炸的写法是 -`from braincell._multi_compartment import Cell`。 - -因此 `__init__.py` 是全 eager 的:`Network`、`NetworkConnections`、 -`NetworkResult`、`Population` 都在模块作用域直接绑定,没有 PEP 562 -`__getattr__`,也没有 `TYPE_CHECKING` 分支。此前那套延迟解析装置守的是一个 -已经不存在的环:把 `__init__.py` 改成全 eager 后 `import braincell` 正常, -整个测试套件也全绿。 - -`braincell/network/__init___test.py` 守住这条不变式:`PartialParentTest` -用 AST 检查禁止 eager 可达的模块从 `braincell._multi_compartment` 包根 -import 名字;`ImportGraphTest` 把包内 import DAG 钉成一张显式的表并断言它 -无环;`MechIsALeafTest` 禁止 `braincell/mech/` 下任何模块 import 其它 -`braincell` 包。`import braincell` 本身是经验性守卫——上述任一环回归都会让 -它直接崩溃。 - -## 命名 - -| 模块 | 职责 | -|---|---| -| `network/core.py` | `Population`、`NetworkResult` | -| `network/event.py` | 事件*源*:`EventSource`、`EventTable`、`EventSequence`、`NetStim`、`VoltageCrossingSource` | -| `network/recording.py` | `RecordingSpec`、`RecordingSchema`、`SampleBlock`、`EventSeries`、`observe` | -| `network/connection.py` | `connect()`、`ConnectionView`、`NetworkConnections` | -| `network/pairing.py` | 端点配对规则:`PairingSpec`、`independent`、`by_source`、`degree` … | -| `network/engine.py` | `Network` | -| `network/lowering.py` | 声明 → `ConnectionBlock` | -| `network/delivery.py` | delay queue / scatter | -| `mech/_event_contract.py` | 事件输入契约 | -| `mech/_synapse_schema.py` | runtime synapse 字段 schema | - -`pairing.py` 取自模块自身导出的词汇(`PairingSpec` / `PairingContext` / -`materialize_pairing`);它此前叫 `braincell/_connection_sampling.py`, -在一个本就以 connection 为主题的包里,`connection_` 前缀是冗余的。 diff --git a/docs/design/network/proposals/connection-plasticity.md b/docs/design/network/proposals/connection-plasticity.md new file mode 100644 index 00000000..de7f5c22 --- /dev/null +++ b/docs/design/network/proposals/connection-plasticity.md @@ -0,0 +1,100 @@ +# Network Connection Plasticity Proposal + +## 状态与目标 + +状态:讨论中。当前 Connection 提供静态 weight 和 delay,Synapse 决定输入后的突触响应, +见 [Network API](../current/api.md)。本文讨论两类可塑性共用的接口边界与运行时架构; +模型清单、来源、横向延伸和数值例子见[可塑性模型参考](../references/plasticity-models.md)。 +进度见 [Network TODO](../TODO.md)。 + +## 两类模型的边界 + +| 类型 | 配置与修改对象 | 动态状态归属 | +| --- | --- | --- | +| Connection 权重可塑 | 挂载规则,更新所属连接的 weight | 每条逻辑 connection row 的 weight、trace 和事件历史 | +| Synapse 内部可塑 | 丰富 Synapse 模型,以内部方程决定释放和响应 | 每个逻辑 Synapse 的资源、释放概率、受体等状态 | + +已确认按状态及修改对象分类,短时和长时只是模型属性。连接规则可以包含连续动力学; +Syn 内部也可以表达长期变化。例如电压驱动规则只更新 weight 时属于第一类,受体数量变化 +由 Syn 内部方程表达时属于第二类。普通电导衰减并不因存在动态状态就成为可塑性。 + +沿用当前[所有权架构](../current/architecture.md):接收 Cell 持有 Connection 与 Synapse, +Network 组织跨模型信号与推进。一条命名 connect call 可以包含多行;逻辑状态归属不要求 +每行分配一个 Python 对象,可以按规则或 Syn 类型打包运行时状态。 + +## 连接上的规则与输入 + +规则配置需要分开表达模型参数和输入信号绑定。初始化时,应将配置绑定到所选 connection rows, +建立各行动态状态及信号映射;配置对象复用不能隐式共享动态状态。具体挂载入口尚未确定。 + +概念数据依赖为: + +```text +selected signals -> rule state transition -> connection weight +source events + connection weight -> delivery -> Synapse dynamics +``` + +箭头不规定同一步的执行顺序。规则只修改所属 weight 和自身状态;读取 Cell、Ion 或其他 +提供者的观测量,不获得这些状态的修改权。连续 trace 演化与事件更新都需要纳入运行时推进。 + +| 输入类别 | 共用接口需要表达的信息 | +| --- | --- | +| pre/post 事件 | 显式 EventSource/port 及到 connection rows 的映射;区分发放时间和延迟后的到达时间 | +| 电压及其他局部量 | 指定空间位置与 observable;区分物理观测量、提供者的滤波量和规则自己的 trace | +| 调制、教学与分组信号 | 指定提供者、目标范围、广播或归约关系;跨行统计不能由某行任意读写其他行状态 | + +缺少规则所需信号时应显式验证,例如没有电压的 NetStim 不能通过补零满足电压依赖。 +具体错误契约与信号绑定接口一并确定。 + +## Synapse 自身的可塑模型 + +Syn 模型沿用自身的参数、状态、事件响应与动力学生命周期,维护资源和受体等内部变化。 +模型参数与可塑状态分开,例如基线利用率与动态释放概率。新增模型需要声明其输入要求, +并与事件投递和积分路径对齐。 + +当前 [ExpSyn](../../../../braincell/synapse/exponential.py) 接收按 Syn 求和的加权电导, +适合线性电导跳变。复杂释放模型可能需要事件次数、幅度或顺序:两次 `0.5 nS` 与一次 +`1 nS` 输入求和相同,资源消耗次数却可能不同。因此只有模型允许时才能先归约再更新; +新增模型不能假定现有标量 payload 保留了逐事件信息。 + +## 两类的组合与共享 + +两类分别维护自己的状态,通过投递和响应连接。例如 Connection 上的 STDP 改变 weight, +Syn 内的 STP 改变释放因子;电导型组合可用 `Delta g = w * q` 表示一次事件的响应, +其中 w 为电导权重,q 为无量纲释放因子。该例只说明职责,事件使用哪一时刻的 w 和 q +仍需按调度合同确定。两层组合应表达不同过程,避免重复计入同一效能变化。 + +已确认独立释放位点使用独立 Syn 状态,可以位于同一 CV。多个 connection 共用一个 Syn 时, +各行 weight 与规则状态仍独立,Syn 的资源、受体等内部状态则共用:一个来源消耗资源会影响 +另一个来源的后续响应。因此共享有状态 Syn 是模型选择,不能仅当作减少对象数的优化。 +参数共享与状态共享必须分别配置,批量打包也不能改变这一语义。 + +## 实现前必须决定 + +| 议题 | 待决定内容 | +| --- | --- | +| 挂载与配置 | connect 声明期或 ConnectionView 的配置入口;参数广播、显式共享分组、初始化后的修改边界 | +| 信号绑定 | 上表各类信号的选择、提供者、位置与行映射;缺失信号的验证契约 | +| 事件与时序 | 窗口端点、配对方式、同刻与重复事件、零/非零 delay;规则与投递顺序,队列保存事件还是已加权输入 | +| Syn 事件输入 | 哪些模型允许归约,哪些需要次数、幅度或顺序;新增输入合同如何兼容现有模型 | +| 状态与生命周期 | 初始参数、动态 weight、规则和 Syn 状态、事件历史及随机状态的初始化、reset、连续 run 与恢复语义 | +| 单位与边界 | 保持现有模型的输入单位合同,定义新增输入与学习率单位、权重上下界及裁剪,不预设所有权重非负 | +| 布局与推进 | connection rows、Syn rows 与 population 轴如何对齐;规则状态如何打包并纳入积分、事件投递与队列路径 | + +以[模型参考](../references/plasticity-models.md)中的代表场景核对这些契约,不从一种 STDP +实现推断所有规则共用相同配对和窗口。可学习连接存在性或增删行仍属于独立的 topology 议题。 + +## 与 Optim 的分工 + +本页管理模型归属、信号、运行时状态与更新时序;[Optim 可塑性提案](../../optim/proposals/connection-plasticity.md) +管理初始权重和规则参数的训练、可微性与 BPTT/full RTRL 验收。规则应能在无优化器的仿真中使用; +优化器参数与仿真中的可塑状态分开,reset 对状态初值的依赖需统一。可仿真不意味着硬事件、 +离散释放及所有规则参数都可微。 + +## 候选验收 + +- 不挂规则时,保留既有 weight、delay、投递、reset 与连续运行行为。 +- 用显式事件、局部观测和调制信号核对绑定、缺失输入验证、同刻/重复事件及延迟时序。 +- 核对独立与共享 Syn 的状态影响、各连接规则状态独立性,以及两类组合后的实际响应。 +- 对比完整与分段运行,覆盖 reset、队列/历史恢复及随机可复现性;核对批量行映射、单位和权重边界。 +- 代表模型的数值对照见[模型例子](../references/plasticity-models.md#模型数值例子),训练与梯度由 Optim 提案补充。 diff --git a/docs/design/network/proposals/random-context.md b/docs/design/network/proposals/random-context.md new file mode 100644 index 00000000..22f4419b --- /dev/null +++ b/docs/design/network/proposals/random-context.md @@ -0,0 +1,84 @@ +# Network Random Context Proposal + +## 状态与目标 + +状态:讨论中。已确认方向是使用 BrainState 随机上下文管理一段代码内的随机过程, +移除对 `Network(seed=...)` 及逐层回退到 Network seed 的依赖。 +局部随机流、生命周期和兼容迁移尚未形成完整实施契约,当前代码仍使用原有 seed 机制。 +进度见 [Network TODO](../TODO.md),当前行为见 [Network API](../current/api.md)。 + +问题不只是 Network 自己怎样抽样。用户可能在构造模型、初始化参数或自定义 callback 中 +调用随机函数;如果每条路径都必须显式实现“seed 为 None 时取 Network seed”,漏掉任何一处 +都会使外部设置无法完整控制实验。默认随机调用应自然使用所在区域的 BrainState 随机流, +用户不需要认识 Network 的 seed owner 或手动转发它。 + +## 当前差距 + +| 路径 | 当前处理 | 提案需要解决的问题 | +| --- | --- | --- | +| Network | 保存自己的 seed | 取消 Network 作为另一套默认 seed 管理入口 | +| NetStim | 显式局部 seed,或由 Network seed 和 population 名称派生;独立使用时隐式 root 为 0 | 明确事件计划实际生成时从哪里取得随机流 | +| Endpoint pairing | Network seed 与规范路径派生,显式 rule seed 覆盖;直接 connect 的隐式 root 为 0 | 统一默认来源,并明确局部子流与顺序稳定性的取舍 | +| 空间采样 | 现有 sample 要求显式 seed,内部构造局部 RNG | 讨论如何参与上下文及与现有采样复现约定兼容 | +| 用户 callback 与随机初值 | 用户自己的调用不自动读取 Network seed | 使用默认 BrainState 随机调用时无需额外转发 seed | + +实现定位:[Network](../../../../braincell/network/engine.py)、 +[NetStim](../../../../braincell/network/event.py)、[pairing](../../../../braincell/network/pairing.py)、 +[空间采样](../../../../braincell/filter/_sampling.py)。本表说明改造边界,不表示已经完成统一。 + +## 上下文方向 + +实际接口名称是 `brainstate.random.seed_context(seed_or_key)`。以下只是上下文写法示意, +不表示当前 Network 内部已经消费该上下文: + +```python +import braincell +import brainstate + +with brainstate.random.seed_context(42): + user_values = brainstate.random.normal(size=(4,)) + net = braincell.Network("demo") +``` + +目标是库内默认随机操作与用户默认随机调用都受这个区域控制。 +BrainState 的 `default_rng()` 无参数时返回默认随机状态;显式给定 seed 时则产生独立状态。 +是否保留现有局部 seed 参数、用嵌套上下文表达局部复现,或从默认流派生持久子流,仍待讨论。 +默认路径不应继续隐式回退到 Network seed 或固定 root 0。 + +上下文管理的是实际执行的随机调用,不是对象所有权。已经生成的数组和事件计划不会因 +后来进入另一个上下文而重新生成;只在构造 Network 时包一次,也不能自动控制之后的初始化和运行。 + +## 控制边界与待决定问题 + +| 问题 | 需要确定的契约 | +| --- | --- | +| 随机源覆盖 | 默认 BrainState 调用纳入统一控制;显式 key、独立 RandomState、NumPy Generator 和其他随机库不自动被接管 | +| 调用顺序 | 相同 seed、相同调用顺序的复现不等于注册顺序无关;插入一次默认流抽样可能改变后续结果 | +| 局部子流 | 是否保留按名称或阶段派生的隔离;若保留,确定从上下文取 key 的时机及用户 callback 的随机语义 | +| 延迟执行 | 分别规定构造、事件计划生成、init_state、reset 和运行时的抽样时机,不在退出上下文后悄悄依赖旧 seed | +| 持久随机状态 | 明确模型保存的 RNG 是否参与 reset、连续 run 和恢复;外部上下文不自动重置既有独立 RNG | +| JIT 与循环变换 | 区分追踪时和执行时随机操作,验证缓存命中、重新追踪及 BrainState 循环中的状态推进 | +| 兼容迁移 | 确定 Network.seed、构造参数及局部 seed 的移除或弃用步骤;已有固定数值和顺序无关测试需要逐项评估 | + +不宣称这个上下文能管理任意用户随机代码,也不承诺不同线程或并行任务天然隔离。 +若后续需要这些能力,应单独确定状态隔离方式。 + +## 候选验收 + +- 在同一个上下文内混合用户默认随机调用、callback、NetStim、pairing 和采样;相同 seed 与相同执行顺序可复现。 +- 验证不同 seed 可改变随机结果;无随机因素的路径不因 seed 改变而改变。 +- 验证嵌套上下文以及正常、异常退出后外层默认随机状态的恢复。 +- 加入额外抽样或改变注册顺序,验证最终选定的顺序依赖或子流隔离契约,不继续无条件沿用旧保证。 +- 对独立 RNG、显式 key 和其他随机库验证控制边界,避免测试误称它们也被外层上下文重置。 +- 覆盖上下文外创建、上下文内初始化及相反情形,以及延迟抽样、JIT 重用、reset 和分段运行。 + +以上是未来实现的验收方向,本次文档工作不新增运行时接口或仿真测试。 + +## 依据与关联 + +- [BrainState seed_context](https://brainstate.readthedocs.io/apis/generated/brainstate.random.seed_context.html):接口名称和默认随机状态的临时切换与恢复。 +- [BrainState default_rng](https://brainstate.readthedocs.io/apis/generated/brainstate.random.default_rng.html):默认状态与显式独立状态的区别。 +- [当前 Network 架构](../current/architecture.md):现有按路径派生 seed 和顺序无关的约定。 + +本地已检查的 BrainState 实现使用 try/finally 保存和恢复默认 RNG key;本文仅据此描述 +BrainState 默认流,不将其扩展为对 NumPy 或任意 RNG 的统一保存恢复保证。 diff --git a/docs/design/network/proposals/runtime-extensions.md b/docs/design/network/proposals/runtime-extensions.md new file mode 100644 index 00000000..2330c1d2 --- /dev/null +++ b/docs/design/network/proposals/runtime-extensions.md @@ -0,0 +1,32 @@ +# Network 运行时扩展 + +状态:待讨论。保留原 issues 的问题编号,并汇总原 implementation-plan 的延后事项。 +进度见 [Network TODO](../TODO.md),已有契约见 [架构](../current/architecture.md) 和 [API](../current/api.md)。 +以下方向尚未形成完整实施与验收合同,不提供占位公共 API。 + +## I-09 Sparse delay slots + +当前每个 target layout 使用 dense time ring,成本与最大 delay 和 layout width 相关。后续评估只保存 +实际 event rows 的 sparse slots,并比较 JIT 静态 shape、scatter 成本和事件密度阈值。 +需要先确定 sparse/dense 自动选择规则及性能基准,不能仅凭理论稀疏度宣称性能提升。 + +## I-10 Trainable topology + +当前只允许初始化前结构编辑和初始化后 shape-preserving 参数更新。可学习连接存在性、位置或新增/ +删除 rows 会改变 JAX shapes,需要独立的 masked/padded 或重编译协议,不能复用普通参数训练接口。 +讨论需覆盖初始化后的结构 mutation、状态 owner、reset 及已有连接身份的兼容性。 + +## I-11 Scalable endpoint generators + +当前通用 pairing 会按实际候选矩阵计算 conditional score;语义已经锁定,但大 N 下仍需增加不改变 +结果的 score chunking、Bernoulli/all-to-all specialized generator,并记录 host peak-memory contract。 + +## Network batch runtime + +这是原计划中的延后方向,不等于已支持的同构 Cell population。先确定 Network 层 batch 的含义、 +拓扑是否共享、事件与 recording 的轴约定,再确定实现范围和验收。 + +## 研究依据 + +- [平台调研](../references/platform-survey-2026-06.md):作为扩展比较背景,不表示引入了任何平台的完整执行模型。 +- [Connection 与 Synapse 语义](../references/bmtk-netpyne-synapse-sharing.md):用于检查扩展能否保持既有 owner 边界;其中候选 recipe 不自动成为已批准接口。 diff --git a/docs/design/network/references/bmtk-netpyne-synapse-sharing.md b/docs/design/network/references/bmtk-netpyne-synapse-sharing.md index 31b8834a..8b593361 100644 --- a/docs/design/network/references/bmtk-netpyne-synapse-sharing.md +++ b/docs/design/network/references/bmtk-netpyne-synapse-sharing.md @@ -1,9 +1,13 @@ # BMTK / NetPyNE 的 Connection 与 Synapse 语义 +关联:[Network TODO](../TODO.md)、[当前决定](../../../specs/2026-09-07-network-decisions-snapshot.md)、[后续扩展](../proposals/runtime-extensions.md)。 +采用状态:Synapse dynamics 与 Connection routing 的 owner 区分已体现在当前架构; +本文中的高层 recipe 和后续建议仅作为讨论依据,不表示都已实现。 + > **Historical, non-normative reference (2026-08).** 本文记录 BMTK 和 NetPyNE > 如何从 cell-pair connectivity 创建 Connection 与 Synapse,并提炼 BrainCell 需要覆盖的 -> 更一般语义。正式接口和内部数据结构仍以 [Network Builder API](../api.md) 与 -> [内部架构规范](../architecture.md) 为准。 +> 更一般语义。正式接口和内部数据结构仍以 [Network Builder API](../current/api.md) 与 +> [内部架构规范](../current/architecture.md) 为准。 ## 1. 要区分的三个问题 diff --git a/docs/design/network/references/plasticity-models.md b/docs/design/network/references/plasticity-models.md new file mode 100644 index 00000000..2246ae4d --- /dev/null +++ b/docs/design/network/references/plasticity-models.md @@ -0,0 +1,129 @@ +# Plasticity Models Reference + +本文比较可塑性模型需要的信号、动态状态与修改对象,为 +[Connection 可塑性提案](../proposals/connection-plasticity.md)的公共接口和架构提供依据。 +模型清单、横向延伸和模型数值例子集中在本页;BrainCell 的职责决定及共享语义以提案为准。 + +采用状态:参考与候选延伸,尚未移植为 BrainCell 可塑性模型。下文“对 BrainCell 的启示”和 +“建议归属”是本项目的设计推论,不是外部库对对象边界的规定。 + +## BrainPy 权重规则 + +参考基线为 2026-09-07 核查的本地 BrainPy 2.7.8 与 brainpy.state 0.0.4 源码。 +下表以 BP 表示 BrainPy,BS 表示 brainpy.state;BS 条目来自其 NEST 兼容模型。 +链接指向官方在线文档,在线版本可能继续更新。它们是方程与模型组织的参考,尚未在 BrainCell +采用;外部类名中的 `synapse` 不决定本项目的职责归属,也不表示其运行时可以直接接入 BrainCell。 + +| 规则家族 | 参考实现 | 输入与核心状态 | 对 BrainCell 的启示 | +| --- | --- | --- | --- | +| 成对 STDP | BP [STDP_Song2000][song];BS [stdp_synapse][pair] | pre/post spike;双侧 trace、weight | 基础时序窗口;区分加性与权重依赖更新 | +| 非线性权重依赖 | BS [stdp_pl_synapse_hom][power]、[jonke_synapse][jonke] | pre/post spike;trace、weight | 分别参考幂律与指数形式,不能仅用一个学习率代表所有规则 | +| 最近邻配对 | BS [stdp_nn_pre_centered_synapse][nn-pre]、[stdp_nn_restr_synapse][nn-restr]、[stdp_nn_symm_synapse][nn-symm] | pre/post spike;最近事件及配对历史 | 配对策略是模型语义,同一 spike 序列可以得到不同更新 | +| Triplet STDP | BS [stdp_triplet_synapse][triplet] | pre/post spike;双侧快慢 trace | 表达三脉冲相互作用及频率依赖 | +| 电压依赖 | BS [clopath_synapse][clopath] | pre 活动、post 电压及滤波量;pre trace、weight | 多区室模型必须明确电压位置;外部模型还依赖目标神经元提供电压历史 | +| 多巴胺调制 STDP | BS [stdp_dopamine_synapse][dopamine] | pre/post spike、多巴胺信号;eligibility 与调制 trace | 局部相关性先形成资格迹,第三因子决定其如何改变 weight | +| 抑制性稳态规则 | BS [vogels_sprekeler_synapse][vogels] | pre/post spike;活动 trace、抑制性 weight | 学习目标可为活动稳态,不能把所有 STDP 都视为兴奋性因果增强 | +| 树突预测学习 | BS [urbanczik_synapse][urbanczik] | pre 活动、树突预测误差;pre trace、滤波学习信号 | 需要 Cell 提供明确的预测误差信号,单个 post spike 端口不够 | + +[stdp_synapse_hom][pair-hom] 将同质可塑性参数与逐连接状态区分,可用于讨论参数共享, +不另列为生物学规则家族。[stdp_facetshw_synapse_hom][hardware] 则提供硬件约束下的离散权重、 +累积量与周期更新参考,作为扩展条目,不作为通用模型默认值。 + +## BrainPy 释放模型 + +版本基线同上。BS 表中的 Tsodyks 等模型在原库按连接模型实现;这里建议借鉴其内部方程, +将独立释放位点的状态放入 BrainCell Syn,而非照搬外部对象边界。 + +| 模型家族 | 参考实现 | 输入与核心状态 | 对 BrainCell 的启示 | +| --- | --- | --- | --- | +| 短时抑制 | BP [STD][bp-std];BS [STD][bs-std] | pre 事件;资源比例 x | 资源消耗与恢复改变后续释放量 | +| 易化与抑制 | BP [STP][bp-stp];BS [STP][bs-stp] | pre 事件;释放变量 u、资源比例 x | 易化和抑制可以同时存在,并有不同恢复时间 | +| Tsodyks 系列 | BS [tsodyks_synapse][tsodyks]、[tsodyks2_synapse][tsodyks2]、[tsodyks_synapse_hom][tsodyks-hom] | pre 事件;利用率、资源状态及事件历史,具体变量依版本而异 | 比较资源方程、释放与消耗顺序;hom 变体另用于讨论参数共享 | +| 随机量子释放 | BS [quantal_stp_synapse][quantal] | pre 事件、随机抽样;可用释放位点与释放概率 | 有限位点的随机释放与恢复,不能用确定性的幅度缩放完全替代 | +| 囊泡池抑制 | BS [ht_synapse][ht] | pre 事件;池可用比例 P | Hill–Tononi 的恢复、释放、消耗顺序提供另一种简化模型 | + +模型需要分别定义基线参数和动态状态,例如固定 `U` 与动态释放变量 `u`,以及固定最大电导 +与动态受体数量。长期变化也可以由内部状态表达,不需要把所有响应变化都回写到 Connection weight。 + +## 延伸方向 + +以下是候选研究方向,不是上述版本已有 API 的清单,也不承诺首批全部实现。 + +| 方向 | 输入、状态及修改对象 | 建议归属与新增需求 | +| --- | --- | --- | +| Hebb / [Oja][oja] / [BCM][bcm] | pre/post 活动;相关性、活动平均或滑动阈值;修改 weight | Connection;需要定义 rate 或 spike 到活动估计的方式。本次源码核查未找到独立同名实现 | +| [突触缩放][scaling]与跨连接归一化 | post 活动或所选输入组统计;慢尺度状态;缩放各行 weight | 抽象规则放 Connection;跨行统计必须显式定义分组、归约和信号 owner,不能由某行任意修改其他行 | +| 一般三因子规则 | pre/post 活动形成 eligibility,奖励或教学信号调制更新 | Connection;将多巴胺实例延伸到显式调制信号,明确广播范围 | +| [钙驱动 LTP/LTD][calcium] | 活动引起的局部钙变化、阈值及效能状态 | 抽象钙 trace 只驱动 weight 时放 Connection;局部钙与释放、受体等耦合时由 Syn 表达 | +| 受体插入、移除与长期释放变化 | 局部活动或生化信号;受体数量、可释放资源或利用率的慢变量 | Syn 的模型延伸;给出具体方程后再确定依赖的 Ion/Cell 信号 | +| [BTSP][btsp] | pre 活动历史、树突平台电位相关教学信号;秒级历史或资格迹 | 更新 weight 的抽象规则放 Connection;平台电位由 Cell 产生,需指定树突位置和信号定义 | + +这些方向说明信号不能只抽象成两个 spike 布尔值。与此同时,读取钙或电压并不自动要求 +Syn 拥有完整的钙或电压模型:要区分规则自己维护的现象学 trace、Cell/Ion 提供的物理观测量, +以及 Syn 内部生化状态。若显式模拟受体变化来实现突触缩放,其表达也应归入第二类。 + +## 模型数值例子 + +以下配置用于手工核对模型含义,不定义 BrainCell 的默认规则、公共 API 或更新时序。 + +### 成对 STDP + +以成对 STDP 为最小例子:设 pre 在 0 ms 发放,post 在 10 ms 发放,无其他事件,初始 trace 为零, +本例忽略传输延迟,使用示意性的加性增强分支: + +```text +Delta w = A_plus * exp(-(t_post - t_pre) / tau_plus) +``` + +取 `A_plus = 0.01 uS`、`tau_plus = 20 ms`,则 `Delta w ≈ 0.00607 uS`。 +这些数值和配对条件仅用于解释模型,不是默认规则;权重尚未触及边界。Synapse 的电导衰减方程 +不因此改变,后续事件的输入幅度由变化后的 weight 决定。 + +### 短时抑制 + +以资源消耗解释短时抑制,示意模型中的 `x` 是可用资源比例,`U` 是每次事件利用的比例, +二者无量纲: + +```text +between events: dx/dt = (1 - x) / tau_rec +on one event: Delta g = w * U * x_before + x_after = x_before * (1 - U) +``` + +`tau_rec` 是恢复时间,`w` 和 `Delta g` 的单位为电导。固定 `w = 1 nS`、`U = 0.5`, +初始 `x = 1`;忽略两次紧邻但先后发生的事件之间的恢复,电导增量依次为 `0.5 nS` 与 +`0.25 nS`。变化来自 Syn 的资源消耗,Connection weight 保持不变;长时间休息后资源恢复。 +这是逐事件“先释放、后消耗”的说明模型,同一步事件的表示方式仍需另外确定。 + +同样设每次事件利用一半资源、两次事件间忽略恢复:A、B 各自拥有初始 `x = 1` 的资源池时, +先后激活的释放因子都是 0.5;若共用一个池,A 消耗后 B 的释放因子只有 0.25。 +该对比用于核对[提案中的状态共享语义](../proposals/connection-plasticity.md#两类的组合与共享)。 + +[song]: https://brainpy.readthedocs.io/apis/generated/brainpy.dyn.STDP_Song2000.html +[pair]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_synapse.html +[power]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_pl_synapse_hom.html +[jonke]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.jonke_synapse.html +[nn-pre]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_nn_pre_centered_synapse.html +[nn-restr]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_nn_restr_synapse.html +[nn-symm]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_nn_symm_synapse.html +[triplet]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_triplet_synapse.html +[clopath]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.clopath_synapse.html +[dopamine]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_dopamine_synapse.html +[vogels]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.vogels_sprekeler_synapse.html +[urbanczik]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.urbanczik_synapse.html +[pair-hom]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_synapse_hom.html +[hardware]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.stdp_facetshw_synapse_hom.html +[bp-std]: https://brainpy.readthedocs.io/apis/generated/brainpy.dyn.STD.html +[bs-std]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.STD.html +[bp-stp]: https://brainpy.readthedocs.io/apis/generated/brainpy.dyn.STP.html +[bs-stp]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.STP.html +[tsodyks]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.tsodyks_synapse.html +[tsodyks2]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.tsodyks2_synapse.html +[tsodyks-hom]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.tsodyks_synapse_hom.html +[quantal]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.quantal_stp_synapse.html +[ht]: https://brainx.chaobrain.com/brainpy-state/apis/generated/brainpy.state.ht_synapse.html +[oja]: https://pubmed.ncbi.nlm.nih.gov/7153672/ +[bcm]: https://pubmed.ncbi.nlm.nih.gov/7054394/ +[scaling]: https://pubmed.ncbi.nlm.nih.gov/9495341/ +[calcium]: https://pubmed.ncbi.nlm.nih.gov/22357758/ +[btsp]: https://pubmed.ncbi.nlm.nih.gov/28883072/ diff --git a/docs/design/network/references/platform-survey-2026-06.md b/docs/design/network/references/platform-survey-2026-06.md index f723c50a..00b486cc 100644 --- a/docs/design/network/references/platform-survey-2026-06.md +++ b/docs/design/network/references/platform-survey-2026-06.md @@ -1,8 +1,11 @@ # Network / Synapse 平台调研 +关联:[Network TODO](../TODO.md)、[当前架构](../current/architecture.md)、[运行时扩展](../proposals/runtime-extensions.md)。 +采用状态:作为已有架构取舍和后续扩展的比较背景;各平台的能力不能直接推断为 BrainCell 已支持。 + > **Historical, non-normative reference (2026-06).** 本文记录当时对 > NEURON/CoreNEURON、Arbor、Jaxley 和 brainevent 的调研。文中的 BrainCell 建议已经被 -> [Network Builder API](../api.md) 与 [内部架构规范](../architecture.md) 部分替代;实现和 +> [Network Builder API](../current/api.md) 与 [内部架构规范](../current/architecture.md) 部分替代;实现和 > 评审不得把本文当作当前规范。 本文聚焦四条相关路线: diff --git a/docs/design/optim/TODO.md b/docs/design/optim/TODO.md new file mode 100644 index 00000000..3baf8a0b --- /dev/null +++ b/docs/design/optim/TODO.md @@ -0,0 +1,92 @@ +# Optimization TODO + +## 当前需要推进的事项 + +参数学习的协作入口。[全局 TODO](../TODO.md) 管理宏观依赖,[Design 规范](../AGENTS.md) 定义文档分工和状态。 + +| 事项 | 状态 | 下一步或待决定问题 | 文档 | +| --- | --- | --- | --- | +| 可塑性参数训练与 weight_initial | 讨论中 | 在 Network 运行时契约基础上确定参数绑定、状态初值与梯度验收 | [训练提案](proposals/connection-plasticity.md)、[Network 可塑性](../network/proposals/connection-plasticity.md) | +| plateau、SGDR、perturb 自动恢复 | 讨论中 | 确定 effective-LR、恢复状态和比较协议 | [训练恢复](proposals/training-recovery.md) | +| Cell 初值与 cable 参数 owner | 待讨论 | 明确构造转换、缓存及初始化依赖 | [后续方向](proposals/roadmap.md#cell-初值与-cable-参数) | +| 公共训练协议与稳定 grouping | 待讨论 | 确定复用边界、持久化和 ownership | [后续方向](proposals/roadmap.md#公共训练协议与分组) | +| 多 CV、多 population、GPU 与 checkpoint 验证 | 待讨论 | 确定组合矩阵和公平测量协议;已有单项结果不代替组合验证 | [验证缺口](proposals/roadmap.md#验证缺口) | +| rollout 内更新参数 | 待讨论 | 定义梯度对应的参数历史 | [研究方向](proposals/roadmap.md#rollout-内更新参数) | + +## 阅读入口 + +optim 是参数学习的问题域;公共参数映射位于 `braincell.trainable`,优化算法由 BrainTools 等提供。 + +| 想知道什么 | 阅读位置 | +| --- | --- | +| Channel、Ion、Synapse 等到底能学什么 | [参数支持度](current/parameter-support.md) | +| 如何注册、分组、共享、读写参数 | [公共 API](current/api.md) | +| 参数依赖、reset、materialization、事件和队列如何连接 | [架构](current/architecture.md) | +| 已实现的 RTRL、训练、诊断和 OED 实验如何调用 | [实验工作流](current/experimental-workflows.md) | +| 还准备实现什么、缺什么验证 | [路线图](proposals/roadmap.md) | +| 可塑性与恢复策略的候选方案 | [Network 可塑性](../network/proposals/connection-plasticity.md)、[可塑性训练](proposals/connection-plasticity.md)、[训练恢复](proposals/training-recovery.md) | +| 精度、训练收敛、耗时、内存的已有证据 | [结果导航](#实验结果) | +| 原理、方法选择和外部研究 | [参考导航](#理论与方法) | + +## 状态口径 + +Current 描述工作区实现,实验代码位于 examples/experimental。 +提交进度沿用 [项目进度](../TODO.md#进度口径);Synapse/Network 扩展已随 `5f90f69` 提交, +CPU 回归及示例验收见 [验证记录](current/results/synapse-network-learning.md#提交验收)。 + +| 标记 | 含义 | +| --- | --- | +| 公共接口已实现 | 当前 braincell 代码存在接口;支持边界和测试证据另列 | +| 实验代码已实现 | root examples/experimental 中可调用,不是 BrainCell 公共 API 承诺 | +| 待设计 | 尚需决定的候选方向 | +| 暂不支持 | 当前接口拒绝或尚未接入 | + +## 能力地图 + +| 能力 | 当前状态 | 说明 | +| --- | --- | --- | +| Channel、Ion、Synapse 参数 source 与映射 | 公共接口已实现 | 签名发现;不是科学参数白名单 | +| Connection weight、检测器 threshold | 公共接口已实现 | threshold 的事件路径使用代理梯度 | +| Cell/Network roots 管理与聚合 | 公共接口已实现 | Network 按对象身份去重,不复制根 | +| direct/scale/parameterized、分组、单位和 reset | 公共接口已实现 | 统一 binding 主链 | +| 完整 RTRL、BPTT、敏感度诊断 | 实验代码已实现 | 固定参数 rollout;不能切断跨 Cell 敏感度 | +| 数据、loss、分阶段拟合、搜索、archive、OED | 实验代码已实现 | 不发布 Dataset/Trainer/checkpoint 等公共训练类型 | +| 可塑性规则及动态 weight 初始化 | 待设计 | 与现有静态 weight 参数区分 | +| plateau/restart/perturb 自动恢复 | 待设计 | 已实现观测与历史 archive 不等于已实现 controller | +| Connection delay 可训、Cell.V_init/cable trainable owner | 暂不支持 | 静态配置能力不等于参数训练能力 | + +## 实验结果 + +结果按实验主题保存,同一实验的精度、训练和性能不拆到不同指标文件。实验条件不同的 +数字不能直接比较;每页分别记录来源、测量口径、复现入口和未验证范围。 + +- [参数学习示例](current/results/parameter-learning.md):Channel、Ion、单突触单参数教学拟合。 +- [突触与网络](current/results/synapse-network-learning.md):事件、自连接、双向 population、近期 CPU 计时。 +- [BPTT/RTRL scaling](current/results/bptt-rtrl-scaling.md):历史多 CV、CPU、A100 和 Adam 一致性。 +- [Batch 与吞吐](current/results/batch-size-and-throughput.md):batch、candidate lanes、GPU 容量和训练质量。 +- [拟合与可辨识性](current/results/fitting-and-identifiability.md):multi-start、优化器对照、诊断、FIM 和 ensemble。 + +“接口可调用”“梯度存在”“拟合成功”“参数唯一可辨识”“性能占优”是不同结论。 +多 CV 与多 population 分别已有证据,不合并为其任意组合已经实测。 + +## 理论与方法 + +- [BPTT 到 RTRL](references/bptt-to-rtrl-neuron-derivation.md):链式法则、初始化、online 与 e-prop。 +- [Solver 梯度](references/staggered-solver-gradient-analysis.md):DHS、机制更新和离散程序导数。 +- [电压与 spike 拟合](references/voltage-and-spike-parameter-fitting.md):loss、评价与 curriculum 方法。 +- [训练诊断](references/modular-training-diagnostics.md):诊断解释、archive 和 region 方法。 +- [刺激与可辨识性](references/stimulus-design-and-identifiability.md):方法原理和数据隔离。 +- [Jaxley 参数模型](references/jaxley-parameter-model.md):外部实现取舍。 + +## 文档对齐 + +相关教程: +[Channel](../../../examples/multi_compartment/channel_learning.ipynb)、 +[Ion](../../../examples/multi_compartment/ion_learning.ipynb)、 +[Synapse/Network](../../../examples/multi_compartment/synapse_learning.ipynb)。 +提交前检查遵循 [仓库约定](../../../AGENTS.md#design-code-and-examples)。 + +已知 JAX 0.10.1 Ion 精度切换/执行上下文问题,以及覆盖率 C tracer 的原生崩溃记录, +见 [验证限制](current/results/synapse-network-learning.md#验证与限制)。 + +旧 [P0 计划](../../specs/2026-08-29-trainable-parameter-implementation.md) 仅供历史追溯。 diff --git a/docs/design/optim/api.md b/docs/design/optim/current/api.md similarity index 68% rename from docs/design/optim/api.md rename to docs/design/optim/current/api.md index ef64fb24..3d17b9f1 100644 --- a/docs/design/optim/api.md +++ b/docs/design/optim/current/api.md @@ -1,15 +1,12 @@ # Trainable Parameter API -## 文档状态 +本文描述当前工作区的可训参数选择和映射接口。 +设计依据和内部数据流见 [Architecture](architecture.md),待实现方向见 +[Roadmap](../proposals/roadmap.md),更宽的模型优化能力边界见 +[Design overview](../TODO.md)。 -本文定义 BrainCell 首版可训参数选择和映射公共 API。设计依据和内部数据流见 -[Architecture](architecture.md),实现阶段和验收项见 -[Implementation plan](implementation-plan.md),更宽的模型优化能力边界见 -[Design overview](design-overview.md)。 - -当前 P0 范围为 multi-compartment `ChannelView` 上的 `IL`、`Na_HH1952` 和 -`K_HH1952`。Ion、Synapse、Connection、Network 聚合、 -数据、loss、搜索、history、checkpoint 和诊断接口不在本文预先占用公共名称。 +当前覆盖 multi-compartment Channel/Ion/Synapse 的构造签名参数、Connection weight、 +电压事件检测器 threshold,以及 Network 参数聚合。机制不维护可训参数白名单。 ## Quick Start @@ -17,8 +14,14 @@ import braincell import brainstate import braintools +import brainunit as u +from braincell.filter import AllRegion -na = cell.on(soma).channels["na"] +branch = braincell.Branch.from_lengths(lengths=[20.0] * u.um, radii=[3.0, 3.0] * u.um) +cell = braincell.Cell(braincell.Morphology.from_root(branch), cv_policy=braincell.CVPerBranch(1)) +cell.paint(AllRegion(), braincell.mech.Ion("SodiumFixed", E=50*u.mV)) +cell.paint(AllRegion(), braincell.mech.Channel("Na_HH1952", name="na", g_max=1.0*u.mS/u.cm**2)) +na = cell.channels["na"] na.trainable( g_max=braincell.trainable.scale( @@ -42,7 +45,7 @@ BrainTools 或用户代码提供。 ## Public Namespace -候选公开符号为: +当前 braincell.trainable.__all__ 导出: ```text braincell.trainable.parameter @@ -51,6 +54,7 @@ braincell.trainable.parameterized braincell.trainable.TrainableManager braincell.trainable.ParameterSet braincell.trainable.ParameterBinding +braincell.trainable.ParameterSource ``` Cell 公开: @@ -63,8 +67,23 @@ View 公开: ```text ChannelView.trainable(**fields) -> ChannelView +IonView.trainable(**fields) -> IonView +SynapseView.trainable(**fields) -> SynapseView +ConnectionView.trainable(weight=source) -> ConnectionView +``` + +Network 与检测器还提供以下入口(不同 owner 接受的字段见 [支持度](parameter-support.md)): + +```text +Network.trainables -> Network aggregation facade +Network.prepare_run(...) -> prepare fixed routing outside AD +Network.update() -> one differentiable step +Cell.event_outputs["spike"].trainable(threshold=source) +VoltageCrossingSource.trainable(threshold=source) ``` +Network 聚合门面不是新增公开导出类,具体类型名不加入 braincell.trainable.__all__。 + ## `View.trainable()` ```python @@ -94,24 +113,56 @@ na.trainable( - 只能在 `Cell.init_state()` 前调用; - View 必须非空,并且只选择一个逻辑 mechanism owner; -- target 必须由统一 schema 标记为连续、shape-preserving、trainable parameter; +- Channel/Ion/Synapse target 必须出现在其构造签名中;单位、形状和模型原有运算约束仍适用; - 同一逻辑 row/field 只能有一个 binding; - 多字段调用原子注册,失败时不留下部分 roots 或 bindings; -- 注册不会立即写 runtime,第一次写入发生在 `init_state()` 的 materialization 阶段; +- 注册不会立即写 runtime;初始化时分配物理缓冲并进行 materialization; - 普通 `set()` 已建立的当前值可以作为 direct initial 或 scale baseline。 -### P0 supported targets +### Channel 候选参数 -首条实现链覆盖: +候选项从 `__init__` 获取,包括继承和转发的已声明参数,而不是任意 `**kwargs`。 | Mechanism | Candidate fields | | --- | --- | | `IL` | `g_max`, `E` | -| `Na_HH1952` | continuous declared physical parameters | -| `K_HH1952` | continuous declared physical parameters | +| `Na_HH1952` / `K_HH1952` | `g_max`, `V_sh`, `temp`, `q10`, `temp_ref` 等签名参数 | +| 其他 Channel | 该类构造签名中的参数,不需要另加白名单 | -动态 concentration、gate state、derived Nernst potential、`valence`、solver、substeps 和 -topology 不可作为 P0 target。 +表中为典型数值参数,并非额外白名单。整数默认值不代表不可微,例如浓度指数 `n`; +布尔比较可能产生零梯度,Python 控制流或字符串配置则可能在转换、初始化或求导时 +自然报错。系统不承诺非零梯度,也不自动判断可辨识性。动态 gate state 和 +不在构造签名中的内部常数不因本次扩展而成为候选项。 + +省略的数值参数可从签名读取默认值并在初始化前 `.set()`。必填参数没有虚构默认值; +若签名没有数值默认值,必须显式提供初值或覆盖值。通过 View 补值时,需覆盖该 +运行时布局的所有有效行,不能猜测未选行的默认值。 + +温度派生的 sodium `phi` 在属性访问时重算;显式独立 `phi` 参数保持独立。 + +### Ion 候选参数与初始状态 + +Ion 使用同一参数 source、分组、区域选择和共享 root 机制,例如: + +```python +import brainunit as u + +cell.ions["pool"].trainable( + Ci_initializer=braincell.trainable.parameter(0.001 * u.mM, group_by="all"), + tau=braincell.trainable.scale(name="clearance"), +) +``` + +`Ci_initializer` 是初始参数,`Ci(t)` 是动态状态。loss 内先 `reset_state()` 再模拟; +reset 读取当前训练初值,不重置 optimizer root。固定 Ion 的 `Ci` 则是普通物理参数。 +`Co/Ci/valence` 的 None 默认值按模型默认值解析。复杂 Ion 的默认初值在 reset 时 +根据当前参数推导,显式初值覆盖仍然独立,未选区域继续使用模型默认关系。 + +固定 `E` 不因浓度改变而更新;InitNernst 在 init/reset/参数同步时刷新存储电位, +DynamicNernst/KineticIon 在读取时计算电位。`species_initializers` 的既有覆盖功能保留, +但不新增该字典内部字段的训练路径;已有具名 `BC_initializer` 等参数可直接选择。 + +示例见 [Ion learning](../../../../examples/multi_compartment/ion_learning.ipynb)。 ## `parameter()` @@ -168,7 +219,7 @@ na.trainable( ## Grouping -P0 接受以下 `group_by`: +当前接受以下 `group_by`: | Value | Root identity | Meaning | | --- | --- | --- | @@ -187,7 +238,7 @@ P0 接受以下 `group_by`: | `all` | 1 | 40 | grouping 根据稳定 row metadata 建立,不要求 selection 可以 reshape 成规则矩阵。branch、 -region、tuple keys 和 callable grouper 不属于 P0。 +region、tuple keys 和 callable grouper 当前暂不支持。 ## `scale()` @@ -285,7 +336,7 @@ na.trainable( | nested `parameter(...)` | 创建带显式 grouping 的 root,并按当前 row gather。 | | Quantity/array/scalar | 作为 fixed argument。 | -P0 要求稳定具名参数。positional-only 参数、`*args` 和不能稳定命名的匿名 varargs 被拒绝。 +当前要求稳定具名参数。positional-only 参数、`*args` 和不能稳定命名的匿名 varargs 被拒绝。 同一个 `nn.Param` 被多个 source 引用时按对象身份去重。 函数输出必须与 target physical unit 兼容,且 shape 在 JIT trace 内固定。函数及其参数 @@ -403,7 +454,7 @@ parameters.set_physical_values(candidate) parameters.set_optimizer_values(candidate_z) ``` -- P0 要求完整 tree; +- 当前要求完整 tree; - 写入前验证 keys、shape、dtype、单位和 finite 状态; - 任一 leaf 失败时不写入任何 root; - physical setter 使用 `Param.set_value()` 或等价正规路径; @@ -427,7 +478,7 @@ unit baseline optional ``` -用户不直接构造 binding;`View.trainable()` 根据 source 创建。P0 不提供 binding inverse、 +用户不直接构造 binding;`View.trainable()` 根据 source 创建。当前不提供 binding inverse、 reduce 或初始化后删除/替换 ownership。 ## Lifecycle @@ -450,9 +501,9 @@ cell.trainables.materialize() `reset_state()` 不回滚 roots 或 scale baseline。完整 `Cell.reset()` 清除 runtime;旧 runtime buffer 引用随之失效。 -## Deferred Owners +## Synapse, Connection, Network -后续 owner 复用同一 source 和 manager,不创建新 namespace: +这些 owner 复用同一 source 和 manager,不创建新 namespace: ```python synapses.trainable( @@ -464,15 +515,26 @@ connections.trainable( ) ``` -Ion/Synapse/Connection binding 和 Network 聚合不属于 P0,只有在对应 runtime buffer 能 -保持固定 shape、单位和 JAX trace 后才进入正式 API。 +`cell.event_outputs["spike"].trainable(threshold=...)` 绑定该位置的 Cell.V_th;显式 +`VoltageCrossingSource(..., threshold=...)` 则有独立阈值。绑定仍须在初始化前声明。 +Synapse 和 Connection 的 row 是逻辑突触/接触 ID,同一 CV 上的多个对象不会误合并。 +`group_by="cv"` 则明确共享该 CV 的参数;其余 grouping 和 Channel/Ion 相同。 + +`network.trainables.parameters()` 聚合原始根,以 `population.local_name` 命名, +共享同一 nn.Param 时按对象身份去重。先在 host 调用 `network.prepare_run(dt=...)`, +再在 `brainstate.transform.for_loop` 中调用 `network.update()`,即可对完整网络做 BPTT。 +`run()` 保留记录和事件表接口,其 host 结果转换不作为网络可微 rollout 接口。 + +Connection delay 显式拒绝训练;塑性规则及 weight_initial 尚未实现。 +事件导数见 [架构](architecture.md#event-derivatives),实验版 RTRL 见 +[实验工作流](experimental-workflows.md),未实现的塑性设计见 [proposal](../proposals/connection-plasticity.md)。 ## Errors -P0 必须在明确边界拒绝: +当前在明确边界拒绝: - 空 selection 或一个 View 跨多个逻辑 owners; -- 未知字段、state、derived、static、整数、布尔或 topology target; +- 不在 Channel/Ion/Synapse 构造签名中的 target,或其他 owner 不支持的 target; - 初始化后新增 binding; - 重叠 row/field ownership; - root name 冲突或共享对象使用冲突名称; @@ -483,11 +545,14 @@ P0 必须在明确边界拒绝: - ParameterSet tree 缺 key、多 key、shape/dtype/单位不匹配; - 任意可能造成部分 root 或部分 target 写入的失败。 +不额外拒绝整数或布尔候选,也不把零梯度视为错误。实际运算仍可能因静态控制流、 +不合法类型等原因失败,这与签名候选发现是两件事。 + ## References -- [Design overview](design-overview.md) +- [Design overview](../TODO.md) - [Architecture](architecture.md) -- [Implementation plan](implementation-plan.md) +- [Roadmap](../proposals/roadmap.md) - [BrainState Parameter Model](https://brainstate.readthedocs.io/concepts/the_parameter_model.html) - [BrainState Transformation Semantics](https://brainstate.readthedocs.io/concepts/transformation_semantics.html) - [BrainState `transform.grad`](https://brainstate.readthedocs.io/apis/generated/brainstate.transform.grad.html) diff --git a/docs/design/optim/architecture.md b/docs/design/optim/current/architecture.md similarity index 52% rename from docs/design/optim/architecture.md rename to docs/design/optim/current/architecture.md index 6404da37..aac2e015 100644 --- a/docs/design/optim/architecture.md +++ b/docs/design/optim/current/architecture.md @@ -12,7 +12,8 @@ - BrainState graph 自动发现 roots; - gradient-based 和 gradient-free 方法写入同一个 ParameterSet。 -公共调用合同见 [API](api.md),落地顺序见 [Implementation plan](implementation-plan.md)。 +参数映射属于公共接口;完整 RTRL 等梯度引擎属于 examples/experimental 中的实验实现。 +公共调用合同见 [API](api.md),落地顺序见 [Roadmap](../proposals/roadmap.md)。 ## 三层参数模型 @@ -41,7 +42,7 @@ tree。`brainstate.graph.states(cell, ParamState)` 因而看到 `q/theta/a/b`, ## 所有权与隔离 -实现集中在独立的 `braincell.trainable` 模块。P0 由 Cell 持有 `trainables` 门面: +实现集中在独立的 `braincell.trainable` 模块。由 Cell 持有 `trainables` 门面: ```text Cell.trainables -> TrainableManager @@ -50,10 +51,10 @@ Cell.trainables -> TrainableManager ParameterSet construction materialization -Future: Network.trainables -> aggregate TrainableManager +Network.trainables -> aggregation facade over Cell managers ``` -Cell manager 是实际 owner,并作为 Cell graph 的子 module 保存 `nn.Param`。未来 Network +Cell manager 是实际 owner,并作为 Cell graph 的子 module 保存 `nn.Param`。Network manager 只形成跨 Cell 聚合 view,不复制 roots。 这使核心 Cell 的侵入保持在两个边界: @@ -75,38 +76,45 @@ View 只把 target selection 和 source 交给目标 Cell manager,不保存 op | ConnectionView | target Cell ConnectionStore/runtime delivery | target Cell | `NetworkConnections` 只聚合查询 Cell-owned ConnectionView,没有第二份 connection columns。 -因此未来训练 connection weight 时,`ConnectionView.trainable(weight=...)` 仍注册到目标 -Cell manager。未来全网优化仍应聚合 Cell managers,不在 Connection 或 Population 上增加 +因此训练 connection weight 时,`ConnectionView.trainable(weight=...)` 仍注册到目标 +Cell manager。全网优化聚合 Cell managers,不在 Connection 或 Population 上增加 独立 manager。 -delay 影响离散调度和 queue schema,不作为首批连续 trainable field。weight 只有在 -runtime delivery buffer 能保持 JAX trace 和固定 shape 后才接入。 +delay 影响离散调度和 queue schema,显式拒绝训练。weight 使用独立物理 State, +delivery block 在运行时读取,不在 lowering 或编译外冻结。 ## Parameter Schema -Channel、Ion 和后续 Synapse 统一使用显式字段角色: +Channel、Ion 和 Synapse 的候选字段来自实际 `__init__` 签名,包括转发的父类签名,不再维护 +`ParameterSpec` 科学分类白名单。内部仍生成默认值、单位验证所需的 metadata;数值 +缓冲分配与训练资格是不同问题,非数值配置不因出现在签名中就被强制数组化。 -| Role | 含义 | 例子 | 可训练 | -| --- | --- | --- | --- | -| `parameter` | 连续、shape-preserving、进入 forward | `g_max`, `E`, `tau`, `V_sh` | 候选 | -| `state` | 随时间积分或由 reset 初始化 | gates, dynamic concentration | 否 | -| `derived` | 根据 parameter/state 计算 | Nernst-derived `E`, factor | 否 | -| `static` | 决定结构、shape 或调度 | solver, substeps, valence, topology | 否 | - -P0 采用显式 `ParameterSpec` 白名单,不根据构造函数签名或数值 dtype 自动开放。spec -只保存 unit/default prototype 和 validator;是否开放训练属于 owner binding 策略,不污染 -物理 schema。Synapse 现有 spec 迁移为公共 schema,但首批 trainable owner 只有三个 -Channel 类。 +只有签名中的参数可选择;内部 gate state、硬编码常数不会自动暴露。选择后可能 +获得非零梯度、合法零梯度,或者原有数值转换、形状和静态控制流错误。dtype 不被 +当作可学习性的判据。Synapse 的状态 schema、事件输入和模型有效性验证仍与参数发现分开。 ## Runtime 参数列 -schema Channel 的物理参数由非可训 `RuntimeParameterState(LongTermState)` 保存。状态内部按 +Channel 的数值物理参数由非可训 `RuntimeParameterState(LongTermState)` 保存。状态内部按 `uniform`、`population`、`cv` 或 `row` 保存最小值,Channel 读取时才广播为执行矩形;旧的 `get_state()` 和 buffer inspection 仍获得矩形兼容视图。point mask 在读取 conductance 时 应用,因此 scalar `g_max` 不必为未 paint 点分配完整数组。 首次 materialization 冻结参数列的轴语义。optimizer 之后只改变同 shape 的 state value, 不会因数值从相同变为不同而触发第二步 JIT 重编译。 +压缩轴同时考虑 binding 的分组和区域所有权,不能仅因初始值相同而合并独立区域。 +浮点训练值写入整数默认缓冲时提升 dtype,避免截断数值并切断梯度。 + +Ion 复用相同的紧凑布局参数列;同名 Ion 跨布局合并到持久的完整矩形参数状态, +同步使用 JAX scatter,不把 tracer 转回 NumPy。自动 root 名包含 category,避免 +同名 Channel/Ion 字段冲突。参数精度服从 JAX/BrainState 的配置,不再隐式依赖 +旧 Ion NumPy 合并路径的 float64。 + +初始参数和动态物种分别存储。初值覆盖使用可跟踪的区域 mask,未覆盖点在 reset +时求模型默认值;完全显式的数值初值仍可作为 Quantity 读取。缓冲物平衡初值和 +caiBase/caliBase 默认关系保存计算方式,不保存构造时的结果。 +InitNernst 的存储电位使用 LongTermState,保持原有刷新时机并避免 JIT 缓存旧值。 +Nannuli 派生的 vrat/dsqvol 动态求值,反应图与单次导数求值内的 factor 复用保留。 ## TrainableManager @@ -152,12 +160,12 @@ root transform inverse 完成;任意 latent function 不存在通用的 target - optimizer 只更新 root; - `materialize()` 根据当前 root 刷新 target。 -已由 binding 占有的 target 不允许普通 set 静默替换关系。最终实现应明确拒绝,或要求 -用户先删除 binding;P0 不提供初始化后改变 ownership。 +已由 binding 占有的 target 不允许普通 set 静默替换关系,当前实现明确拒绝。 +当前没有初始化后删除/替换 ownership 的公共接口。 ## Grouping 与广播 -View selection 展开为稳定 logical rows。P0 的 group key 为: +View selection 展开为稳定 logical rows。当前 group key 为: | Group | Root identity | | --- | --- | @@ -215,13 +223,13 @@ direct root 保留 target 的物理单位;scale factor 通常无量纲;custo 进入训练不需要整体去单位。只有 loss 在选择 canonical unit 或观测尺度后形成无量纲 scalar。 -多个 runtime values 依赖同一 latent 时,逐 target bounds 会形成耦合约束。P0 不自动求 +多个 runtime values 依赖同一 latent 时,逐 target bounds 会形成耦合约束。当前不自动求 约束交集;用户直接约束 latent,或构造始终满足条件的 parameterized function。 ## Materialization 生命周期 现有 density callable 在 lowering 时求值一次,不能承载 optimizer 持续更新的 latent。 -TrainableManager 必须提供新的 JAX-traceable materialization 路径: +TrainableManager 提供 JAX-traceable materialization 路径: 1. `init_state()`:runtime buffer 建立后、mechanism state 初始化前; 2. `reset_state()`:先物化,再重置 gates 和 ion dynamic state; @@ -234,12 +242,89 @@ materialization 必须位于 differentiated trace 内。若参数影响 reset `reset_state()` 不改变 roots 或 frozen baseline。完整 `Cell.reset()` 清除 runtime;重新 初始化必须从仍有效的声明和 manager metadata 重建 binding target,旧 runtime 引用不可复用。 +## Ownership and Initialization + +Synapse constructors declare their physical parameters. `parameter_info()` derives +default and unit metadata from those signatures; it is not a handwritten +trainability whitelist. The state schema and event-input contract remain separate. +Custom classes migrate their former `parameters` dictionary to an explicit +`__init__`, calling `super().__init__(size, name)` and `_init_parameters(...)`. +Legacy nonempty dictionaries raise a migration error instead of silently losing fields. + +Synapse rows are logical synapse IDs; Connection rows are contact IDs. Colocated +objects remain distinct under row grouping. CV grouping deliberately ties them. +The common manager owns parameter roots, transformations, grouping, registration +rollback, and runtime materialization. Runtime physical parameter States are not +additional optimizer roots. Shared roots in Network are deduplicated by identity. + +Units and source shape are checked during registration. Synapse registration also +validates jointly proposed values (`tau > 0`, `0 < tau1 < tau2`). Numerical constraints +are not Python checks inside the differentiated step: users must select parameter +transforms that preserve valid domains during optimization. Exp2Syn's normalization +factor is computed from the current time constants on every event application. + +Reset clears dynamic states and event queues, then uses the current trainable +values. It does not restore the optimizer roots to their original values. +Ordinary setters reject binding-owned fields. Delay remains static and trainable +delay raises `NotImplementedError`. Plasticity and `weight_initial` are not added. + +## Event Derivatives + +Rising: `last < threshold <= next`. Falling: `last > threshold >= next`. +Arriving at equality emits once; staying at or leaving equality does not emit. +Both use `S(sign * (next-threshold)/20mV) * (1-S(sign * (last-threshold)/20mV))`, +where sign is +1 for rising and -1 for falling. `S` has a hard Heaviside forward +value (one at zero) and a surrogate backward derivative. A custom `spk_fun` must +preserve that forward contract. The default is the owning Cell's `spk_fun`. + +An omitted detector threshold uses Cell.V_th; an explicit threshold has its own +State. Default spike and inherited-threshold views detect overlapping ownership. +Floating event values must survive weight multiplication, sparse delivery and +queues. Converting them to bool/integer would sever surrogate derivatives. + +The brainevent delivery adapter keeps `coomv` for the forward result and supplies +its exact bilinear JVP with JAX scatter: `d(W z) = dW z + W dz`. This also preserves +units and repeated destinations. It avoids a dependency batching failure when +full RTRL vmaps weight tangents; it does not introduce another surrogate or +change the detector's event semantics. Reverse mode transposes the same JVP. + +Fixed NetStim input does not require an event-time surrogate to learn synapse tau +or connection weight. Gradients into a presynaptic Cell or detector threshold do +require the voltage surrogate. A zero gradient is valid: no input, an insensitive +loss, or a surrogate with no support at the visited voltages can cause it. Hard +event timing finite differences are not a reference for surrogate derivatives. + +### NEURON Boundary Reference + +NEURON 9.0.1 [APCount](https://github.com/neuronsimulator/nrn/blob/9.0.1/src/nrnoc/apcount.mod) +uses `v >= thresh` and rearms below threshold. Its initialization can count an +initially suprathreshold value, unlike BrainCell's reset-to-zero spike state. +[Fixed-step NetCon detection](https://github.com/neuronsimulator/nrn/blob/9.0.1/src/nrncvode/netcvode.cpp) +uses a strictly positive threshold condition. CVode may interpolate event time. +Thus the arrival-equality convention is not a claim of complete NEURON equivalence. + +## Differentiable Network Execution + +Call `Network.prepare_run(dt=..., event_backend=...)` outside AD to fix routing, +delay quantization and queue shapes. `Network.update()` reuses the existing run +loop's single-step ordering and returns floating spikes keyed by Cell population. +Use `brainstate.transform.for_loop` or `scan`; read continuous observations from +Cell states. `Network.run()` remains the host recording/EventSeries interface. + +The existing experimental full-state RTRL accepts the same prepared target. Its +carry includes every traced Cell, synapse and queue state, including cross-Cell +sensitivities. Do not replace it by independent per-Cell sensitivities. Comparisons +with BPTT use the identical surrogate and parameters fixed throughout a rollout. +The sensitivity carry has fixed shape as duration grows, but cost still scales +with state size times parameter count. Per-time-step optimizer updates and a new +online-learning algorithm are outside this change. + ## 与 Jaxley 的对比 -完整实现调研见 [Jaxley Parameter Model](references/jaxley-parameter-model.md)。本架构只冻结 +完整实现调研见 [Jaxley Parameter Model](../references/jaxley-parameter-model.md)。本架构只冻结 直接影响 BrainCell 的结论: -- 使用显式 parameter/state schema,而不是从构造函数或 dtype 猜测; +- 保留显式 gate/Markov 状态声明;Channel 参数由构造签名发现,不用 dtype 判断可微性; - View selection 和稳定 row metadata 决定 sharing; - 低维 root 通过 gather/scatter 映射到 dense runtime values; - BrainCell root 由 `nn.Param` graph state 持有,不要求显式 `simulate(params)`; diff --git a/docs/design/optim/current/experimental-workflows.md b/docs/design/optim/current/experimental-workflows.md new file mode 100644 index 00000000..0c53dc5d --- /dev/null +++ b/docs/design/optim/current/experimental-workflows.md @@ -0,0 +1,73 @@ +# Experimental Optimization Workflows + +## 边界与入口 + +以下代码已存在于 root examples/experimental,但不是 braincell 公共训练 API。 +公共参数声明见 [API](api.md)。本页定义实验工具的能力与调用入口;数值结果见 +[结果导航](../TODO.md#实验结果),未实现的恢复动作见 [proposal](../proposals/training-recovery.md)。 + +| 工具 | 当前实现入口 | 已有能力 | +| --- | --- | --- | +| 梯度引擎 | [optim/gradients.py](../../../../examples/experimental/optim/gradients.py) | additive rollout、trajectory objective、BPTT/full RTRL、诊断 | +| 参数拟合 | [training.py](../../../../examples/experimental/optim_parameter_fitting/training.py) | 模型/数据/loss 组合、gradient stage、候选交接 | +| 梯度外搜索 | [search.py](../../../../examples/experimental/optim_parameter_fitting/search.py) | 有界候选搜索,与 physical CandidateSet 交接 | +| 优化器适配 | [optimizers.py](../../../../examples/experimental/optim_parameter_fitting/optimizers.py) | Adam、Rprop、Optax Rprop、SGD/momentum/Nesterov stage | +| 诊断与归档 | [diagnostics.py](../../../../examples/experimental/optim_parameter_fitting/diagnostics.py) | 状态/更新观测、历史总结、训练后 best archive | +| OED | [robust_oed.py](../../../../examples/experimental/optim_stimulus_design/robust_oed.py) | observation sensitivity、sampled-prior FIM、候选刺激排序 | +| 全局可辨识性探查 | [global_ensemble.py](../../../../examples/experimental/optim_stimulus_design/global_ensemble.py) | forward-only 候选池,不等于完整 posterior | + +## 梯度与 Network + +build_rollout_value_and_grad(target, step=..., method="bptt" 或 "rtrl") 返回实验引擎。 +step 接收一个时间片、推进一次模型并返回 scalar additive loss;prepare 追踪参数相关 +初始化和完整一步。调用返回逐步 losses、总 loss 和按稳定 root 名组织的 gradients。 +非逐步相加目标使用同模块的 trajectory engine;不要把两者的 loss 合同混用。 + +Network 在求导外 prepare_run 固定路由和队列,step 内 update。引擎在每轮开始物化当前 +root 并 reset,参数在该 rollout 内固定。完整 RTRL 的 carry 包括全网已捕获状态及其 +parameter-major sensitivities;跨 CV/Cell 或 queue 的依赖不能省略。 +常规路径不输出敏感度历史,diagnose(at=...) 是单独编译的诊断路径。 +初值、loss 直接依赖参数以及总/前缀梯度的区别见 [理论](../references/bptt-to-rtrl-neuron-derivation.md)。 + +正常 RTRL 仍返回逐步 loss,输入和输出可随 T 增长;不随 T 增长的是递归敏感度 carry, +不是整个 Python 进程或所有输出。详细测量口径见 [网络结果](results/synapse-network-learning.md)。 + +## 分阶段训练 + +实验配置把模型、数据、loss、梯度引擎、优化器和评价分开。run_pipeline 在 stage 之间 +用 physical CandidateSet 交接;梯度优化在 root 的 optimizer 坐标工作,非梯度搜索可用 +有界归一化坐标。非梯度阶段改变候选后重新建立后续优化器状态,不复用过时 moments。 + +训练协议、validation 和 final-only test 分开。固定目标预处理、mask、normalizer 和 split, +不要让评价修改训练目标或把观察过的 holdout 继续称为未使用的 test。 +这些实验类型不是公共 Dataset、Trainer、Search 或 Checkpoint API。 +具体配置入口见 [参数拟合目录](../../../../examples/experimental/optim_parameter_fitting/README.md)。 + +## 诊断与历史 + +当前 diagnostics 实现 capture_state、capture_update、finalize_history、 +extract_best_archives、summarize_history、save_artifacts 和 plot_diagnostics。 +输入 prediction 为 [time,start,probe],target 为 [time,probe];协议与单位通过 metadata 表达。 +状态历史有 N+1 个位置(含初值和最终 endpoint),更新历史有 N 个位置。 +update 前 loss 必须与 update 前参数配对,不能和更新后参数错位。 + +continuous-best 与 spike-feasible-best 分开提取;无 finite 或无可行项时使用 invalid 标记, +不能把连续 loss 最低当成 spike 成功。当前历史提取不回写训练参数;在线 archive/controller、 +restart/perturb 以及完整 resume 不是因此自动具备的能力。 +诊断分类是启发式,不证明局部最优或参数可辨识;详见 [诊断方法](../references/modular-training-diagnostics.md)。 + +## 刺激设计 + +robust OED 对 train candidates 的 observation sensitivity 累计 per-protocol FIM, +不必保存完整时间敏感度;global ensemble 用纯 forward 比较有限候选池。 +rank、condition 或候选排序改善不保证实际训练成功率提高,更不证明全局唯一性。 +方法、数据隔离与固定协议见 [刺激设计参考](../references/stimulus-design-and-identifiability.md), +历史结果见 [拟合与可辨识性](results/fitting-and-identifiability.md)。 + +## 验证入口 + +梯度 core 的共置测试在 [optim](../../../../examples/experimental/optim/README.md), +跨 Cell 事件验证在 [gradient correctness](../../../../examples/experimental/optim_gradient_correctness/README.md)。 +拟合、诊断和 OED 模块均有对应的 *_test.py;本次文档整理不重新执行这些实验。 +上述实际相关示例仅在 commit 前纳入本次提交的一致性检查,日常开发不要求同步维护 +docs/examples 或增加教程副本;检查范围见 [文档对齐](../TODO.md#文档对齐)。 diff --git a/docs/design/optim/current/parameter-support.md b/docs/design/optim/current/parameter-support.md new file mode 100644 index 00000000..5d6999d2 --- /dev/null +++ b/docs/design/optim/current/parameter-support.md @@ -0,0 +1,108 @@ +# Trainable Parameter Support + +## 口径与共同能力 + +公共入口见 [API](api.md), +实验梯度入口见 [实验工作流](experimental-workflows.md),实现与发布状态见 +[总览](../TODO.md#状态口径)。下面的参数是例子,不是新的可训白名单。 + +Channel、Ion、Synapse 通过构造签名发现候选参数,保留默认 get/set、单位、区域选择、 +row/population/cv/all 分组、共享根,以及 parameter/scale/parameterized。 +声明在初始化前完成。注册通过不保证非零梯度;实际类型转换、单位、shape、静态控制流 +仍可能自然报错。训练发生在原 root 上,reset 清动态状态但不回滚 root。 + +| 对象 | 当前入口与能力 | 关键边界 | +| --- | --- | --- | +| Channel | ChannelView.trainable;签名参数 | 内部常数和动态 gate 不自动暴露 | +| Ion | IonView.trainable;签名参数与具名初始参数 | 初始参数与动态浓度分离 | +| Synapse | SynapseView.trainable;签名参数 | 事件输入不妨碍 tau/e 等连续参数求导 | +| Connection | ConnectionView.trainable(weight=...) | delay 明确拒绝;可塑性未实现 | +| 电压检测器 | event output/source trainable(threshold=...) | 硬事件前向,代理梯度反向 | +| Network | trainables 聚合、prepare_run/update | 路由固定;共享根去重;全网状态一起求导 | + +## Channel + +典型候选包括 g_max、V_sh、temp、q10、temp_ref,以及显式独立 phi。 +签名中的继承/转发参数可以发现;未暴露到签名的内部速率常数仍由自定义模型负责。 + +温度派生的 sodium phi 在属性访问时根据当前参数求值;独立传入的 phi 不绑定温度。 +速率函数本身每次调用会读取当前参数,不必因为依赖其他参数再改成 property。 +开关或比较产生零梯度是合法结果;不能仅凭整数默认值判断某个数值参数不可学。 + +已验证省略默认值、区域隔离、共享根、dtype 提升、重复 JIT/reset、温度依赖、 +有限差分及自然错误。教学拟合覆盖 g_max、V_sh、temp;不是所有 Channel 的训练穷举。 +示例:[channel_learning.ipynb](../../../../examples/multi_compartment/channel_learning.ipynb)。 +测试:[manager](../../../../braincell/trainable/_manager_test.py)、 +[base Channel](../../../../braincell/_base_channel_test.py)。 +实测:[参数学习结果](results/parameter-learning.md#channel)。 + +## Ion + +区分三个层次:固定物理参数(如固定 Ion 的 E/Ci)、动力学参数(如 tau/kf), +以及动态状态的初始参数(如 Ci_initializer)。Ci(t) 本身不是用初始参数替代的常量。 +每轮在求导范围内 reset,使 Ci(0) 读取更新后的初始 root。 + +复杂 Ion 的默认平衡初值在 reset 按当前依赖参数计算;显式初值保持独立,未选择区域 +保留默认依赖。Fixed E 保持固定语义;InitNernst 在初始化/reset/参数同步刷新; +DynamicNernst/KineticIon 在读取时计算。不是所有派生量都具有相同刷新时机。 +species_initializers 字典的覆盖功能保留,但不增加内部字典键的训练入口; +已有具名 BC_initializer 等签名参数可选择。 + +已验证初值依赖、区域覆盖、共享 Channel/Ion 根、Nernst、shell 因子、重复编译和梯度。 +CalciumFirstOrder 既有 alpha/beta 默认单位不一致,不能作为已通过的训练例子。 +示例:[ion_learning.ipynb](../../../../examples/multi_compartment/ion_learning.ipynb)。 +测试:[Ion 参数集成](../../../../braincell/trainable/_manager_test.py)、 +[Ion 基类](../../../../braincell/ion/_base_test.py)。 +结果与已知精度边界:[参数学习](results/parameter-learning.md#ion)、 +[网络验证限制](results/synapse-network-learning.md#验证与限制)。 + +## Synapse + +Synapse 使用构造签名 metadata,不再通过手写字典决定训练资格。 +ExpSyn 的 tau/e、Exp2Syn 的 tau1/tau2/e 是典型候选;实际模型仍要求有效时间常数, +训练时由 transform 或参数化函数维持正值及 tau1 < tau2。 +Exp2Syn 的归一化因子读取当前时间常数;reset 清突触动态状态,不清参数根。 + +固定 NetStim/event 输入下,学习 tau 或 weight 不需要对事件时间求导; +跨 presynaptic Cell 或 threshold 反传才需要代理梯度路径。没有输入或 loss 不敏感时, +零梯度并非接口故障。同一 CV 上的多个逻辑突触仍是独立 row,除非显式按 cv 分组。 + +示例:[synapse_learning.ipynb](../../../../examples/multi_compartment/synapse_learning.ipynb)。 +测试:[ExpSyn/Exp2Syn](../../../../braincell/synapse/exponential_test.py)、 +[点目标参数](../../../../braincell/trainable/_targets_test.py)。 +结果:[单参数拟合](results/parameter-learning.md#synapse-与-connection)。 + +## Connection 与检测器 + +weight 由接收 Cell 持有,按逻辑 contact 分组;投递读取当前物理 State,不冻结编译前权重。 +默认 spike output 的 threshold 绑定 Cell.V_th;显式 detector threshold 可独立持有参数。 +升沿为 last < threshold <= next,降沿为 last > threshold >= next。 +事件值必须保持浮点,从检测器经过乘权、scatter/稀疏投递到 delay queue 保留梯度。 + +delay 明确拒绝训练,routing/source index 等拓扑也不是当前训练入口。 +weight_initial 与动态可塑性 weight 尚未接入,见 [候选方案](../proposals/connection-plasticity.md)。 +边界与 NEURON 差异由 [事件架构](architecture.md#event-derivatives) 统一定义。 +测试:[事件](../../../../braincell/network/event_test.py)、 +[投递](../../../../braincell/network/delivery_test.py)、 +[参数目标](../../../../braincell/trainable/_targets_test.py)。 +示例仍使用上面的 synapse_learning Notebook。 + +## Network 与梯度方法 + +Network.trainables 聚合原始 Cell roots,以 population.local_name 命名,并按对象身份去重。 +host 上先 prepare_run 固定 dt、路由和队列形状,再以 BrainState 编译循环调用 update。 +run 的 host 记录转换不是可微 rollout 接口。BPTT/full RTRL 使用同一个完整状态转移; +不能在有连接时把跨 Cell 敏感度截成独立块。 + +已实测 A(2) 与 B(3)、每成员 1 CV、12 个双向 contacts、45 个独立坐标,含单侧损失、 +共享根、事件截断对照、前缀和队列敏感度。另有单 Cell 多 CV 证据;不意味着任意 +多 CV × 多 population 组合已经验证。rollout 内更新参数、GPU 上的新双向网络组合未测。 + +测试:[Network roots](../../../../braincell/trainable/_network_test.py)、 +[双向网络](../../../../examples/experimental/optim_gradient_correctness/bidirectional_test.py)。 +示例与计时:[网络结果](results/synapse-network-learning.md)。 + +## 暂不支持的 Owner + +Cell.V_init、cable、morphology/topology 和初始化后改变 trainable ownership 尚未接入。 +构造时能够设置这些值不等于当前 View.trainable 支持它们。后续入口见 [路线图](../proposals/roadmap.md)。 diff --git a/docs/design/optim/references/batch-size-and-gpu-throughput.md b/docs/design/optim/current/results/batch-size-and-throughput.md similarity index 93% rename from docs/design/optim/references/batch-size-and-gpu-throughput.md rename to docs/design/optim/current/results/batch-size-and-throughput.md index d26cf59c..e92b8423 100644 --- a/docs/design/optim/references/batch-size-and-gpu-throughput.md +++ b/docs/design/optim/current/results/batch-size-and-throughput.md @@ -1,4 +1,4 @@ -# Batch Size、数据规模与 GPU 吞吐 +# Batch Size and Throughput Results ## 文档定位 @@ -6,6 +6,10 @@ 必须区分 protocol batch、并行 candidate lanes 和 recurrent time length;硬件宽度近似为 `protocol batch * candidate lanes`,但更宽不等于更高优化效率。 +于 2026-09-07 从 references 迁移,数字未重跑。原记录未完整列出运行日期、JAX 版本及 +每组 artifact;保留已知条件,缺项标为未记录,不补造。历史配置与当前默认配置可能不同, +复查入口见 [参数拟合实验](../../../../../examples/experimental/optim_parameter_fitting/README.md)。 + ## A100 容量测量 条件为 A100-SXM4-80GB、8 starts、float32 JAX、pure-voltage MSE、4,000 simulation steps。 diff --git a/docs/design/optim/references/bptt-rtrl-experimental-results.md b/docs/design/optim/current/results/bptt-rtrl-scaling.md similarity index 95% rename from docs/design/optim/references/bptt-rtrl-experimental-results.md rename to docs/design/optim/current/results/bptt-rtrl-scaling.md index 9a9e020c..048020f0 100644 --- a/docs/design/optim/references/bptt-rtrl-experimental-results.md +++ b/docs/design/optim/current/results/bptt-rtrl-scaling.md @@ -1,17 +1,21 @@ -# BPTT/RTRL 实验结果 +# BPTT/RTRL Scaling Results ## Reference 状态 本文是 BrainCell exact RTRL 与 full BPTT 的 tracked 实验结果快照,记录正确性、内存、性能和 -训练一致性。数学推导见 [BPTT/RTRL 通用理论](./bptt-to-rtrl-neuron-derivation.md),实验程序、 +训练一致性。数学推导见 [BPTT/RTRL 通用理论](../../references/bptt-to-rtrl-neuron-derivation.md),实验程序、 CLI 和 notebook 导航见 -[Optimization Experiments](../../../../examples/experimental/README.md)。 +[Optimization Experiments](../../../../../examples/experimental/README.md)。 本文记录的 A100 scaling 数据生成于 2026-08-29;CPU prototype 数据来自同一实现阶段的本机 x64 测量。原始 CSV/NPZ、trial JSON、manifest 和 worker log 位于 Git ignored artifacts,仍是 具体测量的权威来源。本文只固定可复查的结果与保守结论,不把单台硬件的墙钟外推为一般 渐近规律。 +本文于 2026-09-07 从 references 迁入 results,未重跑或修改历史数值。以下包含不同阶段的 +CPU 和 A100 配置;未明确记录的版本/日期不补造,历史 artifact 的存在性不等于新一次验收。 +近期事件网络的不同配置另见 [Synapse/Network](synapse-network-learning.md)。 + ## 1. 实验对象与维度 主 scaling workload 是 multi-CV HH cell。每个 CV 包含 Leak、Na 和 K 三个独立 row parameter: diff --git a/docs/design/optim/current/results/fitting-and-identifiability.md b/docs/design/optim/current/results/fitting-and-identifiability.md new file mode 100644 index 00000000..15ad5d2f --- /dev/null +++ b/docs/design/optim/current/results/fitting-and-identifiability.md @@ -0,0 +1,210 @@ +# Fitting and Identifiability Results + +## 来源与测量范围 + +本页汇集原刺激设计、训练诊断与消融参考文档中的历史实验数据。于 2026-09-07 迁移, +不是当天重新运行。未记载的实际运行日期、软件版本、硬件或 artifact 标为未记录, +不补造。各小节的 starts、模型、预算和成功标准不同,不能跨表直接比较。 +原始解释和数字保留;方法原理见 [刺激设计](../../references/stimulus-design-and-identifiability.md) +与 [诊断参考](../../references/modular-training-diagnostics.md)。 + +复查入口:[参数拟合](../../../../../examples/experimental/optim_parameter_fitting/README.md)、 +[刺激设计](../../../../../examples/experimental/optim_stimulus_design/README.md)、 +[训练恢复提案](../../proposals/training-recovery.md)。旧配置不一定能由当前默认命令原样重现, +应先核对历史配置;本次未新建原始测量 artifact。 + +## 零号训练基线 + +| 类别 | 固定合同 | +| --- | --- | +| 模型/参数 | 1 soma CV;三个 bounded direct `g_max` | +| target | classical HH `(Leak,Na,K)=(0.3,120,36) mS/cm^2` | +| parameterization | `theta=lower+(upper-lower)*sigmoid(z)`;无frozen scale | +| data | Step-only train/validation/test=`5/2/1`;test final-only | +| loss | protocol/time/CV 等权 raw voltage MSE | +| optimizer | exact RTRL + Adam `lr=0.01`;无 clip/schedule/screening/early stopping | +| starts | 一个seed生成64个physical starts;一次进入同一个kernel和optimizer | +| budget | 180 full-batch epochs,每 epoch 对5条train protocols更新一次 | +| primary success | epoch-180 validation RMSE `<=5 mV` 且每条 validation spike count 正确 | +| secondary | 三参数 relative RMS `<=10%` 与 joint success | + +Validation每10轮记录但不改变trajectory;test只在最终状态评价。A100 x64基线为: + +| 指标 | 结果 | +| --- | ---: | +| trace success | `8/64 = 12.5%` | +| Wilson 95% interval | `[6.47%,22.77%]` | +| parameter / joint success | `3/64 / 1/64` | +| median train MSE | `61.2255 mV^2` | +| median validation / test RMSE | `10.6116 / 17.2393 mV` | +| median parameter relative RMS | `0.2260` | +| compile / stage / end-to-end | `2.85 / 48.88 / 82.50 s` | +| XLA temporary / monitored GPU peak | `2.10 MiB / 1166 MiB` | + +64个endpoint均finite;validation count全对`22/64`,RMSE通过`8/64`,交集`8/64`;test +count全对`35/64`,同时通过5 mV与count为`5/64`。train MSE中位数从`178.5024`降至 +`61.2255 mV^2`。后续改动复用相同initial candidates与预算,不能只展示更好的best case。 + +Python stage pipeline以physical `CandidateSet`在方法间交接:gradient stage在`z`空间工作, +derivative-free stage在bounded normalized coordinate工作;非梯度改变参数后重建Adam moments。 +第一阶段依次单独改变dataset、loss、initialization/search和optimizer。单变量有效后使用 +`baseline / A / B / A+B`: + +```text +interaction = improvement(A+B) - improvement(A) - improvement(B) +``` + +同时报告 paired per-start transition、连续 RMSE、Wilson interval、parameter error、wall time 和 +额外 forward budget;除初始化研究外复用同一64 starts。旧7CV/6-scale、四cohort且holdout混入 +PRMLS的`6/64`结果保留为legacy,不与本基线直接比较。 + +只把Adam预算从180延长到300轮后,trace success从`8/64`增至`10/64`、parameter success从 +`3/64`增至`6/64`,joint仍为`1/64`;validation/test RMSE中位数分别从`10.6116/17.2393` +变为`10.1949/17.1670 mV`。配对迁移为trace `6保持/4新增/2丢失`,joint则丢失原start 37并 +新增start 59。因而增加budget有小幅总体收益,但不能当作lane-wise monotonic recovery;后续方法 +仍需保存best archive并报告固定epoch endpoint。 + +300轮下同时使用Adam `lr=0.02`与`0.1--2.0 x target`宽bounds,并保持64个physical初值逐位 +不变,parameter success从`6/64`增至`14/64`,validation/test RMSE中位数降至 +`8.7019/16.4772 mV`;但trace success从`10/64`降至`5/64`。全部endpoint远离新bounds, +而`3-spike` train count exact仅`1/64`。该双变量组合改善continuous fit和parameter recovery, +却损害spike-region保持;不能据此区分收益来自宽bounds还是高LR。 + +补充bounds-only对照后,`lr=0.01`的wide bounds得到trace/parameter/joint=`9/8/3`,validation/test +RMSE=`8.3472/16.8468 mV`。在相同wide bounds下将LR升到0.02后变为`5/14/1`。因此宽bounds +主要改善continuous fit和parameter recovery;高LR会进一步增加parameter success,但降低trace与 +joint success,表现为更不稳定的spike-region跨越。 + +只替换optimizer为Rprop后,final trace/parameter/joint从Adam的`9/8/3`提高到`35/13/11`, +validation/test RMSE从`8.3472/16.8468`降至`4.4684/9.4214 mV`。Validation-feasible archive +得到validation/test trace success=`38/12`,高于Adam的`22/7`。这支持按gradient符号反转自适应 +缩步比固定moment-based Adam更适合当前deterministic spike-region landscape。 + +BrainTools wrapper把Rprop LR应用两次,使名义`0.01`成为实际initial step `1e-4`,min step成为 +`1e-8`。Single-scale Optax Rprop保持initial `1e-4`但恢复min `1e-6`后,K后50轮median step提高 +约80倍;final trace/parameter/joint为`36/11/10`,archive validation/test trace为`39/12`,与 +wrapper的`35/13/11`和`38/12`接近。重复LR是实现bug且解释了冻结量级,但解除它没有产生额外的 +整体性能跃升,sign-flip零更新仍是后期停滞的主要机制。 + +使用target-std protocol-balanced MSE后,final trace/parameter/joint提高到`43/15/12`,validation/ +test RMSE为`3.8424/9.1728 mV`。Validation archive trace达到`47/64`,但对应test trace为`9/64`, +低于raw-loss archive的`12/64`。权重平衡改善了多数连续指标和validation basin覆盖,但不能替代 +phase-robust loss来保证unseen high-spike protocol泛化。 + +将balanced MSE替换为`delta=5 mV`的MSE-normalized Huber后,final trace/parameter/joint为 +`38/17/15`,archive validation/test trace为`39/13`。相比balanced MSE的`43/15/12`与`47/9`, +Huber牺牲部分validation覆盖,却提高parameter/joint和严格test success,符合其降低大spike-phase +残差主导性的设计目的。它同时把`3-spike` median MSE从`13.995`降到`8.930`,但small-positive +从`7.217`升到`96.045`,不是所有regime同时改善;下一步需避免让线性尾部忽略subthreshold错误。 + +Balanced Huber下的vanilla SGD `lr=1e-4`得到final trace/parameter/joint=`0/2/0`,validation/test +RMSE=`15.1178/21.5266 mV`,显著弱于Rprop。SGD没有贴边或数值发散,但300轮没有形成正确 +validation spike signature;固定幅值gradient descent会稳定下降Huber objective,却缺少Rprop在 +连续同号阶段快速增大per-coordinate step、跨入目标spike basin的能力。 + +加入`momentum=0.9`或Nesterov后,final Huber objective中位数从vanilla SGD的`21.12`降到 +`10.69/11.42`,parameter success提高到`8/11`;但两者final trace/joint仍为0,archive +validation/test trace仅`6/2`与`5/6`。Momentum提高移动速度却不能替代Rprop的per-coordinate +sign adaptation;Nesterov在此任务上也没有稳定优于普通Momentum。 + +## 六参数 Identifiability 结果 + +### Local / Sampled-Prior FIM + +target + 16 Sobol references、33 条 train candidates 得到: + +| 诊断 | 结果 | 解释 | +| --- | ---: | --- | +| relative numerical rank | `6` | 六个 log-conductance 方向均非零可见 | +| worst condition number | 约 `1e7` | 最强/最弱 sensitivity 相差数千倍 | +| worst column correlation | 约 `0.99997` | 严重 regional/Na-K compensation | + +最弱 eigenvector 主要为 soma conductance 减小、dend conductance 增大。满秩不表示 practical +identifiability 良好;加入 33 条 protocol 后仍高度病态。 + +### Forward-Only Global Ensemble + +在 `[log(0.5), log(1.5)]^6` 中评估 16,384 个 scrambled Sobol 参数点,只运行 forward, +保存 per-protocol voltage MSE/hard count 与 raw、normalized train/validation/test score。 +normalizer 来自 target + 16 fixed Sobol prior points 的 per-protocol MSE median,下限 +`1 mV^2`,不随 candidate 或 optimizer 改变。 + +| 比较 | 结果 | +| --- | ---: | +| raw/normalized Top256 intersection | `130` | +| Top256 Jaccard | `0.340` | +| raw Top256 PCA 与 target-FIM weakest direction cosine | `0.871` | +| normalized Top256 cosine | `0.790` | +| raw / normalized Top256 median parameter relative RMS | `0.258 / 0.246` | + +结果验证了 FIM 的 soma/dend compensation direction 会影响全局 candidate ordering,也说明 loss +weighting 会改变“好解”集合。但 16,384 点在六维仍很稀疏,这不是 posterior 或完整 uncertainty +quantification,只是 low-loss candidate pool 和 local weak direction 的非梯度验证。 + +当前评价顺序是:loss 能否下降,unseen voltage/spike 是否泛化,最后再报告 parameter recovery +或 equivalent-model ensemble;参数不接近 synthetic target 不自动等于 functional failure。 + + +## 诊断基线 + +原诊断记录未完整注明 backend/precision、运行日期和 artifact。 +下述 RandomState 是历史初始化来源描述,不是新示例推荐的随机 API。 + +默认 1-CV HH 基线以 `RandomState(seed=123)` 产生 32 个 initialization starts,并在固定 +完整数据上运行 Adam;这些 lane 不是独立随机实验。一次 100-update 运行得到: + +| 观测 | 结果 | +| --- | ---: | +| 终点 loss 低于初值 | `32/32` | +| 三个电导 scale 平均相对误差 `<10%` | `10/32` | +| best loss 出现在最终 update 以前 | `19/32` | +| final loss 比 best 高 `>10%` | `5/32` | +| 接近 `[0.1, 2.0]` transform bounds | `0/32` | +| final MSE | 中位数 `4.7627 mV^2`,范围 `0.0923--44.4395 mV^2` | + +因此不能用 final loss 或 bound saturation 单独解释失败。至少要区分 spike basin、低梯度 +平原、高梯度震荡、慢速移动、spike phase 误差和 `gNa/gK` 补偿。 + + +固定 protocol 下,spike count 在参数区域内是整数常量,跨兴奋性边界时跳变。当前四协议 +目标及已观察失败示例为: + +| 类型 | Signature | +| --- | --- | +| target | `(1, 2, 3, 4)` | +| low excitability | `(1, 1, 1, 2)` | +| mixed mismatch | `(1, 1, 3, 4)` | +| high excitability | `(2, 3, 4, 5)` | + + +## 消融的历史参考点 + +以下来自恢复消融计划,不是已经执行了完整 A-F 消融;正式基线仍须按提案重新测量。 + +现有 fixed Adam `lr=0.02`、180-update 结果只作为 promotion reference,不是测试常量: + +| 指标 | 当前值 | +| --- | ---: | +| trace success | `3/8` | +| parameter success | `4/8` | +| median common loss | `0.235153` | +| median aggregate RMSE | `7.8201 mV` | +| median mean parameter error | `0.1562` | +| best common loss | `0.0153285` | + + +## Scheduler 的历史缺陷观察 + +以下只描述特定历史依赖版本;不宣称当前依赖仍有同一错误。正式恢复策略尚未实现, +必须执行 effective-LR 测试,不能将 reported LR 当作实际参数步长。 + +`braintools 0.1.9` 的 `CosineAnnealingWarmRestarts` 曾出现 reported LR 更新但实际参数增量 +仍固定的问题(`base_lr=0.1, T_0=2, eta_min=0.01` 时报告 `0.1, 0.055, ...`,实际 delta +始终 `-0.1`)。正式使用前必须以常梯度回归测试验证 effective LR;临时 controller 只放 +在 example 内。 + + +## 解释边界 + +FIM 满秩、loss 下降、spike signature 正确、held-out 通过和恢复真实参数是不同验收。 +本页保存的是有限配置的证据,不是通用 optimizer 排名或全局可辨识性证明。 diff --git a/docs/design/optim/current/results/parameter-learning.md b/docs/design/optim/current/results/parameter-learning.md new file mode 100644 index 00000000..5a20e9aa --- /dev/null +++ b/docs/design/optim/current/results/parameter-learning.md @@ -0,0 +1,79 @@ +# Parameter Learning Results + +## 来源与口径 + +本页收录 2026-09-07 Channel、Ion、Synapse 学习记录及已执行 Notebook 的教学结果, +本轮文档整理没有重跑。初值、目标与拟合值属于各自实验,不是模型默认参数推荐。 +每个示例只学习一个 scalar root;拟合成功不等于任意参数可辨识。 + +历史来源:[Channel](../../../../specs/2026-09-07-channel-learning.md)、 +[Ion](../../../../specs/2026-09-07-ion-learning.md)、 +[Synapse/Network](../../../../specs/2026-09-07-synapse-network-learning.md)。 +没有新增原始 artifact;环境缺项不从其他实验推断。 + +## Channel + +Python 3.11、JAX 0.8.0、CPU;一 CV、一目标 spike、20 ms waveform、100 次 Adam。 +拟合后也保留一个 spike,MSE 单位为 mV squared。精度以 Notebook 环境配置为准, +原历史结果摘要没有独立列出精度值。本次不补作跨精度比较。 + +| Parameter | Initial | Target | Fitted | Initial MSE | Final MSE | +| --- | ---: | ---: | ---: | ---: | ---: | +| g_max (mS/cm^2) | 108 | 120 | 119.966 | 55.9342 | 0.0000709192 | +| V_sh (mV) | -44 | -45 | -45.0055 | 258.324 | 0.00785799 | +| temp (K) | 308.15 | 309.15 | 309.145 | 24.2734 | 0.000379125 | + +三项拟合当次合计 17.48 s,包含编译,不是预热后的梯度内核耗时。 +历史回归 1,051 passed;兼容回归 392 passed、2 skipped;112 类的 375 个 numeric defaults +已检查。改动可执行行覆盖 135/140 = 96.43%,不是整仓库覆盖率或正确概率。 +gateCurrent 的固定电压探查前向会变但开关梯度为零;既有 enabled current 幅值未校准, +没有用来生成教学训练目标。GPU 未测。 + +示例:[channel_learning.ipynb](../../../../../examples/multi_compartment/channel_learning.ipynb)。 + +## Ion + +Python 3.11、JAX 0.8.0、CPU;原记录说明教学与集成检查亦使用默认 float32。 +每项一 CV、一个 scalar scale root、800 步、dt=0.025 ms、100 次 Adam、lr=0.03。 +前两项训练带 spike 的电压,后三项训练浓度。电压 MSE 用 mV squared,浓度 MSE 用 uM squared。 + +| Parameter | Initial | Target | Fitted | Initial MSE | Final MSE | +| --- | ---: | ---: | ---: | ---: | ---: | +| E (mV) | 45 | 50 | 49.97645 | 14.18368 | 8.10408e-5 | +| temp (K) | 300 | 309.15 | 309.10382 | 4.34521 | 3.91733e-4 | +| Ci_initializer (mM) | 0.0008 | 0.001 | 0.0009999672 | 0.00501708 | 1.32332e-10 | +| tau (ms) | 7 | 5 | 4.998580 | 0.00462772 | 2.85074e-9 | +| kf (1/(mM ms)) | 1.6 | 2 | 2.000439 | 17.27890 | 1.51110e-5 | + +五项拟合当次合计 23.75 s,包含编译。历史回归 1,482 passed、2 skipped,加一个字典顺序 +回归;23 类、310 个 numeric defaults 已检查。改动可执行行覆盖 209/212 = 98.58%。 +未覆盖分支是非均匀 scalar 配置拒绝、默认解析的空间 callback、独立 kinetic 显式 Ci 合并。 +历史覆盖率 artifact 位于临时目录,未新增持久报告。 +CalciumFirstOrder 原有 alpha/beta 默认单位问题未修复,不包含在成功教学例子中。 +原 Ion 记录未测 GPU/其他 JAX;后续 JAX 0.10.1 上下文敏感问题另见网络结果页,不能混成全绿。 + +示例:[ion_learning.ipynb](../../../../../examples/multi_compartment/ion_learning.ipynb)。 + +## Synapse 与 Connection + +来自单 CV、每例一个 scalar root、每个目标含一个 spike 的既有 Notebook。 +以下是 JAX 0.8.0 CPU 已执行结果;JAX 0.10.1 的三个拟合验收也通过。 +helper 使用 dt=0.025 ms、240 步/6 ms、100 次 Adam;tau/weight 的 lr=0.03,threshold 的 +lr=0.5。三项都是 voltage MSE(mV squared),但前两项用固定 NetStim,threshold 用 autapse, +不是相同输入配置。scale 初值为 0.8;threshold 初始为 -30 mV、目标 -20 mV。 + +| Target | Initial MSE | Final MSE | 解释 | +| --- | ---: | ---: | --- | +| Synapse tau | 0.0564968 | 2.48711e-7 | 连续突触动力学参数 | +| Connection weight | 3.11666 | 9.70201e-7 | 独立乘权路径 | +| Detector threshold | 2.5993e-5 | 0 | 离散事件网格等价,不是唯一阈值恢复 | + +示例:[synapse_learning.ipynb](../../../../../examples/multi_compartment/synapse_learning.ipynb), +实现:[synapse_learning.py](../../../../../examples/multi_compartment/synapse_learning.py)。 +网络梯度、双向联合训练及环境限制见 [网络结果](synapse-network-learning.md)。 + +## 复查方式 + +三个 Notebook 均可在选择好依赖环境后用 nbconvert 从新 kernel 执行,旧 spec 中保留原命令。 +本次不执行它们,不把文档检查当作新的一轮数值验收。支持范围与错误语义以 +[参数支持度](../parameter-support.md) 为准,不由这些成功例子构造白名单。 diff --git a/docs/design/optim/current/results/synapse-network-learning.md b/docs/design/optim/current/results/synapse-network-learning.md new file mode 100644 index 00000000..fdba2330 --- /dev/null +++ b/docs/design/optim/current/results/synapse-network-learning.md @@ -0,0 +1,164 @@ +# Synapse and Network Learning Results + +## 来源与配置边界 + +本页保存 2026-09-07 的实验与会话测量,以及 2026-09-08 的提交验收;两组记录分别注明配置。 +公共支持见 [参数支持度](../parameter-support.md),事件导数合同见 +[架构](../architecture.md#event-derivatives),完整 RTRL 仍是实验代码。 +历史来源:[Synapse/Network](../../../../specs/2026-09-07-synapse-network-learning.md)、 +[双向 population](../../../../specs/2026-09-07-bidirectional-population-learning.md)。 + +结果分为三类:单/双 Cell 与自连接验证、A(2)/B(3) 双向 population 验证、独立 CPU 计时。 +不能把它们与 [历史多 CV/A100 scaling](bptt-rtrl-scaling.md) 合成同一配置。 +各组都在 rollout 内固定参数;不验证每个 timestep 更新优化器的语义。 + +## 事件与自连接 + +既有 autapse 实验在 JAX 0.8.0 和 0.10.1 CPU 上验证 voltage/spike loss、零及 0.1 ms +固定 delay、two-Cell sensitivity 和 carry shape。Notebook 的 float64 比较中, +最大绝对梯度差不超过 5.24e-10;spike loss 的差不超过 3.33e-15。 +这是同一 surrogate 图上的 forward/reverse 一致性,不是硬事件时间的有限差分导数。 +其完整模型、步长和长度见 [autapse.py](../../../../../examples/experimental/optim_gradient_correctness/autapse.py); +单参数 tau/weight/threshold 教学结果单列于 [参数学习](parameter-learning.md#synapse-与-connection)。 + +## 双向 Population + +A 含 2 个成员、B 含 3 个成员,每成员 1 CV HH;A 接收 ExpSyn,B 接收 Exp2Syn。 +每方向 6 个 contact,含汇聚与发散。dt=0.025 ms,共 800 步/20 ms,错开的两次 clamp +使每个成员都发放两次 spike,并在接收非零突触电导后继续放电。 + +梯度验证共 15 个具名向量根、45 个独立标量坐标:两边 Channel 的 g_max/V_sh、 +SodiumFixed.E、突触时间常数/e、检测器 threshold,以及每个 contact 的 weight。 +拟合时改成各 population/方向内共享,共 15 个标量根,A/B 不共享;A.weight 指 B 到 A, +B.weight 指 A 到 B。scale 根是无量纲因子,shift/reversal/threshold 根是以 mV 计的偏移。 + +| Loss | Fixed delays | Delivery | +| --- | --- | --- | +| Joint voltage | Heterogeneous, including zero | scatter | +| A-only voltage | Homogeneous positive | scatter | +| B-only voltage | Heterogeneous, including zero | brainevent | +| Joint spikes | Zero | brainevent | + +全部坐标逐项比较,float64 接受界为 atol=1e-8、rtol=1e-7。已执行 Notebook 的 joint voltage +最大绝对梯度差约 3.35e-10。还验证单侧 loss 到另一侧 gmax/threshold 的非零梯度; +仅截断 event 反向时,前向 voltage/spike/conductance 完全相同,但这些跨 population 梯度为零。 +共享 gmax 根得到独立 A/B 梯度之和;前缀、双向电压/队列敏感度、编译后 reset/root 更新和 +恢复也通过检查。四组组合不是 loss/delay/backend 的全部笛卡尔积。 + +| 训练方法 | Adam updates | Initial voltage MSE | Final voltage MSE | Final/initial | +| --- | ---: | ---: | ---: | ---: | +| BPTT | 100 | 25.51068369 | 0.10422649 | 0.0040856 | +| Full RTRL | 100 | 25.51068369 | 0.10422649 | 0.0040856 | + +使用同一个 synthetic spiking target、相同扰动初值、Adam lr=0.01,MSE 单位 mV squared。 +全部 15 根变化;这不证明唯一恢复生成参数。自动化测试允许 200 次更新,要求至少十倍下降, +两个 JAX 环境均通过。实现与测试见 +[bidirectional.py](../../../../../examples/experimental/optim_gradient_correctness/bidirectional.py)、 +[bidirectional_test.py](../../../../../examples/experimental/optim_gradient_correctness/bidirectional_test.py)。 + +## 独立 CPU 计时 + +来源为本次整理之前、2026-09-07 会话中的只读独立进程测量;未保存独立原始 artifact, +也没有可引用的 benchmark commit。下表是会话记录,不伪装成现有 scaling CLI 输出。 + +- Intel Xeon Platinum 8358P,affinity 为逻辑 CPU 0/1/2/3;没有声明独占机器。 +- Python 3.11、JAX 0.8.0、CPU、float64、scatter、固定异质 delay,dt=0.025 ms。 +- 模型来自同一 bidirectional.build,grouped=True/False 分别为 15/45 个标量坐标。 +- 每种方法和配置独立进程、串行测量;OMP/OPENBLAS/MKL_NUM_THREADS=1。 +- engine.prepare 后,把当前 roots 与形状 (T,5)、值为 -60 mV 数值的 target 作为动态参数, + 分别编译引擎 _bptt/_rtrl;它们是本次测量使用的实验私有方法,不是公共 API。 +- 首次执行同步完成后,再同步计时 7 次,取中位数;每次含 reset 与整段 loss/gradient, + 不含构建/trace、编译、Adam、目标生成和进程启动。 +- 800/8000 步对应 20/200 ms,长轨迹不追加 clamp,后段没有新增刺激;不是持续放电负载。 +- BPTT 未使用 checkpoint;RTRL 返回逐步 losses,但不输出 sensitivity history。 + +| 参数数 | 步数 | BPTT median | RTRL median | BPTT 工作内存 | RTRL 工作内存 | +| ---: | ---: | ---: | ---: | ---: | ---: | +| 15 | 800 | 58.000 ms | 52.999 ms | 4.49 MiB | 0.078 MiB | +| 15 | 8000 | 516.739 ms | 357.917 ms | 44.70 MiB | 0.408 MiB | +| 45 | 800 | 54.897 ms | 93.528 ms | 4.49 MiB | 0.146 MiB | +| 45 | 8000 | 544.125 ms | 636.154 ms | 44.70 MiB | 0.475 MiB | + +工作内存为 XLA memory_analysis 的 argument + output + temporary - alias;本次 alias 均为 0。 +它不是 RSS、GPU 峰值、纯 sensitivity carry 或分配器保留总量。以下保存字节口径与编译结果, +避免后续将 MiB 舍入值当成新的原始数据: + +| P/T | 方法 | Compile (s) | Temporary bytes | Argument bytes | Output bytes | Host peak RSS (MiB) | +| --- | --- | ---: | ---: | ---: | ---: | ---: | +| 15/800 | BPTT | 8.071531 | 4667656 | 32120 | 6664 | 1211.44 | +| 15/800 | RTRL | 6.164166 | 43160 | 32120 | 6664 | 1196.21 | +| 15/8000 | BPTT | 8.053768 | 46485256 | 320120 | 64264 | 1254.66 | +| 15/8000 | RTRL | 5.946149 | 43160 | 320120 | 64264 | 1184.76 | +| 45/800 | BPTT | 8.686402 | 4669176 | 32360 | 6904 | 1246.57 | +| 45/800 | RTRL | 6.244573 | 113384 | 32360 | 6904 | 1219.29 | +| 45/8000 | BPTT | 8.425463 | 46486776 | 320360 | 64504 | 1291.93 | +| 45/8000 | RTRL | 6.078932 | 113384 | 320360 | 64504 | 1219.36 | + +Host peak RSS 用 Linux ru_maxrss,包含导入和编译。导入后 baseline RSS 约 472 MiB; +构建/prepare 另外约 9.55-12.12 s。两种方法整个进程都约 1.2 GiB,不能说进程 RAM 小了百倍。 +四组配对最大绝对梯度差依次为 9.313e-10、2.736e-9、2.983e-10、6.112e-10。 + +在这些配置内,15 根时 RTRL 速度接近或更快,45 根时慢约 17%-70%;工作内存约小 +31-110 倍。15 根/800 步的计时范围有重叠,不能据此宣传稳定加速。 +延长时间时 RTRL temporary 不变,而总工作内存仍因输入和逐步 loss 增长。 +优势取决于全网状态 H 与独立参数 P,不是每 Cell 的局部 hidden 数;不能外推到任意 +多 CV、多 population、其他后端或 checkpoint BPTT。 + +复测需按上述协议建立新的独立测量,保存新环境与原始结果;本页不提供不存在的 CLI。 + +## 提交验收 + +功能提交 `5f90f69`,验收日期 2026-09-08。以下检查在导出的暂存快照上使用 CPU 执行; +验收后仅补充 Design 文字,提交中的代码和示例与受测版本一致。 + +| 环境 | 检查范围 | 结果 | +| --- | --- | --- | +| Python 3.11.4 / JAX 0.8.0 | Channel、Ion、compute、trainable、Cell、Network、Synapse、三项基类测试及新增训练示例测试 | 1523 passed, 2 skipped | +| Python 3.11.15 / JAX 0.10.1 | Network、Synapse、Network roots、点参数目标及新增训练示例测试 | 175 passed, 2 skipped;另 25 subtests passed | +| Python 3.11.4 / JAX 0.8.0 | synapse_learning.ipynb 全部 6 个代码单元 | 全部通过 | + +新增示例测试为 autapse_test.py、bidirectional_test.py 和 synapse_learning_test.py, +分别覆盖反馈梯度、双向 population 和单参数拟合,位置见本页各实验入口。 +JAX 0.10.1 本次执行的是所列相关套件;组合 Ion 回归的已有精度问题仍见下方记录。 + +## 验证与限制 + +| 历史检查 | 结果 | +| --- | --- | +| JAX 0.8.0 相关 Channel/Ion/runtime/trainable/Cell/Network/Synapse | 1509 passed, 2 skipped | +| Synapse 扩展的覆盖率运行 | 381 passed, 2 skipped;改动行 364/378 = 96.3% | +| JAX 0.10.1 focused Synapse/Network/target/base | 173 passed, 2 skipped;另 8 subtests | +| JAX 0.8.0 双向网络与 delivery | 11 passed | +| JAX 0.10.1 同组测试 | 11 passed | +| 网络、参数聚合、autapse 回归 | 135 passed, 1 skipped | +| Python tracer 的选定覆盖率运行 | 3 passed | +| 新 bidirectional 实验代码覆盖率 | 139/146 = 95.2%;缺七行命令行报告入口 | +| 新稀疏导数实现 | 无未覆盖可执行行 | +| 最新 synapse_learning Notebook | 六个 code cells 执行成功 | + +这些覆盖率都不是整仓库覆盖率或正确概率。测试组存在重叠,不相加为独立测试总数。 +本次只整理历史结果,没有重新跑测试、Notebook 或 benchmark。 + +brainevent 的 weight JVP 批处理曾在两个环境报 weight_info 参数错误,已有先失败后修复的 +delivery 回归;前向仍用 coomv,精确双线性 JVP 使用 scatter,没有修改依赖文件。 + +JAX 0.10.1 的组合 Ion float32/float64 测试存在执行顺序/上下文敏感失败:旧 HEAD 特定顺序 +也复现五个失败,17 个 manager Ion 测试在新进程通过;旧 HEAD 完整相关运行另有 +1488 passed、2 skipped。不能把 focused 通过写成当前完整 JAX 0.10.1 套件无条件全绿。 +覆盖率插桩是否为原因未确定,该问题未在本轮修复。 + +JAX 0.8.0 的一次 C tracer 覆盖率运行在 AD tracing 原生崩溃,不计成功;随后普通测试及 +JAX 0.10.1 Python tracer 通过。原因未确定,不以文档重组宣称解决。 +GPU 上的新事件网络、多 CV × 多 population、自定义机制以及 rollout 内更新参数尚未验证。 + +## 复查入口 + +[已执行 Notebook](../../../../../examples/multi_compartment/synapse_learning.ipynb) 保存表格和图。 +在选定依赖环境后,功能检查的已有命令为: + +```bash +python -m pytest -q braincell/network/delivery_test.py examples/experimental/optim_gradient_correctness/bidirectional_test.py +python -m pytest -q examples/experimental/optim_gradient_correctness/autapse_test.py examples/multi_compartment/synapse_learning_test.py +``` + +这些命令验证功能而不是生成上述独立计时表;构造耗时、整轮测试耗时不能替代梯度内核时间。 diff --git a/docs/design/optim/design-overview.md b/docs/design/optim/design-overview.md deleted file mode 100644 index d1b1bc64..00000000 --- a/docs/design/optim/design-overview.md +++ /dev/null @@ -1,153 +0,0 @@ -# Model Optimization Design Overview - -## 文档状态 - -本目录讨论 BrainCell 模型参数优化所需的接口、架构和实验依据。目录名 `optim` 表示 -问题域,不对应 `braincell.optim` Python 模块,也不表示 BrainCell 自己实现优化算法。 - -当前已经实现的首个公共 API 是:在三个 Channel 上选择可训参数,并把低维参数或 latent -函数映射到 runtime 物理字段。数据、loss、搜索、诊断和结果协议仍处于需求或实验阶段, -在形成稳定合同前不预先占用公共类型名。 - -## 文档导航 - -### 规范文档 - -- [API](api.md):当前公共接口、调用方式、参数和错误合同。 -- [Architecture](architecture.md):参数所有权、binding、materialization 和单位边界。 -- [Implementation plan](implementation-plan.md):实现阶段、文件边界和验收场景。 -- [Optimization Experiments](../../../examples/experimental/README.md):experimental gradient core、正确性、scaling 和训练实验导航。 -- [Parameter Learning Experiments](../../../examples/experimental/optim_parameter_fitting/README.md):Python组合式模型、数据、loss、gradient/non-gradient stage和结果合同。 - -### References - -以下文档提供实验、方法调研和技术分析,不定义公共 API。与规范文档冲突时,以 API、 -Architecture 和 Implementation plan 为准。推荐按问题选择阅读,不需要顺序通读。 - -#### 训练目标与工作流 - -- [电压轨迹与 Spike-Aware 参数拟合](references/voltage-and-spike-parameter-fitting.md):设计 - subthreshold/spike loss、mask、curriculum 和成功标准时阅读。 -- [模块化训练诊断与优化恢复](references/modular-training-diagnostics.md):实现 observer、 - archive、plateau、spike-region 或非局部恢复时阅读。 -- [优化恢复消融协议](references/optimization-ablation-protocol.md):比较 scheduler、controller - 和 perturb 策略时使用的固定实验合同。 - -#### 梯度理论与 Solver - -- [BPTT/RTRL 通用理论](references/bptt-to-rtrl-neuron-derivation.md):通用链式法则、`v/w` - 梯度路径、online/e-prop 边界和 1-CV HH 示例。 -- [Staggered Solver 梯度分析](references/staggered-solver-gradient-analysis.md):当前 DHS 与 - post-voltage 离散程序的一步梯度,以及替换 solver 时的验证边界。 - -#### 性能 - -- [Batch Size 与 GPU 吞吐](references/batch-size-and-gpu-throughput.md):protocol batch、 - candidate lanes、显存、吞吐和统计效率。 - -#### 外部方法 - -- [Jaxley 参数模型](references/jaxley-parameter-model.md):参数选择、sharing 和 simulation - replacement 的实现取舍。 - -### 主题所有权 - -同一概念只在主文档中完整定义,其他文档只提供链接和本地上下文: - -| 主题 | 唯一主文档 | -| --- | --- | -| 公共 trainable API 与错误合同 | [API](api.md) | -| ownership、binding、materialization、单位 | [Architecture](architecture.md) | -| voltage/spike loss 与 curriculum | [电压轨迹与 Spike-Aware 参数拟合](references/voltage-and-spike-parameter-fitting.md) | -| diagnostics、archive、region、recovery | [模块化训练诊断与优化恢复](references/modular-training-diagnostics.md) | -| BPTT/RTRL 通用公式 | [BPTT/RTRL 通用理论](references/bptt-to-rtrl-neuron-derivation.md) | -| 当前 solver 的程序导数 | [Staggered Solver 梯度分析](references/staggered-solver-gradient-analysis.md) | -| 测量数据 | 对应实验结果或性能文档 | - -## 功能地图 - -参数拟合工作流可以拆成七个能力域: - -| 能力域 | BrainCell 应负责 | 复用或用户负责 | 当前状态 | -| --- | --- | --- | --- | -| 数据集 | 单位、shape、PyTree 兼容性要求 | 文件、切分、增强、DataLoader | 尚未形成 API | -| 可训参数 | 参数选择、共享自由度、稳定状态树 | BrainState `nn.Param` / `ParamState` | P0 已设计 | -| 参数映射 | direct、scale、latent function 到 runtime field | 用户空间函数 | P0 已设计 | -| 损失 | 仿真输出与单位边界的互操作原则 | BrainTools metric 或用户 callable | 实验阶段 | -| 优化器 | 提供原始 ParamState tree | `braintools.optim` 或用户 optimizer | 直接复用 | -| 预采点 | 参数 tree 的批量赋值和候选形状 | Sobol/LHS/Nevergrad/SciPy | 需求阶段 | -| 诊断与评价 | 模型特有 metadata 和结果语义 | 成功规则、profiler、科学结论 | 实验模块已实现 | - -BrainCell 不应因为涵盖这个问题域就再实现一套 Trainer。可训参数系统只负责把模型暴露 -为一个结构清楚、单位正确、可由 BrainState 求导或被黑盒搜索写入的参数化函数。 - -## 公共模型 - -```text -View selection - -> View.trainable(field=source) - -> target Cell TrainableManager - roots: nn.Param / ParamState - bindings: source -> runtime field - ParameterSet: optimizer-facing tree - -> init/reset/run materialization - -> existing BrainCell simulation - -Future: Network.trainables - -> aggregate target Cell managers - -> one deduplicated ParameterSet -``` - -公共 helper 位于 `braincell.trainable`,不是 `braincell.optim`: - -```python -view.trainable( - g_max=braincell.trainable.scale(group_by="all") -) - -parameters = cell.trainables.parameters() -states = parameters.states() - -optimizer = braintools.optim.Adam(lr=1e-2) -optimizer.register_trainable_weights(states) -``` - -`braincell.trainable` 负责模型参数化;`braintools.optim` 负责优化算法。两个 namespace 的 -职责不能合并。 - -## 当前范围 - -P0 只覆盖 multi-compartment `ChannelView` 上 `IL`、`Na_HH1952`、`K_HH1952` 的连续 -物理参数,并要求在 -`init_state()` 前声明。以下内容不属于首批实现: - -- Ion、Synapse 和 Connection weight; -- Network 参数聚合与自动物化; -- cable、Cell initial value 和 topology 参数; -- 初始化后改变 trainable ownership; -- Dataset、loss composition、history、checkpoint 和 convergence 公共类型; -- optimizer、scheduler 或通用搜索算法。 - -Synapse 与 Connection 不需要另一套参数系统。它们后续实现相同的 `View.trainable()`; -因为 SynapseStore 和 ConnectionStore 都由目标 Cell 持有,binding 仍进入该 Cell 的 -`trainables` manager。未来 `Network.trainables` 负责跨 Cell 聚合。 - -## 设计原则 - -- 只把真实训练自由度包装为 `nn.Param`,不批量改写所有 runtime 字段类型。 -- root 与 materialized runtime value 是两个 view;graph state 只暴露 root。 -- direct、shared scale 和任意 latent function 使用同一 binding 主链。 -- 物理量保留 `brainunit` 单位,loss 在明确尺度上无量纲化。 -- 参数选择、group 和 output shape 在 JIT 内固定。 -- BrainCell 只提供模型特有互操作,不隐藏 BrainState 的 grad/state 接口。 -- 尚未通过真实实验证明通用的 helper 不进入公共 API。 - -## 演进方式 - -新增接口时按以下顺序更新文档: - -1. 在本页确定能力边界和与现有系统的关系; -2. 在 `architecture.md` 锁定 ownership、数据流和生命周期; -3. 在 `api.md` 增加完整调用合同; -4. 在 `implementation-plan.md` 增加落地阶段和验收场景; -5. References 继续保留原始方法、数据和结论,不承担公共 API 规范职责。 diff --git a/docs/design/optim/proposals/connection-plasticity.md b/docs/design/optim/proposals/connection-plasticity.md new file mode 100644 index 00000000..f5b1b274 --- /dev/null +++ b/docs/design/optim/proposals/connection-plasticity.md @@ -0,0 +1,50 @@ +# Connection Plasticity Training Proposal + +## 状态 + +讨论中,尚未实现可塑性接口、动态权重规则或 weight_initial 训练入口。 +本文管理参数训练与可微性;两类可塑性模型的划分、规则在 Connection 上的挂载、pre/post +信号选择和运行时更新时序由 [Network 可塑性提案](../../network/proposals/connection-plasticity.md) 管理。 +本文只记录已经讨论的方向,不定义可调用的类、方法、签名或默认窗口。 +当前可用的是静态 Connection weight 参数和事件 threshold,见 [支持度](../current/parameter-support.md)。 +局部训练事项由 [Optimization TODO](../TODO.md) 跟踪。 + +## 参数与状态分开 + +沿用 Ion 初始浓度的分离思路:初始权重参数决定 reset 时的动态 weight;仿真期间规则 +更新的是动态状态,跨 epoch 优化器更新的是 root。概念关系为: + +```text +trainable initial-weight root -> reset -> dynamic weight(t) +rule parameter roots + selected pre/post events or voltage -> plasticity state transition +dynamic weight(t) -> synaptic delivery -> loss +``` + +这只是候选数据流,不是目前已经存在的调用方式。固定 weight 的现有行为必须保留, +接收 Cell 持有 Connection 的所有权原则也应复用,不另造一套参数 manager。 + +## 可微性边界 + +以“pre 后 5 ms 内出现 post,则 weight 增加 alpha”为讨论例子,5 ms 不是已确定默认值。 +当窗口命中分支执行且之后的 loss 对 weight 敏感时,连续 alpha 可能具有梯度;未命中、 +未影响后续输出或被截断时也可能是零。窗口长度、事件配对、硬比较和离散索引不保证可微。 + +不要求所有规则参数都可训,也不另加科学白名单;是否能经统一 source 注册、如何处理 +静态配置及报错边界仍需在接口设计时确认。现有 spike surrogate 不能自动使时间窗口、 +取整或事件配对可微。更不能把 surrogate 梯度解释为硬事件时间的普通有限差分导数。 + +## 实现前必须决定 + +- 先由 [Network 提案](../../network/proposals/connection-plasticity.md#实现前必须决定) 确定规则挂载、 + 信号、同一步更新与投递顺序、delay、单位和 reset 契约,再按该契约建立可微的数据流。 +- 明确初始权重和规则参数的 source 注册、共享分组、持久化,以及状态初值对参数的依赖。 +- 区分初始权重、动态 weight、可塑性 trace、事件历史和优化器 root,避免 setter 改断绑定。 +- 固定 shape 后把全部影响未来输出的可塑性状态纳入 BPTT/full RTRL;不能只跟踪 weight。 + +## 候选验收 + +先用固定外部 pre/post 事件验证 alpha 路径、无命中零梯度和重复 reset;再加入 cell-generated +事件验证代理梯度和完整 queue/trace 敏感度。对离散窗口分别检查前向规则与求导边界, +不承诺可训。电压依赖规则还需覆盖选定电压及滤波状态到后续权重和 loss 的梯度路径。 +最后对同一固定参数 rollout 比较 BPTT/RTRL,再讨论其他 online 语义。 +这些是后续实施条件,本次只整理文档,没有创建上述接口或测试。 diff --git a/docs/design/optim/proposals/roadmap.md b/docs/design/optim/proposals/roadmap.md new file mode 100644 index 00000000..9062fa63 --- /dev/null +++ b/docs/design/optim/proposals/roadmap.md @@ -0,0 +1,41 @@ +# Optimization 后续方向 + +状态:待讨论;具体候选方案见 [可塑性](connection-plasticity.md) 和 [训练恢复](training-recovery.md)。 +进度唯一入口是 [Optimization TODO](../TODO.md),本文只保留讨论边界和验收方向,不重复状态表。 +现有能力见 [支持度](../current/parameter-support.md) 与 [实验工作流](../current/experimental-workflows.md)。 +已完成 P0 的历史明细见 [归档](../../../specs/2026-08-29-trainable-parameter-implementation.md)。 + +## Cell 初值与 cable 参数 + +Cell 初值和 cable 参数目前没有 trainable owner 接入。先梳理构造转换、几何缓存、runtime buffer +与初始化依赖,再确定注册和重置契约。构造时可以配置不等于运行时可以训练。 + +## 公共训练协议与分组 + +数据、loss、result、resume 的公共协议需先验证实验组合接口能否通用,不提前占用公共类型名。 +稳定 branch/region/custom grouping 需先解决 row fingerprint、持久化和 ownership 合同。 + +## 验证缺口 + +- 完整 RTRL 的多 CV × 多 population 组合:固定参数下验证全网梯度、跨 CV/Cell 敏感度和资源扩展。 +- 双向事件网络 GPU 性能:独立进程、公平 warm timing、相同 surrogate、记录峰值内存。 +- checkpoint BPTT 与 RTRL 对照:分别报告重计算成本、临时内存和进程峰值。 + +已有单项结果不能代替上述组合验收。更换 solver 或加入自定义可微机制时,按 +[solver 分析](../references/staggered-solver-gradient-analysis.md) 重新检查初始化、状态捕获和程序导数, +不能沿用旧误差数字作为新验收结果。 + +## Rollout 内更新参数 + +先定义梯度对应哪个参数历史,不从固定参数等价性外推。这是独立研究方向。 + +## 关联提案与边界 + +- [Connection 可塑性训练](connection-plasticity.md):先依据 [Network 可塑性提案](../../network/proposals/connection-plasticity.md) + 确定运行时契约,再讨论初始权重、规则参数的注册和梯度验收;冻结讨论边界不代表发布占位 API。 +- [训练恢复](training-recovery.md):有 observer/archive 不等于有 controller;先完成 effective-LR 与恢复状态测试,保留原有策略和消融协议,正式比较仍未执行。 + +delay 训练保持不支持,不因本路线图而自动进入开发范围。 +proposal 确认并实际实现后才更新当前 API/实验工作流;实测进入 current/results,理论继续留在 references。 +设计可以领先代码,但示例不能展示不存在的调用;历史 specs 不反向覆盖现行合同。 +维护方式见 [文档对齐](../TODO.md#文档对齐)。 diff --git a/docs/design/optim/proposals/training-recovery.md b/docs/design/optim/proposals/training-recovery.md new file mode 100644 index 00000000..e98984e1 --- /dev/null +++ b/docs/design/optim/proposals/training-recovery.md @@ -0,0 +1,264 @@ +# Training Recovery Proposal + +## 状态与目的 + +待设计,未实现自动恢复 controller,未运行正式 A-F 消融。观测、历史总结与训练后 +best archives 已有实验代码,不等于具有 online archive、完整 resume、SGDR 或 perturb。 +当前能力见 [实验工作流](../current/experimental-workflows.md),方法解释见 +[诊断参考](../references/modular-training-diagnostics.md)。 + +本页合并原恢复参考中的候选动作和消融协议。所有阈值、预算、接受规则均属于候选实验 +设计,不是 BrainCell 公共默认值。实施或正式运行仍需单独确认;本次整理不授权它们。 + +## 状态分类与动作 + +plateau 以 best loss 的相对改善为主,并使用每条轨迹自身的梯度和位移尺度: + +```text +warmup_updates = 40 +patience = 25 +relative_improvement = 0.005 +cooldown = 20 +max_recoveries = 3 +epsilon = 1e-8 + +relative_gain = (old_best - new_best) / max(abs(old_best), epsilon) +``` + +只有 warmup 后连续 25 updates 未达到 `0.5%` 改善、不在 cooldown 且 recovery 未达三次 +时才判定 plateau。单次 raw-loss 上升或一次 spike-boundary 抖动不触发恢复。 + +| 状态 | 主要证据 | 下一动作 | +| --- | --- | --- | +| 正常下降 | best loss 持续改善 | 保持优化器 | +| flat plateau | 近期梯度中位数低于早期参考的 `10%`,物理位移小 | LR kick;失败后 perturb | +| oscillatory plateau | 梯度未衰减、频繁符号翻转或 loss 往复 | LR 降至 `0.001`,冷却至少 20 updates | +| slow progress | loss 趋势和参数位移仍一致 | 延长预算或正常退火 | +| spike feasible | count 正确,timing/trace 未收敛 | 小 LR、小扰动、完整 loss | +| 等价低损失解 | held-out 也成功但参数分散 | 报告不可辨识集合 | + +flat plateau 的第一层恢复为 `restart_lr=0.02`、`kick_updates=10`。每条 start 独立维护 +plateau、cooldown 和 recovery 状态,不能由 batch mean loss 统一触发。 + + +## 非局部恢复 + +### Cosine 与 SGDR + +普通 cosine decay 适合正确 basin 内收敛;SGDR 可跨浅 barrier,但不会在严格零梯度区 +创造方向。固定周期消融使用: + +```text +base_lr = 0.02 +eta_min = 0.001 +T_0 = 30 updates +T_mult = 2 +total = 180 updates # restart at update 30 and 90 +``` + +`lr_restart_only` 保留 Adam moments;`lr_and_moment_restart` 清空 moments,二者必须分开 +消融。所有 restart 都依赖 best archive。 + +历史 effective-LR 缺陷观察已迁至 [结果记录](../current/results/fitting-and-identifiability.md#scheduler-的历史缺陷观察);正式使用前仍须回归验证。 + +### Perturb-and-select + +扰动在 bounded sigmoid 前的无约束 `z` 空间执行: + +```text +radii = (0.1, 0.25, 0.5) +candidates_per_radius = 8 +incumbent = 1 +total_forward_candidates = 25 +z_candidate = z_checkpoint + radius * normalized_direction +``` + +direction 由 `brainstate.random` 生成并归一化;random key 按 start 和 recovery event 独立。 +候选经过 transform 后仅做批量 forward,不保留反向图。接受顺序为: + +1. finite 优先; +2. `count_distance` 更小优先; +3. distance 相同而 signature 不同时,优先减少缺失 spike 的 protocol 数,并记录此选择; +4. signature 相同时,Composite loss 至少相对改善 `0.5%`; +5. loss 并列时选离 incumbent 更近的候选; +6. 完全并列时选固定 candidate index。 + +feasible incumbent 默认不能被 infeasible candidate 替换;`allow_feasible_escape` 只能作为 +显式消融,且不得清除 feasible archive。接受 jump 后写入参数、reset dynamic state、清空 +Adam moments、重启 LR phase并记录 region transition;无改善则保留 incumbent 并 cooldown。 + +### 更高成本入口 + +全局筛选以 1024 个 optimizer-space 候选运行 forward,再选择 16 个多样化 starts。候选 +必须包含原八个角点、`z=0`、确定性 low-discrepancy 点和用户先验;选择同时考虑 signature、 +loss、距离、bound proximity 与 finite,不能只取同一 compensation valley 中 loss 最低的 +16 点。 + +curriculum 则依次引入 subthreshold/multiscale/smooth peak、threshold margin/event、count/ +latency/alignment、AP shape/AHP/full trace,最后降低 surrogate temperature 并低 LR 精修。 +它与全局筛选、SGDR 和 perturb 必须分别消融。 + +常见替代方案的边界: + +| 方法 | 不能替代恢复策略的原因 | +| --- | --- | +| 只提高固定 LR | 对严格零梯度无效,在 spike boundary 上更不稳定 | +| AdamW | 对无约束 `z` 的 decay 会拉向 physical bounds 中点,不等于生理先验 | +| L-BFGS | 适合正确 basin 内精修,不提供全局逃逸方向 | +| parameter averaging | 两个可行参数的均值可能位于错误 spike region | +| 只增加 epochs | 只帮助仍在移动的轨迹,不能保证离开错误 basin | +| 只保留 batch best | 隐藏其他 starts 的失败和 basin robustness | + + +## 渐进加入顺序 + +| Stage | Module | 是否改变训练 | 目的 / 状态 | +| ---: | --- | --- | --- | +| 0 | manifest | 否 | 固定环境与配置;metadata 已支持 | +| 1 | observer + evaluator | 否 | 梯度、位移、region、finite;实验版已实现 | +| 2 | dual archives | 仅模型选择 | continuous/feasible best;历史提取已实现 | +| 3 | protocol suite + held-out | 数据/评价 | 泛化与可辨识性 | +| 4 | loss components | 是 | 逐项消融 voltage/count/timing/shape | +| 5 | initializer | 是 | LHS/Sobol/先验与 basin diversity | +| 6 | optimizer policy | 是 | LR、schedule、optimizer space | +| 7 | plateau controller | 是 | 区分 flat/oscillatory/slow | +| 8 | perturb-and-select | 是 | 显式跨 basin | +| 9 | identifiability | 否 | profile、Hessian/Fisher、compensation valley | +| 10 | performance | 否 | compile、step time、memory、throughput | + +每次消融只改变一个行为模块,固定 starts、updates、protocol 和评价规则;额外 forward 单独 +计数。报告所有 starts 的 success rate,而非 batch best。正式比较预算见 +[优化消融协议](#固定条件与-baseline)。 + +## 必须验证的边界 + +- target 必须经同一 hard evaluator 得到 `(1, 2, 3, 4)`; +- non-finite trace 使用 invalid region,不能以极大整数参与普通距离; +- continuous-best loss 更低但 count 错误时不能覆盖 feasible-best; +- Adam 离开可行区或 resume 后,feasible archive 仍保持一致; +- count 相同但 spike 配对错误时 timing metric 必须失败; +- window 边缘 crossing 只计一次,必要时显式定义 refractory; +- CPU/GPU 边界差异必须随 backend、precision 一起报告; +- scheduler、plateau、random key、optimizer moments 和 archives 都属于完整 resume 状态。 + + +## 消融协议 + +以下保留原固定协议,不因文档迁移而变成已经执行的结果。 + +## 固定条件与 Baseline + +| 类别 | 固定值 | +| --- | --- | +| morphology | soma、`dend_a`、`dend_b` 三 compartment | +| 参数 | 全 compartment 共享 leak、HH sodium、HH potassium `g_max` | +| target | `(0.6, 120, 36) mS/cm^2` | +| 数据 | 相同四协议、三个 voltage probes;每次 `100 ms`,`dt=0.025 ms` | +| 参数化/loss | 同一 bounded sigmoid、Composite components 和 normalizers | +| starts | 同一八个 `2 x 2 x 2` physical initial points | +| 执行 | CPU、batch=8;每次 rollout 前 reset dynamic state,无 warm-up | +| optimizer | Adam,`betas=(0.9, 0.999)`,global clip norm `1.0` | + +正式比较必须重跑 CPU baseline,不能用旧 GPU 结果逐值替代。spike boundary 附近的浮点 +差异可能改变轨迹。 + +历史 Adam promotion 数值见 [结果页](../current/results/fitting-and-identifiability.md#消融的历史参考点);不把它当作本轮已执行的 CPU baseline。 + +## 方法与预算 + +Stage 1 对所有方法使用相同八个 starts、一个 seed、batch=8 和 180 optimizer updates: + +| ID | 方法 | LR / Recovery | 隔离变量 | +| --- | --- | --- | --- | +| A | Adam baseline | fixed `0.02` | 公平 CPU baseline | +| B | cosine decay | `0.02 -> 0.001`,无 restart | 后期稳定性 | +| C | periodic SGDR | `eta_min=0.001, T_0=30, T_mult=2` | 周期 restart | +| D | plateau LR | flat kick / oscillatory cooldown,无 perturb | 自适应 LR | +| E | perturb-and-select | fixed Adam + plateau perturb,无 SGDR | 非局部跳跃 | +| F | combined | cosine/SGDR、adaptive recovery、双 archive | 完整策略 | + +第一轮不加入 1024-point screening 或 loss curriculum。E/F 的扰动使用 +`brainstate.random(seed=0)`;所有方法保存 continuous-best,D/E/F 保存 recovery events, +F 还保存 spike-feasible-best。perturb forward 数单列,不能视作免费预算。 + +Stage 2 选择两个非 baseline 方法,运行 360 updates、seeds `0, 1, 2`,并保留 180-update +截面。包含周期 restart 的方法必须覆盖至少两个完整周期和 restart 后收敛窗口;每个 seed +单独报告 region transitions。 + +### Promotion + +候选必须同时满足: + +```text +trace_success >= 5 / 8 +parameter_success >= 4 / 8 +median_common_loss <= 0.8 * CPU_baseline_median +best_common_loss <= 1.1 * CPU_baseline_best +all endpoints finite +``` + +基于旧 baseline,median 阈值约为 `0.1881`;正式值必须由本轮 CPU baseline 计算。Stage 2 +选择顺序为 trace success、spike-feasible starts、median common loss、median aggregate RMSE、 +parameter success、总 forward 和 wall time,不能按单个 best start 晋级。 + +## 实现前测试矩阵 + +| 子系统 | 必须覆盖的场景 | 阻断条件 | +| --- | --- | --- | +| effective LR | 单参数、常梯度 SGD;eager、JIT、`for_loop`、state-aware `vmap` 序列一致 | reported LR 与实际 delta 不一致时阻断 C/F | +| restart/resume | restart 精确在 30、90;resume 后 LR 连续;两种 moment policy 分离 | 中断与连续运行不同 | +| plateau | 单调下降、warmup、25-update patience、0.5% gain、flat/oscillatory/slow、cooldown、最多三次 recovery、NaN/Inf | 状态不能作为 fixed-shape JAX pytree | +| archive/region | update 前 loss 对齐 `trajectory[t]`;final/continuous/feasible 可在不同 epoch;target `(1,2,3,4)`;timing tie `0.025 ms` | infeasible 或 non-finite 覆盖 feasible-best | +| perturb | 三个 radii x 八候选 + incumbent 为 `(25,3)`;seed、bounds、独立 key、0.5% 接受阈值、moment reset | 无改善时污染 incumbent/optimizer state | +| integration | 真实三-compartment、四协议,2 starts、4--8 updates、每 radius 2 candidates | accepted 参数未进入下一 rollout | + +常梯度 scheduler fixture 为 `parameter=0, gradient=1, base_lr=0.1, T_0=2, +T_mult=1, eta_min=0.01`;每次 actual parameter delta 必须等于该 update 的 schedule LR。 +真实集成测试仍使用 `brainstate.transform.for_loop`/`vmap`,controller 数组测试与昂贵 rollout +分离。 + +## 记录与输出 + +每个 method/start/seed 保存: + +| 类别 | 内容 | +| --- | --- | +| histories | total/component loss、optimizer gradients、effective LR、physical/optimizer 参数 | +| region | signature、signed error、region transitions、plateau/cooldown/restart events | +| archives | continuous-best、spike-feasible-best、initial/final/best traces | +| perturb | candidate summary、accepted jump、额外 forward 数 | +| performance | compile、training、recovery evaluation、total wall time、backend、precision | + +SGDR 图必须画 effective LR。最小输出集合为: + +| 图/表 | 回答的问题 | +| --- | --- | +| per-start loss/LR/restart 与参数轨迹 | 是否稳定、何时恢复 | +| signature timeline 与 transition matrix | 是否进入并保持正确 region | +| continuous vs feasible archive | 连续目标是否偏离 hard 成功条件 | +| method x start 指标表 | 收益是否覆盖多数 basin | +| perturb 局部 landscape | jump 为什么被接受 | +| success-cost Pareto | 额外 forward 是否值得 | +| 180/360 截面 | 延长预算还是策略带来收益 | +| CPU/旧 GPU 摘要 | backend 差异有多大 | + +## 停止与后续 + +| 条件 | 处理 | +| --- | --- | +| scheduler 实际 LR 测试失败 | 阻断 SGDR 方法 | +| start non-finite 且无法恢复 finite checkpoint | 标记该 start 失败,其余继续 | +| 三次 recovery 无改善 | 停止该 start 的恢复,保留 archives | +| 相同 signature/loss 但参数分散 | 转入 identifiability 分析 | +| 只改善 best start、不提高成功率 | 不晋升默认流程 | +| 收益使用超过两倍 forward | 报告结果,但不宣称同成本优势 | +| 360-update 终点差于 180 checkpoint | 先审计 restart、archive 和 resume 语义 | + +矩阵完成后再独立测试:1024-point screening + 16 starts、spike-aware curriculum、Adam +`beta2=0.99`/RAdam/L-BFGS、held-out amplitude/location、adaptive landscape refinement,以及 +更多参数或 density coefficients。不得同时塞入方法 F,否则无法归因。 + +## 方法来源 + +原恢复参考的 SGDR、basin-hopping、CMA-ES 与 curriculum 引文见 +[文献列表](../references/modular-training-diagnostics.md#references)。 diff --git a/docs/design/optim/references/bptt-to-rtrl-neuron-derivation.md b/docs/design/optim/references/bptt-to-rtrl-neuron-derivation.md index 781b6621..887e450e 100644 --- a/docs/design/optim/references/bptt-to-rtrl-neuron-derivation.md +++ b/docs/design/optim/references/bptt-to-rtrl-neuron-derivation.md @@ -572,6 +572,11 @@ Optimizer 若逐步消费梯度,应消费本步新增 contribution,而不是 - cell-generated spike 经 ring buffer 反馈到未来状态; - loss 直接依赖 hard event 或 continuous event time。 +当前实验实现已经把 cell-generated floating event 和固定 delay ring buffers 纳入全状态 +递推,比较时使用同一 surrogate;见 [网络验证](../current/results/synapse-network-learning.md)。 +这覆盖第二项的已测模型和 surrogate event loss,不支持 trainable delay,也不等于具有 +continuous event-time root-finding 的导数。多 CV 和多 population 分别有证据,任意组合尚未穷举。 + ## 7. Exact RTRL 与 e-prop 的边界 Exact full-state RTRL 直接保存: @@ -611,9 +616,10 @@ derivative 或 eligibility locality。 | 每步微分 | 一个 VJP/cotangent | $N_\theta$ 个 tangent directions | | Prefix gradient | 需要 reverse prefix | 前向时立即可得 | -Checkpoint/rematerialization 可以用重算降低 BPTT tape。RTRL memory 与时间长度无关,但 +Checkpoint/rematerialization 可以用重算降低 BPTT tape。RTRL 的递归 sensitivity carry 与时间长度无关,但 推进全部 parameter directions 的计算和 carry 会随 $N_\theta$ 增长。具体墙钟还由 solver、 硬件并行度、batch、静态 shape 和编译器调度决定,不能仅由大 O 排序。 +完整输入、逐步 loss 或显式输出的 sensitivity history 仍可随 T 增长;不要把 carry 与进程内存混用。 以下条件下,RTRL 与 full BPTT 对同一个离散 objective 完全等价: @@ -624,8 +630,9 @@ Checkpoint/rematerialization 可以用重算降低 BPTT tape。RTRL memory 与 5. local loss、direct term 和 learning signal 被完整计入; 6. 两者对同一个实际离散 solver program 求导。 -连续 HH 动作电位属于 $v/w$ 动力学。只要 loss 读取 voltage trace 而不读取离散 spike -readout,梯度不需要经过 surrogate event。具体 solver 和 readout 的程序导数见独立 solver +连续 HH 动作电位属于 $v/w$ 动力学。在没有 event feedback 的模型中,若 loss 只读取 +voltage trace 而不读取离散 spike readout,梯度不需要经过 surrogate event;有突触反馈时 +voltage loss 仍可能经过检测器。具体 solver 和 readout 的程序导数见独立 solver 文档。 ## 9. 建议的 PPT 页面顺序 diff --git a/docs/design/optim/references/jaxley-parameter-model.md b/docs/design/optim/references/jaxley-parameter-model.md index 08976cc4..36f4e01d 100644 --- a/docs/design/optim/references/jaxley-parameter-model.md +++ b/docs/design/optim/references/jaxley-parameter-model.md @@ -2,6 +2,9 @@ ## 文档定位 +采用状态:参数选择与 replacement 的取舍已体现在 [当前架构](../current/architecture.md); +具体调用见 [当前 API](../current/api.md)。这不表示 BrainCell 采用 Jaxley 的全部分组或存储协议。 + 本文解释 BrainCell trainable architecture 对 Jaxley 参数选择与 replacement 的取舍,不定义 BrainCell API。调研固定在 Jaxley commit `2638cca2665ec056c40c932dcee924192fc94da2`; 后续版本可能不同,BrainCell 不依赖其私有实现。 diff --git a/docs/design/optim/references/modular-training-diagnostics.md b/docs/design/optim/references/modular-training-diagnostics.md index 93fe3e89..cc67606b 100644 --- a/docs/design/optim/references/modular-training-diagnostics.md +++ b/docs/design/optim/references/modular-training-diagnostics.md @@ -2,28 +2,17 @@ ## 文档定位 -本文是参数学习实验的非规范性 reference,统一定义观测、归档、spike-region 和非凸恢复 -合同;不定义 BrainCell 公共 API,也不引入 Trainer。当前实验实现位于 +本文解释观测、归档与 spike-region 的方法,不定义 BrainCell 公共 API,也不引入 Trainer。 +方法建议不代表每项均已实现;当前实验能力以 [实验工作流](../current/experimental-workflows.md) 为准。 +当前实验实现位于 [`diagnostics.py`](../../../../examples/experimental/optim_parameter_fitting/diagnostics.py)。 参数选择与 runtime 映射仍由 `braincell.trainable` 负责,优化器由 BrainTools 或用户代码 负责。 -## 当前证据 +## 历史证据 -默认 1-CV HH 基线以 `RandomState(seed=123)` 产生 32 个 initialization starts,并在固定 -完整数据上运行 Adam;这些 lane 不是独立随机实验。一次 100-update 运行得到: - -| 观测 | 结果 | -| --- | ---: | -| 终点 loss 低于初值 | `32/32` | -| 三个电导 scale 平均相对误差 `<10%` | `10/32` | -| best loss 出现在最终 update 以前 | `19/32` | -| final loss 比 best 高 `>10%` | `5/32` | -| 接近 `[0.1, 2.0]` transform bounds | `0/32` | -| final MSE | 中位数 `4.7627 mV^2`,范围 `0.0923--44.4395 mV^2` | - -因此不能用 final loss 或 bound saturation 单独解释失败。至少要区分 spike basin、低梯度 -平原、高梯度震荡、慢速移动、spike phase 误差和 `gNa/gK` 补偿。 +32-start 基线及已观察的 spike signatures 见 +[拟合与可辨识性结果](../current/results/fitting-and-identifiability.md#诊断基线)。 ## 可组合观测合同 @@ -76,15 +65,8 @@ best / final loss、best epoch 和可选 parameter error。分类阈值集中保 ## Spike Region -固定 protocol 下,spike count 在参数区域内是整数常量,跨兴奋性边界时跳变。当前四协议 -目标及已观察失败示例为: - -| 类型 | Signature | -| --- | --- | -| target | `(1, 2, 3, 4)` | -| low excitability | `(1, 1, 1, 2)` | -| mixed mismatch | `(1, 1, 3, 4)` | -| high excitability | `(2, 3, 4, 5)` | +固定 protocol 下,spike count 在参数区域内是整数常量,跨兴奋性边界时跳变。 +观测过的 signature 示例保存于结果页;下述记录与评价是方法约定。 每次 forward 至少记录: @@ -108,38 +90,17 @@ component_losses 梯度始终来自连续或 surrogate component。region 只允许控制下一阶段的固定形状 loss 配置。 -## 状态分类与动作 - -plateau 以 best loss 的相对改善为主,并使用每条轨迹自身的梯度和位移尺度: - -```text -warmup_updates = 40 -patience = 25 -relative_improvement = 0.005 -cooldown = 20 -max_recoveries = 3 -epsilon = 1e-8 - -relative_gain = (old_best - new_best) / max(abs(old_best), epsilon) -``` - -只有 warmup 后连续 25 updates 未达到 `0.5%` 改善、不在 cooldown 且 recovery 未达三次 -时才判定 plateau。单次 raw-loss 上升或一次 spike-boundary 抖动不触发恢复。 +## 恢复动作的边界 -| 状态 | 主要证据 | 下一动作 | -| --- | --- | --- | -| 正常下降 | best loss 持续改善 | 保持优化器 | -| flat plateau | 近期梯度中位数低于早期参考的 `10%`,物理位移小 | LR kick;失败后 perturb | -| oscillatory plateau | 梯度未衰减、频繁符号翻转或 loss 往复 | LR 降至 `0.001`,冷却至少 20 updates | -| slow progress | loss 趋势和参数位移仍一致 | 延长预算或正常退火 | -| spike feasible | count 正确,timing/trace 未收敛 | 小 LR、小扰动、完整 loss | -| 等价低损失解 | held-out 也成功但参数分散 | 报告不可辨识集合 | - -flat plateau 的第一层恢复为 `restart_lr=0.02`、`kick_updates=10`。每条 start 独立维护 -plateau、cooldown 和 recovery 状态,不能由 batch mean loss 统一触发。 +诊断观测不自动改变训练。plateau 阈值、LR kick、SGDR、perturb、恢复顺序与验收矩阵 +属于 [训练恢复提案](../proposals/training-recovery.md),尚未实现为自动 controller。 ## Archive 与模型选择 +以下是 archive 与选择的方法约定;当前已实现的是训练后历史提取。 +timing tie-break、在线维护及恢复控制不能由这些建议推断为现成 API,具体边界见 +[实验工作流](../current/experimental-workflows.md#诊断与历史) 与 [恢复提案](../proposals/training-recovery.md)。 + 每条 start 维护两个固定 shape archive: | Archive | 更新条件 | 用途 | @@ -160,77 +121,6 @@ held-out 模型选择依次比较:train/held-out finite、held-out signature voltage、train Composite loss、生理先验或不确定性。synthetic 数据同时报告 trace 和 parameter success;真实数据不能把不可见的真参数作为唯一标准。 -## 非局部恢复 - -### Cosine 与 SGDR - -普通 cosine decay 适合正确 basin 内收敛;SGDR 可跨浅 barrier,但不会在严格零梯度区 -创造方向。固定周期消融使用: - -```text -base_lr = 0.02 -eta_min = 0.001 -T_0 = 30 updates -T_mult = 2 -total = 180 updates # restart at update 30 and 90 -``` - -`lr_restart_only` 保留 Adam moments;`lr_and_moment_restart` 清空 moments,二者必须分开 -消融。所有 restart 都依赖 best archive。 - -`braintools 0.1.9` 的 `CosineAnnealingWarmRestarts` 曾出现 reported LR 更新但实际参数增量 -仍固定的问题(`base_lr=0.1, T_0=2, eta_min=0.01` 时报告 `0.1, 0.055, ...`,实际 delta -始终 `-0.1`)。正式使用前必须以常梯度回归测试验证 effective LR;临时 controller 只放 -在 example 内。 - -### Perturb-and-select - -扰动在 bounded sigmoid 前的无约束 `z` 空间执行: - -```text -radii = (0.1, 0.25, 0.5) -candidates_per_radius = 8 -incumbent = 1 -total_forward_candidates = 25 -z_candidate = z_checkpoint + radius * normalized_direction -``` - -direction 由 `brainstate.random` 生成并归一化;random key 按 start 和 recovery event 独立。 -候选经过 transform 后仅做批量 forward,不保留反向图。接受顺序为: - -1. finite 优先; -2. `count_distance` 更小优先; -3. distance 相同而 signature 不同时,优先减少缺失 spike 的 protocol 数,并记录此选择; -4. signature 相同时,Composite loss 至少相对改善 `0.5%`; -5. loss 并列时选离 incumbent 更近的候选; -6. 完全并列时选固定 candidate index。 - -feasible incumbent 默认不能被 infeasible candidate 替换;`allow_feasible_escape` 只能作为 -显式消融,且不得清除 feasible archive。接受 jump 后写入参数、reset dynamic state、清空 -Adam moments、重启 LR phase并记录 region transition;无改善则保留 incumbent 并 cooldown。 - -### 更高成本入口 - -全局筛选以 1024 个 optimizer-space 候选运行 forward,再选择 16 个多样化 starts。候选 -必须包含原八个角点、`z=0`、确定性 low-discrepancy 点和用户先验;选择同时考虑 signature、 -loss、距离、bound proximity 与 finite,不能只取同一 compensation valley 中 loss 最低的 -16 点。 - -curriculum 则依次引入 subthreshold/multiscale/smooth peak、threshold margin/event、count/ -latency/alignment、AP shape/AHP/full trace,最后降低 surrogate temperature 并低 LR 精修。 -它与全局筛选、SGDR 和 perturb 必须分别消融。 - -常见替代方案的边界: - -| 方法 | 不能替代恢复策略的原因 | -| --- | --- | -| 只提高固定 LR | 对严格零梯度无效,在 spike boundary 上更不稳定 | -| AdamW | 对无约束 `z` 的 decay 会拉向 physical bounds 中点,不等于生理先验 | -| L-BFGS | 适合正确 basin 内精修,不提供全局逃逸方向 | -| parameter averaging | 两个可行参数的均值可能位于错误 spike region | -| 只增加 epochs | 只帮助仍在移动的轨迹,不能保证离开错误 basin | -| 只保留 batch best | 隐藏其他 starts 的失败和 basin robustness | - ## Region-aware loss 与可视化 | Region | 连续目标调整 | 搜索约束 | @@ -256,36 +146,10 @@ summary.json per-start classification and aggregate counts 不按每个 epoch 保存,避免 `time x start x epoch` 膨胀;只保留 initial/final/best 或另设采样 模块。 -## 渐进加入顺序 - -| Stage | Module | 是否改变训练 | 目的 / 状态 | -| ---: | --- | --- | --- | -| 0 | manifest | 否 | 固定环境与配置;metadata 已支持 | -| 1 | observer + evaluator | 否 | 梯度、位移、region、finite;实验版已实现 | -| 2 | dual archives | 仅模型选择 | continuous/feasible best;历史提取已实现 | -| 3 | protocol suite + held-out | 数据/评价 | 泛化与可辨识性 | -| 4 | loss components | 是 | 逐项消融 voltage/count/timing/shape | -| 5 | initializer | 是 | LHS/Sobol/先验与 basin diversity | -| 6 | optimizer policy | 是 | LR、schedule、optimizer space | -| 7 | plateau controller | 是 | 区分 flat/oscillatory/slow | -| 8 | perturb-and-select | 是 | 显式跨 basin | -| 9 | identifiability | 否 | profile、Hessian/Fisher、compensation valley | -| 10 | performance | 否 | compile、step time、memory、throughput | - -每次消融只改变一个行为模块,固定 starts、updates、protocol 和评价规则;额外 forward 单独 -计数。报告所有 starts 的 success rate,而非 batch best。正式比较预算见 -[优化消融协议](optimization-ablation-protocol.md)。 - -## 必须验证的边界 - -- target 必须经同一 hard evaluator 得到 `(1, 2, 3, 4)`; -- non-finite trace 使用 invalid region,不能以极大整数参与普通距离; -- continuous-best loss 更低但 count 错误时不能覆盖 feasible-best; -- Adam 离开可行区或 resume 后,feasible archive 仍保持一致; -- count 相同但 spike 配对错误时 timing metric 必须失败; -- window 边缘 crossing 只计一次,必要时显式定义 refractory; -- CPU/GPU 边界差异必须随 backend、precision 一起报告; -- scheduler、plateau、random key、optimizer moments 和 archives 都属于完整 resume 状态。 +## 实现与验证入口 + +已实现的 history 与训练后 archive 见 [实验工作流](../current/experimental-workflows.md#诊断与历史)。 +会改变训练的恢复状态、在线 archive、完整 resume 和候选接受规则见提案,不是本页的现行 API。 ## References diff --git a/docs/design/optim/references/optimization-ablation-protocol.md b/docs/design/optim/references/optimization-ablation-protocol.md deleted file mode 100644 index 6bf074d8..00000000 --- a/docs/design/optim/references/optimization-ablation-protocol.md +++ /dev/null @@ -1,129 +0,0 @@ -# 非凸优化恢复的消融协议 - -## 文档定位 - -本文记录四协议、三电导实验的固定比较合同,不定义 BrainCell 公共 API,也不重复解释 -controller 机制。状态分类、双 archive、SGDR、perturb 和 spike-region 规则见 -[模块化训练诊断与优化恢复](modular-training-diagnostics.md)。本轮只定义协议;未经单独批准 -不修改训练 API,也不运行正式消融。 - -## 固定条件与 Baseline - -| 类别 | 固定值 | -| --- | --- | -| morphology | soma、`dend_a`、`dend_b` 三 compartment | -| 参数 | 全 compartment 共享 leak、HH sodium、HH potassium `g_max` | -| target | `(0.6, 120, 36) mS/cm^2` | -| 数据 | 相同四协议、三个 voltage probes;每次 `100 ms`,`dt=0.025 ms` | -| 参数化/loss | 同一 bounded sigmoid、Composite components 和 normalizers | -| starts | 同一八个 `2 x 2 x 2` physical initial points | -| 执行 | CPU、batch=8;每次 rollout 前 reset dynamic state,无 warm-up | -| optimizer | Adam,`betas=(0.9, 0.999)`,global clip norm `1.0` | - -正式比较必须重跑 CPU baseline,不能用旧 GPU 结果逐值替代。spike boundary 附近的浮点 -差异可能改变轨迹。 - -现有 fixed Adam `lr=0.02`、180-update 结果只作为 promotion reference,不是测试常量: - -| 指标 | 当前值 | -| --- | ---: | -| trace success | `3/8` | -| parameter success | `4/8` | -| median common loss | `0.235153` | -| median aggregate RMSE | `7.8201 mV` | -| median mean parameter error | `0.1562` | -| best common loss | `0.0153285` | - -## 方法与预算 - -Stage 1 对所有方法使用相同八个 starts、一个 seed、batch=8 和 180 optimizer updates: - -| ID | 方法 | LR / Recovery | 隔离变量 | -| --- | --- | --- | --- | -| A | Adam baseline | fixed `0.02` | 公平 CPU baseline | -| B | cosine decay | `0.02 -> 0.001`,无 restart | 后期稳定性 | -| C | periodic SGDR | `eta_min=0.001, T_0=30, T_mult=2` | 周期 restart | -| D | plateau LR | flat kick / oscillatory cooldown,无 perturb | 自适应 LR | -| E | perturb-and-select | fixed Adam + plateau perturb,无 SGDR | 非局部跳跃 | -| F | combined | cosine/SGDR、adaptive recovery、双 archive | 完整策略 | - -第一轮不加入 1024-point screening 或 loss curriculum。E/F 的扰动使用 -`brainstate.random(seed=0)`;所有方法保存 continuous-best,D/E/F 保存 recovery events, -F 还保存 spike-feasible-best。perturb forward 数单列,不能视作免费预算。 - -Stage 2 选择两个非 baseline 方法,运行 360 updates、seeds `0, 1, 2`,并保留 180-update -截面。包含周期 restart 的方法必须覆盖至少两个完整周期和 restart 后收敛窗口;每个 seed -单独报告 region transitions。 - -### Promotion - -候选必须同时满足: - -```text -trace_success >= 5 / 8 -parameter_success >= 4 / 8 -median_common_loss <= 0.8 * CPU_baseline_median -best_common_loss <= 1.1 * CPU_baseline_best -all endpoints finite -``` - -基于旧 baseline,median 阈值约为 `0.1881`;正式值必须由本轮 CPU baseline 计算。Stage 2 -选择顺序为 trace success、spike-feasible starts、median common loss、median aggregate RMSE、 -parameter success、总 forward 和 wall time,不能按单个 best start 晋级。 - -## 实现前测试矩阵 - -| 子系统 | 必须覆盖的场景 | 阻断条件 | -| --- | --- | --- | -| effective LR | 单参数、常梯度 SGD;eager、JIT、`for_loop`、state-aware `vmap` 序列一致 | reported LR 与实际 delta 不一致时阻断 C/F | -| restart/resume | restart 精确在 30、90;resume 后 LR 连续;两种 moment policy 分离 | 中断与连续运行不同 | -| plateau | 单调下降、warmup、25-update patience、0.5% gain、flat/oscillatory/slow、cooldown、最多三次 recovery、NaN/Inf | 状态不能作为 fixed-shape JAX pytree | -| archive/region | update 前 loss 对齐 `trajectory[t]`;final/continuous/feasible 可在不同 epoch;target `(1,2,3,4)`;timing tie `0.025 ms` | infeasible 或 non-finite 覆盖 feasible-best | -| perturb | 三个 radii x 八候选 + incumbent 为 `(25,3)`;seed、bounds、独立 key、0.5% 接受阈值、moment reset | 无改善时污染 incumbent/optimizer state | -| integration | 真实三-compartment、四协议,2 starts、4--8 updates、每 radius 2 candidates | accepted 参数未进入下一 rollout | - -常梯度 scheduler fixture 为 `parameter=0, gradient=1, base_lr=0.1, T_0=2, -T_mult=1, eta_min=0.01`;每次 actual parameter delta 必须等于该 update 的 schedule LR。 -真实集成测试仍使用 `brainstate.transform.for_loop`/`vmap`,controller 数组测试与昂贵 rollout -分离。 - -## 记录与输出 - -每个 method/start/seed 保存: - -| 类别 | 内容 | -| --- | --- | -| histories | total/component loss、optimizer gradients、effective LR、physical/optimizer 参数 | -| region | signature、signed error、region transitions、plateau/cooldown/restart events | -| archives | continuous-best、spike-feasible-best、initial/final/best traces | -| perturb | candidate summary、accepted jump、额外 forward 数 | -| performance | compile、training、recovery evaluation、total wall time、backend、precision | - -SGDR 图必须画 effective LR。最小输出集合为: - -| 图/表 | 回答的问题 | -| --- | --- | -| per-start loss/LR/restart 与参数轨迹 | 是否稳定、何时恢复 | -| signature timeline 与 transition matrix | 是否进入并保持正确 region | -| continuous vs feasible archive | 连续目标是否偏离 hard 成功条件 | -| method x start 指标表 | 收益是否覆盖多数 basin | -| perturb 局部 landscape | jump 为什么被接受 | -| success-cost Pareto | 额外 forward 是否值得 | -| 180/360 截面 | 延长预算还是策略带来收益 | -| CPU/旧 GPU 摘要 | backend 差异有多大 | - -## 停止与后续 - -| 条件 | 处理 | -| --- | --- | -| scheduler 实际 LR 测试失败 | 阻断 SGDR 方法 | -| start non-finite 且无法恢复 finite checkpoint | 标记该 start 失败,其余继续 | -| 三次 recovery 无改善 | 停止该 start 的恢复,保留 archives | -| 相同 signature/loss 但参数分散 | 转入 identifiability 分析 | -| 只改善 best start、不提高成功率 | 不晋升默认流程 | -| 收益使用超过两倍 forward | 报告结果,但不宣称同成本优势 | -| 360-update 终点差于 180 checkpoint | 先审计 restart、archive 和 resume 语义 | - -矩阵完成后再独立测试:1024-point screening + 16 starts、spike-aware curriculum、Adam -`beta2=0.99`/RAdam/L-BFGS、held-out amplitude/location、adaptive landscape refinement,以及 -更多参数或 density coefficients。不得同时塞入方法 F,否则无法归因。 diff --git a/docs/design/optim/references/staggered-solver-gradient-analysis.md b/docs/design/optim/references/staggered-solver-gradient-analysis.md index 4a7f7e8e..1478bb2c 100644 --- a/docs/design/optim/references/staggered-solver-gradient-analysis.md +++ b/docs/design/optim/references/staggered-solver-gradient-analysis.md @@ -8,7 +8,7 @@ derivative,不重复 BPTT、RTRL、online update 或多步 loss 理论。通 [BPTT/RTRL 理论](./bptt-to-rtrl-neuron-derivation.md)。 本文是非规范性技术分析,不定义 Trainable Parameter API。规范性参数合同见 -[API](../api.md) 和 [Architecture](../architecture.md)。 +[API](../current/api.md) 和 [Architecture](../current/architecture.md)。 实现入口: @@ -376,15 +376,19 @@ Ordinary Hines 与 recursive-doubling backsub 目标是同一线性系统,理 $$ z_+=\frac{v_{n+1}-v_{\mathrm{th}}}{\Delta v_s}, \qquad -z_-=\frac{v_{\mathrm{th}}-v_n}{\Delta v_s}, +z_-=\frac{v_n-v_{\mathrm{th}}}{\Delta v_s}, $$ $$ o_{n+1}^{\mathrm{spike}} = -H_{\mathrm{sg}}(z_+)H_{\mathrm{sg}}(z_-). +H_{\mathrm{sg}}(z_+)\left(1-H_{\mathrm{sg}}(z_-)\right). $$ +这里 Delta v_s 为 20 mV,hard step 在零点取 1,因此旧电压必须严格小于阈值, +新电压可以等于阈值。离开等值点不会再次发放;完整升/降沿合同见 +[事件架构](../current/architecture.md#event-derivatives)。 + 默认 `ReluGrad(alpha=0.3, width=1)` 在 forward 使用 hard step,在 backward 使用有限支撑 三角 slope: @@ -397,7 +401,7 @@ $$ $$ \frac{\widetilde\partial o_{n+1}^{\mathrm{spike}}}{\partial v_{n+1}} = -H(z_-)\frac{\rho(z_+)}{\Delta v_s}, +\left(1-H(z_-)\right)\frac{\rho(z_+)}{\Delta v_s}, $$ $$ @@ -407,8 +411,9 @@ $$ $$ 该 surrogate 只有在 loss 读取 `Cell.spike`、filtered event trace,或 spike feedback 影响未来 -state 时才进入目标路径。单纯拟合连续 voltage trace 时,动作电位仍由连续 $v/w$ dynamics -产生,梯度不经过这个 readout。 +state 时才进入目标路径。没有 event feedback、且 loss 只读取连续 voltage trace 时, +动作电位由连续 $v/w$ dynamics 产生,梯度不经过这个 readout;有突触反馈时即使只读 +voltage,跨 Cell 路径也可能经过 surrogate。 ## 10. Solver-gradient 验证 diff --git a/docs/design/optim/references/stimulus-design-and-identifiability.md b/docs/design/optim/references/stimulus-design-and-identifiability.md index 43eaaebe..2b61a149 100644 --- a/docs/design/optim/references/stimulus-design-and-identifiability.md +++ b/docs/design/optim/references/stimulus-design-and-identifiability.md @@ -58,136 +58,11 @@ Sensitivity/FIM 不能替代实际训练。完整证据链必须是 `design -> f 即使提高 `lambda_min(F)`,spike boundary、Adam coordinate、预算和 loss weighting 仍可能使 success rate 不改善 [9,10,16,17]。 -## 零号训练基线 +## 历史实验结果 -| 类别 | 固定合同 | -| --- | --- | -| 模型/参数 | 1 soma CV;三个 bounded direct `g_max` | -| target | classical HH `(Leak,Na,K)=(0.3,120,36) mS/cm^2` | -| parameterization | `theta=lower+(upper-lower)*sigmoid(z)`;无frozen scale | -| data | Step-only train/validation/test=`5/2/1`;test final-only | -| loss | protocol/time/CV 等权 raw voltage MSE | -| optimizer | exact RTRL + Adam `lr=0.01`;无 clip/schedule/screening/early stopping | -| starts | 一个seed生成64个physical starts;一次进入同一个kernel和optimizer | -| budget | 180 full-batch epochs,每 epoch 对5条train protocols更新一次 | -| primary success | epoch-180 validation RMSE `<=5 mV` 且每条 validation spike count 正确 | -| secondary | 三参数 relative RMS `<=10%` 与 joint success | - -Validation每10轮记录但不改变trajectory;test只在最终状态评价。A100 x64基线为: - -| 指标 | 结果 | -| --- | ---: | -| trace success | `8/64 = 12.5%` | -| Wilson 95% interval | `[6.47%,22.77%]` | -| parameter / joint success | `3/64 / 1/64` | -| median train MSE | `61.2255 mV^2` | -| median validation / test RMSE | `10.6116 / 17.2393 mV` | -| median parameter relative RMS | `0.2260` | -| compile / stage / end-to-end | `2.85 / 48.88 / 82.50 s` | -| XLA temporary / monitored GPU peak | `2.10 MiB / 1166 MiB` | - -64个endpoint均finite;validation count全对`22/64`,RMSE通过`8/64`,交集`8/64`;test -count全对`35/64`,同时通过5 mV与count为`5/64`。train MSE中位数从`178.5024`降至 -`61.2255 mV^2`。后续改动复用相同initial candidates与预算,不能只展示更好的best case。 - -Python stage pipeline以physical `CandidateSet`在方法间交接:gradient stage在`z`空间工作, -derivative-free stage在bounded normalized coordinate工作;非梯度改变参数后重建Adam moments。 -第一阶段依次单独改变dataset、loss、initialization/search和optimizer。单变量有效后使用 -`baseline / A / B / A+B`: - -```text -interaction = improvement(A+B) - improvement(A) - improvement(B) -``` - -同时报告 paired per-start transition、连续 RMSE、Wilson interval、parameter error、wall time 和 -额外 forward budget;除初始化研究外复用同一64 starts。旧7CV/6-scale、四cohort且holdout混入 -PRMLS的`6/64`结果保留为legacy,不与本基线直接比较。 - -只把Adam预算从180延长到300轮后,trace success从`8/64`增至`10/64`、parameter success从 -`3/64`增至`6/64`,joint仍为`1/64`;validation/test RMSE中位数分别从`10.6116/17.2393` -变为`10.1949/17.1670 mV`。配对迁移为trace `6保持/4新增/2丢失`,joint则丢失原start 37并 -新增start 59。因而增加budget有小幅总体收益,但不能当作lane-wise monotonic recovery;后续方法 -仍需保存best archive并报告固定epoch endpoint。 - -300轮下同时使用Adam `lr=0.02`与`0.1--2.0 x target`宽bounds,并保持64个physical初值逐位 -不变,parameter success从`6/64`增至`14/64`,validation/test RMSE中位数降至 -`8.7019/16.4772 mV`;但trace success从`10/64`降至`5/64`。全部endpoint远离新bounds, -而`3-spike` train count exact仅`1/64`。该双变量组合改善continuous fit和parameter recovery, -却损害spike-region保持;不能据此区分收益来自宽bounds还是高LR。 - -补充bounds-only对照后,`lr=0.01`的wide bounds得到trace/parameter/joint=`9/8/3`,validation/test -RMSE=`8.3472/16.8468 mV`。在相同wide bounds下将LR升到0.02后变为`5/14/1`。因此宽bounds -主要改善continuous fit和parameter recovery;高LR会进一步增加parameter success,但降低trace与 -joint success,表现为更不稳定的spike-region跨越。 - -只替换optimizer为Rprop后,final trace/parameter/joint从Adam的`9/8/3`提高到`35/13/11`, -validation/test RMSE从`8.3472/16.8468`降至`4.4684/9.4214 mV`。Validation-feasible archive -得到validation/test trace success=`38/12`,高于Adam的`22/7`。这支持按gradient符号反转自适应 -缩步比固定moment-based Adam更适合当前deterministic spike-region landscape。 - -BrainTools wrapper把Rprop LR应用两次,使名义`0.01`成为实际initial step `1e-4`,min step成为 -`1e-8`。Single-scale Optax Rprop保持initial `1e-4`但恢复min `1e-6`后,K后50轮median step提高 -约80倍;final trace/parameter/joint为`36/11/10`,archive validation/test trace为`39/12`,与 -wrapper的`35/13/11`和`38/12`接近。重复LR是实现bug且解释了冻结量级,但解除它没有产生额外的 -整体性能跃升,sign-flip零更新仍是后期停滞的主要机制。 - -使用target-std protocol-balanced MSE后,final trace/parameter/joint提高到`43/15/12`,validation/ -test RMSE为`3.8424/9.1728 mV`。Validation archive trace达到`47/64`,但对应test trace为`9/64`, -低于raw-loss archive的`12/64`。权重平衡改善了多数连续指标和validation basin覆盖,但不能替代 -phase-robust loss来保证unseen high-spike protocol泛化。 - -将balanced MSE替换为`delta=5 mV`的MSE-normalized Huber后,final trace/parameter/joint为 -`38/17/15`,archive validation/test trace为`39/13`。相比balanced MSE的`43/15/12`与`47/9`, -Huber牺牲部分validation覆盖,却提高parameter/joint和严格test success,符合其降低大spike-phase -残差主导性的设计目的。它同时把`3-spike` median MSE从`13.995`降到`8.930`,但small-positive -从`7.217`升到`96.045`,不是所有regime同时改善;下一步需避免让线性尾部忽略subthreshold错误。 - -Balanced Huber下的vanilla SGD `lr=1e-4`得到final trace/parameter/joint=`0/2/0`,validation/test -RMSE=`15.1178/21.5266 mV`,显著弱于Rprop。SGD没有贴边或数值发散,但300轮没有形成正确 -validation spike signature;固定幅值gradient descent会稳定下降Huber objective,却缺少Rprop在 -连续同号阶段快速增大per-coordinate step、跨入目标spike basin的能力。 - -加入`momentum=0.9`或Nesterov后,final Huber objective中位数从vanilla SGD的`21.12`降到 -`10.69/11.42`,parameter success提高到`8/11`;但两者final trace/joint仍为0,archive -validation/test trace仅`6/2`与`5/6`。Momentum提高移动速度却不能替代Rprop的per-coordinate -sign adaptation;Nesterov在此任务上也没有稳定优于普通Momentum。 - -## 六参数 Identifiability 结果 - -### Local / Sampled-Prior FIM - -target + 16 Sobol references、33 条 train candidates 得到: - -| 诊断 | 结果 | 解释 | -| --- | ---: | --- | -| relative numerical rank | `6` | 六个 log-conductance 方向均非零可见 | -| worst condition number | 约 `1e7` | 最强/最弱 sensitivity 相差数千倍 | -| worst column correlation | 约 `0.99997` | 严重 regional/Na-K compensation | - -最弱 eigenvector 主要为 soma conductance 减小、dend conductance 增大。满秩不表示 practical -identifiability 良好;加入 33 条 protocol 后仍高度病态。 - -### Forward-Only Global Ensemble - -在 `[log(0.5), log(1.5)]^6` 中评估 16,384 个 scrambled Sobol 参数点,只运行 forward, -保存 per-protocol voltage MSE/hard count 与 raw、normalized train/validation/test score。 -normalizer 来自 target + 16 fixed Sobol prior points 的 per-protocol MSE median,下限 -`1 mV^2`,不随 candidate 或 optimizer 改变。 - -| 比较 | 结果 | -| --- | ---: | -| raw/normalized Top256 intersection | `130` | -| Top256 Jaccard | `0.340` | -| raw Top256 PCA 与 target-FIM weakest direction cosine | `0.871` | -| normalized Top256 cosine | `0.790` | -| raw / normalized Top256 median parameter relative RMS | `0.258 / 0.246` | - -结果验证了 FIM 的 soma/dend compensation direction 会影响全局 candidate ordering,也说明 loss -weighting 会改变“好解”集合。但 16,384 点在六维仍很稀疏,这不是 posterior 或完整 uncertainty -quantification,只是 low-loss candidate pool 和 local weak direction 的非梯度验证。 - -当前评价顺序是:loss 能否下降,unseen voltage/spike 是否泛化,最后再报告 parameter recovery -或 equivalent-model ensemble;参数不接近 synthetic target 不自动等于 functional failure。 +零号训练基线、优化器对照、六参数 FIM 和 global ensemble 的完整数字已迁至 +[拟合与可辨识性结果](../current/results/fitting-and-identifiability.md)。下面保留方法定义与协议, +这些定义不等于公共训练 API。 ## Observation Sensitivity 与 OED diff --git a/docs/design/optim/references/voltage-and-spike-parameter-fitting.md b/docs/design/optim/references/voltage-and-spike-parameter-fitting.md index cdf32213..9ac04290 100644 --- a/docs/design/optim/references/voltage-and-spike-parameter-fitting.md +++ b/docs/design/optim/references/voltage-and-spike-parameter-fitting.md @@ -7,10 +7,10 @@ optimizer controller。相关主文档为: | 主题 | 主文档 | | --- | --- | -| 参数选择、映射与单位 | [API](../api.md) 与 [Architecture](../architecture.md) | +| 参数选择、映射与单位 | [API](../current/api.md) 与 [Architecture](../current/architecture.md) | | 训练诊断、archive、spike region 与恢复 | [模块化训练诊断](modular-training-diagnostics.md) | | BPTT/RTRL | [通用理论](bptt-to-rtrl-neuron-derivation.md) | -| batch 与 GPU | [Batch Size 与 GPU 吞吐](batch-size-and-gpu-throughput.md) | +| batch 与 GPU | [Batch Size 与 GPU 吞吐](../current/results/batch-size-and-throughput.md) | 本文区分 subthreshold trace、包含动作电位的 spiking trace,以及由阈值检测得到的 event trace。三者不能使用同一条未经归一化的 raw MSE 作为唯一目标。 diff --git a/docs/design/quad/TODO.md b/docs/design/quad/TODO.md new file mode 100644 index 00000000..4d48b474 --- /dev/null +++ b/docs/design/quad/TODO.md @@ -0,0 +1,12 @@ +# Quad TODO + +数值积分和电压求解的协作入口。宏观依赖见 [全局 TODO](../TODO.md),规范见 [Design 规范](../AGENTS.md)。 + +| 事项 | 状态 | 下一步 | 详情 | +| --- | --- | --- | --- | +| 显式积分消费边界 point 输入 | 讨论中 | 对照单 branch、3 CV 的五行方程,补端点电流与 synapse 的反馈项 | [Cell 边界输入提案](../cell/proposals/explicit-solver-boundary-inputs.md) | +| Single ODE 统一后的积分路径 | 讨论中 | 先确定单 branch single policy,再比较复用装配与独立 ODE 路径 | [Cell 统一提案](../cell/proposals/single-multi-compartment-unification.md) | +| 自适应步长 | 待讨论 | 明确 embedded RK 误差估计、事件时间与 recording 对齐 | [积分 API](current/api.md) | +| 标准模型性能对照 | 待讨论 | 统一 Mainen/Hay/L5PC 模型、精度、编译与计时口径 | [积分架构](current/architecture.md) | + +当前实现:[API](current/api.md)、[架构](current/architecture.md)。 diff --git a/docs/design/quad/current/api.md b/docs/design/quad/current/api.md new file mode 100644 index 00000000..db5e2f49 --- /dev/null +++ b/docs/design/quad/current/api.md @@ -0,0 +1,66 @@ +# Quad API + +`braincell.quad` 把注册名或 callable 解析为状态积分步骤。Cell 选择 solver 后自动调用; +独立 DiffEqModule 使用 brainstate 时间环境及编译后的步函数。 + +## 最小用法 + +```python +import braincell as bc +import brainstate +import brainunit as u + +class Decay(brainstate.nn.Module, bc.DiffEqModule): + def __init__(self): + super().__init__() + self.x = bc.DiffEqSingleState(1.0 * u.mV) + + def compute_derivative(self): + self.x.derivative = -self.x.value / (2.0 * u.ms) + +model = Decay() +step = bc.quad.get_integrator("rk4") + +@brainstate.transform.jit +def advance(): + with brainstate.environ.context(t=0.0 * u.ms, dt=0.1 * u.ms): + step(model) + return model.x.value + +value = advance() +assert u.math.abs(value - u.math.exp(-0.05) * u.mV) < 1e-5 * u.mV +``` + +## 解析、注册与调用 + +```text +get_integrator(method) -> callable +register_integrator(name, *, aliases=(), category="general", order=None, + description="", deprecated=False, override=False) -> decorator +step(target, *args) -> None +``` + +method 为注册名或 callable;callable 原样返回,未知名称抛出 KeyError 并给近似名称建议。 +注册装饰器记录名称、别名、类别、阶数和描述,返回原函数;override 控制重复注册是否覆盖。 +registry 对象和只读 all_integrators 查询表见 [_registry.py](../../../../braincell/quad/_registry.py)。 + +target 遵循 DiffEqModule 协议,并是可遍历状态的 brainstate Module/Node。 +DiffEqState 是状态协议,实际使用 DiffEqSingleState 或 DiffEqGroupState;args 传给目标导数/阶段钩子。 +step 读取环境 t、dt,就地写状态,不为外层推进时钟。多步驱动使用 +brainstate.transform.for_loop/scan,Cell/Network.run 已封装此循环。 +完整目标协议见 [Cell 积分协议](../../cell/current/api.md#积分协议)。 + +| 家族 | 步函数 | +| --- | --- | +| 显式 | euler_step、midpoint_step、rk2/3/4_step、heun2/3_step、ralston2/3/4_step、ssprk3_step | +| 隐式 | backward_euler_step、implicit_euler_step | +| 指数 | exp_euler_step、ind_exp_euler_step | +| Cell staggered | staggered_step | + +函数名去掉 `_step` 为常用注册名,实际别名以 registry 为准。不同积分器对目标的要求不同, +staggered 需要电压与机制分步接口;将它用于任意只有 compute_derivative 的模型会失败。 +`dhs_voltage_step`、dense_voltage_step、sparse_voltage_step 是电压系统后端, +调用与数组契约见 [staggered 实现](../../../../braincell/quad/_staggered.py),不与普通 target-step 签名混用。 + +Cell 显式路径当前遗漏边界输入反馈,例子和缺项见 +[边界输入提案](../../cell/proposals/explicit-solver-boundary-inputs.md)。 diff --git a/docs/design/quad/current/architecture.md b/docs/design/quad/current/architecture.md new file mode 100644 index 00000000..05e5d382 --- /dev/null +++ b/docs/design/quad/current/architecture.md @@ -0,0 +1,25 @@ +# Quad Architecture + +积分步骤消费目标的导数/阶段协议,电压后端消费已装配的电缆系统。几何和机制布局由 Cell 提供,Quad 不拥有 morphology。 + +```text +general step: state -> stage derivative -> weighted stage combination -> new state +staggered: current snapshot -> mechanism/voltage stages -> constrained cable solve -> new state +``` + +通用显式 RK 在每个局部 stage 重新计算导数,以加权组合更新 DiffEqState。 +独立积分机制使用自己的 solver/substeps,外层 Cell 步长由 brainstate.environ.dt 决定。 +staggered 的离子电流快照和 family/integration 更新顺序由 +[Cell 调度](../../cell/current/architecture.md#离子电流快照与调度) 控制。 + +电压求解中,CV 节点有膜电容,边界/分叉 point 提供代数约束;DHS 用树结构求解装配后的线性系统。 +CV 电压与 point 电压不能仅按数组长度互换。完整装配、两条推进路径及方程见 +[Cell 架构](../../cell/current/architecture.md)。 + +当前显式 axial operator 消去边界时没有同步端点输入的等效贡献,这是方程完整性问题, +提高 RK 阶数不能恢复缺项。对应改进由 [Cell proposal](../../cell/proposals/explicit-solver-boundary-inputs.md) 管理。 +求解精度与梯度结论应对应具体积分路径和 dt,已有分析见 +[solver 梯度](../../optim/references/staggered-solver-gradient-analysis.md)。 + +实现入口:[quad 导出](../../../../braincell/quad/__init__.py)、 +[积分协议](../../../../braincell/quad/protocol.py)。选择与注册用法见 [API](api.md)。 diff --git a/docs/design/reduction/TODO.md b/docs/design/reduction/TODO.md new file mode 100644 index 00000000..da812a72 --- /dev/null +++ b/docs/design/reduction/TODO.md @@ -0,0 +1,21 @@ +# Reduction TODO + +约化模型的协作入口。[全局 TODO](../TODO.md) 管理跨模块目标与阻塞, +[维护规范](../../../AGENTS.md#module-documents-and-project-progress) 定义分类和状态。 + +## 当前需要推进的事项 + +| 事项 | 状态 | 下一步或待决定问题 | 文档 | +| --- | --- | --- | --- | +| DBNN 数据生成、训练和部署闭环 | 讨论中 | 确定输入布局、规模、资产格式及分阶段验收;沿用已有 Cell 挂载契约 | [DBNN 设计](proposals/DBNN-plan.md) | + +公共 ReductionModel 接入已存在,不代表 DBNN 模型或训练流程已经实现。 +DBNN 的数学与流程设计仍是提案,范围决定不等于完整实施契约。 + +## 已实现内容索引 + +- [约化模型接入指南](current/model-integration-guide.md):Cell 挂载、生命周期、输入输出与最低测试要求。 +- [Cell API](../cell/current/api.md):详细模型与约化模型的宿主接口。 + +历史决策见 [Cell reduction runtime](../../specs/2026-09-04-cell-reduction-runtime.md), +当前接入合同以 current 中的指南和实现为准。 diff --git a/docs/design/reduction/current/model-integration-guide.md b/docs/design/reduction/current/model-integration-guide.md new file mode 100644 index 00000000..923db044 --- /dev/null +++ b/docs/design/reduction/current/model-integration-guide.md @@ -0,0 +1,308 @@ +# 约化模型接入指南 + +本文面向实现 DBNN、DLIF 或其他 Cell 约化模型的开发者。约化模型不是新的 +Network population 类型,而是挂载在一个已经声明好 morphology、population 和 +synapse 的 `Cell` 上,并替代该 Cell 的 detailed dynamics 执行。 + +运行时的权威行为定义见 +[`cell-reduction-runtime.md`](../../../specs/2026-09-04-cell-reduction-runtime.md)。本文只说明如何实现 +一个模型,以及哪些行为属于公共契约、哪些细节由具体模型自行决定。 + +## 1. 最短接入流程 + +推荐按以下顺序构建 Cell: + +1. 创建 detailed Cell,完成 morphology、CV policy 和 population 声明。 +2. 放置约化模型需要看到的全部 synapse,并设置其类型和参数。 +3. 创建并注册一个或多个 `ReductionModel`。 +4. 如有需要,通过 reduction view 设置不同 population member 的参数。 +5. 用 `cell.use_model(name)` 选择本次仿真使用的模型。 +6. 声明 raw output recording,把原 Cell 加入 Network 并正常连接、运行。 + +```python +import braincell +import brainunit as u + +# morphology、paint 和 synapse placement 已经完成。 +cell.add_reduction("dbnn", DBNNReduction.load("dbnn-model")) +cell.add_reduction("dlif", DLIFReduction(...)) + +# 可选;只有模型实现 get()/set() 时才可使用。 +cell.reductions["dlif"].set(threshold=-50.0 * u.mV) + +cell.use_model("dbnn") +cell.record("soma_voltage", braincell.observe.output("voltage")) + +network = braincell.Network() +reduced = network.add_population("reduced", cell) +# 使用普通 connection API 将上游连接到 cell 已经声明的 SynapseView。 +result = network.run(duration=100.0 * u.ms, dt=0.025 * u.ms) +``` + +一个 Cell 可以注册多个约化模型,但一次初始化只能选择一个。选择作用于整个 root Cell, +不能给同一个 Cell 的不同 `CellView` 分别选择执行模型。`"detailed"` 是保留名称;执行 +`cell.use_model("detailed")` 可在下一次初始化恢复 detailed dynamics。 + +synapse 不要求在 `add_reduction()` 之前放置,但必须在 Cell 初始化之前完成。运行时只在 +初始化时根据最终 synapse 声明创建 `ReductionContext`。对于依赖固定 synapse 分布的训练模型, +应采用上面的推荐顺序,避免注册时误以为 context 已经固定。 + +## 2. 必须实现的公共接口 + +所有模型继承 `braincell.ReductionModel`,并实现以下三个方法: + +```python +class ReductionModel: + def init_state(self, context, batch_size=None) -> ReductionOutput: ... + def update(self, inputs) -> ReductionOutput: ... + def reset_state(self, batch_size=None) -> ReductionOutput: ... +``` + +| 方法 | 由谁调用 | 必须完成的工作 | +| --- | --- | --- | +| `init_state()` | Cell 初始化 | 校验 context,编译静态输入映射,创建动态状态并返回初始输出 | +| `update()` | 每个 Cell update | 消费本步 payload,将模型推进一步并返回新输出 | +| `reset_state()` | 原位重置 | 清空动态状态,保留当前 context、学习参数和已编译映射,并返回初始输出 | +| `reset()` | Cell 完全反初始化 | 可选地删除依赖当前 context 的缓存;必须保留模型配置和学习参数 | +| `get()` / `set()` | reduction view | 可选的逐 population 参数接口;不支持时保留基类默认报错即可 | + +`reset()` 在基类中是空实现。无 context 缓存的模型不需要覆盖它;DBNN、DLIF 这类保存了 +输入映射或动态 state 的模型通常应该覆盖。 + +### 2.1 Context 是静态声明 + +`init_state()` 收到的 `ReductionContext` 包含: + +- `pop_size`:Cell 的 population shape。 +- `population_size`:展平后的 population member 数量。 +- `synapses`:每个逻辑 synapse 的静态记录。 +- `input_groups`:按 synapse runtime type 和 event-input contract 分组的输入 schema。 +- `fingerprint`:当前 synapse 布局的运行时指纹。 +- `cell`:对原 Cell 的弱引用,用于读取仍然存在的静态声明。 + +每个 `ReductionSynapse` 提供 `id`、member-local `synapse_index`、`population_index`、 +`placement_id`、`point_id`、`cv_id`、`branch_id`、`branch_x`、`name`、`synapse_type` 和只读 +`parameters`。模型可以使用全部、部分或完全忽略这些信息。 + +每个 `ReductionInputGroupSchema` 提供稳定的 `layout_id`、`synapse_type`、`event_input`,以及 +同长度的 `synapse_id`、`synapse_index`、`population_index` 数组。不要假设: + +- 所有 population member 拥有相同数量或相同顺序的 synapse; +- 不同 synapse type 会进入同一个 group; +- `synapse_id`、group 行号和 member-local `synapse_index` 相同; +- 一个模型只能遇到一种 payload 单位或 event-input contract。 + +需要 channelized input 的模型应在 `init_state()` 中根据这些显式 id 编译 scatter/gather 映射, +并在 `reset()` 中丢弃该映射。 + +### 2.2 Inputs 是已经交付的 payload + +`update()` 收到 `ReductionInputs`。遍历它得到若干 `ReductionInputGroup`,每组包含静态 +`group.schema` 和当前的 `group.payload`。 + +payload 已经完成以下网络语义: + +- connection delay; +- connection weight; +- 同一时刻、同一目标 synapse row 的聚合。 + +模型不得再次应用 delay 或 connection weight。聚合前的 event 个数不会保留:trigger 类输入 +只能判断聚合后的 slot 是否非零;scalar payload 可以读取聚合后的幅值。每次输入被包装后, +底层 event buffer 会被清零,因此模型若需要历史信息,必须保存在自己的动态 state 中。 + +输入 group 可以为空。一个具体模型可以支持空输入,也可以在 `init_state()` 中以明确错误拒绝 +不支持的 synapse type、event-input contract、单位或布局。 + +### 2.3 Output 必须保持稳定 + +三个生命周期方法都必须返回真正的 `ReductionOutput`: + +```python +output = braincell.ReductionOutput( + values={"voltage": voltage, "latent": latent}, + event=spike, +) +``` + +设运行时前缀为: + +```text +runtime_prefix = ([batch_size] if batched else []) + list(cell.pop_size) +``` + +必须满足: + +- `event.shape` 必须严格等于 `runtime_prefix`,不能带 feature 轴。 +- 每个 raw output 的 shape 必须以 `runtime_prefix` 开头,之后可以有任意 feature 轴。 +- output 名称必须是非空字符串。 +- 从 `init_state()` 到 Cell 完全 `reset()` 之间,名称及顺序、完整 shape、普通数组或 Quantity + 类型、dtype 和单位必须稳定。 +- `init_state()` 与 `reset_state()` 返回的初始输出必须遵守相同结构。 + +`event` 会成为原 Cell 的 canonical `event_outputs["spike"]`,因此约化 Cell 仍可作为普通 +connection 的上游。`values` 是模型自己的 raw outputs,可通过 `cell.outputs` 查看,也可用 +`braincell.observe.output(name)` 记录。recording 取得的是当前 update 返回的状态。 + +当前 Cell-level reduction contract 支持 batch 前缀;当前 `Network` 在线执行仍不接受 +`batch_size`。DBNN 的离线批量前向可以复用同一数学实现,但不能把离线 batch runner 当成 +Network 接口。 + +## 3. 可复制的模型骨架 + +下面的骨架把公共生命周期和模型内部函数分开。实现者只需要替换标有“模型内部”的部分。 + +```python +import brainstate +import jax.numpy as jnp + +import braincell + + +class MyReduction(braincell.ReductionModel): + def __init__(self, parameters): + # 模型内部:学习参数、超参数和资产元数据。 + self.parameters = parameters + self._context = None + self._compiled_inputs = None + self._state = None + self._batch_size = None + + def init_state(self, context, batch_size=None): + self._validate_context(context) # 模型内部 + self._context = context + self._batch_size = batch_size + self._compiled_inputs = self._compile_inputs(context) # 模型内部 + + prefix = ((int(batch_size),) if batch_size is not None else ()) + context.pop_size + self._state = brainstate.ShortTermState(self._initial_state(prefix)) + return self._initial_output(prefix) + + def update(self, inputs): + if self._state is None: + raise RuntimeError("MyReduction requires init_state() first.") + + drive = self._project_inputs(inputs, self._compiled_inputs) # 模型内部 + next_state, raw_values, spike = self._step( # 模型内部 + self._state.value, drive, self.parameters + ) + self._state.value = next_state + return braincell.ReductionOutput(values=raw_values, event=spike) + + def reset_state(self, batch_size=None): + if self._context is None: + raise RuntimeError("MyReduction requires init_state() first.") + if batch_size != self._batch_size: + raise ValueError("reset_state() must preserve the initialized batch size.") + + prefix = ((int(batch_size),) if batch_size is not None else ()) + self._context.pop_size + self._state.value = self._initial_state(prefix) + return self._initial_output(prefix) + + def reset(self): + # 保留 parameters;删除只对当前 Cell/synapse schema 有效的内容。 + self._context = None + self._compiled_inputs = None + self._state = None + self._batch_size = None + + def _initial_output(self, prefix): + # 名称、类型、单位和完整 shape 必须与 update() 返回值一致。 + voltage = jnp.zeros(prefix) + spike = jnp.zeros(prefix, dtype=jnp.int32) + return braincell.ReductionOutput(values={"voltage": voltage}, event=spike) +``` + +骨架中的 `_validate_context()`、`_compile_inputs()`、`_initial_state()`、`_project_inputs()` 和 +`_step()` 都不是公共 API,只是建议的模型内部职责划分。它们可以改名、合并或完全替换。 + +如果模型需要提供逐 member 参数,额外实现: + +```python +def get(self, field: str, population_indices: tuple[int, ...]): ... +def set(self, population_indices: tuple[int, ...], **parameters) -> None: ... +``` + +`set()` 只会在 Cell 未初始化时通过 `ReductionView` 调用。模型负责检查字段名、值的单位和 shape, +以及将 scalar 或选中 member 对应的值保存为声明期参数。 + +## 4. DBNN 和 DLIF 如何落到这个接口 + +### 4.1 DBNN + +DBNN adapter 的公共部分仍然只有上述生命周期。模型内部通常负责: + +- 从模型资产读取学习参数、训练 schema、dt、单位和版本。 +- 在 `init_state()` 对照 `context.synapses`、`input_groups` 和资产 manifest,拒绝错误的 Cell、 + point、synapse prototype、参数或输入单位。 +- 编译 `synapse_id -> DBNN channel` 映射;不能依赖运行时 group 的偶然行顺序。 +- 在 `update()` 将 payload scatter 到 channel,执行一次在线递推,输出胞体 voltage,并从阈值 + 上穿生成 canonical spike。 +- 在 `reset_state()` 清空卷积核、双线性层或其他递归历史,但保留学习参数和 channel mapping。 +- 在 `reset()` 删除 channel mapping 和 context 缓存,但保留已加载的模型资产。 + +`context.fingerprint` 可以用于快速拒绝完全不同的 synapse 声明,但 DBNN 资产仍应保存自己可读、 +可版本化的 manifest。不要只保存一个不透明 hash,否则无法向用户说明是 point、prototype、参数、 +dt 还是单位不兼容。 + +### 4.2 DLIF + +DLIF adapter 可以用同一公共接口,但内部通常是: + +- 在 `init_state()` 根据 synapse metadata 编译输入权重或 E/I channel 映射。 +- 用 `brainstate.ShortTermState` 保存每个 population member 的 membrane/recurrent state。 +- 在 `update()` 聚合本步 payload,执行 decay、积分、threshold 和 reset,返回 voltage 与 spike。 +- 自行决定是否消费 synapse type、位置和参数;公共运行时不要求 DLIF 模拟真实 synapse dynamics。 +- 若提供 threshold、decay 等逐 member 参数,再实现 `get()` / `set()`;否则保持为构造参数或模型 + 资产的一部分。 + +DBNN 和 DLIF 都不应创建或推进 detailed `Cell.V`、ion、channel 或 synapse state。在 reduced +mode 下这些 runtime 根本不会分配。`context.cell` 只应用于读取声明和静态元数据,不能作为 +detailed dynamics 的旁路。 + +## 5. 哪些细节完全属于模型自己 + +以下内容不应加入 `ReductionModel` 公共基类,除非将来至少两个正式模型出现相同、稳定的需求: + +- 网络结构、状态方程、threshold/reset 规则和数值积分方式; +- 学习参数、训练循环、loss、optimizer 和 offline sequence forward; +- 输入 channel 定义,以及是否消费 payload 幅值、synapse type、参数或形态位置; +- checkpoint、部署资产、版本迁移和训练来源追踪格式; +- context 兼容范围,以及允许重建、拒绝或要求重新训练的策略; +- raw output 的名字、单位和 feature 维度; +- 是否支持可编辑的逐 member 参数; +- JIT、gradient、缓存和性能优化方式。 + +约束这些内部选择的只有公共输入、生命周期和输出契约。例如,模型可以输出 `voltage`、latent +features 或多个诊断量,但一旦初始化完成,本次 runtime 中不能动态增删这些字段。 + +## 6. 最低测试清单 + +一个准备接入主库的约化模型至少应验证: + +- 非 batch Cell 下,初始、单步和 reset 输出 shape、dtype、单位一致。 +- 若宣称支持 batch,batch 前缀正确且不同 member、不同 batch 不共享动态状态。 +- 多个 population member 的 synapse 数量不相等时,输入仍按显式 schema 映射正确。 +- 多个 synapse type/group、空 group 或完全无输入时,得到预期结果或明确的初始化错误。 +- connection weight、delay 和同目标聚合只由运行时应用一次。 +- `reset_state()` 与全新初始化得到相同动态初态,同时保留参数和 context 映射。 +- `reset()` 后修改 synapse 并再次初始化时,模型能重新编译或报告需要重新训练。 +- `values` 名称、shape、Quantity 类型和单位在每步保持稳定。 +- 约化 Cell 作为上游和下游连接时,canonical spike 与输入 delay 语义正确。 +- `observe.output()` 和 spike recording 取得的是预期的 post-update 输出。 +- 对训练模型,资产 round trip 后预测、schema 校验和错误信息保持一致。 +- 若模型承诺可训练或可 JIT,分别验证 gradient、JIT 后的单步结果与离线 sequence forward 一致。 + +现有 [`toy.py`](../../../../braincell/reduction/toy.py) 提供三种较小的参考: +`EventAccumulatorReduction` 只看 slot 是否活动,`PayloadAccumulatorReduction` 消费幅值, +`SynapticKernelAccumulatorReduction` 还会读取 synapse type 和参数,但不运行真实 synapse dynamics。 + +## 7. 常见错误 + +- 在构造模型时读取 Cell synapse:此时 placement 可能尚未完成;应在 `init_state(context)` 读取。 +- 按 group 行号固定 channel:异构 population 或重新声明后行顺序可能不再符合模型假设。 +- 在模型内再次乘 connection weight:`group.payload` 已经包含它。 +- 希望从 trigger payload 恢复同一时刻的原始 event 数:聚合后该信息已经不存在。 +- 只在 `update()` 返回某个诊断字段:所有 output 必须从 `init_state()` 起存在并保持结构稳定。 +- 用 `reset_state()` 重新加载资产或改变 batch:它只负责原位重置动态状态。 +- 在 reduced mode 访问详细膜电压或真实 synapse state:这些 runtime 不会被创建。 +- 用一个 fingerprint 代替可解释的资产 manifest:hash 适合快速比较,不足以诊断或迁移模型。 diff --git a/docs/design/reduction/DBNN-plan.md b/docs/design/reduction/proposals/DBNN-plan.md similarity index 87% rename from docs/design/reduction/DBNN-plan.md rename to docs/design/reduction/proposals/DBNN-plan.md index c01c48aa..46ff0aaa 100644 --- a/docs/design/reduction/DBNN-plan.md +++ b/docs/design/reduction/proposals/DBNN-plan.md @@ -1,5 +1,12 @@ # DBNN 约化模型设计大纲 +> 状态:讨论中。公共 Cell 挂载式约化运行时已存在,DBNN 数学模型、数据生成、训练和资产 +> 流程仍是未落地提案。DBNN 实现 `ReductionModel`,注册到已有形态、突触和连接声明的 Cell 上。 +> 接口、生命周期和输入输出以 [当前接入指南](../current/model-integration-guide.md) 为准; +> 进度见 [Reduction TODO](../TODO.md)。原独立 runner 和另建 Network 节点协议的路线已经被取代, +> 本文不再将其列为有效实施步骤。历史决策见 +> [Cell reduction runtime](../../../specs/2026-09-04-cell-reduction-runtime.md)。 + ## 1. 目标 DBNN 是 BrainCell `reduction` 模块中的一种细胞约化模型。它以一个已经完成形态、 @@ -281,8 +288,9 @@ points 时,训练前必须估算通道数、通道对数量、数据量和计 ## 6. 作为 population 运行 -训练后的 DBNN 应表现为没有显式 morphology、但具有固定输入通道和胞体输出的简化细胞模型。 -同一个模型资产可以实例化为多个同构单元组成 population,每个单元维护独立的在线状态。 +训练后的 DBNN 数学模型使用固定输入通道和胞体输出;部署时挂载到保留 morphology、 +SynapseView 和连接声明的 Cell 上,通过已有 `add_reduction()` / `use_model()` 选择模型。 +Network 注册宿主 Cell,同一个模型资产可以供同构 population 使用,每个单元维护独立在线状态。 Network 接入需要覆盖以下语义: @@ -290,21 +298,20 @@ Network 接入需要覆盖以下语义: 稳定映射到对应输入通道; - 同一时间步到达同一通道的多个连接输入能够正确汇聚; - DBNN 的电压状态和阈值 spike 能被网络推进与记录; -- DBNN 可以作为突触前或突触后 population; +- 挂载 DBNN 的 Cell 可以作为突触前或突触后 population; - `Cell -> DBNN`、`DBNN -> Cell` 和 `DBNN -> DBNN` 均能使用统一的事件延迟语义; - DBNN population 与 detailed Cell population 可以出现在同一个 Network 中; - DBNN 接入不能改变仅包含传统 detailed Cell 的现有网络结果。 -当前 Network 运行时对多室 `Cell` 内部结构存在直接依赖,后续实现需要把网络节点运行能力与 -具体 morphology runtime 解耦。但是本文不规定协议方法名、Connection 参数或事件 buffer 的 -具体形状,这些应在 Network 与 reduction 联合实现设计中决定。 +上述接入复用现有 `ReductionModel`、宿主 Cell 和 Network 协议。输入是现有运行时已经交付的 +payload,输出遵守接入指南;DBNN 不另建连接 owner、延迟队列或网络节点协议。 网络侧还必须校验模型资产声明的时间步、point manifest、通道语义、突触原型和输入有效范围。 不能把任意真实突触机制直接连接到一个仅按 E/I 标签匹配的 DBNN 通道,并假定其响应仍然等价。 ## 7. 最小改动的第一阶段实现方案 -第一阶段以“完成一个可独立验证的 DBNN 约化闭环”为目标,只新增 `reduction` 模块并包装调用 +第一阶段以“完成一个可独立验证的 DBNN 约化闭环”为目标,扩展已有 `reduction` 模块并包装调用 现有 Cell、事件源、连接和 Network 能力,不修改多室求解器、突触 runtime、连接 lowering 或 Network 时间循环。 @@ -316,11 +323,12 @@ detailed Cell factory -> DBNN 数据集 -> 训练与评估 -> BrainCell 模型资产 - -> 独立 DBNN population runner + -> ReductionModel 挂载宿主 Cell + -> 现有 Network population 验证 ``` -这一阶段能够完成数据自动生成、模型训练、评估、保存、加载、批量推理和有状态单步运行; -DBNN 暂不直接注册到现有 `Network`,混合网络接入在模型本身稳定后单独处理。 +这一阶段的目标是数据自动生成、模型训练、评估、保存、加载、批量推理和有状态单步运行, +并使用已有 Cell 挂载接口验证最小 Network 接入。它是待实施范围,不是完成记录。 ### 7.1 独立模块组织 @@ -334,8 +342,8 @@ DBNN 首先作为 `braincell.reduction` 下的独立子模块实现。建议按 - 训练与指标:负责优化、验证、VE、Precision 和 Recall; - 模型资产:负责训练状态、部署参数、通道清单和来源信息的保存与加载。 -第一阶段不急于为所有未来 reduction 方法定义统一基类。只有教师运行、数据集或模型资产在 -第二种约化模型中出现真实复用需求后,再把对应能力提升到公共层。 +运行时统一基类已有 `ReductionModel`,DBNN 应遵守它。教师运行、数据集或模型资产只有在 +第二种约化模型中出现真实复用需求后,才考虑进一步提升到公共层。 除可选的顶层导出外,第一阶段代码和测试均应位于 `braincell/reduction/`。训练优先复用项目 已有的 JAX、BrainState 和 Braintools 能力,不为 DBNN 新增 PyTorch 或 Optax 运行依赖。 @@ -399,18 +407,19 @@ DBNN 离线训练只保留一套权威数学实现:事件栅格化、双指数 重复执行的模型前向、训练 step 和时间循环必须使用 JIT 及 BrainState 循环变换,不使用 Python 逐时间步驱动模型。优化器优先使用项目已经依赖的 Braintools。 -### 7.5 保存和独立 population 验证 +### 7.5 保存与 Cell 挂载验证 第一阶段将训练状态与部署模型分开保存。部署资产采用 BrainCell 自有格式,至少由参数数组和 -一份可读 manifest 组成,不依赖教师 Cell 或训练框架即可加载。 +一份可读 manifest 组成。模型参数加载与教师仿真、训练过程解耦;接入 Network 时仍需构建 +与资产兼容的宿主 Cell 声明并进行布局校验。 DBNN 模块需要提供两个数值一致的运行方式: - 对完整事件序列进行批量推理,用于训练、评估和离线预测; -- 持有每个 population member 独立状态的单步递推,用于验证未来网络运行语义。 +- 持有每个 population member 独立状态的单步递推,通过已有 ReductionModel 契约供 Cell 运行。 -独立 population runner 显式接收当前时间步的 `(population, channel)` 事件,返回每个 member -的胞体电压和 threshold spike。它不冒充具有 morphology 和真实 SynapseView 的 detailed Cell。 +单步入口消费接入指南规定的已交付输入,返回符合约定的电压和事件输出;输入通道、population +轴及输出 shape 由已有 ReductionContext/Inputs/Output 契约约束,不另定义 runner 协议。 第一阶段至少进行以下模块级验证: @@ -430,19 +439,9 @@ DBNN 模块需要提供两个数值一致的运行方式: ### 7.6 第一阶段明确不做 -现有 Network 会把具有 `pop_size` 的普通模型按多室 Cell 处理,连接目标也必须是 Cell 拥有的 -真实 SynapseView。因此仅给 DBNN 增加 `pop_size` 或包装几个同名方法,并不能正确实现突触后 -事件接收。 - -第一阶段不伪造 morphology、CV runtime 或 SynapseView,也不让 DBNN 继承 detailed Cell 来绕过 -检查。以下能力推迟到独立模型验证完成之后: - -- 将 DBNN 直接传给 `Network.add_population`; -- `Cell -> DBNN`、`DBNN -> Cell` 和 `DBNN -> DBNN` 的在线连接; -- 递归混合网络和统一事件延迟队列。 - -后续接入 Network 时,应单独引入最小网络节点协议和显式事件输入端口,使 Network 面向运行 -能力而不是多室 Cell 私有实现。该改造不属于第一阶段“只包装调用”的范围。 +第一阶段不将裸 DBNN 对象注册为新的 population 类型,不伪造 morphology 或 SynapseView, +也不新增 Network 节点协议、连接 lowering 或独立事件延迟队列。使用已有宿主 Cell 的声明和运行时。 +大规模递归混合网络的科学有效性与性能验收留待最小闭环之后,不能由接入接口存在直接推断。 ## 8. 完成范围 @@ -458,19 +457,19 @@ DBNN 模块需要提供两个数值一致的运行方式: FP、FN、有效亚阈值样本数和 spike 匹配窗口; - 保存训练状态,并导出可以重新加载的 BrainCell 模型资产; - 资产重新加载后保持通道语义、模型输出和适用范围一致; -- 将 DBNN 作为独立 population runner 完成批量和单步运行验证; +- 完成 DBNN 批量与单步一致性验证,并挂载 Cell 在现有 Network 中运行最小连接场景; - 对 Cell、CV/point 布局、突触原型、反转电位、时间步或输入范围不兼容给出明确错误或警告。 -第一阶段验收不要求 DBNN 出现在现有 Network 中,也不包含与 detailed Cell 的在线混合连接。 +第一阶段采用现有 Cell 挂载方式验证输入交付与模型输出,不要求完成全部递归混合网络场景。 ### 8.2 完整目标验收 -后续完成 Network 节点协议和事件输入端口后,再增加以下系统级验收: +最小闭环通过后,继续使用既有宿主 Cell 和事件协议增加以下系统级验收: -- DBNN 可以作为正式 population 加入现有 Network; +- 挂载 DBNN 的 Cell 可以作为正式 population 加入现有 Network; - `Cell -> DBNN`、`DBNN -> Cell` 和 `DBNN -> DBNN` 的事件、weight 和 delay 语义正确; - DBNN 与 detailed Cell 可以在同一网络中同时运行和记录; -- 混合网络中的结果与 DBNN 独立单步运行结果一致; +- 混合网络中的结果与相同已交付输入下的 DBNN 单步运行结果一致; - 接入 DBNN 后,只有传统 detailed Cell 的现有网络行为和测试结果保持不变。 ## 9. 后续实现前需要继续讨论的问题 @@ -483,4 +482,4 @@ DBNN 模块需要提供两个数值一致的运行方式: - 动作电位邻域在电压训练中的屏蔽范围,以及 spike 阈值的校准方式; - 教师 Cell、CV 离散、NodeTree point manifest 和位置 aliases 的稳定指纹如何定义; - 数据集、训练状态和部署资产的具体存储格式与版本迁移策略; -- Network 为 morphology Cell 和 reduction Cell 提供的最小统一运行协议。 +- DBNN 输入 manifest 与现有 ReductionContext 的具体映射及拒绝不兼容资产的校验边界。 diff --git a/docs/design/synapse/TODO.md b/docs/design/synapse/TODO.md new file mode 100644 index 00000000..5837a772 --- /dev/null +++ b/docs/design/synapse/TODO.md @@ -0,0 +1,10 @@ +# Synapse TODO + +突触内部动力学的协作入口。Connection 路由归 [Network](../network/TODO.md),文档规则见 [Design 规范](../AGENTS.md)。 + +| 事项 | 状态 | 下一步 | 详情 | +| --- | --- | --- | --- | +| 突触内部动力学可塑性 | 讨论中 | 用具体模型区分内部状态变化与 Connection weight 规则 | [可塑性方案](../network/proposals/connection-plasticity.md) | +| 模型验证覆盖 | 待讨论 | 扩展事件序列、时间常数与电压驱动力的参考对照 | [API](current/api.md) | + +当前实现:[ExpSyn/Exp2Syn API](current/api.md)、[状态与事件架构](current/architecture.md)。 diff --git a/docs/design/synapse/current/api.md b/docs/design/synapse/current/api.md new file mode 100644 index 00000000..2d53622a --- /dev/null +++ b/docs/design/synapse/current/api.md @@ -0,0 +1,67 @@ +# Synapse API + +`braincell.synapse.ExpSyn` 和 Exp2Syn 计算突触内部状态及点电流;Cell 用户通过 +`braincell.mech.Synapse` 声明位置,通过 [Connection](../../network/current/connections.md) 连接事件源。 + +## 独立状态示例 + +```python +import braincell as bc +import brainunit as u + +syn = bc.synapse.ExpSyn(size=1, tau=2.0 * u.ms, e=0.0 * u.mV) +syn.init_state() +syn.apply_events(u.math.asarray([0.001]) * u.uS) +syn.compute_derivative() +assert u.math.allclose(syn.g.value, 0.001 * u.uS) +assert u.math.allclose(syn.current(-65.0 * u.mV), 0.065 * u.nA) +assert u.math.allclose(syn.g.derivative, -0.0005 * u.uS / u.ms) +``` + +完整 Cell 连接与运行例子见 [Network 最小用法](../../network/current/api.md#最小用法)。 + +## 构造和方法 + +```text +ExpSyn(size, name=None, tau=0.1*u.ms, e=0*u.mV) +Exp2Syn(size, name=None, tau1=0.1*u.ms, tau2=10*u.ms, e=0*u.mV) +init_state(V_post=None, batch_size=None) -> None +reset_state(V_post=None, batch_size=None) -> None +apply_events(payload, V_post=None) -> None +compute_derivative(V_post=None) -> None +current(V_post) -> point current +``` + +size 是运行时 packed rows 形状,Cell 自动按逻辑突触布局分配;name 为运行时节点名。 +时间常数和 e 接受带单位值或 shape initializer,广播到 size。tau/tau1/tau2 必须正, +Exp2Syn 还要求 tau1 < tau2,否则构造参数校验抛出 ValueError。 +init 分配状态,reset 将状态恢复到零,apply_events 就地增加状态,compute_derivative 只写导数。 +V_post 是突触处电压;current 返回电流而不是电流密度。 + +## 方程与事件 + +ExpSyn 的 g 单位为 uS,事件 payload 也为 uS: + +$$ +g\leftarrow g+w n,\qquad \dot g=-g/\tau,\qquad I=g(e-V). +$$ + +n 是事件计数,w 是 Connection.weight;多个事件按 sum 累加,向内电流为正。 +默认权重由 ScalarEventInput(u.uS) 决定,传错单位在连接/事件校验时失败。 + +Exp2Syn 保存 A、B 两个 uS 状态,g 是只读计算值 `B-A`: + +$$ +\dot A=-A/\tau_1,\quad \dot B=-B/\tau_2,\quad I=(B-A)(e-V), +$$ +$$ +t_p=\frac{\tau_1\tau_2}{\tau_2-\tau_1}\log(\tau_2/\tau_1),\qquad +f=\left(e^{-t_p/\tau_2}-e^{-t_p/\tau_1}\right)^{-1}. +$$ + +事件使 A、B 同时增加 `f*w*n`,使单个事件的峰值电导为 w。 +记录 g 时注意 ExpSyn.g 是 State,Exp2Syn.g 是派生属性;可观测字段选择以 +[Recording](../../network/current/recording.md) 的 runtime field 解析为准。 +参数训练见 [Optim 支持度](../../optim/current/parameter-support.md)。 +来源与数值用例见 [exponential.py](../../../../braincell/synapse/exponential.py)、 +[exponential_test.py](../../../../braincell/synapse/exponential_test.py)。 diff --git a/docs/design/synapse/current/architecture.md b/docs/design/synapse/current/architecture.md new file mode 100644 index 00000000..2955eb75 --- /dev/null +++ b/docs/design/synapse/current/architecture.md @@ -0,0 +1,25 @@ +# Synapse Architecture + +目标 Cell 持有逻辑 Synapse rows 和运行时动力学;Network 只组织到这些 rows 的事件路由。 + +```text +place declaration -> stable Synapse ID -> type-grouped runtime rows +source event -> Connection weight/delay -> event buffer -> apply_events +synapse state -> current(V_post) -> point current -> Cell voltage solve +``` + +同类型模型合并进一个 runtime 节点以向量化计算,逻辑 ID、name、位置和参数行保持独立。 +一个 Synapse 可以被多个 Connection 指向,所有输入作用于同一套内部状态。 +Connection 保存 weight/delay/source routing,不拥有另一份突触时间常数或电导状态。 + +运行时基类从构造签名发现参数,通过 `_init_parameters` 物化;states 类属性声明 StateSpec, +event_input 声明输入单位及聚合规则。ExpSyn/Exp2Syn 在事件边界更新电导状态,之后由积分器推进衰减, +并在 point 电压上求点电流。点与 CV 电压装配见 [Cell 架构](../../cell/current/architecture.md)。 + +可训练的构造参数与 Connection weight 由各自 owner 的 TrainableManager 管理; +改变突触内部动力学的可塑性需要新的模型类,纯 weight 规则的挂载方式还在 +[Network proposal](../../network/proposals/connection-plasticity.md) 中讨论。 + +实现入口:[运行时基类](../../../../braincell/_base_channel.py)、 +[逻辑 storage](../../../../braincell/_multi_compartment/synapses.py)、 +[Exp 模型](../../../../braincell/synapse/exponential.py)。公开调用见 [API](api.md)。 diff --git a/docs/design/vis/TODO.md b/docs/design/vis/TODO.md new file mode 100644 index 00000000..5596a78d --- /dev/null +++ b/docs/design/vis/TODO.md @@ -0,0 +1,17 @@ +# Vis TODO + +`braincell.vis` 已提供脚本和 Notebook 可视化。下一步讨论将可视化迁入 braintools, +由同一模块提供简单绘图和 GUI 两类入口。 + +| 事项 | 状态 | 下一步 | 详情 | +| --- | --- | --- | --- | +| 可视化迁入 braintools | 讨论中 | 比较直接传 Cell 与共享数据入口,确定模块归属及旧入口兼容方案 | [迁移提案](proposals/braintools-migration.md) | +| 渲染回归基线 | 待讨论 | 选取代表性图像,准备基线和实际执行比较的 CI | [验证现状](current/visualization.md#验证现状) | + +## 已有能力 + +[Visualization](current/visualization.md) 汇总形态、数据、拓扑、交互和导出能力, +并列出后端差异、代码入口和教程。 +[Vis API](current/api.md) 提供 Cell 调用示例、全部公共接口规格、数据映射和结果对象说明。 + +项目进度见[全局 TODO](../TODO.md),文档组织与事项状态遵循[设计文档规范](../AGENTS.md)。 diff --git a/docs/design/vis/current/api.md b/docs/design/vis/current/api.md new file mode 100644 index 00000000..50ee8119 --- /dev/null +++ b/docs/design/vis/current/api.md @@ -0,0 +1,673 @@ +# Vis API + +`braincell.vis` 提供形态、Cell 离散拓扑、数值与时间序列的可视化。函数从 +`from braincell import vis` 导入;`Morphology` 和 `Branch` 另有 `vis2d()`、`vis3d()` +快捷方法。Cell 拓扑直接传 `cell`,形态绘图传 `cell.morpho`。 + +| 使用场景 | 接口说明 | +| --- | --- | +| 查看 Cell 的分支、CV、求解节点及状态 | [Cell 拓扑](#cell-拓扑) | +| 画形态、着色与选择位点 | [形态绘图](#形态绘图)、[对象快捷方法](#对象快捷方法)、[数值与选择](#数值与选择) | +| 比较模型和结果、画轨迹或动画 | [对比](#对比)、[时间轨迹](#时间轨迹)、[动画](#动画) | +| 分析树结构与几何 | [结构分析](#结构分析) | +| 设置布局和样式、响应点击、保存结果 | [布局与缓存](#布局与缓存)、[样式配置](#样式配置)、[交互回调](#交互回调)、[后端与导出](#后端与导出) | + +以下签名保留参数顺序、关键字限定及默认值,类型与组合规则在对应表格中说明。 +覆盖范围以[公共导出](../../../../braincell/vis/__init__.py)为准;返回对象的字段也在正文列出。 + +## 从 Cell 开始 + +下面构造一个带 3D 坐标的 soma 和两段 dendrite,共两个 branch、三个几何 segment。 +`CVPerBranch(2)` 为每个 branch 生成两个 CV。后续示例复用这里的 `cell`、`morpho` 和导入。 + +```python +import numpy as np +import brainunit as u +import braincell as bc +from braincell import vis + +soma = bc.Branch.from_points( + points=[[0., 0., 0.], [10., 0., 0.]] * u.um, + radii=[5., 5.] * u.um, type="soma", +) +dend = bc.Branch.from_points( + points=[[10., 0., 0.], [30., 10., 0.], [50., 20., 5.]] * u.um, + radii=[2., 1.5, 1.] * u.um, type="basal_dendrite", +) +morpho = bc.Morphology.from_root(soma, name="soma") +morpho.attach(parent="soma", child_branch=dend, child_name="dend", parent_x=1.) +cell = bc.Cell(morpho, cv_policy=bc.CVPerBranch(2)) + +shape_ax = vis.plot2d(cell.morpho) +branch_ax = vis.plot_cell_topology(cell, level="branch", layout="kamada_kawai") +cv_ax = vis.plot_cell_topology(cell, level="cv", layout="kamada_kawai") + +cell.init_state() +voltage_ax = vis.plot_cell_topology( + cell, level="node", value="V", layout="kamada_kawai", +) +vis.save_figure(voltage_ax, "voltage.png") +assert voltage_ax.figure is not None +``` + +绘图读取调用时的数据,不推进 Cell。`init_state()` 用于初始化模型,不是刷新图像的步骤。 +再次绘图会创建新图;传入已有 `ax` 时在该轴上继续添加图元。修改 Cell 后,旧图不会自动刷新。 + +## Cell 拓扑 + +源码:[cell_topology.py](../../../../braincell/vis/cell_topology.py);数值与选择映射由 +[field_resolution.py](../../../../braincell/_multi_compartment/field_resolution.py)处理。 + +```text +plot_cell_topology(cell, *, level="node", preset="dendrotweaks", + layout=None, layout_scale=1.0, region=None, locset=None, + coverage_mode="fraction", highlight_color="#ef4444", value=None, + cmap=None, vmin=None, vmax=None, norm=None, value_label=None, + show_colorbar=True, node_color=None, edge_color=None, root_color=None, + ax=None) -> matplotlib.axes.Axes +``` + +| `level` | 一个图节点表示什么 | 调用条件与可用数据 | +| --- | --- | --- | +| `"branch"` | 一个 morphology branch | 构造 Cell 后可调用;支持 `region` 覆盖,拒绝 `locset`、`value` 及显式色条参数 | +| `"cv"` | 一个 CV | 结构与选择可在初始化前查看;提供 value 时先初始化,CV 树必须恰有一个根 | +| `"node"`,默认 | 一个 NodeTree 空间位点,包括 CV 中点和边界点 | 要求 `cell.init_state()`;支持选择与数值映射 | + +### 选择与覆盖 + +| 参数 | 类型与语义 | +| --- | --- | +| `cell` | `bc.Cell`,必填 | +| `region=None` | `RegionExpr` 或 `RegionMask`;表达式由 Cell 解析。按膜面积计算与 CV 或 branch 的重叠比例 | +| `locset=None` | `LocsetExpr` 或 `LocsetMask`;位置归属到 CV,在 node 图中高亮该 CV 中点 | +| `coverage_mode="fraction"` | `fraction` 按比例混色;`any` 有重叠即全亮;`all` 完全覆盖才全亮。locset 命中强度为 1 | +| `highlight_color="#ef4444"` | Matplotlib 颜色,高亮目标色 | +| `value=None` | 数值或字段选择器,见下表;与任意非 `None` 的 `region`、`locset` 互斥 | + +region 和 locset 可以组合,重叠位置取较强高亮。node 图中的选择只标记 CV 中点, +包括端点在内的 locset 也按所属 CV 映射,图中被高亮的点不一定是原始几何位置。 + +```python +region = bc.filter.BranchSlice(branch_index=1, prox=0.0, dist=0.5) +locset = bc.filter.RootLocation(0.5) +vis.plot_cell_topology(cell, level="cv", region=region, layout="kamada_kawai") +vis.plot_cell_topology(cell, locset=locset, layout="kamada_kawai") +``` + +### 数值来源与空间 + +| `value` 形式 | 解释与限制 | +| --- | --- | +| 标量 | 在目标空间广播,可带 `brainunit` 单位 | +| `(n_cv,)` 数组 | CV 图直接使用;node 图只写入各 CV 中点,其他点为 `NaN` | +| `(n_point,)` 数组 | node 图保留每点值;CV 图读取各 CV 中点值 | +| `"V"`、`"voltage"` | 读取 `cell.V.value` 的 CV 电压;node 图中的边界点为 `NaN` | +| `("ion", ion_name, field)` | 读取指定离子字段,按命名膜字段规则映射,node 图仅显示中点 | +| `("channel", class_name, field)` | 按通道类名查找唯一 runtime layout;同类有多个 layout 时用 ID 选择 | +| `("layout_id", layout_id, field)` | 读取具体机制 layout 的状态或参数,按其实际挂载位置映射;未覆盖处为 `NaN` | + +运行时数组的空间轴在最后。前置 population 轴全为 1 时自动去掉;含多个成员时抛出 +`ValueError`,调用者应先选一个成员,再通过 `value=` 传入一维数组。空间数组长度不能 +按颜色自动推断成形态 segment;这里的 `n_point` 指求解节点,区别于形态中心线点数。 + +```python +cv_voltage = cell.V.value[0] +vis.plot_cell_topology(cell, level="cv", value=cv_voltage, layout="kamada_kawai") +``` + +### 拓扑图的公共样式与错误 + +以下参数也用于 `plot_point_topology`。 + +| 参数 | 类型、默认值与效果 | +| --- | --- | +| `preset="dendrotweaks"` | 样式预设;可选名称见下方离散点拓扑说明 | +| `layout=None` | 默认取预设布局;可指定 `twopi`、`dot`、`neato`、`kamada_kawai` | +| `layout_scale=1.0` | 正的有限数,控制图布局间距,不代表真实几何长度 | +| `node_color/edge_color/root_color=None` | Matplotlib 颜色;`None` 沿用预设 | +| `cmap=None` | 数值色图;省略时使用预设或 `viridis` | +| `vmin/vmax=None` | 数值范围,省略时根据有效数值计算;使用数据当前单位对应的裸数值 | +| `norm=None` | Matplotlib `Normalize` 对象,优先于范围参数 | +| `value_label=None` | 色条标题;Cell 命名字段自动生成名称,带单位数组自动补单位 | +| `show_colorbar=True` | 是否创建数值色条 | +| `ax=None` | 目标 Matplotlib `Axes`;`None` 新建,返回实际绘制的同一个轴 | + +错误类型:输入不是 Cell 为 `TypeError`;node 层未初始化为 `RuntimeError`;非法 level、 +互斥参数、不支持的数组形状、多成员 population 或不唯一的通道选择为 `ValueError`。 +缺少机制字段可能产生 `AttributeError`,不存在的 layout ID 为 `KeyError`。 +branch 层还拒绝非默认的 `show_colorbar`,因为这一层只展示拓扑和覆盖。 +Graphviz 不可用时发出警告并退回 `kamada_kawai`。 + +## 形态绘图 + +源码:[plot2d.py](../../../../braincell/vis/plot2d.py)、[plot3d.py](../../../../braincell/vis/plot3d.py)。 + +```text +plot2d(morpho, *, region=None, locset=None, values=None, cmap=None, + vmin=None, vmax=None, norm=None, value_label=None, show_colorbar=True, + layout=None, shape=None, branch_type_colors=None, + branch_type_edge_colors_2d=None, frustum_edge_linewidth_2d=None, + backend=None, chooser=None, ax=None, notebook=None, jupyter_backend=None, + return_plotter=False, projection_plane="xy", min_branch_angle_deg=25.0, + root_layout="type_split", layout_config=None, hooks=None) -> object + +plot3d(morpho, *, region=None, locset=None, values=None, cmap=None, + vmin=None, vmax=None, norm=None, value_label=None, show_colorbar=True, + mode=None, backend=None, chooser=None, notebook=None, jupyter_backend=None, + return_plotter=False, hooks=None) -> object +``` + +`morpho` 必须是 `bc.Morphology`;传 Cell 或 Branch 会抛出 `TypeError`。 +2D 返回 `Axes`,3D 返回对象由后端与 Notebook 设置决定,见[后端与导出](#后端与导出)。 + +| 参数 | 类型与语义 | +| --- | --- | +| `region/locset=None` | 已求值的 `RegionMask`、`LocsetMask`,由 `expr.evaluate(morpho)` 获得;与 Cell 接口接收表达式的方式不同 | +| `values=None` | 一维形态标量数组或 `ValueSpec`;形状与优先级见[数值与选择](#数值与选择) | +| `cmap/vmin/vmax/norm/value_label` | 着色参数;`None` 保留传入 ValueSpec 的对应设置。裸数组默认色图为 `viridis`;norm 用于 Matplotlib,3D 后端按 vmin/vmax 线性着色 | +| `show_colorbar=True` | 控制色条;始终覆盖传入 ValueSpec 的同名字段 | +| `layout=None` | 使用全局默认,初始为 `fan`;另有 `stem`(别名 `trunk_first`)、`balloon`、`radial_360`、`projected` | +| `shape=None` | 使用全局默认,初始为 `frustum`;`line` 画中心线,`frustum` 画截锥投影 | +| `projection_plane="xy"` | `projected` 使用的投影平面,可选 `xy`、`xz`、`yz` | +| `min_branch_angle_deg=25.0` | 自动布局的最小分叉角提示,单位度;可传 `None`,由布局算法处理 | +| `root_layout="type_split"` | 根部按分支类型分组;另接受已弃用的 `legacy`,使用时发出 DeprecationWarning | +| `layout_config=None` | `LayoutConfig`;`None` 使用内置布局参数 | +| `mode=None` | 3D 使用全局默认,初始为 `geometry`;PyVista 的 `skeleton` 画中心线,`geometry` 画管状几何;Plotly 均画分支线 | +| `branch_type_colors=None` | 类型名到颜色的映射,临时覆盖当前绘图的默认填色 | +| `branch_type_edge_colors_2d=None` | 类型名到截锥边框色的映射 | +| `frustum_edge_linewidth_2d=None` | 非负边框线宽;`None` 沿用全局配置 | +| `backend=None` | 默认 2D 选 Matplotlib,3D 优先 PyVista,其次 Plotly | +| `chooser=None` | 高级扩展点,传 `braincell.vis.backend.BackendChooser`;`None` 使用默认注册表 | +| `ax=None` | 2D 的目标 Axes;3D 无此参数 | +| `notebook=None` | PyVista 自动检测 Notebook;`True/False` 显式控制 | +| `jupyter_backend=None` | PyVista 的 Notebook 展示方式,如 `client`、`html`、`trame` | +| `return_plotter=False` | PyVista Notebook 返回展示对象;`True` 请求原始 Plotter,详见后端说明 | +| `hooks=None` | `VisHooks`,在支持回调的后端接入拾取等事件 | + +真实投影和 3D 绘图需要分支坐标;`layout="projected"` 要配合 `shape="line"`,否则为 +`ValueError`。只有长度和半径的 morphology 可使用自动 2D 布局。 +非法布局、模式、形状、后端名称或后端维度组合为 `ValueError`;显式指定但未安装的后端为 +`RuntimeError`。没有 `values` 却设置 `cmap/vmin/vmax/norm/value_label` 也会抛出 `ValueError`。 + +```python +vis.plot2d(morpho, layout="projected", shape="line", projection_plane="xz") +figure_3d = vis.plot3d(morpho, backend="plotly", mode="skeleton") +vis.save_figure(figure_3d, "morphology.html") +``` + +## 对象快捷方法 + +源码:[Morphology](../../../../braincell/morph/morphology.py)、[Branch](../../../../braincell/morph/branch.py)。 +这里的分支对象是 `Branch`;`MorphoBranch` 是形态内分支视图,可通过其 `.branch` 取得 Branch。 + +```text +Morphology.vis2d(self, *, layout=None, shape=None, branch_type_colors=None, + branch_type_edge_colors_2d=None, frustum_edge_linewidth_2d=None, + backend=None, region=None, locset=None, values=None, chooser=None, ax=None, + notebook=None, jupyter_backend=None, return_plotter=False, show=True, + projection_plane="xy", min_branch_angle_deg=25.0, root_layout="type_split") + +Morphology.vis3d(self, *, mode=None, backend=None, region=None, locset=None, + values=None, chooser=None, notebook=None, jupyter_backend=None, + return_plotter=False, show=True) + +Branch.vis2d(self, *, layout=None, shape=None, branch_type_colors=None, + branch_type_edge_colors_2d=None, frustum_edge_linewidth_2d=None, + backend=None, chooser=None, projection_plane="xy", return_plotter=False, + show=True) + +Branch.vis3d(self, *, mode=None, backend=None, chooser=None, notebook=None, + jupyter_backend=None, return_plotter=False, show=True) +``` + +同名参数沿用绘图函数的含义。Branch 方法先创建临时 morphology,再调用相应方法。 +这些方法均返回底层绘图结果,即使 `return_plotter=False` 也不统一返回 `None`。 +`show=True` 会在绘图后调用 `matplotlib.pyplot.show()`;它不是统一的 3D 显示开关, +PyVista 的 Notebook 渲染仍由 `notebook` 等参数控制。 + +快捷方法接收的参数是固定子集:例如 `hooks`、`layout_config` 和独立色图参数应使用 +`vis.plot2d/plot3d`,或者通过 `values=ValueSpec(...)` 传递着色设置。 + +```python +shortcut_ax = cell.morpho.vis2d(show=False, return_plotter=True) +single_branch_ax = morpho.root.branch.vis2d(show=False) +``` + +## 数值与选择 + +源码:[scene.py](../../../../braincell/vis/scene.py)、[_values.py](../../../../braincell/vis/_values.py)。 + +```text +ValueSpec(values, cmap="viridis", vmin=None, vmax=None, norm=None, + label=None, unit_label=None, show_colorbar=True) +OverlaySpec(region=None, locset=None, values=None) +OverlaySpec.values_spec(self) -> ValueSpec | None +``` + +两个类型均为冻结 dataclass,但数组和映射内容仍是引用。`ValueSpec` 的 `values` 必填, +其余参数设置色图、范围、Normalize、标题、单位后缀和色条显示;色图范围使用所传数据单位的裸数值。 +自动范围忽略非有限值。构造 spec 本身不完成形状验证,绘图时才解析。 + +形态值只接受一维数组,按下表顺序匹配长度。数组可带 `brainunit` 单位,单位用于色条标签; +这里的裸数组同样合法。多个长度相等时优先采用表中靠前的解释。 + +| 长度 | 排列与显示 | +| --- | --- | +| `n_branches` | 按 `morpho.branches` 顺序,每分支一个值 | +| `sum(n_segments)` | 按分支顺序拼接,每个分支由近端向远端排列;内部中心线点取相邻 segment 的均值 | +| `sum(n_segments + 1)` | 按分支拼接中心线点值;分支连接处的点仍分别计数 | + +后端消费中心线值;2D segment 色值由其两个端点均值形成,因此逐 segment 输入在转换后 +可能被平滑。例如一条双 segment 分支输入 `[0, 2]`,中心线值成为 `[0, 1, 2]`, +2D 两段着色对应 `[0.5, 1.5]`。CV 数组应通过 Cell 接口解释,不能靠长度相同假定映射正确。 + +`OverlaySpec.region/locset` 接收已求值的 mask;`values` 可为数组、ValueSpec 或 `None`。 +`values_spec()` 对数组创建默认 ValueSpec,对已有 spec 返回原对象,对空值返回 `None`。 +高层绘图函数分别接收 `region=`、`locset=`、`values=`,没有 `overlay=` 参数。 +形态图可以同时叠加高亮、位置标记和数值着色。 + +```python +branch_values = np.array([-65., -60.]) * u.mV +region_mask = region.evaluate(morpho) +location_mask = locset.evaluate(morpho) +spec = vis.ValueSpec(branch_values, label="Voltage", vmin=-70., vmax=-50.) +overlay = vis.OverlaySpec(region=region_mask, locset=location_mask, values=spec) +vis.plot2d(morpho, region=overlay.region, locset=overlay.locset, values=overlay.values) +``` + +## 对比 + +源码:[compare.py](../../../../braincell/vis/compare.py)。 + +```text +compare_morphologies(morphologies, *, titles=None, layout=None, shape=None, + align="soma", figsize=None, min_branch_angle_deg=25.0, + root_layout="type_split", layout_config=None) -> (Figure, tuple[Axes, ...]) + +compare_values(morpho, value_arrays, *, titles=None, cmap=None, vmin=None, + vmax=None, value_label=None, layout=None, shape=None, figsize=None, + min_branch_angle_deg=25.0, root_layout="type_split", layout_config=None) + -> (Figure, tuple[Axes, ...]) +``` + +`morphologies` 是非空 Morphology 序列;`value_arrays` 是非空形态值数组序列,每项一幅图。 +`titles=None` 使用形态名或 `panel i`,显式标题序列长度必须等于面板数。`figsize=None` +生成 `(4.5 * 面板数, 4.5)` 英寸的图,返回新 Figure 与按面板顺序排列的 Axes 元组。 +布局和着色参数沿用 `plot2d`。各面板默认独立色标,传相同 `vmin/vmax` 才固定共同范围。 +`align="soma"` 目前只影响标题后缀,`None` 去掉后缀,不执行额外几何对齐;各面板不共享轴限。 +空序列、标题数量不符为 `ValueError`。 + +```python +comparison, panels = vis.compare_values( + morpho, [branch_values, branch_values + 5. * u.mV], + titles=["Before", "After"], vmin=-70., vmax=-50., +) +assert len(panels) == 2 +``` + +## 时间轨迹 + +源码:[traces.py](../../../../braincell/vis/traces.py)。 + +```text +plot_traces(morpho, time, values_over_time, *, locset=None, labels=None, + colors=None, cmap="tab10", layout=None, shape=None, time_unit_label=None, + value_unit_label=None, figsize=None, sharex=True, show_morphology=True) + -> TracesResult +``` + +| 参数 | 类型与语义 | +| --- | --- | +| `morpho` | Morphology | +| `time` | `(T,)` 时间数组,可带时间单位 | +| `values_over_time` | `(T, n_locations)` 数组,可带物理单位;列顺序对应 locset 点顺序 | +| `locset=None` | 已求值 LocsetMask;有值时其点数必须等于列数,用于形态上标记位置;省略时按列绘制轨迹 | +| `labels=None` | 每列一个标题,默认 `Loc i` | +| `colors=None`、`cmap="tab10"` | 每列一个 Matplotlib 颜色;省略 colors 时从 cmap 取色 | +| `layout/shape=None` | 形态面板的布局与形状;显式传值可固定轨迹标记与底图使用的布局 | +| `time_unit_label/value_unit_label=None` | 单位标签覆盖;省略时从 Quantity 提取 | +| `figsize=None` | 英寸,默认 `(10, max(2*n_locations, 3))` | +| `sharex=True` | 轨迹面板共享时间轴 | +| `show_morphology=True` | 同时绘制形态;设 False 只画轨迹 | + +返回 `braincell.vis.traces.TracesResult`,冻结 dataclass,字段为 `figure: Figure`、 +`morpho_axes: Axes | None`、`trace_axes: tuple[Axes, ...]`。这些字段引用新建图对象, +可继续自定义。时间维度、列数、标签或颜色数量不匹配会抛出 `ValueError`;构图需要至少一列。 + +当前省略 layout/shape 时,底图使用全局默认,标记场景却回退到 stem/frustum,可能导致 +标记与形态错位。绘制带 locset 的轨迹时同时显式传入这两个参数,下面的示例使用 fan/line。 + +下面用两帧样例数据说明输入形状,实际使用时传入对应位点的记录结果: + +```python +time = np.array([0., 0.1]) * u.ms +trace_values = np.array([[-65.], [-64.]]) * u.mV +traces = vis.plot_traces( + morpho, time, trace_values, locset=location_mask, layout="fan", shape="line", +) +assert len(traces.trace_axes) == 1 +vis.save_figure(traces.figure, "traces.png") +``` + +## 动画 + +源码:[movie.py](../../../../braincell/vis/movie.py)。 + +```text +plot_movie(morpho, values_over_time, *, dt=None, dimensionality="2d", + out=None, fps=30, cmap="viridis", vmin=None, vmax=None, value_label=None, + layout=None, shape=None, layout_config=None, mode=None, ax=None, + figsize=None) -> MovieResult +``` + +`values_over_time` 是 `(T, N)`,`T > 0`,每帧 `N` 遵循形态值的三种长度;时间或空间形状 +错误为 `ValueError`。`dt=None` 用帧号标记,时间量用于标题时间;`fps=30` 决定播放/导出帧率, +两者职责不同。`vmin/vmax=None` 从所有帧统一计算范围,动画中保持色标固定。 + +| 参数 | 行为 | +| --- | --- | +| `dimensionality="2d"` | Matplotlib FuncAnimation;`"3d"` 使用 PyVista,其余值为 ValueError | +| `out=None` | 2D 返回内存动画;提供路径时导出,父目录需已存在。2D 支持 GIF/MP4,3D 用 movie writer | +| `fps=30` | 输出帧率,使用正整数 | +| `cmap/value_label` | 色图及显式色条标签;当前动画入口先提取裸数值,需用 value_label 显式写明物理单位 | +| `layout/shape/layout_config=None` | 2D 布局,含义同 plot2d | +| `mode=None` | 3D 场景默认 skeleton;动画渲染使用粗中心线,不生成逐帧管网 | +| `ax=None`、`figsize=None` | 2D Axes 与图尺寸;有 ax 时 figsize 不生效,3D 忽略这两个参数 | + +返回 `braincell.vis.movie.MovieResult`:`animation` 为 FuncAnimation 或 Plotter, +`frames` 为输入帧数,`output_path` 为导出 Path 或 `None`。保留结果引用可避免动画提前回收。 +3D 未指定 out 时返回初始场景,逐帧写入发生在输出文件路径;有输出时最终关闭 Plotter。 +GIF 使用 Pillow writer,MP4 通常需要 FFmpeg;缺失环境由对应 writer 报错。 + +```python +movie_values = np.array([[-65., -60.], [-64., -58.]]) * u.mV +movie = vis.plot_movie( + morpho, movie_values, dt=0.1 * u.ms, out="voltage.gif", + layout="fan", value_label="Voltage [mV]", +) +assert movie.frames == 2 +``` + +## 结构分析 + +### 形态树与 Sholl + +源码:[morphometry.py](../../../../braincell/vis/morphometry.py)。 + +```text +plot_dendrogram(morpho, *, ax=None, color_by_type=True, linewidth=1.5) -> Axes +plot_topology(morpho, *, ax=None, color_by_type=True) -> Axes +plot_sholl(morpho, *, ax=None, step_um=10.0, max_radius_um=None, + color="tab:blue") -> Axes +plot_branch_order_histogram(morpho, *, ax=None, color="tab:gray") -> Axes +``` + +四个函数接收 Morphology,`ax=None` 新建 Axes;每个都返回实际绘图轴。 +`color_by_type=True` 按 branch 类型着色,False 使用统一颜色;`linewidth=1.5` 控制树状图线宽。 +`color` 接收 Matplotlib 颜色。 + +树状图展示累计路径长度,拓扑图展示分支连接关系;分支阶数直方图以根为 0 阶,按树深度计数。 +Sholl 在有坐标时使用根近端为中心的径向距离,缺少完整坐标时使用路径距离。 +`step_um=10.0`、`max_radius_um=None` 接收以微米计的裸数值;后者省略时从几何推导最大半径。 +步长非正为 `ValueError`。Sholl 当前只统计近端在阈值内、远端在阈值外的 segment, +径向向内穿越和同一线段两次穿过球面的情况不做完整几何求交。 + +```python +vis.plot_dendrogram(morpho) +vis.plot_topology(morpho) +vis.plot_sholl(morpho, step_um=5.) +vis.plot_branch_order_histogram(morpho) +``` + +### 离散点拓扑 + +源码:[point_topology.py](../../../../braincell/vis/point_topology.py)。 + +```text +plot_point_topology(node_tree, *, preset="dendrotweaks", layout=None, + layout_scale=1.0, highlight_point_ids=None, highlight_fractions=None, + coverage_mode="fraction", highlight_color="#ef4444", color_mode=None, + values=None, cmap=None, vmin=None, vmax=None, norm=None, value_label=None, + value_unit_label=None, show_colorbar=True, node_color=None, + edge_color=None, root_color=None, ax=None) -> Axes +``` + +`node_tree` 为非空 `bc.NodeTree`,从已初始化 Cell 可取 `cell.node_tree`。 +它接收已经解析的点数据,区别于 Cell 接口负责解析表达式与字段。 +`preset` 可选 `dendrotweaks`(默认配色)、`mono`(单色)、`depth`(按树深度着色), +三者默认布局均为 `twopi`,数值模式默认色图为 `viridis`;`dendrotweaks` 使用统一节点色与根色。 + +| 特有参数 | 类型与行为 | +| --- | --- | +| `highlight_point_ids=None` | 整数 ID 可迭代对象,对指定点全强度高亮 | +| `highlight_fractions=None` | `dict[int, float]`,点 ID 到 `[0,1]` 覆盖比例;优先于 ID 高亮 | +| `color_mode=None` | 根据 values 或预设推断,可选 `solid`、`depth`、`values` | +| `values=None` | `(n_point,)` 数组或 Quantity;与高亮参数互斥,显式 values 模式要求提供数组 | +| `value_unit_label=None` | 色条单位后缀;省略时从 Quantity 提取 | + +其他参数沿用[拓扑图公共样式](#拓扑图的公共样式与错误)。输入类型错误为 `TypeError`; +空树、错误形状、未知预设/布局/颜色模式、无效间距或参数互斥为 `ValueError`。 + +```python +vis.plot_point_topology(cell.node_tree, layout="kamada_kawai", highlight_point_ids=[0]) +``` + +## 布局与缓存 + +### LayoutConfig + +`LayoutConfig` 是冻结 dataclass;构造参数顺序与下表一致,均有默认值,可按关键字覆盖。 +`collision_retry_limit`、`stem_collision_window` 为整数,其余字段为 float。 +`layout_config=None` 使用同样的内置默认配置。几何参数使用名称中注明的微米、弧度或比例裸数值。 +完整实现见 [layout/_config.py](../../../../braincell/vis/layout/_config.py)。 + +| 构造参数 | 默认值 | 含义 | +| --- | --- | --- | +| `collision_margin_um` | `2.0` | 碰撞软惩罚距离 | +| `collision_retry_limit` | `8` | 候选放置重试次数 | +| `stem_collision_window` | `24` | stem 检查的最近分支数 | +| `collision_cell_size_um` | `20.0` | 碰撞空间哈希网格尺寸 | +| `default_bend_fraction` | `0.4` | 默认弯曲段占分支长度的比例 | +| `balloon_bend_fraction` | `0.22` | balloon 弯曲比例 | +| `fan_bend_fraction` | `0.24` | fan 弯曲比例 | +| `radial_bend_fraction` | `0.25` | radial 弯曲比例 | +| `stem_root_full_span_rad` | `radians(150)` | stem 根统一扇区角宽 | +| `stem_root_group_span_rad` | `radians(120)` | stem 类型分组扇区角宽 | +| `balloon_root_span_rad` | `radians(180)` | balloon 根扇区角宽 | +| `balloon_child_span_rad` | `radians(120)` | balloon 子分叉角宽 | +| `balloon_type_split_span_rad` | `radians(110)` | balloon 类型分组角宽 | +| `fan_root_left_span_rad` | `radians(95)` | fan 左侧根扇区角宽 | +| `fan_root_middle_upper_span_rad` | `radians(70)` | fan 中部上扇区角宽 | +| `fan_root_middle_lower_span_rad` | `radians(70)` | fan 中部下扇区角宽 | +| `fan_root_right_span_rad` | `radians(95)` | fan 右侧根扇区角宽 | +| `radial_root_span_rad` | `2*pi` | radial 根扇区角宽 | +| `radial_child_span_rad` | `radians(150)` | radial 子分叉角宽 | +| `legacy_root_child_span_rad` | `radians(120)` | 历史布局参数,当前高层布局枚举不提供 legacy | +| `fan_root_left_max_parent_x` | `0.02` | fan 左扇区最大父位置 | +| `fan_root_middle_min_parent_x` | `0.35` | fan 中部父位置下界 | +| `fan_root_middle_max_parent_x` | `0.65` | fan 中部父位置上界 | +| `stem_collision_weight` | `100.0` | 碰撞代价权重 | +| `stem_tail_delta_weight` | `3.0` | 尾部角偏移权重 | +| `stem_settle_delta_weight` | `0.8` | 稳定角偏移权重 | +| `stem_overturn_weight` | `6.0` | 反向弯曲代价权重 | +| `stem_trunk_tail_delta_weight` | `0.75` | 主干尾部角偏移权重 | +| `stem_side_opening_weight` | `2.0` | 侧枝展开权重 | + +构造器本身不统一校验参数范围,算法在使用时消费字段。修改配置通过构造新对象完成。 + +```python +layout_config = vis.LayoutConfig(collision_margin_um=3., collision_retry_limit=12) +vis.plot2d(morpho, layout="stem", layout_config=layout_config) +``` + +### LayoutCache + +```text +LayoutCache(maxsize=64) +LayoutCache.get_or_build(self, morpho, *, mode, layout_family, root_layout, + min_branch_angle_deg, layout_config, build) -> tuple[LayoutBranch2D, ...] +LayoutCache.clear(self) -> None +len(cache) -> int +``` + +源码:[layout/_cache.py](../../../../braincell/vis/layout/_cache.py)。`maxsize` 为正整数,非正值 +抛出 `ValueError`。缓存键包含形态几何、连接与布局参数;`mode/layout_family/root_layout` +是布局构建器对应的字符串,角度可为 float 或 None,`layout_config` 可为 None。 +`build()` 是无参数回调,仅 miss 时调用,返回布局分支序列。 +命中时返回同一缓存 tuple,`hits/misses` 是可读取的计数;超容量淘汰最久未访问项。 +`clear()` 清空内容并归零计数。缓存几何键有微米数值的小数舍入,极小变化可能复用同一项。 + +高层 `plot2d` 自动使用布局分发器的缓存,公开签名没有 `cache=` 参数。 +这个类型供直接使用布局构建器的高级调用方管理缓存。 + +## 样式配置 + +源码:[config.py](../../../../braincell/vis/config.py)。 + +```text +configure_defaults(*, layout_2d_default=None, shape_2d_default=None, + mode_3d_default=None, branch_type_colors=None, branch_type_edge_colors_2d=None, + replace_branch_type_colors=False, replace_branch_type_edge_colors_2d=False, + alpha_2d=None, alpha_2d_poly=None, alpha_2d_line=None, + frustum_edge_linewidth_2d=None, alpha_3d_tube=None, highlight_color=None, + highlight_alpha=None, marker_color=None, marker_size_2d=None, + marker_radius_3d_um=None) -> VisDefaults +set_defaults(...) # configure_defaults 的同一函数别名,完整参数相同 +get_defaults() -> VisDefaults +reset_defaults() -> VisDefaults +theme(**overrides) -> context manager yielding VisDefaults +publication_theme(preset=None, *, rc_overrides=None) + -> context manager yielding VisDefaults +``` + +`configure_defaults` 就地替换全局配置,`None` 表示保持原值。颜色映射默认合并, +`replace_branch_type_colors`、`replace_branch_type_edge_colors_2d` 为 True 时替换对应映射。 +修改只影响后续绘图。`get_defaults()` 返回独立配置副本,包含颜色字典的复制; +修改副本不会更新全局。`reset_defaults()` 恢复初始配置并返回副本。 + +`VisDefaults` 是可变 dataclass,构造字段顺序与下表一致,每项可用同名关键字传入。 + +| 字段 | 初始默认值 | 类型与含义 | +| --- | --- | --- | +| `layout_2d_default` | `"fan"` | 2D 布局名 | +| `shape_2d_default` | `"frustum"` | 2D 图形模式 | +| `mode_3d_default` | `"geometry"` | 3D 模式 | +| `branch_type_colors` | 内置类型配色字典的副本 | 类型到 RGB,统一 2D/3D | +| `branch_type_edge_colors_2d` | `None` | 边框色映射;省略时从填色派生 | +| `alpha_2d` | `0.8` | 2D 透明度 | +| `alpha_2d_poly` | `None` | 截锥透明度,默认继承 alpha_2d | +| `alpha_2d_line` | `None` | 中心线透明度,默认继承 alpha_2d | +| `frustum_edge_linewidth_2d` | `0.9` | 截锥边框宽度 | +| `alpha_3d_tube` | `1.0` | 3D 管透明度 | +| `highlight_color` | `(255, 215, 0)` | region 高亮 RGB | +| `highlight_alpha` | `0.9` | 高亮透明度 | +| `marker_color` | `(30, 144, 255)` | locset 标记 RGB | +| `marker_size_2d` | `36.0` | 2D scatter 标记面积 | +| `marker_radius_3d_um` | `1.5` | 3D 标记半径,微米 | + +颜色配置接受颜色名称、十六进制色和 RGB 序列。通过 configure 设置时,透明度必须在 +`[0,1]`,边框宽度非负,标记尺寸为正;非法值为 `ValueError`。直接构造 dataclass 不执行 +同样的校验。`theme(**overrides)` 使用 configure 的完整参数,退出时恢复配置,包括异常退出。 + +```text +PublicationTheme(branch_type_colors=, + branch_type_edge_colors_2d=None, rc_params=, + alpha_2d=0.7, frustum_edge_linewidth_2d=0.9, alpha_3d_tube=1.0) +``` + +`PublicationTheme` 为冻结 dataclass,参数分别是类型配色、边框配色、Matplotlib rcParams +映射、2D 透明度、边框宽度及 3D 透明度。`publication_theme(preset=None)` 使用默认实例, +`rc_overrides=None` 可改为字典覆盖其 rc 设置;块退出时恢复两类全局配置。 +未知 rc 键被忽略;未安装 Matplotlib 时仅应用 vis 配置。 + +`PUBLICATION_BRANCH_TYPE_COLORS` 是类型名到整数 RGB 的字典,包含 soma、axon、 +basal_dendrite、apical_dendrite、dendrite 和 custom;`PUBLICATION_RC_PARAMS` 是 +出版样式字典,包含字体、线宽、轴样式和默认 300 dpi 保存设置。通过副本定制可避免改动共享常量。 + +```python +with vis.theme(alpha_2d=0.6): + vis.plot2d(morpho) +with vis.publication_theme(rc_overrides={"font.size": 10}): + paper_ax = vis.plot2d(morpho) + vis.save_figure(paper_ax, "morphology.pdf") +``` + +## 交互回调 + +源码:[hooks.py](../../../../braincell/vis/hooks.py)。 + +```text +VisHooks(on_pick=None, on_hover=None, on_leave=None) +VisHooks.is_active(self) -> bool +PickInfo(branch_index, branch_name, branch_type, segment_index=None, + x=None, value=None, position_um=None, artist=None) +``` + +两者都是冻结 dataclass。回调签名为 `on_pick(info: PickInfo) -> None`、 +`on_hover(info: PickInfo) -> None`、`on_leave() -> None`,由图形事件循环同步调用, +返回值被忽略。任意回调非 None 时 `is_active()` 为 True。回调中的耗时操作会阻塞交互。 + +| PickInfo 字段 | 类型与含义 | +| --- | --- | +| `branch_index` | int,morpho.branches 中的索引 | +| `branch_name/branch_type` | str,分支名与类型 | +| `segment_index` | int 或 None,分支内从近端开始的 segment 索引 | +| `x` | float 或 None,分支归一化弧长 `[0,1]` | +| `value` | float 或 None,所拾取图元的标量;单位需从原始数据获知 | +| `position_um` | ndarray 或 None,2D/3D 场景坐标,单位微米;自动布局坐标区别于原始 3D 坐标 | +| `artist` | 底层图元引用或 None,便于后端特定操作 | + +Matplotlib 支持三个回调,但 Agg 等静态后端不会自动产生鼠标事件。 +PyVista 仅支持 on_pick,部分位置与数值字段可能为空;Plotly 有内置悬停提示但没有接入 VisHooks。 +拾取信息目前描述形态分支与 segment,没有统一的 CV/node ID 字段。 + +```python +picked = [] +hooks = vis.VisHooks(on_pick=lambda info: picked.append((info.branch_index, info.x))) +interactive_ax = vis.plot2d(morpho, hooks=hooks) +assert hooks.is_active() +``` + +## 后端与导出 + +### 返回与显示 + +| 调用 | 返回结果 | 显示方式 | +| --- | --- | --- | +| Matplotlib 绘图 | `Axes`,或对应的复合结果 | `plt.show()`;Notebook 可展示图对象 | +| Plotly plot3d | `plotly.graph_objects.Figure` | `figure.show()` 或 Notebook 展示 | +| PyVista,`notebook=False` | `pyvista.Plotter` | `plotter.show()` | +| PyVista,Notebook,`return_plotter=False` | viewer,或支持 `_repr_html_()` 的 HTML 展示对象 | Notebook 渲染;HTML 路径用于嵌入展示 | +| PyVista,Notebook,`return_plotter=True` | `pyvista.Plotter` | 某些 Notebook 路径仍会先建立 viewer;需要原始对象且不触发 Notebook 展示时用 `notebook=False` | + +每次绘图生成独立的图形对象或向指定 ax 添加内容;调用者负责保留动画引用和关闭不用的图。 +PyVista Notebook 的环境与后端尝试失败时会抛出带诊断信息的 `RuntimeError`。 + +### save_figure + +```text +save_figure(figure, path, *, dpi=None, transparent=False, format=None) -> pathlib.Path +``` + +源码:[export.py](../../../../braincell/vis/export.py)。 +`figure` 接受 Matplotlib Axes/Figure、PyVista Plotter 或 Plotly Figure,复合结果先取其 +`.figure`;PyVista HTML 展示对象不是此函数接收的图句柄。 +`path` 为 str 或 PathLike,写入指定文件,父目录需存在,已有目标可能被覆盖。 +`dpi=None` 使用后端默认,`transparent=False` 控制透明背景;`format=None` 从路径后缀推断。 + +| 后端 | 支持与差异 | +| --- | --- | +| Matplotlib | 调用 Figure.savefig,支持 PNG/PDF/SVG 等格式;dpi、transparent、format 直接传递 | +| PyVista | 位图截图或 HTML 导出;矢量路径调用可用的 `save_graphic`,缺少该能力时拒绝;dpi 用于截图缩放 | +| Plotly | HTML 使用 write_html;其他格式使用 write_image,通常需要 Kaleido;dpi 和 transparent 不传递 | + +返回写入路径,不改变输入句柄的类型。未知图对象为 `TypeError`,PyVista 缺少矢量导出方法时为 +`ValueError`,缺少目录或导出依赖时传播文件系统或后端异常。HTML/矢量分派依据路径后缀, +因此 `format` 应与后缀一致。动画文件通过 `plot_movie(out=...)` 输出。 + +## 验证入口与迁移讨论 + +接口边界和空间映射的测试位于 [cell_topology_test.py](../../../../braincell/vis/cell_topology_test.py), +各绘图模块有同目录测试;完整使用流程见[可视化教程](../../../../examples/multi_compartment/vis.ipynb)。 +[Visualization](visualization.md) 汇总后端能力和视觉回归缺口。 +下一步接口调整和两个入口的候选设计见[迁移提案](../proposals/braintools-migration.md)。 diff --git a/docs/design/vis/current/visualization.md b/docs/design/vis/current/visualization.md new file mode 100644 index 00000000..bc9fa111 --- /dev/null +++ b/docs/design/vis/current/visualization.md @@ -0,0 +1,55 @@ +# Visualization + +`braincell.vis` 面向脚本和 Notebook,提供形态展示、数据着色、拓扑分析和结果导出。 +公共入口见[包导出](../../../../braincell/vis/__init__.py),完整用法见 +[可视化教程](../../../../examples/multi_compartment/vis.ipynb)和 [API 文档](../../../apis/vis.rst)。 +完整签名、参数、返回值和调用条件见 [Vis API](api.md)。 + +## 当前支持什么 + +| 能力 | 主要内容 | 代表接口 | +| --- | --- | --- | +| 形态展示 | 2D 真实坐标投影、树形与截锥布局,stem、balloon、radial 等布局;3D 骨架与几何展示 | `plot2d`、`plot3d`、`LayoutConfig` | +| 数据展示 | 按 branch、segment 或中心线采样点着色,带单位色条,region 高亮与 locset 标记 | `ValueSpec`、`OverlaySpec` | +| 对比与动态结果 | 多形态和多组数值对比,位点时间轨迹与形态联动,随时间着色的动画 | `compare_morphologies`、`compare_values`、`plot_traces`、`plot_movie` | +| 结构分析 | 树状图、拓扑图、Sholl 分析、分支阶数统计,以及 Cell 的 branch、CV、node 三级拓扑 | `plot_dendrogram`、`plot_topology`、`plot_sholl`、`plot_branch_order_histogram`、`plot_cell_topology`、`plot_point_topology` | +| 交互与输出 | 拾取回调、主题与出版样式、图片和动画导出 | `VisHooks`、`PickInfo`、`theme`、`publication_theme`、`save_figure` | + +已有 Cell 时,形态图传 `cell.morpho`,拓扑与运行时数据图直接传 `cell`: + +```python +from braincell import vis + +ax = vis.plot2d(cell.morpho) +vis.save_figure(ax, "morphology.png") +vis.plot_cell_topology(cell, level="cv") +# cell 已初始化时,可读取当前膜电压。 +vis.plot_cell_topology(cell, level="node", value="V") +``` + +带对象构造和初始化的完整示例见 [从 Cell 开始](api.md#从-cell-开始)。 + +## 后端与数据 + +| 后端 | 展示与输出 | 交互 | +| --- | --- | --- | +| Matplotlib | 2D 图、拓扑、轨迹,位图与矢量图导出;`plot_movie` 使用 `FuncAnimation` | `VisHooks` 支持拾取、悬停和离开,事件触发需要交互式后端 | +| PyVista | 3D 中心线与管状几何、截图和 HTML 导出;`plot_movie` 写出 3D 动画 | `VisHooks` 支持拾取 | +| Plotly | 3D 分支线、数值着色、HTML 导出;静态图片导出需要额外图像引擎 | 支持视角操作和悬停提示,尚未接入 `VisHooks` | + +绘图依赖按需加载,`braincell[vis]` 包含 Matplotlib、NetworkX、PyVista 和 Plotly。 +动画编码、Notebook 展示及部分导出格式还需要对应后端的运行环境。 + +现有数据流是“形态与位点数据 → 布局和场景 → 后端渲染”。场景构建负责将几何转换为 +微米数值,保留着色数据的单位标签;布局配置和缓存用于重复绘图。 +形态图读取 `Morphology`,Cell 拓扑图还读取离散结果及 region/locset 的解析结果。 +`plot_cell_topology(level="node")` 需要先调用 `cell.init_state()`。 + +## 验证现状 + +[vis 源码目录](../../../../braincell/vis/)已有与模块相邻的布局、场景、后端、着色、 +交互、导出和动画测试;布局、场景及 2D 渲染另有可选的 `pytest-benchmark` 用例。 +当前缺少实际运行的像素回归基线。已有 Matplotlib artist 断言可检查图元和属性, +完整视觉回归还需要提交代表性基线图,并配置执行图像比较的 CI。 + +迁移方向见 [Braintools Migration](../proposals/braintools-migration.md),事项进度见 [Vis TODO](../TODO.md)。 diff --git a/docs/design/vis/proposals/braintools-migration.md b/docs/design/vis/proposals/braintools-migration.md new file mode 100644 index 00000000..312231f4 --- /dev/null +++ b/docs/design/vis/proposals/braintools-migration.md @@ -0,0 +1,147 @@ +# Braintools Migration + +状态:讨论中。准备把可视化集中到 braintools 的一个模块,提供简单绘图和 GUI 两类入口。 +现有 `braincell.vis` 承接简单绘图,GUI 后续接入 BrainVis。两个入口需要共用形态、 +数值与位点映射,当前接口契约见 [Vis API](../current/api.md)。 + +## 当前调用与问题 + +已有且已初始化的 Cell,当前这样查看形态和膜电压: + +```python +from braincell import vis + +shape_ax = vis.plot2d(cell.morpho) +voltage_ax = vis.plot_cell_topology(cell, level="cv", value="V") +``` + +这两个调用的输入语义不同:形态图接收 Morphology,`values` 按 branch、segment 或 +中心线点解释;Cell 图接收 Cell,`value` 按 CV/node 或字段选择器解释。 +例如 CV 电压数组即使恰好与 segment 数量相同,直接交给形态图也可能画到错误位置。 +GUI 选择一个位点后查看曲线,同样需要明确它对应哪个 CV 或 node。 + +vis 的场景构建依赖 Morphology,Cell 拓扑依赖 NodeTree、region/locset 和内部字段解析。 +BrainCell 已经依赖 braintools,搬动代码时需要处理反向导入。迁移的核心是让两个入口 +使用一致的数据解释,并把 BrainCell 对象适配与绘图、窗口显示分开。 + +## 模块位置与入口 + +下面两处位置可选;本提案的候选代码统一用 `braintools.visualize.cell` 展示: + +| 位置 | 好处 | 成立条件与代价 | +| --- | --- | --- | +| `braintools.visualize.cell` | 沿用已有 visualize 命名空间,将细胞形态绘图与 GUI 放在一起 | 检查 visualize 的导入路径和命名,确保普通绘图不会加载 GUI | +| `braintools.vis` 等同级模块 | 可以独立组织这套接口与依赖 | 需要向用户说明它与已有 visualize 中绘图功能的分工 | + +建议先采用已有命名空间下的专用子模块;最终名称需要结合 braintools 源码中的公开导出、 +重名函数和可选依赖配置确认。“两个入口”指脚本绘图函数与 GUI 启动函数,两者可以复用数据层。 + +## Cell 如何传入 + +### 候选 A:直接传 Cell + +以下为候选接口: + +```python +from braintools.visualize import cell as cellvis + +ax = cellvis.plot2d(cell, value="V") +window = cellvis.gui(cell) +``` + +调用者直接使用现有 Cell。适配器负责取形态、解析字段、确定空间位置,再交给绘图或 GUI。 +现有 Morphology 调用也可继续接收 `cell.morpho`;增加 Cell 输入后,需要明确哪些参数 +随输入对象改变含义。 + +这条路径改写调用较少,但必须安排适配器的归属与延迟导入。若适配器在 braintools, +它应是按需加载的 BrainCell 集成层;基础绘图模块不能在导入时依赖 BrainCell。 +还需要记录 BrainCell 内部接口变化的适配责任。 + +### 候选 B:显式转换共享数据 + +以下为候选接口: + +```python +from braintools.visualize import cell as cellvis + +data = cellvis.from_cell(cell, value="V") +ax = cellvis.plot2d(data) +window = cellvis.gui(data) +``` + +`from_cell` 是候选便捷转换器,内部可调用 BrainCell 提供的数据导出能力。 +`data` 表示某一时刻的形态、拓扑、字段值、单位和位置映射。绘图与 GUI 消费同一份数据, +可直接比较显示结果,也便于离线数据接入。 + +代价是需要定义转换结果和更新时间。数据快照不是活模型,Cell 推进后应重新提取; +位置回传使用稳定的 branch/CV/node 标识,不能只依赖渲染网格的临时下标。 + +| 比较项 | 直接传 Cell | 显式数据 | +| --- | --- | --- | +| 使用步骤 | 一次调用 | 转换后再绘图,可供多个视图复用 | +| BrainCell 依赖 | 调用时通过适配器读取 | 集中在转换器,渲染只消费数据 | +| 数据时刻 | 绘图时读取;GUI 持续读取需要另定规则 | 明确对应一次快照 | +| 迁移工作 | 保留现有对象调用习惯,持续维护适配 | 先定义共享数据和映射,便于独立验证 | + +建议以共享数据为基础,支持直接传 Cell 的便捷调用,并让显式转换保持可用。 +决定前先用一个带 region、多个 CV 和两帧电压结果的 Cell 验证两种调用能显示一致位置与数值。 + +## 还需要商量的接口选择 + +### 数值是否显式标明空间 + +可以保留当前形态 `values=` 与 Cell `value=` 两套语义,也可以采用统一的数值描述。 +后一种方式需要显式标明 `branch/segment/centerline/cv/node` 空间,避免数组长度相等时误判。 +候选调用示意: + +```python +ax = cellvis.plot2d(cell, values=cv_voltage, space="cv") +``` + +建议保留命名字段的便捷写法 `value="V"`,显式数组增加空间信息。 +准备时需明确:CV 值怎样投到中心线、边界点怎样着色、未覆盖机制怎样显示,以及如何指定 +population 成员。现有行为及 NaN 映射见 [Cell 数值来源](../current/api.md#数值来源与空间)。 + +### 返回结果与显示方式 + +| 方案 | 使用体验 | 代价 | +| --- | --- | --- | +| 保留 Axes、Plotly Figure、PyVista Plotter | 可以继续使用原生后端的自定义方法 | 保存、显示和关闭方式随后端变化,Notebook 返回值还需明确 | +| 返回统一绘图结果,内部保存后端对象 | 可以提供一致的显示、导出和关闭方法 | 增加包装层,需要覆盖现有后端特性及旧返回值兼容 | + +建议迁移简单绘图时保留原生结果,GUI 返回独立窗口句柄。简单绘图的创建与显示行为应明确, +尤其要处理现有 `show`、`notebook` 与 `return_plotter` 的差异。 +若常见调用仍需大量后端分支,再根据示例评估统一结果对象。 + +### GUI 刷新与选择回传 + +共享数据至少要表达坐标和半径、拓扑连接、空间 ID、数值单位、时间轴及已解析选择。 +GUI 的单次查看可先读取快照;需要查看新状态时显式刷新。持续实时跟随 Cell 需要确定 +采样时机和数据提供方,已有图形回调也需要从 branch/segment 扩展到可追溯的空间标识。 + +下一步用“点选一个 CV,展示该 CV 的轨迹”检验数据是否足够,并明确选中结果返回给调用者 +的内容。窗口刷新与选择接口据此讨论。 + +### 旧入口如何过渡 + +可保留 `braincell.vis` 和 Morphology/Branch 快捷方法,内部转发到 braintools; +也可以要求调用者迁移到新导入路径。建议先转发,以便现有脚本逐步迁移。 + +转发成立的条件是对应参数、返回对象、显示行为和异常仍可兼容。新增空间描述等改变需要 +显式转换,不能只替换 import。保留期限和弃用提示需结合版本发布安排决定;具体旧调用 +清单以 [Vis API](../current/api.md) 为依据。 + +## 准备工作与验证 + +1. 对照公共接口规格清点源码、测试、样例和依赖,定位 Morphology、Cell、NodeTree、 + region/locset、单位和内部字段解析的适配位置。 +2. 检查 braintools 模块布局与可选依赖,确定共享数据和适配器归属;分别验证基础导入、 + 简单绘图及 GUI 入口加载了哪些依赖。 +3. 准备小型分叉形态、真实形态、CV/node 拓扑和两帧电压数据,验证几何、数值、单位、 + 选择与轨迹在两个入口中对应同一位置。 +4. 迁移现有测试与必要夹具,核对旧入口转发、返回值、初始化要求、population 选择、 + 动画及文件导出;补齐代表性图像基线并比较大形态布局和渲染耗时。 +5. 用实际可运行的接口更新教程和 Sphinx API,列出旧调用到新调用的对应关系。 + +已确认的方向是迁入 braintools 并提供简单绘图与 GUI;模块位置、数据入口和兼容方式仍在讨论。 +事项进度见 [Vis TODO](../TODO.md)。 diff --git a/docs/developer/contributing.md b/docs/developer/contributing.md index 649dc590..dbc44847 100644 --- a/docs/developer/contributing.md +++ b/docs/developer/contributing.md @@ -1,67 +1,84 @@ -# Contributing +--- +myst: + heading_anchors: 2 +--- -Contributions of all kinds are welcome — bug reports, fixes, new channels, -documentation, and examples. `braincell` is developed on GitHub at -[chaobrain/braincell](https://github.com/chaobrain/braincell). +# 贡献流程 -## Development setup +从一个可描述、可验证的问题开始贡献。错误报告给出最小复现,新增模型说明来源和用途, +接口改动先查看对应模块的讨论。代码与设计入口见 [项目导航](project_layout.md)。 -Clone the repository and install in editable mode with the testing extras, then -install the pre-commit hooks: +## 配置开发环境 + +在独立 Python 环境中克隆仓库;外部贡献者可以先在 GitHub 创建 fork,再克隆自己的副本。 +以下命令克隆主仓库,安装开发依赖和提交检查工具: ```bash git clone https://github.com/chaobrain/braincell.git cd braincell -pip install -e ".[testing]" +python -m venv .venv +source .venv/bin/activate +python -m pip install -e ".[dev]" pre-commit install ``` -## The workflow - -1. **Open an issue** (or find one) describing the bug or feature. -2. **Create a branch** for your work. -3. **Write a test first.** The project convention is: *reproduce a bug with a - failing test, then fix until it passes.* See {doc}`testing`. -4. **Implement** the change, keeping code simple and intuitive. -5. **Run the checks** locally: - ```bash - pytest braincell/ - pre-commit run --all - ``` -6. **Open a pull request** referencing the issue. - -## Coding conventions - -These conventions come from the project's developer guide and keep the codebase -coherent: - -- **Units are mandatory.** Every physical quantity must carry an explicit - `brainunit` unit; bare numbers are rejected. See {doc}`../concepts/units`. -- **One canonical namespace per API.** Public symbols are re-exported through - the top-level `braincell` namespace; internal packages are underscore-prefixed - (`_cv`, `_compute`, …). See {doc}`project_layout`. -- **Ask when requirements are ambiguous** rather than guessing. -- **NumPy-style docstrings** for every public class, method, and function, with - a runnable, doctestable Examples section where it helps. -- **Absolute imports** for internal modules - (`from braincell.morph import Morphology`). - -## Documentation - -Documentation lives in `docs/` and is built with Sphinx (`sphinx_book_theme`, -MyST, and `myst-nb` for notebooks). To build locally: +Windows PowerShell 将激活命令替换为 `.venv\Scripts\Activate.ps1`。 +已有独立环境时可直接安装。Python 版本要求以 `pyproject.toml` 的 `requires-python` 为准。 + +`dev` 包含测试、文档和 pre-commit 依赖,定义见 +[pyproject.toml](https://github.com/chaobrain/braincell/blob/main/pyproject.toml)。 +CPU/GPU 安装选择见 [安装指南](../getting_started/installation.ipynb), +环境问题见 [开发排错](troubleshooting.md)。下文命令均在仓库根目录执行。 + +新增依赖写入 `pyproject.toml`;`requirements*.txt` 仅供 CI 等工具引用这些 extras。 + +## 修改与验证 + +1. **定位事项**:阅读 [模块 TODO](https://github.com/chaobrain/braincell/blob/main/docs/design/TODO.md) 和对应 Current;涉及新设计时,说明要解决的问题并链接相关 proposal。 +2. **建立工作分支**:例如 `git switch -c fix/swc-validation`,让一个 PR 聚焦一个问题。 +3. **形成可验证的修改**:修复错误时先写失败的复现测试;新增模型按 [扩展指南](extending.md) 验证其动力学和集成行为。 +4. **运行相关检查**:先运行修改模块的测试及受影响的示例,共享行为变更再扩大测试范围,具体命令见 [测试指南](testing.md)。 +5. **提交前核对**:检查本次修改涉及的 Design、实现与测试、实际相关示例,补齐受影响的说明和调用。 + +物理单位、随机数、编译循环、导入、许可证及 docstring 的约定集中在 +[仓库规则](https://github.com/chaobrain/braincell/blob/main/AGENTS.md)。 +提交前同步检查的范围见 +[Design、代码与示例](https://github.com/chaobrain/braincell/blob/main/AGENTS.md#design-code-and-examples)。 + +## 文档修改 + +贡献步骤写在 Developer;接口签名、公式、架构和设计讨论写在对应 Design 页面。 +更新模块契约时,在 Developer 保留到具体主题的链接。说明的分类与写法见 +[Design 规范](https://github.com/chaobrain/braincell/blob/main/docs/design/AGENTS.md)。 +已有示例在实际位置维护,历史 specs 用于追溯旧决定。 + +构建网站: + +```bash +python -m sphinx -b html docs docs/_build/html +``` + +结果位于 `docs/_build/html/index.html`。网站构建当前不执行 notebook;修改其中的可运行代码后, +应另行运行相关 notebook 或配套脚本。API reference 从 docstring 生成,Design 通过仓库链接阅读。 + +## 提交 PR + +提交前运行格式与静态检查,检查差异中是否混入运行产物: ```bash -cd docs -make html # output in docs/_build/html +pre-commit run --all-files +git diff --check +git status --short ``` -Narrative pages are Markdown (MyST) or reStructuredText; tutorials and examples -are Jupyter notebooks. The API reference is generated from docstrings via -`autosummary`, so improving a docstring improves the reference automatically. +格式和 lint 使用 `pyproject.toml` 中配置的 Ruff,行宽为 120,保留已有引号风格。 +pre-commit 可能修改文件;检查其结果后再提交。推送工作分支,按 +[PR 模板](https://github.com/chaobrain/braincell/blob/main/.github/PULL_REQUEST_TEMPLATE.md) 创建 PR,描述中说明: -## See also +- 问题或使用场景,关联的 Issue、proposal 或模型来源。 +- 修改后的行为,必要时给一个修改前后的具体例子。 +- 实际执行的测试、示例及结果;尚未验证的部分写明原因。 +- 影响已有调用的变化,以及需要的迁移步骤。 -- {doc}`testing` — the testing conventions in detail. -- {doc}`extending` — adding channels, ions, and integrators. -- {doc}`project_layout` — where everything lives. +数值模型同时注明参照、单位、温度、初态、求解器、步长与比较容差; +性能结论附上测量配置及复现入口。 diff --git a/docs/developer/extending.ipynb b/docs/developer/extending.ipynb deleted file mode 100644 index 4d4d9ab9..00000000 --- a/docs/developer/extending.ipynb +++ /dev/null @@ -1,148 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b5f84ab1", - "metadata": {}, - "source": [ - "# Extending braincell\n", - "\n", - "`braincell`'s registries are open: you can add your own ion channels, ion\n", - "species, synapses, and numerical integrators, and they become usable by name\n", - "just like the built-ins.\n", - "\n", - "## Adding an ion channel\n", - "\n", - "Concrete channels live in {mod}`braincell.channel` and **self-register** at\n", - "import time with the `@register_channel` decorator from {mod}`braincell.mech`.\n", - "A new channel subclasses the appropriate base ({class}`braincell.IonChannel` /\n", - "{class}`braincell.Channel`), implements its current and gating dynamics, and\n", - "registers a name:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "13f0551b", - "metadata": {}, - "outputs": [], - "source": [ - "import braincell\n", - "from braincell.mech import register_channel\n", - "\n", - "@register_channel(\"MyNa\")\n", - "class MyNa(braincell.Channel):\n", - " # define states, derivatives, and current here\n", - " ..." - ] - }, - { - "cell_type": "markdown", - "id": "c3a57f7f", - "metadata": {}, - "source": [ - "Once imported, the registered name is what string-based declarations resolve to:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4baa263b", - "metadata": {}, - "outputs": [], - "source": [ - "import braincell.mech as mech\n", - "mech.Channel(\"MyNa\", g_max=0.1 * u.S / u.cm**2)" - ] - }, - { - "cell_type": "markdown", - "id": "a418ef60", - "metadata": {}, - "source": [ - "Use the existing channels in `braincell/channel/` (sodium, potassium, calcium,\n", - "…) as templates — they show the expected state declaration, parameter\n", - "normalization, and docstring style.\n", - "\n", - "## Adding an ion species\n", - "\n", - "Ion species register the same way with `@register_ion`, subclassing the\n", - "relevant ion base ({class}`braincell.Ion`). Model the reversal potential and any\n", - "concentration dynamics, following the patterns in `braincell/ion/`.\n", - "\n", - "## Adding a synapse\n", - "\n", - "Synaptic point processes register with `@register_synapse` and are declared via\n", - "{class}`braincell.mech.Synapse`. See `braincell/synapse/exponential.py` for the\n", - "worked examples (`ExpSyn`, `Exp2Syn`).\n", - "\n", - "## Adding an integrator\n", - "\n", - "Solvers live in {mod}`braincell.quad` and register with\n", - "`@register_integrator`, after which they are selectable by name through the\n", - "`solver=` argument of any cell:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60728a7b", - "metadata": {}, - "outputs": [], - "source": [ - "from braincell.quad import register_integrator\n", - "\n", - "@register_integrator(\"my_solver\")\n", - "def my_solver_step(...):\n", - " # advance the state by one dt\n", - " ..." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5d15b594", - "metadata": {}, - "outputs": [], - "source": [ - "cell = braincell.SingleCompartment(size, solver=\"my_solver\")" - ] - }, - { - "cell_type": "markdown", - "id": "0f9d0152", - "metadata": {}, - "source": [ - "The {doc}`../integration/advanced` guide covers the integrator protocol\n", - "({class}`braincell.DiffEqState`, {class}`braincell.DiffEqModule`) in depth.\n", - "\n", - "## Testing your extension\n", - "\n", - "Add a co-located `*_test.py` next to your new module (see {doc}`testing`) that:\n", - "\n", - "- constructs the mechanism with united parameters,\n", - "- checks the dynamics against a known reference (an analytic limit, a published\n", - " trace, or NEURON), and\n", - "- exercises any edge cases.\n", - "\n", - "## See also\n", - "\n", - "- {doc}`../concepts/ions_channels` — how ions and channels relate.\n", - "- {doc}`../concepts/integration` — the solver concept.\n", - "- {doc}`project_layout` — where each kind of mechanism lives." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/developer/extending.md b/docs/developer/extending.md new file mode 100644 index 00000000..44b16dab --- /dev/null +++ b/docs/developer/extending.md @@ -0,0 +1,48 @@ +# 扩展模型与积分器 + +添加机制或积分器时,先选择已有模板,再实现模型特有的方程,并验证它能通过 Cell 的实际路径运行。 +下表给出阅读顺序;完整签名、公式与可运行模型例子由对应 Design Current 维护。 + +| 扩展对象 | 先读模板与契约 | 再读注册与集成 | +| --- | --- | --- | +| Channel | [HH/Markov 模板](https://github.com/chaobrain/braincell/blob/main/docs/design/channel/current/api.md)、[模板校验](https://github.com/chaobrain/braincell/blob/main/docs/design/channel/current/template-invariants.md) | [Mech 注册](https://github.com/chaobrain/braincell/blob/main/docs/design/mech/current/api.md#注册与事件契约)、[Cell.paint](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/current/api.md#paint-与-place) | +| Ion | [公共离子模型](https://github.com/chaobrain/braincell/blob/main/docs/design/ion/current/api.md)、[生命周期与 KineticIon 扩展模板](https://github.com/chaobrain/braincell/blob/main/docs/design/ion/current/kinetic-ion-api.md) | [电流快照与调度](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/current/architecture.md#离子电流快照与调度) | +| Synapse | [动力学与事件契约](https://github.com/chaobrain/braincell/blob/main/docs/design/synapse/current/api.md)、[状态归属](https://github.com/chaobrain/braincell/blob/main/docs/design/synapse/current/architecture.md) | [事件源与连接](https://github.com/chaobrain/braincell/blob/main/docs/design/network/current/connections.md) | +| Integrator | [积分注册与调用](https://github.com/chaobrain/braincell/blob/main/docs/design/quad/current/api.md)、[目标协议](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/current/api.md#积分协议) | [求解路径](https://github.com/chaobrain/braincell/blob/main/docs/design/quad/current/architecture.md) | + +## 添加 Channel 或 Ion + +1. 找到所属家族及相近模型,在 `braincell/channel/` 或 `braincell/ion/` 的合适模块中实现。 +2. 按 Current 选模板,声明所需参数、状态和离子依赖,注明方程来源及本地调整。 +3. 用 `register_channel` 或 `register_ion` 注册具体模型;进入公共模型目录时更新包导出及相邻测试。 +4. 先验证独立模型的初始化、reset、导数、单位和电流符号,再通过 paint 放入 Cell 验证绑定、状态形状及运行结果。 + +HH/Markov 还需验证门稳态或概率守恒;动态 Ion 需检查浓度、守恒约束及电流驱动。 +可复用的模型出处记录在 +[Ion/Channel 文献表](https://github.com/chaobrain/braincell/blob/main/docs/design/ion/references/ion-channel-bibliography.md), +具体模型的对照配置和结果随对应验证工作流维护。 + +## 添加 Synapse + +以 `braincell/synapse/exponential.py` 及相邻测试为起点,确定内部状态和事件输入契约, +实现动力学后用 `register_synapse` 注册。通过 Cell.place 和 Network.connect 完成一次真实事件投递。 + +验证事件前后的状态变化、多事件聚合、电流单位与符号、衰减轨迹及 reset。 +如果改变的是 Connection weight 更新规则,先阅读 +[可塑性讨论](https://github.com/chaobrain/braincell/blob/main/docs/design/network/proposals/connection-plasticity.md), +确定状态应该属于突触模型还是连接。 + +## 添加 Integrator + +在 `braincell/quad/` 选择相邻算法作为实现和测试参照,按目标协议实现步函数, +使用 `register_integrator` 声明名称、别名和阶数。 +明确该算法适用于通用 ODE,还是需要 Cell 的专用电压接口。 + +用解析解检查单步误差和多步收敛,验证阶段钩子、单位、状态形状及编译循环。 +面向 Cell 的算法还需覆盖多 CV、分叉与边界输入,现有问题见 +[边界输入提案](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/proposals/explicit-solver-boundary-inputs.md)。 + +## 完成贡献 + +更新对应 Current 和实际相关示例,在模块 TODO 中关联完成内容或剩余问题。 +测试命令与 fixture 用法见 [测试指南](testing.md),提交要求见 [贡献流程](contributing.md#提交-pr)。 diff --git a/docs/developer/index.rst b/docs/developer/index.rst index bce7ccf2..e5af3c86 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -1,14 +1,17 @@ -:orphan: +贡献者指南 +========== -Developer Guide -=============== +从配置开发环境、定位代码到验证修改和提交 PR,本指南按贡献任务组织阅读入口。 +模块的完整接口、方程和架构由 Design 维护,各主题链接到对应说明。 -This section is for people who want to **work on** ``braincell`` — fix a bug, -add a channel, write a solver, or contribute documentation. If you only want to -*use* the library, the :doc:`../concepts/architecture` and modeling guides are -what you want. +首次贡献从 :doc:`contributing` 开始;已有明确任务时,直接进入相应主题。 +仓库目录职责及规划见 :doc:`../repository`。 -- :doc:`contributing` — dev setup, workflow, and conventions. -- :doc:`project_layout` — how the package maps onto the architecture layers. -- :doc:`testing` — test conventions and the bug-fix workflow. -- :doc:`extending` — adding custom channels, ions, synapses, and integrators. +.. toctree:: + :maxdepth: 1 + + contributing + project_layout + testing + extending + troubleshooting diff --git a/docs/developer/project_layout.md b/docs/developer/project_layout.md index a777e72c..5b0073af 100644 --- a/docs/developer/project_layout.md +++ b/docs/developer/project_layout.md @@ -1,98 +1,38 @@ -# Project Layout - -`braincell` is organized so that the **public API is flat** (everything is -re-exported through the top-level `braincell` namespace) while the -**implementation is layered** into underscore-prefixed internal packages. This -page maps the package tree onto the {doc}`../concepts/architecture` layers. - -## Naming convention - -- **Internal packages** carry a leading underscore (`_base`, `_cv`, `_compute`, - `_single_compartment`, `_multi_compartment`, `_misc`) because their *import - paths* are not part of the supported public API. -- **Public re-exports** flow through `braincell/__init__.py`, plus the curated - sub-namespaces `braincell.channel`, `braincell.ion`, `braincell.synapse`, - `braincell.mech`, `braincell.quad`, `braincell.morph`, `braincell.filter`, - `braincell.io`, and `braincell.vis`. -- **Modules inside** an internal package are unprefixed (`base.py`, `lower.py`, - `runtime.py`) because they are import targets for sibling code in the same - package. - -## The map - -```{list-table} -:header-rows: 1 -:widths: 30 30 40 - -* - Package - - Layer - - Responsibility -* - `_base`, `_base_channel`, `_base_ion` - - declaration - - `HHTypedNeuron`, `IonChannel`, `Ion`, `MixIons`, `Channel`, `Synapse` -* - `_single_compartment` - - declaration - - the `SingleCompartment` class -* - `_multi_compartment` - - declaration + runtime - - `Cell`, `RunResult`, paint/place pipeline, probes, run loop -* - `mech` - - declaration - - the declarative mechanism specs (`Channel`, `Ion`, clamps, `Synapse`, …) -* - `filter` - - declaration - - region & locset selection algebra -* - `morph` - - geometry - - `Branch`, `Morphology`, typed branches, the mutable analysis tree -* - `_cv` - - discretization - - control volumes and CV policies -* - `_compute` - - runtime - - the execution graph and `CellRuntimeState` -* - `quad` - - integration - - the integrator protocol and solver registry -* - `channel`, `ion`, `synapse` - - library - - concrete, self-registering mechanism implementations -* - `io` - - IO - - SWC / ASC / NeuroML2 readers, NeuroMorpho client, checkpointing -* - `vis` - - visualization - - 2-D / 3-D rendering, morphometry, export -``` - -## Prose lives under `docs/` - -No `.md` file sits inside `braincell/`. Written material has two homes, -neither of which is part of this published site: - -- `docs/specs/YYYY-MM-DD-.md` — the spec and plan for a single change, - written before the implementation. The date prefix keeps the directory in - chronological order. -- `docs/design/.md` — durable design notes, invariants, and - architecture maps that outlive any one change. A topic that needs several - documents gets a subdirectory (`docs/design/network/`). - -`docs/design/TODO.md` is the living project-wide architecture and status -index. Keep detailed contracts in their topic documents and link them from -the index instead of duplicating their full specification there. - -Files are named for what they document rather than where the code lives, so -`docs/design/io-swc-reader-invariants.md` rather than a `README.md` beside the -reader. Code that depends on a note cites it by `docs/` path from the module -docstring. - -## Tests are co-located - -Test files live **next to the source** they cover and are named `*_test.py` -(e.g. `braincell/io/neuromorpho/client.py` → -`braincell/io/neuromorpho/client_test.py`). This is the only naming pytest -discovers reliably in this repo. See {doc}`testing`. - -## See also - -- {doc}`../concepts/architecture` — the conceptual layers this maps onto. +# 代码与设计导航 + +先按要修改的功能找到源码,再阅读该模块的 TODO 和 Current。 +目录职责及规划中的调整见 [仓库组织指南](../repository.md),跨模块数据流见 +[系统总览](https://github.com/chaobrain/braincell/blob/main/docs/design/architecture/current/system-overview.md)。 + +## 按任务定位 + +源码路径相对仓库根目录。每个模块的 TODO 提供当前事项及 API、架构、proposal 的入口。 + +| 要修改什么 | 源码入口 | 设计入口 | +| --- | --- | --- | +| 形态构造、连接、统计 | `braincell/morph/` | [Morph](https://github.com/chaobrain/braincell/blob/main/docs/design/morph/TODO.md) | +| SWC/ASC、checkpoint、在线形态读取 | `braincell/io/` | [IO](https://github.com/chaobrain/braincell/blob/main/docs/design/io/TODO.md) | +| 区域、位点与连续采样 | `braincell/filter/` | [Filter](https://github.com/chaobrain/braincell/blob/main/docs/design/filter/TODO.md) | +| 机制声明与注册 | `braincell/mech/` | [Mech](https://github.com/chaobrain/braincell/blob/main/docs/design/mech/TODO.md) | +| 通道与离子动力学 | `braincell/channel/`、`braincell/ion/` | [Channel](https://github.com/chaobrain/braincell/blob/main/docs/design/channel/TODO.md)、[Ion](https://github.com/chaobrain/braincell/blob/main/docs/design/ion/TODO.md) | +| Cell、离散与运行时绑定 | `braincell/_multi_compartment/`、`braincell/_discretization/`、`braincell/_compute/` | [Cell](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/TODO.md) | +| 单室模型 | `braincell/_single_compartment/` | [Single/Cell 统一讨论](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/proposals/single-multi-compartment-unification.md) | +| 突触动力学 | `braincell/synapse/` | [Synapse](https://github.com/chaobrain/braincell/blob/main/docs/design/synapse/TODO.md) | +| 网络、事件源与连接 | `braincell/network/` | [Network](https://github.com/chaobrain/braincell/blob/main/docs/design/network/TODO.md) | +| 数值积分与电压求解 | `braincell/quad/` | [Quad](https://github.com/chaobrain/braincell/blob/main/docs/design/quad/TODO.md) | +| 参数选择与学习映射 | `braincell/trainable/` | [Optim](https://github.com/chaobrain/braincell/blob/main/docs/design/optim/TODO.md) | +| 图形展示 | `braincell/vis/` | [Vis](https://github.com/chaobrain/braincell/blob/main/docs/design/vis/TODO.md) | + +内部源码路径与公共导入名不同。例如 Branch、Morphology 从 `braincell` 顶层导入, +机制声明从 `braincell.mech` 导入;实际调用以对应模块 Current 的公开入口为准。 + +## 阅读与更新顺序 + +1. 从模块 TODO 找到事项;Current 描述当前行为,proposals 保存尚需讨论或实施的方案。 +2. 对照源码和相邻 `*_test.py`,确认现有约束和实际使用方式。 +3. 查看对应示例或数值对照,确定修改后要验证的结果。 +4. 实现后更新受影响的 Current、示例和事项状态;必要的历史决定按日期保存在 specs。 + +新增机制的步骤见 [扩展指南](extending.md),测试定位与 fixture 用法见 [测试指南](testing.md)。 +Design 的文档分工和状态定义由 +[Design 规范](https://github.com/chaobrain/braincell/blob/main/docs/design/AGENTS.md) 维护。 diff --git a/docs/developer/testing.ipynb b/docs/developer/testing.ipynb deleted file mode 100644 index 1786233e..00000000 --- a/docs/developer/testing.ipynb +++ /dev/null @@ -1,101 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "4543253a", - "metadata": {}, - "source": [ - "# Testing\n", - "\n", - "`braincell` uses **pytest** with `unittest.TestCase` classes. The test suite is\n", - "the safety net for a numerically delicate library, so the conventions below are\n", - "project rules, not suggestions.\n", - "\n", - "## Running the tests\n", - "\n", - "```bash\n", - "pytest braincell/\n", - "```\n", - "\n", - "`pytest.ini` sets `testpaths = braincell` and excludes `legacy` and `develop`.\n", - "Two environment defaults are applied automatically by the root `conftest.py`:\n", - "\n", - "- `JAX_PLATFORMS=cpu` — JAX is forced onto CPU so tests are deterministic and\n", - " don't depend on a GPU.\n", - "- `MPLBACKEND=Agg` — matplotlib runs headless so visualization tests don't open\n", - " windows.\n", - "\n", - "## Test file naming — mandatory\n", - "\n", - "Every test module **must**:\n", - "\n", - "- be named `*_test.py` (not `test_*.py`, not bare `test.py`), and\n", - "- sit **next to the source file it covers**.\n", - "\n", - "```text\n", - "braincell/io/neuromorpho/client.py\n", - "braincell/io/neuromorpho/client_test.py ← its test, co-located\n", - "```\n", - "\n", - "When a module is split across several source files, give each its own sibling\n", - "`*_test.py`. The `tests/` directory at the repo root is an empty placeholder —\n", - "do not put tests there.\n", - "\n", - "## Shared test helpers\n", - "\n", - "Helpers that are **not themselves tests** go in a private, leading-underscore\n", - "file (e.g. `_testing.py`) inside the same package, so pytest does not collect\n", - "them. For example, `braincell/io/neuromorpho/_testing.py` provides fake\n", - "HTTP doubles used across that package's tests.\n", - "\n", - "## The bug-fixing workflow\n", - "\n", - "The project rule for bugs is:\n", - "\n", - "> **Write a test that reproduces the bug, then fix until the test passes.**\n", - "\n", - "This guarantees the bug stays fixed and documents the expected behavior.\n", - "\n", - "## Test fixtures\n", - "\n", - "Morphology fixtures (SWC + ASC) live in\n", - "`examples/multi_compartment/morpho_files/`. IO tests locate them relative to the\n", - "test file:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "002dbe8b", - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "MORPHO_DIR = Path(__file__).resolve().parents[2] / \"examples\" / \"multi_compartment\" / \"morpho_files\"" - ] - }, - { - "cell_type": "markdown", - "id": "63935141", - "metadata": {}, - "source": [ - "## See also\n", - "\n", - "- {doc}`contributing` — the overall development workflow.\n", - "- {doc}`extending` — testing a new channel or integrator." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/developer/testing.md b/docs/developer/testing.md new file mode 100644 index 00000000..8008fc3b --- /dev/null +++ b/docs/developer/testing.md @@ -0,0 +1,67 @@ +--- +myst: + heading_anchors: 2 +--- + +# 测试指南 + +先运行修改模块的测试,再按影响范围检查集成行为。BrainCell 使用 pytest, +测试文件与所测源码相邻;测试收集配置在 +[pyproject.toml](https://github.com/chaobrain/braincell/blob/main/pyproject.toml)。 +以下命令在完成 [开发安装](contributing.md#配置开发环境) 后,从仓库根目录运行。 + +## 运行相关测试 + +以 SWC reader 为例,分别运行一个模块、查看其收集结果,或运行整个 IO 包: + +```bash +python -m pytest braincell/io/swc/reader_test.py -q +python -m pytest braincell/io/swc/reader_test.py --collect-only -q +python -m pytest braincell/io/ -q +``` + +涉及多个模块的共享行为时,扩大到相应包;完整核心测试命令为: + +```bash +python -m pytest braincell/ -q +``` + +测试会使用根 conftest 配置的 CPU JAX 和无窗口 Matplotlib 环境。 +GPU 性能或大型数值对照需要按对应工作流另外运行,不能由这些单元测试推断结果。 + +## 编写复现与回归测试 + +错误修复先增加一个会失败的最小复现,修复后保留它作为回归测试。 +新功能选择有可观察结果的场景:返回形状、单位、状态变化、事件时序或数值误差。 +共享行为变更还需验证使用它的 Cell 或 Network 路径。 + +新测试使用与源码对应的 `*_test.py` 文件。拆分规则、包级导出检查、共享辅助代码和依赖跳过方式见 +[仓库测试规则](https://github.com/chaobrain/braincell/blob/main/AGENTS.md#testing)。 + +## 复用形态 fixture + +仓库形态数据位于 `data/morphology/`,通过共享常量获取路径: + +```python +import braincell as bc +from braincell.io._testing import FIXTURE_DIR + +morpho, report = bc.io.SwcReader().read( + FIXTURE_DIR / "three_points_soma.swc", return_report=True, +) +assert morpho.n_branches > 0 +assert not report.has_errors +``` + +同一辅助模块还提供 `VALID_SWC_FIXTURES`、`ALLOWED_TYPES`。HTTP 等外部服务使用已有测试替身, +例如 `braincell/io/neuromorpho/_testing.py`;这样普通回归测试可以离线运行。 + +## 数值结果如何验收 + +先声明参照和容差,再比较结果。固定模型参数、单位、温度、初态、solver 与 dt; +用解析解或可信参照检查动力学,改变 dt 检查收敛,区分实现错误与离散误差。 +需要重复推进模型时使用仓库约定的 brainstate 编译循环。 +模型扩展的具体检查见 [扩展指南](extending.md)。 + +PR 中列出实际执行的命令、通过或跳过情况,以及结果依据。 +文档中的可运行示例单独执行;Sphinx 网站构建当前关闭 notebook 执行。 diff --git a/docs/developer/troubleshooting.ipynb b/docs/developer/troubleshooting.ipynb deleted file mode 100644 index 6b280e98..00000000 --- a/docs/developer/troubleshooting.ipynb +++ /dev/null @@ -1,197 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "814336fa", - "metadata": {}, - "source": [ - "# Troubleshooting & FAQ\n", - "\n", - "Common errors, what they mean, and how to fix them.\n", - "\n", - "## \"TypeError: expected a quantity\" (or a bare-number rejection)\n", - "\n", - "**Cause.** You passed a plain `float`/`int` where `braincell` expects a united\n", - "quantity. Units are mandatory everywhere (see {doc}`../concepts/units`).\n", - "\n", - "**Fix.** Attach a unit:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "954528a6", - "metadata": {}, - "outputs": [], - "source": [ - "# wrong\n", - "g_max = 0.03\n", - "# right\n", - "import brainunit as u\n", - "g_max = 0.03 * (u.mS / u.cm**2)" - ] - }, - { - "cell_type": "markdown", - "id": "6153980f", - "metadata": {}, - "source": [ - "## \"An NVIDIA GPU may be present … falling back to cpu\"\n", - "\n", - "**Cause.** JAX found a GPU but no CUDA-enabled `jaxlib` is installed, so it runs\n", - "on CPU. This is a *warning*, not an error — everything still works.\n", - "\n", - "**Fix (to use the GPU).** Install the matching CUDA build:\n", - "\n", - "```bash\n", - "pip install -U braincell[cuda12] # or braincell[cuda13]\n", - "```\n", - "\n", - "If you *intend* to run on CPU, you can ignore the message.\n", - "\n", - "## \"Cell.run(...) requires at least one placed probe\"\n", - "\n", - "**Cause.** A multi-compartment {class}`~braincell.Cell` simulation returns probe\n", - "traces, so it needs at least one probe to know what to record.\n", - "\n", - "**Fix.** `place` a probe before running:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e3e634bc", - "metadata": {}, - "outputs": [], - "source": [ - "import braincell.mech as mech\n", - "from braincell.filter import RootLocation\n", - "\n", - "cell.place(RootLocation(0.5), mech.StateProbe(\"V\"))" - ] - }, - { - "cell_type": "markdown", - "id": "ae1e4823", - "metadata": {}, - "source": [ - "See {doc}`../concepts/mechanisms`.\n", - "\n", - "## \"dt\"/\"duration\" rejected as zero, negative, or unitless\n", - "\n", - "**Cause.** `Cell.run(dt=..., duration=...)` validates its time arguments: they\n", - "must be positive `brainunit` time quantities.\n", - "\n", - "**Fix.**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b70fefa5", - "metadata": {}, - "outputs": [], - "source": [ - "result = cell.run(dt=0.1 * u.ms, duration=100 * u.ms)" - ] - }, - { - "cell_type": "markdown", - "id": "27291a94", - "metadata": {}, - "source": [ - "## `ImportError` / `ModuleNotFoundError` for matplotlib, pyvista, plotly\n", - "\n", - "**Cause.** Visualization backends are optional dependencies, imported lazily.\n", - "\n", - "**Fix.** Install the visualization extra:\n", - "\n", - "```bash\n", - "pip install -U braincell[vis]\n", - "```\n", - "\n", - "Likewise, the NeuroMorpho client needs `braincell[io]`.\n", - "\n", - "## A solver name isn't recognized\n", - "\n", - "**Cause.** The `solver=` string doesn't match a registered integrator.\n", - "\n", - "**Fix.** List the available names and pick one:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d4002fc1", - "metadata": {}, - "outputs": [], - "source": [ - "import braincell.quad as quad\n", - "sorted(quad.all_integrators)" - ] - }, - { - "cell_type": "markdown", - "id": "9c2fda8d", - "metadata": {}, - "source": [ - "See {doc}`../concepts/integration`.\n", - "\n", - "## My simulation is unstable / blows up\n", - "\n", - "**Cause.** Biophysical models are **stiff**; an explicit solver (e.g. `euler`,\n", - "`rk4`) needs a very small `dt` to stay stable.\n", - "\n", - "**Fix.** Reduce `dt`, or switch to a solver designed for stiff systems —\n", - "`exp_euler` / `ind_exp_euler` for single-compartment channel kinetics, or the\n", - "staggered / Crank–Nicolson solvers for cable equations. See\n", - "{doc}`../integration/index`.\n", - "\n", - "## An SWC/ASC file fails to load\n", - "\n", - "**Cause.** Reconstructions often have irregularities (unknown structure ids,\n", - "non-soma roots, disconnected points).\n", - "\n", - "**Fix.** Read with a report and/or relaxed options to see and handle the issue:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eb51f0ae", - "metadata": {}, - "outputs": [], - "source": [ - "morpho, report = braincell.Morphology.from_swc(\"neuron.swc\", return_report=True)\n", - "print(report)" - ] - }, - { - "cell_type": "markdown", - "id": "0f641cfd", - "metadata": {}, - "source": [ - "See {doc}`../file_formats/swc`.\n", - "\n", - "## Still stuck?\n", - "\n", - "- Re-read the relevant {doc}`concept page <../concepts/architecture>`.\n", - "- Check the {doc}`API reference <../apis/braincell>` for exact signatures.\n", - "- Search or open an issue at\n", - " [github.com/chaobrain/braincell/issues](https://github.com/chaobrain/braincell/issues)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/developer/troubleshooting.md b/docs/developer/troubleshooting.md new file mode 100644 index 00000000..2b285d54 --- /dev/null +++ b/docs/developer/troubleshooting.md @@ -0,0 +1,73 @@ +# 开发排错 + +先确认当前 Python 环境、源码位置和失败阶段,再缩小到可复现的命令。 +提交问题时附上命令、版本、完整异常和最小输入。 + +## 修改源码后结果没有变化 + +确认解释器实际导入的是当前 checkout: + +```bash +python -m pip show braincell +python -c "import sys, braincell; print(sys.executable); print(braincell.__file__)" +``` + +如果路径指向另一个环境或安装副本,回到目标环境,在仓库根目录执行 +`python -m pip install -e ".[dev]"`,然后重新启动已有 Python 或 notebook kernel。 + +## 缺少测试、绘图或文档依赖 + +贡献开发使用 `.[dev]`。按任务单独安装时,测试使用 `.[testing]`,文档使用 `.[doc]`, +绘图使用 `.[vis]`,NeuroMorpho 客户端使用 `.[io]`。 +这些 extras 以 [pyproject.toml](https://github.com/chaobrain/braincell/blob/main/pyproject.toml) 为准。 + +如果看到 GPU 回退 CPU 的提示,先检查当前设备: + +```bash +python -c "import jax; print(jax.__version__); print(jax.devices())" +``` + +根 conftest 为普通测试设置 CPU 环境;GPU 安装和设备选择见 +[安装指南](../getting_started/installation.ipynb)。 + +## 测试未收集或找不到输入文件 + +从仓库根目录运行 `python -m pytest`,先用 `--collect-only` 检查目标测试文件。 +新测试按相邻源码命名为 `*_test.py`,配置在 `pyproject.toml`。 +形态 fixture 从 `braincell.io._testing` 导入,具体例子见 [测试指南](testing.md#复用形态-fixture)。 +其他工作流的数据和可选依赖按其本地说明准备。 + +## 文档构建失败或链接缺页 + +先确认安装了文档依赖,再从仓库根目录构建: + +```bash +python -m sphinx -b html docs docs/_build/html +``` + +按日志中的源文件和行号检查新增警告。Markdown、RST 和 notebook 的内部引用使用相同的文档页面名称; +转换文件格式时保留页面名称,并移除旧格式文件,避免同名页面冲突。 +Design 当前不参与网站构建,Developer 到 Design 的链接指向 GitHub 文件。 +网站构建通过不代表 notebook 代码已执行。 + +重新生成包含 PyVista 交互图的 notebook 时,HTML 导出还需要以下依赖: + +```bash +python -m pip install ipywidgets trame trame-vtk trame-vuetify "jupyterlab>=3" +``` + +使用 `vis3d(notebook=True, jupyter_backend="html")` 并保存执行结果, +让发布页面包含导出的交互 HTML;调用参数见 +[Vis API](https://github.com/chaobrain/braincell/blob/main/docs/design/vis/current/api.md)。 + +## 建模和数值问题的入口 + +| 现象 | 查阅位置 | +| --- | --- | +| 物理单位或参数形状不匹配 | [单位用法](../concepts/units.ipynb)、对应模块 Current 的参数说明 | +| dt/duration、记录结果或连续运行异常 | [Cell 运行接口](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/current/api.md#运行与结果)、[Recording](https://github.com/chaobrain/braincell/blob/main/docs/design/network/current/recording.md) | +| solver 名称或适用模型不匹配 | [Quad API](https://github.com/chaobrain/braincell/blob/main/docs/design/quad/current/api.md) | +| 数值发散或与参照不符 | [测试验收方法](testing.md#数值结果如何验收)、[Cell 求解路径](https://github.com/chaobrain/braincell/blob/main/docs/design/cell/current/architecture.md#两条电压路径) | +| SWC/ASC 读取失败 | [Reader 与诊断报告](https://github.com/chaobrain/braincell/blob/main/docs/design/io/current/api.md) | + +仍不能定位时,在 [Issues](https://github.com/chaobrain/braincell/issues) 提供可运行复现及环境信息。 diff --git a/docs/index.rst b/docs/index.rst index fc000da4..20958eff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -193,11 +193,8 @@ The docs are layered by what you are trying to do: :maxdepth: 1 :caption: Developer Guide - developer/contributing - developer/project_layout - developer/testing - developer/extending - developer/troubleshooting + repository + developer/index .. toctree:: :hidden: diff --git a/docs/repository.md b/docs/repository.md new file mode 100644 index 00000000..eec1d395 --- /dev/null +++ b/docs/repository.md @@ -0,0 +1,183 @@ +# 仓库组织指南 + +状态:目标布局,目录迁移待确认。 + +仓库按内容的用途组织:核心实现、使用示例、基准评测、研究实验、数值验证、公共资产和文档各有归属。 +本页说明目录职责、内容如何分类,以及研究成果如何进入长期维护的位置。 + +## 目录职责 + +| 目录 | 用途 | 典型内容 | +| --- | --- | --- | +| `braincell/` | 核心实现及行为回归测试 | 公共接口、内部计算模块、与源码相邻的 `*_test.py` | +| `examples/` | 展示用户如何完成任务 | 构造 Cell、组网、记录、参数学习、绘图 | +| `benchmarks/` | 在可重复的条件下评价性能或任务表现 | 编译时间、吞吐量、内存、规模扫描、序列学习基准 | +| `experiments/` | 探索 proposal 中的方法与候选实现 | 新模型、学习算法原型、消融实验、方案验证 | +| `validation/` | 验证数值结果和模型行为 | 与 NEURON 对照电压、机制状态、事件时序及误差 | +| `data/` | 保存可共享的输入和参考资产 | 形态、参考机制 `.mod` 源文件、参考轨迹 | +| `docs/` | 解释用法、设计和结论 | 使用文档、模块接口与架构、提案、研究依据、历史记录 | + +目标目录结构如下,子目录按实际内容创建: + +```text +braincell/ +docs/ + repository.md + design/ + specs/ +examples/ + cell/ + network/ + optim/ + vis/ + ... +benchmarks/ + performance/ + profiling/ + tasks/ + sequence_learning/ +experiments/ + / +validation/ + neuron/ + morph/ + channel/ + ion/ + synapse/ + cable/ + cell/ +data/ + morphology/ + mechanisms/ + reference_traces/ +``` + +核心代码的内部模块关系见 {download}`系统总览 `, +开发与测试约定见根目录 {download}`AGENTS.md <../AGENTS.md>`。 + +## 示例按模块组织 + +`examples/` 与 `docs/design/` 使用相同的模块主题名称,例如 `cell`、`network`、`optim`、`vis`。 +对应的是主题;示例目录直接保存可运行脚本、notebook 及必要的辅助文件。 + +一个示例按主要教学目的归属。网络构建放 `network`,学习通道或突触参数放 `optim`, +形态绘图放 `vis`。单室和多室模型的基本用法都归 `cell`,文件较多时再按模型类型细分。 +示例可以使用多个模块,并链接到各模块的接口说明。 + +脚本与讲解它的 notebook 放在一起。多个工作流使用同一个模型时,复用已有的模型构造函数; +共享需求稳定后,再提取共同实现,避免各处复制模型和参数。 + +## 基准、实验与验证如何区分 + +分类依据是代码要回答的问题。同样是训练序列模型,可以有三种用途: + +| 问题 | 归属 | 应保留的内容 | +| --- | --- | --- | +| 如何构建并训练一个网络? | `examples/network/` 或 `examples/optim/` | 最小完整流程、参数设置、结果读取 | +| 不同模型或算法在相同任务上表现如何? | `benchmarks/tasks/sequence_learning/` | 固定任务协议、模型配置、评价指标和结果汇总 | +| 新的学习规则或模型结构是否有效? | `experiments//` | 候选实现、实验配置、分析过程及关联 proposal | + +### Benchmarks + +| 分类 | 评价内容 | +| --- | --- | +| `performance/` | 构建与编译时间、稳定运行耗时、吞吐量、内存占用,以及 neuron/CV/synapse/时间步规模变化 | +| `profiling/` | 定位计算热点、设备利用率、内存与数据传输开销 | +| `tasks/` | 序列学习等固定任务上的学习效果和资源成本 | + +任务目录按任务命名,具体循环网络模型和训练算法作为配置。同一任务可以比较不同模型, +也可以比较同一个模型的 BPTT、RTRL 等训练方式。 + +任务基准应明确数据划分或生成规则、评价指标、训练预算、随机种子、模型与算法配置。 +结果同时记录软件版本和硬件环境。按任务需要报告最终准确率或损失、训练时间、峰值内存, +以及达到目标表现所需的训练步数。 + +性能基准分别记录编译与稳定运行时间,并明确设备同步和数据传输是否计入耗时。 +规模扫描保留各规模的结果、资源需求和失败原因。性能诊断中的额外同步或 profiler 配置会改变执行开销, +因此诊断结果应注明采集条件。 + +### Experiments + +`experiments/` 按 proposal 或研究主题组织,允许自由调整实现和实验结构。 +一个主题可以同时包含原型代码、正确性检查、规模实验和任务测试,保持研究过程完整。 + +本地 README 给出研究问题、运行入口和对应 proposal 的链接。接口还在变化的原型留在实验中; +获得稳定接口、明确用途和验证依据后,再将适合长期维护的部分迁出。 + +### Validation + +`validation/neuron/` 组织 BrainCell 与 NEURON 的数值对照,按形态、通道、离子、突触、 +cable 和整细胞等对象分类。每个工作流明确共同输入、刺激、温度、初态、离散与求解配置, +并记录比较量、容差和差异来源。 + +NEURON 对比若主要评价电压或机制状态的一致性,归 validation;若主要评价速度和内存, +归 benchmarks。性能评测仍需检查数值正确性,避免把不同计算结果当作速度提升。 + +发现具体实现错误后,将适合日常运行的小型复现用例补到核心代码旁。 +大型对照与规模扫描保留独立运行入口,其调度和依赖要求随工作流记录。 + +## 文档分工 + +| 位置 | 维护内容 | +| --- | --- | +| 本页 | 仓库目录职责、内容归属、公共资产与成果流转 | +| `docs/` 中的使用文档和 API 参考 | 安装、建模流程、接口查询及用户排错 | +| 根目录 `CONTRIBUTING.md` 与 `docs/developer/` | 贡献入口、开发环境、代码与设计导航、测试和 PR 流程;模块契约链接 Design | +| `docs/design//current/` | 已实现的接口契约、架构、专题说明和可复现实测结论 | +| `docs/design//proposals/` | 具体问题、候选方案、取舍和待决定事项 | +| `docs/design//references/` | 外部方法、文献、推导和研究依据 | +| `docs/design//TODO.md` | 模块事项、状态、下一步与详情入口 | +| `docs/design/TODO.md` | 宏观目标、模块里程碑、主要依赖和阻塞 | +| `docs/specs/` | 按日期保存的历史决策与变更记录 | +| 工作流旁的 README | 本地文件导航、运行命令、输入输出和环境要求 | + +完整的接口、架构、提案与结果记录规格由 {download}`Design 文档规范 ` 维护; +开发协作规则由根目录 AGENTS.md 维护。 + +设计论证和长期引用的实验分析保留一份权威说明,工作流 README 链接到它,说明中反向链接复现入口。 +根目录 `examples/` 保存实际示例;文档网站中的 `docs/examples/` 负责展示与导航,优先引用已有示例。 + +## 公共资产与运行产物 + +| 内容 | 存放方式 | +| --- | --- | +| 多个工作流复用的形态和参考输入 | `data/` 下按资产类型、来源或模型家族组织 | +| NEURON `.mod` 等参考机制源文件 | `data/mechanisms/`,保留来源、版本、使用许可和本地修改说明 | +| 工作流专用的小型输入与配置 | 随对应工作流维护,便于独立复现 | +| 编译动态库、缓存、训练 checkpoint、原始 trace 和临时图表 | 工作流的 `artifacts/`,默认忽略提交 | +| 长期使用的参考轨迹或基准摘要 | 选择必要的小型资产纳入版本控制,记录生成配置与来源 | +| 大型数据和结果 | 保存获取位置、版本或校验信息及复现命令,按需下载或生成 | + +`.mod` 是可编译的参考机制源文件。原始模型与移除 TABLE、修改积分方法等本地版本应能明确区分, +比较结果注明使用的版本。编译脚本、加载器和模型运行脚本随工作流维护,编译输出进入 artifacts。 + +公共输入保持一个维护位置,各工作流引用它。读取路径的公共辅助函数在存在共享需求时集中维护, +避免多个脚本分别假设目录深度。 + +## 从实验到长期维护 + +| 成果 | 后续归属 | +| --- | --- | +| 接口稳定、适合复用的模型或算法 | `braincell/`,配套模块 API、架构说明和相邻测试 | +| 固定协议、可长期重复比较的评测 | `benchmarks/` | +| 成熟的数值对照或模型验证流程 | `validation/` | +| 能清楚展示已实现接口的用法 | `examples/` | +| 设计决定、研究结论及其证据 | 对应模块的设计文档,必要历史进入 specs | + +一个研究主题可以产出多类成果。迁出公共实现后,实验代码引用新的实现,保留复现实验所需的配置和入口。 + +## 现有内容的整理方向 + +| 当前内容 | 目标归属 | +| --- | --- | +| `examples/single_compartment/` 与 `examples/multi_compartment/` | 按教学主题分入 `examples/cell/`、`network/`、`optim/`、`vis/` 等 | +| `examples/profiling/` | 诊断工具进入 `benchmarks/profiling/`,性能与规模评测进入 `benchmarks/performance/` | +| `examples/profiling/simulator_compare/` | `benchmarks/performance/simulator_compare/` | +| `examples/experimental/` | `experiments/`,按研究主题保持原型与实验的关联 | +| `examples/neuron_compare/` | 对照工作流进入 `validation/neuron/`,公共参考资产进入 `data/` | +| `examples/neuron_compare/Cerebellum_mod/` | 按来源和模型家族整理到 `data/mechanisms/`、`data/morphology/`,编译产物移入 artifacts | +| `docs/developer/` | 保留为贡献者指南,维护贡献步骤和阅读导航;完整接口、公式与架构由 Design 维护 | + +后续目录迁移需要同步处理 Python 导入、notebook 路径、数据定位、运行命令、CI、 +打包排除规则、产物忽略规则和文档引用。迁移按工作流验证运行入口及测试收集,核对共享模型与数据的引用关系。 +贡献者指南已按上述分工更新;仓库入口为根目录 CONTRIBUTING.md,网站入口为 [Developer](developer/index.rst)。 diff --git a/docs/design/optim/implementation-plan.md b/docs/specs/2026-08-29-trainable-parameter-implementation.md similarity index 86% rename from docs/design/optim/implementation-plan.md rename to docs/specs/2026-08-29-trainable-parameter-implementation.md index 1cf2bb07..e79430c3 100644 --- a/docs/design/optim/implementation-plan.md +++ b/docs/specs/2026-08-29-trainable-parameter-implementation.md @@ -1,12 +1,23 @@ -# Trainable Parameter Implementation Plan +# Trainable Parameter Implementation + +> 历史归档:原 docs/design/optim/implementation-plan.md,首次入库日期 2026-08-29。 +> 2026-09-07 迁移,保留旧 P0 计划正文;其中未实现、白名单和 owner 排期均为当时表述。 +> 当前 API/状态以 [优化总览](../design/optim/design-overview.md) 和 [路线图](../design/optim/roadmap.md) 为准。 ## 文档状态 -本文定义 [Trainable Parameter API](api.md) 的实现顺序和验收边界。它不是代码状态报告; +本文定义 [Trainable Parameter API](../design/optim/api.md) 的实现顺序和验收边界。它不是代码状态报告; 在相应代码和测试合入前,所有阶段均视为未实现。 -更宽的模型优化能力地图见 [Design overview](design-overview.md),内部模型见 -[Architecture](architecture.md)。 +本文保留首版 P0 的历史分阶段计划。其中三个 Channel 和手写参数白名单的范围已被 +[Channel Learning](2026-09-07-channel-learning.md) 和 +[Ion Learning](2026-09-07-ion-learning.md) 和 +[Synapse and Network Learning](2026-09-07-synapse-network-learning.md) +扩展取代;当前契约以 +[API](../design/optim/api.md) 和 [Architecture](../design/optim/architecture.md) 为准。 + +更宽的模型优化能力地图见 [Design overview](../design/optim/design-overview.md),内部模型见 +[Architecture](../design/optim/architecture.md)。 ## 实现边界 @@ -149,7 +160,7 @@ delay、routing、source index、synapse ID 和 active mask 不进入连续 trai ## 后续独立设计 -以下能力只有完成独立 API 讨论后才加入 [API](api.md): +以下能力只有完成独立 API 讨论后才加入 [API](../design/optim/api.md): - unit-aware batch/data adapter; - loss component 协议; @@ -159,7 +170,7 @@ delay、routing、source index、synapse ID 和 active mask 不进入连续 trai - checkpoint/resume 和 fitted-model export; - backend-aware timing/memory instrumentation。 -[References](design-overview.md#references) 提供需求证据,但其中的实验结构和候选接口 +[References](../design/optim/design-overview.md#理论与方法) 提供需求证据,但其中的实验结构和候选接口 不自动成为公共类型。 ## 错误与事务测试 diff --git a/docs/specs/2026-09-04-cell-reduction-runtime.md b/docs/specs/2026-09-04-cell-reduction-runtime.md new file mode 100644 index 00000000..f13d1233 --- /dev/null +++ b/docs/specs/2026-09-04-cell-reduction-runtime.md @@ -0,0 +1,123 @@ +# Cell Reduction Runtime + +## Goal + +A detailed `Cell` may register several interchangeable reduction models and +select one execution model before initialization. The detailed model remains +the owner of morphology, logical synapses, connections, and recordings, while +an active reduction model replaces all detailed dynamics and publishes the +Cell's raw and event outputs. + +For a practical model-author checklist and implementation skeleton, see +[`model-integration-guide.md`](../design/reduction/model-integration-guide.md). + +## Public Contract + +- A new Cell starts with the reserved model name `"detailed"`. +- `cell.add_reduction(name, model)` registers a named model. +- `cell.use_model(name="detailed")` selects one model for the whole root Cell. + A `CellView` cannot select a different model. +- `cell.reductions` and `cell_view.reductions` expose registered models through + query views. A view reports whether its model is selected and delegates + optional population-parameter `get()` and `set()` calls to that model. +- `cell.outputs`, `cell_view.outputs`, and `population.outputs` expose live raw + outputs. Detailed execution publishes the complete CV voltage as + `outputs["voltage"]`; reduced execution publishes the active model's named + values. +- `cell.event_outputs["spike"]` remains the canonical event port in both modes. +- `observe.output(name)` records a named raw output after the current step's + update, aligned with the event generated by that update. + +The minimum reduction-model protocol is: + +```python +model.init_state(context, batch_size=None) -> ReductionOutput +model.update(inputs) -> ReductionOutput +model.reset_state(batch_size=None) -> ReductionOutput +model.reset() -> None +``` + +`reset()` has a no-op default for stateless models. Model parameters, learned +weights, caches, validation, and internal state representation belong to the +model. Optional population parameter editing is expressed as +`model.get(field, population_indices)` and +`model.set(population_indices, **parameters)`. + +`ReductionOutput.values` is a fixed mapping of raw output names to arrays or +quantities. Values have an optional batch axis, the Cell population axes, and +arbitrary trailing feature axes. `ReductionOutput.event` has only the +optional batch and population axes. Names, shapes, dtypes, and units remain +stable until the Cell is deinitialized. + +## Input And Execution + +Each reduction initialization receives a fresh `ReductionContext` built from +the Cell's current logical synapses. It contains stable synapse metadata and +packed input-group schemas. The framework does not require synapses or a +homogeneous per-cell layout; a DBNN, DLIF, or other concrete model may reject +an incompatible context itself. + +At runtime, payloads remain grouped by synapse runtime type and event-input +contract. Every packed group carries stable logical synapse ids, +member-local synapse indices, population indices, and the payload delivered to +those rows. The payload is exactly what the detailed synapse would have +received after delay, connection weight, and same-target aggregation. Counts +before aggregation are not retained. + +Network delivery, scheduled Connections, and deprecated bound synapse inputs +all write the same input buffers. Zero-delay events retain the existing +boundary semantics and become input to the following postsynaptic update. + +Cell execution dispatches its normal phases by the selected model. Detailed +mode keeps the existing solver path. Reduced mode packages and clears input +buffers, calls the reduction model, publishes its outputs, and never allocates +or updates detailed voltage, ion, channel, synapse, clamp, solver, or probe +runtime state. + +Only the canonical Cell spike source is valid in reduced mode. An outgoing +Connection using a voltage-dependent or otherwise detailed custom event source +must fail during run setup rather than being silently dropped. + +## Lifecycle And Observation + +`reset_state()` keeps the current mode and runtime, clears event buffers and +time, calls the reduction model's `reset_state()`, and republishes its initial +output. `reset()` deinitializes the Cell and calls the reduction model's +`reset()`, but preserves morphology, paint/place rules, connections, +recordings, registered reductions, their parameter declarations, and the +selected model name. + +Synapses may be edited after deinitialization. The next initialization builds +a new context; the concrete reduction model decides whether to accept it, +rebuild derived structure, or report that retraining is required. + +Detailed recordings and legacy probes remain declared but are silently +inactive in reduced mode. They produce no placeholder or initial-value +results and become active again after switching back to detailed mode. An +`observe.output(name)` declaration is active only when the selected execution +model publishes that name. + +## Reference Models + +`EventAccumulatorReduction` counts nonzero packed synapse payload slots per +population member. It computes `candidate = alpha * state + active_count`. +When `candidate > threshold`, it emits one event, exposes the pre-reset +candidate as `outputs["value"]`, and resets the internal state to zero. +`alpha=0` is the instantaneous form. Payload magnitude and the number of +events already summed into one slot do not affect its contribution. + +`PayloadAccumulatorReduction` applies the same decay, strict threshold, and +reset rule to the sum of scalar payloads. Its state, threshold, and +`outputs["value"]` retain the payload's physical unit. It accepts compatible +`ScalarEventInput` groups, so connection weights summed into one synapse slot +remain distinguishable; trigger inputs and incompatible units are rejected. + +`SynapticKernelAccumulatorReduction` converts each supported scalar payload to +an integrated conductance-kernel area before applying the accumulator rule. +For `ExpSyn`, the unit-weight area is `tau`. For peak-normalized `Exp2Syn`, it +is `factor * (tau2 - tau1)`, using the same dimensionless peak-normalization +factor as the detailed synapse. The result and threshold therefore have +conductance-time units. Kernel coefficients are compiled once from +`ReductionContext.synapses`; the model does not construct or advance detailed +`g`, `A`, or `B` synapse states. Other synapse types are rejected until they +define an explicit analytic kernel-area rule. diff --git a/docs/specs/2026-09-07-bidirectional-population-learning.md b/docs/specs/2026-09-07-bidirectional-population-learning.md new file mode 100644 index 00000000..7a53b2b2 --- /dev/null +++ b/docs/specs/2026-09-07-bidirectional-population-learning.md @@ -0,0 +1,124 @@ +# Bidirectional Population Learning + +## Plan + +Validate the existing full-state RTRL on a Network containing two independent +one-CV HH Cell populations of sizes two and three. Both populations own trainable +channel conductance/voltage shift, ion reversal, synapse kinetics/reversal and +detector threshold. Both directions have six independently addressable contacts +and trainable weights. A uses ExpSyn and B uses Exp2Syn. + +Use asymmetric parameters and repeated, offset stimuli. Require real emissions +and arrivals in both directions and subsequent spikes after feedback. Compare +all scalar/vector root gradients for A-only, B-only, joint voltage and spike +losses. Diagnose event paths by stopping only event derivatives while preserving +the complete forward trajectory. Exercise zero, positive and heterogeneous fixed +delays; scatter and brainevent; shared roots; compiled reset and root replacement; +prefix gradients and full-state carry sizes. Delay is not trainable. + +Train two replicas with BPTT and full RTRL from identical perturbed roots against +one synthetic spiking target. For fitting, share parameters within each +population and each connection direction, but not between populations. Use +Adam 0.01, at most 200 epochs, parameters fixed within each rollout, positive +bounded factors and nonoverlapping Exp2Syn time-constant ranges. Require voltage +MSE to decrease at least tenfold, not unique parameter recovery or identical +optimizer trajectories. Treat gradient validation and optimization separately. + +Keep experiments and co-located tests under the existing gradient-correctness +directory. Extend synapse_learning.ipynb with the executed topology, gradient, +activity and fit results. Validate CPU JAX 0.8.0 and 0.10.1 in separate processes. +Existing workspace changes are preserved; no commit is requested. This is not +exhaustive coverage of arbitrary mechanisms, morphologies, or online parameter +updates inside a rollout. + +## Implementation and Validation + +The experiment uses 20 ms at dt = 0.025 ms. Population A has two members and B +has three; all five emit twice and receive nonzero synaptic conductance before +subsequent firing. A later, stronger clamp overcomes HH afterhyperpolarization; +the initial weaker repeated pulse was insufficient to exercise recurrent firing. +Activity assertions prevent a numerically passing but inactive-feedback fixture. + +Gradient comparisons include every coordinate of 15 named vector roots (45 +scalars), rather than reducing each vector to a single summary before checking: + +| Loss | Fixed delays | Delivery | +| --- | --- | --- | +| Joint voltage | Heterogeneous, including zero | scatter | +| A-only voltage | Homogeneous positive | scatter | +| B-only voltage | Heterogeneous, including zero | brainevent | +| Joint spikes | Zero | brainevent | + +The float64 acceptance bound is `atol=1e-8, rtol=1e-7`. In the executed notebook's +joint-voltage case, the largest absolute gradient difference is approximately +3.35e-10. This is floating-point agreement on the chosen surrogate graph, not +bitwise equality or a finite-difference derivative of hard event timing. + +Additional tests check: + +- Per-member model roots and six independently addressable weights per direction. +- A-only loss reaching B.gmax/B.threshold, and the converse. +- Stopping only event derivatives: identical voltages, spikes and conductances, + but exactly zero cross-population gmax/threshold gradients. +- A shared gmax root receiving the sum of independent A and B contributions. +- Prefix gradient agreement around emissions/arrivals and at the final step. +- Cross-population voltage sensitivity and nonzero delayed-queue sensitivity. +- Reset with the same compiled function, root updates, and exact restoration. +- Full-state/sensitivity carry shapes independent of rollout duration. +- BPTT and full RTRL fitting both populations and both incoming weight groups. + +The executed notebook uses 100 Adam updates of all 15 grouped roots. Both +methods reduce voltage MSE from 25.51068369 to approximately 0.10422649, a ratio +of 0.0040856. All grouped roots change. This demonstrates joint fitting, not +unique recovery of the generating parameters. Automated fitting allows 200 +updates and requires at least a tenfold MSE reduction. + +### Sparse Derivative Regression + +Both installed JAX environments exposed the same brainevent failure: +`coomv_p_call() got an unexpected keyword argument 'weight_info'` when batching +weight JVP directions. Previous backward-only sparse-delivery checks did not +exercise that transformation. A co-located delivery test first reproduced it. +The adapter now retains the requested forward kernel and defines its exact +bilinear JVP with scatter. Tests compare batched weight/event JVPs, reverse +gradients, homogeneous/heterogeneous weights, units and boolean event inputs +against the scatter implementation. No dependency files or global derivative +registrations are modified. + +### Completed Checks + +| Check | Result | +| --- | --- | +| CPU JAX 0.8.0: delivery and bidirectional tests | 11 passed | +| CPU JAX 0.10.1: same tests | 11 passed | +| CPU JAX 0.8.0: network, network parameter manager, autapse regressions | 135 passed, 1 skipped | +| Selected coverage run with Python tracer, JAX 0.10.1 | 3 passed | +| Executed synapse_learning notebook | All six code cells completed without errors | +| Targeted Ruff checks and git diff whitespace check | Passed | + +The new experiment has 139/146 executable lines covered (95.2%); the seven +uncovered lines are its command-line reporting entry point. The new bilinear +delivery adapter has no uncovered executable statements. These figures are +scoped to the new implementation, not a claim of whole-repository coverage. + +The C-tracer coverage attempt on JAX 0.8.0 terminated with a native segmentation +fault during AD tracing. Its results are not counted. Ordinary JAX 0.8.0 tests +and Python-tracer coverage on JAX 0.10.1 subsequently passed; the native crash's +cause has not been established and no unrelated runtime changes were made. + +Reproduce the main checks with the appropriate Python environment: + +```bash +python -m pytest -q braincell/network/delivery_test.py examples/experimental/optim_gradient_correctness/bidirectional_test.py +python -m pytest -q braincell/network/ braincell/trainable/_network_test.py examples/experimental/optim_gradient_correctness/autapse_test.py +python -m nbconvert --to notebook --execute --inplace --ExecutePreprocessor.timeout=1200 examples/multi_compartment/synapse_learning.ipynb +python -m coverage run --timid --source=braincell/network,examples/experimental/optim_gradient_correctness -m pytest -q braincell/network/delivery_test.py examples/experimental/optim_gradient_correctness/bidirectional_test.py -k 'batched or both_gradient_methods or shared_root' +``` + +### Scope + +The four combinations above cover the intended structural boundaries, not the +Cartesian product of every loss/backend/delay option. Remaining extensions +include multi-CV cells, nonlinear/custom synapses, larger sparse topologies, +other surrogate functions, GPU execution and optimizer updates within a rollout. +Delay remains fixed and is not a trainable target. diff --git a/docs/specs/2026-09-07-channel-learning.md b/docs/specs/2026-09-07-channel-learning.md new file mode 100644 index 00000000..7ae86c48 --- /dev/null +++ b/docs/specs/2026-09-07-channel-learning.md @@ -0,0 +1,109 @@ +# Channel Learning + +## Intent + +Replace hand-maintained Channel parameter schemas with constructor-signature +discovery. Preserve default reads, pre-initialization overrides, physical units, +compact runtime parameters, region selection, grouping, shared optimizer roots, +and the existing parameter/scale/parameterized interfaces. Ion and Synapse +schemas and gate/Markov state declarations are out of scope. + +Signature membership is not a promise of differentiability. Do not introduce a +scientific trainability whitelist or reject zero gradients. Existing conversion, +shape, unit, and static-control-flow errors remain meaningful outcomes. Keep +ordinary constructor configuration on its existing path; discovering a field +does not mean allocating a numeric buffer for a missing or nonnumeric default. + +## Implementation + +1. Discover named constructor arguments and defaults, including forwarded parent + signatures, without treating arbitrary **kwargs as declared parameters. + Generate internal metadata instead of requiring Channel.parameters dictionaries. +2. Preserve default get/set and runtime materialization. Constructor conversions + and subclass overrides must not be silently undone by attaching runtime states. +3. Replace the four temperature-derived sodium phi assignments with properties. + Independent explicit phi parameters remain ordinary parameters. +4. Make cached_q10_factor tracer-safe: bypass caching traced inputs and never + retain traced outputs, including results produced from concrete inputs in JIT. +5. Add examples/multi_compartment/channel_learning.ipynb: three deterministic, + independent one-CV, one-parameter fits of a spiking voltage trace, learning + sodium g_max, V_sh, and temp. Keep injected current fixed. Explain observed + finite gradients, legitimate zero gradients, and natural errors in tables. + +## Verification + +Write regression tests before fixes. Exercise omitted defaults, explicit values, +inheritance, units, shapes, constructor configuration, regional bindings and +unselected rows, all existing grouping/source modes, reset, and shared roots. +Test all affected phi variants and cache behavior under JIT and gradients. +Include zero-gradient switches, frozen gates, zero temperature offsets, and a +learnable floating-point concentration exponent despite its integer default. + +Execute the notebook from a fresh CPU kernel with no external dataset. Each +target and fitted trace must contain a spike, all results must be finite, and +final waveform MSE must be at most 10% of initial MSE. Start with 20 ms traces +and 100 Adam updates, tuning only as needed to meet this teaching acceptance +criterion. Record actual results and wall time. Use brainstate transforms for +repeated simulation and optimization; retain JAX >= 0.8.0 compatibility. + +## Execution Results + +Implemented on the `reduction` worktree branch. The existing user changes in +`examples/multi_compartment/reduction.ipynb` were not edited. + +Additional regressions found and fixed while opening signature parameters: + +- Equal initial values had allowed independent regional bindings to collapse to + one shared runtime scalar. Compression now accounts for selection ownership. +- Integer default buffers truncated floating-point updates and erased exponent + gradients. Both training materialization and view writes now promote dtype. +- Constructor bool/int coercions must be distinguished from numeric passthrough: + constructor inputs are arrays, and actual scalar conversions remain effective. + Forced constructor settings are also reflected in parameter queries. +- Required/None-default fields supplied through pre-init overrides or explicit + training initial values now receive runtime storage. Without a numeric default, + every active row in the layout must be supplied rather than guessed. +- One existing K_Kv_test fixture supplied unitless `q=9.0` despite the signature's + voltage slope default. The fixture now uses `9.0 * u.mV`. + +Validation used Python 3.11 and JAX 0.8.0 on CPU: + +| Check | Result | +| --- | --- | +| Channel, compute, trainable, multi-compartment, base Channel tests | 1,051 passed | +| Ion, Synapse, Network compatibility tests | 392 passed, 2 skipped | +| Registered Channel numeric defaults | 112 classes, 375 defaults validated | +| Changed executable line coverage | 135 / 140 = 96.43% | +| Ruff lint, format check, git diff whitespace check | Passed | +| Fresh-kernel notebook | 8 code cells, 4 plots, no errors | + +Coverage is for changed executable lines in the implementation, measured with +coverage.py and the changed-line ranges from git diff, not whole-repository +coverage. GPU and other JAX versions were not executed locally. The existing CI +version matrix remains unchanged. Coverage tooling was installed only in a +temporary directory, not added to project dependencies. + +All three independent notebook fits used one CV, one scalar optimizer root, +one target spike, a 20 ms waveform, and 100 Adam updates. Final fitted traces +also contained one spike each. MSE is measured in mV squared. + +| Parameter | Initial | Target | Fitted | Initial MSE | Final MSE | +| --- | ---: | ---: | ---: | ---: | ---: | +| g_max (mS/cm^2) | 108 | 120 | 119.966 | 55.9342 | 0.0000709192 | +| V_sh (mV) | -44 | -45 | -45.0055 | 258.324 | 0.00785799 | +| temp (K) | 308.15 | 309.15 | 309.145 | 24.2734 | 0.000379125 | + +The final notebook execution took 17.48 seconds for the three fits combined, +including their compilation; this is an observed run, not a performance promise. +The fixed-voltage gateCurrent probe verifies a changed forward value with zero +switch gradients. Its existing gating-current formula produces a very large +enabled current; that amplitude was not calibrated or changed here and was not +used to generate any training target. + +Reproduce functional validation with: + +```bash +pytest -q --disable-warnings braincell/channel braincell/_compute braincell/trainable braincell/_multi_compartment braincell/_base_channel_test.py +pytest -q --disable-warnings braincell/network braincell/synapse braincell/ion +jupyter nbconvert --to notebook --execute --inplace --ExecutePreprocessor.timeout=600 examples/multi_compartment/channel_learning.ipynb +``` diff --git a/docs/specs/2026-09-07-design-todo-snapshot.md b/docs/specs/2026-09-07-design-todo-snapshot.md new file mode 100644 index 00000000..e23d8de6 --- /dev/null +++ b/docs/specs/2026-09-07-design-todo-snapshot.md @@ -0,0 +1,902 @@ +# Design TODO 历史快照 + +归档日期:2026-09-07。来源:`docs/design/TODO.md` 的整理前工作区版本。 +以下保留原记录;其中的实现状态和测试数量没有在归档时重新验收。现行入口为 [Design TODO](../design/TODO.md)。 + +--- + +# BrainCell Project Design and TODO + +> Status: living project-level progress index. Tracks macro goals, module +> milestones, major dependencies, and blockers. Detailed contracts, local +> stages, and acceptance criteria belong in their module design documents. +> Status markers in this file follow: +> +> - `[x]` shipped — implemented, covered by `*_test.py`, and consistent with +> the applicable design and usage examples. +> - `[~]` partial — implementation exists but is missing functionality, +> tests, or runtime integration. Specific gaps are listed inline. +> - `[ ]` planned — design agreed, code not yet written. +> - `[ ]` research — explicitly labeled investigation; the approach or +> implementation contract is not yet settled. +> +> This document describes committed repository state. Experimental work in an +> uncommitted working tree does not become a shipped capability until its API, +> implementation, tests, and applicable examples land together. + +--- + +## Document Navigation + +- [Mission and scope](#1-mission-and-scope) +- [Top-level architecture](#2-top-level-architecture) +- [Module catalogue](#3-module-catalogue) +- [Cross-cutting concerns](#4-cross-cutting-concerns) +- [Public API contract](#6-public-api-contract) +- [End-to-end workflows](#7-end-to-end-user-workflows) + +Each module TODO tracks local questions and next actions; Current documents describe +implemented behavior, Proposals compare designs, and References preserve supporting evidence. + +| Topic | Collaboration entry | +| --- | --- | +| Cross-module architecture and interface consistency | [Architecture TODO](../design/architecture/TODO.md) | +| Cell frontend, runtime, and single ODE discussion | [Cell TODO](../design/cell/TODO.md) | +| Channels and template invariants | [Channel TODO](../design/channel/TODO.md) | +| Ions and shared mechanism bibliography | [Ion TODO](../design/ion/TODO.md) | +| Morphology readers and writers | [IO TODO](../design/io/TODO.md) | +| Morphology layering | [Morph TODO](../design/morph/TODO.md) | +| Spatial selections and callable parameters | [Filter TODO](../design/filter/TODO.md) | +| Populations and event routing | [Network TODO](../design/network/TODO.md) | +| Parameter learning and optimization experiments | [Optimization TODO](../design/optim/TODO.md) | +| Reduction model integration and DBNN | [Reduction TODO](../design/reduction/TODO.md) | +| Visualization and migration to BrainTools | [Vis TODO](../design/vis/TODO.md) | + +Model-specific imports and numerical comparisons are tracked with the actual +[Cerebellum examples](../../examples/neuron_compare/cerebellum-import-progress.md). +Update this index when a macro milestone or blocker changes. Writing and ownership rules +are maintained in [Design conventions](../design/AGENTS.md); commit checks follow the repository +[design, code, and examples agreement](../../AGENTS.md#design-code-and-examples). + +## Current Architecture Snapshot + +The committed repository currently provides: + +- [x] **Cerebellum channel/ion imports and tests expanded.** The channel + catalogue now includes PC MA2024 channel variants and the calcium-ion + catalogue includes concrete Cerebellum kinetic-ion imports such as + `CdpStC_*`, `CdpCAM_MA2024_PC`, and `CdpCR_MA2020_GrC`, with co-located + unit tests and NEURON-comparison notebooks under `examples/neuron_compare`. +- [x] **PC MA2024 assembly scaffold added.** `examples/neuron_compare/cell/pc_ma2024` + contains the simplified NEURON assembly, the matching BrainCell assembly, + shared parameter loading, debug variants, and `run.ipynb` for side-by-side + simulation. +- [x] **Direct multi-compartment runtime.** `Cell` owns declaration and runtime + state, is initialized with `init_state()`, and advances directly with + `run()`. The former `Cell -> RunnableCell` build boundary no longer exists. +- [x] **Population network runtime.** `braincell.network` owns population + registration, event routing, initialization and result aggregation while + synapses, connections and recordings remain owned by their target `Cell`. +- [x] **Trainable parameter mappings.** `braincell.trainable` maps selected + channel fields from direct, shared-scale or latent parameter sources into + runtime values. Optimizers, losses and training loops remain user-owned. +- [x] **SWC writing and structural round trips.** `Morphology.to_swc()` writes + branch trees through `braincell.io.swc`; focused tests cover shared branch + endpoints, soma attachments, reversed branches and validation failures. +- [x] **NEURON-style ion-current snapshot mode added.** `Cell(..., + cache_ion_total_current=True)` caches the total ion current at the start of + the staggered step, before voltage or ion state advances, so current-driven + ion mechanisms can read the same precomputed current snapshot that + NEURON-style scheduling expects. +- [x] **Frozen voltage channel variants added where needed.** Some PC calcium + channels now have `_Frozen` variants which stop differentiation through the + voltage used inside the current expression, matching the intended NEURON + semantics for those mechanisms during the comparison. +- [x] **Two ion/channel update schedules are available.** + `ion_channel_update_order="family"` restores the NEURON-like family + ordering for ion/channel updates; `"integration"` keeps the previous + BrainCell integration-oriented ordering. +- [x] **Homogeneous multi-compartment `Cell` populations now support + multi-dimensional `pop_size`.** `Cell(..., pop_size=(...))` expands + runtime state to `pop_size + (n_cv,)`, point-space runtime arrays to + `pop_size + (n_point,)`, and supports population-specific + `CurrentClamp(...)` amplitudes such as `(2,)` or `(2, 2)`-shaped + current grids. Regression coverage includes `(2,)` and `(2, 2)` + populations. +- [x] **The population axis is mandatory.** `pop_size` defaults to `1` + and an explicitly empty `pop_size=()` is rejected, so every `Cell` + hidden state is at least two-dimensional and its trailing axis always + enumerates compartments or points. That invariant is what lets `Cell` + states be `brainstate.HiddenGroupState` (`Cell.V` is a + `braincell.DiffEqGroupState`) while `SingleCompartment`, which has no + spatial axis, keeps the plain `brainstate.HiddenState`. See + `docs/specs/2026-08-13-cell-hidden-group-state.md`. +- [x] **The channel template layer validates at class-definition time.** + `HH` and `Markov` resolve and check `gates` / `pairs` in + `__init_subclass__`, so a mistyped gate name, a duplicate, a gate + defining neither (or both) rate forms, a transition naming a missing + rate method, and a `dependent_state` outside the state set are all + rejected when the class is created rather than at `reset_state()`. + `init_state` refuses to bind a gate over a non-`DiffEqState` + attribute, which used to silently replace a constructor parameter. +- [x] **Gate and transition rates carry real units.** `Gate.time_unit` + (default `u.ms`) says what a bare `f_*_tau` / `f_*_alpha` / `f_*_beta` + return means; a united return is used as given and a wrong dimension + is rejected against the gate by name. Markov transition rates accept + the same two forms against a fixed `u.ms`. Every state derivative is + asserted to be an inverse time before it reaches the integrator, + which is what catches a dimensioned `phi`. +- [x] **`OhmicHH` carries the ohmic driving force.** 63 channels that + restated `g_max * conductance_factor(...) * (E - V)` verbatim now + inherit it; a channel reading a fixed `self.E` overrides + `reversal_potential()`. GHK-flux and permeability-scaled channels + keep inheriting `HH` and writing their own `current()`. +- [x] **Gate metadata binds by attribute name.** `Gate(q10="q10")` + replaces the 75 `lambda self: self.q10` closures, which were + unpicklable and invisible to tooling. The callable form still works. +- [~] **Gate/state clipping is an explicit policy.** `Gate.clip` + defaults to `False` (NEURON does not clip HH gates, and the catalogue + is validated against those mechanisms); `Markov.clip_states` defaults + to `True`. Both project only the value fed to the conductance product + or the kinetics, never the stored state. Remaining gap: the implicit + `dependent_state` fallback still exists behind a `DeprecationWarning` + and is slated for removal. + +## 1. Mission and Scope + +BrainCell is a JAX-native library for **biologically detailed cell and network +modelling**. It targets the same workload as NEURON, Arbor, and BluePyOpt but +expresses models as differentiable, vectorized JAX programs so that +multi-compartment populations can be simulated, connected, batched, and +parameterized inside the broader `brain*` ecosystem (`brainstate`, +`brainunit`, `brainevent`, `braintools`, `brainpy`). + +The library owns seven concerns end-to-end: + +1. **Morphology ingestion** — read SWC / ASC / NeuroML2, validate, cache. +2. **Geometry & discretization** — turn a morphology + a CV policy into + immutable control-volume (CV) arrays suitable for vectorized solvers. +3. **Mechanism declaration** — paint cable properties, density mechanisms, + and ion channels onto regions; place point mechanisms onto locsets. +4. **Runtime lowering** — initialize `Cell` with resolved ion species, + channel state, point-mechanism storage, and a DHS-ordered node tree. +5. **Numerical integration** — provide a registry of explicit, implicit, + exponential, and staggered step functions, including a custom DHS + voltage solver for branched cables. +6. **Network execution** — connect event sources to Cell-owned synapses, + schedule delayed delivery, and aggregate samples and sparse events. +7. **Parameterization** — expose selected physical fields through stable, + unit-aware trainable parameter mappings. + +Out of scope (for this iteration): a BrainCell-owned optimizer or Trainer, +plasticity learning rules, trainable topology, NEURON HOC compatibility, GUI +tools, and stand-alone NMODL execution. The previous `mech/nmodl/` research +tree has been removed; if NMODL support returns, it will be a separate codegen +design targeting the mechanism registry. + +--- + +## 2. Top-Level Architecture + +[System Overview](../design/architecture/current/system-overview.md) describes module responsibilities, +dependencies, dataflow, state ownership, lifecycle, and execution paths. Cross-module design +questions and next actions are tracked in [Architecture TODO](../design/architecture/TODO.md). + +--- + +## 3. Module Catalogue + +Each subsection lists: **purpose · key types · public API surface · +internal dependencies · status · open work**. + +### 3.1 `braincell.morph` — morphology data model + +- **Purpose** — owns the canonical in-memory representation of a neuron's + geometry. Splits cleanly into immutable per-branch geometry (`Branch`) + and a mutable owning tree (`Morphology`). +- **Key types** + - `Branch` (frozen dataclass) and typed subclasses `Soma`, `Dendrite`, + `Axon`, `BasalDendrite`, `ApicalDendrite`, `CustomBranch`. + Built via `Branch.from_lengths` / `Branch.from_points`. + - `branch_class_for_type(type_str)` factory used by IO readers. + - `Morphology` — mutable owning tree, root attachment, attribute-style + children (`morpho.soma.dendrite = ...`), `topo()` text rendering, + `branches`, `edges`, `branch_by_order`. + - `MorphoBranch` — node view exposing parent / children navigation. + - `MorphoEdge` — frozen, read-only directed edge between two + `MorphoBranch` nodes. + - `MorphoMetric` — frozen snapshot of `n_branches`, `total_length`, + `total_area`, `total_volume`, `max_path_distance`, + `max_euclidean_distance`, `max_branch_order`, range boxes, etc. +- **Status** + - [x] Branch geometry, area, volume, point/length constructors. + - [x] Morphology root construction, `attach`, sugar attribute API, + topology queries, `topo()` text tree. + - [x] `Morphology.from_swc` / `Morphology.from_asc` constructors. + - [x] `save_checkpoint` / `load_checkpoint` (`.bcm` self-contained + format) plus `pickle` / `copy.deepcopy` support. + - [x] `MorphoMetric` covering total length / area / volume, branch + order, path distance, Euclidean distance. + - [ ] **Tree editing primitives**: delete subtree, splice subtree, + merge two morphologies at a chosen attachment point, swap a branch + with another while preserving orientation. + - [ ] **In-place geometry transforms**: translate / rotate / scale / + align principal axis, with corresponding metric invalidation. +- **Open risks** + - Mutability of `Morphology` versus the immutability of `Branch` + (and downstream caches in `Cell`) makes accidental aliasing easy. + Tree-edit operations must follow the existing + `Morphology.clone()` discipline used by `Cell`. + +### 3.2 `braincell.io` — file-format ingestion + +- **Purpose** — read morphologies from common neuroscience formats and + produce a `Morphology` plus a structured report describing parsing + decisions and validation issues. +- **Key types** + - `swc.SwcReader`, `SwcReadOptions`, `SwcReport`, `SwcIssue` plus + rulebook (`rules.py`) and soma reconstruction (`soma.py`). + - `asc.AscReader`, `AscReport`, `AscIssue`, `AscMetadata`. + - `neuroml2.NeuroMlReader`. + - `neuromorpho` package — three-tier NeuroMorpho.Org integration: + - Tier 1: `load_neuromorpho` (also re-exported as + `braincell.load_neuromorpho`), `fetch_neuromorpho`, and the + `Morphology.from_neuromorpho` classmethod sibling to `from_swc` / + `from_asc`. + - Tier 2: `NeuroMorphoClient` (typed `search` / `iter_search`, + `get_neuron`, `get_measurement`, `describe`, `download` with + `dry_run=True`, configurable `retries` / `backoff_base`). + - Tier 3: `NeuroMorphoCache`, `NeuroMorphoCacheLayout`, + `NeuroMorphoQuery`, `NeuroMorphoMeasurement`, `NeuroMorphoFilePlan`, + `NeuroMorphoUrls`, `NeuroMorphoCacheStatus`, + `NeuroMorphoSearchPage`, `NeuroMorphoDetail`, + `NeuroMorphoDownloadItem`, `NeuroMorphoDownloadRecord`, + `NeuroMorphoNeuron`, plus pure URL helpers + (`build_standard_swc_url`, `build_original_file_url`, + `infer_original_extension`, `plan_neuron_files`). + - Errors: `NeuroMorphoError`, `NeuroMorphoHTTPError`, + `NeuroMorphoNotFoundError`. + - `io.checkpoint` — `save_branch` / `load_branch` / + `save_morpho` / `load_morpho` and the `.bcm` single-file format. +- **Status** + - [x] SWC import + rulebook validation + report. + - [x] SWC export through `Morphology.to_swc()` / `swc.write_swc()`, + with structural round-trip coverage for branch endpoint duplication, + soma interior attachments, reversed branches, and invalid geometry. + - [~] ASC import: most Neurolucida trees, metadata, and + `Morphology.from_asc(..., return_report=True)` work; **gaps**: + spine markers, contour-only somas, and multi-tree files are still + handled minimally — see `io/asc/reader_test.py` skips. + - [ ] NeuroML2 import — reader stub exists; needs cell, segment-group, + biophysics decoding and round-trip tests. + - [x] NEURON-based diff harness via `examples/neuron_compare/morph/neuron_diff.py`. + - [x] NeuroMorpho.Org integration: Tier 1 `load_neuromorpho` / + `fetch_neuromorpho` one-liners, Tier 2 `NeuroMorphoClient` with + typed `iter_search` / `download` / retries, Tier 3 `NeuroMorphoCache` + plus pure URL helpers, full NumPy-doc docstrings, and + `Morphology.from_neuromorpho` classmethod. Notebook walkthrough at + `examples/multi_compartment/neuromorpho.ipynb` shows the full search → cache → + metric-diff loop. + - [ ] Automated metric diff against published NeuroMorpho reference + statistics promoted from the notebook into a pytest case (so the + NeuroMorpho corpus becomes a wide regression net). + - [x] Checkpoint API and `.bcm` format with notebook tutorial + (`examples/multi_compartment/morphology-checkpoint.ipynb`). + - [ ] **NMODL parsing compiler** — deferred. The previous + `mech/nmodl/` research tree has been removed from the working + copy; if NMODL support returns it will land as a codegen pass + targeting the mechanism registry (see §3.4 / M5 Phase 4). +- **Open risks** + - Format heterogeneity is the dominant source of bugs. Every reader + must produce a `Report` so user-facing tools can surface issues + instead of silently massaging geometry. + +### 3.3 `braincell.filter` — region & locset selection + +- **Purpose** — declarative, composable selection of regions of a + morphology and points on it. The cell layer consumes these to map + user intent onto control volumes. +- **Key types** + - `RegionExpr` family: `BranchSlice`, `branch_in(...)` predicates for + branch metadata / topology, `branch_range(...)` for scalar branch + properties and metrics, set operations + (union / intersection / difference / complement). + - `LocsetExpr` family: root, branch points, terminals, region-driven + uniform sampling, region-driven random sampling. + - `SelectionCache` — memoizes resolved index sets for stable + Morphology objects. +- **Status** + - [x] BranchSlice, broadcasted inputs, set algebra. + - [x] Discrete predicates (type / name / branch_order / parent_id / + n_children / n_tapers / branch_id). + - [x] Continuous `branch_range(...)` with both numeric and `Quantity` + bounds. + - [x] Branch scalar metric filters: `length`, `mean_radius`, `area`, + `volume`. + - [ ] **Radius-range filter** (e.g., `radius_range(0.5*u.um, 2*u.um)`). + - [ ] **Path-distance filter** (graph distance from soma along the + tree). + - [ ] **Euclidean-distance filter** (3-D distance from a chosen + anchor point). + - [ ] **Subtree region** — everything reachable below a given branch + or locset; needs to interoperate with the planned + `Morphology` subtree-edit operations. + - [x] Locset: root, branch points, terminals. + - [x] Locset: uniform / random sampling driven by a region. + - [~] **Locset anchors and fixed-step sampling**: `RegionAnchors` and + explicit `at(branch, x)` locations are implemented; `StepSamples` + remains a reserved expression that raises `NotImplementedError`. +- **Open risks** + - The reserved distance/radius/subtree expressions must reuse the existing + morphology spatial metrics and `SelectionCache`; they must not introduce + a second geometry cache with different invalidation semantics. + +### 3.4 `braincell.mech` — mechanism declarations + +- **Purpose** — strongly-typed, purely-declarative containers used by + the `Cell` frontend. Everything here describes *what to install*, not + *how to integrate*: no `brainstate`, no JAX, no runtime state. The + concrete ion species, ion channels, and synapses live in peer + top-level modules (`braincell.ion`, `braincell.channel`, + `braincell.synapse`) and register themselves with the + `MechanismRegistry` at import time via class-level decorators; the + runtime lowering in `braincell._compute` resolves a + `Density.class_name` through the registry when it installs channels + on a cell. +- **Key files & types** + - `mech/_base.py` — `Mechanism` marker base class. Every mechanism + declaration (density or point) inherits from it, so consumers can + check `isinstance(x, Mechanism)` without having to know whether + they hold a `Density` or a `Point`. + - `mech/_registry.py` — `MechanismEntry(category, name, cls, + aliases)` frozen dataclass, `MechanismRegistry` with + `register` / `unregister` / `add_alias` / `contains` / `get` / + `entry` / `names` / `items` / `clear`, the `_REGISTRY` singleton + accessed via `get_registry()`, and the three class-level + decorators `register_channel` / `register_ion` / + `register_synapse`. Unknown-name lookups raise `KeyError` with a + `difflib`-based "did you mean ...?" suggestion (same pattern as + `braincell.quad._registry`). Three valid categories: + `"channel"`, `"ion"`, `"synapse"`. + - `mech/_params.py` — `Params(Mapping[str, Any])` frozen hashable + mapping. `__hash__` uses `frozenset(self._items.items())`, so + `Channel("IL", g_max=..., E=...)` and `Channel("IL", E=..., + g_max=...)` deduplicate into a single paint-layout group. Iteration + order is the declared order so `repr()` is stable. Accepts + `Mapping`, `(k,v)` tuples, or another `Params` in the constructor + (`Params.coerce(value)`), supports `**params` unpacking via the + `Mapping` protocol, and exposes non-mutating `with_updates(...)` / + `without(...)`. + - `mech/_density.py` — `Density(Mechanism)` abstract base plus the + concrete subclasses `Channel(Density)` and `Ion(Density)`. `Density` + is a manually-immutable `__slots__` class (not a dataclass) with a + `category: ClassVar[str]` discriminator set by each subclass + (`"channel"` / `"ion"`). The constructor accepts `class_name` as + either a string **or** a class (`braincell.channel.IL`); types are + resolved to their canonical registry name via reverse lookup. + `coverage_area_fraction` is a dedicated first-class field, not a + pseudo-parameter. `instance_name` falls back to `class_name`, + `identity = (instance_name, class_name)` drives paint-layout + grouping, and `with_params(...)` / `with_coverage(...)` / + `with_name(...)` return non-mutating copies via an internal + `object.__new__` + `object.__setattr__` bypass. `Channel` and + `Ion` collect parameters via `**params` kwargs. + - `mech/_point.py` — `Point(Mechanism)` plain base class (not a + `Union`; use `isinstance(x, Point)` in consumers) plus concrete + frozen-dataclass subclasses `CurrentClamp`, `SineClamp`, + `FunctionClamp`, `ProbeMechanism`, and `Synapse`. `CurrentClamp` + has one canonical form `(start, durations, amplitudes)` and a + `CurrentClamp(delay=..., durations=duration, amplitudes=amplitude)` classmethod + shortcut. `Synapse` is itself a frozen dataclass + (`synapse_type`, `params`, `name`); there is no separate factory + function. + - `mech/_junction.py` — `Junction(Point)` frozen dataclass for + gap-junction coupling declarations. Placeholder implementation + (`params` field only); lives in its own module so downstream + work on gap-junction state and partner wiring has a clean home. + - `mech/_cable.py` — `CableProperty` frozen dataclass + (`resting_potential`, `membrane_capacitance`, `axial_resistivity`, + `temperature`, all `brainunit` quantities; temperature defaults to + 36 °C via a `default_factory` and is coerced to kelvin in + `__post_init__`). Exposes non-mutating `with_updates(**kwargs)`. + - `mech/__init__.py` — re-exports the public surface + (`Mechanism`, `Density`, `Channel`, `Ion`, `Point`, `CurrentClamp`, + `SineClamp`, `FunctionClamp`, `ProbeMechanism`, `Synapse`, + `Junction`, `CableProperty`, `Params`, registry API). + - Co-located tests: `_base_test.py`, `_registry_test.py`, + `_params_test.py`, `_density_test.py`, `_point_test.py`, + `_junction_test.py`, `_cable_test.py`. +- **Status** + - [x] `CableProperty`, `Density` (with `Channel` / `Ion` + subclasses), and the full `Point` family (`CurrentClamp`, + `SineClamp`, `FunctionClamp`, `ProbeMechanism`, `Synapse`, + `Junction`) with `brainunit`-typed fields and co-located tests. + Everything inherits from a shared `Mechanism` marker base class. + - [x] **One type per concept.** The legacy `MechanismSpec` / + `DensityMechanism` duality and the eight `density_*` isinstance- + dispatch helpers in `spec.py` are gone. Every density declaration + is a `Density` subclass (`Channel` or `Ion`) carrying a + `category` `ClassVar`; every point declaration is a `Point` + subclass. + - [x] **Class-based `Channel` / `Ion`.** `braincell.mech.Channel` + and `braincell.mech.Ion` are real classes (not factory functions) + inheriting from `Density`. They accept the target class as either + a string name (`"IL"`) or the concrete class object + (`braincell.channel.IL`); the class form is reverse-looked-up in + the registry to produce the canonical name so aliases continue to + collapse into one identity. Top-level `braincell.Channel` / + `braincell.Ion` still point at the runtime base classes from + `_base_channel.py` / `_base_ion.py`; the declaration-layer classes are + reached via + `braincell.mech.Channel` / `braincell.mech.Ion` to avoid the + name collision. + - [x] **Mechanism registry.** `MechanismRegistry` + the + `@register_channel` / `@register_ion` / `@register_synapse` + decorators ship in `mech/_registry.py`. ~49 concrete classes in + `braincell.channel`, `braincell.ion`, and `braincell.synapse` + self-register at import time. `get_registry().get(category, + class_name)` is the single lookup path used by + `_compute/parameters.py` and `_compute/bindings.py` to resolve + `Density.class_name` into a + runtime class. Channel-to-ion binding is inferred from + `issubclass(cls.root_type, Sodium / Potassium / Calcium)`, not + from hardcoded class-name matching. Abstract base classes + (`LeakageChannel`, `SodiumChannel`, `Calcium`, …) are deliberately + **not** decorated. + - [x] **Hash-stable Params.** `Params.__hash__` uses + `frozenset(items)` so two `Channel(...)` calls with the same + parameters in different keyword order compare equal and + deduplicate into the same paint-layout group. Only `params` is + hash-insensitive; `class_name`, `name`, `category`, and + `coverage_area_fraction` remain position-sensitive. + - [x] **`coverage_area_fraction` as a first-class field** on + `Density`. The old abstraction leak where coverage was smuggled + through ordinary mechanism parameters is gone; `_discretization` + and `_compute` preserve it as geometry metadata. + - [x] **Unified `CurrentClamp`.** One canonical frozen-dataclass + form `(delay, durations, amplitudes)`. The old + `CurrentClamp(amplitude=, delay=, duration=)` compatibility form + is gone; use `CurrentClamp(delay=..., durations=duration, amplitudes=amplitude)`. + - [x] **Consumer simplification.** `_discretization/mechanism.py` and + the `_compute` layout, binding, parameter, and table modules operate + directly on the declaration types without a parallel spec hierarchy. + - [ ] **Parameter-unit validation** — `Params` currently stores + values untyped. Needs compile-time validation that each value + carries the brainunit dimension the target channel declares + (e.g. `g_max` must be in `S/cm²`, `E` in `mV`), with an error + that points at the offending `paint(...)` call. The infrastructure + for this lives on the mechanism registry: each entry can declare + the expected unit per parameter name. + - [ ] **`Junction` runtime wiring** — `Junction` currently ships + as a placeholder frozen dataclass with only a `params` field. + It needs a `partner` reference (locset or another placed + `Junction`), symmetric pair resolution in the runtime, and a + gap-junction current contribution in the voltage solve. Tracked + as the first sub-task in milestone M5 Phase 3. + - [ ] **`ProbeMechanism` variable taxonomy** — `variable` is + currently a free-form string. Promote it to a typed enum of known + probes (`"v"`, `"ina"`, `"ik"`, `"ica"`, `"cai"`, `"cao"`, + channel gate names, …) so user typos fail at declaration time + rather than silently producing empty traces. + - [ ] **Mechanism validation harness** — a structured comparison + against NEURON `.mod` reference traces for every channel in + `braincell.channel`. The previous `mech/mod_validate/` tree has + been removed from the working copy; the harness needs to be + re-introduced as a package under `braincell/mech/` (or a sibling + test package) and promoted to automated pytest cases. Tracked in + milestone M5. + - [ ] **NMODL ingestion** — deferred. If NMODL support returns it + must target the mechanism registry so generated channels land + under the standard naming convention in `braincell.channel` + rather than creating a parallel hierarchy. +- **Open risks** + - **Hash-insensitive `Params` equality** only kicks in for the + `params` field; `class_name`, `name`, `category`, and + `coverage_area_fraction` stay position-sensitive. Do not extend + the hash-insensitive treatment to other fields without first + understanding the paint-layout grouping contract in + `_discretization/mechanism.py`. + - **Class-level decorator ordering.** Registration is a side + effect of importing `braincell.channel` / `braincell.ion` / + `braincell.synapse`. If a user imports `braincell.mech` alone + (without importing the concrete modules) the registry is empty — + by design. The canonical entry points in `braincell/__init__.py` + already import all three, so normal users never see this. + - **Ion binding inference** uses + `issubclass(cls.root_type, Sodium/Potassium/Calcium)` in + `_compute/bindings.py`. New ion species must either set + `root_type` on their channels or we extend the dispatch to walk + a lookup table — do not hardcode class-name matching. + - **Name collision with runtime `Channel` / `Ion` bases.** The + declaration-layer `Channel` / `Ion` classes live under + `braincell.mech`, not at the top level of `braincell`, because + `braincell.Channel` / `braincell.Ion` already resolve to the + runtime base classes from `_base_channel.py` / `_base_ion.py`. + Tutorials and user code + should use the fully-qualified `braincell.mech.Channel` / + `braincell.mech.Ion` when declaring mechanisms on a `Cell`. + - The module is intentionally free of `brainstate` / JAX state — + keeping `mech` purely declarative makes importing `braincell.mech` + cheap and keeps the declaration frontend usable even in + environments where the numerical runtime is absent. Do not + import `brainstate`, `jax`, or any concrete channel/ion/synapse + class inside `braincell/mech/`. The one permitted dynamic + import is inside `_density._resolve_class_name`, which consults + the registry via a lazy `from ._registry import get_registry` + local import when a user passes a class object instead of a + name string. + +### 3.5 `_discretization` / `_compute` / `_multi_compartment` — Cell runtime + +- **Purpose** — turn *(Morphology, CVPolicy, paint/place declarations)* + into an initialized, directly runnable `Cell(HHTypedNeuron)`: + - `braincell._discretization` owns immutable CV geometry, policies, + mechanism rules, `CVTree`, and declaration-time `NodeTree` data. + - `braincell._compute` owns runtime layouts, bindings, CV/point bridges, + scheduling, tables, and `CellRuntimeState`. + - `braincell._multi_compartment` owns `Cell`, its spatial and mechanism + views, clamps, synapses, probes, and `RunResult`. +- **Status** + - [x] `Cell(morpho, pop_size=..., cv_policy=...)`, `paint`, and `place` + form the declaration phase; declarations freeze after initialization. + - [x] `Cell.init_state()` lowers the declaration and installs runtime + state on the same object. `Cell.run(dt=..., duration=...)` advances it + directly; there is no public build phase or `RunnableCell`. + - [x] CV policies, geometry, axial-resistance partitioning, mechanism + lowering, point topology, DHS scheduling, and CV/point conversion. + - [x] Homogeneous populations with mandatory population axes and + multi-dimensional `pop_size`. + - [x] Cell, Channel, Ion, Synapse and Clamp views with Cell-owned + connection, recording, and trainable-parameter storage. + - [x] Fixed-step clamps retain their exact continuous interval at runtime; + density parameters are materialized on CVs rather than non-CV points. + - [x] NEURON-compatible ion-current snapshots and selectable + `"family"` / `"integration"` ion-channel update ordering. + - [ ] **SingleCompartment ODE unification.** Design discussion; no new + single mode is implemented. Local decisions, open questions, and next + steps are tracked in [Cell TODO](../design/cell/TODO.md). +- **Open risks** + - Declaration shapes and ownership must remain fixed after + `init_state()` so JIT state trees and network routing stay stable. + - Parameter materialization may change values without changing runtime + layout, topology, units, or state shape. + +### 3.6 `braincell.quad` — numerical integrators + +- **Purpose** — provide a uniform registry of step functions over + `DiffEqModule` targets, plus the specialized branched-cable voltage + solver. +- **Key types** + - `IntegratorRegistry`, `IntegratorEntry`, `register_integrator`, + `get_registry`, `get_integrator`. Decorator-based registration with + canonical name, aliases, category, order, description, deprecation. + - `_RegistryDictView` exposes a read-only `all_integrators` mapping + for legacy callers. + - `DiffEqModule`, `DiffEqState`, `IndependentIntegration` — + structural protocols and helpers for step functions. + - **Explicit families**: `euler_step`, `rk2/3/4_step`, `heun2/3_step`, + `midpoint_step`, `ralston2/3/4_step`, `ssprk3_step`. + - **Implicit / mixed**: `backward_euler_step`, `implicit_euler_step`. + - **Exponential Euler**: `exp_euler_step`, `ind_exp_euler_step`. + - **Staggered**: `staggered_step` (DHS voltage solve + + `ind_exp_euler` for ion-channel state, the workhorse for full + cells). + - **Voltage solvers**: `dhs_voltage_step` (DHS branched Hines), + `dense_voltage_step`, `sparse_voltage_step`. +- **Status** + - [x] Registry, alias resolution, "did you mean ...?" suggestions. + - [x] Backwards-compatible `all_integrators` mapping view. + - [x] All explicit RK / Heun / Ralston / Midpoint / SSPRK families. + - [x] Backward Euler and implicit Euler. The six cell-only variants + (`implicit_rk4`, `implicit_exp_euler`, `cn_rk4`, `cn_exp_euler`, + `exp_exp_euler`, `splitting`) were removed: they had rotted against + several `brainstate` / `Cell` API generations and none could be + invoked successfully. `braincell/quad/_implicit_test.py` pins their + absence from the registry. + - [x] Exponential Euler (`exp_euler_step`, `ind_exp_euler_step`). + - [x] Staggered solver (`staggered_step`). + - [x] The staggered full-cell path calls + `cache_ion_total_currents(...)` when the target supports it, so + NEURON-compatible ion-current snapshot semantics can be selected at + the `Cell` level without changing the integrator API. + - [x] DHS voltage solver (`dhs_voltage_step`). + - [ ] **Adaptive timestep wrapper** that produces a registered + integrator from any embedded RK pair. + - [x] **Convergence test matrix** — pytest-driven order-of-accuracy + checks for every registered integrator on a small set of + reference ODEs (passive cable, single HH spike, two-branch Y). + - [ ] **Performance benchmarks** vs NEURON / Arbor on the standard + Mainen / Hay / L5PC cells, run nightly via `CI-daily.yml`. + +### 3.7 `braincell.vis` — visualization + +`braincell.vis` 已提供 2D/3D 形态图、数值着色、轨迹与动画、拓扑分析、交互和导出。 +当前讨论将可视化迁入 braintools,由同一模块提供简单绘图和 GUI 两类入口;下一步确定 +模块归属、共享数据接口及依赖适配方式。 + +- 模块事项与下一步:[Vis TODO](../design/vis/TODO.md)。 +- 已有功能、后端差异与验证现状:[Visualization](../design/vis/current/visualization.md)。 +- 迁移方案与准备工作:[Braintools Migration](../design/vis/proposals/braintools-migration.md)。 + +### 3.8 `braincell.ion` — ion species + +- **Purpose** — concrete `Ion` subclasses modelling intra/extracellular + concentration, reversal potential, and the container of ion-bearing + channels that consume the species' `IonInfo`. Lives as a peer + top-level module (not under `mech`) because the classes are runtime + objects with JAX state, not declarations. +- **Key files & types** + - `braincell/ion/_base.py` — reusable `FixedIon`, `InitNernstIon`, + `DynamicNernstIon`, and `KineticIon` lifecycle templates. + - `braincell/ion/sodium.py` — `Sodium` (abstract base with + `root_type = HHTypedNeuron`), `SodiumFixed`, and `SodiumInitNernst`. + - `braincell/ion/potassium.py` — `Potassium` abstract base and + fixed and initialized-Nernst variants. + - `braincell/ion/calcium.py` — `Calcium` base class, + fixed/initialized-Nernst variants, and two concrete dynamics models: + - `CalciumDetailed` — Destexhe et al. 1993 thin-shell model with + tunable `d`, `tau`, `C_rest`, `C0`, `T`. + - `CalciumFirstOrder` — Bazhenov et al. 1998 first-order pool + (`Ca' = α I_Ca − β Ca`). + Both expose `C` as a `DiffEqState`, compute the Nernst reversal + `E = (RT/2F) log(C0/C)` as a property, and forward + `compute_derivative` to every attached `Channel` child. + - Co-located tests: `sodium_test.py`, `potassium_test.py`, + `calcium_test.py`. +- **Status** + - [x] `SodiumFixed` / `PotassiumFixed` / `CalciumFixed` parameter + storage, container (`**channels`) attachment, and `pack_info()` + returning an `IonInfo(C, E)` tuple. + - [x] `CalciumDetailed` / `CalciumFirstOrder` with Nernst reversal + and full derivative wiring to child calcium channels. + - [x] `KineticIon`-based Cerebellum calcium-pool mechanisms imported + for the current comparison work, including `CdpStC_MA2020_GoC`, + `CdpStC_NoCAM_MA2020_GoC`, `CdpStC_CAMOnly_MA2020_GoC`, + `CdpStC_MA2025_BC`, `CdpStC_RI2021_SC`, `CdpCAM_MA2024_PC`, and + `CdpCR_MA2020_GrC`. + - [x] Co-located unit tests (~75) covering defaults, custom + parameters, callable broadcasts, `init_state` / + `reset_state` / `compute_derivative`, `pack_info`, + external-current registration, Nernst formula edge cases, and + child-channel forwarding. + - [ ] **`SodiumDetailed` / `SodiumFirstOrder`** — activity- + dependent Na⁺ accumulation (e.g., for spike-frequency adaptation + driven by a Na/K pump). Parallel to the calcium dynamics pair + and needed to reproduce several of the published cortical + models in `examples/`. + - [ ] **`PotassiumDetailed` / `PotassiumFirstOrder`** — activity- + dependent intracellular / extracellular K⁺ accumulation for + network-level effects and K-pump dynamics, with the same + Nernst-reversal property as the calcium path. + - [ ] **`Chloride` ion** (`Chloride`, `ChlorideFixed`, + `ChlorideDynamics`) in a new `braincell/ion/chloride.py` plus a + sibling `chloride_test.py`. Needed for quantitative GABAa + modelling and developmental E_Cl shifts. + - [x] **Shared ion lifecycle templates** — package-private `FixedIon`, + `InitNernstIon`, `DynamicNernstIon`, and `KineticIon` mixins own the + reusable initialization, reversal, and kinetics contracts. + - [x] **`__init__.py` hygiene** — ion and channel re-export sets are + explicit, deduplicated, and guarded by package-level re-export tests. + - [x] **Mechanism-registry plumbing** — every concrete `Ion` + subclass now self-registers via `@register_ion("CalciumFixed")` / + `@register_ion("CalciumDetailed")` / `@register_ion("CalciumFirstOrder")` / + `@register_ion("SodiumFixed")` / `@register_ion("PotassiumFixed")` + at import time, and `braincell.mech.Ion("CalciumFixed")` resolves + through the registry described in §3.4. + - [x] **Current-driven ion dynamics can use cached ion current.** + Kinetic ions that consume total calcium current can receive the + runtime snapshot created by `cache_ion_total_current=True`, matching + the NEURON-style separation between channel-current evaluation and + ion-state integration. + - [ ] **Consistent external-current registration** — audit that + every dynamics class honours `include_external=True` in its + `derivative` (the existing `CalciumDetailed.derivative` already + does; the contract must stay alive across future refactors). +- **Open risks** + - **Nernst unit trap.** Nernst factors resolve correctly only when every + term remains a `brainunit` quantity; changes to the shared ion templates + must preserve units through graph flattening and materialization. + - **Shared lifecycle contracts.** New ion families must use the common + template hooks and contract tests so child-channel reset and derivative + forwarding cannot diverge by species. + - **Test-side coupling with `braincell.channel`.** The calcium + tests instantiate `CaT_HM1992` to exercise child-channel + forwarding, so a heavy top-level import in `braincell.channel` + would drag through the ion suite. Keep the channel package + tree-shakable (see §3.9 risks). + +### 3.9 `braincell.channel` — concrete ion channels + +- **Purpose** — the library's catalogue of ready-to-use HH-style and + Markov-kinetics ion channels. Every class is a subclass of + `Channel` from `_base_channel.py` (so every instance is an `IonChannel` + that registers its gate state as `DiffEqState`s) and declares + `root_type = HHTypedNeuron`. Channels are container children of + an `Ion` species or of a `SingleCompartment` / `Cell` directly. +- **Key families** + - `sodium.py` — `Na_Ba2002`, `Na_TM1991`, `Na_HH1952`, persistent, + resurgent, and cell-specific Nav families. + - `potassium.py` — delayed rectifier, A-type, inward rectifier, Kv, + and M-current families such as `KDR_Ba2002`, `K_HH1952`, and the + MA2020/MA2024 cell-specific variants. + - `calcium.py` — T/L/HVA/LVA and Cav families, including frozen-gradient + variants used by controlled NEURON comparisons. + - `braincell/channel/leaky.py` — `LeakageChannel` base and the + passive leak `IL`. + - `hyperpolarization_activated.py`, `potassium_calcium.py`, and + `potassium_sodium.py` — HCN and mixed-ion channel families. +- **Status** + - [x] Concrete channel families use current-free mechanism names such as + `Na_HH1952`, `K_HH1952`, `CaT_HM1992`, and `HCN_HM1992`; the removed + leading-`I` compatibility aliases are not public API. + - [x] Co-located tests cover kinetics, current sign and shape, lifecycle, + template invariants, and representative reference voltages. + - [x] Concrete classes self-register with the mechanism registry at import + time; abstract family bases are deliberately not registered. + - [x] **PC MA2024 channel set imported.** Sodium, potassium, + calcium, calcium-activated potassium, and HCN PC variants have been + added and covered by targeted tests. The calcium channel set also + includes `_Frozen` variants for the NEURON-comparison path where the + current expression must treat voltage as fixed with respect to + differentiation. + - [ ] **Parameter metadata** — each channel should declare the + unit of every user-facing parameter (`g_max` in `S/cm²`, `E` in + `mV`, time constants in `ms`, …) so that `Density.params` + validation can produce an actionable error at paint time rather + than an opaque JAX trace failure. Store the per-parameter unit + on `MechanismEntry.metadata` and consult it during + `Density.__init__`. + - [~] **GHK current formulation** — `GhkHH` and `ghk_flux` are implemented, + tested, and used by selected Cav channels; the remaining work is a + catalogue-wide audit of which published mechanisms require GHK rather + than an ohmic driving force. + - [~] **Q10 temperature scaling audit** — shared `q10_factor` and + `cached_q10_factor` helpers exist and most gates use the template path; + remaining family-specific temperature assumptions need documentation. + - [ ] **NEURON `.mod` validation** — for every channel in the + catalogue, compare voltage-clamp and current-clamp traces + against the reference `.mod` implementation within a tight + tolerance. Requires re-introducing the `mech/mod_validate/` + harness (see §3.4) and wiring it into milestone M5. + - [ ] **Chloride channels** — add a `braincell/channel/chloride.py` + module once `braincell.ion.Chloride` lands, covering the passive + leak plus GABAa-reversal-driven phasic conductance. + - [ ] **Stiff-channel integrator audit** — run the convergence matrix + over every channel to identify models that require a dedicated + integration path. + - [ ] **Gate-variable naming convention** — most channels use + `p`/`q` for activation / inactivation and a handful use bespoke + names (`m`, `h`, `n`, `s`, …). Tests already rely on the + `p`/`q` convention; unifying the rest will need a deprecation + path because downstream code reaches into `channel.p.value`. +- **Open risks** + - **Import cost.** The package has thirty-plus classes and pulls + `braintools.init`, `brainunit`, and `jax.numpy` at import time. + New families should stay in their own module so the package + remains tree-shakable, and should avoid importing numpy at + module top level beyond what is already there. + - **Cross-ion channels.** `potassium_calcium.py` channels depend + on the attached calcium pool's `C` state. Compile-time checks + that the parent `Cell` actually has a calcium ion attached would + prevent silent `KeyError` / `AttributeError` at simulate time; + this belongs on the mechanism registry in §3.4. + - **API drift vs NEURON naming.** Upstream `.mod` files use lowercase + suffixes (`ih`, `ik`, `ikdr`), while BrainCell names mechanisms by + family/model and provenance. Any validation harness + needs a stable alias table so the diff does not become a + renaming exercise every time a new channel lands. + +### 3.10 `braincell` package root — neuron base classes + +- `_base_neuron.py`, `_base_ion.py`, and `_base_channel.py` define the + runtime bases composed by concrete neurons and mechanisms. +- `_single_compartment/` owns `SingleCompartment`, the simplest concrete + neuron and a numerical sanity surface. +- `_multi_compartment/` owns the directly initialized and executed `Cell`, + its views, point-mechanism stores, probes, and `RunResult` (see §3.5). +- `_misc.py` — `normalize_param` (the brainunit gatekeeper), helpers, + decorators (`set_module_as`, `deprecation_getattr`), `Container`. +- `_typing.py` — type aliases (`Initializer`, `ArrayLike`, `T`, `DT`). + +### 3.11 `braincell.network` — population and event runtime + +- **Purpose** — register Cells and event sources, connect source outputs to + Cell-owned synapses, coordinate lifecycle and delayed delivery, and + aggregate immutable sample and sparse-event results. +- **Status** + - [x] Direct `Network`, `Population`, `NetworkConnections`, and + `NetworkResult` model with no separate public build phase. + - [x] Named connection calls, explicit or sampled endpoint pairing, + heterogeneous delays, split runs, reset semantics, and cached schedules. + - [x] Static recording schemas with regular `SampleBlock` outputs and + sparse `EventSeries` outputs. + - [ ] Runtime scalability and topology extensions; local questions and + acceptance boundaries are tracked in [Network TODO](../design/network/TODO.md). +- **Design authority** — [Network TODO](../design/network/TODO.md) + and its linked current contracts, proposals, and references. + +### 3.12 `braincell.trainable` — parameter ownership and mapping + +- **Purpose** — bind optimizer-facing parameter roots to selected physical + runtime fields while preserving units, sharing semantics, and stable JAX + state trees. It does not own optimizers, losses, datasets, or training loops. +- **Status** + - [x] `ParameterSource`, `ParameterBinding`, `ParameterSet`, and + `TrainableManager`, plus direct, shared-scale, and callable latent sources. + - [x] Cell-local ChannelView mappings for the initial supported channel + families, with transactional validation and differentiable materialization. + - [ ] Ion, Synapse and Connection parameters, Network aggregation, and + broader parameter families. +- **Design authority** — [Optimization TODO](../design/optim/TODO.md) + and its linked current contracts, proposals, and references. Its working-tree + capabilities do not change this index's committed-state milestones. + +--- + +## 4. Cross-Cutting Concerns + +| Concern | Maintained reference | +| --- | --- | +| Units, state ownership, and structural freeze | [System Overview](../design/architecture/current/system-overview.md#数据归属与生命周期), [units and dependencies](../design/architecture/current/system-overview.md#单位参数与外部依赖) | +| Cell initialization and reset | [Cell lifecycle](../design/cell/current/api.md#生命周期) | +| Network execution and event timing | [Network architecture](../design/network/current/architecture.md) | +| Trainable parameters and runtime materialization | [Trainable architecture](../design/optim/current/architecture.md) | +| Test layout and shared fixtures | [Repository testing conventions](../../AGENTS.md#testing) | +| Design documents and interface specifications | [Design conventions](../design/AGENTS.md) | + +--- + +## 5. Data-Model Summary + +The [ownership table](../design/architecture/current/system-overview.md#数据归属与生命周期) +tracks declarations, geometry, runtime states, connections, recordings, and parameter roots. +[State axes and spatial mappings](../design/architecture/current/system-overview.md#状态轴与空间映射) +explain how population, CV, point, and recording rows relate. + +--- + +## 6. Public API Contract + +Public entry points are exported by [braincell/__init__.py](../../braincell/__init__.py) +and the domain packages' explicit exports. Internal implementation paths can host public +objects re-exported from these entry points. + +Use the [module documentation map](../design/architecture/current/system-overview.md#模块与核心对象) +for signatures, units, shapes, return values, and lifecycle requirements. Cross-module naming +and export questions are compared in the [interface proposal](../design/architecture/proposals/interface-consistency.md). + +--- + +## 7. End-to-End User Workflows + +The [runnable system example](../design/architecture/current/system-overview.md#从声明到结果) +constructs a morphology and passive Cell, connects an event source, reads recordings, +plots Cell topology and traces, and performs one parameter update. + +Detailed workflows: [Cell](../design/cell/current/api.md), [Network](../design/network/current/api.md), +[Trainable](../design/optim/current/api.md), and [Vis](../design/vis/current/api.md). + +--- + +## 8. External Dependencies + +[pyproject.toml](../../pyproject.toml) defines dependency floors and extras. +JAX and SciPy remain unpinned there; the supported JAX floor and tested versions are +maintained by the [repository compatibility convention](../../AGENTS.md#working-agreement) +and [daily CI matrix](../../.github/workflows/CI-daily.yml). +Dependency roles and optional visualization backends are described in the +[system overview](../design/architecture/current/system-overview.md#单位参数与外部依赖). + +| Issue | Status | Next action | +| --- | --- | --- | +| Python version coverage | Pending | Classifiers advertise 3.11 through 3.14, while [CI](../../.github/workflows/CI.yml) and daily CI test only 3.13. Expand the matrix or align the advertised support. | + +--- + +## 9. Glossary + +CV, point, population, and recording rows are defined in +[state axes and spatial mappings](../design/architecture/current/system-overview.md#状态轴与空间映射). +Paint/place are illustrated in the [declaration example](../design/architecture/current/system-overview.md#构造-cell-和事件输入); +staggered and DHS are described in [execution paths](../design/architecture/current/system-overview.md#时间推进与求解路径). +Morphology checkpoint format details live with [IO](../design/io/TODO.md). diff --git a/docs/specs/2026-09-07-ion-learning.md b/docs/specs/2026-09-07-ion-learning.md new file mode 100644 index 00000000..915b0577 --- /dev/null +++ b/docs/specs/2026-09-07-ion-learning.md @@ -0,0 +1,127 @@ +# Ion Learning + +## Intent + +Extend the Channel signature-based parameter framework to Ion without a +scientific trainability whitelist. Preserve units, default reads and writes, +regional ownership, grouping, shared roots, constructor conversions, and the +existing parameter/scale/parameterized API. Do not expose internal constants, +train morphology or Cell.V_init, or change reaction equations. + +## Implementation + +- Share signature metadata and compact parameter allocation with Channel. + Resolve Ion's None defaults from model defaults. Keep configuration separate + from numeric storage and preserve explicit species-initializer overrides. +- Use differentiable array operations and persistent runtime parameter states + for Ion broadcasting, regional merging, synchronization, and repeated JIT. +- Keep initial parameters distinct from evolving species states. Reset reads + current trainable initial values inside the differentiated function. +- Reevaluate derived default initializers at reset, including buffer equilibria + and caiBase/caliBase fallbacks; explicit initial values remain independent. +- Preserve fixed, reset-time Nernst, and dynamic Nernst semantics. Remove stale + trainable-dependent shell volume caches without changing geometry formulas. + +## Verification + +Write failing regressions before fixes. Cover defaults, units, inherited +signatures, regional/population ownership, shared roots, dtype conversions, +repeated compiled gradients, reset, finite differences, zero gradients, and +natural errors. Regress Channel, Ion, compute, trainable, Cell, Network, and +Synapse behavior on JAX >= 0.8.0. Measure changed executable-line coverage, +targeting over 90%, and report uncovered branches honestly. + +Execute examples/multi_compartment/ion_learning.ipynb in a fresh CPU kernel. +Use independent one-CV, one-parameter fits for SodiumFixed.E, +SodiumInitNernst.temp, CalciumDetailed.Ci_initializer, CalciumDetailed.tau, +and ToyCaBindingKinetic_SU2015_DCN.kf. Fit spiking voltage for the first two +and concentration traces for the remaining three. Use short traces and at +most 100 Adam updates, compiled brainstate loops, and no external dataset. +Require finite results and final MSE at most 10% of initial MSE. Include +classification, gradient/error, and fit-results tables plus small checks for +derived defaults and explicit overrides. Record actual results and runtime. + +## Execution Results + +Implemented on the `reduction` branch without committing or pushing. + +Channel and Ion now share signature discovery, layout parameter allocation, +sources, grouping, and root ownership. Ion initialization and updates use JAX +numeric scatters with persistent runtime states. The compute-layer import +guard explicitly includes the new `ions -> parameters` edge; the graph remains +acyclic. Registered Ion metadata covers 23 classes and 310 numeric defaults. + +Regression checks cover repeatable compiled reset/gradients, partial initial +overrides, live buffer equilibria and shell factors, the caiBase/caliBase +fallbacks, row/population/CV/all grouping, split layouts, shared Channel/Ion +roots, parameterized profiles, and physical-unit validation. An additional +regression ensures initial-value scale baselines reflect pre-init regional +changes to the parameters that define those initial values. + +Numeric Ion buffers now follow configured JAX/BrainState precision, as Channel +buffers do, rather than implicitly obtaining float64 from NumPy. Legacy Ion +runtime reference tests that demand twelve decimal places explicitly select +BrainState precision 64. Training integration tests and the notebook also run +with the default float32 backend. Constructor integer conversions still apply; +learning `substeps` fails naturally at the traced integer conversion, while +regional floating-point `valence` values retain gradients. + +The pre-existing CalciumFirstOrder unit inconsistency remains outside this +change: its dimensionless alpha/beta defaults do not form a unit-consistent +concentration derivative with physical current input. It is not used as a +training example, and that error is not evidence that all Ion rates are +unlearnable. Species/reaction/conservation declarations remain intact. + +### Notebook Results + +Fresh-kernel execution on Python 3.11 / JAX 0.8.0 CPU completed nine code cells +and five fitted-trajectory figures without errors. Each example used one CV, +one scalar scale root, 800 steps of 0.025 ms, and 100 Adam updates at 0.03. +Both voltage targets and fitted traces contained a spike. All losses were +finite and all five fits exceeded the required tenfold MSE reduction. + +| Parameter | Initial | Target | Fitted | Initial MSE | Final MSE | +| --- | ---: | ---: | ---: | ---: | ---: | +| E (mV) | 45 | 50 | 49.97645 | 14.18368 | 8.10408e-5 | +| temp (K) | 300 | 309.15 | 309.10382 | 4.34521 | 3.91733e-4 | +| Ci_initializer (mM) | 0.0008 | 0.001 | 0.0009999672 | 0.00501708 | 1.32332e-10 | +| tau (ms) | 7 | 5 | 4.998580 | 0.00462772 | 2.85074e-9 | +| kf (1/(mM ms)) | 1.6 | 2 | 2.000439 | 17.27890 | 1.51110e-5 | + +Voltage MSE uses mV squared; concentration MSE uses uM squared. The observed +fit time was 23.75 seconds including compilation, not a performance guarantee. +The notebook also executes zero-gradient, invalid-unit/string, live-derived +initial-value, and independent explicit-initial-value checks. + +### Reproduction + +```bash +pytest -q --disable-warnings braincell/channel braincell/ion braincell/_compute braincell/trainable braincell/_multi_compartment braincell/_base_channel_test.py braincell/_base_ion_test.py braincell/network braincell/synapse +jupyter nbconvert --to notebook --execute --inplace --ExecutePreprocessor.timeout=600 examples/multi_compartment/ion_learning.ipynb +``` + +### Final Verification + +| Check | Result | +| --- | --- | +| Full related regression command above | 1,482 passed, 2 skipped | +| Additional dictionary-key-order regression | 1 passed | +| Changed executable-line coverage (implementation only) | 209 / 212 = 98.58% | +| Ruff lint and format checks | Passed | +| Git diff whitespace check | Passed | +| Fresh-kernel notebook, repeated | 9 executed code cells, 5 figures, no errors | + +The supplementary check verifies that equivalent species-initializer dictionaries +compare structurally through the existing Params implementation, not by their +display representation. Its coverage was combined with the full suite. + +Changed-line coverage intersects `git diff --unified=0` added/replaced line +ranges with coverage.py executable lines, excluding test modules. It is not +whole-repository coverage or a probability of correctness. The three uncovered +lines are the nonuniform scalar-configuration rejection, spatial-callback +evaluation while resolving a pre-init default, and the standalone kinetic +explicit-Ci default merge. Related regular paths have regression coverage. +Coverage data was collected under `/tmp/braincell-ion-checks.baCFcg/`; no +coverage dependency or report artifact was added to the repository. + +GPU and other JAX versions have not been executed locally. diff --git a/docs/design/network/issues.md b/docs/specs/2026-09-07-network-decisions-snapshot.md similarity index 67% rename from docs/design/network/issues.md rename to docs/specs/2026-09-07-network-decisions-snapshot.md index 37658d79..c5b95006 100644 --- a/docs/design/network/issues.md +++ b/docs/specs/2026-09-07-network-decisions-snapshot.md @@ -1,4 +1,15 @@ -# Network Design Issues +# Network 决定历史快照 + +归档日期:2026-09-07。来源:`docs/design/network/current/decisions.md` 的整理前工作区版本。 +以下保留原记录;其中的实现状态和测试数量没有在归档时重新验收。现行入口为 [Design TODO](../design/TODO.md)。 + +--- + +# Network 已实现设计决定 + +本文保留 I-01 至 I-08 的既有决定及编号。具体契约见 [API](../design/network/current/api.md) 和 [架构](../design/network/current/architecture.md), +验证记录见 [实现与验证记录](../design/network/current/implementation-status.md)。 +开放问题 I-09 至 I-11 已移至 [运行时扩展提案](../design/network/proposals/runtime-extensions.md),由 [Network TODO](../design/network/TODO.md) 跟踪。 | ID | Topic | Status | | --- | --- | --- | @@ -10,9 +21,6 @@ | I-06 | density paint CV overlap | `LOCKED` | | I-07 | recording selector 与 current reduction | `LOCKED` | | I-08 | endpoint pairing 语义与 RNG | `LOCKED` | -| I-09 | 稀疏 delay slot representation | `OPEN` | -| I-10 | 可学习 topology 与结构 mutation | `OPEN` | -| I-11 | 大规模 endpoint generator 优化 | `OPEN` | ## Locked decisions @@ -32,20 +40,3 @@ - fixed-count、one-sided degree 与 dual-stub matching 是三种独立行数语义。 - target-cell grouping 只分割 Synapse pool;候选 views 必须 unique,输出 rows 可以重复。 - Network 隐式 seed 由 Network seed 和 canonical connection path 派生;显式 rule seed 完全覆盖。 - -## Open work - -### I-09 Sparse delay slots - -当前每个 target layout 使用 dense time ring,成本与最大 delay 和 layout width 相关。后续评估只保存 -实际 event rows 的 sparse slots,并比较 JIT 静态 shape、scatter 成本和事件密度阈值。 - -### I-10 Trainable topology - -当前只允许初始化前结构编辑和初始化后 shape-preserving 参数更新。可学习连接存在性、位置或新增/ -删除 rows 会改变 JAX shapes,需要独立的 masked/padded 或重编译协议,不能复用普通参数训练接口。 - -### I-11 Scalable endpoint generators - -当前通用 pairing 会按实际候选矩阵计算 conditional score;语义已经锁定,但大 N 下仍需增加不改变 -结果的 score chunking、Bernoulli/all-to-all specialized generator,并记录 host peak-memory contract。 diff --git a/docs/design/network/implementation-plan.md b/docs/specs/2026-09-07-network-verification-snapshot.md similarity index 75% rename from docs/design/network/implementation-plan.md rename to docs/specs/2026-09-07-network-verification-snapshot.md index a33d793e..8c4a8ad8 100644 --- a/docs/design/network/implementation-plan.md +++ b/docs/specs/2026-09-07-network-verification-snapshot.md @@ -1,4 +1,15 @@ -# Network Implementation Plan +# Network 验证历史快照 + +归档日期:2026-09-07。来源:`docs/design/network/current/implementation-status.md` 的整理前工作区版本。 +以下保留原记录;其中的实现状态和测试数量没有在归档时重新验收。现行入口为 [Design TODO](../design/TODO.md)。 + +--- + +# Network 实现与验证记录 + +本文保存原 implementation-plan 中的已完成范围和既有验证记录,并非本次迁移重新执行的结果。 +原记录未注明统一测试日期或 commit;测试数量、示例路径及通过结论仅按当时口径保留,不能充当当前工作区验收。 +现行契约见 [API](../design/network/current/api.md) 和 [架构](../design/network/current/architecture.md),后续工作由 [Network TODO](../design/network/TODO.md) 管理。 ## Completed foundation @@ -39,9 +50,3 @@ - [x] NEURON comparison notebook 不引用已删除 API。 - [x] docs/examples 中无旧 public topology symbol。 - [x] profiling probability workload 使用显式 source/synapse row views 并通过最小规模测试。 - -## Deferred - -- [ ] 大 N endpoint pairing 的 chunked score evaluation 和 specialized sparse generators。 -- [ ] sparse/dense delay queue 自动选择和性能基准。 -- [ ] 初始化后结构 mutation、trainable topology 和 Network batch runtime。 diff --git a/docs/specs/2026-09-07-optim-documentation-organization.md b/docs/specs/2026-09-07-optim-documentation-organization.md new file mode 100644 index 00000000..e26bfad6 --- /dev/null +++ b/docs/specs/2026-09-07-optim-documentation-organization.md @@ -0,0 +1,60 @@ +# Optim Documentation Organization + +## Approved Change + +Reorganize all of docs/design/optim by current capability, future proposals, +experimental results, and theory/method references. Keep public API, experimental +implementation, unimplemented proposals, and unsupported targets distinct. +Status refers to the inspected working tree, not a release announcement. + +Preserve historical measurements, provenance, and limitations. Archive the old +P0 implementation plan under its first tracked date, 2026-08-29. Keep existing +dated specs as historical records rather than rewriting their frozen contracts. +Move current numerical evidence into topic-specific results pages, including the +session-only bidirectional CPU timing measurements; do not invent raw artifacts. + +Current contracts live in docs/design/optim. Check code and docs/examples for +consistency; link the existing root examples requested for this change without +bulk-migrating them. This change modifies only documentation and necessary links, +not Python behavior, notebook outputs, or experiment results. No experiments are +rerun and no commit or push is requested. + +## Acceptance + +- One overview exposes current support, experimental workflows, future work, + result records, and references without contradictory implementation status. +- Channel, Ion, Synapse, Connection/detector, and Network support are documented + separately, including initialization, valid zero gradients, natural errors, + explicit exclusions, tests, and runnable example locations. +- Plasticity remains a proposal, without inventing an implemented public API. +- Moved result tables retain all values and their local conditions. Timing, + compilation, memory estimates, process peaks, and historical snapshots remain + distinguishable. +- Local links and anchors resolve; superseded current-document paths have no + remaining live references. Historical specs may retain historical paths. +- Pre-existing unrelated workspace changes are preserved. + +## Outcome + +The current design tree now has one overview, parameter support, public API, +architecture, experimental workflow and roadmap entry, plus two proposals, +five result pages and six theory/method references. The old P0 plan was archived; +superseded current-document bodies were moved rather than duplicated. + +Static inspection found and corrected stale public-export/owner descriptions, +Synapse dictionary claims, the old threshold-equality formula, the assumption +that voltage-only loss never traverses event feedback, and experimental README +wording that prematurely reserved a future public optim package. + +All 163 numeric table rows from the five migrated historical result/protocol +sources were retained. Protected measured paragraphs were compared with the +pre-edit working-tree text, allowing only the removed section heading. Local +Markdown link and heading-anchor checks passed across the 19 current optim +documents, the archived plan, and three affected example navigation pages. +git diff whitespace checks passed. These are documentation checks, not new +simulation, training, performance, coverage, or external-link validation runs. + +All nine docs/examples notebooks were inspected structurally for parameter +learning entrypoints; no matching tutorial was found. Existing root learning +notebooks and code were not modified. The maintained tutorial gap is recorded +in the current roadmap, not claimed to be resolved by navigation alone. diff --git a/docs/specs/2026-09-07-synapse-network-learning.md b/docs/specs/2026-09-07-synapse-network-learning.md new file mode 100644 index 00000000..be0499b7 --- /dev/null +++ b/docs/specs/2026-09-07-synapse-network-learning.md @@ -0,0 +1,101 @@ +# Synapse and Network Learning + +## Intent + +Extend the existing Channel/Ion parameter sources to signature-declared +Synapse parameters, Connection weight, and event-detector threshold. Aggregate +all supported parameters in Network without copying optimizer roots. Delay, +plasticity rules, plastic weight initializers, and new RTRL algorithms remain +outside this change. Preserve unrelated workspace edits. + +## Implementation + +- Replace handwritten Synapse parameter schemas with explicit constructors and + generated metadata. Preserve default get/set, units, broadcasting, model + validation, states, and event input contracts. +- Reuse parameter/scale/parameterized sources, grouping, transforms, root + sharing, atomic registration, and persistent differentiable runtime values. + Identify synapses, contacts, and detector endpoints by their logical IDs, + not merely by their CV. Reject trainable delay explicitly. +- Unify rising detection as last < threshold <= next and falling detection as + last > threshold >= next. Use the Cell surrogate by default, allow detector + overrides, and retain floating event payloads through delivery. Document + the different NEURON APCount and fixed-step NetCon equality boundaries. +- Expose Network.trainables, prepare_run(), and update(); share the existing + simulation ordering with run(), keeping host result formatting outside AD. + Reset dynamic state and queues without resetting trainable roots. +- First validate a single autaptic Cell, then two-Cell Network execution. + Compare existing full-state RTRL against BPTT for voltage and spike losses, + including queue sensitivities. Parameters remain fixed within a rollout; + no independent-Cell sensitivity truncation or stepwise optimizer is added. + +## Verification + +Write regression tests before fixes. Cover equality boundaries, directions, +surrogates, signatures, units, selection, colocated synapses, shared roots, +repeated compiled execution, reset, errors, zero gradients, fixed delays, +both delivery backends, and existing Channel/Ion/Network behavior. Use finite +differences only for ordinary differentiable paths; compare JVP/VJP and +RTRL/BPTT for surrogate paths. Target meaningful changed-line coverage >90%. + +Create examples/multi_compartment/synapse_learning.ipynb with classification, +gradient/error, and fit tables. Demonstrate short single-sample tau, weight, +and threshold fits, and autaptic voltage/spike losses. Tau and weight fits +must reduce MSE tenfold; threshold must improve loss without claiming unique +parameter recovery. Keep executable RTRL checks in the existing experimental +gradient-correctness area. Record actual results and unavailable environments. + +## Results + +Implemented on branch `reduction`. The pre-existing edits to channel_learning.ipynb +and ion_learning.ipynb were preserved, not rewritten by this change. + +- JAX 0.8.0 / Python 3.11 / CPU: the related Channel, Ion, runtime, trainable, + multi-compartment, Network and Synapse regression run passed 1509 tests with + 2 skips. Additional focused tests cover Exp2Syn finite differences and qualified + root-name collisions. +- Focused coverage run: 381 passed, 2 skipped. Changed executable production + lines, including new production modules: 364/378 covered (96.3%). This is + changed-line coverage, not whole-repository coverage or a correctness proof. +- Both JAX 0.8.0 and 0.10.1 passed autaptic voltage/spike BPTT versus full-RTRL + checks, zero and 0.1 ms delays, and two-Cell sensitivity/carry-shape checks. + The notebook's float64 comparisons had maximum absolute gradient discrepancy + at most 5.24e-10; spike-loss discrepancies were at most 3.33e-15. +- JAX 0.10.1 focused Synapse, Network, point-target and base-class regressions: + 173 passed, 2 skipped (plus 8 passing subtests). +- The notebook executed end to end on JAX 0.8.0. Its tau MSE went from + 0.0564968 to 2.48711e-7, weight from 3.11666 to 9.70201e-7, and threshold from + 2.5993e-5 to zero on the discrete event grid. Each target had one spike. + All three fit acceptance checks also passed on JAX 0.10.1. +- The JAX 0.10.1 combined run exposed existing Ion float32/float64 test-order + failures. Running `ion/_base_test.py`, `_compute/ions_test.py`, then the Ion + cases in `trainable/_manager_test.py` reproduced five such failures in an + unmodified HEAD archive. The manager's 17 Ion cases passed in a fresh isolated + process. The unmodified HEAD full related run also passed 1488 tests with + 2 skips: this is execution-context-sensitive, not an unconditional failure. + The failing modified combined run was coverage-instrumented; instrumenting + alone has not been established as the cause. This pre-existing precision/cache + issue was not changed here, and the full modified JAX 0.10.1 suite is not + claimed to be uniformly green. +- Ruff and `git diff --check` passed. GPU execution was not tested. + +Regression tests prevent partial physical writes after a later unit error, +premature validation of inherited constructor fields, stale compiled weights, +loss of threshold ownership, duplicate equality events, and loss of unit-based +inspection on runtime parameter buffers. Zero gradients remain valid outcomes. + +### Reproduction + +```bash +python -m pytest -q braincell/channel braincell/ion braincell/_compute \ + braincell/trainable braincell/_multi_compartment braincell/network braincell/synapse \ + braincell/_base_channel_test.py braincell/_base_ion_test.py braincell/_base_neuron_test.py +python -m pytest -q examples/experimental/optim_gradient_correctness/autapse_test.py +python -m pytest -q examples/multi_compartment/synapse_learning_test.py +python -m nbconvert --to notebook --execute --inplace \ + --ExecutePreprocessor.timeout=600 examples/multi_compartment/synapse_learning.ipynb +``` + +Choose the intended Python environment explicitly when comparing JAX versions. +Test simultaneous dtype-switching suites in a separate process when investigating +the known JAX 0.10.1 Ion failure; do not reinterpret it as a synapse gradient error. diff --git a/examples/experimental/README.md b/examples/experimental/README.md index 5dc8ad5c..faff957c 100644 --- a/examples/experimental/README.md +++ b/examples/experimental/README.md @@ -6,7 +6,7 @@ apply it. | Directory | Responsibility | | --- | --- | -| [`optim/`](optim/) | Model-independent BPTT/RTRL gradient interfaces being evaluated for `braincell.optim` | +| [`optim/`](optim/) | Experimental BPTT/RTRL gradient interfaces; no public package commitment | | [`optim_gradient_correctness/`](optim_gradient_correctness/) | One-CV and multicompartment numerical correctness | | [`optim_gradient_scaling/`](optim_gradient_scaling/) | State, parameter, time, batch, and seed scaling | | [`optim_training_comparison/`](optim_training_comparison/) | Matched end-to-end BPTT/RTRL training | @@ -19,6 +19,9 @@ The dependency direction is strict: experiment directories may import Parameter selection, sharing, transforms, and materialization remain in the public `braincell.trainable` package. +Current capabilities, proposals, and measured evidence are separated in the +[optimization design overview](../../docs/design/optim/TODO.md). + Generated data, traces, figures, and reports are stored in each experiment's ignored `artifacts/` directory. diff --git a/examples/experimental/optim/README.md b/examples/experimental/optim/README.md index e2601d80..c41ac487 100644 --- a/examples/experimental/optim/README.md +++ b/examples/experimental/optim/README.md @@ -1,7 +1,9 @@ # Experimental Optimization Core -This package contains the model-independent optimization interfaces currently -being evaluated for a future `braincell.optim` package. +This package contains experimental model-independent gradient interfaces. +It does not reserve or promise a future public `braincell.optim` package. +Current usage and boundaries live in +[Experimental Workflows](../../../docs/design/optim/current/experimental-workflows.md). - `gradients.py` exposes fixed-shape rollout and trajectory gradient engines with `method="bptt"` and `method="rtrl"`. diff --git a/examples/experimental/optim_gradient_correctness/README.md b/examples/experimental/optim_gradient_correctness/README.md index 85531e9a..cca53712 100644 --- a/examples/experimental/optim_gradient_correctness/README.md +++ b/examples/experimental/optim_gradient_correctness/README.md @@ -8,6 +8,13 @@ mixing those checks with performance conclusions. differences on a branched multicompartment HH cell. - `gradient_diagnostics.ipynb` inspects sensitivity, learning-signal, direct, and eligibility-gradient decompositions. +- `autapse.py` compares full-state RTRL and BPTT through single-Cell feedback + and two-Cell event delivery. Scope and results: + [Synapse and Network Learning Results](../../../docs/design/optim/current/results/synapse-network-learning.md). +- `bidirectional.py` checks independent populations of sizes two and three, + trainable parameters on both sides, twelve bidirectional contacts, and fits + with both gradient methods. Current results: + [Bidirectional Population Learning](../../../docs/design/optim/current/results/synapse-network-learning.md#双向-population). ```bash pytest -q examples/experimental/optim_gradient_correctness diff --git a/examples/experimental/optim_gradient_correctness/autapse.py b/examples/experimental/optim_gradient_correctness/autapse.py new file mode 100644 index 00000000..9351885c --- /dev/null +++ b/examples/experimental/optim_gradient_correctness/autapse.py @@ -0,0 +1,164 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Full-state BPTT/RTRL checks for a one-CV Hodgkin-Huxley autapse.""" + +import braincell +import brainstate +import brainunit as u +import jax.numpy as jnp +import numpy as np + +from braincell.filter import AllRegion, RootLocation +from examples.experimental.optim.gradients import build_rollout_value_and_grad + +DT = 0.025 * u.ms + + +def build_autapse(*, delay=0.1 * u.ms, network=False, paired=False): + """Build an initialized spiking Cell with a trainable self-connection. + + Parameters + ---------- + delay : brainunit.Quantity, optional + Static self-connection delay. + network : bool, optional + Wrap the same Cell in the public Network execution path. + paired : bool, optional + Add a second postsynaptic Cell and observe it instead. + + Returns + ------- + tuple + Execution target and underlying Cell. + """ + soma = braincell.Branch.from_lengths(lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type="soma") + cell = braincell.Cell( + braincell.Morphology.from_root(soma, name="soma"), pop_size=(1,), V_init=-65.0 * u.mV, V_th=-20.0 * u.mV + ) + cell.paint( + AllRegion(), + braincell.mech.CableProperty( + resting_potential=-65.0 * u.mV, + membrane_capacitance=1.0 * u.uF / u.cm**2, + axial_resistivity=100.0 * u.ohm * u.cm, + ), + braincell.mech.Ion("SodiumFixed", name="sodium", E=50.0 * u.mV), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Channel("IL", name="leak"), + braincell.mech.Channel("Na_HH1952", name="na"), + braincell.mech.Channel("K_HH1952", name="k"), + ) + cell.place( + RootLocation(0.5), braincell.mech.CurrentClamp(delay=0.25 * u.ms, durations=1.0 * u.ms, amplitudes=0.1 * u.nA) + ) + cell.place(RootLocation(0.5), braincell.mech.Synapse("ExpSyn", name="syn", tau=2.0 * u.ms)) + connection = braincell.connect( + "self", source=cell.event_outputs["spike"], synapse=cell.synapses["syn"], weight=0.001 * u.uS, delay=delay + ) + cell.synapses["syn"].trainable(tau=braincell.trainable.scale(name="tau")) + connection.trainable(weight=braincell.trainable.scale(name="weight")) + cell.event_outputs["spike"].trainable(threshold=braincell.trainable.parameter(group_by="all", name="threshold")) + cell.channels["na"].trainable(g_max=braincell.trainable.scale(name="na")) + cell.ions["sodium"].trainable(E=braincell.trainable.scale(name="sodium")) + if network: + target = braincell.Network("autapse") + target.add_population("cell", cell) + if paired: + from examples.multi_compartment.synapse_learning import build_cell + + post = build_cell() + braincell.connect( + "forward", + source=cell.event_outputs["spike"], + synapse=post.synapses["syn"], + weight=0.001 * u.uS, + delay=delay, + ) + target.add_population("post", post) + target.prepare_run(dt=DT, event_backend="scatter") + if paired: + cell = post + else: + cell.init_state() + cell.connections.prepare_runtime(DT) + target = cell + return target, cell + + +def compare(*, loss_kind="voltage", delay=0.1 * u.ms, network=False, paired=False, steps=160): + """Compare full-state derivatives using identical surrogate rules. + + Parameters + ---------- + loss_kind : str, optional + ``voltage`` or ``spike`` additive loss. + delay : brainunit.Quantity, optional + Fixed event delay. + network : bool, optional + Exercise Network.update instead of standalone Cell.run. + paired : bool, optional + Compare gradients from a second Cell back to the presynaptic Cell. + steps : int, optional + Number of time steps. + + Returns + ------- + dict + Loss, parameter gradients, and maximum absolute gradient difference. + """ + results = {} + for method in ("bptt", "rtrl"): + target, cell = build_autapse(delay=delay, network=network, paired=paired) + + def step(_): + if network: + target.update() + else: + cell.run(dt=DT, duration=DT) + value = cell.V.value.to_decimal(u.mV) + 60.0 if loss_kind == "voltage" else cell.spike.value - 1.0 + return jnp.mean(value**2) + + engine = build_rollout_value_and_grad(target, step=step, method=method) + inputs = jnp.arange(steps) + engine.prepare(inputs[0]) + if method == "rtrl": + import jax + + roots = tuple(state.value for state in engine.parameter_states.values()) + values, tangents = engine._initial_full_carry(roots) + state_shapes = tuple(np.shape(leaf) for leaf in jax.tree.leaves(values)) + sensitivity_shapes = tuple(np.shape(leaf) for leaf in jax.tree.leaves(tangents)) + results[method] = brainstate.transform.jit(engine)(inputs) + lhs, rhs = results["bptt"], results["rtrl"] + np.testing.assert_allclose(lhs.losses, rhs.losses, rtol=1e-10, atol=1e-10) + errors = [] + for name in lhs.gradients: + a, b = u.get_mantissa(lhs.gradients[name]), u.get_mantissa(rhs.gradients[name]) + np.testing.assert_allclose(a, b, rtol=1e-8, atol=1e-9) + errors.append(float(np.max(np.abs(np.asarray(a) - np.asarray(b))))) + return { + "loss": float(lhs.loss), + "max_abs_error": max(errors), + "state_shapes": state_shapes, + "sensitivity_shapes": sensitivity_shapes, + "gradients": {k: float(u.get_mantissa(v)) for k, v in lhs.gradients.items()}, + } + + +if __name__ == "__main__": + with brainstate.environ.context(dt=DT, precision=64): + for mode in ("voltage", "spike"): + print(mode, compare(loss_kind=mode)) diff --git a/examples/experimental/optim_gradient_correctness/autapse_test.py b/examples/experimental/optim_gradient_correctness/autapse_test.py new file mode 100644 index 00000000..cabf1e0c --- /dev/null +++ b/examples/experimental/optim_gradient_correctness/autapse_test.py @@ -0,0 +1,50 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Autaptic feedback must retain the full forward sensitivity.""" + +import unittest +import brainstate +import brainunit as u + +from examples.experimental.optim_gradient_correctness.autapse import DT, compare + + +class AutapseTest(unittest.TestCase): + def test_voltage_and_spike_losses_with_event_feedback(self): + with brainstate.environ.context(dt=DT, precision=64): + for kind in ("voltage", "spike"): + for delay in (0.0 * u.ms, 0.1 * u.ms): + with self.subTest(kind=kind, delay=delay): + result = compare(loss_kind=kind, delay=delay) + self.assertGreater(result["loss"], 0.0) + if kind == "voltage": + self.assertNotEqual(result["gradients"]["weight"], 0.0) + else: + self.assertNotEqual(result["gradients"]["threshold"], 0.0) + + def test_network_autapse_uses_the_same_full_state_recurrence(self): + with brainstate.environ.context(dt=DT, precision=64): + result = compare(network=True) + self.assertNotEqual(result["gradients"]["cell.weight"], 0.0) + + def test_cross_cell_sensitivity_and_fixed_size_carry(self): + with brainstate.environ.context(dt=DT, precision=64): + prefix = compare(network=True, paired=True, steps=100) + full = compare(network=True, paired=True, steps=160) + self.assertNotEqual(full["gradients"]["cell.na"], 0.0) + self.assertNotEqual(full["gradients"]["cell.threshold"], 0.0) + self.assertEqual(prefix["state_shapes"], full["state_shapes"]) + self.assertEqual(prefix["sensitivity_shapes"], full["sensitivity_shapes"]) diff --git a/examples/experimental/optim_gradient_correctness/bidirectional.py b/examples/experimental/optim_gradient_correctness/bidirectional.py new file mode 100644 index 00000000..ccd936be --- /dev/null +++ b/examples/experimental/optim_gradient_correctness/bidirectional.py @@ -0,0 +1,336 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Full-state gradient and fitting probes for two bidirectionally coupled populations.""" + +from dataclasses import dataclass + +import braincell +import brainstate +import braintools +import brainunit as u +import jax +import jax.numpy as jnp +import numpy as np + +from braincell.filter import AllRegion, RootLocation +from braincell.network.event import VoltageCrossingSource +from examples.experimental.optim.gradients import build_rollout_value_and_grad + +DT = 0.025 * u.ms +STEPS = 800 +RTOL = 1e-7 +ATOL = 1e-8 + + +class _ProbeSource(VoltageCrossingSource): + def __init__(self, cell, *, detached): + super().__init__(cell, name="probe") + self.detached = detached + + def current_event_count(self, source_index): + event = super().current_event_count(source_index) + return jax.lax.stop_gradient(event) if self.detached else event + + +def _shift(base, group, name): + return braincell.trainable.parameterized( + lambda ctx, delta: base + delta * u.mV, + delta=braincell.trainable.parameter(0.0, group_by=group, name=name), + ) + + +def _scale(group, name, shared=None): + if shared is not None: + return braincell.trainable.scale(shared, group_by="all", name=name) + return braincell.trainable.scale(group_by=group, name=name, transform=brainstate.nn.TanhT(0.5, 1.5)) + + +def _population(size, *, label, grouped, shared, detached): + soma = braincell.Branch.from_lengths(lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type="soma") + cell = braincell.Cell( + braincell.Morphology.from_root(soma, name="soma"), pop_size=(size,), V_init=-65.0 * u.mV, V_th=-20.0 * u.mV + ) + cell.paint( + AllRegion(), + braincell.mech.CableProperty( + resting_potential=-65.0 * u.mV, + membrane_capacitance=1.0 * u.uF / u.cm**2, + axial_resistivity=100.0 * u.ohm * u.cm, + ), + braincell.mech.Ion("SodiumFixed", name="sodium", E=50.0 * u.mV), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Channel("IL", name="leak"), + braincell.mech.Channel("Na_HH1952", name="na"), + braincell.mech.Channel("K_HH1952", name="k"), + ) + for member in range(size): + cell[member].channels["na"].set(g_max=(108.0 + 4.0 * member + (3.0 if label == "B" else 0.0)) * u.mS / u.cm**2) + offset = 0.25 if label == "A" else 2.25 + for pulse in (0.0, 10.0): + cell.place( + RootLocation(0.5), + braincell.mech.CurrentClamp( + delay=(offset + pulse) * u.ms, durations=1.0 * u.ms, amplitudes=(0.1 if pulse == 0.0 else 0.4) * u.nA + ), + ) + kind = "ExpSyn" if label == "A" else "Exp2Syn" + kinetics = {"tau": 2.0 * u.ms} if label == "A" else {"tau1": 0.2 * u.ms, "tau2": 3.0 * u.ms} + reversal = 0.0 * u.mV if label == "A" else -5.0 * u.mV + cell.place(RootLocation(0.5), braincell.mech.Synapse(kind, name="syn", e=reversal, **kinetics)) + group = "all" if grouped else "population" + cell.channels["na"].trainable(g_max=_scale(group, "gmax", shared), V_sh=_shift(-45.0 * u.mV, group, "shift")) + cell.ions["sodium"].trainable(E=_scale(group, "ion")) + cell.synapses["syn"].trainable( + **{key: _scale(group, key) for key in kinetics}, e=_shift(reversal, group, "reversal") + ) + source = _ProbeSource(cell, detached=detached) + source.trainable(threshold=_shift(-20.0 * u.mV, group, "threshold")) + return cell, source + + +@dataclass +class Experiment: + """Hold the network and explicit observation/parameter ownership maps.""" + + network: object + cells: dict + connections: dict + nodes: dict + + def observation(self): + """Read post-step voltages, spikes and receiving synapse conductances.""" + return { + "voltage": jnp.concatenate([self.cells[name].V.value.to_decimal(u.mV)[:, 0] for name in ("A", "B")]), + "spike": jnp.concatenate([self.cells[name].spike.value[:, 0] for name in ("A", "B")]), + "conductance": jnp.concatenate( + [u.get_mantissa(brainstate.maybe_state(self.nodes[name].g)) for name in ("A", "B")] + ), + } + + def rollout(self, steps=STEPS): + """Reset and collect a compiled-loop trajectory without updating roots.""" + self.network.reset_state() + + def step(_): + self.network.update() + return self.observation() + + return brainstate.transform.for_loop(step, jnp.arange(steps)) + + +def build(*, grouped=False, delay="heterogeneous", backend="scatter", shared=False, detached=False): + """Build and prepare a two-by-three bidirectional population experiment. + + Parameters + ---------- + grouped : bool, optional + Share each field inside its population and each direction's weights. + delay : str, optional + ``zero``, ``positive``, or ``heterogeneous`` fixed delays. + backend : str, optional + ``scatter`` or ``brainevent``. + shared : bool, optional + Tie the two populations' conductance factors to one original root. + detached : bool, optional + Stop only event derivatives, leaving forward events unchanged. + + Returns + ------- + Experiment + Initialized network and its explicit ownership maps. + """ + shared_root = brainstate.nn.Param(1.0, t=brainstate.nn.TanhT(0.5, 1.5)) if shared else None + cells, sources = {}, {} + net = braincell.Network("bidirectional") + for label, size in (("A", 2), ("B", 3)): + cells[label], sources[label] = _population( + size, label=label, grouped=grouped, shared=shared_root, detached=detached + ) + net.add_population(label, cells[label]) + delay_values = { + "zero": np.zeros(6), + "positive": np.full(6, 0.1), + "heterogeneous": np.array([0.0, 0.025, 0.1, 0.05, 0.1, 0.025]), + } + connections = {} + for pre, post in (("A", "B"), ("B", "A")): + n_pre, n_post = len(sources[pre]), len(sources[post]) + pre_ids = np.repeat(np.arange(n_pre), n_post) + post_ids = np.tile(np.arange(n_post), n_pre) + name = f"{pre}_to_{post}" + connection = net.connect( + name, + source=sources[pre][pre_ids], + synapse=cells[post].synapses["syn"][post_ids], + weight=np.linspace(0.0003, 0.0005, 6) * u.uS, + delay=delay_values[delay] * u.ms, + ) + connection.trainable(weight=_scale("all" if grouped else "row", "weight")) + connections[name] = connection + net.prepare_run(dt=DT, event_backend=backend) + nodes = { + label: cell.runtime.get_runtime_node( + cell.synapses["syn"]._store.layout_id("ExpSyn" if label == "A" else "Exp2Syn") + ) + for label, cell in cells.items() + } + return Experiment(net, cells, connections, nodes) + + +def engine_for(experiment, method, *, loss_kind="joint"): + """Create the existing gradient engine for an additive voltage/spike loss. + + Parameters + ---------- + experiment : Experiment + Prepared model. + method : str + ``bptt`` or ``rtrl``. + loss_kind : str, optional + ``A``, ``B``, ``joint``, or ``spike``. Inputs are per-step targets. + + Returns + ------- + RolloutGradientEngine + Untraced gradient engine. + """ + selection = {"A": slice(0, 2), "B": slice(2, 5), "joint": slice(None), "spike": slice(None)}[loss_kind] + + def step(target): + experiment.network.update() + key = "spike" if loss_kind == "spike" else "voltage" + observed = experiment.observation()[key] + return jnp.mean((observed[selection] - target[selection]) ** 2) + + return build_rollout_value_and_grad(experiment.network, step=step, method=method) + + +def gradient_comparison(*, loss_kind="joint", steps=STEPS, **configuration): + """Compare every root coordinate using full RTRL and BPTT. + + Parameters + ---------- + loss_kind : str, optional + Loss selection accepted by engine_for. + steps : int, optional + Rollout length. + **configuration + Keyword arguments forwarded to build. + + Returns + ------- + dict + Per-root gradients, maximum absolute error, loss and carry shapes. + """ + values = {} + target = jnp.full((steps, 5), 1.0 if loss_kind == "spike" else -60.0) + for method in ("bptt", "rtrl"): + experiment = build(**configuration) + engine = engine_for(experiment, method, loss_kind=loss_kind) + engine.prepare(target[0]) + if method == "rtrl": + roots = tuple(state.value for state in engine.parameter_states.values()) + state, sensitivity = engine._initial_full_carry(roots) + shapes = ( + tuple(np.shape(x) for x in jax.tree.leaves(state)), + tuple(np.shape(x) for x in jax.tree.leaves(sensitivity)), + ) + values[method] = brainstate.transform.jit(engine)(target) + left, right = values["bptt"], values["rtrl"] + np.testing.assert_allclose(left.losses, right.losses, rtol=1e-10, atol=1e-10) + errors = {} + gradients = {} + for name, value in left.gradients.items(): + a, b = np.asarray(value), np.asarray(right.gradients[name]) + np.testing.assert_allclose(a, b, rtol=RTOL, atol=ATOL, err_msg=name) + errors[name] = float(np.max(np.abs(a - b))) + gradients[name] = a + return dict( + loss=float(left.loss), + gradients=gradients, + errors=errors, + max_abs_error=max(errors.values()), + carry_shapes=shapes, + ) + + +def fit(*, method="bptt", epochs=200): + """Fit both populations and both connections to one spiking trajectory. + + Parameters + ---------- + method : str, optional + BPTT or full-state RTRL. + epochs : int, optional + Maximum number of Adam updates. + + Returns + ------- + dict + Loss history, trajectories and original/fitted physical root values. + """ + reference = build(grouped=True) + target = brainstate.transform.jit(reference.rollout)()["voltage"] + experiment = build(grouped=True) + parameters = experiment.network.trainables.parameters() + initial_roots = parameters.physical_values() + initial_roots = { + name: value + + (0.25 if name.endswith(("shift", "reversal", "threshold")) else -0.02 if name.startswith("A.") else 0.02) + for name, value in initial_roots.items() + } + parameters.set_physical_values(initial_roots) + predict = brainstate.transform.jit(experiment.rollout) + initial = predict()["voltage"] + engine = engine_for(experiment, method) + engine.prepare(target[0]) + optimizer = braintools.optim.Adam(lr=0.01) + optimizer.register_trainable_weights(parameters.states()) + + @brainstate.transform.jit + def optimize(): + def epoch(_): + result = engine(target) + optimizer.update(jax.tree.map(lambda value: value / STEPS, result.gradients)) + return result.loss / STEPS + + return brainstate.transform.for_loop(epoch, jnp.arange(epochs)) + + history = np.asarray(optimize()) + fitted = predict()["voltage"] + initial_mse = float(jnp.mean((initial - target) ** 2)) + final_mse = float(jnp.mean((fitted - target) ** 2)) + return dict( + method=method, + initial_mse=initial_mse, + final_mse=final_mse, + history=history, + target=np.asarray(target), + initial=np.asarray(initial), + fitted=np.asarray(fitted), + initial_roots=initial_roots, + fitted_roots=parameters.physical_values(), + ) + + +if __name__ == "__main__": + with brainstate.environ.context(dt=DT, precision=64): + experiment = build() + trajectory = brainstate.transform.jit(experiment.rollout)() + print("spikes", np.asarray(trajectory["spike"]).sum(axis=0), flush=True) + result = gradient_comparison() + print("gradient error", result["max_abs_error"], flush=True) + print("gradients", result["gradients"], flush=True) diff --git a/examples/experimental/optim_gradient_correctness/bidirectional_test.py b/examples/experimental/optim_gradient_correctness/bidirectional_test.py new file mode 100644 index 00000000..a21b05f8 --- /dev/null +++ b/examples/experimental/optim_gradient_correctness/bidirectional_test.py @@ -0,0 +1,174 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Coupled population gradients must include both directions and all roots.""" + +import unittest + +import brainstate +import jax +import jax.numpy as jnp +import numpy as np + +from examples.experimental.optim_gradient_correctness.bidirectional import ( + ATOL, + DT, + RTOL, + STEPS, + build, + engine_for, + fit, + gradient_comparison, +) + + +class BidirectionalTest(unittest.TestCase): + def setUp(self): + self.enterContext(brainstate.environ.context(dt=DT, precision=64)) + + def test_activity_and_independent_population_contact_coordinates(self): + experiment = build() + values = experiment.network.trainables.parameters().physical_values() + for population, size in (("A", 2), ("B", 3)): + for field in ("gmax", "shift", "ion", "reversal", "threshold"): + self.assertEqual(values[f"{population}.{field}"].shape, (size,)) + self.assertEqual(values[f"{population}.weight"].shape, (6,)) + activity = brainstate.transform.jit(experiment.rollout)() + spikes = np.asarray(activity["spike"]) + conductance = np.asarray(activity["conductance"]) + self.assertTrue(np.all(spikes.sum(axis=0) >= 2), spikes.sum(axis=0)) + for member in range(5): + arrivals = np.flatnonzero(conductance[:, member] > 0.0) + self.assertGreater(len(arrivals), 0) + self.assertGreater(spikes[arrivals[0] + 1 :, member].sum(), 0.0) + + def test_all_parameter_gradients_across_losses_delays_and_backends(self): + for loss, delay, backend in ( + ("joint", "heterogeneous", "scatter"), + ("A", "positive", "scatter"), + ("B", "heterogeneous", "brainevent"), + ("spike", "zero", "brainevent"), + ): + with self.subTest(loss=loss, delay=delay, backend=backend): + result = gradient_comparison(loss_kind=loss, delay=delay, backend=backend) + self.assertTrue(all(np.isfinite(g).all() for g in result["gradients"].values())) + if loss in ("A", "B"): + upstream = "B" if loss == "A" else "A" + for field in ("gmax", "threshold"): + self.assertGreater(np.linalg.norm(result["gradients"][f"{upstream}.{field}"]), 1e-10) + + def test_detached_events_preserve_forward_but_remove_cross_population_gradients(self): + traces = [] + gradients = [] + for detached in (False, True): + experiment = build(detached=detached) + traces.append(brainstate.transform.jit(experiment.rollout)()) + losses = {} + for loss in ("A", "B"): + engine = engine_for(experiment, "bptt", loss_kind=loss) + target = jnp.full((STEPS, 5), -60.0) + engine.prepare(target[0]) + losses[loss] = brainstate.transform.jit(engine)(target).gradients + gradients.append(losses) + for key in traces[0]: + np.testing.assert_array_equal(traces[0][key], traces[1][key]) + for loss, upstream in (("A", "B"), ("B", "A")): + for field in ("gmax", "threshold"): + name = f"{upstream}.{field}" + self.assertGreater(np.linalg.norm(gradients[0][loss][name]), 1e-10) + np.testing.assert_array_equal(gradients[1][loss][name], 0.0) + + def test_shared_root_gradient_is_sum_of_independent_roots(self): + independent = gradient_comparison(grouped=True) + shared = gradient_comparison(grouped=True, shared=True) + self.assertNotIn("B.gmax", shared["gradients"]) + np.testing.assert_allclose( + shared["gradients"]["A.gmax"], + independent["gradients"]["A.gmax"] + independent["gradients"]["B.gmax"], + rtol=RTOL, + atol=ATOL, + ) + + def test_repeated_compiled_reset_and_root_updates(self): + experiment = build(grouped=True) + target = jnp.full((160, 5), -60.0) + engine = engine_for(experiment, "rtrl") + engine.prepare(target[0]) + run = brainstate.transform.jit(engine) + first, repeated = run(target), run(target) + for a, b in zip(jax.tree.leaves(first), jax.tree.leaves(repeated)): + np.testing.assert_array_equal(a, b) + parameters = experiment.network.trainables.parameters() + original = parameters.physical_values() + changed = dict(original) + changed["A.weight"] = changed["A.weight"] * 1.1 + changed["B.tau2"] = changed["B.tau2"] * 0.9 + parameters.set_physical_values(changed) + self.assertNotEqual(float(run(target).loss), float(first.loss)) + parameters.set_physical_values(original) + restored = run(target) + for a, b in zip(jax.tree.leaves(first), jax.tree.leaves(restored)): + np.testing.assert_array_equal(a, b) + + def test_prefix_gradients_and_queue_cross_population_sensitivities(self): + experiment = build(grouped=True) + trajectory = brainstate.transform.jit(experiment.rollout)() + spikes = np.asarray(trajectory["spike"]) + first_a = int(np.flatnonzero(spikes[:, :2].sum(axis=1))[0]) + first_b = int(np.flatnonzero(spikes[:, 2:].sum(axis=1))[0]) + at = tuple(sorted({0, first_a, first_a + 8, first_b, first_b + 8, STEPS - 1})) + target = jnp.full((STEPS, 5), -60.0) + engine = engine_for(experiment, "rtrl") + engine.prepare(target[0]) + diagnostic = brainstate.transform.jit(lambda: engine.diagnose(target, at=at))() + coordinates = engine._parameter_coordinates + reference = build(grouped=True) + bptt = engine_for(reference, "bptt") + bptt.prepare(target[0]) + for sample, index in enumerate(at): + result = brainstate.transform.jit(bptt)(target[: index + 1]) + np.testing.assert_allclose( + diagnostic.prefix_gradients[sample], + coordinates.flatten({name: result.gradients[name] for name in coordinates.names}), + rtol=RTOL, + atol=ATOL, + ) + states = {id(state): i for i, state in enumerate(engine._functional_step.state_trace.states)} + for receiver, sender in (("A", "B"), ("B", "A")): + state_index = states[id(experiment.cells[receiver].V)] + directions = coordinates.slices[coordinates.names.index(f"{sender}.gmax")] + sensitivity = np.asarray(brainstate.maybe_state(diagnostic.sensitivity[state_index]).mantissa) + np.testing.assert_array_equal(sensitivity[0, directions], 0.0) + self.assertGreater(np.linalg.norm(sensitivity[1:, directions]), 1e-10) + delivery = experiment.network._prepared_run[1].delivery_state + for queue in delivery.ring_buffers: + sensitivity = diagnostic.sensitivity[states[id(queue)]] + self.assertGreater(np.linalg.norm(np.asarray(sensitivity.mantissa)), 1e-10) + roots = tuple(state.value for state in engine.parameter_states.values()) + shapes = tuple(np.shape(x) for x in jax.tree.leaves(engine._initial_full_carry(roots))) + brainstate.transform.jit(engine)(target[:100]) + short_shapes = tuple(np.shape(x) for x in jax.tree.leaves(engine._initial_full_carry(roots))) + self.assertEqual(shapes, short_shapes) + + def test_both_gradient_methods_train_both_populations_and_connections(self): + for method in ("bptt", "rtrl"): + with self.subTest(method=method): + result = fit(method=method) + self.assertTrue(np.isfinite(result["history"]).all()) + self.assertLess(result["final_mse"], result["initial_mse"] * 0.1) + for population in ("A", "B"): + for field in ("gmax", "ion", "reversal", "weight"): + name = f"{population}.{field}" + self.assertNotEqual(float(result["initial_roots"][name]), float(result["fitted_roots"][name])) diff --git a/examples/experimental/optim_parameter_fitting/README.md b/examples/experimental/optim_parameter_fitting/README.md index 8f9e3fa3..44ea7e58 100644 --- a/examples/experimental/optim_parameter_fitting/README.md +++ b/examples/experimental/optim_parameter_fitting/README.md @@ -463,7 +463,7 @@ spike basin;Nesterov没有稳定优于普通Momentum。这进一步说明Rprop - [模块化训练诊断与优化恢复](../../../docs/design/optim/references/modular-training-diagnostics.md):当前 `diagnostics.py` 的角色、history alignment、双 archive、spike-region 和恢复策略。 - [电压轨迹与 Spike-Aware 参数训练](../../../docs/design/optim/references/voltage-and-spike-parameter-fitting.md):subthreshold/spike loss、mask、curriculum 和历史实验索引。 -- [Optimization Design Overview](../../../docs/design/optim/design-overview.md):公共 `braincell.trainable` 与实验训练代码的边界。 +- [Optimization Design Overview](../../../docs/design/optim/TODO.md):公共 `braincell.trainable` 与实验训练代码的边界。 - [Experimental Optimization Work](../README.md):exact forward sensitivity、RTRL/BPTT、正确性与 scaling 实验导航。 ## 最小使用方式 diff --git a/examples/multi_compartment/channel_learning.ipynb b/examples/multi_compartment/channel_learning.ipynb new file mode 100644 index 00000000..ea0e1356 --- /dev/null +++ b/examples/multi_compartment/channel_learning.ipynb @@ -0,0 +1,642 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51eeae63", + "metadata": {}, + "source": [ + "# Channel 参数学习\n", + "\n", + "这里只演示 Channel:一个细胞、一个 CV、一条带 spike 的目标电压轨迹,每次只训练一个参数。注入电流、Ion、膜电容等保持固定。初值靠近目标,目的是展示接口,不是证明单条轨迹能唯一辨识所有生物参数。\n", + "\n", + "## 哪些参数可以选?\n", + "\n", + "候选项来自 Channel 的构造签名,不再维护可训参数字典。签名可见不等于一定可微;单位、形状和模型原有写法仍然有效。\n", + "\n", + "| 运行类别 | 例子 | 可能的结果 |\n", + "|---|---|---|\n", + "| 电流系数 | `g_max` | 通道开放且存在驱动力时,通常有梯度 |\n", + "| 门控曲线、速率 | `V_sh`、`mMidV`、`alpha`(仅当签名中存在) | 取决于动力学和观测 |\n", + "| 温度 | `temp`、`q10`、`temp_ref` | 通过温度因子影响速率;派生 `phi` 随依赖项更新 |\n", + "| 独立速率因子 | 显式参数 `phi` | 直接学习自身,不强加温度依赖 |\n", + "| 数字指数 | `AHP_De1994.n` | 虽然默认值是整数,浮点指数路径仍可微 |\n", + "| 比较式开关 | `gateCurrent != 0` | 两侧值可不同,但对开关的梯度为零 |\n", + "| Python 控制流 | `freeze_m_inf`、静态整数配置 | 浮点化或追踪后可能自然报错 |\n", + "| 字符串等配置 | `name`、`solver` | 不是数值训练根;保留原有配置路径 |\n", + "\n", + "| 零梯度情形 | 原因 |\n", + "|---|---|\n", + "| `temp == temp_ref` 时学习 `q10` | 指数为零,温度因子恒为 1 |\n", + "| 冻结的 `m_inf` 对 `mMidV` 的梯度 | 模型主动使用 `stop_gradient` |\n", + "| 目标没有使用某区域、某参数 | 当前损失没有这条依赖路径 |\n", + "| `gateCurrent` 开关 | 比较条件没有连续导数 |\n", + "\n", + "零梯度不是包装失败,也不会自动报错,需要结合模型和训练数据理解。实施与验证范围见 [Channel Learning 记录](../../docs/specs/2026-09-07-channel-learning.md)。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "730fe9c5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:13.981711Z", + "iopub.status.busy": "2026-09-07T03:51:13.981013Z", + "iopub.status.idle": "2026-09-07T03:51:16.560955Z", + "shell.execute_reply": "2026-09-07T03:51:16.560050Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JAX 0.8.0; backend=cpu\n" + ] + } + ], + "source": [ + "import os\n", + "os.environ[\"JAX_PLATFORMS\"] = \"cpu\"\n", + "\n", + "from time import perf_counter\n", + "import braincell\n", + "import brainstate\n", + "import braintools\n", + "import brainunit as u\n", + "import jax\n", + "import jax.numpy as jnp\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import Markdown, display\n", + "from braincell.filter import AllRegion, RootLocation\n", + "\n", + "DT = 0.025 * u.ms\n", + "DURATION = 20.0 * u.ms\n", + "EPOCHS = 100\n", + "LR = 0.03\n", + "print(f\"JAX {jax.__version__}; backend={jax.default_backend()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "caa449ba", + "metadata": {}, + "source": [ + "## 一条目标轨迹\n", + "\n", + "使用 Na/K/Leak 三种通道。所有目标参数取当前 HH Channel 默认值:钠通道 `g_max=120 mS/cm²`、`V_sh=-45 mV`、`temp=309.15 K`(36 °C)。这些是本示例的模型设定,不是声称重现原始 HH 实验温度。\n", + "\n", + "固定电流脉冲:5 ms 开始,持续 10 ms,幅度 0.05 nA。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d929f7f4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:16.566491Z", + "iopub.status.busy": "2026-09-07T03:51:16.566178Z", + "iopub.status.idle": "2026-09-07T03:51:20.317570Z", + "shell.execute_reply": "2026-09-07T03:51:20.316702Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAEiCAYAAABkykQ1AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXCNJREFUeJzt3XdYFNf7NvB7aQsiIEVRpCkqCmKJXVSMJWrUqN9gN5rYTTTB2ImJXYwlxliCMUajMfbYNTH2ksQCKgg2VKJUlY7A0s77hy/7c0ORtjvscn+ua67AzJmZeye47MOZc0YmhBAgIiIiIiIqAz2pAxARERERkfZjYUFERERERGXGwoKIiIiIiMqMhQUREREREZUZCwsiIiIiIiozFhZERERERFRmLCyIiIiIiKjMWFgQEREREVGZsbAgIiIiIqIyY2FBREQEoFmzZujTp4/KOmdnZwwZMqRcz7N9+3ZYWVkhKSmpRPtt3LgRtWrVQmpqarnmISIqLywsiIgk4O/vD5lMVqzlxYsXUsdV0bBhQ/Tv37/Mx0lNTcWvv/6K/v37w9jYGDKZDDExMWUPWIGlpaXB19cX06dPh4WFRYn2HT16NExMTODn56emdEREZcPCgohIAhMnToQQQrlkZWUBAN5//32V9UII2NjYSJxWPVatWoWjR4/iww8/xAcffCB1HNy8eRNHjx5V6zm2bduG2NhYTJgwocT7GhoaYvz48Vi7di1SUlLUkI6IqGxYWBARkSTmzZun7LGQy+VSx9EIf39/vPvuu7C2ti7V/sOHD0dKSgp+/fXXck5GRFR2LCyIiCqodevWqdwSZW5ujk6dOuHYsWMq7SZPnoyqVasiOTkZo0ePhrW1NerXrw8AyM7Oxty5c2Fvb48qVarAy8sLwcHB6NOnDxo3bpzvnAcOHECHDh1QtWpVVKlSBR07dsTZs2eV242NjXHv3j0cOnRImathw4b5Mu/bt09NV+X/nDx5El5eXrC2tka1atXg6emZ77x5YyQCAgLg6ekJExMTODs7w8/PD0IIlbYFjbEoyObNm2FkZIQpU6YgJycHAPDkyRN89NFHsLOzg5GREerUqYN58+Ype6IAICIiArdu3UKXLl3yHfP58+eYMGECHB0dYWJiAldXV0yfPh3x8fEq7RwcHFC/fn0cPny42NeJiEhTWFgQEVVQkydPVt4OlZOTg9u3b6NVq1bo378/rl27lq/9xIkT0b9/f4SFhWHOnDnKdd988w38/PwQHR2N7777DrNnz873gRV4dWuSt7c3evTogXv37uHff//F22+/je7du+PUqVMAgIyMDLi6uqJfv37KbHfv3lXvhSjAnTt30LdvX7Rp0wahoaGIjIzE6tWrsXfvXkRGRqq0jYmJwZdffomNGzciKioKs2bNwrx58zB79uwSnVMIgZkzZ2LChAn45ptvsHbtWujr6+Px48do2bIlHjx4gCNHjiA+Ph6bNm3Cjz/+qHKL18WLFwEALVu2zHfsYcOG4fLlyzh8+DASExNx4sQJ2Nvb4+eff87XtnXr1rh06VK+woiISHKCiIgkl5WVJQCI999//41t69WrJyZOnKj8/pNPPhEAxObNm1Xa3b9/X8hkMvHll1+qrH/48KHQ19cX7u7uynVRUVHCyMhITJgwId/5unXrJlq0aKH83tXVVfTr16+4L61Y8l5DdHR0sdr/+OOPAoCIjIwssp2Tk5MwMjISERERKusnT54sDAwMRFRUlHJd06ZNRe/evfPtP3jwYPHy5UsxYMAAYWZmJo4fP67SZuDAgcLKykq8ePFCZf2+ffsEAHHt2jUhhBDLli0TAMSjR49U2uXm5gpDQ8N8/58KM336dAFAxMXFFas9EZGmsMeCiKiCSktLw5dffolGjRrBxMREeetRWFgYwsLC8rV/7733VL4/d+4chBDo27evyvq6devC3d1dZd3JkyeRmZmJgQMH5jtut27dEBgYWKGmOW3SpAlkMhk++ugj/Pnnn0hPTy+0bYsWLVC7dm2Vdf3790d2djYuXLjwxnPFxsaiU6dOCAgIwOXLl9GrVy/lttzcXBw7dgxdunTJN26iW7duAIDz588DABITEwEAZmZmKu1kMhmaNGkCf39/fP/993j69GmReczNzVWOR0RUUbCwICKqoEaOHIm1a9di6dKliIiIQE5ODoQQaNasmcq9+wBgZGSUb/aouLg4AECNGjXyHfu/6/Kmee3RowcMDAygr68PPT096OnpYfbs2RBCFHj7lFRatWqF3bt3Iy4uDj169ICFhQW8vLywe/fufG1tbW0LXVecqXxv376NgIAA9OrVCx4eHirbUlJSkJaWhv379yuvW961q1atGoD/+/+Q931ycnK+c+zfvx9du3bFrFmz4OjoCBcXF8yY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YUFEREohISE4dOiQ8vtDhw6hfv36cHNzU65755130KpVK6SkpEgREQDwxRdfYMKECbCwsFDbOQYNGgRfX1/s378fAwcOVNt5iIh0hUwIIaQOQUREFVPNmjUxefJkzJ07V2X99evXoVAolD0W//zzD2QyGdzd3XHjxg0kJyfDy8sLVatWRXJyMi5fvgy5XI727dvD2Ng433mCgoLw6NEjODg4oHnz5kX2EDx58gR16tRBWFgY6tSpU+bzh4eH4/bt26hevTreeustGBoaKrfNmzcP58+fx7lz50p7CYmIKg32WBARUYn991aodevWITg4GImJiXB3d0dYWBhSU1Ph5+eH+fPnw83NDXfu3IGFhQX+/vtv5Yf75ORkeHt74+7du2jWrBnu3r0LS0tLHDlyBDVq1Cjw3MePH4ejo6OyqCjL+efPn49vvvkGHTt2RHJyMlJTU/Hbb78pj92lSxcsWbIESUlJau0dISLSBSwsiIioXDx8+BC3bt2Ci4sLFAoFXFxc8Nlnn+HWrVtwcnLCy5cv4eTkhD179mDkyJEAgKlTp8LY2BgPHz6EoaEhcnJy8P7772PmzJnYunVrgecJDAxUuTWrtOdXKBRYsmQJTp48ibfffhsAcO/ePaSnpyuP6eHhgZycHAQGBirbEBFRwVhYEBFRuejatStcXFwAAHK5HM2bN0eVKlXg5OQEADA1NYWHhwfu378PAMjMzMSvv/6Kzz77DIcOHYIQAkII2Nvb48iRI4We58WLF7C0tCzz+fX09GBkZITg4GB4eXlBT08Prq6uKsesVq2a8pxERFQ0FhZERFQu/vthXy6XF7guIyMDABATE4OMjAzcuHED4eHhKu28vLwKPU/VqlULHDhe0vMbGhril19+wbRp07B48WJ4eXlh6NCh+N///qdsn5aWBgAwMzMrNA8REb3CwoKIiCSR92F9woQJKh/m36RBgwbYt29fuWQYMGAABgwYgPv37+P48eP46KOPEBERgU8//RQA8PjxYwDI15NBRET5cWJuIiKShKWlJdq0aYONGzfivxMURkZGFrpf165dcfv2bSQlJZXp/GlpaUhMTATwqljx8fFBnz598M8//yjbXL58GXXr1lUZKE5ERAVjjwUREUnG398f3bp1Q7du3eDt7Y309HT8+eefaNiwIVavXl3gPu3atUOjRo2wd+9ejB07ttTnTkpKQseOHdGvXz80btwYT548wcGDB7F9+3Zlm927d5fpHERElQkLCyIiKlT//v3h7u6eb/1/H5DXrl27fM+e6NChQ76xCZ07d4aDg4Py+2bNmiEkJARbt27FlStXUL16dUydOhXvvPNOkbm+/PJLLFmyBGPGjIFMJivV+WvVqoVr165hy5YtuHTpEqysrHD69Gm0bdsWwKtna4SEhOC3334rMgsREb3CB+QREZFW+vzzzzFmzJgCC5/y8MMPP8DKygre3t5qOT4Rka5hYUFERERERGXGwdtERERERFRmLCyIiIiIiKjMWFgQEREREVGZsbAgIiIiIqIyY2FBRERERERlxsKCiIiIiIjKjIUFERERERGVGQsLIiIiIiIqMxYWRERERERUZiwsiIiIiIiozFhYEBERERFRmf0/N2kEoyujxt8AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def build_cell():\n", + " soma = braincell.Branch.from_lengths(\n", + " lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type=\"soma\",\n", + " )\n", + " cell = braincell.Cell(\n", + " braincell.Morphology.from_root(soma, name=\"soma\"),\n", + " cv_policy=braincell.CVPerBranch(), pop_size=(1,),\n", + " V_init=-65.0 * u.mV, solver=\"staggered\",\n", + " )\n", + " cell.paint(\n", + " AllRegion(),\n", + " braincell.mech.CableProperty(\n", + " resting_potential=-65.0 * u.mV,\n", + " membrane_capacitance=1.0 * u.uF / u.cm**2,\n", + " axial_resistivity=100.0 * u.ohm * u.cm,\n", + " ),\n", + " braincell.mech.Ion(\"SodiumFixed\", E=50.0 * u.mV),\n", + " braincell.mech.Ion(\"PotassiumFixed\", E=-77.0 * u.mV),\n", + " braincell.mech.Channel(\"IL\", name=\"leak\"),\n", + " braincell.mech.Channel(\"Na_HH1952\", name=\"na\"),\n", + " braincell.mech.Channel(\"K_HH1952\", name=\"k\"),\n", + " )\n", + " cell.place(\n", + " RootLocation(0.5),\n", + " braincell.mech.CurrentClamp(\n", + " delay=5.0 * u.ms, durations=10.0 * u.ms, amplitudes=0.05 * u.nA,\n", + " ),\n", + " )\n", + " cell.soma.record(\"v\", braincell.observe.state(\"v\"))\n", + " return cell\n", + "\n", + "\n", + "def simulate(cell):\n", + " cell.reset_state()\n", + " return cell.run(dt=DT, duration=DURATION).samples[\"v\"].values.to_decimal(u.mV)[:, 0]\n", + "\n", + "\n", + "def spike_count(voltage_mv):\n", + " v = np.asarray(voltage_mv)\n", + " return int(np.count_nonzero((v[:-1] < 0.0) & (v[1:] >= 0.0)))\n", + "\n", + "\n", + "target_cell = build_cell()\n", + "target_cell.init_state()\n", + "target = brainstate.transform.jit(lambda: simulate(target_cell))()\n", + "assert target_cell.n_cv == 1 and spike_count(target) >= 1\n", + "# Cell.run records each step's resulting voltage at that step's sample time.\n", + "times_ms = np.arange(target.size) * float(DT / u.ms)\n", + "plt.figure(figsize=(8, 3))\n", + "plt.plot(times_ms, target, color=\"black\")\n", + "plt.xlabel(\"Time (ms)\")\n", + "plt.ylabel(\"Voltage (mV)\")\n", + "plt.title(f\"Target: {spike_count(target)} spike(s)\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "180678f7", + "metadata": {}, + "source": [ + "## 共用的最小拟合过程\n", + "\n", + "损失只有电压 MSE,spike 数量仅用来验收,不参与求导。每次拟合重新建立模型并 reset 动态状态,只保留训练参数。时间推进由 `Cell.run` 编译,优化迭代使用 `brainstate.transform.for_loop`。\n", + "\n", + "`scale` 训练相对电导;`parameterized` 将无量纲偏移映射回 mV 或 K。这样三个实验的优化尺度都比较直接。" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "36a96eca", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:20.322888Z", + "iopub.status.busy": "2026-09-07T03:51:20.322724Z", + "iopub.status.idle": "2026-09-07T03:51:20.333983Z", + "shell.execute_reply": "2026-09-07T03:51:20.333171Z" + } + }, + "outputs": [], + "source": [ + "def fit_one(field, source, unit):\n", + " started = perf_counter()\n", + " cell = build_cell()\n", + " cell.channels[\"na\"].trainable(**{field: source})\n", + " cell.init_state()\n", + " parameters = cell.trainables.parameters()\n", + " states = parameters.states()\n", + " assert len(states) == 1 and next(iter(states.values())).value.shape == ()\n", + " predict = brainstate.transform.jit(lambda: simulate(cell))\n", + " initial = np.asarray(predict())\n", + " initial_parameter = float(cell.channels[\"na\"].get(field).to_decimal(unit)[0])\n", + "\n", + " def loss():\n", + " return jnp.mean((simulate(cell) - target) ** 2)\n", + "\n", + " gradient = brainstate.transform.grad(loss, grad_states=states, return_value=True)\n", + " optimizer = braintools.optim.Adam(lr=LR)\n", + " optimizer.register_trainable_weights(states)\n", + "\n", + " @brainstate.transform.jit\n", + " def optimize():\n", + " def step(_):\n", + " gradients, value = gradient()\n", + " optimizer.update(gradients)\n", + " return value\n", + " return brainstate.transform.for_loop(step, jnp.arange(EPOCHS))\n", + "\n", + " loss_history = np.asarray(optimize())\n", + " fitted = np.asarray(predict())\n", + " initial_mse = float(np.mean((initial - np.asarray(target)) ** 2))\n", + " final_mse = float(np.mean((fitted - np.asarray(target)) ** 2))\n", + " fitted_parameter = float(cell.channels[\"na\"].get(field).to_decimal(unit)[0])\n", + " target_parameter = float(target_cell.channels[\"na\"].get(field).to_decimal(unit)[0])\n", + " elapsed = perf_counter() - started\n", + " assert np.isfinite(loss_history).all() and np.isfinite(fitted).all()\n", + " assert spike_count(fitted) >= 1\n", + " assert final_mse <= 0.1 * initial_mse, (field, initial_mse, final_mse)\n", + "\n", + " fig, axes = plt.subplots(1, 2, figsize=(10, 3))\n", + " axes[0].plot(times_ms, target, color=\"black\", label=\"Target\")\n", + " axes[0].plot(times_ms, initial, color=\"#a76637\", alpha=0.7, label=\"Initial\")\n", + " axes[0].plot(times_ms, fitted, color=\"#168b83\", linestyle=\"--\", label=\"Fitted\")\n", + " axes[0].set(xlabel=\"Time (ms)\", ylabel=\"Voltage (mV)\", title=field)\n", + " axes[0].legend(fontsize=8)\n", + " axes[1].semilogy(np.arange(EPOCHS), loss_history, color=\"#168b83\")\n", + " axes[1].scatter([EPOCHS], [final_mse], color=\"black\", s=15)\n", + " axes[1].set(xlabel=\"Adam update\", ylabel=\"Voltage MSE (mV²)\")\n", + " fig.tight_layout()\n", + " plt.show()\n", + " display(Markdown(\n", + " \"| 参数 | 初值 | 目标 | 拟合 | 初始 MSE | 最终 MSE | 耗时(含编译) |\\n\"\n", + " \"|---|---:|---:|---:|---:|---:|---:|\\n\"\n", + " f\"| {field} ({unit}) | {initial_parameter:.6g} | {target_parameter:.6g} | \"\n", + " f\"{fitted_parameter:.6g} | {initial_mse:.6g} | {final_mse:.6g} | {elapsed:.2f} s |\"\n", + " ))\n", + " return {\"field\": field, \"initial_mse\": initial_mse, \"final_mse\": final_mse,\n", + " \"seconds\": elapsed, \"spikes\": spike_count(fitted)}\n" + ] + }, + { + "cell_type": "markdown", + "id": "885fba42", + "metadata": {}, + "source": [ + "### 1. 电流幅度:只学 `g_max`\n", + "\n", + "电导从目标值的 90% 出发。这里学习的是通道电流系数,不是注入电流;电压和门控状态仍由模型共同决定。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d4bc54af", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:20.337831Z", + "iopub.status.busy": "2026-09-07T03:51:20.337684Z", + "iopub.status.idle": "2026-09-07T03:51:26.302348Z", + "shell.execute_reply": "2026-09-07T03:51:26.301392Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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QW5uLqNGjeL555/3lJk8eTInTpygVatWmEwmdDodc+bMoUWLFpe8H61WS506da5p7GFhYTX2QpDY/acmxy+x+09Njr8mxw7XJn6p6b588r3tX3KuLo+cr8sj5+vSybm6PNfqfF3q93aNSrwbN27M999/T0lJCWfPniU2NhadTudVRqfT8f/+3/9j2rRpnD17lri4OPT6GnWYQgghhBBCCCGuIzUyIw0KCqJ27doXLWMymTCZTD6KSAghhBBCCCGEqFiNGse7JjIajfz973/36rytppDY/acmxy+x+09Njr8mxw41P37xK3ktL52cq8sj5+vyyPm6dHKuLo+/zpdfhxMTQgghhBBCCCGud1LjLYQQQgghhBBCVCFJvIUQQgghhBBCiCokibcQQgghhBBCCFGFamSv5tWJ2+0mPT0dgGbNmqHV/v69jCtZpyq43W7279+PRqMhKSkJg8Fw0fKHDx8mIyPDa15QUBDt2rWryjDLKSoqIi0trdz8li1b/u5YfEVFRezZsweLxUL9+vWrKsRKFRcX88svv1S4rFGjRsTExFS4bOvWrRQWFnrNi42NpWHDhtc8xors2LGDkpISOnbsWOHy6nwdnD59mv3799O8eXPMZnOFcVTX68DpdLJlyxYsFguNGzf2WlYTroN9+/aRlZVFt27d0Gg0nvnV/TpQSnH48GFKSkpo0KABgYGBFZbLysri6NGjJCYmEh0dfUnbvpJ1hO/I61O5U6dOcerUKerXr1/pZ0xBQQF79+4lKiqKevXq+TbAauj853RMTAyNGjUqtzwzM5OMjAwaNmyIxWLxQ4TVR1FREXv37iUhIYFatWpVWGb37t3YbDaaN2/+u9/V17O8vDwOHz5McHAw9evXr/Bc2Gw2du3aRWhoaIXvvevZzp07KSwspHPnzhUud7vd7Nq1C5fLRfPmzcsNTX2pZa6IElds69atKikpScXFxan4+HiVlJSktm7des3XqQqvvvqqiouLU8nJyZ54vvrqq4uu8+STTyqLxaK6devm+RsyZIiPIv5VWlqaAlSnTp28Ytm+fftF1/v0009VaGiouummm1RoaKjq1auXysvL81HUZQ4dOuQVc7du3VRycrICVEpKSqXrtWrVStWrV89rvVdffbXK450+fbpq1aqVslgsKiYmpsIy1fU6SEtLU/fff7+Kjo5WgFq2bFm5MtX1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i7fffpuff/6Z8PBwunXrxpgxY3z+zIjb7WbOnDksWbKEoqIikpOT+dOf/uRVM7V3714eeeQR3nvvPc8dvry8PF577TU2b96MxWLh0UcfpV+/fj6N/bypU6eyZ88eZs2aVeHyhx56iGbNmjF58mQAtm3bxgcffMD+/fupXbs2gwYNYuDAgVUe59NPP82PP/5Ybv5nn33maXJTXa+DVatW8fe//73c/OHDh/P4449X++ugT58+5ToNi4yM9PSgXp2vg9dff73CZ97//e9/06lTJ890dbwO7rrrrnK96gIMHTqUsWPHeqb37NnD66+/zqFDh6hfvz5PP/00ycnJnuVz587lf//7H2vXrr3kdYR/yetTscq+B9q0acPbb7/tmc7JyeG1114jLS2NyMhIHn/8cXr16uXLUKulcePGERcXx3PPPec1/+eff+att94iIyOD5ORkJk+efMM2BV66dCkzZ87EarWSnJzMxIkTy7U2+vTTT/niiy+w2+307t2bCRMmYDQa/ROwH+3bt4933nmHffv2YTKZ6NatG0888QRBQUGeMkVFRUybNo3169cTGhrKQw895NUB2/Xqueee44cffig3/+OPP/aMwuB2u5k+fTopKSm43W769+/PE0884enH5VLLXClJvIUQQgghhBBCiCokz3gLIYQQQgghhBBVSBJvIYQQQgghhBCiCkniLYQQQgghhBBCVCFJvIUQQgghhBBCiCokibcQQgghhBBCCFGFJPEWQgghhBBCCCGqkCTeQgghhBBCCCFEFZLEW4gbyJYtW1i/fr1fY1i9ejXHjh2rsu2vW7eOAwcOVNn2hRBCiGttwYIFZGZm+juMa+56PS4hroTe3wEIIa7e9u3b2bVr10XL3H333UyfPp2cnBy6devmo8i8paenM2zYMPbu3Vtl+8jIyGDixIls2rQJrVbuLQohhPCtoqIiFi1aRKNGjWjXrt0lrTNy5Ejmzp1LfHx8FUfnW5d7XEopvvjiC3r37k10dHQVRyeEb0niLcR1ID09nZSUFM90SkoKjRo1omnTpp55t912Gx06dKCgoMAfIQLw/PPP89hjjxEeHl5l+7j//vt57rnnWLBgAffdd1+V7UcIIYSoyOeff87o0aNJTk5m9+7d/g6nRnG5XAwdOpQ1a9ZI4i2uO5J4C3EdGDp0KEOHDvVMx8bGcv/99/PXv/7Vq1yrVq2w2+2e6R9//BGNRkOzZs1IS0vDarXSo0cPQkJCsFqtrF+/HqPRSNeuXQkMDCy33+3bt3Po0CESEhJo06bNRWuYjx07xqJFi3jzzTevyf6PHDnCzp07qVWrFm3btsVgMACg0WgYPnw47777riTeQgghfG769OmMHz+e999/n/Xr11fYyuzo0aNs3bqVxMREWrZsWW55SkoKJSUlaLVaz3fshd+DTqeT+fPn07dvXwoKCkhPTyc6OpoOHToAsGfPHvbu3VvuJvxvna+dv+uuuwgJCfHMnzdvHt26dSM+Pt5rX1arlfT0dOLi4iqszb/a4/r666+BssfSTp06RVhYGP379weguLiYDRs24HA4aNmyJXXq1Kn0uISojiTxFuIG8tum5u+88w47duwgLy+PZs2aceDAAQoLC5k6dSovvvgiTZs2Zffu3YSHh7Nx40bPl6PVamXw4MHs2bOH1q1bs2fPHiwWC4sWLar0DvXSpUupW7cuSUlJnnlXuv8XX3yRN954g5tvvhmr1UphYSFfffWVZ9u9e/fmlVdeIT8/v0pr14UQQogLpaens2XLFhYsWEBWVhbTp08vl3h/+OGHjBs3ji5dumC1WjGbzTidTq8yy5YtIy8vD5fLxa5du7DZbCxZsoTk5GQAbDYbQ4cO5ZZbbuH06dM0aNCAtWvXcv/99xMcHMyaNWtISkpi9erVTJ06lQkTJlQYb3Z2NkOHDmX//v00bNjQM3/EiBHMnz+f+Ph4z77uuOMOdu3aRZMmTVi/fj133303s2fPvqbHtWzZMqCsv5Z9+/ZRu3Zt+vfvz8qVKxk2bBiNGjXCbDazYcMGnnrqqXIVDEJUa0oIcd2JiYlRU6ZMKTf/scceU/fee69n+sEHH1Qmk0kdOHBAKaWUzWZTtWvXVuHh4erIkSNKKaUKCwtVZGSkmjVrlme9P/7xj2rAgAHK4XAopZRyOp3q7rvvViNHjqw0ptGjR6v+/ft7zbuS/dtsNqXX69Xq1as929mzZ49KT0/3TJ85c0YBXmWEEEKIqjZhwgQ1YMAApZRSq1evViaTSVmtVs/yU6dOqeDgYDVz5kzPvLFjxypALVq0qNLtjhkzRt11112e6YKCAgWoP/zhD8rpdCqllJo3b54C1NChQ5XL5VJKKTVz5kwVEhLiKfNbhw8fVoDav3+/13yj0eiJ5/y+OnTooIqKipRSSu3evVsFBgaqr7766poeV2lpqQLUmjVrPPOys7NVWFiY+vrrrz3z9u7dq0wmk9qwYUOl2xaiupEabyFucH369KFBgwYAGI1G2rRpQ3BwMImJiQCYTCZatGjBvn37AHA4HHz22Wc8+eSTpKSkoJRCKUWdOnVYtGhRpfvJycnBYrFc9f61Wi0BAQHs2LGDHj16oNVqady4sdc2zWazZ59CCCGELzgcDubMmcPMmTMB6NWrF7Vr1+bzzz9nzJgxACxatIiQkBAeeughz3qTJ0/m3XffLbe9/fv3s3//fqxWK+Hh4SxcuLBcmUcffRSdTgdAly5dABg9erTn0a8uXbpQWFjIyZMnr7pp9tixYwkODgYgOTmZgQMH8uWXXzJo0KBrflwX+uqrr9DpdDidTubNmwfg+d2xdu1az3ELUd1J4i3EDe63ybDRaKxwns1mA+DUqVPYbDbS0tI4cuSIV7kePXpUup+QkJAKO3a73P0bDAbmzJnDU089xcsvv0yPHj0YOnQof/jDHzzli4uLAQgNDa00HiGEEOJaWrhwIQUFBeTn5zN37lwAmjZtyowZMzyJ97Fjx6hbt65Xnyh16tRBr//1J7nT6WTIkCGsXLmSTp06YTabyc7OJisrq9w+L/y+NBqNlc47/x16NerVq+c1nZSUxA8//FAlx3WhI0eOoNFomD9/vtf81q1bU7t27as8KiF8RxJvIcRlOZ/MPvbYY17J7u+56aabyn1pXqlBgwYxaNAg9u3bx9KlS3n44Yc5ceIE48ePB+Dw4cMA5WrChRBCiKoyffp0WrVq5dX6y2g0sm3bNnbs2EGLFi2IjIwkNzfXa73CwkKvZ6G/+eYb1qxZw6FDh4iMjARg7ty5rF279prHfD5RdrvdnnlOp7Pcs9lAubhzc3OJiooCqNLjCgsLQ6fTeW5mCFFTySC3QojLYrFY6NSpE++//z5KKa9lGRkZla7Xp08fdu7cSX5+/lXtv7i4mLy8PKAsmZ8wYQJ33XUXP/74o6fM+vXrqV+/vldHbkIIIURVOXr0KKtWrWLGjBnMnTvX669Pnz5Mnz4dgG7dunHo0CF27NjhWferr77y2tapU6eIioryJKfANbtx/VuxsbFotVoOHDjgmZeamorL5SpX9sIm4Xa7nSVLlng6jrtWx6XX671auQH069eP7Ozsctuz2WycPXv2Mo5WCP+SGm8hxGX73//+x6233sqtt97K4MGDKSkpYcWKFSQnJ3sNF3ahLl260KRJE+bNm8ejjz56xfvOz8/n5ptv5u6776Z58+YcO3aMhQsXevWs+sUXX1zVPoQQQojL8dFHH5GUlETz5s3LLbvnnnt49tln+de//kX79u25//77ufPOO3nqqaewWq28//77nue0oSzRfPrppxk9ejSdO3dm+fLlrFq1qkriDggI4IEHHmD8+PEcO3aMvLw8Zs2aVeHwoEuWLGHMmDG0a9eOTz/9FIPBwNixYwGu6XG1b9+et956i5ycHCIiIujfvz/PPfccw4YNY+zYsTRt2pRDhw4xf/585s6dS0RERJWcGyGuNanxFuI6dM8999CsWbNy8zt06ED37t090126dKFTp05eZbp37+4ZB/S8nj170qZNG89069atSU9P57bbbmPTpk2cPHmSiRMnVpp0n/fCCy/w9ttve2rKr2T/cXFxbN68mdq1a7Nu3ToKCwtZtWqVp9n79u3bSU9P5/HHH79oLEIIIcS1UlJSwuTJkytcdvfdd9O3b1/27t0LwOzZs/nzn/9MWloaTqeTdevWMXz4cM/zyg0aNGDjxo2YTCZSU1Pp0qULy5YtY8iQIZ5tGgwGhgwZ4mnqDWXN2ocMGeL1jLfJZGLIkCEX7fNk5syZ/OlPf2Lz5s2UlpayatUqHnzwwXLPT3/xxRc0adKEzZs3c8stt/Djjz96jf19LY4Lypqft23b1isxf+WVV/juu+9QSrFu3TpCQ0NZvXq1128TIao7jfptW1EhhKhCkyZN4pFHHqnwxsC18MEHHxAREcHgwYOrZPtCCCHEjaSwsJDQ0FA2btxI586d/R2OEDWWJN5CCCGEEEKICkniLcS1IU3NhRBCCCGEEBWqqFm7EOLySY23EEIIIYQQQghRhaTGWwghhBBCCCGEqEKSeAshhBBCCCGEEFVIEm8hhBBCCCGEEKIKSeIthBBCCCGEEEJUIUm8hRBCCCGEEEKIKiSJtxBCCCGEEEIIUYUk8RZCCCGEEEIIIaqQJN5CCCGEEEIIIUQVksRbCCGEEEIIIYSoQv8f0kj9kHtrIskAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| 参数 | 初值 | 目标 | 拟合 | 初始 MSE | 最终 MSE | 耗时(含编译) |\n", + "|---|---:|---:|---:|---:|---:|---:|\n", + "| g_max (mS / cm^2) | 108 | 120 | 119.966 | 55.9342 | 7.09192e-05 | 5.63 s |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "conductance_result = fit_one(\n", + " \"g_max\",\n", + " braincell.trainable.scale(brainstate.nn.Param(0.9), name=\"na_conductance_scale\"),\n", + " u.mS / u.cm**2,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b1ca9486", + "metadata": {}, + "source": [ + "### 2. 门控曲线与速率:只学 `V_sh`\n", + "\n", + "`V_sh` 同时进入钠通道的 alpha/beta 速率函数。初值比目标高 1 mV,其余所有参数都保持目标值。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0dce81d6", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:26.304874Z", + "iopub.status.busy": "2026-09-07T03:51:26.304639Z", + "iopub.status.idle": "2026-09-07T03:51:32.866340Z", + "shell.execute_reply": "2026-09-07T03:51:32.865425Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| 参数 | 初值 | 目标 | 拟合 | 初始 MSE | 最终 MSE | 耗时(含编译) |\n", + "|---|---:|---:|---:|---:|---:|---:|\n", + "| V_sh (mV) | -44 | -45 | -45.0055 | 258.324 | 0.00785799 | 6.27 s |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def shifted_voltage(ctx, delta):\n", + " return -45.0 * u.mV + delta * u.mV\n", + "\n", + "\n", + "voltage_shift_result = fit_one(\n", + " \"V_sh\",\n", + " braincell.trainable.parameterized(shifted_voltage, delta=brainstate.nn.Param(1.0)),\n", + " u.mV,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "89b347ab", + "metadata": {}, + "source": [ + "### 3. 温度:只学 `temp`\n", + "\n", + "这里只改变钠通道温度,不改变钾通道或整个细胞的温度。初值为 35 °C,目标为 36 °C。\n", + "\n", + "依赖关系是 `temp → q10 ** ((temp - temp_ref) / (10 K)) → 门控速率 → 电压`。\n", + "HH 通道通过门控辅助函数读取温度因子;部分 sodium Markov 通道则通过动态 `phi` 属性读取。显式独立 `phi` 参数仍保持独立。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0d7af1fa", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:32.869266Z", + "iopub.status.busy": "2026-09-07T03:51:32.869102Z", + "iopub.status.idle": "2026-09-07T03:51:38.793013Z", + "shell.execute_reply": "2026-09-07T03:51:38.792176Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| 参数 | 初值 | 目标 | 拟合 | 初始 MSE | 最终 MSE | 耗时(含编译) |\n", + "|---|---:|---:|---:|---:|---:|---:|\n", + "| temp (K) | 308.15 | 309.15 | 309.145 | 24.2734 | 0.000379125 | 5.58 s |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def shifted_temperature(ctx, delta):\n", + " return u.celsius2kelvin(36.0) + delta * u.kelvin\n", + "\n", + "\n", + "temperature_result = fit_one(\n", + " \"temp\",\n", + " braincell.trainable.parameterized(shifted_temperature, delta=brainstate.nn.Param(-1.0)),\n", + " u.kelvin,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "75fd6893", + "metadata": {}, + "source": [ + "## 零梯度与自然报错的实际检查\n", + "\n", + "下面通过同一训练接口检查 `q10` 和 `gateCurrent`。参考温度时对 `q10` 的梯度为零;温度提高 10 K 后梯度非零。`gateCurrent` 的开关值改变电流,但两个位置的梯度都为零。随后演示字符串、错误单位和形状被拒绝。异常会被捕获,整本 notebook 可以顺序执行。\n", + "\n", + "`gateCurrent` 在这里仅作固定电压下的计算图检查。现有公式的开启分支会给出很大的电流值,本次没有校准或修改其幅值,也没有用该分支生成三个拟合的目标数据。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8babfdbf", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:38.795964Z", + "iopub.status.busy": "2026-09-07T03:51:38.795804Z", + "iopub.status.idle": "2026-09-07T03:51:39.265110Z", + "shell.execute_reply": "2026-09-07T03:51:39.264230Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| 检查 | 实际结果 |\n", + "|---|---|\n", + "| q10: temp == temp_ref | gradient = 0 |\n", + "| q10: temp == temp_ref + 10 K | gradient = 1 |\n", + "| gateCurrent = 0 | gradient = 0; I = -1.86135e-05 mA/cm² |\n", + "| gateCurrent = 1 | gradient = 0; I = -7.92288e+13 mA/cm² |\n", + "| name: text | ValueError |\n", + "| g_max: 1.0 | TypeError |\n", + "| V_sh: 1. K | ValueError |\n", + "| temp: [[1. 1. 1.] [1. 1. 1.]] K | ValueError |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "probe = build_cell()\n", + "probe.channels[\"na\"].trainable(q10=braincell.trainable.parameter(group_by=\"all\", name=\"q10\"))\n", + "probe.init_state()\n", + "na_layout = next(layout for layout in probe.runtime.layouts\n", + " if probe.runtime.layout_mechanisms[layout.id].instance_name == \"na\")\n", + "na = probe.runtime.get_runtime_node(na_layout.id)\n", + "\n", + "\n", + "def temperature_factor():\n", + " probe.trainables.materialize()\n", + " return na.gate_phi(na._iter_gates()[0]).sum()\n", + "\n", + "\n", + "gradient = brainstate.transform.jit(brainstate.transform.grad(\n", + " temperature_factor, grad_states=probe.trainables.parameters().states(),\n", + "))\n", + "zero = float(gradient()[\"q10\"])\n", + "probe.channels[\"na\"].set(temp=u.celsius2kelvin(46.0))\n", + "nonzero = float(gradient()[\"q10\"])\n", + "assert zero == 0.0 and nonzero > 0.0\n", + "rows = [f\"| q10: temp == temp_ref | gradient = {zero:g} |\",\n", + " f\"| q10: temp == temp_ref + 10 K | gradient = {nonzero:g} |\"]\n", + "\n", + "switch_cell = build_cell()\n", + "switch_cell.paint(AllRegion(), braincell.mech.Channel(\"Kv1p1_MA2025_BC\", name=\"switch\"))\n", + "switch_cell.channels[\"switch\"].trainable(\n", + " gateCurrent=braincell.trainable.parameter(group_by=\"all\", name=\"flag\"),\n", + ")\n", + "switch_cell.init_state()\n", + "switch_layout = next(layout for layout in switch_cell.runtime.layouts\n", + " if switch_cell.runtime.layout_mechanisms[layout.id].instance_name == \"switch\")\n", + "switch_node = switch_cell.runtime.get_runtime_node(switch_layout.id)\n", + "potassium = braincell.IonInfo(E=-77.0 * u.mV, Ci=140.0 * u.mM, Co=5.0 * u.mM, valence=1)\n", + "\n", + "\n", + "def switch_current():\n", + " switch_cell.trainables.materialize()\n", + " return switch_node.current(-30.0 * u.mV, potassium).to_decimal(u.mA / u.cm**2).sum()\n", + "\n", + "\n", + "switch_parameters = switch_cell.trainables.parameters()\n", + "switch_gradient = brainstate.transform.jit(brainstate.transform.grad(\n", + " switch_current, grad_states=switch_parameters.states(), return_value=True,\n", + "))\n", + "grad_off, current_off = switch_gradient()\n", + "switch_parameters.set_physical_values({\"flag\": 1.0})\n", + "grad_on, current_on = switch_gradient()\n", + "assert float(grad_off[\"flag\"]) == float(grad_on[\"flag\"]) == 0.0\n", + "assert float(current_off) != float(current_on)\n", + "rows.extend([f\"| gateCurrent = 0 | gradient = 0; I = {float(current_off):.6g} mA/cm² |\",\n", + " f\"| gateCurrent = 1 | gradient = 0; I = {float(current_on):.6g} mA/cm² |\"])\n", + "\n", + "# These are independent registration checks, not repeated model time steps.\n", + "for field, value in [(\"name\", \"text\"), (\"g_max\", 1.0),\n", + " (\"V_sh\", 1.0 * u.kelvin), (\"temp\", np.ones((2, 3)) * u.kelvin)]:\n", + " bad_cell = build_cell()\n", + " try:\n", + " bad_cell.channels[\"na\"].trainable(**{\n", + " field: braincell.trainable.parameter(value, group_by=\"all\"),\n", + " })\n", + " except (TypeError, ValueError) as error:\n", + " description = str(value).replace(\"\\n\", \" \")\n", + " rows.append(f\"| {field}: {description} | {type(error).__name__} |\")\n", + " assert not bad_cell.trainables.bindings()\n", + " else:\n", + " raise AssertionError(f\"{field} unexpectedly accepted the invalid source\")\n", + "display(Markdown(\"| 检查 | 实际结果 |\\n|---|---|\\n\" + \"\\n\".join(rows)))" + ] + }, + { + "cell_type": "markdown", + "id": "069766b9", + "metadata": {}, + "source": [ + "## 区域选择与边界\n", + "\n", + "此 notebook 的三个拟合只有一个 CV。多区域模型沿用原有选择接口,例如 `cell.soma.channels[\"na\"].trainable(...)` 或 `cell[0].branch[1].channels[\"na\"].trainable(...)`。\n", + "\n", + "- `group_by=\"all\"` 只共享**选中区域**内的训练根,不会把未选区域的参数一起更新。\n", + "- `population`、`cv`、`row` 分别指定共享方式;不同区域也可以绑定同一个显式 `nn.Param`。\n", + "- 未选区域的参数不变,不代表其电压不变:区域之间仍然电耦合。\n", + "- 参数没有在声明时传入,也可以先读签名默认值、用 `.set()` 覆盖,再注册训练;注册后应更新训练根,不能再用 `.set()` 覆盖同一绑定。\n", + "\n", + "这些例子只说明局部可学习性和接口用法,不对全局收敛、单轨迹可辨识性或任意参数组合做保证。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c6ea3945", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T03:51:39.268066Z", + "iopub.status.busy": "2026-09-07T03:51:39.267879Z", + "iopub.status.idle": "2026-09-07T03:51:39.274553Z", + "shell.execute_reply": "2026-09-07T03:51:39.273742Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| 拟合参数 | 最终/初始 MSE | 拟合 spike 数 |\n", + "|---|---:|---:|\n", + "| g_max | 1.268e-06 | 1 |\n", + "| V_sh | 3.042e-05 | 1 |\n", + "| temp | 1.562e-05 | 1 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Three fits: 17.48 s including compilation\n" + ] + } + ], + "source": [ + "results = [conductance_result, voltage_shift_result, temperature_result]\n", + "display(Markdown(\n", + " \"| 拟合参数 | 最终/初始 MSE | 拟合 spike 数 |\\n|---|---:|---:|\\n\" +\n", + " \"\\n\".join(f\"| {r['field']} | {r['final_mse'] / r['initial_mse']:.4g} | {r['spikes']} |\"\n", + " for r in results)\n", + "))\n", + "print(f\"Three fits: {sum(r['seconds'] for r in results):.2f} s including compilation\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "braincell_311", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_compartment/ion_learning.ipynb b/examples/multi_compartment/ion_learning.ipynb new file mode 100644 index 00000000..b0988367 --- /dev/null +++ b/examples/multi_compartment/ion_learning.ipynb @@ -0,0 +1,615 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "436a9a8e", + "metadata": {}, + "source": [ + "# Ion 参数学习\n", + "\n", + "只训练 Ion;每个例子一个 CV、一个参数、一条合成目标轨迹。前两个例子拟合带 spike 的电压,其余拟合浓度。\n", + "\n", + "| 参数类型 | 例子 | 更新关系 |\n", + "|---|---|---|\n", + "| 独立物理参数 | SodiumFixed.E | 直接参与电流 |\n", + "| 派生电位的上游参数 | SodiumInitNernst.temp | reset 时更新 Nernst 电位 |\n", + "| 独立初值 | CalciumDetailed.Ci_initializer | reset 时 Ci(0) = theta,随后 Ci 自行演化 |\n", + "| 浓度动力学 | CalciumDetailed.tau | 改变浓度清除速度 |\n", + "| 反应速率 | ToyCaBindingKinetic_SU2015_DCN.kf | 改变结合物形成速度 |\n", + "| 派生默认初值 | caiBase、缓冲物总量 | reset 时重新推导,显式初值覆盖优先 |\n", + "| 其他签名参数 | 开关、整数、配置等 | 不保证可微;零梯度或自然报错由实际计算决定 |\n", + "\n", + "签名是候选入口,不是可辨识性保证。species/reactions/conserves 字典定义模型结构,未被删除;本例不提供嵌套字典的训练路径。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "45fbac42", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:08:58.533764Z", + "iopub.status.busy": "2026-09-07T05:08:58.533043Z", + "iopub.status.idle": "2026-09-07T05:09:01.161806Z", + "shell.execute_reply": "2026-09-07T05:09:01.160964Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JAX 0.10.1; backend=cpu\n" + ] + } + ], + "source": [ + "import os\n", + "os.environ[\"JAX_PLATFORMS\"] = \"cpu\"\n", + "\n", + "from time import perf_counter\n", + "import braincell\n", + "import brainstate\n", + "import braintools\n", + "import brainunit as u\n", + "import jax\n", + "import jax.numpy as jnp\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import Markdown, display\n", + "from braincell.filter import AllRegion, RootLocation\n", + "\n", + "DT = 0.025 * u.ms\n", + "STEPS = 800\n", + "EPOCHS = 100\n", + "LR = 0.03\n", + "print(f\"JAX {jax.__version__}; backend={jax.default_backend()}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "cd90509b", + "metadata": {}, + "source": [ + "## 一个 CV 与统一训练函数\n", + "\n", + "theta 是无量纲缩放参数,物理参数保留单位。每轮 loss 内先 reset,再运行模拟;优化器更新 theta,不更新模拟结束时的状态作为下一轮初值。\n", + "\n", + "浓度曲线包含 t=0。CalciumDetailed 使用不等于 C_rest 的起始浓度,不接钙电流,直接展示清除过程。Toy 模型只演示一条可逆结合反应,不代表经过校准的生物模型。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "81dc3f60", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:01.167591Z", + "iopub.status.busy": "2026-09-07T05:09:01.167248Z", + "iopub.status.idle": "2026-09-07T05:09:01.183621Z", + "shell.execute_reply": "2026-09-07T05:09:01.182901Z" + } + }, + "outputs": [], + "source": [ + "def build_cell(model, field, value, voltage):\n", + " soma = braincell.Branch.from_lengths(\n", + " lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type=\"soma\",\n", + " )\n", + " cell = braincell.Cell(\n", + " braincell.Morphology.from_root(soma, name=\"soma\"),\n", + " cv_policy=braincell.CVPerBranch(), pop_size=(1,),\n", + " V_init=-65.0 * u.mV, solver=\"staggered\",\n", + " )\n", + " params = {\"Ci_initializer\": 0.001 * u.mM} if model == \"CalciumDetailed\" else {}\n", + " params[field] = value\n", + " cell.paint(\n", + " AllRegion(),\n", + " braincell.mech.CableProperty(\n", + " resting_potential=-65.0 * u.mV,\n", + " membrane_capacitance=1.0 * u.uF / u.cm**2,\n", + " axial_resistivity=100.0 * u.ohm * u.cm,\n", + " ),\n", + " braincell.mech.Ion(model, name=\"pool\", **params),\n", + " )\n", + " if voltage:\n", + " cell.paint(\n", + " AllRegion(),\n", + " braincell.mech.Ion(\"PotassiumFixed\", E=-77.0 * u.mV),\n", + " braincell.mech.Channel(\"IL\", name=\"leak\"),\n", + " braincell.mech.Channel(\"Na_HH1952\", name=\"na\", ion_name=\"pool\"),\n", + " braincell.mech.Channel(\"K_HH1952\", name=\"k\"),\n", + " )\n", + " cell.place(\n", + " RootLocation(0.5),\n", + " braincell.mech.CurrentClamp(\n", + " delay=5.0 * u.ms, durations=10.0 * u.ms, amplitudes=0.05 * u.nA,\n", + " ),\n", + " )\n", + " return cell\n", + "\n", + "\n", + "def simulate(cell, observable):\n", + " with brainstate.environ.context(dt=DT):\n", + " cell.reset_state()\n", + "\n", + " def read():\n", + " if observable == \"V\":\n", + " return cell.V.value.to_decimal(u.mV).reshape(())\n", + " return getattr(cell.get_ion(\"pool\"), observable).value.to_decimal(u.uM).reshape(())\n", + "\n", + " initial = read()\n", + "\n", + " def step(i):\n", + " with brainstate.environ.context(t=i * DT):\n", + " cell.update()\n", + " return read()\n", + "\n", + " trace = brainstate.transform.for_loop(step, jnp.arange(STEPS))\n", + " return jnp.concatenate((initial[None], trace))\n", + "\n", + "\n", + "def spike_count(trace):\n", + " v = np.asarray(trace)\n", + " return int(np.count_nonzero((v[:-1] < 0) & (v[1:] >= 0)))\n", + "\n", + "\n", + "def fit_one(model, field, initial_value, target_value, observable):\n", + " started = perf_counter()\n", + " voltage = observable == \"V\"\n", + " target_cell = build_cell(model, field, target_value, voltage)\n", + " target_cell.init_state()\n", + " target = brainstate.transform.jit(lambda: simulate(target_cell, observable))()\n", + " cell = build_cell(model, field, initial_value, voltage)\n", + " cell.ions[\"pool\"].trainable(**{field: braincell.trainable.scale(name=\"theta\")})\n", + " cell.init_state()\n", + " states = cell.trainables.parameters().states()\n", + " assert cell.n_cv == 1 and len(states) == 1\n", + " predict = brainstate.transform.jit(lambda: simulate(cell, observable))\n", + " initial = np.asarray(predict())\n", + "\n", + " def loss():\n", + " return jnp.mean((simulate(cell, observable) - target) ** 2)\n", + "\n", + " gradient = brainstate.transform.grad(loss, grad_states=states, return_value=True)\n", + " optimizer = braintools.optim.Adam(lr=LR)\n", + " optimizer.register_trainable_weights(states)\n", + "\n", + " @brainstate.transform.jit\n", + " def optimize():\n", + " def step(_):\n", + " gradients, value = gradient()\n", + " optimizer.update(gradients)\n", + " return value\n", + " return brainstate.transform.for_loop(step, jnp.arange(EPOCHS))\n", + "\n", + " history = np.asarray(optimize())\n", + " fitted = np.asarray(predict())\n", + " initial_mse = float(np.mean((initial - np.asarray(target)) ** 2))\n", + " final_mse = float(np.mean((fitted - np.asarray(target)) ** 2))\n", + " fitted_value = cell.ions[\"pool\"].get(field)[0]\n", + " elapsed = perf_counter() - started\n", + " assert np.isfinite(history).all() and np.isfinite(fitted).all()\n", + " assert final_mse <= 0.1 * initial_mse, (field, initial_mse, final_mse)\n", + " if voltage:\n", + " assert spike_count(target) >= 1 and spike_count(fitted) >= 1\n", + "\n", + " times = np.arange(STEPS + 1) * float(DT / u.ms)\n", + " fig, axes = plt.subplots(1, 2, figsize=(10, 3))\n", + " axes[0].plot(times, target, color=\"black\", label=\"Target\")\n", + " axes[0].plot(times, initial, color=\"#9a5835\", alpha=0.7, label=\"Initial\")\n", + " axes[0].plot(times, fitted, \"--\", color=\"#138677\", label=\"Fitted\")\n", + " axes[0].set(xlabel=\"Time (ms)\", ylabel=\"Voltage (mV)\" if voltage else \"Concentration (uM)\",\n", + " title=f\"{field}: {observable}\")\n", + " axes[0].legend()\n", + " axes[1].semilogy(np.arange(EPOCHS), history)\n", + " axes[1].set(xlabel=\"Adam update\", ylabel=\"MSE\")\n", + " fig.tight_layout()\n", + " plt.show()\n", + " unit = u.get_unit(target_value)\n", + " result = {\n", + " \"parameter\": f\"{model}.{field}\", \"initial\": float(initial_value / unit),\n", + " \"target\": float(target_value / unit), \"fitted\": float(fitted_value / unit),\n", + " \"initial_mse\": initial_mse, \"final_mse\": final_mse, \"seconds\": elapsed,\n", + " }\n", + " print(result)\n", + " return result\n", + "\n", + "results = []\n" + ] + }, + { + "cell_type": "markdown", + "id": "5ad0bc59", + "metadata": {}, + "source": [ + "## 1. 固定反转电位\n", + "\n", + "E 独立于 Ci/Co。改变 E 影响钠电流,不通过 Nernst 公式。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b771005f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:01.187719Z", + "iopub.status.busy": "2026-09-07T05:09:01.187581Z", + "iopub.status.idle": "2026-09-07T05:09:11.911800Z", + "shell.execute_reply": "2026-09-07T05:09:11.910769Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'parameter': 'SodiumFixed.E', 'initial': 45.0, 'target': 50.0, 'fitted': 49.97644805908203, 'initial_mse': 14.18368148803711, 'final_mse': 8.104076550807804e-05, 'seconds': 10.09541904553771}\n" + ] + } + ], + "source": [ + "results.append(fit_one(\"SodiumFixed\", \"E\", 45 * u.mV, 50 * u.mV, \"V\"))" + ] + }, + { + "cell_type": "markdown", + "id": "7f085447", + "metadata": {}, + "source": [ + "## 2. Nernst 温度\n", + "\n", + "这里只学习 Ion 的 temp;Channel 的温度固定,不共享同一个训练参数。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "536080fd", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:11.916747Z", + "iopub.status.busy": "2026-09-07T05:09:11.916441Z", + "iopub.status.idle": "2026-09-07T05:09:19.279654Z", + "shell.execute_reply": "2026-09-07T05:09:19.278861Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAEiCAYAAADklbFjAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsypJREFUeJzs3Xd4VGX2wPHv9GQmvVMSQu+EagBFQUFABQUbduz609UVXVd2XRR1ZW3o6qK4roodrNhRihTpxdA7oaaH9DKTKb8/Zu5k0jNpk4TzeZ55dGZu7rwZIJlzz3nPUTkcDgdCCCGEEEIIIYRocmpfL0AIIYQQQgghhGivJOgWQgghhBBCCCGaiQTdQgghhBBCCCFEM5GgWwghhBBCCCGEaCYSdAshhBBCCCGEEM1Egm4hhBBCCCGEEKKZSNAthBBCCCGEEEI0Ewm6hRBCCCGEEEKIZiJBtxBCCCGEEEII0Uwk6BZCCCGEEEIIIZqJBN1CtDMbNmzg6aefJjc319dLaTI7duxApVLx5JNP1njM4cOHUalUzJo1qwVXJoQQQgghRO0k6BaindmwYQNz585tV0H30KFD6dOnD5999lmNx3z66acA3HzzzS21LCGEEEIIIeokQbcQok246aabOHbsGJs2bar2+c8++4w+ffowdOjQFl6ZEEIIIYQQNZOgW4h25Omnn+Yvf/kLAF27dkWlUqFSqTh+/Lj7mI8//phhw4bh7+9PWFgYM2bM4NSpUxXOM3bsWAYMGMCuXbu46KKLMBqN9OjRgy+//BKANWvWkJiYiL+/P71792bFihVV1qFSqThw4ADXXXcdQUFBhIeH8/DDD1NaWlrh2KysLA4cOEBxcXGt39tNN90ElGe0PW3fvp2DBw+6jxFCCCGEEKK1kKBbiHZk+vTp3HDDDQC8+uqrfPTRR3z00UdERkYC8M9//pNbb72Vnj17Mn/+fP785z+zcuVKLrzwwirl6Dk5OVxxxRUkJiby4osvYjAYmDFjBkuWLGHGjBlcdtll/Otf/6KoqIhrrrmGgoKCKuu57rrrKC0tZd68eVx22WW8/vrr3HPPPRWO+c9//kPfvn3ZsmVLrd9b165dGT16NJ9//jk2m63Cc0ogfuONN3r1fgkhhBBCCNHctL5egBCi6QwaNIihQ4fy2WefcdVVVxEfH+9+7sSJEzz11FM899xz/O1vf3M/Pn36dIYMGcKbb75Z4fGUlBQ+/fRTdxA/YcIE+vTpw4033siGDRtITEwEoG/fvkycOJGvvvqKmTNnVlhP165d+fbbbwF44IEHCAoK4s033+Sxxx5j0KBBXn9/N910Ew888AArV67k0ksvBcBut7NkyRJGjRpFt27dvD6nEEIIIYQQzUky3UKcI77++mvsdjvXXXcdWVlZ7ltMTAw9e/bkt99+q3B8QEAAM2bMcN/v3bs3ISEh9O3b1x1wA+7/P3bsWJXXfOCBByrc/9Of/gTATz/95H7s6aefxuFwMHbs2Dq/h+uvvx6dTlehxHzNmjWcOXNGSsuFEEIIIUSrJJluIc4Rhw8fxuFw0LNnz2qf1+l0Fe537twZlUpV4bHg4GBiY2OrPAbOcvTKKr9W9+7dUavVFfaYeyM8PJyJEyfyzTffsHDhQvz8/Pj000/RarVcd911DTqnEEIIIYQQzUmCbiHOEXa7HZVKxc8//4xGo6nyfEBAQIX71R1T2+MOh6PONVQO4hvi5ptv5ocffuCHH35g6tSpfPXVV1x66aXufetCCCGEEEK0JhJ0C9HO1BTYdu/eHYfDQdeuXenVq1eLrOXw4cN07drVff/IkSPY7fYKe829NXXqVAIDA/n000/R6XTk5ORIabkQQgghhGi1ZE+3EO2MyWQCqNKNfPr06Wg0GubOnVslK+1wOMjOzm7ytSxYsKDC/TfeeAOAyZMnux+r78gwhb+/P9OmTeOnn37irbfewmQyceWVVzbdooUQQgghhGhCEnQL0c4MGzYMgL///e989NFHLF68mKKiIrp3785zzz3Hp59+ygUXXMBLL73EwoUL+etf/0rv3r15//33m3wtycnJTJ06lTfffJNbbrmFN998kxtvvJGEhAT3MfUdGebp5ptvxmw288svv3DVVVe5LzQIIYQQQgjR2kh5uRDtzIgRI3j22WdZuHAhy5Ytw263k5ycjMlk4oknnqBXr168+uqrzJ07F4DY2FguvfRSpk6d2uRrWbJkCXPmzOGJJ55Aq9Xy4IMP8tJLLzX6vBdffDEdOnQgNTVVSsuFEEIIIUSrpnLUp/uREEJ44emnn2bu3LlkZmYSERHh6+UIIYQQQgjhM1JeLoQQQgghhBBCNBMJuoUQQgghhBBCiGYiQbcQQgghfGratGmEhoZyzTXX+HopQgghRJOTPd1CCCGE8KnVq1dTUFDABx98wJdffunr5QghhBBNSjLdQgghhPCpsWPHEhgY6OtlCCGEEM1Cgm4hhBBCNNjatWuZMmUKHTt2RKVSsXTp0irHLFiwgPj4ePz8/EhMTGTLli0tv1AhhBDCR2ROtwe73U5KSgqBgYGoVCpfL0cIIcQ5xOFwUFBQQMeOHVGr28418aKiIhISErjjjjuYPn16leeXLFnCrFmzWLhwIYmJibz22mtMnDiRgwcPEhUV1ejXl9/dQgghfMGb39sSdHtISUkhNjbW18sQQghxDjt16hSdO3f29TLqbfLkyUyePLnG5+fPn8/dd9/N7bffDsDChQv58ccfee+993jiiSca/fryu1sIIYQv1ef3tgTdHpT9ZKdOnSIoKMjHqxFCCHEuyc/PJzY2tl3tbbZYLGzfvp3Zs2e7H1Or1YwfP56NGzc26Jxmsxmz2ey+r/SDld/dQgghWpI3v7cl6PaglKUFBQXJL24hhBA+0Z5KpLOysrDZbERHR1d4PDo6mgMHDrjvjx8/np07d1JUVETnzp354osvGDVqVLXnnDdvHnPnzq3yuPzuFkII4Qv1+b0tQbcQQgghfGrFihX1Pnb27NnMmjXLfV/JNAghhBCtlQTdQgghhGgWERERaDQa0tPTKzyenp5OTExMg85pMBgwGAxNsTwhhBCiRbSd9qhCCCGEaFP0ej3Dhg1j5cqV7sfsdjsrV66ssXxcCCGEaG8k0y2EEAKbzUZZWZmvl9Gu6XQ6NBqNr5fR5AoLCzly5Ij7fnJyMklJSYSFhREXF8esWbO47bbbGD58OOeddx6vvfYaRUVF7m7mQgghRHsnQbcQQpzDHA4HaWlp5Obm+nop54SQkBBiYmLaVbO0bdu2MW7cOPd9Zb/1bbfdxqJFi7j++uvJzMxkzpw5pKWlMXjwYJYtW1aluZoQQgjRXqkcyqwNQX5+PsHBweTl5UkHVNFmHDp1kgU/fsuYvv255qKLfb0c0cakpqaSm5tLVFQURqOxXQWDrYnD4aC4uJiMjAxCQkLo0KFDlWPkd1DDNNX7diyzkPVHs4kKNDCxf8P2mwshhDh3ePP7RzLdQrRh+48nM+a/8yHQxCcbzvCflctY8dTzaDXyT1vUzWazuQPu8PBwXy+n3fP39wcgIyODqKiodllq3pbtPJ3LP5buYUzPCAm6hRBCNClppCZEGzbrw/9BoAmH3Q7AHn81o//xF+yu+0LURtnDbTQafbySc4fyXsv++dbHqHderCwyW328EiGEEO2NBN1CtGF/FOYAcLNfADPMJaisVvasXM1bb73l45WJtkRKyluOvNetl1HvrDwotth8vBIhhBDtjdSgCtFGpWVnUxYcgAoYVpJBtElPl+Rkkmw5PPnXxxgxYgTnnXeer5cphBBtgjvTbZFMtxBCiKYlmW4h2qi9u3dR8PHX9N6/j2iDnrC4HowYMJB+3eO4vG80d959F/lFRb5ephBCtAkmgzPTXSKZbiGEEE1Mgm4h2qhTx09gP3iMCUXZAPSbcA2JNz3E8FFjoEscZy+/gOte+aePVylE01KpVLXenn76aZ+ubenSpT57fdE4Jvee7rqDbpvdwe+Hs/jtQEZzL0sIIUQ7IOXlQrRRJ0+eJCLAgNHfH52/CWNoJCqVilE33M+y9HTMgUFss5Wxfs8uzh8wyNfLFaJJpKamuv9/yZIlzJkzh4MHD7ofCwgI8Op8FosFvV7fZOsTbZeyp7ukzIbN7kCjrrr//tTZYr7Yfpovt50iJa8UgE/vTmR094gWXasQQoi2RTLdQrRR2zJSMA3sTVlAIEFRndwNmoKiO/PIjDuJzcsBjYb7P3rbxysVounExMS4b8HBwahUKvf9oqIibrrpJqKjowkICGDEiBGsWLGiwtfHx8fz7LPPcuuttxIUFMQ999wDwDvvvENsbCxGo5Fp06Yxf/58QkJCKnztt99+y9ChQ/Hz86Nbt27MnTsXq9XqPi/AtGnTUKlU7vui7TAZyvMQJWVVs92L1icz5sXfeH3lYVLySlF64n2y6WRLLVEIIUQbJZluIdqoPX4q8q+YxMncTPxDKs5Yjh00knvW/Mo/CotJCQlk0S8/MXPiZT5aqWgrHA4HxcXFPnlto9HY6M7ehYWFXHbZZfzzn//EYDDw4YcfMmXKFA4ePEhcXJz7uJdffpk5c+bw1FNPAbB+/Xruu+8+XnjhBaZOncqKFSv4xz/+UeHc69at49Zbb+X1119nzJgxHD161B2wP/XUU2zdupWoqCjef/99Jk2aJDO42yCDVo1aBXYHFJutBBgqfkRa6SolHxwbwh0XdCUuzMhVC9bzy940MgpKiQr088WyhRBCtAESdAvRRpW46lTCNCr8g8MqPKdSqbj99odZ/Oxj7A2N4NlV33PbpZNlXJGoVXFxsdfl2U2lsLAQk8nUqHMkJCSQkJDgvv/ss8/yzTff8N133/Hggw+6H7/44ot59NFH3ff//ve/M3nyZB577DEAevXqxYYNG/jhhx/cx8ydO5cnnniC2267DYBu3brx7LPP8vjjj/PUU08RGRkJQEhICDExMY36PoRvqFQqjHothWYrRdU0U1Pmd993UXcmDXD+GQ+NC2HHyVy+2HaaB8b1aNH1CiGEaDukvFyINsqqdV4zC9aoqwTdAH6BITxx/ljUdht5IcEsWPplSy9RiBZVWFjIY489Rt++fQkJCSEgIID9+/dz8mTF8t/hw4dXuH/w4MEq4/Uq39+5cyfPPPMMAQEB7tvdd99Namqqz6oDRNNT9nUrAbYnpcGaZwb8psQuAHy6+SQ2u6MFViiEEKItkky3EG2U3aBDBYRqtRhMQdUec+nl1zNw/Wp2hkbw4crveXDatS27SNGmGI1GCgsLffbajfXYY4+xfPlyXn75ZXr06IG/vz/XXHMNFoulwnENyagXFhYyd+5cpk+fXuU5Pz8pK24vTAYtFJir3dNd6ArEldFiAJcP6sAzP+zjTG4Jaw9lMq5PlNev6XA4SM93vmbXiMZVewghhGidJOgWog3Kyc9H5eq4HKrTojdWXxKs0ep4YsJU3nnzBUIz0vl99W9cMHZcSy5VtCEqlarRJd6+tH79embOnMm0adMAZ6B8/PjxOr+ud+/ebN26tcJjle8PHTqUgwcP0qNHzSXEOp0Om01mPLdltWW6laDbM9Ptp9NwzbDOvPt7Mp9sPlHvoDstr5SFa46yLyWfg+kF5JWUAfCfG4dwxaCOjf02hBBCtDJSXi5EG3QsNQUAld1OoEaL3r/mQGn8+CkMCAhCp1Hz6b9lbrdov3r27MnXX39NUlISO3fu5MYbb8Rut9f5dX/605/46aefmD9/PocPH+btt9/m559/rtADYc6cOXz44YfMnTuXvXv3sn//fhYvXsyTTz7pPiY+Pp6VK1eSlpZGTk5Os3yPonkps7qLK+3pdjgc7kDcVKnB2o2JziZ9qw5kcCa3pF6v88KyAyzacJwtx8+6A26AOd/uJbvQ3OD1CyGEaJ0k6BaiDTqRkQaAwVqGWq1C51dzaa5KpeKy2x8CwHz2NBu2bW6RNQrR0ubPn09oaCijR49mypQpTJw4kaFDh9b5deeffz4LFy5k/vz5JCQksGzZMh555JEKZeMTJ07khx9+4Ndff2XEiBGMHDmSV199lS5duriPeeWVV1i+fDmxsbEMGTKkWb5H0byMhuoz3WarHatrz3bloLt7ZACjuoVjd8CSLXWPDysyW1m2x/kz/MnL+/LTQ2PYO3cifWICOVtk4anv9jbFtyKEEKIVkfJyIdogXakFvvmRUX06oztvOCp17dfPho2bxD+XJLCxcxzHvvyA34YnttBKhWg+M2fOZObMme778fHxrFq1qsIxDzzwQIX7NZWb33333dx9990V7lcuJZ84cSITJ06scT1TpkxhypQp9Vy9aI1qynR7BuEmfdVxcDcmxrHxWDaLt57iz+N7oVbXPCli+b50SspsdAk3cucFXd0VFS9dk8BVb67nh12pXDEozd0hXQghRNsnmW4h2qJSM36HjtInN7vW0nKFSqVi1HkXYdXq2GcyknzqRAssUoi24+WXX2bnzp0cOXKEN954gw8++MA9HkycO/yVPd2WiplupXO5v06DVlP1o9Ol/aNRqSCjwEx2kaXK856WJp0B4KrBnSpsYRjYOZh7L+wGwD++3UNuce3nEUII0XZI0C1EG5Sfn49eq0aj1dZaWu7p0Zn3E1xYgE2rZfZ7/2nmFQrRtmzZsoUJEyYwcOBAFi5cyOuvv85dd93l62WJFqZksYvNFTPdhTXs51YYtBrCTc7mlpkFNe/Jziwws+5wFgBXDelU5fmHLulJ90gTmQVmnv1hv/ffAPD1jtNc+uoaPtx4HLuMMRNCiFahzQTdTz/9NCqVqsKtT58+7udLS0t54IEHCA8PJyAggKuvvpr09HQfrliI5pOcexZH7x6cDQxCq6/fuCKNRsMlgaEAbLCZKTWXNucShWhTPv/8czIyMigpKWHv3r3cd999vl6S8AGjoYbycovSubxqabkiIsAAQGYtjdB+2JWCze4gITak2vFgfjoNL16TgEoFX+04zZGMAq/W/9uBDB77YieH0guZ8+1ebvzfJk6dlTnyQgjha20m6Abo378/qamp7tvvv//ufu6RRx7h+++/54svvmDNmjWkpKRUO09ViPZgZ2EOBVdM4o/ozmgMhnp/3fP3zkJvMVPsb+SF9xc04wqFEKLtcWe6K5WX15XpBogKcl4ArS3TvTTJOXli2uCax4IN6xLKBT0iAFhzKKseq3bafTqPBz7dgd0BiV3D8Ndp2HTsLBNfW8tHm07gcEjWWwghfKVNBd1arZaYmBj3LSLC+UspLy+Pd999l/nz53PxxRczbNgw3n//fTZs2MCmTZt8vGohml6+K0vth6PemW6AiNAw+pU69wl+l3yoWdYmhBBtldHVSK2ohkZqtQXdkUqmu4ag+1hmITtP5aJRq7giofZZ3GN6Oj/frD9Sv6D7dE4xd3ywlWKLjTE9I/j4rkSW/XkM53UNo9hi4x9L9/Czq2O6EEKIltemgu7Dhw/TsWNHunXrxk033cTJk87RHNu3b6esrIzx48e7j+3Tpw9xcXFs3LixxvOZzWby8/Mr3IRoC4oszsDZz+FAq69/phvgz5ddA8CpoGC2bd3Q5GsTQoi2ymRQ9nRXbqSmlJfXEnQH1h50K1nuMT0j3KXoNTnflenedCwbi7X2WfP5pWXc/v5WMgvM9IkJ5M2bhqLTqOkSbmLx3SOZOToegFeXH5I93kII4SNtJuhOTExk0aJFLFu2jLfeeovk5GTGjBlDQUEBaWlp6PV6QkJCKnxNdHQ0aWk1X9mdN28ewcHB7ltsbGwzfxdCNI1iaxkA/irQGuqf6Qa4Ysw4hh46yOVrfmHtZ+82x/KEEKJNKs90Vy4vd2a+a810u4LujIKq/TIcDgffurqWT6umgVplfWOCCDfpKbbYSDqVW+uxX20/zeGMQqKDDLx/+wgC/XTu59RqFY9M6EWQn5bDGYX8tCe1ztcWQgjR9NpM0D158mSuvfZaBg0axMSJE/npp5/Izc3l888/b/A5Z8+eTV5envt26tSpJlyxEM2n1Ob8QOgHXpWXK24aMRZTaQln9mzDXFzUxKsTQoi2yeje0119eXltjdRqy3QnncrlRHYxRr2GCf2i61yHWq1itCvb/XsdJeZ7zjir9G48rwsdgv2rPB/sr+POC5yjyP694nCjst1Wm51jmYWyP1wIIbzUZoLuykJCQujVqxdHjhwhJiYGi8VCbm5uhWPS09OJiYmp8RwGg4GgoKAKNyHaArPrA09DMt0A1868myIrmEtL+PFTyXYLIQR4ZLprKC836euxp7ua7uU7TuYCcEGPCPdr1GWMEnQfzqz1uIPpzqC7d0xgjcfMPD++0dnudYczmfTvdVz8yhom/3sdn289RWmZre4vFEII0XaD7sLCQo4ePUqHDh0YNmwYOp2OlStXup8/ePAgJ0+eZNSoUT5cpRDNw4oz6DaoVGi83NMNYDQaMQw9n7XDRvHCod2StRDtnkqlYunSpbUeM3PmTK666qp6n/P48eOoVCqSkpIatTbReih7ukss3s3pBogKqjnTfSanBID4asaE1eR8VzO1nafzyC8tq/YYq83OofRCAPrUEnQ3Jtt9MruYuz/cxi3vbuFIhvO1DqQV8PhXuzj/X6tY8NsRbLJXXAghatVmgu7HHnuMNWvWcPz4cTZs2MC0adPQaDTccMMNBAcHc+eddzJr1ix+++03tm/fzu23386oUaMYOXKkr5cuRJMLOnic3ts208VWhkarb9A5Jl5zMylRHTgWHErSH5ubeIVCNC9vA+TU1FQmT54M1Bws//vf/2bRokVNt0jR5tTVvbw+jdQKSq1VMsApuc6gu1NI1fLvmnQK8adbhAmb3cGmo9nVHnM8uxiL1Y6/TkNcmLHW8zUk2/3HyRzGv7qG5fvS0ahV3H5+POseH8ffLutDpxB/sossvPTLQT7YcLze35cQQpyL2kzQffr0aW644QZ69+7NddddR3h4OJs2bSIyMhKAV199lSuuuIKrr76aCy+8kJiYGL7++msfr1qI5qE+mUKX48eIVjnQaHV1f0E1po29hOD8fBxqNa8t/ayJVyhE6xITE4Ohjpn2wcHBVRpyinOLu3t5AxqpBRq0GLTOj1WVs90pec6gu6MXQTeUdzGvaXTYwbQCAHrFBKJWq2o9V0Oy3Z9vO4XFamdwbAjLHh7DU1P6Extm5J4Lu7PmL2P5y8TeAMxffoiM/KoN5LxxMruYTzeflJJ1IUS71GaC7sWLF5OSkoLZbOb06dMsXryY7t27u5/38/NjwYIFnD17lqKiIr7++uta93ML0ZaVlJSg0ajQaDSodQ0LugFGBjk/0G2xlVFWWtJUyxOiRY0dO5aHHnqIxx9/nLCwMGJiYnj66acrHONZXt61a1cAhgwZgkqlYuzYsUDV7PmyZcu44IILCAkJITw8nCuuuIKjR4+2wHckfEXJdJfZHBVGdZXP6a65kZpKpfLoYF4p6M5Vgm7venAoQfe6GoNu537uPtE1l5Z7uv2CePx1Gg5nFHIks7DO47cdzwHg/8Z2p2el19Bq1Nx/UXcSYkMoNFv550/767WG6uxPzeeqN9fzt2928/dv9jT4PEII0Vq1maBbCFGuMDyYs9EdsKi1DS4vB/j7DbejstvJDAjiqx8aPglAtA8OhwOrxeyTW2P7CnzwwQeYTCY2b97Miy++yDPPPMPy5curPXbLli0ArFixgtTU1BqrooqKipg1axbbtm1j5cqVqNVqpk2bht1e+9xk0XYp3cuhYrZbGSFWW3k5VN/BvLTMRlahBfCuvBxgVPdw1Co4llnkDtw97Xdluvt0qF/QHeSno1d0AADH6gi6c4stHHbt4R7WJbTaY9RqFc9dOQCVCr5NSmHD0do7rVdnf2o+N/1vM2eLnO/RVztOs/SPM16fRwghWrP6tdAUQrQqlvGj2Wz059LSXNTahv8z7hfflei8fNJCQ/h42wZmXHNbE65StDW2Mgsr/z3bJ699ycPz0DagKaBi0KBBPPXUUwD07NmT//znP6xcuZIJEyZUOVbZlhQeHl5rRdTVV19d4f57771HZGQk+/btY8CAAQ1eq2i9dBo1eo0ai81OkcVGiGubdH0aqUH1HcyVYNmk1xDs711lUrC/jkGdQ0g6lcv6I1lcOzy2wvNKeXltncsr6xYZwM7TeRzNrH1c5I6Tzix3twgT4QE1/9sc2DmYmxO78NGmE/xj6R5+fvhC9Nr65XQ8A+5BnYNJ7BrGO+uSeXLpHobGhRIXXvs+dSGEaCsk0y1EG+TQOLMxfmp1g/d0Ky7v6Qwedhn8yE1PafTahPCFQYMGVbjfoUMHMjIyGnXOw4cPc8MNN9CtWzeCgoKIj48H4OTJk406r6hq2rRphIaGcs011/h6KRiVfd0eY8Pq00gNqu9gnpLr3OvcMcQflar2fdfVGdOz+nndRWYrJ88WA9Anpv4jT7u5OqgfqyPo3n7CGXQPrSHL7emxS3sTEaDnaGYR7/6eXK91HM0srBBwf3RnIn+d1IcR8aEUmq38afEflNkaVlWSW2zhnbXHeGPlYf6z6jALfjvCkq0npcu6EMJnJNMtRBvjcDjAld32U6vR6BpeXg4w+4bb+PrJP9EtK5UVX3/MNfc/3hTLFG2QRqfnkofn+ey1G0NXqbeBSqVqdBn4lClT6NKlC++88w4dO3bEbrczYMAALBZLo84rqnr44Ye54447+OCDD3y9FEx6LbnFZRR7dDAvqkcjNYDIAOee7YpBd8OaqCnO7xHBG6uOsP5IFna7w90w7WC6M8sdGWggzFT/fz/dIl3l5Vm1l5cr+7mH1yPoDjbqmD25L49+sZM3Vh1m5uh4/PU1738HWPDbkQoBt1IF8NqMIUx+bS07T+Uyf/kh/jqpT32+rQpeX3mE99ZXDf4NWg1XDenk9fmEEKKxJNMtRBtTVFKCSuP8p2vQqBtVXg4QEhDI1EIrPU8mc/D35Thkv+o5S6VSodUbfHJrSAawofR6Z4Bis9XcJTk7O5uDBw/y5JNPcskll9C3b19ycnJaaonnnLFjxxIYWP8S6eak7OtW9nE7HA73/9fWSA0893SXd/I+rYwLC21Y0D00LpRAPy1ZhRa2Hj/rflwpLa9tPnd1ukWWZ7pr6qVQZrOz83QuAMPj6w66AaYP7USgQUuxxcaZavafeyots/HLnjQAnprSr0LZfacQf1642lm5snDNUff36Y01h5xVLuP7RjFjRCyDY0MAWH2wcdUvQgjRUBJ0C9HG5BaWfwDxV6tRN7K8HGD6zHsptdrITDnNqf1JjT6fEK1ZVFQU/v7+LFu2jPT0dPLy8qocExoaSnh4OP/97385cuQIq1atYtasWT5Yre+tXbuWKVOm0LFjxwpd4D0tWLCA+Ph4/Pz8SExMdDera4uMrmx2sSu7XWyxocSmDWmk1pAZ3Z70WjWT+jt7D/ywq3y+dkOD7q4RJlQqyCspczcvq2xvSj6lZXZCjDq6RQTU67ye3duzCs21HrtyfwZFFhudQ/0ZGlc1qJ88sAPj+0bhcMDSJO+aqqXmlXA0swi1Cl65bjD/unqQO1v++5HsRjdtFEKIhpCgW4g2JrewvCTQoNWhVteeeamP0edfQIYuiAMd41j49SeNPp8QrZlWq+X111/n7bffpmPHjlx55ZVVjlGr1SxevJjt27czYMAAHnnkEV566SUfrNb3ioqKSEhIYMGCBdU+v2TJEmbNmsVTTz3Fjh07SEhIYOLEiRX21A8ePJgBAwZUuaWktL4+EqZKmW5lP7daBf66+ma6qysv925cmKcrEjoC8NPuVKyufc77U53jwnp7sZ8bwE+noWOw8wLAsazq93Vvc2XUh8WF1jn/21NEQP2C7m9dgfTUhI41VrlMG9IZgO93pngVKK8/kg3AwM4h7gz60C4h+Os0ZBWaOdCAzLkQQjSW7OkWoo3JK3IG3WqbDZ2x8VlucGYoysaOZ5ufhqy8s8wtLUbnJ11jReu1aNEi9/+vXr26yvOVs7GVP7Tfdddd3HXXXTWeE2D8+PHs27evxvPEx8efE1mzyZMnM3ny5Bqfnz9/PnfffTe33347AAsXLuTHH3/kvffe44knngAgKSmpydZjNpsxm8uDuvz8/CY7N5TP6lb2dLs7l+u1dW6DcAfdhc4xeCqVqjzoDm5YphtgdPdwQo06sossbDp2lvN7hLv3dHub6QZnifmZ3BKOZRYyIj6syvNKE7Vh9SwtV0QEOrduZBXUHHTnFZex+mAmAFcOrnl/9cV9ojDpNZzOKWHHydwax5ZVtt7VcO6CHuHuxwxaDYndwlh9MJPfD2fRt4N3FyqEEKKxJNMtRBujtdnh55UMP7KvSUrLFQ9OngbAicAQtm9a3WTnFUK0XxaLhe3btzN+/Hj3Y2q1mvHjx7Nx48Zmec158+YRHBzsvsXGxtb9RV5w7+k2K5nu+jVRA4gIcAadZTYHeSVl2O0Od/fyhu7pBucos8kDOwDOzG9GgZnc4jLUKugRVb/yb0/dlWZq1XQwdzgcbDuhNFGrGpDXpjzTXXOzwWV7U7HY7PSODqx11Jm/XsOEftGA83uuD4fD4e7yfn6PiArPXeC6v+6I97PEhRCisSToFqKN0VhtaJP20D/1FBpd0wXdl48cjSk/H4dazbsrf26y8woh2q+srCxsNhvR0dEVHo+OjiYtLa3e5xk/fjzXXnstP/30E507d641YJ89ezZ5eXnu26lTpxq8/uoozdKqZLrraKIGzoxqiKsCKaPATFaRGYvNjloF0UENLy8HuGKQM+hetjeN3aedfQi6Rpjwq6PkvTpKM7XqZnWfzikhs8CMTqNiUOdgr85bn/Ly71wB9NTBHes8n3LMD7tS6zXu60hGIZkFZvx06ip7xcf0jARgS3I2pWU1N1EUQojmIEG3EG1MSUkJWrUKjUaDWtN0QTfA8ABnVmOLtYzS/NwmPbcQQtRkxYoVZGZmUlxczOnTpxk1alSNxxoMBoKCgircmlJN5eV1NVFTRAaU7+tWstzRQX7oNI37yJXYNZzIQAN5JWW8s+4Y4N18bk9dlVnd1YwN23bCuZ+7f8dgrwP6ujLdGfmlbDjq3HM9NaHuoPuCHpGEGHVkFZrZdCy7zuOVLPeI+LAqa+8VHUBUoIHSMjs7TjRsEoHD4eCNlYdJfH4FP+xqff0IhBCtlwTdQrQx6QV5EB/L2cDgJs10A/xl2gwAzgQGs3bNsiY9txCi/YmIiECj0ZCenl7h8fT0dGJiYny0qsZRGqkVV2qkVp/ycqjYTK2xM7o9adQqLneVmG9OdgbGDdnPDeWzuk9mF1Nmqzgm0pv53JUp5fU1Zbq/35WKwwHDuoQSG1Z33xC9Vs3kAc6/R98l1R3krq+htBycvUsaU2Jutdn52zd7eGX5IdLzzcz6fKd777sQQtRFgm4h2ph9Z7MovX4a63r0a9I93QAj+w0gODcPVCo+2Li6Sc8thGh/9Ho9w4YNY+XKle7H7HY7K1eurDVb3ZopI8OUvdyFjQi6z+Q0blxYZVMSOlS4X9ue6Np0CPLDT6fGandw6mxxheeUQLK+87k9RdQxMuw7V9fyK+tRWq6Y4sqI/7wnFYvVXuNxZTY7m445L0ZcUE3QDXBBT+fjvx/2LuguLbNx/yc7+GzLSdQq6NchCIvVzr0fbeN0TnHdJxBCnPMk6BaijSk0O8sVdQ47Gp2+yc9/fmRnVHY7qQX5FGan1/0FQoh2rbCwkKSkJHcH8uTkZJKSkjh58iQAs2bN4p133uGDDz5g//793H///RQVFbm7mbc1NWW661teHuXRwfxME2a6AYbEhtIxuHxveEPLy9VqFV0jqjZTyyspc3dFH+ZlEzUoL63PcnVv95ScVcTO03lo1CouG9ihui+vVmLXcKICDeSXWll7KLPG43adzqXQbCXEqKNfDd3JlWB8T0oeOTXMKK+syGzllnc3s3xfOnqtmjdvGsYX942ib4cgsgot3PXBNveFGSGEqIkE3UK0MUWuoFtrd6DWNP3Uv6euv4Vh33xJv53bSPpNGqoJca7btm0bQ4YMYciQIYAzyB4yZAhz5swB4Prrr+fll19mzpw5DB48mKSkJJYtW1aluVpboezpLnLt6S7yopEalGe6M/JL3eXlnRoxo9uTWq1yz+w26jV0bkRHdKWZmue+7s3HsnE4oEu40f19eCPcVV5eWmZ3v3+Kra7Z38O7hLr3fteHRq3iikHO7/m7WrqY/37Yuef7/O4RNc4Wjwryo3d0IA4HrD9av2z3x5tOsPV4DoF+Wj664zwmDYjBZNDy7m3DiQgwcCCtgIc/+6Nejd6EEOcuCbqFaGOKXfNpdTRPprt7bBxhUfEA7Fj1wzkxh1gIUbOxY8ficDiq3Dznmj/44IOcOHECs9nM5s2bSUxM9N2CG0kZGVbsCrYLvRgZBhVndafkNW2mG+DaYZ3x12kY1zuqxuCyProrzdQ8Mt1fbj8NwCV9GnbBxKjXut+/yrO6lVJ7Jdj3hlJWv3xfeo0l5rXt5/bkbYn5ygMZADx2aW8Su5XP/u4Y4s87tw5Dr1Wz8kAGv+6tf7d+IcS5R4JuIdqY4jJnSZzO4UDTxHu6FeOvvhGrzc6R1DOcPX2sWV5DCCFaI/ee7kqZ7gB9fbuXO7PaFfZ0NyIjXVnP6EA2zr6YV68f3KjzdKs0qzsjv9QdYN5wXsNnn9c0Nqw86+/9ezE4NgS9Rk1JmY30/NIqzxeZrew46dyLXtN+boUSdK87nFXnReW84jL3HveL+0RVeX5IXCi3jeoCwK/7ZDuWEKJmEnQL0caUlJUBoHPQ5I3UFNOmX82vQ0fz6XkX8c2yb5rlNYRoDmPHjuXPf/5zi73eokWLCAkJabHXE81P2dNd4trTXej6b4Cfd5nu0zkl5BQ7f143ZaYbIMSoR69t3Ee4yuXlX+44jc3uYFiXUHpGN6xBG9TcwbwxWX+VSkVUkKtsv6Bq0L31+FmsdgedQ/2JC6+9K3pi1zD0GjVncks4ebb2JmjrjmRiszvoGRVQY7f1Cf2c3dVXHcio0gm+Lr8fzmL4c8u5+8Nt0gldiHZOgm4h2phSqyvopvky3aGhoRj9nB/Ivj28H7tNmsSI1mXmzJmoVKoqtxdffJFnn33WfVx8fDyvvfZaha+VQFnUpuY93d4F3cqc70CDliC/5vlZ3RjKrO6sQgt5xWUs2XoKgOtHNDzLDeWZ7sxKs7qVrH9DL0DEBDkrCNLyqnZGP5LhvHCQEBtS53mMei19OjgvKuxPza/12FWuzP+4arLciqFxIYQadeSVlLnHrdWHxWrnyaW7ySq0sHxfOle/tYFrF25g5X7JmAvRHknQLUQbE1VkIXrLFnoX5zfLnm7F5X0SANjlH0Bm8sFmex0hGmrSpEmkpqZWuA0bNozAwIZn6YRQGqYpe7q97V4e4q9Dpynfa93UWe6mEuinc3da/3TLSU5kFxNg0HLFoPp3Fq+Oe2yYx55uu91BSp4zQ93Q8WnRrqC7uvLyNNe5PTu716aXK5N/KL2wxmPsdgdrDjq7pY/tHVnjcVqNmotde+BXeBEwf7TpBMezi4kIMHD98Fj0GjVbj+dw5wfbWLQ+ud7nEUK0DRJ0C9HGBBYUE7NvH71KS1Brm757ueKv196AuqyMIj9/Pv3xq2Z7HSEaymAwEBMTU+F2ySWXuMvLx44dy4kTJ3jkkUfcmfDVq1dz++23k5eX537s6aefBsBsNvPYY4/RqVMnTCYTiYmJrF69usJrLlq0iLi4OIxGI9OmTSM7O7tlv2nR7JRMd3GZDbvd4XUjNbVaVaE7d1Pu525qSon5m6uPADB1cEf3999Q1e3pzioyY7HaUasgpp6BcWVKeXl1QXeq67GY4Pq9172infvZlfFo1dl9Jo/sIgsBBi0j4msfnzahnzMTvnxfer2aj+YWW3h95WEAHr20Fy9cM4h1fx3HTYlxALz0y0FSXeX4Qoj2oUFB98mTJ1m3bh2//PILO3bswGyuWuojhGgeJSUlaNUq1BoNGm3zZbrDgoLpVOT8ILPszAmsFvl3fq4oslhqvCnbG+pzrNJ/oK5jm8vXX39N586deeaZZ9yZ8NGjR/Paa68RFBTkfuyxxx4DnB24N27cyOLFi9m1axfXXnstkyZN4vBh54fjzZs3c+edd/Lggw+SlJTEuHHjeO6555pt/cI3lEy3wwGlVptHprt+I8OACuO2OjbRuLDmoDRTKyh1fo8zGllaDhBZzZ7ulFzn75LoID90moble2KaIdN9uJagWyktH9Mzos41j+kZiV6r5uTZYg5n1Jw9V7yx6gh5JWX0jg7kuuHO9zw6yI9nrxzA0LgQiiw2nvthf72+FyFE21Dvy5nHjx/nrbfeYvHixZw+fbrClTy9Xs+YMWO45557uPrqq1Grmz6BPm/ePL7++msOHDiAv78/o0eP5oUXXqB3797uY8aOHcuaNWsqfN29997LwoULm3w9QvhKlt1KaWQEpTpdszVSU1ydcB6vndjPXmMgKYd2ETdgRLO+nmgdurwyp8bnxnfvzeLrbnff7/v6sxRXCq4Vo+O68t1N97rvD33zBbJLiqoclzX7Xw1a5w8//EBAQID7/uTJkys8HxYWhkajITAwkJiYGPfjwcHBqFSqCo+dPHmS999/n5MnT9Kxo3Mm8GOPPcayZct4//33ef755/n3v//NpEmTePzxxwHo1asXGzZsYNmyZQ1av2id/LQaVCpn0F1ktnm9pxsgMsAz6G7Fme6I8vFd/ToEMbBTcKPPWZ7pLr+gpnQub8x7UV5eXvUCsBJ01zeLrgTdxzKLsFjt1TalW32w7v3cCpNBy/ndw/ntYCbL96W7z1+d41lFfLjxOAB/v7wvGo+xb2q1iueuGsiU//zOj7tTue5QJhf1qrm0vSa5xRZeXX6Ib/44w0OX9OSuMd28PocQomnVKzp+6KGHSEhIIDk5meeee459+/aRl5eHxWIhLS2Nn376iQsuuIA5c+YwaNAgtm7d2uQLXbNmDQ888ACbNm1i+fLllJWVcemll1JUVPED3N13311hf9+LL77Y5GsRwpcORQWxd/Ll7DMFodE1b9D952nXojGbKTX48dHP0sVctC7jxo0jKSnJfXv99dcbfK7du3djs9no1asXAQEB7tuaNWs4evQoAPv3768yf3rUqFGN+h5E66NWq/DXufZ1W6wUKkG3F2XXnpnuhu5hbgndI8svWs04LxaVquFzvxXuPd0eme7GNlGDmvd02+wO92Md6lle3iHYj0CDFqvdwfHsqhcCMwvM7DydB8DYega94/vVb1/3v34+QJnNwUW9IrmwmnP36xjEzNHxAMz5dg+lZbZ6vT4434uPN51g3Mur+WDjCfJLrTz3434+33aq3ucQQjSPev0GMZlMHDt2jPDw8CrPRUVFcfHFF3PxxRfz1FNPsWzZMk6dOsWIEU2bEaucSVi0aBFRUVFs376dCy+80P240WiskL0Qor0pw1llolepmj3THeBvZFBeCfrUXTgMWiwlRej9TXV/oWjTTjz6TI3PeWZlAPY/9I8aj1VX+gC/4//+2riFVWIymejRo0eTnKuwsBCNRsP27dvRaCqWEXtm08W5wajXUmyxUVBqxWx1joGqbyM1aDtBd++YQNQq0GvVXJnQqUnO6c50ezRSO9OIGd2K6Br2dGcXmrHaHWjUqgrve21UKhU9owPYcTKXg2kFVTLTaw45G6gN6BREVFD9sufj+0bz92/2kHQql4yCUqICq37d3pQ8lu1NQ61yZrlr8siEXvywK4UT2cW8tfooj0zoVefr5xWXcdO7m9hzxtmRvVd0AAM7hfDVjtPM/no3YUa9+8KAEKLl1SvTPW/evGoD7upMmjSJ6dOnN2pR9ZGX57wCGRZWsbnFJ598QkREBAMGDGD27NkUF9c8g9FsNpOfn1/hJkRrpwzvMqhotpFhnv56+TUEHj7C2TOnObNvR7O/nvA9k15f482v0t+52o7119Xv2Oak1+ux2Wx1PjZkyBBsNhsZGRn06NGjwk25kNu3b182b95c4es2bdrUrOsXvqHs6/acCe1NeXlUYNsoL+8Y4s9/bxnOx3cmEmxsmt8nypzuIouNEtfYtPKgu+H725VMd5HF5q4+AEh1lZZHBRqqXBSsTe8YpYN51X3dv7lKyy/uXXdpuef6EjoH43DAqv0Z1R6jzOK+sFdkrSXoAQYtc67oD8Bbq4+637/afLb1JHvO5BPop+XpKf346aExvHztIK4e2hmb3cEDn+5g+4mz9f5+hBBNq96br4cPH87ChQtbRWBqt9v585//zPnnn8+AAQPcj9944418/PHH/Pbbb8yePZuPPvqIm2++ucbzzJs3j+DgYPctNrbxDUSEaG4212cKQwtkusFZwpthUWOxlLF52dJmfz0hmlJ8fDxr167lzJkzZGVluR8rLCxk5cqVZGVlUVxcTK9evbjpppu49dZb+frrr0lOTmbLli3MmzePH3/8EXButVq2bBkvv/wyhw8f5j//+Y/s526nlA7eGa79w3qNutp9vzVRMq4atapCAN4aje8XzfA6unN7I8CgxeB6r5QSc2VPd2M6uZsMWgJdFz6UPdxQHnR72xW9Z1T1QXeZzc5aV6Z7bD32c3uaUEeJ+f5U52v17xhU57kuGxhDQmwIFpud9Uey6jz+592pAPx1Uh9mnt8VrUaNSqXiX1cP5OI+UZitdm5/fyunztacjBJCNJ96/wZJSEjg8ccfp0OHDtxyyy1Vxqi0pAceeIA9e/awePHiCo/fc889TJw4kYEDB3LTTTfx4Ycf8s0337j341U2e/Zs8vLy3LdTp2TPi2j97K7/GlSqZp3TrdBoNPQcO4n9XXvySWYGpfm5zf6aQjSVZ555huPHj9O9e3ciI537J0ePHs19993H9ddfT2RkpLv3x/vvv8+tt97Ko48+Su/evbnqqqvYunUrcXHOMT4jR47knXfe4d///jcJCQn8+uuvPPnkkz773kTzMemVTLczaDR50bkcyrPbsaH+aBvYrbutUqnKR6ZlVgq6G5v1V8aGZXiUmKe5Rmt18DLoLs90V+w2vv1EDgWlVsJMehI6h3h1TqV8e93hLHeW39OBNGfiqk9M3UG3SqXivPhQAPaeyav12NM5xew8nYdaBRP7V9xiqdOoWXDjUAZ2Cia/1Mq3SWfq9b0IIZpWvWul3n33Xd544w0+//xzFi1axCWXXELXrl254447uO222+jUqWn2AtXlwQcf5IcffmDt2rV07ty51mOVhjdHjhyhe/fuVZ43GAwYDK37CrQQldlc0wEManWzzun2NGLyZXy1cz1aq5XDSRsZeOHkur9IiGa0aNGiah+vfEF45MiR7Ny5s8pxb731Fm+99VaFx3Q6HXPnzmXu3Lk1vu4dd9zBHXfcUeGxRx99tH6LFm2G0ZVRVcrLvSktBxjYKZg5V/SrV0azPYoINHAmt4SsAjPFFis5xc4JB40NumOC/TiaWUS6R9m/e0Z3kHfn7uma1X0iu4jSMht+ruZ5K11Z6rG9I70qVwfoHR1Ih2A/UvNK2Xk6l5Hdyrdm2u0ODqY5M919O9RcWu6pf0dnN/k9KbVXmS7bkwbAeV3Dqt3X7q/XMDWhI7vP5LG3jnO1hF/2ptEtwkTPWkrshWhvvLr8ajQamTlzJqtXr+bQoUPMmDGDt99+m/j4eC6//HK+/vrr5lonDoeDBx98kG+++YZVq1bRtWvXOr8mKSkJgA4dOjTbuoRoaQ5X1sS5p7v5M90AMydORldUhFWrZdEqKacVQrRv7ky3q7zcmyZq4MxS3nFBVxK71a8fTntTPqvb4s5yB/ppCfJr3JaoaFdzsrS88iZtSqm5t5nuyAADoUYddgcccc3WdjgcLN/nDLon9PW+6ZhKpWJwbAgAO0/lVnjuVE4xxRYbeq2a+PD6NSQd0Ml50WZ/aj42u6PG4350lZZfNrDmz7v9Xefak1J71ry5rTqQzr0fbefKBevZUI+yeSHaiwbXPHXv3p3nnnuO48eP89lnn7Fp0yauvfbaplxbBQ888AAff/wxn376KYGBgaSlpZGWlkZJifOH+dGjR3n22WfZvn07x48f57vvvuPWW2/lwgsvZNCgQc22LiFamm3TH/Q7vI9wlapFGqkBaDVa+qqdH2g2lBRTdLb6JjFCCNEe+LuCbqU82ttM97mufFa3mdM5je9croiqZmxYQ/d0OzuYOzOthzOcGeijmUUczy5Gr1EzpgHzsQESlKD7dG6Fxw+4stw9owLqveWga0QA/joNxRYbyVlVR5uBs3T/j5O5qKopLfekZM1PnS0hz1V50NIcDgdvrDoCQLHFxsxFW92VBUK0d43aaLR69WpmzpzJzJkzsdls3H333U21rireeust8vLyGDt2LB06dHDflixZAji70a5YsYJLL72UPn368Oijj3L11Vfz/fffN9uahPCF4t+3MOjIAUI1atTNPKfb030XTwLgWGAIu7eubbHXFUKIlmaq1EhNgm7veAbdKbnOoLgpgu6YasaGNTTTDc5ycICDac5MtxIAjuwe7nV1g6I8010xo3zA1UStPvu5FRq1in6uLQp7a8hQK6Xlw7uEuju8VyfYX0dsmH+t56rJ2SIL85cfYukfZyi2WOv+ghpsOnaWP07mYtCqGds7EovVzr0fbefHXakNPqcQbYXXP1FOnz7NokWLWLRoEceOHWPMmDG8+eabXHvttfj7N99YDIej5rIagNjYWNasWdNsry9Ea2Cz2XDYnb/wNBp1i2W6Aa65cByP/PoN5sAAPv59FYmXXo1K5d1+NyGEaAuMrsZpmQVKebl3jdTOdRHu8nJzkzVRg/KxYUrQ7XA43EG3t5lucM6yBjjs6mCudB2f0Ne7ruWeBnYKRq1yjknznNetNFGr735uRf+OQWw/kcOeM3lcObhq/6Sf99RdWq4Y0DGYU2dL2JOSx+geEfV6/c3Hsnl4cRJprvfcX6fh0v7RXDWkE2N7RXr1OeDN1c4s93XDY5kzpR+Pfr6T73am8KfPdqBRD2PSgJoz9UK0dfXOdH/++edMmjSJrl278tZbb3Hddddx6NAh1qxZw6233tqsAbcQwqmwqAhdpxjyAgJRqTUtMjJMoVarGWhwfljYVGYlP/10i722EEK0JCXTbbHZK9wX9RPhauaVVWBxz5hukqA7WAm6nRdDzhZZsNjsqFS4g1tvKLOyD6YXkF1ods/RvrgB+7kVJoPWPY5sl0e2Wykv9ybTDc5AGai2AVp6finbXGuuT8A6oJOrMduZupup2ewOXl95mBve2URafinx4Ua6hBspKbPxbVIKt7+/lX8tO1Dv72P36TzWHc5Co1Zxz4Xd0GnUvHr9YK4Z1hm7A/67tvpJQ0K0F/UOum+++Wb8/f355ptvOHXqFM8//zw9evRozrUJISrJys/D/75b+XnMBDR6Q4tnmh+aNAW11QpFhRzZuq5FX1s0H7vdXvdBoknIe902GPUVM9tSXu4dz/LyM00wo1uhZLozCkqx2x3u/dwRAQav5qgrlKD7dE4JP+5Oxe6Afh2CGl0KnxDrDG6Vfd3FFivHs517svt4m+lWGqCdyatS9blsTxoOBwyNC6FDcN1rVrrp19VMrcxmZ+b7W5i//BB2B1w9tDM/PjSG1Y+N5Zv/G80tI7sA8PaaY/WaIQ7lWe4rEzoSG2YEnOXzD1/SE4Cdp/MoNDe8dL0xCkrL+HDjcS779zquW7iR0rKq496EaKx6/xY5ffo0UVENL7cRQjRebpFz35nK4UDfglluxWWJoxk460/01RSwQ6dm6OUzUKnPrRm07Yler0etVpOSkkJkZCR6vV62DDQTh8OBxWIhMzMTtVqNXt8ykwdEw1QOshu6v/dc5Tmn22x1XmjqFOJ9JrqySNd5y2wOcootjdrPDRBq0hMZaCCzwMw7644B5bO2GyMhNoTPt50mydXB/FB6IQ6H831R3pv66hkViE6jIr/UyumcEnfACvBTPbqWe1KaqSVnFVFottb493rVgQzWHc7CX6fh2asGcM2w8hG9Q+JCGRIXis3h4NPNJ3n085388ucLCTbW/JnkSEYhy/Y6957fP7biCN/YMCOxYf6cOlvC1uNnGde75WKNlNwS3lx9hG92nKHIY67674ezmuTvgTeKzFb8dRrUXo6pE21HvX+LeAbcKSkp/P7772RkZFS5av/QQw813eqEEBXkFjqDbo3Nhkbnmy0d4y+fTvIP75Fy4hg5p48RFicVL22VWq2ma9eupKamkpKS4uvlnBOMRiNxcXGo5WJVqyaZ7sZRguOCUivFrmCmKcrL9Vo1EQF6sgotpOebPWZ0Nzyg7xUdQGaBmVNnnRn58Y3Yz61I6BwCOMeGORwODrr2c/eJ8X4utV6rpndMIHvO5LM3Jc8ddKfnl7Ll+FmgfqXlAJGBBmKC/EjLL2V/aj4j4sOqPU4ZmzbjvNgKAbenJy/vy8aj2SRnFfG3pbv5zw1Darxou3DNURwOuLRfdLWzuUd1C+fU2dNsOprdokH3Y1/sZMPRbAC6R5oIMerZfiKH5fvSWzTo3peSzxVvrOPmkV145soBLfa6omV5/Vtk0aJF3Hvvvej1esLDwyv8A1OpVBJ0C9GM8ouc5Wkauw2NzjeZsutn3MBdi17HHBLM9o2rmCBBd5um1+uJi4vDarVis0lJXXPSaDRotVqpJmgDjPrKmW5ppOaNIH8teo0ai82Oze5Aq1Y1aM91daIC/VxBdylpec5AuaGZbnCWmK8/4gy8ooMM7j3UjdE7JhCDVk1+qZXj2cXsd3cu9z7oBue+7j1n8tlzJp9JA5xZ7U82ncDhgBHxoXQONdZxBo9zdQoiLb+UvWfyqg26rTa7u4v7pf1qDuaNei2vXT+Y6W9t4MddqYzvG8W0IVUD9PzSMpb+cQaA/xtX/eeF0d0j+HzbaXcA3FKUSol50wcyY0QsG45mc9P/NrNifzo2uwNNC2Wdtx4/i90Bn24+yQPjetTahV60XV4H3f/4xz+YM2cOs2fPliv1QrSw/JJiALR2e4s2UfPUrVs3zkyYxK7OHeCPLVxyzR2oNZIFastUKhU6nQ5dC46gE6I1M0mmu1FUKhXhAfoKM7SbKoCJDjKwL9WZ6S0/f8Oz6L08Mq+X9I1ukvJenUbNgE7BbD+Rw85Tue7O5X06eNdETdG/UzBsPeXei11aZuOTzScBmDm6q3fn6hjMiv0Z7KmmMRvAthM55BSXEWLUMSI+tNZzJcSG8OdLevLK8kPMWbqXkd3Cq+wt3306D6vdQWyYv3ucWmWjuocDzlFmecVltZaqK8xWG6sPZvLz7lTiwk08Mr6n1xc0C1x7yBM6h6BSqTivaxhBflqyiyz8cTKH4TVUAjQ1ZUqC1e7gk00nmHVp7xZ5XdGyvI6ai4uLmTFjhgTcQvhAgUfQ3ZLjwiob2sXZ+GS7WkdW8kGfrUMIIZqDsVKQLUG39zz3LjfFjG5FjEcH88bu6YaKQXdTlJYrlBLzpFO5Hp3LG5bpdjdAc3Ud/2FXKtlFFjoE+zGxv3dl0OUdzKtvpvbrXmeW++I+UWg1dX/Wv39sdwZ1DqbAbOX7nVW3KSn72pX3ozrRQX50izBhd8Dm5Nqz3QfS8nniq12MeG4F9360naVJKby+8jBvrva++3lhqTPoDvRz/vvWadRc3Mf5d+BXV4l9S1CCboBPt5zEbJWqs/bI68j5zjvv5IsvvmiOtQgh6lBY4iyl0zp8l+kGeHzateBwkBYYzJp1v/psHUII0RwqZ7qlkZr3lFnd0LRBt1KmnpZf2qgZ3Yo+MYGEGHVEBBgY3b1+s6vrQ+lgvnxfOrnFZWjUKnpEBTToXH1jglCrnN3g0/NLeX99MgC3jOpSr8DY0wBXN/TDGYVVunQ7HA6W73c2PKuttNyTVqN2N3LbdjynyvM7XUF3TVluhZLt3nis5qDbZndw8/+2sHjrKfJLrUQHGbhsoHOdL/1ykGV70uq1ZnCW0Ze4vn/Pf9+X9nee79e9aVW6xTeXjIJS9/9nFVrcDfJE++L1b5F58+ZxxRVXsGzZMgYOHFilHHH+/PlNtjghREUBdjBu2U7f6CA0ugSfrWNIz94E5eSTHxbM1/v3MM1iRqv3riOrEEK0VpLpbjzPTHdTNFFTlGe6y8vLG5PpNhm0fP/gBWjUKvx0Tbd3XwkylZFp3SJMDT6/v15Dj6gADqUXsmjDcfam5GPQqrlhRJzX54oJ8iPcpCe7yMLBtAISPILhA2kFnDpbgkGr5sJe9b8AoZShbz+Rg8PhqFDmrYxNS6hH0P3J5pNsrGVf946TOWQVmgny07Lw5mEkdgtHo1bx9Hd7WbThOI8sSaJz6Ch3Nr82nuPJAvzK/31f2CsSvVbN8exijmQUVtv4rallFjoz3UPiQvjjZC6LNpyodn+8aNu8znTPmzePX375hfT0dHbv3s0ff/zhviUlJTXDEoUQikCrneDNWxmamYZG69uRQ2M7OfeR/aEzkHlkr0/XIoQQTalqplsaqXkrItCjvLwJZnQrooOc5z2UXuDOVDa28VRsmLFJLwwAxIUZCfHYm9y7gaXlCmXc1ztrnaPNpg3pRKjJ+88BKpXKuUecqvO6ldLyMT0jqjQTrM2ATsHotWqyiywczy52P56WV0p6vhmNWuUuka/JyG7OTPeBtAKyC83VHrPC1eBtXJ8oRveIcPcJePLyvozpGUFJmY27P9xWIXNckwJXabmfTo3Oo1ogwKDlfFfWvaVKzJXy8ocu6Yleo2bnqVx3Wb5oP7wOul955RXee+899u/fz+rVq/ntt9/ct1WrVjXHGoUQLiUlJWjVKtQajU/LywGeuPp6sNvJCgji1zXLfLoWIYRoSpUDDsl0e6+5Mt1KefnpHGcGOcykb9IMdVNRqVQV9jH3bWATNYUStFrtzpLnmefHN/pcyh5xxa/7vCstVxi0Gga5AvltrjFmUL6fu1d0YJ1BfESAgd6urPLm5LPVHrNyfwbgbHjnSatR858bh9It0kRqXimPf7mrzjUrme4AQ9XPUu4S8xYIuu12B1mFFsC5jeCKBGep/gcbjjf7a4uW5XXQbTAYOP/885tjLUKIOmQXF+EIDcWsN6DW+vZDYK/YLoTmOpvD/JB8GEtJkU/XI4QQTUWvVaP16GItQbf3mmtPd+X9242Z0d3cPEuqG9pETeFZMj2qWzh9YhoexCtj0fZ6ZLrP5JawNyUftQouaUBDuWEeJeYKpbR8cGz9xrAp+7o3HM2q8tyJ7CKOZBSiVau4qFdkleeD/XX895bhqFSw+mAmJ7Jr/0yiBN2BflX/bV/SNwqVyrkfPT2/7qx5Y+QUW7DZHahUEB6gZ+boeAB+2JVSocGaaPu8Droffvhh3njjjeZYixCiDjtK8zl943Ws6RTvszndnm7oOYiE39fSde8u0g7u9PVyhBCiyRg9SsxNXpTaCqfICpnupguMw4x6dJryCyKN2c/d3DyDzYaOC1P08yjPvr0RWW4ob6Z2ILUAi9UOwPK9ziz38C5hhAd436NleBfneK1tnkF3PTqXe3I3U6tmX/cKV5Z7RHwYwf7VV/r1iArgwp7OgHzJ1lO1vlZBaRlQfZPEqEA/hrgumCyvZ7b71NliVu5Pp8hjr3h9ZLgCa+ffazWDOocwJC6EMpuDr3ac9upconXz+rfIli1bWLVqFT/88AP9+/ev0kjt66+/brLFCSEqKikrAz3oHA6fjgxTPHr9jfw8bw7FsaHsXvsrcYNH+3pJQog2Jjc3l/Hjx2O1WrFarTz88MPcfffdvl4WJoOW/FIr/jpNk82YPpd0DjUCzky0N/uD66JWq4gK9HM3KGtM5/LmNjQulGB/HeEmPR0buc4gPx1/mdibzAJzlfJqb8WFGQn001JQamXIM7/St0MQaa6M7oR+DTv3sC7OTPeRjEJyiy0E+enYddqZSa+riZpiZNdwVCo4mllEen5phb36K137uevKwt9wXixrDmXyxfbTPDKhV4X92p4KSmvOdIOzxHzHyVx+3ZfOzSO71Ph620/k8L91x/hlbxp2B4Sb9Dwwrgc3JsbVa9uDks2O9OiBML5vNH+czOVwemGdX99YH248zr9XHCbQT0uYSU94gIEhcSHcf1F3r+ee1+Wt1Uf5eU8qJRYbxRYbZquNywd2YO6VA5r0dRwOB6+vPEKv6AAmuzrrtwZe/xQMCQlh+vTpzbEWIUQdSqxloNegw+HzPd0AwcHBdB08CrL3c2TnFkrzc/ELCvH1soQQbUhgYCBr167FaDRSVFTEgAEDmD59OuHh4T5dl5LpDqjhQ7moXVy4kTduGNKkTdQUUUEGd9DdmjPdIUY9y/48Br1G3SQBzAPjejTBqpz7ze84vytvrTlKkcVWITvd0KA7zKSne6SJo5lFbD+RQ5dwI4Vm50WrnvUclRZs1NG/YxB7zuTz++Esrh7m7OCdX1rGFtc+7/F1XHC4uE80EQF6MgvMrDqQwcT+1e9PL9/TXUPQ3S+af/18gPVHssjILyWq0jaGM7klPPTZHxXK6ZWu8M/8sI//rTvGYxN7M31o7V3Iqwu6lb/TafkltX5tU/hy+2myiywVmuAt35fOpP4xdIts2Ii76hSZrbz0ywHslaawfbL5JE9P7d+kAf6RjEJeXXGIUKOubQfd77//fnOsQwhRD2abFdCgh1aR6QaYfO31vP7TZ6wNj+GKfdvpPvISXy9JCNGGaDQajEZnVtRsNuNwOFpsPm5tlH3cMqO74aYkdGyW80YHlgdAMcFNH9Q3pQ6tdH2PTOjFny7uwbGsIval5LM/NZ/ukQHER5gafM7hXcI4mlnEthM55BQ7y7cHdgr2apb4uN5R7DmTz0u/HGRcnyjCTHrWHMzEanfQPdJU5/r0WjVXD+vM22uOsXjLyRqDbiXTXdNFtW6RAQzvEsq2Ezks3nqKhy7pWeH5f/64j+0nctBr1Fw1pCN3XtCNbpEmvtx+mn+vOExKXimzPt9JqFHPuD41Z+eVcWGe2zGUPgXKHPrmpOxZ/9f0gYQYdTy5dA9ZhRb3n19T2X0mD7sDogINvDZjMGqVihn/3YTV7qDYYmvSvhnKe5pTXEZeSVmN2xFamtd7uoUQvmO2Ocej6FWg1rWOHyJTLruc0716kxIawdLVv/h6OUKIJrZ27VqmTJlCx44dUalULF26tMoxCxYsID4+Hj8/PxITE9myZYtXr5Gbm0tCQgKdO3fmL3/5CxER9Z8R3FyUTLdJxoW1Op4l5a05093aaTVqekUHctWQTsy+rC/XjYht1PmUZmrbjp8t389dzyZqivsu6k63SBNp+aU89sVO7HaHR2l5/bLwM1zzy9ccyiQlt/pscaFSXl5LsKeUlX+25SRWm939+PGsIpbtce6B//r/RvPiNQn0jglEp1Fzw3lxrP7LWK5xZen/89uRWi8iZuS7gu6g8qA72j2LvnkbqdnsDnem/eI+UUwa0ME9dcDbvel1UTrZD+sSyujuESR2DXM3q8wradoAP9/jfKfOFtdyZMuqV9A9adIkNm3aVOdxBQUFvPDCCyxYsKDRCxNCVGWxO3/oGwCtzvtGJ80hIiSEGNdVxRXZWRRmt8xcSyFEyygqKiIhIaHG3+1Llixh1qxZPPXUU+zYsYOEhAQmTpxIRkaG+5jBgwczYMCAKreUlBTAuXVt586dJCcn8+mnn5Ke7vufI0rzNGmi1vpEeQQorXlP97lmuGtf987TeWx1jQ6r735uhcmgZcGNQ9Fr1aw6kMF/1x3jt4OZAFxSS8bYU9cIEyO7hWF3wBfbqm9GVt69vOYExuSBMYSZ9KTmlbLqQPnPs//9fgy7A8b2jqzQVV7hp9Pw+MTe6LVqtp/IcZfGV6e2THeh2epu+NYcsgvN2B2gVuFunqdU9jR10K1chBns+vugUqncc+ybOujO9cjSK6MFW4N6Bd3XXnstV199Nf369eOvf/0rX3zxBevXr2f79u2sWLGC119/neuuu44OHTqwY8cOpkyZ0tzrFuKcZHGUB92tJdMNcG3CcAD2mII4tXebj1cjhGhKkydP5rnnnmPatGnVPj9//nzuvvtubr/9dvr168fChQsxGo2899577mOSkpLYs2dPlVvHjhXLj6Ojo0lISGDdunXN+j3Vh7+yp1vKy1sdzzFhrXlk2Lmma4SJcJMei9XOgTTnSNH6di731LdDEHOu6AfAv34+4C4RVpq11YeS7f582ylslTcSU3d5OTjnj1873Jmx/njzScAZqCqB/D0Xdqvxa6OC/LjWle1esPpojcdlFjjLuz33dJsMWneDt+YcWaZk0iMDDe5mkUqZd2EzZbo9L8IE+TdP0O15vtM5bSzTfeedd3Ls2DH+9re/sW/fPu655x7GjBnDiBEjmDhxIu+88w5xcXFs3bqVJUuWEBcX19zrFuKcZEo/S+fDB+los7SaTDfAI9OuRWU2U2Lw46s1v7aK/ZhCiOZnsVjYvn0748ePdz+mVqsZP348GzdurNc50tPTKShwfkDPy8tj7dq19O7du8bjzWYz+fn5FW7NwZ3plqC71VG6Wgf5aeXPpxVRqVQVAuNwk57ODWykd1NiHJd7NMEa1zvSq73hkwbEEOyv40xuCesOZ1Z5vraRYRXWcV4XVCpYe8g5+/vDjScwW+0M6hzMqG61N3u898LuqF1fu+dMXrXHVNdIDTz3dTdfibkS0Ht2iW+OTHd6fimpeaWoVc49/gplr3VuE+8fz2vL5eUABoOBm2++me+//56cnBxycnJISUmhtLSU3bt38/LLL9O3b9/mXKsQ5zxj8mn67/qDHnZrq+hergg0mogtde43X11QSH5a7fMxhRDN48UXX6SkpLycbv369ZjN5R/aCgoK+L//+78me72srCxsNhvR0RX3WkZHR5OWllavc5w4cYIxY8aQkJDAmDFj+NOf/sTAgQNrPH7evHkEBwe7b7GxjduHWhOjQdnTLUFdazOwczBxYUYuH9Q8jdpEww2PLw+6E2JDGtyVWqVSMe/qgcSGOYP2SQO860Ltp9MwbUgnAH7eXfVnUXl5ee3/vuPCje7Z3+/+nsyHG48Dzix3Xd9bXLiRqa5mgm+uPlLtMUrQHRVYsWIjxt3BvBkz3a4su+drKz0siiy2JnsdJcvdKzqwws9TJejOb+ry8pI2XF5eneDgYGJiYqrM6W4NGtvQRYjWqqSkBK1GhUatQaPT+3o5Fdwy4nwA9puCOb5rs49XI8S5afbs2e6sMThLw8+cOeO+X1xczNtvv+2LpdXovPPOIykpiZ07d7Jr1y7uvffeWo+fPXs2eXl57tupU81zkU8Zl9M9suHdnEXzCPLTseYvY5k3veaLM8I3hnUJc///oM7eNVGrLMhPx1f3jebtW4Yxsb/3o8z6dggEIKOgauBa36AbyhuqfbjxBDnFZcSG+TOphq7old0/1jnm7ec9aRzNrDh3u7TMRr6rzL1ypjvanemuPWg8klHIzPe3cOGLv3HFG+u48Z1N3P/xdr7ecbrOqkOlvDw6qGJpOzRteXlSpf3ciuAWKC8/1YrKy9vd5VulocvChQtJTEzktddeY+LEiRw8eJCoqPo1YBCitSpUObD6G0HT+oLu/5syjRf+sZpO+WfZsnY5gyZcjUotAxKEaEmVP2Q191aPiIgINBpNlcZn6enpxMTU70OptwwGAwZD82+vuem8OIZ3CaVXdGCzv5bwXlPO9RVNZ0CnIPRaNRar3esmatWJCvKrcexXXUKMzs9JudUEdUr38gBD3cnDi/tE0THYjxTXCK+7x3Srd6l775hAxveNYsX+DN5ec5QXr0lwP6dkufVaNUGVgv8OdWS6rTY7/113jNdWHMZitVd5/uc9aazcn8Hz0wfWODIro4Hl5aVlNval5rPnTB5HMgqJCzMyPD6M/h2D0FXzviSdzAWqBt0hzRV0F3uWl5fgcDhaxc+Ldhd0ezZ0AVi4cCE//vgj7733Hk888YSPVydE45y99Hx+CTQxsOgsmlZWZWLQ67m2WEvpzm1kdYnl7KmjhHfpWfcXCiHaLL1ez7Bhw1i5ciVXXXUVAHa7nZUrV/Lggw/6dnGNpFar6NshyNfLEKJNMWg1zJrQi92n8xjdvfY9z80tVAm6q9kznO8OuusOhTRqFTPOi2P+8kOEGnVcO8y7LS3/N64HK/Zn8M0fZ/jHFf3cHdM9O5dXDgqja9nTfSSjgD8vSWLPGWc/i4t6RXLvRd0wl9nJLy3jUHoBb685xo+7U0k6lcvrNwyptgld+Z7u+mW6C0rLuOuDbWw7kVNtczo/nZrEruHMmz6QjiHObQE2u4Pdrv3slS/CeJPpTssrZduJs1zSJ9rd5LImnucrKbORXWRxj0LzpXYVdCsNXWbPnu1+zNuGLkK0Zg7XFUQ/tQpNK2qkprjtttv4y7LPMRrSSP5jgwTdQrQDhYWFHDlSvh8xOTmZpKQkwsLCiIuLY9asWdx2220MHz6c8847j9dee42ioiL3xW8hxLnlvou6+3oJAIS6RlLlFFuqPFdodgZm9SkvB7j9/HhO5xQzoV9MnUFfZUPjQt2Z8v2pBZzX1VmC797PHVT185zSSK267uUPfZbEvtR8gv11zLmiH9OHdqoStE/oF8NDn/3BybPFXPf2RhbcOKTKvnilvDwqyHNPd82Z7u0nctjsGn8WEaBnQKdgekYFcDSziO0ncsgrKWPNoUzmfr+Xt29xTrU5mllIodmKUa+pUjWkdC+vrhJBUWS28vaao/x33TFKy+x0CTcyb9pARveIqPFrcksq/nmfzimRoLup1dbQ5cCBA1WON5vNFRrMNFcHVCGaikOrRQX4q9WtrrwcYNiwYZQFRpIRYGLp+t8YesVNqLXt6seMEK3e//73PwICnPuRrVYrixYtIiLC+QHFc793fW3bto1x48a578+aNQtwXmRbtGgR119/PZmZmcyZM4e0tDQGDx7MsmXLqvwuFkKIlhTsMQfaZne4x2KV2eyUljlLsusbdAf66SqUhnurb4cgV9Cd7w66MwqqzuhWKI3UUvMqBt2lZTb2pznjle8ePJ8u4dX3nBgcG8IPD13AY5/v5Nd96Xyw4USVoFvZ6x7t2UjNdUGhyFy1kZpSHXBe1zCW3DOyQqBvtzvYevwsM97ZxC970zmYVkDvmED3fu4BnYLd77+itky3w+Hgi22neenXg+6LEwatmhPZxdz4v81cN7wzf7+sn/vP2JNSXh5u0pNdZOHU2eIqpe2+0KBPw7m5uXz55ZccPXqUv/zlL4SFhbFjxw6io6Pp1KlTU6+x2cybN4+5c+f6ehlC1IvD4UCld/5w8VOr0bSi7uUKlUpFp2uvYTml7MnP5U/H9hHda5CvlyXEOSMuLo533nnHfT8mJoaPPvqoyjHeGDt2bJ17wx988ME2X04uhGhfQvydyQmHw9khO9TkvK/s54aWm07Qr2MQKw9ksC+lPMFX07gwKA+6s4vMlNns7r3SxzKLcDggxKgjLsxY62sG+em496Lu/LovnRPZRRWeK7PZySp0ZoS9KS8H517sypl1tVpFYrdwJg+I4afdaSz47Qiv3zDEHXQPqSborS3o/mLbaR7/ahcAXcKNPDGpD+f3jOClZQf5aNMJPt92mt8PZ/HLIxe6y/XBWc6uXBzo3ymYtYcyW00zNa+7HO3atYtevXrxwgsv8PLLL5ObmwvA119/XaGs2xe8bejSUh1QhWgKBaXlHSxNen2rbVL2l2nXg8NBalAIK1f/7OvlCHFOOX78OMnJyXXehBCivdNr1e49254l5kpA6adTV9v4qzko/SGULDXUHnSHGfXoNCocjvKMOMARVwf0HpEB9WoOFh/uDMxT8kopLSvPXiuvrdOo3HvfofZGagXKPvhaqgMeGOfs1v7DrhSOZRa6m6hV11RPaXRX3cgw5fucPCCGXx+5kMkDOxDkp+PZqwbwxX2jiAjQk5JXyhZXuXv5GsvPNaCj8z0/dbZ1jA3z+m/arFmzmDlzJocPH8bPr7wc4bLLLmPt2rVNujhveTZ0USgNXUaNGlXleIPBQFBQUIWbEK1VjkdZaGALdO5tqOF9+hKc4/yl8vWRg5gLZduGEEIIIVpeiHtfd3kwpgSPnhnS5tbPFXQfSCvAanOWttcWdKvVKvf87DSPEvOjGc5gtLtrpGFdwkx6dwn9iezyjK+yVzwq0A+1R9l3bXu6lYA2qJb3rX/HYC7pE4XdAfOXH+JguvOza3Xl3bVlurNc701CbAgGbcU99CPiwxjYyTmOLquwYqM5pWmeUa+ha4Sz9P50W810b926tdoZmp06dSItrerw+ZY2a9Ys3nnnHT744AP279/P/fffLw1dRLuQlZcLgNpmw+Dn79vF1OHy7n0B2O4XwJm923y8GiHOHRs3buSHH36o8NiHH35I165diYqK4p577qnQy0QIIdqz8g7mVTPdgS1UWg4QF2bEpNdgsdpJznKWeivdy6M89lR7co8N8wi63ZnuqPoF3SqVyh18Kq8Lnk3UKgb8AQZngFt9eXn9Zps/eLGS7U7FZncQFWhwfy+ePIPuyluYsoqcf141NUALdz2ulMgrlAA+xF9HrKv8/nROG810GwyGahuOHTp0iMjIyCZZVGNcf/31vPzyy8yZM4fBgweTlJQkDV1Eu2AtNaPevpOeZ062yiZqnp68/mZUZWUU+Bv5YtVPzT4rWAjh9Mwzz7B37173/d27d3PnnXcyfvx4nnjiCb7//nvmzZvnwxUKIUTLqT7T7fz/2sqkm5paraKPK9u9L9UZR2W6ss3VZboBoquZ1e3OdEdV30CtOvGuZmvHPfZ1V9dEDTwy3RZblc9u9Q26h8SFMqZneXfxhNiQakvhlaDbZndUCfKzXRckwgOq/7wb4Q66K2W6XUF3kL+OzqHOBNWZnBLs1Yw4a2leB91Tp07lmWeeoazM+U2pVCpOnjzJX//6V66++uomX2BDPPjgg5w4cQKz2czmzZtJTEz09ZKEaDRdmRXjqrWMOroPbSscF+YpKjSMTq6rlMtzcshPP+3jFQlxbkhKSuKSSy5x31+8eDGJiYm88847zJo1i9dff53PP//chysUQoiWU1umuz4zuptSP4+g2+FwlM/priHorjw2zGZ3cMyVre4RGVjt11Qn3pXpPl4h0111RjeUB902uwOz1V7hOeViRX3K8h907e2G6kvLwbmnXu/aU1+5xDzblcGOMFX/3kS4gvEaM91GHR2C/dGqVVhsdtILqo5ea2leB92vvPIKhYWFREVFUVJSwkUXXUSPHj0IDAzkn//8Z3OsUQiBc1auVqNGq9Wi0bfuTDfA7SMvBOC4zsCJPzb4eDVCnBtycnIqVHatWbOGyZMnu++PGDFCmoYKIc4Z1c3qrm/GtqkpzdT2peSTV1JGmc2ZfY2oIZurBN3K2LAzOSVYrHb0WjWdQuu/zbBrhLPMuvry8kqZbn35e1I5+5zvxfuW2C2cC3tFolbBRb2qr4RWqVTuWd2eQbfD4SC7qH6Z7uxKme48159zsL8OjVpFxxDn+9QaSsy9/tsWHBzM8uXL+f3339m1axeFhYUMHTqU8ePHN8f6hBAuZ/Pz0AQYcej0aLStP+j+vylX8c6/X+ZCew5/lBUzYMLVMrNbiGYWHR1NcnIysbGxWCwWduzYUWE0ZkFBATpd6xs3KIQQzUHpkO1ZXl6e6W7Zn4X9XN2096cWuJuohRh1VRqFKZSxYemuoPtIprMpWbcIU5WZ17Wprry8PNNdMejWqFX46zSUlNkoMlsr7Kn2tgHdf28ZRlahmc6hNY82CzHqyCo0Vwi680ut7gsSYSbvysvL93Q7v65zqD8nzxZz6mwxI+LD6rXu5tLgT8AXXHABF1xwQVOuRQhRi03pKWTfczs/FeRyQyvf0w2g0+qYMnIcaas/59Txo2TKzG4hmt1ll13GE088wQsvvMDSpUsxGo2MGTPG/fyuXbvo3r27D1cohBAtR8l051bIdCtl0i2bCOgdHYha5QwUlX3dkTU0CoPyoFvZ033EvZ+7fk3UFEojtfR8M8UWK0a9lgxXprtyeTk4S8xLymxVMt3evm9+Ok2tATeU7+v2HBumZK8DDVr8dNVfkFAy4NmVysuV7uXBrj/32FAjkN0qxoZ5/bft9ddfr/ZxlUqFn58fPXr04MILL0Sjqf5NEkI0TH6Jc+SBzuFA3UYyVbfceit3LfkfJj89u9avZIIE3UI0q2effZbp06dz0UUXERAQwKJFi9B7bEd57733uPTSS324QiGEaDmhrkxpTpFHprvUN3u6/V1jrI5mFrHmYCZQ835uKC8vT8svxeFwcDRD2c/tXdAdYtQTYtSRW1zG8axi+nUMcu9xrpzpBmcH86xCKDLbKjyuBOFBTXixQgm6cz0qEbJdPYFqKi2H8kz32WILVpsdbaW94cp5Y8OU8nLfjw3z+l179dVXyczMpLi4mNDQUMC5h8xoNBIQEEBGRgbdunXjt99+IzY2tskXLMS5qqDUeZVO73C0+u7lin79+lE0cgzf9O5C9v7dXFiYjyEgyNfLEqLdioiIYO3ateTl5REQEFDlAvgXX3xBYGD9G/AIIURbVl5e7pHpNvtmTzc493UfzSxi7eG6g25lnJfFaienuMzrcWGe4sNNJBXncjy7iG6RJneQW7l7OVQ/q9vhcDTLfPPqZnWXdy6v+b0JNepQqcDhcG4dUN7H3CpBtzPTfqotBt3PP/88//3vf/nf//7nLlE7cuQI9957L/fccw/nn38+M2bM4JFHHuHLL79s8gULca4qNJeCum0F3QAjR4/hq8IMNttsnN6zle4jL6n7i4QQDXLHHXfU67j33nuvmVcihBC+V15e7jkyzJXp9kHQ3a9jED/sSnV33a6tvNyg1RBu0pNdZCEtr7S8vNzLTDc4S8yTTjmDbmU/uUGrJsi/6nugBN2e5eUlZTZsrrFbTXmxorqgW3lvwmvYzw2g1agJMzrfm6xCszvo9uxeDrjHhrWG8nKvu5c/+eSTvPrqqxX2hPXo0YOXX36Z2bNn07lzZ1588UXWr1/fpAsV4lxXZHb+kNTjaBON1BRzb7wNlaWMfKOJz1f8IDO7hWhGixYt4rfffiM3N5ecnJwab0IIcS5wjwwr8RgZ5qPycijvYK6IqmZPtSel/HtfqrPjuUoF3SLrP6Nb4W6mllVUoYladfOzA6rJdCsXKpRGa02l+ky3Ul5e+3tTXTO1vOJKmW7XnvLUvBLKbHZ8yeu/bampqVit1iqPW61W0tLSAOjYsSMFBQWNX50Qwq24zAL+BgyAVt+653R7igkPJ66kjBN6HcvyC7j39DHCYqWRkxDN4f777+ezzz4jOTmZ22+/nZtvvpmwMN92bBVCCF9RMp6lZXZKy2z46TQee5Nbvj9O/0pBd23l5eBsprYvNZ/1R7IAZ+a2puZitYl3jQ07nlXsHhdWXRM1qD7TrTRRCzBoqw3UG6r6TLdzfTWNUlOEB+ghvWIztcrdyyMDDRi0asxWO2l5pe5yc1/wOtM9btw47r33Xv744w/3Y3/88Qf3338/F198MQC7d++ma9euTbdKIQTFNucPP38caA31n8/YGvxp3EQADgSF8Mf6FT5ejRDt14IFC0hNTeXxxx/n+++/JzY2luuuu45ffvlFqkyEEOecAIMWrWu8lrKv2z0yzAfl5ZGBhgpl05EBVfdUe1I6mP/uCrq9baKmUDLdydnlme7KM7oVAQZnUO/ZSM2bGd3eqDbTrczorqW8HKrPdCsVDcp5VSqVR4m5b/d1ex10v/vuu4SFhTFs2DAMBgMGg4Hhw4cTFhbGu+++C0BAQACvvPJKky9WiHOZX04BUcnH6GwtQ2toO5lugFsnTEafm49No+WTP7ZgKSmq+4uEEA1iMBi44YYbWL58Ofv27aN///783//9H/Hx8RQWFvp6eUII0WJUKlV5MzVXB3PPrK0v1qPM64Z6ZLpdgbGyD7sh+7kB4l1jwzILzBzLcv4eqK6JGoBJ73xfii1Vy8ubsoka1L6nO6KO90bpbq4cb7baKC1zlpArI8MA99gyXzdT8/pvW0xMDMuXL+fAgQMcOnQIgN69e9O7d2/3MePGjWu6FQohAAg4kUJc1kEGjklsc5lutVrNheEdWGErYoPWQOre7XQZfqGvlyVEu6dWq1GpVDgcDmw2W91fIIQQ7UyoUUdWodk9q7vAh3u6wbmve91hZ+Y6qp5Bt6IhncvBGdyGmfScLbKwJfks0LDy8ibPdBtr6V5u8m5Pt3IOlco541uhjA3zdTM1rzPdij59+jB16lSmTp1aIeAWQjSPgoIC9Fo1Wq0WraH2cqTW6JkbbiVg1x6GJ20haZU0VBOiuZjNZj777DMmTJhAr1692L17N//5z384efIkAQEN+8AmhBBtVah7bFgZFqsds9WZDfXFnm6Afq593TqNyp3prUl0cMXPe90bGHQDxIc7M76H0l2Z7hrLy2tupNaUM7oBQqotL3dluuvY0x1ZOej2aKKmVpfvO1eaqfl6VneD3rnTp0/z3XffcfLkSSwWS4Xn5s+f3yQLE0JUlG8xo/HXotVo0bWxTDdAr9gujLQaCMjL5cieJHJTjhPaSXo/CNGU/u///o/FixcTGxvLHXfcwWeffUZERISvlyWEED6jNFPLKbZUCCRNhqbrwu2NIXEhqFXQLSKgQnBYnQ6Vgu6G7ukGZ4n5jpO57vs1dU4vz3SXV0eVZ7qbp7w8v6QMu92BzeFwj3erq3u5Ul6uNFLLqzSjW9EhxPmZOc21l91XvA66V65cydSpU+nWrRsHDhxgwIABHD9+HIfDwdChQ5tjjUIIIGPi+XwfaKJv0Vm0+raX6Qa4/a57ePGh2/A3nObY1rUMk6BbiCa1cOFC4uLi6NatG2vWrGHNmjXVHvf111+38MqEEMI33GPDii3ujK2/ToNW0+CC30bpEm7iy/tH1zqjW+GZjQ4z6Qmto7lYbbqGVxw1VlOm2+RupFbdnu6mzXQHuQJkuwMKLVZKLc5AX60qz4LXpHJ5uRKsV/46pdS82OLbLVZev3OzZ8/mscceY+7cuQQGBvLVV18RFRXFTTfdxKRJk5pjjUIIwKF3/hAxaVRo/dpm0D1hwgQej45jbd8eFG9cw38mXYfOr+1l7YVorW699dYmHecihBBtXXmmu4wCs6uJmg86l3saGhdar+OC/LT46zSUlNkaleWG8mZqijrLy6ttpNa075ufTuMe6ZVXXOZ+nTCToc4qAM9Mt8PhcGe6gyoF3Ua98yKC5x51X/D6ndu/fz+fffaZ84u1WkpKSggICOCZZ57hyiuv5P7772/yRQpxrrM77Dj0OlRAoEaLRte2upcr1Go1fS6fwkp7EQVFBZzevZmuI8b6ellCtBuLFi3y9RKEEKJVcXcvL7ZQ2EzBY3NRqVTEBPuRnFXUqP3cAF09gm6TXlNjI7nqGqnlN1N5OTjLwTMKzOSVlLnHutW1n9t5jPOzsMVmJ7/USq4yo9tY8WuV76fY7NtMt9d1FSaTyb2Pu0OHDhw9etT9XFZWVtOtTAjhll1Q4M5ehfob23Qm61+33omqzMpZUyBLfvlWGqoJIYQQotmEujLduR6Z1EAfdS5vCKWDefdIUx1H1s4z011Tlhuqb6RW2Iwd30M8Opgr+7PD6xF0++k07j/H7EKzx57uimtUMt2emXtf8DroHjlyJL///jsAl112GY8++ij//Oc/ueOOOxg5cmSTL1AIAafS0wBQ22wEBQbVcXTr1rVDR2JdVzJ/zC8gK/mAj1ckhBBCiPaqQqbbFUj6urzcG7eO6kJi1zAuH9ShUecJMGjd2eGamqhBeWa4qEIjtearEPCc1Z1Vz3FhCs9Z3Xmuz5Yh/hUD9gCPPd2+TPR4HXTPnz+fxMREAObOncsll1zCkiVLiI+P5913323yBQoh4HRmJgB6m7VNjgur7C+XTgHgYHAYG1f/5OPVCCGEEKK9qpDpNvt2RndDTB7YgSX3jqJDcON74HSNcI7Pqi3T7W6kZrG6g1RlL3xzjFnzDLqVcWH1yXRDxWZqNXUvN7r+rG12h3tcnC94/TeuW7du7v83mUwsXLiwSRckhKgq9Ww2AHqrtV00Hrvh4gk8/vPXlIQF8/H+vYzPycIYKmONhBBCCNG0lI7fFfd0+2ZGt691jTCx9XiOu2S9OsoFCYfDmR02GbTNmukO8sx0Fzgz3RH16OwOns3UzO493ZWDbn9d+Wi4IrMVP51vRsV5nenu1q0b2dnZVR7Pzc2tEJALIZqOragYv/0H6Zqb3WbHhVV2Vbe+AGw2BnJs+1ofr0YIIYQQ7ZHnnmElG9qWMt1N6ZaR8YzvG801wzrXeIy/ToPSOFzZ113QjBcrlCA5t7g8012fRmrO45zBeWahpTzTbay4Ro1a5Q68fTk2zOug+/jx49hsVRdsNps5c+ZMkyxKCFGRX1EJkatWMzb1JFpD2890Azxz8+3oMjLpcfwIm5d/h9Vi9vWShBBCCNHOKHt8HQ44k1sCtJ3u5U1tYOdg/nfbcHpGB9Z4jEqlwqQv72DucDgocHcvb9493dle7ulWgu7sQjN5xdVnusFjn7oPm6nV+5377rvv3P//yy+/EBwc7L5vs9lYuXIl8fHxTbo4xfHjx3n22WdZtWoVaWlpdOzYkZtvvpm///3v6PV69zFdu3at8rUbN26UBm+izTt79iwGrQadXofW0DbHhVUWGhjILdowUvev5FRBR1L37SB28ChfL0sIIYQQ7YheqybAoKXQbOV0TjFw7gbd9WUyaCkwWyky2zBb7ZTZnHu7m+N9C3EFyfklZWR50b0cyjPinnu6Q4zVBd0asgorNodrafV+56666irAefXjtttuq/CcTqcjPj6eV155pUkXpzhw4AB2u523336bHj16sGfPHu6++26Kiop4+eWXKxy7YsUK+vfv774fHh7eLGsSoiWlnc1G76dDr9Oj82vcyIjW5IEHH2TakvcI9stix/KldE4Y2abHoQkhhBCi9Qkx6ig0Wzl11pnpDjCcm3u660tpplZotrpndKtUuDPgTSnYc2RYkXd7ussbqVlq3NMNYNRXHYPW0ur9ztntzm5vXbt2ZevWrUREtFzTo0mTJjFp0iT3/W7dunHw4EHeeuutKkF3eHg4MTExLbY2IVrCekcJh2+7jbCcDK42Bfh6OU2mS5cu9Bx1McfsWbx/5jQXnDhMeHwvXy9LCCGEEO1IqFHP6ZwS90iqtjQyzBc8Z3UXeMzoVqubPjGiBMkpeSWUljnjzfpmusNdQfeJ7GJsdmc2vvLIMACTXtnT7bug2+s93cnJyS0acNckLy+PsLCwKo9PnTqVqKgoLrjgggol8UK0ZflWV+MPlQqDseZ9OG3RlLvuZMvQRDZEdmLtiqW+Xo4QQggh2pnKJceB52gjtfry3AOtBN3NMS4MyoPuk9nO0n9/ncadma6LZ3k5gF6jxk9XNbw1VjN7vKXV6zt6/fXX633Chx56qMGLqa8jR47wxhtvVMhyBwQE8Morr3D++eejVqv56quvuOqqq1i6dClTp06t9jxmsxmzubx5U35+frOvXYiGKHI4r/wFq0Dn337KywFunjCJv//6LUVhwXxw8ADjM1MJjOzg62UJIVpQfHw8QUFBqNVqQkND+e2333y9JCFEOxJqrJj9lD3dtVOC7kKztVmbqEF50G11Zarrm+UGiAisWIYebNRVu00xwOD7THe93r1XX321XidTqVReBd1PPPEEL7zwQq3H7N+/nz59+rjvnzlzhkmTJnHttddy9913ux+PiIhg1qxZ7vsjRowgJSWFl156qcage968ecydO7fe6xXCV8wa51W7ELUKg6l9ZboBbuk/jIWpR9gSGMLu35cxetrtvl6SEKKFbdiwgYCA9rN9RgjReoRWynRLeXntaiovbw7BlcrBw+u5nxucFQt6jRqLzZWcqmY/N3js6fbhyLB6vXvJycnN8uKPPvooM2fOrPUYz9nfKSkpjBs3jtGjR/Pf//63zvMnJiayfPnyGp+fPXt2hUA9Pz+f2NjYuhcuRAuz6pz/VEM1avTG9vehdM5Nt/G/px7BEhjAu5t+Z+iE6fgFBNf9hUIIIYQQdQiplOk+V+d011d5IzUbhe4Z3c2b6VZEmOqf6VapVEQE6EnJKwXKO6FXpuzp9mUjNa/3dHtyOBw4HI4Gf31kZCR9+vSp9aaMBDtz5gxjx45l2LBhvP/++6jVdS89KSmJDh1qLlM1GAwEBQVVuAnR2tjsNhz+zqt+YVoNOj+jj1fU9PQ6HReHOf+trtYbSd621scrEkIo1q5dy5QpU+jYsSMqlYqlS5dWOWbBggXEx8fj5+dHYmIiW7Zs8eo1VCoVF110ESNGjOCTTz5popULIYRT5Ux3oHQvr5XJI9Od7y4vb573TK9V46/TuO97U14OFUvMa8x0t4I93Q0Kuj/88EMGDhyIv78//v7+DBo0iI8++qip1+amBNxxcXG8/PLLZGZmkpaWRlpamvuYDz74gM8++4wDBw5w4MABnn/+ed577z3+9Kc/Ndu6hGgJpzIzwHWRqUNwCKp6XHBqi16eeQ8qs5k8o4lFK37EajHX/UVCiGZXVFREQkICCxYsqPb5JUuWMGvWLJ566il27NhBQkICEydOJCMjw33M4MGDGTBgQJVbSkoKAL///jvbt2/nu+++4/nnn2fXrl0t8r0JIc4NoZWyp1JeXrsAfdXy8ubcB+8ZLHtTXg4Q7vFnG1zNjG4or2xo9Xu6Pc2fP59//OMfPPjgg5x//vmA85flfffdR1ZWFo888kiTL3L58uUcOXKEI0eO0Llz5wrPeWban332WU6cOIFWq6VPnz4sWbKEa665psnXI0RLSklNQ7N7H52iQghKGOjr5TSbjhGR9LfrOJOVwdmMM5zZvYUuw8b4ellCnPMmT57M5MmTa3x+/vz53H333dx+u7MXw8KFC/nxxx957733eOKJJwBn5VltOnXqBECHDh247LLL2LFjB4MGDar2WGmCKoTwlmd5uVGvQdMMo6/aE89GakZ30N181QHB/jrS8p0l4uFelJdDxZneNe/pdpWX+3BPt9cpszfeeIO33nqLF154galTpzJ16lRefPFF3nzzTa+6nHtj5syZ7lL2yjfFbbfdxr59+ygqKiIvL4/NmzdLwC3aheKsbEJWrGbi4T3oje2rc3ll79/5AMbPv8VwIpltP3+J3e67H45CiLpZLBa2b9/O+PHj3Y+p1WrGjx/Pxo0b63WOoqIiCgoKACgsLGTVqlX079+/xuPnzZtHcHCw+ya9WIQQdfEsL5f93HWr2EitebuXQ8UMdWSgl5luj6C7uhndACaPzL2veB10p6amMnr06CqPjx49mtTU1CZZlBCi3JkzZ/DXa/Dz80PfzmZ0V9a1SxcGj72MkjIbB/ckkX5wp6+XJISoRVZWFjabjejo6AqPR0dHV9gCVpv09HQuuOACEhISGDlyJLfeeisjRoyo8fjZs2eTl5fnvp06dapR34MQov3zHBkmpeV1M3nsgS6f091C5eUm74LuCI894MH+1a/RaPB9IzWv370ePXrw+eef87e//a3C40uWLKFnz55NtjAhhNOxlDMYjX74+fm1y3FhlT32+OPMvGYiR/sG0/GXb7itz5BqZy4KIdqHbt26sXNn/S+wGQwGDAbvPpQJIc5tnpnU5iyTbi/Ku5db0Wmdn8Gau7xc4W0jNc/MeOUu9Qol013c2keGeZo7dy7XX389a9eude/pXr9+PStXruTzzz9v8gUKca5bUZDFodtvY8PZdGYEh/t6Oc1u4MCB5F8xlRMRIXyScoYpJw8T3qWXr5clhKhGREQEGo2G9PT0Co+np6cTExPjo1UJIURFgQYtWrUKq91BoJSX18ldXm6xotUoQXdLNVLzLuj2zIzXtKfb5PH9+Eq9y8v37NkDwNVXX83mzZuJiIhg6dKlLF26lIiICLZs2cK0adOabaFCnKuyy5wNg0LUKvyDQ328mpbx0Bjn/tBdQeFsWPm9j1cjhKiJXq9n2LBhrFy50v2Y3W5n5cqVjBo1yocrE0KIciqVihBXtlv2dNfNZKiue3nLZLrDashW1yQisO7u5cZWMKe73n/rBg0axIgRI7jrrruYMWMGH3/8cXOuSwjhUqByNgyMUqvwCzo3gu4Hpk7nhQ0rKQ0NZtH+vVyUfoag6E6+XpYQ56TCwkKOHDnivp+cnExSUhJhYWHExcUxa9YsbrvtNoYPH855553Ha6+9RlFRkbubuRBCtAYhRj1ZhRbZ010PAR7dyxUtkekONerQarxrOeZNpru4LczpXrNmDf379+fRRx+lQ4cOzJw5k3Xr1jXn2oQQgNnPeQWvg1aNf1CYj1fTMtRqNTf2co4L2hQQwq51P/t4RUKcu7Zt28aQIUMYMmQIALNmzWLIkCHMmTMHgOuvv56XX36ZOXPmMHjwYJKSkli2bFmV5mpCCOFLSgfz5gwe2wslSC0ts5NX4uxe3pwVAkoVgrczugHCTHpMeg1atarC+DBPJvfIMGuF6Vctqd5B95gxY3jvvfdITU3ljTfeIDk5mYsuuohevXrxwgsv1LtLqRCi/tJzzoK/HwBdgkLQ+fn7eEUt55lb70BTUIhZr+fdrRsoyTvr6yUJcU4aO3ZstSM7Fy1a5D7mwQcf5MSJE5jNZjZv3kxiYqLvFiyEENVQmmzJnu66KY3UAMpsziA1qBnLy/t2CEKtgkGdg73+Wo1axf9uG8F/bx1W85xu15+53QFmq71Ra20or0eGmUwmbr/9dtasWcOhQ4e49tprWbBgAXFxcUydOrU51ijEOWvj3t0A6MssxERE+Xg1LctPb2BCuLOkfLUhgMObV/l4RUIIIYRoq7pFmgCIDTP6eCWtn0GrQaepODmmOcvye0UHsvlv43npmoQGff2o7uFc3Kfm6iqjrvwigq/2dXsddHvq0aMHf/vb33jyyScJDAzkxx9/bKp1CSGAHUcOAxBkMeMffG6Ulnuaf8e9qIuKiMjKYP3yb7GUFPl6SUIIIYRog/58SS8+u3sk04ZIj5j6MHlUBJj0GjTq5h3fGhloaLbXUKtVHs3UfLOvu8FB99q1a5k5cyYxMTH85S9/Yfr06axfv74p1ybEOS8/JZWQAwfpUZSPKezcynQDRIWGcYcjkO6bNpJy6CAnd/zu6yUJIYQQog3y12sY1T3c60Zd5ypltjW0j9nmRr1vx4Z59bcuJSWF559/nl69ejF27FiOHDnC66+/TkpKCu+88w4jR45srnUKcU7KPXSUgX9sZVJhLgER52ZTokceepjd6YXk5OSy/ZevsVnLfL0kIYQQQoh2zbNxWntoPqfsUy/2UdBd73dw8uTJrFixgoiICG699VbuuOMOevfu3ZxrE+Kcd+jQIfqaDJgCTAREdPD1cnwiOjqaCy6/mjPJW1mcncuo3VuIG3K+r5clhBBCCNFueTZTaxdBt5Lp9lF5eb3fQZ1Ox5dffskVV1yBRqOp+wuEEI1itVk5fDadhBgdgYGBBISfm5lugFsfuJ8rfwgBtZoff13KfQmjUKmlPEwIIYQQojmYDO2rvFy5iNDqG6l99913XHnllRJwC9FC1iT9geO2a/l6/BWEdYxDo9P7ekk+c37CECJynU3UPs8+S/qhXT5ekRBCCCFE+9XeysvL93S3sUZqQojm9cuOrQCElpYQGi2dNh+/ZDIAe4PDWbvqOxwOh49XJIQQQgjRPrXXTLev9nRL0C1EK7XpZDIAnS2lhHSM9+1iWoE7Jl2B8WwedrWaj5KPk3PqqK+XJIQQQgjRLnlmuoPaU6a7rY0ME0I0r2Szs5y6l8pBSOeuPl5N63DHoBEAbAsKZcean3y8GiGEEEKI9smzkZpnAN5WKd9Dq9/TLYRoOdl5uZQEmQAYbPInKLqzj1fUOjx5461o8woo0+pYtDuJgswUXy9JCCGEEKLdMbW7Pd2uRmpSXi6EUHzw68+g0eBvLmVg155otG1/L01T0Gq0TO3YFV1xMfkZ6RzduNLXSxJCCCGEaHcC2t2ebuf3Uyzl5UIIxXe7/wCga0khUd37+Xg1rcvLd95HyCffEHfsMDtW/YSluNDXSxJCCCGEaFeUudYgme6mIEG3EK2Mw+Eg9dffGLB/NxfYy4js3t/XS2pVgkwB3HznfWQUlHLs6BHO7N3q6yUJIYQQQrQr7a57uesiQrGMDBNCAOzduxfz4f0MOnmUcZ07ExjV0ddLanXuvfdeDmQXs8dg5OcV38v4MCGEEEKIJtTe5nQrFxEKpZFa7eLj41GpVBVu//rXvyocs2vXLsaMGYOfnx+xsbG8+OKLPlqtEA23ZMkS+kYHEhUVSezAEahUKl8vqdWJiIhAfe11/D5sFF+fzePsicO+XpIQQgghRLvh2b08qB1kuo0yp7v+nnnmGVJTU923P/3pT+7n8vPzufTSS+nSpQvbt2/npZde4umnn+a///2vD1cshHfMFgv/O3UQbf/eRHfsSMf+I3y9pFbr9gvGAbA3KIQ9m1f5eDVCCCGEEO1Hu8t066WRWr0FBgYSExPjvplMJvdzn3zyCRaLhffee4/+/fszY8YMHnroIebPn+/DFQvhnX9+9hFlvbuxfeBQug0cSlB0J18vqdV6cOp0tPmFlGl1fLpjK6WFeb5ekhBCCCFEu+C5pzugHQTd0kjNC//6178IDw9nyJAhvPTSS1it5W/axo0bufDCC9Hr9e7HJk6cyMGDB8nJyan2fGazmfz8/Ao3IXzFbrfz3p5tAIwoyKXvyEt8vKLWTaPRMDooAoB1Gj2p+3f4eEVCCCGEEO1Dh2A/JvaP5sbEOHSaNhUyVkvJ3BdJprt2Dz30EIsXL+a3337j3nvv5fnnn+fxxx93P5+WlkZ0dHSFr1Hup6WlVXvOefPmERwc7L7FxsY23zcgRB1e/PxTSsOCUdtt3NAhipg+g329pFbvmRm3gM1ORmAwv675RRqqCSGEEEI0AZVKxdu3DOf5aQN9vZQmoezpLrJYffJ50adB9xNPPFGlOVrl24EDBwCYNWsWY8eOZdCgQdx333288sorvPHGG5jN5ga//uzZs8nLy3PfTp061VTfmhBeKSgq4rU/NgAwIjebMROmo9a0/VKe5jaga3fC85xzun9Mz6AgI8XHKxJCCCGEEK2Nsqfb4YDSMnuLv75PP9U/+uijzJw5s9ZjunXrVu3jiYmJWK1Wjh8/Tu/evYmJiSE9Pb3CMcr9mJiYas9hMBgwGAzeL1yIJnbdK//EGhKEwWLhvu7d6dh/uK+X1GZM7zeYd9KOcdIBZ/ZsISh6mq+XJIQQQgghWhF/XXk39iKLFX+9ppajm55Pg+7IyEgiIyMb9LVJSUmo1WqioqIAGDVqFH//+98pKytDp3O2tV++fDm9e/cmNDS0ydYsRFP78bdVbFWXARquLc3noumPolK3mZ0fPvf4NTfw9ZgRXBBkZ6dRR++xU6RKQAghhBBCuKnVKox6DcUWm7ODeUALv37LvlzDbNy4kddee42dO3dy7NgxPvnkEx555BFuvvlmd0B94403otfrufPOO9m7dy9Llizh3//+N7NmzfLx6oWoWUpKCrPvmskFOzYxIDudP029nqAo6VjujdDAQMYMO5+SMhsnjx0h+/ghXy9JCCGEEEK0MkpH9kJzy3cwbxNBt8FgYPHixVx00UX079+ff/7znzzyyCMVZnAHBwfz66+/kpyczLBhw3j00UeZM2cO99xzjw9XLkTNsrKyuPryiZwXpaOvuYgXRiTSbeR4Xy+rTbrl1ls5lJHPyfQMDm1f7+vlCCGEEEKIVsbkKikv9sHYsDZRgzl06FA2bdpU53GDBg1i3bp1LbAiIRpnx8EDTF/wIiPDVYSojYydfCXDpt2OSqXy9dLapDFjxnAqcTSH+vVE+8dmzpt6Ezo/o6+XJYQQQgghWgmjq5lakaXlx4a1iUy3EO3Jp6uWM/njNynsEEXS8FGMnXwlF9/1OFq9NPVrKLVaTc9efbFptPzuUJN2YKevlySEEEIIIVoRk2tsWLGUlwvRfpVZrVz7r6d5eMOv2IxGQooKmdOtO5MeeBKdn7+vl9fmPTr5SgBOBIawY+saH69GCCGEEEK0JkqmW/Z0C9FOLfltBb3/9iC/OUpxaDR0yz3Lh6PHcMNdj6HV+/l6ee3CpMRRGHLycKjVfHP4EMW52b5ekhBCCCGEaCUCXI3UiqW8XIj25cSJE8x44E4e2Lic/NAQtFYr0wpy+PmBxxl92fUyGqyJXRTVGYAtOgOp+7b7eDVCCCGEEKK1MLoaqRX5oJGafOIXohns3L+Ph+64mVlXjiF89zrC8nLonZvFu/0HsXDuG4TH9fD1Etulv199A9jtZAQEs2rDKhwOh6+XJISow8GDBxk8eLD75u/vz9KlS329LCGEEO2MMjKs2Nzyme420b1ciLbAUlbGvxZ/yOJdW8k1+jEleTsdgvwIj4jg+bh4Lr/lfvyDQn29zHatf9duhOQUkBsezE/p6VybepKQjl18vSwhRC169+5NUlISAIWFhcTHxzNhwgTfLkoIIUS7o2S6fbGnW4JuIRrp299X8+ayb9mjsmE2GiEsBICi7r2YMeICLrnhLgm2W9CMPgl8t+wrOpYVkLJ3mwTdQrQh3333HZdccgkmk8nXSxFCCNHOuDPdUl4uROvncDjYv28vf376CeL/9gB3rlvGdpMBs9GIvszCiNxs/tu7P+//ZzFX3PsXCbhb2OwbbsGy8Q9UOWfZ8/ty7LaW/8EqRHuydu1apkyZQseOHVGpVNWWfi9YsID4+Hj8/PxITExky5YtDXqtzz//nOuvv76RKxZCCCGqMrn3dEt5uRCtUnFpKe/88DW71q+hdNt6TPZSykwmCsdNRm230SU/jwtDI3hoxh3E9eyPSqXy9ZLPWSaTicRLLqP40O+cTD5KVvIBonoM8PWyhGizioqKSEhI4I477mD69OlVnl+yZAmzZs1i4cKFJCYm8tprrzFx4kQOHjxIVFQUAIMHD8ZqrXoB7Ndff6Vjx44A5Ofns2HDBhYvXty835AQQohzktG9p1vKy4VoFex2Oyu3bmLJ6l/ZkZ1GSoAJq05HB8xcpLKg1mmIDgjkFouF26+4hgFDElGrNb5etnC54eab+MvLB0kK78D5SRsl6BaiESZPnszkyZNrfH7+/Pncfffd3H777QAsXLiQH3/8kffee48nnngCwL1nuzbffvstl156KX5+tY9RNJvNmM1m9/38/Px6fBdCCCHOdSbXnG7JdAvhIw6Hg31797D25+/4IO04p03+lPobnU+GhgDgZzETodVy/rSbufCKa+jYo69ktFupi8ddTPraH7GbjHy+fQvDr7gJnZ/R18sSot2xWCxs376d2bNnux9Tq9WMHz+ejRs3enWuzz//nHvuuafO4+bNm8fcuXO9XqsQQohzm9HgKi+XTHf78cfGdRxIO4NFrcbf4Ie/wYC/nz8mPyNGPz+MRhMRQcEY/Y3oDQYJ3lqY3W7n180b+GLtcg6lnSZ+8+8EauyoVSryR1xAqb8Rtd1OVFEBPVQaLu3Zl5unXkdQWKSvly7qQa/TMVhrZAewCg0pe7fTZdgYXy9LiHYnKysLm81GdHR0hcejo6M5cOBAvc+Tl5fHli1b+Oqrr+o8dvbs2cyaNct9Pz8/n9jY2PovWgghxDkpwN1ITTLd7cbHLz/Fbx07cLJjzR8Epi//Dr3VCjjY0n8IJzrFobY7UNvtqBx21A6H877Dzrik7fjZ7KjUao50iuVMWBhqQANoHKBWqdAAWlScl5dPoFqDRqvltL8/6QY9WrUGnUaDTq1Br9Xip9Vh0OlJCAwmxBiAwc+fIo2aYo0ak7+RAP8AAk0mggODCDSZCPQ3EuDvj1rdNnvv2e12ftm8ni/WrmBndjpp/gbMfv7OJ6Mi6W3Uo7OVYQoJZ4JaS2xUR26cOJUOnbrIBZE26tnrbuHybz7gTFAIy1Z+zz1DL5A/SyFaqeDgYNLT0+t1rMFgwGAwNPOKhBBCtDfKyDDJdLcj/oHB6Gx2/EtLsKvUONQq7Co1drXzBqC2O1xHq7CrNdg0Wmw1bAtWmQtRWSwA5GriSA2puSN2t6SNlJYUA7Cn9wAOdOvl8awd7BawWMBSxKRlXxBS6NwPt6dHH/b07FfjeS9ct5LQ3BxQqUmO78ah7r3ROOyo7Q40dudFAo3DgRYYfCaFiDIrGp2OzKAgTgQFoVWr0anV6NQaDFoteo0Wg1ZHf4ORGKMJvZ8/RVot2SoHAX7+zuDfGECQKYAAo5FgUwDhgUEEmUzodLpaAyi73c6+PbtYt+x7Dm7fyLogEye6dXc+6SoXV9ttRBcW0l2tYcLMB7l0whWERHWUwKydSOzXn/D3C8kOC+KrrCymJx8gsltfXy9LiHYlIiICjUZTJWBOT08nJibGR6sSQgghqlL2dEumux15btE3PFfDczabjdLSUqz3PU6ZxYy5pISM/Fxyi4soLi2h1GympLQEc5mFEouZUouFbrc9DGVllFnMdCgpJtVqocxqxWK3UWazYbXbsdqd/+3aZwjaMgt2q5V4Pz9UuTnYHGBXObChwgbYAKtKRYCfEb0DHA4bBpUaY2kJNrUam1pT4QIBgL8a/LXO+w6djlJ//xq//74HdlKWk00ZkNKlO3tiO1VzlB3sZnLXrqJjlvMD29HOXdg6cFiN5z1v2waiz5zG7nCQ2iGW3UOGo7bbUdvtrsDfjsbuoNhg4MJt6wkvyAMgRhXHKZuN6KICuqu1XNi9NzdOvJLojrESZLdj9428iH8e+oOdQWFsXPU9UyXoFqJJ6fV6hg0bxsqVK7nqqqsAVyPKlSt58MEHfbs4IYQQwoN7T7fFisPhaNEYQIJuH9BoNJhMJjCZ3I9VF5L6isPhwGG3YbdZsZjNFBQWkF9UiOrKGykrLcFcWkJafi4pBfkUl5ZQYjFTYjZTUmahtMyCxWql+/AxGCwWrGUWdHYbfgUFlNntWB12ygCrw4FNBVYgwt9EQFgkDruNAK2e0KICbCq1K/hXO/9fo8au1mBQlV+lyvLTU6bX1/h9ZEdGEa/TE9m1FxOHn897F08iukPnlnkTRavw8LRreeWvqykNC+adgwe44NRRwmK7+3pZQrQphYWFHDlyxH0/OTmZpKQkwsLCiIuLY9asWdx22/+3d+dxUZX7H8A/ZxZmWAcEZJFFMRQzVBRFpK6VuOeSZmqmaFpprnUrq19u19Ssa5t1zboo2WLazTRtMVS09KKiiCuhEuIGLsi+DjPP7w9iriMMm8wMxOf9es1Lzjnf85zv+TrPOfPMmTkThdDQUPTq1QvvvfceCgsLDXczJyIiagoqv9MtBFCs1cHOxnJDYQ66qQpJkiDJFZDJFVDYqGHnqIFH7as1OiEE9Lpy6Mu10JVroS0rQ0nRdJQWF6O0uAg383NxNTcXRSVFKCguRlFpCYrKSlFcVoLWGheMnTILHp4cZLdkMpkMz3QLw6dHf4P6UipO/PIt+j71Ej/dQFQPR44cwUMPPWSYrryJWVRUFGJiYjB27FjcuHEDCxcuRGZmJrp164aff/65ys3ViIiIrEmtkEOSKgbdhaWWHXRLQghRe1jLkJeXB41Gg9zcXDg5OVk7HSJqBHq9Hvf37oWuNrlo6++LqAXvwDOom7XTIqqC56CGYd2IiKiuOi/8GYVlOux76UH4u9rXvkIN6nP+aZ63oiYiqiOZTIZ3V3+ExMvZuHTpMn756mOUFRdaOy0iIiIisjC7Pz9iXmDhO5hz0E1Ef3lhYWHoNmAUEn0CsAA22LnpE/BDPkREREQti/2fPxtm6TuYc9BNRC3CP1aswAW/9ii0tcNrKSk4/NNmDryJiIiIWhD7P690W/q3upvFoHvv3r0VN/eq5pGQkAAAuHDhQrXLDx48aOXsiagp8HB1xbpREyGVliHDyRnT9u/F3u8+g9DrrZ0aEREREVmAtX6ru1kMuvv06YOMjAyjx7Rp09CuXTuEhoYaxe7atcsorkcP07/5TEQty6BevbGy98OQlZYiw1GDqBPHsGD5i8i7ftXaqRERERGRmRl+q9vCV7qbxU+G2djYwNPT0zCt1Wqxbds2zJ49u8pP/7i6uhrFEhHd7qlBj8DZ3gGzfvwPipwc8DGAA8texiQXVzw8eiJ87+0Gmdy6h8bi0hJcyLiCi1ev4MrNa8i8dRM383KRXViA3NJi+OflwT43F6XFhbikVCDJyxtauQxKnQ5uAvCxc0AnL1/07BSMXsEhcHN2ser+EBERETUFlVe6Oeiug++//x5ZWVmYMmVKlWXDhw9HSUkJOnTogJdffhnDhw+3QoZE1JSNeuBB3H9fFzy+ahlOq2WwvX4NR48dxrG9P0Pn4YVzbTugjb0T7nH3hG9rL/h4esPP0xsuzi5wsHeAjY0aAgIQAuW6chQUlaCwpAgFhQUoLCxETn4esvKy4aDVQhQVo6ggH2m52ThTUojCslIUl2tRrNOhRK9DMYBSmYTO58+i1c1rEOVaXGrji4RuPatPXqVE76t/oO3VSwCAktZeuKnpZFh8E8DvAHZlZQD7M9BrzXL4XbkCucoWBe6eOO/jBwe5HI5KFVzUdnB1dEJrjQs8W7mhvbsXPFq5wlGjgb2jE1QqVYv4TfNyXTmKS0tRUlqK4tISCK0WCiGgLStDSWkJLuXmoFRbitIyLUq1ZSjTlqFMq0WZVgsHCXCDDOH9BsLVnb9LTURE1JTZ/XkjtUILf7y8WQ66o6OjMXDgQPj4+BjmOTg4YNWqVYiIiIBMJsO3336LkSNHYuvWrSYH3qWlpSgtLTVM5+XlmT13ImoaWru0wt43VuHo2d+x/fMYnLxwFRpRjMs2tkh0boVEAMi5WfE4e9Kw3t8SDqDNzWuQZBLO+bRFQucQk9t44Mh/0eZGJgAgrY0fDnUJBdQ2AGyqxAYoJCj1WkAGqHUV774qtVrYlGthU14OlV4PtV4PNQBfN2/4evhBbe+Ido6O6GBrB2d7R9zMz8X5WzeQoS1FtkKBArUtbEtKoBA6oKQAN/VlSHF2vm2r5UD+rYrH5VSEJx2Gf8ZlAMBlDy8c6BYGuV4HuU4HhU4PmdBDEgIyIXBfehp8sm9BkmTIdtLguJ8/ZADklfemkwDpz787ZGWhTX4+ACBbrcbxPz+NVN1wvn32LbTJzYXQ65GrUuGIjw8E8OdDqvhXqphul5mBgGsZEHqBfFtb/Pfe+yAgAX8uF5IEvSRBSBLaX7qAoLTzgBAosLXDLxEPVSyXyYA73ljocOE8uiefAAAUq9TY9vAQk//HAZfS0OvUMWhcXPDAwGEm44iIiMj6Km+kVlTWgq50v/LKK1i5cmWNMcnJyQgKCjJMX758GTt37sTmzZuN4tzc3PDCCy8Ypnv27ImrV6/i7bffNjnoXrFiBZYsWXIXe0BEzV2PDkHosfRNiH+sQGpqKmJ+3Apd5kXcLC9DnkIOrVKBMqUN9PKKd0ZlQlQM6PQCUjU3YZP0esj0eij0OkBpA5mtI+RKJVrJVWibnQ0bACpJBlu5HHZKGzip1GhlZ4/gIWPRzt0DLm7ucGrlhlbuHnBwcIQka/itN3Q6HbKuZeBi6llcuZCKpMvpUBXlI79ci8LKq+ySDKUKOcoUCqiEHjKZBL1eoFyugJDJUC6ToVyhROkdbWt1ZUBBNgSAQlsbZNTwEXb3KxfgmpkGAMhr5Y4LnTqZjNVkXoJb5gUAQKGzC644B5uM9bh+FSjMgQRALxfIcXQyGatVKCDXV5xg5UIHncL06U/8OQiXSRLkEqAo10ImhOENB0kIyPR6SAKw1ekhs3OEjdrWZHtERETUNNgbvtNt2SvdkrDib+bcuHEDWVlZNcYEBATAxuZ/V4WWLl2K1atX48qVK1AqlTWu+9FHH+GNN95ARkZGtcuru9Lt6+uL3NxcODmZfvFGRC2LXq9Hdn4+CoqLINeVQyovh1ZbjjKhh14mg51aDXs7WzjaO8LGRmXtdO+KXleO0uJi3Lx1E1dv3sCtvBzkFOQjt7Cg4mPV5VqUlZfDU5LBUS9Qri3DjbIynNWWQKvToVyvg9BXvDFRQcBfD3ig4upzLoDfZTD6uTZx27/+egEfSQZJJkexXMLvkgS5JINcJkEmySCTySD/8+GttEEbGzXkcjlKJQl/aMugkMshk8mgkMshl8mhlMuglCvhbmsHT3sHKJRKQK5Atl4PlY0SahsVbFVqqFVqqFQ2sFWpYWdrBxulEjK5wqIfr8/Ly4NGo+E5qJ5YNyIiqqv41CwkXsxGVx9n3B/odldt1ef8Y9Ur3e7u7nB3d69zvBAC69evx6RJk2odcANAUlISvLy8TC5XqVRQqZr3C2QiMj+ZTAZXjQauGo21UzE7mVwBWwdH+Do4wtevnbXTISIiImo04e1dEd7e1eLbbVbf6d6zZw/S0tIwbdq0Kss+++wz2NjYICSk4vuVW7Zswbp16/Dvf//b0mkSERERERERAWhmg+7o6Gj06dPH6Dvet1u6dCnS09OhUCgQFBSETZs24bHHHrNwlkREREREREQVrPqd7qaG3wsjIiJr4TmoYVg3IiKyhvqcfxp+W1wiIiIiIiIiqhEH3URERERERERmwkE3ERERERERkZk0qxupmVvl19vz8vKsnAkREbU0lece3mqlfnjuJiIia6jPeZuD7tvk5+cDAHx9fa2cCRERtVT5+fnQtIDfhG8sPHcTEZE11eW8zbuX30av1+Pq1atwdHSEJEl31VZeXh58fX1x6dKlZnk31eacP3O3nuacP3O3nuacf2PmLoRAfn4+vL29IZPx2191xXO39bBe9cea1Q/rVX+sWf3cTb3qc97mle7byGQy+Pj4NGqbTk5OzfoJ35zzZ+7W05zzZ+7W05zzb6zceYW7/njutj7Wq/5Ys/phveqPNaufhtarrudtvpVOREREREREZCYcdBMRERERERGZCQfdZqJSqbBo0SKoVCprp9IgzTl/5m49zTl/5m49zTn/5pw7VcX/z/phveqPNasf1qv+WLP6sVS9eCM1IiIiIiIiIjPhlW4iIiIiIiIiM+Ggm4iIiIiIiMhMOOgmIiIiIiIiMhMOuu/CRx99hLZt20KtViMsLAyHDx+uMf6bb75BUFAQ1Go1goOD8eOPP1ooU2MrVqxAz5494ejoiNatW2PkyJFISUmpcZ2YmBhIkmT0UKvVFsr4fxYvXlwlj6CgoBrXaSp1B4C2bdtWyV+SJMycObPaeGvW/ddff8WwYcPg7e0NSZKwdetWo+VCCCxcuBBeXl6wtbVFZGQkzp07V2u79e03jZ27VqvF/PnzERwcDHt7e3h7e2PSpEm4evVqjW025LlnjvwBYPLkyVVyGTRoUK3tWrv2AKp9/kuShLfffttkm5aqfV2OjSUlJZg5cyZcXV3h4OCA0aNH49q1azW229C+QpZlif7RHJmrX7Qkb775JiRJwrx58wzzWDNjV65cwZNPPglXV1fY2toiODgYR44cMSzncdSYTqfDggUL0K5dO9ja2qJ9+/ZYunQpbr9VV0uvWWO8jr116xYmTJgAJycnODs7Y+rUqSgoKGhQPhx0N9CmTZvwwgsvYNGiRUhMTETXrl0xcOBAXL9+vdr4//73vxg/fjymTp2KY8eOYeTIkRg5ciROnTpl4cyBffv2YebMmTh48CBiY2Oh1WoxYMAAFBYW1riek5MTMjIyDI/09HQLZWysc+fORnns37/fZGxTqjsAJCQkGOUeGxsLABgzZozJdaxV98LCQnTt2hUfffRRtcvfeustfPDBB/j4449x6NAh2NvbY+DAgSgpKTHZZn37jTlyLyoqQmJiIhYsWIDExERs2bIFKSkpGD58eK3t1ue5dzdqqz0ADBo0yCiXjRs31thmU6g9AKOcMzIysG7dOkiShNGjR9fYriVqX5dj4/PPP4/t27fjm2++wb59+3D16lWMGjWqxnYb0lfIsizVP5ojc/WLliIhIQFr165Fly5djOazZv+TnZ2NiIgIKJVK/PTTTzhz5gxWrVoFFxcXQwyPo8ZWrlyJNWvW4MMPP0RycjJWrlyJt956C6tXrzbEtPSaNcbr2AkTJuD06dOIjY3Fjh078Ouvv+KZZ55pWEKCGqRXr15i5syZhmmdTie8vb3FihUrqo1//PHHxdChQ43mhYWFiWeffdasedbF9evXBQCxb98+kzHr168XGo3GckmZsGjRItG1a9c6xzflugshxNy5c0X79u2FXq+vdnlTqTsA8d133xmm9Xq98PT0FG+//bZhXk5OjlCpVGLjxo0m26lvv2kMd+ZencOHDwsAIj093WRMfZ97jaW6/KOiosSIESPq1U5Trf2IESPEww8/XGOMtWp/57ExJydHKJVK8c033xhikpOTBQARHx9fbRsN7StkWdboH81VY/SLliI/P18EBgaK2NhY0bdvXzF37lwhBGt2p/nz54v777/f5HIeR6saOnSoeOqpp4zmjRo1SkyYMEEIwZrdqSGvY8+cOSMAiISEBEPMTz/9JCRJEleuXKl3DrzS3QBlZWU4evQoIiMjDfNkMhkiIyMRHx9f7Trx8fFG8QAwcOBAk/GWlJubCwBo1apVjXEFBQXw9/eHr68vRowYgdOnT1sivSrOnTsHb29vBAQEYMKECbh48aLJ2KZc97KyMnzxxRd46qmnIEmSybimUvfbpaWlITMz06i2Go0GYWFhJmvbkH5jKbm5uZAkCc7OzjXG1ee5Z2579+5F69at0bFjR8yYMQNZWVkmY5tq7a9du4YffvgBU6dOrTXWGrW/89h49OhRaLVaozoGBQXBz8/PZB0b0lfIsppq/2iqGqNftBQzZ87E0KFDq7wOYc2Mff/99wgNDcWYMWPQunVrhISE4NNPPzUs53G0qj59+mD37t04e/YsAOD48ePYv38/Bg8eDIA1q01d6hMfHw9nZ2eEhoYaYiIjIyGTyXDo0KF6b5OD7ga4efMmdDodPDw8jOZ7eHggMzOz2nUyMzPrFW8per0e8+bNQ0REBO677z6TcR07dsS6deuwbds2fPHFF9Dr9ejTpw8uX75swWyBsLAwxMTE4Oeff8aaNWuQlpaGBx54APn5+dXGN9W6A8DWrVuRk5ODyZMnm4xpKnW/U2X96lPbhvQbSygpKcH8+fMxfvx4ODk5mYyr73PPnAYNGoQNGzZg9+7dWLlyJfbt24fBgwdDp9NVG99Ua//ZZ5/B0dGx1o9UWqP21R0bMzMzYWNjU+XNmdqO/ZUxdV2HLKup9o+mqLH6RUvw9ddfIzExEStWrKiyjDUz9scff2DNmjUIDAzEzp07MWPGDMyZMwefffYZAB5Hq/PKK69g3LhxCAoKglKpREhICObNm4cJEyYAYM1qU5f6ZGZmonXr1kbLFQoFWrVq1aAaKhqYK/1FzJw5E6dOnar1+5Hh4eEIDw83TPfp0wedOnXC2rVrsXTpUnOnaVD5Dh4AdOnSBWFhYfD398fmzZvrdLWsKYmOjsbgwYPh7e1tMqap1P2vSqvV4vHHH4cQAmvWrKkxtik998aNG2f4Ozg4GF26dEH79u2xd+9e9OvXz6K53I1169ZhwoQJtd4c0Bq1r+uxkaglYb+om0uXLmHu3LmIjY21yk1nmxu9Xo/Q0FAsX74cABASEoJTp07h448/RlRUlJWza5o2b96ML7/8El999RU6d+6MpKQkzJs3D97e3qxZE8Ur3Q3g5uYGuVxe5S6T165dg6enZ7XreHp61iveEmbNmoUdO3YgLi4OPj4+9Vq38l218+fPmym7unF2dkaHDh1M5tEU6w4A6enp2LVrF6ZNm1av9ZpK3SvrV5/aNqTfmFPlgDs9PR2xsbE1XuWuTm3PPUsKCAiAm5ubyVyaWu0B4LfffkNKSkq9+wBg/tqbOjZ6enqirKwMOTk5RvG1HfsrY+q6DllWU+wfTVFj9ou/uqNHj+L69evo3r07FAoFFAoF9u3bhw8++AAKhQIeHh6s2W28vLxw7733Gs3r1KmT4WtEPI5W9dJLLxmudgcHB2PixIl4/vnnDZ+sYM1qVpf6eHp6VrmZZnl5OW7dutWgGnLQ3QA2Njbo0aMHdu/ebZin1+uxe/duo6uStwsPDzeKB4DY2FiT8eYkhMCsWbPw3XffYc+ePWjXrl2929DpdDh58iS8vLzMkGHdFRQUIDU11WQeTanut1u/fj1at26NoUOH1mu9plL3du3awdPT06i2eXl5OHTokMnaNqTfmEvlgPvcuXPYtWsXXF1d691Gbc89S7p8+TKysrJM5tKUal8pOjoaPXr0QNeuXeu9rrlqX9uxsUePHlAqlUZ1TElJwcWLF03WsSF9hSyrKfaPpsQc/eKvrl+/fjh58iSSkpIMj9DQUEyYMMHwN2v2PxEREVV+hu7s2bPw9/cHwONodYqKiiCTGQ/j5HI59Ho9ANasNnWpT3h4OHJycnD06FFDzJ49e6DX6xEWFlb/jTbsHnD09ddfC5VKJWJiYsSZM2fEM888I5ydnUVmZqYQQoiJEyeKV155xRB/4MABoVAoxD//+U+RnJwsFi1aJJRKpTh58qTFc58xY4bQaDRi7969IiMjw/AoKioyxNyZ/5IlS8TOnTtFamqqOHr0qBg3bpxQq9Xi9OnTFs3973//u9i7d69IS0sTBw4cEJGRkcLNzU1cv3692rybUt0r6XQ64efnJ+bPn19lWVOqe35+vjh27Jg4duyYACDeeecdcezYMcMdvt98803h7Owstm3bJk6cOCFGjBgh2rVrJ4qLiw1tPPzww2L16tWG6dr6jSVyLysrE8OHDxc+Pj4iKSnJqA+UlpaazL22556l8s/PzxcvvviiiI+PF2lpaWLXrl2ie/fuIjAwUJSUlJjMvynUvlJubq6ws7MTa9asqbYNa9W+LsfG6dOnCz8/P7Fnzx5x5MgRER4eLsLDw43a6dixo9iyZYthui59hazLUv2jOWqsftHS3X73ciFYs9sdPnxYKBQKsWzZMnHu3Dnx5ZdfCjs7O/HFF18YYngcNRYVFSXatGkjduzYIdLS0sSWLVuEm5ubePnllw0xLb1mjfE6dtCgQSIkJEQcOnRI7N+/XwQGBorx48c3KB8Ouu/C6tWrhZ+fn7CxsRG9evUSBw8eNCzr27eviIqKMorfvHmz6NChg7CxsRGdO3cWP/zwg4UzrgCg2sf69esNMXfmP2/ePMO+enh4iCFDhojExESL5z527Fjh5eUlbGxsRJs2bcTYsWPF+fPnTeYtRNOpe6WdO3cKACIlJaXKsqZU97i4uGqfJ5X56fV6sWDBAuHh4SFUKpXo169flX3y9/cXixYtMppXU7+xRO5paWkm+0BcXJzJ3Gt77lkq/6KiIjFgwADh7u4ulEql8Pf3F08//XSVwUFTrH2ltWvXCltbW5GTk1NtG9aqfV2OjcXFxeK5554TLi4uws7OTjz66KMiIyOjSju3r1OXvkLWZ4n+0Rw1Vr9o6e4cdLNmxrZv3y7uu+8+oVKpRFBQkPjkk0+MlvM4aiwvL0/MnTtX+Pn5CbVaLQICAsT//d//GV08aOk1a4zXsVlZWWL8+PHCwcFBODk5iSlTpoj8/PwG5SMJIUT9r48TERERERERUW34nW4iIiIiIiIiM+Ggm4iIiIiIiMhMOOgmIiIiIiIiMhMOuomIiIiIiIjMhINuIiIiIiIiIjPhoJuIiIiIiIjITDjoJiIiIiIiIjITDrqJiIiIiIiIzISDbiIiIiIiK1m8eDG6detm7TQa1V9xn4juBgfdRC3E5MmTMXLkSKttf+LEiVi+fLnZ2j9z5gx8fHxQWFhotm0QERHVJj4+HnK5HEOHDrV2Ks2KJEnYunWrtdMgMgsOuon+AiRJqvGxePFivP/++4iJibFKfsePH8ePP/6IOXPmmG0b9957L3r37o133nnHbNsgIiKqTXR0NGbPno1ff/0VV69etXY6RNQEcNBN9BeQkZFheLz33ntwcnIymvfiiy9Co9HA2dnZKvmtXr0aY8aMgYODg1m3M2XKFKxZswbl5eVm3Q4REVF1CgoKsGnTJsyYMQNDhw6t9s3uN998Ex4eHnB0dMTUqVNRUlJitDwhIQH9+/eHm5sbNBoN+vbti8TERKMYSZKwdu1aPPLII7Czs0OnTp0QHx+P8+fP48EHH4S9vT369OmD1NRUk7nu3bsXkiQhJyfHMC8pKQmSJOHChQsAgJiYGDg7O2Pr1q0IDAyEWq3GwIEDcenSpUbdp7Zt2wIAHn30UUiSZJgGgG3btqF79+5Qq9UICAjAkiVLeJ6nZoeDbqK/AE9PT8NDo9FAkiSjeQ4ODlU+Xv7ggw9i9uzZmDdvHlxcXODh4YFPP/0UhYWFmDJlChwdHXHPPffgp59+MtrWqVOnMHjwYDg4OMDDwwMTJ07EzZs3Team0+nwn//8B8OGDTOa37ZtW7zxxhuYNGkSHBwc4O/vj++//x43btzAiBEj4ODggC5duuDIkSOGddLT0zFs2DC4uLjA3t4enTt3xo8//mhY3r9/f9y6dQv79u27y4oSERHV3+bNmxEUFISOHTviySefxLp16yCEMFq+ePFiLF++HEeOHIGXlxf+9a9/GbWRn5+PqKgo7N+/HwcPHkRgYCCGDBmC/Px8o7ilS5di0qRJSEpKQlBQEJ544gk8++yzePXVV3HkyBEIITBr1qy73qeioiIsW7YMGzZswIEDB5CTk4Nx48Y16j4lJCQAANavX4+MjAzD9G+//YZJkyZh7ty5OHPmDNauXYuYmBgsW7bsrveLyKIEEf2lrF+/Xmg0mirzo6KixIgRIwzTffv2FY6OjmLp0qXi7NmzYunSpUIul4vBgweLTz75RJw9e1bMmDFDuLq6isLCQiGEENnZ2cLd3V28+uqrIjk5WSQmJor+/fuLhx56yGQ+iYmJAoDIzMw0mu/v7y9atWolPv74Y8O2nJycxKBBg8TmzZtFSkqKGDlypOjUqZPQ6/VCCCGGDh0q+vfvL06cOCFSU1PF9u3bxb59+4zaDQsLE4sWLWpY8YiIiO5Cnz59xHvvvSeEEEKr1Qo3NzcRFxdnWB4eHi6ee+45o3XCwsJE165dTbap0+mEo6Oj2L59u2EeAPH6668bpuPj4wUAER0dbZi3ceNGoVarTbYbFxcnAIjs7GzDvGPHjgkAIi0tTQhR8ZoCgDh48KAhJjk5WQAQhw4davR9+u6774zi+vXrJ5YvX2407/PPPxdeXl4m2yZqinilm6gF69q1K15//XUEBgbi1VdfhVqthpubG55++mkEBgZi4cKFyMrKwokTJwAAH374IUJCQrB8+XIEBQUhJCQE69atQ1xcHM6ePVvtNtLT0yGXy9G6desqy4YMGYJnn33WsK28vDz07NkTY8aMQYcOHTB//nwkJyfj2rVrAICLFy8iIiICwcHBCAgIwCOPPIK//e1vRm16e3sjPT29kStFRERUs5SUFBw+fBjjx48HACgUCowdOxbR0dGGmOTkZISFhRmtFx4ebjR97do1w3lYo9HAyckJBQUFuHjxolFcly5dDH97eHgAAIKDg43mlZSUIC8v7672S6FQoGfPnobpoKAgODs7Izk5udH36U7Hjx/HP/7xDzg4OBgeTz/9NDIyMlBUVHRX+0VkSQprJ0BE1nP7CVsul8PV1bXKCRsArl+/DqDi5BcXF1ftd7NTU1PRoUOHKvOLi4uhUqkgSVKN2zf1gqFy+56enpgzZw5mzJiBX375BZGRkRg9erRRGwBga2vLEzEREVlcdHQ0ysvL4e3tbZgnhIBKpcKHH34IjUZTp3aioqKQlZWF999/H/7+/lCpVAgPD0dZWZlRnFKpNPxdeY6tbp5er692OzKZzJBjJa1WW6cc66uu+3SngoICLFmyBKNGjaqyTK1WmyVXInPglW6iFuz2kzNQcYKu6YRdUFCAYcOGISkpyehx7ty5KlecK7m5uaGoqKjaE2t9XzBMmzYNf/zxByZOnIiTJ08iNDQUq1evNmrz1q1bcHd3r1sBiIiIGkF5eTk2bNiAVatWGZ0fjx8/Dm9vb2zcuBEA0KlTJxw6dMho3YMHDxpNHzhwAHPmzMGQIUPQuXNnqFSqGu+d0lCV58qMjAzDvKSkpCpx5eXlRvdXSUlJQU5ODjp16gSg8fZJqVRCp9MZzevevTtSUlJwzz33VHlUvmlA1BzwSjcR1Vn37t3x7bffom3btlAo6nb46NatG4CK39Gu/Ptu+Pr6Yvr06Zg+fTpeffVVfPrpp5g9e7Zh+alTp/DYY4/d9XaIiIjqaseOHcjOzsbUqVOrXNEePXo0oqOjMX36dMydOxeTJ09GaGgoIiIi8OWXX+L06dMICAgwxAcGBuLzzz9HaGgo8vLy8NJLL8HW1rbRc77nnnvg6+uLxYsXY9myZTh79ixWrVpVJU6pVGL27Nn44IMPoFAoMGvWLPTu3Ru9evUCgEbbp7Zt22L37t2IiIiASqWCi4sLFi5ciEceeQR+fn547LHHIJPJcPz4cZw6dQpvvPFGo9eEyFz4FhER1dnMmTNx69YtjB8/HgkJCUhNTcXOnTsxZcqUKu9OV3J3d0f37t2xf//+u97+vHnzsHPnTqSlpSExMRFxcXGGd9oB4MKFC7hy5QoiIyPveltERER1FR0djcjIyGo/Qj569GgcOXIEJ06cwNixY7FgwQK8/PLL6NGjB9LT0zFjxowqbWVnZ6N79+6YOHEi5syZU+19Ue6WUqnExo0b8fvvv6NLly5YuXJltQNZOzs7zJ8/H0888QQiIiLg4OCATZs2GZY31j6tWrUKsbGx8PX1RUhICABg4MCB2LFjB3755Rf07NkTvXv3xrvvvgt/f/9GrweROfFKNxHVmbe3Nw4cOID58+djwIABKC0thb+/PwYNGlTjx7ymTZuGDRs23PVPl+h0OsycOROXL1+Gk5MTBg0ahHfffdewfOPGjRgwYABPxkREZFHbt283uaxXr15G35t+7bXX8NprrxnFrFy50vB3SEiI4SezKt35Ca7b2wMqrhLfOe/BBx+sMu9OERERhpulmmobAEaNGlXt96orNcY+DRs2rMrPiwIVA++BAwea3gmiZkAStfVGIqK7VFxcjI4dO2LTpk1V7mjaWMrKyhAYGIivvvoKERERZtkGERFRSxITE4N58+YhJyfH2qkQNWv8eDkRmZ2trS02bNhglhvBVLp48SJee+01DriJiIiIqEnhlW4iIiIiIiIiM+GVbiIiIiIiIiIz4aCbiIiIiIiIyEw46CYiIiIiIiIyEw66iYiIiIiIiMyEg24iIiIiIiIiM+Ggm4iIiIiIiMhMOOgmIiIiIiIiMhMOuomIiIiIiIjMhINuIiIiIiIiIjP5f10V6RTWiapRAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'parameter': 'SodiumInitNernst.temp', 'initial': 300.0, 'target': 309.15, 'fitted': 309.10382080078125, 'initial_mse': 4.345213413238525, 'final_mse': 0.0003917326685041189, 'seconds': 7.0562503058463335}\n" + ] + } + ], + "source": [ + "results.append(fit_one(\"SodiumInitNernst\", \"temp\", 300 * u.kelvin, 309.15 * u.kelvin, \"V\"))" + ] + }, + { + "cell_type": "markdown", + "id": "c445e578", + "metadata": {}, + "source": [ + "## 3. 初始浓度\n", + "\n", + "学习 Ci(0),不是把整个轨迹的 Ci(t) 固定为 theta。C_rest 和 tau 均固定。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2fc7cde0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:19.286365Z", + "iopub.status.busy": "2026-09-07T05:09:19.285904Z", + "iopub.status.idle": "2026-09-07T05:09:21.677995Z", + "shell.execute_reply": "2026-09-07T05:09:21.677473Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAEiCAYAAADklbFjAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsZxJREFUeJzs3Xd4k9UXwPFv0r0nXVBoy55tWQVkb5CtgiIiKEtFEUQFRYb4AwdLFARRwM0SFwiIlb0KhTLLLmV17z2S/P4IDRQKtJA0HefzPHk0yfu+97TVNue9556r0Gg0GoQQQgghhBBCCKF3SmMHIIQQQgghhBBCVFSSdAshhBBCCCGEEAYiSbcQQgghhBBCCGEgknQLIYQQQgghhBAGIkm3EEIIIYQQQghhIJJ0CyGEEEIIIYQQBiJJtxBCCCGEEEIIYSCSdAshhBBCCCGEEAYiSbcQQgghhBBCCGEgknQLUcp8fHwYMWKEwc8p0LFjRzp27FisY0eMGIGPj88jjXN3jDt37kShULBz585Hul5ZNHPmTBQKhbHDEEIIIYQQ5Ygk3ULoyaVLlxg7dix+fn5YWlpib2/PE088weeff05WVpaxw9O5efMmM2fOJCwszNihlBnZ2dksXLiQoKAgHBwcsLS0pE6dOowfP57z588bOzwhhBBCCFGOKTQajcbYQQhR3m3evJlnnnkGCwsLhg8fTqNGjcjNzWXv3r38+uuvjBgxgq+//hqAnJwclEolZmZmxb7+o5xTIDc3FwBzc3MAjhw5QosWLVi1atU9s+d5eXmo1WosLCxKPI6Pjw8dO3Zk9erVAKjVanJzczE3N0epLLv39+Lj4+nZsyehoaH06dOHrl27Ymtry7lz51izZg3R0dG672F+fj75+flYWloaOWohhBBCCFFemBo7ACHKu4iICJ599llq1KjBf//9h6enp+691157jYsXL7J582bda4+S0D7KOQUKku3ieJSk/n6USmWpJ6eZmZlYW1uX6JwRI0Zw7NgxNmzYwFNPPVXovdmzZ/P+++/rnpuammJqKr82hRBCCCFE8ZXd6SchyolPP/2U9PR0vv3220IJd4FatWoxYcIE3XN9rOlevXo1CoWCffv2MWnSJKpUqYKNjQ0DBw4kLi6u0Ll3runeuXMnLVq0AGDkyJEoFAoUCoVudrqoNd3z5s2jTZs2uLi4YGVlRbNmzdiwYcNDY757TXdBzEU97l5z/uOPP9KsWTOsrKxwdnbm2Wef5dq1a/d8XY0aNSI0NJT27dtjbW3Ne++9B0BUVBRnz54lLy/vgTEeOnSIzZs38/LLL9+TcIP2Zse8efN0z2VNtxBCCCGEKCmZshHiMf3111/4+fnRpk2bUh/79ddfx8nJiRkzZnDlyhUWLVrE+PHjWbt2bZHH169fnw8//JDp06czZswY2rVrB/DA2D///HP69evH888/T25uLmvWrOGZZ55h06ZNPPnkk8WOtX379vzwww+FXouMjGTatGm4ubnpXvvf//7HBx98wODBgxk1ahRxcXF88cUXtG/fnmPHjuHo6Kg7NiEhgV69evHss88ybNgw3N3dAZg6dSrfffcdERERD2wM9+effwLwwgsvFPvrEEIIIYQQoiQk6RbiMaSmpnLjxg369+9vlPFdXFz4559/dLOvarWaxYsXk5KSgoODwz3Hu7u706tXL6ZPn07r1q0ZNmzYQ8c4f/48VlZWuufjx4+nadOmLFiwoERJt5+fH35+frrn2dnZtG3bFi8vLxYvXgxok/AZM2bw0Ucf6WatAQYNGkRgYCBLly4t9Hp0dDTLli1j7NixxY7jTuHh4QA0btz4kc4XQgghhBDiYaS8XIjHkJqaCoCdnZ1Rxh8zZkyhcud27dqhUqmIjIzU2xh3JtxJSUmkpKTQrl07jh49+ljXffXVVzl58iS//vorHh4eAGzcuBG1Ws3gwYOJj4/XPTw8PKhduzY7duwodA0LCwtGjhx5z7VXr16NRqN56PZnxv75CSGEEEKIik9muoV4DPb29gCkpaUZZfzq1asXeu7k5ARok2N92bRpEx999BFhYWHk5OToXn+ctc3Lly9n1apVLF++nFatWulev3DhAhqNhtq1axd53t2N3qpWrVqiRnF3u/Pnd2fZuhBCCCGEEPoiSbcQj8He3h4vLy9OnTpllPFNTEyKfF1fOwHu2bOHfv360b59e5YuXYqnpydmZmasWrWKn3/++ZGuGRISwoQJExg1ahRjxowp9J5arUahULBly5YivzZbW9tCz++chX8U9erVA+DkyZO69e1CCCGEEELokyTdQjymPn368PXXX3PgwAFat25t7HAeqiQz1L/++iuWlpZs27at0LZlq1ateqSx4+LiePrppwkICGDJkiX3vF+zZk00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bt2QmpqKvXv33vT2Fy1ahO3btyM/Px/Z2dnYtWuX71N1APjuu+9w8eJFZGVl3fS2iIiIbsbatWuRlZXV4C3kU6dOxeHDh3H8+HFMmzYNL7zwAn79619j6NChKCgowLx58+r1VVFRgdTUVMycORMLFixocKyUm6VSqbBp0yZ88803GDRoEFauXNlgIavT6bB48WL89Kc/xYgRI6DX6/Huu+/6lrfVa3r11VexY8cOxMbGIiUlBQAwbtw4bN26Ff/6178wbNgw3HnnnfjjH/+I+Pj4Ns8HUaDxSjcR1RMTE4N9+/Zh8eLFGDt2LJxOJ+Lj4zF+/Pgb3tL12GOPYePGjTf9MyVerxfz58/HhQsXYDQaMX78ePzxj3/0Ld+0aRPGjh3LEy8REXW4LVu2NLosPT3d73vTzz33HJ577jm/NitXrvQ9TklJ8f1kVp3r7+q6tj+g9irx9fNGjRpVb971RowY4RtAtbG+AeD+++9v8HvVddriNU2aNKneT44CtYX3uHHjGn8RRF2ETDS1RxIRNZPdbke/fv3w7rvv1hu9tK24XC4kJibi73//O0aMGBGQbRAREd3uNmzYgEWLFqGysrKjQyHq8nh7ORG1Ga1Wi40bNwZk0Jc6hYWFeO6551hwExEREVGXwCvdRERERERERAHCK91EREREREREAcKim4iIiIiIiChAWHQTERERERERBQiLbiIiIiIiIqIAYdFNREREREREFCAsuomIiIiIiIgChEU3ERERERERUYCw6CYiIiIiIiIKEBbdRERERERERAHy/wFno3X6l/OEGwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'parameter': 'CalciumDetailed.Ci_initializer', 'initial': 0.0008, 'target': 0.001, 'fitted': 0.0009999672183766961, 'initial_mse': 0.00501708360388875, 'final_mse': 1.323315623746879e-10, 'seconds': 2.273266172967851}\n" + ] + } + ], + "source": [ + "results.append(fit_one(\"CalciumDetailed\", \"Ci_initializer\", 0.0008 * u.mM, 0.001 * u.mM, \"Ci\"))" + ] + }, + { + "cell_type": "markdown", + "id": "c9434319", + "metadata": {}, + "source": [ + "## 4. 浓度清除时间常数\n", + "\n", + "初始浓度固定,只改变浓度趋近 C_rest 的速度。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f9e086d7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:21.684804Z", + "iopub.status.busy": "2026-09-07T05:09:21.684648Z", + "iopub.status.idle": "2026-09-07T05:09:23.596655Z", + "shell.execute_reply": "2026-09-07T05:09:23.596114Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'parameter': 'CalciumDetailed.tau', 'initial': 7.0, 'target': 5.0, 'fitted': 4.998579978942871, 'initial_mse': 0.0046277157962322235, 'final_mse': 2.8507436411473464e-09, 'seconds': 1.9560666140168905}\n" + ] + } + ], + "source": [ + "results.append(fit_one(\"CalciumDetailed\", \"tau\", 7 * u.ms, 5 * u.ms, \"Ci\"))" + ] + }, + { + "cell_type": "markdown", + "id": "49dec523", + "metadata": {}, + "source": [ + "## 5. 结合速率\n", + "\n", + "固定初始浓度、解离速率和缓冲物总量,拟合结合物 BC 的轨迹。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "89c55b0f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:23.602745Z", + "iopub.status.busy": "2026-09-07T05:09:23.602591Z", + "iopub.status.idle": "2026-09-07T05:09:26.559060Z", + "shell.execute_reply": "2026-09-07T05:09:26.558586Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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5ptv8t5777F48WJ27tyJu7s7gwcPpqCgoNxzVvb9U1Px5+XlsW/fPmbOnMm+fftYuXIlJ06c4M4777zueSvzGqyK611/gCFDhpSK5euvv77mOZ3l+gOl4k5ISGDp0qWoVCruvvvua563tq5/RT4vp06dys8//8x3333H5s2buXjxInfdddc1z2vP+0bUvtp8r9RFNfX+aCh2797NRx99RKdOnUptl2t2tfT0dPr27YtOp2PNmjUcPXqUd955B19fX1sb+Vy92rx58/jwww95//33OXbsGPPmzePNN99k4cKFtjZy3arnb90xY8Zw5MgR1q9fzy+//MKWLVt47LHH7AtIETY9e/ZUJk2aZLtvNpuV0NBQZe7cuWW2v+eee5Tbb7+91LZevXopjz/+eI3GWRHJyckKoGzevLncNsuWLVO8vb1rL6hrmD17thIVFVXh9s587RVFUaZMmaK0aNFCsVgsZe53pmsPKD/88IPtvsViUYKDg5W33nrLti0jI0MxGAzK119/Xe55Kvv+qS5/j78su3btUgAlLi6u3DaVfQ1Wl7LiHzt2rDJixIhKnceZr/+IESOUW2+99ZptHHX9FeXqz8uMjAxFp9Mp3333na3NsWPHFEDZvn17meew930jap+j3it1VXW8PxqK7OxspVWrVsr69euVW265RZkyZYqiKHLNyvPCCy8oN954Y7n75XO1bLfffrsyfvz4UtvuuusuZcyYMYqiyHUriz1/6x49elQBlN27d9varFmzRlGpVMqFCxcqHYP0dBczmUzs3buXgQMH2rap1WoGDhzI9u3byzxm+/btpdoDDB48uNz2tSkzMxMAPz+/a7bLycmhadOmhIWFMWLECI4cOVIb4ZUpJiaG0NBQmjdvzpgxY4iPjy+3rTNfe5PJxBdffMH48eNRqVTltnOma3+l2NhYEhMTS11fb29vevXqVe71tef9U5syMzNRqVT4+Phcs11lXoM1bdOmTQQGBtK6dWsmTpxIampquW2d+fonJSWxevVqHnnkkeu2ddT1//vn5d69eyksLCx1Pdu0aUN4eHi519Oe942ofc78XnFW1fH+aCgmTZrE7bffftXfJ3LNyvbTTz/RvXt3/vnPfxIYGEiXLl1YsmSJbb98rpatT58+bNy4kZMnTwJw4MABtm7dytChQwG5bhVRkWu0fft2fHx86N69u63N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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'parameter': 'ToyCaBindingKinetic_SU2015_DCN.kf', 'initial': 1.6, 'target': 2.0, 'fitted': 2.000438928604126, 'initial_mse': 17.27889633178711, 'final_mse': 1.4998565347923432e-05, 'seconds': 2.9055867912247777}\n" + ] + } + ], + "source": [ + "results.append(fit_one(\"ToyCaBindingKinetic_SU2015_DCN\", \"kf\",\n", + " 1.6 / (u.mM * u.ms), 2.0 / (u.mM * u.ms), \"BC\"))" + ] + }, + { + "cell_type": "markdown", + "id": "c304a2ae", + "metadata": {}, + "source": [ + "## 梯度与依赖检查\n", + "\n", + "零梯度不自动等于错误:SodiumFixed 的 E 与 Ci 独立,因此以 E 为损失时,Ci 的梯度为零。类型/单位错误与训练框架中的断图错误不同,不能用后者证明一个参数不可学习。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2e76a7d0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:26.561847Z", + "iopub.status.busy": "2026-09-07T05:09:26.561683Z", + "iopub.status.idle": "2026-09-07T05:09:27.891701Z", + "shell.execute_reply": "2026-09-07T05:09:27.890820Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| Check | Expected | Observed |\n", + "|---|---|---|\n", + "| SodiumFixed.Ci -> fixed E | zero gradient | 0.0 |\n", + "| E | natural error | ValueError |\n", + "| name | natural error | ValueError |\n", + "| default Ci <- caiBase | updated | 2x |\n", + "| explicit Ci initializer | independent | unchanged |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "checks = []\n", + "probe = build_cell(\"SodiumFixed\", \"Ci\", 10 * u.mM, False)\n", + "probe.ions[\"pool\"].trainable(Ci=braincell.trainable.scale(name=\"theta\"))\n", + "probe.init_state()\n", + "\n", + "def independent_reversal():\n", + " probe.reset_state()\n", + " return probe.get_ion(\"pool\").E.to_decimal(u.mV).sum()\n", + "\n", + "g = brainstate.transform.grad(independent_reversal, grad_states=probe.trainables.parameters().states())()\n", + "assert float(g[\"theta\"]) == 0\n", + "checks.append((\"SodiumFixed.Ci -> fixed E\", \"zero gradient\", str(float(g[\"theta\"]))))\n", + "\n", + "for field, value in ((\"E\", 1 * u.ms), (\"name\", \"pool\")):\n", + " invalid = build_cell(\"SodiumFixed\", \"E\", 50 * u.mV, False)\n", + " try:\n", + " invalid.ions[\"pool\"].trainable(**{\n", + " field: braincell.trainable.parameter(value, group_by=\"all\"),\n", + " })\n", + " except (TypeError, ValueError) as exc:\n", + " checks.append((field, \"natural error\", type(exc).__name__))\n", + " else:\n", + " raise AssertionError(f\"{field} should fail\")\n", + "\n", + "derived = braincell.ion.CdpHVA_SU2015_DCN(size=1)\n", + "explicit = braincell.ion.CdpHVA_SU2015_DCN(size=1, Ci_initializer=0.0002 * u.mM)\n", + "derived.init_state(-65 * u.mV)\n", + "explicit.init_state(-65 * u.mV)\n", + "before = derived.Ci.value\n", + "derived.caiBase = 2 * derived.caiBase\n", + "explicit.caiBase = 2 * explicit.caiBase\n", + "derived.reset_state(-65 * u.mV)\n", + "explicit.reset_state(-65 * u.mV)\n", + "assert u.math.allclose(derived.Ci.value, 2 * before)\n", + "assert u.math.allclose(explicit.Ci.value, 0.0002 * u.mM)\n", + "checks.append((\"default Ci <- caiBase\", \"updated\", \"2x\"))\n", + "checks.append((\"explicit Ci initializer\", \"independent\", \"unchanged\"))\n", + "display(Markdown(\"| Check | Expected | Observed |\\n|---|---|---|\\n\" +\n", + " \"\\n\".join(f\"| {a} | {b} | {c} |\" for a, b, c in checks)))\n" + ] + }, + { + "cell_type": "markdown", + "id": "e8e92045", + "metadata": {}, + "source": [ + "## 实测结果\n", + "\n", + "MSE 单位:电压为 mV²,浓度为 uM²。参数恢复误差单独报告,不能把低轨迹误差当作一般性的参数可辨识性证明。耗时包括编译。\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "35bc75aa", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T05:09:27.894927Z", + "iopub.status.busy": "2026-09-07T05:09:27.894698Z", + "iopub.status.idle": "2026-09-07T05:09:27.902121Z", + "shell.execute_reply": "2026-09-07T05:09:27.901251Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| Parameter | Initial | Target | Fitted | Relative parameter error | Initial MSE | Final MSE | Seconds |\n", + "|---|---:|---:|---:|---:|---:|---:|---:|\n", + "| SodiumFixed.E | 45 | 50 | 49.9764 | 0.000471 | 14.1837 | 8.10408e-05 | 10.10 |\n", + "| SodiumInitNernst.temp | 300 | 309.15 | 309.104 | 0.000149 | 4.34521 | 0.000391733 | 7.06 |\n", + "| CalciumDetailed.Ci_initializer | 0.0008 | 0.001 | 0.000999967 | 3.28e-05 | 0.00501708 | 1.32332e-10 | 2.27 |\n", + "| CalciumDetailed.tau | 7 | 5 | 4.99858 | 0.000284 | 0.00462772 | 2.85074e-09 | 1.96 |\n", + "| ToyCaBindingKinetic_SU2015_DCN.kf | 1.6 | 2 | 2.00044 | 0.000219 | 17.2789 | 1.49986e-05 | 2.91 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total fit time: 24.29 s\n" + ] + } + ], + "source": [ + "display(Markdown(\n", + " \"| Parameter | Initial | Target | Fitted | Relative parameter error | Initial MSE | Final MSE | Seconds |\\n\"\n", + " \"|---|---:|---:|---:|---:|---:|---:|---:|\\n\" +\n", + " \"\\n\".join(\n", + " f\"| {r['parameter']} | {r['initial']:.6g} | {r['target']:.6g} | {r['fitted']:.6g} | \"\n", + " f\"{abs(r['fitted'] / r['target'] - 1):.3g} | {r['initial_mse']:.6g} | \"\n", + " f\"{r['final_mse']:.6g} | {r['seconds']:.2f} |\" for r in results\n", + " )\n", + "))\n", + "print(f\"Total fit time: {sum(r['seconds'] for r in results):.2f} s\")\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "braincell_311", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_compartment/reduction.ipynb b/examples/multi_compartment/reduction.ipynb new file mode 100644 index 00000000..7f662c0e --- /dev/null +++ b/examples/multi_compartment/reduction.ipynb @@ -0,0 +1,912 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "intro", + "metadata": {}, + "source": [ + "# Detailed HH population drives three reduction models\n", + "\n", + "这个例子使用 10 个依次放电的 HH Cell,同时驱动三个各含 3 个 Cell 的约化 population。三个下游 Cell 拥有完全相同的 morphology、10 个 CV 和异构 synapse 分布,分别展示三层输入语义:\n", + "\n", + "1. `EventAccumulatorReduction` 只判断 synapse slot 是否收到 event。\n", + "2. `PayloadAccumulatorReduction` 累加 connection 送达的真实 payload 幅值。\n", + "3. `SynapticKernelAccumulatorReduction` 读取 synapse 类型及时间常数,把完整 conductance kernel 静态折算为面积。\n", + "\n", + "三者都不会推进 detailed Cell 的膜、离子、通道或真实 synapse 动力学。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "declarations", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T14:17:51.699071Z", + "iopub.status.busy": "2026-09-04T14:17:51.698696Z", + "iopub.status.idle": "2026-09-04T14:17:55.066036Z", + "shell.execute_reply": "2026-09-04T14:17:55.065185Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:2026-09-04 22:17:54,501:jax._src.xla_bridge:850: An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" + ] + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "import braincell\n", + "import brainunit as u\n", + "from braincell import mech\n", + "from braincell.filter import AllRegion, at\n", + "\n", + "HH_SIZE = 10\n", + "REDUCTION_SIZE = 3\n", + "DT = 0.025 * u.ms\n", + "DURATION = 25.0 * u.ms\n", + "CONNECTION_DELAY = 0.1 * u.ms\n", + "SOURCE_WEIGHTS = np.arange(1, HH_SIZE + 1) * 0.02 * u.uS\n", + "EVENT_ALPHA = np.asarray([0.9, 0.99, 1.0])\n", + "EVENT_THRESHOLD = 15.0\n", + "PAYLOAD_THRESHOLDS = np.asarray([0.3, 0.7, 1.0]) * u.uS\n", + "KERNEL_ALPHA = 0.99\n", + "KERNEL_THRESHOLD = 8.0 * u.uS * u.ms\n", + "\n", + "\n", + "def build_reduction_morphology():\n", + " soma = braincell.Branch.from_lengths(\n", + " lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type=\"soma\"\n", + " )\n", + " dend_a = braincell.Branch.from_lengths(\n", + " lengths=[160.0] * u.um, radii=[2.0, 1.0] * u.um, type=\"basal_dendrite\"\n", + " )\n", + " dend_b = braincell.Branch.from_lengths(\n", + " lengths=[220.0] * u.um, radii=[2.5, 0.8] * u.um, type=\"apical_dendrite\"\n", + " )\n", + " morphology = braincell.Morphology.from_root(soma, name=\"soma\")\n", + " morphology.soma.dend_a = dend_a\n", + " morphology.soma.dend_b = dend_b\n", + " return morphology\n", + "\n", + "\n", + "def build_hh_population():\n", + " soma = braincell.Branch.from_lengths(\n", + " lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type=\"soma\"\n", + " )\n", + " cell = braincell.Cell(\n", + " braincell.Morphology.from_root(soma, name=\"soma\"),\n", + " cv_policy=braincell.CVPerBranch(),\n", + " pop_size=(HH_SIZE,),\n", + " V_init=-65.0 * u.mV,\n", + " V_th=0.0 * u.mV,\n", + " name=\"hh\",\n", + " )\n", + " cell.paint(\n", + " AllRegion(),\n", + " mech.CableProperty(\n", + " resting_potential=-54.3 * u.mV,\n", + " membrane_capacitance=1.0 * u.uF / u.cm**2,\n", + " axial_resistivity=100.0 * u.ohm * u.cm,\n", + " ),\n", + " mech.Ion(\"SodiumFixed\", E=50.0 * u.mV),\n", + " mech.Ion(\"PotassiumFixed\", E=-77.0 * u.mV),\n", + " mech.Channel(\"IL\", name=\"leak\", g_max=0.3 * u.mS / u.cm**2, E=-54.3 * u.mV),\n", + " mech.Channel(\"Na_HH1952\", name=\"na\", g_max=120.0 * u.mS / u.cm**2),\n", + " mech.Channel(\"K_HH1952\", name=\"k\", g_max=36.0 * u.mS / u.cm**2),\n", + " )\n", + " cell.place(\n", + " at(\"soma\", 0.5),\n", + " mech.CurrentClamp(\n", + " delay=(2.0 + np.arange(HH_SIZE)) * u.ms,\n", + " durations=1.0 * u.ms,\n", + " amplitudes=1.0 * u.nA,\n", + " ),\n", + " )\n", + " return cell\n", + "\n", + "\n", + "SYNAPSE_LOCATIONS = (\n", + " at(\"dend_a\", 0.2) | at(\"dend_a\", 0.5) | at(\"dend_a\", 0.8)\n", + " | at(\"dend_b\", 0.2) | at(\"dend_b\", 0.5) | at(\"dend_b\", 0.8)\n", + ")\n", + "\n", + "\n", + "def build_reduction_population(name, selected_model):\n", + " cell = braincell.Cell(\n", + " build_reduction_morphology(),\n", + " cv_policy=braincell.CVPerBranchList((1, 4, 5)),\n", + " pop_size=(REDUCTION_SIZE,),\n", + " name=name,\n", + " )\n", + " cell[0].place(\n", + " SYNAPSE_LOCATIONS,\n", + " mech.Synapse(\"ExpSyn\", name=\"exp_tau_1\", tau=1.0 * u.ms),\n", + " )\n", + " cell[1].place(\n", + " SYNAPSE_LOCATIONS,\n", + " mech.Synapse(\"ExpSyn\", name=\"exp_tau_4\", tau=4.0 * u.ms),\n", + " )\n", + " cell[2].place(\n", + " SYNAPSE_LOCATIONS,\n", + " mech.Synapse(\"Exp2Syn\", name=\"exp2\", tau1=0.5 * u.ms, tau2=5.0 * u.ms),\n", + " )\n", + " cell.add_reduction(\n", + " \"event\", braincell.EventAccumulatorReduction(alpha=0.9, threshold=EVENT_THRESHOLD)\n", + " )\n", + " cell.add_reduction(\n", + " \"payload\", braincell.PayloadAccumulatorReduction(threshold=0.3 * u.uS)\n", + " )\n", + " cell.add_reduction(\n", + " \"kernel\",\n", + " braincell.SynapticKernelAccumulatorReduction(\n", + " alpha=KERNEL_ALPHA, threshold=KERNEL_THRESHOLD\n", + " ),\n", + " )\n", + " cell.reductions[\"event\"].set(alpha=EVENT_ALPHA, threshold=EVENT_THRESHOLD)\n", + " cell.reductions[\"payload\"].set(alpha=0.0, threshold=PAYLOAD_THRESHOLDS)\n", + " cell.reductions[\"kernel\"].set(alpha=KERNEL_ALPHA, threshold=KERNEL_THRESHOLD)\n", + " cell.use_model(selected_model)\n", + " cell.record(\"value\", braincell.observe.output(\"value\"))\n", + " return cell" + ] + }, + { + "cell_type": "markdown", + "id": "network-heading", + "metadata": {}, + "source": [ + "## Shared network input\n", + "\n", + "每种 synapse name/type 必须通过独立的 `connect()` 建立连接。每个 HH source 仍会连接到某个目标 population 的全部 18 个逻辑 synapse,所以每组 reduction population 合计有 $10 \\times 18 = 180$ 条 Connection。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "network-run", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T14:17:55.071234Z", + "iopub.status.busy": "2026-09-04T14:17:55.070457Z", + "iopub.status.idle": "2026-09-04T14:18:01.222661Z", + "shell.execute_reply": "2026-09-04T14:18:01.221893Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
populationmembersCVs per membersynapsesincoming connections
0hh10100
1event31018180
2payload31018180
3kernel31018180
\n", + "
" + ], + "text/plain": [ + " population members CVs per member synapses incoming connections\n", + "0 hh 10 1 0 0\n", + "1 event 3 10 18 180\n", + "2 payload 3 10 18 180\n", + "3 kernel 3 10 18 180" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
spike records
population
hh10
event5
payload15
kernel4
\n", + "
" + ], + "text/plain": [ + " spike records\n", + "population \n", + "hh 10\n", + "event 5\n", + "payload 15\n", + "kernel 4" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "network = braincell.Network(\"hh_to_reductions\")\n", + "hh = network.add_population(\"hh\", build_hh_population())\n", + "event = network.add_population(\n", + " \"event\", build_reduction_population(\"event_cell\", \"event\")\n", + ")\n", + "payload = network.add_population(\n", + " \"payload\", build_reduction_population(\"payload_cell\", \"payload\")\n", + ")\n", + "kernel = network.add_population(\n", + " \"kernel\", build_reduction_population(\"kernel_cell\", \"kernel\")\n", + ")\n", + "\n", + "\n", + "def connect_all_to_all(prefix, source, target):\n", + " connections = []\n", + " synapse_names = tuple(dict.fromkeys(target.synapses.name.tolist()))\n", + " for synapse_name in synapse_names:\n", + " synapses = target.synapses[synapse_name]\n", + " source_position = np.repeat(np.arange(source.size), len(synapses))\n", + " synapse_position = np.tile(np.arange(len(synapses)), source.size)\n", + " connections.append(\n", + " network.connect(\n", + " f\"{prefix}_{synapse_name}\",\n", + " source=source.event_outputs[\"spike\"][source_position],\n", + " synapse=synapses[synapse_position],\n", + " weight=SOURCE_WEIGHTS[source_position],\n", + " delay=CONNECTION_DELAY,\n", + " )\n", + " )\n", + " return tuple(connections)\n", + "\n", + "\n", + "connection_groups = {\n", + " name: connect_all_to_all(f\"hh_to_{name}\", hh, population)\n", + " for name, population in ((\"event\", event), (\"payload\", payload), (\"kernel\", kernel))\n", + "}\n", + "structure = pd.DataFrame(\n", + " {\n", + " \"population\": [\"hh\", \"event\", \"payload\", \"kernel\"],\n", + " \"members\": [hh.size, event.size, payload.size, kernel.size],\n", + " \"CVs per member\": [hh.cell.n_cv, event.cell.n_cv, payload.cell.n_cv, kernel.cell.n_cv],\n", + " \"synapses\": [0, len(event.synapses), len(payload.synapses), len(kernel.synapses)],\n", + " \"incoming connections\": [\n", + " 0,\n", + " sum(map(len, connection_groups[\"event\"])),\n", + " sum(map(len, connection_groups[\"payload\"])),\n", + " sum(map(len, connection_groups[\"kernel\"])),\n", + " ],\n", + " }\n", + ")\n", + "display(structure)\n", + "\n", + "result = network.run(dt=DT, duration=DURATION)\n", + "time_ms = np.asarray(result.time.to_decimal(u.ms))\n", + "event_frames = []\n", + "for population_name in (\"hh\", \"event\", \"payload\", \"kernel\"):\n", + " events = result.events[population_name][\"spike\"]\n", + " event_frames.append(\n", + " pd.DataFrame(\n", + " {\n", + " \"population\": population_name,\n", + " \"time_ms\": np.asarray(events.time.to_decimal(u.ms)),\n", + " \"source_id\": events.source_id,\n", + " \"count\": events.count,\n", + " }\n", + " )\n", + " )\n", + "spike_table = pd.concat(event_frames, ignore_index=True)\n", + "display(spike_table.groupby(\"population\", sort=False).size().rename(\"spike records\").to_frame())\n", + "\n", + "hh_spike_times_ms = np.asarray(result.events[\"hh\"][\"spike\"].time.to_decimal(u.ms))\n", + "arrival_times_ms = hh_spike_times_ms + float(CONNECTION_DELAY.to_decimal(u.ms))\n", + "arrival_rows = np.asarray([np.argmin(np.abs(time_ms - value)) for value in arrival_times_ms])" + ] + }, + { + "cell_type": "markdown", + "id": "event-heading", + "metadata": {}, + "source": [ + "## Event accumulator: presence only\n", + "\n", + "三个 member 使用相同阈值 `15`,只改变 `alpha`。圆点表示输入到达时的 candidate value,叉号表示越过阈值时发出的 spike。虽然 source weight 不断增加,这个模型在每次到达时仍只看到 6 个活动 slot。" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "event-plots", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T14:18:01.226016Z", + "iopub.status.busy": "2026-09-04T14:18:01.225770Z", + "iopub.status.idle": "2026-09-04T14:18:01.674244Z", + "shell.execute_reply": "2026-09-04T14:18:01.673150Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/MAAAMrCAYAAAAFkcLhAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4U+XbB/BvRveke1Aou5Sy956CyFDZyBRUVBCVpfxQARUHr7i3gCAge4kiyBaQDWVvSmnpoC107+S8f6Q5bWjaJmnarO/nunrRnpyePLl5kp77mRJBEAQQERERERERkcWQmroARERERERERKQfJvNEREREREREFobJPBEREREREZGFYTJPREREREREZGGYzBMRERERERFZGCbzRERERERERBaGyTwRERERERGRhWEyT0RERERERGRhmMxTtXv99dexePFiUxfDpP73v//h/fffN/j3GUMiIiIiItsmN3UBqtOiRYtw7ty5Mh//+eef4eXlVY0l0m727Nnw9PTEvHnzTF2UKrF7926EhYWZuhgoKCjAqlWrcOTIEQiCgE6dOmHChAmwt7ev8ufev38/XF1dDf59c4mhviob85iYGKxYsQLXr1+Hi4sLevXqhREjRkAikVRxyYmIiIiIzItNJfOHDx/GoUOHsGrVKq2POzk5VXOJtNuzZw8CAgJMXQyrlp2djT59+uD+/fuYM2cOpFIpPvnkEyxduhT79u2rVKJN2lU25vv378fTTz+Nli1bYuzYsUhOTsYrr7yC3377Ddu2bYOdnV01vRIiIiIiItOzqWQeAGQyGYYNG2bqYpCJffzxxzh58iQuX76MRo0aAQCefPJJhIWF4cMPP8Qnn3xi4hJan8rEvKCgAOPGjUOdOnVw4MAByGQyAEDPnj3RqVMnfPPNN5gxY0a1vA4iIiIiInNgc8m8Lt599108evQI3377banHli5dir1792LFihVwdHQEAKSmpmLVqlU4e/YsCgsL0apVK0yaNAkeHh7i773++usIDg7Gm2++iWXLluG///5DjRo18Pzzz6NFixbieRMmTMDdu3eRkJAgNjr4+fnh+++/L7O86mtPnz4dP//8M86cOYPQ0FBMnz4d3t7eyM3NxdKlS3Hy5En4+flh+vTpqFWrVqnr6PM6KvtcAJCbm4uffvoJZ86cgY+PDyZOnIhmzZpVqlyvv/46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fv31V8jlcsycORO7d+/G+++/j+DgYPzxxx9o3bo1rly5gqCgIKO9Lkvn7++PDz74AGPGjMHKlSvh5OQET09PcVrT888/jw8//BC+vr547733MHToUJw4cQJA8VD88ePH46OPPkLDhg1Rr149HDt2DE899RTmzZuH6dOn486dO5g+fToePnyIKVOmAACGDx+OGjVqYPHixcjLy8OGDRvg7OyM0aNHA1Al6k5OTpgzZ474e3K5HP/73/8AAHv37sWgQYPw7rvvolevXti2bRu6d++OkydPonlz7SNK/v77b4wZMwYfffQROnTogNWrV2PZsmVo1apVmfHZtWuXxt8ubebNmyeW+3G6bhuXl5eH7t27o7CwEE2bNsXbb7+N0NBQnX7XUEzmiYiITCghIw8hH+w1yXPfe6c3Qmrottf9c889hzVr1og3RMnJyfjnn3+wc+dOAICHh4fG8O6OHTvi0KFD2Lx5MyZPnmxwGefNm4fx48fjnXfeAQC0b98eMpkMkyZNMutkXqkUMGbNWayLjINUAigFIDYtB4fupGDHlQSsfq5VlSX0jxsxYgTmzZsHQBW/devWYefOnWIyD6h6nj7//HMAQKdOnXD9+nV88MEH2LNnj07PERoaCkdHRwQEBCAiIqLM82rVqiV+HxERgdq1a6Np06b48ccf4erqKj42cOBAvP9+cVKqHtb67rvvYtKkSeLxzMxMjes/99xzmDt3Lk6cOIH27dsDANasWYMBAwaIjUqNGzcWz2/RogUyMjLw008/mX0yDxQn7toS+scT+eBp75U7FN8Qp06dwvTp09G7d28AQNeuXTV6WgsLC/H777+jcePGkMlk8PX1RYcOHbBgwQKEhYWVed24uDj07dsXERERWLVqFezs7HD37l18+eWXuHbtGho0aABAVX9PnDiBH374AR988IFRX5suZsyYofX4nDlzEBAQgISEBCxevFjrOer3V2RkpNbRRf7+/uJn2q5du/Dkk0/qXC57e3uxYa1x48bie0mdzP/yyy/i9Ib/+7//Q+vWrZGUlARf3+KGnu+++05jccK33noL06dPFz/z27dvj7y8PCxcuFBM5k+dOoWdO3eiS5cuAIDevXtr1Ad/f3/8/vvvsLOzAwCcPXsWf/31l5jMv/feexg/fjzmzp0LmUyGzp074/Lly1i4cCG2bNmi9bW+//77eOml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RERkYZjMExEREREREVkYJvNEREREREREFsagZD4jIwNr164Vf96+fTt69OiByZMnIz093WiFIyIiIiIiIqLSDErm586di7S0NABASkoKxo4di8aNG+Py5cuYNWuWUQtIRERERERERJrkhvzS1q1bMX/+fADA7t270a5dO/zwww+4e/cuOnXqZNQCEhEREREREZEmg4fZy+WqdoD9+/ejb9++AAAfHx9kZGQYr3REREREREREVIpBPfNt2rTB3Llz0bdvX6xfvx7Hjh0DAJw9exatW7c2agGJiIiIiIiISJNBPfNff/01Tpw4gRdeeAEzZsxAREQEAOCTTz7BW2+9ZdQCEhEREREREZEmg3rmIyMjce7cuVLHt27dio0bN1a6UERERERERERUNoN65seNG6f1uIODQ5mPEREREREREZFxGJTMlyUhIQGenp7GvCQRERERERERPUavYfZdunTR+j0AKJVK3Lp1C7179zZOyYiIiIiIiIhIK72S+T59+gAAjh49Kn6vZmdnh9DQUAwdOtR4pSMiIiIiIiKiUvRK5hcsWABAtZ/8tGnTqqI8RERERERERFQBg+bMM5EnIiIiIiIiMh2DF8DbsWMHunTpAm9vb3h5eaFLly7YsWOHMctGRERERERERFoYlMwvXboUw4YNQ1hYGD799FP83//9H8LCwjBs2DAsXbrU2GUkIiIiIiIiohL0mjOv9umnn2LFihUYPXq0eGzy5Mno1asX5s+fjxdeeMFoBSQiIiIiIiIiTQb1zEdHR2PAgAGljg8cOBDR0dGVLhQRERERERERlc2gZD4kJAT79u0rdXzPnj0ICQmpdKGIiIiIiIiIqGwGDbOfOXMmxo4dixdffBHt2rUDAJw4cQK//PILPvvsM6MWkIiIiIiIiIg0GZTMv/rqq/Dx8cHixYvxyy+/AAAaN26MFStWYMSIEUYtIBERERERERFpMiiZB4ARI0ZgxIgREAQBEonEmGUiIiIiIiIionIYvM+8GhN5IiIiIiIiouqlc898hw4ddL7o8ePHDSoMEREREREREVVM52R+4MCBVVKA/Px8bNmyBdeuXcPEiRMRGhpa6py0tDRs2bIFiYmJaNq0KZ566imOCCAiIiIiIiKbpXMy/8477xj9ydesWYO33noLTZs2xa5du9CjR49Syfy9e/fQpUsXBAcHo02bNvjmm2/Qrl07bNmyhQk9ERERERER2SSD5sxnZGRg7dq14s/bt29Hjx49MHnyZKSnp+t8nXr16uHs2bPiivjavPXWWwgMDMThw4fxzTff4MCBA9ixYwc2bdpkSNGJiIiIiIiILJ5ByfzcuXORlpYGAEhJScHYsWPRuHFjXL58GbNmzdL5Oh06dICfn1+ZjysUCmzfvh3jxo2DXK4aRNCwYUN069aNyTwRERERERHZLIO2ptu6dSvmz58PANi9ezfatWuHH374AXfv3kWnTp2MVrh79+4hJycHDRo00DjeoEEDnDhxoszfy8vLQ15envizerSAIAgQBMFo5TOEugymLgeZP9YV0gfrC+mD9YX0wfpC+mB9IX2wvminazwMSuYzMjLEnvL9+/ejb9++AAAfHx9kZGQYckmtMjMzAQAeHh4axz09PcXHtPn444+xcOHCUsfT0tJMXlEEQRDLzjn/VB7WFdIH6wvpg/WF9MH6QvpgfSF9sL5op+vUdYOS+TZt2mDu3Lno27cv1q9fj2PHjgEAzp49i9atWxtySa1cXV0BQBzSr5aamio+ps3cuXMxY8YM8ef09HSEhITAw8MD7u7uRiufIdSNCR4eHqywVC7WFdIH6wvpg/WF9MH6QvpgfSF9sL5op2ssDErmv/76a4wbNw4bNmzAjBkzEBERAQD45JNP8NZbbxlySa1CQkLg5OSEmzdvol+/fuLxmzdvolGjRmX+noODAxwcHEodl0gkZlFJ1OUwh7KQeWNdIX2wvpA+WF9IH6wvpA/WF9IH60tpVZrMR0RE4Ny5c6WOb926VWsSbSi5XI6nn34aq1atwssvvwy5XI4bN27g33//xe+//2605yEiIiIiIiKyJAYl82XRN5GPjIzEtm3bxDkBK1aswMGDB9GjRw/06NEDAPDpp5+ic+fO6NatG9q0aYMtW7Zg0KBBGD58uDGLTkRERERERGQxjJrMG8rd3V1cHf9xtWrVwqVLl7B582YkJibixx9/xIABAzgMg4iIiIiIiGyWSZP5Fi1aoEWLFhWe5+HhgUmTJlV9gYiIiIiIiIgsgNTUBSAiIiIiIiIi/VQ6mX/06JExykFEREREREREOjIomc/Ly8OsWbNQo0YNeHl5iccnT56Mq1evGq1wRERERERERFSaQcn8woULsX//fqxZs0bj+FNPPYX333/fKAUjIiIiIiIiIu0MWgDv999/x86dOxEeHq5xvGvXrpg8ebJRCkZERERERERE2hnUMx8fH4/Q0FAA0NgiThAE5OfnG6VgRERERERERKSdQcl848aNcejQIQCayfyvv/6q01ZzRERERERERGQ4g4bZv/feexg7dixmz54NAFi5ciV27dqFDRs2YMeOHUYtIBERERERERFpMiiZHzJkCORyORYtWgSpVIpJkyahRYsW2Lp1K5566iljl5GIiIiIiIiISjAomVcqlRg8eDAGDx4MQRAgCAKk0kpvWU9EREREREREOjAoA69duzbeeustXLx4ERKJhIk8ERERERERUTUyKAt/8803sXfvXjRr1gzNmzfH//3f/+H+/fvGLhsRERERERERaWFQMj9jxgycOXMGV69exeDBg/Hjjz+iVq1a6N27N3799Vdjl5GIiIiIiIiISqjU+PiwsDB88MEHuH37Ng4dOoSUlBRMmjTJWGUjIiIiIiIiIi0MWgCvpLNnz2LNmjVYu3YtkpOTMWjQIGOUi4iIiIiIiIjKYFDP/J07d/Dhhx+icePGaNOmDY4fP453330X8fHx+OOPP4xdRiIiIiIiIiIqwaCe+Xr16iEsLAxjxozB2LFjERoaauRiEREREREREVFZDErmz5w5g1atWhm7LERERERERESkA4OG2TORJyIiIiIiIjIdnXvmO3ToAAA4fvy4+H1Zjh8/XrlSEREREREREVGZdE7mBw4cqPV7IiIiIiIiIqpeOifz77zzjvh9aGgoxo4dq/W81atXV75URERERERERFQmg+bMjxs3zqDHiIiIiIiIiKjyDErmy5KQkABPT09jXpKIiIiIiIiIHqPX1nRdunTR+j0AKJVK3Lp1C7179zZOyYiIiIiIiIhIK72S+T59+gAAjh49Kn6vZmdnh9DQUAwdOtR4pSMiIiIiIiKiUvRK5hcsWAAA8PHxwbRp06qiPERERERERERUAYPmzE+bNg15eXmljms7RkRERERERETGZVAyv3LlSrz00kuljr/44otYtWpVpQtFRERERERERGUzKJlftGiRxr7zau+++y4+/vjjSheKiIiIiIiIiMpmUDJ/7949+Pj4lDru7e2NqKioSheKiIiIiIiIiMpmUDLfpEkTrFu3rtTx33//HWFhYZUuFBERERERERGVTa/V7NXmzZuHUaNG4dy5c+jWrRsEQcC///6L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1TJ1ly9fRufOnWFra4tKlSqhXr166Nq1KwDg/v37hb6+KO+LUlHrU2nK9NNPPxV5mL2NjU2h50dERERERJphpu8CGKr//e9/mD59OpYvX45vv/0WISEhuHHjBpYvXy5uExcXByA7sZe6tbLd3d2RnJwMNzc38ffX2draIjk5udTlVbdvGxubfB/PfUxNnEdRj6VUvnz5fB9TJoErbrlKshpBQkICOnfujLCwMGzduhWtWrUq9j6Unj9/jvbt26NevXq4desWKleuDCA7qaG7u7vapImvK8r7olSU+lTaMiUlJSElJaXQcivLw8R2RERERES6wWA+Hz4+PujUqROWL1+OqVOn4s8//4S9vT0GDBggbqPsQV27di06dOigr6KWmj7O4/Ukc7kfUwapxS2XmVnxqnNycjK6du2Ka9euYfPmzWJvdUkdOXIE8fHxmDZtmhg0A0BYWFiR91GU90WXZZo0aVKe7P75SU5Ohp2dXbHLSERERERExVfmhtkXx6hRo/Do0SNs374dGzZswFtvvQVbW1vx+U6dOsHe3h5///232tcrFIoSHdfBwaFIy6BpirbOoyDXrl3DrVu3VB5bv349zMzMEBwcrPVypaSkoHv37rh48SI2bdqEHj165LttUZems7KyAoA82eSXLVtW5HIV5X0pjtKWydHREe7u7kX6kUrZnBARERER6UqZ65kXBAFRUVFqn3N2doaFhYX4e+/eveHq6op3330XycnJeOedd1S2L1euHP744w8MGzYMMpkM7777Lry9vfHo0SMcOXIEZ86cwZ49e4pdxgYNGmD58uU4ffo0fH19YWZmlmdutCZp6zwK0rZtW3z44Yf47LPPUKlSJWzZsgVLlizBlClTxOHy2ipXZmYm+vbti5MnT2LJkiVo3LixSp2wtLREuXLlxN9Xr16Nd999F//88w+GDh2a735btmwJb29vfPbZZ1i4cCFsbW2xcuXKYo0YKMr7UhylLdNPP/2En376qdjHVScjIwPx8fEAIA7dj42NhZWVFczMzNROMSAiIiIiIvXKVDDv7OwMe3t7BAYGqn1+48aNKnOmzc3NMXr0aCxduhQtWrRAkyZN8rzmrbfegr+/P+bNm4d3330XKSkp8PX1Rfv27fHPP/+I27m6uqoEiErKnmqVZ6wAAFvNSURBVM/cvv76a8THx2P48OF48eIFKlSo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BYlgviARzBckgvmCRDBfkAjmS9O0igJ4AAAAAAB4CSvzAAAAAAC4DMk8AAAAAAAuQzIPAAAAAIDLkMy3oI0bN2rt2rUqLy93eihwibKyMhUWFurHH390eihIcVVVVVq1apU2b97s9FCQ4iorK/X9999r1apVOnDggNPDQYopLi5WYWGhdu3aFfE5O3fu1F//+lft2bOnBUeGVLRt2zYVFhaqtLQ07Ndra2v13Xffaf369aqpqWnh0SHVbNiwQYWFhaquro76vB07dqiwsFA7duxooZG5F8l8C5gzZ45OPPFEDRo0SNdcc426dOmi//iP/3B6WHCBm266SRdddJEef/xxp4eCFPbSSy+pS5cuGj16tIYMGaJRo0bp4MGDTg8LKejNN99UQUGBLr/8co0bN05dunTR7373O6eHhRSwfv16TZgwQaeddpoGDBigjz/+OOQ5pmnq9ttvV7du3TR+/Hjl5+frvvvuc2C0cNrXX3+tkSNHqk+fPhowYIBWr14d9HXTNDVjxgx17dpVV111lYYMGaLjjjtOn3zyiUMjhpPmzZunwYMHq2/fvhowYID27t0b8bnl5eUaMmSILrzwQv3lL39puUG6FMl8C9iyZYvmz59vrcy/8sormjJlihYvXuz00JDCXnjhBW3fvl39+vVzeihIYW+//bYmTpyoV155RT/88IN++OEH3XDDDdq3b5/TQ0OKqaio0Lhx4zRp0iT9+OOPWrVqld5880098sgjWrp0qdPDg8O+//57DRgwQBs2bFBaWlrY57zwwgv685//rG+++UZr167V559/rn/7t3/Tm2++2cKjhdPWrFmj6667Tl999VXYr1dXV6u6ulo//PCDvvvuO23cuFE33HCDrr766qi7PuBNa9as0fTp0/X666/HfO6UKVM0ePBgtWnTpgVG5n4k8y3g3nvv1XHHHWd9/stf/lJdunTRl19+6eCokMrWrl2rhx56SH/+85/l8xGmiOzBBx/U2LFjdcUVV1iPXXHFFcrPz3dwVEhFJSUlqqys1HnnnWc9dv7550uSdu/e7dSwkCKuuOIK3XjjjcrOzo74nJdfflmjRo1Sr169JEl9+/bV0KFD9fLLL7fUMJEibrzxRo0aNUrp6elhv96mTRs98sgjOvLIIyVJhmFo4sSJOnjwoFauXNmCI0UqmDZtmi6++GIZhhH1eXPnztWSJUvYkZoAsgQHbNq0Sbt379YJJ5zg9FCQgsrLyzV69Gg9+eST6tatm9PDQQrbvn27vv/+e11++eXat2+fvvnmG5IyRNS5c2fddddduu+++/TOO+/ok08+0bhx4zR48GANGzbM6eEhxZmmqVWrVunss88Oerxv374qKipyaFRwk7/+9a+SpOOPP97hkSAV/f3vf9fkyZM1Z84cZWVlOT0c1yCZb2HV1dUaP368Tj31VF155ZVODwcp6M4771SfPn00duxYp4eCFLdt2zZJ0qJFi3TKKafolltuUffu3XX11VdTaBNhXX/99TIMQ9OmTdM999yjb775RpMnT1ZGRobTQ0OKO3jwoCorK9WhQ4egxzt06KDi4mKHRgW32LVrl6ZMmaKxY8eSzCNETU2NxowZo/vuu0+9e/d2ejiuQjLfgmpra3X99dfrxx9/1HvvvcdZEIT47LPPNGfOHI0ZM0aFhYVWhdidO3eqsLBQtbW1Tg8RKSSQgH311Vf629/+phUrVmjdunX64osv9Mgjjzg8OqSaHTt26MILL9Q111yjn376Sd9++61effVVjRo1SgsXLnR6eEhxgX9vKioqgh4vLy/nfgZR7du3T8OGDVN+fr5eeOEFp4eDFDRz5kzt2bNHZ599tnX/a5qmNmzYoG+++cbp4aW08AddkHS1tbUaN26cCgsLtWjRIvXo0cPpISEFVVZWqk+fPnrsscesxzZt2qRdu3bpvvvu07x589SuXTsHR4hUEjiGMWbMGOXk5EiS8vPzNXz4cH3xxRdODg0paPHixSorK9Ptt99uPTZ48GCddNJJ+uCDDzRw4EAHR4dUl5mZqc6dO2vr1q1Bj2/dulXHHnusQ6NCqtu/f7+GDh2qrKwszZs3T0cccYTTQ0IKysjIUOfOnTV9+nTrserqar377rvavHlzXIXzWiuS+Rbg9/s1fvx4LV68WAsXLuSsPCK65JJLdMkllwQ9dsEFF+jkk0/Wiy++6NCokKpyc3PVv3//kJvrLVu2qFOnTg6NCqkqMCe2bNmiU045RZJUVVWlXbt2MV8QlyFDhuiDDz7QAw88IKluoeLDDz+k5gLCKikp0dChQ5Wenq558+apffv2Tg8JKequu+7SXXfdFfRYVlaWpk2bpokTJzo0KncgmW8BEydO1Jtvvqnnn39eO3fu1M6dOyVJXbt2DapyDwCJevzxx3XZZZepa9eu6tOnjz799FN99tlnbJtGiAsuuEBnnnmmRo8erQceeEBHHHGEnn/+edXU1OhXv/qV08ODw0pKSvTtt99an69fv16FhYXq3LmzTjzxREnSAw88oHPPPVc333yzRowYoddee03FxcX653/+Z6eGDYfs3LlT69ev144dOyTJmjvdunVTQUGBqqqqNGzYMG3evFkvvfRSUB/6nj176qijjnJk3HDGpk2btHnzZq1Zs0aS9PXXX+sXv/iFTj75ZHXs2NHh0bmbYZqm6fQgvG7kyJFhe2qOGjUq5F0o4HCTJk1St27ddO+99zo9FKSopUuX6rnnntOOHTvUo0cP3X777TrjjDOcHhZSUGlpqZ599ll9/fXXqqqq0imnnKIpU6awTRoqKirS5MmTQx6/9NJLdf/991ufr127Vk899ZQ2bdqk448/Xvfccw87DluhDz/8MGz7sFtvvVXjxo1TcXFxUMvUxn7729+G7EKEt82aNUuzZ88Oefzhhx/W4MGDw/6ZwYMH64477tCIESOae3iuRjIPAAAAAIDLUM0eAAAAAACXIZkHAAAAAMBlSOYBAAAAAHAZknkAAAAAAFyGZB4AAAAAAJchmQcAAAAAwGVI5gEAAAAAcBmSeQAAPGjBggVas2aNo2N49913VVJS0myv/9FHH2n37t3N9voAAKSydKcHAAAA4vfFF19o69atEb/epk0bjRw5Ur/73e908cUX67TTTmvB0TV4++239eijj+rKK69stu+xdOlSvfHGG3r11Veb7XsAAJCqSOYBAHCRJUuWaMWKFZKkqqoqvfvuu7rgggt0zDHHSJKOOOIIjRw50tFE3jRNTZ8+Xb///e9lGEazfZ+pU6eqa9euuvfee9WrV69m+z4AAKQiwzRN0+lBAACAxO3Zs0edOnXSu+++G7ICvmDBAnXu3NlK6OfPn6/u3burQ4cOWrlypQzD0IUXXqiMjAzt2LFDX3/9tTp27KjzzjsvJAGvra3V119/rV27dunEE0/UqaeeGnVc8+bN09ixY7Vjxw61adOmyd9/7dq12rBhg7p166bevXsHfX3UqFE6+uij9eyzz9r9NQIA4EqszAMA4EGHb7O/9957lZ2drW3btql3795avny5OnfurFtvvVVPPPGETjvtNC1btkwXXnih3nrrLet1/v73v+vyyy9XbW2tTjjhBH3zzTfq16+f3njjDWVkZIT93h9++KHOP/98K5G3+/1N09SYMWP02Wef6bzzztOWLVuUl5en9957T+3atZMkDRo0SE888QTJPACg1SGZBwCgldi9e7dWr16t3NxcbdmyRT169NCzzz6r1atXq3379lq/fr1OOukkrVixQmeddZYk6dprr9WwYcP0xBNPSJIOHjyofv366emnn9a0adPCfp8VK1ZowIABTf7+q1ev1ty5c7V161Z16dJFkvT555+rsrLSSuZPP/10bdq0Sbt371anTp2a49cGAEBKopo9AACtxOjRo5WbmytJys/PV35+vsaMGaP27dtLkk488UR16NBBf/vb3yRJ3333nZYtW6bu3bvrrbfe0ty5c/XRRx/phBNO0MKFCyN+nz179ujII49s8vfPzs6WaZr69ttvrde48MIL1aFDB+vzwPfZs2eP7d8LAABuxMo8AACtxOEJdmZmZtjHKioqJNVtsZekxYsXy+dreP8/KysrasG5du3a6eDBg03+/j179tSTTz6p6667Tm3bttWgQYM0YcKEoFX/wPcJvCEAAEBrQTIPAADCysnJkSQ9+uij6tmzZ9x/rmfPntq4cWNSxjB16lTdeeedWrVqld555x0NHDhQ8+fP1+DBgyVJGzduVPv27XX00Ucn5fsBAOAWbLMHAABhnXPOOerQoYNmzZoV9Lhpmtq2bVvEPzd48GB9+eWXTf7+e/bsUVVVlXw+n84880zNmDFDvXr10rJly6znfPnllxo4cKDS0tKa/P0AAHATVuYBAEBYWVlZevHFF3Xttddq+/btGjRokHbv3q333ntPN998s2655Zawf+7aa6/V3XffraVLl6p///62v/+6det000036ZprrtHxxx+vFStW6Mcff9Tw4cMl1bXMe/vtt/XCCy/Y/h4AALgVK/MAALhUZmamRo8erfz8/JCvNW5LJ0nDhg3TySefHPSc4cOHh2yfHzFihHr06GF9fuWVV2r16tU68cQTVVhYqPLycv3Xf/1XxEReqju/fuedd+rpp59u0vc///zzNX/+fLVp00aLFi1STk6OioqKdMYZZ0iS5s6dq6OPPtpK7gEAaE0M0zRNpwcBAAC85dChQ7r99ts1c+bMsJXtk+HBBx/UZZddpn79+jXL6wMAkMpI5gEAAAAAcBm22QMAAAAA4DIk8wAAAAAAuAzJPAAAAAAALkMyDwAAAACAy5DMAwAAAADgMiTzAAAAAAC4DMk8AAAAAAAuQzIPAAAAAIDLkMwDAAAAAOAyJPMAAAAAALgMyTwAAAAAAC7z/wH3jAFvI517FwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def plot_accumulator(population_name, values, parameters, threshold, y_label, titles):\n", + " events = result.events[population_name][\"spike\"]\n", + " event_times = np.asarray(events.time.to_decimal(u.ms))\n", + " event_ids = np.asarray(events.source_id)\n", + " colors = (\"#0072B2\", \"#009E73\", \"#D55E00\")\n", + " fig, axes = plt.subplots(\n", + " REDUCTION_SIZE, 1, figsize=(10, 8), sharex=True, sharey=True, constrained_layout=True\n", + " )\n", + " for member, (axis, color) in enumerate(zip(axes, colors)):\n", + " axis.plot(time_ms, values[:, member], color=color, linewidth=1.6, label=\"value\")\n", + " axis.scatter(\n", + " arrival_times_ms, values[arrival_rows, member], color=color, s=28, zorder=3, label=\"input arrival\"\n", + " )\n", + " member_spikes = event_times[event_ids == member]\n", + " spike_rows = [np.argmin(np.abs(time_ms - value)) for value in member_spikes]\n", + " axis.scatter(\n", + " member_spikes, values[spike_rows, member], color=\"#CC3311\", marker=\"x\",\n", + " s=70, linewidths=2, zorder=4, label=\"spike\"\n", + " )\n", + " member_threshold = threshold[member] if getattr(threshold, \"shape\", ()) else threshold\n", + " axis.axhline(\n", + " member_threshold, color=\"#555555\", linestyle=\"--\", linewidth=1.2,\n", + " label=f\"threshold = {member_threshold:g}\"\n", + " )\n", + " axis.set_title(titles[member], loc=\"left\")\n", + " axis.set_ylabel(y_label)\n", + " axis.grid(alpha=0.2)\n", + " axis.legend(loc=\"upper right\", ncols=4)\n", + " axes[-1].set_xlabel(\"Time (ms)\")\n", + " axes[-1].set_xlim(1.5, 15.0)\n", + " plt.show()\n", + "\n", + "\n", + "event_value = np.asarray(result.samples[\"event\"][\"value\"].values)\n", + "plot_accumulator(\n", + " \"event\",\n", + " event_value,\n", + " EVENT_ALPHA,\n", + " np.full(REDUCTION_SIZE, EVENT_THRESHOLD),\n", + " \"active slots\",\n", + " [f\"Event member {member}: alpha = {alpha:g}\" for member, alpha in enumerate(EVENT_ALPHA)],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "payload-heading", + "metadata": {}, + "source": [ + "## Payload accumulator: physical amplitude\n", + "\n", + "Payload 模型设置 `alpha=0`,因此只显示当前时刻的输入幅值。三个 member 收到相同的 `6 × source weight`,但使用不同的 `uS` 阈值;synapse 类型和 tau 不参与计算。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "payload-table", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T14:18:01.680884Z", + "iopub.status.busy": "2026-09-04T14:18:01.680176Z", + "iopub.status.idle": "2026-09-04T14:18:01.921140Z", + "shell.execute_reply": "2026-09-04T14:18:01.920431Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
HH sourcearrival time (ms)source weight (uS)member 0 value (threshold 0.3 uS)member 1 value (threshold 0.7 uS)member 2 value (threshold 1.0 uS)
002.6750.020.120.120.12
113.6750.040.240.240.24
224.6750.060.360.360.36
335.6750.080.480.480.48
446.6750.100.600.600.60
557.6750.120.720.720.72
668.6750.140.840.840.84
779.6750.160.960.960.96
8810.6750.181.081.081.08
9911.6750.201.201.201.20
\n", + "
" + ], + "text/plain": [ + " HH source arrival time (ms) source weight (uS) \\\n", + "0 0 2.675 0.02 \n", + "1 1 3.675 0.04 \n", + "2 2 4.675 0.06 \n", + "3 3 5.675 0.08 \n", + "4 4 6.675 0.10 \n", + "5 5 7.675 0.12 \n", + "6 6 8.675 0.14 \n", + "7 7 9.675 0.16 \n", + "8 8 10.675 0.18 \n", + "9 9 11.675 0.20 \n", + "\n", + " member 0 value (threshold 0.3 uS) member 1 value (threshold 0.7 uS) \\\n", + "0 0.12 0.12 \n", + "1 0.24 0.24 \n", + "2 0.36 0.36 \n", + "3 0.48 0.48 \n", + "4 0.60 0.60 \n", + "5 0.72 0.72 \n", + "6 0.84 0.84 \n", + "7 0.96 0.96 \n", + "8 1.08 1.08 \n", + "9 1.20 1.20 \n", + "\n", + " member 2 value (threshold 1.0 uS) \n", + "0 0.12 \n", + "1 0.24 \n", + "2 0.36 \n", + "3 0.48 \n", + "4 0.60 \n", + "5 0.72 \n", + "6 0.84 \n", + "7 0.96 \n", + "8 1.08 \n", + "9 1.20 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "payload_value_us = result.samples[\"payload\"][\"value\"].values.to_decimal(u.uS)\n", + "pd.DataFrame(\n", + " {\n", + " \"HH source\": np.asarray(result.events[\"hh\"][\"spike\"].source_id),\n", + " \"arrival time (ms)\": arrival_times_ms,\n", + " \"source weight (uS)\": SOURCE_WEIGHTS.to_decimal(u.uS),\n", + " \"member 0 value (threshold 0.3 uS)\": payload_value_us[arrival_rows, 0],\n", + " \"member 1 value (threshold 0.7 uS)\": payload_value_us[arrival_rows, 1],\n", + " \"member 2 value (threshold 1.0 uS)\": payload_value_us[arrival_rows, 2],\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "kernel-heading", + "metadata": {}, + "source": [ + "## Synaptic kernel accumulator: static metadata-aware area\n", + "\n", + "Kernel 模型不会创建 `ExpSyn.g` 或 `Exp2Syn.A/B`。它在初始化时读取类型和时间常数:单指数的单位权重面积是 `tau`;双指数的单位权重面积是 `factor × (tau2 - tau1)`,其中 factor 保证 kernel 的峰值为 1。三者使用相同 `alpha=0.99` 和阈值 `8 uS·ms`。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "kernel-plots", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T14:18:01.924503Z", + "iopub.status.busy": "2026-09-04T14:18:01.924259Z", + "iopub.status.idle": "2026-09-04T14:18:03.048583Z", + "shell.execute_reply": "2026-09-04T14:18:03.047834Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
membersynapse declarationkernel area / unit weight (ms)
006 x ExpSyn(tau=1 ms)1.000000
116 x ExpSyn(tau=4 ms)4.000000
226 x Exp2Syn(tau1=0.5 ms, tau2=5 ms)6.457748
\n", + "
" + ], + "text/plain": [ + " member synapse declaration kernel area / unit weight (ms)\n", + "0 0 6 x ExpSyn(tau=1 ms) 1.000000\n", + "1 1 6 x ExpSyn(tau=4 ms) 4.000000\n", + "2 2 6 x Exp2Syn(tau1=0.5 ms, tau2=5 ms) 6.457748" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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eSFbsLyUT+1rz0BERERERERUzTGYJyIiIiIiIqpmGMwTERERERERVTMM5omIiIiIiIiqGQbzRERERERERNUMg3kiIiIiIiKiaobBPBEREREREVE1w2CeiIiIiIiIqJqpEsH8+fPnsXXrVty+fbvIeY4fP47ff/8dUVFR9zBnRERERERERFVPpQbz//zzD3r37o2BAwdixIgROHPmjMU8GRkZ6NWrF/r3748lS5agadOmWLhw4b3PLBEREREREVEV4VCZG09MTMTrr7+Oxo0bIyQkxOo8r776KqKjo3Hx4kV4enpi9+7d6NOnD3r37o3evXvf2wwTERERERERVQGVWjM/atQo3H///UVOF0Lg22+/xeOPPw5PT08AQEREBNq3b4+1a9feq2wSERERERERVSmVWjNfklu3biE5ORmtW7c2S2/Tpg1OnjxZ5HJarRZarVZ6n56eDsB4cUAIUTGZLSVTHio7H1T1sayQLVheyBYsL2QLlheyBcsL2YLlxbrSHo8qHcynpaUBgFQrb+Lt7Y3U1NQil1u6dCkWLVpkdX2VXVCEEMjMzAQAyGSySs0LVW0sK2QLlheyBcsL2YLlhWzB8kK2YHmxzlQZXZIqHcw7OTkBALKzs83SMzMzoVQqi1xu3rx5mDVrlvQ+PT0dISEh0Gg0cHd3r5jMlpLpYoJGo2GBpWKxrJAtWF7IFiwvZAuWF7IFywvZguXFutIeiyodzNetWxcKhcLidnRRUVGoX79+kcsplUqrwb5MJqsShcSUj6qQF6raWFbIFiwvZAuWF7IFywvZguWFbMHyYqm0x6JK3Ge+KM7Ozrj//vvx448/SmkpKSnYtWsXBg4cWIk5IyIiIiIiIqo8lVozf/PmTRw7dgxJSUkAgH379iE1NRXNmjVDs2bNABj7v/fs2RNPPvkkunbtis8//xwNGzbEY489VplZJyIiIiIiIqo0lVozf+XKFaxatQrbtm3Dgw8+iP/++w+rVq3CiRMnpHk6dOiA//77Dy4uLtixYweGDh2Kffv2wdnZufIyTkRERERERFSJKrVmPiIiAhERESXO16JFC3z44Yf3IEdEREREREREVV+V7jNPRERERERERJYYzBMRERERERFVMwzmiYiIiIiIiKoZBvNERERERERE1QyDeSIiIiIiIqJqhsE8ERERERERUTXDYJ6IiIiIiIiomrE5mN+/fz9mzpyJNm3awMfHBz4+Pmjbti2ee+45HDx4sCLySERERERERESFOJR2xoMHD+L555/H6dOn0atXLzz44IPw9/cHAMTHx+Pw4cPo06cPWrVqheXLl6Nz584VlmkiIiIiIiKi2qzUwfy4ceMwb948TJgwAWq12uo8GRkZWLduHcaOHYsbN27YK49EREREREREVEipg/mLFy9CqVQWO49arcZTTz2FRx55pLz5IiIiIiIiIqIilLrPfHGBvE6nw99//43r16+XOC8RERERERERlU+ZRrP/448/MGXKFOn9wIEDcf/996NJkybYtm2b3TJHRERERERERJbKFMy/+uqreO655wAAhw4dwtmzZxEXF4cvvvgCixcvtmsGiYiIiIiIiMhcmYL5M2fOICwsDACwa9cujBgxAv7+/hg3bhzOnz9v1wwSERERERERkbkyBfPe3t44deoUAGDLli2IiIgAAMTFxcHX19d+uSMiIiIiIiIiC6Uezb6wJ554Av3790dISAhSUlIwaNAgAMDWrVvx0EMP2TWDRERERERERGSuTMH8a6+9htatWyMyMhIPPfQQXF1dAQB6vR6vvPKKXTNIRERERERERObKFMwDwIMPPmiRNnv27HJlhoiIiIiIiIhKVuZgHgDi4+ORkpJikd6sWbPyrJaIiIiIiIiIilGmYP7QoUOYNGkSrly5YnW6EKJcmbImJiYGKSkpqFOnDjQajd3XT0RERERERFRdlCmYf/LJJxEREYEtW7bAw8PDzlkyd+7cOUyYMAE3b95EYGAgrl27hoceeghff/01lEplhW6biIiIiIiIqCoqUzB/6dIl7N+/H25ubvbOj4Vp06bB19cXhw4dglKpxNWrV9G2bVt88sknmDVrVoVvn4iIiIiIiKiqKdN95ps3b15kE3t7i42NRdeuXaVa+IYNG6JevXqIjY29J9snIiIiIiIiqmrKVDP/3nvv4dFHH8VLL72Ehg0bQiaTmU3v0KGDXTIHAAsWLMArr7yCpk2bol69eti5cyfS0tIwffp0u22DiIiIiIiIqDopUzCflJSECxcuYPz48Van23MAvMGDB2Pbtm145plnEBgYiKioKCxcuBD169cvchmtVgutViu9T09PBwDMmzfPop/9nDlzEBAQgLi4OLz77rtW17ds2TIAwIkTJ7B27VqL6f7+/pg7dy4AYMeO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ZcusLrd06VIsWrTIIj0tLa3SC4oQApmZmQDu7B+RNSwrZAuWF7IFywvZguWFbMHyQrZgebHOVBldEpmo7Oi2GPn5+XBzc8NTTz2FDz74QEp/6aWX8NNPP1m93z1gvWY+JCQEqampcHd3r/B8F0cIIXUXYIGl4rCskC1YXsgWLC9kC5YXsgXLC9mC5cW69PR0eHh4IC0trdj4tUy3prtXHBwc0KNHD2RlZZmlZ2ZmQqVSFbmcUqmEUqm0SJfJZFWikJjyURXyQlUbywrZguWFbMHyQrZgeSFbsLyQLVheLJX2WFTpYB4AFixYgMGDB6NXr17o3r07Dh06hFWrVuG9996r7KwRERERERERVYoqH8z36tULP/zwA5YuXYoFCxagbt26+PTTT6V++0RERERERES1TZUP5gFgwIABGDBgQGVng4iIiIiIiKhKkJd1wejoaHzwwQd47rnnpLTdu3cjLy/PLhkjIiIiIiIiIuvKFMwfPnwYYWFh+O677/Dhhx9K6Vu3bsVXX31lt8wRERERERERkaUyBfNz5szBG2+8gcOHD5ulP/nkk/joo4/skjEiIiIiIiIisq5MfeaPHTuGX3/9FYD5sPkNGjTAlStX7JMzIiIiIiIiIrKqTDXzSqUSKSkpFulnzpyBr69vuTNFREREREREREUrUzA/bNgwzJ8/H3l5eVLN/IULFzB16lSMGDHCrhkkIiIiIiIiInNlCubfe+89XLhwAd7e3jAYDKhfvz7CwsLg7OyMJUuW2DuPRERERERERFRImfrMe3l54eDBg9ixYwf+++8/GAwGtGvXDoMGDYJCobB3HomIiIiIiIiokDIF8wAgl8sxcOBADBw40J75ISIiIiIiIqISlCmY37p1a7HThw8fXpbVEhEREREREVEplCmYnzRpktl7IQSys7MBAK6ursjMzCx/zoiIiIiIiIjIqjIF89aC9ejoaDz++OMYM2ZMuTNFREREREREREUr02j21gQHB+Ozzz7DsmXL7LVKIiIiIiIiIrLCbsE8AGg0GkRGRtpzlURERERERER0lzI1s79w4YJFWkpKCpYtW4ZWrVqVO1NEREREREREVLQyBfPNmze3mt62bVusWbOmXBkiIiIiIiIiouKVKZiPioqySPP09ISrq2u5M0RERERERERExStTMF+nTh1754OIiIiIiIiISqnUwfzvv/9e6pUOGDCgTJkhIiIiIiIiopKVOpgfMmRIqVean59fpswQERERERERUclKHcwzQCciIiIiIiKqGux6n/mK9sorr8DBwQEvvvhiZWeFiIiIiIiIqNKUaQC8wgwGAwwGg/lKHcq9Wgt//vknNm/ejAYNGkCv19t9/URERERERETVRZlq5rOysvDiiy+ibt26cHR0tHjYW0JCAh577DGsXbsWKpXK7usnIiIiIiIiqk7KFMy/8sor2LNnDz788EMYDAb89ttvWLRoEdRqNRYtWmTXDAohMHnyZDz11FPo2LGjXddNREREREREVB2VqT385s2bsWPHDoSFhQEA+vXrhwEDBqBVq1Z455138Nprr9ktg++++y6ysrLw8ssvl3oZrVYLrVYrvU9PTwdgvDAghLBb3srClIfKzgdVfSwrZAuWF7IFywvZguWFbMHyQrZgebGutMejTMF8dHQ0mjVrBgBwc3NDamoqvLy88MADD2DcuHFlWaVVR48exbvvvosjR45AoVCUermlS5dabSGQlpZW6QVFCIHMzEwAgEwmq9S8UNXGskK2YHkhW7C8kC1YXsgWLC9kC5YX60yV0SUpUzAvhIBcbmyh36RJE2zfvh2TJk3Cvn374OnpWZZVWnXgwAEkJSWhUaNGUpper8epU6fw8ccfQ6vVWg3y582bh1mzZknv09PTERISAo1GA3d3d7vlryxMFxM0Gg0LLBWLZYVswfJCtmB5IVuwvJAtWF7IFiwv1pX2WJQpmA8ODpZez507F5MnT8abb76Ja9euYeHChWVZpVVPP/00pk2bZpbWoUMH9OrVC8uWLSuytl6pVEKpVFqky2SyKlFITPmoCnmhqo1lhWzB8kK2YHkhW7C8kC1YXsgWLC+WKjSYv3XrlvR67NixaNKkCQ4dOoRmzZqhd+/eZVmlVTKZzOpt7opKJyIiIiIiIqoNyhQRf/nllxg9ejQ8PDwAAG3btkXbtm3tmS8iIiIiIiIiKkKZbk03b948BAQEYOTIkdiyZQt0Op2981Wko0eP4v33379n2yMiIiIiIiKqasoUzMfGxmLTpk1wdHTExIkTERAQgKeeegp79+6t8NHiFQqFNPgeERERERERUW1UpqjY0dERQ4cOxffff4/4+HgsX74cN27cwP3334/69evbO49EREREREREVEi5R5FTq9UYPHgwsrOzER0djbNn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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tau1_ms = 0.5\n", + "tau2_ms = 5.0\n", + "t_peak_ms = tau1_ms * tau2_ms / (tau2_ms - tau1_ms) * np.log(tau2_ms / tau1_ms)\n", + "exp2_factor = 1.0 / (np.exp(-t_peak_ms / tau2_ms) - np.exp(-t_peak_ms / tau1_ms))\n", + "kernel_area_ms = np.asarray([1.0, 4.0, exp2_factor * (tau2_ms - tau1_ms)])\n", + "kernel_labels = (\n", + " \"6 x ExpSyn(tau=1 ms)\",\n", + " \"6 x ExpSyn(tau=4 ms)\",\n", + " \"6 x Exp2Syn(tau1=0.5 ms, tau2=5 ms)\",\n", + ")\n", + "display(\n", + " pd.DataFrame(\n", + " {\n", + " \"member\": np.arange(REDUCTION_SIZE),\n", + " \"synapse declaration\": kernel_labels,\n", + " \"kernel area / unit weight (ms)\": kernel_area_ms,\n", + " }\n", + " )\n", + ")\n", + "\n", + "kernel_value = result.samples[\"kernel\"][\"value\"].values.to_decimal(u.uS * u.ms)\n", + "plot_accumulator(\n", + " \"kernel\",\n", + " kernel_value,\n", + " np.full(REDUCTION_SIZE, KERNEL_ALPHA),\n", + " np.full(REDUCTION_SIZE, float(KERNEL_THRESHOLD.to_decimal(u.uS * u.ms))),\n", + " \"value (uS ms)\",\n", + " [f\"Kernel member {member}: {label}\" for member, label in enumerate(kernel_labels)],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "spike-heading", + "metadata": {}, + "source": [ + "## Spike output\n", + "\n", + "最终 raster 保留网络中的全部 19 个 source:10 个 detailed HH Cell,以及三组各 3 个 reduction Cell。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "spike-raster", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-04T14:18:03.051631Z", + "iopub.status.busy": "2026-09-04T14:18:03.051085Z", + "iopub.status.idle": "2026-09-04T14:18:03.261653Z", + "shell.execute_reply": "2026-09-04T14:18:03.261034Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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amnQoEH2/e3atbPfQfLXX39p1apVGjt2rHbs2CFJKlGihDp37qw5c+Y4nHfevHkaPHhwmm94mTt3rj755BP9/vvvOn78uHbu3Klly5Y5hCd3GleSZs+ereXLl2v37t06fvy4li9frq+++iqLr9DdJyUlRcOGDdPnn3+uTZs26Z577tG2bds0cOBAffrppzp58qTOnTun9u3bq0ePHg6hV3x8vK5evarz588rKipKVapUkSRt3rxZAwcOVExMjE6fPq3ChQsrPDzcftz777+vxYsXa9euXTp69KhOnz4ti8Wi0NCcfUMS4QoAAACQjQYOHKjg4GBJsv+f2507d2a6D6T58+erZMmSuu+++7Rv3z7t3btXjRo1kslkst/d0LNnT12+fNnhTpRFixapX79+GT7HTa+99pr8/f0lSX379lV8fLwOHDiQI5/1blCwYEFNmDBBkuTt7a3//e9/2rJli3bt2uXQLzY2Vvv375fZbFatWrX0888/2/cNHTpUy5Ytsz/es2HDBh05ckQDBw5Mc8wZM2ZoyJAhqlevniSpZs2aGj58uGbMmJGq7+3GnTVrloYOHaqaNWtKkho2bGifQ0jbjRs31KtXL/3222/atGmTKlWqJEn68MMP1bx5c5UpU0b79u3Tvn371K1bNx04cMDhTiZJ+u9//ysvLy+Htvr166t3796SJF9fX3Xr1s3h9+PUqVPVp08fWa1W7du3T8ePH1e3bt20fPnybP28t/K6cxcAAAAAzipTpozDtr+/v65evZrpPvj7TohTp06pR48eDu3FihVTfHy8JKlIkSJq3769Fi5cqLZt22rbtm3av3+//YtxRs5x07//vdwMWfj3knEVK1ZU/vz57dtVqlSRj4+PDh8+rLp16+ro0aN6/PHHtXXrVpUuXVp+fn46fvy4wxonbdu2VfHixbV48WINGzZMc+bMUdu2bVW2bNk0xzx8+LCefvpph7Y6derY11bx9PTM0LhHjhzR888/73Ce4OBgh9AOjm4GI4cPH1axYsXs7fv27VN0dHSq/+aCg4MdHt3x9fVVqVKlUp33dr8frVarjh07pvnz52vp0qWpjouPj3eYg9mJcAUAAACAW/Dx8VFwcLB+/fXX2/br16+fhgwZouvXr2vRokVq2rSpKleunKlzwLhbw6qkpCQlJibag6pnn31WJUuW1MWLF+Xn5ydJ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1q3Tpk2b9M477zg8mnQnCxcu1LBhwzR+/HjVqlVL+/fv1/79+3X8+HHD9QEAAAAAANdxi8eCChcurOrVqzu0vf766/L19dWECRMUGBiojRs36t1339Xbb7+tGzdu6J577tGnn36qYsWKSZLy58+v6tWrp1o9uWjRoqpQoYJDW8GCBR3Gq127trZt26ZJkybpxRdflL+/v+677z599tlnDjXebv2UTZs2qWrVqvr000/16aef2tsbNmyoRYsWZfqaAAAAAACA3MHDduuzLsi1LBaLzGazYmNjFRAQ4Opybstmsyk2NlZms5kFleAWmLNwN8xZGHVwZFklXY6RV+FAVZt6MtvHY87CCOYr3A1z9u6R0e/hbvlYEAAAAAAAQG5BuAIAAAAAAGAA4QoAAAAAAIABbrGgLQAAALJW0XahSo63yDN/7l7HDZCYr3A/zNm8h3AFAAAgDyraLtTVJQAZxnyFu2HO5j08FgQAAAAAAGAA4QoAAAAAAIABhCsAAAAAAAAGEK4AAAAAAAAYQLgCAAAAAABgAOEKAAAAAACAAYQrAAAAAAAABhCuAAAAAAAAGEC4AgAAAAAAYADhCgAAAAAAgAGEKwAAAAAAAAYQrgAAAAAAABhAuAIAAAAAAGAA4QoAAAAAAIABhCsAAAAAAAAGEK4AAAAAAAAY4OXqAgAAAADgdi6umqzkeIs88weoaLtQV5cDAKkQrgAAAADI1S6umqykyzHyKhxIuAIgV+KxIAAAAAAAAAMIVwAAAAAAAAwgXMki+/fv19dff+3qMgAAAAAAQA5zi3Bl//79WrRoUar2yMhIffrpp7p27ZoLqnL0008/KSws7I79zpw5owULFmjjxo05UBUAAAAAAMhubhGu/PTTTxo2bJhD24cffqi2bdsqJSVF/v7+Lqos486fP69evXrp3nvv1ahRozR79mxXlwQAAAAAALKAW4Qrt5owYYJeeuklLV26VIMGDbK3nzlzRl999ZW+++47nTlzxuGY8+fPa968eUpMTNSmTZu0cOFCnT59Wn/++ae+++47JSQkaOPGjVq2bJmOHz+e5rh79+7V559/rp9//jnTd8vEx8ere/fuio6O1j333JPpzwwAAAAAAHInt3oVs81m07PPPquFCxdq9erVatGihX3fBx98oFdffVXNmzeXh4eH+vfvr/fee08DBw6UJB05ckSDBw/WokWLZLFYVKNGDTVo0EDff/+9pk2bpqJFiyowMFA3btxQ//799dVXX6l9+/aSpOTkZA0ePFhr1qxR8+bNdfr0aZ04cULLly9XgwYNMlR7uXLlVK5cuay/KAAAAAAAwKXcJlxJSUlR3759tW7dOkVGRqpevXr2fZs3b9bLL7+szZs3q2bNmpKkdevWqWPHjmrbtq1Kly5t73vPPffo3XfftW9/++23iomJ0fz58/Xggw9Kkp577jlNnDjRHq5MnjxZu3bt0uHDh1WgQAFJ0vjx4/XEE09ox44d2faZExISlJCQYN+2WCzZNhYAAAAAAHCO24QrVqtVX3zxhUaOHOkQrEjSggULVKFCBW3btk1bt26VzWaTJHl4eGjLli165JFH7H2feuqpVOeuVKmSPViRpGbNmjksoDt//nwFBwdr+fLlstlsstls8vb21s6dOxUbGyuz2ZzFn/ZvERERCg8Pz5ZzAwAAAACArOE24Yqfn59mzJihQYMGqXDhwnrllVfs+44fP67r16/rp59+cjimZ8+eKly4sENbqVKlUp27UKFCDtsmk8nhjpHjx4+rUKFCqc4/cOBAh35ZLSwsTKGhofZti8WioKCgbBsPAAAAAABkntuEK5LUv39/eXl5acCAAUpKStKECRMkSWazWWXLltW8efPueA4PD49Mj2s2m9W6dWv7eDnFZDLJZDLl6JgAAAAAACBz3O5tQb1799bixYv15ptv6tVXX5UkdejQQRs3btTOnTsd+p45c0ZWq9XwmB06dNDcuXNTvSHo2LFjhs8NAAAAAADcm1vduXJTjx495OXlpV69eik5OVlvvvmmvv32W7Vq1UpPP/20AgMDFRUVpZ9//ll//PGHfH19DY335ptvauPGjWrYsKEGDhwoHx8f/fbbb7Jarfruu+8yfJ6bd9acPn1aVqtV8+bNU4ECBdSjRw9D9QEAAAAAANdxiztXatasqf79+zu0de3aVcuXL9epU6e0efNmLV68WIsXL9aNGzd04MABNWzYUDt27LAvNlu8eHENHDhQ3t7eDuepU6eOOnfu7NBWrlw5h/GKFSumbdu2acyYMTp58qTOnj2r/v37a8WKFQ41duvW7bafIzIyUpGRkWrUqJGqVKmiyMhI/f77705dEwAAAAAAkDt42G6+Wge5nsVikdlsVmxsrAICAlxdzm3ZbDb7m5ScWecGyGnMWbgb5izcDXMWRhwcWVZJl2PkVThQ1aaezPbxmK9wN8zZ7JPR7+FucecKAAAAAABAbkW4AgAAAAAAYADhCgAAAAAAgAFu+bYgAAAAAHlH0XahSo63yDN/7l53EEDeRbgCAAAAIFcr2i7U1SUAwG3xWBAAAAAAAIABhCsAAAAAAAAGEK4AAAAAAAAYQLgCAAAAAABgAOEKAAAAAACAAYQrAAAAAAAABhCuAAAAAAAAGEC4AgAAAAAAYADhCgAAAAAAgAGEKwAAAAAAAAYQrgAAAAAAABhAuAIAAAAAAGAA4QoAAAAAAIABhCsAAAAAAAAGEK4AAAAAAAAYQLgCAAAAAABgAOEKAAAAAACAAYQrAAAAAAAABhCuAAAAAAAAGEC4AgAAAAAAYADhShaJjIzUhAkTXF0GAAAAAADIYW4RrkRGRmrkyJEObTabTW+//bZGjx6ta9euuaawf4mKitKSJUtu2yc5OVkLFizQyJEj9cYbb+jo0aM5VB0AAAAAAMgubhGuREVFafbs2fbtpKQkDRw4UJMmTdJjjz0mf39/F1aXMTabTV26dNGECRNUrFgx7dq1S/fcc4+2bdvm6tIAAAAAAIABXq4uILPi4+P12GOPaffu3dq4caOqV69u37dy5UqtW7dOPj4+atGihdq2bWvfd/ToUf3vf//T66+/rk8//VRHjx7ViBEj9Ndff2n79u3q1auXvvrqK509e1b333+/unTp4jCu1WrV4sWLtXv3bhUvXlxdunRR7dq1M1z38uXLtWrVKh08eFCVK1eWJHXu3FmjRo1SZGSksYsCAAAAAABcxi3uXLkpNjZWbdu21ZEjR7Rp0yaHYOXxxx/XyJEjFRAQoPz582vo0KEKDQ217z979qw+/vhjNW3aVH/99ZeCg4NVsGBB7dixQ5MmTVKnTp0UHx8vX19f9evXT++//7792CtXrqhx48b67LPPFBgYqIsXL+r+++/XsmXLMlz7t99+qyZNmtiDFUnq16+fNmzYoCtXrhi7MAAAAAAAwGXc5s6VpKQktWjRQr6+vvrll19UtGhR+76vvvpKP/74o/bv3y+z2SxJ6tOnj6pWraphw4Y5hDAvvviinnrqKYdzx8XF6fvvv1fFihUlSSaTSTNnztRzzz0nSXrttdcUGBiolStX2o+pU6eOnnvuOT366KMZqv/QoUP2899UsWJF2Ww2RUdHq0GDBqmOSUhIUEJCgn3bYrFkaCwAAAAAAJBz3CZcSU5O1vHjx/Xggw/aA5SbVqxYIV9fX7388suy2Wyy2WySJB8fH+3cudMhXOnYsWOqc1etWtUh+KhZs6ZOnDjhcP7AwECNGDHCfv5Lly7p1KlTOnv2rEqWLHnH+uPj41WwYEGHtoCAAEnS9evX0zwmIiJC4eHhdzw3AAAAAABwHbcJV0wmk9auXas2bdqoZ8+e+uKLL+Tt7S1JunDhgooXL55qDZQpU6aobt26Dm2FCxdOde78+fM7bHt6eio5Odm+feHCBTVr1izV+Vu1apXq2PQEBASkevzn8uXL9n1pCQsLc3i0yWKxKCgoKEPjAQAAAACAnOE24YokNWjQQD///LNat26tRx99VEuXLpWPj4/KlCmjkydPpnrcJ6uUKVNGxYsXN3T+4OBgbdiwwaFt79698vb2VtWqVdM8xmQyyWQyOT0mAAAAAADIfm61oK0k1a1bV+vWrdPmzZvVvXt3JSQkqF+/ftq5c6cWL17s0Hft2rW6evWq4TH79eunTz75RAcOHLC3JScna8WKFRk+R+/evfXnn3/ql19+kSTduHFDM2fO1COPPJLhu18AAAAAAEDu41Z3rtxUu3ZtRUZG6sEHH1TXrl319ddfa/LkyRo8eLBmzpypwMBARUVFqUSJElq+fLnh8cLCwrR//341aNBArVu3lo+Pj3bs2KFHH31UnTt3ztA5mjdvrpdeekkdO3ZU27ZttX//flmt1ky9cQgAAAAAAOQ+Hrabq7/mYnv27NHvv/+uJ554wqH98OHD+umnn/Sf//xH9erV05kzZ/TLL7/oxo0buueee1SnTh1733Pnzumrr75SSEiIvLz+P1PauXOnoqOj1b17d3vbsWPHtHbt2lTj7du3T1u3bpW/v78aN26ssmXLOtQYFRWlXr163faz7N69W9u2bVPRokXVpk2bTN21YrFYZDabFRsbm+46LbmFzWZTbGyszGazPDw8XF0OcEfMWbgb5izcDXMW7oT5CnfDnM0+Gf0e7hbhCv5GuAJkH+Ys3A1zFu6GOQt3wnyFu2HOZp+Mfg93uzVXAAAAAAAAchPCFQAAAAAAAAMIVwAAAAAAAAwgXAEAAAAAADCAcAUAAAAAAMAAwhUAAAAAAAADCFcAAAAAAAAMIFwBAAAAAAAwgHAFAAAAAADAAMIVAAAAAAAAAwhXAAAAAAAADPBydQHIPIvF4uoS7shms+natWvy8PCQh4eHq8sB7og5C3fDnIW7Yc7CnTBf4W6Ys9kno9+/CVfc0LZt2+Tv7+/qMgAAAAAAuKtdu3YtQ/0IV9xQw4YNFRAQ4OoybstmsykuLk4FChQgOYVbYM7C3TBn4W6Ys3AnzFe4G+Zs9uHOlbtYQECAW4QrNptNAQEB/McNt8CchbthzsLdMGfhTpivcDfMWddjQVsAAAAAAAADCFcAAAAAAAAMIFwBAAAAAAAwgHAFAAAAAADAAMIVAAAAAAAAAwhXAAAAAAAADOBVzAAAAACQR53duF/J1kR5+nqrZPMari7nrsF1zXsIVwAAAAAgjzq78YASLfHyDshPCJCFuK55D48FAQAAAAAAGEC4AgAAAAAAYMBdHa588cUX6tGjR46M9d1336lLly45MhYAAAAAAMg9XLrmyvDhw7V582ZJkslkUvny5TVw4EA9/PDDWXL+c+fOKSoqKkvOdScXLlzQ7t27092fnJysL7/8UsuXL9eZM2dUrVo1jRw5UrVq1cqR+gAAAAAAQPZw6Z0rBw4cUIkSJTRjxgy9/fbbqlixotq1a6fPP//clWVli+eee04rVqxQ165dNWHCBOXLl08NGzbUtm3bXF0aAAAAAAAwwOVvCypSpIjuvfdeSVKzZs20efNmzZo1S8WLF9fo0aMlSf7+/qpZs6bGjBmjihUrSpKOHTumHj166IsvvlClSpXs51u1apX++9//6qeffkpzvG3btmny5Mk6evSoypYtq+HDh6tly5b2/T///PNtx71p/fr1mjx5sq5du6bGjRsrMDDwtp9z0qRJ8vPzs2+3atVKW7du1YwZMzRr1qyMXi4AAAAAAJDL5Lo1V0qWLKnLly+rfv36mjFjhmbMmKHw8HB5eHjo3nvv1aVLlyRJFSpUUEpKiubNm+dw/LRp01SuXDn5+PikOndUVJSaN2+uYsWK6Y033lC1atXUpk0brV+/3t7nTuNK0s6dO9W2bVvVqFFD48aNU3x8vEaNGnXbz/XvYOWm/PnzKzExMTOXBwAAAAAA5DIuv3Pl344ePapVq1apX79+Kly4sP2OFunvOz22bNmizz//XE8//bQkKSQkRG+99Zb9MZszZ85o1apVWr16dZrnnzBhglq1aqX33ntPkvTggw/q+PHjeuWVV/TLL79IUobGfeONN9SxY0f973//s59nz549OnjwYIY/67p167Rp0ya99NJL6fZJSEhQQkKCfdtisWT4/AAAAAAAIGe4/M6VlStX6t5771WdOnVUs2ZNNWvWTG+88YaSk5M1d+5cde/eXU2bNtW9996r6OhoRUdH24/t16+fLl68aH8EaP78+QoKCtIDDzyQ5ljbtm1LtVhuu3bttH37dvt2RsbdunWrHnroIYfztGnTJsOf+eDBg+rVq5dCQkJu+4ahiIgImc1m+09QUFCGxwAAAAAAADnD5XeuNGnSRBMnTpSPj4/KlSunQoUKSZJee+01zZs3T+PHj1fVqlXl5+enkSNHKj4+3n6s2WxWjx49NGfOHLVt21Zz587V4MGD5eHhkeZYV69elb+/v0NbgQIFZLVadePGDfn4+Cg8PPyO48bFxaV6zOfW86bn8OHDevDBB9W2bVvNmDHjtn3DwsIUGhpq37ZYLAQsAAAAAADkMi4PV/69oO2/ff311xozZoyeeOIJe9uZM2dS9QsJCVGbNm303Xff6dChQxo0aFC6Y1WpUkV79+51aNuzZ4+CgoLsa7RkZNxKlSpp//79Dm379u1L/0P+48iRI3rggQfUokULzZ8/X/ny3f7GIZPJJJPJdMfzAgAAAAAA13H5Y0HpKVKkiLZt2yabzSZJeuedd3TkyJFU/e6//35VrFhRgwcPVuvWrW97Z0dISIjmzZunPXv2SPr7jUMffvihnnzyyUyNO2TIEM2ZM8e+xsqePXv06aef3vbzHD161B6sLFiwQJ6enhm4CgAAAAAAILdz+Z0r6XnrrbfUrVs3lS9fXsnJySpRokSad7hI0hNPPKGXXnpJQ4YMue05hwwZol27dqlhw4YqX768/vrrL/Xs2dPhTT8ZGTckJEQbN25U7dq1VaFCBVksFnXs2FG//vprumOPGjVKJ06c0N69e9WkSRN7e7169TR79uyMXBIAAAAAAJALuTRcmT59ury9vdPc17RpU/311186cuSI/Pz8VKFCBR05ciTN/kWKFFGRIkXUtWtXh/ZevXo5LG6bL18+ffDBB5o4caJOnDihMmXKqFixYpke18vLSwsXLtT//vc/xcfHq1KlSrp8+bJOnTqV7md9++23NW7cuFTtBQsWTPcYAAAAAACQ+7k0XKlWrdpt95tMJtWqVcu+Xbly5VR9kpOT9cEHH2jo0KGp1icpXry4ihcvnuqYwoULq3DhwobGlaTAwED7PxctWlRFixZN95xVqlRJdx8AAAAAAHBfuXbNlYwIDw9XxYoVde3aNY0ePdrV5QAAAAAAgDwo1665khEDBw5U9+7dVb16dfvbfgAAAAAAAHKSW4crFSpUcHUJAAAAAAAgj3PrcAUAAAAA4LySzasr2ZooT9+0XzQC53Bd8x7CFQAAAADIo0o2r+HqEu5KXNe8x60XtAUAAAAAAHA1whUAAAAAAAADCFcAAAAAAAAMIFwBAAAAAAAwgHAFAAAAAADAAMIVAAAAAAAAAwhXAAAAAAAADCBcAQAAAAAAMIBwBQAAAAAAwADCFQAAAAAAAAMIVwAAAAAAAAwgXAEAAAAAADCAcAUAAAAAAMAAwhUAAAAAAAADCFcAAAAAAAAM8HJ1AQAAAMh5ZzfuV7I1UZ6+3irZvIary7lrcF2zB9c1+3BtswfXNe8hXAEAAMiDzm48oERLvLwD8vMX/yzEdc0eXNfsw7XNHlzXvIfHggAAAAAAAAwgXAEAAAAAADDgrg5XbDabkpOT77qxAAAAAABA7uHScCUlJUVJSUlKSkqSzWbL8vNPmzZNwcHBWX7etMyfP1+VK1fOUN/k5GSlpKRkc0UAAAAAACAnuDRcadu2rXx8fOTr6yuTyaRKlSopPDz8rrwDJCUlRStWrFCHDh3k4+OjIUOGuLokAAAAAACQBVz+tqC+fftq4cKFSkxM1KpVq9SjRw/5+Pho7Nix9pDF09NTHh4eDsfdfAzHyyv1R0hKSkqzPSP+/XhPWuM6a9euXZo5c6aeeeYZWa3WLDknAAAAAABwvVyz5oq3t7c6d+6szp07a8WKFfr666/l6+srX19f+fn5qV69evruu+/s/Y8fPy6TyaQtW7Y4nOfjjz9W6dKllZiYmOY4n3zyiSpXrixPT0+VK1dO7777rsP+O41705QpU1SyZEkVLFhQDz74oA4cOHDbz1e/fn2tWLFCnTp1Ur58ueayAwAAAAAAg3Ldt/yUlBR5eHioe/fu9vVYLl26pBdeeEGPPfaYDh06JEkqX7682rRpozlz5jgcP2fOHPXv31/e3t6pzr1q1So988wzeuONN2SxWDR9+nSNHz9e8+bNs/e507iStGLFCo0bN04zZszQ2bNnNWzYsFQhTVZISEiQxWJx+AEAAAAAALmLy8MVm82mpKQkXbt2TcuWLdN3332nRx55xKGPyWRSv3791LhxY61YscLeHhISoiVLlig+Pl6StHfvXm3ZsiXd9UwmTZqkPn36qE+fPvL391enTp30zDPPaNKkSWn2T2/cKVOm6PHHH1e3bt3k5+enXr16qVevXkYvRSoREREym832n6CgoCwfAwAAAAAAGOPycGXx4sXy9fVV8eLF9corr2jChAkaNWqUrly5omHDhqlMmTLy9vaWr6+vNmzYoOPHj9uPfeSRR2QymbRs2TJJfz/y06hRI9WpUyfNsfbt26fGjRs7tDVp0kSHDh2yr7OSkXH37dune++91+E8jRo1ypLr8W9hYWGKjY21/5w4cSLLxwAAAAAAAMa4PFzp27evkpKSdP36de3bt0/jxo2Tp6enRo4cqaioKP3444+Kj49XUlKS2rRpo6SkJPux3t7eevzxxzVnzhwlJiZq4cKFd3wLz62vfL51OyPjZuQ8WcFkMikgIMDhBwAAAAAA5C4uD1fS8/vvv2vAgAEKDg6Wj4+PrFardu3alapfSEiI1q9frw8++EBXr15Vnz590j1ncHCwNm/enGqc6tWry9PTM8PjBgcH648//nBou/W8AAAAAAAgb8i14UqtWrX0+eefKyYmRidOnFBISIjOnj2bql/16tXVrFkzjR07Vo8++qjMZnO65xwzZoyWLFmi+fPn6/Lly/rqq680Y8YMjR07NlPjhoaGasGCBVq8eLEuXbqk+fPn68svv7zjZ7q5UK7NZrOvNXPzcSQAAAAAAOCeXBqueHp62u8YudV7770nk8mkmjVrqlGjRipQoIDatWuXZv8nnnhCiYmJqR4Jypcvn7y8vOzbrVu31ty5c/X2228rMDBQY8aM0f/+9z/169cvU+N26NBB77zzjsaMGaNq1appyZIlGjdunMNYabn5iuf169dr0aJF8vX1VfXq1TN0rQAAAAAAQO50+zQgm61evTrdfUFBQVq1alWGznP69GlVqlRJrVq1cmh/5pln9Mwzzzi09evXzyFMcXbcESNGaMSIEQ5t4eHhtz3m1nVbAAAAAACA+8u1jwVlREpKio4fP64PP/xQzz//vDw8PFxdEgAAAAAAyGPcOlwZOnSoatasqebNm6e6QwUAAAAAACAnuPSxIKM++eQTffLJJ64uAwAAAAAA5GFuHa4AAADAOSWbV1eyNVGevt6uLuWuwnXNHlzX7MO1zR5c17yHcAUAACAPKtm8hqtLuCtxXbMH1zX7cG2zB9c173HrNVcAAAAAAABcjXAFAAAAAADAAMIVAAAAAAAAAwhXAAAAAAAADCBcAQAAAAAAMIBwBQAAAAAAwADCFQAAAAAAAAMIVwAAAAAAAAwgXAEAAAAAADCAcAUAAAAAAMAAwhUAAAAAAAADCFcAAAAAAAAMIFwBAAAAAAAwgHAFAAAAAADAAMIVAAAAAAAAAwhXAAAAAAAADPBydQEAgJxzduN+JVsT5enrrZLNa7i6nLsK1zZ7cF2zD9c2e3BdswfXNftwbbMH1zXvIVwBgDzk7MYDSrTEyzsgP3/QZzGubfbgumYfrm324LpmD65r9uHaZg+ua97DY0EAAAAAAAAGEK4AAAAAAAAYcFeHK6dPn9b27dtzZKxz585p69atOTIWAAAAAADIPVwarkRFRWnjxo3auHGj/vjjD507dy5Lz79s2TL17ds3S8+Znh9++EE9evS4Y7+4uDht3bpVx44dy/6iAAAAAABAtnPpgrYjR47U7t27Va1aNSUkJGjPnj1q2bKlFi9erEKFCrmytGwxb948jRgxQmXLltXJkyfVvHlzLV26VAUKFHB1aQAAAAAAwEkufyyobdu29jtX9u/fr61bt+qVV17RxYsX7Xe17NixQ1evXnU47sqVK9q4caOSk5Md2i0WizZu3KjExMR0xzx79qy2bNmimJiYVPvuNO6/HT16VFFRUbpx48YdP+fevXsVEhKijz76SPv379dff/2lw4cPa/To0Xc8FgAAAAAA5F4uD1f+rVy5cmrbtq1+++03/fnnnxo7dqzGjh2rwYMHq2TJkgoPD3fo37ZtW61atcqh7f3339eQIUPk7e2d6vzJyckaOnSoKlSooJCQEFWtWlU9e/ZUfHy8vU9Gxk1MTFSvXr1Uq1Yt9e3bV0FBQfrhhx9u+9nmz5+voKAgDRgwQJJUtGhRPf3001qwYMFtgyAAAAAAAJC7ufSxoLScPn1aRYoUUatWrbRx40Z7+549e9S0aVO1bt1azZo1U6FChdSjRw/NmTNHHTt2lCTZbDbNmzdPTzzxRJrnnjVrlpYtW6YdO3aoRo0aOnHihJo2baq3335b48ePl6Q7jitJM2fOVGRkpPbu3auKFSvqwIED+s9//iOz2Zzu59qxY4caNmzo0Na4cWPFxcXp8OHDqlmzZqpjEhISlJCQYN+2WCx3unwAAAAAACCHufzOlfPnz2vjxo1au3atXnrpJa1bt05PPvmkff/x48e1efNmXb58WdWrV9f69evt+0JCQrRixQqdP39ekrR+/XodO3ZMAwcOTHOsOXPmaPDgwapRo4YkKSgoSM8++6zmzJmTqu/txp03b56GDBmiihUrSpKqV6+e7pg3Xbp0SUWLFnVou7l96dKlNI+JiIiQ2Wy2/wQFBd12DAAAAAAAkPNcfufKjh07NHbsWPn4+Kh8+fJau3atHnzwQR04cECPPfaYTpw4oQoVKsjPz0/R0dE6c+aM/dgWLVqoUqVKWrhwoV544QXNmTNHDz/8sMqUKZPmWNHR0Ro6dKhDW82aNXXixAnduHFDPj4+GRo3Ojpa1atXdzjPrdu38vb2ltVqdWi7+TiSj49PmseEhYUpNDTUvm2xWAhYAAAAAADIZVwerrRt21YLFy5M1f7CCy8oODhY27Ztk5fX32W2atVKKSkpDv2eeOIJzZkzR0888YSWLVumBQsWpDtWQECA4uLiHNquXr0qPz8/e8CRkXHTO8/tlC9fPtUCuje3y5Url+YxJpNJJpPptucFAAAAAACu5fLHgtJz9OhR3X///faA49y5c9q2bVuqfgMHDtSBAwc0atQo+fv7q3Pnzumes1GjRqkWwF25cqXuvffeTI3bqFEjrVmzxqFt9erVt/08bdq00caNG3X58mV72zfffKM6deqoZMmStz0WAAAAAADkXi6/cyU9Dz74oN59912VKFFCKSkpioiISPOtOiVKlFCXLl00e/ZsvfDCC2m+JeimCRMm6N5779XTTz+tLl26aN26dfryyy/1888/Z2rcl19+WY0bN9bzzz+vhx9+WCtWrNBvv/2mEiVKpDt2//79NXXqVHXp0kWjRo3Srl27NH/+fC1fvjzzFwcAAAAAAOQaLg1X6tSpoyJFiqS575133lHx4sX18ccfy8/PT88995wOHDiQZoDRp08fLVu2TEOGDHFoL1OmjMMbemrWrKnff/9dU6dO1aRJkxQYGKj169frP//5T6bGrVu3rtatW6epU6dq+vTpatSokRYuXKjFixen+1lNJpPWr1+vt956Sx988IGKFi2q1atX66GHHsrw9QIAAAAAALmPS8OVKVOmpLsvf/78mjBhQobOs2nTJjVp0kS1a9d2aO/evbu6d+/u0FanTh198sknhse97777dN9996Ua73aKFCmit99++47nBgAAAAAA7iPXPhaUEYcOHdKOHTs0c+bMNBfFBQAAAAAAyG5uHa58+eWXWrt2rd544w117drV1eUAAAAAAIA8yK3DlXHjxmncuHGuLgMAAAAAAORhbh2uAAAyp2Tz6kq2JsrTN/03q8E5XNvswXXNPlzb7MF1zR5c1+zDtc0eXNe8x8Nms9lcXQQyxmKxyGw2KzY2VgEBAa4u57ZsNptiY2NlNpvl4eHh6nKAO2LOwt0wZ+FumLNwJ8xXuBvmbPbJ6PfwfDlYEwAAAAAAwF2HcAUAAAAAAMAAwhUAAAAAAAADCFcAAAAAAAAMIFwBAAAAAAAwgHAFAAAAAADAAC9XF4DMs1gsri7hjmw2m65duyYPDw9eBQa3wJyFu2HOwt0wZ+FOmK9wN8zZ7JPR79+EK25o27Zt8vf3d3UZAAAAAADc1a5du5ahfoQrbqhhw4YKCAhwdRm3ZbPZFBcXpwIFCpCcwi0wZ+FumLNwN8xZuBPmK9wNczb7cOfKXSwgIMAtwhWbzaaAgAD+44ZbYM7C3TBn4W6Ys3AnzFe4G+as67GgLQAAAAAAgAGEKwAAAAAAAAYQrgAAAAAAABhAuAIAAAAAAGAA4QoAAAAAAIABhCsAAAAAAAAG8CpmAMhDJketlyXRqgBvX4XWbunqcgC4EL8PAEj8LgCyCuEKAOQhk/dsUMz1WAX6mfkLFJDH8fsAgMTvAiCr8FgQAAAAAACAAYQrAAAAAAAABhCuAAAAAAAAGOC2a6788ssveu+999LcN3v2bBUqVCjHavn++++1du1aTZ48+bb9fvvtN33zzTc6c+aMqlWrpqFDh6p48eI5VCUAAAAAAMgObhuu/PXXX1q2bJk+//xz5cvneANO/vz5c7SWQ4cOac2aNbftExYWpg0bNqhz586qUaOGvvzyS7377rvasmWLKleunEOVAgAAAACArOa24cpN3bt3l5dX6o9x9OhRvfTSS5o2bZpKlixpb9+8ebOmTJmi+fPny2Qy6cqVK5o1a5Z2796t4sWLq3v37mrevLm9//Lly/Xbb7+pT58+WrJkic6ePav7779fgwcPloeHh3755RfNmzdPJ06cUI8ePSRJISEhateunUM9w4cPV0REhH17wIABqlOnjqZMmaIPP/wwqy8LAAAAAADIIXftmisVKlTQ5s2b9fnnnzu0z5gxQ1euXJHJZNLZs2fVoEEDbdu2TQ899JCKFy+url27au7cufb++/fv10cffaRBgwapfPnyqlevnl566SW9+eabkqTy5curfv36MpvN6t27t3r37q2qVaumqqds2bIO256enipdurQuX76cDZ8eAAAAAADkFLe/c6VXr17y8PCwb5coUULTp0+Xh4eH+vTpo0WLFum5556TJFmtVn311VeaNm2aJOnVV19VgwYNtGTJEvvxlSpV0ogRIzR48GB7240bN7Ry5UqVLl3avj1nzhy98sorKleunOrWras//vjDfudKRuzatUsbNmzQJ598km6fhIQEJSQk2LctFkuGzw8AAAAAAHLGXRGu/HvNFX9/f/s/9+/fX5MmTdLhw4dVpUoVrVixQsnJyerWrZskadWqVSpWrJh69+4tm80mm80mi8WiCxcu6PTp0/YwpWrVqvZ/lqTKlSsrJibG6ZrPnj2r7t27q127dhowYEC6/SIiIhQeHu70OAAAAAAAIPu5fbiS3porknTPPfeoTp06WrRokcaPH69Fixapa9eu9gDmypUrat++vdq0aeNwXEhIiAICAuzbJpPJYX++fPmUkpLiVL3nz5/XQw89pAoVKuiLL75wuOvmVmFhYQoNDbVvWywWBQUFOTUuAAAAAADIHm4frtxJv3799Mknn+jZZ5/VypUr9c0339j3BQUFycvLK1OP86TldgHJv124cEEPPfSQSpQooRUrVtzxrUYmkylVsAMAAAAAAHKXu3ZB25v69u2rw4cPa/To0SpcuLDDXSpPPPGE5s6dqz/++MPeFh8fr08//TRTYxQrVkwXLlyQzWZLt8/Fixf10EMPqVixYvruu+/k5+eX+Q8DAAAAAAByHbe/c+XWBW2lv9cqufnGnqCgILVo0UKffPKJnn/+eXl6etr7jRw5UseOHVOLFi3UoEED+fj46NChQxoxYkSmamjTpo2Sk5P1n//8R0FBQWm+ivnZZ5/V7t271b59ez3++OP29po1a+r111/P7McGAAAAAAC5hNuGKy1atNCXX36Z5r6iRYs6bE+bNk379u1T06ZNHdrz5cun999/X+PGjdOOHTvk7++vunXrymw22/t069ZNjRs3djiucePGWrhw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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "population_order = (\"hh\", \"event\", \"payload\", \"kernel\")\n", + "population_offsets = {\n", + " \"hh\": 0,\n", + " \"event\": HH_SIZE,\n", + " \"payload\": HH_SIZE + REDUCTION_SIZE,\n", + " \"kernel\": HH_SIZE + 2 * REDUCTION_SIZE,\n", + "}\n", + "population_colors = {\n", + " \"hh\": \"#0072B2\", \"event\": \"#009E73\",\n", + " \"payload\": \"#CC79A7\", \"kernel\": \"#D55E00\"\n", + "}\n", + "fig, axis = plt.subplots(figsize=(11, 8), constrained_layout=True)\n", + "for population_name in population_order:\n", + " frame = spike_table[spike_table[\"population\"] == population_name]\n", + " axis.scatter(\n", + " frame[\"time_ms\"],\n", + " frame[\"source_id\"] + population_offsets[population_name],\n", + " marker=\"|\", s=190, linewidths=2,\n", + " color=population_colors[population_name], label=population_name,\n", + " )\n", + "source_labels = (\n", + " [f\"HH {member}\" for member in range(HH_SIZE)]\n", + " + [f\"Event {member}\" for member in range(REDUCTION_SIZE)]\n", + " + [f\"Payload {member}\" for member in range(REDUCTION_SIZE)]\n", + " + [f\"Kernel {member}\" for member in range(REDUCTION_SIZE)]\n", + ")\n", + "axis.set_yticks(np.arange(HH_SIZE + 3 * REDUCTION_SIZE), source_labels)\n", + "for boundary in (HH_SIZE, HH_SIZE + REDUCTION_SIZE, HH_SIZE + 2 * REDUCTION_SIZE):\n", + " axis.axhline(boundary - 0.5, color=\"#BBBBBB\", linewidth=1)\n", + "axis.set_ylabel(\"Spike source\")\n", + "axis.set_xlabel(\"Time (ms)\")\n", + "axis.set_xlim(1.5, 13.0)\n", + "axis.grid(axis=\"x\", alpha=0.2)\n", + "axis.legend(ncols=4, loc=\"upper right\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "meaning", + "metadata": {}, + "source": [ + "## What the outputs mean\n", + "\n", + "- Event accumulator 的输入始终是 6,与 connection weight、synapse 类型和 tau 无关。\n", + "- Payload accumulator 的输入是 `6 × source weight`,能保留多个 connection 聚合后的幅值,但仍不读取 synapse metadata。\n", + "- Kernel accumulator 把每个 payload 乘以静态 kernel 面积。其 `uS·ms` 输出是积分 conductance,不是膜电压或电流。\n", + "- HH spike 经过 `0.1 ms` delay 后进入 reduction Cell;所有下游继续发布统一的 `event_outputs[\"spike\"]`。" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "braincell_311", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_compartment/synapse_learning.ipynb b/examples/multi_compartment/synapse_learning.ipynb new file mode 100644 index 00000000..03371389 --- /dev/null +++ b/examples/multi_compartment/synapse_learning.ipynb @@ -0,0 +1,519 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bf35e317", + "metadata": {}, + "source": [ + "# Synapse 参数学习\n", + "\n", + "沿用 Channel/Ion 的 `parameter`、`scale`、`parameterized`,每个示例只有一个可训标量、一条带 spike 的 1 CV 电压轨迹。模型与训练函数在同目录 `synapse_learning.py`,本 notebook 可直接从头执行。\n", + "\n", + "| 目标 | 参数入口 | 梯度路径 | 边界 |\n", + "|---|---|---|---|\n", + "| ExpSyn `tau`、`e` | 显式构造签名;`synapses.trainable(...)` | 连续状态/电流 | 无有效输入时可以为零 |\n", + "| Exp2Syn `tau1`、`tau2` | 同上 | 衰减及动态计算的归一化 factor | 初始值需满足 `0 < tau1 < tau2` |\n", + "| Connection `weight` | `connection.trainable(weight=...)` | 浮点事件载荷乘权重 | 固定 NetStim 不需要对事件时间求导 |\n", + "| 检测器 `threshold` | `source.trainable(threshold=...)` | 硬事件前向,代理梯度反向 | 相同事件时间格可以对应多个阈值 |\n", + "| `delay` | 不开放训练 | 固定队列/路由 | 明确抛出 NotImplementedError |\n", + "| 字符串、错误单位/形状 | 不可作为有效数值目标 | 注册/数值运算检查 | 不维护科学参数白名单 |\n", + "\n", + "上穿采用 `last < threshold <= next`,下穿采用 `last > threshold >= next`。到达阈值计一次,在阈值停留或从阈值离开不重复发放。NEURON 9.0.1 的 APCount 使用 `>=`,固定步长 NetCon 使用 `>`;不能将本规则与 NEURON 所有检测器一概视为相同。\n", + "\n", + "训练根与物理运行参数分离:每次 reset 从当前根物化参数,清空动态状态与事件队列,不重置优化器根。参数正值/顺序约束在初始化与注册时检查;优化过程中应由用户选择合适的参数变换维持约束。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fd151939", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T09:27:11.870877Z", + "iopub.status.busy": "2026-09-07T09:27:11.869966Z", + "iopub.status.idle": "2026-09-07T09:27:14.748709Z", + "shell.execute_reply": "2026-09-07T09:27:14.747770Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "JAX 0.8.0; backend=cpu\n" + ] + } + ], + "source": [ + "import os\n", + "os.environ['JAX_PLATFORMS'] = 'cpu'\n", + "from pathlib import Path\n", + "import sys\n", + "root = next(p for p in [Path.cwd(), *Path.cwd().parents] if (p / 'pyproject.toml').exists())\n", + "sys.path.insert(0, str(root))\n", + "import braincell\n", + "import brainstate\n", + "import brainunit as u\n", + "import jax\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import Markdown, display\n", + "from examples.multi_compartment.synapse_learning import DT, build_cell, fit_one\n", + "print(f'JAX {jax.__version__}; backend={jax.default_backend()}')\n" + ] + }, + { + "cell_type": "markdown", + "id": "25148254", + "metadata": {}, + "source": [ + "## 1. 固定 NetStim 输入:学习 tau、weight\n", + "\n", + "每次只拟合一个因子,从 0.8 开始,目标因子为 1。使用 6 ms 电压轨迹与 100 次 Adam 更新。固定外部事件没有需要学习的上游电压,因此 tau/weight 的连续梯度不依赖事件代理梯度。\n", + "\n", + "## 2. 自连接:学习检测阈值\n", + "\n", + "只学习 `threshold = -20 mV + delta * mV`,delta 从 -10 开始。默认 Cell 的 spike 检测器经 0.1 ms 固定延迟驱动自身 ExpSyn,仍使用电压损失。训练阈值使用代理梯度,不把硬事件的有限差分当作正确梯度。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "de29f385", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T09:27:14.754514Z", + "iopub.status.busy": "2026-09-07T09:27:14.754164Z", + "iopub.status.idle": "2026-09-07T09:27:37.985146Z", + "shell.execute_reply": "2026-09-07T09:27:37.984083Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| 参数 | 初始梯度(根坐标) | 初始 MSE | 最终 MSE | 最终根 | 目标 spike 数 |\n", + "|---|---:|---:|---:|---:|---:|\n", + "| tau | -0.695905 | 0.0564968 | 2.48711e-07 | 1.00125 | 1 |\n", + "| weight | -32.5734 | 3.11666 | 9.70201e-07 | 1.00061 | 1 |\n", + "| threshold | -0.000121621 | 2.5993e-05 | 0 | -1.41997 | 1 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "threshold 的最终根是 delta,不是阈值本身;相同事件格不要求 delta 精确回到 0。\n" + ] + } + ], + "source": [ + "results = []\n", + "with brainstate.environ.context(dt=DT):\n", + " for field in ('tau', 'weight', 'threshold'): # independent experiments, not simulation steps\n", + " results.append(fit_one(field))\n", + "fig, axes = plt.subplots(3, 2, figsize=(10, 8))\n", + "for row, result in enumerate(results):\n", + " times = np.arange(result['target'].size) * float(DT / u.ms)\n", + " for key, color in [('target', 'black'), ('initial', '#b06b38'), ('fitted', '#138879')]:\n", + " axes[row, 0].plot(times, result[key], label=key, color=color, alpha=.8)\n", + " axes[row, 0].set(title=result['field'], xlabel='Time (ms)', ylabel='Voltage (mV)')\n", + " axes[row, 0].legend()\n", + " axes[row, 1].semilogy(np.maximum(result['history'], 1e-12), color='#138879')\n", + " axes[row, 1].set(xlabel='Adam update', ylabel='Voltage MSE (mV squared)')\n", + "fig.tight_layout()\n", + "plt.show()\n", + "display(Markdown('| 参数 | 初始梯度(根坐标) | 初始 MSE | 最终 MSE | 最终根 | 目标 spike 数 |\\n|---|---:|---:|---:|---:|---:|\\n' + '\\n'.join(\n", + " f\"| {r['field']} | {r['initial_gradient']:.6g} | {r['initial_mse']:.6g} | {r['final_mse']:.6g} | {r['fitted_root']:.6g} | {r['spikes']} |\" for r in results)))\n", + "print('threshold 的最终根是 delta,不是阈值本身;相同事件格不要求 delta 精确回到 0。')\n" + ] + }, + { + "cell_type": "markdown", + "id": "c344fcb2", + "metadata": {}, + "source": [ + "## 3. 自动报错与零梯度\n", + "\n", + "下面没有输入事件,ExpSyn 的 g 从零开始且保持为零,所以电流对 tau 的梯度为零。这不是注册失败。错误单位、非法时间常数以及 delay 训练会被拒绝。" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "98ecd3b6", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T09:27:37.991050Z", + "iopub.status.busy": "2026-09-07T09:27:37.990794Z", + "iopub.status.idle": "2026-09-07T09:27:38.267547Z", + "shell.execute_reply": "2026-09-07T09:27:38.266700Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| 检查 | 实际结果 |\n", + "|---|---|\n", + "| tau,无事件 | gradient = 0 |\n", + "| tau 单位错误 | ValueError |\n", + "| tau 非正 | ValueError |\n", + "| name 字符串 | KeyError |\n", + "| delay | NotImplementedError |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cell = build_cell()\n", + "cell.synapses['syn'].trainable(tau=braincell.trainable.scale(name='tau'))\n", + "cell.init_state()\n", + "syn = cell.synapses['syn']\n", + "node = cell.runtime.get_runtime_node(syn._store.layout_id('ExpSyn'))\n", + "def silent_current():\n", + " cell.reset_state()\n", + " return node.current(-65.*u.mV).to_decimal(u.nA).sum()\n", + "zero = brainstate.transform.jit(brainstate.transform.grad(\n", + " silent_current, grad_states=cell.trainables.parameters().states()))()['tau']\n", + "assert float(zero) == 0.\n", + "rows = [f'| tau,无事件 | gradient = {float(zero):g} |']\n", + "for label, field, value in [('tau 单位错误', 'tau', 1.*u.mV), ('tau 非正', 'tau', -1.*u.ms), ('name 字符串', 'name', 'text')]:\n", + " bad = build_cell()\n", + " try:\n", + " bad.synapses['syn'].trainable(**{field: braincell.trainable.parameter(value)})\n", + " except (TypeError, ValueError, KeyError) as error:\n", + " rows.append(f'| {label} | {type(error).__name__} |')\n", + " assert not bad.trainables.bindings()\n", + " else:\n", + " raise AssertionError(label)\n", + "bad = build_cell()\n", + "connection = braincell.connect('input', source=braincell.NetStim(), synapse=bad.synapses['syn'], weight=.001*u.uS)\n", + "try:\n", + " connection.trainable(delay=braincell.trainable.parameter(.1*u.ms))\n", + "except NotImplementedError as error:\n", + " rows.append(f'| delay | {type(error).__name__} |')\n", + "else:\n", + " raise AssertionError('delay unexpectedly trainable')\n", + "display(Markdown('| 检查 | 实际结果 |\\n|---|---|\\n' + '\\n'.join(rows)))\n" + ] + }, + { + "cell_type": "markdown", + "id": "7a839c08", + "metadata": {}, + "source": [ + "## 4. BPTT / 完整状态 RTRL\n", + "\n", + "复用已有实验 RTRL,不实现新算法。先比较单 Cell 自连接的电压损失和 spike 损失,再检查 Network。全状态包含突触状态、事件队列及跨 Cell 依赖,参数在一次 rollout 内固定;这里只证明前向敏感度递推兼容,不代表已支持每个时间步更新优化器参数。\n", + "\n", + "spike 损失为 `(spike - 1)^2`,这里只是导数探针,不是生物学目标。某些固定延迟使反馈到达后不再经过代理函数的有效梯度区域,此时 weight 的 spike-loss 梯度可以为零。两种算法必须对此给出相同结果。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd894eb1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T09:27:38.271841Z", + "iopub.status.busy": "2026-09-07T09:27:38.271684Z", + "iopub.status.idle": "2026-09-07T09:28:18.146080Z", + "shell.execute_reply": "2026-09-07T09:28:18.145251Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| 模型 | 损失 | 固定 delay (ms) | 最大绝对梯度差 | weight 梯度 |\n", + "|---|---|---:|---:|---:|\n", + "| Cell autapse | voltage | 0 | 3.49e-10 | -741.38 |\n", + "| Cell autapse | voltage | 0.1 | 5.24e-10 | -792.68 |\n", + "| Cell autapse | spike | 0 | 8.88e-16 | 0.000659412 |\n", + "| Cell autapse | spike | 0.1 | 3.33e-15 | 0 |\n", + "| Two-Cell Network | voltage | 0.1 | 1.88e-12 | 0 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from examples.experimental.optim_gradient_correctness.autapse import compare\n", + "checks = []\n", + "with brainstate.environ.context(dt=DT, precision=64):\n", + " for mode in ('voltage', 'spike'):\n", + " for delay_ms in (0., .1):\n", + " result = compare(loss_kind=mode, delay=delay_ms*u.ms)\n", + " checks.append(f\"| Cell autapse | {mode} | {delay_ms:g} | {result['max_abs_error']:.3g} | {result['gradients']['weight']:.6g} |\")\n", + " result = compare(network=True, paired=True)\n", + " checks.append(f\"| Two-Cell Network | voltage | 0.1 | {result['max_abs_error']:.3g} | {result['gradients']['cell.weight']:.6g} |\")\n", + "display(Markdown('| 模型 | 损失 | 固定 delay (ms) | 最大绝对梯度差 | weight 梯度 |\\n|---|---|---:|---:|---:|\\n' + '\\n'.join(checks)))\n" + ] + }, + { + "cell_type": "markdown", + "id": "bidirectional-scope", + "metadata": {}, + "source": [ + "## 5. 两个 population,双方参数与双向连接同时可训\n", + "\n", + "A 有 2 个成员,B 有 3 个成员,每个成员仍是 1 CV。A→B 和 B→A 各有 6 个 contact,包含汇聚与发散。A 接收 ExpSyn,B 接收 Exp2Syn。两次错开的电流脉冲确保双方接收反馈后仍有后续放电。\n", + "\n", + "| 区域 | 可训字段 | 梯度验证分组 | 拟合分组 |\n", + "|---|---|---|---|\n", + "| A、B 的 Na channel | g_max、V_sh | 每成员独立 | 各 population 内共享 |\n", + "| A、B 的 SodiumFixed | E | 每成员独立 | 各 population 内共享 |\n", + "| A 的 ExpSyn | tau、e | 每突触独立 | A 内共享 |\n", + "| B 的 Exp2Syn | tau1、tau2、e | 每突触独立 | B 内共享 |\n", + "| A、B 的检测器 | threshold | 每成员独立 | 各 population 内共享 |\n", + "| A→B、B→A | weight | 每 contact 独立 | 各方向内共享 |\n", + "\n", + "完整梯度验证有 45 个标量自由度,联合拟合保留 15 个。delay 固定且异质,训练中不改变路由。此处是小型双向网络验证,不是对所有机制或网络规模的穷举证明。\n", + "\n", + "下方 gmax、ion、tau、weight 参数根表示无量纲缩放因子;shift、reversal、threshold 根表示以 mV 计的偏移,不是绝对电压。A.weight 对应 B→A,B.weight 对应 A→B,名称按接收方归属。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bidirectional-gradient", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T09:28:18.151182Z", + "iopub.status.busy": "2026-09-07T09:28:18.150998Z", + "iopub.status.idle": "2026-09-07T09:29:07.279491Z", + "shell.execute_reply": "2026-09-07T09:29:07.277846Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| 成员 | spike 数 | 最大接收电导 (uS) |\n", + "|---|---:|---:|\n", + "| A0 | 2 | 0.00112587 |\n", + "| A1 | 2 | 0.00123126 |\n", + "| B0 | 2 | 0.000751331 |\n", + "| B1 | 2 | 0.0008344 |\n", + "| B2 | 2 | 0.0009164 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| 参数根 | 形状 | 最大绝对梯度 | BPTT/RTRL 最大绝对差 |\n", + "|---|---|---:|---:|\n", + "| A.gmax | (2,) | 70371.5 | 3.35e-10 |\n", + "| A.ion | (2,) | 105331 | 2.33e-10 |\n", + "| A.reversal | (2,) | 30.3741 | 1.99e-13 |\n", + "| A.shift | (2,) | 4555.1 | 2.22e-10 |\n", + "| A.tau | (2,) | 2669.05 | 3.37e-11 |\n", + "| A.threshold | (2,) | 38.4637 | 5.08e-13 |\n", + "| A.weight | (6,) | 566.737 | 3.07e-12 |\n", + "| B.gmax | (3,) | 60480.7 | 1.16e-10 |\n", + "| B.ion | (3,) | 105668 | 1.16e-10 |\n", + "| B.reversal | (3,) | 121.859 | 9.95e-13 |\n", + "| B.shift | (3,) | 743.612 | 4.34e-11 |\n", + "| B.tau1 | (3,) | 450.032 | 4.04e-12 |\n", + "| B.tau2 | (3,) | 1562.53 | 4.32e-12 |\n", + "| B.threshold | (3,) | 183.281 | 1.85e-12 |\n", + "| B.weight | (6,) | 1567.9 | 1.91e-11 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "总梯度坐标数: 45\n" + ] + } + ], + "source": [ + "from examples.experimental.optim_gradient_correctness.bidirectional import (\n", + " build as build_network, gradient_comparison, fit as fit_network, DT as NETWORK_DT,\n", + ")\n", + "with brainstate.environ.context(dt=NETWORK_DT, precision=64):\n", + " network_probe = build_network()\n", + " activity = brainstate.transform.jit(network_probe.rollout)()\n", + " comparison = gradient_comparison()\n", + "counts = np.asarray(activity['spike']).sum(axis=0)\n", + "assert np.all(counts >= 2), counts\n", + "display(Markdown('| 成员 | spike 数 | 最大接收电导 (uS) |\\n|---|---:|---:|\\n' + '\\n'.join(\n", + " f'| {name} | {counts[i]:g} | {np.asarray(activity[\"conductance\"])[:, i].max():.6g} |'\n", + " for i, name in enumerate(['A0', 'A1', 'B0', 'B1', 'B2']))))\n", + "display(Markdown('| 参数根 | 形状 | 最大绝对梯度 | BPTT/RTRL 最大绝对差 |\\n|---|---|---:|---:|\\n' + '\\n'.join(\n", + " f'| {name} | {gradient.shape} | {np.abs(gradient).max():.6g} | {comparison[\"errors\"][name]:.3g} |'\n", + " for name, gradient in comparison['gradients'].items())))\n", + "print('总梯度坐标数:', sum(value.size for value in comparison['gradients'].values()))\n" + ] + }, + { + "cell_type": "markdown", + "id": "bidirectional-training-scope", + "metadata": {}, + "source": [ + "### 联合拟合:BPTT 与完整 RTRL\n", + "\n", + "两个算法从相同的 15 个扰动参数开始,各训练 100 个 epoch。每次 rollout 内参数固定,epoch 之间更新。两边 channel、ion、synapse、threshold 和双向 weight 都交给优化器;是否唯一恢复参数不是验收条件。\n", + "\n", + "测试文件另外覆盖仅 A / 仅 B 损失、截断 event 反向但保留前向、共享参数梯度求和、不同延迟和投递后端、前缀梯度及跨 population / 队列敏感度。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "bidirectional-training", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-07T09:29:07.285495Z", + "iopub.status.busy": "2026-09-07T09:29:07.284882Z", + "iopub.status.idle": "2026-09-07T09:30:05.070184Z", + "shell.execute_reply": "2026-09-07T09:30:05.069324Z" + } + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "| 方法 | 初始 MSE | 最终 MSE | 最终/初始 |\n", + "|---|---:|---:|---:|\n", + "| bptt | 25.5107 | 0.104226 | 0.0040856 |\n", + "| rtrl | 25.5107 | 0.104226 | 0.0040856 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "| 参数根(物理坐标) | 初值 | BPTT 拟合 | RTRL 拟合 |\n", + "|---|---:|---:|---:|\n", + "| A.gmax | 0.98 | 1.02354 | 1.02354 |\n", + "| A.shift | 0.25 | 0.102729 | 0.102729 |\n", + "| A.ion | 0.98 | 1.00073 | 1.00073 |\n", + "| A.tau | 0.98 | 1.08462 | 1.08462 |\n", + "| A.reversal | 0.25 | 0.336268 | 0.336268 |\n", + "| A.threshold | 0.25 | 0.155479 | 0.155479 |\n", + "| A.weight | 0.98 | 1.07271 | 1.07271 |\n", + "| B.gmax | 1.02 | 1.01933 | 1.01933 |\n", + "| B.shift | 0.25 | 0.149915 | 0.149915 |\n", + "| B.ion | 1.02 | 0.989411 | 0.989411 |\n", + "| B.tau1 | 1.02 | 0.973852 | 0.973852 |\n", + "| B.tau2 | 1.02 | 1.01879 | 1.01879 |\n", + "| B.reversal | 0.25 | 0.332946 | 0.332946 |\n", + "| B.threshold | 0.25 | 0.0432725 | 0.0432725 |\n", + "| B.weight | 1.02 | 1.0633 | 1.0633 |" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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FsFHYwFcswUhwPAhgymrSAPgz6xztxmLwo6Sft51EACbZCUuwNdlBsCRJIlmfYai0bfiRGkIRWniNuO6cOLECTp06MCkSZOq/v7PR0JCAuPHj6/xfEsQXE2GewsAik/uVjiJIAj/5ooKhDExMaxfv/686T///DPR0dFXHepSbNq0iZkzZ/Lpp5+e91pKSgqPPfYYc+fOZfny5VRWVtZLJmd0ItsxGIMsmbBUlAKu2YLQy8uLYBzN0ovKvLDYZYrE4BJOoSglmdzfv2WYTzkAbi4y4uHEiRNxH3g9r5b34kiJnZKMk0pHEupATEwMv/32G3a7vdr00tJStmzZUm/7rPfff5833nijxtcOHjzIihUr+OqrrygtLa2XPIKD3e5o4RWkcxQIXfECGjhuM451q6DMnoe+0tGtSFHaMXGbsYI2HUx2/EVVhpu3v0uNKn3PmH7I2B2jGZ/MobIkX+lIDY6vry833HADo0ePrvr7Px/z5s3jhx9+YPHixUpHFYSrVhHYBgB19gGFkwiC8G+u6D7AhQsX8p///IdNmzbRrVs3ALZv387KlSv573//W5v5apSTk8PMmTMpLS3F19eX6dOnV7126NAhevfuzdChQ+nRowdvvfUWH3/8MX/99RdarbbOszmbnOJKwAOt2oos25FUajTunkrHuiI39evK8p0W1HYN6/M0TElPIji+m9KxGr0jR44AEBHgKDy7+QQqGeeSjR8/ntcPVKK2efPtGRM905Pwi2mldCyhlt1www0899xz9O3bl2nTphEUFERaWhorVqzA29ubwYMH13mG1157jUWLFmEymZg/f3611x577DFeeuklRowYwYkTJ7jrrrv4/fffadasWZ3nEkAlO/rmjTPISEho3V1vkJJz7ho9gJEjR1ISHgbd78JkLKSyOBd332ClozVKJ7OKAR/cVaW4+cQqHeeyDO7Vg3s+/Rt3AlmbLjE+LRG3hF5Kx2pQAgICWLhwIUVFRfTt25dx48YpHanOLF++nOXLl2Oz2ZSOIijELaojpIG/UbRsFwRnd0UtCOfOncsnn3zCrl27uO2227jtttvYvXs3q1at4tZbb63tjNXIssyNN97InXfeSWRk5HmvP/DAA3Tr1o0vvviCe++9lw0bNrBv3z4++uijOs3lrErONp701jlaz+g8fFzqKvY/Tbx2HMZyxy1Um7K0lOee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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "network_fits = []\n", + "with brainstate.environ.context(dt=NETWORK_DT, precision=64):\n", + " for method in ('bptt', 'rtrl'):\n", + " result = fit_network(method=method, epochs=100)\n", + " assert np.isfinite(result['history']).all()\n", + " assert result['final_mse'] < .1 * result['initial_mse']\n", + " network_fits.append(result)\n", + "display(Markdown('| 方法 | 初始 MSE | 最终 MSE | 最终/初始 |\\n|---|---:|---:|---:|\\n' + '\\n'.join(\n", + " f\"| {r['method']} | {r['initial_mse']:.6g} | {r['final_mse']:.6g} | {r['final_mse']/r['initial_mse']:.6g} |\" for r in network_fits)))\n", + "display(Markdown('| 参数根(物理坐标) | 初值 | BPTT 拟合 | RTRL 拟合 |\\n|---|---:|---:|---:|\\n' + '\\n'.join(\n", + " f'| {name} | {float(value):.6g} | {float(network_fits[0][\"fitted_roots\"][name]):.6g} | {float(network_fits[1][\"fitted_roots\"][name]):.6g} |'\n", + " for name, value in network_fits[0]['initial_roots'].items())))\n", + "fig, axes = plt.subplots(1, 3, figsize=(13, 3))\n", + "times = np.arange(network_fits[0]['target'].shape[0])*float(NETWORK_DT/u.ms)\n", + "for axis, member, title in [(axes[0], 0, 'A0'), (axes[1], 2, 'B0')]:\n", + " axis.plot(times, network_fits[0]['target'][:, member], color='black', label='Target')\n", + " axis.plot(times, network_fits[0]['initial'][:, member], color='#b06b38', alpha=.6, label='Initial')\n", + " for result, color in zip(network_fits, ['#138879', '#356aa0']):\n", + " axis.plot(times, result['fitted'][:, member], color=color, linestyle='--', label=result['method'])\n", + " axis.set(title=title, xlabel='Time (ms)', ylabel='Voltage (mV)')\n", + " axis.legend(fontsize=8)\n", + "for result in network_fits:\n", + " axes[2].semilogy(result['history'], label=result['method'])\n", + "axes[2].set(xlabel='Adam update', ylabel='Joint voltage MSE')\n", + "axes[2].legend()\n", + "fig.tight_layout()\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/multi_compartment/synapse_learning.py b/examples/multi_compartment/synapse_learning.py new file mode 100644 index 00000000..e77b00e3 --- /dev/null +++ b/examples/multi_compartment/synapse_learning.py @@ -0,0 +1,198 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Small single-parameter fits used by synapse_learning.ipynb.""" + +import braincell +import brainstate +import braintools +import brainunit as u +import jax.numpy as jnp +import numpy as np + +from braincell.filter import AllRegion, RootLocation + +DT = 0.025 * u.ms +STEPS = 240 + + +def build_cell(): + """Build one Hodgkin-Huxley CV with one ExpSyn and a short current pulse. + + Returns + ------- + braincell.Cell + Uninitialized spiking model. + """ + soma = braincell.Branch.from_lengths(lengths=[20.0] * u.um, radii=[10.0, 10.0] * u.um, type="soma") + cell = braincell.Cell( + braincell.Morphology.from_root(soma, name="soma"), pop_size=(1,), V_init=-65.0 * u.mV, V_th=-20.0 * u.mV + ) + cell.paint( + AllRegion(), + braincell.mech.CableProperty( + resting_potential=-65.0 * u.mV, + membrane_capacitance=1.0 * u.uF / u.cm**2, + axial_resistivity=100.0 * u.ohm * u.cm, + ), + braincell.mech.Ion("SodiumFixed", E=50.0 * u.mV), + braincell.mech.Ion("PotassiumFixed", E=-77.0 * u.mV), + braincell.mech.Channel("IL", name="leak"), + braincell.mech.Channel("Na_HH1952", name="na"), + braincell.mech.Channel("K_HH1952", name="k"), + ) + cell.place( + RootLocation(0.5), braincell.mech.CurrentClamp(delay=0.25 * u.ms, durations=1.0 * u.ms, amplitudes=0.1 * u.nA) + ) + cell.place(RootLocation(0.5), braincell.mech.Synapse("ExpSyn", name="syn", tau=2.0 * u.ms)) + return cell + + +def build_experiment(field, *, train=False): + """Create a fixed-input fit or an autaptic threshold fit. + + Parameters + ---------- + field : str + ``tau``, ``weight``, or ``threshold``. + train : bool, optional + Register exactly one scalar parameter at a perturbed initial value. + + Returns + ------- + tuple + Prepared Network and Cell. + """ + cell = build_cell() + source = ( + cell.event_outputs["spike"] + if field == "threshold" + else braincell.NetStim(start=1.0 * u.ms, number=1, interval=10.0 * u.ms) + ) + connection = braincell.connect( + "input", source=source, synapse=cell.synapses["syn"], weight=0.001 * u.uS, delay=0.1 * u.ms + ) + if train: + if field == "tau": + cell.synapses["syn"].trainable(tau=braincell.trainable.scale(brainstate.nn.Param(0.8), name="factor")) + elif field == "weight": + connection.trainable(weight=braincell.trainable.scale(brainstate.nn.Param(0.8), name="factor")) + elif field == "threshold": + source.trainable( + threshold=braincell.trainable.parameterized( + lambda ctx, delta: -20.0 * u.mV + delta * u.mV, delta=brainstate.nn.Param(-10.0) + ) + ) + else: + raise ValueError(field) + net = braincell.Network("fit") + net.add_population("cell", cell) + if field != "threshold": + net.add_population("input", source) + net.prepare_run(dt=DT, event_backend="scatter") + return net, cell + + +def simulate(net, cell): + """Reset and collect one differentiable, six-millisecond voltage trace. + + Parameters + ---------- + net : braincell.Network + Prepared execution target. + cell : braincell.Cell + Observed Cell. + + Returns + ------- + array + Voltage in mV, one scalar per time step. + """ + net.reset_state() + + def step(_): + net.update() + return cell.V.value.to_decimal(u.mV)[0, 0] + + return brainstate.transform.for_loop(step, jnp.arange(STEPS)) + + +def fit_one(field, *, epochs=100): + """Fit one parameter to one synthetic spiking voltage trace. + + Parameters + ---------- + field : str + Target field. + epochs : int, optional + Number of Adam updates. + + Returns + ------- + dict + Traces, losses, fitted root, and initial gradient. + """ + reference, reference_cell = build_experiment(field) + target = brainstate.transform.jit(lambda: simulate(reference, reference_cell))() + net, cell = build_experiment(field, train=True) + states = net.trainables.parameters().states() + assert len(states) == 1 + predict = brainstate.transform.jit(lambda: simulate(net, cell)) + initial = predict() + + def loss(): + return jnp.mean((simulate(net, cell) - target) ** 2) + + gradient = brainstate.transform.grad(loss, grad_states=states, return_value=True) + initial_gradient, _ = brainstate.transform.jit(gradient)() + optimizer = braintools.optim.Adam(lr=0.5 if field == "threshold" else 0.03) + optimizer.register_trainable_weights(states) + + @brainstate.transform.jit + def optimize(): + def epoch(_): + gradients, value = gradient() + optimizer.update(gradients) + return value + + return brainstate.transform.for_loop(epoch, jnp.arange(epochs)) + + history = optimize() + fitted = predict() + initial_mse = float(jnp.mean((initial - target) ** 2)) + final_mse = float(jnp.mean((fitted - target) ** 2)) + spikes = int(np.count_nonzero((np.asarray(target)[:-1] < 0.0) & (np.asarray(target)[1:] >= 0.0))) + assert spikes >= 1 + assert np.isfinite(np.asarray(history)).all() + assert final_mse < initial_mse * (1.0 if field == "threshold" else 0.1), (field, initial_mse, final_mse) + return dict( + field=field, + target=np.asarray(target), + initial=np.asarray(initial), + fitted=np.asarray(fitted), + history=np.asarray(history), + initial_mse=initial_mse, + final_mse=final_mse, + spikes=spikes, + initial_gradient=float(next(iter(initial_gradient.values()))), + fitted_root=float(next(iter(net.trainables.parameters().physical_values().values()))), + ) + + +if __name__ == "__main__": + with brainstate.environ.context(dt=DT): + for field in ("tau", "weight", "threshold"): + result = fit_one(field) + print({k: v for k, v in result.items() if not isinstance(v, np.ndarray)}, flush=True) diff --git a/examples/multi_compartment/synapse_learning_test.py b/examples/multi_compartment/synapse_learning_test.py new file mode 100644 index 00000000..ab9a7cc9 --- /dev/null +++ b/examples/multi_compartment/synapse_learning_test.py @@ -0,0 +1,34 @@ +# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""The notebook's one-parameter demonstrations must converge.""" + +import unittest + +import brainstate + +from examples.multi_compartment.synapse_learning import DT, fit_one + + +class SynapseLearningTest(unittest.TestCase): + def test_single_parameter_spiking_fits(self): + with brainstate.environ.context(dt=DT): + for field in ("tau", "weight", "threshold"): + with self.subTest(field=field): + result = fit_one(field) + self.assertNotEqual(result["initial_gradient"], 0.0) + self.assertGreaterEqual(result["spikes"], 1) + limit = 1.0 if field == "threshold" else 0.1 + self.assertLess(result["final_mse"], limit * result["initial_mse"]) diff --git a/examples/neuron_compare/README.md b/examples/neuron_compare/README.md index d74775ec..98c5309b 100644 --- a/examples/neuron_compare/README.md +++ b/examples/neuron_compare/README.md @@ -15,6 +15,9 @@ ## Typical Flow +小脑 ion、channel 和整细胞导入的范围、比较配置及未完成验证,统一见 +[Cerebellum Import Progress](cerebellum-import-progress.md)。 + 通常按下面的流程组织每个家族: 1. 准备输入配置。 diff --git a/examples/neuron_compare/cerebellum-import-progress.md b/examples/neuron_compare/cerebellum-import-progress.md new file mode 100644 index 00000000..53ec6ce9 --- /dev/null +++ b/examples/neuron_compare/cerebellum-import-progress.md @@ -0,0 +1,84 @@ +# Cerebellum Import Progress + +This record tracks concrete Cerebellum imports and NEURON comparisons across ion, channel, +and cell examples. It is not the reusable API contract or evidence of full numerical equivalence. +The source inventory and test summaries below come from the earlier import work; the documentation +move on 2026-09-07 did not rerun comparisons or advance their validation status. + +## Current status + +- Declarative KineticIon and its public pieces exist. Reusable species, conservation, and current-input + semantics are maintained in the [KineticIon contract](../../docs/design/ion/current/kinetic-ion.md). +- Concrete calcium pools are imported in [calcium.py](../../braincell/ion/calcium.py): + `CdpStC_MA2020_GoC`, `CdpStC_NoCAM_MA2020_GoC`, `CdpStC_CAMOnly_MA2020_GoC`, + `CdpStC_MA2025_BC`, `CdpStC_RI2021_SC`, `CdpCAM_MA2024_PC`, and `CdpCR_MA2020_GrC`. +- PC MA2024 channel imports and targeted tests span sodium, potassium, calcium, + calcium-activated potassium, and HCN modules. +- The [PC MA2024 scaffold](cell/pc_ma2024/pc.md) includes simplified NEURON and BrainCell + assemblies, shared parameters, debug variants, and a [comparison notebook](cell/pc_ma2024/run.ipynb). +- Spatial callable parameters are implemented; the former statement that paint required explicit + arrays or per-region calls is obsolete. Their current boundary is documented in + [Filter spatial parameters](../../docs/design/filter/current/spatial-callable-parameters.md). + +## Comparison entry points + +| Work | Location | +| --- | --- | +| Calcium-pool and ion comparisons | [Ion examples](ion/README.md) | +| Channel comparisons | [Channel examples](channel_no_conc/README.md) | +| Whole-cell comparisons | [Cell examples](cell/README.md) | +| PC MA2024 validation target | [PC comparison notebook](cell/pc_ma2024/run.ipynb) | +| Imported MOD provenance | [Cerebellum MOD sources](Cerebellum_mod/README.md) | + +## Comparison configuration + +For runs with current-driven calcium pools, use `cache_ion_total_current=True` together with +`ion_channel_update_order="family"`. Both are current Cell defaults; specifying them makes the +comparison configuration explicit. `"integration"` remains an alternative for controlled comparisons. +The timing and ownership of these options are defined by the +[Cell scheduling contract](../../docs/design/cell/current/architecture.md#离子电流快照与调度). + +The import work also covered same-name channels on disjoint soma/dendrite layouts and local PC +calcium-channel `_Frozen` variants that stop differentiation through the voltage in the current +expression. These are specific compatibility paths, not proof that the full model matches NEURON. + +## Implementation and test locations + +- [Ion template](../../braincell/ion/_base.py) and [template tests](../../braincell/ion/_base_test.py). +- [Ion lifecycle](../../braincell/_base_ion.py) and [lifecycle tests](../../braincell/_base_ion_test.py). +- [Cell runtime](../../braincell/_multi_compartment/cell.py) and [Cell tests](../../braincell/_multi_compartment/cell_test.py). +- [Ion construction](../../braincell/_compute/ions.py) and [ion tests](../../braincell/_compute/ions_test.py). +- [Runtime bindings](../../braincell/_compute/bindings.py) and [binding tests](../../braincell/_compute/bindings_test.py). +- [Staggered integration](../../braincell/quad/_staggered.py). +- Channel implementations and adjacent tests under [channel](../../braincell/channel/), including + sodium, potassium, calcium, potassium_calcium, and hyperpolarization_activated modules. + +The earlier record also described MechanismProbe support for plain-value fields and listed the old +probes implementation/tests. That is historical context, not a current API entry point; use the current +[recording API](../../docs/design/network/current/api.md) for supported observation interfaces. + +## Validation limits + +- The PC MA2024 scaffold remains a live validation target. Track numerical differences in the + notebooks before promoting it to a regression baseline. +- Not every Cerebellum MOD file has a BrainCell counterpart. The completed import focus was the + ion/channel subset needed by the PC and calcium-pool comparisons. +- These comparisons do not establish kinetic-ion equivalence for a future unified single mode. + SingleCompartment has its own update path; compatibility scope remains in the + [Single/MultiCompartment unification proposal](../../docs/design/cell/proposals/single-multi-compartment-unification.md). +- The original record reported targeted scheduling/runtime, calcium-ion, and PC channel tests. + It did not include a dated run manifest or complete tolerance table, and those results were not + rerun during this documentation move. + +## Next steps + +| Work | Status | Next action | +| --- | --- | --- | +| PC MA2024 whole-cell comparison | 实施中 | Tighten the notebook comparison and record remaining discrepancies | +| Automated regression baseline | 待讨论 | Establish expected tolerances before promoting stable comparisons into tests | +| Remaining Cerebellum imports | 待讨论 | Prioritize remaining MOD mechanisms after the PC scheduling path is stable | + +PC MA2024 remains the current end-to-end validation target for this import effort. +Module-level contracts and source attribution live in [Ion TODO](../../docs/design/ion/TODO.md), +[Channel TODO](../../docs/design/channel/TODO.md), and the shared +[Ion/Channel bibliography](../../docs/design/ion/references/ion-channel-bibliography.md).