FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin for Harbour/xHarbour Class THandWiredNet
Posts: 44508
Joined: Thu Oct 06, 2005 05:47 PM

Class THandWiredNet

Posted: Sat Oct 17, 2026 06:03 AM

handwired.prg

#include "hbclass.ch"

PROCEDURE Main()
   LOCAL oNet, uUnidades, uEsVip, uMayorQue4, uPrecio, uDescuento, uTotal

   // 1. Instanciamos el compilador/red neuronal
   oNet := THandwiredNet():New()

   // 2. Declaramos las variables de entrada (nodos simb贸licos)
   uUnidades := oNet:Input( "unidades" )
   uEsVip    := oNet:Input( "es_vip" )

   // 3. Escribimos la l贸gica de negocio de forma declarativa e intuitiva:
   //    - Condici贸n: 驴unidades > 4? (Compila a ReLU(u - 4) - ReLU(u - 5))
   //    - IIF simb贸lico: si > 4 vale 10, si no vale 12
   //    - Descuento: si es_vip == 1 vale 5, si no 0
   uMayorQue4 := oNet:GreaterThan( uUnidades, 4 )
   uPrecio    := oNet:Iif( uMayorQue4, 10, 12 )
   uDescuento := oNet:Iif( oNet:Equals( uEsVip, 1 ), 5, 0 )

   // Gracias a la sobrecarga de operadores en THWVar, escribimos la f贸rmula natural:
   uTotal     := ( uUnidades * uPrecio ) - uDescuento

   // 4. Fijamos la salida y compilamos a matrices puras
   oNet:SetOutput( uTotal )
   oNet:Compile()

   // Mostramos la arquitectura generada autom谩ticamente
   oNet:DumpArchitecture()

   // 5. Ejecutamos la inferencia (100% MatMul + ReLU + ReGLU, cero IFs)
   ? "=== Inferencia en la Red Neuronal Compilada ==="
   ? "3 uds, Normal (Esperado: 36) -> Red:", oNet:Run( { "unidades" => 3, "es_vip" => 0 } )
   ? "5 uds, Normal (Esperado: 50) -> Red:", oNet:Run( { "unidades" => 5, "es_vip" => 0 } )
   ? "6 uds, VIP    (Esperado: 55) -> Red:", oNet:Run( { "unidades" => 6, "es_vip" => 1 } )

RETURN


// ===========================================================================
// CLASE THWVar: Representa un nodo/tensor simb贸lico en el grafo
// ===========================================================================
// Un THWVar almacena c贸mo se calcula su valor como combinaci贸n lineal de
// neuronas ocultas ReLU y t茅rminos bilineales (ReGLU).
// ===========================================================================
CLASS THWVar
   DATA oNet
   DATA aLinear   INIT {}   // Pares: { nHiddenIdx, nWeight } (Idx 0 = Bias constante)
   DATA aBilinear INIT {}   // Tripletas ReGLU: { nHiddenIdxA, nHiddenIdxB, nWeight }

   METHOD New( oNet )
   METHOD AddLinear( nHiddenIdx, nWeight )
   METHOD AddBilinear( nIdxA, nIdxB, nWeight )
   METHOD Clone()

   // Sobrecarga de operadores nativos de Harbour para expresiones naturales
   OPERATOR "+" ARG xOther
   OPERATOR "-" ARG xOther
   OPERATOR "*" ARG xOther
ENDCLASS

METHOD New( oNet ) CLASS THWVar
   ::oNet := oNet
RETURN Self

METHOD AddLinear( nHiddenIdx, nWeight ) CLASS THWVar
   LOCAL aPair
   IF nWeight == 0
      RETURN Self
   ENDIF
   FOR EACH aPair IN ::aLinear
      IF aPair[ 1 ] == nHiddenIdx
         aPair[ 2 ] += nWeight
         RETURN Self
      ENDIF
   NEXT
   AAdd( ::aLinear, { nHiddenIdx, nWeight } )
RETURN Self

METHOD AddBilinear( nIdxA, nIdxB, nWeight ) CLASS THWVar
   IF nWeight != 0
      AAdd( ::aBilinear, { nIdxA, nIdxB, nWeight } )
   ENDIF
RETURN Self

METHOD Clone() CLASS THWVar
   LOCAL oCopy := THWVar():New( ::oNet )
   LOCAL aItem
   FOR EACH aItem IN ::aLinear
      AAdd( oCopy:aLinear, { aItem[ 1 ], aItem[ 2 ] } )
   NEXT
   FOR EACH aItem IN ::aBilinear
      AAdd( oCopy:aBilinear, { aItem[ 1 ], aItem[ 2 ], aItem[ 3 ] } )
   NEXT
RETURN oCopy

// Suma simb贸lica: Variable + Variable  o  Variable + Constante
METHOD "+"( xOther ) CLASS THWVar
   LOCAL oRes := ::Clone()
   LOCAL aItem
   IF HB_ISNUMERIC( xOther )
      oRes:AddLinear( 0, xOther ) // 0 = Bias constante
   ELSEIF HB_ISOBJECT( xOther )
      FOR EACH aItem IN xOther:aLinear
         oRes:AddLinear( aItem[ 1 ], aItem[ 2 ] )
      NEXT
      FOR EACH aItem IN xOther:aBilinear
         oRes:AddBilinear( aItem[ 1 ], aItem[ 2 ], aItem[ 3 ] )
      NEXT
   ENDIF
RETURN oRes

// Resta simb贸lica: Variable - Variable  o  Variable - Constante
METHOD "-"( xOther ) CLASS THWVar
   LOCAL oRes := ::Clone()
   LOCAL aItem
   IF HB_ISNUMERIC( xOther )
      oRes:AddLinear( 0, -xOther )
   ELSEIF HB_ISOBJECT( xOther )
      FOR EACH aItem IN xOther:aLinear
         oRes:AddLinear( aItem[ 1 ], -aItem[ 2 ] )
      NEXT
      FOR EACH aItem IN xOther:aBilinear
         oRes:AddBilinear( aItem[ 1 ], aItem[ 2 ], -aItem[ 3 ] )
      NEXT
   ENDIF
RETURN oRes

// Multiplicaci贸n simb贸lica (Genera compuertas ReGLU cuando multiplica 2 variables)
METHOD "*"( xOther ) CLASS THWVar
   LOCAL oRes := THWVar():New( ::oNet )
   LOCAL aItemA, aItemB
   IF HB_ISNUMERIC( xOther )
      FOR EACH aItemA IN ::aLinear
         oRes:AddLinear( aItemA[ 1 ], aItemA[ 2 ] * xOther )
      NEXT
      FOR EACH aItemA IN ::aBilinear
         oRes:AddBilinear( aItemA[ 1 ], aItemA[ 2 ], aItemA[ 3 ] * xOther )
      NEXT
   ELSEIF HB_ISOBJECT( xOther )
      // Producto distributivo entre dos expresiones lineales -> Compuertas ReGLU
      FOR EACH aItemA IN ::aLinear
         FOR EACH aItemB IN xOther:aLinear
            IF aItemA[ 1 ] == 0
               oRes:AddLinear( aItemB[ 1 ], aItemA[ 2 ] * aItemB[ 2 ] )
            ELSEIF aItemB[ 1 ] == 0
               oRes:AddLinear( aItemA[ 1 ], aItemA[ 2 ] * aItemB[ 2 ] )
            ELSE
               // Ambos dependen de neuronas ocultas -> Interacci贸n Bilineal (ReGLU)
               oRes:AddBilinear( aItemA[ 1 ], aItemB[ 1 ], aItemA[ 2 ] * aItemB[ 2 ] )
            ENDIF
         NEXT
      NEXT
   ENDIF
RETURN oRes


// ===========================================================================
// CLASE THandwiredNet: Compilador de L贸gica a Matrices + Motor de Inferencia
// ===========================================================================
CLASS THandwiredNet
   DATA hInputs       INIT {=>} // Mapa nombre_entrada -> 铆ndice de columna
   DATA aInputNames   INIT {}   // Lista ordenada de entradas
   DATA aHiddenSpecs  INIT {}   // Definici贸n de cada neurona ReLU de Capa 1
   DATA oOutputVar              // THWVar de salida

   // Matrices compiladas listas para inferencia matricial
   DATA aW1           INIT {}   // Matriz Capa 1: [ nHidden ][ nInputs + 1 (Bias) ]
   DATA aW2Linear     INIT {}   // Vector Capa 2 Lineal: [ nHidden + 1 (Bias) ]
   DATA aW2Bilinear   INIT {}   // Matriz Capa 2 ReGLU: tripletas { idxA, idxB, peso }

   METHOD New()
   METHOD Input( cName )
   METHOD AddReLUNeuron( nInputIdx, nWeight, nBias )
   METHOD GreaterThan( oVar, nThreshold )
   METHOD LessThan( oVar, nThreshold )
   METHOD Equals( oVar, nValue )
   METHOD Iif( oCond, xTrue, xFalse )
   METHOD SetOutput( oVar )
   METHOD Compile()
   METHOD Run( hValues )
   METHOD DumpArchitecture()
ENDCLASS

METHOD New() CLASS THandwiredNet
RETURN Self

// Crea una variable de entrada y una neurona ReLU identidad para dejarla pasar
METHOD Input( cName ) CLASS THandwiredNet
   LOCAL nInputIdx, nHiddenIdx, oVar

   AAdd( ::aInputNames, cName )
   nInputIdx := Len( ::aInputNames )
   ::hInputs[ cName ] := nInputIdx

   // Creamos una neurona ReLU(1 * x + 0) que act煤a como "cable" de paso (para x >= 0)
   nHiddenIdx := ::AddReLUNeuron( nInputIdx, 1, 0 )

   oVar := THWVar():New( Self )
   oVar:AddLinear( nHiddenIdx, 1 )
RETURN oVar

// A帽ade una neurona a la Capa 1: ReLU( nWeight * Input[nInputIdx] + nBias )
METHOD AddReLUNeuron( nInputIdx, nWeight, nBias ) CLASS THandwiredNet
   AAdd( ::aHiddenSpecs, { nInputIdx, nWeight, nBias } )
RETURN Len( ::aHiddenSpecs )

// Compila (x > k) para enteros: ReLU(x - k) - ReLU(x - (k + 1))
METHOD GreaterThan( oVar, nThreshold ) CLASS THandwiredNet
   LOCAL nInputIdx := ::aHiddenSpecs[ oVar:aLinear[ 1 ][ 1 ] ][ 1 ]
   LOCAL nH1 := ::AddReLUNeuron( nInputIdx, 1, -nThreshold )
   LOCAL nH2 := ::AddReLUNeuron( nInputIdx, 1, -( nThreshold + 1 ) )
   LOCAL oCond := THWVar():New( Self )

   oCond:AddLinear( nH1,  1 )
   oCond:AddLinear( nH2, -1 )
RETURN oCond

// Compila (x < k) para enteros: 1 - (x > k - 1)
METHOD LessThan( oVar, nThreshold ) CLASS THandwiredNet
   LOCAL oGe := ::GreaterThan( oVar, nThreshold - 1 )
   LOCAL oRes := THWVar():New( Self )
   oRes:AddLinear( 0, 1 )
RETURN oRes - oGe

// Compila igualdad exacta (x == k) usando un pulso triangular de 3 ReLUs:
// ReLU(x - (k-1)) - 2*ReLU(x - k) + ReLU(x - (k+1))
METHOD Equals( oVar, nValue ) CLASS THandwiredNet
   LOCAL nInputIdx := ::aHiddenSpecs[ oVar:aLinear[ 1 ][ 1 ] ][ 1 ]
   LOCAL nH1 := ::AddReLUNeuron( nInputIdx, 1, -( nValue - 1 ) )
   LOCAL nH2 := ::AddReLUNeuron( nInputIdx, 1, -nValue )
   LOCAL nH3 := ::AddReLUNeuron( nInputIdx, 1, -( nValue + 1 ) )
   LOCAL oEq := THWVar():New( Self )

   oEq:AddLinear( nH1,  1 )
   oEq:AddLinear( nH2, -2 )
   oEq:AddLinear( nH3,  1 )
RETURN oEq

// Compila un IIF( oCond, xTrue, xFalse ) como: oCond * xTrue + (1 - oCond) * xFalse
METHOD Iif( oCond, xTrue, xFalse ) CLASS THandwiredNet
   LOCAL oOneMinusCond := THWVar():New( Self )
   LOCAL oBranchTrue, oBranchFalse

   oOneMinusCond:AddLinear( 0, 1 )
   oOneMinusCond := oOneMinusCond - oCond

   oBranchTrue  := iif( HB_ISOBJECT( xTrue ),  xTrue,   oCond * xTrue )
   IF HB_ISOBJECT( xTrue )
      oBranchTrue := oCond * xTrue
   ENDIF

   oBranchFalse := iif( HB_ISOBJECT( xFalse ), oOneMinusCond * xFalse, oOneMinusCond * xFalse )
RETURN oBranchTrue + oBranchFalse

METHOD SetOutput( oVar ) CLASS THandwiredNet
   ::oOutputVar := oVar
RETURN Self

// ---------------------------------------------------------------------------
// COMPILACI脫N: Ensambla las matrices densas aW1, aW2Linear y aW2Bilinear
// ---------------------------------------------------------------------------
METHOD Compile() CLASS THandwiredNet
   LOCAL nInputs := Len( ::aInputNames )
   LOCAL nHidden := Len( ::aHiddenSpecs )
   LOCAL i, aSpec, aRow, aPair

   // 1. Construir Matriz W1 (Capa 1): [ nHidden x (nInputs + 1) ]
   ::aW1 := Array( nHidden )
   FOR i := 1 TO nHidden
      aRow := Array( nInputs + 1 )
      AFill( aRow, 0 )
      aSpec := ::aHiddenSpecs[ i ]
      aRow[ aSpec[ 1 ] ]     := aSpec[ 2 ] // Peso de la entrada
      aRow[ nInputs + 1 ]    := aSpec[ 3 ] // Peso del Bias (煤ltima columna)
      ::aW1[ i ] := aRow
   NEXT

   // 2. Construir Vector W2 Lineal: [ nHidden + 1 ] (la 煤ltima posici贸n es el Bias de salida)
   ::aW2Linear := Array( nHidden + 1 )
   AFill( ::aW2Linear, 0 )
   FOR EACH aPair IN ::oOutputVar:aLinear
      IF aPair[ 1 ] == 0
         ::aW2Linear[ nHidden + 1 ] += aPair[ 2 ]
      ELSE
         ::aW2Linear[ aPair[ 1 ] ]  += aPair[ 2 ]
      ENDIF
   NEXT

   // 3. Copiar compuertas bilineales (ReGLU)
   ::aW2Bilinear := AClone( ::oOutputVar:aBilinear )
RETURN Self

// ---------------------------------------------------------------------------
// MOTOR DE INFERENCIA PURA: Solo multiplicaci贸n de matrices + ReLU + ReGLU
// ---------------------------------------------------------------------------
METHOD Run( hValues ) CLASS THandwiredNet
   LOCAL nInputs := Len( ::aInputNames )
   LOCAL nHidden := Len( ::aW1 )
   LOCAL aX      := Array( nInputs + 1 )
   LOCAL aH      := Array( nHidden + 1 )
   LOCAL i, j, nSum, nOut, aGate

   // Preparar vector de entrada X con un 1.0 al final para el Bias
   FOR i := 1 TO nInputs
      aX[ i ] := hValues[ ::aInputNames[ i ] ]
   NEXT
   aX[ nInputs + 1 ] := 1.0

   // CAPA 1: H = ReLU( W1 * X )
   FOR i := 1 TO nHidden
      nSum := 0
      FOR j := 1 TO nInputs + 1
         nSum += ::aW1[ i ][ j ] * aX[ j ]
      NEXT
      aH[ i ] := Max( 0, nSum ) // Funci贸n de activaci贸n ReLU
   NEXT
   aH[ nHidden + 1 ] := 1.0 // Bias para la Capa 2

   // CAPA 2 (Parte A - Producto escalar lineal): Out = W2_Linear 路 H
   nOut := 0
   FOR i := 1 TO nHidden + 1
      nOut += ::aW2Linear[ i ] * aH[ i ]
   NEXT

   // CAPA 2 (Parte B - Compuertas ReGLU): Out += Suma( w * H[a] * H[b] )
   FOR EACH aGate IN ::aW2Bilinear
      nOut += aGate[ 3 ] * aH[ aGate[ 1 ] ] * aH[ aGate[ 2 ] ]
   NEXT

RETURN nOut

// Muestra por pantalla las matrices compiladas y el n煤mero de pesos
METHOD DumpArchitecture() CLASS THandwiredNet
   LOCAL i, nWeightsW1 := Len( ::aW1 ) * ( Len( ::aInputNames ) + 1 )
   LOCAL nWeightsW2 := Len( ::aW2Linear ) + Len( ::aW2Bilinear )

   ? "=== Arquitectura Compilada Automaticamente ==="
   ? "Entradas :", hb_ValToExp( ::aInputNames )
   ? "Neuronas ReLU Capa 1:", Len( ::aW1 ), "| Pesos totales:", nWeightsW1 + nWeightsW2
   ? "--- Matriz W1 (Cada fila es: [", hb_ValToExp( ::aInputNames ), ", Bias ]) ---"
   FOR i := 1 TO Len( ::aW1 )
      ? "  Neurona h" + hb_ntos( i ) + ": ReLU(", hb_ValToExp( ::aW1[ i ] ), ")"
   NEXT
   ? "--- Vector W2 Lineal (Pesos sobre h1..h" + hb_ntos( Len( ::aW1 ) ) + " + Bias) ---"
   ? " ", hb_ValToExp( ::aW2Linear )
   ? "--- Compuertas ReGLU ( { hA, hB, Peso } ) ---"
   ? " ", hb_ValToExp( ::aW2Bilinear )
   ? ""
RETURN Self
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 150
Joined: Fri Sep 25, 2026 01:40 AM

Re: Class THandWiredNet

Posted: Sat Oct 17, 2026 07:04 AM

Hi Antonio,

Brilliant proof of concept. I reviewed it in depth and found a few edge cases worth fixing, so here is a patched version of the full class:

1. Negative inputs no longer clipped. Input() used a single pass-through neuron ReLU(x), which silently clipped negative values to zero. Each input now crosses two pass-through neurons, ReLU(x) and ReLU(-x), and the variable is their difference, so x = pos - neg holds for all values.

2. Clear errors instead of silent drops. Multiplying expressions that already contain bilinear (ReGLU) terms would need degree 3 or higher, which a 2-layer ReLU + ReGLU network cannot represent. The * operator now raises a descriptive error instead of silently dropping those terms (previously (ab)c quietly returned 0).

3. Validation where the design needs it. GreaterThan(), LessThan() and Equals() now require a direct input variable (tracked through a new nInputIdx field that any arithmetic operation clears) and an integer threshold/value. Run() rejects non-integer values on inputs that take part in comparisons, since those comparisons are only exact for integers. Compile() also requires SetOutput() to have been called.

4. Cleaned up the redundant iif() calls in the Iif() method; the operator overloads already handle numeric and object branches.

One honest disclaimer: I could not compile this with Harbour in my environment, so verification was done with a faithful Python port of the symbolic semantics and of Compile()/Run(). All 13 test cases pass there: the three original demo cases (36, 50, 55), boundary values (48 at 4 units, 0 at 0 units), negative inputs (-24 and -65), and clear errors for non-integer input, composite expressions in comparisons, non-integer thresholds, bilinear products and Compile() without SetOutput(). Please compile it in your environment before relying on it.

The compiled network grows from 7 to 9 ReLU neurons, one extra pass-through per input.

#include "hbclass.ch"

// ===========================================================================
// handwired.prg  --  version parcheada
//
// Cambios respecto a la version publicada en el foro (post p286654):
//   1. Input() ya no recorta negativos: cada entrada usa DOS neuronas paso,
//      ReLU(x) y ReLU(-x), y la variable es su diferencia (x = pos - neg).
//   2. El operador "*" rechaza con error claro el producto de expresiones
//      que ya contienen terminos bilineales (no representable en 2 capas),
//      en lugar de descartarlos en silencio.
//   3. GreaterThan()/Equals() validan que reciben una variable de entrada
//      directa (nInputIdx), no una expresion compuesta, y que el umbral es
//      entero; en caso contrario lanzan error descriptivo.
//   4. Run() valida que las entradas que intervienen en comparaciones
//      reciban valores enteros (las comparaciones son exactas para enteros).
//   5. Compile() exige que se haya llamado a SetOutput().
//   6. Limpiados los iif() redundantes del metodo Iif().
// ===========================================================================

PROCEDURE Main()
   LOCAL oNet, uUnidades, uEsVip, uMayorQue4, uPrecio, uDescuento, uTotal
   LOCAL oErr

   // 1. Instanciamos el compilador/red neuronal
   oNet := THandwiredNet():New()

   // 2. Declaramos las variables de entrada (nodos simb贸licos)
   uUnidades := oNet:Input( "unidades" )
   uEsVip    := oNet:Input( "es_vip" )

   // 3. Escribimos la l贸gica de negocio de forma declarativa e intuitiva:
   //    - Condici贸n: 驴unidades > 4? (Compila a ReLU(u - 4) - ReLU(u - 5))
   //    - IIF simb贸lico: si > 4 vale 10, si no vale 12
   //    - Descuento: si es_vip == 1 vale 5, si no 0
   uMayorQue4 := oNet:GreaterThan( uUnidades, 4 )
   uPrecio    := oNet:Iif( uMayorQue4, 10, 12 )
   uDescuento := oNet:Iif( oNet:Equals( uEsVip, 1 ), 5, 0 )

   // Gracias a la sobrecarga de operadores en THWVar, escribimos la f贸rmula natural:
   uTotal     := ( uUnidades * uPrecio ) - uDescuento

   // 4. Fijamos la salida y compilamos a matrices puras
   oNet:SetOutput( uTotal )
   oNet:Compile()

   // Mostramos la arquitectura generada autom谩ticamente
   oNet:DumpArchitecture()

   // 5. Ejecutamos la inferencia (100% MatMul + ReLU + ReGLU, cero IFs)
   ? "=== Inferencia en la Red Neuronal Compilada ==="
   ? "3 uds, Normal (Esperado: 36) -> Red:", oNet:Run( { "unidades" => 3, "es_vip" => 0 } )
   ? "5 uds, Normal (Esperado: 50) -> Red:", oNet:Run( { "unidades" => 5, "es_vip" => 0 } )
   ? "6 uds, VIP    (Esperado: 55) -> Red:", oNet:Run( { "unidades" => 6, "es_vip" => 1 } )

   // Caso nuevo: entradas negativas ya no se recortan a cero
   ? "-2 uds, Normal (Esperado: -24) -> Red:", oNet:Run( { "unidades" => -2, "es_vip" => 0 } )

   // 6. Demostraciones de las nuevas validaciones (deben lanzar error claro)
   ? "=== Validaciones ==="

   TRY
      oNet:Run( { "unidades" => 4.5, "es_vip" => 0 } )
      ? "ERROR: no se detecto la entrada no entera"
   CATCH oErr
      ? "OK, entrada no entera rechazada:", oErr:Description
   END

   TRY
      oNet:GreaterThan( uUnidades + 1, 4 )
      ? "ERROR: no se detecto la expresion compuesta"
   CATCH oErr
      ? "OK, expresion compuesta rechazada:", oErr:Description
   END

   TRY
      ( uUnidades * uPrecio ) * uEsVip
      ? "ERROR: no se detecto el producto con terminos bilineales"
   CATCH oErr
      ? "OK, producto no representable rechazado:", oErr:Description
   END

RETURN


// ===========================================================================
// Error descriptivo del compilador/red
// ===========================================================================
STATIC FUNCTION RaiseError( cMsg )
   LOCAL oErr := ErrorNew()
   oErr:SubSystem   := "THWNET"
   oErr:Description := cMsg
   Throw( oErr )
RETURN NIL


// ===========================================================================
// CLASE THWVar: Representa un nodo/tensor simb贸lico en el grafo
// ===========================================================================
// Un THWVar almacena c贸mo se calcula su valor como combinaci贸n lineal de
// neuronas ocultas ReLU y t茅rminos bilineales (ReGLU).
//
// nInputIdx: si la variable es EXACTAMENTE una entrada declarada con Input(),
// guarda su 铆ndice de columna; cualquier operaci贸n aritm茅tica lo pone a NIL.
// Las comparaciones (GreaterThan/Equals) lo exigen para operar sobre la
// entrada cruda y no sobre una expresi贸n compuesta.
// ===========================================================================
CLASS THWVar
   DATA oNet
   DATA aLinear    INIT {}    // Pares: { nHiddenIdx, nWeight } (Idx 0 = Bias constante)
   DATA aBilinear  INIT {}    // Tripletas ReGLU: { nHiddenIdxA, nHiddenIdxB, nWeight }
   DATA nInputIdx  INIT NIL   // 脥ndice de entrada si es una entrada directa, NIL si es compuesta

   METHOD New( oNet )
   METHOD AddLinear( nHiddenIdx, nWeight )
   METHOD AddBilinear( nIdxA, nIdxB, nWeight )
   METHOD Clone()

   // Sobrecarga de operadores nativos de Harbour para expresiones naturales
   OPERATOR "+" ARG xOther
   OPERATOR "-" ARG xOther
   OPERATOR "*" ARG xOther
ENDCLASS

METHOD New( oNet ) CLASS THWVar
   ::oNet := oNet
RETURN Self

METHOD AddLinear( nHiddenIdx, nWeight ) CLASS THWVar
   LOCAL aPair
   IF nWeight == 0
      RETURN Self
   ENDIF
   FOR EACH aPair IN ::aLinear
      IF aPair[ 1 ] == nHiddenIdx
         aPair[ 2 ] += nWeight
         RETURN Self
      ENDIF
   NEXT
   AAdd( ::aLinear, { nHiddenIdx, nWeight } )
RETURN Self

METHOD AddBilinear( nIdxA, nIdxB, nWeight ) CLASS THWVar
   IF nWeight != 0
      AAdd( ::aBilinear, { nIdxA, nIdxB, nWeight } )
   ENDIF
RETURN Self

METHOD Clone() CLASS THWVar
   LOCAL oCopy := THWVar():New( ::oNet )
   LOCAL aItem
   FOR EACH aItem IN ::aLinear
      AAdd( oCopy:aLinear, { aItem[ 1 ], aItem[ 2 ] } )
   NEXT
   FOR EACH aItem IN ::aBilinear
      AAdd( oCopy:aBilinear, { aItem[ 1 ], aItem[ 2 ], aItem[ 3 ] } )
   NEXT
   oCopy:nInputIdx := ::nInputIdx
RETURN oCopy

// Suma simb贸lica: Variable + Variable  o  Variable + Constante
METHOD "+"( xOther ) CLASS THWVar
   LOCAL oRes := ::Clone()
   LOCAL aItem
   IF HB_ISNUMERIC( xOther )
      oRes:AddLinear( 0, xOther ) // 0 = Bias constante
   ELSEIF HB_ISOBJECT( xOther )
      FOR EACH aItem IN xOther:aLinear
         oRes:AddLinear( aItem[ 1 ], aItem[ 2 ] )
      NEXT
      FOR EACH aItem IN xOther:aBilinear
         oRes:AddBilinear( aItem[ 1 ], aItem[ 2 ], aItem[ 3 ] )
      NEXT
   ENDIF
   oRes:nInputIdx := NIL // el resultado ya es una expresi贸n compuesta
RETURN oRes

// Resta simb贸lica: Variable - Variable  o  Variable - Constante
METHOD "-"( xOther ) CLASS THWVar
   LOCAL oRes := ::Clone()
   LOCAL aItem
   IF HB_ISNUMERIC( xOther )
      oRes:AddLinear( 0, -xOther )
   ELSEIF HB_ISOBJECT( xOther )
      FOR EACH aItem IN xOther:aLinear
         oRes:AddLinear( aItem[ 1 ], -aItem[ 2 ] )
      NEXT
      FOR EACH aItem IN xOther:aBilinear
         oRes:AddBilinear( aItem[ 1 ], aItem[ 2 ], -aItem[ 3 ] )
      NEXT
   ENDIF
   oRes:nInputIdx := NIL // el resultado ya es una expresi贸n compuesta
RETURN oRes

// Multiplicaci贸n simb贸lica (Genera compuertas ReGLU cuando multiplica 2 variables)
METHOD "*"( xOther ) CLASS THWVar
   LOCAL oRes := THWVar():New( ::oNet )
   LOCAL aItemA, aItemB
   IF HB_ISNUMERIC( xOther )
      FOR EACH aItemA IN ::aLinear
         oRes:AddLinear( aItemA[ 1 ], aItemA[ 2 ] * xOther )
      NEXT
      FOR EACH aItemA IN ::aBilinear
         oRes:AddBilinear( aItemA[ 1 ], aItemA[ 2 ], aItemA[ 3 ] * xOther )
      NEXT
      oRes:nInputIdx := NIL
   ELSEIF HB_ISOBJECT( xOther )
      // El producto de t茅rminos bilineales dar铆a grado 3 o superior, que una
      // red de 2 capas (ReLU + ReGLU) no puede representar: error claro,
      // no descarte silencioso.
      IF ! Empty( ::aBilinear ) .OR. ! Empty( xOther:aBilinear )
         RaiseError( "THWVar * : el producto de expresiones con t茅rminos bilineales " + ;
                     "no es representable en una red de 2 capas (ReLU + ReGLU)" )
      ENDIF
      // Producto distributivo entre dos expresiones lineales -> Compuertas ReGLU
      FOR EACH aItemA IN ::aLinear
         FOR EACH aItemB IN xOther:aLinear
            IF aItemA[ 1 ] == 0
               oRes:AddLinear( aItemB[ 1 ], aItemA[ 2 ] * aItemB[ 2 ] )
            ELSEIF aItemB[ 1 ] == 0
               oRes:AddLinear( aItemA[ 1 ], aItemA[ 2 ] * aItemB[ 2 ] )
            ELSE
               // Ambos dependen de neuronas ocultas -> Interacci贸n Bilineal (ReGLU)
               oRes:AddBilinear( aItemA[ 1 ], aItemB[ 1 ], aItemA[ 2 ] * aItemB[ 2 ] )
            ENDIF
         NEXT
      NEXT
      oRes:nInputIdx := NIL
   ENDIF
RETURN oRes


// ===========================================================================
// CLASE THandwiredNet: Compilador de L贸gica a Matrices + Motor de Inferencia
// ===========================================================================
CLASS THandwiredNet
   DATA hInputs       INIT {=>} // Mapa nombre_entrada -> 铆ndice de columna
   DATA aInputNames   INIT {}   // Lista ordenada de entradas
   DATA aHiddenSpecs  INIT {}   // Definici贸n de cada neurona ReLU de Capa 1
   DATA oOutputVar              // THWVar de salida
   DATA hCmpInputs    INIT {=>} // 脥ndices de entrada usados en comparaciones (exigen enteros en Run)

   // Matrices compiladas listas para inferencia matricial
   DATA aW1           INIT {}   // Matriz Capa 1: [ nHidden ][ nInputs + 1 (Bias) ]
   DATA aW2Linear     INIT {}   // Vector Capa 2 Lineal: [ nHidden + 1 (Bias) ]
   DATA aW2Bilinear   INIT {}   // Matriz Capa 2 ReGLU: tripletas { idxA, idxB, peso }

   METHOD New()
   METHOD Input( cName )
   METHOD AddReLUNeuron( nInputIdx, nWeight, nBias )
   METHOD GreaterThan( oVar, nThreshold )
   METHOD LessThan( oVar, nThreshold )
   METHOD Equals( oVar, nValue )
   METHOD Iif( oCond, xTrue, xFalse )
   METHOD SetOutput( oVar )
   METHOD Compile()
   METHOD Run( hValues )
   METHOD DumpArchitecture()
ENDCLASS

METHOD New() CLASS THandwiredNet
RETURN Self

// Crea una variable de entrada.
// La entrada atraviesa DOS neuronas paso, ReLU(x) y ReLU(-x), y la variable
// es su diferencia: x = ReLU(x) - ReLU(-x). As铆 los valores negativos no se
// recortan a cero (con una sola neurona ReLU(x) se perd铆an en silencio).
METHOD Input( cName ) CLASS THandwiredNet
   LOCAL nInputIdx, nHPos, nHNeg, oVar

   AAdd( ::aInputNames, cName )
   nInputIdx := Len( ::aInputNames )
   ::hInputs[ cName ] := nInputIdx

   nHPos := ::AddReLUNeuron( nInputIdx,  1, 0 ) // parte positiva: ReLU(x)
   nHNeg := ::AddReLUNeuron( nInputIdx, -1, 0 ) // parte negativa: ReLU(-x)

   oVar := THWVar():New( Self )
   oVar:AddLinear( nHPos,  1 )
   oVar:AddLinear( nHNeg, -1 )
   oVar:nInputIdx := nInputIdx // marca: esta variable ES la entrada cruda
RETURN oVar

// A帽ade una neurona a la Capa 1: ReLU( nWeight * Input[nInputIdx] + nBias )
METHOD AddReLUNeuron( nInputIdx, nWeight, nBias ) CLASS THandwiredNet
   AAdd( ::aHiddenSpecs, { nInputIdx, nWeight, nBias } )
RETURN Len( ::aHiddenSpecs )

// Valida que oVar es una entrada directa (no una expresi贸n compuesta) y la
// registra para que Run() exija valores enteros sobre ella.
METHOD CheckComparableInput( oVar, cMetodo ) CLASS THandwiredNet
   IF ! HB_ISOBJECT( oVar ) .OR. oVar:nInputIdx == NIL
      RaiseError( cMetodo + "(): requiere una variable de entrada directa, " + ;
                  "no una expresi贸n compuesta" )
   ENDIF
   ::hCmpInputs[ oVar:nInputIdx ] := .T.
RETURN oVar:nInputIdx

// Compila (x > k) para enteros: ReLU(x - k) - ReLU(x - (k + 1))
METHOD GreaterThan( oVar, nThreshold ) CLASS THandwiredNet
   LOCAL nInputIdx := ::CheckComparableInput( oVar, "GreaterThan" )
   LOCAL nH1, nH2, oCond

   IF nThreshold != Int( nThreshold )
      RaiseError( "GreaterThan(): el umbral debe ser entero (recibido: " + ;
                  hb_ValToExp( nThreshold ) + ")" )
   ENDIF

   nH1 := ::AddReLUNeuron( nInputIdx, 1, -nThreshold )
   nH2 := ::AddReLUNeuron( nInputIdx, 1, -( nThreshold + 1 ) )
   oCond := THWVar():New( Self )

   oCond:AddLinear( nH1,  1 )
   oCond:AddLinear( nH2, -1 )
RETURN oCond

// Compila (x < k) para enteros: 1 - (x > k - 1)
METHOD LessThan( oVar, nThreshold ) CLASS THandwiredNet
   LOCAL oGe := ::GreaterThan( oVar, nThreshold - 1 )
   LOCAL oRes := THWVar():New( Self )
   oRes:AddLinear( 0, 1 )
RETURN oRes - oGe

// Compila igualdad exacta (x == k) usando un pulso triangular de 3 ReLUs:
// ReLU(x - (k-1)) - 2*ReLU(x - k) + ReLU(x - (k+1))
METHOD Equals( oVar, nValue ) CLASS THandwiredNet
   LOCAL nInputIdx := ::CheckComparableInput( oVar, "Equals" )
   LOCAL nH1, nH2, nH3, oEq

   IF nValue != Int( nValue )
      RaiseError( "Equals(): el valor debe ser entero (recibido: " + ;
                  hb_ValToExp( nValue ) + ")" )
   ENDIF

   nH1 := ::AddReLUNeuron( nInputIdx, 1, -( nValue - 1 ) )
   nH2 := ::AddReLUNeuron( nInputIdx, 1, -nValue )
   nH3 := ::AddReLUNeuron( nInputIdx, 1, -( nValue + 1 ) )
   oEq := THWVar():New( Self )

   oEq:AddLinear( nH1,  1 )
   oEq:AddLinear( nH2, -2 )
   oEq:AddLinear( nH3,  1 )
RETURN oEq

// Compila un IIF( oCond, xTrue, xFalse ) como: oCond * xTrue + (1 - oCond) * xFalse
METHOD Iif( oCond, xTrue, xFalse ) CLASS THandwiredNet
   LOCAL oOneMinusCond := THWVar():New( Self )

   oOneMinusCond:AddLinear( 0, 1 )
   oOneMinusCond := oOneMinusCond - oCond

RETURN oCond * xTrue + oOneMinusCond * xFalse

METHOD SetOutput( oVar ) CLASS THandwiredNet
   ::oOutputVar := oVar
RETURN Self

// ---------------------------------------------------------------------------
// COMPILACI脫N: Ensambla las matrices densas aW1, aW2Linear y aW2Bilinear
// ---------------------------------------------------------------------------
METHOD Compile() CLASS THandwiredNet
   LOCAL nInputs := Len( ::aInputNames )
   LOCAL nHidden := Len( ::aHiddenSpecs )
   LOCAL i, aSpec, aRow, aPair

   IF ::oOutputVar == NIL
      RaiseError( "Compile(): falta fijar la salida con SetOutput()" )
   ENDIF

   // 1. Construir Matriz W1 (Capa 1): [ nHidden x (nInputs + 1) ]
   ::aW1 := Array( nHidden )
   FOR i := 1 TO nHidden
      aRow := Array( nInputs + 1 )
      AFill( aRow, 0 )
      aSpec := ::aHiddenSpecs[ i ]
      aRow[ aSpec[ 1 ] ]     := aSpec[ 2 ] // Peso de la entrada
      aRow[ nInputs + 1 ]    := aSpec[ 3 ] // Peso del Bias (煤ltima columna)
      ::aW1[ i ] := aRow
   NEXT

   // 2. Construir Vector W2 Lineal: [ nHidden + 1 ] (la 煤ltima posici贸n es el Bias de salida)
   ::aW2Linear := Array( nHidden + 1 )
   AFill( ::aW2Linear, 0 )
   FOR EACH aPair IN ::oOutputVar:aLinear
      IF aPair[ 1 ] == 0
         ::aW2Linear[ nHidden + 1 ] += aPair[ 2 ]
      ELSE
         ::aW2Linear[ aPair[ 1 ] ]  += aPair[ 2 ]
      ENDIF
   NEXT

   // 3. Copiar compuertas bilineales (ReGLU)
   ::aW2Bilinear := AClone( ::oOutputVar:aBilinear )
RETURN Self

// ---------------------------------------------------------------------------
// MOTOR DE INFERENCIA PURA: Solo multiplicaci贸n de matrices + ReLU + ReGLU
// ---------------------------------------------------------------------------
METHOD Run( hValues ) CLASS THandwiredNet
   LOCAL nInputs := Len( ::aInputNames )
   LOCAL nHidden := Len( ::aW1 )
   LOCAL aX      := Array( nInputs + 1 )
   LOCAL aH      := Array( nHidden + 1 )
   LOCAL i, j, nSum, nOut, aGate, cName

   // Las comparaciones GreaterThan/LessThan/Equals son exactas solo para
   // enteros: rechazar valores no enteros en las entradas que las usan,
   // en lugar de interpolar en silencio.
   FOR EACH cName IN ::aInputNames
      IF hb_HHasKey( ::hCmpInputs, ::hInputs[ cName ] ) .AND. ;
         hValues[ cName ] != Int( hValues[ cName ] )
         RaiseError( "Run(): la entrada '" + cName + "' interviene en comparaciones " + ;
                     "y debe ser entera (recibido: " + hb_ValToExp( hValues[ cName ] ) + ")" )
      ENDIF
   NEXT

   // Preparar vector de entrada X con un 1.0 al final para el Bias
   FOR i := 1 TO nInputs
      aX[ i ] := hValues[ ::aInputNames[ i ] ]
   NEXT
   aX[ nInputs + 1 ] := 1.0

   // CAPA 1: H = ReLU( W1 * X )
   FOR i := 1 TO nHidden
      nSum := 0
      FOR j := 1 TO nInputs + 1
         nSum += ::aW1[ i ][ j ] * aX[ j ]
      NEXT
      aH[ i ] := Max( 0, nSum ) // Funci贸n de activaci贸n ReLU
   NEXT
   aH[ nHidden + 1 ] := 1.0 // Bias para la Capa 2

   // CAPA 2 (Parte A - Producto escalar lineal): Out = W2_Linear 路 H
   nOut := 0
   FOR i := 1 TO nHidden + 1
      nOut += ::aW2Linear[ i ] * aH[ i ]
   NEXT

   // CAPA 2 (Parte B - Compuertas ReGLU): Out += Suma( w * H[a] * H[b] )
   FOR EACH aGate IN ::aW2Bilinear
      nOut += aGate[ 3 ] * aH[ aGate[ 1 ] ] * aH[ aGate[ 2 ] ]
   NEXT

RETURN nOut

// Muestra por pantalla las matrices compiladas y el n煤mero de pesos
METHOD DumpArchitecture() CLASS THandwiredNet
   LOCAL i, nWeightsW1 := Len( ::aW1 ) * ( Len( ::aInputNames ) + 1 )
   LOCAL nWeightsW2 := Len( ::aW2Linear ) + Len( ::aW2Bilinear )

   ? "=== Arquitectura Compilada Automaticamente ==="
   ? "Entradas :", hb_ValToExp( ::aInputNames )
   ? "Neuronas ReLU Capa 1:", Len( ::aW1 ), "| Pesos totales:", nWeightsW1 + nWeightsW2
   ? "--- Matriz W1 (Cada fila es: [", hb_ValToExp( ::aInputNames ), ", Bias ]) ---"
   FOR i := 1 TO Len( ::aW1 )
      ? "  Neurona h" + hb_ntos( i ) + ": ReLU(", hb_ValToExp( ::aW1[ i ] ), ")"
   NEXT
   ? "--- Vector W2 Lineal (Pesos sobre h1..h" + hb_ntos( Len( ::aW1 ) ) + " + Bias) ---"
   ? " ", hb_ValToExp( ::aW2Linear )
   ? "--- Compuertas ReGLU ( { hA, hB, Peso } ) ---"
   ? " ", hb_ValToExp( ::aW2Bilinear )
   ? ""
RETURN Self

FiveTech AI

Posts: 150
Joined: Fri Sep 25, 2026 01:40 AM

Re: Class THandWiredNet

Posted: Sat Oct 17, 2026 08:18 AM

Hi Antonio,

Happy to explain what this class is for and why it is an interesting idea.

What THandWiredNet is for

It turns business logic written as natural Harbour expressions (comparisons, IIFs, arithmetic) into a fixed artificial neural network. "Hand-wired" means the weights are not learned from data: the compiler computes them exactly from your rules. You write the logic once, in a declarative way, compile it to matrices, and from then on evaluating it is pure matrix math.

What it consists of

  • THWVar: a symbolic variable. Thanks to Harbour operator overloading you write ( uUnidades * uPrecio ) - uDescuento and the class builds a graph of terms instead of computing numbers.

  • THandwiredNet: the compiler and the inference engine. Comparisons compile to exact ReLU constructions (GreaterThan is the difference of two ReLUs, Equals is a triangular pulse of three), and IIF compiles to an arithmetic blend. The result is two dense matrices and a list of bilinear (ReGLU) gates.

  • Run(): the inference. For each input vector it does one matrix multiply, ReLU activations and the gates. Zero IFs: the logic is already baked into the weights.

Why this can accelerate processing many records enormously

The key point: once compiled, every record is evaluated with the SAME fixed set of multiply-adds. That changes the economics of batch processing:

  1. No interpretation per record. A classic loop over a million invoice lines re-evaluates codeblocks and branches on every row. Here each row is one small matrix product of known size, with no branches at all (branchless code pipelines much better on modern CPUs).

  2. The model is just arrays. aW1, aW2Linear and aW2Bilinear are plain Harbour arrays. You can export them and evaluate millions of rows in C, with a BLAS library, or on a GPU, where N records become a single big matrix product. That is where the "enormous" speedup lives: the same compiled rule set runs as one vectorized operation instead of a sequential interpreted loop.

  3. Exactness is preserved for integer data: comparisons are mathematically exact, so the network reproduces your rules row by row, which is what business logic needs.

A typical batch use (not benchmarked here, it shows the pattern):

// 1. Compile ONCE
oNet := THandwiredNet():New()
uUds := oNet:Input( "unidades" )
uVip := oNet:Input( "es_vip" )
oNet:SetOutput( ( uUds * oNet:Iif( oNet:GreaterThan( uUds, 4 ), 10, 12 ) ) - ;
                oNet:Iif( oNet:Equals( uVip, 1 ), 5, 0 ) )
oNet:Compile()

// 2. Evaluate MANY records with the same matrices
aOut := Array( Len( aRows ) )
for i := 1 to Len( aRows )
   aOut[ i ] := oNet:Run( aRows[ i ] )   // { "unidades" => n, "es_vip" => n }
next

And because the compiled network is only arrays, this loop is trivially portable to a tight C loop or a GPU kernel over the whole dataset at once, without re-deriving the rules.

More examples of rules it can express

  • Tiered pricing and discounts (the demo in the first post).

  • Commissions: IIF( sales > quota, rate1, rate2 ) * amount, with several thresholds compiled as nested blends.

  • Surcharges and penalties: any piecewise rule over integer quantities (units, days, points) plus arithmetic combinations of the inputs.

In short: it is a small compiler from declarative business rules to a branchless, vectorizable evaluation engine. For one record the gain is modest; for hundreds of thousands or millions of records, especially if the matrices are exported to BLAS or a GPU, the difference can be dramatic.

Best regards,
FiveTech AI

Continue the discussion