# Copyright 2019 IBM Corporation
#
# Licensed 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.
from typing import Any, Optional, Union
from .schema_utils import JsonSchema
[docs]class SchemaRange:
def __init__(
self,
minimum=None,
maximum=None,
exclusive_minimum=False,
exclusive_maximum=False,
is_integer: bool = False,
distribution: Optional[str] = None,
) -> None:
self.minimum = minimum
self.maximum = maximum
self.exclusive_minimum = exclusive_minimum
self.exclusive_maximum = exclusive_maximum
self.is_integer = is_integer
self.distribution = distribution
def __str__(self):
res = ""
if self.minimum is None:
res += "(infty"
else:
if self.exclusive_minimum:
res += "("
else:
res += "["
res += str(self.minimum)
res += ","
if self.maximum is None:
res += "infty"
if self.distribution == "loguniform":
res += "//log"
res += ")"
else:
res += str(self.maximum)
if self.distribution == "loguniform":
res += "//log"
if self.exclusive_maximum:
res += ")"
else:
res += "]"
return res
[docs] @classmethod
def point(cls, pt: Union[int, float]):
return SchemaRange(
minimum=pt,
maximum=pt,
exclusive_minimum=False,
exclusive_maximum=False,
is_integer=isinstance(pt, int),
)
[docs] @classmethod
def fromSchema(cls, schema: Any) -> "SchemaRange":
return SchemaRange(
minimum=schema.get("minimum", None),
maximum=schema.get("maximum", None),
exclusive_minimum=schema.get("exclusiveMinimum", False),
exclusive_maximum=schema.get("exclusiveMaximum", False),
is_integer=schema.get("type", "number") == "integer",
distribution=schema.get("distribution", None),
)
[docs] @classmethod
def fromSchemaForOptimizer(cls, schema: Any) -> "SchemaRange":
s = cls.fromSchema(schema)
minimum = schema.get("minimumForOptimizer", None)
maximum = schema.get("maximumForOptimizer", None)
exclusive_minimum = schema.get("exclusiveMinimumForOptimizer", False)
exclusive_maximum = schema.get("exclusiveMaximumForOptimizer", False)
is_integer = (
schema.get("type", "numberForOptimizer") == "integer" or s.is_integer
)
if minimum is None:
minimum = s.minimum
if s.minimum is not None:
exclusive_minimum = exclusive_minimum or s.exclusive_minimum
elif s.minimum is not None and minimum == s.minimum:
exclusive_minimum = exclusive_minimum or s.exclusive_minimum
if maximum is None:
maximum = s.maximum
if s.maximum is not None:
exclusive_maximum = exclusive_maximum or s.exclusive_maximum
elif s.maximum is not None and minimum == s.minimum:
exclusive_maximum = exclusive_maximum or s.exclusive_maximum
distribution = s.distribution
return SchemaRange(
minimum=minimum,
maximum=maximum,
exclusive_minimum=exclusive_minimum,
exclusive_maximum=exclusive_maximum,
is_integer=is_integer,
distribution=distribution,
)
[docs] @classmethod
def to_schema_with_optimizer(
cls, actual_range: "SchemaRange", optimizer_range: "SchemaRange"
) -> JsonSchema:
number_schema: JsonSchema = {}
if actual_range.is_integer:
number_schema["type"] = "integer"
else:
number_schema["type"] = "number"
if optimizer_range.is_integer:
number_schema["laleType"] = "integer"
if actual_range.minimum is not None:
number_schema["minimum"] = actual_range.minimum
if actual_range.exclusive_minimum:
number_schema["exclusiveMinimum"] = True
if optimizer_range.minimum is not None:
if (
actual_range.minimum is None
or actual_range.minimum < optimizer_range.minimum
or (
actual_range.minimum == optimizer_range.minimum
and optimizer_range.exclusive_minimum
and not actual_range.exclusive_minimum
)
):
number_schema["minimumForOptimizer"] = optimizer_range.minimum
if optimizer_range.exclusive_minimum:
number_schema["exclusiveMinimumForOptimizer"] = True
if actual_range.maximum is not None:
number_schema["maximum"] = actual_range.maximum
if actual_range.exclusive_maximum:
number_schema["exclusiveMaximum"] = True
if optimizer_range.maximum is not None:
if (
actual_range.maximum is None
or actual_range.maximum > optimizer_range.maximum
or (
actual_range.maximum == optimizer_range.maximum
and optimizer_range.exclusive_maximum
and not actual_range.exclusive_maximum
)
):
number_schema["maximumForOptimizer"] = optimizer_range.maximum
if optimizer_range.exclusive_maximum:
number_schema["exclusiveMaximumForOptimizer"] = True
if optimizer_range.distribution is not None:
number_schema["distribution"] = optimizer_range.distribution
return number_schema
def __iand__(self, other: "SchemaRange"):
self.is_integer = self.is_integer or other.is_integer
if other.minimum is not None:
if self.minimum is None:
self.minimum = other.minimum
self.exclusive_minimum = other.exclusive_minimum
elif self.minimum == other.minimum:
self.exclusive_minimum = (
self.exclusive_minimum or other.exclusive_minimum
)
elif self.minimum < other.minimum:
self.minimum = other.minimum
self.exclusive_minimum = other.exclusive_minimum
if other.maximum is not None:
if self.maximum is None:
self.maximum = other.maximum
self.exclusive_maximum = other.exclusive_maximum
elif self.maximum == other.maximum:
self.exclusive_maximum = (
self.exclusive_maximum or other.exclusive_maximum
)
elif self.maximum > other.maximum:
self.maximum = other.maximum
self.exclusive_maximum = other.exclusive_maximum
if self.distribution is None:
self.distribution = other.distribution
return self
[docs] def diff(self, other: "SchemaRange") -> Optional[bool]:
"""Returns None if the resulting region is impossible.
Returns True if the other constraint was completely subtracted from
self. If it could not be, then it returns False (and the caller should probably
keep the other constraint as a negated constraint)
"""
# for now, just handle simple exclusions
if not other.is_integer or other.is_integer == self.is_integer:
# the exclusion is less than the actual range
if (
self.minimum is not None
and other.maximum is not None
and (
other.maximum < self.minimum
or (
other.maximum == self.minimum
and (self.exclusive_minimum or other.exclusive_maximum)
)
)
):
return True
# the exclusion is greater than the actual range
if (
self.maximum is not None
and other.minimum is not None
and (
other.minimum > self.maximum
or (
other.minimum == self.maximum
and (self.exclusive_maximum or other.exclusive_minimum)
)
)
):
return True
if other.minimum is None:
if self.minimum is None:
# the exclusion and the range have no minimum
if other.maximum is None:
# nothing is possible
return None
else:
self.minimum = other.maximum
self.exclusive_minimum = not other.exclusive_maximum
return True
# else might create a hole, so avoid this case
else:
# ASSERT: other.minimum is not None
if (
self.minimum is None
or self.minimum < other.minimum
or (
self.minimum == other.minimum
and (not self.exclusive_minimum or other.exclusive_minimum)
)
):
if (
other.maximum is None
or self.maximum is not None
and (
other.maximum > self.maximum
or (
(
other.maximum == self.maximum
and (
not other.exclusive_maximum
or self.exclusive_maximum
)
)
)
)
):
self.maximum = other.minimum
self.exclusive_maximum = not other.exclusive_minimum
return True
# else might create a hole, so avoid this case
else:
# self.minimum >= other.minimum
if (
other.maximum is None
or self.maximum < other.maximum
or (
self.maximum == other.maximum
and (not other.exclusive_maximum or self.exclusive_maximum)
)
):
# nothing is possible
return None
else:
self.minimum = other.maximum
self.exclusive_minimum = not other.exclusive_maximum
return True
if other.maximum is None:
if self.maximum is None:
# the exclusion and the range have no maximum
if other.minimum is None:
# nothing is possible
return None
else:
self.maximum = other.minimum
self.exclusive_maximum = not other.exclusive_minimum
return True
# else might create a hole, so avoid this case
else:
# ASSERT: other.maximum is not None
if (
self.maximum is None
or self.maximum > other.maximum
or (
self.maximum == other.maximum
and (not self.exclusive_maximum or other.exclusive_maximum)
)
):
if (
other.minimum is None
or self.minimum is not None
and (
other.minimum < self.minimum
or (
(
other.minimum == self.minimum
and (
not other.exclusive_minimum
or self.exclusive_minimum
)
)
)
)
):
self.minimum = other.maximum
self.exclusive_minimum = not other.exclusive_maximum
return True
# else might create a hole, so avoid this case
else:
# self.maximum >= other.maximum
if (
other.minimum is None
or self.minimum > other.minimum
or (
self.minimum == other.minimum
and (not other.exclusive_minimum or self.exclusive_minimum)
)
):
# nothing is possible
return None
else:
self.maximum = other.minimum
self.exclusive_maximum = not other.exclusive_minimum
return True
return False
[docs] def remove_point(self, other: Union[int, float]) -> Optional[bool]:
"""Returns None if the resulting region is impossible.
Returns True if the other constraint was completely subtracted from
self. If it could not be, then it returns False (and the caller should probably
keep the other constraint as a negated constraint)
"""
return self.diff(SchemaRange.point(other))
[docs] @classmethod
def is_empty2(cls, lower: "SchemaRange", upper: "SchemaRange") -> bool:
"""Determines if the range given by taking lower bounds from lower and upper bound from upper is empty (contains nothing)
is_integer is assumed to be their disjunction
"""
is_integer = lower.is_integer or upper.is_integer
if lower.minimum is not None and upper.maximum is not None:
if lower.minimum > upper.maximum:
return True
if lower.minimum == upper.maximum and (
lower.exclusive_minimum or upper.exclusive_maximum
):
return True
if (
is_integer
and lower.exclusive_minimum
and upper.exclusive_maximum
and lower.minimum + 1 == upper.maximum
):
return True
return False
[docs] def is_empty(self) -> bool:
"""Determines if the range is empty (contains nothing)"""
return SchemaRange.is_empty2(self, self)