Skip to content

05_05_09 - Hierarchical Composition

📋 Descripción

Sistema de composición jerárquica que permite crear sub-topologías (topologías anidadas) como nodos reutilizables. Facilita la modularización, encapsulación y reutilización de componentes DSP complejos.

🎯 Objetivos

  1. Sub-topologías: Encapsular grafos completos como nodos compuestos
  2. Interfaz externa: Exponer puertos y parámetros selectivamente
  3. Parameter forwarding: Propagar parámetros a través de la jerarquía
  4. Flattening: Convertir jerarquía a topología plana para ejecución
  5. Module library: Biblioteca de componentes reutilizables

🏗️ Arquitectura

Componentes Principales

hierarchical_topology.hpp/cpp    # Sistema jerárquico
├── SubTopology                 # Topología como módulo reutilizable
├── HierarchicalTopology        # Topología con sub-topologías
├── TopologyFlattener          # Flatten jerarquía → topología plana
├── HierarchicalBuilder        # Fluent API para construcción
├── SubTopologyFactory         # Factory de módulos built-in
├── HierarchyAnalyzer          # Análisis de estructura jerárquica
├── ParameterForwarder         # Forwarding de parámetros
└── ModuleLibrary              # Registro de módulos reutilizables

Flujo de Trabajo

Build Hierarchy
[SubTopology] → Encapsulate internal graph
[HierarchicalTopology] → Compose with sub-topologies
[Flattener] → Flatten to single-level topology
Execute (using existing pipeline)

📦 Sub-Topology (Módulo)

Concepto

Una SubTopology es una topología completa que se puede usar como un nodo dentro de otra topología. Define:

  • Topología interna: Grafo de nodos y conexiones
  • Interfaz externa: Puertos y parámetros expuestos
  • Mapeos: Relación entre interfaz externa ↔ nodos internos

Crear Sub-Topology Manualmente

auto sub = std::make_shared<SubTopology>("my_filter");

// 1. Construir topología interna
auto& internal = sub->internal_topology();

Node input_gain{"gain_in", "multiply_scalar", NodeType::Processing};
Node filter{"lpf", "biquad_lowpass", NodeType::Processing};
Node output_gain{"gain_out", "multiply_scalar", NodeType::Processing};

internal.addNode(input_gain);
internal.addNode(filter);
internal.addNode(output_gain);

// Conexiones internas
internal.addEdge({"gain_in", "out", "lpf", "in"});
internal.addEdge({"lpf", "out", "gain_out", "in"});

// 2. Definir interfaz externa
Port input{"in", PortDirection::Input, 512};
Port output{"out", PortDirection::Output, 512};

// Mapear puertos externos → nodos internos
sub->add_external_input(input, "gain_in", "in");
sub->add_external_output(output, "gain_out", "out");

// 3. Exponer parámetros
Parameter cutoff{"cutoff", 1000.0f};
Parameter gain{"gain", 1.0f};

sub->add_external_parameter(cutoff, "lpf", "fc");
sub->add_external_parameter(gain, "gain_out", "scalar");

Usar Sub-Topology Factory

// Módulos built-in
auto biquad = SubTopologyFactory::create_biquad_filter();
auto delay = SubTopologyFactory::create_stereo_delay();
auto compressor = SubTopologyFactory::create_compressor();
auto reverb = SubTopologyFactory::create_reverb();
auto eq_band = SubTopologyFactory::create_eq_band();

🏗️ Hierarchical Topology

Crear Topología Jerárquica

HierarchicalBuilder builder;

// Topología top-level
builder.setName("channel_strip")
    .addNode("input", "external_input", NodeType::Source)
    .addNode("output", "external_output", NodeType::Sink);

// Agregar nodos compuestos (sub-topologies)
auto eq = SubTopologyFactory::create_eq_band();
auto compressor = SubTopologyFactory::create_compressor();

builder.addCompoundNode("eq", eq)
       .addCompoundNode("comp", compressor);

// Conexiones (igual que topología normal)
builder.connect("input", "out", "eq", "in")
       .connect("eq", "out", "comp", "in")
       .connect("comp", "out", "output", "in");

// Parámetros en nodos compuestos
builder.setParameter("eq", "cutoff", 5000.0f)
       .setParameter("eq", "Q", 1.0f)
       .setParameter("comp", "threshold", -12.0f);

auto hierarchical = builder.build();

Inspeccionar Jerarquía

std::cout << "Compound nodes: " << hierarchical.get_compound_nodes().size() << "\n";
std::cout << "Depth: " << hierarchical.get_depth() << "\n";

for (const auto& node_id : hierarchical.get_compound_nodes()) {
    auto sub = hierarchical.get_sub_topology(node_id);
    std::cout << "  - " << node_id << ": " << sub->name() << "\n";
    std::cout << "    Internal nodes: "
              << sub->internal_topology().nodes().size() << "\n";
}

🔄 Flattening (Aplanamiento)

Convertir a Topología Plana

El flattening convierte la jerarquía multinivel en una topología plana de un solo nivel, lista para ejecución:

// Topología jerárquica
HierarchicalTopology hierarchical = builder.build();

// Flatten a topología ejecutable
FlatteningOptions options;
options.preserve_hierarchy_metadata = true;
options.inline_single_node_subs = true;
options.id_separator = "_";

Topology flat = TopologyFlattener::flatten(hierarchical, options);

std::cout << "Original nodes (top-level): "
          << hierarchical.top_level().nodes().size() << "\n";
std::cout << "Flattened nodes (total): "
          << flat.nodes().size() << "\n";

Proceso de Flattening

  1. Expandir nodos compuestos: Reemplazar cada nodo compuesto por sus nodos internos
  2. Generar IDs jerárquicos: eq_lpf, eq_gain_out (evita colisiones)
  3. Redirigir conexiones:
  4. Conexiones a puerto externo → puerto interno correspondiente
  5. Conexiones desde puerto externo → puerto interno correspondiente
  6. Eliminar nodos compuestos: Quedan solo nodos atómicos

Ejemplo de IDs Jerárquicos

Original (jerárquico):
  - eq (compound)
    - gain_in
    - lpf
    - gain_out

Flattened (plano):
  - eq_gain_in
  - eq_lpf
  - eq_gain_out

🎛️ Parameter Forwarding

Propagación de Parámetros

// Cambiar parámetro en nodo compuesto
ParameterForwarder::forward_parameter(
    hierarchical,
    "eq",           // Nodo compuesto
    "cutoff",       // Parámetro externo
    2000.0f         // Nuevo valor
);

// Esto se propaga automáticamente a:
//   eq.lpf.fc = 2000.0f (según el mapeo definido)

Rutas de Parámetros

// Obtener todas las rutas de un parámetro
auto paths = ParameterForwarder::get_parameter_paths(hierarchical, "cutoff");

for (const auto& path : paths) {
    std::cout << path << "\n";
}

// Output:
//   eq.lpf.fc
//   comp.filter.fc

Resolver Parámetro por Ruta

auto value = ParameterForwarder::resolve_parameter(
    hierarchical,
    "eq.lpf.fc"
);

if (value.has_value()) {
    std::cout << "Value: " << value.value() << "\n";
}

📊 Análisis de Jerarquía

Estadísticas

auto stats = HierarchyAnalyzer::analyze(hierarchical);

std::cout << "Hierarchy Statistics:\n";
std::cout << "  Total nodes: " << stats.total_nodes << "\n";
std::cout << "  Top-level nodes: " << stats.top_level_nodes << "\n";
std::cout << "  Compound nodes: " << stats.compound_nodes << "\n";
std::cout << "  Leaf nodes: " << stats.leaf_nodes << "\n";
std::cout << "  Max depth: " << stats.max_depth << "\n";
std::cout << "  Avg children per compound: "
          << stats.average_children_per_compound << "\n";
std::cout << "  Hierarchy ratio: " << stats.hierarchy_ratio * 100 << "%\n";

Árbol Jerárquico

auto tree = HierarchyAnalyzer::get_hierarchy_tree(hierarchical);

for (const auto& [parent, children] : tree) {
    std::cout << parent << ":\n";
    for (const auto& child : children) {
        std::cout << "  - " << child << "\n";
    }
}

Nodos por Nivel

// Nivel 0 (top-level)
auto level0 = HierarchyAnalyzer::get_nodes_at_depth(hierarchical, 0);

// Nivel 1 (dentro de sub-topologies)
auto level1 = HierarchyAnalyzer::get_nodes_at_depth(hierarchical, 1);

std::cout << "Level 0: " << level0.size() << " nodes\n";
std::cout << "Level 1: " << level1.size() << " nodes\n";

📚 Module Library

Registro de Módulos

// Registrar módulos built-in
ModuleLibrary::register_builtin_modules();

// Listar módulos disponibles
auto& lib = ModuleLibrary::instance();
auto modules = lib.list_modules();

std::cout << "Available modules:\n";
for (const auto& name : modules) {
    std::cout << "  - " << name << "\n";
}

// Output:
//   - biquad_filter
//   - stereo_delay
//   - compressor
//   - reverb
//   - eq_band

Instanciar Módulos

// Obtener módulo (shared)
auto eq_module = lib.get_module("biquad_filter");

// Instanciar (copia independiente)
auto eq1 = lib.instantiate("biquad_filter");
auto eq2 = lib.instantiate("biquad_filter");

// Cada instancia es independiente

Registrar Módulos Personalizados

// Crear módulo personalizado
auto custom = std::make_shared<SubTopology>("my_distortion");
// ... build internal topology ...

// Registrar en biblioteca
ModuleLibrary::instance().register_module("distortion", custom);

// Usar en construcción
auto dist = ModuleLibrary::instance().instantiate("distortion");
builder.addCompoundNode("dist", dist);

📝 Ejemplo Completo: EQ de 3 Bandas

#include "hierarchical_topology.hpp"

using namespace audiolab::topology::hierarchical;

int main() {
    // 1. Registrar módulos built-in
    ModuleLibrary::register_builtin_modules();

    // 2. Construir topología jerárquica
    HierarchicalBuilder builder;

    builder.setName("three_band_eq")
        .addNode("input", "external_input", NodeType::Source)
        .addNode("output", "external_output", NodeType::Sink);

    // Instanciar 3 bandas de EQ
    auto low_band = ModuleLibrary::instance().instantiate("eq_band");
    auto mid_band = ModuleLibrary::instance().instantiate("eq_band");
    auto high_band = ModuleLibrary::instance().instantiate("eq_band");

    builder.addCompoundNode("low", low_band)
           .addCompoundNode("mid", mid_band)
           .addCompoundNode("high", high_band);

    // Conexiones en serie
    builder.connect("input", "out", "low", "in")
           .connect("low", "out", "mid", "in")
           .connect("mid", "out", "high", "in")
           .connect("high", "out", "output", "in");

    // Configurar cada banda
    builder.setParameter("low", "fc", 200.0f)     // Low shelf @ 200Hz
           .setParameter("low", "Q", 0.707f)
           .setParameter("low", "gain", 0.0f)

           .setParameter("mid", "fc", 1000.0f)    // Peaking @ 1kHz
           .setParameter("mid", "Q", 1.0f)
           .setParameter("mid", "gain", 0.0f)

           .setParameter("high", "fc", 8000.0f)   // High shelf @ 8kHz
           .setParameter("high", "Q", 0.707f)
           .setParameter("high", "gain", 0.0f);

    auto hierarchical = builder.build();

    // 3. Analizar jerarquía
    std::cout << "=== Hierarchical Topology ===\n";
    auto stats = HierarchyAnalyzer::analyze(hierarchical);
    std::cout << "Total nodes: " << stats.total_nodes << "\n";
    std::cout << "Compound nodes: " << stats.compound_nodes << "\n";
    std::cout << "Depth: " << stats.max_depth << "\n\n";

    // 4. Flatten para ejecución
    FlatteningOptions flatten_opts;
    flatten_opts.id_separator = "_";

    Topology flat = TopologyFlattener::flatten(hierarchical, flatten_opts);

    std::cout << "=== Flattened Topology ===\n";
    std::cout << "Total nodes: " << flat.nodes().size() << "\n";
    std::cout << "Total edges: " << flat.edges().size() << "\n\n";

    std::cout << "Flattened node IDs:\n";
    for (const auto& [node_id, node] : flat.nodes()) {
        std::cout << "  - " << node_id << " (" << node.type << ")\n";
    }

    // 5. Ahora se puede usar con el resto del pipeline
    // (causality validation, dependency analysis, buffer management, code gen)

    return 0;
}

Salida Esperada

=== Hierarchical Topology ===
Total nodes: 35  (2 source/sink + 3 compound + ~30 internal)
Compound nodes: 3
Depth: 2

=== Flattened Topology ===
Total nodes: 33  (2 source/sink + 31 expanded internal nodes)
Total edges: 42

Flattened node IDs:
  - input (external_input)
  - output (external_output)
  - low_z1 (delay_1sample)
  - low_z2 (delay_1sample)
  - low_mul_b0 (multiply_scalar)
  - low_mul_b1 (multiply_scalar)
  - low_mul_b2 (multiply_scalar)
  - low_mul_a1 (multiply_scalar)
  - low_mul_a2 (multiply_scalar)
  - low_add1 (add)
  - low_add2 (add)
  - low_add3 (add)
  - low_add4 (add)
  - mid_z1 (delay_1sample)
  - mid_z2 (delay_1sample)
  ... (similar para mid y high)

🔗 Integración con Pipeline Existente

Workflow Completo

// 1. Construir jerarquía
HierarchicalBuilder builder;
// ... build hierarchical topology ...
auto hierarchical = builder.build();

// 2. Flatten
Topology topology = TopologyFlattener::flatten(hierarchical);

// 3. Validar (usa pipeline existente)
auto validation = CausalityValidator::validate(topology);
if (!validation.is_causal) {
    std::cerr << "Causality validation failed\n";
    return;
}

// 4. Analizar dependencias
auto analysis = DependencyAnalyzer::analyze(topology);

// 5. Optimizar memoria
auto buffer_plan = BufferManager::createPlan(topology, analysis.execution_order);

// 6. Generar código
auto code = CodeGenerator::generate(topology, analysis, buffer_plan, options);
code.saveToFiles("./generated/");

Ventajas de Jerarquía + Pipeline

Característica Beneficio
Modularidad Componentes reutilizables (EQ band, compressor)
Abstracción Oculta complejidad interna
Composición Combina módulos fácilmente
Flattening Compatible con pipeline existente
Debugging Jerarquía preserva estructura lógica

🎯 Casos de Uso

1. Channel Strip

auto eq = ModuleLibrary::instance().instantiate("eq_band");
auto comp = ModuleLibrary::instance().instantiate("compressor");
auto gate = ModuleLibrary::instance().instantiate("noise_gate");

builder.addCompoundNode("eq", eq)
       .addCompoundNode("comp", comp)
       .addCompoundNode("gate", gate)
       .connect("input", "out", "gate", "in")
       .connect("gate", "out", "eq", "in")
       .connect("eq", "out", "comp", "in")
       .connect("comp", "out", "output", "in");

2. Reverb con Pre-Delay

auto delay = SubTopologyFactory::create_stereo_delay();
auto reverb = SubTopologyFactory::create_reverb();

builder.addCompoundNode("pre_delay", delay)
       .addCompoundNode("reverb_engine", reverb)
       .connect("input", "out", "pre_delay", "in")
       .connect("pre_delay", "out", "reverb_engine", "in")
       .connect("reverb_engine", "out", "output", "in");

3. Multi-Band Processor

// Crear sub-topology para 1 banda
auto create_band = []() {
    auto band = std::make_shared<SubTopology>("mb_band");

    // Filter + Compressor por banda
    auto lpf = SubTopologyFactory::create_biquad_filter();
    auto hpf = SubTopologyFactory::create_biquad_filter();
    auto comp = SubTopologyFactory::create_compressor();

    // ... build internal topology with crossover + comp ...

    return band;
};

// Instanciar 4 bandas
for (int i = 0; i < 4; i++) {
    auto band = create_band();
    builder.addCompoundNode("band_" + std::to_string(i), band);
}

// Configurar crossover frequencies, etc.

📈 Métricas de Rendimiento

Overhead de Flattening

Operación Complejidad Tiempo Típico
Flatten O(V + E) <1ms para 100 nodos
ID remapping O(V) Negligible
Edge redirect O(E) Negligible

El flattening es una operación offline (compile-time), por lo que no afecta al rendimiento en tiempo real.

Comparación: Manual vs Jerárquico

Aspecto Manual (Flat) Jerárquico
LOC ~200 líneas ~50 líneas
Reutilización Copiar/pegar Instanciar módulo
Mantenimiento Difícil Fácil (cambio en módulo)
Debugging IDs planos IDs jerárquicos informativos
Performance Igual Igual (después de flatten)

🚀 Estado del Sistema

  • SubTopology: Encapsulación completa de topologías internas
  • HierarchicalTopology: Composición con sub-topologías
  • TopologyFlattener: Flatten con IDs jerárquicos y remap
  • HierarchicalBuilder: Fluent API para construcción
  • SubTopologyFactory: 5 módulos built-in
  • HierarchyAnalyzer: Estadísticas y análisis de estructura
  • ParameterForwarder: Propagación de parámetros
  • ModuleLibrary: Registro y instanciación de módulos

📚 Referencias

  1. Hierarchical Systems: Abelson, H. "Structure and Interpretation of Computer Programs"
  2. Component-Based Design: Szyperski, C. "Component Software"
  3. Graph Flattening: Cormen et al. "Introduction to Algorithms" (Graph algorithms)
  4. DSP Architecture: Puckette, M. "The Theory and Technique of Electronic Music"

Subsistema: 05_MODULES → 05_05_TOPOLOGY_DESIGN → 05_05_09_hierarchical_composition Autor: AudioLab Development Team Versión: 1.0.0 Última actualización: 2025-10-10