diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index edc195ff..00000000 --- a/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -examples/data/*.grib filter=lfs diff=lfs merge=lfs -text -examples/data/*.jpg filter=lfs diff=lfs merge=lfs -text -examples/data/*.shp filter=lfs diff=lfs merge=lfs -text -tests/data/*.grib filter=lfs diff=lfs merge=lfs -text diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index e2bbefe3..00000000 --- a/.gitconfig +++ /dev/null @@ -1,2 +0,0 @@ -[lfs] - fetchexclude = * \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4b2f16b7..fb1be91e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,25 +1,19 @@ name: ci - on: # Trigger the workflow on push to master or develop, except tag creation push: branches: - 'main' - 'develop' - # Trigger the workflow on pull request pull_request: ~ - # Trigger the workflow manually workflow_dispatch: ~ - # Trigger after public PR approved for CI pull_request_target: types: [labeled] - release: types: [created] - jobs: qa: name: qa @@ -48,7 +42,6 @@ jobs: - name: Check flake8 run: flake8 . - setup: name: setup runs-on: ubuntu-20.04 @@ -95,7 +88,6 @@ jobs: run: | echo inputs=$(echo "${{ inputs.build_package_inputs || '{}' }}" | yq eval '.' --output-format json --indent 0 -) >> $GITHUB_OUTPUT echo inputs-for-ubuntu=$(echo "${{ inputs.build_package_inputs || '{}' }}" | yq eval '. * {"os":"ubuntu-20.04","compiler":"gnu-10","compiler_cc":"gcc-10","compiler_cxx":"g++-10","compiler_fc":"gfortran-10"}' --output-format json --indent 0 -) >> $GITHUB_OUTPUT - test: name: test needs: @@ -106,20 +98,16 @@ jobs: matrix: ${{ fromJson(needs.setup.outputs.matrix) }} runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Install eccodes and Dependencies + id: install-dependencies + uses: ecmwf-actions/build-package@v2 with: - fetch-depth: 3 # enable full clone to download Git LFS files - - name: Change write permission - run: chmod -R 777 ./tests/data/ - - name: Install Git LFS - run: | - sudo apt-get install git-lfs - git lfs install - - name: Download Git LFS file - run: | - git lfs pull - - name: Install eccodes - run: sudo apt-get install -y libeccodes-dev + self_build: false + dependencies: | + ecmwf/ecbuild@develop + MathisRosenhauer/libaec@master + ecmwf/eccodes@develop - name: Setup Python uses: actions/setup-python@v4 @@ -143,23 +131,18 @@ jobs: LD_LIBRARY_PATH: ${{ steps.install-dependencies.outputs.lib_path }} shell: bash -eux {0} run: | - DYLD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} python -m pytest tests --cov=./ --cov-report=xml + DYLD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} python -m pytest -m "not fdb" tests --cov=./ --cov-report=xml python -m coverage report - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: files: coverage.xml - deploy: needs: test - if: ${{ github.event_name == 'release' }} - name: Upload to Pypi - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 - name: Set up Python @@ -172,8 +155,8 @@ jobs: pip install setuptools wheel twine - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_USERNAME: "__token__" + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python setup.py sdist - twine upload dist/* + twine upload dist/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index 037872e6..c28eebef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,15 @@ polytope.egg-info .pytest_cache *.prof -*.idx \ No newline at end of file +*.idx +*.grib +*.xml +site +.coverage +*.grib +*.gif +*.html +example_eo +example_mri +.mypy_cache +*.req \ No newline at end of file diff --git a/ACKNOWLEDGEMENTS.rst b/ACKNOWLEDGEMENTS.rst new file mode 100644 index 00000000..af7ce40b --- /dev/null +++ b/ACKNOWLEDGEMENTS.rst @@ -0,0 +1,8 @@ +Acknowledgements +================ + +Destination Earth + This software is developed with co-funding by the European Union under the Destination Earth initiative. + +LEXIS + This software is part of a project that has received funding from the European Union’s Horizon 2020 research and innovation programme under grant agreement No 825532. \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..d2086657 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,20 @@ +Contributing to *polytope* +======================================== + +Please report bug_ reports or pull-requests_ on GitHub_ + +.. _bug: https://github.com/ecmwf/polytope/issues + +.. _pull-requests: https://github.com/ecmwf/polytope/pulls + +.. _GitHub: https://github.com/ecmwf/polytope + +We want your feedback, please e-mail: user-services@ecmwf.int + +The package is installed from PyPI with:: + + $ pip install polytope-python + +How you could use polytope: + * plot outputs, examples are provided in the examples folder + * go further than our examples and let us know how it goes \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..540b7204 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt \ No newline at end of file diff --git a/examples/3D_shipping_route.py b/examples/3D_shipping_route.py index 53d28168..57322b51 100644 --- a/examples/3D_shipping_route.py +++ b/examples/3D_shipping_route.py @@ -8,14 +8,14 @@ from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request -from polytope.shapes import Ellipsoid, Path +from polytope.shapes import Ellipsoid, Path, Select class Test: def setup_method(self): ds = data.from_source("file", "./examples/data/winds.grib") array = ds.to_xarray() - array = array.isel(time=0).isel(surface=0).isel(number=0) + array = array.isel(time=0).isel(surface=0).isel(number=0).u10 self.array = array self.slicer = HullSlicer() self.API = Polytope(datacube=array, engine=self.slicer) @@ -58,15 +58,15 @@ def test_slice_shipping_route(self): # Then somehow make this list of points into just a sequence of points ship_route_polytope = Path(["latitude", "longitude", "step"], initial_shape, *new_points) - request = Request(ship_route_polytope) + request = Request( + ship_route_polytope, Select("number", [0]), Select("surface", [0]), Select("time", ["2022-09-30T12:00:00"]) + ) result = self.API.retrieve(request) # Associate the results to the lat/long points in an array lats = [] longs = [] parameter_values = [] - winds_u = [] - winds_v = [] for i in range(len(result.leaves)): cubepath = result.leaves[i].flatten() lat = cubepath["latitude"] @@ -74,13 +74,9 @@ def test_slice_shipping_route(self): lats.append(lat) longs.append(long) - u10_idx = result.leaves[i].result["u10"] + u10_idx = result.leaves[i].result[1] wind_u = u10_idx - v10_idx = result.leaves[i].result["v10"] - wind_v = v10_idx - winds_u.append(wind_u) - winds_v.append(wind_v) - parameter_values.append(math.sqrt(wind_u**2 + wind_v**2)) + parameter_values.append(wind_u) parameter_values = np.array(parameter_values) # Plot this last array according to different colors for the result on a world map diff --git a/examples/3D_shipping_route_wave_model.py b/examples/3D_shipping_route_wave_model.py new file mode 100644 index 00000000..e4edabe9 --- /dev/null +++ b/examples/3D_shipping_route_wave_model.py @@ -0,0 +1,131 @@ +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pytest +from eccodes import codes_grib_find_nearest, codes_grib_new_from_file + +from polytope.datacube.backends.fdb import FDBDatacube +from polytope.engine.hullslicer import HullSlicer +from polytope.polytope import Polytope, Request +from polytope.shapes import Ellipsoid, Path, Select +from tests.helper_functions import download_test_data + + +class TestReducedLatLonGrid: + def setup_method(self, method): + nexus_url = "https://get.ecmwf.int/test-data/polytope/test-data/wave.grib" + download_test_data(nexus_url, "wave.grib") + self.options = { + "values": { + "transformation": { + "mapper": {"type": "reduced_ll", "resolution": 1441, "axes": ["latitude", "longitude"]} + } + }, + "date": {"transformation": {"merge": {"with": "time", "linkers": ["T", "00"]}}}, + "step": {"transformation": {"type_change": "int"}}, + "number": {"transformation": {"type_change": "int"}}, + "longitude": {"transformation": {"cyclic": [0, 360]}}, + } + self.config = {"class": "od", "stream": "wave"} + self.fdbdatacube = FDBDatacube(self.config, axis_options=self.options) + self.slicer = HullSlicer() + self.API = Polytope(datacube=self.fdbdatacube, engine=self.slicer, axis_options=self.options) + + def find_nearest_latlon(self, grib_file, target_lat, target_lon): + messages = grib_file + + # Find the nearest grid points + nearest_points = [] + for message in [messages[0]]: + nearest_index = codes_grib_find_nearest(message, target_lat, target_lon) + nearest_points.append(nearest_index) + + return nearest_points + + @pytest.mark.internet + @pytest.mark.skip(reason="can't install fdb branch on CI") + def test_reduced_ll_grid(self): + shapefile = gpd.read_file("./examples/data/Shipping-Lanes-v1.shp") + geometry_multiline = shapefile.iloc[2] + geometry_object = geometry_multiline["geometry"] + + lines = [] + i = 0 + + for line in geometry_object[:7]: + for point in line.coords: + point_list = list(point) + if list(point)[0] < 0: + point_list[0] = list(point)[0] + 360 + lines.append(point_list) + + # Append for each point a corresponding step + + new_points = [] + for point in lines[:7]: + new_points.append([point[1], point[0], 1]) + + # Pad the shipping route with an initial shape + + padded_point_upper = [0.24, 0.24, 1] + padded_point_lower = [-0.24, -0.24, 1] + initial_shape = Ellipsoid(["latitude", "longitude", "step"], padded_point_lower, padded_point_upper) + + # Then somehow make this list of points into just a sequence of points + + ship_route_polytope = Path(["latitude", "longitude", "step"], initial_shape, *new_points) + + request = Request( + ship_route_polytope, + Select("date", [pd.Timestamp("20231129T000000")]), + Select("domain", ["g"]), + Select("expver", ["0001"]), + Select("param", ["140251"]), + Select("direction", ["1"]), + Select("frequency", ["1"]), + Select("class", ["od"]), + Select("stream", ["wave"]), + Select("levtype", ["sfc"]), + Select("type", ["fc"]), + ) + result = self.API.retrieve(request) + result.pprint() + + lats = [] + lons = [] + eccodes_lats = [] + eccodes_lons = [] + tol = 1e-8 + f = open("./tests/data/wave.grib", "rb") + messages = [] + message = codes_grib_new_from_file(f) + messages.append(message) + + leaves = result.leaves + for i in range(len(leaves)): + cubepath = leaves[i].flatten() + lat = cubepath["latitude"] + lon = cubepath["longitude"] + del cubepath + lats.append(lat) + lons.append(lon) + nearest_points = codes_grib_find_nearest(message, lat, lon)[0] + eccodes_lat = nearest_points.lat + eccodes_lon = nearest_points.lon + eccodes_lats.append(eccodes_lat) + eccodes_lons.append(eccodes_lon) + assert eccodes_lat - tol <= lat + assert lat <= eccodes_lat + tol + assert eccodes_lon - tol <= lon + assert lon <= eccodes_lon + tol + print(i) + f.close() + + worldmap = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + fig, ax = plt.subplots(figsize=(12, 6)) + worldmap.plot(color="darkgrey", ax=ax) + + plt.scatter(lons, lats, s=18, c="red", cmap="YlOrRd") + plt.scatter(eccodes_lons, eccodes_lats, s=6, c="green") + plt.colorbar(label="Temperature") + plt.show() diff --git a/examples/4D_flight_path.py b/examples/4D_flight_path.py index 98906f34..d2d1f1ea 100644 --- a/examples/4D_flight_path.py +++ b/examples/4D_flight_path.py @@ -4,24 +4,24 @@ from earthkit import data from PIL import Image -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request -from polytope.shapes import Box, Path +from polytope.shapes import Box, Path, Select class Test: def setup_method(self): ds = data.from_source("file", "./examples/data/temp_model_levels.grib") array = ds.to_xarray() - array = array.isel(time=0) - options = {"longitude": {"Cyclic": [0, 360.0]}} + array = array.isel(time=0).t + axis_options = {"longitude": {"cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) for dim in array.dims: array = array.sortby(dim) self.array = array self.slicer = HullSlicer() - self.API = Polytope(datacube=array, engine=self.slicer, options=options) + self.API = Polytope(datacube=array, engine=self.slicer, axis_options=axis_options) def test_slice_shipping_route(self): colorscale = [ @@ -84,7 +84,7 @@ def sphere(size, texture): flight_route_polytope = Path(["latitude", "longitude", "step", "hybrid"], initial_shape, *route_point_CDG_LHR) - request = Request(flight_route_polytope) + request = Request(flight_route_polytope, Select("time", ["2022-12-02T12:00:00"])) result = self.API.retrieve(request) lats = [] @@ -99,7 +99,7 @@ def sphere(size, texture): lats.append(lat) longs.append(long) levels.append(level) - t_idx = result.leaves[i].result["t"] + t_idx = result.leaves[i].result[1] parameter_values.append(t_idx) parameter_values = np.array(parameter_values) diff --git a/examples/country_slicing.py b/examples/country_slicing.py index 7673bf47..4a5ed30d 100644 --- a/examples/country_slicing.py +++ b/examples/country_slicing.py @@ -4,21 +4,21 @@ from earthkit import data from shapely.geometry import shape -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request -from polytope.shapes import Polygon, Union +from polytope.shapes import Polygon, Select, Union class Test: def setup_method(self, method): - ds = data.from_source("file", ".examples/data/output8.grib") + ds = data.from_source("file", "./examples/data/output8.grib") array = ds.to_xarray() - array = array.isel(surface=0).isel(step=0).isel(number=0).isel(time=0) - options = {"longitude": {"Cyclic": [0, 360.0]}} + array = array.isel(surface=0).isel(step=0).isel(number=0).isel(time=0).t2m + axis_options = {"longitude": {"cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) self.slicer = HullSlicer() - self.API = Polytope(datacube=array, engine=self.slicer, options=options) + self.API = Polytope(datacube=array, engine=self.slicer, axis_options=axis_options) def test_slice_country(self): # Read a shapefile for a given country and extract the geometry polygons @@ -49,7 +49,14 @@ def test_slice_country(self): request_obj = poly[0] for obj in poly: request_obj = Union(["longitude", "latitude"], request_obj, obj) - request = Request(request_obj) + request = Request( + request_obj, + Select("number", [0]), + Select("time", ["2022-02-06T12:00:00"]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + Select("valid_time", ["2022-02-06T12:00:00"]), + ) # Extract the values of the long and lat from the tree result = self.API.retrieve(request) @@ -64,7 +71,7 @@ def test_slice_country(self): latlong_point = [lat, long] lats.append(lat) longs.append(long) - t_idx = result.leaves[i].result["t2m"] + t_idx = result.leaves[i].result[1] temps.append(t_idx) country_points_plotting.append(latlong_point) temps = np.array(temps) diff --git a/examples/cyclic_route_around_earth.py b/examples/cyclic_route_around_earth.py index dd7d5df4..4e971d67 100644 --- a/examples/cyclic_route_around_earth.py +++ b/examples/cyclic_route_around_earth.py @@ -3,26 +3,33 @@ import numpy as np from earthkit import data -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request -from polytope.shapes import Box, PathSegment +from polytope.shapes import Box, PathSegment, Select class Test: def setup_method(self, method): - ds = data.from_source("file", ".examples/data/output8.grib") + ds = data.from_source("file", "./examples/data/output8.grib") array = ds.to_xarray() - array = array.isel(surface=0).isel(step=0).isel(number=0).isel(time=0) - options = {"longitude": {"Cyclic": [0, 360.0]}} + array = array.isel(surface=0).isel(step=0).isel(number=0).isel(time=0).t2m + axis_options = {"longitude": {"cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) self.slicer = HullSlicer() - self.API = Polytope(datacube=array, engine=self.slicer, options=options) + self.API = Polytope(datacube=array, engine=self.slicer, axis_options=axis_options) def test_slice_country(self): bounding_box = Box(["latitude", "longitude"], [-0.1, -0.1], [0.1, 0.1]) request_obj = PathSegment(["latitude", "longitude"], bounding_box, [-88, -67], [68, 170]) - request = Request(request_obj) + request = Request( + request_obj, + Select("number", [0]), + Select("time", ["2022-02-06T12:00:00"]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + Select("valid_time", ["2022-02-06T12:00:00"]), + ) # Extract the values of the long and lat from the tree result = self.API.retrieve(request) @@ -37,7 +44,7 @@ def test_slice_country(self): latlong_point = [lat, long] lats.append(lat) longs.append(long) - t_idx = result.leaves[i].result["t2m"] + t_idx = result.leaves[i].result[1] temps.append(t_idx) country_points_plotting.append(latlong_point) temps = np.array(temps) diff --git a/examples/healpix_grid_box_example.py b/examples/healpix_grid_box_example.py new file mode 100644 index 00000000..6cfbb5e7 --- /dev/null +++ b/examples/healpix_grid_box_example.py @@ -0,0 +1,83 @@ +import geopandas as gpd +import matplotlib.pyplot as plt +from earthkit import data +from eccodes import codes_grib_find_nearest, codes_grib_new_from_file + +from polytope.datacube.backends.xarray import XArrayDatacube +from polytope.engine.hullslicer import HullSlicer +from polytope.polytope import Polytope, Request +from polytope.shapes import Box, Select + + +class TestOctahedralGrid: + def setup_method(self, method): + ds = data.from_source("file", "./tests/data/healpix.grib") + self.latlon_array = ds.to_xarray().isel(step=0).isel(time=0).isel(isobaricInhPa=0).z + self.xarraydatacube = XArrayDatacube(self.latlon_array) + self.options = {"values": {"mapper": {"type": "healpix", "resolution": 32, "axes": ["latitude", "longitude"]}}} + self.slicer = HullSlicer() + self.API = Polytope(datacube=self.latlon_array, engine=self.slicer, axis_options=self.options) + + def find_nearest_latlon(self, grib_file, target_lat, target_lon): + # Open the GRIB file + f = open(grib_file) + + # Load the GRIB messages from the file + messages = [] + while True: + message = codes_grib_new_from_file(f) + if message is None: + break + messages.append(message) + + # Find the nearest grid points + nearest_points = [] + for message in messages: + nearest_index = codes_grib_find_nearest(message, target_lat, target_lon) + nearest_points.append(nearest_index) + + # Close the GRIB file + f.close() + + return nearest_points + + def test_octahedral_grid(self): + request = Request( + Box(["latitude", "longitude"], [-2, -2], [10, 10]), + Select("time", ["2022-12-14T12:00:00"]), + Select("step", ["01:00:00"]), + Select("isobaricInhPa", [500]), + Select("valid_time", ["2022-12-14T13:00:00"]), + ) + result = self.API.retrieve(request) + assert len(result.leaves) == 35 + + lats = [] + lons = [] + eccodes_lats = [] + eccodes_lons = [] + tol = 1e-8 + for i in range(len(result.leaves)): + cubepath = result.leaves[i].flatten() + lat = cubepath["latitude"] + lon = cubepath["longitude"] + lats.append(lat) + lons.append(lon) + nearest_points = self.find_nearest_latlon("./tests/data/healpix.grib", lat, lon) + eccodes_lat = nearest_points[0][0]["lat"] + eccodes_lon = nearest_points[0][0]["lon"] + eccodes_lats.append(eccodes_lat) + eccodes_lons.append(eccodes_lon) + assert eccodes_lat - tol <= lat + assert lat <= eccodes_lat + tol + assert eccodes_lon - tol <= lon + assert lon <= eccodes_lon + tol + assert len(eccodes_lats) == 35 + worldmap = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + fig, ax = plt.subplots(figsize=(12, 6)) + worldmap.plot(color="darkgrey", ax=ax) + + plt.scatter(eccodes_lons, eccodes_lats, c="blue", marker="s", s=20) + plt.scatter(lons, lats, s=16, c="red", cmap="YlOrRd") + plt.colorbar(label="Temperature") + plt.show() diff --git a/examples/octahedral_grid_box_example.py b/examples/octahedral_grid_box_example.py new file mode 100644 index 00000000..a4cddcac --- /dev/null +++ b/examples/octahedral_grid_box_example.py @@ -0,0 +1,94 @@ +import geopandas as gpd +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from earthkit import data +from eccodes import codes_grib_find_nearest, codes_grib_new_from_file +from matplotlib import markers + +from polytope.datacube.backends.xarray import XArrayDatacube +from polytope.engine.hullslicer import HullSlicer +from polytope.polytope import Polytope, Request +from polytope.shapes import Box, Select + + +def find_nearest_latlon(grib_file, target_lat, target_lon): + # Open the GRIB file + f = open(grib_file) + + # Load the GRIB messages from the file + messages = [] + while True: + message = codes_grib_new_from_file(f) + if message is None: + break + messages.append(message) + + # Find the nearest grid points + nearest_points = [] + for message in messages: + nearest_index = codes_grib_find_nearest(message, target_lat, target_lon) + nearest_points.append(nearest_index) + + # Close the GRIB file + f.close() + + return nearest_points + + +ds = data.from_source("file", "./tests/data/foo.grib") +latlon_array = ds.to_xarray().isel(step=0).isel(number=0).isel(surface=0).isel(time=0) +latlon_array = latlon_array.t2m +nearest_points = find_nearest_latlon("./tests/data/foo.grib", 0, 0) + +latlon_xarray_datacube = XArrayDatacube(latlon_array) + +slicer = HullSlicer() + +grid_options = {"values": {"grid_map": {"type": "octahedral", "resolution": 1280, "axes": ["latitude", "longitude"]}}} + +API = Polytope(datacube=latlon_array, engine=slicer, axis_options=grid_options) + +request = Request( + Box(["latitude", "longitude"], [0, 0], [0.5, 0.5]), + Select("number", [0]), + Select("time", ["2023-06-25T12:00:00"]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + Select("valid_time", ["2023-06-25T12:00:00"]), +) + +result = API.retrieve(request) +result.pprint() + +lats = [] +longs = [] +eccodes_lats = [] +eccodes_longs = [] +parameter_values = [] +for i in range(len(result.leaves)): + cubepath = result.leaves[i].flatten() + lat = cubepath["latitude"] + long = cubepath["longitude"] + lats.append(lat) + longs.append(long) + nearest_points = find_nearest_latlon("./foo.grib", lat, long) + eccodes_lats.append(nearest_points[0][0]["lat"]) + eccodes_longs.append(nearest_points[0][0]["lon"]) + t = result.leaves[i].result[1] + parameter_values.append(t) + + +parameter_values = np.array(parameter_values) +# Plot this last array according to different colors for the result on a world map +worldmap = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) +fig, ax = plt.subplots(figsize=(12, 6)) +worldmap.plot(color="darkgrey", ax=ax) +marker = markers.MarkerStyle(marker="s") +ax.scatter(eccodes_longs, eccodes_lats, s=12, c="red", marker=marker, facecolors="none") +ax.scatter(longs, lats, s=4, c=parameter_values, cmap="viridis") +norm = mpl.colors.Normalize(vmin=min(parameter_values), vmax=max(parameter_values)) +sm = plt.cm.ScalarMappable(cmap="viridis", norm=norm) +sm.set_array([]) +plt.colorbar(sm, label="Wind Speed") +plt.show() diff --git a/examples/octahedral_grid_country_example.py b/examples/octahedral_grid_country_example.py new file mode 100644 index 00000000..280701d2 --- /dev/null +++ b/examples/octahedral_grid_country_example.py @@ -0,0 +1,116 @@ +import geopandas as gpd +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from earthkit import data +from eccodes import codes_grib_find_nearest, codes_grib_new_from_file +from matplotlib import markers +from shapely.geometry import shape + +from polytope.datacube.backends.xarray import XArrayDatacube +from polytope.engine.hullslicer import HullSlicer +from polytope.polytope import Polytope, Request +from polytope.shapes import Polygon, Select, Union + + +def find_nearest_latlon(grib_file, target_lat, target_lon): + # Open the GRIB file + f = open(grib_file) + + # Load the GRIB messages from the file + messages = [] + while True: + message = codes_grib_new_from_file(f) + if message is None: + break + messages.append(message) + + # Find the nearest grid points + nearest_points = [] + for message in messages: + nearest_index = codes_grib_find_nearest(message, target_lat, target_lon) + nearest_points.append(nearest_index) + + # Close the GRIB file + f.close() + + return nearest_points + + +ds = data.from_source("file", "./tests/data/foo.grib") +latlon_array = ds.to_xarray().isel(step=0).isel(number=0).isel(surface=0).isel(time=0) +latlon_array = latlon_array.t2m + +latlon_xarray_datacube = XArrayDatacube(latlon_array) + +slicer = HullSlicer() + +grid_options = {"values": {"grid_map": {"type": "octahedral", "resolution": 1280, "axes": ["latitude", "longitude"]}}} + +API = Polytope(datacube=latlon_array, engine=slicer, axis_options=grid_options) + +shapefile = gpd.read_file("./examples/data/World_Countries__Generalized_.shp") +country = shapefile.iloc[13] +multi_polygon = shape(country["geometry"]) +# If country is just a polygon +polygons = [multi_polygon] +polygons_list = [] + +# Now create a list of x,y points for each polygon + +for polygon in polygons: + xx, yy = polygon.exterior.coords.xy + polygon_points = [list(a) for a in zip(xx, yy)] + polygons_list.append(polygon_points) + +# Then do union of the polygon objects and cut using the slicer +poly = [] +for points in polygons_list: + polygon = Polygon(["longitude", "latitude"], points) + poly.append(polygon) +request_obj = poly[0] +for obj in poly: + request_obj = Union(["longitude", "latitude"], request_obj, obj) +request = Request( + request_obj, + Select("number", [0]), + Select("time", ["2023-06-25T12:00:00"]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + Select("valid_time", ["2023-06-25T12:00:00"]), +) +result = API.retrieve(request) + +lats = [] +longs = [] +eccodes_lats = [] +eccodes_longs = [] +parameter_values = [] +for i in range(len(result.leaves)): + cubepath = result.leaves[i].flatten() + lat = cubepath["latitude"] + long = cubepath["longitude"] + lats.append(lat) + longs.append(long) + nearest_points = find_nearest_latlon("./tests/data/foo.grib", lat, long) + eccodes_lats.append(nearest_points[0][0]["lat"]) + eccodes_longs.append(nearest_points[0][0]["lon"]) + t = result.leaves[i].result[1] + parameter_values.append(t) + +parameter_values = np.array(parameter_values) +# Plot this last array according to different colors for the result on a world map +worldmap = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) +fig, ax = plt.subplots(figsize=(12, 6)) +worldmap.plot(color="darkgrey", ax=ax) + +marker = markers.MarkerStyle(marker="s") +ax.scatter(eccodes_longs, eccodes_lats, s=12, c="red", marker=marker, facecolors="none") +ax.scatter(longs, lats, s=4, c=parameter_values, cmap="viridis") +norm = mpl.colors.Normalize(vmin=min(parameter_values), vmax=max(parameter_values)) + +sm = plt.cm.ScalarMappable(cmap="viridis", norm=norm) +sm.set_array([]) +plt.colorbar(sm, label="Wind Speed") + +plt.show() diff --git a/examples/read_me_example.py b/examples/read_me_example.py index ebca971a..0c13e127 100644 --- a/examples/read_me_example.py +++ b/examples/read_me_example.py @@ -6,16 +6,18 @@ ds = data.from_source("file", "./examples/data/winds.grib") array = ds.to_xarray() -array = array.isel(time=0).isel(surface=0).isel(number=0) +array = array.isel(time=0).isel(surface=0).isel(number=0).u10 -options = {"longitude": {"Cyclic": [0, 360.0]}} +axis_options = {"longitude": {"Cyclic": [0, 360.0]}} -p = Polytope(datacube=array, options=options) +p = Polytope(datacube=array, axis_options=axis_options) box = Box(["latitude", "longitude"], [0, 0], [1, 1]) step_point = Select("step", [np.timedelta64(0, "s")]) -request = Request(box, step_point) +request = Request( + box, step_point, Select("number", [0]), Select("surface", [0]), Select("time", ["2022-09-30T12:00:00"]) +) result = p.retrieve(request) diff --git a/examples/requirements_examples.txt b/examples/requirements_examples.txt index 288d2558..5c46d4d6 100644 --- a/examples/requirements_examples.txt +++ b/examples/requirements_examples.txt @@ -1,12 +1,13 @@ -r ../requirements.txt -r ../tests/requirements_test.txt -matplotlib==3.6.2 -matplotlib-inline==0.1.6 -Pillow==9.3.0 -Shapely==1.8.5.post1 -shp==1.0.2 -Fiona==1.8.22 -geopandas==0.12.2 -plotly==5.11.0 -pyshp==2.3.1 \ No newline at end of file +matplotlib +matplotlib-inline +Pillow +Shapely +shp +Fiona +geopandas +plotly +pyshp +cfgrib \ No newline at end of file diff --git a/examples/slicing_all_ecmwf_countries.py b/examples/slicing_all_ecmwf_countries.py index fc80d212..7178305a 100644 --- a/examples/slicing_all_ecmwf_countries.py +++ b/examples/slicing_all_ecmwf_countries.py @@ -4,21 +4,21 @@ from earthkit import data from shapely.geometry import shape -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request -from polytope.shapes import Polygon, Union +from polytope.shapes import Polygon, Select, Union class Test: def setup_method(self, method): ds = data.from_source("file", "./examples/data/output8.grib") array = ds.to_xarray() - array = array.isel(surface=0).isel(step=0).isel(number=0).isel(time=0) - options = {"longitude": {"Cyclic": [0, 360.0]}} + array = array.isel(surface=0).isel(step=0).isel(number=0).isel(time=0).t2m + axis_options = {"longitude": {"cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) self.slicer = HullSlicer() - self.API = Polytope(datacube=array, engine=self.slicer, options=options) + self.API = Polytope(datacube=array, engine=self.slicer, axis_options=axis_options) def test_slice_country(self): # Read a shapefile for a given country and extract the geometry polygons @@ -68,7 +68,14 @@ def test_slice_country(self): for obj in poly: request_obj = Union(["longitude", "latitude"], request_obj, obj) - request = Request(request_obj) + request = Request( + request_obj, + Select("number", [0]), + Select("time", ["2022-02-06T12:00:00"]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + Select("valid_time", ["2022-02-06T12:00:00"]), + ) # Extract the values of the long and lat from the tree result = self.API.retrieve(request) @@ -114,7 +121,14 @@ def test_slice_country(self): for obj in poly: request_obj = Union(["longitude", "latitude"], request_obj, obj) - request = Request(request_obj) + request = Request( + request_obj, + Select("number", [0]), + Select("time", ["2022-02-06T12:00:00"]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + Select("valid_time", ["2022-02-06T12:00:00"]), + ) # Extract the values of the long and lat from the tree result = self.API.retrieve(request) @@ -127,7 +141,7 @@ def test_slice_country(self): latlong_point = [lat, long] countries_lats.append(lat) countries_longs.append(long) - t_idx = result.leaves[i].result["t2m"] + t_idx = result.leaves[i].result[1] t = t_idx countries_temps.append(t) country_points_plotting.append(latlong_point) diff --git a/examples/timeseries_example.py b/examples/timeseries_example.py index 0df85a33..bd350cc1 100644 --- a/examples/timeseries_example.py +++ b/examples/timeseries_example.py @@ -1,11 +1,9 @@ import geopandas as gpd -import matplotlib.pyplot as plt import numpy as np -import pandas as pd from earthkit import data from shapely.geometry import shape -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request from polytope.shapes import Polygon, Select, Union @@ -15,7 +13,7 @@ class Test: def setup_method(self): ds = data.from_source("file", "./examples/data/timeseries_t2m.grib") array = ds.to_xarray() - array = array.isel(step=0).isel(surface=0).isel(number=0) + array = array.isel(step=0).isel(surface=0).isel(number=0).t2m self.xarraydatacube = XArrayDatacube(array) for dim in array.dims: array = array.sortby(dim) @@ -52,13 +50,22 @@ def test_slice_shipping_route(self): for obj in poly: request_obj = Union(["longitude", "latitude"], request_obj, obj) - request = Request(request_obj, Select("time", [np.datetime64("2022-05-14T12:00:00")])) + request = Request( + request_obj, + Select("time", [np.datetime64("2022-05-14T12:00:00")]), + Select("number", [0]), + Select("step", ["00:00:00"]), + Select("surface", [0]), + ) result = self.API.retrieve(request) + result.pprint() + # For each date/time, we plot an image # Note that only the temperatures should change so we can store them in different arrays + """ country_points_plotting = [] lats1 = [] lats2 = [] @@ -89,7 +96,7 @@ def test_slice_shipping_route(self): lat = cubepath["latitude"] long = cubepath["longitude"] latlong_point = [lat, long] - t_idx = result.leaves[i].result["t2m"] + t_idx = result.leaves[i].result[1] if cubepath["time"] == pd.Timestamp("2022-05-14T12:00:00"): temps1.append(t_idx) lats1.append(lat) @@ -216,3 +223,4 @@ def test_slice_shipping_route(self): ax[3, 1].set_xticks([]) plt.gca().axes.get_yaxis().set_visible(False) plt.show() + """ diff --git a/examples/wind_farms.py b/examples/wind_farms.py index 399102ad..23699eb3 100644 --- a/examples/wind_farms.py +++ b/examples/wind_farms.py @@ -1,5 +1,3 @@ -import math - import geopandas as gpd import matplotlib as mpl import matplotlib.pyplot as plt @@ -8,7 +6,7 @@ from osgeo import gdal from shapely.geometry import shape -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request from polytope.shapes import Polygon, Select, Union @@ -18,12 +16,12 @@ class Test: def setup_method(self): ds = data.from_source("file", "./examples/data/winds.grib") array = ds.to_xarray() - array = array.isel(time=0).isel(surface=0).isel(number=0) + array = array.isel(time=0).isel(surface=0).isel(number=0).u10 self.array = array - options = {"longitude": {"Cyclic": [0, 360.0]}} + axis_options = {"longitude": {"cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) self.slicer = HullSlicer() - self.API = Polytope(datacube=array, engine=self.slicer, options=options) + self.API = Polytope(datacube=array, engine=self.slicer, axis_options=axis_options) def test_slice_wind_farms(self): gdal.SetConfigOption("SHAPE_RESTORE_SHX", "YES") @@ -56,7 +54,13 @@ def test_slice_wind_farms(self): request_obj = poly[0] for obj in poly: request_obj = Union(["longitude", "latitude"], request_obj, obj) - request = Request(request_obj, Select("step", [np.timedelta64(0, "ns")])) + request = Request( + request_obj, + Select("step", [np.timedelta64(0, "ns")]), + Select("number", [0]), + Select("surface", [0]), + Select("time", ["2022-09-30T12:00:00"]), + ) # Extract the values of the long and lat from the tree result = self.API.retrieve(request) @@ -64,20 +68,16 @@ def test_slice_wind_farms(self): longs = [] parameter_values = [] winds_u = [] - winds_v = [] for i in range(len(result.leaves)): cubepath = result.leaves[i].flatten() lat = cubepath["latitude"] long = cubepath["longitude"] lats.append(lat) longs.append(long) - u10_idx = result.leaves[i].result["u10"] + u10_idx = result.leaves[i].result[1] wind_u = u10_idx - v10_idx = result.leaves[i].result["v10"] - wind_v = v10_idx winds_u.append(wind_u) - winds_v.append(wind_v) - parameter_values.append(math.sqrt(wind_u**2 + wind_v**2)) + parameter_values.append(wind_u) parameter_values = np.array(parameter_values) # Plot this last array according to different colors for the result on a world map diff --git a/performance/fdb_performance.py b/performance/fdb_performance.py new file mode 100644 index 00000000..78819d46 --- /dev/null +++ b/performance/fdb_performance.py @@ -0,0 +1,47 @@ +import time + +import pandas as pd + +from polytope.datacube.backends.fdb import FDBDatacube +from polytope.engine.hullslicer import HullSlicer +from polytope.polytope import Polytope, Request +from polytope.shapes import Box, Select + + +class TestSlicingFDBDatacube: + def setup_method(self, method): + # Create a dataarray with 3 labelled axes using different index types + self.options = { + "values": { + "transformation": { + "mapper": {"type": "octahedral", "resolution": 1280, "axes": ["latitude", "longitude"]} + } + }, + "date": {"transformation": {"merge": {"with": "time", "linkers": [" ", "00"]}}}, + "step": {"transformation": {"type_change": "int"}}, + } + self.config = {"class": "od", "expver": "0001", "levtype": "sfc", "step": 0} + self.fdbdatacube = FDBDatacube(self.config, axis_options=self.options) + self.slicer = HullSlicer() + self.API = Polytope(datacube=self.fdbdatacube, engine=self.slicer, axis_options=self.options) + + # Testing different shapes + # @pytest.mark.skip(reason="can't install fdb branch on CI") + def test_fdb_datacube(self): + request = Request( + Select("step", [0]), + Select("levtype", ["sfc"]), + Select("date", [pd.Timestamp("20230625T120000")]), + Select("domain", ["g"]), + Select("expver", ["0001"]), + Select("param", ["167"]), + Select("class", ["od"]), + Select("stream", ["oper"]), + Select("type", ["an"]), + Box(["latitude", "longitude"], [0, 0], [10, 10]), + ) + time1 = time.time() + result = self.API.retrieve(request) + print("ENTIRE TIME") + print(time.time() - time1) + print(len(result.leaves)) diff --git a/performance/fdb_performance_3D.py b/performance/fdb_performance_3D.py new file mode 100644 index 00000000..547d865b --- /dev/null +++ b/performance/fdb_performance_3D.py @@ -0,0 +1,48 @@ +import time + +import pandas as pd + +from polytope.datacube.backends.fdb import FDBDatacube +from polytope.engine.hullslicer import HullSlicer +from polytope.polytope import Polytope, Request +from polytope.shapes import Box, Select, Span + + +class TestSlicingFDBDatacube: + def setup_method(self, method): + # Create a dataarray with 3 labelled axes using different index types + self.options = { + "values": { + "transformation": { + "mapper": {"type": "octahedral", "resolution": 1280, "axes": ["latitude", "longitude"]} + } + }, + "date": {"transformation": {"merge": {"with": "time", "linkers": [" ", "00"]}}}, + "step": {"transformation": {"type_change": "int"}}, + "levelist": {"transformation": {"type_change": "int"}}, + } + self.config = {"class": "od", "expver": "0001", "levtype": "sfc"} + self.fdbdatacube = FDBDatacube(self.config, axis_options=self.options) + self.slicer = HullSlicer() + self.API = Polytope(datacube=self.fdbdatacube, engine=self.slicer, axis_options=self.options) + + # Testing different shapes + # @pytest.mark.skip(reason="can't install fdb branch on CI") + def test_fdb_datacube(self): + request = Request( + Span("step", 1, 15), + Select("levtype", ["sfc"]), + Select("date", [pd.Timestamp("20231102T000000")]), + Select("domain", ["g"]), + Select("expver", ["0001"]), + Select("param", ["167"]), + Select("class", ["od"]), + Select("stream", ["oper"]), + Select("type", ["fc"]), + Box(["latitude", "longitude"], [0, 0], [3, 5]), + ) + time1 = time.time() + result = self.API.retrieve(request) + print("ENTIRE TIME") + print(time.time() - time1) + print(len(result.leaves)) diff --git a/performance/fdb_scalability_plot.py b/performance/fdb_scalability_plot.py new file mode 100644 index 00000000..7230fd47 --- /dev/null +++ b/performance/fdb_scalability_plot.py @@ -0,0 +1,16 @@ +import matplotlib.pyplot as plt + +fdb_time = [ + 7.6377081871032715 - 7.558288812637329, + 73.57192325592041 - 72.99611115455627, + 733.2706120014191 - 727.7059993743896, + 4808.3157522678375 - 4770.814565420151, +] +num_extracted_points = [1986, 19226, 191543, 1267134] + +# for the 1.3M points, we used 100 latitudes too...., maybe that's why it's not as linear... + +plt.plot(num_extracted_points, fdb_time, marker="o") +plt.xlabel("Number of extracted points") +plt.ylabel("Polytope extraction time (in s)") +plt.show() diff --git a/performance/fdb_slice_many_numbers_timeseries.py b/performance/fdb_slice_many_numbers_timeseries.py new file mode 100644 index 00000000..4a497ea2 --- /dev/null +++ b/performance/fdb_slice_many_numbers_timeseries.py @@ -0,0 +1,45 @@ +import time + +import pandas as pd + +from polytope.datacube.backends.fdb import FDBDatacube +from polytope.polytope import Polytope, Request +from polytope.shapes import All, Point, Select + +time1 = time.time() +# Create a dataarray with 3 labelled axes using different index types +options = { + "values": {"mapper": {"type": "octahedral", "resolution": 1280, "axes": ["latitude", "longitude"]}}, + "date": {"merge": {"with": "time", "linkers": ["T", "00"]}}, + "step": {"type_change": "int"}, + "number": {"type_change": "int"}, + "longitude": {"cyclic": [0, 360]}, +} +config = {"class": "od", "expver": "0001", "levtype": "sfc", "type": "pf"} +fdbdatacube = FDBDatacube(config, axis_options=options) +self_API = Polytope(datacube=fdbdatacube, axis_options=options) + +print(time.time() - time1) + +total_polytope_time = 0 +for i in range(10): + time2 = time.time() + +request = Request( + All("step"), + Select("levtype", ["sfc"]), + Select("date", [pd.Timestamp("20231205T000000")]), + Select("domain", ["g"]), + Select("expver", ["0001"]), + Select("param", ["167"]), + Select("class", ["od"]), + Select("stream", ["enfo"]), + Select("type", ["pf"]), + # Select("latitude", [0.035149384216], method="surrounding"), + Point(["latitude", "longitude"], [[0.04, 0]], method="surrounding"), + All("number"), +) +result = self_API.retrieve(request) +print(time.time() - time1) +print(time.time() - time2) +print(len(result.leaves)) diff --git a/performance/scalability_test.py b/performance/scalability_test.py index 186fd561..ab3c5b29 100644 --- a/performance/scalability_test.py +++ b/performance/scalability_test.py @@ -3,7 +3,7 @@ import numpy as np import xarray as xr -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request from polytope.shapes import Box, Disk, Ellipsoid, Select @@ -11,7 +11,7 @@ class Test: def setup_method(self): - array = xr.open_dataset("../examples/data/temp_model_levels.grib", engine="cfgrib") + array = xr.open_dataset("../examples/data/temp_model_levels.grib", engine="cfgrib").t options = {"longitude": {"Cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) for dim in array.dims: diff --git a/performance/scalability_test_2.py b/performance/scalability_test_2.py index de39adc4..b10e86f4 100644 --- a/performance/scalability_test_2.py +++ b/performance/scalability_test_2.py @@ -3,7 +3,7 @@ import numpy as np import xarray as xr -from polytope.datacube.xarray import XArrayDatacube +from polytope.datacube.backends.xarray import XArrayDatacube from polytope.engine.hullslicer import HullSlicer from polytope.polytope import Polytope, Request from polytope.shapes import Box, Select, Union @@ -11,7 +11,7 @@ class Test: def setup_method(self): - array = xr.open_dataset("../examples/data/temp_model_levels.grib", engine="cfgrib") + array = xr.open_dataset("./examples/data/temp_model_levels.grib", engine="cfgrib").t options = {"longitude": {"Cyclic": [0, 360.0]}} self.xarraydatacube = XArrayDatacube(array) for dim in array.dims: @@ -82,23 +82,23 @@ def test_scalability_2D_v3(self): print(len(result.leaves)) print(time.time() - time_start) - def test_scalability_2D_v4(self): - union = Box(["latitude", "longitude"], [0 - 100, 0], [20 - 100, 36]) - for i in range(9): - box = Box(["latitude", "longitude"], [20 * (i + 1) - 100, 0], [20 * (i + 2) - 100, 36]) - union = Union(["latitude", "longitude"], union, box) - for j in range(9): - box = Box(["latitude", "longitude"], [0 - 100, 36 * (j + 1)], [20 - 100, 36 * (j + 2)]) - union = Union(["latitude", "longitude"], union, box) - for i in range(9): - for j in range(9): - box = Box( - ["latitude", "longitude"], [20 * (i + 1) - 100, 36 * (j + 1)], [20 * (i + 2) - 100, 36 * (j + 2)] - ) - union = Union(["latitude", "longitude"], union, box) - time_start = time.time() - print(time_start) - request = Request(union, Select("step", [np.timedelta64(0, "ns")]), Select("hybrid", [1])) - result = self.API.retrieve(request) - print(len(result.leaves)) - print(time.time() - time_start) + # def test_scalability_2D_v4(self): + # union = Box(["latitude", "longitude"], [0 - 100, 0], [20 - 100, 36]) + # for i in range(9): + # box = Box(["latitude", "longitude"], [20 * (i + 1) - 100, 0], [20 * (i + 2) - 100, 36]) + # union = Union(["latitude", "longitude"], union, box) + # for j in range(9): + # box = Box(["latitude", "longitude"], [0 - 100, 36 * (j + 1)], [20 - 100, 36 * (j + 2)]) + # union = Union(["latitude", "longitude"], union, box) + # for i in range(9): + # for j in range(9): + # box = Box( + # ["latitude", "longitude"], [20 * (i + 1) - 100, 36 * (j + 1)], [20 * (i + 2) - 100, 36 * (j + 2)] + # ) + # union = Union(["latitude", "longitude"], union, box) + # time_start = time.time() + # print(time_start) + # request = Request(union, Select("step", [np.timedelta64(0, "ns")]), Select("hybrid", [1])) + # result = self.API.retrieve(request) + # print(len(result.leaves)) + # print(time.time() - time_start) diff --git a/polytope/datacube/__init__.py b/polytope/datacube/__init__.py index d261e197..79871858 100644 --- a/polytope/datacube/__init__.py +++ b/polytope/datacube/__init__.py @@ -1 +1 @@ -from .datacube import * +from .backends.datacube import * diff --git a/polytope/datacube/backends/__init__.py b/polytope/datacube/backends/__init__.py new file mode 100644 index 00000000..63902115 --- /dev/null +++ b/polytope/datacube/backends/__init__.py @@ -0,0 +1 @@ +from ..backends.datacube import * diff --git a/polytope/datacube/backends/datacube.py b/polytope/datacube/backends/datacube.py new file mode 100644 index 00000000..f79be284 --- /dev/null +++ b/polytope/datacube/backends/datacube.py @@ -0,0 +1,159 @@ +import importlib +import logging +import math +from abc import ABC, abstractmethod +from typing import Any + +import xarray as xr + +from ...utility.combinatorics import unique, validate_axes +from ..datacube_axis import DatacubeAxis +from ..index_tree import DatacubePath, IndexTree +from ..transformations.datacube_transformations import ( + DatacubeAxisTransformation, + has_transform, +) + + +class Datacube(ABC): + @abstractmethod + def get(self, requests: IndexTree) -> Any: + """Return data given a set of request trees""" + + @property + def axes(self): + return self._axes + + def validate(self, axes): + """returns true if the input axes can be resolved against the datacube axes""" + return validate_axes(list(self.axes.keys()), axes) + + def _create_axes(self, name, values, transformation_type_key, transformation_options): + # first check what the final axes are for this axis name given transformations + final_axis_names = DatacubeAxisTransformation.get_final_axes( + name, transformation_type_key, transformation_options + ) + transformation = DatacubeAxisTransformation.create_transform( + name, transformation_type_key, transformation_options + ) + for blocked_axis in transformation.blocked_axes(): + self.blocked_axes.append(blocked_axis) + if len(final_axis_names) > 1: + self.coupled_axes.append(final_axis_names) + for axis_name in final_axis_names: + self.fake_axes.append(axis_name) + # if axis does not yet exist, create it + + # first need to change the values so that we have right type + values = transformation.change_val_type(axis_name, values) + if self._axes is None: + DatacubeAxis.create_standard(axis_name, values, self) + elif axis_name not in self._axes.keys(): + DatacubeAxis.create_standard(axis_name, values, self) + # add transformation tag to axis, as well as transformation options for later + setattr(self._axes[axis_name], has_transform[transformation_type_key], True) # where has_transform is a + # factory inside datacube_transformations to set the has_transform, is_cyclic etc axis properties + # add the specific transformation handled here to the relevant axes + # Modify the axis to update with the tag + decorator_module = importlib.import_module("polytope.datacube.datacube_axis") + decorator = getattr(decorator_module, transformation_type_key) + decorator(self._axes[axis_name]) + if transformation not in self._axes[axis_name].transformations: # Avoids duplicates being stored + self._axes[axis_name].transformations.append(transformation) + + def _add_all_transformation_axes(self, options, name, values): + for transformation_type_key in options.keys(): + self._create_axes(name, values, transformation_type_key, options) + + def _check_and_add_axes(self, options, name, values): + if options is not None: + self._add_all_transformation_axes(options, name, values) + else: + if name not in self.blocked_axes: + if self._axes is None: + DatacubeAxis.create_standard(name, values, self) + elif name not in self._axes.keys(): + DatacubeAxis.create_standard(name, values, self) + + def has_index(self, path: DatacubePath, axis, index): + "Given a path to a subset of the datacube, checks if the index exists on that sub-datacube axis" + path = self.fit_path(path) + indexes = axis.find_indexes(path, self) + return index in indexes + + def fit_path(self, path): + for key in path.keys(): + if key not in self.complete_axes and key not in self.fake_axes: + path.pop(key) + return path + + def get_indices(self, path: DatacubePath, axis, lower, upper, method=None): + """ + Given a path to a subset of the datacube, return the discrete indexes which exist between + two non-discrete values (lower, upper) for a particular axis (given by label) + If lower and upper are equal, returns the index which exactly matches that value (if it exists) + e.g. returns integer discrete points between two floats + """ + path = self.fit_path(path) + indexes = axis.find_indexes(path, self) + search_ranges = axis.remap([lower, upper]) + original_search_ranges = axis.to_intervals([lower, upper]) + # Find the offsets for each interval in the requested range, which we will need later + search_ranges_offset = [] + for r in original_search_ranges: + offset = axis.offset(r) + search_ranges_offset.append(offset) + idx_between = self._look_up_datacube(search_ranges, search_ranges_offset, indexes, axis, method) + # Remove duplicates even if difference of the order of the axis tolerance + if offset is not None: + # Note that we can only do unique if not dealing with time values + idx_between = unique(idx_between) + + logging.info(f"For axis {axis.name} between {lower} and {upper}, found indices {idx_between}") + + return idx_between + + def _look_up_datacube(self, search_ranges, search_ranges_offset, indexes, axis, method): + idx_between = [] + for i in range(len(search_ranges)): + r = search_ranges[i] + offset = search_ranges_offset[i] + low = r[0] + up = r[1] + indexes_between = axis.find_indices_between([indexes], low, up, self, method) + # Now the indexes_between are values on the cyclic range so need to remap them to their original + # values before returning them + for j in range(len(indexes_between)): + # if we have a special indexes between range that needs additional offset, treat it here + if len(indexes_between[j]) == 0: + idx_between = idx_between + else: + for k in range(len(indexes_between[j])): + if offset is None: + indexes_between[j][k] = indexes_between[j][k] + else: + indexes_between[j][k] = round(indexes_between[j][k] + offset, int(-math.log10(axis.tol))) + idx_between.append(indexes_between[j][k]) + return idx_between + + def get_mapper(self, axis): + """ + Get the type mapper for a subaxis of the datacube given by label + """ + return self._axes[axis] + + def remap_path(self, path: DatacubePath): + for key in path: + value = path[key] + path[key] = self._axes[key].remap([value, value])[0][0] + return path + + @staticmethod + def create(datacube, axis_options: dict, datacube_options={}): + if isinstance(datacube, (xr.core.dataarray.DataArray, xr.core.dataset.Dataset)): + from .xarray import XArrayDatacube + + xadatacube = XArrayDatacube(datacube, axis_options, datacube_options) + return xadatacube + else: + return datacube diff --git a/polytope/datacube/backends/fdb.py b/polytope/datacube/backends/fdb.py new file mode 100644 index 00000000..2cd6da8d --- /dev/null +++ b/polytope/datacube/backends/fdb.py @@ -0,0 +1,252 @@ +import logging +from copy import deepcopy + +import pygribjump as pygj + +from ...utility.geometry import nearest_pt +from .datacube import Datacube, IndexTree + + +class FDBDatacube(Datacube): + def __init__(self, config=None, axis_options=None, datacube_options=None): + if config is None: + config = {} + if axis_options is None: + axis_options = {} + if datacube_options is None: + datacube_options = {} + + logging.info("Created an FDB datacube with options: " + str(axis_options)) + + self.axis_options = axis_options + self.axis_counter = 0 + self._axes = None + treated_axes = [] + self.complete_axes = [] + self.blocked_axes = [] + self.fake_axes = [] + self.unwanted_path = {} + self.nearest_search = {} + self.coupled_axes = [] + self.axis_with_identical_structure_after = datacube_options.get("identical structure after") + + partial_request = config + # Find values in the level 3 FDB datacube + + self.gj = pygj.GribJump() + self.fdb_coordinates = self.gj.axes(partial_request) + + logging.info("Axes returned from GribJump are: " + str(self.fdb_coordinates)) + + self.fdb_coordinates["values"] = [] + for name, values in self.fdb_coordinates.items(): + values.sort() + options = axis_options.get(name, None) + self._check_and_add_axes(options, name, values) + treated_axes.append(name) + self.complete_axes.append(name) + + # add other options to axis which were just created above like "lat" for the mapper transformations for eg + for name in self._axes: + if name not in treated_axes: + options = axis_options.get(name, None) + val = self._axes[name].type + self._check_and_add_axes(options, name, val) + + logging.info("Polytope created axes for: " + str(self._axes.keys())) + + def get(self, requests: IndexTree): + fdb_requests = [] + fdb_requests_decoding_info = [] + self.get_fdb_requests(requests, fdb_requests, fdb_requests_decoding_info) + output_values = self.gj.extract(fdb_requests) + self.assign_fdb_output_to_nodes(output_values, fdb_requests_decoding_info) + + def get_fdb_requests(self, requests: IndexTree, fdb_requests=[], fdb_requests_decoding_info=[], leaf_path=None): + if leaf_path is None: + leaf_path = {} + + # First when request node is root, go to its children + if requests.axis.name == "root": + logging.info("Looking for data for the tree: " + str([leaf.flatten() for leaf in requests.leaves])) + + for c in requests.children: + self.get_fdb_requests(c, fdb_requests, fdb_requests_decoding_info) + # If request node has no children, we have a leaf so need to assign fdb values to it + else: + key_value_path = {requests.axis.name: requests.value} + ax = requests.axis + (key_value_path, leaf_path, self.unwanted_path) = ax.unmap_path_key( + key_value_path, leaf_path, self.unwanted_path + ) + leaf_path.update(key_value_path) + if len(requests.children[0].children[0].children) == 0: + # find the fdb_requests and associated nodes to which to add results + + (path, range_lengths, current_start_idxs, fdb_node_ranges, lat_length) = self.get_2nd_last_values( + requests, leaf_path + ) + (original_indices, sorted_request_ranges) = self.sort_fdb_request_ranges( + range_lengths, current_start_idxs, lat_length + ) + fdb_requests.append(tuple((path, sorted_request_ranges))) + fdb_requests_decoding_info.append( + tuple((original_indices, fdb_node_ranges, lat_length, range_lengths, current_start_idxs)) + ) + + # Otherwise remap the path for this key and iterate again over children + else: + for c in requests.children: + self.get_fdb_requests(c, fdb_requests, fdb_requests_decoding_info, leaf_path) + + def get_2nd_last_values(self, requests, leaf_path=None): + if leaf_path is None: + leaf_path = {} + # In this function, we recursively loop over the last two layers of the tree and store the indices of the + # request ranges in those layers + + # Find nearest point first before retrieving + if len(self.nearest_search) != 0: + first_ax_name = requests.children[0].axis.name + second_ax_name = requests.children[0].children[0].axis.name + # TODO: throw error if first_ax_name or second_ax_name not in self.nearest_search.keys() + second_ax = requests.children[0].children[0].axis + + # TODO: actually, here we should not remap the nearest_pts, we should instead unmap the + # found_latlon_pts and then remap them later once we have compared found_latlon_pts and nearest_pts + nearest_pts = [ + [lat_val, second_ax._remap_val_to_axis_range(lon_val)] + for (lat_val, lon_val) in zip( + self.nearest_search[first_ax_name][0], self.nearest_search[second_ax_name][0] + ) + ] + + found_latlon_pts = [] + for lat_child in requests.children: + for lon_child in lat_child.children: + found_latlon_pts.append([lat_child.value, lon_child.value]) + + # now find the nearest lat lon to the points requested + nearest_latlons = [] + for pt in nearest_pts: + nearest_latlon = nearest_pt(found_latlon_pts, pt) + nearest_latlons.append(nearest_latlon) + + # need to remove the branches that do not fit + lat_children_values = [child.value for child in requests.children] + for i in range(len(lat_children_values)): + lat_child_val = lat_children_values[i] + lat_child = [child for child in requests.children if child.value == lat_child_val][0] + if lat_child.value not in [latlon[0] for latlon in nearest_latlons]: + lat_child.remove_branch() + else: + possible_lons = [latlon[1] for latlon in nearest_latlons if latlon[0] == lat_child.value] + lon_children_values = [child.value for child in lat_child.children] + for j in range(len(lon_children_values)): + lon_child_val = lon_children_values[j] + lon_child = [child for child in lat_child.children if child.value == lon_child_val][0] + if lon_child.value not in possible_lons: + lon_child.remove_branch() + + lat_length = len(requests.children) + range_lengths = [False] * lat_length + current_start_idxs = [False] * lat_length + fdb_node_ranges = [False] * lat_length + for i in range(len(requests.children)): + lat_child = requests.children[i] + lon_length = len(lat_child.children) + range_lengths[i] = [1] * lon_length + current_start_idxs[i] = [None] * lon_length + fdb_node_ranges[i] = [[IndexTree.root] * lon_length] * lon_length + range_length = deepcopy(range_lengths[i]) + current_start_idx = deepcopy(current_start_idxs[i]) + fdb_range_nodes = deepcopy(fdb_node_ranges[i]) + key_value_path = {lat_child.axis.name: lat_child.value} + ax = lat_child.axis + (key_value_path, leaf_path, self.unwanted_path) = ax.unmap_path_key( + key_value_path, leaf_path, self.unwanted_path + ) + leaf_path.update(key_value_path) + (range_lengths[i], current_start_idxs[i], fdb_node_ranges[i]) = self.get_last_layer_before_leaf( + lat_child, leaf_path, range_length, current_start_idx, fdb_range_nodes + ) + + leaf_path_copy = deepcopy(leaf_path) + leaf_path_copy.pop("values") + return (leaf_path_copy, range_lengths, current_start_idxs, fdb_node_ranges, lat_length) + + def get_last_layer_before_leaf(self, requests, leaf_path, range_l, current_idx, fdb_range_n): + i = 0 + for c in requests.children: + # now c are the leaves of the initial tree + key_value_path = {c.axis.name: c.value} + ax = c.axis + (key_value_path, leaf_path, self.unwanted_path) = ax.unmap_path_key( + key_value_path, leaf_path, self.unwanted_path + ) + leaf_path.update(key_value_path) + last_idx = key_value_path["values"] + if current_idx[i] is None: + current_idx[i] = last_idx + fdb_range_n[i][range_l[i] - 1] = c + else: + if last_idx == current_idx[i] + range_l[i]: + range_l[i] += 1 + fdb_range_n[i][range_l[i] - 1] = c + else: + key_value_path = {c.axis.name: c.value} + ax = c.axis + (key_value_path, leaf_path, self.unwanted_path) = ax.unmap_path_key( + key_value_path, leaf_path, self.unwanted_path + ) + leaf_path.update(key_value_path) + i += 1 + current_start_idx = key_value_path["values"] + current_idx[i] = current_start_idx + return (range_l, current_idx, fdb_range_n) + + def assign_fdb_output_to_nodes(self, output_values, fdb_requests_decoding_info): + for k in range(len(output_values)): + request_output_values = output_values[k] + ( + original_indices, + fdb_node_ranges, + lat_length, + range_lengths, + current_start_idxs, + ) = fdb_requests_decoding_info[k] + new_fdb_range_nodes = [] + new_range_lengths = [] + for j in range(lat_length): + for i in range(len(range_lengths[j])): + if current_start_idxs[j][i] is not None: + new_fdb_range_nodes.append(fdb_node_ranges[j][i]) + new_range_lengths.append(range_lengths[j][i]) + sorted_fdb_range_nodes = [new_fdb_range_nodes[i] for i in original_indices] + sorted_range_lengths = [new_range_lengths[i] for i in original_indices] + for i in range(len(sorted_fdb_range_nodes)): + for j in range(sorted_range_lengths[i]): + n = sorted_fdb_range_nodes[i][j] + n.result = request_output_values[0][i][0][j] + + def sort_fdb_request_ranges(self, range_lengths, current_start_idx, lat_length): + interm_request_ranges = [] + for i in range(lat_length): + for j in range(len(range_lengths[i])): + if current_start_idx[i][j] is not None: + current_request_ranges = (current_start_idx[i][j], current_start_idx[i][j] + range_lengths[i][j]) + interm_request_ranges.append(current_request_ranges) + request_ranges_with_idx = list(enumerate(interm_request_ranges)) + sorted_list = sorted(request_ranges_with_idx, key=lambda x: x[1][0]) + original_indices, sorted_request_ranges = zip(*sorted_list) + return (original_indices, sorted_request_ranges) + + def datacube_natural_indexes(self, axis, subarray): + indexes = subarray[axis.name] + return indexes + + def select(self, path, unmapped_path): + return self.fdb_coordinates + + def ax_vals(self, name): + return self.fdb_coordinates.get(name, None) diff --git a/polytope/datacube/mock.py b/polytope/datacube/backends/mock.py similarity index 79% rename from polytope/datacube/mock.py rename to polytope/datacube/backends/mock.py index b5fc1b1f..6d441fb9 100644 --- a/polytope/datacube/mock.py +++ b/polytope/datacube/backends/mock.py @@ -1,20 +1,20 @@ import math from copy import deepcopy -from ..utility.combinatorics import validate_axes +from ...utility.combinatorics import validate_axes +from ..datacube_axis import IntDatacubeAxis from .datacube import Datacube, DatacubePath, IndexTree -from .datacube_axis import IntAxis class MockDatacube(Datacube): - def __init__(self, dimensions): + def __init__(self, dimensions, datacube_options={}): assert isinstance(dimensions, dict) self.dimensions = dimensions self.mappers = {} for name in self.dimensions: - self.mappers[name] = deepcopy(IntAxis()) + self.mappers[name] = deepcopy(IntDatacubeAxis()) self.mappers[name].name = name self.stride = {} @@ -22,6 +22,8 @@ def __init__(self, dimensions): for k, v in reversed(dimensions.items()): self.stride[k] = stride_cumulative stride_cumulative *= self.dimensions[k] + self.coupled_axes = [] + self.axis_with_identical_structure_after = "" def get(self, requests: IndexTree): # Takes in a datacube and verifies the leaves of the tree are complete @@ -41,7 +43,7 @@ def get(self, requests: IndexTree): def get_mapper(self, axis): return self.mappers[axis] - def get_indices(self, path: DatacubePath, axis, lower, upper): + def get_indices(self, path: DatacubePath, axis, lower, upper, method=None): if lower == upper == math.ceil(lower): if lower >= 0: return [int(lower)] @@ -60,3 +62,9 @@ def axes(self): def validate(self, axes): return validate_axes(self.axes, axes) + + def ax_vals(self, name): + return [] + + def _find_indexes_between(self, axis, indexes, low, up): + pass diff --git a/polytope/datacube/backends/xarray.py b/polytope/datacube/backends/xarray.py new file mode 100644 index 00000000..5412ca4f --- /dev/null +++ b/polytope/datacube/backends/xarray.py @@ -0,0 +1,96 @@ +from copy import deepcopy + +import xarray as xr + +from .datacube import Datacube, IndexTree + + +class XArrayDatacube(Datacube): + """Xarray arrays are labelled, axes can be defined as strings or integers (e.g. "time" or 0).""" + + def __init__(self, dataarray: xr.DataArray, axis_options=None, datacube_options=None): + if axis_options is None: + axis_options = {} + if datacube_options is None: + datacube_options = {} + self.axis_options = axis_options + self.axis_counter = 0 + self._axes = None + self.dataarray = dataarray + treated_axes = [] + self.complete_axes = [] + self.blocked_axes = [] + self.fake_axes = [] + self.nearest_search = None + self.coupled_axes = [] + self.axis_with_identical_structure_after = datacube_options.get("identical structure after") + + for name, values in dataarray.coords.variables.items(): + if name in dataarray.dims: + options = axis_options.get(name, None) + self._check_and_add_axes(options, name, values) + treated_axes.append(name) + self.complete_axes.append(name) + else: + if self.dataarray[name].dims == (): + options = axis_options.get(name, None) + self._check_and_add_axes(options, name, values) + treated_axes.append(name) + for name in dataarray.dims: + if name not in treated_axes: + options = axis_options.get(name, None) + val = dataarray[name].values[0] + self._check_and_add_axes(options, name, val) + treated_axes.append(name) + # add other options to axis which were just created above like "lat" for the mapper transformations for eg + for name in self._axes: + if name not in treated_axes: + options = axis_options.get(name, None) + val = self._axes[name].type + self._check_and_add_axes(options, name, val) + + def get(self, requests: IndexTree): + for r in requests.leaves: + path = r.flatten() + if len(path.items()) == self.axis_counter: + # first, find the grid mapper transform + unmapped_path = {} + path_copy = deepcopy(path) + for key in path_copy: + axis = self._axes[key] + (path, unmapped_path) = axis.unmap_to_datacube(path, unmapped_path) + # TODO: here do nearest point search + path = self.fit_path(path) + subxarray = self.dataarray.sel(path, method="nearest") + subxarray = subxarray.sel(unmapped_path) + value = subxarray.item() + key = subxarray.name + r.result = (key, value) + else: + r.remove_branch() + + def datacube_natural_indexes(self, axis, subarray): + if axis.name in self.complete_axes: + indexes = next(iter(subarray.xindexes.values())).to_pandas_index() + else: + if subarray[axis.name].values.ndim == 0: + indexes = [subarray[axis.name].values] + else: + indexes = subarray[axis.name].values + return indexes + + def select(self, path, unmapped_path): + subarray = self.dataarray.sel(path, method="nearest") + subarray = subarray.sel(unmapped_path) + return subarray + + def ax_vals(self, name): + treated_axes = [] + for _name, values in self.dataarray.coords.variables.items(): + treated_axes.append(_name) + if _name == name: + return values.values + for _name in self.dataarray.dims: + if _name not in treated_axes: + if _name == name: + return self.dataarray[name].values[0] diff --git a/polytope/datacube/datacube.py b/polytope/datacube/datacube.py deleted file mode 100644 index 1fde53de..00000000 --- a/polytope/datacube/datacube.py +++ /dev/null @@ -1,49 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, List - -import xarray as xr - -from .datacube_axis import DatacubeAxis -from .datacube_request_tree import DatacubePath, IndexTree - - -class Datacube(ABC): - @abstractmethod - def get(self, requests: IndexTree) -> Any: - """Return data given a set of request trees""" - - @abstractmethod - def get_mapper(self, axis) -> DatacubeAxis: - """ - Get the type mapper for a subaxis of the datacube given by label - """ - - @abstractmethod - def get_indices(self, path: DatacubePath, axis: str, lower: Any, upper: Any) -> List: - """ - Given a path to a subset of the datacube, return the discrete indexes which exist between - two non-discrete values (lower, upper) for a particular axis (given by label) - If lower and upper are equal, returns the index which exactly matches that value (if it exists) - e.g. returns integer discrete points between two floats - """ - - @abstractmethod - def has_index(self, path: DatacubePath, axis, index) -> bool: - "Given a path to a subset of the datacube, checks if the index exists on that sub-datacube axis" - - @property - @abstractmethod - def axes(self): - pass - - @abstractmethod - def validate(self, axes) -> bool: - """returns true if the input axes can be resolved against the datacube axes""" - - @staticmethod - def create(datacube, options): - if isinstance(datacube, (xr.core.dataarray.DataArray, xr.core.dataset.Dataset)): - from .xarray import XArrayDatacube - - xadatacube = XArrayDatacube(datacube, options=options) - return xadatacube diff --git a/polytope/datacube/datacube_axis.py b/polytope/datacube/datacube_axis.py index 7a5680a0..5bed5dfe 100644 --- a/polytope/datacube/datacube_axis.py +++ b/polytope/datacube/datacube_axis.py @@ -1,23 +1,28 @@ -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod from copy import deepcopy from typing import Any, List import numpy as np import pandas as pd +from .transformations.datacube_cyclic.cyclic_axis_decorator import cyclic +from .transformations.datacube_mappers.mapper_axis_decorator import mapper +from .transformations.datacube_merger.merger_axis_decorator import merge +from .transformations.datacube_reverse.reverse_axis_decorator import reverse +from .transformations.datacube_type_change.type_change_axis_decorator import type_change + class DatacubeAxis(ABC): - @abstractproperty - def name(self): - pass + is_cyclic = False + has_mapper = False + has_merger = False + reorder = False + type_change = False - @abstractproperty - def tol(self): - pass - - @abstractproperty - def range(self): - pass + def update_axis(self): + if self.is_cyclic: + self = cyclic(self) + return self # Convert from user-provided value to CONTINUOUS type (e.g. float, pd.timestamp) @abstractmethod @@ -34,186 +39,98 @@ def to_float(self, value: Any) -> float: def from_float(self, value: float) -> Any: pass - def serialize(self, value) -> Any: - pass - - def remap(self, range: List) -> Any: - pass - - def to_intervals(self, range): - pass - - def remap_val_to_axis_range(self, value): + def serialize(self, value: Any) -> Any: pass - def remap_range_to_axis_range(self, range): - pass - - def to_cyclic_value(self, value): - pass - - def offset(self, value): - pass - - -class IntAxis(DatacubeAxis): - name = None - tol = 1e-12 - range = None - - def parse(self, value: Any) -> Any: - return float(value) - - def to_float(self, value): - return float(value) - - def from_float(self, value): - return float(value) - - def serialize(self, value): - return value - def to_intervals(self, range): return [range] - def remap_val_to_axis_range(self, value): - return value - - def remap_range_to_axis_range(self, range): - return range - def remap(self, range: List) -> Any: return [range] - def to_cyclic_value(self, value): - return value + def unmap_to_datacube(self, path, unmapped_path): + return (path, unmapped_path) + + def find_indexes(self, path, datacube): + unmapped_path = {} + path_copy = deepcopy(path) + for key in path_copy: + axis = datacube._axes[key] + (path, unmapped_path) = axis.unmap_to_datacube(path, unmapped_path) + subarray = datacube.select(path, unmapped_path) + return datacube.datacube_natural_indexes(self, subarray) def offset(self, value): return 0 + def unmap_path_key(self, key_value_path, leaf_path, unwanted_path): + return (key_value_path, leaf_path, unwanted_path) -class IntAxisCyclic(DatacubeAxis): - name = None - tol = 1e-12 - range = None - - def parse(self, value: Any) -> Any: - return float(value) - - def to_float(self, value): - return float(value) - - def from_float(self, value): - return float(value) - - def serialize(self, value): + def _remap_val_to_axis_range(self, value): return value - def to_intervals(self, range): - axis_lower = self.range[0] - axis_upper = self.range[1] - axis_range = axis_upper - axis_lower - lower = range[0] - upper = range[1] - intervals = [] - if lower < axis_upper: - # In this case, we want to go from lower to the first remapped cyclic axis upper - # or the asked upper range value. - # For example, if we have cyclic range [0,360] and we want to break [-270,180] into intervals, - # we first want to obtain [-270, 0] as the first range, where 0 is the remapped cyclic axis upper - # but if we wanted to break [-270, -180] into intervals, we would want to get [-270,-180], - # where -180 is the asked upper range value. - loops = int((axis_upper - lower) / axis_range) - remapped_up = axis_upper - (loops) * axis_range - new_upper = min(upper, remapped_up) - else: - # In this case, since lower >= axis_upper, we need to either go to the asked upper range - # or we need to go to the first remapped cyclic axis upper which is higher than lower - new_upper = min(axis_upper + axis_range, upper) - while new_upper < lower: - new_upper = min(new_upper + axis_range, upper) - intervals.append([lower, new_upper]) - # Now that we have established what the first interval should be, we should just jump from cyclic range - # to cyclic range until we hit the asked upper range value. - new_up = deepcopy(new_upper) - while new_up < upper: - new_upper = new_up - new_up = min(upper, new_upper + axis_range) - intervals.append([new_upper, new_up]) - # Once we have added all the in-between ranges, we need to add the last interval - intervals.append([new_up, upper]) - return intervals - - def remap_range_to_axis_range(self, range): - axis_lower = self.range[0] - axis_upper = self.range[1] - axis_range = axis_upper - axis_lower - lower = range[0] - upper = range[1] - if lower < axis_lower: - # In this case we need to calculate the number of loops between the axis lower - # and the lower to recenter the lower - loops = int((axis_lower - lower - self.tol) / axis_range) - return_lower = lower + (loops + 1) * axis_range - return_upper = upper + (loops + 1) * axis_range - elif lower >= axis_upper: - # In this case we need to calculate the number of loops between the axis upper - # and the lower to recenter the lower - loops = int((lower - axis_upper) / axis_range) - return_lower = lower - (loops + 1) * axis_range - return_upper = upper - (loops + 1) * axis_range - else: - # In this case, the lower value is already in the right range - return_lower = lower - return_upper = upper - return [return_lower, return_upper] - - def remap_val_to_axis_range(self, value): - return_range = self.remap_range_to_axis_range([value, value]) - return return_range[0] - - def remap(self, range: List): - if abs(range[0] - range[1]) <= 2 * self.tol: - # If we have a range that is just one point, then it should still be counted - # and so we should take a small interval around it to find values inbetween - range = [ - self.remap_val_to_axis_range(range[0]) - self.tol, - self.remap_val_to_axis_range(range[0]) + self.tol, - ] - return [range] - if self.range[0] - self.tol <= range[0] <= self.range[1] + self.tol: - if self.range[0] - self.tol <= range[1] <= self.range[1] + self.tol: - # If we are in cyclic range, return it - return [range] - range_intervals = self.to_intervals(range) - ranges = [] - for interval in range_intervals: - if abs(interval[0] - interval[1]) > 0: - # If the interval is not just a single point, we remap it to the axis range - range = self.remap_range_to_axis_range([interval[0], interval[1]]) - up = range[1] - low = range[0] - if up < low: - # Make sure we remap in the right order - ranges.append([up - self.tol, low + self.tol]) + def find_indices_between(self, index_ranges, low, up, datacube, method=None): + # TODO: add method for snappping + indexes_between_ranges = [] + for indexes in index_ranges: + if self.name in datacube.complete_axes: + # Find the range of indexes between lower and upper + # https://pandas.pydata.org/docs/reference/api/pandas.Index.searchsorted.html + # Assumes the indexes are already sorted (could sort to be sure) and monotonically increasing + if method == "surrounding" or method == "nearest": + start = indexes.searchsorted(low, "left") + end = indexes.searchsorted(up, "right") + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + indexes_between = indexes[start:end].to_list() + indexes_between_ranges.append(indexes_between) else: - ranges.append([low - self.tol, up + self.tol]) - return ranges - - def offset(self, range): - # We first unpad the range by the axis tolerance to make sure that - # we find the wanted range of the cyclic axis since we padded by the axis tolerance before. - # Also, it's safer that we find the offset of a value inside the range instead of on the border. - unpadded_range = [range[0] + 1.5 * self.tol, range[1] - 1.5 * self.tol] - cyclic_range = self.remap_range_to_axis_range(unpadded_range) - offset = unpadded_range[0] - cyclic_range[0] - return offset - - -class FloatAxis(DatacubeAxis): - name = None - tol = 1e-12 - range = None + start = indexes.searchsorted(low, "left") + end = indexes.searchsorted(up, "right") + indexes_between = indexes[start:end].to_list() + indexes_between_ranges.append(indexes_between) + else: + if method == "surrounding" or method == "nearest": + start = indexes.index(low) + end = indexes.index(up) + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + indexes_between = indexes[start:end] + indexes_between_ranges.append(indexes_between) + else: + indexes_between = [i for i in indexes if low <= i <= up] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + @staticmethod + def create_standard(name, values, datacube): + values = np.array(values) + DatacubeAxis.check_axis_type(name, values) + if datacube._axes is None: + datacube._axes = {name: deepcopy(_type_to_axis_lookup[values.dtype.type])} + else: + datacube._axes[name] = deepcopy(_type_to_axis_lookup[values.dtype.type]) + datacube._axes[name].name = name + datacube.axis_counter += 1 + + @staticmethod + def check_axis_type(name, values): + # NOTE: The values here need to be a numpy array which has a dtype attribute + if values.dtype.type not in _type_to_axis_lookup: + raise ValueError(f"Could not create a mapper for index type {values.dtype.type} for axis {name}") + + +@reverse +@cyclic +@mapper +@type_change +class IntDatacubeAxis(DatacubeAxis): + def __init__(self): + self.name = None + self.tol = 1e-12 + self.range = None + self.transformations = [] + self.type = 0 def parse(self, value: Any) -> Any: return float(value) @@ -227,32 +144,18 @@ def from_float(self, value): def serialize(self, value): return value - def to_intervals(self, range): - return [range] - - def remap_val_to_axis_range(self, value): - return value - - def remap_range_to_axis_range(self, range): - return range - - def remap(self, range: List) -> Any: - return [range] - - def to_cyclic_value(self, value): - return value - - def offset(self, value): - return 0 - -class FloatAxisCyclic(DatacubeAxis): - # Note that in the cyclic axis here, we only retain the lower part when we remap - # so for eg if the datacube has cyclic axis on [0,360] - # then if we want 360, we will in reality get back 0 (which is the same) - name = None - tol = 1e-12 - range = None +@reverse +@cyclic +@mapper +@type_change +class FloatDatacubeAxis(DatacubeAxis): + def __init__(self): + self.name = None + self.tol = 1e-12 + self.range = None + self.transformations = [] + self.type = 0.0 def parse(self, value: Any) -> Any: return float(value) @@ -266,111 +169,15 @@ def from_float(self, value): def serialize(self, value): return value - def to_intervals(self, range): - axis_lower = self.range[0] - axis_upper = self.range[1] - axis_range = axis_upper - axis_lower - lower = range[0] - upper = range[1] - intervals = [] - if lower < axis_upper: - # In this case, we want to go from lower to the first remapped cyclic axis upper - # or the asked upper range value. - # For example, if we have cyclic range [0,360] and we want to break [-270,180] into intervals, - # we first want to obtain [-270, 0] as the first range, where 0 is the remapped cyclic axis upper - # but if we wanted to break [-270, -180] into intervals, we would want to get [-270,-180], - # where -180 is the asked upper range value. - loops = int((axis_upper - lower) / axis_range) - remapped_up = axis_upper - (loops) * axis_range - new_upper = min(upper, remapped_up) - else: - # In this case, since lower >= axis_upper, we need to either go to the asked upper range - # or we need to go to the first remapped cyclic axis upper which is higher than lower - new_upper = min(axis_upper + axis_range, upper) - while new_upper < lower: - new_upper = min(new_upper + axis_range, upper) - intervals.append([lower, new_upper]) - # Now that we have established what the first interval should be, we should just jump from cyclic range - # to cyclic range until we hit the asked upper range value. - new_up = deepcopy(new_upper) - while new_up < upper: - new_upper = new_up - new_up = min(upper, new_upper + axis_range) - intervals.append([new_upper, new_up]) - # Once we have added all the in-between ranges, we need to add the last interval - intervals.append([new_up, upper]) - return intervals - - def remap_range_to_axis_range(self, range): - axis_lower = self.range[0] - axis_upper = self.range[1] - axis_range = axis_upper - axis_lower - lower = range[0] - upper = range[1] - if lower < axis_lower: - # In this case we need to calculate the number of loops between the axis lower - # and the lower to recenter the lower - loops = int((axis_lower - lower - self.tol) / axis_range) - return_lower = lower + (loops + 1) * axis_range - return_upper = upper + (loops + 1) * axis_range - elif lower >= axis_upper: - # In this case we need to calculate the number of loops between the axis upper - # and the lower to recenter the lower - loops = int((lower - axis_upper) / axis_range) - return_lower = lower - (loops + 1) * axis_range - return_upper = upper - (loops + 1) * axis_range - else: - # In this case, the lower value is already in the right range - return_lower = lower - return_upper = upper - return [return_lower, return_upper] - - def remap_val_to_axis_range(self, value): - return_range = self.remap_range_to_axis_range([value, value]) - return return_range[0] - - def remap(self, range: List): - if self.range[0] - self.tol <= range[0] <= self.range[1] + self.tol: - if self.range[0] - self.tol <= range[1] <= self.range[1] + self.tol: - # If we are already in the cyclic range, return it - return [range] - elif abs(range[0] - range[1]) <= 2 * self.tol: - # If we have a range that is just one point, then it should still be counted - # and so we should take a small interval around it to find values inbetween - range = [ - self.remap_val_to_axis_range(range[0]) - self.tol, - self.remap_val_to_axis_range(range[0]) + self.tol, - ] - return [range] - range_intervals = self.to_intervals(range) - ranges = [] - for interval in range_intervals: - if abs(interval[0] - interval[1]) > 0: - # If the interval is not just a single point, we remap it to the axis range - range = self.remap_range_to_axis_range([interval[0], interval[1]]) - up = range[1] - low = range[0] - if up < low: - # Make sure we remap in the right order - ranges.append([up - self.tol, low + self.tol]) - else: - ranges.append([low - self.tol, up + self.tol]) - return ranges - - def offset(self, range): - # We first unpad the range by the axis tolerance to make sure that - # we find the wanted range of the cyclic axis since we padded by the axis tolerance before. - # Also, it's safer that we find the offset of a value inside the range instead of on the border - unpadded_range = [range[0] + 1.5 * self.tol, range[1] - 1.5 * self.tol] - cyclic_range = self.remap_range_to_axis_range(unpadded_range) - offset = unpadded_range[0] - cyclic_range[0] - return offset - -class PandasTimestampAxis(DatacubeAxis): - name = None - tol = 1e-12 - range = None +@merge +class PandasTimestampDatacubeAxis(DatacubeAxis): + def __init__(self): + self.name = None + self.tol = 1e-12 + self.range = None + self.transformations = [] + self.type = pd.Timestamp("2000-01-01T00:00:00") def parse(self, value: Any) -> Any: if isinstance(value, np.str_): @@ -378,7 +185,10 @@ def parse(self, value: Any) -> Any: return pd.Timestamp(value) def to_float(self, value: pd.Timestamp): - return float(value.value / 10**9) + if isinstance(value, np.datetime64): + return float((value - np.datetime64("1970-01-01T00:00:00")).astype("int")) + else: + return float(value.value / 10**9) def from_float(self, value): return pd.Timestamp(int(value), unit="s") @@ -386,29 +196,18 @@ def from_float(self, value): def serialize(self, value): return str(value) - def to_intervals(self, range): - return [range] - - def remap_val_to_axis_range(self, value): - return value - - def remap_range_to_axis_range(self, range): - return range - - def remap(self, range: List) -> Any: - return [range] - - def to_cyclic_value(self, value): - return value - def offset(self, value): return None -class PandasTimedeltaAxis(DatacubeAxis): - name = None - tol = 1e-12 - range = None +@merge +class PandasTimedeltaDatacubeAxis(DatacubeAxis): + def __init__(self): + self.name = None + self.tol = 1e-12 + self.range = None + self.transformations = [] + self.type = np.timedelta64(0, "s") def parse(self, value: Any) -> Any: if isinstance(value, np.str_): @@ -416,7 +215,10 @@ def parse(self, value: Any) -> Any: return pd.Timedelta(value) def to_float(self, value: pd.Timedelta): - return float(value.value / 10**9) + if isinstance(value, np.timedelta64): + return value.astype("timedelta64[s]").astype(int) + else: + return float(value.value / 10**9) def from_float(self, value): return pd.Timedelta(int(value), unit="s") @@ -424,29 +226,17 @@ def from_float(self, value): def serialize(self, value): return str(value) - def to_intervals(self, range): - return [range] - - def remap_val_to_axis_range(self, value): - return value - - def remap_range_to_axis_range(self, range): - return range - - def remap(self, range: List) -> Any: - return [range] - - def to_cyclic_value(self, value): - return value - def offset(self, value): return None -class UnsliceableaAxis(DatacubeAxis): - name = None - tol = float("NaN") - range = None +@type_change +class UnsliceableDatacubeAxis(DatacubeAxis): + def __init__(self): + self.name = None + self.tol = float("NaN") + self.range = None + self.transformations = [] def parse(self, value: Any) -> Any: return value @@ -460,5 +250,15 @@ def from_float(self, value): def serialize(self, value): raise TypeError("Tried to slice unsliceable axis") - def remap_val_to_axis_range(self, value): - return value + +_type_to_axis_lookup = { + pd.Int64Dtype: IntDatacubeAxis(), + pd.Timestamp: PandasTimestampDatacubeAxis(), + np.int64: IntDatacubeAxis(), + np.datetime64: PandasTimestampDatacubeAxis(), + np.timedelta64: PandasTimedeltaDatacubeAxis(), + np.float64: FloatDatacubeAxis(), + np.str_: UnsliceableDatacubeAxis(), + str: UnsliceableDatacubeAxis(), + np.object_: UnsliceableDatacubeAxis(), +} diff --git a/polytope/datacube/datacube_request_tree.py b/polytope/datacube/index_tree.py similarity index 67% rename from polytope/datacube/datacube_request_tree.py rename to polytope/datacube/index_tree.py index 3f659388..2afb8416 100644 --- a/polytope/datacube/datacube_request_tree.py +++ b/polytope/datacube/index_tree.py @@ -1,9 +1,11 @@ +import copy import json +import logging from typing import OrderedDict from sortedcontainers import SortedList -from .datacube_axis import IntAxis +from .datacube_axis import IntDatacubeAxis, UnsliceableDatacubeAxis class DatacubePath(OrderedDict): @@ -21,7 +23,7 @@ def pprint(self): class IndexTree(object): - root = IntAxis() + root = IntDatacubeAxis() root.name = "root" def __init__(self, axis=root, value=None): @@ -30,6 +32,7 @@ def __init__(self, axis=root, value=None): self._parent = None self.result = None self.axis = axis + self.ancestors = [] @property def leaves(self): @@ -37,10 +40,44 @@ def leaves(self): self._collect_leaf_nodes(leaves) return leaves + @property + def leaves_with_ancestors(self): + # TODO: could store ancestors directly in leaves? Change here + leaves = [] + self._collect_leaf_nodes(leaves) + return leaves + + def copy_children_from_other(self, other): + for o in other.children: + c = IndexTree(o.axis, copy.copy(o.value)) + self.add_child(c) + c.copy_children_from_other(o) + return + + def pprint_2(self, level=0): + if self.axis.name == "root": + print("\n") + print("\t" * level + "\u21b3" + str(self)) + for child in self.children: + child.pprint_2(level + 1) + + def _collect_leaf_nodes_old(self, leaves): + if len(self.children) == 0: + leaves.append(self) + for n in self.children: + n._collect_leaf_nodes(leaves) + def _collect_leaf_nodes(self, leaves): + # NOTE: leaves_and_ancestors is going to be a list of tuples, where first entry is leaf and second entry is a + # list of its ancestors if len(self.children) == 0: leaves.append(self) + self.ancestors.append(self) for n in self.children: + for ancestor in self.ancestors: + n.ancestors.append(ancestor) + if self.axis != IndexTree.root: + n.ancestors.append(self) n._collect_leaf_nodes(leaves) def __setitem__(self, key, value): @@ -58,7 +95,21 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, IndexTree): return False - return (self.axis.name, self.value) == (other.axis.name, other.value) + if self.axis.name != other.axis.name: + return False + else: + if other.value == self.value: + return True + else: + if isinstance(self.axis, UnsliceableDatacubeAxis): + return False + else: + if other.value - 2 * other.axis.tol <= self.value <= other.value + 2 * other.axis.tol: + return True + elif self.value - 2 * self.axis.tol <= other.value <= self.value + 2 * self.axis.tol: + return True + else: + return False def __lt__(self, other): return (self.axis.name, self.value) < (other.axis.name, other.value) @@ -73,12 +124,12 @@ def add_child(self, node): self.children.add(node) node._parent = self - def create_child(self, axis, value): + def create_child_not_safe(self, axis, value): node = IndexTree(axis, value) self.add_child(node) return node - def create_child_safe(self, axis, value): + def create_child(self, axis, value): node = IndexTree(axis, value) existing = self.find_child(node) if not existing: @@ -133,10 +184,12 @@ def intersect(self, other): def pprint(self, level=0): if self.axis.name == "root": - print("\n") - print("\t" * level + "\u21b3" + str(self)) + logging.debug("\n") + logging.debug("\t" * level + "\u21b3" + str(self)) for child in self.children: child.pprint(level + 1) + if len(self.children) == 0: + logging.debug("\t" * (level + 1) + "\u21b3" + str(self.result)) def remove_branch(self): if not self.is_root(): @@ -153,6 +206,13 @@ def flatten(self): path[ancestor.axis.name] = ancestor.value return path + def flatten_with_ancestors(self): + path = DatacubePath() + ancestors = self.ancestors + for ancestor in ancestors: + path[ancestor.axis.name] = ancestor.value + return path + def get_ancestors(self): ancestors = [] current_node = self diff --git a/polytope/datacube/transformations/__init__.py b/polytope/datacube/transformations/__init__.py new file mode 100644 index 00000000..cf6989be --- /dev/null +++ b/polytope/datacube/transformations/__init__.py @@ -0,0 +1 @@ +from ..transformations.datacube_transformations import * diff --git a/polytope/datacube/transformations/datacube_cyclic/__init__.py b/polytope/datacube/transformations/datacube_cyclic/__init__.py new file mode 100644 index 00000000..adfaf9d8 --- /dev/null +++ b/polytope/datacube/transformations/datacube_cyclic/__init__.py @@ -0,0 +1,2 @@ +from .cyclic_axis_decorator import * +from .datacube_cyclic import * diff --git a/polytope/datacube/transformations/datacube_cyclic/cyclic_axis_decorator.py b/polytope/datacube/transformations/datacube_cyclic/cyclic_axis_decorator.py new file mode 100644 index 00000000..972d6d1a --- /dev/null +++ b/polytope/datacube/transformations/datacube_cyclic/cyclic_axis_decorator.py @@ -0,0 +1,189 @@ +import bisect +import math +from copy import deepcopy +from typing import List + +from .datacube_cyclic import DatacubeAxisCyclic + + +def cyclic(cls): + if cls.is_cyclic: + + def update_range(): + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisCyclic): + transformation = transform + cls.range = transformation.range + + def to_intervals(range): + update_range() + if range[0] == -math.inf: + range[0] = cls.range[0] + if range[1] == math.inf: + range[1] = cls.range[1] + axis_lower = cls.range[0] + axis_upper = cls.range[1] + axis_range = axis_upper - axis_lower + lower = range[0] + upper = range[1] + intervals = [] + if lower < axis_upper: + # In this case, we want to go from lower to the first remapped cyclic axis upper + # or the asked upper range value. + # For example, if we have cyclic range [0,360] and we want to break [-270,180] into intervals, + # we first want to obtain [-270, 0] as the first range, where 0 is the remapped cyclic axis upper + # but if we wanted to break [-270, -180] into intervals, we would want to get [-270,-180], + # where -180 is the asked upper range value. + loops = int((axis_upper - lower) / axis_range) + remapped_up = axis_upper - (loops) * axis_range + new_upper = min(upper, remapped_up) + else: + # In this case, since lower >= axis_upper, we need to either go to the asked upper range + # or we need to go to the first remapped cyclic axis upper which is higher than lower + new_upper = min(axis_upper + axis_range, upper) + while new_upper < lower: + new_upper = min(new_upper + axis_range, upper) + intervals.append([lower, new_upper]) + # Now that we have established what the first interval should be, we should just jump from cyclic range + # to cyclic range until we hit the asked upper range value. + new_up = deepcopy(new_upper) + while new_up < upper: + new_upper = new_up + new_up = min(upper, new_upper + axis_range) + intervals.append([new_upper, new_up]) + # Once we have added all the in-between ranges, we need to add the last interval + intervals.append([new_up, upper]) + return intervals + + def _remap_range_to_axis_range(range): + update_range() + axis_lower = cls.range[0] + axis_upper = cls.range[1] + axis_range = axis_upper - axis_lower + lower = range[0] + upper = range[1] + if lower < axis_lower: + # In this case we need to calculate the number of loops between the axis lower + # and the lower to recenter the lower + loops = int((axis_lower - lower - cls.tol) / axis_range) + return_lower = lower + (loops + 1) * axis_range + return_upper = upper + (loops + 1) * axis_range + elif lower >= axis_upper: + # In this case we need to calculate the number of loops between the axis upper + # and the lower to recenter the lower + loops = int((lower - axis_upper) / axis_range) + return_lower = lower - (loops + 1) * axis_range + return_upper = upper - (loops + 1) * axis_range + else: + # In this case, the lower value is already in the right range + return_lower = lower + return_upper = upper + return [return_lower, return_upper] + + def _remap_val_to_axis_range(value): + return_range = _remap_range_to_axis_range([value, value]) + return return_range[0] + + def remap(range: List): + update_range() + if cls.range[0] - cls.tol <= range[0] <= cls.range[1] + cls.tol: + if cls.range[0] - cls.tol <= range[1] <= cls.range[1] + cls.tol: + # If we are already in the cyclic range, return it + return [range] + elif abs(range[0] - range[1]) <= 2 * cls.tol: + # If we have a range that is just one point, then it should still be counted + # and so we should take a small interval around it to find values inbetween + range = [ + _remap_val_to_axis_range(range[0]) - cls.tol, + _remap_val_to_axis_range(range[0]) + cls.tol, + ] + return [range] + range_intervals = cls.to_intervals(range) + ranges = [] + for interval in range_intervals: + if abs(interval[0] - interval[1]) > 0: + # If the interval is not just a single point, we remap it to the axis range + range = _remap_range_to_axis_range([interval[0], interval[1]]) + up = range[1] + low = range[0] + if up < low: + # Make sure we remap in the right order + ranges.append([up - cls.tol, low + cls.tol]) + else: + ranges.append([low - cls.tol, up + cls.tol]) + return ranges + + old_find_indexes = cls.find_indexes + + def find_indexes(path, datacube): + return old_find_indexes(path, datacube) + + old_unmap_path_key = cls.unmap_path_key + + def unmap_path_key(key_value_path, leaf_path, unwanted_path): + value = key_value_path[cls.name] + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisCyclic): + if cls.name == transform.name: + new_val = _remap_val_to_axis_range(value) + key_value_path[cls.name] = new_val + key_value_path, leaf_path, unwanted_path = old_unmap_path_key(key_value_path, leaf_path, unwanted_path) + return (key_value_path, leaf_path, unwanted_path) + + old_unmap_to_datacube = cls.unmap_to_datacube + + def unmap_to_datacube(path, unmapped_path): + (path, unmapped_path) = old_unmap_to_datacube(path, unmapped_path) + return (path, unmapped_path) + + old_find_indices_between = cls.find_indices_between + + def find_indices_between(index_ranges, low, up, datacube, method=None): + update_range() + indexes_between_ranges = [] + + if method != "surrounding" or method != "nearest": + return old_find_indices_between(index_ranges, low, up, datacube, method) + else: + for indexes in index_ranges: + if cls.name in datacube.complete_axes: + start = indexes.searchsorted(low, "left") + end = indexes.searchsorted(up, "right") + else: + start = bisect.bisect_left(indexes, low) + end = bisect.bisect_right(indexes, up) + + if start - 1 < 0: + index_val_found = indexes[-1:][0] + indexes_between_ranges.append([index_val_found]) + if end + 1 > len(indexes): + index_val_found = indexes[:2][0] + indexes_between_ranges.append([index_val_found]) + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + if cls.name in datacube.complete_axes: + indexes_between = indexes[start:end].to_list() + else: + indexes_between = indexes[start:end] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + def offset(range): + # We first unpad the range by the axis tolerance to make sure that + # we find the wanted range of the cyclic axis since we padded by the axis tolerance before. + # Also, it's safer that we find the offset of a value inside the range instead of on the border + unpadded_range = [range[0] + 1.5 * cls.tol, range[1] - 1.5 * cls.tol] + cyclic_range = _remap_range_to_axis_range(unpadded_range) + offset = unpadded_range[0] - cyclic_range[0] + return offset + + cls.to_intervals = to_intervals + cls.remap = remap + cls.offset = offset + cls.find_indexes = find_indexes + cls.unmap_to_datacube = unmap_to_datacube + cls.find_indices_between = find_indices_between + cls.unmap_path_key = unmap_path_key + cls._remap_val_to_axis_range = _remap_val_to_axis_range + + return cls diff --git a/polytope/datacube/transformations/datacube_cyclic/datacube_cyclic.py b/polytope/datacube/transformations/datacube_cyclic/datacube_cyclic.py new file mode 100644 index 00000000..86113aa2 --- /dev/null +++ b/polytope/datacube/transformations/datacube_cyclic/datacube_cyclic.py @@ -0,0 +1,25 @@ +from ..datacube_transformations import DatacubeAxisTransformation + + +class DatacubeAxisCyclic(DatacubeAxisTransformation): + # The transformation here will be to point the old axes to the new cyclic axes + + def __init__(self, name, cyclic_options): + self.name = name + self.transformation_options = cyclic_options + self.range = cyclic_options + + def generate_final_transformation(self): + return self + + def transformation_axes_final(self): + return [self.name] + + def change_val_type(self, axis_name, values): + return values + + def blocked_axes(self): + return [] + + def unwanted_axes(self): + return [] diff --git a/polytope/datacube/transformations/datacube_mappers/__init__.py b/polytope/datacube/transformations/datacube_mappers/__init__.py new file mode 100644 index 00000000..ad69c9c5 --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/__init__.py @@ -0,0 +1,2 @@ +from .datacube_mappers import * +from .mapper_axis_decorator import * diff --git a/polytope/datacube/transformations/datacube_mappers/datacube_mappers.py b/polytope/datacube/transformations/datacube_mappers/datacube_mappers.py new file mode 100644 index 00000000..a2034bfb --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/datacube_mappers.py @@ -0,0 +1,85 @@ +from copy import deepcopy +from importlib import import_module + +from ..datacube_transformations import DatacubeAxisTransformation + + +class DatacubeMapper(DatacubeAxisTransformation): + # Needs to implements DatacubeAxisTransformation methods + + def __init__(self, name, mapper_options): + self.transformation_options = mapper_options + self.grid_type = mapper_options["type"] + self.grid_resolution = mapper_options["resolution"] + self.grid_axes = mapper_options["axes"] + self.local_area = [] + if "local" in mapper_options.keys(): + self.local_area = mapper_options["local"] + self.old_axis = name + self._final_transformation = self.generate_final_transformation() + self._final_mapped_axes = self._final_transformation._mapped_axes + self._axis_reversed = self._final_transformation._axis_reversed + + def generate_final_transformation(self): + map_type = _type_to_datacube_mapper_lookup[self.grid_type] + module = import_module("polytope.datacube.transformations.datacube_mappers.mapper_types." + self.grid_type) + constructor = getattr(module, map_type) + transformation = deepcopy(constructor(self.old_axis, self.grid_axes, self.grid_resolution, self.local_area)) + return transformation + + def blocked_axes(self): + return [] + + def unwanted_axes(self): + return [self._final_mapped_axes[0]] + + def transformation_axes_final(self): + final_axes = self._final_mapped_axes + return final_axes + + # Needs to also implement its own methods + + def change_val_type(self, axis_name, values): + # the new axis_vals created will be floats + return [0.0] + + def _mapped_axes(self): + # NOTE: Each of the mapper method needs to call it's sub mapper method + final_axes = self._final_mapped_axes + return final_axes + + def _base_axis(self): + pass + + def _resolution(self): + pass + + def first_axis_vals(self): + return self._final_transformation.first_axis_vals() + + def second_axis_vals(self, first_val): + return self._final_transformation.second_axis_vals(first_val) + + def map_first_axis(self, lower, upper): + return self._final_transformation.map_first_axis(lower, upper) + + def map_second_axis(self, first_val, lower, upper): + return self._final_transformation.map_second_axis(first_val, lower, upper) + + def find_second_idx(self, first_val, second_val): + return self._final_transformation.find_second_idx(first_val, second_val) + + def unmap_first_val_to_start_line_idx(self, first_val): + return self._final_transformation.unmap_first_val_to_start_line_idx(first_val) + + def unmap(self, first_val, second_val): + return self._final_transformation.unmap(first_val, second_val) + + +_type_to_datacube_mapper_lookup = { + "octahedral": "OctahedralGridMapper", + "healpix": "HealpixGridMapper", + "regular": "RegularGridMapper", + "reduced_ll": "ReducedLatLonMapper", + "local_regular": "LocalRegularGridMapper", +} diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_axis_decorator.py b/polytope/datacube/transformations/datacube_mappers/mapper_axis_decorator.py new file mode 100644 index 00000000..99432186 --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_axis_decorator.py @@ -0,0 +1,108 @@ +import bisect + +from ....utility.list_tools import bisect_left_cmp, bisect_right_cmp +from .datacube_mappers import DatacubeMapper + + +def mapper(cls): + if cls.has_mapper: + + def find_indexes(path, datacube): + # first, find the relevant transformation object that is a mapping in the cls.transformation dico + for transform in cls.transformations: + if isinstance(transform, DatacubeMapper): + transformation = transform + if cls.name == transformation._mapped_axes()[0]: + return transformation.first_axis_vals() + if cls.name == transformation._mapped_axes()[1]: + first_val = path[transformation._mapped_axes()[0]] + return transformation.second_axis_vals(first_val) + + old_unmap_to_datacube = cls.unmap_to_datacube + + def unmap_to_datacube(path, unmapped_path): + (path, unmapped_path) = old_unmap_to_datacube(path, unmapped_path) + for transform in cls.transformations: + if isinstance(transform, DatacubeMapper): + if cls.name == transform._mapped_axes()[0]: + # if we are on the first axis, then need to add the first val to unmapped_path + first_val = path.get(cls.name, None) + path.pop(cls.name, None) + if cls.name not in unmapped_path: + # if for some reason, the unmapped_path already has the first axis val, then don't update + unmapped_path[cls.name] = first_val + if cls.name == transform._mapped_axes()[1]: + # if we are on the second axis, then the val of the first axis is stored + # inside unmapped_path so can get it from there + second_val = path.get(cls.name, None) + path.pop(cls.name, None) + first_val = unmapped_path.get(transform._mapped_axes()[0], None) + unmapped_path.pop(transform._mapped_axes()[0], None) + # if the first_val was not in the unmapped_path, then it's still in path + if first_val is None: + first_val = path.get(transform._mapped_axes()[0], None) + path.pop(transform._mapped_axes()[0], None) + if first_val is not None and second_val is not None: + unmapped_idx = transform.unmap(first_val, second_val) + unmapped_path[transform.old_axis] = unmapped_idx + return (path, unmapped_path) + + old_unmap_path_key = cls.unmap_path_key + + def unmap_path_key(key_value_path, leaf_path, unwanted_path): + key_value_path, leaf_path, unwanted_path = old_unmap_path_key(key_value_path, leaf_path, unwanted_path) + value = key_value_path[cls.name] + for transform in cls.transformations: + if isinstance(transform, DatacubeMapper): + if cls.name == transform._mapped_axes()[0]: + unwanted_val = key_value_path[transform._mapped_axes()[0]] + unwanted_path[cls.name] = unwanted_val + if cls.name == transform._mapped_axes()[1]: + first_val = unwanted_path[transform._mapped_axes()[0]] + unmapped_idx = transform.unmap(first_val, value) + leaf_path.pop(transform._mapped_axes()[0], None) + key_value_path.pop(cls.name) + key_value_path[transform.old_axis] = unmapped_idx + return (key_value_path, leaf_path, unwanted_path) + + def find_indices_between(index_ranges, low, up, datacube, method=None): + # TODO: add method for snappping + indexes_between_ranges = [] + for transform in cls.transformations: + if isinstance(transform, DatacubeMapper): + transformation = transform + if cls.name in transformation._mapped_axes(): + for idxs in index_ranges: + if method == "surrounding" or method == "nearest": + axis_reversed = transform._axis_reversed[cls.name] + if not axis_reversed: + start = bisect.bisect_left(idxs, low) + end = bisect.bisect_right(idxs, up) + else: + # TODO: do the custom bisect + end = bisect_left_cmp(idxs, low, cmp=lambda x, y: x > y) + 1 + start = bisect_right_cmp(idxs, up, cmp=lambda x, y: x > y) + start = max(start - 1, 0) + end = min(end + 1, len(idxs)) + indexes_between = idxs[start:end] + indexes_between_ranges.append(indexes_between) + else: + axis_reversed = transform._axis_reversed[cls.name] + if not axis_reversed: + lower_idx = bisect.bisect_left(idxs, low) + upper_idx = bisect.bisect_right(idxs, up) + indexes_between = idxs[lower_idx:upper_idx] + else: + # TODO: do the custom bisect + end_idx = bisect_left_cmp(idxs, low, cmp=lambda x, y: x > y) + 1 + start_idx = bisect_right_cmp(idxs, up, cmp=lambda x, y: x > y) + indexes_between = idxs[start_idx:end_idx] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + cls.find_indexes = find_indexes + cls.unmap_to_datacube = unmap_to_datacube + cls.find_indices_between = find_indices_between + cls.unmap_path_key = unmap_path_key + + return cls diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_types/__init__.py b/polytope/datacube/transformations/datacube_mappers/mapper_types/__init__.py new file mode 100644 index 00000000..ba9a7b33 --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_types/__init__.py @@ -0,0 +1,5 @@ +from .healpix import * +from .local_regular import * +from .octahedral import * +from .reduced_ll import * +from .regular import * diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_types/healpix.py b/polytope/datacube/transformations/datacube_mappers/mapper_types/healpix.py new file mode 100644 index 00000000..8589ec71 --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_types/healpix.py @@ -0,0 +1,124 @@ +import bisect +import math + +from ..datacube_mappers import DatacubeMapper + + +class HealpixGridMapper(DatacubeMapper): + def __init__(self, base_axis, mapped_axes, resolution, local_area=[]): + # TODO: if local area is not empty list, raise NotImplemented + self._mapped_axes = mapped_axes + self._base_axis = base_axis + self._resolution = resolution + self._axis_reversed = {mapped_axes[0]: True, mapped_axes[1]: False} + self._first_axis_vals = self.first_axis_vals() + + def first_axis_vals(self): + rad2deg = 180 / math.pi + vals = [0] * (4 * self._resolution - 1) + + # Polar caps + for i in range(1, self._resolution): + val = 90 - (rad2deg * math.acos(1 - (i * i / (3 * self._resolution * self._resolution)))) + vals[i - 1] = val + vals[4 * self._resolution - 1 - i] = -val + # Equatorial belts + for i in range(self._resolution, 2 * self._resolution): + val = 90 - (rad2deg * math.acos((4 * self._resolution - 2 * i) / (3 * self._resolution))) + vals[i - 1] = val + vals[4 * self._resolution - 1 - i] = -val + # Equator + vals[2 * self._resolution - 1] = 0 + return vals + + def map_first_axis(self, lower, upper): + axis_lines = self._first_axis_vals + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def second_axis_vals(self, first_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + idx = self._first_axis_vals.index(first_val) + + # Polar caps + if idx < self._resolution - 1 or 3 * self._resolution - 1 < idx <= 4 * self._resolution - 2: + start = 45 / (idx + 1) + vals = [start + i * (360 / (4 * (idx + 1))) for i in range(4 * (idx + 1))] + return vals + # Equatorial belts + start = 45 / self._resolution + if self._resolution - 1 <= idx < 2 * self._resolution - 1 or 2 * self._resolution <= idx < 3 * self._resolution: + r_start = start * (2 - (((idx + 1) - self._resolution + 1) % 2)) + vals = [r_start + i * (360 / (4 * self._resolution)) for i in range(4 * self._resolution)] + if vals[-1] == 360: + vals[-1] = 0 + return vals + # Equator + temp_val = 1 if self._resolution % 2 else 0 + r_start = start * (1 - temp_val) + if idx == 2 * self._resolution - 1: + vals = [r_start + i * (360 / (4 * self._resolution)) for i in range(4 * self._resolution)] + return vals + + def map_second_axis(self, first_val, lower, upper): + axis_lines = self.second_axis_vals(first_val) + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def axes_idx_to_healpix_idx(self, first_idx, second_idx): + idx = 0 + for i in range(self._resolution - 1): + if i != first_idx: + idx += 4 * (i + 1) + else: + idx += second_idx + return idx + for i in range(self._resolution - 1, 3 * self._resolution): + if i != first_idx: + idx += 4 * self._resolution + else: + idx += second_idx + return idx + for i in range(3 * self._resolution, 4 * self._resolution - 1): + if i != first_idx: + idx += 4 * (4 * self._resolution - 1 - i + 1) + else: + idx += second_idx + return idx + + def find_second_idx(self, first_val, second_val): + tol = 1e-10 + second_axis_vals = self.second_axis_vals(first_val) + second_idx = bisect.bisect_left(second_axis_vals, second_val - tol) + return second_idx + + def unmap_first_val_to_start_line_idx(self, first_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + idx = 0 + for i in range(self._resolution - 1): + if i != first_idx: + idx += 4 * (i + 1) + else: + return idx + for i in range(self._resolution - 1, 3 * self._resolution): + if i != first_idx: + idx += 4 * self._resolution + else: + return idx + for i in range(3 * self._resolution, 4 * self._resolution - 1): + if i != first_idx: + idx += 4 * (4 * self._resolution - 1 - i + 1) + else: + return idx + + def unmap(self, first_val, second_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + second_val = [i for i in self.second_axis_vals(first_val) if second_val - tol <= i <= second_val + tol][0] + second_idx = self.second_axis_vals(first_val).index(second_val) + healpix_index = self.axes_idx_to_healpix_idx(first_idx, second_idx) + return healpix_index diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_types/local_regular.py b/polytope/datacube/transformations/datacube_mappers/mapper_types/local_regular.py new file mode 100644 index 00000000..a1514778 --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_types/local_regular.py @@ -0,0 +1,69 @@ +import bisect + +from ..datacube_mappers import DatacubeMapper + + +class LocalRegularGridMapper(DatacubeMapper): + def __init__(self, base_axis, mapped_axes, resolution, local_area=[]): + # TODO: if local area is not empty list, raise NotImplemented + self._mapped_axes = mapped_axes + self._base_axis = base_axis + self._first_axis_min = local_area[0] + self._first_axis_max = local_area[1] + self._second_axis_min = local_area[2] + self._second_axis_max = local_area[3] + if not isinstance(resolution, list): + self.first_resolution = resolution + self.second_resolution = resolution + else: + self.first_resolution = resolution[0] + self.second_resolution = resolution[1] + self._first_deg_increment = (local_area[1] - local_area[0]) / self.first_resolution + self._second_deg_increment = (local_area[3] - local_area[2]) / self.second_resolution + self._axis_reversed = {mapped_axes[0]: True, mapped_axes[1]: False} + self._first_axis_vals = self.first_axis_vals() + + def first_axis_vals(self): + first_ax_vals = [self._first_axis_max - i * self._first_deg_increment for i in range(self.first_resolution + 1)] + return first_ax_vals + + def map_first_axis(self, lower, upper): + axis_lines = self._first_axis_vals + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def second_axis_vals(self, first_val): + second_ax_vals = [ + self._second_axis_min + i * self._second_deg_increment for i in range(self.second_resolution + 1) + ] + return second_ax_vals + + def map_second_axis(self, first_val, lower, upper): + axis_lines = self.second_axis_vals(first_val) + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def axes_idx_to_regular_idx(self, first_idx, second_idx): + final_idx = first_idx * (self.second_resolution + 1) + second_idx + return final_idx + + def find_second_idx(self, first_val, second_val): + tol = 1e-10 + second_axis_vals = self.second_axis_vals(first_val) + second_idx = bisect.bisect_left(second_axis_vals, second_val - tol) + return second_idx + + def unmap_first_val_to_start_line_idx(self, first_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + return first_idx * self.second_resolution + + def unmap(self, first_val, second_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + second_val = [i for i in self.second_axis_vals(first_val) if second_val - tol <= i <= second_val + tol][0] + second_idx = self.second_axis_vals(first_val).index(second_val) + final_index = self.axes_idx_to_regular_idx(first_idx, second_idx) + return final_index diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_types/octahedral.py b/polytope/datacube/transformations/datacube_mappers/mapper_types/octahedral.py new file mode 100644 index 00000000..730ac959 --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_types/octahedral.py @@ -0,0 +1,2753 @@ +import math + +from .....utility.list_tools import bisect_left_cmp, bisect_right_cmp +from ..datacube_mappers import DatacubeMapper + + +class OctahedralGridMapper(DatacubeMapper): + def __init__(self, base_axis, mapped_axes, resolution, local_area=[]): + # TODO: if local area is not empty list, raise NotImplemented + self._mapped_axes = mapped_axes + self._base_axis = base_axis + self._resolution = resolution + self._first_axis_vals = self.first_axis_vals() + self._first_idx_map = self.create_first_idx_map() + self._second_axis_spacing = {} + self._axis_reversed = {mapped_axes[0]: True, mapped_axes[1]: False} + + def gauss_first_guess(self): + i = 0 + gvals = [ + 2.4048255577e0, + 5.5200781103e0, + 8.6537279129e0, + 11.7915344391e0, + 14.9309177086e0, + 18.0710639679e0, + 21.2116366299e0, + 24.3524715308e0, + 27.4934791320e0, + 30.6346064684e0, + 33.7758202136e0, + 36.9170983537e0, + 40.0584257646e0, + 43.1997917132e0, + 46.3411883717e0, + 49.4826098974e0, + 52.6240518411e0, + 55.7655107550e0, + 58.9069839261e0, + 62.0484691902e0, + 65.1899648002e0, + 68.3314693299e0, + 71.4729816036e0, + 74.6145006437e0, + 77.7560256304e0, + 80.8975558711e0, + 84.0390907769e0, + 87.1806298436e0, + 90.3221726372e0, + 93.4637187819e0, + 96.6052679510e0, + 99.7468198587e0, + 102.8883742542e0, + 106.0299309165e0, + 109.1714896498e0, + 112.3130502805e0, + 115.4546126537e0, + 118.5961766309e0, + 121.7377420880e0, + 124.8793089132e0, + 128.0208770059e0, + 131.1624462752e0, + 134.3040166383e0, + 137.4455880203e0, + 140.5871603528e0, + 143.7287335737e0, + 146.8703076258e0, + 150.0118824570e0, + 153.1534580192e0, + 156.2950342685e0, + ] + + numVals = len(gvals) + vals = [] + for i in range(self._resolution): + if i < numVals: + vals.append(gvals[i]) + else: + vals.append(vals[i - 1] + math.pi) + return vals + + def get_precomputed_values_N1280(self): + lats = [0] * 2560 + # lats = SortedList() + # lats = {} + lats[0] = 89.946187715665616 + lats[1] = 89.876478353332288 + lats[2] = 89.806357319542244 + lats[3] = 89.736143271609578 + lats[4] = 89.6658939412157 + lats[5] = 89.595627537554492 + lats[6] = 89.525351592371393 + lats[7] = 89.45506977912261 + lats[8] = 89.3847841013921 + lats[9] = 89.314495744374256 + lats[10] = 89.24420545380525 + lats[11] = 89.173913722284126 + lats[12] = 89.103620888238879 + lats[13] = 89.033327191845927 + lats[14] = 88.96303280826325 + lats[15] = 88.892737868230952 + lats[16] = 88.822442471310097 + lats[17] = 88.752146694650691 + lats[18] = 88.681850598961759 + lats[19] = 88.611554232668382 + lats[20] = 88.541257634868515 + lats[21] = 88.470960837474877 + lats[22] = 88.40066386679355 + lats[23] = 88.330366744702559 + lats[24] = 88.26006948954614 + lats[25] = 88.189772116820762 + lats[26] = 88.119474639706425 + lats[27] = 88.049177069484486 + lats[28] = 87.978879415867283 + lats[29] = 87.908581687261687 + lats[30] = 87.838283890981543 + lats[31] = 87.767986033419561 + lats[32] = 87.697688120188062 + lats[33] = 87.627390156234085 + lats[34] = 87.557092145935584 + lats[35] = 87.486794093180748 + lats[36] = 87.416496001434894 + lats[37] = 87.346197873795816 + lats[38] = 87.275899713041966 + lats[39] = 87.205601521672108 + lats[40] = 87.135303301939786 + lats[41] = 87.065005055882821 + lats[42] = 86.994706785348129 + lats[43] = 86.924408492014166 + lats[44] = 86.854110177408927 + lats[45] = 86.783811842927179 + lats[46] = 86.713513489844246 + lats[47] = 86.643215119328573 + lats[48] = 86.572916732453024 + lats[49] = 86.502618330203831 + lats[50] = 86.432319913489792 + lats[51] = 86.362021483149363 + lats[52] = 86.291723039957418 + lats[53] = 86.221424584631109 + lats[54] = 86.151126117835304 + lats[55] = 86.080827640187209 + lats[56] = 86.010529152260403 + lats[57] = 85.940230654588888 + lats[58] = 85.869932147670127 + lats[59] = 85.799633631968391 + lats[60] = 85.729335107917464 + lats[61] = 85.659036575922883 + lats[62] = 85.588738036364362 + lats[63] = 85.518439489597966 + lats[64] = 85.448140935957483 + lats[65] = 85.377842375756586 + lats[66] = 85.307543809290152 + lats[67] = 85.237245236835548 + lats[68] = 85.16694665865414 + lats[69] = 85.09664807499216 + lats[70] = 85.026349486081983 + lats[71] = 84.95605089214304 + lats[72] = 84.885752293382765 + lats[73] = 84.81545368999717 + lats[74] = 84.745155082171991 + lats[75] = 84.674856470082915 + lats[76] = 84.604557853896708 + lats[77] = 84.534259233771479 + lats[78] = 84.463960609857125 + lats[79] = 84.393661982296322 + lats[80] = 84.323363351224444 + lats[81] = 84.253064716770425 + lats[82] = 84.18276607905679 + lats[83] = 84.112467438200326 + lats[84] = 84.042168794312317 + lats[85] = 83.971870147498763 + lats[86] = 83.901571497860914 + lats[87] = 83.831272845495249 + lats[88] = 83.760974190494011 + lats[89] = 83.690675532945292 + lats[90] = 83.620376872933264 + lats[91] = 83.550078210538487 + lats[92] = 83.479779545838113 + lats[93] = 83.409480878905782 + lats[94] = 83.339182209812321 + lats[95] = 83.268883538625232 + lats[96] = 83.198584865409657 + lats[97] = 83.128286190227698 + lats[98] = 83.057987513139125 + lats[99] = 82.987688834201322 + lats[100] = 82.917390153469313 + lats[101] = 82.84709147099602 + lats[102] = 82.77679278683226 + lats[103] = 82.706494101026948 + lats[104] = 82.63619541362705 + lats[105] = 82.56589672467787 + lats[106] = 82.495598034222837 + lats[107] = 82.425299342304029 + lats[108] = 82.355000648961692 + lats[109] = 82.284701954234833 + lats[110] = 82.214403258160871 + lats[111] = 82.144104560776 + lats[112] = 82.073805862115165 + lats[113] = 82.003507162211946 + lats[114] = 81.933208461098829 + lats[115] = 81.862909758807191 + lats[116] = 81.792611055367345 + lats[117] = 81.722312350808508 + lats[118] = 81.652013645158945 + lats[119] = 81.581714938445955 + lats[120] = 81.511416230696042 + lats[121] = 81.441117521934686 + lats[122] = 81.370818812186627 + lats[123] = 81.300520101475826 + lats[124] = 81.230221389825374 + lats[125] = 81.159922677257711 + lats[126] = 81.089623963794551 + lats[127] = 81.019325249456955 + lats[128] = 80.949026534265244 + lats[129] = 80.878727818239184 + lats[130] = 80.808429101397948 + lats[131] = 80.73813038376008 + lats[132] = 80.667831665343556 + lats[133] = 80.59753294616587 + lats[134] = 80.527234226243991 + lats[135] = 80.456935505594302 + lats[136] = 80.386636784232863 + lats[137] = 80.316338062175078 + lats[138] = 80.246039339436052 + lats[139] = 80.175740616030438 + lats[140] = 80.105441891972376 + lats[141] = 80.035143167275749 + lats[142] = 79.9648444419539 + lats[143] = 79.894545716019948 + lats[144] = 79.824246989486554 + lats[145] = 79.753948262366038 + lats[146] = 79.683649534670437 + lats[147] = 79.61335080641139 + lats[148] = 79.543052077600308 + lats[149] = 79.472753348248219 + lats[150] = 79.402454618365894 + lats[151] = 79.332155887963822 + lats[152] = 79.261857157052191 + lats[153] = 79.191558425640977 + lats[154] = 79.121259693739859 + lats[155] = 79.050960961358285 + lats[156] = 78.980662228505423 + lats[157] = 78.910363495190211 + lats[158] = 78.840064761421445 + lats[159] = 78.769766027207638 + lats[160] = 78.699467292557102 + lats[161] = 78.629168557477882 + lats[162] = 78.558869821977908 + lats[163] = 78.488571086064923 + lats[164] = 78.418272349746417 + lats[165] = 78.347973613029708 + lats[166] = 78.277674875922045 + lats[167] = 78.207376138430348 + lats[168] = 78.137077400561424 + lats[169] = 78.066778662322022 + lats[170] = 77.996479923718596 + lats[171] = 77.926181184757539 + lats[172] = 77.855882445445019 + lats[173] = 77.785583705787161 + lats[174] = 77.71528496578982 + lats[175] = 77.644986225458879 + lats[176] = 77.574687484799924 + lats[177] = 77.504388743818524 + lats[178] = 77.434090002520122 + lats[179] = 77.363791260909963 + lats[180] = 77.293492518993247 + lats[181] = 77.22319377677502 + lats[182] = 77.15289503426024 + lats[183] = 77.082596291453768 + lats[184] = 77.012297548360323 + lats[185] = 76.941998804984564 + lats[186] = 76.871700061330955 + lats[187] = 76.801401317404 + lats[188] = 76.731102573208048 + lats[189] = 76.660803828747362 + lats[190] = 76.59050508402602 + lats[191] = 76.520206339048215 + lats[192] = 76.449907593817869 + lats[193] = 76.379608848338933 + lats[194] = 76.3093101026152 + lats[195] = 76.239011356650423 + lats[196] = 76.16871261044831 + lats[197] = 76.098413864012443 + lats[198] = 76.028115117346374 + lats[199] = 75.957816370453543 + lats[200] = 75.887517623337317 + lats[201] = 75.81721887600105 + lats[202] = 75.746920128447996 + lats[203] = 75.67662138068134 + lats[204] = 75.60632263270422 + lats[205] = 75.536023884519707 + lats[206] = 75.465725136130786 + lats[207] = 75.395426387540439 + lats[208] = 75.325127638751567 + lats[209] = 75.254828889766983 + lats[210] = 75.184530140589501 + lats[211] = 75.114231391221821 + lats[212] = 75.043932641666672 + lats[213] = 74.973633891926625 + lats[214] = 74.903335142004323 + lats[215] = 74.833036391902269 + lats[216] = 74.762737641622991 + lats[217] = 74.692438891168877 + lats[218] = 74.622140140542356 + lats[219] = 74.551841389745761 + lats[220] = 74.481542638781434 + lats[221] = 74.411243887651622 + lats[222] = 74.340945136358584 + lats[223] = 74.270646384904481 + lats[224] = 74.200347633291472 + lats[225] = 74.13004888152166 + lats[226] = 74.059750129597163 + lats[227] = 73.98945137751997 + lats[228] = 73.919152625292114 + lats[229] = 73.848853872915541 + lats[230] = 73.778555120392184 + lats[231] = 73.70825636772399 + lats[232] = 73.637957614912779 + lats[233] = 73.567658861960396 + lats[234] = 73.497360108868662 + lats[235] = 73.427061355639339 + lats[236] = 73.356762602274188 + lats[237] = 73.2864638487749 + lats[238] = 73.216165095143182 + lats[239] = 73.145866341380668 + lats[240] = 73.075567587489019 + lats[241] = 73.005268833469799 + lats[242] = 72.934970079324657 + lats[243] = 72.864671325055056 + lats[244] = 72.794372570662574 + lats[245] = 72.724073816148703 + lats[246] = 72.653775061514935 + lats[247] = 72.583476306762691 + lats[248] = 72.513177551893421 + lats[249] = 72.442878796908545 + lats[250] = 72.3725800418094 + lats[251] = 72.302281286597392 + lats[252] = 72.231982531273843 + lats[253] = 72.161683775840089 + lats[254] = 72.091385020297409 + lats[255] = 72.02108626464711 + lats[256] = 71.950787508890414 + lats[257] = 71.880488753028587 + lats[258] = 71.810189997062835 + lats[259] = 71.739891240994368 + lats[260] = 71.669592484824364 + lats[261] = 71.599293728553988 + lats[262] = 71.528994972184378 + lats[263] = 71.458696215716685 + lats[264] = 71.388397459152031 + lats[265] = 71.318098702491469 + lats[266] = 71.247799945736105 + lats[267] = 71.177501188887007 + lats[268] = 71.107202431945211 + lats[269] = 71.036903674911756 + lats[270] = 70.966604917787635 + lats[271] = 70.896306160573886 + lats[272] = 70.826007403271475 + lats[273] = 70.755708645881384 + lats[274] = 70.685409888404578 + lats[275] = 70.615111130841967 + lats[276] = 70.544812373194532 + lats[277] = 70.474513615463138 + lats[278] = 70.404214857648739 + lats[279] = 70.333916099752187 + lats[280] = 70.263617341774406 + lats[281] = 70.193318583716191 + lats[282] = 70.123019825578467 + lats[283] = 70.052721067362043 + lats[284] = 69.982422309067744 + lats[285] = 69.912123550696421 + lats[286] = 69.841824792248843 + lats[287] = 69.771526033725834 + lats[288] = 69.701227275128161 + lats[289] = 69.630928516456592 + lats[290] = 69.560629757711908 + lats[291] = 69.490330998894862 + lats[292] = 69.420032240006194 + lats[293] = 69.349733481046613 + lats[294] = 69.279434722016902 + lats[295] = 69.209135962917699 + lats[296] = 69.138837203749759 + lats[297] = 69.068538444513763 + lats[298] = 68.998239685210365 + lats[299] = 68.927940925840304 + lats[300] = 68.85764216640419 + lats[301] = 68.787343406902693 + lats[302] = 68.717044647336493 + lats[303] = 68.646745887706189 + lats[304] = 68.576447128012447 + lats[305] = 68.506148368255865 + lats[306] = 68.435849608437067 + lats[307] = 68.365550848556666 + lats[308] = 68.295252088615257 + lats[309] = 68.224953328613438 + lats[310] = 68.154654568551791 + lats[311] = 68.084355808430871 + lats[312] = 68.014057048251274 + lats[313] = 67.943758288013555 + lats[314] = 67.873459527718282 + lats[315] = 67.803160767365966 + lats[316] = 67.732862006957205 + lats[317] = 67.662563246492482 + lats[318] = 67.592264485972336 + lats[319] = 67.521965725397308 + lats[320] = 67.451666964767895 + lats[321] = 67.381368204084609 + lats[322] = 67.311069443347961 + lats[323] = 67.240770682558434 + lats[324] = 67.170471921716526 + lats[325] = 67.100173160822706 + lats[326] = 67.029874399877471 + lats[327] = 66.95957563888129 + lats[328] = 66.889276877834618 + lats[329] = 66.818978116737924 + lats[330] = 66.748679355591662 + lats[331] = 66.678380594396273 + lats[332] = 66.608081833152212 + lats[333] = 66.537783071859891 + lats[334] = 66.467484310519808 + lats[335] = 66.397185549132331 + lats[336] = 66.326886787697887 + lats[337] = 66.256588026216932 + lats[338] = 66.186289264689833 + lats[339] = 66.115990503117033 + lats[340] = 66.045691741498899 + lats[341] = 65.975392979835888 + lats[342] = 65.905094218128355 + lats[343] = 65.834795456376696 + lats[344] = 65.764496694581283 + lats[345] = 65.694197932742526 + lats[346] = 65.623899170860767 + lats[347] = 65.553600408936404 + lats[348] = 65.483301646969792 + lats[349] = 65.413002884961315 + lats[350] = 65.342704122911286 + lats[351] = 65.272405360820116 + lats[352] = 65.202106598688133 + lats[353] = 65.131807836515677 + lats[354] = 65.061509074303089 + lats[355] = 64.991210312050711 + lats[356] = 64.920911549758912 + lats[357] = 64.850612787427963 + lats[358] = 64.780314025058246 + lats[359] = 64.710015262650074 + lats[360] = 64.639716500203733 + lats[361] = 64.569417737719576 + lats[362] = 64.499118975197902 + lats[363] = 64.428820212639039 + lats[364] = 64.358521450043284 + lats[365] = 64.288222687410922 + lats[366] = 64.21792392474228 + lats[367] = 64.147625162037642 + lats[368] = 64.07732639929732 + lats[369] = 64.00702763652157 + lats[370] = 63.93672887371072 + lats[371] = 63.866430110865004 + lats[372] = 63.796131347984762 + lats[373] = 63.725832585070251 + lats[374] = 63.655533822121711 + lats[375] = 63.585235059139464 + lats[376] = 63.514936296123757 + lats[377] = 63.444637533074854 + lats[378] = 63.374338769993031 + lats[379] = 63.304040006878537 + lats[380] = 63.23374124373165 + lats[381] = 63.163442480552604 + lats[382] = 63.093143717341647 + lats[383] = 63.022844954099064 + lats[384] = 62.952546190825068 + lats[385] = 62.882247427519928 + lats[386] = 62.811948664183866 + lats[387] = 62.741649900817137 + lats[388] = 62.67135113741999 + lats[389] = 62.60105237399263 + lats[390] = 62.530753610535321 + lats[391] = 62.460454847048261 + lats[392] = 62.3901560835317 + lats[393] = 62.319857319985871 + lats[394] = 62.249558556410982 + lats[395] = 62.179259792807258 + lats[396] = 62.108961029174914 + lats[397] = 62.038662265514176 + lats[398] = 61.968363501825259 + lats[399] = 61.898064738108381 + lats[400] = 61.827765974363729 + lats[401] = 61.757467210591535 + lats[402] = 61.687168446791986 + lats[403] = 61.616869682965287 + lats[404] = 61.546570919111666 + lats[405] = 61.476272155231321 + lats[406] = 61.405973391324409 + lats[407] = 61.335674627391185 + lats[408] = 61.265375863431785 + lats[409] = 61.195077099446451 + lats[410] = 61.124778335435344 + lats[411] = 61.054479571398652 + lats[412] = 60.984180807336578 + lats[413] = 60.913882043249295 + lats[414] = 60.843583279137007 + lats[415] = 60.773284514999872 + lats[416] = 60.702985750838074 + lats[417] = 60.632686986651805 + lats[418] = 60.562388222441243 + lats[419] = 60.492089458206543 + lats[420] = 60.421790693947884 + lats[421] = 60.35149192966545 + lats[422] = 60.28119316535939 + lats[423] = 60.21089440102989 + lats[424] = 60.140595636677112 + lats[425] = 60.070296872301235 + lats[426] = 59.999998107902378 + lats[427] = 59.929699343480763 + lats[428] = 59.859400579036503 + lats[429] = 59.78910181456979 + lats[430] = 59.718803050080759 + lats[431] = 59.64850428556958 + lats[432] = 59.578205521036402 + lats[433] = 59.507906756481383 + lats[434] = 59.43760799190467 + lats[435] = 59.3673092273064 + lats[436] = 59.29701046268675 + lats[437] = 59.226711698045854 + lats[438] = 59.156412933383855 + lats[439] = 59.086114168700909 + lats[440] = 59.015815403997145 + lats[441] = 58.945516639272725 + lats[442] = 58.875217874527763 + lats[443] = 58.804919109762423 + lats[444] = 58.73462034497684 + lats[445] = 58.664321580171141 + lats[446] = 58.594022815345468 + lats[447] = 58.523724050499972 + lats[448] = 58.453425285634758 + lats[449] = 58.383126520749968 + lats[450] = 58.312827755845746 + lats[451] = 58.242528990922203 + lats[452] = 58.172230225979497 + lats[453] = 58.101931461017728 + lats[454] = 58.031632696037022 + lats[455] = 57.961333931037537 + lats[456] = 57.891035166019364 + lats[457] = 57.820736400982646 + lats[458] = 57.75043763592749 + lats[459] = 57.680138870854037 + lats[460] = 57.60984010576238 + lats[461] = 57.539541340652676 + lats[462] = 57.469242575525016 + lats[463] = 57.398943810379521 + lats[464] = 57.328645045216312 + lats[465] = 57.258346280035504 + lats[466] = 57.188047514837208 + lats[467] = 57.117748749621541 + lats[468] = 57.047449984388614 + lats[469] = 56.977151219138541 + lats[470] = 56.90685245387143 + lats[471] = 56.836553688587379 + lats[472] = 56.766254923286517 + lats[473] = 56.695956157968951 + lats[474] = 56.625657392634771 + lats[475] = 56.555358627284086 + lats[476] = 56.485059861917016 + lats[477] = 56.41476109653366 + lats[478] = 56.34446233113411 + lats[479] = 56.274163565718467 + lats[480] = 56.203864800286865 + lats[481] = 56.133566034839362 + lats[482] = 56.063267269376091 + lats[483] = 55.992968503897131 + lats[484] = 55.922669738402583 + lats[485] = 55.852370972892551 + lats[486] = 55.782072207367136 + lats[487] = 55.711773441826416 + lats[488] = 55.641474676270505 + lats[489] = 55.571175910699488 + lats[490] = 55.500877145113449 + lats[491] = 55.430578379512511 + lats[492] = 55.360279613896743 + lats[493] = 55.289980848266232 + lats[494] = 55.219682082621084 + lats[495] = 55.149383316961377 + lats[496] = 55.07908455128721 + lats[497] = 55.008785785598668 + lats[498] = 54.938487019895831 + lats[499] = 54.868188254178797 + lats[500] = 54.797889488447652 + lats[501] = 54.727590722702473 + lats[502] = 54.657291956943347 + lats[503] = 54.586993191170357 + lats[504] = 54.516694425383605 + lats[505] = 54.446395659583146 + lats[506] = 54.376096893769081 + lats[507] = 54.305798127941479 + lats[508] = 54.235499362100448 + lats[509] = 54.165200596246031 + lats[510] = 54.094901830378333 + lats[511] = 54.024603064497434 + lats[512] = 53.954304298603383 + lats[513] = 53.884005532696307 + lats[514] = 53.813706766776235 + lats[515] = 53.743408000843282 + lats[516] = 53.673109234897495 + lats[517] = 53.602810468938962 + lats[518] = 53.53251170296776 + lats[519] = 53.462212936983953 + lats[520] = 53.391914170987633 + lats[521] = 53.321615404978871 + lats[522] = 53.251316638957725 + lats[523] = 53.181017872924265 + lats[524] = 53.110719106878584 + lats[525] = 53.040420340820731 + lats[526] = 52.970121574750792 + lats[527] = 52.899822808668837 + lats[528] = 52.829524042574917 + lats[529] = 52.759225276469131 + lats[530] = 52.688926510351514 + lats[531] = 52.618627744222159 + lats[532] = 52.548328978081123 + lats[533] = 52.478030211928477 + lats[534] = 52.407731445764284 + lats[535] = 52.337432679588609 + lats[536] = 52.26713391340153 + lats[537] = 52.196835147203096 + lats[538] = 52.126536380993372 + lats[539] = 52.056237614772435 + lats[540] = 51.985938848540336 + lats[541] = 51.915640082297152 + lats[542] = 51.845341316042933 + lats[543] = 51.775042549777737 + lats[544] = 51.704743783501634 + lats[545] = 51.634445017214695 + lats[546] = 51.56414625091697 + lats[547] = 51.493847484608516 + lats[548] = 51.423548718289396 + lats[549] = 51.353249951959683 + lats[550] = 51.282951185619417 + lats[551] = 51.21265241926865 + lats[552] = 51.14235365290746 + lats[553] = 51.072054886535909 + lats[554] = 51.001756120154049 + lats[555] = 50.931457353761914 + lats[556] = 50.86115858735959 + lats[557] = 50.790859820947119 + lats[558] = 50.720561054524559 + lats[559] = 50.650262288091959 + lats[560] = 50.579963521649397 + lats[561] = 50.509664755196901 + lats[562] = 50.439365988734544 + lats[563] = 50.369067222262359 + lats[564] = 50.298768455780426 + lats[565] = 50.228469689288779 + lats[566] = 50.158170922787484 + lats[567] = 50.087872156276575 + lats[568] = 50.017573389756123 + lats[569] = 49.947274623226157 + lats[570] = 49.876975856686762 + lats[571] = 49.80667709013796 + lats[572] = 49.736378323579807 + lats[573] = 49.66607955701236 + lats[574] = 49.595780790435676 + lats[575] = 49.525482023849783 + lats[576] = 49.455183257254745 + lats[577] = 49.384884490650613 + lats[578] = 49.314585724037435 + lats[579] = 49.244286957415234 + lats[580] = 49.173988190784094 + lats[581] = 49.103689424144044 + lats[582] = 49.03339065749514 + lats[583] = 48.963091890837418 + lats[584] = 48.892793124170929 + lats[585] = 48.822494357495721 + lats[586] = 48.752195590811837 + lats[587] = 48.681896824119335 + lats[588] = 48.611598057418242 + lats[589] = 48.541299290708608 + lats[590] = 48.47100052399049 + lats[591] = 48.400701757263917 + lats[592] = 48.330402990528938 + lats[593] = 48.260104223785596 + lats[594] = 48.189805457033941 + lats[595] = 48.119506690274015 + lats[596] = 48.049207923505868 + lats[597] = 47.978909156729507 + lats[598] = 47.908610389945018 + lats[599] = 47.838311623152421 + lats[600] = 47.76801285635176 + lats[601] = 47.697714089543084 + lats[602] = 47.627415322726435 + lats[603] = 47.557116555901828 + lats[604] = 47.486817789069342 + lats[605] = 47.416519022228997 + lats[606] = 47.346220255380835 + lats[607] = 47.275921488524894 + lats[608] = 47.205622721661214 + lats[609] = 47.13532395478984 + lats[610] = 47.065025187910805 + lats[611] = 46.994726421024154 + lats[612] = 46.924427654129929 + lats[613] = 46.85412888722815 + lats[614] = 46.783830120318882 + lats[615] = 46.713531353402139 + lats[616] = 46.643232586477971 + lats[617] = 46.572933819546414 + lats[618] = 46.502635052607502 + lats[619] = 46.432336285661272 + lats[620] = 46.362037518707766 + lats[621] = 46.291738751747012 + lats[622] = 46.221439984779053 + lats[623] = 46.151141217803925 + lats[624] = 46.080842450821663 + lats[625] = 46.01054368383231 + lats[626] = 45.94024491683588 + lats[627] = 45.869946149832437 + lats[628] = 45.799647382821995 + lats[629] = 45.729348615804589 + lats[630] = 45.659049848780263 + lats[631] = 45.588751081749038 + lats[632] = 45.51845231471097 + lats[633] = 45.448153547666081 + lats[634] = 45.377854780614399 + lats[635] = 45.30755601355596 + lats[636] = 45.237257246490813 + lats[637] = 45.166958479418959 + lats[638] = 45.096659712340461 + lats[639] = 45.026360945255341 + lats[640] = 44.956062178163634 + lats[641] = 44.885763411065362 + lats[642] = 44.81546464396056 + lats[643] = 44.745165876849271 + lats[644] = 44.674867109731515 + lats[645] = 44.604568342607337 + lats[646] = 44.534269575476756 + lats[647] = 44.463970808339802 + lats[648] = 44.39367204119651 + lats[649] = 44.323373274046915 + lats[650] = 44.253074506891046 + lats[651] = 44.182775739728925 + lats[652] = 44.112476972560586 + lats[653] = 44.042178205386072 + lats[654] = 43.971879438205391 + lats[655] = 43.9015806710186 + lats[656] = 43.831281903825705 + lats[657] = 43.760983136626741 + lats[658] = 43.690684369421732 + lats[659] = 43.620385602210717 + lats[660] = 43.550086834993728 + lats[661] = 43.479788067770777 + lats[662] = 43.409489300541907 + lats[663] = 43.339190533307139 + lats[664] = 43.26889176606651 + lats[665] = 43.19859299882004 + lats[666] = 43.128294231567757 + lats[667] = 43.057995464309691 + lats[668] = 42.987696697045862 + lats[669] = 42.917397929776307 + lats[670] = 42.847099162501053 + lats[671] = 42.776800395220121 + lats[672] = 42.706501627933541 + lats[673] = 42.63620286064134 + lats[674] = 42.565904093343548 + lats[675] = 42.495605326040177 + lats[676] = 42.425306558731272 + lats[677] = 42.355007791416853 + lats[678] = 42.284709024096927 + lats[679] = 42.214410256771551 + lats[680] = 42.144111489440725 + lats[681] = 42.073812722104492 + lats[682] = 42.003513954762873 + lats[683] = 41.933215187415882 + lats[684] = 41.862916420063563 + lats[685] = 41.792617652705921 + lats[686] = 41.722318885343 + lats[687] = 41.6520201179748 + lats[688] = 41.581721350601363 + lats[689] = 41.511422583222718 + lats[690] = 41.441123815838885 + lats[691] = 41.370825048449873 + lats[692] = 41.300526281055724 + lats[693] = 41.230227513656445 + lats[694] = 41.159928746252085 + lats[695] = 41.089629978842645 + lats[696] = 41.01933121142816 + lats[697] = 40.949032444008644 + lats[698] = 40.878733676584126 + lats[699] = 40.808434909154634 + lats[700] = 40.738136141720176 + lats[701] = 40.667837374280786 + lats[702] = 40.597538606836487 + lats[703] = 40.527239839387299 + lats[704] = 40.456941071933244 + lats[705] = 40.386642304474343 + lats[706] = 40.316343537010617 + lats[707] = 40.246044769542102 + lats[708] = 40.175746002068806 + lats[709] = 40.105447234590748 + lats[710] = 40.035148467107952 + lats[711] = 39.964849699620437 + lats[712] = 39.894550932128247 + lats[713] = 39.824252164631375 + lats[714] = 39.753953397129855 + lats[715] = 39.683654629623703 + lats[716] = 39.613355862112947 + lats[717] = 39.543057094597607 + lats[718] = 39.472758327077692 + lats[719] = 39.402459559553229 + lats[720] = 39.332160792024254 + lats[721] = 39.261862024490775 + lats[722] = 39.191563256952804 + lats[723] = 39.121264489410365 + lats[724] = 39.050965721863491 + lats[725] = 38.980666954312184 + lats[726] = 38.910368186756479 + lats[727] = 38.840069419196389 + lats[728] = 38.769770651631937 + lats[729] = 38.699471884063136 + lats[730] = 38.629173116490001 + lats[731] = 38.558874348912568 + lats[732] = 38.488575581330842 + lats[733] = 38.418276813744846 + lats[734] = 38.347978046154608 + lats[735] = 38.277679278560143 + lats[736] = 38.20738051096145 + lats[737] = 38.137081743358586 + lats[738] = 38.066782975751536 + lats[739] = 37.99648420814033 + lats[740] = 37.926185440524989 + lats[741] = 37.855886672905527 + lats[742] = 37.785587905281965 + lats[743] = 37.715289137654317 + lats[744] = 37.644990370022605 + lats[745] = 37.574691602386856 + lats[746] = 37.504392834747065 + lats[747] = 37.434094067103274 + lats[748] = 37.363795299455489 + lats[749] = 37.293496531803719 + lats[750] = 37.223197764147997 + lats[751] = 37.152898996488332 + lats[752] = 37.082600228824752 + lats[753] = 37.012301461157264 + lats[754] = 36.942002693485883 + lats[755] = 36.871703925810628 + lats[756] = 36.801405158131523 + lats[757] = 36.731106390448581 + lats[758] = 36.660807622761808 + lats[759] = 36.590508855071242 + lats[760] = 36.520210087376888 + lats[761] = 36.449911319678755 + lats[762] = 36.379612551976876 + lats[763] = 36.309313784271254 + lats[764] = 36.239015016561908 + lats[765] = 36.16871624884886 + lats[766] = 36.098417481132117 + lats[767] = 36.028118713411708 + lats[768] = 35.957819945687639 + lats[769] = 35.887521177959933 + lats[770] = 35.817222410228595 + lats[771] = 35.746923642493655 + lats[772] = 35.676624874755113 + lats[773] = 35.606326107012997 + lats[774] = 35.536027339267314 + lats[775] = 35.465728571518085 + lats[776] = 35.395429803765317 + lats[777] = 35.325131036009047 + lats[778] = 35.254832268249267 + lats[779] = 35.184533500486005 + lats[780] = 35.114234732719261 + lats[781] = 35.043935964949064 + lats[782] = 34.973637197175435 + lats[783] = 34.903338429398374 + lats[784] = 34.833039661617903 + lats[785] = 34.762740893834028 + lats[786] = 34.692442126046771 + lats[787] = 34.622143358256153 + lats[788] = 34.551844590462188 + lats[789] = 34.481545822664863 + lats[790] = 34.411247054864234 + lats[791] = 34.340948287060286 + lats[792] = 34.270649519253041 + lats[793] = 34.200350751442521 + lats[794] = 34.130051983628725 + lats[795] = 34.059753215811682 + lats[796] = 33.989454447991392 + lats[797] = 33.919155680167876 + lats[798] = 33.848856912341155 + lats[799] = 33.778558144511237 + lats[800] = 33.708259376678136 + lats[801] = 33.637960608841851 + lats[802] = 33.567661841002426 + lats[803] = 33.497363073159853 + lats[804] = 33.42706430531414 + lats[805] = 33.356765537465314 + lats[806] = 33.286466769613391 + lats[807] = 33.216168001758369 + lats[808] = 33.145869233900278 + lats[809] = 33.075570466039117 + lats[810] = 33.005271698174909 + lats[811] = 32.934972930307666 + lats[812] = 32.864674162437396 + lats[813] = 32.794375394564113 + lats[814] = 32.724076626687825 + lats[815] = 32.653777858808567 + lats[816] = 32.583479090926325 + lats[817] = 32.513180323041112 + lats[818] = 32.442881555152965 + lats[819] = 32.372582787261891 + lats[820] = 32.302284019367875 + lats[821] = 32.231985251470959 + lats[822] = 32.161686483571145 + lats[823] = 32.091387715668439 + lats[824] = 32.021088947762863 + lats[825] = 31.950790179854422 + lats[826] = 31.880491411943137 + lats[827] = 31.810192644029012 + lats[828] = 31.739893876112063 + lats[829] = 31.669595108192297 + lats[830] = 31.599296340269738 + lats[831] = 31.528997572344384 + lats[832] = 31.458698804416255 + lats[833] = 31.388400036485361 + lats[834] = 31.318101268551715 + lats[835] = 31.247802500615318 + lats[836] = 31.177503732676204 + lats[837] = 31.107204964734358 + lats[838] = 31.036906196789811 + lats[839] = 30.966607428842572 + lats[840] = 30.896308660892647 + lats[841] = 30.826009892940046 + lats[842] = 30.755711124984781 + lats[843] = 30.685412357026873 + lats[844] = 30.615113589066322 + lats[845] = 30.544814821103138 + lats[846] = 30.47451605313735 + lats[847] = 30.404217285168947 + lats[848] = 30.333918517197947 + lats[849] = 30.263619749224372 + lats[850] = 30.19332098124822 + lats[851] = 30.123022213269511 + lats[852] = 30.052723445288244 + lats[853] = 29.98242467730444 + lats[854] = 29.91212590931811 + lats[855] = 29.841827141329258 + lats[856] = 29.771528373337894 + lats[857] = 29.701229605344039 + lats[858] = 29.630930837347698 + lats[859] = 29.560632069348884 + lats[860] = 29.490333301347597 + lats[861] = 29.420034533343859 + lats[862] = 29.349735765337677 + lats[863] = 29.279436997329057 + lats[864] = 29.209138229318015 + lats[865] = 29.138839461304556 + lats[866] = 29.068540693288696 + lats[867] = 28.998241925270449 + lats[868] = 28.927943157249814 + lats[869] = 28.857644389226806 + lats[870] = 28.787345621201432 + lats[871] = 28.717046853173709 + lats[872] = 28.646748085143642 + lats[873] = 28.576449317111244 + lats[874] = 28.506150549076519 + lats[875] = 28.435851781039485 + lats[876] = 28.365553013000145 + lats[877] = 28.29525424495851 + lats[878] = 28.224955476914594 + lats[879] = 28.154656708868405 + lats[880] = 28.084357940819952 + lats[881] = 28.014059172769244 + lats[882] = 27.94376040471629 + lats[883] = 27.873461636661098 + lats[884] = 27.803162868603682 + lats[885] = 27.732864100544052 + lats[886] = 27.662565332482213 + lats[887] = 27.592266564418171 + lats[888] = 27.521967796351948 + lats[889] = 27.451669028283543 + lats[890] = 27.381370260212968 + lats[891] = 27.311071492140236 + lats[892] = 27.240772724065348 + lats[893] = 27.170473955988321 + lats[894] = 27.100175187909159 + lats[895] = 27.029876419827872 + lats[896] = 26.959577651744471 + lats[897] = 26.889278883658971 + lats[898] = 26.818980115571364 + lats[899] = 26.748681347481678 + lats[900] = 26.678382579389908 + lats[901] = 26.608083811296069 + lats[902] = 26.53778504320017 + lats[903] = 26.467486275102218 + lats[904] = 26.397187507002222 + lats[905] = 26.326888738900195 + lats[906] = 26.256589970796135 + lats[907] = 26.186291202690064 + lats[908] = 26.115992434581983 + lats[909] = 26.045693666471902 + lats[910] = 25.975394898359827 + lats[911] = 25.90509613024577 + lats[912] = 25.834797362129745 + lats[913] = 25.764498594011751 + lats[914] = 25.694199825891793 + lats[915] = 25.623901057769892 + lats[916] = 25.553602289646051 + lats[917] = 25.483303521520277 + lats[918] = 25.413004753392578 + lats[919] = 25.342705985262967 + lats[920] = 25.272407217131445 + lats[921] = 25.202108448998025 + lats[922] = 25.13180968086272 + lats[923] = 25.061510912725527 + lats[924] = 24.991212144586456 + lats[925] = 24.920913376445526 + lats[926] = 24.850614608302738 + lats[927] = 24.780315840158096 + lats[928] = 24.710017072011613 + lats[929] = 24.639718303863294 + lats[930] = 24.569419535713152 + lats[931] = 24.499120767561195 + lats[932] = 24.428821999407425 + lats[933] = 24.358523231251851 + lats[934] = 24.288224463094483 + lats[935] = 24.217925694935328 + lats[936] = 24.1476269267744 + lats[937] = 24.077328158611696 + lats[938] = 24.007029390447226 + lats[939] = 23.936730622281004 + lats[940] = 23.866431854113038 + lats[941] = 23.796133085943328 + lats[942] = 23.725834317771888 + lats[943] = 23.655535549598721 + lats[944] = 23.585236781423838 + lats[945] = 23.514938013247242 + lats[946] = 23.444639245068949 + lats[947] = 23.374340476888957 + lats[948] = 23.304041708707278 + lats[949] = 23.233742940523921 + lats[950] = 23.163444172338895 + lats[951] = 23.0931454041522 + lats[952] = 23.022846635963852 + lats[953] = 22.952547867773848 + lats[954] = 22.882249099582204 + lats[955] = 22.811950331388925 + lats[956] = 22.741651563194019 + lats[957] = 22.671352794997489 + lats[958] = 22.60105402679935 + lats[959] = 22.530755258599601 + lats[960] = 22.460456490398254 + lats[961] = 22.390157722195315 + lats[962] = 22.319858953990789 + lats[963] = 22.249560185784691 + lats[964] = 22.179261417577013 + lats[965] = 22.108962649367779 + lats[966] = 22.038663881156989 + lats[967] = 21.968365112944642 + lats[968] = 21.898066344730758 + lats[969] = 21.827767576515338 + lats[970] = 21.757468808298391 + lats[971] = 21.687170040079913 + lats[972] = 21.616871271859928 + lats[973] = 21.546572503638437 + lats[974] = 21.47627373541544 + lats[975] = 21.40597496719095 + lats[976] = 21.335676198964972 + lats[977] = 21.265377430737512 + lats[978] = 21.195078662508585 + lats[979] = 21.124779894278181 + lats[980] = 21.054481126046323 + lats[981] = 20.984182357813012 + lats[982] = 20.913883589578251 + lats[983] = 20.843584821342048 + lats[984] = 20.773286053104417 + lats[985] = 20.702987284865355 + lats[986] = 20.632688516624874 + lats[987] = 20.562389748382977 + lats[988] = 20.492090980139672 + lats[989] = 20.421792211894967 + lats[990] = 20.35149344364887 + lats[991] = 20.28119467540138 + lats[992] = 20.210895907152516 + lats[993] = 20.140597138902272 + lats[994] = 20.070298370650661 + lats[995] = 19.999999602397686 + lats[996] = 19.929700834143357 + lats[997] = 19.859402065887682 + lats[998] = 19.789103297630657 + lats[999] = 19.718804529372303 + lats[1000] = 19.648505761112613 + lats[1001] = 19.578206992851602 + lats[1002] = 19.507908224589269 + lats[1003] = 19.437609456325632 + lats[1004] = 19.367310688060684 + lats[1005] = 19.297011919794439 + lats[1006] = 19.226713151526898 + lats[1007] = 19.15641438325807 + lats[1008] = 19.086115614987968 + lats[1009] = 19.015816846716586 + lats[1010] = 18.945518078443939 + lats[1011] = 18.875219310170031 + lats[1012] = 18.804920541894862 + lats[1013] = 18.734621773618446 + lats[1014] = 18.664323005340787 + lats[1015] = 18.594024237061891 + lats[1016] = 18.523725468781763 + lats[1017] = 18.453426700500408 + lats[1018] = 18.383127932217832 + lats[1019] = 18.312829163934047 + lats[1020] = 18.242530395649048 + lats[1021] = 18.172231627362851 + lats[1022] = 18.101932859075458 + lats[1023] = 18.031634090786874 + lats[1024] = 17.96133532249711 + lats[1025] = 17.89103655420616 + lats[1026] = 17.820737785914044 + lats[1027] = 17.75043901762076 + lats[1028] = 17.680140249326314 + lats[1029] = 17.60984148103071 + lats[1030] = 17.539542712733962 + lats[1031] = 17.469243944436066 + lats[1032] = 17.39894517613704 + lats[1033] = 17.328646407836878 + lats[1034] = 17.258347639535586 + lats[1035] = 17.188048871233182 + lats[1036] = 17.117750102929655 + lats[1037] = 17.04745133462502 + lats[1038] = 16.977152566319283 + lats[1039] = 16.906853798012452 + lats[1040] = 16.836555029704527 + lats[1041] = 16.766256261395515 + lats[1042] = 16.69595749308542 + lats[1043] = 16.625658724774254 + lats[1044] = 16.555359956462013 + lats[1045] = 16.485061188148713 + lats[1046] = 16.41476241983435 + lats[1047] = 16.344463651518936 + lats[1048] = 16.274164883202477 + lats[1049] = 16.203866114884974 + lats[1050] = 16.133567346566434 + lats[1051] = 16.063268578246863 + lats[1052] = 15.992969809926265 + lats[1053] = 15.922671041604652 + lats[1054] = 15.852372273282016 + lats[1055] = 15.78207350495838 + lats[1056] = 15.711774736633735 + lats[1057] = 15.641475968308091 + lats[1058] = 15.571177199981456 + lats[1059] = 15.500878431653829 + lats[1060] = 15.430579663325226 + lats[1061] = 15.360280894995643 + lats[1062] = 15.289982126665089 + lats[1063] = 15.219683358333569 + lats[1064] = 15.149384590001089 + lats[1065] = 15.07908582166765 + lats[1066] = 15.008787053333259 + lats[1067] = 14.938488284997929 + lats[1068] = 14.868189516661655 + lats[1069] = 14.797890748324447 + lats[1070] = 14.727591979986309 + lats[1071] = 14.657293211647247 + lats[1072] = 14.586994443307265 + lats[1073] = 14.516695674966371 + lats[1074] = 14.446396906624567 + lats[1075] = 14.376098138281863 + lats[1076] = 14.305799369938256 + lats[1077] = 14.23550060159376 + lats[1078] = 14.165201833248371 + lats[1079] = 14.0949030649021 + lats[1080] = 14.024604296554955 + lats[1081] = 13.954305528206934 + lats[1082] = 13.884006759858046 + lats[1083] = 13.813707991508297 + lats[1084] = 13.743409223157688 + lats[1085] = 13.673110454806226 + lats[1086] = 13.602811686453919 + lats[1087] = 13.532512918100766 + lats[1088] = 13.46221414974678 + lats[1089] = 13.391915381391959 + lats[1090] = 13.32161661303631 + lats[1091] = 13.251317844679837 + lats[1092] = 13.181019076322551 + lats[1093] = 13.110720307964451 + lats[1094] = 13.040421539605545 + lats[1095] = 12.970122771245832 + lats[1096] = 12.899824002885323 + lats[1097] = 12.829525234524022 + lats[1098] = 12.759226466161934 + lats[1099] = 12.688927697799061 + lats[1100] = 12.618628929435411 + lats[1101] = 12.548330161070988 + lats[1102] = 12.478031392705796 + lats[1103] = 12.407732624339841 + lats[1104] = 12.337433855973126 + lats[1105] = 12.267135087605659 + lats[1106] = 12.196836319237443 + lats[1107] = 12.126537550868482 + lats[1108] = 12.056238782498781 + lats[1109] = 11.985940014128348 + lats[1110] = 11.915641245757183 + lats[1111] = 11.845342477385294 + lats[1112] = 11.775043709012685 + lats[1113] = 11.704744940639358 + lats[1114] = 11.634446172265324 + lats[1115] = 11.564147403890583 + lats[1116] = 11.493848635515141 + lats[1117] = 11.423549867139002 + lats[1118] = 11.35325109876217 + lats[1119] = 11.282952330384653 + lats[1120] = 11.212653562006453 + lats[1121] = 11.142354793627575 + lats[1122] = 11.072056025248026 + lats[1123] = 11.001757256867807 + lats[1124] = 10.931458488486923 + lats[1125] = 10.861159720105382 + lats[1126] = 10.790860951723188 + lats[1127] = 10.720562183340341 + lats[1128] = 10.65026341495685 + lats[1129] = 10.579964646572719 + lats[1130] = 10.509665878187954 + lats[1131] = 10.439367109802557 + lats[1132] = 10.369068341416533 + lats[1133] = 10.298769573029887 + lats[1134] = 10.228470804642624 + lats[1135] = 10.158172036254747 + lats[1136] = 10.087873267866264 + lats[1137] = 10.017574499477174 + lats[1138] = 9.9472757310874869 + lats[1139] = 9.8769769626972046 + lats[1140] = 9.8066781943063344 + lats[1141] = 9.7363794259148779 + lats[1142] = 9.6660806575228388 + lats[1143] = 9.5957818891302242 + lats[1144] = 9.5254831207370376 + lats[1145] = 9.4551843523432826 + lats[1146] = 9.3848855839489662 + lats[1147] = 9.3145868155540921 + lats[1148] = 9.2442880471586619 + lats[1149] = 9.1739892787626829 + lats[1150] = 9.1036905103661585 + lats[1151] = 9.0333917419690941 + lats[1152] = 8.963092973571495 + lats[1153] = 8.8927942051733631 + lats[1154] = 8.8224954367747017 + lats[1155] = 8.7521966683755217 + lats[1156] = 8.6818978999758194 + lats[1157] = 8.6115991315756055 + lats[1158] = 8.5413003631748801 + lats[1159] = 8.4710015947736537 + lats[1160] = 8.4007028263719228 + lats[1161] = 8.3304040579696963 + lats[1162] = 8.2601052895669778 + lats[1163] = 8.1898065211637725 + lats[1164] = 8.1195077527600841 + lats[1165] = 8.049208984355916 + lats[1166] = 7.9789102159512737 + lats[1167] = 7.9086114475461606 + lats[1168] = 7.8383126791405831 + lats[1169] = 7.7680139107345463 + lats[1170] = 7.6977151423280494 + lats[1171] = 7.6274163739210996 + lats[1172] = 7.557117605513703 + lats[1173] = 7.4868188371058624 + lats[1174] = 7.4165200686975803 + lats[1175] = 7.3462213002888648 + lats[1176] = 7.2759225318797176 + lats[1177] = 7.2056237634701441 + lats[1178] = 7.1353249950601469 + lats[1179] = 7.0650262266497315 + lats[1180] = 6.994727458238903 + lats[1181] = 6.924428689827665 + lats[1182] = 6.8541299214160212 + lats[1183] = 6.7838311530039768 + lats[1184] = 6.7135323845915353 + lats[1185] = 6.6432336161787013 + lats[1186] = 6.5729348477654792 + lats[1187] = 6.5026360793518734 + lats[1188] = 6.4323373109378874 + lats[1189] = 6.3620385425235257 + lats[1190] = 6.2917397741087928 + lats[1191] = 6.2214410056936931 + lats[1192] = 6.151142237278231 + lats[1193] = 6.0808434688624091 + lats[1194] = 6.0105447004462347 + lats[1195] = 5.9402459320297085 + lats[1196] = 5.869947163612836 + lats[1197] = 5.7996483951956233 + lats[1198] = 5.729349626778073 + lats[1199] = 5.6590508583601888 + lats[1200] = 5.5887520899419751 + lats[1201] = 5.5184533215234373 + lats[1202] = 5.4481545531045787 + lats[1203] = 5.3778557846854023 + lats[1204] = 5.3075570162659149 + lats[1205] = 5.2372582478461194 + lats[1206] = 5.1669594794260192 + lats[1207] = 5.0966607110056197 + lats[1208] = 5.0263619425849244 + lats[1209] = 4.9560631741639369 + lats[1210] = 4.8857644057426626 + lats[1211] = 4.8154656373211049 + lats[1212] = 4.7451668688992683 + lats[1213] = 4.6748681004771564 + lats[1214] = 4.6045693320547736 + lats[1215] = 4.5342705636321252 + lats[1216] = 4.4639717952092139 + lats[1217] = 4.3936730267860451 + lats[1218] = 4.3233742583626205 + lats[1219] = 4.2530754899389471 + lats[1220] = 4.1827767215150269 + lats[1221] = 4.1124779530908659 + lats[1222] = 4.0421791846664661 + lats[1223] = 3.9718804162418326 + lats[1224] = 3.90158164781697 + lats[1225] = 3.8312828793918823 + lats[1226] = 3.7609841109665734 + lats[1227] = 3.6906853425410477 + lats[1228] = 3.6203865741153085 + lats[1229] = 3.5500878056893601 + lats[1230] = 3.4797890372632065 + lats[1231] = 3.4094902688368531 + lats[1232] = 3.339191500410303 + lats[1233] = 3.2688927319835597 + lats[1234] = 3.1985939635566285 + lats[1235] = 3.1282951951295126 + lats[1236] = 3.0579964267022164 + lats[1237] = 2.9876976582747439 + lats[1238] = 2.9173988898470999 + lats[1239] = 2.8471001214192873 + lats[1240] = 2.7768013529913107 + lats[1241] = 2.7065025845631743 + lats[1242] = 2.6362038161348824 + lats[1243] = 2.5659050477064382 + lats[1244] = 2.4956062792778466 + lats[1245] = 2.4253075108491116 + lats[1246] = 2.3550087424202366 + lats[1247] = 2.2847099739912267 + lats[1248] = 2.2144112055620848 + lats[1249] = 2.1441124371328155 + lats[1250] = 2.0738136687034232 + lats[1251] = 2.0035149002739114 + lats[1252] = 1.9332161318442849 + lats[1253] = 1.8629173634145471 + lats[1254] = 1.792618594984702 + lats[1255] = 1.7223198265547539 + lats[1256] = 1.6520210581247066 + lats[1257] = 1.5817222896945646 + lats[1258] = 1.5114235212643317 + lats[1259] = 1.4411247528340119 + lats[1260] = 1.3708259844036093 + lats[1261] = 1.300527215973128 + lats[1262] = 1.2302284475425722 + lats[1263] = 1.1599296791119456 + lats[1264] = 1.0896309106812523 + lats[1265] = 1.0193321422504964 + lats[1266] = 0.949033373819682 + lats[1267] = 0.87873460538881287 + lats[1268] = 0.80843583695789356 + lats[1269] = 0.73813706852692773 + lats[1270] = 0.66783830009591949 + lats[1271] = 0.59753953166487306 + lats[1272] = 0.52724076323379232 + lats[1273] = 0.45694199480268116 + lats[1274] = 0.3866432263715438 + lats[1275] = 0.31634445794038429 + lats[1276] = 0.24604568950920663 + lats[1277] = 0.17574692107801482 + lats[1278] = 0.10544815264681295 + lats[1279] = 0.035149384215604956 + lats[1280] = -0.035149384215604956 + lats[1281] = -0.10544815264681295 + lats[1282] = -0.17574692107801482 + lats[1283] = -0.24604568950920663 + lats[1284] = -0.31634445794038429 + lats[1285] = -0.3866432263715438 + lats[1286] = -0.45694199480268116 + lats[1287] = -0.52724076323379232 + lats[1288] = -0.59753953166487306 + lats[1289] = -0.66783830009591949 + lats[1290] = -0.73813706852692773 + lats[1291] = -0.80843583695789356 + lats[1292] = -0.87873460538881287 + lats[1293] = -0.949033373819682 + lats[1294] = -1.0193321422504964 + lats[1295] = -1.0896309106812523 + lats[1296] = -1.1599296791119456 + lats[1297] = -1.2302284475425722 + lats[1298] = -1.300527215973128 + lats[1299] = -1.3708259844036093 + lats[1300] = -1.4411247528340119 + lats[1301] = -1.5114235212643317 + lats[1302] = -1.5817222896945646 + lats[1303] = -1.6520210581247066 + lats[1304] = -1.7223198265547539 + lats[1305] = -1.792618594984702 + lats[1306] = -1.8629173634145471 + lats[1307] = -1.9332161318442849 + lats[1308] = -2.0035149002739114 + lats[1309] = -2.0738136687034232 + lats[1310] = -2.1441124371328155 + lats[1311] = -2.2144112055620848 + lats[1312] = -2.2847099739912267 + lats[1313] = -2.3550087424202366 + lats[1314] = -2.4253075108491116 + lats[1315] = -2.4956062792778466 + lats[1316] = -2.5659050477064382 + lats[1317] = -2.6362038161348824 + lats[1318] = -2.7065025845631743 + lats[1319] = -2.7768013529913107 + lats[1320] = -2.8471001214192873 + lats[1321] = -2.9173988898470999 + lats[1322] = -2.9876976582747439 + lats[1323] = -3.0579964267022164 + lats[1324] = -3.1282951951295126 + lats[1325] = -3.1985939635566285 + lats[1326] = -3.2688927319835597 + lats[1327] = -3.339191500410303 + lats[1328] = -3.4094902688368531 + lats[1329] = -3.4797890372632065 + lats[1330] = -3.5500878056893601 + lats[1331] = -3.6203865741153085 + lats[1332] = -3.6906853425410477 + lats[1333] = -3.7609841109665734 + lats[1334] = -3.8312828793918823 + lats[1335] = -3.90158164781697 + lats[1336] = -3.9718804162418326 + lats[1337] = -4.0421791846664661 + lats[1338] = -4.1124779530908659 + lats[1339] = -4.1827767215150269 + lats[1340] = -4.2530754899389471 + lats[1341] = -4.3233742583626205 + lats[1342] = -4.3936730267860451 + lats[1343] = -4.4639717952092139 + lats[1344] = -4.5342705636321252 + lats[1345] = -4.6045693320547736 + lats[1346] = -4.6748681004771564 + lats[1347] = -4.7451668688992683 + lats[1348] = -4.8154656373211049 + lats[1349] = -4.8857644057426626 + lats[1350] = -4.9560631741639369 + lats[1351] = -5.0263619425849244 + lats[1352] = -5.0966607110056197 + lats[1353] = -5.1669594794260192 + lats[1354] = -5.2372582478461194 + lats[1355] = -5.3075570162659149 + lats[1356] = -5.3778557846854023 + lats[1357] = -5.4481545531045787 + lats[1358] = -5.5184533215234373 + lats[1359] = -5.5887520899419751 + lats[1360] = -5.6590508583601888 + lats[1361] = -5.729349626778073 + lats[1362] = -5.7996483951956233 + lats[1363] = -5.869947163612836 + lats[1364] = -5.9402459320297085 + lats[1365] = -6.0105447004462347 + lats[1366] = -6.0808434688624091 + lats[1367] = -6.151142237278231 + lats[1368] = -6.2214410056936931 + lats[1369] = -6.2917397741087928 + lats[1370] = -6.3620385425235257 + lats[1371] = -6.4323373109378874 + lats[1372] = -6.5026360793518734 + lats[1373] = -6.5729348477654792 + lats[1374] = -6.6432336161787013 + lats[1375] = -6.7135323845915353 + lats[1376] = -6.7838311530039768 + lats[1377] = -6.8541299214160212 + lats[1378] = -6.924428689827665 + lats[1379] = -6.994727458238903 + lats[1380] = -7.0650262266497315 + lats[1381] = -7.1353249950601469 + lats[1382] = -7.2056237634701441 + lats[1383] = -7.2759225318797176 + lats[1384] = -7.3462213002888648 + lats[1385] = -7.4165200686975803 + lats[1386] = -7.4868188371058624 + lats[1387] = -7.557117605513703 + lats[1388] = -7.6274163739210996 + lats[1389] = -7.6977151423280494 + lats[1390] = -7.7680139107345463 + lats[1391] = -7.8383126791405831 + lats[1392] = -7.9086114475461606 + lats[1393] = -7.9789102159512737 + lats[1394] = -8.049208984355916 + lats[1395] = -8.1195077527600841 + lats[1396] = -8.1898065211637725 + lats[1397] = -8.2601052895669778 + lats[1398] = -8.3304040579696963 + lats[1399] = -8.4007028263719228 + lats[1400] = -8.4710015947736537 + lats[1401] = -8.5413003631748801 + lats[1402] = -8.6115991315756055 + lats[1403] = -8.6818978999758194 + lats[1404] = -8.7521966683755217 + lats[1405] = -8.8224954367747017 + lats[1406] = -8.8927942051733631 + lats[1407] = -8.963092973571495 + lats[1408] = -9.0333917419690941 + lats[1409] = -9.1036905103661585 + lats[1410] = -9.1739892787626829 + lats[1411] = -9.2442880471586619 + lats[1412] = -9.3145868155540921 + lats[1413] = -9.3848855839489662 + lats[1414] = -9.4551843523432826 + lats[1415] = -9.5254831207370376 + lats[1416] = -9.5957818891302242 + lats[1417] = -9.6660806575228388 + lats[1418] = -9.7363794259148779 + lats[1419] = -9.8066781943063344 + lats[1420] = -9.8769769626972046 + lats[1421] = -9.9472757310874869 + lats[1422] = -10.017574499477174 + lats[1423] = -10.087873267866264 + lats[1424] = -10.158172036254747 + lats[1425] = -10.228470804642624 + lats[1426] = -10.298769573029887 + lats[1427] = -10.369068341416533 + lats[1428] = -10.439367109802557 + lats[1429] = -10.509665878187954 + lats[1430] = -10.579964646572719 + lats[1431] = -10.65026341495685 + lats[1432] = -10.720562183340341 + lats[1433] = -10.790860951723188 + lats[1434] = -10.861159720105382 + lats[1435] = -10.931458488486923 + lats[1436] = -11.001757256867807 + lats[1437] = -11.072056025248026 + lats[1438] = -11.142354793627575 + lats[1439] = -11.212653562006453 + lats[1440] = -11.282952330384653 + lats[1441] = -11.35325109876217 + lats[1442] = -11.423549867139002 + lats[1443] = -11.493848635515141 + lats[1444] = -11.564147403890583 + lats[1445] = -11.634446172265324 + lats[1446] = -11.704744940639358 + lats[1447] = -11.775043709012685 + lats[1448] = -11.845342477385294 + lats[1449] = -11.915641245757183 + lats[1450] = -11.985940014128348 + lats[1451] = -12.056238782498781 + lats[1452] = -12.126537550868482 + lats[1453] = -12.196836319237443 + lats[1454] = -12.267135087605659 + lats[1455] = -12.337433855973126 + lats[1456] = -12.407732624339841 + lats[1457] = -12.478031392705796 + lats[1458] = -12.548330161070988 + lats[1459] = -12.618628929435411 + lats[1460] = -12.688927697799061 + lats[1461] = -12.759226466161934 + lats[1462] = -12.829525234524022 + lats[1463] = -12.899824002885323 + lats[1464] = -12.970122771245832 + lats[1465] = -13.040421539605545 + lats[1466] = -13.110720307964451 + lats[1467] = -13.181019076322551 + lats[1468] = -13.251317844679837 + lats[1469] = -13.32161661303631 + lats[1470] = -13.391915381391959 + lats[1471] = -13.46221414974678 + lats[1472] = -13.532512918100766 + lats[1473] = -13.602811686453919 + lats[1474] = -13.673110454806226 + lats[1475] = -13.743409223157688 + lats[1476] = -13.813707991508297 + lats[1477] = -13.884006759858046 + lats[1478] = -13.954305528206934 + lats[1479] = -14.024604296554955 + lats[1480] = -14.0949030649021 + lats[1481] = -14.165201833248371 + lats[1482] = -14.23550060159376 + lats[1483] = -14.305799369938256 + lats[1484] = -14.376098138281863 + lats[1485] = -14.446396906624567 + lats[1486] = -14.516695674966371 + lats[1487] = -14.586994443307265 + lats[1488] = -14.657293211647247 + lats[1489] = -14.727591979986309 + lats[1490] = -14.797890748324447 + lats[1491] = -14.868189516661655 + lats[1492] = -14.938488284997929 + lats[1493] = -15.008787053333259 + lats[1494] = -15.07908582166765 + lats[1495] = -15.149384590001089 + lats[1496] = -15.219683358333569 + lats[1497] = -15.289982126665089 + lats[1498] = -15.360280894995643 + lats[1499] = -15.430579663325226 + lats[1500] = -15.500878431653829 + lats[1501] = -15.571177199981456 + lats[1502] = -15.641475968308091 + lats[1503] = -15.711774736633735 + lats[1504] = -15.78207350495838 + lats[1505] = -15.852372273282016 + lats[1506] = -15.922671041604652 + lats[1507] = -15.992969809926265 + lats[1508] = -16.063268578246863 + lats[1509] = -16.133567346566434 + lats[1510] = -16.203866114884974 + lats[1511] = -16.274164883202477 + lats[1512] = -16.344463651518936 + lats[1513] = -16.41476241983435 + lats[1514] = -16.485061188148713 + lats[1515] = -16.555359956462013 + lats[1516] = -16.625658724774254 + lats[1517] = -16.69595749308542 + lats[1518] = -16.766256261395515 + lats[1519] = -16.836555029704527 + lats[1520] = -16.906853798012452 + lats[1521] = -16.977152566319283 + lats[1522] = -17.04745133462502 + lats[1523] = -17.117750102929655 + lats[1524] = -17.188048871233182 + lats[1525] = -17.258347639535586 + lats[1526] = -17.328646407836878 + lats[1527] = -17.39894517613704 + lats[1528] = -17.469243944436066 + lats[1529] = -17.539542712733962 + lats[1530] = -17.60984148103071 + lats[1531] = -17.680140249326314 + lats[1532] = -17.75043901762076 + lats[1533] = -17.820737785914044 + lats[1534] = -17.89103655420616 + lats[1535] = -17.96133532249711 + lats[1536] = -18.031634090786874 + lats[1537] = -18.101932859075458 + lats[1538] = -18.172231627362851 + lats[1539] = -18.242530395649048 + lats[1540] = -18.312829163934047 + lats[1541] = -18.383127932217832 + lats[1542] = -18.453426700500408 + lats[1543] = -18.523725468781763 + lats[1544] = -18.594024237061891 + lats[1545] = -18.664323005340787 + lats[1546] = -18.734621773618446 + lats[1547] = -18.804920541894862 + lats[1548] = -18.875219310170031 + lats[1549] = -18.945518078443939 + lats[1550] = -19.015816846716586 + lats[1551] = -19.086115614987968 + lats[1552] = -19.15641438325807 + lats[1553] = -19.226713151526898 + lats[1554] = -19.297011919794439 + lats[1555] = -19.367310688060684 + lats[1556] = -19.437609456325632 + lats[1557] = -19.507908224589269 + lats[1558] = -19.578206992851602 + lats[1559] = -19.648505761112613 + lats[1560] = -19.718804529372303 + lats[1561] = -19.789103297630657 + lats[1562] = -19.859402065887682 + lats[1563] = -19.929700834143357 + lats[1564] = -19.999999602397686 + lats[1565] = -20.070298370650661 + lats[1566] = -20.140597138902272 + lats[1567] = -20.210895907152516 + lats[1568] = -20.28119467540138 + lats[1569] = -20.35149344364887 + lats[1570] = -20.421792211894967 + lats[1571] = -20.492090980139672 + lats[1572] = -20.562389748382977 + lats[1573] = -20.632688516624874 + lats[1574] = -20.702987284865355 + lats[1575] = -20.773286053104417 + lats[1576] = -20.843584821342048 + lats[1577] = -20.913883589578251 + lats[1578] = -20.984182357813012 + lats[1579] = -21.054481126046323 + lats[1580] = -21.124779894278181 + lats[1581] = -21.195078662508585 + lats[1582] = -21.265377430737512 + lats[1583] = -21.335676198964972 + lats[1584] = -21.40597496719095 + lats[1585] = -21.47627373541544 + lats[1586] = -21.546572503638437 + lats[1587] = -21.616871271859928 + lats[1588] = -21.687170040079913 + lats[1589] = -21.757468808298391 + lats[1590] = -21.827767576515338 + lats[1591] = -21.898066344730758 + lats[1592] = -21.968365112944642 + lats[1593] = -22.038663881156989 + lats[1594] = -22.108962649367779 + lats[1595] = -22.179261417577013 + lats[1596] = -22.249560185784691 + lats[1597] = -22.319858953990789 + lats[1598] = -22.390157722195315 + lats[1599] = -22.460456490398254 + lats[1600] = -22.530755258599601 + lats[1601] = -22.60105402679935 + lats[1602] = -22.671352794997489 + lats[1603] = -22.741651563194019 + lats[1604] = -22.811950331388925 + lats[1605] = -22.882249099582204 + lats[1606] = -22.952547867773848 + lats[1607] = -23.022846635963852 + lats[1608] = -23.0931454041522 + lats[1609] = -23.163444172338895 + lats[1610] = -23.233742940523921 + lats[1611] = -23.304041708707278 + lats[1612] = -23.374340476888957 + lats[1613] = -23.444639245068949 + lats[1614] = -23.514938013247242 + lats[1615] = -23.585236781423838 + lats[1616] = -23.655535549598721 + lats[1617] = -23.725834317771888 + lats[1618] = -23.796133085943328 + lats[1619] = -23.866431854113038 + lats[1620] = -23.936730622281004 + lats[1621] = -24.007029390447226 + lats[1622] = -24.077328158611696 + lats[1623] = -24.1476269267744 + lats[1624] = -24.217925694935328 + lats[1625] = -24.288224463094483 + lats[1626] = -24.358523231251851 + lats[1627] = -24.428821999407425 + lats[1628] = -24.499120767561195 + lats[1629] = -24.569419535713152 + lats[1630] = -24.639718303863294 + lats[1631] = -24.710017072011613 + lats[1632] = -24.780315840158096 + lats[1633] = -24.850614608302738 + lats[1634] = -24.920913376445526 + lats[1635] = -24.991212144586456 + lats[1636] = -25.061510912725527 + lats[1637] = -25.13180968086272 + lats[1638] = -25.202108448998025 + lats[1639] = -25.272407217131445 + lats[1640] = -25.342705985262967 + lats[1641] = -25.413004753392578 + lats[1642] = -25.483303521520277 + lats[1643] = -25.553602289646051 + lats[1644] = -25.623901057769892 + lats[1645] = -25.694199825891793 + lats[1646] = -25.764498594011751 + lats[1647] = -25.834797362129745 + lats[1648] = -25.90509613024577 + lats[1649] = -25.975394898359827 + lats[1650] = -26.045693666471902 + lats[1651] = -26.115992434581983 + lats[1652] = -26.186291202690064 + lats[1653] = -26.256589970796135 + lats[1654] = -26.326888738900195 + lats[1655] = -26.397187507002222 + lats[1656] = -26.467486275102218 + lats[1657] = -26.53778504320017 + lats[1658] = -26.608083811296069 + lats[1659] = -26.678382579389908 + lats[1660] = -26.748681347481678 + lats[1661] = -26.818980115571364 + lats[1662] = -26.889278883658971 + lats[1663] = -26.959577651744471 + lats[1664] = -27.029876419827872 + lats[1665] = -27.100175187909159 + lats[1666] = -27.170473955988321 + lats[1667] = -27.240772724065348 + lats[1668] = -27.311071492140236 + lats[1669] = -27.381370260212968 + lats[1670] = -27.451669028283543 + lats[1671] = -27.521967796351948 + lats[1672] = -27.592266564418171 + lats[1673] = -27.662565332482213 + lats[1674] = -27.732864100544052 + lats[1675] = -27.803162868603682 + lats[1676] = -27.873461636661098 + lats[1677] = -27.94376040471629 + lats[1678] = -28.014059172769244 + lats[1679] = -28.084357940819952 + lats[1680] = -28.154656708868405 + lats[1681] = -28.224955476914594 + lats[1682] = -28.29525424495851 + lats[1683] = -28.365553013000145 + lats[1684] = -28.435851781039485 + lats[1685] = -28.506150549076519 + lats[1686] = -28.576449317111244 + lats[1687] = -28.646748085143642 + lats[1688] = -28.717046853173709 + lats[1689] = -28.787345621201432 + lats[1690] = -28.857644389226806 + lats[1691] = -28.927943157249814 + lats[1692] = -28.998241925270449 + lats[1693] = -29.068540693288696 + lats[1694] = -29.138839461304556 + lats[1695] = -29.209138229318015 + lats[1696] = -29.279436997329057 + lats[1697] = -29.349735765337677 + lats[1698] = -29.420034533343859 + lats[1699] = -29.490333301347597 + lats[1700] = -29.560632069348884 + lats[1701] = -29.630930837347698 + lats[1702] = -29.701229605344039 + lats[1703] = -29.771528373337894 + lats[1704] = -29.841827141329258 + lats[1705] = -29.91212590931811 + lats[1706] = -29.98242467730444 + lats[1707] = -30.052723445288244 + lats[1708] = -30.123022213269511 + lats[1709] = -30.19332098124822 + lats[1710] = -30.263619749224372 + lats[1711] = -30.333918517197947 + lats[1712] = -30.404217285168947 + lats[1713] = -30.47451605313735 + lats[1714] = -30.544814821103138 + lats[1715] = -30.615113589066322 + lats[1716] = -30.685412357026873 + lats[1717] = -30.755711124984781 + lats[1718] = -30.826009892940046 + lats[1719] = -30.896308660892647 + lats[1720] = -30.966607428842572 + lats[1721] = -31.036906196789811 + lats[1722] = -31.107204964734358 + lats[1723] = -31.177503732676204 + lats[1724] = -31.247802500615318 + lats[1725] = -31.318101268551715 + lats[1726] = -31.388400036485361 + lats[1727] = -31.458698804416255 + lats[1728] = -31.528997572344384 + lats[1729] = -31.599296340269738 + lats[1730] = -31.669595108192297 + lats[1731] = -31.739893876112063 + lats[1732] = -31.810192644029012 + lats[1733] = -31.880491411943137 + lats[1734] = -31.950790179854422 + lats[1735] = -32.021088947762863 + lats[1736] = -32.091387715668439 + lats[1737] = -32.161686483571145 + lats[1738] = -32.231985251470959 + lats[1739] = -32.302284019367875 + lats[1740] = -32.372582787261891 + lats[1741] = -32.442881555152965 + lats[1742] = -32.513180323041112 + lats[1743] = -32.583479090926325 + lats[1744] = -32.653777858808567 + lats[1745] = -32.724076626687825 + lats[1746] = -32.794375394564113 + lats[1747] = -32.864674162437396 + lats[1748] = -32.934972930307666 + lats[1749] = -33.005271698174909 + lats[1750] = -33.075570466039117 + lats[1751] = -33.145869233900278 + lats[1752] = -33.216168001758369 + lats[1753] = -33.286466769613391 + lats[1754] = -33.356765537465314 + lats[1755] = -33.42706430531414 + lats[1756] = -33.497363073159853 + lats[1757] = -33.567661841002426 + lats[1758] = -33.637960608841851 + lats[1759] = -33.708259376678136 + lats[1760] = -33.778558144511237 + lats[1761] = -33.848856912341155 + lats[1762] = -33.919155680167876 + lats[1763] = -33.989454447991392 + lats[1764] = -34.059753215811682 + lats[1765] = -34.130051983628725 + lats[1766] = -34.200350751442521 + lats[1767] = -34.270649519253041 + lats[1768] = -34.340948287060286 + lats[1769] = -34.411247054864234 + lats[1770] = -34.481545822664863 + lats[1771] = -34.551844590462188 + lats[1772] = -34.622143358256153 + lats[1773] = -34.692442126046771 + lats[1774] = -34.762740893834028 + lats[1775] = -34.833039661617903 + lats[1776] = -34.903338429398374 + lats[1777] = -34.973637197175435 + lats[1778] = -35.043935964949064 + lats[1779] = -35.114234732719261 + lats[1780] = -35.184533500486005 + lats[1781] = -35.254832268249267 + lats[1782] = -35.325131036009047 + lats[1783] = -35.395429803765317 + lats[1784] = -35.465728571518085 + lats[1785] = -35.536027339267314 + lats[1786] = -35.606326107012997 + lats[1787] = -35.676624874755113 + lats[1788] = -35.746923642493655 + lats[1789] = -35.817222410228595 + lats[1790] = -35.887521177959933 + lats[1791] = -35.957819945687639 + lats[1792] = -36.028118713411708 + lats[1793] = -36.098417481132117 + lats[1794] = -36.16871624884886 + lats[1795] = -36.239015016561908 + lats[1796] = -36.309313784271254 + lats[1797] = -36.379612551976876 + lats[1798] = -36.449911319678755 + lats[1799] = -36.520210087376888 + lats[1800] = -36.590508855071242 + lats[1801] = -36.660807622761808 + lats[1802] = -36.731106390448581 + lats[1803] = -36.801405158131523 + lats[1804] = -36.871703925810628 + lats[1805] = -36.942002693485883 + lats[1806] = -37.012301461157264 + lats[1807] = -37.082600228824752 + lats[1808] = -37.152898996488332 + lats[1809] = -37.223197764147997 + lats[1810] = -37.293496531803719 + lats[1811] = -37.363795299455489 + lats[1812] = -37.434094067103274 + lats[1813] = -37.504392834747065 + lats[1814] = -37.574691602386856 + lats[1815] = -37.644990370022605 + lats[1816] = -37.715289137654317 + lats[1817] = -37.785587905281965 + lats[1818] = -37.855886672905527 + lats[1819] = -37.926185440524989 + lats[1820] = -37.99648420814033 + lats[1821] = -38.066782975751536 + lats[1822] = -38.137081743358586 + lats[1823] = -38.20738051096145 + lats[1824] = -38.277679278560143 + lats[1825] = -38.347978046154608 + lats[1826] = -38.418276813744846 + lats[1827] = -38.488575581330842 + lats[1828] = -38.558874348912568 + lats[1829] = -38.629173116490001 + lats[1830] = -38.699471884063136 + lats[1831] = -38.769770651631937 + lats[1832] = -38.840069419196389 + lats[1833] = -38.910368186756479 + lats[1834] = -38.980666954312184 + lats[1835] = -39.050965721863491 + lats[1836] = -39.121264489410365 + lats[1837] = -39.191563256952804 + lats[1838] = -39.261862024490775 + lats[1839] = -39.332160792024254 + lats[1840] = -39.402459559553229 + lats[1841] = -39.472758327077692 + lats[1842] = -39.543057094597607 + lats[1843] = -39.613355862112947 + lats[1844] = -39.683654629623703 + lats[1845] = -39.753953397129855 + lats[1846] = -39.824252164631375 + lats[1847] = -39.894550932128247 + lats[1848] = -39.964849699620437 + lats[1849] = -40.035148467107952 + lats[1850] = -40.105447234590748 + lats[1851] = -40.175746002068806 + lats[1852] = -40.246044769542102 + lats[1853] = -40.316343537010617 + lats[1854] = -40.386642304474343 + lats[1855] = -40.456941071933244 + lats[1856] = -40.527239839387299 + lats[1857] = -40.597538606836487 + lats[1858] = -40.667837374280786 + lats[1859] = -40.738136141720176 + lats[1860] = -40.808434909154634 + lats[1861] = -40.878733676584126 + lats[1862] = -40.949032444008644 + lats[1863] = -41.01933121142816 + lats[1864] = -41.089629978842645 + lats[1865] = -41.159928746252085 + lats[1866] = -41.230227513656445 + lats[1867] = -41.300526281055724 + lats[1868] = -41.370825048449873 + lats[1869] = -41.441123815838885 + lats[1870] = -41.511422583222718 + lats[1871] = -41.581721350601363 + lats[1872] = -41.6520201179748 + lats[1873] = -41.722318885343 + lats[1874] = -41.792617652705921 + lats[1875] = -41.862916420063563 + lats[1876] = -41.933215187415882 + lats[1877] = -42.003513954762873 + lats[1878] = -42.073812722104492 + lats[1879] = -42.144111489440725 + lats[1880] = -42.214410256771551 + lats[1881] = -42.284709024096927 + lats[1882] = -42.355007791416853 + lats[1883] = -42.425306558731272 + lats[1884] = -42.495605326040177 + lats[1885] = -42.565904093343548 + lats[1886] = -42.63620286064134 + lats[1887] = -42.706501627933541 + lats[1888] = -42.776800395220121 + lats[1889] = -42.847099162501053 + lats[1890] = -42.917397929776307 + lats[1891] = -42.987696697045862 + lats[1892] = -43.057995464309691 + lats[1893] = -43.128294231567757 + lats[1894] = -43.19859299882004 + lats[1895] = -43.26889176606651 + lats[1896] = -43.339190533307139 + lats[1897] = -43.409489300541907 + lats[1898] = -43.479788067770777 + lats[1899] = -43.550086834993728 + lats[1900] = -43.620385602210717 + lats[1901] = -43.690684369421732 + lats[1902] = -43.760983136626741 + lats[1903] = -43.831281903825705 + lats[1904] = -43.9015806710186 + lats[1905] = -43.971879438205391 + lats[1906] = -44.042178205386072 + lats[1907] = -44.112476972560586 + lats[1908] = -44.182775739728925 + lats[1909] = -44.253074506891046 + lats[1910] = -44.323373274046915 + lats[1911] = -44.39367204119651 + lats[1912] = -44.463970808339802 + lats[1913] = -44.534269575476756 + lats[1914] = -44.604568342607337 + lats[1915] = -44.674867109731515 + lats[1916] = -44.745165876849271 + lats[1917] = -44.81546464396056 + lats[1918] = -44.885763411065362 + lats[1919] = -44.956062178163634 + lats[1920] = -45.026360945255341 + lats[1921] = -45.096659712340461 + lats[1922] = -45.166958479418959 + lats[1923] = -45.237257246490813 + lats[1924] = -45.30755601355596 + lats[1925] = -45.377854780614399 + lats[1926] = -45.448153547666081 + lats[1927] = -45.51845231471097 + lats[1928] = -45.588751081749038 + lats[1929] = -45.659049848780263 + lats[1930] = -45.729348615804589 + lats[1931] = -45.799647382821995 + lats[1932] = -45.869946149832437 + lats[1933] = -45.94024491683588 + lats[1934] = -46.01054368383231 + lats[1935] = -46.080842450821663 + lats[1936] = -46.151141217803925 + lats[1937] = -46.221439984779053 + lats[1938] = -46.291738751747012 + lats[1939] = -46.362037518707766 + lats[1940] = -46.432336285661272 + lats[1941] = -46.502635052607502 + lats[1942] = -46.572933819546414 + lats[1943] = -46.643232586477971 + lats[1944] = -46.713531353402139 + lats[1945] = -46.783830120318882 + lats[1946] = -46.85412888722815 + lats[1947] = -46.924427654129929 + lats[1948] = -46.994726421024154 + lats[1949] = -47.065025187910805 + lats[1950] = -47.13532395478984 + lats[1951] = -47.205622721661214 + lats[1952] = -47.275921488524894 + lats[1953] = -47.346220255380835 + lats[1954] = -47.416519022228997 + lats[1955] = -47.486817789069342 + lats[1956] = -47.557116555901828 + lats[1957] = -47.627415322726435 + lats[1958] = -47.697714089543084 + lats[1959] = -47.76801285635176 + lats[1960] = -47.838311623152421 + lats[1961] = -47.908610389945018 + lats[1962] = -47.978909156729507 + lats[1963] = -48.049207923505868 + lats[1964] = -48.119506690274015 + lats[1965] = -48.189805457033941 + lats[1966] = -48.260104223785596 + lats[1967] = -48.330402990528938 + lats[1968] = -48.400701757263917 + lats[1969] = -48.47100052399049 + lats[1970] = -48.541299290708608 + lats[1971] = -48.611598057418242 + lats[1972] = -48.681896824119335 + lats[1973] = -48.752195590811837 + lats[1974] = -48.822494357495721 + lats[1975] = -48.892793124170929 + lats[1976] = -48.963091890837418 + lats[1977] = -49.03339065749514 + lats[1978] = -49.103689424144044 + lats[1979] = -49.173988190784094 + lats[1980] = -49.244286957415234 + lats[1981] = -49.314585724037435 + lats[1982] = -49.384884490650613 + lats[1983] = -49.455183257254745 + lats[1984] = -49.525482023849783 + lats[1985] = -49.595780790435676 + lats[1986] = -49.66607955701236 + lats[1987] = -49.736378323579807 + lats[1988] = -49.80667709013796 + lats[1989] = -49.876975856686762 + lats[1990] = -49.947274623226157 + lats[1991] = -50.017573389756123 + lats[1992] = -50.087872156276575 + lats[1993] = -50.158170922787484 + lats[1994] = -50.228469689288779 + lats[1995] = -50.298768455780426 + lats[1996] = -50.369067222262359 + lats[1997] = -50.439365988734544 + lats[1998] = -50.509664755196901 + lats[1999] = -50.579963521649397 + lats[2000] = -50.650262288091959 + lats[2001] = -50.720561054524559 + lats[2002] = -50.790859820947119 + lats[2003] = -50.86115858735959 + lats[2004] = -50.931457353761914 + lats[2005] = -51.001756120154049 + lats[2006] = -51.072054886535909 + lats[2007] = -51.14235365290746 + lats[2008] = -51.21265241926865 + lats[2009] = -51.282951185619417 + lats[2010] = -51.353249951959683 + lats[2011] = -51.423548718289396 + lats[2012] = -51.493847484608516 + lats[2013] = -51.56414625091697 + lats[2014] = -51.634445017214695 + lats[2015] = -51.704743783501634 + lats[2016] = -51.775042549777737 + lats[2017] = -51.845341316042933 + lats[2018] = -51.915640082297152 + lats[2019] = -51.985938848540336 + lats[2020] = -52.056237614772435 + lats[2021] = -52.126536380993372 + lats[2022] = -52.196835147203096 + lats[2023] = -52.26713391340153 + lats[2024] = -52.337432679588609 + lats[2025] = -52.407731445764284 + lats[2026] = -52.478030211928477 + lats[2027] = -52.548328978081123 + lats[2028] = -52.618627744222159 + lats[2029] = -52.688926510351514 + lats[2030] = -52.759225276469131 + lats[2031] = -52.829524042574917 + lats[2032] = -52.899822808668837 + lats[2033] = -52.970121574750792 + lats[2034] = -53.040420340820731 + lats[2035] = -53.110719106878584 + lats[2036] = -53.181017872924265 + lats[2037] = -53.251316638957725 + lats[2038] = -53.321615404978871 + lats[2039] = -53.391914170987633 + lats[2040] = -53.462212936983953 + lats[2041] = -53.53251170296776 + lats[2042] = -53.602810468938962 + lats[2043] = -53.673109234897495 + lats[2044] = -53.743408000843282 + lats[2045] = -53.813706766776235 + lats[2046] = -53.884005532696307 + lats[2047] = -53.954304298603383 + lats[2048] = -54.024603064497434 + lats[2049] = -54.094901830378333 + lats[2050] = -54.165200596246031 + lats[2051] = -54.235499362100448 + lats[2052] = -54.305798127941479 + lats[2053] = -54.376096893769081 + lats[2054] = -54.446395659583146 + lats[2055] = -54.516694425383605 + lats[2056] = -54.586993191170357 + lats[2057] = -54.657291956943347 + lats[2058] = -54.727590722702473 + lats[2059] = -54.797889488447652 + lats[2060] = -54.868188254178797 + lats[2061] = -54.938487019895831 + lats[2062] = -55.008785785598668 + lats[2063] = -55.07908455128721 + lats[2064] = -55.149383316961377 + lats[2065] = -55.219682082621084 + lats[2066] = -55.289980848266232 + lats[2067] = -55.360279613896743 + lats[2068] = -55.430578379512511 + lats[2069] = -55.500877145113449 + lats[2070] = -55.571175910699488 + lats[2071] = -55.641474676270505 + lats[2072] = -55.711773441826416 + lats[2073] = -55.782072207367136 + lats[2074] = -55.852370972892551 + lats[2075] = -55.922669738402583 + lats[2076] = -55.992968503897131 + lats[2077] = -56.063267269376091 + lats[2078] = -56.133566034839362 + lats[2079] = -56.203864800286865 + lats[2080] = -56.274163565718467 + lats[2081] = -56.34446233113411 + lats[2082] = -56.41476109653366 + lats[2083] = -56.485059861917016 + lats[2084] = -56.555358627284086 + lats[2085] = -56.625657392634771 + lats[2086] = -56.695956157968951 + lats[2087] = -56.766254923286517 + lats[2088] = -56.836553688587379 + lats[2089] = -56.90685245387143 + lats[2090] = -56.977151219138541 + lats[2091] = -57.047449984388614 + lats[2092] = -57.117748749621541 + lats[2093] = -57.188047514837208 + lats[2094] = -57.258346280035504 + lats[2095] = -57.328645045216312 + lats[2096] = -57.398943810379521 + lats[2097] = -57.469242575525016 + lats[2098] = -57.539541340652676 + lats[2099] = -57.60984010576238 + lats[2100] = -57.680138870854037 + lats[2101] = -57.75043763592749 + lats[2102] = -57.820736400982646 + lats[2103] = -57.891035166019364 + lats[2104] = -57.961333931037537 + lats[2105] = -58.031632696037022 + lats[2106] = -58.101931461017728 + lats[2107] = -58.172230225979497 + lats[2108] = -58.242528990922203 + lats[2109] = -58.312827755845746 + lats[2110] = -58.383126520749968 + lats[2111] = -58.453425285634758 + lats[2112] = -58.523724050499972 + lats[2113] = -58.594022815345468 + lats[2114] = -58.664321580171141 + lats[2115] = -58.73462034497684 + lats[2116] = -58.804919109762423 + lats[2117] = -58.875217874527763 + lats[2118] = -58.945516639272725 + lats[2119] = -59.015815403997145 + lats[2120] = -59.086114168700909 + lats[2121] = -59.156412933383855 + lats[2122] = -59.226711698045854 + lats[2123] = -59.29701046268675 + lats[2124] = -59.3673092273064 + lats[2125] = -59.43760799190467 + lats[2126] = -59.507906756481383 + lats[2127] = -59.578205521036402 + lats[2128] = -59.64850428556958 + lats[2129] = -59.718803050080759 + lats[2130] = -59.78910181456979 + lats[2131] = -59.859400579036503 + lats[2132] = -59.929699343480763 + lats[2133] = -59.999998107902378 + lats[2134] = -60.070296872301235 + lats[2135] = -60.140595636677112 + lats[2136] = -60.21089440102989 + lats[2137] = -60.28119316535939 + lats[2138] = -60.35149192966545 + lats[2139] = -60.421790693947884 + lats[2140] = -60.492089458206543 + lats[2141] = -60.562388222441243 + lats[2142] = -60.632686986651805 + lats[2143] = -60.702985750838074 + lats[2144] = -60.773284514999872 + lats[2145] = -60.843583279137007 + lats[2146] = -60.913882043249295 + lats[2147] = -60.984180807336578 + lats[2148] = -61.054479571398652 + lats[2149] = -61.124778335435344 + lats[2150] = -61.195077099446451 + lats[2151] = -61.265375863431785 + lats[2152] = -61.335674627391185 + lats[2153] = -61.405973391324409 + lats[2154] = -61.476272155231321 + lats[2155] = -61.546570919111666 + lats[2156] = -61.616869682965287 + lats[2157] = -61.687168446791986 + lats[2158] = -61.757467210591535 + lats[2159] = -61.827765974363729 + lats[2160] = -61.898064738108381 + lats[2161] = -61.968363501825259 + lats[2162] = -62.038662265514176 + lats[2163] = -62.108961029174914 + lats[2164] = -62.179259792807258 + lats[2165] = -62.249558556410982 + lats[2166] = -62.319857319985871 + lats[2167] = -62.3901560835317 + lats[2168] = -62.460454847048261 + lats[2169] = -62.530753610535321 + lats[2170] = -62.60105237399263 + lats[2171] = -62.67135113741999 + lats[2172] = -62.741649900817137 + lats[2173] = -62.811948664183866 + lats[2174] = -62.882247427519928 + lats[2175] = -62.952546190825068 + lats[2176] = -63.022844954099064 + lats[2177] = -63.093143717341647 + lats[2178] = -63.163442480552604 + lats[2179] = -63.23374124373165 + lats[2180] = -63.304040006878537 + lats[2181] = -63.374338769993031 + lats[2182] = -63.444637533074854 + lats[2183] = -63.514936296123757 + lats[2184] = -63.585235059139464 + lats[2185] = -63.655533822121711 + lats[2186] = -63.725832585070251 + lats[2187] = -63.796131347984762 + lats[2188] = -63.866430110865004 + lats[2189] = -63.93672887371072 + lats[2190] = -64.00702763652157 + lats[2191] = -64.07732639929732 + lats[2192] = -64.147625162037642 + lats[2193] = -64.21792392474228 + lats[2194] = -64.288222687410922 + lats[2195] = -64.358521450043284 + lats[2196] = -64.428820212639039 + lats[2197] = -64.499118975197902 + lats[2198] = -64.569417737719576 + lats[2199] = -64.639716500203733 + lats[2200] = -64.710015262650074 + lats[2201] = -64.780314025058246 + lats[2202] = -64.850612787427963 + lats[2203] = -64.920911549758912 + lats[2204] = -64.991210312050711 + lats[2205] = -65.061509074303089 + lats[2206] = -65.131807836515677 + lats[2207] = -65.202106598688133 + lats[2208] = -65.272405360820116 + lats[2209] = -65.342704122911286 + lats[2210] = -65.413002884961315 + lats[2211] = -65.483301646969792 + lats[2212] = -65.553600408936404 + lats[2213] = -65.623899170860767 + lats[2214] = -65.694197932742526 + lats[2215] = -65.764496694581283 + lats[2216] = -65.834795456376696 + lats[2217] = -65.905094218128355 + lats[2218] = -65.975392979835888 + lats[2219] = -66.045691741498899 + lats[2220] = -66.115990503117033 + lats[2221] = -66.186289264689833 + lats[2222] = -66.256588026216932 + lats[2223] = -66.326886787697887 + lats[2224] = -66.397185549132331 + lats[2225] = -66.467484310519808 + lats[2226] = -66.537783071859891 + lats[2227] = -66.608081833152212 + lats[2228] = -66.678380594396273 + lats[2229] = -66.748679355591662 + lats[2230] = -66.818978116737924 + lats[2231] = -66.889276877834618 + lats[2232] = -66.95957563888129 + lats[2233] = -67.029874399877471 + lats[2234] = -67.100173160822706 + lats[2235] = -67.170471921716526 + lats[2236] = -67.240770682558434 + lats[2237] = -67.311069443347961 + lats[2238] = -67.381368204084609 + lats[2239] = -67.451666964767895 + lats[2240] = -67.521965725397308 + lats[2241] = -67.592264485972336 + lats[2242] = -67.662563246492482 + lats[2243] = -67.732862006957205 + lats[2244] = -67.803160767365966 + lats[2245] = -67.873459527718282 + lats[2246] = -67.943758288013555 + lats[2247] = -68.014057048251274 + lats[2248] = -68.084355808430871 + lats[2249] = -68.154654568551791 + lats[2250] = -68.224953328613438 + lats[2251] = -68.295252088615257 + lats[2252] = -68.365550848556666 + lats[2253] = -68.435849608437067 + lats[2254] = -68.506148368255865 + lats[2255] = -68.576447128012447 + lats[2256] = -68.646745887706189 + lats[2257] = -68.717044647336493 + lats[2258] = -68.787343406902693 + lats[2259] = -68.85764216640419 + lats[2260] = -68.927940925840304 + lats[2261] = -68.998239685210365 + lats[2262] = -69.068538444513763 + lats[2263] = -69.138837203749759 + lats[2264] = -69.209135962917699 + lats[2265] = -69.279434722016902 + lats[2266] = -69.349733481046613 + lats[2267] = -69.420032240006194 + lats[2268] = -69.490330998894862 + lats[2269] = -69.560629757711908 + lats[2270] = -69.630928516456592 + lats[2271] = -69.701227275128161 + lats[2272] = -69.771526033725834 + lats[2273] = -69.841824792248843 + lats[2274] = -69.912123550696421 + lats[2275] = -69.982422309067744 + lats[2276] = -70.052721067362043 + lats[2277] = -70.123019825578467 + lats[2278] = -70.193318583716191 + lats[2279] = -70.263617341774406 + lats[2280] = -70.333916099752187 + lats[2281] = -70.404214857648739 + lats[2282] = -70.474513615463138 + lats[2283] = -70.544812373194532 + lats[2284] = -70.615111130841967 + lats[2285] = -70.685409888404578 + lats[2286] = -70.755708645881384 + lats[2287] = -70.826007403271475 + lats[2288] = -70.896306160573886 + lats[2289] = -70.966604917787635 + lats[2290] = -71.036903674911756 + lats[2291] = -71.107202431945211 + lats[2292] = -71.177501188887007 + lats[2293] = -71.247799945736105 + lats[2294] = -71.318098702491469 + lats[2295] = -71.388397459152031 + lats[2296] = -71.458696215716685 + lats[2297] = -71.528994972184378 + lats[2298] = -71.599293728553988 + lats[2299] = -71.669592484824364 + lats[2300] = -71.739891240994368 + lats[2301] = -71.810189997062835 + lats[2302] = -71.880488753028587 + lats[2303] = -71.950787508890414 + lats[2304] = -72.02108626464711 + lats[2305] = -72.091385020297409 + lats[2306] = -72.161683775840089 + lats[2307] = -72.231982531273843 + lats[2308] = -72.302281286597392 + lats[2309] = -72.3725800418094 + lats[2310] = -72.442878796908545 + lats[2311] = -72.513177551893421 + lats[2312] = -72.583476306762691 + lats[2313] = -72.653775061514935 + lats[2314] = -72.724073816148703 + lats[2315] = -72.794372570662574 + lats[2316] = -72.864671325055056 + lats[2317] = -72.934970079324657 + lats[2318] = -73.005268833469799 + lats[2319] = -73.075567587489019 + lats[2320] = -73.145866341380668 + lats[2321] = -73.216165095143182 + lats[2322] = -73.2864638487749 + lats[2323] = -73.356762602274188 + lats[2324] = -73.427061355639339 + lats[2325] = -73.497360108868662 + lats[2326] = -73.567658861960396 + lats[2327] = -73.637957614912779 + lats[2328] = -73.70825636772399 + lats[2329] = -73.778555120392184 + lats[2330] = -73.848853872915541 + lats[2331] = -73.919152625292114 + lats[2332] = -73.98945137751997 + lats[2333] = -74.059750129597163 + lats[2334] = -74.13004888152166 + lats[2335] = -74.200347633291472 + lats[2336] = -74.270646384904481 + lats[2337] = -74.340945136358584 + lats[2338] = -74.411243887651622 + lats[2339] = -74.481542638781434 + lats[2340] = -74.551841389745761 + lats[2341] = -74.622140140542356 + lats[2342] = -74.692438891168877 + lats[2343] = -74.762737641622991 + lats[2344] = -74.833036391902269 + lats[2345] = -74.903335142004323 + lats[2346] = -74.973633891926625 + lats[2347] = -75.043932641666672 + lats[2348] = -75.114231391221821 + lats[2349] = -75.184530140589501 + lats[2350] = -75.254828889766983 + lats[2351] = -75.325127638751567 + lats[2352] = -75.395426387540439 + lats[2353] = -75.465725136130786 + lats[2354] = -75.536023884519707 + lats[2355] = -75.60632263270422 + lats[2356] = -75.67662138068134 + lats[2357] = -75.746920128447996 + lats[2358] = -75.81721887600105 + lats[2359] = -75.887517623337317 + lats[2360] = -75.957816370453543 + lats[2361] = -76.028115117346374 + lats[2362] = -76.098413864012443 + lats[2363] = -76.16871261044831 + lats[2364] = -76.239011356650423 + lats[2365] = -76.3093101026152 + lats[2366] = -76.379608848338933 + lats[2367] = -76.449907593817869 + lats[2368] = -76.520206339048215 + lats[2369] = -76.59050508402602 + lats[2370] = -76.660803828747362 + lats[2371] = -76.731102573208048 + lats[2372] = -76.801401317404 + lats[2373] = -76.871700061330955 + lats[2374] = -76.941998804984564 + lats[2375] = -77.012297548360323 + lats[2376] = -77.082596291453768 + lats[2377] = -77.15289503426024 + lats[2378] = -77.22319377677502 + lats[2379] = -77.293492518993247 + lats[2380] = -77.363791260909963 + lats[2381] = -77.434090002520122 + lats[2382] = -77.504388743818524 + lats[2383] = -77.574687484799924 + lats[2384] = -77.644986225458879 + lats[2385] = -77.71528496578982 + lats[2386] = -77.785583705787161 + lats[2387] = -77.855882445445019 + lats[2388] = -77.926181184757539 + lats[2389] = -77.996479923718596 + lats[2390] = -78.066778662322022 + lats[2391] = -78.137077400561424 + lats[2392] = -78.207376138430348 + lats[2393] = -78.277674875922045 + lats[2394] = -78.347973613029708 + lats[2395] = -78.418272349746417 + lats[2396] = -78.488571086064923 + lats[2397] = -78.558869821977908 + lats[2398] = -78.629168557477882 + lats[2399] = -78.699467292557102 + lats[2400] = -78.769766027207638 + lats[2401] = -78.840064761421445 + lats[2402] = -78.910363495190211 + lats[2403] = -78.980662228505423 + lats[2404] = -79.050960961358285 + lats[2405] = -79.121259693739859 + lats[2406] = -79.191558425640977 + lats[2407] = -79.261857157052191 + lats[2408] = -79.332155887963822 + lats[2409] = -79.402454618365894 + lats[2410] = -79.472753348248219 + lats[2411] = -79.543052077600308 + lats[2412] = -79.61335080641139 + lats[2413] = -79.683649534670437 + lats[2414] = -79.753948262366038 + lats[2415] = -79.824246989486554 + lats[2416] = -79.894545716019948 + lats[2417] = -79.9648444419539 + lats[2418] = -80.035143167275749 + lats[2419] = -80.105441891972376 + lats[2420] = -80.175740616030438 + lats[2421] = -80.246039339436052 + lats[2422] = -80.316338062175078 + lats[2423] = -80.386636784232863 + lats[2424] = -80.456935505594302 + lats[2425] = -80.527234226243991 + lats[2426] = -80.59753294616587 + lats[2427] = -80.667831665343556 + lats[2428] = -80.73813038376008 + lats[2429] = -80.808429101397948 + lats[2430] = -80.878727818239184 + lats[2431] = -80.949026534265244 + lats[2432] = -81.019325249456955 + lats[2433] = -81.089623963794551 + lats[2434] = -81.159922677257711 + lats[2435] = -81.230221389825374 + lats[2436] = -81.300520101475826 + lats[2437] = -81.370818812186627 + lats[2438] = -81.441117521934686 + lats[2439] = -81.511416230696042 + lats[2440] = -81.581714938445955 + lats[2441] = -81.652013645158945 + lats[2442] = -81.722312350808508 + lats[2443] = -81.792611055367345 + lats[2444] = -81.862909758807191 + lats[2445] = -81.933208461098829 + lats[2446] = -82.003507162211946 + lats[2447] = -82.073805862115165 + lats[2448] = -82.144104560776 + lats[2449] = -82.214403258160871 + lats[2450] = -82.284701954234833 + lats[2451] = -82.355000648961692 + lats[2452] = -82.425299342304029 + lats[2453] = -82.495598034222837 + lats[2454] = -82.56589672467787 + lats[2455] = -82.63619541362705 + lats[2456] = -82.706494101026948 + lats[2457] = -82.77679278683226 + lats[2458] = -82.84709147099602 + lats[2459] = -82.917390153469313 + lats[2460] = -82.987688834201322 + lats[2461] = -83.057987513139125 + lats[2462] = -83.128286190227698 + lats[2463] = -83.198584865409657 + lats[2464] = -83.268883538625232 + lats[2465] = -83.339182209812321 + lats[2466] = -83.409480878905782 + lats[2467] = -83.479779545838113 + lats[2468] = -83.550078210538487 + lats[2469] = -83.620376872933264 + lats[2470] = -83.690675532945292 + lats[2471] = -83.760974190494011 + lats[2472] = -83.831272845495249 + lats[2473] = -83.901571497860914 + lats[2474] = -83.971870147498763 + lats[2475] = -84.042168794312317 + lats[2476] = -84.112467438200326 + lats[2477] = -84.18276607905679 + lats[2478] = -84.253064716770425 + lats[2479] = -84.323363351224444 + lats[2480] = -84.393661982296322 + lats[2481] = -84.463960609857125 + lats[2482] = -84.534259233771479 + lats[2483] = -84.604557853896708 + lats[2484] = -84.674856470082915 + lats[2485] = -84.745155082171991 + lats[2486] = -84.81545368999717 + lats[2487] = -84.885752293382765 + lats[2488] = -84.95605089214304 + lats[2489] = -85.026349486081983 + lats[2490] = -85.09664807499216 + lats[2491] = -85.16694665865414 + lats[2492] = -85.237245236835548 + lats[2493] = -85.307543809290152 + lats[2494] = -85.377842375756586 + lats[2495] = -85.448140935957483 + lats[2496] = -85.518439489597966 + lats[2497] = -85.588738036364362 + lats[2498] = -85.659036575922883 + lats[2499] = -85.729335107917464 + lats[2500] = -85.799633631968391 + lats[2501] = -85.869932147670127 + lats[2502] = -85.940230654588888 + lats[2503] = -86.010529152260403 + lats[2504] = -86.080827640187209 + lats[2505] = -86.151126117835304 + lats[2506] = -86.221424584631109 + lats[2507] = -86.291723039957418 + lats[2508] = -86.362021483149363 + lats[2509] = -86.432319913489792 + lats[2510] = -86.502618330203831 + lats[2511] = -86.572916732453024 + lats[2512] = -86.643215119328573 + lats[2513] = -86.713513489844246 + lats[2514] = -86.783811842927179 + lats[2515] = -86.854110177408927 + lats[2516] = -86.924408492014166 + lats[2517] = -86.994706785348129 + lats[2518] = -87.065005055882821 + lats[2519] = -87.135303301939786 + lats[2520] = -87.205601521672108 + lats[2521] = -87.275899713041966 + lats[2522] = -87.346197873795816 + lats[2523] = -87.416496001434894 + lats[2524] = -87.486794093180748 + lats[2525] = -87.557092145935584 + lats[2526] = -87.627390156234085 + lats[2527] = -87.697688120188062 + lats[2528] = -87.767986033419561 + lats[2529] = -87.838283890981543 + lats[2530] = -87.908581687261687 + lats[2531] = -87.978879415867283 + lats[2532] = -88.049177069484486 + lats[2533] = -88.119474639706425 + lats[2534] = -88.189772116820762 + lats[2535] = -88.26006948954614 + lats[2536] = -88.330366744702559 + lats[2537] = -88.40066386679355 + lats[2538] = -88.470960837474877 + lats[2539] = -88.541257634868515 + lats[2540] = -88.611554232668382 + lats[2541] = -88.681850598961759 + lats[2542] = -88.752146694650691 + lats[2543] = -88.822442471310097 + lats[2544] = -88.892737868230952 + lats[2545] = -88.96303280826325 + lats[2546] = -89.033327191845927 + lats[2547] = -89.103620888238879 + lats[2548] = -89.173913722284126 + lats[2549] = -89.24420545380525 + lats[2550] = -89.314495744374256 + lats[2551] = -89.3847841013921 + lats[2552] = -89.45506977912261 + lats[2553] = -89.525351592371393 + lats[2554] = -89.595627537554492 + lats[2555] = -89.6658939412157 + lats[2556] = -89.736143271609578 + lats[2557] = -89.806357319542244 + lats[2558] = -89.876478353332288 + lats[2559] = -89.946187715665616 + return lats + + def first_axis_vals(self): + if self._resolution == 1280: + return self.get_precomputed_values_N1280() + else: + precision = 1.0e-14 + nval = self._resolution * 2 + rad2deg = 180 / math.pi + convval = 1 - ((2 / math.pi) * (2 / math.pi)) * 0.25 + vals = self.gauss_first_guess() + new_vals = [0] * nval + denom = math.sqrt(((nval + 0.5) * (nval + 0.5)) + convval) + for jval in range(self._resolution): + root = math.cos(vals[jval] / denom) + conv = 1 + while abs(conv) >= precision: + mem2 = 1 + mem1 = root + for legi in range(nval): + legfonc = ((2.0 * (legi + 1) - 1.0) * root * mem1 - legi * mem2) / (legi + 1) + mem2 = mem1 + mem1 = legfonc + conv = legfonc / ((nval * (mem2 - root * legfonc)) / (1.0 - (root * root))) + root = root - conv + # add maybe a max iter here to make sure we converge at some point + new_vals[jval] = math.asin(root) * rad2deg + new_vals[nval - 1 - jval] = -new_vals[jval] + return new_vals + + def map_first_axis(self, lower, upper): + axis_lines = self._first_axis_vals + end_idx = bisect_left_cmp(axis_lines, lower, cmp=lambda x, y: x > y) + 1 + start_idx = bisect_right_cmp(axis_lines, upper, cmp=lambda x, y: x > y) + return_vals = axis_lines[start_idx:end_idx] + return return_vals + + def second_axis_vals(self, first_val): + first_axis_vals = self._first_axis_vals + tol = 1e-10 + first_idx = bisect_left_cmp(first_axis_vals, first_val - tol, cmp=lambda x, y: x > y) + if first_idx >= self._resolution: + first_idx = (2 * self._resolution) - 1 - first_idx + first_idx = first_idx + 1 + npoints = 4 * first_idx + 16 + second_axis_spacing = 360 / npoints + second_axis_vals = [i * second_axis_spacing for i in range(npoints)] + return second_axis_vals + + def second_axis_spacing(self, first_val): + first_axis_vals = self._first_axis_vals + tol = 1e-10 + _first_idx = bisect_left_cmp(first_axis_vals, first_val - tol, cmp=lambda x, y: x > y) + first_idx = _first_idx + if first_idx >= self._resolution: + first_idx = (2 * self._resolution) - 1 - first_idx + first_idx = first_idx + 1 + npoints = 4 * first_idx + 16 + second_axis_spacing = 360 / npoints + return (second_axis_spacing, _first_idx + 1) + + def map_second_axis(self, first_val, lower, upper): + second_axis_spacing, first_idx = self.second_axis_spacing(first_val) + start_idx = int(lower / second_axis_spacing) + end_idx = int(upper / second_axis_spacing) + 1 + return_vals = [i * second_axis_spacing for i in range(start_idx, end_idx)] + return return_vals + + def axes_idx_to_octahedral_idx(self, first_idx, second_idx): + # NOTE: for now this takes ~2e-4s per point, so taking significant time -> for 20k points, takes 4s + # Would it be better to store a dictionary of first_idx with cumulative number of points on that idx? + # Because this is what we are doing here, but we are calculating for each point... + # But then this would only work for special grid resolutions, so need to do like a O1280 version of this + + # NOTE: OR somehow cache this for a given first_idx and then only modify the axis idx for second_idx when the + # first_idx changes + octa_idx = self._first_idx_map[first_idx - 1] + second_idx + return octa_idx + + def create_first_idx_map(self): + first_idx_list = {} + idx = 0 + for i in range(2 * self._resolution): + first_idx_list[i] = idx + if i <= self._resolution - 1: + idx += 20 + 4 * i + else: + i = i - self._resolution + 1 + if i == 1: + idx += 16 + 4 * self._resolution + else: + i = i - 1 + idx += 16 + 4 * (self._resolution - i) + return first_idx_list + + def find_second_axis_idx(self, first_val, second_val): + (second_axis_spacing, first_idx) = self.second_axis_spacing(first_val) + tol = 1e-8 + if second_val / second_axis_spacing > int(second_val / second_axis_spacing) + 1 - tol: + second_idx = int(second_val / second_axis_spacing) + 1 + else: + second_idx = int(second_val / second_axis_spacing) + return (first_idx, second_idx) + + def unmap(self, first_val, second_val): + (first_idx, second_idx) = self.find_second_axis_idx(first_val, second_val) + octahedral_index = self.axes_idx_to_octahedral_idx(first_idx, second_idx) + return octahedral_index diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_types/reduced_ll.py b/polytope/datacube/transformations/datacube_mappers/mapper_types/reduced_ll.py new file mode 100644 index 00000000..ece09b4a --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_types/reduced_ll.py @@ -0,0 +1,1505 @@ +import bisect + +from ..datacube_mappers import DatacubeMapper + + +class ReducedLatLonMapper(DatacubeMapper): + def __init__(self, base_axis, mapped_axes, resolution, local_area=[]): + # TODO: if local area is not empty list, raise NotImplemented + self._mapped_axes = mapped_axes + self._base_axis = base_axis + self._resolution = resolution + self._axis_reversed = {mapped_axes[0]: False, mapped_axes[1]: False} + self._first_axis_vals = self.first_axis_vals() + + def first_axis_vals(self): + resolution = 180 / (self._resolution - 1) + vals = [-90 + i * resolution for i in range(self._resolution)] + return vals + + def map_first_axis(self, lower, upper): + axis_lines = self._first_axis_vals + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def lon_spacing(self): + if self._resolution == 1441: + return [ + 2, + 6, + 14, + 20, + 26, + 32, + 38, + 44, + 50, + 58, + 64, + 70, + 76, + 82, + 88, + 94, + 102, + 108, + 114, + 120, + 126, + 132, + 138, + 144, + 152, + 158, + 164, + 170, + 176, + 182, + 188, + 196, + 202, + 208, + 214, + 220, + 226, + 232, + 238, + 246, + 252, + 258, + 264, + 270, + 276, + 282, + 290, + 296, + 302, + 308, + 314, + 320, + 326, + 332, + 340, + 346, + 352, + 358, + 364, + 370, + 376, + 382, + 388, + 396, + 402, + 408, + 414, + 420, + 426, + 432, + 438, + 444, + 452, + 458, + 464, + 470, + 476, + 482, + 488, + 494, + 500, + 506, + 512, + 520, + 526, + 532, + 538, + 544, + 550, + 556, + 562, + 568, + 574, + 580, + 586, + 594, + 600, + 606, + 612, + 618, + 624, + 630, + 636, + 642, + 648, + 654, + 660, + 666, + 672, + 678, + 686, + 692, + 698, + 704, + 710, + 716, + 722, + 728, + 734, + 740, + 746, + 752, + 758, + 764, + 770, + 776, + 782, + 788, + 794, + 800, + 806, + 812, + 818, + 824, + 830, + 836, + 842, + 848, + 854, + 860, + 866, + 872, + 878, + 884, + 890, + 896, + 902, + 908, + 914, + 920, + 926, + 932, + 938, + 944, + 950, + 956, + 962, + 968, + 974, + 980, + 986, + 992, + 998, + 1004, + 1010, + 1014, + 1020, + 1026, + 1032, + 1038, + 1044, + 1050, + 1056, + 1062, + 1068, + 1074, + 1080, + 1086, + 1092, + 1096, + 1102, + 1108, + 1114, + 1120, + 1126, + 1132, + 1138, + 1144, + 1148, + 1154, + 1160, + 1166, + 1172, + 1178, + 1184, + 1190, + 1194, + 1200, + 1206, + 1212, + 1218, + 1224, + 1230, + 1234, + 1240, + 1246, + 1252, + 1258, + 1264, + 1268, + 1274, + 1280, + 1286, + 1292, + 1296, + 1302, + 1308, + 1314, + 1320, + 1324, + 1330, + 1336, + 1342, + 1348, + 1352, + 1358, + 1364, + 1370, + 1374, + 1380, + 1386, + 1392, + 1396, + 1402, + 1408, + 1414, + 1418, + 1424, + 1430, + 1436, + 1440, + 1446, + 1452, + 1456, + 1462, + 1468, + 1474, + 1478, + 1484, + 1490, + 1494, + 1500, + 1506, + 1510, + 1516, + 1522, + 1526, + 1532, + 1538, + 1542, + 1548, + 1554, + 1558, + 1564, + 1570, + 1574, + 1580, + 1584, + 1590, + 1596, + 1600, + 1606, + 1610, + 1616, + 1622, + 1626, + 1632, + 1636, + 1642, + 1648, + 1652, + 1658, + 1662, + 1668, + 1672, + 1678, + 1684, + 1688, + 1694, + 1698, + 1704, + 1708, + 1714, + 1718, + 1724, + 1728, + 1734, + 1738, + 1744, + 1748, + 1754, + 1758, + 1764, + 1768, + 1774, + 1778, + 1784, + 1788, + 1794, + 1798, + 1804, + 1808, + 1812, + 1818, + 1822, + 1828, + 1832, + 1838, + 1842, + 1846, + 1852, + 1856, + 1862, + 1866, + 1870, + 1876, + 1880, + 1886, + 1890, + 1894, + 1900, + 1904, + 1908, + 1914, + 1918, + 1922, + 1928, + 1932, + 1936, + 1942, + 1946, + 1950, + 1956, + 1960, + 1964, + 1970, + 1974, + 1978, + 1982, + 1988, + 1992, + 1996, + 2002, + 2006, + 2010, + 2014, + 2020, + 2024, + 2028, + 2032, + 2036, + 2042, + 2046, + 2050, + 2054, + 2060, + 2064, + 2068, + 2072, + 2076, + 2080, + 2086, + 2090, + 2094, + 2098, + 2102, + 2106, + 2112, + 2116, + 2120, + 2124, + 2128, + 2132, + 2136, + 2140, + 2144, + 2150, + 2154, + 2158, + 2162, + 2166, + 2170, + 2174, + 2178, + 2182, + 2186, + 2190, + 2194, + 2198, + 2202, + 2206, + 2210, + 2214, + 2218, + 2222, + 2226, + 2230, + 2234, + 2238, + 2242, + 2246, + 2250, + 2254, + 2258, + 2262, + 2266, + 2270, + 2274, + 2278, + 2282, + 2286, + 2290, + 2292, + 2296, + 2300, + 2304, + 2308, + 2312, + 2316, + 2320, + 2324, + 2326, + 2330, + 2334, + 2338, + 2342, + 2346, + 2348, + 2352, + 2356, + 2360, + 2364, + 2366, + 2370, + 2374, + 2378, + 2382, + 2384, + 2388, + 2392, + 2396, + 2398, + 2402, + 2406, + 2410, + 2412, + 2416, + 2420, + 2422, + 2426, + 2430, + 2432, + 2436, + 2440, + 2442, + 2446, + 2450, + 2452, + 2456, + 2460, + 2462, + 2466, + 2470, + 2472, + 2476, + 2478, + 2482, + 2486, + 2488, + 2492, + 2494, + 2498, + 2500, + 2504, + 2508, + 2510, + 2514, + 2516, + 2520, + 2522, + 2526, + 2528, + 2532, + 2534, + 2538, + 2540, + 2544, + 2546, + 2550, + 2552, + 2556, + 2558, + 2560, + 2564, + 2566, + 2570, + 2572, + 2576, + 2578, + 2580, + 2584, + 2586, + 2590, + 2592, + 2594, + 2598, + 2600, + 2602, + 2606, + 2608, + 2610, + 2614, + 2616, + 2618, + 2622, + 2624, + 2626, + 2628, + 2632, + 2634, + 2636, + 2640, + 2642, + 2644, + 2646, + 2650, + 2652, + 2654, + 2656, + 2658, + 2662, + 2664, + 2666, + 2668, + 2670, + 2674, + 2676, + 2678, + 2680, + 2682, + 2684, + 2686, + 2690, + 2692, + 2694, + 2696, + 2698, + 2700, + 2702, + 2704, + 2706, + 2708, + 2712, + 2714, + 2716, + 2718, + 2720, + 2722, + 2724, + 2726, + 2728, + 2730, + 2732, + 2734, + 2736, + 2738, + 2740, + 2742, + 2744, + 2746, + 2748, + 2750, + 2750, + 2752, + 2754, + 2756, + 2758, + 2760, + 2762, + 2764, + 2766, + 2768, + 2768, + 2770, + 2772, + 2774, + 2776, + 2778, + 2780, + 2780, + 2782, + 2784, + 2786, + 2788, + 2788, + 2790, + 2792, + 2794, + 2794, + 2796, + 2798, + 2800, + 2800, + 2802, + 2804, + 2806, + 2806, + 2808, + 2810, + 2810, + 2812, + 2814, + 2814, + 2816, + 2818, + 2818, + 2820, + 2822, + 2822, + 2824, + 2826, + 2826, + 2828, + 2828, + 2830, + 2832, + 2832, + 2834, + 2834, + 2836, + 2836, + 2838, + 2838, + 2840, + 2842, + 2842, + 2844, + 2844, + 2846, + 2846, + 2846, + 2848, + 2848, + 2850, + 2850, + 2852, + 2852, + 2854, + 2854, + 2856, + 2856, + 2856, + 2858, + 2858, + 2860, + 2860, + 2860, + 2862, + 2862, + 2862, + 2864, + 2864, + 2864, + 2866, + 2866, + 2866, + 2868, + 2868, + 2868, + 2868, + 2870, + 2870, + 2870, + 2872, + 2872, + 2872, + 2872, + 2874, + 2874, + 2874, + 2874, + 2874, + 2876, + 2876, + 2876, + 2876, + 2876, + 2876, + 2878, + 2878, + 2878, + 2878, + 2878, + 2878, + 2878, + 2878, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2880, + 2878, + 2878, + 2878, + 2878, + 2878, + 2878, + 2878, + 2878, + 2876, + 2876, + 2876, + 2876, + 2876, + 2876, + 2874, + 2874, + 2874, + 2874, + 2874, + 2872, + 2872, + 2872, + 2872, + 2870, + 2870, + 2870, + 2868, + 2868, + 2868, + 2868, + 2866, + 2866, + 2866, + 2864, + 2864, + 2864, + 2862, + 2862, + 2862, + 2860, + 2860, + 2860, + 2858, + 2858, + 2856, + 2856, + 2856, + 2854, + 2854, + 2852, + 2852, + 2850, + 2850, + 2848, + 2848, + 2846, + 2846, + 2846, + 2844, + 2844, + 2842, + 2842, + 2840, + 2838, + 2838, + 2836, + 2836, + 2834, + 2834, + 2832, + 2832, + 2830, + 2828, + 2828, + 2826, + 2826, + 2824, + 2822, + 2822, + 2820, + 2818, + 2818, + 2816, + 2814, + 2814, + 2812, + 2810, + 2810, + 2808, + 2806, + 2806, + 2804, + 2802, + 2800, + 2800, + 2798, + 2796, + 2794, + 2794, + 2792, + 2790, + 2788, + 2788, + 2786, + 2784, + 2782, + 2780, + 2780, + 2778, + 2776, + 2774, + 2772, + 2770, + 2768, + 2768, + 2766, + 2764, + 2762, + 2760, + 2758, + 2756, + 2754, + 2752, + 2750, + 2750, + 2748, + 2746, + 2744, + 2742, + 2740, + 2738, + 2736, + 2734, + 2732, + 2730, + 2728, + 2726, + 2724, + 2722, + 2720, + 2718, + 2716, + 2714, + 2712, + 2708, + 2706, + 2704, + 2702, + 2700, + 2698, + 2696, + 2694, + 2692, + 2690, + 2686, + 2684, + 2682, + 2680, + 2678, + 2676, + 2674, + 2670, + 2668, + 2666, + 2664, + 2662, + 2658, + 2656, + 2654, + 2652, + 2650, + 2646, + 2644, + 2642, + 2640, + 2636, + 2634, + 2632, + 2628, + 2626, + 2624, + 2622, + 2618, + 2616, + 2614, + 2610, + 2608, + 2606, + 2602, + 2600, + 2598, + 2594, + 2592, + 2590, + 2586, + 2584, + 2580, + 2578, + 2576, + 2572, + 2570, + 2566, + 2564, + 2560, + 2558, + 2556, + 2552, + 2550, + 2546, + 2544, + 2540, + 2538, + 2534, + 2532, + 2528, + 2526, + 2522, + 2520, + 2516, + 2514, + 2510, + 2508, + 2504, + 2500, + 2498, + 2494, + 2492, + 2488, + 2486, + 2482, + 2478, + 2476, + 2472, + 2470, + 2466, + 2462, + 2460, + 2456, + 2452, + 2450, + 2446, + 2442, + 2440, + 2436, + 2432, + 2430, + 2426, + 2422, + 2420, + 2416, + 2412, + 2410, + 2406, + 2402, + 2398, + 2396, + 2392, + 2388, + 2384, + 2382, + 2378, + 2374, + 2370, + 2366, + 2364, + 2360, + 2356, + 2352, + 2348, + 2346, + 2342, + 2338, + 2334, + 2330, + 2326, + 2324, + 2320, + 2316, + 2312, + 2308, + 2304, + 2300, + 2296, + 2292, + 2290, + 2286, + 2282, + 2278, + 2274, + 2270, + 2266, + 2262, + 2258, + 2254, + 2250, + 2246, + 2242, + 2238, + 2234, + 2230, + 2226, + 2222, + 2218, + 2214, + 2210, + 2206, + 2202, + 2198, + 2194, + 2190, + 2186, + 2182, + 2178, + 2174, + 2170, + 2166, + 2162, + 2158, + 2154, + 2150, + 2144, + 2140, + 2136, + 2132, + 2128, + 2124, + 2120, + 2116, + 2112, + 2106, + 2102, + 2098, + 2094, + 2090, + 2086, + 2080, + 2076, + 2072, + 2068, + 2064, + 2060, + 2054, + 2050, + 2046, + 2042, + 2036, + 2032, + 2028, + 2024, + 2020, + 2014, + 2010, + 2006, + 2002, + 1996, + 1992, + 1988, + 1982, + 1978, + 1974, + 1970, + 1964, + 1960, + 1956, + 1950, + 1946, + 1942, + 1936, + 1932, + 1928, + 1922, + 1918, + 1914, + 1908, + 1904, + 1900, + 1894, + 1890, + 1886, + 1880, + 1876, + 1870, + 1866, + 1862, + 1856, + 1852, + 1846, + 1842, + 1838, + 1832, + 1828, + 1822, + 1818, + 1812, + 1808, + 1804, + 1798, + 1794, + 1788, + 1784, + 1778, + 1774, + 1768, + 1764, + 1758, + 1754, + 1748, + 1744, + 1738, + 1734, + 1728, + 1724, + 1718, + 1714, + 1708, + 1704, + 1698, + 1694, + 1688, + 1684, + 1678, + 1672, + 1668, + 1662, + 1658, + 1652, + 1648, + 1642, + 1636, + 1632, + 1626, + 1622, + 1616, + 1610, + 1606, + 1600, + 1596, + 1590, + 1584, + 1580, + 1574, + 1570, + 1564, + 1558, + 1554, + 1548, + 1542, + 1538, + 1532, + 1526, + 1522, + 1516, + 1510, + 1506, + 1500, + 1494, + 1490, + 1484, + 1478, + 1474, + 1468, + 1462, + 1456, + 1452, + 1446, + 1440, + 1436, + 1430, + 1424, + 1418, + 1414, + 1408, + 1402, + 1396, + 1392, + 1386, + 1380, + 1374, + 1370, + 1364, + 1358, + 1352, + 1348, + 1342, + 1336, + 1330, + 1324, + 1320, + 1314, + 1308, + 1302, + 1296, + 1292, + 1286, + 1280, + 1274, + 1268, + 1264, + 1258, + 1252, + 1246, + 1240, + 1234, + 1230, + 1224, + 1218, + 1212, + 1206, + 1200, + 1194, + 1190, + 1184, + 1178, + 1172, + 1166, + 1160, + 1154, + 1148, + 1144, + 1138, + 1132, + 1126, + 1120, + 1114, + 1108, + 1102, + 1096, + 1092, + 1086, + 1080, + 1074, + 1068, + 1062, + 1056, + 1050, + 1044, + 1038, + 1032, + 1026, + 1020, + 1014, + 1010, + 1004, + 998, + 992, + 986, + 980, + 974, + 968, + 962, + 956, + 950, + 944, + 938, + 932, + 926, + 920, + 914, + 908, + 902, + 896, + 890, + 884, + 878, + 872, + 866, + 860, + 854, + 848, + 842, + 836, + 830, + 824, + 818, + 812, + 806, + 800, + 794, + 788, + 782, + 776, + 770, + 764, + 758, + 752, + 746, + 740, + 734, + 728, + 722, + 716, + 710, + 704, + 698, + 692, + 686, + 678, + 672, + 666, + 660, + 654, + 648, + 642, + 636, + 630, + 624, + 618, + 612, + 606, + 600, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + + def second_axis_vals(self, first_val): + first_idx = self._first_axis_vals.index(first_val) + Ny = self.lon_spacing()[first_idx] + second_spacing = 360 / Ny + return [i * second_spacing for i in range(Ny)] + + def map_second_axis(self, first_val, lower, upper): + axis_lines = self.second_axis_vals(first_val) + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def axes_idx_to_reduced_ll_idx(self, first_idx, second_idx): + Ny_array = self.lon_spacing() + idx = 0 + for i in range(self._resolution): + if i != first_idx: + idx += Ny_array[i] + else: + idx += second_idx + return idx + + def find_second_idx(self, first_val, second_val): + tol = 1e-10 + second_axis_vals = self.second_axis_vals(first_val) + second_idx = bisect.bisect_left(second_axis_vals, second_val - tol) + return second_idx + + def unmap(self, first_val, second_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + second_val = [i for i in self.second_axis_vals(first_val) if second_val - tol <= i <= second_val + tol][0] + second_idx = self.second_axis_vals(first_val).index(second_val) + reduced_ll_index = self.axes_idx_to_reduced_ll_idx(first_idx, second_idx) + return reduced_ll_index diff --git a/polytope/datacube/transformations/datacube_mappers/mapper_types/regular.py b/polytope/datacube/transformations/datacube_mappers/mapper_types/regular.py new file mode 100644 index 00000000..c8f207fc --- /dev/null +++ b/polytope/datacube/transformations/datacube_mappers/mapper_types/regular.py @@ -0,0 +1,57 @@ +import bisect + +from ..datacube_mappers import DatacubeMapper + + +class RegularGridMapper(DatacubeMapper): + def __init__(self, base_axis, mapped_axes, resolution, local_area=[]): + # TODO: if local area is not empty list, raise NotImplemented + self._mapped_axes = mapped_axes + self._base_axis = base_axis + self._resolution = resolution + self.deg_increment = 90 / self._resolution + self._axis_reversed = {mapped_axes[0]: True, mapped_axes[1]: False} + self._first_axis_vals = self.first_axis_vals() + + def first_axis_vals(self): + first_ax_vals = [90 - i * self.deg_increment for i in range(2 * self._resolution)] + return first_ax_vals + + def map_first_axis(self, lower, upper): + axis_lines = self._first_axis_vals + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def second_axis_vals(self, first_val): + second_ax_vals = [i * self.deg_increment for i in range(4 * self._resolution)] + return second_ax_vals + + def map_second_axis(self, first_val, lower, upper): + axis_lines = self.second_axis_vals(first_val) + return_vals = [val for val in axis_lines if lower <= val <= upper] + return return_vals + + def axes_idx_to_regular_idx(self, first_idx, second_idx): + final_idx = first_idx * 4 * self._resolution + second_idx + return final_idx + + def find_second_idx(self, first_val, second_val): + tol = 1e-10 + second_axis_vals = self.second_axis_vals(first_val) + second_idx = bisect.bisect_left(second_axis_vals, second_val - tol) + return second_idx + + def unmap_first_val_to_start_line_idx(self, first_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + return first_idx * 4 * self._resolution + + def unmap(self, first_val, second_val): + tol = 1e-8 + first_val = [i for i in self._first_axis_vals if first_val - tol <= i <= first_val + tol][0] + first_idx = self._first_axis_vals.index(first_val) + second_val = [i for i in self.second_axis_vals(first_val) if second_val - tol <= i <= second_val + tol][0] + second_idx = self.second_axis_vals(first_val).index(second_val) + final_index = self.axes_idx_to_regular_idx(first_idx, second_idx) + return final_index diff --git a/polytope/datacube/transformations/datacube_merger/__init__.py b/polytope/datacube/transformations/datacube_merger/__init__.py new file mode 100644 index 00000000..71a59acb --- /dev/null +++ b/polytope/datacube/transformations/datacube_merger/__init__.py @@ -0,0 +1,2 @@ +from .datacube_merger import * +from .merger_axis_decorator import * diff --git a/polytope/datacube/transformations/datacube_merger/datacube_merger.py b/polytope/datacube/transformations/datacube_merger/datacube_merger.py new file mode 100644 index 00000000..310036f0 --- /dev/null +++ b/polytope/datacube/transformations/datacube_merger/datacube_merger.py @@ -0,0 +1,73 @@ +import logging + +import numpy as np +import pandas as pd + +from ..datacube_transformations import DatacubeAxisTransformation + + +class DatacubeAxisMerger(DatacubeAxisTransformation): + def __init__(self, name, merge_options): + self.transformation_options = merge_options + self.name = name + self._first_axis = name + self._second_axis = merge_options["with"] + self._linkers = merge_options["linkers"] + + def blocked_axes(self): + return [self._second_axis] + + def unwanted_axes(self): + return [] + + def _mapped_axes(self): + return self._first_axis + + def merged_values(self, datacube): + first_ax_vals = datacube.ax_vals(self.name) + second_ax_name = self._second_axis + second_ax_vals = datacube.ax_vals(second_ax_name) + linkers = self._linkers + merged_values = [] + for i in range(len(first_ax_vals)): + first_val = first_ax_vals[i] + for j in range(len(second_ax_vals)): + second_val = second_ax_vals[j] + # TODO: check that the first and second val are strings + val_to_add = pd.to_datetime("".join([first_val, linkers[0], second_val, linkers[1]])) + val_to_add = val_to_add.to_numpy() + val_to_add = val_to_add.astype("datetime64[s]") + merged_values.append(val_to_add) + merged_values = np.array(merged_values) + logging.info( + f"Merged values {first_ax_vals} on axis {self.name} and \ + values {second_ax_vals} on axis {second_ax_name} to values {merged_values}" + ) + return merged_values + + def transformation_axes_final(self): + return [self._first_axis] + + def generate_final_transformation(self): + return self + + def unmerge(self, merged_val): + merged_val = str(merged_val) + first_idx = merged_val.find(self._linkers[0]) + first_val = merged_val[:first_idx] + first_linker_size = len(self._linkers[0]) + second_linked_size = len(self._linkers[1]) + second_val = merged_val[first_idx + first_linker_size : -second_linked_size] + + # TODO: maybe replacing like this is too specific to time/dates? + first_val = str(first_val).replace("-", "") + second_val = second_val.replace(":", "") + logging.info( + f"Unmerged value {merged_val} to values {first_val} on axis {self.name} \ + and {second_val} on axis {self._second_axis}" + ) + return (first_val, second_val) + + def change_val_type(self, axis_name, values): + new_values = pd.to_datetime(values) + return new_values diff --git a/polytope/datacube/transformations/datacube_merger/merger_axis_decorator.py b/polytope/datacube/transformations/datacube_merger/merger_axis_decorator.py new file mode 100644 index 00000000..16d81639 --- /dev/null +++ b/polytope/datacube/transformations/datacube_merger/merger_axis_decorator.py @@ -0,0 +1,77 @@ +import bisect + +from .datacube_merger import DatacubeAxisMerger + + +def merge(cls): + if cls.has_merger: + + def find_indexes(path, datacube): + # first, find the relevant transformation object that is a mapping in the cls.transformation dico + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisMerger): + transformation = transform + if cls.name == transformation._first_axis: + return transformation.merged_values(datacube) + + old_unmap_path_key = cls.unmap_path_key + + def unmap_path_key(key_value_path, leaf_path, unwanted_path): + key_value_path, leaf_path, unwanted_path = old_unmap_path_key(key_value_path, leaf_path, unwanted_path) + new_key_value_path = {} + value = key_value_path[cls.name] + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisMerger): + if cls.name == transform._first_axis: + (first_val, second_val) = transform.unmerge(value) + new_key_value_path[transform._first_axis] = first_val + new_key_value_path[transform._second_axis] = second_val + return (new_key_value_path, leaf_path, unwanted_path) + + old_unmap_to_datacube = cls.unmap_to_datacube + + def unmap_to_datacube(path, unmapped_path): + (path, unmapped_path) = old_unmap_to_datacube(path, unmapped_path) + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisMerger): + transformation = transform + if cls.name == transformation._first_axis: + old_val = path.get(cls.name, None) + (first_val, second_val) = transformation.unmerge(old_val) + path.pop(cls.name, None) + path[transformation._first_axis] = first_val + path[transformation._second_axis] = second_val + return (path, unmapped_path) + + def find_indices_between(index_ranges, low, up, datacube, method=None): + # TODO: add method for snappping + indexes_between_ranges = [] + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisMerger): + transformation = transform + if cls.name in transformation._mapped_axes(): + for indexes in index_ranges: + if method == "surrounding" or method == "nearest": + start = indexes.index(low) + end = indexes.index(up) + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + indexes_between = indexes[start:end] + indexes_between_ranges.append(indexes_between) + else: + lower_idx = bisect.bisect_left(indexes, low) + upper_idx = bisect.bisect_right(indexes, up) + indexes_between = indexes[lower_idx:upper_idx] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + def remap(range): + return [range] + + cls.remap = remap + cls.find_indexes = find_indexes + cls.unmap_to_datacube = unmap_to_datacube + cls.find_indices_between = find_indices_between + cls.unmap_path_key = unmap_path_key + + return cls diff --git a/polytope/datacube/transformations/datacube_null_transformation/__init__.py b/polytope/datacube/transformations/datacube_null_transformation/__init__.py new file mode 100644 index 00000000..13395c4a --- /dev/null +++ b/polytope/datacube/transformations/datacube_null_transformation/__init__.py @@ -0,0 +1,2 @@ +from .datacube_null_transformation import * +from .null_axis_decorator import * diff --git a/polytope/datacube/transformations/datacube_null_transformation/datacube_null_transformation.py b/polytope/datacube/transformations/datacube_null_transformation/datacube_null_transformation.py new file mode 100644 index 00000000..43dccbbe --- /dev/null +++ b/polytope/datacube/transformations/datacube_null_transformation/datacube_null_transformation.py @@ -0,0 +1,22 @@ +from ..datacube_transformations import DatacubeAxisTransformation + + +class DatacubeNullTransformation(DatacubeAxisTransformation): + def __init__(self, name, mapper_options): + self.name = name + self.transformation_options = mapper_options + + def generate_final_transformation(self): + return self + + def transformation_axes_final(self): + return [self.name] + + def change_val_type(self, axis_name, values): + return values + + def blocked_axes(self): + return [] + + def unwanted_axes(self): + return [] diff --git a/polytope/datacube/transformations/datacube_null_transformation/null_axis_decorator.py b/polytope/datacube/transformations/datacube_null_transformation/null_axis_decorator.py new file mode 100644 index 00000000..10e2644d --- /dev/null +++ b/polytope/datacube/transformations/datacube_null_transformation/null_axis_decorator.py @@ -0,0 +1,22 @@ +def null(cls): + if cls.type_change: + old_find_indexes = cls.find_indexes + + def find_indexes(path, datacube): + return old_find_indexes(path, datacube) + + def find_indices_between(index_ranges, low, up, datacube, method=None): + indexes_between_ranges = [] + for indexes in index_ranges: + indexes_between = [i for i in indexes if low <= i <= up] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + def remap(range): + return [range] + + cls.remap = remap + cls.find_indexes = find_indexes + cls.find_indices_between = find_indices_between + + return cls diff --git a/polytope/datacube/transformations/datacube_reverse/__init__.py b/polytope/datacube/transformations/datacube_reverse/__init__.py new file mode 100644 index 00000000..73cb9d86 --- /dev/null +++ b/polytope/datacube/transformations/datacube_reverse/__init__.py @@ -0,0 +1,2 @@ +from .datacube_reverse import * +from .reverse_axis_decorator import * diff --git a/polytope/datacube/transformations/datacube_reverse/datacube_reverse.py b/polytope/datacube/transformations/datacube_reverse/datacube_reverse.py new file mode 100644 index 00000000..6e60de87 --- /dev/null +++ b/polytope/datacube/transformations/datacube_reverse/datacube_reverse.py @@ -0,0 +1,22 @@ +from ..datacube_transformations import DatacubeAxisTransformation + + +class DatacubeAxisReverse(DatacubeAxisTransformation): + def __init__(self, name, mapper_options): + self.name = name + self.transformation_options = mapper_options + + def generate_final_transformation(self): + return self + + def transformation_axes_final(self): + return [self.name] + + def change_val_type(self, axis_name, values): + return values + + def blocked_axes(self): + return [] + + def unwanted_axes(self): + return [] diff --git a/polytope/datacube/transformations/datacube_reverse/reverse_axis_decorator.py b/polytope/datacube/transformations/datacube_reverse/reverse_axis_decorator.py new file mode 100644 index 00000000..18bc8bd6 --- /dev/null +++ b/polytope/datacube/transformations/datacube_reverse/reverse_axis_decorator.py @@ -0,0 +1,66 @@ +import bisect + +from .datacube_reverse import DatacubeAxisReverse + + +def reverse(cls): + if cls.reorder: + + def find_indexes(path, datacube): + # first, find the relevant transformation object that is a mapping in the cls.transformation dico + subarray = datacube.dataarray.sel(path, method="nearest") + unordered_indices = datacube.datacube_natural_indexes(cls, subarray) + if cls.name in datacube.complete_axes: + ordered_indices = unordered_indices.sort_values() + else: + ordered_indices = unordered_indices + return ordered_indices + + def find_indices_between(index_ranges, low, up, datacube, method=None): + # TODO: add method for snappping + indexes_between_ranges = [] + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisReverse): + transformation = transform + if cls.name == transformation.name: + for indexes in index_ranges: + if cls.name in datacube.complete_axes: + # Find the range of indexes between lower and upper + # https://pandas.pydata.org/docs/reference/api/pandas.Index.searchsorted.html + # Assumes the indexes are already sorted (could sort to be sure) and monotonically + # increasing + if method == "surrounding" or method == "nearest": + start = indexes.searchsorted(low, "left") + end = indexes.searchsorted(up, "right") + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + indexes_between = indexes[start:end].to_list() + indexes_between_ranges.append(indexes_between) + else: + start = indexes.searchsorted(low, "left") + end = indexes.searchsorted(up, "right") + indexes_between = indexes[start:end].to_list() + indexes_between_ranges.append(indexes_between) + else: + if method == "surrounding" or method == "nearest": + start = indexes.index(low) + end = indexes.index(up) + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + indexes_between = indexes[start:end] + indexes_between_ranges.append(indexes_between) + else: + lower_idx = bisect.bisect_left(indexes, low) + upper_idx = bisect.bisect_right(indexes, up) + indexes_between = indexes[lower_idx:upper_idx] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + def remap(range): + return [range] + + cls.remap = remap + cls.find_indexes = find_indexes + cls.find_indices_between = find_indices_between + + return cls diff --git a/polytope/datacube/transformations/datacube_transformations.py b/polytope/datacube/transformations/datacube_transformations.py new file mode 100644 index 00000000..2077f346 --- /dev/null +++ b/polytope/datacube/transformations/datacube_transformations.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from copy import deepcopy +from importlib import import_module + + +class DatacubeAxisTransformation(ABC): + @staticmethod + def create_transform(name, transformation_type_key, transformation_options): + transformation_type = _type_to_datacube_transformation_lookup[transformation_type_key] + transformation_file_name = _type_to_transformation_file_lookup[transformation_type_key] + file_name = ".datacube_" + transformation_file_name + module = import_module("polytope.datacube.transformations" + file_name + file_name) + constructor = getattr(module, transformation_type) + transformation_type_option = transformation_options[transformation_type_key] + new_transformation = deepcopy(constructor(name, transformation_type_option)) + + new_transformation.name = name + return new_transformation + + @staticmethod + def get_final_axes(name, transformation_type_key, transformation_options): + new_transformation = DatacubeAxisTransformation.create_transform( + name, transformation_type_key, transformation_options + ) + transformation_axis_names = new_transformation.transformation_axes_final() + return transformation_axis_names + + def name(self): + pass + + def transformation_options(self): + pass + + @abstractmethod + def generate_final_transformation(self): + pass + + @abstractmethod + def transformation_axes_final(self): + pass + + @abstractmethod + def change_val_type(self, axis_name, values): + pass + + +_type_to_datacube_transformation_lookup = { + "mapper": "DatacubeMapper", + "cyclic": "DatacubeAxisCyclic", + "merge": "DatacubeAxisMerger", + "reverse": "DatacubeAxisReverse", + "type_change": "DatacubeAxisTypeChange", + "null": "DatacubeNullTransformation", +} + +_type_to_transformation_file_lookup = { + "mapper": "mappers", + "cyclic": "cyclic", + "merge": "merger", + "reverse": "reverse", + "type_change": "type_change", + "null": "null_transformation", +} + +has_transform = { + "mapper": "has_mapper", + "cyclic": "is_cyclic", + "merge": "has_merger", + "reverse": "reorder", + "type_change": "type_change", + "null": "null", +} diff --git a/polytope/datacube/transformations/datacube_type_change/__init__.py b/polytope/datacube/transformations/datacube_type_change/__init__.py new file mode 100644 index 00000000..89dab3ae --- /dev/null +++ b/polytope/datacube/transformations/datacube_type_change/__init__.py @@ -0,0 +1,2 @@ +from .datacube_type_change import * +from .type_change_axis_decorator import * diff --git a/polytope/datacube/transformations/datacube_type_change/datacube_type_change.py b/polytope/datacube/transformations/datacube_type_change/datacube_type_change.py new file mode 100644 index 00000000..8ba2ef0f --- /dev/null +++ b/polytope/datacube/transformations/datacube_type_change/datacube_type_change.py @@ -0,0 +1,53 @@ +from copy import deepcopy +from importlib import import_module + +from ..datacube_transformations import DatacubeAxisTransformation + + +class DatacubeAxisTypeChange(DatacubeAxisTransformation): + # The transformation here will be to point the old axes to the new cyclic axes + + def __init__(self, name, type_options): + self.name = name + self.transformation_options = type_options + self.new_type = type_options + self._final_transformation = self.generate_final_transformation() + + def generate_final_transformation(self): + map_type = _type_to_datacube_type_change_lookup[self.new_type] + module = import_module("polytope.datacube.transformations.datacube_type_change.datacube_type_change") + constructor = getattr(module, map_type) + transformation = deepcopy(constructor(self.name, self.new_type)) + return transformation + + def transformation_axes_final(self): + return [self._final_transformation.axis_name] + + def change_val_type(self, axis_name, values): + return_idx = [self._final_transformation.transform_type(val) for val in values] + return_idx.sort() + return return_idx + + def make_str(self, value): + return self._final_transformation.make_str(value) + + def blocked_axes(self): + return [] + + def unwanted_axes(self): + return [] + + +class TypeChangeStrToInt(DatacubeAxisTypeChange): + def __init__(self, axis_name, new_type): + self.axis_name = axis_name + self._new_type = new_type + + def transform_type(self, value): + return int(value) + + def make_str(self, value): + return str(value) + + +_type_to_datacube_type_change_lookup = {"int": "TypeChangeStrToInt"} diff --git a/polytope/datacube/transformations/datacube_type_change/type_change_axis_decorator.py b/polytope/datacube/transformations/datacube_type_change/type_change_axis_decorator.py new file mode 100644 index 00000000..0e366982 --- /dev/null +++ b/polytope/datacube/transformations/datacube_type_change/type_change_axis_decorator.py @@ -0,0 +1,73 @@ +import bisect + +from .datacube_type_change import DatacubeAxisTypeChange + + +def type_change(cls): + if cls.type_change: + old_find_indexes = cls.find_indexes + + def find_indexes(path, datacube): + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisTypeChange): + transformation = transform + if cls.name == transformation.name: + original_vals = old_find_indexes(path, datacube) + return transformation.change_val_type(cls.name, original_vals) + + old_unmap_path_key = cls.unmap_path_key + + def unmap_path_key(key_value_path, leaf_path, unwanted_path): + key_value_path, leaf_path, unwanted_path = old_unmap_path_key(key_value_path, leaf_path, unwanted_path) + value = key_value_path[cls.name] + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisTypeChange): + if cls.name == transform.name: + unchanged_val = transform.make_str(value) + key_value_path[cls.name] = unchanged_val + return (key_value_path, leaf_path, unwanted_path) + + def unmap_to_datacube(path, unmapped_path): + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisTypeChange): + transformation = transform + if cls.name == transformation.name: + changed_val = path.get(cls.name, None) + unchanged_val = transformation.make_str(changed_val) + if cls.name in path: + path.pop(cls.name, None) + unmapped_path[cls.name] = unchanged_val + return (path, unmapped_path) + + def find_indices_between(index_ranges, low, up, datacube, method=None): + # TODO: add method for snappping + indexes_between_ranges = [] + for transform in cls.transformations: + if isinstance(transform, DatacubeAxisTypeChange): + transformation = transform + if cls.name == transformation.name: + for indexes in index_ranges: + if method == "surrounding" or method == "nearest": + start = indexes.index(low) + end = indexes.index(up) + start = max(start - 1, 0) + end = min(end + 1, len(indexes)) + indexes_between = indexes[start:end] + indexes_between_ranges.append(indexes_between) + else: + lower_idx = bisect.bisect_left(indexes, low) + upper_idx = bisect.bisect_right(indexes, up) + indexes_between = indexes[lower_idx:upper_idx] + indexes_between_ranges.append(indexes_between) + return indexes_between_ranges + + def remap(range): + return [range] + + cls.remap = remap + cls.find_indexes = find_indexes + cls.unmap_to_datacube = unmap_to_datacube + cls.find_indices_between = find_indices_between + cls.unmap_path_key = unmap_path_key + + return cls diff --git a/polytope/datacube/xarray.py b/polytope/datacube/xarray.py deleted file mode 100644 index 4791fb9a..00000000 --- a/polytope/datacube/xarray.py +++ /dev/null @@ -1,150 +0,0 @@ -import math -import sys -from copy import deepcopy - -import numpy as np -import pandas as pd -import xarray as xr - -from ..utility.combinatorics import unique, validate_axes -from .datacube import Datacube, DatacubePath, IndexTree -from .datacube_axis import ( - FloatAxis, - IntAxis, - PandasTimedeltaAxis, - PandasTimestampAxis, - UnsliceableaAxis, -) - -_mappings = { - pd.Int64Dtype: IntAxis(), - pd.Timestamp: PandasTimestampAxis(), - np.int64: IntAxis(), - np.datetime64: PandasTimestampAxis(), - np.timedelta64: PandasTimedeltaAxis(), - np.float64: FloatAxis(), - np.str_: UnsliceableaAxis(), - str: UnsliceableaAxis(), -} - - -class XArrayDatacube(Datacube): - """Xarray arrays are labelled, axes can be defined as strings or integers (e.g. "time" or 0).""" - - def _set_mapper(self, values, name): - if values.dtype.type not in _mappings: - raise ValueError(f"Could not create a mapper for index type {values.dtype.type} for axis {name}") - if name in self.options.keys(): - # The options argument here is supposed to be a nested dictionary - # like {"latitude":{"Cyclic":range}, ...} - if "Cyclic" in self.options[name].keys(): - value_type = values.dtype.type - axes_type_str = type(_mappings[value_type]).__name__ - axes_type_str += "Cyclic" - cyclic_axis_type = deepcopy(getattr(sys.modules["polytope.datacube.datacube_axis"], axes_type_str)()) - self.mappers[name] = cyclic_axis_type - self.mappers[name].name = name - self.mappers[name].range = self.options[name]["Cyclic"] - else: - self.mappers[name] = deepcopy(_mappings[values.dtype.type]) - self.mappers[name].name = name - - def __init__(self, dataarray: xr.DataArray, options={}): - self.options = options - self.mappers = {} - for name, values in dataarray.coords.variables.items(): - if name in dataarray.dims: - dataarray = dataarray.sortby(name) - self._set_mapper(values, name) - else: # drop non-necessary coordinates which we don't slice on - dataarray = dataarray.reset_coords(names=name, drop=True) - self.dataarray = dataarray - - def get(self, requests: IndexTree): - for r in requests.leaves: - path = r.flatten() - path = self.remap_path(path) - if len(path.items()) == len(self.dataarray.coords): - subxarray = self.dataarray.sel(path, method="nearest") - data_variables = subxarray.data_vars - result_tuples = [(key, value) for key, value in data_variables.items()] - r.result = dict(result_tuples) - else: - r.remove_branch() - - def get_mapper(self, axis): - return self.mappers[axis] - - def remap_path(self, path: DatacubePath): - for key in path: - value = path[key] - path[key] = self.mappers[key].remap_val_to_axis_range(value) - return path - - def _look_up_datacube(self, search_ranges, search_ranges_offset, indexes, axis): - idx_between = [] - for i in range(len(search_ranges)): - r = search_ranges[i] - offset = search_ranges_offset[i] - low = r[0] - up = r[1] - # Find the range of indexes between lower and upper - # https://pandas.pydata.org/docs/reference/api/pandas.Index.searchsorted.html - # Assumes the indexes are already sorted (could sort to be sure) and monotonically increasing - start = indexes.searchsorted(low, "left") # TODO: catch start=0 (not found)? - end = indexes.searchsorted(up, "right") # TODO: catch end=length (not found)? - indexes_between = indexes[start:end].to_list() - - # Now the indexes_between are values on the cyclic range so need to remap them to their original - # values before returning them - for j in range(len(indexes_between)): - if offset is None: - indexes_between[j] = indexes_between[j] - else: - indexes_between[j] = round(indexes_between[j] + offset, int(-math.log10(axis.tol))) - - idx_between.append(indexes_between[j]) - return idx_between - - def get_indices(self, path: DatacubePath, axis, lower, upper): - path = self.remap_path(path) - - # Open a view on the subset identified by the path - subarray = self.dataarray.sel(path, method="nearest") - - # Get the indexes of the axis we want to query - # XArray does not support branching, so no need to use label, we just take the next axis - indexes = next(iter(subarray.xindexes.values())).to_pandas_index() - - # Here, we do a cyclic remapping so we look up on the right existing values in the cyclic range on the datacube - search_ranges = axis.remap([lower, upper]) - original_search_ranges = axis.to_intervals([lower, upper]) - - # Find the offsets for each interval in the requested range, which we will need later - search_ranges_offset = [] - for r in original_search_ranges: - offset = axis.offset(r) - search_ranges_offset.append(offset) - - # Look up the values in the datacube for each cyclic interval range - idx_between = self._look_up_datacube(search_ranges, search_ranges_offset, indexes, axis) - - # Remove duplicates even if difference of the order of the axis tolerance - if offset is not None: - # Note that we can only do unique if not dealing with time values - idx_between = unique(idx_between) - - return idx_between - - def has_index(self, path: DatacubePath, axis, index): - # when we want to obtain the value of an unsliceable axis, need to check the values does exist in the datacube - subarray = self.dataarray.sel(path)[axis.name] - subarray_vals = subarray.values - return index in subarray_vals - - @property - def axes(self): - return self.mappers - - def validate(self, axes): - return validate_axes(self.axes, axes) diff --git a/polytope/engine/engine.py b/polytope/engine/engine.py index 85682e46..259aaa6d 100644 --- a/polytope/engine/engine.py +++ b/polytope/engine/engine.py @@ -1,6 +1,7 @@ from typing import List -from ..datacube.datacube import Datacube +from ..datacube.backends.datacube import Datacube +from ..datacube.index_tree import IndexTree from ..shapes import ConvexPolytope @@ -8,7 +9,7 @@ class Engine: def __init__(self): pass - def extract(self, datacube: Datacube, polytopes: List[ConvexPolytope]): + def extract(self, datacube: Datacube, polytopes: List[ConvexPolytope]) -> IndexTree: pass @staticmethod diff --git a/polytope/engine/hullslicer.py b/polytope/engine/hullslicer.py index 161ed5b5..7ad7190d 100644 --- a/polytope/engine/hullslicer.py +++ b/polytope/engine/hullslicer.py @@ -1,13 +1,14 @@ +import math from copy import copy from itertools import chain from typing import List import scipy.spatial -from ..datacube.datacube import Datacube, IndexTree -from ..datacube.datacube_axis import UnsliceableaAxis +from ..datacube.backends.datacube import Datacube, IndexTree +from ..datacube.datacube_axis import UnsliceableDatacubeAxis from ..shapes import ConvexPolytope -from ..utility.combinatorics import argmax, argmin, group, product, unique +from ..utility.combinatorics import argmax, argmin, group, tensor_product, unique from ..utility.exceptions import UnsliceableShapeError from ..utility.geometry import lerp from .engine import Engine @@ -15,24 +16,42 @@ class HullSlicer(Engine): def __init__(self): - pass + self.ax_is_unsliceable = {} + self.axis_values_between = {} + self.has_value = {} + self.sliced_polytopes = {} + self.remapped_vals = {} def _unique_continuous_points(self, p: ConvexPolytope, datacube: Datacube): - for i, ax in enumerate(p.axes()): + for i, ax in enumerate(p._axes): mapper = datacube.get_mapper(ax) - if isinstance(mapper, UnsliceableaAxis): + if self.ax_is_unsliceable.get(ax, None) is None: + self.ax_is_unsliceable[ax] = isinstance(mapper, UnsliceableDatacubeAxis) + if self.ax_is_unsliceable[ax]: break for j, val in enumerate(p.points): - p.points[j] = list(p.points[j]) p.points[j][i] = mapper.to_float(mapper.parse(p.points[j][i])) # Remove duplicate points unique(p.points) - def _build_unsliceable_child(self, polytope, ax, node, datacube, lower, next_nodes): - if polytope.axes() != [ax.name]: + def _build_unsliceable_child(self, polytope, ax, node, datacube, lower, next_nodes, slice_axis_idx): + if not polytope.is_flat: raise UnsliceableShapeError(ax) path = node.flatten() - if datacube.has_index(path, ax, lower): + + flattened_tuple = tuple() + if len(datacube.coupled_axes) > 0: + if path.get(datacube.coupled_axes[0][0], None) is not None: + flattened_tuple = (datacube.coupled_axes[0][0], path.get(datacube.coupled_axes[0][0], None)) + path = {flattened_tuple[0]: flattened_tuple[1]} + else: + path = {} + + if self.axis_values_between.get((flattened_tuple, ax.name, lower), None) is None: + self.axis_values_between[(flattened_tuple, ax.name, lower)] = datacube.has_index(path, ax, lower) + datacube_has_index = self.axis_values_between[(flattened_tuple, ax.name, lower)] + + if datacube_has_index: child = node.create_child(ax, lower) child["unsliced_polytopes"] = copy(node["unsliced_polytopes"]) child["unsliced_polytopes"].remove(polytope) @@ -41,17 +60,56 @@ def _build_unsliceable_child(self, polytope, ax, node, datacube, lower, next_nod # raise a value not found error raise ValueError() - def _build_sliceable_child(self, polytope, ax, node, datacube, lower, upper, next_nodes): + def _build_sliceable_child(self, polytope, ax, node, datacube, lower, upper, next_nodes, slice_axis_idx): tol = ax.tol lower = ax.from_float(lower - tol) upper = ax.from_float(upper + tol) flattened = node.flatten() - for value in datacube.get_indices(flattened, ax, lower, upper): + method = polytope.method + if method == "nearest": + datacube.nearest_search[ax.name] = polytope.points + + # TODO: this hashing doesn't work because we need to know the latitude val for finding longitude values + # TODO: Maybe create a coupled_axes list inside of datacube and add to it during axis formation, then here + # do something like if ax is in second place of coupled_axes, then take the flattened part of the array that + # corresponds to the first place of cooupled_axes in the hashing + # Else, if we do not need the flattened bit in the hash, can just put an empty string instead? + + flattened_tuple = tuple() + if len(datacube.coupled_axes) > 0: + if flattened.get(datacube.coupled_axes[0][0], None) is not None: + flattened_tuple = (datacube.coupled_axes[0][0], flattened.get(datacube.coupled_axes[0][0], None)) + flattened = {flattened_tuple[0]: flattened_tuple[1]} + else: + flattened = {} + + values = self.axis_values_between.get((flattened_tuple, ax.name, lower, upper, method), None) + if self.axis_values_between.get((flattened_tuple, ax.name, lower, upper, method), None) is None: + values = datacube.get_indices(flattened, ax, lower, upper, method) + self.axis_values_between[(flattened_tuple, ax.name, lower, upper, method)] = values + + if len(values) == 0: + node.remove_branch() + + for value in values: # convert to float for slicing fvalue = ax.to_float(value) - new_polytope = slice(polytope, ax.name, fvalue) + new_polytope = self.sliced_polytopes.get((polytope, ax.name, fvalue, slice_axis_idx), False) + if new_polytope is False: + new_polytope = slice(polytope, ax.name, fvalue, slice_axis_idx) + self.sliced_polytopes[(polytope, ax.name, fvalue, slice_axis_idx)] = new_polytope + # store the native type - child = node.create_child(ax, value) + remapped_val = self.remapped_vals.get((value, ax.name), None) + if remapped_val is None: + remapped_val = value + if ax.is_cyclic: + remapped_val_interm = ax.remap([value, value])[0] + remapped_val = (remapped_val_interm[0] + remapped_val_interm[1]) / 2 + remapped_val = round(remapped_val, int(-math.log10(ax.tol))) + self.remapped_vals[(value, ax.name)] = remapped_val + + child = node.create_child(ax, remapped_val) child["unsliced_polytopes"] = copy(node["unsliced_polytopes"]) child["unsliced_polytopes"].remove(polytope) if new_polytope is not None: @@ -60,13 +118,16 @@ def _build_sliceable_child(self, polytope, ax, node, datacube, lower, upper, nex def _build_branch(self, ax, node, datacube, next_nodes): for polytope in node["unsliced_polytopes"]: - if ax.name in polytope.axes(): - lower, upper = polytope.extents(ax.name) + if ax.name in polytope._axes: + lower, upper, slice_axis_idx = polytope.extents(ax.name) # here, first check if the axis is an unsliceable axis and directly build node if it is - if isinstance(ax, UnsliceableaAxis): - self._build_unsliceable_child(polytope, ax, node, datacube, lower, next_nodes) + + # NOTE: we should have already created the ax_is_unsliceable cache before + + if self.ax_is_unsliceable[ax.name]: + self._build_unsliceable_child(polytope, ax, node, datacube, lower, next_nodes, slice_axis_idx) else: - self._build_sliceable_child(polytope, ax, node, datacube, lower, upper, next_nodes) + self._build_sliceable_child(polytope, ax, node, datacube, lower, upper, next_nodes, slice_axis_idx) del node["unsliced_polytopes"] def extract(self, datacube: Datacube, polytopes: List[ConvexPolytope]): @@ -77,17 +138,50 @@ def extract(self, datacube: Datacube, polytopes: List[ConvexPolytope]): groups, input_axes = group(polytopes) datacube.validate(input_axes) request = IndexTree() - combinations = product(groups) + combinations = tensor_product(groups) + + # NOTE: could optimise here if we know combinations will always be for one request. + # Then we do not need to create a new index tree and merge it to request, but can just + # directly work on request and return it... for c in combinations: + cached_node = None + repeated_sub_nodes = [] + r = IndexTree() r["unsliced_polytopes"] = set(c) current_nodes = [r] for ax in datacube.axes.values(): next_nodes = [] for node in current_nodes: + # detect if node is for number == 1 + # store a reference to that node + # skip processing the other 49 numbers + # at the end, copy that initial reference 49 times and add to request with correct number + + stored_val = None + if node.axis.name == datacube.axis_with_identical_structure_after: + stored_val = node.value + cached_node = node + # logging.info("Caching number 1") + elif node.axis.name == datacube.axis_with_identical_structure_after and node.value != stored_val: + repeated_sub_nodes.append(node) + del node["unsliced_polytopes"] + # logging.info(f"Skipping number {node.value}") + continue + self._build_branch(ax, node, datacube, next_nodes) current_nodes = next_nodes + + # logging.info("=== BEFORE COPYING ===") + + for n in repeated_sub_nodes: + # logging.info(f"Copying children for number {n.value}") + n.copy_children_from_other(cached_node) + + # logging.info("=== AFTER COPYING ===") + # request.pprint() + request.merge(r) return request @@ -121,11 +215,8 @@ def _reduce_dimension(intersects, slice_axis_idx): return temp_intersects -def slice(polytope: ConvexPolytope, axis, value): - slice_axis_idx = polytope._axes.index(axis) - - if len(polytope.points[0]) == 1: - # Note that in this case, we do not need to do linear interpolation so we can save time +def slice(polytope: ConvexPolytope, axis, value, slice_axis_idx): + if polytope.is_flat: if value in chain(*polytope.points): intersects = [[value]] else: @@ -139,7 +230,8 @@ def slice(polytope: ConvexPolytope, axis, value): # Reduce dimension of intersection points, removing slice axis intersects = _reduce_dimension(intersects, slice_axis_idx) - axes = [ax for ax in polytope.axes() if ax != axis] + axes = copy(polytope._axes) + axes.remove(axis) if len(intersects) < len(intersects[0]) + 1: return ConvexPolytope(axes, intersects) @@ -156,7 +248,7 @@ def slice(polytope: ConvexPolytope, axis, value): vertices = hull.vertices except scipy.spatial.qhull.QhullError as e: - if "input is less than" or "simplex is flat" in str(e): + if "less than" or "flat" in str(e): return ConvexPolytope(axes, intersects) # Sliced result is simply the convex hull return ConvexPolytope(axes, [intersects[i] for i in vertices]) diff --git a/polytope/polytope.py b/polytope/polytope.py index d84716c0..f1ba2002 100644 --- a/polytope/polytope.py +++ b/polytope/polytope.py @@ -29,20 +29,31 @@ def polytopes(self): polytopes.extend(shape.polytope()) return polytopes + def __repr__(self): + return_str = "" + for shape in self.shapes: + return_str += shape.__repr__() + "\n" + return return_str + class Polytope: - def __init__(self, datacube, engine=None, options={}): + def __init__(self, datacube, engine=None, axis_options=None, datacube_options=None): from .datacube import Datacube from .engine import Engine - self.datacube = Datacube.create(datacube, options) + if axis_options is None: + axis_options = {} + if datacube_options is None: + datacube_options = {} + + self.datacube = Datacube.create(datacube, axis_options) self.engine = engine if engine is not None else Engine.default() def slice(self, polytopes: List[ConvexPolytope]): """Low-level API which takes a polytope geometry object and uses it to slice the datacube""" return self.engine.extract(self.datacube, polytopes) - def retrieve(self, request: Request): + def retrieve(self, request: Request, method="standard"): """Higher-level API which takes a request and uses it to slice the datacube""" request_tree = self.engine.extract(self.datacube, request.polytopes()) self.datacube.get(request_tree) diff --git a/polytope/shapes.py b/polytope/shapes.py index acc45673..0c170b3f 100644 --- a/polytope/shapes.py +++ b/polytope/shapes.py @@ -23,16 +23,25 @@ def axes(self) -> List[str]: class ConvexPolytope(Shape): - def __init__(self, axes, points): + def __init__(self, axes, points, method=None): self._axes = list(axes) + self.is_flat = False + if len(self._axes) == 1: + self.is_flat = True self.points = points + self.method = method def extents(self, axis): - slice_axis_idx = self.axes().index(axis) - axis_values = [point[slice_axis_idx] for point in self.points] - lower = min(axis_values) - upper = max(axis_values) - return (lower, upper) + if self.is_flat: + slice_axis_idx = 1 + lower = min(self.points)[0] + upper = max(self.points)[0] + else: + slice_axis_idx = self.axes().index(axis) + axis_values = [point[slice_axis_idx] for point in self.points] + lower = min(axis_values) + upper = max(axis_values) + return (lower, upper, slice_axis_idx) def __str__(self): return f"Polytope in {self.axes} with points {self.points}" @@ -48,21 +57,49 @@ def polytope(self): class Select(Shape): """Matches several discrete value""" - def __init__(self, axis, values): + def __init__(self, axis, values, method=None): self.axis = axis self.values = values + self.method = method def axes(self): return [self.axis] def polytope(self): - return [ConvexPolytope([self.axis], [[v]]) for v in self.values] + return [ConvexPolytope([self.axis], [[v]], self.method) for v in self.values] + + def __repr__(self): + return f"Select in {self.axis} with points {self.values}" + + +class Point(Shape): + """Matches several discrete value""" + + def __init__(self, axes, values, method=None): + self._axes = axes + self.values = values + self.method = method + self.polytopes = [] + if method == "nearest": + assert len(self.values) == 1 + for i in range(len(axes)): + polytope_points = [v[i] for v in self.values] + self.polytopes.extend([ConvexPolytope([axes[i]], [[point]], self.method) for point in polytope_points]) + + def axes(self): + return self._axes + + def polytope(self): + return self.polytopes + + def __repr__(self): + return f"Point in {self._axes} with points {self.values}" class Span(Shape): """1-D range along a single axis""" - def __init__(self, axis, lower=None, upper=None): + def __init__(self, axis, lower=-math.inf, upper=math.inf): assert not isinstance(lower, list) assert not isinstance(upper, list) self.axis = axis @@ -75,6 +112,9 @@ def axes(self): def polytope(self): return [ConvexPolytope([self.axis], [[self.lower], [self.upper]])] + def __repr__(self): + return f"Span in {self.axis} with range from {self.lower} to {self.upper}" + class All(Span): """Matches all indices in an axis""" @@ -82,12 +122,17 @@ class All(Span): def __init__(self, axis): super().__init__(axis) + def __repr__(self): + return f"All in {self.axis}" + class Box(Shape): """N-D axis-aligned bounding box (AABB), specified by two opposite corners""" def __init__(self, axes, lower_corner=None, upper_corner=None): dimension = len(axes) + self._lower_corner = lower_corner + self._upper_corner = upper_corner self._axes = axes assert len(lower_corner) == dimension assert len(upper_corner) == dimension @@ -108,7 +153,6 @@ def __init__(self, axes, lower_corner=None, upper_corner=None): if i >> d & 1: vertex[d] = upper_corner[d] self.vertices.append(vertex) - assert lower_corner in self.vertices assert upper_corner in self.vertices assert len(self.vertices) == 2**dimension @@ -119,6 +163,9 @@ def axes(self): def polytope(self): return [ConvexPolytope(self.axes(), self.vertices)] + def __repr__(self): + return f"Box in {self._axes} with with lower corner {self._lower_corner} and upper corner{self._upper_corner}" + class Disk(Shape): """2-D shape bounded by an ellipse""" @@ -157,6 +204,9 @@ def axes(self): def polytope(self): return [ConvexPolytope(self.axes(), self.points)] + def __repr__(self): + return f"Disk in {self._axes} with centred at {self.centre} and with radius {self.radius}" + class Ellipsoid(Shape): # Here we use the formula for the inscribed circle in an icosahedron @@ -209,12 +259,18 @@ def _icosahedron_edge_length_coeff(self): def polytope(self): return [ConvexPolytope(self.axes(), self.points)] + def __repr__(self): + return f"Ellipsoid in {self._axes} with centred at {self.centre} and with radius {self.radius}" + class PathSegment(Shape): """N-D polytope defined by a shape which is swept along a straight line between two points""" def __init__(self, axes, shape: Shape, start: List, end: List): self._axes = axes + self._start = start + self._end = end + self._shape = shape assert shape.axes() == self.axes() assert len(start) == len(self.axes()) @@ -238,12 +294,18 @@ def axes(self): def polytope(self): return self.polytopes + def __repr__(self): + return f"PathSegment in {self._axes} obtained by sweeping a {self._shape.__repr__()} \ + between the points {self._start} and {self._end}" + class Path(Shape): """N-D polytope defined by a shape which is swept along a polyline defined by multiple points""" def __init__(self, axes, shape, *points, closed=False): self._axes = axes + self._shape = shape + self._points = points assert shape.axes() == self.axes() for p in points: @@ -265,6 +327,10 @@ def axes(self): def polytope(self): return self.union.polytope() + def __repr__(self): + return f"Path in {self._axes} obtained by sweeping a {self._shape.__repr__()} \ + between the points {self._points}" + class Union(Shape): """N-D union of two shapes with the same axes""" @@ -275,6 +341,7 @@ def __init__(self, axes, *shapes): assert s.axes() == self.axes() self.polytopes = [] + self._shapes = shapes for s in shapes: self.polytopes.extend(s.polytope()) @@ -285,6 +352,9 @@ def axes(self): def polytope(self): return self.polytopes + def __repr__(self): + return f"Union in {self._axes} of the shapes {self._shapes}" + class Polygon(Shape): """2-D polygon defined by a set of exterior points""" @@ -295,6 +365,7 @@ def __init__(self, axes, points): for p in points: assert len(p) == 2 + self._points = points triangles = tripy.earclip(points) self.polytopes = [] @@ -311,3 +382,6 @@ def axes(self): def polytope(self): return self.polytopes + + def __repr__(self): + return f"Polygon in {self._axes} with points {self._points}" diff --git a/polytope/utility/combinatorics.py b/polytope/utility/combinatorics.py index 1a4a24a5..9dc64108 100644 --- a/polytope/utility/combinatorics.py +++ b/polytope/utility/combinatorics.py @@ -18,7 +18,7 @@ def group(polytopes: List[ConvexPolytope]): return groups, concatenation -def product(groups): +def tensor_product(groups): # Compute the tensor product of polytope groups return list(itertools.product(*groups.values())) diff --git a/polytope/utility/geometry.py b/polytope/utility/geometry.py index bbbb7515..2c88d965 100644 --- a/polytope/utility/geometry.py +++ b/polytope/utility/geometry.py @@ -1,4 +1,22 @@ +import math + + def lerp(a, b, value): direction = [a - b for a, b in zip(a, b)] intersect = [b + value * d for b, d in zip(b, direction)] return intersect + + +def nearest_pt(pts_list, pt): + nearest_pt = pts_list[0] + distance = l2_norm(pts_list[0], pt) + for new_pt in pts_list[1:]: + new_distance = l2_norm(new_pt, pt) + if new_distance < distance: + distance = new_distance + nearest_pt = new_pt + return nearest_pt + + +def l2_norm(pt1, pt2): + return math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) diff --git a/polytope/utility/list_tools.py b/polytope/utility/list_tools.py new file mode 100644 index 00000000..2d18917c --- /dev/null +++ b/polytope/utility/list_tools.py @@ -0,0 +1,22 @@ +def bisect_left_cmp(arr, val, cmp): + left = -1 + r = len(arr) + while r - left > 1: + e = (left + r) >> 1 + if cmp(arr[e], val): + left = e + else: + r = e + return left + + +def bisect_right_cmp(arr, val, cmp): + left = -1 + r = len(arr) + while r - left > 1: + e = (left + r) >> 1 + if cmp(arr[e], val): + left = e + else: + r = e + return r diff --git a/polytope/version.py b/polytope/version.py index 7863915f..976498ab 100644 --- a/polytope/version.py +++ b/polytope/version.py @@ -1 +1 @@ -__version__ = "1.0.2" +__version__ = "1.0.3" diff --git a/pyproject.toml b/pyproject.toml index 89b8eb76..d9a2e1f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,4 +2,7 @@ line-length = 120 [build-system] requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" +[tool.pytest.ini_options] +markers = ["internet: downloads test data from the internet (deselect with '-m \"not internet\"')", + "fdb: test which uses fdb (deselect with '-m \"not fdb\"')",] \ No newline at end of file diff --git a/readme.md b/readme.md index b2ac63de..48dba68f 100644 --- a/readme.md +++ b/readme.md @@ -67,7 +67,7 @@ The Polytope algorithm can for example be used to extract: - and many more high-dimensional shapes in arbitrary dimensions... -For more information about the Polytope algorithm, refer to our [paper] (https://arxiv.org/abs/2306.11553). +For more information about the Polytope algorithm, refer to our [paper](https://arxiv.org/abs/2306.11553). If this project is useful for your work, please consider citing this paper. ## Installation @@ -85,40 +85,45 @@ or from PyPI with the command Here is a step-by-step example of how to use this software. 1. In this example, we first specify the data which will be in our Xarray datacube. Note that the data here comes from the GRIB file called "winds.grib", which is 3-dimensional with dimensions: step, latitude and longitude. - + ```Python import xarray as xr array = xr.open_dataset("winds.grib", engine="cfgrib") + ``` We then construct the Polytope object, passing in some additional metadata describing properties of the longitude axis. - + ```Python options = {"longitude": {"Cyclic": [0, 360.0]}} from polytope.polytope import Polytope p = Polytope(datacube=array, options=options) + ``` 2. Next, we create a request shape to extract from the datacube. In this example, we want to extract a simple 2D box in latitude and longitude at step 0. We thus create the two relevant shapes we need to build this 3-dimensional object, - + ```Python import numpy as np from polytope.shapes import Box, Select box = Box(["latitude", "longitude"], [0, 0], [1, 1]) step_point = Select("step", [np.timedelta64(0, "s")]) + ``` which we then incorporate into a Polytope request. - + ```Python from polytope.polytope import Request request = Request(box, step_point) + ``` 3. Finally, extract the request from the datacube. - + ```Python result = p.retrieve(request) + ``` The result is stored as an IndexTree containing the retrieved data organised hierarchically with axis indices for each point. - + ```Python result.pprint() @@ -132,6 +137,7 @@ Here is a step-by-step example of how to use this software. ↳latitude=1.0 ↳longitude=0.0 ↳longitude=1.0 + ```