eo-tides 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
eo_tides-0.0.1/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: eo-tides
3
+ Version: 0.0.1
4
+ Summary: Placeholder
5
+ Author-email: Robbi Bishop-Taylor <Robbi.BishopTaylor@ga.gov.au>
6
+ Project-URL: Homepage, https://GeoscienceAustralia.github.io/eo-tides/
7
+ Project-URL: Repository, https://github.com/GeoscienceAustralia/eo-tides
8
+ Project-URL: Documentation, https://GeoscienceAustralia.github.io/eo-tides/
9
+ Keywords: python
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: geopandas
14
+ Requires-Dist: numpy
15
+ Requires-Dist: odc-geo[xr]
16
+ Requires-Dist: pandas
17
+ Requires-Dist: pyproj
18
+ Requires-Dist: pyTMD==2.1.6
19
+ Requires-Dist: scikit-learn
20
+ Requires-Dist: scipy
21
+ Requires-Dist: shapely
22
+ Requires-Dist: tqdm
23
+ Provides-Extra: notebooks
24
+ Requires-Dist: odc-stac; extra == "notebooks"
25
+ Requires-Dist: pystac-client; extra == "notebooks"
26
+ Requires-Dist: folium; extra == "notebooks"
27
+ Requires-Dist: matplotlib; extra == "notebooks"
28
+
29
+ # eo-tides: Tide modelling tools for Earth Observation
30
+
31
+ [![Release](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)
32
+ [![Build status](https://img.shields.io/github/actions/workflow/status/GeoscienceAustralia/eo-tides/main.yml?branch=main)](https://github.com/GeoscienceAustralia/eo-tides/actions/workflows/main.yml?query=branch%3Amain)
33
+ [![codecov](https://codecov.io/gh/GeoscienceAustralia/eo-tides/branch/main/graph/badge.svg)](https://codecov.io/gh/GeoscienceAustralia/eo-tides)
34
+ [![Commit activity](https://img.shields.io/github/commit-activity/m/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/commit-activity/m/GeoscienceAustralia/eo-tides)
35
+ [![License](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)
36
+
37
+ - **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
38
+ - **Documentation** <https://GeoscienceAustralia.github.io/eo-tides/>
@@ -0,0 +1,10 @@
1
+ # eo-tides: Tide modelling tools for Earth Observation
2
+
3
+ [![Release](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)
4
+ [![Build status](https://img.shields.io/github/actions/workflow/status/GeoscienceAustralia/eo-tides/main.yml?branch=main)](https://github.com/GeoscienceAustralia/eo-tides/actions/workflows/main.yml?query=branch%3Amain)
5
+ [![codecov](https://codecov.io/gh/GeoscienceAustralia/eo-tides/branch/main/graph/badge.svg)](https://codecov.io/gh/GeoscienceAustralia/eo-tides)
6
+ [![Commit activity](https://img.shields.io/github/commit-activity/m/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/commit-activity/m/GeoscienceAustralia/eo-tides)
7
+ [![License](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)
8
+
9
+ - **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
10
+ - **Documentation** <https://GeoscienceAustralia.github.io/eo-tides/>
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: eo-tides
3
+ Version: 0.0.1
4
+ Summary: Placeholder
5
+ Author-email: Robbi Bishop-Taylor <Robbi.BishopTaylor@ga.gov.au>
6
+ Project-URL: Homepage, https://GeoscienceAustralia.github.io/eo-tides/
7
+ Project-URL: Repository, https://github.com/GeoscienceAustralia/eo-tides
8
+ Project-URL: Documentation, https://GeoscienceAustralia.github.io/eo-tides/
9
+ Keywords: python
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: geopandas
14
+ Requires-Dist: numpy
15
+ Requires-Dist: odc-geo[xr]
16
+ Requires-Dist: pandas
17
+ Requires-Dist: pyproj
18
+ Requires-Dist: pyTMD==2.1.6
19
+ Requires-Dist: scikit-learn
20
+ Requires-Dist: scipy
21
+ Requires-Dist: shapely
22
+ Requires-Dist: tqdm
23
+ Provides-Extra: notebooks
24
+ Requires-Dist: odc-stac; extra == "notebooks"
25
+ Requires-Dist: pystac-client; extra == "notebooks"
26
+ Requires-Dist: folium; extra == "notebooks"
27
+ Requires-Dist: matplotlib; extra == "notebooks"
28
+
29
+ # eo-tides: Tide modelling tools for Earth Observation
30
+
31
+ [![Release](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/v/release/GeoscienceAustralia/eo-tides)
32
+ [![Build status](https://img.shields.io/github/actions/workflow/status/GeoscienceAustralia/eo-tides/main.yml?branch=main)](https://github.com/GeoscienceAustralia/eo-tides/actions/workflows/main.yml?query=branch%3Amain)
33
+ [![codecov](https://codecov.io/gh/GeoscienceAustralia/eo-tides/branch/main/graph/badge.svg)](https://codecov.io/gh/GeoscienceAustralia/eo-tides)
34
+ [![Commit activity](https://img.shields.io/github/commit-activity/m/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/commit-activity/m/GeoscienceAustralia/eo-tides)
35
+ [![License](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)](https://img.shields.io/github/license/GeoscienceAustralia/eo-tides)
36
+
37
+ - **Github repository**: <https://github.com/GeoscienceAustralia/eo-tides/>
38
+ - **Documentation** <https://GeoscienceAustralia.github.io/eo-tides/>
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ eo_tides.egg-info/PKG-INFO
5
+ eo_tides.egg-info/SOURCES.txt
6
+ eo_tides.egg-info/dependency_links.txt
7
+ eo_tides.egg-info/requires.txt
8
+ eo_tides.egg-info/top_level.txt
9
+ tests/test_model.py
10
+ tests/test_validation.py
@@ -0,0 +1,16 @@
1
+ geopandas
2
+ numpy
3
+ odc-geo[xr]
4
+ pandas
5
+ pyproj
6
+ pyTMD==2.1.6
7
+ scikit-learn
8
+ scipy
9
+ shapely
10
+ tqdm
11
+
12
+ [notebooks]
13
+ odc-stac
14
+ pystac-client
15
+ folium
16
+ matplotlib
@@ -0,0 +1 @@
1
+ eo_tides
@@ -0,0 +1,102 @@
1
+ [project]
2
+ name = "eo-tides"
3
+ version = "0.0.1"
4
+ description = "Placeholder"
5
+ authors = [{ name = "Robbi Bishop-Taylor", email = "Robbi.BishopTaylor@ga.gov.au" }]
6
+ readme = "README.md"
7
+ keywords = ['python']
8
+ requires-python = ">=3.9"
9
+ dependencies = [
10
+ "geopandas",
11
+ "numpy",
12
+ "odc-geo[xr]",
13
+ "pandas",
14
+ "pyproj",
15
+ "pyTMD==2.1.6",
16
+ # "pyTMD@git+https://github.com/tsutterley/pyTMD",
17
+ "scikit-learn",
18
+ "scipy",
19
+ "shapely",
20
+ "tqdm",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://GeoscienceAustralia.github.io/eo-tides/"
25
+ Repository = "https://github.com/GeoscienceAustralia/eo-tides"
26
+ Documentation = "https://GeoscienceAustralia.github.io/eo-tides/"
27
+
28
+ [project.optional-dependencies]
29
+ notebooks = [
30
+ "odc-stac",
31
+ "pystac-client",
32
+ "folium",
33
+ "matplotlib",
34
+ ]
35
+
36
+ [tool.uv]
37
+ dev-dependencies = [
38
+ "pytest>=7.2.0",
39
+ "nbval>=0.11.0",
40
+ "pre-commit>=2.20.0",
41
+ "tox>=3.25.1",
42
+ "deptry>=0.20.0",
43
+ "mypy>=0.991",
44
+ "pytest-cov>=4.0.0",
45
+ "ruff>=0.0.235",
46
+ "mkdocs>=1.4.2",
47
+ "mkdocs-material>=8.5.10",
48
+ "mkdocs-jupyter>=0.25.0",
49
+ "mkdocstrings[python]>=0.19.0",
50
+ "black",
51
+ "odc-stac",
52
+ "pystac-client",
53
+ ]
54
+
55
+ [build-system]
56
+ requires = ["setuptools >= 61.0"]
57
+ build-backend = "setuptools.build_meta"
58
+
59
+ [tool.mypy]
60
+ files = ["eo_tides"]
61
+ python_version = "3.10"
62
+ ignore_missing_imports = "True"
63
+ allow_redefinition = "True"
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+
68
+ [tool.ruff]
69
+ target-version = "py310"
70
+ line-length = 120
71
+ fix = true
72
+ lint.ignore = [
73
+ # LineTooLong
74
+ "E501",
75
+ # DoNotAssignLambda
76
+ "E731",
77
+ # Unused import
78
+ "F401"
79
+ ]
80
+
81
+ [tool.ruff.format]
82
+ preview = true
83
+
84
+ [tool.ruff.lint.per-file-ignores]
85
+ "tests/*" = ["S101"]
86
+
87
+ [tool.deptry.per_rule_ignores]
88
+ DEP002 = [
89
+ "matplotlib",
90
+ "folium",
91
+ "pystac-client",
92
+ ]
93
+
94
+ [tool.coverage.report]
95
+ skip_empty = true
96
+
97
+ [tool.coverage.run]
98
+ branch = true
99
+ source = ["eo_tides"]
100
+
101
+ [tool.setuptools]
102
+ py-modules = ["eo_tides"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,520 @@
1
+ import geopandas as gpd
2
+ import numpy as np
3
+ import odc.stac
4
+ import pandas as pd
5
+ import pystac_client
6
+ import pytest
7
+ import xarray as xr
8
+
9
+ from eo_tides.model import model_tides, pixel_tides
10
+ from eo_tides.validation import eval_metrics
11
+
12
+ GAUGE_X = 122.2183
13
+ GAUGE_Y = -18.0008
14
+ ENSEMBLE_MODELS = ["FES2014", "HAMTIDE11"] # simplified for tests
15
+
16
+
17
+ @pytest.fixture()
18
+ def measured_tides_ds():
19
+ """
20
+ Load measured sea level data from the Broome ABSLMP tidal station:
21
+ http://www.bom.gov.au/oceanography/projects/abslmp/data/data.shtml
22
+ """
23
+ # Metadata for Broome ABSLMP tidal station:
24
+ # http://www.bom.gov.au/oceanography/projects/abslmp/data/data.shtml
25
+ ahd_offset = -5.322
26
+
27
+ # Load measured tides from ABSLMP tide gauge data
28
+ measured_tides_df = pd.read_csv(
29
+ "tests/data/IDO71013_2020.csv",
30
+ index_col=0,
31
+ parse_dates=True,
32
+ na_values=-9999,
33
+ )[["Sea Level"]]
34
+
35
+ # Update index and column names
36
+ measured_tides_df.index.name = "time"
37
+ measured_tides_df.columns = ["tide_m"]
38
+
39
+ # Apply station AHD offset
40
+ measured_tides_df += ahd_offset
41
+
42
+ # Return as xarray dataset
43
+ return measured_tides_df.to_xarray()
44
+
45
+
46
+ # Create test data in different CRSs and resolutions
47
+ @pytest.fixture(
48
+ params=[
49
+ ("EPSG:3577", 30), # Australian Albers 30 m pixels
50
+ ("EPSG:4326", 0.00025), # WGS84, 0.0025 degree pixels
51
+ ],
52
+ ids=["satellite_ds_epsg3577", "satellite_ds_epsg4326"],
53
+ )
54
+ def satellite_ds(request):
55
+ """
56
+ Load a sample timeseries of Landsat 8 data using odc-stac
57
+ """
58
+ # Obtain CRS and resolution params
59
+ crs, res = request.param
60
+
61
+ # Connect to stac catalogue
62
+ catalog = pystac_client.Client.open("https://explorer.dea.ga.gov.au/stac")
63
+
64
+ # Set cloud defaults
65
+ odc.stac.configure_rio(
66
+ cloud_defaults=True,
67
+ aws={"aws_unsigned": True},
68
+ )
69
+
70
+ # Build a query with the parameters above
71
+ bbox = [GAUGE_X - 0.08, GAUGE_Y - 0.08, GAUGE_X + 0.08, GAUGE_Y + 0.08]
72
+ query = catalog.search(
73
+ bbox=bbox,
74
+ collections=["ga_ls8c_ard_3"],
75
+ datetime="2020-01/2020-02",
76
+ )
77
+
78
+ # Search the STAC catalog for all items matching the query
79
+ ds = odc.stac.load(
80
+ list(query.items()),
81
+ bands=["nbart_red"],
82
+ crs=crs,
83
+ resolution=res,
84
+ groupby="solar_day",
85
+ bbox=bbox,
86
+ fail_on_error=False,
87
+ chunks={},
88
+ )
89
+
90
+ return ds
91
+
92
+
93
+ # Run test for multiple input coordinates, CRSs and interpolation methods
94
+ @pytest.mark.parametrize(
95
+ "x, y, crs, method",
96
+ [
97
+ (GAUGE_X, GAUGE_Y, "EPSG:4326", "bilinear"), # WGS84, bilinear interp
98
+ (GAUGE_X, GAUGE_Y, "EPSG:4326", "spline"), # WGS84, spline interp
99
+ (
100
+ -1034913,
101
+ -1961916,
102
+ "EPSG:3577",
103
+ "bilinear",
104
+ ), # Australian Albers, bilinear interp
105
+ ],
106
+ )
107
+ def test_model_tides(measured_tides_ds, x, y, crs, method):
108
+ # Run FES2014 tidal model for locations and timesteps in tide gauge data
109
+ modelled_tides_df = model_tides(
110
+ x=[x],
111
+ y=[y],
112
+ time=measured_tides_ds.time,
113
+ crs=crs,
114
+ method=method,
115
+ )
116
+
117
+ # Compare measured and modelled tides
118
+ val_stats = eval_metrics(x=measured_tides_ds.tide_m, y=modelled_tides_df.tide_m)
119
+
120
+ # Test that modelled tides contain correct headings and have same
121
+ # number of timesteps
122
+ assert modelled_tides_df.index.names == ["time", "x", "y"]
123
+ assert modelled_tides_df.columns.tolist() == ["tide_model", "tide_m"]
124
+ assert len(modelled_tides_df.index) == len(measured_tides_ds.time)
125
+
126
+ # Test that modelled tides meet expected accuracy
127
+ assert val_stats["Correlation"] > 0.99
128
+ assert val_stats["RMSE"] < 0.26
129
+ assert val_stats["R-squared"] > 0.96
130
+ assert abs(val_stats["Bias"]) < 0.20
131
+
132
+
133
+ # Run tests for one or multiple models, and long and wide format outputs
134
+ @pytest.mark.parametrize(
135
+ "models, output_format",
136
+ [
137
+ (["FES2014"], "long"),
138
+ (["FES2014"], "wide"),
139
+ (["FES2014", "HAMTIDE11"], "long"),
140
+ (["FES2014", "HAMTIDE11"], "wide"),
141
+ ],
142
+ ids=[
143
+ "single_model_long",
144
+ "single_model_wide",
145
+ "multiple_models_long",
146
+ "multiple_models_wide",
147
+ ],
148
+ )
149
+ def test_model_tides_multiplemodels(measured_tides_ds, models, output_format):
150
+ # Model tides for one or multiple tide models and output formats
151
+ modelled_tides_df = model_tides(
152
+ x=[GAUGE_X],
153
+ y=[GAUGE_Y],
154
+ time=measured_tides_ds.time,
155
+ model=models,
156
+ output_format=output_format,
157
+ )
158
+
159
+ if output_format == "long":
160
+ # Verify output has correct columns
161
+ assert modelled_tides_df.index.names == ["time", "x", "y"]
162
+ assert modelled_tides_df.columns.tolist() == ["tide_model", "tide_m"]
163
+
164
+ # Verify tide model column contains correct values
165
+ assert modelled_tides_df.tide_model.unique().tolist() == models
166
+
167
+ # Verify that dataframe has length of original timesteps multipled by
168
+ # n models
169
+ assert len(modelled_tides_df.index) == len(measured_tides_ds.time) * len(models)
170
+
171
+ elif output_format == "wide":
172
+ # Verify output has correct columns
173
+ assert modelled_tides_df.index.names == ["time", "x", "y"]
174
+ assert modelled_tides_df.columns.tolist() == models
175
+
176
+ # Verify output has same length as orginal timesteps
177
+ assert len(modelled_tides_df.index) == len(measured_tides_ds.time)
178
+
179
+
180
+ # Run tests for each unit, providing expected outputs
181
+ @pytest.mark.parametrize(
182
+ "units, expected_range, expected_dtype",
183
+ [("m", 10, "float32"), ("cm", 1000, "int16"), ("mm", 10000, "int16")],
184
+ ids=["metres", "centimetres", "millimetres"],
185
+ )
186
+ def test_model_tides_units(measured_tides_ds, units, expected_range, expected_dtype):
187
+ # Model tides
188
+ modelled_tides_df = model_tides(
189
+ x=[GAUGE_X],
190
+ y=[GAUGE_Y],
191
+ time=measured_tides_ds.time,
192
+ output_units=units,
193
+ )
194
+
195
+ # Calculate tide range
196
+ tide_range = modelled_tides_df.tide_m.max() - modelled_tides_df.tide_m.min()
197
+
198
+ # Verify tide range and dtypes are as expected for unit
199
+ assert np.isclose(tide_range, expected_range, rtol=0.01)
200
+ assert modelled_tides_df.tide_m.dtype == expected_dtype
201
+
202
+
203
+ # Run test for each combination of mode, output format, and one or
204
+ # multiple tide models
205
+ @pytest.mark.parametrize(
206
+ "mode, models, output_format",
207
+ [
208
+ ("one-to-many", ["FES2014"], "long"),
209
+ ("one-to-one", ["FES2014"], "long"),
210
+ ("one-to-many", ["FES2014"], "wide"),
211
+ ("one-to-one", ["FES2014"], "wide"),
212
+ ("one-to-many", ["FES2014", "HAMTIDE11"], "long"),
213
+ ("one-to-one", ["FES2014", "HAMTIDE11"], "long"),
214
+ ("one-to-many", ["FES2014", "HAMTIDE11"], "wide"),
215
+ ("one-to-one", ["FES2014", "HAMTIDE11"], "wide"),
216
+ ],
217
+ )
218
+ def test_model_tides_mode(mode, models, output_format):
219
+ # Input params
220
+ x = [122.14, 122.30, 122.12]
221
+ y = [-17.91, -17.92, -18.07]
222
+ times = pd.date_range("2020", "2021", periods=3)
223
+
224
+ # Model tides
225
+ modelled_tides_df = model_tides(
226
+ x=x,
227
+ y=y,
228
+ time=times,
229
+ mode=mode,
230
+ output_format=output_format,
231
+ model=models,
232
+ )
233
+
234
+ if mode == "one-to-one":
235
+ if output_format == "wide":
236
+ # Should have the same number of rows as input x, y, times
237
+ assert len(modelled_tides_df.index) == len(x)
238
+ assert len(modelled_tides_df.index) == len(times)
239
+
240
+ # Output indexes should match order of input x, y, times
241
+ assert all(modelled_tides_df.index.get_level_values("time") == times)
242
+ assert all(modelled_tides_df.index.get_level_values("x") == x)
243
+ assert all(modelled_tides_df.index.get_level_values("y") == y)
244
+
245
+ elif output_format == "long":
246
+ # In "long" format, the number of x, y points multiplied by
247
+ # the number of tide models
248
+ assert len(modelled_tides_df.index) == len(x) * len(models)
249
+
250
+ # Verify index values match expected x, y, time order
251
+ assert all(modelled_tides_df.index.get_level_values("time") == np.tile(times, len(models)))
252
+ assert all(modelled_tides_df.index.get_level_values("x") == np.tile(x, len(models)))
253
+ assert all(modelled_tides_df.index.get_level_values("y") == np.tile(y, len(models)))
254
+
255
+ if mode == "one-to-many":
256
+ if output_format == "wide":
257
+ # In "wide" output format, the number of rows should equal
258
+ # the number of x, y points multiplied by timesteps
259
+ assert len(modelled_tides_df.index) == len(x) * len(times)
260
+
261
+ # TODO: Work out what order rows should be returned in in
262
+ # "one-to-many" and "wide" mode
263
+
264
+ elif output_format == "long":
265
+ # In "long" output format, the number of rows should equal
266
+ # the number of x, y points multiplied by timesteps and
267
+ # the number of tide models
268
+ assert len(modelled_tides_df.index) == len(x) * len(times) * len(models)
269
+
270
+ # Verify index values match expected x, y, time order
271
+ assert all(modelled_tides_df.index.get_level_values("time") == np.tile(times, len(x) * len(models)))
272
+ assert all(modelled_tides_df.index.get_level_values("x") == np.tile(np.repeat(x, len(times)), len(models)))
273
+ assert all(modelled_tides_df.index.get_level_values("y") == np.tile(np.repeat(y, len(times)), len(models)))
274
+
275
+
276
+ # Test ensemble modelling functionality
277
+ def test_model_tides_ensemble():
278
+ # Input params
279
+ x = [122.14, 144.910368]
280
+ y = [-17.91, -37.919491]
281
+ times = pd.date_range("2020", "2021", periods=2)
282
+
283
+ # Default, only ensemble requested
284
+ modelled_tides_df = model_tides(
285
+ x=x,
286
+ y=y,
287
+ time=times,
288
+ model="ensemble",
289
+ ensemble_models=ENSEMBLE_MODELS,
290
+ )
291
+
292
+ assert modelled_tides_df.index.names == ["time", "x", "y"]
293
+ assert modelled_tides_df.columns.tolist() == ["tide_model", "tide_m"]
294
+ assert all(modelled_tides_df.tide_model == "ensemble")
295
+
296
+ # Default, ensemble + other models requested
297
+ models = ["FES2014", "HAMTIDE11", "ensemble"]
298
+ modelled_tides_df = model_tides(
299
+ x=x,
300
+ y=y,
301
+ time=times,
302
+ model=models,
303
+ ensemble_models=ENSEMBLE_MODELS,
304
+ )
305
+
306
+ assert modelled_tides_df.index.names == ["time", "x", "y"]
307
+ assert modelled_tides_df.columns.tolist() == ["tide_model", "tide_m"]
308
+ assert set(modelled_tides_df.tide_model) == set(models)
309
+ assert np.allclose(
310
+ modelled_tides_df.tide_m,
311
+ [
312
+ -2.831,
313
+ -1.897,
314
+ -0.207,
315
+ 0.035,
316
+ -2.655,
317
+ -1.772,
318
+ 0.073,
319
+ -0.071,
320
+ -2.743,
321
+ -1.835,
322
+ -0.067,
323
+ -0.018,
324
+ ],
325
+ atol=0.02,
326
+ )
327
+
328
+ # One-to-one mode
329
+ modelled_tides_df = model_tides(
330
+ x=x,
331
+ y=y,
332
+ time=times,
333
+ model=models,
334
+ mode="one-to-one",
335
+ ensemble_models=ENSEMBLE_MODELS,
336
+ )
337
+
338
+ assert modelled_tides_df.index.names == ["time", "x", "y"]
339
+ assert modelled_tides_df.columns.tolist() == ["tide_model", "tide_m"]
340
+ assert set(modelled_tides_df.tide_model) == set(models)
341
+
342
+ # Wide mode, default
343
+ modelled_tides_df = model_tides(
344
+ x=x,
345
+ y=y,
346
+ time=times,
347
+ model=models,
348
+ output_format="wide",
349
+ ensemble_models=ENSEMBLE_MODELS,
350
+ )
351
+
352
+ # Check that expected models exist, and that ensemble is approx average
353
+ # of other two models
354
+ assert set(modelled_tides_df.columns) == set(models)
355
+ assert np.allclose(
356
+ 0.5 * (modelled_tides_df.FES2014 + modelled_tides_df.HAMTIDE11),
357
+ modelled_tides_df.ensemble,
358
+ )
359
+
360
+ # Wide mode, top n == 1
361
+ modelled_tides_df = model_tides(
362
+ x=x,
363
+ y=y,
364
+ time=times,
365
+ model=models,
366
+ output_format="wide",
367
+ ensemble_top_n=1,
368
+ ensemble_models=ENSEMBLE_MODELS,
369
+ )
370
+
371
+ # Check that expected models exist, and that ensemble is equal to at
372
+ # least one of the other models
373
+ assert set(modelled_tides_df.columns) == set(models)
374
+ assert all(
375
+ (modelled_tides_df.FES2014 == modelled_tides_df.ensemble)
376
+ | (modelled_tides_df.HAMTIDE11 == modelled_tides_df.ensemble)
377
+ )
378
+
379
+ # Check that correct model is the closest at each row
380
+ closer_model = modelled_tides_df.apply(
381
+ lambda row: (
382
+ "FES2014"
383
+ if abs(row["ensemble"] - row["FES2014"]) < abs(row["ensemble"] - row["HAMTIDE11"])
384
+ else "HAMTIDE11"
385
+ ),
386
+ axis=1,
387
+ ).tolist()
388
+ assert closer_model == ["FES2014", "HAMTIDE11", "FES2014", "HAMTIDE11"]
389
+
390
+ # Check values are expected
391
+ assert np.allclose(modelled_tides_df.ensemble, [-2.830, 0.073, -1.900, -0.072], atol=0.02)
392
+
393
+ # Wide mode, custom functions
394
+ ensemble_funcs = {
395
+ "ensemble-best": lambda x: x["rank"] == 1,
396
+ "ensemble-worst": lambda x: x["rank"] == 2,
397
+ "ensemble-mean-top2": lambda x: x["rank"].isin([1, 2]),
398
+ "ensemble-mean-weighted": lambda x: 3 - x["rank"],
399
+ "ensemble-mean": lambda x: x["rank"] <= 2,
400
+ }
401
+ modelled_tides_df = model_tides(
402
+ x=x,
403
+ y=y,
404
+ time=times,
405
+ model=models,
406
+ output_format="wide",
407
+ ensemble_func=ensemble_funcs,
408
+ ensemble_models=ENSEMBLE_MODELS,
409
+ )
410
+
411
+ # Check that expected models exist, and that valid data is produced
412
+ assert set(modelled_tides_df.columns) == set([
413
+ "FES2014",
414
+ "HAMTIDE11",
415
+ "ensemble-best",
416
+ "ensemble-worst",
417
+ "ensemble-mean-top2",
418
+ "ensemble-mean-weighted",
419
+ "ensemble-mean",
420
+ ])
421
+ assert all(modelled_tides_df.notnull())
422
+
423
+ # Long mode, custom functions
424
+ modelled_tides_df = model_tides(
425
+ x=x,
426
+ y=y,
427
+ time=times,
428
+ model=models,
429
+ output_format="long",
430
+ ensemble_func=ensemble_funcs,
431
+ ensemble_models=ENSEMBLE_MODELS,
432
+ )
433
+
434
+ # Check that expected models exist in "tide_model" column
435
+ assert set(modelled_tides_df.tide_model) == set([
436
+ "FES2014",
437
+ "HAMTIDE11",
438
+ "ensemble-best",
439
+ "ensemble-worst",
440
+ "ensemble-mean-top2",
441
+ "ensemble-mean-weighted",
442
+ "ensemble-mean",
443
+ ])
444
+
445
+
446
+ # Run tests for default and custom resolutions
447
+ @pytest.mark.parametrize("resolution", [None, "custom"])
448
+ def test_pixel_tides(satellite_ds, measured_tides_ds, resolution):
449
+ # Use different custom resolution depending on CRS
450
+ if resolution == "custom":
451
+ resolution = 0.1 if satellite_ds.odc.geobox.crs.geographic else 10000
452
+
453
+ # Model tides using `pixel_tides`
454
+ modelled_tides_ds, modelled_tides_lowres = pixel_tides(satellite_ds, resolution=resolution)
455
+
456
+ # Interpolate measured tide data to same timesteps
457
+ measured_tides_ds = measured_tides_ds.interp(time=satellite_ds.time, method="linear")
458
+
459
+ # Assert that modelled tides have the same shape and dims as
460
+ # arrays in `satellite_ds`
461
+ assert modelled_tides_ds.shape == satellite_ds.nbart_red.shape
462
+ assert modelled_tides_ds.dims == satellite_ds.nbart_red.dims
463
+
464
+ # Assert that high res and low res data have the same dims
465
+ assert modelled_tides_ds.dims == modelled_tides_lowres.dims
466
+
467
+ # Test through time at tide gauge
468
+
469
+ # Create tide gauge point, and reproject to dataset CRS
470
+ tide_gauge_point = gpd.points_from_xy(
471
+ x=[GAUGE_X],
472
+ y=[GAUGE_Y],
473
+ crs="EPSG:4326",
474
+ ).to_crs(satellite_ds.odc.geobox.crs)
475
+
476
+ try:
477
+ modelled_tides_gauge = modelled_tides_ds.sel(
478
+ y=tide_gauge_point[0].y,
479
+ x=tide_gauge_point[0].x,
480
+ method="nearest",
481
+ )
482
+ except KeyError:
483
+ modelled_tides_gauge = modelled_tides_ds.sel(
484
+ latitude=tide_gauge_point[0].y,
485
+ longitude=tide_gauge_point[0].x,
486
+ method="nearest",
487
+ )
488
+
489
+ # Calculate accuracy stats
490
+ gauge_stats = eval_metrics(x=measured_tides_ds.tide_m, y=modelled_tides_gauge)
491
+
492
+ # Assert pixel_tide outputs are accurate
493
+ assert gauge_stats["Correlation"] > 0.99
494
+ assert gauge_stats["RMSE"] < 0.26
495
+ assert gauge_stats["R-squared"] > 0.96
496
+ assert abs(gauge_stats["Bias"]) < 0.20
497
+
498
+ # Test spatially for a single timestep at corners of array
499
+
500
+ # Create test points, reproject to dataset CRS, and extract coords
501
+ # as xr.DataArrays so we can select data from our array
502
+ points = gpd.points_from_xy(
503
+ x=[122.14438, 122.30304, 122.12964, 122.29235],
504
+ y=[-17.91625, -17.92713, -18.07656, -18.08751],
505
+ crs="EPSG:4326",
506
+ ).to_crs(satellite_ds.odc.geobox.crs)
507
+ x_coords = xr.DataArray(points.x, dims=["point"])
508
+ y_coords = xr.DataArray(points.y, dims=["point"])
509
+
510
+ # Extract modelled tides for each corner
511
+ try:
512
+ extracted_tides = modelled_tides_ds.sel(x=x_coords, y=y_coords, time="2020-01-29", method="nearest")
513
+ except KeyError:
514
+ extracted_tides = modelled_tides_ds.sel(
515
+ longitude=x_coords, latitude=y_coords, time="2020-01-29", method="nearest"
516
+ )
517
+
518
+ # Test if extracted tides match expected results (to within ~3 cm)
519
+ expected_tides = [-0.66, -0.76, -0.75, -0.82]
520
+ assert np.allclose(extracted_tides.values, expected_tides, atol=0.03)
@@ -0,0 +1,57 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from eo_tides.validation import load_gauge_gesla
5
+
6
+ GAUGE_X = 122.2183
7
+ GAUGE_Y = -18.0008
8
+
9
+
10
+ # Run test for different spatial searches
11
+ @pytest.mark.parametrize(
12
+ "x, y, site_code, max_distance, correct_mean, expected",
13
+ [
14
+ # Test nearest gauge lookup
15
+ (GAUGE_X, GAUGE_Y, None, None, False, ["62650"]),
16
+ (-117.4, 32.6, None, None, False, ["569A"]),
17
+ (152.0, -33.0, None, None, True, ["60370"]),
18
+ pytest.param(
19
+ GAUGE_X + 1, GAUGE_Y, None, 0.1, False, ["62650"], marks=pytest.mark.xfail(reason="No nearest gauge")
20
+ ),
21
+ # Test bounding box lookup
22
+ ((GAUGE_X - 0.2, GAUGE_X + 0.2), (GAUGE_Y - 0.2, GAUGE_Y + 0.2), None, None, False, ["62650"]),
23
+ ((100, 160), (-5, -45), None, None, False, ["60370", "62650"]),
24
+ # Test site_code lookup
25
+ (None, None, "62650", None, False, ["62650"]),
26
+ (None, None, ["60370", "62650"], None, False, ["60370", "62650"]),
27
+ ],
28
+ ids=[
29
+ "broome_xy",
30
+ "sandiego_xy",
31
+ "syd_xy_correctmean",
32
+ "no_nearest",
33
+ "broome_bbox",
34
+ "aus_bbox",
35
+ "broome_code",
36
+ "aus_code",
37
+ ],
38
+ )
39
+ def test_load_gauge_gesla(x, y, site_code, max_distance, correct_mean, expected):
40
+ # Load gauge data
41
+ gauge_df = load_gauge_gesla(
42
+ x=x,
43
+ y=y,
44
+ site_code=site_code,
45
+ max_distance=max_distance,
46
+ correct_mean=correct_mean,
47
+ time=("2018-01-01", "2018-01-20"),
48
+ data_path="tests/data/",
49
+ metadata_path="tests/data/GESLA3_ALL 2.csv",
50
+ )
51
+
52
+ assert "sea_level" in gauge_df.columns
53
+ assert set(gauge_df.index.unique(level="site_code")) == set(expected)
54
+
55
+ # Verify that mean is near 0 after subtracting mean from time series
56
+ if correct_mean:
57
+ assert np.isclose(gauge_df.sea_level.mean().item(), 0.0, atol=0.01)