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

Issue:
Nudged over dev minor revision to prep for pip update
Nudging version number to prep for new pip update/release


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 4d13d3038..b38f4d6a4 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ ngc-learn requires:
 1) Python (>=3.10)
 2) NumPy (>=1.26.0)
 3) SciPy (>=1.7.0)
-4) ngcsimlib (>=0.2.b1), (visit official page <a href="https://github.com/NACLab/ngc-sim-lib">here</a>)
+4) ngcsimlib (>=0.3.b2), (visit official page <a href="https://github.com/NACLab/ngc-sim-lib">here</a>)
 5) JAX (>= 0.4.18) (to enable GPU use, make sure to install one of the CUDA variants)
 <!--
 5) scikit-learn (>=1.3.1) if using `ngclearn.utils.density`
@@ -46,13 +46,11 @@ ngc-learn requires:
 -->
 
 ---
-ngc-learn 1.1.beta2 and later require Python 3.10 or newer as well as ngcsimlib >=0.3.b1.
+ngc-learn 1.2.beta0 and later require Python 3.10 or newer as well as ngcsimlib >=0.3.b3.
 ngc-learn's plotting capabilities (routines within `ngclearn.utils.viz`) require
 Matplotlib (>=3.8.0) and imageio (>=2.31.5) and both plotting and density estimation
 tools (routines within ``ngclearn.utils.density``) will require Scikit-learn (>=0.24.2).
 Many of the tutorials will require Matplotlib (>=3.8.0), imageio (>=2.31.5), and Scikit-learn (>=0.24.2).
-<!-- (Note: if using the `_generate_patch_set()` within the
-image patching utilities, then Patchify will be needed).-->
 
 ### User Installation
 
@@ -81,7 +79,7 @@ Python 3.11.4 (main, MONTH  DAY YEAR, TIME) [GCC XX.X.X] on linux
 Type "help", "copyright", "credits" or "license" for more information.
 >>> import ngclearn
 >>> ngclearn.__version__
-'1.1b2'
+'1.2b0'
 ```
 
 <i>Note:</i> For access to the previous Tensorflow-2 version of ngc-learn (of
@@ -128,7 +126,7 @@ $ python install -e .
 </pre>
 
 **Version:**<br>
-1.1.4-Beta <!-- -Alpha -->
+1.2.0-Beta <!-- -Alpha -->
 
 Author:
 Alexander G. Ororbia II<br>
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/installation.md b/docs/installation.md
index 04640d8f9..a158d2e31 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -9,7 +9,7 @@ requires that you ensure that you have installed the following base dependencies
 your system. Note that this library was developed and tested on Ubuntu 22.04 (and 18.04).
 Specifically, ngc-learn requires:
 * Python (>=3.10)
-* ngcsimlib (>=0.2.b1), (<a href="https://github.com/NACLab/ngc-sim-lib">official page</a>)
+* ngcsimlib (>=0.3.b2), (<a href="https://github.com/NACLab/ngc-sim-lib">official page</a>)
 * NumPy (>=1.26.0)
 * SciPy (>=1.7.0)
 * JAX (>= 0.4.18; and jaxlib>=0.4.18) <!--(tested for cuda 11.8)-->
@@ -78,7 +78,7 @@ Python 3.11.4 (main, MONTH  DAY YEAR, TIME) [GCC XX.X.X] on linux
 Type "help", "copyright", "credits" or "license" for more information.
 >>> import ngclearn
 >>> ngclearn.__version__
-'1.1b2'
+'1.2b0'
 ```
 
 <i>Note</i>: If you do not have a JSON configuration file in place (see tutorials
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 d4e086da2..dc32b6f8b 100755
--- a/ngclearn/components/input_encoders/bernoulliCell.py
+++ b/ngclearn/components/input_encoders/bernoulliCell.py
@@ -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 64e03209f..1ca770a49 100755
--- a/ngclearn/components/input_encoders/latencyCell.py
+++ b/ngclearn/components/input_encoders/latencyCell.py
@@ -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/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 3d944eb31..07309fa95 100755
--- a/ngclearn/components/neurons/graded/gaussianErrorCell.py
+++ b/ngclearn/components/neurons/graded/gaussianErrorCell.py
@@ -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 db6dcf7a3..97e817ad9 100755
--- a/ngclearn/components/neurons/graded/laplacianErrorCell.py
+++ b/ngclearn/components/neurons/graded/laplacianErrorCell.py
@@ -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 44454424b..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
@@ -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 e8b2b74eb..7596f7f8a 100755
--- a/ngclearn/components/neurons/spiking/WTASCell.py
+++ b/ngclearn/components/neurons/spiking/WTASCell.py
@@ -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 ddb576a45..fa041ff45 100755
--- a/ngclearn/components/neurons/spiking/adExCell.py
+++ b/ngclearn/components/neurons/spiking/adExCell.py
@@ -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 2ec5cd317..26594a45f 100755
--- a/ngclearn/components/neurons/spiking/fitzhughNagumoCell.py
+++ b/ngclearn/components/neurons/spiking/fitzhughNagumoCell.py
@@ -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 8b958397b..d3acdb982 100644
--- a/ngclearn/components/neurons/spiking/sLIFCell.py
+++ b/ngclearn/components/neurons/spiking/sLIFCell.py
@@ -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 fa508dfd2..9c5a2b6cb 100644
--- a/ngclearn/components/other/expKernel.py
+++ b/ngclearn/components/other/expKernel.py
@@ -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 4eef5e242..7fa93fa67 100644
--- a/ngclearn/components/other/varTrace.py
+++ b/ngclearn/components/other/varTrace.py
@@ -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..734ee9dd4
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/convSynapse.py
@@ -0,0 +1,179 @@
+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_shape: 2d shape of input map signal (component currently assumess a square input maps)
+
+        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: a 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_shape, 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
+        x_size, x_size = x_shape
+        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`",
+            "x_shape": "Shape of any single incoming/input feature map",
+            "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)",
+            "stride": "length / size of stride",
+            "padding": "pre-operator padding to use, i.e., `VALID` `SAME`"
+        }
+        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..992e55eac
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/deconvSynapse.py
@@ -0,0 +1,167 @@
+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_shape: 2d shape of input map signal (component currently assumess a square input maps)
+
+        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: a 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_shape, 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
+        x_size, x_size = x_shape
+        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`",
+            "x_shape": "Shape of any single incoming/input feature map",
+            "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)",
+            "stride": "length / size of stride",
+            "padding": "pre-operator padding to use, i.e., `VALID` `SAME`"
+        }
+        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..76d2229c8
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/hebbianConvSynapse.py
@@ -0,0 +1,263 @@
+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_shape: 2d shape of input map signal (component currently assumess a square input maps)
+
+        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: a 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_shape, 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_shape=x_shape, 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)
+        self.antiPad = None
+        k_size, k_size, n_in_chan, n_out_chan = self.shape
+        if padding == "SAME":
+            self.antiPad = _conv_same_transpose_padding(self.post.value.shape[1],
+                                                        self.x_size, k_size, stride)
+        elif padding == "VALID":
+            self.antiPad = _conv_valid_transpose_padding(self.post.value.shape[1],
+                                                         self.x_size, k_size, stride)
+
+        ########################################################################
+
+        ## 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,
+                      antiPad, 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`",
+            "x_shape": "Shape of any single incoming/input feature map",
+            "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)",
+            "stride": "length / size of stride",
+            "padding": "pre-operator padding to use, i.e., `VALID` `SAME`",
+            "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..af5e9e0ee
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/hebbianDeconvSynapse.py
@@ -0,0 +1,243 @@
+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_shape: 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: a 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_shape, 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_shape=x_shape, 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`",
+            "x_shape": "Shape of any single incoming/input feature map",
+            "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)",
+            "stride": "length / size of stride",
+            "padding": "pre-operator padding to use, i.e., `VALID` `SAME`",
+            "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..c85b4a34a
--- /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_shape: 2d shape of input map signal (component currently assumess a square input maps)
+
+        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: a 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..6690bdbe4
--- /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_shape: 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: a 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..a1426b76f
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/traceSTDPConvSynapse.py
@@ -0,0 +1,244 @@
+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_shape: 2d shape of input map signal (component currently assumess a square input maps)
+
+        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: a 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_shape, 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_shape=x_shape, 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)
+        k_size, k_size, n_in_chan, n_out_chan = self.shape
+        if padding == "SAME":
+            self.antiPad = _conv_same_transpose_padding(
+                self.postSpike.value.shape[1],
+                self.x_size, k_size, stride)
+        elif padding == "VALID":
+            self.antiPad = _conv_valid_transpose_padding(
+                self.postSpike.value.shape[1],
+                self.x_size, k_size, stride)
+        ########################################################################
+
+    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, antiPad,
+                      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`",
+            "x_shape": "Shape of any single incoming/input feature map",
+            "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)",
+            "stride": "length / size of stride",
+            "padding": "pre-operator padding to use, i.e., `VALID` `SAME`",
+            "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..b3f60dda6
--- /dev/null
+++ b/ngclearn/components/synapses/convolution/traceSTDPDeconvSynapse.py
@@ -0,0 +1,226 @@
+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_shape: 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: a 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_shape, 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_shape=x_shape, 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`",
+            "x_shape": "Shape of any single incoming/input feature map",
+            "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)",
+            "stride": "length / size of stride",
+            "padding": "pre-operator padding to use, i.e., `VALID` `SAME`",
+            "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 bb998b4ed..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.
@@ -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 7d1f8ba0f..1d5c8766d 100755
--- a/ngclearn/components/synapses/hebbian/BCMSynapse.py
+++ b/ngclearn/components/synapses/hebbian/BCMSynapse.py
@@ -180,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":
@@ -190,7 +190,10 @@ 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 modification threshold variable",
                  "dWeights": "Synaptic weight value adjustment matrix produced at time t"},
diff --git a/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py b/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py
index 409a44771..5c5d3150e 100755
--- a/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py
+++ b/ngclearn/components/synapses/hebbian/eventSTDPSynapse.py
@@ -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 606652cf8..d8afa19f5 100644
--- a/ngclearn/components/synapses/hebbian/expSTDPSynapse.py
+++ b/ngclearn/components/synapses/hebbian/expSTDPSynapse.py
@@ -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 9558ed5ee..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,8 +167,10 @@ 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(opt, w_bounds, is_nonnegative, sign_value, w_decay, pre_wght,
@@ -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 c3612dfaf..6c45553f8 100755
--- a/ngclearn/components/synapses/hebbian/traceSTDPSynapse.py
+++ b/ngclearn/components/synapses/hebbian/traceSTDPSynapse.py
@@ -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 cdbfab337..f48472f84 100755
--- a/ngclearn/utils/metric_utils.py
+++ b/ngclearn/utils/metric_utils.py
@@ -26,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:
+    if preserve_batch is False:
         fano = jnp.mean(fano)
     return fano
 
@@ -49,7 +49,7 @@ 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:
+    if preserve_batch is False:
         fireRates = jnp.mean(fireRates)
     return fireRates
 
@@ -78,7 +78,7 @@ def measure_breadth_TC(spikes, preserve_batch=False):
     sigSqr = jnp.square(jnp.std(spikes, axis=0, keepdims=True))
     C = sigSqr/mu
     BTC = 1./(1 + jnp.square(C))
-    if preserve_batch:
+    if preserve_batch is False:
         BTC = jnp.mean(BTC)
     return BTC
 
@@ -199,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:
+    if preserve_batch is False:
         kld = jnp.mean(kld)
     return kld
 
@@ -226,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:
+    if preserve_batch is False:
         nll = jnp.mean(nll)
     return nll #tf.reduce_mean(nll)
 
@@ -251,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:
+    if preserve_batch is False:
         mse = jnp.mean(mse) # this is proper mse
     return mse
 
@@ -277,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:
+    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 a0f66844f..6a1c22e70 100755
--- a/ngclearn/utils/viz/dim_reduce.py
+++ b/ngclearn/utils/viz/dim_reduce.py
@@ -80,6 +80,8 @@ 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(
@@ -93,6 +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
-
+    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..dff8745b4 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()
+            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 758eb6efb..a449a7efe 100644
--- a/ngclearn/utils/viz/spike_plot.py
+++ b/ngclearn/utils/viz/spike_plot.py
@@ -1,7 +1,5 @@
-import matplotlib
 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/weight_distribution.py b/ngclearn/utils/weight_distribution.py
index 6e8e5ef78..9c3b3c891 100755
--- a/ngclearn/utils/weight_distribution.py
+++ b/ngclearn/utils/weight_distribution.py
@@ -184,6 +184,7 @@ def initialize_params(dkey, init_kernel, shape, use_numpy=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)
diff --git a/pyproject.toml b/pyproject.toml
index a43c414c7..8e7da02c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "ngclearn"
-version = "1.1.beta4" 
+version = "1.2.beta0"
 description = "Simulation software for building and analyzing arbitrary predictive coding, spiking network, and biomimetic neural systems."
 authors = [
   {name = "Alexander Ororbia", email = "ago@cs.rit.edu"},
diff --git a/requirements.txt b/requirements.txt
index 940c57d22..f7e4060c4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,5 +5,5 @@ matplotlib>=3.8.0
 patchify
 jax>=0.4.18
 jaxlib>=0.4.18
-ngcsimlib>=0.3.b2
+ngcsimlib>=0.3.b3
 imageio>=2.31.5


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