From 7ba0637a2b30138c1fb11c285aeae64215aeb233 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Wed, 6 Dec 2023 07:05:31 -0700 Subject: [PATCH 01/11] Upgrade to mojo 0.6, mojo-pytest latest. --- environment.yml | 2 +- geo_features/geom/envelope.mojo | 4 ++-- geo_features/geom/point.mojo | 4 ++-- geo_features/test/geom/test_multi_point.mojo | 2 +- geo_features/test/pytest.mojo | 5 ++++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/environment.yml b/environment.yml index fd6fb85..6bdc149 100644 --- a/environment.yml +++ b/environment.yml @@ -13,4 +13,4 @@ dependencies: - pip: - geoarrow-pyarrow - geoarrow-pandas - - git+https://github.com/guidorice/mojo-pytest.git@v0.2.0 + - git+https://github.com/guidorice/mojo-pytest.git@v0.6.0 diff --git a/geo_features/geom/envelope.mojo b/geo_features/geom/envelope.mojo index f14a49d..9e80f0c 100644 --- a/geo_features/geom/envelope.mojo +++ b/geo_features/geom/envelope.mojo @@ -144,8 +144,8 @@ struct Envelope[dims: Int = 2, dtype: DType = DType.float64]: fn southwesterly_point(self) -> Point[dims, dtype]: let coords = self.coords.slice[dims](0) - return Point[dims, dtype](coords ^) + return Point[dims, dtype](coords) fn northeasterly_point(self) -> Point[dims, dtype]: let coords = self.coords.slice[dims](dims) - return Point[dims, dtype](coords ^) + return Point[dims, dtype](coords) diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index cf063d0..7e62fd2 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -53,7 +53,7 @@ Alias for 4D point with dtype float64, including Z (height) and M (measure) dime @value @register_passable("trivial") -struct Point[simd_dims: Int = 2, dtype: DType = DType.float64]: +struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement): """ Point is a register-passable, copy-efficient struct holding 2 or more dimension values. """ @@ -175,7 +175,7 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64]: let chunk = geoarrow[0] let n = chunk.value.__len__() if n > simd_dims: - raise Error("Invalid Point dims parameter vs. geoarrow: " + n.to_string()) + raise Error("Invalid Point dims parameter vs. geoarrow: " + str(n)) var result = Self() for dim in range(n): let val = chunk.value[dim].as_py().to_float64().cast[dtype]() diff --git a/geo_features/test/geom/test_multi_point.mojo b/geo_features/test/geom/test_multi_point.mojo index 93e75ce..acd6ef3 100644 --- a/geo_features/test/geom/test_multi_point.mojo +++ b/geo_features/test/geom/test_multi_point.mojo @@ -72,7 +72,7 @@ fn test_constructors() raises: test.assert_true(mpt_z[2] == PointM(lon, lat + 2, height + 6), "non power of two dims constructor/2") test.assert_true(mpt_z.__len__() == 4, "non power of two dims constructor/len") - let mpt_zm = MultiPoint[dims=4, point_simd_dims=4]( + let mpt_zm = MultiPoint[dims=4, point_simd_dims=4]( PointZM(lon, lat, height, measure), PointZM(lon, lat + 1, height + 3, measure + 5), PointZM(lon, lat + 2, height + 2, measure + 6), diff --git a/geo_features/test/pytest.mojo b/geo_features/test/pytest.mojo index 4398fda..2fcd0e0 100644 --- a/geo_features/test/pytest.mojo +++ b/geo_features/test/pytest.mojo @@ -17,4 +17,7 @@ struct MojoTest: """ Wraps testing.assert_true. """ - _ = testing.assert_true(cond, message) + try: + testing.assert_true(cond, message) + except e: + print(e) From e97bc53b235d980fa3853b4f801bcd67d3be912c Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Sat, 9 Dec 2023 07:01:18 -0700 Subject: [PATCH 02/11] (wip) add traits, integrate with mojo 0.6 features. --- Makefile | 2 +- README.md | 2 +- docs/docstrings.monopic | Bin 0 -> 1948 bytes geo_features/geom/__init__.mojo | 4 +- geo_features/geom/_utils.mojo | 23 ++ geo_features/geom/envelope.mojo | 6 +- geo_features/geom/layout.mojo | 4 +- geo_features/geom/line_string.mojo | 75 ++++-- geo_features/geom/multi_point.mojo | 105 +++++--- geo_features/geom/point.mojo | 252 +++++++++++++------ geo_features/geom/traits.mojo | 0 geo_features/serialization/json.mojo | 9 +- geo_features/test/geom/test_envelope.mojo | 6 +- geo_features/test/geom/test_line_string.mojo | 21 +- geo_features/test/geom/test_multi_point.mojo | 59 ++--- geo_features/test/geom/test_point.mojo | 105 ++++---- 16 files changed, 416 insertions(+), 257 deletions(-) create mode 100644 docs/docstrings.monopic create mode 100644 geo_features/geom/_utils.mojo create mode 100644 geo_features/geom/traits.mojo diff --git a/Makefile b/Makefile index 496a844..eff084d 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ install-py-packages: conda env create -p venv --file environment.yml clean: - rm -rf ~/.modular/.mojo_cache + rm -rf ~/.modular/.mojo_cache build/geo_features.mojopkg test: pytest -W error diff --git a/README.md b/README.md index 4777b3d..b55794f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If you are interested in contributing or discussing, please first contact me by ## requirements -- [Mojo](https://github.com/modularml/mojo) >= 0.5.0 +- [Mojo](https://github.com/modularml/mojo) >= 0.6 - [Python](https://www.python.org/) >= 3.9 - [Conda](https://docs.conda.io/en/latest/) diff --git a/docs/docstrings.monopic b/docs/docstrings.monopic new file mode 100644 index 0000000000000000000000000000000000000000..5f1d0bb392d5aa689af75920883a5029554e524a GIT binary patch literal 1948 zcmV;N2V?mEO;1iwP)S1pABzY8000000u$|A&2Hm15PlUwXD=ZBip;eIiURFkdfW{R z*_Lf}Z7P+N-Axw(`ZRs8K1oSRBU_H-jjV93#6HAOWN|ng&V1jHL;0JZ&i;(&Meg5x zGneluHCKytKF#8#e`D_|g z*|l6FVg6~=*4}6ssnz`^%Bo)U_Y;4aMca59srfq1_C^&_-9e>{?Q%NH%3d2cjV;a1 zO*;Q%hPRA1dE929b+TB*s(xTzth4lfwXSc>BCWQ*rC^S6<6f-HFl+l2(W>bMcxRw= zxwJ0h2a~{u@#V?iTBeWj#EdJypXrBnQ>|lOPJY$6?g#X&=I=mg!vW^Q4|Jh5l3w*ut0?+f%N3}?{vC6g2+<4wsee1#ds6MJ%<9V7XGeu~p1UPRwK~g`O zX2~j1W?&O*<+AC4ve3N6Cz=ynsa%;NF|Bl*K3F%|4_RZfos-xL1{WMILbwqAZQao# z%GWy^Yap<*cyB*b{rPxk#ZAMsrg=Rz~fK5d!;J~N@B5^PjBK7RJ0(NXUW0R=(V3I2%9sorB zV22kRz(q%N_M5E^EUt&gnm&aqE%V~R04&*ywlb~&gsKq#O+V}zYqo|8flwh3Dg;7> zgeF4l+^MvPO2gx}sXpT4M}j<$S6lOy9(7YC)=;1Y6!(B<8{O?GL!{q@x2@=54OyC% zv@sc|*ig>`#}lGBBt39#>4yDnFh-j@-J=;K0w2lazozPbJBu?bPuQ=x{Y3T?+fQ0X zNUfJ~!$^IO^2SgD{}#e1>>Z)_nGwo@?~2~ZK}JquY~yPPKt5R626;1-&k?Gm8LDD0 zHs7I%d>Wj;XeYXBjn3N_477s?D*2!aGU?y;3Yl719&aGGY}#!vevU?oy#-D{I>?3L zq2$8&l>v@100+G1kQ#CD4KB~a06R@&Uj_(~dj`-euO1A*q%-IgcsO*DptpMRx$3cw zpcZs!P!o$6`Ik5@Lo`vk<#ZD-%jSE^wsllKU8H5(7RQV>$x3Z42M1 z;En;Dtf@{`7bpnoD|8-++tUU0#W)lY_aKJ?+y~{%FxrE?H1@D)=O^PFt#Qs#BF?ch z{KT&!t3_l~X)Of17)N4@PF>whFnBI!Z=CC%Y&#l?(|`LUhe+=*3yk~-r})K$zw!;5)%Fr3Tq0+udq z3zxr}%NEL%=#3X2^m?T8S^(;Xr`kmG+hRst9Tb3A79SjKu2`;PBOz^-;GkF#3-e642-Vpos@t!d(d$Dj<(Pv#7 zO>FQ!6Qd`QsO&>@C>$Blq2Nk%PKmBH7w$uJh@GG4P`zW0-~u4WZUEW~dRat=_&}l) zft&RN3;Q4vXCp%-Caz~Zf7}_*{ms_rA`&O(M)3iQC`!>k*AMx1nHv$XqEuY64FNr>7%@` z^id8?A4%8&*7gYZAh70vv-P_l8|fcNfmW43I%W z+6MU}`~XM&qd9$f<8*T1)Y0!{xZAAauOcU5V@gRdcp7G-NJ)5CC4~>Jq{!hf03t^j ziFEf)9eRm97}TdQPUXl7;}pNe!Z-=Xaf<259#fx+I1SHV5vM@Dv}RubMGE{&`DIna isTf!hmoNUZ;$w;M#+$zRpEb!oKK>6`b SIMD[dtype, 1]: + """ + Define a special value to mark empty slots or dimensions in structs. Required because SIMD must be power of two. + """ + + @parameter + if dtype.is_floating_point(): + return nan[dtype]() + else: + return max_finite[dtype]() + + +@always_inline +fn is_empty[dtype: DType, simd_dims: Int](value: SIMD[dtype, simd_dims]) -> Bool: + """ + Check for empty value. + """ + return value == empty_value[dtype]() diff --git a/geo_features/geom/envelope.mojo b/geo_features/geom/envelope.mojo index 9e80f0c..8680f44 100644 --- a/geo_features/geom/envelope.mojo +++ b/geo_features/geom/envelope.mojo @@ -79,11 +79,11 @@ struct Envelope[dims: Int = 2, dtype: DType = DType.float64]: @parameter fn min_max_simd[simd_width: Int](feature_idx: Int): let index = Index(dim, feature_idx) - let vals = data.coordinates.simd_load[simd_width](index) - let min = vals.reduce_min() + let values = data.coordinates.simd_load[simd_width](index) + let min = values.reduce_min() if min < coords[dim]: coords[dim] = min - let max = vals.reduce_max() + let max = values.reduce_max() if max > coords[dims + dim]: coords[dims + dim] = max diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index f55fb14..c4c34f5 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -3,7 +3,9 @@ from tensor import Tensor @value -struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]: +struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( + Sized +): """ Memory layout inspired by, but not exactly following, the GeoArrow format. diff --git a/geo_features/geom/line_string.mojo b/geo_features/geom/line_string.mojo index e7c64cf..d40f95d 100644 --- a/geo_features/geom/line_string.mojo +++ b/geo_features/geom/line_string.mojo @@ -9,8 +9,31 @@ from .point import Point from .layout import Layout +alias LineString2 = LineString[simd_dims=2, dtype = DType.float64] +""" +Alias for 2D LineString with dtype float64. +""" + +alias LineStringZ = LineString[simd_dims=4, dtype = DType.float64] +""" +Alias for 3D LineString with dtype float64, including Z (height) dimension. +Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). +""" + +alias LineStringM = LineString[simd_dims=4, dtype = DType.float64] +""" +Alias for 3D LineString with dtype float64, including M (measure) dimension. +Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). +""" + +alias LineStringZM = LineString[simd_dims=4, dtype = DType.float64] +""" +Alias for 4D LineString with dtype float64, including Z (height) and M (measure) dimension. +""" + + @value -struct LineString[dims: Int = 2, dtype: DType = DType.float64]: +struct LineString[simd_dims: Int, dtype: DType]: """ Models an OGC-style LineString. @@ -25,31 +48,39 @@ struct LineString[dims: Int = 2, dtype: DType = DType.float64]: - If these conditions are not met, the constructors raise an Error. """ - var data: Layout[dtype] + var data: Layout[coord_dtype=dtype] fn __init__(inout self): """ Create empty linestring. """ - self.data = Layout[coord_dtype=dtype](dims=dims) + self.data = Layout[coord_dtype=dtype]() - fn __init__(inout self, *points: Point[dims, dtype]): + fn __init__(inout self, *points: Point[simd_dims, dtype]): """ Create LineString from a variadic (var args) list of Points. """ let n = len(points) - var v = DynamicVector[Point[dims, dtype]](n) + var v = DynamicVector[Point[simd_dims, dtype]](n) for i in range(n): v.push_back(points[i]) self.__init__(v) - fn __init__(inout self, points: DynamicVector[Point[dims, dtype]]): + fn __init__(inout self, points: DynamicVector[Point[simd_dims, dtype]]): """ Create LineString from a vector of Points. """ # here the geometry_offsets, part_offsets, and ring_offsets are unused because # of using "struct coordinate representation" (tensor) let n = len(points) + + if n == 0: + # empty linestring + self.data = Layout[coord_dtype=dtype]() + return + + let sample_pt = points[0] + let dims = len(sample_pt) self.data = Layout[coord_dtype=dtype]( dims=dims, coords_size=n, geoms_size=0, parts_size=0, rings_size=0 ) @@ -63,18 +94,19 @@ struct LineString[dims: Int = 2, dtype: DType = DType.float64]: fn __len__(self) -> Int: return self.data.coordinates.shape()[1] - @always_inline + fn dims(self) -> Int: + return self.data.coordinates.shape()[0] + fn __eq__(self, other: Self) -> Bool: return self.data == other.data - @always_inline fn __ne__(self, other: Self) -> Bool: return not self.__eq__(other) fn __repr__(self) -> String: return ( "LineString[" - + String(dims) + + String(self.dims()) + ", " + dtype.__str__() + "](" @@ -82,20 +114,13 @@ struct LineString[dims: Int = 2, dtype: DType = DType.float64]: + " points)" ) - @always_inline - fn __getitem__( - self: Self, feature_index: Int - ) -> Point[simd_dims=dims, dtype=dtype]: + fn __getitem__(self: Self, feature_index: Int) -> Point[simd_dims, dtype]: """ Get Point from LineString at index. """ - var result = Point[dims, dtype]() - - @unroll - for dim_index in range(dims): - result.coords[dim_index] = self.data.coordinates[ - Index(dim_index, feature_index) - ] + var result = Point[simd_dims, dtype]() + for i in range(self.dims()): + result.coords[i] = self.data.coordinates[Index(i, feature_index)] return result fn is_valid(self, inout err: String) -> Bool: @@ -126,19 +151,19 @@ struct LineString[dims: Int = 2, dtype: DType = DType.float64]: var json_coords = json_dict.get("coordinates", Python.none()) if not json_coords: raise Error("LineString.from_json(): coordinates property missing in dict.") - var points = DynamicVector[Point[dims, dtype]]() + var points = DynamicVector[Point[simd_dims, dtype]]() for coords in json_coords: let lon = coords[0].to_float64().cast[dtype]() let lat = coords[1].to_float64().cast[dtype]() - let pt = Point[dims, dtype](lon, lat) + let pt = Point[simd_dims, dtype](lon, lat) points.push_back(pt) - return LineString[dims, dtype](points) + return LineString[simd_dims, dtype](points) @staticmethod fn from_wkt(wkt: String) raises -> Self: # TODO: impl from_wkt() # raise Error("not implemented") - return LineString[dims, dtype]() + return LineString[simd_dims, dtype]() fn __str__(self) -> String: return self.wkt() @@ -162,6 +187,7 @@ struct LineString[dims: Int = 2, dtype: DType = DType.float64]: """ var res = String('{"type":"LineString","coordinates":[') let len = self.__len__() + let dims = self.dims() for feature_index in range(len): let pt = self[feature_index] res += "[" @@ -187,6 +213,7 @@ struct LineString[dims: Int = 2, dtype: DType = DType.float64]: """ if self.is_empty(): return "LINESTRING EMPTY" + let dims = self.dims() var res = String("LINESTRING(") let len = self.__len__() for i in range(len): diff --git a/geo_features/geom/multi_point.mojo b/geo_features/geom/multi_point.mojo index 2f97d5a..d147369 100644 --- a/geo_features/geom/multi_point.mojo +++ b/geo_features/geom/multi_point.mojo @@ -6,51 +6,70 @@ from memory import memcmp from geo_features.serialization import WKTParser, JSONParser from .point import Point from .layout import Layout +from ._utils import is_empty, empty_value -struct MultiPoint[dims: Int=2, point_simd_dims: Int = 2, dtype: DType = DType.float64]: - """ - Models an OGC-style MultiPoint. Any collection of Points is a valid MultiPoint. +alias MultiPoint2 = MultiPoint[simd_dims=2, dtype = DType.float64] +""" +Alias for 2D MultiPoint with dtype float64. +""" + +alias MultiPointZ = MultiPoint[simd_dims=4, dtype = DType.float64] +""" +Alias for 3D MultiPoint with dtype float64, including Z (height) dimension. +Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). +""" + +alias MultiPointM = MultiPoint[simd_dims=4, dtype = DType.float64] +""" +Alias for 3D MultiPoint with dtype float64, including M (measure) dimension. +Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). +""" + +alias MultiPointZM = MultiPoint[simd_dims=4, dtype = DType.float64] +""" +Alias for 4D MultiPoint with dtype float64, including Z (height) and M (measure) dimension. +""" - Note: we do not support [heterogeneous dimension multipoints](https://geoarrow.org/format). + +struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): + """ + Models an OGC-style MultiPoint. Any collection of Points is a valid MultiPoint, + except [heterogeneous dimension multipoints](https://geoarrow.org/format) which are unsupported. """ var data: Layout[coord_dtype=dtype] - fn __init(inout self): + fn __init__(inout self): """ Create empty MultiPoint. """ - @parameter - constrained[point_simd_dims >= dims, "Parameter point_simd_dims to small for MultiPoint dims"]() - - self.data = Layout[coord_dtype=dtype](dims) + self.data = Layout[coord_dtype=dtype]() - fn __init__(inout self, *points: Point[point_simd_dims, dtype]): + fn __init__(inout self, *points: Point[simd_dims, dtype]): """ Create MultiPoint from a variadic (var args) list of Points. """ - @parameter - constrained[point_simd_dims >= dims, "Parameter point_simd_dims to small for MultiPoint dims"]() - let n = len(points) - var v = DynamicVector[Point[point_simd_dims, dtype]](n) + var v = DynamicVector[Point[simd_dims, dtype]](n) for i in range(n): v.push_back(points[i]) self.__init__(v) - fn __init__(inout self, points: DynamicVector[Point[point_simd_dims, dtype]]): + fn __init__(inout self, points: DynamicVector[Point[simd_dims, dtype]]): """ Create MultiPoint from a vector of Points. """ - @parameter - constrained[point_simd_dims >= dims, "Parameter point_simd_dims to small for MultiPoint dims"]() - let n = len(points) + if len(points) == 0: + # create empty Multipoint + self.data = Layout[coord_dtype=dtype]() + return + let sample_pt = points[0] + let dims = len(sample_pt) self.data = Layout[dtype]( - dims=dims, - coords_size=n, geoms_size=0, parts_size=0, rings_size=0 + dims=dims, coords_size=n, geoms_size=0, parts_size=0, rings_size=0 ) for y in range(dims): for x in range(len(points)): @@ -71,8 +90,10 @@ struct MultiPoint[dims: Int=2, point_simd_dims: Int = 2, dtype: DType = DType.fl # TODO: impl from_wkt raise Error("not implemented") - @always_inline fn __len__(self) -> Int: + """ + Returns the number of point elements. + """ return self.data.coordinates.shape()[1] fn __eq__(self, other: Self) -> Bool: @@ -82,35 +103,37 @@ struct MultiPoint[dims: Int=2, point_simd_dims: Int = 2, dtype: DType = DType.fl return not self.__eq__(other) fn __repr__(self) -> String: + let dims = self.data.dims() return ( "MultiPoint[" + String(dims) + ", " - + dtype.__str__() + + str(dtype) + "](" - + String(self.__len__()) + + String(len(self)) + " points)" ) - @always_inline - fn __getitem__(self: Self, feature_index: Int) -> Point[simd_dims=point_simd_dims, dtype=dtype]: + fn __getitem__(self: Self, feature_index: Int) -> Point[simd_dims, dtype]: """ Get Point from MultiPoint at index. """ - var point = Point[point_simd_dims, dtype]() + let dims = self.data.dims() + var point = Point[simd_dims, dtype]() - @unroll for dim_index in range(dims): point.coords[dim_index] = self.data.coordinates[ Index(dim_index, feature_index) ] - @parameter - if point_simd_dims >= 4: - if dims == 3: - # Handle case where because of memory model, cannot distinguish a PointZ from a PointM. - # Just copy the value between dim 3 and 4. - point.coords[3] = point[2] + # TODO: handle 3 dim cases + + # @parameter + # if point_simd_dims >= 4: + # if dims == 3: + # # Handle case where because of memory model, cannot distinguish a PointZ from a PointM. + # # Just copy the value between dim 3 and 4. + # point.coords[3] = point[2] return point @@ -136,9 +159,10 @@ struct MultiPoint[dims: Int=2, point_simd_dims: Int = 2, dtype: DType = DType.fl } ``` """ + let n = len(self) + let dims = self.data.dims() var res = String('{"type":"MultiPoint","coordinates":[') - let len = self.__len__() - for feature_index in range(len): + for feature_index in range(n): let pt = self[feature_index] res += "[" for dim_index in range(3): @@ -148,7 +172,7 @@ struct MultiPoint[dims: Int=2, point_simd_dims: Int = 2, dtype: DType = DType.fl if dim_index < 2 and dim_index < dims - 1: res += "," res += "]" - if feature_index < len - 1: + if feature_index < n - 1: res += "," res += "]}" return res @@ -163,18 +187,19 @@ struct MultiPoint[dims: Int=2, point_simd_dims: Int = 2, dtype: DType = DType.fl """ if self.is_empty(): return "MULTIPOINT EMPTY" + let dims = self.data.dims() var res = String("MULTIPOINT(") - let len = self.__len__() - for i in range(len): + let n = len(self) + for i in range(n): let pt = self[i] for j in range(dims): res += pt.coords[j] if j < dims - 1: res += " " - if i < len - 1: + if i < n - 1: res += ", " res += ")" return res fn is_empty(self) -> Bool: - return self.__len__() == 0 + return len(self) == 0 diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index 7e62fd2..7c2574f 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -2,60 +2,131 @@ from python import Python from math import nan, isnan from math.limit import max_finite +from ._utils import empty_value, is_empty + from geo_features.serialization import WKTParser, JSONParser -alias Point2 = Point[simd_dims=2, dtype=DType.float64] +alias Point2 = Point[simd_dims=2, dtype=DType.float64, T=PointEnum.Point] """ Alias for 2D point with dtype float64. """ -alias PointZ = Point[simd_dims=4, dtype=DType.float64] +alias PointZ = Point[simd_dims=4, dtype=DType.float64, T=PointEnum.PointZ] """ Alias for 3D point with dtype float64, including Z (height) dimension. Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). """ -alias PointM = Point[simd_dims=4, dtype=DType.float64] +alias PointM = Point[simd_dims=4, dtype=DType.float64, T=PointEnum.PointM] """ Alias for 3D point with dtype float64, including M (measure) dimension. Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). """ -alias PointZM = Point[simd_dims=4, dtype=DType.float64] +alias PointZM = Point[simd_dims=4, dtype=DType.float64, T=PointEnum.PointZM] """ Alias for 4D point with dtype float64, including Z (height) and M (measure) dimension. """ -# struct PointEnum: -# TODO is this PointEnum even needed? -# """ -# Enum for expressing the variants of Points as either parameter values or runtime args. -# """ -# var value: SIMD[DType.uint8, 1] - -# alias Point = PointEnum(0) -# """ -# 2 dimensional Point. -# """ -# alias PointZ = PointEnum(1) -# """ -# 3 dimensional Point, has height or altitude (Z). -# """ -# alias PointM = PointEnum(2) -# """ -# 3 dimensional Point, has measure (M). -# """ -# alias PointZM = PointEnum(3) -# """ -# 4 dimensional Point, has height and measure (ZM) -# """ +@register_passable("trivial") +struct PointEnum(Stringable): + """ + Enum for expressing the variants of Points as either parameter values or runtime args. + """ + var value: SIMD[DType.uint8, 1] + alias Point = PointEnum(0) + """ + 2 dimensional Point. + """ + alias PointZ = PointEnum(1) + """ + 3 dimensional Point, has height or altitude (Z). + """ + alias PointM = PointEnum(2) + """ + 3 dimensional Point, has measure (M). + """ + alias PointZM = PointEnum(3) + """ + 4 dimensional Point, has height and measure (ZM) + """ + + alias PointN = PointEnum(4) + """ + 5 or higher dimensional Point. + """ + + fn __init__(value: Int) -> Self: + return Self { value: value } + + fn __eq__(self, other: Self) -> Bool: + return self.value == other.value + + fn __str__(self) -> String: + if self == PointEnum.Point: + return "Point" + if self == PointEnum.PointZ: + return "PointZ" + if self == PointEnum.PointM: + return "PointM" + if self == PointEnum.PointZM: + return "PointZM" + return "PointN" @value @register_passable("trivial") -struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement): +struct Point[simd_dims: Int = 2, dtype: DType = DType.float64, T: PointEnum = PointEnum.Point]( + CollectionElement, + Sized, + Stringable +): """ Point is a register-passable, copy-efficient struct holding 2 or more dimension values. + + ### Parameters + + - simd_dims: constrained to power of two (default = 2) + - dtype: supports any float or integer type (default = float64) + - T: enum type, ex: Point, PointZ, PointM, PointZM, or POINTN + + ### Memory Layouts + +```txt + + Point2 Empty Points are either NaN (float) max-finite (int). +┌───────────┬────────┐ ┌───────────┬────────┐ +│SIMD index:│0 1 │ │SIMD index:│0 1 │ +│dimension: │x y │ │dimension: │NaN NaN │ +└───────────┴────────┘ └───────────┴────────┘ + +PointM (measure) +┌───────────┬──────────┐ +│SIMD index:│0 1 2 3 │ +│dimension: │x y NaN m │ +└───────────┴──────────┘ + +PointZ (height) +┌───────────┬──────────┐ +│SIMD index:│0 1 2 3 │ +│dimension: │x y z NaN │ +└───────────┴──────────┘ + +PointZM (height and measure) +┌───────────┬──────────┐ +│SIMD index:│0 1 2 3 │ +│dimension: │x y z m │ +└───────────┴──────────┘ + +PointN (n-dimensional point) +┌───────────┬───────────────────────┐ +│SIMD index:│0 1 2 3 4 5 6 7 │ +│dimension: │n0 n1 n2 n3 n4 n5 n6 n7│ +└───────────┴───────────────────────┘ +. +``` + + """ var coords: SIMD[dtype, simd_dims] @@ -67,24 +138,21 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement @parameter constrained[simd_dims % 2 == 0, "dims must be power of two"]() - @parameter - if dtype.is_floating_point(): - let coords = SIMD[dtype, simd_dims](nan[dtype]()) - return Self{ coords: coords } - else: - let coords = SIMD[dtype, simd_dims](max_finite[dtype]()) - return Self{ coords: coords } + let empty = empty_value[dtype]() + let coords = SIMD[dtype, simd_dims](empty) + return Self { coords: coords } fn __init__(*coords_list: SIMD[dtype, 1]) -> Self: """ - Create Point from variadic list of SIMD values. Any missing elements are padded with zeros. - Warning: it is not possible to distinguish a PointZ from a PointM in this implementation with SIMD dim 4. + Create Point from variadic list of SIMD values. Any missing elements are padded with empty values. """ + # TODO add arg for has_measure, has_height, so dimensions 3-4 can be encoded + @parameter constrained[simd_dims % 2 == 0, "dims must be power of two"]() - var result = Self() + var result = Self() # start with empty values let n = len(coords_list) debug_assert(n <= simd_dims, "coords_list length is longer than simd_dims parameter") @@ -93,41 +161,28 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement break result.coords[i] = coords_list[i] - @parameter - if simd_dims >= 4: - if n == 3: - # Handle case where because of memory model, cannot distinguish a PointZ from a PointM. - # Just copy the value between dim 3 and 4. - result.coords[3] = coords_list[2] - return result fn __init__(coords: SIMD[dtype, simd_dims]) -> Self: """ Create Point from existing SIMD vector of coordinates. - Warning: does not initialize unused dims with NaN values. """ @parameter constrained[simd_dims % 2 == 0, "dims must be power of two"]() - + return Self {coords: coords} fn has_height(self) -> Bool: + if T == PointEnum.Point or T == PointEnum.PointM: + return False alias z_idx = 2 - @parameter - if dtype.is_floating_point(): - return not isnan(self.coords[z_idx]) - else: - return self.coords[z_idx] != max_finite[dtype]() - + return not is_empty(self.coords[z_idx]) fn has_measure(self) -> Bool: + if T == PointEnum.Point or T == PointEnum.PointZ: + return False alias m_idx = 3 - @parameter - if dtype.is_floating_point(): - return not isnan(self.coords[m_idx]) - else: - return self.coords[m_idx] != max_finite[dtype]() + return not is_empty(self.coords[m_idx]) @staticmethod fn from_json(json_dict: PythonObject) raises -> Self: @@ -135,14 +190,17 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement Create Point from geojson (expect to have been parsed into a python dict). """ let json_coords = json_dict["coordinates"] - let coords_len = json_coords.__len__().to_float64().to_int() # FIXME: to_int workaround + let coords_len = int(json_coords.__len__()) var result = Self() - debug_assert( - simd_dims >= coords_len, "from_json() invalid dims vs. json coordinates" - ) - for i in range(coords_len): - result.coords[i] = json_coords[i].to_float64().cast[dtype]() + print(simd_dims, dtype, T) + print(len(result.coords)) + # print(str(result)) return result + # # TODO: bounds checking + # for i in range(coords_len): + # result.coords[i] = json_coords[i].to_float64().cast[dtype]() + # print(str(result)) + # return result @staticmethod fn from_json(json_str: String) raises -> Self: @@ -190,7 +248,7 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement ### See also - empty() and is_empty()- the zero point is not the same as empty point. + empty() and is_empty(). note, the zero point is not the same as an empty point! """ let coords = SIMD[dtype, simd_dims](0) return Self { coords: coords } @@ -214,10 +272,11 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement Get the z or altitude value (2 index). """ @parameter - constrained[simd_dims >= 3, "Point has no Z dimension"]() + constrained[simd_dims >= 4, "Point has no Z dimension"]() return self.coords[2] + fn alt(self) -> SIMD[dtype, 1]: """ Get the z or altitude value (2 index). @@ -229,9 +288,22 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement Get the measure value (3 index). """ @parameter - constrained[simd_dims >= 3, "Point has no M dimension"]() + constrained[simd_dims >= 4, "Point has no M dimension"]() return self.coords[3] + fn __len__(self) -> Int: + """ + Returns the number of non-empty dimensions. + """ + if len(self.coords > 4): + return len(self.coords) + var dims = 2 + if self.has_height(): + dims += 1 + if self.has_measure(): + dims += 1 + return dims + fn __getitem__(self, d: Int) -> SIMD[dtype, 1]: """ Get the value of coordinate at this dimension. @@ -244,8 +316,26 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement fn __ne__(self, other: Self) -> Bool: return not self.__eq__(other) + fn enum(self) -> PointEnum: + """ + Describe point's type as a PointEnum. + """ + if self.has_height() and self.has_measure(): + return PointEnum.PointZM + if self.has_height(): + return PointEnum.PointZ + if self.has_measure(): + return PointEnum.PointM + return PointEnum.Point + fn __repr__(self) -> String: - var res = "Point[" + String(simd_dims) + ", " + dtype.__str__() + "](" + let desc = str(self.enum()) + var res = desc + + "[simd_dims:" + + String(simd_dims) + + ", dtype:" + + dtype.__str__() + + "](" for i in range(simd_dims): res += self.coords[i] if i < simd_dims - 1: @@ -266,14 +356,16 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement - https://geojson.org - https://datatracker.ietf.org/doc/html/rfc7946 """ - # include only x, y, and optionally z (altitude) + # include only x, y, and optionally z (height) var res = String('{"type":"Point","coordinates":[') - for i in range(3): + let dims = 3 if self.has_height() else 2 + for i in range(dims): if i > simd_dims - 1: break res += self.coords[i] - if i < 2 and i < simd_dims - 1: + if i < dims - 1: res += "," + print(res) res += "]}" return res @@ -287,19 +379,13 @@ struct Point[simd_dims: Int = 2, dtype: DType = DType.float64](CollectionElement """ if self.is_empty(): return "POINT EMPTY" - - var res = String("POINT(") + var result = str(self.enum()) for i in range(simd_dims): - res += self.coords[i] + result += self.coords[i] if i < simd_dims - 1: - res += " " - res += ")" - return res + result += " " + result += ")" + return result fn is_empty(self) -> Bool: - @parameter - if dtype.is_floating_point(): - return isnan(self.coords) - else: - let all_nan = max_finite[dtype]() - return self.coords == all_nan + return is_empty[dtype, simd_dims](self.coords) diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo new file mode 100644 index 0000000..e69de29 diff --git a/geo_features/serialization/json.mojo b/geo_features/serialization/json.mojo index e2530cd..398de11 100644 --- a/geo_features/serialization/json.mojo +++ b/geo_features/serialization/json.mojo @@ -2,11 +2,16 @@ from python import Python from python.object import PythonObject +trait JSONable: + fn json(self) -> String: + ... + + struct JSONParser: @staticmethod fn parse(json_str: String) raises -> PythonObject: """ Wraps json parser implementation. """ - let pyjson = Python.import_module("orjson") - return pyjson.loads(json_str) + let orjson = Python.import_module("orjson") + return orjson.loads(json_str) diff --git a/geo_features/test/geom/test_envelope.mojo b/geo_features/test/geom/test_envelope.mojo index 2b486ee..3cb68ed 100644 --- a/geo_features/test/geom/test_envelope.mojo +++ b/geo_features/test/geom/test_envelope.mojo @@ -14,6 +14,10 @@ from geo_features.geom import ( PointM, PointZM, LineString, + LineString2, + LineStringM, + LineStringZ, + LineStringZM, Envelope, Envelope2, EnvelopeZ, @@ -198,7 +202,7 @@ fn test_with_geos() raises: let geojson_dict = json.loads(geojson) let geometry = shape(geojson_dict) let expect_bounds = geometry.bounds - let lstr = LineString.from_json(geojson_dict) + let lstr = LineString2.from_json(geojson_dict) let env = Envelope(lstr) for i in range(4): test.assert_true( diff --git a/geo_features/test/geom/test_line_string.mojo b/geo_features/test/geom/test_line_string.mojo index 785c102..6aa8b23 100644 --- a/geo_features/test/geom/test_line_string.mojo +++ b/geo_features/test/geom/test_line_string.mojo @@ -14,7 +14,14 @@ from geo_features.geom.point import ( PointZM, ) -from geo_features.geom.line_string import LineString +# TODO: it should be possible to use implicit parameters and only import LineString into this module (not LineString2 etc) +from geo_features.geom.line_string import ( + LineString, + LineString2, + LineStringM, + LineStringZ, + LineStringZM, +) fn main() raises: @@ -95,7 +102,7 @@ fn test_memory_layout() raises: var points_vec20 = DynamicVector[Point2](10) for n in range(10): points_vec20.push_back(Point2(lon + n, lat - n)) - let lstr = LineString(points_vec20) + let lstr = LineString2(points_vec20) for n in range(10): let expect_pt = Point2(lon + n, lat - n) test.assert_true(lstr[n] == expect_pt, "memory_layout") @@ -115,7 +122,7 @@ fn test_get_item() raises: var points_vec = DynamicVector[Point2](10) for n in range(10): points_vec.push_back(Point2(lon + n, lat - n)) - let lstr = LineString(points_vec) + let lstr = LineString2(points_vec) for n in range(10): let expect_pt = Point2(lon + n, lat - n) let got_pt = lstr[n] @@ -170,8 +177,8 @@ fn test_equality_ops() raises: for n in range(10): points_vec.push_back(Point2(lon + n, lat - n)) - let lstr2 = LineString(points_vec) - let lstr3 = LineString(points_vec) + let lstr2 = LineString2(points_vec) + let lstr3 = LineString2(points_vec) test.assert_true(lstr2 == lstr3, "__eq__") let lstr4 = LineString(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) @@ -184,7 +191,7 @@ fn test_equality_ops() raises: fn test_is_empty() raises: let test = MojoTest("is_empty") - let empty_lstr = LineString() + let empty_lstr = LineString2() _ = empty_lstr.is_empty() @@ -248,7 +255,7 @@ fn test_from_json() raises: with open(file.path, "r") as f: let geojson = f.read() let geojson_dict = json.loads(geojson) - _ = LineString.from_json(geojson_dict) + _ = LineString2.from_json(geojson_dict) # fn test_from_wkt() raises: diff --git a/geo_features/test/geom/test_multi_point.mojo b/geo_features/test/geom/test_multi_point.mojo index acd6ef3..5aa8659 100644 --- a/geo_features/test/geom/test_multi_point.mojo +++ b/geo_features/test/geom/test_multi_point.mojo @@ -11,9 +11,17 @@ from geo_features.geom import ( PointM, PointZM, ) -from geo_features.geom import MultiPoint from geo_features.test.constants import lat, lon, height, measure +# TODO: it should be possible to use implicit parameters and only import MultiPoint into this module (not MultiPoint2 etc) +from geo_features.geom import ( + MultiPoint, + MultiPoint2, + MultiPointM, + MultiPointZ, + MultiPointZM, +) + fn main() raises: test_multi_point() @@ -47,41 +55,8 @@ fn test_constructors() raises: var points_vec = DynamicVector[Point2](10) for n in range(10): points_vec.push_back(Point2(lon + n, lat - n)) - _ = MultiPoint[dims = Point2.simd_dims, dtype = Point2.dtype](points_vec) - - test = MojoTest("non power of two dims constructor") - let mpt_z = MultiPoint[dims=3, point_simd_dims=4]( - PointZ(lon, lat, height), - PointZ(lon, lat + 1, height + 5), - PointZ(lon, lat + 2, height + 6), - PointZ(lon, lat + 3, height + 9), - ) - test.assert_true(mpt_z[0] == PointZ(lon, lat, height), "non power of two dims constructor/0") - test.assert_true(mpt_z[1] == PointZ(lon, lat + 1, height + 5), "non power of two dims constructor/1") - test.assert_true(mpt_z[2] == PointZ(lon, lat + 2, height + 6), "non power of two dims constructor/2") - test.assert_true(mpt_z.__len__() == 4, "non power of two dims constructor/len") - - let mpt_m = MultiPoint[dims=3, point_simd_dims=4]( - PointM(lon, lat, measure), - PointM(lon, lat + 1, measure + 5), - PointM(lon, lat + 2, measure + 6), - PointM(lon, lat + 3, measure + 9), - ) - test.assert_true(mpt_z[0] == PointM(lon, lat, height), "non power of two dims constructor/0") - test.assert_true(mpt_z[1] == PointM(lon, lat + 1, height + 5), "non power of two dims constructor/1") - test.assert_true(mpt_z[2] == PointM(lon, lat + 2, height + 6), "non power of two dims constructor/2") - test.assert_true(mpt_z.__len__() == 4, "non power of two dims constructor/len") - - let mpt_zm = MultiPoint[dims=4, point_simd_dims=4]( - PointZM(lon, lat, height, measure), - PointZM(lon, lat + 1, height + 3, measure + 5), - PointZM(lon, lat + 2, height + 2, measure + 6), - PointZM(lon, lat + 3, height + 1, measure + 9), - ) - test.assert_true(mpt_zm[0] == PointZM(lon, lat, height, measure), "non power of two dims constructor/0") - test.assert_true(mpt_zm[1] == PointZM(lon, lat + 1, height + 3, measure + 5), "non power of two dims constructor/1") - test.assert_true(mpt_zm[2] == PointZM(lon, lat + 2, height + 2, measure + 6), "non power of two dims constructor/2") - test.assert_true(mpt_zm.__len__() == 4, "non power of two dims constructor/len") + _ = MultiPoint[simd_dims = Point2.simd_dims, dtype = Point2.dtype](points_vec) + fn test_mem_layout() raises: """ @@ -93,7 +68,7 @@ fn test_mem_layout() raises: var points_vec = DynamicVector[Point2](10) for n in range(10): points_vec.push_back(Point2(lon + n, lat - n)) - let mpt2 = MultiPoint[dims = Point2.simd_dims, dtype = Point2.dtype](points_vec) + let mpt2 = MultiPoint2(points_vec) for n in range(10): let expect_pt = Point2(lon + n, lat - n) test.assert_true(mpt2[n] == expect_pt, "test_mem_layout") @@ -113,7 +88,7 @@ fn test_get_item() raises: var points_vec = DynamicVector[Point2](10) for n in range(10): points_vec.push_back(Point2(lon + n, lat - n)) - let mpt = MultiPoint(points_vec) + let mpt = MultiPoint2(points_vec) for n in range(10): let expect_pt = Point2(lon + n, lat - n) let got_pt = mpt[n] @@ -166,8 +141,8 @@ fn test_equality_ops() raises: var points_vec2 = DynamicVector[Point2](10) for n in range(10): points_vec2.push_back(Point2(lon + n, lat - n)) - let mpt9 = MultiPoint(points_vec2) - let mpt10 = MultiPoint(points_vec2) + let mpt9 = MultiPoint2(points_vec2) + let mpt10 = MultiPoint2(points_vec2) test.assert_true(mpt9 == mpt10, "__eq__") test.assert_true(mpt9 != mpt2, "__ne__") @@ -179,7 +154,7 @@ fn test_equality_ops() raises: fn test_is_empty() raises: let test = MojoTest("is_empty") - let empty_mpt = MultiPoint() + let empty_mpt = MultiPoint2() test.assert_true(empty_mpt.is_empty() == True, "is_empty()") @@ -226,7 +201,7 @@ fn test_from_json() raises: let json_dict = json.loads(json_str) try: - _ = MultiPoint.from_json(json_dict) + _ = MultiPoint2.from_json(json_dict) raise Error("unreachable") except e: test.assert_true( diff --git a/geo_features/test/geom/test_point.mojo b/geo_features/test/geom/test_point.mojo index 7500de9..9078481 100644 --- a/geo_features/test/geom/test_point.mojo +++ b/geo_features/test/geom/test_point.mojo @@ -4,7 +4,7 @@ from pathlib import Path from math import nan, isnan from math.limit import max_finite -from geo_features.geom.point import Point, Point2, PointZ, PointM, PointZM +from geo_features.geom.point import PointEnum, Point, Point2, PointZ, PointM, PointZM from geo_features.test.helpers import load_geoarrow_test_fixture from geo_features.test.pytest import MojoTest from geo_features.test.constants import lon, lat, height, measure @@ -12,7 +12,7 @@ from geo_features.test.constants import lon, lat, height, measure fn main() raises: test_constructors() - test_repr() + # test_repr() test_has_height() test_has_measure() test_equality_ops() @@ -22,20 +22,8 @@ fn main() raises: test_wkt() test_json() test_from_json() - test_from_wkt() - test_from_geoarrow() - - -fn test_has_height() raises: - let test = MojoTest("has_height") - let pt_z = PointZ(lon, lat, height) - test.assert_true(pt_z.has_height(), "has_height") - - -fn test_has_measure() raises: - let test = MojoTest("has_measure") - let pt_m = PointM(lon, lat, measure) - test.assert_true(pt_m.has_measure(), "has_measure") + # test_from_wkt() + # test_from_geoarrow() fn test_constructors(): @@ -56,15 +44,17 @@ fn test_constructors(): # try all constructors, various parameters - _ = Point[2, DType.int32]() - _ = Point[2, DType.float64]() - _ = Point[4, DType.float64]() + _ = Point[2, DType.int32, PointEnum.Point]() + _ = Point[2, DType.float64, PointEnum.PointM]() + _ = Point[2, DType.float64, PointEnum.PointZ]() + _ = Point[2, DType.float64, PointEnum.PointZM]() + _ = Point[4, DType.float64, PointEnum.PointZM]() _ = Point[2, DType.int32](lon, lat) _ = Point[2, DType.float64](lon, lat) - _ = Point[4, DType.float64](lon, lat) + _ = Point[4, DType.float64, PointEnum.PointZM](lon, lat) - _ = Point[dtype = DType.float16, simd_dims=4]( + _ = Point[dtype = DType.float16, simd_dims=4, T=PointEnum.PointZM]( SIMD[DType.float16, 4](lon, lat, height, measure) ) _ = Point[dtype = DType.float32, simd_dims=4]( @@ -74,6 +64,33 @@ fn test_constructors(): # power of two dims: compile time constraint (uncomment to test) # _ = Point[3, DType.float32](lon, lat) +# fn test_repr() raises: +# let test = MojoTest("repr") +# let pt1 = Point(lon, lat) +# test.assert_true( +# pt1.__repr__() == "Point[2, float64](-108.68000000000001, 38.973999999999997)", +# "__repr__", +# ) +# let pt2 = Point(SIMD[DType.float64, 2](lon, lat)) +# test.assert_true( +# pt2.__repr__() == "Point[2, float64](-108.68000000000001, 38.973999999999997)", +# "__repr__", +# ) + + + +fn test_has_height() raises: + let test = MojoTest("has_height") + let pt_z = PointZ(lon, lat, height) + test.assert_true(pt_z.has_height(), "has_height") + + +fn test_has_measure() raises: + let test = MojoTest("has_measure") + let pt_m = PointM(lon, lat, measure) + test.assert_true(pt_m.has_measure(), "has_measure") + + fn test_empty_default_values() raises: let test = MojoTest("test empty default values") @@ -88,19 +105,6 @@ fn test_empty_default_values() raises: test.assert_true(pt_4_int.coords[3] == expect_empty, "maxint expected") -fn test_repr() raises: - let test = MojoTest("repr") - let pt1 = Point(lon, lat) - test.assert_true( - pt1.__repr__() == "Point[2, float64](-108.68000000000001, 38.973999999999997)", - "__repr__", - ) - let pt2 = Point(SIMD[DType.float64, 2](lon, lat)) - test.assert_true( - pt2.__repr__() == "Point[2, float64](-108.68000000000001, 38.973999999999997)", - "__repr__", - ) - fn test_equality_ops() raises: let test = MojoTest("equality operators") @@ -241,24 +245,25 @@ fn test_from_json() raises: let json_str = String('{"type": "Point","coordinates": [102.0, 3.5]}') let json = Python.import_module("orjson") let json_dict = json.loads(json_str) - - let pt1 = Point[2, DType.float64].from_json(json_dict) + print("json_dict", json_dict) + let pt1 = Point2.from_json(json_dict) + print(pt1.__repr__()) test.assert_true(pt1.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") - let pt2 = Point2.from_json(json_dict) - test.assert_true(pt2.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + # let pt2 = Point2.from_json(json_dict) + # test.assert_true(pt2.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") - let pt3 = Point[2, DType.uint8].from_json(json_dict) - test.assert_true(pt3.__repr__() == "Point[2, uint8](102, 3)", "from_json()") + # let pt3 = Point[2, DType.uint8].from_json(json_dict) + # test.assert_true(pt3.__repr__() == "Point[2, uint8](102, 3)", "from_json()") - let pt4 = Point[2, DType.float64].from_json(json_str) - test.assert_true(pt4.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + # let pt4 = Point[2, DType.float64].from_json(json_str) + # test.assert_true(pt4.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") - let pt5 = Point2.from_json(json_dict) - test.assert_true(pt5.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + # let pt5 = Point2.from_json(json_dict) + # test.assert_true(pt5.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") - let pt6 = Point[2, DType.uint8].from_json(json_dict) - test.assert_true(pt6.__repr__() == "Point[2, uint8](102, 3)", "from_json()") + # let pt6 = Point[2, DType.uint8].from_json(json_dict) + # test.assert_true(pt6.__repr__() == "Point[2, uint8](102, 3)", "from_json()") fn test_from_wkt() raises: @@ -289,10 +294,10 @@ fn test_from_wkt() raises: point_2d_u8.__repr__() == "Point[2, uint8](148, 38)", "from_wkt())" ) - let point_2d_f64 = Point[2, DType.float64].from_wkt(wkt) + let point_2d_f32 = Point[2, DType.float32].from_wkt(wkt) test.assert_true( - point_2d_f64.__repr__() - == "Point[2, float64](-108.68000000000001, 38.973999999999997)", + point_2d_f32.__repr__() + == "Point[2, float32](-108.68000000000001, 38.973999999999997)", "from_wkt()", ) except: @@ -326,7 +331,7 @@ fn test_from_geoarrow() raises: 30.0, 10.0, 40.0, nan[PointZ.dtype]() ) for i in range(3): - # cannto check the nan for equality + # cannot check the nan for equality test.assert_true(point_3d.coords[i] == expect_coords_3d[i], "expect_coords_3d") file = path / "example-point_zm.arrow" From ba2f9c72e07b9b26f29bb1a7b769054a9b2eb2b8 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Mon, 11 Dec 2023 22:25:47 -0700 Subject: [PATCH 03/11] Add traits, improve Point struct. --- geo_features/geom/empty.mojo | 27 ++ geo_features/geom/enums.mojo | 45 +++ geo_features/geom/point.mojo | 353 +++++++++-------------- geo_features/geom/traits.mojo | 35 +++ geo_features/serialization/__init__.mojo | 5 +- geo_features/serialization/geoarrow.mojo | 18 ++ geo_features/serialization/json.mojo | 18 ++ geo_features/serialization/wkt.mojo | 16 + geo_features/test/geom/test_empty.mojo | 25 ++ geo_features/test/geom/test_point.mojo | 314 ++++++++++---------- 10 files changed, 471 insertions(+), 385 deletions(-) create mode 100644 geo_features/geom/empty.mojo create mode 100644 geo_features/geom/enums.mojo create mode 100644 geo_features/serialization/geoarrow.mojo create mode 100644 geo_features/test/geom/test_empty.mojo diff --git a/geo_features/geom/empty.mojo b/geo_features/geom/empty.mojo new file mode 100644 index 0000000..e407178 --- /dev/null +++ b/geo_features/geom/empty.mojo @@ -0,0 +1,27 @@ +from math import nan, isnan +from math.limit import max_finite + + +@always_inline +fn empty_value[dtype: DType]() -> SIMD[dtype, 1]: + """ + Define a special value to mark empty slots or dimensions in structs. Required because SIMD must be power of two. + """ + @parameter + if dtype.is_floating_point(): + return nan[dtype]() + else: + return max_finite[dtype]() + + +@always_inline +fn is_empty[dtype: DType, simd_width: Int](value: SIMD[dtype, simd_width]) -> Bool: + """ + Check for empty value. Note: NaN cannot be compared by equality. This helper function calls isnan() if the dtype + is floating point. + """ + @parameter + if dtype.is_floating_point(): + return isnan[dtype, simd_width](value) + else: + return value == max_finite[dtype]() diff --git a/geo_features/geom/enums.mojo b/geo_features/geom/enums.mojo new file mode 100644 index 0000000..c8a16aa --- /dev/null +++ b/geo_features/geom/enums.mojo @@ -0,0 +1,45 @@ +@register_passable("trivial") +struct CoordDims(Stringable): + """ + Enum for encoding the OGC/WKT variants of Points. + """ + # TODO: use a real enum here, when mojo supports. + + var value: SIMD[DType.uint8, 1] + + alias Point = CoordDims(252) + """ + 2 dimensional Point. + """ + alias PointZ = CoordDims(253) + """ + 3 dimensional Point, has height or altitude (Z). + """ + alias PointM = CoordDims(254) + """ + 3 dimensional Point, has measure (M). + """ + alias PointZM = CoordDims(255) + """ + 4 dimensional Point, has height and measure (ZM) + """ + + fn __init__(value: Int) -> Self: + return Self { value: value } + + fn __eq__(self, other: Self) -> Bool: + return self.value == other.value + + fn __str__(self) -> String: + """ + Convert to string, using WKT point variants. + """ + if self == CoordDims.Point: + return "Point" + if self == CoordDims.PointZ: + return "Point Z" + if self == CoordDims.PointM: + return "Point M" + if self == CoordDims.PointZM: + return "Point ZM" + return "Point" diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index 7c2574f..e47e437 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -2,210 +2,112 @@ from python import Python from math import nan, isnan from math.limit import max_finite -from ._utils import empty_value, is_empty +from geo_features.geom.empty import empty_value, is_empty +from geo_features.serialization import WKTParser, WKTable, JSONParser, JSONable, Geoarrowable +from .traits import Geometric, Zeroable +from .enums import CoordDims -from geo_features.serialization import WKTParser, JSONParser - -alias Point2 = Point[simd_dims=2, dtype=DType.float64, T=PointEnum.Point] -""" -Alias for 2D point with dtype float64. -""" - -alias PointZ = Point[simd_dims=4, dtype=DType.float64, T=PointEnum.PointZ] -""" -Alias for 3D point with dtype float64, including Z (height) dimension. -Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). -""" - -alias PointM = Point[simd_dims=4, dtype=DType.float64, T=PointEnum.PointM] -""" -Alias for 3D point with dtype float64, including M (measure) dimension. -Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). -""" - -alias PointZM = Point[simd_dims=4, dtype=DType.float64, T=PointEnum.PointZM] -""" -Alias for 4D point with dtype float64, including Z (height) and M (measure) dimension. -""" - -@register_passable("trivial") -struct PointEnum(Stringable): - """ - Enum for expressing the variants of Points as either parameter values or runtime args. - """ - var value: SIMD[DType.uint8, 1] - - alias Point = PointEnum(0) - """ - 2 dimensional Point. - """ - alias PointZ = PointEnum(1) - """ - 3 dimensional Point, has height or altitude (Z). - """ - alias PointM = PointEnum(2) - """ - 3 dimensional Point, has measure (M). - """ - alias PointZM = PointEnum(3) - """ - 4 dimensional Point, has height and measure (ZM) - """ - - alias PointN = PointEnum(4) - """ - 5 or higher dimensional Point. - """ - - fn __init__(value: Int) -> Self: - return Self { value: value } - - fn __eq__(self, other: Self) -> Bool: - return self.value == other.value - - fn __str__(self) -> String: - if self == PointEnum.Point: - return "Point" - if self == PointEnum.PointZ: - return "PointZ" - if self == PointEnum.PointM: - return "PointM" - if self == PointEnum.PointZM: - return "PointZM" - return "PointN" @value @register_passable("trivial") -struct Point[simd_dims: Int = 2, dtype: DType = DType.float64, T: PointEnum = PointEnum.Point]( +struct Point[dtype: DType = DType.float64]( CollectionElement, + Geoarrowable, + Geometric, + JSONable, Sized, - Stringable + Stringable, + WKTable, + Zeroable, ): """ - Point is a register-passable, copy-efficient struct holding 2 or more dimension values. - - ### Parameters +Point is a register-passable, copy-efficient struct holding 2 or more dimension values. + +### Parameters - - simd_dims: constrained to power of two (default = 2) - dtype: supports any float or integer type (default = float64) - - T: enum type, ex: Point, PointZ, PointM, PointZM, or POINTN - ### Memory Layouts +### Memory Layouts + + Some examples of memory layout using Mojo SIMD variables. ```txt - Point2 Empty Points are either NaN (float) max-finite (int). -┌───────────┬────────┐ ┌───────────┬────────┐ -│SIMD index:│0 1 │ │SIMD index:│0 1 │ -│dimension: │x y │ │dimension: │NaN NaN │ -└───────────┴────────┘ └───────────┴────────┘ - -PointM (measure) -┌───────────┬──────────┐ -│SIMD index:│0 1 2 3 │ -│dimension: │x y NaN m │ -└───────────┴──────────┘ - -PointZ (height) -┌───────────┬──────────┐ -│SIMD index:│0 1 2 3 │ -│dimension: │x y z NaN │ -└───────────┴──────────┘ - -PointZM (height and measure) -┌───────────┬──────────┐ -│SIMD index:│0 1 2 3 │ -│dimension: │x y z m │ -└───────────┴──────────┘ - -PointN (n-dimensional point) -┌───────────┬───────────────────────┐ -│SIMD index:│0 1 2 3 4 5 6 7 │ -│dimension: │n0 n1 n2 n3 n4 n5 n6 n7│ -└───────────┴───────────────────────┘ -. ``` - """ + alias simd_dims = 4 + alias x_index = 0 + alias y_index = 1 + alias z_index = 2 + alias m_index = 3 - var coords: SIMD[dtype, simd_dims] + var coords: SIMD[dtype, Self.simd_dims] + var ogc_dims: CoordDims - fn __init__() -> Self: + # + # Constructors + # + fn __init__(dims: CoordDims = CoordDims.Point) -> Self: """ Create Point with empty values (NaN for float or max finite for integers). """ - @parameter - constrained[simd_dims % 2 == 0, "dims must be power of two"]() - let empty = empty_value[dtype]() - let coords = SIMD[dtype, simd_dims](empty) - return Self { coords: coords } - + let coords = SIMD[dtype, Self.simd_dims](empty) + return Self { coords: coords, ogc_dims: dims } fn __init__(*coords_list: SIMD[dtype, 1]) -> Self: """ Create Point from variadic list of SIMD values. Any missing elements are padded with empty values. - """ - # TODO add arg for has_measure, has_height, so dimensions 3-4 can be encoded - @parameter - constrained[simd_dims % 2 == 0, "dims must be power of two"]() + ### See also - var result = Self() # start with empty values + Setter method for ogc_dims enum. May be useful when Point Z vs Point M is ambiguous in this constructor. + """ + let empty = empty_value[dtype]() + var coords = SIMD[dtype, Self.simd_dims](empty) + var ogc_dims = CoordDims.Point let n = len(coords_list) - debug_assert(n <= simd_dims, "coords_list length is longer than simd_dims parameter") - for i in range(n): - if i >= simd_dims: - break - result.coords[i] = coords_list[i] + for i in range(Self.simd_dims): + if i < n: + coords[i] = coords_list[i] - return result + if n == 3: + ogc_dims = CoordDims.PointZ + # workaround in case this is a Point M (measure). Duplicate the measure value in index 2 and 3. + coords[Self.m_index] = coords[Self.z_index] + elif n >= 4: + ogc_dims = CoordDims.PointZM - fn __init__(coords: SIMD[dtype, simd_dims]) -> Self: + return Self { coords: coords, ogc_dims: ogc_dims } + + fn __init__(coords: SIMD[dtype, Self.simd_dims], dims: CoordDims = CoordDims.Point) -> Self: """ Create Point from existing SIMD vector of coordinates. """ - @parameter - constrained[simd_dims % 2 == 0, "dims must be power of two"]() - - return Self {coords: coords} - - fn has_height(self) -> Bool: - if T == PointEnum.Point or T == PointEnum.PointM: - return False - alias z_idx = 2 - return not is_empty(self.coords[z_idx]) - - fn has_measure(self) -> Bool: - if T == PointEnum.Point or T == PointEnum.PointZ: - return False - alias m_idx = 3 - return not is_empty(self.coords[m_idx]) + return Self {coords: coords, ogc_dims: dims} + # + # Static constructor methods. + # @staticmethod fn from_json(json_dict: PythonObject) raises -> Self: """ - Create Point from geojson (expect to have been parsed into a python dict). + JSONable trait. """ + # TODO: bounds checking of coords_len let json_coords = json_dict["coordinates"] let coords_len = int(json_coords.__len__()) var result = Self() - print(simd_dims, dtype, T) - print(len(result.coords)) - # print(str(result)) + for i in range(coords_len): + result.coords[i] = json_coords[i].to_float64().cast[dtype]() return result - # # TODO: bounds checking - # for i in range(coords_len): - # result.coords[i] = json_coords[i].to_float64().cast[dtype]() - # print(str(result)) - # return result @staticmethod fn from_json(json_str: String) raises -> Self: """ - Create Point from geojson string. + JSONable trait. """ let json_dict = JSONParser.parse(json_str) return Self.from_json(json_dict) @@ -213,7 +115,7 @@ PointN (n-dimensional point) @staticmethod fn from_wkt(wkt: String) raises -> Self: """ - Create Point from WKT string. + WKTable trait. """ var result = Self() let geos_pt = WKTParser.parse(wkt) @@ -226,13 +128,13 @@ PointN (n-dimensional point) @staticmethod fn from_geoarrow(table: PythonObject) raises -> Self: """ - Create Point from geoarrow / pyarrow table with geometry column. + Geoarrowable trait. """ let ga = Python.import_module("geoarrow.pyarrow") let geoarrow = ga.as_geoarrow(table["geometry"]) let chunk = geoarrow[0] let n = chunk.value.__len__() - if n > simd_dims: + if n > Self.simd_dims: raise Error("Invalid Point dims parameter vs. geoarrow: " + str(n)) var result = Self() for dim in range(n): @@ -241,7 +143,7 @@ PointN (n-dimensional point) return result @staticmethod - fn zero() -> Self: + fn zero(dims: CoordDims = CoordDims.Point) -> Self: """ Null Island is an imaginary place located at zero degrees latitude and zero degrees longitude (0°N 0°E) https://en.wikipedia.org/wiki/Null_Island . @@ -250,52 +152,77 @@ PointN (n-dimensional point) empty() and is_empty(). note, the zero point is not the same as an empty point! """ - let coords = SIMD[dtype, simd_dims](0) - return Self { coords: coords } + let coords = SIMD[dtype, Self.simd_dims](0) + return Self { coords: coords, ogc_dims: dims } + + # + # Getters/Setters + # + fn set_ogc_dims(inout self, ogc_dims: CoordDims): + """ + Setter for ogc_dims enum. May be useful if the Point constructor with variadic list of coordinate values. + (Point Z vs Point M is ambiguous). + """ + self.ogc_dims = ogc_dims + + fn has_height(self) -> Bool: + return not is_empty(self.coords[Self.z_index]) + + fn has_measure(self) -> Bool: + return not is_empty(self.coords[Self.m_index]) + + fn is_empty(self) -> Bool: + return is_empty[dtype](self.coords) @always_inline fn x(self) -> SIMD[dtype, 1]: """ Get the x value (0 index). """ - return self.coords[0] + return self.coords[self.x_index] @always_inline fn y(self) -> SIMD[dtype, 1]: """ Get the y value (1 index). """ - return self.coords[1] + return self.coords[self.y_index] + @always_inline fn z(self) -> SIMD[dtype, 1]: """ Get the z or altitude value (2 index). """ - @parameter - constrained[simd_dims >= 4, "Point has no Z dimension"]() - - return self.coords[2] - + return self.coords[self.z_index] + @always_inline fn alt(self) -> SIMD[dtype, 1]: """ Get the z or altitude value (2 index). """ return self.z() + @always_inline fn m(self) -> SIMD[dtype, 1]: """ Get the measure value (3 index). """ - @parameter - constrained[simd_dims >= 4, "Point has no M dimension"]() - return self.coords[3] + return self.coords[self.m_index] + fn dims(self) -> Int: + """ + Dimensionable trait. + """ + return self.__len__() + + # + # Dunder methods + # fn __len__(self) -> Int: """ Returns the number of non-empty dimensions. """ - if len(self.coords > 4): + if len(self.coords > Self.simd_dims): return len(self.coords) var dims = 2 if self.has_height(): @@ -308,84 +235,78 @@ PointN (n-dimensional point) """ Get the value of coordinate at this dimension. """ - return self.coords[d] if d < simd_dims else 0 + return self.coords[d] if d < Self.simd_dims else empty_value[dtype]() fn __eq__(self, other: Self) -> Bool: - return Bool(self.coords == other.coords) + """ + Equality comparison (==). + """ + # Warning: NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. + @unroll + for i in range(Self.simd_dims): - fn __ne__(self, other: Self) -> Bool: - return not self.__eq__(other) + if is_empty(self.coords[i]) and is_empty(other.coords[i]): + pass # equality at index i + else: + if is_empty(self.coords[i]) or is_empty(other.coords[i]): + return False # early out: one or the other is empty (but not both) -> not equal + if self.coords[i] != other.coords[i]: + return False # not equal + return True # equal - fn enum(self) -> PointEnum: + fn __ne__(self, other: Self) -> Bool: """ - Describe point's type as a PointEnum. + Inequality comparison (!=). """ - if self.has_height() and self.has_measure(): - return PointEnum.PointZM - if self.has_height(): - return PointEnum.PointZ - if self.has_measure(): - return PointEnum.PointM - return PointEnum.Point + return not self.__eq__(other) fn __repr__(self) -> String: - let desc = str(self.enum()) - var res = desc + - "[simd_dims:" + - String(simd_dims) + - ", dtype:" + - dtype.__str__() + - "](" - for i in range(simd_dims): - res += self.coords[i] - if i < simd_dims - 1: + let point_variant = str(self.ogc_dims) + var res = point_variant + " [" + dtype.__str__() + "](" + for i in range(Self.simd_dims): + res += str(self.coords[i]) + if i < Self.simd_dims -1: res += ", " res += ")" return res fn __str__(self) -> String: + """ + Convert to String, uses WKT for convenience. See also __repr__(). + """ return self.wkt() fn json(self) -> String: """ - GeoJSON representation of Point. Point coordinates are in x, y order (easting, northing for projected - coordinates, longitude, and latitude for geographic coordinates). - - ### Spec - - - https://geojson.org - - https://datatracker.ietf.org/doc/html/rfc7946 + JSONable trait. """ # include only x, y, and optionally z (height) var res = String('{"type":"Point","coordinates":[') let dims = 3 if self.has_height() else 2 for i in range(dims): - if i > simd_dims - 1: + if i > 3: break res += self.coords[i] if i < dims - 1: res += "," - print(res) res += "]}" return res fn wkt(self) -> String: """ - Well Known Text (WKT) representation of Point. - - ### Spec - - https://libgeos.org/specifications/wkt + See WKTable trait. """ if self.is_empty(): return "POINT EMPTY" - var result = str(self.enum()) - for i in range(simd_dims): - result += self.coords[i] - if i < simd_dims - 1: - result += " " + var result = str(self.ogc_dims) + " (" + result += str(self.x()) + " " + str(self.y()) + if self.ogc_dims == CoordDims.PointZ or self.ogc_dims == CoordDims.PointZM: + result += " " + str(self.z()) + if self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZM: + result += " " + str(self.m()) result += ")" return result - fn is_empty(self) -> Bool: - return is_empty[dtype, simd_dims](self.coords) + fn geoarrow(self) -> PythonObject: + # TODO: geoarrow() + return None diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo index e69de29..2824e55 100644 --- a/geo_features/geom/traits.mojo +++ b/geo_features/geom/traits.mojo @@ -0,0 +1,35 @@ +from .enums import CoordDims + + +trait Dimensionable: + fn dims(self) -> Int: + ... + +trait Zeroable: + @staticmethod + fn zero(dims: CoordDims = CoordDims.Point) -> Self: + ... + +trait Geometric(Dimensionable): + """ + TODO: geometric trait + + contains(Geometry): Tests if one geometry contains another. + intersects(Geometry): Tests if two geometries intersect. + overlaps(Geometry): Tests if two geometries overlap, i.e. their intersection results in a geometry of the same dimension and not just a point/line. + disjoint(Geometry): Tests if two geometries are disjoint, i.e. they do not intersect. + touches(Geometry): Tests if two geometries touch, i.e. their intersection is just one point. + intersection(Geometry): Returns a geometry representing the shared portion of the two geometries. + union(Geometry): Returns a geometry representing all points of the two geometries. + difference(Geometry): Returns a geometry representing all points of one geometry that do not intersect with another. + buffer(double): Returns an area around the geometry wider by a given distance. + convexHull(): Returns the convex hull of a geometry, i.e. the shape enclosing all outer points. + simplify(double): Returns a simplified version of a geometry using the Douglas-Peucker algorithm within a given tolerance. + centroid(): Returns the geometric center point of a geometry. + getArea(): Returns the area of a polygonal geometry. + getLength(): Returns the length of a linear geometry. + translate(double, double): Moves a geometry by given offsets. + rotate(double): Rotates a geometry around a point by a given angle. + + """ + ... \ No newline at end of file diff --git a/geo_features/serialization/__init__.mojo b/geo_features/serialization/__init__.mojo index cbc2e95..1bdd824 100644 --- a/geo_features/serialization/__init__.mojo +++ b/geo_features/serialization/__init__.mojo @@ -1,5 +1,6 @@ """ Interchange, parsers, and serialization formats. """ -from .json import JSONParser -from .wkt import WKTParser +from .json import * +from .wkt import * +from .geoarrow import * diff --git a/geo_features/serialization/geoarrow.mojo b/geo_features/serialization/geoarrow.mojo new file mode 100644 index 0000000..2b33820 --- /dev/null +++ b/geo_features/serialization/geoarrow.mojo @@ -0,0 +1,18 @@ +trait Geoarrowable: + """ + Serializable to and from GeoArrow representation of a Point. + + ### Spec + + - https://geoarrow.org/ + """ + + @staticmethod + fn from_geoarrow(table: PythonObject) raises -> Self: + """ + Create Point from geoarrow / pyarrow table with geometry column. + """ + ... + + fn geoarrow(self) -> PythonObject: + ... diff --git a/geo_features/serialization/json.mojo b/geo_features/serialization/json.mojo index 398de11..7cc8dd5 100644 --- a/geo_features/serialization/json.mojo +++ b/geo_features/serialization/json.mojo @@ -3,6 +3,24 @@ from python.object import PythonObject trait JSONable: + """ + Serializable to and from GeoJSON representation of Point. Point coordinates are in x, y order (easting, northing for + projected coordinates, longitude, and latitude for geographic coordinates). + + ### Spec + + - https://geojson.org + - https://datatracker.ietf.org/doc/html/rfc7946 + """ + + @staticmethod + fn from_json(json: PythonObject) raises -> Self: + ... + + @staticmethod + fn from_json(json_str: String) raises -> Self: + ... + fn json(self) -> String: ... diff --git a/geo_features/serialization/wkt.mojo b/geo_features/serialization/wkt.mojo index 78d0ed6..533d2ee 100644 --- a/geo_features/serialization/wkt.mojo +++ b/geo_features/serialization/wkt.mojo @@ -1,6 +1,22 @@ from python import Python from python.object import PythonObject +trait WKTable: + """ + Serializable to and from Well Known Text (WKT). + + ### Spec + + https://libgeos.org/specifications/wkt + """ + + @staticmethod + fn from_wkt(wkt: String) raises -> Self: + ... + + fn wkt(self) -> String: + ... + struct WKTParser: @staticmethod diff --git a/geo_features/test/geom/test_empty.mojo b/geo_features/test/geom/test_empty.mojo new file mode 100644 index 0000000..94c4459 --- /dev/null +++ b/geo_features/test/geom/test_empty.mojo @@ -0,0 +1,25 @@ +from python import Python +from python.object import PythonObject +from pathlib import Path + +from geo_features.geom.empty import empty_value, is_empty +from geo_features.test.pytest import MojoTest + + +fn main() raises: + let test = MojoTest("empty_value") + + let empty_f64 = empty_value[DType.float64]() + let empty_f32 = empty_value[DType.float32]() + let empty_f16 = empty_value[DType.float16]() + let empty_int = empty_value[DType.int32]() + let empty_uint = empty_value[DType.uint32]() + + test.assert_true(is_empty(empty_f64), "empty_f64") + test.assert_true(is_empty(empty_f32), "empty_f32") + test.assert_true(is_empty(empty_f16), "empty_f16") + test.assert_true(is_empty(empty_int), "empty_int") + test.assert_true(is_empty(empty_uint), "empty_uint") + + test.assert_true(not is_empty[DType.float64, 1](42), "not empty") + test.assert_true(not is_empty[DType.uint16, 1](42), "not empty") \ No newline at end of file diff --git a/geo_features/test/geom/test_point.mojo b/geo_features/test/geom/test_point.mojo index 9078481..f42759d 100644 --- a/geo_features/test/geom/test_point.mojo +++ b/geo_features/test/geom/test_point.mojo @@ -1,10 +1,9 @@ from python import Python from python.object import PythonObject from pathlib import Path -from math import nan, isnan -from math.limit import max_finite -from geo_features.geom.point import PointEnum, Point, Point2, PointZ, PointM, PointZM +from geo_features.geom.empty import empty_value, is_empty +from geo_features.geom.point import Point, CoordDims from geo_features.test.helpers import load_geoarrow_test_fixture from geo_features.test.pytest import MojoTest from geo_features.test.constants import lon, lat, height, measure @@ -12,98 +11,83 @@ from geo_features.test.constants import lon, lat, height, measure fn main() raises: test_constructors() - # test_repr() + test_repr() test_has_height() test_has_measure() test_equality_ops() test_zero() test_is_empty() test_getters() + test_setters() test_wkt() test_json() test_from_json() - # test_from_wkt() - # test_from_geoarrow() + test_from_wkt() + test_from_geoarrow() fn test_constructors(): - let test = MojoTest("constructors, aliases") - - # aliases - _ = Point2() - _ = Point2(lon, lat) - - _ = PointZ(lon, lat, height) - _ = PointZ() - - _ = PointM() - _ = PointM(lon, lat, measure) - - _ = PointZM() - _ = PointZM(lon, lat, height, measure) - - # try all constructors, various parameters - - _ = Point[2, DType.int32, PointEnum.Point]() - _ = Point[2, DType.float64, PointEnum.PointM]() - _ = Point[2, DType.float64, PointEnum.PointZ]() - _ = Point[2, DType.float64, PointEnum.PointZM]() - _ = Point[4, DType.float64, PointEnum.PointZM]() - - _ = Point[2, DType.int32](lon, lat) - _ = Point[2, DType.float64](lon, lat) - _ = Point[4, DType.float64, PointEnum.PointZM](lon, lat) - - _ = Point[dtype = DType.float16, simd_dims=4, T=PointEnum.PointZM]( + let test = MojoTest("constructors") + + _ = Point() + _ = Point(lon, lat) + _ = Point(lon, lat, height) + _ = Point(lon, lat, measure) + _ = Point(lon, lat, height, measure) + _ = Point[DType.int32]() + _ = Point[DType.float32]() + _ = Point[DType.int32](lon, lat) + _ = Point[DType.float32](lon, lat) + _ = Point[dtype=DType.float16]( SIMD[DType.float16, 4](lon, lat, height, measure) ) - _ = Point[dtype = DType.float32, simd_dims=4]( + _ = Point[dtype=DType.float32]( SIMD[DType.float32, 4](lon, lat, height, measure) ) - # power of two dims: compile time constraint (uncomment to test) - # _ = Point[3, DType.float32](lon, lat) -# fn test_repr() raises: -# let test = MojoTest("repr") -# let pt1 = Point(lon, lat) -# test.assert_true( -# pt1.__repr__() == "Point[2, float64](-108.68000000000001, 38.973999999999997)", -# "__repr__", -# ) -# let pt2 = Point(SIMD[DType.float64, 2](lon, lat)) -# test.assert_true( -# pt2.__repr__() == "Point[2, float64](-108.68000000000001, 38.973999999999997)", -# "__repr__", -# ) +fn test_repr() raises: + let test = MojoTest("repr") + + let pt = Point(lon, lat) + test.assert_true(pt.__repr__() == "Point [float64](-108.68000000000001, 38.973999999999997, nan, nan)", "repr") + + let pt_z = Point(lon, lat, height) + test.assert_true(pt_z.__repr__() == "Point Z [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", "repr") + # the variadic list constructor cannot distinguish Point Z from Point M, so use the set_ogc_dims method. + var pt_m = pt_z + pt_m.set_ogc_dims(CoordDims.PointM) + test.assert_true(pt_m.__repr__() == "Point M [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", "repr") + + let pt_zm = Point(lon, lat, height, measure) + test.assert_true(pt_zm.__repr__() == "Point ZM [float64](-108.68000000000001, 38.973999999999997, 8.0, 42.0)", "repr") fn test_has_height() raises: let test = MojoTest("has_height") - let pt_z = PointZ(lon, lat, height) + let pt_z = Point(lon, lat, height) test.assert_true(pt_z.has_height(), "has_height") fn test_has_measure() raises: let test = MojoTest("has_measure") - let pt_m = PointM(lon, lat, measure) + let pt_m = Point(lon, lat, measure) test.assert_true(pt_m.has_measure(), "has_measure") - fn test_empty_default_values() raises: let test = MojoTest("test empty default values") - let pt_4 = Point[4, DType.float64](lon, lat) - test.assert_true(isnan(pt_4.coords[2]), "NaN expected") - test.assert_true(isnan(pt_4.coords[3]), "NaN expected") - - let pt_4_int = Point[4, DType.uint16](lon, lat) - let expect_empty = max_finite[DType.uint16]() - test.assert_true(pt_4_int.coords[2] == expect_empty, "maxint expected") - test.assert_true(pt_4_int.coords[3] == expect_empty, "maxint expected") + let pt_4 = Point(lon, lat) + let expect_value = empty_value[pt_4.dtype]() + test.assert_true(pt_4.coords[2] == expect_value, "NaN expected") + test.assert_true(pt_4.coords[3] == expect_value, "NaN expected") + let pt_4_int = Point[DType.uint16](lon, lat) + let expect_value_int = empty_value[pt_4_int.dtype]() + test.assert_true(pt_4_int.coords[2] == expect_value_int, "max_finite expected") + test.assert_true(pt_4_int.coords[3] == expect_value_int, "max_finite expected") fn test_equality_ops() raises: @@ -113,114 +97,96 @@ fn test_equality_ops() raises: let p2b = Point(lon, lat) test.assert_true(p2a == p2b, "__eq__") - let p2i = Point[2, DType.int16](lon, lat) - let p2ib = Point[2, DType.int16](lon, lat) + let p2i = Point[DType.int16](lon, lat) + let p2ib = Point[DType.int16](lon, lat) test.assert_true(p2i == p2ib, "__eq__") - let p2ic = Point[2, DType.int16](lon + 1, lat) + let p2ic = Point[DType.int16](lon + 1, lat) test.assert_true(p2i != p2ic, "__ne_") - let p4 = PointZM(lon, lat, height, measure) - let p4a = PointZM(lon, lat, height, measure) - let p4b = PointZM(lon + 0.001, lat, height, measure) + let p4 = Point(lon, lat, height, measure) + let p4a = Point(lon, lat, height, measure) + let p4b = Point(lon + 0.001, lat, height, measure) test.assert_true(p4 == p4a, "__eq__") test.assert_true(p4 != p4b, "__eq__") fn test_zero() raises: let test = MojoTest("zero") - let pt2 = Point2.zero() - test.assert_true(pt2.x() == 0, "zero().x()") - test.assert_true(pt2.y() == 0, "zero().y()") - let pt_z = PointZ.zero() - test.assert_true(pt_z.x() == 0, "zero().x()") - test.assert_true(pt_z.y() == 0, "zero().y()") - test.assert_true(pt_z.z() == 0, "zero().z()") - - let pt_m = PointM.zero() - test.assert_true(pt_m.x() == 0, "zero().x()") - test.assert_true(pt_m.y() == 0, "zero().y()") - test.assert_true(pt_m.m() == 0, "zero().m()") - - let pt_zm = PointZM.zero() - test.assert_true(pt_zm.x() == 0, "zero().x()") - test.assert_true(pt_zm.y() == 0, "zero().y()") - test.assert_true(pt_zm.z() == 0, "zero().z()") - test.assert_true(pt_zm.m() == 0, "zero().m()") - - let pti = Point[2, DType.int8].zero() - test.assert_true(pti.x() == 0, "zero().x()") - test.assert_true(pti.y() == 0, "zero().y()") + let pt = Point.zero() + test.assert_true(pt.x() == 0, "zero().x()") + test.assert_true(pt.y() == 0, "zero().y()") fn test_is_empty() raises: let test = MojoTest("is_empty") - let pt2 = Point2() + + let pt2 = Point() test.assert_true(pt2.is_empty(), "is_empty") - let pt_z = PointZ() + let pti = Point[DType.int8]() + test.assert_true(pti.is_empty(), "is_empty") + + let pt_z = Point[DType.int8](CoordDims.PointZ) test.assert_true(pt_z.is_empty(), "is_empty") - let pt_m = PointM() + let pt_m = Point[DType.int8](CoordDims.PointM) test.assert_true(pt_m.is_empty(), "is_empty") - let pt_zm = PointZM() + let pt_zm = Point[DType.int8](CoordDims.PointZM) test.assert_true(pt_zm.is_empty(), "is_empty") - let pti = Point[2, DType.int8]() - test.assert_true(pti.is_empty(), "is_empty") - fn test_getters() raises: let test = MojoTest("getters") - let pt2 = Point2(lon, lat) + let pt2 = Point(lon, lat) test.assert_true(pt2.x() == lon, "p2.x() == lon") test.assert_true(pt2.y() == lat, "p2.y() == lat") - # Z is compile-time constrained (uncomment to check) - # _ = pt2.z() - # M is compile-time constrained (uncomment to check) - # _ = pt2.m() - let pt_z = PointZ(lon, lat, height) + let pt_z = Point(lon, lat, height) test.assert_true(pt_z.x() == lon, "pt_z.x() == lon") test.assert_true(pt_z.y() == lat, "pt_z.y() == lat") test.assert_true(pt_z.z() == height, "pt_z.z() == height") - # M is compile-time constrained (uncomment to check) - # _ = pt_z.m() - let pt_m = PointM(lon, lat, measure) + let pt_m = Point(lon, lat, measure) test.assert_true(pt_m.x() == lon, "pt_m.x() == lon") test.assert_true(pt_m.y() == lat, "pt_m.y() == lat") test.assert_true(pt_m.m() == measure, "pt_m.m() == measure") - # Z is compile-time constrained (uncomment to check) - # _ = pt_m.z() - let point_zm = PointZM(lon, lat, height, measure) + let point_zm = Point(lon, lat, height, measure) test.assert_true(point_zm.x() == lon, "point_zm.x() == lon") test.assert_true(point_zm.y() == lat, "point_zm.y() == lat") test.assert_true(point_zm.z() == height, "point_zm.z() == height") test.assert_true(point_zm.m() == measure, "point_zm.m() == measure") +fn test_setters() raises: + let test = MojoTest("setters") + + var pt = Point(lon, lat, measure) + pt.set_ogc_dims(CoordDims.PointM) + test.assert_true(pt.ogc_dims == CoordDims.PointM, "set_ogc_dims") + + fn test_json() raises: let test = MojoTest("json") - let pt2 = Point2(lon, lat) + let pt2 = Point(lon, lat) test.assert_true( pt2.json() == '{"type":"Point","coordinates":[-108.68000000000001,38.973999999999997]}', "json()", ) - let pt3 = PointZ(lon, lat, height) + let pt3 = Point(lon, lat, height) test.assert_true( pt3.json() == '{"type":"Point","coordinates":[-108.68000000000001,38.973999999999997,8.0]}', "json()", ) - let pt4 = PointZM(lon, lat, height, measure) + let pt4 = Point(lon, lat, height, measure) test.assert_true( pt4.json() == '{"type":"Point","coordinates":[-108.68000000000001,38.973999999999997,8.0]}', @@ -228,42 +194,57 @@ fn test_json() raises: ) -fn test_wkt() raises: - let test = MojoTest("wkt") - let pt4 = PointZM(lon, lat, height, measure) - test.assert_true( - pt4.wkt() == "POINT(-108.68000000000001 38.973999999999997 8.0 42.0)", "wkt()" - ) +fn test_from_json() raises: + let test = MojoTest("from_json") - let p2i = Point[2, DType.int32](lon, lat) - test.assert_true(p2i.wkt() == "POINT(-108 38)", "wkt()") + let orjson = Python.import_module("orjson") + let json_str = String('{"type":"Point","coordinates":[102.001, 3.502]}') + let json_dict = orjson.loads(json_str) + let pt2 = Point.from_json(json_dict) + test.assert_true(pt2.x() == 102.001, "pt2.x()") + test.assert_true(pt2.y() == 3.502, "pt2.y()") -fn test_from_json() raises: - let test = MojoTest("from_json") - let json_str = String('{"type": "Point","coordinates": [102.0, 3.5]}') - let json = Python.import_module("orjson") - let json_dict = json.loads(json_str) - print("json_dict", json_dict) - let pt1 = Point2.from_json(json_dict) - print(pt1.__repr__()) - test.assert_true(pt1.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + let ptz = Point.from_json(json_dict) + test.assert_true(ptz.x() == 102.001, "ptz.x()") + test.assert_true(ptz.y() == 3.502, "ptz.y()") + + let pt_f32 = Point[dtype=DType.float32].from_json(json_str) + test.assert_true(pt_f32.x() == 102.001, "pt_f32.x()") + test.assert_true(pt_f32.y() == 3.502, "pt_f32.y()") + + let pt_int = Point[dtype=DType.uint8].from_json(json_dict) + test.assert_true(pt_int.x() == 102, "pt_int.x()") + test.assert_true(pt_int.y() == 3, "pt_int.y()") + + +fn test_wkt() raises: + let test = MojoTest("wkt") - # let pt2 = Point2.from_json(json_dict) - # test.assert_true(pt2.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + let pt = Point(lon, lat) + test.assert_true( + pt.wkt() == "Point (-108.68000000000001 38.973999999999997)", "wkt" + ) - # let pt3 = Point[2, DType.uint8].from_json(json_dict) - # test.assert_true(pt3.__repr__() == "Point[2, uint8](102, 3)", "from_json()") + let pt_z = Point(lon, lat, height) + test.assert_true( + pt_z.wkt() == "Point Z (-108.68000000000001 38.973999999999997 8.0)", "wkt" + ) - # let pt4 = Point[2, DType.float64].from_json(json_str) - # test.assert_true(pt4.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + var pt_m = Point(lon, lat, measure) + pt_m.set_ogc_dims(CoordDims.PointM) + test.assert_true( + pt_m.wkt() == "Point M (-108.68000000000001 38.973999999999997 42.0)", "wkt" + ) - # let pt5 = Point2.from_json(json_dict) - # test.assert_true(pt5.__repr__() == "Point[2, float64](102.0, 3.5)", "from_json()") + let pt_zm = Point(lon, lat, height, measure) + test.assert_true( + pt_zm.wkt() == "Point ZM (-108.68000000000001 38.973999999999997 8.0 42.0)", "wkt" + ) - # let pt6 = Point[2, DType.uint8].from_json(json_dict) - # test.assert_true(pt6.__repr__() == "Point[2, uint8](102, 3)", "from_json()") + let p2i = Point[DType.int32](lon, lat) + test.assert_true(p2i.wkt() == "Point (-108 38)", "wkt") fn test_from_wkt() raises: @@ -274,31 +255,30 @@ fn test_from_wkt() raises: with open(path, "rb") as f: wkt = f.read() + let expect_x = -108.68000000000001 + let expect_y = 38.973999999999997 try: - let point_2d = Point2.from_wkt(wkt) - test.assert_true( - point_2d.__repr__() - == "Point[2, float64](-108.68000000000001, 38.973999999999997)", - "from_wkt()", - ) + let point_2d = Point.from_wkt(wkt) + test.assert_true(point_2d.x() == expect_x, "point_2d.x()") + test.assert_true(point_2d.y() == expect_y, "point_2d.y()") - let point_3d = PointZ.from_wkt(wkt) + let point_3d = Point.from_wkt(wkt) test.assert_true( point_3d.__repr__() - == "Point[4, float64](-108.68000000000001, 38.973999999999997, nan, nan)", - "from_wkt()", + == "Point [float64](-108.68000000000001, 38.973999999999997, nan, nan)", + "from_wkt", ) - let point_2d_u8 = Point[2, DType.uint8].from_wkt(wkt) + let point_2d_u8 = Point[DType.uint8].from_wkt(wkt) test.assert_true( - point_2d_u8.__repr__() == "Point[2, uint8](148, 38)", "from_wkt())" + point_2d_u8.__repr__() == "Point [uint8](148, 38, 255, 255)", "from_wkt())" ) - let point_2d_f32 = Point[2, DType.float32].from_wkt(wkt) + let point_2d_f32 = Point[DType.float32].from_wkt(wkt) test.assert_true( point_2d_f32.__repr__() - == "Point[2, float32](-108.68000000000001, 38.973999999999997)", - "from_wkt()", + == "Point [float32](-108.68000030517578, 38.9739990234375, nan, nan)", + "from_wkt", ) except: raise Error( @@ -312,48 +292,48 @@ fn test_from_geoarrow() raises: let ga = Python.import_module("geoarrow.pyarrow") let path = Path("geo_features/test/fixtures/geoarrow/geoarrow-data/example") - + let empty = empty_value[DType.float64]() var file = path / "example-point.arrow" var table = load_geoarrow_test_fixture(file) var geoarrow = ga.as_geoarrow(table["geometry"]) var chunk = geoarrow[0] - let point_2d = Point2.from_geoarrow(table) - let expect_coords_2d = SIMD[Point2.dtype, Point2.simd_dims](30.0, 10.0) - test.assert_true(point_2d.coords == expect_coords_2d, "expect_coords_2d") + let point_2d = Point.from_geoarrow(table) + let expect_point_2d = Point(SIMD[point_2d.dtype, point_2d.simd_dims](30.0, 10.0, empty, empty)) + test.assert_true(point_2d == expect_point_2d, "expect_coords_2d") file = path / "example-point_z.arrow" table = load_geoarrow_test_fixture(file) geoarrow = ga.as_geoarrow(table["geometry"]) chunk = geoarrow[0] # print(chunk.wkt) - let point_3d = PointZ.from_geoarrow(table) - let expect_coords_3d = SIMD[PointZ.dtype, PointZ.simd_dims]( - 30.0, 10.0, 40.0, nan[PointZ.dtype]() + let point_3d = Point.from_geoarrow(table) + let expect_point_3d = Point( + SIMD[point_3d.dtype, point_3d.simd_dims](30.0, 10.0, 40.0, empty_value[point_3d.dtype]()) ) for i in range(3): # cannot check the nan for equality - test.assert_true(point_3d.coords[i] == expect_coords_3d[i], "expect_coords_3d") + test.assert_true(point_3d == expect_point_3d, "expect_point_3d") file = path / "example-point_zm.arrow" table = load_geoarrow_test_fixture(file) geoarrow = ga.as_geoarrow(table["geometry"]) chunk = geoarrow[0] # print(chunk.wkt) - let point_4d = PointZM.from_geoarrow(table) - let expect_coords_4d = SIMD[PointZM.dtype, PointZM.simd_dims]( - 30.0, 10.0, 40.0, 300.0 + let point_4d = Point.from_geoarrow(table) + let expect_point_4d = Point( + SIMD[point_4d.dtype, point_4d.simd_dims](30.0, 10.0, 40.0, 300.0) ) - test.assert_true(point_4d.coords == expect_coords_4d, "expect_coords_4d") + test.assert_true(point_4d == expect_point_4d, "expect_point_4d") file = path / "example-point_m.arrow" table = load_geoarrow_test_fixture(file) geoarrow = ga.as_geoarrow(table["geometry"]) chunk = geoarrow[0] # print(chunk.wkt) - let point_m = PointM.from_geoarrow(table) - let expect_coords_m = SIMD[PointM.dtype, PointM.simd_dims]( - 30.0, 10.0, 300.0, nan[PointM.dtype]() + let point_m = Point.from_geoarrow(table) + let expect_coords_m = SIMD[point_m.dtype, point_m.simd_dims]( + 30.0, 10.0, 300.0, empty_value[point_m.dtype]() ) for i in range(3): # cannot equality check the NaN test.assert_true(point_m.coords[i] == expect_coords_m[i], "expect_coords_m") - test.assert_true(isnan(point_m.coords[3]), "expect_coords_m") + test.assert_true(is_empty(point_m.coords[3]), "expect_coords_m") From 15fed069bafbfecac2abf5e2a8b79528992098d9 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Mon, 11 Dec 2023 23:00:39 -0700 Subject: [PATCH 04/11] Layout tweaks. --- geo_features/geom/layout.mojo | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index c4c34f5..c21d9c7 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -1,10 +1,10 @@ from math.limit import max_finite from tensor import Tensor - +from .traits import Dimensionable @value -struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( - Sized +struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( + Sized, Dimensionable, ): """ Memory layout inspired by, but not exactly following, the GeoArrow format. @@ -13,8 +13,10 @@ struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.ui https://geoarrow.org """ + alias dimensions_idx = 0 + alias features_idx = 1 - var coordinates: Tensor[coord_dtype] + var coordinates: Tensor[dtype] var geometry_offsets: Tensor[offset_dtype] var part_offsets: Tensor[offset_dtype] var ring_offsets: Tensor[offset_dtype] @@ -28,7 +30,7 @@ struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.ui rings_size: Int = 0, ): """ - Create column-oriented tensor: rows (dims) x cols (coords), and offsets vectors. + Create column-oriented tensor: rows (dims) x cols (coords), plus offsets vectors. """ if max_finite[offset_dtype]() < coords_size: print( @@ -36,7 +38,7 @@ struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.ui offset_dtype, coords_size, ) - self.coordinates = Tensor[coord_dtype](dims, coords_size) + self.coordinates = Tensor[dtype](dims, coords_size) self.geometry_offsets = Tensor[offset_dtype](geoms_size) self.part_offsets = Tensor[offset_dtype](parts_size) self.ring_offsets = Tensor[offset_dtype](rings_size) @@ -62,12 +64,12 @@ struct Layout[coord_dtype: DType = DType.float64, offset_dtype: DType = DType.ui fn __len__(self) -> Int: """ - Length is the number of coordinates, and is the constructor's `coords_size` argument. + Length is the number of coordinates (constructor's `coords_size` argument) """ - return self.coordinates.shape()[1] + return self.coordinates.shape()[self.features_idx] fn dims(self) -> Int: """ - Dims is the dimensions argument. + Num dimensions (X, Y, Z, M, etc). (constructor's `dims` argument). """ - return self.coordinates.shape()[0] + return self.coordinates.shape()[self.dimensions_idx] From aebef03710847f9dbd19a57668ae97a67d9ce64f Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Mon, 11 Dec 2023 23:15:01 -0700 Subject: [PATCH 05/11] wip --- geo_features/geom/layout.mojo | 4 ++-- geo_features/geom/point.mojo | 23 ++++++++++++++--------- geo_features/geom/traits.mojo | 11 +++++++++-- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index c21d9c7..eb59e95 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -4,7 +4,7 @@ from .traits import Dimensionable @value struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( - Sized, Dimensionable, + Sized, Dimensionable ): """ Memory layout inspired by, but not exactly following, the GeoArrow format. @@ -68,7 +68,7 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( """ return self.coordinates.shape()[self.features_idx] - fn dims(self) -> Int: + fn dims(self) -> SIMD[DType.uint8, 1]: """ Num dimensions (X, Y, Z, M, etc). (constructor's `dims` argument). """ diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index e47e437..ea27f64 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -4,7 +4,7 @@ from math.limit import max_finite from geo_features.geom.empty import empty_value, is_empty from geo_features.serialization import WKTParser, WKTable, JSONParser, JSONable, Geoarrowable -from .traits import Geometric, Zeroable +from .traits import Geometric, Zeroable, Emptyable from .enums import CoordDims @@ -14,6 +14,7 @@ struct Point[dtype: DType = DType.float64]( CollectionElement, Geoarrowable, Geometric, + Emptyable, JSONable, Sized, Stringable, @@ -34,7 +35,6 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension ```txt ``` - """ alias simd_dims = 4 alias x_index = 0 @@ -155,6 +155,14 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension let coords = SIMD[dtype, Self.simd_dims](0) return Self { coords: coords, ogc_dims: dims } + + @staticmethod + fn empty(dims: CoordDims = CoordDims.Point) -> Self: + """ + Emptyable trait. + """ + return Self.__init__(dims) + # # Getters/Setters # @@ -209,7 +217,7 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension """ return self.coords[self.m_index] - fn dims(self) -> Int: + fn dims(self) -> SIMD[DType.uint8, 1]: """ Dimensionable trait. """ @@ -238,9 +246,6 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension return self.coords[d] if d < Self.simd_dims else empty_value[dtype]() fn __eq__(self, other: Self) -> Bool: - """ - Equality comparison (==). - """ # Warning: NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. @unroll for i in range(Self.simd_dims): @@ -255,9 +260,6 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension return True # equal fn __ne__(self, other: Self) -> Bool: - """ - Inequality comparison (!=). - """ return not self.__eq__(other) fn __repr__(self) -> String: @@ -308,5 +310,8 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension return result fn geoarrow(self) -> PythonObject: + """ + See Geoarrowable trait. + """ # TODO: geoarrow() return None diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo index 2824e55..b977435 100644 --- a/geo_features/geom/traits.mojo +++ b/geo_features/geom/traits.mojo @@ -2,7 +2,7 @@ from .enums import CoordDims trait Dimensionable: - fn dims(self) -> Int: + fn dims(self) -> SIMD[DType.uint8, 1]: ... trait Zeroable: @@ -10,9 +10,16 @@ trait Zeroable: fn zero(dims: CoordDims = CoordDims.Point) -> Self: ... + +trait Emptyable: + @staticmethod + fn empty(dims: CoordDims = CoordDims.Point) -> Self: + ... + + trait Geometric(Dimensionable): """ - TODO: geometric trait + TODO: geometric trait. contains(Geometry): Tests if one geometry contains another. intersects(Geometry): Tests if two geometries intersect. From 7df50cb5539f11ec26e7e0a0b74082ada38fa77f Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Tue, 12 Dec 2023 23:24:17 -0700 Subject: [PATCH 06/11] Traits, envelope improvements. --- geo_features/__init__.mojo | 22 +- geo_features/geom/__init__.mojo | 13 +- geo_features/geom/_utils.mojo | 23 -- geo_features/geom/envelope.mojo | 195 ++++++++------ ...llection.mojo => geometry_collection.nojo} | 0 geo_features/geom/layout.mojo | 12 + .../{line_string.mojo => line_string.nojo} | 4 +- ...ine_string.mojo => multi_line_string.nojo} | 0 .../{multi_point.mojo => multi_point.nojo} | 26 +- ...{multi_polygon.mojo => multi_polygon.nojo} | 0 geo_features/geom/point.mojo | 53 ++-- .../geom/{polygon.mojo => polygon.nojo} | 0 geo_features/geom/traits.mojo | 10 +- geo_features/serialization/wkt.mojo | 5 +- geo_features/test/geom/test_envelope.mojo | 249 ++++++++---------- ...line_string.mojo => test_line_string.nojo} | 0 ...multi_point.mojo => test_multi_point.nojo} | 4 +- geo_features/test/geom/test_point.mojo | 13 +- 18 files changed, 303 insertions(+), 326 deletions(-) delete mode 100644 geo_features/geom/_utils.mojo rename geo_features/geom/{geometry_collection.mojo => geometry_collection.nojo} (100%) rename geo_features/geom/{line_string.mojo => line_string.nojo} (98%) rename geo_features/geom/{multi_line_string.mojo => multi_line_string.nojo} (100%) rename geo_features/geom/{multi_point.mojo => multi_point.nojo} (86%) rename geo_features/geom/{multi_polygon.mojo => multi_polygon.nojo} (100%) rename geo_features/geom/{polygon.mojo => polygon.nojo} (100%) rename geo_features/test/geom/{test_line_string.mojo => test_line_string.nojo} (100%) rename geo_features/test/geom/{test_multi_point.mojo => test_multi_point.nojo} (98%) diff --git a/geo_features/__init__.mojo b/geo_features/__init__.mojo index e1269dd..ad1a5ab 100644 --- a/geo_features/__init__.mojo +++ b/geo_features/__init__.mojo @@ -2,14 +2,14 @@ Core Geometric / Geographic structs and aliases. """ -from geo_features.geom.envelope import ( - Envelope, - Envelope2, - EnvelopeM, - EnvelopeZ, - EnvelopeZM, -) -from geo_features.geom.layout import Layout -from geo_features.geom.line_string import LineString -from geo_features.geom.multi_point import MultiPoint -from geo_features.geom.point import Point, Point2, PointZ, PointM, PointZM +# from geo_features.geom.envelope import ( +# Envelope, +# Envelope2, +# EnvelopeM, +# EnvelopeZ, +# EnvelopeZM, +# ) +# from geo_features.geom.layout import Layout +# from geo_features.geom.line_string import LineString +# from geo_features.geom.multi_point import MultiPoint +# from geo_features.geom.point import Point, Point2, PointZ, PointM, PointZM diff --git a/geo_features/geom/__init__.mojo b/geo_features/geom/__init__.mojo index e458c3c..168d1e8 100644 --- a/geo_features/geom/__init__.mojo +++ b/geo_features/geom/__init__.mojo @@ -1,5 +1,8 @@ -from .point import Point, Point2, PointM, PointZ, PointZM -from .envelope import Envelope, Envelope2, EnvelopeZ, EnvelopeM, EnvelopeZM -from .layout import Layout -from .line_string import LineString, LineString2, LineStringM, LineStringZ, LineStringZM -from .multi_point import MultiPoint, MultiPoint2, MultiPointM, MultiPointZ, MultiPointZM +# from .point import Point +# from .envelope import Envelope +# from .layout import Layout +# from .enums import CoordDims +# from .traits import * + +# # from .line_string import * +# # from .multi_point import * diff --git a/geo_features/geom/_utils.mojo b/geo_features/geom/_utils.mojo deleted file mode 100644 index 2fb942f..0000000 --- a/geo_features/geom/_utils.mojo +++ /dev/null @@ -1,23 +0,0 @@ -from math import nan, isnan -from math.limit import max_finite - - -@always_inline -fn empty_value[dtype: DType]() -> SIMD[dtype, 1]: - """ - Define a special value to mark empty slots or dimensions in structs. Required because SIMD must be power of two. - """ - - @parameter - if dtype.is_floating_point(): - return nan[dtype]() - else: - return max_finite[dtype]() - - -@always_inline -fn is_empty[dtype: DType, simd_dims: Int](value: SIMD[dtype, simd_dims]) -> Bool: - """ - Check for empty value. - """ - return value == empty_value[dtype]() diff --git a/geo_features/geom/envelope.mojo b/geo_features/geom/envelope.mojo index 8680f44..32e7257 100644 --- a/geo_features/geom/envelope.mojo +++ b/geo_features/geom/envelope.mojo @@ -1,81 +1,85 @@ from utils.index import Index -from math.limit import inf, neginf +from math.limit import inf, neginf, max_finite, min_finite + from sys.info import simdwidthof, simdbitwidth from algorithm import vectorize from algorithm.functional import parallelize import math from tensor import Tensor -from geo_features.geom import Point, LineString, Layout - - -alias Envelope2 = Envelope[dims=2, dtype=DType.float64] -""" -Alias for 2D Envelope with dtype float64. -""" - -alias EnvelopeZ = Envelope[dims=4, dtype=DType.float64] -alias EnvelopeM = Envelope[dims=4, dtype=DType.float64] -alias EnvelopeZM = Envelope[dims=4, dtype=DType.float64] - - +from geo_features.geom.empty import empty_value, is_empty +from geo_features.geom.point import Point +from geo_features.geom.enums import CoordDims +from geo_features.geom.layout import Layout +from geo_features.geom.traits import Geometric, Emptyable +from geo_features.geom.empty import empty_value, is_empty @value @register_passable("trivial") -struct Envelope[dims: Int = 2, dtype: DType = DType.float64]: +struct Envelope[dtype: DType]( + Geometric, + Emptyable, +): """ Envelope aka Bounding Box. > "The value of the bbox member must be an array of length 2*n where n is the number of dimensions represented in the contained geometries, with all axes of the most southwesterly point followed by all axes of the more - northeasterly point." https://datatracker.ietf.org/doc/html/rfc7946 + northeasterly point." GeoJSON spec https://datatracker.ietf.org/doc/html/rfc7946 """ - alias CoordsT = SIMD[dtype, 2 * dims] - alias NegInf = neginf[dtype]() - alias Inf = inf[dtype]() - var coords: Self.CoordsT + alias point_simd_dims = 4 + alias envelope_simd_dims = 8 + alias PointCoordsT = SIMD[dtype, Self.point_simd_dims] + alias PointT = Point[dtype] + alias x_index = 0 + alias y_index = 1 + alias z_index = 2 + alias m_index = 3 - fn __init__(point: Point[dims, dtype]) -> Self: + var coords: SIMD[dtype, Self.envelope_simd_dims] + var ogc_dims: CoordDims + + fn __init__(point: Point[dtype]) -> Self: """ Construct Envelope of Point. """ - var coords = Self.CoordsT() + var coords = SIMD[dtype, Self.envelope_simd_dims]() @unroll - for i in range(dims): + for i in range(Self.point_simd_dims): coords[i] = point.coords[i] - coords[i + dims] = point.coords[i] - return Self {coords: coords} + coords[i + Self.point_simd_dims] = point.coords[i] + return Self { coords: coords, ogc_dims: point.ogc_dims } - fn __init__(line_string: LineString[dims, dtype]) -> Self: - """ - Construct Envelope of LineString. - """ - return Self(line_string.data) + # fn __init__(line_string: LineString[simd_dims, dtype]) -> Self: + # """ + # Construct Envelope of LineString. + # """ + # return Self(line_string.data) - fn __init__(data: Layout[coord_dtype=dtype]) -> Self: + fn __init__(data: Layout[dtype=dtype]) -> Self: """ Construct Envelope from memory Layout. """ - alias n = 2 * dims alias nelts = simdbitwidth() - var coords = SIMD[dtype, 2 * dims]() + alias n = Self.envelope_simd_dims + var coords = SIMD[dtype, Self.envelope_simd_dims]() # fill initial values of with inf/neginf at each position in the 2*n array @unroll - for d in range(dims): - coords[d] = Self.Inf # min (southwest) values, start from inf. + for d in range(n): # dims 1:4 + coords[d] = max_finite[dtype]() # min (southwest) values, start from max finite. @unroll - for d in range(dims, n): - coords[d] = Self.NegInf # max (northeast) values, start from neginf + for d in range(Self.point_simd_dims, n): # dims 5:8 + coords[d] = min_finite[dtype]() # max (northeast) values, start from min finite let num_features = data.coordinates.shape()[1] # vectorized load and min/max calculation for each of the dims @unroll - for dim in range(dims): + for dim in range(Self.point_simd_dims): @parameter fn min_max_simd[simd_width: Int](feature_idx: Int): let index = Index(dim, feature_idx) @@ -84,68 +88,111 @@ struct Envelope[dims: Int = 2, dtype: DType = DType.float64]: if min < coords[dim]: coords[dim] = min let max = values.reduce_max() - if max > coords[dims + dim]: - coords[dims + dim] = max + if max > coords[Self.point_simd_dims + dim]: + coords[Self.point_simd_dims + dim] = max vectorize[nelts, min_max_simd](num_features) - return Self {coords: coords} + return Self {coords: coords, ogc_dims: data.ogc_dims } + + + @staticmethod + fn empty(ogc_dims: CoordDims = CoordDims.Point) -> Self: + let coords = SIMD[dtype, Self.envelope_simd_dims](empty_value[dtype]()) + return Self { coords: coords, ogc_dims: ogc_dims } fn __eq__(self, other: Self) -> Bool: - return self.coords == other.coords + # NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. + @unroll + for i in range(Self.envelope_simd_dims): + if is_empty(self.coords[i]) and is_empty(other.coords[i]): + pass # equality at index i + else: + if is_empty(self.coords[i]) or is_empty(other.coords[i]): + return False # early out: one or the other is empty (but not both) -> not equal + if self.coords[i] != other.coords[i]: + return False # not equal + return True # equal fn __ne__(self, other: Self) -> Bool: return not self == other fn __repr__(self) -> String: - var res = "Envelope[" + String(dims) + ", " + dtype.__str__() + "](" - for i in range(2 * dims): - res += self.coords[i] - if i < 2 * dims - 1: + var res = "Envelope [" + dtype.__str__() + "](" + for i in range(Self.envelope_simd_dims): + res += str(self.coords[i]) + if i < Self.envelope_simd_dims - 1: res += ", " res += ")" return res + # + # Getters + # + fn southwesterly_point(self) -> Self.PointT: + alias offset = 0 + return Self.PointT(self.coords.slice[Self.point_simd_dims](offset)) + + fn northeasterly_point(self) -> Self.PointT: + alias offset = Self.point_simd_dims + return Self.PointT(self.coords.slice[Self.point_simd_dims](offset)) + + @always_inline fn min_x(self) -> SIMD[dtype, 1]: - return self.coords[0] + let i = self.x_index + return self.coords[i] + @always_inline fn max_x(self) -> SIMD[dtype, 1]: - let offset = len(self.coords) // 2 - return self.coords[offset] + let i = Self.point_simd_dims + self.x_index + return self.coords[i] + @always_inline fn min_y(self) -> SIMD[dtype, 1]: - return self.coords[1] + alias i = self.y_index + return self.coords[i] + @always_inline fn max_y(self) -> SIMD[dtype, 1]: - let offset = len(self.coords) // 2 + 1 - return self.coords[offset] + alias i = Self.point_simd_dims + Self.y_index + return self.coords[i] + @always_inline fn min_z(self) -> SIMD[dtype, 1]: - @parameter - constrained[dims >= 3, "Envelope dims cannot hold z values"]() - return self.coords[2] + alias i = Self.z_index + return self.coords[i] + @always_inline fn max_z(self) -> SIMD[dtype, 1]: - @parameter - constrained[dims >= 3, "Envelope dims cannot hold z values"]() - let offset = len(self.coords) // 2 + 2 - return self.coords[offset] + alias i = Self.point_simd_dims + Self.z_index + return self.coords[i] + @always_inline fn min_m(self) -> SIMD[dtype, 1]: - @parameter - constrained[dims >= 4, "Envelope dims cannot hold m values"]() - return self.coords[3] + let i = self.m_index + return self.coords[i] + @always_inline fn max_m(self) -> SIMD[dtype, 1]: - @parameter - constrained[dims >= 4, "Envelope dims cannot hold m values"]() - let offset = len(self.coords) // 2 + 3 - return self.coords[offset] - - fn southwesterly_point(self) -> Point[dims, dtype]: - let coords = self.coords.slice[dims](0) - return Point[dims, dtype](coords) - - fn northeasterly_point(self) -> Point[dims, dtype]: - let coords = self.coords.slice[dims](dims) - return Point[dims, dtype](coords) + let i = Self.point_simd_dims + Self.m_index + return self.coords[i] + + fn dims(self) -> SIMD[DType.uint8, 1]: + pass + + fn has_height(self) -> Bool: + return (self.ogc_dims == CoordDims.PointZ) or (self.ogc_dims == CoordDims.PointZM) + + fn has_measure(self) -> Bool: + return (self.ogc_dims == CoordDims.PointM) or (self.ogc_dims == CoordDims.PointZM) + + fn is_empty(self) -> Bool: + return is_empty[dtype](self.coords) + + + fn wkt(self) -> String: + """ + TODO: wkt. + POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin)). + """ + return "POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin))" \ No newline at end of file diff --git a/geo_features/geom/geometry_collection.mojo b/geo_features/geom/geometry_collection.nojo similarity index 100% rename from geo_features/geom/geometry_collection.mojo rename to geo_features/geom/geometry_collection.nojo diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index eb59e95..4a5bd78 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -1,6 +1,9 @@ from math.limit import max_finite from tensor import Tensor + from .traits import Dimensionable +from .enums import CoordDims + @value struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( @@ -20,10 +23,12 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( var geometry_offsets: Tensor[offset_dtype] var part_offsets: Tensor[offset_dtype] var ring_offsets: Tensor[offset_dtype] + var ogc_dims: CoordDims fn __init__( inout self, dims: Int = 2, + ogc_dims: CoordDims = CoordDims.Point, coords_size: Int = 0, geoms_size: Int = 0, parts_size: Int = 0, @@ -42,6 +47,7 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( self.geometry_offsets = Tensor[offset_dtype](geoms_size) self.part_offsets = Tensor[offset_dtype](parts_size) self.ring_offsets = Tensor[offset_dtype](rings_size) + self.ogc_dims = ogc_dims fn __eq__(self, other: Self) -> Bool: """ @@ -73,3 +79,9 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( Num dimensions (X, Y, Z, M, etc). (constructor's `dims` argument). """ return self.coordinates.shape()[self.dimensions_idx] + + fn has_height(self) -> Bool: + return self.ogc_dims == CoordDims.PointZ or self.ogc_dims == CoordDims.PointZM + + fn has_measure(self) -> Bool: + return self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZM diff --git a/geo_features/geom/line_string.mojo b/geo_features/geom/line_string.nojo similarity index 98% rename from geo_features/geom/line_string.mojo rename to geo_features/geom/line_string.nojo index d40f95d..8fd3fc9 100644 --- a/geo_features/geom/line_string.mojo +++ b/geo_features/geom/line_string.nojo @@ -56,12 +56,12 @@ struct LineString[simd_dims: Int, dtype: DType]: """ self.data = Layout[coord_dtype=dtype]() - fn __init__(inout self, *points: Point[simd_dims, dtype]): + fn __init__(inout self, *points: Point[simd_dims, dtype, _]): """ Create LineString from a variadic (var args) list of Points. """ let n = len(points) - var v = DynamicVector[Point[simd_dims, dtype]](n) + var v = DynamicVector[Point](n) for i in range(n): v.push_back(points[i]) self.__init__(v) diff --git a/geo_features/geom/multi_line_string.mojo b/geo_features/geom/multi_line_string.nojo similarity index 100% rename from geo_features/geom/multi_line_string.mojo rename to geo_features/geom/multi_line_string.nojo diff --git a/geo_features/geom/multi_point.mojo b/geo_features/geom/multi_point.nojo similarity index 86% rename from geo_features/geom/multi_point.mojo rename to geo_features/geom/multi_point.nojo index d147369..391d88a 100644 --- a/geo_features/geom/multi_point.mojo +++ b/geo_features/geom/multi_point.nojo @@ -6,30 +6,8 @@ from memory import memcmp from geo_features.serialization import WKTParser, JSONParser from .point import Point from .layout import Layout -from ._utils import is_empty, empty_value - - -alias MultiPoint2 = MultiPoint[simd_dims=2, dtype = DType.float64] -""" -Alias for 2D MultiPoint with dtype float64. -""" - -alias MultiPointZ = MultiPoint[simd_dims=4, dtype = DType.float64] -""" -Alias for 3D MultiPoint with dtype float64, including Z (height) dimension. -Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). -""" - -alias MultiPointM = MultiPoint[simd_dims=4, dtype = DType.float64] -""" -Alias for 3D MultiPoint with dtype float64, including M (measure) dimension. -Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). -""" - -alias MultiPointZM = MultiPoint[simd_dims=4, dtype = DType.float64] -""" -Alias for 4D MultiPoint with dtype float64, including Z (height) and M (measure) dimension. -""" +from .empty import is_empty, empty_value + struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): diff --git a/geo_features/geom/multi_polygon.mojo b/geo_features/geom/multi_polygon.nojo similarity index 100% rename from geo_features/geom/multi_polygon.mojo rename to geo_features/geom/multi_polygon.nojo diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index ea27f64..f24386f 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -4,7 +4,7 @@ from math.limit import max_finite from geo_features.geom.empty import empty_value, is_empty from geo_features.serialization import WKTParser, WKTable, JSONParser, JSONable, Geoarrowable -from .traits import Geometric, Zeroable, Emptyable +from .traits import Geometric, Emptyable from .enums import CoordDims @@ -19,7 +19,6 @@ struct Point[dtype: DType = DType.float64]( Sized, Stringable, WKTable, - Zeroable, ): """ Point is a register-passable, copy-efficient struct holding 2 or more dimension values. @@ -142,20 +141,6 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension result.coords[dim] = val return result - @staticmethod - fn zero(dims: CoordDims = CoordDims.Point) -> Self: - """ - Null Island is an imaginary place located at zero degrees latitude and zero degrees longitude (0°N 0°E) - https://en.wikipedia.org/wiki/Null_Island . - - ### See also - - empty() and is_empty(). note, the zero point is not the same as an empty point! - """ - let coords = SIMD[dtype, Self.simd_dims](0) - return Self { coords: coords, ogc_dims: dims } - - @staticmethod fn empty(dims: CoordDims = CoordDims.Point) -> Self: """ @@ -173,11 +158,25 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension """ self.ogc_dims = ogc_dims + fn dims(self) -> SIMD[DType.uint8, 1]: + """ + Dimensionable trait. + """ + if self.ogc_dims == CoordDims.Point: + return 2 + elif self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZ: + return 3 + elif self.ogc_dims == CoordDims.PointZM: + return 4 + else: + debug_assert(False, "Invalid ogc_dims value") + + fn has_height(self) -> Bool: - return not is_empty(self.coords[Self.z_index]) + return (self.ogc_dims == CoordDims.PointZ) or (self.ogc_dims == CoordDims.PointZM) fn has_measure(self) -> Bool: - return not is_empty(self.coords[Self.m_index]) + return (self.ogc_dims == CoordDims.PointM) or (self.ogc_dims == CoordDims.PointZM) fn is_empty(self) -> Bool: return is_empty[dtype](self.coords) @@ -217,12 +216,6 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension """ return self.coords[self.m_index] - fn dims(self) -> SIMD[DType.uint8, 1]: - """ - Dimensionable trait. - """ - return self.__len__() - # # Dunder methods # @@ -230,14 +223,7 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension """ Returns the number of non-empty dimensions. """ - if len(self.coords > Self.simd_dims): - return len(self.coords) - var dims = 2 - if self.has_height(): - dims += 1 - if self.has_measure(): - dims += 1 - return dims + return self.dims().to_int() fn __getitem__(self, d: Int) -> SIMD[dtype, 1]: """ @@ -246,10 +232,9 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension return self.coords[d] if d < Self.simd_dims else empty_value[dtype]() fn __eq__(self, other: Self) -> Bool: - # Warning: NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. + # NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. @unroll for i in range(Self.simd_dims): - if is_empty(self.coords[i]) and is_empty(other.coords[i]): pass # equality at index i else: diff --git a/geo_features/geom/polygon.mojo b/geo_features/geom/polygon.nojo similarity index 100% rename from geo_features/geom/polygon.mojo rename to geo_features/geom/polygon.nojo diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo index b977435..9b584a6 100644 --- a/geo_features/geom/traits.mojo +++ b/geo_features/geom/traits.mojo @@ -4,12 +4,10 @@ from .enums import CoordDims trait Dimensionable: fn dims(self) -> SIMD[DType.uint8, 1]: ... - -trait Zeroable: - @staticmethod - fn zero(dims: CoordDims = CoordDims.Point) -> Self: + fn has_height(self) -> Bool: + ... + fn has_measure(self) -> Bool: ... - trait Emptyable: @staticmethod @@ -39,4 +37,4 @@ trait Geometric(Dimensionable): rotate(double): Rotates a geometry around a point by a given angle. """ - ... \ No newline at end of file + ... diff --git a/geo_features/serialization/wkt.mojo b/geo_features/serialization/wkt.mojo index 533d2ee..b957dc5 100644 --- a/geo_features/serialization/wkt.mojo +++ b/geo_features/serialization/wkt.mojo @@ -5,9 +5,10 @@ trait WKTable: """ Serializable to and from Well Known Text (WKT). - ### Spec + ### Spec(s) - https://libgeos.org/specifications/wkt + - https://libgeos.org/specifications/wkt + - https://www.ogc.org/standard/sfa/ """ @staticmethod diff --git a/geo_features/test/geom/test_envelope.mojo b/geo_features/test/geom/test_envelope.mojo index 3cb68ed..af0b3fb 100644 --- a/geo_features/test/geom/test_envelope.mojo +++ b/geo_features/test/geom/test_envelope.mojo @@ -6,25 +6,12 @@ from random import rand from geo_features.test.pytest import MojoTest from geo_features.test.constants import lon, lat, height, measure -from geo_features.geom import ( - Layout, - Point, - Point2, - PointZ, - PointM, - PointZM, - LineString, - LineString2, - LineStringM, - LineStringZ, - LineStringZM, - Envelope, - Envelope2, - EnvelopeZ, - EnvelopeM, - EnvelopeZM, -) +from geo_features.geom.envelope import Envelope +from geo_features.geom.point import Point +from geo_features.geom.enums import CoordDims +from geo_features.geom.layout import Layout +from geo_features.geom.traits import Geometric, Emptyable fn main() raises: test_envelope() @@ -44,31 +31,30 @@ fn test_envelope() raises: # test_from_json() # test_from_wkt() - fn test_constructors() raises: let test = MojoTest("constructors, aliases") # from Point - _ = Envelope(Point2(lon, lat)) - _ = EnvelopeZ(PointZ(lon, lat, height)) - _ = EnvelopeM(PointM(lon, lat, measure)) - _ = EnvelopeZM(PointZM(lon, lat, height, measure)) + _ = Envelope(Point(lon, lat)) + _ = Envelope(Point(lon, lat, height)) + _ = Envelope(Point(lon, lat, measure)) + _ = Envelope(Point(lon, lat, height, measure)) - _ = Envelope(Point[2, DType.int8](lon, lat)) - _ = Envelope(Point[4, DType.float64](lon, lat, height, measure)) + _ = Envelope(Point[DType.int8](lon, lat)) + _ = Envelope(Point[DType.float64](lon, lat, height, measure)) # from LineString - alias Point2_f16 = Point[2, DType.float16] - _ = Envelope( - LineString( - Point2_f16(lon, lat), - Point2_f16(lon + 1, lat + 1), - Point2_f16(lon + 2, lat + 2), - Point2_f16(lon + 3, lat + 3), - Point2_f16(lon + 4, lat + 4), - Point2_f16(lon + 5, lat + 5), - ) - ) + # alias Point2_f16 = Point[DType.float16] + # _ = Envelope( + # LineString( + # Point2_f16(lon, lat), + # Point2_f16(lon + 1, lat + 1), + # Point2_f16(lon + 2, lat + 2), + # Point2_f16(lon + 3, lat + 3), + # Point2_f16(lon + 4, lat + 4), + # Point2_f16(lon + 5, lat + 5), + # ) + # ) fn test_repr() raises: @@ -76,94 +62,93 @@ fn test_repr() raises: # TODO: more variations of envelope structs - var e = Envelope(Point2(lon, lat)) + let e_pt2 = Envelope(Point(lon, lat)) test.assert_true( - e.__repr__() - == "Envelope[2, float64](-108.68000000000001, 38.973999999999997," - " -108.68000000000001, 38.973999999999997)", + e_pt2.__repr__() + == "Envelope [float64](-108.68000000000001, 38.973999999999997, nan, nan, -108.68000000000001, 38.973999999999997, nan, nan)", "__repr__", ) - e = Envelope( - LineString(Point2(lon, lat), Point2(lon + 1, lat + 1), Point2(lon + 2, lat + 2)) - ) - test.assert_true( - e.__repr__() - == "Envelope[2, float64](-108.68000000000001, 38.973999999999997," - " -106.68000000000001, 40.973999999999997)", - "__repr__", - ) + # e = Envelope( + # LineString(Point2(lon, lat), Point2(lon + 1, lat + 1), Point2(lon + 2, lat + 2)) + # ) + # test.assert_true( + # e.__repr__() + # == "Envelope[float64](-108.68000000000001, 38.973999999999997," + # " -106.68000000000001, 40.973999999999997)", + # "__repr__", + # ) fn test_min_max() raises: let test = MojoTest("min/max methods") - let e_of_pt2 = Envelope(Point2(lon, lat)) + let e_of_pt2 = Envelope(Point(lon, lat)) test.assert_true(e_of_pt2.min_x() == lon, "min_x") test.assert_true(e_of_pt2.min_y() == lat, "min_y") test.assert_true(e_of_pt2.max_x() == lon, "max_x") test.assert_true(e_of_pt2.max_y() == lat, "max_y") - let e_of_ls2 = Envelope( - LineString( - Point2(lon, lat), - Point2(lon + 1, lat + 1), - Point2(lon + 2, lat + 5), - Point2(lon + 5, lat + 3), - Point2(lon + 4, lat + 4), - Point2(lon + 3, lat + 2), - ) - ) - test.assert_true(e_of_ls2.min_x() == lon, "min_x") - test.assert_true(e_of_ls2.min_y() == lat, "min_y") - - test.assert_true(e_of_ls2.max_x() == lon + 5, "max_x") - test.assert_true(e_of_ls2.max_y() == lat + 5, "max_y") - - let e_of_ls3 = Envelope( - LineString( - PointZ(lon, lat, height), - PointZ(lon + 1, lat + 1, height - 1), - PointZ(lon + 2, lat + 2, height - 2), - PointZ(lon + 7, lat + 5, height - 5), - PointZ(lon + 4, lat + 4, height - 4), - PointZ(lon + 5, lat + 3, height - 3), - ) - ) - test.assert_true(e_of_ls3.min_x() == lon, "min_x") - test.assert_true(e_of_ls3.min_y() == lat, "min_y") - test.assert_true(e_of_ls3.min_z() == height - 5, "min_z") - - test.assert_true(e_of_ls3.max_x() == lon + 7, "max_x") - test.assert_true(e_of_ls3.max_y() == lat + 5, "max_y") - test.assert_true(e_of_ls3.max_z() == height, "max_z") - - let e_of_ls4 = Envelope( - LineString( - PointZ(lon, lat, height, measure), - PointZ(lon + 1, lat + 1, height - 1, measure + 0.01), - PointZ(lon + 2, lat + 2, height - 7, measure + 0.05), - PointZ(lon + 5, lat + 3, height - 3, measure + 0.03), - PointZ(lon + 4, lat + 5, height - 4, measure + 0.04), - PointZ(lon + 3, lat + 4, height - 5, measure + 0.02), - ) - ) - - test.assert_true(e_of_ls4.min_x() == lon, "min_x") - test.assert_true(e_of_ls4.min_y() == lat, "min_y") - test.assert_true(e_of_ls4.min_z() == height - 7, "min_z") - test.assert_true(e_of_ls4.min_m() == measure, "min_m") - - test.assert_true(e_of_ls4.max_x() == lon + 5, "max_x") - test.assert_true(e_of_ls4.max_y() == lat + 5, "max_y") - test.assert_true(e_of_ls4.max_z() == height, "max_z") - test.assert_true(e_of_ls4.max_m() == measure + 0.05, "max_m") + # let e_of_ls2 = Envelope( + # LineString( + # Point2(lon, lat), + # Point2(lon + 1, lat + 1), + # Point2(lon + 2, lat + 5), + # Point2(lon + 5, lat + 3), + # Point2(lon + 4, lat + 4), + # Point2(lon + 3, lat + 2), + # ) + # ) + # test.assert_true(e_of_ls2.min_x() == lon, "min_x") + # test.assert_true(e_of_ls2.min_y() == lat, "min_y") + + # test.assert_true(e_of_ls2.max_x() == lon + 5, "max_x") + # test.assert_true(e_of_ls2.max_y() == lat + 5, "max_y") + + # let e_of_ls3 = Envelope( + # LineStringZ( + # PointZ(lon, lat, height), + # PointZ(lon + 1, lat + 1, height - 1), + # PointZ(lon + 2, lat + 2, height - 2), + # PointZ(lon + 7, lat + 5, height - 5), + # PointZ(lon + 4, lat + 4, height - 4), + # PointZ(lon + 5, lat + 3, height - 3), + # ) + # ) + # test.assert_true(e_of_ls3.min_x() == lon, "min_x") + # test.assert_true(e_of_ls3.min_y() == lat, "min_y") + # test.assert_true(e_of_ls3.min_z() == height - 5, "min_z") + + # test.assert_true(e_of_ls3.max_x() == lon + 7, "max_x") + # test.assert_true(e_of_ls3.max_y() == lat + 5, "max_y") + # test.assert_true(e_of_ls3.max_z() == height, "max_z") + + # let e_of_ls4 = Envelope( + # LineString( + # PointZ(lon, lat, height, measure), + # PointZ(lon + 1, lat + 1, height - 1, measure + 0.01), + # PointZ(lon + 2, lat + 2, height - 7, measure + 0.05), + # PointZ(lon + 5, lat + 3, height - 3, measure + 0.03), + # PointZ(lon + 4, lat + 5, height - 4, measure + 0.04), + # PointZ(lon + 3, lat + 4, height - 5, measure + 0.02), + # ) + # ) + + # test.assert_true(e_of_ls4.min_x() == lon, "min_x") + # test.assert_true(e_of_ls4.min_y() == lat, "min_y") + # test.assert_true(e_of_ls4.min_z() == height - 7, "min_z") + # test.assert_true(e_of_ls4.min_m() == measure, "min_m") + + # test.assert_true(e_of_ls4.max_x() == lon + 5, "max_x") + # test.assert_true(e_of_ls4.max_y() == lat + 5, "max_y") + # test.assert_true(e_of_ls4.max_z() == height, "max_z") + # test.assert_true(e_of_ls4.max_m() == measure + 0.05, "max_m") fn test_southwesterly_point() raises: let test = MojoTest("southwesterly_point") - let e = Envelope(Point2(lon, lat)) + let e = Envelope(Point(lon, lat)) let sw_pt = e.southwesterly_point() test.assert_true(sw_pt.x() == lon, "southwesterly_point") test.assert_true(sw_pt.y() == lat, "southwesterly_point") @@ -171,7 +156,7 @@ fn test_southwesterly_point() raises: fn test_northeasterly_point() raises: let test = MojoTest("northeasterly_point") - let e = Envelope(Point2(lon, lat)) + let e = Envelope(Point(lon, lat)) let sw_pt = e.northeasterly_point() test.assert_true(sw_pt.x() == lon, "northeasterly_point") test.assert_true(sw_pt.y() == lat, "northeasterly_point") @@ -193,23 +178,23 @@ fn test_with_geos() raises: # LineString - let path = Path("geo_features/test/fixtures/geojson/line_string") - let fixtures = VariadicList("curved.geojson", "straight.geojson", "zigzag.geojson") - for i in range(len(fixtures)): - let file = path / fixtures[i] - with open(file, "r") as f: - let geojson = f.read() - let geojson_dict = json.loads(geojson) - let geometry = shape(geojson_dict) - let expect_bounds = geometry.bounds - let lstr = LineString2.from_json(geojson_dict) - let env = Envelope(lstr) - for i in range(4): - test.assert_true( - env.coords[i].cast[DType.float64]() - == expect_bounds[i].to_float64(), - "envelope index:" + String(i), - ) + # let path = Path("geo_features/test/fixtures/geojson/line_string") + # let fixtures = VariadicList("curved.geojson", "straight.geojson", "zigzag.geojson") + # for i in range(len(fixtures)): + # let file = path / fixtures[i] + # with open(file, "r") as f: + # let geojson = f.read() + # let geojson_dict = json.loads(geojson) + # let geometry = shape(geojson_dict) + # let expect_bounds = geometry.bounds + # let lstr = LineString.from_json(geojson_dict) + # let env = Envelope(lstr) + # for i in range(4): + # test.assert_true( + # env.coords[i].cast[DType.float64]() + # == expect_bounds[i].to_float64(), + # "envelope index:" + String(i), + # ) fn test_equality_ops() raises: @@ -218,20 +203,20 @@ fn test_equality_ops() raises: """ let test = MojoTest("equality ops") - let e2 = Envelope(Point2(lon, lat)) - let e2_eq = Envelope(Point2(lon, lat)) - let e2_ne = Envelope(Point2(lon + 0.01, lat - 0.02)) + let e2 = Envelope(Point(lon, lat)) + let e2_eq = Envelope(Point(lon, lat)) + let e2_ne = Envelope(Point(lon + 0.01, lat - 0.02)) test.assert_true(e2 == e2_eq, "__eq__") test.assert_true(e2 != e2_ne, "__ne__") - let e3 = Envelope(PointZ(lon, lat, height)) - let e3_eq = Envelope(PointZ(lon, lat, height)) - let e3_ne = Envelope(PointZ(lon, lat, height * 2)) + let e3 = Envelope(Point(lon, lat, height)) + let e3_eq = Envelope(Point(lon, lat, height)) + let e3_ne = Envelope(Point(lon, lat, height * 2)) test.assert_true(e3 == e3_eq, "__eq__") test.assert_true(e3 != e3_ne, "__ne__") - let e4 = Envelope(PointZM(lon, lat, height, measure)) - let e4_eq = Envelope(PointZM(lon, lat, height, measure)) - let e4_ne = Envelope(PointZM(lon, lat, height, measure * 2)) + let e4 = Envelope(Point(lon, lat, height, measure)) + let e4_eq = Envelope(Point(lon, lat, height, measure)) + let e4_ne = Envelope(Point(lon, lat, height, measure * 2)) test.assert_true(e4 == e4_eq, "__eq__") test.assert_true(e4 != e4_ne, "__ne__") diff --git a/geo_features/test/geom/test_line_string.mojo b/geo_features/test/geom/test_line_string.nojo similarity index 100% rename from geo_features/test/geom/test_line_string.mojo rename to geo_features/test/geom/test_line_string.nojo diff --git a/geo_features/test/geom/test_multi_point.mojo b/geo_features/test/geom/test_multi_point.nojo similarity index 98% rename from geo_features/test/geom/test_multi_point.mojo rename to geo_features/test/geom/test_multi_point.nojo index 5aa8659..79fc9ed 100644 --- a/geo_features/test/geom/test_multi_point.mojo +++ b/geo_features/test/geom/test_multi_point.nojo @@ -3,6 +3,7 @@ from python.object import PythonObject from utils.vector import DynamicVector from utils.index import Index +from geo_features.test.constants import lat, lon, height, measure from geo_features.test.pytest import MojoTest from geo_features.geom import ( Point, @@ -11,9 +12,6 @@ from geo_features.geom import ( PointM, PointZM, ) -from geo_features.test.constants import lat, lon, height, measure - -# TODO: it should be possible to use implicit parameters and only import MultiPoint into this module (not MultiPoint2 etc) from geo_features.geom import ( MultiPoint, MultiPoint2, diff --git a/geo_features/test/geom/test_point.mojo b/geo_features/test/geom/test_point.mojo index f42759d..65714f7 100644 --- a/geo_features/test/geom/test_point.mojo +++ b/geo_features/test/geom/test_point.mojo @@ -15,7 +15,6 @@ fn main() raises: test_has_height() test_has_measure() test_equality_ops() - test_zero() test_is_empty() test_getters() test_setters() @@ -72,7 +71,9 @@ fn test_has_height() raises: fn test_has_measure() raises: let test = MojoTest("has_measure") - let pt_m = Point(lon, lat, measure) + var pt_m = Point(lon, lat, measure) + test.assert_true(not pt_m.has_measure(), "has_measure") + pt_m.set_ogc_dims(CoordDims.PointM) test.assert_true(pt_m.has_measure(), "has_measure") @@ -111,14 +112,6 @@ fn test_equality_ops() raises: test.assert_true(p4 != p4b, "__eq__") -fn test_zero() raises: - let test = MojoTest("zero") - - let pt = Point.zero() - test.assert_true(pt.x() == 0, "zero().x()") - test.assert_true(pt.y() == 0, "zero().y()") - - fn test_is_empty() raises: let test = MojoTest("is_empty") From d93499d106e11fbe76be2988322a9b5bf8cb6fd0 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Tue, 12 Dec 2023 23:26:16 -0700 Subject: [PATCH 07/11] Format. --- geo_features/geom/empty.mojo | 2 + geo_features/geom/enums.mojo | 3 +- geo_features/geom/envelope.mojo | 30 ++++++++------ geo_features/geom/layout.mojo | 1 + geo_features/geom/point.mojo | 46 ++++++++++++++-------- geo_features/geom/traits.mojo | 4 ++ geo_features/serialization/wkt.mojo | 1 + geo_features/test/geom/test_empty.mojo | 2 +- geo_features/test/geom/test_envelope.mojo | 5 ++- geo_features/test/geom/test_point.mojo | 48 +++++++++++++++-------- 10 files changed, 95 insertions(+), 47 deletions(-) diff --git a/geo_features/geom/empty.mojo b/geo_features/geom/empty.mojo index e407178..ae1126f 100644 --- a/geo_features/geom/empty.mojo +++ b/geo_features/geom/empty.mojo @@ -7,6 +7,7 @@ fn empty_value[dtype: DType]() -> SIMD[dtype, 1]: """ Define a special value to mark empty slots or dimensions in structs. Required because SIMD must be power of two. """ + @parameter if dtype.is_floating_point(): return nan[dtype]() @@ -20,6 +21,7 @@ fn is_empty[dtype: DType, simd_width: Int](value: SIMD[dtype, simd_width]) -> Bo Check for empty value. Note: NaN cannot be compared by equality. This helper function calls isnan() if the dtype is floating point. """ + @parameter if dtype.is_floating_point(): return isnan[dtype, simd_width](value) diff --git a/geo_features/geom/enums.mojo b/geo_features/geom/enums.mojo index c8a16aa..ed155e8 100644 --- a/geo_features/geom/enums.mojo +++ b/geo_features/geom/enums.mojo @@ -3,6 +3,7 @@ struct CoordDims(Stringable): """ Enum for encoding the OGC/WKT variants of Points. """ + # TODO: use a real enum here, when mojo supports. var value: SIMD[DType.uint8, 1] @@ -25,7 +26,7 @@ struct CoordDims(Stringable): """ fn __init__(value: Int) -> Self: - return Self { value: value } + return Self {value: value} fn __eq__(self, other: Self) -> Bool: return self.value == other.value diff --git a/geo_features/geom/envelope.mojo b/geo_features/geom/envelope.mojo index 32e7257..b287c1f 100644 --- a/geo_features/geom/envelope.mojo +++ b/geo_features/geom/envelope.mojo @@ -14,6 +14,7 @@ from geo_features.geom.layout import Layout from geo_features.geom.traits import Geometric, Emptyable from geo_features.geom.empty import empty_value, is_empty + @value @register_passable("trivial") struct Envelope[dtype: DType]( @@ -50,7 +51,7 @@ struct Envelope[dtype: DType]( for i in range(Self.point_simd_dims): coords[i] = point.coords[i] coords[i + Self.point_simd_dims] = point.coords[i] - return Self { coords: coords, ogc_dims: point.ogc_dims } + return Self {coords: coords, ogc_dims: point.ogc_dims} # fn __init__(line_string: LineString[simd_dims, dtype]) -> Self: # """ @@ -69,17 +70,22 @@ struct Envelope[dtype: DType]( # fill initial values of with inf/neginf at each position in the 2*n array @unroll for d in range(n): # dims 1:4 - coords[d] = max_finite[dtype]() # min (southwest) values, start from max finite. + coords[d] = max_finite[ + dtype + ]() # min (southwest) values, start from max finite. @unroll for d in range(Self.point_simd_dims, n): # dims 5:8 - coords[d] = min_finite[dtype]() # max (northeast) values, start from min finite + coords[d] = min_finite[ + dtype + ]() # max (northeast) values, start from min finite let num_features = data.coordinates.shape()[1] # vectorized load and min/max calculation for each of the dims @unroll for dim in range(Self.point_simd_dims): + @parameter fn min_max_simd[simd_width: Int](feature_idx: Int): let index = Index(dim, feature_idx) @@ -93,20 +99,19 @@ struct Envelope[dtype: DType]( vectorize[nelts, min_max_simd](num_features) - return Self {coords: coords, ogc_dims: data.ogc_dims } - + return Self {coords: coords, ogc_dims: data.ogc_dims} @staticmethod fn empty(ogc_dims: CoordDims = CoordDims.Point) -> Self: let coords = SIMD[dtype, Self.envelope_simd_dims](empty_value[dtype]()) - return Self { coords: coords, ogc_dims: ogc_dims } + return Self {coords: coords, ogc_dims: ogc_dims} fn __eq__(self, other: Self) -> Bool: # NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. @unroll for i in range(Self.envelope_simd_dims): if is_empty(self.coords[i]) and is_empty(other.coords[i]): - pass # equality at index i + pass # equality at index i else: if is_empty(self.coords[i]) or is_empty(other.coords[i]): return False # early out: one or the other is empty (but not both) -> not equal @@ -181,18 +186,21 @@ struct Envelope[dtype: DType]( pass fn has_height(self) -> Bool: - return (self.ogc_dims == CoordDims.PointZ) or (self.ogc_dims == CoordDims.PointZM) + return (self.ogc_dims == CoordDims.PointZ) or ( + self.ogc_dims == CoordDims.PointZM + ) fn has_measure(self) -> Bool: - return (self.ogc_dims == CoordDims.PointM) or (self.ogc_dims == CoordDims.PointZM) + return (self.ogc_dims == CoordDims.PointM) or ( + self.ogc_dims == CoordDims.PointZM + ) fn is_empty(self) -> Bool: return is_empty[dtype](self.coords) - fn wkt(self) -> String: """ TODO: wkt. POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin)). """ - return "POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin))" \ No newline at end of file + return "POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin))" diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index 4a5bd78..e32a535 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -16,6 +16,7 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( https://geoarrow.org """ + alias dimensions_idx = 0 alias features_idx = 1 diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index f24386f..53b7043 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -3,7 +3,13 @@ from math import nan, isnan from math.limit import max_finite from geo_features.geom.empty import empty_value, is_empty -from geo_features.serialization import WKTParser, WKTable, JSONParser, JSONable, Geoarrowable +from geo_features.serialization import ( + WKTParser, + WKTable, + JSONParser, + JSONable, + Geoarrowable, +) from .traits import Geometric, Emptyable from .enums import CoordDims @@ -21,20 +27,21 @@ struct Point[dtype: DType = DType.float64]( WKTable, ): """ -Point is a register-passable, copy-efficient struct holding 2 or more dimension values. + Point is a register-passable, copy-efficient struct holding 2 or more dimension values. -### Parameters + ### Parameters - - dtype: supports any float or integer type (default = float64) + - dtype: supports any float or integer type (default = float64) -### Memory Layouts + ### Memory Layouts - Some examples of memory layout using Mojo SIMD variables. + Some examples of memory layout using Mojo SIMD variables. -```txt + ```txt -``` + ``` """ + alias simd_dims = 4 alias x_index = 0 alias y_index = 1 @@ -53,7 +60,7 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension """ let empty = empty_value[dtype]() let coords = SIMD[dtype, Self.simd_dims](empty) - return Self { coords: coords, ogc_dims: dims } + return Self {coords: coords, ogc_dims: dims} fn __init__(*coords_list: SIMD[dtype, 1]) -> Self: """ @@ -79,9 +86,11 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension elif n >= 4: ogc_dims = CoordDims.PointZM - return Self { coords: coords, ogc_dims: ogc_dims } + return Self {coords: coords, ogc_dims: ogc_dims} - fn __init__(coords: SIMD[dtype, Self.simd_dims], dims: CoordDims = CoordDims.Point) -> Self: + fn __init__( + coords: SIMD[dtype, Self.simd_dims], dims: CoordDims = CoordDims.Point + ) -> Self: """ Create Point from existing SIMD vector of coordinates. """ @@ -171,12 +180,15 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension else: debug_assert(False, "Invalid ogc_dims value") - fn has_height(self) -> Bool: - return (self.ogc_dims == CoordDims.PointZ) or (self.ogc_dims == CoordDims.PointZM) + return (self.ogc_dims == CoordDims.PointZ) or ( + self.ogc_dims == CoordDims.PointZM + ) fn has_measure(self) -> Bool: - return (self.ogc_dims == CoordDims.PointM) or (self.ogc_dims == CoordDims.PointZM) + return (self.ogc_dims == CoordDims.PointM) or ( + self.ogc_dims == CoordDims.PointZM + ) fn is_empty(self) -> Bool: return is_empty[dtype](self.coords) @@ -236,7 +248,7 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension @unroll for i in range(Self.simd_dims): if is_empty(self.coords[i]) and is_empty(other.coords[i]): - pass # equality at index i + pass # equality at index i else: if is_empty(self.coords[i]) or is_empty(other.coords[i]): return False # early out: one or the other is empty (but not both) -> not equal @@ -249,10 +261,10 @@ Point is a register-passable, copy-efficient struct holding 2 or more dimension fn __repr__(self) -> String: let point_variant = str(self.ogc_dims) - var res = point_variant + " [" + dtype.__str__() + "](" + var res = point_variant + " [" + dtype.__str__() + "](" for i in range(Self.simd_dims): res += str(self.coords[i]) - if i < Self.simd_dims -1: + if i < Self.simd_dims - 1: res += ", " res += ")" return res diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo index 9b584a6..15c5afc 100644 --- a/geo_features/geom/traits.mojo +++ b/geo_features/geom/traits.mojo @@ -4,11 +4,14 @@ from .enums import CoordDims trait Dimensionable: fn dims(self) -> SIMD[DType.uint8, 1]: ... + fn has_height(self) -> Bool: ... + fn has_measure(self) -> Bool: ... + trait Emptyable: @staticmethod fn empty(dims: CoordDims = CoordDims.Point) -> Self: @@ -37,4 +40,5 @@ trait Geometric(Dimensionable): rotate(double): Rotates a geometry around a point by a given angle. """ + ... diff --git a/geo_features/serialization/wkt.mojo b/geo_features/serialization/wkt.mojo index b957dc5..d0f995c 100644 --- a/geo_features/serialization/wkt.mojo +++ b/geo_features/serialization/wkt.mojo @@ -1,6 +1,7 @@ from python import Python from python.object import PythonObject + trait WKTable: """ Serializable to and from Well Known Text (WKT). diff --git a/geo_features/test/geom/test_empty.mojo b/geo_features/test/geom/test_empty.mojo index 94c4459..0acc598 100644 --- a/geo_features/test/geom/test_empty.mojo +++ b/geo_features/test/geom/test_empty.mojo @@ -22,4 +22,4 @@ fn main() raises: test.assert_true(is_empty(empty_uint), "empty_uint") test.assert_true(not is_empty[DType.float64, 1](42), "not empty") - test.assert_true(not is_empty[DType.uint16, 1](42), "not empty") \ No newline at end of file + test.assert_true(not is_empty[DType.uint16, 1](42), "not empty") diff --git a/geo_features/test/geom/test_envelope.mojo b/geo_features/test/geom/test_envelope.mojo index af0b3fb..d3787b7 100644 --- a/geo_features/test/geom/test_envelope.mojo +++ b/geo_features/test/geom/test_envelope.mojo @@ -13,6 +13,7 @@ from geo_features.geom.enums import CoordDims from geo_features.geom.layout import Layout from geo_features.geom.traits import Geometric, Emptyable + fn main() raises: test_envelope() @@ -31,6 +32,7 @@ fn test_envelope() raises: # test_from_json() # test_from_wkt() + fn test_constructors() raises: let test = MojoTest("constructors, aliases") @@ -65,7 +67,8 @@ fn test_repr() raises: let e_pt2 = Envelope(Point(lon, lat)) test.assert_true( e_pt2.__repr__() - == "Envelope [float64](-108.68000000000001, 38.973999999999997, nan, nan, -108.68000000000001, 38.973999999999997, nan, nan)", + == "Envelope [float64](-108.68000000000001, 38.973999999999997, nan, nan," + " -108.68000000000001, 38.973999999999997, nan, nan)", "__repr__", ) diff --git a/geo_features/test/geom/test_point.mojo b/geo_features/test/geom/test_point.mojo index 65714f7..15e19d7 100644 --- a/geo_features/test/geom/test_point.mojo +++ b/geo_features/test/geom/test_point.mojo @@ -37,30 +37,42 @@ fn test_constructors(): _ = Point[DType.float32]() _ = Point[DType.int32](lon, lat) _ = Point[DType.float32](lon, lat) - _ = Point[dtype=DType.float16]( - SIMD[DType.float16, 4](lon, lat, height, measure) - ) - _ = Point[dtype=DType.float32]( - SIMD[DType.float32, 4](lon, lat, height, measure) - ) + _ = Point[dtype = DType.float16](SIMD[DType.float16, 4](lon, lat, height, measure)) + _ = Point[dtype = DType.float32](SIMD[DType.float32, 4](lon, lat, height, measure)) fn test_repr() raises: let test = MojoTest("repr") let pt = Point(lon, lat) - test.assert_true(pt.__repr__() == "Point [float64](-108.68000000000001, 38.973999999999997, nan, nan)", "repr") + test.assert_true( + pt.__repr__() + == "Point [float64](-108.68000000000001, 38.973999999999997, nan, nan)", + "repr", + ) let pt_z = Point(lon, lat, height) - test.assert_true(pt_z.__repr__() == "Point Z [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", "repr") + test.assert_true( + pt_z.__repr__() + == "Point Z [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", + "repr", + ) # the variadic list constructor cannot distinguish Point Z from Point M, so use the set_ogc_dims method. var pt_m = pt_z pt_m.set_ogc_dims(CoordDims.PointM) - test.assert_true(pt_m.__repr__() == "Point M [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", "repr") + test.assert_true( + pt_m.__repr__() + == "Point M [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", + "repr", + ) let pt_zm = Point(lon, lat, height, measure) - test.assert_true(pt_zm.__repr__() == "Point ZM [float64](-108.68000000000001, 38.973999999999997, 8.0, 42.0)", "repr") + test.assert_true( + pt_zm.__repr__() + == "Point ZM [float64](-108.68000000000001, 38.973999999999997, 8.0, 42.0)", + "repr", + ) fn test_has_height() raises: @@ -187,7 +199,6 @@ fn test_json() raises: ) - fn test_from_json() raises: let test = MojoTest("from_json") @@ -203,11 +214,11 @@ fn test_from_json() raises: test.assert_true(ptz.x() == 102.001, "ptz.x()") test.assert_true(ptz.y() == 3.502, "ptz.y()") - let pt_f32 = Point[dtype=DType.float32].from_json(json_str) + let pt_f32 = Point[dtype = DType.float32].from_json(json_str) test.assert_true(pt_f32.x() == 102.001, "pt_f32.x()") test.assert_true(pt_f32.y() == 3.502, "pt_f32.y()") - let pt_int = Point[dtype=DType.uint8].from_json(json_dict) + let pt_int = Point[dtype = DType.uint8].from_json(json_dict) test.assert_true(pt_int.x() == 102, "pt_int.x()") test.assert_true(pt_int.y() == 3, "pt_int.y()") @@ -233,7 +244,8 @@ fn test_wkt() raises: let pt_zm = Point(lon, lat, height, measure) test.assert_true( - pt_zm.wkt() == "Point ZM (-108.68000000000001 38.973999999999997 8.0 42.0)", "wkt" + pt_zm.wkt() == "Point ZM (-108.68000000000001 38.973999999999997 8.0 42.0)", + "wkt", ) let p2i = Point[DType.int32](lon, lat) @@ -291,7 +303,9 @@ fn test_from_geoarrow() raises: var geoarrow = ga.as_geoarrow(table["geometry"]) var chunk = geoarrow[0] let point_2d = Point.from_geoarrow(table) - let expect_point_2d = Point(SIMD[point_2d.dtype, point_2d.simd_dims](30.0, 10.0, empty, empty)) + let expect_point_2d = Point( + SIMD[point_2d.dtype, point_2d.simd_dims](30.0, 10.0, empty, empty) + ) test.assert_true(point_2d == expect_point_2d, "expect_coords_2d") file = path / "example-point_z.arrow" @@ -301,7 +315,9 @@ fn test_from_geoarrow() raises: # print(chunk.wkt) let point_3d = Point.from_geoarrow(table) let expect_point_3d = Point( - SIMD[point_3d.dtype, point_3d.simd_dims](30.0, 10.0, 40.0, empty_value[point_3d.dtype]()) + SIMD[point_3d.dtype, point_3d.simd_dims]( + 30.0, 10.0, 40.0, empty_value[point_3d.dtype]() + ) ) for i in range(3): # cannot check the nan for equality From 9c79f214434ce243aa1312052565e6329b9fc563 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Fri, 15 Dec 2023 00:48:51 -0700 Subject: [PATCH 08/11] wip --- geo_features/geom/enums.mojo | 45 ++++-- geo_features/geom/envelope.mojo | 31 +++- geo_features/geom/layout.mojo | 7 +- .../{multi_point.nojo => multi_point.mojo} | 131 +++++++++++----- geo_features/geom/point.mojo | 45 ++---- geo_features/geom/traits.mojo | 47 +++--- .../test/fixtures/wkt/multi_point/point.wkt | 1 + .../test/fixtures/wkt/multi_point/point_z.wkt | 1 + geo_features/test/geom/test_empty.mojo | 4 - .../test/geom/test_enums_coorddims.mojo | 82 ++++++++++ geo_features/test/geom/test_layout.mojo | 29 ++-- ...multi_point.nojo => test_multi_point.mojo} | 142 +++++++----------- geo_features/test/geom/test_point.mojo | 82 ++++++++-- 13 files changed, 413 insertions(+), 234 deletions(-) rename geo_features/geom/{multi_point.nojo => multi_point.mojo} (57%) create mode 100644 geo_features/test/fixtures/wkt/multi_point/point.wkt create mode 100644 geo_features/test/fixtures/wkt/multi_point/point_z.wkt create mode 100644 geo_features/test/geom/test_enums_coorddims.mojo rename geo_features/test/geom/{test_multi_point.nojo => test_multi_point.mojo} (51%) diff --git a/geo_features/geom/enums.mojo b/geo_features/geom/enums.mojo index ed155e8..d0d96bf 100644 --- a/geo_features/geom/enums.mojo +++ b/geo_features/geom/enums.mojo @@ -1,5 +1,6 @@ +@value @register_passable("trivial") -struct CoordDims(Stringable): +struct CoordDims(Stringable, Sized): """ Enum for encoding the OGC/WKT variants of Points. """ @@ -8,39 +9,61 @@ struct CoordDims(Stringable): var value: SIMD[DType.uint8, 1] - alias Point = CoordDims(252) + alias Point = CoordDims(100) """ 2 dimensional Point. """ - alias PointZ = CoordDims(253) + alias PointZ = CoordDims(101) """ 3 dimensional Point, has height or altitude (Z). """ - alias PointM = CoordDims(254) + alias PointM = CoordDims(102) """ 3 dimensional Point, has measure (M). """ - alias PointZM = CoordDims(255) + alias PointZM = CoordDims(103) """ 4 dimensional Point, has height and measure (ZM) """ - fn __init__(value: Int) -> Self: - return Self {value: value} + alias PointND = CoordDims(104) + """ + N-dimensional Point, number of dimensions from constructor. + """ fn __eq__(self, other: Self) -> Bool: return self.value == other.value + fn __ne__(self, other: Self) -> Bool: + return not self.__eq__(other) + fn __str__(self) -> String: """ Convert to string, using WKT point variants. """ if self == CoordDims.Point: return "Point" - if self == CoordDims.PointZ: + elif self == CoordDims.PointZ: return "Point Z" - if self == CoordDims.PointM: + elif self == CoordDims.PointM: return "Point M" - if self == CoordDims.PointZM: + elif self == CoordDims.PointZM: return "Point ZM" - return "Point" + else: + return "Point ND" + + fn __len__(self) -> Int: + if self == CoordDims.Point: + return 2 + elif self == CoordDims.PointM or self == CoordDims.PointZ: + return 3 + elif self == CoordDims.PointZM: + return 4 + else: + return self.value.to_int() + + fn has_height(self) -> Bool: + return (self == CoordDims.PointZ) or (self == CoordDims.PointZM) + + fn has_measure(self) -> Bool: + return (self == CoordDims.PointM) or (self == CoordDims.PointZM) diff --git a/geo_features/geom/envelope.mojo b/geo_features/geom/envelope.mojo index b287c1f..3c65712 100644 --- a/geo_features/geom/envelope.mojo +++ b/geo_features/geom/envelope.mojo @@ -12,14 +12,24 @@ from geo_features.geom.point import Point from geo_features.geom.enums import CoordDims from geo_features.geom.layout import Layout from geo_features.geom.traits import Geometric, Emptyable -from geo_features.geom.empty import empty_value, is_empty - +from geo_features.serialization import ( + WKTParser, + WKTable, + JSONParser, + JSONable, + Geoarrowable, +) @value @register_passable("trivial") struct Envelope[dtype: DType]( - Geometric, + CollectionElement, Emptyable, + Geometric, + # JSONable, + Sized, + Stringable, + # WKTable, ): """ Envelope aka Bounding Box. @@ -131,6 +141,11 @@ struct Envelope[dtype: DType]( res += ")" return res + fn __len__(self) -> Int: + return self.dims() + + fn __str__(self) -> String: + return self.__repr__() # # Getters # @@ -182,8 +197,8 @@ struct Envelope[dtype: DType]( let i = Self.point_simd_dims + Self.m_index return self.coords[i] - fn dims(self) -> SIMD[DType.uint8, 1]: - pass + fn dims(self) -> Int: + return len(self.ogc_dims) fn has_height(self) -> Bool: return (self.ogc_dims == CoordDims.PointZ) or ( @@ -198,6 +213,12 @@ struct Envelope[dtype: DType]( fn is_empty(self) -> Bool: return is_empty[dtype](self.coords) + fn envelope[dtype: DType = dtype](self) -> Self: + """ + Geometric trait. + """ + return self + fn wkt(self) -> String: """ TODO: wkt. diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index e32a535..287793c 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -28,7 +28,6 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( fn __init__( inout self, - dims: Int = 2, ogc_dims: CoordDims = CoordDims.Point, coords_size: Int = 0, geoms_size: Int = 0, @@ -44,11 +43,11 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( offset_dtype, coords_size, ) - self.coordinates = Tensor[dtype](dims, coords_size) + self.ogc_dims = ogc_dims + self.coordinates = Tensor[dtype](len(ogc_dims), coords_size) self.geometry_offsets = Tensor[offset_dtype](geoms_size) self.part_offsets = Tensor[offset_dtype](parts_size) self.ring_offsets = Tensor[offset_dtype](rings_size) - self.ogc_dims = ogc_dims fn __eq__(self, other: Self) -> Bool: """ @@ -75,7 +74,7 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( """ return self.coordinates.shape()[self.features_idx] - fn dims(self) -> SIMD[DType.uint8, 1]: + fn dims(self) -> Int: """ Num dimensions (X, Y, Z, M, etc). (constructor's `dims` argument). """ diff --git a/geo_features/geom/multi_point.nojo b/geo_features/geom/multi_point.mojo similarity index 57% rename from geo_features/geom/multi_point.nojo rename to geo_features/geom/multi_point.mojo index 391d88a..59b0058 100644 --- a/geo_features/geom/multi_point.nojo +++ b/geo_features/geom/multi_point.mojo @@ -4,70 +4,115 @@ from utils.vector import DynamicVector from memory import memcmp from geo_features.serialization import WKTParser, JSONParser -from .point import Point -from .layout import Layout -from .empty import is_empty, empty_value - - - -struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): +from geo_features.geom.layout import Layout +from geo_features.geom.empty import is_empty, empty_value +from geo_features.geom.traits import Geometric, Emptyable +from geo_features.geom.point import Point +from geo_features.geom.enums import CoordDims +from geo_features.serialization import ( + WKTParser, + WKTable, + JSONParser, + JSONable, + Geoarrowable, +) + + +@value +struct MultiPoint[dtype: DType = DType.float64]( + CollectionElement, + Emptyable, + Geoarrowable, + Geometric, + JSONable, + Sized, + Stringable, + WKTable, +): """ Models an OGC-style MultiPoint. Any collection of Points is a valid MultiPoint, except [heterogeneous dimension multipoints](https://geoarrow.org/format) which are unsupported. """ - var data: Layout[coord_dtype=dtype] + var data: Layout[dtype] fn __init__(inout self): """ Create empty MultiPoint. """ - self.data = Layout[coord_dtype=dtype]() + self.data = Layout[dtype]() + + fn __init(inout self, data: Layout[dtype]): + self.data = data - fn __init__(inout self, *points: Point[simd_dims, dtype]): + fn __init__(inout self, *points: Point[dtype]): """ Create MultiPoint from a variadic (var args) list of Points. """ let n = len(points) - var v = DynamicVector[Point[simd_dims, dtype]](n) - for i in range(n): - v.push_back(points[i]) - self.__init__(v) + let dims = len(points[0]) + self.data = Layout[dtype](coords_size=n) + for y in range(dims): + for x in range(n): + self.data.coordinates[Index(y, x)] = points[x].coords[y] - fn __init__(inout self, points: DynamicVector[Point[simd_dims, dtype]]): + fn __init__(inout self, points: DynamicVector[Point[dtype]]): """ Create MultiPoint from a vector of Points. """ let n = len(points) if len(points) == 0: # create empty Multipoint - self.data = Layout[coord_dtype=dtype]() + self.data = Layout[dtype]() return - let sample_pt = points[0] let dims = len(sample_pt) - self.data = Layout[dtype]( - dims=dims, coords_size=n, geoms_size=0, parts_size=0, rings_size=0 - ) + self.data = Layout[dtype](coords_size=n) for y in range(dims): - for x in range(len(points)): - self.data.coordinates[Index(y, x)] = points[x].coords[y] - - fn __copyinit__(inout self, other: Self): - self.data = other.data + for x in range(n): + let value = points[x].coords[y] + self.data.coordinates[Index(y, x)] = value @staticmethod fn from_json(json_dict: PythonObject) raises -> Self: - """ """ + # TODO: impl from_json + raise Error("not implemented") + + @staticmethod + fn from_json(json_str: String) raises -> Self: # TODO: impl from_json raise Error("not implemented") @staticmethod fn from_wkt(wkt: String) raises -> Self: - """ """ - # TODO: impl from_wkt + let geometry_sequence = WKTParser.parse(wkt) + let n = geometry_sequence.geoms.__len__().to_float64().to_int() + if n == 0: + return Self() + let sample_pt = geometry_sequence.geoms[0] + let coords_tuple = sample_pt.coords[0] + let dims = coords_tuple.__len__().to_float64().to_int() + let ogc_dims = CoordDims.PointZ if dims == 3 else CoordDims.Point + var data = Layout[dtype](ogc_dims, coords_size=n) + for y in range(dims): + for x in range(n): + let geom = geometry_sequence.geoms[x] + let coords_tuple = geom.coords[0] + let value = coords_tuple[y].to_float64().cast[dtype]() + data.coordinates[Index(y, x)] = value + return Self(data) + + @staticmethod + fn from_geoarrow(table: PythonObject) raises -> Self: + """ + Create Point from geoarrow / pyarrow table with geometry column. + """ raise Error("not implemented") + @staticmethod + fn empty(ogc_dims: CoordDims = CoordDims.Point) -> Self: + return Self() + fn __len__(self) -> Int: """ Returns the number of point elements. @@ -81,10 +126,9 @@ struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): return not self.__eq__(other) fn __repr__(self) -> String: - let dims = self.data.dims() return ( - "MultiPoint[" - + String(dims) + "MultiPoint [" + + str(self.data.ogc_dims) + ", " + str(dtype) + "](" @@ -92,14 +136,21 @@ struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): + " points)" ) - fn __getitem__(self: Self, feature_index: Int) -> Point[simd_dims, dtype]: + fn dims(self) -> Int: + return len(self.data.ogc_dims) + + fn has_height(self) -> Bool: + return self.data.has_height() + + fn has_measure(self) -> Bool: + return self.data.has_measure() + + fn __getitem__(self: Self, feature_index: Int) -> Point[dtype]: """ Get Point from MultiPoint at index. """ - let dims = self.data.dims() - var point = Point[simd_dims, dtype]() - - for dim_index in range(dims): + var point = Point[dtype]() + for dim_index in range(self.dims()): point.coords[dim_index] = self.data.coordinates[ Index(dim_index, feature_index) ] @@ -116,7 +167,7 @@ struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): return point fn __str__(self) -> String: - return self.wkt() + return self.__repr__() fn json(self) -> String: """ @@ -166,7 +217,7 @@ struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): if self.is_empty(): return "MULTIPOINT EMPTY" let dims = self.data.dims() - var res = String("MULTIPOINT(") + var res = String("MULTIPOINT (") let n = len(self) for i in range(n): let pt = self[i] @@ -181,3 +232,7 @@ struct MultiPoint[simd_dims: Int, dtype: DType](Sized, Stringable): fn is_empty(self) -> Bool: return len(self) == 0 + + fn geoarrow(self) -> PythonObject: + # TODO: geoarrow + return PythonObject() diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index 53b7043..73245db 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -3,6 +3,7 @@ from math import nan, isnan from math.limit import max_finite from geo_features.geom.empty import empty_value, is_empty +from geo_features.geom.envelope import Envelope from geo_features.serialization import ( WKTParser, WKTable, @@ -27,7 +28,7 @@ struct Point[dtype: DType = DType.float64]( WKTable, ): """ - Point is a register-passable, copy-efficient struct holding 2 or more dimension values. + Point is a register-passable (copy-efficient) struct holding 2 or more dimension values. ### Parameters @@ -35,7 +36,7 @@ struct Point[dtype: DType = DType.float64]( ### Memory Layouts - Some examples of memory layout using Mojo SIMD variables. + Some examples of memory layout using Mojo SIMD[dtype, 4] value: ```txt @@ -52,11 +53,11 @@ struct Point[dtype: DType = DType.float64]( var ogc_dims: CoordDims # - # Constructors + # Constructors (in addition to @value's member-wise init) # fn __init__(dims: CoordDims = CoordDims.Point) -> Self: """ - Create Point with empty values (NaN for float or max finite for integers). + Create Point with empty values. """ let empty = empty_value[dtype]() let coords = SIMD[dtype, Self.simd_dims](empty) @@ -167,18 +168,8 @@ struct Point[dtype: DType = DType.float64]( """ self.ogc_dims = ogc_dims - fn dims(self) -> SIMD[DType.uint8, 1]: - """ - Dimensionable trait. - """ - if self.ogc_dims == CoordDims.Point: - return 2 - elif self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZ: - return 3 - elif self.ogc_dims == CoordDims.PointZM: - return 4 - else: - debug_assert(False, "Invalid ogc_dims value") + fn dims(self) -> Int: + return len(self.ogc_dims) fn has_height(self) -> Bool: return (self.ogc_dims == CoordDims.PointZ) or ( @@ -228,14 +219,14 @@ struct Point[dtype: DType = DType.float64]( """ return self.coords[self.m_index] - # - # Dunder methods - # + fn envelope(self) -> Envelope[dtype]: + return Envelope[dtype](self) + fn __len__(self) -> Int: """ Returns the number of non-empty dimensions. """ - return self.dims().to_int() + return self.dims() fn __getitem__(self, d: Int) -> SIMD[dtype, 1]: """ @@ -270,15 +261,9 @@ struct Point[dtype: DType = DType.float64]( return res fn __str__(self) -> String: - """ - Convert to String, uses WKT for convenience. See also __repr__(). - """ - return self.wkt() + return self.__repr__() fn json(self) -> String: - """ - JSONable trait. - """ # include only x, y, and optionally z (height) var res = String('{"type":"Point","coordinates":[') let dims = 3 if self.has_height() else 2 @@ -292,9 +277,6 @@ struct Point[dtype: DType = DType.float64]( return res fn wkt(self) -> String: - """ - See WKTable trait. - """ if self.is_empty(): return "POINT EMPTY" var result = str(self.ogc_dims) + " (" @@ -307,8 +289,5 @@ struct Point[dtype: DType = DType.float64]( return result fn geoarrow(self) -> PythonObject: - """ - See Geoarrowable trait. - """ # TODO: geoarrow() return None diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo index 15c5afc..42533aa 100644 --- a/geo_features/geom/traits.mojo +++ b/geo_features/geom/traits.mojo @@ -1,8 +1,9 @@ from .enums import CoordDims +from .envelope import Envelope trait Dimensionable: - fn dims(self) -> SIMD[DType.uint8, 1]: + fn dims(self) -> Int: ... fn has_height(self) -> Bool: @@ -19,26 +20,24 @@ trait Emptyable: trait Geometric(Dimensionable): - """ - TODO: geometric trait. - - contains(Geometry): Tests if one geometry contains another. - intersects(Geometry): Tests if two geometries intersect. - overlaps(Geometry): Tests if two geometries overlap, i.e. their intersection results in a geometry of the same dimension and not just a point/line. - disjoint(Geometry): Tests if two geometries are disjoint, i.e. they do not intersect. - touches(Geometry): Tests if two geometries touch, i.e. their intersection is just one point. - intersection(Geometry): Returns a geometry representing the shared portion of the two geometries. - union(Geometry): Returns a geometry representing all points of the two geometries. - difference(Geometry): Returns a geometry representing all points of one geometry that do not intersect with another. - buffer(double): Returns an area around the geometry wider by a given distance. - convexHull(): Returns the convex hull of a geometry, i.e. the shape enclosing all outer points. - simplify(double): Returns a simplified version of a geometry using the Douglas-Peucker algorithm within a given tolerance. - centroid(): Returns the geometric center point of a geometry. - getArea(): Returns the area of a polygonal geometry. - getLength(): Returns the length of a linear geometry. - translate(double, double): Moves a geometry by given offsets. - rotate(double): Rotates a geometry around a point by a given angle. - - """ - - ... + # TODO: Geometric trait seems to require parameter support on Traits (TBD mojo version?) + + # fn envelope(self) -> Envelope[dtype]: + # fn contains(self, other: Self) -> Bool + # fn contains(self, other: Self) -> Bool + # fn intersects(self, other: Self) -> Bool + # fn overlaps(self, other: Self) -> Bool + # fn disjoint(self, other: Self) -> Bool + # fn touches(self, other: Self) -> Bool + # fn intersection(self, other: Self) -> Self + # fn union(self, other: Self) -> Self + # fn difference(self, other: Self) -> Self + # fn buffer(self, size: SIMD[dtype, 1]) -> Self + # fn convex_hull(self) -> Polygon[dtype] + # fn simplify(self) -> Self + # fn centroid(self) -> SIMD[dtype, 1] + # fn area(self) -> SIMD[dtype, 1] + # fn length(self) -> SIMD[dtype, 1] + # fn translate(self, SIMD[dtype, simd_dims]) -> Self + # fn rotate(self, degrees: SIMD[dtype, 1]) -> Self + diff --git a/geo_features/test/fixtures/wkt/multi_point/point.wkt b/geo_features/test/fixtures/wkt/multi_point/point.wkt new file mode 100644 index 0000000..b2074c1 --- /dev/null +++ b/geo_features/test/fixtures/wkt/multi_point/point.wkt @@ -0,0 +1 @@ +MULTIPOINT (-108.68 38.974, -109.68 38.974, -108.68 39.974) diff --git a/geo_features/test/fixtures/wkt/multi_point/point_z.wkt b/geo_features/test/fixtures/wkt/multi_point/point_z.wkt new file mode 100644 index 0000000..a8fb083 --- /dev/null +++ b/geo_features/test/fixtures/wkt/multi_point/point_z.wkt @@ -0,0 +1 @@ +MULTIPOINT (-108.68 38.974 42, -109.68 38.974 43, -108.68 39.974 44) diff --git a/geo_features/test/geom/test_empty.mojo b/geo_features/test/geom/test_empty.mojo index 0acc598..ef22056 100644 --- a/geo_features/test/geom/test_empty.mojo +++ b/geo_features/test/geom/test_empty.mojo @@ -1,7 +1,3 @@ -from python import Python -from python.object import PythonObject -from pathlib import Path - from geo_features.geom.empty import empty_value, is_empty from geo_features.test.pytest import MojoTest diff --git a/geo_features/test/geom/test_enums_coorddims.mojo b/geo_features/test/geom/test_enums_coorddims.mojo new file mode 100644 index 0000000..c852a89 --- /dev/null +++ b/geo_features/test/geom/test_enums_coorddims.mojo @@ -0,0 +1,82 @@ +from python import Python +from python.object import PythonObject +from pathlib import Path + +from geo_features.geom.empty import empty_value, is_empty +from geo_features.test.pytest import MojoTest +from geo_features.geom.enums import CoordDims + + +fn main() raises: + test_coord_dims() + +fn test_coord_dims() raises: + test_constructors() + test_str() + test_eq() + test_getters() + test_len() + + +fn test_constructors(): + let test = MojoTest("constructors") + _ = CoordDims(42) + + +fn test_len(): + let test = MojoTest("len") + + let n = 42 + let pt = CoordDims(n) + test.assert_true(len(pt) == n, "dims()") + + +fn test_getters(): + let test = MojoTest("getters") + let pt = CoordDims.Point + test.assert_true(not pt.has_height(), "has_height") + test.assert_true(not pt.has_measure(), "has_measure") + + let pt_z = CoordDims.PointZ + test.assert_true(pt_z.has_height(), "has_height") + test.assert_true(not pt_z.has_measure(), "has_measure") + + let pt_m = CoordDims.PointM + test.assert_true(pt_m.has_measure(), "has_height") + test.assert_true(not pt_m.has_height(), "has_measure") + + let pt_zm = CoordDims.PointZM + test.assert_true(pt_zm.has_measure(), "has_height") + test.assert_true(pt_zm.has_height(), "has_measure") + + +fn test_str() raises: + let test = MojoTest("__str__") + + let pt = CoordDims.Point + test.assert_true(str(pt) == "Point", "__str__") + + let pt_z = CoordDims.PointZ + test.assert_true(str(pt_z) == "Point Z", "__str__") + + let pt_m = CoordDims.PointM + test.assert_true(str(pt_m) == "Point M", "__str__") + + let pt_zm = CoordDims.PointZM + test.assert_true(str(pt_zm) == "Point ZM", "__str__") + + let pt_nd = CoordDims.PointND + test.assert_true(str(pt_nd) == "Point ND", "__str__") + + +fn test_eq() raises: + let test = MojoTest("__eq__, __ne__") + + let pt = CoordDims.Point + let pt_z = CoordDims.PointZ + test.assert_true(pt != pt_z, "__ne__") + + let n = 42 + let pt_nd_a = CoordDims(n) + let pt_nd_b = CoordDims(n) + test.assert_true(pt_nd_a == pt_nd_b, "__eq__") diff --git a/geo_features/test/geom/test_layout.mojo b/geo_features/test/geom/test_layout.mojo index 9c4f70c..ca1be5f 100644 --- a/geo_features/test/geom/test_layout.mojo +++ b/geo_features/test/geom/test_layout.mojo @@ -4,7 +4,7 @@ from utils.index import Index from geo_features.test.pytest import MojoTest from geo_features.geom.layout import Layout from geo_features.test.constants import lat, lon, height, measure - +from geo_features.geom.enums import CoordDims fn main() raises: test_constructors() @@ -15,28 +15,25 @@ fn main() raises: fn test_constructors() raises: let test = MojoTest("constructors") + var n = 10 # 2x10 (default of 2 dims) - let layout_a = Layout(coords_size=n, geoms_size=0, parts_size=0, rings_size=0) + let layout_a = Layout(coords_size=n) var shape = layout_a.coordinates.shape() test.assert_true(shape[0] == 2, "2x10 constructor") test.assert_true(shape[1] == n, "2x10 constructor") # 3x15 n = 15 - let layout_b = Layout( - dims=3, coords_size=n, geoms_size=0, parts_size=0, rings_size=0 - ) + let layout_b = Layout(ogc_dims=CoordDims.PointZ, coords_size=n) shape = layout_b.coordinates.shape() test.assert_true(shape[0] == 3, "3x15 constructor") test.assert_true(shape[1] == n, "3x15 constructor") # 4x20 n = 20 - let layout_c = Layout( - dims=4, coords_size=n, geoms_size=0, parts_size=0, rings_size=0 - ) + let layout_c = Layout(ogc_dims=CoordDims.PointZM, coords_size=n) shape = layout_c.coordinates.shape() test.assert_true(shape[0] == 4, "4x20 constructor") test.assert_true(shape[1] == n, "4x20 constructor") @@ -58,17 +55,15 @@ fn test_equality_ops() raises: ga2.coordinates[Index(0, n - 1)] = 3.14 test.assert_true(ga2 != ga2b, "__ne__") - fn test_len() raises: - let test = MojoTest("len") - let n = 50 - let ga1 = Layout(coords_size=n, geoms_size=0, parts_size=0, rings_size=0) - let l = ga1.__len__() - test.assert_true(l == 50, "__len__") + let test = MojoTest("__len__") + let n = 50 + let l = Layout(coords_size=n) + test.assert_true(len(l) == 50, "__len__") fn test_dims() raises: let test = MojoTest("dims") - for d in range(1, 5): - let l = Layout(dims=d, coords_size=10) - test.assert_true(l.dims() == d, "dims") + let l = Layout(coords_size=10) + let expect_dims = len(CoordDims.Point) + test.assert_true(l.dims() == expect_dims, "dims") diff --git a/geo_features/test/geom/test_multi_point.nojo b/geo_features/test/geom/test_multi_point.mojo similarity index 51% rename from geo_features/test/geom/test_multi_point.nojo rename to geo_features/test/geom/test_multi_point.mojo index 79fc9ed..3950527 100644 --- a/geo_features/test/geom/test_multi_point.nojo +++ b/geo_features/test/geom/test_multi_point.mojo @@ -2,23 +2,12 @@ from python import Python from python.object import PythonObject from utils.vector import DynamicVector from utils.index import Index +from pathlib import Path from geo_features.test.constants import lat, lon, height, measure from geo_features.test.pytest import MojoTest -from geo_features.geom import ( - Point, - Point2, - PointZ, - PointM, - PointZM, -) -from geo_features.geom import ( - MultiPoint, - MultiPoint2, - MultiPointM, - MultiPointZ, - MultiPointZM, -) +from geo_features.geom.point import Point +from geo_features.geom.multi_point import MultiPoint fn main() raises: @@ -32,28 +21,26 @@ fn test_multi_point() raises: test_equality_ops() test_is_empty() test_repr() - test_str() - test_wkt() - test_json() - test_from_json() - test_from_wkt() + test_stringable() + test_wktable() + test_jsonable() fn test_constructors() raises: var test = MojoTest("variadic list constructor") - let mpt = MultiPoint(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - test.assert_true(mpt[0] == Point2(lon, lat), "variadic list constructor") - test.assert_true(mpt[1] == Point2(lon, lat), "variadic list constructor") - test.assert_true(mpt[2] == Point2(lon, lat + 1), "variadic list constructor") + let mpt = MultiPoint(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) + test.assert_true(mpt[0] == Point(lon, lat), "variadic list constructor") + test.assert_true(mpt[1] == Point(lon, lat), "variadic list constructor") + test.assert_true(mpt[2] == Point(lon, lat + 1), "variadic list constructor") test.assert_true(mpt.__len__() == 3, "variadic list constructor") test = MojoTest("vector constructor") - var points_vec = DynamicVector[Point2](10) + var points_vec = DynamicVector[Point[]](10) for n in range(10): - points_vec.push_back(Point2(lon + n, lat - n)) - _ = MultiPoint[simd_dims = Point2.simd_dims, dtype = Point2.dtype](points_vec) + points_vec.push_back(Point(lon + n, lat - n)) + _ = MultiPoint[](points_vec) fn test_mem_layout() raises: @@ -63,12 +50,12 @@ fn test_mem_layout() raises: let test = MojoTest("mem layout") # equality check each point by indexing into the MultiPoint. - var points_vec = DynamicVector[Point2](10) + var points_vec = DynamicVector[Point[]](10) for n in range(10): - points_vec.push_back(Point2(lon + n, lat - n)) - let mpt2 = MultiPoint2(points_vec) + points_vec.push_back(Point(lon + n, lat - n)) + let mpt2 = MultiPoint(points_vec) for n in range(10): - let expect_pt = Point2(lon + n, lat - n) + let expect_pt = Point(lon + n, lat - n) test.assert_true(mpt2[n] == expect_pt, "test_mem_layout") let layout = mpt2.data @@ -83,12 +70,12 @@ fn test_mem_layout() raises: fn test_get_item() raises: let test = MojoTest("get_item") - var points_vec = DynamicVector[Point2](10) + var points_vec = DynamicVector[Point[]](10) for n in range(10): - points_vec.push_back(Point2(lon + n, lat - n)) - let mpt = MultiPoint2(points_vec) + points_vec.push_back(Point(lon + n, lat - n)) + let mpt = MultiPoint(points_vec) for n in range(10): - let expect_pt = Point2(lon + n, lat - n) + let expect_pt = Point(lon + n, lat - n) let got_pt = mpt[n] test.assert_true(got_pt == expect_pt, "get_item") @@ -98,19 +85,19 @@ fn test_equality_ops() raises: # partial simd_load (n - i < nelts) let mpt1 = MultiPoint( - Point2(1, 2), Point2(3, 4), Point2(5, 6), Point2(7, 8), Point2(9, 10) + Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8), Point(9, 10) ) let mpt2 = MultiPoint( - Point2(1.1, 2.1), - Point2(3.1, 4.1), - Point2(5.1, 6.1), - Point2(7.1, 8.1), - Point2(9.1, 10.1), + Point(1.1, 2.1), + Point(3.1, 4.1), + Point(5.1, 6.1), + Point(7.1, 8.1), + Point(9.1, 10.1), ) test.assert_true(mpt1 != mpt2, "partial simd_load (n - i < nelts)") # partial simd_load (n - i < nelts) - alias Point2F32 = Point[2, DType.float32] + alias Point2F32 = Point[DType.float32] let mpt5 = MultiPoint( Point2F32(1, 2), Point2F32(5, 6), @@ -123,7 +110,7 @@ fn test_equality_ops() raises: ) test.assert_true(mpt5 != mpt6, "partial simd_load (n - i < nelts) (b)") - alias Point2F16 = Point[2, DType.float16] + alias Point2F16 = Point[DType.float16] let mpt7 = MultiPoint( Point2F16(1, 2), Point2F16(5, 6), @@ -136,62 +123,61 @@ fn test_equality_ops() raises: ) test.assert_true(mpt7 != mpt8, "__ne__") - var points_vec2 = DynamicVector[Point2](10) + var points_vec2 = DynamicVector[Point[]](10) for n in range(10): - points_vec2.push_back(Point2(lon + n, lat - n)) - let mpt9 = MultiPoint2(points_vec2) - let mpt10 = MultiPoint2(points_vec2) + points_vec2.push_back(Point(lon + n, lat - n)) + let mpt9 = MultiPoint(points_vec2) + let mpt10 = MultiPoint(points_vec2) test.assert_true(mpt9 == mpt10, "__eq__") test.assert_true(mpt9 != mpt2, "__ne__") - let mpt11 = MultiPoint(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - let mpt12 = MultiPoint(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) + let mpt11 = MultiPoint(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) + let mpt12 = MultiPoint(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) test.assert_true(mpt11 == mpt12, "__eq__") test.assert_true(mpt9 != mpt12, "__ne__") fn test_is_empty() raises: let test = MojoTest("is_empty") - let empty_mpt = MultiPoint2() + let empty_mpt = MultiPoint() test.assert_true(empty_mpt.is_empty() == True, "is_empty()") fn test_repr() raises: let test = MojoTest("__repr__") - let mpt = MultiPoint(Point2(lon, lat), Point2(lon + 1, lat + 1)) + let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) let s = mpt.__repr__() - test.assert_true(s == "MultiPoint[2, float64](2 points)", "__repr__") + test.assert_true(s == "MultiPoint [Point, float64](2 points)", "__repr__") -fn test_str() raises: +fn test_stringable() raises: let test = MojoTest("__str__") - let mpt = MultiPoint(Point2(lon, lat), Point2(lon + 1, lat + 1)) - test.assert_true(mpt.__str__() == mpt.wkt(), "__str__") + let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) + test.assert_true(mpt.__str__() == mpt.__repr__(), "__str__") -fn test_wkt() raises: - let test = MojoTest("wkt") - let mp = MultiPoint(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - test.assert_true( - mp.wkt() - == "MULTIPOINT(-108.68000000000001 38.973999999999997, -108.68000000000001" - " 38.973999999999997, -108.68000000000001 39.973999999999997)", - "wkt", - ) +fn test_wktable() raises: + let test = MojoTest("wktable") + let path = Path("geo_features/test/fixtures/wkt/multi_point") + let fixtures = VariadicList("point.wkt", "point_z.wkt") + for i in range(len(fixtures)): + let file = path / fixtures[i] + with open(file.path, "r") as f: + let wkt = f.read() + let mp = MultiPoint.from_wkt(wkt) + test.assert_true(mp.wkt() != "FIXME", "wkt") # FIXME: no number formatting so cannot compare wkt strings. -fn test_json() raises: - let test = MojoTest("json") - let mpt = MultiPoint(Point2(lon, lat), Point2(lon + 1, lat + 1)) +fn test_jsonable() raises: + var test = MojoTest("json") + let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) test.assert_true( mpt.json() == '{"type":"MultiPoint","coordinates":[[-108.68000000000001,38.973999999999997],[-107.68000000000001,39.973999999999997]]}', "json", ) - - -fn test_from_json() raises: - let test = MojoTest("from_json (⚠️ not implemented)") + + test = MojoTest("from_json") let json_str = String( '{"type":"MultiPoint","coordinates":[[42.0,38.9739990234375],[42.0,38.9739990234375]]}' ) @@ -199,18 +185,6 @@ fn test_from_json() raises: let json_dict = json.loads(json_str) try: - _ = MultiPoint2.from_json(json_dict) - raise Error("unreachable") + _ = MultiPoint.from_json(json_dict) except e: - test.assert_true( - e.__str__() == "not implemented", "unexpected error value" - ) # TODO - - -fn test_from_wkt() raises: - let test = MojoTest("from_wkt (⚠️ not implemented)") - # try: - # _ = MultiPoint.from_wkt("") - # raise Error("unreachable") - # except e: - # test.assert_true(e.__str__() == "not implemented", "unexpected error value") # TODO + test.assert_true(False, "TODO: from_json") diff --git a/geo_features/test/geom/test_point.mojo b/geo_features/test/geom/test_point.mojo index 15e19d7..b1f7394 100644 --- a/geo_features/test/geom/test_point.mojo +++ b/geo_features/test/geom/test_point.mojo @@ -4,6 +4,7 @@ from pathlib import Path from geo_features.geom.empty import empty_value, is_empty from geo_features.geom.point import Point, CoordDims +from geo_features.geom.traits import Dimensionable, Geometric, Emptyable from geo_features.test.helpers import load_geoarrow_test_fixture from geo_features.test.pytest import MojoTest from geo_features.test.constants import lon, lat, height, measure @@ -12,17 +13,18 @@ from geo_features.test.constants import lon, lat, height, measure fn main() raises: test_constructors() test_repr() - test_has_height() - test_has_measure() test_equality_ops() - test_is_empty() test_getters() test_setters() - test_wkt() - test_json() - test_from_json() - test_from_wkt() - test_from_geoarrow() + test_sized() + test_stringable() + test_dimensionable() + test_geometric() + test_emptyable() + test_stringable() + test_wktable() + test_jsonable() + test_geoarrowable() fn test_constructors(): @@ -75,22 +77,56 @@ fn test_repr() raises: ) -fn test_has_height() raises: - let test = MojoTest("has_height") +fn test_stringable() raises: + let test = MojoTest("stringable") let pt_z = Point(lon, lat, height) - test.assert_true(pt_z.has_height(), "has_height") + test.assert_true(str(pt_z) == pt_z.__repr__(), "__str__") + + +fn test_sized() raises: + let test = MojoTest("sized") + let pt_z = Point(lon, lat, height) + test.assert_true(len(pt_z) == 3, "__len__") -fn test_has_measure() raises: - let test = MojoTest("has_measure") +fn test_dimensionable() raises: + let pt = Point(lon, lat) + let pt_z = Point(lon, lat, height) var pt_m = Point(lon, lat, measure) + let pt_zm = Point(lon, lat, height, measure) + + var test = MojoTest("dims") + test.assert_true(pt.dims() == 2, "dims") + test.assert_true(pt_z.dims() == 3, "dims") + test.assert_true(pt_m.dims() == 3, "dims") + test.assert_true(pt_zm.dims() == 4, "dims") + + test = MojoTest("has_height") + test.assert_true(pt_z.has_height(), "has_height") + + test = MojoTest("has_measure") test.assert_true(not pt_m.has_measure(), "has_measure") pt_m.set_ogc_dims(CoordDims.PointM) test.assert_true(pt_m.has_measure(), "has_measure") +fn test_geometric() raises: + let test = MojoTest("geometric") + test.assert_true(False, "TODO: geometric") + + +fn test_emptyable() raises: + let test = MojoTest("emptyable") + let pt_e = Point.empty() + test.assert_true(is_empty(pt_e.x()), "empty") + test.assert_true(is_empty(pt_e.y()), "empty") + test.assert_true(is_empty(pt_e.z()), "empty") + test.assert_true(is_empty(pt_e.m()), "empty") + test.assert_true(is_empty(pt_e.coords), "empty") + + fn test_empty_default_values() raises: - let test = MojoTest("test empty default values") + let test = MojoTest("empty default/padding values") let pt_4 = Point(lon, lat) let expect_value = empty_value[pt_4.dtype]() @@ -175,6 +211,11 @@ fn test_setters() raises: test.assert_true(pt.ogc_dims == CoordDims.PointM, "set_ogc_dims") +fn test_jsonable() raises: + test_json() + test_from_json() + + fn test_json() raises: let test = MojoTest("json") @@ -223,6 +264,11 @@ fn test_from_json() raises: test.assert_true(pt_int.y() == 3, "pt_int.y()") +fn test_wktable() raises: + test_wkt() + test_from_wkt() + + fn test_wkt() raises: let test = MojoTest("wkt") @@ -291,6 +337,14 @@ fn test_from_wkt() raises: " packages." ) +fn test_geoarrowable() raises: + test_geoarrow() + test_from_geoarrow() + +fn test_geoarrow() raises: + let test = MojoTest("geoarrow") + test.assert_true(False, "TODO: geoarrow") + fn test_from_geoarrow() raises: let test = MojoTest("from_geoarrow") From 4596682d23d5aa51633505d20b616e8251e7a6d6 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Fri, 15 Dec 2023 17:17:43 -0700 Subject: [PATCH 09/11] Port to mojo v0.6, begin implementing traits. --- geo_features/geom/envelope.mojo | 17 +- ...llection.nojo => geometry_collection.mojo} | 0 geo_features/geom/layout.mojo | 10 + .../{line_string.nojo => line_string.mojo} | 221 ++++++++------ ...ine_string.nojo => multi_line_string.mojo} | 0 geo_features/geom/multi_point.mojo | 132 ++++---- ...{multi_polygon.nojo => multi_polygon.mojo} | 0 geo_features/geom/point.mojo | 36 ++- geo_features/geom/polygon.mojo | 2 + geo_features/geom/polygon.nojo | 102 ------- geo_features/geom/traits.mojo | 12 +- geo_features/serialization/geoarrow.mojo | 18 -- geo_features/serialization/json.mojo | 23 -- geo_features/serialization/traits.mojo | 72 +++++ geo_features/serialization/wkt.mojo | 21 +- .../geojson/multi_point/multi_point.geojson | 1 + .../geojson/multi_point/multi_point_z.geojson | 1 + .../test/geom/test_enums_coorddims.mojo | 1 + geo_features/test/geom/test_envelope.mojo | 2 +- geo_features/test/geom/test_layout.mojo | 3 + geo_features/test/geom/test_line_string.mojo | 249 +++++++++++++++ geo_features/test/geom/test_line_string.nojo | 286 ------------------ geo_features/test/geom/test_multi_point.mojo | 73 +++-- geo_features/test/geom/test_point.mojo | 27 +- 24 files changed, 657 insertions(+), 652 deletions(-) rename geo_features/geom/{geometry_collection.nojo => geometry_collection.mojo} (100%) rename geo_features/geom/{line_string.nojo => line_string.mojo} (54%) rename geo_features/geom/{multi_line_string.nojo => multi_line_string.mojo} (100%) rename geo_features/geom/{multi_polygon.nojo => multi_polygon.mojo} (100%) create mode 100644 geo_features/geom/polygon.mojo delete mode 100644 geo_features/geom/polygon.nojo create mode 100644 geo_features/serialization/traits.mojo create mode 100644 geo_features/test/fixtures/geojson/multi_point/multi_point.geojson create mode 100644 geo_features/test/fixtures/geojson/multi_point/multi_point_z.geojson create mode 100644 geo_features/test/geom/test_line_string.mojo delete mode 100644 geo_features/test/geom/test_line_string.nojo diff --git a/geo_features/geom/envelope.mojo b/geo_features/geom/envelope.mojo index 3c65712..ccc98a9 100644 --- a/geo_features/geom/envelope.mojo +++ b/geo_features/geom/envelope.mojo @@ -12,14 +12,13 @@ from geo_features.geom.point import Point from geo_features.geom.enums import CoordDims from geo_features.geom.layout import Layout from geo_features.geom.traits import Geometric, Emptyable +from geo_features.serialization.traits import JSONable, WKTable, Geoarrowable from geo_features.serialization import ( WKTParser, - WKTable, JSONParser, - JSONable, - Geoarrowable, ) + @value @register_passable("trivial") struct Envelope[dtype: DType]( @@ -146,6 +145,7 @@ struct Envelope[dtype: DType]( fn __str__(self) -> String: return self.__repr__() + # # Getters # @@ -200,6 +200,17 @@ struct Envelope[dtype: DType]( fn dims(self) -> Int: return len(self.ogc_dims) + fn set_ogc_dims(inout self, ogc_dims: CoordDims): + """ + Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. + (ex: when Point Z vs Point M is ambiguous. + """ + debug_assert( + len(self.ogc_dims) == 3 and len(ogc_dims) == 3, + "Unsafe change of dimension number", + ) + self.ogc_dims = ogc_dims + fn has_height(self) -> Bool: return (self.ogc_dims == CoordDims.PointZ) or ( self.ogc_dims == CoordDims.PointZM diff --git a/geo_features/geom/geometry_collection.nojo b/geo_features/geom/geometry_collection.mojo similarity index 100% rename from geo_features/geom/geometry_collection.nojo rename to geo_features/geom/geometry_collection.mojo diff --git a/geo_features/geom/layout.mojo b/geo_features/geom/layout.mojo index 287793c..ce86256 100644 --- a/geo_features/geom/layout.mojo +++ b/geo_features/geom/layout.mojo @@ -85,3 +85,13 @@ struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( fn has_measure(self) -> Bool: return self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZM + + fn set_ogc_dims(inout self, ogc_dims: CoordDims): + """ + Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. + (ex: when Point Z vs Point M is ambiguous. + """ + debug_assert( + len(self.ogc_dims) == len(ogc_dims), "Unsafe change of dimension number" + ) + self.ogc_dims = ogc_dims diff --git a/geo_features/geom/line_string.nojo b/geo_features/geom/line_string.mojo similarity index 54% rename from geo_features/geom/line_string.nojo rename to geo_features/geom/line_string.mojo index 8fd3fc9..77a6d62 100644 --- a/geo_features/geom/line_string.nojo +++ b/geo_features/geom/line_string.mojo @@ -5,35 +5,29 @@ from memory import memcmp from python import Python from geo_features.serialization import WKTParser, JSONParser -from .point import Point -from .layout import Layout - - -alias LineString2 = LineString[simd_dims=2, dtype = DType.float64] -""" -Alias for 2D LineString with dtype float64. -""" - -alias LineStringZ = LineString[simd_dims=4, dtype = DType.float64] -""" -Alias for 3D LineString with dtype float64, including Z (height) dimension. -Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). -""" - -alias LineStringM = LineString[simd_dims=4, dtype = DType.float64] -""" -Alias for 3D LineString with dtype float64, including M (measure) dimension. -Note: dims is 4 because of SIMD memory model length 4 (power of two constraint). -""" - -alias LineStringZM = LineString[simd_dims=4, dtype = DType.float64] -""" -Alias for 4D LineString with dtype float64, including Z (height) and M (measure) dimension. -""" +from geo_features.geom.point import Point +from geo_features.geom.layout import Layout +from geo_features.geom.enums import CoordDims +from geo_features.geom.empty import is_empty, empty_value +from geo_features.geom.traits import Geometric, Emptyable +from geo_features.serialization.traits import WKTable, JSONable, Geoarrowable +from geo_features.serialization import ( + WKTParser, + JSONParser, +) @value -struct LineString[simd_dims: Int, dtype: DType]: +struct LineString[dtype: DType = DType.float64]( + CollectionElement, + Emptyable, + # Geoarrowable, + Geometric, + JSONable, + Sized, + Stringable, + # WKTable, +): """ Models an OGC-style LineString. @@ -48,54 +42,71 @@ struct LineString[simd_dims: Int, dtype: DType]: - If these conditions are not met, the constructors raise an Error. """ - var data: Layout[coord_dtype=dtype] + var data: Layout[dtype] fn __init__(inout self): """ - Create empty linestring. + Construct empty LineString. """ - self.data = Layout[coord_dtype=dtype]() + self.data = Layout[dtype]() + + fn __init(inout self, data: Layout[dtype]): + self.data = data - fn __init__(inout self, *points: Point[simd_dims, dtype, _]): + fn __init__(inout self, *points: Point[dtype]): """ - Create LineString from a variadic (var args) list of Points. + Construct `LineString` from variadic list of `Point`. """ + debug_assert(len(points) > 0, "unreachable") let n = len(points) - var v = DynamicVector[Point](n) - for i in range(n): - v.push_back(points[i]) - self.__init__(v) - fn __init__(inout self, points: DynamicVector[Point[simd_dims, dtype]]): + if n == 0: + # empty linestring + self.data = Layout[dtype]() + return + + let sample_pt = points[0] + let dims = len(sample_pt) + self.data = Layout[dtype](coords_size=n) + + for y in range(dims): + for x in range(len(points)): + self.data.coordinates[Index(y, x)] = points[x].coords[y] + + fn __init__(inout self, points: DynamicVector[Point[dtype]]): """ - Create LineString from a vector of Points. + Construct `LineString` from a vector of `Points`. """ # here the geometry_offsets, part_offsets, and ring_offsets are unused because # of using "struct coordinate representation" (tensor) + let n = len(points) if n == 0: # empty linestring - self.data = Layout[coord_dtype=dtype]() + self.data = Layout[dtype]() return let sample_pt = points[0] let dims = len(sample_pt) - self.data = Layout[coord_dtype=dtype]( - dims=dims, coords_size=n, geoms_size=0, parts_size=0, rings_size=0 - ) + self.data = Layout[dtype](coords_size=n) + for y in range(dims): for x in range(len(points)): self.data.coordinates[Index(y, x)] = points[x].coords[y] - fn __copyinit__(inout self, other: Self): - self.data = other.data + @staticmethod + fn empty(dims: CoordDims = CoordDims.Point) -> Self: + return Self() fn __len__(self) -> Int: + """ + Return the number of Point elements. + """ return self.data.coordinates.shape()[1] fn dims(self) -> Int: - return self.data.coordinates.shape()[0] + return len(self.data.ogc_dims) fn __eq__(self, other: Self) -> Bool: return self.data == other.data @@ -105,24 +116,41 @@ struct LineString[simd_dims: Int, dtype: DType]: fn __repr__(self) -> String: return ( - "LineString[" - + String(self.dims()) + "LineString [" + + str(self.data.ogc_dims) + ", " + dtype.__str__() + "](" - + String(self.__len__()) + + String(len(self)) + " points)" ) - fn __getitem__(self: Self, feature_index: Int) -> Point[simd_dims, dtype]: + fn __getitem__(self: Self, feature_index: Int) -> Point[dtype]: """ Get Point from LineString at index. """ - var result = Point[simd_dims, dtype]() + var result = Point[dtype]() for i in range(self.dims()): result.coords[i] = self.data.coordinates[Index(i, feature_index)] return result + fn has_height(self) -> Bool: + return self.data.has_height() + + fn has_measure(self) -> Bool: + return self.data.has_measure() + + fn set_ogc_dims(inout self, ogc_dims: CoordDims): + """ + Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. + (ex: when Point Z vs Point M is ambiguous. + """ + debug_assert( + len(self.data.ogc_dims) == len(ogc_dims), + "Unsafe change of dimension number", + ) + self.data.set_ogc_dims(ogc_dims) + fn is_valid(self, inout err: String) -> Bool: """ Validate geometry. When False, sets the `err` string with a condition. @@ -134,11 +162,11 @@ struct LineString[simd_dims: Int, dtype: DType]: if self.is_empty(): return True - let self_len = self.__len__() - if self_len == 2 and self[0] == self[1]: + let n = len(self) + if n == 2 and self[0] == self[1]: err = "LineStrings with exactly two identical points are invalid." return False - if self_len == 1: + if n == 1: err = "LineStrings must have either 0 or 2 or more points." return False if self.is_closed(): @@ -148,35 +176,41 @@ struct LineString[simd_dims: Int, dtype: DType]: @staticmethod fn from_json(json_dict: PythonObject) raises -> Self: + """ + Construct `MultiPoint` from GeoJSON Python dictionary. + """ var json_coords = json_dict.get("coordinates", Python.none()) if not json_coords: raise Error("LineString.from_json(): coordinates property missing in dict.") - var points = DynamicVector[Point[simd_dims, dtype]]() + var points = DynamicVector[Point[dtype]]() for coords in json_coords: let lon = coords[0].to_float64().cast[dtype]() let lat = coords[1].to_float64().cast[dtype]() - let pt = Point[simd_dims, dtype](lon, lat) + let pt = Point[dtype](lon, lat) points.push_back(pt) - return LineString[simd_dims, dtype](points) + return LineString[dtype](points) @staticmethod - fn from_wkt(wkt: String) raises -> Self: - # TODO: impl from_wkt() - # raise Error("not implemented") - return LineString[simd_dims, dtype]() + fn from_json(json_str: String) raises -> Self: + """ + Construct `LineString` from GeoJSON serialized string. + """ + let json_dict = JSONParser.parse(json_str) + return Self.from_json(json_dict) fn __str__(self) -> String: - return self.wkt() + return self.__repr__() - fn json(self) -> String: + fn json(self) raises -> String: """ - GeoJSON representation of LineString. Coordinates of LineString are an array of positions. + Serialize `LineString` to GeoJSON. Coordinates of LineString are an array of positions. - ### Spec + ### Spec - - https://geojson.org - - https://datatracker.ietf.org/doc/html/rfc7946 + - https://geojson.org + - https://datatracker.ietf.org/doc/html/rfc7946 + ```json { "type": "LineString", "coordinates": [ @@ -184,45 +218,44 @@ struct LineString[simd_dims: Int, dtype: DType]: [101.0, 1.0] ] } + ``` """ - var res = String('{"type":"LineString","coordinates":[') - let len = self.__len__() + if self.data.ogc_dims.value > CoordDims.PointZ.value: + raise Error( + "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" + ) + let dims = self.dims() - for feature_index in range(len): - let pt = self[feature_index] + let n = len(self) + var res = String('{"type":"LineString","coordinates":[') + for i in range(n): + let pt = self[i] res += "[" - for dim_index in range(3): - if dim_index > dims - 1: + for dim in range(3): + if dim > dims - 1: break - res += pt[dim_index] - if dim_index < 2 and dim_index < dims - 1: + res += pt[dim] + if dim < dims - 1: res += "," res += "]" - if feature_index < len - 1: + if i < n - 1: res += "," res += "]}" return res fn wkt(self) -> String: - """ - Well Known Text (WKT) representation of LineString. - - ### Spec - - https://libgeos.org/specifications/wkt - """ if self.is_empty(): return "LINESTRING EMPTY" let dims = self.dims() var res = String("LINESTRING(") - let len = self.__len__() - for i in range(len): + let n = len(self) + for i in range(n): let pt = self[i] for j in range(dims): res += pt.coords[j] if j < dims - 1: res += " " - if i < len - 1: + if i < n - 1: res += ", " res += ")" return res @@ -231,24 +264,12 @@ struct LineString[simd_dims: Int, dtype: DType]: """ If LineString is closed (0 and n-1 points are equal), it's not valid: a LinearRing should be used instead. """ - let len = self.__len__() - if len == 1: + let n = len(self) + if n == 1: return False let start_pt = self[0] - let end_pt = self[len - 1] + let end_pt = self[n - 1] return start_pt == end_pt - fn is_ring(self) -> Bool: - # TODO: implement is_simple() after traits land: will be easier to implement operators then (see JTS) - # return self.is_closed() and self.is_simple() - return self.is_closed() - - fn is_simple(self) raises -> Bool: - """ - A geometry is simple if it has no points of self-tangency, self-intersection or other anomalous points. - """ - # TODO impl is_simple() - raise Error("not implemented") - fn is_empty(self) -> Bool: - return self.__len__() == 0 + return len(self) == 0 diff --git a/geo_features/geom/multi_line_string.nojo b/geo_features/geom/multi_line_string.mojo similarity index 100% rename from geo_features/geom/multi_line_string.nojo rename to geo_features/geom/multi_line_string.mojo diff --git a/geo_features/geom/multi_point.mojo b/geo_features/geom/multi_point.mojo index 59b0058..84abc26 100644 --- a/geo_features/geom/multi_point.mojo +++ b/geo_features/geom/multi_point.mojo @@ -2,6 +2,7 @@ from tensor import Tensor, TensorSpec, TensorShape from utils.index import Index from utils.vector import DynamicVector from memory import memcmp +from python import Python from geo_features.serialization import WKTParser, JSONParser from geo_features.geom.layout import Layout @@ -9,12 +10,10 @@ from geo_features.geom.empty import is_empty, empty_value from geo_features.geom.traits import Geometric, Emptyable from geo_features.geom.point import Point from geo_features.geom.enums import CoordDims +from geo_features.serialization.traits import WKTable, JSONable, Geoarrowable from geo_features.serialization import ( WKTParser, - WKTable, JSONParser, - JSONable, - Geoarrowable, ) @@ -38,7 +37,7 @@ struct MultiPoint[dtype: DType = DType.float64]( fn __init__(inout self): """ - Create empty MultiPoint. + Construct empty MultiPoint. """ self.data = Layout[dtype]() @@ -47,45 +46,67 @@ struct MultiPoint[dtype: DType = DType.float64]( fn __init__(inout self, *points: Point[dtype]): """ - Create MultiPoint from a variadic (var args) list of Points. + Construct `MultiPoint` from a variadic list of `Points`. """ + debug_assert(len(points) > 0, "unreachable") let n = len(points) - let dims = len(points[0]) - self.data = Layout[dtype](coords_size=n) + # sample 1st point as prototype to get dims + let sample_pt = points[0] + let dims = len(sample_pt) + self.data = Layout[dtype](ogc_dims=sample_pt.ogc_dims, coords_size=n) for y in range(dims): for x in range(n): self.data.coordinates[Index(y, x)] = points[x].coords[y] fn __init__(inout self, points: DynamicVector[Point[dtype]]): """ - Create MultiPoint from a vector of Points. + Construct `MultiPoint` from a vector of `Point`. """ let n = len(points) if len(points) == 0: - # create empty Multipoint + # early out with empty MultiPoint self.data = Layout[dtype]() return + # sample 1st point as prototype to get dims let sample_pt = points[0] let dims = len(sample_pt) - self.data = Layout[dtype](coords_size=n) - for y in range(dims): - for x in range(n): - let value = points[x].coords[y] - self.data.coordinates[Index(y, x)] = value + self.data = Layout[dtype](ogc_dims=sample_pt.ogc_dims, coords_size=n) + for dim in range(dims): + for i in range(n): + let value = points[i].coords[dim] + self.data.coordinates[Index(dim, i)] = value @staticmethod fn from_json(json_dict: PythonObject) raises -> Self: - # TODO: impl from_json - raise Error("not implemented") + """ + Construct `MultiPoint` from GeoJSON (Python dictionary). + """ + let json_coords = json_dict["coordinates"] + let n = int(json_coords.__len__()) + # TODO: type checking of json_dict (coordinates property exists) + let dims = json_coords[0].__len__().to_float64().to_int() + let ogc_dims = CoordDims.PointZ if dims == 3 else CoordDims.Point + var data = Layout[dtype](ogc_dims, coords_size=n) + for dim in range(dims): + for i in range(n): + let point = json_coords[i] + # TODO: bounds check of geojson point + let value = point[dim].to_float64().cast[dtype]() + data.coordinates[Index(dim, i)] = value + return Self(data) @staticmethod fn from_json(json_str: String) raises -> Self: - # TODO: impl from_json - raise Error("not implemented") + """ + Construct `MultiPoint` from GeoJSON serialized string. + """ + let json_dict = JSONParser.parse(json_str) + return Self.from_json(json_dict) @staticmethod fn from_wkt(wkt: String) raises -> Self: let geometry_sequence = WKTParser.parse(wkt) + # TODO: validate PythonObject is a class MultiPoint https://shapely.readthedocs.io/en/stable/reference/shapely.MultiPoint.html let n = geometry_sequence.geoms.__len__().to_float64().to_int() if n == 0: return Self() @@ -104,10 +125,16 @@ struct MultiPoint[dtype: DType = DType.float64]( @staticmethod fn from_geoarrow(table: PythonObject) raises -> Self: - """ - Create Point from geoarrow / pyarrow table with geometry column. - """ - raise Error("not implemented") + let ga = Python.import_module("geoarrow.pyarrow") + let geoarrow = ga.as_geoarrow(table["geometry"]) + let chunk = geoarrow[0] + let n = chunk.value.__len__() + # TODO: inspect first point to see number of dims (same as in from_wkt above) + if n > 2: + raise Error("Invalid Point dims parameter vs. geoarrow: " + str(n)) + # TODO: add to Layout + # return result + return Self() @staticmethod fn empty(ogc_dims: CoordDims = CoordDims.Point) -> Self: @@ -115,7 +142,7 @@ struct MultiPoint[dtype: DType = DType.float64]( fn __len__(self) -> Int: """ - Returns the number of point elements. + Returns the number of Point elements. """ return self.data.coordinates.shape()[1] @@ -145,33 +172,34 @@ struct MultiPoint[dtype: DType = DType.float64]( fn has_measure(self) -> Bool: return self.data.has_measure() + fn set_ogc_dims(inout self, ogc_dims: CoordDims): + """ + Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. + (ex: when Point Z vs Point M is ambiguous. + """ + debug_assert( + len(self.data.ogc_dims) == len(ogc_dims), + "Unsafe change of dimension number", + ) + self.data.set_ogc_dims(ogc_dims) + fn __getitem__(self: Self, feature_index: Int) -> Point[dtype]: """ Get Point from MultiPoint at index. """ - var point = Point[dtype]() + var point = Point[dtype](self.data.ogc_dims) for dim_index in range(self.dims()): point.coords[dim_index] = self.data.coordinates[ Index(dim_index, feature_index) ] - - # TODO: handle 3 dim cases - - # @parameter - # if point_simd_dims >= 4: - # if dims == 3: - # # Handle case where because of memory model, cannot distinguish a PointZ from a PointM. - # # Just copy the value between dim 3 and 4. - # point.coords[3] = point[2] - return point fn __str__(self) -> String: return self.__repr__() - fn json(self) -> String: + fn json(self) raises -> String: """ - GeoJSON representation of MultiPoint. Coordinates of MultiPoint are an array of positions. + Serialize `MultiPoint` to GeoJSON. Coordinates of MultiPoint are an array of positions. ### Spec @@ -188,32 +216,28 @@ struct MultiPoint[dtype: DType = DType.float64]( } ``` """ + if self.data.ogc_dims.value > CoordDims.PointZ.value: + raise Error( + "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" + ) + let n = len(self) let dims = self.data.dims() var res = String('{"type":"MultiPoint","coordinates":[') - for feature_index in range(n): - let pt = self[feature_index] + for i in range(n): + let pt = self[i] res += "[" - for dim_index in range(3): - if dim_index > dims - 1: - break - res += pt[dim_index] - if dim_index < 2 and dim_index < dims - 1: + for dim in range(dims): + res += pt[dim] + if dim < dims - 1: res += "," res += "]" - if feature_index < n - 1: + if i < n - 1: res += "," res += "]}" return res fn wkt(self) -> String: - """ - Well Known Text (WKT) representation of MultiPoint. - - ### Spec - - https://libgeos.org/specifications/wkt - """ if self.is_empty(): return "MULTIPOINT EMPTY" let dims = self.data.dims() @@ -221,9 +245,9 @@ struct MultiPoint[dtype: DType = DType.float64]( let n = len(self) for i in range(n): let pt = self[i] - for j in range(dims): - res += pt.coords[j] - if j < dims - 1: + for dim in range(dims): + res += pt[dims] + if dim < dims - 1: res += " " if i < n - 1: res += ", " diff --git a/geo_features/geom/multi_polygon.nojo b/geo_features/geom/multi_polygon.mojo similarity index 100% rename from geo_features/geom/multi_polygon.nojo rename to geo_features/geom/multi_polygon.mojo diff --git a/geo_features/geom/point.mojo b/geo_features/geom/point.mojo index 73245db..3fc4776 100644 --- a/geo_features/geom/point.mojo +++ b/geo_features/geom/point.mojo @@ -4,16 +4,18 @@ from math.limit import max_finite from geo_features.geom.empty import empty_value, is_empty from geo_features.geom.envelope import Envelope +from geo_features.serialization.traits import WKTable, JSONable, Geoarrowable from geo_features.serialization import ( WKTParser, - WKTable, JSONParser, - JSONable, - Geoarrowable, ) from .traits import Geometric, Emptyable from .enums import CoordDims +alias Point64 = Point[DType.float64] +alias Point32 = Point[DType.float32] +alias Point16 = Point[DType.float16] + @value @register_passable("trivial") @@ -75,7 +77,6 @@ struct Point[dtype: DType = DType.float64]( var coords = SIMD[dtype, Self.simd_dims](empty) var ogc_dims = CoordDims.Point let n = len(coords_list) - for i in range(Self.simd_dims): if i < n: coords[i] = coords_list[i] @@ -102,9 +103,7 @@ struct Point[dtype: DType = DType.float64]( # @staticmethod fn from_json(json_dict: PythonObject) raises -> Self: - """ - JSONable trait. - """ + # TODO: type checking of json_dict # TODO: bounds checking of coords_len let json_coords = json_dict["coordinates"] let coords_len = int(json_coords.__len__()) @@ -115,17 +114,11 @@ struct Point[dtype: DType = DType.float64]( @staticmethod fn from_json(json_str: String) raises -> Self: - """ - JSONable trait. - """ let json_dict = JSONParser.parse(json_str) return Self.from_json(json_dict) @staticmethod fn from_wkt(wkt: String) raises -> Self: - """ - WKTable trait. - """ var result = Self() let geos_pt = WKTParser.parse(wkt) let coords_tuple = geos_pt.coords[0] @@ -136,9 +129,6 @@ struct Point[dtype: DType = DType.float64]( @staticmethod fn from_geoarrow(table: PythonObject) raises -> Self: - """ - Geoarrowable trait. - """ let ga = Python.import_module("geoarrow.pyarrow") let geoarrow = ga.as_geoarrow(table["geometry"]) let chunk = geoarrow[0] @@ -163,9 +153,12 @@ struct Point[dtype: DType = DType.float64]( # fn set_ogc_dims(inout self, ogc_dims: CoordDims): """ - Setter for ogc_dims enum. May be useful if the Point constructor with variadic list of coordinate values. - (Point Z vs Point M is ambiguous). + Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. + (ex: when Point Z vs Point M is ambiguous. """ + debug_assert( + len(self.ogc_dims) == len(ogc_dims), "Unsafe change of dimension number" + ) self.ogc_dims = ogc_dims fn dims(self) -> Int: @@ -263,7 +256,12 @@ struct Point[dtype: DType = DType.float64]( fn __str__(self) -> String: return self.__repr__() - fn json(self) -> String: + fn json(self) raises -> String: + if self.ogc_dims.value > CoordDims.PointZ.value: + raise Error( + "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" + ) + # include only x, y, and optionally z (height) var res = String('{"type":"Point","coordinates":[') let dims = 3 if self.has_height() else 2 diff --git a/geo_features/geom/polygon.mojo b/geo_features/geom/polygon.mojo new file mode 100644 index 0000000..6484fcc --- /dev/null +++ b/geo_features/geom/polygon.mojo @@ -0,0 +1,2 @@ +struct Polygon: + pass diff --git a/geo_features/geom/polygon.nojo b/geo_features/geom/polygon.nojo deleted file mode 100644 index 203b27c..0000000 --- a/geo_features/geom/polygon.nojo +++ /dev/null @@ -1,102 +0,0 @@ -# from geo_features.serialization import WKTParser, JSONParser -# from .coordinate_sequence import CoordinateSequence - -# alias Polygon2 = Polygon[DType.float32, 2] -# """ -# Alias for 2D polygon with dtype: float32. -# """ - -# alias Polygon3 = Polygon[DType.float32, 4] -# """ -# Alias for 3D polygon with dtype float32. Note: is backed by SIMD vector of size 4 (must be power of two). -# """ - -# alias Polygon4 = Polygon[DType.float32, 4] -# """ -# Alias for 4D polygon with dtype float32. -# """ - -# struct Polygon[dtype: DType, dims: Int]: -# """ -# Polygon is a plane figure made up of line segments connected to form a closed polygonal chain. -# """ -# var coords: CoordinateSequence[DType.float32, DimList(8, 2)] -# var adjacency_matrix: CoordinateSequence[DType.bool, DimList(8, 2)] - -# fn __init__(*elems: SIMD[dtype, 1]) -> Self: -# """ -# """ -# pass - -# fn __init__(owned coords: SIMD[dtype, dims]) -> Self: -# """ -# """ -# pass - -# @staticmethod -# fn from_json(json_dict: PythonObject) raises -> Polygon[dtype, dims]: -# """ -# """ -# pass - -# @staticmethod -# fn from_json(json_str: String) raises -> Polygon[dtype, dims]: -# """ -# """ -# pass - -# @staticmethod -# def from_wkt(wkt: String) -> Polygon[dtype, dims]: -# """ -# """ -# pass - -# @staticmethod -# fn zero() -> Polygon[dtype, dims]: -# """ -# Null Island is an imaginary place located at zero degrees latitude and zero degrees longitude (0°N 0°E) -# https://en.wikipedia.org/wiki/Null_Island . -# """ -# pass - - -# fn __eq__(self, other: Self) -> Bool: -# # return Bool(self.coords == other.coords) -# return False - -# fn __ne__(self, other: Self) -> Bool: -# return not self.__eq__(other) - -# fn __repr__(self) -> String: -# var res = "Polygon[" + dtype.__str__() + ", " + String(dims) + "](TODO)" -# return res - -# fn __str__(self) -> String: -# return self.wkt() - -# fn json(self) -> String: -# """ -# GeoJSON representation of Polygon. - -# Example: - -# ```json -# ``` - -# ### Spec - -# - https://geojson.org -# - https://datatracker.ietf.org/doc/html/rfc7946 -# """ -# # include only x, y, and optionally z (altitude) -# pass - -# fn wkt(self) -> String: -# """ -# Well Known Text (WKT) representation of Polygon. - -# ### Spec - -# - https://libgeos.org/specifications/wkt -# """ -# pass diff --git a/geo_features/geom/traits.mojo b/geo_features/geom/traits.mojo index 42533aa..600b3ac 100644 --- a/geo_features/geom/traits.mojo +++ b/geo_features/geom/traits.mojo @@ -12,14 +12,25 @@ trait Dimensionable: fn has_measure(self) -> Bool: ... + fn set_ogc_dims(inout self, ogc_dims: CoordDims): + """ + Setter for ogc_dims enum. May be useful if the Point constructor with variadic list of coordinate values. + (Point Z vs Point M is ambiguous). + """ + ... + trait Emptyable: @staticmethod fn empty(dims: CoordDims = CoordDims.Point) -> Self: ... + fn is_empty(self) -> Bool: + ... + trait Geometric(Dimensionable): + ... # TODO: Geometric trait seems to require parameter support on Traits (TBD mojo version?) # fn envelope(self) -> Envelope[dtype]: @@ -40,4 +51,3 @@ trait Geometric(Dimensionable): # fn length(self) -> SIMD[dtype, 1] # fn translate(self, SIMD[dtype, simd_dims]) -> Self # fn rotate(self, degrees: SIMD[dtype, 1]) -> Self - diff --git a/geo_features/serialization/geoarrow.mojo b/geo_features/serialization/geoarrow.mojo index 2b33820..e69de29 100644 --- a/geo_features/serialization/geoarrow.mojo +++ b/geo_features/serialization/geoarrow.mojo @@ -1,18 +0,0 @@ -trait Geoarrowable: - """ - Serializable to and from GeoArrow representation of a Point. - - ### Spec - - - https://geoarrow.org/ - """ - - @staticmethod - fn from_geoarrow(table: PythonObject) raises -> Self: - """ - Create Point from geoarrow / pyarrow table with geometry column. - """ - ... - - fn geoarrow(self) -> PythonObject: - ... diff --git a/geo_features/serialization/json.mojo b/geo_features/serialization/json.mojo index 7cc8dd5..06a105d 100644 --- a/geo_features/serialization/json.mojo +++ b/geo_features/serialization/json.mojo @@ -2,29 +2,6 @@ from python import Python from python.object import PythonObject -trait JSONable: - """ - Serializable to and from GeoJSON representation of Point. Point coordinates are in x, y order (easting, northing for - projected coordinates, longitude, and latitude for geographic coordinates). - - ### Spec - - - https://geojson.org - - https://datatracker.ietf.org/doc/html/rfc7946 - """ - - @staticmethod - fn from_json(json: PythonObject) raises -> Self: - ... - - @staticmethod - fn from_json(json_str: String) raises -> Self: - ... - - fn json(self) -> String: - ... - - struct JSONParser: @staticmethod fn parse(json_str: String) raises -> PythonObject: diff --git a/geo_features/serialization/traits.mojo b/geo_features/serialization/traits.mojo new file mode 100644 index 0000000..c62890a --- /dev/null +++ b/geo_features/serialization/traits.mojo @@ -0,0 +1,72 @@ +trait WKTable: + """ + Serializable to and from Well Known Text (WKT). + + ### Specs + + - https://libgeos.org/specifications/wkt + - https://www.ogc.org/standard/sfa/ + - https://www.ogc.org/standard/sfs/ + """ + + @staticmethod + fn from_wkt(wkt: String) raises -> Self: + ... + + fn wkt(self) -> String: + ... + + +trait JSONable: + """ + Serializable to and from GeoJSON representation of Point. Point coordinates are in x, y order (easting, northing for + projected coordinates, longitude, and latitude for geographic coordinates). + + ### Specs + + - https://geojson.org + - https://datatracker.ietf.org/doc/html/rfc7946 + """ + + @staticmethod + fn from_json(json: PythonObject) raises -> Self: + ... + + @staticmethod + fn from_json(json_str: String) raises -> Self: + ... + + fn json(self) raises -> String: + """ + Serialize to GeoJSON format. + + ### Raises Error + + Error is raised for PointM and PointZM, because measure and other higher dimensions are not part of the GeoJSON + spec. + + > An OPTIONAL third-position element SHALL be the height in meters above or below the WGS 84 reference + > ellipsoid. (RFC 7946) + """ + ... + + +trait Geoarrowable: + """ + Serializable to and from GeoArrow representation of a Point. + + ### Spec + + - https://geoarrow.org/ + """ + + @staticmethod + fn from_geoarrow(table: PythonObject) raises -> Self: + """ + Create Point from geoarrow / pyarrow table with geometry column. + """ + ... + + # TODO: to geoarrow + # fn geoarrow(self) -> PythonObject: + # ... diff --git a/geo_features/serialization/wkt.mojo b/geo_features/serialization/wkt.mojo index d0f995c..ba607c5 100644 --- a/geo_features/serialization/wkt.mojo +++ b/geo_features/serialization/wkt.mojo @@ -2,26 +2,11 @@ from python import Python from python.object import PythonObject -trait WKTable: - """ - Serializable to and from Well Known Text (WKT). - - ### Spec(s) - - - https://libgeos.org/specifications/wkt - - https://www.ogc.org/standard/sfa/ - """ - - @staticmethod - fn from_wkt(wkt: String) raises -> Self: - ... - - fn wkt(self) -> String: - ... - - struct WKTParser: @staticmethod fn parse(wkt: String) raises -> PythonObject: + """ + Wraps shapely.from_wkt to convert WKT string to a Shapely object. + """ let shapely = Python.import_module("shapely") return shapely.from_wkt(wkt) diff --git a/geo_features/test/fixtures/geojson/multi_point/multi_point.geojson b/geo_features/test/fixtures/geojson/multi_point/multi_point.geojson new file mode 100644 index 0000000..3bf6cbf --- /dev/null +++ b/geo_features/test/fixtures/geojson/multi_point/multi_point.geojson @@ -0,0 +1 @@ +{"type":"MultiPoint","coordinates":[[-117.2,32.7],[-115.8,33.9],[-114.6,35.1],[-116.5,36.2],[-118.4,34.3],[-119.8,35.6],[-121.7,37.3],[-120.1,38.1],[-118.7,39.5],[-117.4,40.2]]} diff --git a/geo_features/test/fixtures/geojson/multi_point/multi_point_z.geojson b/geo_features/test/fixtures/geojson/multi_point/multi_point_z.geojson new file mode 100644 index 0000000..b1551fe --- /dev/null +++ b/geo_features/test/fixtures/geojson/multi_point/multi_point_z.geojson @@ -0,0 +1 @@ +{"type":"MultiPoint","coordinates":[[-117.2,32.7,853],[-115.8,33.9,412],[-114.6,35.1,224],[-116.5,36.2,731],[-118.4,34.3,66],[-119.8,35.6,924],[-121.7,37.3,102],[-120.1,38.1,569],[-118.7,39.5,888],[-117.4,40.2,632]]} diff --git a/geo_features/test/geom/test_enums_coorddims.mojo b/geo_features/test/geom/test_enums_coorddims.mojo index c852a89..d0d314f 100644 --- a/geo_features/test/geom/test_enums_coorddims.mojo +++ b/geo_features/test/geom/test_enums_coorddims.mojo @@ -10,6 +10,7 @@ from geo_features.geom.enums import CoordDims fn main() raises: test_coord_dims() + fn test_coord_dims() raises: test_constructors() test_str() diff --git a/geo_features/test/geom/test_envelope.mojo b/geo_features/test/geom/test_envelope.mojo index d3787b7..9747cb5 100644 --- a/geo_features/test/geom/test_envelope.mojo +++ b/geo_features/test/geom/test_envelope.mojo @@ -43,7 +43,7 @@ fn test_constructors() raises: _ = Envelope(Point(lon, lat, height, measure)) _ = Envelope(Point[DType.int8](lon, lat)) - _ = Envelope(Point[DType.float64](lon, lat, height, measure)) + _ = Envelope(Point(lon, lat, height, measure)) # from LineString # alias Point2_f16 = Point[DType.float16] diff --git a/geo_features/test/geom/test_layout.mojo b/geo_features/test/geom/test_layout.mojo index ca1be5f..185ea60 100644 --- a/geo_features/test/geom/test_layout.mojo +++ b/geo_features/test/geom/test_layout.mojo @@ -6,6 +6,7 @@ from geo_features.geom.layout import Layout from geo_features.test.constants import lat, lon, height, measure from geo_features.geom.enums import CoordDims + fn main() raises: test_constructors() test_equality_ops() @@ -55,6 +56,7 @@ fn test_equality_ops() raises: ga2.coordinates[Index(0, n - 1)] = 3.14 test.assert_true(ga2 != ga2b, "__ne__") + fn test_len() raises: let test = MojoTest("__len__") @@ -62,6 +64,7 @@ fn test_len() raises: let l = Layout(coords_size=n) test.assert_true(len(l) == 50, "__len__") + fn test_dims() raises: let test = MojoTest("dims") let l = Layout(coords_size=10) diff --git a/geo_features/test/geom/test_line_string.mojo b/geo_features/test/geom/test_line_string.mojo new file mode 100644 index 0000000..0286d4f --- /dev/null +++ b/geo_features/test/geom/test_line_string.mojo @@ -0,0 +1,249 @@ +from python import Python +from python.object import PythonObject +from utils.vector import DynamicVector +from utils.index import Index +from pathlib import Path + +from geo_features.test.pytest import MojoTest +from geo_features.test.constants import lon, lat, height, measure +from geo_features.geom.point import Point, Point64 +from geo_features.geom.line_string import LineString + + +fn main() raises: + test_constructors() + test_validate() + test_memory_layout() + test_get_item() + test_equality_ops() + test_repr() + test_stringable() + test_emptyable() + test_wktable() + test_jsonable() + test_geoarrowable() + + +fn test_constructors() raises: + var test = MojoTest("variadic list constructor") + + let lstr = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) + test.assert_true(lstr[0] == Point(lon, lat), "variadic list constructor") + test.assert_true(lstr[1] == Point(lon, lat), "variadic list constructor") + test.assert_true(lstr[2] == Point(lon, lat + 1), "variadic list constructor") + test.assert_true(lstr.__len__() == 3, "variadic list constructor") + + test = MojoTest("vector constructor") + + var points_vec = DynamicVector[Point64](10) + for n in range(10): + points_vec.push_back(Point(lon + n, lat - n)) + let lstr2 = LineString[DType.float64](points_vec) + for n in range(10): + let expect_pt = Point(lon + n, lat - n) + test.assert_true(lstr2[n] == expect_pt, "vector constructor") + test.assert_true(lstr2.__len__() == 10, "vector constructor") + + +fn test_validate() raises: + let test = MojoTest("is_valid") + + var err = String() + var valid: Bool = False + + valid = LineString(Point(lon, lat), Point(lon, lat)).is_valid(err) + test.assert_true(not valid, "is_valid") + test.assert_true( + err == "LineStrings with exactly two identical points are invalid.", + "unexpected error value", + ) + + valid = LineString(Point(lon, lat)).is_valid(err) + test.assert_true( + err == "LineStrings must have either 0 or 2 or more points.", + "unexpected error value", + ) + + valid = LineString( + Point(lon, lat), Point(lon + 1, lat + 1), Point(lon, lat) + ).is_valid(err) + test.assert_true( + err == "LineStrings must not be closed: try LinearRing.", + "unexpected error value", + ) + + +fn test_memory_layout() raises: + # Test if LineString fills the Layout struct correctly. + let test = MojoTest("memory_layout") + + # equality check each point by indexing into the LineString. + var points_vec20 = DynamicVector[Point64](10) + for n in range(10): + points_vec20.push_back(Point(lon + n, lat - n)) + let lstr = LineString(points_vec20) + for n in range(10): + let expect_pt = Point(lon + n, lat - n) + test.assert_true(lstr[n] == expect_pt, "memory_layout") + + # here the geometry_offsets, part_offsets, and ring_offsets are unused because + # of using "struct coordinate representation" (tensor) + let layout = lstr.data + test.assert_true( + layout.geometry_offsets.num_elements() == 0, "geo_arrow geometry_offsets" + ) + test.assert_true(layout.part_offsets.num_elements() == 0, "geo_arrow part_offsets") + test.assert_true(layout.ring_offsets.num_elements() == 0, "geo_arrow ring_offsets") + + +fn test_get_item() raises: + let test = MojoTest("get_item") + var points_vec = DynamicVector[Point64](10) + for n in range(10): + points_vec.push_back(Point(lon + n, lat - n)) + let lstr = LineString(points_vec) + for n in range(10): + let expect_pt = Point(lon + n, lat - n) + let got_pt = lstr[n] + test.assert_true(got_pt == expect_pt, "get_item") + + +fn test_equality_ops() raises: + let test = MojoTest("equality operators") + + # partial simd_load (n - i < nelts) + let lstr8 = LineString( + Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8), Point(9, 10) + ) + let lstr9 = LineString( + Point(1.1, 2.1), + Point(3.1, 4.1), + Point(5.1, 6.1), + Point(7.1, 8.1), + Point(9.1, 10.1), + ) + test.assert_true(lstr8 != lstr9, "partial simd_load (n - i < nelts)") + + # partial simd_load (n - i < nelts) + alias PointF32 = Point[DType.float32] + let lstr10 = LineString( + PointF32(1, 2), + PointF32(5, 6), + PointF32(10, 11), + ) + let lstr11 = LineString( + PointF32(1, 2), + PointF32(5, 6), + PointF32(10, 11.1), + ) + test.assert_true(lstr10 != lstr11, "partial simd_load (n - i < nelts) (b)") + + # not equal + alias PointF16 = Point[DType.float16] + let lstr12 = LineString( + PointF16(1, 2), + PointF16(5, 6), + PointF16(10, 11), + ) + let lstr13 = LineString( + PointF16(1, 2), + PointF16(5, 6), + PointF16(10, 11.1), + ) + test.assert_true(lstr12 != lstr13, "__ne__") + + var points_vec = DynamicVector[Point64](10) + for n in range(10): + points_vec.push_back(Point(lon + n, lat - n)) + + let lstr2 = LineString(points_vec) + let lstr3 = LineString(points_vec) + test.assert_true(lstr2 == lstr3, "__eq__") + + let lstr4 = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) + let lstr5 = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) + test.assert_true(lstr4 == lstr5, "__eq__") + + let lstr6 = LineString(Point(42, lat), Point(lon, lat)) + test.assert_true(lstr5 != lstr6, "__eq__") + + +fn test_emptyable() raises: + let test = MojoTest("is_empty") + let empty_lstr = LineString() + _ = empty_lstr.is_empty() + + +fn test_repr() raises: + let test = MojoTest("__repr__") + let lstr = LineString(Point(42, lat), Point(lon, lat)) + test.assert_true( + lstr.__repr__() == "LineString [Point, float64](2 points)", "__repr__" + ) + + +fn test_sized() raises: + let test = MojoTest("sized") + let lstr = LineString(Point(42, lat), Point(lon, lat)) + test.assert_true(len(lstr) == 2, "__len__") + + +fn test_stringable() raises: + let test = MojoTest("stringable") + let lstr = LineString(Point(42, lat), Point(lon, lat)) + test.assert_true(lstr.__str__() == lstr.__repr__(), "__str__") + + +fn test_wktable() raises: + test_wkt() + # test_from_wkt() + + +fn test_wkt() raises: + let test = MojoTest("wkt") + let lstr = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) + test.assert_true( + lstr.wkt() + == "LINESTRING(-108.68000000000001 38.973999999999997, -108.68000000000001" + " 38.973999999999997, -108.68000000000001 39.973999999999997)", + "wkt", + ) + + +fn test_jsonable() raises: + test_json() + test_from_json() + + +fn test_json() raises: + let test = MojoTest("json") + var points_vec = DynamicVector[Point64](10) + for n in range(10): + points_vec.push_back(Point(lon + n, lat - n)) + let json = LineString(points_vec).json() + test.assert_true( + json + == '{"type":"LineString","coordinates":[[-108.68000000000001,38.973999999999997],[-107.68000000000001,37.973999999999997],[-106.68000000000001,36.973999999999997],[-105.68000000000001,35.973999999999997],[-104.68000000000001,34.973999999999997],[-103.68000000000001,33.973999999999997],[-102.68000000000001,32.973999999999997],[-101.68000000000001,31.973999999999997],[-100.68000000000001,30.973999999999997],[-99.680000000000007,29.973999999999997]]}', + "json", + ) + + +fn test_from_json() raises: + let test = MojoTest("from_json()") + + let json = Python.import_module("orjson") + let builtins = Python.import_module("builtins") + let path = Path("geo_features/test/fixtures/geojson/line_string") + let fixtures = VariadicList("curved.geojson", "straight.geojson", "zigzag.geojson") + + for i in range(len(fixtures)): + let file = path / fixtures[i] + with open(file.path, "r") as f: + let geojson = f.read() + let geojson_dict = json.loads(geojson) + _ = LineString.from_json(geojson_dict) + + +fn test_geoarrowable() raises: + # TODO: geoarrowable trait + pass diff --git a/geo_features/test/geom/test_line_string.nojo b/geo_features/test/geom/test_line_string.nojo deleted file mode 100644 index 6aa8b23..0000000 --- a/geo_features/test/geom/test_line_string.nojo +++ /dev/null @@ -1,286 +0,0 @@ -from python import Python -from python.object import PythonObject -from utils.vector import DynamicVector -from utils.index import Index -from pathlib import Path - -from geo_features.test.pytest import MojoTest -from geo_features.test.constants import lon, lat, height, measure -from geo_features.geom.point import ( - Point, - Point2, - PointZ, - PointM, - PointZM, -) - -# TODO: it should be possible to use implicit parameters and only import LineString into this module (not LineString2 etc) -from geo_features.geom.line_string import ( - LineString, - LineString2, - LineStringM, - LineStringZ, - LineStringZM, -) - - -fn main() raises: - test_constructors() - test_validate() - test_memory_layout() - test_get_item() - test_equality_ops() - test_is_empty() - test_repr() - test_str() - test_wkt() - test_from_geoarrow() - - # TODO: https://github.com/modularml/mojo/issues/1160 - # test_is_simple() - - test_from_json() - - # TODO: https://github.com/modularml/mojo/issues/1160 - # test_from_wkt() - - -fn test_constructors() raises: - var test = MojoTest("variadic list constructor") - - let lstr = LineString(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - test.assert_true(lstr[0] == Point2(lon, lat), "variadic list constructor") - test.assert_true(lstr[1] == Point2(lon, lat), "variadic list constructor") - test.assert_true(lstr[2] == Point2(lon, lat + 1), "variadic list constructor") - test.assert_true(lstr.__len__() == 3, "variadic list constructor") - - test = MojoTest("vector constructor") - - var points_vec = DynamicVector[Point2](10) - for n in range(10): - points_vec.push_back(Point2(lon + n, lat - n)) - let lstr2 = LineString[Point2.simd_dims, Point2.dtype](points_vec) - for n in range(10): - let expect_pt = Point2(lon + n, lat - n) - test.assert_true(lstr2[n] == expect_pt, "vector constructor") - test.assert_true(lstr2.__len__() == 10, "vector constructor") - - -fn test_validate() raises: - let test = MojoTest("is_valid") - - var err = String() - var valid: Bool = False - - valid = LineString(Point2(lon, lat), Point2(lon, lat)).is_valid(err) - test.assert_true(not valid, "is_valid") - test.assert_true( - err == "LineStrings with exactly two identical points are invalid.", - "unexpected error value", - ) - - valid = LineString(Point2(lon, lat)).is_valid(err) - test.assert_true( - err == "LineStrings must have either 0 or 2 or more points.", - "unexpected error value", - ) - - valid = LineString( - Point2(lon, lat), Point2(lon + 1, lat + 1), Point2(lon, lat) - ).is_valid(err) - test.assert_true( - err == "LineStrings must not be closed: try LinearRing.", - "unexpected error value", - ) - - -fn test_memory_layout() raises: - # Test if LineString fills the Layout struct correctly. - let test = MojoTest("memory_layout") - - # equality check each point by indexing into the LineString. - var points_vec20 = DynamicVector[Point2](10) - for n in range(10): - points_vec20.push_back(Point2(lon + n, lat - n)) - let lstr = LineString2(points_vec20) - for n in range(10): - let expect_pt = Point2(lon + n, lat - n) - test.assert_true(lstr[n] == expect_pt, "memory_layout") - - # here the geometry_offsets, part_offsets, and ring_offsets are unused because - # of using "struct coordinate representation" (tensor) - let layout = lstr.data - test.assert_true( - layout.geometry_offsets.num_elements() == 0, "geo_arrow geometry_offsets" - ) - test.assert_true(layout.part_offsets.num_elements() == 0, "geo_arrow part_offsets") - test.assert_true(layout.ring_offsets.num_elements() == 0, "geo_arrow ring_offsets") - - -fn test_get_item() raises: - let test = MojoTest("get_item") - var points_vec = DynamicVector[Point2](10) - for n in range(10): - points_vec.push_back(Point2(lon + n, lat - n)) - let lstr = LineString2(points_vec) - for n in range(10): - let expect_pt = Point2(lon + n, lat - n) - let got_pt = lstr[n] - test.assert_true(got_pt == expect_pt, "get_item") - - -fn test_equality_ops() raises: - let test = MojoTest("equality operators") - - # partial simd_load (n - i < nelts) - let lstr8 = LineString( - Point2(1, 2), Point2(3, 4), Point2(5, 6), Point2(7, 8), Point2(9, 10) - ) - let lstr9 = LineString( - Point2(1.1, 2.1), - Point2(3.1, 4.1), - Point2(5.1, 6.1), - Point2(7.1, 8.1), - Point2(9.1, 10.1), - ) - test.assert_true(lstr8 != lstr9, "partial simd_load (n - i < nelts)") - - # partial simd_load (n - i < nelts) - alias Point2F32 = Point[simd_dims=2, dtype = DType.float32] - let lstr10 = LineString( - Point2F32(1, 2), - Point2F32(5, 6), - Point2F32(10, 11), - ) - let lstr11 = LineString( - Point2F32(1, 2), - Point2F32(5, 6), - Point2F32(10, 11.1), - ) - test.assert_true(lstr10 != lstr11, "partial simd_load (n - i < nelts) (b)") - - # not equal - alias Point2F16 = Point[2, DType.float16] - let lstr12 = LineString( - Point2F16(1, 2), - Point2F16(5, 6), - Point2F16(10, 11), - ) - let lstr13 = LineString( - Point2F16(1, 2), - Point2F16(5, 6), - Point2F16(10, 11.1), - ) - test.assert_true(lstr12 != lstr13, "__ne__") - - var points_vec = DynamicVector[Point2](10) - for n in range(10): - points_vec.push_back(Point2(lon + n, lat - n)) - - let lstr2 = LineString2(points_vec) - let lstr3 = LineString2(points_vec) - test.assert_true(lstr2 == lstr3, "__eq__") - - let lstr4 = LineString(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - let lstr5 = LineString(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - test.assert_true(lstr4 == lstr5, "__eq__") - - let lstr6 = LineString(Point2(42, lat), Point2(lon, lat)) - test.assert_true(lstr5 != lstr6, "__eq__") - - -fn test_is_empty() raises: - let test = MojoTest("is_empty") - let empty_lstr = LineString2() - _ = empty_lstr.is_empty() - - -fn test_repr() raises: - let test = MojoTest("__repr__") - let lstr = LineString(Point2(42, lat), Point2(lon, lat)) - test.assert_true(lstr.__repr__() == "LineString[2, float64](2 points)", "__repr__") - - -fn test_str() raises: - let test = MojoTest("__str__") - let lstr = LineString(Point2(42, lat), Point2(lon, lat)) - # str() is expected to be the same as wkt() - test.assert_true(lstr.__str__() == lstr.wkt(), "__str__") - - -fn test_wkt() raises: - let test = MojoTest("wkt") - let lstr = LineString(Point2(lon, lat), Point2(lon, lat), Point2(lon, lat + 1)) - test.assert_true( - lstr.wkt() - == "LINESTRING(-108.68000000000001 38.973999999999997, -108.68000000000001" - " 38.973999999999997, -108.68000000000001 39.973999999999997)", - "wkt", - ) - - -# fn test_json() raises: -# print("json") -# var points_vec = DynamicVector[Point2](10) -# for n in range(10): -# points_vec.push_back(Point2(lon + n, lat - n)) -# let json = LineString(points_vec).json() -# test.assert_true( -# json -# == '{"type":"LineString","coordinates":[[-108.68000030517578,38.9739990234375],[-107.68000030517578,37.9739990234375],[-106.68000030517578,36.9739990234375],[-105.68000030517578,35.9739990234375],[-104.68000030517578,34.9739990234375],[-103.68000030517578,33.9739990234375],[-102.68000030517578,32.9739990234375],[-101.68000030517578,31.974000930786133],[-100.68000030517578,30.974000930786133],[-99.680000305175781,29.974000930786133]]}', -# "json", -# ) - - -fn test_is_simple() raises: - let test = MojoTest("is_simple (⚠️ not implemented)") - try: - _ = LineString(Point2(42, lat), Point2(lon, lat)).is_simple() - raise Error("unreachable") - except e: - pass - # test.assert_true(e.__str__() == "not implemented", "unexpected error value") # TODO - - -fn test_from_json() raises: - let test = MojoTest("from_json()") - - let json = Python.import_module("orjson") - let builtins = Python.import_module("builtins") - let path = Path("geo_features/test/fixtures/geojson/line_string") - let fixtures = VariadicList("curved.geojson", "straight.geojson", "zigzag.geojson") - - for i in range(len(fixtures)): - let file = path / fixtures[i] - with open(file.path, "r") as f: - let geojson = f.read() - let geojson_dict = json.loads(geojson) - _ = LineString2.from_json(geojson_dict) - - -# fn test_from_wkt() raises: -# print("from_wkt (⚠️ not implemented)") -# try: -# _ = LineString.from_wkt("") -# # raise Error("unreachable") -# except e: -# test.assert_true(e.__str__() == "not implemented", "unexpected error value") # TODO - - -fn test_from_geoarrow() raises: - let test = MojoTest("from_geoarrow") - - # raise Error("TODO") - # TODO: read() binary arrow when mojo supports - - # let ga = Python.import_module("geoarrow.pyarrow") - # let path = Path("geo_features/test/fixtures/wkt/line_string") - # let fixtures = VariadicList("curved.wkt") - # var wkt: String - # for i in range(len(fixtures)): - # let file = path / fixtures[i] - # with open(file, "r") as f: - # wkt = f.read() - # print(wkt) - # let arrow = ga.as_geoarrow("["+ wkt + "]") - # print(arrow) diff --git a/geo_features/test/geom/test_multi_point.mojo b/geo_features/test/geom/test_multi_point.mojo index 3950527..8c824cc 100644 --- a/geo_features/test/geom/test_multi_point.mojo +++ b/geo_features/test/geom/test_multi_point.mojo @@ -6,8 +6,10 @@ from pathlib import Path from geo_features.test.constants import lat, lon, height, measure from geo_features.test.pytest import MojoTest -from geo_features.geom.point import Point +from geo_features.geom.point import Point, Point64 from geo_features.geom.multi_point import MultiPoint +from geo_features.serialization.json import JSONParser +from geo_features.geom.enums import CoordDims fn main() raises: @@ -33,14 +35,14 @@ fn test_constructors() raises: test.assert_true(mpt[0] == Point(lon, lat), "variadic list constructor") test.assert_true(mpt[1] == Point(lon, lat), "variadic list constructor") test.assert_true(mpt[2] == Point(lon, lat + 1), "variadic list constructor") - test.assert_true(mpt.__len__() == 3, "variadic list constructor") + test.assert_true(len(mpt) == 3, "variadic list constructor") test = MojoTest("vector constructor") - var points_vec = DynamicVector[Point[]](10) + var points_vec = DynamicVector[Point64](10) for n in range(10): points_vec.push_back(Point(lon + n, lat - n)) - _ = MultiPoint[](points_vec) + _ = MultiPoint(points_vec) fn test_mem_layout() raises: @@ -50,7 +52,7 @@ fn test_mem_layout() raises: let test = MojoTest("mem layout") # equality check each point by indexing into the MultiPoint. - var points_vec = DynamicVector[Point[]](10) + var points_vec = DynamicVector[Point64](10) for n in range(10): points_vec.push_back(Point(lon + n, lat - n)) let mpt2 = MultiPoint(points_vec) @@ -70,7 +72,7 @@ fn test_mem_layout() raises: fn test_get_item() raises: let test = MojoTest("get_item") - var points_vec = DynamicVector[Point[]](10) + var points_vec = DynamicVector[Point64](10) for n in range(10): points_vec.push_back(Point(lon + n, lat - n)) let mpt = MultiPoint(points_vec) @@ -123,7 +125,7 @@ fn test_equality_ops() raises: ) test.assert_true(mpt7 != mpt8, "__ne__") - var points_vec2 = DynamicVector[Point[]](10) + var points_vec2 = DynamicVector[Point64](10) for n in range(10): points_vec2.push_back(Point(lon + n, lat - n)) let mpt9 = MultiPoint(points_vec2) @@ -165,26 +167,63 @@ fn test_wktable() raises: with open(file.path, "r") as f: let wkt = f.read() let mp = MultiPoint.from_wkt(wkt) - test.assert_true(mp.wkt() != "FIXME", "wkt") # FIXME: no number formatting so cannot compare wkt strings. + test.assert_true( + mp.wkt() != "FIXME", "wkt" + ) # FIXME: no number formatting so cannot compare wkt strings. fn test_jsonable() raises: - var test = MojoTest("json") + test_json() + test_from_json() + + +fn test_json() raises: + let test = MojoTest("json") + let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) test.assert_true( mpt.json() == '{"type":"MultiPoint","coordinates":[[-108.68000000000001,38.973999999999997],[-107.68000000000001,39.973999999999997]]}', "json", ) - - test = MojoTest("from_json") - let json_str = String( - '{"type":"MultiPoint","coordinates":[[42.0,38.9739990234375],[42.0,38.9739990234375]]}' + + let mpt_z = MultiPoint(Point(lon, lat, height), Point(lon + 1, lat + 1, height - 1)) + test.assert_true( + mpt_z.json() + == '{"type":"MultiPoint","coordinates":[[-108.68000000000001,38.973999999999997,8.0],[-107.68000000000001,39.973999999999997,7.0]]}', + "json", ) - let json = Python.import_module("orjson") - let json_dict = json.loads(json_str) + let expect_error = "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" + var mpt_m = MultiPoint( + Point(lon, lat, measure), Point(lon + 1, lat + 1, measure - 1) + ) + mpt_m.set_ogc_dims(CoordDims.PointM) + try: + _ = mpt_m.json() + except e: + test.assert_true(str(e) == expect_error, "json raises") + + let mpt_zm = MultiPoint( + Point(lon, lat, height, measure), + Point(lon + 1, lat + 1, height * 2, measure - 1), + ) try: - _ = MultiPoint.from_json(json_dict) + _ = mpt_zm.json() except e: - test.assert_true(False, "TODO: from_json") + test.assert_true(str(e) == expect_error, "json raises") + + +fn test_from_json() raises: + pass + # let test = MojoTest("from_json") + + # let path = Path("geo_features/test/fixtures/geojson/multi_point") + # let fixtures = VariadicList("multi_point.geojson") # , "multi_point_z.geojson" + # for i in range(len(fixtures)): + # let file = path / fixtures[i] + # with open(file.path, "r") as f: + # let json_str = f.read() + # _ = MultiPoint.from_json(json_str) + # let json_dict = JSONParser.parse(json_str) + # _ = MultiPoint.from_json(json_dict) diff --git a/geo_features/test/geom/test_point.mojo b/geo_features/test/geom/test_point.mojo index b1f7394..1eb9cb6 100644 --- a/geo_features/test/geom/test_point.mojo +++ b/geo_features/test/geom/test_point.mojo @@ -112,7 +112,6 @@ fn test_dimensionable() raises: fn test_geometric() raises: let test = MojoTest("geometric") - test.assert_true(False, "TODO: geometric") fn test_emptyable() raises: @@ -232,12 +231,19 @@ fn test_json() raises: "json()", ) + let expect_error = "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" + var pt_m = Point(lon, lat, measure) + pt_m.set_ogc_dims(CoordDims.PointM) + try: + _ = pt_m.json() + except e: + test.assert_true(str(e) == expect_error, "json raises") + let pt4 = Point(lon, lat, height, measure) - test.assert_true( - pt4.json() - == '{"type":"Point","coordinates":[-108.68000000000001,38.973999999999997,8.0]}', - "json()", - ) + try: + _ = pt4.json() + except e: + test.assert_true(str(e) == expect_error, "json raises") fn test_from_json() raises: @@ -337,13 +343,14 @@ fn test_from_wkt() raises: " packages." ) + fn test_geoarrowable() raises: - test_geoarrow() + # TODO test_geoarrow() test_from_geoarrow() -fn test_geoarrow() raises: - let test = MojoTest("geoarrow") - test.assert_true(False, "TODO: geoarrow") + +# fn test_geoarrow() raises: +# let test = MojoTest("geoarrow") fn test_from_geoarrow() raises: From c328254b9a702b713fd6527770590f4c0d58547f Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Fri, 15 Dec 2023 17:23:58 -0700 Subject: [PATCH 10/11] debug GH workflow --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5c0e188..dc3ab6b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -35,6 +35,7 @@ jobs: run: | curl https://get.modular.com | MODULAR_AUTH=${{ secrets.MODULAR_AUTH }} sh - modular auth ${{ secrets.MODULAR_AUTH }} + modular host-info modular install mojo - name: "Setup conda env (geo-features)" From 0c1b48ea6971ab03ddc6f3929e03ad78bcc59d86 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Fri, 15 Dec 2023 17:36:36 -0700 Subject: [PATCH 11/11] fix GH actions --- .github/workflows/tests.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index dc3ab6b..137c3f0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -33,9 +33,8 @@ jobs: - name: "Install mojo" run: | - curl https://get.modular.com | MODULAR_AUTH=${{ secrets.MODULAR_AUTH }} sh - - modular auth ${{ secrets.MODULAR_AUTH }} - modular host-info + curl https://get.modular.com | sh - && \ + modular auth ${{secrets.MODULAR_AUTH}} && \ modular install mojo - name: "Setup conda env (geo-features)"