Ir para o conteúdo

Validação do modelo de cobertura

Esta página descreve a cadeia que responde POST /coverage/predict hoje, como ela é validada, como um artefato novo chega à produção e o que a validação não cobre. O modelo ridge-v1 aparece só na última seção: é o fallback.

Números ao vivo. As métricas do artefato servido podem ser conferidas sem chave em GET /coverage/claim-readiness (bloco artefact); a banda aplicada em produção, em GET /coverage/effective-band. Os valores citados abaixo foram medidos em 10/10/2026 em GET /coverage/claim-readiness. Quando divergirem, vale o endpoint.


1. A cadeia servida

Para cada receptor, o valor servido é a perda de um baseline físico corrigida por um modelo de resíduo treinado com medidas de campo.

Etapa O que faz Onde está
Baseline físico ITU-R P.1812 para percursos de 250 m ou mais. Abaixo de 250 m, o P.1812 é avaliado a 250 m e estendido com o expoente de distância LoS (street canyon) da ITU-R P.1411; a perda em espaço livre é o limite inferior rf_engines/hybrid_short_path_engine.py (baseline_engine = p1812-p1411-hybrid no sidecar)
Resíduo LightGBM Prevê o erro do baseline frente à medida de campo e aplica essa correção à perda do baseline rf_engines/residual_model.py; artefato rf_engines/artifacts/residual_lightgbm.txt + sidecar residual_lightgbm_features.json
Recalibração linear Ajuste a + b·L sobre a perda, estimado pelo treino em pares de campo retidos bloco linear_recalibration do sidecar
Correção espacial Quando o RX cai numa célula H3 de resolução 10 (~65 m) com medidas, subtrai o erro médio dessa célula, validado por leave-one-out bloco spatial_correction do sidecar
Banda conformal Intervalo de 90 % em torno do valor servido (§2) bloco conformal do sidecar
Teto físico Um valor acima do espaço livre é cortado, e a resposta marca physics_capped coverage_predict.py

O resíduo usa 14 features, na ordem fixada pelo sidecar (feature_names): log_d_km, log_f_ghz, tx_h_m, rx_h_m, tx_power_dbm, tx_gain_dbi, rx_gain_dbi, cable_loss_db, rx_clutter_class, rx_bldg_height_m, path_obstruction_frac, depression_angle_deg, terrain_roughness_m, path_clearance_min_m. No treino deste artefato, a classe de clutter veio do ESA WorldCover (clutter.chain) e a altura de edificações, do GHSL (building_height.source).

Quem respondeu. Em modo ponto, o campo model_source da resposta diz qual modelo produziu o número: residual-lgbm para a cadeia acima; local-model ou local-model-band para o ridge; itu-p1812 ou hybrid-ml-itu quando o resíduo não está disponível. O bloco uncertainty traz a banda aplicada, quando há uma. Os modos ponto e grade passam pela mesma função (coverage_predict.predict_signal). Na grade, model_source e model_version são os da fonte que respondeu mais células; a célula a menos de 50 m da torre não passa por modelo e fica fora da conta.

Qual artefato respondeu. Quando o resíduo responde, model_version identifica o artefato treinado que calculou o número: residual_lgbm-<dataset_max_id>-<hash>. O <dataset_max_id> vem do sidecar: é o maior id de link_observations que o treino leu. O <hash> são os 12 primeiros dígitos hexadecimais do SHA-256 das duas linhas model:<sha256 do booster> e features:<sha256 do sidecar>. Um sidecar sem dataset_max_id dá residual_lgbm-<hash>. A identidade é calculada uma vez, quando o modelo carrega, só a partir dos bytes desses dois arquivos: é a mesma em todas as réplicas e muda só quando um deles muda, como num retreino promovido. Se não puder ser calculada, a previsão sai do mesmo jeito, com residual_lgbm-unknown, e o carregamento registra um aviso no log. Para saber se o resíduo respondeu, leia model_source; model_version diz qual treino. Para conferir um artefato, no diretório dele:

printf 'model:%s\nfeatures:%s\n' \
  "$(sha256sum residual_lightgbm.txt | cut -d' ' -f1)" \
  "$(sha256sum residual_lightgbm_features.json | cut -d' ' -f1)" \
  | sha256sum | cut -c1-12
jq .dataset_max_id residual_lightgbm_features.json

Engine. O default de /coverage/predict é engine='p1812'. Com o artefato residual carregado, p1812, auto (quando não é promovido a sionna_rt) e ridge são respondidos pela cadeia acima; itm e sionna_rt são caminhos próprios. O FSPL não é aceito nesse endpoint; ver Modelos de propagação.


2. Como o número é medido

Dados

  • 124.309 medições reais de drive-test, agregadas em 76.139 pontos (target.n_raw e target.n_bins no sidecar). Cada ponto é a média local das medidas da mesma torre, na mesma frequência, com o RX dentro de ~11 m (coordenadas arredondadas a 4 casas decimais): scripts/calibrate_p1812_residual.py, _aggregate_local_mean.
  • Procedência real (data_provenance): nenhuma linha sintética no treino.
  • Envelope calibrado: 5G NR n78 3,5 GHz + LTE B28 700 MHz/B1/B3/B7 (Vivo, TIM, Claro), suburbano/urbano de Brasília. Todos os pontos estão no Centro-Oeste: o bloco conformal.by_region do sidecar tem uma única região.

Validação cruzada espacial

  • GroupKFold com grupos espaciais por célula H3 (cv_method = h3_res8), 5 folds efetivos. Uma célula nunca está ao mesmo tempo no treino e no teste.
  • cv_rmse_db = 7,85 dB sobre n_observations = 76.139.

Banda conformal

  • Split-conformal sobre os resíduos out-of-fold da validação acima (method = cv_split_conformal_abs), nível de 90 %.
  • Meia-largura do artefato: q_abs_db = 12,54 dB, com cobertura empírica de 0,90 nos resíduos de calibração.
  • O sidecar traz também bandas por classe de clutter, por clutter × altura, por faixa de frequência e a banda de interpolação da correção espacial. Fora de uma célula medida vale a banda de extrapolação.
  • Em produção, a banda do artefato pode ser substituída por uma recalibração de campo (override). Em 10/10/2026 o override estava ativo e mais largo que a banda do artefato. A largura aplicada está em GET /coverage/effective-band e no bloco served_band de GET /coverage/claim-readiness.

Métricas exportadas

GET /metrics (autenticado: só o Prometheus de produção o raspa), formato Prometheus (rf_engines/_metrics.py):

residual_engine_available                 # 1 com artefato carregado
residual_engine_schema_version            # 6 hoje
residual_engine_n_observations            # pontos de treino
residual_engine_cv_rmse_db                # média dos folds espaciais
residual_engine_cv_rmse_std_db            # desvio entre folds
residual_engine_cv_mae_db
residual_engine_cv_n_splits
residual_engine_cv_n_groups
residual_engine_cv_metric_present{key}    # 0 = chave ausente no sidecar
residual_conformal_halfwidth_db{scope}
residual_conformal_empirical_coverage{scope}
residual_conformal_nominal_coverage
residual_conformal_live_coverage          # cobertura medida em observações de campo recebidas
residual_conformal_live_n
residual_fallback_total{reason}           # respostas que ficaram no baseline
residual_data_max_psi                     # drift dos dados novos
itu_p1812_available                       # baseline disponível neste processo

As métricas residual_engine_cv_* e residual_conformal_* descrevem o artefato carregado, não o que foi servido. Com itu_p1812_available = 0 o resíduo não responde: o default (engine='p1812') devolve HTTP 503 e engine='auto' cai para o ridge.


3. Como um artefato novo chega à produção

  1. O workflow residual-drift-retrain.yml roda a cada 6 h (ou à mão). Ele treina um candidato a partir das observações de produção (scripts/calibrate_p1812_residual.py --from-db) quando o drift (residual_data_max_psi) passa do limiar ou quando o dataset cresceu além do artefato em uso.
  2. O candidato passa por três portas automáticas: o teto de CV-RMSE do próprio treino (--max-rmse-db, 8,0 dB por padrão; o teto efetivo é o maior entre esse valor e o RMS do resíduo bruto); a melhoria mínima sobre o artefato versionado (0,05 dB, ou até 0,25 dB de piora tolerada quando o espectro treinado se alarga); e a validação da calibração conformal (scripts/residual_rmse_honesty_report.py --validate-candidate).
  3. Se passa, o workflow abre um PR com o booster e o sidecar. A promoção é o merge humano desse PR: nenhum processo automático troca o modelo em produção.
  4. O artefato fica em rf_engines/artifacts/, copiado para a imagem da API, e é carregado no próximo deploy.

O artefato servido é sempre o do último PR de retreino promovido por esse merge humano; GET /coverage/model/info confirma o schema_version e o número de pontos dele.

Grau de uso. GET /coverage/promotion-readiness decide se o modelo atinge o grau sole-source (quatro portas: acurácia, volume de dados, integridade de campo e saúde da banda). Em 10/10/2026 a resposta era ready = false: a porta health_gate recusa porque a meia-largura conformal servida está acima do teto de 6 dB. O grau é de triagem.


4. O que esta validação não cobre

  • Outras regiões. Todos os pontos de treino estão em Brasília. Fora do envelope não há recusa nem troca automática de modelo: o resíduo continua a responder, com a banda de extrapolação. Não há dado para medir a transferência entre macrorregiões; a validação leave-one-region-out segue pendente (Validação técnica e limitações §8).
  • Relevo. O artefato servido foi treinado sem perfil de relevo: o bloco terrain do sidecar tem enabled = false e n_resolved = 0. Para manter paridade entre treino e serviço, o baseline P.1812 é chamado com perfil plano de dois pontos (rf_engines/residual_model.py, _terrain_trained), e as duas features de relevo do esquema 6 (terrain_roughness_m, path_clearance_min_m) ficaram vazias no treino. A obstrução por edificações entra pela feature path_obstruction_frac. Para comparar com o P.1812 sobre um perfil de relevo, POST /coverage/engines/predict e POST /coverage/engines/compare recebem o perfil na requisição.
  • Laudo. A predição é de grau triagem. Não substitui medição nem o relatório assinado por engenheiro com CREA/ART.

ridge-v1 (fallback)

Regressão ridge (NumPy) sobre 17 features: distância, frequência, alturas TX/RX, potência, ganhos, estatísticas do perfil de terreno (média, máximo, desvio-padrão, declividade, número de obstruções, obstrução máxima, razão de Fresnel mínima) e três termos não lineares (coverage_predict.py, _FEATURE_NAMES).

Quando responde. Só quando o resíduo não responde (artefato ausente, baseline P.1812 indisponível ou falha na chamada). O que vem no lugar depende da engine pedida:

  • engine='p1812' (default): sem o resíduo, a resposta é o P.1812 puro (itu-p1812); sem os mapas digitais da ITU, HTTP 503.
  • engine='auto': sem o resíduo e com o P.1812 disponível, mistura ridge × P.1812 (hybrid-ml-itu); com o P.1812 indisponível (por exemplo enquanto os mapas da ITU são provisionados no boot), o ridge sozinho (local-model ou local-model-band).
  • engine='ridge': o ridge, quando o resíduo não responde.

Um pedido sem as coordenadas de TX e RX também cai no ridge, que responde sozinho: o resíduo e o P.1812 precisam delas (coverage_predict.predict_signal).

Estado em 20/10/2026 (GET /health/ready, bloco coverage_model): versão ridge-v1, treinada em 13/07/2026, degraded = true (idade acima de 30 dias). O serviço carrega o artefato embutido na imagem. Por decisão de registro (#2819, opção A, decidida em 19/10/2026), esse artefato fica como reserva e a entrega do retreino ao serviço web segue desligada: s3_delivery.state = decided_off, expected = false, COVERAGE_MODEL_S3_URI sem definir. Sob a decisão, degraded = true é esperado: mede a idade da reserva, não uma falha. O retreino diário (retrain-coverage-model.yml) só publica um artefato que passe a porta de promoção de scripts/retrain_coverage_model.py (phase3_gate), que recusa quando não avalia.

A decisão. O bloco s3_delivery.decision do mesmo endpoint traz a revisão: revisar_ate = 2027-01-19 (vence às 00:00 de São Paulo) ou a primeira campanha de campo fora do Centro-Oeste, o que vier antes. O campo vigente diz se a decisão vale, e reserva_confere, se a reserva carregada é a que ela nomeia (null quando nada está carregado). Vencida a data, o estado volta a unconfigured e expected a true. O alerta CoverageModelReserveDecisionBreached toca quando algo sai do combinado, e o rótulo breach diz o quê: a URI foi definida (s3_uri_set), a reserva servida não é a da imagem (reserve_not_image) ou a data de revisão passou (review_overdue). A decisão vale só no SaaS gerenciado; num deploy on-prem, decision é null e, sem a URI, o estado é unconfigured.

Números. O CV do ridge é k-fold com permutação aleatória das linhas (coverage_predict._kfold_evaluate, seed 42), sem separação geográfica. Não é comparável ao CV espacial do resíduo.

Métrica Valor Fonte
cv_rmse_db do artefato carregado 12,84 dB (k=5) coverage_model_cv_rmse_db em GET /metrics (autenticado), 10/10/2026
Referência citada nas superfícies de claim ~14,9 dB procurement_cv_rmse_db em GET /platform/maturity
RMSE por banda e por morfologia — local_model.rmse_by_band e local_model.rmse_by_morphology em GET /coverage/model/info (autenticado); coverage_model_rmse_by_band_db{band} e coverage_model_rmse_by_morphology_db{morphology} em GET /metrics (autenticado)

Os buckets de morfologia do ridge são definidos pelo desvio-padrão do terreno ao longo do perfil (terrain_std_m): open_or_flat (< 5 m), rural_rolling (5–15 m) e rural_mountainous (≥ 15 m).