Files
Ai/docs/location_data_current_workflow.md
2026-05-09 16:55:06 +03:30

686 lines
19 KiB
Markdown

# مستند کامل عملکرد فعلی `location_data`
این فایل شرح می‌دهد که اپ `location_data` در وضعیت فعلی دقیقاً چه کاری انجام می‌دهد، چه مدل‌هایی دارد، جریان درخواست‌ها چگونه است، منطق تقسیم‌بندی بلوک‌ها چگونه اجرا می‌شود و چه بخش‌هایی فقط داده ذخیره‌شده را برمی‌گردانند.
---
## 1) هدف فعلی اپ `location_data`
اپ `location_data` در وضعیت فعلی چند مسئولیت اصلی دارد:
- نگه‌داری موقعیت جغرافیایی زمین با `lat` و `lon`
- نگه‌داری مرز زمین یا بلوک در `farm_boundary`
- نگه‌داری ساختار بلوک‌های اصلی زمین در `block_layout`
- نگه‌داری نتیجه خردسازی هوشمند هر بلوک در مدل `BlockSubdivision`
- تولید نقاط شبکه‌ای 100 متری یا هر اندازه‌ای که با `SUBDIVISION_CHUNK_SQM` تنظیم شود
- اجرای خوشه‌بندی `KMeans` روی نقاط شبکه‌ای
- پیدا کردن تعداد بهینه خوشه‌ها با روش `Elbow`
- ذخیره centroidهای نهایی هر بخش خردشده
- تولید و ذخیره تصویر نمودار `K-SSE` برای هر subdivision
- نگه‌داری داده‌های خاک در `SoilDepthData`
- نگه‌داری داده‌های NDVI در `NdviObservation`
نکته مهم:
- در فاز فعلی، `GET` هیچ پردازش جدیدی انجام نمی‌دهد.
- تمام پردازش subdivision فقط در زمان `POST` و فقط اگر subdivision آن بلوک قبلاً ساخته نشده باشد اجرا می‌شود.
---
## 2) تنظیمات محیطی
### `SUBDIVISION_CHUNK_SQM`
در `config/settings.py` یک متغیر جدید اضافه شده است:
- `SUBDIVISION_CHUNK_SQM`
- مقدار پیش‌فرض: `100`
- واحد: متر مربع
کاربرد:
- تعیین می‌کند شبکه اولیه برای subdivision با چه اندازه‌ای ساخته شود.
- اگر مقدار `100` باشد، هر chunk تقریباً یک سلول `10m x 10m` خواهد بود، چون:
```text
step = sqrt(100) = 10 meters
```
این مقدار از `.env` یا environment خوانده می‌شود:
```env
SUBDIVISION_CHUNK_SQM=100
```
---
## 3) مدل‌های اصلی اپ
## 3.1) `SoilLocation`
این مدل رکورد اصلی location را نگه می‌دارد.
### فیلدها
- `latitude`
- `longitude`
- `task_id`
- `farm_boundary`
- `input_block_count`
- `block_layout`
- `created_at`
- `updated_at`
### نقش
- هر location با ترکیب `latitude + longitude` یکتا است.
- اطلاعات کلی زمین یا مرکز زمین را نگه می‌دارد.
- اگر هنوز هیچ تقسیم‌بندی انجام نشده باشد، ساختار اولیه بلوک‌ها را در `block_layout` نگه می‌دارد.
### `block_layout`
این فیلد JSON ساختار بلوک‌ها را نگه می‌دارد. نمونه ساده:
```json
{
"input_block_count": 1,
"default_full_farm": true,
"algorithm_status": "completed",
"blocks": [
{
"block_code": "block-1",
"order": 1,
"source": "default",
"needs_subdivision": true,
"sub_blocks": [
{
"sub_block_code": "sub-block-1",
"centroid_lat": 35.689123,
"centroid_lon": 51.389456
}
],
"subdivision_summary": {
"chunk_size_sqm": 100,
"grid_point_count": 24,
"centroid_count": 3,
"optimal_k": 3
}
}
]
}
```
### رفتار مهم
- اگر `block_layout` خالی باشد، به صورت پیش‌فرض با یک بلوک کامل ساخته می‌شود.
- متد `set_input_block_count()` ساختار اولیه بلوک‌های اصلی را می‌سازد.
---
## 3.2) `BlockSubdivision`
این مدل نتیجه واقعی subdivision برای هر بلوک را ذخیره می‌کند.
### فیلدها
- `soil_location`: ارتباط با `SoilLocation`
- `block_code`: شناسه بلوکی که subdivision روی آن اجرا شده
- `source_boundary`: مرز همان بلوک
- `chunk_size_sqm`: اندازه هر chunk
- `grid_points`: نقاط اولیه شبکه
- `centroid_points`: centroidهای نهایی خوشه‌ها
- `grid_point_count`: تعداد نقاط اولیه
- `centroid_count`: تعداد centroidهای نهایی
- `elbow_plot`: تصویر نمودار elbow
- `status`: وضعیت رکورد
- `metadata`: داده تکمیلی مانند `optimal_k` و `inertia_curve`
- `created_at`
- `updated_at`
### نقش
این مدل منبع اصلی داده subdivision است.
یعنی:
- نقاط خام شبکه در این مدل ذخیره می‌شوند
- centroidهای نهایی هم در این مدل ذخیره می‌شوند
- نمودار elbow هم در همین مدل ذخیره می‌شود
### قید یکتا
برای هر location و هر `block_code` فقط یک subdivision وجود دارد:
```text
(soil_location, block_code) unique
```
بنابراین اگر برای یک بلوک قبلاً subdivision ساخته شده باشد، دوباره ایجاد نمی‌شود.
---
## 3.3) `SoilDepthData`
این مدل داده‌های خاک برای عمق‌های مختلف را نگه می‌دارد.
عمق‌های فعلی:
- `0-5cm`
- `5-15cm`
- `15-30cm`
این بخش در حال حاضر مستقل از subdivision است و هنوز برای هر sub-block جداگانه داده خاک تولید نمی‌کند.
---
## 3.4) `NdviObservation`
این مدل داده‌های NDVI و سلامت پوشش گیاهی را نگه می‌دارد.
این بخش هم فعلاً مستقل از منطق subdivision است.
---
## 4) فایل `block_subdivision.py`
فایل `location_data/block_subdivision.py` مرکز اصلی منطق هوشمند subdivision است.
### وظایف اصلی این فایل
- استخراج polygon از ورودی
- تبدیل مختصات جغرافیایی به صفحه محلی متری
- ساخت grid points با اندازه chunk مشخص
- اجرای `KMeans` برای `K=1..10`
- ذخیره `SSE` یا همان `Inertia`
- پیدا کردن elbow point
- ساخت centroidهای نهایی خوشه‌ها
- sync کردن نتیجه با `block_layout`
- تولید تصویر نمودار elbow
- ذخیره تصویر در مدل با `ContentFile`
---
## 5) روند هندسی subdivision
## 5.1) استخراج Polygon
ورودی boundary می‌تواند به چند شکل بیاید:
- GeoJSON Polygon
- `corners`
- آرایه مستقیم از نقاط
تابع `extract_polygon()` این ورودی را به لیستی از نقاط جغرافیایی تبدیل می‌کند.
نمونه ورودی معتبر:
```json
{
"type": "Polygon",
"coordinates": [
[
[51.3890, 35.6890],
[51.3902, 35.6890],
[51.3902, 35.6900],
[51.3890, 35.6900],
[51.3890, 35.6890]
]
]
}
```
---
## 5.2) تبدیل مختصات به فضای محلی متری
برای اینکه بتوانیم فاصله‌ها و گریدبندی را بر اساس متر حساب کنیم، polygon از مختصات جغرافیایی به مختصات محلی متری تبدیل می‌شود.
تابع مربوط:
- `project_polygon_to_local_meters()`
ویژگی این تبدیل:
- نقطه اول polygon به عنوان origin در نظر گرفته می‌شود
- با تقریب محلی، `lat/lon` به `x/y` در واحد متر تبدیل می‌شوند
این تبدیل برای subdivision کوچک و محلی مناسب است.
---
## 5.3) تولید grid points
تابع:
- `generate_grid_points()`
منطق:
1. ابتدا اندازه گام محاسبه می‌شود:
```text
step_m = sqrt(chunk_size_sqm)
```
2. روی bounding box polygon، نقاط مرکزی grid بررسی می‌شوند.
3. هر نقطه‌ای که داخل polygon باشد نگه داشته می‌شود.
خروجی:
- `grid_points`: مختصات جغرافیایی قابل ذخیره در JSON
- `grid_vectors`: مختصات محلی متری برای ورود به `KMeans`
نمونه هر grid point:
```json
{
"point_code": "pt-1",
"lat": 35.689123,
"lon": 51.389456
}
```
---
## 6) الگوریتم خوشه‌بندی هوشمند
## 6.1) اجرای `KMeans`
تابع:
- `cluster_grid_points()`
منطق:
- روی `grid_vectors` خوشه‌بندی انجام می‌شود
- برای `K=1` تا `K=10` اجرا می‌شود
- اگر تعداد نقاط کمتر از 10 باشد، `max_k = len(grid_vectors)` در نظر گرفته می‌شود
برای هر `K`:
- مدل `KMeans` ساخته می‌شود
- `fit()` اجرا می‌شود
- مقدار `model.inertia_` به عنوان `SSE` ذخیره می‌شود
خروجی میانی:
```json
[
{"k": 1, "sse": 1300.5},
{"k": 2, "sse": 640.2},
{"k": 3, "sse": 390.1}
]
```
---
## 6.2) پیدا کردن Elbow Point
تابع:
- `detect_elbow_point()`
منطق فعلی:
1. از روی SSEها، شیب افت بین نقاط متوالی محاسبه می‌شود.
2. سپس تغییرات شیب محاسبه می‌شود.
3. هر جایی که افت شیب ناگهان متوقف شود، همان نقطه elbow در نظر گرفته می‌شود.
یعنی در عمل:
- ابتدا `slopes` محاسبه می‌شود
- سپس اختلاف شیب‌ها بررسی می‌شود
- بیشترین تغییر شیب به عنوان elbow انتخاب می‌شود
خروجی:
- `optimal_k`
---
## 6.3) تولید centroidهای نهایی
بعد از پیدا شدن `optimal_k`:
- مدل `KMeans` همان `K` نهایی انتخاب می‌شود
- مختصات مراکز خوشه‌ها (`cluster_centers_`) گرفته می‌شود
- از فضای متری به `lat/lon` تبدیل می‌شود
- در `centroid_points` ذخیره می‌شود
نمونه centroid:
```json
{
"sub_block_code": "sub-block-1",
"centroid_lat": 35.689321,
"centroid_lon": 51.389789
}
```
این centroidها در عمل همان مراکز بخش‌های کوچکتر زمین هستند.
---
## 7) تولید و ذخیره نمودار Elbow
### تابع
- `render_elbow_plot()`
### منطق
پس از محاسبه `inertia_curve` و `optimal_k`:
1. نمودار `K` در برابر `SSE` رسم می‌شود
2. نقطه elbow با رنگ قرمز مشخص می‌شود
3. تصویر به صورت PNG در `BytesIO` ذخیره می‌شود
4. با `ContentFile` به `ImageField` مدل `BlockSubdivision` داده می‌شود
### نکته مهم حافظه
برای جلوگیری از memory leak:
- از backend غیرتعاملی `Agg` استفاده می‌شود
- بعد از ذخیره تصویر، `plt.close(fig)` اجرا می‌شود
- buffer هم بسته می‌شود
این برای پردازش‌های همزمان سرور ضروری است.
---
## 8) جریان کامل `POST /api/soil-data/`
این endpoint الان مهم‌ترین ورودی subdivision است.
### ورودی‌های قابل پشتیبانی
- `lat`
- `lon`
- `block_count`
- `block_code`
- `farm_boundary`
### سناریوی اجرا
#### مرحله 1: اعتبارسنجی ورودی
سریالایزر `SoilDataRequestSerializer` داده را validate می‌کند.
#### مرحله 2: پیدا کردن یا ساخت location
بر اساس `lat/lon`:
- اگر location وجود نداشته باشد ساخته می‌شود
- اگر وجود داشته باشد از همان رکورد استفاده می‌شود
#### مرحله 3: آپدیت ساختار اولیه بلوک‌ها
اگر `block_count` فرق کرده باشد:
- `block_layout` دوباره با `set_input_block_count()` ساخته می‌شود
#### مرحله 4: انتخاب boundary برای subdivision
اولویت:
1. `farm_boundary` ارسالی در request
2. اگر نبود، `location.farm_boundary` ذخیره‌شده
#### مرحله 5: اجرای subdivision فقط در صورت نیاز
تابع:
- `create_or_get_block_subdivision()`
اگر رکورد `(location, block_code)` از قبل وجود داشته باشد:
- هیچ پردازش جدیدی اجرا نمی‌شود
- همان رکورد قبلی برگردانده می‌شود
اگر وجود نداشته باشد:
- grid ساخته می‌شود
- KMeans اجرا می‌شود
- elbow پیدا می‌شود
- centroidها ساخته می‌شوند
- نمودار elbow ساخته می‌شود
- همه چیز در `BlockSubdivision` ذخیره می‌شود
- `block_layout` با `sub_blocks` sync می‌شود
#### مرحله 6: response
خروجی شامل این‌هاست:
- اطلاعات `SoilLocation`
- `farm_boundary`
- `block_layout`
- `block_subdivisions`
- `depths`
فیلد `source` در response:
- `created` اگر location یا subdivision جدید ساخته شده باشد
- `database` اگر قبلاً وجود داشته باشد
---
## 9) جریان کامل `GET /api/soil-data/`
این endpoint الان فقط برای read استفاده می‌شود.
### ورودی
- `lat`
- `lon`
- `block_code` اختیاری
### رفتار
- location را از دیتابیس پیدا می‌کند
- subdivisionهای ذخیره‌شده را می‌خواند
- هیچ الگوریتمی را اجرا نمی‌کند
- هیچ `KMeans` یا پردازش هندسی انجام نمی‌دهد
### پاسخ
داده ذخیره‌شده را با `source = database` برمی‌گرداند.
اگر location پیدا نشود:
- `404`
---
## 10) نقش `serializers.py`
### `SoilDataRequestSerializer`
ورودی endpoint اصلی را مدیریت می‌کند:
- `lat`
- `lon`
- `block_count`
- `block_code`
- `farm_boundary`
### `SoilLocationResponseSerializer`
خروجی location را برمی‌گرداند:
- `id`
- `lat`
- `lon`
- `input_block_count`
- `farm_boundary`
- `block_layout`
- `block_subdivisions`
- `depths`
### `BlockSubdivisionSerializer`
خروجی subdivision را برمی‌گرداند:
- `block_code`
- `chunk_size_sqm`
- `grid_points`
- `centroid_points`
- `grid_point_count`
- `centroid_count`
- `elbow_plot`
- `status`
- `metadata`
- `created_at`
- `updated_at`
---
## 11) نقش `block_layout` در کنار `BlockSubdivision`
در معماری فعلی دو سطح ذخیره‌سازی داریم:
### 11.1) `BlockSubdivision`
منبع اصلی و canonical برای subdivision
### 11.2) `block_layout`
خلاصه‌ای از نتیجه subdivision برای مصرف سریع‌تر در response و ساختار کلی location
یعنی:
- داده دقیق در `BlockSubdivision` است
- خلاصه آن در `block_layout.blocks[].sub_blocks` قرار می‌گیرد
---
## 12) وضعیت فعلی بخش‌های قدیمی‌تر اپ
## 12.1) `tasks.py`
این فایل هنوز وجود دارد و برای fetch داده خاک به صورت قدیمی استفاده می‌شود، اما در مسیر subdivision فعلی نقشی ندارد.
## 12.2) `soil_adapters.py`
این فایل adapterهای داده خاک را نگه می‌دارد و فعلاً برای subdivision استفاده نمی‌شود.
## 12.3) `remote_sensing.py`
منطق سنجش‌ازدور را نگه می‌دارد و هنوز مستقیماً به subdivision وصل نشده است.
## 12.4) `ndvi.py`
برای endpoint مربوط به NDVI استفاده می‌شود و فعلاً از centroidهای subdivision استفاده نمی‌کند.
---
## 13) وابستگی‌های جدید
برای عملکرد فعلی subdivision این dependencyها لازم هستند:
- `scikit-learn`
- `matplotlib`
- `Pillow`
- `numpy`
### دلیل هرکدام
- `scikit-learn`: اجرای `KMeans`
- `matplotlib`: رسم elbow plot
- `Pillow`: پشتیبانی از `ImageField`
- `numpy`: وابستگی پایه `scikit-learn`
---
## 14) migrationهای مهم مرتبط با ساختار فعلی
- `0008_soillocation_block_layout.py`
- اضافه شدن `input_block_count`
- اضافه شدن `block_layout`
- `0009_blocksubdivision.py`
- اضافه شدن مدل `BlockSubdivision`
- `0010_blocksubdivision_elbow_plot.py`
- اضافه شدن فیلد `elbow_plot`
---
## 15) محدودیت‌های فعلی
چند محدودیت مهم در پیاده‌سازی فعلی وجود دارد:
- subdivision فعلاً بر اساس هندسه و خوشه‌بندی نقاط انجام می‌شود، نه بر اساس داده واقعی خاک یا NDVI
- برای هر `block_code` فرض می‌شود یک مرز مستقل از بیرون داده می‌شود
- هنوز برای هر `sub_block` رکورد location مستقل ساخته نمی‌شود
- هنوز داده خاک، هوا و NDVI برای centroidهای جدید به صورت جداگانه fetch نمی‌شود
- elbow detection فعلی heuristic-based است و هنوز نسخه پیشرفته‌تر آماری ندارد
---
## 16) تست‌های مرتبط
### `location_data/test_block_subdivision.py`
این تست‌ها بررسی می‌کنند:
- elbow detection کار می‌کند
- payload subdivision ساخته می‌شود
- grid points و centroid points خروجی دارند
### `location_data/test_soil_api.py`
این تست‌ها بررسی می‌کنند:
- `POST` subdivision جدید می‌سازد
- `GET` فقط داده ذخیره‌شده را برمی‌گرداند
- الگوریتم در `GET` دوباره اجرا نمی‌شود
---
## 17) جمع‌بندی معماری فعلی
در وضعیت فعلی، `location_data` این معماری را دارد:
### لایه 1: Location پایه
- `SoilLocation`
- `farm_boundary`
- `block_layout`
### لایه 2: Subdivision هوشمند
- `BlockSubdivision`
- grid generation
- KMeans
- elbow detection
- centroid generation
- elbow plot generation
### لایه 3: داده‌های مکمل
- `SoilDepthData`
- `NdviObservation`
- بخش‌های legacy مثل `tasks.py`
در نتیجه، اپ الان می‌تواند:
- یک بلوک با مرز مشخص بگیرد
- آن را به نقاط شبکه‌ای خرد کند
- تعداد بهینه بخش‌ها را با KMeans + Elbow پیدا کند
- centroidهای نهایی را ذخیره کند
- نمودار elbow را ذخیره کند
- و در درخواست‌های بعدی فقط همان نتیجه ذخیره‌شده را بدون پردازش مجدد برگرداند
---
## 18) پیشنهاد برای مراحل بعدی
اگر در مرحله بعد بخواهی این ساختار را توسعه بدهی، منطقی‌ترین قدم‌ها این‌ها هستند:
1. ساخت endpoint مستقل برای subdivision هر block
2. اتصال هر centroid به fetch داده خاک و هوا
3. ساخت رکورد مستقل برای هر `sub_block`
4. استفاده از NDVI یا داده سنسور برای تعیین `K` یا وزن‌دهی خوشه‌ها
5. نمایش مستقیم `elbow_plot` با URL کامل media