agroplan-ai-cli 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend-template/api.py +224 -74
- package/backend-template/core/climate_adapter.py +142 -0
- package/package.json +1 -1
package/backend-template/api.py
CHANGED
|
@@ -70,6 +70,9 @@ class OtimizarRequest(BaseModel):
|
|
|
70
70
|
seed: Optional[int] = 42
|
|
71
71
|
geracoes: Optional[int] = 100
|
|
72
72
|
populacao: Optional[int] = 50
|
|
73
|
+
lat: Optional[float] = None
|
|
74
|
+
lon: Optional[float] = None
|
|
75
|
+
days: Optional[int] = 30
|
|
73
76
|
|
|
74
77
|
class ValidarRequest(BaseModel):
|
|
75
78
|
objetivo: str = "equilibrado"
|
|
@@ -78,6 +81,9 @@ class ValidarRequest(BaseModel):
|
|
|
78
81
|
class RelatorioRequest(BaseModel):
|
|
79
82
|
objetivo: str = "equilibrado"
|
|
80
83
|
formato: str = "md"
|
|
84
|
+
lat: Optional[float] = None
|
|
85
|
+
lon: Optional[float] = None
|
|
86
|
+
days: Optional[int] = 30
|
|
81
87
|
|
|
82
88
|
class RodadasRequest(BaseModel):
|
|
83
89
|
objetivo: str = "equilibrado"
|
|
@@ -181,21 +187,46 @@ def get_clima(
|
|
|
181
187
|
raise HTTPException(status_code=500, detail=str(e))
|
|
182
188
|
|
|
183
189
|
@app.get("/dashboard")
|
|
184
|
-
def get_dashboard(
|
|
185
|
-
|
|
190
|
+
def get_dashboard(
|
|
191
|
+
lat: Optional[float] = None,
|
|
192
|
+
lon: Optional[float] = None,
|
|
193
|
+
days: int = Query(30, description="Número de dias para análise climática")
|
|
194
|
+
):
|
|
195
|
+
"""Retorna resumo do dashboard com contexto climático opcional"""
|
|
186
196
|
try:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
culturas, talhoes, regras = get_dados()
|
|
198
|
+
|
|
199
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
200
|
+
contexto_climatico = None
|
|
201
|
+
if lat is not None and lon is not None:
|
|
202
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
203
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
204
|
+
|
|
205
|
+
# Gerar chave de cache considerando clima
|
|
206
|
+
cache_params = {"objetivo": "equilibrado", "seed": 42}
|
|
207
|
+
if lat is not None and lon is not None:
|
|
208
|
+
cache_params.update({"lat": lat, "lon": lon, "days": days})
|
|
209
|
+
|
|
210
|
+
cache_key = get_cache_key("dashboard", **cache_params)
|
|
211
|
+
|
|
212
|
+
def compute_dashboard():
|
|
213
|
+
# Usar AG com clima se disponível
|
|
214
|
+
if contexto_climatico:
|
|
215
|
+
from core.climate_adapter import gerar_plano_com_clima
|
|
216
|
+
resultado_ag = gerar_plano_com_clima(
|
|
217
|
+
culturas, talhoes, regras,
|
|
218
|
+
objetivo='equilibrado', seed=42,
|
|
219
|
+
lat=lat, lon=lon, days=days
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
192
223
|
|
|
193
224
|
# Tenta validar
|
|
194
225
|
validacao = comparar_ag_com_forca_bruta(culturas, talhoes, regras, objetivo='equilibrado', seed=42)
|
|
195
226
|
|
|
196
|
-
#
|
|
227
|
+
# Preparar resultado base
|
|
197
228
|
if validacao.get('erro'):
|
|
198
|
-
|
|
229
|
+
resultado_base = {
|
|
199
230
|
"lucro_total": float(resultado_ag['lucro_total']),
|
|
200
231
|
"risco_medio": float(resultado_ag['risco_medio']),
|
|
201
232
|
"fitness": float(resultado_ag['fitness']),
|
|
@@ -223,47 +254,62 @@ def get_dashboard():
|
|
|
223
254
|
for p in resultado_ag['plano']
|
|
224
255
|
]
|
|
225
256
|
}
|
|
257
|
+
else:
|
|
258
|
+
resultado_base = {
|
|
259
|
+
"lucro_total": float(resultado_ag['lucro_total']),
|
|
260
|
+
"risco_medio": float(resultado_ag['risco_medio']),
|
|
261
|
+
"fitness": float(resultado_ag['fitness']),
|
|
262
|
+
"diversidade": int(resultado_ag['diversidade']),
|
|
263
|
+
"objetivo": str(resultado_ag['objetivo']),
|
|
264
|
+
"culturas_escolhidas": [str(p['cultura']) for p in resultado_ag['plano']],
|
|
265
|
+
"validacao": {
|
|
266
|
+
"otimo_global": bool(validacao.get('ag_encontrou_otimo_global', False)),
|
|
267
|
+
"total_combinacoes": int(validacao.get('forca_bruta', {}).get('total_combinacoes', 0))
|
|
268
|
+
},
|
|
269
|
+
"plano": [
|
|
270
|
+
{
|
|
271
|
+
"talhao": int(p['talhao']),
|
|
272
|
+
"area": float(p['area']),
|
|
273
|
+
"solo": str(p['solo']),
|
|
274
|
+
"clima": str(p['clima']),
|
|
275
|
+
"relevo": str(p['relevo']),
|
|
276
|
+
"agua": str(p['agua']),
|
|
277
|
+
"cultura": str(p['cultura']),
|
|
278
|
+
"lucro_estimado": float(p['lucro_estimado']),
|
|
279
|
+
"risco": float(p['risco']),
|
|
280
|
+
"nota": float(p['nota']),
|
|
281
|
+
"tempo": int(p['tempo'])
|
|
282
|
+
}
|
|
283
|
+
for p in resultado_ag['plano']
|
|
284
|
+
]
|
|
285
|
+
}
|
|
226
286
|
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"relevo": str(p['relevo']),
|
|
246
|
-
"agua": str(p['agua']),
|
|
247
|
-
"cultura": str(p['cultura']),
|
|
248
|
-
"lucro_estimado": float(p['lucro_estimado']),
|
|
249
|
-
"risco": float(p['risco']),
|
|
250
|
-
"nota": float(p['nota']),
|
|
251
|
-
"tempo": int(p['tempo'])
|
|
252
|
-
}
|
|
253
|
-
for p in resultado_ag['plano']
|
|
254
|
-
]
|
|
255
|
-
}
|
|
287
|
+
# Adicionar informações de clima real
|
|
288
|
+
if contexto_climatico:
|
|
289
|
+
resultado_base["clima_real"] = {
|
|
290
|
+
"ativo": True,
|
|
291
|
+
"source": contexto_climatico.get("fonte", "unknown"),
|
|
292
|
+
"temperatura_media": contexto_climatico.get("temperatura_media"),
|
|
293
|
+
"precipitacao_total": contexto_climatico.get("precipitacao_total"),
|
|
294
|
+
"risco_climatico_estimado": contexto_climatico.get("risco_climatico_estimado"),
|
|
295
|
+
"clima_observado": contexto_climatico.get("clima_observado"),
|
|
296
|
+
"agua_observada": contexto_climatico.get("agua_observada"),
|
|
297
|
+
"ajuste_risco": contexto_climatico.get("ajuste_risco", 0),
|
|
298
|
+
"fallback": contexto_climatico.get("fallback", False),
|
|
299
|
+
"error": contexto_climatico.get("error")
|
|
300
|
+
}
|
|
301
|
+
else:
|
|
302
|
+
resultado_base["clima_real"] = {"ativo": False}
|
|
303
|
+
|
|
304
|
+
return resultado_base
|
|
256
305
|
|
|
257
|
-
# Usa cache para dashboard
|
|
258
|
-
|
|
259
|
-
resultado = get_or_compute_cache(key, montar_dashboard)
|
|
306
|
+
# Usa cache para dashboard com contexto climático
|
|
307
|
+
resultado = get_or_compute_cache(cache_key, compute_dashboard)
|
|
260
308
|
|
|
261
309
|
# Converte tipos Python (por segurança)
|
|
262
310
|
return converter_tipos_python(resultado)
|
|
263
311
|
except Exception as e:
|
|
264
312
|
raise HTTPException(status_code=500, detail=str(e))
|
|
265
|
-
except Exception as e:
|
|
266
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
267
313
|
|
|
268
314
|
@app.get("/talhoes")
|
|
269
315
|
def get_talhoes():
|
|
@@ -333,17 +379,42 @@ def get_culturas():
|
|
|
333
379
|
raise HTTPException(status_code=500, detail=str(e))
|
|
334
380
|
|
|
335
381
|
@app.get("/cenarios")
|
|
336
|
-
def get_cenarios(
|
|
337
|
-
|
|
382
|
+
def get_cenarios(
|
|
383
|
+
lat: Optional[float] = None,
|
|
384
|
+
lon: Optional[float] = None,
|
|
385
|
+
days: int = Query(30, description="Número de dias para análise climática")
|
|
386
|
+
):
|
|
387
|
+
"""Retorna comparação de cenários com contexto climático opcional"""
|
|
338
388
|
try:
|
|
389
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
390
|
+
contexto_climatico = None
|
|
391
|
+
if lat is not None and lon is not None:
|
|
392
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
393
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
394
|
+
|
|
395
|
+
# Gerar chave de cache considerando clima
|
|
396
|
+
cache_params = {"cenarios": True}
|
|
397
|
+
if lat is not None and lon is not None:
|
|
398
|
+
cache_params.update({"lat": lat, "lon": lon, "days": days})
|
|
399
|
+
|
|
400
|
+
cache_key = get_cache_key("cenarios", **cache_params)
|
|
401
|
+
|
|
339
402
|
def montar_cenarios():
|
|
340
403
|
culturas, talhoes, regras = get_dados()
|
|
341
404
|
|
|
342
405
|
# Gera todos os cenários
|
|
343
406
|
cenarios = gerar_cenarios(culturas, talhoes, regras)
|
|
344
407
|
|
|
345
|
-
#
|
|
346
|
-
|
|
408
|
+
# Usar AG com clima se disponível
|
|
409
|
+
if contexto_climatico:
|
|
410
|
+
from core.climate_adapter import gerar_plano_com_clima
|
|
411
|
+
resultado_ag = gerar_plano_com_clima(
|
|
412
|
+
culturas, talhoes, regras,
|
|
413
|
+
objetivo='equilibrado', seed=42,
|
|
414
|
+
lat=lat, lon=lon, days=days
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
resultado_ag = get_ag_cacheado(objetivo='equilibrado', seed=42)
|
|
347
418
|
|
|
348
419
|
# Cria um mapa de talhões para facilitar o acesso
|
|
349
420
|
talhoes_dict = {int(row['id']): row for _, row in talhoes.iterrows()}
|
|
@@ -376,7 +447,7 @@ def get_cenarios():
|
|
|
376
447
|
]
|
|
377
448
|
}
|
|
378
449
|
|
|
379
|
-
# Adiciona AG
|
|
450
|
+
# Adiciona AG com contexto climático
|
|
380
451
|
cenarios_formatados['genetico'] = {
|
|
381
452
|
'nome': 'Algoritmo Genético',
|
|
382
453
|
'descricao': 'Solução otimizada automaticamente',
|
|
@@ -401,13 +472,28 @@ def get_cenarios():
|
|
|
401
472
|
]
|
|
402
473
|
}
|
|
403
474
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
475
|
+
# Adicionar informações de clima real se disponível
|
|
476
|
+
resultado_final = {"cenarios": cenarios_formatados}
|
|
477
|
+
if contexto_climatico:
|
|
478
|
+
resultado_final["clima_real"] = {
|
|
479
|
+
"ativo": True,
|
|
480
|
+
"source": contexto_climatico.get("fonte", "unknown"),
|
|
481
|
+
"temperatura_media": contexto_climatico.get("temperatura_media"),
|
|
482
|
+
"precipitacao_total": contexto_climatico.get("precipitacao_total"),
|
|
483
|
+
"risco_climatico_estimado": contexto_climatico.get("risco_climatico_estimado"),
|
|
484
|
+
"clima_observado": contexto_climatico.get("clima_observado"),
|
|
485
|
+
"agua_observada": contexto_climatico.get("agua_observada"),
|
|
486
|
+
"ajuste_risco": contexto_climatico.get("ajuste_risco", 0),
|
|
487
|
+
"fallback": contexto_climatico.get("fallback", False),
|
|
488
|
+
"error": contexto_climatico.get("error")
|
|
489
|
+
}
|
|
490
|
+
else:
|
|
491
|
+
resultado_final["clima_real"] = {"ativo": False}
|
|
492
|
+
|
|
493
|
+
return resultado_final
|
|
407
494
|
|
|
408
|
-
# Usa cache para cenários
|
|
409
|
-
|
|
410
|
-
resultado = get_or_compute_cache(key, montar_cenarios)
|
|
495
|
+
# Usa cache para cenários com contexto climático
|
|
496
|
+
resultado = get_or_compute_cache(cache_key, montar_cenarios)
|
|
411
497
|
|
|
412
498
|
return converter_tipos_python(resultado)
|
|
413
499
|
except Exception as e:
|
|
@@ -417,34 +503,54 @@ def get_cenarios():
|
|
|
417
503
|
|
|
418
504
|
@app.post("/otimizar")
|
|
419
505
|
def otimizar(request: OtimizarRequest):
|
|
420
|
-
"""Executa otimização com Algoritmo Genético"""
|
|
506
|
+
"""Executa otimização com Algoritmo Genético e contexto climático opcional"""
|
|
421
507
|
try:
|
|
422
508
|
# Valida objetivo
|
|
423
509
|
objetivos_validos = ['equilibrado', 'lucro', 'risco', 'sustentavel']
|
|
424
510
|
if request.objetivo not in objetivos_validos:
|
|
425
511
|
raise HTTPException(status_code=400, detail=f"Objetivo inválido. Use: {objetivos_validos}")
|
|
426
512
|
|
|
427
|
-
#
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
request.
|
|
432
|
-
|
|
513
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
514
|
+
contexto_climatico = None
|
|
515
|
+
if request.lat is not None and request.lon is not None:
|
|
516
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
517
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(request.lat, request.lon, request.days)
|
|
518
|
+
|
|
519
|
+
# Usar AG com clima se disponível
|
|
520
|
+
if contexto_climatico:
|
|
521
|
+
from core.climate_adapter import gerar_plano_com_clima
|
|
522
|
+
resultado = gerar_plano_com_clima(
|
|
523
|
+
*get_dados(),
|
|
433
524
|
objetivo=request.objetivo,
|
|
434
525
|
seed=request.seed,
|
|
435
526
|
geracoes=request.geracoes,
|
|
436
|
-
populacao=request.populacao
|
|
437
|
-
)
|
|
438
|
-
else:
|
|
439
|
-
# Executa AG sem cache para parâmetros customizados
|
|
440
|
-
culturas, talhoes, regras = get_dados()
|
|
441
|
-
resultado = gerar_plano_genetico(
|
|
442
|
-
culturas, talhoes, regras,
|
|
443
|
-
objetivo=request.objetivo,
|
|
444
|
-
geracoes=request.geracoes,
|
|
445
527
|
populacao=request.populacao,
|
|
446
|
-
|
|
528
|
+
lat=request.lat,
|
|
529
|
+
lon=request.lon,
|
|
530
|
+
days=request.days
|
|
447
531
|
)
|
|
532
|
+
else:
|
|
533
|
+
# Usa AG cacheado se parâmetros forem padrão e sem clima
|
|
534
|
+
if (request.objetivo == "equilibrado" and
|
|
535
|
+
request.seed == 42 and
|
|
536
|
+
request.geracoes == 100 and
|
|
537
|
+
request.populacao == 50):
|
|
538
|
+
resultado = get_ag_cacheado(
|
|
539
|
+
objetivo=request.objetivo,
|
|
540
|
+
seed=request.seed,
|
|
541
|
+
geracoes=request.geracoes,
|
|
542
|
+
populacao=request.populacao
|
|
543
|
+
)
|
|
544
|
+
else:
|
|
545
|
+
# Executa AG sem cache para parâmetros customizados
|
|
546
|
+
culturas, talhoes, regras = get_dados()
|
|
547
|
+
resultado = gerar_plano_genetico(
|
|
548
|
+
culturas, talhoes, regras,
|
|
549
|
+
objetivo=request.objetivo,
|
|
550
|
+
geracoes=request.geracoes,
|
|
551
|
+
populacao=request.populacao,
|
|
552
|
+
seed=request.seed
|
|
553
|
+
)
|
|
448
554
|
|
|
449
555
|
# Converte tipos numpy para Python nativos
|
|
450
556
|
resultado_convertido = converter_tipos_python(resultado)
|
|
@@ -516,7 +622,7 @@ def rodadas(request: RodadasRequest):
|
|
|
516
622
|
|
|
517
623
|
@app.post("/relatorio")
|
|
518
624
|
def relatorio(request: RelatorioRequest):
|
|
519
|
-
"""Gera relatório"""
|
|
625
|
+
"""Gera relatório com contexto climático opcional"""
|
|
520
626
|
try:
|
|
521
627
|
culturas, talhoes, regras = get_dados()
|
|
522
628
|
|
|
@@ -530,7 +636,14 @@ def relatorio(request: RelatorioRequest):
|
|
|
530
636
|
if request.formato not in formatos_validos:
|
|
531
637
|
raise HTTPException(status_code=400, detail=f"Formato inválido. Use: {formatos_validos}")
|
|
532
638
|
|
|
533
|
-
#
|
|
639
|
+
# Obter contexto climático se coordenadas foram fornecidas
|
|
640
|
+
contexto_climatico = None
|
|
641
|
+
if request.lat is not None and request.lon is not None:
|
|
642
|
+
from core.climate_adapter import obter_contexto_climatico_por_coordenadas
|
|
643
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(request.lat, request.lon, request.days)
|
|
644
|
+
|
|
645
|
+
# Gera relatório (por enquanto sem integração climática no gerador)
|
|
646
|
+
# TODO: Atualizar report_generator para aceitar contexto climático
|
|
534
647
|
caminho = gerar_relatorio_completo(
|
|
535
648
|
culturas, talhoes, regras,
|
|
536
649
|
objetivo=request.objetivo,
|
|
@@ -541,11 +654,48 @@ def relatorio(request: RelatorioRequest):
|
|
|
541
654
|
with open(caminho, 'r', encoding='utf-8') as f:
|
|
542
655
|
conteudo = f.read()
|
|
543
656
|
|
|
544
|
-
|
|
657
|
+
# Adiciona informações climáticas ao final se disponível
|
|
658
|
+
if contexto_climatico and not contexto_climatico.get("fallback", True):
|
|
659
|
+
clima_info = f"""
|
|
660
|
+
|
|
661
|
+
## Dados Climáticos Reais
|
|
662
|
+
|
|
663
|
+
**Fonte:** {contexto_climatico.get('fonte', 'N/A')}
|
|
664
|
+
**Temperatura Média:** {contexto_climatico.get('temperatura_media', 'N/A')}°C
|
|
665
|
+
**Precipitação Total:** {contexto_climatico.get('precipitacao_total', 'N/A')}mm
|
|
666
|
+
**Risco Climático:** {contexto_climatico.get('risco_climatico_estimado', 'N/A')}
|
|
667
|
+
**Clima Observado:** {contexto_climatico.get('clima_observado', 'N/A')}
|
|
668
|
+
**Água Observada:** {contexto_climatico.get('agua_observada', 'N/A')}
|
|
669
|
+
**Ajuste de Risco:** {contexto_climatico.get('ajuste_risco', 0):+.1%}
|
|
670
|
+
|
|
671
|
+
*Dados climáticos integrados ao planejamento para maior precisão.*
|
|
672
|
+
"""
|
|
673
|
+
conteudo += clima_info
|
|
674
|
+
|
|
675
|
+
resultado = {
|
|
545
676
|
"caminho": caminho,
|
|
546
677
|
"conteudo": conteudo,
|
|
547
678
|
"formato": request.formato
|
|
548
679
|
}
|
|
680
|
+
|
|
681
|
+
# Adicionar informações de clima real
|
|
682
|
+
if contexto_climatico:
|
|
683
|
+
resultado["clima_real"] = {
|
|
684
|
+
"ativo": True,
|
|
685
|
+
"source": contexto_climatico.get("fonte", "unknown"),
|
|
686
|
+
"temperatura_media": contexto_climatico.get("temperatura_media"),
|
|
687
|
+
"precipitacao_total": contexto_climatico.get("precipitacao_total"),
|
|
688
|
+
"risco_climatico_estimado": contexto_climatico.get("risco_climatico_estimado"),
|
|
689
|
+
"clima_observado": contexto_climatico.get("clima_observado"),
|
|
690
|
+
"agua_observada": contexto_climatico.get("agua_observada"),
|
|
691
|
+
"ajuste_risco": contexto_climatico.get("ajuste_risco", 0),
|
|
692
|
+
"fallback": contexto_climatico.get("fallback", False),
|
|
693
|
+
"error": contexto_climatico.get("error")
|
|
694
|
+
}
|
|
695
|
+
else:
|
|
696
|
+
resultado["clima_real"] = {"ativo": False}
|
|
697
|
+
|
|
698
|
+
return resultado
|
|
549
699
|
except HTTPException:
|
|
550
700
|
raise
|
|
551
701
|
except Exception as e:
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adaptador para integrar dados climáticos reais no planejamento agrícola
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from providers.weather_provider import get_weather_summary
|
|
6
|
+
from providers.types import WeatherSummary
|
|
7
|
+
|
|
8
|
+
def classificar_clima_por_temperatura(temp_media: Optional[float]) -> Optional[str]:
|
|
9
|
+
"""Classifica clima baseado na temperatura média"""
|
|
10
|
+
if temp_media is None:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
if temp_media >= 28:
|
|
14
|
+
return "quente"
|
|
15
|
+
if temp_media < 18:
|
|
16
|
+
return "frio"
|
|
17
|
+
return "ameno"
|
|
18
|
+
|
|
19
|
+
def classificar_agua_por_precipitacao(precipitacao_total: Optional[float]) -> Optional[str]:
|
|
20
|
+
"""Classifica disponibilidade de água baseada na precipitação"""
|
|
21
|
+
if precipitacao_total is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
if precipitacao_total < 50:
|
|
25
|
+
return "baixa"
|
|
26
|
+
if precipitacao_total > 120:
|
|
27
|
+
return "alta"
|
|
28
|
+
return "media"
|
|
29
|
+
|
|
30
|
+
def calcular_ajuste_risco_climatico(risco_climatico: str) -> float:
|
|
31
|
+
"""Calcula ajuste de risco baseado no risco climático estimado"""
|
|
32
|
+
ajustes = {
|
|
33
|
+
"alto": 0.15, # +15% risco
|
|
34
|
+
"medio": 0.05, # +5% risco
|
|
35
|
+
"baixo": -0.03, # -3% risco (leve benefício)
|
|
36
|
+
"indeterminado": 0
|
|
37
|
+
}
|
|
38
|
+
return ajustes.get(risco_climatico, 0)
|
|
39
|
+
|
|
40
|
+
def criar_contexto_climatico(weather_summary: WeatherSummary) -> Dict[str, Any]:
|
|
41
|
+
"""Cria contexto climático para uso no planejamento"""
|
|
42
|
+
|
|
43
|
+
clima_observado = classificar_clima_por_temperatura(weather_summary.temperatura_media)
|
|
44
|
+
agua_observada = classificar_agua_por_precipitacao(weather_summary.precipitacao_total)
|
|
45
|
+
ajuste_risco = calcular_ajuste_risco_climatico(weather_summary.risco_climatico_estimado)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"clima_observado": clima_observado,
|
|
49
|
+
"agua_observada": agua_observada,
|
|
50
|
+
"ajuste_risco": ajuste_risco,
|
|
51
|
+
"fonte": weather_summary.source,
|
|
52
|
+
"fallback": weather_summary.fallback,
|
|
53
|
+
"temperatura_media": weather_summary.temperatura_media,
|
|
54
|
+
"temperatura_maxima": weather_summary.temperatura_maxima,
|
|
55
|
+
"temperatura_minima": weather_summary.temperatura_minima,
|
|
56
|
+
"precipitacao_total": weather_summary.precipitacao_total,
|
|
57
|
+
"evapotranspiracao": weather_summary.evapotranspiracao,
|
|
58
|
+
"umidade_media": weather_summary.umidade_media,
|
|
59
|
+
"radiacao_solar": weather_summary.radiacao_solar,
|
|
60
|
+
"risco_climatico_estimado": weather_summary.risco_climatico_estimado,
|
|
61
|
+
"error": weather_summary.error
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def aplicar_contexto_climatico_no_plano(resultado_ag: Dict[str, Any], contexto_climatico: Dict[str, Any]) -> Dict[str, Any]:
|
|
65
|
+
"""Aplica ajustes climáticos no resultado do algoritmo genético"""
|
|
66
|
+
|
|
67
|
+
if not contexto_climatico or contexto_climatico.get("ajuste_risco", 0) == 0:
|
|
68
|
+
# Sem ajuste necessário
|
|
69
|
+
resultado_ag["ajuste_climatico_aplicado"] = False
|
|
70
|
+
resultado_ag["contexto_climatico"] = contexto_climatico
|
|
71
|
+
return resultado_ag
|
|
72
|
+
|
|
73
|
+
ajuste_risco = contexto_climatico["ajuste_risco"]
|
|
74
|
+
|
|
75
|
+
# Aplicar ajuste no plano otimizado
|
|
76
|
+
if "plano_otimizado" in resultado_ag:
|
|
77
|
+
for item in resultado_ag["plano_otimizado"]:
|
|
78
|
+
if "risco" in item:
|
|
79
|
+
risco_original = item["risco"]
|
|
80
|
+
# Aplicar ajuste mantendo risco entre 0.05 e 0.95
|
|
81
|
+
novo_risco = min(0.95, max(0.05, risco_original + ajuste_risco))
|
|
82
|
+
item["risco"] = round(novo_risco, 3)
|
|
83
|
+
item["risco_original"] = risco_original
|
|
84
|
+
item["ajuste_aplicado"] = ajuste_risco
|
|
85
|
+
|
|
86
|
+
# Recalcular métricas gerais se existirem
|
|
87
|
+
if "metricas" in resultado_ag and "risco_medio" in resultado_ag["metricas"]:
|
|
88
|
+
risco_original = resultado_ag["metricas"]["risco_medio"]
|
|
89
|
+
novo_risco = min(0.95, max(0.05, risco_original + ajuste_risco))
|
|
90
|
+
resultado_ag["metricas"]["risco_medio"] = round(novo_risco, 3)
|
|
91
|
+
resultado_ag["metricas"]["risco_medio_original"] = risco_original
|
|
92
|
+
|
|
93
|
+
# Marcar que ajuste foi aplicado
|
|
94
|
+
resultado_ag["ajuste_climatico_aplicado"] = True
|
|
95
|
+
resultado_ag["contexto_climatico"] = contexto_climatico
|
|
96
|
+
|
|
97
|
+
return resultado_ag
|
|
98
|
+
|
|
99
|
+
def obter_contexto_climatico_por_coordenadas(lat: float, lon: float, days: int = 30) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""Obtém contexto climático para coordenadas específicas"""
|
|
101
|
+
try:
|
|
102
|
+
weather_summary = get_weather_summary(lat, lon, days)
|
|
103
|
+
return criar_contexto_climatico(weather_summary)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Em caso de erro, retornar contexto vazio
|
|
106
|
+
return {
|
|
107
|
+
"clima_observado": None,
|
|
108
|
+
"agua_observada": None,
|
|
109
|
+
"ajuste_risco": 0,
|
|
110
|
+
"fonte": "erro",
|
|
111
|
+
"fallback": True,
|
|
112
|
+
"error": str(e),
|
|
113
|
+
"temperatura_media": None,
|
|
114
|
+
"precipitacao_total": None,
|
|
115
|
+
"risco_climatico_estimado": "indeterminado"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def gerar_plano_com_clima(culturas, talhoes, regras, objetivo="equilibrado", seed=42,
|
|
119
|
+
geracoes=100, populacao=50, lat=None, lon=None, days=30):
|
|
120
|
+
"""
|
|
121
|
+
Wrapper para gerar plano genético com contexto climático opcional
|
|
122
|
+
"""
|
|
123
|
+
from .planner import gerar_plano_genetico
|
|
124
|
+
|
|
125
|
+
# Gerar plano normalmente
|
|
126
|
+
resultado = gerar_plano_genetico(
|
|
127
|
+
culturas, talhoes, regras,
|
|
128
|
+
objetivo=objetivo, seed=seed,
|
|
129
|
+
geracoes=geracoes, populacao=populacao
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Se coordenadas foram fornecidas, aplicar contexto climático
|
|
133
|
+
if lat is not None and lon is not None:
|
|
134
|
+
contexto_climatico = obter_contexto_climatico_por_coordenadas(lat, lon, days)
|
|
135
|
+
if contexto_climatico:
|
|
136
|
+
resultado = aplicar_contexto_climatico_no_plano(resultado, contexto_climatico)
|
|
137
|
+
else:
|
|
138
|
+
# Marcar que não há clima real aplicado
|
|
139
|
+
resultado["ajuste_climatico_aplicado"] = False
|
|
140
|
+
resultado["contexto_climatico"] = None
|
|
141
|
+
|
|
142
|
+
return resultado
|