Files
Logic/Modules/Ai/crop_simulation/SERVICES_INTEGRATION.md
2026-05-11 03:27:21 +03:30

823 lines
22 KiB
Markdown

# راهنمای کامل `crop_simulation/services.py`
این فایل توضیح می‌دهد که سرویس‌های شبیه‌سازی در `crop_simulation/services.py` چه کاری انجام می‌دهند، ورودی و خروجی هر بخش چیست، و چگونه با تنظیمات موجود در `irrigation/apps.py` و `fertilization/apps.py` ارتباط می‌گیرند.
---
## نمای کلی
فایل `crop_simulation/services.py` هسته اجرای سناریوهای شبیه‌سازی محصول در پروژه است. این فایل سه مسئولیت اصلی دارد:
1. نرمال‌سازی ورودی‌ها برای موتور شبیه‌سازی
2. اجرای مدل PCSE/WOFOST
3. ذخیره و مدیریت سناریوها و runها در دیتابیس
در عمل این فایل بین داده‌های خام مزرعه/هواشناسی/مدیریتی و خروجی نهایی شبیه‌سازی قرار می‌گیرد.
---
## ساختار کلی فایل
این فایل را می‌توان به ۴ بخش تقسیم کرد:
1. توابع کمکی برای تبدیل ورودی‌ها
2. کلاس `PcseSimulationManager`
3. کلاس `CropSimulationService`
4. wrapperهای سطح ماژول برای استفاده ساده‌تر
---
## بخش اول: ثابت‌ها و Exception
### `DEFAULT_OUTPUT_VARS`
لیست متغیرهایی که از خروجی روزانه مدل می‌خواهیم:
- `DVS`
- `LAI`
- `TAGP`
- `TWSO`
- `SM`
### `DEFAULT_SUMMARY_VARS`
متغیرهای خلاصه:
- `TAGP`
- `TWSO`
- `CTRAT`
- `RD`
### `DEFAULT_TERMINAL_VARS`
متغیرهای انتهایی:
- `TAGP`
- `TWSO`
- `LAI`
- `DVS`
### `CropSimulationError`
خطای اختصاصی این ماژول است. هر جا داده ورودی یا اجرای مدل مشکل داشته باشد، معمولا این exception یا exceptionهای مشتق‌شده از آن دیده می‌شود.
---
## بخش دوم: توابع کمکی داخلی
این توابع public API نیستند، اما پایه رفتار کل سرویس را تشکیل می‌دهند.
### `_json_ready(value)`
داده‌های Python را برای ذخیره در JSON آماده می‌کند.
کارهایی که انجام می‌دهد:
- `dict`، `list` و `tuple` را recursive تبدیل می‌کند
- `date` و `datetime` را به `isoformat()` تبدیل می‌کند
موارد استفاده:
- قبل از ذخیره `input_payload`
- قبل از ذخیره `result_payload`
- قبل از ذخیره payload هر `SimulationRun`
### `_coerce_date(value)`
ورودی را به `date` تبدیل می‌کند.
ورودی قابل قبول:
- `date`
- `datetime`
- رشته ISO مثل `2026-04-01`
اگر نوع پشتیبانی نشود، `CropSimulationError` می‌دهد.
### `_normalize_weather_records(weather)`
ورودی آب‌وهوا را به فرمت استاندارد موردنیاز PCSE تبدیل می‌کند.
ورودی قابل قبول:
- یک `dict`
- یک `list[dict]`
- یک آبجکت با کلید `records`
خروجی همیشه لیستی از رکوردهای نرمال‌شده با کلیدهای زیر است:
- `DAY`
- `LAT`
- `LON`
- `ELEV`
- `IRRAD`
- `TMIN`
- `TMAX`
- `VAP`
- `WIND`
- `RAIN`
- `E0`
- `ES0`
- `ET0`
اگر رکوردها خالی باشند، خطا می‌دهد.
### `_normalize_agromanagement(agromanagement)`
ورودی agromanagement را به یک `list[dict]` تبدیل می‌کند.
ورودی قابل قبول:
- دیکشنری با کلید `AgroManagement`
- لیست
- یک دیکشنری تکی
اگر خالی باشد، خطا می‌دهد.
### `_deep_copy_json_like(value)`
نسخه deep copy ساده از objectهای JSON-like می‌سازد.
برای جلوگیری از mutation روی ورودی اصلی استفاده می‌شود.
### `_parse_recommendation_events(...)`
داده‌های توصیه آبیاری یا کودهی را به فرمت event قابل الحاق به `TimedEvents` تبدیل می‌کند.
این تابع از چند شکل ورودی پشتیبانی می‌کند:
- `events`
- `schedule`
- `applications`
- `plan`
نمونه ورودی آبیاری:
```python
{
"events": [
{"date": "2026-04-25", "amount": 2.5, "efficiency": 0.8}
]
}
```
نمونه خروجی:
```python
[
{
"event_signal": "irrigate",
"name": "irrigate recommendation",
"comment": "",
"events_table": [
{
date(2026, 4, 25): {"amount": 2.5, "efficiency": 0.8}
}
],
}
]
```
### `_merge_management_recommendations(...)`
مهم‌ترین تابع glue برای اتصال recommendationها به شبیه‌سازی است.
کار این تابع:
1. agromanagement را normalize می‌کند
2. توصیه آبیاری را به eventهای `irrigate` تبدیل می‌کند
3. توصیه کودهی را به eventهای `apply_n` تبدیل می‌کند
4. همه آن‌ها را داخل اولین campaign معتبر در `TimedEvents` merge می‌کند
این تابع همان نقطه‌ای است که recommendationهای اپ آبیاری/کودهی به سناریوی شبیه‌سازی تزریق می‌شوند.
### `_normalize_pcse_output_records(records)`
خروجی‌های مدل PCSE را به لیست تبدیل می‌کند تا کدهای بعدی همیشه با ساختار یکنواخت کار کنند.
### `_pick_first_not_none(*values)`
اولین مقدار non-null را برمی‌گرداند.
برای ساخت metricهای نهایی مثل `yield_estimate` استفاده می‌شود.
### `_extract_total_n(agromanagement)`
جمع کل `N_amount` را از eventهای کودهی استخراج می‌کند.
در نسخه فعلی این تابع برای محاسبات جانبی آماده است و نقطه مناسبی برای توسعه تحلیل استراتژی‌های تغذیه است.
### `_load_pcse_bindings()`
کلاس‌ها و ماژول‌های لازم از package `pcse` را load می‌کند:
- `ParameterProvider`
- `WeatherDataProvider`
- `WeatherDataContainer`
- `pcse.models`
اگر `pcse` نصب نباشد، `None` برمی‌گرداند.
### `_resolve_model_class(bindings, model_name)`
کلاس مدل PCSE را با نامی مثل `Wofost81_NWLP_CWB_CNB` پیدا می‌کند.
---
## `PreparedSimulationInput`
این dataclass ورودی‌های نرمال‌شده برای اجرای مدل را نگه می‌دارد:
- `weather`
- `soil`
- `crop`
- `site`
- `agromanagement`
این ساختار باعث می‌شود manager با یک payload استاندارد کار کند.
---
## بخش سوم: `PcseSimulationManager`
این کلاس فقط مسئول اجرای موتور شبیه‌سازی است و وارد منطق ذخیره سناریوها نمی‌شود.
### `__init__(model_name="Wofost81_NWLP_CWB_CNB")`
مدل PCSE مورد استفاده را مشخص می‌کند.
مدل پیش‌فرض:
```python
Wofost81_NWLP_CWB_CNB
```
### `run_simulation(...)`
ورودی خام می‌گیرد، normalize می‌کند، dependencyهای PCSE را load می‌کند، و شبیه‌سازی را اجرا می‌کند.
پارامترها:
- `weather`
- `soil`
- `crop_parameters`
- `agromanagement`
- `site_parameters`
خروجی:
```python
{
"engine": "pcse",
"model_name": "Wofost81_NWLP_CWB_CNB",
"metrics": {...},
"daily_output": [...],
"summary_output": [...],
"terminal_output": [...]
}
```
### `_run_with_pcse(prepared, bindings)`
اجرای واقعی مدل را انجام می‌دهد.
جریان داخلی:
1. ساخت weather provider سفارشی از روی dictها
2. ساخت `ParameterProvider`
3. ساخت instance مدل PCSE
4. اجرای `run_till_terminate()` یا `run()`
5. گرفتن خروجی‌ها
6. تبدیل خروجی به فرم نهایی
### `_build_result(...)`
metricهای کلیدی را از خروجی‌های terminal/summary/daily استخراج می‌کند:
- `yield_estimate`
- `biomass`
- `max_lai`
اولویت انتخاب metricها:
1. terminal
2. summary
3. آخرین رکورد daily
---
## بخش چهارم: `CropSimulationService`
این کلاس service layer سطح بالاتر است. علاوه بر اجرای مدل، سناریوها و runها را در دیتابیس ذخیره می‌کند.
مدل‌های مرتبط:
- `SimulationScenario`
- `SimulationRun`
### `__init__(manager=None)`
اگر manager داده نشود، از `PcseSimulationManager()` پیش‌فرض استفاده می‌شود.
---
## متدهای public اصلی
### 1) `run_single_simulation(...)`
برای اجرای یک سناریوی تکی.
پارامترها:
- `weather`
- `soil`
- `crop_parameters`
- `agromanagement`
- `site_parameters`
- `irrigation_recommendation`
- `fertilization_recommendation`
- `name`
کارها:
1. merge کردن recommendationها داخل management
2. ساخت `SimulationScenario` با نوع `SINGLE`
3. ساخت `SimulationRun`
4. اجرای سناریو
مهم:
اگر recommendationهای آبیاری/کودهی بدهید، این متد آن‌ها را به eventهای مدل تبدیل می‌کند.
نمونه:
```python
from crop_simulation.services import run_single_simulation
result = run_single_simulation(
weather=weather_payload,
soil={"SMFCF": 0.34, "SMW": 0.12, "RDMSOL": 120.0},
crop_parameters={"crop_name": "wheat", "TSUM1": 800},
agromanagement=agromanagement_payload,
site_parameters={"WAV": 40.0},
irrigation_recommendation={
"events": [
{"date": "2026-04-25", "amount": 2.5, "efficiency": 0.8}
]
},
)
```
### 2) `compare_crops(...)`
برای مقایسه دو محصول.
ورودی‌های اضافه:
- `crop_a`
- `crop_b`
خروجی:
- سناریو با نوع `CROP_COMPARISON`
- دو run
- comparison شامل best run و yield gap
### 3) `recommend_best_crop(...)`
برای مقایسه چند محصول و انتخاب بهترین گزینه.
ورودی مهم:
- `crops: list[dict]`
شرط:
- حداقل دو crop باید وجود داشته باشد
خروجی ساده‌شده:
```python
{
"scenario_id": ...,
"scenario_type": "crop_comparison",
"recommended_crop": {
"run_key": "...",
"label": "...",
"expected_yield_estimate": ...
},
"candidates": [...],
"raw_result": {...}
}
```
### 4) `compare_fertilization_strategies(...)`
برای مقایسه چند strategy کودهی روی یک crop ثابت.
ورودی ویژه:
```python
strategies = [
{
"label": "base",
"agromanagement": [...]
},
{
"label": "high_n",
"agromanagement": [...]
}
]
```
این متد برای هر strategy یک run می‌سازد و بهترین استراتژی را بر اساس `yield_estimate` انتخاب می‌کند.
### 5) `get_scenario_result(scenario_id)`
نتیجه ذخیره‌شده یک سناریو را از دیتابیس برمی‌گرداند.
خروجی شامل:
- اطلاعات scenario
- اطلاعات همه runها
- status
- input payload
- result payload
- error message
---
## متدهای داخلی مهم در `CropSimulationService`
### `_execute_scenario(...)`
قلب اجرای سناریو است.
جریان:
1. status سناریو را `RUNNING` می‌کند
2. تک‌تک runها را اجرا می‌کند
3. خروجی هر run را ذخیره می‌کند
4. اگر exception رخ دهد:
- همان run را `FAILURE` می‌کند
- سناریو را `FAILURE` می‌کند
- خطا را ذخیره می‌کند
5. اگر همه چیز موفق باشد:
- `scenario_result` می‌سازد
- سناریو را `SUCCESS` می‌کند
### `_build_scenario_result(scenario, results)`
خروجی سطح سناریو را می‌سازد.
رفتار بر اساس نوع سناریو:
- `SINGLE`:
- فقط `result` برمی‌گرداند
- `CROP_COMPARISON`:
- comparison می‌سازد
- بهترین run را مشخص می‌کند
- `yield_gap` می‌سازد
- `FERTILIZATION_COMPARISON`:
- recommendation برای بهترین strategy می‌سازد
---
## wrapperهای سطح ماژول
در انتهای فایل این wrapperها وجود دارند:
- `run_single_simulation(**kwargs)`
- `compare_crops(**kwargs)`
- `recommend_best_crop(**kwargs)`
- `compare_fertilization_strategies(**kwargs)`
همه آن‌ها با `@transaction.atomic` تزئین شده‌اند.
یعنی اگر بخواهید ساده از بیرون صدا بزنید، لازم نیست خودتان instance بسازید:
```python
from crop_simulation.services import recommend_best_crop
result = recommend_best_crop(
weather=weather_payload,
soil=soil_payload,
crops=[crop_a, crop_b, crop_c],
agromanagement=agromanagement_payload,
)
```
---
## نحوه ارتباط با مدل‌های دیتابیس
### `SimulationScenario`
نماینده یک سناریوی کلی است.
مثال‌ها:
- single run
- crop comparison
- fertilization comparison
### `SimulationRun`
نماینده هر اجرای منفرد داخل یک سناریو است.
مثلا در `compare_crops`:
- یک `SimulationScenario`
- دو `SimulationRun`
---
## ارتباط `crop_simulation/services.py` با `crop_simulation/apps.py`
فایل `crop_simulation/apps.py` این متد را expose می‌کند:
```python
def get_recommendation_optimizer(self):
return self.recommendation_optimizer
```
این optimizer در فایل `crop_simulation/recommendation_optimizer.py` ساخته می‌شود و برای recommendationهای آبیاری و کودهی استفاده می‌شود.
نکته مهم:
- `services.py` موتور اجرای سناریوهاست
- `recommendation_optimizer.py` روی همین موتور سناریوهای candidate می‌سازد
- `apps.py` فقط نقطه دسترسی مرکزی به optimizer است
یعنی:
```python
optimizer = apps.get_app_config("crop_simulation").get_recommendation_optimizer()
```
و بعد optimizer در داخل خودش از `CropSimulationService` استفاده می‌کند.
---
## ارتباط با `irrigation/apps.py`
فایل `irrigation/apps.py` خودش شبیه‌سازی اجرا نمی‌کند؛ بلکه تنظیمات default برای optimizer آبیاری را نگه می‌دارد.
### فیلدهای مهم
#### `tone_file`
مسیر tone مربوط به LLM:
```python
config/tones/irrigation_tone.txt
```
#### `optimizer_defaults`
این property تنظیمات پایه بهینه‌سازی آبیاری را برمی‌گرداند:
- `validity_days`
- `minimum_event_mm`
- `significant_rain_threshold_mm`
- `stage_targets`
- `strategy_profiles`
### `stage_targets`
هدف رطوبت یا رفتار پایه برای stageهای مختلف:
- `initial`
- `vegetative`
- `flowering`
- `fruiting`
### `strategy_profiles`
سه سناریوی پایه برای optimizer:
- `conservative`
- `balanced`
- `protective`
هر سناریو مشخص می‌کند:
- ضریب آب (`multiplier`)
- ضریب تعداد دفعات (`frequency_factor`)
- تعداد event پایه (`event_count`)
### نحوه استفاده در کد
در optimizer آبیاری معمولا به شکل زیر خوانده می‌شود:
```python
defaults = apps.get_app_config("irrigation").get_optimizer_defaults()
```
سپس این defaults به سناریوهای recommendation تبدیل می‌شوند و در صورت نیاز به `run_single_simulation()` پاس داده می‌شوند.
### نقش آن در ارتباط با `services.py`
ارتباط غیرمستقیم است:
1. `irrigation/apps.py` تنظیمات baseline را می‌دهد
2. optimizer با این تنظیمات candidate strategy می‌سازد
3. strategyها به recommendation event تبدیل می‌شوند
4. `crop_simulation/services.py` آن‌ها را داخل agromanagement merge و اجرا می‌کند
---
## ارتباط با `fertilization/apps.py`
این فایل مشابه irrigation است اما برای منطق کودهی.
### `tone_file`
```python
config/tones/fertilization_tone.txt
```
### `optimizer_defaults`
این تنظیمات را می‌دهد:
- `validity_days`
- `rain_delay_threshold_mm`
- `stage_targets`
- `strategy_profiles`
### `stage_targets`
برای هر stage اطلاعات زیر مشخص می‌شود:
- `n`
- `p`
- `k`
- `formula`
- `application_method`
- `timing`
### `strategy_profiles`
سناریوهای پایه:
- `maintenance`
- `balanced`
- `corrective`
هرکدام مشخص می‌کنند:
- ضریب مصرف (`multiplier`)
- focus تغذیه‌ای
- روش مصرف
- override فرمول در صورت نیاز
### نحوه استفاده در کد
```python
defaults = apps.get_app_config("fertilization").get_optimizer_defaults()
```
سپس optimizer با این defaults چند strategy می‌سازد. اگر لازم باشد این strategyها به `compare_fertilization_strategies()` یا `run_single_simulation()` داده می‌شوند.
### ارتباط آن با `services.py`
ارتباط باز هم غیرمستقیم است:
1. `fertilization/apps.py` پروفایل stage و strategy را می‌دهد
2. optimizer از روی آن strategy تولید می‌کند
3. strategy به eventهای `apply_n` تبدیل می‌شود
4. `services.py` این eventها را داخل agromanagement merge می‌کند
5. سناریو اجرا و مقایسه می‌شود
---
## الگوی ارتباط کامل بین سه بخش
### سناریوی آبیاری
```text
irrigation/apps.py
-> optimizer_defaults
-> recommendation optimizer
-> irrigation recommendation events
-> crop_simulation/services.py:_merge_management_recommendations()
-> run_single_simulation()
-> PCSE run
-> scenario/run result
```
### سناریوی کودهی
```text
fertilization/apps.py
-> optimizer_defaults
-> recommendation optimizer
-> fertilization recommendation events
-> crop_simulation/services.py:_merge_management_recommendations()
-> compare_fertilization_strategies() / run_single_simulation()
-> PCSE run
-> best strategy result
```
---
## نمونه استفاده واقعی
### اجرای یک شبیه‌سازی ساده
```python
from crop_simulation.services import run_single_simulation
result = run_single_simulation(
weather=[
{
"DAY": "2026-04-01",
"LAT": 35.7,
"LON": 51.4,
"ELEV": 1200,
"IRRAD": 16000000,
"TMIN": 11,
"TMAX": 22,
"VAP": 12,
"WIND": 2.4,
"RAIN": 0.8,
"E0": 0.35,
"ES0": 0.3,
"ET0": 0.32,
}
],
soil={"SMFCF": 0.34, "SMW": 0.12, "RDMSOL": 120.0},
crop_parameters={"crop_name": "wheat", "TSUM1": 800, "YIELD_SCALE": 1.0},
agromanagement=[
{
"2026-04-01": {
"CropCalendar": {
"crop_name": "wheat",
"variety_name": "winter-wheat",
"crop_start_date": "2026-04-05",
"crop_start_type": "sowing",
"crop_end_date": "2026-09-01",
"crop_end_type": "harvest",
"max_duration": 180,
},
"TimedEvents": [],
"StateEvents": [],
}
}
],
site_parameters={"WAV": 40.0},
)
```
### مقایسه دو محصول
```python
from crop_simulation.services import compare_crops
result = compare_crops(
weather=weather_payload,
soil=soil_payload,
crop_a={"crop_name": "wheat", "TSUM1": 800},
crop_b={"crop_name": "maize", "TSUM1": 900},
agromanagement=agromanagement_payload,
site_parameters={"WAV": 40.0},
)
```
### مقایسه strategyهای کودهی
```python
from crop_simulation.services import compare_fertilization_strategies
result = compare_fertilization_strategies(
weather=weather_payload,
soil=soil_payload,
crop_parameters={"crop_name": "wheat", "TSUM1": 800},
strategies=[
{"label": "base", "agromanagement": agm_base},
{"label": "high_n", "agromanagement": agm_high_n},
],
site_parameters={"WAV": 40.0},
)
```
---
## نکات مهم توسعه
### 1. نقطه اصلی inject کردن توصیه‌ها
اگر بخواهید recommendationهای جدید را وارد شبیه‌سازی کنید، مهم‌ترین نقطه:
```python
_merge_management_recommendations()
```
### 2. نقطه اصلی اجرای موتور
اگر بخواهید backend engine عوض شود یا مدل جدید اضافه شود:
```python
PcseSimulationManager.run_simulation()
```
### 3. نقطه اصلی مدیریت lifecycle سناریو
اگر بخواهید queueing، logging یا audit بیشتری اضافه کنید:
```python
CropSimulationService._execute_scenario()
```
### 4. ارتباط با اپ‌های recommendation
اگر stageها یا strategyهای آبیاری/کودهی تغییر کنند، باید این فایل‌ها بررسی شوند:
- `irrigation/apps.py`
- `fertilization/apps.py`
چون optimizer از آن‌ها defaultها را می‌خواند.
---
## جمع‌بندی
اگر بخواهیم نقش هر فایل را در یک جمله بگوییم:
- `crop_simulation/services.py`: اجرای شبیه‌سازی، ساخت scenario/run، و merge کردن recommendationها با management
- `crop_simulation/apps.py`: نقطه دسترسی مرکزی به optimizer
- `irrigation/apps.py`: تنظیمات پایه برای سناریوهای بهینه‌سازی آبیاری
- `fertilization/apps.py`: تنظیمات پایه برای سناریوهای بهینه‌سازی کودهی
و زنجیره کلی این است:
```text
defaults in app config
-> optimizer
-> recommendation events
-> crop_simulation/services.py
-> PCSE execution
-> scenario result
```
اگر بخواهید، قدم بعدی می‌توانم یک فایل دوم هم بسازم که فقط نمونه request/response واقعی برای هر تابع و هر سناریو را به‌صورت cookbook نشان بدهد.