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