diff --git a/docs/_static/logo_text.png b/docs/_static/logo_text.png new file mode 100644 index 00000000..bbd887fd Binary files /dev/null and b/docs/_static/logo_text.png differ diff --git a/docs/_static/logo_text.svg b/docs/_static/logo_text.svg new file mode 100644 index 00000000..9c7baa8d --- /dev/null +++ b/docs/_static/logo_text.svg @@ -0,0 +1,33240 @@ + + + + + + + + 2026-01-26T13:29:30.057901 + image/svg+xml + + + Matplotlib v3.10.7, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/advanced_stress_packages/01_lake.ipynb b/docs/advanced_stress_packages/01_lake.ipynb index 60e0e015..8edd47e6 100644 --- a/docs/advanced_stress_packages/01_lake.ipynb +++ b/docs/advanced_stress_packages/01_lake.ipynb @@ -181,7 +181,7 @@ "mask = lak_grid.area > 0.5 * ds[\"area\"].sel(icell2d=lak_grid.index)\n", "lak_grid = lak_grid[mask]\n", "# set the geometry to the entire cell\n", - "gi = flopy.utils.GridIntersect(nlmod.grid.modelgrid_from_ds(ds), method=\"vertex\")\n", + "gi = flopy.utils.GridIntersect(nlmod.grid.modelgrid_from_ds(ds))\n", "lak_grid.geometry = gi.geoms[lak_grid.index]\n", "\n", "# remove drains that overlap with the lake\n", diff --git a/docs/generate_logo_text.py b/docs/generate_logo_text.py new file mode 100644 index 00000000..fb6cb04e --- /dev/null +++ b/docs/generate_logo_text.py @@ -0,0 +1,122 @@ +# %% +import os +import geopandas as gpd +from shapely.geometry import Polygon, MultiPolygon +from matplotlib.textpath import TextPath +from matplotlib.font_manager import FontProperties +import matplotlib.pyplot as plt + +import nlmod +import numpy as np + + +# %% +def string_to_gdf(text, font, size=500, x_offset=0, **kwargs): + tp = TextPath((x_offset, 0), text, size=size, prop=font) + polygons = [] + + # Matplotlib returns list of polygons (outer + inner contours) + for poly in tp.to_polygons(): + if len(poly) >= 3: + polygons.append(Polygon(poly)) + + # if a polygon has holes, we need to reconstruct it + final_polygons = [] + used = [False] * len(polygons) + for i, outer in enumerate(polygons): + if used[i]: + continue + holes = [] + for j, inner in enumerate(polygons): + if i != j and not used[j]: + if outer.contains(inner): + holes.append(inner.exterior.coords) + used[j] = True + final_polygons.append(Polygon(outer.exterior.coords, holes)) + used[i] = True + + gdf = gpd.GeoDataFrame(geometry=final_polygons, **kwargs) + return gdf + + +text = "NLMOD" +# font_path = "C:/Windows/Fonts/consola.ttf" # Adjust for your system +font_path = "C:/Windows/Fonts/cour.ttf" # Courier font +font = FontProperties(fname=font_path) +text_size = 100 # Larger size -> smoother curves + +gdf = string_to_gdf(text, font, size=text_size) + +if False: + gdf.plot() + plt.axis("equal") + plt.show() + +dx = 10 +extent = gdf.total_bounds[[0, 2, 1, 3]] + +# add a buffer of 10 % of width +buffer = 0.1 * (extent[1] - extent[0]) +extent = np.array( + [ + extent[0] - buffer, + extent[1] + buffer, + extent[2] - buffer, + extent[3] + buffer, + ] +) +if False: + # make sure vertical extent is at least half of the horizontal extent + height = extent[3] - extent[2] + width = extent[1] - extent[0] + center_y = 0.5 * (extent[2] + extent[3]) + if height < 0.5 * width: + height = 0.5 * width + extent = np.array( + [ + extent[0], + extent[1], + center_y - 0.5 * height, + center_y + 0.5 * height, + ] + ) + +extent = (extent / dx).round() * dx +ds = nlmod.get_ds(extent, dx) +ds = nlmod.grid.refine(ds, "logo", [(gdf, 2)]) + +nlmod.plot.modelgrid(ds) + +# %% plot the logo +# add 1 percent margin +margin = 0.01 * (extent[1] - extent[0]) +extent = ( + extent[0] - margin, + extent[1] + margin, + extent[2] - margin, + extent[3] + margin, +) +figwidth = 5 +figheight = figwidth * (extent[3] - extent[2]) / (extent[1] - extent[0]) +f = plt.figure(figsize=(figwidth, figheight)) +ax = f.add_axes([0, 0, 1, 1]) +ax.axis("equal") +ax.axis(extent) +color = "k" +nlmod.plot.modelgrid(ds, color=color, ax=ax, linewidth=0.5, clip_on=False, antialiased=False) + +ax.set_xlabel("") +ax.set_ylabel("") +ax.set_title("") +ax.axis("off") + +# %% save logo +fname = f"logo_text" +dpi = 300 +if figwidth != 5: + fname = f"{fname}_{figwidth}" + dpi = None +f.savefig(os.path.join("_static", f"{fname}.png"), dpi=dpi) +f.savefig(os.path.join("_static", f"{fname}.svg")) + +# %% diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 4f9afbe4..b14209ca 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -111,7 +111,6 @@ On top of that there are some optional dependecies: - scikit-image (used in nlmod.read.rws.calculate_sea_coverage) - py7zr (used in nlmod.read.bofek.download_bofek_gdf) - joblib (used in nlmod.cache) -- colorama (used in nlmod.util.get_color_logger) - tqdm (used for showing progress in long-running methods) - hydropandas (used in nlmod.read.knmi and nlmod.read.bro) - owslib (used in nlmod.read.ahn.get_latest_ahn_from_wcs) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 2fa5c4e8..d454ee55 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -156,10 +156,8 @@ def get_icell2d_from_xy(x, y, ds, gi=None, rotated=True): msg = "get_icell2d_from_xy can only be applied to a vertex grid" assert ds.gridtype == "vertex", msg if gi is None: - gi = flopy.utils.GridIntersect( - modelgrid_from_ds(ds, rotated=rotated), method="vertex" - ) - cellids = gi.intersects(Point(x, y))["cellids"] + gi = flopy.utils.GridIntersect(modelgrid_from_ds(ds, rotated=rotated)) + cellids = gi.intersects(Point(x, y), dataframe=True)["cellid"].values if len(cellids) < 1: raise (ValueError(f"Point ({x}, {y}) is outside of the model grid")) icell2d = cellids[0] @@ -227,7 +225,7 @@ def get_row_col_from_xy(x, y, ds, rotated=True, gi=None): msg = "get_row_col_from_xy can only be applied to a structured grid" assert ds.gridtype == "structured", msg if gi is not None: - cellids = gi.intersects(Point(x, y))["cellids"] + cellids = gi.intersects(Point(x, y), dataframe=True)[["row", "col"]].values if len(cellids) < 1: raise (ValueError(f"Point ({x}, {y}) is outside of the model grid")) row, col = cellids[0] @@ -622,7 +620,7 @@ def get_cellids_from_xy(x, y, ds=None, modelgrid=None, gi=None): # build geometries and cellids for grid if gi is None: - gi = flopy.utils.GridIntersect(modelgrid, method="vertex") + gi = flopy.utils.GridIntersect(modelgrid) # spatial join points with grid and add resulting cellid to obs_ds spatial_join = gpd.GeoDataFrame(geometry=gpd.points_from_xy(x, y)).sjoin( @@ -1345,18 +1343,16 @@ def polygon_to_area(modelgrid, polygon, da, gridtype="structured"): f'input geometry should by of type "Polygon" not {polygon.geom_type}' ) - ix = GridIntersect(modelgrid, method="vertex") - opp_cells = ix.intersect(polygon) + ix = GridIntersect(modelgrid) + df = ix.intersect(polygon, geo_dataframe=True) if gridtype == "structured": - area_array = util.get_da_from_da_ds(da, dims=("y", "x"), data=0) - for cellid, area in zip(opp_cells["cellids"], opp_cells["areas"]): - area_array[cellid[0], cellid[1]] = area + area_array = util.get_da_from_da_ds(da, dims=("y", "x"), data=0.0) + for row, col, area in zip(df["row"], df["col"], df["areas"]): + area_array[row, col] = area elif gridtype == "vertex": - area_array = util.get_da_from_da_ds(da, dims=("icell2d",), data=0) - cids = opp_cells.cellids - area = opp_cells.areas - area_array[cids.astype(int)] = area + area_array = util.get_da_from_da_ds(da, dims=("icell2d",), data=0.0) + area_array[df["cellid"].values] = df["areas"] return area_array @@ -1770,7 +1766,7 @@ def gdf_to_bool_da( # Rotate the polygon instead of the modelgrid if ix is None: modelgrid = modelgrid_from_ds(ds, rotated=False) - ix = GridIntersect(modelgrid, method="vertex") + ix = GridIntersect(modelgrid) grid_rotation = ds.attrs.get("angrot", 0.0) ix_rotation = ix.mfgrid.angrot @@ -1780,20 +1776,21 @@ def gdf_to_bool_da( multipolygon = affine_transform(multipolygon, affine) if kwargs or contains_centroid or min_area_fraction is not None: - r = ix.intersect( + df = ix.intersect( multipolygon, contains_centroid=contains_centroid, min_area_fraction=min_area_fraction, + geo_dataframe=True, **kwargs, ) else: - r = ix.intersects(multipolygon) + df = ix.intersects(multipolygon, dataframe=True) - if r.size > 0 and ds.gridtype == "structured": - ixs, iys = zip(*r["cellids"], strict=True) - da.values[ixs, iys] = True - elif r.size > 0 and ds.gridtype == "vertex": - da[r["cellids"].astype(int)] = True + if df.shape[0] > 0 and ds.gridtype == "structured": + for _, row in df.iterrows(): + da[row["row"], row["col"]] = True + elif df.shape[0] > 0 and ds.gridtype == "vertex": + da[df["cellid"].values] = True return da @@ -1893,7 +1890,7 @@ def gdf_to_count_da(gdf, ds, ix=None, buffer=0.0, **kwargs): # build list of gridcells if ix is None: modelgrid = modelgrid_from_ds(ds, rotated=False) - ix = GridIntersect(modelgrid, method="vertex") + ix = GridIntersect(modelgrid) if ds.gridtype == "structured": da = util.get_da_from_da_ds(ds, dims=("y", "x"), data=0) @@ -1909,18 +1906,17 @@ def gdf_to_count_da(gdf, ds, ix=None, buffer=0.0, **kwargs): for geom in geoms: if buffer > 0.0: - cids = ix.intersects(geom.buffer(buffer), **kwargs)["cellids"] + df = ix.intersects(geom.buffer(buffer), dataframe=True, **kwargs) else: - cids = ix.intersects(geom, **kwargs)["cellids"] + df = ix.intersects(geom, dataframe=True, **kwargs) - if len(cids) == 0: + if len(df) == 0: continue if ds.gridtype == "structured": - ixs, iys = zip(*cids) - da.values[ixs, iys] += 1 + da.values[df["row"].values, df["col"].values] += 1 elif ds.gridtype == "vertex": - da[cids.astype(int)] += 1 + da[df["cellid"].values] += 1 return da @@ -1962,7 +1958,6 @@ def gdf_to_count_ds(gdf, ds, da_name, keep_coords=None, ix=None, buffer=0.0, **k def gdf_to_grid( gdf, ml=None, - method="vertex", ix=None, desc="Intersecting with grid", silent=False, @@ -1981,8 +1976,6 @@ def gdf_to_grid( The flopy model or xarray dataset that defines the grid. When a Dataset is supplied, and the grid is rotated, the geodataframe is transformed in model coordinates. The default is None. - method : string, optional - Method passed to the GridIntersect-class. The default is 'vertex'. ix : flopy.utils.GridIntersect, optional GridIntersect, if not provided the modelgrid in ml is used. desc : string, optional @@ -2019,21 +2012,20 @@ def gdf_to_grid( ) if ix is None: - ix = flopy.utils.GridIntersect(modelgrid, method=method) + ix = flopy.utils.GridIntersect(modelgrid) shps = [] geometry = gdf.geometry.name for _, shp in tqdm(gdf.iterrows(), total=gdf.shape[0], desc=desc, disable=silent): - r = ix.intersect(shp[geometry], **kwargs) - for i in range(r.shape[0]): + df = ix.intersect(shp[geometry], geo_dataframe=True, **kwargs) + for _, row in df.iterrows(): shpn = shp.copy() - shpn["cellid"] = r["cellids"][i] - shpn[geometry] = r["ixshapes"][i] + shpn["cellid"] = row["cellids"] + shpn[geometry] = row["geometry"] if shp[geometry].geom_type == ["LineString", "MultiLineString"]: - shpn["length"] = r["lengths"][i] + shpn["length"] = row["lengths"] elif shp[geometry].geom_type in ["Polygon", "MultiPolygon"]: - shpn["area"] = r["areas"][i] + shpn["area"] = row["areas"] shps.append(shpn) - if len(shps) == 0: # Unable to determine the column names, because no geometries intersect with the grid logger.info("No geometries intersect with the grid") @@ -2115,7 +2107,7 @@ def gdf_area_to_da( affine = get_affine_world_to_mod(ds) gdf = affine_transform_gdf(gdf, affine) if ix is None: - ix = flopy.utils.GridIntersect(modelgrid, method="vertex") + ix = flopy.utils.GridIntersect(modelgrid) if index_name is None: index_name = gdf.index.name @@ -2135,12 +2127,12 @@ def gdf_area_to_da( for irow, index in tqdm( enumerate(gdf.index), total=gdf.shape[0], desc=desc, disable=silent ): - r = ix.intersect(gdf.at[index, geometry], **kwargs) + df = ix.intersect(gdf.at[index, geometry], geo_dataframe=True, **kwargs) if structured: - for i in range(r.shape[0]): - data[r["cellids"][i] + (irow,)] += r["areas"][i] + for row, col, area in zip(df["row"], df["col"], df["areas"]): + data[(row, col, irow)] += area else: - data[list(r["cellids"]), irow] = r["areas"] + data[list(df["cellid"]), irow] = df["areas"] if sparse: try: @@ -2451,6 +2443,22 @@ def get_extent_polygon(ds, rotated=True): def get_extent_gdf(ds, rotated=True, crs="EPSG:28992"): + """Get the model extent as a GeoDataFrame with a polygon. + + Parameters + ---------- + ds : xr.Dataset + model dataset. + rotated : bool, optional + if True, the extent is corrected for angrot. The default is True. + crs : str, optional + Coordinate reference system. The default is "EPSG:28992". + + Returns + ------- + gdf : geopandas.GeoDataFrame + GeoDataFrame containing the model extent as a polygon geometry. + """ polygon = get_extent_polygon(ds, rotated=rotated) return gpd.GeoDataFrame(geometry=[polygon], crs=crs) diff --git a/nlmod/dims/layers.py b/nlmod/dims/layers.py index b76c6fa9..7a43df6a 100644 --- a/nlmod/dims/layers.py +++ b/nlmod/dims/layers.py @@ -2151,7 +2151,7 @@ def get_modellayers_screens(ds, screen_top, screen_bottom, xy=None, icell2d=None """ if grid.is_vertex(ds): if icell2d is None: - gi = flopy.utils.GridIntersect(grid.modelgrid_from_ds(ds), method="vertex") + gi = flopy.utils.GridIntersect(grid.modelgrid_from_ds(ds)) icell2d = [grid.get_icell2d_from_xy(x, y, ds, gi=gi) for x, y in xy] # make dataset of observations ds_obs = ds.sel(icell2d=icell2d) diff --git a/nlmod/dims/time.py b/nlmod/dims/time.py index 45a32fe0..a9c6da47 100644 --- a/nlmod/dims/time.py +++ b/nlmod/dims/time.py @@ -394,7 +394,7 @@ def set_ds_time_numeric( def set_time_variables(ds, start, time, steady, steady_start, time_units, nstp, tsmult): - """Add data variables: steady, nstp and tsmult, set attributes: start, time_units + """Add data variables: steady, nstp and tsmult, set attributes: start, time_units. Parameters ---------- diff --git a/nlmod/gwf/lake.py b/nlmod/gwf/lake.py index f9af0c16..c0258819 100644 --- a/nlmod/gwf/lake.py +++ b/nlmod/gwf/lake.py @@ -405,7 +405,7 @@ def _copy_da_from_ds(gdf, ds, variable, boundname_column=None, set_to_0_in_ds=Fa else: da_cells = ds[variable].loc[cellids].copy() if set_to_0_in_ds: - ds[variable][:, cellids] = 0.0 + ds[variable][cellids] = 0.0 # calculate thea area-weighted mean df[column] = float((da_cells * area).sum("icell2d") / area.sum()) return df diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index bdd452b6..a15c74ca 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -1227,18 +1227,21 @@ def get_seaonal_timeseries( def rivdata_from_xylist(gwf, xylist, layer, stage, cond, rbot, aux=None): - gi = flopy.utils.GridIntersect(gwf.modelgrid, method="vertex") - cellids = gi.intersect(xylist, shapetype="linestring")["cellids"] + gi = flopy.utils.GridIntersect(gwf.modelgrid) + df = gi.intersect(xylist, shapetype="linestring", geo_dataframe=True) riv_data = [] - for cid in cellids: - if isinstance(cid, (list, tuple)) and len(cid) == 2: - idata = [(layer, cid[0], cid[1]), stage, cond, rbot] + + if "row" in df.columns: # structured grid + for row, col in zip(df["row"], df["col"]): + idata = [(layer, row, col), stage, cond, rbot] if aux is not None: idata.append(aux) riv_data.append(idata) - else: + else: # unstructured grid + for cid in df["cellid"]: idata = [(layer, cid), stage, cond, rbot] if aux is not None: idata.append(aux) riv_data.append(idata) + return riv_data diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index 59102350..3049d67a 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -162,7 +162,7 @@ def _get_heads_da( flopy HeadFile object for binary heads modelgrid : flopy.discretization.Grid, optional flopy modelgrid object, default is None, in which case the modelgrid - is derived from `hobj.mg` + is derived from `hobj.modelgrid` Returns ------- @@ -175,7 +175,7 @@ def _get_heads_da( kstpkper = hobj.get_kstpkper() if modelgrid is None: - modelgrid = hobj.mg + modelgrid = hobj.modelgrid # shape is derived from hobj, not modelgrid as array read from # binary file always has 3 dimensions shape = (hobj.nlay, hobj.nrow, hobj.ncol) diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index 5f65614b..26de80ac 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -66,21 +66,29 @@ def __init__( if self.icell2d in ds.dims: # determine the cells that are crossed modelgrid = modelgrid_from_ds(ds, rotated=False) - gi = flopy.utils.GridIntersect(modelgrid, method="vertex") - r = gi.intersect(line) + gi = flopy.utils.GridIntersect(modelgrid) + df = gi.intersect(line, geo_dataframe=True) s_cell = [] - for i, ic2d in enumerate(r["cellids"]): - intersection = r["ixshapes"][i] + for _, row in df.iterrows(): + intersection = row["geometry"] if intersection.length == 0: continue if isinstance(intersection, MultiLineString): for ix in intersection.geoms: - s_cell.append([line.project(Point(ix.coords[0])), 1, ic2d]) - s_cell.append([line.project(Point(ix.coords[-1])), 0, ic2d]) + s_cell.append( + [line.project(Point(ix.coords[0])), 1, row["cellid"]] + ) + s_cell.append( + [line.project(Point(ix.coords[-1])), 0, row["cellid"]] + ) continue assert isinstance(intersection, LineString) - s_cell.append([line.project(Point(intersection.coords[0])), 1, ic2d]) - s_cell.append([line.project(Point(intersection.coords[-1])), 0, ic2d]) + s_cell.append( + [line.project(Point(intersection.coords[0])), 1, row["cellid"]] + ) + s_cell.append( + [line.project(Point(intersection.coords[-1])), 0, row["cellid"]] + ) s_cell = np.array(s_cell) ind = np.lexsort((s_cell[:, 1], s_cell[:, 0])) s_cell = s_cell[ind, :] diff --git a/nlmod/prt/prt.py b/nlmod/prt/prt.py index d95fa878..e553a869 100644 --- a/nlmod/prt/prt.py +++ b/nlmod/prt/prt.py @@ -144,6 +144,8 @@ def prp(ds, prt, packagedata, perioddata, pname="prp", **kwargs): """ logger.info("creating mf6 PRP") prp_track_file = kwargs.pop("prp_track_file", f"{ds.model_name}.prp.trk") + # the default value in flopy for coordinate_check_method of 'eager' gives an error + # in MODFLOW verion 6.6.3 prp = fp.mf6.ModflowPrtprp( prt, pname=pname, @@ -156,6 +158,7 @@ def prp(ds, prt, packagedata, perioddata, pname="prp", **kwargs): boundnames=kwargs.pop("boundnames", False), exit_solve_tolerance=kwargs.pop("exit_solve_tolerance", 1e-5), extend_tracking=kwargs.pop("extend_tracking", True), + coordinate_check_method=kwargs.pop("coordinate_check_method", None), **kwargs, ) return prp diff --git a/nlmod/read/bofek.py b/nlmod/read/bofek.py index 7f837c5c..04321ff0 100644 --- a/nlmod/read/bofek.py +++ b/nlmod/read/bofek.py @@ -103,6 +103,7 @@ def download_bofek_gdf(extent, dirname, timeout=3600): # download zip logger.info("Downloading BOFEK2020 GIS data (~35 seconds)") r = requests.get(bofek_zip_url, timeout=timeout, stream=True) + r.raise_for_status() # show download progress total_size = int(r.headers.get("content-length", 0)) diff --git a/nlmod/read/rws.py b/nlmod/read/rws.py index 68309b5a..87aa1f8b 100644 --- a/nlmod/read/rws.py +++ b/nlmod/read/rws.py @@ -96,8 +96,9 @@ def get_surface_water(ds, gdf=None, da_basename="rws_oppwater"): @cache.cache_netcdf(coords_3d=True) def discretize_surface_water(ds, gdf, da_basename="rws_oppwater"): - """Create 3 data-arrays from the shapefile with surface water: + """Create 3 data-arrays from the shapefile with surface water. + These data arrays are: - area: area of the shape in the cell - cond: conductance based on the area and "bweerstand" column in shapefile - stage: surface water level based on the "peil" column in the shapefile @@ -150,8 +151,9 @@ def discretize_surface_water(ds, gdf, da_basename="rws_oppwater"): @cache.cache_netcdf(coords_2d=True) def get_northsea(ds, gdf=None, da_name="northsea"): - """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is defined by - rws surface water shapefile. + """Get Dataset which is 1 at the northsea and 0 everywhere else. + + Sea is defined by rws surface water shapefile. .. deprecated:: 0.10.0 `get_northsea` will be removed in nlmod 1.0.0, it is replaced by @@ -174,7 +176,6 @@ def get_northsea(ds, gdf=None, da_name="northsea"): Dataset with a single DataArray, this DataArray is 1 at sea and 0 everywhere else. Grid dimensions according to ds. """ - warnings.warn( "'get_northsea' is deprecated and will be removed in a future version. " "Use 'nlmod.read.rws.discretize_northsea' to project the northsea on the model grid", @@ -186,8 +187,9 @@ def get_northsea(ds, gdf=None, da_name="northsea"): @cache.cache_netcdf(coords_2d=True) def discretize_northsea(ds, gdf=None, da_name="northsea"): - """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is defined by - rws surface water shapefile. + """Get Dataset which is 1 at the northsea and 0 everywhere else. + + Sea is defined by rws surface water shapefile. Parameters ---------- @@ -249,7 +251,7 @@ def calculate_sea_coverage( filled by the minial value of dtm. ds : xr.Dataset, optional Dataset with model information. When ds is not None, the sea DataArray is - transformed to the model grid. THe default is None. + transformed to the model grid. The default is None. zmax : float, optional Locations thet become sea when the sea level reaches a level of zmax will get a value of 1 in the resulting DataArray. The default is 0.0. diff --git a/nlmod/util.py b/nlmod/util.py index a66be514..d0334051 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -878,13 +878,7 @@ def __init__( def format(self, record) -> str: """Format the specified record as text.""" record.color = self.colors.get(record.levelname, "") - try: - from colorama import Style - - record.reset = Style.RESET_ALL - - except ImportError: - record.reset = "" + record.reset = "\x1b[0m" return super().format(record) @@ -907,19 +901,13 @@ def get_color_logger(level="INFO", logger_name=None): else: FORMAT = "{color}{levelname}:{name}.{funcName}:{message}{reset}" - try: - from colorama import Back, Fore, Style - - colors = { - "DEBUG": Fore.CYAN, - "INFO": Fore.GREEN, - "WARNING": Fore.YELLOW, - "ERROR": Fore.RED, - "CRITICAL": Fore.RED + Back.WHITE + Style.BRIGHT, - } - except ImportError: - logger.warning("colorama package not found, colored logging is disabled.") - colors = {} + colors = { + "DEBUG": "\x1b[36m", + "INFO": "\x1b[32m", + "WARNING": "\x1b[33m", + "ERROR": "\x1b[31m", + "CRITICAL": "\x1b[31m" + "\x1b[47m" + "\x1b[1m", + } formatter = ColoredFormatter( FORMAT, diff --git a/nlmod/version.py b/nlmod/version.py index fb65585c..0918bce5 100644 --- a/nlmod/version.py +++ b/nlmod/version.py @@ -1,7 +1,7 @@ from importlib import metadata from platform import python_version -__version__ = "0.11.1" +__version__ = "0.11.2" def show_versions() -> None: diff --git a/pyproject.toml b/pyproject.toml index 0ddb2be7..80f62af3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ maintainers = [ ] requires-python = ">= 3.10" dependencies = [ - "flopy>=3.3.6", + "flopy>=3.10.0", "xarray>=0.16.1", "netcdf4>=1.7.2", "rasterio>=1.1.0", @@ -63,7 +63,6 @@ full = [ "scikit-image", "py7zr", "joblib", - "colorama", "tqdm", "hydropandas>=0.9.2", "owslib>=0.24.1", @@ -71,7 +70,7 @@ full = [ ] knmi = ["h5netcdf", "h5py", "nlmod[grib]"] grib = ["cfgrib", "ecmwflibs"] -test = ["pytest>=7", "pytest-cov", "pytest-dependency"] +test = ["pytest>=7", "pytest-cov", "lxml"] nbtest = ["nbformat", "nbconvert>6.4.5"] lint = ["ruff"] ci = ["nlmod[full,lint,test,nbtest]"] diff --git a/tests/test_016_time.py b/tests/test_016_time.py index 8338d9a8..9e780aed 100644 --- a/tests/test_016_time.py +++ b/tests/test_016_time.py @@ -112,8 +112,14 @@ def test_time_out_of_bounds(): ) # start cf.datetime and time list of str (no general method to convert str to cftime) - with pytest.raises(OutOfBoundsDatetime): + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": nlmod.dims.set_ds_time(ds, start=start_model, time=["1000-01-02", "1000-01-03"]) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=start_model, time=["1000-01-02", "1000-01-03"] + ) # start cf.datetime and perlen int _ = nlmod.dims.set_ds_time(ds, start=start_model, perlen=365) @@ -122,12 +128,20 @@ def test_time_out_of_bounds(): _ = nlmod.dims.set_ds_time(ds, start="1000-01-01", time=cftime_ind) # start str and time int - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time(ds, start="1000-01-01", time=1) + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time(ds, start="1000-01-01", time=1) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start="1000-01-01", time=1) # start str and time list of int - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time(ds, start="1000-01-01", time=[10, 20, 21, 55]) + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time(ds, start="1000-01-01", time=[10, 20, 21, 55]) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start="1000-01-01", time=[10, 20, 21, 55]) # start str and time list of timestamp _ = nlmod.dims.set_ds_time( @@ -135,18 +149,36 @@ def test_time_out_of_bounds(): ) # start str and time list of str (no general method to convert str to cftime) - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time(ds, start="1000-01-01", time=["1000-2-1", "1000-3-1"]) + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time( + ds, start="1000-01-01", time=["1000-2-1", "1000-3-1"] + ) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start="1000-01-01", time=["1000-2-1", "1000-3-1"] + ) # start str and perlen int - with pytest.raises(OutOfBoundsTimedelta): - nlmod.dims.set_ds_time(ds, start="1000-01-01", perlen=365000) + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time(ds, start="1000-01-01", perlen=365000) + else: + with pytest.raises(OutOfBoundsTimedelta): + nlmod.dims.set_ds_time(ds, start="1000-01-01", perlen=365000) # start numpy datetime and perlen list of int - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time( + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time( ds, start=np.datetime64("1000-01-01"), perlen=[10, 100, 24] ) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=np.datetime64("1000-01-01"), perlen=[10, 100, 24] + ) # start numpy datetime and time list of timestamps _ = nlmod.dims.set_ds_time( @@ -156,16 +188,28 @@ def test_time_out_of_bounds(): ) # start numpy datetime and time list of str - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time( + # gives error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time( ds, start=np.datetime64("1000-01-01"), time=["1000-2-1", "1000-3-1"] ) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=np.datetime64("1000-01-01"), time=["1000-2-1", "1000-3-1"] + ) # start timestamp and perlen list of int - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time( + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time( ds, start=pd.Timestamp("1000-01-01"), perlen=[10, 100, 24] ) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time( + ds, start=pd.Timestamp("1000-01-01"), perlen=[10, 100, 24] + ) # start timestamp and time CFTimeIndex _ = nlmod.dims.set_ds_time(ds, start=pd.Timestamp("1000-01-01"), time=cftime_ind) @@ -174,8 +218,12 @@ def test_time_out_of_bounds(): _ = nlmod.dims.set_ds_time(ds, start=96500, time=cftime_ind) # start int and time timestamp - with pytest.raises(OutOfBoundsDatetime): - nlmod.dims.set_ds_time(ds, start=96500, time=pd.Timestamp("1000-01-01")) + # gives OutOfBoundsDatetime-error below pandas version 3.0 + if pd.__version__ >= "3.0": + _ = nlmod.dims.set_ds_time(ds, start=96500, time=pd.Timestamp("1000-01-01")) + else: + with pytest.raises(OutOfBoundsDatetime): + nlmod.dims.set_ds_time(ds, start=96500, time=pd.Timestamp("1000-01-01")) # start int and time str with pytest.raises(TypeError): diff --git a/tests/test_021_nhi.py b/tests/test_021_nhi.py index 378ad93d..2ea38a10 100644 --- a/tests/test_021_nhi.py +++ b/tests/test_021_nhi.py @@ -5,6 +5,7 @@ import geopandas as gpd import matplotlib.pyplot as plt import numpy as np +import requests import pytest import nlmod @@ -32,16 +33,26 @@ def test_gwo(): password = os.environ["NHI_GWO_PASSWORD"] # download all wells from Brabant Water - wells = nlmod.read.nhi.get_gwo_wells( - username=username, password=password, organisation="Brabant Water" - ) - assert isinstance(wells, gpd.GeoDataFrame) + try: + wells = nlmod.read.nhi.get_gwo_wells( + username=username, password=password, organisation="Brabant Water" + ) + assert isinstance(wells, gpd.GeoDataFrame) - # download extractions from well "13-PP016" of pomping station Veghel - measurements, gdf = nlmod.read.nhi.get_gwo_measurements( - username, password, well_site="veghel", filter__well__name="13-PP016" - ) - assert measurements.reset_index()["Name"].isin(gdf.index).all() + # download extractions from well "13-PP016" of pomping station Veghel + measurements, gdf = nlmod.read.nhi.get_gwo_measurements( + username, password, well_site="veghel", filter__well__name="13-PP016" + ) + assert measurements.reset_index()["Name"].isin(gdf.index).all() + except Exception as e: + allow_network_fail(e) + + +def allow_network_fail(e): + allowed = (requests.exceptions.HTTPError,) + if isinstance(e, allowed): + pytest.skip(f"Network unavailable: {e}") + raise @pytest.mark.skip("too slow") diff --git a/tests/test_026_grid.py b/tests/test_026_grid.py index 683a414a..beb2fc91 100644 --- a/tests/test_026_grid.py +++ b/tests/test_026_grid.py @@ -212,7 +212,7 @@ def test_fillnan_da_uniform_vs_nonuniform(): # Create non-uniform grid by adjusting coordinates ds_nonuniform = ds.copy() - x_coords = ds.x.values + x_coords = ds.x.values.copy() x_coords[5:] += 10 # Make spacing non-uniform ds_nonuniform = ds_nonuniform.assign_coords(x=x_coords)