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(blocoartefact); a banda aplicada em produção, emGET /coverage/effective-band. Os valores citados abaixo foram medidos em 10/10/2026 emGET /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_rawetarget.n_binsno 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_regiondo sidecar tem uma única região.
Validação cruzada espacial¶
GroupKFoldcom 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 sobren_observations= 76.139.
- O histórico do método (por que os folds são espaciais) está em Validação técnica e limitações.
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-bande no blocoserved_banddeGET /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¶
- O workflow
residual-drift-retrain.ymlroda 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. - 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). - 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.
- 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
terraindo sidecar temenabled = falseen_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 featurepath_obstruction_frac. Para comparar com o P.1812 sobre um perfil de relevo,POST /coverage/engines/predictePOST /coverage/engines/comparerecebem 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-modeloulocal-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).