Skip to content

Precision system

Ngoguey42 edited this page Dec 1, 2017 · 15 revisions

Introduction

Floating point precision loss is critical within buzzard, the user must set a number of significant digits for internal roundings using the buzz.Env context manager. Those roundings happen when

  • converting coordinates from spatial to raster
  • converting sizes from spatial to raster
  • comparing coordinates for equality
  • checking right angles
  • checking the quality of a spatial reference conversion.

This document describes how the Footprint class interact with the global environment variable buzz.env.significant to provide a consistent behavior.

Let's take as an example a Footprint with those attributes, and a computation within a with buzz.Env(significant=10): context.

Footprint
       tl=(672787.349686, 6876216.087215)
       br=(673115.409686, 6875996.107215)
       size=(328.060000, 219.980000)
       rsize=(16403, 10999)
       scale=(0.020000000000003407, -0.01999999999995597)
       angle=0.0

Minimum significant for each Footprint

The precision system adds a significant_min attribute to Footprints, which is the minimum significant digits that a Footprint requires to operate.

It is defined this way:

>>> largest_coord = np.abs(self.coords).max()
>>> largest_coord
6876216.08721489646

>>> smallest_reso = self.pxsize.min()
>>> smallest_reso
0.01999999999995597

>>> ratio_pixel_increment = smallest_reso / largest_coord
>>> ratio_pixel_increment
0.00000000290857642

>>> significant_min = -np.log10(ratio_pixel_increment)
>>> significant_min
8.53631952040470310

For that Footprint in means that

  • the smallest increment that we are interested in is 0.01999999999995597
  • the largest coordinate that we might encounter is 6876216.08721489646
  • near largest_coord the smallest increment (smallest_reso) is 0.000000290857642% of the coordinate
  • near largest_coord the smallest increment (smallest_reso) changes the 8th and 9th significant digits
  • if buzz.env.significant is set to 8.5 some methods will fail with an exception

It also means that if the Footprint is moved or scaled, the significant_min changes:

# 1. Move near 0: Geatly reduces `significant_min`
Footprint
    tl=(-164.03000000002794, 109.98999999975786)
    bl=(-164.03000000002794, -109.98999999975786)
    size=(328.06000000005588, 219.97999999951571)
    rsize=(16403, 10999)
    scale=(0.020000000000003407, -0.01999999999995597)
    angle=0.0
    significant_min=3.9138932892319462

# 2. Move near 0. and scale to a 2x2 square: Same `significant_min`
Footprint
    tl=(-1.0, 1.0)
    bl=(-1.0, -1.0)
    size=(2.0, 2.0)
    rsize=(16403, 10999)
    scale=(0.00012192891544229714, -0.00018183471224656787)
    angle=0.0
    significant_min=3.9138932892309164

# 3. Move near 1 billion: Increases `significant_min`
Footprint
    tl=(1000000000.0, 1000000000.0)
    bl=(1000000000.0, 999999780.01999998)
    size=(328.05999994277954, 219.98000001907349)
    rsize=(16403, 10999)
    scale=(0.019999999996511586, -0.02000000000173411)
    angle=0.0
    significant_min=10.698970146886394

# Move near 1billion and scale to a 1x1 square: Increases even more `significant_min`
Footprint
    tl=(1000000000.0, 1000000000.0)
    bl=(1000000000.0, 999999999.0)
    size=(1.0, 1.0)
    rsize=(16403, 10999)
    scale=(6.0964457721148569e-05, -9.0917356123283935e-05)
    angle=0.0
    significant_min=13.214923285329192

In the method Footprint.spatial_to_raster(self, xy, dtype=None, op=np.floor) -> array

Spatial to raster coordinates conversion

To convert spatial coordinates to pixel indices, floating point pixel coordinates should be rounded before the truncate operation. The floating point pixel coordinates are rounded to an abstract grid with a resolution lower or equal to the pixel grid, and aligned with the pixel grid.

# Check that user's `buzz.env.significant` is high enough
>>> significant = buzz.env.significant
>>> assert significant > self.significant_min


# Conversion from `significant` to `spatial_precision`
>>> largest_coord = np.abs(self.coords).max()
>>> spatial_precision = largest_coord * 10 ** -significant
>>> spatial_precision
0.00068762160872149

# Conversion from `spatial_precision` to `pixel_precision`
>>> smallest_reso = self.pxsize.min()
>>> pixel_precision = spatial_precision / smallest_reso
>>> pixel_precision
0.03438108043615017

# Compute the size of the abstract grid relative to the pixel grid
>>> abstract_grid_density = np.floor(1 / pixel_precision)
>>> abstract_grid_density
29.


>>> input_spatial
array([[  672788.18868577585089952,  6876215.24721489660441875],
       [  672788.1890306033892557 ,  6876215.24721489660441875],
       [  672788.18937543092761189,  6876215.24721489660441875],
       [  672788.18972025846596807,  6876215.24721489660441875],
       [  672788.19006508600432426,  6876215.24721489660441875],
       [  672788.19040991354268044,  6876215.24721489660441875]])

>>> input_pixel
array([[ 41.95000000298023224,  42.                 ],
       [ 41.96724137663841248,  42.                 ],
       [ 41.98448275029659271,  42.                 ],
       [ 42.00172413140535355,  42.                 ],
       [ 42.01896550506353378,  42.                 ],
       [ 42.03620688617229462,  42.                 ]])

>>> indices_float = np.around(input_pixel * abstract_grid_density, 0) / abstract_grid_density
>>> indices_float
array([[ 41.96551724137930961,  42.                 ],
       [ 41.96551724137930961,  42.                 ],
       [ 42.                 ,  42.                 ],
       [ 42.                 ,  42.                 ],
       [ 42.03448275862069039,  42.                 ],
       [ 42.03448275862069039,  42.                 ]])

>>> indices = np.floor(indices_float).astype(int)
>>> indices
array([[41, 42],
       [41, 42],
       [42, 42],
       [42, 42],
       [42, 42],
       [42, 42]])

In the method Footprint.equals(self, other) -> bool

Footprints equality

# Check that user's `buzz.env.significant` is high enough
>>> significant = buzz.env.significant
>>> assert significant > self.significant_min
>>> assert significant > other.significant_min

# Check the the raster sizes for equality
>>> (self.rsize == other.rsize).all()

# Compute the spatial precision
>>> largest_coord = np.abs(np.r_[self.coords, other.coords]).max()
>>> spatial_precision = largest_coord * 10 ** -significant

# Check that both footprints have their 4 extreme points within spatial_precision
>>> (np.abs(self.coords - other.coords) < spatial_precision).all()

In the method Footprint.same_grid(self, other) -> bool

Footprints' grid equality

# Check that user's `buzz.env.significant` is high enough
>>> significant = buzz.env.significant
>>> assert significant > self.significant_min
>>> assert significant > other.significant_min

# Compute the spatial precision
>>> largest_coord = np.abs(np.r_[self.coords, other.coords]).max()
>>> spatial_precision = largest_coord * 10 ** -significant

# Check that other's top-left lie on self's grid
>>> half_scale = self.scale / 2
>>> (np.abs(((self.tl - other.tl) + half_scale) % self.scale - half_scale) < spatial_precision).all()


# Check that self's height is compatible with other's grid
>>> (np.abs(self.tl + other.pxtbvec * self.rheight - self.bl) < spatial_precision).all()

# Check that self's width is compatible with other's grid
>>> (np.abs(self.tl + other.pxlrvec * self.rwidth - self.tr) < spatial_precision).all()

# Check that other's height is compatible with self's grid
>>> (np.abs(other.tl + self.pxtbvec * other.rheight - other.bl) < spatial_precision).all()

# Check that other's width is compatible with self's grid
>>> (np.abs(other.tl + self.pxlrvec * other.rwidth - other.tr) < spatial_precision).all()


# `.pxtbvec` is a spatial vector: (pixel bottom left - pixel top left)
# `.pxlrvec` is a spatial vector: (pixel top right - pixel top left)

In the method Footprint.move(self, tl, tr=None, br=None) -> bool

Move a Footprint to new coordinates

If 3 points are provided, the method must check that tl-tr-br is a right angle.

# Check that user's `buzz.env.significant` is high enough
>>> significant = buzz.env.significant
>>> assert significant > self.significant_min

# Compute the spatial precision
>>> largest_coord = np.abs(self.coords).max()
>>> spatial_precision = largest_coord * 10 ** -significant

>>> tl, tr, br
array([0., 100.]), array([100., 100.]), array([100.0001, 0.])

>>> angle = angle_between(tl, tr, br)
>>> angle
90.0000572958



# Compute the maximum error vector in which br should lie
>>> slack_br = self.pxlrvec / self.pxsizex * spatial_precision
>>> slack_br
array([ 0.00068762160872149,  0.                 ])

# Compute the angles accounting for the error vector
>>> slack_br_angles = np.asarray([
    angle_between(tl, tr, br + slack_br),
    angle_between(tl, tr, br - slack_br),
])
>>> slack_br_angles
array([ 90.00045127394031397,  89.99966331761871174])

# Check that ones of those angle is below 90 and the other above 90
>>> np.prod(np.sign(slack_br_angles - 90)) == -1
True



# Repeat the same steps for tl
>>> slack_tl = self.pxtbvec / self.pxsizey * spatial_precision
>>> slack_tl
array([ 0.                 , -0.00068762160872149])

>>> slack_tl_angles = np.asarray([
    angle_between(tl + slack_tl, tr, br),
    angle_between(tl - slack_tl, tr, br),
])
array([ 89.99966331761871174,  90.00045127394031397])

>>> np.prod(np.sign(slack_tl_angles - 90)) == -1
True


# angle_between = lambda a, b, c: np.arccos(np.dot((a - b) / np.linalg.norm(a - b), (c - b) / np.linalg.norm(c - b))) / np.pi * 180.