|
7 | 7 | from access_moppy.derivations.calc_ocean import ( |
8 | 8 | calc_areacello, |
9 | 9 | calc_global_ave_ocean, |
| 10 | + calc_hfds, |
10 | 11 | calc_hfgeou, |
11 | 12 | calc_msftbarot, |
12 | 13 | calc_overturning_streamfunction, |
@@ -586,3 +587,87 @@ def test_equivalent_to_depth_sum_then_cumsum(self): |
586 | 587 | result = calc_msftbarot(tx) |
587 | 588 | expected = tx.sum("st_ocean").cumsum("yt_ocean") |
588 | 589 | np.testing.assert_allclose(result.values, expected.values, rtol=1e-12) |
| 590 | + |
| 591 | + |
| 592 | +# --------------------------------------------------------------------------- |
| 593 | +# calc_hfds |
| 594 | +# --------------------------------------------------------------------------- |
| 595 | + |
| 596 | + |
| 597 | +class TestCalcHfds: |
| 598 | + """Tests for calc_hfds — surface downward heat flux with frazil fallback.""" |
| 599 | + |
| 600 | + def _base_fields(self): |
| 601 | + """Return three base surface flux DataArrays.""" |
| 602 | + times = xr.date_range("2000-01-01", periods=NT, freq="ME") |
| 603 | + shape = (NT, NY, NX) |
| 604 | + dims = ["time", "yt_ocean", "xt_ocean"] |
| 605 | + runoff = xr.DataArray( |
| 606 | + RNG.random(shape) * 10.0, dims=dims, coords={"time": times} |
| 607 | + ) |
| 608 | + coupler = xr.DataArray( |
| 609 | + RNG.random(shape) * 50.0, dims=dims, coords={"time": times} |
| 610 | + ) |
| 611 | + pme = xr.DataArray(RNG.random(shape) * 5.0, dims=dims, coords={"time": times}) |
| 612 | + return runoff, coupler, pme |
| 613 | + |
| 614 | + @pytest.mark.unit |
| 615 | + def test_uses_frazil_3d_int_z_when_available(self): |
| 616 | + """When frazil_3d_int_z is provided it should be included in the sum.""" |
| 617 | + runoff, coupler, pme = self._base_fields() |
| 618 | + frazil_3d = xr.DataArray( |
| 619 | + np.ones((NT, NY, NX)) * 2.0, |
| 620 | + dims=["time", "yt_ocean", "xt_ocean"], |
| 621 | + ) |
| 622 | + frazil_2d = xr.DataArray( |
| 623 | + np.ones((NT, NY, NX)) * 99.0, # should NOT be used |
| 624 | + dims=["time", "yt_ocean", "xt_ocean"], |
| 625 | + ) |
| 626 | + result = calc_hfds( |
| 627 | + runoff, coupler, pme, frazil_3d_int_z=frazil_3d, frazil_2d=frazil_2d |
| 628 | + ) |
| 629 | + expected = runoff + coupler + pme + frazil_3d |
| 630 | + np.testing.assert_allclose(result.values, expected.values, rtol=1e-10) |
| 631 | + |
| 632 | + @pytest.mark.unit |
| 633 | + def test_falls_back_to_frazil_2d(self): |
| 634 | + """When frazil_3d_int_z is None, frazil_2d should be used instead.""" |
| 635 | + runoff, coupler, pme = self._base_fields() |
| 636 | + frazil_2d = xr.DataArray( |
| 637 | + np.ones((NT, NY, NX)) * 3.0, |
| 638 | + dims=["time", "yt_ocean", "xt_ocean"], |
| 639 | + ) |
| 640 | + result = calc_hfds( |
| 641 | + runoff, coupler, pme, frazil_3d_int_z=None, frazil_2d=frazil_2d |
| 642 | + ) |
| 643 | + expected = runoff + coupler + pme + frazil_2d |
| 644 | + np.testing.assert_allclose(result.values, expected.values, rtol=1e-10) |
| 645 | + |
| 646 | + @pytest.mark.unit |
| 647 | + def test_falls_back_to_frazil_2d_emits_warning(self, caplog): |
| 648 | + """Falling back to frazil_2d should log a warning.""" |
| 649 | + import logging |
| 650 | + |
| 651 | + runoff, coupler, pme = self._base_fields() |
| 652 | + frazil_2d = xr.DataArray( |
| 653 | + np.ones((NT, NY, NX)), dims=["time", "yt_ocean", "xt_ocean"] |
| 654 | + ) |
| 655 | + with caplog.at_level( |
| 656 | + logging.WARNING, logger="access_moppy.derivations.calc_ocean" |
| 657 | + ): |
| 658 | + calc_hfds(runoff, coupler, pme, frazil_3d_int_z=None, frazil_2d=frazil_2d) |
| 659 | + assert any("frazil_2d" in msg for msg in caplog.messages) |
| 660 | + |
| 661 | + @pytest.mark.unit |
| 662 | + def test_no_frazil_returns_base_sum(self): |
| 663 | + """When neither frazil term is provided, result equals the three base fluxes.""" |
| 664 | + runoff, coupler, pme = self._base_fields() |
| 665 | + result = calc_hfds(runoff, coupler, pme, frazil_3d_int_z=None, frazil_2d=None) |
| 666 | + expected = runoff + coupler + pme |
| 667 | + np.testing.assert_allclose(result.values, expected.values, rtol=1e-10) |
| 668 | + |
| 669 | + @pytest.mark.unit |
| 670 | + def test_returns_dataarray(self): |
| 671 | + runoff, coupler, pme = self._base_fields() |
| 672 | + result = calc_hfds(runoff, coupler, pme) |
| 673 | + assert isinstance(result, xr.DataArray) |
0 commit comments