pulsatrix
Loading...
Searching...
No Matches
pulsatrix::ConjunctionModule Class Reference

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>

Inheritance diagram for pulsatrix::ConjunctionModule:
Collaboration diagram for pulsatrix::ConjunctionModule:

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.
 

Detailed Description

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).

Note
Design decision (mission_0_tnorm_operators.md Stage 3, resolved): Option 1 – Stack-based. Conjunction is genuinely binary (two independent external operand tensors), but Module::forward() is single-Tensor in/out. The two operands are combined via 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.

Member Enumeration Documentation

◆ TNorm

Which t-norm this instance computes. Product is the campaign's primary case.

Enumerator
Product 

a * b

Lukasiewicz 

max(0, a + b - 1)

Godel 

min(a, b)

Constructor & Destructor Documentation

◆ ConjunctionModule()

pulsatrix::ConjunctionModule::ConjunctionModule ( DeviceBackend *  backend,
TNorm  t_norm = TNorm::Product 
)
explicit

Constructs a conjunction module.

Parameters
backendBackend to allocate/compute through. Not owned; must outlive this module.
t_normWhich t-norm to compute. Defaults to Product (campaign's primary case).

Member Function Documentation

◆ backward()

Tensor pulsatrix::ConjunctionModule::backward ( const Tensor &  grad_output)
overridevirtual

Gradient w.r.t. this module's (stacked) input, via the selected t-norm's closed-form partial derivatives.

Parameters
grad_outputGradient 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).
Returns
Gradient w.r.t. the stacked input, shape (2, output.shape()...).
Exceptions
std::logic_errorif called before any forward().
std::invalid_argumentif grad_output's shape differs from the cached output shape.
Note
Tie-breaking convention (Godel, and Lukasiewicz's boundary): when the two operands are exactly equal, or exactly at Lukasiewicz's clip boundary, the whole gradient is attributed to the first operand (a) – a deterministic, documented choice, the same style as ReluModule's own x == 0 "treated as blocked" convention rather than an unhandled tie.

Implements pulsatrix::Module.

◆ compute_device()

std::optional< DeviceType > pulsatrix::ConjunctionModule::compute_device ( ) const
inlineoverridevirtual

Where this layer computes, so forward() rejects an input on another device (FND-8).

Reimplemented from pulsatrix::Module.

◆ forward() [1/2]

Tensor pulsatrix::ConjunctionModule::forward ( const Tensor &  a,
const Tensor &  b 
)

Convenience two-operand entry point: builds the stacked input via stack_operands() and delegates to Module::forward().

Parameters
aFirst operand tensor.
bSecond 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).
Returns
The conjunction, same shape as a (and b).

◆ forward() [2/2]

Tensor pulsatrix::Module::forward ( const Tensor &  input)
inline

Runs this module's forward computation.

Parameters
inputInput tensor. Must be non-empty.
Returns
The module's output.
Exceptions
std::invalid_argumentif 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.

◆ forward_impl()

Tensor pulsatrix::ConjunctionModule::forward_impl ( const Tensor &  input)
overrideprotectedvirtual

Splits input (leading dim 2) into the two operands and computes the selected t-norm elementwise.

Parameters
inputA stacked tensor, shape (2, ...), as built by stack_operands().
Returns
The t-norm result, shape equal to input's shape with the leading dim dropped.
Exceptions
std::invalid_argumentif 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.

◆ op_type()

OpType pulsatrix::ConjunctionModule::op_type ( ) const
inlineoverridevirtual

Elementwise per this module's own op_type() convention (ReluModule/ResidualModule).

Implements pulsatrix::Module.

◆ propagate_relevance()

Tensor pulsatrix::ConjunctionModule::propagate_relevance ( const Tensor &  relevance_out,
const LRPRuleConfig &  config 
)
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.

  • Product (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).
  • Lukasiewicz (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.
  • Godel (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.

◆ stack_operands()

static Tensor pulsatrix::ConjunctionModule::stack_operands ( const Tensor &  a,
const Tensor &  b,
DeviceBackend *  backend 
)
static

Combines two independent operand tensors into the leading-dim-2 stacked tensor forward_impl()/backward()/propagate_relevance() expect.

Parameters
aFirst operand.
bSecond operand.
backendBackend to allocate the stacked tensor through.
Returns
A tensor of shape (2, a.shape()...).
Exceptions
std::invalid_argumentif a and b differ in rank or any dimension – delegated to Tensor::Stack, which already implements exactly this external-boundary check.

The documentation for this class was generated from the following file: