forked from s_ranjbar/city_retrofit
126 lines
3.4 KiB
Python
126 lines
3.4 KiB
Python
|
"""
|
||
|
interval.py
|
||
|
--------------
|
||
|
|
||
|
Deal with 1D intervals which are defined by:
|
||
|
[start position, end position]
|
||
|
"""
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
|
||
|
def check(a, b, digits):
|
||
|
"""
|
||
|
Check input ranges, convert them to vector form,
|
||
|
and get a fixed precision integer version of them.
|
||
|
|
||
|
Parameters
|
||
|
--------------
|
||
|
a : (2, ) or (2, n) float
|
||
|
Start and end of a 1D interval
|
||
|
b : (2, ) or (2, n) float
|
||
|
Start and end of a 1D interval
|
||
|
digits : int
|
||
|
How many digits to consider
|
||
|
|
||
|
Returns
|
||
|
--------------
|
||
|
a : (2, n) float
|
||
|
Ranges as vector
|
||
|
b : (2, n) float
|
||
|
Ranges as vector
|
||
|
a_int : (2, n) int64
|
||
|
Ranges rounded to digits, as vector
|
||
|
b_int : (2, n) int64
|
||
|
Ranges rounded to digits, as vector
|
||
|
is_1D : bool
|
||
|
If True, input was single pair of ranges
|
||
|
"""
|
||
|
a = np.array(a, dtype=np.float64)
|
||
|
b = np.array(b, dtype=np.float64)
|
||
|
|
||
|
if a.shape != b.shape or a.shape[-1] != 2:
|
||
|
raise ValueError('ranges must be identical and (2,)!')
|
||
|
|
||
|
# if input was single interval reshape it here
|
||
|
is_1D = False
|
||
|
if len(a.shape) == 1:
|
||
|
a = a.reshape((-1, 2))
|
||
|
b = b.reshape((-1, 2))
|
||
|
is_1D = True
|
||
|
|
||
|
# make sure ranges are sorted
|
||
|
a.sort(axis=1)
|
||
|
b.sort(axis=1)
|
||
|
|
||
|
# compare in fixed point as integers
|
||
|
a_int = (a * 10**digits).round().astype(np.int64)
|
||
|
b_int = (b * 10**digits).round().astype(np.int64)
|
||
|
|
||
|
return a, b, a_int, b_int, is_1D
|
||
|
|
||
|
|
||
|
def intersection(a, b, digits=8):
|
||
|
"""
|
||
|
Given a pair of ranges, merge them in to
|
||
|
one range if they overlap at all
|
||
|
|
||
|
Parameters
|
||
|
--------------
|
||
|
a : (2, ) float
|
||
|
Start and end of a 1D interval
|
||
|
b : (2, ) float
|
||
|
Start and end of a 1D interval
|
||
|
digits : int
|
||
|
How many digits to consider
|
||
|
|
||
|
Returns
|
||
|
--------------
|
||
|
intersects : bool or (n,) bool
|
||
|
Indicates if the ranges overlap at all
|
||
|
new_range : (2, ) or (2, 2) float
|
||
|
The unioned range from the two inputs,
|
||
|
or both of the original ranges if not overlapping
|
||
|
"""
|
||
|
# check shape and convert
|
||
|
a, b, a_int, b_int, is_1D = check(a, b, digits)
|
||
|
|
||
|
# what are the starting and ending points of the overlap
|
||
|
overlap = np.zeros(a.shape, dtype=np.float64)
|
||
|
|
||
|
# A fully overlaps B
|
||
|
current = np.logical_and(a_int[:, 0] <= b_int[:, 0],
|
||
|
a_int[:, 1] >= b_int[:, 1])
|
||
|
overlap[current] = b[current]
|
||
|
|
||
|
# B fully overlaps A
|
||
|
current = np.logical_and(a_int[:, 0] >= b_int[:, 0],
|
||
|
a_int[:, 1] <= b_int[:, 1])
|
||
|
overlap[current] = a[current]
|
||
|
|
||
|
# A starts B ends
|
||
|
# A:, 0 B:, 0 A:, 1 B:, 1
|
||
|
current = np.logical_and(
|
||
|
np.logical_and(a_int[:, 0] <= b_int[:, 0],
|
||
|
b_int[:, 0] < a_int[:, 1]),
|
||
|
a_int[:, 1] < b_int[:, 1])
|
||
|
overlap[current] = np.column_stack([b[current][:, 0],
|
||
|
a[current][:, 1]])
|
||
|
|
||
|
# B starts A ends
|
||
|
# B:, 0 A:, 0 B:, 1 A:, 1
|
||
|
current = np.logical_and(
|
||
|
np.logical_and(b_int[:, 0] <= a_int[:, 0],
|
||
|
a_int[:, 0] < b_int[:, 1]),
|
||
|
b_int[:, 1] < a_int[:, 1])
|
||
|
overlap[current] = np.column_stack([a[current][:, 0],
|
||
|
b[current][:, 1]])
|
||
|
|
||
|
# is range overlapping at all
|
||
|
intersects = overlap.ptp(axis=1) > 10**-digits
|
||
|
|
||
|
if is_1D:
|
||
|
return intersects[0], overlap[0]
|
||
|
|
||
|
return intersects, overlap
|