2026-04-24 18:34:17 +03:30
# راهنمای کامل `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)`
2026-04-24 22:20:15 +03:30
کلاس مدل PCSE را با نامی مثل `Wofost81_NWLP_CWB_CNB` پیدا میکند.
2026-04-24 18:34:17 +03:30
---
## `PreparedSimulationInput`
این dataclass ورودیهای نرمالشده برای اجرای مدل را نگه میدارد:
- `weather`
- `soil`
- `crop`
- `site`
- `agromanagement`
این ساختار باعث میشود manager با یک payload استاندارد کار کند.
---
## بخش سوم: `PcseSimulationManager`
این کلاس فقط مسئول اجرای موتور شبیهسازی است و وارد منطق ذخیره سناریوها نمیشود.
2026-04-24 22:20:15 +03:30
### `__init__(model_name="Wofost81_NWLP_CWB_CNB")`
2026-04-24 18:34:17 +03:30
مدل PCSE مورد استفاده را مشخص میکند.
مدل پیشفرض:
``` python
2026-04-24 22:20:15 +03:30
Wofost81_NWLP_CWB_CNB
2026-04-24 18:34:17 +03:30
```
### `run_simulation(...)`
ورودی خام میگیرد، normalize میکند، dependencyهای PCSE را load میکند، و شبیهسازی را اجرا میکند.
پارامترها:
- `weather`
- `soil`
- `crop_parameters`
- `agromanagement`
- `site_parameters`
خروجی:
``` python
{
" engine " : " pcse " ,
2026-04-24 22:20:15 +03:30
" model_name " : " Wofost81_NWLP_CWB_CNB " ,
2026-04-24 18:34:17 +03:30
" 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 نشان بدهد.