UPDATE
This commit is contained in:
@@ -7,17 +7,25 @@ from django.core.files.base import ContentFile
|
||||
from django.test import TestCase
|
||||
|
||||
from location_data.data_driven_subdivision import (
|
||||
ClusteringDataset,
|
||||
EmptyObservationDatasetError,
|
||||
_persist_remote_sensing_diagnostic_artifacts,
|
||||
_build_observation_label,
|
||||
_build_cluster_geometry,
|
||||
build_cluster_summaries,
|
||||
build_clustering_dataset,
|
||||
create_remote_sensing_subdivision_result,
|
||||
enforce_spatial_contiguity,
|
||||
sync_block_subdivision_with_result,
|
||||
)
|
||||
from location_data.models import (
|
||||
AnalysisGridCell,
|
||||
AnalysisGridObservation,
|
||||
BlockSubdivision,
|
||||
RemoteSensingClusterBlock,
|
||||
RemoteSensingRun,
|
||||
RemoteSensingSubdivisionResult,
|
||||
RemoteSensingSubdivisionOption,
|
||||
SoilLocation,
|
||||
)
|
||||
|
||||
@@ -58,7 +66,8 @@ class DataDrivenSubdivisionSyncTests(TestCase):
|
||||
status=RemoteSensingRun.STATUS_SUCCESS,
|
||||
)
|
||||
|
||||
def test_sync_block_subdivision_with_result_updates_saved_sub_blocks(self):
|
||||
@patch("location_data.data_driven_subdivision.render_elbow_plot", return_value=None)
|
||||
def test_sync_block_subdivision_with_result_updates_saved_sub_blocks(self, _mock_plot):
|
||||
cell_1 = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
@@ -190,6 +199,9 @@ class DataDrivenSubdivisionSyncTests(TestCase):
|
||||
), patch(
|
||||
"location_data.data_driven_subdivision._render_feature_pair_plot",
|
||||
return_value=ContentFile(b"pairs"),
|
||||
), patch(
|
||||
"location_data.data_driven_subdivision._render_feature_projection_plot",
|
||||
return_value=ContentFile(b"projection"),
|
||||
):
|
||||
artifacts = _persist_remote_sensing_diagnostic_artifacts(
|
||||
result=result,
|
||||
@@ -207,13 +219,23 @@ class DataDrivenSubdivisionSyncTests(TestCase):
|
||||
selected_features=["ndvi", "ndwi", "soil_vv_db"],
|
||||
scaled_matrix=[[0.0, 0.0, 0.0]],
|
||||
inertia_curve=[{"k": 1, "sse": 0.0}],
|
||||
requested_k=1,
|
||||
effective_cluster_count=1,
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(artifacts["files"].keys()),
|
||||
["cluster_map", "cluster_sizes", "elbow_plot", "feature_pairs"],
|
||||
[
|
||||
"cluster_map",
|
||||
"cluster_sizes",
|
||||
"elbow_plot",
|
||||
"feature_pairs",
|
||||
"feature_projection",
|
||||
],
|
||||
)
|
||||
self.assertIn("k-1-effective-1", artifacts["directory"])
|
||||
for path in artifacts["files"].values():
|
||||
self.assertTrue(os.path.exists(path))
|
||||
self.assertIn("__k-1__effective-1__", path)
|
||||
|
||||
def test_build_clustering_dataset_raises_clear_error_when_all_selected_features_are_null(self):
|
||||
cell = AnalysisGridCell.objects.create(
|
||||
@@ -250,3 +272,301 @@ class DataDrivenSubdivisionSyncTests(TestCase):
|
||||
self.assertIn("No usable observations available for clustering", joined)
|
||||
self.assertIn('"run_id": {}'.format(self.run.id), joined)
|
||||
self.assertIn('"region_id": {}'.format(self.location.id), joined)
|
||||
|
||||
def test_build_cluster_summaries_selects_middle_grid_as_k_center(self):
|
||||
observations = []
|
||||
for index in range(3):
|
||||
cell = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code=f"cell-{index}",
|
||||
chunk_size_sqm=900,
|
||||
geometry={
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[51.3890 + (index * 0.0001), 35.6890],
|
||||
[51.3891 + (index * 0.0001), 35.6890],
|
||||
[51.3891 + (index * 0.0001), 35.6891],
|
||||
[51.3890 + (index * 0.0001), 35.6891],
|
||||
[51.3890 + (index * 0.0001), 35.6890],
|
||||
]],
|
||||
},
|
||||
centroid_lat="35.689200",
|
||||
centroid_lon=f"{51.3892 + (index * 0.0001):.6f}",
|
||||
)
|
||||
observations.append(
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=cell,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.2 + index,
|
||||
)
|
||||
)
|
||||
|
||||
cluster_summaries = build_cluster_summaries(
|
||||
observations=observations,
|
||||
labels=[0, 0, 0],
|
||||
)
|
||||
|
||||
self.assertEqual(cluster_summaries[0]["center_cell_code"], "cell-1")
|
||||
self.assertEqual(cluster_summaries[0]["center_cell_lat"], 35.6892)
|
||||
self.assertEqual(cluster_summaries[0]["center_cell_lon"], 51.3893)
|
||||
|
||||
def test_build_observation_label_uses_numeric_index_for_30m_cells(self):
|
||||
cell = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code="cell-arbitrary-name",
|
||||
chunk_size_sqm=900,
|
||||
geometry=self.boundary,
|
||||
centroid_lat="35.689200",
|
||||
centroid_lon="51.389200",
|
||||
)
|
||||
observation = AnalysisGridObservation.objects.create(
|
||||
cell=cell,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.5,
|
||||
)
|
||||
|
||||
self.assertEqual(_build_observation_label(observation=observation, index=0), "1")
|
||||
self.assertEqual(_build_observation_label(observation=observation, index=7), "8")
|
||||
|
||||
@patch("location_data.data_driven_subdivision.run_kmeans_labels", return_value=[0, 1, 1])
|
||||
@patch("location_data.data_driven_subdivision.choose_cluster_count", return_value=(2, []))
|
||||
@patch("location_data.data_driven_subdivision.build_clustering_dataset")
|
||||
@patch("location_data.data_driven_subdivision._persist_remote_sensing_diagnostic_artifacts", return_value={})
|
||||
@patch("location_data.data_driven_subdivision.render_elbow_plot", return_value=None)
|
||||
def test_create_remote_sensing_subdivision_result_persists_cluster_blocks_with_geometry(
|
||||
self,
|
||||
_mock_plot,
|
||||
_mock_artifacts,
|
||||
mock_build_dataset,
|
||||
_mock_choose_k,
|
||||
_mock_run_kmeans,
|
||||
):
|
||||
cells = [
|
||||
AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code=f"cell-{index}",
|
||||
chunk_size_sqm=900,
|
||||
geometry={
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[51.3890 + (index * 0.0001), 35.6890],
|
||||
[51.3891 + (index * 0.0001), 35.6890],
|
||||
[51.3891 + (index * 0.0001), 35.6891],
|
||||
[51.3890 + (index * 0.0001), 35.6891],
|
||||
[51.3890 + (index * 0.0001), 35.6890],
|
||||
]],
|
||||
},
|
||||
centroid_lat=f"{35.6892 + (index * 0.0001):.6f}",
|
||||
centroid_lon=f"{51.3892 + (index * 0.0001):.6f}",
|
||||
)
|
||||
for index in range(3)
|
||||
]
|
||||
observations = [
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=cell,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.2 + (index * 0.3),
|
||||
ndwi=0.1 + (index * 0.2),
|
||||
soil_vv_db=-8.0 + index,
|
||||
)
|
||||
for index, cell in enumerate(cells)
|
||||
]
|
||||
mock_build_dataset.return_value = ClusteringDataset(
|
||||
observations=observations,
|
||||
selected_features=["ndvi", "ndwi", "soil_vv_db"],
|
||||
raw_feature_rows=[[0.2, 0.1, -8.0], [0.5, 0.3, -7.0], [0.8, 0.5, -6.0]],
|
||||
raw_feature_maps=[
|
||||
{"ndvi": 0.2, "ndwi": 0.1, "soil_vv_db": -8.0},
|
||||
{"ndvi": 0.5, "ndwi": 0.3, "soil_vv_db": -7.0},
|
||||
{"ndvi": 0.8, "ndwi": 0.5, "soil_vv_db": -6.0},
|
||||
],
|
||||
skipped_cell_codes=[],
|
||||
used_cell_codes=[cell.cell_code for cell in cells],
|
||||
imputed_matrix=[[0.2, 0.1, -8.0], [0.5, 0.3, -7.0], [0.8, 0.5, -6.0]],
|
||||
scaled_matrix=[[-1.0, -1.0, -1.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]],
|
||||
imputer_statistics={"ndvi": 0.5, "ndwi": 0.3, "soil_vv_db": -7.0},
|
||||
scaler_means={"ndvi": 0.5, "ndwi": 0.3, "soil_vv_db": -7.0},
|
||||
scaler_scales={"ndvi": 0.1, "ndwi": 0.1, "soil_vv_db": 1.0},
|
||||
missing_value_counts={"ndvi": 0, "ndwi": 0, "soil_vv_db": 0},
|
||||
skipped_reasons={"all_features_missing": []},
|
||||
)
|
||||
|
||||
result = create_remote_sensing_subdivision_result(
|
||||
location=self.location,
|
||||
run=self.run,
|
||||
observations=observations,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
selected_features=["ndvi", "ndwi", "soil_vv_db"],
|
||||
explicit_k=2,
|
||||
)
|
||||
|
||||
self.assertEqual(_mock_artifacts.call_count, 4)
|
||||
requested_ks = sorted(
|
||||
{
|
||||
call.kwargs.get("requested_k")
|
||||
for call in _mock_artifacts.call_args_list
|
||||
if call.kwargs.get("requested_k") is not None
|
||||
}
|
||||
)
|
||||
self.assertEqual(requested_ks, [1, 2, 3])
|
||||
|
||||
cluster_blocks = list(result.cluster_blocks.order_by("cluster_label"))
|
||||
self.assertEqual(len(cluster_blocks), 2)
|
||||
self.assertTrue(all(cluster_block.uuid for cluster_block in cluster_blocks))
|
||||
self.assertTrue(all(cluster_block.geometry for cluster_block in cluster_blocks))
|
||||
self.assertEqual(
|
||||
sum(cluster_block.cell_count for cluster_block in cluster_blocks),
|
||||
3,
|
||||
)
|
||||
self.assertEqual(RemoteSensingClusterBlock.objects.filter(result=result).count(), 2)
|
||||
|
||||
result.refresh_from_db()
|
||||
cluster_summaries = result.metadata["cluster_summaries"]
|
||||
self.assertTrue(all(summary.get("cluster_uuid") for summary in cluster_summaries))
|
||||
self.assertTrue(all(summary.get("geometry") for summary in cluster_summaries))
|
||||
self.assertEqual(cluster_summaries[0]["center_cell_code"], "cell-0")
|
||||
self.assertEqual(cluster_summaries[1]["center_cell_code"], "cell-1")
|
||||
self.assertEqual(result.cluster_count, 2)
|
||||
self.assertEqual(
|
||||
result.metadata["spatial_constraint"]["final_cluster_count"],
|
||||
2,
|
||||
)
|
||||
self.assertEqual(
|
||||
list(
|
||||
RemoteSensingSubdivisionOption.objects.filter(result=result)
|
||||
.order_by("requested_k")
|
||||
.values_list("requested_k", flat=True)
|
||||
),
|
||||
[1, 2, 3],
|
||||
)
|
||||
self.assertEqual(result.options.filter(is_active=True).get().requested_k, 2)
|
||||
self.assertEqual(result.options.filter(is_recommended=True).get().requested_k, 2)
|
||||
|
||||
self.subdivision.refresh_from_db()
|
||||
self.assertTrue(all(point.get("cluster_uuid") for point in self.subdivision.centroid_points))
|
||||
self.assertEqual(self.subdivision.centroid_points[1]["center_cell_code"], "cell-1")
|
||||
|
||||
self.location.refresh_from_db()
|
||||
block_layout = self.location.block_layout["blocks"][0]
|
||||
self.assertTrue(all(block.get("cluster_uuid") for block in block_layout["sub_blocks"]))
|
||||
self.assertEqual(block_layout["sub_blocks"][1]["center_cell_code"], "cell-1")
|
||||
self.assertEqual(cluster_blocks[1].geometry["type"], "Polygon")
|
||||
self.assertEqual(cluster_blocks[1].center_cell_code, "cell-1")
|
||||
|
||||
def test_enforce_spatial_contiguity_merges_diagonal_island_into_adjacent_cluster(self):
|
||||
cell_payloads = [
|
||||
("cell-00", [[51.3890, 35.6890], [51.3891, 35.6890], [51.3891, 35.6891], [51.3890, 35.6891], [51.3890, 35.6890]]),
|
||||
("cell-01", [[51.3891, 35.6890], [51.3892, 35.6890], [51.3892, 35.6891], [51.3891, 35.6891], [51.3891, 35.6890]]),
|
||||
("cell-10", [[51.3890, 35.6891], [51.3891, 35.6891], [51.3891, 35.6892], [51.3890, 35.6892], [51.3890, 35.6891]]),
|
||||
("cell-11", [[51.3891, 35.6891], [51.3892, 35.6891], [51.3892, 35.6892], [51.3891, 35.6892], [51.3891, 35.6891]]),
|
||||
]
|
||||
observations = []
|
||||
for index, (cell_code, ring) in enumerate(cell_payloads):
|
||||
cell = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code=cell_code,
|
||||
chunk_size_sqm=900,
|
||||
geometry={"type": "Polygon", "coordinates": [ring]},
|
||||
centroid_lat=f"{35.68905 + (index // 2) * 0.0001:.6f}",
|
||||
centroid_lon=f"{51.38905 + (index % 2) * 0.0001:.6f}",
|
||||
)
|
||||
observations.append(
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=cell,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.1 + index,
|
||||
)
|
||||
)
|
||||
|
||||
labels, metadata = enforce_spatial_contiguity(
|
||||
observations=observations,
|
||||
labels=[0, 1, 1, 0],
|
||||
scaled_matrix=[
|
||||
[0.0, 0.0, 0.0],
|
||||
[1.0, 1.0, 1.0],
|
||||
[1.1, 1.1, 1.1],
|
||||
[0.1, 0.1, 0.1],
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(labels, [0, 1, 1, 1])
|
||||
self.assertTrue(metadata["applied"])
|
||||
self.assertEqual(metadata["disconnected_components_merged"], 1)
|
||||
|
||||
def test_build_cluster_geometry_returns_single_polygon_for_adjacent_cells(self):
|
||||
left_cell = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code="cell-left",
|
||||
chunk_size_sqm=900,
|
||||
geometry={
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[51.3890, 35.6890],
|
||||
[51.3891, 35.6890],
|
||||
[51.3891, 35.6891],
|
||||
[51.3890, 35.6891],
|
||||
[51.3890, 35.6890],
|
||||
]],
|
||||
},
|
||||
centroid_lat="35.689050",
|
||||
centroid_lon="51.389050",
|
||||
)
|
||||
right_cell = AnalysisGridCell.objects.create(
|
||||
soil_location=self.location,
|
||||
block_subdivision=self.subdivision,
|
||||
block_code="block-1",
|
||||
cell_code="cell-right",
|
||||
chunk_size_sqm=900,
|
||||
geometry={
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[51.3891, 35.6890],
|
||||
[51.3892, 35.6890],
|
||||
[51.3892, 35.6891],
|
||||
[51.3891, 35.6891],
|
||||
[51.3891, 35.6890],
|
||||
]],
|
||||
},
|
||||
centroid_lat="35.689050",
|
||||
centroid_lon="51.389150",
|
||||
)
|
||||
observations = [
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=left_cell,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.4,
|
||||
),
|
||||
AnalysisGridObservation.objects.create(
|
||||
cell=right_cell,
|
||||
run=self.run,
|
||||
temporal_start=date(2025, 1, 1),
|
||||
temporal_end=date(2025, 1, 31),
|
||||
ndvi=0.5,
|
||||
),
|
||||
]
|
||||
|
||||
geometry = _build_cluster_geometry(observations)
|
||||
|
||||
self.assertEqual(geometry["type"], "Polygon")
|
||||
self.assertEqual(len(geometry["coordinates"][0]), 7)
|
||||
|
||||
Reference in New Issue
Block a user