diff --git a/pixi.lock b/pixi.lock index 17c5ba1f1..02b0c8b90 100644 --- a/pixi.lock +++ b/pixi.lock @@ -266,7 +266,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.9.17-h76e24b7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -302,12 +302,14 @@ environments: - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a7/0d4490de967a67f68a538cc9cdb259bff971c4b5787f7765dc7c8f118f71/keras-2.15.0-py3-none-any.whl @@ -318,6 +320,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/6a/e8cca34f85b18a0280e3a19faca1923f6a04e7d587e9d8e33bc295a52b6d/nvidia_cublas_cu12-12.2.5.6-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cf/27/7c2f33b4fbab658117fce3b029f44cce7886fc5a50f526a81b4b0436af02/nvidia_cuda_cupti_cu12-12.2.142-py3-none-manylinux1_x86_64.whl @@ -353,9 +356,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/absl-py-2.1.0-pyhd8ed1ab_0.conda @@ -588,7 +592,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uv-0.9.17-h1bde295_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.0.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.14.1-py311he2be06e_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hd74edd7_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.9.0-pyhd8ed1ab_0.conda @@ -600,6 +604,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311ha60cc69_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda - pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/e7/b73934c0d209997279430e870ba21720edc4a9a73709f7bdb279f18343b1/netCDF4-1.6.5-cp311-cp311-macosx_11_0_arm64.whl @@ -607,6 +613,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/7c/eb6fcb6e94075bea4ab56c50d1bfb8a66d43fdc2fb67001181928dd7ddb1/pyjson5-2.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/86/df4771915e64a564c577ea2573956861c9c9f6c79450b172c5f9277cc48a/requests_unixsocket-0.4.1-py3-none-any.whl @@ -616,6 +623,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ default: channels: @@ -860,7 +868,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-15.1.0-py311h459d7ec_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -895,12 +903,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a7/0d4490de967a67f68a538cc9cdb259bff971c4b5787f7765dc7c8f118f71/keras-2.15.0-py3-none-any.whl @@ -911,6 +921,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/6a/e8cca34f85b18a0280e3a19faca1923f6a04e7d587e9d8e33bc295a52b6d/nvidia_cublas_cu12-12.2.5.6-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cf/27/7c2f33b4fbab658117fce3b029f44cce7886fc5a50f526a81b4b0436af02/nvidia_cuda_cupti_cu12-12.2.142-py3-none-manylinux1_x86_64.whl @@ -946,9 +957,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/absl-py-2.1.0-pyhd8ed1ab_0.conda @@ -1160,7 +1172,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.0.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.14.1-py311he2be06e_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hd74edd7_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.9.0-pyhd8ed1ab_0.conda @@ -1171,6 +1183,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311ha60cc69_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/e7/b73934c0d209997279430e870ba21720edc4a9a73709f7bdb279f18343b1/netCDF4-1.6.5-cp311-cp311-macosx_11_0_arm64.whl @@ -1178,6 +1192,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/7c/eb6fcb6e94075bea4ab56c50d1bfb8a66d43fdc2fb67001181928dd7ddb1/pyjson5-2.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/86/df4771915e64a564c577ea2573956861c9c9f6c79450b172c5f9277cc48a/requests_unixsocket-0.4.1-py3-none-any.whl @@ -1187,6 +1202,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ dev: channels: @@ -1617,7 +1633,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -1662,7 +1678,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a7/0d4490de967a67f68a538cc9cdb259bff971c4b5787f7765dc7c8f118f71/keras-2.15.0-py3-none-any.whl @@ -1695,6 +1711,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/0f/3c/e4ef8f3ef83254de96aba69f24fa613bc1277cf34802c6b4e82cc311121f/pyjson5-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5c/e2b18e66d73b69de87c198cba8744934b91247d8ab657a7253b591f4cf23/python_discovery-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/86/df4771915e64a564c577ea2573956861c9c9f6c79450b172c5f9277cc48a/requests_unixsocket-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl @@ -2108,7 +2125,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.14.1-py311he2be06e_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hd74edd7_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.9.0-pyhd8ed1ab_0.conda @@ -2135,6 +2152,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/7c/eb6fcb6e94075bea4ab56c50d1bfb8a66d43fdc2fb67001181928dd7ddb1/pyjson5-2.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5c/e2b18e66d73b69de87c198cba8744934b91247d8ab657a7253b591f4cf23/python_discovery-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/86/df4771915e64a564c577ea2573956861c9c9f6c79450b172c5f9277cc48a/requests_unixsocket-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/00/ac/12b1a7b9486dfcfd9c4c20a4ea5e2e4584681af8fe2f8898a716c2c51edb/sphinx_autosummary_accessors-2025.3.1-py3-none-any.whl @@ -2393,7 +2411,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-15.1.0-py311h459d7ec_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -2428,12 +2446,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a7/0d4490de967a67f68a538cc9cdb259bff971c4b5787f7765dc7c8f118f71/keras-2.15.0-py3-none-any.whl @@ -2444,6 +2464,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/6a/e8cca34f85b18a0280e3a19faca1923f6a04e7d587e9d8e33bc295a52b6d/nvidia_cublas_cu12-12.2.5.6-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cf/27/7c2f33b4fbab658117fce3b029f44cce7886fc5a50f526a81b4b0436af02/nvidia_cuda_cupti_cu12-12.2.142-py3-none-manylinux1_x86_64.whl @@ -2480,9 +2501,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/absl-py-2.1.0-pyhd8ed1ab_0.conda @@ -2699,7 +2721,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.0.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.14.1-py311he2be06e_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hd74edd7_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.9.0-pyhd8ed1ab_0.conda @@ -2710,6 +2732,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311ha60cc69_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/e7/b73934c0d209997279430e870ba21720edc4a9a73709f7bdb279f18343b1/netCDF4-1.6.5-cp311-cp311-macosx_11_0_arm64.whl @@ -2717,6 +2741,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/7c/eb6fcb6e94075bea4ab56c50d1bfb8a66d43fdc2fb67001181928dd7ddb1/pyjson5-2.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/86/df4771915e64a564c577ea2573956861c9c9f6c79450b172c5f9277cc48a/requests_unixsocket-0.4.1-py3-none-any.whl @@ -2727,6 +2752,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ test: channels: @@ -2973,7 +2999,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-15.1.0-py311h459d7ec_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-hd6090a7_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -3008,12 +3034,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a7/0d4490de967a67f68a538cc9cdb259bff971c4b5787f7765dc7c8f118f71/keras-2.15.0-py3-none-any.whl @@ -3024,6 +3052,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/6a/e8cca34f85b18a0280e3a19faca1923f6a04e7d587e9d8e33bc295a52b6d/nvidia_cublas_cu12-12.2.5.6-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cf/27/7c2f33b4fbab658117fce3b029f44cce7886fc5a50f526a81b4b0436af02/nvidia_cuda_cupti_cu12-12.2.142-py3-none-manylinux1_x86_64.whl @@ -3060,9 +3089,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/absl-py-2.1.0-pyhd8ed1ab_0.conda @@ -3276,7 +3306,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/werkzeug-3.0.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.14.1-py311he2be06e_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hd74edd7_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.9.0-pyhd8ed1ab_0.conda @@ -3287,6 +3317,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311ha60cc69_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/e7/b73934c0d209997279430e870ba21720edc4a9a73709f7bdb279f18343b1/netCDF4-1.6.5-cp311-cp311-macosx_11_0_arm64.whl @@ -3294,6 +3326,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/7c/eb6fcb6e94075bea4ab56c50d1bfb8a66d43fdc2fb67001181928dd7ddb1/pyjson5-2.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl @@ -3304,6 +3337,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ viz: channels: @@ -3708,7 +3742,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/x264-1!164.3095-h166bdaf_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/x265-3.5-h924138e_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -3745,12 +3779,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/a7/0d4490de967a67f68a538cc9cdb259bff971c4b5787f7765dc7c8f118f71/keras-2.15.0-py3-none-any.whl @@ -3761,6 +3797,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/4b/195ac84cc8f6077b4f0f421e8daee21b7f1bd88cb7716414234379fe68ec/numcodecs-0.16.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/6a/e8cca34f85b18a0280e3a19faca1923f6a04e7d587e9d8e33bc295a52b6d/nvidia_cublas_cu12-12.2.5.6-py3-none-manylinux1_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cf/27/7c2f33b4fbab658117fce3b029f44cce7886fc5a50f526a81b4b0436af02/nvidia_cuda_cupti_cu12-12.2.142-py3-none-manylinux1_x86_64.whl @@ -3796,9 +3833,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/66/7f/e36ae148c2f03d61ca1bff24bc13a0fef6d6825c966abef73fc6f880a23b/tensorflow_io_gcs_filesystem-0.37.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bc/26/953f79a4233603958234358820434d973bd859d3831b19fde23078e09771/wrapt-1.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/absl-py-2.1.0-pyhd8ed1ab_0.conda @@ -4168,7 +4206,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.14.1-py311he2be06e_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x264-1!164.3095-h57fd34a_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/x265-3.5-hbc6ce65_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hd74edd7_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2024.9.0-pyhd8ed1ab_0.conda @@ -4181,6 +4219,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py311ha60cc69_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.6-hb46c0d2_0.conda + - pypi: https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/8d/9e/f7caf7486a22c3f8dde60228a9905c73dd676cdcacbdaa4390acfc9ae959/h5pyd-0.18.0.tar.gz - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/e7/b73934c0d209997279430e870ba21720edc4a9a73709f7bdb279f18343b1/netCDF4-1.6.5-cp311-cp311-macosx_11_0_arm64.whl @@ -4188,6 +4228,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b0/51/a025e1b9fbe459fa45eef37abc6602a16e20979cba77b723bbea2dccc203/NREL_gaps-0.6.14-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/44/c3/4c9a17ca2b2ae1f09f91b4ee2608e185af82ddc751e9dbedaa6949bde644/nrel_phygnn-0.0.33-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/15/b6b2b49b4e5e17f0d2c1006d609b8adb13aa96944c6b8b5eb02a39df99a4/NREL_rex-0.2.98-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/cc/0d97ef55dda48cb0f93d7b92d761208e7a99bd2eea6b0e859426e6a99a21/numcodecs-0.16.5-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/29/7c/eb6fcb6e94075bea4ab56c50d1bfb8a66d43fdc2fb67001181928dd7ddb1/pyjson5-2.0.0-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/63/86/df4771915e64a564c577ea2573956861c9c9f6c79450b172c5f9277cc48a/requests_unixsocket-0.4.1-py3-none-any.whl @@ -4197,6 +4238,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/6b/c6/f47505b564b918a3ba60c1e99232d4942c4a7e44ecaae603e829e3d05dae/sphinx_tabs-3.4.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/15/bb13b4913ef95ad5448490821eee4671d0e67673342e4d4070854e5fe081/zarr-3.1.5-py3-none-any.whl - pypi: ./ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -6440,13 +6482,13 @@ packages: purls: [] size: 81202 timestamp: 1755102333712 -- pypi: https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: grpcio - version: 1.78.0 - sha256: 85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 + version: 1.78.1 + sha256: a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496 requires_dist: - typing-extensions~=4.12 - - grpcio-tools>=1.78.0 ; extra == 'protobuf' + - grpcio-tools>=1.78.1 ; extra == 'protobuf' requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/osx-arm64/grpcio-1.59.3-py311hf5d242d_0.conda sha256: d881b0a9a45e09e2c3dacf68bfb403088d165a5cac7de8e775de5d205bf09718 @@ -10539,7 +10581,8 @@ packages: - pytest>=5.2 - scipy>=1.0.0 - sphinx>=7.0 - - xarray>=2023.0 + - xarray>=2024.0 + - zarr>=2.0.0,<4 - pre-commit ; extra == 'dev' - pylint ; extra == 'dev' - ruff>=0.4 ; extra == 'dev' @@ -11771,6 +11814,23 @@ packages: - pkg:pypi/python-dateutil?source=hash-mapping size: 222742 timestamp: 1709299922152 +- pypi: https://files.pythonhosted.org/packages/c0/5c/e2b18e66d73b69de87c198cba8744934b91247d8ab657a7253b591f4cf23/python_discovery-1.0.0-py3-none-any.whl + name: python-discovery + version: 1.0.0 + sha256: 7cd9eaf3b1845875e22084f92d0ec2e309be2a3f839a9eb52980d647b72bd891 + requires_dist: + - filelock>=3.15.4 + - platformdirs>=4.3.6,<5 + - furo>=2025.12.19 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.6.3 ; extra == 'docs' + - sphinx>=9.1 ; extra == 'docs' + - sphinxcontrib-mermaid>=2 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'testing' + - coverage>=7.5.4 ; extra == 'testing' + - pytest-mock>=3.14 ; extra == 'testing' + - pytest>=8.3.5 ; extra == 'testing' + - setuptools>=75.1 ; extra == 'testing' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.20.0-pyhd8ed1ab_0.conda sha256: 7d8c931b89c9980434986b4deb22c2917b58d9936c3974139b9c10ae86fdfe60 md5: b98d2018c01ce9980c03ee2850690fab @@ -13557,10 +13617,10 @@ packages: - pkg:pypi/websocket-client?source=hash-mapping size: 47066 timestamp: 1713923494501 -- pypi: https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl name: werkzeug - version: 3.1.5 - sha256: 5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc + version: 3.1.6 + sha256: 7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131 requires_dist: - markupsafe>=2.1.1 - watchdog>=2.3 ; extra == 'watchdog' @@ -13654,41 +13714,44 @@ packages: purls: [] size: 1832744 timestamp: 1646609481185 -- conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2024.9.0-pyhd8ed1ab_1.conda - sha256: 8bb5b522cdf1905d831a9b371a3a3bd2932a9f53398332fbd38ed3442015bbaf - md5: dc790d427d89b85ae12fc094e264833f +- conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.2.0-pyhcf101f3_0.conda + sha256: 1d49f2c80c63913c5a9a525b64434a30cf1386502d0f24607db61bd46fa36a40 + md5: b1b3a2477c1b888f15bbef01d7a9615f depends: - - numpy >=1.24 - - packaging >=23.1 - - pandas >=2.1 - - python >=3.10 + - python >=3.11 + - numpy >=1.26 + - packaging >=24.1 + - pandas >=2.2 + - python constrains: - - seaborn-base >=0.12 - - distributed >=2023.9 - - scipy >=1.11 + - bottleneck >=1.4 + - cartopy >=0.23 + - cftime >=1.6 + - dask-core >=2024.6 + - distributed >=2024.6 + - flox >=0.9 + - h5netcdf >=1.3 + - h5py >=3.11 + - hdf5 >=1.14 + - iris >=3.9 + - matplotlib-base >=3.8 + - nc-time-axis >=1.4 - netcdf4 >=1.6.0 + - numba >=0.60 + - numbagg >=0.8 + - pint >=0.24 + - pydap >=3.5.0 + - scipy >=1.13 + - seaborn-base >=0.13 + - sparse >=0.15 - toolz >=0.12 - - nc-time-axis >=1.4 - - cftime >=1.6 - - h5netcdf >=1.2 - - matplotlib-base >=3.7 - - h5py >=3.8 - - zarr >=2.16 - - hdf5 >=1.12 - - numba >=0.57 - - iris >=3.7 - - cartopy >=0.22 - - dask-core >=2023.9 - - flox >=0.7 - - bottleneck >=1.3 - - pint >=0.22 - - sparse >=0.14 + - zarr >=2.18 license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/xarray?source=hash-mapping - size: 801066 - timestamp: 1728453306227 + - pkg:pypi/xarray?source=compressed-mapping + size: 1011911 + timestamp: 1771083999178 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-hb711507_2.conda sha256: 416aa55d946ce4ab173ab338796564893a2f820e80e04e098ff00c25fb981263 md5: 8637c3e5821654d0edf97e2b0404b443 diff --git a/pyproject.toml b/pyproject.toml index 79c16ba98..61d23aaee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,8 @@ dependencies = [ "pytest>=5.2", "scipy>=1.0.0", "sphinx>=7.0", - "xarray>=2023.0" + "xarray>=2024.0", + "zarr>=2.0.0,<4", ] # If used, cause glibc conflict @@ -298,7 +299,7 @@ matplotlib = ">=3.1" numpy = "~=1.7" pandas = ">=2.0" scipy = ">=1.0.0" -xarray = ">=2023.0" +xarray = ">=2024.0" [tool.pixi.pypi-dependencies] NREL-sup3r = { path = ".", editable = true } diff --git a/sup3r/models/__init__.py b/sup3r/models/__init__.py index 0cb5f1bf5..185a921db 100644 --- a/sup3r/models/__init__.py +++ b/sup3r/models/__init__.py @@ -7,6 +7,7 @@ from .multi_step import MultiStepGan, MultiStepSurfaceMetGan, SolarMultiStepGan from .solar_cc import SolarCC from .surface import SurfaceSpatialMetModel +from .training_config import TrainingConfig from .with_obs import Sup3rGanWithObs SPATIAL_FIRST_MODELS = (MultiStepSurfaceMetGan, SolarMultiStepGan) diff --git a/sup3r/models/abstract.py b/sup3r/models/abstract.py index eb819ec9a..e7ad79356 100644 --- a/sup3r/models/abstract.py +++ b/sup3r/models/abstract.py @@ -51,6 +51,10 @@ def __init__(self): self._stdevs = None self._train_record = pd.DataFrame() self._val_record = pd.DataFrame() + self._swa_weights = None + self._swa_n = 0 + self._swa_enabled = False + self._pre_swa_weights = None def load_network(self, model, name): """Load a CustomNetwork object from hidden layers config, .json file @@ -722,6 +726,147 @@ def early_stop(history, column, threshold=0.005, n_epoch=5): return stop + def enable_swa(self): + """Enable stochastic weight averaging.""" + self._swa_enabled = True + self._swa_weights = None + self._swa_n = 0 + logger.info('SWA enabled') + + def update_swa(self): + """Update the SWA running average with current model weights. + + This should be called at the end of epochs where you want to include + the current weights in the average (e.g., after each epoch in the + last 25% of training, or at the end of each LR cycle). + """ + if not self._swa_enabled: + return + + current_weights = [w.numpy() for w in self.weights] + + if self._swa_weights is None: + # First snapshot + self._swa_weights = current_weights + self._swa_n = 1 + else: + for i, w in enumerate(current_weights): + self._swa_weights[i] = ( + self._swa_weights[i] * self._swa_n + w + ) / (self._swa_n + 1) + self._swa_n += 1 + + logger.info(f'Updated SWA weights (n={self._swa_n})') + + def apply_swa(self, config, epoch, extras): + """Apply SWA updates if enabled in config. + + Parameters + ---------- + config : TrainingConfig + Training configuration object. + epoch : int + Current epoch number. + extras : dict + Dictionary of extra information to log for the current epoch. + This is updated in-place with any SWA-related information + (e.g., swa_n) and returned at the end. + + Returns + ------- + extras : dict + Updated dictionary of extra information to log for the + current epoch. + """ + if config.swa_start is not None and epoch >= config.swa_start: + # Switch to constant LR if specified (only once) + if config.swa_lr is not None and epoch == config.swa_start: + self.update_optimizer('all', learning_rate=config.swa_lr) + logger.info(f'Switched to SWA constant LR: {config.swa_lr}') + + # Update SWA weights at specified frequency + if (epoch - config.swa_start) % config.swa_freq == 0: + self.update_swa() + extras['swa_n'] = self._swa_n + + return extras + + def swap_swa_weights(self): + """Replace current model weights with SWA averaged weights. + + Call this after training is complete to use the averaged weights. + """ + if self._swa_weights is None: + logger.warning('No SWA weights to swap') + return + + logger.info( + f'Swapping to SWA weights (averaged over {self._swa_n} snapshots)' + ) + + # Store original weights as backup + self._pre_swa_weights = [w.numpy() for w in self.weights] + + # Set model weights to SWA averages + for weight_var, swa_weight in zip(self.weights, self._swa_weights): + weight_var.assign(swa_weight) + + def restore_pre_swa_weights(self): + """Restore weights from before SWA swap (for comparison/debugging).""" + if self._pre_swa_weights is None: + logger.warning('No pre-SWA weights to restore') + return + + for weight_var, pre_swa_weight in zip( + self.weights, self._pre_swa_weights + ): + weight_var.assign(pre_swa_weight) + logger.info('Restored pre-SWA weights') + + def update_bn_stats(self, batch_handler, n_batches=None): + """Update batch normalization statistics after swapping to SWA weights. + + This is critical because BN layers have running statistics computed + during training with the original weights, not the SWA averaged + weights. + + Parameters + ---------- + batch_handler : sup3r.preprocessing.BatchHandler + BatchHandler to iterate through for BN updates + n_batches : int | None + Number of batches to use. If None, uses all available batches. + """ + has_bn_layers = any( + isinstance(layer, (tf.keras.layers.BatchNormalization,)) + for layer in self.generator.layers + ) + if not has_bn_layers: + logger.info( + 'No batch normalization layers found, skipping BN stats update' + ) + return + + logger.info('Updating batch normalization statistics for SWA model...') + + # Reset BN layer statistics + for layer in self.generator.layers: + if isinstance(layer, (tf.keras.layers.BatchNormalization,)): + layer.moving_mean.assign(tf.zeros_like(layer.moving_mean)) + layer.moving_variance.assign( + tf.ones_like(layer.moving_variance) + ) + + # Do forward passes to recompute statistics + count = 0 + for batch in batch_handler: + if n_batches is not None and count >= n_batches: + break + _ = self.generate(batch.low_res, norm_in=True) + count += 1 + + logger.info(f'Updated BN stats using {count} batches') + @abstractmethod def save(self, out_dir): """Save the model with its sub-networks to a directory. diff --git a/sup3r/models/base.py b/sup3r/models/base.py index 0fa852d28..1bb5afc46 100644 --- a/sup3r/models/base.py +++ b/sup3r/models/base.py @@ -15,6 +15,7 @@ from .abstract import AbstractSingleModel from .interface import AbstractInterface +from .training_config import TrainingConfig from .utilities import get_optimizer_class logger = logging.getLogger(__name__) @@ -622,26 +623,42 @@ def check_batch_handler_attrs(batch_handler): if hasattr(batch_handler, k) } - def train( - self, - batch_handler, - input_resolution, - n_epoch, - weight_gen_advers=0.001, - train_gen=True, - train_disc=True, - disc_loss_bounds=(0.45, 0.6), - checkpoint_int=None, - out_dir='./gan_{epoch}', - early_stop_on=None, - early_stop_threshold=0.005, - early_stop_n_epoch=5, - adaptive_update_bounds=(0.9, 0.99), - adaptive_update_fraction=0.0, - multi_gpu=False, - log_tb=False, - export_tb=False, - ): + def finish_training(self, batch_handler, epoch=None, config=None): + """Finish training by applying SWA weights if enabled and updating + BN stats. + + Parameters + ---------- + batch_handler : sup3r.preprocessing.BatchHandler + BatchHandler object to iterate through + epoch : int | None + The current epoch number. If None, it will not be used. + config : TrainingConfig | None + Training configuration object. If None, one will be created from + kwargs. Using TrainingConfig is recommended for cleaner code. + """ + if config.swa_start is not None and self._swa_n > 0: + logger.info('Training complete. Applying SWA...') + self.swap_swa_weights() + + self.update_bn_stats( + batch_handler, n_batches=config.swa_bn_update_batches + ) + + logger.info('Computing validation loss with SWA weights...') + swa_val_loss = self.calc_val_loss( + batch_handler, config.weight_gen_advers + ) + logger.info(f'SWA validation loss: {swa_val_loss}') + + if '{epoch}' in config.out_dir: + swa_out_dir = config.out_dir.format(epoch=f'{epoch}_swa_final') + self.save(swa_out_dir) + logger.info(f'Saved SWA model to {swa_out_dir}') + + batch_handler.stop() + + def train(self, batch_handler, input_resolution, config=None, **kwargs): """Train the GAN model on real low res data and real high res data Parameters @@ -651,78 +668,45 @@ def train( input_resolution : dict Dictionary specifying spatiotemporal input resolution. e.g. {'temporal': '60min', 'spatial': '30km'} - n_epoch : int - Number of epochs to train on - weight_gen_advers : float - Weight factor for the adversarial loss component of the generator - vs. the discriminator. - train_gen : bool - Flag whether to train the generator for this set of epochs - train_disc : bool - Flag whether to train the discriminator for this set of epochs - disc_loss_bounds : tuple - Lower and upper bounds for the discriminator loss outside of which - the discriminator will not train unless train_disc=True and - train_gen=False. - checkpoint_int : int | None - Epoch interval at which to save checkpoint models. - out_dir : str - Directory to save checkpoint GAN models. Should have {epoch} in - the directory name. This directory will be created if it does not - already exist. - early_stop_on : str | None - If not None, this should be a column in the training history to - evaluate for early stopping (e.g. validation_loss_gen, - validation_loss_disc). If this value in this history decreases by - an absolute fractional relative difference of less than 0.01 for - more than 5 epochs in a row, the training will stop early. - early_stop_threshold : float - The absolute relative fractional difference in validation loss - between subsequent epochs below which an early termination is - warranted. E.g. if val losses were 0.1 and 0.0998 the relative - diff would be calculated as 0.0002 / 0.1 = 0.002 which would be - less than the default thresold of 0.01 and would satisfy the - condition for early termination. - early_stop_n_epoch : int - The number of consecutive epochs that satisfy the threshold that - warrants an early stop. - adaptive_update_bounds : tuple - Tuple specifying allowed range for loss_details[comparison_key]. If - history[comparison_key] < threshold_range[0] then the weight will - be increased by (1 + update_frac). If history[comparison_key] > - threshold_range[1] then the weight will be decreased by 1 / (1 + - update_frac). - adaptive_update_fraction : float - Amount by which to increase or decrease adversarial weights for - adaptive updates - multi_gpu : bool - Flag to break up the batch for parallel gradient descent - calculations on multiple gpus. If True and multiple GPUs are - present, each batch from the batch_handler will be divided up - between the GPUs and resulting gradients from each GPU will be - summed and then applied once per batch at the nominal learning - rate that the model and optimizer were initialized with. - If true and multiple gpus are found, ``default_device`` device - should be set to /gpu:0 - log_tb : bool - Whether to write log file for use with tensorboard. Log data can - be viewed with ``tensorboard --logdir `` where ```` - is the parent directory of ``out_dir``, and pointing the browser to - the printed address. - export_tb : bool - Whether to export profiling information to tensorboard. This can - then be viewed in the tensorboard dashboard under the profile tab - - TODO: (1) args here are getting excessive. Might be time for some - refactoring. - (2) cal_val_loss should be done in a separate thread from train_epoch - so they can be done concurrently. This would be especially important - for batch handlers which require val data, like dc handlers. - (3) Would like an automatic way to exit the batch handler thread - instead of manually calling .stop() here. + config : TrainingConfig | None + Training configuration object. If None, one will be created from + kwargs. Using TrainingConfig is recommended for cleaner code. + **kwargs : dict + Training parameters passed to TrainingConfig if config is None. + For backwards compatibility, supports all original train() params: + n_epoch, weight_gen_advers, train_gen, train_disc, + disc_loss_bounds, checkpoint_int, out_dir, early_stop_on, + early_stop_threshold, early_stop_n_epoch, adaptive_update_bounds, + adaptive_update_fraction, multi_gpu, log_tb, export_tb, swa_start, + swa_freq, swa_lr, swa_bn_update_batches + + Examples + -------- + Using TrainingConfig (recommended): + >>> from sup3r.models.training_config import TrainingConfig + >>> config = TrainingConfig( + ... n_epoch=100, + ... swa_start=75, + ... swa_lr=0.01, + ... checkpoint_int=10 + ... ) + >>> model.train(batch_handler, input_resolution, config=config) + + Using kwargs (backwards compatible): + >>> model.train( + ... batch_handler, + ... input_resolution, + ... n_epoch=100, + ... swa_start=75, + ... swa_lr=0.01 + ... ) """ - if log_tb: - self._init_tensorboard_writer(out_dir) + # Create config from kwargs if not provided + if config is None: + config = TrainingConfig(**kwargs) + + if config.log_tb: + self._init_tensorboard_writer(config.out_dir) self.set_norm_stats(batch_handler.means, batch_handler.stds) params = self.check_batch_handler_attrs(batch_handler) @@ -733,7 +717,7 @@ def train( **params, ) - epochs = list(range(n_epoch)) + epochs = list(range(config.n_epoch)) if self._history is None: self._history = pd.DataFrame(columns=['elapsed_time']) @@ -745,23 +729,31 @@ def train( logger.info( 'Training model with adversarial weight: {} ' 'for {} epochs starting at epoch {}'.format( - weight_gen_advers, n_epoch, epochs[0] + config.weight_gen_advers, config.n_epoch, epochs[0] ) ) + if config.swa_start is not None: + self.enable_swa() + logger.info( + f'SWA will start at epoch {config.swa_start} with ' + f'update frequency {config.swa_freq}' + ) + for epoch in epochs: t_epoch = time.time() + loss_details = self._train_epoch( batch_handler, - weight_gen_advers, - train_gen, - train_disc, - disc_loss_bounds, - multi_gpu=multi_gpu, - export_tb=export_tb, + config.weight_gen_advers, + config.train_gen, + config.train_disc, + config.disc_loss_bounds, + multi_gpu=config.multi_gpu, + export_tb=config.export_tb, ) loss_details.update( - self.calc_val_loss(batch_handler, weight_gen_advers) + self.calc_val_loss(batch_handler, config.weight_gen_advers) ) msg = f'Epoch {epoch} of {epochs[-1]} ' @@ -779,9 +771,9 @@ def train( logger.info(msg) extras = { - 'weight_gen_advers': weight_gen_advers, - 'disc_loss_bound_0': disc_loss_bounds[0], - 'disc_loss_bound_1': disc_loss_bounds[1], + 'weight_gen_advers': config.weight_gen_advers, + 'disc_loss_bound_0': config.disc_loss_bounds[0], + 'disc_loss_bound_1': config.disc_loss_bounds[1], } opt_g = self.get_optimizer_state(self.optimizer) @@ -791,12 +783,14 @@ def train( extras.update(opt_g) extras.update(opt_d) - weight_gen_advers = self.update_adversarial_weights( + extras = self.apply_swa(config, epoch, extras) + + config.weight_gen_advers = self.update_adversarial_weights( loss_details, - adaptive_update_fraction, - adaptive_update_bounds, - weight_gen_advers, - train_disc, + config.adaptive_update_fraction, + config.adaptive_update_bounds, + config.weight_gen_advers, + config.train_disc, ) stop = self.finish_epoch( @@ -804,11 +798,11 @@ def train( epochs, t0, loss_details, - checkpoint_int, - out_dir, - early_stop_on, - early_stop_threshold, - early_stop_n_epoch, + config.checkpoint_int, + config.out_dir, + config.early_stop_on, + config.early_stop_threshold, + config.early_stop_n_epoch, extras=extras, ) logger.info( @@ -818,14 +812,15 @@ def train( ) if stop: break + logger.info( 'Finished training {} epochs in {:.4f} seconds'.format( - n_epoch, + config.n_epoch, time.time() - t0, ) ) - batch_handler.stop() + self.finish_training(batch_handler, epoch=epochs[-1], config=config) def calc_loss( self, diff --git a/sup3r/models/training_config.py b/sup3r/models/training_config.py new file mode 100644 index 000000000..b701d96f6 --- /dev/null +++ b/sup3r/models/training_config.py @@ -0,0 +1,110 @@ +"""Training configuration for Sup3r models""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class TrainingConfig: + """Configuration for GAN training. + + This dataclass consolidates all training parameters to simplify the + train() method signature and make it easier to manage training configs. + + Parameters + ---------- + n_epoch : int + Number of epochs to train on + weight_gen_advers : float + Weight factor for the adversarial loss component of the generator + vs. the discriminator. + train_gen : bool + Flag whether to train the generator for this set of epochs + train_disc : bool + Flag whether to train the discriminator for this set of epochs + disc_loss_bounds : tuple + Lower and upper bounds for the discriminator loss outside of which + the discriminator will not train unless train_disc=True and + train_gen=False. + checkpoint_int : int | None + Epoch interval at which to save checkpoint models. + out_dir : str + Directory to save checkpoint GAN models. Should have {epoch} in + the directory name. This directory will be created if it does not + already exist. + early_stop_on : str | None + If not None, this should be a column in the training history to + evaluate for early stopping (e.g. validation_loss_gen, + validation_loss_disc). + early_stop_threshold : float + The absolute relative fractional difference in validation loss + between subsequent epochs below which an early termination is + warranted. + early_stop_n_epoch : int + The number of consecutive epochs that satisfy the threshold that + warrants an early stop. + adaptive_update_bounds : tuple + Tuple specifying allowed range for loss_details[comparison_key]. + adaptive_update_fraction : float + Amount by which to increase or decrease adversarial weights for + adaptive updates + multi_gpu : bool + Flag to break up the batch for parallel gradient descent + calculations on multiple gpus. + log_tb : bool + Whether to write log file for use with tensorboard. + export_tb : bool + Whether to export profiling information to tensorboard. + swa_start : int | None + Epoch to start SWA (e.g., int(0.75 * n_epoch)). If None, SWA is + disabled. + swa_freq : int + How often to update SWA (1 = every epoch) + swa_lr : float | None + Constant learning rate for SWA phase (if None, keeps current schedule) + swa_bn_update_batches : int + Number of batches to use for updating batch normalization statistics + after swapping to SWA weights. (Only used if model has batch + normalization layers and swa_start is not None) + """ + + n_epoch: int + weight_gen_advers: float = 0.001 + train_gen: bool = True + train_disc: bool = True + disc_loss_bounds: tuple[float, float] = (0.45, 0.6) + checkpoint_int: Optional[int] = None + out_dir: str = './gan_e{epoch}' + early_stop_on: Optional[str] = None + early_stop_threshold: float = 0.005 + early_stop_n_epoch: int = 5 + adaptive_update_bounds: tuple[float, float] = (0.9, 0.99) + adaptive_update_fraction: float = 0.0 + multi_gpu: bool = False + log_tb: bool = False + export_tb: bool = False + swa_start: Optional[int] = None + swa_freq: int = 1 + swa_lr: Optional[float] = None + swa_bn_update_batches: int = 100 + + def __post_init__(self): + """Validate configuration after initialization.""" + if self.n_epoch <= 0: + raise ValueError(f'n_epoch must be positive, got {self.n_epoch}') + + if self.swa_start is not None: + if self.swa_start < 0: + raise ValueError( + f'swa_start must be non-negative, got {self.swa_start}' + ) + if self.swa_freq <= 0: + raise ValueError( + f'swa_freq must be positive, got {self.swa_freq}' + ) + + if '{epoch}' not in self.out_dir and self.checkpoint_int is not None: + raise ValueError( + f"out_dir must contain '{{epoch}}' when checkpoint_int is " + f'set, got {self.out_dir}' + ) diff --git a/sup3r/preprocessing/loaders/xr.py b/sup3r/preprocessing/loaders/xr.py index 816c99fb6..f6aa2b881 100644 --- a/sup3r/preprocessing/loaders/xr.py +++ b/sup3r/preprocessing/loaders/xr.py @@ -57,12 +57,12 @@ def _enforce_descending_levels(cls, dset): if invert_levels: for var in list(dset.data_vars): if Dimension.PRESSURE_LEVEL in dset[var].dims: - new_var = dset[var].isel( - {Dimension.PRESSURE_LEVEL: slice(None, None, -1)} - ) - dset.update( - {var: (dset[var].dims, new_var.data, dset[var].attrs)} - ) + new_var = dset[var].isel({ + Dimension.PRESSURE_LEVEL: slice(None, None, -1) + }) + dset.update({ + var: (dset[var].dims, new_var.data, dset[var].attrs) + }) new_press = dset[Dimension.PRESSURE_LEVEL][::-1] dset.update({Dimension.PRESSURE_LEVEL: new_press}) return dset diff --git a/sup3r/preprocessing/utilities.py b/sup3r/preprocessing/utilities.py index 457f45580..dbf0ed26e 100644 --- a/sup3r/preprocessing/utilities.py +++ b/sup3r/preprocessing/utilities.py @@ -499,7 +499,8 @@ def parse_keys( def parse_to_list(features=None, data=None): """Parse features and return as a list, even if features is a string.""" features = ( - np.array( + np + .array( list(features) if isinstance(features, (set, tuple)) else features diff --git a/sup3r/utilities/utilities.py b/sup3r/utilities/utilities.py index 23f9d7c8a..1edf1a547 100644 --- a/sup3r/utilities/utilities.py +++ b/sup3r/utilities/utilities.py @@ -120,13 +120,18 @@ def preprocess_datasets(dset): def xr_open_mfdataset(files, **kwargs): """Wrapper for xr.open_mfdataset with default opening options.""" - default_kwargs = {'engine': 'netcdf4'} - default_kwargs.update(kwargs) if isinstance(files, str): files = [files] - out = xr.open_mfdataset( - files, preprocess=preprocess_datasets, **default_kwargs - ) + + # Auto-detect zarr files and set appropriate engine + if 'engine' not in kwargs: + first_file = files[0] if files else '' + if first_file.endswith('.zarr'): + kwargs['engine'] = 'zarr' + else: + kwargs['engine'] = 'netcdf4' + + out = xr.open_mfdataset(files, preprocess=preprocess_datasets, **kwargs) bad_dims = ( 'latitude' in out and len(out['latitude'].dims) == 2 diff --git a/tests/training/test_train_swa.py b/tests/training/test_train_swa.py new file mode 100644 index 000000000..05fdfafcc --- /dev/null +++ b/tests/training/test_train_swa.py @@ -0,0 +1,694 @@ +"""Test training with Stochastic Weight Averaging (SWA)""" + +import os +import tempfile + +import numpy as np +import pytest + +from sup3r.models import Sup3rGan, TrainingConfig +from sup3r.preprocessing import BatchHandler, DataHandler +from sup3r.utilities.utilities import RANDOM_GENERATOR + +TARGET_COORD = (39.01, -105.15) +FEATURES = ['u_100m', 'v_100m'] + + +def _get_handlers(): + """Initialize training and validation handlers used across tests.""" + + kwargs = { + 'file_paths': pytest.FP_WTK, + 'features': FEATURES, + 'target': TARGET_COORD, + 'shape': (20, 20), + } + train_handler = DataHandler( + **kwargs, + time_slice=slice(1000, None, 1), + ) + + val_handler = DataHandler( + **kwargs, + time_slice=slice(None, 1000, 1), + ) + + return train_handler, val_handler + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.ST_FP_GEN, pytest.ST_FP_DISC, 3, 4, (12, 12, 16)), + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_basic( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=10 +): + """Test basic SWA training with TrainingConfig.""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # Configure SWA to start at epoch 7 + config = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + checkpoint_int=None, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=7, + swa_freq=1, + swa_lr=lr * 0.1, + swa_bn_update_batches=2, + ) + + # Verify SWA is not enabled before training + assert not model._swa_enabled + assert model._swa_n == 0 + + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config, + ) + + # Verify SWA was enabled during training + assert model._swa_enabled + # Should have 3 SWA updates (epochs 7, 8, 9) + assert model._swa_n == 3 + + # Verify SWA weights were created + assert model._swa_weights is not None + assert len(model._swa_weights) == len(model.weights) + + # Verify pre-SWA weights were saved + assert model._pre_swa_weights is not None + + # Verify SWA model was saved + swa_dir = os.path.join(td, f'test_{n_epoch - 1}_swa_final') + assert os.path.exists(swa_dir) + assert 'model_gen.pkl' in os.listdir(swa_dir) + assert 'model_disc.pkl' in os.listdir(swa_dir) + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.ST_FP_GEN, pytest.ST_FP_DISC, 3, 4, (12, 12, 16)), + ], +) +def test_swa_kwargs( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=10 +): + """Test SWA training using kwargs (backwards compatibility).""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # Train using kwargs instead of TrainingConfig + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=7, + swa_freq=1, + swa_lr=lr * 0.1, + ) + + # Verify SWA was applied + assert model._swa_enabled + assert model._swa_n == 3 + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_weight_averaging( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=8 +): + """Test that SWA actually averages weights correctly.""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # Enable SWA manually to track weights + model.enable_swa() + + # Get initial weights + initial_weights = [w.numpy().copy() for w in model.weights] + + config = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=5, + swa_freq=1, + swa_lr=lr * 0.1, + ) + + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config, + ) + + # Verify weights changed from initial + current_weights = [w.numpy() for w in model.weights] + for i, (init_w, curr_w) in enumerate( + zip(initial_weights, current_weights) + ): + assert not np.allclose(init_w, curr_w), ( + f'Weight {i} did not change' + ) + + # Verify SWA weights are different from final weights + # (before swap, final weights are the last SGD weights) + assert model._pre_swa_weights is not None + for i, (swa_w, pre_swa_w) in enumerate( + zip(model._swa_weights, model._pre_swa_weights) + ): + # SWA weights should be different from the last SGD weights + # (unless they happened to converge to the same point) + if not np.allclose(swa_w, pre_swa_w, rtol=1e-3): + # Found at least one weight that differs + break + else: + # This is unlikely but not impossible + pass + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_manual_control( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape +): + """Test manual SWA control methods.""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # Initialize weights + lr_shape, hr_shape = batch_handler.shapes + model.meta['hr_out_features'] = ['u_100m', 'v_100m'] + model.init_weights(lr_shape, hr_shape) + + # Test enable_swa + assert not model._swa_enabled + model.enable_swa() + assert model._swa_enabled + assert model._swa_n == 0 + + # Test update_swa + weights_before = [w.numpy().copy() for w in model.weights] + model.update_swa() + assert model._swa_n == 1 + assert model._swa_weights is not None + + # Verify SWA weights match current weights after first update + for swa_w, curr_w in zip(model._swa_weights, weights_before): + assert np.allclose(swa_w, curr_w) + + # Test swap_swa_weights + model.swap_swa_weights() + assert model._pre_swa_weights is not None + + # Test restore_pre_swa_weights + model.restore_pre_swa_weights() + weights_after_restore = [w.numpy() for w in model.weights] + for w_before, w_after in zip(weights_before, weights_after_restore): + assert np.allclose(w_before, w_after) + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_freq( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=12 +): + """Test SWA with different update frequencies.""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # SWA starts at epoch 6, updates every 2 epochs + # Should update at epochs: 6, 8, 10 (3 updates) + config = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=6, + swa_freq=2, + swa_lr=lr * 0.1, + ) + + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config, + ) + + # Should have 3 SWA updates + assert model._swa_n == 3 + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_no_constant_lr( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=8 +): + """Test SWA without constant learning rate (keeps existing schedule).""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # Configure SWA without constant LR (swa_lr=None) + config = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=5, + swa_freq=1, + swa_lr=None, # Don't change LR schedule + ) + + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config, + ) + + # Verify SWA was still applied + assert model._swa_enabled + assert model._swa_n == 3 # epochs 5, 6, 7 + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_no_swa( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=8 +): + """Test that training works normally without SWA enabled.""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # Configure without SWA (swa_start=None) + config = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=None, # Disable SWA + ) + + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config, + ) + + # Verify SWA was not enabled + assert not model._swa_enabled + assert model._swa_n == 0 + assert model._swa_weights is None + + # Verify regular checkpoint was saved but not SWA + assert not os.path.exists(os.path.join(td, 'test_swa_final')) + + batch_handler.stop() + + +def test_training_config_validation(): + """Test TrainingConfig validation.""" + + # Valid config + config = TrainingConfig( + n_epoch=100, swa_start=75, out_dir='./model_{epoch}' + ) + assert config.n_epoch == 100 + assert config.swa_start == 75 + + # Invalid: n_epoch must be positive + with pytest.raises(ValueError, match='n_epoch must be positive'): + TrainingConfig(n_epoch=0) + + with pytest.raises(ValueError, match='swa_start must be non-negative'): + TrainingConfig(n_epoch=100, swa_start=-1) + + # Invalid: swa_freq must be positive + with pytest.raises(ValueError, match='swa_freq must be positive'): + TrainingConfig(n_epoch=100, swa_start=75, swa_freq=0) + + # Invalid: out_dir must contain {epoch} when checkpoint_int is set + with pytest.raises(ValueError, match='out_dir must contain'): + TrainingConfig( + n_epoch=100, checkpoint_int=10, out_dir='./model_no_epoch' + ) + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_load_and_continue( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=10 +): + """Test loading a SWA model and verifying weights.""" + + lr = 5e-5 + Sup3rGan.seed() + model = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + config = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'test_{epoch}'), + swa_start=7, + swa_freq=1, + swa_lr=lr * 0.1, + ) + + model.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config, + ) + + # Save current weights (should be SWA weights) + swa_weights_saved = [w.numpy().copy() for w in model.weights] + + # Load the SWA model + swa_dir = os.path.join(td, f'test_{n_epoch - 1}_swa_final') + loaded_model = Sup3rGan.load(swa_dir) + + # Verify loaded weights match SWA weights + loaded_weights = [w.numpy() for w in loaded_model.weights] + for orig_w, load_w in zip(swa_weights_saved, loaded_weights): + assert np.allclose(orig_w, load_w) + + batch_handler.stop() + + +@pytest.mark.parametrize( + ['fp_gen', 'fp_disc', 's_enhance', 't_enhance', 'sample_shape'], + [ + (pytest.S_FP_GEN, pytest.S_FP_DISC, 2, 1, (10, 10, 1)), + ], +) +def test_swa_single_epoch_equals_no_swa( + fp_gen, fp_disc, s_enhance, t_enhance, sample_shape, n_epoch=8 +): + """Test that SWA with single epoch averaging equals no SWA. + + When SWA averages only the final epoch's weights, the result should be + identical to training without SWA (both should have the same final + weights). + """ + + lr = 5e-5 + n_epoch = 1 + + # Train model WITHOUT SWA + Sup3rGan.seed(42) + state = RANDOM_GENERATOR.bit_generator.state + model_no_swa = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + config_no_swa = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'no_swa_{epoch}'), + swa_start=None, + ) + + model_no_swa.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config_no_swa, + ) + + # Save weights from model without SWA + weights_no_swa = [w.numpy().copy() for w in model_no_swa.weights] + + # Train model WITH SWA starting at last epoch (single snapshot) + Sup3rGan.seed(42) # Reset seed for identical training + RANDOM_GENERATOR.bit_generator.state = state + model_swa = Sup3rGan( + fp_gen, fp_disc, learning_rate=lr, loss='MeanAbsoluteError' + ) + + train_handler, val_handler = _get_handlers() + + with tempfile.TemporaryDirectory() as td: + batch_handler = BatchHandler( + train_containers=[train_handler], + val_containers=[val_handler], + sample_shape=sample_shape, + batch_size=15, + s_enhance=s_enhance, + t_enhance=t_enhance, + n_batches=5, + means=None, + stds=None, + ) + + # SWA starts at the last epoch (n_epoch - 1), so only 1 snapshot + config_swa = TrainingConfig( + n_epoch=n_epoch, + weight_gen_advers=0, + train_gen=True, + train_disc=False, + out_dir=os.path.join(td, 'swa_{epoch}'), + swa_start=n_epoch - 1, # Start at last epoch + swa_freq=1, + swa_lr=None, # Keep same LR schedule + ) + + model_swa.train( + batch_handler, + input_resolution={'spatial': '30km', 'temporal': '60min'}, + config=config_swa, + ) + + # Verify SWA was enabled but only took 1 snapshot + assert model_swa._swa_enabled + assert model_swa._swa_n == 1 + + # Save weights from model with SWA (should be SWA averaged) + weights_swa = [w.numpy().copy() for w in model_swa.weights] + + # Verify weights are identical (or very close due to numerical precision) + # Since SWA with 1 snapshot should equal the final SGD weights + for i, (w_no_swa, w_swa) in enumerate(zip(weights_no_swa, weights_swa)): + assert np.allclose(w_no_swa, w_swa, rtol=1e-3, atol=1e-3), ( + f'Weight {i} differs between no-SWA and single-epoch SWA. ' + f'Max diff: {np.max(np.abs(w_no_swa - w_swa))}' + )