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

19 KiB

مستند کامل عملکرد فعلی 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 خواهد بود، چون:
step = sqrt(100) = 10 meters

این مقدار از .env یا environment خوانده می‌شود:

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 ساختار بلوک‌ها را نگه می‌دارد. نمونه ساده:

{
  "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 وجود دارد:

(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() این ورودی را به لیستی از نقاط جغرافیایی تبدیل می‌کند.

نمونه ورودی معتبر:

{
  "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. ابتدا اندازه گام محاسبه می‌شود:
step_m = sqrt(chunk_size_sqm)
  1. روی bounding box polygon، نقاط مرکزی grid بررسی می‌شوند.
  2. هر نقطه‌ای که داخل polygon باشد نگه داشته می‌شود.

خروجی:

  • grid_points: مختصات جغرافیایی قابل ذخیره در JSON
  • grid_vectors: مختصات محلی متری برای ورود به KMeans

نمونه هر grid point:

{
  "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 ذخیره می‌شود

خروجی میانی:

[
  {"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:

{
  "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