mirror of
https://github.com/google/earthengine-api.git
synced 2025-12-08 19:26:12 +00:00
199 lines
7.2 KiB
Python
199 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""An object representing a custom EE Function."""
|
|
|
|
from typing import Any, Callable, Dict, Optional
|
|
|
|
from ee import computedobject
|
|
from ee import ee_exception
|
|
from ee import ee_types
|
|
from ee import encodable
|
|
from ee import function
|
|
from ee import serializer
|
|
|
|
|
|
# Multiple inheritance is necessary because a CustomFunction needs to know how
|
|
# to encode itself in different ways:
|
|
# - as an Encodable: encode its definition
|
|
# - as a Function: encode its invocation (which may also involve encoding its
|
|
# definition, if that hasn't happened yet).
|
|
class CustomFunction(function.Function, encodable.Encodable):
|
|
"""An object representing a custom EE Function."""
|
|
_body: Any
|
|
_signature: Dict[str, Any]
|
|
|
|
def __init__(self, signature: Dict[str, Any], body: Any):
|
|
"""Creates a function defined by a given expression with unbound variables.
|
|
|
|
The expression is created by evaluating the given function
|
|
using variables as placeholders.
|
|
|
|
Args:
|
|
signature: The function signature. If any of the argument names are
|
|
null, their names will be generated deterministically, based on
|
|
the body.
|
|
body: The Python function to evaluate.
|
|
"""
|
|
variables = [CustomFunction.variable(arg['type'], arg['name'])
|
|
for arg in signature['args']]
|
|
|
|
if body(*variables) is None:
|
|
raise ee_exception.EEException('User-defined methods must return a value')
|
|
|
|
# The signature of the function.
|
|
self._signature = CustomFunction._resolveNamelessArgs(
|
|
signature, variables, body)
|
|
|
|
# The expression to evaluate.
|
|
self._body = body(*variables)
|
|
|
|
def encode(self, encoder: Callable[[Any], Any]) -> Dict[str, Any]:
|
|
return {
|
|
'type': 'Function',
|
|
'argumentNames': [x['name'] for x in self._signature['args']],
|
|
'body': encoder(self._body)
|
|
}
|
|
|
|
def encode_cloud_value(self, encoder: Callable[[Any], Any]) -> Dict[str, Any]:
|
|
return {
|
|
'functionDefinitionValue': {
|
|
'argumentNames': [x['name'] for x in self._signature['args']],
|
|
'body': encoder(self._body)
|
|
}
|
|
}
|
|
|
|
def encode_invocation(self, encoder: Callable[[Any], Any]) -> Dict[str, Any]:
|
|
return self.encode(encoder)
|
|
|
|
def encode_cloud_invocation(
|
|
self, encoder: Callable[[Any], Any]
|
|
) -> Dict[str, Any]:
|
|
return {'functionReference': encoder(self)}
|
|
|
|
def getSignature(self) -> Dict[str, Any]:
|
|
"""Returns a description of the interface provided by this function."""
|
|
return self._signature
|
|
|
|
@staticmethod
|
|
def variable(type_name: Optional[str], name: str) -> Any:
|
|
"""Returns a placeholder variable with a given name and EE type.
|
|
|
|
Args:
|
|
type_name: A class to mimic.
|
|
name: The name of the variable as it will appear in the
|
|
arguments of the custom functions that use this variable. If null,
|
|
a name will be auto-generated in _resolveNamelessArgs().
|
|
|
|
Returns:
|
|
A variable with the given name implementing the given type.
|
|
"""
|
|
var_type = ee_types.nameToClass(type_name) or computedobject.ComputedObject
|
|
result = var_type.__new__(var_type)
|
|
result.func = None
|
|
result.args = None
|
|
result.varName = name
|
|
return result
|
|
|
|
@staticmethod
|
|
def create(
|
|
func: Callable[[Any], Any], return_type: Any, arg_types: Any
|
|
) -> 'CustomFunction':
|
|
"""Creates a CustomFunction.
|
|
|
|
The result calls a given native function with the specified return type and
|
|
argument types and auto-generated argument names.
|
|
|
|
Args:
|
|
func: The native function to wrap.
|
|
return_type: The type of the return value, either as a string or a
|
|
class reference.
|
|
arg_types: The types of the arguments, either as strings or class
|
|
references.
|
|
|
|
Returns:
|
|
The constructed CustomFunction.
|
|
"""
|
|
|
|
def StringifyType(t: Any) -> str:
|
|
return t if isinstance(t, str) else ee_types.classToName(t)
|
|
|
|
args = [{'name': None, 'type': StringifyType(i)} for i in arg_types]
|
|
signature = {
|
|
'name': '',
|
|
'returns': StringifyType(return_type),
|
|
'args': args
|
|
}
|
|
return CustomFunction(signature, func)
|
|
|
|
@staticmethod
|
|
def _resolveNamelessArgs(signature: Any, variables: Any, body: Any) -> Any:
|
|
"""Deterministically generates names for unnamed variables.
|
|
|
|
The names are based on the body of the function.
|
|
|
|
Args:
|
|
signature: The signature which may contain null argument names.
|
|
variables: A list of variables, some of which may be nameless.
|
|
These will be updated to include names when this method returns.
|
|
body: The Python function to evaluate.
|
|
|
|
Returns:
|
|
The signature with null arg names resolved.
|
|
"""
|
|
nameless_arg_indices = []
|
|
for i, variable in enumerate(variables):
|
|
if variable.varName is None:
|
|
nameless_arg_indices.append(i)
|
|
|
|
# Do we have any nameless arguments at all?
|
|
if not nameless_arg_indices:
|
|
return signature
|
|
|
|
# Generate the name base by counting the number of custom functions
|
|
# within the body.
|
|
def CountFunctions(expression: Any) -> int:
|
|
"""Counts the number of custom functions in a serialized expression."""
|
|
def CountNodes(nodes: Any) -> int:
|
|
return sum([CountNode(node) for node in nodes])
|
|
|
|
def CountNode(node: Any) -> int:
|
|
if 'functionDefinitionValue' in node:
|
|
return 1
|
|
elif 'arrayValue' in node:
|
|
return CountNodes(node['arrayValue']['values'])
|
|
elif 'dictionaryValue' in node:
|
|
return CountNodes(node['dictionaryValue']['values'].values())
|
|
elif 'functionInvocationValue' in node:
|
|
fn = node['functionInvocationValue']
|
|
return CountNodes(fn['arguments'].values())
|
|
return 0
|
|
|
|
return CountNodes(expression['values'].values())
|
|
|
|
# There are three function building phases, which each call body():
|
|
# 1 - Check Return. The constructor verifies that body() returns a result,
|
|
# but does not try to serialize the result. If the function tries to use
|
|
# unbound variables (eg, using .getInfo() or print()), ComputedObject will
|
|
# throw an exception when these calls try to serialize themselves, so that
|
|
# unbound variables are not passed in server calls.
|
|
# 2 - Count Functions. We serialize the result here. At this point all
|
|
# variables must have names for serialization to succeed, but we don't yet
|
|
# know the correct function depth. So we serialize with unbound_name set to
|
|
# '<unbound>', which should silently succeed. If this does end up in server
|
|
# calls, the function is very unusual: the first call doesn't use unbound
|
|
# variables but the second call does. In this rare case we will return
|
|
# server errors complaining about <unbound>.
|
|
# 3 - Final Serialize. Finally, the constructor calls body() with the
|
|
# correct, depth-dependent names, which are used when the CustomFunction
|
|
# is serialized and sent to the server.
|
|
serialized_body = serializer.encode(
|
|
body(*variables), for_cloud_api=True, unbound_name='<unbound>')
|
|
base_name = '_MAPPING_VAR_%d_' % CountFunctions(serialized_body)
|
|
|
|
# Update the vars and signature by the name.
|
|
for i, index in enumerate(nameless_arg_indices):
|
|
name = base_name + str(i)
|
|
variables[index].varName = name
|
|
signature['args'][index]['name'] = name
|
|
|
|
return signature
|