Pular para o conteúdo principal

Motor Evolucionário e Otimização no Simplex 🧬

O otimizador de alocações do Monvix (FinancialGeneticOptimizer) implementa um Algoritmo Genético contínuo projetado especificamente para convergir sobre o simplex unitário de 8 dimensões.


📐 Representação Cromossômica

O cromossomo de cada indivíduo é modelado como um vetor de pesos reais wR8w \in \mathbb{R}^8:

w=[wfoodwtransportwhealthwfamily_supportwlifestylewdebtwemergencywinvestments]w = \begin{bmatrix} w_{\text{food}} \\ w_{\text{transport}} \\ w_{\text{health}} \\ w_{\text{family\_support}} \\ w_{\text{lifestyle}} \\ w_{\text{debt}} \\ w_{\text{emergency}} \\ w_{\text{investments}} \end{bmatrix}

A Restrição do Simplex (Simplex Constraint)

Todo indivíduo na população deve estritamente respeitar o simplex canônico Δ7\Delta^7:

i=18wi=1.0ewi0i{1,,8}\sum_{i=1}^8 w_i = 1.0 \quad \text{e} \quad w_i \ge 0 \quad \forall i \in \{1, \dots, 8\}

[!IMPORTANT] Isolamento de Obrigações Fixas: A moradia (housing) e a pensão alimentícia judicial (alimonyAmount) são avaliadas previamente pela função evaluate_obligations(). O Algoritmo Genético opera exclusivamente sobre o saldo flexível remanescente (flexible_balance). Se a renda do usuário apresentar déficit imediato para cobrir obrigações vitais, o otimizador bypassa a evolução e retorna alocações zeradas com sinalização de déficit.


⚙️ Hiperparâmetros do Algoritmo

Os hiperparâmetros foram calibrados empiricamente em mais de 100 cenários de teste (app/tests/test_optimizer.py):

ParâmetroValorJustificativa de Engenharia
Tamanho da População (population_size)120Garante diversidade genética ampla no simplex Δ7\Delta^7 sem degradar tempo de CPU.
Gerações (generations)60Suficiente para convergência assintótica em menos de 15 milissegundos com NumPy.
Taxa de Crossover (crossover_rate)0.85Alta recombinação entre indivíduos com bons traços de adaptação orçamentária.
Taxa de Mutação (mutation_rate)0.25Impede estagnação prematura em ótimos locais de sub-alocação.
Tamanho da Elite (elite_size)4Preservação estrita dos 4 melhores indivíduos sem alteração para a geração seguinte.
Seleção por Torneio (tournament_size)3Pressão seletiva equilibrada sem eliminação precoce de diversidade.

🧬 Operadores Genéticos

1. Inicialização Populacional (Distribuição de Dirichlet)

Para acelerar a convergência sem perder diversidade estocástica, a população inicial não é gerada aleatoriamente de forma uniforme. Em vez disso, é amostrada a partir de uma Distribuição de Dirichlet parametrizada pelo vetor de metas preliminar TT:

Populac¸a˜o0Dirichlet(α=50T+1.0)\text{População}_0 \sim \text{Dirichlet}(\alpha = 50 \cdot T + 1.0)

Isso garante que toda a população inicial já nasça dentro do simplex (wi=1.0\sum w_i = 1.0) e concentrada na vizinhança plausível do perfil do usuário.

2. Crossover Aritmético Convexo

Para dois pais p1p_1 e p2p_2 selecionados por torneio de tamanho 3, o cruzamento gera um filho linearmente combinado:

filho=αp1+(1α)p2,onde αU(0.2,0.8)\text{filho} = \alpha \cdot p_1 + (1 - \alpha) \cdot p_2, \quad \text{onde } \alpha \sim U(0.2, 0.8)

Como Δ7\Delta^7 é um conjunto convexo, a combinação convexa de dois pontos no simplex permanece garantidamente dentro do simplex.

3. Mutação Gaussiana com Projeção

Quando ocorre mutação, uma perturbação estocástica é aplicada:

wi=max(0,wi+N(0,σ=0.03))w'_i = \max(0, w_i + \mathcal{N}(0, \sigma = 0.03))

Após a perturbação, o vetor é renormalizado para que sua soma retorne a 1.01.0:

wi=wij=18wjw_i = \frac{w'_i}{\sum_{j=1}^8 w'_j}


🪙 Distribuição Exata de Centavos (Largest Remainder Method)

Ao converter percentuais contínuos wi[0,1]w_i \in [0, 1] em moeda real (Decimal), o arredondamento ingênuo pode gerar discrepâncias de centavos. O Monvix implementa o Método do Maior Resto (Hamilton-Hare Method):

# Trecho de app/ga/optimizer.py
raw = [balance * Decimal(str(weight)) for weight in chromosome]
amounts = [value.quantize(CENT, rounding=ROUND_DOWN) for value in raw]
cents_left = int((balance - sum(amounts, ZERO)) / CENT)
fractions = [raw[index] - amounts[index] for index in range(len(raw))]
order = sorted(range(len(raw)), key=lambda index: fractions[index], reverse=True)
for index in order[:cents_left]:
amounts[index] += CENT

Isso assegura com precisão bancária que a soma das alocações é matematicamente igual a flexible_balance.

👉 Veja a Formulação Matemática da Função de Fitness →