analysis/webservice/algorithms/StandardDeviationSearch.py (142 lines of code) (raw):
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
from datetime import datetime
from functools import partial
from nexustiles.exception import NexusTileServiceException
from pytz import timezone
from webservice.NexusHandler import nexus_handler
from webservice.algorithms.NexusCalcHandler import NexusCalcHandler
from webservice.webmodel import NexusProcessingException, CustomEncoder
SENTINEL = 'STOP'
EPOCH = timezone('UTC').localize(datetime(1970, 1, 1))
@nexus_handler
class StandardDeviationSearchCalcHandlerImpl(NexusCalcHandler):
name = "Standard Deviation Search"
path = "/standardDeviation"
description = "Retrieves the pixel standard deviation if it exists for a given longitude and latitude"
params = {
"ds": {
"name": "Dataset",
"type": "string",
"description": "One or more comma-separated dataset shortnames. Required."
},
"longitude": {
"name": "Longitude",
"type": "float",
"description": "Longitude in degrees from -180 to 180. Required."
},
"latitude": {
"name": "Latitude",
"type": "float",
"description": "Latitude in degrees from -90 to 90. Required."
},
"day": {
"name": "Day of Year",
"type": "int",
"description": "Day of year to search from 0 to 365. One of day or date are required but not both."
},
"date": {
"name": "Date",
"type": "string",
"description": "Datetime in format YYYY-MM-DDTHH:mm:ssZ or seconds since epoch (Jan 1st, 1970). One of day "
"or date are required but not both."
},
"allInTile": {
"name": "Get all Standard Deviations in Tile",
"type": "boolean",
"description": "Optional True/False flag. If true, return the standard deviations for every pixel in the "
"tile that contains the searched lon/lat point. If false, return the "
"standard deviation only for the searched lon/lat point. Default: True"
}
}
singleton = True
def parse_arguments(self, request):
# Parse input arguments
try:
ds = request.get_dataset()[0]
except:
raise NexusProcessingException(reason="'ds' argument is required", code=400)
try:
longitude = float(request.get_decimal_arg("longitude", default=None))
except:
raise NexusProcessingException(reason="'longitude' argument is required", code=400)
try:
latitude = float(request.get_decimal_arg("latitude", default=None))
except:
raise NexusProcessingException(reason="'latitude' argument is required", code=400)
search_datetime = request.get_datetime_arg('date', default=None)
day_of_year = request.get_int_arg('day', default=None)
if (search_datetime is not None and day_of_year is not None) \
or (search_datetime is None and day_of_year is None):
raise NexusProcessingException(
reason="At least one of 'day' or 'date' arguments are required but not both.",
code=400)
if search_datetime is not None:
day_of_year = search_datetime.timetuple().tm_yday
return_all = request.get_boolean_arg("allInTile", default=True)
return ds, longitude, latitude, day_of_year, return_all
def calc(self, request, **args):
raw_args_dict = {k: request.get_argument(k) for k in request.requestHandler.request.arguments}
ds, longitude, latitude, day_of_year, return_all = self.parse_arguments(request)
if return_all:
func = partial(get_all_std_dev, tile_service=self._get_tile_service(), ds=ds, longitude=longitude,
latitude=latitude, day_of_year=day_of_year)
else:
func = partial(get_single_std_dev, tile_service=self._get_tile_service(), ds=ds, longitude=longitude,
latitude=latitude, day_of_year=day_of_year)
try:
results = StandardDeviationSearchCalcHandlerImpl.to_result_dict(func())
except (NoTileException, NoStandardDeviationException):
return StandardDeviationSearchResult(raw_args_dict, [])
return StandardDeviationSearchResult(raw_args_dict, results)
@staticmethod
def to_result_dict(list_of_tuples):
# list_of_tuples = [(lon, lat, st_dev)]
return [
{
"longitude": lon,
"latitude": lat,
"standard_deviation": st_dev
} for lon, lat, st_dev in list_of_tuples]
class NoTileException(Exception):
pass
class NoStandardDeviationException(Exception):
pass
def find_tile_and_std_name(tile_service, ds, longitude, latitude, day_of_year):
from shapely.geometry import Point
point = Point(longitude, latitude)
try:
tile = tile_service.find_tile_by_polygon_and_most_recent_day_of_year(point, ds, day_of_year)[0]
except NexusTileServiceException:
raise NoTileException
# Check if this tile has any meta data that ends with 'std'. If it doesn't, just return nothing.
try:
st_dev_meta_name = next(iter([key for key in list(tile.meta_data.keys()) if key.endswith('std')]))
except StopIteration:
raise NoStandardDeviationException
return tile, st_dev_meta_name
def get_single_std_dev(tile_service, ds, longitude, latitude, day_of_year):
from scipy.spatial import distance
tile, st_dev_meta_name = find_tile_and_std_name(tile_service, ds, longitude, latitude, day_of_year)
# Need to find the closest point in the tile to the input lon/lat point and return only that result
valid_indices = tile.get_indices()
tile_points = [tuple([tile.longitudes[lon_idx], tile.latitudes[lat_idx]]) for time_idx, lat_idx, lon_idx in
valid_indices]
closest_point_index = distance.cdist([(longitude, latitude)], tile_points).argmin()
closest_lon, closest_lat = tile_points[closest_point_index]
closest_point_tile_index = tuple(valid_indices[closest_point_index])
std_at_point = tile.meta_data[st_dev_meta_name][closest_point_tile_index]
return [tuple([closest_lon, closest_lat, std_at_point])]
def get_all_std_dev(tile_service, ds, longitude, latitude, day_of_year):
tile, st_dev_meta_name = find_tile_and_std_name(tile_service, ds, longitude, latitude, day_of_year)
valid_indices = tile.get_indices()
return [tuple([tile.longitudes[lon_idx], tile.latitudes[lat_idx],
tile.meta_data[st_dev_meta_name][time_idx, lat_idx, lon_idx]]) for time_idx, lat_idx, lon_idx in
valid_indices]
class StandardDeviationSearchResult(object):
def __init__(self, request_params, results):
self.request_params = request_params
self.results = results
def toJson(self):
data = {
'meta': self.request_params,
'data': self.results,
'stats': {}
}
return json.dumps(data, indent=4, cls=CustomEncoder)