Repository: NACLab/ngc-learn (language: python)

Issue:
Conv and deconv core feature written/tested
Integrate the generalized conv/deconv feature 


Discussion / hints:

Fix patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9391976d2..a10ea4e55 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,10 +15,17 @@ We encourage the community to make major types of contributions including:
 - **Bug fixes**: Address open issues and fix bugs presented in the `master` branch (or find and fix currently unknown bugs).
 - **Documentation fixes**: Improving the documentation is just as important as improving the library itself. If you find a typo in the documentation, or have made improvements, do not hesitate to send an [email](mailto:ago@cs.rit.edu) or preferably submit a GitHub pull request. Documentation can be found under the [docs](https://github.com/NACLab/ngc-learn/tree/master/docs) directory (**note** that you will need to have installed all of the items in the specific `requirements.txt` file in `/docs/` folder in order to build the Read the Docs documentation files).
 - **Model museum designs:** Implement new/classical predictive processing/coding and biomimetic (neural) models that appear in both modern/older publications or have been used in useful/interesting applications or analyses using ngc-learn's nodes and cables and integrate them into the Model Museum. Also [notify us](mailto:ago@cs.rit.edu) if you would like the NAC Laboratory to internally schedule integrating and including your published/used neurobiological model in the [Model Museum](https://ngc-learn.readthedocs.io/en/latest/museum.html).
-- **Extensions to the core engine:** Incorporate new [nodes](https://github.com/ago109/ngc-learn/tree/master/ngclearn/components/neurons), including new/missing functionalities/aspects of neural dynamics, spiking cell models / response kernels, error neuron simulations, dendritic tree calculations, and optimizations, or [cables](https://github.com/ago109/ngc-learn/tree/master/ngclearn/components/synapses), including new/missing synaptic transformations, such as complex or more neurobiologically-faithful transmission of (vector) signals, or synaptic update rules (neuromodulation, other variants of spike-time dependent plasticity, etc.).
+- **Extensions to the core engine:** Incorporate new [nodes](https://github.com/NACLab/ngc-learn/tree/master/ngclearn/components/neurons), including new/missing functionalities/aspects of neural dynamics, spiking cell models / response kernels, error neuron simulations, dendritic tree calculations, and optimizations, or [cables](https://github.com/NACLab/ngc-learn/tree/master/ngclearn/components/synapses), including new/missing synaptic transformations, such as complex or more neurobiologically-faithful transmission of (vector) signals, or synaptic update rules (neuromodulation, other variants of spike-time dependent plasticity, etc.).
 - **Additional assets:** Incorporate models (which would also go into the `Model Museum`) currently missing; for example, models/systems based on equilibrium propagation or competitive learning. This can include any missing functionality in the neural dynamics simulation that would support other aspects of neurobiological processing.
-- **New functionalities:** Implement new features (often placed in `ngclearn/utils/`) including, but not limited to, rendering/visualization tools, data generating processes to test/evaluate neural systems on, mathematical functions/distribution utilities helpful for analyzing or facilitating the calculations underlying simulated neural dynamics (such as other integration methods beyond Euler integration), and/or tests to further improve the library's completeness.
-- **New demonstrations/tutorials:** If you would like to contribute to ngc-learn's tutorials/demonstrations, we happily welcome informative contributions that explore different ways of using the library, including how a particular classical or landmark model or system can be built, simulated, and/or analyzed with ngc-learn. Please adhere to the style of the other tutorials (found under "Tutorials" in the [documentation](https://ngc-learn.readthedocs.io/en/latest/#)) to ensure uniformity.
+- **New functionalities:** Implement new features (often placed in `ngclearn/utils/`) including, but not limited to, 
+  rendering/visualization tools, data generating processes to test/evaluate neural systems on, mathematical 
+  functions/distribution utilities helpful for analyzing or facilitating the calculations underlying simulated neural 
+  dynamics (such as other integration methods not found in ngc-learn's backend), and/or tests to further improve the library's 
+  completeness.
+- **New demonstrations/tutorials:** If you would like to contribute to ngc-learn's 
+  tutorials/demonstrations, we happily welcome informative contributions that explore different ways of using the library, 
+  including how a particular classical or landmark model or system can be built, simulated, and/or analyzed with ngc-learn. 
+  Please adhere to the style of the other tutorials (found under "Tutorials" in the [documentation](https://ngc-learn.readthedocs.io/en/latest/#)) to ensure uniformity.
 
 Beyond the above items, there are many other ways to help. In particular answering queries on the issue tracker, <!--and reviewing other developers' pull requests are very valuable contributions that decrease the burden on the project maintainers.-->
 reporting issues that you are facing, and giving a "thumbs up" on issues that others report and that are relevant to you.
@@ -51,7 +58,7 @@ Submission
 ----------
 Please read the coding conventions below and make sure that your code is consistent with ours. When making a contribution, make a [pull request](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests)
 to ngc-learn with an itemized list of what you have done. When you submit a pull request, it is immensely helpful to include example script(s) that showcase the proposed changes and highlight any new APIs.
-We always love to see more test coverage. When it is appropriate, add a new test to accompany your contribution for checking the correctness of your code. <!-- to the [tests](https://github.com/ago109/ngc-learn/tree/master/tests) folder -->
+We always love to see more test coverage. When it is appropriate, add a new test to accompany your contribution for checking the correctness of your code. <!-- to the [tests](https://github.com/NACLab/ngc-learn/tree/master/tests) folder -->
 
 Coding Conventions
 ------------------
@@ -59,9 +66,6 @@ We value readability and adhere to the following coding conventions:
 - Indent using four spaces (soft tabs)
 - Always put spaces after list items and method parameters (e.g., `[1, 2, 3]` rather than `[1,2,3]`), and around operators and hash arrows (e.g., `x += 1` rather than `x+=1`)
 - You must use the [Google Python Style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for the docstrings
-<!--
-- For scripts such as in [examples](https://github.com/ago109/ngc-learn/tree/master/examples) and [tests](https://github.com/ago109/ngc-learn/tree/master/tests), please include a docstring at the top of the file that describes the high-level purpose of the script and/or instructions on how to use the scripts (if relevant).
--->
 
 We look forward to your contributions. Thank you!
 
diff --git a/docs/images/demo1/gncn_pdh_samples.png b/docs/images/demo1/gncn_pdh_samples.png
deleted file mode 100644
index f0713c7f8..000000000
Binary files a/docs/images/demo1/gncn_pdh_samples.png and /dev/null differ
diff --git a/docs/images/demo1/gncn_t1_samples.png b/docs/images/demo1/gncn_t1_samples.png
deleted file mode 100644
index d98e346e4..000000000
Binary files a/docs/images/demo1/gncn_t1_samples.png and /dev/null differ
diff --git a/docs/images/demo1/gncn_t1_sigma_samples.png b/docs/images/demo1/gncn_t1_sigma_samples.png
deleted file mode 100644
index 9a55d885e..000000000
Binary files a/docs/images/demo1/gncn_t1_sigma_samples.png and /dev/null differ
diff --git a/docs/images/demo2/dataset.jpg b/docs/images/demo2/dataset.jpg
deleted file mode 100644
index 2a416c143..000000000
Binary files a/docs/images/demo2/dataset.jpg and /dev/null differ
diff --git a/docs/images/demo2/model_fit.jpg b/docs/images/demo2/model_fit.jpg
deleted file mode 100644
index b06c36451..000000000
Binary files a/docs/images/demo2/model_fit.jpg and /dev/null differ
diff --git a/docs/images/demo2/model_latents.jpg b/docs/images/demo2/model_latents.jpg
deleted file mode 100644
index eab7b625b..000000000
Binary files a/docs/images/demo2/model_latents.jpg and /dev/null differ
diff --git a/docs/images/demo2/model_samples.jpg b/docs/images/demo2/model_samples.jpg
deleted file mode 100644
index 7fcc60cba..000000000
Binary files a/docs/images/demo2/model_samples.jpg and /dev/null differ
diff --git a/docs/images/demo2/ngclearn_nodes_and_cables.png b/docs/images/demo2/ngclearn_nodes_and_cables.png
deleted file mode 100644
index 0ca3f0dff..000000000
Binary files a/docs/images/demo2/ngclearn_nodes_and_cables.png and /dev/null differ
diff --git a/docs/images/demo2/tod_curve.jpg b/docs/images/demo2/tod_curve.jpg
deleted file mode 100644
index 1f907b165..000000000
Binary files a/docs/images/demo2/tod_curve.jpg and /dev/null differ
diff --git a/docs/images/demo3/mnist_learning_curves.jpg b/docs/images/demo3/mnist_learning_curves.jpg
deleted file mode 100644
index 30706a24e..000000000
Binary files a/docs/images/demo3/mnist_learning_curves.jpg and /dev/null differ
diff --git a/docs/images/demo3/ngclearn_demo3_arch.png b/docs/images/demo3/ngclearn_demo3_arch.png
deleted file mode 100644
index 3688a8611..000000000
Binary files a/docs/images/demo3/ngclearn_demo3_arch.png and /dev/null differ
diff --git a/docs/images/demo3/valid_acc_output.png b/docs/images/demo3/valid_acc_output.png
deleted file mode 100644
index 374d3d465..000000000
Binary files a/docs/images/demo3/valid_acc_output.png and /dev/null differ
diff --git a/docs/images/demo4/data_img1.png b/docs/images/demo4/data_img1.png
deleted file mode 100644
index f00e7df90..000000000
Binary files a/docs/images/demo4/data_img1.png and /dev/null differ
diff --git a/docs/images/demo4/data_img2.png b/docs/images/demo4/data_img2.png
deleted file mode 100644
index d63407baf..000000000
Binary files a/docs/images/demo4/data_img2.png and /dev/null differ
diff --git a/docs/images/demo4/face_img1.png b/docs/images/demo4/face_img1.png
deleted file mode 100644
index 3316c1f6b..000000000
Binary files a/docs/images/demo4/face_img1.png and /dev/null differ
diff --git a/docs/images/demo4/face_img2.png b/docs/images/demo4/face_img2.png
deleted file mode 100644
index 27fdf1f2d..000000000
Binary files a/docs/images/demo4/face_img2.png and /dev/null differ
diff --git a/docs/images/demo4/sc_cauchy_filters.jpg b/docs/images/demo4/sc_cauchy_filters.jpg
deleted file mode 100644
index 0bb962d17..000000000
Binary files a/docs/images/demo4/sc_cauchy_filters.jpg and /dev/null differ
diff --git a/docs/images/demo4/sc_ista_filters.jpg b/docs/images/demo4/sc_ista_filters.jpg
deleted file mode 100644
index 167568308..000000000
Binary files a/docs/images/demo4/sc_ista_filters.jpg and /dev/null differ
diff --git a/docs/images/demo4/sc_laplace_filters.jpg b/docs/images/demo4/sc_laplace_filters.jpg
deleted file mode 100644
index 96f704125..000000000
Binary files a/docs/images/demo4/sc_laplace_filters.jpg and /dev/null differ
diff --git a/docs/images/demo5/model_filters.jpg b/docs/images/demo5/model_filters.jpg
deleted file mode 100644
index 841b02b57..000000000
Binary files a/docs/images/demo5/model_filters.jpg and /dev/null differ
diff --git a/docs/images/demo5/recog_filters.jpg b/docs/images/demo5/recog_filters.jpg
deleted file mode 100644
index dbb3162b6..000000000
Binary files a/docs/images/demo5/recog_filters.jpg and /dev/null differ
diff --git a/docs/images/demo6/rbm_filters.jpg b/docs/images/demo6/rbm_filters.jpg
deleted file mode 100644
index 98a37d848..000000000
Binary files a/docs/images/demo6/rbm_filters.jpg and /dev/null differ
diff --git a/docs/images/demo6/rbm_samples1.png b/docs/images/demo6/rbm_samples1.png
deleted file mode 100644
index 196b96ab5..000000000
Binary files a/docs/images/demo6/rbm_samples1.png and /dev/null differ
diff --git a/docs/images/demo6/rbm_samples2.png b/docs/images/demo6/rbm_samples2.png
deleted file mode 100644
index a20d59c00..000000000
Binary files a/docs/images/demo6/rbm_samples2.png and /dev/null differ
diff --git a/docs/images/demo6/rbm_samples3.png b/docs/images/demo6/rbm_samples3.png
deleted file mode 100644
index 5e33629c5..000000000
Binary files a/docs/images/demo6/rbm_samples3.png and /dev/null differ
diff --git a/docs/images/demo7/input_raster_plot.png b/docs/images/demo7/input_raster_plot.png
deleted file mode 100644
index 3dfb432c6..000000000
Binary files a/docs/images/demo7/input_raster_plot.png and /dev/null differ
diff --git a/docs/images/demo7/lif_analysis.png b/docs/images/demo7/lif_analysis.png
deleted file mode 100644
index 284cfeb69..000000000
Binary files a/docs/images/demo7/lif_analysis.png and /dev/null differ
diff --git a/docs/images/demo7/lif_ref_analysis.png b/docs/images/demo7/lif_ref_analysis.png
deleted file mode 100644
index e0e691fc0..000000000
Binary files a/docs/images/demo7/lif_ref_analysis.png and /dev/null differ
diff --git a/docs/images/demo7/mnist_learning_curves.png b/docs/images/demo7/mnist_learning_curves.png
deleted file mode 100644
index a628a24e8..000000000
Binary files a/docs/images/demo7/mnist_learning_curves.png and /dev/null differ
diff --git a/docs/modeling/synapses.md b/docs/modeling/synapses.md
index 5654d0e37..6d4a9b88a 100644
--- a/docs/modeling/synapses.md
+++ b/docs/modeling/synapses.md
@@ -3,14 +3,15 @@
 The synapse is a key building blocks for connecting/wiring together the various
 component cells that one would use for characterizing a biomimetic neural system.
 These particular objects are meant to perform, per simulated time step, a
-specific type of transformation utilizing their underlying synaptic parameters.
-Most times, a synaptic cable will be represented by a set of matrices that
-used to conduct a projection of an input signal (a value presented to its
+specific type of transformation -- such as a linear transform or a 
+convolution -- utilizing their underlying synaptic parameters.
+Most times, a synaptic cable will be represented by a set of matrices (or filters) 
+that are used to conduct a projection of an input signal (a value presented to a
 pre-synaptic/input compartment) resulting in an output signal (a value that
-appears within its post-synaptic compartment). Notably, a synapse component is
+appears within one of its post-synaptic compartments). Notably, a synapse component is
 typically associated with a local plasticity rule, e.g., a Hebbian-type
-update, that either is either triggered online (at some or all simulation time
-steps) or by integrating a differential equation, e.g., via eligibility traces.
+update, that either is triggered online, i.e., at some or all simulation time
+steps, or by integrating a differential equation, e.g., via eligibility traces.
 
 ## Non-Plastic Synapse Types
 
@@ -30,20 +31,53 @@ used for fixed value (dense) synaptic connections.
     :noindex:
 ```
 
-## Simple Factor Learning Synapse Types
+### Static Convolutional Synapse
+
+This synapse performs a convolutional transform of its input signals.
+Note that this synaptic cable does not evolve and is meant to be 
+used for fixed value convolution synaptic filters.
+
+```{eval-rst}
+.. autoclass:: ngclearn.components.ConvSynapse
+  :noindex:
+
+  .. automethod:: advance_state
+    :noindex:
+  .. automethod:: reset
+    :noindex:
+```
+
+### Static Deconvolutional Synapse
+
+This synapse performs a deconvolutional transform of its input signals.
+Note that this synaptic cable does not evolve and is meant to be 
+used for fixed value deconvolution/transposed convolution synaptic filters.
+
+```{eval-rst}
+.. autoclass:: ngclearn.components.DeconvSynapse
+  :noindex:
+
+  .. automethod:: advance_state
+    :noindex:
+  .. automethod:: reset
+    :noindex:
+```
+
+## Multi-Factor Learning Synapse Types
 
 Hebbian rules operate in a local manner -- they generally use information more
 immediately available to synapses in both space and time -- and can come in a
 wide variety of flavors. One general way to categorize variants of Hebbian learning
 is to clarify what (neural) statistics they operate on, e.g, do they work with
 real-valued information or discrete spikes, and how many factors (or distinct
-terms) are involved in calculating the rule. (Note that, in principle, all
-forms of plasticity in ngc-learn are technically local, factor-based rules. )
+terms) are involved in calculating the update to synaptic values by the 
+relevant learning rule. <!--(Note that, in principle, all forms of plasticity in 
+ngc-learn technically work like local, factor-based rules. )-->
 
 ### (Two-Factor) Hebbian Synapse
 
 This synapse performs a linear transform of its input signals and evolves
-according to a simple, strictly two-factor update rule. In other words, the
+according to a strictly two-factor update rule. In other words, the
 underlying synaptic efficacy matrix is changed according to a product between
 pre-synaptic compartment values (`pre`) and post-synaptic compartment (`post`)
 values, which can contain any type of vector/matrix statistics.
@@ -63,10 +97,10 @@ values, which can contain any type of vector/matrix statistics.
 ### (Two-Factor) BCM Synapse
 
 This synapse performs a linear transform of its input signals and evolves
-according to multi-factor Bienenstock-Cooper-Munro (BCM) update rule. The
+according to a multi-factor, Bienenstock-Cooper-Munro (BCM) update rule. The
 underlying synaptic efficacy matrix is changed according to an evolved 
 synaptic threshold parameter `theta` and a product between
-pre-synaptic compartment values (`pre`) and nonlinear function of post-synaptic 
+pre-synaptic compartment values (`pre`) and a nonlinear function of post-synaptic 
 compartment (`post`) values, which can contain any type of vector/matrix 
 statistics.
 
@@ -82,14 +116,53 @@ statistics.
     :noindex:
 ```
 
+### (Two-Factor) Hebbian Convolutional Synapse
+
+This synapse performs a convolutional transform of its input signals and evolves
+according to a two-factor update rule. The underlying synaptic filters are 
+changed according to products between pre-synaptic compartment values (`pre`) 
+and post-synaptic compartment (`post`)  feature map values.
+
+```{eval-rst}
+.. autoclass:: ngclearn.components.HebbianConvSynapse
+  :noindex:
+
+  .. automethod:: advance_state
+    :noindex:
+  .. automethod:: evolve
+    :noindex:
+  .. automethod:: reset
+    :noindex:
+```
+
+### (Two-Factor) Hebbian Deconvolutional Synapse
+
+This synapse performs a deconvolutional (transposed convolutional) transform of 
+its input signals and evolves according to a two-factor update rule. The 
+underlying synaptic filters are changed according to products between 
+pre-synaptic compartment values (`pre`) and post-synaptic compartment (`post`) 
+feature map values.
+
+```{eval-rst}
+.. autoclass:: ngclearn.components.HebbianDeconvSynapse
+  :noindex:
+
+  .. automethod:: advance_state
+    :noindex:
+  .. automethod:: evolve
+    :noindex:
+  .. automethod:: reset
+    :noindex:
+```
+
 ## Spike-Timing-Dependent Plasticity (STDP) Synapse Types
 
-Synapse that evolve according to a spike-timing-dependent plasticity (STDP)
+Synapses that evolve according to a spike-timing-dependent plasticity (STDP)
 process operate, at a high level, much like multi-factor Hebbian rules (given
 that STDP is a generalization of Hebbian adjustment to spike trains) and share
 many of their properties. Nevertheless, a distinguishing factor for STDP-based
-synapses is that they must involve action potentials (spikes) in their
-calculations and are typically computing synaptic change according to the
+synapses is that they must involve action potential pulses (spikes) in their
+calculations and they typically compute synaptic change according to the
 relative timing of spikes. In principle, any of the synapses in this grouping
 of components adapt their efficacies according to rules that are at least special
 four-factor terms, i.e., a pre-synaptic spike (an "event"), a pre-synaptic delta
@@ -97,7 +170,7 @@ timing (which can come in the form of a trace), a post-synaptic spike (or event)
 and a post-synaptic delta timing (also can be a trace). In addition, STDP rules
 in ngc-learn typically enforce soft/hard synaptic strength bounding, i.e., there
 is a maximum magnitude allowed for any single synaptic efficacy, and, by default,
-enforce that synaptic strengths are non-negative.
+an STDP synapse enforces that its synaptic strengths are non-negative.
 
 ### Trace-based STDP
 
@@ -141,9 +214,10 @@ described above.
     :noindex:
 ```
 
-### Event-Driven Post-Synaptic STDP
+### Event-Driven Post-Synaptic STDP Synapse
 
-This is a two-factor STDP rule that is driven by only spike events. 
+This is a synaptic evolved under a two-factor STDP rule that is driven by 
+only spike events. 
 
 
 ```{eval-rst}
@@ -157,3 +231,45 @@ This is a two-factor STDP rule that is driven by only spike events.
   .. automethod:: reset
     :noindex:
 ```
+
+### Trace-based STDP Convolutional Synapse
+
+This is a four-factor STDP rule for convolutional synapses that adjusts the 
+underlying filters via a weighted combination of long-term depression (LTD) and 
+long-term potentiation (LTP). For the LTP portion of the update, a pre-synaptic 
+trace and a post-synaptic event/spike-trigger are used, and for the LTD portion 
+of the update, a pre-synaptic event/spike-trigger and a post-synaptic trace are
+utilized. 
+
+```{eval-rst}
+.. autoclass:: ngclearn.components.TraceSTDPConvSynapse
+  :noindex:
+
+  .. automethod:: advance_state
+    :noindex:
+  .. automethod:: evolve
+    :noindex:
+  .. automethod:: reset
+    :noindex:
+```
+
+### Trace-based STDP Deonvolutional Synapse
+
+This is a four-factor STDP rule for deconvolutional (transposed convolutional) 
+synapses that adjusts the underlying filters via a weighted combination of
+long-term depression (LTD) and long-term potentiation (LTP). For the LTP portion 
+of the update, a pre-synaptic trace and a post-synaptic event/spike-trigger are 
+used, and for the LTD portion of the update, a pre-synaptic event/spike-trigger 
+and a post-synaptic trace are utilized. 
+
+```{eval-rst}
+.. autoclass:: ngclearn.components.TraceSTDPDeconvSynapse
+  :noindex:
+
+  .. automethod:: advance_state
+    :noindex:
+  .. automethod:: evolve
+    :noindex:
+  .. automethod:: reset
+    :noindex:
+```
diff --git a/docs/source/ngclearn.components.lava.rst b/docs/source/ngclearn.components.lava.rst
index a5879b915..6b8be4264 100644
--- a/docs/source/ngclearn.components.lava.rst
+++ b/docs/source/ngclearn.components.lava.rst
@@ -11,6 +11,17 @@ Subpackages
    ngclearn.components.lava.synapses
    ngclearn.components.lava.traces
 
+Submodules
+----------
+
+ngclearn.components.lava.monitor module
+---------------------------------------
+
+.. automodule:: ngclearn.components.lava.monitor
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 
diff --git a/docs/source/ngclearn.components.rst b/docs/source/ngclearn.components.rst
index 2f08b3fa8..e32097829 100644
--- a/docs/source/ngclearn.components.rst
+++ b/docs/source/ngclearn.components.rst
@@ -16,6 +16,14 @@ Subpackages
 Submodules
 ----------
 
+ngclearn.components.base\_monitor module
+----------------------------------------
+
+.. automodule:: ngclearn.components.base_monitor
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 ngclearn.components.jaxComponent module
 ---------------------------------------
 
@@ -24,6 +32,14 @@ ngclearn.components.jaxComponent module
    :undoc-members:
    :show-inheritance:
 
+ngclearn.components.monitor module
+----------------------------------
+
+.. automodule:: ngclearn.components.monitor
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 
diff --git a/docs/source/ngclearn.components.synapses.convolution.rst b/docs/source/ngclearn.components.synapses.convolution.rst
new file mode 100644
index 000000000..0587f6292
--- /dev/null
+++ b/docs/source/ngclearn.components.synapses.convolution.rst
@@ -0,0 +1,85 @@
+ngclearn.components.synapses.convolution package
+================================================
+
+Submodules
+----------
+
+ngclearn.components.synapses.convolution.convSynapse module
+-----------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.convSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.deconvSynapse module
+-------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.deconvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.hebbianConvSynapse module
+------------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.hebbianConvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.hebbianDeconvSynapse module
+--------------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.hebbianDeconvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.ngcconv module
+-------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.ngcconv
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.staticConvSynapse module
+-----------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.staticConvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.staticDeconvSynapse module
+-------------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.staticDeconvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.traceSTDPConvSynapse module
+--------------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.traceSTDPConvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ngclearn.components.synapses.convolution.traceSTDPDeconvSynapse module
+----------------------------------------------------------------------
+
+.. automodule:: ngclearn.components.synapses.convolution.traceSTDPDeconvSynapse
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: ngclearn.components.synapses.convolution
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/ngclearn.components.synapses.rst b/docs/source/ngclearn.components.synapses.rst
index e1c470665..8da0b089e 100644
--- a/docs/source/ngclearn.components.synapses.rst
+++ b/docs/source/ngclearn.components.synapses.rst
@@ -7,6 +7,7 @@ Subpackages
 .. toctree::
    :maxdepth: 4
 
+   ngclearn.components.synapses.convolution
    ngclearn.components.synapses.hebbian
 
 Submodules
diff --git a/docs/tutorials/foundations/monitors.md b/docs/tutorials/foundations/monitors.md
index bc57849dd..f835a59df 100644
--- a/docs/tutorials/foundations/monitors.md
+++ b/docs/tutorials/foundations/monitors.md
@@ -1,8 +1,8 @@
 # NGC Monitors
 
-Ngc-monitors are a way of storing a rolling window of compartment values
-automatically. Their intended purpose is not to be used inside of a model but 
-just as an auxiliary way to view the internal state of the module even when it is 
+NGC-monitors are a way of storing a rolling window of compartment values
+automatically. Their intended purpose is not to be used "inside" of a model but 
+just as an auxiliary way to view the internal state of the model even when it is 
 compiled. A monitor will track the last `n` values it has observed within the
 compartment with the oldest value being at `index=0` and the newest being at
 `index=n-1`.
@@ -10,8 +10,8 @@ compartment with the oldest value being at `index=0` and the newest being at
 ## Building a Monitor
 
 Monitors are constructed exactly like regular components are for general models. 
-Simply import the monitor `from ngclearn.components import Monitor`. Now, 
-inside of your model, build it like a regular component.
+To use one, simply import the monitor `from ngclearn.components import Monitor`. Now, 
+inside of your model, build it like a regular component:
 
 ```python
 with Context("model") as model:
diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst
index 0b3618a34..458e57e2c 100644
--- a/docs/tutorials/index.rst
+++ b/docs/tutorials/index.rst
@@ -34,6 +34,7 @@ to configuration).
   foundations/contexts
   foundations/commands
   foundations/operations
+  foundations/monitors
 
 .. toctree::
   :maxdepth: 1
@@ -44,3 +45,4 @@ to configuration).
   lava/lava_components
   lava/lava_context
   lava/hebbian_learning
+  lava/monitors
diff --git a/docs/tutorials/model_basics/evolving_synapses.md b/docs/tutorials/model_basics/evolving_synapses.md
index 8f65d6b4e..fa1478d4b 100755
--- a/docs/tutorials/model_basics/evolving_synapses.md
+++ b/docs/tutorials/model_basics/evolving_synapses.md
@@ -17,14 +17,11 @@ compute a simple Hebbian adjustment.
 We do this specifically as follows:
 
 ```python
+from jax import numpy as jnp, random, jit
 from ngcsimlib.compilers import compile_command, wrap_command
 from ngcsimlib.context import Context
-from ngcsimlib.commands import Command
-
 from ngclearn.components import HebbianSynapse, RateCell
-
-from jax import numpy as jnp, random, jit
-import numpy as np
+import ngclearn.utils.weight_distribution as dist
 
 ## create seeding keys
 dkey = random.PRNGKey(1234)
@@ -38,7 +35,7 @@ with Context("Circuit") as circuit:
                act_fx="identity", key=subkeys[1])
 
   Wab = HebbianSynapse(name="Wab", shape=(1, 1), eta=1.,
-                       sign_value=-1., weight_init=("constant", 1., None),
+                       sign_value=-1., weight_init=dist.constant(value=1.),
                        w_bound=0., key=subkeys[3])
 
   # wire output compartment (rate-coded output zF) of RateCell `a` to input compartment of HebbianSynapse `Wab`
diff --git a/docs/tutorials/model_basics/model_building.md b/docs/tutorials/model_basics/model_building.md
index 827468a27..981cf9b56 100755
--- a/docs/tutorials/model_basics/model_building.md
+++ b/docs/tutorials/model_basics/model_building.md
@@ -8,10 +8,11 @@ two simple graded cells that are connected by one synaptic cable.
 While building our dynamical system we will set up a Context and then add the three different components to it.
 
 ```python
+from jax import numpy as jnp, random
 from ngclearn import Context
-from ngclearn.components import RateCell, HebbianSynapse
 from ngclearn.commands import Reset, Clamp, AdvanceState
-from jax import numpy as jnp, random
+from ngclearn.components import RateCell, HebbianSynapse
+import ngclearn.utils.weight_distribution as dist
 
 ## create seeding keys
 dkey = random.PRNGKey(1234)
@@ -24,7 +25,7 @@ with Context("model") as model:
    b = RateCell(name="b", n_units=1, tau_m=20.,
                 act_fx="identity", key=subkeys[1])
    Wab = HebbianSynapse(name="Wab", shape=(1, 1),
-                        weight_init=("constant", 1., None), key=subkeys[2])
+                        weight_init=dist.constant(value=1.), key=subkeys[2])
 ```
 
 Next, we will want to wire together the three components we have embedded into
diff --git a/docs/tutorials/neurocog/stdp.md b/docs/tutorials/neurocog/stdp.md
index c735cfc06..73c33f7b3 100755
--- a/docs/tutorials/neurocog/stdp.md
+++ b/docs/tutorials/neurocog/stdp.md
@@ -29,14 +29,12 @@ Writing the above 3-component system can be in the following manner:
 
 ```python
 from jax import numpy as jnp, random, jit
-import time
-
 from ngcsimlib.context import Context
-from ngcsimlib.commands import Command
 from ngcsimlib.compilers import compile_command, wrap_command
 ## import model-specific mechanisms
 from ngclearn.components.other.varTrace import VarTrace
 from ngclearn.components.synapses.hebbian.traceSTDPSynapse import TraceSTDPSynapse
+import ngclearn.utils.weight_distribution as dist
 
 ## create seeding keys (JAX-style)
 dkey = random.PRNGKey(231)
@@ -49,7 +47,7 @@ with Context("Model") as model:
     tr0 = VarTrace("tr0", n_units=1, tau_tr=8., a_delta=1.)
     tr1 = VarTrace("tr1", n_units=1, tau_tr=8., a_delta=1.)
     W = TraceSTDPSynapse("W1", shape=(1, 1), eta=0., A_plus=1., A_minus=0.8,
-                         wInit=("uniform", 0.0, 0.3), key=subkeys[0])
+                         weight_init=dist.uniform(0.0, 0.3), key=subkeys[0])
 
     # wire only relevant compartments to synaptic cable W for demo purposes
     W.preTrace << tr0.trace
diff --git a/ngclearn/components/__init__.py b/ngclearn/components/__init__.py
index d1bcc40e5..518001c24 100644
--- a/ngclearn/components/__init__.py
+++ b/ngclearn/components/__init__.py
@@ -26,5 +26,14 @@
 from .synapses.hebbian.expSTDPSynapse import ExpSTDPSynapse
 from .synapses.hebbian.eventSTDPSynapse import EventSTDPSynapse
 from .synapses.hebbian.BCMSynapse import BCMSynapse
-
+## point to convolutional component types
+from .synapses.convolution.convSynapse import ConvSynapse
+from .synapses.convolution.staticConvSynapse import StaticConvSynapse
+from .synapses.convolution.hebbianConvSynapse import HebbianConvSynapse
+from .synapses.convolution.traceSTDPConvSynapse import TraceSTDPConvSynapse
+from .synapses.convolution.deconvSynapse import DeconvSynapse
+from .synapses.convolution.staticDeconvSynapse import StaticDeconvSynapse
+from .synapses.convolution.hebbianDeconvSynapse import HebbianDeconvSynapse
+from .synapses.convolution.traceSTDPDeconvSynapse import TraceSTDPDeconvSynapse
+## point to monitors
 from .monitor import Monitor
diff --git a/ngclearn/components/base_monitor.py b/ngclearn/components/base_monitor.py
index 79b2c5350..a97a796ff 100644
--- a/ngclearn/components/base_monitor.py
+++ b/ngclearn/components/base_monitor.py
@@ -8,8 +8,9 @@
 
 class Base_Monitor(Component):
     """
-    An abstract base for monitors for both ngclearn and ngclava. Compartments wired directly into this component will
-    have their value tracked during `advance_state` loops automatically.
+    An abstract base for monitors for both ngclearn and ngclava. Compartments
+    wired directly into this component will have their value tracked during
+    `advance_state` loops automatically.
 
     Note the monitor only works for compiled methods currently
 
diff --git a/ngclearn/components/input_encoders/bernoulliCell.py b/ngclearn/components/input_encoders/bernoulliCell.py
index 92800fe94..dc32b6f8b 100755
--- a/ngclearn/components/input_encoders/bernoulliCell.py
+++ b/ngclearn/components/input_encoders/bernoulliCell.py
@@ -68,7 +68,7 @@ def __init__(self, name, n_units, **kwargs):
         self.tols = Compartment(restVals) # time of last spike
 
     @staticmethod
-    def _advance_state(t, dt, key, inputs, tols):
+    def _advance_state(t, key, inputs, tols):
         key, *subkeys = random.split(key, 2)
         outputs = sample_bernoulli(subkeys[0], data=inputs)
         timeOfLastSpike = update_times(t, outputs, tols)
@@ -102,7 +102,7 @@ def load(self, directory, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "BernoulliCell - samples input to produce spikes, "
+            "cell_type": "BernoulliCell - samples input to produce spikes, "
                           "where dimension is a probability proportional to "
                           "the dimension's magnitude/value/intensity"
         }
@@ -110,7 +110,7 @@ def help(self): ## component help function
             "input_compartments":
                 {"inputs": "Takes in external input signal values",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"tols": "Time-of-last-spike",
                  "outputs": "Binary spike values emitted at time t"},
         }
diff --git a/ngclearn/components/input_encoders/latencyCell.py b/ngclearn/components/input_encoders/latencyCell.py
index 810c8bc61..1ca770a49 100755
--- a/ngclearn/components/input_encoders/latencyCell.py
+++ b/ngclearn/components/input_encoders/latencyCell.py
@@ -177,8 +177,8 @@ def __init__(self, name, n_units, tau=1., threshold=0.01, first_spike_time=0.,
         #self.reset()
 
     @staticmethod
-    def _calc_spike_times(t, dt, linearize, tau, threshold, first_spike_time,
-        num_steps, normalize, inputs):
+    def _calc_spike_times(linearize, tau, threshold, first_spike_time, num_steps,
+                          normalize, inputs):
         ## would call this function before processing a spike train (at start)
         data = inputs
         if linearize == True: ## linearize spike time calculation
@@ -237,7 +237,7 @@ def load(self, directory, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "LatencyCell - samples input to produce spikes via latency "
+            "cell_type": "LatencyCell - samples input to produce spikes via latency "
                          "coding, where each dimension's magnitude determines how "
                          "early in the spike train a value occurs. This is a "
                          "temporal/order encoder."
@@ -246,7 +246,7 @@ def help(self): ## component help function
             "input_compartments":
                 {"inputs": "Takes in external input signal values",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"tols": "Time-of-last-spike",
                  "outputs": "Binary spike values emitted at time t",
                  "mask": "Spike ordering mask",
diff --git a/ngclearn/components/input_encoders/poissonCell.py b/ngclearn/components/input_encoders/poissonCell.py
index c12d264c7..a62ae15fb 100644
--- a/ngclearn/components/input_encoders/poissonCell.py
+++ b/ngclearn/components/input_encoders/poissonCell.py
@@ -114,7 +114,7 @@ def load(self, directory, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "PoissonCell - samples input to produce spikes, "
+            "cell_type": "PoissonCell - samples input to produce spikes, "
                           "where dimension is a probability proportional to "
                           "the dimension's magnitude/value/intensity and "
                          "constrained by a maximum spike frequency (spikes follow "
@@ -124,7 +124,7 @@ def help(self): ## component help function
             "input_compartments":
                 {"inputs": "Takes in external input signal values",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"tols": "Time-of-last-spike",
                  "outputs": "Binary spike values emitted at time t"},
         }
diff --git a/ngclearn/components/lava/neurons/LIFCell.py b/ngclearn/components/lava/neurons/LIFCell.py
index 31524ae1e..7fb672901 100644
--- a/ngclearn/components/lava/neurons/LIFCell.py
+++ b/ngclearn/components/lava/neurons/LIFCell.py
@@ -115,7 +115,7 @@ def _init(self, thr_theta0):
 
     @staticmethod
     def _advance_state(dt, tau_m, R_m, v_rest, v_reset, v_decay, refract_T, thr, tau_theta,
-                       theta_plus, j_exc, j_inh, v, s, rfr, thr_theta): #, tols):
+                       theta_plus, j_exc, j_inh, v, s, rfr, thr_theta):
         #j = j * (tau_m/dt) ## scale electrical current
         j = j_exc - j_inh ## sum the excitatory and inhibitory input channels
         mask = (rfr >= refract_T) * 1. #numpy.greater_equal(rfr, refract_T) * 1.
diff --git a/ngclearn/components/monitor.py b/ngclearn/components/monitor.py
index 595ab69aa..e919e535d 100644
--- a/ngclearn/components/monitor.py
+++ b/ngclearn/components/monitor.py
@@ -2,7 +2,8 @@
 
 class Monitor(Base_Monitor):
     """
-    A jax implementation of `Base_Monitor`. Designed to be used with all non-lava ngclearn components
+    A jax implementation of `Base_Monitor`. Designed to be used with all
+    non-lava ngclearn components
     """
     @staticmethod
     def build_advance(compartments):
diff --git a/ngclearn/components/neurons/graded/gaussianErrorCell.py b/ngclearn/components/neurons/graded/gaussianErrorCell.py
index dd3949c1e..07309fa95 100755
--- a/ngclearn/components/neurons/graded/gaussianErrorCell.py
+++ b/ngclearn/components/neurons/graded/gaussianErrorCell.py
@@ -85,7 +85,7 @@ def __init__(self, name, n_units, **kwargs):
         self.modulator = Compartment(restVals + 1.0) # to be set/consumed
 
     @staticmethod
-    def _advance_state(t, dt, mu, dmu, target, dtarget, modulator):
+    def _advance_state(dt, mu, dmu, target, dtarget, modulator):
         ## compute Gaussian error cell output
         dmu, dtarget, L = run_cell(dt, target, mu)
         dmu = dmu * modulator
@@ -119,7 +119,7 @@ def reset(self, dmu, dtarget, target, mu, modulator, L):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "GaussianErrorcell - computes mismatch/error signals at "
+            "cell_type": "GaussianErrorcell - computes mismatch/error signals at "
                          "each time step t (between a `target` and a prediction `mu`)"
         }
         compartment_props = {
@@ -127,7 +127,7 @@ def help(self): ## component help function
                 {"mu": "External input prediction value(s)",
                  "target": "External input target signal value(s)",
                  "modulator": "External input modulatory/scaling signal(s)"},
-            "outputs_compartments":
+            "output_compartments":
                 {"L": "Local loss value computed/embodied by this error-cell",
                  "dmu": "first derivative of loss w.r.t. prediction value(s)",
                  "dtarget": "first derivative of loss w.r.t. target value(s)"},
diff --git a/ngclearn/components/neurons/graded/laplacianErrorCell.py b/ngclearn/components/neurons/graded/laplacianErrorCell.py
index 5a0ef614e..97e817ad9 100755
--- a/ngclearn/components/neurons/graded/laplacianErrorCell.py
+++ b/ngclearn/components/neurons/graded/laplacianErrorCell.py
@@ -87,7 +87,7 @@ def __init__(self, name, n_units, **kwargs):
         self.modulator = Compartment(restVals + 1.0) # to be set/consumed
 
     @staticmethod
-    def _advance_state(t, dt, mu, dmu, target, dtarget, modulator):
+    def _advance_state(dt, mu, target, modulator):
         ## compute Laplacian error cell output
         dmu, dtarget, L = run_cell(dt, target, mu)
         dmu = dmu * modulator
@@ -121,7 +121,7 @@ def reset(self, dmu, dtarget, target, mu, modulator, L):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "LaplacianErrorcell - computes mismatch/error signals at "
+            "cell_type": "LaplacianErrorcell - computes mismatch/error signals at "
                          "each time step t (between a `target` and a prediction `mu`)"
         }
         compartment_props = {
@@ -129,7 +129,7 @@ def help(self): ## component help function
                 {"mu": "External input prediction value(s)",
                  "target": "External input target signal value(s)",
                  "modulator": "External input modulatory/scaling signal(s)"},
-            "outputs_compartments":
+            "output_compartments":
                 {"L": "Local loss value computed/embodied by this error-cell",
                  "dmu": "first derivative of loss w.r.t. prediction value(s)",
                  "dtarget": "first derivative of loss w.r.t. target value(s)"},
diff --git a/ngclearn/components/neurons/graded/rateCell.py b/ngclearn/components/neurons/graded/rateCell.py
index fb4e37ae4..e04be8dd1 100755
--- a/ngclearn/components/neurons/graded/rateCell.py
+++ b/ngclearn/components/neurons/graded/rateCell.py
@@ -1,6 +1,5 @@
 from jax import numpy as jnp, random, jit
 from functools import partial
-import time, math
 from ngclearn.utils import tensorstats
 from ngclearn import resolver, Component, Compartment
 from ngclearn.components.jaxComponent import JaxComponent
@@ -172,8 +171,8 @@ def __init__(self, name, n_units, tau_m, prior=("gaussian", 0.), act_fx="identit
         self.z = Compartment(restVals) # rate activity
 
     @staticmethod
-    def _advance_state(t, dt, fx, dfx, tau_m, priorLeakRate, intgFlag, priorType,
-                       thresholdType, thr_lmbda, j, j_td, z, zF):
+    def _advance_state(dt, fx, dfx, tau_m, priorLeakRate, intgFlag, priorType,
+                       thresholdType, thr_lmbda, j, j_td, z):
         if tau_m > 0.:
             ### run a step of integration over neuronal dynamics
             ## Notes:
@@ -217,7 +216,7 @@ def reset(self, j, zF, j_td, z):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "RateCell - evolves neurons according to rate-coded/"
+            "cell_type": "RateCell - evolves neurons according to rate-coded/"
                          "continuous dynamics "
         }
         compartment_props = {
@@ -225,7 +224,7 @@ def help(self): ## component help function
                 {"j": "External input stimulus value(s)",
                  "j_td": "External top-down input stimulus value(s); these get "
                          "multiplied by the derivative of f(x), i.e., df(x)"},
-            "outputs_compartments":
+            "output_compartments":
                 {"z": "Update to rate-coded continuous dynamics; value at time t",
                  "zF": "Nonlinearity/function applied to rate-coded dynamics; f(z)"},
         }
diff --git a/ngclearn/components/neurons/spiking/LIFCell.py b/ngclearn/components/neurons/spiking/LIFCell.py
index fd792a49e..0eee98124 100644
--- a/ngclearn/components/neurons/spiking/LIFCell.py
+++ b/ngclearn/components/neurons/spiking/LIFCell.py
@@ -314,14 +314,14 @@ def load(self, directory, seeded=False, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "LIFCell - evolves neurons according to leaky integrate-"
+            "cell_type": "LIFCell - evolves neurons according to leaky integrate-"
                          "and-fire spiking dynamics."
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "s": "Emitted spikes/pulses at time t",
                  "rfr": "Current state of (relative) refractory variable",
diff --git a/ngclearn/components/neurons/spiking/WTASCell.py b/ngclearn/components/neurons/spiking/WTASCell.py
index b595b6c05..7596f7f8a 100755
--- a/ngclearn/components/neurons/spiking/WTASCell.py
+++ b/ngclearn/components/neurons/spiking/WTASCell.py
@@ -131,7 +131,7 @@ def __init__(self, name, n_units, tau_m, resist_m=1., thr_base=0.4, thr_gain=0.0
         self.tols = Compartment(restVals) ## time-of-last-spike
 
     @staticmethod
-    def _advance_state(t, dt, tau_m, R_m, thr_gain, refract_T, j, v, s, thr, rfr, tols):
+    def _advance_state(t, dt, tau_m, R_m, thr_gain, refract_T, j, v, thr, rfr, tols):
         v, s, thr, rfr = run_cell(dt, j, v, rfr, thr, tau_m, R_m, thr_gain, refract_T)
         tols = update_times(t, s, tols) ## update tols
         return v, s, thr, rfr, tols
@@ -173,14 +173,14 @@ def load(self, directory, seeded=False, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "WTASCell - evolves neurons according to winner-take-all "
+            "cell_type": "WTASCell - evolves neurons according to winner-take-all "
                          "spiking dynamics "
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "s": "Emitted spikes/pulses at time t",
                  "rfr": "Current state of (relative) refractory variable",
diff --git a/ngclearn/components/neurons/spiking/adExCell.py b/ngclearn/components/neurons/spiking/adExCell.py
index c4ff6e121..fa041ff45 100755
--- a/ngclearn/components/neurons/spiking/adExCell.py
+++ b/ngclearn/components/neurons/spiking/adExCell.py
@@ -171,7 +171,7 @@ def __init__(self, name, n_units, tau_m=15., resist_m=1., tau_w=400.,
 
     @staticmethod
     def _advance_state(t, dt, tau_m, R_m, tau_w, v_thr, a, b, sharpV, vT,
-                     v_rest, v_reset, intgFlag, j, v, w, s, tols):
+                     v_rest, v_reset, intgFlag, j, v, w, tols):
         v, w, s = run_cell(dt, j, v, w, v_thr, tau_m, tau_w, a, b, sharpV, vT,
                            v_rest, v_reset, R_m, intgFlag)
         tols = update_times(t, s, tols)
@@ -205,14 +205,14 @@ def reset(self, j, v, w, s, tols):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "AdExCell - evolves neurons according to nonlinear, "
+            "cell_type": "AdExCell - evolves neurons according to nonlinear, "
                          "adaptive exponential dual-ODE spiking cell dynamics."
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "w": "Recovery variable at time t",
                  "s": "Emitted spikes/pulses at time t",
diff --git a/ngclearn/components/neurons/spiking/fitzhughNagumoCell.py b/ngclearn/components/neurons/spiking/fitzhughNagumoCell.py
index 8a500610e..26594a45f 100755
--- a/ngclearn/components/neurons/spiking/fitzhughNagumoCell.py
+++ b/ngclearn/components/neurons/spiking/fitzhughNagumoCell.py
@@ -192,7 +192,7 @@ def __init__(self, name, n_units, tau_m=1., resist_m=1., tau_w=12.5, alpha=0.7,
 
     @staticmethod
     def _advance_state(t, dt, tau_m, R_m, tau_w, v_thr, alpha, beta, gamma,
-                       intgFlag, j, v, w, s, tols):
+                       intgFlag, j, v, w, tols):
         v, w, s = run_cell(dt, j * R_m, v, w, v_thr, tau_m, tau_w, alpha, beta,
                            gamma, intgFlag)
         tols = update_times(t, s, tols)
@@ -226,14 +226,14 @@ def reset(self, j, v, w, s, tols):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "FitzhughNagumoCell - evolves neurons according to nonlinear, "
+            "cell_type": "FitzhughNagumoCell - evolves neurons according to nonlinear, "
                          "Fizhugh-Nagumo dual-ODE spiking cell dynamics."
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "w": "Recovery variable at time t",
                  "s": "Emitted spikes/pulses at time t",
diff --git a/ngclearn/components/neurons/spiking/izhikevichCell.py b/ngclearn/components/neurons/spiking/izhikevichCell.py
index f99871f18..710d141d9 100755
--- a/ngclearn/components/neurons/spiking/izhikevichCell.py
+++ b/ngclearn/components/neurons/spiking/izhikevichCell.py
@@ -265,14 +265,14 @@ def reset(self, j, v, w, s, tols):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "IzhikevichCell - evolves neurons according to nonlinear, "
+            "cell_type": "IzhikevichCell - evolves neurons according to nonlinear, "
                          "dual-ODE Izhikevich spiking cell dynamics."
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "w": "Recovery variable at time t",
                  "s": "Emitted spikes/pulses at time t",
diff --git a/ngclearn/components/neurons/spiking/quadLIFCell.py b/ngclearn/components/neurons/spiking/quadLIFCell.py
index 13072cf74..c4dbfd323 100755
--- a/ngclearn/components/neurons/spiking/quadLIFCell.py
+++ b/ngclearn/components/neurons/spiking/quadLIFCell.py
@@ -245,14 +245,14 @@ def advance_state(self, j, v, s, rfr, thr, thr_theta, tols, key):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "QuadLIFCell - evolves neurons according to quadratic "
+            "cell_type": "QuadLIFCell - evolves neurons according to quadratic "
                          "leaky integrate-and-fire spiking dynamics."
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "s": "Emitted spikes/pulses at time t",
                  "rfr": "Current state of (relative) refractory variable",
diff --git a/ngclearn/components/neurons/spiking/sLIFCell.py b/ngclearn/components/neurons/spiking/sLIFCell.py
index 3fad5e783..d3acdb982 100644
--- a/ngclearn/components/neurons/spiking/sLIFCell.py
+++ b/ngclearn/components/neurons/spiking/sLIFCell.py
@@ -256,9 +256,9 @@ def __init__(self, name, n_units, tau_m, resist_m, thr, resist_inh=0.,
         self.surrogate = Compartment(restVals + 1.) ## surrogate signal
 
     @staticmethod
-    def _advance_state(t, dt, inh_weights, R_m, inh_R, d_spike_fx, tau_m, spike_fx,
-                 refract_T, thrGain, thrLeak, rho_b, sticky_spikes, v_min,
-                 j, s, v, thr, rfr, tols):
+    def _advance_state(t, dt, inh_weights, R_m, inh_R, d_spike_fx, tau_m,
+                       spike_fx, refract_T, thrGain, thrLeak, rho_b,
+                       sticky_spikes, v_min, j, s, v, thr, rfr, tols):
         ## run one step of Euler integration over neuronal dynamics
         j_curr = j
         ## apply simplified inhibitory pressure
@@ -321,14 +321,14 @@ def load(self, directory, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "SLIFCell - evolves neurons according to simplified "
+            "cell_type": "SLIFCell - evolves neurons according to simplified "
                          "leaky integrate-and-fire spiking dynamics."
         }
         compartment_props = {
             "input_compartments":
                 {"j": "External input electrical current",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "output_compartments":
                 {"v": "Membrane potential/voltage at time t",
                  "s": "Emitted spikes/pulses at time t",
                  "rfr": "Current state of (relative) refractory variable",
diff --git a/ngclearn/components/other/expKernel.py b/ngclearn/components/other/expKernel.py
index 3647c8b06..9c5a2b6cb 100644
--- a/ngclearn/components/other/expKernel.py
+++ b/ngclearn/components/other/expKernel.py
@@ -59,7 +59,7 @@ def __init__(self, name, n_units, dt, tau_w=500., nu=4., **kwargs):
         self.tf = Compartment(jnp.zeros((self.win_len, self.batch_size, self.n_units)))
 
     @staticmethod
-    def _advance_state(t, dt, tau_w, win_len, inputs, epsp, tf):
+    def _advance_state(t, tau_w, win_len, inputs, tf):
         s = inputs
         ## update spike time window and corresponding window volume
         tf, epsp = apply_kernel(tf, s, t, tau_w, win_len, krn_start=0,
@@ -85,13 +85,13 @@ def reset(self, inputs, epsp, tf):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "ExpKernel - maintains an exponential kernel over "
+            "cell_type": "ExpKernel - maintains an exponential kernel over "
                          "incoming signal values (such as sequences of discrete pulses)"
         }
         compartment_props = {
             "input_compartments":
                 {"inputs": "Takes in external input signal values"},
-            "outputs_compartments":
+            "output_compartments":
                 {"epsp": "Excitatory postsynaptic potential/pulse emitted at time t",
                  "tr": "Value signal (rolling) time window"},
         }
diff --git a/ngclearn/components/other/varTrace.py b/ngclearn/components/other/varTrace.py
index ca47b0455..7fa93fa67 100644
--- a/ngclearn/components/other/varTrace.py
+++ b/ngclearn/components/other/varTrace.py
@@ -79,7 +79,7 @@ def __init__(self, name, n_units, tau_tr, a_delta, decay_type="exp", **kwargs):
         self.trace = Compartment(restVals)
 
     @staticmethod
-    def _advance_state(t, dt, decay_type, tau_tr, a_delta, inputs, trace):
+    def _advance_state(dt, decay_type, tau_tr, a_delta, inputs, trace):
         ## compute the decay factor
         decayFactor = 0. ## <-- pulse filter decay (default)
         if "exp" in decay_type:
@@ -109,13 +109,13 @@ def reset(self, inputs, outputs, trace):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "VarTrace - maintains a low pass filter over incoming signal "
+            "cell_type": "VarTrace - maintains a low pass filter over incoming signal "
                          "values (such as sequences of discrete pulses)"
         }
         compartment_props = {
             "input_compartments":
                 {"inputs": "Takes in external input signal values"},
-            "outputs_compartments":
+            "output_compartments":
                 {"trace": "Continuous low-pass filtered signal values, at time t",
                  "outputs": "Continuous low-pass filtered signal values, "
                             "at time t (same as `trace`)"},
diff --git a/ngclearn/components/synapses/__init__.py b/ngclearn/components/synapses/__init__.py
index 50c77e703..5035b5cff 100644
--- a/ngclearn/components/synapses/__init__.py
+++ b/ngclearn/components/synapses/__init__.py
@@ -5,3 +5,11 @@
 from .hebbian.expSTDPSynapse import ExpSTDPSynapse
 from .hebbian.eventSTDPSynapse import EventSTDPSynapse
 from .hebbian.BCMSynapse import BCMSynapse
+from .convolution.convSynapse import ConvSynapse
+from .convolution.staticConvSynapse import StaticConvSynapse
+from .convolution.hebbianConvSynapse import HebbianConvSynapse
+from .convolution.traceSTDPConvSynapse import TraceSTDPConvSynapse
+from .convolution.deconvSynapse import DeconvSynapse
+from .convolution.staticDeconvSynapse import StaticDeconvSynapse
+from .convolution.hebbianDeconvSynapse import HebbianDeconvSynapse
+from .convolution.traceSTDPDeconvSynapse import TraceSTDPDeconvSynapse
diff --git a/ngclearn/components/synapses/convolution/__init__.py b/ngclearn/components/synapses/convolution/__init__.py
new file mode 100755
index 000000000..ed305c381
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/__init__.py
@@ -0,0 +1,8 @@
+from .convSynapse import ConvSynapse
+from .staticConvSynapse import StaticConvSynapse
+from .deconvSynapse import DeconvSynapse
+from .staticDeconvSynapse import StaticDeconvSynapse
+from .hebbianConvSynapse import HebbianConvSynapse
+from .hebbianDeconvSynapse import HebbianDeconvSynapse
+from .traceSTDPConvSynapse import TraceSTDPConvSynapse
+from .traceSTDPDeconvSynapse import TraceSTDPDeconvSynapse
diff --git a/ngclearn/components/synapses/convolution/convSynapse.py b/ngclearn/components/synapses/convolution/convSynapse.py
new file mode 100755
index 000000000..61a443fae
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/convSynapse.py
@@ -0,0 +1,175 @@
+from jax import random, numpy as jnp, jit
+from ngclearn import resolver, Component, Compartment
+from ngclearn.components.jaxComponent import JaxComponent
+import ngclearn.utils.weight_distribution as dist
+from ngclearn.components.synapses.convolution.ngcconv import conv2d
+from ngcsimlib.logger import info
+from ngclearn.utils import tensorstats
+
+class ConvSynapse(JaxComponent): ## base-level convolutional cable
+    """
+    A base convolutional synaptic cable.
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output
+    | filters - current value tensor of filter/kernel efficacies
+    | biases - current base-rate/bias efficacies
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((K @ in) * resist_scale) + b
+            where `@` denotes convolution
+
+        batch_size: batch size dimension of this component
+    """
+
+    # Define Functions
+    def __init__(self, name, shape, x_size, filter_init=None, bias_init=None, stride=1,
+                 padding=None, resist_scale=1., batch_size=1, **kwargs):
+        super().__init__(name, **kwargs)
+
+        self.filter_init = filter_init
+        self.bias_init = bias_init
+
+        ## Synapse meta-parameters
+        self.shape = shape ## shape of synaptic filter tensor
+        self.x_size = x_size
+        self.Rscale = resist_scale ## post-transformation scale factor
+        self.padding = padding
+        self.stride = stride
+
+        ####################### Set up padding arguments #######################
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        self.pad_args = None
+        if self.padding is not None and self.padding == "SAME":
+            if (x_size % stride == 0):
+                pad_along_height = max(k_size - stride, 0)
+            else:
+                pad_along_height = max(k_size - (x_size % stride), 0)
+            pad_bottom = pad_along_height // 2
+            pad_top = pad_along_height - pad_bottom
+            pad_left = pad_bottom
+            pad_right = pad_top
+            self.pad_args = ((pad_bottom, pad_top), (pad_left, pad_right))
+
+        if self.padding is not None and self.padding == "VALID":
+            self.pad_args = ((0, 0), (0, 0))
+
+        ######################### set up compartments ##########################
+        tmp_key, *subkeys = random.split(self.key.value, 4)
+        weights = dist.initialize_params(subkeys[0], filter_init, shape) ## filter tensor
+        self.batch_size = batch_size # 1
+        ## Compartment setup and shape computation
+        _x = jnp.zeros((self.batch_size, x_size, x_size, n_in_chan))
+        _d = conv2d(_x, weights, stride_size=stride, padding=padding) * 0
+        self.in_shape = _x.shape
+        self.out_shape = _d.shape
+        self.inputs = Compartment(jnp.zeros(self.in_shape))
+        self.outputs = Compartment(jnp.zeros(self.out_shape))
+        self.weights = Compartment(weights)
+        if self.bias_init is None:
+            info(self.name, "is using default bias value of zero (no bias "
+                            "kernel provided)!")
+        self.biases = Compartment(dist.initialize_params(subkeys[2], bias_init,
+                                                         (1, shape[1]))
+                                  if bias_init else 0.0)
+
+    @staticmethod
+    def _advance_state(Rscale, padding, stride, weights, biases, inputs):
+        _x = inputs
+        outputs = conv2d(_x, weights, stride_size=stride, padding=padding) * Rscale + biases
+        return outputs
+
+    @resolver(_advance_state)
+    def advance_state(self, outputs):
+        self.outputs.set(outputs)
+
+    @staticmethod
+    def _reset(in_shape, out_shape):
+        preVals = jnp.zeros(in_shape)
+        postVals = jnp.zeros(out_shape)
+        inputs = preVals
+        outputs = postVals
+        return inputs, outputs
+
+    @resolver(_reset)
+    def reset(self, inputs, outputs):
+        self.inputs.set(inputs)
+        self.outputs.set(outputs)
+
+    def save(self, directory, **kwargs):
+        file_name = directory + "/" + self.name + ".npz"
+        if self.bias_init != None:
+            jnp.savez(file_name, weights=self.weights.value,
+                      biases=self.biases.value)
+        else:
+            jnp.savez(file_name, weights=self.weights.value)
+
+    def load(self, directory, **kwargs):
+        file_name = directory + "/" + self.name + ".npz"
+        data = jnp.load(file_name)
+        self.weights.set(data['weights'])
+        if "biases" in data.keys():
+            self.biases.set(data['biases'])
+
+    def help(self): ## component help function
+        properties = {
+            "synapse type": "ConvSynapse - performs a synaptic convolution (@) of inputs "
+                         "to produce output signals"
+        }
+        compartment_props = {
+            "input_compartments":
+                {"inputs": "Takes in external input signal values",
+                 "key": "JAX RNG key"},
+            "parameter_compartments":
+                {"filters": "Synaptic filter parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
+                {"outputs": "Output of synaptic transformation"},
+        }
+        hyperparams = {
+            "shape": "Shape of synaptic filter value matrix; `kernel width` x `kernel height` "
+                     "x `number input channels` x `number output channels`",
+            "weight_init": "Initialization conditions for synaptic filter (K) values",
+            "bias_init": "Initialization conditions for bias/base-rate (b) values",
+            "resist_scale": "Resistance level output scaling factor (R)"
+        }
+        info = {self.name: properties,
+                "compartments": compartment_props,
+                "dynamics": "outputs = [K @ inputs] * R + b",
+                "hyperparameters": hyperparams}
+        return info
+
+    def __repr__(self):
+        comps = [varname for varname in dir(self) if Compartment.is_compartment(getattr(self, varname))]
+        maxlen = max(len(c) for c in comps) + 5
+        lines = f"[{self.__class__.__name__}] PATH: {self.name}\n"
+        for c in comps:
+            stats = tensorstats(getattr(self, c).value)
+            if stats is not None:
+                line = [f"{k}: {v}" for k, v in stats.items()]
+                line = ", ".join(line)
+            else:
+                line = "None"
+            lines += f"  {f'({c})'.ljust(maxlen)}{line}\n"
+        return lines
diff --git a/ngclearn/components/synapses/convolution/deconvSynapse.py b/ngclearn/components/synapses/convolution/deconvSynapse.py
new file mode 100755
index 000000000..c80bc4c8c
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/deconvSynapse.py
@@ -0,0 +1,163 @@
+from jax import random, numpy as jnp, jit
+from ngclearn import resolver, Component, Compartment
+from ngclearn.components.jaxComponent import JaxComponent
+import ngclearn.utils.weight_distribution as dist
+from ngclearn.components.synapses.convolution.ngcconv import deconv2d
+from ngcsimlib.logger import info
+from ngclearn.utils import tensorstats
+
+class DeconvSynapse(JaxComponent): ## base-level deconvolutional cable
+    """
+    A base deconvolutional (transposed convolutional) synaptic cable.
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output
+    | filters - current value tensor of filter/kernel efficacies
+    | biases - current base-rate/bias efficacies
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((W @.T Rscale) * in) + b
+            where `@.T` denotes deconvolution
+
+        batch_size: batch size dimension of this component
+    """
+
+    # Define Functions
+    def __init__(self, name, shape, x_size, filter_init=None, bias_init=None, stride=1,
+                 padding=None, resist_scale=1., batch_size=1, **kwargs):
+        super().__init__(name, **kwargs)
+
+        self.filter_init = filter_init
+        self.bias_init = bias_init
+
+        ## Synapse meta-parameters
+        self.shape = shape ## shape of synaptic filter tensor
+        self.x_size = x_size
+        self.Rscale = resist_scale ## post-transformation scale factor
+        self.padding = padding
+        self.stride = stride
+
+        ####################### Set up padding arguments #######################
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        self.pad_args = None
+
+        ######################### set up compartments ##########################
+        tmp_key, *subkeys = random.split(self.key.value, 4)
+        weights = dist.initialize_params(subkeys[0], filter_init,
+                                         shape)  ## filter tensor
+        self.batch_size = batch_size # 1
+        ## Compartment setup and shape computation
+        _x = jnp.zeros((self.batch_size, x_size, x_size, n_in_chan))
+        _d = deconv2d(_x, weights, stride_size=stride, padding=padding) * 0
+        self.in_shape = _x.shape
+        self.out_shape = _d.shape
+        self.inputs = Compartment(jnp.zeros(self.in_shape))
+        self.outputs = Compartment(jnp.zeros(self.out_shape))
+        self.weights = Compartment(weights)
+        if self.bias_init is None:
+            info(self.name, "is using default bias value of zero (no bias "
+                            "kernel provided)!")
+        self.biases = Compartment(dist.initialize_params(subkeys[2], bias_init,
+                                                         (1, shape[1]))
+                                  if bias_init else 0.0)
+
+    @staticmethod
+    def _advance_state(Rscale, padding, stride, weights, biases, inputs):
+        _x = inputs
+        out = deconv2d(_x, weights, stride_size=stride, padding=padding) * Rscale + biases
+        return out
+
+    @resolver(_advance_state)
+    def advance_state(self, outputs):
+        self.outputs.set(outputs)
+
+    @staticmethod
+    def _reset(in_shape, out_shape):
+        preVals = jnp.zeros(in_shape)
+        postVals = jnp.zeros(out_shape)
+        inputs = preVals
+        outputs = postVals
+        return inputs, outputs
+
+    @resolver(_reset)
+    def reset(self, inputs, outputs):
+        self.inputs.set(inputs)
+        self.outputs.set(outputs)
+
+    def save(self, directory, **kwargs):
+        file_name = directory + "/" + self.name + ".npz"
+        if self.bias_init != None:
+            jnp.savez(file_name, weights=self.weights.value,
+                      biases=self.biases.value)
+        else:
+            jnp.savez(file_name, weights=self.weights.value)
+
+    def load(self, directory, **kwargs):
+        file_name = directory + "/" + self.name + ".npz"
+        data = jnp.load(file_name)
+        self.weights.set(data['weights'])
+        if "biases" in data.keys():
+            self.biases.set(data['biases'])
+
+    def help(self): ## component help function
+        properties = {
+            "synapse type": "DeconvSynapse - performs a synaptic deconvolution (@.T) of "
+                         "inputs to produce output signals"
+        }
+        compartment_props = {
+            "input_compartments":
+                {"inputs": "Takes in external input signal values",
+                 "key": "JAX RNG key"},
+            "parameter_compartments":
+                {"filters": "Synaptic filter parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
+                {"outputs": "Output of synaptic transformation"},
+        }
+        hyperparams = {
+            "shape": "Shape of synaptic filter value matrix; `kernel width` x `kernel height` "
+                     "x `number input channels` x `number output channels`",
+            "weight_init": "Initialization conditions for synaptic filter (K) values",
+            "bias_init": "Initialization conditions for bias/base-rate (b) values",
+            "resist_scale": "Resistance level output scaling factor (R)"
+        }
+        info = {self.name: properties,
+                "compartments": compartment_props,
+                "dynamics": "outputs = [K @.T inputs] * R + b",
+                "hyperparameters": hyperparams}
+        return info
+
+    def __repr__(self):
+        comps = [varname for varname in dir(self) if Compartment.is_compartment(getattr(self, varname))]
+        maxlen = max(len(c) for c in comps) + 5
+        lines = f"[{self.__class__.__name__}] PATH: {self.name}\n"
+        for c in comps:
+            stats = tensorstats(getattr(self, c).value)
+            if stats is not None:
+                line = [f"{k}: {v}" for k, v in stats.items()]
+                line = ", ".join(line)
+            else:
+                line = "None"
+            lines += f"  {f'({c})'.ljust(maxlen)}{line}\n"
+        return lines
diff --git a/ngclearn/components/synapses/convolution/hebbianConvSynapse.py b/ngclearn/components/synapses/convolution/hebbianConvSynapse.py
new file mode 100755
index 000000000..8f2c5b0b0
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/hebbianConvSynapse.py
@@ -0,0 +1,251 @@
+from jax import random, numpy as jnp, jit
+from ngclearn import resolver, Component, Compartment
+from .convSynapse import ConvSynapse
+from ngclearn.components.synapses.convolution.ngcconv import (_conv_same_transpose_padding,
+                                                              _conv_valid_transpose_padding)
+from ngclearn.components.synapses.convolution.ngcconv import (conv2d, _calc_dX_conv,
+                                                              _calc_dK_conv, calc_dX_conv,
+                                                              calc_dK_conv)
+from ngclearn.utils.optim import get_opt_init_fn, get_opt_step_fn
+
+class HebbianConvSynapse(ConvSynapse): ## Hebbian-evolved convolutional cable
+    """
+    A synaptic convolutional cable that adjusts its efficacies via a two-factor
+    Hebbian adjustment rule.
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output signal (transformation induced by filters)
+    | filters - current value matrix of synaptic filter efficacies
+    | biases - current value vector of synaptic bias values
+    | key - JAX RNG key
+    | --- Synaptic Plasticity Compartments: ---
+    | pre - pre-synaptic signal to drive first term of Hebbian update (takes in external signals)
+    | post - post-synaptic signal to drive 2nd term of Hebbian update (takes in external signals)
+    | dWeights - delta tensor containing changes to be applied to synaptic efficacies
+    | dBiases - delta tensor containing changes to be applied to bias values
+    | dInputs - delta tensor containing back-transmitted signal values ("backpropagating pulse")
+    | opt_params - locally-embedded optimizer statisticis (e.g., Adam 1st/2nd moments if adam is used)
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        eta: global learning rate (default: 0)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((K @ in) * resist_scale) + b
+            where `@` denotes convolution
+
+        w_bound: maximum weight to softly bound this cable's value matrix to; if
+            set to 0, then no synaptic value bounding will be applied
+
+        is_nonnegative: enforce that synaptic efficacies are always non-negative
+            after each synaptic update (if False, no constraint will be applied)
+
+        w_decay: degree to which (L2) synaptic weight decay is applied to the
+            computed Hebbian adjustment (Default: 0); note that decay is not
+            applied to any configured biases
+
+        sign_value: multiplicative factor to apply to final synaptic update before
+            it is applied to synapses; this is useful if gradient descent style
+            optimization is required (as Hebbian rules typically yield
+            adjustments for ascent)
+
+        optim_type: optimization scheme to physically alter synaptic values
+            once an update is computed (Default: "sgd"); supported schemes
+            include "sgd" and "adam"
+
+            :Note: technically, if "sgd" or "adam" is used but `signVal = 1`,
+                then the ascent form of each rule is employed (signVal = -1) or
+                a negative learning rate will mean a descent form of the
+                `optim_scheme` is being employed
+
+        batch_size: batch size dimension of this component
+    """
+
+    # Define Functions
+    def __init__(self, name, shape, x_size, eta=0., filter_init=None, bias_init=None,
+                 stride=1, padding=None, resist_scale=1., w_bound=0.,
+                 is_nonnegative=False, w_decay=0., sign_value=1., optim_type="sgd",
+                 batch_size=1, **kwargs):
+        super().__init__(name, shape, x_size=x_size, filter_init=filter_init,
+                         bias_init=bias_init, resist_scale=resist_scale, stride=stride,
+                         padding=padding, batch_size=batch_size, **kwargs)
+
+        self.eta = eta
+        self.w_bounds = w_bound
+        self.w_decay = w_decay  ## synaptic decay
+        self.is_nonnegative = is_nonnegative
+        self.sign_value = sign_value
+        ## optimization / adjustment properties (given learning dynamics above)
+        self.opt = get_opt_step_fn(optim_type, eta=self.eta)
+
+        ######################### set up compartments ##########################
+        ## Compartment setup and shape computation
+        self.dWeights = Compartment(self.weights.value * 0)
+        self.dInputs = Compartment(jnp.zeros(self.in_shape))
+        self.dBiases = Compartment(self.biases.value * 0)
+        self.pre = Compartment(jnp.zeros(self.in_shape))
+        self.post = Compartment(jnp.zeros(self.out_shape))
+
+        ########################################################################
+        ## Shape error correction -- do shape correction inference for local updates
+        self._init(self.batch_size, self.x_size, self.shape, self.stride,
+                   self.padding, self.pad_args, self.weights)
+        ########################################################################
+
+        ## set up outer optimization compartments
+        self.opt_params = Compartment(get_opt_init_fn(optim_type)(
+            [self.weights.value, self.biases.value]
+            if bias_init else [self.weights.value]))
+
+    def _init(self, batch_size, x_size, shape, stride, padding, pad_args,
+              weights):
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        _x = jnp.zeros((batch_size, x_size, x_size, n_in_chan))
+        _d = conv2d(_x, weights.value, stride_size=stride, padding=padding) * 0
+        _dK = _calc_dK_conv(_x, _d, stride_size=stride, padding=pad_args)
+        ## get filter update correction
+        dx = _dK.shape[0] - weights.value.shape[0]
+        dy = _dK.shape[1] - weights.value.shape[1]
+        self.delta_shape = (max(dx, 0), max(dy, 0))
+        ## get input update correction
+        _dx = _calc_dX_conv(weights.value, _d, stride_size=stride,
+                            anti_padding=pad_args)
+        dx = (_dx.shape[1] - _x.shape[1])
+        dy = (_dx.shape[2] - _x.shape[2])
+        self.x_delta_shape = (dx, dy)
+
+    @staticmethod
+    def _evolve(opt, sign_value, w_decay, w_bounds, is_nonnegative, bias_init,
+                stride, pad_args, delta_shape, pre, post, weights, biases,
+                opt_params):
+        ## calc dFilters / dBiases - update to filters and biases
+        dWeights = calc_dK_conv(pre, post, delta_shape=delta_shape,
+                                stride_size=stride, padding=pad_args)
+        dWeights = dWeights * sign_value
+        if w_decay > 0.: ## apply synaptic decay
+            dWeights = dWeights - weights * w_decay
+
+        dBiases = 0. #jnp.zeros((1,1))
+        if bias_init != None:
+            dBiases = jnp.sum(post, axis=0, keepdims=True) * sign_value
+            opt_params, [weights, biases] = opt(opt_params, [weights, biases],
+                                                [dWeights, dBiases])
+        else: ## ignore dBiases since no biases configured
+            opt_params, [weights] = opt(opt_params, [weights], [dWeights])
+
+        ## apply any enforced filter constraints
+        if w_bounds > 0.:
+            if is_nonnegative:
+                weights = jnp.clip(weights, 0., w_bounds)
+            else:
+                weights = jnp.clip(weights, -w_bounds, w_bounds)
+        return opt_params, weights, biases, dWeights, dBiases
+
+    @resolver(_evolve)
+    def evolve(self, opt_params, weights, biases, dWeights, dBiases):
+        self.opt_params.set(opt_params)
+        self.weights.set(weights)
+        self.biases.set(biases)
+        self.dWeights.set(dWeights)
+        self.dBiases.set(dBiases)
+
+    @staticmethod
+    def _backtransmit(sign_value, x_size, shape, stride, padding, x_delta_shape,
+                      pre, post, weights): ## action-backpropagating routine
+        ## calc dInputs - adjustment w.r.t. input signal
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        antiPad = None
+        if padding == "SAME":
+            antiPad = _conv_same_transpose_padding(post.shape[1], x_size,
+                                                   k_size, stride)
+        elif padding == "VALID":
+            antiPad = _conv_valid_transpose_padding(post.shape[1], x_size,
+                                                    k_size, stride)
+        dInputs = calc_dX_conv(weights, post, delta_shape=x_delta_shape,
+                               stride_size=stride, anti_padding=antiPad)
+        ## flip sign of back-transmitted signal (if applicable)
+        dInputs = dInputs * sign_value
+        return dInputs
+
+    @resolver(_backtransmit)
+    def backtransmit(self, dInputs):
+        self.dInputs.set(dInputs)
+
+    @staticmethod
+    def _reset(in_shape, out_shape):
+        preVals = jnp.zeros(in_shape)
+        postVals = jnp.zeros(out_shape)
+        inputs = preVals
+        outputs = postVals
+        pre = preVals
+        post = postVals
+        dInputs = preVals
+        return inputs, outputs, pre, post, dInputs
+
+    @resolver(_reset)
+    def reset(self, inputs, outputs, pre, post, dInputs):
+        self.inputs.set(inputs)
+        self.outputs.set(outputs)
+        self.pre.set(pre)
+        self.post.set(post)
+        self.dInputs.set(dInputs)
+
+    def help(self): ## component help function
+        properties = {
+            "synapse_type": "HebbianConvSynapse - performs a synaptic convolution "
+                            "(@) of inputs  to produce output signals; synaptic "
+                            "filters are adjusted via two-term/factor Hebbian "
+                            "adjustment"
+        }
+        compartment_props = {
+            "input_compartments":
+                {"inputs": "Takes in external input signal values",
+                 "key": "JAX RNG key",
+                 "pre": "Pre-synaptic statistic for Hebb rule (z_j)",
+                 "post": "Post-synaptic statistic for Hebb rule (z_i)"},
+            "parameter_compartments":
+                {"filters": "Synaptic filter parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
+                {"outputs": "Output of synaptic/filter transformation",
+                 "dWeights": "Synaptic filter value adjustment 4D-tensor produced at time t",
+                 "dBiases": "Synaptic bias/base-rate value adjustment 3D-tensor produced at time t",
+                 "dInputs": "Tensor containing back-transmitted signal values; backpropagating pulse"},
+        }
+        hyperparams = {
+            "shape": "Shape of synaptic filter value matrix; `kernel width` x `kernel height` "
+                     "x `number input channels` x `number output channels`",
+            "weight_init": "Initialization conditions for synaptic filter (K) values",
+            "bias_init": "Initialization conditions for bias/base-rate (b) values",
+            "resist_scale": "Resistance level output scaling factor (R)",
+            "is_nonnegative": "Should filters be constrained to be non-negative post-updates?",
+            "sign_value": "Scalar `flipping` constant -- changes direction to Hebbian descent if < 0",
+            "w_bound": "Soft synaptic bound applied to filters post-update",
+            "w_decay": "Synaptic filter decay term"
+        }
+        info = {self.name: properties,
+                "compartments": compartment_props,
+                "dynamics": "outputs = [K @ inputs] * R + b; "
+                            "dW_{ij}/dt = eta * [(z_j * q_pre) * (z_i * q_post)] - W_{ij} * w_decay",
+                "hyperparameters": hyperparams}
+        return info
+
diff --git a/ngclearn/components/synapses/convolution/hebbianDeconvSynapse.py b/ngclearn/components/synapses/convolution/hebbianDeconvSynapse.py
new file mode 100755
index 000000000..ed2b4a59b
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/hebbianDeconvSynapse.py
@@ -0,0 +1,240 @@
+from jax import random, numpy as jnp, jit
+from ngclearn import resolver, Component, Compartment
+from .deconvSynapse import DeconvSynapse
+from ngclearn.components.synapses.convolution.ngcconv import (deconv2d, _calc_dX_deconv,
+                                                              _calc_dK_deconv, calc_dX_deconv,
+                                                              calc_dK_deconv)
+from ngclearn.utils.optim import get_opt_init_fn, get_opt_step_fn
+
+class HebbianDeconvSynapse(DeconvSynapse): ## Hebbian-evolved deconvolutional cable
+    """
+    A synaptic deconvolutional (transposed convolutional) cable that adjusts its
+    efficacies via a two-factor Hebbian adjustment rule.
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output signal (transformation induced by filters)
+    | filters - current value matrix of synaptic filter efficacies
+    | biases - current value vector of synaptic bias values
+    | key - JAX RNG key
+    | --- Synaptic Plasticity Compartments: ---
+    | pre - pre-synaptic signal to drive first term of Hebbian update (takes in external signals)
+    | post - post-synaptic signal to drive 2nd term of Hebbian update (takes in external signals)
+    | dWeights - delta tensor containing changes to be applied to synaptic efficacies
+    | dBiases - delta tensor containing changes to be applied to bias values
+    | dInputs - delta tensor containing back-transmitted signal values ("backpropagating pulse")
+    | opt_params - locally-embedded optimizer statisticis (e.g., Adam 1st/2nd moments if adam is used)
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        eta: global learning rate (default: 0)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((W @.T Rscale) * in) + b
+            where `@.T` denotes deconvolution
+
+         w_bound: maximum weight to softly bound this cable's value matrix to; if
+            set to 0, then no synaptic value bounding will be applied
+
+        is_nonnegative: enforce that synaptic efficacies are always non-negative
+            after each synaptic update (if False, no constraint will be applied)
+
+        w_decay: degree to which (L2) synaptic weight decay is applied to the
+            computed Hebbian adjustment (Default: 0); note that decay is not
+            applied to any configured biases
+
+        sign_value: multiplicative factor to apply to final synaptic update before
+            it is applied to synapses; this is useful if gradient descent style
+            optimization is required (as Hebbian rules typically yield
+            adjustments for ascent)
+
+        optim_type: optimization scheme to physically alter synaptic values
+            once an update is computed (Default: "sgd"); supported schemes
+            include "sgd" and "adam"
+
+            :Note: technically, if "sgd" or "adam" is used but `signVal = 1`,
+                then the ascent form of each rule is employed (signVal = -1) or
+                a negative learning rate will mean a descent form of the
+                `optim_scheme` is being employed
+
+        batch_size: batch size dimension of this component
+    """
+
+    # Define Functions
+    def __init__(self, name, shape, x_size, eta=0., filter_init=None, bias_init=None,
+                 stride=1, padding=None, resist_scale=1., w_bound=0., is_nonnegative=False,
+                 w_decay=0., sign_value=1., optim_type="sgd", batch_size=1, **kwargs):
+        super().__init__(name, shape, x_size=x_size, filter_init=filter_init,
+                         bias_init=bias_init, resist_scale=resist_scale,
+                         stride=stride, padding=padding, batch_size=batch_size,
+                         **kwargs)
+
+        self.eta = eta
+        self.w_bounds = w_bound
+        self.w_decay = w_decay  ## synaptic decay
+        self.is_nonnegative = is_nonnegative
+        self.sign_value = sign_value
+        ## optimization / adjustment properties (given learning dynamics above)
+        self.opt = get_opt_step_fn(optim_type, eta=self.eta)
+
+        self.dWeights = Compartment(self.weights.value * 0)
+        self.dInputs = Compartment(jnp.zeros(self.in_shape))
+        self.pre = Compartment(jnp.zeros(self.in_shape))
+        self.post = Compartment(jnp.zeros(self.out_shape))
+        self.dBiases = Compartment(self.biases.value * 0)
+
+        ########################################################################
+        ## Shape error correction -- do shape correction inference (for local updates)
+        self._init(self.batch_size, self.x_size, self.shape, self.stride,
+                   self.padding, self.pad_args, self.weights)
+        ########################################################################
+
+        ## set up outer optimization compartments
+        self.opt_params = Compartment(get_opt_init_fn(optim_type)(
+            [self.weights.value, self.biases.value]
+            if bias_init else [self.weights.value]))
+
+    def _init(self, batch_size, x_size, shape, stride, padding, pad_args,
+              weights):
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        _x = jnp.zeros((batch_size, x_size, x_size, n_in_chan))
+        _d = deconv2d(_x, self.weights.value, stride_size=self.stride,
+                      padding=self.padding) * 0
+        _dK = _calc_dK_deconv(_x, _d, stride_size=self.stride, out_size=k_size)
+        ## get filter update correction
+        dx = _dK.shape[0] - self.weights.value.shape[0]
+        dy = _dK.shape[1] - self.weights.value.shape[1]
+        self.delta_shape = (abs(dx), abs(dy))
+
+        ## get input update correction
+        _dx = _calc_dX_deconv(self.weights.value, _d, stride_size=self.stride,
+                       padding=self.padding)
+        dx = (_dx.shape[1] - _x.shape[1])  # abs()
+        dy = (_dx.shape[2] - _x.shape[2])
+        self.x_delta_shape = (dx, dy)
+
+    @staticmethod
+    def _evolve(opt, sign_value, w_decay, w_bounds, is_nonnegative, bias_init,
+                shape, stride, padding, delta_shape, pre, post, weights, biases,
+                opt_params):
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        ## calc dFilters
+        dWeights = calc_dK_deconv(pre, post, delta_shape=delta_shape,
+                                  stride_size=stride, out_size=k_size, padding=padding)
+        dWeights = dWeights * sign_value
+        if w_decay > 0.: ## apply synaptic decay
+            dWeights = dWeights - weights * w_decay
+
+        dBiases = 0.  # jnp.zeros((1,1))
+        if bias_init != None:
+            dBiases = jnp.sum(post, axis=0, keepdims=True) * sign_value
+            opt_params, [weights, biases] = opt(opt_params, [weights, biases],
+                                                [dWeights, dBiases])
+        else: ## ignore dBiases since no biases configured
+            opt_params, [weights] = opt(opt_params, [weights], [dWeights])
+        ## apply any enforced filter constraints
+        if w_bounds > 0.:
+            if is_nonnegative:
+                weights = jnp.clip(weights, 0., w_bounds)
+            else:
+                weights = jnp.clip(weights, -w_bounds, w_bounds)
+        return opt_params, weights, biases, dWeights, dBiases
+
+    @resolver(_evolve)
+    def evolve(self, opt_params, weights, biases, dWeights, dBiases):
+        self.opt_params.set(opt_params)
+        self.weights.set(weights)
+        self.biases.set(biases)
+        self.dWeights.set(dWeights)
+        self.dBiases.set(dBiases)
+
+    @staticmethod
+    def _backtransmit(sign_value, stride, padding, x_delta_shape, pre, post,
+                      weights):  ## action-backpropagating routine
+        ## calc dInputs
+        dInputs = calc_dX_deconv(weights, post, delta_shape=x_delta_shape,
+                                 stride_size=stride, padding=padding)
+        ## flip sign of back-transmitted signal (if applicable)
+        dInputs = dInputs * sign_value
+        return dInputs
+
+    @resolver(_backtransmit)
+    def backtransmit(self, dInputs):
+        self.dInputs.set(dInputs)
+
+    @staticmethod
+    def _reset(in_shape, out_shape):
+        preVals = jnp.zeros(in_shape)
+        postVals = jnp.zeros(out_shape)
+        inputs = preVals
+        outputs = postVals
+        pre = preVals
+        post = postVals
+        dInputs = preVals
+        return inputs, outputs, pre, post, dInputs
+
+    @resolver(_reset)
+    def reset(self, inputs, outputs, pre, post, dInputs):
+        self.inputs.set(inputs)
+        self.outputs.set(outputs)
+        self.pre.set(pre)
+        self.post.set(post)
+        self.dInputs.set(dInputs)
+
+    def help(self): ## component help function
+        properties = {
+            "synapse_type": "DeconvSynapse - performs a synaptic deconvolution "
+                            "(@.T) of inputs to produce output signals; synaptic "
+                            "filters are adjusted via two-term/factor Hebbian "
+                            "adjustment"
+        }
+        compartment_props = {
+            "input_compartments":
+                {"inputs": "Takes in external input signal values",
+                 "key": "JAX RNG key",
+                 "pre": "Pre-synaptic statistic for Hebb rule (z_j)",
+                 "post": "Post-synaptic statistic for Hebb rule (z_i)"},
+            "parameter_compartments":
+                {"filters": "Synaptic filter parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
+                {"outputs": "Output of synaptic/filter transformation",
+                 "dWeights": "Synaptic filter value adjustment 4D-tensor produced at time t",
+                 "dBiases": "Synaptic bias/base-rate value adjustment 3D-tensor produced at time t",
+                 "dInputs": "Tensor containing back-transmitted signal values; backpropagating pulse"},
+        }
+        hyperparams = {
+            "shape": "Shape of synaptic filter value matrix; `kernel width` x `kernel height` "
+                     "x `number input channels` x `number output channels`",
+            "weight_init": "Initialization conditions for synaptic filter (K) values",
+            "bias_init": "Initialization conditions for bias/base-rate (b) values",
+            "resist_scale": "Resistance level output scaling factor (R)",
+            "is_nonnegative": "Should filters be constrained to be non-negative post-updates?",
+            "sign_value": "Scalar `flipping` constant -- changes direction to Hebbian descent if < 0",
+            "w_bound": "Soft synaptic bound applied to filters post-update",
+            "w_decay": "Synaptic filter decay term"
+        }
+        info = {self.name: properties,
+                "compartments": compartment_props,
+                "dynamics": "outputs = [K @.T inputs] * R + b; "
+                            "dW_{ij}/dt = eta * [(z_j * q_pre) * (z_i * q_post)] - W_{ij} * w_decay",
+                "hyperparameters": hyperparams}
+        return info
diff --git a/ngclearn/components/synapses/convolution/ngcconv.py b/ngclearn/components/synapses/convolution/ngcconv.py
new file mode 100755
index 000000000..5733ac36e
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/ngcconv.py
@@ -0,0 +1,529 @@
+"""
+Calculation toolbox that drives conv/deconv operations in the ngc-learn
+convolution components sub-branch; this contains routines/co-routines
+for `ngclearn.components.synapses.convolution`.
+"""
+import numpy as np
+from jax import jit, numpy as jnp, random, nn, lax
+from functools import partial
+from jax._src import core
+
+@partial(jit, static_argnums=[1])
+def _pad(x, padding):
+    """
+    Jit-i-fied padding function.
+
+    Args:
+        x (ndarray): The input array to be padded.
+
+        padding (tuple): A tuple containing the amounts of padding to apply to each dimension;
+            Format: (pad_bottom, pad_top, pad_left, pad_right).
+
+    Returns:
+        _x (ndarray): The padded array.
+    """
+    
+    # Unpack the padding amounts for each dimension
+    pad_bottom, pad_top, pad_left, pad_right = padding
+
+    # Apply padding to the input array 'x'
+    # Padding is applied as:
+    # [[0, 0],           # No padding to the batch dimension
+    #  [pad_bottom, pad_top], # Padding for the height dimension
+    #  [pad_left, pad_right], # Padding for the width dimension
+    #  [0, 0]]           # No padding to the channel dimension
+    _x = jnp.pad(x,
+                 [[0, 0],
+                  [pad_bottom, pad_top],
+                  [pad_left, pad_right],
+                  [0, 0]], mode="constant").astype(jnp.float32) #To ensure all variables are of type float32
+    
+    # Return the padded array
+    return _x
+
+@jit
+def rot180(tensor):
+    """
+    Rotate the input tensor by 180 degrees.
+
+    Args:
+        tensor (ndarray): The input tensor to be rotated.
+
+    Returns:
+        ndarray: The tensor rotated by 180 degrees.
+    """
+    
+    # Flip the tensor along the first two axes (height and width) to achieve a 180-degree rotation
+    flipped_tensor = jnp.flip(tensor, axis=[0, 1])
+    
+    # Transpose the tensor to reorder the axes
+    # The axes [0, 1, 3, 2] correspond to:
+    # 0: Height
+    # 1: Width
+    # 3: Input Channels
+    # 2: Output Channels
+    rotated_tensor = jnp.transpose(flipped_tensor, axes=[0, 1, 3, 2])
+    
+    # Return the rotated tensor
+    return rotated_tensor
+
+@partial(jit, static_argnums=[2, 3, 4])
+def get_same_conv_padding(lhs, rhs, stride_size=1, rhs_dilation=(1, 1),
+                          lhs_dilation=(1, 1)):
+    padding = "SAME"
+    window_strides = (stride_size, stride_size)
+    dimension_numbers = ('NHWC', 'HWIO', 'NHWC')
+    dnums = lax.conv_dimension_numbers(lhs.shape, rhs.shape, dimension_numbers)
+    lhs_perm, rhs_perm, _ = dnums
+    rhs_shape = jnp.take(rhs.shape, rhs_perm)[2:]  # type: ignore[index]
+    effective_rhs_shape = [core.dilate_dim(k, r) for k, r in
+                           zip(rhs_shape, rhs_dilation)]
+    padding = lax.padtype_to_pads(
+        jnp.take(lhs.shape, lhs_perm)[2:], effective_rhs_shape,
+        # type: ignore[index]
+        window_strides, padding)
+    return padding
+
+
+@partial(jit, static_argnums=[2, 3, 4])
+def get_valid_conv_padding(lhs, rhs, stride_size=1, rhs_dilation=(1, 1),
+                           lhs_dilation=(1, 1)):
+    padding = "VALID"
+    window_strides = (stride_size, stride_size)
+    dimension_numbers = ('NHWC', 'HWIO', 'NHWC')
+    dnums = lax.conv_dimension_numbers(lhs.shape, rhs.shape, dimension_numbers)
+    lhs_perm, rhs_perm, _ = dnums
+    rhs_shape = jnp.take(rhs.shape, rhs_perm)[2:]  # type: ignore[index]
+    effective_rhs_shape = [core.dilate_dim(k, r) for k, r in
+                           zip(rhs_shape, rhs_dilation)]
+    padding = lax.padtype_to_pads(
+        jnp.take(lhs.shape, lhs_perm)[2:], effective_rhs_shape,
+        # type: ignore[index]
+        window_strides, padding)
+    return padding
+
+def _conv_same_transpose_padding(inputs, output, kernel, stride):
+    """
+    Calculate the padding for a transpose convolution operation to achieve 'SAME' padding.
+
+    Args:
+        inputs (int): The size of the input.
+
+        output (int): The size of the output.
+
+        kernel (int): The size of the convolution kernel.
+
+        stride (int): The stride length of the convolution.
+
+    Returns:
+        tuple: The padding for the height and width dimensions.
+    """
+    pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+    if stride > kernel:
+        pad_a = kernel - 1
+    else:
+        pad_a = int(np.ceil(pad_len / 2))
+    pad_b = pad_len - pad_a
+    return ((pad_a, pad_b), (pad_a, pad_b))
+
+## Better optimized version of conv_same_padding
+#@jit
+# def _conv_same_transpose_padding(inputs, output, kernel, stride):
+#     """
+#     Calculate the padding for a transpose convolution operation to achieve 'same' padding.
+#
+#     Parameters:
+#     inputs (int): The size of the input.
+#     output (int): The size of the output.
+#     kernel (int): The size of the convolution kernel.
+#     stride (int): The stride length of the convolution.
+#
+#     Returns:
+#     tuple: The padding for the height and width dimensions.
+#     """
+#     # Calculate the total padding length required
+#     pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+#
+#     # Determine padding based on stride and kernel size
+#     pad_a = jnp.where(stride > kernel, kernel - 1, jnp.ceil(pad_len / 2).astype(int))
+#     pad_b = pad_len - pad_a
+#
+#     # Return the padding for height and width as tuples
+#     return ((pad_a, pad_b), (pad_a, pad_b))
+
+
+def _conv_valid_transpose_padding(inputs, output, kernel, stride):
+    """
+    Calculate the padding for a transpose convolution operation to achieve 'VALID' padding.
+
+    Args:
+        inputs (int): The size of the input.
+
+        output (int): The size of the output.
+
+        kernel (int): The size of the convolution kernel.
+
+        stride (int): The stride length of the convolution.
+
+    Returns:
+        tuple: The padding for the height and width dimensions.
+    """
+    pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+    pad_a = kernel - 1
+    pad_b = pad_len - pad_a
+    return ((pad_a, pad_b), (pad_a, pad_b))
+
+# ## Optimized version version2 for conv_valid_transpose_padding
+# @jit
+# def _conv_valid_transpose_padding(inputs, output, kernel, stride):
+#     """
+#     Calculate the padding for a transpose convolution operation to achieve 'valid' padding.
+#
+#     Parameters:
+#     inputs (int): The size of the input.
+#     output (int): The size of the output.
+#     kernel (int): The size of the convolution kernel.
+#     stride (int): The stride length of the convolution.
+#
+#     Returns:
+#     tuple: The padding for the height and width dimensions.
+#     """
+#     # Calculate the total padding length required
+#     pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+#
+#     # Set the padding value
+#     pad_a = kernel - 1
+#     pad_b = pad_len - pad_a
+#
+#     # Ensure values are cast to integers
+#     pad_a = jnp.int32(pad_a)
+#     pad_b = jnp.int32(pad_b)
+#
+#     # Return the padding for height and width as tuples
+#     return ((pad_a, pad_b), (pad_a, pad_b))
+
+
+# @jit
+# def _deconv_valid_transpose_padding(inputs, output, kernel, stride):
+#     """
+#     Calculate the padding for a transpose deconvolution operation to achieve 'valid' padding.
+#
+#     Parameters:
+#     inputs (int): The size of the input.
+#     output (int): The size of the output.
+#     kernel (int): The size of the deconvolution kernel.
+#     stride (int): The stride length of the deconvolution.
+#
+#     Returns:
+#     tuple: The padding for the height and width dimensions.
+#     """
+#     # Calculate the total padding length required
+#     pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+#
+#     # Set the padding value
+#     pad_a = output - 1
+#     pad_b = pad_len - pad_a
+#
+#     # Ensure values are cast to integers
+#     pad_a = jnp.int32(pad_a)
+#     pad_b = jnp.int32(pad_b)
+#
+#     # Return the padding for height and width as tuples
+#     return ((pad_a, pad_b), (pad_a, pad_b))
+    
+def _deconv_valid_transpose_padding(inputs, output, kernel, stride):
+    """
+    Calculate the padding for a transpose deconvolution operation to achieve 'VALID' padding.
+
+    Args::
+        inputs (int): The size of the input.
+
+        output (int: The size of the output.
+
+        kernel (int): The size of the deconvolution kernel.
+
+        stride (int): The stride length of the deconvolution.
+
+    Returns:
+        tuple: The padding for the height and width dimensions.
+    """
+    pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+    pad_a = output - 1
+    pad_b = pad_len - pad_a
+    return ((pad_a, pad_b), (pad_a, pad_b))
+
+# @jit
+# def _deconv_same_transpose_padding(inputs, output, kernel, stride):
+#     """
+#     Calculate the padding for a transpose deconvolution operation to achieve 'same' padding.
+#
+#     Parameters:
+#     inputs (int): The size of the input.
+#     output (int): The size of the output.
+#     kernel (int): The size of the deconvolution kernel.
+#     stride (int): The stride length of the deconvolution.
+#
+#     Returns:
+#     tuple: The padding for the height and width dimensions.
+#     """
+#     # Calculate the total padding length required
+#     pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+#
+#     # Determine padding values
+#     pad_a = jnp.where(stride >= output - 1, output - 1, jnp.ceil(pad_len / 2)).astype(int)
+#     pad_b = pad_len - pad_a
+#
+#     # Return the padding for height and width as tuples
+#     return ((pad_a, pad_b), (pad_a, pad_b))
+
+def _deconv_same_transpose_padding(inputs, output, kernel, stride):
+    """
+    Calculate the padding for a transpose deconvolution operation to achieve 'SAME' padding.
+
+    Args:
+        inputs (int): The size of the input.
+
+        output (int): The size of the output.
+
+        kernel (int): The size of the deconvolution kernel.
+
+        stride (int): The stride length of the deconvolution.
+
+    Returns:
+        tuple: The padding for the height and width dimensions.
+    """
+    pad_len = output - ((stride - 1) * (inputs - 1) + inputs - (kernel - 1))
+    if stride >= output - 1:
+        pad_a = output - 1
+    else:
+        pad_a = int(np.ceil(pad_len / 2)) # int(jnp.ceil(pad_len / 2))
+    pad_b = pad_len - pad_a
+    return ((pad_a, pad_b), (pad_a, pad_b))
+
+@partial(jit, static_argnums=[2, 3, 4])
+def deconv2d(inputs, filters, stride_size=1, rhs_dilation=(1, 1),
+             padding=((0, 0), (0, 0))):  ## Deconv2D
+    dim_numbers = ('NHWC', 'HWIO', 'NHWC')
+    out = lax.conv_transpose(inputs,  # lhs = image tensor
+                             filters,  # rhs = conv kernel tensor
+                             (stride_size, stride_size),  # window strides
+                             padding,  # padding mode
+                             rhs_dilation,  # rhs/kernel dilation
+                             dim_numbers)
+    return out
+
+
+@partial(jit, static_argnums=[2, 3, 4, 5])
+def conv2d(inputs, filters, stride_size=1, rhs_dilation=(1, 1),
+           lhs_dilation=(1, 1), padding=((0, 0), (0, 0))):  ## Conv2D
+    dim_numbers = ('NHWC', 'HWIO', 'NHWC')
+    out = lax.conv_general_dilated(inputs,  # lhs = image tensor
+                                   filters,  # rhs = conv kernel tensor
+                                   (stride_size, stride_size),  # window strides
+                                   padding,  # padding mode
+                                   lhs_dilation,  # lhs/image dilation
+                                   rhs_dilation,  # rhs/kernel dilation
+                                   dim_numbers)
+    return out
+
+################################################################################
+## ngc-learn convolution calculations
+
+# @partial(jit, static_argnums=[2, 3, 4])
+# def calc_dK_conv(x, d_out, delta_shape, stride_size=1, padding=((0, 0), (0, 0))):
+#     _x = x
+#     deX, deY = delta_shape
+#     if deX > 0:
+#         ## apply a pre-computation trimming step ("negative padding")
+#         _x = x[:, 0:x.shape[1]-deX, 0:x.shape[2]-deY, :]
+#     return _calc_dK_conv(_x, d_out, stride_size=stride_size, padding=padding)
+
+@partial(jit, static_argnums=[2, 3, 4])
+def calc_dK_conv(x, d_out, delta_shape, stride_size=1, padding=((0, 0), (0, 0))):
+    """
+    Calculate the gradient with respect to the kernel for a convolution operation.
+    
+    Args:
+        x (ndarray): The input array.
+
+        d_out (ndarray): The gradient with respect to the output.
+
+        delta_shape (tuple): The shape difference (deX, deY) between the input and output.
+
+        stride_size (int): The stride size for the convolution. Defaults to 1.
+
+        padding (tuple): Padding to apply to the input. Defaults to ((0, 0), (0, 0)).
+    
+    Returns:
+        ndarray: The gradient with respect to the kernel.
+    """
+    deX, deY = delta_shape
+
+    # Apply a pre-computation trimming step ("negative padding") if needed
+
+    #_x = jnp.where(deX > 0, x[:, 0:x.shape[1]-deX, 0:x.shape[2]-deY, :], x)
+    # _deX = jnp.maximum(deX, 0).astype(jnp.int32)
+    # _deY = jnp.maximum(deY, 0).astype(jnp.int32)
+    _x = x[:, 0:x.shape[1]-deX, 0:x.shape[2]-deY, :]
+
+    # Calculate the gradient with respect to the kernel
+    return _calc_dK_conv(_x, d_out, stride_size=stride_size, padding=padding)
+
+@partial(jit, static_argnums=[2, 3])
+def _calc_dK_conv(x, d_out, stride_size=1, padding=((0, 0), (0, 0))):
+    xT = jnp.transpose(x, axes=[3, 1, 2, 0])
+    d_out_T = jnp.transpose(d_out, axes=[1, 2, 0, 3])
+    ## original conv2d
+    dW = conv2d(inputs=xT, filters=d_out_T, stride_size=1, padding=padding,
+                rhs_dilation=(stride_size, stride_size)).astype(jnp.float32)
+    return jnp.transpose(dW, axes=[1, 2, 0, 3])
+
+################################################################################
+# input update computation
+@partial(jit, static_argnums=[2, 3, 4])
+def calc_dX_conv(K, d_out, delta_shape, stride_size=1, anti_padding=None):
+    deX, deY = delta_shape
+    # if abs(deX) > 0 and stride_size > 1:
+    #     return _calc_dX_subset(K, d_out, (abs(deX),abs(deY)), stride_size=stride_size,
+    #                            anti_padding=anti_padding)
+    dx = _calc_dX_conv(K, d_out, stride_size=stride_size, anti_padding=anti_padding)
+    return dx
+
+@partial(jit, static_argnums=[2, 3])
+def _calc_dX_conv(K, d_out, stride_size=1, anti_padding=None):
+    w_size = K.shape[0]
+    K_T = rot180(K)  # Assuming rot180 is defined elsewhere.
+    _pad = w_size - 1
+    return deconv2d(d_out, filters=K_T, stride_size=stride_size,
+                    padding=anti_padding).astype(jnp.float32)
+
+################################################################################
+## ngc-learn deconvolution calculations
+
+# @partial(jit, static_argnums=[2, 3, 4, 5])
+# def calc_dK_deconv(x, d_out, delta_shape, stride_size=1, out_size =2, padding="SAME"):
+#     _x = x
+#     deX, deY = delta_shape
+#     if deX > 0:
+#         ## apply a pre-computation trimming step ("negative padding")
+#         _x = x[:, 0:x.shape[1]-deX, 0:x.shape[2]-deY, :]
+#     return _calc_dK_deconv(_x, d_out, stride_size=stride_size, out_size = out_size)
+
+@partial(jit, static_argnums=[2, 3, 4, 5])
+def calc_dK_deconv(x, d_out, delta_shape, stride_size=1, out_size=2, padding="SAME"):
+    """
+    Calculate the gradient with respect to the kernel for a deconvolution operation.
+    
+    Args:
+        x (ndarray): The input array.
+
+        d_out (ndarray): The gradient with respect to the output.
+
+        delta_shape (tuple): The shape difference (deX, deY) between the input and output.
+
+        stride_size (int): The stride size for the deconvolution. Defaults to 1.
+
+        out_size (int): The output size for the deconvolution.
+
+        padding (str): Padding to apply to the input. Defaults to "SAME".
+    
+    Returns:
+        ndarray: The gradient with respect to the kernel.
+    """
+    deX, deY = delta_shape
+
+    # Apply a pre-computation trimming step ("negative padding") if needed
+    #_x = jnp.where(deX > 0, x[:, :x.shape[1]-deX, :x.shape[2]-deY, :], x)
+    _x = x[:, :x.shape[1]-deX, :x.shape[2]-deY, :]
+
+    # Calculate the gradient with respect to the kernel
+    return _calc_dK_deconv(_x, d_out, stride_size=stride_size, out_size=out_size, padding=padding)
+
+
+@partial(jit, static_argnums=[2, 3, 4])
+def _calc_dK_deconv(x, d_out, stride_size=1, out_size=2, padding="SAME"):
+    xT = jnp.transpose(x, axes=[3, 1, 2, 0])
+    d_out_T = jnp.transpose(d_out, axes=[1, 2, 0, 3])
+    if padding == "VALID":
+        pad_args = _deconv_valid_transpose_padding(xT.shape[1], out_size, d_out_T.shape[1], stride_size)
+    elif padding == "SAME":
+        pad_args = _deconv_same_transpose_padding(xT.shape[1], out_size, d_out_T.shape[1], stride_size)
+    dW = deconv2d(inputs=xT, filters=d_out_T, stride_size=stride_size,
+                  padding=pad_args)
+    dW = jnp.transpose(dW, axes=[1, 2, 0, 3])
+    return dW
+################################################################################
+# input update computation
+@partial(jit, static_argnums=[2, 3, 4])
+def calc_dX_deconv(K, d_out, delta_shape, stride_size=1, padding=((0, 0), (0, 0))):
+    """
+    Wrapper function to calculate the gradient with respect to the input (dX)
+    from the gradient with respect to the output (d_out) using the kernel (K).
+    This version takes into account the shape difference (delta_shape) between
+    the input and the output of the convolution.
+
+    Args:
+        K (ndarray): The convolution kernel.
+
+        d_out (ndarray): The gradient with respect to the output of the convolution.
+
+        delta_shape (tuple): The shape difference (deX, deY) between the input and output.
+
+        stride_size (int): The stride size for the deconvolution. Defaults to 1.
+
+        padding (tuple): Padding to apply to the input. Defaults to ((0, 0), (0, 0)).
+
+    Returns:
+        dx (ndarray): The gradient with respect to the input.
+    """
+    
+    # Extract the shape difference in the x and y dimensions
+    deX, deY = delta_shape
+
+    # Conditional logic to handle cases where delta_shape is non-zero and stride_size is greater than 1
+    # if abs(deX) > 0 and stride_size > 1:
+    #     return _calc_dX_subset(K, d_out, (abs(deX), abs(deY)), stride_size=stride_size, padding=padding)
+
+    # Call the _calc_dX_deconv function to perform the deconvolution
+    dx = _calc_dX_deconv(K, d_out, stride_size=stride_size, padding=padding)
+    
+    # Return the gradient with respect to the input
+    return dx
+
+@partial(jit, static_argnums=[2, 3])
+def _calc_dX_deconv(K, d_out, stride_size=1, padding=((0, 0), (0, 0))):
+    """
+    Perform a deconvolution to get the gradient with respect to the input (dX)
+    from the gradient with respect to the output (d_out) using the kernel (K).
+
+    Args:
+        K (ndarray): The convolution kernel.
+
+        d_out (ndarray): The gradient with respect to the output of the convolution.
+
+        stride_size (int): The stride size for the deconvolution. Defaults to 1.
+
+        padding (tuple): Padding to apply to the input. Defaults to ((0, 0), (0, 0)).
+
+    Returns:
+        dx (ndarray): The gradient with respect to the input.
+    """
+
+    # The size of the kernel
+    w_size = K.shape[0]
+
+    # Rotate the kernel by 180 degrees
+    K_T = rot180(K)  # Equivalent to jnp.transpose(K, axes=[1, 0, 3, 2])
+    
+    # Padding size for the deconvolution, derived from the kernel size
+    _pad = w_size - 1
+
+    # Perform the deconvolution using conv2d with the rotated kernel
+    dx = conv2d(d_out,
+                filters=K_T,
+                stride_size=stride_size,
+                padding=padding)
+    
+    # Return the gradient with respect to the input
+    return dx
+################################################################################
diff --git a/ngclearn/components/synapses/convolution/staticConvSynapse.py b/ngclearn/components/synapses/convolution/staticConvSynapse.py
new file mode 100755
index 000000000..8c6f1f77a
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/staticConvSynapse.py
@@ -0,0 +1,41 @@
+from .convSynapse import ConvSynapse
+
+class StaticConvSynapse(ConvSynapse):
+    """
+    A static convolutional synaptic cable; no form of synaptic evolution/adaptation
+    is in-built to this component.
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output
+    | weights - current value tensor of kernel efficacies
+    | biases - current base-rate/bias efficacies
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((K @ in) * resist_scale) + b
+            where `@` denotes convolution
+
+        batch_size: batch size dimension of this component
+    """
+
+    pass
diff --git a/ngclearn/components/synapses/convolution/staticDeconvSynapse.py b/ngclearn/components/synapses/convolution/staticDeconvSynapse.py
new file mode 100755
index 000000000..12e254bc9
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/staticDeconvSynapse.py
@@ -0,0 +1,41 @@
+from .deconvSynapse import DeconvSynapse
+
+class StaticDeconvSynapse(DeconvSynapse):
+    """
+    A static deconvolutional (transposed convolutional) synaptic cable; no form
+    of synaptic evolution/adaptation is in-built to this component.
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output
+    | weights - current value tensor of kernel efficacies
+    | biases - current base-rate/bias efficacies
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((K @ in) * resist_scale) + b
+            where `@` denotes convolution
+
+        batch_size: batch size dimension of this component
+    """
+
+    pass
diff --git a/ngclearn/components/synapses/convolution/traceSTDPConvSynapse.py b/ngclearn/components/synapses/convolution/traceSTDPConvSynapse.py
new file mode 100755
index 000000000..e63dfbcc3
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/traceSTDPConvSynapse.py
@@ -0,0 +1,232 @@
+from jax import random, numpy as jnp, jit
+from ngclearn import resolver, Component, Compartment
+from .convSynapse import ConvSynapse
+from ngclearn.components.synapses.convolution.ngcconv import (_conv_same_transpose_padding,
+                                                              _conv_valid_transpose_padding)
+from ngclearn.components.synapses.convolution.ngcconv import (conv2d, _calc_dX_conv,
+                                                              _calc_dK_conv, calc_dX_conv,
+                                                              calc_dK_conv)
+
+class TraceSTDPConvSynapse(ConvSynapse): ## trace-based STDP convolutional cable
+    """
+    A synaptic convolutional cable that adjusts its filter efficacies via a
+    trace-based form of spike-timing-dependent plasticity (STDP).
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output signal (transformation induced by filters)
+    | filters - current value matrix of synaptic filter efficacies
+    | biases - current value vector of synaptic bias values
+    | key - JAX RNG key
+    | --- Synaptic Plasticity Compartments: ---
+    | preSpike - pre-synaptic spike to drive 1st term of STDP update (takes in external signals)
+    | postSpike - post-synaptic spike to drive 2nd term of STDP update (takes in external signals)
+    | preTrace - pre-synaptic trace value to drive 1st term of STDP update (takes in external signals)
+    | postTrace - post-synaptic trace value to drive 2nd term of STDP update (takes in external signals)
+    | dWeights - delta tensor containing changes to be applied to synaptic efficacies
+    | dInputs - delta tensor containing back-transmitted signal values ("backpropagating pulse")
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        A_plus: strength of long-term potentiation (LTP)
+
+        A_minus: strength of long-term depression (LTD)
+
+        eta: global learning rate (default: 0)
+
+        pretrace_target: controls degree of pre-synaptic disconnect, i.e., amount of decay
+                 (higher -> lower synaptic values)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((K @ in) * resist_scale) + b
+            where `@` denotes convolution
+
+        w_bound: maximum weight to softly bound this cable's value matrix to; if
+            set to 0, then no synaptic value bounding will be applied
+
+        w_decay: degree to which (L2) synaptic weight decay is applied to the
+            computed STDP adjustment (Default: 0)
+
+        batch_size: batch size dimension of this component
+    """
+
+    # Define Functions
+    def __init__(self, name, shape, x_size, A_plus, A_minus, eta=0.,
+                 pretrace_target=0., filter_init=None, stride=1, padding=None,
+                 resist_scale=1., w_bound=0., w_decay=0., batch_size=1, **kwargs):
+        super().__init__(name, shape, x_size=x_size, filter_init=filter_init,
+                         bias_init=None, resist_scale=resist_scale, stride=stride,
+                         padding=padding, batch_size=batch_size, **kwargs)
+
+        self.eta = eta
+        self.w_bounds = w_bound ## soft weight constraint
+        self.w_decay = w_decay  ## synaptic decay
+        self.eta = eta  ## global learning rate governing plasticity
+        self.pretrace_target = pretrace_target  ## target (pre-synaptic) trace activity value # 0.7
+        self.Aplus = A_plus  ## LTP strength
+        self.Aminus = A_minus  ## LTD strength
+
+        ######################### set up compartments ##########################
+        ## Compartment setup and shape computation
+        self.dWeights = Compartment(self.weights.value * 0)
+        self.dInputs = Compartment(jnp.zeros(self.in_shape))
+        self.preSpike = Compartment(jnp.zeros(self.in_shape))
+        self.preTrace = Compartment(jnp.zeros(self.in_shape))
+        self.postSpike = Compartment(jnp.zeros(self.out_shape))
+        self.postTrace = Compartment(jnp.zeros(self.out_shape))
+
+        ########################################################################
+        ## Shape error correction -- do shape correction inference for local updates
+        self._init(self.batch_size, self.x_size, self.shape, self.stride,
+                   self.padding, self.pad_args, self.weights)
+        ########################################################################
+
+    def _init(self, batch_size, x_size, shape, stride, padding, pad_args,
+              weights):
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        _x = jnp.zeros((batch_size, x_size, x_size, n_in_chan))
+        _d = conv2d(_x, weights.value, stride_size=stride, padding=padding) * 0
+        _dK = _calc_dK_conv(_x, _d, stride_size=stride, padding=pad_args)
+        ## get filter update correction
+        dx = _dK.shape[0] - weights.value.shape[0]
+        dy = _dK.shape[1] - weights.value.shape[1]
+        #self.delta_shape = (dx, dy)
+        self.delta_shape = (max(dx, 0), max(dy, 0))
+        ## get input update correction
+        _dx = _calc_dX_conv(weights.value, _d, stride_size=stride,
+                            anti_padding=pad_args)
+        dx = (_dx.shape[1] - _x.shape[1])
+        dy = (_dx.shape[2] - _x.shape[2])
+        self.x_delta_shape = (dx, dy)
+
+    @staticmethod
+    def _evolve(eta, pretrace_target, Aplus, Aminus, w_decay, w_bounds,
+                stride, pad_args, delta_shape, preSpike, preTrace, postSpike,
+                postTrace, weights):
+        ## Compute long-term potentiation to filters
+        dW_ltp = calc_dK_conv(preTrace - pretrace_target, postSpike * Aplus,
+                              delta_shape=delta_shape, stride_size=stride,
+                              padding=pad_args)
+        ## Compute long-term depression to filters
+        dW_ltd = -calc_dK_conv(preSpike, postTrace * Aminus,
+                               delta_shape=delta_shape, stride_size=stride,
+                               padding=pad_args)
+        dWeights = (dW_ltp + dW_ltd)
+        if w_decay > 0.:  ## apply synaptic decay
+            weights = weights + dWeights * eta - weights * w_decay  ## conduct decayed STDP-ascent
+        else:
+            weights = weights + dWeights * eta  ## conduct STDP-ascent
+        ## Apply any enforced filter constraints
+        if w_bounds > 0.:
+            ## enforce non-negativity
+            eps = 0.01  # 0.001
+            weights = jnp.clip(weights, eps, w_bounds - eps)
+        return weights, dWeights
+
+    @resolver(_evolve)
+    def evolve(self, weights, dWeights):
+        self.weights.set(weights)
+        self.dWeights.set(dWeights)
+
+    @staticmethod
+    def _backtransmit(x_size, shape, stride, padding, x_delta_shape,
+                      preSpike, postSpike, weights): ## action-backpropagating routine
+        ## calc dInputs - adjustment w.r.t. input signal
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        antiPad = None
+        if padding == "SAME":
+            antiPad = _conv_same_transpose_padding(postSpike.shape[1], x_size,
+                                                   k_size, stride)
+        elif padding == "VALID":
+            antiPad = _conv_valid_transpose_padding(postSpike.shape[1], x_size,
+                                                    k_size, stride)
+        dInputs = calc_dX_conv(weights, postSpike, delta_shape=x_delta_shape,
+                               stride_size=stride, anti_padding=antiPad)
+        return dInputs
+
+    @resolver(_backtransmit)
+    def backtransmit(self, dInputs):
+        self.dInputs.set(dInputs)
+
+    @staticmethod
+    def _reset(in_shape, out_shape):
+        preVals = jnp.zeros(in_shape)
+        postVals = jnp.zeros(out_shape)
+        inputs = preVals
+        outputs = postVals
+        preSpike = preVals
+        postSpike = postVals
+        preTrace = preVals
+        postTrace = postVals
+        return inputs, outputs, preSpike, postSpike, preTrace, postTrace
+
+    @resolver(_reset)
+    def reset(self, inputs, outputs, preSpike, postSpike, preTrace, postTrace):
+        self.inputs.set(inputs)
+        self.outputs.set(outputs)
+        self.preSpike.set(preSpike)
+        self.postSpike.set(postSpike)
+        self.preTrace.set(preTrace)
+        self.postTrace.set(postTrace)
+
+    def help(self): ## component help function
+        properties = {
+            "synapse_type": "TraceSTDPConvSynapse - performs a synaptic convolution "
+                            "(@) of inputs  to produce output signals; synaptic "
+                            "filters are adjusted via trace-based spike-timing-dependent "
+                            "plasticity (STDP)"
+        }
+        compartment_props = {
+            "input_compartments":
+                {"inputs": "Takes in external input signal values",
+                 "key": "JAX RNG key",
+                 "preSpike": "Pre-synaptic spike compartment value/term for STDP (s_j)",
+                 "postSpike": "Post-synaptic spike compartment value/term for STDP (s_i)",
+                 "preTrace": "Pre-synaptic trace value term for STDP (z_j)",
+                 "postTrace": "Post-synaptic trace value term for STDP (z_i)"},
+            "parameter_compartments":
+                {"filters": "Synaptic filter parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
+                {"outputs": "Output of synaptic/filter transformation",
+                 "dWeights": "Synaptic filter value adjustment 4D-tensor produced at time t",
+                 "dInputs": "Tensor containing back-transmitted signal values; backpropagating pulse"},
+        }
+        hyperparams = {
+            "shape": "Shape of synaptic filter value matrix; `kernel width` x `kernel height` "
+                     "x `number input channels` x `number output channels`",
+            "weight_init": "Initialization conditions for synaptic filter (K) values",
+            "bias_init": "Initialization conditions for bias/base-rate (b) values",
+            "resist_scale": "Resistance level output scaling factor (R)",
+            "A_plus": "Strength of long-term potentiation (LTP)",
+            "A_minus": "Strength of long-term depression (LTD)",
+            "eta": "Global learning rate (multiplier beyond A_plus and A_minus)",
+            "preTrace_target": "Pre-synaptic disconnecting/decay factor (x_tar)",
+            "w_decay": "Synaptic filter decay term",
+            "w_bound": "Soft synaptic bound applied to filters post-update"
+        }
+        info = {self.name: properties,
+                "compartments": compartment_props,
+                "dynamics": "outputs = [K @ inputs] * R + b; "
+                            "dW_{ij}/dt = A_plus * (z_j - x_tar) * s_i - A_minus * s_j * z_i",
+                "hyperparameters": hyperparams}
+        return info
+
diff --git a/ngclearn/components/synapses/convolution/traceSTDPDeconvSynapse.py b/ngclearn/components/synapses/convolution/traceSTDPDeconvSynapse.py
new file mode 100755
index 000000000..095d064a1
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/traceSTDPDeconvSynapse.py
@@ -0,0 +1,223 @@
+from jax import random, numpy as jnp, jit
+from ngclearn import resolver, Component, Compartment
+from .deconvSynapse import DeconvSynapse
+from ngclearn.components.synapses.convolution.ngcconv import (deconv2d, _calc_dX_deconv,
+                                                              _calc_dK_deconv, calc_dX_deconv,
+                                                              calc_dK_deconv)
+
+class TraceSTDPDeconvSynapse(DeconvSynapse): ## trace-based STDP deconvolutional cable
+    """
+    A synaptic deconvolutional (transposed convolutional) cable that adjusts its
+    filter efficacies via a trace-based form of spike-timing-dependent plasticity (STDP).
+
+    | --- Synapse Compartments: ---
+    | inputs - input (takes in external signals)
+    | outputs - output signal (transformation induced by filters)
+    | filters - current value matrix of synaptic filter efficacies
+    | biases - current value vector of synaptic bias values
+    | key - JAX RNG key
+    | --- Synaptic Plasticity Compartments: ---
+    | preSpike - pre-synaptic spike to drive 1st term of STDP update (takes in external signals)
+    | postSpike - post-synaptic spike to drive 2nd term of STDP update (takes in external signals)
+    | preTrace - pre-synaptic trace value to drive 1st term of STDP update (takes in external signals)
+    | postTrace - post-synaptic trace value to drive 2nd term of STDP update (takes in external signals)
+    | dWeights - delta tensor containing changes to be applied to synaptic efficacies
+    | dInputs - delta tensor containing back-transmitted signal values ("backpropagating pulse")
+
+    Args:
+        name: the string name of this cell
+
+        x_size: dimension of input signal (assuming a square input)
+
+        shape: tuple specifying shape of this synaptic cable (usually a 4-tuple
+            with number `filter height x filter width x input channels x number output channels`);
+            note that currently filters/kernels are assumed to be square
+            (kernel.width = kernel.height)
+
+        A_plus: strength of long-term potentiation (LTP)
+
+        A_minus: strength of long-term depression (LTD)
+
+        eta: global learning rate (default: 0)
+
+        pretrace_target: controls degree of pre-synaptic disconnect, i.e., amount of decay
+                 (higher -> lower synaptic values)
+
+        filter_init: a kernel to drive initialization of this synaptic cable's
+            filter values
+
+        bias_init: kernel to drive initialization of bias/base-rate values
+            (Default: None, which turns off/disables biases)
+
+        stride: length/size of stride
+
+        padding: pre-operator padding to use -- "VALID" (none), "SAME"
+
+        resist_scale: aa fixed (resistance) scaling factor to apply to synaptic
+            transform (Default: 1.), i.e., yields: out = ((K @ in) * resist_scale) + b
+            where `@` denotes convolution
+
+        w_bound: maximum weight to softly bound this cable's value matrix to; if
+            set to 0, then no synaptic value bounding will be applied
+
+        w_decay: degree to which (L2) synaptic weight decay is applied to the
+            computed STDP adjustment (Default: 0)
+
+        batch_size: batch size dimension of this component
+    """
+
+    # Define Functions
+    def __init__(self, name, shape, x_size, A_plus, A_minus, eta=0.,
+                 pretrace_target=0., filter_init=None, stride=1, padding=None,
+                 resist_scale=1., w_bound=0., w_decay=0., batch_size=1,
+                 **kwargs):
+        super().__init__(name, shape, x_size=x_size, filter_init=filter_init,
+                         bias_init=None, resist_scale=resist_scale, stride=stride,
+                         padding=padding, batch_size=batch_size, **kwargs)
+
+        self.eta = eta
+        self.w_bounds = w_bound  ## soft weight constraint
+        self.w_decay = w_decay  ## synaptic decay
+        self.eta = eta  ## global learning rate governing plasticity
+        self.pretrace_target = pretrace_target  ## target (pre-synaptic) trace activity value # 0.7
+        self.Aplus = A_plus  ## LTP strength
+        self.Aminus = A_minus  ## LTD strength
+
+        ######################### set up compartments ##########################
+        ## Compartment setup and shape computation
+        self.dWeights = Compartment(self.weights.value * 0)
+        self.dInputs = Compartment(jnp.zeros(self.in_shape))
+        self.preSpike = Compartment(jnp.zeros(self.in_shape))
+        self.preTrace = Compartment(jnp.zeros(self.in_shape))
+        self.postSpike = Compartment(jnp.zeros(self.out_shape))
+        self.postTrace = Compartment(jnp.zeros(self.out_shape))
+
+        ########################################################################
+        ## Shape error correction -- do shape correction inference (for local updates)
+        self._init(self.batch_size, self.x_size, self.shape, self.stride,
+                   self.padding, self.pad_args, self.weights)
+        ########################################################################
+
+    def _init(self, batch_size, x_size, shape, stride, padding, pad_args,
+              weights):
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        _x = jnp.zeros((batch_size, x_size, x_size, n_in_chan))
+        _d = deconv2d(_x, self.weights.value, stride_size=self.stride,
+                      padding=self.padding) * 0
+        _dK = _calc_dK_deconv(_x, _d, stride_size=self.stride, out_size=k_size)
+        ## get filter update correction
+        dx = _dK.shape[0] - self.weights.value.shape[0]
+        dy = _dK.shape[1] - self.weights.value.shape[1]
+        self.delta_shape = (abs(dx), abs(dy))
+
+        ## get input update correction
+        _dx = _calc_dX_deconv(self.weights.value, _d, stride_size=self.stride,
+                       padding=self.padding)
+        dx = (_dx.shape[1] - _x.shape[1])  # abs()
+        dy = (_dx.shape[2] - _x.shape[2])
+        self.x_delta_shape = (dx, dy)
+
+    @staticmethod
+    def _evolve(eta, pretrace_target, Aplus, Aminus, w_decay, w_bounds,
+                shape, stride, padding, delta_shape, preSpike, preTrace, postSpike,
+                postTrace, weights):
+        k_size, k_size, n_in_chan, n_out_chan = shape
+        ## calc dFilters
+        dW_ltp = calc_dK_deconv(preTrace - pretrace_target, postSpike * Aplus,
+                                delta_shape=delta_shape, stride_size=stride,
+                                out_size=k_size, padding=padding)
+        dW_ltd = -calc_dK_deconv(preSpike, postTrace * Aminus,
+                                 delta_shape=delta_shape, stride_size=stride,
+                                 out_size=k_size, padding=padding)
+        dWeights = (dW_ltp + dW_ltd)
+        if w_decay > 0.: ## apply synaptic decay
+            weights = weights + dWeights * eta - weights * w_decay  ## conduct decayed STDP-ascent
+        else:
+            weights = weights + dWeights * eta ## conduct STDP-ascent
+        ## Apply any enforced filter constraints
+        if w_bounds > 0.:
+            ## enforce non-negativity
+            eps = 0.01  # 0.001
+            weights = jnp.clip(weights, eps, w_bounds - eps)
+        return weights, dWeights
+
+    @resolver(_evolve)
+    def evolve(self, weights, dWeights):
+        self.weights.set(weights)
+        self.dWeights.set(dWeights)
+
+    @staticmethod
+    def _backtransmit(stride, padding, x_delta_shape, preSpike, postSpike,
+                      weights):  ## action-backpropagating routine
+        ## calc dInputs
+        dInputs = calc_dX_deconv(weights, postSpike, delta_shape=x_delta_shape,
+                                 stride_size=stride, padding=padding)
+        return dInputs
+
+    @resolver(_backtransmit)
+    def backtransmit(self, dInputs):
+        self.dInputs.set(dInputs)
+
+    @staticmethod
+    def _reset(in_shape, out_shape):
+        preVals = jnp.zeros(in_shape)
+        postVals = jnp.zeros(out_shape)
+        inputs = preVals
+        outputs = postVals
+        preSpike = preVals
+        postSpike = postVals
+        preTrace = preVals
+        postTrace = postVals
+        return inputs, outputs, preSpike, postSpike, preTrace, postTrace
+
+    @resolver(_reset)
+    def reset(self, inputs, outputs, preSpike, postSpike, preTrace, postTrace):
+        self.inputs.set(inputs)
+        self.outputs.set(outputs)
+        self.preSpike.set(preSpike)
+        self.postSpike.set(postSpike)
+        self.preTrace.set(preTrace)
+        self.postTrace.set(postTrace)
+
+    def help(self): ## component help function
+        properties = {
+            "synapse_type": "TraceSTDPDeconvSynapse - performs a synaptic deconvolution "
+                            "(@.T) of inputs to produce output signals; synaptic "
+                            "filters are adjusted via trace-based spike-timing-dependent "
+                            "plasticity (STDP)"
+        }
+        compartment_props = {
+            "input_compartments":
+                {"inputs": "Takes in external input signal values",
+                 "key": "JAX RNG key",
+                 "preSpike": "Pre-synaptic spike compartment value/term for STDP (s_j)",
+                 "postSpike": "Post-synaptic spike compartment value/term for STDP (s_i)",
+                 "preTrace": "Pre-synaptic trace value term for STDP (z_j)",
+                 "postTrace": "Post-synaptic trace value term for STDP (z_i)"},
+            "parameter_compartments":
+                {"filters": "Synaptic filter parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
+                {"outputs": "Output of synaptic/filter transformation",
+                 "dWeights": "Synaptic filter value adjustment 4D-tensor produced at time t",
+                 "dInputs": "Tensor containing back-transmitted signal values; backpropagating pulse"},
+        }
+        hyperparams = {
+            "shape": "Shape of synaptic filter value matrix; `kernel width` x `kernel height` "
+                     "x `number input channels` x `number output channels`",
+            "weight_init": "Initialization conditions for synaptic filter (K) values",
+            "bias_init": "Initialization conditions for bias/base-rate (b) values",
+            "resist_scale": "Resistance level output scaling factor (R)",
+            "A_plus": "Strength of long-term potentiation (LTP)",
+            "A_minus": "Strength of long-term depression (LTD)",
+            "eta": "Global learning rate (multiplier beyond A_plus and A_minus)",
+            "preTrace_target": "Pre-synaptic disconnecting/decay factor (x_tar)",
+            "w_decay": "Synaptic filter decay term",
+            "w_bound": "Soft synaptic bound applied to filters post-update"
+        }
+        info = {self.name: properties,
+                "compartments": compartment_props,
+                "dynamics": "outputs = [K @.T inputs] * R + b; "
+                            "dW_{ij}/dt = A_plus * (z_j - x_tar) * s_i - A_minus * s_j * z_i",
+                "hyperparameters": hyperparams}
+        return info
diff --git a/ngclearn/components/synapses/denseSynapse.py b/ngclearn/components/synapses/denseSynapse.py
index dd0ce7965..f80a73f0e 100755
--- a/ngclearn/components/synapses/denseSynapse.py
+++ b/ngclearn/components/synapses/denseSynapse.py
@@ -26,7 +26,7 @@ def compute_layer(inp, weight, biases=0., Rscale=1.):
     """
     return jnp.matmul(inp, weight * Rscale) + biases
 
-class DenseSynapse(JaxComponent): ## static non-learnable synaptic cable
+class DenseSynapse(JaxComponent): ## base dense synaptic cable
     """
     A dense synaptic cable; no form of synaptic evolution/adaptation
     is in-built to this component.
@@ -96,7 +96,7 @@ def __init__(self, name, shape, weight_init=None, bias_init=None,
                                   if bias_init else 0.0)
 
     @staticmethod
-    def _advance_state(t, dt, Rscale, inputs, weights, biases):
+    def _advance_state(Rscale, inputs, weights, biases):
         outputs = compute_layer(inputs, weights, biases, Rscale)
         return outputs
 
@@ -134,14 +134,18 @@ def load(self, directory, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "DenseSynapse - performs a synaptic transformation of inputs to produce "
-                         "output signals (e.g., a scaled linear multivariate transformation)"
+            "synapse_type": "DenseSynapse - performs a synaptic transformation "
+                            "of inputs to produce  output signals (e.g., a "
+                            "scaled linear multivariate transformation)"
         }
         compartment_props = {
             "input_compartments":
                 {"inputs": "Takes in external input signal values",
                  "key": "JAX RNG key"},
-            "outputs_compartments":
+            "parameter_compartments":
+                {"weights": "Synapse efficacy/strength parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
                 {"outputs": "Output of synaptic transformation"},
         }
         hyperparams = {
diff --git a/ngclearn/components/synapses/hebbian/BCMSynapse.py b/ngclearn/components/synapses/hebbian/BCMSynapse.py
index 50168a7bb..1d5c8766d 100755
--- a/ngclearn/components/synapses/hebbian/BCMSynapse.py
+++ b/ngclearn/components/synapses/hebbian/BCMSynapse.py
@@ -33,28 +33,47 @@ def evolve(dt, pre, post, theta, W, tau_w, tau_theta, w_bound=0., w_decay=0.):
         the newly evolved synaptic threshold variables,
         the synaptic update matrix
     """
-    post_term = post - theta
+    eps = 1e-7
+    #post_term = post * (post - theta) # post - theta
+    #theta = jnp.mean(post * post, axis=1, keepdims=True)
+    post_term = post * (post - theta) # post - theta
+    post_term = post_term * (1. / (theta + eps))
     dW = jnp.matmul(pre.T, post_term)
     if w_bound > 0.:
         dW = dW * (w_bound - jnp.abs(W))
-    _W = W + (-W * w_decay + dW) * dt/tau_w
-    _theta = theta + (-theta + jnp.square(post)) * dt/tau_theta
-    return _W, _theta, dW
+    ## update synaptic efficacies according to a leaky ODE
+    dW = -W * w_decay + dW
+    _W = W + dW * dt/tau_w
+    ## update synaptic modification threshold as a leaky ODE
+    dtheta = jnp.mean(jnp.square(post), axis=0, keepdims=True) ## batch avg
+    _theta = theta + (-theta + dtheta) * dt/tau_theta
+    return _W, _theta, dW, post_term
 
 class BCMSynapse(DenseSynapse): # BCM-adjusted synaptic cable
     """
     A synaptic cable that adjusts its efficacies in accordance with BCM
     (Bienenstock-Cooper-Munro) theory.
 
+    Mathematically, a synaptic update performed according to BCM theory is:
+    | tau_w d(W_{ij})/dt = -w_decay W_{ij} + x_j * [y_i * (y_i - theta_i)] / theta_i
+    | tau_theta d(theta_i)/dt = -theta_i + <(y_i)^2>_{batch}
+    | where x_j is the pre-synaptic input, y_i is the post-synaptic output
+
+    Note that, in most literature related to BCM, the average value used for
+    threshold `theta` can be assumed to be the average over all input patterns
+    (as in a full dataset batch update) but a temporal average maintained for
+    `theta` will "usually be equivalent" (and ngc-learn implements the threshold
+    `theta` in terms of a leaky ODE to dynamically compute the temporal mean).
+
     | --- Synapse Compartments: ---
     | inputs - input (takes in external signals)
     | outputs - output signal (transformation induced by synapses)
     | weights - current value matrix of synaptic efficacies
     | key - JAX RNG key
     | --- Synaptic Plasticity Compartments: ---
-    | pre - pre-synaptic spike to drive 1st term of BCM update (takes in external signals)
-    | post - post-synaptic spike to drive 2nd term of BCM update (takes in external signals)
-    | theta - synaptic threshold (post-synaptic) variables
+    | pre - pre-synaptic signal/value to drive 1st term of BCM update (x)
+    | post - post-synaptic signal/value to drive 2nd term of BCM update (y)
+    | theta - synaptic modification threshold (post-synaptic) variables
     | dWeights - current delta matrix containing changes to be applied to synapses
 
     | References:
@@ -72,6 +91,8 @@ class BCMSynapse(DenseSynapse): # BCM-adjusted synaptic cable
 
         tau_theta: threshold variable evolution time constant
 
+        theta0: initial condition for synaptic modification threshold
+
         w_bound: maximum value to enforce over newly computed efficacies
             (default: 0.); must > 0. to be used
 
@@ -89,7 +110,7 @@ class BCMSynapse(DenseSynapse): # BCM-adjusted synaptic cable
     """
 
     # Define Functions
-    def __init__(self, name, shape, tau_w, tau_theta, w_bound=0., w_decay=0.,
+    def __init__(self, name, shape, tau_w, tau_theta, theta0=-1., w_bound=0., w_decay=0.,
                  weight_init=None, resist_scale=1., p_conn=1., **kwargs):
         super().__init__(name, shape, weight_init, None, resist_scale, p_conn, **kwargs)
 
@@ -100,7 +121,7 @@ def __init__(self, name, shape, tau_w, tau_theta, w_bound=0., w_decay=0.,
         self.w_decay = w_decay ## synaptic decay factor
         self.w_bound = w_bound  ## soft weight constraint
         self.Rscale = resist_scale ## post-transformation scale factor
-        self.theta0 = -1. ## initial condition for theta/threshold variables
+        self.theta0 = theta0 #-1. ## initial condition for theta/threshold variables
 
         self.batch_size = 1
         ## Compartment setup
@@ -108,20 +129,22 @@ def __init__(self, name, shape, tau_w, tau_theta, w_bound=0., w_decay=0.,
         postVals = jnp.zeros((self.batch_size, shape[1]))
         self.pre = Compartment(preVals) ## pre-synaptic statistic
         self.post = Compartment(postVals) ## post-synaptic statistic
-        self.theta = Compartment(postVals + self.theta0) ## synaptic threshold variables
+        self.post_term = Compartment(postVals)
+        self.theta = Compartment(postVals + self.theta0) ## synaptic modification thresholds
         self.dWeights = Compartment(self.weights.value * 0)
 
     @staticmethod
     def _evolve(t, dt, tau_w, tau_theta, w_bound, w_decay, pre, post, theta, weights):
-        weights, theta, dWeights = evolve(dt, pre, post, theta, weights, tau_w,
-                                          tau_theta, w_bound, w_decay)
-        return weights, theta, dWeights
+        weights, theta, dWeights, post_term = evolve(dt, pre, post, theta, weights, tau_w,
+                                                     tau_theta, w_bound, w_decay)
+        return weights, theta, dWeights, post_term
 
     @resolver(_evolve)
-    def evolve(self, weights, theta, dWeights):
+    def evolve(self, weights, theta, dWeights, post_term):
         self.weights.set(weights)
         self.theta.set(theta)
         self.dWeights.set(dWeights)
+        self.post_term.set(post_term)
 
     @staticmethod
     def _reset(batch_size, shape):
@@ -132,15 +155,17 @@ def _reset(batch_size, shape):
         pre = preVals
         post = postVals
         dWeights = jnp.zeros(shape)
-        return inputs, outputs, pre, post, dWeights
+        post_term = postVals
+        return inputs, outputs, pre, post, dWeights, post_term
 
     @resolver(_reset)
-    def reset(self, inputs, outputs, pre, post, dWeights):
+    def reset(self, inputs, outputs, pre, post, dWeights, post_term):
         self.inputs.set(inputs)
         self.outputs.set(outputs)
         self.pre.set(pre)
         self.post.set(post)
         self.dWeights.set(dWeights)
+        self.post_term.set(post_term)
 
     def save(self, directory, **kwargs):
         file_name = directory + "/" + self.name + ".npz"
@@ -155,9 +180,9 @@ def load(self, directory, **kwargs):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "BCMSTDPSynapse - performs an adaptable synaptic transformation "
-                         "of inputs to produce output signals; synapses are adjusted via "
-                         "BCM theory"
+            "synapse_type": "BCMSTDPSynapse - performs an adaptable synaptic "
+                            "transformation  of inputs to produce output signals; "
+                            "synapses are adjusted via BCM theory"
         }
         compartment_props = {
             "input_compartments":
@@ -165,9 +190,12 @@ def help(self): ## component help function
                  "key": "JAX RNG key",
                  "pre": "Pre-synaptic statistic for BCM (z_j)",
                  "post": "Post-synaptic statistic for BCM (z_i)"},
-            "outputs_compartments":
+            "parameter_compartments":
+                {"weights": "Synapse efficacy/strength parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
                 {"outputs": "Output of synaptic transformation",
-                 "theta": "Synaptic threshold variable",
+                 "theta": "Synaptic modification threshold variable",
                  "dWeights": "Synaptic weight value adjustment matrix produced at time t"},
         }
         hyperparams = {
diff --git a/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py b/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py
index c15c80d67..5c5d3150e 100755
--- a/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py
+++ b/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py
@@ -95,7 +95,7 @@ def __init__(self, name, shape, eta, lmbda=0.01, w_bound=1.,
         self.dWeights = Compartment(self.weights.value * 0)
 
     @staticmethod
-    def _evolve(t, dt, eta, lmbda, w_bound, preSpike, postSpike, weights):
+    def _evolve(eta, lmbda, w_bound, preSpike, postSpike, weights):
         weights, dWeights = evolve(preSpike, postSpike, weights, eta, lmbda, w_bound)
         return weights, dWeights
 
@@ -125,9 +125,10 @@ def reset(self, inputs, outputs, preSpike, postSpike, dWeights):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "EventSTDPSynapse - performs an adaptable synaptic transformation "
-                         "of inputs to produce output signals; synapses are adjusted with "
-                         "event-based post-synaptic spike-timing-dependent plasticity"
+            "synapse_type": "EventSTDPSynapse - performs an adaptable synaptic "
+                            "transformation of inputs to produce output signals; "
+                            "synapses are adjusted with event-based post-synaptic "
+                            "spike-timing-dependent plasticity (STDP)"
         }
         compartment_props = {
             "input_compartments":
@@ -135,7 +136,10 @@ def help(self): ## component help function
                  "key": "JAX RNG key",
                  "preSpike": "Pre-synaptic spike compartment value/term for STDP (s_j)",
                  "postSpike": "Post-synaptic spike compartment value/term for STDP (s_i)"},
-            "outputs_compartments":
+            "parameter_compartments":
+                {"weights": "Synapse efficacy/strength parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
                 {"outputs": "Output of synaptic transformation",
                  "dWeights": "Synaptic weight value adjustment matrix produced at time t"},
         }
diff --git a/ngclearn/components/synapses/hebbian/expSTDPSynapse.py b/ngclearn/components/synapses/hebbian/expSTDPSynapse.py
index f7fdbbce8..d8afa19f5 100644
--- a/ngclearn/components/synapses/hebbian/expSTDPSynapse.py
+++ b/ngclearn/components/synapses/hebbian/expSTDPSynapse.py
@@ -137,7 +137,7 @@ def __init__(self, name, shape, A_plus, A_minus, exp_beta, eta=1.,
         self.dWeights = Compartment(self.weights.value * 0)
 
     @staticmethod
-    def _evolve(t, dt, w_bound, eta, preTrace_target, exp_beta, Aplus, Aminus,
+    def _evolve(dt, w_bound, eta, preTrace_target, exp_beta, Aplus, Aminus,
                     preSpike, postSpike, preTrace, postTrace, weights):
         weights, dW = evolve(dt, preSpike, preTrace, postSpike, postTrace, weights,
                              w_bound=w_bound, eta=eta, x_tar=preTrace_target,
@@ -174,9 +174,10 @@ def reset(self, inputs, outputs, preSpike, postSpike, preTrace, postTrace, dWeig
 
     def help(self): ## component help function
         properties = {
-            "cell type": "ExpSTDPSynapse - performs an adaptable synaptic transformation "
-                         "of inputs to produce output signals; synapses are adjusted with "
-                         "exponential trace-based spike-timing-dependent plasticity"
+            "synapse_type": "ExpSTDPSynapse - performs an adaptable synaptic "
+                            "transformation of inputs to produce output signals; "
+                            "synapses are adjusted with exponential trace-based "
+                            "spike-timing-dependent plasticity (STDP)"
         }
         compartment_props = {
             "input_compartments":
@@ -186,7 +187,10 @@ def help(self): ## component help function
                  "postSpike": "Post-synaptic spike compartment value/term for STDP (s_i)",
                  "preTrace": "Pre-synaptic trace value term for STDP (z_j)",
                  "postTrace": "Post-synaptic trace value term for STDP (z_i)"},
-            "outputs_compartments":
+            "parameter_compartments":
+                {"weights": "Synapse efficacy/strength parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
                 {"outputs": "Output of synaptic transformation",
                  "dWeights": "Synaptic weight value adjustment matrix produced at time t"},
         }
diff --git a/ngclearn/components/synapses/hebbian/hebbianSynapse.py b/ngclearn/components/synapses/hebbian/hebbianSynapse.py
index a4585cfc2..2714f372c 100644
--- a/ngclearn/components/synapses/hebbian/hebbianSynapse.py
+++ b/ngclearn/components/synapses/hebbian/hebbianSynapse.py
@@ -83,7 +83,7 @@ class HebbianSynapse(DenseSynapse):
     | pre - pre-synaptic signal to drive first term of Hebbian update (takes in external signals)
     | post - post-synaptic signal to drive 2nd term of Hebbian update (takes in external signals)
     | dW - current delta matrix containing changes to be applied to synaptic efficacies
-    | db - current delta vector containing changes to be applied to bias value
+    | db - current delta vector containing changes to be applied to bias values
     | opt_params - locally-embedded optimizer statisticis (e.g., Adam 1st/2nd moments if adam is used)
 
     Args:
@@ -167,12 +167,14 @@ def __init__(self, name, shape, eta=0., weight_init=None, bias_init=None,
         self.dW = Compartment(jnp.zeros(shape))
         self.db = Compartment(jnp.zeros(shape[1]))
 
-        key, subkey = random.split(self.key.value)
-        self.opt_params = Compartment(get_opt_init_fn(optim_type)([self.weights.value, self.biases.value] if bias_init else [self.weights.value]))
+        #key, subkey = random.split(self.key.value)
+        self.opt_params = Compartment(get_opt_init_fn(optim_type)(
+            [self.weights.value, self.biases.value]
+            if bias_init else [self.weights.value]))
 
     @staticmethod
-    def _evolve(t, dt, opt, w_bounds, is_nonnegative, sign_value, w_decay, pre_wght,
-                post_wght, bias_init, pre, post, weights, biases, dW, db, opt_params):
+    def _evolve(opt, w_bounds, is_nonnegative, sign_value, w_decay, pre_wght,
+                post_wght, bias_init, pre, post, weights, biases, opt_params):
         ## calculate synaptic update values
         dW, db = calc_update(pre, post,
                              weights, w_bounds, is_nonnegative=is_nonnegative,
@@ -195,7 +197,7 @@ def evolve(self, opt_params, weights, biases):
         self.biases.set(biases)
 
     @staticmethod
-    def _reset(batch_size, shape, weight_init, bias_init):
+    def _reset(batch_size, shape):
         preVals = jnp.zeros((batch_size, shape[0]))
         postVals = jnp.zeros((batch_size, shape[1]))
         return (
@@ -209,20 +211,23 @@ def _reset(batch_size, shape, weight_init, bias_init):
 
     def help(self): ## component help function
         properties = {
-            "cell type": "HebbianSynapse - performs an adaptable synaptic transformation "
-                         "of inputs to produce output signals; synapses are adjusted via "
-                         "two-term/factor Hebbian adjustment"
+            "synapse_type": "HebbianSynapse - performs an adaptable synaptic "
+                            "transformation of inputs to produce output signals; "
+                            "synapses are adjusted via two-term/factor Hebbian adjustment"
         }
         compartment_props = {
             "input_compartments":
                 {"inputs": "Takes in external input signal values",
                  "key": "JAX RNG key",
-                 "pre": "Pre-synaptic statistic for BCM (z_j)",
-                 "post": "Post-synaptic statistic for BCM (z_i)"},
-            "outputs_compartments":
+                 "pre": "Pre-synaptic statistic for Hebb rule (z_j)",
+                 "post": "Post-synaptic statistic for Hebb rule (z_i)"},
+            "parameter_compartments":
+                {"weights": "Synapse efficacy/strength parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
                 {"outputs": "Output of synaptic transformation",
-                 "theta": "Synaptic threshold variable",
-                 "dWeights": "Synaptic weight value adjustment matrix produced at time t"},
+                 "dWeights": "Synaptic weight value adjustment matrix produced at time t",
+                 "dBiases": "Synaptic bias/base-rate value adjustment vector produced at time t"},
         }
         hyperparams = {
             "shape": "Shape of synaptic weight value matrix; number inputs x number outputs",
@@ -232,8 +237,8 @@ def help(self): ## component help function
             "p_conn": "Probability of a connection existing (otherwise, it is masked to zero)",
             "is_nonnegative": "Should synapses be constrained to be non-negative post-updates?",
             "sign_value": "Scalar `flipping` constant -- changes direction to Hebbian descent if < 0",
-            "pre_wght" : "Pre-synaptic weighting coefficient (q_pre)",
-            "post_wght" : "Post-synaptic weighting coefficient (q_post)",
+            "pre_wght": "Pre-synaptic weighting coefficient (q_pre)",
+            "post_wght": "Post-synaptic weighting coefficient (q_post)",
             "w_bound": "Soft synaptic bound applied to synapses post-update",
             "w_decay": "Synaptic decay term"
         }
diff --git a/ngclearn/components/synapses/hebbian/traceSTDPSynapse.py b/ngclearn/components/synapses/hebbian/traceSTDPSynapse.py
index 1ea15fb67..6c45553f8 100755
--- a/ngclearn/components/synapses/hebbian/traceSTDPSynapse.py
+++ b/ngclearn/components/synapses/hebbian/traceSTDPSynapse.py
@@ -142,7 +142,7 @@ def __init__(self, name, shape, A_plus, A_minus, eta=1., mu=0.,
         self.dWeights = Compartment(self.weights.value * 0)
 
     @staticmethod
-    def _evolve(t, dt, w_bound, eta, preTrace_target, mu, Aplus, Aminus,
+    def _evolve(dt, w_bound, eta, preTrace_target, mu, Aplus, Aminus,
                 preSpike, postSpike, preTrace, postTrace, weights):
         weights, dW = evolve(dt, preSpike, preTrace, postSpike, postTrace, weights,
                              w_bound=w_bound, eta=eta, x_tar=preTrace_target, mu=mu,
@@ -179,9 +179,10 @@ def reset(self, inputs, outputs, preSpike, postSpike, preTrace, postTrace, dWeig
 
     def help(self): ## component help function
         properties = {
-            "cell type": "TraceSTDPSynapse - performs an adaptable synaptic transformation "
-                         "of inputs to produce output signals; synapses are adjusted with "
-                         "trace-based spike-timing-dependent plasticity"
+            "synapse_type": "TraceSTDPSynapse - performs an adaptable synaptic "
+                            "transformation of inputs to produce output signals; "
+                            "synapses are adjusted with trace-based "
+                            "spike-timing-dependent plasticity (STDP)"
         }
         compartment_props = {
             "input_compartments":
@@ -191,7 +192,10 @@ def help(self): ## component help function
                  "postSpike": "Post-synaptic spike compartment value/term for STDP (s_i)",
                  "preTrace": "Pre-synaptic trace value term for STDP (z_j)",
                  "postTrace": "Post-synaptic trace value term for STDP (z_i)"},
-            "outputs_compartments":
+            "parameter_compartments":
+                {"weights": "Synapse efficacy/strength parameter values",
+                 "biases": "Base-rate/bias parameter values"},
+            "output_compartments":
                 {"outputs": "Output of synaptic transformation",
                  "dWeights": "Synaptic weight value adjustment matrix produced at time t"},
         }
diff --git a/ngclearn/utils/metric_utils.py b/ngclearn/utils/metric_utils.py
index 5c7d63b34..f48472f84 100755
--- a/ngclearn/utils/metric_utils.py
+++ b/ngclearn/utils/metric_utils.py
@@ -4,6 +4,7 @@
 """
 from jax import numpy as jnp, jit
 from functools import partial
+from sklearn.metrics import confusion_matrix, precision_score, recall_score
 
 @partial(jit, static_argnums=[1])
 def measure_fanoFactor(spikes, preserve_batch=False):
@@ -25,7 +26,7 @@ def measure_fanoFactor(spikes, preserve_batch=False):
     mu = jnp.mean(spikes, axis=0, keepdims=True)
     sigSqr = jnp.square(jnp.std(spikes, axis=0, keepdims=True))
     fano = sigSqr/mu
-    if preserve_batch == False:
+    if preserve_batch is False:
         fano = jnp.mean(fano)
     return fano
 
@@ -48,10 +49,39 @@ def measure_firingRate(spikes, preserve_batch=False):
     counts = jnp.sum(spikes, axis=0, keepdims=True)
     T = spikes.shape[0] * 1.
     fireRates = counts/T
-    if preserve_batch == False:
+    if preserve_batch is False:
         fireRates = jnp.mean(fireRates)
     return fireRates
 
+@partial(jit, static_argnums=[1])
+def measure_breadth_TC(spikes, preserve_batch=False):
+    """
+    Calculates the breath tuning curve (BTC) of a group of neurons given full
+    spike train.(s). BTC measures the neural selectivity such that the
+    sparse code distribution concentrates near zero with a heavy tail. For a
+    neural layer where most of the neurons fire, the activity distribution is
+    more uniformly spread and BTC > 0.5. When most of the neurons do not fire,
+    the firing distribution is peaked at zero and BTC < 0.5.
+
+    Args:
+        spikes: full spike train matrix; shape is (T x D) where D is number of
+            neurons in a group/cluster
+
+        preserve_batch: if True, will return one score per sample in batch
+            (Default: False), otherwise, returns scalar average score
+
+    Returns:
+        a 1 x D Fano factor vector (one factor per neuron) OR a single
+        average Fano factor across the neuronal group
+    """
+    mu = jnp.mean(spikes, axis=0, keepdims=True)
+    sigSqr = jnp.square(jnp.std(spikes, axis=0, keepdims=True))
+    C = sigSqr/mu
+    BTC = 1./(1 + jnp.square(C))
+    if preserve_batch is False:
+        BTC = jnp.mean(BTC)
+    return BTC
+
 @jit
 def measure_sparsity(codes, tolerance=0.):
     """
@@ -71,6 +101,46 @@ def measure_sparsity(codes, tolerance=0.):
     rho = jnp.sum(m, axis=1, keepdims=True)/(codes.shape[1] * 1.)
     return rho
 
+#@partial(jit, static_argnums=[2])
+def analyze_scores(mu, y, extract_label_indx=True): ## examines classifcation statistics
+    """
+        Analyzes a set of prediction matrix and target/ground-truth matrix or vector.
+
+        Args:
+            mu: prediction (design) matrix; shape is (N x C) where C is number of classes
+                and N is the number of patterns examined
+
+            y: target / ground-truth (design) matrix; shape is (N x C) OR an array
+                of class integers of length N (with "extract_label_indx = True")
+
+            extract_label_indx: run an argmax to pull class integer indices from
+                "y", assuming y is a one-hot binary encoding matrix (Default: True),
+                otherwise, this assumes "y" is an array of class integer indices
+                of length N
+
+        Returns:
+            confusion matrix, precision, recall, misses (empty predictions/all-zero rows),
+            accuracy, adjusted-accuracy (counts all misses as incorrect)
+        """
+    miss_mask = (jnp.sum(mu, axis=1, keepdims=True) == 0.) * 1.
+    misses = jnp.sum(miss_mask) ## how many misses?
+    labels = y
+    if extract_label_indx:
+        labels = jnp.argmax(y, axis=1)
+    guesses = jnp.argmax(mu, axis=1)
+    conf_matrix = confusion_matrix(labels, guesses)
+    precision = precision_score(labels, guesses, average='macro')
+    recall = recall_score(labels, guesses, average='macro')
+    ## produce accuracy score measurements
+    guess = jnp.argmax(mu, axis=1) ## gather all model/output guesses
+    equality_mask = jnp.equal(guess, labels)
+    ### compute raw accuracy
+    acc = jnp.sum(equality_mask) / (y.shape[0] * 1.)
+    ### compute hit-masked accuracy (adjusted accuracy
+    adj_acc = jnp.sum(equality_mask * (1. - miss_mask)) / (y.shape[0] * 1.)
+    ## output analysis statistics
+    return conf_matrix, precision, recall, misses, acc, adj_acc
+
 @partial(jit, static_argnums=[2])
 def measure_ACC(mu, y, extract_label_indx=True): ## measures/calculates accuracy
     """
@@ -92,7 +162,7 @@ def measure_ACC(mu, y, extract_label_indx=True): ## measures/calculates accuracy
         scalar accuracy score
     """
     guess = jnp.argmax(mu, axis=1)
-    if extract_label_indx == True:
+    if extract_label_indx:
         lab = jnp.argmax(y, axis=1)
     acc = jnp.sum( jnp.equal(guess, lab) )/(y.shape[0] * 1.)
     return acc
@@ -129,7 +199,7 @@ def measure_KLD(p_xHat, p_x, preserve_batch=False):
     term1 = jnp.sum(_p_x * jnp.log(_p_x), axis=1, keepdims=True) # * (1/N)
     term2 = -jnp.sum(_p_x * jnp.log(_p_xHat), axis=1, keepdims=True) # * (1/N)
     kld = (term1 + term2) * (1/N)
-    if preserve_batch == False:
+    if preserve_batch is False:
         kld = jnp.mean(kld)
     return kld
 
@@ -156,7 +226,7 @@ def measure_CatNLL(p, x, offset=1e-7, preserve_batch=False):
     p_ = jnp.clip(p, offset, 1.0 - offset)
     loss = -(x * jnp.log(p_))
     nll = jnp.sum(loss, axis=1, keepdims=True) #/(y_true.shape[0] * 1.0)
-    if preserve_batch == False:
+    if preserve_batch is False:
         nll = jnp.mean(nll)
     return nll #tf.reduce_mean(nll)
 
@@ -181,7 +251,7 @@ def measure_MSE(mu, x, preserve_batch=False):
     diff = mu - x
     se = jnp.square(diff) ## squared error
     mse = jnp.sum(se, axis=1, keepdims=True) # technically se at this point
-    if preserve_batch == False:
+    if preserve_batch is False:
         mse = jnp.mean(mse) # this is proper mse
     return mse
 
@@ -207,6 +277,6 @@ def measure_BCE(p, x, offset=1e-7, preserve_batch=False): #1e-10
     """
     p_ = jnp.clip(p, offset, 1 - offset)
     bce = -jnp.sum(x * jnp.log(p_) + (1.0 - x) * jnp.log(1.0 - p_),axis=1, keepdims=True)
-    if preserve_batch == False:
+    if preserve_batch is False:
         bce = jnp.mean(bce)
     return bce
diff --git a/ngclearn/utils/viz/dim_reduce.py b/ngclearn/utils/viz/dim_reduce.py
index 340805b79..6a1c22e70 100755
--- a/ngclearn/utils/viz/dim_reduce.py
+++ b/ngclearn/utils/viz/dim_reduce.py
@@ -1,5 +1,4 @@
-import matplotlib #.pyplot as plt
-matplotlib.use('Agg')
+import matplotlib
 import matplotlib.pyplot as plt
 cmap = plt.cm.jet
 
@@ -81,7 +80,12 @@ def plot_latents(code_vectors, labels, plot_fname="2Dcode_plot.jpg", alpha=1.):
 
         plot_fname: /path/to/plot_fname.<suffix> for saving the plot to disk
     """
+    curr_backend = plt.rcParams["backend"]
+    matplotlib.use('Agg') ## temporarily go in Agg plt backend for tsne plotting
     print(" > Plotting 2D latent encodings...")
+    curr_backend = plt.rcParams["backend"]
+    matplotlib.use(
+        'Agg')  ## temporarily go in Agg plt backend for tsne plotting
     lab = labels
     if lab.shape[1] > 1: ## extract integer class labels from a one-hot matrix
         lab = np.argmax(lab, 1)
@@ -91,3 +95,4 @@ def plot_latents(code_vectors, labels, plot_fname="2Dcode_plot.jpg", alpha=1.):
     plt.grid()
     plt.savefig("{0}".format(plot_fname), dpi=300)
     plt.clf()
+    matplotlib.use(curr_backend) ## return back to auto-selected plt backend for system
diff --git a/ngclearn/utils/viz/raster.py b/ngclearn/utils/viz/raster.py
index ac7a17bba..4ae3be236 100755
--- a/ngclearn/utils/viz/raster.py
+++ b/ngclearn/utils/viz/raster.py
@@ -1,9 +1,6 @@
 """
 Raster visualization functions/utilities.
 """
-import math
-import random
-#import tensorflow as tf
 import matplotlib.pyplot as plt
 from matplotlib import gridspec
 import numpy as np
@@ -58,22 +55,23 @@ def create_raster_plot(spike_train, ax=None, s=0.5, c="black",
         # plt.ylabel("Neuron Index")
         save = True
 
-    ax = ax if ax is not None else plt
+    _ax = ax if ax is not None else plt
 
     events = []
     for t in range(n_count):
         if indices is None or t in indices:
             e = spike_train[t,:].nonzero()
             events.append(e[0])
-    ax.eventplot(events, linelengths=s, colors=c)
-    ax.yticks(ticks=[i for i in (range(n_count if indices is None else len(indices)))],
-              labels=["N" + str(i) for i in (range(n_count) if indices is None else indices)])
-    ax.xticks(ticks=[i for i in range(0, step_count+1, max(int(step_count / 5), 1))])
+    _ax.eventplot(events, linelengths=s, colors=c)
+    if ax is None:
+        _ax.yticks(ticks=[i for i in (range(n_count if indices is None else len(indices)))],
+                  labels=["N" + str(i) for i in (range(n_count) if indices is None else indices)])
+        _ax.xticks(ticks=[i for i in range(0, step_count+1, max(int(step_count / 5), 1))])
 
     if save:
-        ax.savefig(plot_fname)
-        ax.clf()
-        ax.close()
+        _ax.savefig(plot_fname)
+        _ax.clf()
+        _ax.close()
         plt.close()
 
 def create_overlay_raster_plot(spike_train, targ_train, Y, idxs, s=1.5, c="black", marker="|",
diff --git a/ngclearn/utils/viz/spike_plot.py b/ngclearn/utils/viz/spike_plot.py
index 1753a569f..a449a7efe 100644
--- a/ngclearn/utils/viz/spike_plot.py
+++ b/ngclearn/utils/viz/spike_plot.py
@@ -1,8 +1,5 @@
-import matplotlib #.pyplot as plt
-matplotlib.use('Agg')
 import matplotlib.pyplot as plt
 cmap = plt.cm.jet
-import jax.numpy as jnp
 import numpy as np
 
 def plot_spiking_neuron(curr, mem, spike, ref, dt, thr_line=False,
diff --git a/ngclearn/utils/viz/synapse_plot.py b/ngclearn/utils/viz/synapse_plot.py
index 13d4483e6..9c4a0e49f 100644
--- a/ngclearn/utils/viz/synapse_plot.py
+++ b/ngclearn/utils/viz/synapse_plot.py
@@ -2,8 +2,6 @@
 Synaptic/receptive field visualization functions/utilities.
 """
 import math
-import random
-#import tensorflow as tf
 import matplotlib.pyplot as plt
 from matplotlib import gridspec
 import numpy as np
diff --git a/ngclearn/utils/weight_distribution.py b/ngclearn/utils/weight_distribution.py
index 705f7b141..9c3b3c891 100755
--- a/ngclearn/utils/weight_distribution.py
+++ b/ngclearn/utils/weight_distribution.py
@@ -4,7 +4,7 @@
 """
 import numpy as np
 import jax
-from jax import numpy as jnp, jit, vmap, lax, nn
+from jax import numpy as jnp, jit, vmap, lax, nn, random
 from ngcsimlib.logger import critical
 
 ################################################################################
@@ -116,7 +116,9 @@ def initialize_params(dkey, init_kernel, shape, use_numpy=False):
                 "amin" (clip weights values to be >= amin);
                 "amax" (clip weights values to be <= amin);
                 "hollow" (zero out values along main diagonal);
-                "eye" (zero out off-diagonal values)
+                "eye" (zero out off-diagonal values);
+                "n_row_active" (keep only n random rows non-masked/zero);
+                "n_col_active" (keep only n random columns non-masked/zero)
 
         shape: tuple containing the dimensions/shape of the tensor to initialize
 
@@ -180,6 +182,9 @@ def initialize_params(dkey, init_kernel, shape, use_numpy=False):
     clip_max = _init_kernel.get("amax")
     is_hollow = _init_kernel.get("hollow", False)
     is_eye = _init_kernel.get("eye", False)
+    n_row_active = _init_kernel.get("n_row_active", None)
+    n_col_active = _init_kernel.get("n_col_active", None)
+    ## run any configured post-processing to condition the final value matrix
     if clip_min is not None: ## bound all values to be > clip_min
         if use_numpy:
             params = np.maximum(params, clip_min)
@@ -200,5 +205,16 @@ def initialize_params(dkey, init_kernel, shape, use_numpy=False):
             params = np.eye(N=shape[0], M=shape[1]) * params
         else:
             params = jnp.eye(N=shape[0], M=shape[1]) * params
+    if n_row_active is not None:  ## keep only n rows active (rest are zero)
+        row_ind = random.permutation(dkey, shape[0])[0:n_row_active]
+        mask = jnp.zeros(shape)
+        mask = mask.at[row_ind, :].set(jnp.ones((shape[0], 1))) ## only set keep rows to ones
+        params = params * mask
+    if n_col_active is not None:  ## keep only n cols active (rest are zero)
+        row_col = random.permutation(dkey, shape[1])[0:n_col_active]
+        mask = jnp.zeros(shape)
+        mask = mask.at[:, row_col].set(jnp.zeros((1, shape[0]))) ## only set keep cols to ones
+        params = params * mask
+
     return params ## return initial distribution conditions
 


Test patch:
diff --git a/docs/images/demo3/test_acc_output.png b/docs/images/demo3/test_acc_output.png
deleted file mode 100644
index c0526af47..000000000
Binary files a/docs/images/demo3/test_acc_output.png and /dev/null differ