@@ -1050,3 +1050,180 @@ def test_random_ls_poly_f64(self, cpp):
10501050 i_cpp = cpp .intersects_linestring_polygon (cpp .linestring (l ), cpp .polygon (c ))
10511051 i_py = PyLineString (l ).intersects (PyPolygon (c ))
10521052 assert i_cpp == i_py
1053+
1054+
1055+ # ==============================================================================
1056+ # 3-COLUMN ARRAY SHAPE HANDLING — verify (N,≥3) → x,y extraction
1057+ # ==============================================================================
1058+ # Regression tests for the array_to_double_vec() fix (hardcoded rows*2 → stride).
1059+ # Covers: generic py::array overload (int dtype), typed double overload, point,
1060+ # polygon, multipoint; plus row/column layout consistency and non-contiguous view guard.
1061+
1062+ class TestArrayShape3D :
1063+ """Verify (N,≥3) arrays correctly extract x,y columns across all factory overloads."""
1064+
1065+ # -- (N,2) regression: generic overload (int32) still works ----------------------
1066+
1067+ def test_2col_generic (self , cpp ):
1068+ coords = np .array ([[0 , 1 ], [2 , 3 ], [4 , 5 ]], dtype = np .int32 )
1069+ ls = cpp .linestring (coords )
1070+ assert ls .coords () == [(0.0 , 1.0 ), (2.0 , 3.0 ), (4.0 , 5.0 )]
1071+
1072+ # -- (N,3) generic overload: z column correctly ignored -------------------------
1073+
1074+ def test_3col_generic (self , cpp ):
1075+ coords = np .array ([[10 , 20 , 999 ], [30 , 40 , 888 ], [50 , 60 , 777 ]], dtype = np .int32 )
1076+ ls = cpp .linestring (coords )
1077+ assert ls .coords () == [(10.0 , 20.0 ), (30.0 , 40.0 ), (50.0 , 60.0 )]
1078+
1079+ # -- (N,3) typed double overload: stride by shape[1]=3 --------------------------
1080+
1081+ def test_3col_typed_double (self , cpp ):
1082+ coords = np .array ([[10.0 , 20.0 , 99.0 ], [30.0 , 40.0 , 88.0 ], [50.0 , 60.0 , 77.0 ]],
1083+ dtype = np .float64 )
1084+ ls = cpp .linestring (coords )
1085+ assert ls .coords () == [(10.0 , 20.0 ), (30.0 , 40.0 ), (50.0 , 60.0 )]
1086+
1087+ # -- Equality: (N,3) produces identical coords to (N,2) -------------------------
1088+
1089+ def test_3col_equals_2col_generic (self , cpp ):
1090+ coords_3 = np .array ([[1 , 2 , 99 ], [3 , 4 , 88 ], [5 , 6 , 77 ]], dtype = np .int32 )
1091+ coords_2 = np .array ([[1.0 , 2.0 ], [3.0 , 4.0 ], [5.0 , 6.0 ]], dtype = np .float64 )
1092+ ls3 = cpp .linestring (coords_3 )
1093+ ls2 = cpp .linestring (coords_2 )
1094+ assert ls3 .coords () == ls2 .coords ()
1095+ # Distance to self should also match
1096+ assert ls3 .length () == ls2 .length ()
1097+
1098+ def test_3col_equals_2col_typed (self , cpp ):
1099+ coords_3 = np .array ([[1.0 , 2.0 , 99.0 ], [3.0 , 4.0 , 88.0 ], [5.0 , 6.0 , 77.0 ]],
1100+ dtype = np .float64 )
1101+ coords_2 = np .array ([[1.0 , 2.0 ], [3.0 , 4.0 ], [5.0 , 6.0 ]], dtype = np .float64 )
1102+ ls3 = cpp .linestring (coords_3 )
1103+ ls2 = cpp .linestring (coords_2 )
1104+ assert ls3 .coords () == ls2 .coords ()
1105+ assert ls3 .length () == ls2 .length ()
1106+
1107+ # -- Polygon (N,3) generic ------------------------------------------------------
1108+
1109+ def test_polygon_3col_generic (self , cpp ):
1110+ coords = np .array ([[0 , 0 , 1 ], [10 , 0 , 2 ], [10 , 10 , 3 ], [0 , 10 , 4 ], [0 , 0 , 5 ]],
1111+ dtype = np .int32 )
1112+ poly = cpp .polygon (coords )
1113+ ext = cpp .polygon_exterior (poly )
1114+ # First 5 points must match (GEOS may auto-close to 6 points)
1115+ assert ext .tolist ()[:5 ] == [[0.0 , 0.0 ], [10.0 , 0.0 ], [10.0 , 10.0 ], [0.0 , 10.0 ], [0.0 , 0.0 ]]
1116+
1117+ def test_polygon_3col_typed (self , cpp ):
1118+ coords = np .array ([[0.0 , 0.0 , 1.0 ], [10.0 , 0.0 , 2.0 ], [10.0 , 10.0 , 3.0 ],
1119+ [0.0 , 10.0 , 4.0 ], [0.0 , 0.0 , 5.0 ]], dtype = np .float64 )
1120+ poly = cpp .polygon (coords )
1121+ ext = cpp .polygon_exterior (poly )
1122+ assert ext .tolist ()[:5 ] == [[0.0 , 0.0 ], [10.0 , 0.0 ], [10.0 , 10.0 ], [0.0 , 10.0 ], [0.0 , 0.0 ]]
1123+
1124+ # -- Point: 1D [x,y] and 2D (N,≥2) int arrays (auto-double overload) -------------
1125+
1126+ def test_point_1d_int (self , cpp ):
1127+ p = cpp .point (np .array ([3 , 4 ], dtype = np .int32 ))
1128+ assert p .coords () == [(3.0 , 4.0 )]
1129+
1130+ def test_point_2d_int (self , cpp ):
1131+ p = cpp .point (np .array ([[5 , 6 ]], dtype = np .int32 ))
1132+ assert p .coords () == [(5.0 , 6.0 )]
1133+
1134+ def test_point_2d_3col_int (self , cpp ):
1135+ """Point from (N,3) int: takes first 2 elements (row 0, col 0-1)."""
1136+ p = cpp .point (np .array ([[7 , 8 , 999 ], [9 , 10 , 888 ]], dtype = np .int32 ))
1137+ assert p .coords () == [(7.0 , 8.0 )]
1138+
1139+ # -- MultiPoint (N,3) generic ---------------------------------------------------
1140+
1141+ def test_multipoint_3col_generic (self , cpp ):
1142+ mp = cpp .multipoint (np .array ([[1 , 2 , 99 ], [4 , 6 , 88 ]], dtype = np .int32 ))
1143+ # Verify via distance: (1,2) should be a point in the multipoint
1144+ p = cpp .point (1.0 , 2.0 )
1145+ assert mp .distance (p ) == 0.0
1146+
1147+ # -- LinearRing (N,3) -----------------------------------------------------------
1148+
1149+ def test_linearring_3col (self , cpp , C ):
1150+ ring = C .linearring (np .array ([[0 , 0 , 1 ], [1 , 0 , 2 ], [1 , 1 , 3 ], [0 , 0 , 4 ]],
1151+ dtype = np .float64 ))
1152+ assert ring .is_closed () == True
1153+ assert ring .is_ring () == True
1154+
1155+ # -- Row/column layout consistency ----------------------------------------------
1156+
1157+ def test_row_col_layout (self , cpp ):
1158+ """Verify Python arr[i,j] ↔ C++ ptr[i*shape[1]+j] mapping."""
1159+ # (3,4) array: row-major [1,2,3,4, 5,6,7,8, 9,10,11,12]
1160+ arr = np .array ([[1 , 2 , 3 , 4 ], [5 , 6 , 7 , 8 ], [9 , 10 , 11 , 12 ]], dtype = np .float64 )
1161+ ls = cpp .linestring (arr )
1162+ # C++ takes coords_[i*4+0], coords_[i*4+1] → first 2 columns per row
1163+ assert ls .coords () == [(1.0 , 2.0 ), (5.0 , 6.0 ), (9.0 , 10.0 )]
1164+
1165+ # (3,3) direct: stride=3, skip z
1166+ arr3 = np .array ([[10 , 20 , 99 ], [30 , 40 , 88 ], [50 , 60 , 77 ]], dtype = np .float64 )
1167+ ls3 = cpp .linestring (arr3 )
1168+ assert ls3 .coords () == [(10.0 , 20.0 ), (30.0 , 40.0 ), (50.0 , 60.0 )]
1169+
1170+ # -- poly_exterior output shape --------------------------------------------------
1171+
1172+ def test_exterior_shape (self , cpp ):
1173+ coords = np .array ([[0 , 0 ], [10 , 0 ], [10 , 10 ], [0 , 10 ]], dtype = np .float64 )
1174+ poly = cpp .polygon (coords )
1175+ ext = cpp .polygon_exterior (poly )
1176+ assert ext .ndim == 2
1177+ assert ext .shape [1 ] == 2 # always 2 columns
1178+ assert ext .dtype == np .float64
1179+
1180+ # -- Non-contiguous view rejection (fail-fast with clear error) ----------------
1181+
1182+ def test_noncontiguous_view_rejected_typed (self , cpp ):
1183+ """Typed overloads throw on non-contiguous views (e.g. [:, :2])."""
1184+ arr = np .array ([[10 , 20 , 99 ], [30 , 40 , 88 ], [50 , 60 , 77 ]], dtype = np .float64 )
1185+ view = arr [:, :2 ]
1186+ assert not view .flags ['C_CONTIGUOUS' ]
1187+ with pytest .raises (ValueError , match = "C-contiguous" ):
1188+ cpp .linestring (view )
1189+
1190+ def test_noncontiguous_view_rejected_generic (self , cpp ):
1191+ """Generic (int dtype) overloads throw on non-contiguous views."""
1192+ arr = np .array ([[10 , 20 , 99 ], [30 , 40 , 88 ], [50 , 60 , 77 ]], dtype = np .int32 )
1193+ view = arr [:, :2 ]
1194+ assert not view .flags ['C_CONTIGUOUS' ]
1195+ with pytest .raises (ValueError , match = "C-contiguous" ):
1196+ cpp .linestring (view )
1197+
1198+ def test_noncontiguous_copy_works_typed (self , cpp ):
1199+ """.copy() on a non-contiguous view produces correct results."""
1200+ arr = np .array ([[10 , 20 , 99 ], [30 , 40 , 88 ], [50 , 60 , 77 ]], dtype = np .float64 )
1201+ cpy = arr [:, :2 ].copy ()
1202+ assert cpy .flags ['C_CONTIGUOUS' ]
1203+ ls = cpp .linestring (cpy )
1204+ assert ls .coords () == [(10.0 , 20.0 ), (30.0 , 40.0 ), (50.0 , 60.0 )]
1205+
1206+ def test_noncontiguous_copy_works_generic (self , cpp ):
1207+ """.copy() on a non-contiguous view works for generic overload."""
1208+ arr = np .array ([[10 , 20 , 99 ], [30 , 40 , 88 ], [50 , 60 , 77 ]], dtype = np .int32 )
1209+ cpy = arr [:, :2 ].copy ()
1210+ assert cpy .flags ['C_CONTIGUOUS' ]
1211+ ls = cpp .linestring (cpy )
1212+ assert ls .coords () == [(10.0 , 20.0 ), (30.0 , 40.0 ), (50.0 , 60.0 )]
1213+
1214+ @pytest .mark .parametrize ("factory, arr_dtype" , [
1215+ ("point" , np .float64 ),
1216+ ("point" , np .int32 ),
1217+ ("polygon" , np .float64 ),
1218+ ("polygon" , np .int32 ),
1219+ ("multipoint" , np .float64 ),
1220+ ("multipoint" , np .int32 ),
1221+ ])
1222+ def test_noncontiguous_rejected_all_factories (self , cpp , factory , arr_dtype ):
1223+ """All factory functions reject non-contiguous views."""
1224+ arr = np .array ([[10 , 20 , 99 ], [30 , 40 , 88 ], [50 , 60 , 77 ]], dtype = arr_dtype )
1225+ view = arr [:, :2 ]
1226+ assert not view .flags ['C_CONTIGUOUS' ]
1227+ fn = getattr (cpp , factory )
1228+ with pytest .raises (ValueError , match = "C-contiguous" ):
1229+ fn (view )
0 commit comments