# راهنمای کامل `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 نشان بدهد.
