|
77 | 77 |
|
78 | 78 | # fmt:on |
79 | 79 |
|
| 80 | + |
| 81 | +def run_and_assert_forecast( |
| 82 | + precip, forecast_kwargs, expected_n_ens_members, n_timesteps, converter, metadata |
| 83 | +): |
| 84 | + """Run a blended nowcast and assert the output has the expected shape.""" |
| 85 | + precip_forecast = blending.steps.forecast(precip=precip, **forecast_kwargs) |
| 86 | + |
| 87 | + assert precip_forecast.ndim == 4, "Wrong amount of dimensions in forecast output" |
| 88 | + assert ( |
| 89 | + precip_forecast.shape[0] == expected_n_ens_members |
| 90 | + ), "Wrong amount of output ensemble members in forecast output" |
| 91 | + assert ( |
| 92 | + precip_forecast.shape[1] == n_timesteps |
| 93 | + ), "Wrong amount of output time steps in forecast output" |
| 94 | + |
| 95 | + # Transform the data back into mm/h |
| 96 | + precip_forecast, _ = converter(precip_forecast, metadata) |
| 97 | + |
| 98 | + assert ( |
| 99 | + precip_forecast.ndim == 4 |
| 100 | + ), "Wrong amount of dimensions in converted forecast output" |
| 101 | + assert ( |
| 102 | + precip_forecast.shape[0] == expected_n_ens_members |
| 103 | + ), "Wrong amount of output ensemble members in converted forecast output" |
| 104 | + assert ( |
| 105 | + precip_forecast.shape[1] == n_timesteps |
| 106 | + ), "Wrong amount of output time steps in converted forecast output" |
| 107 | + |
| 108 | + |
80 | 109 | steps_arg_names = ( |
81 | 110 | "n_models", |
82 | 111 | "timesteps", |
@@ -357,10 +386,9 @@ def test_steps_blending( |
357 | 386 | assert nwp_velocity.ndim == 5, "nwp_velocity must be a five-dimensional array" |
358 | 387 |
|
359 | 388 | ### |
360 | | - # The blending |
| 389 | + # Shared forecast kwargs |
361 | 390 | ### |
362 | | - precip_forecast = blending.steps.forecast( |
363 | | - precip=radar_precip, |
| 391 | + forecast_kwargs = dict( |
364 | 392 | precip_models=nwp_precip_decomp, |
365 | 393 | velocity=radar_velocity, |
366 | 394 | velocity_models=nwp_velocity, |
@@ -402,179 +430,125 @@ def test_steps_blending( |
402 | 430 | measure_time=False, |
403 | 431 | ) |
404 | 432 |
|
405 | | - assert precip_forecast.ndim == 4, "Wrong amount of dimensions in forecast output" |
406 | | - |
407 | | - assert ( |
408 | | - precip_forecast.shape[0] == expected_n_ens_members |
409 | | - ), "Wrong amount of output ensemble members in forecast output" |
410 | | - |
411 | | - assert ( |
412 | | - precip_forecast.shape[1] == n_timesteps |
413 | | - ), "Wrong amount of output time steps in forecast output" |
414 | | - |
415 | | - # Transform the data back into mm/h |
416 | | - precip_forecast, _ = converter(precip_forecast, metadata) |
417 | | - |
418 | | - assert ( |
419 | | - precip_forecast.ndim == 4 |
420 | | - ), "Wrong amount of dimensions in converted forecast output" |
421 | | - |
422 | | - assert ( |
423 | | - precip_forecast.shape[0] == expected_n_ens_members |
424 | | - ), "Wrong amount of output ensemble members in converted forecast output" |
425 | | - |
426 | | - assert ( |
427 | | - precip_forecast.shape[1] == n_timesteps |
428 | | - ), "Wrong amount of output time steps in converted forecast output" |
429 | | - |
430 | | - ### Add another test for observations that are 0 at last time step and non-zero before that |
431 | | - radar_precip_zero_latest = radar_precip.copy() |
432 | | - radar_precip_zero_latest[-1] = metadata["zerovalue"] |
433 | | - |
434 | 433 | ### |
435 | 434 | # The blending |
436 | 435 | ### |
437 | | - precip_forecast = blending.steps.forecast( |
438 | | - precip=radar_precip_zero_latest, |
439 | | - precip_models=nwp_precip_decomp, |
440 | | - velocity=radar_velocity, |
441 | | - velocity_models=nwp_velocity, |
442 | | - timesteps=timesteps, |
443 | | - timestep=5.0, |
444 | | - issuetime=datetime.datetime.strptime("202112012355", "%Y%m%d%H%M"), |
445 | | - n_ens_members=n_ens_members, |
446 | | - n_cascade_levels=n_cascade_levels, |
447 | | - blend_nwp_members=blend_nwp_members, |
448 | | - precip_thr=metadata["threshold"], |
449 | | - kmperpixel=1.0, |
450 | | - extrap_method="semilagrangian", |
451 | | - decomp_method="fft", |
452 | | - bandpass_filter_method="gaussian", |
453 | | - noise_method="nonparametric", |
454 | | - noise_stddev_adj="auto", |
455 | | - ar_order=2, |
456 | | - vel_pert_method=vel_pert_method, |
457 | | - weights_method=weights_method, |
458 | | - timestep_start_full_nwp_weight=timestep_start_full_nwp_weight, |
459 | | - conditional=False, |
460 | | - probmatching_method=probmatching_method, |
461 | | - mask_method=mask_method, |
462 | | - resample_distribution=resample_distribution, |
463 | | - smooth_radar_mask_range=smooth_radar_mask_range, |
464 | | - callback=None, |
465 | | - return_output=True, |
466 | | - seed=None, |
467 | | - num_workers=1, |
468 | | - fft_method="numpy", |
469 | | - domain="spatial", |
470 | | - outdir_path_skill=outdir_path_skill, |
471 | | - extrap_kwargs=None, |
472 | | - filter_kwargs=None, |
473 | | - noise_kwargs=None, |
474 | | - vel_pert_kwargs=None, |
475 | | - clim_kwargs=clim_kwargs, |
476 | | - mask_kwargs=mask_kwargs, |
477 | | - measure_time=False, |
| 436 | + # Test with full radar data |
| 437 | + run_and_assert_forecast( |
| 438 | + radar_precip, |
| 439 | + forecast_kwargs, |
| 440 | + expected_n_ens_members, |
| 441 | + n_timesteps, |
| 442 | + converter, |
| 443 | + metadata, |
478 | 444 | ) |
479 | 445 |
|
480 | | - assert precip_forecast.ndim == 4, "Wrong amount of dimensions in forecast output" |
481 | | - |
482 | | - assert ( |
483 | | - precip_forecast.shape[0] == expected_n_ens_members |
484 | | - ), "Wrong amount of output ensemble members in forecast output" |
485 | 446 |
|
486 | | - assert ( |
487 | | - precip_forecast.shape[1] == n_timesteps |
488 | | - ), "Wrong amount of output time steps in forecast output" |
489 | | - |
490 | | - # Transform the data back into mm/h |
491 | | - precip_forecast, _ = converter(precip_forecast, metadata) |
492 | | - |
493 | | - assert ( |
494 | | - precip_forecast.ndim == 4 |
495 | | - ), "Wrong amount of dimensions in converted forecast output" |
496 | | - |
497 | | - assert ( |
498 | | - precip_forecast.shape[0] == expected_n_ens_members |
499 | | - ), "Wrong amount of output ensemble members in converted forecast output" |
500 | | - |
501 | | - assert ( |
502 | | - precip_forecast.shape[1] == n_timesteps |
503 | | - ), "Wrong amount of output time steps in converted forecast output" |
504 | | - |
505 | | - ### Add the case where the precip observation is non-zero for the latest time step but 0 before |
506 | | - radar_precip_zero_previous = radar_precip.copy() |
507 | | - radar_precip_zero_previous[:-1] = metadata["zerovalue"] |
| 447 | +@pytest.mark.parametrize("ar_order", [1, 2]) |
| 448 | +def test_steps_blending_partial_zero_radar(ar_order): |
| 449 | + """Test that a forecast succeeds when only the latest radar frame has |
| 450 | + precipitation (initiating cell corner case that produces NaN autocorrelations |
| 451 | + for the earlier, all-zero cascade levels).""" |
| 452 | + pytest.importorskip("cv2") |
508 | 453 |
|
509 | | - ### |
510 | | - # The blending |
511 | | - ### |
512 | | - precip_forecast = blending.steps.forecast( |
513 | | - precip=radar_precip_zero_previous, |
514 | | - precip_models=nwp_precip_decomp, |
515 | | - velocity=radar_velocity, |
516 | | - velocity_models=nwp_velocity, |
517 | | - timesteps=timesteps, |
518 | | - timestep=5.0, |
519 | | - issuetime=datetime.datetime.strptime("202112012355", "%Y%m%d%H%M"), |
520 | | - n_ens_members=n_ens_members, |
521 | | - n_cascade_levels=n_cascade_levels, |
522 | | - blend_nwp_members=blend_nwp_members, |
523 | | - precip_thr=metadata["threshold"], |
524 | | - kmperpixel=1.0, |
525 | | - extrap_method="semilagrangian", |
526 | | - decomp_method="fft", |
527 | | - bandpass_filter_method="gaussian", |
528 | | - noise_method="nonparametric", |
529 | | - noise_stddev_adj="auto", |
530 | | - ar_order=2, |
531 | | - vel_pert_method=vel_pert_method, |
532 | | - weights_method=weights_method, |
533 | | - timestep_start_full_nwp_weight=timestep_start_full_nwp_weight, |
534 | | - conditional=False, |
535 | | - probmatching_method=probmatching_method, |
536 | | - mask_method=mask_method, |
537 | | - resample_distribution=resample_distribution, |
538 | | - smooth_radar_mask_range=smooth_radar_mask_range, |
539 | | - callback=None, |
540 | | - return_output=True, |
541 | | - seed=None, |
542 | | - num_workers=1, |
543 | | - fft_method="numpy", |
544 | | - domain="spatial", |
545 | | - outdir_path_skill=outdir_path_skill, |
546 | | - extrap_kwargs=None, |
547 | | - filter_kwargs=None, |
548 | | - noise_kwargs=None, |
549 | | - vel_pert_kwargs=None, |
550 | | - clim_kwargs=clim_kwargs, |
551 | | - mask_kwargs=mask_kwargs, |
552 | | - measure_time=False, |
| 454 | + n_timesteps = 3 |
| 455 | + metadata = dict( |
| 456 | + unit="mm", |
| 457 | + transformation="dB", |
| 458 | + accutime=5.0, |
| 459 | + transform="dB", |
| 460 | + zerovalue=0.0, |
| 461 | + threshold=0.01, |
| 462 | + zr_a=200.0, |
| 463 | + zr_b=1.6, |
553 | 464 | ) |
554 | 465 |
|
555 | | - assert precip_forecast.ndim == 4, "Wrong amount of dimensions in forecast output" |
556 | | - |
557 | | - assert ( |
558 | | - precip_forecast.shape[0] == expected_n_ens_members |
559 | | - ), "Wrong amount of output ensemble members in forecast output" |
560 | | - |
561 | | - assert ( |
562 | | - precip_forecast.shape[1] == n_timesteps |
563 | | - ), "Wrong amount of output time steps in forecast output" |
564 | | - |
565 | | - # Transform the data back into mm/h |
566 | | - precip_forecast, _ = converter(precip_forecast, metadata) |
567 | | - |
568 | | - assert ( |
569 | | - precip_forecast.ndim == 4 |
570 | | - ), "Wrong amount of dimensions in converted forecast output" |
| 466 | + # Build minimal NWP data (1 model, 4 time steps) |
| 467 | + nwp_precip = np.zeros((1, n_timesteps + 1, 200, 200)) |
| 468 | + for i in range(nwp_precip.shape[1]): |
| 469 | + nwp_precip[0, i, 30:185, 32 + i] = 1.0 |
| 470 | + nwp_precip[0, i, 30:185, 33 + i] = 5.0 |
| 471 | + nwp_precip[0, i, 30:185, 34 + i] = 5.0 |
| 472 | + nwp_precip[0, i, 30:185, 35 + i] = 4.5 |
571 | 473 |
|
572 | | - assert ( |
573 | | - precip_forecast.shape[0] == expected_n_ens_members |
574 | | - ), "Wrong amount of output ensemble members in converted forecast output" |
| 474 | + # Build radar data: only the latest (most recent) frame has precipitation |
| 475 | + radar_precip = np.zeros((3, 200, 200)) |
| 476 | + radar_precip[2, 30:155, 32] = 1.0 |
| 477 | + radar_precip[2, 30:155, 33] = 5.0 |
| 478 | + radar_precip[2, 30:155, 34] = 5.0 |
| 479 | + radar_precip[2, 30:155, 35] = 4.5 |
575 | 480 |
|
576 | | - assert ( |
577 | | - precip_forecast.shape[1] == n_timesteps |
578 | | - ), "Wrong amount of output time steps in converted forecast output" |
| 481 | + # Threshold, convert and transform |
| 482 | + radar_precip[radar_precip < metadata["threshold"]] = 0.0 |
| 483 | + nwp_precip[nwp_precip < metadata["threshold"]] = 0.0 |
| 484 | + converter = pysteps.utils.get_method("mm/h") |
| 485 | + radar_precip, _ = converter(radar_precip, metadata) |
| 486 | + nwp_precip, metadata = converter(nwp_precip, metadata) |
| 487 | + transformer = pysteps.utils.get_method(metadata["transformation"]) |
| 488 | + radar_precip, _ = transformer(radar_precip, metadata) |
| 489 | + nwp_precip, metadata = transformer(nwp_precip, metadata) |
| 490 | + radar_precip[~np.isfinite(radar_precip)] = metadata["zerovalue"] |
| 491 | + nwp_precip[~np.isfinite(nwp_precip)] = metadata["zerovalue"] |
579 | 492 |
|
| 493 | + # Decompose NWP |
| 494 | + n_cascade_levels = 6 |
| 495 | + decomp_method, _ = cascade.get_method("fft") |
| 496 | + bp_filter = cascade.get_method("gaussian")(radar_precip.shape[1:], n_cascade_levels) |
| 497 | + nwp_precip_decomp = nwp_precip.copy() |
580 | 498 |
|
| 499 | + # Velocity fields |
| 500 | + oflow_method = pysteps.motion.get_method("lucaskanade") |
| 501 | + radar_velocity = oflow_method(radar_precip) |
| 502 | + _V_NWP = [ |
| 503 | + oflow_method(nwp_precip[0, t - 1 : t + 1, :]) |
| 504 | + for t in range(1, nwp_precip.shape[1]) |
| 505 | + ] |
| 506 | + nwp_velocity = np.stack([np.insert(_V_NWP, 0, _V_NWP[0], axis=0)]) |
| 507 | + |
| 508 | + run_and_assert_forecast( |
| 509 | + radar_precip, |
| 510 | + dict( |
| 511 | + precip_models=nwp_precip_decomp, |
| 512 | + velocity=radar_velocity, |
| 513 | + velocity_models=nwp_velocity, |
| 514 | + timesteps=n_timesteps, |
| 515 | + timestep=5.0, |
| 516 | + issuetime=datetime.datetime.strptime("202112012355", "%Y%m%d%H%M"), |
| 517 | + n_ens_members=2, |
| 518 | + n_cascade_levels=n_cascade_levels, |
| 519 | + blend_nwp_members=False, |
| 520 | + precip_thr=metadata["threshold"], |
| 521 | + kmperpixel=1.0, |
| 522 | + extrap_method="semilagrangian", |
| 523 | + decomp_method="fft", |
| 524 | + bandpass_filter_method="gaussian", |
| 525 | + noise_method="nonparametric", |
| 526 | + noise_stddev_adj="auto", |
| 527 | + ar_order=ar_order, |
| 528 | + vel_pert_method=None, |
| 529 | + weights_method="bps", |
| 530 | + conditional=False, |
| 531 | + probmatching_method=None, |
| 532 | + mask_method="incremental", |
| 533 | + resample_distribution=False, |
| 534 | + smooth_radar_mask_range=0, |
| 535 | + callback=None, |
| 536 | + return_output=True, |
| 537 | + seed=42, |
| 538 | + num_workers=1, |
| 539 | + fft_method="numpy", |
| 540 | + domain="spatial", |
| 541 | + outdir_path_skill="./tmp/", |
| 542 | + extrap_kwargs=None, |
| 543 | + filter_kwargs=None, |
| 544 | + noise_kwargs=None, |
| 545 | + vel_pert_kwargs=None, |
| 546 | + clim_kwargs=None, |
| 547 | + mask_kwargs=None, |
| 548 | + measure_time=False, |
| 549 | + ), |
| 550 | + expected_n_ens_members=2, |
| 551 | + n_timesteps=n_timesteps, |
| 552 | + converter=converter, |
| 553 | + metadata=metadata, |
| 554 | + ) |
0 commit comments