From 53657ca5fbf333e1d3a4fb3317236c4c72e8cd02 Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 19:20:07 +0200 Subject: [PATCH 01/35] lock --- uv.lock | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index d67e605..0c624ff 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", @@ -211,6 +211,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] +[[package]] +name = "cartopy" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyproj" }, + { name = "pyshp" }, + { name = "shapely" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/ec3dee34237b696a486d566a6d3ae6550ae821836e0412bafdcbbec2cfd2/cartopy-0.25.0.tar.gz", hash = "sha256:55f1a390e5f3f075b221c7d91fb10258ad978db786c7930eba06eb45d28753fe", size = 10767728, upload-time = "2025-08-01T12:44:16.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e1/6a52ee21424da0ed30860f4e94d1657ade8d4436f0718485badf0e63011e/cartopy-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e41d52160548a7ab7774423911db3bfb5a8bc0929580958b1945d3a004da872", size = 11006320, upload-time = "2025-08-01T12:43:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/68/06/38bcfeab9822acffc86474659d33c4dc3c5dec4e61e9927fb8cc8617f651/cartopy-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:432e2a2688fc58af43b9b6bf1d343bb08e2d6ef298efa91e55445f1d308b5ef3", size = 10995635, upload-time = "2025-08-01T12:43:50.855Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b6/f39407d27d641a949496a52ab00220fe0635758e3cb7afb4b7328abe17e7/cartopy-0.25.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:999e44021db07dcf895b115934fb0816aef39985fbaca6ded61d2536355531de", size = 11808214, upload-time = "2025-08-01T12:43:53.218Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c0/b33ac1f586608e80a5e10f3924e16c117da333fcb5e5240839e6681ac3d5/cartopy-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:4139e5ca9faaa037e0576cdcf625b9461a0b404d60e9d20ea24c4d8dbe6f689d", size = 10983301, upload-time = "2025-08-01T12:43:55.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/35/b19901cbe7f1b118dccbb9e655cda7d01a31ee1ecd67e5d2d8afe119f6d3/cartopy-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:060a7b835c0c4222c1067b6ffb2f9c18458abaa35b6624573a3aa37ecf55f4bf", size = 11006900, upload-time = "2025-08-01T12:43:57.708Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4f/09e824f86be09152ec0f1fa1fe69affbd34eac7a13b545e2e08b9b6bc8ff/cartopy-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57717cb603aecff03ecfee1bc153bb4022c054fcd51a4214a1bb53e5a6f74465", size = 10994813, upload-time = "2025-08-01T12:44:00.069Z" }, + { url = "https://files.pythonhosted.org/packages/b9/30/7465b650110514fc5c9c3b59935264c35ab56f876322de34efa55367ee4e/cartopy-0.25.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c256351433155ef51dde976557212f4e230b8cca4e5d0d9b9a2737ad92959d", size = 11799069, upload-time = "2025-08-01T12:44:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/1d/52/3a57ecb4598c33ee06b512d3686e46b3983e65abd6ec94c5262d01930ed9/cartopy-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:efedb82f38409b72becdfee02231126952816d33a68b1c584bd2136713036bfb", size = 10983127, upload-time = "2025-08-01T12:44:04.441Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b9/0773ff8f1c755b8a362029e6910db87064d27ca021b060c48ce511ec98b7/cartopy-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6fcd2df8039293096f957fc9c76e969b1a9715d12ab8cee1a6bdae0c6773b8b", size = 11007728, upload-time = "2025-08-01T12:44:06.64Z" }, + { url = "https://files.pythonhosted.org/packages/34/a6/75738630b7f64bca7afc6bc5de08ddf0c61f13563f2a1abf642373d1095e/cartopy-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4def451617e6957169447fe6ecdad0f63ef2d2007e7d451dd7b9656ada57382", size = 10996613, upload-time = "2025-08-01T12:44:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/669d4bbeb36b87ba504409d85c68ec297e6f434ea6525424f8aa5f14abac/cartopy-0.25.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c388824cb13e4fa9c2901dc4fbb2dbe9547acd2f4a6a3440983d4e6c6973ae3", size = 11829044, upload-time = "2025-08-01T12:44:11.402Z" }, + { url = "https://files.pythonhosted.org/packages/01/ff/b46e2120abd99b2ff3d376dc91ed58ae8f0a052d57c242c9b140497573dd/cartopy-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:60bad14c072d16e3c96967638cd66eb5a62cf24bc70087bcbfc6b30a3872ed26", size = 10987060, upload-time = "2025-08-01T12:44:14.222Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -926,6 +954,7 @@ version = "0.1.0b1" source = { editable = "." } dependencies = [ { name = "anemoi-datasets" }, + { name = "cartopy" }, { name = "click" }, { name = "meteodata-lab" }, { name = "mlflow" }, @@ -953,6 +982,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anemoi-datasets", specifier = ">=0.5.25" }, + { name = "cartopy" }, { name = "click" }, { name = "fastparquet", marker = "extra == 'kerchunk'" }, { name = "kerchunk", marker = "extra == 'kerchunk'" }, @@ -2566,6 +2596,71 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, ] +[[package]] +name = "pyproj" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bd/f205552cd1713b08f93b09e39a3ec99edef0b3ebbbca67b486fdf1abe2de/pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5", size = 6227022, upload-time = "2025-08-14T12:03:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/75/4c/9a937e659b8b418ab573c6d340d27e68716928953273e0837e7922fcac34/pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a", size = 4625810, upload-time = "2025-08-14T12:03:53.808Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/a9f41e814dc4d1dc54e95b2ccaf0b3ebe3eb18b1740df05fe334724c3d89/pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25", size = 9638694, upload-time = "2025-08-14T12:03:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ab/9bdb4a6216b712a1f9aab1c0fcbee5d3726f34a366f29c3e8c08a78d6b70/pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a", size = 9493977, upload-time = "2025-08-14T12:03:57.937Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/2db75b1b6190f1137b1c4e8ef6a22e1c338e46320f6329bfac819143e063/pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc", size = 10841151, upload-time = "2025-08-14T12:04:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/89/f7/989643394ba23a286e9b7b3f09981496172f9e0d4512457ffea7dc47ffc7/pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5", size = 10751585, upload-time = "2025-08-14T12:04:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/53/6d/ad928fe975a6c14a093c92e6a319ca18f479f3336bb353a740bdba335681/pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a", size = 5908533, upload-time = "2025-08-14T12:04:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433", size = 6320742, upload-time = "2025-08-14T12:04:06.357Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/536e8f93bca808175c2d0a5ac9fdf69b960d8ab6b14f25030dccb07464d7/pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71", size = 6245772, upload-time = "2025-08-14T12:04:08.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, +] + [[package]] name = "pyreadline3" version = "3.5.4" @@ -2575,6 +2670,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] +[[package]] +name = "pyshp" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9f/0dd21250c60375a532c35e89fad8d5e8a3f1a2e3f7c389ccc5a60b05263e/pyshp-2.3.1.tar.gz", hash = "sha256:4caec82fd8dd096feba8217858068bacb2a3b5950f43c048c6dc32a3489d5af1", size = 1731544, upload-time = "2022-07-27T19:51:28.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/2f/68116db5b36b895c0450e3072b8cb6c2fac0359279b182ea97014d3c8ac0/pyshp-2.3.1-py2.py3-none-any.whl", hash = "sha256:67024c0ccdc352ba5db777c4e968483782dfa78f8e200672a90d2d30fd8b7b49", size = 46537, upload-time = "2022-07-27T19:51:26.34Z" }, +] + [[package]] name = "pytest" version = "8.4.1" @@ -2966,6 +3070,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +name = "shapely" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422, upload-time = "2025-05-19T11:04:41.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/2df985b1e03f90c503796ad5ecd3d9ed305123b64d4ccb54616b30295b29/shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7", size = 1819368, upload-time = "2025-05-19T11:03:55.937Z" }, + { url = "https://files.pythonhosted.org/packages/56/17/504518860370f0a28908b18864f43d72f03581e2b6680540ca668f07aa42/shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea", size = 1625362, upload-time = "2025-05-19T11:03:57.06Z" }, + { url = "https://files.pythonhosted.org/packages/36/a1/9677337d729b79fce1ef3296aac6b8ef4743419086f669e8a8070eff8f40/shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7", size = 2999005, upload-time = "2025-05-19T11:03:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/a2/17/e09357274699c6e012bbb5a8ea14765a4d5860bb658df1931c9f90d53bd3/shapely-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586f6aee1edec04e16227517a866df3e9a2e43c1f635efc32978bb3dc9c63753", size = 3108489, upload-time = "2025-05-19T11:04:00.059Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/93a6c37c4b4e9955ad40834f42b17260ca74ecf36df2e81bb14d12221b90/shapely-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9878b9e37ad26c72aada8de0c9cfe418d9e2ff36992a1693b7f65a075b28647", size = 3945727, upload-time = "2025-05-19T11:04:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1a/ad696648f16fd82dd6bfcca0b3b8fbafa7aacc13431c7fc4c9b49e481681/shapely-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9a531c48f289ba355e37b134e98e28c557ff13965d4653a5228d0f42a09aed0", size = 4109311, upload-time = "2025-05-19T11:04:03.134Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/150dd245beab179ec0d4472bf6799bf18f21b1efbef59ac87de3377dbf1c/shapely-2.1.1-cp311-cp311-win32.whl", hash = "sha256:4866de2673a971820c75c0167b1f1cd8fb76f2d641101c23d3ca021ad0449bab", size = 1522982, upload-time = "2025-05-19T11:04:05.217Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/842022c00fbb051083c1c85430f3bb55565b7fd2d775f4f398c0ba8052ce/shapely-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:20a9d79958b3d6c70d8a886b250047ea32ff40489d7abb47d01498c704557a93", size = 1703872, upload-time = "2025-05-19T11:04:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/fb/64/9544dc07dfe80a2d489060791300827c941c451e2910f7364b19607ea352/shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43", size = 1833021, upload-time = "2025-05-19T11:04:08.022Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/fb5f545e72e89b6a0f04a0effda144f5be956c9c312c7d4e00dfddbddbcf/shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad", size = 1643018, upload-time = "2025-05-19T11:04:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/03/46/61e03edba81de729f09d880ce7ae5c1af873a0814206bbfb4402ab5c3388/shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9", size = 2986417, upload-time = "2025-05-19T11:04:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/83ec268ab8254a446b4178b45616ab5822d7b9d2b7eb6e27cf0b82f45601/shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef", size = 3098224, upload-time = "2025-05-19T11:04:11.903Z" }, + { url = "https://files.pythonhosted.org/packages/f1/44/0c21e7717c243e067c9ef8fa9126de24239f8345a5bba9280f7bb9935959/shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1", size = 3925982, upload-time = "2025-05-19T11:04:13.224Z" }, + { url = "https://files.pythonhosted.org/packages/15/50/d3b4e15fefc103a0eb13d83bad5f65cd6e07a5d8b2ae920e767932a247d1/shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d", size = 4089122, upload-time = "2025-05-19T11:04:14.477Z" }, + { url = "https://files.pythonhosted.org/packages/bd/05/9a68f27fc6110baeedeeebc14fd86e73fa38738c5b741302408fb6355577/shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8", size = 1522437, upload-time = "2025-05-19T11:04:16.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e9/a4560e12b9338842a1f82c9016d2543eaa084fce30a1ca11991143086b57/shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a", size = 1703479, upload-time = "2025-05-19T11:04:18.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3004a644d9e89e26c20286d5fdc10f41b1744c48ce910bd1867fdff963fe6c48", size = 1832107, upload-time = "2025-05-19T11:04:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1415146fa12d80a47d13cfad5310b3c8b9c2aa8c14a0c845c9d3d75e77cb54f6", size = 1642355, upload-time = "2025-05-19T11:04:21.035Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/6d28b43d53fea56de69c744e34c2b999ed4042f7a811dc1bceb876071c95/shapely-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21fcab88b7520820ec16d09d6bea68652ca13993c84dffc6129dc3607c95594c", size = 2968871, upload-time = "2025-05-19T11:04:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ce6a5cc52c974b291237a96c08c5592e50f066871704fb5b12be2639d9026a", size = 3080830, upload-time = "2025-05-19T11:04:23.997Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fe/f4a03d81abd96a6ce31c49cd8aaba970eaaa98e191bd1e4d43041e57ae5a/shapely-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:04e4c12a45a1d70aeb266618d8cf81a2de9c4df511b63e105b90bfdfb52146de", size = 3908961, upload-time = "2025-05-19T11:04:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/ef/59/7605289a95a6844056a2017ab36d9b0cb9d6a3c3b5317c1f968c193031c9/shapely-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ca74d851ca5264aae16c2b47e96735579686cb69fa93c4078070a0ec845b8d8", size = 4079623, upload-time = "2025-05-19T11:04:27.171Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4d/9fea036eff2ef4059d30247128b2d67aaa5f0b25e9fc27e1d15cc1b84704/shapely-2.1.1-cp313-cp313-win32.whl", hash = "sha256:fd9130501bf42ffb7e0695b9ea17a27ae8ce68d50b56b6941c7f9b3d3453bc52", size = 1521916, upload-time = "2025-05-19T11:04:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:ab8d878687b438a2f4c138ed1a80941c6ab0029e0f4c785ecfe114413b498a97", size = 1702746, upload-time = "2025-05-19T11:04:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/60/36/b1452e3e7f35f5f6454d96f3be6e2bb87082720ff6c9437ecc215fa79be0/shapely-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c062384316a47f776305ed2fa22182717508ffdeb4a56d0ff4087a77b2a0f6d", size = 1833482, upload-time = "2025-05-19T11:04:30.852Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/8e6f59be0718893eb3e478141285796a923636dc8f086f83e5b0ec0036d0/shapely-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4ecf6c196b896e8f1360cc219ed4eee1c1e5f5883e505d449f263bd053fb8c05", size = 1642256, upload-time = "2025-05-19T11:04:32.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/0053aea449bb1d4503999525fec6232f049abcdc8df60d290416110de943/shapely-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb00070b4c4860f6743c600285109c273cca5241e970ad56bb87bef0be1ea3a0", size = 3016614, upload-time = "2025-05-19T11:04:33.7Z" }, + { url = "https://files.pythonhosted.org/packages/ee/53/36f1b1de1dfafd1b457dcbafa785b298ce1b8a3e7026b79619e708a245d5/shapely-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14a9afa5fa980fbe7bf63706fdfb8ff588f638f145a1d9dbc18374b5b7de913", size = 3093542, upload-time = "2025-05-19T11:04:34.952Z" }, + { url = "https://files.pythonhosted.org/packages/b9/bf/0619f37ceec6b924d84427c88835b61f27f43560239936ff88915c37da19/shapely-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b640e390dabde790e3fb947198b466e63223e0a9ccd787da5f07bcb14756c28d", size = 3945961, upload-time = "2025-05-19T11:04:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/93/c9/20ca4afeb572763b07a7997f00854cb9499df6af85929e93012b189d8917/shapely-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:69e08bf9697c1b73ec6aa70437db922bafcea7baca131c90c26d59491a9760f9", size = 4089514, upload-time = "2025-05-19T11:04:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/27036a5a560b80012a544366bceafd491e8abb94a8db14047b5346b5a749/shapely-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:ef2d09d5a964cc90c2c18b03566cf918a61c248596998a0301d5b632beadb9db", size = 1540607, upload-time = "2025-05-19T11:04:38.925Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061, upload-time = "2025-05-19T11:04:40.082Z" }, +] + [[package]] name = "six" version = "1.17.0" From b8c64408caadc03f2e45c4b9802705058f2148df Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 19:20:22 +0200 Subject: [PATCH 02/35] add cartopy --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4c60c9c..3ffd473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "pydantic>=2.11.7", "toml>=0.10.2", "netcdf4>=1.7.2", + "cartopy" ] [project.optional-dependencies] From 4759a307450027472841b9e4b982f2a8505f99d9 Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 19:21:01 +0200 Subject: [PATCH 03/35] generate raw --- resources/inference/configs/forecaster.yaml | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/resources/inference/configs/forecaster.yaml b/resources/inference/configs/forecaster.yaml index dac3e49..59cdc1f 100644 --- a/resources/inference/configs/forecaster.yaml +++ b/resources/inference/configs/forecaster.yaml @@ -7,18 +7,19 @@ allow_nans: true output: tee: - outputs: - - extract_lam: - output: - assign_mask: - mask: "source0/trimedge_mask" - output: - grib: - path: grib/{dateTime}_{step:03}.grib - encoding: - typeOfGeneratingProcess: 2 - templates: - samples: _resources/templates_index_cosmo.yaml + outputs: +# - extract_lam: +# output: +# assign_mask: +# mask: "source0/trimedge_mask" +# output: +# grib: +# path: grib/{dateTime}_{step:03}.grib +# encoding: +# typeOfGeneratingProcess: 2 +# templates: +# samples: _resources/templates_index_cosmo.yaml - printer + - raw: raw/ write_initial_state: true From 4db180ed9aff768f6ecc8d1d00d0803b39977487 Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 19:21:35 +0200 Subject: [PATCH 04/35] add plot rule --- workflow/Snakefile | 7 ++++++- workflow/rules/plot.smk | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 workflow/rules/plot.smk diff --git a/workflow/Snakefile b/workflow/Snakefile index 8b8d99a..d10003b 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -15,6 +15,7 @@ include: "rules/data.smk" include: "rules/inference.smk" include: "rules/verif.smk" include: "rules/report.smk" +include: "rules/plot.smk" # optional messages, log and error handling @@ -46,7 +47,11 @@ rule experiment_all: rule showcase_all: """Target rule for showcase workflow.""" input: - [], + expand( + OUT_ROOT / "data/runs/{run_id}/{init_time}/plots", + init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES], + run_id=collect_all_runs(), + ), rule sandbox_all: diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk new file mode 100644 index 0000000..5c37f88 --- /dev/null +++ b/workflow/rules/plot.smk @@ -0,0 +1,26 @@ +# ----------------------------------------------------- # +# PLOTTING WORKFLOW # +# ----------------------------------------------------- # +from datetime import datetime + +include: "common.smk" + +rule plot_forecast: + input: +# grib_output=rules.map_init_time_to_inference_group.output[0], + raw_output=rules.inference_routing.output[1], + output: + directory(OUT_ROOT / "data/runs/{run_id}/{init_time}/plots/"), + params: + sources=",".join(list(EXPERIMENT_PARTICIPANTS.keys())), + log: + OUT_ROOT / "logs/plot_forecast/{run_id}-{init_time}.log", + resources: + slurm_partition="postproc", + cpus_per_task=24, + runtime="20m", + shell: + """ + python workflow/scripts/plot_map.py \ + --input {input.raw_output} --date {init_time} --output {output}\ + """ From db9d4db0f4d246ab73f10c7d3b9f06e2323fa32d Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 21:09:38 +0200 Subject: [PATCH 05/35] fix init_time --- workflow/rules/plot.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 5c37f88..5138d84 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -22,5 +22,5 @@ rule plot_forecast: shell: """ python workflow/scripts/plot_map.py \ - --input {input.raw_output} --date {init_time} --output {output}\ + --input {input.raw_output} --date {wildcards.init_time} --output {output[0]}\ """ From 7faa37b2bb3e17f8008aac6969958d91f7877e27 Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 21:13:01 +0200 Subject: [PATCH 06/35] adding plotting files --- workflow/scripts/src/cmaps.py | 176 ++++++++++++++++++++++++++++++ workflow/scripts/src/compat.py | 21 ++++ workflow/scripts/src/plotting.py | 177 +++++++++++++++++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 workflow/scripts/src/cmaps.py create mode 100644 workflow/scripts/src/compat.py create mode 100644 workflow/scripts/src/plotting.py diff --git a/workflow/scripts/src/cmaps.py b/workflow/scripts/src/cmaps.py new file mode 100644 index 0000000..bc74054 --- /dev/null +++ b/workflow/scripts/src/cmaps.py @@ -0,0 +1,176 @@ +import matplotlib.pyplot as plt +from matplotlib.colors import LinearSegmentedColormap + +# WInd RGB values +rgb_list = [ + (255, 255, 255), + (239, 244, 209), + (232, 244, 158), + (170, 206, 99), + (226, 237, 22), + (255, 237, 0), + (255, 237, 130), + (244, 209, 127), + (237, 165, 73), + (229, 140, 61), + (219, 124, 61), + (239, 7, 61), + (232, 86, 163), + (155, 112, 168), + (99, 112, 247), + (127, 150, 255), + (142, 178, 255), + (181, 201, 255), +] + + +levels = [ + 4.0, + 6.0, + 10.0, + 14.0, + 18.0, + 22.0, + 26.0, + 30.0, + 35.0, + 40.0, + 45.0, + 50.0, + 60.0, + 70.0, + 80.0, + 90.0, + 100.0, +] + +# levels = [(lv / 100.0) * 75.0 for lv in levels] + +# Normalize your levels between 0 and 1 for colormap mapping +min_level = min(levels) +max_level = max(levels) +normalized_positions = [(lv - min_level) / (max_level - min_level) for lv in levels] + +# Normalize RGB values +normalized_colors = [(r / 255, g / 255, b / 255) for r, g, b in rgb_list] + +# Create the colormap using LinearSegmentedColormap +wind_cmap = LinearSegmentedColormap.from_list( + "custom_map", list(zip(normalized_positions, normalized_colors)) +) + +levels = [ + -18.0, + -16.0, + -14.0, + -12.0, + -10.0, + -8.0, + -6.0, + -4.0, + -2.0, + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0, + 22.0, + 24.0, + 26.0, + 28.0, + 30.0, + 32.0, + 34.0, + 36.0, + 38.0, +] + +rgb_list = [ + (109, 227, 255), + (175, 240, 255), + (255, 196, 226), + (255, 153, 204), + (255, 0, 255), + (128, 0, 128), + (0, 0, 128), + (70, 70, 255), + (51, 102, 255), + (133, 162, 255), + (255, 255, 255), + (204, 204, 204), + (179, 179, 179), + (153, 153, 153), + (96, 96, 96), + (128, 128, 0), + (0, 92, 0), + (0, 128, 0), + (51, 153, 102), + (157, 213, 0), + (212, 255, 91), + (255, 255, 0), + (255, 184, 112), + (255, 153, 0), + (255, 102, 0), + (255, 0, 0), + (188, 75, 0), + (171, 0, 56), + (128, 0, 0), + (163, 112, 255), +] + +# Normalize your levels between 0 and 1 for colormap mapping +min_level = min(levels) +max_level = max(levels) +normalized_positions = [(lv - min_level) / (max_level - min_level) for lv in levels] + +# Normalize RGB values +normalized_colors = [(r / 255, g / 255, b / 255) for r, g, b in rgb_list] + +# Create the colormap using LinearSegmentedColormap +t2m_cmap = LinearSegmentedColormap.from_list( + "custom_map", list(zip(normalized_positions, normalized_colors)) +) + +levels = [30, 45, 60, 75, 90, 95] +rgb_list = [ + (251, 155, 52), + (253, 206, 102), + (254, 255, 153), + (206, 254, 154), + (120, 240, 116), + (55, 202, 51), + (54, 177, 52), +] + +# Normalize your levels between 0 and 1 for colormap mapping +min_level = min(levels) +max_level = max(levels) +normalized_positions = [(lv - min_level) / (max_level - min_level) for lv in levels] + +# Normalize RGB values +normalized_colors = [(r / 255, g / 255, b / 255) for r, g, b in rgb_list] + +# Create the colormap using LinearSegmentedColormap +qv_cmap = LinearSegmentedColormap.from_list( + "custom_map", list(zip(normalized_positions, normalized_colors)) +) + +FIELD_DEFAULTS = { + "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, + "2d": {"cmap": plt.get_cmap("inferno", 11), "vmin": 240, "vmax": 300}, + "2t": {"cmap": t2m_cmap, "vmin": -18, "vmax": 38}, + "10v": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 40}, + "10u": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, + "uv": {"cmap": wind_cmap, "vmin": 0, "vmax": 40}, + "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, + "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, + "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, + "q_925": {"cmap": qv_cmap, "vmin": 0, "vmax": 0.0125}, +} +"""Mapping of field names to good default plotting parameters.""" diff --git a/workflow/scripts/src/compat.py b/workflow/scripts/src/compat.py new file mode 100644 index 0000000..f3dac74 --- /dev/null +++ b/workflow/scripts/src/compat.py @@ -0,0 +1,21 @@ +from datetime import datetime +from pathlib import Path + +import numpy as np + + +def load_state_from_raw(file: Path, paramlist: list[str] | None = None) -> dict[str, np.ndarray | dict[str, np.ndarray]]: + _state: dict[str, np.ndarray] = np.load(file, allow_pickle=True) + reftime = datetime.strptime(file.parents[1].name, "%Y%m%d%H%M") + validtime = datetime.strptime(file.stem, "%Y%m%d%H%M%S") + state = {} + state["longitudes"] = _state["longitudes"] + state["latitudes"] = _state["latitudes"] + state["valid_time"] = validtime + state["lead_time"] = state["valid_time"] - reftime + state["forecast_reference_time"] = reftime + state["fields"] = {} + for key, value in _state.items(): + if key.startswith("field_"): + state["fields"][key.removeprefix("field_")] = value + return state diff --git a/workflow/scripts/src/plotting.py b/workflow/scripts/src/plotting.py new file mode 100644 index 0000000..aef90d8 --- /dev/null +++ b/workflow/scripts/src/plotting.py @@ -0,0 +1,177 @@ +import typing as tp +from pathlib import Path +from functools import cached_property + +import cartopy.crs as ccrs +from cartopy.mpl.geoaxes import GeoAxes +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.tri import Triangulation + +State = dict[str, np.ndarray | dict[str, np.ndarray]] + +PROJECTIONS: dict[str, ccrs.Projection] = { + "plate_carree": ccrs.PlateCarree(), + "orthographic": ccrs.Orthographic(central_longitude=5.0, central_latitude=45.0), +} +"""Mapping of projection names to their cartopy projection objects.""" + +REGION_EXTENTS = { + "europe": [-16.0, 25.0, 30.0, 65.0], + "central_europe": [-2.6, 19.5, 40.2, 52.3], + "switzerland": [-1.5, 17.5, 40.5, 53.0], +} +"""Mapping of region names to their extents.""" + + +class StatePlotter: + """A class to plot state fields on various map projections and regions.""" + + def __init__( + self, + lon: np.ndarray, + lat: np.ndarray, + out_dir: Path, + ): + """Initialize the StatePlotter object. + + Latitudes and longitudes are passed during initialization so that + the triangulation can be computed once and reused for all plots. + + Parameters + ---------- + lon : np.ndarray + The longitudes of the grid. + lat : np.ndarray + The latitudes of the grid. + out_dir : Path + The output directory to save the plots. + """ + + self.lon = lon + self.lat = lat + + out_dir.mkdir(exist_ok=True, parents=True) + self.out_dir = out_dir + + self.tri = Triangulation(self.lon, self.lat) + + def init_geoaxes( + self, + nrows: int = 1, + ncols: int = 1, + projection: str = "orthographic", + region: str | None = None, + coastlines: bool = True, + **kwargs, + ) -> tuple[plt.Figure, tp.Sequence[GeoAxes]]: + """Initialize a figure with GeoAxes for plotting fields. + + Parameters + ---------- + nrows : int, optional + The number of rows in the figure, by default 1. + ncols : int, optional + The number of columns in the figure, by default 1. + projection : str, optional + The projection of the map, by default "orthographic". + region : str, optional + The region to plot, by default None. + coastlines : bool, optional + Whether to plot coastlines, by default True. + + Returns + ------- + tuple[plt.Figure, tp.Sequence[GeoAxes]] + The figure and the GeoAxes objects. + """ + + proj = PROJECTIONS.get(projection, PROJECTIONS["orthographic"]) + fig, ax = plt.subplots(nrows, ncols, subplot_kw={"projection": proj}) + ax: GeoAxes | tp.Sequence[GeoAxes] = ( + [ax] if nrows == 1 and ncols == 1 else ax.ravel() + ) + + for i in range(nrows * ncols): + ax[i].set_global() + if coastlines: + ax[i].coastlines() + + if region != "globe": + ax[i].set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) + + return fig, ax + + def plot_field( + self, + ax: GeoAxes, + field: np.ndarray, + region: str | None = None, + validtime: str = "", + colorbar: dict | bool = True, + **kwargs, + ): + """Plot a field on a GeoAxes object. + + Parameters + ---------- + ax : GeoAxes + The GeoAxes object to plot on. + field : np.ndarray + The field to plot. + region : str, optional + The region to plot, by default None. + colorbar : dict | bool, optional + Whether to plot a colorbar, by default True. + If a dictionary, it is passed as keyword arguments to plt.colorbar. + kwargs : dict + Additional keyword arguments to pass to ax.tripcolor, including cmap, + vmin, vmax, etc. + """ + + proj = ax.projection + + if proj == PROJECTIONS["orthographic"]: + triang, mask = self._ortographic_tri + else: + triang, mask = self.tri, slice(None, None) + + # TODO: this is hardcoded for when the initial state has two timesteps + # need to ditch this later + field = field[-1] if field.ndim == 2 else field.squeeze() + + im = ax.tripcolor(triang, field[mask], **kwargs) + ax.text( + 0.05, + 0.95, + f"Time: {validtime}", + transform=ax.transAxes, + fontsize=12, + color="white", + verticalalignment="top", + bbox=dict(facecolor="black", alpha=0.5), + ) + + if region and region != "globe": + ax.set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) + + if colorbar: + colorbar = { + "orientation": "horizontal", + "pad": 0.04, + "aspect": 45, + "extend": "both", + "shrink": 0.75, + } | (colorbar if isinstance(colorbar, dict) else {}) + plt.colorbar(im, **colorbar) + + @cached_property + def _ortographic_tri(self) -> Triangulation: + """Compute the triangulation for the orthographic projection.""" + x, y, _ = ( + PROJECTIONS["orthographic"] + .transform_points(ccrs.PlateCarree(), self.lon, self.lat) + .T + ) + mask = ~(np.isnan(x) | np.isnan(y)) + return Triangulation(x[mask], y[mask]), mask From 82e0a2c34a2c2e3274dfe988c3f59ccf310582c1 Mon Sep 17 00:00:00 2001 From: Carlos Osuna Date: Mon, 22 Sep 2025 21:13:23 +0200 Subject: [PATCH 07/35] adding plotting files --- workflow/scripts/plot_map.py | 195 +++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 workflow/scripts/plot_map.py diff --git a/workflow/scripts/plot_map.py b/workflow/scripts/plot_map.py new file mode 100644 index 0000000..4c18b73 --- /dev/null +++ b/workflow/scripts/plot_map.py @@ -0,0 +1,195 @@ +from pathlib import Path + +import numpy as np +import matplotlib.pyplot as plt + +State = dict[str, np.ndarray | dict[str, np.ndarray]] + +from argparse import ArgumentParser, Namespace +from functools import partial +import logging +import os +from pathlib import Path +from concurrent.futures import ProcessPoolExecutor + +import matplotlib.pyplot as plt +import numpy as np + +from src.plotting import StatePlotter +from src.calc import process_augment_state +from src.compat import load_state_from_raw +from src.cmaps import FIELD_DEFAULTS + +State = dict[str, np.ndarray | dict[str, np.ndarray]] + +LOG = logging.getLogger(__name__) +LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" +logging.basicConfig(level=logging.INFO, format=LOG_FMT) + + +def plot_state( + plotter: StatePlotter, + pred_state: State, + paramlist: list[str], + projection: str = "orthographic", + region: str = "europe", +) -> None: + + for param in paramlist: + + # # plot individual fields + fig, [gax] = plotter.init_geoaxes( + projection=projection, region=region, coastlines=True, zorder=2 + ) + fig.set_size_inches(10, 10) + + if param == "uv": + field = np.sqrt( + pred_state["fields"]["10u"] ** 2 + pred_state["fields"]["10v"] ** 2 + ) + elif param == "2t": + field = pred_state["fields"][param] - 273.15 + else: + field = pred_state["fields"][param] + + plotter.plot_field( + ax=gax, + field=field, + zorder=1, + validtime=pred_state["valid_time"].strftime("%Y%m%d%H%M"), + **FIELD_DEFAULTS[param], + ) + + validtime = pred_state["valid_time"].strftime("%Y%m%d%H%M") + leadtime = int(pred_state["lead_time"].total_seconds() // 3600) + fn = f"{validtime}_{leadtime:03}_{param}_{projection}_{region}.png" + plt.savefig(plotter.out_dir / fn, bbox_inches="tight", dpi=400) + plt.clf() + plt.cla() + plt.close() + + +def process_plot_leadtime( + file: Path, + # dataset: Dataset, + paramlist: list[str], + plots_dir: Path, +): + LOG.info(f"Started plotting {file.name}...") + + LOG.info(f"Loading predicted and true states") + pred_state = load_state_from_raw(file) + print(pred_state.keys()) + + LOG.info(f"Augmenting states") + pred_state = process_augment_state(pred_state) + + LOG.info(f"Initializing plotter") + plotter = StatePlotter( + pred_state["longitudes"], + pred_state["latitudes"], + plots_dir, + ) + + for region in ["europe", "globe", "switzerland"]: + for proj in ["orthographic"]: + plot_state( + plotter, + pred_state, + paramlist, + projection=proj, + region=region, + ) + LOG.info(f"Done plotting {file}.") + logging.basicConfig(level=logging.INFO, format=LOG_FMT) + return 0 + + +def create_animation( + plot_dir: Path, + param: str, + projection: str, + region: str, + name_prefix: str | None = None, +) -> None: + + out_dir = plot_dir / "animations" + out_dir.mkdir(exist_ok=True, parents=True) + name_prefix = f"{name_prefix}_" if name_prefix else "" + cmd = f"convert -delay 80 -loop 0" + + # # animations of prediction plots + plots_glob = f"{plot_dir}/*{param}_{projection}_{region}.png" + gif_fn = f"{out_dir}/{name_prefix}{param}_{projection}_{region}.gif" + print("CMD", plots_glob) + os.system(f"{cmd} {plots_glob} {gif_fn}") + + +class ScriptConfig(Namespace): + checkpoint_run_id: Path + model_name: None | str + # date: datetime + raw_dir: Path + paramlist: list[str] + out_dir: Path + + +def main(cfg: ScriptConfig) -> None: + + LOG.info(f"Plotting inference results for {cfg}") + # set up output directory + + out_dir = Path(cfg.out_dir) + print(out_dir) + plots_dir = Path(out_dir) + + raw_dir = Path(cfg.raw_dir) + raw_files = sorted(raw_dir.glob(f"*.npz")) + _map_fn = partial( + process_plot_leadtime, + paramlist=cfg.paramlist, + plots_dir=plots_dir, + ) + + with ProcessPoolExecutor(max_workers=len(raw_files)) as executor: + results = executor.map(_map_fn, raw_files) + + # wait for all processes to finish + for _ in results: + pass + + LOG.info(f"Creating animations") + for param in cfg.paramlist: + for region in ["europe", "globe", "switzerland"]: + for proj in ["orthographic"]: + create_animation( + plots_dir, + param, + proj, + region, + name_prefix=cfg.model_name, + ) + + +if __name__ == "__main__": + + ROOT = Path(__file__).parent + OUT_DIR = ROOT / "output" + + parser = ArgumentParser() + + parser.add_argument("--input", type=str, default=None, help="Directory to raw data") + parser.add_argument("--date", type=str, default=None, help="reference datetime") + parser.add_argument("--output", type=str, help="output directory") + + args = parser.parse_args() + + config = ScriptConfig( + checkpoint_run_id="", + model_name="icon", + raw_dir=args.input, + paramlist=["10u", "2t"], + out_dir=args.output, + ) + + main(config) From 0b294295195bacfe3fe8453ebd480cf4c2bf954e Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:15:17 +0200 Subject: [PATCH 08/35] generate colormaps from NCL color table files --- resources/report/plotting/README | 2 + resources/report/plotting/RH_6lev.ct | 11 ++ resources/report/plotting/t2m_29lev.ct | 34 +++++ resources/report/plotting/uv_17lev.ct | 23 ++++ workflow/scripts/plot_map.py | 8 +- workflow/scripts/src/cmaps.py | 176 ------------------------- workflow/scripts/src/colormaps.py | 71 ++++++++++ 7 files changed, 145 insertions(+), 180 deletions(-) create mode 100644 resources/report/plotting/README create mode 100644 resources/report/plotting/RH_6lev.ct create mode 100644 resources/report/plotting/t2m_29lev.ct create mode 100644 resources/report/plotting/uv_17lev.ct delete mode 100644 workflow/scripts/src/cmaps.py create mode 100644 workflow/scripts/src/colormaps.py diff --git a/resources/report/plotting/README b/resources/report/plotting/README new file mode 100644 index 0000000..a60dfbe --- /dev/null +++ b/resources/report/plotting/README @@ -0,0 +1,2 @@ +# This is a copy of the ncl colortables developed in the comsolib +# SRC: https://github.com/MeteoSwiss-APN/mch-ncl/tree/master/cosmolib/ct \ No newline at end of file diff --git a/resources/report/plotting/RH_6lev.ct b/resources/report/plotting/RH_6lev.ct new file mode 100644 index 0000000..1adc5a1 --- /dev/null +++ b/resources/report/plotting/RH_6lev.ct @@ -0,0 +1,11 @@ +; Type: COSMO Library Color Table +; Description: Color table for plotting the relative humidity in % as in NinJo +6 +30 45 60 75 90 95 +251 155 52 +253 206 102 +254 255 153 +206 254 154 +120 240 116 + 55 202 51 + 54 177 52 \ No newline at end of file diff --git a/resources/report/plotting/t2m_29lev.ct b/resources/report/plotting/t2m_29lev.ct new file mode 100644 index 0000000..7f52605 --- /dev/null +++ b/resources/report/plotting/t2m_29lev.ct @@ -0,0 +1,34 @@ +; Type: COSMO Library Color Table +; Description: Color table for 2m temperature as in model browser +29 +-18.0 -16.0 -14.0 -12.0 -10.0 -8.0 -6.0 -4.0 -2.0 0.0 2.0 4.0 6.0 8.0 10.0 12.0 14.0 16.0 18.0 20.0 22.0 24.0 26.0 28.0 30.0 32.0 34.0 36.0 38.0 +109 227 255 +175 240 255 +255 196 226 +255 153 204 +255 0 255 +128 0 128 + 0 0 128 + 70 70 255 + 51 102 255 +133 162 255 +255 255 255 +204 204 204 +179 179 179 +153 153 153 + 96 96 96 +128 128 0 + 0 92 0 + 0 128 0 + 51 153 102 +157 213 0 +212 255 91 +255 255 0 +255 184 112 +255 153 0 +255 102 0 +255 0 0 +188 75 0 +171 0 56 +128 0 0 +163 112 255 \ No newline at end of file diff --git a/resources/report/plotting/uv_17lev.ct b/resources/report/plotting/uv_17lev.ct new file mode 100644 index 0000000..769d11c --- /dev/null +++ b/resources/report/plotting/uv_17lev.ct @@ -0,0 +1,23 @@ +; Type: COSMO Library Color Table +; Description: Wind speed representation in 17 levels. This color table +; is used in the model browser to plot wind at 10m, 850, 700 and 500hPa +17 +4.0 6.0 10.0 14.0 18.0 22.0 26.0 30.0 35.0 40.0 45.0 50.0 60.0 70.0 80.0 90.0 100.0 +255 255 255 +239 244 209 +232 244 158 +170 206 99 +226 237 22 +255 237 0 +255 237 130 +244 209 127 +237 165 73 +229 140 61 +219 124 61 +239 7 61 +232 86 163 +155 112 168 + 99 112 247 +127 150 255 +142 178 255 +181 201 255 \ No newline at end of file diff --git a/workflow/scripts/plot_map.py b/workflow/scripts/plot_map.py index 4c18b73..72ec21f 100644 --- a/workflow/scripts/plot_map.py +++ b/workflow/scripts/plot_map.py @@ -16,9 +16,9 @@ import numpy as np from src.plotting import StatePlotter -from src.calc import process_augment_state +#from src.calc import process_augment_state from src.compat import load_state_from_raw -from src.cmaps import FIELD_DEFAULTS +from src.colormaps import CMAP_DEFAULTS State = dict[str, np.ndarray | dict[str, np.ndarray]] @@ -57,7 +57,7 @@ def plot_state( field=field, zorder=1, validtime=pred_state["valid_time"].strftime("%Y%m%d%H%M"), - **FIELD_DEFAULTS[param], + **CMAP_DEFAULTS[param], ) validtime = pred_state["valid_time"].strftime("%Y%m%d%H%M") @@ -82,7 +82,7 @@ def process_plot_leadtime( print(pred_state.keys()) LOG.info(f"Augmenting states") - pred_state = process_augment_state(pred_state) + #pred_state = process_augment_state(pred_state) LOG.info(f"Initializing plotter") plotter = StatePlotter( diff --git a/workflow/scripts/src/cmaps.py b/workflow/scripts/src/cmaps.py deleted file mode 100644 index bc74054..0000000 --- a/workflow/scripts/src/cmaps.py +++ /dev/null @@ -1,176 +0,0 @@ -import matplotlib.pyplot as plt -from matplotlib.colors import LinearSegmentedColormap - -# WInd RGB values -rgb_list = [ - (255, 255, 255), - (239, 244, 209), - (232, 244, 158), - (170, 206, 99), - (226, 237, 22), - (255, 237, 0), - (255, 237, 130), - (244, 209, 127), - (237, 165, 73), - (229, 140, 61), - (219, 124, 61), - (239, 7, 61), - (232, 86, 163), - (155, 112, 168), - (99, 112, 247), - (127, 150, 255), - (142, 178, 255), - (181, 201, 255), -] - - -levels = [ - 4.0, - 6.0, - 10.0, - 14.0, - 18.0, - 22.0, - 26.0, - 30.0, - 35.0, - 40.0, - 45.0, - 50.0, - 60.0, - 70.0, - 80.0, - 90.0, - 100.0, -] - -# levels = [(lv / 100.0) * 75.0 for lv in levels] - -# Normalize your levels between 0 and 1 for colormap mapping -min_level = min(levels) -max_level = max(levels) -normalized_positions = [(lv - min_level) / (max_level - min_level) for lv in levels] - -# Normalize RGB values -normalized_colors = [(r / 255, g / 255, b / 255) for r, g, b in rgb_list] - -# Create the colormap using LinearSegmentedColormap -wind_cmap = LinearSegmentedColormap.from_list( - "custom_map", list(zip(normalized_positions, normalized_colors)) -) - -levels = [ - -18.0, - -16.0, - -14.0, - -12.0, - -10.0, - -8.0, - -6.0, - -4.0, - -2.0, - 0.0, - 2.0, - 4.0, - 6.0, - 8.0, - 10.0, - 12.0, - 14.0, - 16.0, - 18.0, - 20.0, - 22.0, - 24.0, - 26.0, - 28.0, - 30.0, - 32.0, - 34.0, - 36.0, - 38.0, -] - -rgb_list = [ - (109, 227, 255), - (175, 240, 255), - (255, 196, 226), - (255, 153, 204), - (255, 0, 255), - (128, 0, 128), - (0, 0, 128), - (70, 70, 255), - (51, 102, 255), - (133, 162, 255), - (255, 255, 255), - (204, 204, 204), - (179, 179, 179), - (153, 153, 153), - (96, 96, 96), - (128, 128, 0), - (0, 92, 0), - (0, 128, 0), - (51, 153, 102), - (157, 213, 0), - (212, 255, 91), - (255, 255, 0), - (255, 184, 112), - (255, 153, 0), - (255, 102, 0), - (255, 0, 0), - (188, 75, 0), - (171, 0, 56), - (128, 0, 0), - (163, 112, 255), -] - -# Normalize your levels between 0 and 1 for colormap mapping -min_level = min(levels) -max_level = max(levels) -normalized_positions = [(lv - min_level) / (max_level - min_level) for lv in levels] - -# Normalize RGB values -normalized_colors = [(r / 255, g / 255, b / 255) for r, g, b in rgb_list] - -# Create the colormap using LinearSegmentedColormap -t2m_cmap = LinearSegmentedColormap.from_list( - "custom_map", list(zip(normalized_positions, normalized_colors)) -) - -levels = [30, 45, 60, 75, 90, 95] -rgb_list = [ - (251, 155, 52), - (253, 206, 102), - (254, 255, 153), - (206, 254, 154), - (120, 240, 116), - (55, 202, 51), - (54, 177, 52), -] - -# Normalize your levels between 0 and 1 for colormap mapping -min_level = min(levels) -max_level = max(levels) -normalized_positions = [(lv - min_level) / (max_level - min_level) for lv in levels] - -# Normalize RGB values -normalized_colors = [(r / 255, g / 255, b / 255) for r, g, b in rgb_list] - -# Create the colormap using LinearSegmentedColormap -qv_cmap = LinearSegmentedColormap.from_list( - "custom_map", list(zip(normalized_positions, normalized_colors)) -) - -FIELD_DEFAULTS = { - "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, - "2d": {"cmap": plt.get_cmap("inferno", 11), "vmin": 240, "vmax": 300}, - "2t": {"cmap": t2m_cmap, "vmin": -18, "vmax": 38}, - "10v": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 40}, - "10u": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, - "uv": {"cmap": wind_cmap, "vmin": 0, "vmax": 40}, - "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, - "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, - "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, - "q_925": {"cmap": qv_cmap, "vmin": 0, "vmax": 0.0125}, -} -"""Mapping of field names to good default plotting parameters.""" diff --git a/workflow/scripts/src/colormaps.py b/workflow/scripts/src/colormaps.py new file mode 100644 index 0000000..df4c65c --- /dev/null +++ b/workflow/scripts/src/colormaps.py @@ -0,0 +1,71 @@ +import pathlib +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import ListedColormap, BoundaryNorm + +# Base directory for colormap files +BASE_DIR = pathlib.Path(__file__).resolve().parents[3] / "resources" / "report" / "plotting" + + +def load_ncl_colormap(filename): + """Load colormap file into a matplotlib ListedColormap and BoundaryNorm. + + Returns + ------- + dict + Dictionary containing the colormap and normalisation generated from the + colormap file + {cmap : matplotlib.colors.ListedColormap, + norm : matplotlib.colors.BoundaryNorm } + """ + cmap_path = BASE_DIR / filename + if not cmap_path.exists(): + raise FileNotFoundError(f"Colormap file not found: {cmap_path}") + with open(cmap_path, "r") as f: + lines = f.readlines() + + # Remove header + lines = [l.strip() for l in lines if l.strip() and not l.strip().startswith(";")] + + # Number of levels on first line + try: + n_levs = int(lines[0]) + except ValueError: + raise ValueError(f"Expected number of levels in first non-header line of {cmap_path}") + + # Colormap bounds on second line + bounds = [float(x) for x in lines[1].split()] + if len(bounds) != n_levs: + raise ValueError( + f"Bounds must have {n_levs} values, got {len(bounds)}" + ) + + # RGB values + rgb_lines = lines[2:] + rgb = np.array([[int(x) for x in line.split()] for line in rgb_lines], dtype=float) + rgb /= 255.0 # scale to [0,1] for matplotlib + if len(rgb) != n_levs + 1: + raise ValueError(f"Expected {n_levs} RGB rows, got {len(rgb)}.") + + # Create colormap and norm + cmap = ListedColormap(colors=rgb[1:-1], name=pathlib.Path(filename).stem) + cmap.set_under(rgb[0]) + cmap.set_over(rgb[-1]) + norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N) + + return {"cmap": cmap, "norm": norm} + + +CMAP_DEFAULTS = { + "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, + "2d": load_ncl_colormap("t2m_29lev.ct"), + "2t": load_ncl_colormap("t2m_29lev.ct"), + "10v": load_ncl_colormap("uv_17lev.ct"), + "10u": load_ncl_colormap("uv_17lev.ct"), + "uv": load_ncl_colormap("uv_17lev.ct"), + "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, + "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, + "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, + "q_925": load_ncl_colormap("RH_6lev.ct"), +} +"""Mapping of field names to good default plotting parameters.""" From bc34ec06ea8ad527a956ac04b3dffccf93cef77c Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:08:42 +0200 Subject: [PATCH 09/35] split colormap configs for variables and colormap loader --- workflow/scripts/plot_map.py | 5 +++-- workflow/scripts/src/colormap_defaults.py | 17 +++++++++++++++++ .../src/{colormaps.py => colormap_loader.py} | 16 ---------------- 3 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 workflow/scripts/src/colormap_defaults.py rename workflow/scripts/src/{colormaps.py => colormap_loader.py} (72%) diff --git a/workflow/scripts/plot_map.py b/workflow/scripts/plot_map.py index 72ec21f..5f45154 100644 --- a/workflow/scripts/plot_map.py +++ b/workflow/scripts/plot_map.py @@ -18,7 +18,7 @@ from src.plotting import StatePlotter #from src.calc import process_augment_state from src.compat import load_state_from_raw -from src.colormaps import CMAP_DEFAULTS +from src.colormap_defaults import CMAP_DEFAULTS State = dict[str, np.ndarray | dict[str, np.ndarray]] @@ -91,7 +91,8 @@ def process_plot_leadtime( plots_dir, ) - for region in ["europe", "globe", "switzerland"]: + #for region in ["europe", "globe", "switzerland"]: + for region in ["switzerland"]: for proj in ["orthographic"]: plot_state( plotter, diff --git a/workflow/scripts/src/colormap_defaults.py b/workflow/scripts/src/colormap_defaults.py new file mode 100644 index 0000000..ae17486 --- /dev/null +++ b/workflow/scripts/src/colormap_defaults.py @@ -0,0 +1,17 @@ +"""Mapping of field names to good default plotting parameters.""" +from matplotlib import pyplot as plt +from colormap_loader import load_ncl_colormap + + +CMAP_DEFAULTS = { + "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, + "2d": load_ncl_colormap("t2m_29lev.ct"), + "2t": load_ncl_colormap("t2m_29lev.ct"), + "10v": load_ncl_colormap("uv_17lev.ct"), + "10u": load_ncl_colormap("uv_17lev.ct"), + "uv": load_ncl_colormap("uv_17lev.ct"), + "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, + "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, + "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, + "q_925": load_ncl_colormap("RH_6lev.ct"), +} diff --git a/workflow/scripts/src/colormaps.py b/workflow/scripts/src/colormap_loader.py similarity index 72% rename from workflow/scripts/src/colormaps.py rename to workflow/scripts/src/colormap_loader.py index df4c65c..e4866b3 100644 --- a/workflow/scripts/src/colormaps.py +++ b/workflow/scripts/src/colormap_loader.py @@ -1,6 +1,5 @@ import pathlib import numpy as np -import matplotlib.pyplot as plt from matplotlib.colors import ListedColormap, BoundaryNorm # Base directory for colormap files @@ -54,18 +53,3 @@ def load_ncl_colormap(filename): norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N) return {"cmap": cmap, "norm": norm} - - -CMAP_DEFAULTS = { - "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, - "2d": load_ncl_colormap("t2m_29lev.ct"), - "2t": load_ncl_colormap("t2m_29lev.ct"), - "10v": load_ncl_colormap("uv_17lev.ct"), - "10u": load_ncl_colormap("uv_17lev.ct"), - "uv": load_ncl_colormap("uv_17lev.ct"), - "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, - "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, - "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, - "q_925": load_ncl_colormap("RH_6lev.ct"), -} -"""Mapping of field names to good default plotting parameters.""" From 6cb8bc0a7e2a4b06a2ed64d0fd1d960ec9498895 Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:09:37 +0200 Subject: [PATCH 10/35] add tests for colormap_loader.py and colormap_defaults.py --- workflow/scripts/tests/unit/test_colormaps.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 workflow/scripts/tests/unit/test_colormaps.py diff --git a/workflow/scripts/tests/unit/test_colormaps.py b/workflow/scripts/tests/unit/test_colormaps.py new file mode 100644 index 0000000..3f73a2a --- /dev/null +++ b/workflow/scripts/tests/unit/test_colormaps.py @@ -0,0 +1,89 @@ +import pathlib +import numpy as np +import pytest +import sys +from matplotlib import pyplot as plt +from matplotlib.colors import ListedColormap, BoundaryNorm + +# add top-level "src" to sys.path +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT / "src")) + +import colormap_loader, colormap_defaults + + +def test_load_valid_colormap(monkeypatch, tmp_path): + file = tmp_path / "test_colormap.ct" + file.write_text("; test colormap\n3\n0 1 2\n10 20 30\n40 50 60\n70 80 90\n100 110 120\n") + monkeypatch.setattr(colormap_loader, "BASE_DIR", tmp_path) + + result = colormap_loader.load_ncl_colormap("test_colormap.ct") + assert isinstance(result["cmap"], ListedColormap) + assert isinstance(result["norm"], BoundaryNorm) + + cmap = result["cmap"] + norm = result["norm"] + + # cmap has n_levs-1 colors inside + assert cmap.N == 2 + # name is stem + assert cmap.name == "test_colormap" + # cmap colors + assert np.allclose(cmap(0), (40/255, 50/255, 60/255, 1.0)) + assert np.allclose(cmap(1), (70/255, 80/255, 90/255, 1.0)) + # under/over colors + assert np.allclose(cmap(-9999), (10/255, 20/255, 30/255, 1.0)) + assert np.allclose(cmap(9999), (100/255, 110/255, 120/255, 1.0)) + # bounds + assert np.allclose(norm.boundaries, [0, 1, 2]) + + +def test_missing_file_raises(monkeypatch, tmp_path): + monkeypatch.setattr(colormap_loader, "BASE_DIR", tmp_path) + with pytest.raises(FileNotFoundError): + colormap_loader.load_ncl_colormap("does_not_exist.ct") + + +def test_invalid_first_line(monkeypatch, tmp_path): + file = tmp_path / "bad.ct" + file.write_text("not_a_number\n") + monkeypatch.setattr(colormap_loader, "BASE_DIR", tmp_path) + with pytest.raises(ValueError): + colormap_loader.load_ncl_colormap("bad.ct") + + +def test_wrong_bounds(monkeypatch, tmp_path): + file = tmp_path / "bad_bounds.ct" + file.write_text("2\n1 2 3\n10 20 30\n40 50 60\n") + monkeypatch.setattr(colormap_loader, "BASE_DIR", tmp_path) + with pytest.raises(ValueError): + colormap_loader.load_ncl_colormap("bad_bounds.ct") + + +def test_wrong_rgb_count(monkeypatch, tmp_path): + file = tmp_path / "bad_rgb.ct" + file.write_text("2\n1 2\n10 20 30\n") + monkeypatch.setattr(colormap_loader, "BASE_DIR", tmp_path) + with pytest.raises(ValueError): + colormap_loader.load_ncl_colormap("bad_rgb.ct") + + +@pytest.mark.parametrize("field, var", colormap_defaults.CMAP_DEFAULTS.items()) +def test_cmap_defaults_smoke(field, var): + """Smoke test: can we use every entry in CMAP_DEFAULTS to plot data?""" + cmap = var["cmap"] + norm = var.get("norm", None) + vmin = var.get("vmin", None) + vmax = var.get("vmax", None) + + # make some synthetic data + data = np.linspace(0, 1, 100).reshape(10, 10) + + # just try plotting + fig, ax = plt.subplots() + if norm is not None: + ax.imshow(data, cmap=cmap, norm=norm) + else: + ax.imshow(data, cmap=cmap, vmin=vmin, vmax=vmax) + + plt.close(fig) From 9ab55d8c48871e705e599de2952a4e539792bec8 Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:12:07 +0200 Subject: [PATCH 11/35] revert change commited accidentally --- workflow/scripts/plot_map.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workflow/scripts/plot_map.py b/workflow/scripts/plot_map.py index 5f45154..5eab034 100644 --- a/workflow/scripts/plot_map.py +++ b/workflow/scripts/plot_map.py @@ -91,8 +91,7 @@ def process_plot_leadtime( plots_dir, ) - #for region in ["europe", "globe", "switzerland"]: - for region in ["switzerland"]: + for region in ["europe", "globe", "switzerland"]: for proj in ["orthographic"]: plot_state( plotter, From 12ed14cd26c877de415d54a816ff40f1e8077f75 Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:13:42 +0200 Subject: [PATCH 12/35] fix typo --- resources/report/plotting/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/report/plotting/README b/resources/report/plotting/README index a60dfbe..b6a0a25 100644 --- a/resources/report/plotting/README +++ b/resources/report/plotting/README @@ -1,2 +1,2 @@ -# This is a copy of the ncl colortables developed in the comsolib +# This is a copy of the ncl colortables developed in the cosmolib # SRC: https://github.com/MeteoSwiss-APN/mch-ncl/tree/master/cosmolib/ct \ No newline at end of file From 89b3ae8ede76c37911378a6c317543e0251ac80e Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:47:45 +0200 Subject: [PATCH 13/35] fix imports for code and tests to fit the project's structure --- README.md | 7 +++++++ workflow/scripts/src/colormap_defaults.py | 2 +- workflow/scripts/tests/unit/test_colormaps.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b01a35a..bd71b3b 100644 --- a/README.md +++ b/README.md @@ -120,3 +120,10 @@ ln -s $SCRATCH/evalenv/output output This way data will be written to your scratch, but you will still be able to browse it with your IDE. If you are using VSCode, we advise that you install the YAML extension, which will enable config validation, autocompletion, hovering support, and more. + +## Hints for developers + +To run pytest for the code in `workflow/scripts/` call: +``` +PYTHONPATH=workflow/scripts pytest -s +``` \ No newline at end of file diff --git a/workflow/scripts/src/colormap_defaults.py b/workflow/scripts/src/colormap_defaults.py index ae17486..d3c4739 100644 --- a/workflow/scripts/src/colormap_defaults.py +++ b/workflow/scripts/src/colormap_defaults.py @@ -1,6 +1,6 @@ """Mapping of field names to good default plotting parameters.""" from matplotlib import pyplot as plt -from colormap_loader import load_ncl_colormap +from .colormap_loader import load_ncl_colormap CMAP_DEFAULTS = { diff --git a/workflow/scripts/tests/unit/test_colormaps.py b/workflow/scripts/tests/unit/test_colormaps.py index 3f73a2a..114e38a 100644 --- a/workflow/scripts/tests/unit/test_colormaps.py +++ b/workflow/scripts/tests/unit/test_colormaps.py @@ -9,7 +9,7 @@ REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO_ROOT / "src")) -import colormap_loader, colormap_defaults +from src import colormap_loader, colormap_defaults def test_load_valid_colormap(monkeypatch, tmp_path): From 87596a1c84b257dad86de7d9a5bc9121c669c988 Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:31:31 +0200 Subject: [PATCH 14/35] adapt plotting to earthkit-plots --- pyproject.toml | 3 +- workflow/scripts/plot_map.py | 100 ++++++--- workflow/scripts/src/colormap_defaults.py | 15 +- workflow/scripts/src/colormap_loader.py | 2 +- workflow/scripts/src/plotting.py | 248 ++++++++++++++++------ 5 files changed, 276 insertions(+), 92 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ffd473..2456272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "pydantic>=2.11.7", "toml>=0.10.2", "netcdf4>=1.7.2", - "cartopy" + "cartopy", + "earthkit-plots", ] [project.optional-dependencies] diff --git a/workflow/scripts/plot_map.py b/workflow/scripts/plot_map.py index 5eab034..6db4d09 100644 --- a/workflow/scripts/plot_map.py +++ b/workflow/scripts/plot_map.py @@ -1,19 +1,12 @@ -from pathlib import Path - -import numpy as np -import matplotlib.pyplot as plt - -State = dict[str, np.ndarray | dict[str, np.ndarray]] - from argparse import ArgumentParser, Namespace from functools import partial import logging +import numpy as np import os from pathlib import Path from concurrent.futures import ProcessPoolExecutor -import matplotlib.pyplot as plt -import numpy as np +import earthkit.plots as ekp from src.plotting import StatePlotter #from src.calc import process_augment_state @@ -37,11 +30,11 @@ def plot_state( for param in paramlist: - # # plot individual fields - fig, [gax] = plotter.init_geoaxes( - projection=projection, region=region, coastlines=True, zorder=2 + # plot individual fields + fig = plotter.init_geoaxes( + nrows=1, ncols=1, projection=projection, region=region, size=(8,8), ) - fig.set_size_inches(10, 10) + subplot = fig.add_map(row=0, column=0) if param == "uv": field = np.sqrt( @@ -53,20 +46,60 @@ def plot_state( field = pred_state["fields"][param] plotter.plot_field( - ax=gax, - field=field, - zorder=1, - validtime=pred_state["valid_time"].strftime("%Y%m%d%H%M"), - **CMAP_DEFAULTS[param], + subplot, + field, + **get_style(param) ) validtime = pred_state["valid_time"].strftime("%Y%m%d%H%M") leadtime = int(pred_state["lead_time"].total_seconds() // 3600) + + fig.title(f"{param}, time: {validtime}") + fn = f"{validtime}_{leadtime:03}_{param}_{projection}_{region}.png" - plt.savefig(plotter.out_dir / fn, bbox_inches="tight", dpi=400) - plt.clf() - plt.cla() - plt.close() + fig.save(plotter.out_dir / fn, bbox_inches="tight", dpi=400) + + +# def plot_state( +# plotter: StatePlotter, +# pred_state: State, +# paramlist: list[str], +# projection: str = "orthographic", +# region: str = "europe", +# ) -> None: + +# for param in paramlist: + +# # # plot individual fields +# fig, [gax] = plotter.init_geoaxes( +# projection=projection, region=region, coastlines=True, zorder=2 +# ) +# fig.set_size_inches(10, 10) + +# if param == "uv": +# field = np.sqrt( +# pred_state["fields"]["10u"] ** 2 + pred_state["fields"]["10v"] ** 2 +# ) +# elif param == "2t": +# field = pred_state["fields"][param] - 273.15 +# else: +# field = pred_state["fields"][param] + +# plotter.plot_field( +# ax=gax, +# field=field, +# zorder=1, +# validtime=pred_state["valid_time"].strftime("%Y%m%d%H%M"), +# **CMAP_DEFAULTS[param], +# ) + +# validtime = pred_state["valid_time"].strftime("%Y%m%d%H%M") +# leadtime = int(pred_state["lead_time"].total_seconds() // 3600) +# fn = f"{validtime}_{leadtime:03}_{param}_{projection}_{region}.png" +# plt.savefig(plotter.out_dir / fn, bbox_inches="tight", dpi=400) +# plt.clf() +# plt.cla() +# plt.close() def process_plot_leadtime( @@ -79,7 +112,7 @@ def process_plot_leadtime( LOG.info(f"Loading predicted and true states") pred_state = load_state_from_raw(file) - print(pred_state.keys()) + #print(pred_state["valid_time"], pred_state["fields"].keys()) LOG.info(f"Augmenting states") #pred_state = process_augment_state(pred_state) @@ -91,7 +124,7 @@ def process_plot_leadtime( plots_dir, ) - for region in ["europe", "globe", "switzerland"]: + for region in ["europe", None, "switzerland"]: for proj in ["orthographic"]: plot_state( plotter, @@ -134,6 +167,23 @@ class ScriptConfig(Namespace): out_dir: Path +def get_style(param): + """"Get style and colormap settings for the plot. + Needed because cmap/norm does not work in Style(colors=cmap), still needs + to be passed as arguments to tripcolor()/tricontourf(). + """ + cfg = CMAP_DEFAULTS[param] + return { + "style": ekp.styles.Style( + levels=cfg.get("bounds", None), + extend="both", + units=cfg.get("units", ""), + ), + "cmap": cfg["cmap"], + "norm": cfg.get("norm", None), + } + + def main(cfg: ScriptConfig) -> None: LOG.info(f"Plotting inference results for {cfg}") @@ -160,7 +210,7 @@ def main(cfg: ScriptConfig) -> None: LOG.info(f"Creating animations") for param in cfg.paramlist: - for region in ["europe", "globe", "switzerland"]: + for region in ["europe", None, "switzerland"]: for proj in ["orthographic"]: create_animation( plots_dir, diff --git a/workflow/scripts/src/colormap_defaults.py b/workflow/scripts/src/colormap_defaults.py index d3c4739..bc6541a 100644 --- a/workflow/scripts/src/colormap_defaults.py +++ b/workflow/scripts/src/colormap_defaults.py @@ -1,17 +1,24 @@ """Mapping of field names to good default plotting parameters.""" +from collections import defaultdict from matplotlib import pyplot as plt +import warnings from .colormap_loader import load_ncl_colormap +def _fallback(): + warnings.warn("No colormap found for this parameter, using fallback.", UserWarning) + return {"cmap": plt.get_cmap("viridis"), "norm": None, "units": ""} -CMAP_DEFAULTS = { +_CMAP_DEFAULTS = { "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, "2d": load_ncl_colormap("t2m_29lev.ct"), - "2t": load_ncl_colormap("t2m_29lev.ct"), - "10v": load_ncl_colormap("uv_17lev.ct"), - "10u": load_ncl_colormap("uv_17lev.ct"), + "2t": load_ncl_colormap("t2m_29lev.ct") | {"units": "degC"}, + "10v": load_ncl_colormap("uv_17lev.ct") | {"units": "m/s"}, + "10u": load_ncl_colormap("uv_17lev.ct") | {"units": "m/s"}, "uv": load_ncl_colormap("uv_17lev.ct"), "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, "q_925": load_ncl_colormap("RH_6lev.ct"), } + +CMAP_DEFAULTS = defaultdict(_fallback, _CMAP_DEFAULTS) diff --git a/workflow/scripts/src/colormap_loader.py b/workflow/scripts/src/colormap_loader.py index e4866b3..0dbf089 100644 --- a/workflow/scripts/src/colormap_loader.py +++ b/workflow/scripts/src/colormap_loader.py @@ -52,4 +52,4 @@ def load_ncl_colormap(filename): cmap.set_over(rgb[-1]) norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N) - return {"cmap": cmap, "norm": norm} + return {"cmap": cmap, "norm": norm, "bounds": bounds} diff --git a/workflow/scripts/src/plotting.py b/workflow/scripts/src/plotting.py index aef90d8..81ae427 100644 --- a/workflow/scripts/src/plotting.py +++ b/workflow/scripts/src/plotting.py @@ -1,22 +1,25 @@ -import typing as tp -from pathlib import Path -from functools import cached_property - import cartopy.crs as ccrs -from cartopy.mpl.geoaxes import GeoAxes +from functools import cached_property import numpy as np -import matplotlib.pyplot as plt from matplotlib.tri import Triangulation +import typing as tp +from pathlib import Path + +import earthkit.plots as ekp + State = dict[str, np.ndarray | dict[str, np.ndarray]] PROJECTIONS: dict[str, ccrs.Projection] = { "plate_carree": ccrs.PlateCarree(), "orthographic": ccrs.Orthographic(central_longitude=5.0, central_latitude=45.0), + # added some pojections to test the behaviour, can be deleted later + "rotated_latlon": ccrs.RotatedPole(pole_longitude=-170.0, pole_latitude=43.0), + "azimuthal_equidist": ccrs.AzimuthalEquidistant(), } """Mapping of projection names to their cartopy projection objects.""" -REGION_EXTENTS = { +REGION_EXTENTS = { # coordinate reference system: PlateCarree() "europe": [-16.0, 25.0, 30.0, 65.0], "central_europe": [-2.6, 19.5, 40.2, 52.3], "switzerland": [-1.5, 17.5, 40.5, 53.0], @@ -37,6 +40,8 @@ def __init__( Latitudes and longitudes are passed during initialization so that the triangulation can be computed once and reused for all plots. + The reference coordinate system of lon/lat is assumed to be PlateCarree() + currently. Parameters ---------- @@ -61,10 +66,9 @@ def init_geoaxes( nrows: int = 1, ncols: int = 1, projection: str = "orthographic", + size: tuple[float] | None = None, region: str | None = None, - coastlines: bool = True, - **kwargs, - ) -> tuple[plt.Figure, tp.Sequence[GeoAxes]]: + ) -> ekp.Figure: """Initialize a figure with GeoAxes for plotting fields. Parameters @@ -75,95 +79,217 @@ def init_geoaxes( The number of columns in the figure, by default 1. projection : str, optional The projection of the map, by default "orthographic". + size : tuple + size of the figure in inches region : str, optional The region to plot, by default None. - coastlines : bool, optional - Whether to plot coastlines, by default True. Returns ------- - tuple[plt.Figure, tp.Sequence[GeoAxes]] - The figure and the GeoAxes objects. + earthkit.plots.Figure + The figure object. """ proj = PROJECTIONS.get(projection, PROJECTIONS["orthographic"]) - fig, ax = plt.subplots(nrows, ncols, subplot_kw={"projection": proj}) - ax: GeoAxes | tp.Sequence[GeoAxes] = ( - [ax] if nrows == 1 and ncols == 1 else ax.ravel() + + domain = None + if region: + domain = ekp.geo.domains.Domain( + bbox=REGION_EXTENTS[region], + crs=ccrs.PlateCarree(), # coordinate reference system of the region coords + name=region + ) + + ekp_fig = ekp.Figure( + crs=proj, # coordinate reference system of the map + domain=domain, + rows=nrows, + columns=ncols, + size=size, ) + self.fig = ekp_fig + return ekp_fig + + # def init_geoaxes( + # self, + # nrows: int = 1, + # ncols: int = 1, + # projection: str = "orthographic", + # region: str | None = None, + # coastlines: bool = True, + # **kwargs, + # ) -> tuple[plt.Figure, tp.Sequence[GeoAxes]]: + # """Initialize a figure with GeoAxes for plotting fields. + + # Parameters + # ---------- + # nrows : int, optional + # The number of rows in the figure, by default 1. + # ncols : int, optional + # The number of columns in the figure, by default 1. + # projection : str, optional + # The projection of the map, by default "orthographic". + # region : str, optional + # The region to plot, by default None. + # coastlines : bool, optional + # Whether to plot coastlines, by default True. + + # Returns + # ------- + # tuple[plt.Figure, tp.Sequence[GeoAxes]] + # The figure and the GeoAxes objects. + # """ - for i in range(nrows * ncols): - ax[i].set_global() - if coastlines: - ax[i].coastlines() + # proj = PROJECTIONS.get(projection, PROJECTIONS["orthographic"]) + # fig, ax = plt.subplots(nrows, ncols, subplot_kw={"projection": proj}) + # ax: GeoAxes | tp.Sequence[GeoAxes] = ( + # [ax] if nrows == 1 and ncols == 1 else ax.ravel() + # ) - if region != "globe": - ax[i].set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) + # for i in range(nrows * ncols): + # ax[i].set_global() + # if coastlines: + # ax[i].coastlines() - return fig, ax + # if region != "globe": + # ax[i].set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) + + # return fig, ax def plot_field( self, - ax: GeoAxes, + subplot: ekp.Map, field: np.ndarray, - region: str | None = None, - validtime: str = "", - colorbar: dict | bool = True, + style: ekp.styles.Style | None = None, + colorbar: bool = True, + title: str | None= None, **kwargs, ): - """Plot a field on a GeoAxes object. + """Plot a field on a Map object. Parameters ---------- - ax : GeoAxes - The GeoAxes object to plot on. + subplot : earthkit.plots.Map + The Map subplot object to plot on. field : np.ndarray The field to plot. - region : str, optional - The region to plot, by default None. - colorbar : dict | bool, optional + style : ekp.styles.Style, optional + Earthkit.plots style for the map plot. + colorbar : bool Whether to plot a colorbar, by default True. - If a dictionary, it is passed as keyword arguments to plt.colorbar. + title: str, optional + Map subplot title. kwargs : dict Additional keyword arguments to pass to ax.tripcolor, including cmap, vmin, vmax, etc. """ - proj = ax.projection + proj = subplot.ax.projection - if proj == PROJECTIONS["orthographic"]: - triang, mask = self._ortographic_tri - else: - triang, mask = self.tri, slice(None, None) + # transform data coordinates to map coordinate reference system outside + # of the plotting function is a lot faster than letting tricontourf or + # tripcolor handle it in general, but not sure if using earthkit + # removed for now to simplify the workflow + #if proj == PROJECTIONS["orthographic"]: + # triang, mask = self._ortographic_tri + #else: + # triang, mask = self.tri, slice(None, None) # TODO: this is hardcoded for when the initial state has two timesteps # need to ditch this later field = field[-1] if field.ndim == 2 else field.squeeze() - im = ax.tripcolor(triang, field[mask], **kwargs) - ax.text( - 0.05, - 0.95, - f"Time: {validtime}", - transform=ax.transAxes, - fontsize=12, - color="white", - verticalalignment="top", - bbox=dict(facecolor="black", alpha=0.5), - ) + # TODO: clip data to domain would make plotting faster (especially tripcolor) + # tried using Map.domain.extract() but too memory heavy (probably uses + # meshgrid in the background), implement clipping with e.g. + # points = np.column_stack((lon,lat)); mask = domain_polygon.contains_points(points) + # would work but needs handling domain crossing dateline etc. - if region and region != "globe": - ax.set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) + # TODO: tricontourf/tripcolor can handle a Triangulation when used directly, + # for some reason this doesn not work when using it with earthkit-plot, + # guess is that the earthkit-plots check throwing: + # "ValueError: x and y arrays must have the same length" is incorrect, + # therefore using x,y here + #subplot.tripcolor( # also works but is slower + subplot.tricontourf( + x=self.lon, + y=self.lat, + z=field, + style=style, + **kwargs, # for earthkit.plots to work properly cmap and norm are needed here + ) + # TODO: gridlines etc would be nicer to have in the init, but I didn't get + # them to overlay the plot layer + subplot.standard_layers() if colorbar: - colorbar = { - "orientation": "horizontal", - "pad": 0.04, - "aspect": 45, - "extend": "both", - "shrink": 0.75, - } | (colorbar if isinstance(colorbar, dict) else {}) - plt.colorbar(im, **colorbar) + subplot.legend() + if title: + subplot.title(title) + + + # def plot_field( + # self, + # ax: GeoAxes, + # field: np.ndarray, + # region: str | None = None, + # validtime: str = "", + # colorbar: dict | bool = True, + # **kwargs, + # ): + # """Plot a field on a GeoAxes object. + + # Parameters + # ---------- + # ax : GeoAxes + # The GeoAxes object to plot on. + # field : np.ndarray + # The field to plot. + # region : str, optional + # The region to plot, by default None. + # colorbar : dict | bool, optional + # Whether to plot a colorbar, by default True. + # If a dictionary, it is passed as keyword arguments to plt.colorbar. + # kwargs : dict + # Additional keyword arguments to pass to ax.tripcolor, including cmap, + # vmin, vmax, etc. + # """ + + # proj = ax.projection + + # if proj == PROJECTIONS["orthographic"]: + # triang, mask = self._ortographic_tri + # else: + # triang, mask = self.tri, slice(None, None) + + # # TODO: this is hardcoded for when the initial state has two timesteps + # # need to ditch this later + # field = field[-1] if field.ndim == 2 else field.squeeze() + + # im = ax.tripcolor(triang, field[mask], **kwargs) + # ax.text( + # 0.05, + # 0.95, + # f"Time: {validtime}", + # transform=ax.transAxes, + # fontsize=12, + # color="white", + # verticalalignment="top", + # bbox=dict(facecolor="black", alpha=0.5), + # ) + + # if region and region != "globe": + # ax.set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) + + # if colorbar: + # colorbar = { + # "orientation": "horizontal", + # "pad": 0.04, + # "aspect": 45, + # "extend": "both", + # "shrink": 0.75, + # } | (colorbar if isinstance(colorbar, dict) else {}) + # plt.colorbar(im, **colorbar) @cached_property def _ortographic_tri(self) -> Triangulation: From 206bc074748405f73ba4b4bd5b3cf29903e2a1fc Mon Sep 17 00:00:00 2001 From: clairemerker <34312518+clairemerker@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:39:43 +0200 Subject: [PATCH 15/35] improve tests --- workflow/scripts/tests/unit/test_colormaps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workflow/scripts/tests/unit/test_colormaps.py b/workflow/scripts/tests/unit/test_colormaps.py index 114e38a..5051b35 100644 --- a/workflow/scripts/tests/unit/test_colormaps.py +++ b/workflow/scripts/tests/unit/test_colormaps.py @@ -23,6 +23,7 @@ def test_load_valid_colormap(monkeypatch, tmp_path): cmap = result["cmap"] norm = result["norm"] + bounds = result["bounds"] # cmap has n_levs-1 colors inside assert cmap.N == 2 @@ -36,6 +37,7 @@ def test_load_valid_colormap(monkeypatch, tmp_path): assert np.allclose(cmap(9999), (100/255, 110/255, 120/255, 1.0)) # bounds assert np.allclose(norm.boundaries, [0, 1, 2]) + assert np.allclose(bounds, [0, 1, 2]) def test_missing_file_raises(monkeypatch, tmp_path): From a6fcea6f3c1d2294fdb3c46e92fe10f9f4d15529 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 8 Oct 2025 22:46:20 +0200 Subject: [PATCH 16/35] Refactor plot rule and adopt marimo (#54) * Adopt more atomic approach Also use marimo for interactive editing * Move notebook to dedicated folder * Remove original script * Add example config * Revert some wrong changes --- config/showcase.yaml | 45 ++++ pyproject.toml | 1 + uv.lock | 288 +++++++++++++++++++++- workflow/Snakefile | 6 +- workflow/notebooks/plot_forecast_frame.py | 150 +++++++++++ workflow/rules/plot.smk | 54 +++- workflow/scripts/plot_map.py | 245 ------------------ 7 files changed, 529 insertions(+), 260 deletions(-) create mode 100644 config/showcase.yaml create mode 100644 workflow/notebooks/plot_forecast_frame.py delete mode 100644 workflow/scripts/plot_map.py diff --git a/config/showcase.yaml b/config/showcase.yaml new file mode 100644 index 0000000..cec3cf0 --- /dev/null +++ b/config/showcase.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=../workflow/tools/config.schema.json +description: | + Basic config for showcasing M-1 forecaster. + +dates: + start: 2020-01-01T12:00 + end: 2020-01-01T12:00 + # end: 2020-03-30T00:00 + frequency: 12h + +lead_time: 24h + +runs: + - forecaster: + mlflow_id: d0846032fc7248a58b089cbe8fa4c511 + label: M-1 forecaster + +baselines: + - baseline: + baseline_id: COSMO-E + label: COSMO-E + root: /store_new/mch/msopr/ml/COSMO-E + steps: 0/12/6 + +analysis: + label: COSMO KENDA + analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-6h-v3-pl13.zarr + +locations: + output_root: output/ + mlflow_uri: + - https://servicedepl.meteoswiss.ch/mlstore + - https://mlflow.ecmwf.int + +profile: + executor: slurm + global_resources: + gpus: 15 + default_resources: + slurm_partition: "postproc" + cpus_per_task: 1 + mem_mb_per_cpu: 1800 + runtime: "1h" + gpus: 0 + jobs: 50 diff --git a/pyproject.toml b/pyproject.toml index 2456272..adb5991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "netcdf4>=1.7.2", "cartopy", "earthkit-plots", + "marimo>=0.16.5", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 0c624ff..a9df25b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,25 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version < '3.12'", ] +[[package]] +name = "adjusttext" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/d4/6585f3b6fdb75648bca294664af4becc8aa2fb3fb08f4e4e9fd27e10d773/adjusttext-1.3.0.tar.gz", hash = "sha256:4ab75cd4453af4828876ac3e964f2c49be642ea834f0c1f7449558d5f12cbca1", size = 15724, upload-time = "2024-10-31T16:45:36.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/1c/8feedd607cc14c5df9aef74fe3af9a99bf660743b842a9b5b1865326b4aa/adjustText-1.3.0-py3-none-any.whl", hash = "sha256:da23d7b24b6db5ffa039bb136bfa556207365e32f48ac74b07ad26dd485bc691", size = 13154, upload-time = "2024-10-31T16:45:35.227Z" }, +] + [[package]] name = "alembic" version = "1.16.5" @@ -877,6 +891,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/df/8178bb8aa5897cd65369356402267b1609b1bdb592ba80c917c716aefe11/earthkit_meteo-0.4.1-py3-none-any.whl", hash = "sha256:f90b708a338bacf593a34c3da8e0526c9344a5480ce7627a10f7a1df67bd320f", size = 56920, upload-time = "2025-06-05T12:00:17.649Z" }, ] +[[package]] +name = "earthkit-plots" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adjusttext" }, + { name = "cartopy" }, + { name = "earthkit-data" }, + { name = "earthkit-plots-default-styles" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pint" }, + { name = "plotly" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/f1/23d6f86ea8d36ef7acb2b6b981b6c52406965f0521bf656f4968a4fcdff9/earthkit_plots-0.3.5.tar.gz", hash = "sha256:8398843e3eed3e2a8628d1f8173176f50526ca5af4f70ecac9ac89e838ef0263", size = 46455951, upload-time = "2025-08-28T16:00:46.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/89/5f2003e80b2db570131caeb0a1421029f03762e6ffb5bfccbeb7e8064b64/earthkit_plots-0.3.5-py3-none-any.whl", hash = "sha256:110725931cffcb36c52237efd2a67895bcb7166764a8e1b72507a651fd126177", size = 2657104, upload-time = "2025-08-28T16:00:44.406Z" }, +] + +[[package]] +name = "earthkit-plots-default-styles" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/07/b40157b8e7c7292b743d3df35891b035f947b33cf7cf363bb599e88debbe/earthkit_plots_default_styles-0.1.2.tar.gz", hash = "sha256:6676b6aaf90e82be27a6e3c3a25d7df9a71608e08cb7a621d18d02982cd77d69", size = 16047, upload-time = "2025-08-26T16:14:13.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/56/9946e930ec700b02dee7e2aefcf37b840e91208afcf534a540c99609e087/earthkit_plots_default_styles-0.1.2-py3-none-any.whl", hash = "sha256:0a009096fd72af54efd450050958e5466656ec7bb9a8ad4b15c6304207c05f25", size = 19058, upload-time = "2025-08-26T16:14:12.608Z" }, +] + [[package]] name = "earthkit-regrid" version = "0.4.0" @@ -956,6 +1000,8 @@ dependencies = [ { name = "anemoi-datasets" }, { name = "cartopy" }, { name = "click" }, + { name = "earthkit-plots" }, + { name = "marimo" }, { name = "meteodata-lab" }, { name = "mlflow" }, { name = "netcdf4" }, @@ -984,8 +1030,10 @@ requires-dist = [ { name = "anemoi-datasets", specifier = ">=0.5.25" }, { name = "cartopy" }, { name = "click" }, + { name = "earthkit-plots" }, { name = "fastparquet", marker = "extra == 'kerchunk'" }, { name = "kerchunk", marker = "extra == 'kerchunk'" }, + { name = "marimo", specifier = ">=0.16.5" }, { name = "meteodata-lab", specifier = ">=0.4.0" }, { name = "mlflow", specifier = ">=3.1.1" }, { name = "netcdf4", specifier = ">=1.7.2" }, @@ -1421,6 +1469,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1598,6 +1658,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, ] +[[package]] +name = "loro" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/be/4e00ced4b8f2d852dc581109de9b4cd9362395e276b509eece098c42eedd/loro-1.8.1.tar.gz", hash = "sha256:22cfb19625bd7245e9747ee9d43b10511c16a35775a38cf914dc74863c4dbe88", size = 64093, upload-time = "2025-09-23T15:53:20.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/ac/341b48d2b6ac7af877b009361438f76de550b3161f9946e68f2ebc77bc47/loro-1.8.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8bd225a2697b76b5d3d7df0a9bd4e37aa68ff9d46cd9ad3f00b57bade3fb1642", size = 3107035, upload-time = "2025-09-23T15:50:33.74Z" }, + { url = "https://files.pythonhosted.org/packages/29/e5/a56a0761df4fbeb551d0c9146533cb1a21bb8236e475f791e62263b8c1f2/loro-1.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:12a3d692d1f3c29e89c740f4aef7ab251bc2d6593279aa0e4e83eaa70c59e9a1", size = 2898298, upload-time = "2025-09-23T15:50:17.445Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/162807e021e2ae3d4fc8b7bb87a2f7f559ff84014611f351528549c01b13/loro-1.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec12410983b9a9a431791bae5f16585bcbe10ac5bda1808acec91b7aac27bee9", size = 3109377, upload-time = "2025-09-23T15:46:48.329Z" }, + { url = "https://files.pythonhosted.org/packages/7f/dc/4db74a6e36399c18687e4f9cb4b5c5e945839362b0d1b22d2492e89739ff/loro-1.8.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee0c47132e2e9e75c7dcbe28ea003ab2d3ec9c3cf1a18bfc2c706c9bd3805e79", size = 3181693, upload-time = "2025-09-23T15:47:26.636Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/5af09cf5c63b3a7c01f56194a0589eaaee64d58fd77bcc123adf20a79584/loro-1.8.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1031d717af5515d63335843466f6c1726d0ed609744c143b83f41097caa16762", size = 3565385, upload-time = "2025-09-23T15:48:04.998Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/63855b31146277e5a29cfaa5924df1559ca0ff408b86fddc96e46f69e1ab/loro-1.8.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:578bd80bd64131aaf3a0e7b2dbb1aa7804c99eedd7ab0a43fd8ea4f6a6d43c35", size = 3280889, upload-time = "2025-09-23T15:48:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/e4/20/b73b3e010371126a5778c93f98f36da880f7f7573ec04903d900414f0768/loro-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88db40a908697bbe37981fcb7e732117c1780ac14b11efd5805c5bd8e04cd3ed", size = 3170921, upload-time = "2025-09-23T15:49:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/40/97/3bcfaea321dd9ecc34e68cb0f8994c58e8fd805eb881975b7a73c321fbfe/loro-1.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d87de3ea9999e65801dfbb0b04a64aad39402c76a934c398a10efd7b5d3394e1", size = 3506727, upload-time = "2025-09-23T15:49:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/07315509a3956b76ec4c7cbc6113c82d1096544e4de6b552889612ed8529/loro-1.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b18d4917f0df0df81d09aff5fab034519a3de818e7160d8a0d86b080ae7fdc1c", size = 3290762, upload-time = "2025-09-23T15:50:51.314Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a9/2e1e8f77696e4106f5313248707ae5aeabfe20fa22dc6528e5ca2f280317/loro-1.8.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8e44e48704b0391ced218178064ce479ee24e56386605fe75472b16747af3cb9", size = 3445333, upload-time = "2025-09-23T15:51:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/78/57/11ee3e0d972a959a93fc7fbcfb0076547b9a53bf00c9b0ccf3787b46f20c/loro-1.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:95ebc4c28558875cb8eb5d1b1d3faaf5c7711895af45baafdfcbcca4c7965087", size = 3488752, upload-time = "2025-09-23T15:52:06.808Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/8c9f96fbcf17ae7883e126099b38ba34a038bc08986b7cd65564de2e977f/loro-1.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:563836006b4e254614b94fac76c94c9b5823b8fbe97755027cd94f1c0bd86103", size = 3387391, upload-time = "2025-09-23T15:52:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/72/a1/f0482c6f01ab452af89cc6f5950629f62b4cfa5f254a0f8d8018db98cb07/loro-1.8.1-cp311-cp311-win32.whl", hash = "sha256:6e59d893deedf4ad84a958f42bf00dcdb235293e86cf74c4d58bdc517e96a17e", size = 2597184, upload-time = "2025-09-23T15:53:44.74Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5f/d1e001ca135a6e54b8acf07dedf461e6326b7da93fa494a197a0b16d3955/loro-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04789df148e7d077ce2e206cf60e3d908fb8ced56460a7a7abcf12afcbf3e39", size = 2737770, upload-time = "2025-09-23T15:53:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/00/e1/2d381182a111ca8cf4f4869bcf43e68c4ebabf1d84da4a08eda355834547/loro-1.8.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9eeab61cac92d504eecd580c577becc12a0a3830141b17a49812ecaf5d3f3ebf", size = 3088136, upload-time = "2025-09-23T15:50:35.106Z" }, + { url = "https://files.pythonhosted.org/packages/3f/9c/00a5476efb54b1091145ed3c7dc0d5961f283b407e7608b649d00ded4a28/loro-1.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f08fa1b4bdc79901c763d81361537ca5c086560acb80291f5d6fe163613c603", size = 2878906, upload-time = "2025-09-23T15:50:19.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/e55ba22e04313979c4f0eb74db1100c179c592d99cb0e514e60a155bbf02/loro-1.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420fb0112115dc85b8abd392e18aa163c7fda72b5329be46e7d0cb2261ef8adc", size = 3114935, upload-time = "2025-09-23T15:46:49.989Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/55deb84ed33d1d8a4f45c112bcb36d00701d8c94bf3f2071e610a993b36e/loro-1.8.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97153d8929cda5903fdd5d681a5d0d4a28382e2707b292cfad6a387f4b73396c", size = 3181672, upload-time = "2025-09-23T15:47:27.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/05/181f8051b2142c28e5cf294ac5f13b34bb3e3e802d256842010e05c29596/loro-1.8.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:934362a533d0ebf75216799f1305252502a2e9733b3a7bb311012c4b8495f541", size = 3567186, upload-time = "2025-09-23T15:48:06.357Z" }, + { url = "https://files.pythonhosted.org/packages/03/9b/e91146ad0a0cfb73bd47f39e69685ab3e8654aa17875f1806ba484be88ef/loro-1.8.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7c777cd9ddc13cde9d0ec887ae3e02879d2f861d5862a0b6efd29fe4eff30dc", size = 3286193, upload-time = "2025-09-23T15:48:41.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/2e/c07116cf6a22dbcb5d7d7d693b184358f8a59737290076c98108f17ffb29/loro-1.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca9d40e6e65d748d36867fa623275d3b3bdb7c1da68f4005bc17f69a57034c0", size = 3177660, upload-time = "2025-09-23T15:49:48.468Z" }, + { url = "https://files.pythonhosted.org/packages/83/05/8ec0261ac604b76a716c0f57afbf5454448b1d82f0a06c99972ae89e28de/loro-1.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca5bd845dc9b880a3fcbe1c977549157ed3e22566a38ee3d4bd94bfd76e12e50", size = 3507836, upload-time = "2025-09-23T15:49:19.107Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/1be760344ca3f9cff3732b6d4ea0c03a9118b479074568bd9908dc935b30/loro-1.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:48884003f2268d83864205445ac8302894c0f51c63c7d8375c4ffd8e100e7ced", size = 3295335, upload-time = "2025-09-23T15:50:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/b6362dc3103e45e4f3680d6c8df44c7f5a3e266c3940119956b0120e1b7a/loro-1.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7392c983eb8c6fa062925dcca583dd2d635ea16105153b0cea3a0f40333bf60c", size = 3444357, upload-time = "2025-09-23T15:51:30.694Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9c7537846bb6d2a1267adcabd202f02a3c3fa7a3fbcf6537106574fc8fd9/loro-1.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03f9e0ea6c72929cacebef44e698db50e0e9caa77cc4d87d43a5b5836896a5a3", size = 3489985, upload-time = "2025-09-23T15:52:08.234Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8a/66b7859080d9017ecae74d7835fe2419dfd27435382195d508644530b141/loro-1.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e0a0188d120bce70a64c58731330ed9b495ae2034061b5a3616b2b7940ead89", size = 3393867, upload-time = "2025-09-23T15:52:44.75Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9e/7c83206c10f8cb38532da00f0814cac0e6207956a6a39e5e183227cece21/loro-1.8.1-cp312-cp312-win32.whl", hash = "sha256:90a02ac5c85629d920c4767dc4b31382d21bde7af93d5dc4d3a4fcde4b4fece0", size = 2597517, upload-time = "2025-09-23T15:53:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/af/86/4357a818e5a03d1be1fa62cc1c0591b19b8a5e71dd00d45a7f8e8b48b28a/loro-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:92a31a8613fc6d9bb33a64767202e19592ac670618a174c0fbc940e31dba9d87", size = 2741953, upload-time = "2025-09-23T15:53:24.587Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7c/e0f6d6376dedb504e826b09a71bb871f4c032c2c95db0f96ee9f1b463a17/loro-1.8.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:55ee9ded98d6e328f210a1b9e2f01e8befb6994da82dd03756c56d8aa047a2ce", size = 3088156, upload-time = "2025-09-23T15:50:37.613Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/9fa9fd4a244539943df17c4fb3e3c5e90f0726731b9bf59bfbd9e57b09bb/loro-1.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4dcc9a3f558912d0ba2d39954f8391084e987e7970b375bfd96f67d9499ad4a0", size = 2879185, upload-time = "2025-09-23T15:50:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f2/48ab3634a1dc3f5951e05905d93c7e9dc2061d93e1facf6896f0d385cb61/loro-1.8.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a2b318bb08d1bdc7a0b8467a7ec6d90c7c46b0c58e7aafc9fc307825fa868f7", size = 3115017, upload-time = "2025-09-23T15:46:51.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/a1/7a80b48fca9366cb6867e4394b80dae7db9044e3f1e8ed586d5dfc467c2c/loro-1.8.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a7e09cb68da97559cd103a93ae69004bb929fba3db6a13846c83ac979698ce", size = 3181487, upload-time = "2025-09-23T15:47:29.219Z" }, + { url = "https://files.pythonhosted.org/packages/50/f9/881d9a4658f5d33ac822735ee503d8e5590db552a1ac3f992a36a4fae03d/loro-1.8.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc5acb22ca0ae3e6793024cfc3ea99f200b57c549fa71e48cdaedf22cde6fe19", size = 3566686, upload-time = "2025-09-23T15:48:07.701Z" }, + { url = "https://files.pythonhosted.org/packages/be/6b/3ff95d187483b0f71e026e86a3b3043e27048d9a554777254b8005f396c8/loro-1.8.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc92b19a44b16e86dced2b76d760715f1099aa99433c908e0fe5627d7897b98d", size = 3286348, upload-time = "2025-09-23T15:48:43.416Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/414915e26d2463107425f3ff249a2f992f2b15d0f98d75c99422fc34eb48/loro-1.8.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eed7933a3e1500c5a8e826c5faf7904ce253725512234eb2b2bfb01ca085217", size = 3177439, upload-time = "2025-09-23T15:49:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/d0/25/538488ceb0a7b857eadecc4e46c6bea20df2b9f6ad1660ad6d10b201d931/loro-1.8.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e62783de33f8bf0cf8b834defacd4dd62d1adb227d93d9d24cc28febf9f53eec", size = 3508131, upload-time = "2025-09-23T15:49:20.534Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f0/8c06a5ae198c7fdc636fd40cf6edc604b45e51affbd537d099eb93a95143/loro-1.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8f394e65acd54af19b5cea796d9d9aa4e512f7979f8514f6938fd9813a753f5", size = 3295009, upload-time = "2025-09-23T15:50:54.272Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/2fb82afaab5899cc3a05d31e4059aded41571e6fd5c310cb5bc5520c563f/loro-1.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6205cc3fcb75b4913678ca399ab97abab0f253c8f72ece637d183979c06d19a1", size = 3444600, upload-time = "2025-09-23T15:51:32.046Z" }, + { url = "https://files.pythonhosted.org/packages/f6/87/4b9ac56d371c7a4b85ea223ca17b7ab33de858dab8a1a176ad33af9d7cb7/loro-1.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1cd5d028309f8ae94b14b7b1fb3a6e488b8a09d205a37d44eb3af04061be742", size = 3489090, upload-time = "2025-09-23T15:52:09.704Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/abf2a9f9f6c0cfd6ccb940fa81d9561767d01d43684505884e404ee4e930/loro-1.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3da7bc3cc0e8b04f094bc52c3f416f86be4c3a332dcbd9428b466d98329f26f5", size = 3393536, upload-time = "2025-09-23T15:52:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/76136846dc793e96a34f73220d65279f7b7f391a3446838fd095bf804d73/loro-1.8.1-cp313-cp313-win32.whl", hash = "sha256:a90e5d56a030e284a998b73a1c55c5b8c5f62f96bee4cc017b88ff815f9fb743", size = 2598079, upload-time = "2025-09-23T15:53:47.994Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6e/dfd0d18a7bd7d90b111cde4e628e0fc26d70307caae33f3ee6d28094125b/loro-1.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:fbee625170327de616709af943410b72c5a4c12ebd8f7dff6324d59aa51da5b2", size = 2742638, upload-time = "2025-09-23T15:53:26.074Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ac/e134286c4275af5ab0149ee1a200c64f35df2cccb1b70142af04b509ed7f/loro-1.8.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6f3dfa58cebfe1f0e08a8a929303338c506733dd8650afd3d1f3ac70546ece", size = 3109397, upload-time = "2025-09-23T15:46:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ee/578a588f5f0a642491b852d0bc7bbec90e6a93fa95b12c4e22e7514d156e/loro-1.8.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c632a0a4a00f4a73df32fcaf266320995f89b68fc5f1d875efc979cda810babd", size = 3182019, upload-time = "2025-09-23T15:47:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8932e77fb6563fcab5ce470a3b754a758b8ce743a389b14ba9c436cd5d/loro-1.8.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:888de919b082ace4cb88f7244aa7a5263233604fc0fb9e7571703940a6897be2", size = 3562136, upload-time = "2025-09-23T15:48:09.165Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/440e9ff25150908e9e91362fed32097c008956ff173e9d852adfd06ce25f/loro-1.8.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1689f8fd79dc76f75e4bd027e9685bb73b44e0b60cfc0412d78369da300e6f68", size = 3283645, upload-time = "2025-09-23T15:48:44.959Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/4444939a3d244242dbcc14c98789c7c89d2468cb541629695335a953cbc3/loro-1.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:028194142bc4b628ec0f926018fbfd18d92912d69eb2f57a14adf4a3ef1fc7e7", size = 3288947, upload-time = "2025-09-23T15:50:55.972Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/70201ccf7b57f172ee1bb4d14fc7194359802aa17c1ac1608d503c19ee47/loro-1.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:9e6dde01971d72ba678161aaa24bc5261929def86a6feb8149d3e2dab0964aea", size = 3444718, upload-time = "2025-09-23T15:51:33.872Z" }, + { url = "https://files.pythonhosted.org/packages/14/b8/01c1d4339ab67d8aff6a5038db6251f6d44967a663f2692be6aabe276035/loro-1.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d3789752b26b40f26a44a80d784a4f9e40f2bd0e40a4eeb01e1e386920feaaa", size = 3490418, upload-time = "2025-09-23T15:52:11.183Z" }, + { url = "https://files.pythonhosted.org/packages/60/67/88e0edaf4158184d87eee4efdce283306831632ef7ef010153abf6d36b82/loro-1.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ab04743218b6cbbfdf4ca74d158aed20ed0c9d7019620d35548e89f1d519923b", size = 3389761, upload-time = "2025-09-23T15:52:47.785Z" }, + { url = "https://files.pythonhosted.org/packages/54/fb/ccf317276518df910340ddf7729a0ed1602d215db1f6ca8ccda0fc6071df/loro-1.8.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:29c25f33c659a8027974cd88c94f08b4708376b200290a858c8abd891d64ba15", size = 3072231, upload-time = "2025-09-23T15:50:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/87f37c4bbef478373b15ad4052ab9ee69ae87646a9c853dda97147f4e87a/loro-1.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a9643d19eee7379b6980fc3b31a492bd22aa1e9aaa6fd67c8b5b4b57a0c7a1c", size = 2870631, upload-time = "2025-09-23T15:50:26.223Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7f/b0d121297000d1278c4be96ebaed245b7e1edf74851b9ed5aa552daf85eb/loro-1.8.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323370115c37a793e805952e21703d8e8c91cc7ef16dd3a378043fe40174599f", size = 3156119, upload-time = "2025-09-23T15:49:51.227Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/35c62e7acfc572397ffb09db60f20b32be422a7983ae3d891527983a6a7e/loro-1.8.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69265d6751e536fd7ba1f04c6be200239b4d8090bcd1325a95ded08621c4c910", size = 3492080, upload-time = "2025-09-23T15:49:22.137Z" }, + { url = "https://files.pythonhosted.org/packages/23/36/543916bb43228e4d13e155d9f31cbe16cf4f995d306aa5dbf4aba2b44170/loro-1.8.1-cp314-cp314-win32.whl", hash = "sha256:00c3662f50b81276a0f45d90504402e36512fda9f98e3e9353cc2b2394aa56a5", size = 2584938, upload-time = "2025-09-23T15:53:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/8369c393107cafcaf6d5bdfe8cc4fead384b8ab8c7ddaf5d16235e5482e2/loro-1.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6ebacceed553dad118dd61f946f5f8fb23ace5ca93e8ee8ebd4f6ca4cffa854", size = 2722278, upload-time = "2025-09-23T15:53:36.035Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3b/2d13e114e6e4e0fed0e2626d00437b9295b4cf700831b363b3a5cebf1704/loro-1.8.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3767a49698ca87c981cf081e83cd693bb6db1891afa735e26eb07e4a8e251eb", size = 3106733, upload-time = "2025-09-23T15:46:59.98Z" }, + { url = "https://files.pythonhosted.org/packages/d3/78/c44830c89c786dfa2164e573b4954ce1efca708bcffffc1ea283f26dbfeb/loro-1.8.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af22361dd0f9d1fde7fe51da97a6348d811f1c89e4646d1ae539a8ebf08d2174", size = 3178590, upload-time = "2025-09-23T15:47:38.454Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1b/3aea45999e3a3f9d8162824cee70ec358b5a7b0e603d475b7856c7269246/loro-1.8.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b499c748cb6840223c39e07844975e62d7405de4341ea6f84cf61fc7d9f983c7", size = 3562843, upload-time = "2025-09-23T15:48:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/d5795cde4cbddaa1954d8ebba3a133aae4900d27866bc2bd7d5ce053837a/loro-1.8.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a83d4aed44ce2845012793110e4381fbca48cd9ad7c7534567e11d8d6b3fe6a", size = 3278780, upload-time = "2025-09-23T15:48:51.042Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/f2f9eb748ba64835cc58488b0e41ba1fc65b4386332d04563cf61f066035/loro-1.8.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d2375b3a37574345e1744e6580b278e50265f496573a7e2b2523d452a67766c", size = 3167056, upload-time = "2025-09-23T15:49:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0e/3c43fa589feb18932222cf76e236dc8314c206644a43aaf8a8780c78cb74/loro-1.8.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff9ab48ff7625f266ab91490f38553811965429a027f24b014a979af03a26181", size = 3504168, upload-time = "2025-09-23T15:49:26.674Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/677b02cee8ce43ca2816a68b455f6cf6d10daf36a99aec140ab5e121b57f/loro-1.8.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:fc99167626c3b0938db9172bd935bc6d72ae701b11448cd4107b71aa76a5a7ab", size = 3287927, upload-time = "2025-09-23T15:51:01.967Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/212a46bf505f7518fd235f7e3e7355e24f612cdd70cb3f9b254742bf7b9f/loro-1.8.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:ce7cfdb085aa9c7f6f16d9090cff64e7a8f5ccde0f11b0cb0f20567f7798c3f3", size = 3440556, upload-time = "2025-09-23T15:51:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ca/e9184f8a595add3dfa8d0a8bf44e0ae848b18e6758b4a05b9dc1f492221e/loro-1.8.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a2f8db61477b6951c6ee45b48e7c307014540229b09b17f07c6eea8ced519448", size = 3487021, upload-time = "2025-09-23T15:52:17.245Z" }, + { url = "https://files.pythonhosted.org/packages/f2/89/17dd2c68018d11bcd54e9504a9ae71d8ca520fd78c4769d54aae016946f1/loro-1.8.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7bc42670b8cc05d7bc26525ea9f0f1404dc49c0f611ea21eb9d551d51d9f0839", size = 3384697, upload-time = "2025-09-23T15:52:53.385Z" }, +] + [[package]] name = "lru-dict" version = "1.3.0" @@ -1644,6 +1778,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] +[[package]] +name = "marimo" +version = "0.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "docutils" }, + { name = "itsdangerous" }, + { name = "jedi" }, + { name = "loro" }, + { name = "markdown" }, + { name = "msgspec" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/6b/5e1fcdeebebf6cebfbbf7cd6eda3be1ee019e50c693d99f00e478c4f3f8c/marimo-0.16.5.tar.gz", hash = "sha256:8f5939d3c4e67ff25f6cfeefe731971ed7f3346c20098034b923a24a0d7770d6", size = 33882430, upload-time = "2025-10-02T19:57:49.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/a0/14bd2ae122db3c4c43c6ebf0c861196328156f61e6452c2cd67b730430b0/marimo-0.16.5-py3-none-any.whl", hash = "sha256:1f98c0ee0fed9337e26c895c662f92cc578cdd03502c194eac9ceeb434bf479b", size = 34400840, upload-time = "2025-10-02T19:57:45.076Z" }, +] + [[package]] name = "markdown" version = "3.8.2" @@ -1880,6 +2042,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/86/396a17af4e994c7ffa65609739baddc17f4436aec9511478816e157a1bda/mlflow_tracing-3.3.2-py3-none-any.whl", hash = "sha256:9a3175fb3b069c9f541c7a60a663f482b3fcb4ca8f3583da3fdf036a50179e05", size = 1120520, upload-time = "2025-08-27T12:32:13.539Z" }, ] +[[package]] +name = "msgspec" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939, upload-time = "2024-12-27T17:39:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202, upload-time = "2024-12-27T17:39:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" }, + { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" }, +] + [[package]] name = "multiurl" version = "0.3.7" @@ -1904,6 +2095,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "narwhals" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/40ff412dabf90ef6b99266b0b74f217bb88733541733849e0153a108c750/narwhals-2.6.0.tar.gz", hash = "sha256:5c9e2ba923e6a0051017e146184e49fb793548936f978ce130c9f55a9a81240e", size = 561649, upload-time = "2025-09-29T09:08:56.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3b/0e2c535c3e6970cfc5763b67f6cc31accaab35a7aa3e322fb6a12830450f/narwhals-2.6.0-py3-none-any.whl", hash = "sha256:3215ea42afb452c6c8527e79cefbe542b674aa08d7e2e99d46b2c9708870e0d4", size = 408435, upload-time = "2025-09-29T09:08:54.503Z" }, +] + [[package]] name = "nbformat" version = "5.10.4" @@ -2224,6 +2424,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, ] +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + [[package]] name = "partd" version = "1.4.2" @@ -2369,6 +2578,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] +[[package]] +name = "plotly" +version = "6.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/63/961d47c9ffd592a575495891cdcf7875dc0903ebb33ac238935714213789/plotly-6.3.1.tar.gz", hash = "sha256:dd896e3d940e653a7ce0470087e82c2bd903969a55e30d1b01bb389319461bb0", size = 6956460, upload-time = "2025-10-02T16:10:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/93/023955c26b0ce614342d11cc0652f1e45e32393b6ab9d11a664a60e9b7b7/plotly-6.3.1-py3-none-any.whl", hash = "sha256:8b4420d1dcf2b040f5983eed433f95732ed24930e496d36eb70d211923532e64", size = 9833698, upload-time = "2025-10-02T16:10:22.584Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -2587,6 +2809,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, +] + [[package]] name = "pyparsing" version = "3.2.3" @@ -3397,6 +3632,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + [[package]] name = "toolz" version = "1.0.0" @@ -3571,6 +3815,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, ] +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + [[package]] name = "werkzeug" version = "3.1.3" diff --git a/workflow/Snakefile b/workflow/Snakefile index d10003b..06b6516 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -48,9 +48,13 @@ rule showcase_all: """Target rule for showcase workflow.""" input: expand( - OUT_ROOT / "data/runs/{run_id}/{init_time}/plots", + OUT_ROOT + / "showcases/{run_id}/{init_time}/{init_time}_{param}_{projection}_{region}.gif", init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES], run_id=collect_all_runs(), + param="2t", + projection="orthographic", + region="switzerland", ), diff --git a/workflow/notebooks/plot_forecast_frame.py b/workflow/notebooks/plot_forecast_frame.py new file mode 100644 index 0000000..fe1fa83 --- /dev/null +++ b/workflow/notebooks/plot_forecast_frame.py @@ -0,0 +1,150 @@ +import marimo + +__generated_with = "0.16.5" +app = marimo.App(width="medium") + + +@app.cell +def _(): + from argparse import ArgumentParser + import logging + import numpy as np + from pathlib import Path + + import earthkit.plots as ekp + + from src.plotting import StatePlotter + from src.compat import load_state_from_raw + from src.colormap_defaults import CMAP_DEFAULTS + return ( + ArgumentParser, + CMAP_DEFAULTS, + Path, + StatePlotter, + ekp, + load_state_from_raw, + logging, + np, + ) + + +@app.cell +def _(logging): + LOG = logging.getLogger(__name__) + LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + logging.basicConfig(level=logging.INFO, format=LOG_FMT) + return (LOG,) + + +@app.cell +def _(ArgumentParser, Path): + ROOT = Path(__file__).parent + + parser = ArgumentParser() + + parser.add_argument("--input", type=str, default=None, help="Directory to raw data") + parser.add_argument("--date", type=str, default=None, help="reference datetime") + parser.add_argument("--outfn", type=str, help="output filename") + parser.add_argument("--leadtime", type=str, help="leadtime") + parser.add_argument("--param", type=str, help="parameter") + parser.add_argument("--projection", type=str, help="projection") + parser.add_argument("--region", type=str, help="region") + + args = parser.parse_args() + raw_dir = Path(args.input) + outfn = Path(args.outfn) + leadtime = int(args.leadtime) + param = args.param + region = args.region + projection = args.projection + return args, leadtime, outfn, param, projection, raw_dir, region + + +@app.cell +def _(raw_dir): + # get all input files + raw_files = sorted(raw_dir.glob(f"*.npz")) + raw_files + return (raw_files,) + + +@app.cell +def _(leadtime, load_state_from_raw, raw_files): + # TODO: do not hardcode leadtimes + leadtimes = list(range(0, 126, 6)) + file_index = leadtimes.index(leadtime) + state = load_state_from_raw(raw_files[file_index]) + return (state,) + + +@app.cell +def _(CMAP_DEFAULTS, ekp): + def get_style(param): + """"Get style and colormap settings for the plot. + Needed because cmap/norm does not work in Style(colors=cmap), still needs + to be passed as arguments to tripcolor()/tricontourf(). + """ + cfg = CMAP_DEFAULTS[param] + return { + "style": ekp.styles.Style( + levels=cfg.get("bounds", None), + extend="both", + units=cfg.get("units", ""), + ), + "cmap": cfg["cmap"], + "norm": cfg.get("norm", None), + } + return (get_style,) + + +@app.cell +def _( + LOG, + StatePlotter, + args, + get_style, + np, + outfn, + param, + projection, + region, + state, +): + # plot individual fields + plotter = StatePlotter( + state["longitudes"], + state["latitudes"], + outfn.parent, + ) + fig = plotter.init_geoaxes( + nrows=1, ncols=1, projection=projection, region=region, size=(8,8), + ) + subplot = fig.add_map(row=0, column=0) + + if param == "uv": + field = np.sqrt( + state["fields"]["10u"] ** 2 + state["fields"]["10v"] ** 2 + ) + elif param == "2t": + field = state["fields"][param] - 273.15 + else: + field = state["fields"][param] + + plotter.plot_field( + subplot, + field, + **get_style(args.param) + ) + + validtime = state["valid_time"].strftime("%Y%m%d%H%M") + # leadtime = int(state["lead_time"].total_seconds() // 3600) + + fig.title(f"{param}, time: {validtime}") + + fig.save(outfn, bbox_inches="tight", dpi=400) + LOG.info(f"saved: {outfn}") + return + + +if __name__ == "__main__": + app.run() diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 5138d84..e51a308 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -1,26 +1,54 @@ # ----------------------------------------------------- # -# PLOTTING WORKFLOW # +# PLOTTING WORKFLOW # # ----------------------------------------------------- # -from datetime import datetime + include: "common.smk" -rule plot_forecast: + +rule plot_forecast_frames: input: -# grib_output=rules.map_init_time_to_inference_group.output[0], raw_output=rules.inference_routing.output[1], output: - directory(OUT_ROOT / "data/runs/{run_id}/{init_time}/plots/"), - params: - sources=",".join(list(EXPERIMENT_PARTICIPANTS.keys())), - log: - OUT_ROOT / "logs/plot_forecast/{run_id}-{init_time}.log", + temp( + OUT_ROOT + / "showcases/{run_id}/{init_time}/frames/{init_time}_{leadtime}_{param}_{projection}_{region}.png" + ), resources: slurm_partition="postproc", - cpus_per_task=24, - runtime="20m", + cpus_per_task=1, + runtime="5m", + # localrule: True + shell: + """ + python workflow/scripts/plot_forecast_frame.py \ + --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ + --param {wildcards.param} --leadtime {wildcards.leadtime} \ + --projection {wildcards.projection} --region {wildcards.region} \ + # interactive editing (needs to set localrule: True and use only one core) + # marimo edit workflow/scripts/notebook_plot_map.py -- \ + # --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ + # --param {wildcards.param} --leadtime {wildcards.leadtime} \ + # --projection {wildcards.projection} --region {wildcards.region} \ + """ + + +LEADTIME = int(pd.to_timedelta(config["lead_time"]).total_seconds() // 3600) + + +rule make_forecast_animation: + input: + expand( + OUT_ROOT + / "showcases/{run_id}/{init_time}/frames/{init_time}_{leadtime}_{param}_{projection}_{region}.png", + leadtime=[f"{i:03}" for i in range(0, LEADTIME + 6, 6)], + allow_missing=True, + ), + output: + OUT_ROOT + / "showcases/{run_id}/{init_time}/{init_time}_{param}_{projection}_{region}.gif", + localrule: True shell: """ - python workflow/scripts/plot_map.py \ - --input {input.raw_output} --date {wildcards.init_time} --output {output[0]}\ + convert -delay 80 -loop 0 {input} {output} """ diff --git a/workflow/scripts/plot_map.py b/workflow/scripts/plot_map.py deleted file mode 100644 index 6db4d09..0000000 --- a/workflow/scripts/plot_map.py +++ /dev/null @@ -1,245 +0,0 @@ -from argparse import ArgumentParser, Namespace -from functools import partial -import logging -import numpy as np -import os -from pathlib import Path -from concurrent.futures import ProcessPoolExecutor - -import earthkit.plots as ekp - -from src.plotting import StatePlotter -#from src.calc import process_augment_state -from src.compat import load_state_from_raw -from src.colormap_defaults import CMAP_DEFAULTS - -State = dict[str, np.ndarray | dict[str, np.ndarray]] - -LOG = logging.getLogger(__name__) -LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" -logging.basicConfig(level=logging.INFO, format=LOG_FMT) - - -def plot_state( - plotter: StatePlotter, - pred_state: State, - paramlist: list[str], - projection: str = "orthographic", - region: str = "europe", -) -> None: - - for param in paramlist: - - # plot individual fields - fig = plotter.init_geoaxes( - nrows=1, ncols=1, projection=projection, region=region, size=(8,8), - ) - subplot = fig.add_map(row=0, column=0) - - if param == "uv": - field = np.sqrt( - pred_state["fields"]["10u"] ** 2 + pred_state["fields"]["10v"] ** 2 - ) - elif param == "2t": - field = pred_state["fields"][param] - 273.15 - else: - field = pred_state["fields"][param] - - plotter.plot_field( - subplot, - field, - **get_style(param) - ) - - validtime = pred_state["valid_time"].strftime("%Y%m%d%H%M") - leadtime = int(pred_state["lead_time"].total_seconds() // 3600) - - fig.title(f"{param}, time: {validtime}") - - fn = f"{validtime}_{leadtime:03}_{param}_{projection}_{region}.png" - fig.save(plotter.out_dir / fn, bbox_inches="tight", dpi=400) - - -# def plot_state( -# plotter: StatePlotter, -# pred_state: State, -# paramlist: list[str], -# projection: str = "orthographic", -# region: str = "europe", -# ) -> None: - -# for param in paramlist: - -# # # plot individual fields -# fig, [gax] = plotter.init_geoaxes( -# projection=projection, region=region, coastlines=True, zorder=2 -# ) -# fig.set_size_inches(10, 10) - -# if param == "uv": -# field = np.sqrt( -# pred_state["fields"]["10u"] ** 2 + pred_state["fields"]["10v"] ** 2 -# ) -# elif param == "2t": -# field = pred_state["fields"][param] - 273.15 -# else: -# field = pred_state["fields"][param] - -# plotter.plot_field( -# ax=gax, -# field=field, -# zorder=1, -# validtime=pred_state["valid_time"].strftime("%Y%m%d%H%M"), -# **CMAP_DEFAULTS[param], -# ) - -# validtime = pred_state["valid_time"].strftime("%Y%m%d%H%M") -# leadtime = int(pred_state["lead_time"].total_seconds() // 3600) -# fn = f"{validtime}_{leadtime:03}_{param}_{projection}_{region}.png" -# plt.savefig(plotter.out_dir / fn, bbox_inches="tight", dpi=400) -# plt.clf() -# plt.cla() -# plt.close() - - -def process_plot_leadtime( - file: Path, - # dataset: Dataset, - paramlist: list[str], - plots_dir: Path, -): - LOG.info(f"Started plotting {file.name}...") - - LOG.info(f"Loading predicted and true states") - pred_state = load_state_from_raw(file) - #print(pred_state["valid_time"], pred_state["fields"].keys()) - - LOG.info(f"Augmenting states") - #pred_state = process_augment_state(pred_state) - - LOG.info(f"Initializing plotter") - plotter = StatePlotter( - pred_state["longitudes"], - pred_state["latitudes"], - plots_dir, - ) - - for region in ["europe", None, "switzerland"]: - for proj in ["orthographic"]: - plot_state( - plotter, - pred_state, - paramlist, - projection=proj, - region=region, - ) - LOG.info(f"Done plotting {file}.") - logging.basicConfig(level=logging.INFO, format=LOG_FMT) - return 0 - - -def create_animation( - plot_dir: Path, - param: str, - projection: str, - region: str, - name_prefix: str | None = None, -) -> None: - - out_dir = plot_dir / "animations" - out_dir.mkdir(exist_ok=True, parents=True) - name_prefix = f"{name_prefix}_" if name_prefix else "" - cmd = f"convert -delay 80 -loop 0" - - # # animations of prediction plots - plots_glob = f"{plot_dir}/*{param}_{projection}_{region}.png" - gif_fn = f"{out_dir}/{name_prefix}{param}_{projection}_{region}.gif" - print("CMD", plots_glob) - os.system(f"{cmd} {plots_glob} {gif_fn}") - - -class ScriptConfig(Namespace): - checkpoint_run_id: Path - model_name: None | str - # date: datetime - raw_dir: Path - paramlist: list[str] - out_dir: Path - - -def get_style(param): - """"Get style and colormap settings for the plot. - Needed because cmap/norm does not work in Style(colors=cmap), still needs - to be passed as arguments to tripcolor()/tricontourf(). - """ - cfg = CMAP_DEFAULTS[param] - return { - "style": ekp.styles.Style( - levels=cfg.get("bounds", None), - extend="both", - units=cfg.get("units", ""), - ), - "cmap": cfg["cmap"], - "norm": cfg.get("norm", None), - } - - -def main(cfg: ScriptConfig) -> None: - - LOG.info(f"Plotting inference results for {cfg}") - # set up output directory - - out_dir = Path(cfg.out_dir) - print(out_dir) - plots_dir = Path(out_dir) - - raw_dir = Path(cfg.raw_dir) - raw_files = sorted(raw_dir.glob(f"*.npz")) - _map_fn = partial( - process_plot_leadtime, - paramlist=cfg.paramlist, - plots_dir=plots_dir, - ) - - with ProcessPoolExecutor(max_workers=len(raw_files)) as executor: - results = executor.map(_map_fn, raw_files) - - # wait for all processes to finish - for _ in results: - pass - - LOG.info(f"Creating animations") - for param in cfg.paramlist: - for region in ["europe", None, "switzerland"]: - for proj in ["orthographic"]: - create_animation( - plots_dir, - param, - proj, - region, - name_prefix=cfg.model_name, - ) - - -if __name__ == "__main__": - - ROOT = Path(__file__).parent - OUT_DIR = ROOT / "output" - - parser = ArgumentParser() - - parser.add_argument("--input", type=str, default=None, help="Directory to raw data") - parser.add_argument("--date", type=str, default=None, help="reference datetime") - parser.add_argument("--output", type=str, help="output directory") - - args = parser.parse_args() - - config = ScriptConfig( - checkpoint_run_id="", - model_name="icon", - raw_dir=args.input, - paramlist=["10u", "2t"], - out_dir=args.output, - ) - - main(config) From 73544e0ece9d691dd023836731472b655c957adc Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 8 Oct 2025 23:33:44 +0200 Subject: [PATCH 17/35] Fix paths --- workflow/rules/plot.smk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index e51a308..ca8dff8 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -21,12 +21,12 @@ rule plot_forecast_frames: # localrule: True shell: """ - python workflow/scripts/plot_forecast_frame.py \ + python workflow/notebooks/plot_forecast_frame.py \ --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ --param {wildcards.param} --leadtime {wildcards.leadtime} \ --projection {wildcards.projection} --region {wildcards.region} \ # interactive editing (needs to set localrule: True and use only one core) - # marimo edit workflow/scripts/notebook_plot_map.py -- \ + # marimo edit workflow/notebooks/plot_forecast_frame.py -- \ # --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ # --param {wildcards.param} --leadtime {wildcards.leadtime} \ # --projection {wildcards.projection} --region {wildcards.region} \ From aefe77b151afa052c4d254e409b015384f26419d Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 8 Oct 2025 23:54:10 +0200 Subject: [PATCH 18/35] Revert using separate folder for notebooks --- workflow/rules/plot.smk | 7 ++++--- .../plot_forecast_frame.mo.py} | 0 2 files changed, 4 insertions(+), 3 deletions(-) rename workflow/{notebooks/plot_forecast_frame.py => scripts/plot_forecast_frame.mo.py} (100%) diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index ca8dff8..0bf7f2a 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -6,8 +6,9 @@ include: "common.smk" -rule plot_forecast_frames: +rule plot_forecast_frame: input: + script="workflow/scripts/plot_forecast_frame.mo.py", raw_output=rules.inference_routing.output[1], output: temp( @@ -21,12 +22,12 @@ rule plot_forecast_frames: # localrule: True shell: """ - python workflow/notebooks/plot_forecast_frame.py \ + python {input.script} \ --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ --param {wildcards.param} --leadtime {wildcards.leadtime} \ --projection {wildcards.projection} --region {wildcards.region} \ # interactive editing (needs to set localrule: True and use only one core) - # marimo edit workflow/notebooks/plot_forecast_frame.py -- \ + # marimo edit {input.script} -- \ # --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ # --param {wildcards.param} --leadtime {wildcards.leadtime} \ # --projection {wildcards.projection} --region {wildcards.region} \ diff --git a/workflow/notebooks/plot_forecast_frame.py b/workflow/scripts/plot_forecast_frame.mo.py similarity index 100% rename from workflow/notebooks/plot_forecast_frame.py rename to workflow/scripts/plot_forecast_frame.mo.py From b7642ccdeef87ec8105a5291ee28651425b491fb Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Thu, 9 Oct 2025 11:19:23 +0200 Subject: [PATCH 19/35] Specify individual showcases dates --- config/showcase.yaml | 9 ++++----- workflow/rules/common.smk | 2 ++ workflow/rules/report.smk | 15 ++++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/config/showcase.yaml b/config/showcase.yaml index cec3cf0..380f874 100644 --- a/config/showcase.yaml +++ b/config/showcase.yaml @@ -3,12 +3,11 @@ description: | Basic config for showcasing M-1 forecaster. dates: - start: 2020-01-01T12:00 - end: 2020-01-01T12:00 - # end: 2020-03-30T00:00 - frequency: 12h + - 2020-02-03T00:00 # Storm Petra + - 2020-02-07T00:00 # Storm Sabine + - 2020-10-01T00:00 # Storm Brigitte -lead_time: 24h +lead_time: 72h runs: - forecaster: diff --git a/workflow/rules/common.smk b/workflow/rules/common.smk index cea00b2..c7236d2 100644 --- a/workflow/rules/common.smk +++ b/workflow/rules/common.smk @@ -51,6 +51,8 @@ def _parse_timedelta(td): def _reftimes(): cfg = config["dates"] + if isinstance(cfg, list): + return [datetime.strptime(t, "%Y-%m-%dT%H:%M") for t in cfg] start = datetime.strptime(cfg["start"], "%Y-%m-%dT%H:%M") end = datetime.strptime(cfg["end"], "%Y-%m-%dT%H:%M") freq = _parse_timedelta(cfg["frequency"]) diff --git a/workflow/rules/report.smk b/workflow/rules/report.smk index 2ab6ce0..02ddd9d 100644 --- a/workflow/rules/report.smk +++ b/workflow/rules/report.smk @@ -7,6 +7,13 @@ from datetime import datetime include: "common.smk" +def make_header_text(): + dates = config["dates"] + if isinstance(dates, list): + return f"Explicit initializations from {len(dates)} runs have been used." + return f"Initializations from {dates.get('start')} to {dates.get('end')} by {dates.get('frequency')} have been used." + + rule report_experiment_dashboard: localrule: True input: @@ -21,13 +28,7 @@ rule report_experiment_dashboard: ), params: sources=",".join(list(EXPERIMENT_PARTICIPANTS.keys())), - header_text="Initializations from " - + config.get("dates").get("start") - + " to " - + config.get("dates").get("end") - + " by " - + config.get("dates").get("frequency") - + " have been used.", + header_text=make_header_text(), log: OUT_ROOT / "logs/report_experiment_dashboard/{experiment}.log", shell: From 69f3dc9bed962015e3ae03c8888f322633ced7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oph=C3=A9lia=20Miralles?= Date: Tue, 14 Oct 2025 09:09:03 +0200 Subject: [PATCH 20/35] Switch to GRIB and fix projection issues (#60) * Adopt more atomic approach Also use marimo for interactive editing * Move notebook to dedicated folder * add plot rule * Revert some wrong changes * Solve bugs adopt grib * Replace by plot_forecast_frame.py * Rename * Fix number of points cosmo 1e * Remove globe because it doesn't seem right * Add possibility to set region to none for global plots * Review * Update workflow/scripts/src/plotting.py * Simplify code --- config/forecasters-co1e.yaml | 8 +- config/forecasters.yaml | 5 +- resources/inference/configs/forecaster.yaml | 26 +-- .../forecaster_no_trimedge_with_global.yaml | 28 +++ .../configs/interpolator_from_test_data.yaml | 5 - workflow/Snakefile | 2 +- workflow/rules/plot.smk | 6 +- workflow/scripts/plot_forecast_frame.mo.py | 122 ++++++++---- workflow/scripts/src/compat.py | 47 ++++- workflow/scripts/src/plotting.py | 185 ++++-------------- 10 files changed, 219 insertions(+), 215 deletions(-) create mode 100644 resources/inference/configs/forecaster_no_trimedge_with_global.yaml diff --git a/config/forecasters-co1e.yaml b/config/forecasters-co1e.yaml index 490196c..76d8d70 100644 --- a/config/forecasters-co1e.yaml +++ b/config/forecasters-co1e.yaml @@ -4,8 +4,8 @@ description: | (KENDA-1) at 1km resolution. dates: - start: 2020-01-01T12:00 - end: 2020-01-10T00:00 + start: 2024-01-01T12:00 + end: 2024-01-10T00:00 frequency: 54h lead_time: 120h @@ -14,10 +14,12 @@ runs: - forecaster: mlflow_id: 2174c939c8844555a52843b71219d425 label: Cosmo 1km + era5 N320, finetuned on cerra checkpoint, lam resolution 11 - config: resources/inference/configs/forecaster_no_trimedge_fromtraining.yaml + config: resources/inference/configs/forecaster_no_trimedge_with_global.yaml inference_resources: gpu: 4 tasks: 4 + extra_dependencies: + - git+https://github.com/ecmwf/anemoi-inference@fix/interp_files baselines: - baseline: diff --git a/config/forecasters.yaml b/config/forecasters.yaml index 17d9872..6a51de8 100644 --- a/config/forecasters.yaml +++ b/config/forecasters.yaml @@ -5,18 +5,15 @@ description: | dates: start: 2020-01-01T12:00 end: 2020-01-10T00:00 - # end: 2020-03-30T00:00 frequency: 36h lead_time: 120h runs: - - forecaster: - mlflow_id: 2f962c89ff644ca7940072fa9cd088ec - label: Stage D - N320 global grid with CERRA finetuning - forecaster: mlflow_id: d0846032fc7248a58b089cbe8fa4c511 label: M-1 forecaster + config: resources/inference/configs/forecaster_with_global.yaml baselines: - baseline: diff --git a/resources/inference/configs/forecaster.yaml b/resources/inference/configs/forecaster.yaml index 59cdc1f..87cfb36 100644 --- a/resources/inference/configs/forecaster.yaml +++ b/resources/inference/configs/forecaster.yaml @@ -8,18 +8,18 @@ allow_nans: true output: tee: outputs: -# - extract_lam: -# output: -# assign_mask: -# mask: "source0/trimedge_mask" -# output: -# grib: -# path: grib/{dateTime}_{step:03}.grib -# encoding: -# typeOfGeneratingProcess: 2 -# templates: -# samples: _resources/templates_index_cosmo.yaml - - printer - - raw: raw/ + - extract_lam: + output: + assign_mask: + mask: "source0/trimedge_mask" + output: + grib: + path: grib/{dateTime}_{step:03}.grib + encoding: + typeOfGeneratingProcess: 2 + templates: + samples: _resources/templates_index_cosmo.yaml + - printer + - raw: raw/ write_initial_state: true diff --git a/resources/inference/configs/forecaster_no_trimedge_with_global.yaml b/resources/inference/configs/forecaster_no_trimedge_with_global.yaml new file mode 100644 index 0000000..9a13dd4 --- /dev/null +++ b/resources/inference/configs/forecaster_no_trimedge_with_global.yaml @@ -0,0 +1,28 @@ +input: + test: + use_original_paths: true + +allow_nans: true + +output: + tee: + outputs: + - extract_lam: + output: + grib: + path: grib/{dateTime}_{step:03}.grib + encoding: + typeOfGeneratingProcess: 2 + templates: + samples: _resources/templates_index_cosmo.yaml + - grib: + path: grib/ifs-{dateTime}_{step:03}.grib + encoding: + typeOfGeneratingProcess: 2 + templates: + samples: _resources/templates_index_ifs.yaml + post_processors: + - extract_slice: [919619, -1] + - assign_mask: "global/cutout_mask" + +write_initial_state: true diff --git a/resources/inference/configs/interpolator_from_test_data.yaml b/resources/inference/configs/interpolator_from_test_data.yaml index aaa938f..7594608 100644 --- a/resources/inference/configs/interpolator_from_test_data.yaml +++ b/resources/inference/configs/interpolator_from_test_data.yaml @@ -1,5 +1,4 @@ runner: time_interpolator -include_forcings: true input: test: @@ -17,10 +16,6 @@ output: templates: samples: _resources/templates_index_cosmo.yaml -forcings: - test: - use_original_paths: true - verbosity: 1 allow_nans: true output_frequency: "1h" diff --git a/workflow/Snakefile b/workflow/Snakefile index 06b6516..1d25d22 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -54,7 +54,7 @@ rule showcase_all: run_id=collect_all_runs(), param="2t", projection="orthographic", - region="switzerland", + region="none", ), diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 0bf7f2a..49a85e8 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -5,11 +5,12 @@ include: "common.smk" +import pandas as pd rule plot_forecast_frame: input: script="workflow/scripts/plot_forecast_frame.mo.py", - raw_output=rules.inference_routing.output[1], + raw_output=rules.inference_routing.output[0], output: temp( OUT_ROOT @@ -19,11 +20,10 @@ rule plot_forecast_frame: slurm_partition="postproc", cpus_per_task=1, runtime="5m", - # localrule: True shell: """ python {input.script} \ - --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ + --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]} \ --param {wildcards.param} --leadtime {wildcards.leadtime} \ --projection {wildcards.projection} --region {wildcards.region} \ # interactive editing (needs to set localrule: True and use only one core) diff --git a/workflow/scripts/plot_forecast_frame.mo.py b/workflow/scripts/plot_forecast_frame.mo.py index fe1fa83..b426fab 100644 --- a/workflow/scripts/plot_forecast_frame.mo.py +++ b/workflow/scripts/plot_forecast_frame.mo.py @@ -6,23 +6,23 @@ @app.cell def _(): - from argparse import ArgumentParser import logging - import numpy as np + from argparse import ArgumentParser from pathlib import Path import earthkit.plots as ekp - - from src.plotting import StatePlotter - from src.compat import load_state_from_raw + import numpy as np from src.colormap_defaults import CMAP_DEFAULTS + from src.compat import load_state_from_grib + from src.plotting import StatePlotter + return ( ArgumentParser, CMAP_DEFAULTS, Path, StatePlotter, ekp, - load_state_from_raw, + load_state_from_grib, logging, np, ) @@ -42,20 +42,20 @@ def _(ArgumentParser, Path): parser = ArgumentParser() - parser.add_argument("--input", type=str, default=None, help="Directory to raw data") + parser.add_argument("--input", type=str, default=None, help="Directory to grib data") parser.add_argument("--date", type=str, default=None, help="reference datetime") parser.add_argument("--outfn", type=str, help="output filename") parser.add_argument("--leadtime", type=str, help="leadtime") parser.add_argument("--param", type=str, help="parameter") parser.add_argument("--projection", type=str, help="projection") - parser.add_argument("--region", type=str, help="region") + parser.add_argument("--region", type=str, default="none", help="region (or 'none')") args = parser.parse_args() raw_dir = Path(args.input) outfn = Path(args.outfn) leadtime = int(args.leadtime) param = args.param - region = args.region + region = None if (args.region is None or str(args.region).lower() in {"none", "", "null"}) else args.region projection = args.projection return args, leadtime, outfn, param, projection, raw_dir, region @@ -63,40 +63,93 @@ def _(ArgumentParser, Path): @app.cell def _(raw_dir): # get all input files - raw_files = sorted(raw_dir.glob(f"*.npz")) - raw_files - return (raw_files,) + grib_files = sorted(raw_dir.glob("*.grib")) + return (grib_files,) @app.cell -def _(leadtime, load_state_from_raw, raw_files): +def _(leadtime, load_state_from_grib, grib_files, param): # TODO: do not hardcode leadtimes leadtimes = list(range(0, 126, 6)) file_index = leadtimes.index(leadtime) - state = load_state_from_raw(raw_files[file_index]) + state = load_state_from_grib(grib_files[file_index], paramlist=[param]) return (state,) @app.cell def _(CMAP_DEFAULTS, ekp): - def get_style(param): - """"Get style and colormap settings for the plot. - Needed because cmap/norm does not work in Style(colors=cmap), still needs - to be passed as arguments to tripcolor()/tricontourf(). + def get_style(param, units_override=None): + """Get style and colormap settings for the plot. + Needed because cmap/norm does not work in Style(colors=cmap), + still needs to be passed as arguments to tripcolor()/tricontourf(). """ cfg = CMAP_DEFAULTS[param] + units = units_override if units_override is not None else cfg.get("units", "") return { "style": ekp.styles.Style( levels=cfg.get("bounds", None), extend="both", - units=cfg.get("units", ""), + units=units, ), "cmap": cfg["cmap"], "norm": cfg.get("norm", None), } + return (get_style,) +@app.cell +def _(LOG, np): + """Preprocess fields with pint-based unit conversion and derived quantities.""" + try: + import pint # type: ignore + + _ureg = pint.UnitRegistry() + + def _k_to_c(arr): + # robust conversion with pint, fallback if dtype unsupported + try: + return (_ureg.Quantity(arr, _ureg.kelvin).to(_ureg.degC)).magnitude + except Exception: + return arr - 273.15 + + except Exception: + LOG.warning("pint not available; falling back to K->C by subtracting 273.15") + + def _k_to_c(arr): + return arr - 273.15 + + def preprocess_field(param: str, state: dict): + """ + - Temperatures (2t, 2d, t, d): K -> °C + - Wind speed at 10m (10sp): sqrt(10u^2 + 10v^2) + - Wind speed (sp): sqrt(u^2 + v^2) + Returns: (field_array, units_override or None) + """ + fields = state["fields"] + # temperature variables + if param in ("2t", "2d", "t", "d"): + return _k_to_c(fields[param]), "°C" + # 10m wind speed (allow legacy 'uv' alias) + if param == "10sp": + u = fields.get("10u") + v = fields.get("10v") + if u is None or v is None: + raise KeyError("Required components 10u/10v not in state['fields']") + return np.sqrt(u**2 + v**2), "m s$^{-1}$" + # wind speed from standard-level components + if param == "sp": + u = fields.get("u") + v = fields.get("v") + if u is None or v is None: + raise KeyError("Required components u/v not in state['fields']") + return np.sqrt(u**2 + v**2), "m s$^{-1}$" + # default: passthrough + return fields[param], None + + return (preprocess_field,) + + @app.cell def _( LOG, @@ -109,32 +162,27 @@ def _( projection, region, state, + preprocess_field, ): # plot individual fields plotter = StatePlotter( - state["longitudes"], - state["latitudes"], - outfn.parent, + state["longitudes"], + state["latitudes"], + outfn.parent, ) fig = plotter.init_geoaxes( - nrows=1, ncols=1, projection=projection, region=region, size=(8,8), + nrows=1, + ncols=1, + projection=projection, + region=region, + size=(8, 8), ) subplot = fig.add_map(row=0, column=0) - if param == "uv": - field = np.sqrt( - state["fields"]["10u"] ** 2 + state["fields"]["10v"] ** 2 - ) - elif param == "2t": - field = state["fields"][param] - 273.15 - else: - field = state["fields"][param] - - plotter.plot_field( - subplot, - field, - **get_style(args.param) - ) + # preprocess field (unit conversion, derived quantities) + field, units_override = preprocess_field(param, state) + + plotter.plot_field(subplot, field, **get_style(args.param, units_override)) validtime = state["valid_time"].strftime("%Y%m%d%H%M") # leadtime = int(state["lead_time"].total_seconds() // 3600) diff --git a/workflow/scripts/src/compat.py b/workflow/scripts/src/compat.py index f3dac74..304d333 100644 --- a/workflow/scripts/src/compat.py +++ b/workflow/scripts/src/compat.py @@ -1,10 +1,53 @@ from datetime import datetime from pathlib import Path +import earthkit.data as ekd import numpy as np +import pandas as pd +from anemoi.datasets.grids import cutout_mask +from meteodatalab import data_source +from meteodatalab import grib_decoder -def load_state_from_raw(file: Path, paramlist: list[str] | None = None) -> dict[str, np.ndarray | dict[str, np.ndarray]]: +def load_state_from_grib( + file: Path, paramlist: list[str] | None = None +) -> dict[str, np.ndarray | dict[str, np.ndarray]]: + reftime = datetime.strptime(file.parents[1].name, "%Y%m%d%H%M") + lead_time_hours = int(file.stem.split("_")[-1]) + fds = data_source.FileDataSource(datafiles=[str(file)]) + ds = grib_decoder.load(fds, {"param": paramlist}) + state = {} + lats = ds[paramlist[0]].lat.data.flatten() + lons = ds[paramlist[0]].lon.data.flatten() + state["forecast_reference_time"] = reftime + state["valid_time"] = reftime + pd.to_timedelta(lead_time_hours, unit="h") + state["longitudes"] = lons + state["latitudes"] = lats + state["fields"] = {} + for param in paramlist: + if param in ds: + state["fields"][param] = ds[param].values.flatten() + global_file = str(file.parent / f"ifs-{file.stem}.grib") + if Path(global_file).exists(): + global_file = str(file.parent / f"ifs-{file.stem}.grib") + fds_global = ekd.from_source("file", global_file) + ds_global = {u.metadata("param"): u.values for u in fds_global if u.metadata("param") in paramlist} + global_lats = fds_global.metadata("latitudes")[0] + global_lons = fds_global.metadata("longitudes")[0] + if max(global_lons) > 180: + global_lons = ((global_lons + 180) % 360) - 180 + mask = cutout_mask(lats, lons, global_lats, global_lons, cropping_distance=0) + state["longitudes"] = np.concatenate([state["longitudes"], global_lons[mask]]) + state["latitudes"] = np.concatenate([state["latitudes"], global_lats[mask]]) + for param in paramlist: + if param in ds and param in ds_global: + state["fields"][param] = np.concatenate([state["fields"][param], ds_global[param][mask]]) + return state + + +def load_state_from_raw( + file: Path, paramlist: list[str] | None = None +) -> dict[str, np.ndarray | dict[str, np.ndarray]]: _state: dict[str, np.ndarray] = np.load(file, allow_pickle=True) reftime = datetime.strptime(file.parents[1].name, "%Y%m%d%H%M") validtime = datetime.strptime(file.stem, "%Y%m%d%H%M%S") @@ -14,7 +57,7 @@ def load_state_from_raw(file: Path, paramlist: list[str] | None = None) -> dict[ state["valid_time"] = validtime state["lead_time"] = state["valid_time"] - reftime state["forecast_reference_time"] = reftime - state["fields"] = {} + state["fields"] = {} for key, value in _state.items(): if key.startswith("field_"): state["fields"][key.removeprefix("field_")] = value diff --git a/workflow/scripts/src/plotting.py b/workflow/scripts/src/plotting.py index 81ae427..e986f30 100644 --- a/workflow/scripts/src/plotting.py +++ b/workflow/scripts/src/plotting.py @@ -1,28 +1,26 @@ -import cartopy.crs as ccrs from functools import cached_property -import numpy as np -from matplotlib.tri import Triangulation -import typing as tp from pathlib import Path +import cartopy.crs as ccrs import earthkit.plots as ekp - +import numpy as np +from matplotlib.tri import Triangulation State = dict[str, np.ndarray | dict[str, np.ndarray]] PROJECTIONS: dict[str, ccrs.Projection] = { - "plate_carree": ccrs.PlateCarree(), + "platecarree": ccrs.PlateCarree(), "orthographic": ccrs.Orthographic(central_longitude=5.0, central_latitude=45.0), # added some pojections to test the behaviour, can be deleted later - "rotated_latlon": ccrs.RotatedPole(pole_longitude=-170.0, pole_latitude=43.0), - "azimuthal_equidist": ccrs.AzimuthalEquidistant(), + "rotatedlatlon": ccrs.RotatedPole(pole_longitude=-170.0, pole_latitude=43.0), + "azimuthalequidist": ccrs.AzimuthalEquidistant(), } """Mapping of projection names to their cartopy projection objects.""" REGION_EXTENTS = { # coordinate reference system: PlateCarree() "europe": [-16.0, 25.0, 30.0, 65.0], - "central_europe": [-2.6, 19.5, 40.2, 52.3], - "switzerland": [-1.5, 17.5, 40.5, 53.0], + "centraleurope": [-2.6, 19.5, 40.2, 52.3], + "switzerland": [0, 17.5, 40.5, 53.0], } """Mapping of region names to their extents.""" @@ -55,7 +53,6 @@ def __init__( self.lon = lon self.lat = lat - out_dir.mkdir(exist_ok=True, parents=True) self.out_dir = out_dir @@ -68,7 +65,7 @@ def init_geoaxes( projection: str = "orthographic", size: tuple[float] | None = None, region: str | None = None, - ) -> ekp.Figure: + ) -> ekp.Figure: """Initialize a figure with GeoAxes for plotting fields. Parameters @@ -93,15 +90,18 @@ def init_geoaxes( proj = PROJECTIONS.get(projection, PROJECTIONS["orthographic"]) domain = None - if region: - domain = ekp.geo.domains.Domain( - bbox=REGION_EXTENTS[region], - crs=ccrs.PlateCarree(), # coordinate reference system of the region coords - name=region - ) + # Use a map domain only if region is set and known; accept "none"/None for no clipping + if region is not None and str(region).lower() not in {"none", "", "null"}: + bbox = REGION_EXTENTS.get(region) + if bbox is not None: + domain = ekp.geo.domains.Domain( + bbox=bbox, + crs=ccrs.PlateCarree(), + name=region, + ) ekp_fig = ekp.Figure( - crs=proj, # coordinate reference system of the map + crs=proj, domain=domain, rows=nrows, columns=ncols, @@ -110,59 +110,13 @@ def init_geoaxes( self.fig = ekp_fig return ekp_fig - # def init_geoaxes( - # self, - # nrows: int = 1, - # ncols: int = 1, - # projection: str = "orthographic", - # region: str | None = None, - # coastlines: bool = True, - # **kwargs, - # ) -> tuple[plt.Figure, tp.Sequence[GeoAxes]]: - # """Initialize a figure with GeoAxes for plotting fields. - - # Parameters - # ---------- - # nrows : int, optional - # The number of rows in the figure, by default 1. - # ncols : int, optional - # The number of columns in the figure, by default 1. - # projection : str, optional - # The projection of the map, by default "orthographic". - # region : str, optional - # The region to plot, by default None. - # coastlines : bool, optional - # Whether to plot coastlines, by default True. - - # Returns - # ------- - # tuple[plt.Figure, tp.Sequence[GeoAxes]] - # The figure and the GeoAxes objects. - # """ - - # proj = PROJECTIONS.get(projection, PROJECTIONS["orthographic"]) - # fig, ax = plt.subplots(nrows, ncols, subplot_kw={"projection": proj}) - # ax: GeoAxes | tp.Sequence[GeoAxes] = ( - # [ax] if nrows == 1 and ncols == 1 else ax.ravel() - # ) - - # for i in range(nrows * ncols): - # ax[i].set_global() - # if coastlines: - # ax[i].coastlines() - - # if region != "globe": - # ax[i].set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) - - # return fig, ax - def plot_field( self, subplot: ekp.Map, field: np.ndarray, style: ekp.styles.Style | None = None, colorbar: bool = True, - title: str | None= None, + title: str | None = None, **kwargs, ): """Plot a field on a Map object. @@ -184,20 +138,21 @@ def plot_field( vmin, vmax, etc. """ - proj = subplot.ax.projection - + proj = subplot._crs # transform data coordinates to map coordinate reference system outside # of the plotting function is a lot faster than letting tricontourf or # tripcolor handle it in general, but not sure if using earthkit # removed for now to simplify the workflow - #if proj == PROJECTIONS["orthographic"]: - # triang, mask = self._ortographic_tri - #else: - # triang, mask = self.tri, slice(None, None) - + if proj == PROJECTIONS["orthographic"]: + triang, mask = self._orthographic_tri + else: + triang, mask = self.tri, slice(None, None) + x, y = triang.x, triang.y # TODO: this is hardcoded for when the initial state has two timesteps # need to ditch this later + field = field[mask] field = field[-1] if field.ndim == 2 else field.squeeze() + finite = np.isfinite(field) # TODO: clip data to domain would make plotting faster (especially tripcolor) # tried using Map.domain.extract() but too memory heavy (probably uses @@ -210,12 +165,16 @@ def plot_field( # guess is that the earthkit-plots check throwing: # "ValueError: x and y arrays must have the same length" is incorrect, # therefore using x,y here - #subplot.tripcolor( # also works but is slower + # subplot.tripcolor( # also works but is slower + # have to overwrite _plot_kwargs to avoid earthkit-plots trying to pass transform + # PlateCarree based on NumpySource + subplot._plot_kwargs = lambda source: {} subplot.tricontourf( - x=self.lon, - y=self.lat, - z=field, + x=x[finite], + y=y[finite], + z=field[finite], style=style, + transform=proj, **kwargs, # for earthkit.plots to work properly cmap and norm are needed here ) # TODO: gridlines etc would be nicer to have in the init, but I didn't get @@ -227,77 +186,9 @@ def plot_field( if title: subplot.title(title) - - # def plot_field( - # self, - # ax: GeoAxes, - # field: np.ndarray, - # region: str | None = None, - # validtime: str = "", - # colorbar: dict | bool = True, - # **kwargs, - # ): - # """Plot a field on a GeoAxes object. - - # Parameters - # ---------- - # ax : GeoAxes - # The GeoAxes object to plot on. - # field : np.ndarray - # The field to plot. - # region : str, optional - # The region to plot, by default None. - # colorbar : dict | bool, optional - # Whether to plot a colorbar, by default True. - # If a dictionary, it is passed as keyword arguments to plt.colorbar. - # kwargs : dict - # Additional keyword arguments to pass to ax.tripcolor, including cmap, - # vmin, vmax, etc. - # """ - - # proj = ax.projection - - # if proj == PROJECTIONS["orthographic"]: - # triang, mask = self._ortographic_tri - # else: - # triang, mask = self.tri, slice(None, None) - - # # TODO: this is hardcoded for when the initial state has two timesteps - # # need to ditch this later - # field = field[-1] if field.ndim == 2 else field.squeeze() - - # im = ax.tripcolor(triang, field[mask], **kwargs) - # ax.text( - # 0.05, - # 0.95, - # f"Time: {validtime}", - # transform=ax.transAxes, - # fontsize=12, - # color="white", - # verticalalignment="top", - # bbox=dict(facecolor="black", alpha=0.5), - # ) - - # if region and region != "globe": - # ax.set_extent(REGION_EXTENTS[region], crs=ccrs.PlateCarree()) - - # if colorbar: - # colorbar = { - # "orientation": "horizontal", - # "pad": 0.04, - # "aspect": 45, - # "extend": "both", - # "shrink": 0.75, - # } | (colorbar if isinstance(colorbar, dict) else {}) - # plt.colorbar(im, **colorbar) - @cached_property - def _ortographic_tri(self) -> Triangulation: + def _orthographic_tri(self) -> Triangulation: """Compute the triangulation for the orthographic projection.""" - x, y, _ = ( - PROJECTIONS["orthographic"] - .transform_points(ccrs.PlateCarree(), self.lon, self.lat) - .T - ) + x, y, _ = PROJECTIONS["orthographic"].transform_points(ccrs.PlateCarree(), self.lon, self.lat).T mask = ~(np.isnan(x) | np.isnan(y)) return Triangulation(x[mask], y[mask]), mask From 7262e324f69ec5e7ab2d7cce37b6f3260bba98e1 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 14 Oct 2025 22:41:54 +0200 Subject: [PATCH 21/35] Set path to eccodes defintions --- workflow/rules/plot.smk | 1 + 1 file changed, 1 insertion(+) diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 49a85e8..5397d26 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -22,6 +22,7 @@ rule plot_forecast_frame: runtime="5m", shell: """ + export ECCODES_DEFINITION_PATH=/user-environment/share/eccodes-cosmo-resources/definitions python {input.script} \ --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]} \ --param {wildcards.param} --leadtime {wildcards.leadtime} \ From 77b3893a26d4909ed9ca3af64c4987d6abbb6966 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 14 Oct 2025 22:45:29 +0200 Subject: [PATCH 22/35] Add Switzerland region to outputs --- workflow/Snakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 1d25d22..00ccc97 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -54,7 +54,7 @@ rule showcase_all: run_id=collect_all_runs(), param="2t", projection="orthographic", - region="none", + region=["none", "switzerland"], ), From 8fa6c67778ae52558c0c4480c17d544248985a99 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 15 Oct 2025 08:47:38 +0200 Subject: [PATCH 23/35] Batch plot jobs in larger submissions --- config/showcase.yaml | 2 ++ src/evalml/config.py | 13 +++++++++++++ workflow/tools/config.schema.json | 8 ++++++++ 3 files changed, 23 insertions(+) diff --git a/config/showcase.yaml b/config/showcase.yaml index 97ed8dd..28b1a6a 100644 --- a/config/showcase.yaml +++ b/config/showcase.yaml @@ -43,3 +43,5 @@ profile: runtime: "1h" gpus: 0 jobs: 50 + batch_rules: + plot_forecast_frame: 32 diff --git a/src/evalml/config.py b/src/evalml/config.py index c6bcf1e..33c4611 100644 --- a/src/evalml/config.py +++ b/src/evalml/config.py @@ -221,6 +221,10 @@ class Profile(BaseModel): global_resources: GlobalResources default_resources: DefaultResources jobs: int = Field(..., ge=1, description="Maximum number of parallel jobs.") + batch_rules: Dict[str, int] = Field( + default_factory=dict, + description="Define batches of the same rule that shall be executed within one job submission.", + ) def parsable(self) -> Dict[str, str]: """Convert the profile to a dictionary of command-line arguments.""" @@ -229,6 +233,15 @@ def parsable(self) -> Dict[str, str]: out += ["--resources"] + self.global_resources.parsable() out += ["--default-resources"] + self.default_resources.parsable() out += ["--jobs", str(self.jobs)] + + # Add rule grouping options if specified + if self.batch_rules: + # Groups: rule=rule + groups = [f"{rule}={rule}" for rule in self.batch_rules.keys()] + # Group components: rule= + components = [f"{rule}={n}" for rule, n in self.batch_rules.items()] + out += ["--groups"] + groups + out += ["--group-components"] + components return out diff --git a/workflow/tools/config.schema.json b/workflow/tools/config.schema.json index 5b3216c..30f96bd 100644 --- a/workflow/tools/config.schema.json +++ b/workflow/tools/config.schema.json @@ -478,6 +478,14 @@ "minimum": 1, "title": "Jobs", "type": "integer" + }, + "batch_rules": { + "additionalProperties": { + "type": "integer" + }, + "description": "Define batches of the same rule that shall be executed within one job submission.", + "title": "Batch Rules", + "type": "object" } }, "required": [ From d2be5cabbec66199ec06f7e6cccd11da8f0fbe5c Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 21:18:17 +0200 Subject: [PATCH 24/35] Fix pre-commits --- resources/inference/configs/forecaster.yaml | 2 +- resources/report/plotting/README | 2 +- resources/report/plotting/RH_6lev.ct | 2 +- resources/report/plotting/t2m_29lev.ct | 2 +- resources/report/plotting/uv_17lev.ct | 2 +- workflow/rules/plot.smk | 2 ++ workflow/scripts/plot_forecast_frame.mo.py | 10 ++++++++-- 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/resources/inference/configs/forecaster.yaml b/resources/inference/configs/forecaster.yaml index 48cd8ae..51ece47 100644 --- a/resources/inference/configs/forecaster.yaml +++ b/resources/inference/configs/forecaster.yaml @@ -6,7 +6,7 @@ allow_nans: true output: tee: - outputs: + outputs: - extract_lam: output: assign_mask: diff --git a/resources/report/plotting/README b/resources/report/plotting/README index b6a0a25..e127246 100644 --- a/resources/report/plotting/README +++ b/resources/report/plotting/README @@ -1,2 +1,2 @@ # This is a copy of the ncl colortables developed in the cosmolib -# SRC: https://github.com/MeteoSwiss-APN/mch-ncl/tree/master/cosmolib/ct \ No newline at end of file +# SRC: https://github.com/MeteoSwiss-APN/mch-ncl/tree/master/cosmolib/ct diff --git a/resources/report/plotting/RH_6lev.ct b/resources/report/plotting/RH_6lev.ct index 1adc5a1..ce17e49 100644 --- a/resources/report/plotting/RH_6lev.ct +++ b/resources/report/plotting/RH_6lev.ct @@ -8,4 +8,4 @@ 206 254 154 120 240 116 55 202 51 - 54 177 52 \ No newline at end of file + 54 177 52 diff --git a/resources/report/plotting/t2m_29lev.ct b/resources/report/plotting/t2m_29lev.ct index 7f52605..800f19c 100644 --- a/resources/report/plotting/t2m_29lev.ct +++ b/resources/report/plotting/t2m_29lev.ct @@ -31,4 +31,4 @@ 188 75 0 171 0 56 128 0 0 -163 112 255 \ No newline at end of file +163 112 255 diff --git a/resources/report/plotting/uv_17lev.ct b/resources/report/plotting/uv_17lev.ct index 769d11c..cee8efa 100644 --- a/resources/report/plotting/uv_17lev.ct +++ b/resources/report/plotting/uv_17lev.ct @@ -20,4 +20,4 @@ 99 112 247 127 150 255 142 178 255 -181 201 255 \ No newline at end of file +181 201 255 diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 5397d26..f34f320 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -5,8 +5,10 @@ include: "common.smk" + import pandas as pd + rule plot_forecast_frame: input: script="workflow/scripts/plot_forecast_frame.mo.py", diff --git a/workflow/scripts/plot_forecast_frame.mo.py b/workflow/scripts/plot_forecast_frame.mo.py index 932e4b7..fccd7b9 100644 --- a/workflow/scripts/plot_forecast_frame.mo.py +++ b/workflow/scripts/plot_forecast_frame.mo.py @@ -43,7 +43,9 @@ def _(ArgumentParser, Path): parser = ArgumentParser() - parser.add_argument("--input", type=str, default=None, help="Directory to grib data") + parser.add_argument( + "--input", type=str, default=None, help="Directory to grib data" + ) parser.add_argument("--date", type=str, default=None, help="reference datetime") parser.add_argument("--outfn", type=str, help="output filename") parser.add_argument("--leadtime", type=str, help="leadtime") @@ -56,7 +58,11 @@ def _(ArgumentParser, Path): outfn = Path(args.outfn) leadtime = int(args.leadtime) param = args.param - region = None if (args.region is None or str(args.region).lower() in {"none", "", "null"}) else args.region + region = ( + None + if (args.region is None or str(args.region).lower() in {"none", "", "null"}) + else args.region + ) projection = args.projection return args, leadtime, outfn, param, projection, raw_dir, region From 9df35a684eb201a05fad3e19a126005c746171aa Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 21:20:29 +0200 Subject: [PATCH 25/35] Revert change in co1e config --- config/forecasters-co1e.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/forecasters-co1e.yaml b/config/forecasters-co1e.yaml index 96f4797..84943b7 100644 --- a/config/forecasters-co1e.yaml +++ b/config/forecasters-co1e.yaml @@ -17,8 +17,6 @@ runs: inference_resources: gpu: 4 tasks: 4 - extra_dependencies: - - git+https://github.com/ecmwf/anemoi-inference@fix/interp_files baselines: - baseline: From 257c7487bb25cf2ed54e4207bd60dd610aeca4d7 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 21:54:55 +0200 Subject: [PATCH 26/35] Parse steps --- workflow/rules/plot.smk | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index f34f320..85b9d18 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -37,7 +37,10 @@ rule plot_forecast_frame: """ -LEADTIME = int(pd.to_timedelta(config["lead_time"]).total_seconds() // 3600) +def get_leadtimes(wc): + """Get all lead times from the run config.""" + start, end, step = map(int, RUN_CONFIGS[wc.run_id]["steps"].split("/")) + return [f"{i:03}" for i in range(start, end + 1, step)] rule make_forecast_animation: @@ -45,7 +48,7 @@ rule make_forecast_animation: expand( OUT_ROOT / "showcases/{run_id}/{init_time}/frames/{init_time}_{leadtime}_{param}_{projection}_{region}.png", - leadtime=[f"{i:03}" for i in range(0, LEADTIME + 6, 6)], + leadtime=lambda wc: get_leadtimes(wc), allow_missing=True, ), output: From ef00ccc8660e54b81fdaecfbd4cc9204a7c0140b Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 22:17:37 +0200 Subject: [PATCH 27/35] Do not hardcode leadtimes --- workflow/rules/plot.smk | 6 ++-- workflow/scripts/plot_forecast_frame.mo.py | 38 ++++++++++------------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 85b9d18..187e23a 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -12,7 +12,7 @@ import pandas as pd rule plot_forecast_frame: input: script="workflow/scripts/plot_forecast_frame.mo.py", - raw_output=rules.inference_routing.output[0], + grib_output=rules.inference_routing.output[0], output: temp( OUT_ROOT @@ -26,12 +26,12 @@ rule plot_forecast_frame: """ export ECCODES_DEFINITION_PATH=/user-environment/share/eccodes-cosmo-resources/definitions python {input.script} \ - --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]} \ + --input {input.grib_output} --date {wildcards.init_time} --outfn {output[0]} \ --param {wildcards.param} --leadtime {wildcards.leadtime} \ --projection {wildcards.projection} --region {wildcards.region} \ # interactive editing (needs to set localrule: True and use only one core) # marimo edit {input.script} -- \ - # --input {input.raw_output} --date {wildcards.init_time} --outfn {output[0]}\ + # --input {input.grib_output} --date {wildcards.init_time} --outfn {output[0]}\ # --param {wildcards.param} --leadtime {wildcards.leadtime} \ # --projection {wildcards.projection} --region {wildcards.region} \ """ diff --git a/workflow/scripts/plot_forecast_frame.mo.py b/workflow/scripts/plot_forecast_frame.mo.py index fccd7b9..c0cfde9 100644 --- a/workflow/scripts/plot_forecast_frame.mo.py +++ b/workflow/scripts/plot_forecast_frame.mo.py @@ -16,7 +16,6 @@ def _(): from plotting import StatePlotter from plotting.colormap_defaults import CMAP_DEFAULTS from plotting.compat import load_state_from_grib - return ( ArgumentParser, CMAP_DEFAULTS, @@ -54,9 +53,10 @@ def _(ArgumentParser, Path): parser.add_argument("--region", type=str, default="none", help="region (or 'none')") args = parser.parse_args() - raw_dir = Path(args.input) + grib_dir = Path(args.input) + init_time = args.date outfn = Path(args.outfn) - leadtime = int(args.leadtime) + lead_time = args.leadtime param = args.param region = ( None @@ -64,22 +64,23 @@ def _(ArgumentParser, Path): else args.region ) projection = args.projection - return args, leadtime, outfn, param, projection, raw_dir, region - - -@app.cell -def _(raw_dir): - # get all input files - grib_files = sorted(raw_dir.glob("*.grib")) - return (grib_files,) + return ( + args, + grib_dir, + init_time, + lead_time, + outfn, + param, + projection, + region, + ) @app.cell -def _(leadtime, load_state_from_grib, grib_files, param): - # TODO: do not hardcode leadtimes - leadtimes = list(range(0, 126, 6)) - file_index = leadtimes.index(leadtime) - state = load_state_from_grib(grib_files[file_index], paramlist=[param]) +def _(grib_dir, init_time, lead_time, load_state_from_grib, param): + # load grib file + grib_file = grib_dir / f"{init_time}_{lead_time}.grib" + state = load_state_from_grib(grib_file, paramlist=[param]) return (state,) @@ -101,7 +102,6 @@ def get_style(param, units_override=None): "cmap": cfg["cmap"], "norm": cfg.get("norm", None), } - return (get_style,) @@ -153,7 +153,6 @@ def preprocess_field(param: str, state: dict): return np.sqrt(u**2 + v**2), "m s$^{-1}$" # default: passthrough return fields[param], None - return (preprocess_field,) @@ -163,13 +162,12 @@ def _( StatePlotter, args, get_style, - np, outfn, param, + preprocess_field, projection, region, state, - preprocess_field, ): # plot individual fields plotter = StatePlotter( From 73066944ff80e29fa3d6471ce86eb5b14b93991b Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 22:52:16 +0200 Subject: [PATCH 28/35] Plot wind speed I'm assuming the default colormap is in knots --- src/plotting/colormap_defaults.py | 6 ++-- workflow/Snakefile | 2 +- workflow/scripts/plot_forecast_frame.mo.py | 40 ++++++++++++++-------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/plotting/colormap_defaults.py b/src/plotting/colormap_defaults.py index 9fd411e..29cafec 100644 --- a/src/plotting/colormap_defaults.py +++ b/src/plotting/colormap_defaults.py @@ -15,9 +15,9 @@ def _fallback(): "sp": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 800 * 100, "vmax": 1100 * 100}, "2d": load_ncl_colormap("t2m_29lev.ct"), "2t": load_ncl_colormap("t2m_29lev.ct") | {"units": "degC"}, - "10v": load_ncl_colormap("uv_17lev.ct") | {"units": "m/s"}, - "10u": load_ncl_colormap("uv_17lev.ct") | {"units": "m/s"}, - "uv": load_ncl_colormap("uv_17lev.ct"), + "10v": load_ncl_colormap("uv_17lev.ct") | {"units": "kt"}, + "10u": load_ncl_colormap("uv_17lev.ct") | {"units": "kt"}, + "10sp": load_ncl_colormap("uv_17lev.ct") | {"units": "kt"}, "10si": {"cmap": plt.get_cmap("GnBu", 11), "vmin": 0, "vmax": 25}, "t_850": {"cmap": plt.get_cmap("inferno", 11), "vmin": 220, "vmax": 310}, "z_850": {"cmap": plt.get_cmap("coolwarm", 11), "vmin": 8000, "vmax": 17000}, diff --git a/workflow/Snakefile b/workflow/Snakefile index 778a2ac..fae5d9d 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -52,7 +52,7 @@ rule showcase_all: / "showcases/{run_id}/{init_time}/{init_time}_{param}_{projection}_{region}.gif", init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES], run_id=collect_all_runs(), - param="2t", + param=["2t", "10sp"], projection="orthographic", region=["none", "switzerland"], ), diff --git a/workflow/scripts/plot_forecast_frame.mo.py b/workflow/scripts/plot_forecast_frame.mo.py index c0cfde9..6d2ccbc 100644 --- a/workflow/scripts/plot_forecast_frame.mo.py +++ b/workflow/scripts/plot_forecast_frame.mo.py @@ -80,7 +80,13 @@ def _(ArgumentParser, Path): def _(grib_dir, init_time, lead_time, load_state_from_grib, param): # load grib file grib_file = grib_dir / f"{init_time}_{lead_time}.grib" - state = load_state_from_grib(grib_file, paramlist=[param]) + if param == "10sp": + paramlist = ["10u", "10v"] + elif param == "sp": + paramlist = ["u", "v"] + else: + paramlist = [param] + state = load_state_from_grib(grib_file, paramlist=paramlist) return (state,) @@ -120,17 +126,27 @@ def _k_to_c(arr): except Exception: return arr - 273.15 + def _ms_to_knots(arr): + # robust conversion with pint, fallback if dtype unsupported + try: + return (_ureg.Quantity(arr, _ureg.meter / _ureg.second).to(_ureg.knot)).magnitude + except Exception: + return arr * 1.943844 + except Exception: - LOG.warning("pint not available; falling back to K->C by subtracting 273.15") + LOG.warning("pint not available; falling back hardcoded conversions") def _k_to_c(arr): return arr - 273.15 + def _ms_to_knots(arr): + return arr * 1.943844 + def preprocess_field(param: str, state: dict): """ - Temperatures (2t, 2d, t, d): K -> °C - - Wind speed at 10m (10sp): sqrt(10u^2 + 10v^2) - - Wind speed (sp): sqrt(u^2 + v^2) + - Wind speed at 10m (10sp): m/s -> kn, sqrt(10u^2 + 10v^2) + - Wind speed (sp): m/s -> kn, sqrt(u^2 + v^2) Returns: (field_array, units_override or None) """ fields = state["fields"] @@ -139,18 +155,14 @@ def preprocess_field(param: str, state: dict): return _k_to_c(fields[param]), "°C" # 10m wind speed (allow legacy 'uv' alias) if param == "10sp": - u = fields.get("10u") - v = fields.get("10v") - if u is None or v is None: - raise KeyError("Required components 10u/10v not in state['fields']") - return np.sqrt(u**2 + v**2), "m s$^{-1}$" + u = _ms_to_knots(fields["10u"]) + v = _ms_to_knots(fields["10v"]) + return np.sqrt(u**2 + v**2), "kn" # wind speed from standard-level components if param == "sp": - u = fields.get("u") - v = fields.get("v") - if u is None or v is None: - raise KeyError("Required components u/v not in state['fields']") - return np.sqrt(u**2 + v**2), "m s$^{-1}$" + u = _ms_to_knots(fields["u"]) + v = _ms_to_knots(fields["v"]) + return np.sqrt(u**2 + v**2), "kn" # default: passthrough return fields[param], None return (preprocess_field,) From 4b56b35bb14f438c28a1cd3fb01c16d6fb2d83bc Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 23:13:11 +0200 Subject: [PATCH 29/35] Showcase M-2 model --- config/showcase.yaml | 22 +++++++++++++++------- workflow/rules/plot.smk | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/config/showcase.yaml b/config/showcase.yaml index eb44385..41e24af 100644 --- a/config/showcase.yaml +++ b/config/showcase.yaml @@ -4,15 +4,23 @@ description: | dates: - 2020-02-03T00:00 # Storm Petra - - 2020-02-07T00:00 # Storm Sabine - - 2020-10-01T00:00 # Storm Brigitte + - 2020-02-07T00:00 # Storm Sabine + - 2020-10-01T00:00 # Storm Brigitte runs: - - forecaster: - mlflow_id: d0846032fc7248a58b089cbe8fa4c511 - label: M-1 forecaster - config: resources/inference/configs/forecaster_with_global.yaml - steps: 0/120/6 + - interpolator: + mlflow_id: 8d1e0410ca7d4f74b368b3079878259a + label: M-2 interpolator + steps: 0/120/1 + config: resources/inference/configs/interpolator_stretched.yaml + forecaster: + mlflow_id: d0846032fc7248a58b089cbe8fa4c511 + config: resources/inference/configs/forecaster_with_global.yaml + steps: 0/120/6 + extra_dependencies: + - git+https://github.com/ecmwf/anemoi-inference@14189907b4f4e3b204b7994f828831b8aa51e9b6 + - torch-geometric==2.6.1 + - anemoi-graphs==0.5.2 baselines: - baseline: diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 187e23a..1868fb5 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -57,5 +57,5 @@ rule make_forecast_animation: localrule: True shell: """ - convert -delay 80 -loop 0 {input} {output} + convert -delay 40 -loop 0 {input} {output} """ From e50e6b236925b1e4335a21c1d7d94d71d7955772 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Tue, 21 Oct 2025 23:27:58 +0200 Subject: [PATCH 30/35] Fix pre-commit --- workflow/scripts/plot_forecast_frame.mo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/workflow/scripts/plot_forecast_frame.mo.py b/workflow/scripts/plot_forecast_frame.mo.py index 6d2ccbc..321040f 100644 --- a/workflow/scripts/plot_forecast_frame.mo.py +++ b/workflow/scripts/plot_forecast_frame.mo.py @@ -16,6 +16,7 @@ def _(): from plotting import StatePlotter from plotting.colormap_defaults import CMAP_DEFAULTS from plotting.compat import load_state_from_grib + return ( ArgumentParser, CMAP_DEFAULTS, @@ -108,6 +109,7 @@ def get_style(param, units_override=None): "cmap": cfg["cmap"], "norm": cfg.get("norm", None), } + return (get_style,) @@ -129,7 +131,9 @@ def _k_to_c(arr): def _ms_to_knots(arr): # robust conversion with pint, fallback if dtype unsupported try: - return (_ureg.Quantity(arr, _ureg.meter / _ureg.second).to(_ureg.knot)).magnitude + return ( + _ureg.Quantity(arr, _ureg.meter / _ureg.second).to(_ureg.knot) + ).magnitude except Exception: return arr * 1.943844 @@ -165,6 +169,7 @@ def preprocess_field(param: str, state: dict): return np.sqrt(u**2 + v**2), "kn" # default: passthrough return fields[param], None + return (preprocess_field,) From bf07cf71148e6d3c3de2f553f3a663ab2aca5e15 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 22 Oct 2025 07:49:58 +0200 Subject: [PATCH 31/35] Add global outputs to interpolator showcase --- config/showcase.yaml | 6 +- .../interpolator_stretched_with_global.yaml | 68 +++++++++++++++++++ workflow/rules/plot.smk | 2 +- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 resources/inference/configs/interpolator_stretched_with_global.yaml diff --git a/config/showcase.yaml b/config/showcase.yaml index 41e24af..458e6b5 100644 --- a/config/showcase.yaml +++ b/config/showcase.yaml @@ -4,15 +4,15 @@ description: | dates: - 2020-02-03T00:00 # Storm Petra - - 2020-02-07T00:00 # Storm Sabine - - 2020-10-01T00:00 # Storm Brigitte + - 2020-02-07T00:00 # Storm Sabine + - 2020-10-01T00:00 # Storm Brigitte runs: - interpolator: mlflow_id: 8d1e0410ca7d4f74b368b3079878259a label: M-2 interpolator steps: 0/120/1 - config: resources/inference/configs/interpolator_stretched.yaml + config: resources/inference/configs/interpolator_stretched_with_global.yaml forecaster: mlflow_id: d0846032fc7248a58b089cbe8fa4c511 config: resources/inference/configs/forecaster_with_global.yaml diff --git a/resources/inference/configs/interpolator_stretched_with_global.yaml b/resources/inference/configs/interpolator_stretched_with_global.yaml new file mode 100644 index 0000000..662f067 --- /dev/null +++ b/resources/inference/configs/interpolator_stretched_with_global.yaml @@ -0,0 +1,68 @@ +# yaml-language-server: $schema=../workflow/tools/config.schema.json +description: | + Evaluate skill of SGM interpolator (M-2 interpolator). + +dates: + start: 2020-01-01T12:00 + end: 2020-01-10T00:00 + frequency: 60h + +runs: + - interpolator: + mlflow_id: 8d1e0410ca7d4f74b368b3079878259a + label: M-2 interpolator (KENDA) + steps: 0/120/1 + config: resources/inference/configs/interpolator_from_test_data_stretched.yaml + forecaster: null + extra_dependencies: + - git+https://github.com/ecmwf/anemoi-inference@14189907b4f4e3b204b7994f828831b8aa51e9b6 + - torch-geometric==2.6.1 + - anemoi-graphs==0.5.2 + - interpolator: + mlflow_id: 8d1e0410ca7d4f74b368b3079878259a + label: M-2 interpolator (M-1 forecaster) + steps: 0/120/1 + config: resources/inference/configs/interpolator_stretched.yaml + forecaster: + mlflow_id: d0846032fc7248a58b089cbe8fa4c511 + config: resources/inference/configs/forecaster_with_global.yaml + steps: 0/120/6 + extra_dependencies: + - git+https://github.com/ecmwf/anemoi-inference@14189907b4f4e3b204b7994f828831b8aa51e9b6 + - torch-geometric==2.6.1 + - anemoi-graphs==0.5.2 + - forecaster: + mlflow_id: d0846032fc7248a58b089cbe8fa4c511 + label: M-1 forecaster + config: resources/inference/configs/forecaster_with_global.yaml + steps: 0/120/6 + +baselines: + - baseline: + baseline_id: COSMO-E-1h + label: COSMO-E + root: /scratch/mch/bhendj/COSMO-E + steps: 0/120/1 + +analysis: + label: COSMO KENDA + analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-1h-v3-pl13.zarr + +locations: + output_root: output/ + mlflow_uri: + # - https://service.meteoswiss.ch/mlstore + - https://servicedepl.meteoswiss.ch/mlstore + - https://mlflow.ecmwf.int + +profile: + executor: slurm + global_resources: + gpus: 16 + default_resources: + slurm_partition: "postproc" + cpus_per_task: 1 + mem_mb_per_cpu: 1800 + runtime: "1h" + gpus: 0 + jobs: 50 diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 1868fb5..98f9df9 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -57,5 +57,5 @@ rule make_forecast_animation: localrule: True shell: """ - convert -delay 40 -loop 0 {input} {output} + convert -delay 10 -loop 0 {input} {output} """ From a20ae758504e674f8ac6abead6fa59d3b51a0af0 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 22 Oct 2025 08:02:52 +0200 Subject: [PATCH 32/35] Fix wrong inference config... --- .../interpolator_stretched_with_global.yaml | 174 +++++++++++------- 1 file changed, 111 insertions(+), 63 deletions(-) diff --git a/resources/inference/configs/interpolator_stretched_with_global.yaml b/resources/inference/configs/interpolator_stretched_with_global.yaml index 662f067..f81ffaa 100644 --- a/resources/inference/configs/interpolator_stretched_with_global.yaml +++ b/resources/inference/configs/interpolator_stretched_with_global.yaml @@ -1,68 +1,116 @@ -# yaml-language-server: $schema=../workflow/tools/config.schema.json -description: | - Evaluate skill of SGM interpolator (M-2 interpolator). +runner: time_interpolator -dates: - start: 2020-01-01T12:00 - end: 2020-01-10T00:00 - frequency: 60h +input: + cutout: + lam_0: + grib: + path: forecaster_grib/20* + pre_processors: + - extract_mask: "source0/trimedge_mask" + namer: + rules: + - - shortName: T + - t_{level} + - - shortName: U + - u_{level} + - - shortName: V + - v_{level} + - - shortName: W + - w_{level} + - - shortName: QV + - q_{level} + - - shortName: FI + - z_{level} + - - shortName: PMSL + - msl + - - shortName: FIS + - z + - - shortName: PS + - sp + - - shortName: T_2M + - 2t + - - shortName: TD_2M + - 2d + - - shortName: T_G + - skt + - - shortName: U_10M + - 10u + - - shortName: V_10M + - 10v + - - shortName: FR_LAND + - lsm + - - shortName: TOT_PREC + - tp + global: + grib: + path: forecaster_grib/ifs* + namer: + rules: + - - shortName: T + - t_{level} + - - shortName: U + - u_{level} + - - shortName: V + - v_{level} + - - shortName: W + - w_{level} + - - shortName: QV + - q_{level} + - - shortName: FI + - z_{level} + - - shortName: PMSL + - msl + - - shortName: FIS + - z + - - shortName: PS + - sp + - - shortName: T_2M + - 2t + - - shortName: TD_2M + - 2d + - - shortName: T_G + - skt + - - shortName: U_10M + - 10u + - - shortName: V_10M + - 10v + - - shortName: FR_LAND + - lsm + - - shortName: TOT_PREC + - tp -runs: - - interpolator: - mlflow_id: 8d1e0410ca7d4f74b368b3079878259a - label: M-2 interpolator (KENDA) - steps: 0/120/1 - config: resources/inference/configs/interpolator_from_test_data_stretched.yaml - forecaster: null - extra_dependencies: - - git+https://github.com/ecmwf/anemoi-inference@14189907b4f4e3b204b7994f828831b8aa51e9b6 - - torch-geometric==2.6.1 - - anemoi-graphs==0.5.2 - - interpolator: - mlflow_id: 8d1e0410ca7d4f74b368b3079878259a - label: M-2 interpolator (M-1 forecaster) - steps: 0/120/1 - config: resources/inference/configs/interpolator_stretched.yaml - forecaster: - mlflow_id: d0846032fc7248a58b089cbe8fa4c511 - config: resources/inference/configs/forecaster_with_global.yaml - steps: 0/120/6 - extra_dependencies: - - git+https://github.com/ecmwf/anemoi-inference@14189907b4f4e3b204b7994f828831b8aa51e9b6 - - torch-geometric==2.6.1 - - anemoi-graphs==0.5.2 - - forecaster: - mlflow_id: d0846032fc7248a58b089cbe8fa4c511 - label: M-1 forecaster - config: resources/inference/configs/forecaster_with_global.yaml - steps: 0/120/6 +constant_forcings: + test: + use_original_paths: true -baselines: - - baseline: - baseline_id: COSMO-E-1h - label: COSMO-E - root: /scratch/mch/bhendj/COSMO-E - steps: 0/120/1 +patch_metadata: + dataset: + constant_fields: [z, lsm] -analysis: - label: COSMO KENDA - analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-1h-v3-pl13.zarr +output: + tee: + outputs: + - extract_lam: + output: + assign_mask: + mask: "source0/trimedge_mask" + output: + grib: + path: grib/{dateTime}_{step:03}.grib + encoding: + typeOfGeneratingProcess: 2 + templates: + samples: _resources/templates_index_cosmo.yaml + - grib: + path: grib/ifs-{dateTime}_{step:03}.grib + encoding: + typeOfGeneratingProcess: 2 + templates: + samples: _resources/templates_index_ifs.yaml + post_processors: + - extract_slice: [189699, -1] + - assign_mask: "global/cutout_mask" -locations: - output_root: output/ - mlflow_uri: - # - https://service.meteoswiss.ch/mlstore - - https://servicedepl.meteoswiss.ch/mlstore - - https://mlflow.ecmwf.int - -profile: - executor: slurm - global_resources: - gpus: 16 - default_resources: - slurm_partition: "postproc" - cpus_per_task: 1 - mem_mb_per_cpu: 1800 - runtime: "1h" - gpus: 0 - jobs: 50 +verbosity: 1 +allow_nans: true +output_frequency: "1h" From d0185913b309081c65931912c1f17624b9266882 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Wed, 22 Oct 2025 16:34:02 +0200 Subject: [PATCH 33/35] Plot meteograms --- config/showcase.yaml | 2 +- workflow/Snakefile | 9 +- workflow/rules/plot.smk | 27 ++++ workflow/scripts/plot_meteogram.mo.py | 198 ++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 workflow/scripts/plot_meteogram.mo.py diff --git a/config/showcase.yaml b/config/showcase.yaml index 458e6b5..e329174 100644 --- a/config/showcase.yaml +++ b/config/showcase.yaml @@ -31,7 +31,7 @@ baselines: analysis: label: COSMO KENDA - analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-6h-v3-pl13.zarr + analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-1h-v3-pl13.zarr locations: output_root: output/ diff --git a/workflow/Snakefile b/workflow/Snakefile index fae5d9d..d14b178 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -51,11 +51,18 @@ rule showcase_all: OUT_ROOT / "showcases/{run_id}/{init_time}/{init_time}_{param}_{projection}_{region}.gif", init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES], - run_id=collect_all_runs(), + run_id=collect_all_candidates(), param=["2t", "10sp"], projection="orthographic", region=["none", "switzerland"], ), + expand( + OUT_ROOT / "showcases/{run_id}/meteograms/{init_time}_{param}_{sta}.png", + init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES], + run_id=collect_all_candidates(), + param=["2t"], + sta=["GVE", "KLO", "LUG"], + ), rule sandbox_all: diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 98f9df9..b9d0010 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -37,6 +37,33 @@ rule plot_forecast_frame: """ +rule plot_meteogram: + input: + script="workflow/scripts/plot_meteogram.mo.py", + fct_grib=rules.inference_routing.output[0], + analysis_zarr=config["analysis"].get("analysis_zarr"), + output: + OUT_ROOT / "showcases/{run_id}/meteograms/{init_time}_{param}_{sta}.png", + # localrule: True + resources: + slurm_partition="postproc", + cpus_per_task=1, + runtime="5m", + shell: + """ + export ECCODES_DEFINITION_PATH=/user-environment/share/eccodes-cosmo-resources/definitions + python {input.script} \ + --forecast {input.fct_grib} --analysis {input.analysis_zarr} \ + --date {wildcards.init_time} --outfn {output[0]} \ + --param {wildcards.param} --station {wildcards.sta} + # interactive editing (needs to set localrule: True and use only one core) + # marimo edit {input.script} -- \ + # --forecast {input.fct_grib} --analysis {input.analysis_zarr} \ + # --date {wildcards.init_time} --outfn {output[0]} \ + # --param {wildcards.param} --station {wildcards.sta} + """ + + def get_leadtimes(wc): """Get all lead times from the run config.""" start, end, step = map(int, RUN_CONFIGS[wc.run_id]["steps"].split("/")) diff --git a/workflow/scripts/plot_meteogram.mo.py b/workflow/scripts/plot_meteogram.mo.py new file mode 100644 index 0000000..75f7304 --- /dev/null +++ b/workflow/scripts/plot_meteogram.mo.py @@ -0,0 +1,198 @@ +import marimo + +__generated_with = "0.16.5" +app = marimo.App(width="medium") + + +@app.cell +def _(): + import logging + from argparse import ArgumentParser + from pathlib import Path + + import earthkit.plots as ekp + import matplotlib.pyplot as plt + import numpy as np + import pandas as pd + import xarray as xr + + from meteodatalab import data_source, grib_decoder + return ( + ArgumentParser, + Path, + data_source, + grib_decoder, + logging, + np, + pd, + plt, + xr, + ) + + +@app.cell +def _(logging): + LOG = logging.getLogger(__name__) + LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + logging.basicConfig(level=logging.INFO, format=LOG_FMT) + return + + +@app.cell +def _(ArgumentParser, Path): + ROOT = Path(__file__).parent + + parser = ArgumentParser() + + parser.add_argument( + "--forecast", type=str, default=None, help="Directory to forecast grib data" + ) + parser.add_argument( + "--analysis", type=str, default=None, help="Path to analysis zarr data" + ) + parser.add_argument("--date", type=str, default=None, help="reference datetime") + parser.add_argument("--outfn", type=str, help="output filename") + parser.add_argument("--param", type=str, help="parameter") + parser.add_argument("--station", type=str, help="station") + + args = parser.parse_args() + grib_dir = Path(args.forecast) + zarr_dir = Path(args.analysis) + init_time = args.date + outfn = Path(args.outfn) + station = args.station + param = args.param + return grib_dir, init_time, outfn, param, station, zarr_dir + + +@app.cell +def _(pd): + stations = pd.DataFrame( + [ + ("BAS", "1_75", 1, 75, "BAS", "Basel / Binningen", 1, "MeteoSchweiz", 7.583, 47.541, 316.14, 12.0, 0.0), + ("LUG", "1_47", 1, 47, "LUG", "Lugano", 1, "MeteoSchweiz", 8.960, 46.004, 272.56, 10.0, 27.34), + ("GVE", "1_58", 1, 58, "GVE", "Gen\u00e8ve / Cointrin", 1, "MeteoSchweiz", 6.122, 46.248, 415.53, 10.0, 0.0), + ("GUT", "1_79", 1, 79, "GUT", "G\u00fcttingen", 1, "MeteoSchweiz", 9.279, 47.602, 439.78, 12.0, 0.0), + ("KLO", "1_59", 1, 59, "KLO", "Z\u00fcrich / Kloten", 1, "MeteoSchweiz", 8.536, 47.48, 435.92, 10.5, 0.0), + ("SCU", "1_30", 1, 30, "SCU", "Scuol", 1, "MeteoSchweiz", 10.283, 46.793, 1304.42, 10.0, 0.0), + ("LUZ", "1_68", 1, 68, "LUZ", "Luzern", 1, "MeteoSchweiz", 8.301, 47.036, 454.0, 8.41, 32.51), + ("DIS", "1_54", 1, 54, "DIS", "Disentis", 1, "MeteoSchweiz", 8.853, 46.707, 1198.03, 10.0, 0.0), + ("PMA", "1_862", 1, 862, "PMA", "Piz Martegnas", 1, "MeteoSchweiz", 9.529, 46.577, 2668.34, 10.0, 0.0), + ("CEV", "1_843", 1, 843, "CEV", "Cevio", 1, "MeteoSchweiz", 8.603, 46.32, 420.0, 10.0, 6.85), + ("MLS", "1_38", 1, 38, "MLS", "Le Mol\u00e9son", 1, "MeteoSchweiz", 7.018, 46.546, 1977.0, 10.0, 13.31), + ("PAY", "1_32", 1, 32, "PAY", "Payerne", 1, "MeteoSchweiz", 6.942, 46.811, 489.17, 10.0, 0.0), + ("NAP", "1_48", 1, 48, "NAP", "Napf", 1, "MeteoSchweiz", 7.94, 47.005, 1404.03, 15.0, 0.0), + + ], + columns=[ + "station", + "name", + "type_id", + "point_id", + "nat_abbr", + "fullname", + "owner_id", + "owner_name", + "longitude", + "latitude", + "height_masl", + "pole_height", + "roof_height", + ], + ) + return (stations,) + + +@app.cell +def load_grib_data(data_source, grib_decoder, grib_dir, init_time, param): + if param == "10sp": + paramlist = ["10u", "10v"] + elif param == "sp": + paramlist = ["u", "v"] + else: + paramlist = [param] + + grib_files = sorted(grib_dir.glob(f"{init_time}*.grib")) + fds = data_source.FileDataSource(datafiles=grib_files) + ds_grib = grib_decoder.load(fds, {"param": paramlist}) + ds_grib = ds_grib[param].squeeze() + ds_grib + return ds_grib, paramlist + + +@app.cell +def load_zarr_data(ds_grib, np, param, paramlist, xr, zarr_dir): + ds_zarr = xr.open_zarr(zarr_dir, consolidated=False) + ds_zarr = ds_zarr.set_index(time="dates") + ds_zarr = ds_zarr.sel(time=ds_grib.valid_time.values) + ds_zarr = ds_zarr.assign_coords({"variable": ds_zarr.attrs["variables"]}) + ds_zarr = ds_zarr.sel(variable=[p for p in paramlist]).squeeze("ensemble", drop=True) + + # recover original 2D shape + ny, nx = ds_zarr.attrs["field_shape"] + y_idx, x_idx = np.unravel_index(np.arange(ny * nx), shape=(ny, nx)) + ds_zarr = ds_zarr.assign_coords({"y": ("cell", y_idx), "x": ("cell", x_idx)}) + ds_zarr = ds_zarr.set_index(cell=("y", "x")) + ds_zarr = ds_zarr.unstack("cell") + + # set lat lon as coords + ds_zarr = ds_zarr.rename({"latitudes": "lat", "longitudes": "lon"}) + ds_zarr = ds_zarr.set_coords(["lat", "lon"]) + ds_zarr = ds_zarr.sel(variable=param).squeeze()["data"] + ds_zarr + return (ds_zarr,) + + +@app.cell +def _(ds_grib, ds_zarr, np, pd, stations): + def nearest_yx_euclid(ds_grib, lat_s, lon_s): + """ + Return (y_idx, x_idx) of the grid point nearest to (lat_s, lon_s) + using Euclidean distance in degrees. + """ + lat2d = ds_grib['lat'] # (y, x) + lon2d = ds_grib['lon'] # (y, x) + + dist2 = (lat2d - lat_s)**2 + (lon2d - lon_s)**2 + flat_idx = np.nanargmin(dist2.values) + y_idx, x_idx = np.unravel_index(flat_idx, dist2.shape) + return int(y_idx), int(x_idx) + + def get_grib_idx_row(row): + return nearest_yx_euclid(ds_grib, row['latitude'], row['longitude']) + idxs_grib = stations.apply(get_grib_idx_row, axis=1, result_type='expand') + idxs_grib.columns = ['grib_y_idx', 'grib_x_idx'] + + def get_zarr_idx_row(row): + return nearest_yx_euclid(ds_zarr, row['latitude'], row['longitude']) + idxs_zarr = stations.apply(get_zarr_idx_row, axis=1, result_type='expand') + idxs_zarr.columns = ['zarr_y_idx', 'zarr_x_idx'] + + sta_idxs = pd.concat([stations, idxs_grib, idxs_zarr], axis=1).set_index("station") + sta_idxs + return (sta_idxs,) + + +@app.cell +def _(ds_grib, ds_zarr, outfn, plt, sta_idxs, station): + grib_x_idx, grib_y_idx = sta_idxs.loc[station].grib_x_idx, sta_idxs.loc[station].grib_y_idx + zarr_x_idx, zarr_y_idx = sta_idxs.loc[station].zarr_x_idx, sta_idxs.loc[station].zarr_y_idx + + # analysis + ds_zarr.isel(x=zarr_x_idx, y=zarr_y_idx).plot(x="time", label="analysis", color="k", ls="--") + + # TODO: plot actual forecaster data... + ds_grib.isel(x=grib_x_idx, y=grib_y_idx).isel(lead_time=list(range(0, 126, 6))).plot(x="valid_time", marker="o", linestyle='None', color="k", label="forecaster") + + # interpolator + ds_grib.isel(x=grib_x_idx, y=grib_y_idx).plot(x="valid_time", label="interpolator") + + plt.legend() + plt.ylabel(f"{ds_grib.attrs["parameter"]["shortName"]} ({ds_grib.attrs["parameter"]["units"]})") + plt.title(f"{init_time} {ds_grib.attrs["parameter"]["name"]} at {station}") + plt.savefig(outfn) + return + + +if __name__ == "__main__": + app.run() From b841d194f3c23e6a7af48df2d43857775c6b3e5a Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Fri, 7 Nov 2025 17:43:50 +0100 Subject: [PATCH 34/35] Fix merge commit --- .../configs/interpolator_stretched_with_global.yaml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/resources/inference/configs/interpolator_stretched_with_global.yaml b/resources/inference/configs/interpolator_stretched_with_global.yaml index b989016..6fad4e0 100644 --- a/resources/inference/configs/interpolator_stretched_with_global.yaml +++ b/resources/inference/configs/interpolator_stretched_with_global.yaml @@ -100,22 +100,12 @@ output: encoding: typeOfGeneratingProcess: 2 templates: -<<<<<<< HEAD - samples: _resources/templates_index_cosmo.yaml -======= samples: resources/templates_index_cosmo.yaml ->>>>>>> main - grib: path: grib/ifs-{dateTime}_{step:03}.grib encoding: typeOfGeneratingProcess: 2 templates: -<<<<<<< HEAD - samples: _resources/templates_index_ifs.yaml - post_processors: - - extract_slice: [189699, -1] - - assign_mask: "global/cutout_mask" -======= samples: resources/templates_index_ifs.yaml post_processors: - extract_mask: @@ -125,7 +115,6 @@ output: - assign_mask: mask: "global/cutout_mask" # fill local/global overlapping points with nan ->>>>>>> main verbosity: 1 allow_nans: true From dd36d534f3a6f48e869f0ae99ecda0a551048d00 Mon Sep 17 00:00:00 2001 From: Daniele Nerini Date: Fri, 7 Nov 2025 17:46:29 +0100 Subject: [PATCH 35/35] Fix path to ICON definitions --- workflow/Snakefile | 2 +- workflow/rules/plot.smk | 2 +- workflow/scripts/plot_meteogram.mo.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index cf706d6..0692dd0 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -59,7 +59,7 @@ rule showcase_all: OUT_ROOT / "showcases/{run_id}/{init_time}/{init_time}_{param}_{sta}.png", init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES], run_id=collect_all_candidates(), - param=["2t"], + param=["T_2M"], sta=["GVE", "KLO", "LUG"], ), diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 794c53b..3e34a48 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -23,7 +23,7 @@ rule plot_meteogram: runtime="5m", shell: """ - export ECCODES_DEFINITION_PATH=/user-environment/share/eccodes-cosmo-resources/definitions + export ECCODES_DEFINITION_PATH=$(realpath .venv/share/eccodes-cosmo-resources/definitions) python {input.script} \ --forecast {input.fct_grib} --analysis {input.analysis_zarr} \ --date {wildcards.init_time} --outfn {output[0]} \ diff --git a/workflow/scripts/plot_meteogram.mo.py b/workflow/scripts/plot_meteogram.mo.py index 74899c8..a624072 100644 --- a/workflow/scripts/plot_meteogram.mo.py +++ b/workflow/scripts/plot_meteogram.mo.py @@ -286,10 +286,10 @@ def _(pd): @app.cell def load_grib_data(data_source, grib_decoder, grib_dir, init_time, param): - if param == "10sp": - paramlist = ["10u", "10v"] - elif param == "sp": - paramlist = ["u", "v"] + if param == "SP_10M": + paramlist = ["U_10M", "V_10M"] + elif param == "SP": + paramlist = ["U", "V"] else: paramlist = [param]