-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: IDSSE-256: GridProj #5
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
The only thing I wonder about is the mix of EXAMPLE data values defined at the top of unit test and literal values found in many test functions. To me there are two styles and I don't see the distinction. Not that the code needs to change, I was just wondering the reason.
Pixels are only identified by whole numbers when graphically rendered, | ||
so the transformed numerical values (floats) can be safely rounded to ints | ||
""" | ||
return round((x - self._x_offset) / self._dx), round((y - self._y_offset) / self._dy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Geary-Layne it's safe to assume that pixels can be rounded integers, correct? From my general understanding of how browsers actually paint onto a display, a fraction of a pixel is not possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ask because unit testing uncovered this strange precision behavior, before I made the change here to round the resulting pixel. I don't think this extra precision is meaningful (or comprehendible) for a browser client, but I could be wrong:
geo_x, geo_y = self.map_pixel_to_geo(0, 1)
pixel_x, pixel_y = self.map_geo_to_pixel(geo_x, geo_y)
print(pixel_x, pixel_y) # (0.0 0.9999999999951331)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct that for some uses, an non integer pixel value is not meaningful and is problematic. There are however some use case for this code (now that it is a general tool and not specific to the slice operator) where you would want floating point numbers. Also, sometime the use would dictate round and sometime floor.
In you test code the precision error is totally executable, and you should us approx().
When I'd implement code like this class in Java and python in the past, I supported an option argument with _to_pixel() function. Something like:
def map_geo_to_pixel(self, x: float, y: float, round: enum=None)
where round can be None, Round, Floor.
Not sure the best way to do this in python, I think I used magic strings before which isn't the best way, and actually making enum seems excessive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel like Enum is overkill here. It makes the comparison easy, although I didn't find an elegant way to directly map, and invoke, Enum members to functions.
Simple if statements seemed sufficient for now, and default to no rounding as previously defined in the data-access-service version of GridProj.
return (round(pixel_x), round(pixel_y)) | ||
if rounding is PixelRounding.FLOOR: | ||
return (floor(pixel_x), floor(pixel_y)) | ||
return (pixel_x, pixel_y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Geary-Layne I added a rounding
argument to map_geo_to_pixel that is either ROUND, FLOOR or None, is this about what you envisioned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Biggest thing is the need to update the rounding logic. Also make sure that the result from rounding are ints vs float that represent int values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found two real comments and a couple nice to haves.
from pyproj.enums import TransformDirection | ||
|
||
|
||
class PixelRounding(Enum): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to have: could we make this so the caller to _to_pixel could use strings, say 'ROUND', 'round', 'FLOOR', or 'floor'?
int(grid_args['w']), int(grid_args['h']), | ||
grid_args['dx'], grid_args['dy']) | ||
|
||
def map_proj_to_pixel(self, x, y) -> Tuple[float, float]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be best of all _to_pixel functions supported rounding, so add the same logic to map_proj_to_pixel() as map_geo_to_pixel() has. It might be good to create a private function to round so that both _to_pixel functions can call the same code.
"""Map pixel to geographic coordinates""" | ||
return x * self._dx + self._x_offset, y * self._dy + self._y_offset | ||
|
||
def map_geo_to_proj(self, x: float, y: float) -> Tuple[Any, Any]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to have: the return type of a tuple of Any is less helpful than Tuple[Union[int, float], Union[int, float]]. I think that is the correct syntax.
pixel_y = (y - self._y_offset) / self._dy | ||
|
||
if rounding is PixelRounding.ROUND: | ||
return (round(pixel_x), round(pixel_y)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python 3.x switch rounding logic from 2.x and it is not what we want to use here. In 3.x the rounding logic is "round half to even" and it is better for our use to "round half away from zero". In place of round() use "return floor(x + 0.5) if x >= 0.0 else ceil(x - 0.5)". This works since we are always round to the ones place, it is more complex if we needed to round to a specified diget.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, didn't know our requirements were rounding away from zero instead of to the nearest even. From my reading, numpy.round()
behaves the same as Python 3.x round()
.
IEEE-754: floating-point rounding
Do we already have a utility function for this rounding technique? Otherwise the one we're making here should probably be used across IDSS projects for consistency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe we have a utility function for rounding. It would be straight forward to add a "round half away from zero, to the ones place" to utils.py. It would be better to implement to round to an optional decimal place. If you would like to do this (either one) and have GridProj call it, that would be great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, doing that now. Created a sub-task in linear to track this (IDSSE-306).
Tuple[Union[int, float], Union[int, float]): | ||
x, y values for pixel, rounded to ints if rounding parameter passed, otherwise floats | ||
""" | ||
pixel_x = (x - self._x_offset) / self._dx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably just my preference so feel free to ignore. I like to use i,j for pixel space vs x,y.
|
||
# convert geographic coordinates back to pixel, full circle, and data should be unchanged | ||
pixel_x, pixel_y = grid_proj.map_geo_to_pixel(geo_x, geo_y) | ||
assert (round(pixel_x, 6), round(pixel_y, 6)) == initial_pixel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will have to change when you update the round logic in grid_proj.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the test now be using round_half_away, or is round correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was still using the incorrect round function, good catch. Fixed now.
|
||
|
||
# transformation methods testing | ||
def test_map_proj_to_pixel(grid_proj: GridProj): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should include testing rounding, once you add rounding option to map_proj_to_pixel().
This PR is now blocked by #7 which contains the rounding utility function we need here. |
@Geary-Layne I remembered now that in the So map_proj_to_pixel() would just be rounding an already-rounded float. Instead it passes the rounding arguments ( I can refactor that to be less confusing if you think of a better way. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rounding function looks good.
It makes me happy to see our test code being build out.
@@ -83,8 +84,7 @@ def exec_cmd(commands: Sequence[str], timeout: Optional[int] = None) -> Sequence | |||
Sequence[str]: Result of executing the commands | |||
""" | |||
logger.debug('Making system call %s', commands) | |||
# with Popen(commands, stdout=PIPE, stderr=PIPE) as proc: | |||
# out = proc.readlines() | |||
# pylint: disable=consider-using-with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you decide to not use "with Popen"? In the current implementation is process ever closed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I assumed it was commented out for a reason and simply silenced the warning. We can re-enable those lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not clear to me the correct way to use subprocess. Maybe proc.kill() actually frees underlying resources, but if that is the case there is a few line lower that I'm not sure will always work. I think it good enough for now, but maybe we should create a task to figure out the correct to leverage subprocess.
|
||
# convert geographic coordinates back to pixel, full circle, and data should be unchanged | ||
pixel_x, pixel_y = grid_proj.map_geo_to_pixel(geo_x, geo_y) | ||
assert (round(pixel_x, 6), round(pixel_y, 6)) == initial_pixel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the test now be using round_half_away, or is round correct?
Linear Task
IDSSE-256
Changes
GridProj
class from data-access-service to this projectint
s. By default, return pixel values asfloat
s.