Skip to main content

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​

  1. Make PolyWeave's multiplicative layers usable from the EdgeWeave canvas as drop-in alternatives to the existing torch_* layer nodes.
  2. Reuse backend/core/nodes/torch_nodes.py's LayerSpec/GraphModel DAG machinery rather than inventing a parallel composition mechanism — PolyWeave layers are ordinary nn.Modules, so this is a clean fit.
  3. 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.
  4. Preserve the Python export path — every node carries a codegen.python hint so an exported .weave is 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 roleEdgeWeave 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.

ClassNodeFieldsNotes
SigmaPiLinearpolyweave_sigma_pi_linearin_features, out_features, signed_products, center_productDense multiplicative layer, magnitude-only product by default
PolyLinearpolyweave_poly_linearin_features, out_features, rankSum-of-low-rank-bilinear-terms alternative; rank=0 degrades to plain nn.Linear
ConvSigmaPi2dpolyweave_conv_sigma_pi_2dchannels, kernel_size, padding, signed_products, center_productSelf-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)​

MethodNodeInputsNotes
pi_scale_mean() (aliased quad_scale_mean() on PolyLinear)polyweave_pi_scale_meanlayerWorks uniformly across all three layer types
exponent_abs_mean()polyweave_exponent_abs_meanlayerNot exposed by PolyLinear — node reports "has no exponent_abs_mean()" rather than raising
branch_energy(x)polyweave_branch_energylayer, dataNeeds 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.

FunctionNodeFieldsNotes
occlusion_sensitivity_1dpolyweave_occlusion_sensitivity_1dwindow, stride, baseline[N, ...] → [N, P] drops; rendered as a Plotly bar chart
occlusion_sensitivity_2dpolyweave_occlusion_sensitivity_2dwindow, 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/classNodeNotes
fuzzy_and / fuzzy_or / fuzzy_xorpolyweave_fuzzy_and / _or / _xorTwo-input gates, t_norm field (product/min)
fuzzy_notpolyweave_fuzzy_notSingle input, no t_norm
fuzzy_nand / fuzzy_norpolyweave_fuzzy_nand / _norSame 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
SoftSignedLiteralpolyweave_soft_signed_literalTrainable, LayerSpec-based (see mental model above); fields n_features, signed
SoftRuleLayerpolyweave_soft_rule_layerTrainable, 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/methodNodeNotes
PropKB()polyweave_prop_kbSource node — empty KB
PropKB.add_rule(premises, conclusion, name)polyweave_kb_add_ruleChainable — 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_factsBuilds the (1, N) truth vector
ForwardChainer(kb, max_steps, t_norm)polyweave_forward_chainerConstruction node
ForwardChainer.__call__(facts)polyweave_run_forward_chainerRuns 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 — the MathTool/LogicTool LLM 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 through polyweave.viz at all; a PNG/SVG-to-<img> preview helper or a thin Plotly-conversion layer is still unbuilt for whatever in viz isn'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​

  1. .weave exemplars 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).
  2. 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 a PolyLinear upstream, versus the current run-time message — likely not worth the added validation complexity unless it trips users up in practice.
  3. 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's expr used {t_norm!r} — the generator's substitution already applies repr() and never matches the literal substring {t_norm} inside {t_norm!r}, so the token was left completely unsubstituted (a SyntaxError, not a subtler bug).
  • Separately: the runtime handlers coerce whatever flows into a/b to a tensor via to_numpy + torch.as_tensor before calling pwlogic.fuzzy_and(...), but the exported code called the bare library function directly — a plain Python list from e.g. list_obj would compile fine but raise TypeError: can't multiply sequence by non-int of type 'list' at runtime. Fixed by wrapping each operand in torch.as_tensor(..., dtype=torch.float32) inside the expr template 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.