|
pulsatrix
|
y = a T b for a selected t-norm T, over two independent fuzzy-truth-valued operand tensors (values intended in [0,1]; out-of-range values are not rejected – see propagate_relevance()'s note and mission_0_tnorm_operators.md's adversarial section).
More...
#include <conjunction_module.hpp>


Public Types | |
| enum class | TNorm { Product , Lukasiewicz , Godel } |
| Which t-norm this instance computes. Product is the campaign's primary case. More... | |
Public Member Functions | |
| ConjunctionModule (DeviceBackend *backend, TNorm t_norm=TNorm::Product) | |
| Constructs a conjunction module. | |
| Tensor | forward (const Tensor &a, const Tensor &b) |
| Convenience two-operand entry point: builds the stacked input via stack_operands() and delegates to Module::forward(). | |
| Tensor | backward (const Tensor &grad_output) override |
| Gradient w.r.t. this module's (stacked) input, via the selected t-norm's closed-form partial derivatives. | |
| OpType | op_type () const override |
| Elementwise per this module's own op_type() convention (ReluModule/ResidualModule). | |
| Tensor | propagate_relevance (const Tensor &relevance_out, const LRPRuleConfig &config) override |
| LRP relevance propagation for the selected t-norm – genuinely novel, no prior art (research_2026_neuro_symbolic_ai.md §3's honest finding); hand-derived here, conservation-tested before this implementation existed. | |
| std::optional< DeviceType > | compute_device () const override |
| Where this layer computes, so forward() rejects an input on another device (FND-8). | |
| Tensor | forward (const Tensor &input) |
| Runs this module's forward computation. | |
Public Member Functions inherited from pulsatrix::Module | |
| virtual | ~Module ()=default |
| Tensor | forward (const Tensor &input) |
| Runs this module's forward computation. | |
| std::pair< Tensor, NodeId > | forward_traced (const Tensor &input, NodeId input_node, ComputationGraph &graph, Autograd &autograd) |
| Runs forward() while also registering a ComputationGraph node (tagged with this module's op_type(), parented to input_node) and wiring an Autograd backward function that reuses this module's own backward() – the opt-in traced/explainable path, per Phase 2 Mission 0. | |
| virtual bool | supports_lrp_rule (LRPRule rule) const |
Whether propagate_relevance() implements rule (no silent fallback: callers such as ExplainerContext::relevance_pass() throw rather than run a module on a rule it does not implement). | |
| virtual std::vector< NamedParamRef > | named_parameters () |
| This module's trainable parameters, each with its hierarchical name – the one place a module declares its parameters (roadmap FND-1). | |
| virtual std::vector< ParamRef > | parameters () |
| This module's trainable parameters and their gradients, for an optimizer to update uniformly across module types. | |
| void | set_requires_grad (bool requires_grad, const std::string &prefix="") |
Freezes (false) or unfreezes (true) parameters by name (roadmap FND-2). | |
| virtual void | set_training (bool training) |
| Sets this module's training/eval mode. Defaults to training (matches every mainstream framework's Module default). | |
| bool | is_training () const |
| Whether this module is currently in training mode. | |
Static Public Member Functions | |
| static Tensor | stack_operands (const Tensor &a, const Tensor &b, DeviceBackend *backend) |
| Combines two independent operand tensors into the leading-dim-2 stacked tensor forward_impl()/backward()/propagate_relevance() expect. | |
Protected Member Functions | |
| Tensor | forward_impl (const Tensor &input) override |
| Splits input (leading dim 2) into the two operands and computes the selected t-norm elementwise. | |
y = a T b for a selected t-norm T, over two independent fuzzy-truth-valued operand tensors (values intended in [0,1]; out-of-range values are not rejected – see propagate_relevance()'s note and mission_0_tnorm_operators.md's adversarial section).
stack_operands() below (a thin wrapper around the already-existing Tensor::Stack, tensor.hpp:84) into one leading-dim-2 tensor before entering the ordinary Module::forward()/forward_impl() NVI path; forward_impl splits it back into the two operands internally, and backward()/propagate_relevance() re-split the incoming gradient/relevance the same way. Chosen over the MSELoss-shaped free-function alternative (Option 2, mission file's own comparison) specifically because this keeps ConjunctionModule a genuine Module subclass – needed for op_type()/forward_traced() participation once Mission 1's AggregatorModule and the satisfaction loss compose these operators inside a traced graph (per campaign scope's explicit "native Modules" framing) – at the cost of an internal split/re-split each call, which is O(n) elementwise work, not a new execution model. forward(const Tensor&, const Tensor&) is the convenience two-operand entry point; Module::forward(const Tensor&) (the single-Tensor NVI base method) remains reachable via the using declaration below and expects an already-stacked tensor (leading dim exactly 2) – exactly what stack_operands()/forward(a, b) build, and what forward_traced() threads through when this module participates in a ComputationGraph.
|
strong |
|
explicit |
Constructs a conjunction module.
| backend | Backend to allocate/compute through. Not owned; must outlive this module. |
| t_norm | Which t-norm to compute. Defaults to Product (campaign's primary case). |
Gradient w.r.t. this module's (stacked) input, via the selected t-norm's closed-form partial derivatives.
| grad_output | Gradient w.r.t. this module's output. Must match the shape of the most recent forward() call's output (the per-operand shape, not the stacked shape). |
| std::logic_error | if called before any forward(). |
| std::invalid_argument | if grad_output's shape differs from the cached output shape. |
Implements pulsatrix::Module.
|
inlineoverridevirtual |
Where this layer computes, so forward() rejects an input on another device (FND-8).
Reimplemented from pulsatrix::Module.
Convenience two-operand entry point: builds the stacked input via stack_operands() and delegates to Module::forward().
| a | First operand tensor. |
| b | Second operand tensor. Must match a's rank and every non-leading-adjusted dimension – see stack_operands()'s note; a shape mismatch surfaces as Tensor::Stack's own std::invalid_argument (external boundary, already implemented there, not re-validated here). |
Runs this module's forward computation.
| input | Input tensor. Must be non-empty. |
| std::invalid_argument | if input is empty – external boundary (campaign_exai_dl_library_adversarial_hardening.md, Mission 2, finding 15 systemic sweep): the single most external-facing check in the whole system, since every Module::forward() call – including from Phase 5's Python bindings – passes through this NVI wrapper first. Escalated from PULSATRIX_ASSERT-only. |
Splits input (leading dim 2) into the two operands and computes the selected t-norm elementwise.
| input | A stacked tensor, shape (2, ...), as built by stack_operands(). |
| std::invalid_argument | if input's rank is 0 or its leading dimension isn't 2 – external boundary: forward_impl is reachable directly through the inherited, non-virtual Module::forward(const Tensor&) by any caller bypassing forward(a, b)/stack_operands() (e.g. Phase 5's Python bindings, or forward_traced()'s own single-Tensor threading). |
Implements pulsatrix::Module.
|
inlineoverridevirtual |
Elementwise per this module's own op_type() convention (ReluModule/ResidualModule).
Implements pulsatrix::Module.
|
overridevirtual |
LRP relevance propagation for the selected t-norm – genuinely novel, no prior art (research_2026_neuro_symbolic_ai.md §3's honest finding); hand-derived here, conservation-tested before this implementation existed.
y = a*b): the bilinear/"uniform" split this codebase already uses for Q@K^T-shaped matmul products (AttnLRP Eq. 15, bilinear_lrp_eq15 in multihead_attention_module.cpp) – applied here to the elementwise (inner dimension 1) case: R_a = R_b = (a*b) / (2*y + eps*sign(y)) * R_out. Extending an already-vetted-in-this-codebase bilinear rule from a matmul's inner-product structure to a t-norm's elementwise product is the novel step (no literature applies AttnLRP-style bilinear splitting to fuzzy logic operators).y = max(0, a+b-1)): active region (a+b-1 > 0) is exactly LinearModule's own bias-excluded epsilon rule. The pre-bias value is z = a+b (the -1 is the bias, absorbed rather than distributed, same convention as LinearModule's pre-bias z_j): R_a = a/(z+eps*sign(z)) * R_out, R_b = b/(z+eps*sign(z)) * R_out, so R_a + R_b = R_out * z/(z+eps) – conservative. (Before 2026-10 the denominator used the biased a+b-1, which created relevance: (a+b)/(a+b-1) times R_out.) Inactive region (a+b-1 <= 0, y = 0): both operands' local derivative is 0 (matches backward()'s own gradient there), so both receive 0 – ReluModule's "blocked" convention, deliberately kept consistent with backward() rather than force-conserving through a dead branch.y = min(a, b)): the winning (smaller, tie -> a) operand receives all of R_out exactly (R_a = a/y * R_out = R_out when a wins, since a == y there); the other receives 0. Exact (not merely near-exact) conservation, since min's active branch is a pure identity map on the winning operand. Implements pulsatrix::Module.
|
static |
Combines two independent operand tensors into the leading-dim-2 stacked tensor forward_impl()/backward()/propagate_relevance() expect.
| a | First operand. |
| b | Second operand. |
| backend | Backend to allocate the stacked tensor through. |
| std::invalid_argument | if a and b differ in rank or any dimension – delegated to Tensor::Stack, which already implements exactly this external-boundary check. |