PolyWeave Integration Plan
This document maps the PolyWeave
public API onto EdgeWeave nodes and records what's shipped so far. PolyWeave
is a small PyTorch library built around genuinely multiplicative (Sigma-Pi)
computation — see the polyweave Claude skill for the full API surface and
mental model.
Goals
- Make PolyWeave's multiplicative layers usable from the EdgeWeave canvas
as drop-in alternatives to the existing
torch_*layer nodes. - Reuse
backend/core/nodes/torch_nodes.py'sLayerSpec/GraphModelDAG machinery rather than inventing a parallel composition mechanism — PolyWeave layers are ordinarynn.Modules, so this is a clean fit. - Surface PolyWeave's recruitment diagnostics (
pi_scale_mean,exponent_abs_mean,branch_energy) and interpretability probes (occlusion sensitivity) as inspection nodes, not just library calls buried in exported code. - Preserve the Python export path — every node carries a
codegen.pythonhint so an exported.weaveis a runnable Python script.
Mental model
Unlike SimWeave (simulation constructors/actions/results/viz), most of
PolyWeave's surface is either an nn.Module subclass or a plain tensor
function, so the EdgeWeave mapping is flatter than SimWeave's:
| PolyWeave role | EdgeWeave node shape |
|---|---|
Layer construction (SigmaPiLinear, PolyLinear, ConvSigmaPi2d, SoftSignedLiteral, SoftRuleLayer) | A torch_*_layer-style node returning a LayerSpec, wireable into torch_sequential / torch_graph_model / torch_add_merge / torch_cat_merge / the training nodes / torch_save_model exactly like the built-in torch layers — these are all trainable nn.Modules, SoftSignedLiteral/SoftRuleLayer included |
Fuzzy gates (fuzzy_and/or/xor/not/nand/nor) | Plain tensor-in/tensor-out data-flow nodes, like the numpy_* nodes — a fuzzy AND of two truth-value tensors isn't a step in a layer chain |
Diagnostics (pi_scale_mean, exponent_abs_mean, branch_energy) | A passthrough "info" node: takes the upstream LayerSpec, returns it unchanged, and renders the diagnostic value as its preview (always_preview=True) — not meant to be a wired output most graphs consume |
Interpretability (occlusion_sensitivity_1d/2d) | Takes a model + data input, renders a native Plotly bar chart / heatmap as its preview and returns the raw drops/heatmap tensor |
Reasoning KB (PropKB) | Built up on the canvas like a dict with dict_set: one source node, then any number of chained "add rule" nodes, each mutating and passing along the same KB object |
Reasoning execution (ForwardChainer) | Constructed from a finished KB (like a SimWeave "construct" node), then run against a facts vector (like a SimWeave "action" node) |
Subsystem coverage
Layers (shipped)
File: backend/core/nodes/polyweave/polyweave_layers.py. Category: PolyWeave / Layers.
| Class | Node | Fields | Notes |
|---|---|---|---|
SigmaPiLinear | polyweave_sigma_pi_linear | in_features, out_features, signed_products, center_product | Dense multiplicative layer, magnitude-only product by default |
PolyLinear | polyweave_poly_linear | in_features, out_features, rank | Sum-of-low-rank-bilinear-terms alternative; rank=0 degrades to plain nn.Linear |
ConvSigmaPi2d | polyweave_conv_sigma_pi_2d | channels, kernel_size, padding, signed_products, center_product | Self-contained block (BatchNorm + ReLU baked in) |
Advanced constructor kwargs (pi_scale_init, max_exponent, max_log,
bias, symmetric, quad_scale_init, eps) are intentionally not exposed
as UI fields in this first slice, matching how torch_conv2d_layer doesn't
expose every nn.Conv2d kwarg either — add fields for these only if a real
workflow needs to tune them from the canvas.
Diagnostics (shipped)
| Method | Node | Inputs | Notes |
|---|---|---|---|
pi_scale_mean() (aliased quad_scale_mean() on PolyLinear) | polyweave_pi_scale_mean | layer | Works uniformly across all three layer types |
exponent_abs_mean() | polyweave_exponent_abs_mean | layer | Not exposed by PolyLinear — node reports "has no exponent_abs_mean()" rather than raising |
branch_energy(x) | polyweave_branch_energy | layer, data | Needs a real batch; data is coerced via to_numpy → torch.as_tensor. Not exposed by PolyLinear either |
Interpretability (shipped)
Also in polyweave_layers.py. response_fn must reduce to a per-item
scalar [N]; both nodes mean-reduce every dim after the batch axis by
default rather than exposing an output-selection field — a deliberate
scoping choice for this first cut.
| Function | Node | Fields | Notes |
|---|---|---|---|
occlusion_sensitivity_1d | polyweave_occlusion_sensitivity_1d | window, stride, baseline | [N, ...] → [N, P] drops; rendered as a Plotly bar chart |
occlusion_sensitivity_2d | polyweave_occlusion_sensitivity_2d | window, stride, baseline, relative | [N,C,H,W] → [N, Hout, Wout]; rendered as a Plotly heatmap. relative=True is the interesting mode — it's what "exposes the AND-signature visually" per the library's own docstring: every contributing region is ~100% critical for a multiplicative detector, only partially critical for an additive one |
Rendered via the existing _plotly_html helper (imported from
plotly_nodes.py), not polyweave.viz's matplotlib figures — this
sidesteps the PNG/SVG conversion problem noted below entirely, and means
these plots get right-click-to-fullscreen for free, same as every other
Plotly node.
Worth knowing before using these on a real model: an untrained model's sensitivity map is just noise. This diagnostic is only meaningful once there's something worth interrogating — the shipped exemplar demonstrates the wiring, not a real analysis.
Fuzzy logic (shipped)
File: backend/core/nodes/polyweave/polyweave_logic.py. Category: PolyWeave / Logic.
| Function/class | Node | Notes |
|---|---|---|
fuzzy_and / fuzzy_or / fuzzy_xor | polyweave_fuzzy_and / _or / _xor | Two-input gates, t_norm field (product/min) |
fuzzy_not | polyweave_fuzzy_not | Single input, no t_norm |
fuzzy_nand / fuzzy_nor | polyweave_fuzzy_nand / _nor | Same shape as and/or |
| — | — | fuzzy_xnor is not wrapped — there's no matching free function, only the FuzzyXnor module, and adding one gate via a different mechanism than its five siblings wasn't worth the inconsistency for this slice |
SoftSignedLiteral | polyweave_soft_signed_literal | Trainable, LayerSpec-based (see mental model above); fields n_features, signed |
SoftRuleLayer | polyweave_soft_rule_layer | Trainable, LayerSpec-based; fields n_features, n_rules, signed. Output is always a single soft-DNF truth value per item regardless of n_rules (a probabilistic OR over the rule firings) |
Reasoning (shipped)
File: backend/core/nodes/polyweave/polyweave_reasoning.py. Category: PolyWeave / Reasoning.
| Class/method | Node | Notes |
|---|---|---|
PropKB() | polyweave_prop_kb | Source node — empty KB |
PropKB.add_rule(premises, conclusion, name) | polyweave_kb_add_rule | Chainable — mutates and passes along the same KB object, mirroring dict_set's accumulate-via-wiring pattern rather than a single "define all rules as JSON" node |
PropKB.initial_facts(true_facts) | polyweave_kb_initial_facts | Builds the (1, N) truth vector |
ForwardChainer(kb, max_steps, t_norm) | polyweave_forward_chainer | Construction node |
ForwardChainer.__call__(facts) | polyweave_run_forward_chainer | Runs to the fixpoint; preview is a checkbox-style fact table |
ForwardChainer.entails(facts, goal) | polyweave_entails | (bool, float) result, colour-coded preview |
PropKB.fact_names/.num_facts/.num_rules are plain attributes, not
methods — confirmed against the installed library after kb.fact_names()
raised TypeError: 'list' object is not callable; dir() lists them
identically to real methods, so this only surfaces by actually calling
them. PropKB.describe() and reasoning.print_facts() both print directly
and return None, so every preview here is built manually from
fact_names + the fact/closure tensor rather than capturing their stdout.
Deferred to a later slice
polyweave.tools— theMathTool/LogicToolLLM tool-use dispatchers. This is the natural fit for an EdgeWeave "tool-calling agent" subgraph (LLM node → parse-call node → PolyWeave tool node → splice result back into the prompt), but needs the chat-assistant LLM nodes to exist first.polyweave.hypernets/training/targets/prototypes/students/evaluation— the hypernetwork research surface. Higher-effort wrapping (multi-node training loops with checkpoint I/O); revisit once the layer/logic/reasoning nodes have seen real use.polyweave.viz— returns matplotlib figures (PDF-oriented), not Plotly. The occlusion sensitivity nodes sidestepped this by building their own Plotly figures directly from the returned tensors rather than going throughpolyweave.vizat all; a PNG/SVG-to-<img>preview helper or a thin Plotly-conversion layer is still unbuilt for whatever invizisn't easily replicated as a native Plotly chart.
Cross-cutting decisions
Optional dependency, not on PyPI. Unlike SimWeave (pip install simweave[viz]), PolyWeave isn't published — it's installed editable from a
local checkout (pip install -e <path-to-polyweave>). The import in each
polyweave_*.py file is guarded the same way as simweave_continuous.py:
handlers boot fine when the package is missing and raise a clear
install-hint message (with a link to the GitHub repo, not a hardcoded local
path) at execution time.
Reusing torch_nodes.py internals. polyweave_layers.py and
polyweave_logic.py's trainable-layer nodes import LayerSpec,
_extract_layer_inputs, and _resolve_param directly from
backend.core.nodes.torch_nodes rather than re-implementing DAG-composition
logic. This is a deliberate coupling to a sibling core module's "private"
(underscore-prefixed) helpers — acceptable here because it's the only way to
compose with the existing GraphModel/torch_sequential/merge-node
machinery without duplicating it. If torch_nodes.py's internals are ever
refactored, all three polyweave_*.py files need a matching pass.
Categories. PolyWeave nodes use sub-categories per subsystem —
PolyWeave / Layers, PolyWeave / Logic, PolyWeave / Reasoning —
matching the SimWeave / Continuous, SimWeave / Discrete convention.
(The layers-only slice originally claimed this convention but actually
shipped everything under a flat PolyWeave category; fixed once there
were enough nodes for the sidebar to need it.)
Open follow-ups
.weaveexemplars now exist for every shipped subsystem:polyweave_sigma_pi_linear.weave,polyweave_poly_linear.weave,polyweave_mixed_graph.weave(layers + torch interop),polyweave_fuzzy_logic.weave,polyweave_reasoning_wet_grass.weave,polyweave_occlusion_sensitivity.weave(untrained model — demonstrates the wiring, not a real analysis).- Decide whether
exponent_abs_mean/branch_energy's "not available for this layer type" behavior should instead grey out those node types in the UI when wired from aPolyLinearupstream, versus the current run-time message — likely not worth the added validation complexity unless it trips users up in practice. - The occlusion sensitivity nodes' mean-reduction default (for models with more than one output) hasn't been tested against a real multi-class classifier — worth revisiting if it turns out users want to pick a specific class/output index instead.
Codegen / export reliability (2026-08-12)
The feature/codegen-export-coverage pass found and fixed real bugs in
the fuzzy gate nodes' own codegen, caught by actually exporting and
executing every shipped .weave demo (see
backend/tests/test_demo_exports.py), not just compiling the output:
polyweave_fuzzy_and/or/xor/nand/nor'sexprused{t_norm!r}— the generator's substitution already appliesrepr()and never matches the literal substring{t_norm}inside{t_norm!r}, so the token was left completely unsubstituted (aSyntaxError, not a subtler bug).- Separately: the runtime handlers coerce whatever flows into
a/bto a tensor viato_numpy+torch.as_tensorbefore callingpwlogic.fuzzy_and(...), but the exported code called the bare library function directly — a plain Python list from e.g.list_objwould compile fine but raiseTypeError: can't multiply sequence by non-int of type 'list'at runtime. Fixed by wrapping each operand intorch.as_tensor(..., dtype=torch.float32)inside theexprtemplate itself, mirroring the handler's own coercion.
Also found and fixed a generator-level bug affecting far more than
PolyWeave: PythonGenerator._expand_expr's named-port substitution only
matched a port's declared name (or its overrides field) — the legacy
{input_N} positional convention only worked for old integer-count-mode
nodes. But 22 nodes across the codebase (including every layers/logic
diagnostic node here: polyweave_pi_scale_mean, _exponent_abs_mean,
_branch_energy, plus all the fuzzy gates) declare named ports yet still
use {input_N} in their expr. Fixed at the generator level (both
conventions now substitute for named-port nodes) rather than editing
every affected node individually — see backend/export/python_generator.py.