7 minute read

Nel mondo della composizione algoritmica e, più in generale, nella gestione di sistemi complessi, ci troviamo spesso a dover semplificare. Immagina di avere un sistema descritto da molteplici parametri, ma di volerlo controllare o rappresentare con un singolo valore intuitivo. Questo processo, noto come riduzione dimensionale, è potente ma nasconde delle insidie: i conflitti di mappatura, dove configurazioni di input diverse producono lo stesso identico output ridotto.

Nel mio progetto di composizione algoritmica, Delta-Engine, mi sono scontrato proprio con questo problema. Il sistema descrive uno “stato armonico” attraverso tre parametri normalizzati (da 0 a 1):

  1. Densità Armonica: Quanti registri sono attivi simultaneamente.
  2. Spread di Ottave: Ampiezza della distribuzione verticale delle ottave.
  3. Centroide Spettrale: Baricentro frequenziale dell’energia sonora.

Inizialmente, avevo pensato a una semplice formula ponderata per mapparli a un singolo valore di “stato armonico”:

# Formula originale (causa di conflitti)
def stato_armonico_ponderato(centroide, densita, spread):
    return 0.5 * centroide + 0.3 * densita + 0.2 * spread

Questa formula è intuitiva, ma ha un grosso limite: diverse combinazioni di centroide, densita e spread possono facilmente produrre lo stesso stato_armonico_ponderato. Ad esempio:

  • (C=1.0, D=0.0, S=0.0) -> SA = 0.5 * 1.0 + 0.3 * 0.0 + 0.2 * 0.0 = 0.5
  • (C=0.0, D=1.0, S=1.0) -> SA = 0.5 * 0.0 + 0.3 * 1.0 + 0.2 * 1.0 = 0.5

Due stati armonicamente molto diversi vengono mappati allo stesso identico valore! Questo rende difficile creare transizioni fluide e prevedibili, poiché il sistema “perde” informazione.

La Soluzione: Discretizzare e Ordinare per l’Unicità

Per superare questo problema, ho adottato una tecnica di Mappatura Basata sulla Concatenazione Discretizzata. L’idea di fondo è trattare i nostri tre parametri come le coordinate di una cella in una griglia 3D e poi “srotolare” questa griglia in una singola linea, assegnando un numero unico a ogni cella. Questo garantisce che, dopo la discretizzazione, ogni combinazione unica dei parametri originali produca un valore 1D unico.

Passo dopo Passo: Come Funziona l’Algoritmo

Vediamo come implementare questa mappatura.

1. Discretizzazione dei Parametri

Il primo passo è decidere quanti livelli distinti vogliamo per ogni parametro. Chiamiamo questo numero LIVELLI_K. Se LIVELLI_K = 100, ogni parametro (che varia da 0 a 1) verrà mappato a un indice intero da 0 a 99.

import numpy as np # O math.round per scalari

LIVELLI_K = 100 # Numero di livelli di discretizzazione

# Ipotizziamo che i parametri 'centroide', 'densita', 'spread' siano tra 0 e 1
centroide_originale = 0.78
densita_originale = 0.5
spread_originale = 0.23

# Calcoliamo gli indici interi
centroide_int = int(round(centroide_originale * (LIVELLI_K - 1)))
densita_int = int(round(densita_originale * (LIVELLI_K - 1)))
spread_int = int(round(spread_originale * (LIVELLI_K - 1)))

# Assicuriamoci che gli indici siano nei limiti [0, LIVELLI_K-1]
centroide_int = max(0, min(LIVELLI_K - 1, centroide_int))
densita_int = max(0, min(LIVELLI_K - 1, densita_int))
spread_int = max(0, min(LIVELLI_K - 1, spread_int))

# Esempio con LIVELLI_K = 100:
# centroide_int = round(0.78 * 99) = round(77.22) = 77
# densita_int = round(0.5 * 99) = round(49.5) = 50
# spread_int = round(0.23 * 99) = round(22.77) = 23

2. Combinazione degli Indici in un Valore Unico

Ora combiniamo questi indici interi in un singolo numero intero. L’ordine con cui li combiniamo è cruciale e dovrebbe riflettere l’importanza relativa dei parametri. Nella mia formula originale, il Centroide aveva il peso maggiore (0.5), seguito dalla Densità (0.3) e infine dallo Spread (0.2). Manteniamo questa gerarchia: il Centroide sarà la cifra più significativa.

# Continuando l'esempio precedente (LIVELLI_K = 100)
# centroide_int = 77
# densita_int = 50
# spread_int = 23

valore_unico_int = (centroide_int * (LIVELLI_K * LIVELLI_K) +
                    densita_int * LIVELLI_K +
                    spread_int)

# valore_unico_int = (77 * (100 * 100) + 
#                     50 * 100 + 
#                     23)
#                  = (77 * 10000 + 
#                     5000 + 
#                     23)
#                  = 770000 + 5000 + 23 = 775023

Questo valore_unico_int è ora un identificatore univoco per la combinazione discretizzata dei nostri tre parametri.

3. Normalizzazione (Opzionale)

Se desideriamo che il nostro “stato armonico unico” finale sia nuovamente compreso nell’intervallo [0, 1] (come i parametri originali), possiamo normalizzare valore_unico_int. Per fare ciò, lo dividiamo per il valore massimo possibile che valore_unico_int può assumere.

# Il valore massimo si ha quando tutti gli indici sono (LIVELLI_K - 1)
max_val_int = ((LIVELLI_K - 1) * (LIVELLI_K * LIVELLI_K) +
               (LIVELLI_K - 1) * LIVELLI_K +
               (LIVELLI_K - 1))

# Con LIVELLI_K = 100, max_val_int = (99*10000) + (99*100) + 99 = 990000 + 9900 + 99 = 999999

if max_val_int == 0: # Evita divisione per zero se LIVELLI_K = 1
    stato_armonico_unico_normalizzato = 0.0
else:
    stato_armonico_unico_normalizzato = valore_unico_int / max_val_int

# stato_armonico_unico_normalizzato = 775023 / 999999 ≈ 0.775023775...

4. Un Esempio Pratico: Risolvere un Conflitto

Torniamo ai due stati che prima generavano un conflitto (entrambi SA=0.5) e vediamo come si comportano con LIVELLI_K = 100:

Stato A (originale: C=1.0, D=0.0, S=0.0):

  • C_int = round(1.0 * 99) = 99
  • D_int = round(0.0 * 99) = 0
  • S_int = round(0.0 * 99) = 0
  • val_A_int = (99 * 10000) + (0 * 100) + 0 = 990000
  • sa_A_unico = 990000 / 999999 ≈ 0.99000099…

Stato B (originale: C=0.0, D=1.0, S=1.0):

  • C_int = round(0.0 * 99) = 0
  • D_int = round(1.0 * 99) = 99
  • S_int = round(1.0 * 99) = 99
  • val_B_int = (0 * 10000) + (99 * 100) + 99 = 0 + 9900 + 99 = 9999
  • sa_B_unico = 9999 / 999999 ≈ 0.00999900…

Come possiamo vedere, i due stati ora producono valori di “stato armonico unico” completamente diversi! Il conflitto è risolto.

Il Ruolo Chiave della Granularità: Scegliere LIVELLI_K

Il parametro LIVELLI_K è fondamentale:

  • Un LIVELLI_K basso (es. 10) significa meno livelli discreti. Questo potrebbe portare a “conflitti di discretizzazione”, dove due configurazioni di input (C,D,S) molto vicine tra loro vengono arrotondate alla stessa combinazione di indici interi (C_int, D_int, S_int), producendo quindi lo stesso stato_armonico_unico.
  • Un LIVELLI_K alto (es. 100, 200 o più) aumenta la granularità. Ciò riduce la probabilità di conflitti di discretizzazione, permettendo di distinguere stati di input molto simili. Tuttavia, aumenta anche il numero totale di stati unici possibili (LIVELLI_K al cubo).

La scelta di LIVELLI_K dipende dalla sensibilità desiderata nel vostro sistema.

Pro e Contro di Questa Tecnica

Pro:

  • Unicità Garantita (dopo discretizzazione): Ogni combinazione distinta di parametri discretizzati produce un valore 1D unico.
  • Ordinamento Strutturato: Il valore 1D riflette l’ordine di importanza dato ai parametri (Centroide > Densità > Spread nel nostro caso). Transizioni lungo la scala 1D tendono a essere più prevedibili.
  • Controllo della Granularità: LIVELLI_K permette di bilanciare risoluzione e numero totale di stati.

Contro:

  • Perdita di Continuità Pura: I parametri vengono trattati come discreti. Se è necessaria una sensibilità infinitesimale, questo approccio introduce una granularità.
  • Interpretazione Diversa: Il valore 1D non è più una “media” intuitiva, ma un indice normalizzato che rappresenta una posizione in uno spazio di stati ordinato.
  • Effetto “Scalino”: Le transizioni tra valori 1D adiacenti corrispondono a un cambiamento nel più piccolo dei parametri discretizzati (lo Spread, nel nostro ordinamento), o un cambiamento più grande in un parametro più significativo.

L’implementazione JavaScript completa per questa visualizzazione interattiva, inclusa la logica per Plotly.js, è disponibile nel repository.

Sentiti libero di esplorarlo, modificarlo o trarne ispirazione per i tuoi progetti!

Conclusione: Un Nuovo Livello di Controllo

La mappatura basata sulla concatenazione discretizzata offre un metodo robusto per convertire uno spazio multi-parametro in un singolo valore dimensionale, eliminando i conflitti inerenti a formule di proiezione più semplici come le medie ponderate. Sebbene introduca una discretizzazione, il controllo sulla granularità e l’ordinamento gerarchico dei parametri la rendono una tecnica preziosa per sistemi che richiedono transizioni prevedibili e una rappresentazione univoca degli stati.

Per il Delta-Engine, questo approccio ha significato poter definire e navigare lo spazio armonico con maggiore precisione e senza ambiguità.

Spero questa spiegazione vi sia utile per i vostri progetti!

Appendice: Funzione Python Completa

Ecco una possibile implementazione della funzione in Python:

import numpy as np # O import math per scalari

def calcola_stato_armonico_unico(centroide, densita, spread, livelli_k):
    """
    Calcola un valore di stato armonico unico mappando i parametri 3D discretizzati
    a uno spazio 1D. L'ordine di importanza è Centroide > Densità > Spread.
    I parametri di input (centroide, densita, spread) sono attesi nell'intervallo [0, 1].
    Restituisce un valore normalizzato [0, 1].
    """

    # Validazione input (semplificata per l'esempio, gestire scalari/array come necessario)
    params = [centroide, densita, spread]
    if not all(0 <= p <= 1 for p in params):
        raise ValueError("I parametri devono essere nell'intervallo [0, 1]")
    if not isinstance(livelli_k, int) or livelli_k < 1:
        raise ValueError("livelli_k deve essere un intero positivo.")

    # Discretizza ogni parametro
    # Usiamo np.round per coerenza con l'esempio, math.round per scalari puri
    centroide_int = int(np.round(centroide * (livelli_k - 1)))
    densita_int = int(np.round(densita * (livelli_k - 1)))
    spread_int = int(np.round(spread * (livelli_k - 1)))

    # Assicura che gli indici rimangano nell'intervallo [0, k-1]
    centroide_int = max(0, min(livelli_k - 1, centroide_int))
    densita_int = max(0, min(livelli_k - 1, densita_int))
    spread_int = max(0, min(livelli_k - 1, spread_int))

    # Combina gli indici in un valore intero unico
    valore_unico_int = (centroide_int * (livelli_k * livelli_k) +
                        densita_int * livelli_k +
                        spread_int)

    # Normalizza il valore nell'intervallo [0, 1]
    if livelli_k == 1: # Caso speciale per evitare divisione per zero e gestire output
        return 0.0
    
    max_val_int = ((livelli_k - 1) * (livelli_k * livelli_k) +
                   (livelli_k - 1) * livelli_k +
                   (livelli_k - 1))

    stato_armonico_normalizzato = valore_unico_int / max_val_int
    return stato_armonico_normalizzato

# Esempio d'uso:
# sa_test = calcola_stato_armonico_unico(0.78, 0.5, 0.23, livelli_k=100)
# print(f"Stato Armonico Unico Test: {sa_test}")