# ادامهٔ تازهٔ گفت‌وگوی «ادامه موتور برآورد» - **منبع:** https://chatgpt.com/share/6aba98d5-3f08-83eb-a756-a5ce48aa47ca - **تاریخ استخراج:** ۲۰۲۶٫۰۹٫۲۸ - **روش:** بازیابی متن نمایان `innerText` برای همهٔ پیام‌ها با پیمایش صفحهٔ مجازی‌سازی‌شده در Chrome؛ ترتیب از دادهٔ رندرشدهٔ همان صفحه بازسازی و با توالی متناوب ۱۲۴ پرسش/پاسخ کنترل شد. - **مرز:** پیوند جدید ۲۴۸ پیام دارد. ۶۰ شناسهٔ نخست همان شناسه‌های گفت‌وگوی دوم‌اند؛ ۱۸۸ پیام زیر (۹۴ پرسش و ۹۴ پاسخ) تازه‌اند. شناسه‌های پایدار زیر برای ارجاع استفاده می‌شوند. رونوشت قبلی در ترتیب نمایش برخی پیام‌ها خطا داشت؛ این سند فقط بخش تازه را به ترتیب منبع جدید ثبت می‌کند. --- ## ادامه 1 — پرسش کاربر (`message-id:21f9d568-33ab-453e-9b86-e6377e88772b`) بله لطفا --- ## ادامه 1 — پاسخ دستیار (`message-id:0f7eab59-cbb8-44ce-b810-e48031800884`) حتماً. حالا که خود Mockup را هم دوباره مستقیم بررسی کردم، می‌توانیم یک قدم جدی‌تر برویم. ساختار فعلی واقعاً یک Prototype قابل توسعه است؛ صفحه‌ها و منطق فعلی مشخص‌اند و حتی خود Mockup صراحتاً آن را «محاسبه زنده با پارامترهای نمایشی» معرفی می‌کند. Motortolid در ادامه، سندی که پیشنهاد می‌کنم مستقیماً مبنای طراحی Figma و بعد Frontend قرار بگیرد: سند UX/UI Specification موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی — نسخه 1.0 سند UX/UI Specification موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی Version: 1.0 Status: UX Architecture / Functional Specification Scope: Desktop Web Application Direction: RTL / Persian Design Approach: Enterprise Railway Planning & Decision Support 1. هدف سند این سند معماری تجربه کاربری و مشخصات عملکردی رابط کاربری موتور ظرفیت حمل بار ریلی را تعریف می‌کند. هدف این است که UI از یک نمایش‌دهنده مدل ریاضی به یک محیط عملیاتی، تحلیلی و تصمیم‌یار ریلی تبدیل شود. اصل اصلی: کاربر نباید صرفاً ظرفیت را ببیند؛ باید بتواند بفهمد ظرفیت از کجا آمده، چرا محدود شده و با چه تغییری افزایش می‌یابد. سه سؤال دائمی UI: Where? کجا محدودیت داریم؟ Why? چرا محدودیت داریم؟ What If? اگر چیزی را تغییر دهیم چه اتفاقی می‌افتد؟ 2. اصل معماری تجربه کاربر جریان اصلی محصول: Market Demand ↓ Transport Requirement ↓ Wagon Requirement ↓ Train Formation ↓ Baseline / New Train Run ↓ Schedule Generation ↓ Conflict Resolution ↓ Operational Feasibility ↓ Route Capacity ↓ Network Capacity ↓ Allocation ↓ Marketplace این جریان باید در UI قابل مشاهده باشد. 3. اصل مهم: Capacity نباید نقطه شروع کاربر باشد UI فعلی از مفاهیم: Block → Operational Regime → Cb → Cs → Batch → Conflict → Cr → Cn حرکت می‌کند. این ساختار برای نمایش مدل ریاضی مناسب است و در Mockup فعلی نیز همین مسیر دیده می‌شود. اما در محصول واقعی، کاربر باید بتواند از یک مسئله عملیاتی یا بازار شروع کند: «می‌خواهم ۲۰۰۰ تن بار جدید حمل کنم» و موتور خودش وارد Capacity Engine شود. بنابراین: Mathematical Model = پشت صحنه Business / Operational Problem = نقطه ورود UI 4. ساختار اصلی Navigation داشبورد شبکه و زیرساخت نقشه شبکه خطوط Segment Block Station Junction Resources برنامه قطار برنامه پایه قطارها Train Run Time-Space بازار و تقاضا درخواست‌ها Demand OD Commodity تخصیص تشکیل قطار Wagon Requirement Train Formation Wagon Pool Locomotive ظرفیت Physical Capacity Operational Capacity Route Capacity Network Capacity زمان‌بندی Schedule Conflict Batch Operational Window بهینه‌سازی Route Optimization Network Optimization Allocation سناریوها Scenario Builder Comparison Sensitivity Investment گلوگاه‌ها Bottlenecks Binding Constraints Hidden Capacity Capacity Release نتایج و گزارش Run History Reports Explainability Export 5. Global Application Shell تمام صفحات باید Shell مشترک داشته باشند. ┌──────────────────────────────────────────────────────────────┐ │ موتور ظرفیت حمل بار ریلی │ │ Scenario | Data Version | Model Version | Last Run | User │ ├─────────────────┬────────────────────────────────────────────┤ │ │ │ │ Navigation │ Workspace │ │ │ │ │ │ │ └─────────────────┴────────────────────────────────────────────┘ Header موارد ثابت: Scenario Data Version Model Version Last Run Solver Status Notification User اجرای تحلیل نمونه: Scenario: Baseline Data: DATA-1.8 Model: MODEL-2.3 Last Run: RUN-127 Status: Ready 6. P0 — Dashboard هدف نمایش وضعیت کلی شبکه و آخرین نتیجه موتور. Layout ┌─────────────────────────────────────────────────────────┐ │ وضعیت ظرفیت شبکه [اجرای تحلیل] │ ├─────────┬─────────┬─────────┬─────────┬───────────────┤ │ Physical│Operational│ Route │ Network │ Allocated │ │ 42 │ 31 │ 27 │ 21 │ 19 │ ├─────────────────────┬───────────────────────────────────┤ │ │ چرا ظرفیت محدود شده؟ │ │ NETWORK MAP │ Station S03 -4 │ │ │ Block B17 -2 │ │ │ Loco Pool -1 │ ├─────────────────────┴───────────────────────────────────┤ │ TIME–SPACE DIAGRAM │ ├─────────────────────────────────────────────────────────┤ │ Demand | Capacity | Bottleneck | Scenario │ └─────────────────────────────────────────────────────────┘ KPIها چهار ظرفیت: Physical Operational Route Network و سه مقدار بازار: Market Demand Transportable Demand Allocated Demand 7. Dashboard باید Drill-down داشته باشد مثلاً: کاربر روی: Network Capacity = 21 کلیک کند. سیستم وارد: Network Capacity Analysis شود. روی: Station S03 کلیک شود. سیستم وارد: Station Operational Capacity شود. این اصل در تمام UI برقرار باشد: Summary → Detail → Entity → Evidence 8. P0 — Network & Infrastructure هدف مشاهده و تحلیل ساختار فیزیکی و عملیاتی شبکه. Tabs نقشه خطوط Segment Block Station Junction Resources Map لایه‌ها: Railway Stations Blocks Single Track Double Track Routes Train Flow Capacity Bottleneck Conflicts 9. Network Entity Inspector با کلیک روی هر Segment: Segment S-102 Length Track Type Direction Max Speed Blocks Physical Capacity Operational Capacity Current Usage Available Capacity Binding Constraints با کلیک روی Block: Block B-17 Length Running Time Headway Occupancy Direction Conflict Capacity 10. P0 — Station Detail Station در این محصول صرفاً یک نقطه جغرافیایی نیست. ساختار Station S03 Infrastructure Tracks Usable Length Junctions Operations Arrival Departure Crossing Overtaking Formation Loading Unloading Capacity Physical Operational Current Usage Visual Station Time-Space Diagram و: Station Capacity Inspector 11. P0 — Train & Timetable Train List Filters: Origin Destination Date Train Type Load State Status جدول: Train Origin Destination Departure Arrival Status 12. Train Detail اطلاعات به چهار گروه تقسیم شود. Identity Train Number Train Name Service Origin Destination Formation Wagon Count Wagon Type Weight Length Locomotive Operation Load State Direction Train Type Speed Profile Schedule Departure Arrival Station Calls Waiting Conflicts 13. Train / TrainRun / TrainFormation UI باید این سه مفهوم را کاملاً جدا نگه دارد. Train Service │ ├── Train Run │ │ │ ├── Station Calls │ └── Schedule │ └── Formation ├── Wagons └── Locomotive این تفکیک برای جلوگیری از خطای مفهومی در محصول ضروری است. 14. P0 — Market & Demand Create Demand Wizard Step 1 — OD Origin Destination Step 2 — Commodity Commodity Quantity Step 3 — Transport Requirement Wagon Type Frequency Time Window Load State Step 4 — Capacity Check Demand 2,000 ton Wagon Requirement 50 wagon Train Requirement 2 trains 15. Demand Result نتیجه نباید فقط: Available / Not Available باشد. بلکه: Market Demand 2,000 Transportable Demand 1,800 Allocated Demand 1,500 Unserved Demand 500 و علت: Station Capacity -200 Locomotive -150 Single Track Conflict -150 16. P0 — Train Formation این صفحه رابط Demand و Train است. Layout Demand 2,000 ton ↓ Formation Builder 🚂 + Wagon + Wagon + Wagon ... ↓ Formation Summary نمایش: Wagon Count Weight Length Locomotive Load State Train Type اگر لازم باشد: Train 101 → 25 wagons Train 103 → 25 wagons 17. P0 — Scheduling Workspace این یکی از مهم‌ترین صفحات کل محصول است. Layout ┌──────────────┬───────────────────────────┬───────────────┐ │ Train List │ Time-Space Diagram │ Inspector │ │ │ │ │ │ Train 101 │ ╱ Train 101 │ Conflict │ │ Train 103 │ ╱ │ │ │ Train 105 │ ────╳──── Train 103 │ Block B17 │ │ │ │ Wait 8 min │ └──────────────┴───────────────────────────┴───────────────┘ Interaction Click Train: → Train Inspector Click Conflict: → Conflict Inspector Click Station: → Station Inspector Click Block: → Block Inspector 18. Conflict Inspector مثال: Conflict #124 Train 101 Train 103 Resource Block B17 Type Opposing Movement Resolution Train 103 waits 8 min Status Resolved Actions: [Accept Resolution] [Try Alternative] [Manual Adjustment] 19. Batch Manager Batch باید Schedule واقعی پشت خود داشته باشد. Batch B-04 Direction A → B Trains 101 103 105 Start 06:00 End 10:42 Headway 18 min Switch Time 12 min Feasible ✓ اگر Batch Size تغییر کند، موتور Schedule را دوباره حل کند. 20. P0 — Route Capacity هدف پاسخ به: حداکثر چند Train Run قابل برنامه‌ریزی است؟ Result Route Capacity 27 Train Runs / Day Generated Schedule 27 trains Unresolved Conflicts 0 Station Violations 0 Fleet Violations 0 این صفحه باید Schedule تولیدشده را هم نشان دهد. اصل: [ C_r=\max{F:\text{Schedule}(F)\ is\ feasible} ] باید در UX به زبان ساده ترجمه شود: ظرفیت مسیر = بیشترین تعداد قطاری که موتور بتواند برای آن برنامه زمان‌بندی معتبر تولید کند. 21. P0 — Network Capacity Layout Route A 8 Route B 6 Route C 4 Route D 3 ──────────────── Network 21 Shared Resources Junction J3 97% Station S03 92% Locomotive 88% Wagon Pool 76% کلیک روی هر Resource: → Routeهای تحت تأثیر 22. P0 — Explainability این باید یک قابلیت عمومی باشد، نه صفحه‌ای فرعی. Capacity Inspector Capacity 27 trains/day Derived From ✓ Infrastructure ✓ Operating Regime ✓ Station Capacity ✓ Schedule ✓ Fleet Binding Constraint Station S03 Marginal Impact +4 trains/day Run RUN-127 23. Capacity Waterfall Visualization اختصاصی: Physical Capacity 42 ↓ Operating Rules 37 ↓ Station Constraints 34 ↓ Fleet 30 ↓ Scheduling 27 ↓ Network Conflict 21 این Visualization باید یکی از Signature Componentهای محصول باشد. 24. P1 — Bottleneck Center Bottleneck Explorer Resource Utilization Binding Marginal Impact Station S03 94% Yes +4 Block B17 91% Yes +2 Locomotive 88% No 0 کاربر باید بتواند از Bottleneck: به علت → Constraint → Scenario حرکت کند. 25. P0 — Scenario Builder Scenario به‌صورت Workspace: Baseline Infrastructure Operations Fleet Demand Policy کاربر تغییرات را اضافه کند: + Crossing Loop + 2 Locomotives + 100 Wagons سپس: Run Scenario 26. Scenario Result Baseline Scenario A Network Capacity 21 28 Demand Served 19 26 Conflicts 7 3 Loco Utilization 91% 84% و: Why? Crossing Loop +4 Locomotive +2 Operating Window +1 27. Scenario Comparison به‌جای نمودارهای متعدد، ابتدا جدول مقایسه: KPI Baseline S-A S-B S-C Network Capacity 21 25 27 28 Demand Served 19 23 25 26 Conflicts 7 5 4 3 سپس Visualization. 28. P1 — Data Quality Center به دلیل وجود Excel و Access، این بخش ضروری است. Data Quality 94% Valid Warnings 7 Errors 2 Unmapped 4 Unverified 3 مثال: Field: RequiredWait Source: Access Semantic Status: Unverified Production Usage: Blocked 29. P1 — Run History هر تحلیل یک Run مستقل باشد. RUN-127 Scenario: Baseline Data: DATA-1.8 Model: MODEL-2.3 Solver: CP-SAT Status: Completed با کلیک: Inputs Parameters Constraints Schedule Capacity Bottlenecks Explanation 30. Error UX خطا باید قابل اقدام باشد. بد: Solver Error 2048 خوب: زمان توقف Train 103 در Station S03 مشخص نشده است. Source: Access Field: RequiredWait Record: 127 Status: Unverified Actions: [مشاهده داده] [اصلاح] [بازبینی Mapping] 31. Empty State هیچ صفحه‌ای نباید Blank باشد. مثلاً: هنوز Baseline Schedule تعریف نشده است. [ایجاد برنامه پایه] یا: برای اجرای تحلیل، ۳ پارامتر ضروری تأیید نشده است. [مشاهده خطاهای داده] 32. Loading State برای عملیات Solver، Loading معمولی مناسب نیست. مثلاً: در حال حل مسئله... ✓ Loading Network ✓ Loading Demand ✓ Generating Candidate Schedule ● Resolving Conflicts ○ Validating Fleet ○ Calculating Capacity این به کاربر حس «محاسبه واقعی» می‌دهد. 33. Result State پس از Run: ✓ Analysis Completed Route Capacity 27 Network Capacity 21 Feasible Schedule YES Binding Constraints 3 Unused Capacity 2 و: [مشاهده Schedule] [مشاهده گلوگاه] [ساخت سناریو] 34. Technical Details برای کاربران متخصص، هر نتیجه باید امکان باز کردن جزئیات داشته باشد. مثلاً: Methodology Capacity Model C_r Scheduling Method Conflict-based Scheduling Solver CP-SAT Time Resolution 1 minute Scenario Baseline اما این اطلاعات نباید در مسیر اصلی UX مزاحم باشند. 35. UX برای کاربران مختلف Planner نیاز دارد: Train Schedule Conflict Station Block Capacity Analyst نیاز دارد: Capacity Bottleneck Sensitivity Scenario Marketplace User نیاز دارد: Demand OD Available Capacity Allocation Manager نیاز دارد: Network Capacity Demand Served Bottleneck Scenario Comparison Investment Impact بنابراین UI باید Role-aware باشد، حتی اگر Backend مشترک باشد. 36. Search سراسری در Header یک Global Search بسیار مفید خواهد بود. مثلاً: جستجو: تهران Train 101 Station S03 Block B17 Scenario 12 Run 127 نتیجه: Station S03 Train 101 Route Tehran–Rasht Run RUN-127 37. Notification Center اعلان‌های مهم: ⚠ Capacity changed ⚠ Data mapping issue ⚠ Scenario completed ⚠ New bottleneck detected ✓ Schedule generated ✓ Validation passed 38. Permission UX Roleها: Viewer Planner Capacity Analyst Network Optimizer Market Operator Administrator مثلاً Viewer نتواند Scenario را Run کند. 39. Visual Design Principles در این مرحله هنوز وارد انتخاب رنگ نهایی نمی‌شویم. اما اصول: Density Enterprise / Information Dense Spacing Consistent 8px grid Typography خوانا برای فارسی + اعداد و اصطلاحات انگلیسی Cards کم، هدفمند، نه Dashboard Card overload Charts Decision-oriented Tables قابل Filter / Sort / Export Map Interactive Timeline Zoomable 40. رنگ معنایی رنگ‌ها باید Semantic باشند. Green Feasible / Available Yellow Warning / Near Capacity Red Conflict / Binding / Infeasible Blue Information / Selection Gray Inactive / Historical ولی هیچ وضعیت مهمی فقط با رنگ مشخص نشود. 41. اصل Responsive این محصول Desktop-first است. Desktop Full Planning Workspace Tablet Monitoring + Analysis Mobile KPI + Alert + Scenario Result ویرایش کامل Schedule روی موبایل هدف اصلی نیست. 42. Page Inventory نسخه اول محصول: ID Page Priority P01 Dashboard P0 P02 Network Map P0 P03 Station Detail P0 P04 Block Detail P0 P05 Train List P0 P06 Train Detail P0 P07 Baseline Schedule P0 P08 Demand P0 P09 Demand Result P0 P10 Train Formation P0 P11 Scheduling Workspace P0 P12 Conflict Inspector P0 P13 Route Capacity P0 P14 Network Capacity P0 P15 Bottleneck Center P0 P16 Scenario Builder P0 P17 Scenario Comparison P1 P18 Data Quality P1 P19 Run History P1 P20 Reports P1 43. Figma Prototype Flow برای Prototype اولیه لازم نیست تمام ۲۰ صفحه ساخته شود. اول این مسیر را Interactive کنیم: P01 Dashboard ↓ P08 Demand ↓ P10 Train Formation ↓ P11 Scheduling ↓ P12 Conflict ↓ P13 Route Capacity ↓ P15 Bottleneck ↓ P16 Scenario ↓ P17 Scenario Result این مسیر تقریباً تمام ارزش اصلی محصول را نمایش می‌دهد. 44. مهم‌ترین Prototype Scenario برای Demo محصول، یک سناریوی واقعی و کامل تعریف شود: Demand: + X ton OD: A → B Commodity: X Wagon: X Train Requirement: X trains/day Existing Schedule: Baseline Requested Capacity: + X trains/day Result: Feasible / Partially Feasible / Infeasible Route Capacity: X Network Capacity: X Binding Constraint: Station / Block / Fleet Scenario: Infrastructure / Fleet / Operation New Capacity: X این Demo خیلی قدرتمندتر از نمایش صرفاً محاسبه Cb/Cs/Cr/Cn خواهد بود. 45. اصول UX که باید به Acceptance Criteria تبدیل شوند UX-001 کاربر باید بتواند از یک Demand به Schedule برسد. UX-002 کاربر باید بتواند علت محدودیت Capacity را مشاهده کند. UX-003 کاربر باید بتواند از Bottleneck به Scenario برود. UX-004 کاربر باید بتواند Schedule تولیدشده را مشاهده کند. UX-005 هر Capacity Result باید Run ID و Data/Model Version داشته باشد. UX-006 نتیجه Infeasible باید علت قابل فهم داشته باشد. UX-007 کاربر نباید مجبور باشد برای عملیات معمول با فرمول‌های ریاضی کار کند. UX-008 کاربر متخصص باید بتواند Technical Details را مشاهده کند. UX-009 Demand Market، Transportable و Allocated باید از هم قابل تشخیص باشند. UX-010 Train، TrainRun، TrainFormation و Batch نباید در UI با یک مفهوم نمایش داده شوند. 46. Signature Components محصول برای اینکه UI هویت خودش را داشته باشد، پنج Component اختصاصی پیشنهاد می‌شود: 1. Capacity Waterfall نشان می‌دهد ظرفیت چگونه کاهش یافته است. 2. Time-Space Planner نشان می‌دهد قطارها چگونه در شبکه حرکت می‌کنند. 3. Bottleneck Inspector نشان می‌دهد چه چیزی ظرفیت را محدود کرده است. 4. Scenario Lab نشان می‌دهد با تغییر چه چیزی ظرفیت تغییر می‌کند. 5. Capacity Inspector نشان می‌دهد عدد Capacity دقیقاً از کجا آمده است. 47. تفاوت این محصول با یک BI Dashboard این نکته باید در طراحی حفظ شود. BI Dashboard: Data → Charts این محصول: Data ↓ Model ↓ Schedule ↓ Solver ↓ Feasibility ↓ Capacity ↓ Explanation ↓ Decision بنابراین UI باید Interactive Model Workspace باشد، نه فقط Dashboard. 48. اصلاح مستقیم Mockup فعلی Mockup فعلی بخش‌های زیر را دارد: تعریف ورودی تعریف Block/Route رژیم بهره‌برداری Capacity Layers Batch Conflict Route Capacity Network Capacity Scenario Validation Map Marketplace این ساختار برای Prototype مدل ریاضی مناسب است. اما در نسخه UX جدید: حفظ Capacity Layers Batch Conflict Scenario Validation Map Marketplace تبدیل به صفحات واقعی Input → Problem Definition Block/Route → Network & Infrastructure Operational Regime → Scheduling Configuration Cb/Cs → Capacity Analysis Cr → Route Capacity Cn → Network Capacity اضافه شدن Baseline Schedule Train Train Run Train Formation Demand Wagon Requirement Station Block Time-Space Bottleneck Explainability Run History Data Quality 49. معماری نهایی UX در نهایت محصول باید در ذهن کاربر این‌گونه دیده شود: ┌──────────────┐ │ MARKET │ │ DEMAND │ └──────┬───────┘ ↓ ┌──────────────┐ │ OD │ │ COMMODITY │ └──────┬───────┘ ↓ ┌──────────────┐ │ WAGON NEED │ └──────┬───────┘ ↓ ┌──────────────┐ │ TRAIN │ │ FORMATION │ └──────┬───────┘ ↓ ┌──────────────────────────┐ │ SCHEDULING WORKSPACE │ │ │ │ Station / Block / Time │ │ Conflict / Batch │ └────────────┬─────────────┘ ↓ ┌──────────────┐ │ FEASIBILITY │ └──────┬───────┘ ↓ ┌─────────────────────────────┐ │ CAPACITY │ │ │ │ Cb → Cs → Cr → Cn │ └─────────────┬───────────────┘ ↓ ┌──────────────┐ │ EXPLAINABILITY│ │ WHY / WHERE │ └──────┬───────┘ ↓ ┌──────────────┐ │ SCENARIO │ │ WHAT IF? │ └──────┬───────┘ ↓ ┌──────────────┐ │ ALLOCATION │ └──────────────┘ 50. اصل نهایی UX کل محصول باید این تجربه را ایجاد کند: «من مسئله حمل را تعریف کردم؛ سیستم خودش قطار موردنیاز را تشکیل داد، برنامه حرکت قابل اجرا ساخت، تعارض‌ها را حل کرد، ظرفیت واقعی را محاسبه کرد، گفت کجا محدودیت داریم، توضیح داد چرا محدودیت ایجاد شده و نشان داد با چه تغییری می‌توان ظرفیت را افزایش داد.» این دقیقاً همان نقطه‌ای است که UI/UX را با مدل ریاضی، داده واقعی، بازارگاه و موتور Scheduling/Optimization یکپارچه می‌کند. 51. اولویت اجرای طراحی پیشنهاد اجرایی من: Sprint 1 — Information Architecture Navigation User Journey Page hierarchy Entity relationships Sprint 2 — Core Wireframe Dashboard Demand Formation Scheduling Capacity Bottleneck Sprint 3 — Interactive Prototype Map Time-Space Conflict Scenario Drill-down Sprint 4 — Visual Design Design System Typography Color Components States Sprint 5 — Frontend Specification API bindings Domain entities Events Loading/Error/Empty states Permissions Audit / Run History Sprint 6 — Real Data Integration Excel Access Canonical Model Baseline Schedule Marketplace API نتیجه در این مرحله من بازطراحی کامل Mockup فعلی را پیشنهاد نمی‌کنم؛ پیشنهاد بهتر، تبدیل آن به نسخه 2 با معماری UX جدید است. هسته فعلی ارزش حفظ کردن دارد؛ چون مفاهیم Cb/Cs/Cr/Cn، Batch، Conflict، Scenario، Validation و Map را قبلاً در خود دارد. اما Version 2 باید از یک «مدل ریاضی قابل کلیک» به یک «Railway Capacity Planning Workbench» تبدیل شود. و اگر بخواهیم این را به تیم طراحی بدهیم، قدم بعدی دیگر متن مفهومی نیست؛ باید برای P01 تا P20، Wireframe دقیق هر صفحه را با جای Componentها و تعاملاتش تعریف کنیم. این دقیقاً همان سندی است که طراح Figma می‌تواند مستقیماً بر اساس آن شروع به کار کند. به نظرم بعد از این سند، بهترین قدم بعدی طراحی ۶ صفحه کلیدی P01، P08، P10، P11، P13 و P16 به‌صورت Wireframe متنی دقیق است؛ یعنی حتی مشخص کنیم در هر صفحه، هر کارت، جدول، دکمه، Drawer و نمودار دقیقاً کجا قرار بگیرد و با کلیک روی آن چه اتفاقی بیفتد. این مرحله عملاً پلی بین این Specification و Figma خواهد بود. --- ## ادامه 2 — پرسش کاربر (`message-id:5cb137eb-ede4-4ed4-86ce-d794430de8e5`) بله --- ## ادامه 2 — پاسخ دستیار (`message-id:f9ddcf80-31f9-48d2-93a4-d8a919036312`) بله. حالا می‌رویم یک مرحله پایین‌تر: Wireframe Functional؛ یعنی چیزی که طراح Figma بتواند تقریباً مستقیم از روی آن طراحی کند. من این را بر اساس ساختار واقعی Mockup فعلی هم تنظیم می‌کنم؛ چون در نسخه موجود، جریان فعلی عمدتاً از تعریف Block/Route به Capacity Layers، Batch/Conflict، Route/Network و سپس Scenario/Validation می‌رود. Motortolid سند Wireframe و Functional UI — صفحات کلیدی موتور ظرفیت حمل بار ریلی — نسخه 1.0 سند Wireframe و Functional UI صفحات کلیدی موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی Version: 1.0 Type: Functional Wireframe Specification Platform: Desktop Web Application Language: Persian / RTL Target: Figma → Interactive Prototype → Frontend 1. هدف این سند جایگزین طراحی Visual نیست. هدف آن مشخص کردن این موارد است: جای Componentها سلسله‌مراتب اطلاعات رفتار کاربر Actionها Drill-down Navigation وضعیت‌های Loading / Empty / Error / Success ارتباط صفحات با Domain Model ارتباط صفحات با Engine شش صفحه اصلی این سند: Dashboard Demand / Market Request Train Formation Scheduling Workspace Route Capacity Scenario Lab و چند صفحه پشتیبان: Bottleneck Inspector Conflict Inspector Network Capacity Capacity Inspector Run Result 2. اصول مشترک تمام صفحات 2.1 Header ┌──────────────────────────────────────────────────────────────┐ │ موتور ظرفیت حمل بار ریلی │ │ │ │ شبکه: سراسری سناریو: Baseline داده: 1.8 مدل: 2.3 │ │ │ │ آخرین اجرا: RUN-127 [اجرای تحلیل] [کاربر]│ └──────────────────────────────────────────────────────────────┘ Header Actions Scenario Selector Data Version Model Version Last Run Run Engine Notifications User 3. Sidebar داشبورد شبکه و زیرساخت نقشه شبکه خطوط Segment Block Station Resources برنامه قطار برنامه پایه قطارها Train Run Time-Space بازار و تقاضا درخواست‌ها Demand OD تخصیص تشکیل قطار Wagon Requirement Train Formation Wagon Pool Locomotive ظرفیت Physical Operational Route Network زمان‌بندی Schedule Conflict Batch Operational Window بهینه‌سازی Route Network Allocation سناریوها Scenario Builder Comparison Sensitivity Investment گلوگاه‌ها Bottlenecks Constraints Hidden Capacity نتایج Run History Reports Explainability 4. P01 — Dashboard 4.1 هدف Dashboard باید در کمتر از ۳۰ ثانیه به کاربر بگوید: شبکه چه وضعیتی دارد؟ ظرفیت چقدر است؟ تقاضا چقدر است؟ گلوگاه کجاست؟ آیا ظرفیت جدید قابل ایجاد است؟ 4.2 Wireframe ┌─────────────────────────────────────────────────────────────────────┐ │ وضعیت شبکه [اجرای تحلیل] │ ├───────────┬───────────┬───────────┬───────────┬─────────────────────┤ │ Physical │Operational│ Route │ Network │ Allocated Demand │ │ 42 │ 31 │ 27 │ 21 │ 19 │ │ train/day │ train/day │ train/day │ train/day │ train/day │ ├───────────────────────────────────┬─────────────────────────────────┤ │ │ │ │ │ چرا ظرفیت محدود شده؟ │ │ NETWORK MAP │ │ │ │ ● Station S03 +4 │ │ ●──────●══════●────● │ ● Block B17 +2 │ │ ▲ │ ● Locomotive Pool 0 │ │ Bottleneck │ │ │ │ [مشاهده کامل] │ ├───────────────────────────────────┴─────────────────────────────────┤ │ TIME–SPACE DIAGRAM │ │ │ │ Train 101 ───────────────╱──────────────────────────────── │ │ Train 103 ─────────────╱───────────⚠──────────────────── │ │ Train 105 ─────────────────────────────────────────────── │ ├─────────────────────────────────────────────────────────────────────┤ │ تقاضای بازار │ قابل حمل │ تخصیص‌یافته │ ظرفیت آزاد │ هشدارها │ └─────────────────────────────────────────────────────────────────────┘ 5. Dashboard — تعاملات کلیک روی Capacity مثلاً: Route = 27 → باز شدن Capacity Inspector کلیک روی Station → Station Detail کلیک روی Train → Train Detail کلیک روی Conflict → Conflict Inspector کلیک روی Bottleneck → Bottleneck Center کلیک روی Demand → Demand Analysis 6. Dashboard — KPI Model سه گروه KPI داشته باشیم. Infrastructure Physical Capacity Operational Capacity Scheduling Route Capacity Network Capacity Market Market Demand Transportable Demand Allocated Demand این تفکیک مهم است. 7. Dashboard — Capacity Inspector با کلیک روی هر Capacity: ┌─────────────────────────────┐ │ Capacity Inspector │ ├─────────────────────────────┤ │ Value │ │ 27 trains/day │ │ │ │ Derived From │ │ ✓ Infrastructure │ │ ✓ Operating Regime │ │ ✓ Station Capacity │ │ ✓ Schedule │ │ ✓ Fleet │ │ │ │ Binding Constraint │ │ Station S03 │ │ │ │ Marginal Impact │ │ +4 trains/day │ │ │ │ Run │ │ RUN-127 │ └─────────────────────────────┘ 8. P02 — Demand / Market Request هدف ورودی بازارگاه را به یک مسئله قابل حل برای Capacity Engine تبدیل کند. 9. Demand Wizard صفحه باید Wizard باشد، نه فرم طولانی. ① OD ↓ ② Cargo ↓ ③ Transport ↓ ④ Wagon ↓ ⑤ Capacity Check 10. Step 1 — OD ┌───────────────────────────────────────────┐ │ درخواست حمل جدید │ ├───────────────────────────────────────────┤ │ │ │ مبدا │ │ [ تهران ▼ ] │ │ │ │ مقصد │ │ [ رشت ▼ ] │ │ │ │ بازه زمانی │ │ [ از ] [ تا ] │ │ │ │ [ادامه →] │ └───────────────────────────────────────────┘ Interaction Origin و Destination باید از Master Data شبکه انتخاب شوند. 11. Step 2 — Commodity کالا [ سنگ‌آهن ▼ ] مقدار [ 2,000 ] تن الگوی تقاضا ○ روزانه ○ هفتگی ○ ماهانه ○ یک‌باره 12. Step 3 — Transport Requirement نوع واگن [ واگن باری X ▼ ] وضعیت بار [ Loaded ] بازه حرکت [ 06:00 — 22:00 ] روزهای مجاز [ شنبه ] [ یکشنبه ] ... 13. Step 4 — Wagon Requirement موتور پیشنهاد اولیه بدهد: ┌──────────────────────────────────┐ │ برآورد اولیه │ ├──────────────────────────────────┤ │ Demand 2,000 ton │ │ Wagon Requirement 50 │ │ Train Requirement 2 │ │ │ │ [مشاهده جزئیات محاسبه] │ └──────────────────────────────────┘ 14. Step 5 — Capacity Check نتیجه سه سطح داشته باشد. Full ✓ قابل تخصیص کامل Demand 2,000 Transportable 2,000 Allocatable 2,000 Partial ! قابل تخصیص جزئی Demand 2,000 Transportable 1,800 Allocatable 1,500 Unserved 500 Infeasible ✕ فعلاً قابل تخصیص نیست Requested 4 trains Available 0 Primary Constraint Station S03 15. Demand Result — Why Panel در سمت راست: چرا کامل تخصیص نیافت؟ Station Capacity -200 Locomotive -150 Single Track Conflict -150 [بررسی سناریوی افزایش ظرفیت] این دکمه مستقیماً وارد Scenario Lab شود. 16. P03 — Train Formation هدف تبدیل Transport Requirement به Train Formation. 17. Wireframe ┌──────────────────┬──────────────────────────┬─────────────────────┐ │ Demand │ Formation Builder │ Summary │ │ │ │ │ │ 2,000 ton │ 🚂 │ Train 101 │ │ │ □ □ □ □ □ │ │ │ 50 wagons │ □ □ □ □ □ │ Wagons: 25 │ │ │ □ □ □ □ □ │ Weight: ... │ │ │ │ Length: ... │ │ │ [افزودن واگن] │ Locomotive: 1 │ │ │ [حذف واگن] │ │ └──────────────────┴──────────────────────────┴─────────────────────┘ 18. Formation Actions Add Wagon Remove Wagon Change Wagon Type Change Locomotive Split Train Merge Formation Validate Formation 19. Formation Validation مثلاً: ✓ Length OK ✓ Weight OK ✓ Locomotive OK ⚠ Station S03 usable length محدود کاربر بتواند: [مشاهده محدودیت] را بزند. 20. P04 — Scheduling Workspace این صفحه مهم‌ترین Workspace عملیاتی است. 21. Layout ┌───────────────┬───────────────────────────────┬──────────────────┐ │ TRAIN LIST │ TIME–SPACE │ INSPECTOR │ │ │ │ │ │ 101 │ ╱──── Train 101 │ Selected Train │ │ 103 │ ╱ │ │ │ 105 │ ────╳──── Train 103 │ Conflict #124 │ │ 107 │ │ │ │ │ │ │ ├───────────────┴───────────────────────────────┴──────────────────┤ │ Timeline Controls | Zoom | Filter | Direction | Batch | Run │ └───────────────────────────────────────────────────────────────────┘ 22. Train List فیلتر: Direction Load State Train Type Origin Destination Status Status: ✓ Feasible ⚠ Waiting ⚠ Conflict ✕ Infeasible 23. Time-Space محور X: Time محور Y: Station / Distance هر Train یک trajectory دارد. روی trajectory: Arrival Departure Waiting Conflict Crossing Batch 24. Conflict Interaction وقتی کاربر روی Conflict کلیک کند: Conflict #124 Train 101 Train 103 Resource Block B17 Type Opposing Direction Current Resolution Train 103 waits 8 min [قبول] [راه‌حل جایگزین] [ویرایش دستی] 25. Alternative Resolution با کلیک روی: راه‌حل جایگزین موتور چند گزینه بدهد: Option A Wait Train 103 +8 min Option B Wait Train 101 +6 min Option C Change Crossing Station +3 min کاربر یکی را انتخاب کند. این قابلیت از نظر UX بسیار ارزشمند است. 26. Batch View کاربر بتواند چند Train را انتخاب کند: [101] [103] [105] و: Create Batch بعد: Batch B-04 Direction: A → B Trains: 3 Start: 06:00 End: 10:42 Headway: 18 min Switch: 12 min Status: Feasible 27. P05 — Route Capacity هدف تبدیل Scheduling Feasibility به Capacity. 28. Route Capacity Layout ┌──────────────────────────────────────────────────────────────┐ │ Route Capacity │ │ A → B [Generate Schedule] │ ├────────────┬────────────┬────────────┬───────────────────────┤ │ Physical │Operational │ Route │ Available │ │ 42 │31 │27 │2 │ ├──────────────────────────────────────────────────────────────┤ │ │ │ GENERATED SCHEDULE │ │ │ │ Train 101 06:00 → 10:30 ✓ │ │ Train 103 08:15 → 13:00 ✓ │ │ Train 105 10:40 → 15:30 ✓ │ │ ... │ │ │ ├──────────────────────────────────────────────────────────────┤ │ Feasibility │ │ ✓ Schedule Valid │ │ ✓ Station Valid │ │ ✓ Fleet Valid │ │ ✓ Buffer Valid │ │ ✓ Conflict Resolved │ └──────────────────────────────────────────────────────────────┘ 29. Route Capacity — Critical UX Rule عدد ظرفیت بدون Schedule معتبر قابل قبول نیست. اگر: Route Capacity = 27 باشد: [مشاهده برنامه 27 قطار] باید فعال باشد. 30. Capacity Proof Panel در پایین صفحه: Capacity Proof C_r = 27 Schedule(F=27) ✓ Feasible Schedule(F=28) ✕ Infeasible Binding Constraint: Station S03 Therefore: C_r = 27 این یکی از قوی‌ترین بخش‌های UI خواهد بود. 31. P06 — Scenario Lab هدف پاسخ به: اگر شرایط را تغییر بدهم چه اتفاقی می‌افتد؟ 32. Scenario Layout ┌────────────────────┬──────────────────────────┬───────────────────┐ │ BASELINE │ SCENARIO CHANGES │ RESULT │ │ │ │ │ │ Infrastructure │ + Crossing Loop │ Capacity │ │ Operations │ + 2 Locomotives │ 21 → 28 │ │ Fleet │ + 100 Wagons │ │ │ Demand │ │ Demand Served │ │ Policy │ │ 19 → 26 │ │ │ │ │ │ │ [Run Scenario] │ Conflicts │ │ │ │ 7 → 3 │ └────────────────────┴──────────────────────────┴───────────────────┘ 33. Scenario Parameter Editor هر تغییر باید با مقدار Baseline و New Value نمایش داده شود. Station S03 Usable Track Baseline: 2 Scenario: 3 Δ: +1 یا: Locomotive Pool Baseline: 12 Scenario: 14 Δ: +2 34. Scenario Result پس از Run: ✓ Scenario Completed Network Capacity 21 → 28 Demand Served 19 → 26 Conflicts 7 → 3 Unused Capacity 2 → 1 35. Scenario Explanation حتماً نمایش داده شود: Capacity Increase Drivers Crossing Loop +4 Locomotive +2 Operating Window +1 Network Interaction +0 ──────────────────────── Total +7 36. Scenario Comparison برای چند سناریو: BASE S-A S-B S-C Capacity 21 25 27 28 Demand Served 19 23 25 26 Conflicts 7 5 4 3 Loco Utilization 91 88 86 84 کاربر روی هر سلول بتواند Drill-down کند. 37. P07 — Bottleneck Inspector این صفحه از Scenario جداست ولی به آن متصل است. ┌──────────────────────────────────────────┐ │ Bottleneck: Station S03 │ ├──────────────────────────────────────────┤ │ Utilization 94% │ │ Binding YES │ │ Marginal Impact +4 │ │ │ │ Cause │ │ Crossing Capacity │ │ │ │ Affected Routes │ │ A → B │ │ A → C │ │ │ │ [ساخت سناریو] [مشاهده Schedule] │ └──────────────────────────────────────────┘ 38. P08 — Network Capacity این صفحه از Route Capacity متمایز است. Route A 8 Route B 6 Route C 4 Route D 3 ──────────────── Network 21 اما مهم‌تر: Shared Resource Impact Junction J3 Routes A,B,C Current Usage 97% Binding: YES کلیک: → مسیرهای متاثر 39. P09 — Run Result هر اجرای موتور یک Result Package تولید کند. RUN-127 Scenario Baseline Data Version DATA-1.8 Model Version MODEL-2.3 Solver CP-SAT Status Completed Tabs: Summary Schedule Capacity Bottlenecks Constraints Demand Explanation Technical 40. Loading State اجرای Solver نباید فقط Spinner باشد. در حال اجرای تحلیل... ✓ Loading Network ✓ Loading Demand ✓ Loading Baseline Schedule ✓ Generating Candidate Trains ● Resolving Conflicts ○ Validating Fleet ○ Calculating Route Capacity ○ Calculating Network Capacity 41. Empty State بدون Baseline برنامه پایه‌ای برای این شبکه تعریف نشده است. [ایجاد برنامه پایه] بدون Demand هنوز درخواست حملی ثبت نشده است. [ایجاد درخواست حمل] بدون Scenario سناریویی ایجاد نشده است. [ساخت سناریو] 42. Error State خطا باید با Context نمایش داده شود. ⚠ اجرای تحلیل متوقف شد علت: RequiredWait برای Train 103 در Station S03 تأیید نشده است. Source: Access Field: RequiredWait Status: Unverified [مشاهده Mapping] [بازگشت به داده] 43. UX برای Data Quality در هر Run: Data Quality ────────────── ✓ Valid 94% ⚠ Warning 4% ✕ Error 2% اگر Error روی پارامتر مؤثر باشد: Run باید Block شود. 44. Drill-down Architecture قانون عمومی: Dashboard ↓ KPI ↓ Entity ↓ Constraint ↓ Evidence ↓ Scenario مثلاً: Network Capacity 21 ↓ Station S03 ↓ Crossing Capacity ↓ Train Conflict ↓ Schedule ↓ Scenario این یکی از اصول اصلی UX محصول باشد. 45. Context Drawer برای اینکه کاربر مدام صفحه عوض نکند، بسیاری از Detailها در Drawer باز شوند. مثلاً: Dashboard │ └── Click Station S03 ↓ ┌─────────────────┐ │ Station S03 │ │ │ │ Capacity │ │ Utilization │ │ Trains │ │ Constraints │ │ │ │ [Open Detail] │ └─────────────────┘ برای Explorer UX بسیار مناسب است. 46. Action Hierarchy در هر صفحه حداکثر یک Primary Action. مثلاً Dashboard: اجرای تحلیل Demand: بررسی ظرفیت Formation: اعتبارسنجی تشکیل قطار Scheduling: حل برنامه Route: تولید Schedule Scenario: اجرای سناریو این باعث کاهش Cognitive Load می‌شود. 47. Terminology در UI از زبان کاربر استفاده شود. مثلاً: Technical UI Label (C_b) ظرفیت فیزیکی (C_s) ظرفیت عملیاتی (C_r) ظرفیت مسیر (C_n) ظرفیت شبکه Feasible قابل اجرا Binding Constraint محدودیت تعیین‌کننده Operational Window پنجره بهره‌برداری Train Formation تشکیل قطار Operational Batch بچ عملیاتی Train Run گردش/حرکت قطار فرمول‌ها در Technical Details دیده شوند. 48. Signature Interaction پیشنهاد می‌کنم یک Interaction ویژه داشته باشیم: «از عدد تا دلیل» هر Capacity Card: 27 ظرفیت مسیر در گوشه یک: ⓘ چرا؟ با کلیک: 27 ↓ Schedule ↓ Station ↓ Block ↓ Conflict ↓ Binding Constraint این Interaction می‌تواند هویت محصول شود. 49. Signature Interaction دوم «از گلوگاه تا راه‌حل» در Bottleneck: Station S03 Binding دکمه: چطور ظرفیت را آزاد کنم؟ سیستم Scenarioهای ممکن را پیشنهاد می‌دهد: + Crossing Track Potential Impact: +4 + Operating Window Potential Impact: +1 + Fleet Potential Impact: +0 این بخش را می‌توان بعداً با Optimization واقعی متصل کرد. 50. Signature Interaction سوم «Demand → Capacity» در Market: 2,000 ton کاربر بزند: آیا امکان حمل وجود دارد؟ و سیستم مسیر را نشان دهد: 2,000 ton ↓ 50 wagons ↓ 2 trains ↓ Schedule ↓ Capacity ↓ ✓ 1,800 ton transportable این تجربه، نقطه اتصال Marketplace و Engine است. 51. Prototype Navigation برای Figma Prototype ابتدا فقط این Flow ساخته شود: Dashboard ↓ New Demand ↓ Demand Result ↓ Train Formation ↓ Scheduling Workspace ↓ Conflict Inspector ↓ Route Capacity ↓ Bottleneck ↓ Scenario Lab ↓ Scenario Result این Flow باید کاملاً Clickable باشد. 52. Figma Frames پیشنهادی برای اولین Prototype: 01_Dashboard 02_Demand_Create 03_Demand_Result 04_Formation 05_Scheduling 06_Conflict 07_Route_Capacity 08_Bottleneck 09_Scenario 10_Scenario_Result بعد: 11_Network 12_Station 13_Block 14_Train 15_Run_Result 53. Component Library اولیه در Figma ابتدا این Componentها ساخته شوند: AppShell Sidebar Header KPI Card Capacity Card Status Badge Filter Bar Data Table Entity Drawer Inspector Timeline Time-Space Diagram Map Panel Constraint Card Bottleneck Card Scenario Card Stepper Wizard Run Status Validation Message Empty State Error State Loading State 54. Component Variants مثلاً KPI: CapacityCard ├── Physical ├── Operational ├── Route └── Network Status: StatusBadge ├── Feasible ├── Warning ├── Conflict ├── Infeasible └── Pending 55. مهم‌ترین قانون Visual در UI نباید همه چیز Card باشد. خصوصاً: Schedule → Table + Timeline Network → Map Capacity → KPI + Waterfall Conflict → Inspector Scenario → Workspace Demand → Wizard Results → Evidence + Summary هر مسئله باید با مناسب‌ترین Interaction Model نمایش داده شود. 56. Acceptance Test برای UX Prototype Prototype باید بتواند این سناریو را بدون توضیح شفاهی طراح اجرا کند: کاربر: یک تقاضای جدید ثبت می‌کند. سیستم: نیاز واگن را محاسبه می‌کند. کاربر: تشکیل قطار را می‌بیند. سیستم: Schedule تولید می‌کند. کاربر: Conflict را مشاهده می‌کند. سیستم: ظرفیت Route را تولید می‌کند. کاربر: می‌بیند ظرفیت به دلیل Station S03 محدود است. کاربر: Scenario ایجاد می‌کند. سیستم: اثر Scenario را محاسبه می‌کند. کاربر: قبل و بعد را مقایسه می‌کند. اگر این Flow بدون توضیح تیم محصول قابل فهم باشد، UX پایه درست است. 57. سه اصل طلایی Prototype اصل 1 هر عدد مهم باید قابل توضیح باشد. اصل 2 هر محدودیت باید قابل ریشه‌یابی باشد. اصل 3 هر محدودیت مهم باید بتواند به یک What-if Scenario متصل شود. 58. رابطه مستقیم با Model UI نباید یک مدل مستقل بسازد. هر Component باید به Domain Model متصل باشد: Demand UI → Demand Formation UI → TrainFormation Train UI → Train / TrainRun Schedule UI → Schedule / BlockOccupancy Conflict UI → Conflict Capacity UI → CapacityProfile Bottleneck UI → BindingConstraint Scenario UI → Scenario Result UI → Run / Result / Explanation 59. نتیجه نهایی Wireframe نسخه اول Figma را من با این شش صفحه شروع می‌کنم: 1. Dashboard Network Overview + Capacity + Map + Bottleneck + Time-Space 2. Demand Market Request → Transport Requirement 3. Formation Demand → Wagon → Train 4. Scheduling Train → Time-Space → Conflict → Feasible Schedule 5. Route Capacity Schedule → Capacity Proof 6. Scenario Lab Bottleneck → What-if → New Capacity این شش صفحه تقریباً تمام DNA محصول را نشان می‌دهند. 60. اصل نهایی طراحی محصول نباید به کاربر بگوید: «ظرفیت مسیر 27 است.» بلکه باید بتواند بگوید: «ظرفیت مسیر 27 قطار در روز است؛ این عدد از یک برنامه زمان‌بندی معتبر 27 قطاری به دست آمده، قطار 28 به دلیل محدودیت ایستگاه S03 قابل برنامه‌ریزی نیست، و با ایجاد یک امکان تلاقی جدید در S03 ظرفیت بالقوه تا X افزایش می‌یابد.» و UI باید تمام این زنجیره را قابل مشاهده، قابل کلیک و قابل اثبات کند. این تفاوت اصلی بین یک Mockup زیبا و یک Railway Capacity Planning Workbench واقعی است. به نظرم حالا دیگر Spec به اندازه کافی مشخص شده که وارد طراحی خود صفحات شویم. اولویت من برای Figma هم دقیقاً همین ترتیب است: Dashboard → Demand → Formation → Scheduling → Route Capacity → Scenario؛ چون با این شش صفحه، تقریباً کل زنجیره واقعی سیستم را می‌توان به‌صورت یک Prototype قابل کلیک نمایش داد. --- ## ادامه 3 — پرسش کاربر (`message-id:1aa2683f-0645-4b1a-99f3-0aa2a7108134`) بله لطفا --- ## ادامه 3 — پاسخ دستیار (`message-id:e6955960-6297-4672-8609-fc18d1a5942f`) حتماً. با توجه به ساختار فعلی ماکاپ و الگوهای رایج در نرم‌افزارهای حرفه‌ای برنامه‌ریزی ریلی، پیشنهاد می‌کنم مرحله بعد را از یک Wireframe متنی به یک Figma-ready Design Specification تبدیل کنیم؛ یعنی طراح UI بتواند تقریباً بد��ن تصمیم‌گیری مجدد آن را در Figma پیاده کند. در سامانه‌های حرفه‌ای، Time-Distance Diagram، جدول برنامه و Conflict/Blocking View معمولاً در کنار هم قرار می‌گیرند، نه به‌صورت صفحات کاملاً جدا. Toshiba Global +2 trenolab.com +2 ماکاپ فعلی شما هم دقیقاً ظرفیت این تحول را دارد؛ چون همین حالا زنجیره Cb → Cs → Batch → Conflict → Cr → Network → Scenario را نمایش می‌دهد و آن را به بازارگاه متصل کرده است. Motortolid در ادامه، نسخه‌ی Figma-ready را به‌عنوان مبنای طراحی نهایی پیشنهاد می‌کنم: Figma-Ready UI Design Specification — موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی — نسخه 1.0 Figma-Ready UI Design Specification موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی نسخه 1.0 1. هدف طراحی رابط کاربری باید از یک «نمایشگر فرمول‌های ظرفیت» به یک: Railway Capacity Planning & Optimization Workbench تبدیل شود. کاربر باید بتواند مسیر زیر را بدون خروج از محیط سامانه طی کند: Market Request ↓ Demand ↓ Wagon Requirement ↓ Train Formation ↓ Train Service ↓ Schedule Generation ↓ Conflict Resolution ↓ Route Capacity ↓ Network Capacity ↓ Bottleneck ↓ Scenario ↓ Decision اصل اصلی UX: هر عدد ظرفیت باید قابل ردیابی تا برنامه حرکتی و محدودیت ایجادکننده آن باشد. 2. Frame استاندارد مبنای طراحی Desktop: 1440 × 900 px حداقل پشتیبانی: 1280 × 800 نسخه Tablet در MVP اولویت ندارد. 3. Grid Desktop Grid Width: 1440 Sidebar: 248 Content: 1192 Outer margin: 24 Column gap: 16 Card radius: 10–12 ساختار: ┌────────────────────────────────────────────────────────────┐ │ Header │ ├──────────────┬─────────────────────────────────────────────┤ │ │ │ │ Sidebar │ Main Content │ │ │ │ │ │ │ │ │ │ └──────────────┴─────────────────────────────────────────────┘ 4. App Shell Header ارتفاع: 64 px سمت راست: نام سامانه Scenario Data Version Model Version مرکز: Run Status سمت چپ: Notifications Help User دکمه اصلی: ▶ اجرای محاسبه در حالت Running: ⟳ در حال محاسبه... 5. Sidebar ساختار پیشنهادی: داشبورد شبکه و زیرساخت ├─ شبکه ریلی ├─ ایستگاه‌ها ├─ بلوک‌ها └─ مسیرها تقاضا و بازار ├─ درخواست بازار ├─ تقاضا └─ جریان بار قطار و تشکیل ├─ قطارها ├─ تشکیل قطار ├─ واگن └─ لکوموتیو برنامه‌ریزی ├─ برنامه پایه ├─ زمان‌بندی ├─ Time-Space └─ تعارض‌ها ظرفیت ├─ ظرفیت فیزیکی ├─ ظرفیت عملیاتی ├─ ظرفیت مسیر └─ ظرفیت شبکه سناریو ├─ Scenario Builder ├─ Sensitivity └─ Investment نتایج ├─ گلوگاه‌ها ├─ گزارش ظرفیت ├─ Explainability └─ Run History مدیریت داده ├─ Data Quality ├─ Mapping └─ Versioning 6. صفحه P01 — Dashboard هدف Dashboard نباید صرفاً BI Dashboard باشد. باید نشان دهد: What is the capacity? Why is it limited? Where is it limited? What can change it? Layout ┌─────────────────────────────────────────────────────────────┐ │ KPI │ KPI │ KPI │ KPI │ KPI │ ├─────────────────────────────┬───────────────────────────────┤ │ │ │ │ Network Map │ Bottleneck Center │ │ │ │ ├─────────────────────────────┴───────────────────────────────┤ │ │ │ Time-Space / Train Graph │ │ │ ├───────────────────────┬──────────────────────┬──────────────┤ │ Demand │ Capacity │ Alerts │ └───────────────────────┴──────────────────────┴──────────────┘ 7. KPI Cards پنج KPI اصلی: Physical Capacity Cb 128 trains/day Operational Capacity Cs 91 trains/day Route Capacity Cr 76 trains/day Network Capacity Cn 64 trains/day Allocated Capacity Allocated 51 trains/day زیر هر عدد: ↓ 8% vs Baseline یا: +6 trains/day Scenario S-014 8. Capacity Waterfall یک کامپوننت کلیدی: Physical 128 ↓ Operational 91 ↓ Route 76 ↓ Network 64 ↓ Allocated 51 اما نباید صرفاً نمودار باشد. با کلیک روی هر مرحله: Capacity Inspector باز شود. 9. Capacity Inspector Drawer سمت راست: Route Capacity 76 trains/day Status: Feasible Generated Schedule: ✓ Binding Constraint: Station S03 Resource: Platform / Crossing Track Marginal Impact: +4 trains/day سپس: View Schedule View Constraint Create Scenario 10. صفحه P02 — Demand / Market Request این صفحه باید نقطه اتصال مستقیم موتور ظرفیت و بازارگاه باشد. Wizard 01 OD 02 Cargo 03 Transport Requirement 04 Wagon 05 Capacity Check 06 Allocation Step 01 — OD Origin [ تهران ] Destination [ خواف ] Departure Window [ 06:00 — 22:00 ] Operating Days [ شنبه ] [ دوشنبه ] [ چهارشنبه ] 11. Step 02 — Cargo Commodity [ سنگ آهن ] Quantity [ 120,000 ton/month ] Load Type [ Bulk ] Priority [ Normal ] 12. Step 03 — Transport Requirement سیستم تبدیل کند: 120,000 ton/month ↓ Required Trips ↓ Required Wagons ↓ Required Train Runs و وضعیت: Market Demand 120,000 t Transportable Demand 98,000 t Capacity-Constrained 76,000 t سه مقدار نباید با هم یکی فرض شوند. 13. Capacity Check نمای اصلی: ┌──────────────────────────────────────────┐ │ Capacity Check │ ├──────────────────────────────────────────┤ │ Required: 82 trains/month │ │ Available: 64 trains/month │ │ Allocatable: 64 trains/month │ │ │ │ Status: PARTIAL │ └──────────────────────────────────────────┘ 14. Why Panel در حالت Partial: چرا تقاضای کامل قابل تخصیص نیست؟ 1. ظرفیت ایستگاه S03 2. تعارض در بلوک B12 3. محدودیت لکوموتیو 4. محدودیت چرخه واگن هر مورد Clickable باشد. 15. صفحه P03 — Train Formation این صفحه از نظر معماری برای مدل ایرانی بسیار مهم است. ساختار: Demand ↓ Wagon Requirement ↓ Formation ↓ Train Formation Builder Train: TH-KHF-023 Origin: تهران Destination: خواف Cargo: سنگ آهن Locomotive: L-204 واگن‌ها: 01 Hopper 02 Hopper 03 Hopper ... 24 Hopper 16. Formation Inspector سمت راست: Formation Validation ✓ Wagon Count ✓ Total Weight ✓ Train Length ✓ Locomotive Compatibility ✓ Route Compatibility ✓ Station Length ✓ Brake Requirement Result: VALID در صورت خطا: INVALID Train length = 612 m Station usable length = 580 m Binding Constraint: Station S03 17. صفحه P04 — Scheduling Workspace این صفحه باید مهم‌ترین صفحه عملیاتی سامانه باشد. چیدمان: ┌──────────────┬──────────────────────────────────────────────┐ │ Train List │ │ │ │ TIME-SPACE │ │ T001 │ │ │ T002 │ ╱ T001 │ │ T003 │ ╱ │ │ T004 │ ───╱──────────── T003 │ │ │ ╲ │ │ │ ╲ T002 │ ├──────────────┴──────────────────────────────────────────────┤ │ Timeline / Zoom / Filters │ └─────────────────────────────────────────────────────────────┘ 18. Time-Space Diagram محور افقی: Time 00:00 04:00 08:00 12:00 16:00 20:00 24:00 محور عمودی: Station / Distance S01 S02 S03 S04 S05 هر Train: T001 ───────────────╱ T002 ─────────────╲ T003 ───────────────╱ این نوع نمایش در ابزارهای برنامه‌ریزی ریلی حرفه‌ای یک الگوی شناخته‌شده است و برای مشاهده حرکت قطار، تعارض و ظرفیت با اهمیت بالایی استفاده می‌شود. 19. Train Inspector با انتخاب قطار: T001 Origin: Tehran Destination: Khowaf Formation: 24 wagons Load: Loaded Departure: 08:15 Arrival: 18:42 Status: FEASIBLE تب‌ها: Summary Stations Blocks Resources Conflicts Formation 20. Conflict Visualization تعارض باید در خود نمودار دیده شود. مثلاً: B12 T001 ───────╱ ╳ T002 ─────╲────── روی Conflict: Conflict C-021 Resource: Block B12 Type: Opposite Direction Train A: T001 Train B: T002 Required Resolution: Delay / Reorder / Batch 21. Conflict Resolution Panel سه گزینه: ① Delay Train A ② Delay Train B ③ Change Directional Batch هر گزینه باید نتیجه را Preview کند: Route Capacity Before: 72 After: 70 یا: No Capacity Change 22. Batch Manager نمای اختصاصی: Direction A → Batch K01 T001 T003 T005 Duration: 01:42 Direction B ← Batch K02 T002 T004 بین Batchها: Switch Time 12 min و: Next Batch Start ≥ Previous Batch End + Switch Time 23. صفحه P05 — Route Capacity این صفحه باید «Capacity Proof» داشته باشد. Header: Route Capacity Tehran → Khowaf Cb 128 Cs 91 Cr 76 24. Capacity Proof بخش مرکزی: Capacity Proof ──────────────────────────── Schedule(F = 76) ✓ FEASIBLE Schedule(F = 77) ✕ INFEASIBLE Binding Constraint: Station S03 Resource: Crossing Track Therefore: Cr = 76 trains/day این قسمت یکی از مهم‌ترین تفاوت‌های محصول با یک داشبورد معمولی است. عدد ظرفیت باید با Schedule قابل اثبات باشد. 25. Feasibility Matrix Constraint Status Block Occupancy ✓ Single Track Conflict ✓ Station Length ✓ Station Crossing ✕ Locomotive Fleet ✓ Wagon Cycle ✓ Empty Wagon Buffer ✓ Operational Window ✓ Policy Constraint ✓ 26. Bottleneck Section Primary Bottleneck Station S03 Impact: -4 trains/day Current: 76 Without Constraint: 80 Marginal Capacity: +4 دکمه: Create Scenario 27. صفحه P06 — Scenario Lab ساختار دو ستونه: BASELINE SCENARIO Station S03 Station S03 Capacity = 76 +1 crossing track Batch = 3 Batch = 4 Switch = 15 min Switch = 10 min پایین صفحه: BASELINE SCENARIO DELTA Cr: 76 Cr: 84 +8 Cn: 64 Cn: 71 +7 28. Scenario Parameter Editor پارامترها: Infrastructure [ ] Add Passing Loop [ ] Add Double Track Station [ ] Add Platform [ ] Increase Usable Length Operation [ ] Change Batch Size [ ] Change Switch Time Fleet [ ] Add Locomotive [ ] Add Wagon Pool Demand [ ] Increase Market Demand 29. Scenario Result نمایش: Capacity Change +8 trains/day Primary Driver: Station S03 Secondary Driver: Reduced Switch Time No Impact: Wagon Pool 30. Sensitivity Chart محور X: Parameter محور Y: Capacity مثلاً: Capacity 90 | ● 85 | ● 80 | ● 75 | ● └──────────────────── Switch Time با Hover: Switch Time = 12 min Cr = 84 31. Bottleneck Center صفحه اختصاصی: Bottleneck Ranking اما منظور از Ranking در اینجا اولویت تصمیم‌گیری سیاسی یا تجاری نیست؛ صرفاً ترتیب فنی محدودیت‌ها بر اساس معیار انتخاب‌شده در مدل است. ستون‌ها: Resource Utilization Binding? Capacity Impact Marginal Gain Scenario مثال: Station S03 92% YES +4 Scenario S14 Block B12 88% YES +3 Scenario S17 Locomotive Pool 81% NO +0 32. Network Capacity در این صفحه، چند Route هم‌زمان نمایش داده شوند: Route Cr Allocated Tehran-Khowaf 76 64 Tehran-Rasht 42 31 Zahedan-Khash 28 21 سپس Shared Resources: Shared Block B12 Demand from routes: A + B + C Available: 64 Allocated: 64 Status: SATURATED 33. Network Map Map باید فقط Map نباشد. روی هر Route: Route Capacity Utilization Bottleneck Demand مثلاً: Tehran ───────── Khowaf Cr: 76 Used: 64 Utilization: 84% Bottleneck: S03 با کلیک: Route Detail 34. Result Package پس از هر Run: Run #2026-00142 Scenario: Baseline Data Version: DV-18 Model Version: MV-07 Solver: Hybrid Status: FEASIBLE تب‌ها: Summary Capacity Schedule Bottlenecks Demand Resources Conflicts Explainability Audit 35. Explainability View این بخش باید کاملاً جدی طراحی شود. مثلاً کاربر بپرسد: چرا ظرفیت مسیر 76 قطار شد؟ سیستم: Cr = 76 ↓ because Station S03 limits crossing sequence ↓ because Opposite-direction Batch K08 requires 14 min additional occupation ↓ because Single-track section B12 ↓ therefore Train 77 cannot be inserted within the planning horizon این همان: Number → Schedule → Resource → Constraint → Cause است. 36. Data Quality Center با توجه به Excel و Access موجود، این صفحه ضروری است. نمایش: Data Quality Train Records 12,482 Mapped Stations 98.7% Unmapped Stations 14 Missing Time 23 Invalid Sequence 7 Unknown Field Semantics 4 برای هر رکورد: Source: Access Table: TrainMovement Field: faultV Status: SEMANTICALLY UNVERIFIED Action: Review Mapping هیچ Field ناشناخته‌ای نباید بدون تأیید وارد مدل تولیدی شود. 37. Baseline Schedule Baseline باید Entity مستقل باشد. Baseline Schedule BS-1405-06 Source: Access + Excel Status: Validated Data Version: DV-18 Period: 1405/06/01 – 1405/06/31 دکمه‌ها: Compare Clone Scenario Validate Use as Baseline 38. Run History Run ID Scenario Result Cr Cn RUN-142 Baseline FEASIBLE 76 64 RUN-143 S03 Upgrade FEASIBLE 84 71 RUN-144 Batch Test FEASIBLE 81 68 RUN-145 Demand +20% INFEASIBLE 76 64 هر Run قابل باز شدن باشد. 39. Global Search Search باید Entity-aware باشد. مثلاً: Search: S03 نتایج: Station S03 Route R-017 Conflict C-021 Scenario S14 Train T001 Block B12 40. Command Palette میانبر: Ctrl + K فرمان‌ها: Create Demand Create Train Formation Open Route Run Capacity Open Time-Space Find Bottleneck Create Scenario Compare Runs 41. وضعیت‌های استاندارد هر Entity باید Status مشخص داشته باشد. Feasible ✓ قابل اجرا Partial ◐ جزئی Infeasible ✕ غیرقابل اجرا Conflict ⚠ دارای تعارض Waiting ◷ در انتظار Unverified ? تأیید نشده 42. Loading State به‌جای Spinner ساده: Generating Schedule... ✓ Loading Infrastructure ✓ Loading Train Types ✓ Loading Demand ✓ Building Graph ✓ Generating Batches ⟳ Resolving Conflicts ○ Calculating Capacity ○ Generating Explanation کاربر باید بداند موتور در چه مرحله‌ای است. 43. Solver Progress برای محاسبات سنگین: Optimization Run Progress ██████████████░░░░ 72% Best Feasible: 76 trains/day Current Bound: 78 Gap: 2.6% Elapsed: 02:41 44. Error State خطا نباید: Calculation Failed باشد. باید: Schedule Generation Failed Reason: No feasible crossing sequence exists. Affected: Route R-017 Resource: Station S03 Suggested Actions: • Change Batch Size • Increase Planning Horizon • Add Passing Capacity • Modify Operating Window 45. UI اصطلاحات اصطلاح فنی و اصطلاح کاربر: Technical UI Cb ظرفیت فیزیکی Cs ظرفیت عملیاتی Cr ظرفیت مسیر Cn ظرفیت شبکه Block بلوک Route مسیر Train Formation تشکیل قطار Train Run حرکت قطار Operational Batch بچ عملیاتی Conflict تعارض Binding Constraint محدودیت تعیین‌کننده Capacity Proof اثبات ظرفیت Bottleneck گلوگاه Scenario سناریو Operational Window پنجره عملیاتی Resource منبع Allocation تخصیص 46. Design Tokens Spacing 4 8 12 16 24 32 48 64 Radius 4 — Input 8 — Button 10 — Card 12 — Panel 16 — Modal Typography RTL-first. پیشنهاد: Font: Vazirmatn / IRANSansX / equivalent enterprise Persian font Hierarchy: Display 28–32 H1 24 H2 20 H3 16 Body 14 Caption 12 Data 14–16 KPI 28–36 47. رنگ‌بندی معنایی رنگ‌ها باید Semantic باشند، نه تزئینی. Neutral Infrastructure Blue Operational / Schedule Green Feasible Amber Warning / Partial Red Conflict / Infeasible Purple Scenario Dark Binding Constraint در Time-Space Diagram رنگ قطارها بهتر است بر اساس: Direction Train Type Load State Status قابل تغییر باشد. 48. اصل مهم Accessibility نباید فقط با رنگ وضعیت را منتقل کنیم. مثلاً: ✓ Feasible ⚠ Conflict ✕ Infeasible ◷ Waiting همراه با رنگ. 49. Interaction Pattern — از عدد تا دلیل سناریوی اصلی: Dashboard ↓ Cr = 76 ↓ Capacity Inspector ↓ Binding Constraint ↓ Station S03 ↓ Time-Space ↓ Conflict C-021 ↓ Batch K08 ↓ Block B12 این باید در تمام سامانه قابل اجرا باشد. 50. Interaction Pattern — از گلوگاه تا راه‌حل Bottleneck ↓ Station S03 ↓ Why? ↓ Crossing Constraint ↓ Create Scenario ↓ Add Passing Capacity ↓ Run ↓ Cr = 84 ↓ Cn = 71 51. Interaction Pattern — از بازار تا ظرفیت Market Request ↓ Demand ↓ OD ↓ Cargo ↓ Wagon Requirement ↓ Train Formation ↓ Schedule ↓ Conflict ↓ Capacity ↓ Allocation این مسیر باید یکی از اصلی‌ترین User Journeyهای محصول باشد. 52. Figma Page Structure فایل Figma: 00 — Cover 01 — Foundations 02 — Components 03 — Patterns 04 — Dashboard 05 — Demand 06 — Train Formation 07 — Scheduling 08 — Route Capacity 09 — Network Capacity 10 — Scenario Lab 11 — Bottleneck 12 — Data Quality 13 — Run History 14 — Prototype 15 — Responsive 53. Component Library کامپوننت‌های اصلی: AppShell Sidebar Header KpiCard CapacityCard StatusBadge DataTable FilterBar Stepper Wizard EntityDrawer Inspector Timeline TimeSpaceDiagram TrainLine ConflictMarker BatchCard ConstraintCard BottleneckCard ScenarioCard MapPanel RunStatus SolverProgress ValidationMessage EmptyState ErrorState LoadingState 54. Prototype Flow Prototype اصلی: 01 Dashboard ↓ 02 Create Demand ↓ 03 Capacity Check ↓ 04 Formation ↓ 05 Scheduling ↓ 06 Conflict ↓ 07 Route Capacity ↓ 08 Bottleneck ↓ 09 Scenario ↓ 10 Scenario Result این باید Demo اصلی محصول باشد. 55. سناریوی Demo پیشنهادی برای Demo واقعی محصول: OD: Tehran → Khowaf Demand: 120,000 ton/month Wagon: 24 wagons/train Train: Heavy Freight Route: Single + Double Track Scenario: Baseline سپس: Market Demand ↓ Wagon Requirement ↓ Train Formation ↓ Schedule ↓ Conflict ↓ Cr ↓ Bottleneck ↓ Scenario ↓ New Cr این Demo باید در کمتر از چند دقیقه قابل اجرا باشد. 56. اصل طلایی طراحی محصول نباید UI به این شکل باشد: Input ↓ Calculate ↓ 76 بلکه: Input ↓ Model ↓ Schedule ↓ Feasibility ↓ 76 ↓ Why 76? ↓ What prevents 77? ↓ What changes 76 → 84? 57. تفاوت محصول با Dashboard Dashboard: Data → Chart این محصول: Data ↓ Model ↓ Schedule ↓ Conflict ↓ Solver ↓ Feasibility ↓ Capacity ↓ Explanation ↓ Scenario ↓ Decision Support 58. معیار پذیرش UI کاربر باید بتواند: یک Market Request ایجاد کند. OD را تعیین کند. تقاضا را به Wagon Requirement تبدیل کند. Formation ایجاد کند. Schedule تولید کند. Time-Space Diagram را ببیند. Conflict را مشاهده کند. Conflict را حل کند. Capacity Route را دریافت کند. Capacity Proof را مشاهده کند. Bottleneck را پیدا کند. Scenario بسازد. نتیجه Scenario را با Baseline مقایسه کند. اثر تغییر را روی Capacity و Network مشاهده کند. مسیر محاسبه عدد ظرفیت تا Constraint را Trace کند. 59. معماری نهایی تجربه کاربر ┌──────────────┐ │ MARKETPLACE │ └──────┬───────┘ ↓ ┌──────────────┐ │ DEMAND │ └──────┬───────┘ ↓ ┌──────────────┐ │ FORMATION │ └──────┬───────┘ ↓ ┌──────────────┐ │ SCHEDULING │ └──────┬───────┘ ↓ ┌─────────┴─────────┐ ↓ ↓ ┌───────────┐ ┌───────────┐ │ TIME-SPACE│ │ CONFLICT │ └─────┬─────┘ └─────┬─────┘ └─────────┬─────────┘ ↓ ┌──────────────┐ │ CAPACITY │ └──────┬───────┘ ↓ ┌──────────────┐ │ BOTTLENECK │ └──────┬───────┘ ↓ ┌──────────────┐ │ SCENARIO │ └──────┬───────┘ ↓ ┌──────────────┐ │ RESULT │ └──────────────┘ 60. نتیجه نهایی طراحی محصول باید سه سطح را هم‌زمان پوشش دهد: Level 1 — Executive Capacity Demand Bottleneck Scenario Level 2 — Planner Route Train Schedule Time-Space Conflict Batch Level 3 — Analyst / Engineer Block Occupation Constraint Solver Sensitivity Calibration Data Quality بنابراین یک Interface واحد باید سه نوع کاربر را بدون تبدیل‌شدن به سه محصول جداگانه پشتیبانی کند. 61. Design North Star جمله‌ای که باید بالای Design System محصول ثبت شود: The system does not merely calculate railway capacity; it generates a feasible operational plan, proves the capacity, explains the binding constraints, and shows how capacity changes under alternative scenarios. و در فارسی: سامانه صرفاً ظرفیت را محاسبه نمی‌کند؛ برنامه عملیاتی قابل‌اجرا را تولید می‌کند، ظرفیت را اثبات می‌کند، محدودیت‌های تعیین‌کننده را توضیح می‌دهد و اثر سناریوهای مختلف بر ظرفیت را نشان می‌دهد. گام بعدی پیشنهادی حالا این Specification به اندازه‌ای دقیق شده که می‌توانیم از مرحله‌ی «شرح UI» وارد طراحی واقعی صفحه شویم. برای این پروژه، من پیشنهاد می‌کنم صفحه‌ی بعدی که واقعاً طراحی شود P04 — Scheduling Workspace باشد؛ چون همان‌جا Train + Train Formation + Station + Block + Batch + Conflict + Time-Space + Schedule یکجا به هم می‌رسند و هسته‌ی واقعی موتور شما را نشان می‌دهد. نمونه‌های حرفه‌ای نیز همین ترکیب جدول/نمودار زمان-مسافت/Conflict را برای برنامه‌ریزی ریلی به‌کار می‌برند. Toshiba Global +2 berkeleysimulation.com +2 پس از آن، P05 Route Capacity + Capacity Proof را طراحی می‌کنیم؛ چون این دو صفحه عملاً قلب تمایز محصول شما با یک داشبورد یا سیستم گزارش‌گیری ساده هستند. --- ## ادامه 4 — پرسش کاربر (`message-id:ce35f1f9-fa61-4704-b1cd-bda87992647c`) بله لطفا --- ## ادامه 4 — پاسخ دستیار (`message-id:b2d69f88-886b-41a7-9fdd-c549d14c2f7a`) عالی. پس می‌رویم سراغ طراحی واقعی هسته‌ی عملیاتی محصول: ابتدا P04 یعنی Scheduling Workspace و بعد P05 یعنی Route Capacity / Capacity Proof. این انتخاب از نظر معماری UX هم منطقی است؛ ابزارهای حرفه‌ای برنامه‌ریزی ریلی معمولاً جدول قطار، Time-Distance Diagram، blocking/occupancy و conflict resolution را در یک محیط تعاملی کنار هم قرار می‌دهند. Toshiba Global +2 www.quattron.com +2 Figma-Ready Detailed Design — P04 Scheduling Workspace و P05 Route Capacity — نسخه 1.0 طراحی تفصیلی Figma P04 — Scheduling Workspace P05 — Route Capacity & Capacity Proof نسخه 1.0 بخش اول — P04 Scheduling Workspace 1. هدف صفحه P04 قلب عملیاتی موتور است. کاربر باید بتواند در یک صفحه: قطارها را ببیند؛ حرکت قطارها را روی Time-Space Diagram مشاهده کند؛ برنامه پایه را با برنامه تولیدشده مقایسه کند؛ تعارض‌ها را تشخیص دهد؛ Batchهای جهت‌دار را ببیند؛ Station / Block / Route را بررسی کند؛ علت Waiting را پیدا کند؛ Conflict را حل یا برای Solver ارسال کند؛ و در نهایت Schedule را به‌عنوان یک برنامه قابل‌اجرا Validate کند. اصل UX: Schedule باید یک Object قابل مشاهده، قابل ویرایش، قابل اعتبارسنجی و قابل اثبات باشد؛ نه صرفاً مجموعه‌ای از زمان‌ها. 2. ساختار صفحه Frame: 1440 × 900 Layout: ┌──────────────────────────────────────────────────────────────┐ │ Header │ ├──────────────┬───────────────────────────────────────────────┤ │ │ Breadcrumb / Route / Scenario │ │ ├───────────────────────────────────────────────┤ │ │ Toolbar │ │ ├──────────────┬────────────────────────────────┤ │ │ │ │ │ Sidebar │ Train List │ Time-Space Diagram │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├──────────────┴────────────────────────────────┤ │ │ Inspector / Conflict / Batch / Resource │ └──────────────┴───────────────────────────────────────────────┘ 3. Header عنوان: برنامه‌ریزی زمان‌بندی زیرعنوان: مسیر: تهران → خواف Scenario: Baseline Schedule: BS-1405-06 سمت راست: ● FEASIBLE یا: ● NEEDS REVIEW یا: ● INFEASIBLE 4. Toolbar ترتیب پیشنهادی: [ + Train ] [ Generate Schedule ] [ Validate ] [ Resolve Conflicts ] [ Compare ] [ Save ] [ Run Solver ] سمت دیگر: Zoom − 100% Zoom + Day Week Custom 5. Filter Bar Direction: [ All ] Load State: [ Loaded ] [ Empty ] Train Type: [ All ] Status: [ All ] Station: [ All ] Conflict: [ Only Conflicts ] Batch: [ All ] فیلترها باید بدون Reload کل صفحه عمل کنند. 6. Train List عرض: 300 px ستون‌ها: Status Train Origin Destination Dep. Arr. Direction Load مثال: ✓ T001 تهران → خواف 08:15 → 18:42 Loaded ⚠ T002 خواف → تهران 09:05 → 19:30 Empty ✓ T003 تهران → خواف 10:20 → 20:15 Loaded 7. Train Row Interaction Hover: T001 Highlight: مسیر قطار روی Time-Space؛ Station calls؛ Block occupation؛ Batch مربوطه؛ Conflictهای مرتبط. Click: Open Train Inspector Double-click: Edit Train Run 8. Time-Space Diagram این مهم‌ترین Component صفحه است. محور X: Time 06:00 08:00 10:00 12:00 14:00 16:00 18:00 20:00 22:00 محور Y: Station S01 Tehran S02 ... S03 ... S04 ... S05 Khowaf 9. Train Path قطار روی نمودار: S01 ● ╲ S02 ╲ ● S03 ╲ ╲ S04 ● ╲ S05 ● برای حرکت قطار: Line = حرکت Node = Arrival/Departure Horizontal segment = Dwell Dashed = Waiting Highlight = Conflict 10. تفاوت حرکت و Occupancy این نکته باید در UI بسیار جدی باشد. نمای ساده: Train Path ──────────────╱ نمای دقیق: Train Path ────────────╱ Block Occupancy ████████████████ کاربر باید بتواند: View: [ Train Path ] [ Blocking Time ] [ Both ] را انتخاب کند. زیرا Time-Distance ساده به‌تنهایی لزوماً همه‌ی occupancy و conflictهای زیرساختی را نشان نمی‌دهد؛ در مدل‌های دقیق‌تر، blocking time و route occupancy برای تشخیص تعارض اهمیت دارند. 11. Blocking Time View در حالت Detailed: B01 ██████████ B02 ███████████ B03 ████████ B04 █████████ اگر دو Occupancy روی یک Resource هم‌پوشانی داشته باشند: ████████ ████████ ↑ CONFLICT 12. Conflict Marker Conflict باید مستقیماً روی نمودار دیده شود. مثلاً: T001 ╲ ╲ X C-021 ╱ ╱ T002 Hover: Conflict C-021 Resource: Block B12 Type: Opposite Direction Severity: Binding Time: 11:42–11:56 Click: Open Conflict Inspector 13. Conflict Inspector Drawer: ┌────────────────────────────────────┐ │ Conflict C-021 │ ├────────────────────────────────────┤ │ Resource │ │ Block B12 │ │ │ │ Train A │ │ T001 → │ │ │ │ Train B │ │ T002 ← │ │ │ │ Occupancy overlap │ │ 11:42 — 11:56 │ │ │ │ Type │ │ Opposite Direction │ │ │ │ Binding Constraint │ │ YES │ └────────────────────────────────────┘ 14. Conflict Resolution پایین Drawer: Resolve Conflict گزینه‌ها: ○ Delay T001 ○ Delay T002 ○ Change Batch ○ Change Crossing Station ○ Re-sequence ○ Automatic Solver هر گزینه باید Preview داشته باشد. 15. Resolution Preview مثلاً: Current T001 arrival S03: 11:42 T002 arrival S03: 11:48 بعد از انتخاب: Delay T002 +8 min T001: 11:42 T002: 11:56 Conflict: ✓ Resolved Route Capacity: 76 → 76 Buffer: 14 → 6 min 16. Conflict Resolution باید فقط «حل شد» نباشد بعد از حل: ✓ Conflict Resolved اما پایین آن: Impact Travel Time: +8 min Buffer: −8 min Route Capacity: No Change Robustness: − این موضوع برای تصمیم‌گیری بسیار مهم است. 17. Batch Panel وقتی کاربر Batch را انتخاب می‌کند: Directional Batch → Tehran to Khowaf Batch K08 T001 T003 T005 T007 Batch Start: 08:15 Batch End: 10:04 Switch: 14 min سپس: ← K09 T002 T004 T006 18. Batch Timeline → Batch K08 ████████████████ Switch │14 min│ ← Batch K09 ███████████████ با Hover: Batch Duration 109 min Trains 4 Average Headway 18 min 19. Station View با کلیک روی Station: Station S03 Incoming: T001 T002 T003 Outgoing: T001 T004 Resources: Platform 1 Platform 2 Crossing Track Entry Route Exit Route 20. Station Occupancy نمای گرافیکی: Track 1 ████████ T001 Track 2 ███████ T002 Crossing █████████ Platform █████ و: Capacity Status Crossing Track: 92% Platform: 71% Entry Route: 84% 21. Block Inspector Block B12 Length: 18.4 km Track: Single Direction: Both Max Speed: 80 km/h Current Occupancy: 64% Critical: YES تب‌ها: Running Time Occupancy Conflicts Trains Capacity 22. Train Run Inspector Train Run T001 Origin: Tehran Destination: Khowaf Load: Loaded Formation: 24 wagons Weight: 1,920 t Length: 612 m Departure: 08:15 Arrival: 18:42 Station Calls: S01 08:15 / 08:25 S02 10:14 / 10:24 S03 12:05 / 12:19 S04 15:10 / 15:20 S05 18:42 23. Waiting Analysis Waiting باید جداگانه قابل مشاهده باشد. مثلاً: T001 Running Time: 08:42 Dwell: 00:48 Conflict Waiting: 00:22 Operational Waiting: 00:15 Total: 10:07 این امکان مستقیماً به تحلیل ظرفیت و گلوگاه کمک می‌کند. 24. Schedule Quality Panel بالای صفحه: Schedule Quality Feasibility ✓ Conflicts 0 Buffer 8.4% Waiting 6.2% Station Usage 78% Critical Blocks 2 25. Schedule Validation دکمه: Validate Schedule نتیجه: ✓ Running Time ✓ Dwell ✓ Headway ✓ Block Occupancy ✓ Station Capacity ✓ Train Length ✓ Locomotive ✓ Wagon Cycle ✓ Empty Return ✓ Operational Window ✓ Policy 26. Validation Failure مثلاً: ✕ Station Capacity Train: T007 Station: S03 Train Length: 612 m Usable Length: 580 m و: Action: Open Station 27. Schedule Compare دو Schedule: Baseline Scenario S14 T001 08:15 T001 08:15 T002 09:05 T002 09:17 T003 10:20 T003 10:29 T004 11:15 T004 11:42 روی Time-Space: Baseline ────────╱ Scenario ──────────╱ 28. Schedule Diff فقط تغییرات: Changed: T002 +12 min T004 +27 min Batch K08 +1 train Conflict C021 Resolved 29. Automatic Schedule Generation دکمه: Generate Schedule Modal: Generate Feasible Schedule Demand: 82 trains/day Planning Horizon: 24 h Objective: ○ Maximize Throughput ○ Minimize Waiting ○ Maximize Buffer ○ Balanced Constraints: ☑ Infrastructure ☑ Station ☑ Fleet ☑ Wagon ☑ Operational Windows ☑ Policy [ Generate ] 30. Solver Progress پس از Run: Generating Schedule ✓ Infrastructure loaded ✓ Train types loaded ✓ Demand loaded ✓ Candidate paths generated ✓ Batches generated ⟳ Resolving conflicts ⟳ Evaluating schedules ○ Capacity proof ○ Explanation 31. Schedule Result Schedule Generated Feasible: YES Trains: 76 Conflicts: 0 Minimum Buffer: 6 min Average Waiting: 8.4 min Critical Resource: S03 دکمه: Open Schedule بخش دوم — P05 Route Capacity 32. هدف P05 نباید فقط یک KPI Page باشد. این صفحه باید پاسخ دهد: «حداکثر چند قطار را می‌توان واقعاً در این Route اجرا کرد؟» تعریف UI: Cr = Maximum F such that Schedule(F) is Feasible 33. Route Capacity Layout ┌──────────────────────────────────────────────────────────────┐ │ Route: Tehran → Khowaf │ ├────────────┬────────────┬────────────┬──────────────────────┤ │ Cb │ Cs │ Cr │ Demand │ │ 128 │ 91 │ 76 │ 82 │ ├────────────┴────────────┴────────────┴──────────────────────┤ │ │ │ Capacity Proof │ │ │ ├────────────────────────────┬─────────────────────────────────┤ │ Generated Schedule │ Binding Constraints │ ├────────────────────────────┴─────────────────────────────────┤ │ Feasibility Matrix │ └──────────────────────────────────────────────────────────────┘ 34. Capacity Cards Physical Cb = 128 Operational Cs = 91 Route Cr = 76 Demand D = 82 و: Unserved Demand = 6 trains/day 35. Capacity Proof بخش اصلی صفحه: CAPACITY PROOF F = 76 ✓ FEASIBLE F = 77 ✕ INFEASIBLE Therefore: Cr = 76 36. Why F=77 Failed کاربر روی 77 کلیک کند: Why Infeasible? Primary Constraint: Station S03 Secondary: Block B12 Conflict: C-031 Affected Trains: T077 T021 Required: 14 min Available: 9 min 37. Proof Chain نمایش بصری: Cr = 76 ↓ Schedule(76) ↓ Feasible ↓ Schedule(77) ↓ Conflict C031 ↓ Station S03 ↓ Crossing Capacity ↓ Cr = 76 این باید Signature Component محصول باشد. 38. Capacity Search برای تعداد قطار: 76 ✓ 77 ✕ اگر Solver با Binary Search کار کند: 64 ✓ 96 ✕ 80 ✕ 72 ✓ 76 ✓ 78 ✕ 77 ✕ و در نهایت: Maximum Feasible = 76 39. Feasibility Matrix Constraint 76 77 Block Occupancy ✓ ✓ Station Crossing ✓ ✕ Headway ✓ ✕ Train Length ✓ ✓ Locomotive ✓ ✓ Wagon Cycle ✓ ✓ Empty Return ✓ ✓ Operational Window ✓ ✕ 40. Binding Constraint کارت: Binding Constraint Station S03 Crossing Capacity Current Cr: 76 Without Constraint: 80 Marginal Capacity: +4 41. Bottleneck Chain Route ↓ Station S03 ↓ Crossing ↓ Single Track B12 ↓ Directional Batch K08 ↓ Switch Time ↓ Train 77 rejected 42. Hidden Capacity اگر ظرفیت ظاهراً استفاده نشده باشد: Physical: 128 Route: 76 Used: 64 Unused: 12 ولی: Unused ≠ Available سیستم باید توضیح دهد: 12 train/day nominal unused Reason: No feasible insertion window 43. Capacity Release مثلاً: Current: Cr = 76 Potential: Reduce Switch Time +3 Increase Station Crossing +4 Additional Locomotive +0 Additional Wagon Pool +0 44. Scenario Shortcut از هر Bottleneck: [ Create Scenario ] سناریو: Scenario S14 Change: Station S03 +1 Crossing Track سپس: Run Scenario نتیجه: Cr 76 → 80 ΔCr = +4 45. Route Capacity vs Demand نمودار: Demand ████████████████████ 82 Capacity █████████████████ 76 Allocated ████████████████ 64 سه مفهوم باید جدا بمانند: Market Demand Transportable Demand Allocated Demand 46. Capacity Profile Capacity فقط یک عدد نباشد. Filter: Direction Load State Train Type Commodity Time Window Day Season مثلاً: Loaded → 76 Empty ← 58 یا: Day Saturday 76 Sunday 71 Monday 78 47. Capacity by Time Window 00–06 31 06–12 44 12–18 39 18–24 47 اما Capacity نهایی باید بر اساس تعریف Planning Horizon و منطق مدل تولید شود، نه جمع ساده این اعداد. 48. Capacity by Direction برای Single Track: → Direction Batch Capacity ← Direction Batch Capacity Combined Feasible Schedule نمایش: → 38 ← 34 ──────── Total 72 ولی اگر تعامل زمانی وجود داشته باشد: Independent Sum: 72 Network-feasible: 68 و UI باید دلیل اختلاف را نشان دهد. 49. Route Capacity Report خروجی: Route Capacity Report Route: Tehran → Khowaf Period: 1405/06 Physical Capacity: 128 Operational Capacity: 91 Route Capacity: 76 Demand: 82 Allocated: 64 Unserved: 18 50. Evidence پایین گزارش: Evidence Schedule: SCH-2026-0142 Data Version: DV-18 Model Version: MV-07 Scenario: Baseline Solver Run: RUN-142 Validation: PASSED 51. Audit Trail کاربر باید بتواند ببیند: Who: Capacity Analyst When: 1405/06/18 14:32 Action: Generated Route Capacity Inputs: DV-18 Model: MV-07 Result: Cr = 76 Validation: Passed 52. Export گزینه‌ها: Export PDF Export Excel Export CSV Export JSON Share Result PDF باید شامل: Executive Summary Capacity Schedule Bottlenecks Capacity Proof Scenario Evidence باشد. 53. Route Capacity UX Journey Select Route ↓ Select Scenario ↓ Select Demand ↓ Generate Schedule ↓ Validate ↓ Calculate Cr ↓ Find F+1 ↓ Identify Binding Constraint ↓ Generate Capacity Proof ↓ Explain ↓ Create Scenario 54. مهم‌ترین تفاوت UX سیستم نباید بگوید: Capacity = 76 و تمام. باید بگوید: Capacity = 76 because: Schedule(76) ✓ Schedule(77) ✕ Primary constraint: Station S03 Evidence: Conflict C031 Potential improvement: +4 trains/day 55. Component مشترک P04/P05 Capacity Proof Card این Component را به‌عنوان یک Design System Component مستقل بسازید: ┌─────────────────────────────────────┐ │ CAPACITY PROOF │ │ │ │ 76 trains/day │ │ │ │ Schedule(76) ✓ FEASIBLE │ │ Schedule(77) ✕ INFEASIBLE │ │ │ │ Binding: Station S03 │ │ Impact: +4 trains/day │ │ │ │ [View Schedule] [Why?] [Scenario] │ └─────────────────────────────────────┘ 56. Component مشترک Constraint Card ┌───────────────────────────────────┐ │ Station S03 │ │ │ │ Crossing Capacity │ │ │ │ Utilization 92% │ │ Binding YES │ │ Impact +4 trains/day │ │ │ │ [Inspect] [Scenario] │ └───────────────────────────────────┘ 57. Component مشترک Train Graph Mini-View داخل Inspector: S01 ● ╲ S02 ╲ ● S03 ╲ X S04 ╲ ● S05 Click: Open in Scheduling Workspace 58. Component مشترک Explainability Breadcrumb Capacity > Route > Station S03 > Block B12 > Conflict C031 > Train T077 این Breadcrumb باید در کل سامانه استاندارد شود. 59. دو صفحه، یک مدل ذهنی P04: How do trains move? P05: How many trains can move? و ارتباط: P04 Schedule ↓ Feasibility ↓ P05 Capacity نه: P05 Capacity ↓ Fake Schedule 60. قانون معماری UI Rule 01 No Feasible Schedule ↓ No Operational Capacity Rule 02 No Validated Schedule ↓ No Accepted Capacity Result Rule 03 No Verified Data Mapping ↓ No Production Use Rule 04 Every Capacity Number ↓ Must Have Evidence 61. Prototype اتصال دو صفحه Prototype اصلی: Dashboard ↓ Route Capacity ↓ Cr = 76 ↓ Capacity Proof ↓ Schedule(76) ↓ Scheduling Workspace ↓ Conflict C031 ↓ Station S03 ↓ Create Scenario ↓ Station Upgrade ↓ Run ↓ Cr = 80 ↓ Compare این Prototype باید Demo اصلی محصول باشد. 62. Figma Frames در فایل Figma: P04.01 Scheduling Overview P04.02 Train Inspector P04.03 Time-Space P04.04 Blocking Time P04.05 Conflict Inspector P04.06 Conflict Resolution P04.07 Batch Manager P04.08 Station Inspector P04.09 Schedule Validation P04.10 Schedule Compare P05.01 Route Capacity P05.02 Capacity Proof P05.03 Feasibility Matrix P05.04 Bottleneck P05.05 Capacity Profile P05.06 Scenario Shortcut P05.07 Evidence P05.08 Capacity Report 63. Prototype Test یک کاربر Planner باید بتواند: 1. Route را انتخاب کند 2. Schedule را باز کند 3. T001 را پیدا کند 4. Time-Space را مشاهده کند 5. Conflict را پیدا کند 6. Conflict را باز کند 7. علت را ببیند 8. راه‌حل انتخاب کند 9. Schedule را Validate کند 10. Route Capacity را باز کند 11. Cr را ببیند 12. Proof را باز کند 13. F+1 را مشاهده کند 14. Binding Constraint را پیدا کند 15. Scenario بسازد 16. Scenario را Run کند 17. Capacity جدید را مقایسه کند اگر کاربر برای انجام این مسیر مجبور به برگشت‌های زیاد بین صفحات شود، Information Architecture هنوز بهینه نیست. 64. Design Principle نهایی P04 باید نشان دهد: قطارها چگونه حرکت می‌کنند؟ P05 باید نشان دهد: حداکثر چند قطار می‌توانند واقعاً حرکت کنند؟ و Scenario باید نشان دهد: اگر چیزی را تغییر دهیم، چند قطار بیشتر می‌توانند حرکت کنند و چرا؟ این سه سؤال در کنار هم هسته‌ی UX موتور ظرفیت را تشکیل می‌دهند. نتیجه این دو صفحه حالا می‌توانند به‌عنوان مرکز ثقل Prototype قابل‌نمایش به مدیر، برنامه‌ریز، تیم فنی و حتی بازارگاه عمل کنند. از نظر الگوی صنعتی هم این جهت‌گیری با ابزارهای حرفه‌ای هم‌راستاست: محیط‌های برنامه‌ریزی ریلی تعاملی معمولاً Time-Distance/Graphical Timetable را با جدول، blocking، conflict detection و سناریو ترکیب می‌کنند؛ بعضی محصولات نیز ظرفیت را مستقیماً روی timetable و occupancy تحلیل می‌کنند. Toshiba Global +2 www.quattron.com +2 گام بعدی که پیشنهاد می‌کنم: تبدیل همین مشخصات به یک Visual Mockup واقعی 1440×900 برای P04 Scheduling Workspace و P05 Route Capacity است؛ یعنی دیگر فقط Wireframe نباشد و ظاهر واقعی Dashboard، Sidebar، Time-Space Diagram، کارت‌های ظرفیت، Conflict Inspector و Capacity Proof را ببینیم. --- ## ادامه 5 — پرسش کاربر (`message-id:9d5fb13c-9c67-45f2-91bb-9b5726554137`) بله لطفا --- ## ادامه 5 — پاسخ دستیار (`message-id:7f7273f4-f454-42f2-b661-0f00a684f7e0`) It looks like image creation is temporarily unavailable. Do you want to try something else? The image-generation tool is unavailable because your Instant limit has been reached, and the Mini model is text-only. I won’t retry it until you send a new request. --- ## ادامه 6 — پرسش کاربر (`message-id:94400044-e4e4-4467-9ed5-df9a89671f7b`) تا همینجا چقدر از نظر کیفیت به نرم افزارهای معتبر این موضوع نزدیک هستیم؟ --- ## ادامه 6 — پاسخ دستیار (`message-id:b359a674-22c3-4a4b-84b8-f46ab7932b1d`) بله؛ و اگر بخواهم واقع‌بینانه و بدون تعریف اضافه ارزیابی کنم، تا اینجا از نظر معماری مفهومی، مدل ظرفیت، UX و مسیر محصول به یک نرم‌افزار حرفه‌ای این حوزه نزدیک شده‌ایم؛ اما از نظر موتور محاسباتی واقعی، عمق شبیه‌سازی، داده عملیاتی، اعتبارسنجی و بلوغ Production هنوز فاصله مهمی داریم. من وضعیت فعلی را این‌طور می‌بینم: جمع‌بندی خیلی کوتاه اگر نرم‌افزارهای معتبر بین‌المللی را به‌عنوان مرجع 100 در نظر بگیریم: حوزه وضعیت فعلی ما Concept / Domain Model 85–90% Capacity Methodology 80–85% Scheduling Concept 75–80% UX / Product Concept 70–80% Explainability 80–90% Scenario / Sensitivity 75–80% Data Architecture 75–85% GIS / Spatial Model 55–65% Solver / Optimization Engine 35–50% Microscopic Simulation 20–35% Real Railway Calibration 20–30% Interoperability 40–55% Production Readiness 25–35% این درصدها رتبه‌بندی علمی یا Benchmark رسمی نیستند؛ برآورد مهندسی من از میزان بلوغ فعلی طرح نسبت به دامنه قابلیت‌هایی است که در ابزارهای تخصصی وجود دارد. و یک نکته خیلی مهم: از نظر «فکر و معماری محصول»، فاصله‌مان خیلی کمتر از فاصله‌مان از نظر «پیاده‌سازی موتور واقعی» است. 1. جایی که واقعاً خوب جلو آمده‌ایم مهم‌ترین نکته این است که ما از مسیر اشتباه شروع نکرده‌ایم. خیلی از پروژه‌ها از اینجا شروع می‌کنند: Excel ↓ Dashboard ↓ Chart ↓ Capacity Number ما به این مدل رسیده‌ایم: Market ↓ Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Train Run ↓ Schedule ↓ Conflict ↓ Feasibility ↓ Capacity ↓ Bottleneck ↓ Scenario ↓ Decision این تفاوت بسیار مهم است. 2. از نظر Domain Model اینجا وضعیت ما به نظرم بسیار خوب است. ما Entityهای زیر را از هم جدا کرده‌ایم: Train TrainRun TrainFormation TrainFormationItem TrainService OperationalBatch Route RouteSegment Block Station StationOperation Schedule BaselineSchedule Demand FreightFlow WagonRequirement WagonCycle LocomotiveCycle Capacity Constraint Conflict Scenario Allocation Result Explanation این سطح از تفکیک، برای یک پروژه جدی ضروری است. حتی railML هم در مدل خود حوزه‌های Infrastructure، Timetable، Rolling Stock، Interlocking و Visualization را جدا می‌کند. GitLab +1 بنابراین از نظر Domain Thinking، ما در مسیر درستی هستیم. 3. یک نقطه قوت مهم‌تر: Train را با TrainRun قاطی نکردیم این موضوع ممکن است ظاهراً ساده باشد، ولی بسیار مهم است. ما گفته‌ایم: Train ≠ Train Run ≠ Train Formation ≠ Operational Batch یعنی: Train ↓ Formation ↓ Run ↓ Schedule ↓ Batch این دقیقاً همان نوع تفکیکی است که وقتی سیستم از Prototype به سیستم واقعی تبدیل می‌شود، جلوی بسیاری از مشکلات معماری را می‌گیرد. 4. OD-centric بودن برای ایران این یکی از مهم‌ترین تصمیم‌های معماری ماست. ما نگفتیم: سیستم باید از ابتدا مثل شبکه‌های بسیار متراکم اروپایی با Dynamic Formation طراحی شود. بلکه گفتیم: Origin ↓ Train Formation ↓ Movement ↓ Destination ↓ Unload ↓ Empty Wagon Return و در عین حال: Detailed Intermediate Operational Path را نگه داشتیم. این برای داده واقعی شما تصمیم درستی است. 5. از نظر Capacity Model اینجا نیز به نظرم به سطح خوبی رسیده‌ایم. ما ظرفیت را یک عدد ساده نکرده‌ایم. بلکه: Cb ↓ Cs ↓ Cr ↓ Cn و مهم‌تر: Cr ≠ min(Block Capacity) بلکه: C r ​ =max{F:Schedule(F) is feasible} این یک جهش مفهومی مهم است. چون Capacity در عمل حاصل تعامل زمان، قطار، زیرساخت، ایستگاه، تعارض و منابع است. 6. جایی که UX ما از یک Dashboard معمولی جلوتر رفته این قسمت را واقعاً باید حفظ کنیم. مثلاً به جای: Route Capacity = 76 می‌گوییم: Cr = 76 Schedule(76) ✓ Schedule(77) ✕ Binding Constraint: Station S03 Conflict: C031 Resource: Block B12 این یعنی: Capacity Proof این ایده از نظر محصول بسیار قدرتمند است. 7. Explainability به نظرم این یکی از نقاط تمایز بالقوه محصول ماست. مثلاً: Cr = 76 ↓ Schedule(77) infeasible ↓ Conflict C031 ↓ Station S03 ↓ Crossing Capacity ↓ Block B12 ↓ Directional Batch ↓ Switch Time کاربر می‌تواند از: عدد → دلیل → منبع → محدودیت حرکت کند. این چیزی است که من توصیه می‌کنم در محصول نهایی تبدیل به یکی از Signature Features شود. 8. اما کجا هنوز فاصله داریم؟ اینجا بخش مهم‌تر است. بزرگ‌ترین فاصله: موتور واقعی Scheduling ما الان Specification خوبی برای Scheduling Engine داریم. ولی هنوز باید واقعاً بسازیم: Infrastructure Graph ↓ Candidate Paths ↓ Train Movement Model ↓ Resource Occupancy ↓ Conflict Graph ↓ Scheduling Algorithm ↓ Feasible Schedule این دیگر UI نیست. این قلب نرم‌افزار است. 9. Solver هنوز بخش اصلی کار است ما تصمیم خوبی گرفته‌ایم که Solver را Abstract کنیم: Solver Interface │ ┌────┼────────┐ ↓ ↓ ↓ MILP CP-SAT Heuristic │ │ └──── Simulation اما هنوز باید مشخص شود: چه چیزی با MILP حل شود؟ مثلاً: Train frequency Fleet allocation Flow allocation Capacity expansion Network optimization چه چیزی با CP-SAT؟ مثلاً: Detailed scheduling Conflict ordering Time windows Resource constraints چه چیزی با Heuristic؟ مثلاً: Large network scheduling Batch generation Fast feasible solution Warm start و Simulation کجا وارد شود؟ برای: Microscopic validation Delay propagation Robustness Dispatching Disturbance scenarios این قسمت هنوز باید مهندسی شود. 10. فاصله بزرگ بعدی: Microscopic Simulation اینجا ابزارهایی مثل OpenTrack وارد حوزه‌ای می‌شوند که ما هنوز در آن کامل نیستیم. OpenTrack خودش را برای شبیه‌سازی عمیق عملیات ریلی، تحلیل تأخیر و تعارض و استفاده به‌عنوان Digital Twin معرفی می‌کند و API برای اتصال سیستم‌های دیگر دارد. Opentrack ما فعلاً بیشتر در این سطح هستیم: Macroscopic + Operational Scheduling در حالی که برای بلوغ بالاتر باید به: Macroscopic + Mesoscopic + Microscopic Simulation برسیم. 11. اما آیا باید همین الان Simulation بسازیم؟ نه. اتفاقاً توصیه من این است که فعلاً نسازیم. ابتدا: Capacity Engine + Scheduling Engine + Network Optimization را کاملاً معتبر کنیم. بعد Simulation را به‌عنوان: Validation / Digital Twin Layer اضافه کنیم. 12. فاصله مهم دیگر: Interlocking / Signalling مدل فعلی ما: Block Station Route Occupancy Conflict است. ولی در سطح حرفه‌ای‌تر باید به: Signal Route Locking Interlocking Overlap Release Route Setting Signal Aspect Train Protection هم برسیم. جالب اینکه حتی railML نیز در ساختار فعلی خود Interlocking را به‌عنوان یک زیرمدل مستقل دارد. GitLab پس این باید در Roadmap باشد. اما باز هم: برای MVP موتور ظرفیت، لازم نیست از روز اول کل Interlocking واقعی را مدل کنیم. 13. Rolling Stock در این قسمت نیز مسیر خوبی داریم، ولی باید عمیق‌تر شویم. فعلاً: Locomotive Wagon Length Weight Speed Load State داریم. اما مدل حرفه‌ای باید بتواند مواردی مثل: Traction Braking Axle Load Train Length Train Weight Speed Profile Brake Capability Vehicle Compatibility Formation Locomotive Position را هم وارد محاسبات کند. حتی در مدل‌های داده‌ای railML برای قطار/rolling stock مواردی مانند طول، تعداد واگن، وزن، سرعت، بار محوری و قابلیت ترمز مطرح می‌شوند. GitLab 14. Interoperability اینجا هنوز فاصله داریم. ما معماری Adapter را طراحی کرده‌ایم: Excel Access railML RailDax Other Systems ↓ Canonical Model ↓ Capacity Engine این بسیار خوب است. اما هنوز Adapter واقعی نداریم. railML امروز فقط یک ایده تئوریک نیست؛ توسعه آن همچنان ادامه دارد و نسخه‌های جدید 3.x در سال 2026 نیز در حال توسعه‌اند. GitLab پس باید: Canonical Model ↕ railML Adapter را واقعاً پیاده کنیم. 15. یک نکته بسیار مهم درباره Benchmark من اصلاً توصیه نمی‌کنم محصول را با این ذهنیت بسازیم: «باید RailSys را کپی کنیم.» یا: «باید OpenTrack را کپی کنیم.» یا: «باید Viriato را کپی کنیم.» چون هرکدام فلسفه و حوزه متفاوتی دارند. بهتر است: International Best Practices ↓ Capability Map ↓ Iranian Railway Domain ↓ Our Product یعنی ما Capabilityها را Benchmark کنیم، نه UI یا محصول را کپی کنیم. 16. چیزی که در محصول ما بالقوه متفاوت است من این زنجیره را بسیار مهم می‌بینم: Marketplace ↓ Demand ↓ Capacity ↓ Formation ↓ Schedule ↓ Allocation ↓ Marketplace یعنی Capacity Engine مستقیماً به Market وصل است. در نتیجه محصول فقط: Railway Capacity Software نیست. می‌تواند به: Railway Capacity + Freight Planning + Market Allocation Platform تبدیل شود. این بخش را باید جدی بگیریم. 17. معماری نهایی که الان در ذهن من شکل گرفته به نظرم محصول باید نهایتاً پنج Engine داشته باشد: ┌────────────────────┐ │ MARKET ENGINE │ └─────────┬──────────┘ ↓ ┌────────────────────┐ │ DEMAND / FLOW │ └─────────┬──────────┘ ↓ ┌────────────────────────────────────────────────┐ │ CAPACITY PLATFORM │ │ │ │ Infrastructure Engine │ │ Scheduling Engine │ │ Optimization Engine │ │ Simulation Engine │ │ │ └────────────────────────────────────────────────┘ ↓ ┌────────────────────┐ │ ALLOCATION ENGINE │ └─────────┬──────────┘ ↓ ┌────────────────────┐ │ MARKETPLACE │ └────────────────────┘ و در کنار همه: GIS Data Platform Explainability Scenario Engine Audit / Versioning 18. اگر بخواهم صادقانه وضعیت پروژه را بگویم ما الان یک نرم‌افزار حرفه‌ای ساخته‌شده نداریم. اما چیزی مهم‌تر از آن داریم: یک Product Architecture و Mathematical/Operational Blueprint داریم که قابلیت تبدیل شدن به یک نرم‌افزار حرفه‌ای را دارد. این تفاوت بسیار مهم است. 19. سه مرحله‌ای که پیش رو داریم Phase A — چیزی که تقریباً انجام داده‌ایم ✓ Domain Model ✓ Capacity Model ✓ Mathematical Model ✓ Data Mapping ✓ Architecture ✓ SOW ✓ Requirements ✓ UX Architecture ✓ Wireframes ✓ Explainability Concept ✓ Scenario Concept ✓ Marketplace Integration Concept این مرحله تقریباً بالغ است. 20. Phase B — چیزی که الان باید بسازیم این قسمت مهم‌ترین مرحله است: Canonical Database ↓ Data Import ↓ Infrastructure Graph ↓ Train Model ↓ Schedule Engine ↓ Conflict Engine ↓ Route Capacity Solver ↓ Network Optimizer ↓ Capacity Proof من این را MVP واقعی موتور می‌دانم. 21. Phase C — سطح Enterprise بعد: Microscopic Simulation + Digital Twin + Advanced Signalling + Interlocking + Real-time Data + Delay Propagation + Robustness + Advanced Optimization + International Interoperability آنجاست که واقعاً وارد قلمرو سیستم‌های بسیار پیشرفته می‌شویم. 22. یک معیار بسیار مهم اگر فردا کسی بپرسد: «چطور مطمئن شویم Capacity = 76 واقعی است؟» نسخه فعلی معماری ما باید بتواند جواب دهد: Data Version = DV-18 Scenario = Baseline Model Version = MV-07 Route = R017 Demand = D-142 Formation = F-882 Schedule = SCH-991 Schedule(76) = FEASIBLE Schedule(77) = INFEASIBLE Binding Constraint = S03 Conflict = C031 Resource = B12 اگر این زنجیره را واقعاً در نرم‌افزار پیاده کنیم، محصول از یک Capacity Calculator به یک Engineering Decision System تبدیل می‌شود. 23. بنابراین ارزیابی من اگر سه محور داشته باشیم: Current ● / \ / \ / \ Concept / \ Implementation / \ / \ ●---------------● Domain Production از نظر: Concept / Architecture: خیلی جلو هستیم. از نظر: Mathematical / Operational Model: جلو هستیم. از نظر: UX/Product Design: مسیر بسیار خوبی داریم. از نظر: Actual Solver: هنوز ابتدای راه اصلی هستیم. از نظر: Simulation: هنوز فاصله قابل توجهی داریم. از نظر: Production Railway Software: هنوز فاصله داریم. اما مهم‌ترین حرف من به نظرم نباید الان UI را بیش از این جلو ببریم. ماکاپ فعلی به اندازه کافی برای شروع توسعه مفهومی شده است. حتی سایت فعلی شما هم زنجیره ظرفیت فیزیکی تا شبکه، Batch، Conflict، Scenario و بازارگاه را نمایش می‌دهد. Motortolid از اینجا به بعد بیشترین ارزش را این کار خواهد داشت: UI │ ▼ API │ ▼ Canonical Domain Model │ ▼ Scheduling Engine │ ├── Conflict Engine │ ├── Batch Engine │ ├── Station Capacity │ ├── Wagon/Loco Cycle │ ▼ Route Capacity Engine │ ▼ Network Optimization │ ▼ Capacity Proof │ ▼ UI یعنی مرحله بعدی را از Figma به Engine ببریم. اگر بخواهیم واقعاً به سطح نرم‌افزارهای معتبر نزدیک شویم، به نظرم سند بعدی که باید تولید کنیم «Software Engineering Specification برای Scheduling Engine» است؛ شامل مدل داده عملیاتی، ساخت Graph، محاسبه Running/Blocking Time، Conflict Graph، Batch Generation، الگوریتم تولید Schedule، Feasibility Checker، Capacity Search و Pseudocode/Interfaces. این دقیقاً جایی است که فاصله فعلی بین «طراحی بسیار خوب» و «محصول واقعاً قابل اجرا» شروع به کم‌شدن می‌کند. --- ## ادامه 7 — پرسش کاربر (`message-id:71667e8c-58c3-4742-a897-77d3e6418668`) بله لطفا --- ## ادامه 7 — پاسخ دستیار (`message-id:de0b452f-d2e5-45e9-8ca2-b1254cd3e620`) حتماً. در این مرحله پیشنهاد می‌کنم از طراحی مفهومی و UI عبور کنیم و وارد سند پیاده‌سازی‌پذیر هسته زمان‌بندی (Scheduling Engine) شویم. این دقیقاً همان لایه‌ای است که فاصله بین «مدل ریاضی خوب» و «نرم‌افزار واقعی در سطح حرفه‌ای» را پر می‌کند. برای هم‌راستایی با اکوسیستم‌های حرفه‌ای، در این سند Infrastructure / Rolling Stock / Timetable از یکدیگر تفکیک شده‌اند؛ این تفکیک با ساختار railML نیز هم‌خوان است. railML در حال حاضر نیز در خط توسعه 3.4 قرار دارد و RailTopoModel را به‌عنوان مدل منطقی اشیای ریلی دنبال می‌کند. GitLab +2 GitLab +2 همچنین، لایه Scheduling را عمداً از Simulation جدا کرده‌ام؛ رویکردهایی مانند OpenTrack نشان می‌دهند که Simulation/Digital Twin می‌تواند بعداً به‌عنوان لایه اعتبارسنجی و ارزیابی عملیاتی به Scheduling متصل شود، نه اینکه الزاماً جای آن را بگیرد. Opentrack Software Engineering Specification — هسته زمان‌بندی و تولید برنامه حرکت موتور ظرفیت حمل بار ریلی — نسخه 1.0 سند مشخصات مهندسی نرم‌افزار هسته زمان‌بندی و تولید برنامه حرکت Scheduling & Feasible Timetable Engine محصول: موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی دامنه: Railway Capacity Generation, Estimation & Network Optimization Engine نسخه سند: 1.0 وضعیت: Software Engineering Specification معماری مبنا: Canonical Railway Domain Model + Time-Space Scheduling مدل عملیاتی مبنا: OD-Centric Train Formation + Detailed Intermediate Operational Path 1. هدف سند این سند مشخص می‌کند که چگونه مدل مفهومی و ریاضی موتور ظرفیت باید به یک Scheduling Engine واقعی و قابل پیاده‌سازی تبدیل شود. هدف این Engine صرفاً تولید ساعت حرکت و رسیدن نیست. هدف اصلی عبارت است از: برای مجموعه‌ای از قطارها، تقاضاها، مسیرها، منابع، محدودیت‌ها و قواعد عملیاتی، یک برنامه حرکت زمانی–مکانی معتبر تولید، بررسی و در صورت امکان بهینه کند. در نتیجه: Feasible\ Time\text{-}Space\ Plan ] و ظرفیت عملیاتی مسیر به صورت مستقیم به قابلیت تولید برنامه معتبر وابسته است: [ C_r= \max {F:\ Schedule(F)\ is\ feasible} ] بنابراین عدد ظرفیت بدون Schedule معتبر، صرفاً یک برآورد نظری است. اصل حاکم: [ \boxed{ No\ Feasible\ Schedule \Rightarrow No\ Operational\ Capacity } ] و برای نتیجه نهایی: [ \boxed{ No\ Validated\ Schedule \Rightarrow No\ Accepted\ Capacity\ Result } ] 2. جایگاه Scheduling Engine در معماری کلان معماری پیشنهادی: Marketplace │ ▼ Market Request │ ▼ Demand / Freight Flow │ ▼ Wagon Requirement │ ▼ Train Formation │ ▼ Canonical Railway Domain Model │ ▼ ┌───────────────────────────────┐ │ Schedule Preparation │ │ │ │ - Train Path Generation │ │ - Running Time Calculation │ │ - Station Operation │ │ - Resource Mapping │ └───────────────┬───────────────┘ │ ▼ Time-Space Model │ ▼ Conflict Graph │ ▼ Scheduling Engine │ ┌────────┴────────┐ ▼ ▼ Constructive Optimization Scheduler Solver │ │ └────────┬────────┘ ▼ Feasibility Checker │ ▼ Validated Schedule │ ┌───────┴────────┐ ▼ ▼ Route Capacity Network Capacity │ │ └───────┬────────┘ ▼ Capacity Proof │ ▼ Explanation Layer │ ▼ UI/API Scheduling Engine نباید مستقیماً با Excel، Access یا Marketplace صحبت کند. تمام این سیستم‌ها باید از طریق Canonical Domain Model و Adapter/Service Layer متصل شوند. 3. مرز Scheduling Engine 3.1 مسئولیت‌های Engine Scheduling Engine مسئول موارد زیر است: ساخت Train Path محاسبه زمان حرکت بین نقاط محاسبه Block Occupancy مدل‌سازی Station Operation تشخیص منابع مشترک تشخیص Conflict مدل‌سازی Single Track مدل‌سازی Double Track مدل‌سازی Mixed Track مدیریت Direction مدیریت Loaded/Empty مدیریت Batch مدیریت Switch Time مدیریت Operational Window رعایت Train Length رعایت Train Weight رعایت Station Length رعایت Locomotive Constraints رعایت Wagon Cycle رعایت Fixed/Baseline Movement تولید Schedule Validation Conflict Resolution محاسبه Feasibility تولید Evidence برای Capacity Proof 4. موارد خارج از مسئولیت مستقیم موارد زیر نباید در Scheduling Engine دفن شوند: مدیریت کاربران مدیریت قراردادهای بازار قیمت‌گذاری پرداخت مدیریت مشتری GIS Rendering Data Import مستقیم از Excel Data Cleaning عمومی مدیریت اسناد BI عمومی Simulation میکروسکوپی کامل مدیریت تعمیرات به‌عنوان سیستم مستقل اما Scheduling Engine باید API و Data Contract لازم برای اتصال این لایه‌ها را فراهم کند. 5. مدل عملیاتی پایه مدل عملیاتی این پروژه باید بر اساس ساختار زیر باشد: OD-Centric Train Formation + Detailed Intermediate Operational Path یعنی: قطار از نظر تجاری و ظرفیت، دارای: Origin Destination Commodity Load State Wagon Requirement Train Formation است؛ ولی از نظر عملیاتی دارای: Station 1 Station 2 Station 3 ... Block 1 Block 2 Block 3 ... Time In Time Take Time Out Running Time Dwell Waiting Conflict است. این تفکیک بسیار مهم است. 6. موجودیت‌های اصلی 6.1 Train تعریف منطقی قطار. Train ├── TrainId ├── TrainNo ├── TrainType ├── Origin ├── Destination ├── Direction ├── LoadState └── FormationId 6.2 TrainRun یک اجرای مشخص از یک Train Service. TrainRun ├── TrainRunId ├── TrainId ├── OperatingDate ├── PlannedDeparture ├── PlannedArrival └── ScheduleId 6.3 TrainFormation تشکیل فیزیکی قطار. TrainFormation ├── FormationId ├── WagonCount ├── TotalLength ├── GrossWeight ├── LocomotiveCount ├── BrakeConfiguration └── FormationItems 6.4 TrainStationCall حرکت قطار در یک ایستگاه: TrainStationCall ├── TrainRunId ├── StationId ├── Sequence ├── TimeIn ├── TimeTake ├── TimeOut ├── RequiredWait └── Track/Platform 7. Operational Batch TrainFormation با OperationalBatch یکی نیست. Formation مشخص می‌کند: قطار از چه واگن‌ها و لکوموتیوهایی تشکیل شده است؟ Batch مشخص می‌کند: چند قطار در یک جهت و در یک بازه زمانی به‌صورت گروهی برنامه‌ریزی می‌شوند؟ مدل: [ K=(r,d,t_s,t_e,N,H) ] که: (r): Route (d): Direction (t_s): Start Time (t_e): End Time (N): Number of trains (H): Headway 8. Canonical Input Contract Scheduling Engine باید فقط از Canonical Model ورودی بگیرد. ورودی‌های اصلی: Infrastructure Network Node Station Segment Block Route Junction Signal Track Platform Rolling Stock Wagon Locomotive Train Type Formation Length Weight Speed Profile Brake Capability Demand Market Demand Transportable Demand Allocated Demand OD Commodity Wagon Requirement Operations Operating Pattern Operational Window Baseline Schedule Fixed Movement Station Operation Constraints Infrastructure Constraint Station Constraint Rolling Stock Constraint Wagon Constraint Locomotive Constraint Policy Constraint Demand Constraint Time Constraint 9. Data Preparation Layer قبل از اجرای Solver باید یک مرحله مستقل به نام: Schedule Preparation وجود داشته باشد. این مرحله داده خام را به داده قابل حل تبدیل می‌کند. Pipeline: Canonical Data ↓ Validation ↓ Normalization ↓ Route Expansion ↓ Train Path Construction ↓ Running Time ↓ Station Operation ↓ Block Occupancy ↓ Resource Mapping ↓ Conflict Candidate Generation ↓ Scheduling Model 10. Route Expansion برای هر OD: Origin → Destination مسیر باید به اجزای زیر Expand شود: Route ├── RouteSegment │ ├── Segment │ ├── Block │ ├── Direction │ └── RunningTime │ ├── RouteStation │ ├── Station │ ├── ArrivalOperation │ ├── Dwell │ ├── Crossing │ ├── Overtaking │ └── Departure 11. Running Time Engine Running Time نباید یک عدد ثابت برای هر مسیر باشد. حداقل: \sum_i \frac{L_i}{V_i} ] و: \frac{L_{total}}{T_{run}} ] اما در نسخه Production: f( L, SpeedProfile, TrainType, LoadState, Weight, Gradient, Curvature, Restriction, Direction ) ] خواهد بود. 12. Blocking Time برای هر Block باید Occupancy Time مشخص شود. مدل پایه: T_{run} + T_{entry} + T_{clear} + T_{release} ] و در مدل ساده‌تر: [ T_b= T_{run,b} + T_{dwell,b} + T_{operational,b} ] اما این دو نباید با هم قاطی شوند. Running Time زمان حرکت است. Blocking Time زمانی است که Resource واقعاً برای سایر قطارها غیرقابل استفاده است. این تفاوت برای Capacity بسیار مهم است. 13. Time-Space Model Scheduling Engine باید یک مدل Time-Space داخلی بسازد. محور: X = Time Y = Location / Station / Distance هر Train یک Path دارد: Train A Station 1 @ 08:00 │ ├──── Block 1 │ Station 2 @ 08:18 │ ├──── Block 2 │ Station 3 @ 08:42 این مدل باید امکان استخراج موارد زیر را بدهد: Train Path Running Dwell Waiting Crossing Overtaking Conflict Block Occupancy Station Occupancy 14. Resource Model هر چیزی که می‌تواند بین دو حرکت مشترک باشد باید به عنوان Resource مدل شود. مثلاً: Block Track Station Track Platform Junction Single Track Section Crossing Point Signal Section Loading Track Unloading Track Formation Track Locomotive Wagon Pool Terminal مدل عمومی: [ ResourceUsage(train,resource,t_{start},t_{end}) ] 15. Single Track Single Track باید به عنوان یک Resource Conflict واقعی مدل شود. اگر دو قطار در جهت مخالف باشند: A → B B → A هر دو نمی‌توانند هم‌زمان از بخش مشترک عبور کنند. شرط پایه: [ t_i^{exit}\le t_j^{enter} ] یا: [ t_j^{exit}\le t_i^{enter} ] در حالت عمومی‌تر: [ Order(i,j)= {i\prec j,\ j\prec i} ] و Solver باید یکی را انتخاب کند. 16. Crossing Station در Single Track، ایستگاه‌های مناسب Crossing Point هستند. برای هر Station باید مشخص شود: CanCross CanOvertake CanWait CanFormTrain CanUnload CanLoad UsableTrackLength NumberOfTracks بنابراین Station صرفاً Node جغرافیایی نیست. Station یک Operational Resource است. 17. Double Track در Double Track: Direction A → B Track 1 Direction B → A Track 2 ظرفیت جهت‌دار می‌تواند جدا شود. اما این به معنی حذف Conflict نیست. هنوز ممکن است منابع مشترک وجود داشته باشند: Junction Station Platform Signal Terminal Crossing Route پس: [ DoubleTrack \neq ConflictFree ] 18. Mixed Track Mixed Track یکی از مهم‌ترین حالت‌های واقعی سیستم است. مثلاً: Station A │ Double Track │ Station B │ Single Track │ Station C │ Double Track │ Station D Engine باید بتواند Track Regime را در سطح Segment/Block تغییر دهد. مدل: Route ├── Segment 1 → DOUBLE ├── Segment 2 → DOUBLE ├── Segment 3 → SINGLE ├── Segment 4 → SINGLE └── Segment 5 → DOUBLE این یکی از نقاط مهم تفاوت موتور ما با یک مدل ساده Headway Calculator است. 19. Operational Regime برای Single Track چند Regime قابل تعریف است: Regime A — Alternating Direction A Direction B Direction A Direction B Regime B — Directional Batch A A A A Switch B B B B Regime C — Optimized Mixed ترکیبی که Solver تعیین می‌کند. هدف: [ \max Capacity ] یا: [ \min Delay ] یا: [ \min Total\ Travel\ Time ] 20. Switch Time بعد از پایان یک Direction Batch: [ Start_{new} \ge End_{previous} + T_{switch} ] ولی: [ T_{switch} ] نباید صرفاً یک عدد دلخواه باشد. می‌تواند شامل: Last Train Clearance Signal Release Route Release Station Preparation Operational Confirmation Direction Change Dispatching Preparation باشد. در نسخه Production بهتر است: SwitchTimeProfile وجود داشته باشد. 21. Batch Generation Batch Size باید Variable باشد. مثلاً: N = 1 N = 2 N = 3 ... N = Nmax و برای هر Batch: T_{first} + (N-1)H + T_{clear} ] یا بر اساس مدل دقیق‌تر: t_{last,exit} t_{first,entry} ] Solver باید بین Batchهای مختلف انتخاب کند. 22. چرا بزرگ‌ترین Batch همیشه بهترین نیست؟ چون ممکن است: Batch 8 از نظر ظرفیت خام خوب باشد ولی باعث: Waiting زیاد تأخیر Direction مخالف اشغال Station کاهش انعطاف افزایش Delay کاهش Robustness شود. بنابراین: [ N_{optimal} \neq N_{maximum} ] 23. Station Operation Model برای هر TrainStationCall باید عملیات زیر قابل مدل‌سازی باشد: Arrival Dwell Inspection Brake Test Loading Unloading Locomotive Operation Formation Crossing Overtaking Waiting Crew Operation Fueling Operational Preparation Departure اما در مدل عمومی: ActivityType + Duration + Resource + Window + Constraint ] 24. Operational Availability Window به جای اینکه برای هر نوع فعالیت منطق جداگانه در Engine نوشته شود: MaintenanceWindow FuelingWindow BrakeTestWindow PrayerWindow ShiftWindow InspectionWindow همه باید به یک مفهوم عمومی تبدیل شوند: Operational Availability Window مثلاً: OperationalWindow ├── ResourceId ├── Start ├── End ├── ActivityType ├── Mandatory └── Priority 25. Train Length Constraint اگر: [ L_{train}>L_{usableStation} ] آنگاه Train نمی‌تواند در آن Station به شکل موردنظر توقف کند، مگر اینکه Alternative Operation تعریف شده باشد. پس: [ L_{train}\le L_{usableStation} ] یک Constraint واقعی است. 26. Train Weight همین منطق برای Weight نیز برقرار است. [ W_{train} \le W_{max,route} ] یا بسته به Segment: [ W_{train} \le W_{max,segment} ] بنابراین Train Formation مستقیماً بر Schedule اثر می‌گذارد. 27. Loaded / Empty Scheduling Engine باید Load State را بخشی از Train Type بداند. مثلاً: Loaded O→D Empty D→O اما ممکن است: Loaded Empty PartiallyLoaded LocomotiveOnly Maintenance نیز وجود داشته باشد. پس: [ TrainType= (Direction, LoadState, Length, Weight, SpeedProfile) ] 28. Wagon Cycle Capacity فقط تابع Infrastructure نیست. اگر تعداد واگن موجود محدود باشد: [ W_{available} 1: Mid = floor((Low + High)/2) result = Schedule(Mid) if result.feasible: Low = Mid else: High = Mid Cr = Low اما در مدل‌های پیچیده، باید Monotonicity به‌صورت تجربی/ساختاری بررسی شود. 58. Capacity Proof خروجی Capacity نباید فقط: Cr = 76 باشد. باید: Capacity Proof داشته باشد. مثلاً: F = 76 ✓ Feasible F = 77 ✕ Infeasible Primary Constraint: Station S03 Secondary: Block B12 Conflict: C031 Reason: Insufficient crossing opportunity 59. Capacity Proof Chain Cr = 76 ↓ Schedule(F=76) ↓ Validated ↓ Schedule(F=77) ↓ Conflict ↓ Station S03 ↓ Crossing Constraint ↓ Capacity Limit این زنجیره باید در Database ذخیره شود. 60. Network Scheduling برای Network: [ \max \sum_r F_r ] با محدودیت: [ F_r\le C_r ] و منابع مشترک: [ \sum_r a_{rg}F_r \le C_g ] اما در سطح Schedule باید این محدودیت‌ها به Time-Space تبدیل شوند. یعنی: Route Capacity به تنهایی کافی نیست. Network Scheduler باید حرکت‌های Routeها را روی منابع مشترک قرار دهد. 61. Shared Resource مثلاً: Route A Route B Route C همگی از: Station S استفاده می‌کنند. پس: [ Usage_A+ Usage_B+ Usage_C \le Capacity_S ] 62. Network Capacity تعریف: [ C_n= \max \left{ \sum_r Q_r: FeasibleNetworkSchedule \right} ] این ظرفیت باید با Schedule شبکه‌ای اثبات شود. 63. Marketplace Integration Marketplace نباید مستقیماً Solver را صدا بزند. معماری: Marketplace ↓ Market API ↓ Demand Adapter ↓ Canonical Demand ↓ Capacity Engine ↓ Scheduling Engine ↓ Feasible Capacity ↓ Allocation ↓ Marketplace 64. سه نوع Demand باید سه مفهوم حفظ شوند: [ D_{market} \neq D_{transportable} \neq D_{allocated} ] Market Demand آنچه بازار درخواست کرده. Transportable Demand آنچه با ناوگان و شبکه قابل حمل است. Allocated Demand آنچه پس از ظرفیت و سیاست واقعاً تخصیص یافته. 65. Capacity Profile ظرفیت یک عدد ساده نیست. مدل: [ C= f( Route, OD, TrainType, Commodity, LoadState, Time, Direction, Station, Wagon, Locomotive ) ] بنابراین API ظرفیت باید بتواند Profile برگرداند. 66. APIهای اصلی Generate Schedule POST /api/v1/schedules/generate Validate Schedule POST /api/v1/schedules/{id}/validate Get Conflicts GET /api/v1/schedules/{id}/conflicts Resolve Conflict POST /api/v1/conflicts/{id}/resolve Calculate Route Capacity POST /api/v1/capacity/routes/{routeId}/calculate Capacity Proof GET /api/v1/capacity/runs/{runId}/proof Run Scenario POST /api/v1/scenarios/{scenarioId}/run 67. Schedule Result Contract نمونه: { "scheduleId": "SCH-1405-001", "scenarioId": "BASELINE", "modelVersion": "1.0", "dataVersion": "2026.09", "status": "VALIDATED", "objectiveValue": 124.8, "trains": 76, "feasible": true, "violations": [], "resourceUsage": {}, "conflicts": [], "bindingConstraints": [], "explanationId": "EXP-001" } 68. Failed Schedule Result { "scheduleId": "SCH-1405-002", "status": "INFEASIBLE", "feasible": false, "failedConstraints": [ { "constraintId": "CON-S03-001", "type": "STATION_CAPACITY", "resourceId": "S03", "severity": "HARD" } ], "conflicts": [ { "conflictId": "C031", "trainA": "T76", "trainB": "T77" } ] } 69. Explanation Trace هر نتیجه باید قابل توضیح باشد. مثلاً: Capacity = 76 Why not 77? → Train T77 cannot be inserted → Block B12 unavailable → Because T76 occupies B12 → T76 cannot be moved earlier → Station S03 is occupied → Alternative crossing station S04 unavailable → Therefore F=77 infeasible 70. Explanation Object Explanation ├── Result ├── Question ├── Evidence ├── Constraint ├── Resource ├── Related Trains ├── Alternative Scenarios └── Conclusion 71. Bottleneck Detection Bottleneck فقط Resource با بیشترین Utilization نیست. باید بررسی شود: [ \Delta C ] مثلاً: Station S03 utilization = 82% ولی اگر افزایش ظرفیت S03 باعث: [ \Delta C=0 ] شود، S03 الزاماً Bottleneck مؤثر نیست. 72. Marginal Capacity Impact برای هر Resource: C^{after}_n C^{before}_n ] مثلاً: Station S03: Capacity Increase = +2 Block B12: Capacity Increase = +0 Wagon Pool: Capacity Increase = +5 این داده مستقیماً وارد Scenario Engine می‌شود. 73. Scenario Engine سناریو باید بتواند تغییرات زیر را اعمال کند: Add Track Double Track Increase Station Length Add Crossing Station Increase Wagon Pool Add Locomotive Change Running Time Change Switch Time Change Operational Window Change Demand Change Train Type Change Batch Policy و سپس کل Scheduling را دوباره اجرا کند. 74. اصل مهم Scenario سناریو نباید فقط با فرمول: Capacity + X محاسبه شود. بلکه: Scenario ↓ Network Update ↓ Schedule Regeneration ↓ Validation ↓ Capacity Recalculation باشد. 75. Baseline vs Scenario هر Run باید به Version متصل باشد: Baseline Scenario A Scenario B Scenario C و قابل مقایسه باشد: Cr Cn Demand Served Delay Waiting Empty Movement Bottleneck Utilization 76. Run Management هر اجرای Solver باید Run داشته باشد: Run ├── RunId ├── ScenarioId ├── DataVersion ├── ModelVersion ├── Solver ├── SolverVersion ├── Parameters ├── StartTime ├── EndTime ├── Status └── Result 77. Reproducibility یک Result معتبر باید قابل بازتولید باشد. بنابراین: [ Result= f( DataVersion, ModelVersion, Scenario, Parameters, SolverVersion ) ] است. اگر یکی از این‌ها تغییر کند، Result جدید باید تولید شود. 78. Data Lineage برای هر پارامتر مهم: Source ↓ Mapping ↓ Canonical Field ↓ Scheduling Parameter ↓ Constraint ↓ Result باید قابل Trace باشد. 79. Excel Mapping مثلاً: شماره قطار از مبدا → Train.TrainNo ساعت حرکت از مبدا → TrainRun.PlannedDeparture ساعت ورود به مقصد → TrainRun.PlannedArrival شماره قطار از مقصد → Return TrainRun اما هر Mapping باید تأیید شود. اصل: No Verified Mapping → No Production Use 80. Access Mapping فیلدهای: StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir faultV باید ابتدا به Data Dictionary و سپس به Canonical Model Mapping شوند. معنای فیلدهایی مانند: time_take RequiredWait seir faultV نباید توسط Engine حدس زده شود. تا زمانی که Semantic Validation انجام نشده: Status = UNVERIFIED 81. Calibration Running Time Engine باید با داده واقعی Calibration شود. مثلاً: Predicted Running Time vs Actual Running Time و: T_{actual} ] شاخص‌هایی مانند: MAE RMSE Bias P95 Error باید محاسبه شوند. 82. Validation Dataset داده واقعی باید به چند دسته تقسیم شود: Training / Calibration Validation Test برای مثال: Historical Period A → Calibration Historical Period B → Validation Historical Period C → Blind Test 83. Schedule Validation Schedule باید با برنامه واقعی مقایسه شود. شاخص‌ها: Departure Error Arrival Error Running Time Error Dwell Error Conflict Agreement Station Occupancy Agreement Train Count Agreement 84. Test Architecture Testها باید چهار سطح داشته باشند. Unit Test مثلاً: RunningTimeCalculator ConflictDetector BatchGenerator Integration Test Route + Station + Block Scenario Test Single Track Mixed Track Loaded/Empty End-to-End Demand → Formation → Schedule → Capacity → Proof 85. نمونه Test Case CASE-SCH-001 موضوع: دو قطار مخالف در Single Track Input: Train A → B Train B → A Shared Block = B12 Expected: No overlapping occupancy 86. CASE-SCH-002 موضوع: Crossing Station Expected: Both trains pass No resource conflict Station occupancy valid 87. CASE-SCH-003 موضوع: Station Length اگر: [ L_{train}>L_{station} ] Expected: INFEASIBLE Constraint = STATION_LENGTH 88. CASE-SCH-004 موضوع: Empty Wagon Availability اگر: [ E_D(t)= previous_exit) model.Add( exit == entry + running_time(train, block) ) previous_exit = exit 46. Conflict Model for block in single_track_blocks: trains_on_block = relevant_trains(block) for a, b in pairwise(trains_on_block): if opposite_direction(a, b): order = model.NewBoolVar( f"order_{a.id}_{b.id}_{block.id}" ) model.Add( exit[a, block] + separation(block) <= entry[b, block] ).OnlyEnforceIf(order) model.Add( exit[b, block] + separation(block) <= entry[a, block] ).OnlyEnforceIf(order.Not()) 47. Station Constraint اگر طول قطار از خط مفید بیشتر باشد: [ L_{train}>L_{usable} ] آن Station/Track نباید انتخاب شود. در Solver: model.Add( train_length <= station_usable_length ) یا در حالت Track Assignment: model.Add( train_length <= usable_length ).OnlyEnforceIf(track_selected) 48. Objective نسخه اول نباید با یک Objective پیچیده شروع شود. اول: [ Feasible ] سپس: [ \min TotalWaiting ] سپس: [ \min TotalTravelTime ] سپس: [ \max DemandServed ] اما برای Production باید Multi-Objective/lexicographic داشته باشیم. ترتیب پیشنهادی: 1. Feasibility 2. Fixed Movement Compliance 3. Demand Served 4. Capacity Utilization 5. Total Waiting 6. Total Travel Time 7. Robustness 49. Feasibility First یک اشتباه معماری ممنوع: Objective = minimize delay قبل از اینکه Schedule feasible باشد. در ابتدا: Find Any Feasible Schedule بعد Optimization. 50. Independent Feasibility Checker حتی اگر CP-SAT جواب: OPTIMAL داد، نباید آن را مستقیماً Accepted Schedule بدانیم. Pipeline: CP-SAT ↓ Candidate Schedule ↓ Feasibility Checker ↓ Validated Schedule Feasibility Checker باید مستقل از Solver باشد. 51. Feasibility Layers F1 — Structural Route exists Block sequence valid Station exists Train exists Formation exists F2 — Temporal entry < exit precedence dwell headway F3 — Resource single-track conflict station conflict junction conflict F4 — Operational maintenance window fueling inspection brake test station availability F5 — Fleet wagon availability locomotive availability wagon cycle locomotive cycle F6 — Policy mandatory movement minimum service market priority direction quota 52. Conflict Graph قبل از Solver: [ G_C=(V_C,E_C) ] که: Node = Train Path Edge = Potential Conflict مثلاً: TR01 ───── TR02 │ │ │ │ TR03 ───── TR04 Edge attributes: resource block station direction minimum separation conflict type 53. Conflict Types SINGLE_TRACK BLOCK_OVERLAP HEADWAY STATION_TRACK JUNCTION PLATFORM LOADING_LINE UNLOADING_LINE FORMATION OPERATIONAL_WINDOW ROUTE_CLOSURE TRAIN_LENGTH 54. Capacity Engine پس از ساخت Scheduler: [ C_r=\max{F:\ Schedule(F)\ is\ feasible} ] اما این (F) باید به Schedule واقعی متصل باشد. مثلاً: F = 27 Schedule(27) = FEASIBLE F = 28 Schedule(28) = INFEASIBLE پس: Cr = 27 55. Capacity Search برای MVP: Upper Bound ↓ Feasibility Check ↓ Binary Search مثلاً: LB = 0 UB = theoretical upper bound سپس: mid = floor((LB + UB) / 2) اگر feasible: LB = mid اگر infeasible: UB = mid - 1 56. Capacity Proof Object { "routeId": "R-TEH-KHF-01", "capacity": 27, "proof": { "feasibleSchedule": { "flow": 27, "scheduleId": "SCH-027", "status": "FEASIBLE" }, "infeasibleSchedule": { "flow": 28, "status": "INFEASIBLE" } }, "bindingConstraints": [ { "type": "SINGLE_TRACK_CONFLICT", "resource": "B12" }, { "type": "STATION_CROSSING", "resource": "S03" } ] } 57. Infeasibility Explanation صرف: INFEASIBLE کافی نیست. باید: Why? را هم استخراج کنیم. مثلاً: F = 28 infeasible Primary constraint: Station S03 Reason: No feasible crossing order exists for TR-021 and TR-022. Affected resource: S03 / Crossing Line 2 Required separation: 18 min Available window: 11 min 58. Business Explanation تفاوت: Solver Explanation و: Business Explanation Solver ممکن است بگوید: constraint_1842 violated Business Layer باید بگوید: ظرفیت مسیر به دلیل محدودیت عبور قطارهای دوطرفه در ایستگاه S03 افزایش پیدا نکرد. گزینه‌های اصلاح: 1. افزایش ظرفیت خط ایستگاه 2. تغییر ترتیب عبور 3. افزایش طول بازه بهره‌برداری 4. تغییر Batch Direction 5. سرمایه‌گذاری زیرساختی 59. Scheduling API POST /scheduling/runs { "scenarioId": "BASELINE", "routeId": "R01", "trainRunIds": [ "TR001", "TR002", "TR003" ], "solver": { "type": "CP_SAT", "timeLimitSeconds": 120 } } Response: { "runId": "RUN001", "status": "RUNNING" } 60. Schedule Result API GET /scheduling/runs/{runId} Response: { "runId": "RUN001", "status": "FEASIBLE", "scheduleId": "SCH001", "trainCount": 27, "conflictCount": 0 } 61. Capacity API POST /capacity/routes/{routeId}/calculate Request: { "scenarioId": "BASELINE", "constraints": { "demandEnabled": true, "fleetEnabled": true, "wagonCycleEnabled": true, "locomotiveCycleEnabled": true }, "search": { "method": "BINARY" } } 62. Capacity Result API { "routeId": "R01", "physicalCapacity": 96, "operationalCapacity": 71, "routeCapacity": 64, "availableCapacity": 17, "proof": { "acceptedFlow": 64, "nextFlow": 65, "nextFlowStatus": "INFEASIBLE" }, "bindingConstraint": { "type": "STATION_CROSSING", "resource": "S03" } } 63. First MVP Solver Scope نسخه اول CP-SAT نباید همه چیز را یک‌جا حل کند. Phase 1 Route Block Train Direction Running Time Single Track Double Track Headway Station Crossing Earliest Departure Latest Arrival Phase 2 Batch Operational Window Station Track Assignment Train Length Station Capacity Phase 3 Wagon Cycle Locomotive Cycle Demand Selection Network Shared Resources Phase 4 Robustness Delay Propagation Scenario Optimization Investment Network-wide Optimization 64. اولین Vertical Slice برای اینکه پروژه وارد Implementation شود، اولین Vertical Slice باید کوچک و واقعی باشد. پیشنهاد: Sangan → Foolad با: 5–10 Block 3–5 Station 5–15 Train Run Single Track Double Track 1–2 Crossing Station Loaded Train Empty Train Baseline Schedule و سپس: Baseline ↓ Canonical Model ↓ Train Paths ↓ Conflict Graph ↓ CP-SAT ↓ Feasible Schedule ↓ Capacity Search ↓ Capacity Proof 65. Golden Test حداقل یک Schedule معتبر باید به‌عنوان Golden Schedule ذخیره شود. مثلاً: CASE-CP-001 Input: 5 trains Expected: FEASIBLE Expected conflicts: 0 Expected order: TR01 TR03 TR02 TR04 TR05 و: CASE-CP-002 Input: 10 trains Expected: INFEASIBLE Expected bottleneck: Station S03 66. تست حیاتی Capacity Proof CASE-CAP-001 F = 10 → FEASIBLE F = 11 → FEASIBLE F = 12 → FEASIBLE F = 13 → INFEASIBLE Expected Cr = 12 این تست از صدها تست UI مهم‌تر است. 67. تست Single Track Train A: OUTBOUND Block B01 06:00–06:40 Train B: INBOUND Block B01 06:20–07:00 نتیجه: INFEASIBLE زیرا: 06:20 < 06:40 پس باید یکی جابه‌جا شود. 68. تست Double Track Train A: OUTBOUND B01-UP Train B: INBOUND B01-DOWN اگر B01 دوخطه و Resource مشترک دیگری وجود نداشته باشد: FEASIBLE 69. تست Mixed Track B01 SINGLE B02 DOUBLE B03 SINGLE دو قطار ممکن است: B01 conflict B02 simultaneous B03 conflict بنابراین Conflict Graph باید در هر Segment متفاوت باشد. این تست برای معماری ما بسیار مهم است. 70. Repository Structure پیشنهاد: rail-capacity-engine/ │ ├── domain/ │ ├── train/ │ ├── route/ │ ├── station/ │ ├── wagon/ │ └── demand/ │ ├── canonical/ │ ├── mapper/ │ ├── validator/ │ └── contracts/ │ ├── scheduling/ │ ├── preparation/ │ ├── path/ │ ├── blocking/ │ ├── conflict/ │ ├── batch/ │ └── feasibility/ │ ├── solver/ │ ├── interface/ │ ├── cp_sat/ │ ├── milp/ │ └── heuristic/ │ ├── capacity/ │ ├── route/ │ ├── network/ │ ├── proof/ │ └── explanation/ │ ├── scenario/ │ ├── gis/ │ ├── api/ │ ├── persistence/ │ └── tests/ ├── unit/ ├── integration/ ├── golden/ ├── regression/ └── performance/ 71. Interface اصلی Scheduler ISchedulingEngine ورودی: SchedulingProblem خروجی: SchedulingResult مثلاً: class ISchedulingEngine: def solve( self, problem: SchedulingProblem ) -> SchedulingResult: ... 72. Interface Solver class ISchedulingSolver: def build_model( self, scheduling_model ): ... def solve( self, time_limit_seconds ): ... def get_solution( self ): ... CP-SAT فقط یکی از implementationهاست: ISchedulingSolver │ ├── CPSATSolver ├── MILPSolver ├── HeuristicSolver └── HybridSolver 73. چرا این Abstraction مهم است؟ زیرا ممکن است: CP-SAT برای: Route Scheduling مناسب باشد، اما برای: Network Optimization یک MILP یا Hybrid Method بهتر باشد. یا برای: Real-time Rescheduling یک Heuristic سریع‌تر باشد. بنابراین Domain Model نباید به OR-Tools وابسته شود. 74. Data Lineage هر Result باید بتواند به عقب برگردد: Capacity Result ↓ Scheduling Run ↓ Scheduling Model ↓ Canonical Data Version ↓ Source Dataset ↓ Excel / Access / API مثلاً: Cr = 64 Model Version: CAP-1.0.0 Solver: CP-SAT Data Version: DATA-1405-06-31 Baseline: BS-1405-06 Scenario: BASELINE 75. نسخه‌بندی حداقل: Data Version Model Version Algorithm Version Solver Version Scenario Version Schedule Version باید جدا باشند. 76. اصل مهم در ارتباط با Standards Canonical Model ما نباید railML را جایگزین مدل داخلی کند. بهتر است: railML ↓ railML Adapter ↓ Canonical Model و: Canonical Model ↓ railML Exporter ↓ railML این موضوع مهم است، چون railML اساساً برای تبادل داده بین سیستم‌های ریلی طراحی شده است، نه اینکه الزاماً Domain Model داخلی موتور ما را تعیین کند. حتی در کاربردهای واقعی، داده‌های زیرساخت، وسایل نقلیه و timetable به‌عنوان داده‌های قابل تبادل مستقل استفاده می‌شوند. در اکوسیستم جدیدتر اروپا نیز جهت‌گیری به سمت data sharing، ontology مشترک، کیفیت داده و ظرفیت دیجیتال پررنگ‌تر شده است؛ بنابراین داشتن Adapter/Canonical Layer از ابتدا تصمیم معماری مناسبی است، بدون اینکه مدل ایرانی ما مجبور به تقلید از مدل عملیاتی خارجی شود. 77. جمع‌بندی معماری اجرایی مدل نهایی: MARKETPLACE │ ▼ MARKET REQUEST │ ▼ DEMAND │ ▼ FREIGHT FLOW │ ▼ WAGON REQUIREMENT │ ▼ TRAIN FORMATION │ ▼ ┌──────────────┐ │ CANONICAL │ │ DOMAIN MODEL │ └──────┬───────┘ │ ▼ SCHEDULING MODEL │ ┌──────────┼──────────┐ ▼ ▼ ▼ TRAIN PATH RESOURCE CONFLICT │ │ │ └──────────┼──────────┘ ▼ CP-SAT / SOLVER │ ▼ CANDIDATE SCHEDULE │ ▼ INDEPENDENT VALIDATOR │ ┌──────┴──────┐ ▼ ▼ FEASIBLE INFEASIBLE │ ▼ VALIDATED SCHEDULE │ ▼ CAPACITY PROOF │ ▼ BOTTLENECK / EXPLANATION │ ▼ MARKETABLE CAPACITY 78. سه Graph اصلی موتور در نهایت هسته Engine بر سه Graph استوار خواهد بود: Infrastructure Graph [ G_I=(V,E) ] نماینده: Station Block Segment Junction Track Time-Space Graph [ G_T=(N,A) ] نماینده: Train Movement Arrival Departure Running Dwell Waiting Conflict Graph [ G_C=(V_C,E_C) ] نماینده: Train Train Resource Conflict Ordering و کل موتور: [ G_I \rightarrow G_T \rightarrow G_C \rightarrow SchedulingModel \rightarrow Solver \rightarrow ValidatedSchedule \rightarrow Capacity ] 79. Definition of Done این مرحله این مرحله زمانی تکمیل‌شده تلقی می‌شود که سیستم بتواند: یک Route واقعی را Load کند؛ Blockها را بسازد؛ Stationها را بشناسد؛ Train Runها را Load کند؛ Formation را محاسبه کند؛ Running Time را تولید کند؛ Blocking Time را تولید کند؛ Time-Space Path بسازد؛ Conflict Graph تولید کند؛ Single Track Conflict را تشخیص دهد؛ Double Track را صحیح مدل کند؛ Mixed Track را مدل کند؛ Crossing Station را مدل کند؛ CP-SAT Model بسازد؛ Schedule تولید کند؛ Schedule را مستقل Validate کند؛ Capacity Search اجرا کند؛ Capacity Proof تولید کند؛ Binding Constraint را معرفی کند؛ Result را Version کند؛ Result را به Source Data ردیابی کند. و مهم‌تر از همه: [ \boxed{ No\ Feasible\ Schedule \Rightarrow No\ Operational\ Capacity } ] و: [ \boxed{ No\ Validated\ Schedule \Rightarrow No\ Accepted\ Capacity\ Result } ] 80. تصمیم معماری نهایی برای شروع Implementation، Stack منطقی MVP به این شکل است: PostgreSQL + Canonical Domain Model + Scheduling Engine + Conflict Engine + OR-Tools CP-SAT + Independent Feasibility Checker + Capacity Engine + REST API + GIS Adapter + React/TypeScript UI اما Dependency باید این‌گونه باشد: Domain ↓ Scheduling ↓ Solver Adapter نه: Domain ↓ OR-Tools این تفاوت کوچک در ظاهر، برای جلوگیری از قفل‌شدن کل محصول به یک Solver بسیار مهم است. نتیجه در این نقطه، معماری ما دیگر صرفاً یک «مدل ریاضی» یا «Mockup» نیست؛ یک Implementation Blueprint داریم که از داده خام شروع می‌شود، به مدل Canonical می‌رسد، Train Path و Conflict Graph می‌سازد، CP-SAT را تغذیه می‌کند، Schedule معتبر تحویل می‌دهد و سپس همان Schedule را به‌عنوان مدرک ظرفیت استفاده می‌کند. گام بعدی طبیعی، پیاده‌سازی خود Vertical Slice است: یک نمونه واقعی Single/Mixed Track با داده مصنوعی کنترل‌شده، Schema واقعی PostgreSQL، JSON ورودی/خروجی، و کد CP-SAT قابل اجرا؛ سپس همان موتور را روی داده Sangan–Foolad و بعد داده Excel/Access موجود می‌نشانیم. این مرحله عملاً مرز بین طراحی سیستم و شروع توسعه واقعی موتور است. پیشنهاد من این است که مرحله بعد را دیگر با سندهای صرفاً مفهومی ادامه ندهیم و یک MVP اجرایی CP-SAT + PostgreSQL برای یک Route نمونه بسازیم؛ یعنی واقعاً مدل را اجرا کنیم و اولین Capacity Proof را تولید کنیم. --- ## ادامه 10 — پرسش کاربر (`message-id:95400253-dc4f-42fa-8fa4-fe1fb40ea96e`) بله لطفا --- ## ادامه 10 — پاسخ دستیار (`message-id:b020d317-eeeb-4801-a40a-169f0d4d5e63`) عالی. حالا می‌توانیم یک قدم مهم‌تر برداریم: اولین Vertical Slice واقعاً اجرایی را تعریف کنیم؛ نه فقط Specification. برای این مرحله، من پیشنهاد می‌کنم یک نمونه کوچک ولی کامل بسازیم که از Route و TrainRun شروع شود، CP-SAT برنامه حرکت را تولید کند، Conflict را تشخیص دهد و در نهایت Cr را با Capacity Proof به‌دست آورد. در CP-SAT نیز مدل‌سازی زمان، Boolean ordering و interval/resource constraints با ماهیت این مسئله سازگار است؛ مستندات رسمی Google نیز CP-SAT را برای مدل‌سازی مسائل زمان‌بندی و محدودیت‌های گسسته پشتیبانی می‌کند. Google for Developers MVP اجرایی موتور ظرفیت — Vertical Slice، PostgreSQL، CP-SAT و Capacity Proof — نسخه 1.0 سند MVP اجرایی موتور ظرفیت حمل بار ریلی Vertical Slice — Single / Mixed Track Route نسخه: 1.0 وضعیت: Implementation Blueprint هدف: اولین نمونه End-to-End قابل اجرا Solver: OR-Tools CP-SAT Database: PostgreSQL Backend پیشنهادی: Python API: REST / FastAPI مدل عملیاتی: OD-Centric Train Formation + Detailed Intermediate Operational Path 1. هدف MVP این MVP باید ثابت کند که موتور می‌تواند: Route ↓ Blocks / Stations ↓ Train Runs ↓ Running / Blocking Time ↓ Time-Space Paths ↓ Conflict Detection ↓ CP-SAT Scheduling ↓ Feasibility Validation ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck Explanation را به‌صورت واقعی اجرا کند. بنابراین MVP صرفاً یک Demo UI نیست. هسته اصلی آن باید یک Engine قابل تست باشد. 2. محدوده MVP نسخه اول فقط این موارد را حل می‌کند: زیرساخت Station Block Single Track Double Track Mixed Track Station Crossing Station usable length قطار Train TrainRun Direction Loaded / Empty Train Formation Train Length Train Weight زمان‌بندی Earliest Departure Latest Departure Running Time Blocking Time Dwell Headway Single-track conflict Station crossing Solver CP-SAT Feasibility Conflict ordering Objective ساده ظرفیت Route Capacity Binary Search Capacity Proof Binding Constraint Explanation 3. چیزی که فعلاً وارد MVP نمی‌شود برای جلوگیری از پیچیده‌شدن بیش از حد: Wagon Assignment واقعی Locomotive Assignment واقعی Network-wide optimization Microscopic simulation Delay propagation Robust timetable optimization Investment optimization Dynamic rerouting Dynamic train formation Real-time dispatching اما Data Model از ابتدا باید جای این موارد را داشته باشد. 4. نمونه Route برای تست، یک Route مصنوعی ولی از نظر عملیاتی واقعی تعریف می‌کنیم: A ── B01 ── S01 ── B02 ── S02 ── B03 ── S03 ── B04 ── D نوع Track: B01 = SINGLE B02 = SINGLE B03 = DOUBLE B04 = SINGLE بنابراین: A │ │ SINGLE │ S01 │ │ SINGLE │ S02 │ │ DOUBLE │ S03 │ │ SINGLE │ D این Route عمداً Mixed Track است. 5. ایستگاه‌ها Station نوع Crossing A Origin خیر S01 Intermediate بله S02 Intermediate بله S03 Intermediate بله D Destination خیر برای S01 و S02 و S03: crossing_tracks = 2 usable_length = 700 m 6. Block Parameters Block Track Length Loaded Empty B01 SINGLE 42 km 42 min 38 min B02 SINGLE 31 km 32 min 29 min B03 DOUBLE 36 km 35 min 32 min B04 SINGLE 28 km 30 min 27 min برای MVP زمان‌ها از قبل محاسبه‌شده‌اند. در نسخه Production باید از: [ T_{run}=f(L,V_{profile},TrainType,LoadState) ] تولید شوند. 7. Blocking Time فرض: entry_clearance = 2 min exit_clearance = 2 min پس: [ T_{blocking}=T_{running}+T_{entry}+T_{exit} ] مثلاً برای B01: [ 42+2+2=46 ] بنابراین: Running Time = 42 min Blocking Time = 46 min این تفاوت باید در Engine حفظ شود. 8. Train Types دو Train Type تعریف می‌کنیم: FRT-L FRT-E FRT-L Loaded Freight: length = 620 m weight = 3200 t FRT-E Empty Wagon Train: length = 620 m weight = 1200 t 9. Train Runs برای تست اولیه: TR01 A → D LOADED TR02 D → A EMPTY TR03 A → D LOADED TR04 D → A EMPTY TR05 A → D LOADED TR06 D → A EMPTY این دقیقاً همان منطق Loaded/Empty Cycle را آزمایش می‌کند. 10. Time Horizon برای اولین Run: Start = 06:00 End = 23:00 در Solver: 06:00 → 0 06:01 → 60 ... 23:00 → 61200 واحد: second است. 11. Headway نسخه MVP: headway_single_track = 5 min headway_double_track = 3 min station_departure_gap = 3 min در Production: HeadwayProfile جایگزین این مقادیر ثابت خواهد شد. 12. Canonical JSON Fixture اولین Fixture سیستم: { "route": { "id": "R001", "origin": "A", "destination": "D" }, "stations": [ { "id": "A", "crossing": false }, { "id": "S01", "crossing": true, "tracks": 2, "usableLengthM": 700 }, { "id": "S02", "crossing": true, "tracks": 2, "usableLengthM": 700 }, { "id": "S03", "crossing": true, "tracks": 2, "usableLengthM": 700 }, { "id": "D", "crossing": false } ], "blocks": [ { "id": "B01", "from": "A", "to": "S01", "trackType": "SINGLE", "runningLoaded": 2520, "runningEmpty": 2280 }, { "id": "B02", "from": "S01", "to": "S02", "trackType": "SINGLE", "runningLoaded": 1920, "runningEmpty": 1740 }, { "id": "B03", "from": "S02", "to": "S03", "trackType": "DOUBLE", "runningLoaded": 2100, "runningEmpty": 1920 }, { "id": "B04", "from": "S03", "to": "D", "trackType": "SINGLE", "runningLoaded": 1800, "runningEmpty": 1620 } ] } 13. Train Fixture { "trains": [ { "id": "TR01", "origin": "A", "destination": "D", "direction": "OUTBOUND", "loadState": "LOADED", "lengthM": 620, "weightT": 3200, "earliestDeparture": 0, "latestDeparture": 3600 }, { "id": "TR02", "origin": "D", "destination": "A", "direction": "INBOUND", "loadState": "EMPTY", "lengthM": 620, "weightT": 1200, "earliestDeparture": 0, "latestDeparture": 3600 } ] } 14. Python Project ساختار اولیه: rail_capacity_mvp/ │ ├── app/ │ ├── domain/ │ ├── canonical/ │ ├── scheduling/ │ ├── conflicts/ │ ├── solver/ │ │ └── cp_sat/ │ ├── capacity/ │ ├── validation/ │ └── api/ │ ├── fixtures/ │ ├── route_001.json │ └── trains_001.json │ ├── tests/ │ ├── test_conflicts.py │ ├── test_single_track.py │ ├── test_mixed_track.py │ ├── test_feasibility.py │ └── test_capacity.py │ └── main.py 15. Domain Objects from dataclasses import dataclass from enum import Enum class TrackType(Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class Direction(Enum): OUTBOUND = "OUTBOUND" INBOUND = "INBOUND" class LoadState(Enum): LOADED = "LOADED" EMPTY = "EMPTY" @dataclass class Block: id: str from_station: str to_station: str track_type: TrackType running_loaded: int running_empty: int 16. TrainRun @dataclass class TrainRun: id: str origin: str destination: str direction: Direction load_state: LoadState length_m: int weight_t: int earliest_departure: int latest_departure: int latest_arrival: int 17. Scheduling Problem @dataclass class SchedulingProblem: route: list[Block] trains: list[TrainRun] horizon: int single_track_headway: int double_track_headway: int 18. Solver Model برای هر Train و Block: entry[train, block] exit[train, block] و: start[train] arrival[train] تعریف می‌کنیم. 19. CP-SAT Model هسته اولیه: from ortools.sat.python import cp_model def build_model(problem): model = cp_model.CpModel() entry = {} exit = {} for train in problem.trains: previous_exit = model.NewIntVar( train.earliest_departure, problem.horizon, f"start_{train.id}" ) for block in problem.route: duration = ( block.running_loaded if train.load_state == LoadState.LOADED else block.running_empty ) e = model.NewIntVar( 0, problem.horizon, f"entry_{train.id}_{block.id}" ) x = model.NewIntVar( 0, problem.horizon, f"exit_{train.id}_{block.id}" ) entry[(train.id, block.id)] = e exit[(train.id, block.id)] = x model.Add(e >= previous_exit) model.Add(x == e + duration) previous_exit = x return model, entry, exit این اولین هسته Scheduling Engine است. 20. Constraint: Latest Arrival برای آخرین Block: last_block = problem.route[-1] model.Add( exit[(train.id, last_block.id)] <= train.latest_arrival ) 21. Single Track Conflict برای هر دو Train مخالف: def add_single_track_conflict( model, train_a, train_b, block, entry, exit, separation ): order = model.NewBoolVar( f"order_{train_a.id}_{train_b.id}_{block.id}" ) model.Add( exit[(train_a.id, block.id)] + separation <= entry[(train_b.id, block.id)] ).OnlyEnforceIf(order) model.Add( exit[(train_b.id, block.id)] + separation <= entry[(train_a.id, block.id)] ).OnlyEnforceIf(order.Not()) این قسمت هسته واقعی Conflict Resolution است. 22. چرا فقط قطارهای مخالف؟ در Single Track، مسئله اصلی: A → B B ← A است. اما دو قطار هم‌جهت نیز ممکن است به دلیل Headway با هم Conflict داشته باشند. بنابراین در مرحله بعد: Same Direction + Opposite Direction هر دو بررسی خواهند شد. 23. Double Track در Double Track اگر Directionها مخالف باشند: A → │ │ DOUBLE │ ← B نباید یک NoOverlap مشترک ایجاد کنیم. Resource: B03_OUTBOUND B03_INBOUND خواهد بود. این یکی از مهم‌ترین تفاوت‌های Single و Double Track در مدل Solver است. 24. Mixed Track Conflict Detection قبل از ساخت Constraint: for block in route: if block.track_type == TrackType.SINGLE: create_single_track_conflicts(block) elif block.track_type == TrackType.DOUBLE: create_directional_resources(block) بنابراین Infrastructure Model تعیین می‌کند که Solver چه نوع Constraint بسازد. 25. Solver Execution from ortools.sat.python import cp_model def solve_model(model): solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = 30 solver.parameters.num_search_workers = 8 status = solver.Solve(model) return solver, status در Production، num_search_workers و سایر پارامترها باید versioned و در Run ذخیره شوند. 26. Schedule Extraction def extract_schedule( solver, trains, route, entry, exit ): result = [] for train in trains: blocks = [] for block in route: blocks.append({ "blockId": block.id, "entry": solver.Value( entry[(train.id, block.id)] ), "exit": solver.Value( exit[(train.id, block.id)] ) }) result.append({ "trainId": train.id, "blocks": blocks }) return result 27. مستقل بودن Feasibility Checker حتی اگر: status == cp_model.OPTIMAL باشد، باید: validate_schedule(schedule) را اجرا کنیم. 28. Feasibility Checker def validate_schedule(schedule, problem): errors = [] for train in schedule: blocks = train["blocks"] for i in range(len(blocks) - 1): current = blocks[i] next_block = blocks[i + 1] if next_block["entry"] < current["exit"]: errors.append({ "type": "TEMPORAL_PRECEDENCE", "trainId": train["trainId"] }) return errors در نسخه Production این Checker بسیار کامل‌تر خواهد بود. 29. Conflict Checker مستقل def detect_conflicts(schedule, trains, route): conflicts = [] for block in route: block_trains = [] for train in schedule: movement = next( x for x in train["blocks"] if x["blockId"] == block.id ) block_trains.append( (train, movement) ) for i in range(len(block_trains)): for j in range(i + 1, len(block_trains)): a, ma = block_trains[i] b, mb = block_trains[j] overlap = ( ma["entry"] < mb["exit"] and mb["entry"] < ma["exit"] ) if overlap: conflicts.append({ "blockId": block.id, "trainA": a["trainId"], "trainB": b["trainId"] }) return conflicts در نسخه بهینه، این قسمت با Sort/Sweep Line پیاده‌سازی می‌شود تا از (O(n^2)) در داده‌های بزرگ فاصله بگیریم. 30. Capacity Engine API ساده: def is_feasible( train_count, base_problem ): problem = generate_problem( train_count, base_problem ) model, entry, exit = build_model(problem) add_conflicts( model, problem, entry, exit ) solver, status = solve_model(model) if status not in ( cp_model.FEASIBLE, cp_model.OPTIMAL ): return False schedule = extract_schedule( solver, problem.trains, problem.route, entry, exit ) errors = validate_schedule( schedule, problem ) conflicts = detect_conflicts( schedule, problem.trains, problem.route ) return len(errors) == 0 and len(conflicts) == 0 31. Binary Search Capacity def calculate_capacity(base_problem, lower, upper): best = lower while lower <= upper: mid = (lower + upper) // 2 feasible = is_feasible( mid, base_problem ) if feasible: best = mid lower = mid + 1 else: upper = mid - 1 return best 32. Capacity Proof پس از Search: capacity = calculate_capacity( problem, lower=0, upper=100 ) باید دو Run نهایی را نگه داریم: F = capacity F = capacity + 1 مثلاً: 64 → FEASIBLE 65 → INFEASIBLE پس: [ C_r=64 ] اما این عدد فقط زمانی Accepted است که Schedule مربوط به 64 قطار Validation شده باشد. 33. Proof Contract { "routeId": "R001", "capacity": 64, "proof": { "acceptedFlow": 64, "acceptedScheduleId": "SCH-064", "acceptedStatus": "FEASIBLE", "nextFlow": 65, "nextScheduleStatus": "INFEASIBLE" }, "bindingConstraints": [ { "type": "SINGLE_TRACK", "blockId": "B04" }, { "type": "CROSSING", "stationId": "S03" } ] } 34. اولین سناریوی آزمایشی سناریوی Baseline: 06:00 → 23:00 قطارها: TR01 A→D Loaded TR02 D→A Empty TR03 A→D Loaded TR04 D→A Empty TR05 A→D Loaded TR06 D→A Empty همه قطارها: length = 620m و ایستگاه‌ها: usableLength = 700m پس محدودیت طول قطار فعال نیست. 35. Expected Behavior Solver باید بتواند: Train Path بسازد؛ در B01 و B02 و B04 تعارض‌های Single Track را تشخیص دهد؛ در B03 امکان حرکت همزمان دو جهت را حفظ کند؛ در Stationهای S01/S02/S03 امکان Crossing را در نظر بگیرد؛ زمان انتظار ایجاد کند؛ Schedule را Feasible کند؛ تعداد قطار قابل عبور را افزایش دهد تا اولین نقطه Infeasible برسد. 36. Time-Space Output خروجی باید به شکل زیر قابل نمایش باشد: Time → 06:00 07:00 08:00 09:00 10:00 A │ TR01 ────────────────> │ S01 │ TR02 <──── │ S02 │ TR01 ───────────> │ S03 │ TR02 <──── │ D این داده مستقیماً از: TrainPath + TrainStationCall + BlockOccupancy تولید می‌شود. 37. Schedule Diff برای دو Schedule: SCH-064 SCH-065 باید مشخص شود: TR01 +0 TR02 +7 min TR03 +12 min TR04 +18 min و: New Conflict: S03 این اطلاعات برای Capacity Proof بسیار ارزشمند است. 38. Binding Constraint بعد از Infeasible شدن: F = 65 باید بتوانیم یک Chain ایجاد کنیم: 65 trains ↓ Conflict ↓ B04 ↓ Single Track ↓ S03 ↓ Crossing Window ↓ No Feasible Ordering این Chain باید در Result ذخیره شود. 39. Capacity Bottleneck Object { "bottleneckId": "BOT-001", "type": "SINGLE_TRACK", "resource": { "type": "BLOCK", "id": "B04" }, "impact": { "capacityBefore": 65, "capacityAfter": 64, "delta": -1 }, "evidence": { "conflictCount": 4, "affectedTrains": [ "TR61", "TR62" ] } } 40. اولین API Health GET /health خروجی: { "status": "UP", "engineVersion": "0.1.0" } Solve POST /api/v1/scheduling/solve Validate POST /api/v1/scheduling/validate Capacity POST /api/v1/capacity/routes/R001/calculate Result GET /api/v1/runs/{runId} 41. Database Persistence در MVP در اولین نسخه، همه Intermediate Objectها الزاماً نباید در PostgreSQL ذخیره شوند. پیشنهاد: Persistent Route Station Block Train TrainRun TrainFormation BaselineSchedule Scenario Run CapacityResult CapacityProof قابل Rebuild TrainPath ConflictGraph SolverModel CandidateSchedule TemporaryResourceUsage اما Schedule Accepted باید Persist شود. 42. چرا Conflict Graph را می‌توان Rebuild کرد؟ زیرا: [ G_C=f(G_I,G_T,Rules) ] پس Conflict Graph یک Derived Artifact است. اگر: Infrastructure Version + Scheduling Rules Version + Train Paths تغییر نکنند، Conflict Graph باید قابل بازتولید باشد. 43. Run Reproducibility هر Run باید حداقل این اطلاعات را داشته باشد: { "runId": "RUN-001", "dataVersion": "DATA-001", "modelVersion": "MODEL-001", "algorithmVersion": "ALG-001", "solver": { "name": "CP-SAT", "version": "stored", "timeLimitSeconds": 120, "workers": 8 }, "scenarioId": "BASELINE", "inputHash": "..." } 44. Test Suite حداقل تست‌ها: TC-001 Route Loading TC-002 Block Ordering TC-003 Train Path TC-004 Single Track Conflict TC-005 Same Direction Headway TC-006 Double Track Opposite Direction TC-007 Mixed Track TC-008 Station Crossing TC-009 Train Length TC-010 Latest Arrival TC-011 Feasibility Validation TC-012 Capacity Search TC-013 Capacity Proof TC-014 Binding Constraint TC-015 Reproducibility 45. تست بسیار مهم: Monotonicity برای Capacity: اگر: [ F ] Feasible باشد، در شرایط یکسان معمولاً باید بتوان Schedule را برای تعداد کمتری از قطارها نیز یافت. بنابراین تست: F = 10 → FEASIBLE F = 11 → FEASIBLE F = 12 → FEASIBLE ... F = C → FEASIBLE F = C+1 → INFEASIBLE باید بررسی شود. اگر: 10 = feasible 11 = infeasible 12 = feasible شد، احتمالاً یک ایراد در: model search randomness constraints or fixture داریم. 46. تست Empty Train این تست برای مدل شما بسیار مهم است. فرض: TR01 Loaded A→D TR02 Empty D→A و: Loaded running time > Empty running time بنابراین Solver نباید فرض کند همه Trainها زمان یکسان دارند. باید: [ T_{run}=f(Train,LoadState,Block) ] باشد. 47. تست Train Formation اگر: Train Length = 750m و: Station usable length = 700m آنگاه: Crossing Track Assignment = INVALID باید Capacity Proof بتواند این محدودیت را توضیح دهد. 48. تست Baseline Baseline Schedule باید به Solver وارد شود. سه حالت: FIXED FLEXIBLE PREFERRED FIXED Solver حق تغییر ندارد. FLEXIBLE Solver می‌تواند تغییر دهد. PREFERRED Solver تغییر می‌دهد ولی جریمه می‌گیرد. 49. Objective با Penalty برای Preferred Movement: [ Objective= TravelTime+ \lambda_1 Waiting+ \lambda_2 Deviation ] مثلاً: deviation = |GeneratedDeparture - BaselineDeparture| 50. اتصال به Marketplace در MVP فقط Interface را آماده می‌کنیم. Marketplace ↓ Market Request ↓ Demand ↓ Train Requirement ↓ Scheduling خروجی: Capacity Offer مثلاً: { "routeId": "R001", "capacity": { "total": 64, "allocated": 42, "available": 22 }, "profile": { "direction": "OUTBOUND", "loadState": "LOADED" } } 51. Capacity Offer ظرفیت Marketplace نباید صرفاً: 64 trains باشد. بلکه: 64 trains + direction + time window + train type + load state + OD + commodity compatibility باشد. یعنی: [ C=f( Route, OD, TrainType, LoadState, Time, Direction, Station, Wagon, Locomotive ) ] 52. مرحله بعد از MVP بعد از اینکه این Vertical Slice کاملاً درست کار کرد: MVP-01 Single/Mixed Track ↓ MVP-02 Real Baseline Schedule ↓ MVP-03 Excel/Access Adapter ↓ MVP-04 Sangan–Foolad ↓ MVP-05 Wagon Cycle ↓ MVP-06 Locomotive Cycle ↓ MVP-07 Network Optimization ↓ MVP-08 Marketplace Allocation 53. معیار موفقیت MVP MVP زمانی موفق است که بتوانیم در یک تست واقعی نشان دهیم: Input: Route + Infrastructure + Train Runs ↓ Generated Schedule ↓ No unresolved conflicts ↓ Independent Validation = PASS ↓ Capacity = C ↓ Schedule(C) = FEASIBLE ↓ Schedule(C+1) = INFEASIBLE ↓ Binding Constraint identified ↓ Explanation generated این زنجیره، هسته واقعی محصول است. 54. Definition of Done برای این Vertical Slice: Data Route loaded Stations loaded Blocks loaded Train Runs loaded Formation loaded Scheduling Train Path generated Running Time calculated Blocking Time calculated Single Track constraints Double Track behavior Mixed Track behavior Crossing Solver CP-SAT model built Feasible solution generated Objective applied Solver result stored Validation Independent validation Conflict detection Resource validation Station validation Capacity Binary Search Capacity Proof Binding Constraint Explanation Audit Data Version Model Version Algorithm Version Solver Configuration Input Hash Run ID 55. اصل نهایی MVP نباید بگوییم: «مدل Solver گفت ظرفیت 64 است.» باید بتوانیم بگوییم: «برای 64 حرکت، یک برنامه حرکت معتبر تولید و مستقل اعتبارسنجی شد؛ برای 65 حرکت، هیچ برنامه حرکت feasible تحت قیود سناریو پیدا نشد؛ محدودیت اصلی در Block B04 و فرآیند Crossing در S03 شناسایی شد.» این تفاوت، مرز بین Capacity Calculator و Railway Capacity Engine است. Maximum\ Feasible\ Operational\ Schedule } ] و نه: Simple\ Headway\ Formula ] 56. معماری نهایی Vertical Slice PostgreSQL │ ▼ Canonical Model │ ▼ Scheduling Builder │ ┌────────────┼────────────┐ ▼ ▼ ▼ Train Path Block Time Station Rules │ │ │ └────────────┼────────────┘ ▼ Conflict Engine │ ▼ Conflict Graph │ ▼ CP-SAT Model │ ▼ Solver Result │ ▼ Independent Validator │ ┌──────┴──────┐ ▼ ▼ FEASIBLE INFEASIBLE │ ▼ Schedule │ ▼ Capacity Search │ ▼ Capacity Proof │ ┌─────┴──────┐ ▼ ▼ Bottleneck Explanation │ │ └─────┬──────┘ ▼ Capacity Offer │ ▼ Marketplace 57. خروجی‌ای که باید در UI نمایش داده شود پس از اجرای MVP، صفحه Route Capacity باید بتواند چیزی شبیه این نشان دهد: ROUTE CAPACITY ──────────────────────────────────── Route A → D Physical Capacity 96 Operational Capacity 71 Route Capacity 64 Available Capacity 22 ──────────────────────────────────── CAPACITY PROOF 64 trains ✓ FEASIBLE 65 trains ✕ INFEASIBLE ──────────────────────────────────── PRIMARY BOTTLENECK S03 Crossing Capacity Resource: S03 / Track 2 Affected: TR61 TR62 ──────────────────────────────────── WHY? At 65 trains, no feasible ordering exists for the opposing movements through B04 under the current crossing windows. ──────────────────────────────────── WHAT IF? [ Add Crossing Track ] [ Change Operating Regime ] [ Increase Time Window ] [ Recalculate ] این دقیقاً همان نقطه‌ای است که UI طراحی‌شده قبلی به Engine واقعی متصل می‌شود. 58. مسیر توسعه از اینجا ترتیب پیشنهادی من برای توسعه واقعی: STEP 01 Fixture + Domain Classes ↓ STEP 02 Route / Train / Formation Loader ↓ STEP 03 Train Path Generator ↓ STEP 04 Conflict Engine ↓ STEP 05 CP-SAT Scheduler ↓ STEP 06 Independent Validator ↓ STEP 07 Capacity Search ↓ STEP 08 Capacity Proof ↓ STEP 09 PostgreSQL Persistence ↓ STEP 10 REST API ↓ STEP 11 Existing Excel/Access Adapter ↓ STEP 12 Sangan–Foolad ↓ STEP 13 UI Integration نکته معماری: PostgreSQL را از ابتدا در معماری نهایی قرار می‌دهیم، ولی برای اثبات الگوریتم بهتر است اولین اجرای Solver با Fixtureهای JSON کاملاً deterministic شروع شود. سپس Persistence به آن اضافه شود. این کار debugging مدل ریاضی را بسیار ساده‌تر می‌کند. پیشنهاد عملی من برای گام بعد: دیگر Specification را بزرگ‌تر نکنیم؛ وارد کدنویسی واقعی همین Vertical Slice شویم. در مرحله بعد می‌توانم یک بسته اجرایی کامل شامل requirements.txt، ساختار Python، Fixture واقعی، کلاس‌های Domain، Conflict Engine، مدل CP-SAT، Capacity Search و تست‌های pytest آماده کنم تا بتوانید مستقیماً آن را اجرا کنید. --- ## ادامه 11 — پرسش کاربر (`message-id:d7d313dd-084a-4ff0-9cf9-06a2d086f810`) آماده کردن یک بسته اجرایی کامل شامل requirements.txt، ساختار Python، Fixture واقعی، کلاس‌های Domain، Conflict Engine، مدل CP-SAT، Capacity Search و تست‌های pytest --- ## ادامه 11 — پاسخ دستیار (`message-id:e2510a67-bb81-4c13-b239-6360d78da279`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 12 — پرسش کاربر (`message-id:29a1832b-1be8-4163-94d6-94568deac3f5`) بله به صورت متن همینجا اگه میشه ادامه بدیم و همین ها رو تولید کنیم --- ## ادامه 12 — پاسخ دستیار (`message-id:8d252e05-206f-4615-8e07-5037a8f7d122`) حتماً. این بار بسته را به‌صورت متن قابل کپی مستقیم می‌سازیم و یک نکته مهم را هم اصلاح می‌کنم: در نسخه قبلی، مدل Capacity برای Fixture کوچک بود و تست «C+1 حتماً infeasible» الزاماً با آن داده‌ها تضمین نمی‌شد. بنابراین این نسخه را طوری طراحی می‌کنیم که تست‌های ظرفیت deterministic و قابل اتکا باشند. همچنین CP-SAT برای این مرحله انتخاب مناسبی است چون مدل زمان‌بندی ما ذاتاً بر متغیرهای زمانی، تقدم‌وتأخر و محدودیت‌های منابع/تعارض بنا شده است. Google for Developers +1 بسته اجرایی Railway Capacity MVP — Python + CP-SAT + Conflict Engine + Capacity Search — نسخه 0.2 Railway Capacity MVP بسته اجرایی اولیه موتور تولید، برآورد و بهینه‌سازی ظرفیت حمل بار ریلی Version: 0.2 Language: Python 3.10+ Solver: OR-Tools CP-SAT Testing: pytest Architecture: Domain → Scheduling → Conflict → Solver → Validation → Capacity 1. ساختار پروژه rail_capacity_mvp/ │ ├── requirements.txt ├── pyproject.toml ├── README.md │ ├── app/ │ ├── __init__.py │ │ │ ├── domain/ │ │ ├── __init__.py │ │ └── models.py │ │ │ ├── scheduling/ │ │ ├── __init__.py │ │ └── problem.py │ │ │ ├── conflicts/ │ │ ├── __init__.py │ │ └── engine.py │ │ │ ├── solver/ │ │ ├── __init__.py │ │ └── cp_sat.py │ │ │ ├── validation/ │ │ ├── __init__.py │ │ └── checker.py │ │ │ ├── capacity/ │ │ ├── __init__.py │ │ └── search.py │ │ │ └── main.py │ ├── fixtures/ │ └── route_001.json │ └── tests/ ├── __init__.py ├── conftest.py ├── test_domain.py ├── test_conflicts.py ├── test_solver.py ├── test_validation.py └── test_capacity.py 2. requirements.txt ortools>=9.14,<10 pytest>=8.0,<10 pydantic>=2.0,<3 در این MVP، pydantic هنوز هسته مدل نیست؛ برای مرحله API/JSON Contract نگه داشته شده است. 3. pyproject.toml [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [project] name = "rail-capacity-mvp" version = "0.2.0" description = "Railway capacity scheduling MVP using OR-Tools CP-SAT" requires-python = ">=3.10" dependencies = [ "ortools>=9.14,<10", "pydantic>=2.0,<3", ] [project.optional-dependencies] dev = [ "pytest>=8.0,<10" ] [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] addopts = "-q" 4. README.md # Railway Capacity MVP First executable vertical slice of the Railway Capacity Generation, Estimation and Network Optimization Engine. ## Scope - Railway domain model - Route / Station / Block - Train / TrainRun - Loaded / Empty - Single / Double / Mixed Track - Train Path - Conflict Detection - CP-SAT Scheduling - Independent Validation - Capacity Search - Capacity Proof - pytest regression tests ## Install python -m venv .venv Linux/macOS: source .venv/bin/activate Windows: .venv\Scripts\activate pip install -r requirements.txt ## Test pytest ## Run demo python -m app.main ## Important The included railway fixture is synthetic. It is for algorithm verification and does not represent verified railway operational data. 5. Domain Model فایل: app/domain/models.py from __future__ import annotations from dataclasses import dataclass from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class Direction(str, Enum): OUTBOUND = "OUTBOUND" INBOUND = "INBOUND" class LoadState(str, Enum): LOADED = "LOADED" EMPTY = "EMPTY" @dataclass(frozen=True) class Station: id: str crossing: bool = False tracks: int = 1 usable_length_m: int = 0 @dataclass(frozen=True) class Block: id: str from_station: str to_station: str track_type: TrackType running_loaded_s: int running_empty_s: int entry_clearance_s: int = 120 exit_clearance_s: int = 120 def running_time(self, load_state: LoadState) -> int: if load_state == LoadState.LOADED: return self.running_loaded_s return self.running_empty_s def blocking_time(self, load_state: LoadState) -> int: return ( self.running_time(load_state) + self.entry_clearance_s + self.exit_clearance_s ) @dataclass(frozen=True) class Route: id: str origin: str destination: str stations: tuple[Station, ...] blocks: tuple[Block, ...] @dataclass(frozen=True) class TrainRun: id: str origin: str destination: str direction: Direction load_state: LoadState length_m: int weight_t: int earliest_departure_s: int latest_departure_s: int latest_arrival_s: int @dataclass(frozen=True) class SchedulingProblem: route: Route trains: tuple[TrainRun, ...] horizon_s: int single_track_separation_s: int = 300 same_direction_headway_s: int = 300 @dataclass(frozen=True) class BlockMovement: train_id: str block_id: str entry_s: int exit_s: int @dataclass(frozen=True) class TrainSchedule: train_id: str movements: tuple[BlockMovement, ...] @dataclass(frozen=True) class Schedule: trains: tuple[TrainSchedule, ...] @dataclass(frozen=True) class Conflict: block_id: str train_a: str train_b: str conflict_type: str @dataclass(frozen=True) class ValidationResult: feasible: bool errors: tuple[str, ...] conflicts: tuple[Conflict, ...] @dataclass(frozen=True) class CapacityProof: capacity: int accepted_flow: int accepted_feasible: bool next_flow: int next_flow_feasible: bool 6. Fixture واقعی MVP برای اینکه Capacity Search معنی‌دار باشد، Fixture را به‌صورت ظرفیت‌سنجی مستقل طراحی می‌کنیم. Route: A │ │ B01 SINGLE │ S01 │ │ B02 SINGLE │ S02 │ │ B03 DOUBLE │ S03 │ │ B04 SINGLE │ D 7. route_001.json { "route": { "id": "R001", "origin": "A", "destination": "D", "stations": [ { "id": "A", "crossing": false, "tracks": 1, "usableLengthM": 0 }, { "id": "S01", "crossing": true, "tracks": 2, "usableLengthM": 700 }, { "id": "S02", "crossing": true, "tracks": 2, "usableLengthM": 700 }, { "id": "S03", "crossing": true, "tracks": 2, "usableLengthM": 700 }, { "id": "D", "crossing": false, "tracks": 1, "usableLengthM": 0 } ], "blocks": [ { "id": "B01", "from": "A", "to": "S01", "trackType": "SINGLE", "runningLoaded": 2520, "runningEmpty": 2280 }, { "id": "B02", "from": "S01", "to": "S02", "trackType": "SINGLE", "runningLoaded": 1920, "runningEmpty": 1740 }, { "id": "B03", "from": "S02", "to": "S03", "trackType": "DOUBLE", "runningLoaded": 2100, "runningEmpty": 1920 }, { "id": "B04", "from": "S03", "to": "D", "trackType": "SINGLE", "runningLoaded": 1800, "runningEmpty": 1620 } ] } } 8. Problem Builder فایل: app/scheduling/problem.py from app.domain.models import ( Block, Direction, LoadState, Route, SchedulingProblem, Station, TrainRun, TrackType, ) def build_fixture_problem(train_count: int) -> SchedulingProblem: stations = ( Station("A", False, 1, 0), Station("S01", True, 2, 700), Station("S02", True, 2, 700), Station("S03", True, 2, 700), Station("D", False, 1, 0), ) blocks = ( Block( "B01", "A", "S01", TrackType.SINGLE, 2520, 2280, ), Block( "B02", "S01", "S02", TrackType.SINGLE, 1920, 1740, ), Block( "B03", "S02", "S03", TrackType.DOUBLE, 2100, 1920, ), Block( "B04", "S03", "D", TrackType.SINGLE, 1800, 1620, ), ) route = Route( id="R001", origin="A", destination="D", stations=stations, blocks=blocks, ) trains = [] for i in range(train_count): outbound = i % 2 == 0 train = TrainRun( id=f"TR{i + 1:03d}", origin="A" if outbound else "D", destination="D" if outbound else "A", direction=( Direction.OUTBOUND if outbound else Direction.INBOUND ), load_state=( LoadState.LOADED if outbound else LoadState.EMPTY ), length_m=620, weight_t=( 3200 if outbound else 1200 ), earliest_departure_s=i * 60, latest_departure_s=8 * 3600, latest_arrival_s=17 * 3600, ) trains.append(train) return SchedulingProblem( route=route, trains=tuple(trains), horizon_s=17 * 3600, single_track_separation_s=300, same_direction_headway_s=300, ) 9. Conflict Engine فایل: app/conflicts/engine.py from itertools import combinations from app.domain.models import ( Conflict, Schedule, SchedulingProblem, TrackType, ) def detect_conflicts( schedule: Schedule, problem: SchedulingProblem, ) -> tuple[Conflict, ...]: train_map = { train.id: train for train in problem.trains } conflicts = [] for block in problem.route.blocks: movements = [] for train_schedule in schedule.trains: for movement in train_schedule.movements: if movement.block_id == block.id: movements.append( ( train_schedule.train_id, movement, ) ) for ( (train_a_id, movement_a), (train_b_id, movement_b), ) in combinations(movements, 2): overlap = ( movement_a.entry_s < movement_b.exit_s and movement_b.entry_s < movement_a.exit_s ) if not overlap: continue train_a = train_map[train_a_id] train_b = train_map[train_b_id] if block.track_type == TrackType.SINGLE: conflict_type = ( "SINGLE_TRACK_OPPOSITE" if train_a.direction != train_b.direction else "SAME_DIRECTION_HEADWAY" ) conflicts.append( Conflict( block_id=block.id, train_a=train_a_id, train_b=train_b_id, conflict_type=conflict_type, ) ) else: # Opposite directions on separate tracks # are allowed in this MVP. if train_a.direction == train_b.direction: conflicts.append( Conflict( block_id=block.id, train_a=train_a_id, train_b=train_b_id, conflict_type=( "DOUBLE_TRACK_SAME_DIRECTION" ), ) ) return tuple(conflicts) 10. CP-SAT Scheduler فایل: app/solver/cp_sat.py from dataclasses import dataclass from itertools import combinations from ortools.sat.python import cp_model from app.domain.models import ( BlockMovement, Direction, Schedule, SchedulingProblem, TrackType, TrainSchedule, ) @dataclass class CPSATResult: feasible: bool schedule: Schedule | None status_name: str def solve( problem: SchedulingProblem, time_limit_s: float = 10.0, ) -> CPSATResult: model = cp_model.CpModel() entry = {} exit = {} # -------------------------------------------------- # Train paths # -------------------------------------------------- for train in problem.trains: previous_exit = model.NewIntVar( train.earliest_departure_s, problem.horizon_s, f"start_{train.id}", ) for block in problem.route.blocks: duration = block.blocking_time( train.load_state ) e = model.NewIntVar( 0, problem.horizon_s, f"entry_{train.id}_{block.id}", ) x = model.NewIntVar( 0, problem.horizon_s, f"exit_{train.id}_{block.id}", ) entry[(train.id, block.id)] = e exit[(train.id, block.id)] = x # Route precedence model.Add( e >= previous_exit ) # Occupancy duration model.Add( x == e + duration ) previous_exit = x # Arrival constraint last_block = problem.route.blocks[-1] model.Add( exit[ (train.id, last_block.id) ] <= train.latest_arrival_s ) # -------------------------------------------------- # Single-track conflicts # -------------------------------------------------- for block in problem.route.blocks: if block.track_type != TrackType.SINGLE: continue for train_a, train_b in combinations( problem.trains, 2, ): order = model.NewBoolVar( ( f"order_" f"{train_a.id}_" f"{train_b.id}_" f"{block.id}" ) ) separation = ( problem.single_track_separation_s ) # A before B model.Add( exit[ (train_a.id, block.id) ] + separation <= entry[ (train_b.id, block.id) ] ).OnlyEnforceIf(order) # B before A model.Add( exit[ (train_b.id, block.id) ] + separation <= entry[ (train_a.id, block.id) ] ).OnlyEnforceIf(order.Not()) # -------------------------------------------------- # Same-direction headway # -------------------------------------------------- for block in problem.route.blocks: for train_a, train_b in combinations( problem.trains, 2, ): if ( train_a.direction != train_b.direction ): continue order = model.NewBoolVar( ( f"headway_" f"{train_a.id}_" f"{train_b.id}_" f"{block.id}" ) ) h = ( problem.same_direction_headway_s ) model.Add( entry[ (train_a.id, block.id) ] + h <= entry[ (train_b.id, block.id) ] ).OnlyEnforceIf(order) model.Add( entry[ (train_b.id, block.id) ] + h <= entry[ (train_a.id, block.id) ] ).OnlyEnforceIf(order.Not()) # -------------------------------------------------- # Objective # -------------------------------------------------- last_block = problem.route.blocks[-1] total_arrival = sum( exit[ (train.id, last_block.id) ] for train in problem.trains ) model.Minimize(total_arrival) # -------------------------------------------------- # Solver # -------------------------------------------------- solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = ( time_limit_s ) solver.parameters.num_search_workers = 1 solver.parameters.random_seed = 1 status = solver.Solve(model) feasible = status in ( cp_model.FEASIBLE, cp_model.OPTIMAL, ) if not feasible: return CPSATResult( feasible=False, schedule=None, status_name=solver.StatusName(status), ) # -------------------------------------------------- # Extract schedule # -------------------------------------------------- train_schedules = [] for train in problem.trains: movements = [] for block in problem.route.blocks: movements.append( BlockMovement( train_id=train.id, block_id=block.id, entry_s=solver.Value( entry[ (train.id, block.id) ] ), exit_s=solver.Value( exit[ (train.id, block.id) ] ), ) ) train_schedules.append( TrainSchedule( train_id=train.id, movements=tuple( movements ), ) ) schedule = Schedule( trains=tuple( train_schedules ) ) return CPSATResult( feasible=True, schedule=schedule, status_name=solver.StatusName(status), ) 11. نکته مهم درباره Single Track در این مدل: Train A │ │ SINGLE │ Train B Solver باید یکی از دو حالت را انتخاب کند: [ Exit_A + Separation \le Entry_B ] یا: [ Exit_B + Separation \le Entry_A ] این همان disjunctive scheduling constraint است. 12. Double Track برای: B03 = DOUBLE قطار: A → D و قطار: D → A می‌توانند هم‌زمان از B03 عبور کنند. در این MVP: Opposite Direction + Double Track = No shared occupancy conflict ولی: Same Direction + Double Track = Headway Constraint است. 13. Independent Validation فایل: app/validation/checker.py from app.conflicts.engine import detect_conflicts from app.domain.models import ( Schedule, SchedulingProblem, ValidationResult, ) def validate_schedule( schedule: Schedule, problem: SchedulingProblem, ) -> ValidationResult: errors = [] train_map = { train.id: train for train in problem.trains } block_map = { block.id: block for block in problem.route.blocks } # ---------------------------------------------- # Train-level validation # ---------------------------------------------- for train_schedule in schedule.trains: train = train_map[ train_schedule.train_id ] movements = train_schedule.movements if len(movements) != len( problem.route.blocks ): errors.append( ( f"{train.id}: " "invalid number of block movements" ) ) continue # ------------------------------------------ # Block sequence # ------------------------------------------ for i, movement in enumerate( movements ): block = block_map[ movement.block_id ] if movement.exit_s <= movement.entry_s: errors.append( ( f"{train.id}/" f"{block.id}: " "non-positive interval" ) ) if i > 0: previous = movements[ i - 1 ] if ( movement.entry_s < previous.exit_s ): errors.append( ( f"{train.id}: " "route precedence violated" ) ) # ------------------------------------------ # Departure # ------------------------------------------ if ( movements[0].entry_s < train.earliest_departure_s ): errors.append( f"{train.id}: before earliest departure" ) # ------------------------------------------ # Arrival # ------------------------------------------ if ( movements[-1].exit_s > train.latest_arrival_s ): errors.append( f"{train.id}: after latest arrival" ) # ------------------------------------------ # Station length # ------------------------------------------ crossing_lengths = [ station.usable_length_m for station in problem.route.stations if station.crossing ] if crossing_lengths: max_length = max( crossing_lengths ) if train.length_m > max_length: errors.append( ( f"{train.id}: " "train exceeds crossing-track length" ) ) # ---------------------------------------------- # Conflict validation # ---------------------------------------------- conflicts = detect_conflicts( schedule, problem, ) return ValidationResult( feasible=( len(errors) == 0 and len(conflicts) == 0 ), errors=tuple(errors), conflicts=tuple(conflicts), ) 14. Capacity Search فایل: app/capacity/search.py from dataclasses import dataclass from app.domain.models import ( CapacityProof, SchedulingProblem, ) from app.scheduling.problem import ( build_fixture_problem, ) from app.solver.cp_sat import solve from app.validation.checker import ( validate_schedule, ) @dataclass class CapacityRun: flow: int feasible: bool def evaluate_flow( base_problem: SchedulingProblem, flow: int, time_limit_s: float = 10.0, ) -> CapacityRun: problem = build_fixture_problem( flow ) result = solve( problem, time_limit_s=time_limit_s, ) if ( not result.feasible or result.schedule is None ): return CapacityRun( flow=flow, feasible=False, ) validation = validate_schedule( result.schedule, problem, ) return CapacityRun( flow=flow, feasible=validation.feasible, ) def calculate_capacity( base_problem: SchedulingProblem, lower: int, upper: int, time_limit_s: float = 10.0, ) -> CapacityProof: best = lower while lower <= upper: mid = ( lower + upper ) // 2 result = evaluate_flow( base_problem, mid, time_limit_s, ) if result.feasible: best = mid lower = mid + 1 else: upper = mid - 1 accepted = evaluate_flow( base_problem, best, time_limit_s, ) next_run = evaluate_flow( base_problem, best + 1, time_limit_s, ) return CapacityProof( capacity=best, accepted_flow=best, accepted_feasible=( accepted.feasible ), next_flow=best + 1, next_flow_feasible=( next_run.feasible ), ) 15. یک اصلاح مهم برای Production در نسخه MVP بالا: problem = build_fixture_problem(flow) استفاده شده است. در Production این کاملاً باید حذف شود. به جای آن: Canonical Demand ↓ Train Requirement ↓ Train Formation ↓ Candidate Train Runs ↓ Scheduling Problem باید ایجاد شود. 16. Capacity API مفهومی capacity = calculate_capacity( problem, lower=0, upper=100, ) خروجی: CapacityProof( capacity=C, accepted_flow=C, accepted_feasible=True, next_flow=C+1, next_flow_feasible=False ) بنابراین: [ C_r=C ] فقط در صورتی Accepted است که: [ Schedule(C)=Feasible ] و: [ Schedule(C+1)=Infeasible ] 17. تست Domain فایل: tests/test_domain.py from app.domain.models import LoadState from app.scheduling.problem import ( build_fixture_problem, ) def test_fixture_is_mixed_track(): problem = build_fixture_problem(2) track_types = [ block.track_type.value for block in problem.route.blocks ] assert track_types == [ "SINGLE", "SINGLE", "DOUBLE", "SINGLE", ] def test_loaded_time_is_greater_than_empty_time(): problem = build_fixture_problem(2) block = problem.route.blocks[0] assert ( block.running_time( LoadState.LOADED ) > block.running_time( LoadState.EMPTY ) ) def test_blocking_time_exceeds_running_time(): problem = build_fixture_problem(2) block = problem.route.blocks[0] assert ( block.blocking_time( LoadState.LOADED ) > block.running_time( LoadState.LOADED ) ) 18. تست Conflict Engine فایل: tests/test_conflicts.py from app.conflicts.engine import ( detect_conflicts, ) from app.domain.models import ( BlockMovement, Schedule, TrainSchedule, ) from app.scheduling.problem import ( build_fixture_problem, ) def test_single_track_overlap_is_detected(): problem = build_fixture_problem(2) train_a = problem.trains[0] train_b = problem.trains[1] schedule = Schedule( trains=( TrainSchedule( train_id=train_a.id, movements=( BlockMovement( train_a.id, "B01", 0, 1000, ), ), ), TrainSchedule( train_id=train_b.id, movements=( BlockMovement( train_b.id, "B01", 500, 1500, ), ), ), ) ) conflicts = detect_conflicts( schedule, problem, ) assert any( conflict.block_id == "B01" for conflict in conflicts ) def test_double_track_allows_opposite_direction(): problem = build_fixture_problem(2) train_a = problem.trains[0] train_b = problem.trains[1] schedule = Schedule( trains=( TrainSchedule( train_a.id, ( BlockMovement( train_a.id, "B03", 100, 500, ), ), ), TrainSchedule( train_b.id, ( BlockMovement( train_b.id, "B03", 200, 600, ), ), ), ) ) conflicts = detect_conflicts( schedule, problem, ) assert not any( conflict.block_id == "B03" for conflict in conflicts ) 19. تست Solver فایل: tests/test_solver.py from app.solver.cp_sat import solve from app.validation.checker import ( validate_schedule, ) def test_cp_sat_generates_schedule(problem): result = solve( problem, time_limit_s=10, ) assert result.feasible assert result.schedule is not None validation = validate_schedule( result.schedule, problem, ) assert validation.feasible assert not validation.errors assert not validation.conflicts def test_solver_preserves_route_order(problem): result = solve( problem, time_limit_s=10, ) assert result.schedule is not None for train_schedule in ( result.schedule.trains ): movements = ( train_schedule.movements ) for previous, current in zip( movements, movements[1:], ): assert ( current.entry_s >= previous.exit_s ) 20. تست Validation فایل: tests/test_validation.py from app.domain.models import ( BlockMovement, Schedule, TrainSchedule, ) from app.validation.checker import ( validate_schedule, ) def test_invalid_route_order_is_detected( problem, ): train = problem.trains[0] schedule = Schedule( trains=( TrainSchedule( train.id, ( BlockMovement( train.id, "B01", 100, 200, ), BlockMovement( train.id, "B02", 150, 250, ), BlockMovement( train.id, "B03", 250, 350, ), BlockMovement( train.id, "B04", 350, 450, ), ), ), ) ) result = validate_schedule( schedule, problem, ) assert not result.feasible assert any( "precedence" in error for error in result.errors ) 21. تست Capacity فایل: tests/test_capacity.py from app.capacity.search import ( calculate_capacity, evaluate_flow, ) def test_zero_flow_is_feasible(problem): result = evaluate_flow( problem, 0, ) assert result.feasible def test_capacity_result_has_valid_accepted_flow( problem, ): proof = calculate_capacity( problem, lower=0, upper=20, time_limit_s=5, ) assert ( proof.accepted_flow == proof.capacity ) assert proof.accepted_feasible assert ( proof.next_flow == proof.capacity + 1 ) 22. تست Monotonicity این تست را حتماً اضافه می‌کنیم: tests/test_capacity_monotonicity.py from app.capacity.search import ( evaluate_flow, ) def test_feasibility_is_monotonic_until_capacity( problem, ): previous = True for flow in range(0, 10): result = evaluate_flow( problem, flow, time_limit_s=5, ) if not result.feasible: previous = False if not previous: assert not result.feasible این تست عمداً ساده است؛ نسخه Production باید monotonicity را با توجه به نوع ظرفیت، انتخاب قطار و policy دقیق‌تر تعریف کند. 23. Fixture pytest فایل: tests/conftest.py import pytest from app.scheduling.problem import ( build_fixture_problem, ) @pytest.fixture def problem(): return build_fixture_problem(6) 24. main.py فایل: app/main.py from app.capacity.search import ( calculate_capacity, ) from app.scheduling.problem import ( build_fixture_problem, ) from app.solver.cp_sat import solve from app.validation.checker import ( validate_schedule, ) def main(): problem = build_fixture_problem( 6 ) result = solve( problem, time_limit_s=10, ) print( f"Solver status: " f"{result.status_name}" ) print( f"Feasible: " f"{result.feasible}" ) if result.schedule: validation = validate_schedule( result.schedule, problem, ) print( f"Validation: " f"{validation.feasible}" ) print( f"Conflicts: " f"{len(validation.conflicts)}" ) for train in ( result.schedule.trains ): print( f"\\n{train.train_id}" ) for movement in ( train.movements ): print( f" " f"{movement.block_id}: " f"{movement.entry_s // 60}m " f"-> " f"{movement.exit_s // 60}m" ) proof = calculate_capacity( problem, lower=0, upper=20, time_limit_s=5, ) print( "\\nCapacity Proof" ) print(proof) if __name__ == "__main__": main() 25. اجرای پروژه بعد از ساخت فایل‌ها: cd rail_capacity_mvp سپس: python -m venv .venv فعال‌سازی: Linux / macOS source .venv/bin/activate Windows .venv\Scripts\activate نصب: pip install -r requirements.txt 26. اجرای تست pytest خروجی مورد انتظار: ............ 12 passed تعداد دقیق تست‌ها ممکن است با توسعه suite تغییر کند؛ معیار اصلی این است که تمام تست‌ها PASS شوند. 27. اجرای Engine python -m app.main نمونه خروجی: Solver status: OPTIMAL Feasible: True Validation: True Conflicts: 0 TR001 B01: ... B02: ... B03: ... B04: ... TR002 B01: ... B02: ... B03: ... B04: ... Capacity Proof CapacityProof( capacity=..., accepted_flow=..., accepted_feasible=True, next_flow=..., next_flow_feasible=False ) عدد ظرفیت را عمداً در Specification ثابت نمی‌کنیم. ظرفیت باید خروجی Solver باشد، نه یک عدد hard-coded. 28. نکته بسیار مهم درباره Fixture فعلی این Fixture هنوز یک Scheduling Fixture است، نه داده واقعی راه‌آهن ایران. بنابراین: B01 B02 B03 B04 و زمان‌های آن فقط برای تست Engine هستند. به محض ورود به مرحله اتصال Excel/Access، این زنجیره باید ساخته شود: Excel / Access ↓ Source Adapter ↓ Raw/Staging ↓ Mapping ↓ Canonical Model ↓ Calibration ↓ Scheduling Engine نباید Excel مستقیماً وارد CP-SAT شود. 29. مرحله بعد: TrainPath در نسخه بعد باید TrainPath را از TrainRun جدا کنیم. یعنی: TrainRun ↓ Candidate Train Path ↓ Block Occupancy ↓ Station Calls مدل: @dataclass(frozen=True) class TrainPathPoint: station_id: str sequence: int arrival_s: int departure_s: int dwell_s: int و: @dataclass(frozen=True) class BlockOccupancy: train_id: str block_id: str entry_s: int exit_s: int running_s: int blocking_s: int این تفکیک برای اتصال به Access شما بسیار مهم است. 30. مرحله بعد: Station Operation بعد از TrainPath باید Station Operation را اضافه کنیم: Arrival ↓ Reception ↓ Dwell ↓ Brake Test / Operational Activity ↓ Crossing / Overtaking ↓ Departure بنابراین: @dataclass(frozen=True) class StationOperation: train_id: str station_id: str arrival_s: int departure_s: int dwell_s: int operation_type: str و بعد: Station Resource + Operational Window + Station Capacity وارد Solver می‌شود. 31. مرحله بعد: Conflict Graph واقعی نسخه فعلی: [ G_C=(V_C,E_C) ] را implicit می‌سازد. نسخه بعد باید آن را explicit کند: @dataclass(frozen=True) class ConflictNode: id: str train_id: str resource_id: str entry_s: int exit_s: int و: @dataclass(frozen=True) class ConflictEdge: node_a: str node_b: str conflict_type: str minimum_separation_s: int سپس: Train Paths ↓ Occupancies ↓ Conflict Nodes ↓ Conflict Edges ↓ Conflict Graph ↓ CP-SAT 32. مرحله بعد: Station Crossing Variables در نسخه فعلی Solver فقط ترتیب Blockها را کنترل می‌کند. نسخه بعد باید Variable داشته باشد: [ X_{i,s,k} ] یعنی: Train i در Station s از Track k استفاده کند. مثلاً: S03 ├── Track 1 └── Track 2 و: track_assignment[ train_id, station_id ] 33. مرحله بعد: Crossing Decision برای دو قطار: TR01 A → D TR02 D → A Solver باید بتواند تصمیم بگیرد: TR01 first TR02 second یا: TR02 first TR01 second و محل Crossing نیز Decision Variable باشد: S01 S02 S03 در نتیجه مدل از: Fixed Crossing به: Optimized Crossing تبدیل می‌شود. این نقطه، جهش مهمی در کیفیت Scheduling Engine خواهد بود. 34. مرحله بعد: Operational Regime بعد باید سه Regime را وارد کنیم: REGIME-A Alternating REGIME-B Directional Batch REGIME-C Optimized Mixed و Solver بتواند بین آنها Scenario بسازد. مثلاً: Scenario 1 Alternating در برابر: Scenario 2 Outbound Batch و سپس: [ C_r^{(1)} ] در برابر: [ C_r^{(2)} ] محاسبه شود. 35. مرحله بعد: Capacity Profile Capacity دیگر فقط یک عدد نیست: { "routeId": "R001", "profiles": [ { "direction": "OUTBOUND", "loadState": "LOADED", "timeWindow": "06:00-12:00", "capacity": 18 }, { "direction": "INBOUND", "loadState": "EMPTY", "timeWindow": "12:00-18:00", "capacity": 15 } ] } و بعد: [ C=f( Route, OD, Direction, LoadState, TrainType, TimeWindow ) ] 36. مرحله بعد: اتصال Marketplace بعد از Capacity Profile: Market Request ↓ Demand ↓ Wagon Requirement ↓ Train Requirement ↓ Capacity Profile ↓ Allocation سه مفهوم همچنان جدا می‌مانند: [ D_{market} \neq D_{transportable} \neq D_{allocated} ] 37. مرحله بعد: PostgreSQL وقتی Engine با Fixture کاملاً تثبیت شد، Schema را اضافه می‌کنیم: stations blocks routes route_blocks trains train_runs train_formations train_formation_items train_station_calls baseline_schedules schedules block_occupancies station_operations conflicts runs scenarios capacity_results capacity_proofs binding_constraints explanation_traces و مهم‌تر: data_versions model_versions algorithm_versions 38. مرحله بعد: API سپس: POST /api/v1/runs POST /api/v1/schedules/generate POST /api/v1/schedules/validate POST /api/v1/capacity/routes/{routeId}/calculate GET /api/v1/capacity-results/{runId} GET /api/v1/runs/{runId}/proof و: GET /api/v1/runs/{runId}/explanation 39. جایگاه این MVP در معماری کلان MARKETPLACE │ ▼ MARKET DEMAND │ ▼ FREIGHT FLOW │ ▼ TRAIN FORMATION │ ▼ TRAIN RUN │ ▼ ┌─────────────────┐ │ SCHEDULING CORE │ └─────────────────┘ │ ┌───────────┼───────────┐ ▼ ▼ ▼ TrainPath Conflict Station Engine Rules │ │ │ └───────────┼───────────┘ ▼ CP-SAT │ ▼ FEASIBLE SCHEDULE │ ▼ INDEPENDENT VALIDATOR │ ▼ CAPACITY SEARCH │ ▼ CAPACITY PROOF │ ┌────────┴────────┐ ▼ ▼ BOTTLENECK EXPLANATION │ │ └────────┬────────┘ ▼ CAPACITY OFFER │ ▼ MARKETPLACE 40. Definition of Done این مرحله این بسته زمانی از نظر Engineering موفق است که: [✓] Python project structure [✓] requirements.txt [✓] pyproject.toml [✓] Domain classes [✓] Mixed-track fixture [✓] TrainRun [✓] Loaded / Empty [✓] Single Track [✓] Double Track [✓] Conflict Engine [✓] CP-SAT model [✓] Schedule extraction [✓] Independent validation [✓] Capacity Search [✓] Capacity Proof structure [✓] pytest fixtures [✓] Domain tests [✓] Conflict tests [✓] Solver tests [✓] Validation tests [✓] Capacity tests و موارد زیر عمداً برای مرحله بعد هستند: [ ] Real Excel Adapter [ ] Access Adapter [ ] PostgreSQL [ ] TrainFormation persistence [ ] Station Track Assignment [ ] Crossing Optimization [ ] Operational Windows [ ] Batch Optimization [ ] Wagon Cycle [ ] Locomotive Cycle [ ] Network Optimization [ ] Marketplace API [ ] GIS [ ] Production Authentication 41. مهم‌ترین تصمیم معماری در این مرحله سه لایه را عمداً از هم جدا نگه می‌داریم: DOMAIN MODEL ↓ SCHEDULING MODEL ↓ SOLVER MODEL یعنی مثلاً: TrainRun نباید مستقیماً تبدیل شود به: CP-SAT Variable بلکه: TrainRun ↓ Scheduling Entity ↓ Solver Variable این تفکیک بعداً اجازه می‌دهد CP-SAT را با: MILP CP Optimizer Heuristic Metaheuristic Simulation Hybrid Solver تعویض یا ترکیب کنیم، بدون اینکه Canonical Domain Model تغییر کند. 42. اصل نهایی این بسته هدف این کد این نیست که همین امروز «موتور ظرفیت نهایی راه‌آهن» باشد. هدف آن اثبات اولین زنجیره واقعی است: [ \boxed{ Infrastructure \rightarrow Train \rightarrow TrainPath \rightarrow Conflict \rightarrow Schedule \rightarrow Validation \rightarrow Capacity } ] و مهم‌تر: [ \boxed{ No\ Feasible\ Schedule \Rightarrow No\ Operational\ Capacity } ] بعد از تثبیت همین هسته، داده‌های واقعی Excel و Access شما را وارد می‌کنیم و به‌جای Fixture مصنوعی، Baseline Schedule واقعی را به همین Engine می‌دهیم. یک نکته مهم هم باقی می‌ماند: کدی که بالا دادم از نظر معماری قابل شروع است، ولی قبل از اینکه آن را به‌عنوان Solver Production استفاده کنیم باید دو اصلاح جدی انجام دهیم: اول، جهت حرکت Inbound را واقعاً روی توپولوژی مسیر مدل کنیم، نه اینکه صرفاً همان ترتیب فیزیکی Blockها را برگردانیم؛ دوم، Crossing Station و Track Assignment را به متغیرهای واقعی CP-SAT تبدیل کنیم. این دو مورد، مرحله بعدی‌ای هستند که MVP را از یک scheduling demonstrator به یک Railway Scheduling Core واقعی نزدیک می‌کنند. --- ## ادامه 13 — پرسش کاربر (`message-id:e0b6e46c-fed5-4c86-86ed-5c9f0cd03dbd`) بله لطفا --- ## ادامه 13 — پاسخ دستیار (`message-id:3d02e53b-2ef0-409c-990c-162d5988314a`) دقیقاً. این مرحله از نظر معماری خیلی مهم است، چون از اینجا به بعد شبکه ریلی را واقعاً به‌صورت Directed Time-Space Network مدل می‌کنیم. CP-SAT هم برای چنین مسائل زمان‌بندی/منابع مناسب است و در مدل‌های scheduling می‌تواند تقدم‌وتأخر و منابع انحصاری را مدل کند. Google for Developers +1 Technical Design Specification — توپولوژی جهت‌دار، Crossing Station و Track Assignment در Scheduling Engine — نسخه 0.3 1. هدف نسخه 0.3 در نسخه قبل، یک ضعف مهم داشتیم: Route: A → S01 → S02 → S03 → D برای قطار Inbound نیز عملاً همین ترتیب Blockها استفاده می‌شد. این برای Demo کافی است، اما برای Railway Scheduling Engine واقعی کافی نیست. از این نسخه: [ Route \neq مجرد\ لیست\ Blockها ] بلکه: [ Network = Directed\ Infrastructure\ Graph ] خواهد بود. هدف این نسخه: مدل‌کردن جهت واقعی حرکت ساخت مسیر Outbound ساخت مسیر Inbound تعریف Crossing Station تعریف Station Track تخصیص Track به قطار تصمیم‌گیری محل Crossing توسط Solver انتقال Crossing از Rule ثابت به Decision Variable تولید Schedule قابل نمایش در Time-Space Diagram 2. مدل جدید شبکه مدل فیزیکی: A │ │ B01 │ S01 │ │ B02 │ S02 │ │ B03 │ S03 │ │ B04 │ D اما Graph واقعی: B01 A ─────────────────► S01 │ │ B02 ▼ S02 │ │ B03 ▼ S03 │ │ B04 ▼ D برای Inbound: D │ │ B04_REV ▼ S03 │ │ B03_REV ▼ S02 │ │ B02_REV ▼ S01 │ │ B01_REV ▼ A بنابراین یک Block فیزیکی: B01 A ↔ S01 دارای دو Movement Direction است: B01_OUT A → S01 B01_IN S01 → A 3. Physical Block در برابر Directed Block این دو مفهوم باید از هم جدا شوند. Physical Block @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str distance_m: int track_type: TrackType مثلاً: PB01 A ↔ S01 Directed Block @dataclass(frozen=True) class DirectedBlock: id: str physical_block_id: str from_station: str to_station: str direction: Direction دو نمونه: PB01 DB01_OUT A → S01 DB01_IN S01 → A 4. دلیل این تفکیک این کار چند مسئله را هم‌زمان حل می‌کند. مثلاً: PhysicalBlock = B01 ممکن است: A → S01 را برای قطار Loaded و: S01 → A را برای Empty Train سرویس دهد. ولی زمان حرکت، سرعت، محدودیت، Occupancy و Conflict باید بر اساس Directed Movement قابل محاسبه باشد. بنابراین: [ PhysicalInfrastructure \neq OperationalMovement ] 5. مدل جدید Domain فایل: app/domain/network.py from dataclasses import dataclass from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class Direction(str, Enum): OUTBOUND = "OUTBOUND" INBOUND = "INBOUND" @dataclass(frozen=True) class StationTrack: id: str station_id: str track_number: int usable_length_m: int is_main_track: bool = False is_crossing_track: bool = False @dataclass(frozen=True) class Station: id: str name: str tracks: tuple[StationTrack, ...] crossing_allowed: bool = False @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str distance_m: int track_type: TrackType running_loaded_ab_s: int running_loaded_ba_s: int running_empty_ab_s: int running_empty_ba_s: int @dataclass(frozen=True) class DirectedBlock: id: str physical_block_id: str from_station: str to_station: str direction: Direction running_loaded_s: int running_empty_s: int 6. ساخت Directed Network def build_directed_blocks( physical_block: PhysicalBlock, ) -> tuple[DirectedBlock, DirectedBlock]: outbound = DirectedBlock( id=f"{physical_block.id}_AB", physical_block_id=physical_block.id, from_station=physical_block.station_a, to_station=physical_block.station_b, direction=Direction.OUTBOUND, running_loaded_s=( physical_block.running_loaded_ab_s ), running_empty_s=( physical_block.running_empty_ab_s ), ) inbound = DirectedBlock( id=f"{physical_block.id}_BA", physical_block_id=physical_block.id, from_station=physical_block.station_b, to_station=physical_block.station_a, direction=Direction.INBOUND, running_loaded_s=( physical_block.running_loaded_ba_s ), running_empty_s=( physical_block.running_empty_ba_s ), ) return outbound, inbound 7. Route Direction Route دیگر فقط: route.blocks نیست. بلکه: route.outbound_blocks route.inbound_blocks داریم. مثلاً: Outbound: B01_AB B02_AB B03_AB B04_AB و: Inbound: B04_BA B03_BA B02_BA B01_BA 8. TrainRun و Direction TrainRun: @dataclass(frozen=True) class TrainRun: id: str origin: str destination: str direction: Direction load_state: LoadState length_m: int weight_t: int earliest_departure_s: int latest_departure_s: int latest_arrival_s: int اما مسیر قطار از روی: origin + destination + direction Resolve می‌شود. 9. Route Resolver def resolve_blocks_for_train( train: TrainRun, route: Route, ) -> tuple[DirectedBlock, ...]: if train.direction == Direction.OUTBOUND: return route.outbound_blocks return route.inbound_blocks در نسخه واقعی بهتر است این تابع Graph Search انجام دهد: Origin ↓ Graph Search ↓ Destination ↓ Directed Path نه اینکه صرفاً بر اساس Direction تصمیم بگیرد. 10. Crossing Station Crossing Station یک مفهوم مستقل است. مثلاً: S01 S02 S03 می‌توانند Crossing Point باشند. اما: A D لزوماً Crossing Station نیستند. مدل: @dataclass(frozen=True) class CrossingStation: station_id: str allowed: bool min_dwell_s: int preparation_s: int release_s: int 11. Station Track Assignment هر Station می‌تواند چند Track داشته باشد: S02 Track 1 ───────────── Track 2 ───────────── Track 3 ───────────── مدل: @dataclass(frozen=True) class StationTrack: id: str station_id: str track_number: int usable_length_m: int is_main_track: bool is_crossing_track: bool 12. Constraint طول قطار برای هر Assignment: [ TrainLength_i \le UsableLength_{s,k} ] پس: model.Add( train_length[i] <= usable_length[s, k] ).OnlyEnforceIf( track_assignment[i, s, k] ) در واقع چون train_length پارامتر است: if train.length_m > track.usable_length_m: assignment_possible = False و آن Assignment اصلاً وارد Solver نمی‌شود. 13. Track Assignment Variable برای هر قطار، ایستگاه و Track: [ X_{i,s,k}\in{0,1} ] تعریف می‌کنیم. track_assignment[ train_id, station_id, track_id ] معنی: 1 = قطار روی این Track قرار دارد 0 = قرار ندارد 14. Exactly One Track اگر قطار باید در ایستگاه S02 روی یکی از Trackها قرار گیرد: [ \sum_k X_{i,s,k}=1 ] کد: model.add_exactly_one( track_assignment[ train.id, station.id, track.id ] for track in station.tracks ) 15. اما یک نکته مهم در Railway Scheduling همیشه لازم نیست هر Train یک Track مستقل داشته باشد. بنابراین در مدل Production بهتر است: StationActivity را به: Track Occupancy تبدیل کنیم. یعنی: Train ↓ Station Activity ↓ Track Occupancy و فقط در صورت نیاز Assignment شود. 16. Station Occupancy تعریف: @dataclass(frozen=True) class StationOccupancy: train_id: str station_id: str track_id: str start_s: int end_s: int این Object بعداً مستقیماً وارد Conflict Engine می‌شود. 17. Conflict Station اگر: TR01 → Track 1 10:00–10:30 و: TR02 → Track 1 10:20–10:50 داشته باشیم: Station Track Conflict ولی اگر: TR01 → Track 1 TR02 → Track 2 باشد: No Track Conflict البته ممکن است همچنان: Junction Conflict Route Conflict Platform Conflict Operational Conflict وجود داشته باشد. 18. Crossing Decision دو قطار: TR01: A → D TR02: D → A ممکن است در: S01 از یکدیگر عبور کنند. یا: S02 یا: S03 بنابراین Crossing Location باید Variable باشد. 19. Crossing Variable برای هر Pair قطار مخالف: [ Y_{i,j,s}\in{0,1} ] معنی: Y = 1 یعنی: قطار i و j در Station s با یکدیگر Cross می‌کنند. 20. Exactly One Crossing Station اگر دو قطار مخالف الزاماً باید در یک Station یکدیگر را Cross کنند: [ \sum_s Y_{i,j,s}=1 ] ولی در نسخه Production بهتر است: [ \sum_s Y_{i,j,s}\le1 ] و اگر واقعاً مسیرهای آنها در Single Track مشترک باشند: [ \sum_s Y_{i,j,s}\ge1 ] در نتیجه: [ \sum_s Y_{i,j,s}=1 ] 21. شرط مهم Crossing فقط در Stationهایی مجاز است که: station.crossing_allowed == True بنابراین: for station in stations: if not station.crossing_allowed: model.add( crossing[i, j, station.id] == 0 ) 22. Crossing و زمان فرض کنیم: TR01: A → S01 TR02: D → S01 برای Cross در S01: قطار اول باید به Station برسد: [ A_{i,S01} ] و قطار دوم نیز: [ A_{j,S01} ] بعد از آن باید ترتیب ورود/خروج رعایت شود. 23. مدل ساده Crossing برای Crossing در S: Train i arrives ↓ Station operation ↓ Train i departs Train j arrives ↓ Station operation ↓ Train j departs دو حالت: حالت A [ D_i(S)+T_{sep}\le A_j(S) ] حالت B [ D_j(S)+T_{sep}\le A_i(S) ] 24. Linking Crossing Variable اگر: crossing[i, j, s] == 1 آنگاه یکی از دو Ordering باید فعال باشد. برای این کار دو Boolean دیگر: i_before_j j_before_i داریم. و: crossing → i_before_j XOR j_before_i 25. مدل CP-SAT شبه‌کد: for i, j in opposing_pairs: for s in crossing_stations: c = crossing[i, j, s] order_ij = model.new_bool_var( f"{i}_before_{j}_{s}" ) order_ji = model.new_bool_var( f"{j}_before_{i}_{s}" ) model.add( order_ij + order_ji == 1 ).only_enforce_if(c) model.add( departure[i, s] + separation <= arrival[j, s] ).only_enforce_if( [c, order_ij] ) model.add( departure[j, s] + separation <= arrival[i, s] ).only_enforce_if( [c, order_ji] ) این دقیقاً جایی است که CP-SAT برای چنین Scheduling Modelی مفید می‌شود؛ مدل‌های زمان‌بندی OR-Tools نیز تقدم‌وتأخر و منابع غیرقابل‌اشتراک را به همین سبک مدل می‌کنند. 26. اما یک اصلاح مهم در عمل نباید Crossing را فقط به Pair قطار محدود کنیم. چون ممکن است: TR01 TR02 TR03 در یک Station حضور داشته باشند. پس Station باید Capacity داشته باشد: [ N_{simultaneous}(s) \le N_{tracks}(s) ] و هر Track نیز Resource مستقل باشد. 27. Track Resource برای هر Track: Resource: S02-T01 S02-T02 S02-T03 و: station_track_intervals[ station_id, track_id ] ساخته می‌شود. در صورت ثابت بودن Assignment می‌توان از NoOverlap برای جلوگیری از هم‌پوشانی روی یک Track استفاده کرد؛ این الگو در مثال رسمی Job Shop OR-Tools نیز برای منابع غیرقابل‌اشتراک استفاده شده است. 28. Time-Space Model جدید اکنون Time-Space Diagram اطلاعات بسیار بیشتری خواهد داشت: Time → 08:00 09:00 10:00 11:00 A ●───────────────► TR01 S01 ●───────● Track 1 ●───────● Track 2 S02 ●──────────► TR01 S03 ●────► D ● و برای Inbound: D ● │ S03 ●──┘ / S02 ●─── / S01 ●──── / A ● در نتیجه Time-Space دیگر فقط visualization نیست؛ مستقیماً از Schedule تولید می‌شود. 29. TrainPath جدید @dataclass(frozen=True) class TrainPathPoint: train_id: str station_id: str sequence: int arrival_s: int departure_s: int assigned_track_id: str | None و: @dataclass(frozen=True) class TrainPath: train_id: str points: tuple[ TrainPathPoint, ... ] 30. Block Movement جدید @dataclass(frozen=True) class BlockMovement: train_id: str physical_block_id: str directed_block_id: str from_station: str to_station: str entry_s: int exit_s: int blocking_start_s: int blocking_end_s: int این بسیار بهتر از مدل قبلی است. چون مشخص می‌کند: کدام Block فیزیکی؟ کدام جهت؟ از کدام Station؟ به کدام Station؟ چه زمانی وارد؟ چه زمانی خارج؟ چه زمانی واقعاً Resource را اشغال کرده؟ 31. Running Time vs Blocking Time این دو نباید یکی باشند: [ T_{running} ] و: [ T_{blocking} ] مثلاً: Running: 42 min Entry clearance: 2 min Exit clearance: 2 min Blocking: 46 min بنابراین: T_{entry} + T_{running} + T_{exit} ] این تفاوت برای ظرفیت بسیار مهم است. 32. Single Track Occupancy در Single Track: Physical Block B01 یک Resource مشترک دارد. پس: TR01 A → S01 و: TR02 S01 → A نمی‌توانند هم‌زمان Block را Occupy کنند. 33. Double Track برای Double Track: Physical Block B03 به دو Resource تبدیل می‌شود: B03_AB_TRACK B03_BA_TRACK پس: TR01 A → D از: B03_AB و: TR02 D → A از: B03_BA استفاده می‌کند. در نتیجه Opposite Direction ذاتاً Conflict ندارد. 34. اما Double Track نیز Resource دارد این نکته بسیار مهم است. Double Track به معنی: Unlimited Capacity نیست. هنوز ممکن است: Station Junction Signal Platform Turnout Terminal Level Crossing Capacity را محدود کنند. پس: [ DoubleTrack \neq NoConstraint ] 35. Mixed Track برای Route: B01 SINGLE B02 SINGLE B03 DOUBLE B04 SINGLE قطار ممکن است: A │ ▼ B01 SINGLE │ ▼ S01 │ ▼ B02 SINGLE │ ▼ S02 │ ▼ B03 DOUBLE │ ▼ S03 │ ▼ B04 SINGLE │ ▼ D و Solver باید بتواند از B03 برای عبور هم‌زمان استفاده کند ولی در B01/B02/B04 Conflict را حل کند. 36. Operational Regime جدید اکنون می‌توانیم Regime را واقعاً مدل کنیم. Regime A — Alternating Outbound ↓ Switch ↓ Inbound ↓ Switch ↓ Outbound Regime B — Directional Batch Outbound Outbound Outbound Outbound ↓ Switch ↓ Inbound Inbound Inbound Regime C — Optimized Mixed Outbound ↘ Inbound → Crossing ↗ Outbound در Regime C، Solver خودش Crossing و ترتیب را انتخاب می‌کند. 37. Batch Variable برای مرحله بعد: [ B_k\in{0,1} ] یا: batch_id[train] و: batch_direction[k] batch_start[k] batch_end[k] batch_size[k] 38. هدف Solver فعلاً Objective: [ \min \sum_i Arrival_i ] است. اما در نسخه واقعی باید Multi-Objective داشته باشیم: Priority 1 Feasibility Priority 2 Fixed Movement Preservation Priority 3 Demand Served Priority 4 Capacity Priority 5 Total Travel Time Priority 6 Waiting Priority 7 Robustness Priority 8 Operational Cost 39. Objective پیشنهادی مثلاً: W_W Waiting W_C Conflicts W_R Risk \right) ] ولی در Production بهتر است Lexicographic باشد، نه اینکه همه چیز را با Weightهای دلخواه مخلوط کنیم. 40. Feasibility قبل از Optimization این اصل حفظ می‌شود: Phase 1 Feasibility Phase 2 Optimization یعنی اول: [ \exists Schedule? ] بعد: [ Best(Schedule) ] این موضوع برای Capacity Proof نیز حیاتی است. 41. Capacity Search جدید حالا: [ C_r= \max { F: Schedule(F) \text{ is feasible} } ] ولی Schedule دیگر فقط زمان Block نیست. شامل: Train Path + Block Occupancy + Station Track + Crossing + Operational Windows + Single Track Conflicts + Double Track Resources است. 42. Capacity Proof جدید مثلاً: { "routeId": "R001", "capacity": 24, "proof": { "F24": { "feasible": true, "scheduleId": "SCH-024" }, "F25": { "feasible": false, "bindingConstraint": { "type": "SINGLE_TRACK", "resource": "B02" } } } } این همان چیزی است که UI بعداً باید نمایش دهد. 43. Explanation Trace برای F25: Capacity = 24 ↓ Attempt F = 25 ↓ Schedule Search ↓ Conflict ↓ B02 ↓ Single Track ↓ No feasible crossing ↓ S01/S02/S03 unavailable ↓ F=25 infeasible این Trace باید Object رسمی سیستم باشد: @dataclass(frozen=True) class ExplanationStep: sequence: int entity_type: str entity_id: str message: str evidence: dict 44. Conflict Types جدید از این نسخه: SINGLE_TRACK_OPPOSITE SAME_DIRECTION_HEADWAY STATION_TRACK STATION_CAPACITY CROSSING_ORDER JUNCTION TURNOUT TERMINAL OPERATIONAL_WINDOW TRAIN_LENGTH TRAIN_WEIGHT LOCOMOTIVE WAGON BUFFER FIXED_MOVEMENT 45. Conflict Graph اکنون: [ G_C=(V_C,E_C) ] می‌شود: Train Resource Occupancy │ ▼ Conflict Node │ ▼ Conflict Edge مثلاً: TR01-B02 │ │ conflict ▼ TR04-B02 و: TR01-S02-T01 │ │ conflict ▼ TR03-S02-T01 46. نتیجه معماری مهم در این مرحله سه Graph داریم: Graph 1 — Infrastructure Graph [ G_I=(V,E) ] شبکه فیزیکی. Graph 2 — Time-Space Graph [ G_T=(N,A) ] مسیرهای قطار در زمان. Graph 3 — Conflict Graph [ G_C=(V_C,E_C) ] تعارض‌های زمانی/منبعی. و موتور اصلی: GI ↓ GT ↓ GC ↓ CP-SAT ↓ Schedule 47. ارتباط با معماری Marketplace این تغییر هیچ چیزی از اصل Marketplace کم نمی‌کند. برعکس: Marketplace ↓ Demand ↓ Wagon Requirement ↓ Train Formation ↓ TrainRun ↓ Directed TrainPath ↓ Crossing ↓ Schedule ↓ Capacity حالا ظرفیت تولیدشده واقعاً operationally meaningful می‌شود. 48. ارتباط با Excel فعلی از فایل Excel شما: شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت اطلاعات Baseline را می‌گیریم. اما از Access: StationName StationNumber Sequence time_in time_take time_out RequiredWait ... TrainPath جزئی‌تر ساخته می‌شود. بنابراین Mapping آینده: Excel ↓ Train Service / Operating Pattern Access ↓ Train Station Calls Infrastructure ↓ Physical Blocks Canonical Model ↓ Directed Train Path Solver ↓ Generated Schedule 49. یک نکته بسیار مهم درباره Access فیلدهای: time_take RequiredWait seir faultV sumDistancezz هنوز نباید بدون Data Dictionary وارد Solver شوند. تا زمانی که Semantics آنها تأیید نشده: Source Field → Mapping Status = UNVERIFIED → Not Production Solver Input این همان قاعده قبلی ماست: [ No\ Verified\ Mapping \Rightarrow No\ Production\ Use ] 50. Test جدید برای Direction def test_inbound_path_is_reverse_of_physical_route(): problem = build_fixture_problem(2) outbound = resolve_blocks_for_train( problem.trains[0], problem.route, ) inbound = resolve_blocks_for_train( problem.trains[1], problem.route, ) assert outbound[0].from_station == "A" assert outbound[-1].to_station == "D" assert inbound[0].from_station == "D" assert inbound[-1].to_station == "A" 51. Test Crossing def test_crossing_station_is_selected(): result = solve(problem) assert result.feasible crossings = result.schedule.crossings assert len(crossings) > 0 در نسخه کامل‌تر: assert all( crossing.station_id in {"S01", "S02", "S03"} for crossing in crossings ) 52. Test Track Assignment def test_train_is_assigned_to_valid_station_track(): result = solve(problem) assert result.feasible for assignment in ( result.schedule.track_assignments ): assert ( assignment.train_length_m <= assignment.usable_length_m ) 53. Test Opposite Direction on Double Track def test_opposite_trains_can_share_double_track(): problem = build_fixture_problem(2) result = solve(problem) assert result.feasible validation = validate_schedule( result.schedule, problem, ) assert validation.feasible ولی در تست واقعی‌تر باید مشخصاً اثبات کنیم: TR01 → B03_AB TR02 → B03_BA و: No DOUBLE_TRACK_OPPOSITE conflict 54. Test Single Track Crossing این تست بسیار مهم است: def test_single_track_requires_ordering(): problem = build_fixture_problem(2) result = solve(problem) assert result.feasible for crossing in result.schedule.crossings: assert crossing.station_id in { "S01", "S02", "S03", } و بعد باید Verify کنیم که: قبل از Crossing: Train A وارد Station شده بعد: Train B وارد Station شده و هیچ‌کدام Single Track را هم‌زمان Occupy نکرده‌اند. 55. معیار پذیرش نسخه 0.3 نسخه جدید وقتی Accepted است که: [✓] Physical Block [✓] Directed Block [✓] Outbound Path [✓] Inbound Path [✓] Crossing Station [✓] Station Track [✓] Track Assignment [✓] Station Occupancy [✓] Single Track Conflict [✓] Double Track Direction [✓] Crossing Decision [✓] CP-SAT Integration [✓] Independent Validation [✓] Time-Space Path [✓] Capacity Proof [✓] Explanation Trace 56. خروجی مورد انتظار Engine در پایان Run: RUN │ ├── Train Paths │ ├── Station Assignments │ ├── Block Occupancies │ ├── Crossings │ ├── Conflicts │ ├── Validated Schedule │ ├── Capacity │ ├── Binding Constraints │ └── Explanation این ساختار همان چیزی است که بعداً UI، API، GIS و Marketplace روی آن سوار می‌شوند. 57. Definition of Done مهم‌ترین معیار: اگر Engine اعلام کند: [ C_r=24 ] باید بتواند ارائه کند: Schedule(F=24) ↓ VALID و برای: [ F=25 ] باید بتواند نشان دهد: Schedule(F=25) ↓ INFEASIBLE ↓ Why? ↓ Binding Constraint ↓ Evidence بنابراین: Feasible\ Schedule + Proof } ] نه: Formula ] 58. مرحله بعد از این نسخه بعد از تثبیت این مدل، من پیشنهاد می‌کنم مستقیماً برویم سراغ: VERSION 0.4 Operational Regime + Batch Engine یعنی Solver دیگر فقط بگوید: TR01 TR02 TR03 TR04 را چگونه عبور دهد؛ بلکه خودش بتواند تصمیم بگیرد: Alternating VS Directional Batch VS Mixed Optimized و سپس: [ C_r^{Alternating} ] [ C_r^{Batch} ] [ C_r^{Mixed} ] را تولید کند و Switch Time، Batch Size، Empty Train Flow و Crossing Location را هم‌زمان بهینه کند. این مرحله عملاً هسته اصلی ایده‌ای است که از ابتدا در مدل ریاضی شما داشتیم: ظرفیت Single/Mixed Track فقط تابع Headway نیست؛ رژیم بهره‌برداری، Batch، Crossing، Switch، Empty Flow و منابع مشترک هم‌زمان تعیین‌کننده‌اند. یک نکته فنی مهم: در CP-SAT همه زمان‌ها و ضرایب این مدل باید به شکل عدد صحیح مدل شوند؛ مستندات رسمی OR-Tools نیز CP-SAT را بر مدل‌های integer-based بنا می‌کنند. همچنین برای منابع غیرقابل‌اشتراک، الگوی NoOverlap در مدل‌های scheduling رسمی OR-Tools استفاده می‌شود. Google for Developers +1 گام بعدی منطقی را من Version 0.4 — Operational Regime + Directional Batch Engine می‌دانم؛ چون بعد از آن تازه می‌توانیم یک Capacity Proof واقعاً معنی‌دار برای مقایسه‌ی «Alternating vs Batch vs Mixed» بسازیم و مستقیماً به مدل ریاضی اصلی شما وصلش کنیم. --- ## ادامه 14 — پرسش کاربر (`message-id:ada20ce9-1c8b-4358-8036-306bc2fd6d71`) بله لطفا --- ## ادامه 14 — پاسخ دستیار (`message-id:4ecce69f-c965-4e42-baca-a5989e515bc8`) حتماً. این نسخه یک گام مهم‌تر است: Operational Regime و Directional Batch را از مفهوم نظری به Engine قابل اجرا تبدیل می‌کنیم. همچنین یک نکته را از همین ابتدا اصلاح می‌کنم: Batch نباید ذاتاً «هرچه بزرگ‌تر بهتر» فرض شود؛ اندازه Batch خودش متغیر تصمیم است و باید اثر آن بر ظرفیت، Waiting، Switch Time، Empty Flow و Robustness سنجیده شود. Technical Design Specification — Operational Regime و Directional Batch Engine — نسخه 0.4 1. هدف نسخه 0.4 در نسخه 0.3 شبکه جهت‌دار، Crossing و Track Assignment را وارد مدل کردیم. اکنون هدف این نسخه تبدیل این مفاهیم به یک: Operational Regime Engine است. Engine باید بتواند بین این سه حالت تفاوت واقعی ایجاد کند: REGIME-A Alternating Operation REGIME-B Directional Batch Operation REGIME-C Optimized Mixed Operation و برای هرکدام Schedule تولید کند. سپس: [ C_r^{A} ] [ C_r^{B} ] [ C_r^{C} ] را محاسبه و با معیارهای عملیاتی مقایسه کند. 2. اصل بنیادی نباید فرض کنیم: [ Larger\ Batch \Rightarrow Higher\ Capacity ] ممکن است Batch بزرگ‌تر باعث شود: Waiting ↓ Switch Frequency ↓ Crossing Conflict ↓ اما هم‌زمان: Direction Imbalance ↑ Empty Waiting ↑ Demand Mismatch ↑ Terminal Congestion ↑ Robustness ↓ بنابراین: DecisionVariable ] نه: FixedParameter ] 3. Operational Regime به‌عنوان Domain Object from dataclasses import dataclass from enum import Enum class OperationalRegime(str, Enum): ALTERNATING = "ALTERNATING" DIRECTIONAL_BATCH = "DIRECTIONAL_BATCH" OPTIMIZED_MIXED = "OPTIMIZED_MIXED" @dataclass(frozen=True) class OperationalRegimeConfig: regime: OperationalRegime min_batch_size: int max_batch_size: int switch_time_s: int allow_empty_flow: bool allow_mixed_direction: bool allow_crossing_optimization: bool max_waiting_s: int | None = None 4. Regime A — Alternating در این حالت جهت‌ها یکی‌درمیان حرکت می‌کنند: OUT IN OUT IN OUT IN یا: IN OUT IN OUT هدف این Regime: ساده قابل پیش‌بینی مناسب Baseline مناسب عملیات سنتی کمترین پیچیدگی تصمیم‌گیری 5. Constraint رژیم Alternating برای Sequence حرکت: [ d_i\neq d_{i+1} ] یعنی: OUT → IN IN → OUT نباید: OUT → OUT باشد. 6. مدل CP-SAT برای Alternating برای هر Train: direction_is_outbound[i] و برای دو Train متوالی: model.Add( direction_is_outbound[i] != direction_is_outbound[i + 1] ) البته در مدل Production بهتر است Sequence قطارها نیز Variable باشد و صرفاً لیست ثابت قطارها را Alternating نکنیم. 7. Regime B — Directional Batch مثلاً: OUT OUT OUT OUT OUT SWITCH IN IN IN IN SWITCH OUT OUT ... Batch: [ K=(direction,start,end,N) ] است. 8. Batch Object @dataclass(frozen=True) class OperationalBatch: id: str direction: Direction train_ids: tuple[str, ...] start_s: int end_s: int switch_before_s: int switch_after_s: int batch_size: int 9. Batch Duration برای Batch: T_{first} + (N_K-1)H + T_{clear} ] که در آن: (T_{first}): زمان لازم برای قطار اول (N_K): تعداد قطار (H): فاصله حرکت قطارها (T_{clear}): زمان آزادسازی انتهای Batch پس: [ T_K \neq N_K\times T_{run} ] زیرا قطارهای Batch با Headway حرکت می‌کنند. 10. Switch Time Switch یک Parameter ساده نیست؛ یک Operational Event است. Last OUT train ↓ Block clear ↓ Route release ↓ Station preparation ↓ Direction switch ↓ First IN train بنابراین: T_{clear} + T_{release} + T_{prepare} + T_{route} ] در مدل ساده: [ T_{switch}=constant ] ولی در مدل Production: f( station, direction, traffic, operating\ regime, resource ) ] 11. Switch Event @dataclass(frozen=True) class SwitchEvent: id: str from_direction: Direction to_direction: Direction start_s: int end_s: int reason: str resource_ids: tuple[str, ...] 12. Switch Constraint اگر Batch قبلی تمام شود: [ End(K_i)+T_{switch} \le Start(K_{i+1}) ] مثلاً: OUT Batch 08:00–10:20 Switch 10:20–10:40 IN Batch 10:40–12:30 13. Batch Size Variable در CP-SAT: [ N_K\in [MinBatch,MaxBatch] ] اما برای MVP ساده‌تر: batch_size[k] به‌صورت Integer Variable تعریف می‌شود. 14. چرا Batch Size را مستقیم Variable کنیم؟ چون ممکن است: Batch = 2 بهتر از: Batch = 6 باشد. مثلاً: Batch 2: Switch overhead = زیاد Waiting = کم Batch 6: Switch overhead = کم Waiting = زیاد در نتیجه Objective باید Trade-off را ببیند. 15. Directional Batch Sequence Sequence کلی: Batch 1 → OUT Batch 2 → IN Batch 3 → OUT Batch 4 → IN ولی اندازه هر Batch: N1 = 3 N2 = 2 N3 = 4 N4 = 3 می‌تواند متفاوت باشد. 16. Batch Formation ورودی: Candidate Trains + Direction + Earliest Departure + Demand + Operational Regime خروجی: Candidate Batches مثلاً: OUT: TR01 TR03 TR05 TR07 IN: TR02 TR04 TR06 17. Batch Candidate Generator def generate_batch_candidates( trains: list[TrainRun], config: OperationalRegimeConfig, ) -> list[OperationalBatch]: candidates = [] for direction in ( Direction.OUTBOUND, Direction.INBOUND, ): directional = [ t for t in trains if t.direction == direction ] directional.sort( key=lambda x: x.earliest_departure_s ) for size in range( config.min_batch_size, config.max_batch_size + 1, ): for start in range( 0, len(directional) - size + 1, ): selected = directional[ start:start + size ] candidates.append( build_candidate_batch( selected, config, ) ) return candidates این Generator هنوز Optimization نیست؛ فقط Candidate Generation است. 18. Batch Selection بعداً Solver باید تصمیم بگیرد: Candidate Batch 1 Candidate Batch 2 Candidate Batch 3 ... کدام‌ها انتخاب شوند. برای هر Batch: [ B_k\in{0,1} ] 19. Batch Selection Constraint هر Train نباید در دو Batch فعال قرار گیرد: [ \sum_k B_{i,k}\le1 ] اگر Train حتماً باید Schedule شود: [ \sum_k B_{i,k}=1 ] 20. Batch Direction Constraint اگر: Batch K Direction = OUT آنگاه تمام Trainهای آن Batch: Direction = OUT هستند. پس: BatchDirection_k ] 21. Batch و Single Track این مهم‌ترین ارتباط است. در Single Track: OUT Batch می‌تواند Block را پشت سر هم Occupy کند. مثلاً: TR01 TR02 TR03 و بعد: SWITCH و سپس: TR04 TR05 در نتیجه: [ T_{switch} ] فقط یک بار برای Batch پرداخت می‌شود، نه برای هر Train. 22. Capacity اثر Batch در مدل ساده: [ C_{batch} \approx \frac{N} {T_{batch}} ] اما باید توجه کنیم: N\cdot H + T_{first} + T_{switch} + T_{clear} ] و بنابراین Capacity تابعی از N است. 23. تابع تقریبی Capacity برای دو جهت: T_{OUT}(N_{OUT}) + T_{switch} + T_{IN}(N_{IN}) + T_{switch} ] و: [ C \approx \frac{N_{OUT}+N_{IN}} {T_{cycle}} ] این فقط Estimation اولیه است. Capacity نهایی همچنان باید از Schedule واقعی حاصل شود: \max { F: Schedule(F) \ feasible } ] 24. Regime C — Optimized Mixed در این حالت دیگر Sequence ثابت نیست. مثلاً: OUT OUT IN OUT IN IN OUT ممکن است به دلیل: Double Track Crossing Station Demand Empty Flow Station Capacity Fixed Train Operational Window بهترین Schedule باشد. 25. Mixed Regime در Mixed: Direction + Crossing + Batch + Station + Block همگی هم‌زمان تصمیم‌گیری می‌شوند. این حالت هسته واقعی Optimization Engine است. 26. Decision Variables مجموعه اصلی: Train sequence [ S_{i,j} ] Direction order [ O_{i,j} ] Batch selection [ B_k ] Batch size [ N_k ] Switch [ W_k ] Crossing [ X_{i,j,s} ] Track Assignment [ A_{i,s,t} ] Departure [ D_i ] Arrival [ A_i ] 27. Conflict Resolution برای دو قطار: TR01 TR02 اگر روی Single Track مشترک باشند: یا: [ Exit_{01}\le Entry_{02} ] یا: [ Exit_{02}\le Entry_{01} ] برای Batch می‌توان این Ordering را به Batch Direction نیز Link کرد. 28. Batch Linking اگر: TR01 TR02 TR03 در یک Batch OUT باشند: TR01 < TR02 < TR03 و: [ D_{TR01} \le D_{TR02} \le D_{TR03} ] با Headway: [ D_{TR02} \ge D_{TR01}+H ] و: [ D_{TR03} \ge D_{TR02}+H ] 29. Switch Linking اگر Batch 1 و Batch 2 جهت مخالف داشته باشند: [ Start(B_2) \ge End(B_1)+T_{switch} ] این Constraint فقط وقتی فعال می‌شود که هر دو Batch انتخاب شده باشند. 30. Empty Train Flow اکنون Empty Train نیز باید در Batch Logic وارد شود. مثلاً: Loaded: A → D Empty: D → A اگر Empty Trainها فقط در پایان Batch برگشت داده شوند: OUT Loaded Batch ↓ D ↓ Empty Formation ↓ IN Empty Batch پس: [ LoadedFlow \leftrightarrow EmptyFlow ] به هم مرتبط می‌شوند. 31. Wagon Cycle برای هر Wagon: Loaded O → D ↓ Unloading ↓ Empty D → O ↓ Loading ↓ Loaded O → D Cycle: T_{loaded} + T_{unload} + T_{empty} + T_{load} ] 32. Wagon Availability اگر: [ W_{available} < W_{required} ] ظرفیت فقط به خاطر Track محدود نمی‌شود. ممکن است: Infrastructure Capacity بالا باشد ولی: Rolling Stock Capacity پایین باشد. بنابراین: f( Infrastructure, Scheduling, Wagon, Locomotive, Demand ) ] 33. Empty Flow Constraint مثلاً: [ E_D(t) \ge EmptyDepartureRequired(t) ] و: [ 0\le E_D(t)\le Buffer_D ] در صورت نبود Wagon: Loaded Train ممکن است نتواند تشکیل شود. 34. Demand-aware Batch Batch نباید فقط بر اساس Train Availability ساخته شود. مثلاً: Demand: A → D = 120 wagons و: Train capacity = 30 wagons نیاز: [ N_{train}=4 ] پس Batch Candidate می‌تواند: 4 Loaded Trains باشد. 35. Market → Batch زنجیره کامل: Market Request ↓ Demand ↓ Wagon Requirement ↓ Train Requirement ↓ Train Formation ↓ Candidate Train Runs ↓ Batch Formation ↓ Schedule ↓ Capacity این اتصال برای Marketplace بسیار مهم است. 36. Objective Function برای MVP: [ \min \left( \alpha W + \beta S + \gamma C \right) ] که: (W): Waiting (S): Switch Cost (C): Conflict Penalty اما Conflict واقعی باید Hard Constraint باشد، نه صرفاً Penalty. بنابراین: [ Conflict=0 ] برای Schedule معتبر. 37. Objective پیشنهادی Production بهتر است به صورت Lexicographic باشد: Level 1 Feasibility Level 2 Demand Served Level 3 Fixed Movement Preservation Level 4 Capacity Level 5 Total Waiting Level 6 Switch Count Level 7 Travel Time Level 8 Robustness 38. Robustness دو Schedule ممکن است هر دو Feasible باشند: Schedule A Slack = 1 min Schedule B Slack = 12 min هر دو: FEASIBLE اما از نظر Operational Robustness یکسان نیستند. بنابراین: f(Slack, Buffer, ConflictMargin, RecoveryTime) ] 39. Minimum Separation برای هر Block: [ Entry_j \ge Exit_i+Headway ] اما Headway باید Resource-specific باشد. مثلاً: B01: 3 min B02: 5 min B03: 2 min Station S02: 7 min پس: headway(resource_id) داریم. 40. Batch Headway در داخل Batch: [ H_{batch} ] می‌تواند متفاوت باشد. مثلاً: Loaded: 6 min Empty: 8 min و: Loaded → Empty ممکن است: 10 min باشد. 41. Operational Window Batch نیز باید Operational Window را رعایت کند. مثلاً: S02: Maintenance 10:00–10:30 اگر Batch: 10:15 به S02 برسد: INFEASIBLE مگر اینکه امکان Waiting داشته باشد. 42. Batch Feasibility برای هر Batch: [✓] Direction [✓] Block [✓] Station [✓] Crossing [✓] Headway [✓] Switch [✓] Operational Window [✓] Wagon [✓] Locomotive 43. Batch Feasibility Object @dataclass(frozen=True) class BatchFeasibility: batch_id: str feasible: bool violations: tuple[str, ...] occupied_blocks: tuple[str, ...] occupied_stations: tuple[str, ...] switch_events: tuple[str, ...] waiting_s: int 44. Operational Regime Result @dataclass(frozen=True) class RegimeResult: regime: OperationalRegime feasible: bool capacity: int train_count: int batch_count: int switch_count: int total_waiting_s: int total_travel_s: int robustness_score: float | None binding_constraints: tuple[str, ...] schedule_id: str | None 45. Regime Comparison خروجی: Regime Feasible Trains Batches Switches Waiting Capacity Alternating ✓ 20 20 19 320 20 Directional Batch ✓ 24 6 5 510 24 Mixed ✓ 26 8 7 390 26 این جدول فقط مثال ساختاری است؛ اعداد Production باید از Solver بیایند. 46. نکته مهم درباره مقایسه Regimeها نباید فقط Capacity را مقایسه کنیم. برای هر Regime: Capacity Demand Served Waiting Travel Time Switches Empty Flow Station Utilization Block Utilization Robustness Binding Constraint باید ثبت شود. 47. Capacity Profile خروجی بهتر: [ C= f( Direction, LoadState, TrainType, Commodity, Time, Regime ) ] مثلاً: Outbound Loaded: 24 Inbound Loaded: 20 Outbound Empty: 18 Inbound Empty: 22 این همان Capacity Profile است. 48. Capacity Search Capacity Search باید برای هر Regime اجرا شود. for regime in regimes: result = capacity_search( problem, regime=regime, ) results.append(result) و: return compare_regimes(results) 49. Binary Search اگر Feasibility نسبت به تعداد Trainها Monotonic باشد: F = 16 ✓ F = 24 ✓ F = 28 ✗ می‌توان Binary Search کرد. اما این فرض باید در هر مدل مشخصاً اعتبارسنجی شود. 50. نکته مهم درباره Monotonicity نباید در تست‌ها کورکورانه فرض کنیم: [ F_1\le F_2 \Rightarrow Feasible(F_2)\le Feasible(F_1) ] در مدل ظرفیت معمولاً چنین رفتاری انتظار می‌رود، ولی با: Fixed Movements Batch Constraints Policy Constraints Demand Coupling Discrete Formation Resource Activation ممکن است رفتار پیچیده‌تر شود. پس Capacity Search باید: Monotonicity Check داشته باشد. 51. Capacity Search API @dataclass(frozen=True) class CapacitySearchConfig: lower_bound: int upper_bound: int use_binary_search: bool = True verify_monotonicity: bool = True verify_boundary: bool = True 52. Capacity Search Result @dataclass(frozen=True) class CapacitySearchResult: accepted_capacity: int lower_feasible: int | None upper_infeasible: int | None monotonicity_verified: bool feasibility_runs: tuple[ "CapacityTrial", ... ] proof_schedule_id: str | None failure_explanation: str | None 53. Capacity Trial @dataclass(frozen=True) class CapacityTrial: requested_flow: int feasible: bool schedule_id: str | None runtime_ms: int binding_constraints: tuple[str, ...] solver_status: str 54. Capacity Proof برای: [ C=24 ] باید حداقل این Evidence وجود داشته باشد: F=24 FEASIBLE F=25 INFEASIBLE و: Schedule(24) باید واقعاً قابل Validate باشد. 55. Proof Chain Capacity = 24 ↓ Schedule(24) ↓ Validation = PASS ↓ All hard constraints = PASS ↓ Schedule(25) ↓ Validation = FAIL ↓ Conflict / Resource ↓ Binding Constraint 56. Binding Constraint ممکن است: Single Track B02 باشد. یا: Station S03 یا: Wagon Availability یا: Locomotive Cycle یا: Operational Window پس: [ BindingConstraint \neq Always\ Infrastructure ] 57. Bottleneck Impact برای هر Constraint: C_{scenario} C_{baseline} ] مثلاً: Add crossing station C = 24 → 28 Add wagon fleet C = 24 → 25 Reduce switch time C = 24 → 26 این‌ها باید از Scenario Engine بیایند، نه از یک محاسبه دستی. 58. سناریوی Batch Size یک Scenario می‌تواند: Baseline: Max Batch = 2 Scenario: Max Batch = 4 باشد. Solver دوباره اجرا می‌شود: Baseline C = ... Scenario C = ... و: [ \Delta C ] محاسبه می‌شود. 59. سناریوی Switch Time مثلاً: Baseline: T_switch = 20 min Scenario: T_switch = 15 min اما نباید نتیجه را: [ Capacity + X ] فرض کنیم. باید کل Schedule دوباره حل شود. 60. Scenario Engine @dataclass(frozen=True) class ScenarioChange: parameter: str old_value: object new_value: object @dataclass(frozen=True) class Scenario: id: str name: str changes: tuple[ScenarioChange, ...] 61. Regime Solver Interface class IOperationalRegimeSolver: def solve( self, problem: SchedulingProblem, config: OperationalRegimeConfig, ) -> RegimeResult: raise NotImplementedError پیاده‌سازی‌ها: AlternatingSolver BatchSolver MixedRegimeSolver 62. معماری پیشنهادی SchedulingProblem │ ▼ Regime Engine │ ┌──────┼────────┐ ▼ ▼ ▼ Alt Batch Mixed │ │ │ └──────┼────────┘ ▼ Candidate Schedule ▼ CP-SAT ▼ Feasibility Checker ▼ Validated Schedule ▼ Capacity Search 63. چرا Regime Engine را از Solver جدا می‌کنیم؟ چون: Operational Logic نباید داخل CP-SAT دفن شود. مثلاً: Batch Rule Switch Rule Empty Flow Rule Domain Logic هستند. Solver فقط باید بگوید: با این قواعد، چه Scheduleای امکان‌پذیر است؟ 64. Solver Abstraction class SchedulingSolver: def build_model( self, problem, regime, ): ... def solve( self, model, ): ... def extract_solution( self, solver, model, ): ... 65. Feasibility Checker مستقل حتی اگر CP-SAT بگوید: OPTIMAL باز هم: validate_schedule(...) باید اجرا شود. این Validator باید مستقل از Solver باشد. 66. چرا؟ برای جلوگیری از: Solver Model Error یا: Mapping Error یا: Extraction Error در نتیجه: [ SolverSuccess \neq ScheduleValidity ] 67. Test Suite نسخه 0.4 ساختار: tests/ │ ├── test_direction.py ├── test_crossing.py ├── test_track_assignment.py ├── test_regime_alternating.py ├── test_regime_batch.py ├── test_regime_mixed.py ├── test_switch.py ├── test_empty_flow.py ├── test_capacity_search.py ├── test_capacity_proof.py └── test_regime_comparison.py 68. Test Alternating def test_alternating_direction(): result = solve( problem, OperationalRegimeConfig( regime=OperationalRegime.ALTERNATING, min_batch_size=1, max_batch_size=1, switch_time_s=600, allow_empty_flow=True, allow_mixed_direction=False, allow_crossing_optimization=True, ), ) assert result.feasible directions = [ x.direction for x in result.schedule.train_runs ] for a, b in zip( directions, directions[1:], ): assert a != b 69. Test Batch def test_directional_batch(): result = solve( problem, OperationalRegimeConfig( regime=OperationalRegime.DIRECTIONAL_BATCH, min_batch_size=2, max_batch_size=4, switch_time_s=600, allow_empty_flow=True, allow_mixed_direction=False, allow_crossing_optimization=True, ), ) assert result.feasible batches = result.schedule.batches assert len(batches) > 0 for batch in batches: assert 2 <= batch.batch_size <= 4 70. Test Switch def test_switch_time_is_respected(): schedule = solve_batch_problem() for previous, current in zip( schedule.batches, schedule.batches[1:], ): if ( previous.direction != current.direction ): assert ( current.start_s >= previous.end_s + schedule.switch_time_s ) 71. Test Mixed def test_mixed_regime_can_change_direction(): result = solve( problem, OperationalRegimeConfig( regime=OperationalRegime.OPTIMIZED_MIXED, min_batch_size=1, max_batch_size=4, switch_time_s=600, allow_empty_flow=True, allow_mixed_direction=True, allow_crossing_optimization=True, ), ) assert result.feasible 72. Test Capacity Proof def test_capacity_proof_has_boundary(): result = capacity_search( problem, regime=OperationalRegime.DIRECTIONAL_BATCH, ) assert result.accepted_capacity >= 0 assert ( result.lower_feasible == result.accepted_capacity ) assert ( result.upper_infeasible is not None ) 73. Test Proof Schedule def test_capacity_has_validated_schedule(): result = capacity_search( problem, regime=OperationalRegime.DIRECTIONAL_BATCH, ) assert result.proof_schedule_id is not None schedule = load_schedule( result.proof_schedule_id ) validation = validate_schedule( schedule, problem, ) assert validation.feasible 74. Test Regime Comparison def test_compare_regimes(): results = compare_operational_regimes( problem ) assert OperationalRegime.ALTERNATING in results assert OperationalRegime.DIRECTIONAL_BATCH in results assert OperationalRegime.OPTIMIZED_MIXED in results 75. Result API Endpoint مفهومی: POST /capacity/regime-analysis Request: { "route_id": "R001", "scenario_id": "BASELINE", "regimes": [ "ALTERNATING", "DIRECTIONAL_BATCH", "OPTIMIZED_MIXED" ] } Response: { "route_id": "R001", "results": [ { "regime": "ALTERNATING", "capacity": 20 }, { "regime": "DIRECTIONAL_BATCH", "capacity": 24 }, { "regime": "OPTIMIZED_MIXED", "capacity": 25 } ] } اعداد اینجا صرفاً ساختار Response را نشان می‌دهند. 76. UI Output صفحه Route Capacity می‌تواند: Operational Regime ┌──────────────────────────────────────┐ │ Alternating │ │ Capacity: 20 │ │ Waiting: 320 min │ │ Switches: 19 │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ Directional Batch │ │ Capacity: 24 │ │ Waiting: 510 min │ │ Switches: 5 │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ Optimized Mixed │ │ Capacity: 25 │ │ Waiting: 390 min │ │ Switches: 7 │ └──────────────────────────────────────┘ اما UI نباید صرفاً «برنده» معرفی کند. باید تفاوت عملیاتی هر Regime را نشان دهد: Capacity Waiting Switches Robustness Binding Constraint Demand Served 77. Capacity Waterfall برای هر Regime: Physical Capacity ↓ Operational Capacity ↓ Regime Capacity ↓ Route Capacity ↓ Demand-Constrained Capacity ↓ Allocated Capacity مثلاً: [ C_b \rightarrow C_s \rightarrow C_{regime} \rightarrow C_r \rightarrow C_{allocated} ] این زنجیره در UI بسیار ارزشمند خواهد بود. 78. تفاوت ظرفیت با تقاضا همچنان سه مفهوم باید جدا باشند: [ D_{market} ] [ D_{transportable} ] [ D_{allocated} ] مثلاً: Market Demand = 35 trains Operational Capacity = 27 trains Allocated = 27 این یعنی: Demand > Capacity نه اینکه: Demand = 27 79. Empty Train Capacity در Result Package: Loaded Outbound Loaded Inbound Empty Outbound Empty Inbound باید جداگانه دیده شوند. چون ممکن است: Loaded Capacity = 30 ولی: Empty Return Capacity = 22 باشد. در این صورت ظرفیت واقعی چرخه Wagon ممکن است کمتر شود. 80. Batch و Wagon Cycle برای هر Batch: Loaded Batch ↓ Destination ↓ Unload ↓ Empty Batch ↓ Origin بنابراین: [ Batch_{loaded} \leftrightarrow Batch_{empty} ] باید قابل Link شدن باشد. 81. Locomotive Cycle همین مسئله برای Locomotive: Train ↓ Destination ↓ Turnback ↓ Return ↓ Maintenance/Fueling ↓ Next Assignment پس در مرحله بعد: [ LocomotiveCycle ] نیز وارد Solver خواهد شد. 82. محدودیت مهم MVP در نسخه 0.4 هنوز این موارد را کامل حل نمی‌کنیم: [ ] Full locomotive circulation [ ] Full wagon circulation [ ] Terminal shunting [ ] Detailed yard operations [ ] Junction interlocking [ ] Microscopic signaling [ ] Maintenance possession [ ] Crew scheduling [ ] Detailed braking simulation این‌ها باید به‌صورت لایه‌ای اضافه شوند. 83. اما Architecture برای آن آماده است Resource Model باید Generic باشد: @dataclass(frozen=True) class Resource: id: str type: str capacity: int direction_dependent: bool time_dependent: bool در نتیجه Resource می‌تواند: BLOCK TRACK STATION JUNCTION PLATFORM LOCOMOTIVE WAGON TERMINAL MAINTENANCE_WINDOW باشد. 84. Resource Usage @dataclass(frozen=True) class ResourceUsage: resource_id: str train_id: str | None batch_id: str | None start_s: int end_s: int quantity: int این Object در آینده ستون فقرات: Conflict Engine Capacity Engine Bottleneck Engine Visualization خواهد بود. 85. Bottleneck Detection برای هر Resource: \frac{OccupiedTime_g} {AvailableTime_g} ] اما: [ Bottleneck \neq HighestUtilization ] به‌تنهایی. باید اثر Marginal آن نیز بررسی شود: C_{after(g)} C_{before} ] 86. خروجی نهایی Version 0.4 Operational Regime │ ├── Alternating │ ├── Directional Batch │ └── Optimized Mixed │ ▼ Candidate Batches │ ▼ CP-SAT Model │ ┌─────────┼─────────┐ ▼ ▼ ▼ Crossing Track Switch │ Assign │ └─────────┼─────────┘ ▼ Schedule │ ▼ Feasibility Check │ ▼ Capacity Search │ ▼ Capacity Proof │ ▼ Explanation 87. Definition of Done — Version 0.4 نسخه 0.4 زمانی Accepted است که: [✓] Alternating Regime [✓] Directional Batch [✓] Optimized Mixed [✓] Batch Candidate Generator [✓] Batch Size Decision [✓] Switch Event [✓] Switch Time Constraint [✓] Single Track Batch [✓] Crossing Integration [✓] Track Assignment Integration [✓] Empty Flow Interface [✓] Demand-aware Train Requirement [✓] Regime Comparison [✓] Capacity Search per Regime [✓] Capacity Proof [✓] Independent Validation [✓] Explanation Trace [✓] Pytest Coverage 88. اصل نهایی نسخه 0.4 اکنون Engine دیگر صرفاً نمی‌گوید: این مسیر چند قطار ظرفیت دارد؟ بلکه می‌تواند بگوید: با چه رژیم بهره‌برداری، چه اندازه Batch، چه محل Crossing، چه زمان Switch، چه ترتیب قطارها و چه تخصیص منابعی این ظرفیت قابل تحقق است؟ و مهم‌تر: \max { F: \exists Schedule, Batch, Crossing, TrackAssignment, OperationalRegime } } ] بنابراین Capacity دیگر یک عدد منفرد نیست؛ یک Operationally Proven Capacity Result است. یک نتیجه معماری مهم از این نسخه این است که اکنون می‌توانیم وارد نسخه 0.5 — Wagon Cycle + Locomotive Cycle + Train Formation شویم. آن نسخه اتصال واقعی سه لایه‌ای را کامل می‌کند: Marketplace Demand ↓ Wagon Requirement ↓ Train Formation ↓ Loaded / Empty Cycle ↓ Locomotive Cycle ↓ Operational Regime ↓ Schedule ↓ Route Capacity ↓ Network Capacity و این دقیقاً جایی است که موتور شما از یک Railway Scheduling Prototype به یک Railway Capacity Generation & Optimization Engine تبدیل می‌شود. --- ## ادامه 15 — پرسش کاربر (`message-id:b99bca6a-b135-4f9b-96f9-6c111e19bd4b`) بله لطفا --- ## ادامه 15 — پاسخ دستیار (`message-id:2384dc81-3c58-4794-9e51-199a32df4fcd`) حتماً. در نسخه 0.5 اتصال تقاضای بازار به عملیات واقعی قطار را کامل می‌کنیم: Wagon Requirement → Train Formation → Loaded/Empty Cycle → Locomotive Cycle → Capacity. این بخش برای مدل ایرانی شما بسیار کلیدی است، چون ظرفیت واقعی فقط با «تعداد مسیر قطار» تعیین نمی‌شود؛ چرخه واگن، لکوموتیو، تشکیل قطار و برگشت خالی می‌توانند ظرفیت عملیاتی را محدود کنند. Technical Design Specification — Wagon Cycle، Locomotive Cycle و Train Formation Engine — نسخه 0.5 1. هدف نسخه 0.5 در نسخه‌های قبلی: Infrastructure → Directed Network → Train Path → Conflict → Operational Regime → Batch → Schedule → Capacity ساخته شد. در نسخه 0.5 لایه Rolling Stock را وارد زنجیره اصلی می‌کنیم: Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Loaded Train ↓ Destination ↓ Unload ↓ Empty Wagon Flow ↓ Origin ↓ Reload و هم‌زمان: Locomotive ↓ Train Assignment ↓ Destination ↓ Turnback ↓ Maintenance / Fuel ↓ Next Assignment بنابراین ظرفیت نهایی تابعی از سه حوزه می‌شود: [ C= f( Infrastructure, Schedule, RollingStock ) ] 2. اصل اصلی مدل باید بین این سه مفهوم تفاوت قطعی داشته باشیم: Wagon Requirement Wagon Availability Wagon Cycle تقاضا ممکن است بگوید: [ W_{required}=900 ] اما اگر موجودی واگن: [ W_{available}=700 ] باشد، ظرفیت بازار به‌تنهایی نمی‌تواند 900 واگن را جابه‌جا کند. از طرف دیگر حتی اگر: [ W_{available}=900 ] باشد، اگر چرخه واگن طولانی باشد، ممکن است ظرفیت روزانه کافی نباشد. 3. زنجیره ظرفیت Rolling Stock مدل: \min ( C_{infrastructure}, C_{schedule}, C_{wagon}, C_{locomotive}, C_{demand} ) ] این رابطه برای Estimation اولیه مفید است، ولی Capacity نهایی باید با Schedule واقعی Verify شود. یعنی: [ C_{effective} \neq صرفاً \min(...) ] بلکه: \max { F: FeasibleNetworkSchedule(F) } ] 4. Freight Flow @dataclass(frozen=True) class FreightFlow: id: str origin: str destination: str commodity_id: str demand_tons: int earliest_departure_s: int latest_arrival_s: int تقاضا در واحد تن است. اما Train Formation نیازمند تبدیل: tons ↓ wagon ↓ train است. 5. Wagon Type @dataclass(frozen=True) class WagonType: id: str name: str payload_tons: int tare_tons: int length_m: int compatible_commodities: tuple[str, ...] loading_time_s: int unloading_time_s: int مثلاً: WAGON-ORE payload = 60 t length = 14 m 6. Wagon Inventory @dataclass class WagonInventory: wagon_type_id: str total_count: int available_count: int loaded_count: int empty_count: int in_transit_count: int maintenance_count: int Invariant: Available + Loaded + Empty + InTransit + Maintenance ] 7. Wagon Pool برای ظرفیت‌سنجی بهتر است Wagon Pool را نیز داشته باشیم: @dataclass(frozen=True) class WagonPool: id: str wagon_type_id: str home_station_id: str capacity_count: int این مسئله بعداً برای Empty Repositioning مهم می‌شود. 8. Wagon Requirement تقاضا: [ D_{tons} ] ظرفیت هر واگن: [ Q_w ] پس: \left\lceil \frac{D_{tons}} {Q_w} \right\rceil ] مثلاً: [ D=5400t ] و: [ Q_w=60t ] بنابراین: [ W=90 ] 9. Wagon Requirement Object @dataclass(frozen=True) class WagonRequirement: freight_flow_id: str wagon_type_id: str required_wagons: int required_capacity_tons: int actual_capacity_tons: int 10. ظرفیت واقعی واگن اگر: [ D=5410t ] و: [ Q_w=60t ] آنگاه: [ W=91 ] و ظرفیت اسمی: [ 91\times60=5460t ] پس: Demand = 5410 Capacity = 5460 Unused = 50 این Unused Capacity باید قابل مشاهده باشد. 11. Train Formation Train Formation یعنی تبدیل Wagon Requirement به یک قطار عملیاتی. @dataclass(frozen=True) class TrainFormationItem: wagon_id: str | None wagon_type_id: str count: int loaded: bool و: @dataclass(frozen=True) class TrainFormation: id: str train_id: str origin: str destination: str wagon_items: tuple[TrainFormationItem, ...] locomotive_count: int total_length_m: int gross_weight_t: int payload_t: int 12. Formation Rule Train Formation باید محدودیت‌های زیر را رعایت کند: Wagon compatibility Train length Train gross weight Axle load Locomotive traction Station usable length Route restrictions Commodity compatibility Brake capability 13. Train Length اگر: [ L_{wagon,i} ] طول واگن i باشد: L_{locomotive} + \sum_i L_{wagon,i} ] و باید: [ L_{train} \le L_{route,max} ] باشد. همچنین در هر Station: [ L_{train} \le L_{usableStation} ] مگر اینکه Rule دیگری اجازه دهد. 14. Train Weight W_{locomotive} + \sum_i ( W_{tare,i} + W_{payload,i} ) ] و: [ W_{gross} \le W_{route,max} ] اما در Production باید Segment-specific باشد: [ W_{gross} \le W_{max}(Block) ] 15. Locomotive Type @dataclass(frozen=True) class LocomotiveType: id: str name: str tractive_power_kw: int max_train_weight_t: int length_m: int tare_tons: int compatible_routes: tuple[str, ...] 16. Locomotive Pool @dataclass class LocomotiveInventory: locomotive_type_id: str home_depot_id: str total_count: int available_count: int assigned_count: int maintenance_count: int 17. Train Formation Feasibility برای هر Formation: [✓] Wagon compatibility [✓] Wagon count [✓] Train length [✓] Train weight [✓] Locomotive traction [✓] Route compatibility [✓] Station compatibility [✓] Brake capability خروجی: @dataclass(frozen=True) class FormationValidation: feasible: bool violations: tuple[str, ...] warnings: tuple[str, ...] 18. Formation Builder class TrainFormationBuilder: def build( self, requirement: WagonRequirement, wagon_type: WagonType, locomotive: LocomotiveType, route: Route, ) -> TrainFormation: wagon_count = requirement.required_wagons total_length = ( wagon_count * wagon_type.length_m + locomotive.length_m ) payload = ( wagon_count * wagon_type.payload_tons ) gross_weight = ( locomotive.tare_tons + wagon_count * ( wagon_type.tare_tons + wagon_type.payload_tons ) ) return TrainFormation( ... ) در Production این Builder باید Formation Optimization نیز پشتیبانی کند. 19. Train Formation Optimization اگر چند Wagon Type داشته باشیم: W1 = 60t W2 = 65t W3 = 70t و محدودیت: Train Length ≤ 700m Weight ≤ 4000t ممکن است چند Formation مختلف وجود داشته باشد. پس: OptimizationProblem ] می‌تواند باشد. 20. Wagon Requirement به Train Requirement اگر: [ W_{required}=91 ] و ظرفیت هر Train: [ W_{train}=30 ] آنگاه: 4 ] پس: Train 1 = 30 Train 2 = 30 Train 3 = 30 Train 4 = 1 ولی در Production ممکن است Train چهارم از نظر عملیاتی مناسب نباشد. بنابراین Formation Engine باید بتواند: 90 + 1 را با: 60 + 31 یا: 23 + 23 + 23 + 22 مقایسه کند. 21. Minimum Economic Train Size در صورت وجود Rule: [ W_{train} \ge W_{min} ] مثلاً: [ W_{min}=20 ] در این صورت Train چهارم با یک واگن قابل تشکیل نیست. این باید Constraint باشد، نه Warning. 22. Market Demand به Train Formation زنجیره: Market Request ↓ Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Formation Candidate ↓ Train Formation ↓ TrainRun این دقیقاً Interface بین Marketplace و Capacity Engine است. 23. Train Formation vs TrainRun این دو را نباید یکی کنیم. TrainFormation می‌گوید: قطار از چه واگن‌ها و لکوموتیوهایی تشکیل شده است؟ TrainRun می‌گوید: این قطار چه زمانی و در چه مسیری حرکت می‌کند؟ پس: TrainFormation ↓ TrainRun است. 24. Train Formation vs Operational Batch همچنین: TrainFormation با: OperationalBatch متفاوت است. مثلاً: TrainFormation: TR01 = 30 wagons ولی: OperationalBatch: TR01 TR02 TR03 یعنی سه Train از نظر بهره‌برداری در یک Directional Batch قرار گرفته‌اند. 25. Loaded State هر TrainRun: class LoadState(str, Enum): LOADED = "LOADED" EMPTY = "EMPTY" PARTIAL = "PARTIAL" در نسخه Production حتی: PARTIAL باید مقدار بار دقیق داشته باشد. 26. Loaded Train Origin ↓ Loading ↓ Formation ↓ Loaded Train ↓ Route ↓ Destination 27. Unloading Event @dataclass(frozen=True) class UnloadingOperation: train_id: str station_id: str start_s: int end_s: int wagon_count: int commodity_id: str 28. Empty Wagon Formation بعد از تخلیه: Loaded Wagon ↓ Unloaded ↓ Empty Wagon بنابراین Formation دوم ساخته می‌شود: Empty Formation 29. Empty Wagon Flow اگر: Loaded: A → D باشد، Empty Flow معمولاً: D → A است. اما نباید این را همیشه به‌صورت Rule مطلق فرض کنیم. در Production ممکن است: D → B یا: D → C به‌عنوان Repositioning اقتصادی/عملیاتی لازم باشد. بنابراین: DecisionVariable ] در مدل پیشرفته. 30. Wagon Cycle برای هر گروه واگن: A │ │ Loaded ▼ D │ │ Unload ▼ Empty │ │ Reposition ▼ A │ │ Load ▼ Loaded Cycle Time: T_{load} + T_{loadedMove} + T_{unload} + T_{emptyMove} + T_{reposition} ] به اضافه: Waiting Yard Formation Inspection 31. Wagon Cycle Object @dataclass(frozen=True) class WagonCycle: wagon_group_id: str loaded_origin: str loaded_destination: str empty_origin: str empty_destination: str load_time_s: int loaded_transport_s: int unload_time_s: int empty_transport_s: int reposition_time_s: int waiting_s: int cycle_time_s: int 32. Wagon Cycle Capacity اگر: [ W_{fleet}=300 ] و: [ T_{cycle}=3days ] آنگاه ظرفیت روزانه تقریباً: 100 ] واگن در روز. ولی اگر Cycle Time به 2 روز کاهش یابد: [ W_{daily} \approx150 ] این نشان می‌دهد چرا کاهش Waiting و Dwell می‌تواند بدون ساخت زیرساخت جدید ظرفیت ایجاد کند. 33. Wagon Buffer در هر Station: [ 0\le E_j(t)\le Buffer_j ] اگر: [ E_j(t)=Buffer_j ] باشد: No additional empty arrival تا زمانی که مصرف ایجاد شود. 34. Buffer Constraint model.Add( empty_inventory[station, time] <= buffer_capacity[station] ) در مدل Time-indexed Production. در مدل Event-based بهتر است Inventory Balance داشته باشیم. 35. Inventory Balance Departures_j(t) ] و: [ 0\le E_j(t)\le C_j ] 36. Locomotive Cycle برای Locomotive: Depot ↓ Train Assignment ↓ Origin ↓ Train Movement ↓ Destination ↓ Turnback ↓ Return / Next Train ↓ Maintenance 37. Locomotive Assignment @dataclass(frozen=True) class LocomotiveAssignment: locomotive_id: str train_run_id: str start_s: int end_s: int origin_station: str destination_station: str 38. Locomotive Cycle Constraint اگر: Loco 01 در: TR01 08:00–13:00 استفاده شده، نمی‌تواند: TR02 11:00–15:00 را نیز انجام دهد. بنابراین: [ End(TR01) + TurnbackTime \le Start(TR02) ] 39. Locomotive Turnback @dataclass(frozen=True) class TurnbackOperation: locomotive_id: str station_id: str start_s: int end_s: int required_s: int 40. Maintenance Window مثلاً: Locomotive Depot D01 Maintenance: 23:00–03:00 اگر Locomotive در این Window نیاز به Maintenance داشته باشد: Assignment باید با آن سازگار باشد. 41. Fueling Fueling نیز یک Resource Operation است: @dataclass(frozen=True) class OperationalWindow: id: str resource_id: str activity_type: str start_s: int end_s: int capacity: int بنابراین: FUELING BRAKE_TEST INSPECTION MAINTENANCE CREW_CHANGE همگی می‌توانند از همین مدل استفاده کنند. 42. Locomotive Capacity ظرفیت لکوموتیو: \max { TrainAssignments: CycleFeasible } ] ممکن است: Infrastructure = 30 trains/day Wagon = 28 trains/day Locomotive = 22 trains/day باشد. در این حالت ظرفیت واقعی بدون حل کامل Network ممکن است نزدیک به 22 باشد، اما مقدار نهایی باید Schedule + Fleet را با هم Verify کند. 43. Double Locomotive ممکن است یک Train نیازمند: 2 Locomotives باشد. پس: 2 ] و: [ AvailableLoco \ge 2 ] باید برقرار باشد. 44. Traction Constraint برای هر Segment: [ TrainWeight \le LocoCapacity( Gradient, Speed, Direction ) ] در نسخه ساده: [ TrainWeight\le MaxTrainWeight ] در نسخه پیشرفته: f( Locomotive, Gradient, Speed, Direction, Weather ) ] 45. Formation + Route Train Formation نباید فقط در Origin بررسی شود. ممکن است: Station S01: 700m اما: Station S02: 600m باشد. اگر: [ L_{train}=650m ] قطار ممکن است از نظر حرکت روی Route قابل‌عبور باشد ولی در S02 برای Crossing مناسب نباشد. پس: [ TrainFormation \rightarrow RouteCompatibility ] باید بررسی شود. 46. Formation Constraint برای Crossing اگر Track: [ L_{usable}=600m ] و: [ L_{train}=650m ] باشد: Crossing at S02 = forbidden اما ممکن است: Passing through S02 = allowed باشد. این تفاوت بسیار مهم است. 47. Crossing Compatibility پس: @dataclass(frozen=True) class CrossingCompatibility: train_id: str station_id: str allowed: bool reason: str | None مثلاً: allowed = false reason = TRAIN_TOO_LONG_FOR_CROSSING_TRACK 48. Formation و Operational Batch Batch اکنون باید بتواند قطارهایی با Formation متفاوت داشته باشد. مثلاً: Batch OUT TR01 = 30 wagons TR03 = 28 wagons TR05 = 30 wagons ولی: TR05 ممکن است طول بیشتری داشته باشد. پس Batch Feasibility باید همه Trainها را بررسی کند. 49. Batch Resource Envelope برای یک Batch: \max_i L_i ] \max_i W_i ] و باید: Block Station Crossing با بدترین/بزرگ‌ت��ین Train سازگار باشند. 50. Empty Formation Empty Train ممکن است: 30 wagons باشد، اما Loaded Train: 30 wagons × 60t باشد. در نتیجه: [ Weight_{empty} < Weight_{loaded} ] و Running Time می‌تواند متفاوت باشد: [ T_{loaded} \neq T_{empty} ] این موضوع باید مستقیماً وارد Running Time Engine شود. 51. Train Type پس TrainType: @dataclass(frozen=True) class TrainType: id: str name: str load_state: LoadState max_speed_kmh: int acceleration_class: str braking_class: str default_headway_s: int اما LoadState به‌تنهایی TrainType نیست. 52. Running Time مدل ساده: \frac{L}{V_{eff}} ] اما در Production: f( Block, TrainType, LoadState, Weight, Length, Direction, SpeedProfile ) ] پس Loaded/Empty می‌توانند Running Time متفاوت داشته باشند. 53. Complete Rolling Stock Chain اکنون: Demand ↓ Wagon Requirement ↓ Wagon Assignment ↓ Train Formation ↓ Locomotive Assignment ↓ TrainRun ↓ TrainPath ↓ Operational Batch ↓ Schedule این ترتیب باید در Canonical Model تثبیت شود. 54. Domain Relationship FreightFlow │ ▼ WagonRequirement │ ▼ TrainFormation │ ┌───┴────┐ ▼ ▼ Wagon Locomotive │ │ └───┬────┘ ▼ Train │ ▼ TrainRun │ ▼ TrainPath │ ▼ OperationalBatch │ ▼ Schedule 55. ظرفیت چندبعدی اکنون Capacity را می‌توان به‌صورت: [ C= f( Route, OD, TrainType, Commodity, LoadState, Time, Direction, WagonType, LocomotiveType, Regime ) ] مدل کرد. این همان چیزی است که Marketplace برای عرضه ظرفیت نیاز دارد. 56. Capacity Offer خروجی Engine: @dataclass(frozen=True) class CapacityOffer: route_id: str origin: str destination: str valid_from_s: int valid_to_s: int direction: Direction commodity_id: str | None wagon_type_id: str | None train_capacity: int wagon_capacity: int ton_capacity: int regime: OperationalRegime confidence: float 57. Market Allocation Marketplace نباید Capacity خام را مستقیماً به مشتری بدهد. زنجیره: Capacity Generated ↓ Capacity Validated ↓ Capacity Published ↓ Market Allocation 58. Demand Allocation اگر: [ D=1000 ] و: [ Capacity=700 ] نتیجه: Allocated = 700 Unserved = 300 ولی باید دلیل نیز ثبت شود: UNSERVED_DUE_TO_CAPACITY 59. Capacity vs Fleet برای Explainability: Route Capacity: 30 Fleet Capacity: 24 Effective: 24 سپس: Why? Locomotive cycle یا: Wagon availability باید نمایش داده شود. 60. Capacity Proof با Rolling Stock مثلاً: F=24 ✓ Feasible F=25 ✕ Infeasible Binding Constraint: Locomotive Depot D01 Required: 13 locomotives Available: 12 این بسیار بهتر از: Capacity = 24 است. 61. Scenario — افزایش واگن Baseline: Wagon = 300 Capacity = 24 Scenario: Wagon = 360 Engine دوباره حل می‌کند. ممکن است نتیجه: Capacity = 24 بماند. در این صورت: Wagon was not binding. و این نتیجه بسیار ارزشمند است. 62. Scenario — افزایش لکوموتیو Baseline: Locomotive = 12 Capacity = 24 Scenario: Locomotive = 15 Capacity = 27 در این حالت: [ \Delta C=+3 ] و می‌توان گفت: Locomotive fleet has marginal capacity impact. 63. Scenario — کاهش Cycle Time اگر: [ T_{cycle} ] از: 72h به: 60h کاهش یابد، تعداد چرخه‌های ممکن افزایش می‌یابد. ولی دوباره باید کل Schedule حل شود. نباید ظرفیت را صرفاً با: [ Fleet/CycleTime ] اعلام کنیم. 64. Rolling Stock Bottleneck سه نوع مهم: WAGON_AVAILABILITY WAGON_CYCLE LOCOMOTIVE_AVAILABILITY LOCOMOTIVE_CYCLE به علاوه: FORMATION_CONSTRAINT 65. New Bottleneck Types ROLLING_STOCK WAGON_SHORTAGE WAGON_CYCLE LOCOMOTIVE_SHORTAGE LOCOMOTIVE_CYCLE FORMATION_LENGTH FORMATION_WEIGHT TRACTION_LIMIT 66. Python Package Structure ساختار از این نسخه: app/ ├── domain/ │ ├── network.py │ ├── train.py │ ├── wagon.py │ ├── locomotive.py │ ├── demand.py │ ├── formation.py │ ├── batch.py │ └── capacity.py │ ├── formation/ │ ├── builder.py │ ├── validator.py │ └── optimizer.py │ ├── rolling_stock/ │ ├── wagon_cycle.py │ ├── locomotive_cycle.py │ ├── inventory.py │ └── assignment.py │ ├── scheduling/ │ ├── problem.py │ ├── train_path.py │ └── resources.py │ ├── conflicts/ │ └── engine.py │ ├── solver/ │ └── cp_sat.py │ ├── capacity/ │ ├── search.py │ ├── profile.py │ └── proof.py │ └── validation/ └── checker.py 67. Test Structure tests/ ├── test_wagon_requirement.py ├── test_formation.py ├── test_formation_length.py ├── test_formation_weight.py ├── test_wagon_inventory.py ├── test_wagon_cycle.py ├── test_empty_flow.py ├── test_locomotive_assignment.py ├── test_locomotive_cycle.py ├── test_traction.py ├── test_crossing_length.py ├── test_loaded_empty_runtime.py ├── test_rolling_stock_capacity.py └── test_capacity_proof.py 68. Test Wagon Requirement def test_wagon_requirement(): requirement = calculate_wagon_requirement( demand_tons=5400, payload_per_wagon_tons=60, ) assert requirement.required_wagons == 90 69. Test Formation def test_train_formation(): formation = build_formation( wagon_count=30, wagon_type=ORE_WAGON, locomotive=LOCO_TYPE, ) assert formation.payload_t == 1800 assert formation.total_length_m > 0 70. Test Formation Length def test_formation_fails_station_length(): formation = build_formation( wagon_count=40, wagon_type=ORE_WAGON, locomotive=LOCO_TYPE, ) result = validate_formation( formation, max_length_m=600, ) assert not result.feasible 71. Test Wagon Inventory def test_wagon_inventory_balance(): inventory = WagonInventory( wagon_type_id="W01", total_count=100, available_count=40, loaded_count=20, empty_count=20, in_transit_count=10, maintenance_count=10, ) assert ( inventory.available_count + inventory.loaded_count + inventory.empty_count + inventory.in_transit_count + inventory.maintenance_count == inventory.total_count ) 72. Test Locomotive Cycle def test_locomotive_turnback(): first_end = 13 * 3600 turnback = 30 * 60 next_start = 14 * 3600 assert next_start >= first_end + turnback 73. Test Empty Flow def test_loaded_train_generates_empty_requirement(): loaded_wagons = 30 empty_required = calculate_empty_requirement( loaded_wagons ) assert empty_required == 30 در Production باید این تابع Allocation و Wagon Pool را نیز لحاظ کند. 74. Test Loaded vs Empty Running Time def test_loaded_and_empty_runtime_can_differ(): loaded = calculate_running_time( load_state=LoadState.LOADED, ... ) empty = calculate_running_time( load_state=LoadState.EMPTY, ... ) assert loaded != empty البته این Assertion فقط در صورتی معتبر است که داده/پارامترهای مدل واقعاً تفاوت داشته باشند؛ نباید اختلاف مصنوعی ایجاد کنیم. 75. Test Rolling Stock Capacity def test_capacity_is_limited_by_fleet_when_fleet_is_binding(): result = solve_capacity( problem_with_limited_locomotives ) assert result.feasible assert any( x.type == "LOCOMOTIVE_CYCLE" for x in result.binding_constraints ) 76. Data Quality برای Rolling Stock نیز: Wagon Type Payload Length Tare Weight Commodity Compatibility Locomotive Power Max Weight Brake Capability باید Data Quality داشته باشند. مثلاً: @dataclass(frozen=True) class DataQualityFlag: entity_id: str field_name: str status: str message: str 77. Source Mapping داده‌های واقعی: Excel Access Fleet DB Marketplace Infrastructure DB نباید مستقیماً وارد Solver شوند. معماری: Source ↓ Adapter ↓ Staging ↓ Validation ↓ Canonical Domain ↓ Solver Model 78. Fixture جدید Fixture نسخه 0.5 باید شامل: Route: A-S01-S02-S03-D Stations: A S01 S02 S03 D Wagon: W01 payload = 60t Locomotive: L01 max train weight = 2400t Fleet: 120 wagons 6 locomotives Demand: 5400t/day و: Loaded: A → D Empty: D → A باشد. 79. سناریوی اصلی Fixture فرض: Demand = 5400t Wagon Payload = 60t پس: [ W=90 ] اگر هر Train: [ 30W ] داشته باشد: [ N=3 ] پس: TR01 = 30 TR02 = 30 TR03 = 30 و بعد: Empty TR04 Empty TR05 Empty TR06 در صورت نیاز به برگشت همه واگن‌ها. 80. Cycle چرخه: TR01 Loaded A → D Unload TR04 Empty D → A و: TR02 Loaded A → D TR05 Empty D → A این Link باید در Domain Model ثبت شود. 81. Wagon Group برای جلوگیری از اتصال اجباری یک‌به‌یک: @dataclass(frozen=True) class WagonGroup: id: str wagon_type_id: str count: int source_station: str current_station: str state: LoadState Wagon Group می‌تواند مجموعه‌ای از واگن‌ها باشد. 82. Wagon Group State AVAILABLE LOADING LOADED IN_TRANSIT UNLOADING EMPTY REPOSITIONING MAINTENANCE این State Machine برای نسخه Production بسیار مهم است. 83. Wagon State Transition AVAILABLE ↓ LOADING ↓ LOADED ↓ IN_TRANSIT ↓ UNLOADING ↓ EMPTY ↓ REPOSITIONING ↓ AVAILABLE هر Transition باید زمان و Resource مصرف کند. 84. Locomotive State مشابه: AVAILABLE ASSIGNED RUNNING TURNBACK FUELING INSPECTION MAINTENANCE AVAILABLE 85. Resource State این Stateها بعداً به Simulation نیز قابل اتصال هستند. پس Domain Model فعلی باید طوری طراحی شود که: Optimization و: Simulation بتوانند از یک Canonical State Model استفاده کنند. 86. ظرفیت نهایی اکنون تعریف جامع‌تر: [ \boxed{ C_r= \max { F: \exists \begin{array}{l} Formation\ WagonCycle\ LocomotiveCycle\ TrainPath\ Batch\ Crossing\ TrackAssignment\ Schedule \end{array} } } ] مشروط به: [ AllHardConstraints=TRUE ] 87. Network Capacity برای چند Route: \max \sum_r F_r ] مشروط به: [ \sum_r ResourceUsage_{r,g} \le Capacity_g ] و همچنین: Wagon Locomotive Station Terminal Buffer باید Shared Resource باشند. 88. Marketplace Capacity در نهایت: Marketplace Demand ↓ Transportable Demand ↓ Formation ↓ Rolling Stock ↓ Schedule ↓ Route Capacity ↓ Network Capacity ↓ Allocation بنابراین Marketplace دیگر فقط مصرف‌کننده Capacity نیست؛ Demand را به Engine می‌دهد و Engine ظرفیت قابل تحقق را به Marketplace برمی‌گرداند. 89. APIهای نسخه 0.5 Wagon Requirement POST /formation/wagon-requirement Train Formation POST /formation/build Formation Validation POST /formation/validate Wagon Cycle POST /rolling-stock/wagon-cycle Locomotive Cycle POST /rolling-stock/locomotive-cycle Capacity with Fleet POST /capacity/rolling-stock 90. Result Package Result نهایی باید شامل: Run Scenario Data Version Model Version Demand Freight Flow Wagon Requirement Train Formation Wagon Assignment Locomotive Assignment Train Paths Operational Batches Crossings Track Assignments Schedule Validation Wagon Cycle Locomotive Cycle Capacity Capacity Proof Binding Constraints Bottlenecks Explanation 91. Definition of Done — Version 0.5 [✓] Freight Flow [✓] Wagon Type [✓] Wagon Requirement [✓] Wagon Inventory [✓] Wagon Pool [✓] Train Formation [✓] Formation Validation [✓] Train Length [✓] Train Weight [✓] Locomotive Type [✓] Locomotive Inventory [✓] Locomotive Assignment [✓] Locomotive Cycle [✓] Wagon Cycle [✓] Loaded Flow [✓] Empty Flow [✓] Wagon Buffer [✓] Operational Window [✓] Formation ↔ TrainRun [✓] TrainRun ↔ Batch [✓] Fleet ↔ Capacity [✓] Capacity Proof [✓] Rolling Stock Bottleneck [✓] Pytest Coverage 92. اصل نهایی Version 0.5 از این مرحله دیگر نباید بگوییم: «این خط مثلاً 25 قطار در روز ظرفیت دارد.» بلکه باید بتوانیم بگوییم: برای Demand مشخص، با Wagon Type مشخص، Formation مشخص، Fleet مشخص، Operational Regime مشخص، Batch مشخص، Crossing و Track Assignment مشخص، یک Schedule معتبر برای F قطار تولید شده است؛ و افزایش به F+1 به دلیل یک Constraint مشخص امکان‌پذیر نیست. یعنی: Infrastructure + Operations + RollingStock + Demand + FeasibleSchedule } ] و نه یک فرمول مستقل از عملیات. 93. وضعیت معماری پس از Version 0.5 اکنون هسته موتور به این شکل است: MARKETPLACE │ ▼ MARKET DEMAND │ ▼ FREIGHT FLOW │ ▼ WAGON REQUIREMENT │ ▼ TRAIN FORMATION │ │ ▼ ▼ WAGONS LOCOMOTIVES │ │ └──────┬───────┘ ▼ TRAIN RUN │ ▼ DIRECTED PATH │ ┌────────┼────────┐ ▼ ▼ ▼ BLOCK STATION RESOURCE │ │ │ └────────┼────────┘ ▼ CONFLICT ENGINE │ ▼ OPERATIONAL REGIME │ ┌──────┼──────┐ ▼ ▼ ▼ ALTERNATE BATCH MIXED │ │ │ └──────┼──────┘ ▼ CP-SAT │ ▼ SCHEDULE │ ▼ VALIDATION ENGINE │ ▼ CAPACITY SEARCH │ ▼ CAPACITY PROOF │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ BOTTLENECK EXPLANATION SCENARIO │ ▼ MARKETPLACE ALLOCATION 94. North Star معماری هدف نهایی موتور: [ \boxed{ Demand \rightarrow Formation \rightarrow Resources \rightarrow Schedule \rightarrow Feasibility \rightarrow Capacity \rightarrow Proof \rightarrow Allocation } ] است. و اصل حاکم همچنان: [ \boxed{ No\ Feasible\ Schedule \Rightarrow No\ Operational\ Capacity } ] و از این نسخه به بعد یک اصل دوم نیز اضافه می‌شود: [ \boxed{ No\ Feasible\ RollingStockCycle \Rightarrow No\ FleetSupported\ Capacity } ] با این نسخه، معماری ما از نظر Domain تقریباً به هسته‌ای که برای محصول نهایی لازم داریم رسیده است. گام بسیار مهم بعدی Version 0.6 باید Network Optimization باشد: چند OD/Route به‌طور هم‌زمان، منابع مشترک، Allocation تقاضا، Wagon/Loco Pool مشترک، Policy Constraints و نهایتاً حل هم‌زمان Capacity شبکه. آنجا Route Capacity دیگر به‌صورت جداگانه محاسبه نمی‌شود، بلکه موتور می‌تواند نشان دهد مثلاً افزایش ظرفیت یک مسیر چه اثری روی ظرفیت مسیرهای دیگر و کل شبکه دارد. --- ## ادامه 16 — پرسش کاربر (`message-id:62f98c9e-be77-44f4-8553-6531fb09646d`) بله دقیقا --- ## ادامه 16 — پاسخ دستیار (`message-id:93ee5fb6-0249-4c3f-8a61-526def4610a4`) دقیقاً. پس وارد Version 0.6 — Network Optimization Engine می‌شویم. در این نسخه، چند Route دیگر مستقل از هم حل نمی‌شوند؛ موتور باید هم‌زمان تقاضا، مسیرها، منابع مشترک، واگن، لکوموتیو، ایستگاه، Single Track، Batch و Schedule را ببیند و ظرفیت قابل تخصیص شبکه را پیدا کند. برای CP-SAT هم جهت معماری، استفاده از Interval/Optional Interval، NoOverlap و منابع مشترک کاملاً با قابلیت‌های رسمی مدل‌سازی Scheduling در OR-Tools هم‌راستاست. GitHub +1 Technical Design Specification — Network Optimization Engine — نسخه 0.6 1. هدف نسخه 0.6 در نسخه 0.5 موتور برای یک جریان حمل و یک/چند Train Formation قادر است: Demand → Wagon Requirement → Train Formation → Train Run → Train Path → Conflict → Schedule → Rolling Stock Cycle → Capacity را مدل کند. در Version 0.6 مسئله از: Route Capacity به: Network Capacity ارتقا پیدا می‌کند. یعنی: [ C_n= \max \sum_{r\in R}F_r ] مشروط به اینکه تمام Routeها به‌صورت هم‌زمان قابل زمان‌بندی باشند. 2. تفاوت Route Capacity و Network Capacity برای هر Route به‌صورت مستقل ممکن است داشته باشیم: [ C_{r1}=30 ] و: [ C_{r2}=25 ] بنابراین در نگاه ساده: [ C_n=55 ] اما اگر دو Route از یک Junction مشترک استفاده کنند: Route A \ J01 / Route B ممکن است: [ C_n<55 ] باشد. پس: [ \boxed{ C_n \neq \sum_r C_r } ] مگر اینکه Routeها Resource مشترک نداشته باشند. 3. تعریف رسمی Network Capacity تعریف: [ \boxed{ C_n= \max \left{ \sum_{r\in R}F_r : Schedule(F) \text{ is feasible} \right} } ] اما Schedule(F) اکنون شامل موارد زیر است: Infrastructure Train Paths Station Operations Conflicts Batches Wagons Locomotives Demand Policies Operational Windows Shared Resources 4. Network Flow شبکه از مجموعه Routeها تشکیل می‌شود: [ R={r_1,r_2,\dots,r_m} ] برای هر Route: [ F_r\ge0 ] تعداد Train Runهای تخصیص‌یافته به Route است. مثلاً: R01 Tehran → Khowaf R02 Tehran → Mashhad R03 Khowaf → Sangan R04 Sangan → Steel Plant 5. Demand Vector برای هر OD: [ D_{od} ] داریم. مثلاً: Tehran → Khowaf 40 trains Tehran → Mashhad 30 trains Khowaf → Sangan 25 trains ولی Demand الزاماً ظرفیت قابل تحقق نیست. بنابراین: [ D_{market} \neq D_{transportable} \neq D_{allocated} ] 6. Route Flow Variable متغیر اصلی: [ F_r ] است. در CP-SAT: flow_r = model.new_int_var( 0, demand_upper_bound, f"flow_{route.id}", ) CP-SAT ذاتاً با متغیرها و قیود صحیح کار می‌کند، بنابراین واحدهای زمان، وزن و ظرفیت باید در مدل به صورت integer representation وارد شوند. 7. Demand Constraint برای هر Route: [ F_r\le D_r ] اگر چند Route یک OD را پوشش دهند: [ \sum_{r\in R_{od}}F_r \le D_{od} ] 8. Transportable Demand ممکن است: [ D_{market}=100 ] اما: [ D_{transportable}=80 ] به دلیل: Wagon Locomotive Commodity Route Station Infrastructure باشد. پس: [ F\le D_{transportable} ] 9. Shared Resource هر Resource ممکن است توسط چند Route استفاده شود. مجموعه: [ G={g_1,g_2,\dots,g_k} ] مثلاً: Junction J01 Station S03 Block B12 Terminal T01 Loco Depot D01 Wagon Pool W01 10. Resource Capacity برای Resource (g): [ \sum_r Usage_{r,g} \le Capacity_g ] اما برای Railway Scheduling این رابطه معمولاً باید Time-Dependent باشد. یعنی: [ Usage_g(t) \le Capacity_g(t) ] 11. Infrastructure Shared Resource مثلاً: B12 بین: R01 R02 R04 مشترک است. پس Trainها باید در Time-Space Model هم‌زمان بررسی شوند. 12. Resource Reservation هر Train Movement یک Reservation ایجاد می‌کند: Train → Resource → Entry → Exit مثلاً: TR101 B12 08:15 08:32 13. Network Conflict Conflict زمانی رخ می‌دهد که دو Movement: same resource + overlapping time + incompatible operation داشته باشند. مثلاً: TR101 B12 08:15–08:32 TR205 B12 08:25–08:42 Conflict: BLOCK_OVERLAP 14. Conflict Graph برای Network Optimization یک Conflict Graph ایجاد می‌کنیم: [ G_c=(V_c,E_c) ] هر Node: Train Movement و هر Edge: Conflict است. مثلاً: TR01 ───── TR02 \ / \ / TR03 15. Conflict Edge @dataclass(frozen=True) class ConflictEdge: movement_a: str movement_b: str resource_id: str conflict_type: str minimum_separation_s: int 16. Resource Types نسخه 0.6 باید حداقل این Resourceها را بشناسد: BLOCK TRACK STATION_TRACK JUNCTION PLATFORM TERMINAL LOADING_FACILITY UNLOADING_FACILITY LOCOMOTIVE WAGON_POOL CREW DEPOT FUELING INSPECTION 17. Resource Capacity Profile هر Resource ممکن است Capacity ثابت نداشته باشد. مثلاً: Station S01 00:00–06:00 → 2 tracks 06:00–22:00 → 1 track 22:00–24:00 → 2 tracks پس: [ C_g(t) ] به‌صورت Profile ذخیره می‌شود. 18. Operational Window برای Resource: @dataclass(frozen=True) class ResourceWindow: resource_id: str start_s: int end_s: int capacity: int operation_type: str مثلاً: BRAKE_TEST 06:00–08:00 Capacity = 2 19. Network Route Object @dataclass(frozen=True) class NetworkRoute: id: str origin: str destination: str path_id: str demand_id: str train_type_id: str wagon_type_id: str 20. Route Resource Footprint هر Route باید مشخص کند چه منابعی را مصرف می‌کند: @dataclass(frozen=True) class RouteResourceUsage: route_id: str resource_id: str usage_type: str duration_s: int quantity: int ولی این صرفاً برای Screening است. ظرفیت نهایی باید از Schedule واقعی به دست آید. 21. Route Capacity Screening قبل از حل کامل: Demand ↓ Route Capacity Upper Bound ↓ Fleet Upper Bound ↓ Station Upper Bound ↓ Network Upper Bound ↓ Detailed Scheduling این کار فضای جست‌وجو را کاهش می‌دهد. 22. Upper Bound برای Route: [ UB_r= \min( D_r, UB_{Infrastructure}, UB_{Wagon}, UB_{Loco} ) ] این Capacity نهایی نیست. فقط Upper Bound است. 23. Network Upper Bound \min ( \sum_r D_r, UB_{Infrastructure}, UB_{Fleet}, UB_{Terminal} ) ] بعد Solver باید مقدار واقعی را پیدا کند. 24. Wagon Pool Shared فرض: Pool W01 = 500 wagons و: R01 needs 300 R02 needs 250 به صورت مستقل: R01 = feasible R02 = feasible اما هم‌زمان: [ 300+250>500 ] بنابراین: Network infeasible 25. Wagon Pool Constraint [ \sum_r W_r \le W_{available} ] در حالت ساده. در حالت Dynamic: Departures_j(t) \le Buffer_j ] 26. Locomotive Pool Shared مثلاً: Depot D01 12 locomotives و: R01 → 8 R02 → 6 در نگاه مستقل هر دو ممکن‌اند. اما: [ 8+6>12 ] پس باید Allocation هم‌زمان انجام شود. 27. Locomotive Assignment Constraint [ \sum_r L_r \le L_{available} ] ولی در مدل دقیق‌تر: [ \sum_{trains}Assignment(l,t) \le 1 ] برای هر Locomotive و هر Time Interval. 28. Train Generation Network Solver باید در صورت نیاز Trainهای Candidate ایجاد کند. Demand ↓ Required Wagons ↓ Candidate Formation ↓ Candidate TrainRun ↓ Candidate Path 29. Candidate Train @dataclass(frozen=True) class CandidateTrain: id: str route_id: str formation_id: str load_state: LoadState earliest_departure_s: int latest_arrival_s: int priority: int 30. Fixed vs Optional Train دو نوع Train: FIXED OPTIONAL Fixed: Baseline Schedule Optional: Generated Capacity Train 31. Optional Train در CP-SAT برای هر Candidate Train: [ y_i\in{0,1} ] که: y_i = 1 یعنی Train انتخاب شده است. در CP-SAT می‌توان از Optional Interval استفاده کرد؛ این Intervalها با یک Boolean حضور کنترل می‌شوند و در NoOverlap/Cumulative نیز قابل استفاده‌اند. 32. Train Selection \sum_{i\in T_r}y_i ] بنابراین: model.add( flow_r == sum(selected_train[i] for i in route_trains) ) 33. Objective اصلی برای Capacity: [ \max \sum_r F_r ] اما در محصول واقعی بهتر است Objective قابل انتخاب باشد. 34. Objective Hierarchy پیشنهاد: Level 1 بیشینه‌سازی تقاضای تخصیص‌یافته: [ \max AllocatedDemand ] Level 2 بیشینه‌سازی Ton-Km یا Revenue-weighted flow: [ \max \sum_r P_rF_r ] Level 3 کاهش Delay: [ \min Delay ] Level 4 کاهش Empty Movement: [ \min EmptyTrainKm ] Level 5 افزایش Robustness: [ \max Robustness ] 35. Lexicographic Optimization برای جلوگیری از مخلوط‌شدن اهداف: Stage 1: Maximize Allocated Demand Stage 2: Fix Stage 1 optimum Minimize Delay Stage 3: Fix previous optima Minimize Empty Movement این ساختار برای توضیح نتیجه نیز مناسب‌تر است. 36. Market Allocation Variable برای هر Demand: [ q_d ] مقدار Allocated Demand است. [ 0\le q_d\le D_d ] 37. Train-Demand Link اگر هر Train ظرفیت: [ Q_i ] داشته باشد: [ q_d \le \sum_i Q_i y_i ] در مدل دقیق‌تر، هر Train می‌تواند فقط یک Demand یا ترکیب مجاز Demandها را حمل کند. 38. OD Constraint برای OD: [ \sum_{i\in T_{od}}y_i \le DemandTrain_{od} ] 39. Commodity Constraint اگر Commodity: Iron Ore فقط Wagon Type خاصی را قبول کند: [ y_{train,commodity} \le Compatibility ] 40. Empty Flow Link اگر Train Loaded انتخاب شود: Loaded A → D و مدل سیاست آن را لازم بداند: Empty D → A باید Candidate Empty Train ایجاد شود. پس: ExistingEmptySupply ] این یکی از مهم‌ترین اتصال‌های Version 0.6 است. 41. Empty Repositioning Optimization ممکن است Empty Flow از: D → A یا: D → B باشد. Solver باید در مدل پیشرفته انتخاب کند: [ e_{d,j}\ge0 ] با هدف: [ \min EmptyCost ] مشروط به Balance واگن. 42. Wagon Balance برای Station (j): Departures_j^t ] و: [ 0\le E_j^t\le Buffer_j ] 43. Locomotive Balance مشابه: Assignments_j^t Maintenance_j^t ] 44. Station Capacity Station می‌تواند چند Resource داشته باشد: S03 ├── Main Track 1 ├── Main Track 2 ├── Loop 1 ├── Yard ├── Loading └── Unloading بنابراین Station را نباید یک عدد Capacity در نظر گرفت. 45. Station Resource Assignment برای Train: [ track_{i,s}\in Tracks_s ] و: [ TrackAssignment ] باید با طول قطار و نوع عملیات سازگار باشد. 46. Track Assignment مثلاً: Track 1 = 800m Track 2 = 500m Track 3 = 450m قطار: 650m پس: Track 1 ✓ Track 2 ✕ Track 3 ✕ 47. Crossing Decision در Single Track: TR01 → S03 TR02 ← S03 Solver باید تصمیم بگیرد: TR01 first یا: TR02 first و Station Crossing Resource را رزرو کند. 48. Crossing Variable [ z_{ij,s}\in{0,1} ] مثلاً: [ z_{ij,s}=1 ] یعنی: Train i crosses before Train j at Station s 49. Single Track Constraint برای دو Train مخالف: [ Exit_i + SwitchTime \le Entry_j ] یا: [ Exit_j + SwitchTime \le Entry_i ] یکی باید برقرار باشد. 50. Double Track در Double Track معمولاً دو Train مخالف می‌توانند هم‌زمان در Segment حرکت کنند. ولی: Junction Station Signal Crossing ممکن است همچنان Conflict داشته باشند. پس: [ DoubleTrack \neq NoConflict ] 51. Mixed Network Network واقعی می‌تواند: A ── Single ── S01 S01 ── Double ── S02 S02 ── Single ── S03 S03 ── Double ── D باشد. Solver باید Track Type را Resource-level مدل کند. 52. Operational Regime برای هر Route: ALTERNATING DIRECTIONAL_BATCH MIXED OPTIMIZED می‌تواند Candidate باشد. در نسخه 0.6 می‌توان Regime را به‌صورت: [ r_{regime}\in{0,1,2,3} ] مدل کرد. 53. Batch Selection برای Batch (k): [ b_k\in{0,1} ] و: \sum_{i\in Batch_k}y_i ] 54. Batch Switch اگر Batch A قبل از Batch B باشد: [ End_A+T_{switch} \le Start_B ] 55. Shared Junction Junction یک Resource خاص است. مثلاً: R01 ──\ J01 ── R03 R02 ──/ در J01 ممکن است: TR01 TR02 TR03 نتوانند هم‌زمان عبور کنند. 56. Junction Conflict Conflict Type: JUNCTION_CONFLICT باید مستقل از: BLOCK_CONFLICT STATION_CONFLICT ثبت شود. 57. Network Bottleneck پس Bottleneck می‌تواند: BLOCK STATION JUNCTION TERMINAL WAGON LOCOMOTIVE BUFFER SCHEDULE DEMAND POLICY باشد. 58. Binding Constraint مهم‌تر از Utilization: [ BindingConstraint ] است. مثلاً: Station S03 Track Assignment نه صرفاً: Station Utilization = 91% 59. Marginal Capacity برای Resource (g): C_{after} C_{before} ] مثلاً: Add Station Track C = 42 → 47 پس: [ \Delta C=+5 ] 60. Network Scenario Scenario: Baseline و: Add Loop at S03 Engine باید: Baseline Solve ↓ Scenario Modification ↓ Network Rebuild ↓ Network Solve ↓ Compare انجام دهد. 61. Scenario Diff خروجی: Baseline Capacity 42 Scenario Capacity 47 Delta +5 S03 Utilization 98% → 71% Conflicts 17 → 9 Waiting 420m → 280m 62. مهم: Capacity Transfer اگر Resource جدید ظرفیت یک Route را آزاد کند، ممکن است ظرفیت به Route دیگر منتقل شود. مثلاً: Before: R01 = 20 R02 = 15 Total = 35 Scenario: R01 = 27 R02 = 12 Total = 39 بنابراین صرفاً افزایش Route مورد نظر کافی نیست؛ باید کل Network دوباره حل شود. 63. Policy Constraints مثلاً: [ F_{R01}\ge20 ] یا: [ F_{R02}\ge10 ] این Constraint می‌تواند از Policy یا قرارداد خدماتی بیاید. 64. Priority Demandها ممکن است Priority داشته باشند: 1 = Mandatory 2 = Strategic 3 = Normal 4 = Opportunistic اما Priority نباید بدون تعریف Business Rule وارد Objective شود. 65. Mandatory Flow اگر: [ F_{R01}\ge20 ] Constraint سخت است. اگر امکان تأخیر/کاهش وجود دارد: SOFT با Penalty مدل می‌شود. 66. Penalty Model \alpha Delay \beta EmptyKm \gamma UnservedDemand ] ضرایب باید Configuration باشند، نه Hardcoded. 67. Robustness یک Schedule ممکن است Feasible باشد ولی بسیار شکننده. مثلاً: Slack = 1 minute پس Robustness باید سنجیده شود. 68. Buffer Time برای Movement: Latest_i Scheduled_i ] و: f(Slack,Conflicts,Buffer) ] 69. Robust Capacity دو تعریف خواهیم داشت: Nominal Capacity Robust Capacity مثلاً: Nominal = 45 Robust = 41 ظرفیت Robust فقط وقتی اعلام شود که Robustness Policy فعال باشد. 70. Network Solver Architecture Canonical Domain ↓ Network Problem Builder ↓ Candidate Generator ↓ Conflict Graph ↓ CP-SAT / MILP / Heuristic ↓ Schedule ↓ Independent Validator ↓ Capacity Engine ↓ Explanation 71. Solver Abstraction class INetworkOptimizer(Protocol): def solve( self, problem: NetworkOptimizationProblem, ) -> NetworkOptimizationResult: ... 72. Network Optimization Problem @dataclass(frozen=True) class NetworkOptimizationProblem: routes: tuple[NetworkRoute, ...] demands: tuple[FreightFlow, ...] candidate_trains: tuple[CandidateTrain, ...] resources: tuple[Resource, ...] wagon_pools: tuple[WagonPool, ...] locomotives: tuple[LocomotiveType, ...] fixed_movements: tuple[TrainRun, ...] horizon_start_s: int horizon_end_s: int 73. Network Result @dataclass(frozen=True) class NetworkOptimizationResult: status: str allocated_demand: int selected_trains: tuple[str, ...] schedule_id: str route_flows: dict[str, int] resource_usage: dict[str, int] binding_constraints: tuple[str, ...] bottlenecks: tuple[str, ...] objective_value: int 74. CP-SAT Model Layers مدل CP-SAT را به چند Layer تقسیم می‌کنیم: Layer 1 — Train Selection Layer 2 — Time Variables Layer 3 — Path Precedence Layer 4 — Resource Intervals Layer 5 — Conflict Constraints Layer 6 — Station Assignment Layer 7 — Fleet Constraints Layer 8 — Demand Allocation Layer 9 — Policy Layer 10 — Objective 75. Time Variables برای هر Train: [ A_{i,b} ] و: [ D_{i,b} ] 76. Block Interval برای هر Train و Block: interval = model.new_optional_interval_var( start, duration, end, selected, name, ) این ساختار دقیقاً برای مدل‌سازی فعالیت‌های زمان‌دار و Optional Activity مناسب است. 77. NoOverlap اگر Block Single Track باشد: model.add_no_overlap( block_intervals ) در صورت نیاز به Separation Time باید مدل Resource Interval را با زمان اشغال واقعی/Blocking Time تنظیم کند. 78. Double Track برای Double Track: Track 1 Track 2 دو Resource مجزا داریم. Train می‌تواند یکی از آن‌ها را انتخاب کند. 79. Alternative Resource برای Station Track Assignment: Track 1 Track 2 Track 3 می‌توان Optional Interval برای هر Alternative ساخت. فقط یکی فعال: [ \sum_k x_{i,s,k}=1 ] 80. Resource Capacity > 1 اگر Resource ظرفیت 2 داشته باشد، مانند دو Track موازی: [ \sum_i Active_i(t)\le2 ] که در CP-SAT می‌تواند با Cumulative مدل شود؛ مستندات رسمی Scheduling، NoOverlap و Cumulative و Alternative Resources را به‌عنوان الگوهای مدل‌سازی معرفی می‌کند. 81. Fixed Movements Baseline Trainها: selected = 1 و زمان آنها: fixed است. Generated Trainها: optional هستند. 82. Baseline Preservation Scenario نباید بدون اجازه Baseline را تغییر دهد. Rule: FIXED یعنی: Cannot move Cannot delete Cannot change formation مگر Scenario Policy اجازه دهد. 83. Capacity Search Network Capacity نیز Search دارد: F = candidate flow ↓ Generate candidate trains ↓ Solve ↓ Validate ↓ Feasible? اگر feasible: Increase F اگر infeasible: Decrease F 84. Binary Search اگر: [ LB=0 ] و: [ UB=100 ] باشد: 50 25 37 31 ... تا Maximum Feasible Flow پیدا شود. 85. اما یک نکته مهم Binary Search فقط وقتی ایمن است که Feasibility نسبت به Flow Monotonic باشد: [ Feasible(F+1) \Rightarrow Feasible(F) ] برای مدل ظرفیت کلاسیک این فرض معمولاً مطلوب است، ولی با بعضی Policyها، Fixed Movements، Batch Rules یا Objectiveهای پیچیده ممکن است به‌سادگی برقرار نباشد. پس Engine باید: MONOTONICITY_ASSUMED یا: MONOTONICITY_VERIFIED را ثبت کند. 86. Capacity Search Result @dataclass(frozen=True) class NetworkCapacityProof: maximum_feasible_flow: int feasible_schedule_id: str next_flow: int next_flow_status: str binding_constraints: tuple[str, ...] monotonicity_status: str 87. Capacity Proof مثلاً: F = 41 ✓ FEASIBLE F = 42 ✕ INFEASIBLE Capacity = 41 Binding: Junction J01 Secondary: Station S03 Fleet: No binding 88. Proof Chain Network Capacity ↓ F = 41 ↓ Schedule S-1021 ↓ Validated ↓ F = 42 ↓ Conflict ↓ Junction J01 ↓ No alternative slot ↓ Capacity = 41 89. Explainability Object @dataclass(frozen=True) class ExplanationTrace: run_id: str statement: str evidence_ids: tuple[str, ...] constraint_ids: tuple[str, ...] affected_routes: tuple[str, ...] marginal_impact: int | None 90. Network Bottleneck Center UI باید بتواند بگوید: Network Bottlenecks 1. J01 Binding ΔCapacity = +6 2. S03 Binding ΔCapacity = +3 3. Locomotive Depot D01 Near Binding ΔCapacity = +2 4. Wagon Pool W01 Slack 91. Bottleneck Classification BINDING NEAR_BINDING HIGH_UTILIZATION CAPACITY_RELEASE NON_BINDING اما UI نباید صرفاً بر اساس Utilization تصمیم بگیرد. 92. Network Optimization API Solve POST /network/optimize Capacity POST /network/capacity Scenario POST /network/scenarios/{id}/solve Explain GET /network/runs/{id}/explanation Bottlenecks GET /network/runs/{id}/bottlenecks 93. Input Example { "scenario_id": "BASELINE", "horizon": "2026-07-01/2026-07-31", "routes": [ { "id": "R01", "origin": "TEH", "destination": "KHO", "demand_trains": 40 }, { "id": "R02", "origin": "TEH", "destination": "MSH", "demand_trains": 30 } ], "fleet": { "locomotives": 12, "wagons": 500 } } 94. Output Example { "status": "FEASIBLE", "network_capacity": 57, "allocated_demand": 57, "route_flows": { "R01": 34, "R02": 23 }, "bottlenecks": [ "J01", "S03" ], "binding_constraints": [ "JUNCTION_J01_CAPACITY" ] } این صرفاً ساختار نمونه است و نباید به‌عنوان نتیجه واقعی شبکه تلقی شود. 95. Test — Shared Resource def test_shared_resource_limits_network_flow(): result = solve_network( routes=[ route_a, route_b, ], shared_resource_capacity=10, ) assert ( result.route_flows["R01"] + result.route_flows["R02"] <= 10 ) 96. Test — Shared Wagon Pool def test_shared_wagon_pool(): result = solve_network( wagon_pool_count=100, route_wagon_requirements={ "R01": 70, "R02": 60, }, ) assert ( result.route_wagon_usage["R01"] + result.route_wagon_usage["R02"] <= 100 ) 97. Test — Shared Locomotive Pool def test_shared_locomotive_pool(): result = solve_network( locomotive_count=5, route_locomotive_requirements={ "R01": 3, "R02": 3, }, ) assert ( result.route_locomotive_usage["R01"] + result.route_locomotive_usage["R02"] <= 5 ) 98. Test — Demand Conservation def test_allocated_demand_does_not_exceed_market_demand(): result = solve_network(...) for demand_id, allocated in result.allocations.items(): assert allocated <= demand[demand_id].demand_tons 99. Test — Fixed Train def test_fixed_baseline_train_is_preserved(): result = solve_network( fixed_movements=[baseline_train] ) assert baseline_train.id in result.selected_trains assert ( result.schedule[baseline_train.id] == baseline_train.fixed_schedule ) 100. Test — Network vs Independent Capacity یک Test بسیار مهم: def test_network_capacity_can_be_less_than_sum_of_route_capacities(): r1 = solve_route_capacity(route_1) r2 = solve_route_capacity(route_2) network = solve_network( routes=[route_1, route_2] ) assert ( network.capacity <= r1.capacity + r2.capacity ) 101. Test — Capacity Transfer def test_capacity_can_transfer_between_routes(): baseline = solve_network( scenario=baseline ) scenario = solve_network( scenario=add_station_loop ) assert ( scenario.network_capacity >= baseline.network_capacity ) این Assertion فقط در سناریوهایی معتبر است که تغییر زیرساختی واقعاً فضای feasible را به‌صورت monotone گسترش دهد؛ در مدل‌های دارای تغییر Policy یا Objective نباید چنین فرضی را کورکورانه اعمال کرد. 102. Test — Empty Wagon Balance def test_wagon_balance(): result = solve_network(...) for station in stations: assert ( result.final_empty_inventory[station] >= 0 ) و: [ E_j(t)\le Buffer_j ] نیز باید در تمام Time Points برقرار باشد. 103. Test — Capacity Proof def test_capacity_proof(): proof = find_network_capacity(problem) assert proof.maximum_feasible_flow > 0 assert proof.feasible_schedule_id assert proof.next_flow_status == "INFEASIBLE" 104. Test — Explanation def test_capacity_has_explanation(): result = solve_network(...) assert result.binding_constraints assert result.explanation 105. Test — No Feasible Schedule def test_no_schedule_means_no_operational_capacity(): result = solve_network( impossible_problem ) assert result.operational_capacity == 0 اصل: [ \boxed{ No\ Feasible\ Schedule \Rightarrow No\ Operational\ Capacity } ] 106. Architecture Version 0.6 ساختار Python: app/ ├── domain/ │ ├── network.py │ ├── demand.py │ ├── route.py │ ├── train.py │ ├── formation.py │ ├── wagon.py │ ├── locomotive.py │ ├── resource.py │ └── scenario.py │ ├── network/ │ ├── problem.py │ ├── candidate_generator.py │ ├── resource_usage.py │ ├── flow.py │ └── allocation.py │ ├── conflicts/ │ ├── graph.py │ ├── detector.py │ └── resolver.py │ ├── scheduling/ │ ├── train_path.py │ ├── station.py │ ├── batch.py │ └── timetable.py │ ├── rolling_stock/ │ ├── wagon_cycle.py │ ├── locomotive_cycle.py │ └── assignment.py │ ├── solver/ │ ├── base.py │ ├── cp_sat.py │ ├── milp.py │ └── heuristic.py │ ├── capacity/ │ ├── route.py │ ├── network.py │ ├── search.py │ └── proof.py │ ├── scenario/ │ ├── engine.py │ └── sensitivity.py │ ├── validation/ │ ├── schedule.py │ ├── network.py │ └── rolling_stock.py │ └── explanation/ ├── bottleneck.py └── trace.py 107. Separation of Engines از این نسخه چهار Engine مستقل داریم: 1. Scheduling Engine 2. Network Optimization Engine 3. Capacity Engine 4. Explanation Engine و این چهار مورد نباید در یک کلاس بزرگ ادغام شوند. 108. Solver Abstraction نسخه Production: class SolverBackend(Protocol): def solve( self, model: OptimizationModel, options: SolverOptions, ) -> SolverResult: ... Backendها: CP-SAT MILP Heuristic Metaheuristic Simulation-assisted 109. چرا CP-SAT؟ برای MVP و بسیاری از Scheduling/Resource Allocationهای گسسته، CP-SAT انتخاب مناسبی برای Prototype/Production اولیه است، چون متغیرهای صحیح، قیود منطقی و مدل‌سازی Scheduling را پوشش می‌دهد. مستندات رسمی نیز نمونه‌های Scheduling، Interval، Optional Interval، NoOverlap، Cumulative و Alternative Resources را ارائه می‌کنند. اما: [ CP-SAT \neq کل موتور ] بلکه: SolverBackend ] است. 110. چرا Solver نباید Domain باشد؟ اشتباه: Train → CpModel در Domain. مدل درست: Canonical Domain ↓ Optimization Model ↓ CP-SAT Model یعنی Domain از Solver مستقل می‌ماند. 111. Result مستقل از Solver نتیجه باید: NetworkOptimizationResult باشد، نه: CpSolverResult این امکان می‌دهد Backend عوض شود. 112. Run Metadata هر Run: @dataclass(frozen=True) class OptimizationRun: run_id: str scenario_id: str data_version: str model_version: str solver_name: str solver_version: str started_at: str completed_at: str status: str 113. Reproducibility برای هر Run ثبت شود: Data Version Model Version Solver Version Parameters Seed Objective Scenario Input Hash 114. Performance Strategy در Network بزرگ نباید از ابتدا تمام Candidate Trainها را وارد CP-SAT کنیم. Pipeline: Screening ↓ Candidate Generation ↓ Dominance Filtering ↓ Conflict Graph Reduction ↓ CP-SAT ↓ Local Improvement 115. Dominance Rule اگر Train Candidate A: same OD same formation same route same availability و Candidate B دقیقاً بدتر باشد: later longer more resource consumption A بر B غالب است. B می‌تواند حذف شود. 116. Rolling Horizon برای شبکه بزرگ: Day 1 ↓ Day 2 ↓ Day 3 یا: 00–06 06–12 12–18 18–24 حل Rolling Horizon می‌تواند به‌کار رود. ولی باید اثر Boundary Conditions مدیریت شود. 117. Warm Start Scenario جدید می‌تواند از Schedule قبلی شروع کند. Baseline Schedule ↓ Scenario ↓ Warm Start ↓ Re-optimization 118. Scenario Delta Model به‌جای ساخت کامل مدل از صفر: Baseline Model + Delta مثلاً: ADD_TRACK(S03, 600m) ADD_LOCOMOTIVE(L13) INCREASE_BUFFER(S02, +20) CHANGE_SPEED(B12, +10%) 119. Network Optimization UI صفحه جدید: Network Optimization با: Demand Routes Fleet Shared Resources Objective Constraints 120. UI — Network Flow Matrix جدول: Route Demand Allocated Capacity Fleet Bottleneck R01 40 34 34 OK J01 R02 30 23 23 Loco D01 R03 25 18 18 OK S03 اعداد جدول بالا صرفاً نمونه UI هستند. 121. UI — Network Capacity NETWORK CAPACITY Market Demand 95 Transportable 82 Scheduled 75 Allocated 75 Unused 7 Unserved 20 122. UI — Resource Heatmap Resource Utilization Status J01 100% BINDING S03 97% BINDING B12 92% HIGH D01 88% HIGH W01 71% NORMAL 123. UI — Capacity Transfer Visualization: Baseline Scenario Delta R01 20 27 +7 R02 15 14 -1 R03 10 12 +2 NETWORK TOTAL 45 53 +8 124. UI — What If کاربر: Add Loop at S03 سیستم: Running Scenario... Network Rebuild Candidate Regeneration Schedule Optimization Validation Capacity Proof سپس: Capacity +8 trains/day به همراه دلیل و Evidence. 125. Explainability به‌جای: Capacity = 53 نمایش: 53 trains/day Why not 54? 1. J01 conflict 2. No alternative crossing slot 3. S03 track unavailable 4. Loco D01 has no spare cycle 126. Network Optimization Output هر Run باید حداقل این بخش‌ها را برگرداند: Network Capacity Route Flows Demand Allocation Train Schedule Resource Usage Wagon Usage Locomotive Usage Conflicts Binding Constraints Bottlenecks Unused Capacity Unserved Demand Capacity Proof Explanation Scenario Comparison 127. Version 0.6 Definition of Done [✓] Multi-Route Model [✓] Shared Resources [✓] Shared Wagon Pools [✓] Shared Locomotive Pools [✓] Demand Allocation [✓] Candidate Train Selection [✓] Network Conflict Graph [✓] Junction Resource [✓] Station Resource [✓] Track Assignment [✓] Single Track Crossing [✓] Double Track Resource [✓] Batch Selection [✓] Policy Constraints [✓] Fixed Baseline Movements [✓] CP-SAT Network Model [✓] Network Capacity Search [✓] Capacity Proof [✓] Bottleneck Detection [✓] Marginal Capacity [✓] Scenario Re-solve [✓] Explanation Trace [✓] Pytest Network Tests 128. معماری نهایی تا این مرحله MARKETPLACE │ ▼ MARKET DEMAND │ ▼ FREIGHT FLOW │ ▼ WAGON REQUIREMENT │ ▼ TRAIN FORMATION │ │ ▼ ▼ WAGON LOCO │ │ └────┬─────┘ ▼ TRAIN RUNS │ ▼ TRAIN PATHS │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ BLOCK STATION JUNCTION │ │ │ └─────────────┼─────────────┘ ▼ CONFLICT GRAPH │ ▼ NETWORK SCHEDULER │ ┌─────────┼─────────┐ ▼ ▼ ▼ BATCH CROSSING TRACK │ │ │ └─────────┼─────────┘ ▼ CP-SAT / Solver │ ▼ SCHEDULE │ ┌───────────┼───────────┐ ▼ ▼ ▼ WAGON LOCO STATION CYCLE CYCLE CHECK │ │ │ └───────────┼───────────┘ ▼ VALIDATION │ ▼ NETWORK CAPACITY │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ROUTE FLOWS BOTTLENECKS ALLOCATION │ │ │ └────────────────┼────────────────┘ ▼ CAPACITY PROOF │ ▼ EXPLANATION │ ▼ MARKETPLACE 129. اصل معماری Version 0.6 از اینجا به بعد، موتور دیگر یک: Capacity Calculator نیست. بلکه: [ \boxed{ Network\ Capacity\ Optimization\ Engine } ] است. و مسئله اصلی آن: [ \boxed{ \max \sum_d AllocatedDemand_d } ] با مجموعه‌ای از قیود: [ \begin{aligned} Infrastructure & \ Schedule & \ Conflict & \ Station & \ Junction & \ Wagon & \ Locomotive & \ Formation & \ Demand & \ Policy & \ OperationalWindow & \ Buffer & \ Baseline \end{aligned} ] است. 130. اصل نهایی سه سطح اکنون کاملاً از هم جدا هستند: Generation Generate Train / Formation / Candidate Schedule Estimation Estimate Upper Bound / Feasible Capacity Optimization Find Best Feasible Network Allocation + Schedule بنابراین: [ \boxed{ Generation \neq Estimation \neq Optimization } ] اما هر سه روی یک Canonical Railway Domain Model کار می‌کنند. و در نهایت: [ \boxed{ No\ Feasible\ Network\ Schedule \Rightarrow No\ Network\ Operational\ Capacity } ] و: [ \boxed{ Every\ Published\ Capacity \rightarrow Validated\ Schedule \rightarrow Capacity\ Proof \rightarrow Explanation } ] این زنجیره باید معیار اصلی پذیرش موتور در Production باشد. این نسخه عملاً یک جهش مهم است: از اینجا به بعد می‌توانیم موتور را روی یک شبکه چندمسیره واقعی آزمایش کنیم، نه فقط روی یک Route. قدم بعدی منطقی، Version 0.7: اتصال مستقیم Excel/Access واقعی شما به Canonical Model و ساخت اولین Vertical Slice واقعی است؛ یعنی داده‌های واقعی TrainNo / StationName / Sequence / time_in / time_take / time_out / RequiredWait / ... وارد Adapter شوند، مسیرهای جهت‌دار ساخته شوند، Baseline Schedule استخراج شود و سپس همان داده‌ها مستقیماً وارد Conflict Engine و CP-SAT شوند. --- ## ادامه 17 — پرسش کاربر (`message-id:66787ebd-2872-4c64-9348-918863c46cae`) بله دقیقا --- ## ادامه 17 — پاسخ دستیار (`message-id:9f63f723-838f-4a1d-b165-5b05d8f2ec06`) عالی. پس وارد Version 0.7 — Real Data Adapter & First Vertical Slice می‌شویم. این نسخه به نظرم یکی از مهم‌ترین مراحل پروژه است، چون از اینجا به بعد دیگر فقط «مدل درست» نداریم؛ باید نشان دهیم داده واقعی شما واقعاً می‌تواند وارد موتور شود و تا Capacity Proof پیش برود. برای Excel، pandas.read_excel و ExcelFile مسیر مناسبی برای Adapter هستند و امکان خواندن Sheetهای مختلف و کنترل نوع داده را می‌دهند. Pandas +1 برای Access نیز لایه دسترسی باید از Canonical Model جدا بماند؛ Microsoft دسترسی به Access را از طریق ODBC/Providerها مستند کرده است. Microsoft Learn +1 Technical Design Specification — Real Data Adapter و First Vertical Slice — نسخه 0.7 1. هدف Version 0.7 تا Version 0.6 معماری موتور به این نقطه رسیده است: Marketplace ↓ Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Train Run ↓ Train Path ↓ Conflict Engine ↓ Scheduling ↓ Network Optimization ↓ Capacity Proof اما بخش مهمی هنوز Synthetic است. Version 0.7 این فاصله را حذف می‌کند: Real Excel Real Access ↓ Source Adapter ↓ Staging ↓ Mapping ↓ Canonical Domain Model ↓ Baseline Schedule ↓ Train Path ↓ Conflict Detection ↓ Feasibility Validation ↓ Capacity Engine هدف اصلی: [ \boxed{ Real\ Data \rightarrow Canonical\ Model \rightarrow Validated\ Schedule } ] 2. اصل بسیار مهم فایل Excel یا Access نباید مستقیماً وارد Solver شود. غلط: Excel ↓ CP-SAT و: Access ↓ CP-SAT معماری صحیح: Excel / Access ↓ Source Adapter ↓ Raw/Staging ↓ Mapping ↓ Canonical Domain Model ↓ Validation ↓ Scheduling Model ↓ Solver 3. چرا این جداسازی حیاتی است؟ چون Source Data ممکن است: نام‌گذاری محلی داشته باشد؛ چند مفهوم را در یک ستون نگه دارد؛ واحدهای متفاوت داشته باشد؛ داده ناقص داشته باشد؛ تاریخ و ساعت را به شکل متفاوت ذخیره کند؛ معنای بعضی فیلدها هنوز قطعی نباشد. بنابراین: [ Source\ Schema \neq Domain\ Schema ] 4. Sourceهای واقعی پروژه Excel فایل: REPORTKholase_31-06-1405_02-19-35.xlsx با ساختاری مشابه: ردیف نام قطار شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت از مبدا ساعت ورود به مقصد شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت از مقصد ساعت رسیدن به مبدا 5. Access فایل: aaa.accdb با فیلدهای: TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir faultV 6. وضعیت معنایی Access در این مرحله نباید برای این فیلدها معنای قطعی اختراع کنیم: time_take RequiredWait Kilometerage Distance sumDistancezz seir faultV وضعیت آنها: SEMANTIC_STATUS = PENDING_VALIDATION است. تا زمانی که Mapping آنها توسط صاحب داده تأیید نشده باشد: [ \boxed{ No\ Verified\ Mapping \Rightarrow No\ Production\ Use } ] 7. Data Adapter Interface هر Source Adapter باید یک Interface مشترک داشته باشد: from typing import Protocol class SourceAdapter(Protocol): def inspect(self) -> "SourceInspection": ... def extract(self) -> "RawDataset": ... 8. Source Inspection قبل از Import باید Schema Discovery انجام شود. from dataclasses import dataclass @dataclass(frozen=True) class SourceColumn: name: str data_type: str | None nullable: bool sample_values: tuple[str, ...] @dataclass(frozen=True) class SourceInspection: source_id: str source_type: str columns: tuple[SourceColumn, ...] row_count: int warnings: tuple[str, ...] 9. Excel Adapter from pathlib import Path import pandas as pd class ExcelSourceAdapter: def __init__( self, path: Path, sheet_name: str | int = 0, ): self.path = path self.sheet_name = sheet_name def extract(self) -> pd.DataFrame: return pd.read_excel( self.path, sheet_name=self.sheet_name, dtype=object, ) استفاده از dtype=object در مرحله Raw مهم است، چون نمی‌خواهیم Adapter اولیه با تبدیل خودکار، معنای داده را تغییر دهد. read_excel امکان کنترل dtype و converters را نیز فراهم می‌کند. 10. Excel Sheet Discovery اگر Sheet مشخص نباشد: import pandas as pd def discover_excel(path): with pd.ExcelFile(path) as workbook: return workbook.sheet_names این امکان مستقیماً در ExcelFile.sheet_names وجود دارد. 11. Access Adapter برای Access، Adapter باید جدا از Domain باشد. ساختار: Access ↓ ODBC / Provider ↓ AccessAdapter ↓ RawDataset Microsoft مستند کرده که Access می‌تواند از طریق ODBC و Providerهای مربوط به Access مورد دسترسی قرار گیرد. 12. Interface class AccessSourceAdapter: def __init__( self, connection_string: str, ): self.connection_string = connection_string def list_tables(self) -> list[str]: ... def extract_table( self, table_name: str, ): ... 13. نکته Deployment برای Access نباید در requirements.txt فرض کنیم که Driver سیستم‌عامل خودکار نصب است. این دو موضوع جدا هستند: Python Dependency و: Operating System Driver بنابراین Deployment Specification باید Driver مورد نیاز را نیز مستند کند. 14. Staging Layer پس از Extract: Raw Source ↓ Staging ساختار: staging/ ├── excel/ │ └── report_kholase/ ├── access/ │ └── aaa/ └── manifests/ 15. Raw Record هر Record باید Lineage داشته باشد: @dataclass(frozen=True) class RawRecord: source_id: str source_type: str source_table: str | None source_row: int | None values: dict[str, object] 16. Data Lineage مثلاً: Canonical TrainStationCall ↓ Source: aaa.accdb ↓ Table: TrainMovement ↓ Row: 1821 این برای Audit بسیار مهم است. 17. Mapping Layer Mapping باید Configuration باشد، نه Code Hardcoded. مثلاً: train_no: source: TrainNo target: train_run.train_no station_name: source: StationName target: station.name sequence: source: Sequence target: station_call.sequence time_in: source: time_in target: station_call.time_in 18. Mapping Status هر Mapping: VERIFIED PROVISIONAL UNMAPPED REJECTED دارد. 19. Mapping Registry @dataclass(frozen=True) class FieldMapping: source_field: str target_entity: str target_field: str transformation: str | None status: str verified_by: str | None 20. Data Quality Rule هیچ فیلدی که: UNMAPPED باشد نباید وارد بخش محاسباتی شود. 21. Transformation Layer مثلاً: "08:35" به: 31500 seconds تبدیل می‌شود. اما Transformation باید ثبت شود: Transformation( source="08:35", target=31500, rule="HH:MM_TO_SECONDS", ) 22. Time Representation در Core Engine پیشنهاد می‌شود زمان محاسباتی: [ t\in\mathbb{Z} ] و واحد: seconds باشد. UI می‌تواند: 08:35 نمایش دهد. 23. Persian Calendar اگر Source از تاریخ شمسی استفاده کند: 1405/06/31 Canonical Model باید Date استاندارد داخلی داشته باشد و Calendar Conversion در Adapter انجام شود. اصل: Source Calendar ↓ Adapter ↓ Canonical Date 24. ساعت‌های عبور از نیمه‌شب مثلاً: Departure = 23:50 Arrival = 01:20 نباید: [ 01:20-23:50<0 ] تفسیر شود. Adapter/Schedule Builder باید Day Offset ایجاد کند: 23:50 Day 0 01:20 Day 1 25. Operating Pattern Excel دارای: روزهای حرکت از مبدا روزهای حرکت از مقصد است. پس این داده نباید صرفاً به Train تبدیل شود. بلکه: Train + OperatingPattern + OperatingCalendar ساخته شود. 26. Operating Pattern @dataclass(frozen=True) class OperatingPattern: id: str days_of_week: tuple[int, ...] departure_time_s: int active_from: str | None active_to: str | None 27. Train و TrainRun این Excel احتمالاً اطلاعات Service Pattern را می‌دهد. بنابراین: Train از: TrainRun جدا می‌شود. مثلاً: Train: TEH-KHO-001 TrainRun: 2026-09-01 06:30 28. TrainFormation اگر Excel فقط شماره قطار و زمان را داشته باشد: Formation را نباید از خودمان اختراع کنیم. وضعیت: UNKNOWN تا زمانی که منبع Wagon/Formation موجود باشد. 29. TrainStationCall Access داده بسیار مهم‌تری می‌دهد: TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait ... پس هر رکورد باید به: TrainStationCall تبدیل شود. 30. Canonical TrainStationCall @dataclass(frozen=True) class TrainStationCall: train_run_id: str station_id: str sequence: int time_in_s: int | None time_take_s: int | None time_out_s: int | None required_wait_s: int | None kilometerage: float | None distance_m: float | None max_speed_kmh: float | None 31. Unknown Semantics در این کلاس: time_take نباید بدون تأیید تبدیل به: Dwell شود. ممکن است معنی دیگری داشته باشد. پس: raw_time_take می‌تواند موقتاً حفظ شود. 32. Raw + Canonical برای فیلدهای مشکوک: @dataclass class SourceMappedValue: raw_value: object canonical_value: object | None mapping_status: str mapping_rule: str | None 33. Station Identity StationName به تنهایی برای Identity کافی نیست. پیشنهاد: StationCode StationNumber NormalizedName و در صورت وجود: InfrastructureNodeId 34. Station Matching مثلاً: "تهران" "تهران-راه‌آهن" "Tehran" "TEH" نباید چهار Station مستقل ساخته شود. 35. Station Master @dataclass(frozen=True) class StationMaster: station_id: str station_code: str | None official_name: str normalized_name: str station_number: int | None 36. Route Reconstruction Access Sequence به ما امکان می‌دهد: Train → Station 1 → Station 2 → Station 3 → ... را بازسازی کنیم. 37. Direction برای هر TrainRun: origin_station destination_station direction تعیین می‌شود. مثلاً: TEH → KHO و Return: KHO → TEH دو Directed Path هستند. 38. بسیار مهم: Route را Reverse نکنیم اشتباه Version 0.2: A → B → C → D برای Train برگشتی نیز همان ترتیب استفاده شود. در Version 0.7 اصلاح: Outbound: A → B → C → D Inbound: D → C → B → A 39. Directed Route @dataclass(frozen=True) class DirectedRoutePath: route_id: str direction: str ordered_nodes: tuple[str, ...] ordered_segments: tuple[str, ...] ordered_blocks: tuple[str, ...] 40. Train Path Construction برای هر TrainRun: TrainRun ↓ Origin ↓ Destination ↓ DirectedRoutePath ↓ Ordered Blocks ↓ Movement Intervals 41. Block Construction اگر Infrastructure Data موجود باشد: Block Block Length Track Type Speed Signal Direction ساخته می‌شود. اگر موجود نباشد: BLOCK_DATA_MISSING و موتور نباید آن را با حدس پر کند. 42. Access Distance Data فیلدهای: Kilometerage Distance sumDistancezz فعلاً فقط به Staging/Canonical Mapping وارد می‌شوند. معنای دقیق هرکدام باید Validation شود. 43. Speed MaxSpeed می‌تواند Candidate Parameter باشد، اما: [ V_{max} \neq V_{effective} ] است. 44. Effective Running Time در مدل دقیق: \sum_i \frac{L_i}{V_i} ] اما V_i باید از Speed Profile، Train Type، Load State و سایر محدودیت‌ها حاصل شود. پس: MaxSpeed به تنهایی برای محاسبه Capacity کافی نیست. 45. Baseline Schedule Excel می‌تواند Baseline Service Pattern بدهد. Access می‌تواند Detailed Movement بدهد. ترکیب: Excel → Service Pattern Access → Detailed Train Movement 46. Baseline Merge Excel \ → Identity Resolution / Access ↓ Train Service ↓ Train Run ↓ Station Calls ↓ Baseline Schedule 47. Identity Resolution کلید احتمالی: TrainNo + TrainName + Origin + OperatingDate اما این Key باید با داده واقعی Validation شود. 48. Data Reconciliation مثلاً Excel می‌گوید: Train 1201 Departure 06:30 و Access: Train 1201 First Station time_out = 06:32 نباید یکی را حذف کنیم. باید: RECONCILIATION ISSUE ثبت شود. 49. Reconciliation Result @dataclass(frozen=True) class ReconciliationIssue: entity_type: str entity_key: str source_a: str source_b: str field: str value_a: object value_b: object severity: str 50. Severity INFO WARNING ERROR BLOCKING 51. Data Quality Gate قبل از Solver: Source Validation ↓ Mapping Validation ↓ Identity Validation ↓ Temporal Validation ↓ Network Validation ↓ Data Quality Gate 52. Data Quality Gate سه حالت: PASS PASS_WITH_WARNINGS BLOCK اگر: BLOCK باشد: NOT\ STARTED ] 53. Data Quality Report DATA QUALITY Records 12,481 Mapped 12,102 Warnings 281 Errors 98 Blocking Errors 12 Status: BLOCKED اعداد فوق نمونه UI هستند. 54. First Vertical Slice برای اینکه پروژه واقعاً جلو برود، کل Network را از ابتدا وارد نمی‌کنیم. یک Vertical Slice انتخاب می‌کنیم: One OD + One Route + One Direction Pair + Baseline Trains + Detailed Station Calls + Single/Double Track + Conflict Engine + Schedule Validation + Capacity Proof 55. پیشنهاد Pilot بر اساس داده‌های موجود: Sangan → Foolad به‌عنوان Validation Scenario نگه داشته می‌شود. اما ابتدا باید Mapping واقعی داده آن تأیید شود. 56. Vertical Slice Pipeline aaa.accdb ↓ Access Adapter ↓ Train Movement Records ↓ Canonical TrainStationCall ↓ Directed Route ↓ Train Path ↓ Baseline Schedule ↓ Conflict Detection ↓ Validation ↓ Capacity Search 57. Excel در همین Slice Excel: Train Service Pattern را فراهم می‌کند. Access: Detailed Train Movement را. در نتیجه: [ Excel+Access \rightarrow Baseline\ Operational\ Model ] 58. Baseline Schedule Object @dataclass(frozen=True) class BaselineSchedule: id: str train_runs: tuple[str, ...] station_calls: tuple[TrainStationCall, ...] source_versions: tuple[str, ...] validation_status: str 59. Baseline vs Generated Baseline: Observed / Existing Generated: Engine Proposed این دو نباید در Database یکی شوند. 60. Schedule Source Type OBSERVED PLANNED GENERATED OPTIMIZED SIMULATED 61. First Real Conflict Test بعد از Import: Train A Train B و برای هر Block: Entry Exit Blocking Time محاسبه می‌شود. 62. Conflict Result @dataclass(frozen=True) class DetectedConflict: conflict_id: str train_a: str train_b: str resource_id: str start_a_s: int end_a_s: int start_b_s: int end_b_s: int conflict_type: str 63. Conflict Types حداقل: BLOCK_OVERLAP SINGLE_TRACK_OPPOSING HEADWAY STATION_TRACK JUNCTION TERMINAL BUFFER LOCOMOTIVE WAGON OPERATIONAL_WINDOW 64. Baseline Validation Baseline ممکن است خودش Conflict داشته باشد. این بسیار مهم است. Engine نباید فرض کند: Existing Schedule = Feasible بلکه: Existing Schedule ↓ Validate ↓ Feasible / Infeasible 65. دو نوع Feasibility Operational Feasibility آیا Schedule طبق Infrastructure و Rules قابل اجراست؟ Historical Consistency آیا Schedule با داده ثبت‌شده موجود سازگار است؟ این دو یکی نیستند. 66. Historical Data ≠ Ground Truth داده موجود ممکن است: خطا داشته باشد؛ ناقص باشد؛ زمان واقعی را ثبت کرده باشد نه برنامه؛ زمان برنامه‌ای را ثبت کرده باشد؛ استثنا داشته باشد. پس: Observed و: Validated Operational Model دو مفهوم جدا هستند. 67. Calibration پس از Import: Observed Running Time ↓ Model Running Time ↓ Residual T_{observed} T_{model} ] 68. Running Time Calibration برای Block: f(L_b,V_b,TrainType,LoadState) ] و: T_{observed,b} T_{model,b} ] 69. Calibration Statistics برای هر Block: Mean Error Median Error P90 Error Max Error Sample Count 70. Data Quality vs Calibration این دو را قاطی نکنیم: Data Quality → آیا داده معتبر و قابل استفاده است؟ Calibration → مدل چقدر با داده معتبر سازگار است؟ 71. Acceptance Threshold Thresholdها Configuration هستند. مثلاً: calibration: max_mean_error_seconds: ... max_p90_error_seconds: ... مقدار واقعی باید با تیم بهره‌برداری تعیین شود. 72. First Capacity Run پس از Validation: Baseline ↓ Candidate F ↓ Schedule ↓ Validate مثلاً: F = Existing Train Count ابتدا بررسی می‌شود. 73. سپس Capacity Search Existing F ↓ F + 1 ↓ F + 2 ↓ ... یا Binary Search پس از اثبات مناسب بودن Monotonicity. 74. Capacity Proof خروجی: Observed Baseline: F = X Generated: F = X+1 → Feasible F = X+2 → Infeasible Calculated Capacity: Cr = X+1 مقادیر X صرفاً Placeholder هستند. 75. اگر Baseline Infeasible باشد این حالت کاملاً ممکن است. مثلاً: Observed Schedule = 30 Validated Schedule = Infeasible نباید نتیجه بگیریم: Capacity < 30 بلکه: Baseline requires investigation و دلیل مشخص می‌شود. 76. Production Capacity Rule تنها زمانی: Published Capacity تولید شود که: Data Quality = PASS Schedule = VALID Capacity Proof = VALID باشد. 77. Data Version هر Import: DATA-2026-09-28-001 مثلاً. 78. Mapping Version MAP-0.7.1 79. Model Version MODEL-0.7.0 80. Run Version RUN-2026-09-28-0001 81. Full Lineage RUN ↓ MODEL VERSION ↓ MAPPING VERSION ↓ DATA VERSION ↓ SOURCE FILE HASH ↓ SOURCE ROW 82. Source Hash برای Excel/Access: SHA-256 ثبت شود. این باعث می‌شود بدانیم یک Run دقیقاً با کدام فایل انجام شده است. 83. Import Manifest { "source_id": "ACCESS-AAA", "source_type": "ACCESS", "file_name": "aaa.accdb", "sha256": "...", "imported_at": "...", "mapping_version": "MAP-0.7.1" } 84. Excel Manifest { "source_id": "EXCEL-KHOLASE", "source_type": "EXCEL", "file_name": "REPORTKholase_31-06-1405_02-19-35.xlsx", "sha256": "...", "sheet": "Sheet1" } 85. Project Structure Version 0.7 rail_capacity/ │ ├── app/ │ ├── domain/ │ │ ├── station.py │ │ ├── train.py │ │ ├── train_run.py │ │ ├── train_formation.py │ │ ├── route.py │ │ ├── block.py │ │ ├── schedule.py │ │ └── resource.py │ │ │ ├── adapters/ │ │ ├── excel/ │ │ │ ├── reader.py │ │ │ └── mapper.py │ │ │ │ │ └── access/ │ │ ├── reader.py │ │ └── mapper.py │ │ │ ├── staging/ │ │ ├── models.py │ │ └── manifest.py │ │ │ ├── mapping/ │ │ ├── registry.py │ │ ├── rules.py │ │ └── validation.py │ │ │ ├── reconciliation/ │ │ ├── identity.py │ │ └── conflicts.py │ │ │ ├── quality/ │ │ ├── rules.py │ │ ├── report.py │ │ └── gate.py │ │ │ ├── scheduling/ │ │ ├── train_path.py │ │ ├── baseline.py │ │ ├── resources.py │ │ └── timetable.py │ │ │ ├── conflicts/ │ │ ├── engine.py │ │ └── graph.py │ │ │ ├── solver/ │ │ └── cp_sat.py │ │ │ ├── capacity/ │ │ ├── route.py │ │ ├── search.py │ │ └── proof.py │ │ │ ├── calibration/ │ │ ├── running_time.py │ │ └── statistics.py │ │ │ └── runs/ │ ├── manager.py │ └── lineage.py │ ├── mappings/ │ ├── excel_kholase.yml │ └── access_train_movement.yml │ ├── fixtures/ │ ├── real_data_sample/ │ └── synthetic/ │ ├── tests/ │ ├── adapters/ │ ├── mapping/ │ ├── quality/ │ ├── reconciliation/ │ ├── scheduling/ │ ├── conflicts/ │ ├── calibration/ │ └── capacity/ │ └── configs/ ├── model.yml ├── solver.yml └── quality.yml 86. Test Pyramid سه سطح Test: Unit ↓ Integration ↓ End-to-End 87. Adapter Unit Tests def test_excel_column_mapping(): ... def test_access_station_call_mapping(): ... def test_time_conversion(): ... def test_midnight_rollover(): ... 88. Mapping Tests def test_unverified_mapping_is_blocked(): ... def test_required_field_is_mapped(): ... def test_unknown_source_column_is_reported(): ... 89. Reconciliation Tests def test_same_train_is_matched(): assert resolve_train_identity( excel_record, access_record, ) 90. Data Quality Tests def test_missing_station_blocks_run(): ... def test_invalid_sequence_blocks_run(): ... def test_negative_runtime_blocks_run(): ... 91. Schedule Tests def test_access_sequence_creates_directed_path(): ... def test_return_train_reverses_path(): ... def test_station_calls_are_monotonic(): ... 92. Critical Test — Direction def test_inbound_path_is_reversed(): outbound = [ "A", "B", "C", "D", ] inbound = build_return_path(outbound) assert inbound == [ "D", "C", "B", "A", ] 93. Critical Test — Time def test_station_times_are_monotonic(): calls = [...] for previous, current in pairwise(calls): assert ( current.time_in_s >= previous.time_out_s ) 94. Critical Test — Conflict def test_real_baseline_conflict_is_detected(): conflicts = conflict_engine.detect( baseline_schedule ) assert any( c.conflict_type == "BLOCK_OVERLAP" for c in conflicts ) 95. Critical Test — Capacity def test_capacity_comes_from_feasible_schedule(): proof = capacity_engine.solve( real_problem ) assert proof.feasible_schedule_id assert proof.validation_status == "VALID" 96. End-to-End Test این Test مهم‌ترین Test Version 0.7 است: def test_real_vertical_slice(): excel = ExcelSourceAdapter( excel_path ) access = AccessSourceAdapter( connection_string ) excel_raw = excel.extract() access_raw = access.extract_table( "TrainMovement" ) staged = stage( excel_raw, access_raw, ) canonical = map_to_canonical( staged ) quality = validate_data( canonical ) assert quality.status == "PASS" baseline = build_baseline_schedule( canonical ) conflicts = detect_conflicts( baseline ) result = capacity_engine.solve( canonical ) assert result.capacity >= 0 97. نکته مهم درباره Fixture واقعی در Version 0.2 گفته بودیم: route_001.json یک Fixture است. اکنون باید صریحاً آن را به دو نوع تقسیم کنیم: Synthetic Fixture Real-data Fixture 98. Synthetic Fixture برای: Unit Tests Algorithm Tests Regression Tests است. 99. Real-data Fixture یک Subset کوچک از داده واقعی است که: PII-free Operationally representative Versioned Reproducible باشد. 100. عدم انتشار داده حساس اگر داده عملیاتی محرمانه باشد، Fixture عمومی نباید شامل: Sensitive commercial data Personal data Credentials Internal identifiers باشد. 101. Real Data Sample ساختار: fixtures/ └── real_data_sample/ ├── excel_sample.xlsx ├── access_export.csv ├── manifest.json └── expected/ ├── stations.json ├── train_runs.json └── baseline_schedule.json برای CI بهتر است خروجی Access در صورت امکان به یک فرمت قابل حمل مثل CSV/Parquet تبدیل شود و Adapter واقعی Access در Integration Test جداگانه اجرا شود. 102. Why? CI نباید به: Windows Access Driver Office وابسته باشد. بنابراین: CI → Portable Fixture Integration Environment → Real Access Adapter 103. First Vertical Slice Acceptance Version 0.7 قبول است اگر: [✓] Excel imported [✓] Access imported [✓] Source lineage preserved [✓] Mapping registered [✓] Unknown mappings flagged [✓] Stations normalized [✓] Train identity resolved [✓] TrainRun generated [✓] Direction generated [✓] Return path reversed [✓] Station Calls generated [✓] Baseline Schedule generated [✓] Conflicts detected [✓] Schedule validated [✓] Capacity searched [✓] Capacity Proof generated [✓] Explanation generated 104. چیزی که Version 0.7 نباید انجام دهد هنوز وارد این موارد نمی‌شویم: Full PostgreSQL migration Full Marketplace API Full GIS Microscopic Simulation Nationwide Network Optimization Automatic semantic interpretation اینها بعداً اضافه می‌شوند. 105. Vertical Slice Target در پایان Version 0.7 باید بتوانیم یک سؤال واقعی را پاسخ دهیم: «بر اساس داده عملیاتی موجود برای این Route، با همین Baseline و همین Infrastructure، چند Train واقعاً قابل برنامه‌ریزی است و دقیقاً چه Constraintای ظرفیت را محدود می‌کند؟» و پاسخ موتور باید چیزی شبیه این ساختار باشد: Route: X → Y Observed Trains: N Validated Baseline: N_valid Generated Capacity: C_r Capacity Proof: C_r ✓ FEASIBLE C_r + 1 ✕ INFEASIBLE Binding Constraint: Station S03 Secondary Constraint: Block B12 Wagon: Non-binding Locomotive: Near-binding Data Quality: PASS Model Version: 0.7.x Run: RUN-... اعداد در این مثال Placeholder هستند. 106. Architecture بعد از Version 0.7 REAL SOURCES / \ EXCEL ACCESS │ │ ▼ ▼ SOURCE ADAPTERS │ ▼ STAGING │ ▼ MAPPING / LINEAGE │ ▼ DATA QUALITY │ ┌──────┴──────┐ ▼ ▼ RECONCILIATION MASTER DATA │ │ └──────┬──────┘ ▼ CANONICAL DOMAIN │ ▼ BASELINE SCHEDULE │ ▼ TRAIN PATH ENGINE │ ▼ CONFLICT ENGINE │ ▼ SCHEDULING ENGINE │ ▼ NETWORK OPTIMIZER │ ▼ CAPACITY ENGINE │ ▼ CAPACITY PROOF │ ┌──────┴──────┐ ▼ ▼ EXPLANATION BOTTLENECK │ │ └──────┬──────┘ ▼ MARKETPLACE 107. اصل Version 0.7 از این نسخه به بعد: [ \boxed{ Source\ Data \neq Domain\ Model } ] و: [ \boxed{ Observed\ Schedule \neq Validated\ Schedule } ] و: [ \boxed{ Validated\ Schedule \neq Optimal\ Schedule } ] این سه تفکیک برای جلوگیری از بسیاری از خطاهای معماری پروژه حیاتی هستند. 108. اصل نهایی Data-to-Capacity زنجیره رسمی موتور از اینجا: [ \boxed{ Source \rightarrow Staging \rightarrow Mapping \rightarrow Canonical \rightarrow Validation \rightarrow TrainPath \rightarrow Conflict \rightarrow Schedule \rightarrow Capacity \rightarrow Proof } ] است. و در Production: [ \boxed{ No\ Verified\ Mapping \Rightarrow No\ Production\ Calculation } ] [ \boxed{ No\ Validated\ Schedule \Rightarrow No\ Accepted\ Capacity } ] [ \boxed{ No\ Capacity\ Proof \Rightarrow No\ Published\ Capacity } ] 109. خروجی نهایی Version 0.7 در پایان این مرحله ما سه چیز بسیار ارزشمند خواهیم داشت: 1. Real Data Adapter Excel + Access 2. Canonical Real Dataset Station Train TrainRun TrainStationCall Route BaselineSchedule 3. First Real Capacity Proof Real Data → Real Schedule → Real Conflicts → Feasible Schedule → Capacity → Proof → Explanation و این دقیقاً همان نقطه‌ای است که پروژه از یک Conceptual/Prototype Engine به یک Operational Engineering System نزدیک می‌شود. گام بعدی بعد از این نسخه، من پیشنهاد می‌کنم دیگر وارد طراحی مفهومی جدید نشویم و مستقیماً Version 0.8 را به شکل کدنویسی واقعی ببندیم: 0.8 ├── adapters/excel/reader.py ├── adapters/access/reader.py ├── staging/models.py ├── mapping/registry.py ├── mapping/rules.py ├── quality/gate.py ├── reconciliation/identity.py ├── domain/train_run.py ├── domain/train_station_call.py ├── scheduling/baseline.py ├── scheduling/train_path.py ├── conflicts/engine.py ├── capacity/search.py └── tests/test_real_vertical_slice.py یعنی مرحله بعد دیگر صرفاً Specification نخواهد بود؛ کد Python قابل اجرا برای Adapterهای Excel/Access، Mapping واقعی فیلدهای شما، ساخت TrainRun و TrainStationCall، بازسازی مسیر رفت/برگشت و اولین End-to-End Test را تولید می‌کنیم. --- ## ادامه 18 — پرسش کاربر (`message-id:fee6e762-ec61-4fdc-b819-a72cd28d6f59`) بله درسته --- ## ادامه 18 — پاسخ دستیار (`message-id:6aace57f-e56e-4201-99af-8735b59b1ac2`) عالی. پس این بار از Specification عبور می‌کنیم و Version 0.8 را به‌صورت کد واقعی و قابل اجرا می‌سازیم. نکته مهم این است که Adapterها را طوری می‌نویسیم که بعداً بدون دست‌زدن به هسته Scheduling/Capacity بتوانیم Source را عوض کنیم. برای Excel، pandas.read_excel امکان نگه‌داشتن داده به‌صورت object و همچنین خواندن Sheetهای مشخص یا همه Sheetها را دارد؛ این دقیقاً برای لایه Raw/Staging مناسب است. Pandas +1 برای Access نیز دسترسی باید پشت Adapter و وابسته به ODBC/Driver باقی بماند، نه اینکه منطق Access وارد Domain شود. Microsoft Learn +1 Implementation Package — Real Data Adapter & First Vertical Slice — نسخه 0.8 1. هدف Version 0.8 در این نسخه اولین مسیر اجرایی واقعی را می‌سازیم: Excel / Access ↓ Adapter ↓ Raw Records ↓ Mapping ↓ Canonical Domain ↓ TrainRun ↓ Directed Route ↓ TrainStationCall ↓ Baseline Schedule ↓ Conflict Engine ↓ Validation ↓ Capacity Search ↓ Capacity Proof این نسخه هنوز Production کامل نیست؛ هدف آن First Real Vertical Slice است. 2. ساختار پروژه rail_capacity/ │ ├── pyproject.toml ├── requirements.txt │ ├── app/ │ ├── __init__.py │ │ │ ├── domain/ │ │ ├── __init__.py │ │ ├── station.py │ │ ├── train.py │ │ ├── train_run.py │ │ ├── route.py │ │ ├── schedule.py │ │ └── source.py │ │ │ ├── adapters/ │ │ ├── __init__.py │ │ ├── excel.py │ │ └── access.py │ │ │ ├── staging/ │ │ ├── __init__.py │ │ └── models.py │ │ │ ├── mapping/ │ │ ├── __init__.py │ │ ├── registry.py │ │ └── rules.py │ │ │ ├── quality/ │ │ ├── __init__.py │ │ ├── rules.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ ├── __init__.py │ │ └── identity.py │ │ │ ├── scheduling/ │ │ ├── __init__.py │ │ ├── baseline.py │ │ └── path.py │ │ │ ├── conflicts/ │ │ ├── __init__.py │ │ └── engine.py │ │ │ ├── capacity/ │ │ ├── __init__.py │ │ └── search.py │ │ │ └── pipeline/ │ ├── __init__.py │ └── vertical_slice.py │ ├── mappings/ │ ├── excel_kholase.yml │ └── access_train_movement.yml │ ├── fixtures/ │ ├── synthetic/ │ │ └── train_movements.json │ │ │ └── real_sample/ │ └── README.md │ └── tests/ ├── test_time.py ├── test_mapping.py ├── test_identity.py ├── test_direction.py ├── test_baseline.py ├── test_conflict.py ├── test_quality.py └── test_vertical_slice.py 3. requirements.txt نسخه اولیه: pandas>=3.0,<4 openpyxl>=3.1,<4 pydantic>=2,<3 PyYAML>=6,<7 ortools>=9.14,<10 pytest>=8,<10 برای Access: pyodbc>=5,<6 به‌عنوان Dependency اختیاری Environment نگه داشته می‌شود. علت این تفکیک این است که Python Package و Access ODBC Driver دو وابستگی متفاوت‌اند. 4. pyproject.toml [project] name = "rail-capacity-engine" version = "0.8.0" description = "Railway Capacity Generation, Estimation and Network Optimization Engine" requires-python = ">=3.11" dependencies = [ "pandas>=3.0,<4", "openpyxl>=3.1,<4", "pydantic>=2,<3", "PyYAML>=6,<7", "ortools>=9.14,<10", ] [project.optional-dependencies] access = [ "pyodbc>=5,<6", ] dev = [ "pytest>=8,<10", ] [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] 5. Source Domain فایل: app/domain/source.py from dataclasses import dataclass from typing import Any @dataclass(frozen=True) class RawRecord: source_id: str source_type: str source_name: str row_number: int | None values: dict[str, Any] @dataclass(frozen=True) class SourceColumn: name: str detected_type: str | None nullable: bool @dataclass(frozen=True) class SourceInspection: source_id: str source_type: str columns: tuple[SourceColumn, ...] row_count: int warnings: tuple[str, ...] 6. Excel Adapter فایل: app/adapters/excel.py from pathlib import Path import pandas as pd from app.domain.source import ( RawRecord, SourceColumn, SourceInspection, ) class ExcelAdapter: def __init__( self, path: str | Path, source_id: str = "EXCEL", ): self.path = Path(path) self.source_id = source_id def inspect( self, sheet_name: str | int = 0, ) -> SourceInspection: df = pd.read_excel( self.path, sheet_name=sheet_name, dtype=object, ) columns = tuple( SourceColumn( name=str(column), detected_type=str(df[column].dtype), nullable=bool(df[column].isna().any()), ) for column in df.columns ) return SourceInspection( source_id=self.source_id, source_type="EXCEL", columns=columns, row_count=len(df), warnings=(), ) def extract( self, sheet_name: str | int = 0, ) -> tuple[RawRecord, ...]: df = pd.read_excel( self.path, sheet_name=sheet_name, dtype=object, ) records: list[RawRecord] = [] for index, row in df.iterrows(): values = { str(column): row[column] for column in df.columns } records.append( RawRecord( source_id=self.source_id, source_type="EXCEL", source_name=self.path.name, row_number=int(index) + 2, values=values, ) ) return tuple(records) استفاده از dtype=object در این مرحله عمدی است؛ Source Adapter نباید قبل از Mapping تصمیم بگیرد که مثلاً یک مقدار زمانی یا عددی دقیقاً چه معنای Domain-level دارد. pandas این حالت را صراحتاً پشتیبانی می‌کند. 7. Excel Sheet Discovery import pandas as pd def list_excel_sheets(path: str): with pd.ExcelFile(path) as workbook: return workbook.sheet_names این قابلیت مستقیماً توسط ExcelFile.sheet_names ارائه می‌شود. 8. Access Adapter فایل: app/adapters/access.py from typing import Any import pyodbc from app.domain.source import RawRecord class AccessAdapter: def __init__( self, connection_string: str, source_id: str = "ACCESS", ): self.connection_string = connection_string self.source_id = source_id def _connect(self): return pyodbc.connect( self.connection_string ) def list_tables(self) -> tuple[str, ...]: with self._connect() as connection: cursor = connection.cursor() tables = [] for row in cursor.tables( tableType="TABLE" ): tables.append(row.table_name) return tuple(sorted(set(tables))) def extract_table( self, table_name: str, ) -> tuple[RawRecord, ...]: with self._connect() as connection: cursor = connection.cursor() cursor.execute( f"SELECT * FROM [{table_name}]" ) columns = [ column[0] for column in cursor.description ] records = [] for row_number, row in enumerate( cursor.fetchall(), start=1, ): values = { column: value for column, value in zip(columns, row) } records.append( RawRecord( source_id=self.source_id, source_type="ACCESS", source_name=table_name, row_number=row_number, values=values, ) ) return tuple(records) ODBC یک لایه عمومی دسترسی به DBMSهای مختلف است و Driver مسئول ارتباط با DBMS مشخص است؛ به همین دلیل Access Adapter را پشت Interface نگه می‌داریم. 9. نکته امنیتی Access در Production: connection_string نباید در Source Code باشد. در عوض: Environment Variable Secret Store Configuration Service استفاده شود. مثلاً: ACCESS_CONNECTION_STRING="..." 10. Time Parser فایل: app/mapping/rules.py import math import re TIME_PATTERN = re.compile( r"^\s*(\d{1,2}):(\d{2})(?::(\d{2}))?\s*$" ) def parse_time_to_seconds( value, ) -> int | None: if value is None: return None if isinstance(value, float): if math.isnan(value): return None text = str(value).strip() if not text: return None match = TIME_PATTERN.match(text) if not match: raise ValueError( f"Invalid time value: {value!r}" ) hour = int(match.group(1)) minute = int(match.group(2)) second = int( match.group(3) or 0 ) if hour > 23 or minute > 59 or second > 59: raise ValueError( f"Invalid clock time: {value!r}" ) return ( hour * 3600 + minute * 60 + second ) 11. Day Rollover def normalize_time_sequence( previous_s: int | None, current_s: int | None, ) -> int | None: if current_s is None: return None if previous_s is None: return current_s result = current_s while result < previous_s: result += 24 * 3600 return result مثلاً: 23:50 → 01:20 تبدیل می‌شود به: 85800 → 91200 12. Station Domain app/domain/station.py from dataclasses import dataclass @dataclass(frozen=True) class Station: id: str name: str normalized_name: str station_number: int | None = None 13. Train Domain app/domain/train.py from dataclasses import dataclass @dataclass(frozen=True) class Train: id: str train_no: str name: str | None origin_station_id: str destination_station_id: str 14. TrainRun app/domain/train_run.py from dataclasses import dataclass from datetime import date @dataclass(frozen=True) class TrainRun: id: str train_id: str operating_date: date origin_station_id: str destination_station_id: str direction: str 15. TrainStationCall from dataclasses import dataclass @dataclass(frozen=True) class TrainStationCall: train_run_id: str station_id: str sequence: int time_in_s: int | None time_take_s: int | None time_out_s: int | None required_wait_s: int | None kilometerage: float | None distance_m: float | None max_speed_kmh: float | None raw_values: dict[str, object] 16. چرا raw_values را نگه می‌داریم؟ برای Audit و Debugging: Canonical Field ↓ Source Field ↓ Raw Value مثلاً: time_out_s = 30600 ولی: raw_values["time_out"] = "08:30" همچنان موجود است. 17. Directed Route from dataclasses import dataclass @dataclass(frozen=True) class DirectedRoutePath: id: str route_id: str direction: str ordered_station_ids: tuple[str, ...] 18. Route Builder def build_directed_route( route_id: str, station_ids: list[str], direction: str, ) -> DirectedRoutePath: if direction == "FORWARD": ordered = tuple(station_ids) elif direction == "REVERSE": ordered = tuple( reversed(station_ids) ) else: raise ValueError( f"Unsupported direction: {direction}" ) return DirectedRoutePath( id=f"{route_id}:{direction}", route_id=route_id, direction=direction, ordered_station_ids=ordered, ) 19. Identity Resolution فایل: app/reconciliation/identity.py def normalize_text( value: object, ) -> str: if value is None: return "" return ( str(value) .strip() .replace("ي", "ی") .replace("ك", "ک") ) def normalize_train_no( value: object, ) -> str: return normalize_text(value).upper() 20. Train Identity Key def train_identity_key( train_no: object, train_name: object, ) -> tuple[str, str]: return ( normalize_train_no(train_no), normalize_text(train_name), ) در نسخه بعد می‌توان Origin/Destination و Calendar Pattern را نیز در Identity وارد کرد. 21. Mapping Registry فایل: app/mapping/registry.py from dataclasses import dataclass @dataclass(frozen=True) class FieldMapping: source_field: str target_field: str status: str transformation: str | None = None class MappingRegistry: def __init__( self, mappings: list[FieldMapping], ): self._mappings = { item.source_field: item for item in mappings } def get( self, source_field: str, ) -> FieldMapping | None: return self._mappings.get( source_field ) def is_verified( self, source_field: str, ) -> bool: mapping = self.get(source_field) return ( mapping is not None and mapping.status == "VERIFIED" ) 22. Excel Mapping فایل: mappings/excel_kholase.yml source_id: EXCEL-KHOLASE fields: "نام قطار": target: train.name status: VERIFIED "شماره قطار از مبدا": target: train.train_no status: VERIFIED "ساعت حرکت از مبدا": target: train_run.departure_time status: VERIFIED transformation: HHMM_TO_SECONDS "روزهای حرکت از مبدا": target: operating_pattern.days status: VERIFIED "ساعت ورود به مقصد": target: train_run.arrival_time status: VERIFIED transformation: HHMM_TO_SECONDS "شماره قطار از مقصد": target: return_train.train_no status: VERIFIED "ساعت حرکت از مقصد": target: return_train.departure_time status: VERIFIED transformation: HHMM_TO_SECONDS "روزهای حرکت از مقصد": target: return_operating_pattern.days status: VERIFIED "ساعت رسیدن به مبدا": target: return_train.arrival_time status: VERIFIED transformation: HHMM_TO_SECONDS 23. Access Mapping فایل: mappings/access_train_movement.yml source_id: ACCESS-TRAIN-MOVEMENT fields: TrainNo: target: train.train_no status: VERIFIED StationName: target: station.name status: VERIFIED StationNumber: target: station.station_number status: VERIFIED Sequence: target: station_call.sequence status: VERIFIED time_in: target: station_call.time_in status: PROVISIONAL transformation: HHMM_TO_SECONDS time_take: target: station_call.time_take status: PROVISIONAL transformation: HHMM_TO_SECONDS time_out: target: station_call.time_out status: PROVISIONAL transformation: HHMM_TO_SECONDS RequiredWait: target: station_call.required_wait status: PROVISIONAL Kilometerage: target: station_call.kilometerage status: PROVISIONAL MaxSpeed: target: station_call.max_speed_kmh status: VERIFIED Distance: target: station_call.distance_m status: PROVISIONAL sumDistancezz: target: station_call.cumulative_distance status: PROVISIONAL seir: target: source.seir status: UNMAPPED faultV: target: source.faultV status: UNMAPPED نکته کلیدی: PROVISIONAL یعنی برای Exploration قابل مشاهده است، اما نباید بدون تأیید وارد Production Capacity Calculation شود. 24. Canonical Mapper class TrainMovementMapper: def map_record( self, record, ) -> dict: values = record.values return { "train_no": normalize_train_no( values.get("TrainNo") ), "station_name": normalize_text( values.get("StationName") ), "station_number": values.get( "StationNumber" ), "sequence": int( values["Sequence"] ), "time_in_s": parse_time_to_seconds( values.get("time_in") ), "time_take_s": parse_time_to_seconds( values.get("time_take") ), "time_out_s": parse_time_to_seconds( values.get("time_out") ), "required_wait_s": values.get("RequiredWait"), "kilometerage": values.get("Kilometerage"), "max_speed_kmh": values.get("MaxSpeed"), "distance_m": values.get("Distance"), "raw_values": dict(values), } 25. Data Quality app/quality/rules.py from dataclasses import dataclass @dataclass(frozen=True) class QualityIssue: code: str severity: str message: str entity_id: str | None = None 26. Quality Rules def validate_station_call( call, ) -> list[QualityIssue]: issues = [] if not call.station_id: issues.append( QualityIssue( code="MISSING_STATION", severity="BLOCKING", message="Station is missing", ) ) if call.sequence < 0: issues.append( QualityIssue( code="INVALID_SEQUENCE", severity="BLOCKING", message="Sequence must be non-negative", ) ) if ( call.time_in_s is not None and call.time_out_s is not None and call.time_out_s < call.time_in_s ): issues.append( QualityIssue( code="NEGATIVE_DWELL", severity="BLOCKING", message=( "time_out occurs before time_in" ), ) ) return issues 27. Quality Gate from dataclasses import dataclass @dataclass(frozen=True) class QualityReport: issues: tuple[QualityIssue, ...] status: str def quality_gate( issues: list[QualityIssue], ) -> QualityReport: if any( issue.severity == "BLOCKING" for issue in issues ): status = "BLOCK" elif issues: status = "PASS_WITH_WARNINGS" else: status = "PASS" return QualityReport( issues=tuple(issues), status=status, ) 28. Station Call Ordering def validate_call_sequence( calls, ) -> list[QualityIssue]: issues = [] ordered = sorted( calls, key=lambda x: x.sequence, ) for previous, current in zip( ordered, ordered[1:], ): if ( previous.time_out_s is not None and current.time_in_s is not None and current.time_in_s < previous.time_out_s ): issues.append( QualityIssue( code="NON_MONOTONIC_TIME", severity="BLOCKING", message=( "Station call time is not monotonic" ), ) ) return issues 29. Baseline Schedule app/scheduling/baseline.py from dataclasses import dataclass @dataclass(frozen=True) class BaselineSchedule: id: str train_runs: tuple station_calls: tuple source_ids: tuple[str, ...] status: str 30. Build Baseline def build_baseline_schedule( train_runs, station_calls, source_ids, ): return BaselineSchedule( id="BASELINE-001", train_runs=tuple(train_runs), station_calls=tuple(station_calls), source_ids=tuple(source_ids), status="IMPORTED", ) 31. Directed Train Path app/scheduling/path.py from dataclasses import dataclass @dataclass(frozen=True) class TrainPath: train_run_id: str direction: str ordered_station_ids: tuple[str, ...] ordered_block_ids: tuple[str, ...] 32. Train Path Builder def build_train_path( train_run, station_calls, station_to_block, ) -> TrainPath: ordered_calls = sorted( station_calls, key=lambda c: c.sequence, ) stations = tuple( call.station_id for call in ordered_calls ) blocks = tuple( station_to_block[ (a, b) ] for a, b in zip( stations, stations[1:], ) ) return TrainPath( train_run_id=train_run.id, direction=train_run.direction, ordered_station_ids=stations, ordered_block_ids=blocks, ) 33. Important Validation اگر: station A station B station C station D باشد: Forward: A → B → C → D Reverse: D → C → B → A و Blockها نیز باید با Direction درست ساخته شوند. 34. Conflict Engine app/conflicts/engine.py from dataclasses import dataclass @dataclass(frozen=True) class Movement: train_run_id: str resource_id: str start_s: int end_s: int direction: str 35. Interval Overlap def overlaps( a: Movement, b: Movement, ) -> bool: return ( a.start_s < b.end_s and b.start_s < a.end_s ) 36. Conflict Detection @dataclass(frozen=True) class Conflict: train_a: str train_b: str resource_id: str conflict_type: str def detect_conflicts( movements: list[Movement], ) -> list[Conflict]: conflicts = [] for i, a in enumerate(movements): for b in movements[i + 1:]: if ( a.resource_id != b.resource_id ): continue if not overlaps(a, b): continue conflict_type = ( "SINGLE_TRACK_OPPOSING" if a.direction != b.direction else "RESOURCE_OVERLAP" ) conflicts.append( Conflict( train_a=a.train_run_id, train_b=b.train_run_id, resource_id=a.resource_id, conflict_type=conflict_type, ) ) return conflicts 37. چرا Conflict Engine مستقل است؟ چون باید بتوانیم: Observed Schedule را بدون Solver بررسی کنیم. و: Generated Schedule را نیز بدون Solver دوباره Validate کنیم. 38. ظرفیت فایل: app/capacity/search.py @dataclass(frozen=True) class CapacityProof: feasible_flow: int next_flow: int next_flow_feasible: bool schedule_id: str | None binding_constraints: tuple[str, ...] 39. اصل Capacity Search def search_capacity( problem, solve_fn, validate_fn, lower_bound: int, upper_bound: int, ): best = None low = lower_bound high = upper_bound while low <= high: candidate = (low + high) // 2 schedule = solve_fn( problem, candidate, ) if schedule is None: high = candidate - 1 continue validation = validate_fn( schedule ) if validation.is_valid: best = ( candidate, schedule, ) low = candidate + 1 else: high = candidate - 1 if best is None: return CapacityProof( feasible_flow=0, next_flow=lower_bound, next_flow_feasible=False, schedule_id=None, binding_constraints=(), ) flow, schedule = best next_flow = flow + 1 next_schedule = solve_fn( problem, next_flow, ) next_feasible = ( next_schedule is not None and validate_fn( next_schedule ).is_valid ) return CapacityProof( feasible_flow=flow, next_flow=next_flow, next_flow_feasible=next_feasible, schedule_id=schedule.id, binding_constraints=(), ) 40. اصلاح مهم نسبت به Version 0.2 در نسخه قبلی Capacity Search به اشتباه می‌توانست یک Synthetic Problem جدید بسازد. در Version 0.8: Capacity Search فقط روی: Canonical Real Problem کار می‌کند. یعنی: search_capacity( problem=real_problem, ... ) نه: build_fixture_problem(flow) 41. Vertical Slice Pipeline def run_vertical_slice( excel_path, access_connection_string, ): # 1. Extract excel = ExcelAdapter( excel_path, source_id="EXCEL-KHOLASE", ) excel_records = excel.extract() access = AccessAdapter( access_connection_string, source_id="ACCESS-AAA", ) access_records = access.extract_table( "TrainMovement" ) # 2. Mapping mapped_records = map_access_records( access_records ) # 3. Canonical stations = build_stations( mapped_records ) train_runs = build_train_runs( mapped_records ) station_calls = build_station_calls( mapped_records ) # 4. Quality quality = validate_dataset( stations, train_runs, station_calls, ) if quality.status == "BLOCK": return { "status": "BLOCKED", "quality": quality, } # 5. Baseline baseline = build_baseline_schedule( train_runs, station_calls, source_ids=( "EXCEL-KHOLASE", "ACCESS-AAA", ), ) # 6. Paths paths = build_paths( train_runs, station_calls, ) # 7. Conflicts conflicts = detect_baseline_conflicts( baseline, paths, ) return { "status": "READY_FOR_CAPACITY", "quality": quality, "baseline": baseline, "paths": paths, "conflicts": conflicts, } 42. نکته درباره Excel + Access در این مرحله Excel و Access هر دو وارد Pipeline می‌شوند، ولی نقش آنها یکسان نیست. پیشنهاد فعلی: Excel → Service / Operating Pattern Access → Detailed Operational Movement بنابراین: Excel + Access نباید دو نسخه مستقل از یک Train تلقی شوند. 43. Reconciliation ساختار: def reconcile_train( excel_record, access_records, ): train_no = normalize_train_no( excel_record.values.get( "شماره قطار از مبدا" ) ) matches = [ record for record in access_records if normalize_train_no( record.values.get("TrainNo") ) == train_no ] return matches در نسخه بعد باید Origin/Destination و Operating Date نیز به Matching اضافه شود. 44. مشکل Train Number شماره قطار به تنهایی ممکن است Unique نباشد. پس Production Identity باید بتواند از: TrainNo + OperatingDate + Origin + Destination + Service Pattern استفاده کند. 45. Data Quality Dashboard خروجی Pipeline: Source: EXCEL-KHOLASE Rows: ... ACCESS: aaa.accdb Train Runs: ... Station Calls: ... Blocking Errors: ... Warnings: ... Reconciliation Issues: ... Status: PASS / BLOCK 46. End-to-End Pytest def test_real_vertical_slice( excel_fixture, access_fixture, ): result = run_vertical_slice( excel_path=excel_fixture, access_connection_string=access_fixture, ) assert result["status"] in { "READY_FOR_CAPACITY", "BLOCKED", } این تست عمداً اجازه BLOCKED را می‌دهد، چون Data Quality واقعی بخشی از رفتار مورد انتظار سیستم است. 47. Test — Reverse Route def test_reverse_route(): route = build_directed_route( route_id="R01", station_ids=[ "A", "B", "C", "D", ], direction="REVERSE", ) assert route.ordered_station_ids == ( "D", "C", "B", "A", ) 48. Test — Midnight def test_midnight_rollover(): departure = parse_time_to_seconds( "23:50" ) arrival = parse_time_to_seconds( "01:20" ) normalized = normalize_time_sequence( departure, arrival, ) assert normalized > departure 49. Test — Access Mapping def test_access_mapping(): record = RawRecord( source_id="ACCESS", source_type="ACCESS", source_name="TrainMovement", row_number=1, values={ "TrainNo": "1201", "StationName": "تهران", "StationNumber": 1, "Sequence": 1, "time_in": "06:00", "time_take": "06:05", "time_out": "06:10", "RequiredWait": 300, "MaxSpeed": 120, }, ) mapped = TrainMovementMapper().map_record( record ) assert mapped["train_no"] == "1201" assert mapped["sequence"] == 1 assert mapped["time_out_s"] == 22200 50. Test — Unknown Mapping def test_unknown_mapping_does_not_become_domain_field(): registry = MappingRegistry([ FieldMapping( source_field="faultV", target_field="source.faultV", status="UNMAPPED", ) ]) assert not registry.is_verified( "faultV" ) 51. Test — Quality Gate def test_blocking_quality_issue(): issues = [ QualityIssue( code="MISSING_STATION", severity="BLOCKING", message="Station missing", ) ] report = quality_gate(issues) assert report.status == "BLOCK" 52. Test — Conflict def test_block_overlap(): movements = [ Movement( train_run_id="TR01", resource_id="B01", start_s=100, end_s=200, direction="FORWARD", ), Movement( train_run_id="TR02", resource_id="B01", start_s=150, end_s=250, direction="FORWARD", ), ] conflicts = detect_conflicts( movements ) assert len(conflicts) == 1 assert ( conflicts[0].conflict_type == "RESOURCE_OVERLAP" ) 53. Test — Non-overlap def test_no_conflict_when_intervals_do_not_overlap(): movements = [ Movement( train_run_id="TR01", resource_id="B01", start_s=100, end_s=200, direction="FORWARD", ), Movement( train_run_id="TR02", resource_id="B01", start_s=200, end_s=300, direction="FORWARD", ), ] conflicts = detect_conflicts( movements ) assert conflicts == [] 54. Test — Capacity Proof def test_capacity_proof_requires_next_flow_check(): proof = CapacityProof( feasible_flow=20, next_flow=21, next_flow_feasible=False, schedule_id="SCH-20", binding_constraints=( "S03", ), ) assert proof.feasible_flow == 20 assert proof.next_flow == 21 assert not proof.next_flow_feasible 55. مهم: Fixture واقعی برای Fixture واقعی نباید خودمان داده جعلی را به اسم داده واقعی وارد کنیم. بنابراین: fixtures/real_sample/ در Repository فقط زمانی پر می‌شود که Sample واقعی از کاربر دریافت و Sanitized شود. فعلاً: fixtures/synthetic/ برای Testهای الگوریتمی استفاده می‌شود. 56. Sample مورد نیاز از کاربر برای اجرای واقعی Version 0.8 کافی است یک Sample کوچک از دو فایل داشته باشیم: Excel ترجیحاً: 10–50 rows Access ترجیحاً: یک Table یا Export آن به CSV: 100–1000 rows با حفظ همان ستون‌ها. 57. چرا Sample کوچک؟ برای اولین Vertical Slice نیازی به کل Database نداریم. ابتدا: 5–20 Train و: 10–30 Station کافی است. بعد همان Pipeline را روی کل داده اجرا می‌کنیم. 58. چیزی که در این مرحله نباید انجام دهیم نباید: Access Field → حدس معنایی → Hardcode → Solver انجام شود. مثلاً: time_take = dwell تا زمانی که معنای آن تأیید نشده، نباید به‌صورت قطعی وارد مدل شود. 59. نتیجه فنی Version 0.8 اکنون یک Pipeline واقعی داریم: [ \boxed{ Excel/Access \rightarrow Raw \rightarrow Mapping \rightarrow Canonical } ] و: [ \boxed{ Canonical \rightarrow TrainRun \rightarrow DirectedPath \rightarrow StationCalls \rightarrow Baseline } ] و: [ \boxed{ Baseline \rightarrow Conflict \rightarrow Validation \rightarrow Capacity } ] 60. معیار موفقیت Version 0.8 نسخه 0.8 زمانی موفق است که بتوانیم بدون تغییر در Core Engine: Excel Source را با: Synthetic Fixture جایگزین کنیم و همان: Conflict Engine Scheduling Engine Capacity Engine را اجرا کنیم. این یعنی: [ \boxed{ Source\ Independence } ] و دقیقاً همان چیزی است که از یک معماری جدی Enterprise انتظار داریم. 61. مرحله بعد — Version 0.9 بعد از این Vertical Slice، گام بعدی باید: Version 0.9 Real Scheduling + CP-SAT + Station/Junction Resources باشد. یعنی دیگر فقط Conflict Detection انجام نمی‌دهیم؛ Solver واقعاً روی داده واقعی تصمیم می‌گیرد: Train Order Crossing Station Station Track Waiting Batch Headway Single Track Double Track Junction Operational Window و Schedule جدید تولید می‌کند. در آن مرحله زنجیره کامل می‌شود: REAL DATA ↓ BASELINE ↓ CONFLICTS ↓ CP-SAT ↓ GENERATED SCHEDULE ↓ INDEPENDENT VALIDATION ↓ CAPACITY SEARCH ↓ CAPACITY PROOF و این همان نقطه‌ای است که می‌توانیم اولین عدد ظرفیت تولیدشده توسط خود موتور را در مقابل Baseline واقعی قرار دهیم. یک نکته اجرایی مهم: در این مرحله بهترین کار این است که دیگر فیلدهای Access را حدس نزنیم. اگر فایل‌های REPORTKholase...xlsx و aaa.accdb را در همین گفتگو در دسترس قرار دهید، می‌توانیم دقیقاً بر اساس Schema واقعی آنها Mapping را ببندیم؛ مخصوصاً معنای time_take, RequiredWait, seir, faultV, Distance و sumDistancezz. این کار جلوی یکی از خطرناک‌ترین خطاهای این پروژه را می‌گیرد: اینکه موتور از یک Mapping ظاهراً درست ولی معناییِ اشتباه، ظرفیت تولید کند. --- ## ادامه 19 — پرسش کاربر (`message-id:2d62e97d-9759-4cae-9a03-b32598483495`) این نام فیلدهای فایل هست: ID kol TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir --- ## ادامه 19 — پاسخ دستیار (`message-id:c173535b-0fe6-49d1-b603-503862698c48`) عالی است. با این مشخصات، می‌توانیم Mapping فایل Access را دقیق‌تر و رسمی‌تر کنیم و دیگر لازم نیست نام فیلدها را حدس بزنیم. نکته مهم این است که نام فیلدها مشخص شده، اما Semantic بعضی فیلدها هنوز باید از روی داده واقعی تأیید شود. بنابراین در Version 0.9 این تفکیک را حفظ می‌کنیم. Mapping پیشنهادی فعلی Source Field Canonical Field وضعیت ID source_record_id قطعی kol source.kol نیازمند تأیید معنا TrainNo train_run.train_no قطعی StationName station.name قطعی StationNumber station.source_number قطعی Sequence train_station_call.sequence قطعی time_in train_station_call.time_in نام فیلد قطعی؛ Semantic قابل تأیید time_take train_station_call.time_take نیازمند تأیید time_out train_station_call.time_out نام فیلد قطعی؛ Semantic قابل تأیید RequiredWait train_station_call.required_wait نیازمند تأیید Kilometerage train_station_call.kilometerage نیازمند تأیید MaxSpeed train_station_call.max_speed قطعی از نظر نام/نوع مفهومی TrainName train.train_name قطعی Distance train_station_call.distance نیازمند تأیید واحد/تعریف sumDistancezz train_station_call.cumulative_distance موقت؛ باید تأیید شود seir source.seir هنوز Semantic نامشخص نکته بسیار مهم درباره seir در این مرحله نباید فرض کنیم seir همان direction یا route است. فعلاً بهتر است: seir ↓ Source Field ↓ Unmapped / Provisional ↓ Semantic Validation ↓ Canonical Field و همین‌طور برای kol. Mapping YAML نسخه جدید می‌توانیم Mapping را به این شکل تثبیت کنیم: YAML source: access table: fields: ID: target: source_record_id status: VERIFIED kol: target: source.kol status: PROVISIONAL TrainNo: target: train_run.train_no status: VERIFIED StationName: target: station.name status: VERIFIED StationNumber: target: station.source_number status: VERIFIED Sequence: target: train_station_call.sequence status: VERIFIED time_in: target: train_station_call.time_in status: PROVISIONAL time_take: target: train_station_call.time_take status: PROVISIONAL time_out: target: train_station_call.time_out status: PROVISIONAL RequiredWait: target: train_station_call.required_wait status: PROVISIONAL Kilometerage: target: train_station_call.kilometerage status: PROVISIONAL MaxSpeed: target: train_station_call.max_speed status: VERIFIED TrainName: target: train.train_name status: VERIFIED Distance: target: train_station_call.distance status: PROVISIONAL sumDistancezz: target: train_station_call.cumulative_distance status: PROVISIONAL seir: target: source.seir status: UNMAPPED چیزی که برای Version 0.9 تغییر می‌کند با این اطلاعات، من دیگر پیشنهاد نمی‌کنم داده Access را صرفاً به عنوان یک TrainMovement ساده بخوانیم. ساختار واقعی فایل نشان می‌دهد که هر رکورد اساساً یک Train–Station Call است: Train │ └── TrainRun │ ├── StationCall #1 │ ├── Station │ ├── Sequence │ ├── time_in │ ├── time_take │ ├── time_out │ ├── RequiredWait │ ├── Kilometerage │ ├── Distance │ └── MaxSpeed │ ├── StationCall #2 │ ├── StationCall #3 │ └── ... این دقیقاً با مدل OD-Centric Train Formation + Detailed Intermediate Operational Path که برای پروژه تعریف کرده‌ایم سازگار است. در نتیجه، TrainNo + Sequence یکی از کلیدهای بسیار مهم برای بازسازی مسیر واقعی قطار خواهد بود: TrainNo + Sequence ↓ Ordered TrainStationCall[] ↓ Directed Operational Path ↓ Block / Station / Resource Occupancy ↓ Schedule ↓ Capacity و TrainName نیز بهتر است فقط به‌عنوان attribute قطار باقی بماند و کلید شناسایی TrainRun نباشد. اگر حالا خود فایل Access (aaa.accdb) را هم در اختیارم بگذاری، مرحله بعدی می‌تواند دقیقاً روی داده واقعی انجام شود: استخراج Schema و چند رکورد نمونه، بررسی نوع داده هر فیلد، بررسی مقدارهای kol، time_take، RequiredWait، seir و sumDistancezz، و سپس قفل‌کردن Mapping واقعی قبل از ورود به CP-SAT و Scheduling. --- ## ادامه 20 — پرسش کاربر (`message-id:cf45b56a-637c-4816-a935-095448c37689`) متاسفانه امکان آپلود نیست. خود محتوا رو به صورت متنی اینجا بذارم؟ --- ## ادامه 20 — پاسخ دستیار (`message-id:68496ff5-8f95-48e4-b6e8-cc2cc3e7a4b0`) بله، حتماً؛ اتفاقاً برای این مرحله متن کاملاً کافی است. بهترین کار این است که چند نمونه واقعی از رکوردهای Access را اینجا بگذاری تا Semantic فیلدها را از روی الگوی داده بررسی کنیم. لطفاً ترجیحاً به همین ترتیب بفرست: ID | kol | TrainNo | StationName | StationNumber | Sequence | time_in | time_take | time_out | RequiredWait | Kilometerage | MaxSpeed | TrainName | Distance | sumDistancezz | seir و بعد حدود 20 تا 50 رکورد واقعی، ترجیحاً مربوط به یک یا دو TrainNo کامل که چند Station پشت سر هم داشته باشند. مثلاً: 1 | ... | 1234 | تهران | ... | 1 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... 2 | ... | 1234 | سمنان | ... | 2 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... 3 | ... | 1234 | شاهرود | ... | 3 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... ... اگر حجم داده زیاد است، یک‌جا نفرست. می‌توانی در چند پیام پشت‌سرهم بفرستی و من همه را در همین تحلیل نگه می‌دارم. به‌خصوص این فیلدها برایمان مهم‌اند: kol time_in time_take time_out RequiredWait Kilometerage Distance sumDistancezz seir با نمونه واقعی می‌توانیم مشخص کنیم مثلاً آیا: Distance فاصله بین دو ایستگاه است یا فاصله از مبدأ، و آیا: sumDistancezz واقعاً cumulative distance است یا معنای دیگری دارد. همچنین از روی تغییرات Sequence، StationNumber، زمان‌ها و seir می‌توانیم Direction / مسیر رفت و برگشت را بدون حدس‌زدن استخراج کنیم. اگر می‌توانی، اول 20–50 ردیف مربوط به یک TrainNo کامل را بفرست؛ این برای شروع بهترین نمونه است. --- ## ادامه 21 — پرسش کاربر (`message-id:a9c63453-d03b-4de9-a6b0-1818182d369b`) 1 1 100 گار 385 1 09:00 0 09:00 157.000 گار-اندیمشک1 0.000 0.000 34 2 1 100 ساقه 157 2 09:34 0 09:34 200.000 گار-اندیمشک1 0.000 0.000 38 3 1 100 باغ  يك 158 3 10:12 0 10:12 220.000 گار-اندیمشک1 0.000 0.000 22 4 1 100 سواريان 159 4 10:34 0 10:34 234.000 گار-اندیمشک1 0.000 0.000 24 5 1 100 راه گرد 160 5 10:58 0 10:58 248.000 گار-اندیمشک1 0.000 0.000 22 6 1 100 نان گرد 161 6 11:20 0 11:20 265.000 گار-اندیمشک1 0.000 0.000 25 7 1 100 مشك اباد 162 7 11:45 0 11:45 286.000 گار-اندیمشک1 0.000 0.000 22 8 1 100 ملك اباد 163 8 12:07 0 12:07 304.000 گار-اندیمشک1 0.000 0.000 20 9 1 100 اراك 164 9 12:27 60 13:27 60 320.000 گار-اندیمشک1 0.000 0.000 30 10 1 100 سمنگان 165 10 13:57 0 13:57 337.000 گار-اندیمشک1 0.000 0.000 20 11 1 100 شازند 166 11 14:17 18 14:35 354.000 گار-اندیمشک1 0.000 0.000 32 12 1 100 نوراباد 167 12 15:07 0 15:07 373.000 گار-اندیمشک1 0.000 0.000 20 13 1 100 سميه 168 13 15:27 0 15:27 388.000 گار-اندیمشک1 0.000 0.000 16 14 1 100 مومن آباد 169 14 15:43 27 16:10 402.000 گار-اندیمشک1 0.000 0.000 25 15 1 100 ازنا 170 15 16:35 30 17:05 30 417.000 گار-اندیمشک1 0.000 0.000 26 16 1 100 دربند 171 16 17:31 0 17:31 440.000 گار-اندیمشک1 0.000 0.000 20 17 1 100 رودك 172 17 17:51 0 17:51 455.000 گار-اندیمشک1 0.000 0.000 20 18 1 100 دورود 173 18 18:11 120 20:11 120 467.000 گار-اندیمشک1 0.000 0.000 18 19 1 100 قارون 174 19 20:29 5 20:34 478.000 گار-اندیمشک1 0.000 0.000 22 20 1 100 بيشه 175 20 20:56 64 22:00 495.000 گار-اندیمشک1 0.000 0.000 22 21 1 100 سپيددشت 176 21 22:22 31 22:53 508.000 گار-اندیمشک1 0.000 0.000 28 22 1 100 چمسنگر 177 22 23:21 0 23:21 526.000 گار-اندیمشک1 0.000 0.000 25 23 1 100 كشور 178 23 23:46 50 00:36 541.000 گار-اندیمشک1 0.000 0.000 20 24 1 100 تنگ  هفت 179 24 00:56 83 02:19 60 554.000 گار-اندیمشک1 0.000 0.000 22 25 1 100 تنگ  پنج 180 25 02:41 29 03:10 570.000 گار-اندیمشک1 0.000 0.000 25 26 1 100 تله  زنگ 181 26 03:35 0 03:35 587.000 گار-اندیمشک1 0.000 0.000 30 27 1 100 شهبازان 182 27 04:05 0 04:05 601.000 گار-اندیمشک1 0.000 0.000 20 28 1 100 مازو 183 28 04:25 25 04:50 617.000 گار-اندیمشک1 0.000 0.000 26 29 1 100 بالارود 184 29 05:16 0 05:16 636.000 گار-اندیمشک1 0.000 0.000 18 30 1 100 گل  محك 185 30 05:34 5 05:39 649.000 گار-اندیمشک1 0.000 0.000 18 31 1 100 دوكوهه 186 31 05:57 18 06:15 662.000 گار-اندیمشک1 0.000 0.000 16 32 1 100 انديمشك 187 32 06:30 0 06:30 674.000 گار-اندیمشک1 1 1 101 انديمشك 187 1 01:00 0 01:00 674.000 گار-اندیمشک1 0.000 0.000 16 2 1 101 دوكوهه 186 2 01:16 0 01:16 662.000 گار-اندیمشک1 0.000 0.000 22 3 1 101 گل  محك 185 3 01:38 0 01:38 649.000 گار-اندیمشک1 0.000 0.000 22 4 1 101 بالارود 184 4 02:00 24 02:24 636.000 گار-اندیمشک1 0.000 0.000 26 5 1 101 مازو 183 5 02:50 32 03:22 617.000 گار-اندیمشک1 0.000 0.000 22 6 1 101 شهبازان 182 6 03:44 22 04:06 601.000 گار-اندیمشک1 0.000 0.000 25 7 1 101 تله  زنگ 181 7 04:31 0 04:31 587.000 گار-اندیمشک1 0.000 0.000 25 8 1 101 تنگ  پنج 180 8 04:56 36 05:32 570.000 گار-اندیمشک1 0.000 0.000 22 9 1 101 تنگ  هفت 179 9 05:54 45 06:39 45 554.000 گار-اندیمشک1 0.000 0.000 20 10 1 101 كشور 178 10 06:59 4 07:03 541.000 گار-اندیمشک1 0.000 0.000 25 11 1 101 چمسنگر 177 11 07:28 0 07:28 526.000 گار-اندیمشک1 0.000 0.000 28 12 1 101 سپيددشت 176 12 07:56 0 07:56 508.000 گار-اندیمشک1 0.000 0.000 22 13 1 101 بيشه 175 13 08:18 29 08:47 495.000 گار-اندیمشک1 0.000 0.000 22 14 1 101 قارون 174 14 09:09 0 09:09 478.000 گار-اندیمشک1 0.000 0.000 18 15 1 101 دورود 173 15 09:27 120 11:27 120 467.000 گار-اندیمشک1 0.000 0.000 20 16 1 101 رودك 172 16 11:47 0 11:47 455.000 گار-اندیمشک1 0.000 0.000 20 17 1 101 دربند 171 17 12:07 0 12:07 440.000 گار-اندیمشک1 0.000 0.000 26 --- ## ادامه 21 — پاسخ دستیار (`message-id:437721ba-e35a-4d40-87cf-a276e183aa0b`) بله؛ این نمونه خیلی ارزشمند است. با همین داده‌ها چند ابهام اصلی Mapping عملاً برطرف شد. مخصوصاً یک کشف مهم داریم: seir به احتمال بسیار بالا زمان سیر بین ایستگاه فعلی و ایستگاه بعدی است، نه یک کد یا Direction. 1. نتیجه تحلیل Semantic فیلدها بر اساس الگوی عددی و تطبیق دقیق با زمان‌ها: Field برداشت فعلی اطمینان توضیح ID شناسه رکورد منبع قطعی در هر TrainNo مجدداً از 1 شروع می‌شود kol یک فیلد منبع با معنای هنوز نامشخص نامشخص در نمونه همه 1 هستند TrainNo شماره حرکت قطار قطعی 100 رفت، 101 برگشت StationName نام ایستگاه قطعی StationNumber کد/شماره ایستگاه قطعی مثلاً 385 برای گار و 187 برای اندیمشک Sequence ترتیب ایستگاه در مسیر قطار قطعی از 1 به بعد time_in زمان ورود قطعی time_take مدت توقف واقعی بسیار محتمل time_out - time_in time_out زمان خروج قطعی RequiredWait حداقل زمان توقف/انتظار موردنیاز بسیار محتمل در برخی ایستگاه‌ها مقدار دارد Kilometerage کیلومتراژ/Chainage ایستگاه روی مسیر بسیار محتمل در رفت افزایش و در برگشت کاهش می‌یابد MaxSpeed حداکثر سرعت فعلاً نامشخص در نمونه مقدار ندارد TrainName نام سرویس/الگوی قطار قطعی گار-اندیمشک1 Distance فاصله Segment نامشخص در داده فعلی همه صفر sumDistancezz احتمالاً فاصله تجمعی تأیید نشده در داده فعلی همه صفر seir زمان سیر تا ایستگاه بعدی، دقیقه بسیار قوی با داده زمانی چندین بار دقیقاً تطبیق دارد 2. کشف بسیار مهم: seir این یکی تقریباً خودش را اثبات می‌کند. مثلاً: گار time_out = 09:00 seir = 34 ساقه time_in = 09:34 پس: 09:34−09:00=34 دقیقه دقیقاً برابر seir = 34. مثال بعد: ساقه time_out = 09:34 seir = 38 باغ یک time_in = 10:12 و: 10:12−09:34=38 باز هم دقیقاً seir = 38. چند نمونه دیگر: سواریان 10:34 → راه گرد 10:58 seir = 22 و: راه گرد 10:58 → نان گرد 11:20 seir = 22 و: نان گرد 11:20 → مشک آباد 11:45 seir = 25 همه دقیقاً منطبق هستند. بنابراین مدل Canonical باید این را داشته باشد: TrainStationCall ├── time_in ├── time_take ├── time_out ├── required_wait └── running_time_to_next و: running_time_to_next = seir × 60 seconds نکته مهم: seir متعلق به حرکت از ایستگاه فعلی به ایستگاه بعدی است، نه زمان رسیدن به ایستگاه فعلی. بنابراین در آخرین رکورد مسیر، طبیعی است که seir خالی باشد. 3. time_take نیز تقریباً روشن شد مثلاً در اراک: time_in = 12:27 time_take = 60 time_out = 13:27 پس: T dwell ​ =time_out−time_in=60 در دورود: 09:27 + 120 min = 11:27 در بیشه: 20:56 + 64 min = 22:00 پس فعلاً بهترین Semantic برای آن: time_take = Actual Dwell / Stop Duration است. در Canonical Model: actual_dwell_seconds = time_take × 60 و باید بتوانیم مستقل بررسی کنیم: time_out=time_in+time_take با درنظرگرفتن عبور از نیمه‌شب. 4. RequiredWait یک چیز متفاوت از time_take است این تفکیک برای Scheduling خیلی مهم است. مثلاً در تنگ هفت: time_in = 00:56 time_take = 83 time_out = 02:19 RequiredWait = 60 یعنی: Actual Dwell = 83 min Minimum Required Wait = 60 min پس نباید این دو را یکی کنیم. مدل پیشنهادی: TrainStationCall actual_dwell minimum_required_wait و Constraint: Dwell actual ​ ≥RequiredWait البته هنوز باید با داده‌های بیشتری مشخص کنیم که RequiredWait دقیقاً حداقل توقف عملیاتی است یا نوع خاصی از انتظار، اما از همین نمونه مشخص است که با time_take یکسان نیست. 5. Kilometerage خیلی جالب است در مسیر 100: گار 157 ساقه 200 باغ یک 220 سواریان 234 ... اندیمشک 674 در مسیر 101: اندیمشک 674 دوکوهه 662 گل محك 649 بالارود 636 ... بنابراین Kilometerage ظاهراً Chainage / کیلومتراژ موقعیت ایستگاه روی محور است. و این برای ما بسیار مهم است، چون می‌توانیم Segment Distance را از آن استخراج کنیم. مثلاً: Distance Gar,Sake ​ =200−157=43 km و: Distance Sake,BaghYak ​ =220−200=20 km و: Distance Arak,Samangan ​ =337−320=17 km در برگشت نیز: 674−662=12 km یعنی جهت قطار باعث منفی‌شدن فاصله نمی‌شود؛ ما فاصله را به صورت قدرمطلق اختلاف Chainage می‌گیریم: L ij ​ =∣K i ​ −K j ​ ∣ این دقیقاً برای ساخت RouteSegment بسیار مفید است. 6. یک نتیجه مهم درباره Distance در نمونه شما: Distance = 0.000 sumDistancezz = 0.000 برای تقریباً تمام رکوردها. بنابراین فعلاً نباید این دو فیلد را وارد محاسبات ظرفیت کنیم. اتفاقاً می‌توانیم یک Rule بسیار خوب برای Data Quality داشته باشیم: if Distance == 0 and abs(Kilometerage[i+1] - Kilometerage[i]) > 0: raise/flag: "Source Distance is not populated; derived segment distance available from Kilometerage" و به صورت Canonical: Source Distance ↓ Untrusted ↓ Derived Segment Distance ↓ abs(Kilometerage_next - Kilometerage_current) البته این Derived Distance باید به عنوان Derived Field علامت‌گذاری شود، نه اینکه وانمود کنیم همان Distance اصلی Access است. 7. Train 100 و 101 یک فرصت عالی برای تشخیص Direction هستند این قسمت خیلی مهم است. Train 100 گار → ساقه → باغ یک → ... → اندیمشک 385 → 157 → 158 → ... → 187 Train 101 اندیمشک → دوکوهه → گل محك → ... → ... 187 → 186 → 185 → ... یعنی TrainNo به‌تنهایی برای Direction کافی نیست، ولی Sequence و StationNumber به‌وضوح نشان می‌دهند که مسیر برگشت Reverse شده است. پس در Version 0.9 باید چیزی شبیه این داشته باشیم: TrainRun 100 direction = GAR_TO_ANDIMESHK path = [ Gar, Sakheh, BaghYek, ... Andimeshk ] TrainRun 101 direction = ANDIMESHK_TO_GAR path = [ Andimeshk, Dokuheh, ... Gar ] و این دقیقاً همان ایرادی را که قبلاً در نسخه MVP داشتیم برطرف می‌کند: RoutePath باید Directed باشد. 8. یک نکته حتی مهم‌تر درباره seir و Scheduling حالا می‌توانیم ساختار زمانی را بسیار تمیزتر کنیم. برای هر Segment: Station A │ │ time_out(A) │ │ seir(A) ▼ Station B │ │ time_in(B) و Constraint: T B in ​ =T A out ​ +T A,B run ​ که در داده Baseline: T A,B run ​ =seir A ​ مثلاً: T Sakeh in ​ =09:00+34=09:34 بعد در ایستگاه: T Sakeh out ​ =T Sakeh in ​ +Dwell Sakeh ​ و سپس Segment بعدی. بنابراین مسیر زمانی واقعی به شکل زیر می‌شود: Actual Dwell ↓ Arrival ───────────────→ Departure │ │ │ │ │ └── seir ──→ │ Arrival │ │ └─────────────────────────────────────────┘ این دقیقاً چیزی است که Scheduling Engine برای ساخت Time-Space Train Path نیاز دارد. 9. Mapping را الان اصلاح می‌کنیم نسخه قبلی را با این اطلاعات به‌روزرسانی می‌کنم: YAML TrainNo: target: train_run.train_no status: VERIFIED StationName: target: station.name status: VERIFIED StationNumber: target: station.source_number status: VERIFIED Sequence: target: train_station_call.sequence status: VERIFIED time_in: target: train_station_call.time_in status: VERIFIED time_take: target: train_station_call.actual_dwell unit: minute status: VERIFIED time_out: target: train_station_call.time_out status: VERIFIED RequiredWait: target: train_station_call.minimum_required_wait unit: minute status: PROVISIONAL Kilometerage: target: station.chainage unit: km status: VERIFIED MaxSpeed: target: train_station_call.max_speed unit: km/h status: PROVISIONAL TrainName: target: train_service.name status: VERIFIED Distance: target: source.distance status: UNTRUSTED_ZERO_IN_SAMPLE sumDistancezz: target: source.sum_distance_zz status: UNMAPPED seir: target: train_station_call.running_time_to_next unit: minute status: VERIFIED_BY_TIME_RECONCILIATION kol: target: source.kol status: UNMAPPED و یک Derived Mapping جدید: YAML derived_fields: segment_distance: formula: abs(next_station.chainage - current_station.chainage) unit: km source: - Kilometerage status: DERIVED running_time: formula: next.time_in - current.time_out unit: minute cross_check_with: - seir status: DERIVED_AND_VALIDATED 10. و مهم‌تر از همه: این داده حالا یک «Baseline Schedule» واقعی به ما می‌دهد برای Train 100 می‌توانیم از همین رکوردها تقریباً کل مسیر زمانی را بازسازی کنیم: Gar 09:00 ↓ 34 Sakheh 09:34 ↓ 38 Bagh Yek 10:12 ↓ 22 Sowarian 10:34 ↓ 24 Rah Gard 10:58 ↓ 22 Nan Gard 11:20 ↓ 25 ... Dorud 18:11 ├─ dwell 120 ↓ 20:11 ↓ 18 Qarun 20:29 ... Andimeshk 06:30 (+1 day) یعنی Midnight Rollover هم در داده واقعی وجود دارد؛ مثلاً: 23:46 → 00:36 00:56 → 02:19 بنابراین تست Midnight که در Version 0.8 داشتیم، دیگر یک تست مصنوعی نیست؛ واقعاً در Source Data وجود دارد. نتیجه به نظرم از اینجا به بعد، Mapping ما دیگر صرفاً یک حدس اولیه نیست. برای این بخش‌ها شواهد بسیار خوبی داریم: TrainNo, StationName, StationNumber, Sequence, time_in, time_take, time_out, Kilometerage, TrainName, seir و سه موضوع هنوز باید با نمونه‌های بیشتری بررسی شوند: kol, RequiredWait, Distance, sumDistancezz, MaxSpeed به‌خصوص RequiredWait را فعلاً به‌عنوان Minimum Required Dwell/Wait مدل می‌کنیم، ولی تا زمانی که چند نمونه دیگر ببینیم آن را PROVISIONAL نگه می‌داریم. قدم بعدی بسیار خوب این است که چند TrainNo دیگر، مخصوصاً یک قطار از یک مسیر متفاوت، همین‌طور متنی بفرستی. با آن می‌توانیم بفهمیم kol و RequiredWait و همچنین الگوی MaxSpeed دقیقاً چه هستند و سپس Mapping Version 0.9 را قفل کنیم و برویم سراغ پیاده‌سازی واقعی Scheduling Engine و CP-SAT. --- ## ادامه 22 — پرسش کاربر (`message-id:528bee90-efad-4570-8383-bb77821432ee`) 1 1 100 گار 385 1 09:00 0 09:00 157.000 گار-اندیمشک1 0.000 0.000 34 2 1 100 ساقه 157 2 09:34 0 09:34 200.000 گار-اندیمشک1 0.000 0.000 38 3 1 100 باغ  يك 158 3 10:12 0 10:12 220.000 گار-اندیمشک1 0.000 0.000 22 4 1 100 سواريان 159 4 10:34 0 10:34 234.000 گار-اندیمشک1 0.000 0.000 24 5 1 100 راه گرد 160 5 10:58 0 10:58 248.000 گار-اندیمشک1 0.000 0.000 22 6 1 100 نان گرد 161 6 11:20 0 11:20 265.000 گار-اندیمشک1 0.000 0.000 25 7 1 100 مشك اباد 162 7 11:45 0 11:45 286.000 گار-اندیمشک1 0.000 0.000 22 8 1 100 ملك اباد 163 8 12:07 0 12:07 304.000 گار-اندیمشک1 0.000 0.000 20 9 1 100 اراك 164 9 12:27 60 13:27 60 320.000 گار-اندیمشک1 0.000 0.000 30 10 1 100 سمنگان 165 10 13:57 0 13:57 337.000 گار-اندیمشک1 0.000 0.000 20 11 1 100 شازند 166 11 14:17 18 14:35 354.000 گار-اندیمشک1 0.000 0.000 32 12 1 100 نوراباد 167 12 15:07 0 15:07 373.000 گار-اندیمشک1 0.000 0.000 20 13 1 100 سميه 168 13 15:27 0 15:27 388.000 گار-اندیمشک1 0.000 0.000 16 14 1 100 مومن آباد 169 14 15:43 27 16:10 402.000 گار-اندیمشک1 0.000 0.000 25 15 1 100 ازنا 170 15 16:35 30 17:05 30 417.000 گار-اندیمشک1 0.000 0.000 26 16 1 100 دربند 171 16 17:31 0 17:31 440.000 گار-اندیمشک1 0.000 0.000 20 17 1 100 رودك 172 17 17:51 0 17:51 455.000 گار-اندیمشک1 0.000 0.000 20 18 1 100 دورود 173 18 18:11 120 20:11 120 467.000 گار-اندیمشک1 0.000 0.000 18 19 1 100 قارون 174 19 20:29 5 20:34 478.000 گار-اندیمشک1 0.000 0.000 22 20 1 100 بيشه 175 20 20:56 64 22:00 495.000 گار-اندیمشک1 0.000 0.000 22 21 1 100 سپيددشت 176 21 22:22 31 22:53 508.000 گار-اندیمشک1 0.000 0.000 28 22 1 100 چمسنگر 177 22 23:21 0 23:21 526.000 گار-اندیمشک1 0.000 0.000 25 23 1 100 كشور 178 23 23:46 50 00:36 541.000 گار-اندیمشک1 0.000 0.000 20 24 1 100 تنگ  هفت 179 24 00:56 83 02:19 60 554.000 گار-اندیمشک1 0.000 0.000 22 25 1 100 تنگ  پنج 180 25 02:41 29 03:10 570.000 گار-اندیمشک1 0.000 0.000 25 26 1 100 تله  زنگ 181 26 03:35 0 03:35 587.000 گار-اندیمشک1 0.000 0.000 30 27 1 100 شهبازان 182 27 04:05 0 04:05 601.000 گار-اندیمشک1 0.000 0.000 20 28 1 100 مازو 183 28 04:25 25 04:50 617.000 گار-اندیمشک1 0.000 0.000 26 29 1 100 بالارود 184 29 05:16 0 05:16 636.000 گار-اندیمشک1 0.000 0.000 18 30 1 100 گل  محك 185 30 05:34 5 05:39 649.000 گار-اندیمشک1 0.000 0.000 18 31 1 100 دوكوهه 186 31 05:57 18 06:15 662.000 گار-اندیمشک1 0.000 0.000 16 32 1 100 انديمشك 187 32 06:30 0 06:30 674.000 گار-اندیمشک1 1 1 101 انديمشك 187 1 01:00 0 01:00 674.000 گار-اندیمشک1 0.000 0.000 16 2 1 101 دوكوهه 186 2 01:16 0 01:16 662.000 گار-اندیمشک1 0.000 0.000 22 3 1 101 گل  محك 185 3 01:38 0 01:38 649.000 گار-اندیمشک1 0.000 0.000 22 4 1 101 بالارود 184 4 02:00 24 02:24 636.000 گار-اندیمشک1 0.000 0.000 26 5 1 101 مازو 183 5 02:50 32 03:22 617.000 گار-اندیمشک1 0.000 0.000 22 6 1 101 شهبازان 182 6 03:44 22 04:06 601.000 گار-اندیمشک1 0.000 0.000 25 7 1 101 تله  زنگ 181 7 04:31 0 04:31 587.000 گار-اندیمشک1 0.000 0.000 25 8 1 101 تنگ  پنج 180 8 04:56 36 05:32 570.000 گار-اندیمشک1 0.000 0.000 22 9 1 101 تنگ  هفت 179 9 05:54 45 06:39 45 554.000 گار-اندیمشک1 0.000 0.000 20 10 1 101 كشور 178 10 06:59 4 07:03 541.000 گار-اندیمشک1 0.000 0.000 25 11 1 101 چمسنگر 177 11 07:28 0 07:28 526.000 گار-اندیمشک1 0.000 0.000 28 12 1 101 سپيددشت 176 12 07:56 0 07:56 508.000 گار-اندیمشک1 0.000 0.000 22 13 1 101 بيشه 175 13 08:18 29 08:47 495.000 گار-اندیمشک1 0.000 0.000 22 14 1 101 قارون 174 14 09:09 0 09:09 478.000 گار-اندیمشک1 0.000 0.000 18 15 1 101 دورود 173 15 09:27 120 11:27 120 467.000 گار-اندیمشک1 0.000 0.000 20 16 1 101 رودك 172 16 11:47 0 11:47 455.000 گار-اندیمشک1 0.000 0.000 20 17 1 101 دربند 171 17 12:07 0 12:07 440.000 گار-اندیمشک1 0.000 0.000 26 --- ## ادامه 22 — پاسخ دستیار (`message-id:95a058fe-a156-4e4f-ab00-18508b223a57`) بله، این تکرار هم مفید است چون حالا می‌توانیم همین نمونه را به‌عنوان Reference Sample برای Access در نظر بگیریم. با بررسی دقیق‌تر، یک نکته مهم هم باید اصلاح شود: درباره Kilometerage و seir باید Mapping را کمی دقیق‌تر تعریف کنیم تا در پیاده‌سازی اشتباه مفهومی ایجاد نشود. 1. ساختار واقعی رکورد را الان می‌توانیم این‌طور تعریف کنیم هر رکورد Access در این نمونه یک Station Call برای یک Train Run است: TrainRun │ ├── TrainNo ├── TrainName │ └── Ordered Station Calls │ ├── Station ├── Sequence ├── time_in ├── time_take ├── time_out ├── RequiredWait ├── Kilometerage ├── MaxSpeed └── seir → Running Time to Next Station این ساختار برای موتور Scheduling ما بسیار مناسب است. 2. seir تقریباً با قطعیت Running Time است مثلاً: گار 09:00 seir = 34 ↓ ساقه 09:34 و: ساقه 09:34 seir = 38 ↓ باغ یک 10:12 بنابراین: T run,i ​ =time_in(i+1)−time_out(i) و در این Dataset: T run,i ​ =seir i ​ یعنی: seir را باید به Segment قبل از ایستگاه بعدی متصل کنیم، نه به Station خودش. مثلاً: گار ─────────────── ساقه seir = 34 min و نه: گار └── seir = 34 بدون مشخص‌کردن اینکه این زمان مربوط به کدام Segment است. در Canonical Model بهتر است اصلاً آن را این‌گونه ذخیره کنیم: RouteSegment from_station = گار to_station = ساقه baseline_run_time = 34 min و seir فقط Source Evidence آن باشد. 3. time_take هم با داده واقعی تأیید می‌شود مثلاً: اراک 12:27 arrival 13:27 departure time_take = 60 یا: دورود 18:11 arrival 20:11 departure time_take = 120 بنابراین: time_take=time_out−time_in البته با درنظرگرفتن rollover نیمه‌شب. پس در Canonical: actual_dwell_minutes = time_take و برای Solver: actual_dwell_seconds = time_take × 60 4. RequiredWait قطعاً نباید با time_take یکی شود مثلاً تنگ هفت: time_in = 00:56 time_take = 83 time_out = 02:19 RequiredWait = 60 یعنی: ActualDwell=83 ولی: RequiredWait=60 بنابراین: ActualDwell≥RequiredWait و این دقیقاً همان چیزی است که در Scheduling Engine به یک Constraint تبدیل می‌شود. اما هنوز یک نکته را باز می‌گذاریم: آیا RequiredWait همیشه Minimum Dwell است، یا در بعضی ایستگاه‌ها مفهوم عملیاتی خاص‌تری دارد؟ فعلاً: RequiredWait = PROVISIONAL: Minimum Operational Wait 5. Kilometerage را باید کمی متفاوت از چیزی که قبلاً گفتیم مدل کنیم یک نکته جالب در داده شما این است: گار StationNumber=385 Kilometerage=157 ساقه StationNumber=157 Kilometerage=200 باغ یک StationNumber=158 Kilometerage=220 ... پس: StationNumber اصلاً کیلومتراژ نیست و یک شناسه/کد ایستگاه است. اما: Kilometerage به‌نظر می‌رسد Chainage/Position روی محور باشد. مثلاً: L گار−ساقه ​ =∣200−157∣=43km و: L ساقه−باغ‌یک ​ =∣220−200∣=20km بنابراین می‌توانیم از آن برای Derived Segment Length استفاده کنیم. ولی فعلاً نباید مقدار Derived را جایگزین Distance منبع کنیم. 6. Distance فعلاً برای ما قابل استفاده نیست چون در تمام نمونه: Distance = 0.000 است. بنابراین در Version 0.9: Source Distance ↓ Do not trust و در صورت نیاز: Derived Distance = abs(Kilometerage[next] - Kilometerage[current]) را ایجاد می‌کنیم. این تفکیک خیلی مهم است: distance_source distance_derived نه اینکه مقدار Derived را در فیلد Source بنویسیم. 7. sumDistancezz نیز فعلاً Unknown باقی می‌ماند چون همه مقادیر نمونه: 0.000 هستند. پس فعلاً: sumDistancezz → Source/Unmapped و هیچ نقش محاسباتی در Capacity Engine ندارد. 8. یک تست بسیار مهم برای Data Quality ساختیم برای هر TrainRun می‌توانیم خودکار بررسی کنیم: تست شماره 1 — زمان سیر seir i ​ =time_in i+1 ​ −time_out i ​ تست شماره 2 — توقف time_take i ​ =time_out i ​ −time_in i ​ تست شماره 3 — Required Wait time_take i ​ ≥RequiredWait i ​ تست شماره 4 — ترتیب Sequence i+1 ​ =Sequence i ​ +1 تست شماره 5 — مسیر در Train 100: Kilometerage 157 → 200 → 220 → ... → 674 و Train 101: 674 → 662 → 649 → ... یعنی مسیر جهت‌دار باید مستقل ساخته شود. تست شماره 6 — Midnight Rollover مثلاً: 23:46 ↓ 00:36 باید به شکل: Day 0, 23:46 Day 1, 00:36 ذخیره شود. 9. مدل زمانی نهایی ما این داده باعث می‌شود مدل زمانی Version 0.9 خیلی تمیز شود: Station A │ │ time_out ▼ ┌─────────────────────┐ │ Running on Segment │ │ seir = T_run │ └─────────────────────┘ │ ▼ Station B │ │ time_in │ ├── actual dwell │ └── RequiredWait │ ▼ time_out │ ▼ Segment C یعنی: Arrival i+1 ​ =Departure i ​ +RunningTime i ​ و: Departure i ​ ≥Arrival i ​ +RequiredWait i ​ در Baseline موجود: Departure i ​ =Arrival i ​ +time_take i ​ 10. بنابراین Mapping فعلی من این است Access Canonical وضعیت ID source_record_id VERIFIED kol source.kol UNKNOWN TrainNo train_run.train_no VERIFIED StationName station.name VERIFIED StationNumber station.source_number VERIFIED Sequence station_call.sequence VERIFIED time_in station_call.arrival_time VERIFIED time_take station_call.actual_dwell VERIFIED time_out station_call.departure_time VERIFIED RequiredWait station_call.required_wait PROVISIONAL Kilometerage station.chainage HIGH CONFIDENCE MaxSpeed station_call.max_speed UNKNOWN TrainName train_service.name VERIFIED Distance source.distance UNTRUSTED sumDistancezz source.sum_distance_zz UNKNOWN seir segment.baseline_running_time VERIFIED BY RECONCILIATION 11. یک تغییر مهم در Architecture Version 0.9 قبلاً می‌گفتیم: Access ↓ TrainStationCall حالا بهتر است بگوییم: Access Record │ ├──────── TrainRun │ ├──────── Station │ └──────── StationCall │ └── Segment Evidence │ ├── seir ├── Kilometerage └── Derived Distance و بعد: StationCall[] ↓ Directed Train Path ↓ RouteSegment[] ↓ Block / Station Resources ↓ Scheduling این معماری از نظر من خیلی بهتر و دقیق‌تر است، چون seir را مستقیماً به یک Station Attribute تبدیل نمی‌کنیم؛ آن را به زمان سیر بین دو StationCall تبدیل می‌کنیم. 12. و یک نکته برای کدنویسی در Adapter نباید همین حالا داده را بیش از حد Semantic کنیم. بهتر است ابتدا با همان Source Structure بخوانیم؛ مثلاً در Excel/Access داده خام را با نوع object نگه داریم و بعد در Mapping/Normalization تبدیل کنیم. این با رویکرد مستندات فعلی pandas نیز سازگار است؛ dtype=object برای حفظ مقدار خام و جلوگیری از inference زودهنگام قابل استفاده است. pandas.pydata.org +1 یعنی: Raw ↓ Normalize ↓ Semantic Mapping ↓ Canonical ↓ Validation ↓ Scheduling نه: Raw ↓ حدس Semantic ↓ Solver نتیجه مهم با همین نمونه، اولین Vertical Slice واقعی ما تقریباً شکل گرفته است: Access ↓ TrainNo / Station / Sequence ↓ TrainRun ↓ Ordered StationCalls ↓ time_in / time_out ↓ seir → Segment Running Time ↓ Kilometerage → Chainage ↓ Derived Segment Distance ↓ Directed TrainPath ↓ Baseline Schedule ↓ Conflict Detection ↓ CP-SAT Generated Schedule ↓ Capacity Proof و این بار دیگر Vertical Slice روی یک Fixture مصنوعی نیست؛ منطق آن مستقیماً از همین داده واقعی 100/101 استخراج شده است. --- ## ادامه 23 — پرسش کاربر (`message-id:e41cc81b-5367-4269-960b-f5a2cdd1958f`) بله --- ## ادامه 23 — پاسخ دستیار (`message-id:a568dc05-b231-4f4a-b8b3-bd474f8a399b`) عالی. پس مرحله بعد را به‌صورت رسمی می‌بریم روی Version 0.9 — Real Scheduling Engine + CP-SAT + Station/Junction Resources. در این نسخه، داده‌ی نمونه‌ی TrainNo=100/101 را به‌عنوان Reference Operational Dataset مبنا می‌گیریم و این زنجیره را پیاده می‌کنیم: Access Source ↓ Raw Records ↓ Semantic Mapping ↓ Canonical TrainRun / StationCall ↓ Directed Route Path ↓ Route Segments + Running Times ↓ Station / Block / Junction Resources ↓ Baseline Schedule ↓ Conflict Detection ↓ CP-SAT Scheduling ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof یک نکته مهم: kol, RequiredWait, MaxSpeed, Distance و sumDistancezz را تا زمان تأیید Semantic همچنان با وضعیت مناسب نگه می‌داریم؛ اما seir را با time reconciliation به baseline_running_time_to_next متصل می‌کنیم. برای Version 0.9، خروجی فقط یک عدد ظرفیت نخواهد بود؛ برای مثال باید بتواند چنین Proof تولید کند: Route: Gar → Andimeshk Train Type: Freight Direction: Forward F = 76 trains/day FEASIBLE ✓ F = 77 trains/day INFEASIBLE ✕ Capacity = 76 Binding Constraints: - Single-track conflict - Station crossing capacity - Headway - Operational window Evidence: - Generated timetable - Block occupancy - Station track assignment - Conflict list - Independent validation و مهم‌تر از همه: اگر Solver نتواند Schedule معتبر برای F بسازد، ظرفیت F را قبول نمی‌کنیم. از اینجا به بعد، مبنای توسعه را روی داده واقعی Train 100/101 و همین Mapping می‌گذاریم، نه Fixture مصنوعی قبلی. --- ## ادامه 24 — پرسش کاربر (`message-id:ff0af0bd-a967-40aa-b754-9b0583e943d1`) بله لطفا --- ## ادامه 24 — پاسخ دستیار (`message-id:304ebe9e-343e-4ac2-bacc-7b90e31267c8`) حتماً. در ادامه Version 0.9 را به‌صورت یک Specification/Implementation Package قابل تبدیل مستقیم به کد ارائه می‌کنم. این نسخه دقیقاً روی داده واقعی TrainNo=100/101 بنا می‌شود و شکاف اصلی نسخه 0.8 یعنی عبور از Baseline Data به Feasible Generated Schedule را می‌بندد. Railway Capacity Engine — Version 0.9: Real Scheduling, CP-SAT & Station/Junction Resources Railway Capacity Engine — Version 0.9 Real Scheduling + CP-SAT + Station/Junction Resources Status: Implementation Specification Base Version: 0.8 — Real Data Adapter & First Vertical Slice Target: v0.9 Domain: Railway Freight Capacity Planning & Optimization Solver: OR-Tools CP-SAT Canonical Model: OD-Centric Railway Capacity Domain Model with Detailed Time-Space Train Movement 1. هدف Version 0.9 Version 0.8 توانست زنجیره زیر را ایجاد کند: Excel / Access ↓ Source Adapter ↓ Raw Records ↓ Mapping ↓ Canonical Model ↓ TrainRun ↓ Directed Route ↓ TrainStationCall ↓ Baseline Schedule ↓ Conflict Detection Version 0.9 باید یک گام اساسی جلوتر برود: Real Source Data ↓ Canonical Model ↓ Directed Infrastructure ↓ Train Path ↓ Station / Block / Junction Resources ↓ Operational Rules ↓ CP-SAT ↓ Generated Feasible Schedule ↓ Independent Validation ↓ Baseline vs Generated ↓ Capacity Search ↓ Capacity Proof ↓ Binding Constraints ↓ Explanation اصل کلیدی: Capacity فقط زمانی معتبر است که برای آن یک Schedule معتبر و مستقل از Solver قابل تأیید وجود داشته باشد. بنابراین: [ C_r=\max{F\mid Schedule(F)\ is\ feasible} ] و برای یک مقدار پیشنهادی (F): [ F\ feasible \iff \exists S: Validate(S)=TRUE ] 2. Reference Operational Dataset داده واقعی ارائه‌شده برای Version 0.9 به‌عنوان اولین Reference Dataset استفاده می‌شود. دو حرکت واقعی: TrainNo = 100 گار → اندیمشک TrainNo = 101 اندیمشک → گار ساختار حرکت: TrainRun └── TrainStationCall[1..N] ├── Station ├── Sequence ├── time_in ├── time_take ├── time_out ├── RequiredWait ├── Kilometerage └── seir → running_time_to_next در نمونه واقعی: time_out(i) + seir(i) = time_in(i+1) برای نمونه: Gar 09:00 + 34 min ↓ Sakheh 09:34 + 38 min ↓ Bagh Yek 10:12 این رابطه در Version 0.9 به‌عنوان Baseline Movement Evidence استفاده می‌شود. 3. اصلاح مهم مدل مسیر در Version 0.8 مسیر به‌صورت ساده از فهرست ایستگاه‌ها استخراج می‌شد. در Version 0.9 باید مفهوم زیر به‌صورت صریح ایجاد شود: Directed Route Path Physical Infrastructure ↓ Directed Route Path ↓ Ordered Route Resources یک خط فیزیکی: Gar -------- Andimeshk دو مسیر عملیاتی: RoutePath A: Gar → ... → Andimeshk RoutePath B: Andimeshk → ... → Gar بنابراین ترتیب ایستگاه‌ها نباید از روی نام Route حدس زده شود. class DirectedRoutePath: route_id: str direction: Direction station_ids: list[str] segment_ids: list[str] block_ids: list[str] برای Train 100: direction = FORWARD station_ids = [Gar, Sakheh, ..., Andimeshk] برای Train 101: direction = REVERSE station_ids = [Andimeshk, ..., Sakheh, Gar] 4. Canonical Infrastructure Model Version 0.9 این منابع را به‌عنوان Resource قابل زمان‌بندی مدل می‌کند: Block Station StationTrack Junction Terminal SingleTrackSection DoubleTrackSection Route OperationalWindow 5. Block Model class Block: id: str from_station_id: str to_station_id: str directionality: str length_km: float single_track: bool baseline_running_time_min: int clearing_time_min: int headway_min: int دو نوع زمان باید تفکیک شوند: Running Time زمان حرکت قطار: [ T_{run} ] Blocking / Occupancy Time مدت زمانی که Resource عملاً در اختیار قطار است: T_{run} + T_{entry} + T_{clear} ] بنابراین: [ T_{run}\neq T_{occ} ] این تفکیک برای ظرفیت بسیار مهم است. 6. Block Occupancy برای هر TrainRun و Block: Entry ↓ Running ↓ Exit ↓ Clear متغیرهای CP-SAT: entry_time exit_time clear_time و: [ exit \ge entry + T_{run} ] [ clear \ge exit + T_{clear} ] Interval: occupancy = model.NewIntervalVar( entry, occupancy_duration, clear, name ) 7. Station Resource Model Station نباید فقط یک Point در Route باشد. در Version 0.9: Station ├── StationTrack ├── Arrival Resource ├── Departure Resource ├── Crossing Resource ├── Formation Resource ├── Loading Resource └── Unloading Resource حداقل MVP: class StationTrack: id: str station_id: str usable_length_m: float direction: Direction | None supports_crossing: bool supports_overtaking: bool 8. Station Track Assignment اگر طول قطار: [ L_{train} ] و طول خط ایستگاه: [ L_{track} ] باشد: [ L_{train}\le L_{track} ] شرط باید قبل از زمان‌بندی بررسی شود. اما در Version 0.9 انتخاب Track نیز decision variable می‌شود. برای هر Train: Track A Track B Track C و: track_choice[i, k] ∈ {0,1} با: [ \sum_k trackChoice_{i,k}=1 ] 9. Station Occupancy برای هر قطار: Arrival ↓ Station Occupancy ↓ Departure حداقل: [ departure_i \ge arrival_i + dwell_i ] و: [ dwell_i\ge RequiredWait_i ] در صورت وجود Operational Window: [ arrival_i\in W ] یا: [ departure_i\in W ] بسته به نوع Window. 10. Crossing Station در Single Track، دو قطار مخالف نمی‌توانند همزمان Block را اشغال کنند. اما این الزام نباید به معنی «ممنوعیت عبور قطار» باشد. راه‌حل: Train A → Block ↓ Crossing Station ↓ Train B ← Block یکی از قطارها باید وارد Crossing Station شود و منتظر بماند. این یعنی: Single Track + Crossing Station = Scheduling Decision نه یک ظرفیت ثابت. 11. Opposing Train Constraint اگر Train A و Train B روی یک Single Track Block باشند: A: entry_A → exit_A B: entry_B → exit_B باید یکی از این دو برقرار باشد: [ clear_A\le entry_B ] یا: [ clear_B\le entry_A ] در CP-SAT: order = model.NewBoolVar("A_before_B") model.Add(clear_a <= entry_b).OnlyEnforceIf(order) model.Add(clear_b <= entry_a).OnlyEnforceIf(order.Not()) اما Version 0.9 این constraint را فقط برای Resourceهایی اعمال می‌کند که واقعاً Single Track / mutually exclusive هستند. 12. Same Direction Headway دو قطار هم‌جهت روی یک Block لزوماً نباید کل Occupancy را پشت سر هم بدون فاصله استفاده کنند. Constraint عمومی: [ entry_j \ge clear_i + H_{i,j} ] که: f(Block,TrainType_i,TrainType_j,Direction) ] است. بنابراین: Headway دیگر یک مقدار global نیست. 13. Switch Time در یک Single Track / Crossing Regime: Train A clears section ↓ Resource Release ↓ Route/Switch preparation ↓ Train B enters بنابراین: [ entry_B \ge clear_A+T_{switch} ] و: [ T_{switch} ] باید Resource-specific باشد. class ResourceTimingRule: resource_id: str headway_min: int switch_time_min: int clearing_time_min: int 14. Junction Resource Junction یکی از منابع مهم ظرفیت شبکه است. مثلاً: Route A ───┐ ├── Junction J1 ─── Main Line Route B ───┘ دو قطار ممکن است از Blockهای متفاوت حرکت کنند ولی در Junction conflict داشته باشند. بنابراین: Block Conflict کافی نیست. باید: Junction Conflict نیز مدل شود. برای هر Movement: class JunctionMovement: train_run_id: str junction_id: str entry: int exit: int و برای Movementهای ناسازگار: [ exit_i\le entry_j ] یا برعکس. 15. Conflict Matrix برای هر Resource: Resource Conflict Matrix تعریف می‌شود. مثلاً: Movement A Movement B Conflict Main→Branch Main→Main Yes Branch→Main Main→Branch Yes Main→Main Main→Main No Opposite Single Track Opposite Single Track Yes این Matrix باید data-driven باشد، نه hard-coded در Solver. 16. Operational Window پنجره‌های عملیاتی شامل مواردی مانند: Brake Test Fueling Maintenance Prayer / operational availability Terminal availability Loading Unloading Station closure Temporary restriction است. مدل عمومی: class OperationalWindow: resource_id: str start: int end: int window_type: str hard: bool مثلاً: Station S1 22:00–23:00 Closed در این حالت: [ departure\notin[22:00,23:00] ] 17. Generated Schedule خروجی Solver باید شامل موارد زیر باشد: class GeneratedTrainSchedule: train_run_id: str station_events: list[StationEvent] block_movements: list[BlockMovement] assigned_tracks: dict[str, str] objective_value: float solver_status: str برای هر Station: Arrival Dwell Departure Track برای هر Block: Entry Exit Clear 18. Baseline Schedule Baseline Schedule حذف نمی‌شود. دو Schedule داریم: Baseline Schedule Generated Feasible Schedule و سپس: Baseline vs Generated مقایسه می‌شود. 19. Baseline Diff برای هر TrainRun: Baseline Departure Generated Departure Δ Departure Baseline Arrival Generated Arrival Δ Arrival Baseline Block Occupancy Generated Block Occupancy Δ Occupancy مثلاً: Train 100 Gar Departure Baseline: 09:00 Generated: 09:00 Δ: 0 Andimeshk Arrival Baseline: 21:34 Generated: 21:48 Δ: +14 min این اختلاف باید توضیح داده شود: Reason: Opposing Train 101 + Single Track Conflict + Switch Time 20. CP-SAT Model Solver باید Resource-based باشد. ساختار: Model ├── Train variables ├── Station variables ├── Block variables ├── Track assignment variables ├── Crossing variables ├── Junction variables ├── Operational window constraints ├── Headway constraints ├── Switch constraints └── Objective 21. Variables برای هر Train (i) و Station (s): [ A_{i,s} ] Arrival [ D_{i,s} ] Departure برای هر Block (b): [ E_{i,b} ] Entry [ X_{i,b} ] Exit [ C_{i,b} ] Clear برای Track: [ z_{i,s,k}\in{0,1} ] برای Conflict Ordering: [ o_{i,j,r}\in{0,1} ] 22. Route Precedence برای هر قطعه: [ D_{i,s} + T_{run,i,b} \le A_{i,s+1} ] و: [ A_{i,s} + Dwell_{i,s} \le D_{i,s} ] بنابراین: Arrival ↓ Dwell ↓ Departure ↓ Running ↓ Next Arrival 23. Actual Baseline Running Time برای Reference Dataset: seir به‌عنوان Evidence استفاده می‌شود. Canonical field: baseline_running_time_to_next مثلاً: Current Station: Gar Next Station: Sakheh seir: 34 پس: [ T_{run}(Gar,Sakheh)=34 ] در Version 0.9 مقدار seir از داده واقعی مستقیماً قابل استفاده است، مشروط به عبور از Quality Gate. 24. Midnight Normalization زمان‌ها باید به Absolute Minute تبدیل شوند. مثلاً: 23:46 + 50 min = 00:36 next day بنابراین: normalize_time_sequence(...) باید Day Offset تولید کند. مثلاً: 23:46 → 1426 00:36 → 1476 نه اینکه: 00:36 < 23:46 تفسیر شود. 25. Independent Validator Solver نباید خودش داور خودش باشد. پس: CP-SAT ↓ Generated Schedule ↓ Independent Validator Validator باید بدون اعتماد به Solver بررسی کند: Structural All movements present Correct direction Correct station order Temporal Arrival < Departure Running time satisfied Dwell satisfied Infrastructure Block conflict Station conflict Junction conflict Track length Operational Operational Window Switch time Headway Crossing Fleet در صورت فعال بودن Fleet constraints: Wagon availability Locomotive availability 26. Validation Result class ValidationResult: feasible: bool violations: list[ConstraintViolation] resource_conflicts: list[Conflict] binding_constraints: list[BindingConstraint] utilization: list[ResourceUsage] هر Violation باید Evidence داشته باشد. مثلاً: Constraint: SINGLE_TRACK_HEADWAY Train A: 100 Train B: 101 Resource: Block B17 Required: 20 min Actual: 12 min Violation: 8 min 27. Capacity Search Capacity Search باید روی همان Canonical Problem واقعی اجرا شود. اشتباه Versionهای اولیه: Capacity Search ↓ Synthetic Problem در Version 0.9: Canonical Problem ↓ Build F trains ↓ Build Schedule Problem ↓ CP-SAT ↓ Independent Validation ↓ Feasible / Infeasible 28. Capacity Search Algorithm فرض کنید: [ F_{low}=1 ] و: [ F_{high}=F_{candidate} ] Search: while low <= high: mid = (low + high) // 2 result = solve_for_frequency( canonical_problem, frequency=mid ) if result.validated_feasible: best = result low = mid + 1 else: high = mid - 1 اما نتیجه فقط زمانی Accepted است که: Solver = FEASIBLE AND Independent Validator = VALID 29. Capacity Proof مثلاً: F = 76 FEASIBLE ✓ F = 77 INFEASIBLE ✕ بنابراین: [ C_r=76 ] اما Proof باید شامل دو Schedule/Run باشد: Proof ├── Feasible Witness @ 76 └── Infeasible Witness @ 77 30. Feasible Witness باید شامل: Train List Station Times Block Times Track Assignments Crossings Junction Usage Operational Windows Validation Result باشد. 31. Infeasible Witness برای F+1 باید مشخص شود Solver/Validation کجا شکست خورده است. مثلاً: Infeasible @ 77 Primary binding resource: Block B17 Demand: 77 Maximum schedulable: 76 Reason: Additional train cannot be inserted without violating opposing-direction headway. این توضیح باید از مدل استخراج شود، نه متن ثابت. 32. Binding Constraint Constraint binding زمانی مهم است که واقعاً ظرفیت را محدود کرده باشد. ساختار: class BindingConstraint: constraint_id: str resource_id: str train_run_ids: list[str] slack_min: int utilization_ratio: float marginal_capacity_impact: float 33. Slack برای هر Constraint: [ Slack=Allowed-Used ] مثلاً: Headway required = 20 min Actual = 20 min Slack = 0 این Resource کاندید Binding Constraint است. 34. Bottleneck Detection Bottleneck فقط: highest utilization نیست. باید سه شاخص ترکیب شود: Utilization Slack Marginal Capacity Impact مثلاً: C_{after}-C_{before} ] اگر با افزایش ظرفیت یک Block از: 76 → 82 ظرفیت Route از: 76 → 80 برسد، اثر آن: [ \Delta C=4 ] است. 35. Station Bottleneck ممکن است Route Blockها ظرفیت کافی داشته باشند اما Station ظرفیت محدود کند: Block capacity = 90 Station capacity = 76 Route capacity = 76 در این حالت: Binding Resource: Station S12 و نه Block. 36. Junction Bottleneck همین منطق برای Junction: Blocks: adequate Station: adequate Junction J1: binding ظرفیت Route تابع shared junction resource خواهد بود. 37. Double Track در Double Track: Direction A → Track 1 Direction B → Track 2 در حالت پایه، قطارهای مخالف الزاماً conflict ندارند. اما موارد زیر همچنان ممکن است conflict ایجاد کنند: Station Junction Crossover Terminal Single-track transition Shared signal/resource بنابراین: Double Track به معنی «عدم وجود Conflict» نیست. 38. Single/Double Transition یکی از مهم‌ترین موارد Version 0.9: Double Track ↓ Single Track ↓ Double Track در محل Transition: Transition Resource باید ایجاد شود. ظرفیت کل Route ممکن است توسط کوتاه‌ترین بخش از نظر زمان آزاد محدود شود، اما این مقدار باید با Schedule اثبات شود. 39. Operational Regime برای Single Track می‌توان چند Regime تعریف کرد: Alternating Batch Direction Scheduled Crossing Dynamic Crossing در Version 0.9 حداقل: Alternating Scheduled Crossing پشتیبانی شود. Regime بخشی از Scenario است. 40. Batch Support Batch همچنان Decision Variable/Planning Object است. class OperationalBatch: id: str route_id: str direction: Direction start_time: int end_time: int train_run_ids: list[str] اما: Batch size نباید ثابت فرض شود. Solver باید بتواند: K = 1 K = 2 K = 3 ... را بررسی کند. 41. Batch Duration تقریب: [ T_k=N_kH_k+T_{first,k} ] اما در Engine بهتر است: T_{entry} + \sum T_{train} + T_{clear} ] محاسبه شود. 42. Objective Version 0.9 Objective پایه: Primary Maximize number of successfully scheduled train movements یا: [ \max \sum_i x_i ] Secondary در صورت برابر بودن: Minimize total delay [ \min \sum_i Delay_i ] Tertiary Minimize resource conflicts / operational deviations در نتیجه Objective Lexicographic خواهد بود. 43. Fixed Baseline Mode دو حالت Solver: Mode A: Baseline Preservation Mode B: Capacity Optimization Baseline Preservation تا حد ممکن: [ GeneratedTime\approx BaselineTime ] Capacity Optimization اولویت: Feasibility Capacity Delay Operational Efficiency 44. Schedule Deviation برای هر Event: [ Deviation=|Generated-Baseline| ] یا در Objective: [ \min\sum_i |D_i-D_i^{baseline}| ] این باعث می‌شود Solver بی‌دلیل Schedule واقعی را جابه‌جا نکند. 45. Project Structure v0.9 ساختار پیشنهادی: rail_capacity/ │ ├── app/ │ ├── domain/ │ │ ├── station.py │ │ ├── station_track.py │ │ ├── block.py │ │ ├── junction.py │ │ ├── route.py │ │ ├── train.py │ │ ├── train_run.py │ │ ├── schedule.py │ │ ├── resource.py │ │ ├── operational_window.py │ │ ├── conflict.py │ │ └── capacity.py │ │ │ ├── adapters/ │ │ ├── excel.py │ │ └── access.py │ │ │ ├── staging/ │ │ └── models.py │ │ │ ├── mapping/ │ │ ├── registry.py │ │ └── rules.py │ │ │ ├── quality/ │ │ ├── rules.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ └── identity.py │ │ │ ├── topology/ │ │ ├── builder.py │ │ ├── directed_path.py │ │ └── resources.py │ │ │ ├── scheduling/ │ │ ├── baseline.py │ │ ├── train_path.py │ │ ├── model.py │ │ ├── constraints.py │ │ └── objective.py │ │ │ ├── solver/ │ │ └── cp_sat.py │ │ │ ├── conflicts/ │ │ ├── block.py │ │ ├── station.py │ │ ├── junction.py │ │ └── engine.py │ │ │ ├── validation/ │ │ ├── schedule.py │ │ └── capacity.py │ │ │ ├── capacity/ │ │ ├── search.py │ │ ├── proof.py │ │ └── bottleneck.py │ │ │ ├── explanation/ │ │ └── engine.py │ │ │ └── pipeline/ │ └── vertical_slice.py │ ├── mappings/ │ ├── fixtures/ │ ├── synthetic/ │ └── real_sample/ │ ├── tests/ │ ├── test_direction.py │ ├── test_midnight.py │ ├── test_baseline.py │ ├── test_station_track.py │ ├── test_single_track.py │ ├── test_double_track.py │ ├── test_junction.py │ ├── test_headway.py │ ├── test_switch_time.py │ ├── test_operational_window.py │ ├── test_solver.py │ ├── test_validation.py │ ├── test_capacity.py │ ├── test_capacity_proof.py │ └── test_baseline_diff.py │ ├── pyproject.toml └── README.md 46. CP-SAT Resource Modeling برای Resourceهای mutually exclusive: model.AddNoOverlap(intervals) برای منابع با ظرفیت چندواحدی: model.AddCumulative( intervals, demands, capacity ) مثلاً: Station has 3 usable tracks می‌تواند به‌صورت ظرفیت Resource مدل شود. اما اگر Track Assignment اهمیت داشته باشد، Trackهای مستقل بهتر است جداگانه مدل شوند. 47. Interval Strategy برای هر Train/Block: Optional Interval وقتی: Train uses Block فعال می‌شود. برای Track: Optional Station Track Interval برای Junction: Optional Junction Movement Interval این ساختار از Pairwise Booleanهای بیش از حد جلوگیری می‌کند. 48. Solver Determinism برای reproducibility: solver.parameters.num_search_workers = 1 solver.parameters.random_seed = 1 همچنین: Model Version Solver Version Solver Parameters Scenario ID Data Version باید در Result ذخیره شوند. 49. Solver Status نتایج: OPTIMAL FEASIBLE INFEASIBLE UNKNOWN MODEL_INVALID نباید: UNKNOWN به‌عنوان Capacity معتبر تفسیر شود. 50. Capacity Acceptance Rule نتیجه فقط زمانی Accepted: Solver Status ∈ {OPTIMAL, FEASIBLE} AND Independent Validation = PASS AND Data Quality Gate = PASS AND Required mappings = VERIFIED در غیر این صورت: Capacity Status = REJECTED / UNVERIFIED 51. Explanation Engine هر Capacity Result باید پاسخ دهد: Why this capacity? Why not one more train? Which resource binds? Which trains conflict? How much slack exists? What changes would increase capacity? مثلاً: Route Capacity = 76 The 77th movement cannot be inserted because: 1. Block B17 is single-track. 2. Opposing movement 101 occupies the conflicting interval. 3. Required switch/headway time is 20 minutes. 4. Available gap is 12 minutes. 5. No alternative crossing station is available in the required time window. 52. Capacity Waterfall Data خروجی: Infrastructure Potential ↓ Operationally Realizable ↓ Route Scheduling ↓ Station Constraints ↓ Junction Constraints ↓ Fleet Constraints ↓ Demand ↓ Allocated Capacity مثلاً: Physical Potential 110 Operational Potential 94 Route Feasible 82 Station Limited 78 Junction Limited 76 Demand 70 Allocated 68 این اعداد صرفاً نمونه ساختاری هستند و نباید بدون محاسبه واقعی نمایش داده شوند. 53. Resource Utilization برای هر Resource: [ Utilization= \frac{OccupiedTime}{AvailableTime} ] اما برای Single Track بهتر است علاوه بر Utilization: Critical Gap Minimum Slack Conflict Count Marginal Capacity Impact نیز ذخیره شود. 54. Hidden Capacity اگر: Resource Utilization = 65% ولی ظرفیت Route فقط 76 باشد، Engine نباید نتیجه بگیرد که 35% ظرفیت آزاد است. باید بررسی شود: Temporal Fragmentation Directional Imbalance Crossing Constraints Station Availability Junction Conflicts Fleet Cycles Demand Shape بنابراین: [ UnusedCapacity\neq HiddenCapacity ] 55. Version 0.9 Result Package Run Scenario Data Version Model Version Solver Version Input Summary Train Runs Directed Paths Station Assignments Block Assignments Junction Movements Generated Schedule Baseline Schedule Baseline Diff Validation Conflicts Resource Usage Binding Constraints Capacity Capacity Proof Bottlenecks Sensitivity Explanation 56. API Contract پیشنهادی Generate Schedule POST /scheduling/generate Input: { "scenario_id": "SC-001", "data_version": "DATA-001", "train_run_ids": ["100", "101"], "mode": "CAPACITY_OPTIMIZATION" } Output: { "run_id": "RUN-001", "status": "FEASIBLE", "schedule_id": "SCH-001", "validation_status": "PASS" } 57. Capacity API POST /capacity/search Input: { "route_id": "R-GAR-AND", "direction_mode": "BOTH", "start_frequency": 1, "max_frequency": 100 } Output: { "capacity": 76, "proof": { "feasible_at": 76, "infeasible_at": 77 }, "status": "VALIDATED" } 58. Capacity Proof API GET /capacity/{run_id}/proof خروجی: { "capacity": 76, "feasible_witness": "...", "infeasible_witness": "...", "binding_constraints": [], "evidence": [] } 59. Required Tests Direction 100: Gar → Andimeshk 101: Andimeshk → Gar Expected: Directed path reversed correctly Midnight Input: 23:46 duration 50 Expected: 00:36 next day Running Time time_out + seir = next time_in Expected: PASS Station Dwell time_take >= RequiredWait Expected: PASS Single Track Opposing Two trains: A → B B → A Expected: No overlap Crossing Expected: One train waits at crossing station Double Track Opposing trains: Track 1 Track 2 Expected: No false conflict Station Track Train: length = 700m Track: usable = 600m Expected: INFEASIBLE Headway Required: 20 min Actual: 15 min Expected: INFEASIBLE Switch Time clear = 100 switch = 10 next entry = 105 Expected: INFEASIBLE Operational Window Closed: 22:00–23:00 Train departure: 22:30 Expected: INFEASIBLE Capacity Proof Expected: F=76 → PASS F=77 → FAIL و سپس: Cr=76 فقط در صورت اثبات واقعی. 60. Baseline vs Generated Test اگر Baseline: Train 100: 09:00 → ... و Generated: 09:05 → ... باید: delta = +5 ثبت شود. 61. Determinism Test دو بار اجرای یک Run با: same Data Version same Model Version same Scenario same Solver parameters باید نتیجه قابل بازتولید بدهد. حداقل: Feasibility Capacity Binding Constraints باید deterministic باشند. 62. Data Quality Gate برای Production: VERIFIED TrainNo StationName StationNumber Sequence time_in time_take time_out TrainName seir PROVISIONAL RequiredWait Kilometerage semantics MaxSpeed UNTRUSTED / UNKNOWN Distance sumDistancezz kol قاعده: Fieldی که هنوز semantic mapping آن تأیید نشده است، نباید بدون flag وارد محاسبات حساس Capacity شود. 63. مهم‌ترین اصلاح نسبت به Version 0.8 Version 0.9 نباید این کار را انجام دهد: Access ↓ Schedule ↓ Conflict ↓ Capacity بلکه: Access ↓ Canonical ↓ Infrastructure Reconstruction ↓ Directed Train Path ↓ Resource Model ↓ Scheduling Model ↓ CP-SAT ↓ Independent Validation ↓ Capacity یعنی Solver باید روی مدل واقعی زیرساخت کار کند، نه روی یک Route ساده‌شده. 64. Vertical Slice واقعی Version 0.9 اولین End-to-End Test: Access Train 100 + Access Train 101 ↓ Identity/Reconciliation ↓ TrainRun 100/101 ↓ Directed Route ↓ TrainStationCall ↓ Block Reconstruction ↓ Baseline Running Time ↓ Station Resources ↓ Single Track Resources ↓ Crossing Logic ↓ CP-SAT ↓ Generated Schedule ↓ Independent Validation ↓ Baseline Diff ↓ Capacity Search ↓ Capacity Proof 65. Acceptance Criteria Version 0.9 Version 0.9 زمانی Accepted است که: Data Access واقعی ingest شود. Train 100 و 101 correctly mapped شوند. Midnight rollover صحیح باشد. seir با Time Evidence reconcile شود. Topology مسیر Forward ساخته شود. مسیر Reverse ساخته شود. Blockها directed باشند. Single/Double Track قابل مدل‌سازی باشد. Scheduling Station Track assignment فعال باشد. Station occupancy فعال باشد. Single-track crossing فعال باشد. Headway فعال باشد. Switch time فعال باشد. Junction resource فعال باشد. Operational Window فعال باشد. Solver CP-SAT model ساخته شود. Interval-based resources استفاده شود. Solver deterministic باشد. Generated Schedule تولید شود. Validation Schedule مستقل validate شود. هیچ conflict hard باقی نماند. baseline diff تولید شود. Capacity Capacity Search روی Canonical Problem اجرا شود. F feasible اثبات شود. F+1 infeasible اثبات شود. Binding constraints استخراج شوند. Capacity Proof ذخیره شود. 66. مرز Version 0.9 Version 0.9 هنوز این موارد را کامل نمی‌کند: Network-wide multi-route optimization Marketplace allocation Full wagon circulation Full locomotive circulation PostgreSQL production persistence Multi-user run management Calibration against historical performance Large-scale decomposition Real-time dispatching این موارد در Versionهای بعدی می‌آیند. 67. مسیر توسعه بعد از 0.9 v0.8 Real Data Adapter ↓ v0.9 Real Scheduling + Resources + CP-SAT ↓ v1.0 Network Optimization + Shared Resources ↓ v1.1 PostgreSQL + API + Run Management ↓ v1.2 Calibration + Historical Validation ↓ v1.3 Marketplace Allocation Integration ↓ v1.4 Scenario / Investment Optimization ↓ v2.0 Production Railway Capacity Platform 68. معماری نهایی مورد انتظار ┌─────────────────────┐ │ Railway Marketplace │ └──────────┬──────────┘ ↓ Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ ┌────────────┴────────────┐ │ │ Wagon Cycle Loco Cycle │ │ └────────────┬────────────┘ ↓ TrainRun ↓ Directed TrainPath ↓ ┌────────────────┼────────────────┐ ↓ ↓ ↓ Blocks Stations Junctions │ │ │ └────────────────┼────────────────┘ ↓ Resource Constraints ↓ Operational Rules ↓ CP-SAT ↓ Generated Schedule ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck / Explanation ↓ Scenario Analysis ↓ Capacity / Allocation ↓ Railway Marketplace 69. اصل نهایی Version 0.9 معماری از این نقطه به بعد باید بر این زنجیره سخت‌گیرانه بنا شود: DATA ↓ MAPPING ↓ QUALITY ↓ CANONICAL MODEL ↓ TOPOLOGY ↓ RESOURCE MODEL ↓ SCHEDULE MODEL ↓ SOLVER ↓ VALIDATOR ↓ CAPACITY ↓ PROOF ↓ EXPLANATION و چهار Rule غیرقابل مذاکره: No Feasible Schedule → No Operational Capacity No Independent Validation → No Accepted Capacity No Verified Data Mapping → No Production Use No Evidence → No Capacity Number این Version نقطه‌ای است که پروژه از یک capacity calculator به یک railway scheduling and capacity engine واقعی تبدیل می‌شود. گام بعدی منطقی، پیاده‌سازی خود Version 0.9 به‌صورت کد اجرایی کامل است؛ یعنی فایل‌های domain/topology/scheduling/solver/conflicts/validation/capacity را یکپارچه کنیم، وابستگی‌های ناقص Version 0.8 را برطرف کنیم و یک تست End-to-End با Train 100/101 بسازیم که واقعاً Schedule تولید و مستقل validate کند. --- ## ادامه 25 — پرسش کاربر (`message-id:087c153b-e084-497b-8e79-6168f83cf6a0`) بله لطفا --- ## ادامه 25 — پاسخ دستیار (`message-id:4fbd11f1-acf0-4830-8743-d3ba8fe11d0a`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 26 — پرسش کاربر (`message-id:50a88d47-c410-4e36-8518-a9e8573eb499`) بله این رو ادامه بدیم. به صورت متنی همینجا بیاد --- ## ادامه 26 — پاسخ دستیار (`message-id:285f5bd8-737e-44c5-a1e7-761817685295`) حتماً. پس ادامه را کاملاً متنی و اجرایی پیش می‌بریم و فعلاً فایل تولید نمی‌کنیم. نکته مهم: نسخه‌ای که در پیام قبلی برای v0.9 تعریف کردیم، باید یک اصلاح معماری داشته باشد: Blockهای جهت‌دار را نباید صرفاً با رشته‌ی A->B به‌عنوان Resource فیزیکی مستقل در نظر بگیریم؛ چون در Single Track، دو جهت مخالف باید روی یک Physical Block Resource مشترک conflict داشته باشند. این اصلاح برای جلوگیری از خطای محاسبه Capacity حیاتی است. در ادامه، هسته اجرایی v0.9 را مرحله‌به‌مرحله می‌سازیم. Railway Capacity Engine v0.9 — Executable Core Implementation Railway Capacity Engine v0.9 Executable Core Implementation 1. هدف هسته v0.9 باید این زنجیره را واقعاً اجرا کند: Access / Excel ↓ Canonical TrainRun ↓ Directed Path ↓ Physical Blocks ↓ Station Resources ↓ Junction Resources ↓ Operational Windows ↓ CP-SAT ↓ Generated Schedule ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof 2. اصل مهم: Directed Path ≠ Physical Resource در مدل واقعی باید دو مفهوم جدا باشند. Directed Segment مثلاً: GAR → MID و: MID → GAR دو Movement متفاوت هستند. اما Resource فیزیکی: Physical Block = GAR--MID یکی است. بنابراین: Train 100: GAR → MID Train 101: MID → GAR هر دو از: Physical Block GAR--MID استفاده می‌کنند. اگر TrackType برابر SINGLE باشد: Mutual Exclusion = TRUE اگر DOUBLE باشد: Direction-specific Resource خواهیم داشت. 3. Domain Model فایل: app/domain/infrastructure.py مدل: from dataclasses import dataclass from enum import Enum from typing import Optional class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_min: int clearing_time_min: int = 0 headway_min: int = 0 switch_time_min: int = 0 @dataclass(frozen=True) class StationTrack: id: str station_id: str usable_length_m: int direction: Optional[Direction] = None supports_crossing: bool = True @dataclass(frozen=True) class Station: id: str name: str tracks: tuple[StationTrack, ...] @dataclass(frozen=True) class Junction: id: str conflicting_movements: frozenset[str] @dataclass(frozen=True) class OperationalWindow: resource_id: str start_min: int end_min: int window_type: str 4. TrainStationCall فایل: app/domain/train.py from dataclasses import dataclass from typing import Optional from .infrastructure import Direction @dataclass(frozen=True) class TrainStationCall: station_id: str sequence: int arrival_min: int dwell_min: int departure_min: int required_wait_min: int = 0 baseline_running_time_to_next_min: Optional[int] = None chainage_km: Optional[float] = None @dataclass(frozen=True) class TrainRun: id: str train_no: str direction: Direction station_calls: tuple[TrainStationCall, ...] length_m: int weight_t: int = 0 earliest_departure_min: Optional[int] = None latest_arrival_min: Optional[int] = None 5. Directed Segment از روی StationCallها ساخته می‌شود. @dataclass(frozen=True) class DirectedSegment: train_run_id: str sequence: int from_station: str to_station: str physical_block_id: str direction: Direction مثلاً: Train 100 GAR → MID physical_block_id = BLOCK_GAR_MID direction = FORWARD و: Train 101 MID → GAR physical_block_id = BLOCK_GAR_MID direction = REVERSE 6. Physical Block Identity این تابع بسیار مهم است: def physical_block_id(station_a: str, station_b: str) -> str: a, b = sorted((station_a, station_b)) return f"BLOCK:{a}:{b}" بنابراین: physical_block_id("GAR", "MID") و: physical_block_id("MID", "GAR") هر دو می‌دهند: BLOCK:GAR:MID 7. Directed Path Builder def build_directed_segments( train: TrainRun, blocks: dict[str, PhysicalBlock], ) -> list[DirectedSegment]: result = [] calls = train.station_calls for i in range(len(calls) - 1): current = calls[i] nxt = calls[i + 1] block_id = physical_block_id( current.station_id, nxt.station_id, ) if block_id not in blocks: raise ValueError( f"Missing physical block: {block_id}" ) result.append( DirectedSegment( train_run_id=train.id, sequence=i + 1, from_station=current.station_id, to_station=nxt.station_id, physical_block_id=block_id, direction=train.direction, ) ) return result 8. Time Variables برای هر Train و Station: A(i,s) = Arrival D(i,s) = Departure برای هر Train و Block: E(i,b) = Entry X(i,b) = Exit C(i,b) = Clear 9. Fundamental Temporal Constraints در Station: [ D_{i,s}\ge A_{i,s}+Dwell_{i,s} ] و: [ Dwell_{i,s}\ge RequiredWait_{i,s} ] در Block: [ X_{i,b}\ge E_{i,b}+T_{run,i,b} ] و: [ C_{i,b}\ge X_{i,b}+T_{clear,b} ] رابطه Station و Block: [ E_{i,b}\ge D_{i,s} ] و: [ A_{i,s+1}\ge C_{i,b} ] 10. Baseline Running Time در داده واقعی Access: seir به‌عنوان: baseline_running_time_to_next_min وارد Canonical Model می‌شود. مثلاً: Gar time_out = 09:00 seir = 34 Sakheh time_in = 09:34 Validation: [ 09:00+34=09:34 ] 11. CP-SAT Model فایل: app/scheduling/cp_sat_model.py ساختار: from ortools.sat.python import cp_model class SchedulingModel: def __init__( self, infrastructure, trains, horizon, ): self.infrastructure = infrastructure self.trains = trains self.horizon = horizon self.model = cp_model.CpModel() self.arrival = {} self.departure = {} self.entry = {} self.exit = {} self.clear = {} self.station_track_choice = {} 12. Station Variables for train in trains: for index, call in enumerate(train.station_calls): a = model.new_int_var( 0, horizon, f"A_{train.id}_{index}" ) d = model.new_int_var( 0, horizon, f"D_{train.id}_{index}" ) arrival[(train.id, index)] = a departure[(train.id, index)] = d model.add( d >= a + max( call.dwell_min, call.required_wait_min, ) ) 13. Baseline Preservation برای اینکه Solver بدون دلیل زمان‌بندی موجود را جابه‌جا نکند: MAX_DEVIATION = 720 model.add( a >= max( 0, call.arrival_min - MAX_DEVIATION ) ) model.add( a <= call.arrival_min + MAX_DEVIATION ) model.add( d >= max( 0, call.departure_min - MAX_DEVIATION ) ) model.add( d <= call.departure_min + MAX_DEVIATION ) 14. Block Variables برای هر Segment: e = model.new_int_var( 0, horizon, f"E_{train.id}_{i}" ) x = model.new_int_var( 0, horizon, f"X_{train.id}_{i}" ) c = model.new_int_var( 0, horizon, f"C_{train.id}_{i}" ) 15. Running Constraint اگر: seir = 34 باشد: [ X-E\ge34 ] model.add( x >= e + running_time ) 16. Clearing Constraint مثلاً: clearing_time = 5 پس: [ C\ge X+5 ] model.add( c >= x + clearing_time ) 17. Station → Block model.add( e >= departure[(train.id, i)] ) 18. Block → Station model.add( arrival[(train.id, i + 1)] >= c ) این چهار Constraint در کنار هم یک Time-Space Movement معتبر ایجاد می‌کنند. 19. Single Track Resource برای یک Physical Block: BLOCK:GAR:MID اگر: track_type = SINGLE تمام Occupancy Intervalها باید NoOverlap باشند. interval = model.new_interval_var( e, c - e, c, f"OCC_{train.id}_{block.id}" ) سپس: model.add_no_overlap( block_intervals[block.id] ) این constraint به‌صورت خودکار اجازه نمی‌دهد: Train A: 10:00 ───── 11:00 Train B: 10:30 ───── 11:30 روی Single Track قرار بگیرند. 20. Headway NoOverlap به‌تنهایی کافی نیست. اگر: clear A = 11:00 headway = 10 باشد: [ entry_B\ge11:10 ] پس برای هر دو ترتیب: order = model.new_bool_var( f"ORDER_{train_a}_{train_b}_{block.id}" ) model.add( clear_a + gap <= entry_b ).only_enforce_if(order) model.add( clear_b + gap <= entry_a ).only_enforce_if(order.Not()) که: gap = max( block.headway_min, block.switch_time_min ) 21. چرا NoOverlap و Headway هر دو لازم‌اند؟ مثلاً: Train A: 10:00–10:40 Train B: 10:41–11:20 NoOverlap: PASS اما اگر Headway: 10 min باشد: FAIL بنابراین: NoOverlap و: Headway دو مفهوم مستقل هستند. 22. Double Track در Double Track نباید دو جهت مخالف را روی یک Resource مشترک قرار داد. برای Double Track: BLOCK:GAR:MID:FWD BLOCK:GAR:MID:REV دو Resource مجزا خواهند بود. در نتیجه: Train 100: GAR → MID روی: FWD و: Train 101: MID → GAR روی: REV قرار می‌گیرد. در این حالت conflict بین این دو فقط در Resourceهای مشترک بعدی بررسی می‌شود: Station Junction Crossover Terminal Transition 23. Resource Key تابع: def block_resource_key( block: PhysicalBlock, direction: Direction, ) -> str: if block.track_type == TrackType.SINGLE: return block.id return f"{block.id}:{direction.value}" این تابع یکی از اجزای اصلی correctness مدل است. 24. Station Track Assignment برای هر Train و Station: choice = model.new_bool_var( f"TRACK_{train.id}_{station.id}_{track.id}" ) و: model.add_exactly_one( choices ) فقط Trackهایی وارد choices می‌شوند که: track.usable_length_m >= train.length_m باشند. 25. Station Track Occupancy برای Track: Arrival → Departure یک Optional Interval ساخته می‌شود: interval = model.new_optional_interval_var( arrival, departure - arrival, departure, choice, name ) سپس برای هر Track: model.add_no_overlap( station_track_intervals[track.id] ) 26. نتیجه اگر دو قطار بخواهند از یک Track استفاده کنند: Train A: 10:00–10:30 Train B: 10:20–10:50 Solver مجبور است: Track 1 → A Track 2 → B یا زمان یکی را جابه‌جا کند. اگر هیچ Track دیگری وجود نداشته باشد: INFEASIBLE 27. Junction Junction نیز Resource است. مثلاً: J1 و Movementها: M1 M2 M3 اگر: M1 conflicts with M2 باید فقط همین دو Movement mutually exclusive باشند. نباید تمام Junction را کورکورانه NoOverlap کرد، زیرا ممکن است دو Movement همزمان قابل انجام باشند. بنابراین: Conflict Matrix باید منبع تصمیم باشد. 28. Junction Conflict Constraint برای Movementهای conflicting: [ C_i\le E_j ] یا: [ C_j\le E_i ] با Boolean Ordering. 29. Operational Window مثلاً Block: BLOCK:GAR:MID در: 22:00–23:00 بسته است. Constraint: [ C_i\le22:00 ] یا: [ E_i\ge23:00 ] در CP-SAT: before = model.new_bool_var("BEFORE_WINDOW") model.add( clear <= window.start_min ).only_enforce_if(before) model.add( entry >= window.end_min ).only_enforce_if(before.Not()) 30. Objective Objective پایه: [ \min \sum |A-A^{baseline}| + \sum |D-D^{baseline}| ] یعنی Schedule تا حد ممکن به Baseline نزدیک بماند. برای Capacity Search: ابتدا Feasibility مهم است. پس دو Mode داریم: BASELINE_MODE CAPACITY_MODE 31. Capacity Mode در Capacity Mode، تعداد TrainRunهای قابل Schedule شدن پارامتر اصلی است. def solve_frequency( frequency: int, problem_factory, ): trains = problem_factory(frequency) schedule = solve( trains ) validation = validate( schedule ) return validation.feasible 32. Capacity Search def search_capacity( problem_factory, low: int, high: int, ): best = 0 while low <= high: mid = (low + high) // 2 feasible = solve_frequency( mid, problem_factory, ) if feasible: best = mid low = mid + 1 else: high = mid - 1 return { "capacity": best, "feasible_frequency": best, "infeasible_frequency": best + 1, } اما این فقط زمانی معتبر است که: Solver Feasible + Independent Validator Pass باشد. 33. Independent Validator Validator باید تمام خروجی‌ها را دوباره بررسی کند. def validate_schedule( infrastructure, trains, schedule, ): چک‌ها: 1. station order 2. arrival/departure 3. dwell 4. running time 5. clearing 6. block conflict 7. headway 8. station track conflict 9. track length 10. junction conflict 11. operational windows 12. earliest departure 13. latest arrival 34. Baseline Reconciliation برای هر Event: delta_arrival = ( generated.arrival_min - baseline.arrival_min ) delta_departure = ( generated.departure_min - baseline.departure_min ) خروجی: { "train_run": "100", "station": "Arak", "baseline_arrival": 747, "generated_arrival": 752, "delta": 5 } 35. Explanation هر تغییر Schedule باید قابل توضیح باشد. مثلاً: Train 101 delayed by 12 minutes. Cause: Single-track conflict on BLOCK:MID:AND. Conflicting movement: Train 100. Required separation: 10 minutes. Available gap: 3 minutes. Resolution: Train 101 waits at MID station. این Explanation از: Conflict + Resource + Timing + Resolution ساخته می‌شود. 36. Capacity Proof Object @dataclass class CapacityProof: capacity: int feasible_frequency: int infeasible_frequency: int feasible_schedule_id: str infeasible_run_id: str binding_constraints: list[str] status: str 37. Proof Contract اگر: F = 76 باشد: Run-76 Solver = FEASIBLE Validator = PASS و: F = 77 باشد: Run-77 Solver = INFEASIBLE آنگاه: [ C=76 ] قابل گزارش است. اما اگر: Run-77 Solver = UNKNOWN باشد: Capacity = UNVERIFIED نه 76. 38. Binding Constraint Extraction بعد از Capacity Search: def extract_binding_constraints( feasible_schedule, failed_attempt, ): باید Resourceهایی را استخراج کند که: Slack ≈ 0 یا: prevented insertion را ایجاد کرده‌اند. خروجی: { "resource": "BLOCK:MID:AND", "type": "SINGLE_TRACK", "slack_min": 0, "impact": "CAPACITY_LIMITING" } 39. Real Access Adapter برای فایل: aaa.accdb Adapter: class AccessAdapter: def __init__(self, connection_string): self.connection_string = connection_string def list_tables(self): ... def read_table(self, table_name): ... اما: table_name نباید از User Input خام وارد SQL شود. باید ابتدا از Mapping Registry تأیید شود. 40. Mapping Registry source: access fields: ID: target: source_record_id status: VERIFIED TrainNo: target: train_run.train_no status: VERIFIED StationName: target: station.name status: VERIFIED StationNumber: target: station.source_number status: VERIFIED Sequence: target: train_station_call.sequence status: VERIFIED time_in: target: train_station_call.arrival_time status: VERIFIED time_take: target: train_station_call.actual_dwell status: VERIFIED time_out: target: train_station_call.departure_time status: VERIFIED RequiredWait: target: train_station_call.minimum_required_wait status: PROVISIONAL Kilometerage: target: station.chainage status: HIGH_CONFIDENCE MaxSpeed: target: train_station_call.max_speed status: PROVISIONAL TrainName: target: train_service.name status: VERIFIED seir: target: route_segment.baseline_running_time status: VERIFIED 41. مهم‌ترین نکته درباره داده فعلی از نمونه Train 100/101 هنوز نمی‌توان با قطعیت نتیجه گرفت که: StationNumber نشان‌دهنده چه سیستم کدگذاری عملیاتی است. همچنین: MaxSpeed Distance sumDistancezz kol نباید بدون نمونه/مستندات بیشتر وارد محاسبات Capacity شوند. 42. Topology Reconstruction داده Train 100/101 برای استخراج: Station Sequence Chainage Running Time کافی است. اما برای استخراج قطعی: Single / Double Track Station Track Count Junction Crossing Capability Signal Headway Switch Time کافی نیست. پس Topology باید از Source مستقل زیرساختی وارد شود: Infrastructure Master مثلاً: blocks: - id: BLOCK:GAR:MID station_a: GAR station_b: MID track_type: SINGLE clearing_time_min: 5 headway_min: 10 switch_time_min: 5 43. Reference Data Assembly در MVP می‌توان داده را به این صورت Assemble کرد: Access ↓ Train Movement Evidence ↓ Infrastructure Master ↓ Canonical TrainRun ↓ Scheduling Problem نه اینکه از Access حدس بزنیم کدام خط Single یا Double است. 44. Test: Reverse Direction ورودی: Train 100: GAR → ... → AND Train 101: AND → ... → GAR Expected: Train 100 direction = FORWARD Train 101 direction = REVERSE و: physical_block( GAR, MID ) == physical_block( MID, GAR ) 45. Test: Single Track Train A: BLOCK B 10:00–11:00 Train B: BLOCK B 11:05–12:00 headway = 10 نتیجه: INFEASIBLE زیرا: [ 11:00+10>11:05 ] 46. Test: Valid Headway Train A: 10:00–11:00 Train B: 11:10–12:00 headway = 10 نتیجه: FEASIBLE 47. Test: Double Track Train A: GAR → MID Train B: MID → GAR و Block: DOUBLE Expected: No single-track conflict 48. Test: Station Conflict Station: MID یک Track: MID-1 Train A: 10:00–10:30 Train B: 10:20–10:50 Expected: INFEASIBLE با دو Track: MID-1 MID-2 Expected: FEASIBLE 49. Test: Track Length Train length = 700m Track length = 600m Expected: NO VALID TRACK و Schedule نباید ساخته شود. 50. Test: Midnight time_in = 23:46 time_take = 50 Expected: time_out = 00:36 next day 51. Test: seir time_out = 09:00 seir = 34 next time_in = 09:34 Expected: PASS 52. Test: Capacity Proof Test باید این Contract را بررسی کند: capacity = F F: Solver = FEASIBLE Validator = PASS F+1: Solver = INFEASIBLE و نه فقط: F: Solver = FEASIBLE 53. Test: Determinism یک Scenario دو بار اجرا شود: RUN-A RUN-B با: same data same model same seed same solver parameters حداقل این موارد باید برابر باشند: Feasibility Capacity Train sequence Binding constraints 54. Production Safety Rules هیچ یک از موارد زیر مجاز نیست: Synthetic topology → Production Capacity Unknown mapping → Solver input Solver feasible → Automatically accepted capacity Utilization alone → Bottleneck conclusion Baseline conflict → Automatically infeasible without resource model 55. اولین Vertical Slice واقعی برای داده فعلی پروژه: TrainNo 100 TrainNo 101 ابتدا: Access Adapter سپس: Train Movement Mapper سپس: TrainRun 100 TrainRun 101 سپس: StationCall[] سپس: seir reconciliation سپس: Directed Segment[] سپس: Physical Block[] سپس: Station Track[] سپس: CP-SAT سپس: Independent Validator و در نهایت: Capacity Proof 56. Definition of Done Version 0.9 زمانی واقعاً Done است که اجرای زیر ممکن باشد: result = engine.run( data_version="DATA-001", scenario_id="SC-001", train_run_ids=["100", "101"], ) و: result.schedule result.validation result.baseline_diff result.capacity result.capacity_proof result.binding_constraints result.explanation همگی تولید شوند. 57. Architecture after v0.9 پس از این نسخه، هسته واقعی به شکل زیر خواهد بود: SOURCE │ ┌────────┴────────┐ │ │ Access Excel │ │ └────────┬────────┘ ↓ RAW / STAGING ↓ MAPPING ↓ QUALITY GATE ↓ CANONICAL MODEL ↓ ┌────────┴────────┐ │ │ Infrastructure TrainRun │ │ └────────┬────────┘ ↓ DIRECTED PATH ↓ RESOURCE MODEL ↓ CP-SAT MODEL ↓ SOLVER ↓ GENERATED SCHEDULE ↓ INDEPENDENT VALIDATOR ↓ CAPACITY SEARCH ↓ CAPACITY PROOF ↓ BOTTLENECK ENGINE ↓ EXPLANATION ENGINE این ساختار از اینجا به بعد مبنای Version 1.0 خواهد بود. یک نکته بسیار مهم برای ادامه پروژه الان هسته الگوریتمی v0.9 مشخص و قابل کدنویسی است؛ اما برای اینکه خروجی Capacity مربوط به خط واقعی ایران باشد، سه دسته داده را دیگر نباید حدس بزنیم: Topology واقعی خط: Single/Double Track، Blockها و Junctionها ظرفیت و مشخصات ایستگاه‌ها: تعداد خطوط، طول قابل استفاده، امکان Crossing/O for passing Operational Rules: Headway، Switch/Clearing Time، پنجره‌های عملیاتی داده Train 100/101 برای زمان‌های واقعی، ترتیب ایستگاه‌ها، جهت، seir، dwell و chainage بسیار ارزشمند است؛ ولی این سه دسته را به‌تنهایی اثبات نمی‌کند. بنابراین قدم بعدی را می‌توانیم خیلی عملی انجام دهیم: همین‌جا یک نسخه کامل‌تر از v0.9 را به شکل چند فایل Python پشت‌سرهم بنویسیم؛ به‌طوری‌که شما فقط فایل‌ها را در پروژه قرار دهید و با pytest اجرا کنید. در آن مرحله، کدهای ناقص Version 0.8 مثل map_access_records, build_train_runs, build_station_calls, validate_dataset و build_paths هم یکجا تکمیل می‌شوند. --- ## ادامه 27 — پرسش کاربر (`message-id:9912faa5-e6b1-4a31-9528-fc01379c09f9`) بله لطفا --- ## ادامه 27 — پاسخ دستیار (`message-id:bbb527d8-bd74-419f-933d-dba72a9856a3`) حتماً. در ادامه، Version 0.9 — Executable Core را از حالت specification به یک هسته‌ی اجرایی منسجم تبدیل می‌کنیم؛ با این هدف که بعداً بتوانیم بدون بازطراحی، Access/Excel واقعی، PostgreSQL، API و Marketplace را روی آن سوار کنیم. نکته مهم: این نسخه هسته‌ی اجرایی و Vertical Slice است، نه هنوز Production Enterprise. همچنین topology واقعی خط باید از Master Data بیاید و عمداً از روی Kilometerage به‌صورت خودکار به‌عنوان حقیقت زیرساختی ساخته نمی‌شود. 1. ساختار پروژه rail_capacity/ ├── pyproject.toml ├── requirements.txt │ ├── app/ │ ├── __init__.py │ ├── domain.py │ ├── time_utils.py │ ├── infrastructure.py │ ├── source.py │ ├── mapper.py │ ├── path.py │ ├── scheduler.py │ ├── validator.py │ ├── capacity.py │ ├── engine.py │ └── demo.py │ └── tests/ └── test_core.py معماری اجرایی: Access / Excel │ ▼ Raw / Staging │ ▼ Mapping + Data Quality │ ▼ Canonical Domain │ ▼ Directed Train Path │ ▼ Physical Resource Model │ ▼ CP-SAT Scheduler │ ▼ Generated Schedule │ ▼ Independent Validator │ ▼ Capacity Search │ ▼ Capacity Proof │ ▼ Bottleneck + Explanation 2. dependencies requirements.txt pandas>=3.0,<4 openpyxl>=3.1,<4 PyYAML>=6,<7 ortools>=9.14,<10 pytest>=8,<10 برای Access: pyodbc>=5,<6 را نیز اضافه کنید. 3. مدل Canonical Domain app/domain.py Python from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from typing import Optional class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class WindowType(str, Enum): CLOSED = "CLOSED" @dataclass(frozen=True) class StationTrack: id: str station_id: str usable_length_m: int direction: Optional[Direction] = None @dataclass(frozen=True) class Station: id: str name: str source_number: Optional[str] = None tracks: tuple[StationTrack, ...] = () @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str length_m: int track_type: TrackType running_time_forward_min: int running_time_reverse_min: int headway_min: int = 3 switch_time_min: int = 5 def running_time(self, direction: Direction) -> int: if direction == Direction.FORWARD: return self.running_time_forward_min return self.running_time_reverse_min @dataclass(frozen=True) class OperationalWindow: id: str resource_id: str start_min: int end_min: int window_type: WindowType = WindowType.CLOSED @dataclass(frozen=True) class TrainStationCall: station_id: str sequence: int baseline_arrival_min: int baseline_departure_min: int dwell_min: int required_wait_min: int kilometerage: Optional[float] = None source_record_id: Optional[str] = None baseline_running_time_to_next_min: Optional[int] = None @dataclass(frozen=True) class TrainRun: id: str train_no: str service_name: str direction: Direction train_length_m: int train_weight_t: float station_calls: tuple[TrainStationCall, ...] earliest_departure_min: Optional[int] = None latest_arrival_min: Optional[int] = None @dataclass(frozen=True) class DirectedSegment: id: str train_run_id: str sequence: int from_station_id: str to_station_id: str physical_block_id: str direction: Direction baseline_running_time_min: int @dataclass(frozen=True) class TrainPath: train_run_id: str segments: tuple[DirectedSegment, ...] @dataclass class MovementSchedule: train_run_id: str segment_id: str entry_min: int exit_min: int clear_min: int from_station_id: str to_station_id: str physical_block_id: str resource_id: str @dataclass class StationSchedule: train_run_id: str station_id: str sequence: int arrival_min: int departure_min: int assigned_track_id: Optional[str] = None @dataclass class TrainSchedule: train_run_id: str stations: list[StationSchedule] = field(default_factory=list) movements: list[MovementSchedule] = field(default_factory=list) @dataclass class Schedule: trains: list[TrainSchedule] = field(default_factory=list) objective_value: Optional[float] = None solver_status: Optional[str] = None @dataclass class ValidationIssue: code: str message: str train_run_id: Optional[str] = None resource_id: Optional[str] = None @dataclass class ValidationResult: feasible: bool issues: list[ValidationIssue] = field(default_factory=list) @property def errors(self) -> list[ValidationIssue]: return self.issues @dataclass class CapacityProof: requested_flow: int feasible: bool independently_validated: bool next_flow: Optional[int] = None next_flow_feasible: Optional[bool] = None capacity: Optional[int] = None explanation: str = "" @dataclass class SchedulingProblem: stations: dict[str, Station] blocks: dict[str, PhysicalBlock] train_runs: list[TrainRun] paths: dict[str, TrainPath] operational_windows: list[OperationalWindow] = field( default_factory=list ) planning_horizon_min: int = 24 * 60 minimize_baseline_deviation: bool = True 4. Time utilities این قسمت برای داده‌ی واقعی شما بسیار مهم است، چون در Access نمونه‌ی واقعی، midnight rollover وجود داشت. app/time_utils.py Python from __future__ import annotations import math from typing import Iterable, Optional def parse_clock(value: object) -> Optional[int]: """ Convert HH:MM or HH:MM:SS to minute-of-day. Returns None for empty/invalid values. """ if value is None: return None if isinstance(value, float) and math.isnan(value): return None text = str(value).strip() if not text: return None parts = text.split(":") try: hour = int(parts[0]) minute = int(parts[1]) except (ValueError, IndexError): return None return hour * 60 + minute def normalize_time_sequence(values: Iterable[object]) -> list[Optional[int]]: """ Convert a sequence of clock times into monotonically increasing absolute minutes, handling midnight rollover. Example: 23:46 00:36 00:56 02:19 becomes: 1426 1476 1496 1579 """ result: list[Optional[int]] = [] previous: Optional[int] = None offset = 0 for value in values: minute = parse_clock(value) if minute is None: result.append(None) continue absolute = minute + offset if previous is not None and absolute < previous: while absolute < previous: offset += 24 * 60 absolute = minute + offset result.append(absolute) previous = absolute return result def minutes_to_clock(value: int) -> str: """ Convert absolute minute to HH:MM. """ minute_of_day = value % (24 * 60) hour = minute_of_day // 60 minute = minute_of_day % 60 return f"{hour:02d}:{minute:02d}" 5. Infrastructure و Physical Resource این بخش یک اصلاح مهم نسبت به نسخه‌های قبلی است: DirectedSegment مسیر حرکت قطار است؛ PhysicalBlock منبع فیزیکی است. در Single Track، دو جهت مختلف یک PhysicalBlock مشترک دارند. app/infrastructure.py Python from __future__ import annotations from .domain import ( Direction, DirectedSegment, PhysicalBlock, Station, TrainPath, TrainRun, ) def canonical_physical_block_id( station_a: str, station_b: str, ) -> str: """ Physical identity is direction-independent. """ a, b = sorted([station_a, station_b]) return f"BLOCK:{a}:{b}" def resource_id( block: PhysicalBlock, direction: Direction, ) -> str: """ SINGLE: both directions share one resource. DOUBLE: each direction gets a separate directional resource. """ if block.track_type.value == "SINGLE": return block.id return f"{block.id}:{direction.value}" def build_directed_path( train_run: TrainRun, blocks: dict[str, PhysicalBlock], ) -> TrainPath: calls = list(train_run.station_calls) if len(calls) < 2: raise ValueError( f"TrainRun {train_run.id} must contain at least 2 stations." ) segments: list[DirectedSegment] = [] for index in range(len(calls) - 1): current = calls[index] next_call = calls[index + 1] physical_id = canonical_physical_block_id( current.station_id, next_call.station_id, ) if physical_id not in blocks: raise ValueError( f"Physical block {physical_id} not found for " f"{train_run.id}." ) block = blocks[physical_id] if current.baseline_running_time_to_next_min is not None: running_time = current.baseline_running_time_to_next_min else: running_time = block.running_time(train_run.direction) segments.append( DirectedSegment( id=f"{train_run.id}:SEG:{index + 1}", train_run_id=train_run.id, sequence=index + 1, from_station_id=current.station_id, to_station_id=next_call.station_id, physical_block_id=block.id, direction=train_run.direction, baseline_running_time_min=running_time, ) ) return TrainPath( train_run_id=train_run.id, segments=tuple(segments), ) def build_paths( train_runs: list[TrainRun], blocks: dict[str, PhysicalBlock], ) -> dict[str, TrainPath]: return { train.id: build_directed_path(train, blocks) for train in train_runs } 6. Excel و Access Adapter این Adapterها نباید وارد Solver شوند. فقط: Source → Raw Data app/source.py Python from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Optional import pandas as pd @dataclass class SourceInspection: source_type: str source_path: str sheets_or_tables: list[str] class ExcelAdapter: def __init__(self, path: str | Path): self.path = Path(path) def inspect(self) -> SourceInspection: if not self.path.exists(): raise FileNotFoundError(self.path) excel = pd.ExcelFile(self.path) return SourceInspection( source_type="EXCEL", source_path=str(self.path), sheets_or_tables=excel.sheet_names, ) def extract( self, sheet_name: Optional[str] = None, ) -> pd.DataFrame: inspection = self.inspect() if sheet_name is None: sheet_name = inspection.sheets_or_tables[0] return pd.read_excel( self.path, sheet_name=sheet_name, dtype=object, ) class AccessAdapter: def __init__(self, path: str | Path): self.path = Path(path) def _connection_string(self) -> str: return ( "DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};" f"DBQ={self.path};" ) def list_tables(self) -> list[str]: try: import pyodbc except ImportError as exc: raise RuntimeError( "pyodbc is required for Access extraction." ) from exc connection = pyodbc.connect(self._connection_string()) try: cursor = connection.cursor() tables = [] for row in cursor.tables( tableType="TABLE" ): tables.append(row.table_name) return tables finally: connection.close() def extract_table( self, table_name: str, ) -> pd.DataFrame: try: import pyodbc except ImportError as exc: raise RuntimeError( "pyodbc is required for Access extraction." ) from exc tables = self.list_tables() if table_name not in tables: raise ValueError( f"Access table '{table_name}' does not exist." ) connection = pyodbc.connect( self._connection_string() ) try: return pd.read_sql( f"SELECT * FROM [{table_name}]", connection, ) finally: connection.close() نکته: روی سرور، علاوه بر pyodbc باید Microsoft Access ODBC Driver مناسب نصب باشد. 7. Mapping واقعی Access این قسمت دقیقاً برای schema فعلی شما نوشته شده است: ID kol TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir app/mapper.py Python from __future__ import annotations from dataclasses import dataclass from typing import Optional import pandas as pd from .domain import ( Direction, TrainRun, TrainStationCall, ) from .time_utils import ( normalize_time_sequence, parse_clock, ) @dataclass class MappingIssue: code: str message: str train_no: Optional[str] = None REQUIRED_COLUMNS = { "TrainNo", "StationName", "Sequence", "time_in", "time_take", } def normalize_train_no(value: object) -> str: if value is None: return "" text = str(value).strip() if text.endswith(".0"): text = text[:-2] return text def normalize_station_name(value: object) -> str: return " ".join(str(value).strip().split()) def station_id_from_row(row: pd.Series) -> str: value = row.get("StationNumber") if value is not None: text = str(value).strip() if text and text.lower() != "nan": return f"STN:{text}" return ( "STN:" + normalize_station_name( row["StationName"] ) ) def numeric_or_none(value: object) -> Optional[float]: if value is None: return None try: number = float(value) if pd.isna(number): return None return number except (ValueError, TypeError): return None def infer_direction( rows: pd.DataFrame, ) -> Direction: chainages = [] for value in rows["Kilometerage"]: number = numeric_or_none(value) if number is not None: chainages.append(number) if len(chainages) < 2: raise ValueError( "Direction cannot be inferred: insufficient " "Kilometerage values." ) delta = chainages[-1] - chainages[0] if delta > 0: return Direction.FORWARD if delta < 0: return Direction.REVERSE raise ValueError( "Direction cannot be inferred from Kilometerage." ) def validate_columns(df: pd.DataFrame) -> None: missing = REQUIRED_COLUMNS - set(df.columns) if missing: raise ValueError( "Missing required columns: " + ", ".join(sorted(missing)) ) def map_access_train_runs( df: pd.DataFrame, train_length_m: int = 0, train_weight_t: float = 0.0, ) -> tuple[list[TrainRun], list[MappingIssue]]: validate_columns(df) runs: list[TrainRun] = [] issues: list[MappingIssue] = [] working = df.copy() working["_TrainNo"] = working["TrainNo"].map( normalize_train_no ) working["_Sequence"] = pd.to_numeric( working["Sequence"], errors="coerce", ) for train_no, group in working.groupby( "_TrainNo", sort=False, ): if not train_no: issues.append( MappingIssue( code="EMPTY_TRAIN_NO", message="TrainNo is empty.", ) ) continue group = group.sort_values( "_Sequence" ).reset_index(drop=True) try: direction = infer_direction(group) except ValueError as exc: issues.append( MappingIssue( code="DIRECTION_UNKNOWN", message=str(exc), train_no=train_no, ) ) continue normalized_arrivals = normalize_time_sequence( group["time_in"].tolist() ) calls: list[TrainStationCall] = [] for index, row in group.iterrows(): arrival = normalized_arrivals[index] if arrival is None: issues.append( MappingIssue( code="INVALID_TIME_IN", message=( f"Invalid time_in at sequence " f"{index + 1}." ), train_no=train_no, ) ) continue dwell_value = numeric_or_none( row["time_take"] ) dwell = int(dwell_value or 0) required_wait_value = numeric_or_none( row.get("RequiredWait") ) required_wait = int( required_wait_value or 0 ) departure = arrival + dwell seir_value = numeric_or_none( row.get("seir") ) seir = ( int(seir_value) if seir_value is not None else None ) kilometerage = numeric_or_none( row.get("Kilometerage") ) station_id = station_id_from_row(row) source_id = row.get("ID") source_id_text = ( str(source_id) if source_id is not None else None ) calls.append( TrainStationCall( station_id=station_id, sequence=index + 1, baseline_arrival_min=arrival, baseline_departure_min=departure, dwell_min=dwell, required_wait_min=required_wait, kilometerage=kilometerage, source_record_id=source_id_text, baseline_running_time_to_next_min=seir, ) ) if len(calls) < 2: issues.append( MappingIssue( code="INSUFFICIENT_STATIONS", message=( "TrainRun has fewer than two valid " "station calls." ), train_no=train_no, ) ) continue first = calls[0] last = calls[-1] service_name = str( group["TrainName"].iloc[0] ).strip() runs.append( TrainRun( id=f"RUN:{train_no}", train_no=train_no, service_name=service_name, direction=direction, train_length_m=train_length_m, train_weight_t=train_weight_t, station_calls=tuple(calls), earliest_departure_min=( first.baseline_departure_min ), latest_arrival_min=( last.baseline_arrival_min ), ) ) return runs, issues نکته مهم درباره mapping در این نسخه: TrainNo → TrainRun TrainName → service_name Sequence → ترتیب TrainStationCall time_in → arrival baseline time_take → dwell time_out → برای reconciliation قابل استفاده است، اما فعلاً schedule را از time_in + time_take می‌سازیم RequiredWait → minimum operational wait Kilometerage → chainage seir → running time تا station بعدی Distance → فعلاً source-only و وارد محاسبه capacity نمی‌شود sumDistancezz → unknown/source-only MaxSpeed → فعلاً untrusted/provisional 8. CP-SAT Scheduling Engine app/scheduler.py Python from __future__ import annotations from collections import defaultdict from ortools.sat.python import cp_model from .domain import ( Direction, MovementSchedule, Schedule, SchedulingProblem, StationSchedule, TrainSchedule, ) from .infrastructure import resource_id class CpSatScheduler: def __init__( self, max_time_seconds: float = 30.0, num_workers: int = 1, random_seed: int = 1, ): self.max_time_seconds = max_time_seconds self.num_workers = num_workers self.random_seed = random_seed def solve( self, problem: SchedulingProblem, ) -> Schedule | None: model = cp_model.CpModel() horizon = problem.planning_horizon_min arrival_vars = {} departure_vars = {} block_entry_vars = {} block_exit_vars = {} block_clear_vars = {} station_track_presence = {} resource_intervals = defaultdict(list) train_by_id = { train.id: train for train in problem.train_runs } # -------------------------------------------------- # Station variables # -------------------------------------------------- for train in problem.train_runs: for call in train.station_calls: key = (train.id, call.sequence) arrival = model.new_int_var( 0, horizon, f"arr_{train.id}_{call.sequence}", ) departure = model.new_int_var( 0, horizon, f"dep_{train.id}_{call.sequence}", ) arrival_vars[key] = arrival departure_vars[key] = departure model.add( departure >= arrival ) model.add( departure >= arrival + call.dwell_min ) model.add( departure >= arrival + call.required_wait_min ) first_key = ( train.id, train.station_calls[0].sequence, ) if train.earliest_departure_min is not None: model.add( departure_vars[first_key] >= train.earliest_departure_min ) last_key = ( train.id, train.station_calls[-1].sequence, ) if train.latest_arrival_min is not None: model.add( arrival_vars[last_key] <= train.latest_arrival_min ) # -------------------------------------------------- # Station track assignment # -------------------------------------------------- for train in problem.train_runs: for call in train.station_calls: station = problem.stations[ call.station_id ] if not station.tracks: continue eligible_tracks = [] for track in station.tracks: if ( track.direction is not None and track.direction != train.direction ): continue eligible_tracks.append(track) if not eligible_tracks: raise ValueError( f"No eligible station track for " f"{train.id} at {station.id}." ) presence_vars = [] for track in eligible_tracks: presence = model.new_bool_var( f"track_{train.id}_" f"{call.sequence}_{track.id}" ) presence_vars.append(presence) interval = ( model.new_optional_interval_var( arrival_vars[ (train.id, call.sequence) ], ( departure_vars[ (train.id, call.sequence) ] - arrival_vars[ (train.id, call.sequence) ] ), departure_vars[ (train.id, call.sequence) ], presence, f"station_track_" f"{train.id}_{call.sequence}_" f"{track.id}", ) ) station_track_presence[ ( train.id, call.sequence, track.id, ) ] = presence resource_intervals[ f"STATION_TRACK:{track.id}" ].append(interval) model.add_exactly_one( presence_vars ) # -------------------------------------------------- # Block movements # -------------------------------------------------- movement_meta = {} for train in problem.train_runs: path = problem.paths[train.id] for segment in path.segments: block = problem.blocks[ segment.physical_block_id ] entry = model.new_int_var( 0, horizon, f"entry_{segment.id}", ) exit_var = model.new_int_var( 0, horizon, f"exit_{segment.id}", ) clear = model.new_int_var( 0, horizon, f"clear_{segment.id}", ) block_entry_vars[segment.id] = entry block_exit_vars[segment.id] = exit_var block_clear_vars[segment.id] = clear model.add( exit_var >= entry + segment.baseline_running_time_min ) # Minimum clearing time. model.add( clear >= exit_var ) from_call = train.station_calls[ segment.sequence - 1 ] to_call = train.station_calls[ segment.sequence ] model.add( entry >= departure_vars[ ( train.id, from_call.sequence, ) ] ) model.add( arrival_vars[ ( train.id, to_call.sequence, ) ] >= clear ) movement_resource = resource_id( block, train.direction, ) interval = model.new_interval_var( entry, clear - entry, clear, f"block_{segment.id}", ) resource_intervals[ f"BLOCK:{movement_resource}" ].append(interval) movement_meta[segment.id] = ( train, segment, movement_resource, ) # -------------------------------------------------- # Resource NoOverlap # -------------------------------------------------- for intervals in resource_intervals.values(): if intervals: model.add_no_overlap(intervals) # -------------------------------------------------- # Headway / switch-time # -------------------------------------------------- for resource_key, intervals in resource_intervals.items(): if not resource_key.startswith("BLOCK:"): continue movements = [ sid for sid in movement_meta if ( f"BLOCK:{movement_meta[sid][2]}" == resource_key ) ] for i in range(len(movements)): for j in range(i + 1, len(movements)): sid_a = movements[i] sid_b = movements[j] train_a, seg_a, _ = movement_meta[sid_a] train_b, seg_b, _ = movement_meta[sid_b] # Same train cannot have overlapping # physical movements anyway. if train_a.id == train_b.id: continue block = problem.blocks[ seg_a.physical_block_id ] gap = block.headway_min if ( block.track_type.value == "SINGLE" and train_a.direction != train_b.direction ): gap = max( gap, block.switch_time_min, ) order = model.new_bool_var( f"order_{sid_a}_{sid_b}" ) model.add( block_clear_vars[sid_a] + gap <= block_entry_vars[sid_b] ).only_enforce_if(order) model.add( block_clear_vars[sid_b] + gap <= block_entry_vars[sid_a] ).only_enforce_if(order.Not()) # -------------------------------------------------- # Operational windows # -------------------------------------------------- for window in problem.operational_windows: if window.window_type.value != "CLOSED": continue resource_key = ( f"BLOCK:{window.resource_id}" ) for segment_id, ( train, segment, movement_resource, ) in movement_meta.items(): if ( f"BLOCK:{movement_resource}" != resource_key ): continue before = model.new_bool_var( f"before_window_{segment_id}_" f"{window.id}" ) model.add( block_clear_vars[segment_id] <= window.start_min ).only_enforce_if(before) model.add( block_entry_vars[segment_id] >= window.end_min ).only_enforce_if(before.Not()) # -------------------------------------------------- # Objective # -------------------------------------------------- if problem.minimize_baseline_deviation: deviations = [] for train in problem.train_runs: for call in train.station_calls: key = ( train.id, call.sequence, ) deviation_arrival = ( model.new_int_var( 0, horizon, f"dev_arr_{train.id}_" f"{call.sequence}", ) ) model.add_abs_equality( deviation_arrival, arrival_vars[key] - call.baseline_arrival_min, ) deviations.append( deviation_arrival ) deviation_departure = ( model.new_int_var( 0, horizon, f"dev_dep_{train.id}_" f"{call.sequence}", ) ) model.add_abs_equality( deviation_departure, departure_vars[key] - call.baseline_departure_min, ) deviations.append( deviation_departure ) model.minimize( sum(deviations) ) # -------------------------------------------------- # Solve # -------------------------------------------------- solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = ( self.max_time_seconds ) solver.parameters.num_search_workers = ( self.num_workers ) solver.parameters.random_seed = ( self.random_seed ) status = solver.solve(model) if status not in ( cp_model.OPTIMAL, cp_model.FEASIBLE, ): return None schedule = Schedule( objective_value=solver.objective_value, solver_status=solver.status_name(status), ) for train in problem.train_runs: train_schedule = TrainSchedule( train_run_id=train.id ) for call in train.station_calls: key = ( train.id, call.sequence, ) assigned_track = None station = problem.stations[ call.station_id ] for track in station.tracks: presence = station_track_presence.get( ( train.id, call.sequence, track.id, ) ) if ( presence is not None and solver.value(presence) == 1 ): assigned_track = track.id break train_schedule.stations.append( StationSchedule( train_run_id=train.id, station_id=call.station_id, sequence=call.sequence, arrival_min=solver.value( arrival_vars[key] ), departure_min=solver.value( departure_vars[key] ), assigned_track_id=assigned_track, ) ) path = problem.paths[train.id] for segment in path.segments: block = problem.blocks[ segment.physical_block_id ] movement_resource = resource_id( block, train.direction, ) train_schedule.movements.append( MovementSchedule( train_run_id=train.id, segment_id=segment.id, entry_min=solver.value( block_entry_vars[ segment.id ] ), exit_min=solver.value( block_exit_vars[ segment.id ] ), clear_min=solver.value( block_clear_vars[ segment.id ] ), from_station_id=( segment.from_station_id ), to_station_id=( segment.to_station_id ), physical_block_id=( segment.physical_block_id ), resource_id=movement_resource, ) ) schedule.trains.append( train_schedule ) return schedule 9. Independent Validator اصل مهم اینجا: Validator نباید صرفاً خروجی Solver را trust کند. app/validator.py Python from __future__ import annotations from collections import defaultdict from .domain import ( Schedule, SchedulingProblem, ValidationIssue, ValidationResult, ) from .infrastructure import resource_id class IndependentValidator: def validate( self, problem: SchedulingProblem, schedule: Schedule, ) -> ValidationResult: issues: list[ValidationIssue] = [] train_map = { train.id: train for train in problem.train_runs } schedule_map = { train.train_run_id: train for train in schedule.trains } # -------------------------------------------------- # Train-level checks # -------------------------------------------------- for train in problem.train_runs: if train.id not in schedule_map: issues.append( ValidationIssue( code="MISSING_TRAIN", message=( "Train has no generated schedule." ), train_run_id=train.id, ) ) continue generated = schedule_map[train.id] station_map = { x.sequence: x for x in generated.stations } if len(generated.stations) != len( train.station_calls ): issues.append( ValidationIssue( code="STATION_COUNT", message=( "Generated station count does " "not match canonical path." ), train_run_id=train.id, ) ) # -------------------------------------------------- # Station timing # -------------------------------------------------- for call in train.station_calls: current = station_map.get( call.sequence ) if current is None: continue if ( current.departure_min < current.arrival_min + call.dwell_min ): issues.append( ValidationIssue( code="DWELL", message=( "Departure violates minimum " "dwell." ), train_run_id=train.id, ) ) if ( current.departure_min < current.arrival_min + call.required_wait_min ): issues.append( ValidationIssue( code="REQUIRED_WAIT", message=( "Departure violates " "RequiredWait." ), train_run_id=train.id, ) ) # -------------------------------------------------- # Movement timing # -------------------------------------------------- movements = sorted( generated.movements, key=lambda x: x.entry_min, ) for movement in movements: segment = next( ( s for s in problem.paths[train.id].segments if s.id == movement.segment_id ), None, ) if segment is None: issues.append( ValidationIssue( code="UNKNOWN_SEGMENT", message=( f"Unknown segment " f"{movement.segment_id}." ), train_run_id=train.id, ) ) continue if ( movement.exit_min < movement.entry_min + segment.baseline_running_time_min ): issues.append( ValidationIssue( code="RUNNING_TIME", message=( "Block running time is " "violated." ), train_run_id=train.id, resource_id=( movement.resource_id ), ) ) if movement.clear_min < movement.exit_min: issues.append( ValidationIssue( code="CLEAR_TIME", message=( "Block clear time is " "before exit time." ), train_run_id=train.id, ) ) if ( train.earliest_departure_min is not None and generated.stations ): first = generated.stations[0] if ( first.departure_min < train.earliest_departure_min ): issues.append( ValidationIssue( code="EARLIEST_DEPARTURE", message=( "Train departs before " "earliest departure." ), train_run_id=train.id, ) ) if ( train.latest_arrival_min is not None and generated.stations ): last = generated.stations[-1] if ( last.arrival_min > train.latest_arrival_min ): issues.append( ValidationIssue( code="LATEST_ARRIVAL", message=( "Train arrives after " "latest allowed arrival." ), train_run_id=train.id, ) ) # -------------------------------------------------- # Train length vs station track # -------------------------------------------------- for station_event in generated.stations: if station_event.assigned_track_id is None: continue station = problem.stations[ station_event.station_id ] track = next( ( t for t in station.tracks if t.id == station_event.assigned_track_id ), None, ) if track is None: issues.append( ValidationIssue( code="UNKNOWN_TRACK", message=( "Assigned station track " "does not exist." ), train_run_id=train.id, ) ) continue if ( train.train_length_m > track.usable_length_m ): issues.append( ValidationIssue( code="TRAIN_TOO_LONG", message=( "Train length exceeds " "usable station track." ), train_run_id=train.id, resource_id=track.id, ) ) # -------------------------------------------------- # Block conflicts # -------------------------------------------------- resource_movements = defaultdict(list) for generated in schedule.trains: for movement in generated.movements: resource_movements[ movement.resource_id ].append(movement) for resource, movements in resource_movements.items(): movements.sort( key=lambda x: x.entry_min ) for previous, current in zip( movements, movements[1:], ): block = problem.blocks[ previous.physical_block_id ] if current.entry_min < previous.clear_min: issues.append( ValidationIssue( code="BLOCK_OVERLAP", message=( "Two movements overlap " "on the same physical resource." ), train_run_id=( current.train_run_id ), resource_id=resource, ) ) gap = block.headway_min train_a = train_map[ previous.train_run_id ] train_b = train_map[ current.train_run_id ] if ( block.track_type.value == "SINGLE" and train_a.direction != train_b.direction ): gap = max( gap, block.switch_time_min, ) if ( current.entry_min < previous.clear_min + gap ): issues.append( ValidationIssue( code="HEADWAY", message=( "Required headway/switch " "time is violated." ), train_run_id=( current.train_run_id ), resource_id=resource, ) ) # -------------------------------------------------- # Station track conflicts # -------------------------------------------------- station_events = defaultdict(list) for generated in schedule.trains: for event in generated.stations: if event.assigned_track_id is None: continue station_events[ event.assigned_track_id ].append(event) for track_id, events in station_events.items(): events.sort( key=lambda x: x.arrival_min ) for previous, current in zip( events, events[1:], ): if ( current.arrival_min < previous.departure_min ): issues.append( ValidationIssue( code="STATION_TRACK_OVERLAP", message=( "Station track is occupied " "by overlapping trains." ), train_run_id=( current.train_run_id ), resource_id=track_id, ) ) return ValidationResult( feasible=not issues, issues=issues, ) 10. Capacity Search و Capacity Proof اینجا تعریف رسمی قبلی را اجرایی می‌کنیم: C r ​ =max{F:Schedule(F) is feasible} و مهم‌تر: F feasible و F+1 infeasible باید هر دو اثبات شوند. app/capacity.py Python from __future__ import annotations from collections.abc import Callable from .domain import ( CapacityProof, SchedulingProblem, ) from .scheduler import CpSatScheduler from .validator import IndependentValidator class CapacitySearcher: def __init__( self, scheduler: CpSatScheduler, validator: IndependentValidator, ): self.scheduler = scheduler self.validator = validator def is_feasible( self, problem: SchedulingProblem, ) -> bool: schedule = self.scheduler.solve(problem) if schedule is None: return False validation = self.validator.validate( problem, schedule, ) return validation.feasible def search( self, problem_factory: Callable[[int], SchedulingProblem], lower_bound: int, upper_bound: int, ) -> CapacityProof: if lower_bound < 0: raise ValueError( "lower_bound cannot be negative." ) if upper_bound < lower_bound: raise ValueError( "upper_bound must be >= lower_bound." ) low = lower_bound high = upper_bound last_feasible = None while low <= high: mid = (low + high) // 2 problem = problem_factory(mid) feasible = self.is_feasible(problem) if feasible: last_feasible = mid low = mid + 1 else: high = mid - 1 if last_feasible is None: return CapacityProof( requested_flow=lower_bound, feasible=False, independently_validated=False, capacity=None, explanation=( "No feasible flow was found " "within the requested search range." ), ) capacity = last_feasible capacity_problem = problem_factory( capacity ) capacity_schedule = self.scheduler.solve( capacity_problem ) if capacity_schedule is None: return CapacityProof( requested_flow=capacity, feasible=False, independently_validated=False, capacity=None, explanation=( "Solver could not reproduce " "the capacity result." ), ) validation = self.validator.validate( capacity_problem, capacity_schedule, ) if not validation.feasible: return CapacityProof( requested_flow=capacity, feasible=True, independently_validated=False, capacity=None, explanation=( "Solver found the flow feasible, " "but independent validation failed." ), ) next_flow = capacity + 1 next_problem = problem_factory( next_flow ) next_feasible = self.is_feasible( next_problem ) if next_feasible: return CapacityProof( requested_flow=capacity, feasible=True, independently_validated=True, next_flow=next_flow, next_flow_feasible=True, capacity=None, explanation=( "Search boundary is not proven: " "capacity+1 is still feasible." ), ) return CapacityProof( requested_flow=capacity, feasible=True, independently_validated=True, next_flow=next_flow, next_flow_feasible=False, capacity=capacity, explanation=( f"Capacity proven at F={capacity}: " f"F is feasible and F+1 is infeasible." ), ) 11. Engine اصلی app/engine.py Python from __future__ import annotations from dataclasses import dataclass from .domain import ( Schedule, SchedulingProblem, ValidationResult, ) from .scheduler import CpSatScheduler from .validator import IndependentValidator @dataclass class EngineResult: schedule: Schedule | None validation: ValidationResult feasible: bool explanation: str class CapacityEngine: def __init__( self, scheduler: CpSatScheduler | None = None, validator: IndependentValidator | None = None, ): self.scheduler = ( scheduler or CpSatScheduler() ) self.validator = ( validator or IndependentValidator() ) def run( self, problem: SchedulingProblem, ) -> EngineResult: schedule = self.scheduler.solve( problem ) if schedule is None: return EngineResult( schedule=None, validation=ValidationResult( feasible=False, issues=[], ), feasible=False, explanation=( "No feasible schedule was found " "by CP-SAT." ), ) validation = self.validator.validate( problem, schedule, ) if validation.feasible: explanation = ( "Generated schedule is feasible " "and independently validated." ) else: explanation = ( "Solver generated a schedule, " "but independent validation found " "violations." ) return EngineResult( schedule=schedule, validation=validation, feasible=validation.feasible, explanation=explanation, ) 12. Demo واقعی از Single Track + Crossing این demo عمداً کوچک است، اما یک نکته مهم را اثبات می‌کند: A ---- SINGLE ---- B ---- SINGLE ---- C Train 1: A → C Train 2: C → A قطارها نمی‌توانند هم‌زمان وارد block مشترک شوند و باید در station B مدیریت شوند. app/demo.py Python from .domain import ( Direction, PhysicalBlock, SchedulingProblem, Station, StationTrack, TrackType, TrainRun, TrainStationCall, ) from .engine import CapacityEngine from .infrastructure import ( build_paths, canonical_physical_block_id, ) def build_demo_problem() -> SchedulingProblem: stations = {} for station_id in ["A", "B", "C"]: stations[station_id] = Station( id=station_id, name=station_id, tracks=( StationTrack( id=f"{station_id}-T1", station_id=station_id, usable_length_m=800, ), StationTrack( id=f"{station_id}-T2", station_id=station_id, usable_length_m=800, ), ), ) block_ab_id = canonical_physical_block_id( "A", "B", ) block_bc_id = canonical_physical_block_id( "B", "C", ) blocks = { block_ab_id: PhysicalBlock( id=block_ab_id, station_a="A", station_b="B", length_m=10_000, track_type=TrackType.SINGLE, running_time_forward_min=20, running_time_reverse_min=20, headway_min=5, switch_time_min=10, ), block_bc_id: PhysicalBlock( id=block_bc_id, station_a="B", station_b="C", length_m=12_000, track_type=TrackType.SINGLE, running_time_forward_min=25, running_time_reverse_min=25, headway_min=5, switch_time_min=10, ), } train_1 = TrainRun( id="RUN:100", train_no="100", service_name="A-C", direction=Direction.FORWARD, train_length_m=500, train_weight_t=1500, station_calls=( TrainStationCall( station_id="A", sequence=1, baseline_arrival_min=0, baseline_departure_min=0, dwell_min=0, required_wait_min=0, baseline_running_time_to_next_min=20, ), TrainStationCall( station_id="B", sequence=2, baseline_arrival_min=20, baseline_departure_min=25, dwell_min=5, required_wait_min=5, baseline_running_time_to_next_min=25, ), TrainStationCall( station_id="C", sequence=3, baseline_arrival_min=50, baseline_departure_min=50, dwell_min=0, required_wait_min=0, ), ), earliest_departure_min=0, latest_arrival_min=100, ) train_2 = TrainRun( id="RUN:101", train_no="101", service_name="C-A", direction=Direction.REVERSE, train_length_m=500, train_weight_t=1500, station_calls=( TrainStationCall( station_id="C", sequence=1, baseline_arrival_min=0, baseline_departure_min=0, dwell_min=0, required_wait_min=0, baseline_running_time_to_next_min=25, ), TrainStationCall( station_id="B", sequence=2, baseline_arrival_min=25, baseline_departure_min=30, dwell_min=5, required_wait_min=5, baseline_running_time_to_next_min=20, ), TrainStationCall( station_id="A", sequence=3, baseline_arrival_min=55, baseline_departure_min=55, dwell_min=0, required_wait_min=0, ), ), earliest_departure_min=0, latest_arrival_min=120, ) trains = [ train_1, train_2, ] paths = build_paths( trains, blocks, ) return SchedulingProblem( stations=stations, blocks=blocks, train_runs=trains, paths=paths, planning_horizon_min=240, ) def main() -> None: problem = build_demo_problem() engine = CapacityEngine() result = engine.run(problem) print( "FEASIBLE:", result.feasible, ) print( "EXPLANATION:", result.explanation, ) if result.schedule is None: return for train in result.schedule.trains: print() print( f"TRAIN {train.train_run_id}" ) for station in train.stations: print( " STATION", station.station_id, "ARR", station.arrival_min, "DEP", station.departure_min, "TRACK", station.assigned_track_id, ) for movement in train.movements: print( " BLOCK", movement.physical_block_id, movement.entry_min, movement.exit_min, movement.clear_min, ) if __name__ == "__main__": main() 13. Testهای اصلی tests/test_core.py Python from app.demo import build_demo_problem from app.engine import CapacityEngine from app.infrastructure import canonical_physical_block_id from app.time_utils import ( normalize_time_sequence, parse_clock, ) def test_parse_clock(): assert parse_clock("09:00") == 540 assert parse_clock("23:46") == 1426 def test_midnight_rollover(): values = normalize_time_sequence( [ "23:46", "00:36", "00:56", "02:19", ] ) assert values == [ 1426, 1476, 1496, 1579, ] def test_physical_block_is_direction_independent(): assert ( canonical_physical_block_id( "A", "B", ) == canonical_physical_block_id( "B", "A", ) ) def test_demo_schedule_is_feasible(): problem = build_demo_problem() engine = CapacityEngine() result = engine.run(problem) assert result.feasible is True assert result.schedule is not None assert result.validation.feasible is True def test_single_track_has_no_overlap(): problem = build_demo_problem() engine = CapacityEngine() result = engine.run(problem) assert result.schedule is not None movements = [] for train in result.schedule.trains: movements.extend( train.movements ) for i in range(len(movements)): for j in range(i + 1, len(movements)): a = movements[i] b = movements[j] if ( a.resource_id != b.resource_id ): continue overlap = ( a.entry_min < b.clear_min and b.entry_min < a.clear_min ) assert not overlap 14. اجرای پروژه در root پروژه: Bash python -m pip install -r requirements.txt سپس: Bash pytest -q و Demo: Bash python -m app.demo خروجی مفهومی چیزی شبیه این خواهد بود: FEASIBLE: True EXPLANATION: Generated schedule is feasible and independently validated. TRAIN RUN:100 STATION A ARR 0 DEP 0 TRACK A-T1 STATION B ARR ... STATION C ARR ... BLOCK BLOCK:A:B ... BLOCK BLOCK:B:C ... TRAIN RUN:101 STATION C ARR 0 DEP 0 TRACK C-T1 STATION B ARR ... STATION A ARR ... BLOCK BLOCK:B:C ... BLOCK BLOCK:A:B ... مقادیر دقیق schedule را Solver تعیین می‌کند و نباید به‌صورت hard-coded فرض شوند. 15. اتصال به Access واقعی شما برای دیتابیس واقعی: Python from app.source import AccessAdapter from app.mapper import map_access_train_runs from app.infrastructure import build_paths adapter = AccessAdapter( "aaa.accdb" ) tables = adapter.list_tables() print(tables) df = adapter.extract_table( tables[0] ) runs, issues = map_access_train_runs( df, train_length_m=500, train_weight_t=1500, ) for issue in issues: print(issue) for run in runs: print( run.train_no, run.service_name, run.direction, len(run.station_calls), ) اما یک مرحله‌ی بسیار مهم هنوز باقی است: Access TrainMovement │ ▼ Canonical TrainRun │ ▼ Infrastructure Master │ ├── Station ├── StationTrack ├── PhysicalBlock ├── TrackType ├── Junction └── OperationalWindow یعنی از روی Access نباید فرض کنیم که: Kilometerage ↓ PhysicalBlock ↓ TrackType این تبدیل باید از Infrastructure Master Data بیاید. 16. Mapping دقیق داده واقعی شما برای داده‌ای که قبلاً بررسی کردیم، mapping اجرایی باید نهایتاً به این شکل ثبت شود: Access Field Canonical Field وضعیت ID TrainStationCall.source_record_id Verified TrainNo TrainRun.train_no Verified TrainName TrainRun.service_name Verified StationName Station.name Verified StationNumber Station.source_number Verified Sequence TrainStationCall.sequence Verified time_in baseline_arrival Verified time_take dwell Verified time_out source departure evidence Verified RequiredWait required_wait Provisional Kilometerage chainage High confidence MaxSpeed max_speed Provisional Distance source distance Do not trust yet sumDistancezz source field Unknown seir baseline_running_time_to_next Strongly verified و نکته بسیار مهم: seir نباید در مدل نهایی به شکل: Python station.seir ذخیره شود. بلکه: Python TrainStationCall.baseline_running_time_to_next_min یا در مدل route: Python RouteSegment.baseline_running_time_min باشد. 17. چیزی که این Version 0.9 واقعاً حل می‌کند این نسخه دیگر صرفاً یک calculation prototype نیست. زنجیره‌ی زیر را executable می‌کند: TrainRun ↓ Station Calls ↓ Directed Path ↓ Physical Blocks ↓ Track Resources ↓ Temporal Constraints ↓ Single/Double Track Logic ↓ Headway ↓ Switch Time ↓ Station Track Assignment ↓ CP-SAT ↓ Generated Timetable ↓ Independent Validation ↓ Operational Feasibility و مهم‌تر از همه: Capacity Number ↓ Feasible Schedule ↓ Independent Validation ↓ Capacity Proof بنابراین دیگر اجازه نداریم مثلاً بگوییم: Cr = 76 صرفاً چون یک formula عدد 76 تولید کرده است. باید بتوانیم نشان دهیم: F = 76 ↓ Schedule exists ↓ Schedule validated ↓ All resources feasible ↓ F = 77 ↓ No feasible schedule 18. شکاف‌های باقی‌مانده برای Version 1.0 این بخش به نظرم مهم‌تر از اضافه کردن featureهای ظاهری است. قدم بعدی باید این موارد باشد: 1. Real Infrastructure Master Station StationTrack PhysicalBlock Junction Signal Route RouteSegment با topology واقعی شبکه. 2. Baseline Schedule واقعی از Excel فعلی: شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت ساعت ورود مقصد شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت ساعت رسیدن مبدا باید OperatingPattern و BaselineSchedule واقعی ساخته شود. 3. Train Formation در این مرحله باید این بخش وارد Solver شود: Freight Flow ↓ Wagon Requirement ↓ Wagon Formation ↓ Train Length ↓ Train Weight ↓ Locomotive Assignment ↓ TrainRun 4. Crossing Station Logic به‌خصوص برای Single Track: Block A-B ↓ Station B ├── Track 1 ├── Track 2 └── Crossing Capability ↓ Block B-C این قسمت باید به شکل واقعی در solver مدل شود، نه صرفاً با NoOverlap ساده. 5. Junction Conflict Matrix به جای: NoOverlap(all junction movements) باید: Movement A ↔ Movement B = conflict Movement A ↔ Movement C = no conflict Movement B ↔ Movement D = conflict مدل شود. 6. Wagon Cycle Loaded O → D ↓ Unload ↓ Empty D → O ↓ Buffer ↓ Reload و ظرفیت شبکه دیگر فقط تابع Track Capacity نباشد: C network ​ =f(Infrastructure,Schedule,WagonCycle,LocomotiveCycle,Station,Terminal,Buffer,Demand) 7. Marketplace در نهایت: Marketplace Request ↓ Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Capacity Engine ↓ Feasible Capacity ↓ Allocation ↓ Marketplace نقطه‌ی بسیار مهم برای مرحله بعد از اینجا به بعد، پیشنهاد فنی من این است که دیگر feature جدید اضافه نکنیم تا یکپارچگی داده واقعی کامل شود. مرحله‌ی بعد باید تبدیل این هسته به: Version 1.0 — Real Iranian Railway Vertical Slice باشد؛ یعنی یک OD واقعی از داده‌های Access/Excel شما را از ابتدا تا انتها عبور دهیم: Access ↓ Train 100 / 101 ↓ Station Calls ↓ seir + Kilometerage ↓ Infrastructure Master ↓ Directed Route ↓ Single/Double Track ↓ Crossing ↓ Station Track ↓ Baseline Schedule ↓ CP-SAT ↓ Validated Schedule ↓ Route Capacity ↓ Capacity Proof ↓ Bottleneck Explanation و بعد از اینکه این Vertical Slice روی داده واقعی درست شد، Wagon Cycle + Locomotive Cycle + Train Formation + Marketplace Allocation را روی همان هسته اضافه کنیم. این ترتیب باعث می‌شود موتور از همان ابتدا روی داده واقعی ایران ساخته شود، نه اینکه یک Solver جداگانه بسازیم و بعداً تلاش کنیم داده‌های عملیاتی را به آن تحمیل کنیم. --- ## ادامه 28 — پرسش کاربر (`message-id:4ee02948-bdea-46a1-aafd-3e1d8664593a`) بله لطفا --- ## ادامه 28 — پاسخ دستیار (`message-id:047cb378-4b69-4ad5-9868-5945c5165907`) حتماً. حالا وارد Version 1.0 — Real Iranian Railway Vertical Slice می‌شویم. این مرحله تفاوت مهمی با نسخه 0.9 دارد: هدف دیگر صرفاً اثبات Solver نیست؛ هدف این است که داده‌ی واقعی Access/Excel شما بدون دستکاری معنایی، وارد Canonical Model شود و تا Capacity Proof پیش برود. در این نسخه سه اصل را تثبیت می‌کنیم: Train 100 / 101 و سایر TrainRunهای واقعی از Access مستقیماً به Canonical Model می‌روند. PhysicalBlock / TrackType / StationTrack فقط از Infrastructure Master می‌آیند، نه از حدس روی داده عملیاتی. خروجی Capacity فقط زمانی معتبر است که Schedule + Independent Validation + Capacity Proof هر سه وجود داشته باشند. Version 1.0 — معماری اجرایی ┌─────────────────────┐ │ Access / Excel │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Raw / Staging Layer │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Mapping + Quality │ └──────────┬──────────┘ │ ┌──────────▼──────────┐ │ Canonical Model │ └──────────┬──────────┘ │ ┌──────────────────┴──────────────────┐ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Operational Data │ │ Infrastructure │ │ TrainStationCall │ │ Master Data │ │ seir │ │ Station │ │ Kilometerage │ │ Track │ │ time_in/out │ │ Block │ └────────┬─────────┘ │ Junction │ │ └────────┬─────────┘ └────────────────┬──────────────────┘ ▼ ┌─────────────────────┐ │ Directed Train Path │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Baseline Schedule │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ CP-SAT Scheduling │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Generated Schedule │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Independent │ │ Validation │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Capacity Search │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Capacity Proof │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Bottleneck + Why │ └─────────────────────┘ 1. اولین تغییر: Infrastructure Master در Version 0.9 امکان داشت topology را خیلی ساده داخل Demo بسازیم. در Version 1.0 این کار ممنوع است. مدل جدید: Python # app/infrastructure_master.py from dataclasses import dataclass from enum import Enum from typing import Optional class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" @dataclass(frozen=True) class StationTrack: id: str station_id: str usable_length_m: int direction: Optional[str] = None crossing_allowed: bool = True overtaking_allowed: bool = False @dataclass(frozen=True) class StationMaster: id: str name: str station_number: Optional[str] tracks: tuple[StationTrack, ...] @dataclass(frozen=True) class BlockMaster: id: str station_a_id: str station_b_id: str length_m: int track_type: TrackType running_time_forward_min: int running_time_reverse_min: int headway_same_direction_min: int = 3 switch_time_opposite_direction_min: int = 5 @dataclass(frozen=True) class JunctionMovement: movement_id: str junction_id: str @dataclass(frozen=True) class JunctionConflict: junction_id: str movement_a: str movement_b: str separation_min: int = 2 @dataclass class InfrastructureMaster: stations: dict[str, StationMaster] blocks: dict[str, BlockMaster] junction_movements: list[JunctionMovement] junction_conflicts: list[JunctionConflict] این مدل عمداً با TrainStationCall جداست. یعنی: TrainStationCall = what actually happened / baseline operational evidence StationMaster = what infrastructure physically allows این تفکیک برای پروژه شما حیاتی است. 2. Physical Block Identity یک اشتباه رایج این است که این دو را دو block مستقل در نظر بگیریم: Gar → Sakheh Sakheh → Gar در مدل فیزیکی، اگر خط Single Track باشد: Physical Block A ↔ B / \ A → B B → A پس: Python def physical_block_id( station_a: str, station_b: str, ) -> str: a, b = sorted( [station_a, station_b] ) return f"BLOCK:{a}:{b}" و Resource: Python def movement_resource( block: BlockMaster, direction: str, ) -> str: if block.track_type == TrackType.SINGLE: return block.id return f"{block.id}:{direction}" بنابراین: Single Track Train A → B Train B → A SAME RESOURCE │ ▼ BLOCK:A:B Double Track Train A → B ↓ BLOCK:A:B:FORWARD Train B → A ↓ BLOCK:A:B:REVERSE این دقیقاً همان distinction است که برای capacity calculation لازم داریم. 3. تبدیل Access واقعی به Canonical Model برای Access شما، Pipeline باید این باشد: Python df = access.extract_table( "YourTable" ) runs, mapping_issues = map_access_train_runs( df, train_length_m=500, train_weight_t=1500, ) اما در Version 1.0 یک Data Quality Gate اضافه می‌کنیم. Python # app/data_quality.py from dataclasses import dataclass @dataclass class QualityIssue: code: str severity: str message: str train_no: str | None = None sequence: int | None = None class DataQualityGate: def validate_train_run( self, train_run, ) -> list[QualityIssue]: issues = [] calls = list( train_run.station_calls ) if len(calls) < 2: issues.append( QualityIssue( code="LESS_THAN_TWO_STATIONS", severity="ERROR", message=( "TrainRun must contain " "at least two station calls." ), train_no=train_run.train_no, ) ) return issues previous_sequence = 0 for call in calls: if call.sequence <= previous_sequence: issues.append( QualityIssue( code="INVALID_SEQUENCE", severity="ERROR", message=( "Station sequence is not " "strictly increasing." ), train_no=train_run.train_no, sequence=call.sequence, ) ) previous_sequence = call.sequence if ( call.baseline_departure_min < call.baseline_arrival_min ): issues.append( QualityIssue( code="DEPARTURE_BEFORE_ARRIVAL", severity="ERROR", message=( "Departure occurs before " "arrival." ), train_no=train_run.train_no, sequence=call.sequence, ) ) if call.dwell_min < 0: issues.append( QualityIssue( code="NEGATIVE_DWELL", severity="ERROR", message="Negative dwell time.", train_no=train_run.train_no, sequence=call.sequence, ) ) return issues def validate_all( self, train_runs, ) -> list[QualityIssue]: issues = [] for train_run in train_runs: issues.extend( self.validate_train_run( train_run ) ) return issues 4. یک نکته مهم درباره time_out در داده واقعی شما: time_in time_take time_out رابطه‌ای شبیه این داریم: time_out=time_in+time_take اما به دلیل midnight باید روی absolute minutes کار کنیم. مثلاً: 23:46 + 50 min = 00:36 در Canonical Model نباید این را به صورت: 00:36 < 23:46 تفسیر کنیم. بلکه: 23:46 = 1426 00:36 next day = 1476 است. بنابراین داده canonical: Python arrival_min = 1426 departure_min = 1476 خواهد بود. 5. seir در Version 1.0 این field اکنون در pipeline به شکل زیر وارد می‌شود: TrainStationCall │ ├── station_id ├── arrival ├── dwell ├── departure │ └── baseline_running_time_to_next مثلاً: Gar 09:00 │ │ seir = 34 ▼ Sakheh 09:34 پس: T run,i ​ =seir i ​ و در absence of another validated running-time model: Python segment.baseline_running_time_min = ( current_call .baseline_running_time_to_next_min ) این برای Vertical Slice فعلی بسیار مناسب است. بعداً می‌توانیم آن را به: T run ​ =f(Distance,Gradient,SpeedProfile,TrainType,LoadState,Direction) ارتقا دهیم. 6. ساخت Directed Train Path حالا از Access data و Infrastructure Master با هم استفاده می‌کنیم: Python def build_train_path( train_run, infrastructure, ): segments = [] calls = list( train_run.station_calls ) for i in range(len(calls) - 1): current = calls[i] nxt = calls[i + 1] block_id = physical_block_id( current.station_id, nxt.station_id, ) block = infrastructure.blocks.get( block_id ) if block is None: raise ValueError( f"Missing infrastructure block " f"{block_id}" ) running_time = ( current .baseline_running_time_to_next_min ) if running_time is None: running_time = ( block.running_time_forward_min if train_run.direction.value == "FORWARD" else block.running_time_reverse_min ) segments.append( { "sequence": i + 1, "from_station": current.station_id, "to_station": nxt.station_id, "physical_block_id": block.id, "direction": train_run.direction, "baseline_running_time_min": running_time, } ) return segments 7. Baseline Schedule واقعی اکنون یک مفهوم مهم اضافه می‌کنیم: Baseline Schedule یعنی schedule موجود در Railway Operation، نه schedule ساخته‌شده توسط Solver. مدل: Python from dataclasses import dataclass @dataclass(frozen=True) class BaselineStationEvent: train_run_id: str station_id: str sequence: int arrival_min: int departure_min: int @dataclass(frozen=True) class BaselineMovement: train_run_id: str segment_id: str entry_min: int exit_min: int clear_min: int @dataclass class BaselineSchedule: station_events: list[ BaselineStationEvent ] movements: list[ BaselineMovement ] و سپس: Baseline Schedule │ ├──────────────┐ │ │ ▼ ▼ Generated Schedule Conflict Engine │ │ └──────┬───────┘ ▼ Baseline Diff 8. Baseline Diff این قابلیت برای UI و Explanation بسیار ارزشمند است. Python from dataclasses import dataclass @dataclass class ScheduleDifference: train_run_id: str station_id: str sequence: int baseline_arrival_min: int generated_arrival_min: int baseline_departure_min: int generated_departure_min: int arrival_delta_min: int departure_delta_min: int محاسبه: Python def compare_station_event( baseline, generated, ): return ScheduleDifference( train_run_id=generated.train_run_id, station_id=generated.station_id, sequence=generated.sequence, baseline_arrival_min=( baseline.arrival_min ), generated_arrival_min=( generated.arrival_min ), baseline_departure_min=( baseline.departure_min ), generated_departure_min=( generated.departure_min ), arrival_delta_min=( generated.arrival_min - baseline.arrival_min ), departure_delta_min=( generated.departure_min - baseline.departure_min ), ) حالا UI می‌تواند مثلاً بگوید: Train 100 Arak Baseline Arrival: 12:27 Generated Arrival: 12:31 Delta: +4 min Dorud Baseline Arrival: 18:11 Generated Arrival: 18:17 Delta: +6 min این بسیار مهم‌تر از یک نمودار ساده‌ی utilization است. 9. Bottleneck Explanation در Version 1.0 خروجی Solver نباید فقط این باشد: Capacity = 76 بلکه: Python @dataclass class BindingConstraint: constraint_id: str type: str resource_id: str | None value: float | None limit: float | None utilization: float | None impact: str مثلاً: Binding Constraint ──────────────────────────────────── Type: SINGLE_TRACK_BLOCK Resource: BLOCK:STN:100:STN:120 Capacity: 76 trains/day Next flow: 77 trains/day Status: BINDING یا: Binding Constraint ──────────────────────────────────── Type: STATION_TRACK_LENGTH Station: DORUD Track: DORUD-T2 Train length: 620 m Usable length: 580 m Status: INFEASIBLE 10. Capacity Proof در Version 1.0 نتیجه نهایی باید structureای شبیه این داشته باشد: Python @dataclass class CapacityResult: route_id: str capacity: int feasible_schedule_found: bool independently_validated: bool next_flow: int next_flow_feasible: bool proof_status: str binding_constraints: list مثلاً: ROUTE CAPACITY RESULT ──────────────────────────────────── Route: GAR → ANDIMESHK Capacity: 76 trains/day Proof: F = 76 FEASIBLE ✓ F = 76 VALIDATED ✓ F = 77 INFEASIBLE ✓ Proof Status: PROVEN Primary Binding Constraint: Single-track block Secondary Constraint: Station crossing capacity این همان Capacity Proof است که در معماری اولیه تعریف کردیم. 11. Vertical Slice واقعی برای Train 100 / 101 حالا مسیر واقعی داده‌ی شما چنین خواهد شد: Access │ │ TrainNo = 100 ▼ TrainRun 100 │ ├── Gar ├── Sakheh ├── Bagh Yek ├── ... └── Andimeshk │ ▼ Direction = FORWARD │ ▼ Directed Path │ ├── Gar → Sakheh ├── Sakheh → Bagh Yek ├── ... └── ... و: Access │ │ TrainNo = 101 ▼ TrainRun 101 │ ├── Andimeshk ├── Dokuheh ├── Gol Mahak ├── ... └── Gar │ ▼ Direction = REVERSE نکته بسیار مهم: Train 100 Kilometerage: 157 → 674 Train 101 Kilometerage: 674 → ... بنابراین reverse path واقعاً باید reverse باشد؛ نه اینکه فقط Direction=REVERSE ذخیره شود ولی ترتیب segmentها همان forward بماند. 12. Pipeline نهایی Version 1.0 کلاس اصلی را به این شکل تعریف می‌کنیم: Python class RealVerticalSlice: def __init__( self, source_adapter, infrastructure, scheduler, validator, ): self.source_adapter = source_adapter self.infrastructure = infrastructure self.scheduler = scheduler self.validator = validator def run( self, table_name: str, ): # 1. Extract df = ( self.source_adapter .extract_table(table_name) ) # 2. Map train_runs, mapping_issues = ( map_access_train_runs(df) ) if mapping_issues: raise ValueError( "Mapping failed." ) # 3. Quality quality_gate = ( DataQualityGate() ) quality_issues = ( quality_gate.validate_all( train_runs ) ) errors = [ x for x in quality_issues if x.severity == "ERROR" ] if errors: raise ValueError( "Data Quality Gate failed." ) # 4. Directed paths paths = {} for train in train_runs: paths[train.id] = ( build_train_path( train, self.infrastructure, ) ) # 5. Canonical SchedulingProblem problem = build_problem( train_runs=train_runs, paths=paths, infrastructure=self.infrastructure, ) # 6. Solve schedule = ( self.scheduler.solve(problem) ) if schedule is None: return { "status": "INFEASIBLE", "mapping_issues": [], "quality_issues": quality_issues, } # 7. Independent validation validation = ( self.validator.validate( problem, schedule, ) ) # 8. Result return { "status": ( "FEASIBLE" if validation.feasible else "INVALID" ), "train_runs": train_runs, "schedule": schedule, "validation": validation, "mapping_issues": mapping_issues, "quality_issues": quality_issues, } 13. چیزی که عمداً هنوز وارد این Vertical Slice نمی‌کنیم این موارد بعد از موفقیت Vertical Slice واقعی اضافه می‌شوند: Wagon Cycle Locomotive Cycle Train Formation Optimization Demand Allocation Marketplace Network-wide Optimization Investment Optimization علت این ترتیب این است که ابتدا باید ثابت کنیم: REAL DATA ↓ REAL TOPOLOGY ↓ REAL TRAIN MOVEMENT ↓ REAL SCHEDULE ↓ REAL CONFLICT ↓ REAL CAPACITY بعد: CAPACITY + WAGON + LOCOMOTIVE + DEMAND ↓ NETWORK CAPACITY را بسازیم. 14. یک اصلاح مهم در Solver نسخه قبلی در Version 0.9، NoOverlap برای block resource به‌عنوان پایه استفاده شد. برای MVP مناسب است، اما برای مدل نهایی Single Track باید مفهوم زیر را دقیق‌تر کنیم: Clear i ​ +H ij ​ ≤Entry j ​ که: H ij ​ ={ H same ​ , max(H same ​ ,T switch ​ ), ​ direction i ​ =direction j ​ direction i ​  =direction j ​ ​ در نتیجه: same direction ↓ Headway opposite direction ↓ Headway +/or Switch Time ↓ Physical Block Release ↓ Route Availability ↓ Next Movement و در مراحل بعدی: Switch Time = Last Train Clear + Block Release + Route Release + Signal/Interlocking + Station Preparation را از یک switch_time_min ثابت به یک Operational Transition Model تبدیل می‌کنیم. 15. Definition of Done برای Version 1.0 نسخه 1.0 را زمانی موفق می‌دانیم که برای حداقل یک مسیر واقعی بتوانیم این خروجی را تولید کنیم: RUN ──────────────────────────────────── Data Version: REAL_ACCESS_V1 Model Version: 1.0 Scenario: BASELINE Train Runs: 100, 101, ... DATA QUALITY ──────────────────────────────────── Errors: 0 Warnings: N PATH ──────────────────────────────────── Direction: FORWARD / REVERSE Segments: N Physical Blocks: N BASELINE ──────────────────────────────────── Loaded: YES Validated: YES SCHEDULER ──────────────────────────────────── Solver: CP-SAT Status: OPTIMAL / FEASIBLE SCHEDULE ──────────────────────────────────── Generated: YES VALIDATION ──────────────────────────────────── Feasible: YES Errors: 0 CAPACITY ──────────────────────────────────── F: XX F+1: INFEASIBLE PROOF ──────────────────────────────────── Capacity Proven: YES BOTTLENECK ──────────────────────────────────── Primary: BLOCK / STATION / JUNCTION Resource: ... Impact: ... EXPLANATION ──────────────────────────────────── Capacity is limited because ... این خروجی، اولین Evidence Package واقعی موتور Capacity خواهد بود. گام بعدی دقیق حالا مهم‌ترین مرحله این است که این ساختار را روی فایل واقعی aaa.accdb و Excel واقعی REPORTKholase...xlsx ببندیم؛ یعنی دیگر fixture مصنوعی استفاده نکنیم. در آن مرحله من ساختار را به این شکل یکپارچه می‌کنم: aaa.accdb │ ├── Access Adapter │ ├── Train 100 / 101 │ └── Real TrainStationCall │ ▼ REPORTKholase.xlsx │ ├── Operating Pattern ├── Outbound └── Return │ ▼ Infrastructure Master │ ▼ Canonical Route │ ▼ Baseline Schedule │ ▼ Conflict Detection │ ▼ CP-SAT │ ▼ Validated Operational Schedule │ ▼ Route Capacity │ ▼ Capacity Proof │ ▼ Bottleneck / Explanation و از آنجا به بعد، Version 1.1 را روی همین مسیر واقعی برای Wagon Cycle + Locomotive Cycle + Train Formation می‌سازیم؛ یعنی زنجیره‌ی کامل موردنظر شما: Marketplace→Demand→FreightFlow→WagonRequirement→TrainFormation→TrainRun→Schedule→Capacity→Allocation این همان نقطه‌ای است که موتور از یک Railway Scheduling Prototype به یک Railway Capacity Planning Engine واقعی تبدیل می‌شود. --- ## ادامه 29 — پرسش کاربر (`message-id:0b8b1ac4-b585-419e-9c87-0726b0c46559`) بله لطفا --- ## ادامه 29 — پاسخ دستیار (`message-id:b43844cb-83b6-4bdf-b76c-ade08598dd2e`) حتماً. در این مرحله پیشنهاد می‌کنم از Version 1.0 مفهومی/نیمه‌کامل عبور کنیم و یک Vertical Slice واقعاً منسجم و قابل اجرا بسازیم که مستقیماً برای aaa.accdb و فایل Excel واقعی شما آماده باشد. نکته مهم: من در این پاسخ ادعا نمی‌کنم که فایل‌های واقعی شما را اجرا کرده‌ام؛ چون خود فایل‌های aaa.accdb و Excel در این نوبت در دسترس اجرای من نیستند. اما ساختار زیر دقیقاً بر اساس schema واقعی Access و ساختار واقعی Excel که قبلاً استخراج کرده‌ایم تنظیم شده است. Version 1.1 — Executable Real-Data Vertical Slice هدف این نسخه: aaa.accdb + REPORTKholase_31-06-1405_02-19-35.xlsx + Infrastructure Master ↓ Raw/Staging ↓ Mapping ↓ Data Quality Gate ↓ Canonical Railway Model ↓ Directed Train Path ↓ Baseline Schedule ↓ CP-SAT Scheduling ↓ Independent Validation ↓ Capacity Proof ↓ Binding Constraints ↓ Explanation و اصل کلیدی همچنان: No Feasible Schedule → No Operational Capacity در بخش solver نیز از مدل‌های interval و resource constraints استفاده می‌کنیم؛ این با الگوی CP-SAT برای زمان‌بندی سازگار است، هرچند برای production باید نسخه مورد استفاده را در CI pin کنیم و API همان نسخه را تست کنیم. Google for Developers 1. ساختار نهایی پروژه من این ساختار را برای v1.1 پیشنهاد می‌کنم: rail_capacity_engine/ │ ├── pyproject.toml ├── requirements.txt ├── README.md ├── config.yml │ ├── app/ │ ├── __init__.py │ ├── main.py │ │ │ ├── config/ │ │ └── settings.py │ │ │ ├── domain/ │ │ ├── common.py │ │ ├── station.py │ │ ├── train.py │ │ ├── train_run.py │ │ ├── route.py │ │ ├── schedule.py │ │ ├── infrastructure.py │ │ └── result.py │ │ │ ├── adapters/ │ │ ├── excel.py │ │ └── access.py │ │ │ ├── staging/ │ │ └── models.py │ │ │ ├── mapping/ │ │ ├── registry.py │ │ ├── access_mapping.py │ │ └── excel_mapping.py │ │ │ ├── quality/ │ │ ├── issues.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ └── train_identity.py │ │ │ ├── infrastructure/ │ │ ├── loader.py │ │ └── topology.py │ │ │ ├── scheduling/ │ │ ├── path.py │ │ ├── baseline.py │ │ ├── problem.py │ │ └── scheduler.py │ │ │ ├── conflicts/ │ │ └── engine.py │ │ │ ├── validation/ │ │ └── validator.py │ │ │ ├── capacity/ │ │ ├── search.py │ │ └── proof.py │ │ │ ├── explanation/ │ │ └── engine.py │ │ │ └── pipeline/ │ └── vertical_slice.py │ ├── data/ │ ├── source/ │ │ ├── aaa.accdb │ │ └── REPORTKholase_31-06-1405_02-19-35.xlsx │ │ │ ├── infrastructure/ │ │ └── infrastructure.yml │ │ │ └── output/ │ └── tests/ ├── test_time.py ├── test_access_mapping.py ├── test_direction.py ├── test_seir.py ├── test_midnight.py ├── test_single_track.py ├── test_double_track.py ├── test_validation.py └── test_vertical_slice.py 2. Dependencyها pandas>=3.0,<4 openpyxl>=3.1,<4 pydantic>=2,<3 PyYAML>=6,<7 ortools>=9.14,<10 pytest>=8,<10 برای Access: pyodbc>=5,<6 اما pyodbc به‌تنهایی کافی نیست؛ روی ماشین اجرا باید Microsoft Access ODBC Driver مناسب نیز نصب باشد. 3. Canonical model یکی از مهم‌ترین تصمیمات این نسخه این است که دیگر Access fieldها مستقیماً وارد solver نشوند. مثلاً: Access: seir ↓ Mapping ↓ TrainStationCall.baseline_running_time_to_next ↓ RouteSegment.baseline_running_time ↓ Scheduling Constraint و نه: CP-SAT → seir app/domain/train_run.py Python from __future__ import annotations from dataclasses import dataclass, field from typing import Optional @dataclass(frozen=True) class TrainStationCall: train_run_id: str sequence: int station_id: str arrival_minute: int dwell_minutes: int departure_minute: int required_wait_minutes: Optional[int] = None kilometerage: Optional[float] = None source_distance: Optional[float] = None max_speed: Optional[float] = None baseline_running_time_to_next: Optional[int] = None source_record_id: Optional[str] = None @dataclass class TrainRun: train_run_id: str train_no: str train_name: str origin_station_id: str destination_station_id: str direction: str station_calls: list[TrainStationCall] = field(default_factory=list) earliest_departure: Optional[int] = None latest_arrival: Optional[int] = None train_length_m: Optional[float] = None train_weight_t: Optional[float] = None 4. چرا seir باید همین‌جا باشد؟ با داده واقعی شما، مثلاً: Gar 09:00 + seir = 34 ↓ Sakheh 09:34 بعد: Sakheh 09:34 + 38 ↓ Bagh Yek 10:12 پس: seir یک property از station call به station call بعدی است، نه property خود station. بنابراین: Python call.baseline_running_time_to_next مدل صحیح‌تری است. 5. مدیریت زمان و Midnight Rollover این قسمت برای داده شما حیاتی است. مثلاً: 23:46 + 50 min = 00:36 نباید solver تصور کند: 00:36 < 23:46 بلکه باید: 23:46 → 1426 00:36 → 1476 باشد. app/domain/common.py Python from __future__ import annotations def parse_hhmm(value: str) -> int: if value is None: raise ValueError("Time value cannot be None") value = str(value).strip() hour, minute = value.split(":") hour = int(hour) minute = int(minute) if not 0 <= hour <= 23: raise ValueError(f"Invalid hour: {hour}") if not 0 <= minute <= 59: raise ValueError(f"Invalid minute: {minute}") return hour * 60 + minute def normalize_forward(previous_minute: int, current_minute: int) -> int: """ Convert a clock time to the next occurrence after previous_minute. Supports midnight rollover. """ while current_minute < previous_minute: current_minute += 24 * 60 return current_minute def add_minutes(start: int, duration: int) -> int: if duration < 0: raise ValueError("Duration cannot be negative") return start + duration 6. Access Mapping واقعی Fieldهای واقعی که قبلاً استخراج کردیم: ID kol TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir Mapping پیشنهادی v1.1: Access Canonical وضعیت ID source_record_id Verified TrainNo train_no Verified TrainName train_name Verified StationName station_name Verified StationNumber source_station_number Verified Sequence sequence Verified time_in arrival Verified time_take dwell Verified time_out source_departure Verified RequiredWait required_wait Provisional Kilometerage chainage High confidence MaxSpeed max_speed Provisional Distance source_distance Untrusted sumDistancezz source_sum_distance Unknown seir baseline_running_time_to_next Strongly verified و بسیار مهم: Distance را با: abs(next.Kilometerage - current.Kilometerage) overwrite نمی‌کنیم. بلکه: source_distance derived_segment_distance دو مفهوم مستقل خواهند بود. 7. Access Adapter app/adapters/access.py Python from __future__ import annotations from pathlib import Path from typing import Any import pandas as pd class AccessAdapter: def __init__(self, database_path: str): self.database_path = Path(database_path) def _connection_string(self) -> str: return ( "DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};" f"DBQ={self.database_path};" ) def list_tables(self) -> list[str]: import pyodbc with pyodbc.connect(self._connection_string()) as conn: cursor = conn.cursor() tables = [] for row in cursor.tables(): table_name = row.table_name table_type = row.table_type if table_type == "TABLE": tables.append(table_name) return sorted(set(tables)) def extract_table(self, table_name: str) -> pd.DataFrame: import pyodbc # Production hardening: # validate table_name against list_tables() before execution. allowed = set(self.list_tables()) if table_name not in allowed: raise ValueError( f"Unknown Access table: {table_name}" ) with pyodbc.connect(self._connection_string()) as conn: return pd.read_sql( f"SELECT * FROM [{table_name}]", conn, ) این نسخه عمداً قبل از query، نام table را با tableهای موجود مقایسه می‌کند. 8. Excel Adapter app/adapters/excel.py Python from __future__ import annotations from pathlib import Path import pandas as pd class ExcelAdapter: def __init__(self, file_path: str): self.file_path = Path(file_path) def sheets(self) -> list[str]: workbook = pd.ExcelFile(self.file_path) return workbook.sheet_names def extract( self, sheet_name: str | None = None, ) -> pd.DataFrame: if sheet_name is None: sheet_name = self.sheets()[0] return pd.read_excel( self.file_path, sheet_name=sheet_name, dtype=object, ) dtype=object در این مرحله عمدی است؛ چون نمی‌خواهیم pandas قبل از mapping، معنای domain داده را حدس بزند. 9. Mapping Access → Canonical app/mapping/access_mapping.py Python from __future__ import annotations import math from typing import Any from app.domain.common import parse_hhmm, normalize_forward from app.domain.train_run import TrainRun, TrainStationCall def clean(value: Any) -> Any: if value is None: return None try: if isinstance(value, float) and math.isnan(value): return None except TypeError: pass return value def text(value: Any) -> str: value = clean(value) if value is None: return "" return str(value).strip() def integer(value: Any) -> int | None: value = clean(value) if value is None or value == "": return None return int(float(value)) def number(value: Any) -> float | None: value = clean(value) if value is None or value == "": return None return float(value) def map_access_train_records( records, ) -> list[TrainRun]: required = { "ID", "TrainNo", "StationName", "Sequence", "time_in", "time_take", "TrainName", "seir", } missing = required - set(records.columns) if missing: raise ValueError( f"Missing required Access fields: {sorted(missing)}" ) runs: list[TrainRun] = [] for train_no, group in records.groupby( records["TrainNo"].astype(str), sort=False, ): rows = list( group.sort_values("Sequence").to_dict("records") ) if not rows: continue train_name = text(rows[0]["TrainName"]) station_calls: list[TrainStationCall] = [] previous_arrival = None for row in rows: arrival_clock = parse_hhmm( text(row["time_in"]) ) if previous_arrival is not None: arrival = normalize_forward( previous_arrival, arrival_clock, ) else: arrival = arrival_clock dwell = integer(row["time_take"]) or 0 departure = arrival + dwell required_wait = integer( row.get("RequiredWait") ) if required_wait is not None: required_wait = max( required_wait, 0, ) sequence = integer(row["Sequence"]) seir = integer(row.get("seir")) call = TrainStationCall( train_run_id=str(train_no), sequence=sequence, station_id=text(row["StationName"]), arrival_minute=arrival, dwell_minutes=dwell, departure_minute=departure, required_wait_minutes=required_wait, kilometerage=number( row.get("Kilometerage") ), source_distance=number( row.get("Distance") ), max_speed=number( row.get("MaxSpeed") ), baseline_running_time_to_next=seir, source_record_id=text(row["ID"]), ) station_calls.append(call) previous_arrival = arrival if seir is not None: previous_arrival = ( departure + seir ) origin = station_calls[0].station_id destination = station_calls[-1].station_id direction = ( "FORWARD" if ( station_calls[-1].kilometerage is not None and station_calls[0].kilometerage is not None and station_calls[-1].kilometerage >= station_calls[0].kilometerage ) else "REVERSE" ) runs.append( TrainRun( train_run_id=str(train_no), train_no=str(train_no), train_name=train_name, origin_station_id=origin, destination_station_id=destination, direction=direction, station_calls=station_calls, ) ) return runs 10. یک اصلاح مهم در Mapping زمان در داده شما seir متعلق به فاصله بین دو station call است. بنابراین برای ساخت baseline بهتر است arrival ایستگاه بعدی را از: departure_current + seir_current بسازیم. این یعنی در validation می‌توانیم بررسی کنیم: next.arrival = current.departure + seir و اگر مغایرت وجود داشت، آن را به‌عنوان: BASELINE_TIME_INCONSISTENCY ثبت کنیم. 11. Infrastructure Master این بخش را نباید از داده Access حدس بزنیم. چون مثلاً از این داده: Gar Sakheh Bagh Yek ... Andimeshk می‌توانیم بفهمیم قطار در این ایستگاه‌ها حرکت کرده، اما نمی‌توانیم با اطمینان نتیجه بگیریم: Single Track Double Track Station Track Capacity Junction Conflict Passing Loop پس Infrastructure Master مستقل خواهد بود. data/infrastructure/infrastructure.yml ساختار: YAML stations: GAR: name: "Gar" usable_length_m: 700 tracks: - id: GAR-01 length_m: 700 - id: GAR-02 length_m: 700 SAKHEH: name: "Sakheh" usable_length_m: 650 tracks: - id: SAKHEH-01 length_m: 650 blocks: GAR-SAKHEH: from_station: GAR to_station: SAKHEH track_type: SINGLE baseline_running_time_min: 34 clearance_time_min: 2 SAKHEH-BAGHYEK: from_station: SAKHEH to_station: BAGHYEK track_type: SINGLE baseline_running_time_min: 38 clearance_time_min: 2 اما اعداد بالا نمونه ساختاری هستند، نه داده واقعی راه‌آهن شما. برای production نباید آن‌ها را بدون Master Data واقعی وارد کنیم. 12. Physical Block یکی از مهم‌ترین اصلاحات معماری: GAR → SAKHEH و: SAKHEH → GAR دو DirectedSegment هستند، اما یک: PhysicalBlock دارند. پس: Python def physical_block_id( station_a: str, station_b: str, ) -> str: return "::".join( sorted( [ station_a, station_b, ] ) ) مثلاً: GAR::SAKHEH در هر دو جهت یکسان است. 13. Track Type Python from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" Resource logic: Python def movement_resource( physical_block: str, direction: str, track_type: TrackType, ) -> str: if track_type == TrackType.SINGLE: return f"BLOCK::{physical_block}" return ( f"BLOCK::{physical_block}" f"::{direction}" ) نتیجه: Single Gar → Andimeshk ↓ BLOCK::GAR::ANDIMESHK ↑ Andimeshk → Gar Double BLOCK::GAR::ANDIMESHK::FORWARD BLOCK::GAR::ANDIMESHK::REVERSE این دقیقاً همان distinction مهمی است که در مدل ریاضی پروژه داشتیم. 14. Directed Route Path app/scheduling/path.py Python from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class RouteSegment: sequence: int from_station: str to_station: str physical_block_id: str resource_id: str direction: str baseline_running_time_min: int clearance_time_min: int = 0 @dataclass class DirectedTrainPath: train_run_id: str direction: str segments: list[RouteSegment] Builder: Python def build_directed_path(train_run): calls = sorted( train_run.station_calls, key=lambda x: x.sequence, ) segments = [] for i in range(len(calls) - 1): current = calls[i] nxt = calls[i + 1] if current.baseline_running_time_to_next is None: raise ValueError( f"Missing seir for train {train_run.train_run_id}, " f"sequence {current.sequence}" ) physical = physical_block_id( current.station_id, nxt.station_id, ) direction = ( "FORWARD" if nxt.kilometerage is None or current.kilometerage is None or nxt.kilometerage >= current.kilometerage else "REVERSE" ) segments.append( RouteSegment( sequence=i + 1, from_station=current.station_id, to_station=nxt.station_id, physical_block_id=physical, resource_id=( f"BLOCK::{physical}" ), direction=direction, baseline_running_time_min=( current.baseline_running_time_to_next ), ) ) return DirectedTrainPath( train_run_id=train_run.train_run_id, direction=train_run.direction, segments=segments, ) در نسخه واقعی باید InfrastructureMaster به builder تزریق شود تا resource_id بر اساس TrackType تعیین شود. 15. Scheduling Model مدل زمان‌بندی را باید به این شکل نگه داریم: Station Arrival ↓ Station Dwell ↓ Station Departure ↓ Block Entry ↓ Block Exit ↓ Clear ↓ Next Station Arrival نه اینکه فقط یک: start / end برای کل route داشته باشیم. 16. Constraintهای اصلی برای هر station call: Departure i ​ ≥Arrival i ​ +Dwell i ​ برای هر block: Exit i ​ ≥Entry i ​ +T run,i ​ و: Clear i ​ ≥Exit i ​ +T clear,i ​ رابطه: Entry i ​ ≥Departure i ​ و: Arrival i+1 ​ ≥Exit i ​ در حالت baseline: Arrival i+1 ​ =Departure i ​ +T run,i baseline ​ ولی در generated schedule می‌تواند: Arrival i+1 ​ ≥Departure i ​ +T run,i minimum ​ باشد. 17. Single Track Conflict برای دو حرکت: A → B B → A اگر block مشترک باشد: Exit_A + T_switch <= Entry_B یا: Exit_B + T_switch <= Entry_A یعنی: t clear A ​ +T switch ​ ≤t entry B ​ یا برعکس. برای same direction: Entry B ​ ≥Clear A ​ +H same ​ بنابراین transition gap به‌صورت: same direction: H_same opposite direction: max(H_same, T_switch) مدل می‌شود. 18. Station Capacity Station هم resource است. برای هر train: arrival departure station track را داریم. اگر دو train از یک track استفاده کنند: NoOverlap ولی اینجا نباید کل station را یک resource واحد فرض کنیم. مدل: Station ├── Track 1 ├── Track 2 ├── Track 3 ├── Arrival route ├── Departure route └── Formation resource خواهد بود. این برای crossing و overtaking بعداً بسیار مهم است. 19. Independent Validator این قسمت از خود solver مهم‌تر است. Solver می‌گوید: FEASIBLE ولی validator مستقل باید بگوید: واقعاً feasible است یا خیر؟ چک‌ها: 1. Sequence 2. Arrival >= previous departure 3. Dwell >= required wait 4. Running time >= minimum 5. Block overlap 6. Single-track conflict 7. Headway 8. Station-track conflict 9. Train length <= usable length 10. Operational window 11. Earliest departure 12. Latest arrival 13. Junction conflict 20. Capacity Search در اینجا یک اصلاح اساسی نسبت به نسخه‌های قبلی لازم است. نباید بنویسیم: Python problem_factory(F): create F synthetic trains چون این دیگر ظرفیت واقعی راه‌آهن نیست. برای v1.1 دو حالت تعریف می‌کنیم. Mode A — Candidate Capacity تعدادی TrainRun واقعی داریم: 100 101 ... و می‌پرسیم: چند حرکت از candidate set با محدودیت‌های موجود قابل زمان‌بندی است؟ Mode B — Operational Pattern Capacity در نسخه بعدی: OperatingPattern + frequency + calendar + OD + turnaround حرکت‌های جدید تولید می‌شوند. این همان چیزی است که برای: Cr = max F در production لازم داریم. 21. Capacity Proof تعریف دقیق: F feasible F independently validated F + 1 infeasible بنابراین: Python @dataclass class CapacityProof: capacity: int feasible_at_capacity: bool validated_at_capacity: bool tested_above_capacity: bool infeasible_above_capacity: bool proof_valid: bool و: Python def build_capacity_proof( capacity: int, result_at_capacity, result_above, ): feasible = ( result_at_capacity.feasible ) validated = ( result_at_capacity.validated ) above_infeasible = ( not result_above.feasible ) return CapacityProof( capacity=capacity, feasible_at_capacity=feasible, validated_at_capacity=validated, tested_above_capacity=True, infeasible_above_capacity=above_infeasible, proof_valid=( feasible and validated and above_infeasible ), ) 22. Binding Constraint Capacity بدون explanation برای محصول نهایی کافی نیست. مثلاً: Capacity = 76 trains/day باید بدانیم: Binding Constraint: Physical Block GAR::SAKHEH Resource: Single Track Utilization: 100% Marginal Capacity: +1 train requires approximately +X minutes usable time / regime change و اگر station binding باشد: Binding Constraint: Station DORUD Resource: Station Track 2 Reason: simultaneous occupancy 23. Explanation Object Python @dataclass class BindingConstraint: constraint_type: str resource_id: str value: float | None limit: float | None slack: float | None severity: str explanation: str مثلاً: constraint_type = SINGLE_TRACK resource_id = BLOCK::GAR::SAKHEH severity = BINDING explanation = "این بلاک در بازه 09:00–14:30 به علت حرکت‌های دوطرفه و زمان آزادسازی، ظرفیت افزوده قابل استفاده ندارد." 24. Result Contract خروجی یک Run باید self-contained باشد: JSON { "run_id": "RUN-2026-001", "model_version": "1.1.0", "data_version": "ACCESS-2026-09", "scenario_id": "BASE", "status": "VALIDATED", "train_runs": [], "schedule": [], "validation": {}, "capacity": { "value": 76, "unit": "train_movements" }, "capacity_proof": { "f_feasible": true, "f_validated": true, "f_plus_1_infeasible": true }, "binding_constraints": [], "resource_utilization": [], "explanation": [] } این Result Package بعداً مستقیماً به: Capacity API Marketplace Adapter UI Reporting Scenario Engine متصل می‌شود. 25. CLI app/main.py نسخه اول CLI: Python from __future__ import annotations import argparse from app.pipeline.vertical_slice import run_vertical_slice def main(): parser = argparse.ArgumentParser( description="Railway Capacity Engine" ) parser.add_argument( "--access", required=True, help="Path to .accdb", ) parser.add_argument( "--excel", required=True, help="Path to Excel source", ) parser.add_argument( "--infrastructure", required=True, help="Path to infrastructure YAML", ) parser.add_argument( "--table", required=True, help="Access movement table", ) parser.add_argument( "--output", required=True, help="Output JSON", ) args = parser.parse_args() result = run_vertical_slice( access_path=args.access, excel_path=args.excel, infrastructure_path=args.infrastructure, access_table=args.table, ) result.write_json(args.output) print( f"Run completed: {result.run_id}" ) print( f"Status: {result.status}" ) print( f"Capacity: {result.capacity}" ) if __name__ == "__main__": main() اجرای واقعی: Bash python -m app.main \ --access data/source/aaa.accdb \ --excel data/source/REPORTKholase_31-06-1405_02-19-35.xlsx \ --infrastructure data/infrastructure/infrastructure.yml \ --table YOUR_ACCESS_TABLE \ --output data/output/run.json نام YOUR_ACCESS_TABLE را نباید حدس بزنیم؛ اول باید: list_tables() روی فایل واقعی اجرا شود. 26. Pipeline نهایی app/pipeline/vertical_slice.py باید orchestration را انجام دهد: Python def run_vertical_slice( access_path, excel_path, infrastructure_path, access_table, ): # 1 access_df = AccessAdapter( access_path ).extract_table( access_table ) # 2 excel_df = ExcelAdapter( excel_path ).extract() # 3 quality_report = run_quality_gate( access_df, excel_df, ) quality_report.raise_if_blocking() # 4 train_runs = map_access_train_records( access_df ) # 5 infrastructure = load_infrastructure( infrastructure_path ) # 6 paths = build_paths( train_runs, infrastructure, ) # 7 baseline = build_baseline_schedule( train_runs, paths, ) # 8 problem = build_scheduling_problem( train_runs=train_runs, paths=paths, infrastructure=infrastructure, baseline=baseline, ) # 9 generated = solve( problem ) # 10 validation = validate_schedule( generated, problem, ) # 11 capacity = calculate_capacity( problem_factory=... ) # 12 explanation = explain( generated, validation, capacity, ) return build_result( ... ) این separation بسیار مهم است: Adapter ≠ Domain ≠ Solver ≠ Validator ≠ Explanation 27. اولین Run واقعی که باید انجام دهیم با توجه به داده‌ای که قبلاً از Access استخراج کرده‌ایم، اولین Vertical Slice را روی: TrainNo = 100 TrainNo = 101 می‌گذاریم. یعنی: 100: Gar → Andimeshk 101: Andimeshk → Gar و از داده واقعی شما: TrainNo TrainName StationName Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed Distance sumDistancezz seir استفاده می‌کنیم. این دو حرکت برای تست فوق‌العاده مهم‌اند، چون هم‌زمان داریم: Forward Direction Reverse Direction Single Physical Block Identity Midnight Rollover Running Time Dwell Required Wait Chainage را validate می‌کنیم. 28. یک نکته مهم درباره Train 100 و 101 برای این دو قطار: 100: Gar → Andimeshk 101: Andimeshk → Gar نباید route را دو route فیزیکی مستقل فرض کنیم. باید داشته باشیم: Physical Route Gar | | Physical Block 1 | Sakheh | | Physical Block 2 | ... | Andimeshk و سپس: Train 100: Block1 → Block2 → Block3 → ... Train 101: BlockN → BlockN-1 → BlockN-2 → ... این دقیقاً همان distinction بین: Physical Infrastructure و: Directed Operational Path است. 29. Data Quality Gate برای داده واقعی قبل از اینکه حتی یک constraint وارد solver شود: Q1 — Required Columns Q2 — Train Identity Q3 — Sequence Continuity Q4 — Time Parse Q5 — Midnight Normalization Q6 — Dwell Consistency Q7 — seir Consistency Q8 — Direction Consistency Q9 — Kilometerage Consistency Q10 — Station Identity مثلاً: ERROR: TrainNo=100 Sequence=17 seir=83 but: next_arrival - departure != 83 نباید silently اصلاح شود. باید: QualityIssue ثبت شود. 30. کیفیت داده سه سطح داشته باشد ERROR داده اجازه اجرای production نمی‌دهد. Missing Sequence Invalid Time Duplicate Station Sequence Impossible Movement WARNING اجرای تحلیل ممکن است، ولی نتیجه محدودیت دارد. MaxSpeed missing Distance untrusted RequiredWait provisional INFO مثلاً: Derived distance generated from Kilometerage 31. Derived Data برای مثال: Python derived_segment_distance = abs( next.kilometerage - current.kilometerage ) اما خروجی: source_distance = 0 derived_distance = 43 distance_status = DERIVED نه اینکه: Distance = 43 و provenance از بین برود. 32. چیزی که در v1.1 هنوز وارد solver نمی‌کنیم عمداً این‌ها را هنوز وارد نمی‌کنیم: Wagon Cycle Locomotive Cycle Train Formation Optimization Demand Allocation Marketplace Allocation Empty Wagon Optimization Network-wide Multi-route Optimization دلیل این نیست که اهمیت ندارند. برعکس؛ این‌ها بخش اصلی محصول نهایی‌اند. اما باید ترتیب صحیح باشد: Infrastructure ↓ Train Movement ↓ Schedule ↓ Capacity ↓ Wagon/Loco ↓ Formation ↓ Demand ↓ Marketplace ↓ Network Optimization اگر هسته زمان‌بندی هنوز روی داده واقعی validate نشده باشد، اضافه کردن wagon/loco فقط complexity را زیاد می‌کند. 33. Definition of Done برای v1.1 Version 1.1 زمانی Done است که این مسیر بدون synthetic data کار کند: aaa.accdb ↓ Access Adapter ↓ Actual Train Movement Records ↓ Train 100 / 101 ↓ Canonical TrainRun ↓ Directed Path ↓ Infrastructure Master ↓ Baseline ↓ CP-SAT ↓ Generated Schedule ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof ↓ Binding Constraint ↓ JSON Result و نتیجه باید بتواند صریحاً بگوید: RUN DATA VERSION MODEL VERSION SCENARIO TRAIN RUNS SCHEDULE FEASIBLE: YES/NO VALIDATED: YES/NO CAPACITY: X F=X FEASIBLE F=X VALIDATED F=X+1 INFEASIBLE BINDING CONSTRAINTS RESOURCE UTILIZATION DATA QUALITY EXPLANATION 34. بعد از v1.1 چه می‌شود؟ بعد از اینکه این Vertical Slice روی aaa.accdb واقعی و Excel واقعی بدون synthetic fixture اجرا شد، Version 1.2 را به این صورت می‌سازیم: v1.1 Real Infrastructure + Schedule + Capacity ↓ v1.2 Wagon Cycle ↓ v1.3 Locomotive Cycle ↓ v1.4 Train Formation Optimization ↓ v1.5 Demand / Freight Flow ↓ v1.6 Marketplace Allocation ↓ v1.7 Network Optimization ↓ v2.0 Production Railway Capacity Platform و در نهایت مدل کامل شما به این زنجیره می‌رسد: Marketplace ↓ Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Wagon Cycle ─────┐ │ Locomotive Cycle ┘ ↓ Train Run ↓ Directed Train Path ↓ Block / Station / Junction ↓ Operational Regime ↓ CP-SAT Scheduler ↓ Feasible Timetable ↓ Independent Validation ↓ Route Capacity ↓ Network Capacity ↓ Capacity Proof ↓ Bottleneck ↓ Scenario / Sensitivity ↓ Marketplace Allocation این نقطه، از نظر معماری، همان هسته‌ای است که پروژه را از یک «محاسبه‌گر ظرفیت» به یک Railway Capacity Planning & Optimization Engine واقعی تبدیل می‌کند. گام عملی بعدی من برای این پروژه این است که همین v1.1 را از حالت specification به یک package کامل و یکپارچه Python با فایل‌های واقعی app/...، تست‌های pytest، config.yml، Infrastructure Master schema و CLI واحد تبدیل کنیم؛ بدون snippetهای پراکنده و با رفع helperهای ناقص نسخه‌های 0.8/0.9/1.0. --- ## ادامه 30 — پرسش کاربر (`message-id:205b435f-aebc-4b4d-a8b4-ebfd09aa69ed`) بله لطفا --- ## ادامه 30 — پاسخ دستیار (`message-id:0518ab75-232e-4fe6-981e-3c7089a5c49d`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 31 — پرسش کاربر (`message-id:fd964b61-6352-4a32-858a-2b8620c27e22`) لطفا به صورت متن همینجا بفرست --- ## ادامه 31 — پاسخ دستیار (`message-id:52b20514-2472-4fc4-8a31-c2b2fdbb0e70`) حتماً. در ادامه نسخه متنی و یکپارچه Railway Capacity Engine v1.1 را می‌فرستم؛ به‌صورت یک package قابل پیاده‌سازی، نه صرفاً چند snippet پراکنده. یک نکته مهم: این نسخه بر اساس schema واقعی aaa.accdb و Excel شما طراحی شده، اما چون فایل‌های واقعی در این لحظه برای اجرا در اختیارم نیستند، اعداد Infrastructure Master را جعل نمی‌کنم. فایل زیر فقط schema و نمونه ساختار Infrastructure Master را نشان می‌دهد و باید با داده تأییدشده شبکه جایگزین شود. Railway Capacity Engine — Version 1.1 1. ساختار پروژه rail_capacity_engine/ │ ├── pyproject.toml ├── requirements.txt ├── config.yml ├── README.md │ ├── app/ │ ├── __init__.py │ ├── main.py │ │ │ ├── domain/ │ │ ├── __init__.py │ │ ├── common.py │ │ ├── train_run.py │ │ ├── infrastructure.py │ │ ├── schedule.py │ │ └── result.py │ │ │ ├── adapters/ │ │ ├── __init__.py │ │ ├── access.py │ │ └── excel.py │ │ │ ├── mapping/ │ │ ├── __init__.py │ │ └── access_mapping.py │ │ │ ├── quality/ │ │ ├── __init__.py │ │ └── gate.py │ │ │ ├── infrastructure/ │ │ ├── __init__.py │ │ └── loader.py │ │ │ ├── scheduling/ │ │ ├── __init__.py │ │ ├── path.py │ │ └── scheduler.py │ │ │ ├── validation/ │ │ ├── __init__.py │ │ └── validator.py │ │ │ ├── capacity/ │ │ ├── __init__.py │ │ └── search.py │ │ │ ├── explanation/ │ │ ├── __init__.py │ │ └── engine.py │ │ │ └── pipeline/ │ ├── __init__.py │ └── vertical_slice.py │ ├── data/ │ ├── source/ │ │ ├── aaa.accdb │ │ ├── REPORTKholase_31-06-1405_02-19-35.xlsx │ │ └── README.md │ │ │ ├── infrastructure/ │ │ └── infrastructure.yml │ │ │ └── output/ │ └── tests/ ├── test_time.py ├── test_mapping.py ├── test_path.py └── test_validator.py 2. pyproject.toml TOML [build-system] requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [project] name = "rail-capacity-engine" version = "1.1.0" description = "Iranian railway operational capacity planning engine" requires-python = ">=3.11" dependencies = [ "pandas>=3.0,<4", "openpyxl>=3.1,<4", "pydantic>=2,<3", "PyYAML>=6,<7", "ortools>=9.14,<10" ] [project.optional-dependencies] access = [ "pyodbc>=5,<6" ] dev = [ "pytest>=8,<10" ] [project.scripts] rail-capacity = "app.main:main" [tool.pytest.ini_options] pythonpath = ["."] testpaths = ["tests"] 3. requirements.txt pandas>=3.0,<4 openpyxl>=3.1,<4 pydantic>=2,<3 PyYAML>=6,<7 ortools>=9.14,<10 pytest>=8,<10 برای Access: pyodbc>=5,<6 4. مدل زمان app/domain/common.py Python from __future__ import annotations DAY_MINUTES = 24 * 60 def parse_hhmm(value: str) -> int: if value is None: raise ValueError("Time value cannot be None") value = str(value).strip() hour, minute = value.split(":") hour = int(hour) minute = int(minute) if not 0 <= hour <= 23: raise ValueError(f"Invalid hour: {hour}") if not 0 <= minute <= 59: raise ValueError(f"Invalid minute: {minute}") return hour * 60 + minute def normalize_forward( previous_minute: int, current_clock_minute: int, ) -> int: """ Converts a clock time into the next occurrence after previous_minute. Example: previous = 23:46 current = 00:36 returns: 24:36 """ while current_clock_minute < previous_minute: current_clock_minute += DAY_MINUTES return current_clock_minute 5. Train Run app/domain/train_run.py Python from __future__ import annotations from dataclasses import dataclass, field from typing import Optional @dataclass(frozen=True) class TrainStationCall: train_run_id: str sequence: int station_id: str arrival_minute: int dwell_minutes: int departure_minute: int required_wait_minutes: Optional[int] = None kilometerage: Optional[float] = None source_distance: Optional[float] = None derived_distance: Optional[float] = None max_speed: Optional[float] = None baseline_running_time_to_next: Optional[int] = None source_record_id: Optional[str] = None @dataclass class TrainRun: train_run_id: str train_no: str train_name: str origin_station_id: str destination_station_id: str direction: str station_calls: list[TrainStationCall] = field( default_factory=list ) earliest_departure: Optional[int] = None latest_arrival: Optional[int] = None train_length_m: Optional[float] = None train_weight_t: Optional[float] = None 6. Infrastructure Master app/domain/infrastructure.py Python from __future__ import annotations from dataclasses import dataclass, field from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" @dataclass(frozen=True) class StationTrack: track_id: str length_m: float @dataclass class StationMaster: station_id: str name: str usable_length_m: float tracks: list[StationTrack] = field( default_factory=list ) @dataclass(frozen=True) class BlockMaster: block_id: str from_station: str to_station: str track_type: TrackType baseline_running_time_min: int clearance_time_min: int = 0 same_direction_headway_min: int = 0 opposite_direction_switch_min: int = 0 @dataclass(frozen=True) class JunctionConflict: movement_a: str movement_b: str separation_min: int = 0 @dataclass class InfrastructureMaster: stations: dict[str, StationMaster] blocks: dict[str, BlockMaster] junction_conflicts: list[JunctionConflict] = field( default_factory=list ) def physical_block_id( station_a: str, station_b: str, ) -> str: return "::".join( sorted( ( station_a, station_b, ) ) ) def movement_resource( block: BlockMaster, direction: str, ) -> str: if block.track_type == TrackType.SINGLE: return ( f"BLOCK::{block.block_id}" ) return ( f"BLOCK::{block.block_id}" f"::{direction}" ) 7. Schedule Model app/domain/schedule.py Python from __future__ import annotations from dataclasses import dataclass, field @dataclass(frozen=True) class MovementSchedule: train_run_id: str sequence: int from_station: str to_station: str arrival: int departure: int block_entry: int block_exit: int block_clear: int resource_id: str @dataclass class TrainSchedule: train_run_id: str movements: list[MovementSchedule] = field( default_factory=list ) @dataclass class Schedule: trains: list[TrainSchedule] = field( default_factory=list ) status: str = "UNKNOWN" 8. Result Model app/domain/result.py Python from __future__ import annotations from dataclasses import dataclass, field, asdict import json @dataclass class ValidationIssue: code: str severity: str message: str train_run_id: str | None = None @dataclass class ValidationReport: valid: bool issues: list[ValidationIssue] = field( default_factory=list ) @dataclass class CapacityProof: capacity: int f_feasible: bool f_validated: bool f_plus_1_tested: bool f_plus_1_infeasible: bool proof_valid: bool @dataclass class BindingConstraint: constraint_type: str resource_id: str explanation: str slack: float | None = None @dataclass class RunResult: run_id: str model_version: str data_version: str scenario_id: str status: str capacity: int | None = None capacity_proof: CapacityProof | None = None validation: ValidationReport | None = None binding_constraints: list[BindingConstraint] = field( default_factory=list ) metadata: dict = field( default_factory=dict ) def write_json(self, path: str): def convert(obj): if hasattr( obj, "__dataclass_fields__", ): return { key: convert(value) for key, value in asdict(obj).items() } if isinstance(obj, list): return [ convert(x) for x in obj ] if isinstance(obj, dict): return { key: convert(value) for key, value in obj.items() } return obj with open( path, "w", encoding="utf-8", ) as f: json.dump( convert(self), f, ensure_ascii=False, indent=2, ) 9. Access Adapter app/adapters/access.py Python from __future__ import annotations from pathlib import Path import pandas as pd class AccessAdapter: def __init__( self, database_path: str, ): self.database_path = Path( database_path ) def _connection_string(self): return ( "DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};" f"DBQ={self.database_path};" ) def list_tables(self): import pyodbc with pyodbc.connect( self._connection_string() ) as conn: return sorted( { row.table_name for row in conn.cursor().tables() if row.table_type == "TABLE" } ) def extract_table( self, table_name: str, ): import pyodbc allowed_tables = set( self.list_tables() ) if table_name not in allowed_tables: raise ValueError( f"Unknown Access table: {table_name}" ) with pyodbc.connect( self._connection_string() ) as conn: return pd.read_sql( f"SELECT * FROM [{table_name}]", conn, ) 10. Excel Adapter app/adapters/excel.py Python from __future__ import annotations from pathlib import Path import pandas as pd class ExcelAdapter: def __init__( self, file_path: str, ): self.file_path = Path( file_path ) def sheets(self): return pd.ExcelFile( self.file_path ).sheet_names def extract( self, sheet_name: str | None = None, ): sheet = ( sheet_name or self.sheets()[0] ) return pd.read_excel( self.file_path, sheet_name=sheet, dtype=object, ) 11. Access Mapping این فایل قلب اتصال schema واقعی Access به Domain Model است. app/mapping/access_mapping.py Python from __future__ import annotations import math from typing import Any from app.domain.common import ( parse_hhmm, normalize_forward, ) from app.domain.train_run import ( TrainRun, TrainStationCall, ) REQUIRED_COLUMNS = { "ID", "TrainNo", "TrainName", "StationName", "Sequence", "time_in", "time_take", "seir", } def clean(value: Any): if value is None: return None if ( isinstance(value, float) and math.isnan(value) ): return None return value def text(value: Any) -> str: value = clean(value) if value is None: return "" return str(value).strip() def integer(value: Any): value = clean(value) if value is None or value == "": return None return int(float(value)) def number(value: Any): value = clean(value) if value is None or value == "": return None return float(value) def map_access_train_records( df, ) -> list[TrainRun]: missing = ( REQUIRED_COLUMNS - set(df.columns) ) if missing: raise ValueError( "Missing required Access fields: " + str(sorted(missing)) ) runs = [] grouped = df.groupby( df["TrainNo"].astype(str), sort=False, ) for train_no, group in grouped: rows = list( group .sort_values("Sequence") .to_dict("records") ) if not rows: continue calls = [] previous_arrival = None for index, row in enumerate(rows): clock = parse_hhmm( text(row["time_in"]) ) if previous_arrival is None: arrival = clock else: arrival = normalize_forward( previous_arrival, clock, ) dwell = max( integer(row["time_take"]) or 0, 0, ) departure = ( arrival + dwell ) required_wait = integer( row.get("RequiredWait") ) seir = integer( row.get("seir") ) km = number( row.get("Kilometerage") ) next_km = None if index + 1 < len(rows): next_km = number( rows[index + 1] .get("Kilometerage") ) derived_distance = None if ( km is not None and next_km is not None ): derived_distance = abs( next_km - km ) call = TrainStationCall( train_run_id=str( train_no ), sequence=integer( row["Sequence"] ), station_id=text( row["StationName"] ), arrival_minute=arrival, dwell_minutes=dwell, departure_minute=departure, required_wait_minutes=required_wait, kilometerage=km, source_distance=number( row.get("Distance") ), derived_distance=derived_distance, max_speed=number( row.get("MaxSpeed") ), baseline_running_time_to_next=seir, source_record_id=text( row["ID"] ), ) calls.append(call) if seir is not None: previous_arrival = ( departure + seir ) else: previous_arrival = ( departure ) first = calls[0] last = calls[-1] direction = "FORWARD" if ( first.kilometerage is not None and last.kilometerage is not None ): direction = ( "FORWARD" if last.kilometerage >= first.kilometerage else "REVERSE" ) runs.append( TrainRun( train_run_id=str( train_no ), train_no=str( train_no ), train_name=text( rows[0]["TrainName"] ), origin_station_id=( first.station_id ), destination_station_id=( last.station_id ), direction=direction, station_calls=calls, ) ) return runs 12. Data Quality Gate app/quality/gate.py Python from __future__ import annotations from dataclasses import dataclass @dataclass class QualityIssue: code: str severity: str message: str train_run_id: str | None = None class DataQualityReport: def __init__( self, issues=None, ): self.issues = ( issues or [] ) @property def errors(self): return [ issue for issue in self.issues if issue.severity == "ERROR" ] @property def warnings(self): return [ issue for issue in self.issues if issue.severity == "WARNING" ] def raise_if_blocking(self): if not self.errors: return messages = [ ( f"{x.code}: " f"{x.message}" ) for x in self.errors ] raise ValueError( "Blocking data-quality issues:\n" + "\n".join(messages) ) def validate_train_runs( train_runs, ): issues = [] for run in train_runs: if not run.station_calls: issues.append( QualityIssue( "EMPTY_RUN", "ERROR", "TrainRun has no station calls", run.train_run_id, ) ) continue for index, call in enumerate( run.station_calls ): if ( call.departure_minute < call.arrival_minute ): issues.append( QualityIssue( "NEGATIVE_DWELL", "ERROR", "Departure before arrival", run.train_run_id, ) ) if ( call.dwell_minutes < ( call.required_wait_minutes or 0 ) ): issues.append( QualityIssue( "REQUIRED_WAIT_VIOLATION", "ERROR", ( f"Sequence {call.sequence}: " "dwell below RequiredWait" ), run.train_run_id, ) ) if ( index < len(run.station_calls) - 1 and call.baseline_running_time_to_next is None ): issues.append( QualityIssue( "MISSING_SEIR", "ERROR", ( f"Missing seir at " f"sequence {call.sequence}" ), run.train_run_id, ) ) return DataQualityReport( issues ) 13. Infrastructure Loader app/infrastructure/loader.py Python from __future__ import annotations import yaml from app.domain.infrastructure import ( InfrastructureMaster, StationMaster, StationTrack, BlockMaster, TrackType, JunctionConflict, ) def load_infrastructure( path: str, ) -> InfrastructureMaster: with open( path, "r", encoding="utf-8", ) as f: data = yaml.safe_load(f) or {} stations = {} for station_id, item in ( data.get("stations") or {} ).items(): tracks = [ StationTrack( track_id=str(track["id"]), length_m=float( track["length_m"] ), ) for track in item.get( "tracks", [], ) ] stations[station_id] = ( StationMaster( station_id=station_id, name=item.get( "name", station_id, ), usable_length_m=float( item.get( "usable_length_m", 0, ) ), tracks=tracks, ) ) blocks = {} for block_id, item in ( data.get("blocks") or {} ).items(): blocks[block_id] = BlockMaster( block_id=block_id, from_station=item[ "from_station" ], to_station=item[ "to_station" ], track_type=TrackType( item["track_type"] ), baseline_running_time_min=int( item[ "baseline_running_time_min" ] ), clearance_time_min=int( item.get( "clearance_time_min", 0, ) ), same_direction_headway_min=int( item.get( "same_direction_headway_min", 0, ) ), opposite_direction_switch_min=int( item.get( "opposite_direction_switch_min", 0, ) ), ) junction_conflicts = [ JunctionConflict( movement_a=item[ "movement_a" ], movement_b=item[ "movement_b" ], separation_min=int( item.get( "separation_min", 0, ) ), ) for item in ( data.get( "junction_conflicts", [], ) ) ] return InfrastructureMaster( stations=stations, blocks=blocks, junction_conflicts=( junction_conflicts ), ) 14. Directed Path app/scheduling/path.py Python from __future__ import annotations from dataclasses import dataclass from app.domain.infrastructure import ( InfrastructureMaster, physical_block_id, movement_resource, ) @dataclass(frozen=True) class RouteSegment: sequence: int from_station: str to_station: str physical_block_id: str resource_id: str direction: str running_time_min: int clearance_time_min: int def build_path( train_run, infrastructure: InfrastructureMaster, ): calls = sorted( train_run.station_calls, key=lambda x: x.sequence, ) segments = [] for index in range( len(calls) - 1 ): current = calls[index] next_call = calls[ index + 1 ] physical_id = physical_block_id( current.station_id, next_call.station_id, ) block = infrastructure.blocks.get( physical_id ) if block is None: raise ValueError( "Missing Infrastructure Master " f"block: {physical_id}" ) direction = ( "FORWARD" if ( current.kilometerage is None or next_call.kilometerage is None or next_call.kilometerage >= current.kilometerage ) else "REVERSE" ) running_time = ( current .baseline_running_time_to_next ) if running_time is None: running_time = ( block.baseline_running_time_min ) segments.append( RouteSegment( sequence=index + 1, from_station=( current.station_id ), to_station=( next_call.station_id ), physical_block_id=( physical_id ), resource_id=( movement_resource( block, direction, ) ), direction=direction, running_time_min=( running_time ), clearance_time_min=( block.clearance_time_min ), ) ) return segments 15. CP-SAT Scheduler app/scheduling/scheduler.py Python from __future__ import annotations from ortools.sat.python import cp_model from app.domain.schedule import ( Schedule, TrainSchedule, MovementSchedule, ) class CPSATScheduler: def __init__( self, time_limit_seconds=30, workers=1, random_seed=1, ): self.time_limit_seconds = ( time_limit_seconds ) self.workers = workers self.random_seed = ( random_seed ) def solve( self, train_runs, paths, infrastructure, ): model = cp_model.CpModel() horizon = 48 * 60 variables = {} resource_intervals = {} for run, path in zip( train_runs, paths, ): calls = sorted( run.station_calls, key=lambda x: x.sequence, ) arrivals = {} departures = {} movements = [] for index, call in enumerate( calls ): arrival = model.new_int_var( 0, horizon, ( f"arrival_" f"{run.train_run_id}_" f"{index}" ), ) departure = model.new_int_var( 0, horizon, ( f"departure_" f"{run.train_run_id}_" f"{index}" ), ) minimum_dwell = max( call.dwell_minutes, call.required_wait_minutes or 0, ) model.add( departure >= arrival + minimum_dwell ) if ( index == 0 and run.earliest_departure is not None ): model.add( departure >= run.earliest_departure ) arrivals[index] = arrival departures[index] = departure for index, segment in enumerate( path ): entry = model.new_int_var( 0, horizon, ( f"entry_" f"{run.train_run_id}_" f"{index}" ), ) exit_ = model.new_int_var( 0, horizon, ( f"exit_" f"{run.train_run_id}_" f"{index}" ), ) clear = model.new_int_var( 0, horizon, ( f"clear_" f"{run.train_run_id}_" f"{index}" ), ) model.add( entry >= departures[index] ) model.add( exit_ >= ( entry + segment.running_time_min ) ) model.add( clear >= ( exit_ + segment.clearance_time_min ) ) model.add( arrivals[index + 1] >= exit_ ) duration = ( segment.running_time_min + segment.clearance_time_min ) interval = ( model.new_interval_var( entry, max( 1, duration, ), clear, ( f"interval_" f"{run.train_run_id}_" f"{index}" ), ) ) resource_intervals.setdefault( segment.resource_id, [], ).append( interval ) movements.append( ( segment, entry, exit_, clear, ) ) variables[ run.train_run_id ] = ( calls, arrivals, departures, movements, ) # Physical resource constraints. for ( resource_id, intervals, ) in resource_intervals.items(): model.add_no_overlap( intervals ) # Baseline deviation objective. deviations = [] for run in train_runs: ( calls, arrivals, departures, movements, ) = variables[ run.train_run_id ] for index, call in enumerate( calls ): deviation = ( model.new_int_var( 0, horizon, ( f"dev_" f"{run.train_run_id}_" f"{index}" ), ) ) target = ( call.arrival_minute ) model.add( deviation >= ( arrivals[index] - target ) ) model.add( deviation >= ( target - arrivals[index] ) ) deviations.append( deviation ) if deviations: model.minimize( sum(deviations) ) solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = ( self.time_limit_seconds ) solver.parameters.num_search_workers = ( self.workers ) solver.parameters.random_seed = ( self.random_seed ) status = solver.solve( model ) feasible = status in ( cp_model.OPTIMAL, cp_model.FEASIBLE, ) if not feasible: return Schedule( status="INFEASIBLE" ) train_schedules = [] for run in train_runs: ( calls, arrivals, departures, movements, ) = variables[ run.train_run_id ] movement_results = [] for index, ( segment, entry, exit_, clear, ) in enumerate( movements ): movement_results.append( MovementSchedule( train_run_id=( run.train_run_id ), sequence=index + 1, from_station=( segment.from_station ), to_station=( segment.to_station ), arrival=solver.value( arrivals[index + 1] ), departure=solver.value( departures[index] ), block_entry=solver.value( entry ), block_exit=solver.value( exit_ ), block_clear=solver.value( clear ), resource_id=( segment.resource_id ), ) ) train_schedules.append( TrainSchedule( train_run_id=( run.train_run_id ), movements=( movement_results ), ) ) return Schedule( trains=train_schedules, status="FEASIBLE", ) 16. Independent Validator app/validation/validator.py Python from __future__ import annotations from app.domain.result import ( ValidationIssue, ValidationReport, ) def validate_schedule( schedule, train_runs, paths, ): issues = [] if schedule.status != "FEASIBLE": return ValidationReport( valid=False, issues=[ ValidationIssue( code="SOLVER_NOT_FEASIBLE", severity="ERROR", message=( "Generated schedule " "is not feasible." ), ) ], ) run_by_id = { run.train_run_id: run for run in train_runs } intervals = {} for train_schedule in ( schedule.trains ): run = run_by_id[ train_schedule.train_run_id ] calls = sorted( run.station_calls, key=lambda x: x.sequence, ) expected_movements = max( 0, len(calls) - 1, ) if ( len( train_schedule.movements ) != expected_movements ): issues.append( ValidationIssue( "MOVEMENT_COUNT", "ERROR", ( "Generated movement count " "does not match route." ), run.train_run_id, ) ) continue previous_clear = None for movement in ( train_schedule.movements ): if ( movement.block_exit < movement.block_entry ): issues.append( ValidationIssue( "NEGATIVE_RUN", "ERROR", "Block exit before entry.", run.train_run_id, ) ) if ( movement.block_clear < movement.block_exit ): issues.append( ValidationIssue( "NEGATIVE_CLEAR", "ERROR", "Block clear before exit.", run.train_run_id, ) ) if ( previous_clear is not None and movement.block_entry < previous_clear ): issues.append( ValidationIssue( "ROUTE_ORDER", "ERROR", ( "Movement entered before " "previous block was cleared." ), run.train_run_id, ) ) intervals.setdefault( movement.resource_id, [], ).append( ( movement.block_entry, movement.block_clear, movement.train_run_id, movement.sequence, ) ) previous_clear = ( movement.block_clear ) # Independent resource overlap check. for ( resource_id, resource_intervals, ) in intervals.items(): resource_intervals.sort( key=lambda x: x[0] ) for first, second in zip( resource_intervals, resource_intervals[1:], ): if ( second[0] < first[1] ): issues.append( ValidationIssue( "RESOURCE_OVERLAP", "ERROR", ( f"Resource overlap on " f"{resource_id}: " f"{first[2]} vs " f"{second[2]}" ), ) ) valid = not any( issue.severity == "ERROR" for issue in issues ) return ValidationReport( valid=valid, issues=issues, ) 17. Candidate Capacity Search در v1.1 عمداً ظرفیت را از تعداد قطارهای مصنوعی تولیدشده محاسبه نمی‌کنیم. app/capacity/search.py Python from __future__ import annotations def search_candidate_capacity( candidate_runs, build_problem_and_solve, validate, ): if not candidate_runs: return 0, None low = 0 high = len( candidate_runs ) best = None while low < high: mid = ( low + high + 1 ) // 2 subset = candidate_runs[ :mid ] schedule = ( build_problem_and_solve( subset ) ) validation = validate( schedule, subset, ) if ( schedule.status == "FEASIBLE" and validation.valid ): low = mid best = ( schedule, validation, ) else: high = mid - 1 return low, best اما دقت کنید: این هنوز Route Capacity واقعی بر مبنای frequency/day نیست. این فقط: Maximum Feasible Candidate Set است. برای ظرفیت واقعی باید OperatingPattern و Frequency Expansion وارد مدل شود. 18. Explanation Engine app/explanation/engine.py Python from __future__ import annotations from app.domain.result import ( BindingConstraint, ) def explain( schedule, validation, ): if not validation.valid: return [ BindingConstraint( constraint_type="VALIDATION", resource_id="SCHEDULE", explanation=( "Generated schedule failed " "independent validation." ), ) ] usage = {} for train in schedule.trains: for movement in train.movements: usage.setdefault( movement.resource_id, 0, ) usage[ movement.resource_id ] += 1 if not usage: return [] resource, count = max( usage.items(), key=lambda item: item[1], ) return [ BindingConstraint( constraint_type=( "RESOURCE_USAGE" ), resource_id=resource, explanation=( f"Resource appears in " f"{count} generated movements. " "This is a diagnostic indicator " "and not, by itself, proof of a " "binding bottleneck." ), ) ] این نکته را عمداً گذاشته‌ام تا یک اشتباه رایج رخ ندهد: High Utilization ≠ Automatically Bottleneck باید marginal capacity impact نیز بررسی شود. 19. Vertical Slice app/pipeline/vertical_slice.py Python from __future__ import annotations from datetime import datetime, timezone import uuid from app.adapters.access import ( AccessAdapter, ) from app.adapters.excel import ( ExcelAdapter, ) from app.mapping.access_mapping import ( map_access_train_records, ) from app.quality.gate import ( validate_train_runs, ) from app.infrastructure.loader import ( load_infrastructure, ) from app.scheduling.path import ( build_path, ) from app.scheduling.scheduler import ( CPSATScheduler, ) from app.validation.validator import ( validate_schedule, ) from app.explanation.engine import ( explain, ) from app.domain.result import ( RunResult, CapacityProof, ) def run_vertical_slice( access_path: str, excel_path: str, infrastructure_path: str, access_table: str, time_limit_seconds: int = 30, ): # ------------------------------------------------ # 1. Extract # ------------------------------------------------ access_df = ( AccessAdapter( access_path ) .extract_table( access_table ) ) excel_df = ( ExcelAdapter( excel_path ) .extract() ) # ------------------------------------------------ # 2. Mapping # ------------------------------------------------ train_runs = ( map_access_train_records( access_df ) ) # ------------------------------------------------ # 3. Quality # ------------------------------------------------ quality = ( validate_train_runs( train_runs ) ) quality.raise_if_blocking() # ------------------------------------------------ # 4. Infrastructure # ------------------------------------------------ infrastructure = ( load_infrastructure( infrastructure_path ) ) # ------------------------------------------------ # 5. Path # ------------------------------------------------ def build_paths(runs): return [ build_path( run, infrastructure, ) for run in runs ] # ------------------------------------------------ # 6. Solve # ------------------------------------------------ def solve_and_validate( runs ): paths = build_paths( runs ) scheduler = ( CPSATScheduler( time_limit_seconds=( time_limit_seconds ), workers=1, random_seed=1, ) ) schedule = scheduler.solve( runs, paths, infrastructure, ) validation = ( validate_schedule( schedule, runs, paths, ) ) return ( schedule, validation, ) # ------------------------------------------------ # 7. Baseline / Schedule # ------------------------------------------------ schedule, validation = ( solve_and_validate( train_runs ) ) # ------------------------------------------------ # 8. Candidate capacity # ------------------------------------------------ capacity = ( len(train_runs) if ( schedule.status == "FEASIBLE" and validation.valid ) else 0 ) # ------------------------------------------------ # 9. Proof # ------------------------------------------------ proof = CapacityProof( capacity=capacity, f_feasible=( schedule.status == "FEASIBLE" ), f_validated=( validation.valid ), f_plus_1_tested=False, f_plus_1_infeasible=False, proof_valid=False, ) # ------------------------------------------------ # 10. Explanation # ------------------------------------------------ explanations = explain( schedule, validation, ) # ------------------------------------------------ # 11. Result # ------------------------------------------------ return RunResult( run_id=( "RUN-" + uuid.uuid4() .hex[:10] ), model_version="1.1.0", data_version=( datetime.now( timezone.utc ).strftime( "SOURCE-%Y%m%d" ) ), scenario_id="BASE", status=( "VALIDATED" if validation.valid else "INVALID" ), capacity=capacity, capacity_proof=proof, validation=validation, binding_constraints=( explanations ), metadata={ "access_rows": int( len(access_df) ), "excel_rows": int( len(excel_df) ), "train_runs": len( train_runs ), }, ) 20. CLI app/main.py Python from __future__ import annotations import argparse import json from app.adapters.access import ( AccessAdapter, ) from app.pipeline.vertical_slice import ( run_vertical_slice, ) def main(): parser = argparse.ArgumentParser( description=( "Iranian Railway " "Capacity Engine" ) ) parser.add_argument( "--access", required=False, ) parser.add_argument( "--excel", required=False, ) parser.add_argument( "--infrastructure", required=False, ) parser.add_argument( "--table", required=False, ) parser.add_argument( "--output", required=False, ) parser.add_argument( "--inspect-access", action="store_true", ) args = parser.parse_args() # ---------------------------------------------- # Access table discovery # ---------------------------------------------- if args.inspect_access: if not args.access: parser.error( "--access is required" ) adapter = AccessAdapter( args.access ) for table in ( adapter.list_tables() ): print(table) return required = [ "access", "excel", "infrastructure", "table", "output", ] missing = [ name for name in required if not getattr(args, name) ] if missing: parser.error( "Missing arguments: " + ", ".join( f"--{x}" for x in missing ) ) result = ( run_vertical_slice( access_path=args.access, excel_path=args.excel, infrastructure_path=( args.infrastructure ), access_table=args.table, ) ) result.write_json( args.output ) print( json.dumps( { "run_id": result.run_id, "status": result.status, "capacity": result.capacity, "proof_valid": ( result .capacity_proof .proof_valid ), }, ensure_ascii=False, indent=2, ) ) if __name__ == "__main__": main() 21. Infrastructure YAML فایل: data/infrastructure/infrastructure.yml باید به شکل زیر باشد: YAML stations: GAR: name: "Gar" usable_length_m: 700 tracks: - id: "GAR-01" length_m: 700 SAKHEH: name: "Sakheh" usable_length_m: 650 tracks: - id: "SAKHEH-01" length_m: 650 blocks: GAR::SAKHEH: from_station: GAR to_station: SAKHEH track_type: SINGLE baseline_running_time_min: 34 clearance_time_min: 2 same_direction_headway_min: 2 opposite_direction_switch_min: 5 junction_conflicts: [] هشدار: مقادیر بالا فقط نمونه schema هستند. نباید به‌عنوان داده واقعی شبکه استفاده شوند. برای کل شبکه واقعی باید چیزی شبیه این داشته باشیم: StationMaster | +-- StationTrack +-- Usable Length +-- Crossing Capability +-- Overtaking Capability +-- Formation Capability +-- Loading/Unloading Capability +-- Arrival/Departure Routes | PhysicalBlock | +-- TrackType +-- Length +-- Chainage +-- Direction +-- Running Time +-- Clearance Time +-- Headway | Junction | +-- Movement +-- Conflict Matrix +-- Separation 22. تست Midnight tests/test_time.py Python from app.domain.common import ( parse_hhmm, normalize_forward, ) def test_parse_hhmm(): assert ( parse_hhmm("09:34") == 574 ) def test_midnight_rollover(): previous = parse_hhmm( "23:46" ) current = parse_hhmm( "00:36" ) normalized = ( normalize_forward( previous, current, ) ) assert normalized == ( 24 * 60 + 36 ) 23. تست Mapping tests/test_mapping.py Python import pandas as pd from app.mapping.access_mapping import ( map_access_train_records, ) def test_seir_and_direction(): df = pd.DataFrame( [ { "ID": 1, "TrainNo": 100, "TrainName": "Gar-Andimeshk1", "StationName": "Gar", "Sequence": 1, "time_in": "09:00", "time_take": 0, "RequiredWait": 0, "Kilometerage": 157, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": 34, }, { "ID": 2, "TrainNo": 100, "TrainName": "Gar-Andimeshk1", "StationName": "Sakheh", "Sequence": 2, "time_in": "09:34", "time_take": 5, "RequiredWait": 5, "Kilometerage": 200, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": None, }, ] ) runs = ( map_access_train_records( df ) ) assert len(runs) == 1 run = runs[0] assert ( run.direction == "FORWARD" ) assert ( run.station_calls[0] .baseline_running_time_to_next == 34 ) assert ( run.station_calls[0] .derived_distance == 43 ) 24. تست Physical Block tests/test_path.py Python from app.domain.infrastructure import ( InfrastructureMaster, BlockMaster, TrackType, ) from app.domain.train_run import ( TrainRun, TrainStationCall, ) from app.scheduling.path import ( build_path, ) def test_single_track_resource(): infrastructure = ( InfrastructureMaster( stations={}, blocks={ "A::B": BlockMaster( block_id="A::B", from_station="A", to_station="B", track_type=TrackType.SINGLE, baseline_running_time_min=10, ) }, ) ) run = TrainRun( train_run_id="1", train_no="1", train_name="TEST", origin_station_id="A", destination_station_id="B", direction="FORWARD", station_calls=[ TrainStationCall( train_run_id="1", sequence=1, station_id="A", arrival_minute=0, dwell_minutes=0, departure_minute=0, kilometerage=0, baseline_running_time_to_next=10, ), TrainStationCall( train_run_id="1", sequence=2, station_id="B", arrival_minute=10, dwell_minutes=0, departure_minute=10, kilometerage=10, ), ], ) path = build_path( run, infrastructure, ) assert ( path[0].resource_id == "BLOCK::A::B" ) 25. تست Independent Validator Python from app.domain.schedule import ( Schedule, TrainSchedule, MovementSchedule, ) from app.domain.train_run import ( TrainRun, TrainStationCall, ) from app.scheduling.path import ( RouteSegment, ) from app.validation.validator import ( validate_schedule, ) def test_validator_accepts_nonoverlap(): run = TrainRun( train_run_id="1", train_no="1", train_name="TEST", origin_station_id="A", destination_station_id="B", direction="FORWARD", station_calls=[ TrainStationCall( train_run_id="1", sequence=1, station_id="A", arrival_minute=0, dwell_minutes=0, departure_minute=0, baseline_running_time_to_next=10, ), TrainStationCall( train_run_id="1", sequence=2, station_id="B", arrival_minute=10, dwell_minutes=0, departure_minute=10, ), ], ) schedule = Schedule( trains=[ TrainSchedule( train_run_id="1", movements=[ MovementSchedule( train_run_id="1", sequence=1, from_station="A", to_station="B", arrival=10, departure=0, block_entry=0, block_exit=10, block_clear=10, resource_id=( "BLOCK::A::B" ), ) ], ) ], status="FEASIBLE", ) path = [ RouteSegment( sequence=1, from_station="A", to_station="B", physical_block_id="A::B", resource_id="BLOCK::A::B", direction="FORWARD", running_time_min=10, clearance_time_min=0, ) ] report = validate_schedule( schedule, [run], [path], ) assert report.valid 26. اجرای سیستم ابتدا: Bash pip install -e ".[access,dev]" بعد برای دیدن tableهای Access: Bash rail-capacity \ --access data/source/aaa.accdb \ --inspect-access مثلاً خروجی می‌تواند چیزی شبیه: TableA TableB ... باشد. ما نباید نام table را حدس بزنیم. سپس: Bash rail-capacity \ --access data/source/aaa.accdb \ --excel data/source/REPORTKholase_31-06-1405_02-19-35.xlsx \ --infrastructure data/infrastructure/infrastructure.yml \ --table REAL_TABLE_NAME \ --output data/output/run.json 27. خروجی خروجی باید چیزی در این سطح داشته باشد: JSON { "run_id": "RUN-xxxxxxxxxx", "model_version": "1.1.0", "data_version": "SOURCE-20260928", "scenario_id": "BASE", "status": "VALIDATED", "capacity": 2, "capacity_proof": { "capacity": 2, "f_feasible": true, "f_validated": true, "f_plus_1_tested": false, "f_plus_1_infeasible": false, "proof_valid": false }, "validation": { "valid": true, "issues": [] }, "binding_constraints": [], "metadata": { "access_rows": 64, "excel_rows": 20, "train_runs": 2 } } عدد 2 در این مثال فقط نشان‌دهنده دو candidate واقعی مانند Train 100 و 101 است؛ این عدد به هیچ وجه ظرفیت مسیر نیست. 28. اصلاح بسیار مهم نسبت به Version 1.0 در نسخه جدید، این سه مفهوم را کاملاً جدا می‌کنیم: Candidate Train Runs ≠ Operational Frequency ≠ Route Capacity مثلاً اگر Access دارای: 100 101 باشد، نتیجه: 2 feasible train runs فقط می‌گوید: این دو movement قابل schedule شدن هستند. اما نمی‌توانیم بگوییم: Cr = 2 برای ظرفیت مسیر. برای رسیدن به: C r ​ =maxF باید بتوانیم F را روی یک Operating Pattern معتبر افزایش دهیم. 29. بنابراین Version 1.2 نسخه بعدی باید این objectها را اضافه کند: OperatingPattern OperatingPatternRule Calendar Frequency DepartureWindow ArrivalWindow Turnaround TrainService CandidateGenerationPolicy مثلاً: YAML service: id: GAR-ANDIMESHK-FREIGHT origin: GAR destination: ANDIMESHK direction: FORWARD frequency: type: DAILY value: 4 departure_window: start: "06:00" end: "22:00" train_type: id: FREIGHT-100 formation: length_m: 600 weight_t: 3000 سپس: Operating Pattern ↓ Generate Candidate Runs ↓ Scheduling ↓ Feasibility ↓ F ↓ F+1 ↓ Capacity Proof آن زمان می‌توانیم واقعاً به: C r ​ =max{F:Schedule(F) feasible} نزدیک شویم. 30. یک اصلاح دیگر: time_out در داده Access شما: time_in time_take time_out وجود دارد. در مدل canonical: arrival dwell departure را داریم. ولی نباید time_out را حذف کنیم. بهتر است در Source Evidence نگهداری شود: Source: time_in time_take time_out Canonical: arrival dwell departure Derived: normalized departure Validation: source time_out vs arrival + dwell بنابراین بعداً می‌توانیم گزارش کنیم: TIME_OUT_CONSISTENCY مثلاً: TrainNo = 100 Station = Dorud time_in = 18:11 time_take = 120 expected_time_out = 20:11 source_time_out = 20:11 STATUS = CONSISTENT 31. جایگاه Excel در این معماری Excel نباید با Access مخلوط شود. مدل: Excel ↓ Service / OD / Calendar Evidence و: Access ↓ Detailed Operational Movement Evidence سپس: ┌── Excel │ Sources ─────┤ │ └── Access ↓ Reconciliation ↓ Canonical Model این بسیار مهم است، چون Excel شما اطلاعاتی مانند: نام قطار شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت ساعت ورود مقصد شماره قطار از مقصد ... دارد، در حالی که Access جزئیات: Station Sequence time_in time_take time_out seir Kilometerage ... را می‌دهد. 32. Reconciliation که باید در گام بعدی اضافه شود مثلاً: Excel: Tehran → Van TrainNo = X Departure = 08:00 Access: TrainNo = X Station sequence = 1..N باید یک: TrainIdentityResolution داشته باشیم. و نتیجه: Excel Service ↓ Access TrainRun ↓ MATCH یا: MATCH MATCH_WITH_WARNING UNRESOLVED CONFLICT نباید صرفاً با TrainNo match کنیم اگر در داده واقعی شماره‌ها در contextهای مختلف تکرار می‌شوند. 33. وضعیت واقعی پروژه بعد از این نسخه اکنون معماری به این شکل است: ┌──────────────────┐ │ Excel │ │ Service / OD │ │ Calendar │ └────────┬─────────┘ │ │ ┌────────▼─────────┐ │ Reconciliation │ └────────┬─────────┘ │ ┌───────────────┐ │ │ Access │ │ │ TrainMovement │────────────┤ └───────────────┘ │ ▼ ┌──────────────────┐ │ Canonical Model │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Directed Path │ └────────┬─────────┘ │ ┌─────────────▼─────────────┐ │ Infrastructure Master │ │ Blocks / Stations / etc. │ └─────────────┬─────────────┘ │ ▼ ┌──────────────────┐ │ CP-SAT Scheduler │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Generated │ │ Schedule │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Independent │ │ Validator │ └────────┬─────────┘ │ ┌────────▼────────┐ │ Feasible? │ └────────┬────────┘ │ ▼ ┌──────────────────┐ │ Capacity / Proof │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Bottleneck / │ │ Explanation │ └──────────────────┘ 34. و جایگاه Versionهای بعدی بعد از اینکه این هسته با داده واقعی شما تثبیت شد: V1.1 Real Data → Canonical → Schedule → Validation ↓ V1.2 Operating Pattern → Frequency Capacity ↓ V1.3 Wagon Cycle ↓ V1.4 Locomotive Cycle ↓ V1.5 Train Formation Optimization ↓ V1.6 Freight Flow / Demand ↓ V1.7 Marketplace Allocation ↓ V1.8 Multi-route Network Optimization ↓ V2.0 Production Railway Capacity Platform و مدل نهایی دقیقاً به زنجیره موردنظر شما می‌رسد: Marketplace ↓ Market Request ↓ Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Wagon Cycle ─────────┐ │ Locomotive Cycle ────┤ ↓ TrainRun ↓ Directed TrainPath ↓ Block / Station / Junction ↓ Operational Regime ↓ Scheduling ↓ Feasible Timetable ↓ Independent Validation ↓ Route Capacity ↓ Network Capacity ↓ Capacity Proof ↓ Bottleneck ↓ Scenario & Sensitivity ↓ Marketplace Allocation گام فنی بعدی که ارزش بالاتری دارد، پیاده‌سازی V1.2 — Operating Pattern & Real Capacity Search است؛ یعنی به‌جای اینکه فقط TrainNoهای موجود در Access را schedule کنیم، از اطلاعات واقعی Excel مثل روزهای حرکت از مبدا/مقصد، ساعت حرکت، ساعت ورود و چرخه رفت‌وبرگشت یک OperatingPattern بسازیم و سپس الگوریتم واقعاً F, F+1, ... را تولید، schedule، validate و اثبات کند. این همان نقطه‌ای است که خروجی از «تست زمان‌بندی قطارهای موجود» به محاسبه ظرفیت عملیاتی مسیر تبدیل می‌شود. --- ## ادامه 32 — پرسش کاربر (`message-id:b3baea64-4e12-4625-a2e1-4ea00a074d8e`) بله --- ## ادامه 32 — پاسخ دستیار (`message-id:7415040e-0127-46e6-901b-7d6893567acc`) حتماً. پس می‌رویم سراغ Version 1.2 — Operating Pattern & Real Capacity Search. هدف این نسخه این است که دیگر صرفاً قطارهای موجود در Access را زمان‌بندی نکنیم؛ بلکه از الگوی واقعی بهره‌برداری موجود در Excel، Candidate Train Runs تولید کنیم و سپس ظرفیت مسیر را با آزمون واقعی F / F+1 به دست آوریم. 1. تعریف دقیق ظرفیت در V1.2 در این نسخه: C r ​ =max{F∈Z ≥0 ​ ∣∃S F ​ :S F ​  feasible and validated} یعنی: F ↓ Generate F candidate train runs ↓ Build directed paths ↓ Assign resources ↓ Schedule ↓ Independent validation ↓ FEASIBLE / INFEASIBLE و اثبات ظرفیت: F → FEASIBLE F+1 → INFEASIBLE Therefore: Cr = F اما یک نکته بسیار مهم داریم: F باید متعلق به یک Operating Pattern معتبر باشد، نه اینکه صرفاً قطارهای مصنوعی با فاصله زمانی ثابت بسازیم. 2. Operating Pattern مدل جدید: TrainService │ ├── Origin ├── Destination ├── Direction ├── TrainType ├── LoadState │ └── OperatingPattern │ ├── Calendar ├── Frequency ├── Departure Window ├── Arrival Window ├── Turnaround └── Pattern Rules مدل Python: Python from dataclasses import dataclass, field from typing import Optional @dataclass(frozen=True) class OperatingPattern: pattern_id: str service_id: str origin_station_id: str destination_station_id: str direction: str departures_per_day: int departure_window_start: int departure_window_end: int minimum_headway_min: int operating_days: set[int] = field( default_factory=lambda: { 0, 1, 2, 3, 4, 5, 6 } ) turnaround_min: int = 0 max_daily_frequency: Optional[int] = None 3. Calendar برای سیستم ایرانی، Calendar را نباید فقط با Monday/Tuesday مدل کنیم. در سطح Production بهتر است: OperatingCalendar ↓ Gregorian Date ↓ Persian Date ↓ Operating Day ↓ Day-of-week ↓ Holiday / Special Day ↓ Service Pattern اما برای V1.2 می‌توانیم از یک abstraction ساده استفاده کنیم: Python @dataclass(frozen=True) class OperatingDay: date: str day_index: int is_holiday: bool = False calendar_type: str = "GREGORIAN" 4. Excel Mapping اطلاعات Excel شما باید به OperatingPattern تبدیل شود. Schema شناخته‌شده: نام قطار شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت از مبدا ساعت ورود به مقصد شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت از مقصد ساعت رسیدن به مبدا بنابراین Adapter جدید: Excel ↓ Raw Service Pattern ↓ Normalized Operating Pattern 5. مدل Raw Excel Python from dataclasses import dataclass from typing import Optional @dataclass(frozen=True) class RawExcelService: row_number: int train_name: str outbound_train_no: Optional[str] outbound_departure_time: Optional[str] outbound_days: Optional[str] outbound_arrival_time: Optional[str] return_train_no: Optional[str] return_departure_time: Optional[str] return_days: Optional[str] return_arrival_time: Optional[str] 6. Excel Adapter Python import pandas as pd EXCEL_COLUMNS = { "نام قطار", "شماره قطار از مبدا", "ساعت حرکت از مبدا", "روزهای حرکت از مبدا", "ساعت ورود به مقصد", "شماره قطار از مقصد", "ساعت حرکت از مقصد", "روزهای حرکت از مقصد", "ساعت رسیدن به مبدا", } def normalize_text(value): if pd.isna(value): return None return str(value).strip() def extract_excel_services( dataframe: pd.DataFrame, ): missing = ( EXCEL_COLUMNS - set(dataframe.columns) ) if missing: raise ValueError( f"Missing Excel columns: {sorted(missing)}" ) result = [] for index, row in dataframe.iterrows(): result.append( RawExcelService( row_number=index + 2, train_name=normalize_text( row["نام قطار"] ), outbound_train_no=normalize_text( row["شماره قطار از مبدا"] ), outbound_departure_time=normalize_text( row["ساعت حرکت از مبدا"] ), outbound_days=normalize_text( row["روزهای حرکت از مبدا"] ), outbound_arrival_time=normalize_text( row["ساعت ورود به مقصد"] ), return_train_no=normalize_text( row["شماره قطار از مقصد"] ), return_departure_time=normalize_text( row["ساعت حرکت از مقصد"] ), return_days=normalize_text( row["روزهای حرکت از مقصد"] ), return_arrival_time=normalize_text( row["ساعت رسیدن به مبدا"] ), ) ) return result 7. Day Parser چون روزهای حرکت در فایل واقعی ممکن است به شکل‌های مختلف وارد شده باشد، نباید parser را فقط برای یک notation hard-code کنیم. یک parser اولیه: Python DAY_MAP = { "شنبه": 0, "یکشنبه": 1, "دوشنبه": 2, "سه‌شنبه": 3, "سه شنبه": 3, "چهارشنبه": 4, "پنجشنبه": 5, "جمعه": 6, } def parse_operating_days(value): if not value: return set() normalized = ( value .replace("،", ",") .replace("/", ",") .replace("-", ",") ) parts = [ x.strip() for x in normalized.split(",") if x.strip() ] days = set() for part in parts: if part in DAY_MAP: days.add( DAY_MAP[part] ) return days ولی در Production باید mapping configuration داشته باشیم، نه اینکه semantics روزها در code دفن شوند. 8. Train Service Python from dataclasses import dataclass @dataclass(frozen=True) class TrainService: service_id: str name: str origin_station_id: str destination_station_id: str direction: str train_type_id: str load_state: str pattern_id: str در اینجا: TrainService ≠ TrainRun مثلاً: TrainService: GAR-ANDIMESHK-FREIGHT TrainRun: GAR-ANDIMESHK-2026-09-28-01 GAR-ANDIMESHK-2026-09-28-02 GAR-ANDIMESHK-2026-09-28-03 9. Candidate Run Generator این بخش هسته V1.2 است. Python from dataclasses import replace def generate_candidate_runs( base_run, pattern, requested_frequency, ): if requested_frequency <= 0: return [] if pattern.max_daily_frequency is not None: if ( requested_frequency > pattern.max_daily_frequency ): raise ValueError( "Requested frequency exceeds " "OperatingPattern maximum." ) available_window = ( pattern.departure_window_end - pattern.departure_window_start ) required_span = ( ( requested_frequency - 1 ) * pattern.minimum_headway_min ) if required_span > available_window: return [] runs = [] for i in range( requested_frequency ): departure = ( pattern.departure_window_start + i * pattern.minimum_headway_min ) run_id = ( f"{base_run.train_no}" f"-CAND-{i + 1}" ) run = replace( base_run, train_run_id=run_id, earliest_departure=departure, ) runs.append(run) return runs این implementation اولیه است. در نسخه production، departureها باید توسط Candidate Generation Policy تعیین شوند و لزوماً از ابتدای window با فاصله ثابت شروع نشوند. 10. چرا این موضوع مهم است؟ فرض کنید: Window = 06:00–22:00 و: Headway = 30 min از نظر ساده: F≤1+ 30 16×60 ​ اما این هنوز ظرفیت واقعی نیست. چون ممکن است: Station conflict Junction conflict Opposite direction Crossing Dwell Train length Locomotive Wagon cycle Terminal Operational window ظرفیت را کاهش دهند. پس: Theoretical frequency ≠ Operational capacity 11. Capacity Search واقعی حالا الگوریتم: Python def solve_frequency( frequency, base_run, pattern, infrastructure, scheduler, ): candidate_runs = ( generate_candidate_runs( base_run, pattern, frequency, ) ) if len(candidate_runs) != frequency: return None, False paths = [ build_path( run, infrastructure, ) for run in candidate_runs ] schedule = scheduler.solve( candidate_runs, paths, infrastructure, ) validation = validate_schedule( schedule, candidate_runs, paths, ) feasible = ( schedule.status == "FEASIBLE" and validation.valid ) return ( schedule, feasible, ) و جستجوی ظرفیت: Python def search_real_capacity( base_run, pattern, infrastructure, scheduler, lower_bound=0, upper_bound=None, ): if upper_bound is None: upper_bound = ( pattern.max_daily_frequency or 100 ) low = lower_bound high = upper_bound best_frequency = 0 best_schedule = None while low <= high: mid = ( low + high ) // 2 schedule, feasible = ( solve_frequency( mid, base_run, pattern, infrastructure, scheduler, ) ) if feasible: best_frequency = mid best_schedule = schedule low = mid + 1 else: high = mid - 1 return ( best_frequency, best_schedule, ) 12. Capacity Proof اینجا باید یک تفاوت اساسی با نسخه قبلی ایجاد کنیم. فقط گفتن: Cr = 10 کافی نیست. باید: F = 10 ✓ feasible ✓ independently validated F = 11 ✕ infeasible ثبت شود. مدل: Python from dataclasses import dataclass @dataclass class CapacityProof: capacity: int feasible_frequency: int infeasible_frequency: int | None feasible_validated: bool infeasible_confirmed: bool proof_valid: bool و: Python def build_capacity_proof( capacity, base_run, pattern, infrastructure, scheduler, ): feasible_schedule, feasible = ( solve_frequency( capacity, base_run, pattern, infrastructure, scheduler, ) ) if not feasible: raise RuntimeError( "Capacity candidate itself " "is not feasible." ) next_frequency = ( capacity + 1 ) _, next_feasible = ( solve_frequency( next_frequency, base_run, pattern, infrastructure, scheduler, ) ) return CapacityProof( capacity=capacity, feasible_frequency=capacity, infeasible_frequency=( next_frequency if not next_feasible else None ), feasible_validated=True, infeasible_confirmed=( not next_feasible ), proof_valid=( not next_feasible ), ) این همان چیزی است که در P05 قبلاً تعریف کردیم: F=76 ✓ FEASIBLE F=77 ✕ INFEASIBLE Cr=76 13. اما یک مشکل مهم باقی می‌ماند اگر: F=10 feasible باشد و: F=11 infeasible، ممکن است: F=12 دوباره feasible شود؟ در یک مدل صحیح ظرفیت عملیاتی معمولاً مجموعه feasible frequencies باید monotonic باشد، اما این monotonicity باید به‌صورت property مدل و تست شود، نه اینکه بدون بررسی فرض شود. بنابراین در نسخه Production باید: Monotonicity Test داشته باشیم. برای مثال: Python def check_monotonicity( results ): previous = True for frequency in sorted( results ): feasible = results[ frequency ] if not feasible: previous = False elif not previous: raise RuntimeError( "Non-monotonic capacity " "feasibility detected." ) اگر این تست fail شود، باید علت مشخص شود؛ مثلاً: Candidate generation policy Calendar rule Alternative timetable Solver search limitation و نباید فوراً آن را bug در infrastructure فرض کنیم. 14. Capacity Profile ظرفیت فقط یک عدد نیست. خروجی بهتر: JSON { "route": "GAR-ANDIMESHK", "capacity": 6, "profile": [ { "frequency": 1, "feasible": true }, { "frequency": 2, "feasible": true }, { "frequency": 3, "feasible": true }, { "frequency": 4, "feasible": true }, { "frequency": 5, "feasible": true }, { "frequency": 6, "feasible": true }, { "frequency": 7, "feasible": false } ] } بعداً این تبدیل می‌شود به: Capacity Profile │ ├── By Hour ├── By Direction ├── By Train Type ├── By Load State ├── By Day └── By Scenario 15. Single Track در V1.2 برای Single Track، resource همچنان: BLOCK::A::B است. بنابراین: A → B و: B → A هر دو یک resource دارند. ولی در Double Track: BLOCK::A::B::FORWARD BLOCK::A::B::REVERSE داریم. این distinction باید حفظ شود: Directed Path ≠ Physical Resource 16. Headway برای دو train: Train i Train j در یک physical block: Same Direction Entry j ​ ≥Clear i ​ +H same ​ یا ترتیب معکوس. Opposite Direction Entry j ​ ≥Clear i ​ +H switch ​ یا: Entry i ​ ≥Clear j ​ +H switch ​ و: H opposite ​ =max(H same ​ ,T switch ​ ) این همان اصلاح مهم V1.0 است. 17. Station Capacity در V1.2 باید station را نیز resource واقعی بدانیم. مثلاً: Station Dorud │ ├── Track 1 ├── Track 2 ├── Track 3 │ ├── Arrival Route ├── Departure Route ├── Crossing ├── Overtaking └── Formation اگر دو train همزمان نیازمند یک station track باشند: NoOverlap اگر station چند track داشته باشد: OptionalInterval + ExactlyOne(track) مورد استفاده قرار می‌گیرد. 18. Train Length Constraint اگر: Train Length = 650m و: Station usable length = 600m باید: INFEASIBLE باشد. نه اینکه solver somehow آن را بپذیرد. Constraint: L train ​ ≤L usable ​ و در صورت track-specific بودن: L train ​ ≤L track ​ 19. Operational Availability Window در V1.2: Python @dataclass(frozen=True) class OperationalWindow: resource_id: str start_minute: int end_minute: int window_type: str برای یک window بسته: Resource unavailable: 10:00–11:00 حرکت باید: before 10:00 OR after 11:00 باشد. این موضوع برای: Brake Test Fueling Maintenance Prayer / operational availability Station possession Track maintenance قابل استفاده است. 20. Bottleneck در V1.2 بعد از محاسبه Cr، فقط utilization را نگاه نمی‌کنیم. برای هر resource: ΔC g ​ =C r base ​ −C r without constraint g ​ ​ اگر: Base Capacity = 8 Remove constraint X Capacity = 10 آن‌وقت: ΔC X ​ =2 این resource از نظر marginal capacity impact اهمیت دارد. خروجی: JSON { "resource": "BLOCK::DORUD::A", "base_capacity": 8, "relaxed_capacity": 10, "marginal_capacity": 2 } این بسیار بهتر از: Utilization = 93% → Bottleneck است. 21. Capacity Explanation در نهایت UI باید چیزی شبیه این نشان دهد: Route Capacity ──────────────────────────── Capacity 8 trains/day Proof ──────────────────────────── 8 trains/day ✓ FEASIBLE 9 trains/day ✕ INFEASIBLE Primary Binding Constraints ──────────────────────────── 1. Single-track block 2. Station crossing capacity 3. Opposite-direction switch time Unused / Hidden Capacity ──────────────────────────── Potential +1 with: - reduced switch time - additional crossing track Demand ──────────────────────────── Market Demand 11 Transportable Demand 10 Capacity Allocated 8 Unserved 2 22. نقطه اتصال به Marketplace حالا زنجیره کاملاً مشخص می‌شود: Marketplace ↓ Market Request ↓ Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Operating Pattern ↓ Candidate Train Runs ↓ Train Formation ↓ Capacity Engine ↓ Feasible Schedule ↓ Capacity ↓ Allocation و سه Demand را نگه می‌داریم: D market ​  =D transportable ​  =D allocated ​ مثلاً: Market Demand = 11 trains/day Transportable = 10 trains/day Operational Capacity = 8 trains/day Allocated = 8 trains/day Unserved = 2 trains/day 23. اما Wagon و Locomotive هنوز کجا هستند؟ در V1.2 هنوز آنها را hard constraint نکرده‌ایم. پس: V1.2 Capacity = Infrastructure + Operational Scheduling در V1.3: Infrastructure + Wagon Cycle + Locomotive Cycle + Formation خواهیم داشت: C r ​ =maxF subject to: F≤C infrastructure ​ F≤C wagon ​ F≤C locomotive ​ F≤C station ​ F≤C terminal ​ F≤C operational ​ ولی باز هم: C r ​  =min(C infrastructure ​ ,C wagon ​ ,C locomotive ​ ,...) چون interactions بین آنها وجود دارد. 24. معماری نهایی V1.2 ┌──────────────┐ │ Excel │ └──────┬───────┘ │ ▼ ┌───────────────┐ │ Train Service │ └───────┬───────┘ │ ▼ ┌────────────────────┐ │ Operating Pattern │ └─────────┬──────────┘ │ F Candidate │ ▼ ┌────────────────────┐ │ Candidate Generator│ └─────────┬──────────┘ │ ▼ ┌───────────────┐ │ TrainRun[] │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ Directed Path │ └───────┬───────┘ │ ▼ ┌──────────────────────────┐ │ Infrastructure Resources │ │ │ │ Blocks │ │ Stations │ │ Junctions │ │ Windows │ └────────────┬─────────────┘ │ ▼ ┌───────────────┐ │ CP-SAT │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ Schedule │ └───────┬───────┘ │ ▼ ┌─────────────────────┐ │ Independent │ │ Validation │ └──────────┬──────────┘ │ ┌───────▼───────┐ │ Capacity │ │ Search │ └───────┬───────┘ │ ┌───────▼───────┐ │ Capacity │ │ Proof │ └───────┬───────┘ │ ┌──────────▼──────────┐ │ Bottleneck & │ │ Explanation │ └─────────────────────┘ 25. Definition of Done برای V1.2 نسخه V1.2 زمانی کامل است که: ✓ Excel service patterns loaded ✓ OperatingPattern created ✓ Operating calendar handled ✓ Candidate TrainRuns generated ✓ Forward / Reverse supported ✓ Single Track supported ✓ Double Track supported ✓ Physical block identity preserved ✓ Station resource modeled ✓ Station length checked ✓ Junction conflicts modeled ✓ Operational windows modeled ✓ CP-SAT schedule generated ✓ Independent validation executed ✓ F tested ✓ F+1 tested ✓ Capacity proof generated ✓ Capacity profile generated ✓ Binding constraints identified ✓ Explanation generated ✓ Run/Data/Model/Scenario versioned و مهم‌تر از همه: No Feasible Schedule ↓ No Operational Capacity No Independent Validation ↓ No Capacity Proof No Verified Data Mapping ↓ No Production Result گام بعدی منطقی پس از این V1.2، V1.3 باید Wagon Cycle + Locomotive Cycle را مستقیماً داخل همین Capacity Search وارد کند. در آن مرحله دیگر سؤال فقط «چند قطار از نظر خط می‌توانیم عبور دهیم؟» نیست؛ بلکه موتور می‌تواند برای هر F بررسی کند که آیا واگن، چرخه واگن، لکوموتیو، Turnback، Maintenance/Fueling و تشکیل قطار همزمان برای آن فرکانس قابل تحقق هستند یا خیر. آنجا برای اولین بار Capacity = Infrastructure × Rolling Stock × Operations به یک مدل اجرایی واقعی تبدیل می‌شود. --- ## ادامه 33 — پرسش کاربر (`message-id:4270a4f0-65a0-4c41-81fe-842d5fe5a9b0`) بله --- ## ادامه 33 — پاسخ دستیار (`message-id:f8d1eb1c-a164-42b3-923f-39ce81ea8860`) عالی. پس وارد Version 1.3 — Integrated Rolling Stock Capacity Engine می‌شویم. این نسخه یک تغییر معماری مهم دارد: از اینجا به بعد، ظرفیت دیگر فقط ظرفیت خط و زمان‌بندی نیست؛ بلکه همزمانی زیرساخت، واگن، لکوموتیو، تشکیل قطار و چرخه عملیاتی در یک feasibility model واحد وارد می‌شود. V1.3 — Integrated Rolling Stock & Formation Capacity معماری هدف: Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Operating Pattern ↓ Candidate Train Runs ↓ ┌──────────────────────────────────────┐ │ Integrated Capacity Engine │ │ │ │ Infrastructure │ │ + Station / Block / Junction │ │ + Wagon Cycle │ │ + Locomotive Cycle │ │ + Train Formation │ │ + Empty Wagon Repositioning │ │ + Operational Windows │ │ + Schedule │ └──────────────────┬───────────────────┘ ↓ Feasible Schedule ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck / Explanation 1. تعریف جدید Capacity در V1.3 تعریف ظرفیت را به شکل زیر می‌گیریم: C r ​ =max{F:∃S F ​ ,W F ​ ,L F ​ ,T F ​ } به‌طوری که: S F ​ : Schedule W F ​ : Wagon Cycle L F ​ : Locomotive Cycle T F ​ : Train Formation و همه constraints همزمان برقرار باشند. بنابراین: F≤C infrastructure ​ اما همچنین: F≤C wagon ​ و: F≤C locomotive ​ و: F≤C terminal ​ ولی همان‌طور که قبلاً تثبیت کردیم، ظرفیت نهایی را نباید صرفاً minimum این اعداد گرفت. 2. Domain Model جدید ساختار: app/ ├── domain/ │ ├── demand.py │ ├── wagon.py │ ├── locomotive.py │ ├── formation.py │ ├── train_run.py │ ├── route.py │ ├── schedule.py │ ├── resource.py │ └── result.py │ ├── formation/ │ ├── requirement.py │ ├── builder.py │ └── validator.py │ ├── rolling_stock/ │ ├── wagon_cycle.py │ ├── wagon_inventory.py │ ├── locomotive_cycle.py │ ├── locomotive_assignment.py │ └── validator.py │ ├── scheduling/ │ ├── candidate.py │ ├── problem.py │ ├── resources.py │ └── solver.py │ ├── capacity/ │ ├── search.py │ ├── proof.py │ ├── profile.py │ └── bottleneck.py │ └── pipeline/ └── integrated_capacity.py 3. Wagon Requirement برای هر Freight Flow: W req ​ =⌈ Q wagon ​ Q flow ​ ​ ⌉ اما تعداد واگن تنها quantity نیست. مدل: Python from dataclasses import dataclass @dataclass(frozen=True) class WagonRequirement: requirement_id: str freight_flow_id: str wagon_type_id: str quantity: int payload_tons: float origin_station_id: str destination_station_id: str مثلاً: Freight Flow: Tehran → Khowaf Demand: 8,000 ton/day Wagon: 50 ton Requirement: 160 wagon/day اما اگر هر train شامل 40 واگن باشد: F trains ​ =⌈ 40 160 ​ ⌉=4 4. Train Formation Formation دیگر فقط یک لیست واگن نیست. Python @dataclass(frozen=True) class TrainFormationItem: wagon_type_id: str quantity: int loaded: bool gross_weight_tons: float length_m: float @dataclass(frozen=True) class TrainFormation: formation_id: str train_run_id: str items: tuple[TrainFormationItem, ...] total_wagons: int total_weight_tons: float total_length_m: float locomotive_count: int و constraints: N wagon ​ ≤N max ​ L train ​ ≤L usable ​ W train ​ ≤W route ​ و: Traction(train)≥RequiredTraction 5. Wagon Pool حالا inventory واقعی وارد مدل می‌شود: Python @dataclass class WagonPool: wagon_type_id: str total_available: int loaded_available: int empty_available: int reserved: int = 0 maintenance: int = 0 unavailable: int = 0 Available: W available ​ =W total ​ −W reserved ​ −W maintenance ​ −W unavailable ​ 6. Wagon Cycle این بخش بسیار مهم است. فرض کنیم: Origin ↓ Loaded Movement ↓ Destination ↓ Unload ↓ Empty Return ↓ Origin چرخه: T wagon ​ =T load ​ +T loaded ​ +T unload ​ +T empty ​ +T reposition ​ +T waiting ​ و تعداد واگن موردنیاز برای فرکانس F: W required ​ =F×N wagon/train ​ × T planning ​ T wagon ​ ​ تقریب ساده فقط زمانی مجاز است که چرخه واقعاً پایدار و تکرارشونده باشد. 7. مدل Wagon Cycle Python @dataclass(frozen=True) class WagonCycle: cycle_id: str wagon_type_id: str origin_station_id: str destination_station_id: str load_duration_min: int loaded_running_min: int unload_duration_min: int empty_running_min: int reposition_duration_min: int waiting_duration_min: int total_cycle_min: int و: Python def calculate_cycle_time(cycle: WagonCycle) -> int: return ( cycle.load_duration_min + cycle.loaded_running_min + cycle.unload_duration_min + cycle.empty_running_min + cycle.reposition_duration_min + cycle.waiting_duration_min ) 8. Wagon Feasibility برای هر candidate frequency: F = 8 اگر: 40 wagon/train پس: 320 wagon departures/day اگر cycle time: 2.5 days آنگاه: 320×2.5=800 بنابراین حداقل حدود: 800 wagons لازم است. اگر: Available = 700 آنگاه: F=8 → INFEASIBLE حتی اگر infrastructure کاملاً قادر به عبور 8 قطار باشد. 9. Locomotive Cycle برای لکوموتیو نیز همین منطق برقرار است. چرخه: Depot ↓ Train Formation ↓ Loaded Trip ↓ Destination ↓ Turnback ↓ Return ↓ Maintenance/Fueling ↓ Available مدل: Python @dataclass(frozen=True) class LocomotiveCycle: cycle_id: str locomotive_type_id: str start_location_id: str end_location_id: str outbound_duration_min: int turnback_duration_min: int return_duration_min: int fueling_duration_min: int maintenance_duration_min: int operational_buffer_min: int total_cycle_min: int 10. Locomotive Availability برای frequency: L required ​ =F×N loco/train ​ × T planning ​ T loco−cycle ​ ​ مثلاً: 8 trains/day 1 locomotive/train cycle = 1.5 day پس: 8×1.5=12 یعنی تقریباً: 12 locomotives مورد نیاز است. اگر فقط: 10 locomotives داریم: F=8 → INFEASIBLE 11. Turnback یک نکته مهم: TrainRun و: LocomotiveCycle یکی نیستند. مثلاً: TrainRun 100: Gar → Andimeshk TrainRun 101: Andimeshk → Gar ممکن است یک locomotive بعد از رسیدن TrainRun 100، برای TrainRun 101 استفاده شود. بنابراین باید assignment داشته باشیم: Python @dataclass(frozen=True) class LocomotiveAssignment: assignment_id: str locomotive_id: str train_run_id: str start_minute: int end_minute: int و constraint: Start next ​ ≥End current ​ +T turnback ​ 12. Maintenance و Fueling اگر لکوموتیو در window زیر unavailable باشد: 02:00–05:00 scheduler باید این را بفهمد. یعنی: Locomotive │ ├── Train Assignment ├── Turnback ├── Fueling ├── Maintenance └── Operational Buffer همگی resource occupancy ایجاد می‌کنند. 13. Empty Wagon Flow این قسمت برای مدل ایرانی حیاتی است. پس از: Loaded: O → D واگن نمی‌تواند ناپدید شود. باید: Empty: D → O را مدل کنیم. یعنی: LoadedFlow O,D ​ →EmptyFlow D,O ​ و balance: E D ​ (t+1)=E D ​ (t)+Arrivals empty ​ −Departures loaded ​ به زبان ساده: هر واگن باری که در مقصد تخلیه شد، باید در مدل وضعیت بعدی داشته باشد. 14. Wagon State Machine مدل state: EMPTY_AT_ORIGIN ↓ LOADING ↓ LOADED_READY ↓ LOADED_IN_TRANSIT ↓ AT_DESTINATION ↓ UNLOADING ↓ EMPTY_AT_DESTINATION ↓ EMPTY_IN_TRANSIT ↓ EMPTY_AT_ORIGIN این state machine بعداً برای simulation و digital twin نیز قابل استفاده است. 15. Integrated Feasibility حالا برای هر F: Python @dataclass class IntegratedCapacityResult: frequency: int infrastructure_feasible: bool wagon_feasible: bool locomotive_feasible: bool formation_feasible: bool schedule_feasible: bool validation_passed: bool feasible: bool محاسبه: Python def integrated_feasibility( frequency, infrastructure_result, wagon_result, locomotive_result, formation_result, schedule_result, validation_result, ): feasible = all([ infrastructure_result.feasible, wagon_result.feasible, locomotive_result.feasible, formation_result.feasible, schedule_result.feasible, validation_result.valid, ]) return IntegratedCapacityResult( frequency=frequency, infrastructure_feasible=( infrastructure_result.feasible ), wagon_feasible=( wagon_result.feasible ), locomotive_feasible=( locomotive_result.feasible ), formation_feasible=( formation_result.feasible ), schedule_feasible=( schedule_result.feasible ), validation_passed=( validation_result.valid ), feasible=feasible, ) 16. Capacity Search جدید از اینجا: F ↓ Formation ↓ Wagon Cycle ↓ Locomotive Cycle ↓ Candidate Runs ↓ Infrastructure Schedule ↓ Validation بنابراین: Python def evaluate_frequency( frequency, context, ): formation = ( context.formation_engine .build(frequency) ) if not formation.feasible: return False wagon = ( context.wagon_engine .evaluate( frequency, formation, ) ) if not wagon.feasible: return False locomotive = ( context.locomotive_engine .evaluate( frequency, formation, ) ) if not locomotive.feasible: return False schedule = ( context.scheduler .solve( frequency, formation, wagon, locomotive, ) ) if not schedule.feasible: return False validation = ( context.validator .validate( schedule, formation, wagon, locomotive, ) ) return validation.valid 17. اما یک اصلاح معماری بسیار مهم نباید این‌طور عمل کنیم: Infrastructure Capacity = 8 Wagon Capacity = 10 Locomotive Capacity = 12 min = 8 بلکه: F=1 → solve integrated model F=2 → solve integrated model ... F=8 → solve integrated model F=9 → solve integrated model چون ممکن است interactions داشته باشیم. مثلاً: F=8 با یک formation خاص feasible باشد. ولی: F=9 نیازمند formation دیگری باشد که station length را نقض کند. پس capacity نتیجه‌ی کل مدل است. 18. Bottleneck Classification V1.3 باید bottleneck را طبقه‌بندی کند: Infrastructure Rolling Stock Station Locomotive Wagon Formation Terminal Operational Window Schedule Conflict Demand مثلاً: JSON { "frequency": 8, "feasible": true } ولی: JSON { "frequency": 9, "feasible": false, "binding_constraints": [ { "type": "WAGON", "resource": "WAGON_POOL_W1", "impact": 1 }, { "type": "STATION", "resource": "STATION_X_TRACK_2", "impact": 1 } ] } 19. Marginal Bottleneck Analysis برای هر bottleneck: Base: Cr = 8 بعد: Relax Wagon +100 → Cr = 9 Add locomotive +2 → Cr = 8 Add station track → Cr = 10 پس: Wagon: ΔC = +1 Locomotive: ΔC = 0 Station Track: ΔC = +2 این اطلاعات بعداً وارد Scenario Lab و Investment Analysis می‌شود. 20. Investment Scenario مثلاً سناریو: Scenario S-014 Investment: Add Station Track at X Cost: ... Expected: Station capacity +1 ولی engine نباید صرفاً بگوید: +1 station track = +1 train بلکه باید: Modified Infrastructure ↓ Full Re-Solve ↓ New Schedule ↓ New Wagon Cycle ↓ New Locomotive Cycle ↓ New Capacity را اجرا کند. 21. خروجی نهایی V1.3 هر Run: JSON { "run_id": "RUN-2026-00124", "model_version": "1.3.0", "scenario_id": "BASE", "route": "GAR-ANDIMESHK", "capacity": { "value": 8, "unit": "train_per_day" }, "proof": { "f": 8, "f_feasible": true, "f_plus_1": 9, "f_plus_1_feasible": false, "validated": true, "proof_valid": true }, "rolling_stock": { "wagon_required": 720, "wagon_available": 800, "locomotive_required": 10, "locomotive_available": 12 }, "formation": { "wagons_per_train": 40, "length_m": 620, "gross_weight_tons": 3200 }, "bottlenecks": [ { "type": "INFRASTRUCTURE", "resource": "BLOCK-X", "marginal_capacity": 1 } ] } اعداد بالا صرفاً نمونه ساختار خروجی هستند و به‌عنوان داده واقعی پروژه تلقی نمی‌شوند. 22. Data Lineage در V1.3 باید بتوانیم برای هر عدد بگوییم از کجا آمده: Capacity = 8 │ ├── Schedule │ ├── TrainRun 1 │ ├── TrainRun 2 │ └── ... │ ├── Formation │ └── 40 wagons/train │ ├── Wagon Cycle │ └── 2.1 days │ ├── Locomotive Cycle │ └── 1.4 days │ └── Infrastructure ├── Block ├── Station └── Junction بنابراین اصل: Every Capacity Number → Must Have Evidence حفظ می‌شود. 23. V1.3 Definition of Done ✓ Wagon Requirement ✓ Wagon Pool ✓ Wagon Inventory ✓ Wagon Cycle ✓ Empty Wagon Flow ✓ Wagon State ✓ Locomotive Pool ✓ Locomotive Assignment ✓ Locomotive Cycle ✓ Turnback ✓ Maintenance ✓ Fueling ✓ Train Formation ✓ Formation Validation ✓ Train Length ✓ Train Weight ✓ Traction ✓ Infrastructure Scheduling ✓ Station Resources ✓ Single Track ✓ Double Track ✓ Junction Conflict ✓ Operational Window ✓ Integrated Feasibility ✓ F Search ✓ F+1 Proof ✓ Independent Validation ✓ Bottleneck Classification ✓ Marginal Capacity Impact ✓ Scenario Re-Solve ✓ Full Result Package معماری‌ای که از اینجا به بعد تثبیت می‌کنیم MARKETPLACE │ ▼ MARKET DEMAND │ ▼ FREIGHT FLOW │ ▼ WAGON REQUIREMENT │ ┌───────────┴───────────┐ ▼ ▼ WAGON CYCLE TRAIN FORMATION │ │ │ ▼ │ LOCOMOTIVE CYCLE │ │ └───────────┬───────────┘ ▼ CANDIDATE TRAINS │ ▼ DIRECTED TRAIN PATH │ ▼ INFRASTRUCTURE RESOURCES │ ▼ SCHEDULER │ ▼ FEASIBLE SCHEDULE │ ▼ INDEPENDENT VALIDATOR │ ▼ CAPACITY SEARCH │ ┌────┴────┐ ▼ ▼ F F + 1 ✓ ✕ └────┬────┘ ▼ CAPACITY PROOF │ ┌──────────┼──────────┐ ▼ ▼ ▼ Bottleneck Scenario Explanation │ │ │ └──────────┴──────────┘ ▼ MARKETPLACE ALLOCATION گام بعدی باید V1.4 باشد: Network Capacity + Shared Resources + Multi-OD Optimization. در آن نسخه، دیگر یک Route را جداگانه حل نمی‌کنیم؛ چند OD و چند مسیر را همزمان وارد مدل می‌کنیم تا مثلاً یک واگن، لکوموتیو، ایستگاه، گلوگاه یا بخش مشترک خط همزمان بین چند سرویس رقابت کند. این همان جایی است که موتور از Route Capacity Engine به یک Railway Network Capacity Optimization Engine تبدیل می‌شود. --- ## ادامه 34 — پرسش کاربر (`message-id:7c654892-3b5e-4c52-9938-b1d84282b0e5`) بله لطفا --- ## ادامه 34 — پاسخ دستیار (`message-id:20f52cec-5d42-4635-8876-b7acbdf2d5a0`) حتماً. در V1.4 موتور را از «ظرفیت یک Route» به ظرفیت شبکه با چند OD، چند سرویس، منابع مشترک و تخصیص همزمان ارتقا می‌دهیم. این گام از نظر معماری بسیار مهم است، چون از اینجا به بعد دیگر نمی‌توانیم هر Route را مستقل حل کنیم. مدل باید بداند که چند جریان مختلف ممکن است از یک Block، Station، Junction، Wagon Pool یا Locomotive Pool مشترک استفاده کنند. در مدل‌های تبادل داده ریلی نیز تفکیک زیرساخت، timetable و rolling stock یک اصل شناخته‌شده است؛ مثلاً railML این حوزه‌ها را به زیرمدل‌های جدا ولی مرتبط تقسیم می‌کند و RailTopoModel نیز برای مدل‌سازی توپولوژی شبکه استفاده می‌شود. GitLab +1 V1.4 — Network Capacity & Multi-OD Optimization 1. تغییر اصلی تا V1.3 داشتیم: One Route ↓ F Candidate ↓ Schedule ↓ Capacity در V1.4 داریم: NETWORK │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ OD-1 OD-2 OD-3 │ │ │ Service Service Service │ │ │ └──────────────┼──────────────┘ ↓ Shared Resources │ ┌────────────┼────────────┐ ↓ ↓ ↓ Blocks Stations Junctions ↓ ↓ ↓ Wagons Locos Terminals │ ↓ Network Scheduler │ ↓ Feasible Network │ ↓ Capacity Allocation یعنی: C n ​ =max r ∑ ​ Q r ​ F r ​ subject to: Network Schedule Feasible 2. Multi-OD مدل پایه: Python from dataclasses import dataclass from typing import Optional @dataclass(frozen=True) class ODPair: od_id: str origin_station_id: str destination_station_id: str commodity_id: Optional[str] = None مثلاً: OD-01: Tehran → Khowaf OD-02: Tehran → Rasht OD-03: Gar → Andimeshk OD-04: Khowaf → Zarand این ODها می‌توانند بخش‌هایی از network را با یکدیگر share کنند. 3. Network Route Route دیگر فقط یک مسیر مستقل نیست. Python @dataclass(frozen=True) class NetworkRoute: route_id: str od_id: str segment_ids: tuple[str, ...] station_ids: tuple[str, ...] direction: str train_type_id: str load_state: str مثلاً: OD-A │ ├── Block 1 ├── Block 2 ├── Station X ├── Block 3 └── Station Y و: OD-B │ ├── Block 1 ├── Block 2 ├── Junction J ├── Block 8 └── Station Z در نتیجه Block 1 و Block 2 منابع مشترک هستند. 4. Shared Resource این مفهوم در V1.4 باید First-Class شود. Python @dataclass(frozen=True) class SharedResource: resource_id: str resource_type: str capacity: int unit: str description: str = "" انواع: BLOCK STATION_TRACK STATION_THROUGHPUT JUNCTION TERMINAL WAGON_POOL LOCOMOTIVE_POOL LOADING_FACILITY UNLOADING_FACILITY EMPTY_WAGON_BUFFER MAINTENANCE_FACILITY 5. Resource Usage هر TrainRun باید مشخص کند چه منابعی را در چه زمانی مصرف می‌کند. Python @dataclass(frozen=True) class ResourceUsage: resource_id: str train_run_id: str start_minute: int end_minute: int quantity: int = 1 usage_type: str = "OCCUPANCY" مثلاً: Train 101 │ ├── BLOCK-A-B 10:20–10:42 ├── STATION-X 10:42–11:10 ├── JUNCTION-J 11:12–11:15 └── BLOCK-B-C 11:15–11:37 این representation بعداً برای solver، validator، visualization و explanation مشترک استفاده می‌شود. 6. Resource Graph شبکه را بهتر است علاوه بر Railway Graph، به صورت Resource Graph نیز ببینیم: ┌──────────────┐ │ Station A │ └──────┬───────┘ │ BLOCK A-B │ ┌──────▼───────┐ │ Station B │ └───┬─────┬────┘ │ │ BLOCK B-C │ │ │ ┌─────▼─┐ JUNCTION │ C │ │ └───────┘ │ │ BLOCK │ ▼ D ولی Resource Graph یک abstraction بالاتر است: Train ↓ uses ↓ Resource ↓ shared by ↓ other trains / routes / services 7. Candidate Frequency برای هر OD برای هر OD: F r ​ ∈Z ≥0 ​ مثلاً: OD-1: F1 = 4 OD-2: F2 = 3 OD-3: F3 = 5 ولی دیگر نمی‌توانیم بگوییم هر کدام مستقل feasible هستند. باید: (F 1 ​ ,F 2 ​ ,F 3 ​ ) را همزمان evaluate کنیم. 8. Network Decision Variables متغیر اصلی: F r ​ برای هر Route. اما برای timetable: a i,r ​ و: d i,r ​ برای arrival/departure. برای assignment: y i,g ​ ={ 1 0 ​ train i uses resource g otherwise ​ و برای route selection: x od,r ​ ={ 1 0 ​ OD uses route r otherwise ​ 9. Demand Constraint اگر Market Demand باشد: Tehran → Khowaf Demand = 10 trains/day نباید بیشتر از demand برای آن OD تولید کنیم: F r ​ ≤D r ​ اگر چند Route برای یک OD وجود دارد: r∈R od ​ ∑ ​ F r ​ ≤D od ​ 10. Transportable Demand همان تفکیک قبلی: D market ​  =D transportable ​  =D allocated ​ مثلاً: Market Demand = 15 Transportable = 13 Network Capacity = 10 Allocated = 10 Unserved = 3 این تفکیک در Network Optimization بسیار مهم می‌شود. 11. Shared Block Constraint اگر چند Route از یک Block استفاده کنند: r ∑ ​ Usage r,g ​ ≤Capacity g ​ اما برای Railway Block معمولاً فقط شمارش ساده کافی نیست؛ زمان نیز مهم است. پس در Scheduling: Interval i,g ​ باید با سایر intervalهای resource conflict نداشته باشد. در Single Track: Route A → B Route B → A هر دو: PHYSICAL_BLOCK(A,B) را مصرف می‌کنند. در Double Track: PHYSICAL_BLOCK(A,B,FORWARD) PHYSICAL_BLOCK(A,B,REVERSE) می‌توانند مستقل باشند، مگر اینکه resource مشترک دیگری وجود داشته باشد. 12. Shared Station فرض کنیم: Station X Track 1 Track 2 سه Route: R1 → X R2 → X R3 → X ممکن است infrastructure از نظر track capacity اجازه سه train را بدهد، اما: Station throat یا: Arrival route ظرفیت پایین‌تری داشته باشد. بنابراین: Station ├── Tracks ├── Arrival routes ├── Departure routes ├── Throat ├── Formation └── Crossing باید resourceهای جداگانه داشته باشد. 13. Junction Junction را نباید به شکل: NoOverlap(all trains) مدل کنیم. چون ممکن است: Movement A → B و: Movement C → D همزمان feasible باشند، ولی: A → C با: B → D conflict داشته باشد. بنابراین: Python @dataclass(frozen=True) class JunctionMovement: movement_id: str junction_id: str from_track: str to_track: str @dataclass(frozen=True) class JunctionConflict: movement_a: str movement_b: str minimum_separation_min: int و conflict matrix: M1 M2 M3 M4 M1 - X - X M2 X - X - M3 - X - X M4 X - X - 14. Network CP-SAT V1.4 scheduler باید از یک مدل per-route جداگانه به یک global scheduling problem تبدیل شود. ساختار: Python class NetworkSchedulingProblem: trains: list paths: list resources: list station_tracks: list junctions: list operational_windows: list demands: list wagon_constraints: list locomotive_constraints: list Solver: Python class NetworkScheduler: def solve( self, problem: NetworkSchedulingProblem, ): ... 15. Objective Function در Network دیگر فقط feasibility نداریم. چند objective ممکن است وجود داشته باشد. Objective 1 — Maximize Freight max r ∑ ​ Q r ​ F r ​ Objective 2 — Maximize Revenue max r ∑ ​ Revenue r ​ F r ​ Objective 3 — Minimize Unserved Demand min od ∑ ​ (D od ​ −Allocated od ​ ) Objective 4 — Minimize Schedule Deviation min i ∑ ​ ∣d i ​ −d i baseline ​ ∣ اما باید Objectiveها قابل پیکربندی باشند. 16. Weighted Objective برای نسخه اولیه: max[α r ∑ ​ Q r ​ F r ​ −βUnserved−γDeviation] ولی در Production بهتر است objective hierarchy داشته باشیم: Priority 1: Hard feasibility Priority 2: Demand satisfaction Priority 3: Freight / revenue Priority 4: Baseline deviation Priority 5: Operational preference یعنی یک preference نباید feasibility را خراب کند. 17. Policy Constraints مثلاً Marketplace یا Railway Policy بگوید: Route B: minimum 4 trains/day پس: F B ​ ≥4 یا: OD A: minimum allocated tonnage = 5000 یا: Empty wagon flow: minimum 2 return movements/day این‌ها باید به صورت generic policy constraint مدل شوند: Python @dataclass(frozen=True) class PolicyConstraint: constraint_id: str constraint_type: str target_id: str operator: str value: float priority: str = "HARD" 18. Route Choice اگر یک OD چند مسیر داشته باشد: Tehran → Khowaf Route A: Tehran → Semnan → ... Route B: Tehran → ... Route C: Alternative Corridor باید route selection وارد optimization شود. x od,r ​ ∈{0,1} و: r ∑ ​ x od,r ​ =1 در صورت انتخاب یک route. یا اگر splitting مجاز باشد: r ∑ ​ F od,r ​ =F od ​ 19. Multi-Commodity Flow در سطح Network: Commodity OD Route Train Wagon می‌توانیم داشته باشیم: f od,r,t ​ یعنی freight flow مربوط به OD، روی route و time. Constraint شبکه: od,r ∑ ​ a od,r,g ​ f od,r,t ​ ≤C g,t ​ این پایه مدل Network Capacity Allocation خواهد بود. 20. Wagon Pool Shared Across OD این قسمت بسیار مهم است. فرض: OD-1 → نیاز 300 wagon OD-2 → نیاز 250 wagon OD-3 → نیاز 200 wagon و pool: Available = 600 نمی‌توانیم هر Route را مستقل حل کنیم. باید: W 1 ​ +W 2 ​ +W 3 ​ ≤600 ولی چون Wagon Cycle زمان‌مند است، در حالت دقیق‌تر: W i,t ​ نیز وارد مدل می‌شود. 21. Locomotive Pool Shared Across Routes همین موضوع برای locomotive: r ∑ ​ L r,t ​ ≤L available,t ​ ولی با Turnback: Train A ↓ Locomotive ↓ Destination ↓ Turnback ↓ Train B بنابراین availability تابع زمان است. 22. Empty Wagon Network در V1.4 empty wagon دیگر فقط یک return flow ساده نیست. شبکه: Loaded A ───────────→ B ↑ │ │ │ │ ↓ └──────────── Empty ولی اگر: B → C نیز demand داشته باشد، ممکن است empty wagon به جای A به C تخصیص یابد. پس مسئله تبدیل می‌شود به: Empty Wagon Repositioning که خودش یک optimization problem است. 23. Wagon Flow Network برای هر time interval: E j,t+1 ​ =E j,t ​ +Inflow j,t ​ −Outflow j,t ​ و: 0≤E j,t ​ ≤Buffer j ​ این constraint مستقیماً ظرفیت network را تحت تأثیر قرار می‌دهد. 24. Terminal Capacity Terminal هم resource مشترک است: Terminal ├── Loading Tracks ├── Unloading Tracks ├── Cranes ├── Loading Capacity ├── Unloading Capacity ├── Yard └── Wagon Buffer مثلاً: trains ∑ ​ UnloadTime train ​ ≤AvailableTerminalTime یا در مدل زمان‌مند: TerminalResource → Interval → NoOverlap / Cumulative 25. Network Capacity تعریف نهایی اکنون می‌توانیم تعریف قبلی را توسعه دهیم: C n ​ =max{ r ∑ ​ Q r ​ F r ​ } subject to: Schedule(F) Infrastructure(F) WagonCycle(F) LocomotiveCycle(F) Formation(F) Station(F) Junction(F) Terminal(F) Buffer(F) Demand(F) Policy(F) همگی feasible باشند. 26. تفاوت Cr و Cn این تفاوت را در سیستم به صورت رسمی نگه می‌داریم. Route Capacity C r ​ =maxF r ​ برای یک Route یا corridor تحت سناریوی مشخص. Network Capacity C n ​ =max r ∑ ​ Q r ​ F r ​ با درنظر گرفتن interaction بین Routeها. بنابراین ممکن است: Route A: Cr = 10 Route B: Cr = 8 اما: Network: Ca + Cb ≠ 18 مثلاً به علت shared junction: Network Capacity = 14 عدد 14 فقط مثال است و نشان‌دهنده نتیجه واقعی پروژه نیست. 27. Network Capacity Search در V1.4 دیگر binary search یک متغیر ساده نیست. به جای: F = 1,2,3,... با vector مواجهیم: F=(F 1 ​ ,F 2 ​ ,…,F R ​ ) مثلاً: F = (4,3,2) یعنی: Route A = 4 Route B = 3 Route C = 2 و solver باید این vector را بهینه کند. 28. Network Optimization Algorithm Load Data ↓ Build Network ↓ Load Demand ↓ Generate Routes ↓ Generate Candidate Train Runs ↓ Build Formations ↓ Build Wagon Cycles ↓ Build Locomotive Cycles ↓ Build Shared Resources ↓ Build Conflicts ↓ Build CP-SAT Model ↓ Solve ↓ Independent Validation ↓ Demand Allocation ↓ Capacity Result ↓ Bottleneck Analysis 29. Network Result مدل نتیجه: Python @dataclass class NetworkCapacityResult: run_id: str scenario_id: str objective_value: float total_freight_tons: float total_trains: int route_frequencies: dict[str, int] od_allocations: dict[str, float] wagon_usage: dict[str, int] locomotive_usage: dict[str, int] resource_utilization: dict[str, float] binding_constraints: list bottlenecks: list validated: bool 30. Capacity Waterfall از اینجا UI بسیار قوی‌تر می‌شود: Network Theoretical Capacity 42 trains │ ▼ Infrastructure Capacity 35 trains │ ▼ Station / Junction 31 trains │ ▼ Rolling Stock 27 trains │ ▼ Terminal 24 trains │ ▼ Demand 21 trains │ ▼ Allocated 21 trains ولی این نمودار نباید به‌صورت ساده min-chain تفسیر شود. باید از actual re-solve results ساخته شود. 31. Bottleneck Analysis در Network برای هر shared resource: Base Network: C = 21 Relax Block X: C = 22 Relax Station Y: C = 21 Add Wagon Pool: C = 23 Add Locomotive: C = 21 بنابراین: Block X: ΔC = +1 Station Y: ΔC = 0 Wagon Pool: ΔC = +2 Locomotive: ΔC = 0 این نتیجه بسیار ارزشمند است، چون می‌گوید کدام intervention واقعاً capacity شبکه را افزایش می‌دهد. 32. Scenario Engine V1.4 باید Scenario را first-class کند. Python @dataclass(frozen=True) class NetworkScenario: scenario_id: str base_data_version: str infrastructure_changes: tuple rolling_stock_changes: tuple demand_changes: tuple policy_changes: tuple operational_changes: tuple مثلاً: Scenario S-001 +1 Station Track +100 Wagons Demand +20% سپس: Base ↓ Modify Data ↓ Full Network Rebuild ↓ Full Solve ↓ Compare 33. Scenario Delta خروجی: JSON { "base_capacity": 21, "scenario_capacity": 23, "delta": 2, "changes": { "station_track": 1, "wagon_pool": 100 }, "binding_constraints_before": [ "STATION_X", "WAGON_POOL_A" ], "binding_constraints_after": [ "BLOCK_Y" ] } این دقیقاً همان چیزی است که بعداً برای Investment Planning لازم داریم. 34. Explanation Engine هر نتیجه باید بتواند پاسخ دهد: چرا ظرفیت 21 شد؟ Because: 1. Demand allows 25 2. Infrastructure schedule allows 24 3. Shared junction reduces feasible combination to 22 4. Wagon cycle limits one OD 5. Final integrated allocation = 21 چرا Route A فقط 4 قطار گرفت؟ Not because Route A alone is limited to 4. The network optimization allocated the remaining shared capacity to other OD flows because of shared resources / demand / policy constraints. این تفاوت بسیار مهم است. 35. Explainability Chain Capacity = 21 ↓ Allocation = 21 ↓ Train Runs ↓ Resource Usage ↓ Conflicts ↓ Binding Constraints ↓ Scenario Relaxation ↓ Marginal Impact 36. API Architecture از اینجا APIها: POST /network/capacity/run POST /network/capacity/scenario GET /network/capacity/{run_id} GET /network/capacity/{run_id}/routes GET /network/capacity/{run_id}/resources GET /network/capacity/{run_id}/bottlenecks GET /network/capacity/{run_id}/proof GET /network/capacity/{run_id}/explanation و Marketplace فقط از API canonical استفاده می‌کند: Marketplace ↓ Market API ↓ Canonical Demand ↓ Network Capacity Engine ↓ Capacity / Allocation ↓ Market API Marketplace نباید مستقیماً با CP-SAT صحبت کند. 37. Database Model از V1.4 به بعد schema دیتابیس باید حداقل این entityها را داشته باشد: od_pair train_service operating_pattern train_run route route_segment resource resource_usage station station_track junction junction_movement junction_conflict wagon_type wagon_pool wagon_inventory wagon_cycle locomotive_type locomotive_pool locomotive_assignment locomotive_cycle freight_flow wagon_requirement demand allocation scenario capacity_run capacity_result capacity_proof binding_constraint resource_utilization explanation 38. Versioning هر Network Run: RUN-2026-000145 باید دقیقاً بداند: Data Version Infrastructure Version Demand Version Rolling Stock Version Operating Pattern Version Model Version Solver Version Scenario Version مثلاً: JSON { "run_id": "RUN-2026-000145", "model_version": "1.4.0", "data_version": "DATA-2026-09-28-01", "infrastructure_version": "INFRA-07", "demand_version": "DEMAND-12", "rolling_stock_version": "RS-04", "scenario_id": "BASE" } این برای auditability ضروری است. 39. ارتباط با داده واقعی شما در مورد Access شما، این بخش به شکل زیر وارد Network Model می‌شود: TrainNo ↓ TrainRun ↓ TrainStationCall[] ↓ Directed Path ↓ Physical Block ↓ Shared Network Resource و: seir همچنان: baseline_running_time_to_next است، نه ویژگی Station. همچنین: Kilometerage برای استنتاج فاصله segment می‌تواند استفاده شود، اما باید به عنوان: Derived Distance نگهداری شود، نه اینکه مقدار source Distance را overwrite کنیم. این تفکیک lineage را حفظ می‌کنیم. 40. یک اصل جدید برای V1.4 از این نسخه به بعد: Capacity is a property of a feasible network solution, not a property of an isolated infrastructure element. یعنی: Block Capacity Station Capacity Route Capacity Network Capacity چهار مفهوم متفاوت‌اند. و: C n ​  = r ∑ ​ C r ​ مگر اینکه هیچ shared resource یا interaction مؤثری بین Routeها وجود نداشته باشد. 41. Definition of Done — V1.4 نسخه V1.4 زمانی کامل است که: ✓ Multi-OD ✓ Multi-Route ✓ Shared Physical Blocks ✓ Shared Stations ✓ Station Tracks ✓ Junction Conflict Matrix ✓ Shared Terminals ✓ Shared Wagon Pools ✓ Shared Locomotive Pools ✓ Empty Wagon Network ✓ Time-dependent Resource Availability ✓ Demand Constraints ✓ Policy Constraints ✓ Route Choice ✓ Freight Allocation ✓ Network CP-SAT ✓ Integrated Schedule ✓ Independent Validation ✓ Network Capacity ✓ Capacity Allocation ✓ Bottleneck Analysis ✓ Marginal Capacity Impact ✓ Scenario Re-Solve ✓ Capacity Proof ✓ Explanation ✓ Full Data Lineage معماری تثبیت‌شده تا این مرحله MARKETPLACE │ ▼ MARKET DEMAND │ ▼ FREIGHT FLOW │ ┌─────────────┴─────────────┐ ▼ ▼ WAGON REQUIREMENT OD PAIRS │ │ └─────────────┬─────────────┘ ▼ TRAIN FORMATION │ ┌─────────┴─────────┐ ▼ ▼ WAGON CYCLE LOCOMOTIVE CYCLE │ │ └─────────┬─────────┘ ▼ CANDIDATE TRAINS │ ▼ NETWORK ROUTES │ ▼ ┌──────────────────────┐ │ SHARED RESOURCES │ │ │ │ Blocks │ │ Stations │ │ Junctions │ │ Terminals │ │ Wagons │ │ Locomotives │ │ Buffers │ └──────────┬───────────┘ ▼ NETWORK SCHEDULER │ ▼ FEASIBLE NETWORK │ ▼ INDEPENDENT VALIDATOR │ ▼ CAPACITY OPTIMIZATION │ ┌────────────┼────────────┐ ▼ ▼ ▼ Allocation Bottleneck Scenario │ │ │ └────────────┼────────────┘ ▼ CAPACITY PROOF │ ▼ EXPLANATION │ ▼ MARKETPLACE گام بعدی: V1.5 بعد از تثبیت V1.4، مرحله منطقی بعدی Production Data Platform + Real Access/Excel Reconciliation + Database + API + Run Management است؛ یعنی دیگر فقط الگوریتم را توسعه نمی‌دهیم، بلکه کل موتور را به یک سیستم اجرایی واقعی تبدیل می‌کنیم: aaa.accdb REPORTKholase.xlsx ↓ Data Ingestion ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Canonical Database ↓ Network Capacity Engine ↓ Run Management ↓ API ↓ UI / Marketplace در این مرحله همچنین باید کد V1.1–V1.4 را یکپارچه و واقعاً executable کنیم؛ یعنی helperهای باقی‌مانده، schema دیتابیس، migration، API contract، configuration، logging، deterministic run، error handling و تست‌های end-to-end را از حالت specification خارج کرده و به package قابل اجرا تبدیل کنیم. این همان نقطه‌ای است که می‌توانیم از «معماری و prototype» به MVP نرم‌افزار واقعی Railway Capacity Engine عبور کنیم. --- ## ادامه 35 — پرسش کاربر (`message-id:4ede46eb-612c-4224-bd29-5a80e4bdb51b`) بله لطفا --- ## ادامه 35 — پاسخ دستیار (`message-id:d199884f-bd4c-4042-a083-9c525dbbec5c`) حتماً. از اینجا وارد V1.5 — Production Data Platform & Executable MVP می‌شویم. این نسخه باید یک هدف مشخص داشته باشد: کدی که تا اینجا طراحی کرده‌ایم باید از حالت مجموعه‌ای از prototypeها و specificationها خارج شود و به یک هسته نرم‌افزاری منسجم، قابل اجرا، قابل تست و آماده اتصال به aaa.accdb و REPORTKholase_31-06-1405_02-19-35.xlsx تبدیل شود. برای دیتابیس عملیاتی، پیشنهاد معماری این است که Access و Excel فقط Source System باشند و Canonical Database مستقل داشته باشیم. این با معماری FastAPI/SQLAlchemy سازگار است؛ مستندات FastAPI نیز برای production استفاده از database server مانند PostgreSQL را مناسب‌تر از SQLite معرفی می‌کند و migrationها را به ابزارهایی مانند Alembic واگذار می‌کند. FastAPI +1 همچنین pyodbc فقط لایه اتصال ODBC است و برای Access وجود ODBC Driver روی ماشین اجرا ضروری است؛ بنابراین Access باید Adapter ورودی باقی بماند، نه database اصلی سیستم. ODBC +1 1. معماری Production V1.5 معماری نهایی: ┌──────────────────────┐ │ Excel Sources │ │ REPORTKholase.xlsx │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Excel Adapter │ └──────────┬───────────┘ │ │ ┌──────────────────┐ ▼ │ aaa.accdb │───► Access Adapter └──────────────────┘ │ ▼ ┌──────────────────┐ │ Staging │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Mapping Engine │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Quality Gate │ └────────┬─────────┘ │ ▼ ┌────────────────────┐ │ Canonical Database │ └─────────┬──────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ Demand Model Network Model Rolling Stock │ │ │ └────────────────┼────────────────┘ ▼ ┌──────────────────┐ │ Capacity Engine │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Run Management │ └────────┬─────────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ API UI Marketplace 2. اصل مهم: Source ≠ Canonical این اصل را در V1.5 کاملاً enforce می‌کنیم: Access ≠ Canonical Database Excel ≠ Canonical Database بلکه: Source ↓ Raw ↓ Staging ↓ Mapped ↓ Validated ↓ Canonical بنابراین اگر فردا ساختار Access عوض شود، Solver نباید تغییر کند. 3. Project Structure نسخه Production را این‌گونه می‌چینیم: railway_capacity_engine/ │ ├── pyproject.toml ├── README.md ├── alembic.ini ├── .env.example │ ├── config/ │ ├── development.yml │ ├── test.yml │ └── production.yml │ ├── alembic/ │ ├── env.py │ └── versions/ │ ├── app/ │ │ │ ├── main.py │ │ │ ├── config/ │ │ └── settings.py │ │ │ ├── db/ │ │ ├── base.py │ │ ├── session.py │ │ └── models/ │ │ │ ├── api/ │ │ ├── routes/ │ │ │ ├── health.py │ │ │ ├── data.py │ │ │ ├── runs.py │ │ │ ├── capacity.py │ │ │ ├── scenarios.py │ │ │ └── explanations.py │ │ └── schemas/ │ │ │ ├── domain/ │ │ ├── infrastructure.py │ │ ├── station.py │ │ ├── route.py │ │ ├── train.py │ │ ├── demand.py │ │ ├── wagon.py │ │ ├── locomotive.py │ │ ├── formation.py │ │ ├── schedule.py │ │ ├── resource.py │ │ ├── scenario.py │ │ └── result.py │ │ │ ├── adapters/ │ │ ├── access.py │ │ ├── excel.py │ │ └── filesystem.py │ │ │ ├── staging/ │ │ ├── models.py │ │ └── repository.py │ │ │ ├── mapping/ │ │ ├── registry.py │ │ ├── train.py │ │ ├── station.py │ │ └── infrastructure.py │ │ │ ├── quality/ │ │ ├── rules.py │ │ ├── gate.py │ │ └── report.py │ │ │ ├── reconciliation/ │ │ ├── identity.py │ │ └── service.py │ │ │ ├── engines/ │ │ ├── formation.py │ │ ├── wagon_cycle.py │ │ ├── locomotive_cycle.py │ │ ├── scheduling.py │ │ ├── network.py │ │ └── capacity.py │ │ │ ├── validation/ │ │ ├── schedule.py │ │ ├── formation.py │ │ ├── rolling_stock.py │ │ └── network.py │ │ │ ├── explanation/ │ │ ├── bottleneck.py │ │ ├── proof.py │ │ └── narrative.py │ │ │ └── services/ │ ├── ingestion.py │ ├── run_manager.py │ ├── capacity_service.py │ └── scenario_service.py │ ├── mappings/ ├── fixtures/ ├── tests/ │ ├── unit/ │ ├── integration/ │ └── e2e/ │ └── scripts/ ├── inspect_access.py ├── inspect_excel.py ├── ingest.py └── run_capacity.py 4. Database Strategy برای Production: PostgreSQL برای unit test: SQLite برای Access: ODBC → Adapter → Staging یعنی: aaa.accdb ↓ pyodbc ↓ AccessAdapter ↓ Staging ↓ PostgreSQL نه: Solver → Access این جداسازی بسیار مهم است. 5. SQLAlchemy Base Python from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass و Session: Python from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker engine = create_engine( DATABASE_URL, pool_pre_ping=True, ) SessionLocal = sessionmaker( bind=engine, autoflush=False, autocommit=False, ) 6. Versioned Data هر ingestion باید Data Version ایجاد کند: Python @dataclass(frozen=True) class DataVersion: version_id: str source_name: str source_hash: str imported_at: datetime mapping_version: str quality_status: str مثلاً: DATA-2026-09-28-001 اگر فایل Access تغییر کرد: DATA-2026-09-28-002 نباید نتیجه قبلی overwrite شود. 7. Source Hash برای audit: Python import hashlib def sha256_file(path: str) -> str: digest = hashlib.sha256() with open(path, "rb") as f: for chunk in iter( lambda: f.read(1024 * 1024), b"", ): digest.update(chunk) return digest.hexdigest() در نتیجه: File ↓ SHA256 ↓ Data Version و بعداً دقیقاً می‌توان گفت یک Capacity Run با کدام فایل اجرا شده است. 8. Staging Table برای Access: stg_access_train_movement با fieldهای source اصلی: source_record_id source_table TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir نکته مهم: هیچ fieldی را در Staging تغییر معنایی نمی‌دهیم. مثلاً: seir در staging همان seir است. فقط در Mapping می‌گوییم: seir → baseline_running_time_to_next 9. Canonical TrainStationCall در Canonical: Python @dataclass class TrainStationCall: train_run_id: str sequence: int station_id: str time_in: int time_take: int time_out: int required_wait: int | None kilometerage: float | None source_distance: float | None derived_distance: float | None max_speed: float | None baseline_running_time_to_next: int | None این تفکیک بسیار مهم است: Distance ↓ source_distance Kilometerage(next)-Kilometerage(current) ↓ derived_distance و: seir ↓ baseline_running_time_to_next 10. Identity Reconciliation این بخش یکی از مهم‌ترین بخش‌های V1.5 است. Access: TrainNo = 100 TrainName = گار-اندیمشک1 Excel: شماره قطار از مبدا = ... نام قطار = ... نباید صرفاً: Python TrainNo == ExcelTrainNo فرض کنیم. Identity باید از چند evidence ساخته شود: Train Number + Train Name + Origin + Destination + Direction + Operating Pattern 11. Reconciliation Result Python @dataclass(frozen=True) class IdentityMatch: source_a_id: str source_b_id: str confidence: float matched_by: tuple[str, ...] status: str مثلاً: status = MATCHED confidence = 0.98 matched_by: - train_name - origin - destination - direction یا: status = REVIEW_REQUIRED 12. Quality Gate هیچ data version نباید بدون Quality Gate وارد Solver شود. RAW ↓ STAGING ↓ QUALITY ├── PASS ├── WARNING └── FAIL FAIL Examples Sequence duplicated Train has no origin Train has no destination Arrival < previous departure Negative dwell Impossible time Unknown station Unknown mapping Missing mandatory route segment WARNING Distance missing MaxSpeed missing sumDistancezz unknown RequiredWait absent 13. Quality Policy مثلاً: YAML quality: fail_on: - invalid_sequence - invalid_time_order - unknown_station - missing_required_mapping warning_on: - missing_distance - missing_max_speed - missing_required_wait پس: No Verified Mapping ↓ No Production Use حفظ می‌شود. 14. Run Management اکنون هر اجرای Solver یک Entity مستقل است: Python @dataclass class CapacityRun: run_id: str scenario_id: str data_version_id: str model_version: str solver_version: str status: str started_at: datetime finished_at: datetime | None objective: str | None States: CREATED VALIDATING SOLVING VALIDATING_RESULT COMPLETED FAILED CANCELLED 15. Run Lifecycle POST /runs ↓ CREATED ↓ Data Validation ↓ SOLVING ↓ Independent Validation ↓ Explanation ↓ COMPLETED اگر solver crash کرد: FAILED ولی Run از بین نمی‌رود. 16. FastAPI ساختار: Python from fastapi import FastAPI app = FastAPI( title="Railway Capacity Engine", version="1.5.0", ) Health: Python @app.get("/health") def health(): return { "status": "ok", "model_version": "1.5.0", } 17. Capacity API Python from pydantic import BaseModel class CapacityRunRequest(BaseModel): scenario_id: str route_ids: list[str] planning_horizon_days: int = 1 objective: str = "MAX_FREIGHT" Endpoint: Python @app.post("/network/capacity/run") def run_capacity( request: CapacityRunRequest, ): run = capacity_service.create_run( request ) return run 18. API نباید Solver را مستقیماً expose کند این معماری غلط است: API ↓ CP-SAT معماری صحیح: API ↓ CapacityService ↓ RunManager ↓ Canonical Repository ↓ Network Model Builder ↓ Solver ↓ Validator ↓ Result Repository این باعث می‌شود API، UI، CLI و Marketplace همگی از یک business logic استفاده کنند. 19. Run Result JSON { "run_id": "RUN-000145", "status": "COMPLETED", "model_version": "1.5.0", "capacity": { "network": 21 }, "routes": { "R001": 8, "R002": 7, "R003": 6 }, "demand": { "market": 25, "transportable": 23, "allocated": 21, "unserved": 2 }, "validation": { "valid": true }, "proof": { "valid": true } } اعداد این JSON صرفاً نمونه قرارداد هستند، نه نتیجه واقعی داده‌های شما. 20. Error Model نباید exception خام Python به UI برسد. مثلاً: Python class CapacityEngineError(Exception): pass class DataQualityError(CapacityEngineError): pass class MappingError(CapacityEngineError): pass class InfeasibleModelError(CapacityEngineError): pass class CapacityProofError(CapacityEngineError): pass API: JSON { "error_code": "DATA_QUALITY_FAILED", "message": "Required station mapping is missing.", "run_id": "RUN-000145" } 21. Logging هر Run باید correlation ID داشته باشد: run_id=RUN-000145 Log: INFO ingestion started INFO Access records loaded: 1248 INFO Excel rows loaded: 31 INFO mapping completed INFO quality gate passed INFO canonical model built INFO network model built INFO solver started INFO solver completed INFO independent validation passed INFO capacity proof completed 22. Determinism برای ظرفیت‌سنجی، reproducibility بسیار مهم است. بنابراین: Python SOLVER_SEED = 1 NUM_WORKERS = 1 در baseline و regression test. در production ممکن است parallel solving فعال شود، ولی باید: Solver Version Seed Parameters Time Limit Threads Objective در Run ثبت شوند. 23. Configuration به جای hard-code: YAML model: version: "1.5.0" solver: backend: "CP-SAT" workers: 1 random_seed: 1 time_limit_seconds: 300 capacity: proof_required: true validate_f_plus_1: true data: quality_gate: true database: url_env: "DATABASE_URL" 24. Access Configuration YAML sources: access: enabled: true file: "./data/source/aaa.accdb" table: "YourTable" excel: enabled: true file: "./data/source/REPORTKholase_31-06-1405_02-19-35.xlsx" sheet: 0 نام table Access را در اینجا باید از inspection واقعی فایل بگیریم؛ نباید حدس بزنیم. 25. Access Adapter Production اصل مهم: Allowed table names باید قبل از query بررسی شوند. مثلاً: Python def validate_table_name( requested: str, available: set[str], ) -> str: if requested not in available: raise ValueError( f"Unknown Access table: {requested}" ) return requested بعد query ساخته می‌شود. این بهتر از پذیرش مستقیم نام table از request است. 26. Access Driver در Deployment باید check داشته باشیم: Python ↓ pyodbc ↓ ODBC Driver ↓ Access Database وجود pyodbc به تنهایی به معنای امکان باز کردن .accdb نیست؛ ODBC driver باید روی محیط اجرا موجود باشد. ODBC +1 پس CLI: Bash python scripts/inspect_access.py باید خروجی بدهد: ODBC available: YES Access file: FOUND Connection: OK Tables: ... 27. Migration Schema را با Alembic مدیریت می‌کنیم: alembic/ ├── env.py └── versions/ ├── 001_initial.py ├── 002_train_network.py ├── 003_rolling_stock.py └── 004_capacity_run.py Alembic برای migration و schema evolution مناسب است و autogenerate می‌تواند migration candidate ایجاد کند، اما migration تولیدشده باید قبل از اعمال review شود. Alembic 28. Initial Database Tables نسخه اول: data_version source_file source_record station station_track block junction junction_movement junction_conflict train_service train_run train_station_call route route_segment operating_pattern demand freight_flow wagon_requirement wagon_type wagon_pool wagon_inventory wagon_cycle locomotive_type locomotive_pool locomotive_cycle locomotive_assignment train_formation train_formation_item resource resource_usage capacity_run capacity_result capacity_proof binding_constraint explanation scenario allocation 29. Repository Pattern Solver نباید SQL بداند. بد: Python solver.execute( "SELECT * FROM train_run" ) خوب: Python train_runs = ( train_repository .get_by_scenario( scenario_id ) ) و: Python network = ( network_repository .load_version( infrastructure_version ) ) 30. Capacity Service هسته application: Python class CapacityService: def run( self, request, ): run = self.run_manager.create( request ) data = self.loader.load( request ) self.quality_gate.validate( data ) canonical = ( self.canonical_builder .build(data) ) network = ( self.network_builder .build(canonical) ) result = ( self.engine.solve( network ) ) validation = ( self.validator.validate( result ) ) proof = ( self.proof_engine .build( network, result, ) ) explanation = ( self.explanation_engine .build( result, proof, ) ) return self.result_store.save( run, result, validation, proof, explanation, ) این باید single orchestration point باشد. 31. End-to-End Pipeline حالا کل سیستم: 1. Source Inspection ↓ 2. Ingestion ↓ 3. Data Version ↓ 4. Staging ↓ 5. Mapping ↓ 6. Reconciliation ↓ 7. Quality Gate ↓ 8. Canonical Model ↓ 9. Network Model ↓ 10. Demand ↓ 11. Formation ↓ 12. Wagon Cycle ↓ 13. Locomotive Cycle ↓ 14. Candidate Generation ↓ 15. Network Scheduling ↓ 16. Independent Validation ↓ 17. Capacity Search ↓ 18. F+1 Proof ↓ 19. Bottleneck ↓ 20. Explanation ↓ 21. Persist Result ↓ 22. API / UI / Marketplace 32. تست‌ها V1.5 باید حداقل این Test Pyramid را داشته باشد: E2E / \ Integration / \ Unit -------- Domain Unit time parsing day parsing direction distance derivation seir mapping formation wagon cycle locomotive cycle headway physical block identity Integration Access → Staging Excel → Staging Staging → Canonical Canonical → Network Network → Solver E2E Source ↓ Run ↓ Capacity ↓ Proof 33. Golden Test یک test بسیار مهم: Golden Case با fixture کنترل‌شده: 2 stations 1 single-track block 2 trains انتظار: F=1 → feasible F=2 → feasible F=3 → infeasible و: Capacity = 2 Proof = valid این test باید همیشه اجرا شود. 34. Real Data Test بعد از آن: aaa.accdb + REPORTKholase... اما نتیجه باید به این شکل باشد: REAL DATA TEST ────────────── Access: connection: PASS records: ... Excel: workbook: PASS rows: ... Mapping: PASS / REVIEW Quality: PASS / FAIL Reconciliation: MATCHED: ... REVIEW: ... UNMATCHED: ... Canonical: train runs: ... station calls: ... Solver: NOT RUN تا زمانی که mapping و quality تأیید نشده‌اند، نباید عدد ظرفیت واقعی اعلام کنیم. 35. بسیار مهم: Real Data ≠ Real Capacity حتی اگر Access و Excel بدون خطا load شوند: Data Loaded به معنی: Capacity Verified نیست. زنجیره باید: Data Loaded ↓ Mapping Verified ↓ Quality Passed ↓ Infrastructure Verified ↓ Schedule Generated ↓ Independent Validation Passed ↓ F Feasible ↓ F+1 Infeasible ↓ Capacity Proven باشد. 36. MVP واقعی V1.5 در پایان V1.5 یک دستور باید داشته باشیم: Bash python -m app.cli.run_capacity \ --config config/development.yml \ --scenario BASE و: ╔══════════════════════════════════════════╗ ║ RAILWAY CAPACITY ENGINE 1.5 ║ ╚══════════════════════════════════════════╝ Data Version: DATA-001 Scenario: BASE [1/10] Inspecting sources ........ PASS [2/10] Loading Access ............ PASS [3/10] Loading Excel ............ PASS [4/10] Mapping .................. PASS [5/10] Quality Gate ............. PASS [6/10] Canonical Model .......... PASS [7/10] Network Build ............ PASS [8/10] Scheduling ............... PASS [9/10] Validation ............... PASS [10/10] Capacity Proof .......... PASS Network Capacity: 21 trains/day Proof: F=21 FEASIBLE F=22 INFEASIBLE Result: RUN-000145 البته عدد 21 در این نمایش صرفاً نمونه UI است. 37. Definition of Done — V1.5 ✓ Production project structure ✓ PostgreSQL-ready persistence ✓ SQLAlchemy ✓ Alembic migrations ✓ Access Adapter ✓ Excel Adapter ✓ Source hashing ✓ Data Versioning ✓ Staging ✓ Mapping Registry ✓ Reconciliation ✓ Quality Gate ✓ Canonical Model ✓ Network Model ✓ Wagon Cycle ✓ Locomotive Cycle ✓ Formation ✓ Network Scheduler ✓ Independent Validator ✓ Capacity Search ✓ Capacity Proof ✓ Run Management ✓ Scenario Management ✓ Bottleneck Analysis ✓ Explanation ✓ REST API ✓ CLI ✓ Logging ✓ Configuration ✓ Deterministic Solver Mode ✓ Unit Tests ✓ Integration Tests ✓ End-to-End Tests 38. مهم‌ترین تغییر V1.5 تا اینجا پروژه داشتیم: Mathematical Model + Architecture + Prototype از V1.5 تبدیل می‌شود به: Railway Capacity Platform │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ Data Platform Capacity Engine API Platform │ │ │ └─────────────────┼─────────────────┘ ▼ UI / UX │ ▼ Marketplace و این دقیقاً همان معماری‌ای است که اجازه می‌دهد بعداً بدون بازنویسی هسته: Dashboard Scheduling Workspace Capacity Inspector Scenario Lab Bottleneck Inspector Marketplace Allocation Reporting Digital Twin Simulation را روی همین Core سوار کنیم. یک تصمیم فنی مهم برای ادامه من برای Production Target این stack را تثبیت می‌کنم: Python 3.12+ FastAPI Pydantic SQLAlchemy 2.x Alembic PostgreSQL OR-Tools CP-SAT pandas openpyxl pyodbc pytest و برای Access فقط به‌عنوان Legacy Source Adapter از ODBC استفاده می‌کنیم. این انتخاب با واقعیت فنی Access سازگار است، چون pyodbc به ODBC driver وابسته است. ODBC +1 گام بعدی را باید V1.6 بگذاریم: Complete Executable Codebase؛ یعنی دیگر specification جدید اضافه نکنیم، بلکه کل قطعات V1.1 تا V1.5 را در یک package واحد ادغام کنیم و فایل‌های واقعی pyproject.toml، مدل‌های SQLAlchemy، migrations، adapters، mapping YAML، services، API، CLI و test suite را یکجا بسازیم؛ سپس آن را برای اتصال مستقیم به aaa.accdb و فایل Excel واقعی آماده کنیم. --- ## ادامه 36 — پرسش کاربر (`message-id:dcf86b06-ed2c-4e5d-b646-9c69a9ca5ec0`) بله لطفا --- ## ادامه 36 — پاسخ دستیار (`message-id:0e269155-8b49-4fb7-acb3-b70b7d1a7faf`) حتماً. برویم سراغ Version 1.6 — Complete Executable Codebase؛ این نسخه باید از حالت Specification خارج شود و به یک Repository قابل اجرا تبدیل شود. در این نسخه، چند اصلاح مهم نسبت به V1.5 هم اعمال می‌کنم: Access/Excel → Raw → Staging → Canonical مدل SQLAlchemy 2.x PostgreSQL برای Production و SQLite برای Test Alembic migration FastAPI API Adapter واقعی برای aaa.accdb و فایل Excel Mapping واقعی فیلدهای Access که تا اینجا از داده‌های شما شناسایی کرده‌ایم seir → baseline_running_time_to_next Kilometerage → source chainage محاسبه derived_distance مدیریت Midnight rollover TrainRun → TrainStationCall → DirectedRoutePath Physical Block مستقل از Direction Single Track / Double Track baseline schedule CP-SAT scheduler Independent Validator Capacity Search واقعی با تست F و F+1 Capacity Proof Binding Constraint / Explanation Run / Data Version / Model Version تست‌های Unit / Integration / E2E CLI برای ingest و capacity run انتخاب PostgreSQL برای Production و استفاده از migration به‌جای create_all() در محیط Production با الگوی مستندشده FastAPI/Alembic هم‌راستا است. FastAPI +2 Alembic +2 V1.6 — Complete Executable Codebase 1. ساختار نهایی Repository railway_capacity_engine/ │ ├── pyproject.toml ├── README.md ├── .env.example ├── alembic.ini │ ├── config/ │ ├── development.yml │ ├── test.yml │ └── production.yml │ ├── alembic/ │ ├── env.py │ └── versions/ │ └── 0001_initial.py │ ├── mappings/ │ ├── access_train_movement.yml │ └── excel_kholase.yml │ ├── fixtures/ │ └── infrastructure.yml │ ├── app/ │ ├── __init__.py │ ├── main.py │ │ │ ├── config/ │ │ ├── __init__.py │ │ └── settings.py │ │ │ ├── db/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── session.py │ │ └── models/ │ │ ├── __init__.py │ │ ├── source.py │ │ ├── infrastructure.py │ │ ├── train.py │ │ ├── run.py │ │ └── result.py │ │ │ ├── domain/ │ │ ├── common.py │ │ ├── station.py │ │ ├── infrastructure.py │ │ ├── train.py │ │ ├── route.py │ │ ├── schedule.py │ │ ├── capacity.py │ │ └── result.py │ │ │ ├── adapters/ │ │ ├── access.py │ │ └── excel.py │ │ │ ├── staging/ │ │ ├── models.py │ │ └── repository.py │ │ │ ├── mapping/ │ │ ├── registry.py │ │ └── train.py │ │ │ ├── quality/ │ │ ├── rules.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ └── identity.py │ │ │ ├── scheduling/ │ │ ├── path.py │ │ ├── problem.py │ │ └── solver.py │ │ │ ├── validation/ │ │ └── schedule.py │ │ │ ├── capacity/ │ │ ├── search.py │ │ └── proof.py │ │ │ ├── explanation/ │ │ ├── bottleneck.py │ │ └── narrative.py │ │ │ ├── services/ │ │ ├── ingestion.py │ │ ├── run_manager.py │ │ └── capacity.py │ │ │ └── api/ │ ├── deps.py │ └── routes/ │ ├── health.py │ ├── data.py │ ├── runs.py │ └── capacity.py │ ├── scripts/ │ ├── inspect_access.py │ ├── inspect_excel.py │ ├── ingest.py │ └── run_capacity.py │ └── tests/ ├── unit/ │ ├── test_time.py │ ├── test_mapping.py │ ├── test_direction.py │ └── test_capacity.py │ ├── integration/ │ ├── test_access_mapping.py │ └── test_pipeline.py │ └── e2e/ └── test_capacity_run.py 2. pyproject.toml TOML [build-system] requires = ["setuptools>=69", "wheel"] build-backend = "setuptools.build_meta" [project] name = "railway-capacity-engine" version = "1.6.0" description = "Iranian railway freight capacity planning and scheduling engine" requires-python = ">=3.12" dependencies = [ "fastapi>=0.115,<1", "uvicorn[standard]>=0.30,<1", "pydantic>=2.9,<3", "pydantic-settings>=2.5,<3", "sqlalchemy>=2.0,<3", "alembic>=1.14,<2", "psycopg[binary]>=3.2,<4", "pandas>=2.2,<3", "openpyxl>=3.1,<4", "PyYAML>=6,<7", "ortools>=9.14,<10", "python-dotenv>=1,<2" ] [project.optional-dependencies] access = [ "pyodbc>=5,<6" ] test = [ "pytest>=8,<9", "httpx>=0.27,<1" ] dev = [ "pytest>=8,<9", "httpx>=0.27,<1", "ruff>=0.8,<1" ] [project.scripts] railway-ingest = "scripts.ingest:main" railway-capacity = "scripts.run_capacity:main" [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] addopts = "-q" [tool.ruff] line-length = 100 target-version = "py312" 3. Configuration app/config/settings.py Python from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): app_name: str = "Railway Capacity Engine" environment: str = "development" database_url: str = ( "sqlite:///./railway_capacity.db" ) solver_time_limit_seconds: int = 60 solver_workers: int = 1 solver_seed: int = 1 model_version: str = "1.6.0" model_config = SettingsConfigDict( env_file=".env", extra="ignore", ) @lru_cache def get_settings() -> Settings: return Settings() 4. Database Layer app/db/base.py Python from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass app/db/session.py Python from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.config.settings import get_settings settings = get_settings() connect_args = {} if settings.database_url.startswith("sqlite"): connect_args = {"check_same_thread": False} engine = create_engine( settings.database_url, connect_args=connect_args, pool_pre_ping=True, ) SessionLocal = sessionmaker( bind=engine, autoflush=False, expire_on_commit=False, ) def get_db(): db = SessionLocal() try: yield db finally: db.close() 5. Source/Data Version Model app/db/models/source.py Python from datetime import datetime from sqlalchemy import DateTime, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class DataVersion(Base): __tablename__ = "data_versions" id: Mapped[int] = mapped_column(Integer, primary_key=True) source_name: Mapped[str] = mapped_column(String(255), nullable=False) source_type: Mapped[str] = mapped_column(String(50), nullable=False) source_hash: Mapped[str] = mapped_column( String(128), nullable=False, ) status: Mapped[str] = mapped_column( String(50), default="INGESTED", nullable=False, ) notes: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, nullable=False, ) 6. Train Database Models app/db/models/train.py Python from sqlalchemy import Float, Integer, String from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class TrainRunRecord(Base): __tablename__ = "train_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True) train_no: Mapped[str] = mapped_column( String(100), nullable=False, index=True, ) train_name: Mapped[str | None] = mapped_column(String(255)) direction: Mapped[str | None] = mapped_column(String(20)) origin_station: Mapped[str | None] = mapped_column(String(255)) destination_station: Mapped[str | None] = mapped_column(String(255)) source_id: Mapped[int | None] = mapped_column(Integer) sequence: Mapped[int] = mapped_column(Integer, nullable=False) station_name: Mapped[str] = mapped_column( String(255), nullable=False, ) station_number: Mapped[str | None] = mapped_column(String(100)) time_in: Mapped[int | None] = mapped_column(Integer) time_take: Mapped[int | None] = mapped_column(Integer) time_out: Mapped[int | None] = mapped_column(Integer) required_wait: Mapped[int | None] = mapped_column(Integer) kilometerage: Mapped[float | None] = mapped_column(Float) max_speed: Mapped[float | None] = mapped_column(Float) source_distance: Mapped[float | None] = mapped_column(Float) derived_distance: Mapped[float | None] = mapped_column(Float) sum_distance_source: Mapped[float | None] = mapped_column(Float) baseline_running_time_to_next: Mapped[int | None] = mapped_column( Integer ) seir_source: Mapped[int | None] = mapped_column(Integer) 7. Capacity Run app/db/models/run.py Python from datetime import datetime from sqlalchemy import DateTime, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class CapacityRunRecord(Base): __tablename__ = "capacity_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True) run_id: Mapped[str] = mapped_column( String(100), unique=True, nullable=False, index=True, ) scenario_id: Mapped[str] = mapped_column( String(100), nullable=False, ) data_version: Mapped[str] = mapped_column( String(100), nullable=False, ) model_version: Mapped[str] = mapped_column( String(100), nullable=False, ) status: Mapped[str] = mapped_column( String(50), nullable=False, ) capacity: Mapped[int | None] = mapped_column(Integer) proof_valid: Mapped[bool] = mapped_column( default=False, nullable=False, ) result_json: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, nullable=False, ) 8. Domain Model app/domain/common.py Python from enum import Enum class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class LoadState(str, Enum): LOADED = "LOADED" EMPTY = "EMPTY" 9. Time Utilities app/domain/schedule.py Python from dataclasses import dataclass DAY = 24 * 60 def parse_hhmm(value: str | None) -> int | None: if value is None: return None value = str(value).strip() if not value: return None hour, minute = value.split(":")[:2] return int(hour) * 60 + int(minute) def normalize_time_sequence( values: list[int | None], ) -> list[int | None]: result: list[int | None] = [] offset = 0 previous = None for value in values: if value is None: result.append(None) continue current = value + offset if previous is not None and current < previous: offset += DAY current = value + offset result.append(current) previous = current return result @dataclass(frozen=True) class TimeWindow: start: int end: int 10. Train Station Call app/domain/train.py Python from dataclasses import dataclass, field from app.domain.common import Direction @dataclass class TrainStationCall: sequence: int station_id: str time_in: int | None = None time_take: int | None = None time_out: int | None = None required_wait: int | None = None kilometerage: float | None = None source_distance: float | None = None derived_distance: float | None = None max_speed: float | None = None baseline_running_time_to_next: int | None = None @dataclass class TrainRun: train_no: str train_name: str direction: Direction origin_station: str destination_station: str calls: list[TrainStationCall] = field( default_factory=list ) def ordered_calls(self) -> list[TrainStationCall]: return sorted( self.calls, key=lambda x: x.sequence, ) 11. Infrastructure app/domain/infrastructure.py Python from dataclasses import dataclass from app.domain.common import TrackType @dataclass(frozen=True) class Station: id: str name: str usable_length_m: float = 0 @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int = 3 switch_time: int = 5 @staticmethod def make_id( station_a: str, station_b: str, ) -> str: a, b = sorted( [station_a, station_b] ) return f"{a}::{b}" 12. Directed Path app/domain/route.py Python from dataclasses import dataclass from app.domain.common import Direction from app.domain.infrastructure import PhysicalBlock from app.domain.train import TrainRun @dataclass(frozen=True) class DirectedBlock: physical_block_id: str station_from: str station_to: str direction: Direction running_time: int track_type: str @dataclass class DirectedRoutePath: train_no: str blocks: list[DirectedBlock] def build_directed_path( train: TrainRun, infrastructure: dict[str, PhysicalBlock], ) -> DirectedRoutePath: calls = train.ordered_calls() blocks: list[DirectedBlock] = [] for current, nxt in zip(calls, calls[1:]): block_id = PhysicalBlock.make_id( current.station_id, nxt.station_id, ) if block_id not in infrastructure: raise ValueError( f"Infrastructure block not found: {block_id}" ) block = infrastructure[block_id] running_time = ( current.baseline_running_time_to_next or ( block.running_time_forward if train.direction.value == "FORWARD" else block.running_time_reverse ) ) blocks.append( DirectedBlock( physical_block_id=block.id, station_from=current.station_id, station_to=nxt.station_id, direction=train.direction, running_time=running_time, track_type=block.track_type.value, ) ) return DirectedRoutePath( train_no=train.train_no, blocks=blocks, ) 13. Access Adapter این قسمت عمداً فقط Adapter است و نباید هیچ منطق Domain یا Solver داخل آن قرار گیرد. app/adapters/access.py Python from pathlib import Path from typing import Any import pandas as pd class AccessAdapter: def __init__(self, path: str): self.path = Path(path) def _connection_string(self) -> str: return ( "DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};" f"DBQ={self.path};" ) def list_tables(self) -> list[str]: import pyodbc connection = pyodbc.connect( self._connection_string() ) try: cursor = connection.cursor() return [ row.table_name for row in cursor.tables() if row.table_type == "TABLE" ] finally: connection.close() def extract_table( self, table_name: str, ) -> pd.DataFrame: import pyodbc if not table_name.replace("_", "").isalnum(): raise ValueError( "Unsafe Access table name" ) connection = pyodbc.connect( self._connection_string() ) try: query = f"SELECT * FROM [{table_name}]" return pd.read_sql( query, connection, ) finally: connection.close() در محیط اجرا باید Microsoft Access ODBC Driver نصب باشد؛ pyodbc خودش Driver مربوط به Access را فراهم نمی‌کند. 14. Excel Adapter app/adapters/excel.py Python from pathlib import Path import pandas as pd class ExcelAdapter: def __init__(self, path: str): self.path = Path(path) def sheets(self) -> list[str]: with pd.ExcelFile(self.path) as xls: return xls.sheet_names def extract( self, sheet_name: str | None = None, ) -> pd.DataFrame: if sheet_name is None: sheet_name = self.sheets()[0] return pd.read_excel( self.path, sheet_name=sheet_name, dtype=object, ) 15. Mapping Registry app/mapping/registry.py Python from dataclasses import dataclass @dataclass(frozen=True) class FieldMapping: source_field: str target_field: str confidence: str notes: str = "" class MappingRegistry: def __init__( self, mappings: list[FieldMapping], ): self._mappings = { m.source_field: m for m in mappings } def get( self, source_field: str, ) -> FieldMapping | None: return self._mappings.get(source_field) def require_verified( self, source_field: str, ) -> FieldMapping: mapping = self.get(source_field) if mapping is None: raise ValueError( f"No mapping for field: {source_field}" ) if mapping.confidence not in { "HIGH", "VERIFIED", }: raise ValueError( f"Field {source_field} is not verified" ) return mapping 16. Access Mapping app/mapping/train.py Python from typing import Any from app.domain.common import Direction from app.domain.schedule import parse_hhmm from app.domain.train import ( TrainRun, TrainStationCall, ) def normalize_text(value: Any) -> str: return str(value).strip() def infer_direction( kilometerages: list[float], ) -> Direction: if len(kilometerages) < 2: raise ValueError( "Cannot infer direction" ) if kilometerages[-1] > kilometerages[0]: return Direction.FORWARD if kilometerages[-1] < kilometerages[0]: return Direction.REVERSE raise ValueError( "Direction cannot be inferred from equal kilometerages" ) def map_access_train( rows: list[dict[str, Any]], ) -> TrainRun: rows = sorted( rows, key=lambda r: int(r["Sequence"]), ) km = [ float(r["Kilometerage"]) for r in rows if r.get("Kilometerage") not in (None, "") ] direction = infer_direction(km) calls = [] for row in rows: time_in = parse_hhmm(row.get("time_in")) time_out = parse_hhmm(row.get("time_out")) calls.append( TrainStationCall( sequence=int(row["Sequence"]), station_id=normalize_text( row["StationName"] ), time_in=time_in, time_take=( int(row["time_take"]) if row.get("time_take") not in (None, "") else None ), time_out=time_out, required_wait=( int(row["RequiredWait"]) if row.get("RequiredWait") not in (None, "") else None ), kilometerage=( float(row["Kilometerage"]) if row.get("Kilometerage") not in (None, "") else None ), source_distance=( float(row["Distance"]) if row.get("Distance") not in (None, "") else None ), max_speed=( float(row["MaxSpeed"]) if row.get("MaxSpeed") not in (None, "") else None ), baseline_running_time_to_next=( int(row["seir"]) if row.get("seir") not in (None, "") else None ), ) ) for i in range(len(calls) - 1): a = calls[i] b = calls[i + 1] if ( a.kilometerage is not None and b.kilometerage is not None ): a.derived_distance = abs( b.kilometerage - a.kilometerage ) return TrainRun( train_no=normalize_text( rows[0]["TrainNo"] ), train_name=normalize_text( rows[0]["TrainName"] ), direction=direction, origin_station=calls[0].station_id, destination_station=calls[-1].station_id, calls=calls, ) 17. Quality Gate app/quality/gate.py Python from dataclasses import dataclass from app.domain.train import TrainRun @dataclass class QualityIssue: severity: str code: str message: str def validate_train_run( train: TrainRun, ) -> list[QualityIssue]: issues: list[QualityIssue] = [] calls = train.ordered_calls() if not calls: issues.append( QualityIssue( "ERROR", "EMPTY_TRAIN", "Train has no station calls", ) ) return issues for i, call in enumerate(calls): if call.sequence != i + 1: issues.append( QualityIssue( "ERROR", "INVALID_SEQUENCE", ( f"{train.train_no}: " f"expected {i + 1}, " f"got {call.sequence}" ), ) ) if ( call.time_in is not None and call.time_out is not None and call.time_out < call.time_in ): issues.append( QualityIssue( "ERROR", "NEGATIVE_DWELL", ( f"{train.train_no}/" f"{call.station_id}" ), ) ) if ( call.time_take is not None and call.required_wait is not None and call.time_take < call.required_wait ): issues.append( QualityIssue( "WARNING", "DWELL_BELOW_REQUIRED_WAIT", ( f"{train.train_no}/" f"{call.station_id}" ), ) ) return issues def quality_gate( trains: list[TrainRun], ) -> list[QualityIssue]: issues = [] for train in trains: issues.extend( validate_train_run(train) ) if any( issue.severity == "ERROR" for issue in issues ): raise ValueError( "Data Quality Gate failed" ) return issues 18. CP-SAT Scheduler app/scheduling/solver.py Python from dataclasses import dataclass from ortools.sat.python import cp_model from app.domain.infrastructure import PhysicalBlock from app.domain.route import DirectedRoutePath from app.domain.train import TrainRun @dataclass class BlockSchedule: train_no: str block_id: str entry: int exit: int @dataclass class TrainSchedule: train_no: str blocks: list[BlockSchedule] @dataclass class SolverResult: feasible: bool schedules: list[TrainSchedule] objective: int | None = None status: str = "" class ScheduleSolver: def __init__( self, time_limit_seconds: int = 60, workers: int = 1, seed: int = 1, ): self.time_limit_seconds = ( time_limit_seconds ) self.workers = workers self.seed = seed def solve( self, trains: list[TrainRun], paths: list[DirectedRoutePath], infrastructure: dict[str, PhysicalBlock], horizon: int = 2880, ) -> SolverResult: model = cp_model.CpModel() variables = {} intervals_by_resource = {} for train, path in zip(trains, paths): previous_exit = None for index, block in enumerate( path.blocks ): entry = model.NewIntVar( 0, horizon, f"{train.train_no}_entry_{index}", ) exit_ = model.NewIntVar( 0, horizon, f"{train.train_no}_exit_{index}", ) duration = block.running_time model.Add( exit_ == entry + duration ) if previous_exit is not None: model.Add( entry >= previous_exit ) previous_exit = exit_ variables[ (train.train_no, index) ] = ( entry, exit_, ) block_resource = ( block.physical_block_id ) interval = model.NewIntervalVar( entry, duration, exit_, f"interval_{train.train_no}_{index}", ) intervals_by_resource.setdefault( block_resource, [], ).append(interval) for intervals in intervals_by_resource.values(): model.AddNoOverlap(intervals) solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = ( self.time_limit_seconds ) solver.parameters.num_search_workers = ( self.workers ) solver.parameters.random_seed = ( self.seed ) status = solver.Solve(model) feasible = status in ( cp_model.OPTIMAL, cp_model.FEASIBLE, ) if not feasible: return SolverResult( feasible=False, schedules=[], status=solver.StatusName(status), ) schedules = [] for train, path in zip(trains, paths): blocks = [] for index, block in enumerate( path.blocks ): entry, exit_ = variables[ (train.train_no, index) ] blocks.append( BlockSchedule( train_no=train.train_no, block_id=( block.physical_block_id ), entry=solver.Value(entry), exit=solver.Value(exit_), ) ) schedules.append( TrainSchedule( train_no=train.train_no, blocks=blocks, ) ) return SolverResult( feasible=True, schedules=schedules, objective=None, status=solver.StatusName(status), ) 19. Independent Validator app/validation/schedule.py Python from dataclasses import dataclass from app.domain.route import DirectedRoutePath from app.scheduling.solver import TrainSchedule @dataclass class ValidationIssue: code: str message: str @dataclass class ValidationResult: valid: bool issues: list[ValidationIssue] def validate_schedule( schedules: list[TrainSchedule], paths: list[DirectedRoutePath], ) -> ValidationResult: issues: list[ValidationIssue] = [] expected = { path.train_no: path for path in paths } for schedule in schedules: path = expected.get(schedule.train_no) if path is None: issues.append( ValidationIssue( "UNKNOWN_TRAIN", schedule.train_no, ) ) continue if len(schedule.blocks) != len( path.blocks ): issues.append( ValidationIssue( "BLOCK_COUNT", schedule.train_no, ) ) previous_exit = None for actual, expected_block in zip( schedule.blocks, path.blocks, ): if ( actual.block_id != expected_block.physical_block_id ): issues.append( ValidationIssue( "WRONG_BLOCK", schedule.train_no, ) ) if actual.exit <= actual.entry: issues.append( ValidationIssue( "INVALID_INTERVAL", schedule.train_no, ) ) if ( previous_exit is not None and actual.entry < previous_exit ): issues.append( ValidationIssue( "ROUTE_PRECEDENCE", schedule.train_no, ) ) previous_exit = actual.exit # Independent resource conflict check. resource_intervals = {} for schedule in schedules: for block in schedule.blocks: resource_intervals.setdefault( block.block_id, [], ).append(block) for resource, intervals in resource_intervals.items(): ordered = sorted( intervals, key=lambda x: x.entry, ) for a, b in zip( ordered, ordered[1:], ): if b.entry < a.exit: issues.append( ValidationIssue( "BLOCK_CONFLICT", ( f"{resource}: " f"{a.train_no} / " f"{b.train_no}" ), ) ) return ValidationResult( valid=not issues, issues=issues, ) 20. Capacity Search این قسمت یکی از مهم‌ترین تغییرات V1.6 است. دیگر: capacity = len(train_runs) نداریم. بلکه: F ↓ Generate candidate trains ↓ Schedule ↓ Independent validation ↓ Feasible? ↓ F+1 app/capacity/search.py Python from dataclasses import dataclass from app.domain.infrastructure import PhysicalBlock from app.domain.route import DirectedRoutePath from app.domain.train import TrainRun from app.scheduling.solver import ScheduleSolver from app.validation.schedule import validate_schedule @dataclass class CapacityProof: feasible_f: int tested_f_plus_one: int f_feasible: bool f_plus_one_feasible: bool proof_valid: bool @dataclass class CapacityResult: capacity: int proof: CapacityProof class CapacitySearcher: def __init__( self, solver: ScheduleSolver, ): self.solver = solver def is_feasible( self, trains: list[TrainRun], paths: list[DirectedRoutePath], infrastructure: dict[str, PhysicalBlock], ) -> bool: result = self.solver.solve( trains=trains, paths=paths, infrastructure=infrastructure, ) if not result.feasible: return False validation = validate_schedule( result.schedules, paths, ) return validation.valid def search( self, candidate_trains: list[TrainRun], candidate_paths: list[DirectedRoutePath], infrastructure: dict[str, PhysicalBlock], ) -> CapacityResult: low = 0 high = len(candidate_trains) while low < high: mid = (low + high + 1) // 2 if self.is_feasible( candidate_trains[:mid], candidate_paths[:mid], infrastructure, ): low = mid else: high = mid - 1 capacity = low f_feasible = self.is_feasible( candidate_trains[:capacity], candidate_paths[:capacity], infrastructure, ) f_plus_one_feasible = False if capacity < len(candidate_trains): f_plus_one_feasible = self.is_feasible( candidate_trains[:capacity + 1], candidate_paths[:capacity + 1], infrastructure, ) proof = CapacityProof( feasible_f=capacity, tested_f_plus_one=capacity + 1, f_feasible=f_feasible, f_plus_one_feasible=f_plus_one_feasible, proof_valid=( f_feasible and not f_plus_one_feasible ), ) return CapacityResult( capacity=capacity, proof=proof, ) 21. Explanation Engine app/explanation/bottleneck.py Python from dataclasses import dataclass @dataclass class BindingConstraint: resource_id: str constraint_type: str impact: str evidence: str def identify_block_bottleneck( schedules, ) -> BindingConstraint | None: usage = {} for schedule in schedules: for block in schedule.blocks: usage.setdefault( block.block_id, [], ).append(block) if not usage: return None resource = max( usage, key=lambda x: len(usage[x]), ) return BindingConstraint( resource_id=resource, constraint_type="BLOCK_OCCUPANCY", impact="HIGH", evidence=( f"{len(usage[resource])} train movements " f"assigned to resource {resource}" ), ) 22. Narrative app/explanation/narrative.py Python from app.explanation.bottleneck import ( BindingConstraint, ) def build_capacity_explanation( capacity: int, proof_valid: bool, bottleneck: BindingConstraint | None, ) -> str: lines = [ f"Operational route capacity = {capacity} train(s)." ] if proof_valid: lines.append( "The capacity proof is valid because F is feasible " "and F+1 is infeasible." ) else: lines.append( "The capacity proof is incomplete and must not " "be treated as a final production capacity result." ) if bottleneck: lines.append( f"Binding resource: {bottleneck.resource_id}." ) lines.append( f"Constraint type: {bottleneck.constraint_type}." ) return "\n".join(lines) 23. Capacity Service app/services/capacity.py Python from dataclasses import dataclass from app.capacity.search import ( CapacityResult, CapacitySearcher, ) from app.domain.infrastructure import PhysicalBlock from app.domain.route import DirectedRoutePath from app.domain.train import TrainRun from app.explanation.bottleneck import ( identify_block_bottleneck, ) from app.explanation.narrative import ( build_capacity_explanation, ) from app.scheduling.solver import ScheduleSolver @dataclass class CapacityServiceResult: capacity: int proof_valid: bool explanation: str solver_status: str class CapacityService: def __init__( self, solver: ScheduleSolver, ): self.searcher = CapacitySearcher( solver ) def execute( self, trains: list[TrainRun], paths: list[DirectedRoutePath], infrastructure: dict[str, PhysicalBlock], ) -> CapacityServiceResult: result: CapacityResult = ( self.searcher.search( trains, paths, infrastructure, ) ) final_schedule = self.searcher.solver.solve( trains[:result.capacity], paths[:result.capacity], infrastructure, ) bottleneck = identify_block_bottleneck( final_schedule.schedules ) explanation = build_capacity_explanation( capacity=result.capacity, proof_valid=result.proof.proof_valid, bottleneck=bottleneck, ) return CapacityServiceResult( capacity=result.capacity, proof_valid=result.proof.proof_valid, explanation=explanation, solver_status=final_schedule.status, ) 24. Infrastructure YAML fixtures/infrastructure.yml ساختار Production باید از Master Data خوانده شود؛ این فایل صرفاً برای bootstrap/Test است. YAML stations: - id: GAR name: گار usable_length_m: 700 - id: SAKHEH name: ساکه usable_length_m: 700 - id: BAGHYEK name: باغ یک usable_length_m: 700 - id: ANDIMESHK name: اندیمشک usable_length_m: 700 blocks: - id: GAR::SAKHEH station_a: GAR station_b: SAKHEH track_type: SINGLE running_time_forward: 34 running_time_reverse: 34 headway_same_direction: 3 switch_time: 5 - id: BAGHYEK::SAKHEH station_a: SAKHEH station_b: BAGHYEK track_type: SINGLE running_time_forward: 38 running_time_reverse: 38 headway_same_direction: 3 switch_time: 5 در Production، این YAML جایگزین Infrastructure Master نیست؛ فقط Seed/Bootstrap است. 25. Access Mapping YAML mappings/access_train_movement.yml YAML fields: ID: target: source_record_id confidence: VERIFIED TrainNo: target: train_run.train_no confidence: VERIFIED TrainName: target: train_run.train_name confidence: VERIFIED StationName: target: train_station_call.station_id confidence: VERIFIED StationNumber: target: train_station_call.source_station_number confidence: VERIFIED Sequence: target: train_station_call.sequence confidence: VERIFIED time_in: target: train_station_call.time_in confidence: VERIFIED time_take: target: train_station_call.time_take confidence: VERIFIED time_out: target: train_station_call.time_out confidence: VERIFIED RequiredWait: target: train_station_call.required_wait confidence: PROVISIONAL Kilometerage: target: train_station_call.kilometerage confidence: HIGH MaxSpeed: target: train_station_call.max_speed confidence: PROVISIONAL Distance: target: train_station_call.source_distance confidence: VERIFIED sumDistancezz: target: train_station_call.source_sum_distance confidence: UNKNOWN seir: target: train_station_call.baseline_running_time_to_next confidence: HIGH نکته مهم: RequiredWait, MaxSpeed, sumDistancezz عمداً با confidence پایین‌تر باقی مانده‌اند و تا زمانی که معنای آن‌ها با داده بیشتر تثبیت نشده، نباید به‌عنوان حقیقت عملیاتی وارد Solver شوند. 26. Excel Mapping mappings/excel_kholase.yml YAML fields: ردیف: target: source_row_id نام قطار: target: train_service_name شماره قطار از مبدا: target: outbound_train_no ساعت حرکت از مبدا: target: outbound_departure_time روزهای حرکت از مبدا: target: outbound_operating_days ساعت ورود به مقصد: target: outbound_arrival_time شماره قطار از مقصد: target: return_train_no ساعت حرکت از مقصد: target: return_departure_time روزهای حرکت از مقصد: target: return_operating_days ساعت رسیدن به مبدا: target: return_arrival_time 27. Pipeline app/services/run_manager.py Python from dataclasses import dataclass from app.domain.infrastructure import PhysicalBlock from app.domain.route import build_directed_path from app.domain.train import TrainRun from app.quality.gate import quality_gate from app.scheduling.solver import ScheduleSolver from app.services.capacity import ( CapacityService, ) @dataclass class RunResult: capacity: int proof_valid: bool explanation: str quality_issues: int class RunManager: def __init__( self, solver: ScheduleSolver, ): self.solver = solver def run( self, trains: list[TrainRun], infrastructure: dict[str, PhysicalBlock], ) -> RunResult: issues = quality_gate( trains ) paths = [ build_directed_path( train, infrastructure, ) for train in trains ] service = CapacityService( self.solver ) result = service.execute( trains, paths, infrastructure, ) return RunResult( capacity=result.capacity, proof_valid=result.proof_valid, explanation=result.explanation, quality_issues=len(issues), ) 28. API app/api/deps.py Python from functools import lru_cache from app.config.settings import get_settings from app.scheduling.solver import ScheduleSolver from app.services.run_manager import RunManager @lru_cache def get_run_manager() -> RunManager: settings = get_settings() solver = ScheduleSolver( time_limit_seconds=( settings.solver_time_limit_seconds ), workers=settings.solver_workers, seed=settings.solver_seed, ) return RunManager(solver) app/api/routes/health.py Python from fastapi import APIRouter router = APIRouter() @router.get("/health") def health(): return { "status": "ok", "service": "railway-capacity-engine", "version": "1.6.0", } 29. Capacity API app/api/routes/capacity.py Python from fastapi import APIRouter from app.api.deps import get_run_manager from app.domain.common import Direction from app.domain.infrastructure import PhysicalBlock from app.domain.train import ( TrainRun, TrainStationCall, ) router = APIRouter( prefix="/capacity", tags=["capacity"], ) @router.post("/demo") def demo_capacity(): infrastructure = { "GAR::SAKHEH": PhysicalBlock( id="GAR::SAKHEH", station_a="GAR", station_b="SAKHEH", track_type="SINGLE", running_time_forward=34, running_time_reverse=34, ) } trains = [] for i in range(10): trains.append( TrainRun( train_no=str(100 + i), train_name="GAR-SAKHEH", direction=Direction.FORWARD, origin_station="GAR", destination_station="SAKHEH", calls=[ TrainStationCall( sequence=1, station_id="GAR", ), TrainStationCall( sequence=2, station_id="SAKHEH", baseline_running_time_to_next=None, ), ], ) ) manager = get_run_manager() result = manager.run( trains, infrastructure, ) return { "capacity": result.capacity, "proof_valid": result.proof_valid, "quality_issues": result.quality_issues, "explanation": result.explanation, } 30. Main Application app/main.py Python from fastapi import FastAPI from app.api.routes.capacity import router as capacity_router from app.api.routes.health import router as health_router app = FastAPI( title="Railway Capacity Engine", version="1.6.0", ) app.include_router( health_router ) app.include_router( capacity_router ) 31. Alembic برای Production، migration باید قبل از Start برنامه اجرا شود؛ این دقیقاً با توصیه FastAPI برای Production و ساختار migration Alembic سازگار است. FastAPI +1 alembic/env.py Python from logging.config import fileConfig from alembic import context from sqlalchemy import engine_from_config from sqlalchemy import pool from app.config.settings import get_settings from app.db.base import Base from app.db.models.source import DataVersion from app.db.models.train import TrainRunRecord from app.db.models.run import CapacityRunRecord config = context.config if config.config_file_name is not None: fileConfig( config.config_file_name ) target_metadata = Base.metadata settings = get_settings() def run_migrations_offline(): url = settings.database_url context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={ "paramstyle": "named" }, ) with context.begin_transaction(): context.run_migrations() def run_migrations_online(): configuration = config.get_section( config.config_ini_section ) configuration[ "sqlalchemy.url" ] = settings.database_url connectable = engine_from_config( configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, ) with connectable.connect() as connection: context.configure( connection=connection, target_metadata=target_metadata, ) with context.begin_transaction(): context.run_migrations() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() 32. Initial Migration alembic/versions/0001_initial.py Python from alembic import op import sqlalchemy as sa revision = "0001_initial" down_revision = None branch_labels = None depends_on = None def upgrade(): op.create_table( "data_versions", sa.Column( "id", sa.Integer, primary_key=True, ), sa.Column( "source_name", sa.String(255), nullable=False, ), sa.Column( "source_type", sa.String(50), nullable=False, ), sa.Column( "source_hash", sa.String(128), nullable=False, ), sa.Column( "status", sa.String(50), nullable=False, ), sa.Column( "notes", sa.Text, ), sa.Column( "created_at", sa.DateTime, nullable=False, ), ) op.create_table( "train_runs", sa.Column( "id", sa.Integer, primary_key=True, ), sa.Column( "train_no", sa.String(100), nullable=False, ), sa.Column( "train_name", sa.String(255), ), sa.Column( "direction", sa.String(20), ), sa.Column( "origin_station", sa.String(255), ), sa.Column( "destination_station", sa.String(255), ), sa.Column( "source_id", sa.Integer, ), sa.Column( "sequence", sa.Integer, nullable=False, ), sa.Column( "station_name", sa.String(255), nullable=False, ), sa.Column( "station_number", sa.String(100), ), sa.Column( "time_in", sa.Integer, ), sa.Column( "time_take", sa.Integer, ), sa.Column( "time_out", sa.Integer, ), sa.Column( "required_wait", sa.Integer, ), sa.Column( "kilometerage", sa.Float, ), sa.Column( "max_speed", sa.Float, ), sa.Column( "source_distance", sa.Float, ), sa.Column( "derived_distance", sa.Float, ), sa.Column( "sum_distance_source", sa.Float, ), sa.Column( "baseline_running_time_to_next", sa.Integer, ), sa.Column( "seir_source", sa.Integer, ), ) op.create_index( "ix_train_runs_train_no", "train_runs", ["train_no"], ) op.create_table( "capacity_runs", sa.Column( "id", sa.Integer, primary_key=True, ), sa.Column( "run_id", sa.String(100), unique=True, nullable=False, ), sa.Column( "scenario_id", sa.String(100), nullable=False, ), sa.Column( "data_version", sa.String(100), nullable=False, ), sa.Column( "model_version", sa.String(100), nullable=False, ), sa.Column( "status", sa.String(50), nullable=False, ), sa.Column( "capacity", sa.Integer, ), sa.Column( "proof_valid", sa.Boolean, nullable=False, ), sa.Column( "result_json", sa.Text, ), sa.Column( "created_at", sa.DateTime, nullable=False, ), ) def downgrade(): op.drop_table( "capacity_runs" ) op.drop_index( "ix_train_runs_train_no", table_name="train_runs", ) op.drop_table( "train_runs" ) op.drop_table( "data_versions" ) Alembic برای همین الگوی versioned migration طراحی شده و migrationها را می‌توان به‌صورت offline SQL نیز تولید کرد؛ این موضوع برای محیط‌هایی که DBA کنترل DDL را در دست دارد مفید است. Alembic +1 33. تست Time tests/unit/test_time.py Python from app.domain.schedule import ( normalize_time_sequence, parse_hhmm, ) def test_parse_hhmm(): assert parse_hhmm("09:30") == 570 def test_midnight_rollover(): values = [ parse_hhmm("23:46"), parse_hhmm("00:36"), parse_hhmm("02:19"), ] result = normalize_time_sequence(values) assert result == [ 1426, 1476, 1579, ] 34. تست Direction tests/unit/test_direction.py Python from app.domain.common import Direction from app.mapping.train import infer_direction def test_forward(): assert ( infer_direction( [157, 200, 220, 674] ) == Direction.FORWARD ) def test_reverse(): assert ( infer_direction( [674, 662, 649, 157] ) == Direction.REVERSE ) 35. تست Capacity Proof tests/unit/test_capacity.py Python from app.capacity.search import CapacityProof def test_capacity_proof(): proof = CapacityProof( feasible_f=76, tested_f_plus_one=77, f_feasible=True, f_plus_one_feasible=False, proof_valid=True, ) assert proof.proof_valid is True 36. اجرای پروژه Bash python -m venv .venv Linux/macOS: Bash source .venv/bin/activate Windows: PowerShell .venv\Scripts\activate نصب: Bash pip install -e ".[test,access]" 37. Database Migration ابتدا: Bash alembic upgrade head سپس اجرای API: Bash uvicorn app.main:app --reload و: GET /health باید چیزی شبیه این برگرداند: JSON { "status": "ok", "service": "railway-capacity-engine", "version": "1.6.0" } 38. تست‌ها Bash pytest 39. اتصال به Access واقعی شما برای فایل: aaa.accdb ابتدا: Bash python scripts/inspect_access.py aaa.accdb هدف این command این است که قبل از ingestion بگوید: Tables ------ ... Columns ------- ID kol TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir بعد: Bash python scripts/ingest.py \ --access aaa.accdb \ --excel REPORTKholase_31-06-1405_02-19-35.xlsx و سپس: Bash python scripts/run_capacity.py \ --access aaa.accdb \ --excel REPORTKholase_31-06-1405_02-19-35.xlsx 40. نکته بسیار مهم درباره داده واقعی شما در V1.6 این mapping را عمداً این‌گونه نگه می‌داریم: Access │ ├── TrainNo ├── TrainName ├── StationName ├── StationNumber ├── Sequence ├── time_in ├── time_take ├── time_out ├── RequiredWait ├── Kilometerage ├── MaxSpeed ├── Distance ├── sumDistancezz └── seir │ ▼ Canonical │ ├── TrainRun │ └── TrainStationCall │ ├── time_in ├── time_take ├── time_out ├── required_wait ├── kilometerage ├── source_distance ├── derived_distance └── baseline_running_time_to_next و مخصوصاً: seir ↓ baseline_running_time_to_next نه: seir → Station.running_time چون seir از شواهد داده‌ای که بررسی کردیم، زمان حرکت از ایستگاه فعلی تا ایستگاه بعدی است. همچنین: Kilometerage ↓ chainage / position ↓ abs(KM[i+1] - KM[i]) ↓ derived_distance ولی: Distance به‌عنوان source field دست‌نخورده حفظ می‌شود و با مقدار derived جایگزین نمی‌شود. 41. معماری اجرایی V1.6 اکنون زنجیره اصلی به این شکل است: ┌──────────────────────┐ │ aaa.accdb │ │ REPORT...xlsx │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Source Adapters │ │ Access / Excel │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Raw / Staging │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Mapping Registry │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Quality Gate │ └──────────┬───────────┘ │ ▼ ┌──────────────────────────────┐ │ Canonical Railway Domain │ │ │ │ TrainRun │ │ TrainStationCall │ │ Infrastructure │ │ PhysicalBlock │ │ Route │ └──────────────┬───────────────┘ │ ▼ ┌──────────────────────┐ │ Directed Path │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ CP-SAT Scheduler │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Generated Schedule │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Independent │ │ Validator │ └──────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ Capacity Search │ │ │ │ F feasible │ │ F+1 infeasible │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Capacity Proof │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ Bottleneck │ │ Explanation │ └─────────────────────┘ 42. اما هنوز یک نکته مهم باقی است این V1.6 را باید هسته اجرایی واقعی در نظر گرفت، اما هنوز نباید آن را Production Capacity Engine نهایی بدانیم. سه لایه مهم باید در V1.7 تکمیل شوند: V1.7 — Operationally Correct Scheduler اضافه کردن: Station Track Assignment Junction Conflict Matrix Single Track Opposing Direction Same Direction Headway Switch Time Crossing Station Station Length Operational Windows Brake Test Dwell Earliest Departure Latest Arrival Baseline Deviation یعنی: Schedule(F) دیگر فقط بر اساس NoOverlap(block) نباشد. V1.8 — Rolling Stock Integrated Capacity سپس: Train Formation ↓ Wagon Requirement ↓ Wagon Cycle ↓ Empty Wagon Repositioning ↓ Locomotive Cycle ↓ Maintenance ↓ Fueling ↓ Operational Availability ↓ Train Formation Feasibility ↓ Schedule و تعریف ظرفیت تبدیل شود به: C r ​ =max ⎩ ⎨ ⎧ ​ F: ∃Formation, ∃WagonCycle, ∃LocomotiveCycle, ∃TrainPath, ∃Schedule ​ ⎭ ⎬ ⎫ ​ V1.9 — Multi-OD / Network Capacity و بعد: Marketplace ↓ Demand ↓ OD ↓ Freight Flow ↓ Wagon Requirement ↓ Route Choice ↓ Train Formation ↓ Shared Network ↓ Station/Junction/Block ↓ Wagon Pool ↓ Locomotive Pool ↓ CP-SAT ↓ Network Capacity و در نهایت: C n ​ =max{ r ∑ ​ Q r ​ F r ​ :FeasibleNetworkSchedule} مهم‌ترین نتیجه V1.6 از این نسخه به بعد، دیگر نباید ظرفیت را به صورت: تعداد قطار موجود در فایل تعریف کنیم. بلکه: Candidate Demand ↓ Candidate Train Services ↓ Feasible Schedule ↓ Independent Validation ↓ F ↓ F+1 ↓ Capacity Proof بنابراین خروجی معتبر Engine باید چیزی در این جنس باشد: JSON { "run_id": "RUN-2026-001", "model_version": "1.6.0", "data_version": "DV-001", "capacity": { "F": 76, "F_plus_1": 77, "F_feasible": true, "F_plus_1_feasible": false, "proof_valid": true }, "schedule": { "validated": true }, "binding_constraints": [ { "resource": "BLOCK_X", "type": "BLOCK_OCCUPANCY" } ] } اما عددهای 76 و 77 در این JSON فقط نمونه ساختار خروجی هستند و نتیجه واقعی شبکه شما محسوب نمی‌شوند. برای عدد واقعی باید همین Engine روی aaa.accdb، Excel، Infrastructure Master و قواعد عملیاتی واقعی اجرا شود. در گام بعدی، منطقی‌ترین توسعه V1.7 است: یعنی همین کد را از Scheduler ساده به Operationally Correct Railway Scheduler ارتقا دهیم و Single Track + Opposing Direction + Headway + Switch Time + Station Track + Junction + Crossing + Operational Window را داخل CP-SAT و Validator واقعی وارد کنیم؛ این مرحله همان چیزی است که ظرفیت محاسبه‌شده را از یک MVP محاسباتی به ظرفیت عملیاتی قابل اتکا نزدیک می‌کند. --- ## ادامه 37 — پرسش کاربر (`message-id:0a1243ad-24a7-4796-a323-523ea69af652`) بله لطفا --- ## ادامه 37 — پاسخ دستیار (`message-id:5b37f9a0-cf77-435b-88d7-88846e89a039`) حتماً. حالا وارد V1.7 — Operationally Correct Scheduler می‌شویم. این نسخه یک تغییر معماری مهم دارد: دیگر NoOverlap ساده روی Blockها را به‌عنوان مدل کامل ظرفیت قبول نمی‌کنیم؛ بلکه زمان، جهت، Track، Headway، Switch/Clearing، Junction و Operational Window را به‌صورت Resource/Constraint مستقل مدل می‌کنیم. این رویکرد با ماهیت Scheduling در CP-SAT هم‌خوان است: مسئله شامل Taskهای ترتیبی، Resourceهای مشترک و Precedence است و OR-Tools نیز برای چنین مسائل زمان‌بندی از CP-SAT و Interval/NoOverlap استفاده می‌کند. Google for Developers +1 V1.7 — Operationally Correct Railway Scheduler 1. هدف نسخه زنجیره اکنون باید این باشد: TrainRun ↓ DirectedTrainPath ↓ Station Call ↓ Block Movement ↓ Station Resource ↓ Physical Block Resource ↓ Junction Resource ↓ Operational Window ↓ Headway / Switch / Clearing ↓ CP-SAT ↓ Generated Timetable ↓ Independent Validator ↓ Capacity Search و مهم‌تر: Physical Block ≠ Directed Movement ≠ Station Track ≠ Junction Movement 2. اصلاح اساسی مدل Single / Double Track در V1.6 اگر دو قطار روی یک physical_block_id بودند، صرفاً NoOverlap اعمال می‌شد. در V1.7 این رفتار را داریم: Single Track هر دو جهت از یک Resource فیزیکی استفاده می‌کنند: A ───────────── B SINGLE بنابراین: Train 1 A → B Train 2 B → A باید با یک Constraint ترتیبی سازگار شوند. اگر: Train 1 exits block at t1 و: Train 2 enters block at t2 آنگاه: t 2 ​ ≥t 1 ​ +T switch ​ یا برعکس: t 1 ​ ≥t 2 ​ +T switch ​ اما برای دو قطار هم‌جهت: t 2 ​ ≥t 1 ​ +H same ​ یا برعکس. بنابراین: H ij ​ ={ H same ​ , max(H same ​ ,T switch ​ ), ​ direction i ​ =direction j ​ direction i ​  =direction j ​ ​ این همان اصلاح مهمی است که قبلاً برای مدل شما مشخص کرده بودیم. 3. Domain Model جدید app/domain/infrastructure.py Python from dataclasses import dataclass, field from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" @dataclass(frozen=True) class StationTrack: id: str station_id: str usable_length_m: float bidirectional: bool = True electrified: bool = False @dataclass(frozen=True) class Station: id: str name: str usable_length_m: float tracks: tuple[StationTrack, ...] = () @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int = 3 switch_time: int = 5 clearing_time: int = 0 @staticmethod def make_id( station_a: str, station_b: str, ) -> str: a, b = sorted( [station_a, station_b] ) return f"{a}::{b}" def running_time( self, direction: Direction, ) -> int: if direction == Direction.FORWARD: return self.running_time_forward return self.running_time_reverse 4. Junction Model Junction را نباید به‌صورت blanket NoOverlap روی همه حرکت‌ها مدل کنیم. مثلاً: B | | A -----------J----------- C | | D ممکن است: A → C و: B → D با هم Conflict داشته باشند، ولی: A → C و: D → B لزوماً همان Conflict را نداشته باشند. پس: Python from dataclasses import dataclass @dataclass(frozen=True) class JunctionMovement: id: str junction_id: str from_station: str to_station: str @dataclass(frozen=True) class JunctionConflict: movement_a: str movement_b: str separation_time: int = 1 و Master Data: YAML junctions: - id: J001 movements: - id: M_A_C from_station: A to_station: C - id: M_B_D from_station: B to_station: D conflicts: - movement_a: M_A_C movement_b: M_B_D separation_time: 3 5. Operational Window این مفهوم باید عمومی باشد تا بعداً موارد مختلف را پوشش دهد: Brake Test Fueling Prayer Maintenance Station closure Track possession Engineering work Crew availability Terminal availability مدل: Python from dataclasses import dataclass @dataclass(frozen=True) class OperationalWindow: id: str resource_id: str start: int end: int kind: str blocking: bool = True مثلاً: YAML windows: - id: W001 resource_id: STATION_ARAK_TRACK_3 start: 720 end: 780 kind: MAINTENANCE blocking: true 6. Train Operational Requirements به TrainRun چند پارامتر واقعی اضافه می‌کنیم: Python from dataclasses import dataclass @dataclass(frozen=True) class TrainOperationalProfile: train_length_m: int train_weight_t: int earliest_departure: int = 0 latest_arrival: int | None = None station_dwell_min: int = 0 brake_test_minutes: int = 0 formation_minutes: int = 0 clearance_minutes: int = 0 بنابراین ظرفیت فقط تابع Track نیست: C r ​ =f(Infrastructure,TrainType,OperationalRules,Station,Junction,Schedule) 7. Block Movement مدل زمان‌فضایی دقیق‌تر: Python from dataclasses import dataclass @dataclass class BlockMovement: train_no: str sequence: int physical_block_id: str direction: str entry: int exit: int clear: int station_from: str station_to: str سه زمان مهم داریم: entry ↓ occupancy ↓ exit ↓ clear یعنی: t exit =t entry +T run ​ و: t clear =t exit +T clear ​ این موضوع مهم است چون آزادشدن فیزیکی Resource لزوماً دقیقاً برابر با رسیدن قطار به Station بعدی نیست. 8. Solver Architecture Solver جدید را به Constraint Builderها تقسیم می‌کنیم: SchedulingModel │ ├── PrecedenceConstraintBuilder │ ├── BlockConstraintBuilder │ ├── HeadwayConstraintBuilder │ ├── StationTrackConstraintBuilder │ ├── JunctionConstraintBuilder │ ├── WindowConstraintBuilder │ ├── TimeWindowConstraintBuilder │ └── ObjectiveBuilder این کار برای Production بسیار مهم است؛ چون در آینده نمی‌خواهیم یک فایل 3000 خطی CP-SAT داشته باشیم. 9. Scheduling Variables برای هر Train و Block: Python entry[i,b] exit[i,b] clear[i,b] و برای Station: Python arrival[i,s] departure[i,s] و برای Track: Python track_assignment[i,s,k] که: track_assignment i,s,k ​ ∈{0,1} و: k ∑ ​ track_assignment i,s,k ​ =1 اگر Track Assignment برای آن Station لازم باشد. 10. Precedence Constraints اگر Train به‌ترتیب: A → B → C → D حرکت کند: Block A-B Block B-C Block C-D داریم: entry i,b k+1 ​ ​ ≥clear i,b k ​ ​ در کد: Python def add_train_precedence( model, block_vars, ): for train_id, movements in block_vars.items(): for current, nxt in zip( movements, movements[1:], ): model.add( nxt.entry >= current.clear ) این ساختار همان الگوی Precedence در Scheduling است که در مثال Job Shop رسمی OR-Tools نیز استفاده می‌شود. Google for Developers 11. Running Time برای هر Block: Python model.add( movement.exit == movement.entry + running_time ) و: Python model.add( movement.clear == movement.exit + clearing_time ) بنابراین: T occupancy ​ =T run ​ +T clear ​ 12. Single Track Constraint این قسمت قلب V1.7 است. برای هر دو Movement که روی یک Physical Block هستند: Python from ortools.sat.python import cp_model def add_single_track_pair_constraint( model: cp_model.CpModel, a, b, same_direction_headway: int, switch_time: int, ): a_before_b = model.new_bool_var( f"{a.name}_before_{b.name}" ) b_before_a = model.new_bool_var( f"{b.name}_before_{a.name}" ) model.add( a_before_b + b_before_a == 1 ) # A before B model.add( b.entry >= a.clear + same_direction_headway ).only_enforce_if( a_before_b ) # B before A model.add( a.entry >= b.clear + same_direction_headway ).only_enforce_if( b_before_a ) اما برای Opposite Direction: Python def add_opposite_direction_constraint( model, a, b, switch_time: int, ): a_before_b = model.new_bool_var( f"{a.name}_opp_before_{b.name}" ) b_before_a = model.new_bool_var( f"{b.name}_opp_before_{a.name}" ) model.add( a_before_b + b_before_a == 1 ) model.add( b.entry >= a.clear + switch_time ).only_enforce_if( a_before_b ) model.add( a.entry >= b.clear + switch_time ).only_enforce_if( b_before_a ) اما در Production نسخه نهایی را کمی بهتر می‌کنیم و یک تابع عمومی داریم: Python def required_separation( same_direction: bool, headway_same_direction: int, switch_time: int, ) -> int: if same_direction: return headway_same_direction return max( headway_same_direction, switch_time, ) 13. نکته مهم درباره Single Track این constraint نباید صرفاً روی DirectedBlock باشد. باید روی: PhysicalBlock باشد. مثلاً: GAR → SAKHEH و: SAKHEH → GAR هر دو: PhysicalBlock = GAR::SAKHEH دارند. این دقیقاً همان چیزی است که جلوی اشتباه زیر را می‌گیرد: GAR::SAKHEH::FORWARD GAR::SAKHEH::REVERSE به‌عنوان دو Resource مستقل. برای Single Track این اشتباه است. 14. Double Track در Double Track: A ================= B A ================= B حرکت‌های مخالف می‌توانند Resource متفاوت داشته باشند. بنابراین: Python def movement_resource( block, direction, ): if block.track_type == TrackType.SINGLE: return block.id return f"{block.id}:{direction.value}" پس: Single: A::B Double: A::B:FORWARD A::B:REVERSE 15. Station Track Assignment برای Station: Station ├── Track 1 ├── Track 2 ├── Track 3 └── Track 4 برای هر Train یک Track انتخاب می‌کنیم: Python def add_station_track_assignment( model, train, station, track_vars, ): tracks = station.tracks if not tracks: return model.add( sum(track_vars) == 1 ) و برای هر Track: Python intervals_by_track = {} for track in station.tracks: intervals_by_track[ track.id ] = [] for train in trains: interval = ... intervals_by_track[ track.id ].append(interval) for intervals in intervals_by_track.values(): model.add_no_overlap( intervals ) این همان کاربرد Resourceهای زمان‌بندی است؛ OR-Tools نیز NoOverlap را برای Taskهایی که نمی‌توانند همزمان روی یک Resource باشند استفاده می‌کند. Google for Developers 16. Station Length Constraint اگر: Train Length = 720m Station Track = 700m نباید Assignment انجام شود. Python def compatible_station_track( train_length_m: int, track_length_m: float, ) -> bool: return ( train_length_m <= track_length_m ) در Solver: Python for track in station.tracks: if not compatible_station_track( train.profile.train_length_m, track.usable_length_m, ): model.add( track_assignment[ train.id, track.id ] == 0 ) 17. Junction Conflict فرض کنیم: Movement A Movement B Conflict دارند. پس: Python def add_junction_conflict( model, a, b, separation, ): a_before_b = model.new_bool_var( "junction_a_before_b" ) b_before_a = model.new_bool_var( "junction_b_before_a" ) model.add( a_before_b + b_before_a == 1 ) model.add( b.entry >= a.clear + separation ).only_enforce_if( a_before_b ) model.add( a.entry >= b.clear + separation ).only_enforce_if( b_before_a ) اما این Constraint فقط برای pairهایی که در JunctionConflict ثبت شده‌اند اعمال می‌شود. 18. Operational Window مثلاً: Track 3: 12:00 ───────── 13:00 CLOSED برای یک Movement: entry exit نباید در آن Window قرار بگیرد. اگر: W=[720,780] داریم: exit≤720 یا: entry≥780 یعنی: Python model.add( movement.exit <= window.start ).only_enforce_if( before_window ) model.add( movement.entry >= window.end ).only_enforce_if( after_window ) model.add( before_window + after_window == 1 ) 19. Earliest Departure برای Train: Earliest Departure = 06:00 داریم: departure i ​ ≥360 در کد: Python model.add( first_entry >= train.profile.earliest_departure ) 20. Latest Arrival اگر: Latest Arrival = 22:00 آنگاه: arrival destination ​ ≤1320 Python if train.profile.latest_arrival is not None: model.add( final_exit <= train.profile.latest_arrival ) 21. Dwell در Station: departure i ​ ≥arrival i ​ +T dwell,i ​ و اگر: RequiredWait = 60 Actual baseline dwell = 83 در سناریوی Baseline: 83 و در سناریوی Minimum Operational: 60 بنابراین دو مفهوم جدا داریم: Baseline Dwell Minimum Required Dwell 22. مدل Station Call Python @dataclass class StationCallTiming: arrival: object departure: object minimum_dwell: int baseline_arrival: int | None = None baseline_departure: int | None = None Constraint: Python model.add( departure >= arrival + minimum_dwell ) 23. Baseline Objective در حالت Baseline Reconstruction، هدف: Find feasible schedule closest to actual schedule مثلاً: min i ∑ ​ ∣A i ​ −A i baseline ​ ∣+∣D i ​ −D i baseline ​ ∣ برای CP-SAT می‌توان deviation را linearize کرد: Python arrival_deviation = model.new_int_var( 0, horizon, "arrival_deviation", ) model.add( arrival_deviation >= arrival - baseline_arrival ) model.add( arrival_deviation >= baseline_arrival - arrival ) و سپس: Python model.minimize( sum(deviations) ) CP-SAT برای چنین مدل‌های Integer/Constraint مناسب است و همه ضرایب/متغیرهای مدل باید در حوزه integer باشند. Google for Developers 24. Capacity Mode اما در Capacity Search نمی‌خواهیم الزاماً Baseline را تقلید کنیم. دو Mode: Python class SchedulingMode(str, Enum): BASELINE_RECONSTRUCTION = ( "BASELINE_RECONSTRUCTION" ) OPERATIONAL_CAPACITY = ( "OPERATIONAL_CAPACITY" ) SCENARIO = "SCENARIO" Baseline Minimize schedule deviation Capacity Maximize feasible train count Scenario مثلاً: Minimize delay Subject to: F >= 40 25. Solver Objective برای Capacity: Python model.maximize( sum( train_selected for train_selected in selected_trains ) ) در صورتی که Capacity Search بیرونی نیز استفاده شود، می‌توان مدل را به‌صورت Feasibility Mode اجرا کرد. این دو روش را باید از هم جدا نگه داریم: Outer Binary Search + Inner Feasibility Solver یا: Single CP-SAT Optimization برای MVP: Outer Search ساده‌تر و قابل Proof است. 26. Operational Capacity Proof تعریف نهایی: F feasible AND F+1 infeasible ولی یک نکته بسیار مهم اضافه می‌کنیم: INFEASIBLE فقط وقتی Proof است که Solver واقعاً آن را اثبات کرده باشد. اگر CP-SAT به Time Limit برسد و: UNKNOWN برگرداند، نباید بنویسیم: F+1 = infeasible طبق مستندات CP-SAT، INFEASIBLE یعنی مسئله اثباتاً infeasible شده، در حالی که UNKNOWN می‌تواند به علت محدودیت زمان/منابع رخ دهد. Google for Developers +1 پس: Python class SolveStatus(str, Enum): OPTIMAL = "OPTIMAL" FEASIBLE = "FEASIBLE" INFEASIBLE = "INFEASIBLE" UNKNOWN = "UNKNOWN" MODEL_INVALID = "MODEL_INVALID" 27. Capacity Proof جدید Python @dataclass class CapacityProof: F: int F_plus_one: int F_status: str F_plus_one_status: str F_validated: bool F_plus_one_proven_infeasible: bool proof_valid: bool و: Python proof_valid = ( F_validated and F_plus_one_status == "INFEASIBLE" ) بنابراین: UNKNOWN دیگر مساوی: INFEASIBLE نیست. این برای Production بسیار مهم است. 28. Validator جدید Validator دیگر فقط Block Overlap را بررسی نمی‌کند. ساختار: IndependentScheduleValidator │ ├── validate_train_precedence() ├── validate_running_time() ├── validate_dwell() ├── validate_single_track() ├── validate_headway() ├── validate_switch_time() ├── validate_station_track() ├── validate_station_length() ├── validate_junction() ├── validate_operational_window() ├── validate_earliest_departure() ├── validate_latest_arrival() └── validate_terminal_conditions() 29. Conflict Types Python class ConflictType(str, Enum): SAME_DIRECTION_HEADWAY = ( "SAME_DIRECTION_HEADWAY" ) OPPOSING_DIRECTION = ( "OPPOSING_DIRECTION" ) SWITCH_TIME = ( "SWITCH_TIME" ) STATION_TRACK = ( "STATION_TRACK" ) STATION_LENGTH = ( "STATION_LENGTH" ) JUNCTION = ( "JUNCTION" ) OPERATIONAL_WINDOW = ( "OPERATIONAL_WINDOW" ) PRECEDENCE = ( "PRECEDENCE" ) DWELL = ( "DWELL" ) این به Explainability کمک زیادی می‌کند. 30. Conflict Record Python @dataclass class Conflict: conflict_id: str conflict_type: ConflictType train_a: str train_b: str | None resource_id: str time_a: int time_b: int required_separation: int actual_separation: int severity: str explanation: str مثلاً: JSON { "conflict_type": "OPPOSING_DIRECTION", "train_a": "100", "train_b": "101", "resource_id": "GAR::ANDIMESHK", "required_separation": 5, "actual_separation": 2, "severity": "ERROR", "explanation": "Opposing movements violate switch separation." } 31. Bottleneck Engine از اینجا به بعد Bottleneck فقط: بیشترین utilization نیست. سه سطح: Level 1 — Binding Constraint باعث شده F+1 infeasible شود. Level 2 — Near Binding Constraint دارای Slack بسیار کم است. Level 3 — Structural Resource در بسیاری از Solutionها نزدیک ظرفیت است. مدل: Python @dataclass class Bottleneck: resource_id: str constraint_type: str utilization: float slack_minutes: int binding: bool marginal_capacity_impact: int | None 32. Slack برای هر Constraint: Slack=Actual−Required مثلاً: Required switch time = 5 Actual = 5 Slack = 0 پس: Binding اگر: Actual = 12 Required = 5 داریم: Slack = 7 33. Capacity Explanation خروجی باید شبیه این باشد: Route Capacity = 76 trains/day Proof: F = 76 Status = FEASIBLE Validation = PASS F+1 = 77 Status = INFEASIBLE Proof = VALID Primary Binding Constraint: Physical Block: GAR::ANDIMESHK Type: OPPOSING_DIRECTION / SINGLE_TRACK Supporting Constraint: Station: XXXXX Track Utilization: 94% Hidden Capacity: 2 trains/day under alternative operating regime Reason: Opposing-direction movements require switch separation and consume the single-track block resource. 34. Hidden Capacity این مفهوم بسیار مهم است. اگر: Regime A Alternating direction Capacity = 70 ولی: Regime B Batch operation Capacity = 76 نباید فقط بگوییم: Capacity = 76 بلکه: Baseline Regime Capacity = 70 Alternative Regime Capacity = 76 Recoverable / Hidden Capacity = 6 35. Operating Regime مدل: Python class OperatingRegime(str, Enum): STRICT_ALTERNATING = ( "STRICT_ALTERNATING" ) DIRECTIONAL_BATCH = ( "DIRECTIONAL_BATCH" ) MIXED = "MIXED" و Batch: Python @dataclass class OperationalBatch: id: str direction: Direction train_ids: list[str] start: int end: int switching_time: int 36. Batch Constraint اگر: Batch 1 = Forward Train 1 Train 2 Train 3 و سپس Reverse: Batch 2 = Reverse آنگاه: Start B2 ​ ≥End B1 ​ +T switch ​ این Constraint می‌تواند به‌صورت Optional/Decision Variable نیز مدل شود تا Solver خودش Regime را انتخاب کند. 37. تصمیم مهم معماری در V1.7 پیشنهاد من این است که ابتدا Regime را Parameter کنیم: YAML scenario: operating_regime: MIXED و در V1.8 اجازه دهیم Solver خودش Batch/Regime را انتخاب کند. دلیل: اگر هم‌زمان این موارد را وارد کنیم: Train Count Train Timing Track Assignment Crossing Junction Batch Formation Regime Selection فضای Search به‌سرعت بزرگ می‌شود. CP-SAT برای Schedulingهای پیچیده مناسب است، اما مدل باید مرحله‌ای و قابل کنترل ساخته شود. Google for Developers 38. Configuration V1.7 YAML solver: time_limit_seconds: 120 workers: 1 seed: 1 mode: OPERATIONAL_CAPACITY rules: same_direction_headway: 3 opposing_switch_time: 5 clearing_time: 1 minimum_station_dwell: 5 capacity: proof_requires_f_plus_one: true unknown_is_infeasible: false operating_regime: mode: MIXED 39. Test بسیار مهم Single Track Python def test_opposing_direction_conflict(): block = PhysicalBlock( id="A::B", station_a="A", station_b="B", track_type=TrackType.SINGLE, running_time_forward=30, running_time_reverse=30, headway_same_direction=3, switch_time=5, ) assert ( required_separation( same_direction=False, headway_same_direction=3, switch_time=5, ) == 5 ) 40. Test Same Direction Python def test_same_direction_headway(): assert ( required_separation( same_direction=True, headway_same_direction=3, switch_time=5, ) == 3 ) 41. Test Double Track Python def test_double_track_resources(): block = PhysicalBlock( id="A::B", station_a="A", station_b="B", track_type=TrackType.DOUBLE, running_time_forward=30, running_time_reverse=30, ) assert ( movement_resource( block, Direction.FORWARD, ) != movement_resource( block, Direction.REVERSE, ) ) 42. Test Station Length Python def test_train_does_not_fit_station(): assert ( compatible_station_track( train_length_m=720, track_length_m=700, ) is False ) 43. Test Operational Window Python def test_window_requires_before_or_after(): # Window: 12:00-13:00 window_start = 720 window_end = 780 # A movement cannot occupy this window. # It must finish before 720 # or start at/after 780. assert window_start < window_end 44. Test Capacity Proof Python def test_unknown_does_not_prove_capacity(): proof = CapacityProof( F=76, F_plus_one=77, F_status="FEASIBLE", F_plus_one_status="UNKNOWN", F_validated=True, F_plus_one_proven_infeasible=False, proof_valid=False, ) assert proof.proof_valid is False این تست باید حتماً وجود داشته باشد. 45. Architecture V1.7 MARKET / DEMAND │ ▼ Candidate Trains │ ▼ ┌───────────────────────┐ │ Train Formation │ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ Directed Train Path │ └───────────┬───────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ Block Resource Station Resource Junction │ │ │ └──────────────────┼──────────────────┘ │ ▼ Operational Windows │ ▼ Headway / Switch │ ▼ CP-SAT Solver │ ▼ Feasible Schedule │ ▼ Independent Validator │ ┌───────────┴───────────┐ │ │ PASS FAIL │ │ ▼ ▼ Capacity Search Conflict │ ▼ F / F+1 │ ▼ Capacity Proof │ ▼ Bottleneck / Explanation 46. تعریف جدید Capacity از V1.7 به بعد تعریف Route Capacity این است: C r ​ =max{F:∃S ​ به‌طوری که Schedule باید همزمان این موارد را ارضا کند: S⊨ ⎩ ⎨ ⎧ ​ Train Precedence Running Time Dwell Single Track Headway Switch Station Track Station Length Junction Operational Windows Earliest Departure Latest Arrival ​ یا به‌صورت فشرده: C r ​ =max{F:Schedule(F) is operationally feasible} ​ 47. نکته بسیار مهم درباره F+1 در نسخه‌های قبلی، این عبارت: F+1 infeasible گاهی می‌توانست به‌اشتباه از: solver timeout استنباط شود. در V1.7 این ممنوع است. فقط: CP-SAT = INFEASIBLE می‌تواند بخش F+1 Proof را تأیید کند. اگر: UNKNOWN باشد: Capacity Proof = INVALID / INCOMPLETE و UI باید نشان دهد: Capacity Candidate نه: Proven Capacity 48. UI تغییر می‌کند در Capacity Inspector: ┌──────────────────────────────────────────────┐ │ ROUTE CAPACITY │ ├──────────────────────────────────────────────┤ │ │ │ Proven Capacity │ │ │ │ 76 trains/day │ │ │ │ F = 76 ✓ FEASIBLE │ │ F+1 = 77 ✕ INFEASIBLE │ │ │ │ PROOF VALID │ │ │ ├──────────────────────────────────────────────┤ │ Binding Constraint │ │ │ │ GAR — ANDIMESHK │ │ SINGLE TRACK │ │ Opposing Direction Separation │ │ │ ├──────────────────────────────────────────────┤ │ Hidden Capacity │ │ │ │ Alternative operating regime: +6 trains/day │ │ │ └──────────────────────────────────────────────┘ اگر UNKNOWN باشد: ┌──────────────────────────────────────────────┐ │ CAPACITY NOT PROVEN │ │ │ │ F = 76 ✓ FEASIBLE │ │ F+1 = 77 ? UNKNOWN │ │ │ │ Solver time limit reached. │ │ No infeasibility proof available. │ └──────────────────────────────────────────────┘ این تفکیک برای سیستم شما حیاتی است. 49. V1.7 Definition of Done نسخه V1.7 زمانی کامل است که: [✓] Single Track physical resource [✓] Double Track directional resource [✓] Same-direction headway [✓] Opposing-direction separation [✓] Switch time [✓] Clearing time [✓] Station Track assignment [✓] Station length [✓] Junction movement [✓] Junction conflict matrix [✓] Operational Window [✓] Earliest Departure [✓] Latest Arrival [✓] Dwell / Required Wait [✓] Baseline deviation [✓] Independent Validator [✓] Conflict classification [✓] Slack calculation [✓] Binding constraint [✓] F/F+1 proof [✓] UNKNOWN ≠ INFEASIBLE [✓] Deterministic seed [✓] Scenario-based operating regime جایگاه V1.7 در Roadmap حالا معماری عملاً به این نقطه رسیده است: V1.0 Real Data Vertical Slice ↓ V1.1 Cohesive Executable Package ↓ V1.4 Network / Multi-OD ↓ V1.5 Production Data Platform ↓ V1.6 Executable MVP ↓ ★ V1.7 Operationally Correct Scheduler ↓ V1.8 Wagon + Locomotive + Formation Integrated ↓ V1.9 Network Capacity Optimization ↓ V2.0 Railway Capacity Planning Workbench نقطه کلیدی V1.7 این است که از اینجا به بعد عدد Capacity دیگر صرفاً یک خروجی Solver نیست؛ باید یک Schedule معتبر و مستقل از Solver پشت آن باشد. خود CP-SAT نیز بین FEASIBLE، OPTIMAL، INFEASIBLE و UNKNOWN تفاوت رسمی قائل می‌شود، بنابراین این Statusها باید عیناً وارد مدل Capacity Proof شوند. Google for Developers گام بعدی منطقی V1.8 است: اتصال همین Scheduler عملیاتی به Wagon Cycle + Empty Wagon Repositioning + Locomotive Cycle + Train Formation؛ یعنی جایی که Engine برای اولین بار می‌تواند بگوید «این مسیر از نظر خط ظرفیت دارد، اما به دلیل کمبود واگن/لوکوموتیو فقط بخشی از آن ظرفیت واقعاً قابل عرضه است». --- ## ادامه 38 — پرسش کاربر (`message-id:f3c50757-dc57-4b13-a1da-281459cefb61`) بله لطفا --- ## ادامه 38 — پاسخ دستیار (`message-id:f25038bc-a1cc-41de-aad9-da7c1a72752d`) حتماً. بریم سراغ V1.8 — Integrated Rolling Stock Capacity؛ این نسخه یک جهش مهم است، چون از اینجا به بعد موتور دیگر فقط نمی‌گوید «خط چند قطار را تحمل می‌کند»، بلکه بررسی می‌کند آیا برای آن قطارها واقعاً واگن، لکوموتیو و Formation قابل اجرا وجود دارد یا نه. این طراحی را روی V1.7 سوار می‌کنیم و Scheduler عملیاتی آن را حفظ می‌کنیم. استفاده از CP-SAT برای این بخش هم با ماهیت مسئله سازگار است: OR-Tools برای scheduling با precedence و resource constraints طراحی شده و NoOverlap/intervalها برای منابع زمانی مناسب‌اند. Google for Developers +1 V1.8 — Wagon + Locomotive + Formation Integrated Capacity 1. جایگاه V1.8 در معماری تا V1.7 زنجیره اصلی ما این بود: Infrastructure ↓ Train Run ↓ Directed Path ↓ Block / Station / Junction ↓ Operational Scheduler ↓ Feasible Timetable ↓ Capacity در V1.8 زنجیره کامل‌تر می‌شود: Market Demand ↓ Freight Flow ↓ Wagon Requirement ↓ Train Formation ↓ Wagon Assignment ↓ Locomotive Assignment ↓ Wagon Cycle ↓ Locomotive Cycle ↓ Train Run ↓ Directed Path ↓ V1.7 Operational Scheduler ↓ Integrated Feasibility ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck / Explanation یعنی ظرفیت نهایی دیگر صرفاً Infrastructure Capacity نیست. 2. چهار مفهوم ظرفیت از این نسخه باید چهار مفهوم را کاملاً جدا کنیم. 2.1 Infrastructure Capacity ظرفیتی که از زیرساخت و قواعد حرکت ناشی می‌شود: C infra ​ مثلاً: Line Block Station Junction Headway Switching Operational Window 2.2 Operational Capacity ظرفیتی که بعد از زمان‌بندی واقعی به دست می‌آید: C op ​ =maxF به‌طوری که: Schedule(F)=Feasible 2.3 Rolling Stock Constrained Capacity حالا واگن و لکوموتیو را هم وارد می‌کنیم: C rs ​ =maxF به شرط وجود همزمان: Formation WagonCycle LocomotiveCycle Schedule و همه قیود دیگر. 2.4 Allocated / Served Capacity در نهایت ممکن است ظرفیت عملیاتی وجود داشته باشد ولی بازار تقاضای کافی نداشته باشد: D market ​ FormationFeasibility: reasons = [] if formation.total_length_m > route.max_train_length_m: reasons.append("TRAIN_LENGTH_EXCEEDED") if formation.gross_weight_t > route.max_train_weight_t: reasons.append("TRAIN_WEIGHT_EXCEEDED") if not locomotives: reasons.append("NO_TRACTION_AVAILABLE") return FormationFeasibility( feasible=not reasons, reasons=tuple(reasons), gross_weight_t=formation.gross_weight_t, total_length_m=formation.total_length_m, ) این نسخه اولیه است؛ در نسخه production، traction model باید route-profile-based باشد. 16. Locomotive Model Python @dataclass(frozen=True) class LocomotiveType: id: str code: str name: str traction_effort_kn: float power_kw: float length_m: float weight_t: float compatible_train_types: tuple[str, ...] = () 17. Locomotive Python @dataclass(frozen=True) class Locomotive: id: str locomotive_type_id: str home_depot_id: str | None = None available_from: int = 0 available_until: int | None = None maintenance_required: bool = False 18. Locomotive Assignment Python @dataclass(frozen=True) class LocomotiveAssignment: train_run_id: str locomotive_ids: tuple[str, ...] departure_station_id: str arrival_station_id: str start_time: int end_time: int 19. Locomotive Cycle چرخه: Loco ↓ Train A ↓ Destination ↓ Turnback ↓ Train B / Return ↓ Fueling / Maintenance ↓ Available مدل: Python @dataclass(frozen=True) class LocomotiveCycle: id: str locomotive_id: str first_run_id: str next_run_id: str | None turnback_minutes: int fueling_minutes: int = 0 maintenance_minutes: int = 0 قید اصلی: Start next ​ ≥End current ​ +Turnback+Fueling+Maintenance 20. Operational Availability این را با Operational Window نسخه V1.7 یکپارچه می‌کنیم. Python @dataclass(frozen=True) class OperationalAvailability: resource_id: str start: int end: int reason: str مثلاً: Loco 801 00:00–04:00 Available 04:00–05:00 Fueling 05:00–16:00 Available 16:00–20:00 Maintenance 20:00–24:00 Available 21. Maintenance و Fueling نباید این‌ها خارج از Scheduler باقی بمانند. حداقل: Python @dataclass(frozen=True) class MaintenanceWindow: resource_id: str start: int end: int maintenance_type: str و: Python @dataclass(frozen=True) class FuelingWindow: resource_id: str start: int end: int در مدل نهایی این‌ها همان resource occupation هستند. 22. ارتباط با V1.7 Scheduler نکته مهم: V1.8 نباید Scheduler V1.7 را دوباره بنویسد. بلکه: Formation Engine ↓ Rolling Stock Feasibility ↓ Eligible Train Runs ↓ V1.7 Scheduler Scheduler همچنان مسئول: Block Station Junction Headway Switch Track Window Precedence Dwell است. و V1.8 مسئول: Wagon Formation Locomotive Cycle Availability Buffer خواهد بود. این separation برای maintainability بسیار مهم است. 23. Integration Layer یک Orchestrator لازم داریم: Python class IntegratedCapacityEngine: def run(self, request): demand = self.load_demand(request) wagon_requirements = ( self.wagon_requirement_engine .calculate(demand) ) formations = ( self.formation_engine .build(wagon_requirements) ) formation_check = ( self.formation_feasibility .validate(formations) ) if not formation_check.feasible: return self.fail( reason="FORMATION_INFEASIBLE" ) wagon_plan = ( self.wagon_cycle_engine .build(formations) ) if not wagon_plan.feasible: return self.fail( reason="WAGON_CYCLE_INFEASIBLE" ) loco_plan = ( self.locomotive_cycle_engine .build(formations) ) if not loco_plan.feasible: return self.fail( reason="LOCOMOTIVE_CYCLE_INFEASIBLE" ) schedule = ( self.operational_scheduler .solve( train_runs=formations.train_runs ) ) if not schedule.feasible: return self.fail( reason="OPERATIONAL_SCHEDULE_INFEASIBLE" ) return self.validate_integrated_solution( formations, wagon_plan, loco_plan, schedule, ) 24. ولی یک اصلاح مهم در نسخه ابتدایی V1.8 نباید این چهار مرحله را کاملاً sequential و مستقل نگه داریم. چرا؟ مثلاً ممکن است: Formation A از نظر واگن feasible باشد. و: Formation B هم feasible باشد. اما وقتی هر دو را با هم اجرا می‌کنیم: Wagon Pool = insufficient پس: Feasible(A)∧Feasible(B)  ⇒Feasible(A+B) همین مسئله درباره لکوموتیو هم وجود دارد. بنابراین V1.8 باید دو سطح داشته باشد. 25. سطح اول — Feasibility Precheck قبل از CP-SAT: Commodity Compatibility Wagon Count Train Length Train Weight Loco Traction Basic Availability برای حذف candidateهای غیرممکن. 26. سطح دوم — Integrated CP-SAT بعد منابع مشترک وارد مدل می‌شوند. مثلاً: WagonPool A = 240 wagons اگر هر Train: 40 wagons مصرف کند: 40F≤240 اما با cycle و زمان‌بندی: ∑ActiveWagonUsage(t)≤Inventory(t) این بسیار قوی‌تر است. 27. مدل Resource در CP-SAT از نظر modeling، برای task/resource scheduling، interval variables و constraints مثل NoOverlap ابزارهای طبیعی هستند؛ برای منابع ظرفیت‌دار نیز OR-Tools از مدل‌های resource/cumulative پشتیبانی می‌کند. Google for Developers +1 برای هر Train Run: Python start = model.new_int_var(...) end = model.new_int_var(...) interval = model.new_interval_var( start, duration, end, f"wagon_use_{train_id}", ) و برای یک resource محدود: WagonPool می‌توانیم capacity را مدل کنیم. 28. اما Wagon یک resource ساده نیست این نکته را باید از همین الآن در معماری ثبت کنیم. Wagon: Origin → Loaded → Destination → Empty → Origin پس resource: قابل حمل است location-dependent است state-dependent است time-dependent است بنابراین نباید Wagon را صرفاً مثل یک Machine در Job Shop مدل کنیم. مدل صحیح‌تر: WagonState(j,w,t) که state می‌تواند باشد: AVAILABLE_EMPTY LOADING LOADED IN_LOADED_TRAIN UNLOADING EMPTY_IN_TRANSIT MAINTENANCE UNAVAILABLE 29. همین موضوع برای Locomotive Locomotive هم: Depot A ↓ Train 101 ↓ Station B ↓ Train 205 ↓ Station C ↓ Maintenance بنابراین: LocoLocation(l,t) و: LocoAvailability(l,t) لازم است. 30. V1.8 Resource State Model پیشنهاد من این است که مدل مشترک زیر را اضافه کنیم: Python class ResourceState(str, Enum): AVAILABLE = "AVAILABLE" ASSIGNED = "ASSIGNED" IN_TRANSIT = "IN_TRANSIT" LOADING = "LOADING" UNLOADING = "UNLOADING" MAINTENANCE = "MAINTENANCE" FUELING = "FUELING" WAITING = "WAITING" و: Python @dataclass(frozen=True) class ResourceOccupation: resource_id: str state: ResourceState location_id: str | None start: int end: int train_run_id: str | None = None این abstraction بعداً برای: Wagon Loco Station Track Terminal Crew هم قابل استفاده خواهد بود. 31. ظرفیت یک Route بعد از V1.8 اکنون: C r ​ دیگر یک عدد ساده نیست. بهتر است: CapacityProfile داشته باشیم: Infrastructure Capacity Operational Capacity Rolling Stock Capacity Demand-Constrained Capacity Allocated Capacity مثلاً خروجی فرضی: Infrastructure Capacity 76 trains/day Operational Capacity 68 trains/day Rolling Stock Capacity 61 trains/day Market Demand Equivalent 54 trains/day Allocated Capacity 54 trains/day این اعداد صرفاً مثال معماری هستند، نه نتیجه واقعی. 32. چرا این تفکیک خیلی مهم است؟ فرض کنیم: Line capacity = 68 ولی: Available wagons = 55 trains equivalent اگر سیستم فقط Capacity Infrastructure را نشان دهد، مدیر ممکن است تصور کند: «68 قطار امکان‌پذیر است.» درحالی‌که با rolling stock موجود: فقط بخشی از این ظرفیت transportable است. بنابراین Explanation Engine باید بتواند بگوید: Infrastructure is not binding. The current limiting constraint is Wagon Cycle Availability. Increasing infrastructure capacity alone does not increase served freight under the current rolling-stock scenario. 33. Bottleneck جدید در V1.8 دسته‌بندی Bottleneck را گسترش می‌دهیم: INFRASTRUCTURE OPERATIONAL STATION JUNCTION WAGON WAGON_BUFFER WAGON_CYCLE LOCOMOTIVE LOCOMOTIVE_CYCLE FORMATION DEMAND TERMINAL POLICY 34. Marginal Impact برای هر resource: ΔC=C(x+Δx)−C(x) مثلاً: +1 locomotive → +2 trains/day +20 wagons → +1 train/day +1 station track → +0 trains/day +1 double-track section → +6 trains/day این‌ها باید خروجی Solver باشند، نه حدس کارشناسی. 35. Capacity Search در V1.8 الگوریتم: F = 1 Formation feasible? ↓ Wagon cycle feasible? ↓ Loco cycle feasible? ↓ Operational schedule feasible? ↓ Independent validation ↓ YES → F + 1 NO → candidate bottleneck ولی همان قاعده V1.7 حفظ می‌شود: UNKNOWN != INFEASIBLE اگر CP-SAT بگوید: UNKNOWN نباید بگوییم: F+1 impossible در مستندات رسمی CP-SAT نیز وضعیت‌های OPTIMAL, FEASIBLE, INFEASIBLE, MODEL_INVALID, و UNKNOWN از هم متمایزند. Google for Developers 36. Capacity Proof جدید Python @dataclass(frozen=True) class IntegratedCapacityProof: F: int F_plus_one: int formation_feasible: bool wagon_cycle_feasible: bool locomotive_cycle_feasible: bool F_schedule_status: str F_validation_passed: bool F_plus_one_status: str proven_infeasible_reason: str | None proof_valid: bool و: Python proof_valid = ( F_schedule_status in {"OPTIMAL", "FEASIBLE"} and F_validation_passed and F_plus_one_status == "INFEASIBLE" ) 37. نکته مهم‌تر: F+1 ممکن است به دلیل متفاوتی شکست بخورد مثلاً: F = 40 Feasible. برای: F = 41 ممکن است: Infrastructure conflict باشد. یا: No wagon cycle یا: No locomotive یا: Station length بنابراین Proof باید علت شکست را هم نگه دارد. 38. Explanation Engine خروجی: JSON { "capacity": 40, "next_candidate": 41, "proof_status": "PROVEN", "binding_constraints": [ { "type": "WAGON_CYCLE", "resource": "WAGON_TYPE_A", "slack": 0 }, { "type": "SINGLE_TRACK_HEADWAY", "resource": "BLOCK_17", "slack": 0 } ] } 39. UI جدید Capacity Inspector از UI V1.7 استفاده می‌کنیم و یک بخش اضافه می‌کنیم: CAPACITY INSPECTOR ──────────────────────────────────── Infrastructure Capacity 68 Operational Capacity 64 Rolling Stock Capacity 57 Demand Constrained Capacity 51 Allocated Capacity 51 ──────────────────────────────────── Current Binding Constraints 1. Wagon Cycle Wagon Type: WT-02 Slack: 0 Marginal Impact: +1 train 2. Single Track Block Block: B-17 Slack: 0 Marginal Impact: +4 trains 3. Locomotive Cycle Loco Pool: LP-03 Slack: 1 Marginal Impact: +2 trains ──────────────────────────────────── Capacity Proof F = 57 FEASIBLE ✓ F+1 = 58 INFEASIBLE ✓ Proof: VALID 40. Scenario Analysis V1.8 باید سناریوهای rolling stock را هم اجرا کند. مثلاً: Scenario A Current Fleet Scenario B +100 Wagons Scenario C +2 Locomotives Scenario D +100 Wagons +2 Locomotives و سپس: C A ​ , C B ​ , C C ​ , C D ​ را با یکدیگر مقایسه کند. 41. نکته کلیدی درباره Investment فرض کنیم: +100 wagons هیچ اثری نداشت. این الزاماً به معنی بی‌فایده بودن واگن نیست. ممکن است: Station Capacity یا: Single Track قبل از آن binding باشد. بنابراین: ΔC wagon ​ =0 در Scenario فعلی. اما اگر ابتدا infrastructure توسعه پیدا کند: ΔC wagon ​ >0 این دقیقاً همان reason است که Scenario Engine باید کل مدل را دوباره solve کند. 42. ساختار پروژه V1.8 پیشنهاد می‌کنم ساختار V1.7 به این شکل توسعه پیدا کند: app/ ├── domain/ │ ├── infrastructure.py │ ├── station.py │ ├── route.py │ ├── train.py │ ├── demand.py │ │ │ ├── wagon.py │ ├── wagon_requirement.py │ ├── wagon_cycle.py │ │ │ ├── locomotive.py │ ├── locomotive_cycle.py │ │ │ ├── formation.py │ ├── resource_state.py │ │ │ ├── schedule.py │ ├── capacity.py │ └── result.py │ ├── engines/ │ ├── formation_engine.py │ ├── formation_feasibility.py │ ├── wagon_requirement.py │ ├── wagon_cycle.py │ ├── locomotive_cycle.py │ ├── rolling_stock.py │ ├── integrated_capacity.py │ └── capacity_search.py │ ├── scheduling/ │ ├── scheduler.py │ ├── precedence.py │ ├── block_constraints.py │ ├── headway.py │ ├── station_tracks.py │ ├── junctions.py │ ├── windows.py │ ├── objective.py │ └── rolling_stock_constraints.py │ ├── validation/ │ ├── schedule.py │ ├── formation.py │ ├── wagon_cycle.py │ ├── locomotive_cycle.py │ └── integrated.py │ ├── explanation/ │ ├── bottleneck.py │ ├── proof.py │ └── narrative.py │ └── services/ ├── capacity_service.py ├── scenario_service.py └── run_manager.py 43. Integration Test اصلی V1.8 یک Golden Test باید داشته باشیم. Route: A → B Infrastructure: Single Track Wagon: 40 wagons/train Wagon Pool: 120 wagons Locomotives: 3 Cycle: A → B → A Operational: Headway = 10 min Demand: unlimited Engine باید خودش تشخیص دهد: Infrastructure feasible Formation feasible Wagon cycle feasible Locomotive cycle feasible Schedule feasible و سپس ظرفیت را search کند. 44. تست مهم‌تر Wagon bottleneck test Infrastructure: 100 trains/day Wagon: 2 trains/day equivalent Locomotive: 100 trains/day Expected: Infrastructure Capacity = 100 Rolling Stock Capacity = 2 Integrated Capacity = 2 نه: 100 45. تست Locomotive Bottleneck Infrastructure = 100 Wagons = 100 Locomotives = 5 اگر هر locomotive cycle اجازه 4 service/day بدهد: C loco ​ ≈20 و integrated capacity باید بر اساس schedule واقعی و cycle به حدود آن محدود شود. عدد نهایی باید توسط Solver تعیین شود، نه فرمول تقریبی. 46. تست Empty Wagon Bottleneck Loaded trains: Origin → Destination Empty return: Destination → Origin اگر single-track reverse movement باعث شود Empty Return نتواند به‌موقع برگردد: Loaded Demand ↓ Wagon shortage at Origin ↓ Train formation failure ↓ Capacity reduction این تست برای سیستم شما بسیار مهم است، چون دقیقاً تفاوت بین یک Railway Capacity Calculator ساده و یک Railway Capacity Engine واقعی را نشان می‌دهد. 47. خروجی نهایی V1.8 هر Run باید package زیر را تولید کند: Run Scenario Data Version Model Version Demand Freight Flow Wagon Requirement Train Formation Formation Validation Wagon Assignment Wagon Cycle Empty Wagon Flow Wagon Inventory Locomotive Assignment Locomotive Cycle Availability Maintenance Fueling Train Runs Directed Paths Operational Schedule Schedule Validation Formation Validation Wagon Validation Locomotive Validation Integrated Validation Infrastructure Capacity Operational Capacity Rolling Stock Capacity Transportable Capacity Allocated Capacity Capacity Proof Binding Constraints Bottlenecks Resource Utilization Slack Marginal Impact Explanation 48. Definition of Done — V1.8 V1.8 زمانی کامل است که: Formation Wagon type Wagon compatibility Length Weight Train type Locomotive traction Station compatibility Wagon Wagon pool Inventory Loaded state Empty state Wagon requirement Wagon cycle Empty return Buffer Time-dependent balance Locomotive Loco type Loco pool Assignment Cycle Turnback Fueling Maintenance Availability Scheduler V1.7 scheduler preserved Block conflict Single track Double track Headway Switch time Station track Junction Operational windows Dwell Earliest departure Latest arrival Integrated Capacity Formation feasibility Wagon feasibility Loco feasibility Operational feasibility Integrated feasibility F feasible F+1 explicitly tested UNKNOWN ≠ INFEASIBLE Independent validation Binding constraint Bottleneck explanation Scenario comparison 49. معماری نهایی تا اینجا اکنون سیستم ما عملاً به این معماری رسیده است: MARKETPLACE │ ▼ Market Demand │ ▼ Freight Flow │ ▼ Wagon Requirement │ ┌────────────┴────────────┐ ▼ ▼ Train Formation Empty Flow │ │ ▼ ▼ Wagon Assignment Wagon Inventory │ │ └────────────┬────────────┘ ▼ Wagon Cycle │ ▼ Locomotive Assignment │ ▼ Locomotive Cycle │ ▼ TrainRun │ ▼ Directed Route │ ▼ ┌────────────────────────┐ │ V1.7 Operational │ │ Scheduler │ │ │ │ Block │ │ Station │ │ Junction │ │ Headway │ │ Switch │ │ Track │ │ Window │ └───────────┬────────────┘ ▼ Feasible Timetable │ ▼ Integrated Validation │ ▼ Capacity Search │ ┌───────┴────────┐ ▼ ▼ F feasible F+1 test │ ┌────────────┴────────────┐ ▼ ▼ INFEASIBLE UNKNOWN │ │ ▼ ▼ Valid Capacity No Proof │ ▼ Bottleneck Analysis │ ▼ Scenario Analysis │ ▼ Marketplace مهم‌ترین تصمیم معماری V1.8 من پیشنهاد می‌کنم فعلاً Wagon و Locomotive را مستقیماً داخل تمام متغیرهای CP-SAT منفجر نکنیم. ابتدا: Formation Engine Wagon Cycle Engine Locomotive Cycle Engine Rolling Stock Feasibility سپس اتصال کنترل‌شده به CP-SAT را انجام دهیم. چون اگر از ابتدا هر واگن، هر لکوموتیو، هر حرکت و هر state را به‌صورت individual decision variable وارد کنیم، اندازه مدل به‌شدت رشد می‌کند. CP-SAT برای مسائل constraint-heavy مناسب است، اما مدل باید با abstraction درست ساخته شود. Google for Developers +1 قدم بعدی منطقی V1.8.1 بنابراین پیاده‌سازی واقعی همین چهار موتور است: FormationEngine ↓ WagonCycleEngine ↓ LocomotiveCycleEngine ↓ IntegratedRollingStockFeasibility ↓ V1.7 OperationalScheduler و بعد از آن، V1.9 — Network Capacity با Multi-OD + Shared Wagon/Loco Pools + Empty Wagon Network خواهد بود؛ یعنی دقیقاً جایی که چند OD به‌صورت همزمان برای یک شبکه ریلی با منابع مشترک رقابت می‌کنند. --- ## ادامه 39 — پرسش کاربر (`message-id:f5ce5533-d14d-48d3-9924-ad243356f456`) بله لطفا --- ## ادامه 39 — پاسخ دستیار (`message-id:d11927ff-a366-4bf0-98e4-3271769cbdc7`) حتماً. حالا وارد V1.9 — Network Capacity & Multi-OD Optimization می‌شویم. این نسخه نقطه‌ای است که موتور از «ظرفیت یک مسیر» به یک Network Capacity Engine واقعی تبدیل می‌شود؛ یعنی چند OD، چند Route، واگن‌های مشترک، لکوموتیوهای مشترک، Empty Wagon Flow، ایستگاه‌ها و Junctionهای مشترک همزمان وارد حل می‌شوند. یک اصل را از V1.8 حفظ می‌کنیم: V1.7 Operational Scheduler هسته زمان‌بندی باقی می‌ماند و V1.9 روی آن یک Network Optimization Layer می‌سازد. V1.9 — Network Capacity & Multi-OD Optimization 1. مسئله‌ای که V1.9 حل می‌کند تا V1.8 فرض اصلی تقریباً این بود: OD ↓ Formation ↓ Wagon/Loco ↓ Route ↓ Schedule ↓ Capacity اما در شبکه واقعی: OD-1 ───── Route-A ───┐ │ OD-2 ───── Route-B ───┼── Shared Block │ OD-3 ───── Route-C ───┘ و همچنین: ┌── OD-1 Wagon Pool ──┼── OD-2 └── OD-3 و: ┌── Train 101 Loco Pool ┼── Train 205 └── Train 310 پس ظرفیت هر Route دیگر مستقل نیست. 2. تعریف رسمی ظرفیت شبکه تعریف V1.9: C N ​ =max r∈R ∑ ​ Q r ​ F r ​ ​ subject to: Schedule(F) Formation(F) WagonCycle(F) LocomotiveCycle(F) StationCapacity(F) JunctionCapacity(F) TerminalCapacity(F) EmptyWagonBalance(F) Demand(F) Policy(F) همگی feasible باشند. نکته مهم این است که: C N ​  = r ∑ ​ C r ​ مگر زمانی که Routeها واقعاً منابع مشترک نداشته باشند. 3. مدل Multi-OD در V1.9 یک موجودیت اصلی اضافه می‌کنیم: Python @dataclass(frozen=True) class ODPair: id: str origin_station_id: str destination_station_id: str commodity_ids: tuple[str, ...] = () و: Python @dataclass(frozen=True) class NetworkRoute: id: str od_pair_id: str route_id: str priority: int = 0 max_daily_trains: int | None = None بنابراین: OD-01 ├── Route-A └── Route-B OD-02 ├── Route-C └── Route-D ممکن است یک OD چند Route داشته باشد. 4. Route Choice این موضوع باید داخل Solver باشد. متغیر: x od,r,t ​ که نشان می‌دهد چند قطار از OD مشخص روی Route مشخص در Time Period مشخص حرکت می‌کنند. مثلاً: x Tehran−Khowaf,RouteA,day1 ​ 5. Demand Constraint اگر: D od,t ​ تقاضای OD باشد: Q od ​ F od,t ​ ≤D od,t ​ یا اگر چند نوع قطار داشته باشیم: r ∑ ​ Q od,r ​ F od,r ​ ≤D od ​ 6. Demand نباید الزاماً hard constraint باشد این یکی از تصمیم‌های مهم V1.9 است. دو Mode داریم: Capacity Mode هدف: maxServedDemand پس تقاضا upper bound است. Service Obligation Mode مثلاً: ServedDemand OD ​ ≥Target OD ​ یعنی سیاست یا قرارداد حداقل خدمت داریم. 7. Market Demand / Transportable / Allocated مدل قبلی را حفظ می‌کنیم: D market ​  =D transportable ​  =D allocated ​ و در V1.9 این سه در سطح Network نیز ثبت می‌شوند. مثلاً: Market Demand 12.0 Mt Transportable Demand 9.4 Mt Allocated Demand 8.1 Mt Unserved Demand 3.9 Mt این اعداد صرفاً نمونه هستند. 8. Shared Resource مهم‌ترین entity جدید: Python @dataclass(frozen=True) class SharedResource: id: str resource_type: str capacity: int station_id: str | None = None block_id: str | None = None terminal_id: str | None = None Resource type: BLOCK STATION JUNCTION TERMINAL WAGON_POOL LOCOMOTIVE_POOL BUFFER CREW_POOL 9. Resource Usage هر Route باید بگوید چه منابعی را مصرف می‌کند: Python @dataclass(frozen=True) class ResourceUsage: resource_id: str train_count: int = 1 duration_minutes: int | None = None wagon_units: int = 0 locomotive_units: int = 0 ولی این مدل برای resourceهای زمانی کافی نیست؛ بنابراین دو نوع Resource Usage تعریف می‌کنیم: Static Resource Usage Temporal Resource Usage 10. Static Resource مثلاً: Wagon Pool WT-01 = 500 Loco Pool LP-02 = 12 قید: r ∑ ​ a r,g ​ F r ​ ≤C g ​ 11. Temporal Resource مثلاً Block: Train A 08:00–08:25 Train B 08:20–08:45 این فقط با جمع روزانه قابل کنترل نیست. باید زمان دقیق وارد شود: Occupancy r,g,t ​ و: r ∑ ​ Occupancy r,g,t ​ ≤C g ​ این همان جایی است که Network Solver به V1.7 Scheduler متصل می‌شود. 12. Shared Block فرض کنیم: OD1 → Route A → Block B17 OD2 → Route B → Block B17 حتی اگر: Route A capacity = 20 Route B capacity = 20 ممکن است: Network capacity = 20 یا هر مقدار دیگری که timetable واقعی اجازه دهد. بنابراین: C A ​ +C B ​ به‌صورت خودکار معتبر نیست. 13. Shared Station Station نیز ممکن است bottleneck باشد: Route A ↓ Train ──────── Station X ↑ Route B منابع: Station Tracks Platform Entry Exit Formation Track Loading Track Crossing Track همگی باید Resource باشند. 14. Shared Junction در V1.7 Junction Conflict داشتیم. در V1.9 همان مدل را Network-level می‌کنیم. مثلاً: Movement M1 A → J → B Movement M2 C → J → D اگر: M1⊥M2 آنگاه: End(M1)+T sep ​ ≤Start(M2) یا برعکس. 15. Multi-OD Empty Wagon Network این بخش یکی از مهم‌ترین قسمت‌های V1.9 است. فرض: OD1: Tehran → Khowaf OD2: Tehran → Zarand هر دو: Origin = Tehran و Wagon Pool مشترک دارند. بعد: Khowaf → Tehran Zarand → Tehran Empty Wagon Flow تولید می‌شود. پس شبکه واگن خودش یک Network Flow است. 16. Wagon State Network برای هر: w,j,t داریم: E j,w,t ​ و: L j,w,t ​ که: E = Empty L = Loaded Balance: E j,w,t+1 ​ =E j,w,t ​ +EmptyIn+Unload−EmptyOut−Load و: L j,w,t+1 ​ =L j,w,t ​ +Load−Unload 17. مهم: Empty Wagon Flow می‌تواند خودش ظرفیت بسازد یا از بین ببرد مثلاً: Tehran ↓ loaded Khowaf ↓ empty Tehran اگر Empty Return بیش از حد زمان ببرد: Next loading ↓ No wagon available ↓ Formation failure پس: C loaded ​ و: C empty ​ به‌صورت coupled باید حل شوند. 18. Locomotive Network همین مسئله برای لکوموتیو وجود دارد. مثلاً: Loco 801 Tehran → Khowaf ↓ Turnback ↓ Khowaf → Tehran ولی ممکن است به‌جای Return: Khowaf → Zarand برود. بنابراین Locomotive Assignment نیز Network Flow + Scheduling است. 19. Locomotive Compatibility برای هر Train: LocoType∈Compatible(TrainType) و: TE available ​ ≥TE required ​ اگر double-heading باشد: TE 1 ​ +TE 2 ​ ≥TE required ​ اما این دو لکوموتیو باید خودشان در همان زمان و مکان available باشند. 20. Network Decision Variables مجموعه متغیرها: Train Count F r ​ OD Allocation x od,r,t ​ Freight Flow q od,r,t ​ Wagon Assignment w k,r,d ​ Locomotive Assignment l k,r,d ​ Route Choice z od,r ​ Resource Usage u r,g,t ​ 21. Route Selection برای هر OD: r ∑ ​ z od,r ​ =1 اگر فقط یک Route انتخاب شود. ولی برای Split Flow: r ∑ ​ z od,r ​ ≥1 و: x od,r ​ ≤Mz od,r ​ 22. Multi-Route Split مثلاً: Tehran → Khowaf Route A 60% Route B 40% اما Solver ممکن است بر اساس objective، constraints و resource availability مقدار دیگری پیدا کند. یعنی Route Split decision variable است، نه ورودی ثابت. 23. Objective V1.9 باید چند Objective داشته باشد. Objective 1 — Max Freight max od,r,t ∑ ​ q od,r,t ​ Objective 2 — Max Revenue max od,r,t ∑ ​ Revenue od,r ​ q od,r,t ​ Objective 3 — Min Unserved Demand min od ∑ ​ (D od ​ −Served od ​ ) Objective 4 — Min Operational Cost مثلاً: minC loco ​ +C wagon ​ +C empty ​ +C delay ​ 24. Lexicographic Objective برای سیستم مدیریتی پیشنهاد من: Priority 1: Meet mandatory demand / policy Priority 2: Maximize served freight Priority 3: Minimize unserved demand Priority 4: Minimize empty movement Priority 5: Minimize operational cost Priority 6: Minimize schedule deviation این ترتیب باید Configurable باشد. نباید در کد hard-code شود. 25. Policy Constraints مثلاً مدیریت بگوید: OD-A must receive at least 40 trains/day. مدل: F A ​ ≥40 یا: Route B must receive at least 20%. F B ​ ≥0.2F total ​ Policy باید در Scenario ذخیره شود. 26. Network Solver Architecture V1.9 را به این شکل می‌سازیم: Network Input │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ Demand Infrastructure Rolling Stock │ │ │ └──────────────┼──────────────┘ ▼ Candidate Generator │ ▼ Route Alternatives │ ▼ Formation Engine │ ▼ Wagon/Loco Feasibility │ ▼ Network Model Builder │ ┌────────┴────────┐ ▼ ▼ Static Constraints V1.7 Scheduler │ │ └────────┬────────┘ ▼ Network Solver │ ▼ Feasible Solution │ ▼ Independent Validator │ ▼ Capacity / Allocation 27. یک نکته معماری بسیار مهم Network Solver نباید مستقیماً وارد جزئیات Access/Excel شود. همان اصل قبلی حفظ می‌شود: Access Excel API Marketplace ↓ Adapters ↓ Canonical Model ↓ Network Solver 28. Candidate Generation قبل از Optimization باید Candidate Train Service بسازیم. مثلاً: Python @dataclass(frozen=True) class TrainServiceCandidate: id: str od_pair_id: str route_id: str train_type_id: str wagon_type_id: str wagon_count: int payload_t: float locomotive_type_ids: tuple[str, ...] direction: str load_state: str این Candidate به V1.8 Formation Engine متصل می‌شود. 29. Candidate Pruning قبل از CP-SAT: Commodity incompatible? ↓ remove Station too short? ↓ remove Train too heavy? ↓ remove No compatible locomotive? ↓ remove No wagon type? ↓ remove Impossible route? ↓ remove این کار اندازه مدل را بسیار کاهش می‌دهد. 30. اما Candidate Feasible ≠ Network Feasible مثلاً: Candidate A ✓ Candidate B ✓ Candidate C ✓ ولی: A + B + C ممکن است به دلیل: Shared block Shared station Shared wagons Shared locomotives غیرممکن باشد. این distinction باید در مدل و UI کاملاً حفظ شود. 31. Network Capacity Result Python @dataclass(frozen=True) class NetworkCapacityResult: infrastructure_capacity: float operational_capacity: float rolling_stock_capacity: float transportable_capacity: float allocated_capacity: float served_demand: float unserved_demand: float route_allocations: dict[str, float] binding_constraints: tuple[str, ...] 32. Network Result Python @dataclass(frozen=True) class NetworkSolution: run_id: str selected_routes: tuple[str, ...] train_services: tuple[str, ...] od_flows: dict[str, float] wagon_assignments: tuple[str, ...] locomotive_assignments: tuple[str, ...] empty_wagon_flows: tuple[str, ...] schedule_id: str objective_value: float validated: bool 33. Bottleneck Analysis در سطح شبکه این قسمت باید بسیار قوی باشد. مثلاً: NETWORK BOTTLENECK REPORT Resource Utilization Slack ------------------------------------------------ Block B17 100% 0 Station S12 98% 4 min Junction J4 100% 0 Wagon Pool WT02 94% 18 Loco Pool LP01 100% 0 Terminal T7 71% 29 اما Utilization به‌تنهایی bottleneck نیست. برای Binding: Slack=0 و سپس: ΔC را محاسبه می‌کنیم. 34. Marginal Network Capacity برای resource g: MC g ​ =C(g+Δg)−C(g) مثلاً: Resource: Block B17 Current: Capacity = 820,000 ton/day Scenario: Add double-track capability New: Capacity = 1,050,000 ton/day Marginal impact: +230,000 ton/day اعداد فقط illustrative هستند. 35. Hidden Capacity V1.9 باید Operating Regime را هم در سطح شبکه وارد کند. مثلاً: Scenario 1 STRICT_ALTERNATING Scenario 2 DIRECTIONAL_BATCH Scenario 3 MIXED و Solver بتواند بررسی کند: C N strict ​ C N batch ​ C N mixed ​ بدون اینکه یکی را از قبل «بهتر» فرض کنیم. 36. Network Capacity با چند OD نمونه: OD-01: Tehran → Khowaf Demand = 1000 kt/day OD-02: Tehran → Zarand Demand = 700 kt/day OD-03: Aprin → Khowaf Demand = 500 kt/day همگی ممکن است از: Shared Block B17 Shared Junction J4 Shared Loco Pool LP1 Shared Wagon Pool WT2 استفاده کنند. Solver باید allocation را همزمان پیدا کند. 37. خروجی مهم‌تر از Capacity در Network Engine فقط نمی‌خواهیم بگوییم: Network Capacity = X باید بگوییم: X چگونه بین ODها توزیع شده است؟ مثلاً: OD Served -------------------------------- Tehran-Khowaf 820 kt Tehran-Zarand 510 kt Aprin-Khowaf 370 kt Total 1700 kt و: Unserved: Tehran-Khowaf 180 kt Tehran-Zarand 190 kt Aprin-Khowaf 130 kt 38. Traceability این خروجی باید قابل Trace باشد: Market Request ↓ Demand ↓ Freight Flow ↓ Train Service ↓ Formation ↓ Wagon Assignment ↓ Loco Assignment ↓ Route ↓ Schedule ↓ Capacity ↓ Allocation یعنی مدیر بتواند از: «چرا این 180 هزار تن سرو نشده؟» برگردد به: Unserved Demand ↓ No feasible Train Service ↓ Wagon Cycle Conflict ↓ Empty Wagon unavailable at Origin ↓ Previous OD consumed Wagon Pool این یکی از مهم‌ترین قابلیت‌های کل سیستم خواهد بود. 39. Data Model V1.9 در DB باید entityهای زیر اضافه شوند: od_pair network_route train_service_candidate shared_resource resource_usage network_run network_solution network_allocation empty_wagon_flow wagon_state_transition locomotive_state_transition policy_constraint objective_definition bottleneck_result marginal_capacity_result 40. API Endpointهای پیشنهادی: http POST /api/v1/network/runs GET /api/v1/network/runs/{run_id} POST /api/v1/network/capacity GET /api/v1/network/capacity/{run_id} GET /api/v1/network/{run_id}/allocations GET /api/v1/network/{run_id}/bottlenecks GET /api/v1/network/{run_id}/resources GET /api/v1/network/{run_id}/empty-wagons POST /api/v1/network/{run_id}/scenarios GET /api/v1/network/{run_id}/compare 41. Scenario API مثلاً: JSON { "scenario_id": "SC-203", "base_scenario": "CURRENT", "changes": [ { "type": "ADD_WAGONS", "wagon_type": "WT-02", "quantity": 100 }, { "type": "ADD_LOCOMOTIVES", "locomotive_type": "L-03", "quantity": 2 } ], "objective": "MAX_SERVED_FREIGHT" } 42. Network Scenario Matrix خروجی: Scenario Infra Operational Rolling Stock Served Freight Current — — — baseline + Wagons unchanged unchanged ↑ recalculated + Locos unchanged unchanged ↑ recalculated Double Track ↑ ↑ unchanged recalculated + Wagons + Double Track ↑ ↑ ↑ recalculated عددهای واقعی باید از Engine بیایند. 43. V1.9 Capacity Proof در سطح Network دیگر فقط: F+1 کافی نیست. Proof باید نشان دهد: C N ​ +ϵ feasible نیست. اما چون چند OD و objective داریم، Proof را بر اساس صورت دقیق مسئله تعریف می‌کنیم. مثلاً: Objective: MAX_SERVED_FREIGHT Current optimum: 1,720,000 ton/day Upper-bound test: 1,720,001 ton/day Status: INFEASIBLE این بسیار قوی‌تر از گفتن «ظرفیت حدود 1720 هزار تن است» خواهد بود. 44. دو نوع Proof Capacity Proof بیشینه objective قابل دستیابی است. Allocation Proof برای یک Allocation خاص، feasibility اثبات شده است. این دو نباید یکی تلقی شوند. 45. Independent Network Validator پس از Solver: NetworkValidator باید دوباره بررسی کند: Demand Route Train Count Block Occupancy Station Occupancy Junction Conflict Wagon Inventory Empty Flow Loco Assignment Loco Cycle Formation Terminal Policy بدون استفاده از همان منطق داخلی Solver. 46. این اصل خیلی مهم است اگر Solver بگوید: FEASIBLE هنوز نتیجه Production-ready نیست. باید: Solver ↓ Independent Validator ↓ PASS باشد. اگر: Solver = FEASIBLE Validator = FAIL نتیجه باید: INVALID SOLUTION شود. 47. Project Structure نهایی V1.9 app/ ├── domain/ │ ├── od.py │ ├── demand.py │ ├── route.py │ ├── train.py │ ├── formation.py │ ├── wagon.py │ ├── wagon_cycle.py │ ├── locomotive.py │ ├── locomotive_cycle.py │ ├── infrastructure.py │ ├── resource.py │ ├── schedule.py │ ├── network.py │ ├── scenario.py │ └── result.py │ ├── network/ │ ├── candidate_generator.py │ ├── route_choice.py │ ├── flow_builder.py │ ├── resource_model.py │ ├── wagon_flow.py │ ├── locomotive_flow.py │ ├── network_model.py │ ├── network_solver.py │ ├── network_validator.py │ └── capacity.py │ ├── engines/ │ ├── formation_engine.py │ ├── wagon_cycle.py │ ├── locomotive_cycle.py │ ├── scheduling.py │ └── integrated_capacity.py │ ├── scheduling/ │ └── ... V1.7 ... │ ├── validation/ │ └── ... V1.8 ... │ ├── explanation/ │ ├── bottleneck.py │ ├── marginal.py │ ├── proof.py │ └── traceability.py │ └── services/ ├── network_service.py ├── capacity_service.py └── scenario_service.py 48. معماری Solver پیشنهادی در V1.9 یک Model Builder مرکزی می‌خواهیم: Python class NetworkModelBuilder: def build(self, network_input): model = CpModel() # 1. OD flow self.add_demand_constraints(model) # 2. Route choice self.add_route_choice(model) # 3. Train services self.add_train_service_variables(model) # 4. Formation self.add_formation_constraints(model) # 5. Wagon inventory self.add_wagon_balance(model) # 6. Empty wagon flow self.add_empty_wagon_flow(model) # 7. Locomotive cycle self.add_locomotive_constraints(model) # 8. Shared infrastructure self.add_shared_resource_constraints(model) # 9. V1.7 schedule self.add_operational_schedule_constraints(model) # 10. Policy self.add_policy_constraints(model) # 11. Objective self.add_objective(model) return model 49. اما یک اصلاح فنی مهم در implementation واقعی نباید add_operational_schedule_constraints() صرفاً یک کپی از Scheduler V1.7 باشد. بهتر است Scheduler V1.7 به یک interface تبدیل شود: Python class OperationalSchedulingModel: def add_constraints( self, model, train_services, infrastructure, ): ... بعد Network Model Builder آن را صدا بزند. این باعث می‌شود: Route Capacity و: Network Capacity از یک Scheduling Core مشترک استفاده کنند. 50. نتیجه معماری V1.9 تا اینجا سیستم ما به این ساختار رسیده: MARKETPLACE │ ▼ MARKET DEMAND │ ▼ FREIGHT FLOW │ ┌───────────────┴────────────────┐ ▼ ▼ OD / Commodity Demand Policy │ ▼ Route Alternatives │ ▼ Train Service Candidates │ ┌─────┴─────┐ ▼ ▼ Formation Train Type │ ┌────┴───────────────┐ ▼ ▼ Wagon Cycle Locomotive Cycle │ │ └─────────┬──────────┘ ▼ Resource Model │ ┌──────┴───────────┐ ▼ ▼ Infrastructure Rolling Stock │ │ └────────┬─────────┘ ▼ NETWORK SOLVER │ ▼ V1.7 SCHEDULER CORE │ ▼ Feasible Network Timetable │ ▼ Independent Validation │ ▼ Network Capacity │ ┌───────┴────────┐ ▼ ▼ Allocation Bottleneck │ │ └───────┬────────┘ ▼ Scenario Analysis │ ▼ MARKETPLACE 51. Definition of Done — V1.9 V1.9 را زمانی Production-level می‌دانیم که: Multi-OD OD Pair Multiple OD Multiple Route Route Choice Split Flow Demand Market Demand Transportable Demand Allocated Demand Unserved Demand Demand Policy Network Shared Block Shared Station Shared Junction Shared Terminal Shared Wagon Pool Shared Locomotive Pool Wagon Loaded Flow Empty Flow Inventory Balance Buffer Wagon Cycle Multi-OD competition Locomotive Assignment Cycle Turnback Availability Maintenance Fueling Multi-OD competition Scheduling V1.7 Scheduler reused Network-wide block conflicts Station conflicts Junction conflicts Headway Switch Operational windows Optimization Max Freight Max Revenue Min Unserved Min Cost Lexicographic objective Policy constraints Validation Independent Network Validator Resource balance Wagon balance Loco balance Schedule validation Capacity proof Allocation traceability Explanation Binding constraint Bottleneck Slack Marginal Capacity Scenario Delta Market-to-Capacity Traceability قدم بعدی: V2.0 — Railway Capacity Planning Workbench بعد از V1.9، دیگر مسئله صرفاً «Engine» نیست. ما می‌توانیم یک Railway Capacity Planning Workbench بسازیم که روی همین هسته قرار بگیرد: V1.7 Operationally Correct Scheduler ↓ V1.8 Rolling Stock Integrated Capacity ↓ V1.9 Network / Multi-OD Capacity ↓ V2.0 Railway Capacity Planning Workbench و V2.0 شامل یکپارچه‌سازی کامل این موارد خواهد بود: Data Management Scenario Builder Demand Management Infrastructure Editor Train Formation Wagon/Loco Fleet Route & Timetable Capacity Calculation Network Optimization Investment Scenarios Sensitivity Analysis Bottleneck Explorer Capacity Proof Market Allocation Visualization API Audit / Lineage در آن مرحله، سیستم دیگر صرفاً یک calculator نخواهد بود؛ بلکه یک decision-support and optimization platform برای برنامه‌ریزی ظرفیت شبکه ریلی خواهد بود. --- ## ادامه 40 — پرسش کاربر (`message-id:ac85c626-ec69-4806-9b14-d4cafcf1a9d4`) بله لطفا --- ## ادامه 40 — پاسخ دستیار (`message-id:8d1da6e6-bd6f-4c0d-9d0c-9d639d4d9bed`) حتماً! حالا V2.0 را به‌عنوان Railway Capacity Planning Workbench از حالت ایده خارج می‌کنیم و به یک معماری اجرایی و Production-oriented تبدیل می‌کنیم. این نسخه در واقع لایه‌ای است که تمام موتورهای V1.7 تا V1.9 را به یک محصول واحد تبدیل می‌کند. برای طراحی این Workbench، اصل مهم این است که Constraint/Optimization Engine از UI، Data Platform و Business Workflow جدا بماند؛ چون در مسئله‌ای با ظرفیت، منابع، time-window، compatibility و objectiveهای متعدد، خود مدل به اندازه UI اهمیت دارد. این رویکرد با الگوی عمومی سیستم‌های optimization که منابع، ظرفیت، محدودیت‌های زمانی و objective را از هم تفکیک می‌کنند نیز هم‌راستاست. Google for Developers +1 V2.0 — Railway Capacity Planning Workbench 1. تعریف محصول V2.0 دیگر فقط: Railway Capacity Calculator نیست. تعریف مناسب‌تر: Railway Capacity Planning, Scheduling & Network Optimization Workbench با این چرخه: Data ↓ Model ↓ Scenario ↓ Demand ↓ Formation ↓ Capacity ↓ Scheduling ↓ Network Optimization ↓ Allocation ↓ Scenario Comparison ↓ Investment / Decision Support 2. معماری کلان V2.0 ┌──────────────────────────────────────────────────────────────┐ │ RAILWAY CAPACITY WORKBENCH │ ├──────────────────────────────────────────────────────────────┤ │ │ │ Presentation Layer │ │ ┌────────────┬────────────┬────────────┬───────────────┐ │ │ │ Dashboard │ Scenario │ Network │ Capacity │ │ │ │ │ Builder │ Explorer │ Inspector │ │ │ └────────────┴────────────┴────────────┴───────────────┘ │ │ │ │ ├─────────────────────────┼────────────────────────────────────┤ │ Application Layer │ │ ┌────────────┬────────────┬────────────┬───────────────┐ │ │ │ Run │ Scenario │ Capacity │ Allocation │ │ │ │ Manager │ Service │ Service │ Service │ │ │ └────────────┴────────────┴────────────┴───────────────┘ │ │ │ │ ├─────────────────────────┼────────────────────────────────────┤ │ Domain / Engine Layer │ │ │ │ Demand → Formation → Wagon → Loco → Schedule → Network │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Formation │ │ Rolling │ │ Scheduling │ │ │ │ Engine │ │ Stock │ │ Engine │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Network Optimization Engine │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ ├──────────────────────────────────────────────────────────────┤ │ Data Platform │ │ PostgreSQL | Staging | Lineage | Versioning | Quality │ ├──────────────────────────────────────────────────────────────┤ │ Source / Integration │ │ Access | Excel | Marketplace API | GIS | External Systems │ └──────────────────────────────────────────────────────────────┘ 3. اصل معماری بسیار مهم Workbench نباید مستقیماً با Solver صحبت کند. یعنی این اشتباه: UI ↓ CP-SAT ممنوع. معماری درست: UI ↓ Application Service ↓ Canonical Scenario ↓ Engine ↓ Solver ↓ Validated Result ↓ Application Service ↓ UI 4. Core Domain Domain اصلی V2.0: Infrastructure Station Block Junction Route Train TrainRun TrainType Demand FreightFlow ODPair WagonType Wagon WagonPool WagonInventory WagonRequirement WagonCycle LocomotiveType Locomotive LocomotivePool LocomotiveCycle TrainFormation TrainFormationItem Schedule OperationalWindow ResourceUsage CapacityProfile CapacityOffer Allocation Scenario Investment PolicyConstraint Run Result Proof Bottleneck Explanation 5. Scenario تبدیل به First-Class Entity می‌شود Scenario نباید فقط یک JSON پارامتر باشد. Python @dataclass(frozen=True) class Scenario: id: str name: str base_scenario_id: str | None data_version_id: str model_version: str planning_horizon_start: int planning_horizon_end: int objective_id: str demand_policy_id: str | None operating_regime: str changes: tuple["ScenarioChange", ...] 6. Scenario Change مثلاً: Python @dataclass(frozen=True) class ScenarioChange: entity_type: str entity_id: str attribute: str base_value: str | None new_value: str نمونه: Scenario: DOUBLE_TRACK_B17 Change: Block B17 track_type: SINGLE → DOUBLE یا: Scenario: ADD_100_WAGONS WagonPool WT02 available_count: 400 → 500 7. Scenario باید Immutable باشد این اصل برای Audit مهم است. به‌جای: Scenario-001 modified داشته باشیم: SC-001 CURRENT SC-002 ADD_WAGONS SC-003 DOUBLE_TRACK SC-004 ADD_WAGONS_DOUBLE_TRACK هر Run دقیقاً می‌داند با کدام Scenario اجرا شده است. 8. Data Version همین اصل برای Data: DataVersion ↓ Source Files ↓ Mapping Version ↓ Quality Report ↓ Canonical Dataset مثلاً: DV-2026-09-28-001 و Run: Run: R-2026-09-28-004 Data: DV-2026-09-28-001 Scenario: SC-004 Model: V2.0.0 9. Run باید قابل بازتولید باشد اصل: Result=f(DataVersion,Scenario,ModelVersion,SolverConfiguration) بنابراین اگر چهار مورد یکسان باشند، نتیجه باید تا حد ممکن deterministic باشد. 10. Run Lifecycle CREATED ↓ VALIDATING ↓ READY ↓ SOLVING ↓ VALIDATING_RESULT ↓ COMPLETED شاخه‌های خطا: FAILED_VALIDATION FAILED_SOLVER INVALID_RESULT CANCELLED TIMEOUT TIMEOUT نباید به معنی INFEASIBLE تلقی شود. 11. Capacity Planning Workflow صفحه اصلی Workbench: 1. Select Data ↓ 2. Select Scenario ↓ 3. Select Demand ↓ 4. Select Planning Horizon ↓ 5. Select Objective ↓ 6. Configure Operating Regime ↓ 7. Run Capacity ↓ 8. Validate ↓ 9. Inspect Capacity ↓ 10. Inspect Bottlenecks ↓ 11. Compare Scenarios 12. Dashboard صفحه Dashboard باید وضعیت کل شبکه را نشان دهد: ┌───────────────────────────────────────────────────────────┐ │ RAILWAY CAPACITY WORKBENCH │ ├───────────────────────────────────────────────────────────┤ │ │ │ Network Capacity Served Demand │ │ 8.4 Mt/day 7.1 Mt/day │ │ │ │ Operational Capacity Rolling Stock │ │ 1,240 trains/day 1,080 trains/day │ │ │ ├───────────────────────────────────────────────────────────┤ │ Binding Constraints │ │ │ │ B17 Single Track 100% │ │ J04 Junction 100% │ │ WT02 Wagon Pool 96% │ │ LP01 Locomotive Pool 100% │ │ │ ├───────────────────────────────────────────────────────────┤ │ Capacity Proof │ │ │ │ F FEASIBLE ✓ │ │ F + 1 INFEASIBLE ✓ │ │ │ └───────────────────────────────────────────────────────────┘ 13. Network Explorer این بخش یکی از مهم‌ترین صفحات محصول است. نمای شبکه: Station A │ │ B01 │ Station B / \ B02 B03 / \ Station C Station D \ / \ / B04 │ Station E روی هر Block: Capacity Utilization Train Count Direction Track Type Bottleneck Status نمایش داده می‌شود. 14. Network Heatmap مثلاً: BLOCK UTILIZATION STATUS --------------------------------------- B01 62% Normal B02 88% Near Binding B03 100% Binding B04 47% Available اما همان‌طور که در نسخه‌های قبلی گفتیم: Utilization ≠ Bottleneck باید Marginal Impact نیز محاسبه شود. 15. Capacity Inspector کاربر روی B03 کلیک می‌کند: BLOCK B03 ──────────────────────────── Track Type: SINGLE Direction: Bidirectional Daily Trains: 42 Capacity: 42 Utilization: 100% Slack: 0 Binding: YES Marginal Impact: +6 trains/day Primary Conflict: Opposing Direction Secondary Conflict: Station Crossing 16. Bottleneck Explorer این صفحه برای مدیریت بسیار مهم است. BOTTLENECK EXPLORER Rank by: [Marginal Capacity ▼] Resource Type Δ Capacity ---------------------------------------- B03 BLOCK +6 J04 JUNCTION +5 LP01 LOCO +3 WT02 WAGON +2 S12 STATION +1 این ranking در UI صرفاً نمایش نتایج محاسبه‌شده توسط مدل است، نه یک ارزیابی دستی. 17. Investment Scenario مثلاً مدیر می‌خواهد بررسی کند: اگر Block B03 دوخطه شود چه اتفاقی می‌افتد؟ Workbench: BASE SCENARIO ↓ Clone ↓ Change B03: SINGLE → DOUBLE ↓ Run ↓ Compare نتیجه: BASE SCENARIO --------------------------------------- Network Capacity X Network Capacity Y Served Freight A Served Freight B Train Count M Train Count N Empty Flow E Empty Flow F مقدار واقعی فقط از Run حاصل می‌شود. 18. Scenario Comparison نمای بسیار مهم: BASE SCENARIO DELTA -------------------------------------------------------- Capacity X Y Y-X Served Freight A B B-A Unserved Demand U V V-U Empty Wagon km E F F-E Loco Utilization L1 L2 L2-L1 B03 Utilization 100% 73% -27% J04 Utilization 100% 96% -4% 19. Sensitivity Engine Workbench باید بتواند سؤال‌هایی از این جنس را پاسخ دهد: What happens if: +1 station track? +5 locomotives? +100 wagons? +10% demand? +5 min headway? -5 min running time? Single → Double track? هر تغییر: Scenario → Full Re-solve → Validated Result → Delta نه یک محاسبه تقریبی جداگانه. 20. Batch Scenario برای تصمیم‌گیری، اجرای دستی 30 سناریو مناسب نیست. پس: Python @dataclass(frozen=True) class ScenarioBatch: id: str base_scenario_id: str scenario_ids: tuple[str, ...] execution_policy: str مثلاً: SC-01 Current SC-02 +50 Wagons SC-03 +100 Wagons SC-04 +2 Locos SC-05 +4 Locos SC-06 Double B03 SC-07 Double B03 +50 Wagons ... و همه با یک Data Version و Model Version اجرا شوند. 21. Experiment Matrix Workbench باید Matrix View داشته باشد: Scenario Wagons Locos B03 Capacity Served Freight Base 0 0 Single X A S1 +50 0 Single Y B S2 +100 0 Single Z C S3 0 +2 Single … … S4 +100 +2 Double … … این Matrix به‌خصوص برای Investment Planning ارزشمند است. 22. Objective Configuration Objective نباید hard-coded باشد. Python @dataclass(frozen=True) class ObjectiveDefinition: id: str name: str priorities: tuple[str, ...] weights: dict[str, float] مثلاً: MAX_SERVED_FREIGHT Priority: 1 Served Freight 2 Mandatory Demand 3 Empty Movement 4 Cost 5 Schedule Deviation 23. Hard vs Soft Constraints Workbench باید این distinction را در UI نشان دهد. Hard Train Length Block Conflict Track Capacity Wagon Availability Loco Availability Safety Window Soft Preferred Departure Preferred Route Preferred Train Formation Schedule Deviation Cost Preference در optimization، soft constraint باید penalty داشته باشد، نه اینکه silently شکسته شود. مفهوم hard/soft constraints و time windows نیز در سامانه‌های optimization رایج است. Google for Developers +1 24. Constraint Inspector کاربر باید بتواند ببیند: CONSTRAINT ──────────────────────── Type: SINGLE_TRACK_OPPOSING Resource: B03 Train A: T101 Train B: T205 Required Separation: 12 min Actual: 12 min Slack: 0 Status: BINDING این قابلیت برای اعتمادپذیری سیستم بسیار مهم است. 25. Capacity Proof Viewer یک صفحه اختصاصی: CAPACITY PROOF ──────────────────────── Objective: MAX SERVED FREIGHT Current: 1,720,000 ton/day F: FEASIBLE ✓ F + ε: INFEASIBLE ✓ Solver Status: INFEASIBLE Independent Validation: PASSED Proof: VALID ──────────────────────── Binding Constraints: B03 Single Track J04 Junction LP01 Locomotive Cycle 26. Proof نباید فقط یک Status باشد باید Evidence ذخیره شود: Python @dataclass(frozen=True) class ProofEvidence: run_id: str objective_value: int | float incumbent_value: int | float | None solver_status: str validation_status: str constraint_evidence: tuple[str, ...] generated_at: str 27. Explainability Engine هدف: کاربر نباید مجبور باشد مدل ریاضی را بخواند تا علت نتیجه را بفهمد. مثلاً: ظرفیت شبکه در سناریوی فعلی محدود شده است. عامل اصلی: Block B03 دلیل: Single-track opposing movements اثر: افزایش 1 قطار در این بخش باعث ایجاد conflict با Train 205 می‌شود. اثر بالقوه توسعه: در سناریوی Double Track، ظرفیت شبکه مجدداً محاسبه می‌شود. این متن باید از Evidence ساخته شود. نه از متن ثابت. 28. Traceability Graph یک قابلیت بسیار قدرتمند: Market Request │ ▼ Demand D-103 │ ▼ Freight Flow FF-12 │ ▼ Train Service TS-204 │ ├──────── Wagon Formation │ ├──────── Locomotive Assignment │ ▼ Route R-17 │ ▼ Block B03 │ ▼ Conflict C-882 │ ▼ Capacity Limit کاربر بتواند روی هر node کلیک کند. 29. Data Quality Workbench این بخش برای پروژه شما حیاتی است چون Access/Excel داده واقعی و بعضاً semantic uncertainty دارد. Dashboard: DATA QUALITY Source: aaa.accdb Records: 12,842 Mapped: 12,630 Warnings: 172 Rejected: 40 Unverified fields: 6 30. Field Confidence برای هر field: Field Confidence -------------------------------- TrainNo VERIFIED StationName VERIFIED Sequence VERIFIED time_in VERIFIED time_take VERIFIED RequiredWait PROVISIONAL Kilometerage HIGH MaxSpeed PROVISIONAL Distance UNTRUSTED sumDistancezz UNKNOWN seir VERIFIED این دقیقاً با اصل قبلی: No Verified Mapping → No Production Use هماهنگ است. 31. Mapping Workbench کاربر Data Engineer بتواند mapping را ببیند: Source Field ↓ Canonical Field ↓ Transformation ↓ Confidence ↓ Validation Rule مثلاً: seir ↓ baseline_running_time_to_next ↓ integer minutes ↓ VERIFIED ↓ arrival_next = departure_current + seir 32. Baseline vs Optimized Schedule Workbench باید این دو را کنار هم نشان دهد. TRAIN 100 Baseline Optimized -------------------------------- Gar 09:00 09:00 Sakheh 09:34 09:35 Bagh 10:12 10:14 ... و: Schedule Deviation: +4 min اما در Capacity Mode ممکن است deviation مجاز متفاوت باشد. 33. Time-Space Diagram یکی از مهم‌ترین Visualizationها: Time ↑ │ / Train A │ / │ / │ /──── Station B │ / │ / Train B │ / └────────────────────────→ Distance برای هر Train: movement dwell crossing conflict waiting block occupation نمایش داده می‌شود. 34. Conflict Visualization مثلاً: Train A ────────────────┐ │ Conflict Train B ────────────────┘ و کاربر روی conflict کلیک می‌کند: Conflict C-102 Type: OPPOSING_DIRECTION Block: B03 Required: 12 min Actual: 12 min 35. Operational Regime Explorer در V1.7 داشتیم: STRICT_ALTERNATING DIRECTIONAL_BATCH MIXED V2.0 این را تبدیل به Scenario Dimension می‌کند. Operating Regime [ MIXED ▼ ] Candidate Regimes: - Strict Alternating - Directional Batch - Mixed هر regime یک Run مستقل خواهد داشت. 36. Market Allocation Screen Marketplace باید نتیجه Capacity Engine را مصرف کند. DEMAND ALLOCATION OD Demand Capacity Allocated Unserved ---------------------------------------------------------------- Tehran-Khowaf 1000 kt 820 kt 820 kt 180 kt Tehran-Zarand 700 kt 510 kt 510 kt 190 kt Aprin-Khowaf 500 kt 370 kt 370 kt 130 kt این همان interface اصلی: Marketplace ↕ Market API ↕ Capacity API است. 37. Capacity Offer Capacity Engine باید Offer تولید کند: Python @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str time_window: str train_capacity: int freight_capacity_t: float confidence: str proof_id: str Marketplace فقط Capacity Offer معتبر را دریافت می‌کند. 38. Capacity Offer نباید با Raw Capacity یکی باشد مثلاً: Route Capacity: 68 trains/day ولی: OD Tehran-Khowaf: 31 trains/day به علت: Wagon Loco Demand Route Time Window Shared Resource پس Marketplace باید profiled capacity دریافت کند، نه یک عدد کلی. 39. API Architecture V2.0 API: /api/v1/ ├── data/ ├── infrastructure/ ├── stations/ ├── routes/ ├── trains/ ├── demand/ ├── wagons/ ├── locomotives/ ├── formations/ │ ├── scenarios/ ├── runs/ ├── capacity/ ├── network/ ├── allocations/ ├── bottlenecks/ ├── explanations/ ├── proofs/ │ └── marketplace/ ├── demands ├── capacity-offers └── allocations 40. Event Architecture برای Production بهتر است بعضی عملیات asynchronous باشند: POST /runs ↓ Run Created ↓ Queue ↓ Solver Worker ↓ Result ↓ Notification چون Network Optimization ممکن است طولانی باشد. در optimization واقعی، runtime باید قابل کنترل و قابل ثبت باشد؛ time limit نیز باید بخشی از Solver Configuration باشد، نه یک مقدار مخفی. Google for Developers 41. Solver Worker API ↓ Run Manager ↓ Job Queue ↓ Optimization Worker ├── Build Model ├── Solve ├── Validate ├── Explain └── Persist Result API نباید thread را برای یک solve سنگین block کند. 42. Solver Configuration Python @dataclass(frozen=True) class SolverConfiguration: time_limit_seconds: int random_seed: int num_workers: int absolute_gap: float | None = None relative_gap: float | None = None log_search_progress: bool = False این configuration باید همراه Run ذخیره شود. 43. Production Run Record RUN ────────────────────────── Run ID: RUN-2026-00182 Model: 2.0.0 Data Version: DV-2026-09-28-014 Scenario: SC-2026-044 Objective: MAX_SERVED_FREIGHT Solver: CP-SAT Time Limit: 1800 sec Workers: 8 Seed: 42 Status: COMPLETED Validation: PASSED Proof: VALID 44. Security V2.0 باید Role-Based Access Control داشته باشد. Roles ADMIN DATA_ENGINEER CAPACITY_ANALYST PLANNER MARKET_OPERATOR VIEWER مثلاً: DATA_ENGINEER → edit mapping CAPACITY_ANALYST → run scenarios PLANNER → modify demand assumptions MARKET_OPERATOR → consume capacity offers VIEWER → read results 45. Audit Log هر تغییر: User Timestamp Entity Old Value New Value Reason Scenario مثلاً: User: analyst01 Entity: Block B03 Field: track_type OLD: SINGLE NEW: DOUBLE Scenario: SC-44 Reason: Investment Scenario 46. Data Lineage برای هر نتیجه: Result ↓ Run ↓ Scenario ↓ Data Version ↓ Source Dataset ↓ Source File ↓ Source Record این برای پروژه‌ای با داده Access/Excel بسیار مهم است. 47. PostgreSQL Domain V2.0 database schema تقریباً: infrastructure stations station_tracks blocks junctions junction_movements routes route_segments train_types trains train_runs train_station_calls wagon_types wagons wagon_pools wagon_inventory wagon_cycles locomotive_types locomotives locomotive_pools locomotive_cycles od_pairs demands freight_flows wagon_requirements formations formation_items formation_locomotives scenarios scenario_changes objectives policies investments runs run_resources run_results schedules block_movements station_movements conflicts capacity_profiles capacity_offers allocations bottlenecks binding_constraints proofs proof_evidence data_versions source_files source_records mapping_versions quality_reports audit_logs 48. نسخه‌بندی مدل سه version باید جدا باشند: Data Version Model Version Scenario Version مثلاً: Data: DV-014 Model: V2.0.3 Scenario: SC-044 و Run: R-9182 49. Golden Test Suite V2.0 باید یک مجموعه تست ثابت داشته باشد. Test 1 — Single Track A-B Single Opposing trains Test 2 — Double Track A-B Double Directional capacity Test 3 — Junction Two conflicting movements Test 4 — Station Length Train > usable track Test 5 — Wagon Cycle Loaded + empty return Test 6 — Locomotive Cycle Turnback Test 7 — Multi-OD OD1 + OD2 Shared block Test 8 — Shared Wagon Pool OD1 + OD2 Shared wagon inventory Test 9 — Capacity Proof F feasible F+1 infeasible Test 10 — UNKNOWN Solver UNKNOWN → Proof invalid 50. Production Acceptance Test مهم‌ترین E2E Test: Access ↓ Staging ↓ Mapping ↓ Quality ↓ Canonical ↓ Demand ↓ Formation ↓ Wagon ↓ Loco ↓ Network ↓ Schedule ↓ Validation ↓ Capacity ↓ Proof ↓ Marketplace Capacity Offer اگر این pipeline کامل بدون manual intervention اجرا شود، می‌توانیم بگوییم هسته محصول واقعاً end-to-end شده است. 51. Roadmap اجرایی V2.0 من V2.0 را به 7 Work Package تقسیم می‌کنم: WP20.1 — Workbench Foundation Project Authentication RBAC Run Manager Scenario Data Version Audit WP20.2 — Data Workbench Access Excel Mapping Quality Reconciliation Lineage WP20.3 — Planning Workbench Infrastructure Demand Train Formation Wagon Locomotive WP20.4 — Optimization Workbench Route Schedule Network Multi-OD Shared Resources WP20.5 — Capacity & Proof Capacity Search F/F+1 Proof Bottleneck Marginal Capacity Sensitivity WP20.6 — Decision Support Scenario Investment Comparison What-if WP20.7 — Marketplace Integration Capacity Offer Allocation Demand Feedback Traceability 52. ترتیب پیاده‌سازی پیشنهادی نکته مهم: UI را قبل از تثبیت Engine نسازیم. ترتیب: 1. Domain Contracts ↓ 2. Database Schema ↓ 3. Data Version / Scenario ↓ 4. V1.7 Scheduler ↓ 5. V1.8 Rolling Stock ↓ 6. V1.9 Network ↓ 7. Independent Validation ↓ 8. Proof ↓ 9. Explanation ↓ 10. API ↓ 11. UI ↓ 12. Marketplace این ترتیب ریسک rework را به‌شدت کاهش می‌دهد. 53. یک تصمیم مهم برای فناوری UI Backend: Python FastAPI SQLAlchemy PostgreSQL OR-Tools CP-SAT Frontend پیشنهادی: React TypeScript و برای visualization: Network Graph Time-Space Diagram Gantt Heatmap Scenario Matrix اما UI نباید هیچ business rule مربوط به capacity را خودش پیاده کند. 54. اصل طلایی Frontend Frontend فقط: Display Filter Edit Input Submit Scenario Request Run Display Result Backend مسئول: Validation Calculation Optimization Proof Explanation یعنی اگر UI بگوید: Capacity = 57 این عدد را خودش محاسبه نکرده؛ از: Validated CapacityResult گرفته است. 55. Architecture نهایی محصول در این مرحله معماری کل پروژه به شکل زیر تثبیت می‌شود: ┌───────────────────────────────────────────────────────────────┐ │ MARKETPLACE │ └────────────────────────────┬──────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────┐ │ MARKET / DEMAND API │ └────────────────────────────┬──────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────┐ │ RAILWAY CAPACITY WORKBENCH │ │ │ │ Data Scenario Planning Network Results │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ DOMAIN MODEL │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ ▼ ▼ ▼ │ │ Formation Engine Rolling Stock Scheduler │ │ Engine V1.7 │ │ │ │ │ │ │ └────────────────────┼────────────────────┘ │ │ ▼ │ │ NETWORK OPTIMIZATION │ │ │ │ │ ▼ │ │ INDEPENDENT VALIDATOR │ │ │ │ │ ┌────────────┼────────────┐ │ │ ▼ ▼ ▼ │ │ Capacity Bottleneck Proof │ │ │ │ │ │ │ └────────────┼────────────┘ │ │ ▼ │ │ EXPLANATION ENGINE │ └─────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────┐ │ CAPACITY API │ └────────────────────────────┬──────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────┐ │ MARKETPLACE │ │ Capacity Offer → Allocation │ └───────────────────────────────────────────────────────────────┘ 56. تعریف نهایی محصول با رسیدن به این نقطه، تعریف رسمی پروژه را پیشنهاد می‌کنم این‌گونه تثبیت کنیم: Railway Capacity Planning Workbench is an OD-centric, network-aware, time-space railway capacity planning and optimization platform that integrates infrastructure capacity, operational scheduling, train formation, wagon cycles, locomotive cycles, multi-OD demand allocation, shared-resource constraints, scenario analysis, capacity proof, bottleneck analysis, and marketplace allocation through a versioned and auditable canonical railway data model. و اصل مرکزی آن: Capacity=Feasible Infrastructure∩Feasible Schedule∩Feasible Formation∩Feasible WagonCycle∩Feasible LocomotiveCycle∩Feasible Network∩Demand/Policy ​ و برای شبکه: C N ​ =max ⎩ ⎨ ⎧ ​ od,r,t ∑ ​ Q od,r ​ F od,r,t ​ ⎭ ⎬ ⎫ ​ ​ subject to the complete integrated railway system constraints. این نقطه‌ای است که تمام کارهای قبلی — از Access/Excel واقعی، seir و Kilometerage، OD-centric Train Formation، V1.7 Scheduler، V1.8 Wagon/Loco Cycle، V1.9 Multi-OD Network — در یک محصول واحد جمع می‌شوند. مهم‌تر اینکه Marketplace دیگر یک سیستم جانبی نیست؛ Demand و Allocation آن دو سر زنجیره Capacity Engine را تشکیل می‌دهند. --- ## ادامه 41 — پرسش کاربر (`message-id:d4c50694-6976-4c16-8ab6-bb5540d4ff25`) احسنت --- ## ادامه 41 — پاسخ دستیار (`message-id:8b1e5334-c9ea-40f1-bad4-b1cc536e1f6e`) ممنونم! خوشحالم که مسیر مورد تأییدت است. این پروژه الان از یک مدل محاسبات ظرفیت عبور کرده و عملاً به سمت یک Railway Capacity Planning & Optimization Workbench کامل می‌رود؛ یعنی زنجیره‌ی: Data → Canonical Model → Demand → Formation → Wagon/Loco → Scheduling → Network Optimization → Capacity Proof → Bottleneck → Marketplace Allocation به‌صورت یک سیستم یکپارچه. هر زمان بگویی «ادامه بده»، منطقی‌ترین گام بعدی را از همین‌جا می‌گیریم: V2.1 — Production Implementation و وارد پیاده‌سازی واقعی Codebase، Database Schema، API، Solver orchestration و تست‌های End-to-End می‌شویم. --- ## ادامه 42 — پرسش کاربر (`message-id:32cc2132-0366-4081-98c6-fb12c3a804d0`) ادامه بده لطفا --- ## ادامه 42 — پاسخ دستیار (`message-id:5e8bee94-f5a1-4427-8c61-601f7ecf34f0`) حتماً. از همین‌جا V2.1 — Production Implementation Foundation را ادامه می‌دهیم؛ بدون بازطراحی دوباره‌ی معماری قبلی. در این مرحله هدف این است که معماری V2.0 را به یک Core واقعی، قابل اجرا و قابل توسعه تبدیل کنیم. برای ORM نیز از الگوی Declarative مدرن SQLAlchemy 2.x استفاده می‌کنیم که مستقیماً برای typed mappings طراحی شده است. SQLAlchemy Documentation +1 V2.1 — Production Implementation Foundation 1. هدف نسخه V2.1 باید این زنجیره را واقعاً executable کند: Source ↓ DataVersion ↓ Canonical Model ↓ Scenario ↓ Run ↓ Demand ↓ Formation ↓ Wagon/Loco Feasibility ↓ Operational Scheduling ↓ Network Optimization ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck / Explanation ↓ Capacity Offer بنابراین V2.1 صرفاً UI یا API نیست؛ هسته‌ی قابل اتکای محصول است. 2. Project Structure نهایی V2.1 ساختار را به این شکل تثبیت می‌کنیم: railway_capacity_workbench/ │ ├── pyproject.toml ├── README.md ├── .env.example ├── docker-compose.yml │ ├── alembic.ini ├── alembic/ │ ├── env.py │ └── versions/ │ ├── config/ │ ├── development.yml │ ├── test.yml │ └── production.yml │ ├── app/ │ │ │ ├── main.py │ │ │ ├── config/ │ │ ├── settings.py │ │ └── logging.py │ │ │ ├── db/ │ │ ├── base.py │ │ ├── session.py │ │ └── models/ │ │ │ ├── domain/ │ │ ├── infrastructure/ │ │ ├── station/ │ │ ├── route/ │ │ ├── train/ │ │ ├── demand/ │ │ ├── wagon/ │ │ ├── locomotive/ │ │ ├── formation/ │ │ ├── schedule/ │ │ ├── network/ │ │ ├── scenario/ │ │ ├── capacity/ │ │ └── result/ │ │ │ ├── adapters/ │ │ ├── access/ │ │ ├── excel/ │ │ ├── marketplace/ │ │ └── gis/ │ │ │ ├── staging/ │ ├── mapping/ │ ├── reconciliation/ │ ├── quality/ │ │ │ ├── engines/ │ │ ├── formation/ │ │ ├── wagon_cycle/ │ │ ├── locomotive_cycle/ │ │ ├── scheduling/ │ │ ├── network/ │ │ ├── capacity/ │ │ └── scenario/ │ │ │ ├── validation/ │ │ ├── data/ │ │ ├── formation/ │ │ ├── schedule/ │ │ ├── rolling_stock/ │ │ ├── network/ │ │ └── capacity/ │ │ │ ├── explanation/ │ │ ├── bottleneck.py │ │ ├── proof.py │ │ ├── narrative.py │ │ └── traceability.py │ │ │ ├── services/ │ │ ├── ingestion.py │ │ ├── scenario.py │ │ ├── run_manager.py │ │ ├── capacity.py │ │ └── allocation.py │ │ │ └── api/ │ ├── routes/ │ └── schemas/ │ ├── tests/ │ ├── unit/ │ ├── integration/ │ ├── golden/ │ └── e2e/ │ └── scripts/ ├── inspect_access.py ├── inspect_excel.py ├── ingest.py └── run_capacity.py این separation مهم است: Domain نباید SQLAlchemy، FastAPI یا OR-Tools را بشناسد. Solver هم نباید مستقیماً با database کار کند. 3. اولین قرارداد حیاتی: DataVersion تمام محاسبات باید به نسخه‌ی داده وابسته باشند. Python from dataclasses import dataclass from datetime import datetime @dataclass(frozen=True) class DataVersion: id: str name: str source_type: str source_uri: str source_hash: str created_at: datetime status: str quality_status: str مثلاً: DV-2026-09-28-001 می‌تواند به: Access: aaa.accdb Excel: REPORTKholase... وابسته باشد. اصل مهم: Same DataVersion + Same Scenario + Same ModelVersion + Same SolverConfiguration = Reproducible Run 4. Scenario باید Immutable باشد Scenario نباید object mutable عادی باشد. Python @dataclass(frozen=True) class Scenario: id: str name: str base_scenario_id: str | None data_version_id: str model_version: str planning_start: int planning_end: int objective_id: str demand_policy_id: str | None operating_regime: str changes: tuple["ScenarioChange", ...] و: Python @dataclass(frozen=True) class ScenarioChange: entity_type: str entity_id: str attribute: str base_value: str | None new_value: str بنابراین: BASE │ ├── Scenario A │ ├── Scenario B │ └── Scenario C و هیچ‌کدام Base را mutate نمی‌کنند. 5. Run Contract Run قلب سیستم اجرایی است. Python from dataclasses import dataclass from enum import Enum class RunStatus(str, Enum): CREATED = "CREATED" VALIDATING = "VALIDATING" READY = "READY" SOLVING = "SOLVING" VALIDATING_RESULT = "VALIDATING_RESULT" COMPLETED = "COMPLETED" FAILED_VALIDATION = "FAILED_VALIDATION" FAILED_SOLVER = "FAILED_SOLVER" INVALID_RESULT = "INVALID_RESULT" TIMEOUT = "TIMEOUT" CANCELLED = "CANCELLED" @dataclass class CapacityRun: id: str scenario_id: str data_version_id: str model_version: str status: RunStatus solver_seed: int solver_workers: int time_limit_seconds: int objective_value: float | None = None Run نباید فقط یک solve() ساده باشد. Lifecycle: CREATED ↓ VALIDATING ↓ READY ↓ SOLVING ↓ VALIDATING_RESULT ↓ COMPLETED 6. Canonical Railway Model هسته‌ی Domain از database جدا می‌ماند. برای مثال: Python @dataclass(frozen=True) class Station: id: str name: str usable_length_m: float Python @dataclass(frozen=True) class StationTrack: id: str station_id: str usable_length_m: float bidirectional: bool Python @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int switch_time: int clearing_time: int و: DirectedPath ↓ PhysicalBlock ↓ MovementResource یعنی: Directed Path ≠ Physical Resource این تفکیک برای Single Track حیاتی است. 7. Database Layer در SQLAlchemy 2.x از DeclarativeBase و typed Mapped استفاده می‌کنیم؛ این همان mapping style مدرن توصیه‌شده در SQLAlchemy 2.x است. SQLAlchemy Documentation +1 مثلاً: Python from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass سپس: Python from sqlalchemy import String from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column class DataVersionModel(Base): __tablename__ = "data_version" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) name: Mapped[str] = mapped_column( String(255), nullable=False, ) source_type: Mapped[str] = mapped_column( String(50), nullable=False, ) source_uri: Mapped[str] = mapped_column( String(1000), nullable=False, ) source_hash: Mapped[str] = mapped_column( String(128), nullable=False, index=True, ) status: Mapped[str] = mapped_column( String(50), nullable=False, ) quality_status: Mapped[str] = mapped_column( String(50), nullable=False, ) این layer فقط Persistence است. Domain از آن خبر ندارد. 8. Database Domains Schema منطقی را به چند حوزه تقسیم می‌کنیم. Infrastructure station station_track physical_block junction junction_movement junction_conflict operational_window Train train train_run train_station_call train_operational_profile Route route route_segment route_block route_station Rolling Stock wagon_type wagon wagon_pool wagon_inventory wagon_requirement wagon_cycle locomotive_type locomotive locomotive_assignment locomotive_cycle Formation train_formation train_formation_item train_formation_locomotive Demand od_pair market_demand freight_flow Planning scenario scenario_change objective policy investment Execution capacity_run run_train run_schedule run_block_occupancy run_station_occupancy run_conflict run_resource_usage Result capacity_result capacity_profile capacity_offer capacity_proof proof_evidence bottleneck allocation 9. Run Result نباید فقط یک عدد باشد اشتباه بزرگ این است که خروجی engine این باشد: JSON { "capacity": 42 } خروجی واقعی باید چیزی شبیه این باشد: JSON { "run_id": "RUN-001", "status": "COMPLETED", "capacity": { "train_count": 42, "freight_tons": 84000 }, "capacity_profiles": { "infrastructure": 55, "operational": 48, "rolling_stock": 44, "transportable": 43, "allocated": 42 }, "proof": { "f": 42, "f_plus_one": 43, "f_status": "FEASIBLE", "f_plus_one_status": "INFEASIBLE", "validated": true, "proof_valid": true } } اما مهم‌تر از همه: JSON { "binding_constraints": [], "bottlenecks": [], "schedule": [], "conflicts": [], "resource_usage": [], "explanation": [] } 10. Scheduler Interface Solver را مستقیماً به Service وصل نمی‌کنیم. Interface: Python class SchedulingEngine: def solve( self, infrastructure, trains, paths, profiles, scenario, config, ) -> ScheduleResult: ... و: Python @dataclass class ScheduleResult: status: str train_movements: list block_occupancies: list station_occupancies: list junction_occupancies: list conflicts: list objective_value: float | None solver_status: str این abstraction بعداً اجازه می‌دهد: CP-SAT MILP Heuristic Simulation بدون تغییر Application Layer جایگزین شوند. 11. Constraint Architecture Scheduler را نیز یک monolithic solver نمی‌سازیم. OperationalSchedulingModel │ ├── PrecedenceConstraints ├── BlockConstraints ├── HeadwayConstraints ├── StationTrackConstraints ├── JunctionConstraints ├── OperationalWindowConstraints ├── TrainTimeWindowConstraints └── Objective مثلاً: Python class BlockConstraintBuilder: def add_constraints(self, model, context): ... و: Python class HeadwayConstraintBuilder: def add_constraints(self, model, context): ... این طراحی اجازه می‌دهد یک constraint جدید مثل: Crew Change Brake Test Fueling Prayer Window Terminal Handling بدون بازنویسی Solver اضافه شود. 12. Single Track باید واقعاً Physical Resource باشد برای block: A ───────── B اگر Single Track باشد: Train 1 A→B Train 2 B→A هر دو روی: BLOCK(A,B) resource قرار می‌گیرند. بنابراین: Resource(B,d)=B برای Single Track. ولی در Double Track: Resource(B,Forward)=B:F و: Resource(B,Reverse)=B:R این همان چیزی است که باعث می‌شود مدل از یک timetable ساده به یک time-space resource model واقعی تبدیل شود. 13. Capacity Engine Capacity Engine نباید خودش timetable بسازد. وظیفه: Candidate F ↓ Build Scenario ↓ Build Scheduling Model ↓ Solve ↓ Independent Validation ↓ Feasible? سپس: F F+1 بررسی می‌شود. تعریف: C r ​ =max{F∣Schedule(F) is feasible} اما شرط مهم: Feasible(F)∧Validated(F)∧Infeasible(F+1) در غیر این صورت: Capacity = NOT PROVEN و نه اینکه: Capacity = F 14. UNKNOWN ≠ INFEASIBLE این مورد را در V2.1 به‌عنوان invariant ثبت می‌کنیم: Python if status == "INFEASIBLE": proven = True elif status == "UNKNOWN": proven = False elif status == "TIME_LIMIT": proven = False بنابراین: Solver Timeout هرگز به معنای: F+1 impossible نیست. 15. Independent Validator این بخش عمداً از Solver جداست. مثلاً Solver می‌گوید: Train 100: Block B03 = 120–140 Validator دوباره بررسی می‌کند: B03 occupancy 120–140 در مقابل همه‌ی قطارهای دیگر. سپس: same direction? opposite direction? headway? switch time? clearing? را مستقل محاسبه می‌کند. همین برای: Station Track Junction Station Length Dwell Running Time Operational Window Earliest Departure Latest Arrival انجام می‌شود. این separation یکی از مهم‌ترین ویژگی‌های Production Engine است. 16. Wagon/Loco بعد از Scheduler قرار نمی‌گیرند در معماری نهایی این ترتیب را داریم: Demand ↓ Train Service Candidate ↓ Formation Feasibility ↓ Wagon Feasibility ↓ Locomotive Feasibility ↓ Train Path ↓ Schedule نه: Schedule ↓ بعداً ببینیم واگن داریم یا نه چون ممکن است: Infrastructure Capacity = 50 trains/day Operational Capacity = 45 Wagon Capacity = 37 Loco Capacity = 40 Demand = 60 در نتیجه: Integrated Capacity = 37 نه 50. 17. Network Engine Network Engine ورودی‌اش دیگر route منفرد نیست. OD1 → Route A → Route B OD2 → Route A → Route C OD3 → Route B و shared resources: B03 Station S1 Junction J2 WagonPool WP1 LocomotivePool LP1 Terminal T1 EmptyBuffer EB1 داریم. مدل: C N ​ =max od,r,t ∑ ​ Q od,r ​ F od,r,t ​ با قیود: Q od,r ​ F od,r,t ​ ≤D od,t ​ و: r ∑ ​ a r,g ​ F r ​ ≤C g ​ به‌علاوه‌ی قیود زمانی واقعی. 18. Marketplace Integration Marketplace نباید Solver را بشناسد. رابط: Marketplace ↓ Marketplace Adapter ↓ Canonical Demand ↓ Capacity Engine ↓ Capacity Offer ↓ Marketplace مثلاً: JSON { "od_pair": "TEH-RAJ", "commodity": "STEEL", "requested_tons": 12000, "time_window": "2026-10-01/2026-10-07" } تبدیل می‌شود به: Market Demand ↓ Transportable Demand ↓ Capacity Allocation و خروجی: JSON { "requested_tons": 12000, "transportable_tons": 10800, "allocated_tons": 9600, "unserved_tons": 2400 } یعنی: D market ​  =D transportable ​  =D allocated ​ 19. Capacity Offer خروجی مناسب Marketplace: Python @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str time_window: str train_capacity: int freight_capacity_t: float confidence: str proof_id: str مثلاً: OD: Tehran → Rasht Route: R-TEH-RAS Window: 08:00–20:00 Train Capacity: 6 Freight Capacity: 7,200 t Confidence: PROVEN Proof: PROOF-0182 بنابراین Marketplace فقط یک عدد خام «ظرفیت مسیر» دریافت نمی‌کند. 20. Traceability یکی از مهم‌ترین قسمت‌های V2.1: Market Request │ ▼ Demand │ ▼ Freight Flow │ ▼ Wagon Requirement │ ▼ Train Service │ ▼ Formation │ ├──── Wagon │ └──── Locomotive │ ▼ Route │ ▼ Schedule │ ▼ Capacity │ ▼ Allocation هر node باید source_id / entity_id قابل ردیابی داشته باشد. در نتیجه planner می‌تواند بپرسد: چرا این 2,400 تن demand سرو نشد؟ و سیستم باید بتواند برگردد: Unserved Demand ↓ Capacity Allocation ↓ Route Capacity ↓ B03 Opposing Movement ↓ Single Track ↓ Switch Time ↓ F+1 Infeasibility Proof این همان چیزی است که سیستم را از یک calculator به decision-support system تبدیل می‌کند. 21. اولین Vertical Slice واقعی V2.1 قبل از ساخت کل سیستم، یک مسیر کامل را executable می‌کنیم: Access / Excel ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Canonical TrainRun ↓ Directed Route ↓ Infrastructure ↓ Operational Scheduler ↓ Independent Validator ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck ↓ API Result در این Vertical Slice هنوز: Multi-OD Marketplace Advanced rolling-stock optimization را کامل نمی‌کنیم. ابتدا باید این مسیر بدون شک کار کند. 22. Golden Test شماره 1 اولین تست Production باید بسیار کوچک ولی سخت باشد. Infrastructure A ── B ── C هر دو block: SINGLE Train 1 A → C Train 2 C → A اگر Train 1: A → B : 20 min B → C : 20 min باشد، scheduler باید crossing/conflict را تشخیص دهد. اگر: Train 1: A → B → C Train 2: C → B → A همزمان وارد B03 شوند، schedule باید infeasible باشد. 23. Golden Test شماره 2 — Double Track همان topology: A ══ B ══ C اما: DOUBLE و: Forward Resource Reverse Resource جدا هستند. در این حالت opposing-direction movement لزوماً conflict ندارد. اما: Station Junction Terminal هنوز می‌توانند bottleneck باشند. این تست دقیقاً ثابت می‌کند که: Double Track به معنی «کل مسیر بدون محدودیت» نیست. 24. Golden Test شماره 3 — Station Length مثلاً: Train length = 750 m Station Track = 650 m باید: INFEASIBLE شود. ولی اگر: Alternative Track = 900 m وجود داشته باشد: FEASIBLE است. 25. Golden Test شماره 4 — Wagon Cycle فرض: Demand = 10 trains Wagons required/train = 20 Available wagons = 100 Cycle = 2 days ظرفیت wagon فقط با: 100 / 20 = 5 trains تمام نمی‌شود؛ cycle time وارد مدل می‌شود. اگر هر wagon طی planning horizon بتواند چند بار استفاده شود: N available ​ × CycleTime Horizon ​ حد ظرفیت را تغییر می‌دهد. بنابراین Wagon Capacity ذاتاً time-dependent است. 26. Golden Test شماره 5 — Locomotive Cycle همین منطق برای locomotive: Outbound ↓ Turnback ↓ Return ↓ Fueling ↓ Maintenance ↓ Available اگر cycle اجازه ندهد: Train 12 شروع شود، حتی اگر infrastructure آزاد باشد، ظرفیت integrated کاهش می‌یابد. 27. Golden Test شماره 6 — Capacity Proof برای هر Capacity Result: F = 20 باید: 20 → FEASIBLE 20 → VALIDATED 21 → INFEASIBLE باشد. اگر: 21 → UNKNOWN باشد: Capacity = 20 Proof = INVALID و UI باید بنویسد: Capacity Not Proven نه: Capacity = 20 28. Run Orchestration Application Service نهایی: Python class CapacityService: def run(self, request): data = self.load_data_version( request.data_version_id ) scenario = self.load_scenario( request.scenario_id ) self.validate_inputs( data, scenario ) candidates = self.build_candidates( data, scenario ) formations = self.build_formations( candidates ) rolling_stock = self.check_rolling_stock( formations ) schedule = self.schedule( formations, rolling_stock, scenario ) validation = self.validate_schedule( schedule ) capacity = self.search_capacity( schedule, validation ) proof = self.build_proof( capacity ) bottlenecks = self.analyze_bottlenecks( schedule, validation ) explanation = self.explain( proof, bottlenecks ) return CapacityRunResult( capacity=capacity, proof=proof, bottlenecks=bottlenecks, explanation=explanation, ) این Service همان orchestration boundary است. 29. API Contract اولین endpoint: http POST /api/v1/runs Request: JSON { "data_version_id": "DV-001", "scenario_id": "SC-001", "time_limit_seconds": 300, "random_seed": 1, "workers": 1 } Response: JSON { "run_id": "RUN-001", "status": "CREATED" } بعد: http GET /api/v1/runs/RUN-001 و: JSON { "run_id": "RUN-001", "status": "COMPLETED", "capacity": 42, "capacity_proven": true } 30. API نباید Solver را synchronously نگه دارد برای Production: POST /runs ↓ RUN CREATED ↓ Job Queue ↓ Worker ↓ Solver ↓ Validation ↓ Result نه: HTTP Request ↓ 5-minute Solver ↓ HTTP Response برای MVP development می‌توان synchronous mode داشت، ولی contract باید از ابتدا asynchronous-compatible باشد. 31. V2.1 Definition of Done نسخه V2.1 زمانی تمام است که این سناریو واقعاً اجرا شود: Access/Excel ↓ Data Version ↓ Quality Gate ↓ Canonical Model ↓ Scenario ↓ Train Formation ↓ Wagon Feasibility ↓ Locomotive Feasibility ↓ Directed Path ↓ Single/Double Track Scheduler ↓ Station Constraints ↓ Junction Constraints ↓ Operational Windows ↓ Schedule ↓ Independent Validation ↓ Capacity Search ↓ F / F+1 Proof ↓ Bottleneck ↓ Explanation ↓ API و هیچ مرحله‌ای fake/mock نباشد، مگر در Golden Tests که داده‌ی synthetic و صریحاً علامت‌گذاری‌شده دارند. 32. ترتیب پیاده‌سازی بعدی از اینجا به بعد ترتیب کار را این‌گونه قفل می‌کنیم: V2.1-A Canonical Contracts + Database Foundation V2.1-B Real Data Ingestion + Data Versioning V2.1-C Operational Scheduler V1.7 به‌صورت Production Module V2.1-D Independent Validation + Capacity Proof V2.1-E Wagon/Loco/Formation Integration V2.1-F Network Multi-OD V2.1-G API + Async Run Manager V2.1-H Workbench UI V2.1-I Marketplace Adapter تصمیم مهم فنی در این مرحله من پیشنهاد می‌کنم دیگر مستندات معماری جدید تولید نکنیم و وارد Codebase واقعی شویم. اولین deliverable اجرایی باید این باشد: V2.1-A Canonical Domain + SQLAlchemy Models + Alembic Initial Migration + PostgreSQL/SQLite Configuration + DataVersion + Scenario + CapacityRun + Result Contracts + Repository Layer + Unit Tests بعد از آن، مستقیم می‌رویم سراغ V2.1-B: اتصال واقعی Access/Excel به همین مدل Canonical و سپس Scheduler. این ترتیب باعث می‌شود هسته‌ی سیستم از همان ابتدا بر مبنای versioned data + reproducible scenarios + auditable runs ساخته شود، نه اینکه بعداً این قابلیت‌ها را به یک Solver آماده اضافه کنیم. SQLAlchemy 2.x نیز برای این لایه‌ی typed declarative mapping کاملاً مناسب است. SQLAlchemy Documentation +1 --- ## ادامه 43 — پرسش کاربر (`message-id:c24d3bb5-acf6-4e8e-8b9d-41444112e058`) بله لطفا --- ## ادامه 43 — پاسخ دستیار (`message-id:684f6532-f3cf-4806-b4c1-2742166aa037`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 44 — پرسش کاربر (`message-id:e044b2d0-7347-425e-a307-14640cc6b836`) لطفا به صورت متن همینجا ادامه بده --- ## ادامه 44 — پاسخ دستیار (`message-id:d24d96b4-3b3e-4e49-b76f-5058f7eb1ff9`) حتماً. ادامه را کاملاً متنی و اجرایی می‌بریم جلو. در این مرحله V2.1-A را به‌صورت یک Foundation Specification + Code Skeleton قابل تبدیل مستقیم به فایل تکمیل می‌کنیم. یک نکته را هم تثبیت کنیم: انتخاب SQLAlchemy 2.x با DeclarativeBase، Mapped و mapped_column() انتخاب مناسبی برای این لایه است؛ مستندات رسمی فعلی SQLAlchemy همین الگوی Declarative و type-annotated mapping را پوشش می‌دهند. SQLAlchemy Documentation +1 V2.1-A — Canonical Domain + Database Foundation 1. اصل معماری از این نسخه سه مدل را عمداً جدا می‌کنیم: ┌──────────────────────┐ │ Canonical Domain │ │ Pure Python Models │ └──────────┬───────────┘ │ Mapping / Repository │ ┌──────────▼───────────┐ │ Persistence │ │ SQLAlchemy ORM/DB │ └──────────────────────┘ ┌──────────────────────┐ │ Optimization Model │ │ CP-SAT / Solver │ └──────────────────────┘ یعنی: Domain از DB مستقل است. DB از Solver مستقل است. Solver از API مستقل است. API فقط Application Service را صدا می‌زند. UI مستقیماً به Solver دسترسی ندارد. این separation را از همین ابتدا حفظ می‌کنیم. 2. Domain Contract فایل: app/domain/contracts.py محتوای پایه: Python from dataclasses import dataclass from enum import Enum from datetime import datetime class RunStatus(str, Enum): CREATED = "CREATED" VALIDATING = "VALIDATING" READY = "READY" SOLVING = "SOLVING" VALIDATING_RESULT = "VALIDATING_RESULT" COMPLETED = "COMPLETED" FAILED_VALIDATION = "FAILED_VALIDATION" FAILED_SOLVER = "FAILED_SOLVER" INVALID_RESULT = "INVALID_RESULT" TIMEOUT = "TIMEOUT" CANCELLED = "CANCELLED" class QualityStatus(str, Enum): NOT_CHECKED = "NOT_CHECKED" PASSED = "PASSED" PASSED_WITH_WARNINGS = "PASSED_WITH_WARNINGS" FAILED = "FAILED" class DataVersionStatus(str, Enum): CREATED = "CREATED" INGESTING = "INGESTING" VALIDATING = "VALIDATING" READY = "READY" REJECTED = "REJECTED" @dataclass(frozen=True) class DataVersion: id: str name: str source_type: str source_uri: str source_hash: str created_at: datetime status: DataVersionStatus quality_status: QualityStatus 3. چرا DataVersion فقط یک فایل نیست؟ چون ممکن است: Access DB Excel Marketplace GIS Manual Override همگی در ساخت یک Dataset مشارکت کنند. بنابراین: DataVersion │ ├── SourceFile │ ├── SourceRecord │ ├── Mapping │ ├── QualityResult │ └── ReconciliationResult مدل نهایی: DataVersion=Sources+Mapping+Transformation+Quality 4. SourceFile Entity بعدی: Python @dataclass(frozen=True) class SourceFile: id: str data_version_id: str file_name: str source_type: str uri: str sha256: str file_size: int imported_at: datetime برای مثال: DV-00042 │ ├── aaa.accdb │ SHA256=... │ └── REPORTKholase_31-06-1405.xlsx SHA256=... این باعث می‌شود بعداً دقیقاً بدانیم یک نتیجه بر اساس کدام فایل ایجاد شده است. 5. Scenario Contract Python @dataclass(frozen=True) class ScenarioChange: entity_type: str entity_id: str attribute: str base_value: str | None new_value: str و: Python @dataclass(frozen=True) class Scenario: id: str name: str base_scenario_id: str | None data_version_id: str model_version: str planning_start: int planning_end: int objective_id: str demand_policy_id: str | None operating_regime: str changes: tuple[ScenarioChange, ...] = () 6. Scenario باید Versioned باشد فرض کنیم Base: SC-BASE و تغییر: B03: SINGLE → DOUBLE نباید: SC-BASE تغییر کند. باید: SC-BASE │ └── SC-DOUBLE-B03 ساخته شود. و: ScenarioChange: entity_type = "PhysicalBlock" entity_id = "B03" attribute = "track_type" base_value = "SINGLE" new_value = "DOUBLE" 7. Objective Definition Objective را هم Domain Entity می‌کنیم: Python @dataclass(frozen=True) class ObjectiveDefinition: id: str name: str priorities: tuple[str, ...] weights: dict[str, float] مثلاً: Python ObjectiveDefinition( id="MAX_FREIGHT", name="Maximum Freight", priorities=( "maximize_freight", "minimize_unserved", "minimize_schedule_deviation", ), weights={ "freight": 1.0, "unserved": 100.0, "schedule_deviation": 0.1, }, ) این قسمت مهم است چون Objective نباید داخل Solver hard-code شود. 8. Solver Configuration Python @dataclass(frozen=True) class SolverConfiguration: time_limit_seconds: int = 300 random_seed: int = 1 num_workers: int = 1 absolute_gap: float | None = None relative_gap: float | None = None log_search_progress: bool = False بنابراین reproducibility: Result=f(DataVersion,Scenario,ModelVersion,SolverConfiguration) 9. CapacityRun Run باید snapshot اجرایی باشد: Python @dataclass class CapacityRun: id: str scenario_id: str data_version_id: str model_version: str status: RunStatus solver_seed: int solver_workers: int time_limit_seconds: int objective_value: float | None = None ولی در نسخه Production یک نکته دیگر هم اضافه می‌کنیم: Python solver_configuration_hash: str چرا؟ چون ممکن است configuration تغییر کند: workers=1 در مقابل: workers=8 و برای audit باید مشخص باشد. 10. Run Input Snapshot من پیشنهاد می‌کنم Run علاوه بر reference، یک hash از ورودی canonical هم داشته باشد: Python input_snapshot_hash: str در نتیجه: RUN-001 │ ├── DataVersion = DV-001 ├── Scenario = SC-001 ├── Model = 2.1.0 ├── SolverConfig = HASH-A └── InputSnapshot = HASH-B این برای Production Audit بسیار مهم است. 11. Capacity Proof Python @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str validated: bool proof_valid: bool اما نسخه قوی‌تر: Python @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str f_validated: bool f_plus_one_validated: bool proof_valid: bool objective_name: str proof_method: str evidence_ids: tuple[str, ...] مثلاً: proof_method = "FEASIBLE_F_PLUS_INFEASIBLE_F1" 12. Invariant بسیار مهم Python proof_valid = ( f_status in {"FEASIBLE", "OPTIMAL"} and f_validated and f_plus_one_status == "INFEASIBLE" ) ولی: UNKNOWN TIME_LIMIT MODEL_INVALID هیچ‌کدام: INFEASIBLE محسوب نمی‌شوند. 13. CapacityResult Python @dataclass(frozen=True) class CapacityResult: run_id: str capacity: int capacity_proven: bool proof: CapacityProof infrastructure_capacity: int | None operational_capacity: int | None rolling_stock_capacity: int | None transportable_capacity: int | None allocated_capacity: int | None binding_constraints: tuple[str, ...] bottlenecks: tuple[str, ...] explanation: tuple[str, ...] این همان جایی است که تفاوت: Capacity و: Capacity Profile را نگه می‌داریم. 14. Database Base فایل: app/db/base.py Python from sqlalchemy import MetaData from sqlalchemy.orm import DeclarativeBase NAMING_CONVENTION = { "ix": "ix_%(column_0_label)s", "uq": "uq_%(table_name)s_%(column_0_name)s", "ck": "ck_%(table_name)s_%(constraint_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s", } class Base(DeclarativeBase): metadata = MetaData( naming_convention=NAMING_CONVENTION ) این دقیقاً با الگوی Declarative جدید SQLAlchemy 2.x هم‌راستا است. SQLAlchemy Documentation +1 15. DataVersion ORM Python from datetime import datetime from sqlalchemy import DateTime, String from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class DataVersionModel(Base): __tablename__ = "data_version" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) name: Mapped[str] = mapped_column( String(255), nullable=False, ) source_type: Mapped[str] = mapped_column( String(50), nullable=False, ) source_uri: Mapped[str] = mapped_column( String(1000), nullable=False, ) source_hash: Mapped[str] = mapped_column( String(128), nullable=False, index=True, ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, ) status: Mapped[str] = mapped_column( String(50), nullable=False, ) quality_status: Mapped[str] = mapped_column( String(50), nullable=False, ) در SQLAlchemy 2.x، mapped_column() برای Declarative mapping و ترکیب آن با Mapped[...] الگوی type-aware رسمی است. SQLAlchemy Documentation +1 16. Scenario ORM Python from sqlalchemy import ForeignKey, Integer, String from sqlalchemy.orm import Mapped, mapped_column class ScenarioModel(Base): __tablename__ = "scenario" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) name: Mapped[str] = mapped_column( String(255), nullable=False, ) base_scenario_id: Mapped[str | None] = mapped_column( ForeignKey("scenario.id"), nullable=True, ) data_version_id: Mapped[str] = mapped_column( ForeignKey("data_version.id"), nullable=False, index=True, ) model_version: Mapped[str] = mapped_column( String(50), nullable=False, ) planning_start: Mapped[int] = mapped_column( Integer, nullable=False, ) planning_end: Mapped[int] = mapped_column( Integer, nullable=False, ) objective_id: Mapped[str] = mapped_column( String(100), nullable=False, ) demand_policy_id: Mapped[str | None] = mapped_column( String(100), ) operating_regime: Mapped[str] = mapped_column( String(50), nullable=False, ) 17. Scenario Change ORM Python from sqlalchemy import Text class ScenarioChangeModel(Base): __tablename__ = "scenario_change" id: Mapped[int] = mapped_column( primary_key=True, autoincrement=True, ) scenario_id: Mapped[str] = mapped_column( ForeignKey("scenario.id"), nullable=False, index=True, ) entity_type: Mapped[str] = mapped_column( String(100), nullable=False, ) entity_id: Mapped[str] = mapped_column( String(100), nullable=False, ) attribute: Mapped[str] = mapped_column( String(100), nullable=False, ) base_value: Mapped[str | None] = mapped_column( Text, ) new_value: Mapped[str] = mapped_column( Text, nullable=False, ) 18. CapacityRun ORM Python from sqlalchemy import Float class CapacityRunModel(Base): __tablename__ = "capacity_run" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) scenario_id: Mapped[str] = mapped_column( ForeignKey("scenario.id"), nullable=False, index=True, ) data_version_id: Mapped[str] = mapped_column( ForeignKey("data_version.id"), nullable=False, index=True, ) model_version: Mapped[str] = mapped_column( String(50), nullable=False, ) status: Mapped[str] = mapped_column( String(50), nullable=False, ) solver_seed: Mapped[int] = mapped_column( Integer, nullable=False, ) solver_workers: Mapped[int] = mapped_column( Integer, nullable=False, ) time_limit_seconds: Mapped[int] = mapped_column( Integer, nullable=False, ) objective_value: Mapped[float | None] = mapped_column( Float, nullable=True, ) solver_configuration_hash: Mapped[str] = mapped_column( String(128), nullable=False, ) input_snapshot_hash: Mapped[str] = mapped_column( String(128), nullable=False, ) 19. Repository Layer Repository باید بسیار ساده بماند. Python class DataVersionRepository: def __init__(self, session): self.session = session def add(self, entity): self.session.add(entity) return entity def get(self, entity_id): return self.session.get( DataVersionModel, entity_id, ) برای Scenario: Python class ScenarioRepository: def __init__(self, session): self.session = session def add(self, entity): self.session.add(entity) return entity def get(self, entity_id): return self.session.get( ScenarioModel, entity_id, ) و Run: Python class CapacityRunRepository: def __init__(self, session): self.session = session def add(self, entity): self.session.add(entity) return entity def get(self, run_id): return self.session.get( CapacityRunModel, run_id, ) 20. چرا Repository را پیچیده نمی‌کنیم؟ چون نمی‌خواهیم این اتفاق بیفتد: Repository ↓ Business Logic ↓ Solver Repository فقط: Persistence است. Business logic باید در: Application Service Domain Service Engine باشد. 21. Run Manager Python class RunManager: def __init__( self, session, run_repository, scenario_repository, data_version_repository, ): self.session = session self.runs = run_repository self.scenarios = scenario_repository self.data_versions = data_version_repository def create_run( self, scenario_id, solver_config, ): scenario = self.scenarios.get( scenario_id ) if scenario is None: raise ValueError( f"Scenario not found: {scenario_id}" ) data_version = self.data_versions.get( scenario.data_version_id ) if data_version is None: raise ValueError( f"DataVersion not found: " f"{scenario.data_version_id}" ) ... در اینجا هنوز Solver اجرا نمی‌شود. فقط Run ساخته می‌شود. 22. Run Lifecycle بعد: Python run.status = RunStatus.VALIDATING سپس: VALIDATING اگر ورودی صحیح: READY سپس Worker: SOLVING پس از solver: VALIDATING_RESULT و در صورت موفقیت: COMPLETED 23. Failure States سه failure مهم را جدا می‌کنیم: Failed Validation Input invalid Failed Solver Solver crashed/model invalid Invalid Result Solver says feasible but independent validator rejects it این سه وضعیت نباید یکی شوند. 24. Database Migration Migration اولیه باید این چهار جدول را بسازد: data_version scenario scenario_change capacity_run ولی در Migration بعدی: infrastructure station station_track physical_block junction ... اضافه می‌شوند. یعنی: 0001 → Foundation 0002 → Infrastructure 0003 → Train/Route 0004 → Demand 0005 → Rolling Stock 0006 → Formation 0007 → Scheduling 0008 → Results 0009 → Network 0010 → Marketplace این migration sequencing را عمداً مرحله‌ای نگه می‌داریم. 25. چرا کل Database را در Migration اول نمی‌ریزیم؟ چون هنوز برخی semanticها در داده واقعی: MaxSpeed Distance sumDistancezz RequiredWait نیازمند validation هستند. ما نباید schema را طوری قفل کنیم که فرض اشتباه را تبدیل به حقیقت production کند. اصل: No Verified Mapping → No Production Semantics 26. Data Quality Metadata برای هر source field در نسخه بعدی: Python @dataclass(frozen=True) class FieldMapping: source_system: str source_field: str canonical_entity: str canonical_field: str transformation: str confidence: str status: str مثلاً: Access.TrainNo ↓ TrainRun.train_no ↓ IDENTITY ↓ VERIFIED ولی: Access.Distance ↓ RouteSegment.distance ↓ UNKNOWN ↓ UNTRUSTED 27. این موضوع برای داده واقعی شما بسیار مهم است برای داده‌ای که قبلاً بررسی کردیم، mapping باید چیزی شبیه این باشد: Access Field Canonical وضعیت TrainNo TrainRun.train_no VERIFIED TrainName TrainRun.service_name VERIFIED StationName Station.name VERIFIED Sequence TrainStationCall.sequence VERIFIED time_in TrainStationCall.arrival VERIFIED time_take TrainStationCall.dwell VERIFIED RequiredWait minimum operational dwell PROVISIONAL Kilometerage chainage HIGH MaxSpeed speed constraint PROVISIONAL Distance source distance UNTRUSTED sumDistancezz unknown UNKNOWN seir running_time_to_next VERIFIED و این mapping باید metadata-driven باشد، نه hard-coded داخل Scheduler. 28. زمان و Midnight یک اصلاح مهم نسبت به نسخه‌های قبلی: HH:MM به‌تنهایی کافی نیست. مثلاً: 23:46 00:36 نباید به صورت: 00:36 < 23:46 تفسیر شود. Canonical schedule باید absolute minute داشته باشد: DayOffset + MinuteOfDay مثلاً: 23:46 → 1426 00:36 next day → 1476 پس: 1476>1426 است. این normalization باید در Data Adapter / Staging → Canonical انجام شود، نه در Solver. 29. TrainStationCall ساختار Canonical: Python @dataclass(frozen=True) class TrainStationCall: train_run_id: str sequence: int station_id: str arrival_minute: int departure_minute: int dwell_minutes: int required_wait_minutes: int | None source_distance: float | None derived_distance: float | None baseline_running_time_to_next: int | None دقت کن: seir نباید attribute ایستگاه باشد. بلکه: running_time_to_next است. 30. Directed Path از: TrainStationCall[] ساخته می‌شود. مثلاً: 100: Gar ↓ Sakheh ↓ Bagh Yek ↓ ... ↓ Andimeshk و Train 101: Andimeshk ↓ Dokuheh ↓ Gol Mahak ↓ ... ↓ Gar بنابراین: PhysicalBlock یک مفهوم direction-independent است؛ اما: DirectedPath direction-dependent است. 31. اینجا یک قانون مهم را قفل می‌کنیم هیچ‌وقت: Python sorted(stations) را برای ساخت operational route استفاده نمی‌کنیم. Order فقط از: Sequence یا Canonical directed topology می‌آید. 32. V2.1-A Acceptance Tests حداقل این تست‌ها: Test 1 Create DataVersion باید موفق باشد. Test 2 Create Scenario Test 3 Create Scenario Variant Test 4 Create Run Test 5 Run references correct Scenario Test 6 Scenario references correct DataVersion Test 7 UNKNOWN ≠ INFEASIBLE Test 8 Same input → same configuration hash Test 9 Changed solver seed → different configuration hash Test 10 Midnight normalization 33. Configuration Hash برای reproducibility: Python import hashlib import json def configuration_hash(config) -> str: payload = { "time_limit_seconds": config.time_limit_seconds, "random_seed": config.random_seed, "num_workers": config.num_workers, "absolute_gap": config.absolute_gap, "relative_gap": config.relative_gap, } raw = json.dumps( payload, sort_keys=True, separators=(",", ":"), ) return hashlib.sha256( raw.encode("utf-8") ).hexdigest() این hash در CapacityRun ذخیره می‌شود. 34. Input Snapshot Hash برای Canonical Input: Python def snapshot_hash(canonical_snapshot: dict) -> str: raw = json.dumps( canonical_snapshot, sort_keys=True, ensure_ascii=False, separators=(",", ":"), ) return hashlib.sha256( raw.encode("utf-8") ).hexdigest() پس نتیجه دقیقاً traceable می‌شود. 35. اولین API فعلاً فقط: http GET /api/v1/health اما Contractهای بعدی: http POST /api/v1/data-versions GET /api/v1/data-versions/{id} POST /api/v1/scenarios GET /api/v1/scenarios/{id} POST /api/v1/runs GET /api/v1/runs/{id} بعداً: http POST /api/v1/runs/{id}/execute ولی بهتر است اجرای Run نهایتاً به Job Queue سپرده شود. 36. Response Contract برای Run: JSON { "run_id": "RUN-000001", "scenario_id": "SC-000001", "data_version_id": "DV-000001", "model_version": "2.1.0", "status": "COMPLETED", "solver": { "seed": 1, "workers": 1, "time_limit_seconds": 300 } } 37. Result Contract وقتی Solver اضافه شد: JSON { "run_id": "RUN-000001", "capacity": { "train_count": 42, "freight_tons": 84000, "proven": true }, "profiles": { "infrastructure": 55, "operational": 48, "rolling_stock": 44, "transportable": 43, "allocated": 42 }, "proof": { "f": 42, "f_plus_one": 43, "f_status": "FEASIBLE", "f_plus_one_status": "INFEASIBLE", "validated": true, "proof_valid": true } } 38. مرحله بعد: V2.1-B حالا Foundation تثبیت شد. گام بعدی باید مستقیماً به داده واقعی موجود شما وصل شود: V2.1-B Real Data Ingestion Platform و ساختار آن: Access / Excel ↓ SourceFile ↓ Raw Record ↓ Staging ↓ Field Mapping ↓ Transformation ↓ Quality Rules ↓ Identity Reconciliation ↓ Canonical TrainRun ↓ TrainStationCall ↓ DirectedPath در V2.1-B، من پیشنهاد می‌کنم به‌صورت مشخص این اجزا را پیاده کنیم: Access Adapter Excel Adapter Raw Record Model Staging Tables Mapping Registry Field Confidence Quality Gate TrainRun Reconciliation Station Identity Reconciliation Midnight Normalization Directed Path Builder Baseline Schedule Builder Data Version Hashing Real-data Golden Test و بعد از آن، اولین Vertical Slice واقعی خواهیم داشت: aaa.accdb ↓ TrainNo 100 / 101 ↓ Gar ↔ Andimeshk ↓ TrainStationCall ↓ seir ↓ Kilometerage ↓ Directed Path ↓ Baseline Timetable بدون اینکه هنوز معنای فیلدهای Distance و sumDistancezz را حدس بزنیم. این دقیقاً همان نقطه‌ای است که پروژه از Production Foundation وارد Real Iranian Railway Data Execution می‌شود. --- ## ادامه 45 — پرسش کاربر (`message-id:15f26677-efc1-4393-8850-12e7e83395be`) احسنت --- ## ادامه 45 — پاسخ دستیار (`message-id:d66b27bf-fd2f-4543-aeb4-c26e35bcf720`) ممنونم، عالیه! مسیر را دقیقاً همین‌طور مرحله‌به‌مرحله جلو می‌بریم. در گام بعد، V2.1-B — Real Data Ingestion Platform را ادامه می‌دهم و تمرکز را می‌گذارم روی: Access/Excel → Raw/Staging → Mapping → Quality Gate → Reconciliation → Canonical TrainRun → TrainStationCall → DirectedPath → Baseline Schedule با تأکید ویژه روی سه موضوعی که برای داده‌های واقعی راه‌آهن ایران حیاتی‌اند: مدیریت صحیح Midnight / Day Offset تفکیک دقیق TrainRun، TrainFormation و OperationalBatch حفظ Source Evidence و عدم تبدیل فیلدهای تأییدنشده به فرضیات محاسباتی بعد از آن، همین لایه مستقیماً به V1.7 Operational Scheduler متصل می‌شود تا اولین Vertical Slice واقعی از داده خام تا Schedule و Capacity Proof شکل بگیرد. --- ## ادامه 46 — پرسش کاربر (`message-id:050522d1-129f-4bc6-b206-145694fd78ee`) بله لطفا --- ## ادامه 46 — پاسخ دستیار (`message-id:a52816fc-deb7-4104-b41e-429f2e924e10`) حتماً. حالا وارد V2.1-B — Real Data Ingestion Platform می‌شویم؛ این قسمت بسیار مهم است چون از اینجا به بعد دیگر با داده‌ی فرضی کار نمی‌کنیم و هر رکورد باید قابل ردیابی، قابل اعتبارسنجی و قابل بازتولید باشد. برای Excel، pandas.read_excel امکان خواندن workbook، sheetهای مشخص یا همه sheetها را فراهم می‌کند و برای .xlsx در حالت معمول از openpyxl استفاده می‌شود. Pandas +1 V2.1-B — Real Data Ingestion Platform 1. معماری این مرحله معماری را این‌گونه قفل می‌کنیم: SOURCE SYSTEMS │ ┌────────────┼────────────┐ │ │ │ Access Excel Marketplace │ │ │ └────────────┼────────────┘ ▼ Source Adapter │ ▼ Source File │ ▼ Raw Record │ ▼ Staging │ ▼ Mapping Registry │ ▼ Transformation │ ▼ Quality Gate │ ▼ Identity Reconciliation │ ▼ Canonical Model │ ┌──────────┴──────────┐ ▼ ▼ TrainRun StationCall │ │ └──────────┬──────────┘ ▼ DirectedPath │ ▼ Baseline Schedule نکته‌ی کلیدی: Source Data هرگز مستقیماً وارد Solver نمی‌شود. 2. چهار Layer داده چهار لایه‌ی اصلی: RAW ↓ STAGING ↓ CANONICAL ↓ OPTIMIZATION RAW دقیقاً همان چیزی که از منبع آمده. STAGING پاک‌سازی اولیه و type normalization. CANONICAL معنای domain به داده داده شده است. OPTIMIZATION نسخه‌ای که Solver مصرف می‌کند. 3. Raw Record هر رکورد ورودی باید provenance داشته باشد. Python @dataclass(frozen=True) class RawRecord: id: str source_file_id: str source_system: str source_table: str | None source_sheet: str | None source_row_number: int payload: dict[str, object] record_hash: str مثلاً برای Access: source_system = ACCESS source_table = TrainMovement source_row = 1842 و Excel: source_system = EXCEL source_sheet = Sheet1 source_row = 27 4. چرا Payload را JSON نگه می‌داریم؟ چون در RAW نباید semantic فرض کنیم. مثلاً: Distance = 0 نباید در RAW تبدیل شود به: Distance = unknown RAW باید همان: JSON { "Distance": 0 } را حفظ کند. تفسیر در Mapping/Quality انجام می‌شود. 5. Source Evidence برای هر canonical field یک evidence داریم: Python @dataclass(frozen=True) class SourceEvidence: source_record_id: str source_field: str raw_value: str | None transformed_value: str | None transformation: str | None confidence: str verification_status: str مثلاً: Access.seir ↓ Raw = 34 ↓ Canonical = 34 minutes ↓ Transformation = integer ↓ Confidence = VERIFIED 6. Mapping Registry Mapping را configuration-driven می‌کنیم. مثلاً: YAML source_system: ACCESS fields: TrainNo: canonical_entity: TrainRun canonical_field: train_no transformation: identity confidence: VERIFIED TrainName: canonical_entity: TrainRun canonical_field: service_name transformation: identity confidence: VERIFIED StationName: canonical_entity: TrainStationCall canonical_field: station_name transformation: identity confidence: VERIFIED Sequence: canonical_entity: TrainStationCall canonical_field: sequence transformation: integer confidence: VERIFIED time_in: canonical_entity: TrainStationCall canonical_field: arrival_time transformation: time confidence: VERIFIED time_take: canonical_entity: TrainStationCall canonical_field: dwell_minutes transformation: integer confidence: VERIFIED RequiredWait: canonical_entity: TrainStationCall canonical_field: required_wait_minutes transformation: integer confidence: PROVISIONAL Kilometerage: canonical_entity: TrainStationCall canonical_field: chainage transformation: decimal confidence: HIGH MaxSpeed: canonical_entity: TrainStationCall canonical_field: max_speed transformation: decimal confidence: PROVISIONAL Distance: canonical_entity: TrainStationCall canonical_field: source_distance transformation: decimal confidence: UNTRUSTED sumDistancezz: canonical_entity: TrainStationCall canonical_field: source_sum_distance transformation: decimal confidence: UNKNOWN seir: canonical_entity: TrainStationCall canonical_field: baseline_running_time_to_next transformation: integer confidence: VERIFIED 7. یک اصل بسیار مهم درباره seir در داده واقعی مشاهده‌شده: seir با زمان حرکت از ایستگاه فعلی تا ورود به ایستگاه بعدی منطبق است. بنابراین: seir را تبدیل می‌کنیم به: TrainStationCall.running_time_to_next یا در مدل Route: RouteSegment.baseline_running_time نه: Station.seir این تفاوت برای ساخت Time-Space Model حیاتی است. 8. Access Adapter Interface: Python class AccessAdapter: def inspect(self, source_uri): ... def read_table( self, source_uri, table_name, ): ... def read_records( self, source_uri, table_name, ): ... در Production بهتر است Adapter خودش semantic mapping انجام ندهد. یعنی: Access Adapter ↓ Raw Records و نه: Access Adapter ↓ TrainRun 9. Excel Adapter برای Excel: Python class ExcelAdapter: def inspect(self, source_uri): ... def list_sheets(self, source_uri): ... def read_sheet( self, source_uri, sheet_name, ): ... برای .xlsx می‌توان از: Python pd.read_excel( source_uri, sheet_name=sheet_name, ) استفاده کرد؛ pandas همچنین ExcelFile.sheet_names را برای کشف sheetها ارائه می‌کند. Pandas +1 10. Excel باید ابتدا Inspect شود نباید مستقیم: Python pd.read_excel(...) وارد Canonical شود. ابتدا: Excel ↓ Workbook Inspection ↓ Sheet List ↓ Column List ↓ Row Count ↓ Data Types ↓ Sample مثلاً: JSON { "file": "REPORTKholase_31-06-1405_02-19-35.xlsx", "sheets": [ { "name": "Sheet1", "rows": 128, "columns": 10 } ] } 11. Excel Source Schema بر اساس داده‌ای که قبلاً بررسی کرده‌ایم: ردیف نام قطار شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت از مبدا ساعت ورود به مقصد شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت از مقصد ساعت رسیدن به مبدا این داده Train Movement Pattern است. نباید آن را با: TrainStationCall یکی کنیم. 12. دو نوع Source Dataset داریم این موضوع را در V2.1-B رسماً ثبت می‌کنیم: Operational Movement Detail Access: TrainNo Station Sequence time_in time_take time_out seir ... Service Calendar / Cycle Excel: Train Name Outbound Number Outbound Departure Outbound Days Outbound Arrival Return Number Return Departure Return Days Return Arrival این دو dataset complementary هستند. 13. Reconciliation باید بفهمیم: Excel Train Service کدام: Access TrainRun را نمایندگی می‌کند. هرگز: Python Excel.TrainNo == Access.TrainNo را به تنهایی identity فرض نمی‌کنیم. 14. Train Identity Key Identity Candidate: Python @dataclass(frozen=True) class TrainIdentityKey: train_no: str | None train_name: str | None origin_station: str | None destination_station: str | None direction: str | None operating_pattern: str | None و سپس: Exact Match Candidate Match Ambiguous No Match 15. Reconciliation Result Python @dataclass(frozen=True) class ReconciliationResult: source_entity_id: str canonical_entity_id: str | None status: str confidence: float reasons: tuple[str, ...] مثلاً: JSON { "source_entity_id": "EXCEL-17", "canonical_entity_id": "RUN-100", "status": "MATCHED", "confidence": 0.96, "reasons": [ "train_name_match", "origin_match", "destination_match", "direction_match" ] } 16. Station Identity StationName هم نباید مستقیماً identity باشد. مثلاً: "اراک" "اراک " "اراك" می‌توانند شکل‌های متفاوت یک station باشند. پس: Source Station ↓ Normalization ↓ Station Identity Candidate ↓ Canonical Station 17. Station Identity Table station_identity ---------------- id canonical_station_id source_system source_station_code source_station_name normalized_name confidence status مثلاً: ACCESS StationNumber=... StationName=اراک ↓ Station S-ARAK 18. StationNumber را با Kilometerage قاطی نمی‌کنیم این را به‌عنوان invariant ثبت می‌کنیم: StationNumber ≠ Kilometerage StationNumber: source station identifier/code است. Kilometerage: chainage / positional evidence است. 19. Derived Distance از داده واقعی: Kilometerage_i Kilometerage_i+1 می‌توانیم: Distance i ​ =∣K i+1 ​ −K i ​ ∣ بسازیم. مثلاً: Gar 157 Sakheh 200 پس: 43km اما این مقدار باید: derived_distance باشد، نه: source_distance 20. Distance Policy بنابراین Canonical: Python source_distance: float | None derived_distance: float | None دارد. و: source_distance هرگز با derived overwrite نمی‌شود. 21. Midnight Normalization تابع پایه: Python def normalize_time( current_minute: int, previous_absolute: int | None, ) -> int: candidate = current_minute if previous_absolute is None: return candidate while candidate < previous_absolute: candidate += 1440 return candidate اما برای railway بهتر است به شکل event-aware انجام شود. مثلاً: Arrival Departure Running to Next همگی sequence را دنبال کنند. 22. مثال واقعی اگر: Station A time_in = 23:46 time_take = 50 آنگاه: departure = 00:36 next day نه: 00:36 same day بعد: seir = 20 پس: next arrival = 00:56 next day Canonical: A arrival = 1426 A departure = 1476 B arrival = 1496 23. Schedule Derivation رابطه اصلی: Departure i ​ =Arrival i ​ +Dwell i ​ و: Arrival i+1 ​ =Departure i ​ +RunningTime i ​ بنابراین: time_out می‌تواند Source Evidence باقی بماند، اما schedule canonical باید بتواند از: arrival + dwell بازسازی شود. 24. Quality Rules Quality Gate حداقل این موارد را بررسی کند: Q01 Sequence strictly increasing Q02 StationName not null Q03 time_in valid Q04 time_take >= 0 Q05 departure >= arrival Q06 next arrival >= current departure Q07 seir >= 0 Q08 RequiredWait >= 0 Q09 TrainNo not null Q10 duplicate TrainNo + Sequence 25. Quality Result Python @dataclass(frozen=True) class QualityIssue: rule_id: str severity: str source_record_id: str | None entity_id: str | None field: str | None message: str مثلاً: JSON { "rule_id": "Q07", "severity": "ERROR", "source_record_id": "ACC-991", "entity_id": "RUN-100", "field": "seir", "message": "Running time cannot be negative" } 26. Quality Severity سه سطح: INFO WARNING ERROR و Gate: ERROR > 0 ↓ REJECT ولی: WARNING > 0 ↓ PASS_WITH_WARNINGS 27. Unknown Field Policy اگر: sumDistancezz unknown است: UNKNOWN اما ingestion نباید fail شود. فقط: Quality Warning ثبت می‌شود. ولی اگر Solver بخواهد از آن استفاده کند: Production Use = BLOCKED 28. Canonical TrainRun پس از mapping: Python @dataclass(frozen=True) class TrainRun: id: str train_no: str service_name: str origin_station_id: str destination_station_id: str direction: str station_calls: tuple[ "TrainStationCall", ... ] 29. TrainStationCall Python @dataclass(frozen=True) class TrainStationCall: sequence: int station_id: str arrival_minute: int departure_minute: int dwell_minutes: int required_wait_minutes: int | None chainage: float | None source_distance: float | None derived_distance: float | None baseline_running_time_to_next: int | None 30. DirectedPath Python @dataclass(frozen=True) class DirectedPath: id: str train_run_id: str direction: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] ساخت: TrainStationCall sequence ↓ ordered stations ↓ physical blocks ↓ directed movements 31. Physical Block ID برای جلوگیری از ایجاد block متفاوت برای رفت و برگشت: Python def physical_block_id( station_a: str, station_b: str, ) -> str: a, b = sorted( [station_a, station_b] ) return f"{a}::{b}" مثلاً: Gar → Sakheh و: Sakheh → Gar هر دو: Gar::Sakheh هستند. اما direction جداست. 32. Baseline Schedule از source schedule: TrainStationCall یک Baseline می‌سازیم: Python @dataclass(frozen=True) class BaselineMovement: train_run_id: str block_id: str direction: str entry: int exit: int clear: int فعلاً: exit=entry+RunningTime و: clear=exit+ClearingTime است. 33. نکته مهم Baseline: Observed / Source Schedule است. Optimized Schedule: Engine Generated Schedule است. نباید این دو را یکی کنیم. 34. Baseline Diff بعداً: Python @dataclass(frozen=True) class ScheduleDifference: train_run_id: str object_type: str object_id: str baseline_value: int optimized_value: int delta: int مثلاً: Train 100 Block B03 Baseline Entry = 520 Optimized Entry = 535 Delta = +15 35. Pipeline اصلی حالا کل V2.1-B: Python class RealDataIngestionPipeline: def run(self, source): source_file = self.register_source( source ) raw_records = self.ingest_raw( source_file ) staging = self.build_staging( raw_records ) mapped = self.apply_mapping( staging ) quality = self.run_quality_gate( mapped ) if quality.has_errors: return quality reconciled = self.reconcile_identity( mapped ) canonical = self.build_canonical( reconciled ) paths = self.build_directed_paths( canonical ) baseline = self.build_baseline_schedule( canonical, paths ) return IngestionResult( canonical=canonical, paths=paths, baseline=baseline, quality=quality, ) 36. یک تغییر مهم در Pipeline من پیشنهاد می‌کنم Quality Gate دو مرحله داشته باشد: Raw Quality ↓ Semantic Quality Raw Quality بررسی: null type duplicate format Semantic Quality بررسی: sequence time consistency station identity direction running time chainage این separation خطاها را بسیار بهتر trace می‌کند. 37. خروجی Ingestion Python @dataclass(frozen=True) class IngestionResult: data_version_id: str source_files: tuple[str, ...] raw_record_count: int staging_record_count: int canonical_train_run_count: int canonical_station_call_count: int quality_errors: int quality_warnings: int reconciliation_matched: int reconciliation_ambiguous: int reconciliation_unmatched: int train_runs: tuple[TrainRun, ...] paths: tuple[DirectedPath, ...] 38. Data Version Hash کل DataVersion باید deterministic باشد: Source Files + File Hashes + Mapping Version + Transformation Version بنابراین: DV=Hash(Sources,Mappings,Transformations) این یعنی اگر Mapping تغییر کند: DV-001 و: DV-002 دیگر دو dataset متفاوت‌اند، حتی اگر فایل اصلی یکی باشد. 39. Mapping Version پس: Python mapping_version = "MAP-2.1.0" و: Python transformation_version = "TRANS-2.1.0" را نیز ذخیره می‌کنیم. Run نهایی: DataVersion MappingVersion TransformationVersion ModelVersion SolverVersion را خواهد داشت. 40. اولین Vertical Slice حالا با داده‌ی Train 100/101: Access ↓ TrainNo=100 ↓ گار → اندیمشک ↓ Sequence ↓ Station Calls ↓ seir ↓ Kilometerage ↓ Derived Distance ↓ Directed Path ↓ Baseline Schedule و: TrainNo=101 ↓ اندیمشک → گار به شکل reverse direction ساخته می‌شود. 41. چیزی که فعلاً استفاده نمی‌کنیم تا زمانی که validation مستقل انجام نشده: Distance sumDistancezz MaxSpeed RequiredWait به شکل کامل وارد Capacity Solver نمی‌شوند. به‌طور مشخص: Distance با: Derived Distance جایگزین نمی‌شود. 42. ولی seir وارد Baseline می‌شود چون شواهد داده‌ای آن قوی است: RunningTime i ​ =seir i ​ و baseline: Arrival i+1 ​ =Departure i ​ +seir i ​ خواهد بود. بعد Scheduler می‌تواند آن را به: Baseline Running Time تبدیل کند. 43. Vertical Slice بعدی پس از این مرحله، اتصال مستقیم: RealDataIngestion ↓ Canonical TrainRun ↓ DirectedPath ↓ InfrastructureMaster ↓ V1.7 OperationalSchedulingModel ↓ CP-SAT ↓ Generated Schedule ↓ Independent Validator انجام می‌شود. و این اولین نقطه‌ای است که سیستم می‌تواند واقعاً بگوید: «برای این تعداد قطار، یک timetable feasible پیدا کردم.» نه اینکه فقط از روی: Distance / Speed ظرفیت را تخمین زده باشد. 44. نتیجه معماری V2.1-B در پایان این مرحله زنجیره‌ی واقعی ما این است: ┌──────────────────────────────┐ │ Access / Excel │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Source Registration │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ RAW Records │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ STAGING │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Mapping + Transformation │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Quality Gate │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Identity Reconciliation │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Canonical Railway Model │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Directed Operational Path │ └──────────────┬───────────────┘ ↓ ┌──────────────────────────────┐ │ Baseline Schedule │ └──────────────┬───────────────┘ ↓ V1.7 Scheduler این معماری یک مزیت بسیار مهم دارد: داده‌ی خام، تفسیر داده، مدل Canonical و مدل Optimization هر کدام مرز مشخص خودشان را دارند. گام بعدی قطعی بعد از V2.1-B، باید وارد V2.1-C — Production Operational Scheduler شویم و همان TrainRun + DirectedPath واقعی را به Scheduler بدهیم؛ یعنی برای اولین بار Single Track، Double Track، Headway، Opposing Movement، Switch Time، Station Track، Station Length، Junction و Operational Window را روی داده‌ی واقعی وارد مدل زمانی کنیم و سپس Independent Validation + F/F+1 Capacity Proof را روی آن اجرا کنیم. --- ## ادامه 47 — پرسش کاربر (`message-id:49d2eaee-48f9-4cf4-b052-d23688c5c01a`) بله لطفا --- ## ادامه 47 — پاسخ دستیار (`message-id:3545e8be-8ac6-4e99-ac76-b13bf30e22fb`) حتماً. حالا وارد V2.1-C — Production Operational Scheduler می‌شویم. این مرحله نقطه‌ی اتصال واقعی بین داده‌های Access/Excel و موتور ظرفیت است: یعنی TrainRun واقعی را می‌گیریم، روی زیرساخت واقعی قرار می‌دهیم، زمان‌بندی feasible تولید می‌کنیم و سپس مستقل از Solver آن را validate می‌کنیم. در مدل زمان‌بندی، استفاده از interval/resource constraints با CP-SAT با ساختار مسئله ما سازگار است؛ اما در معماری ما Solver فقط تولیدکننده‌ی schedule است و Independent Validator مرجع نهایی صحت schedule خواهد بود. V2.1-C — Production Operational Scheduler 1. هدف هدف این مرحله: Real Data→Canonical TrainRun→DirectedPath→Resource Occupancy→Feasible Schedule→Independent Validation→Capacity Proof است. یعنی خروجی دیگر فقط: Capacity = 30 نیست. بلکه: Capacity = 30 + 30 Train Runs + Station Times + Block Occupancy + Crossing Decisions + Track Assignments + Conflict Evidence + Independent Validation خواهیم داشت. 2. معماری V2.1-C CANONICAL DATA │ ▼ ┌──────────────┐ │ TrainRun │ └──────┬───────┘ │ ▼ ┌──────────────┐ │ DirectedPath │ └──────┬───────┘ │ ▼ ┌──────────────────────┐ │ Operational Profile │ └──────────┬───────────┘ │ ▼ ┌──────────────────────┐ │ Resource Expansion │ └──────────┬───────────┘ │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ Blocks Stations Junctions │ │ │ └──────────────┼──────────────┘ ▼ ┌─────────────────┐ │ CP-SAT Scheduler│ └────────┬────────┘ │ ▼ Generated Schedule │ ▼ Independent Validator │ ┌───────────┴───────────┐ ▼ ▼ VALID INVALID │ ▼ Capacity Search │ ▼ F / F+1 Proof 3. اصل مهم: Solver ≠ Validator این اصل را در V2.1-C غیرقابل مذاکره می‌کنیم. Solver می‌گوید: من یک assignment پیدا کردم که constraints مدل را رعایت می‌کند. Validator می‌گوید: آیا schedule خروجی، بر اساس domain rules مستقل، واقعاً معتبر است؟ بنابراین: Solver ↓ Candidate Schedule ↓ Independent Validator ↓ Validated Schedule و نه: Solver = Truth 4. مدل زمانی برای هر TrainRun و هر StationCall: A i ​ =Arrival D i ​ =Departure و برای هر block: E i,b ​ =Entry X i,b ​ =Exit C i,b ​ =Clear داریم. بنابراین: Station A │ │ departure ▼ ┌───────────── Block ─────────────┐ │ │ │ Entry → Running → Exit → Clear │ │ │ └─────────────────────────────────┘ │ ▼ Station B 5. Precedence Constraints برای هر station call: D i ​ ≥A i ​ +Dwell i ​ و برای block: E i,b ​ ≥D i ​ X i,b ​ =E i,b ​ +T run,i,b ​ C i,b ​ =X i,b ​ +T clear,b ​ و: A i+1 ​ ≥C i,b ​ در حالت ساده. 6. Running Time اگر baseline موجود باشد: T run,i,b ​ =T i,b baseline ​ یعنی seir. اما در نسخه Production باید امکان جایگزینی داشته باشیم: T run ​ =f(Block,TrainType,LoadState,Direction,SpeedProfile) بنابراین: Python @dataclass(frozen=True) class RunningTimePolicy: mode: str use_baseline: bool minimum_factor: float = 1.0 train_type_adjustment: dict[str, float] = field( default_factory=dict ) 7. Station Dwell Dwell نباید صرفاً یک عدد ثابت باشد. در مدل: D i ​ ≥A i ​ +T i dwell ​ که: T i dwell ​ =max(T scheduled ​ ,T required ​ ,T operational ​ ) است. مثلاً: Scheduled dwell = 30 Required wait = 45 پس: Dwell=45 8. Operational Profile برای هر TrainRun: Python @dataclass(frozen=True) class TrainOperationalProfile: train_length_m: int train_weight_t: int earliest_departure: int = 0 latest_arrival: int | None = None default_dwell_min: int = 0 brake_test_minutes: int = 0 formation_minutes: int = 0 clearance_minutes: int = 0 train_type_id: str | None = None load_state: str = "LOADED" این object باید از TrainRun جدا باشد. چون: TrainRun = operational service identity Profile = physical/operational characteristics 9. Single Track برای Single Track: Forward ──────────────► Reverse ◄────────────── هر دو یک resource دارند: PhysicalBlock B03 بنابراین: Occupancy(B03) برای هر دو direction مشترک است. این یکی از مهم‌ترین تفاوت‌های مدل ما با یک مدل ساده‌ی directed graph است. 10. Double Track در Double Track: Track 1 ──────────────► Track 2 ◄────────────── دو movement resource داریم: B03:FORWARD B03:REVERSE اما این به معنی استقلال کامل نیست. هنوز ممکن است: Station conflict Junction conflict Track assignment conflict Terminal conflict وجود داشته باشد. 11. Movement Resource Python def movement_resource(block, direction): if block.track_type == TrackType.SINGLE: return block.id return f"{block.id}:{direction.value}" نتیجه: Single: B03 Double: B03:FORWARD B03:REVERSE 12. Same-Direction Headway برای دو قطار هم‌جهت: Entry j ​ ≥Clear i ​ +H same ​ اگر: Train A clear = 120 Headway = 5 آنگاه: Train B entry >= 125 13. Opposite-Direction Conflict برای دو قطار مخالف در Single Track: یا: Clear i ​ +T switch ​ ≤Entry j ​ یا: Clear j ​ +T switch ​ ≤Entry i ​ این constraint باید به صورت disjunction مدل شود. 14. Conflict Variable برای هر pair: Python before = model.NewBoolVar( f"{i}_before_{j}" ) و: before = 1 یعنی: i → j و: before = 0 یعنی: j → i اما این Boolean فقط زمانی ساخته می‌شود که دو movement واقعاً resource مشترک داشته باشند. این برای scalability بسیار مهم است. 15. NoOverlap برای resources ساده می‌توان از intervalها استفاده کرد: Resource │ ├── Train A interval ├── Train B interval ├── Train C interval └── ... و: Python model.AddNoOverlap(intervals) برای resourceهایی مثل Single Track مناسب است. در مدل CP-SAT، constraintهای resource/interval دقیقاً برای چنین زمان‌بندی‌هایی طراحی شده‌اند. Google for Developers اما برای Junction که conflict matrix دارد، NoOverlap کلی کافی نیست. 16. Junction Junction را به شکل: Junction │ ├── Movement M1 ├── Movement M2 ├── Movement M3 └── Movement M4 مدل می‌کنیم. Conflict matrix: M1 × M2 = CONFLICT M1 × M3 = compatible M1 × M4 = CONFLICT 17. Junction Separation برای دو movement متعارض: Exit i ​ +S ij ​ ≤Entry j ​ یا: Exit j ​ +S ji ​ ≤Entry i ​ 18. Station Track Assignment هر Train باید هنگام حضور در Station روی Track قرار گیرد. مثلاً: Station S01 Track 1 ───────────── Track 2 ───────────── Track 3 ───────────── برای Train: Length = 620m فقط trackهایی که: L usable ​ ≥620 دارند قابل انتخاب هستند. 19. Optional Interval برای هر train-track candidate: Python assigned = model.NewBoolVar(...) interval = model.NewOptionalIntervalVar( start, duration, end, assigned, ... ) و: k ∑ ​ assignment i,k ​ =1 برای هر train که الزاماً باید track داشته باشد. 20. Station Conflict اگر دو Train روی یک Track قرار گیرند: Departure i ​ ≤Arrival j ​ یا برعکس. پس Station Track نیز یک resource است. 21. Station Length Constraint قبل از ساخت interval: Python if train.length_m > track.usable_length_m: candidate = False یعنی assignment ناممکن اصلاً وارد Solver نشود. این candidate pruning است و بسیار بهتر از ساخت constraintهای غیرضروری است. 22. Operational Windows مثلاً: Block B03 Maintenance 10:00–11:30 قطار باید: یا قبل از window تمام کند: Clear≤600 یا بعد از آن وارد شود: Entry≥690 یعنی: Clear≤W start ​ ∨Entry≥W end ​ 23. Earliest / Latest برای Train: Departure origin ​ ≥EarliestDeparture و اگر deadline داریم: Arrival destination ​ ≤LatestArrival این‌ها Hard Constraint هستند مگر اینکه Scenario صراحتاً آن‌ها را Soft تعریف کند. 24. Brake Test و Formation در Origin: Departure≥RequestedStart+Formation+BrakeTest مثلاً: Requested start = 08:00 Formation = 20 Brake test = 15 پس: Departure≥08:35 25. Train Clearance Clearance باید بخشی از resource occupancy باشد. یعنی: Entry ↓ Running ↓ Exit ↓ Clearance ↓ Resource Released نه اینکه resource در لحظه Exit آزاد فرض شود. 26. Constraint Builder Architecture Scheduler را یک class بزرگ نمی‌سازیم. SchedulingModelBuilder │ ├── PrecedenceBuilder ├── BlockResourceBuilder ├── HeadwayBuilder ├── StationBuilder ├── JunctionBuilder ├── WindowBuilder ├── TimeWindowBuilder └── ObjectiveBuilder این تفکیک برای تست و توسعه حیاتی است. 27. Scheduler Interface Python class OperationalScheduler: def solve( self, problem: SchedulingProblem, configuration: SolverConfiguration, ) -> SchedulingResult: ... و: Python @dataclass(frozen=True) class SchedulingProblem: trains: tuple[TrainRun, ...] profiles: dict[str, TrainOperationalProfile] stations: tuple[Station, ...] blocks: tuple[PhysicalBlock, ...] junctions: tuple[JunctionMovement, ...] conflicts: tuple[JunctionConflict, ...] windows: tuple[OperationalWindow, ...] horizon_start: int horizon_end: int mode: str 28. Schedule Result Python @dataclass(frozen=True) class GeneratedSchedule: train_schedules: tuple[ TrainSchedule, ... ] block_movements: tuple[ BlockMovement, ... ] station_assignments: tuple[ StationTrackAssignment, ... ] junction_movements: tuple[ JunctionMovementSchedule, ... ] 29. Train Schedule Python @dataclass(frozen=True) class TrainSchedule: train_run_id: str origin_departure: int destination_arrival: int station_calls: tuple[ ScheduledStationCall, ... ] 30. Independent Validator Validator را کاملاً مستقل نگه می‌داریم: Python class ScheduleValidator: def validate( self, schedule, problem, ) -> ValidationReport: ... 31. Validation Rules Validator حداقل این‌ها را بررسی می‌کند: V01 Station sequence V02 Arrival/Departure V03 Dwell V04 Running time V05 Block occupancy V06 Block headway V07 Opposing movement V08 Switch time V09 Station track conflict V10 Station length V11 Junction conflict V12 Operational window V13 Earliest departure V14 Latest arrival V15 Block clearing V16 Route continuity V17 Missing assignment 32. Conflict Report هر violation باید evidence داشته باشد. Python @dataclass(frozen=True) class ValidationViolation: rule_id: str conflict_type: str severity: str train_ids: tuple[str, ...] resource_id: str | None actual_value: float | None required_value: float | None slack: float | None message: str 33. مثال JSON { "rule_id": "V07", "conflict_type": "OPPOSING_DIRECTION", "trains": ["100", "101"], "resource": "GAR::SAKHEH", "actual_value": 3, "required_value": 5, "slack": -2 } این داده بعداً مستقیماً وارد Explanation Engine می‌شود. 34. Slack برای هر constraint: Slack=Actual−Required اگر: Slack>0 constraint فاصله دارد. اگر: Slack=0 binding است. اگر: Slack<0 schedule invalid است. 35. Binding Constraint در schedule معتبر: Slack = 0 می‌تواند candidate binding constraint باشد. اما برای bottleneck واقعی بهتر است علاوه بر slack، marginal impact هم محاسبه شود. مثلاً: C(H=5)=30 و: C(H=6)=28 پس: ΔC=−2 این بسیار معنادارتر از صرفاً utilization=95% است. 36. Objective برای Baseline Mode: min i ∑ ​ ∣A i ​ −A i baseline ​ ∣+∣D i ​ −D i baseline ​ ∣ اما در Capacity Mode: Primary: maximize capacity Secondary: minimize deviation Tertiary: minimize unnecessary waiting یعنی Capacity Mode نباید صرفاً timetable تاریخی را تقلید کند. 37. Operational Regime سه حالت اصلی: STRICT_ALTERNATING DIRECTIONAL_BATCH MIXED Strict Alternating F R F R F R Directional Batch F F F F R R R Mixed F F R F R R F 38. Regime باید Scenario باشد در V2.1-C ابتدا regime را به عنوان Scenario Parameter می‌گذاریم: Python operating_regime = "MIXED" در مرحله بعد می‌توانیم regime را داخل Optimization به Decision Variable تبدیل کنیم. این تفکیک پیچیدگی Solver را کنترل می‌کند. 39. Capacity Search حالا Capacity دیگر یک فرمول ساده نیست. تعریف: C r ​ =max{F:Schedule(F) is feasible and validated} Algorithm: Candidate F ↓ Build Problem ↓ Solve ↓ Solver Status ↓ Independent Validation ↓ Feasible? │ ├── YES → increase F │ └── NO → decrease F 40. نکته مهم درباره Solver Status این invariant را قفل می‌کنیم: OPTIMAL → feasible FEASIBLE → feasible INFEASIBLE → infeasible UNKNOWN → unknown MODEL_INVALID → invalid TIME_LIMIT → unknown unless a valid feasible incumbent exists بنابراین: UNKNOWN ≠ INFEASIBLE و: TIME_LIMIT ≠ INFEASIBLE 41. Capacity Proof برای ظرفیت F: F ↓ SOLVED ↓ VALIDATED سپس: F+1 ↓ SOLVE ↓ INFEASIBLE تنها در این حالت: Capacity=F و: capacity_proven = true 42. Proof Object Python @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str f_validated: bool f_plus_one_validated: bool proof_valid: bool proof_method: str evidence_ids: tuple[str, ...] 43. چرا F+1 باید واقعاً ساخته شود؟ چون اگر candidate list تمام شود، نباید بگوییم: F = last_candidate F+1 = infeasible این proof جعلی است. باید: F+1 واقعاً به Solver داده شود. 44. Candidate Generation برای ظرفیت Route، Candidate TrainRun باید بتواند از یک template ساخته شود: Python @dataclass(frozen=True) class TrainServiceTemplate: service_id: str origin: str destination: str direction: str train_type_id: str load_state: str frequency_candidate: int تا ��رفیت به تعداد trainهای تاریخی محدود نشود. 45. یک اصلاح مهم نسبت به نسخه‌های قبلی در V2.1-C دیگر این فرض را قبول نمی‌کنیم: capacity = number of existing TrainRun records Existing TrainRuns فقط evidence / template هستند. Capacity باید بتواند: existing + generated candidate services را آزمایش کند. 46. Baseline و Capacity دو Mode جدا Baseline Mode سؤال: آیا timetable موجود قابل بازسازی/اعتبارسنجی است؟ Capacity Mode سؤال: حداکثر چند TrainRun قابل برنامه‌ریزی است؟ این دو را در یک optimization objective قاطی نمی‌کنیم. 47. Production Pipeline حالا زنجیره کامل: Access ↓ Raw ↓ Staging ↓ Mapping ↓ Quality ↓ Reconciliation ↓ Canonical TrainRun ↓ Infrastructure Master ↓ Directed Path ↓ Operational Profile ↓ Scheduling Problem ↓ CP-SAT ↓ Generated Schedule ↓ Independent Validator ↓ Capacity Search ↓ F Feasible + Validated ↓ F+1 INFEASIBLE ↓ Capacity Proof ↓ Bottleneck ↓ Explanation ↓ Capacity Offer 48. Golden Test شماره 1 — Single Track A ===== B ===== C دو قطار: T1: A → C T2: C → A هر دو B03 را استفاده می‌کنند. Expected: No overlap + switch time Validator باید crossing conflict را detect کند. 49. Golden Test شماره 2 — Double Track A ===== B ===== C اما: Track 1 → Forward Track 2 → Reverse دو قطار opposite direction می‌توانند همزمان در block باشند. اما: Station Junction Terminal هنوز ممکن است conflict ایجاد کند. 50. Golden Test شماره 3 — Station Length Train = 700m Track = 650m Expected: Assignment impossible نه اینکه Solver train را somehow روی آن track قرار دهد. 51. Golden Test شماره 4 — Midnight 23:46 + 50 min = 00:36 next day و: 00:36 + 20 = 00:56 next day Expected absolute timeline: 1426 1476 1496 نه reset شدن به: 00:36 00:56 52. Golden Test شماره 5 — Capacity Proof مثلاً اگر: F=10 → FEASIBLE + VALIDATED F=11 → INFEASIBLE آنگاه: capacity = 10 proof_valid = true اما: F=10 → FEASIBLE F=11 → UNKNOWN آنگاه: capacity_proven = false 53. Golden Test شماره 6 — Independent Validation حتی اگر Solver بگوید: FEASIBLE ولی Validator بگوید: V07 OPPOSING_DIRECTION نتیجه: F = NOT VALIDATED و ظرفیت proven نیست. 54. Bottleneck Engine پس از ظرفیت proven: Python @dataclass(frozen=True) class Bottleneck: resource_id: str bottleneck_type: str utilization: float slack: float capacity_impact: int | None binding: bool evidence_ids: tuple[str, ...] 55. Bottleneck Types BLOCK HEADWAY OPPOSING_DIRECTION SWITCH STATION_TRACK STATION_LENGTH JUNCTION WINDOW TERMINAL WAGON WAGON_BUFFER LOCOMOTIVE FORMATION DEMAND POLICY 56. Explanation Explanation دیگر متن hardcoded نیست. مثلاً: Capacity = 24 trains/day Primary binding resource: Block GAR::SAKHEH Track: SINGLE Direction conflict: FORWARD / REVERSE Required separation: 5 min Available slack: 0 min F+1: INFEASIBLE این متن باید از evidence ساخته شود. 57. Traceability برای هر Capacity Result: Capacity ↓ Proof ↓ Schedule ↓ TrainRun ↓ DirectedPath ↓ Block ↓ Infrastructure Source و حتی: Block ↓ Access Station Calls ↓ Source Record IDs باید قابل برگشت باشد. 58. API جدید در V2.1-C: POST /api/v1/scheduler/runs GET /api/v1/scheduler/runs/{id} GET /api/v1/scheduler/runs/{id}/schedule GET /api/v1/scheduler/runs/{id}/conflicts GET /api/v1/scheduler/runs/{id}/validation POST /api/v1/capacity/search GET /api/v1/capacity/{run_id}/proof GET /api/v1/capacity/{run_id}/bottlenecks 59. Output Contract خروجی Run باید تقریباً چنین ساختاری داشته باشد: JSON { "run_id": "RUN-001", "status": "COMPLETED", "solver": { "status": "FEASIBLE", "seed": 1, "workers": 1 }, "schedule": { "train_count": 24, "validated": true }, "capacity": { "value": 24, "proven": true }, "proof": { "f": 24, "f_plus_one": 25, "f_status": "FEASIBLE", "f_plus_one_status": "INFEASIBLE", "proof_valid": true }, "bottlenecks": [], "violations": [] } 60. Definition of Done — V2.1-C این مرحله زمانی کامل است که همه این موارد کار کنند: مورد وضعیت مورد انتظار Real TrainRun ✓ DirectedPath ✓ Single Track ✓ Double Track ✓ Same-direction Headway ✓ Opposing Direction ✓ Switch Time ✓ Block Clearing ✓ Station Track ✓ Station Length ✓ Junction Conflict ✓ Operational Window ✓ Earliest Departure ✓ Latest Arrival ✓ Dwell ✓ Midnight ✓ Baseline Schedule ✓ Optimized Schedule ✓ Independent Validator ✓ Conflict Classification ✓ Slack ✓ Binding Constraint ✓ Capacity Search ✓ F+1 Test ✓ UNKNOWN ≠ INFEASIBLE ✓ Capacity Proof ✓ Explanation ✓ Source Traceability ✓ جایگاه V2.1-C در محصول نهایی با تکمیل این مرحله، ما یک Railway Capacity Calculator واقعی داریم، نه یک estimator ساده: RAILWAY CAPACITY ENGINE │ ┌───────────────┴───────────────┐ │ │ REAL DATA INFRASTRUCTURE │ │ └───────────────┬───────────────┘ ↓ TRAIN RUN ↓ DIRECTED PATH ↓ TIME-SPACE MODEL ↓ OPERATIONAL SCHEDULER ↓ FEASIBLE TIMETABLE ↓ INDEPENDENT VALIDATOR ↓ CAPACITY SEARCH ↓ ┌─────────┴─────────┐ │ │ F F+1 │ │ FEASIBLE INFEASIBLE VALIDATED │ └─────────┬─────────┘ ↓ PROVEN CAPACITY ↓ BOTTLENECK / EXPLANATION ↓ CAPACITY OFFER ↓ MARKETPLACE و یک نکته مهم دیگر: V2.1-C را عمداً قبل از فعال‌کردن کامل Wagon/Loco/Formation و Multi-OD در Solver قرار می‌دهیم. ابتدا باید هسته‌ی Infrastructure + Time-Space + Scheduling + Validation + Proof روی داده واقعی کاملاً قابل اتکا شود؛ سپس V2.1-D آن را با Wagon Cycle + Locomotive Cycle + Train Formation متصل می‌کند. این ترتیب، ریسک معماری و خطایابی را به‌طور محسوسی پایین می‌آورد. --- ## ادامه 48 — پرسش کاربر (`message-id:8752719b-7681-409a-aa66-09d295b26b8e`) بله لطفا --- ## ادامه 48 — پاسخ دستیار (`message-id:85bf6008-2435-40c0-ba11-b7f7cc0ac451`) عالی. حالا می‌رسیم به V2.1-D — Integrated Rolling Stock Capacity؛ یعنی جایی که ظرفیت زیرساختیِ صرف را به ظرفیت واقعیِ قابل بهره‌برداری با Wagon Cycle + Locomotive Cycle + Train Formation تبدیل می‌کنیم. این مرحله از نظر محصولی بسیار مهم است، چون از اینجا دیگر سؤال فقط «خط چند قطار می‌کشد؟» نیست، بلکه: با این زیرساخت، این تقاضا، این واگن‌ها، این لکوموتیوها و این قواعد تشکیل قطار، چند سرویس واقعاً قابل اجراست؟ برای لایه persistence نیز همان الگوی مدرن SQLAlchemy 2.x را حفظ می‌کنیم؛ DeclarativeBase و Mapped/mapped_column() الگوی اصلی مستندات فعلی SQLAlchemy هستند. SQLAlchemy Documentation +1 V2.1-D — Integrated Rolling Stock Capacity Specification V2.1-D — Integrated Rolling Stock Capacity 1. هدف V2.1-D موتور زمان‌بندی V2.1-C را با سه حوزه متصل می‌کند: Train Formation Wagon Cycle Locomotive Cycle بنابراین ظرفیت عملیاتی از: [ C^{Infrastructure} ] به: [ C^{Operational} ] و سپس به: [ C^{RollingStock} ] و نهایتاً: [ C^{Integrated} ] ارتقا پیدا می‌کند. تعریف نهایی: \max F ] به‌طوری‌که همزمان موارد زیر feasible باشند: [ Infrastructure \cap Schedule \cap Formation \cap WagonCycle \cap LocomotiveCycle \cap Station \cap Terminal \cap Buffer \cap Demand \cap Policy ] 2. جایگاه V2.1-D معماری فعلی: Access / Excel ↓ Canonical Data ↓ TrainRun ↓ DirectedPath ↓ Operational Scheduler ↓ Feasible Schedule ↓ Independent Validator ↓ Capacity اکنون: Access / Excel ↓ Canonical Data ↓ Demand ↓ Wagon Requirement ↓ Train Formation ↓ Wagon Availability ↓ Locomotive Availability ↓ DirectedPath ↓ Operational Scheduler ↓ Wagon Cycle ↓ Locomotive Cycle ↓ Independent Validation ↓ Integrated Capacity ↓ Capacity Proof 3. چهار نوع ظرفیت از این مرحله باید Capacity Profile را صریحاً ذخیره کنیم. 3.1 Infrastructure Capacity [ C_I ] حداکثر ظرفیت با فرض کافی بودن rolling stock. 3.2 Operational Capacity [ C_O ] ظرفیت پس از اعمال: timetable headway crossing switch station junction operational windows 3.3 Rolling Stock Capacity [ C_{RS} ] ظرفیت پس از اعمال: wagon availability wagon cycle locomotive availability locomotive cycle formation constraints 3.4 Integrated Capacity [ C_{Integrated} ] ظرفیت نهایی: \min_{\text{conceptually}} { C_I,C_O,C_{RS},C_{Terminal},C_{Demand},... } ] اما این عبارت فقط برای intuition است. در Solver: [ C_{Integrated} ] از حل کامل مسئله به دست می‌آید و الزاماً برابر minimum ساده‌ی پروفایل‌ها نیست. 4. Train Formation Formation باید قبل از Schedule feasibility بررسی شود. مدل: @dataclass(frozen=True) class TrainFormationItem: wagon_id: str wagon_type_id: str commodity_id: str position: int gross_weight_t: float length_m: float و: @dataclass(frozen=True) class TrainFormation: id: str train_run_id: str items: tuple[TrainFormationItem, ...] total_length_m: float total_weight_t: float locomotive_ids: tuple[str, ...] 5. Formation Feasibility Formation باید حداقل این شرایط را رعایت کند: Wagon Compatibility [ Commodity \leftrightarrow WagonType ] Length [ L_{train} \le L_{station/route} ] Weight [ W_{train} \le W_{route} ] Traction [ TE_{locomotive} \ge RequiredTE ] Brake Capability قطار باید از نظر brake capability معتبر باشد. Route Compatibility برخی wagon/train typeها ممکن است روی route مشخص مجاز نباشند. 6. Formation Engine Interface: class FormationEngine: def generate_candidates( self, demand, wagon_pool, locomotive_pool, route, ): ... def validate( self, formation, route, ): ... def optimize( self, candidates, objective, ): ... 7. Formation نباید الزاماً Individual-Wagon CP-SAT باشد برای مقیاس Production: 100,000 wagons را مستقیماً به هزاران Boolean Solver Variable تبدیل نمی‌کنیم. روش staged: Demand ↓ Wagon Requirement ↓ Formation Candidate Generation ↓ Compatibility Filtering ↓ Formation Feasibility ↓ Candidate Formations ↓ CP-SAT / Network Optimization این کار حجم مدل را کنترل می‌کند. 8. Wagon Requirement برای هر Freight Flow: f( Demand, Commodity, WagonCapacity, LoadFactor ) ] مثلاً اگر: [ Demand=4000t ] و هر wagon: [ 80t ] باشد: 50 ] اما این هنوز Formation نیست. چون ممکن است: station length train weight locomotive traction wagon compatibility تعداد قابل استفاده را کاهش دهند. 9. Wagon Pool مدل: @dataclass(frozen=True) class WagonPool: id: str wagon_type_id: str total_count: int available_count: int maintenance_count: int blocked_count: int و: Total Maintenance Blocked ] 10. Wagon Inventory Inventory باید time-dependent باشد: [ E_{j,w,t} ] یعنی موجودی واگن نوع (w) در location (j) و زمان (t). Constraint: [ 0 \le E_{j,w,t} \le C_{j,w} ] 11. Wagon Cycle چرخه اصلی: Origin ↓ Loaded Wagon ↓ Train ↓ Destination ↓ Unload ↓ Empty Wagon ↓ Return / Reposition ↓ Origin ↓ Next Load بنابراین: T_{loaded} + T_{unload} + T_{empty} + T_{reposition} + T_{wait} ] 12. Wagon Turnover اگر: [ N_w ] تعداد wagon موجود باشد و: [ T_{cycle} ] چرخه باشد، ظرفیت بالقوه فقط به تعداد wagon محدود نیست؛ به turnover نیز وابسته است. تقریب اولیه: [ F_{wagon} \approx \frac{ N_w \times PlanningHorizon }{ T_{cycle} \times WagonsPerTrain } ] اما این فقط screening است. ظرفیت Production باید با زمان‌بندی واقعی محاسبه شود. 13. Empty Wagon Flow Empty wagon یک flow مستقل نیست که بعداً نادیده گرفته شود. برای هر location: EmptyOut LoadDispatch ] و: [ 0 \le E_{j,t} \le BufferCapacity_j ] 14. Empty Wagon Buffer هر terminal/station ممکن است buffer محدود داشته باشد: [ E_j(t) \le C_{buffer,j} ] اگر buffer پر باشد: Unload ↓ Empty Wagon ↓ Buffer FULL ↓ No additional empty return در نتیجه ممکن است حتی با وجود track capacity، wagon capacity کاهش یابد. 15. Wagon Cycle Constraint اگر wagon مورد نیاز Train B هنوز از Train A برنگشته باشد: Train A ↓ Unload ↓ Empty Return ↓ Origin ↓ Train B Train B نمی‌تواند از همان wagon استفاده کند. این باید با زمان واقعی مدل شود. 16. Locomotive Model @dataclass(frozen=True) class LocomotiveType: id: str name: str traction_capacity_t: float max_train_length_m: float compatible_train_types: tuple[str, ...] و: @dataclass(frozen=True) class Locomotive: id: str locomotive_type_id: str home_station_id: str available_from: int available_to: int | None 17. Locomotive Assignment @dataclass(frozen=True) class LocomotiveAssignment: train_run_id: str locomotive_ids: tuple[str, ...] ممکن است: 1 locomotive یا: 2 locomotives باشد. 18. Traction Constraint برای Formation: [ RequiredTE \le \sum_l TE_l ] مثلاً: [ RequiredTE=420kN ] و: [ Loco_1=250kN ] پس یک locomotive کافی نیست. با دو: [ 250+250=500kN ] formation feasible می‌شود. 19. Locomotive Cycle چرخه: Locomotive ↓ Train A ↓ Destination ↓ Turnback / Return ↓ Maintenance / Fuel ↓ Train B مدل: T_{service} + T_{turnback} + T_{deadhead} + T_{maintenance} + T_{fuel} ] 20. Locomotive Conflict اگر: L1 → Train A تا زمان: t=500 مشغول باشد، نمی‌تواند: Train B at t=480 را سرویس دهد. بنابراین: [ Start_B \ge End_A + Turnback ] 21. Operational Availability Maintenance/Fueling/Prayer/Operational availability را یک abstraction واحد نگه می‌داریم: @dataclass(frozen=True) class OperationalAvailability: resource_id: str start: int end: int kind: str blocking: bool این همان الگوی V1.7 است و از ایجاد چند subsystem جدا جلوگیری می‌کند. 22. Integrated Capacity Problem حالا مسئله: [ \max F ] subject to: Infrastructure [ BlockConflict=0 ] Station [ TrackConflict=0 ] Junction [ JunctionConflict=0 ] Headway [ H_{ij}\ge H_{required} ] Formation [ FormationFeasible=1 ] Wagon [ WagonDemand_t \le WagonAvailable_t ] Empty Flow [ E_{j,t}\ge0 ] Wagon Buffer [ E_{j,t}\le C_{buffer,j} ] Locomotive [ LocoDemand_t \le LocoAvailable_t ] Loco Cycle [ NoLocoOverlap=1 ] Demand [ F_{od,t} \le D_{od,t} ] 23. Integrated Engine Interface: class IntegratedCapacityEngine: def solve( self, problem: IntegratedCapacityProblem, configuration: SolverConfiguration, ) -> IntegratedCapacityResult: ... Problem: @dataclass(frozen=True) class IntegratedCapacityProblem: demand: tuple[Demand, ...] train_templates: tuple[TrainServiceTemplate, ...] formations: tuple[TrainFormation, ...] wagons: tuple[WagonPool, ...] locomotives: tuple[Locomotive, ...] routes: tuple[NetworkRoute, ...] infrastructure: InfrastructureMaster planning_start: int planning_end: int objective: ObjectiveDefinition 24. دو مرحله Solver برای Production بهتر است Solver را دو مرحله‌ای کنیم. Stage A — Resource Feasibility ابتدا: Formation Wagon Locomotive را screening می‌کنیم. Stage B — Integrated Schedule سپس فقط candidateهای feasible را وارد CP-SAT می‌کنیم: Formation Candidates + Wagon Availability + Loco Availability ↓ Integrated Scheduler این architecture از رشد بی‌رویه مدل جلوگیری می‌کند. 25. Resource Occupation تمام منابع را به یک abstraction نزدیک می‌کنیم: @dataclass(frozen=True) class ResourceOccupation: resource_type: str resource_id: str owner_type: str owner_id: str start: int end: int quantity: float = 1.0 مثلاً: BLOCK / B03 / TRAIN / T100 / 400 / 450 یا: LOCOMOTIVE / L17 / TRAIN / T100 / 400 / 480 یا: WAGON_POOL / WAGON-COVERED / FLOW / F100 / 400 / 800 26. چرا ResourceOccupation مهم است؟ چون بعداً می‌توانیم Bottleneck Engine را مستقل از نوع resource بسازیم: Resource ↓ Occupation ↓ Conflict ↓ Slack ↓ Marginal Impact این architecture برای Network Optimization V2.1-E بسیار مهم خواهد بود. 27. Capacity Profiles خروجی: @dataclass(frozen=True) class IntegratedCapacityProfile: infrastructure_capacity: int operational_capacity: int rolling_stock_capacity: int transportable_capacity: int allocated_capacity: int integrated_capacity: int مثلاً: Infrastructure 42 Operational 36 Rolling Stock 29 Transportable 27 Allocated 24 Integrated 24 این اعداد صرفاً نمونه‌ی ساختاری هستند و نباید به‌عنوان نتیجه واقعی تلقی شوند. 28. یک نکته مهم درباره MIN ممکن است: [ \min(42,36,29,27)=27 ] اما: [ C_{Integrated}=24 ] چرا؟ چون محدودیت‌ها ممکن است همزمان و تعاملی باشند. مثلاً: 24 trains + specific OD distribution + empty wagon balance + locomotive turnback ممکن است feasible باشد ولی 25 نه. بنابراین: Capacity Profile برای diagnosis است؛ Integrated Capacity از حل کامل به دست می‌آید. 29. Rolling Stock Bottleneck مثلاً: Infrastructure Capacity = 40 Operational Capacity = 34 Rolling Stock Capacity = 22 Integrated Capacity = 22 در این حالت infrastructure الزاماً bottleneck نهایی نیست. Bottleneck باید با evidence مشخص شود: Wagon Pool W-07 Available = 180 Required at F=23 = 184 Shortage = 4 30. Marginal Impact برای wagon: C(W+1)-C(W) ] برای مثال: [ C(180)=22 ] و: [ C(190)=24 ] پس: [ \Delta C=+2 ] این یعنی افزایش 10 wagon در این Scenario ظرفیت را 2 train افزایش داده است. این نتیجه باید از re-solve واقعی بیاید، نه از یک فرمول ثابت. 31. Locomotive Marginal Impact مثلاً: [ C(L=8)=22 ] و: [ C(L=9)=25 ] آنگاه: [ \Delta C=+3 ] و explanation می‌تواند بگوید: Three additional train services become feasible under the tested locomotive scenario. 32. Wagon Buffer Impact مثلاً: [ Buffer=100 \rightarrow C=22 ] و: [ Buffer=200 \rightarrow C=22 ] پس افزایش buffer در این Scenario اثر ظرفیتی نداشته است. اما اگر: [ Buffer=100 \rightarrow C=22 ] و: [ Buffer=150 \rightarrow C=24 ] آنگاه buffer constraint واقعی است. 33. Formation Bottleneck مثلاً: Demand = 4000t Wagons available = sufficient Locomotives = sufficient Infrastructure = sufficient اما: Train Length Limit = 600m باعث می‌شود formation فقط: 12 wagons را قبول کند. پس: FORMATION / TRAIN_LENGTH می‌تواند constraint binding باشد. 34. Integrated Validator Validator جدید: class IntegratedCapacityValidator: def validate( self, result, problem, ) -> IntegratedValidationReport: ... باید همه این‌ها را بررسی کند: Infrastructure Schedule Formation Wagon Availability Wagon Cycle Empty Wagon Flow Buffer Locomotive Assignment Locomotive Cycle Station Junction Terminal Demand Policy 35. Validator Hierarchy Integrated Validator │ ├── Schedule Validator │ ├── Formation Validator │ ├── Wagon Validator │ ├── Wagon Cycle Validator │ ├── Locomotive Validator │ ├── Locomotive Cycle Validator │ ├── Network Validator │ └── Demand / Policy Validator هر validator خروجی مستقل دارد. 36. Integrated Proof Proof دیگر فقط infrastructure نیست. تعریف: Feasible_{all} ] و: Infeasible_{all} ] پس: @dataclass(frozen=True) class IntegratedCapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str f_schedule_valid: bool f_formation_valid: bool f_wagon_valid: bool f_locomotive_valid: bool f_network_valid: bool f_plus_one_proven_infeasible: bool proof_valid: bool binding_constraints: tuple[str, ...] evidence_ids: tuple[str, ...] 37. Proof Rule فقط این حالت: F: FEASIBLE + Schedule Valid + Formation Valid + Wagon Valid + Locomotive Valid + Network Valid F+1: INFEASIBLE می‌تواند: PROVEN CAPACITY تولید کند. 38. UNKNOWN Policy اگر F+1: UNKNOWN باشد: Capacity = candidate upper bound Capacity Proven = FALSE نه: Capacity Proven = TRUE همچنان invariant: [ UNKNOWN\neq INFEASIBLE ] است. 39. Scenario Analysis V2.1-D باید از ابتدا Scenario-aware باشد. مثلاً: BASE WAGONS +50 LOCOS +2 BUFFER +100 DEMAND +10% FORMATION LIMIT +50m هر Scenario: Clone Base ↓ Apply Changes ↓ Full Re-solve ↓ Validate ↓ Compare 40. Scenario Change Examples ScenarioChange( entity_type="WAGON_POOL", entity_id="WP-01", attribute="available_count", base_value="150", new_value="200", ) یا: ScenarioChange( entity_type="LOCOMOTIVE_POOL", entity_id="LP-01", attribute="available_count", base_value="8", new_value="10", ) یا: ScenarioChange( entity_type="STATION_TRACK", entity_id="ST-05-T2", attribute="usable_length_m", base_value="600", new_value="700", ) 41. Capacity Comparison خروجی Scenario: Metric Base Scenario Delta Infrastructure Capacity — — — Operational Capacity — — — Rolling Stock Capacity — — — Integrated Capacity — — — Served Freight — — — Unserved Demand — — — Wagon Utilization — — — Locomotive Utilization — — — مقادیر واقعی باید از Runهای واقعی تولید شوند. 42. Marketplace Interface پس از Integrated Capacity: Demand ↓ Integrated Capacity ↓ Allocation Capacity Offer دیگر فقط: Route = X Capacity = 30 نیست. بلکه: @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str time_window: str train_capacity: int freight_capacity_t: float wagon_type_id: str | None train_type_id: str | None confidence: str proof_id: str 43. Capacity Offer باید Profiled باشد مثلاً: OD: Tehran → Khowaf Route: R-17 Train Type: Freight Heavy Commodity: Iron Ore Wagon: Open Wagon Direction: Outbound Time Window: 06:00–18:00 Capacity: 18 trains/day Proof: PROVEN این چیزی است که Marketplace واقعاً می‌تواند مصرف کند. 44. Data Traceability هر Capacity Offer باید قابل برگشت باشد: CapacityOffer ↓ CapacityResult ↓ CapacityProof ↓ Schedule ↓ TrainFormation ↓ WagonAssignment ↓ LocomotiveAssignment ↓ TrainRun ↓ Demand ↓ MarketRequest این همان Market-to-Capacity Traceability است. 45. Production Database در این مرحله جداول زیر اضافه می‌شوند: wagon_type wagon wagon_pool wagon_inventory wagon_requirement wagon_cycle empty_wagon_flow locomotive_type locomotive locomotive_assignment locomotive_cycle operational_availability train_formation train_formation_item train_formation_locomotive resource_occupation integrated_capacity_result integrated_capacity_proof capacity_evidence برای ORM همچنان Domain و Persistence را جدا نگه می‌داریم؛ SQLAlchemy امکان mapping declarative و type-annotated را به‌صورت native فراهم می‌کند. 46. API Endpointهای V2.1-D: POST /api/v1/formations/candidates POST /api/v1/formations/validate GET /api/v1/wagons/inventory GET /api/v1/wagons/cycles/{run_id} GET /api/v1/locomotives/availability GET /api/v1/locomotives/cycles/{run_id} POST /api/v1/integrated-capacity/runs GET /api/v1/integrated-capacity/{run_id} GET /api/v1/integrated-capacity/{run_id}/proof GET /api/v1/integrated-capacity/{run_id}/resources GET /api/v1/integrated-capacity/{run_id}/bottlenecks GET /api/v1/integrated-capacity/{run_id}/explanation 47. UI — Capacity Inspector در UI: ┌──────────────────────────────────────────────┐ │ Integrated Capacity │ ├──────────────────────────────────────────────┤ │ Infrastructure Capacity 42 │ │ Operational Capacity 36 │ │ Rolling Stock Capacity 29 │ │ Transportable Capacity 27 │ │ Integrated Capacity 24 │ ├──────────────────────────────────────────────┤ │ Proof PROVEN │ │ F 24 │ │ F+1 25 │ │ F+1 Status INFEASIBLE │ ├──────────────────────────────────────────────┤ │ Binding Constraints │ │ • Wagon Cycle │ │ • Single Track B03 │ │ • Loco Turnback │ └──────────────────────────────────────────────┘ 48. Wagon Cycle Explorer UI باید نشان دهد: Wagon W-1023 08:00 Load 09:00 Depart 15:00 Arrive 15:30 Unload 16:00 Empty 22:00 Return 23:00 Available و سپس: Cycle Time = 15h 49. Locomotive Cycle Explorer مثلاً: Loco L-17 Train 100 08:00 → 15:00 Turnback 15:00 → 15:30 Train 101 16:00 → 23:00 Maintenance 23:30 → 01:30 این visualization برای تشخیص bottleneck بسیار مهم است. 50. Golden Test — Wagon Cycle سناریو: 2 trains/day هر train: 10 wagons و فقط: 15 wagons موجود است. اگر cycle به اندازه کافی طولانی باشد، هر دو train ممکن است همزمان feasible نباشند. Expected: Infrastructure ≠ bottleneck Wagon Cycle = bottleneck 51. Golden Test — Locomotive Cycle فرض: Infrastructure = 20 trains Wagons = sufficient Locomotives = 2 ولی هر locomotive cycle اجازه فقط: 4 services/day را می‌دهد. پس: [ C_{loco}\le8 ] و Integrated Capacity باید این constraint را منعکس کند. 52. Golden Test — Empty Wagon Buffer فرض: Destination Buffer = 30 و unload باعث تولید: 40 empty wagons شود. پس: [ 40>30 ] و schedule/flow باید infeasible شود مگر اینکه: empty wagons dispatched شوند، buffer افزایش یابد، یا عملیات تغییر کند. 53. Golden Test — Formation فرض: Station usable length = 600m Train formation = 650m Expected: FORMATION INVALID حتی اگر: Block Capacity = sufficient باشد. 54. Golden Test — Integrated F/F+1 مثلاً: F = 24 همه این‌ها: Schedule ✓ Formation ✓ Wagon Cycle ✓ Loco Cycle ✓ Demand ✓ Network ✓ ولی: F+1 = 25 به دلیل: Wagon Cycle INFEASIBLE. پس: Integrated Capacity = 24 Proof = VALID 55. Explanation Engine Explanation باید علت را به زبان business تبدیل کند. نمونه: The integrated capacity is limited to 24 train services for the selected OD, route, demand and planning horizon. The infrastructure can support more services, but the wagon cycle becomes infeasible at the 25th service. At F=25, the required wagon pool exceeds available cycle-compatible wagons during the destination-return window. The result is proven because F=24 is feasible and independently validated, while F+1=25 is explicitly proven infeasible. 56. نکته مهم درباره «علت اصلی» اگر چند constraint همزمان binding باشند: Wagon Cycle Single Track Locomotive Cycle نباید سیستم به صورت خودکار فقط یکی را «علت قطعی» معرفی کند. بهتر است: Primary Binding Secondary Binding Contributing Constraint داشته باشیم و evidence هرکدام را نگه داریم. 57. Definition of Done V2.1-D زمانی Complete است که: ✓ Train Formation ✓ Wagon Compatibility ✓ Wagon Requirement ✓ Wagon Inventory ✓ Wagon Cycle ✓ Empty Wagon Flow ✓ Wagon Buffer ✓ Locomotive Inventory ✓ Locomotive Assignment ✓ Locomotive Cycle ✓ Maintenance/Fueling Window ✓ Traction ✓ Train Length ✓ Schedule Integration ✓ Independent Formation Validation ✓ Independent Wagon Validation ✓ Independent Loco Validation ✓ Integrated Validation ✓ Integrated Capacity ✓ F/F+1 Proof ✓ Bottleneck Classification ✓ Marginal Impact ✓ Capacity Offer ✓ Marketplace Traceability 58. معماری نهایی تا این نقطه MARKETPLACE │ Market Request │ ▼ Demand │ ▼ Freight Flow │ ▼ Wagon Requirement │ ┌───────────┴───────────┐ ▼ ▼ Train Formation Wagon Pool │ │ │ Wagon Cycle │ │ └───────────┬───────────┘ │ ▼ Locomotive Pool │ Loco Cycle │ ▼ Train Service │ ▼ Directed Path │ ▼ Time-Space Scheduler │ ┌───────────┼───────────┐ ▼ ▼ ▼ Block Station Junction │ │ │ └───────────┼───────────┘ ▼ Feasible Schedule │ ▼ Integrated Validator │ ▼ Capacity Search │ ┌────┴────┐ ▼ ▼ F F+1 FEASIBLE INFEASIBLE │ │ └────┬────┘ ▼ Capacity Proof │ ▼ Bottleneck Engine │ ▼ Explanation Engine │ ▼ Capacity Offer │ ▼ MARKETPLACE 59. نتیجه V2.1-D با این مرحله، تعریف Capacity در سیستم ما به شکل زیر تثبیت می‌شود: \max \left{ F: \begin{array}{l} Schedule(F)\ feasible\ Formation(F)\ feasible\ WagonCycle(F)\ feasible\ LocomotiveCycle(F)\ feasible\ Network(F)\ feasible\ Demand/Policy(F)\ feasible \end{array} \right} } ] و این همان نقطه‌ای است که پروژه از یک Infrastructure Capacity Engine به یک Railway Transport Capacity Engine تبدیل می‌شود. 60. مرحله بعد — V2.1-E پس از تثبیت این لایه، مرحله بعدی باید: V2.1-E — Multi-OD Network Optimization & Empty Wagon Network باشد. در آن مرحله دیگر فقط یک OD یا یک Route را حل نمی‌کنیم. مسئله به: [ \max \sum_{od,r,t} Q_{od,r} F_{od,r,t} ] تبدیل می‌شود، با منابع مشترک: Multiple OD Multiple Routes Shared Blocks Shared Stations Shared Junctions Shared Wagon Pools Empty Wagon Network Shared Locomotives Terminal Capacity Demand Policy و مهم‌تر از همه، Empty Wagon Flow خودش به یک Network Flow زمان‌مند تبدیل خواهد شد؛ بنابراین انتخاب ظرفیت یک OD می‌تواند مستقیماً روی ظرفیت OD دیگری اثر بگذارد. این همان مرحله‌ای است که V1.9 Network Optimization را از یک طراحی مفهومی به یک Integrated Network Solver قابل اجرا تبدیل می‌کنیم. --- ## ادامه 49 — پرسش کاربر (`message-id:5425c9eb-28c7-4451-a549-4fe49d66ef32`) هر دو مورد لطفا --- ## ادامه 49 — پاسخ دستیار (`message-id:e22098cb-c3e3-489a-8813-1178ac63ab81`) حتماً. هر دو را یکپارچه جلو می‌بریم، چون از نظر ریاضی و معماری واقعاً از هم جدا نیستند: V2.1-E — Multi-OD Network Optimization V2.1-F — Time-Dependent Empty Wagon Network نکته مهم این است که Empty Wagon را یک ماژول جانبی نمی‌سازیم؛ آن را به‌عنوان یک Network Flow زمان‌مند داخل مدل Multi-OD قرار می‌دهیم. این تصمیم باعث می‌شود ظرفیت یک OD بتواند واقعاً روی ظرفیت OD دیگر اثر بگذارد. برای لایه داده نیز همان جداسازی Domain/ORM را ادامه می‌دهیم؛ SQLAlchemy 2.x الگوی Declarative و Mapped/mapped_column() را برای mapping تایپ‌محور پشتیبانی می‌کند. SQLAlchemy Documentation +1 V2.1-E/F — Multi-OD Network Optimization & Time-Dependent Empty Wagon Network V2.1-E/F — Multi-OD Network Optimization & Time-Dependent Empty Wagon Network 1. هدف تا V2.1-D موتور ما قادر است یک یا چند سرویس را با: Infrastructure Schedule Formation Wagon Locomotive حل کند. اما هنوز یک سؤال بزرگ باقی است: وقتی چند OD همزمان از شبکه استفاده کنند، ظرفیت واقعی کل شبکه چقدر است؟ مثلاً: OD-01: Tehran → Khowaf OD-02: Tehran → Mashhad OD-03: Aprin → Khowaf OD-04: Khowaf → Zarand همه ممکن است از بخشی از: Block Station Junction Wagon Pool Locomotive Pool Terminal مشترک استفاده کنند. بنابراین: [ C_{OD1}+C_{OD2} ] الزاماً برابر ظرفیت واقعی شبکه نیست. 2. اصل مرکزی تعریف ظرفیت شبکه: [ \boxed{ C_N= \max \sum_{od,r,t} Q_{od,r,t}F_{od,r,t} } ] subject to: [ Infrastructure ] [ Schedule ] [ Formation ] [ WagonCycle ] [ EmptyWagonFlow ] [ LocomotiveCycle ] [ Station ] [ Junction ] [ Terminal ] [ Demand ] [ Policy ] 3. معماری MARKETPLACE │ ▼ DEMAND │ ┌────────────┼────────────┐ ▼ ▼ ▼ OD-01 OD-02 OD-03 │ │ │ └────────────┼────────────┘ ▼ ROUTE CANDIDATES │ ▼ TRAIN SERVICE CANDIDATES │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ FORMATION WAGON FLOW LOCO FLOW │ │ │ └────────────────┼────────────────┘ ▼ NETWORK SCHEDULER │ ┌───────────────────┼───────────────────┐ ▼ ▼ ▼ BLOCKS STATIONS JUNCTIONS │ │ │ └───────────────────┼───────────────────┘ ▼ EMPTY WAGON NETWORK │ ▼ INTEGRATED SOLUTION │ ▼ NETWORK VALIDATOR │ ▼ CAPACITY SEARCH │ ▼ PROOF │ ▼ MARKETPLACE ALLOCATION 4. ODPair @dataclass(frozen=True) class ODPair: id: str origin_station_id: str destination_station_id: str commodity_id: str | None wagon_type_id: str | None اما OD به تنهایی کافی نیست. زیرا یک OD می‌تواند چند Route داشته باشد. 5. NetworkRoute @dataclass(frozen=True) class NetworkRoute: id: str od_pair_id: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] distance_km: float nominal_running_time: int allowed_train_types: tuple[str, ...] بنابراین: OD ↓ Route 1 Route 2 Route 3 ممکن است. 6. Route Choice برای هر OD و Route: [ x_{od,r,t} ] تعداد سرویس تخصیص‌یافته است. اگر: OD-01 دو route داشته باشد: [ x_{01,R1}+x_{01,R2} ] تعداد کل trainهای OD را تشکیل می‌دهد. 7. Demand Constraint برای هر OD: [ \sum_{r,t} Q_{od,r,t} x_{od,r,t} \le D_{od,t} ] یعنی Solver نمی‌تواند بیشتر از تقاضای موجود سرویس ایجاد کند. 8. Transportable Demand همچنان سه سطح جدا داریم: [ D_{market} \neq D_{transportable} \neq D_{allocated} ] بنابراین: Market Demand ↓ Transportable Demand ↓ Capacity-Constrained Allocation 9. TrainServiceCandidate برای جلوگیری از explosion، قبل از CP-SAT candidate generation انجام می‌دهیم: @dataclass(frozen=True) class TrainServiceCandidate: id: str od_pair_id: str route_id: str train_type_id: str wagon_type_id: str direction: str load_state: str freight_capacity_t: float train_length_m: float train_weight_t: float required_wagons: int required_locomotives: int 10. Candidate Pruning Candidate قبل از Solver حذف می‌شود اگر: Commodity incompatible Wagon unavailable Station too short Train too heavy Route incompatible Locomotive insufficient Brake capability insufficient No valid formation این مرحله برای scalability حیاتی است. 11. Network Resource هر resource مشترک: @dataclass(frozen=True) class SharedResource: id: str resource_type: str capacity: float unit: str مثلاً: BLOCK STATION_TRACK JUNCTION TERMINAL WAGON_POOL LOCOMOTIVE_POOL BUFFER 12. Resource Usage @dataclass(frozen=True) class ResourceUsage: resource_id: str candidate_id: str quantity: float duration: int | None برای منابع aggregate: [ \sum_i Usage_{i,g} \le Capacity_g ] اما برای منابع زمانی مثل block، باید occupancy زمانی هم حفظ شود. 13. دو نوع Shared Resource Aggregate مثل: Wagon Pool Locomotive Pool Terminal daily throughput Time-Space مثل: Single Track Block Station Track Junction این دو نباید با یک constraint ساده مدل شوند. 14. Network Scheduler Interface: class NetworkScheduler: def solve( self, problem: NetworkSchedulingProblem, configuration: SolverConfiguration, ) -> NetworkSolution: ... Problem: @dataclass(frozen=True) class NetworkSchedulingProblem: od_pairs: tuple[ODPair, ...] routes: tuple[NetworkRoute, ...] candidates: tuple[TrainServiceCandidate, ...] infrastructure: InfrastructureMaster stations: tuple[Station, ...] shared_resources: tuple[SharedResource, ...] demands: tuple[Demand, ...] wagon_pools: tuple[WagonPool, ...] locomotives: tuple[Locomotive, ...] planning_start: int planning_end: int objective: ObjectiveDefinition 15. Decision Variables برای هر candidate: [ F_c ] تعداد train services. اگر زمان‌مند باشد: [ F_{c,t} ] برای route: [ x_{od,r,t} ] و برای انتخاب route: [ z_{od,r}\in{0,1} ] 16. Train Count [ F_c\ge0 ] و: [ F_c\in\mathbb{Z} ] 17. Freight Flow اگر هر train: [ Q_c ] تن ظرفیت حمل داشته باشد: Q_cF_c ] و: \sum_cQ_cF_c ] 18. Objective Objective قابل پیکربندی است. مثلاً: Max Freight [ \max\sum_c Q_cF_c ] Max Revenue [ \max\sum_c Revenue_cF_c ] Min Unserved Demand [ \min \sum_{od} (D_{od}-Served_{od}) ] Lexicographic 1. Max served freight 2. Min unserved demand 3. Min operating cost 4. Min schedule deviation 5. Min unnecessary empty movement 19. Hard vs Soft Constraints دو نوع‌اند: HARD SOFT Hard: Safety Track Conflict Station Length Wagon Availability Locomotive Availability Soft: Preferred departure Preferred route Baseline deviation Soft constraint نباید silently violated شود. 20. Shared Block Constraint برای block: [ \sum_{trains} Occupancy_{train,b} ] باید از resource capacity عبور نکند. در Single Track: Forward + Reverse یک resource مشترک دارند. در Double Track: Forward Resource Reverse Resource اما shared station/junction constraints همچنان برقرارند. 21. Network Schedule هر candidate به یک TrainRun واقعی یا generated TrainRun تبدیل می‌شود: Candidate ↓ TrainRun Instance ↓ Formation ↓ DirectedPath ↓ Time-Space Schedule پس Network Solver نباید یک مدل جدا از V2.1-C باشد. باید همان scheduling core را فراخوانی کند. 22. Architecture Rule این ساختار ممنوع است: Network Solver ↓ Custom separate scheduler ساختار درست: Network Solver ↓ Scheduling Problem Builder ↓ V2.1-C Scheduling Engine Network فقط تصمیم می‌گیرد: چه trainهایی، از کدام route، در چه تعداد؟ Scheduler تصمیم می‌گیرد: دقیقاً چه زمانی و از چه resources عبور کنند؟ 23. Empty Wagon Network حالا بخش دوم. برای هر wagon type: [ E_{j,w,t} ] موجودی empty wagon در location (j)، نوع (w)، زمان (t) است. 24. Empty Wagon Balance معادله اصلی: EmptyOut_{j,w,t} Load_{j,w,t} } ] این معادله باید یکی از core constraints باشد. 25. Loaded Flow اگر train یک freight flow را از A به B ببرد: A │ │ loaded ▼ B در B واگن تخلیه می‌شود. بنابراین: Unload یک ورودی به Empty Wagon Network ایجاد می‌کند. 26. Empty Return بعد: B │ │ empty ▼ A این movement خودش: زمان مسیر block capacity station capacity locomotive capacity ممکن است مصرف کند. این نکته بسیار مهم است. Empty return نباید خارج از railway network فرض شود. 27. Empty Wagon Route @dataclass(frozen=True) class EmptyWagonMovement: id: str wagon_type_id: str origin_station_id: str destination_station_id: str route_id: str | None start_time: int end_time: int quantity: int 28. Empty Flow خودش Capacity Consumes اگر 20 train loaded: A → B داریم و همه واگن‌ها باید: B → A برگردند، empty trains یا empty repositioning ممکن است بخشی از ظرفیت شبکه را مصرف کند. پس: [ C_{loaded} ] و: [ C_{empty} ] رقابت می‌کنند. 29. Empty Movement Types سه حالت را مدل می‌کنیم: Attached Empty واگن خالی داخل یک freight train برمی‌گردد. Empty Freight Train قطار اختصاصی empty wagons. Repositioning Movement حرکت repositioning برای تأمین demand آینده. 30. Empty Wagon Decision Variable [ e_{j,k,t} ] تعداد empty wagonهایی که از location (j) به (k) حرکت می‌کنند. و: [ e_{j,k,t}\ge0 ] 31. Wagon Balance برای هر node: Load_t ] و: [ Inventory_t\le BufferCapacity ] 32. Buffer Constraint [ 0 \le E_{j,w,t} \le B_{j,w} ] اگر: [ E=B ] دیگر empty return اضافی قابل پذیرش نیست. 33. Initial Inventory این مورد بسیار مهم است. برای هر location: @dataclass(frozen=True) class InitialWagonInventory: station_id: str wagon_type_id: str available_count: int بدون Initial Inventory، Empty Wagon Network ممکن است از هیچ واگن تولید کند. این یک خطای جدی مدل‌سازی است. 34. Final Inventory Scenario می‌تواند constraint داشته باشد: [ E_{j,w,T} \ge E^{minimum}_{j,w} ] تا Solver نتواند همه واگن‌ها را در پایان horizon مصرف کند و ظرفیت مصنوعی ایجاد کند. 35. Horizon Boundary دو policy: Open Horizon موجودی انتهای horizon آزاد است. Closed / Protected Horizon حداقل موجودی پایان horizon باید حفظ شود. این باید Scenario parameter باشد. 36. Wagon Cycle در Network حالا Wagon Cycle به شکل واقعی‌تر: Load ↓ Loaded Train ↓ Unload ↓ Empty Inventory ↓ Empty Movement ↓ Empty Inventory ↓ Next Load این یعنی Wagon Cycle دیگر فقط یک duration نیست؛ یک network state transition است. 37. Time-Expanded Network برای Production، Empty Wagon Network را می‌توان به صورت Time-Expanded Network مدل کرد: Station A @ t0 │ ├── wait ──► A @ t1 │ └── move ──► B @ t1 │ ▼ B @ t2 هر node: [ (j,t) ] و هر arc: [ (j,t)\rightarrow(k,t+\tau) ] است. 38. Empty Wagon Arc @dataclass(frozen=True) class EmptyWagonArc: id: str origin: str destination: str departure_time: int arrival_time: int wagon_type_id: str capacity: int resource_ids: tuple[str, ...] 39. Arc Capacity اگر arc از railway route عبور کند: [ e_a \le Capacity_a ] ولی در حالت دقیق‌تر، arc به train movement متصل می‌شود و block occupancy واقعی ایجاد می‌کند. بنابراین برای Production: Time-Expanded Flow باید با: Railway Schedule coupled باشد. 40. Coupling Loaded / Empty این coupling یکی از مهم‌ترین روابط کل سیستم است. اگر: [ F_{A\rightarrow B}=10 ] و هر train: [ 20 ] wagon نیاز داشته باشد: [ 200 ] wagon از A خارج می‌شود. پس در B: [ Unload=200 ] و سپس: [ EmptyReturn ] باید بتواند این 200 wagon را به موقع به A برگرداند. 41. نتیجه ممکن است: Infrastructure: 30 trains/day Wagons: enough total count But: Empty Return Capacity = 18 پس: [ C_{Integrated}\le18 ] حتی اگر خط از نظر loaded train ظرفیت بیشتری داشته باشد. 42. Locomotive Coupling Empty movement اگر قطار مستقل باشد: Empty Wagon Train خودش ممکن است locomotive بخواهد. پس: [ LocoDemand_{loaded} + LocoDemand_{empty} \le LocoCapacity ] 43. Terminal Coupling Unload و Load نیز terminal resource مصرف می‌کنند. مثلاً: [ LoadOperations_t \le TerminalCapacity_t ] و: [ UnloadOperations_t \le TerminalCapacity_t ] بنابراین یک OD ممکن است ظرفیت دیگری را در terminal محدود کند. 44. Network Flow Model برای هر wagon type: Flow_{out} + Load + Final ] این باید برای تمام: [ (j,w,t) ] ها برقرار باشد. 45. Shared Wagon Pool اگر دو OD از یک pool استفاده کنند: OD-01 ──┐ ├── Wagon Pool WP-01 OD-02 ──┘ داریم: [ \sum_{od}W_{od,t} \le WPool_t ] 46. Shared Locomotive Pool مشابه: OD-01 ──┐ OD-02 ──┼── Loco Pool OD-03 ──┘ و: [ \sum_{services}LocoUsage \le LocoAvailability ] با cycle زمانی واقعی. 47. Network Integrated Problem در نهایت: [ \boxed{ \max \sum_{od,r,t} Q_{od,r,t}F_{od,r,t} } ] subject to: Demand [ F_{od,r,t} \le D_{od,t} ] Schedule [ Schedule(F) \ feasible ] Infrastructure [ ResourceOccupancy \ feasible ] Formation [ Formation(F) \ feasible ] Wagon [ WagonBalance(F,E) \ feasible ] Empty Flow [ EmptyNetwork(E) \ feasible ] Locomotive [ LocoCycle(F,E) \ feasible ] Terminal [ Terminal(F,E) \ feasible ] Policy [ Policy(F)\ feasible ] 48. Solver Architecture مدل را یک monolithic class نمی‌سازیم. NetworkModelBuilder │ ├── DemandConstraintBuilder ├── CandidateBuilder ├── RouteChoiceBuilder ├── ScheduleConstraintBuilder ├── SharedResourceBuilder ├── WagonFlowBuilder ├── EmptyFlowBuilder ├── LocomotiveFlowBuilder ├── TerminalBuilder ├── PolicyBuilder └── ObjectiveBuilder 49. Network Solution @dataclass(frozen=True) class NetworkSolution: train_services: tuple[TrainServiceAllocation, ...] schedules: tuple[TrainSchedule, ...] formations: tuple[TrainFormation, ...] wagon_movements: tuple[WagonMovement, ...] empty_movements: tuple[EmptyWagonMovement, ...] locomotive_assignments: tuple[LocomotiveAssignment, ...] served_demand: tuple[DemandAllocation, ...] resource_occupations: tuple[ResourceOccupation, ...] objective_value: float 50. Network Validator class NetworkValidator: def validate( self, solution, problem, ) -> NetworkValidationReport: ... Rules: N01 Demand N02 Route N03 Train Count N04 Block N05 Station N06 Junction N07 Formation N08 Wagon Inventory N09 Wagon Balance N10 Empty Flow N11 Buffer N12 Locomotive Assignment N13 Locomotive Cycle N14 Terminal N15 Policy N16 Horizon Boundary 51. Empty Flow Validator برای هر: [ (j,w,t) ] باید بررسی شود: E_t In Unload + Out + Load =0 ] هر deviation غیر صفر: EMPTY_FLOW_BALANCE_VIOLATION است. 52. Network Capacity Search در V2.1-E/F ظرفیت دیگر فقط: [ F+1 ] برای یک OD نیست. می‌تواند objective-based باشد. مثلاً: [ Z(F)=TotalServedFreight ] و proof: [ Z^* ] به‌عنوان optimum. اگر بخواهیم capacity را بر اساس تعداد train بسنجیم: \sum_{od,r,t}F_{od,r,t} ] و: [ F_{total}+1 ] را آزمایش می‌کنیم. اما برای شبکه‌های چندکالایی، proof باید دقیقاً مشخص کند objective چه بوده است. 53. دو نوع Network Proof Capacity Count Proof [ F^* ] حداکثر تعداد train. Freight Objective Proof \max Freight ] حداکثر tonnage. این دو نباید در UI با یکدیگر مخلوط شوند. 54. Bottleneck Attribution حالا bottleneck می‌تواند: INFRASTRUCTURE WAGON EMPTY_FLOW LOCOMOTIVE TERMINAL DEMAND POLICY باشد. اما یک bottleneck ممکن است interaction باشد: Single Track + Empty Return + Locomotive Turnback بنابراین Evidence Graph لازم داریم. 55. Resource Interaction Graph Single Track B03 │ ┌──────┴──────┐ ▼ ▼ Loaded Flow Empty Flow │ │ ▼ ▼ Wagon Pool Loco Pool │ │ └──────┬──────┘ ▼ Capacity این Graph برای Explanation بسیار ارزشمند است. 56. Marginal Scenario Engine حالا می‌توانیم آزمایش کنیم: Base + 50 Wagons + 1 Locomotive + 1 Station Track + 10% Demand + Double Track B03 + 5 min lower running time هر Scenario: [ Run(Scenario_i) ] و: C_i-C_{base} ] 57. Important Rule هیچ‌گاه نگوییم: Adding 50 wagons = +5 trains مگر اینکه Solver واقعاً Scenario را حل کرده باشد. چون ممکن است: Infrastructure یا: Loco همچنان bottleneck باشد. 58. Multi-OD Example فرض ساختاری: OD-01 A → D Demand = 1000t OD-02 A → E Demand = 800t هر دو: A → B را مشترک دارند. اگر capacity مشترک: [ C_{AB}=20 ] باشد، نمی‌توان: [ F_{01}=20 ] و: [ F_{02}=20 ] را همزمان پذیرفت. Constraint: [ F_{01}+F_{02}\le20 ] 59. Empty Interaction Example حالا اگر OD-01: A → D واگن‌ها را در D آزاد کند و OD-02 به واگن در A نیاز داشته باشد، empty repositioning: D → A لازم می‌شود. بنابراین OD-01 و OD-02 ممکن است از طریق wagon network به یکدیگر وابسته شوند، حتی اگر route آن‌ها کاملاً یکسان نباشد. این دقیقاً دلیل ت��دیل Empty Wagon به Network Flow است. 60. Market Allocation پس از optimization: Demand ↓ Network Capacity ↓ Allocated ↓ Unserved برای هر OD: @dataclass(frozen=True) class DemandAllocation: od_pair_id: str requested_tons: float transportable_tons: float allocated_tons: float unserved_tons: float train_count: int route_id: str | None 61. Marketplace Capacity Offer در نهایت Offer: @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str planning_window: str train_capacity: int freight_capacity_t: float wagon_type_id: str | None train_type_id: str | None confidence: str proof_id: str data_version_id: str scenario_id: str model_version: str 62. Traceability نهایی Market Request ↓ Market Demand ↓ OD Pair ↓ Freight Flow ↓ Train Service Candidate ↓ Route Choice ↓ Formation ↓ Loaded Train ↓ Unload ↓ Empty Wagon Flow ↓ Empty Return ↓ Wagon Cycle ↓ Locomotive Cycle ↓ Network Schedule ↓ Validation ↓ Capacity Proof ↓ Capacity Offer ↓ Marketplace Allocation 63. Database Tables V2.1-E/F این جداول را اضافه می‌کند: od_pair network_route train_service_candidate train_service_allocation shared_resource resource_usage resource_occupation demand demand_allocation freight_flow wagon_requirement wagon_inventory wagon_movement empty_wagon_movement empty_wagon_arc wagon_flow_state locomotive_assignment locomotive_cycle terminal_resource terminal_operation network_solution network_validation network_capacity_result network_capacity_proof capacity_offer capacity_allocation capacity_evidence 64. Indexهای مهم برای Production DB: od_pair(origin, destination) network_route(od_pair_id) train_service_candidate(od_pair_id, route_id) wagon_inventory(station_id, wagon_type_id, timestamp) empty_wagon_movement(origin, destination, departure_time) resource_occupation(resource_id, start_time, end_time) demand(od_pair_id, time_bucket) capacity_offer(od_pair_id, route_id) این قسمت برای performance بسیار مهم خواهد بود. 65. API Network POST /api/v1/network/runs GET /api/v1/network/runs/{run_id} GET /api/v1/network/runs/{run_id}/schedule GET /api/v1/network/runs/{run_id}/allocations GET /api/v1/network/runs/{run_id}/resources GET /api/v1/network/runs/{run_id}/bottlenecks GET /api/v1/network/runs/{run_id}/validation Empty Wagon GET /api/v1/network/{run_id}/wagon-flow GET /api/v1/network/{run_id}/empty-flow GET /api/v1/network/{run_id}/wagon-inventory GET /api/v1/network/{run_id}/buffers Capacity POST /api/v1/network/capacity GET /api/v1/network/capacity/{run_id} GET /api/v1/network/capacity/{run_id}/proof 66. UI — Network Explorer ┌─────────────────────────────────────────────────────┐ │ NETWORK CAPACITY │ ├─────────────────────────────────────────────────────┤ │ Total Served Freight --- t │ │ Train Services --- │ │ Integrated Capacity --- │ │ Unserved Demand --- t │ ├─────────────────────────────────────────────────────┤ │ OD Allocation │ │ │ │ Tehran → Khowaf --- trains --- t │ │ Tehran → Mashhad --- trains --- t │ │ Aprin → Khowaf --- trains --- t │ ├─────────────────────────────────────────────────────┤ │ Shared Resources │ │ │ │ B03 ---% SINGLE │ │ S05-T2 ---% STATION │ │ J07 ---% JUNCTION │ │ WP-01 ---% WAGON │ │ LP-01 ---% LOCOMOTIVE │ └─────────────────────────────────────────────────────┘ 67. Empty Wagon Network UI ┌───────────┐ │ Tehran │ │ Empty: 40 │ └─────┬─────┘ │ 20 wagons │ ▼ ┌───────────┐ │ Khowaf │ │ Empty: 15 │ └─────┬─────┘ │ 10 wagons │ ▼ ┌───────────┐ │ Mashhad │ │ Empty: 25 │ └───────────┘ کاربر باید بتواند timeline را نیز ببیند، نه فقط flow aggregate. 68. Time-Space + Empty Flow UI نهایی باید بتواند این دو را کنار هم نمایش دهد: TIME │ │ Loaded Train │ ╲ │ ╲ │ ╲ │ ╲ │ Destination │ │ │ │ Empty │ ╲ │ ╲ │ ╲ │ Origin └──────────────────────────── DISTANCE این visualization به planner نشان می‌دهد که empty return دقیقاً چه زمانی و کجا ظرفیت مصرف کرده است. 69. Golden Test — Multi-OD Shared Block A ===== B ===== C │ └===== D OD1: A → C OD2: A → D هر دو B03 را استفاده می‌کنند. Expected: [ F_{OD1}+F_{OD2} \le C_{B03} ] 70. Golden Test — Shared Wagon Pool OD1 needs 50 wagons OD2 needs 40 wagons Pool = 70 Expected: [ 50+40>70 ] پس allocation همزمان کامل ممکن نیست. Solver باید allocation را بهینه کند. 71. Golden Test — Empty Flow Initial: [ E_A=50 ] OD1: [ Load_A=40 ] پس: [ E_A=10 ] بعد unload در B: [ Unload_B=40 ] پس: [ E_B=40 ] و اگر: [ EmptyReturn_{B\rightarrow A}=30 ] آنگاه: [ E_B=10 ] و: [ E_A=40 ] این balance باید دقیقاً توسط Validator تأیید شود. 72. Golden Test — Empty Capacity Conflict فرض: Loaded: A → B = 10 trains Empty: B → A = 10 movements و Single Track: A ===== B در این حالت empty movement و loaded movement یک resource را share می‌کنند. پس ظرفیت واقعی ممکن است به دلیل empty return کاهش یابد. این test برای ما بسیار مهم است. 73. Golden Test — Locomotive Coupling فرض: 8 loaded services + 4 empty services و: Loco capacity = 10 Expected: [ 8+4=12>10 ] پس allocation باید تغییر کند یا بخشی از demand unserved شود. 74. Golden Test — Buffer فرض: [ Buffer_B=20 ] و: [ Unload_B=30 ] بدون empty dispatch: [ 30>20 ] پس solution infeasible است. اما اگر: [ EmptyOut_B=15 ] باشد: [ 30-15=15 ] و buffer feasible می‌شود. 75. Golden Test — End Horizon اگر: [ E_A(0)=50 ] و: [ MinimumFinalInventory_A=30 ] Solver نباید همه 50 را مصرف کند و: [ E_A(T)=0 ] تحویل دهد. باید: [ E_A(T)\ge30 ] رعایت شود. 76. Network Bottleneck Bottleneck Engine اکنون می‌تواند این خروجی را بسازد: Resource Impact ------------------------------------------------ B03 Single Track -4 trains Wagon Pool WP-01 -3 trains Loco Pool LP-01 -2 trains Terminal T-05 -1 train Demand OD-04 -0 trains اما این اعداد فقط پس از Scenario Re-Solve معتبرند. 77. Network Capacity Proof Proof باید شامل: Objective Optimal Value Solver Status Independent Validation Resource Feasibility Demand Feasibility Wagon Flow Feasibility Locomotive Feasibility Evidence باشد. برای مثال: { "objective": "MAX_SERVED_FREIGHT", "objective_value": 185000, "solver_status": "OPTIMAL", "network_validated": true, "wagon_flow_validated": true, "locomotive_cycle_validated": true, "proof_valid": true } 78. Solver Status و Proof همچنان: OPTIMAL قوی‌ترین حالت برای اثبات optimum است. اگر: FEASIBLE باشد ولی هنوز امکان بهبود وجود داشته باشد، نباید آن را بدون qualification به‌عنوان optimum قطعی معرفی کنیم. و: UNKNOWN TIME_LIMIT نباید به: INFEASIBLE تبدیل شوند. 79. Scaling Strategy برای Production، یک مدل عظیم برای کل شبکه و کل horizon ایجاد نمی‌کنیم. چهار لایه: Layer 1 Candidate Pruning Layer 2 Aggregate Network Optimization Layer 3 Detailed Time-Space Scheduling Layer 4 Independent Validation داریم. 80. Rolling Horizon برای horizon بزرگ: Day 1 Day 2 Day 3 ... یا: 06:00–12:00 12:00–18:00 18:00–24:00 می‌توانیم rolling horizon داشته باشیم. اما باید state انتقالی حفظ شود: Wagon Inventory Loco Position Train Occupancy Terminal State 81. State Snapshot بین دو window: @dataclass(frozen=True) class NetworkStateSnapshot: timestamp: int wagon_inventory: dict[tuple[str, str], int] locomotive_locations: dict[str, str] locomotive_available_at: dict[str, int] terminal_inventory: dict[str, float] open_train_services: tuple[str, ...] این برای حل روزانه/ساعتی بسیار مهم است. 82. Run Reproducibility Network Run اکنون: f( DataVersion, Scenario, ModelVersion, SolverConfiguration ) ] است. و snapshot: [ InputHash ] را نگه می‌دارد. بنابراین اگر: same Data same Scenario same Model same Solver Config داشته باشیم، run قابل بازتولید است. 83. V2.1-E/F Project Structure app/ ├── engines/ │ ├── network/ │ │ ├── candidate_generator.py │ │ ├── route_choice.py │ │ ├── network_model.py │ │ ├── network_solver.py │ │ ├── resource_model.py │ │ ├── demand_model.py │ │ └── capacity.py │ │ │ ├── wagon_flow/ │ │ ├── inventory.py │ │ ├── empty_flow.py │ │ ├── time_expanded.py │ │ ├── balance.py │ │ └── cycle.py │ │ │ └── locomotive_flow/ │ ├── availability.py │ ├── assignment.py │ └── cycle.py │ ├── validation/ │ ├── network/ │ │ ├── network_validator.py │ │ ├── demand_validator.py │ │ ├── resource_validator.py │ │ └── flow_validator.py │ │ │ └── rolling_stock/ │ ├── wagon_validator.py │ └── locomotive_validator.py │ └── explanation/ ├── network_bottleneck.py ├── resource_interaction.py ├── empty_flow.py └── capacity_proof.py 84. Application Service یک orchestration service: class NetworkCapacityService: def run( self, scenario_id: str, data_version_id: str, ): data = self.load_canonical_data() demand = self.load_demand() candidates = self.generate_candidates( data, demand, ) candidates = self.prune( candidates ) problem = self.build_problem( candidates, data, demand, ) solution = self.solve( problem ) validation = self.validate( solution, problem, ) result = self.build_result( solution, validation, ) return result 85. مهم: UI مستقیماً Solver را صدا نمی‌زند ساختار: React UI ↓ FastAPI ↓ NetworkCapacityService ↓ NetworkModelBuilder ↓ Solver ↓ Validator ↓ Result ↓ API ↓ UI نه: React ↓ CP-SAT 86. Data Layer Canonical Data باید شامل: Infrastructure Stations Blocks Routes Train Types Train Runs Demands Freight Flows Wagons Wagon Pools Locomotives Terminals Operational Windows باشد. Marketplace Data از طریق adapter وارد Canonical Demand می‌شود. 87. Marketplace Boundary Marketplace ↓ Market Adapter ↓ Market Demand ↓ Demand Normalization ↓ OD / Commodity ↓ Network Capacity Marketplace نباید مستقیماً variables Solver را تعیین کند. 88. Policy Policy می‌تواند مثلاً: [ F_{OD1}\ge10 ] باشد. یا: [ Allocated_{OD2}\ge500t ] Policy به‌عنوان constraint وارد Solver می‌شود، نه به‌عنوان hard-coded business logic. 89. Scenario Example BASE در مقابل: SCENARIO-01 +50 wagons +1 locomotive و: SCENARIO-02 B03 SINGLE → DOUBLE و: SCENARIO-03 Demand +20% و: SCENARIO-04 Empty Buffer +100 هرکدام full network re-solve می‌شوند. 90. Investment Decision Support در Scenario: B03 SINGLE → DOUBLE صرفاً نباید بگوییم: ظرفیت افزایش می‌یابد. باید: Base Run ↓ Investment Scenario ↓ Full Re-Solve ↓ Validation ↓ Capacity Delta ↓ Served Freight Delta ↓ Bottleneck Migration محاسبه شود. ممکن است پس از Double Track شدن B03، bottleneck به: Station S05 منتقل شود. این Bottleneck Migration باید در سیستم ثبت شود. 91. Network Bottleneck Migration مثلاً: BASE B03 Single Track → Capacity limiting resource بعد: SCENARIO B03 Double Track → no longer binding New binding: Station S05 Track این برای Decision Support بسیار ارزشمند است. 92. Final Capacity Layers از اینجا محصول ما می‌تواند همزمان این‌ها را نشان دهد: Infrastructure Capacity Operational Capacity Rolling Stock Capacity Network Capacity Transportable Capacity Allocated Capacity و: [ Allocated \le Transportable \le Network ] در سطح business، با توجه به تعریف دقیق هر profile و horizon. 93. تعریف نهایی Network Capacity [ \boxed{ C_N = \max \left{ \sum_{od,r,t} Q_{od,r,t}F_{od,r,t} : \begin{array}{l} Schedule\ feasible\ Infrastructure\ feasible\ Formation\ feasible\ WagonCycle\ feasible\ EmptyFlow\ feasible\ LocomotiveCycle\ feasible\ Station\ feasible\ Junction\ feasible\ Terminal\ feasible\ Demand\ feasible\ Policy\ feasible \end{array} \right} } ] این از اینجا به بعد تعریف رسمی Network Capacity Engine پروژه خواهد بود. 94. Definition of Done — V2.1-E/F Multi-OD ✓ Multiple OD ✓ Multiple Routes ✓ Route Choice ✓ Split Flow ✓ Shared Blocks ✓ Shared Stations ✓ Shared Junctions ✓ Shared Wagons ✓ Shared Locomotives ✓ Shared Terminals ✓ Demand Constraints ✓ Policy Constraints ✓ Network Scheduling ✓ Network Validation ✓ Network Capacity ✓ Network Proof Empty Wagon Network ✓ Initial Inventory ✓ Loaded Flow ✓ Unload Event ✓ Empty Inventory ✓ Empty Movement ✓ Empty Train ✓ Repositioning ✓ Wagon Balance ✓ Buffer Capacity ✓ Final Inventory ✓ Time-Expanded Flow ✓ Railway Resource Coupling ✓ Locomotive Coupling ✓ Terminal Coupling ✓ Empty Flow Validation ✓ Empty Flow Bottleneck ✓ Empty Flow Scenario 95. جایگاه محصول پس از V2.1-F در این نقطه معماری محصول به این سطح می‌رسد: MARKETPLACE │ ▼ DEMAND │ ▼ MULTI-OD MODEL │ ┌────────────┼────────────┐ ▼ ▼ ▼ ROUTES FORMATION POLICY │ │ └─────┬──────┘ ▼ TRAIN SERVICES │ ┌────────────┼────────────┐ ▼ ▼ ▼ SCHEDULE WAGON LOCO │ CYCLE CYCLE │ │ │ │ ▼ │ │ EMPTY NETWORK │ │ │ │ └─────────────┼─────────────┘ ▼ NETWORK SOLVER │ ▼ INTEGRATED SCHEDULE │ ▼ INDEPENDENT VALIDATOR │ ▼ CAPACITY PROOF │ ┌──────────┴──────────┐ ▼ ▼ BOTTLENECKS EXPLANATION │ │ └──────────┬──────────┘ ▼ CAPACITY OFFER │ ▼ MARKETPLACE 96. نتیجه از این مرحله به بعد، پروژه دیگر صرفاً یک Route Capacity Calculator نیست. تعریف محصول اکنون: OD-Centric, Time-Space, Rolling-Stock-Constrained, Multi-OD Railway Network Capacity Optimization Engine است که در آن: [ \boxed{ Market\ Demand \rightarrow OD \rightarrow Train \rightarrow Formation \rightarrow Schedule \rightarrow Wagon\ Cycle \rightarrow Empty\ Flow \rightarrow Locomotive\ Cycle \rightarrow Network \rightarrow Capacity \rightarrow Allocation } ] و مهم‌تر از همه، Empty Wagon Network داخل خود مسئله ظرفیت قرار گرفته است، نه خارج از آن. این تصمیم معماری، پایه‌ی مرحله بعدی یعنی V2.2 — Production Implementation & Executable Network Solver خواهد بود؛ در آن مرحله دیگر صرفاً specification نمی‌نویسیم و باید این مدل را به packageهای Python، مدل‌های SQLAlchemy، CP-SAT model builders، validatorها، migrationها و Golden/E2E tests تبدیل کنیم تا زنجیره‌ی واقعی Access/Excel → Network Solver → Proven Capacity Offer قابل اجرا شود. --- ## ادامه 50 — پرسش کاربر (`message-id:9d10a40c-67ea-4195-8b45-64dff879e37e`) هر دو مورد لطفا --- ## ادامه 50 — پاسخ دستیار (`message-id:97c9ab23-7bc6-4e57-b6ca-3ec4226f26ae`) حتماً. این مرحله را هم هر دو بخش را با هم جلو می‌بریم: V2.2-A — Production Database/Domain Implementation V2.2-B — Executable Multi-OD + Empty-Wagon Network Solver این بار تمرکز را از Specification به Implementation واقعی و قابل تست می‌بریم. برای Scheduling/Constraint Optimization، استفاده از CP-SAT با معماری فعلی مناسب است؛ مستندات رسمی OR-Tools نیز CP-SAT را برای constraint optimization و scheduling پشتیبانی می‌کنند. Google for Developers +1 برای ORM نیز الگوی SQLAlchemy 2.x مبتنی بر DeclarativeBase/Mapped/mapped_column() را مبنا می‌گیریم. SQLAlchemy Documentation +1 V2.2-A — Production Database & Domain 1. ساختار نهایی railway_capacity_workbench/ │ ├── app/ │ ├── domain/ │ │ ├── network/ │ │ │ ├── od.py │ │ │ ├── route.py │ │ │ ├── candidate.py │ │ │ ├── resource.py │ │ │ └── solution.py │ │ │ │ │ ├── wagon/ │ │ │ ├── inventory.py │ │ │ ├── movement.py │ │ │ └── flow.py │ │ │ │ │ └── locomotive/ │ │ ├── assignment.py │ │ └── cycle.py │ │ │ ├── db/ │ │ ├── base.py │ │ ├── session.py │ │ └── models/ │ │ ├── od.py │ │ ├── route.py │ │ ├── demand.py │ │ ├── resource.py │ │ ├── wagon.py │ │ ├── locomotive.py │ │ └── network_run.py │ │ │ ├── engines/ │ │ ├── network/ │ │ │ ├── candidate_generator.py │ │ │ ├── model.py │ │ │ ├── solver.py │ │ │ └── capacity.py │ │ │ │ │ ├── wagon_flow/ │ │ │ ├── balance.py │ │ │ └── empty_flow.py │ │ │ │ │ └── scheduling/ │ │ └── scheduler.py │ │ │ ├── validation/ │ │ ├── network.py │ │ ├── wagon.py │ │ └── capacity.py │ │ │ └── services/ │ └── network_capacity.py │ ├── alembic/ │ └── tests/ ├── unit/ ├── golden/ └── e2e/ 2. Domain Contract OD Python from dataclasses import dataclass @dataclass(frozen=True) class ODPair: id: str origin_station_id: str destination_station_id: str commodity_id: str | None = None wagon_type_id: str | None = None Route Python @dataclass(frozen=True) class NetworkRoute: id: str od_pair_id: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] distance_km: float nominal_running_time_min: int Candidate Python @dataclass(frozen=True) class TrainServiceCandidate: id: str od_pair_id: str route_id: str train_type_id: str wagon_type_id: str freight_capacity_t: int train_length_m: int train_weight_t: int required_wagons: int required_locomotives: int 3. Wagon Flow Contract Python @dataclass(frozen=True) class WagonInventoryState: station_id: str wagon_type_id: str timestamp: int quantity: int Python @dataclass(frozen=True) class EmptyWagonMovement: id: str wagon_type_id: str origin_station_id: str destination_station_id: str departure_time: int arrival_time: int quantity: int route_id: str | None = None 4. Network Solution Python @dataclass(frozen=True) class TrainServiceAllocation: candidate_id: str train_count: int freight_tons: int Python @dataclass(frozen=True) class NetworkSolution: allocations: tuple[TrainServiceAllocation, ...] empty_movements: tuple[EmptyWagonMovement, ...] objective_value: int solver_status: str 5. SQLAlchemy Base ساختار ORM بر مبنای Declarative Mapping مدرن SQLAlchemy 2.x خواهد بود. SQLAlchemy Documentation +1 Python from sqlalchemy import MetaData from sqlalchemy.orm import DeclarativeBase NAMING_CONVENTION = { "ix": "ix_%(column_0_label)s", "uq": "uq_%(table_name)s_%(column_0_name)s", "ck": "ck_%(table_name)s_%(constraint_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s", } class Base(DeclarativeBase): metadata = MetaData( naming_convention=NAMING_CONVENTION ) 6. OD ORM Python from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column class ODPairModel(Base): __tablename__ = "od_pair" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) origin_station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) destination_station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) commodity_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) wagon_type_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) 7. Network Route ORM Python class NetworkRouteModel(Base): __tablename__ = "network_route" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) od_pair_id: Mapped[str] = mapped_column( ForeignKey("od_pair.id"), index=True, ) distance_km: Mapped[float] nominal_running_time_min: Mapped[int] مسیر station/block sequence بهتر است در جدول child ذخیره شود: network_route_station network_route_block نه به شکل JSON غیرقابل query. 8. Demand Python class DemandModel(Base): __tablename__ = "demand" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) od_pair_id: Mapped[str] = mapped_column( ForeignKey("od_pair.id"), index=True, ) time_bucket: Mapped[int] = mapped_column( index=True ) requested_tons: Mapped[int] minimum_service_tons: Mapped[int] = 0 9. Wagon Inventory Python class WagonInventoryModel(Base): __tablename__ = "wagon_inventory" id: Mapped[int] = mapped_column( primary_key=True ) station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) wagon_type_id: Mapped[str] = mapped_column( index=True ) timestamp: Mapped[int] = mapped_column( index=True ) quantity: Mapped[int] 10. Empty Wagon Movement Python class EmptyWagonMovementModel(Base): __tablename__ = "empty_wagon_movement" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) wagon_type_id: Mapped[str] = mapped_column( index=True ) origin_station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) destination_station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) departure_time: Mapped[int] = mapped_column( index=True ) arrival_time: Mapped[int] = mapped_column( index=True ) quantity: Mapped[int] 11. Network Run Python class NetworkRunModel(Base): __tablename__ = "network_run" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) scenario_id: Mapped[str] = mapped_column( ForeignKey("scenario.id"), index=True, ) data_version_id: Mapped[str] = mapped_column( ForeignKey("data_version.id"), index=True, ) model_version: Mapped[str] solver_status: Mapped[str] objective_value: Mapped[int | None] input_snapshot_hash: Mapped[str | None] solver_configuration_hash: Mapped[str | None] V2.2-B — Executable Network Solver حالا به بخش مهم می‌رسیم. 12. مدل CP-SAT OR-Tools، CP-SAT را برای constraint optimization و scheduling ارائه می‌کند و مدل‌های آن بر متغیرها، constraints و objective بنا می‌شوند. Google for Developers +1 مدل اولیه: Python from ortools.sat.python import cp_model class NetworkModelBuilder: def __init__(self): self.model = cp_model.CpModel() self.train_vars = {} 13. Train Count Variables برای هر candidate: Python def add_train_variables( self, candidates, upper_bound: int, ): for candidate in candidates: self.train_vars[candidate.id] = ( self.model.new_int_var( 0, upper_bound, f"trains_{candidate.id}", ) ) 14. Demand Constraint برای هر OD: Python def add_demand_constraints( self, candidates, demands, ): by_od = {} for candidate in candidates: by_od.setdefault( candidate.od_pair_id, [] ).append(candidate) for demand in demands: vars_ = [ self.train_vars[c.id] * c.freight_capacity_t for c in by_od.get( demand.od_pair_id, [] ) ] if vars_: self.model.add( sum(vars_) <= demand.requested_tons ) این اولین coupling واقعی بین: Demand ↓ Train ↓ Freight است. 15. Shared Block Capacity فرض کنیم هر candidate تعداد مشخصی train روی block مصرف می‌کند: Python @dataclass(frozen=True) class BlockCapacity: block_id: str max_train_count: int Constraint: Python def add_block_capacity( self, candidates, block_capacities, candidate_blocks, ): for block in block_capacities: usages = [] for candidate in candidates: if block.block_id in candidate_blocks[ candidate.id ]: usages.append( self.train_vars[candidate.id] ) if usages: self.model.add( sum(usages) <= block.max_train_count ) این نسخه aggregate است. نسخه Production باید به scheduler time-space متصل شود. 16. Shared Wagon Pool Python def add_wagon_pool_constraints( self, candidates, wagon_capacity, ): wagon_usage = [] for candidate in candidates: wagon_usage.append( self.train_vars[candidate.id] * candidate.required_wagons ) self.model.add( sum(wagon_usage) <= wagon_capacity ) این فقط pool capacity است. برای Production، cycle و timestamp جایگزین این constraint ساده خواهد شد. 17. Locomotive Pool Python def add_locomotive_constraints( self, candidates, locomotive_capacity, ): usage = [] for candidate in candidates: usage.append( self.train_vars[candidate.id] * candidate.required_locomotives ) self.model.add( sum(usage) <= locomotive_capacity ) 18. Objective Python def add_objective(self, candidates): objective_terms = [] for candidate in candidates: objective_terms.append( self.train_vars[candidate.id] * candidate.freight_capacity_t ) self.model.maximize( sum(objective_terms) ) در نتیجه: max c ∑ ​ Q c ​ F c ​ 19. Solver Python from ortools.sat.python import cp_model class NetworkSolver: def solve( self, model: cp_model.CpModel, config, ): solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = ( config.time_limit_seconds ) solver.parameters.random_seed = ( config.random_seed ) solver.parameters.num_search_workers = ( config.num_workers ) status = solver.solve(model) return solver, status تنظیم explicit time limit برای Production مهم است؛ OR-Tools نیز برای مسائل طولانی، محدودکردن زمان solver را به‌صورت رسمی پشتیبانی می‌کند. Google for Developers 20. Status Mapping نباید status خام OR-Tools را مستقیماً وارد Domain کنیم. Python def map_status(status): if status == cp_model.OPTIMAL: return "OPTIMAL" if status == cp_model.FEASIBLE: return "FEASIBLE" if status == cp_model.INFEASIBLE: return "INFEASIBLE" if status == cp_model.MODEL_INVALID: return "MODEL_INVALID" return "UNKNOWN" و اصل پروژه: UNKNOWN != INFEASIBLE حتماً حفظ می‌شود. 21. Empty Wagon Balance Builder این بخش را جدا از NetworkModelBuilder نگه می‌داریم. Python class EmptyWagonFlowBuilder: def __init__(self, model): self.model = model self.inventory_vars = {} self.empty_flow_vars = {} برای هر: station wagon_type time موجودی: Python inventory = model.new_int_var( 0, buffer_capacity, f"empty_{station}_{wagon}_{t}" ) 22. Empty Movement Variable Python flow = model.new_int_var( 0, max_empty_flow, f"empty_flow_{origin}_{destination}_{t}" ) 23. Balance Equation برای هر state: Python model.add( inventory_next == inventory_current + empty_in + unload - empty_out - load ) دقیقاً: E j,w,t+1 ​ =E j,w,t ​ +EmptyIn+Unload−EmptyOut−Load 24. Initial Inventory Python model.add( inventory_vars[ station, wagon, first_time, ] == initial_inventory ) 25. Final Inventory در حالت protected horizon: Python model.add( inventory_vars[ station, wagon, final_time, ] >= minimum_final_inventory ) 26. Coupling با Loaded Train اگر: TrainServiceCandidate.required_wagons = 20 و: TrainCount = 5 آنگاه: Load=100 و این مقدار باید وارد Wagon Balance شود. 27. Coupling با Unload همان 100 واگن بعد از رسیدن به destination: Unload=100 خواهد شد. در نتیجه: Loaded Train ↓ Destination ↓ Empty Inventory +100 28. Coupling با Empty Return بعد Solver می‌تواند: EmptyOut destination ​ را انتخاب کند. مثلاً: 100 arrived 80 returned 20 remain و بنابراین: E destination ​ =20 29. نکته مهم معماری در نسخه اول اجرایی V2.2، Empty Flow را به صورت aggregate/time-expanded پیاده می‌کنیم. اما در نسخه Production کامل: Empty Flow ↓ Train Service ↓ Directed Path ↓ Block Occupancy ↓ Schedule خواهد رفت. یعنی Empty Wagon Train هم در نهایت یک Railway Movement واقعی خواهد بود. 30. Network Result Python @dataclass(frozen=True) class NetworkResult: run_id: str solver_status: str objective_value: int served_freight_tons: int train_count: int allocations: tuple empty_wagon_states: tuple resource_utilization: tuple validation_passed: bool capacity_proven: bool 31. Independent Validator حتی اگر Solver جواب بدهد: OPTIMAL هنوز Result معتبر تلقی نمی‌شود. ابتدا: Solver ↓ Solution ↓ Independent Validator ↓ Validated Solution 32. Validator — Demand Python def validate_demand(solution, demand): for allocation in solution.allocations: if allocation.freight_tons < 0: raise ValidationError( "NEGATIVE_FREIGHT" ) if ( allocation.freight_tons > demand[allocation.od_pair_id] ): raise ValidationError( "DEMAND_EXCEEDED" ) 33. Validator — Wagon Balance Python def validate_wagon_balance( before, after, empty_in, unload, empty_out, load, ): expected = ( before + empty_in + unload - empty_out - load ) if after != expected: raise ValidationError( "EMPTY_WAGON_BALANCE_VIOLATION" ) 34. Validator — Buffer Python if inventory < 0: raise ValidationError( "NEGATIVE_WAGON_INVENTORY" ) if inventory > buffer_capacity: raise ValidationError( "WAGON_BUFFER_EXCEEDED" ) 35. Capacity Proof برای integrated network: Python @dataclass(frozen=True) class NetworkCapacityProof: objective_name: str optimum_value: int solver_status: str independently_validated: bool proof_valid: bool evidence_ids: tuple[str, ...] اگر هدف freight باشد: Z ∗ =maxFreight و solver: OPTIMAL و validator: PASS آنگاه proof معتبر است، مشروط به اینکه definition of proof برای آن objective کامل باشد. 36. Proof of F+1 برای capacity count: Solve(F) Validate(F) Solve(F+1) Require INFEASIBLE نه: UNKNOWN → proof 37. Golden Test 1 Shared Block A ---- B ---- C \ \---- D دو OD: A → C A → D و: B03 capacity = 10 Expected: F1 + F2 <= 10 38. Golden Test 2 Shared Wagon Pool OD1 = 40 wagons OD2 = 30 wagons Pool = 50 Expected: OD1 + OD2 <= 50 39. Golden Test 3 Empty Flow Initial: A = 50 B = 0 Load: A = 40 Unload: B = 40 Return: B → A = 30 Expected: A = 40 B = 10 40. Golden Test 4 Empty Flow + Single Track A ===== B Loaded: A → B Empty: B → A هر دو باید یک Physical Block را share کنند. این تست مستقیماً اتصال: Wagon Flow ↕ Schedule ↕ Infrastructure را کنترل می‌کند. 41. Golden Test 5 Buffer Buffer(B) = 20 Unload = 30 EmptyOut = 15 موجودی نهایی: 30−15=15 پس: PASS 42. Golden Test 6 Final Inventory Initial = 50 Minimum Final = 30 Solution: Final = 25 باید: FAIL شود. 43. Golden Test 7 Unknown Solver Status اگر: F = FEASIBLE F+1 = UNKNOWN نتیجه: capacity_proven = false و نه: capacity_proven = true 44. E2E Test تست اصلی محصول: Access/Excel ↓ Data Version ↓ Canonical Data ↓ Scenario ↓ Demand ↓ Candidates ↓ Network Model ↓ CP-SAT ↓ Empty Wagon Flow ↓ Schedule ↓ Validation ↓ Capacity ↓ Proof ↓ Capacity Offer این تست باید نهایتاً با یک command اجرا شود: Bash pytest tests/e2e/test_real_network_capacity.py 45. Acceptance Test اصلی Python def test_network_capacity_end_to_end(): result = service.run( scenario_id="SC-001", data_version_id="DV-001", ) assert result.solver_status in { "OPTIMAL", "FEASIBLE", } assert result.validation_passed assert result.served_freight_tons >= 0 assert result.train_count >= 0 و برای Proof: Python if result.capacity_proven: assert result.proof.proof_valid 46. API نهایی Run http POST /api/v1/network/runs Request: JSON { "scenario_id": "SC-001", "data_version_id": "DV-001" } Response: JSON { "run_id": "RUN-0001", "status": "CREATED" } 47. Result http GET /api/v1/network/runs/RUN-0001 نمونه: JSON { "run_id": "RUN-0001", "status": "COMPLETED", "solver_status": "OPTIMAL", "served_freight_tons": 185000, "train_count": 42, "validation_passed": true, "capacity_proven": true } اعداد بالا صرفاً نمونه قراردادی API هستند، نه نتیجه اجرای داده واقعی. 48. API برای Empty Wagon http GET /api/v1/network/runs/RUN-0001/wagon-flow JSON { "flows": [ { "origin": "KHOWAF", "destination": "TEHRAN", "wagon_type": "WAGON_X", "quantity": 80, "departure_time": 1320, "arrival_time": 1760 } ] } 49. API برای Bottleneck http GET /api/v1/network/runs/RUN-0001/bottlenecks خروجی: JSON { "bottlenecks": [ { "resource_id": "B03", "resource_type": "SINGLE_TRACK", "binding": true, "slack": 0 }, { "resource_id": "WP-01", "resource_type": "WAGON_POOL", "binding": true, "slack": 0 } ] } 50. مهم‌ترین اصلاح معماری نسبت به نسخه‌های اولیه از این مرحله یک اصل را قطعی می‌کنیم: Capacity Engine │ ├── Infrastructure ├── Schedule ├── Formation ├── Wagon Cycle ├── Empty Wagon Network ├── Locomotive Cycle ├── Terminal ├── Demand └── Policy همه این‌ها باید در نهایت به یک Feasible Integrated Solution برسند. نه اینکه: Infrastructure Capacity = 30 Wagon Capacity = 25 Loco Capacity = 27 و بعد صرفاً: min(30,25,27)=25 بگیریم. این روش برای شبکه واقعی کافی نیست، چون couplingهای زمانی و resource interaction را از دست می‌دهد. 51. تعریف Production نهایی از این مرحله تعریف رسمی موتور: C N ​ =max{∑Q od,r,t ​ F od,r,t ​ ∣IntegratedNetworkSolution is feasible} ​ است. و Integrated Network Solution باید همزمان دارای: ✓ Valid Train Formation ✓ Valid Train Path ✓ Valid Schedule ✓ Valid Infrastructure Occupancy ✓ Valid Station/Junction Usage ✓ Valid Wagon Cycle ✓ Valid Empty Wagon Flow ✓ Valid Buffer State ✓ Valid Locomotive Cycle ✓ Valid Terminal Operations ✓ Valid Demand Allocation ✓ Valid Policy Constraints باشد. 52. جایگاه فعلی پروژه اکنون معماری ما به این شکل تثبیت می‌شود: V2.1-A Production Foundation ↓ V2.1-B Real Data Ingestion ↓ V2.1-C Operational Scheduler ↓ V2.1-D Formation + Wagon + Loco ↓ V2.1-E Multi-OD Network ↓ V2.1-F Time-Dependent Empty Wagon ↓ V2.2-A Production DB / Domain ↓ V2.2-B Executable Network Solver ↓ V2.3 Production Validation + Proof ↓ V2.4 Marketplace Integration ↓ V3 Planning Workbench نکته فنی مهم در V2.2-B عمداً دو سطح Solver خواهیم داشت: سطح 1 — Aggregate Network Optimization برای اینکه سریع مشخص شود: کدام ODها، چه تعداد train، با چه routeهایی، با چه مقدار freight قابل تخصیص هستند. سطح 2 — Detailed Time-Space Scheduling برای اینکه همان allocation واقعاً به timetable تبدیل شود و: Single Track Opposing Movement Headway Switch Time Station Track Junction Operational Window Train Length Block Occupancy را پاس کند. این تفکیک از نظر scalability بسیار مهم است؛ OR-Tools نیز network-flow و scheduling را به‌عنوان کلاس‌های متفاوت مسائل به رسمیت می‌شناسد و برای network flow، solverهای تخصصی نیز دارد. Google for Developers +1 در نتیجه، مرحله بعدی منطقی دیگر Specification نیست؛ ساخت یک V2.2 executable repository واقعی است که شامل pyproject.toml، domain contracts، SQLAlchemy models، Alembic migration، Network CP-SAT builder، Empty-Wagon Flow Builder، validator، golden tests و یک run_network_capacity.py قابل اجرا باشد. --- ## ادامه 51 — پرسش کاربر (`message-id:0e89ae98-9f95-49ba-b9c6-e0bf26b48984`) بله لطفا --- ## ادامه 51 — پاسخ دستیار (`message-id:0878511f-c394-47b5-9a01-4b2e0087436c`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 52 — پرسش کاربر (`message-id:8df20670-1a05-4dd6-a5b7-71883ba800e7`) بله بدون اون همینجا متنی ادامه بده --- ## ادامه 52 — پاسخ دستیار (`message-id:196b7672-17bd-4e9e-a483-9ff5a987dfa0`) حتماً. این‌بار ادامه را به‌صورت متن فنی قابل تبدیل مستقیم به Repository می‌دهم؛ و یک اصلاح مهم هم نسبت به طرح قبلی اعمال می‌کنم: در V2.2 نباید هنوز CapacityProof را معتبر اعلام کنیم مگر اینکه آزمون مرزی بعدی واقعاً با وضعیت INFEASIBLE تأیید شده باشد. همچنین مدل Persistence را از Domain جدا نگه می‌داریم؛ این با الگوی Declarative مدرن SQLAlchemy 2.x سازگار است. SQLAlchemy Documentation +1 V2.2 — Executable Production Foundation 1. هدف نسخه هدف این نسخه: Real / Canonical Data ↓ Demand ↓ Candidate Services ↓ Aggregate Multi-OD Optimization ↓ Time-Dependent Empty Wagon Balance ↓ Independent Validation ↓ Capacity Boundary Test ↓ Capacity Proof اما یک مرزبندی مهم: V2.2 هنوز Detailed Time-Space Scheduler را بازنویسی نمی‌کند. Network Engine باید Scheduler موجود V1.7 را consume کند، نه اینکه یک Scheduler دوم بسازد. معماری نهایی: ┌──────────────────────┐ │ Network Optimizer │ └──────────┬───────────┘ │ Train Allocation │ ▼ ┌──────────────────────┐ │ Detailed Scheduler │ │ V1.7 │ └──────────┬───────────┘ │ Feasible Schedule │ ▼ ┌──────────────────────┐ │ Independent │ │ Validation │ └──────────┬───────────┘ │ ▼ Capacity Proof 2. Project Structure ساختار اجرایی را این‌گونه تثبیت می‌کنیم: railway_capacity_workbench/ │ ├── pyproject.toml ├── README.md │ ├── app/ │ ├── domain/ │ │ ├── network/ │ │ │ ├── models.py │ │ │ ├── candidate.py │ │ │ └── result.py │ │ │ │ │ ├── wagon/ │ │ │ ├── models.py │ │ │ └── balance.py │ │ │ │ │ └── capacity/ │ │ └── proof.py │ │ │ ├── engines/ │ │ ├── network/ │ │ │ ├── candidate_generator.py │ │ │ ├── model_builder.py │ │ │ ├── solver.py │ │ │ └── capacity.py │ │ │ │ │ ├── wagon_flow/ │ │ │ ├── time_expanded.py │ │ │ └── validator.py │ │ │ │ │ └── scheduling/ │ │ └── interface.py │ │ │ ├── validation/ │ │ ├── network.py │ │ ├── wagon.py │ │ └── integrated.py │ │ │ ├── db/ │ │ ├── base.py │ │ ├── models/ │ │ └── repositories/ │ │ │ └── services/ │ └── network_capacity.py │ ├── alembic/ │ ├── scripts/ │ └── run_network_capacity.py │ └── tests/ ├── unit/ ├── golden/ └── e2e/ 3. Domain Layer ODPair Python from dataclasses import dataclass @dataclass(frozen=True) class ODPair: id: str origin_station_id: str destination_station_id: str commodity_id: str | None = None wagon_type_id: str | None = None NetworkRoute Python @dataclass(frozen=True) class NetworkRoute: id: str od_pair_id: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] distance_km: float nominal_running_time_min: int نکته مهم: station_ids باید sequence عملیاتی واقعی باشد. هرگز: Python sorted(station_ids) نباید انجام شود. 4. Train Service Candidate Python @dataclass(frozen=True) class TrainServiceCandidate: id: str od_pair_id: str route_id: str train_type_id: str wagon_type_id: str freight_capacity_t: int train_length_m: int train_weight_t: int required_wagons: int required_locomotives: int این object هنوز TrainRun واقعی نیست. بلکه: یک service pattern قابل انتخاب توسط optimizer است. پس: Candidate ≠ TrainRun 5. Demand Python @dataclass(frozen=True) class Demand: id: str od_pair_id: str time_bucket: int requested_tons: int minimum_service_tons: int = 0 و constraint: c ∑ ​ Q c ​ F c ​ ≤D od,t ​ 6. Candidate Generator Candidate Generator قبل از CP-SAT باید candidateهای ناممکن را حذف کند. مثلاً: Python class CandidateGenerator: def generate( self, od_pairs, routes, train_types, wagon_types, locomotives, ): candidates = [] for od in od_pairs: for route in routes: if route.od_pair_id != od.id: continue for train in train_types: if train.max_train_length_m < 1: continue for wagon in wagon_types: if wagon.id != od.wagon_type_id: continue candidates.append( self._build_candidate( od, route, train, wagon, ) ) return tuple(candidates) در Production این مرحله باید موارد زیر را prune کند: Commodity incompatibility Wagon incompatibility Station length Train length Weight limit Locomotive traction Route compatibility Brake capability Terminal compatibility Loading capability Unloading capability Operational availability این کار حجم مدل CP-SAT را به‌شدت کاهش می‌دهد. 7. Network Model مدل aggregate: F c ​ ∈Z ≥0 ​ برای هر Candidate. Python from ortools.sat.python import cp_model class NetworkModelBuilder: def __init__(self): self.model = cp_model.CpModel() self.train_vars = {} برای هر candidate: Python self.train_vars[candidate.id] = ( self.model.new_int_var( 0, max_trains, f"F_{candidate.id}", ) ) 8. Demand Constraint Python for demand in demands: terms = [] for candidate in candidates: if candidate.od_pair_id == demand.od_pair_id: terms.append( train_vars[candidate.id] * candidate.freight_capacity_t ) if terms: model.add( sum(terms) <= demand.requested_tons ) یعنی: c ∑ ​ Q c ​ F c ​ ≤D od,t ​ ​ 9. Shared Infrastructure فرض کنیم: B03 B04 B05 داریم. برای هر candidate مشخص می‌کنیم: Python candidate_blocks = { "C001": ("B03", "B04"), "C002": ("B03", "B05"), "C003": ("B04",), } و: Python block_capacity = { "B03": 20, "B04": 30, "B05": 40, } constraint: c ∑ ​ A c,b ​ F c ​ ≤C b ​ 10. Shared Wagon Pool اگر: W1 = 100 wagons و: C001 → 20 wagons/train C002 → 15 wagons/train داریم: 20F 1 ​ +15F 2 ​ ≤100 اما این هنوز Rolling Stock Cycle نیست. این distinction بسیار مهم است. 11. Rolling Stock Constraint Levels ما سه سطح خواهیم داشت: Level 1 — Pool Capacity Total wagon requirement <= available wagons Level 2 — Time-Dependent Inventory Inventory(station, wagon, time) Level 3 — Full Wagon Cycle Load ↓ Loaded Movement ↓ Unload ↓ Empty Return ↓ Maintenance/Waiting ↓ Next Load V2.2 سطح 1 و 2 را اجرایی می‌کند و interface سطح 3 را آماده می‌گذارد. 12. Time-Dependent Empty Wagon Network این بخش بسیار مهم است. State: (j,w,t) که: j: station w: wagon type t: time Variable: E j,w,t ​ یعنی تعداد واگن خالی موجود. 13. Empty Flow برای هر movement: X i,j,w,t ​ تعداد واگن‌هایی که از i به j حرکت می‌کنند. Constraint: X i,j,w,t ​ ≥0 14. Inventory Balance فرمول رسمی: E j,w,t+1 ​ =E j,w,t ​ +I j,w,t ​ +U j,w,t ​ −O j,w,t ​ −L j,w,t ​ ​ که: I: Empty In U: Unload O: Empty Out L: Load 15. Buffer Constraint 0≤E j,w,t ​ ≤B j,w ​ ​ این constraint باید در تمام time points برقرار باشد. نه فقط در ابتدا و انتها. 16. مثال فرض: Station B Initial Empty = 10 Unload = 30 Load = 5 Empty Out = 20 آنگاه: 10+30−5−20=15 پس: E(B,W,t+1)=15 17. Empty Flow و Loaded Train Coupling فرض: Train C001 20 wagons و: F_C001 = 4 پس: Load=80 در Origin: E_origin -= 80 و بعد از unloading: E_destination += 80 در نتیجه optimizer دیگر نمی‌تواند صرفاً train count را افزایش دهد بدون اینکه wagon state اجازه دهد. 18. Critical Architecture Loaded movement: Origin │ ▼ Loaded Train │ ▼ Destination │ ▼ Unload بعد: Empty Wagon │ ▼ Empty Flow │ ▼ Origin و دوباره: Load این همان چیزی است که در مدل قبلی با عنوان: Wagon Cycle تعریف کردیم. 19. Network Objective Objective اصلی: max od,r,t ∑ ​ Q od,r,t ​ F od,r,t ​ ​ اما معماری Objective باید قابل تغییر باشد. مثلاً: Python @dataclass(frozen=True) class ObjectiveDefinition: id: str priorities: tuple[str, ...] weights: dict[str, float] مثال: MAX_FREIGHT MAX_REVENUE MIN_UNSERVED MIN_COST LEXICOGRAPHIC 20. Solver Configuration Python @dataclass(frozen=True) class SolverConfiguration: time_limit_seconds: int = 300 random_seed: int = 1 num_workers: int = 1 absolute_gap: float | None = None relative_gap: float | None = None برای reproducibility: DataVersion + Scenario + ModelVersion + SolverConfiguration باید هویت اجرای optimization را تشکیل دهد. 21. Solver Status Mapping: Python OPTIMAL FEASIBLE INFEASIBLE UNKNOWN MODEL_INVALID و اصل غیرقابل مذاکره: UNKNOWN ≠ INFEASIBLE اگر solver به time limit برسد و proof infeasibility نداده باشد: Capacity Proven = FALSE 22. Independent Validator Solver نباید مرجع نهایی صحت باشد. Architecture: CP-SAT ↓ Raw Solution ↓ Independent Validator ↓ Validated Solution Validator باید بررسی کند: Demand Block Station Junction Wagon Empty Flow Buffer Locomotive Terminal Policy 23. Network Validation مثلاً: Python def validate_demand( allocations, candidates, demands, ): ... برای هر OD: Served od ​ ≤Demand od ​ 24. Wagon Validation برای هر state: Python expected = ( previous + empty_in + unload - empty_out - load ) و: Python assert actual == expected 25. Capacity Proof در V2.2 باید دو مفهوم را جدا کنیم: Optimization Result Best solution found Capacity Proof Proven boundary این دو یکی نیستند. 26. اگر نتیجه این باشد F = 30 Status = OPTIMAL Validation = PASS می‌گوییم: Validated Optimal Solution = 30 اما هنوز نمی‌گوییم: Capacity Proven = 30 تا boundary test انجام شود. 27. Boundary Test اگر ظرفیت train count باشد: Test F = 30 Test F+1 = 31 برای 31 باید مدل با constraint مناسب دوباره حل شود. اگر: 31 → INFEASIBLE آنگاه: Proof Valid = TRUE اگر: 31 → UNKNOWN آنگاه: Proof Valid = FALSE 28. Proof Object Python @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str f_validated: bool f_plus_one_proven_infeasible: bool proof_valid: bool objective_name: str proof_method: str 29. Proof Logic Python proof_valid = ( f_status in {"OPTIMAL", "FEASIBLE"} and f_validated and f_plus_one_status == "INFEASIBLE" ) ولی برای objective-based network capacity، روش proof باید متناسب با objective تعریف شود. مثلاً: Train Count Capacity Freight Capacity Revenue Capacity OD-specific Capacity هر کدام proof semantics متفاوت دارند. 30. Network Result Python @dataclass(frozen=True) class NetworkResult: run_id: str solver_status: str objective_value: float served_freight_tons: float train_count: int allocations: tuple empty_wagon_states: tuple validation_passed: bool capacity_proven: bool proof_id: str | None 31. Database Boundary Domain: app/domain/ نباید هیچ importی از: sqlalchemy fastapi ortools داشته باشد. این قانون را حفظ می‌کنیم. یعنی: Domain ↑ Application ↑ Infrastructure نه برعکس. 32. SQLAlchemy Model برای persistence: Python from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class StationModel(Base): __tablename__ = "station" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) name: Mapped[str] = mapped_column( String(200), nullable=False, ) این دقیقاً با الگوی DeclarativeBase + Mapped + mapped_column() در SQLAlchemy 2.x هم‌راستا است. SQLAlchemy Documentation +1 33. OD Database Model Python class ODPairModel(Base): __tablename__ = "od_pair" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) origin_station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) destination_station_id: Mapped[str] = mapped_column( ForeignKey("station.id"), index=True, ) commodity_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) wagon_type_id: Mapped[str | None] = mapped_column( String(64), nullable=True, ) 34. Network Run Python class NetworkRunModel(Base): __tablename__ = "network_run" id: Mapped[str] = mapped_column( String(64), primary_key=True, ) scenario_id: Mapped[str] = mapped_column( String(64), index=True, ) data_version_id: Mapped[str] = mapped_column( String(64), index=True, ) model_version: Mapped[str] = mapped_column( String(32), ) solver_status: Mapped[str] = mapped_column( String(32), ) objective_value: Mapped[float | None] solver_configuration_hash: Mapped[str | None] = mapped_column( String(64), ) input_snapshot_hash: Mapped[str | None] = mapped_column( String(64), ) 35. Repository Repository نباید business logic داشته باشد: Python class NetworkRunRepository: def __init__(self, session): self.session = session def add(self, entity): self.session.add(entity) def get(self, run_id): return self.session.get( NetworkRunModel, run_id, ) نه: Python repository.calculate_capacity() 36. Application Service این قسمت orchestrator است: Python class NetworkCapacityService: def run( self, scenario, data_version, ): canonical = self.load_canonical_data( data_version ) candidates = self.generate_candidates( canonical, scenario, ) aggregate_solution = ( self.solve_network( candidates, canonical, scenario, ) ) scheduled_solution = ( self.schedule( aggregate_solution, canonical, scenario, ) ) validation = ( self.validate( scheduled_solution, canonical, ) ) return self.build_result( scheduled_solution, validation, ) این Service قلب Application Layer است. 37. مهم‌ترین بخش: اتصال به Scheduler در اینجا: Python scheduled_solution = self.schedule(...) نباید یک scheduler جدید ساخته شود. بلکه: Network Candidate Allocation ↓ TrainRun Builder ↓ DirectedPath Builder ↓ V1.7 Scheduler ↓ Schedule 38. مثال Aggregate optimizer می‌گوید: OD1/R1 = 4 trains OD2/R2 = 2 trains Network Engine فقط این را تولید کرده است. سپس: TrainRun: OD1/R1/01 OD1/R1/02 OD1/R1/03 OD1/R1/04 OD2/R2/01 OD2/R2/02 و بعد Scheduler بررسی می‌کند: B03 Single Track آیا این شش train واقعاً قابل schedule هستند؟ اگر نه: Aggregate Solution ≠ Operationally Feasible Solution و باید allocation اصلاح شود. این دقیقاً همان جایی است که V2.2 را از یک optimization demo به یک Railway Capacity Engine تبدیل می‌کند. 39. Iterative Architecture برای این coupling، معماری Production بهتر است: Network Optimization ↓ Candidate Allocation ↓ Detailed Scheduler ↓ Feasibility │ ├── PASS → Final │ └── FAIL ↓ Conflict Feedback ↓ Network Model ↓ Re-solve Feedback می‌تواند شامل: B03 capacity unavailable Station T1 track unavailable Junction J2 conflict Wagon shortage at station X Locomotive cycle violation باشد. 40. اینجا یک نکته بسیار مهم درباره bottleneck داریم اگر Network Solver allocation زیر را بدهد: OD1 = 5 OD2 = 4 و scheduler بگوید: B03 conflict نباید فقط بگوییم: B03 bottleneck باید بدانیم: B03 ↓ OD1 train 3 OD2 train 2 ↓ Opposing direction conflict ↓ Required separation = 15 min ↓ Available window insufficient این Evidence وارد Explanation Engine می‌شود. 41. Bottleneck Object Python @dataclass(frozen=True) class BottleneckEvidence: resource_id: str resource_type: str affected_candidates: tuple[str, ...] constraint_type: str utilization: float slack: float marginal_capacity_delta: int | None evidence_ids: tuple[str, ...] 42. Scenario Engine وقتی bottleneck پیدا شد: Base ↓ B03 Single Scenario: B03 Double سپس: Scenario Clone ↓ Full Re-Solve ↓ Compare نه اینکه صرفاً: capacity += estimated_delta 43. Scenario Comparison Metric Base Scenario Delta -------------------------------------------------------- Network Freight ... ... ... Train Count ... ... ... OD1 Served ... ... ... OD2 Served ... ... ... Empty Wagon km ... ... ... Loco Utilization ... ... ... B03 Utilization ... ... ... Unserved Demand ... ... ... تمام اعداد باید از دو Run واقعی بیایند. 44. Golden Test — Shared Resource سناریو: OD1 → B03 OD2 → B03 و: B03 Capacity = 5 trains Demand: OD1 = 10,000 ton OD2 = 10,000 ton اگر هر train: 1,000 ton آنگاه: F 1 ​ +F 2 ​ ≤5 و: C N ​ =5,000 در سطح aggregate. 45. Golden Test — Wagon Pool W1 = 100 wagons OD1: 20 wagon/train OD2: 20 wagon/train پس: 20F 1 ​ +20F 2 ​ ≤100 بنابراین حداکثر: F 1 ​ +F 2 ​ ≤5 46. Golden Test — Empty Return Initial: A = 50 B = 0 Loaded: A → B = 40 پس: A = 10 B = 40 اگر: B → A = 30 آنگاه: A = 40 B = 10 و این باید مستقل validate شود. 47. Golden Test — Buffer اگر: B buffer = 20 و: B unload = 30 B empty-out = 15 پس: E B ​ =15 PASS. ولی: unload = 40 empty-out = 10 می‌دهد: E B ​ =30 و باید: BUFFER_EXCEEDED برگرداند. 48. Golden Test — UNKNOWN اگر: F = FEASIBLE F+1 = UNKNOWN خروجی: JSON { "capacity": 30, "capacity_proven": false, "proof_status": "NOT_PROVEN" } نه: JSON { "capacity_proven": true } 49. API Contract ایجاد Run http POST /api/v1/network/runs JSON { "scenario_id": "SC-001", "data_version_id": "DV-001", "solver_configuration": { "time_limit_seconds": 300, "random_seed": 1, "num_workers": 1 } } 50. Run Result http GET /api/v1/network/runs/{run_id} JSON { "run_id": "RUN-001", "status": "COMPLETED", "solver_status": "OPTIMAL", "validation_passed": true, "capacity_proven": false } 51. Capacity http GET /api/v1/network/runs/{run_id}/capacity خروجی: JSON { "network_capacity_tons": 185000, "capacity_proven": true, "proof_id": "PROOF-001" } فقط وقتی true است که Evidence واقعاً وجود داشته باشد. 52. Empty Wagon API http GET /api/v1/network/runs/{run_id}/wagon-flow باید بتواند: Station Wagon Type Time Inventory Empty In Unload Empty Out Load را برگرداند. 53. Traceability در نهایت باید بتوانیم از هر Capacity Offer به عقب برگردیم: CapacityOffer ↓ Allocation ↓ NetworkRun ↓ TrainService ↓ Formation ↓ Wagon Requirement ↓ FreightFlow ↓ Demand ↓ MarketRequest و از پایین به بالا: Block ↓ Schedule ↓ Train ↓ OD ↓ Demand ↓ Market 54. Capacity Offer Python @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str time_window: str train_capacity: int freight_capacity_t: float confidence: str proof_id: str | None و نکته مهم: CapacityOffer ≠ Route Capacity Capacity Offer باید profiled capacity باشد. 55. Data Lineage هر نتیجه باید بداند از چه داده‌ای آمده: DataVersion = DV-2026-09-001 Scenario = SC-BASE-004 Model = 2.2.0 Solver = configuration hash Run = RUN-... و اگر Access/Excel تغییر کرد: New Source Hash ↓ New DataVersion ↓ New Run 56. ارتباط با داده واقعی Access در V2.2 هیچ‌کدام از semantics تأییدشده قبلی تغییر نمی‌کنند: TrainNo → Train Run identity component StationName → Station identity evidence Sequence → Ordered Station Call time_in → Arrival time_take → Actual dwell time_out → Source departure evidence seir → Baseline running time to next station Kilometerage → Chainage derived_distance → abs(next Kilometerage - current Kilometerage) و: Distance sumDistancezz تا زمان تأیید semantic همچنان source evidence می‌مانند. همچنین: RequiredWait = PROVISIONAL MaxSpeed = PROVISIONAL باقی می‌مانند. 57. Midnight برای نمونه: 23:46 + 50 min = 00:36 next day نباید scheduler با datetime.time خام کار کند. Canonical representation: absolute_minute + day_offset مثلاً: Day 0, 23:46 = 1426 و: Day 1, 00:36 = 1476 بنابراین: 1476−1426=50 58. Production Acceptance Chain این تست نهایی V2.2 است: Access ↓ Raw ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Canonical ↓ Scenario ↓ Demand ↓ Candidate Generation ↓ Network Optimization ↓ Empty Wagon Flow ↓ Train Allocation ↓ Detailed Scheduler ↓ Independent Validation ↓ Capacity Search ↓ Capacity Proof ↓ Bottleneck Evidence ↓ Explanation ↓ Capacity Offer اگر هر مرحله fail شود: NO PRODUCTION CAPACITY OFFER 59. Definition of Done — V2.2 V2.2 زمانی Complete محسوب می‌شود که: Domain OD Route Candidate Demand Wagon State Empty Flow Network Result Optimization Multi-OD Multi-Route Demand Constraint Shared Block Wagon Pool Locomotive Pool Objective deterministic configuration Wagon Initial Inventory Unload Load Empty In Empty Out Buffer Time-dependent state Validation Demand Freight Allocation Wagon Balance Buffer Solver Status Proof F result F+1 framework UNKNOWN ≠ INFEASIBLE Independent validation Evidence structure Persistence SQLAlchemy Base Station OD Network Run Complete canonical migration PostgreSQL integration test Scheduler Integration V1.7 scheduler adapter Network → TrainRun conversion Network → detailed timetable schedule feedback into network optimization این چهار مورد آخر عمداً هنوز Done نشده‌اند؛ چون اگر الان آنها را صوری پیاده کنیم، معماری دوباره به دو Scheduler مستقل تبدیل می‌شود. 60. مرحله بعدی: V2.3 پس از این نسخه، گام واقعی بعدی باید این باشد: V2.3 — Network-to-Time-Space Integrated Solver و در آن این زنجیره را واقعاً executable می‌کنیم: Network Allocation ↓ TrainRun Generation ↓ Directed Path ↓ Station Calls ↓ Block Movements ↓ Single/Double Track ↓ Headway ↓ Opposing Direction ↓ Switch Time ↓ Station Track ↓ Junction ↓ Operational Window ↓ Detailed Schedule ↓ Independent Validation ↓ Feedback to Network و اینجا برای اولین بار می‌توانیم یک سؤال بسیار مهم را واقعاً پاسخ دهیم: «اگر Marketplace مثلاً ۸۰٬۰۰۰ تن تقاضای چند OD بدهد، دقیقاً چند تن آن با تشکیل واقعی قطار، واگن، لوکوموتیو، مسیر، زمان‌بندی، عبور از single-track، ایستگاه‌ها و چرخه بازگشت واگن قابل حمل است؟» این همان نقطه‌ای است که موتور از Aggregate Optimization Engine به Operational Railway Capacity Engine تبدیل می‌شود. برای لایه ORM نیز مبنای فعلی صحیح است: SQLAlchemy 2.x استفاده از DeclarativeBase و Mapped/mapped_column() را به‌عنوان الگوی مدرن Declarative Mapping مستند کرده است. SQLAlchemy Documentation +1 --- ## ادامه 53 — پرسش کاربر (`message-id:5ee4d647-1b2d-4319-b225-33b74834dc3e`) بله لطفا --- ## ادامه 53 — پاسخ دستیار (`message-id:c382384f-521f-468e-8125-8e64ac799cf2`) حتماً. حالا وارد V2.3 — Network-to-Time-Space Integrated Solver می‌شویم؛ یعنی جایی که allocation شبکه دیگر صرفاً یک عدد نیست و به قطار، مسیر، حرکت روی بلوک، ایستگاه، conflict و timetable واقعی تبدیل می‌شود. در این طراحی، همچنان از مدل‌سازی زمان‌مند و constraint-based استفاده می‌کنیم؛ CP-SAT برای چنین مدل‌هایی مناسب است و در اکوسیستم OR-Tools ابزارهای scheduling/constraint optimization در دسترس‌اند. Google for Developers V2.3 — Network-to-Time-Space Integrated Solver 1. هدف اصلی در V2.2 داشتیم: Network Optimization→Train Allocation در V2.3 باید داشته باشیم: Network Allocation→TrainRun→DirectedPath→TimeSpace Schedule ​ و در نهایت: Capacity=max{F:Integrated Solution is Feasible} ​ 2. معماری V2.3 MARKETPLACE │ ▼ DEMAND │ ▼ ┌───────────────────┐ │ Candidate │ │ Generation │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ Network Optimizer │ │ Aggregate │ └─────────┬─────────┘ │ Train Allocation │ ▼ ┌───────────────────┐ │ TrainRun Builder │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ Directed Path │ └─────────┬─────────┘ │ ▼ ┌─────────────────────────┐ │ Detailed Time-Space │ │ Scheduler V1.7 │ └────────────┬────────────┘ │ ▼ ┌────────────────────────────────┐ │ Independent Validation │ └───────────────┬────────────────┘ │ ┌─────────┴──────────┐ │ │ PASS FAIL │ │ ▼ ▼ RESULT Conflict Feedback │ ▼ Network Re-Optimization این حلقه بسیار مهم است. 3. چرا یک بار Optimization کافی نیست؟ فرض کنید Network Optimizer بگوید: OD-A/B = 8 trains OD-C/D = 6 trains از دید aggregate همه چیز ممکن است. اما وقتی آنها را به timetable واقعی تبدیل کنیم، ممکن است: B03 = SINGLE TRACK Train 1 → Forward Train 2 → Reverse Train 3 → Forward ... و schedule به conflict بخورد. پس: Aggregate Feasibility  =Operational Feasibility 4. Domain جدید: TrainRun Python from dataclasses import dataclass @dataclass(frozen=True) class TrainRun: id: str service_candidate_id: str od_pair_id: str route_id: str sequence_number: int direction: str load_state: str formation_id: str | None = None wagon_cycle_id: str | None = None locomotive_cycle_id: str | None = None نکته مهم: TrainServiceCandidate ≠ TrainRun مثلاً: Candidate C001 F = 5 تبدیل می‌شود به: C001-01 C001-02 C001-03 C001-04 C001-05 5. Directed Path Route فیزیکی: A ─ B ─ C ─ D ولی TrainRun: D → C → B → A بنابراین: Python @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] نباید از ترتیب اسمی استفاده شود. ترتیب باید از: Sequence + Topology + Direction بیاید. 6. Physical Block همان مدل V1.7 حفظ می‌شود: Python @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int switch_time: int clearing_time: int و: PhysicalBlock(A,B)=PhysicalBlock(B,A) اما: Directed Movement جهت دارد. 7. Movement Resource Single Track هر دو جهت: B03 را share می‌کنند. Double Track مثلاً: B03:FORWARD B03:REVERSE منابع جدا هستند. Python def movement_resource(block, direction): if block.track_type == TrackType.SINGLE: return block.id return f"{block.id}:{direction}" این distinction برای مدل شبکه حیاتی است. 8. Time-Space Variables برای هر TrainRun و Station: A i,s ​ زمان ورود. D i,s ​ زمان خروج. برای هر Block: E i,b ​ ورود به block. X i,b ​ خروج. و: C i,b ​ clear شدن block. 9. Precedence اگر Train از station A وارد block شود: D i,A ​ ≤E i,b ​ و: X i,b ​ =E i,b ​ +T i,b ​ سپس: X i,b ​ ≤A i,B ​ 10. Running Time برای direction: Python running_time = block.running_time(direction) ولی در نسخه کامل: T run ​ =f(L,V profile ​ ,TrainType,LoadState,Gradient,Restrictions) و برای داده فعلی Access: T baseline ​ =seir به‌عنوان evidence پایه استفاده می‌شود. 11. Dwell برای station: D i,s ​ ≥A i,s ​ +T dwell,i,s ​ اگر RequiredWait معتبر باشد: T dwell ​ ≥RequiredWait ولی چون در داده فعلی RequiredWait هنوز PROVISIONAL است، نباید بدون validation آن را به‌عنوان قانون قطعی production اعمال کنیم. 12. Train Length برای Station Track: L train ​ ≤L track ​ اگر هیچ track سازگار وجود نداشته باشد: STATION_LENGTH_INFEASIBLE 13. Single Track Conflict دو movement: Train A A → B Train B B → A روی یک physical block. باید یکی قبل از دیگری باشد: X A ​ +H AB ​ ≤E B ​ یا: X B ​ +H BA ​ ≤E A ​ که: H AB ​ =max(H same ​ ,T switch ​ ) برای opposing direction. 14. Same Direction اگر: Train A → B Train B → B آنگاه: E B ​ ≥X A ​ +H same ​ در حالت ساده. در مدل دقیق‌تر: Train type Load state Signal regime Block length Braking می‌توانند headway را تغییر دهند. 15. Clearing Time اگر قطار وارد block شود: entry ↓ movement ↓ exit ↓ clearance پس: T occupancy ​ =T run ​ +T clear ​ و: C i,b ​ =X i,b ​ +T clear ​ این clear باید مبنای آزادشدن resource باشد. 16. Station Track Assignment برای هر TrainRun: Station S ├── Track 1 ├── Track 2 └── Track 3 متغیر: z i,s,k ​ ∈{0,1} که: k ∑ ​ z i,s,k ​ =1 برای stationهایی که assignment اجباری دارند. و: z i,s,k ​ =1⇒L i ​ ≤L k ​ 17. Station Conflict دو train نمی‌توانند در یک track overlapping باشند: [Arrival i ​ ,Departure i ​ ] و: [Arrival j ​ ,Departure j ​ ] باید جدا شوند. در CP-SAT می‌توان این نوع resource را با interval/resource constraints مدل کرد؛ OR-Tools برای مسائل scheduling و interval-based constraints امکانات تخصصی دارد. 18. Junction Junction را نباید مثل یک block ساده مدل کنیم. مثلاً: B │ A ─────── J ───── C │ D حرکت: A → C ممکن است با: B → D conflict داشته باشد. اما: A → C و: C → A الزاماً همان conflict matrix را ندارند. پس: Python @dataclass(frozen=True) class JunctionConflict: movement_a: str movement_b: str separation_time: int حفظ می‌شود. 19. Operational Windows مثلاً: B03 maintenance 01:00–03:00 اگر train movement: 00:30–01:20 باشد: FAIL ولی: 00:30–00:55 می‌تواند pass کند. Constraint: Exit≤WindowStart یا: Entry≥WindowEnd 20. Earliest Departure برای هر TrainRun: D origin ​ ≥E i ​ که E i ​ می‌تواند ناشی از: Market release Formation completion Loading Brake test Wagon availability Locomotive availability Operational window باشد. 21. Latest Arrival اگر تعریف شده باشد: A destination ​ ≤L i ​ این constraint برای: Customer delivery window Terminal slot Marketplace commitment بسیار مهم است. 22. Formation Coupling TrainRun باید Formation داشته باشد: TrainRun ↓ Formation ├── Wagon 1 ├── Wagon 2 ├── ... └── Locomotive و Formation باید: length weight commodity wagon compatibility traction brake route compatibility را پاس کند. 23. Wagon Cycle Coupling فرض: TrainRun T01 A → B بعد: Unload at B و: Empty return B → A پس TrainRun تنها یک movement نیست؛ بخشی از: WagonCycle است. 24. Locomotive Cycle همین منطق برای locomotive: Loco ↓ T01 A→B ↓ Turnback ↓ T02 B→A ↓ Fuel/Maintenance ↓ Next assignment بنابراین locomotive availability نیز timestamp-based است. 25. Integrated Scheduler Input Python @dataclass(frozen=True) class IntegratedSchedulingProblem: train_runs: tuple[TrainRun, ...] paths: tuple[DirectedPath, ...] blocks: tuple[PhysicalBlock, ...] stations: tuple[Station, ...] junctions: tuple[Junction, ...] operational_windows: tuple[OperationalWindow, ...] formations: tuple[TrainFormation, ...] wagon_states: tuple[WagonInventoryState, ...] locomotive_states: tuple[LocomotiveState, ...] 26. Scheduler Output Python @dataclass(frozen=True) class TrainSchedule: train_run_id: str station_calls: tuple[ScheduledStationCall, ...] block_movements: tuple[BlockMovement, ...] status: str 27. Complete Network Solution حالا نتیجه نهایی دیگر فقط allocation نیست: Python @dataclass(frozen=True) class IntegratedNetworkSolution: train_allocations: tuple train_schedules: tuple formations: tuple wagon_movements: tuple locomotive_assignments: tuple conflicts: tuple resource_usage: tuple objective_value: float solver_status: str validation_status: str 28. Network → TrainRun Builder مثلاً: Candidate C001 Train Count = 4 Builder: Python def build_train_runs(candidate, count): return tuple( TrainRun( id=f"{candidate.id}-{i:03d}", service_candidate_id=candidate.id, od_pair_id=candidate.od_pair_id, route_id=candidate.route_id, sequence_number=i, direction="FORWARD", load_state="LOADED", ) for i in range(1, count + 1) ) در Production باید sequence و direction از service definition بیاید. 29. Allocation Ordering یک مسئله مهم: اگر چهار train تولید کردیم، نباید scheduler آنها را صرفاً بر اساس ID مرتب کند. Ordering باید بر اساس: Planning Horizon Earliest Departure Service Priority OD Direction Operational Rules باشد. 30. Schedule Feedback اگر scheduler شکست دهد: C001-02 C003-01 به علت: B03 opposing conflict باید feedback تولید شود: Python @dataclass(frozen=True) class SchedulingConflictFeedback: conflict_id: str resource_id: str conflict_type: str affected_train_runs: tuple[str, ...] affected_candidates: tuple[str, ...] required_separation: int available_separation: int severity: str 31. Feedback به Network Optimizer مثلاً: B03 C001 + C003 ممکن است constraint جدید بسازد: F C001 ​ +F C003 ​ ≤K یا در مدل دقیق‌تر، candidate pairهای زمانی را محدود کند. این همان حلقه: Network↔Schedule است. 32. دو روش حل روش A — Iterative Network Solve ↓ Schedule ↓ Conflict ↓ Add Constraint ↓ Network Re-Solve برای MVP مناسب‌تر است. روش B — Integrated Monolithic تمام variables شبکه و زمان را در یک CP-SAT Model قرار می‌دهیم. F_c + A_i,s + D_i,s + E_i,b + X_i,b + Track Assignment + Junction Assignment + Wagon Flow + Loco Flow این از نظر تئوری یکپارچه‌تر است ولی مدل می‌تواند بسیار بزرگ شود. برای سیستم شما، Hybrid/Iterative architecture منطقی‌تر است. 33. چرا Hybrid؟ چون مسئله شما چند مقیاس دارد: Network ↓ OD / Freight ↓ Train Count ↓ Rolling Stock ↓ Time ↓ Block ↓ Minute-level conflict اگر همه را یکباره وارد CP-SAT کنیم، مدل می‌تواند بسیار بزرگ شود. بنابراین: Level 1 Aggregate Optimization Level 2 Rolling Stock Feasibility Level 3 Detailed Scheduling Level 4 Independent Validation و سپس iteration. 34. Integrated Capacity Algorithm الگوریتم اصلی: INPUT DataVersion Scenario Demand Infrastructure Rolling Stock Operational Rules │ ▼ Candidate Generation │ ▼ Candidate Pruning │ ▼ Aggregate Network Optimization │ ▼ Train Allocation │ ▼ Formation Builder │ ▼ Wagon/Loco Feasibility │ ▼ TrainRun Generation │ ▼ Directed Path │ ▼ Detailed Scheduler │ ▼ Independent Validator │ ├── FAIL ──► Conflict Feedback ──► Re-Optimize │ └── PASS │ ▼ Capacity Candidate │ ▼ Boundary Test │ ▼ Capacity Proof │ ▼ Bottleneck Analysis │ ▼ Capacity Offer 35. Capacity Search در V2.3 نباید فقط: Python capacity = max_train_count داشته باشیم. بلکه: Python def evaluate_capacity(F): allocation = solve_network(F) if allocation.status not in {"OPTIMAL", "FEASIBLE"}: return INFEASIBLE schedule = schedule_allocation( allocation ) if not schedule.feasible: return INFEASIBLE validation = validate( schedule ) if not validation.passed: return INVALID return FEASIBLE 36. Critical distinction سه وضعیت داریم: FEASIBLE INFEASIBLE INVALID و همچنین: UNKNOWN پس: INVALID ≠ INFEASIBLE UNKNOWN ≠ INFEASIBLE 37. Capacity Search Result Python @dataclass(frozen=True) class CapacityEvaluation: requested_capacity: int network_status: str schedule_status: str validation_status: str feasible: bool evidence_ids: tuple[str, ...] 38. F/F+1 Proof مثلاً: F = 30 Test 1 30 Network = FEASIBLE Scheduler = FEASIBLE Validation = PASS Test 2 31 Network = FEASIBLE Scheduler = INFEASIBLE پس: Capacity = 30 Proof = VALID ولی اگر: 31 Scheduler = UNKNOWN آنگاه: Capacity = 30 Proof = NOT PROVEN 39. Bottleneck Attribution بعد از capacity proof باید بپرسیم: چه چیزی باعث شد F+1 infeasible شود؟ Evidence مثلاً: B03 Single Track Opposing Direction Conflict Required Separation = 15 min Available Window = 12 min یا: Wagon Pool W1 Required = 102 Available = 100 یا: Station S07 Required Train Length = 720m Maximum Track = 680m 40. Binding Constraint Python @dataclass(frozen=True) class BindingConstraint: id: str type: str resource_id: str train_runs: tuple[str, ...] slack: float status: str evidence_id: str 41. Marginal Capacity اگر scenario: B03 Single را به: B03 Double تبدیل کنیم: ΔC=C double ​ −C single ​ این delta باید با دو Run واقعی محاسبه شود. 42. Interaction Effects مثلاً: B03 capacity +10 به تنهایی: +4 trains اما: B03 + wagon pool ممکن است: +7 trains بدهد. بنابراین: ΔC(A+B)  =ΔC(A)+ΔC(B) در همه موارد. این interactionها باید در Scenario Engine قابل مشاهده باشند. 43. Time-Space Diagram خروجی V2.3 باید قابل نمایش به شکل: Time → ──────────────────────────────────────────── Train 001 / / / / Train 002 \ \ \ Train 003 / / / محور عمودی: Station / Block محور افقی: Absolute Time این visualization برای conflict analysis بسیار مهم است. 44. Conflict Inspector برای هر conflict: Conflict ID: CF-0042 Type: Opposing Direction Resource: B03 Train A: T001 Train B: T002 Train A: Entry = 10:20 Exit = 10:45 Clear = 10:47 Train B: Entry = 10:38 Required Separation: 15 min Actual: -9 min Status: BINDING این دقیقاً همان اطلاعاتی است که analyst نیاز دارد. 45. Network Capacity Inspector OD Route Trains Freight ------------------------------------------------ Tehran → Khowaf R01 12 24,000 Tehran → Rasht R02 8 12,800 Khowaf → Zarand R03 7 14,000 و در کنار آن: Infrastructure Capacity Operational Capacity Rolling Stock Capacity Transportable Capacity Allocated Capacity 46. Capacity Profiles به جای یک عدد: C داریم: C=(C infra ​ ,C operational ​ ,C rolling ​ ,C transportable ​ ,C allocated ​ ) ​ مثلاً: Infrastructure Capacity = ... Operational Capacity = ... Rolling Stock Capacity = ... Transportable Capacity = ... Allocated Capacity = ... اعداد واقعی فقط از Run محاسبه می‌شوند. 47. Marketplace Integration پس از validation: Network Result ↓ Capacity Offer ↓ Marketplace مثلاً: JSON { "od_pair": "TEHRAN-KHOWAF", "route": "R01", "time_window": "06:00-22:00", "train_capacity": 12, "freight_capacity_tons": 24000, "confidence": "PROVEN", "proof_id": "PROOF-0042" } Marketplace فقط این Feasible Capacity Offer را می‌بیند. 48. نکته بسیار مهم برای Marketplace Marketplace نباید بگوید: "Route R01 has 30 trains capacity." بلکه: "OD X → Y Train Type T Wagon Type W Time Window TW 12 feasible train slots 24,000 ton freight capacity Proof = VALID" چون: Capacity=f(OD,Route,TrainType,WagonType,Direction,Time,Station,Infrastructure,RollingStock) 49. V2.3 Database additions جداول اصلی: train_run train_station_call train_block_movement schedule schedule_station_call schedule_block_movement station_track_assignment junction_movement junction_conflict resource_usage resource_conflict binding_constraint capacity_evaluation capacity_proof proof_evidence wagon_state empty_wagon_movement locomotive_assignment locomotive_cycle network_iteration network_feedback 50. Network Iteration برای traceability: Python @dataclass(frozen=True) class NetworkIteration: id: str run_id: str iteration_number: int network_objective: float scheduler_status: str validation_status: str conflict_count: int feedback_ids: tuple[str, ...] مثلاً: Iteration 1 Network = feasible Schedule = infeasible Conflicts = 7 Iteration 2 Network = feasible Schedule = infeasible Conflicts = 2 Iteration 3 Network = feasible Schedule = feasible Validation = PASS این برای explainability فوق‌العاده ارزشمند است. 51. Stop Conditions Iteration نباید بی‌نهایت ادامه پیدا کند. مثلاً: Python MAX_ITERATIONS = 20 و stop: PASS OR INFEASIBLE OR TIMEOUT OR NO_PROGRESS OR MAX_ITERATIONS NO_PROGRESS یعنی feedback جدیدی ایجاد نمی‌شود. 52. Production Run Status CREATED ↓ VALIDATING_INPUT ↓ GENERATING_CANDIDATES ↓ NETWORK_OPTIMIZING ↓ FORMATION_CHECK ↓ ROLLING_STOCK_CHECK ↓ SCHEDULING ↓ VALIDATING_RESULT ↓ FEEDBACK / REOPTIMIZING ↓ PROOF ↓ COMPLETED Failure: FAILED_VALIDATION FAILED_SOLVER INVALID_RESULT TIMEOUT CANCELLED 53. Definition of Done — V2.3 V2.3 را زمانی Complete می‌دانیم که یک scenario بتواند واقعاً این مسیر را طی کند: Demand ↓ Multi-OD Network Optimization ↓ Train Count ↓ Train Formation ↓ Wagon Availability ↓ Locomotive Availability ↓ TrainRun ↓ DirectedPath ↓ Detailed Timetable ↓ Single Track Conflict ↓ Double Track Resource ↓ Station Track ↓ Junction ↓ Operational Window ↓ Independent Validation ↓ F/F+1 Capacity Test ↓ Proof ↓ Bottleneck ↓ Capacity Offer و مهم‌تر از همه: اگر موتور عدد 30 trains/day را اعلام کند، باید بتواند timetable معتبر برای همان 30 قطار تولید کند. این اصل از ابتدای پروژه حفظ شده و در V2.3 تبدیل به Acceptance Criterion اجرایی می‌شود. V2.4 بعد از این مرحله پس از V2.3، مرحله طبیعی بعدی دیگر توسعه هسته ریاضی نیست؛ بلکه تبدیل آن به Production Railway Capacity Platform است: V2.3 Integrated Solver ↓ V2.4 Real Access/Excel Data Execution ↓ V2.5 PostgreSQL + FastAPI ↓ V2.6 Scenario & Sensitivity Workbench ↓ V2.7 Marketplace API ↓ V3.0 Full Railway Capacity Planning Workbench در V2.4، مهم‌ترین کار این است که همین زنجیره را روی داده واقعی Access/Excel شما ببندیم؛ مخصوصاً TrainNo / Sequence / time_in / time_take / time_out / seir / Kilometerage و midnight rollover، و سپس اولین Golden Real-Data Capacity Run را بسازیم. این مرحله همان نقطه‌ای است که مدل از fixture و synthetic case به Vertical Slice واقعی راه‌آهن ایران عبور می‌کند. --- ## ادامه 54 — پرسش کاربر (`message-id:dadd0df8-791c-42bc-9e35-c527056b4275`) بله لطفا --- ## ادامه 54 — پاسخ دستیار (`message-id:a9718161-58dd-4b25-8dc9-81ae3eec9d29`) حتماً. پس ادامه منطقی پروژه را وارد V2.4 — Real Data Execution & Golden Real-Data Capacity Run می‌کنیم. این مرحله از نظر من یکی از مهم‌ترین نقاط پروژه است، چون برای اولین‌بار فاصله بین مدل طراحی‌شده و داده عملیاتی واقعی راه‌آهن را به‌صورت اجرایی می‌بندد. یک نکته مهم هم در این مرحله تثبیت می‌کنیم: لایه Persistence با SQLAlchemy 2.x بر مبنای DeclarativeBase، Mapped و mapped_column() پیاده‌سازی می‌شود که الگوی مدرن مستندشده SQLAlchemy است. SQLAlchemy Documentation +1 V2.4 — Real Data Execution & Golden Real-Data Capacity Run V2.4 — Real Data Execution & Golden Real-Data Capacity Run 1. هدف نسخه V2.4 هدف V2.4 این است که سیستم از وضعیت: Production Architecture + Executable Synthetic/Controlled Core به وضعیت: Executable Real-Data Vertical Slice منتقل شود. در این نسخه، داده واقعی Access و Excel مستقیماً وارد زنجیره اجرایی می‌شود: Access / Excel ↓ Raw Source Records ↓ Staging ↓ Mapping Registry ↓ Data Quality Gate ↓ Identity Reconciliation ↓ Canonical Railway Model ↓ TrainRun ↓ DirectedPath ↓ Baseline Schedule ↓ Infrastructure Resource Model ↓ Detailed Time-Space Scheduler ↓ Independent Validation ↓ Capacity Search ↓ F / F+1 Proof ↓ Bottleneck & Explanation ↓ Capacity Offer این نسخه نباید با تولید یک عدد ظرفیت شروع شود. ابتدا باید ثابت شود که: داده واقعاً قابل خواندن است. Mapping صحیح است. هویت TrainRunها درست تشخیص داده می‌شود. زمان‌ها، مخصوصاً عبور از نیمه‌شب، صحیح Normalize می‌شوند. مسیر Directed صحیح ساخته می‌شود. Baseline Schedule با داده منبع سازگار است. زیرساخت به Physical Resource تبدیل شده است. Schedule تولیدشده مستقل Validation می‌شود. سپس Capacity محاسبه می‌شود. و در نهایت Capacity Proof تولید می‌شود. 2. اصل اساسی V2.4 در V2.4 هیچ داده‌ای صرفاً به دلیل وجود داشتن در Access یا Excel، معتبر فرض نمی‌شود. قاعده: Source Exists ≠ Semantic Verified و: Mapped ≠ Production Usable و: Solver Feasible ≠ Operationally Valid و: F+1 Solver Failure ≠ Capacity Proof تنها زمانی Capacity نهایی قابل انتشار است که زنجیره زیر کامل باشد: Source → Mapping → Quality → Canonical → Schedule → Independent Validation → F Feasible → F+1 Explicitly Infeasible → Proof Valid 3. داده واقعی مبنا V2.4 بر اساس ساختار واقعی داده‌ای که تاکنون بررسی شده طراحی می‌شود. 3.1 Access فیلدهای مشاهده‌شده: ID kol TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir 3.2 Excel فیلدهای مشاهده‌شده: ردیف نام قطار شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت از مبدا ساعت ورود به مقصد شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت از مقصد ساعت رسیدن به مبدا Excel و Access نباید مستقیماً با هم Merge شوند. معماری: Access ──→ Access Staging ──→ Canonical ↑ Excel ──→ Excel Staging ─────────┘ و Reconciliation یک سرویس مستقل خواهد بود. 4. Mapping Registry واقعی برای هر فیلد باید چهار مفهوم ذخیره شود: Source Field Canonical Field Transformation Confidence مثلاً: Access Field Canonical Field Transformation Confidence TrainNo TrainRun.source_train_no مستقیم VERIFIED TrainName TrainRun.service_name مستقیم VERIFIED StationName Station.source_name Identity Mapping VERIFIED Sequence TrainStationCall.sequence integer VERIFIED time_in TrainStationCall.arrival normalize VERIFIED time_take TrainStationCall.dwell minute VERIFIED time_out source_departure normalize VERIFIED RequiredWait required_wait minute PROVISIONAL Kilometerage chainage_km numeric HIGH MaxSpeed max_speed numeric PROVISIONAL Distance source_distance preserve UNTRUSTED sumDistancezz source_sum_distance preserve UNKNOWN seir running_time_to_next minute VERIFIED قاعده: UNKNOWN / UNTRUSTED ↓ Preserve ↓ Do not use in production solver 5. مهم‌ترین Semantic Mapping در Access 5.1 TrainNo TrainNo به‌تنهایی هویت TrainRun نیست. هویت باید بر اساس ترکیبی از موارد زیر ساخته شود: TrainNo + TrainName + Origin + Destination + Direction + Operating Pattern زیرا در داده واقعی ممکن است یک TrainNo در contextهای مختلف تکرار شود. 6. TrainRun Canonical: @dataclass(frozen=True) class TrainRun: id: str service_name: str source_train_no: str origin_station_id: str destination_station_id: str direction: str operating_pattern: str | None train_type_id: str | None و: @dataclass(frozen=True) class TrainStationCall: train_run_id: str sequence: int station_id: str arrival_minute: int departure_minute: int dwell_minute: int required_wait_minute: int | None chainage_km: float | None source_distance_km: float | None derived_distance_km: float | None baseline_running_time_to_next: int | None 7. Midnight Normalization این بخش در V2.4 باید کاملاً اجرایی شود. داده واقعی نشان داده است که مثلاً: 23:46 + 50 min = 00:36 بنابراین مقایسه مستقیم HH:MM غلط است. مدل داخلی: absolute_minute مثلاً: Day 0 23:46 → 1426 Day 1 00:36 → 1476 تابع: def normalize_time( hhmm: str, previous_absolute_minute: int | None = None, ) -> int: hour, minute = map(int, hhmm.split(":")) current = hour * 60 + minute if previous_absolute_minute is None: return current while current < previous_absolute_minute: current += 24 * 60 return current اما در نسخه Production باید این تابع با منطق دقیق Day Offset و calendar pattern تکمیل شود و صرفاً یک heuristic ساده نباشد. 8. Validation روابط زمانی سه رابطه اصلی باید بررسی شوند. 8.1 Departure departure_i = arrival_i + dwell_i 8.2 Running Time برای ایستگاه‌های متوالی: arrival_(i+1) = departure_i + seir_i در صورت وجود تغییرات ناشی از calendar/day rollover، همه محاسبات در absolute minute انجام می‌شوند. 8.3 Required Wait در صورت وجود مقدار معتبر: dwell_i >= RequiredWait_i ولی چون معنای RequiredWait هنوز در همه داده‌ها به‌صورت قطعی تأیید نشده است، تا زمان تأیید نهایی: RequiredWait = PROVISIONAL خواهد بود. 9. Kilometerage Kilometerage با StationNumber یکی نیست. در نمونه واقعی: Gar → 157 ... Andimeshk → 674 و در مسیر برگشت: Andimeshk → 674 ... Gar → 157 بنابراین: derived_distance_km = abs(next.chainage_km - current.chainage_km) به‌عنوان Derived Evidence ذخیره می‌شود. این مقدار نباید مقدار source field Distance را overwrite کند. مدل: source_distance_km derived_distance_km هر دو باید قابل مشاهده باشند. 10. seir یکی از مهم‌ترین اصلاحات V2.4: seir نباید به Station attribute تبدیل شود. Semantics عملیاتی مشاهده‌شده: seir_i = Arrival_(i+1) - Departure_i بنابراین: TrainStationCall.running_time_to_next یا: RouteSegment.baseline_running_time مکان صحیح آن است. در آخرین Station: seir = NULL زیرا Segment بعدی وجود ندارد. 11. Directed Path مسیر نباید بر اساس نام ایستگاه‌ها یا ID مرتب شود. ترتیب: Sequence و در صورت نیاز: Infrastructure Topology است. مدل: @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: str station_ids: tuple[str, ...] physical_block_ids: tuple[str, ...] مثلاً: Gar ↓ Sakheh ↓ Bagh Yek ↓ ... ↓ Andimeshk و مسیر برگشت: Andimeshk ↓ ... ↓ Bagh Yek ↓ Sakheh ↓ Gar همان Physical Block می‌تواند در دو جهت استفاده شود: PhysicalBlock = direction-independent DirectedPath = direction-dependent MovementResource = depends on TrackType 12. Physical Block برای Single Track: Forward + Reverse = same physical resource بنابراین: B03 یک resource مشترک است. برای Double Track: B03:FORWARD B03:REVERSE می‌توانند resourceهای مجزا باشند. این موضوع یکی از مهم‌ترین تفاوت‌های V2.4 با مدل‌های ساده قبلی است. 13. Infrastructure Master در V2.4 حداقل باید این داده‌ها وجود داشته باشد: Station StationTrack PhysicalBlock Junction JunctionMovement JunctionConflict OperationalWindow برای هر Physical Block: @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int switch_time: int clearing_time: int 14. مشکل مهم: Infrastructure Master واقعی Access موجود به‌تنهایی الزاماً اطلاعات کامل زیرساخت را ندارد. بنابراین V2.4 باید بین: Operational Evidence و: Infrastructure Master تفکیک ایجاد کند. مثلاً وجود دو Station Call متوالی: A → B اثبات نمی‌کند که: A-B = SINGLE یا: A-B = DOUBLE پس TrackType باید از Infrastructure Master معتبر وارد شود. اگر اطلاعات موجود نباشد: TrackType = UNKNOWN و solver نباید حدس بزند. 15. Baseline Schedule Builder وظیفه: TrainStationCall + Running Time + Dwell + Operational Rules → Baseline Schedule خروجی: @dataclass(frozen=True) class BaselineSchedule: train_run_id: str station_calls: tuple[ScheduledStationCall, ...] block_movements: tuple[ScheduledBlockMovement, ...] این Schedule فقط برای reference نیست. Baseline باید برای این موارد استفاده شود: Validation Comparison Schedule Deviation Objective Calibration Model Verification 16. Baseline Difference بعد از Optimization: Optimized Schedule vs Baseline Schedule برای هر Station: arrival_delta departure_delta dwell_delta و برای هر Block: running_delta entry_delta exit_delta محاسبه می‌شود. مثلاً: Train 100 Arak Baseline Arrival = 747 Optimized Arrival = 752 Delta = +5 min این اطلاعات باید قابل ردیابی باشند. 17. Real Data Quality Gate Quality Gate حداقل باید این موارد را بررسی کند: Identity TrainRun identity Station identity Direction Origin Destination Sequence Sequence unique Sequence increasing No duplicate station call unless explicitly allowed Time arrival valid departure valid dwell >= 0 departure >= arrival arrival_next >= departure_current Running seir >= 0 Chainage numeric not contradictory Infrastructure station exists block exists directed path exists Operational train length known or explicitly unavailable train type known or explicitly unavailable 18. Quality Status هر Record باید یکی از وضعیت‌های زیر را داشته باشد: VALID VALID_WITH_WARNING REJECTED UNVERIFIED و کل DataVersion: NOT_CHECKED PASSED PASSED_WITH_WARNINGS FAILED 19. Reconciliation بین Access و Excel این مرحله باید جدا از Mapping انجام شود. هدف: Access Train Run ↕ Excel Service/Cycle مثلاً: Access: TrainNo = 100 TrainName = گار-اندیمشک1 Origin = Gar Destination = Andimeshk Excel: TrainNo from origin = ... TrainName = ... Reconciliation باید بر اساس: TrainNo TrainName Origin Destination Direction Operating Pattern انجام شود. خروجی: MATCHED PARTIAL_MATCH CONFLICT UNMATCHED 20. Conflict در Reconciliation مثلاً: Access: Gar → Andimeshk Excel: Andimeshk → Gar نباید به‌صورت silent merge شود. باید: CONFLICT_DIRECTION ثبت شود. همین موضوع برای: TrainNo mismatch Station mismatch Schedule mismatch Operating-day mismatch نیز برقرار است. 21. Real Data Execution Pipeline Pipeline رسمی V2.4: def execute_real_data_vertical_slice( access_source, excel_source, infrastructure_source, scenario, solver_config, ): raw_access = access_adapter.read(access_source) raw_excel = excel_adapter.read(excel_source) access_stage = staging.store(raw_access) excel_stage = staging.store(raw_excel) mapped_access = mapper.map_access(access_stage) mapped_excel = mapper.map_excel(excel_stage) quality_access = quality_gate.validate(mapped_access) quality_excel = quality_gate.validate(mapped_excel) reconciliation = reconciler.reconcile( mapped_access, mapped_excel, ) canonical = canonical_builder.build( mapped_access, mapped_excel, reconciliation, infrastructure_source, ) train_runs = train_run_builder.build(canonical) directed_paths = path_builder.build( train_runs, canonical.infrastructure, ) baseline = baseline_builder.build( train_runs, directed_paths, ) network_solution = network_optimizer.solve( canonical, scenario, solver_config, ) train_runs = train_run_builder.from_network_solution( network_solution ) schedule = detailed_scheduler.solve( train_runs, directed_paths, canonical.infrastructure, scenario, solver_config, ) validation = independent_validator.validate( schedule, canonical, ) capacity = capacity_engine.evaluate( canonical, scenario, solver_config, ) return ResultPackage( reconciliation=reconciliation, quality=quality_access, baseline=baseline, schedule=schedule, validation=validation, capacity=capacity, ) 22. معماری Hybrid در V2.4 Network Optimizer نباید خودش Scheduler دیگری بسازد. معماری: ┌────────────────────┐ │ Network Optimizer │ └─────────┬──────────┘ ↓ Train Allocations ↓ TrainRun Builder ↓ Directed Paths ↓ ┌────────────────────┐ │ Detailed Scheduler│ └─────────┬──────────┘ ↓ Validation ↓ ┌─────────┴──────────┐ │ │ PASS FAIL │ │ ↓ ↓ Capacity Conflict Feedback │ ↓ Network Re-optimization این ساختار از یک Monolithic CP-SAT بسیار بزرگ جلوگیری می‌کند. 23. Scheduling Conflict Feedback اگر Network Optimization بگوید: Train A = 10 Train B = 10 Train C = 8 ولی Detailed Scheduler نتواند آن را اجرا کند، خروجی صرفاً: INFEASIBLE نباید باشد. باید مشخص شود: resource = B03 conflict_type = OPPOSING_DIRECTION trains = A,B required_separation = 15 available = 8 و سپس: @dataclass(frozen=True) class SchedulingConflictFeedback: resource_id: str conflict_type: str train_ids: tuple[str, ...] required_separation: int available_separation: int penalty: float به Network Optimizer برگردد. 24. Iterative Network Scheduling V2.4 اولین نسخه‌ای است که این loop را اجرایی می‌کند: Iteration 1 Network Optimization ↓ Detailed Scheduling ↓ Validation ↓ Conflict Feedback ↓ Iteration 2 Network Optimization ↓ Detailed Scheduling ↓ ... و: NetworkIteration برای هر iteration ذخیره می‌شود. 25. Capacity Search واقعی Capacity Search دیگر نباید صرفاً تعداد Trainهای موجود در دیتاست را جست‌وجو کند. دو مفهوم جدا داریم: Existing Service Capacity ظرفیت بر اساس سرویس‌های موجود. Generated Service Capacity ظرفیت با تولید TrainRunهای کاندید. در V2.4 هر دو باید پشتیبانی شوند. 26. Candidate Train Generation اگر تقاضا: Gar → Andimeshk باشد، سیستم باید بتواند: Candidate Train 1 Candidate Train 2 Candidate Train 3 ... بسازد. پارامترها: OD TrainType Formation WagonRequirement EarliestDeparture LatestArrival OperatingDays Direction Route بنابراین ظرفیت دیگر وابسته به این نیست که در Access دقیقاً چند Train موجود بوده است. 27. Capacity Proof تعریف رسمی: [ C_r = \max{F: Schedule(F)\text{ is feasible} } ] اما Proof فقط زمانی معتبر است که: F = FEASIBLE + F = INDEPENDENTLY VALIDATED + F+1 = EXPLICITLY INFEASIBLE بنابراین: proof_valid = ( f_status in {"OPTIMAL", "FEASIBLE"} and f_validated and f_plus_one_status == "INFEASIBLE" ) و: UNKNOWN TIME_LIMIT MODEL_INVALID هرگز نباید به: INFEASIBLE تبدیل شوند. 28. Golden Real-Data Run مهم‌ترین Artifact V2.4: Golden Real-Data Capacity Run یک Run مرجع که با یک DataVersion مشخص و Scenario مشخص اجرا می‌شود. مثلاً: DataVersion: DV-IR-001 Scenario: SC-BASELINE-001 ModelVersion: 2.4.0 SolverConfiguration: seed = 1 workers = 1 time_limit = ... Run باید کاملاً Reproducible باشد. 29. Golden Run Snapshot باید این موارد ذخیره شوند: Input Snapshot Hash Data Version Source File Hash Mapping Version Model Version Scenario Version Solver Configuration Hash Infrastructure Version Candidate Set Hash بنابراین: Result = f( DataVersion, Scenario, ModelVersion, InfrastructureVersion, CandidateSet, SolverConfiguration ) 30. Result Package خروجی Golden Run: Run ├── Input Snapshot ├── Data Quality ├── Reconciliation ├── Train Runs ├── Directed Paths ├── Baseline Schedule ├── Candidate Services ├── Train Formations ├── Wagon Assignments ├── Locomotive Assignments ├── Network Allocation ├── Detailed Schedule ├── Station Track Assignments ├── Block Movements ├── Junction Movements ├── Conflicts ├── Validation ├── Capacity ├── F/F+1 Proof ├── Bottlenecks ├── Binding Constraints ├── Marginal Impact ├── Explanation └── Capacity Offers 31. Capacity Profiles یک عدد کافی نیست. خروجی: Infrastructure Capacity Operational Capacity Rolling Stock Capacity Transportable Capacity Allocated Capacity مثلاً: Infrastructure Capacity = CI Operational Capacity = CO Rolling Stock Capacity = CR Transportable Capacity = CT Allocated Capacity = CA اما این اعداد صرفاً وقتی معتبرند که هرکدام با روش محاسباتی و Constraint Set مشخص تعریف شده باشند. 32. Bottleneck Evidence هر Bottleneck باید Evidence داشته باشد. مثلاً: Resource: B03 Track Type: SINGLE Direction: BIDIRECTIONAL Conflict: OPPOSING_DIRECTION Required Separation: 15 min Available: 8 min Capacity Impact: -2 trains/day و: Evidence: ScheduleConflict #1842 TrainRun #TR-102 TrainRun #TR-117 33. Bottleneck Attribution سه سطح: BINDING NEAR_BINDING STRUCTURAL اما Classification باید از داده Solver/Validator تولید شود، نه از rule ثابت بدون evidence. 34. Marginal Capacity برای یک Resource: [ \Delta C = C(x+\Delta x)-C(x) ] مثلاً: Scenario A: B03 = SINGLE Scenario B: B03 = DOUBLE هر دو باید Full Re-solve شوند. سپس: Delta Capacity Delta Served Freight Delta Unserved Demand Delta Wagon Utilization Delta Loco Utilization محاسبه می‌شود. 35. Scenario V2.4 سناریوهای Golden: BASELINE Existing Infrastructure Existing Demand Existing Operating Pattern SINGLE-TRACK STRESS selected block = SINGLE DOUBLE-TRACK SCENARIO selected block = DOUBLE HEADWAY SCENARIO headway +5 min DEMAND SCENARIO demand +10% ROLLING STOCK SCENARIO + wagons + locomotives هر Scenario باید Full Re-solve شود. 36. V2.4 Database Additions حداقل جداول: data_version source_file source_record field_mapping quality_issue reconciliation_result station station_track physical_block junction junction_movement junction_conflict train train_run train_station_call directed_path route_segment baseline_schedule schedule schedule_station_call schedule_block_movement network_run network_iteration train_service_candidate train_allocation wagon_state empty_wagon_movement locomotive_assignment locomotive_cycle resource_usage resource_conflict binding_constraint capacity_evaluation capacity_proof proof_evidence bottleneck capacity_offer marketplace_allocation 37. Source Record برای Data Lineage: @dataclass(frozen=True) class SourceRecord: id: str source_file_id: str source_table: str | None source_row_number: int | None source_primary_key: str | None raw_payload: dict بنابراین اگر مثلاً TrainRun TR-100 ساخته شد، بتوانیم برگردیم به: Access File → Table → Record → Field 38. Field Evidence برای هر mapping: @dataclass(frozen=True) class FieldEvidence: source_record_id: str source_field: str canonical_entity: str canonical_field: str source_value: str | None transformed_value: str | None confidence: str rule_id: str این بخش برای Production بسیار مهم است. 39. API V2.4 Ingestion POST /api/v1/data/import/access POST /api/v1/data/import/excel Quality GET /api/v1/data/{version_id}/quality GET /api/v1/data/{version_id}/mapping GET /api/v1/data/{version_id}/reconciliation Train Runs GET /api/v1/train-runs GET /api/v1/train-runs/{id} GET /api/v1/train-runs/{id}/path Baseline GET /api/v1/baseline/{run_id} GET /api/v1/baseline/{run_id}/differences Capacity POST /api/v1/capacity/runs GET /api/v1/capacity/runs/{run_id} GET /api/v1/capacity/runs/{run_id}/proof GET /api/v1/capacity/runs/{run_id}/bottlenecks GET /api/v1/capacity/runs/{run_id}/offers 40. Real Data Import Command CLI: python -m scripts.ingest \ --access data/aaa.accdb \ --excel data/REPORTKholase_31-06-1405_02-19-35.xlsx \ --infrastructure data/infrastructure.yml خروجی: DataVersion: DV-... Access Records: ... Excel Records: ... Mapped: ... Warnings: ... Rejected: ... Reconciled: ... Canonical TrainRuns: ... 41. Golden Run Command python -m scripts.run_capacity \ --data-version DV-001 \ --scenario SC-BASELINE-001 \ --model-version 2.4.0 \ --seed 1 \ --workers 1 \ --proof 42. Acceptance Criteria V2.4 زمانی Complete است که: Data Access واقعی خوانده شود. Excel واقعی خوانده شود. Source Hash ثبت شود. Raw Records حفظ شوند. Mapping Mapping Registry فعال باشد. Confidence برای Fieldها ثبت شود. Unknown fields وارد Solver نشوند. Reconciliation Access/Excel reconciliation انجام شود. Conflictها قابل مشاهده باشند. Canonical TrainRun ساخته شود. TrainStationCall ساخته شود. DirectedPath ساخته شود. PhysicalBlock mapping انجام شود. Schedule Baseline Schedule ساخته شود. Midnight rollover صحیح باشد. Running time صحیح باشد. Dwell صحیح باشد. Solver Single Track درست مدل شود. Double Track درست مدل شود. Opposing Direction درست مدل شود. Same Direction Headway درست مدل شود. Clearing Time لحاظ شود. Station Track لحاظ شود. Junction Conflict لحاظ شود. Operational Window لحاظ شود. Validation Independent Validator اجرا شود. هیچ schedule غیرمعتبر به Capacity تبدیل نشود. Capacity F محاسبه شود. F validated باشد. F+1 explicitly tested باشد. UNKNOWN ≠ INFEASIBLE. Proof فقط در صورت کامل بودن evidence معتبر باشد. Explainability Binding Constraints ثبت شوند. Bottleneck Evidence ثبت شود. Marginal Impact قابل محاسبه باشد. 43. Golden Testهای V2.4 حداقل: G01 — Access ingestion G02 — Excel ingestion G03 — Field mapping G04 — Train identity reconciliation G05 — Midnight rollover G06 — seir mapping G07 — Kilometerage derived distance G08 — Directed path forward G09 — Directed path reverse G10 — Single track conflict G11 — Double track independence G12 — Opposing direction switch time G13 — Station track conflict G14 — Junction conflict G15 — Operational window G16 — Baseline reconstruction G17 — Network allocation G18 — Detailed schedule G19 — Independent validation G20 — F feasible G21 — F+1 infeasible G22 — UNKNOWN not proof G23 — Bottleneck evidence G24 — Capacity offer G25 — Reproducibility 44. مهم‌ترین Golden Test یک تست نهایی باید کل سیستم را اجرا کند: REAL ACCESS + REAL EXCEL + REAL INFRASTRUCTURE + BASE SCENARIO ↓ CANONICAL ↓ NETWORK ↓ TRAIN FORMATION ↓ WAGON ↓ LOCOMOTIVE ↓ TRAIN RUN ↓ DIRECTED PATH ↓ TIME-SPACE SCHEDULE ↓ VALIDATION ↓ CAPACITY ↓ F/F+1 ↓ PROOF ↓ CAPACITY OFFER این تست همان Production Vertical Slice واقعی سیستم خواهد بود. 45. نکته مهم درباره اجرای واقعی در این مرحله نباید ادعا کنیم که aaa.accdb یا فایل Excel واقعاً در محیط فعلی اجرا شده‌اند؛ فایل‌های اصلی فعلاً در این گفتگو به‌صورت قابل اجرای مستقیم در اختیار Runtime نیستند. بنابراین V2.4 باید به‌صورت زیر تعریف شود: V2.4 Design + Production Implementation و پس از قرار گرفتن فایل‌های واقعی در محیط اجرا: V2.4 Real Data Execution انجام شود. این تفکیک مهم است؛ چون در یک سیستم ظرفیت‌سنجی Production، تفاوت بین: Code Ready و: Real Data Executed باید کاملاً قابل audit باشد. 46. V2.4 Definition of Done V2.4 فقط زمانی Done است که حداقل یک سرویس واقعی OD بتواند این مسیر را بدون manual intervention طی کند: Access/Excel → Raw → Staging → Mapping → Quality → Reconciliation → Canonical → Infrastructure Mapping → TrainRun → DirectedPath → Baseline → Network Allocation → Train Formation → Wagon Feasibility → Locomotive Feasibility → Detailed Scheduling → Independent Validation → Capacity Search → F/F+1 Proof → Bottleneck → Explanation → Capacity Offer و برای همان Run بتوانیم بگوییم: Which source record created this TrainRun? Which mapping created this StationCall? Which infrastructure block created this conflict? Which constraint limited capacity? Which schedule proves F? Why is F+1 infeasible? Which CapacityOffer was generated? اگر این زنجیره برقرار باشد، سیستم از یک Railway Optimization Prototype عبور کرده و وارد محدوده یک Auditable Railway Capacity Planning Engine می‌شود. 47. نسخه بعدی — V2.5 پس از V2.4، مرحله طبیعی بعدی: V2.5 — Production Network Solver & Rolling-Horizon Capacity Engine خواهد بود. تمرکز V2.5: Real Data + Multi-OD + Time-Dependent Demand + Time-Dependent Empty Wagon Network + Locomotive Cycle + Detailed Time-Space Scheduling + Rolling Horizon + Iterative Network/Schedule Feedback + Parallel Scenario Evaluation و نهایتاً: Railway Capacity Planning Workbench ↓ Production Engine ↓ Real Iranian Railway Data ↓ Network Capacity + Scheduling ↓ Marketplace Capacity Offer این مسیر، از نظر معماری، ادامه مستقیم V2.3 و V2.4 است و نیاز به بازطراحی مدل پایه ندارد. گام بعدی پیشنهادی من V2.4 را از سند طراحی به کد اجرایی واقعی تبدیل کنیم: ابتدا Access Adapter + Excel Adapter + Staging + Mapping Registry + Quality Gate را کامل پیاده‌سازی کنیم، سپس روی همان خروجی TrainRun → DirectedPath → Baseline Schedule را اجرا کنیم. این کار ریسک را خیلی کمتر از این دارد که مستقیماً وارد توسعه UI یا یک Solver بزرگ شویم. --- ## ادامه 55 — پرسش کاربر (`message-id:514cb831-e95f-468b-8859-b6b6796d6733`) بله لطفا --- ## ادامه 55 — پاسخ دستیار (`message-id:58d66b85-c113-4292-aef9-94ae8ee35316`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 56 — پرسش کاربر (`message-id:baa47e27-3f60-4198-98e7-183737379f51`) بله بدون اون به صورت متنی همینجا ادامه بده --- ## ادامه 56 — پاسخ دستیار (`message-id:56cf4fb4-2d61-4237-8b3c-5c844688e7ee`) حتماً. ادامه را کاملاً متنی و در همان مسیر فنی پروژه می‌بریم. در این مرحله بهتر است به‌جای تولید دوباره سند معماری، وارد پیاده‌سازی واقعی V2.4 شویم؛ یعنی دقیقاً مشخص کنیم چه فایل‌هایی باید ساخته شوند، چه کدی داخلشان باشد و چگونه از داده Access/Excel به TrainRun → DirectedPath → Baseline Schedule برسیم. V2.4 — Implementation Package 1. ساختار اجرایی پیشنهادی railway_capacity_workbench/ │ ├── pyproject.toml ├── README.md │ ├── app/ │ ├── __init__.py │ │ │ ├── domain/ │ │ ├── __init__.py │ │ ├── train.py │ │ ├── station.py │ │ ├── route.py │ │ ├── schedule.py │ │ └── infrastructure.py │ │ │ ├── adapters/ │ │ ├── __init__.py │ │ ├── access.py │ │ └── excel.py │ │ │ ├── staging/ │ │ ├── __init__.py │ │ └── models.py │ │ │ ├── mapping/ │ │ ├── __init__.py │ │ ├── registry.py │ │ └── access_train_movement.py │ │ │ ├── quality/ │ │ ├── __init__.py │ │ ├── rules.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ ├── __init__.py │ │ └── train_identity.py │ │ │ ├── canonical/ │ │ ├── __init__.py │ │ ├── train_builder.py │ │ ├── station_builder.py │ │ └── path_builder.py │ │ │ ├── scheduling/ │ │ ├── __init__.py │ │ └── baseline.py │ │ │ └── pipeline/ │ ├── __init__.py │ └── real_data.py │ ├── mappings/ │ ├── access_train_movement.yml │ └── excel_kholase.yml │ ├── config/ │ └── v2_4.yml │ └── tests/ ├── test_access_mapping.py ├── test_midnight.py ├── test_identity.py ├── test_path.py └── test_baseline.py 2. Domain Model ابتدا مدل Canonical را از Source جدا می‌کنیم. app/domain/train.py Python from dataclasses import dataclass from typing import Optional @dataclass(frozen=True) class TrainStationCall: train_run_id: str sequence: int station_id: str arrival_minute: int departure_minute: int dwell_minute: int required_wait_minute: Optional[int] chainage_km: Optional[float] source_distance_km: Optional[float] derived_distance_km: Optional[float] baseline_running_time_to_next: Optional[int] @dataclass(frozen=True) class TrainRun: id: str source_train_no: str service_name: str origin_station_id: str destination_station_id: str direction: str operating_pattern: Optional[str] calls: tuple[TrainStationCall, ...] نکته مهم: TrainRun با TrainService یکی نیست. مثلاً: TrainService گار-اندیمشک1 TrainRun TrainNo = 100 Direction = FORWARD و: TrainRun TrainNo = 101 Direction = REVERSE ممکن است هر دو به یک Service تعلق داشته باشند. 3. Station Model Python from dataclasses import dataclass from typing import Optional @dataclass(frozen=True) class Station: id: str name: str source_station_number: Optional[str] chainage_km: Optional[float] = None در این مرحله StationNumber را به‌عنوان ID فیزیکی Station فرض نمی‌کنیم. مثلاً: StationName = Gar StationNumber = 123 یعنی: station.id != station_number بلکه: station.source_station_number = 123 4. Time Normalization این بخش باید از همان ابتدا درست طراحی شود. Python MINUTES_PER_DAY = 1440 def parse_hhmm(value: str) -> int: hour, minute = map(int, value.strip().split(":")) if not 0 <= hour <= 23: raise ValueError(f"Invalid hour: {hour}") if not 0 <= minute <= 59: raise ValueError(f"Invalid minute: {minute}") return hour * 60 + minute و: Python def normalize_after_previous( current_minute: int, previous_minute: int | None, ) -> int: if previous_minute is None: return current_minute while current_minute < previous_minute: current_minute += MINUTES_PER_DAY return current_minute مثلاً: 23:46 → 1426 00:36 → 1476 00:56 → 1496 02:19 → 1579 این دقیقاً برای داده‌هایی مثل مسیر واقعی که در آن زمان از نیمه‌شب عبور می‌کند ضروری است. 5. Access Adapter Access Adapter نباید هیچ مفهوم Domain را بشناسد. وظیفه آن فقط: Access → Raw Record است. Python from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class RawAccessRecord: source_file: str source_table: str source_row_number: int payload: dict سپس: Python def read_access( path: str, table: str, ) -> list[RawAccessRecord]: import pyodbc connection_string = ( "DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};" f"DBQ={Path(path).resolve()};" ) records = [] with pyodbc.connect(connection_string) as conn: cursor = conn.cursor() cursor.execute(f"SELECT * FROM [{table}]") columns = [ column[0] for column in cursor.description ] for row_number, row in enumerate( cursor.fetchall(), start=2, ): records.append( RawAccessRecord( source_file=str(path), source_table=table, source_row_number=row_number, payload=dict(zip(columns, row)), ) ) return records این Adapter نباید مثلاً seir را تبدیل کند. این کار Mapping Layer است. 6. Excel Adapter Excel هم مشابه: Python @dataclass(frozen=True) class RawExcelRecord: source_file: str sheet_name: str source_row_number: int payload: dict و: Python import pandas as pd def read_excel( path: str, sheet_name=0, ) -> list[RawExcelRecord]: df = pd.read_excel( path, sheet_name=sheet_name, dtype=object, ) result = [] for index, row in df.iterrows(): payload = {} for key, value in row.items(): if pd.isna(value): value = None payload[str(key)] = value result.append( RawExcelRecord( source_file=str(path), sheet_name=str(sheet_name), source_row_number=index + 2, payload=payload, ) ) return result 7. Staging Staging باید Source را تقریباً بدون تغییر نگه دارد. Python @dataclass(frozen=True) class StagingRecord: id: str source_type: str source_file: str source_table: str | None source_row_number: int payload: dict چرا؟ چون اگر شش ماه بعد کسی بپرسد: این TrainRun از کدام رکورد Access ساخته شد؟ باید بتوانیم برگردیم: TrainRun ↓ Canonical Call ↓ Mapping Evidence ↓ Staging Record ↓ Access Row 8. Mapping Registry Mapping باید explicit باشد. Python from dataclasses import dataclass from enum import Enum class Confidence(str, Enum): VERIFIED = "VERIFIED" HIGH = "HIGH" PROVISIONAL = "PROVISIONAL" UNTRUSTED = "UNTRUSTED" UNKNOWN = "UNKNOWN" @dataclass(frozen=True) class FieldMapping: source_field: str canonical_field: str confidence: Confidence rule_id: str Mappingهای Access: Python ACCESS_MAPPING = [ FieldMapping( "TrainNo", "source_train_no", Confidence.VERIFIED, "ACC_TRAINNO_001", ), FieldMapping( "TrainName", "service_name", Confidence.VERIFIED, "ACC_TRAINNAME_001", ), FieldMapping( "StationName", "station_name", Confidence.VERIFIED, "ACC_STATIONNAME_001", ), FieldMapping( "StationNumber", "source_station_number", Confidence.VERIFIED, "ACC_STATIONNUMBER_001", ), FieldMapping( "Sequence", "sequence", Confidence.VERIFIED, "ACC_SEQUENCE_001", ), FieldMapping( "time_in", "arrival", Confidence.VERIFIED, "ACC_TIMEIN_001", ), FieldMapping( "time_take", "dwell", Confidence.VERIFIED, "ACC_TIMETAKE_001", ), FieldMapping( "RequiredWait", "required_wait", Confidence.PROVISIONAL, "ACC_REQUIREDWAIT_001", ), FieldMapping( "Kilometerage", "chainage_km", Confidence.HIGH, "ACC_CHAINAGE_001", ), FieldMapping( "MaxSpeed", "max_speed", Confidence.PROVISIONAL, "ACC_MAXSPEED_001", ), FieldMapping( "Distance", "source_distance_km", Confidence.UNTRUSTED, "ACC_DISTANCE_001", ), FieldMapping( "sumDistancezz", "source_sum_distance", Confidence.UNKNOWN, "ACC_SUMDIST_001", ), FieldMapping( "seir", "baseline_running_time_to_next", Confidence.VERIFIED, "ACC_SEIR_001", ), ] 9. Mapping Rule برای seir این Rule باید صریح باشد: Python def map_seir(value): if value is None: return None return int(float(value)) و Canonical: baseline_running_time_to_next نه: Station.running_time 10. Mapping Rule برای Distance اینجا دو مقدار جدا نگه می‌داریم. Python def map_source_distance(value): if value in (None, ""): return None return float(value) و: Python def derive_distance( current_chainage, next_chainage, ): if current_chainage is None: return None if next_chainage is None: return None return abs( float(next_chainage) - float(current_chainage) ) بنابراین: Distance ↓ source_distance_km Kilometerage(i) + Kilometerage(i+1) ↓ derived_distance_km 11. Mapping RequiredWait فعلاً: Python required_wait = ( int(float(value)) if value not in (None, "") else None ) اما metadata: confidence = PROVISIONAL خواهد بود. یعنی Solver Production نباید صرفاً به خاطر وجود این Field آن را به‌عنوان یک Railway Rule قطعی فرض کند. 12. Canonical Builder اکنون Raw/Mapped data تبدیل به TrainRun می‌شود. Python def build_train_run(group): rows = sorted( group, key=lambda x: int(x["Sequence"]), ) train_run_id = ( f'TR:{rows[0]["TrainNo"]}:' f'{rows[0]["TrainName"]}' ) raw_arrivals = [ parse_hhmm(row["time_in"]) for row in rows ] arrivals = [] previous = None for value in raw_arrivals: value = normalize_after_previous( value, previous, ) arrivals.append(value) previous = value calls = [] for index, row in enumerate(rows): arrival = arrivals[index] dwell = int( float(row["time_take"] or 0) ) departure = arrival + dwell current_chainage = ( float(row["Kilometerage"]) if row["Kilometerage"] not in (None, "") else None ) next_chainage = None if index + 1 < len(rows): next_value = rows[index + 1]["Kilometerage"] if next_value not in (None, ""): next_chainage = float(next_value) derived_distance = derive_distance( current_chainage, next_chainage, ) running_time = ( int(float(row["seir"])) if row["seir"] not in (None, "") else None ) call = TrainStationCall( train_run_id=train_run_id, sequence=int(row["Sequence"]), station_id=f'ST:{row["StationName"]}', arrival_minute=arrival, departure_minute=departure, dwell_minute=dwell, required_wait_minute=( int(float(row["RequiredWait"])) if row["RequiredWait"] not in (None, "") else None ), chainage_km=current_chainage, source_distance_km=( float(row["Distance"]) if row["Distance"] not in (None, "") else None ), derived_distance_km=derived_distance, baseline_running_time_to_next=running_time, ) calls.append(call) return TrainRun( id=train_run_id, source_train_no=str(rows[0]["TrainNo"]), service_name=str(rows[0]["TrainName"]), origin_station_id=calls[0].station_id, destination_station_id=calls[-1].station_id, direction=detect_direction(calls), operating_pattern=None, calls=tuple(calls), ) 13. Direction Detection در V2.4 بهتر است Direction صرفاً با افزایش/کاهش Chainage تشخیص داده شود، ولی این نتیجه باید Evidence-based باشد. Python def detect_direction(calls): chainages = [ c.chainage_km for c in calls if c.chainage_km is not None ] if len(chainages) < 2: return "UNKNOWN" if chainages[-1] > chainages[0]: return "FORWARD" if chainages[-1] < chainages[0]: return "REVERSE" return "UNKNOWN" در داده واقعی نمونه: Gar → Andimeshk 157 → 674 بنابراین: FORWARD و: Andimeshk → Gar 674 → 157 بنابراین: REVERSE اما این Direction باید بعداً با Infrastructure/Service definition قابل تطبیق باشد. 14. Identity Reconciliation هویت TrainRun نباید: TrainNo باشد. Canonical Identity Key: Python @dataclass(frozen=True) class TrainIdentityKey: train_no: str train_name: str origin: str destination: str direction: str operating_pattern: str | None و: Python def identity_key(train_run): return TrainIdentityKey( train_no=train_run.source_train_no, train_name=train_run.service_name, origin=train_run.origin_station_id, destination=train_run.destination_station_id, direction=train_run.direction, operating_pattern=train_run.operating_pattern, ) 15. Reconciliation Result Python class ReconciliationStatus(str, Enum): MATCHED = "MATCHED" PARTIAL_MATCH = "PARTIAL_MATCH" CONFLICT = "CONFLICT" UNMATCHED = "UNMATCHED" و: Python @dataclass(frozen=True) class ReconciliationResult: access_identity: str excel_identity: str | None status: ReconciliationStatus differences: tuple[str, ...] این خروجی برای Data Quality Workbench بسیار مهم خواهد بود. 16. Directed Path Builder Python @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: str station_ids: tuple[str, ...] physical_block_ids: tuple[str, ...] ساخت: Python def build_directed_path(train_run): station_ids = tuple( call.station_id for call in sorted( train_run.calls, key=lambda x: x.sequence, ) ) block_ids = [] for a, b in zip( station_ids, station_ids[1:], ): block_ids.append( make_physical_block_id(a, b) ) return DirectedPath( id=f"PATH:{train_run.id}", route_id=f"ROUTE:{train_run.origin_station_id}:" f"{train_run.destination_station_id}", direction=train_run.direction, station_ids=station_ids, physical_block_ids=tuple(block_ids), ) 17. Physical Block ID Python def make_physical_block_id( station_a: str, station_b: str, ) -> str: a, b = sorted( [station_a, station_b] ) return f"{a}::{b}" این یعنی: Gar::A و: A::Gar یک Physical Block هستند. اما: DirectedPath دو حالت متفاوت دارد: Gar → A و: A → Gar 18. Baseline Schedule Baseline باید از Source Evidence ساخته شود. Python @dataclass(frozen=True) class BaselineBlockMovement: train_run_id: str sequence: int physical_block_id: str direction: str entry_minute: int exit_minute: int station_from: str station_to: str سپس: Python def build_baseline_schedule( train_run, path, ): movements = [] calls = sorted( train_run.calls, key=lambda x: x.sequence, ) for i, block_id in enumerate( path.physical_block_ids ): current = calls[i] next_call = calls[i + 1] movements.append( BaselineBlockMovement( train_run_id=train_run.id, sequence=i + 1, physical_block_id=block_id, direction=train_run.direction, entry_minute=current.departure_minute, exit_minute=next_call.arrival_minute, station_from=current.station_id, station_to=next_call.station_id, ) ) return tuple(movements) 19. Baseline Validation برای هر Block: T run,i ​ =Arrival i+1 ​ −Departure i ​ و باید با: seir مقایسه شود. Python def validate_running_time(train_run): issues = [] calls = train_run.calls for i in range(len(calls) - 1): current = calls[i] next_call = calls[i + 1] actual = ( next_call.arrival_minute - current.departure_minute ) expected = ( current.baseline_running_time_to_next ) if expected is None: continue if actual != expected: issues.append( f"{train_run.id}: " f"sequence={current.sequence}: " f"actual={actual}, " f"expected={expected}" ) return issues این تست بسیار ارزشمند است. 20. Quality Gate Quality Gate باید قبل از Canonical Production Run اجرا شود. Python class QualityGateError(Exception): pass def quality_gate(issues): errors = [ issue for issue in issues if issue.severity == "ERROR" ] if errors: raise QualityGateError( f"{len(errors)} quality errors found" ) اما Warning نباید الزاماً Run را متوقف کند. 21. DataVersion برای هر Import: Python @dataclass(frozen=True) class DataVersion: id: str name: str source_type: str source_uri: str source_hash: str model_version: str status: str Hash: Python import hashlib def sha256_file(path): h = hashlib.sha256() with open(path, "rb") as file: for chunk in iter( lambda: file.read(1024 * 1024), b"", ): h.update(chunk) return h.hexdigest() این Hash بعداً برای Reproducibility حیاتی است. 22. Pipeline نهایی V2.4 حالا کل Pipeline: Python def real_data_pipeline( access_path, access_table, excel_path, infrastructure, ): # 1 access_raw = read_access( access_path, access_table, ) # 2 excel_raw = read_excel( excel_path, ) # 3 access_staging = stage_access( access_raw ) excel_staging = stage_excel( excel_raw ) # 4 access_mapped = map_access( access_staging ) excel_mapped = map_excel( excel_staging ) # 5 quality = validate( access_mapped, excel_mapped, ) quality_gate(quality) # 6 reconciliation = reconcile( access_mapped, excel_mapped, ) # 7 canonical = build_canonical( access_mapped, infrastructure, ) # 8 train_runs = canonical.train_runs # 9 paths = [ build_directed_path(train_run) for train_run in train_runs ] # 10 baseline = [ build_baseline_schedule( train_run, path, ) for train_run, path in zip(train_runs, paths) ] # 11 return { "quality": quality, "reconciliation": reconciliation, "train_runs": train_runs, "paths": paths, "baseline": baseline, } 23. یک اصلاح معماری مهم در نسخه قبلی zip(train_runs, paths) به‌عنوان shortcut وجود داشت. در Production نباید به ترتیب لیست اعتماد کنیم. بهتر: Python paths_by_train_run = { path.train_run_id: path for path in paths } و: Python path = paths_by_train_run[ train_run.id ] قاعده: هیچ Association مهمی نباید بر اساس Position در List انجام شود. فقط: ID / Foreign Key 24. بعد از این مرحله: وارد Scheduler شویم وقتی V2.4 Data Pipeline سالم شد، خروجی: TrainRun + DirectedPath + BaselineSchedule + Infrastructure را به Scheduler V1.7 می‌دهیم. برای هر Train: A_i,s D_i,s E_i,b X_i,b C_i,b ساخته می‌شود. و سپس: Precedence + Running Time + Dwell + Headway + Switch Time + Clearing + Station Track + Junction + Operational Window اعمال می‌شوند. 25. ترتیب اجرای واقعی پروژه من پیشنهاد می‌کنم از اینجا دقیقاً این Sequence را حفظ کنیم: V2.4.1 Access Adapter ↓ V2.4.2 Excel Adapter ↓ V2.4.3 Raw/Staging ↓ V2.4.4 Mapping Registry ↓ V2.4.5 Quality Gate ↓ V2.4.6 Identity Reconciliation ↓ V2.4.7 Canonical TrainRun ↓ V2.4.8 Midnight Normalization ↓ V2.4.9 DirectedPath ↓ V2.4.10 Baseline Schedule ↓ V2.4.11 Infrastructure Mapping ↓ V2.4.12 V1.7 Detailed Scheduler ↓ V2.4.13 Independent Validator ↓ V2.4.14 Capacity Search ↓ V2.4.15 F/F+1 Proof ↓ V2.4.16 Bottleneck ↓ V2.4.17 Capacity Offer و یک نکته بسیار مهم در حال حاضر V2.4.1 تا V2.4.10 را می‌توان مستقل از Solver کامل و تست کرد. این دقیقاً کاری است که باید انجام دهیم؛ چون اگر داده از ابتدا اشتباه Canonical شود، هر مقدار پیچیدگی در CP-SAT فقط یک جواب اشتباه را با ظاهر علمی‌تر تولید می‌کند. پس اولین Milestone واقعی پروژه اکنون این است: Access/Excel → Canonical TrainRun → DirectedPath → Baseline Schedule با 100٪ Data Lineage و Midnight Handling صحیح. بعد از آن، مستقیم می‌رویم سراغ V2.4.11 تا V2.4.15: اتصال همین داده واقعی به Scheduler عملیاتی V1.7 و ساخت اولین Capacity Proof واقعی. --- ## ادامه 57 — پرسش کاربر (`message-id:99d76f45-9ee7-470b-b112-b5b02dd75d0b`) بله لطفا --- ## ادامه 57 — پاسخ دستیار (`message-id:73f80b53-8642-4101-a00b-09db7fce4839`) حتماً. حالا وارد بخش حساس‌تر V2.4 می‌شویم: اتصال Canonical Real Data به Scheduler عملیاتی V1.7 و سپس Capacity Proof. در این مرحله دیگر صرفاً Data Pipeline نداریم؛ خروجی داده واقعی باید به یک Time-Space Scheduling Problem تبدیل شود و Scheduler بتواند برای تعداد مشخصی Train، برنامه زمانی معتبر تولید کند. V2.4.11 تا V2.4.17 Real Data → Operational Scheduler → Capacity Proof معماری این مرحله Canonical TrainRun │ ├── TrainStationCall[] │ ├── DirectedPath │ └── Infrastructure Master │ ▼ Scheduling Problem Builder │ ├── Time Variables ├── Block Resources ├── Station Tracks ├── Junctions ├── Headway ├── Switch Time ├── Clearing └── Operational Windows │ ▼ CP-SAT │ ▼ Generated Schedule │ ▼ Independent Validator │ ┌─────┴─────┐ │ │ PASS FAIL │ │ ▼ ▼ Capacity Conflict │ Evidence ▼ F / F+1 │ ▼ Capacity Proof 1. Infrastructure Domain اول باید Infrastructure را از Train Movement جدا نگه داریم. Python from dataclasses import dataclass from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" @dataclass(frozen=True) class PhysicalBlock: id: str station_a: str station_b: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int = 3 switch_time: int = 5 clearing_time: int = 0 def running_time(self, direction: Direction) -> int: if direction == Direction.FORWARD: return self.running_time_forward return self.running_time_reverse نکته اساسی: PhysicalBlock جهت ندارد. ولی: DirectedPath جهت دارد. 2. Movement Resource این تابع یکی از اجزای کلیدی Scheduler است. Python def movement_resource( block: PhysicalBlock, direction: Direction, ) -> str: if block.track_type == TrackType.SINGLE: return block.id return f"{block.id}:{direction.value}" بنابراین اگر: B03 = SINGLE باشد: Train A → B03 Train B ← B03 هر دو: Resource = B03 دارند. اما اگر: B03 = DOUBLE باشد: B03:FORWARD B03:REVERSE مجزا هستند. 3. Time-Space Variables برای هر Train و Station: A i,s ​ Arrival و: D i,s ​ Departure. برای هر Block: E i,b ​ Entry X i,b ​ Exit و: C i,b ​ Clear. در Python: Python arrival[(train.id, station.id)] = model.NewIntVar( 0, horizon, ... ) departure[(train.id, station.id)] = model.NewIntVar( 0, horizon, ... ) و: Python entry[(train.id, block.id)] = model.NewIntVar( 0, horizon, ... ) exit[(train.id, block.id)] = model.NewIntVar( 0, horizon, ... ) clear[(train.id, block.id)] = model.NewIntVar( 0, horizon, ... ) 4. Station Precedence اگر Train در Station S1 است و سپس به S2 می‌رود: E i,b ​ =D i,S1 ​ و: A i,S2 ​ =X i,b ​ بنابراین: Python model.Add( entry[train_block] == departure[train_station] ) model.Add( arrival[next_station] == exit[train_block] ) 5. Running Time برای هر Block: X i,b ​ ≥E i,b ​ +T run ​ (i,b) یعنی: Python running_time = block.running_time( train.direction ) model.Add( exit_var >= entry_var + running_time ) در V2.4 می‌توانیم از seir برای Baseline استفاده کنیم، ولی باید بین: Baseline Running Time و: Infrastructure Minimum Running Time تفاوت قائل شویم. این دو الزاماً یکی نیستند. 6. Minimum Running Time مدل Production بهتر است: Python @dataclass(frozen=True) class BlockRunningTime: block_id: str train_type_id: str | None direction: Direction minimum_minutes: int baseline_minutes: int | None داشته باشد. پس: T run actual ​ ≥T run minimum ​ و در Baseline: T run baseline ​ =seir 7. Dwell برای Station: D i,s ​ ≥A i,s ​ +T dwell,i,s ​ اگر Required Wait معتبر باشد: T dwell ​ ≥RequiredWait ولی فعلاً چون RequiredWait در داده واقعی PROVISIONAL است، باید Configuration داشته باشیم: YAML use_required_wait: enabled: false confidence_required: VERIFIED این کار جلوی استفاده تصادفی از یک Field تأییدنشده را می‌گیرد. 8. Earliest Departure برای Train: Python model.Add( departure[origin] >= profile.earliest_departure ) مثلاً: Train 100 Earliest Departure = 1200 پس: D 100,Origin ​ ≥1200 9. Latest Arrival اگر وجود داشته باشد: Python model.Add( arrival[destination] <= profile.latest_arrival ) این Constraint برای Candidate Train بسیار مهم است. 10. Same-Direction Headway اگر دو Train در یک Single Track و یک جهت حرکت کنند: Entry j ​ ≥Clear i ​ +H same ​ یا برعکس. برای هر زوج: Python same_direction = ( train_a.direction == train_b.direction ) سپس: Python separation = block.headway_same_direction 11. Opposing Direction برای Single Track: H opposite ​ =max(H same ​ ,T switch ​ ) پس: Python separation = max( block.headway_same_direction, block.switch_time, ) و: Train A: B03 FORWARD Train B: B03 REVERSE باید یکی قبل از دیگری Block را آزاد کند. 12. Clearing Time Clear کردن Block با Exit یکی نیست. Entry ↓ Running ↓ Exit ↓ Clearing ↓ Block Released پس: C i,b ​ =X i,b ​ +T clear,b ​ و: Python model.Add( clear_var == exit_var + block.clearing_time ) 13. Station Track Assignment برای هر Train و Station: Train ↓ Station ↓ Track 1 Track 2 Track 3 ... متغیر: z i,s,k ​ ∈{0,1} و: k ∑ ​ z i,s,k ​ =1 در CP-SAT: Python assignment_vars = [] for track in station.tracks: z = model.NewBoolVar( f"assign_{train.id}_{station.id}_{track.id}" ) assignment_vars.append(z) model.AddExactlyOne( assignment_vars ) 14. Station Length Constraint اگر: Train Length = 750 m و: Track = 600 m نباید Assignment ساخته شود. Python if train.length_m > track.usable_length_m: continue این Constraint باید قبل از Solver نیز به‌عنوان Candidate Pruning اجرا شود. 15. Station Track Conflict اگر دو Train همزمان روی یک Track باشند: Train A ───────────── Train B ───────────── ↑ Conflict NoOverlap لازم است. برای هر Track: Python model.AddNoOverlap( intervals_for_track ) 16. Junction Junction نباید با یک NoOverlap کلی مدل شود. مثلاً: Movement M1: A → J → B Movement M2: C → J → D Movement M3: A → J → D ممکن است: M1 conflicts M3 ولی: M1 does not conflict M2 بنابراین: Python @dataclass(frozen=True) class JunctionConflict: movement_a: str movement_b: str separation_time: int و فقط زوج‌های واقعاً متعارض Constraint می‌گیرند. 17. Operational Window مثلاً: Maintenance: 02:00 – 04:00 Train باید: قبل از 02:00 عبور کند یا: بعد از 04:00 پس: Exit≤Start یا: Entry≥End در CP-SAT: Python before = model.NewBoolVar(...) after = model.NewBoolVar(...) model.Add( exit_var <= window.start ).OnlyEnforceIf(before) model.Add( entry_var >= window.end ).OnlyEnforceIf(after) model.AddExactlyOne(before, after) 18. Baseline Objective در حالت Baseline Reconstruction هدف Capacity نیست. هدف: نزدیک‌ترین Schedule معتبر به برنامه موجود. مثلاً: min∑∣A optimized −A baseline ∣+∑∣D optimized −D baseline ∣ در CP-SAT: Python deviation = model.NewIntVar( 0, horizon, ... ) model.AddAbsEquality( deviation, arrival_var - baseline_arrival, ) و: Python model.Minimize( sum(deviations) ) 19. Capacity Mode در Capacity Mode هدف تغییر می‌کند. دیگر: Baseline similarity اولویت اصلی نیست. هدف: Maximum feasible train count یا در Network: max∑QF است. بنابراین: BASELINE MODE ↓ minimum deviation CAPACITY MODE ↓ maximum feasible capacity 20. Independent Validator این قسمت نباید همان کد Constraint Builder را دوباره استفاده کند. اگر Solver می‌گوید: FEASIBLE Validator باید مستقل بررسی کند. مثلاً: Python def validate_block_sequence(schedule): issues = [] for train in schedule.trains: movements = sorted( train.block_movements, key=lambda x: x.sequence, ) for a, b in zip( movements, movements[1:], ): if b.entry < a.exit: issues.append( ValidationIssue( code="BLOCK_SEQUENCE", ... ) ) return issues 21. Validation Result Python @dataclass(frozen=True) class ValidationResult: valid: bool errors: tuple[str, ...] warnings: tuple[str, ...] conflicts: tuple[str, ...] قاعده: valid = False یعنی Schedule قابل استفاده برای Capacity نیست. 22. Capacity Evaluation حالا: Python @dataclass(frozen=True) class CapacityEvaluation: requested_f: int solver_status: str schedule_valid: bool feasible: bool validation_errors: tuple[str, ...] و: Python def evaluate_f(f): solution = scheduler.solve( train_count=f ) if solution.status not in { "OPTIMAL", "FEASIBLE", }: return CapacityEvaluation( requested_f=f, solver_status=solution.status, schedule_valid=False, feasible=False, validation_errors=(), ) validation = validator.validate( solution.schedule ) return CapacityEvaluation( requested_f=f, solver_status=solution.status, schedule_valid=validation.valid, feasible=validation.valid, validation_errors=validation.errors, ) 23. بسیار مهم: INVALID ≠ INFEASIBLE اگر Solver Schedule تولید کند ولی Validator آن را رد کند: INVALID است. نه: INFEASIBLE این دو باید کاملاً جدا بمانند. همین‌طور: UNKNOWN نباید: INFEASIBLE تعبیر شود. 24. Capacity Search برای Candidate Services: Python def search_capacity(candidates): feasible_results = [] for f in range( 1, len(candidates) + 1, ): result = evaluate_f(f) if not result.feasible: break feasible_results.append(result) return feasible_results[-1] اما این نسخه فقط وقتی معتبر است که Monotonicity مسئله تضمین شده باشد. برای Production بهتر است Search به شکل زیر باشد: Candidate F ↓ Solve ↓ Validate ↓ Feasible? ├── Yes → continue └── No → classify reason و در صورت استفاده از Binary Search باید ابتدا Monotonicity assumption مستند و تست شود. 25. Capacity Proof فرض کنید: F = 30 داریم. ابتدا: Solve(30) باید: FEASIBLE و Validator: VALID باشد. سپس: Solve(31) باید: INFEASIBLE باشد. فقط در این حالت: Python proof_valid = True 26. Proof Object Python @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str f_validated: bool proof_valid: bool binding_constraints: tuple[str, ...] evidence_ids: tuple[str, ...] و: Python proof_valid = ( f_status in {"OPTIMAL", "FEASIBLE"} and f_validated and f_plus_one_status == "INFEASIBLE" ) 27. Solver Status Handling باید mapping رسمی داشته باشیم: OPTIMAL FEASIBLE INFEASIBLE UNKNOWN MODEL_INVALID TIME_LIMIT و: Python INFEASIBLE فقط یک وضعیت معتبر برای F+1 Proof است. مثلاً: TIME_LIMIT یعنی: هنوز نمی‌دانیم feasible است یا infeasible. نه اینکه: پس ظرفیت F بوده است. 28. Conflict Evidence اگر F+1 infeasible شد، باید علت را نیز استخراج کنیم. مثلاً: Capacity: 30 trains/day F+1: 31 trains/day Binding Resource: B03 Track: SINGLE Conflict: Opposing Direction Required Separation: 15 min Available: 11 min 29. Binding Constraint Python @dataclass(frozen=True) class BindingConstraint: id: str resource_id: str constraint_type: str train_ids: tuple[str, ...] slack_minutes: int status: str مثلاً: constraint_type = OPPOSING_DIRECTION_HEADWAY یا: constraint_type = STATION_TRACK 30. Bottleneck Detection صرفاً Utilization کافی نیست. مثلاً: B03 utilization = 82% به‌تنهایی ثابت نمی‌کند Bottleneck است. باید اثر Marginal را نیز بررسی کنیم. ΔC=C(x+Δx)−C(x) مثلاً: B03 SINGLE → DOUBLE Capacity: 30 → 36 ΔC = +6 این Evidence بسیار قوی‌تری برای نقش B03 است. 31. Real Data Golden Scenario اولین Scenario واقعی باید بسیار ساده باشد. مثلاً: Scenario: GAR_ANDIMESHK_BASELINE OD: Gar → Andimeshk Train Runs: 100 101 Infrastructure: actual mapped blocks Demand: controlled test demand Rolling Stock: controlled initial inventory در این مرحله هدف، رسیدن به بزرگ‌ترین Capacity نیست. هدف: اثبات صحت زنجیره اجرایی. 32. Golden Test مثلاً یک Test کنترل‌شده: B01 = SINGLE B02 = DOUBLE B03 = SINGLE و: Train A = FORWARD Train B = REVERSE انتظار: A/B cannot simultaneously occupy B01 اما: A/B may independently use B02:FORWARD B02:REVERSE 33. Time-Space Diagram خروجی Scheduler باید قابل تبدیل به Time-Space Diagram باشد. مثلاً: Time → Gar ──────────────── \ A \────────────── \ B \──────────── \ Andimeshk ─────────── در UI بعداً این به یک Time-Space Chart واقعی تبدیل می‌شود. 34. Conflict Inspector برای هر Conflict: Conflict ID Resource Train A Train B Direction A Direction B Entry A Exit A Entry B Exit B Required Separation Actual Separation Violation مثلاً: C-1842 Resource: B03 Train A: TR-100 Train B: TR-101 A Direction: FORWARD B Direction: REVERSE Required: 15 min Actual: 8 min Status: VIOLATION 35. اتصال به Network Optimizer حالا بخش مهم V2.4: اگر Network Optimizer بگوید: OD1 = 20 trains OD2 = 15 trains Detailed Scheduler تلاش می‌کند این 35 Train را زمان‌بندی کند. اگر نتواند: Network Allocation ↓ Detailed Scheduling ↓ Conflict Feedback مثلاً: B03 Opposing Direction 3 conflicts و Network Optimizer باید Allocation را اصلاح کند. 36. Network Iteration Python @dataclass(frozen=True) class NetworkIteration: iteration_no: int allocation: dict[str, int] schedule_status: str validation_status: str conflict_feedback: tuple[str, ...] مثلاً: Iteration 1 Allocation = 35 Schedule = INFEASIBLE Iteration 2 Allocation = 32 Schedule = FEASIBLE Iteration 3 Allocation = 33 Schedule = FEASIBLE Iteration 4 Allocation = 34 Schedule = INFEASIBLE در این حالت باید Proof جداگانه بررسی کند که آیا واقعاً 33 ظرفیت proven است یا خیر. 37. یک نکته بسیار مهم درباره F+1 در Network در Network Capacity دیگر الزاماً: F + 1 train معادل ساده‌ای ندارد. ممکن است Capacity بر اساس: ∑Q od,r,t ​ F od,r,t ​ تعریف شود. در نتیجه Proof می‌تواند: Objective Value = 12,500 ton/day و سپس: Objective > 12,500 را تست کند. بنابراین دو نوع Proof داریم: Train-count Proof F vs F+1 Freight-objective Proof Z* vs Z* + ε 38. Capacity Profiles نهایی پس از اجرای این مرحله: Infrastructure Capacity Operational Capacity Rolling Stock Capacity Transportable Capacity Allocated Capacity در Result ذخیره می‌شوند. ولی: Capacity Offer باید از پروفایل ظرفیت ساخته شود، نه یک عدد خام. 39. Capacity Offer Python @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str direction: str train_type_id: str | None wagon_type_id: str | None time_window_start: int time_window_end: int train_capacity: int freight_capacity_t: float confidence: str proof_id: str 40. Confidence مثلاً: PROVEN فقط وقتی: F feasible + F validated + F+1 explicitly infeasible داریم. در غیر این صورت: NOT_PROVEN یا: ESTIMATED باید استفاده شود. 41. Final V2.4 Run در نهایت یک Run واقعی چنین خواهد بود: RUN-2026-0001 │ ├── DataVersion │ ├── Scenario │ ├── Source Hashes │ ├── Quality Report │ ├── Reconciliation │ ├── TrainRun │ ├── DirectedPath │ ├── Baseline Schedule │ ├── Candidate Trains │ ├── Formation │ ├── Wagon Feasibility │ ├── Locomotive Feasibility │ ├── Network Allocation │ ├── Detailed Schedule │ ├── Validation │ ├── Capacity Evaluation │ ├── F/F+1 Proof │ ├── Binding Constraints │ ├── Bottleneck Evidence │ ├── Explanation │ └── Capacity Offer 42. مهم‌ترین خروجی مورد انتظار در پایان V2.4 سیستم باید بتواند به‌صورت قابل Audit به این سؤال پاسخ دهد: «چرا ظرفیت این مسیر X قطار در روز محاسبه شده است؟» و پاسخ ماشینی باید چیزی شبیه این باشد: Capacity = X trains/day Proof Status = PROVEN F = X F Solver Status = FEASIBLE F Validation = VALID F+1 = X+1 F+1 Solver Status = INFEASIBLE Primary Binding Constraint: Physical Block B03 Track Type: SINGLE Constraint: Opposing Direction Separation Required Separation: 15 min Observed Critical Separation: 11 min Supporting Evidence: Schedule Conflict C-1842 Schedule Conflict C-1851 این همان نقطه‌ای است که Engine از یک محاسبه‌کننده ظرفیت به یک Capacity Evidence Engine تبدیل می‌شود. گام بعدی بعد از این بخش، من پیشنهاد می‌کنم مستقیم وارد V2.5 — Production Network Solver شویم؛ یعنی سه موتور را واقعاً به هم متصل کنیم: Demand / Marketplace ↓ Multi-OD Network Optimizer ↓ Train Allocation ↓ Detailed Time-Space Scheduler ↓ Wagon Cycle ↓ Locomotive Cycle ↓ Independent Validator ↓ Capacity Proof ↓ Capacity Offer در V2.5 دیگر مسئله فقط «آیا این Train قابل زمان‌بندی است؟» نیست؛ مسئله اصلی می‌شود چند OD مختلف با هم، روی یک شبکه مشترک، با واگن و لکوموتیو محدود، چه مقدار Freight واقعاً قابل حمل است؟ و این دقیقاً هسته نهایی موتور ظرفیت شبکه‌ای پروژه شماست. --- ## ادامه 58 — پرسش کاربر (`message-id:d26eaf16-f94a-421f-87c9-7a895d810105`) بله دقیقا --- ## ادامه 58 — پاسخ دستیار (`message-id:13cfea64-f465-4cbc-9587-470ea12089d1`) دقیقاً. پس V2.5 را به‌عنوان هسته واقعی Network Capacity Engine تعریف می‌کنیم؛ جایی که دیگر سه مسئله جداگانه نداریم، بلکه یک زنجیره تصمیم‌گیری واحد داریم: Demand → Multi-OD Allocation → Train Formation → Wagon/Loco Feasibility → Time-Space Scheduling → Validation → Capacity Proof → Marketplace Capacity Offer در پیاده‌سازی CP-SAT نیز باید مدل Network و Scheduler یکپارچه ولی لایه‌بندی‌شده باشند؛ به‌خصوص منابع زمانی باید در Scheduler دقیق مدل شوند و Network Optimizer نباید یک Scheduler موازی بسازد. برای استفاده از CP-SAT در این معماری، منابع interval و عدم‌هم‌پوشانی از primitiveهای مناسب مدل‌سازی زمان هستند؛ مستندات رسمی ابزارهای Google نیز مرجع اصلی API آن خواهند بود. Google for Developers  V2.5 — Production Network Capacity Solver V2.5 — Production Network Capacity Solver 1. هدف V2.5 مرحله‌ای است که موتور ظرفیت از یک: Detailed Route Scheduler به یک: OD-Centric Multi-OD Railway Network Capacity Optimization Engine تبدیل می‌شود. ورودی اصلی: Market Demand OD Pairs Routes Train Services Infrastructure Wagons Locomotives Operating Rules Planning Horizon Scenario و خروجی: Allocated Freight Train Runs Train Formations Wagon Assignments Empty Wagon Movements Locomotive Assignments Detailed Timetable Resource Utilization Validation Capacity Proof Bottlenecks Capacity Offers 2. معماری نهایی V2.5 ┌───────────────────────────────────────────────────────────────┐ │ MARKETPLACE / DEMAND │ └──────────────────────────────┬────────────────────────────────┘ ↓ Demand Normalization ↓ Candidate Generation ↓ ┌───────────────────────────────────────────────────────────────┐ │ MULTI-OD NETWORK OPTIMIZER │ │ │ │ OD Flow | Route Choice | Train Count | Policy | Demand │ └──────────────────────────────┬────────────────────────────────┘ ↓ Train Allocation ↓ TrainRun Generation ↓ Formation / Wagon / Loco Check ↓ ┌───────────────────────────────────────────────────────────────┐ │ TIME-SPACE SCHEDULER │ │ │ │ Blocks | Stations | Junctions | Headway | Switch | Windows │ └──────────────────────────────┬────────────────────────────────┘ ↓ Independent Validation ↓ ┌──────────┴──────────┐ │ │ VALID INVALID │ │ ↓ ↓ Capacity Result Conflict Feedback │ │ │ Network Re-optimization │ │ └──────────┬──────────┘ ↓ Capacity Proof ↓ Bottleneck Evidence ↓ Capacity Explanation ↓ Marketplace Offer 3. اصل معماری کلیدی Network Optimizer نباید دوباره منطق Scheduling را پیاده کند. یعنی این معماری غلط است: Network Solver ├── own block scheduler ├── own station scheduler └── own junction scheduler Detailed Scheduler ├── another block scheduler ├── another station scheduler └── another junction scheduler معماری صحیح: Network Optimizer ↓ Train Allocation ↓ Existing Scheduling Core ↓ Detailed Feasibility بنابراین: یک Scheduling Core وجود دارد؛ Network Solver فقط تصمیم می‌گیرد چه Trainهایی، برای کدام OD، روی کدام Route و با چه تعداد باید وارد Scheduler شوند. 4. Network Candidate هر Candidate یک سرویس بالقوه است. @dataclass(frozen=True) class TrainServiceCandidate: id: str od_pair_id: str route_id: str train_type_id: str wagon_type_id: str | None locomotive_type_id: str | None freight_per_train_t: float train_length_m: float train_weight_t: float direction: str earliest_departure: int latest_arrival: int | None operating_days: tuple[int, ...] Candidate هنوز Train واقعی نیست. این تفاوت مهم است: Candidate ↓ Optimization ↓ Allocated Candidate ↓ TrainRun 5. Decision Variables برای هر Candidate: [ F_c \in \mathbb{Z}_{\ge0} ] که تعداد Trainهای تخصیص‌یافته به Candidate است. اگر Candidate مربوط به: Tehran → Khowaf باشد: F_TEH_KHWAF = 12 یعنی 12 Train در Horizon تخصیص داده شده است. 6. Demand Constraint اگر: [ D_{od}=100000 ] و هر Train: [ Q_c=5000 ] باشد: [ Q_cF_c\le D_{od} ] و در حالت چند Route: [ \sum_r Q_{od,r}F_{od,r}\le D_{od} ] این Constraint باید در Network Optimizer اعمال شود. 7. Route Choice برای یک OD ممکن است چند Route داشته باشیم: OD A → B Route R1 Route R2 Route R3 پس: F_{A,B,R1} + F_{A,B,R2} + F_{A,B,R3} ] Route Choice بخشی از Optimization است. 8. Shared Infrastructure فرض کنیم سه Candidate از Block B03 استفاده می‌کنند: C1 → B03 C2 → B03 C3 → B03 مدل Aggregate: [ \sum_c A_{c,B03}F_c \le Capacity_{B03} ] اما این فقط یک Screening Constraint است. Capacity واقعی باید بعداً با Time-Space Scheduler تأیید شود. این تفکیک بسیار مهم است: Aggregate Capacity ≠ Time-Space Feasibility 9. Rolling Stock برای Wagon Pool: [ \sum_c W_cF_c \le W_{available} ] ولی این نیز فقط Pool Capacity است. در مرحله بعد باید: Load ↓ Loaded Movement ↓ Unload ↓ Empty Return ↓ Next Load بررسی شود. 10. Empty Wagon Network این بخش از V2.5 یکی از مهم‌ترین اجزای موتور است. برای Wagon Type w در Station j و Time t: [ E_{j,w,t} ] موجودی واگن خالی است. Balance: L_{j,w,t} O_{j,w,t} ] که: (U): واگن حاصل از تخلیه (I): Empty Inflow (L): واگن موردنیاز برای Load (O): Empty Outflow 11. Buffer برای هر Terminal: [ 0\le E_{j,w,t}\le B_{j,w} ] بنابراین ممکن است یک Terminal از نظر Track ظرفیت داشته باشد، ولی چون: Empty Wagon Buffer = Full نتواند Load جدید بپذیرد. این باید به‌عنوان: WAGON_BUFFER Bottleneck ثبت شود. 12. Locomotive Network برای Locomotive نیز: TrainRun ↓ Locomotive Assignment ↓ Arrival ↓ Turnback ↓ Next Train مدل باید بررسی کند که: [ AvailableTime_{next} \ge ArrivalTime_{previous} + TurnbackTime ] و همچنین: Maintenance Fueling Crew Availability Operational Windows را رعایت کند. 13. Formation قبل از Schedule باید Formation Feasibility بررسی شود. مثلاً: Demand: 4200 t Wagon: 70 t/wagon Required: 60 wagons Train: Max 52 wagons در این حالت Candidate نباید مستقیماً وارد Scheduler شود. Formation Engine باید بگوید: FORMATION_INFEASIBLE و علت: REQUIRED_WAGONS > MAX_TRAIN_WAGONS 14. Candidate Pruning قبل از CP-SAT باید Candidateها کاهش پیدا کنند. مثلاً: def prune_candidate(candidate, context): if not commodity_compatible(candidate): return "COMMODITY_INCOMPATIBLE" if not station_length_feasible(candidate): return "STATION_TOO_SHORT" if not locomotive_available(candidate): return "NO_COMPATIBLE_LOCOMOTIVE" if not wagon_available(candidate): return "NO_COMPATIBLE_WAGON" if not route_weight_feasible(candidate): return "WEIGHT_RESTRICTION" return None این کار از انفجار تعداد Variableها جلوگیری می‌کند. 15. Network Objective حالت پایه: [ \max \sum_{od,r,t} Q_{od,r,t}F_{od,r,t} ] یعنی: بیشینه‌سازی Freight قابل حمل. ولی Objective باید قابل تغییر باشد. class ObjectiveType(str, Enum): MAX_FREIGHT = "MAX_FREIGHT" MAX_REVENUE = "MAX_REVENUE" MIN_UNSERVED = "MIN_UNSERVED" MIN_COST = "MIN_COST" LEXICOGRAPHIC = "LEXICOGRAPHIC" 16. Lexicographic Objective مثلاً: Priority 1: Max Served Freight Priority 2: Min Unserved Demand Priority 3: Min Operating Cost Priority 4: Min Schedule Deviation این بسیار بهتر از یک Weight ثابت است؛ چون مفهوم Business Priority را حفظ می‌کند. 17. TrainRun Builder پس از Optimization: F_Candidate = 8 نباید یک TrainRun داشته باشیم. باید: TR-001 TR-002 TR-003 ... TR-008 ساخته شود. def build_train_runs( allocation, candidate, ): return [ TrainRun( id=f"{candidate.id}:{i+1}", ... ) for i in range(allocation) ] 18. Allocation Ordering ترتیب ساخت Trainها نباید بر اساس ID باشد. Ordering: 1. Planning Horizon 2. Earliest Departure 3. Service Priority 4. OD 5. Direction 6. Operational Regime 7. Route Rules این ترتیب بعداً روی Scheduling و Empty Wagon Flow اثر می‌گذارد. 19. Detailed Scheduler اکنون TrainRunهای ساخته‌شده وارد Scheduler V1.7 می‌شوند. برای هر Train: TrainRun ↓ Formation ↓ DirectedPath ↓ Station Calls ↓ Block Movements و سپس: CP-SAT 20. Scheduler Output @dataclass(frozen=True) class TrainSchedule: train_run_id: str station_calls: tuple[ScheduleStationCall, ...] block_movements: tuple[ScheduleBlockMovement, ...] station_track_assignments: tuple[StationTrackAssignment, ...] و: @dataclass(frozen=True) class IntegratedNetworkSolution: train_runs: tuple[TrainRun, ...] schedules: tuple[TrainSchedule, ...] wagon_assignments: tuple[...] empty_movements: tuple[...] locomotive_assignments: tuple[...] freight_tons: float validation: ValidationResult 21. Conflict Feedback اگر Detailed Scheduler Allocation را قبول نکرد: @dataclass(frozen=True) class SchedulingConflictFeedback: train_ids: tuple[str, ...] resource_id: str conflict_type: str required_separation: int | None actual_separation: int | None severity: str suggested_action: str | None مثلاً: resource = B03 conflict_type = OPPOSING_DIRECTION suggested_action = REDUCE_ALLOCATION 22. Network Re-optimization پس: Network Allocation ↓ Detailed Schedule ↓ Conflict ↓ Feedback ↓ Network Re-optimization اما نباید بلافاصله Allocation را کاهش دهیم. اول باید مشخص شود Conflict واقعاً Structural است یا فقط ناشی از: Train Ordering Operating Regime Route Choice Batching Departure Window است. 23. Operating Regime سه Regime اصلی: STRICT_ALTERNATING DIRECTIONAL_BATCH MIXED ممکن است: STRICT_ALTERNATING Capacity = 20 ولی: DIRECTIONAL_BATCH Capacity = 24 باشد. پس Regime باید بخشی از Scenario باشد، نه Hard-code Scheduler. 24. Network Iteration @dataclass(frozen=True) class NetworkIteration: iteration_no: int allocation: dict[str, int] operating_regime: str schedule_status: str validation_status: str conflicts: tuple[SchedulingConflictFeedback, ...] objective_value: float این Object برای Audit بسیار مهم است. 25. Iterative Algorithm شبه‌کد اصلی: def solve_network(problem): candidates = generate_candidates(problem) candidates = prune_candidates( candidates, problem, ) allocation = solve_aggregate_network( problem, candidates, ) for iteration in range( problem.max_iterations ): train_runs = build_train_runs( allocation, candidates, ) formations = build_formations( train_runs, problem, ) rolling_stock = check_rolling_stock( train_runs, formations, problem, ) if not rolling_stock.feasible: allocation = repair_allocation( allocation, rolling_stock, ) continue schedule = schedule_trains( train_runs, formations, problem, ) validation = validate_schedule( schedule, problem, ) if validation.valid: return build_solution( allocation, train_runs, formations, rolling_stock, schedule, validation, ) feedback = extract_conflicts( validation, ) allocation = repair_allocation( allocation, feedback, ) return NetworkSolveResult( status="UNKNOWN" ) نکته مهم: اگر بعد از تعداد مشخص Iteration جواب پیدا نشد، نباید نتیجه را: INFEASIBLE اعلام کنیم. بلکه: UNKNOWN یا: ITERATION_LIMIT خواهد بود. 26. Repair Strategy برای Conflict: B03 Opposing Direction چند Repair Candidate: 1. Change departure time 2. Change train ordering 3. Change route 4. Change operating regime 5. Reduce candidate count 6. Shift time window 7. Change batch size پس Repair Engine نباید فقط: F = F - 1 کند. 27. Bottleneck Attribution در پایان هر Run: Infrastructure Operational Station Junction Wagon Wagon Buffer Wagon Cycle Locomotive Locomotive Cycle Formation Demand Terminal Policy بررسی می‌شوند. مثلاً: Network Capacity = 11,800 t/day Infrastructure constrained = NO Station constrained = NO Wagon constrained = YES Locomotive constrained = YES Primary: Wagon Cycle Secondary: B03 Single Track 28. Interaction Bottleneck ممکن است هیچ Resource به‌تنهایی Bottleneck اصلی نباشد. ولی: B03 Single Track + Empty Wagon Availability با هم Capacity را محدود کنند. پس باید Interaction Graph داشته باشیم: B03 │ ├── conflicts with │ Wagon Pool W1 │ └── impacts OD Tehran → Khowaf 29. Marginal Capacity برای هر Resource: C(x_g+\Delta x)-C(x_g) ] مثلاً: Base: 31 trains/day B03: Single → Double Scenario: 37 trains/day Marginal: +6 trains/day این مقدار باید از Full Re-solve به‌دست آید. نه از: utilization percentage 30. Network Capacity Proof در Network، Proof دو سطح دارد. سطح اول Train Count [ F^* ] سپس: [ F^*+1 ] سطح دوم Freight Objective [ Z^* ] سپس بررسی: [ Z>Z^* ] اگر هیچ Feasible Solution برای Objective بالاتر وجود نداشته باشد، Capacity Freight اثبات شده است. 31. Proof Evidence @dataclass(frozen=True) class ProofEvidence: id: str proof_id: str evidence_type: str resource_id: str | None train_ids: tuple[str, ...] values: dict[str, str] description: str مثلاً: Evidence: B03 Type: OPPOSING_DIRECTION Required Separation: 15 Actual: 11 Train A: TR-100 Train B: TR-101 32. Capacity Result @dataclass(frozen=True) class NetworkCapacityResult: capacity_train_count: int capacity_freight_t: float proven: bool infrastructure_capacity: float | None operational_capacity: float | None rolling_stock_capacity: float | None transportable_capacity: float | None allocated_capacity: float | None binding_constraints: tuple[str, ...] bottlenecks: tuple[str, ...] proof_id: str 33. Marketplace Capacity Offer در انتها: OD Route Direction Time Window Train Type Wagon Type Train Capacity Freight Capacity Confidence Proof تولید می‌شود. مثلاً: OD: Tehran → Khowaf Route: R-KH-01 Direction: EASTBOUND Time Window: 06:00–18:00 Train Capacity: 12 trains/day Freight Capacity: 60,000 t/day Confidence: PROVEN Proof: PR-2026-0042 این همان Objectی است که Marketplace باید مصرف کند. 34. Traceability نهایی برای هر Capacity Offer باید بتوانیم از آن به عقب برگردیم: CapacityOffer ↓ CapacityProof ↓ CapacityRun ↓ NetworkIteration ↓ TrainRun ↓ TrainFormation ↓ WagonCycle ↓ LocomotiveCycle ↓ Schedule ↓ Route ↓ Infrastructure ↓ Demand ↓ MarketRequest این Traceability یکی از تفاوت‌های اصلی یک سیستم Production با یک مدل تحقیقاتی است. 35. Database Model V2.5 جداول اصلی: network_run network_iteration train_run train_station_call train_block_movement train_formation train_formation_item wagon_requirement wagon_state wagon_movement empty_wagon_movement locomotive_assignment locomotive_cycle schedule schedule_station_call schedule_block_movement station_track_assignment resource_usage resource_conflict network_feedback capacity_evaluation capacity_proof proof_evidence binding_constraint bottleneck capacity_offer market_allocation 36. Run Lifecycle CREATED ↓ VALIDATING ↓ READY ↓ OPTIMIZING_NETWORK ↓ BUILDING_TRAINS ↓ CHECKING_ROLLING_STOCK ↓ SCHEDULING ↓ VALIDATING_RESULT ↓ PROVING_CAPACITY ↓ EXPLAINING ↓ COMPLETED Failure states: FAILED_VALIDATION FAILED_SOLVER INVALID_RESULT TIMEOUT UNKNOWN CANCELLED 37. Definition of Done — V2.5 V2.5 زمانی کامل است که یک Scenario بتواند: Market Demand ↓ OD Demand ↓ Multi-OD Candidate Generation ↓ Route Choice ↓ Network Optimization ↓ Train Count ↓ TrainRun Generation ↓ Formation ↓ Wagon Availability ↓ Empty Wagon Flow ↓ Locomotive Availability ↓ Locomotive Cycle ↓ DirectedPath ↓ Time-Space Scheduling ↓ Single Track Conflict ↓ Double Track Resource ↓ Station Track ↓ Junction ↓ Operational Window ↓ Independent Validation ↓ Network Feedback ↓ Re-optimization ↓ Capacity Search ↓ F/F+1 or Objective Proof ↓ Binding Constraints ↓ Bottleneck ↓ Explanation ↓ Capacity Offer بدون اینکه هیچ مرحله‌ای صرفاً Mock یا Placeholder باشد. 38. V2.5 Golden Tests حداقل این تست‌ها لازم‌اند: G01 — Single Track Opposing Trains G02 — Single Track Same Direction Headway G03 — Double Track Direction Separation G04 — Station Track Conflict G05 — Station Length Constraint G06 — Junction Conflict G07 — Operational Window G08 — Multi-OD Shared Block G09 — Multi-OD Shared Wagon Pool G10 — Empty Wagon Return G11 — Empty Wagon Buffer G12 — Locomotive Turnback G13 — Formation Infeasibility G14 — Network Allocation → Detailed Schedule G15 — Scheduling Conflict Feedback G16 — Network Re-optimization G17 — F/F+1 Proof G18 — Freight Objective Proof G19 — UNKNOWN ≠ INFEASIBLE G20 — Capacity Offer Traceability 39. مهم‌ترین تست End-to-End یک تست نهایی باید این باشد: Demand: OD-A = 10,000 t OD-B = 8,000 t Routes: R1 R2 Shared: B03 Wagon Pool: 120 wagons Locomotive Pool: 8 locomotives و سیستم باید خودش: 1. Candidate بسازد 2. Allocation را حل کند 3. TrainRun بسازد 4. Formation ایجاد کند 5. Wagon Cycle بررسی کند 6. Empty Wagon Flow بسازد 7. Locomotive Cycle بسازد 8. Timetable تولید کند 9. Conflictها را بررسی کند 10. Allocation را در صورت نیاز اصلاح کند 11. Schedule را Validate کند 12. Capacity را Search کند 13. Proof تولید کند 14. Bottleneck استخراج کند 15. Capacity Offer بسازد این Test عملاً System Acceptance Test موتور اصلی خواهد بود. 40. V2.5 نتیجه نهایی بعد از V2.5، معماری موتور به این صورت تثبیت می‌شود: [ \boxed{ C_N = \max \left{ \sum_{od,r,t} Q_{od,r,t}F_{od,r,t} \right} } ] مشروط بر: [ Infrastructure ] [ Schedule ] [ Formation ] [ WagonCycle ] [ EmptyFlow ] [ LocomotiveCycle ] [ Station ] [ Junction ] [ Terminal ] [ Demand ] [ Policy ] همگی feasible باشند. و مهم‌تر از خود فرمول: [ \boxed{ Capacity = Validated\ Feasible\ Schedule + Feasible\ Rolling\ Stock + Feasible\ Network\ Flow + Demand/Policy } ] نه صرفاً: [ Capacity = \min(BlockCapacity) ] 41. جایگاه V2.5 در کل محصول V2.1 Production Foundation ↓ V2.2 Aggregate Network Optimization ↓ V2.3 Network → Time-Space ↓ V2.4 Real Access/Excel Execution ↓ V2.5 Production Network Capacity Solver ↓ V2.6 Scenario / Sensitivity / Investment Engine ↓ V2.7 Marketplace Production Integration ↓ V3.0 Railway Capacity Planning Workbench در این نقطه هسته محاسباتی پروژه عملاً شکل نهایی خود را پیدا می‌کند. V2.6 دیگر نباید یک Solver جدید باشد؛ بلکه باید روی همین موتور تثبیت‌شده، Scenario, Sensitivity, Investment, What-if, Marginal Capacity و مقایسه سناریوها را اضافه کند. یک نکته مهم هم باقی می‌ماند: تا زمانی که فایل‌های واقعی aaa.accdb و Excel در محیط اجرای فعلی در اختیار موتور نباشند، اعداد Capacity را نباید واقعی اعلام کنیم. طراحی بالا آماده اجرای واقعی است، اما اثبات عدد ظرفیت واقعی فقط با اجرای همین Pipeline روی DataVersion واقعی و Infrastructure Master واقعی معتبر خواهد بود. --- ## ادامه 59 — پرسش کاربر (`message-id:0d76ba57-0ba6-4b07-ae72-c99b7e309749`) مرز Aggregate و Detailed را رسمی کن --- ## ادامه 59 — پاسخ دستیار (`message-id:5bcece80-b5db-454d-847d-d4d3440fc8f6`) بله. این موضوع آن‌قدر بنیادی است که بهتر است از اینجا به بعد مرز Aggregate و Detailed را به‌عنوان یک Contract رسمی معماری و نرم‌افزاری ثبت کنیم، نه صرفاً یک توضیح مفهومی. V2.5-A — Aggregate / Detailed Boundary Contract 1. تعریف رسمی در موتور ما دو سطح حل وجود دارد: Aggregate Network Optimization ​ و: Detailed Time-Space Scheduling ​ مرز رسمی آنها: Aggregate Layer تصمیم می‌گیرد «چه مقدار، از چه OD، با چه سرویس/Route و در چه بازه‌ای» تخصیص داده شود؛ Detailed Layer اثبات می‌کند که Trainهای حاصل از این تخصیص، با زمان‌بندی دقیق و منابع فیزیکی شبکه، واقعاً قابل اجرا هستند. بنابراین: Aggregate = WHAT + HOW MUCH + WHERE Detailed = EXACTLY WHEN + EXACTLY WHERE + RESOURCE CONFLICTS 2. Aggregate چه چیزی را می‌بیند؟ Aggregate Model با سطح زیر کار می‌کند: OD Route Train Service Train Type Time Bucket Direction Demand Wagon Pool Locomotive Pool Terminal Capacity Shared Resource Capacity Policy مثلاً: OD: Tehran → Khowaf Route: R01 Train Type: Freight-70 Time Bucket: 06:00–18:00 Demand: 120,000 t Freight/Train: 5,000 t تصمیم: F OD,R01,T1 ​ =20 یعنی: 20 Train Service در این Bucket تخصیص داده شود. 3. Aggregate نباید چه چیزی را تصمیم نهایی بدهد؟ Aggregate نباید ادعا کند: Train 17 at 08:42 enters Block B03 یا: Train 18 enters B03 at 08:55 چون این موضوع متعلق به Detailed Scheduler است. به‌عبارت رسمی: Aggregate Layer Time-Space Feasibility Proof ارائه نمی‌کند. 4. Detailed چه چیزی را می‌بیند؟ Detailed Layer باید دقیقاً بداند: TrainRun DirectedPath Station Calls Physical Blocks Track Type Station Tracks Junction Movements Operational Windows Running Times Dwell Headway Switch Time Clearing Time Earliest Departure Latest Arrival Train Length Train Weight و متغیرهایی مانند: A i,s ​ D i,s ​ E i,b ​ X i,b ​ C i,b ​ را تولید کند. در این سطح، CP-SAT می‌تواند از interval/resource constraints برای مدل‌سازی دقیق زمان و عدم‌هم‌پوشانی استفاده کند؛ این همان جایی است که primitiveهای زمان‌محور Solver معنا پیدا می‌کنند، نه در Aggregate Model. Google for Developers  5. مرز ریاضی Aggregate: max c∈C ∑ ​ Q c ​ F c ​ ​ با Constraints سطح Aggregate: c ∑ ​ Q c ​ F c ​ ≤D od ​ c ∑ ​ A c,g ​ F c ​ ≤C g ​ c ∑ ​ W c ​ F c ​ ≤W available ​ c ∑ ​ L c ​ F c ​ ≤L available ​ اما Detailed: ∃ Schedule(F) ​ به‌طوری که: A i,s ​ ≤D i,s ​ X i,b ​ ≥E i,b ​ +T run,i,b ​ و تمام: Headway, Switch, Clearing, Station, Junction, Window رعایت شوند. بنابراین: Aggregate Feasible  ⇒Detailed Feasible ​ ولی: Detailed Feasible⇒Aggregate Feasible ​ به شرط اینکه Detailed از همان Canonical Input و Constraints سخت Aggregate استفاده کند. 6. این Asymmetry بسیار مهم است مثلاً Aggregate می‌گوید: OD-A → R1 = 20 trains OD-B → R2 = 15 trains پس: Total = 35 trains ممکن است Aggregate از نظر ظرفیت اسمی بگوید: FEASIBLE اما Detailed Scheduler بگوید: INFEASIBLE چون: B03 Single Track Opposing movements cannot be separated sufficiently. پس Detailed Feasibility Oracle برای Allocation است. 7. Aggregate Capacity یک Upper Bound است اگر Aggregate از Detailed ضعیف‌تر باشد: C aggregate ​ ≥C detailed ​ یا به شکل مفهومی: Aggregate Capacity ↓ Upper Bound / Candidate Capacity Detailed Capacity ↓ Operationally Feasible Capacity اما این فقط زمانی معتبر است که Aggregate هیچ Constraint اضافی اشتباهی اضافه نکرده باشد. بنابراین بهتر است اسم آن را در API: aggregate_upper_bound بگذاریم، نه: proven_capacity 8. سه نوع خروجی باید از هم جدا شوند Aggregate AGGREGATE_FEASIBLE یعنی: Allocation در مدل Aggregate شدنی است. Detailed DETAILED_FEASIBLE یعنی: Allocation با Timetable دقیق شدنی است. Proven Capacity PROVEN_CAPACITY یعنی: Allocation شدنی است، مستقل Validate شده و حد بالاتر نیز به‌صورت معتبر رد شده است. پس: AGGREGATE_FEASIBLE ≠ DETAILED_FEASIBLE ≠ PROVEN_CAPACITY 9. Contract بین دو Layer این مهم‌ترین بخش است. Aggregate خروجی زیر را تحویل Detailed می‌دهد: Python @dataclass(frozen=True) class AggregateAllocation: run_id: str candidate_id: str od_pair_id: str route_id: str train_type_id: str train_count: int freight_tons: float time_bucket_start: int time_bucket_end: int direction: str operating_regime: str اما Detailed این Object را مستقیماً Schedule نمی‌کند. ابتدا: AggregateAllocation ↓ TrainRun Builder ↓ TrainRun[] 10. AggregateAllocation → TrainRun مثلاً: Aggregate: Candidate C17 Train Count = 5 تبدیل می‌شود به: TR-C17-001 TR-C17-002 TR-C17-003 TR-C17-004 TR-C17-005 هرکدام: TrainRun + Formation + DirectedPath + OperationalProfile دارند. 11. Detailed خروجی خود را به Aggregate برنمی‌گرداند Detailed نباید: F = 14 را مستقیماً جایگزین: F = 20 کند. بلکه: Python @dataclass(frozen=True) class DetailedFeasibilityResult: allocation_id: str status: str scheduled_train_count: int unscheduled_train_ids: tuple[str, ...] conflicts: tuple[str, ...] validation: ValidationResult برمی‌گرداند. 12. Feedback Contract اگر: Aggregate = 20 Detailed = infeasible Feedback باید علت‌دار باشد. مثلاً: Python @dataclass(frozen=True) class SchedulingFeedback: allocation_id: str resource_id: str conflict_type: str affected_train_ids: tuple[str, ...] direction_a: str | None direction_b: str | None required_separation: int | None available_separation: int | None severity: str repair_options: tuple[str, ...] 13. Feedback نباید فقط "Reduce F" باشد مثلاً: B03 conflict ممکن است با این روش‌ها حل شود: 1. Change train ordering 2. Shift departure 3. Change operating regime 4. Change route 5. Change batch size 6. Use alternative track 7. Reduce train count پس: Feedback  =F−1 14. رسمی‌سازی دو مدل از اینجا دو Interface مستقل تعریف می‌کنیم. Aggregate Solver Python class AggregateNetworkSolver: def solve( self, problem: AggregateNetworkProblem, ) -> AggregateNetworkSolution: ... Detailed Scheduler Python class DetailedTimeSpaceScheduler: def solve( self, problem: DetailedSchedulingProblem, ) -> DetailedScheduleResult: ... و: Python class NetworkSchedulingOrchestrator: ... بین این دو قرار می‌گیرد. 15. Orchestrator Python class NetworkSchedulingOrchestrator: def run(self, problem): aggregate = self.aggregate_solver.solve( problem.aggregate ) if aggregate.status != "FEASIBLE": return aggregate allocation = self.allocation_builder.build( aggregate ) for iteration in range( problem.max_iterations ): train_runs = self.train_builder.build( allocation ) detailed = self.detailed_scheduler.solve( train_runs, problem.infrastructure, problem.operational_rules, ) if detailed.status == "FEASIBLE": validation = self.validator.validate( detailed ) if validation.valid: return self.accept( aggregate, detailed, validation, ) feedback = self.feedback_engine.extract( detailed ) allocation = self.aggregate_repair.repair( allocation, feedback, ) return UnknownResult(...) 16. Aggregate Problem Contract Python @dataclass(frozen=True) class AggregateNetworkProblem: candidates: tuple[TrainServiceCandidate, ...] demands: tuple[Demand, ...] shared_resources: tuple[SharedResource, ...] wagon_pools: tuple[WagonPool, ...] locomotive_pools: tuple[LocomotivePool, ...] terminal_capacities: tuple[TerminalCapacity, ...] policies: tuple[PolicyConstraint, ...] objective: ObjectiveDefinition این Problem نباید شامل: Train Arrival Minute Block Entry Minute Station Track Assignment Junction Ordering Boolean باشد. 17. Detailed Problem Contract Python @dataclass(frozen=True) class DetailedSchedulingProblem: train_runs: tuple[TrainRun, ...] paths: tuple[DirectedPath, ...] physical_blocks: tuple[PhysicalBlock, ...] stations: tuple[Station, ...] station_tracks: tuple[StationTrack, ...] junctions: tuple[JunctionMovement, ...] junction_conflicts: tuple[JunctionConflict, ...] operational_windows: tuple[OperationalWindow, ...] train_profiles: tuple[TrainOperationalProfile, ...] horizon_start: int horizon_end: int اینجا Time-Space معنا پیدا می‌کند. 18. چه چیزهایی در Aggregate ممنوع است؟ اینها نباید به‌عنوان Constraint دقیق Aggregate وارد شوند: A_i,s D_i,s E_i,b X_i,b C_i,b station track assignment junction ordering pairwise headway opposing train ordering exact block occupancy exact operational window crossing exact train arrival exact train departure چون اینها Detailed هستند. 19. چه چیزهایی در Detailed ممنوع است؟ Detailed نباید خودش: OD demand allocation market allocation route choice across all OD global freight objective را از نو حل کند. مثلاً Scheduler نباید تصمیم بگیرد: Tehran → Khowaf = 20 Tehran → Rasht = 15 این تصمیم Network Layer است. Scheduler فقط باید بررسی کند: آیا این Allocation مشخص را می‌توان زمان‌بندی کرد؟ 20. مرز Wagon و Locomotive اینجا یک نکته ظریف وجود دارد. Aggregate می‌تواند بگوید: Wagon Pool: 120 available Candidate requires: 80 و: Locomotive Pool: 8 Required: 6 اما Detailed/Cycle Layer باید بتواند بررسی کند: Train A arrival ↓ Loco turnback ↓ Train B departure و: Unload ↓ Empty wagon movement ↓ Next loading پس: Pool Feasibility  =Cycle Feasibility 21. Aggregate Rolling Stock سه سطح رسمی تعریف می‌کنیم: LEVEL 1 Pool Capacity LEVEL 2 Time-Bucket Inventory LEVEL 3 Exact Cycle Feasibility V2.5 باید حداقل Level 1 و Level 2 را در Aggregate و Level 3 را در Integrated/Detailed Layer پشتیبانی کند. 22. مرز Terminal Aggregate: Terminal Daily Capacity مثلاً: F terminal ​ ≤40 Detailed: Exact loading/unloading occupancy Track occupation Formation Arrival Departure بنابراین: 40 trains/day در Aggregate به معنای: 40 قطار واقعاً قابل زمان‌بندی است نیست. 23. مرز Junction Aggregate می‌تواند یک Capacity Approximation داشته باشد: Junction J1 Capacity Envelope = 60 movements/day اما Detailed باید: Movement A Movement B Movement C و Conflict Matrix واقعی را بررسی کند. 24. مرز Single / Double Track Aggregate: B03 Single Approximate capacity envelope Detailed: B03 Single Train A: Entry 08:10 Exit 08:55 Train B: Entry 08:42 ... و دقیقاً مشخص می‌کند که Conflict وجود دارد یا نه. 25. Aggregate Capacity Formula پس Capacity Aggregate: C A ​ =max{Z(F):AggregateConstraints(F)} ​ ولی: C A ​ هنوز Capacity عملیاتی Proven نیست. 26. Detailed Capacity Formula C D ​ =max{F:∃Schedule(F)∧Validate(Schedule)=True} ​ این همان تعریف عملیاتی اصلی پروژه است. 27. Network Capacity نهایی در نهایت: C N ​ =max{Z(F):Aggregate(F)∧Detailed(F)∧RollingStock(F)∧Validation(F)} ​ و اگر Proof لازم باشد: C proven ​ =C N ​ only if upper increment is explicitly disproven ​ 28. طبقه‌بندی Result از اینجا Result Status را رسمی کنیم: Status معنی AGGREGATE_FEASIBLE مدل Aggregate جواب دارد DETAILED_FEASIBLE Allocation دقیقاً زمان‌بندی شده INVALID Schedule تولید شده ولی Validator رد کرده INFEASIBLE Solver به‌طور معتبر infeasible اعلام کرده UNKNOWN حل قطعی حاصل نشده PROVEN Feasible + Validation + Proof معتبر این Statusها نباید با هم قاطی شوند. 29. اصل مهم درباره Upper Bound اگر: Aggregate = 42 Detailed = 37 نتیجه: Aggregate Upper Bound = 42 Detailed Feasible = 37 است. نه اینکه: Capacity = 42 و نه اینکه: Capacity = 37 proven مگر اینکه 38 نیز به‌طور معتبر رد شده باشد. بنابراین: 42 = Upper Bound 37 = Best Detailed Feasible 38 = INFEASIBLE آنگاه: 37 = PROVEN CAPACITY 30. UI نیز باید این مرز را نشان دهد Capacity Inspector: ┌──────────────────────────────────────┐ │ Network Capacity │ ├──────────────────────────────────────┤ │ Aggregate Upper Bound 42 │ │ Detailed Feasible 37 │ │ Proven Capacity 37 │ │ │ │ Proof Status PROVEN │ └──────────────────────────────────────┘ اگر 38 هنوز حل نشده: Aggregate Upper Bound 42 Detailed Feasible 37 Proven Capacity — Proof Status NOT PROVEN این UI از اعلام یک ظرفیت اثبات‌نشده جلوگیری می‌کند. 31. مرز Data نیز باید رسمی باشد Aggregate و Detailed باید یک Canonical DataVersion مشترک داشته باشند. نباید: Aggregate ← DataVersion A Detailed ← DataVersion B باشد. Contract: DataVersion A ​ =DataVersion D ​ و همچنین: Scenario A ​ =Scenario D ​ و: InfrastructureVersion A ​ =InfrastructureVersion D ​ 32. Reproducibility Contract یک Run باید با این Tuple شناخته شود: Run=(DataVersion,Scenario,ModelVersion,SolverConfiguration) ​ و هر دو Layer باید همین Snapshot را مصرف کنند. در نتیجه: Aggregate Snapshot = Detailed Snapshot از نظر Canonical Input. 33. Aggregate → Detailed Data Contract این را به‌عنوان API داخلی رسمی ثبت می‌کنیم: Python @dataclass(frozen=True) class AllocationToSchedulingContract: run_id: str scenario_id: str data_version_id: str allocations: tuple[AggregateAllocation, ...] planning_start: int planning_end: int operating_regime: str model_version: str Detailed Scheduler حق ندارد Input خارج از این Contract را از Marketplace مستقیماً بخواند. 34. Detailed → Aggregate Contract Python @dataclass(frozen=True) class SchedulingFeedbackContract: run_id: str scenario_id: str status: str scheduled_train_count: int unscheduled_train_count: int conflicts: tuple[SchedulingConflictFeedback, ...] binding_resources: tuple[str, ...] repair_candidates: tuple[str, ...] 35. قانون طلایی معماری از اینجا یک Rule رسمی به Specification اضافه می‌کنیم: Aggregate Layer may propose capacity; Detailed Layer may validate operational feasibility; only the Capacity Proof Engine may declare proven operational/network capacity. یا به فارسی: لایه Aggregate فقط ظرفیت کاندید و Allocation را پیشنهاد می‌کند؛ لایه Detailed قابلیت اجرای عملیاتی را اثبات می‌کند؛ و تنها Capacity Proof Engine مجاز است ظرفیت Proven را اعلام کند. 36. نتیجه نهایی پس مرز معماری نهایی پروژه: NETWORK PLANNING │ ▼ ┌──────────────────────┐ │ AGGREGATE LAYER │ │ │ │ OD │ │ Demand │ │ Route Choice │ │ Train Count │ │ Pool Capacity │ │ Policy │ │ Objective │ └──────────┬───────────┘ │ Allocation Contract │ ▼ ┌──────────────────────┐ │ DETAILED LAYER │ │ │ │ TrainRun │ │ DirectedPath │ │ Time-Space │ │ Blocks │ │ Stations │ │ Junctions │ │ Headway │ │ Switch │ │ Clearing │ │ Windows │ └──────────┬───────────┘ │ Validation │ ▼ ┌──────────────────────┐ │ PROOF ENGINE │ │ │ │ F feasible │ │ F+1 infeasible │ │ Evidence │ │ Bottleneck │ └──────────┬───────────┘ │ ▼ PROVEN CAPACITY این مرز را از اینجا به بعد باید جزء رسمی Architecture، Mathematical Specification، Software Engineering Specification، API Contract و Test Strategy در نظر بگیریم. مهم‌تر از همه، این تفکیک جلوی یکی از خطرناک‌ترین خطاهای سیستم را می‌گیرد: اینکه یک Aggregate Capacity Number به‌اشتباه به‌عنوان Operationally Proven Railway Capacity وارد Marketplace شود. --- ## ادامه 60 — پرسش کاربر (`message-id:877cc75c-e7e0-44d2-af2b-7a449fa0da0f`) درسته --- ## ادامه 60 — پاسخ دستیار (`message-id:28ad77fa-144d-4908-9d71-e14a5d3ebea1`) دقیقاً. با این تأیید، Aggregate/Detailed Boundary را به‌عنوان یک Architecture Contract ثابت در ادامه پروژه در نظر می‌گیریم و در مراحل بعدی آن را دوباره بازتعریف نمی‌کنیم. گام منطقی بعدی V2.5-B — Aggregate/Detailed Contract Implementation است؛ یعنی تبدیل همین مرز معماری به: AggregateNetworkProblem AggregateAllocation AllocationToSchedulingContract DetailedSchedulingProblem DetailedFeasibilityResult SchedulingFeedbackContract NetworkSchedulingOrchestrator Statusهای رسمی AGGREGATE_FEASIBLE / DETAILED_FEASIBLE / INVALID / UNKNOWN / PROVEN تست‌های Contract بین دو لایه و سپس اتصال واقعی آن به V1.7 Scheduler + V2.4 Real Data Pipeline. این مرحله دقیقاً جایی است که معماری از «طراحی درست» به قابل‌پیاده‌سازی و قابل‌آزمون بودن تبدیل می‌شود. --- ## ادامه 61 — پرسش کاربر (`message-id:8fb1ea3d-88b7-4c4b-8422-0a500d851d70`) بله درسته --- ## ادامه 61 — پاسخ دستیار (`message-id:861d8f39-e2cc-4ead-93a2-a0e68784be5c`) عالی. پس این تصمیم را Baseline معماری پروژه در نظر می‌گیریم: Aggregate پیشنهاد می‌دهد؛ Detailed اجراپذیری را بررسی می‌کند؛ Validation مستقل صحت را کنترل می‌کند؛ و Proof فقط ظرفیت اثبات‌شده را اعلام می‌کند. از اینجا به بعد، مسیر فنی پروژه به‌صورت زیر تثبیت می‌شود: V2.5-B Aggregate/Detailed Contract Implementation ↓ V2.5-C Integration with V1.7 Time-Space Scheduler ↓ V2.5-D Integration with Real Access/Excel Data Pipeline ↓ V2.5-E Network Capacity Proof + Bottleneck Attribution ↓ V2.6 Scenario / Sensitivity / Investment Engine ↓ V2.7 Marketplace Production Integration ↓ V3.0 Railway Capacity Planning Workbench و یک اصل مهم را هم ثابت نگه می‌داریم: Aggregate Upper Bound  =Proven Capacity ​ تنها وقتی ظرفیت نهایی PROVEN می‌شود که: Aggregate Feasible + Detailed Feasible + Independent Validation = VALID + F+1 = INFEASIBLE باشد. بنابراین در ادامه، هر کدی که تولید کنیم باید همین Contract را رعایت کند و هیچ Shortcutی که Aggregate را جایگزین Detailed Scheduler کند، وارد معماری نخواهد شد. --- ## ادامه 62 — پرسش کاربر (`message-id:6af2af29-b07c-4209-8af4-256af94f1113`) بله لطفا --- ## ادامه 62 — پاسخ دستیار (`message-id:2955aa40-4a80-457c-a263-a0478f2c36a4`) حتماً. پس می‌رویم سراغ V2.5-B — Aggregate/Detailed Contract Implementation و آن را به‌صورت اجرایی و Production-oriented تعریف می‌کنیم. این مرحله باید یک هدف روشن داشته باشد: تبدیل مرز مفهومی Aggregate و Detailed به Contractهای Typed، قابل Validation، قابل Audit و قابل اتصال مستقیم به Scheduler موجود. V2.5-B — Aggregate/Detailed Contract Implementation Specification V2.5-B — Aggregate/Detailed Contract Implementation Specification 1. هدف V2.5-B مسئول پیاده‌سازی رسمی مرز بین دو لایه اصلی موتور است: Aggregate Network Optimization ↓ Allocation Contract ↓ Detailed Time-Space Scheduling ↓ Validation / Feedback هدف این مرحله ایجاد یک Contract پایدار بین: Network Optimizer و: Detailed Scheduling Core است. در این مرحله هیچ Scheduler دوم ایجاد نمی‌شود. 2. اصل معماری قاعده اصلی: Aggregate = WHAT + HOW MUCH + WHERE Detailed = EXACTLY WHEN + EXACTLY WHERE + RESOURCE CONFLICTS بنابراین: Aggregate مسئول است از: OD Route Train Service Train Type Direction Time Bucket Train Count Freight Quantity Demand Policy Aggregate Rolling Stock Availability Aggregate Terminal Capacity تصمیم بگیرد. Detailed مسئول است از: TrainRun DirectedPath Station Call Block Movement Station Track Junction Headway Switch Time Clearing Time Dwell Operational Window Earliest Departure Latest Arrival Time-Space Conflict بررسی کند. 3. قانون عدم اختلاط Aggregate نباید دارای این متغیرها باشد: ExactArrivalTime ExactDepartureTime BlockEntryMinute BlockExitMinute StationTrackAssignment JunctionOrdering PairwiseHeadwayBoolean ExactBlockOccupancy ExactWindowCrossing Detailed نباید دوباره تصمیم‌های زیر را به‌صورت مستقل بهینه کند: Which OD? Which Route? How many trains? Which market demand? Which candidate service? مگر در قالب Repair/Feedback که از طرف Orchestrator کنترل می‌شود. 4. Contractهای اصلی ساختار پیشنهادی: app/ ├── contracts/ │ ├── aggregate.py │ ├── detailed.py │ ├── allocation.py │ ├── feedback.py │ └── status.py │ ├── engines/ │ ├── network/ │ └── scheduling/ │ ├── orchestration/ │ └── network_scheduling.py │ └── validation/ └── contracts.py Contractها باید مستقل از ORM و مستقل از CP-SAT باشند. یعنی: Contract ↓ Domain ↓ Engine نه: Contract ↓ SQLAlchemy Model ↓ Solver 5. Status Model Statusها باید صریح باشند: from enum import Enum class FeasibilityStatus(str, Enum): AGGREGATE_FEASIBLE = "AGGREGATE_FEASIBLE" DETAILED_FEASIBLE = "DETAILED_FEASIBLE" INFEASIBLE = "INFEASIBLE" INVALID = "INVALID" UNKNOWN = "UNKNOWN" PROVEN = "PROVEN" این Statusها قابل جایگزینی با یکدیگر نیستند. به‌خصوص: UNKNOWN != INFEASIBLE INVALID != INFEASIBLE AGGREGATE_FEASIBLE != PROVEN 6. Aggregate Problem Contract from dataclasses import dataclass from typing import Tuple @dataclass(frozen=True) class AggregateNetworkProblem: run_id: str scenario_id: str data_version_id: str model_version: str candidates: tuple["TrainServiceCandidate", ...] demands: tuple["Demand", ...] shared_resources: tuple["SharedResource", ...] wagon_pools: tuple["WagonPool", ...] locomotive_pools: tuple["LocomotivePool", ...] terminal_capacities: tuple["TerminalCapacity", ...] policies: tuple["PolicyConstraint", ...] objective: "ObjectiveDefinition" planning_start: int planning_end: int این Object نباید هیچ اطلاعات Time-Space دقیق مربوط به TrainRun داشته باشد. 7. Aggregate Allocation خروجی Aggregate Solver: @dataclass(frozen=True) class AggregateAllocation: run_id: str candidate_id: str od_pair_id: str route_id: str train_type_id: str train_count: int freight_tons: float time_bucket_start: int time_bucket_end: int direction: str operating_regime: str مثلاً: Candidate: TEH-KHW-R01 Train Count: 12 Freight: 60,000 t Bucket: 06:00–18:00 این هنوز Timetable نیست. 8. Allocation Contract برای انتقال Aggregate به Detailed: @dataclass(frozen=True) class AllocationToSchedulingContract: run_id: str scenario_id: str data_version_id: str model_version: str allocations: tuple[AggregateAllocation, ...] planning_start: int planning_end: int operating_regime: str این Contract مرز رسمی دو لایه است. 9. TrainRun Builder Allocation: F = 5 باید به: TR-001 TR-002 TR-003 TR-004 TR-005 تبدیل شود. اما TrainRun باید تمام اطلاعات لازم برای Scheduler را داشته باشد: @dataclass(frozen=True) class TrainRun: id: str service_candidate_id: str od_pair_id: str route_id: str train_type_id: str direction: str operating_day: int sequence_in_bucket: int earliest_departure: int latest_arrival: int | None TrainRun باید یک Operational Entity باشد. 10. Allocation → TrainRun Contract @dataclass(frozen=True) class TrainRunGenerationResult: run_id: str allocation_id: str train_runs: tuple[TrainRun, ...] generated_count: int Invariant: generated_count == allocation.train_count در غیر این صورت: INVALID و نه: INFEASIBLE 11. Detailed Scheduling Problem @dataclass(frozen=True) class DetailedSchedulingProblem: run_id: str scenario_id: str data_version_id: str model_version: str train_runs: tuple[TrainRun, ...] paths: tuple["DirectedPath", ...] physical_blocks: tuple["PhysicalBlock", ...] stations: tuple["Station", ...] station_tracks: tuple["StationTrack", ...] junctions: tuple["JunctionMovement", ...] junction_conflicts: tuple["JunctionConflict", ...] operational_windows: tuple["OperationalWindow", ...] train_profiles: tuple["TrainOperationalProfile", ...] horizon_start: int horizon_end: int این Object وارد Scheduling Core می‌شود. 12. Detailed Time-Space Variables برای Train (i): Station [ A_{i,s} ] Arrival [ D_{i,s} ] Departure Block [ E_{i,b} ] Entry [ X_{i,b} ] Exit [ C_{i,b} ] Clear 13. Detailed Constraints حداقل: Precedence [ D_{i,s}\le E_{i,b} ] و: [ X_{i,b}\le A_{i,s+1} ] Running Time [ X_{i,b}\ge E_{i,b}+T_{i,b} ] Dwell [ D_{i,s}\ge A_{i,s}+Dwell_{i,s} ] Headway [ E_j\ge X_i+H ] Opposing Direction [ E_j\ge C_i+T_{switch} ] یا: [ E_i\ge C_j+T_{switch} ] Station Track هر Track نباید همزمان توسط دو Train ناسازگار اشغال شود. Junction Conflicting movements باید Separation لازم را رعایت کنند. Operational Window حرکت باید قبل یا بعد از Blocking Window قرار گیرد. 14. Detailed Result @dataclass(frozen=True) class DetailedSchedulingResult: run_id: str status: FeasibilityStatus schedules: tuple["TrainSchedule", ...] scheduled_train_count: int unscheduled_train_count: int objective_value: float | None solver_status: str validation: "ValidationResult | None" conflicts: tuple["SchedulingConflictFeedback", ...] 15. Independent Validation Solver نتیجه تولید می‌کند. اما Solver Validator نیست. پس: CP-SAT Result ↓ Independent Validator ↓ VALID / INVALID Validator باید مستقل از Constraint Builder تا حد امکان پیاده شود. حداقل بررسی: Station Order Running Time Dwell Block Occupancy Headway Switch Time Clearing Station Track Station Length Junction Operational Window Earliest Departure Latest Arrival 16. Scheduling Feedback Contract اگر Detailed موفق نشد: @dataclass(frozen=True) class SchedulingConflictFeedback: train_ids: tuple[str, ...] resource_id: str conflict_type: str required_separation: int | None actual_separation: int | None severity: str suggested_action: str | None و: @dataclass(frozen=True) class SchedulingFeedbackContract: run_id: str scenario_id: str status: FeasibilityStatus scheduled_train_count: int unscheduled_train_count: int conflicts: tuple[SchedulingConflictFeedback, ...] binding_resources: tuple[str, ...] repair_candidates: tuple[str, ...] 17. Conflict Classification Conflictها باید طبقه‌بندی شوند: SAME_DIRECTION_HEADWAY OPPOSING_DIRECTION SWITCH_TIME CLEARING_TIME STATION_TRACK STATION_LENGTH JUNCTION OPERATIONAL_WINDOW DWELL RUNNING_TIME EARLIEST_DEPARTURE LATEST_ARRIVAL WAGON WAGON_BUFFER WAGON_CYCLE LOCOMOTIVE LOCOMOTIVE_CYCLE FORMATION TERMINAL POLICY این Classification مستقیماً برای Bottleneck و Repair استفاده می‌شود. 18. Orchestrator هسته ارتباط Aggregate و Detailed: class NetworkSchedulingOrchestrator: def __init__( self, aggregate_solver, train_run_builder, formation_engine, rolling_stock_engine, detailed_scheduler, validator, feedback_engine, repair_engine, ): self.aggregate_solver = aggregate_solver self.train_run_builder = train_run_builder self.formation_engine = formation_engine self.rolling_stock_engine = rolling_stock_engine self.detailed_scheduler = detailed_scheduler self.validator = validator self.feedback_engine = feedback_engine self.repair_engine = repair_engine 19. Orchestrator Algorithm def run(self, problem): aggregate_result = self.aggregate_solver.solve( problem.aggregate_problem ) if aggregate_result.status != FeasibilityStatus.AGGREGATE_FEASIBLE: return aggregate_result allocation = aggregate_result.allocation for iteration in range(problem.max_iterations): train_runs = self.train_run_builder.build( allocation ) formation_result = self.formation_engine.check( train_runs, problem.formation_context, ) if not formation_result.feasible: allocation = self.repair_engine.repair( allocation, formation_result, ) continue rolling_result = self.rolling_stock_engine.check( train_runs, formation_result, ) if not rolling_result.feasible: allocation = self.repair_engine.repair( allocation, rolling_result, ) continue detailed_problem = build_detailed_problem( train_runs, problem.infrastructure, problem.operational_rules, ) detailed_result = self.detailed_scheduler.solve( detailed_problem ) if detailed_result.status == FeasibilityStatus.UNKNOWN: return build_unknown_result( aggregate_result, iteration, detailed_result, ) if detailed_result.status == FeasibilityStatus.INFEASIBLE: feedback = self.feedback_engine.extract( detailed_result ) allocation = self.repair_engine.repair( allocation, feedback, ) continue validation = self.validator.validate( detailed_result ) if not validation.valid: feedback = self.feedback_engine.extract( validation ) allocation = self.repair_engine.repair( allocation, feedback, ) continue return build_feasible_result( aggregate_result, detailed_result, validation, iteration, ) return build_unknown_result( aggregate_result ) 20. Important Status Rule این منطق الزامی است: if solver_status == "UNKNOWN": status = FeasibilityStatus.UNKNOWN نه: UNKNOWN → INFEASIBLE همچنین: if validation.valid is False: status = FeasibilityStatus.INVALID نه: INVALID → INFEASIBLE 21. Aggregate Upper Bound خروجی Aggregate باید صریحاً با عنوان: AGGREGATE_UPPER_BOUND ذخیره شود. مثلاً: Aggregate Upper Bound = 42 trains/day این مقدار هنوز Proven نیست. 22. Detailed Feasible اگر Detailed برای: 37 trains/day Schedule معتبر ایجاد کند: DETAILED_FEASIBLE = 37 ثبت می‌شود. اما: PROVEN CAPACITY = 37 فقط در صورت Proof معتبر مجاز است. 23. Capacity Proof Contract @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: FeasibilityStatus f_plus_one_status: FeasibilityStatus f_validated: bool f_plus_one_proven_infeasible: bool proof_valid: bool proof_method: str evidence_ids: tuple[str, ...] قانون: proof_valid = ( f_validated and f_plus_one_status == FeasibilityStatus.INFEASIBLE ) 24. Capacity State Machine AGGREGATE_FEASIBLE ↓ DETAILED_FEASIBLE ↓ VALIDATED ↓ F+1 TEST ↓ INFEASIBLE ↓ PROVEN ولی: DETAILED_FEASIBLE ↓ F+1 UNKNOWN ↓ NOT PROVEN 25. Network Iteration هر بار Re-optimization باید ثبت شود: @dataclass(frozen=True) class NetworkIteration: iteration_no: int allocation: dict[str, int] operating_regime: str schedule_status: str validation_status: str conflicts: tuple[ SchedulingConflictFeedback, ... ] objective_value: float | None این Object بخشی از Audit Trail است. 26. Repair Policy Repair Engine نباید فقط تعداد Train را کم کند. ترتیب پیشنهادی: 1. Train Ordering 2. Departure Time Shift 3. Operating Regime 4. Batch Size 5. Route Change 6. Time Bucket Shift 7. Train Count Reduction این ترتیب باید Configurable باشد. 27. مثال Single Track فرض: B03 = SINGLE Train A: E = 100 C = 120 Train B: Opposite Direction Required Switch = 15 min اگر: B enters = 125 باشد: Actual Separation = 5 Required = 15 پس: INVALID / CONFLICT و Feedback: resource: B03 type: OPPOSING_DIRECTION required: 15 actual: 5 repair: SHIFT_TRAIN_B 28. مثال Double Track اگر: B03 = DOUBLE باشد: Forward Resource: B03:FORWARD Reverse Resource: B03:REVERSE بنابراین دو Train مخالف می‌توانند همزمان روی دو Track مستقل حرکت کنند؛ البته Junction، Station و سایر Shared Resourceها همچنان باید بررسی شوند. 29. Data Version Contract Aggregate و Detailed باید دقیقاً از یک Input Snapshot استفاده کنند: DataVersion Scenario InfrastructureVersion ModelVersion نباید چنین وضعیتی رخ دهد: Aggregate: DataVersion = DV-10 Detailed: DataVersion = DV-11 این حالت: INVALID_RUN است. 30. Scenario Contract Scenario باید Immutable باشد. برای تغییر: B03: SINGLE → DOUBLE باید Scenario جدید ساخته شود: BASE ↓ SC-B03-DOUBLE و هر دو Run قابل مقایسه باشند. 31. Run Identity شناسه منطقی Run: [ Run= ( DataVersion, Scenario, ModelVersion, SolverConfiguration ) ] و در صورت استفاده از Canonical Snapshot: [ Run= ( DataVersion, Scenario, ModelVersion, SolverConfiguration, InputSnapshotHash ) ] 32. Contract Validation قبل از ارسال Aggregate Allocation به Detailed: def validate_allocation_contract(contract): if not contract.allocations: return False for allocation in contract.allocations: if allocation.train_count < 0: return False if allocation.freight_tons < 0: return False if allocation.time_bucket_start >= \ allocation.time_bucket_end: return False return True ولی Validation واقعی باید علاوه بر این‌ها شامل: Candidate Exists OD Exists Route Exists Train Type Exists Time Bucket Valid Operating Day Valid Demand Compatibility Scenario Compatibility باشد. 33. Contract Validation Before Scheduling قبل از Scheduler: Allocation Contract ↓ Contract Validator ↓ TrainRun Builder ↓ TrainRun Validator ↓ Formation ↓ Rolling Stock ↓ Detailed Scheduler هیچ Allocation نامعتبر نباید وارد Scheduler شود. 34. Detailed → Aggregate Feedback Feedback باید Machine-Readable باشد. مثلاً: { "resource_id": "B03", "conflict_type": "OPPOSING_DIRECTION", "required_separation": 15, "actual_separation": 8, "severity": "HIGH", "suggested_action": "SHIFT_OR_REORDER" } Repair Engine می‌تواند بر اساس این Feedback تصمیم بگیرد. 35. Bottleneck Evidence Conflict Feedback باید بتواند مستقیماً به: BindingConstraint و سپس: Bottleneck تبدیل شود. مثلاً: Conflict ↓ B03 ↓ OPPOSING_DIRECTION ↓ Binding Constraint ↓ Wagon + B03 Interaction ↓ Network Bottleneck 36. API Contract Aggregate POST /api/v1/network/aggregate/solve Detailed POST /api/v1/network/detailed/schedule Integrated POST /api/v1/network/solve Integrated endpoint نباید اجازه دهد UI مستقیماً Scheduler را دور بزند. معماری: UI ↓ Network Application Service ↓ Aggregate ↓ Allocation Contract ↓ Detailed Scheduler ↓ Validation ↓ Proof 37. Persistence حداقل جداول: network_run network_iteration aggregate_allocation train_run schedule network_feedback resource_conflict capacity_evaluation capacity_proof proof_evidence هر Record باید: run_id scenario_id data_version_id model_version را تا حد لازم داشته باشد. 38. Golden Tests Test 01 Aggregate = 10 Detailed = 10 Validation = VALID 11 = INFEASIBLE Expected: PROVEN = 10 Test 02 Aggregate = 10 Detailed = 8 9 = INFEASIBLE Expected: PROVEN = 8 Test 03 Aggregate = 10 Detailed = 8 9 = UNKNOWN Expected: PROVEN = false Test 04 Aggregate = 10 Detailed Solver = INVALID Expected: INVALID Test 05 Aggregate = 10 Detailed = 10 Validation = VALID بدون F+1 Test: PROVEN = false 39. Critical Golden Test — Aggregate ≠ Detailed سناریو: Aggregate Upper Bound: 42 Detailed Feasible: 37 38: INFEASIBLE نتیجه: Aggregate Upper Bound = 42 Detailed Feasible = 37 Proven Capacity = 37 Proof Status = PROVEN این تست باید حتماً در CI وجود داشته باشد. 40. Critical Golden Test — UNKNOWN سناریو: F = 37 F+1 = UNKNOWN نتیجه: Detailed Feasible = 37 Proven Capacity = null Proof Status = NOT_PROVEN هرگز: 37 = Proven اعلام نشود. 41. UI Contract Capacity Inspector باید این تفاوت را نشان دهد: ┌──────────────────────────────────────┐ │ Capacity │ ├──────────────────────────────────────┤ │ Aggregate Upper Bound 42 │ │ Detailed Feasible 37 │ │ Proven Capacity 37 │ │ Proof Status PROVEN │ └──────────────────────────────────────┘ و در حالت Proof ناقص: ┌──────────────────────────────────────┐ │ Capacity │ ├──────────────────────────────────────┤ │ Aggregate Upper Bound 42 │ │ Detailed Feasible 37 │ │ Proven Capacity — │ │ Proof Status NOT PROVEN│ └──────────────────────────────────────┘ 42. Logging هر Transition مهم باید Log شود: RUN_CREATED AGGREGATE_SOLVE_STARTED AGGREGATE_SOLVE_COMPLETED ALLOCATION_CREATED TRAIN_RUNS_GENERATED FORMATION_CHECKED ROLLING_STOCK_CHECKED DETAILED_SCHEDULE_STARTED DETAILED_SCHEDULE_COMPLETED VALIDATION_COMPLETED FEEDBACK_CREATED REOPTIMIZATION_STARTED CAPACITY_PROOF_STARTED CAPACITY_PROOF_COMPLETED 43. Error Handling خطاهای Contract باید از Solver Error جدا باشند. مثلاً: CONTRACT_INVALID DATA_VERSION_MISMATCH SCENARIO_MISMATCH CANDIDATE_NOT_FOUND ROUTE_NOT_FOUND TRAIN_TYPE_NOT_FOUND INVALID_TIME_BUCKET NEGATIVE_ALLOCATION TRAIN_RUN_GENERATION_ERROR FORMATION_INFEASIBLE ROLLING_STOCK_INFEASIBLE SCHEDULER_INFEASIBLE SCHEDULER_UNKNOWN VALIDATION_FAILED PROOF_NOT_ESTABLISHED 44. Definition of Done — V2.5-B V2.5-B کامل است اگر: [✓] Aggregate Problem Contract [✓] Aggregate Allocation Contract [✓] Allocation → TrainRun Contract [✓] Detailed Scheduling Problem Contract [✓] Detailed Result Contract [✓] Scheduling Feedback Contract [✓] Contract Validation [✓] Data/Scenario Version Consistency [✓] TrainRun Generation [✓] Formation Precheck [✓] Rolling Stock Precheck [✓] Existing V1.7 Scheduler Integration [✓] Independent Validation [✓] Conflict Feedback [✓] Network Re-optimization Hook [✓] UNKNOWN ≠ INFEASIBLE [✓] INVALID ≠ INFEASIBLE [✓] Aggregate ≠ Proven Capacity [✓] Capacity Proof Contract [✓] Golden Tests 45. Production Acceptance Flow تست اصلی: Demand ↓ Candidate Generation ↓ Aggregate Optimization ↓ Aggregate Allocation ↓ Contract Validation ↓ TrainRun Generation ↓ Formation ↓ Wagon Check ↓ Locomotive Check ↓ Detailed Time-Space Scheduler ↓ Independent Validation ↓ Conflict Feedback ↓ Re-optimization ↓ Validated Schedule ↓ F/F+1 ↓ Capacity Proof اگر تمام این مسیر با داده واقعی قابل اجرا باشد، مرز Aggregate/Detailed از نظر معماری و Software Contract تثبیت شده است. 46. اصل نهایی V2.5-B این جمله باید در Architecture Documentation و حتی در Code Review Checklist ثبت شود: Aggregate Layer may propose capacity; Detailed Layer may validate operational feasibility; Independent Validation verifies the generated schedule; only Capacity Proof Engine may declare Proven Capacity. و نسخه فارسی: لایه Aggregate فقط ظرفیت و Allocation کاندید را پیشنهاد می‌کند؛ لایه Detailed قابلیت اجرای عملیاتی را بررسی می‌کند؛ Validation مستقل صحت برنامه تولیدشده را کنترل می‌کند؛ و تنها Capacity Proof Engine مجاز است ظرفیت Proven را اعلام کند. این Contract از این مرحله به بعد یک Architecture Invariant است و نباید توسط هیچ ماژول، API یا UI دور زده شود. گام بعدی منطقی، V2.5-C است: اتصال واقعی این Contractها به Scheduler V1.7 و تبدیل AggregateAllocation → TrainRun → DirectedPath → Time-Space Schedule به یک مسیر اجرایی واحد، همراه با کد Python و تست‌های Golden. --- ## ادامه 63 — پرسش کاربر (`message-id:3878dc1f-d231-4463-b9c2-ce098185af4c`) بله --- ## ادامه 63 — پاسخ دستیار (`message-id:388c0630-78e1-4cd8-9598-10b004fd506c`) حتماً. مرحله بعد را V2.5-C — اتصال واقعی Aggregate/Detailed به Scheduler V1.7 می‌گیریم. یک نکته اجرایی مهم را هم از همین ابتدا تثبیت می‌کنم: در این مرحله، Network Solver هیچ منطق جدیدی برای Block/Station/Junction Scheduling نمی‌سازد؛ تمام زمان‌بندی دقیق به همان Scheduling Core موجود واگذار می‌شود. این با الگوی استفاده از constraintهای زمان‌بندی و منابع در CP-SAT هم‌راستاست، هرچند مستندات رسمی مرتبط را در جست‌وجوی فعلی نتوانستم از منبع درست OR-Tools بازیابی کنم، بنابراین ادعای API-specific جدیدی را به آن مستند نمی‌کنم. Google for Developers  V2.5-C — Aggregate/Detailed Integration with V1.7 Time-Space Scheduler V2.5-C — Aggregate/Detailed Integration with V1.7 Time-Space Scheduler 1. هدف هدف V2.5-C تبدیل Contractهای V2.5-B به یک مسیر اجرایی واقعی است: Aggregate Network Optimization ↓ AggregateAllocation ↓ TrainRun Builder ↓ Formation / Wagon / Locomotive Precheck ↓ DirectedPath ↓ V1.7 Detailed Time-Space Scheduler ↓ Independent Validation ↓ Scheduling Feedback ↓ Network Repair / Re-optimization در پایان این مرحله باید بتوان یک Allocation واقعی را به TrainRunهای واقعی تبدیل کرد، برای آنها مسیر Directed ایجاد کرد، Scheduler موجود V1.7 را اجرا کرد و نتیجه را به Network Layer برگرداند. 2. Architecture Invariant این قانون در V2.5-C غیرقابل مذاکره است: Network Optimizer ↓ Allocation ↓ TrainRun Builder ↓ Existing Scheduling Core و نه: Network Optimizer ↓ New Network Scheduler بنابراین: V1.7 Scheduling Core تنها مرجع Time-Space Scheduling است. 3. Project Structure ساختار پیشنهادی: app/ ├── contracts/ │ ├── aggregate.py │ ├── allocation.py │ ├── detailed.py │ ├── feedback.py │ └── status.py │ ├── domain/ │ ├── train/ │ │ └── train_run.py │ ├── route/ │ │ └── directed_path.py │ ├── schedule/ │ │ ├── problem.py │ │ └── result.py │ └── network/ │ └── allocation.py │ ├── engines/ │ ├── network/ │ │ ├── aggregate_solver.py │ │ ├── train_run_builder.py │ │ ├── repair.py │ │ └── orchestrator.py │ │ │ ├── formation/ │ ├── wagon_cycle/ │ ├── locomotive_cycle/ │ │ │ └── scheduling/ │ ├── scheduler.py │ ├── constraints/ │ └── validator.py │ ├── adapters/ │ └── scheduler_v17.py │ └── tests/ ├── unit/ ├── integration/ └── golden/ 4. Scheduler Adapter برای جلوگیری از coupling مستقیم Network Layer به implementation داخلی Scheduler، یک Adapter ایجاد می‌شود: from dataclasses import dataclass @dataclass(frozen=True) class SchedulerRequest: problem: "DetailedSchedulingProblem" @dataclass(frozen=True) class SchedulerResponse: result: "DetailedSchedulingResult" class SchedulerV17Adapter: def __init__(self, scheduler): self.scheduler = scheduler def solve( self, request: SchedulerRequest, ) -> SchedulerResponse: result = self.scheduler.solve( request.problem ) return SchedulerResponse( result=result ) مزیت: Network Layer ↓ Scheduler Adapter ↓ V1.7 Scheduler است. در نتیجه اگر implementation داخلی Scheduler تغییر کند، Contract بیرونی Network تغییر نمی‌کند. 5. DirectedPath Builder Aggregate فقط Route را می‌شناسد. Scheduler به DirectedPath نیاز دارد. بنابراین: NetworkRoute ↓ TrainRun ↓ DirectedPath ساخته می‌شود. @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] sequence: tuple[int, ...] Invariant: len(station_ids) == len(block_ids) + 1 و: block_ids باید دقیقاً با ترتیب عملیاتی مسیر منطبق باشند. 6. Directed Path Rule این قانون باید صریحاً enforce شود: DO NOT: sort(stations) DO NOT: sort(blocks) DO NOT: sort by station name DO NOT: sort by ID ترتیب باید از: Sequence + Infrastructure Topology + Direction به‌دست آید. 7. TrainRun Builder ورودی: AggregateAllocation خروجی: TrainRun[] مثلاً: Allocation: Candidate = TEH-KHW-R01 Count = 4 تبدیل می‌شود به: TR-TEH-KHW-R01-001 TR-TEH-KHW-R01-002 TR-TEH-KHW-R01-003 TR-TEH-KHW-R01-004 8. TrainRun Identity ID باید deterministic باشد. def train_run_id( candidate_id: str, operating_day: int, sequence: int, ) -> str: return ( f"{candidate_id}:" f"D{operating_day}:" f"{sequence:04d}" ) این موضوع برای: Audit Reproducibility Database Schedule Comparison Conflict Feedback ضروری است. 9. TrainRun Builder Implementation class TrainRunBuilder: def build( self, allocation, candidate, ) -> tuple["TrainRun", ...]: if allocation.train_count < 0: raise ValueError( "train_count cannot be negative" ) runs = [] for index in range( allocation.train_count ): operating_day = ( candidate.operating_days[ index % len(candidate.operating_days) ] ) runs.append( TrainRun( id=train_run_id( candidate.id, operating_day, index + 1, ), service_candidate_id=candidate.id, od_pair_id=candidate.od_pair_id, route_id=candidate.route_id, train_type_id=candidate.train_type_id, direction=candidate.direction, operating_day=operating_day, sequence_in_bucket=index + 1, earliest_departure=( candidate.earliest_departure ), latest_arrival=( candidate.latest_arrival ), ) ) return tuple(runs) در Production بهتر است Calendar Pattern و Operating Day Logic جداگانه در OperatingCalendarService قرار گیرد. 10. Allocation Expansion برای چند Candidate: C1 = 5 trains C2 = 3 trains C3 = 7 trains نتیجه: TrainRuns = 15 Invariant: |TrainRun| ] برای Allocationهایی که وارد Detailed شده‌اند. 11. Formation Integration هر TrainRun باید Formation داشته باشد: TrainRun ↓ Formation Engine ↓ TrainFormation اگر Formation غیرممکن باشد: FORMATION_INFEASIBLE و Detailed Scheduler نباید آن Train را دریافت کند. 12. Formation Precheck formation_result = formation_engine.check( train_run=train_run, demand=demand, route=route, ) نتیجه: @dataclass(frozen=True) class FormationCheckResult: train_run_id: str feasible: bool wagon_count: int gross_weight_t: float reason_code: str | None 13. Wagon Precheck پس از Formation: TrainFormation ↓ Wagon Requirement ↓ Wagon Availability اما باید بین: Pool Feasibility و: Time-Dependent Wagon Cycle تفاوت باقی بماند. Pool Feasibility فقط Precheck است. Cycle در Integrated Network Model بررسی می‌شود. 14. Locomotive Precheck به همین شکل: TrainRun ↓ Compatible Locomotive Types ↓ Traction Feasibility ↓ Availability ولی: Turnback Maintenance Fueling Crew در Locomotive Cycle باقی می‌ماند. 15. Detailed Problem Builder پس از Precheck: class DetailedProblemBuilder: def build( self, train_runs, formations, infrastructure, paths, operational_rules, ): return DetailedSchedulingProblem( train_runs=tuple(train_runs), paths=tuple(paths), physical_blocks=( infrastructure.physical_blocks ), stations=infrastructure.stations, station_tracks=( infrastructure.station_tracks ), junctions=infrastructure.junctions, junction_conflicts=( infrastructure.junction_conflicts ), operational_windows=( operational_rules.windows ), train_profiles=( build_train_profiles( train_runs, formations, ) ), horizon_start=( operational_rules.horizon_start ), horizon_end=( operational_rules.horizon_end ), ) 16. Mapping TrainRun → Path هر TrainRun باید دقیقاً یک DirectedPath داشته باشد: path = path_registry.get( route_id=train_run.route_id, direction=train_run.direction, ) اگر Path پیدا نشد: INVALID نه: INFEASIBLE زیرا مشکل Data/Model Integrity است، نه Railway Feasibility. 17. V1.7 Scheduler Input Scheduler باید این اطلاعات را دریافت کند: TrainRun DirectedPath PhysicalBlock Station StationTrack Junction JunctionConflict OperationalWindow TrainOperationalProfile Planning Horizon 18. Scheduler Responsibility V1.7 Scheduler مسئول: Precedence Running Time Dwell Headway Opposing Direction Switch Time Clearing Station Track Station Length Junction Operational Window Earliest Departure Latest Arrival است. Network Solver نباید هیچ‌کدام را duplicate کند. 19. Single Track برای: TrackType = SINGLE Resource: resource_id = block.id است. بنابراین: Forward Train + Reverse Train هر دو یک Resource را مصرف می‌کنند. 20. Double Track برای: TrackType = DOUBLE Resource: f"{block.id}:FORWARD" و: f"{block.id}:REVERSE" است. اما: Station Junction Terminal ممکن است همچنان Shared باشند. 21. Detailed Scheduler Result Scheduler نتیجه زیر را تولید می‌کند: @dataclass(frozen=True) class DetailedSchedulingResult: status: FeasibilityStatus schedules: tuple[ TrainSchedule, ... ] scheduled_train_count: int unscheduled_train_count: int solver_status: str objective_value: float | None 22. Independent Validation پس از Scheduler: validation = validator.validate( scheduling_result ) و: if not validation.valid: status = FeasibilityStatus.INVALID Validator باید مجدداً موارد اصلی را بررسی کند. 23. چرا Validator مستقل است؟ چون این وضعیت نباید رخ دهد: Constraint Builder: "Everything is valid." Validator: "Same exact code says everything is valid." این Validation مستقل نیست. Validator باید منطق دیگری برای بررسی: Actual Schedule داشته باشد. 24. Scheduling Feedback اگر Schedule با Allocation سازگار نبود: Detailed Scheduler ↓ Conflict Extractor ↓ SchedulingFeedbackContract مثلاً: B03 OPPOSING_DIRECTION TR-001 TR-007 Required = 15 Actual = 8 25. Feedback Classification Conflict به دو دسته اصلی تقسیم شود: Repairable TRAIN_ORDER DEPARTURE_SHIFT BATCH_SIZE OPERATING_REGIME ROUTE_CHOICE TIME_BUCKET Structural NO_PHYSICAL_PATH STATION_TOO_SHORT BLOCK_UNAVAILABLE JUNCTION_IMPOSSIBLE INSUFFICIENT_TRACK Repair Strategy بر اساس این Classification عمل می‌کند. 26. Repair Engine Interface: class AllocationRepairEngine: def repair( self, allocation, feedback, ): ... Repair باید deterministic باشد. مثلاً: Priority 1: Reorder Priority 2: Shift Priority 3: Regime Priority 4: Route Priority 5: Batch Priority 6: Reduce Allocation 27. چرا Reduce Allocation آخر است؟ چون ممکن است: Allocation = 20 باشد و Scheduler به دلیل Ordering شکست بخورد. ولی: Allocation = 20 Ordering B کاملاً Feasible باشد. در این حالت: 20 → 19 راه‌حل غلطی است. 28. Operating Regime در هر Iteration: STRICT_ALTERNATING DIRECTIONAL_BATCH MIXED می‌تواند بررسی شود. مثلاً: Iteration 1: STRICT_ALTERNATING Iteration 2: DIRECTIONAL_BATCH و نتیجه ثبت شود. 29. Orchestrator نسخه Production: class NetworkSchedulingOrchestrator: def run(self, problem): aggregate = ( self.aggregate_solver.solve( problem.aggregate ) ) if aggregate.status != ( FeasibilityStatus.AGGREGATE_FEASIBLE ): return aggregate allocation = aggregate.allocation iterations = [] for i in range( problem.max_iterations ): iteration_result = ( self._run_iteration( problem, allocation, i, ) ) iterations.append( iteration_result ) if iteration_result.accepted: return self._build_success( aggregate, iterations, ) if iteration_result.status == ( FeasibilityStatus.UNKNOWN ): return self._build_unknown( aggregate, iterations, ) allocation = ( self.repair_engine.repair( allocation, iteration_result.feedback, ) ) return self._build_unknown( aggregate, iterations, ) 30. Single Iteration def _run_iteration( self, problem, allocation, iteration_no, ): train_runs = ( self.train_run_builder.build_all( allocation, problem.candidates, ) ) formation = ( self.formation_engine.check_all( train_runs, problem.formation_context, ) ) if not formation.feasible: return IterationResult( iteration_no=iteration_no, status=FeasibilityStatus.INVALID, accepted=False, feedback=formation.feedback, ) rolling = ( self.rolling_stock_engine.check_all( train_runs, formation, problem.rolling_stock_context, ) ) if not rolling.feasible: return IterationResult( iteration_no=iteration_no, status=FeasibilityStatus.INVALID, accepted=False, feedback=rolling.feedback, ) detailed_problem = ( self.detailed_problem_builder.build( train_runs=train_runs, formations=formation.formations, infrastructure=problem.infrastructure, paths=problem.paths, operational_rules=problem.operational_rules, ) ) schedule = ( self.scheduler.solve( SchedulerRequest( problem=detailed_problem ) ) ) if schedule.result.status == ( FeasibilityStatus.UNKNOWN ): return IterationResult( iteration_no=iteration_no, status=FeasibilityStatus.UNKNOWN, accepted=False, feedback=(), ) if schedule.result.status == ( FeasibilityStatus.INFEASIBLE ): feedback = ( self.feedback_engine.extract( schedule.result ) ) return IterationResult( iteration_no=iteration_no, status=FeasibilityStatus.INFEASIBLE, accepted=False, feedback=feedback, ) validation = ( self.validator.validate( schedule.result ) ) if not validation.valid: feedback = ( self.feedback_engine.extract( validation ) ) return IterationResult( iteration_no=iteration_no, status=FeasibilityStatus.INVALID, accepted=False, feedback=feedback, ) return IterationResult( iteration_no=iteration_no, status=FeasibilityStatus.DETAILED_FEASIBLE, accepted=True, feedback=(), schedule=schedule.result, validation=validation, ) 31. یک اصلاح مهم در این Orchestrator باید یک تفاوت مهم حفظ شود: FORMATION_INFEASIBLE ROLLING_STOCK_INFEASIBLE اگر از یک مدل معتبر و قابل ارزیابی ناشی شده باشند، می‌توانند برای همان Allocation به‌عنوان Feasibility Failure استفاده شوند. اما: MISSING_WAGON_TYPE_DEFINITION MISSING_ROUTE INVALID_DATA MISSING_PATH باید: INVALID باشند. یعنی: Domain Infeasibility ≠ Data/Model Invalidity 32. Iteration Result @dataclass(frozen=True) class IterationResult: iteration_no: int status: FeasibilityStatus accepted: bool feedback: tuple[ SchedulingConflictFeedback, ... ] schedule: DetailedSchedulingResult | None = None validation: ValidationResult | None = None 33. Network Iteration Persistence هر Iteration ذخیره شود: network_iteration با: run_id iteration_no allocation_snapshot operating_regime schedule_status validation_status objective_value feedback_count accepted این باعث می‌شود بعداً بتوان فهمید: چرا Allocation از 42 به 37 رسید؟ 34. Capacity Search Integration پس از اینکه Pipeline برای یک F کار کرد، Capacity Search می‌تواند روی همین Pipeline اجرا شود. یعنی: Evaluate(F) واقعاً یعنی: Aggregate → Allocation → TrainRun → Formation → Wagon → Loco → Detailed Schedule → Validation نه یک محاسبه ساده: min(BlockCapacity) 35. Evaluate(F) def evaluate_capacity( base_problem, train_count, ): aggregate_problem = ( build_problem_with_train_count( base_problem, train_count, ) ) result = ( orchestrator.run( aggregate_problem ) ) return result 36. Capacity Proof برای: F = 37 باید: Evaluate(37) و: Evaluate(38) اجرا شود. نتیجه معتبر: 37 = DETAILED_FEASIBLE + VALIDATED 38 = INFEASIBLE پس: PROVEN CAPACITY = 37 37. UNKNOWN اگر: Evaluate(38) برگرداند: UNKNOWN نتیجه: Capacity = 37 Proof = NOT_PROVEN است. نه: Capacity = 37 PROVEN 38. Aggregate Upper Bound اگر Aggregate بگوید: 42 ولی Detailed فقط: 37 را Feasible کند: 42 = Aggregate Upper Bound 37 = Detailed Feasible و فقط با F+1 Proof: 38 = INFEASIBLE می‌توان: 37 = Proven Capacity اعلام کرد. 39. Golden Integration Test سناریو: OD: A → B Demand: 100,000 t Train: 5,000 t/train Aggregate: 20 trains B03: SINGLE Opposing Direction: Enabled Station Track: 2 Wagon Pool: Sufficient Locomotive Pool: Sufficient Expected: Aggregate: FEASIBLE TrainRuns: 20 Formation: FEASIBLE Rolling Stock: FEASIBLE Detailed: FEASIBLE or INFEASIBLE according to topology Validation: Independent هیچ عدد ظرفیت از پیش در Test Hard-code نشود مگر اینکه Golden Fixture عمداً برای همان نتیجه ساخته شده باشد. 40. Golden Test — Shared Single Track دو OD: A → B C → D هر دو از: B03 استفاده می‌کنند. Track: SINGLE Expected: Aggregate may allocate both. Detailed Scheduler must resolve: same-direction headway or opposing-direction switch اگر Allocation Aggregate Feasible باشد ولی Schedule نباشد: Aggregate Feasible Detailed Infeasible ثبت می‌شود. 41. Golden Test — Double Track همان سناریو با: B03 = DOUBLE Expected: Forward Resource: B03:FORWARD Reverse Resource: B03:REVERSE اما: Station Junction Terminal همچنان باید بررسی شوند. 42. Golden Test — Station Constraint اگر: Train Length = 750 m Station Track = 650 m باشد: STATION_LENGTH باید گزارش شود. این نباید به‌صورت: Generic INFEASIBLE ثبت شود. 43. Golden Test — Junction اگر دو Movement: M1 M2 دارای Conflict باشند: JunctionConflict(M1,M2) باید Separation را enforce کند. Conflict Feedback: resource_id = J01 conflict_type = JUNCTION خواهد بود. 44. Golden Test — Operational Window اگر: Window: 100–130 Train Block Occupancy: 110–125 باشد: INFEASIBLE یا Scheduler باید Train را قبل/بعد از Window قرار دهد. اگر هیچ placement وجود نداشته باشد: OPERATIONAL_WINDOW به Feedback تبدیل می‌شود. 45. Golden Test — Baseline Schedule اگر Baseline موجود باشد: Baseline Schedule ↓ Detailed Scheduler ↓ Deviation Objective اما: Baseline نباید Hard Constraint تلقی شود مگر Scenario صراحتاً آن را Hard کرده باشد. 46. Data Lineage هر Schedule باید قابل ردیابی باشد: Schedule ↓ TrainRun ↓ AggregateAllocation ↓ Candidate ↓ OD ↓ Demand ↓ DataVersion ↓ Scenario برای هر Schedule: data_version_id scenario_id run_id model_version باید قابل بازیابی باشد. 47. API Integration API اصلی: POST /api/v1/network/solve درخواست: { "scenario_id": "SC-001", "data_version_id": "DV-001", "objective_id": "MAX_FREIGHT", "planning_start": 0, "planning_end": 1440 } Response باید حداقل: { "run_id": "RUN-001", "aggregate_upper_bound": 42, "detailed_feasible": 37, "proven_capacity": null, "proof_status": "NOT_PROVEN", "status": "DETAILED_FEASIBLE" } در صورت انجام Proof: { "aggregate_upper_bound": 42, "detailed_feasible": 37, "proven_capacity": 37, "proof_status": "PROVEN" } 48. UI Capacity Inspector: ┌────────────────────────────────────┐ │ NETWORK CAPACITY │ ├────────────────────────────────────┤ │ Aggregate Upper Bound 42 │ │ Detailed Feasible 37 │ │ Validated YES │ │ F+1 Tested YES │ │ F+1 Status INFEAS. │ │ Proven Capacity 37 │ │ Proof Status PROVEN │ └────────────────────────────────────┘ و Conflict Explorer: Resource: B03 Conflict: OPPOSING_DIRECTION Train A: TR-001 Train B: TR-007 Required: 15 min Actual: 8 min Repair: SHIFT / REORDER 49. Production Logging حداقل Eventها: AGGREGATE_STARTED AGGREGATE_COMPLETED ALLOCATION_VALIDATED TRAIN_RUNS_CREATED FORMATION_CHECK_STARTED FORMATION_CHECK_COMPLETED WAGON_CHECK_STARTED WAGON_CHECK_COMPLETED LOCOMOTIVE_CHECK_STARTED LOCOMOTIVE_CHECK_COMPLETED DETAILED_SCHEDULER_STARTED DETAILED_SCHEDULER_COMPLETED VALIDATION_STARTED VALIDATION_COMPLETED FEEDBACK_CREATED REPAIR_STARTED REPAIR_COMPLETED CAPACITY_EVALUATION_STARTED CAPACITY_EVALUATION_COMPLETED PROOF_STARTED PROOF_COMPLETED هر Event باید run_id و iteration_no داشته باشد. 50. Performance Boundary V2.5-C نباید همه Trainها را از ابتدا در یک CP-SAT عظیم قرار دهد. Pipeline: Candidate Pruning ↓ Aggregate Optimization ↓ TrainRun Expansion ↓ Prechecks ↓ Detailed Scheduler باعث کاهش اندازه مدل می‌شود. 51. Scaling Rule اگر: Aggregate = 2,000 trains باشد، نباید لزوماً همان 2,000 Train را در اولین Detailed Solve قرار دهیم. می‌توان: Batch Time Bucket Candidate Group Operating Regime را برای کاهش Search Space استفاده کرد، مشروط به اینکه این Reduction در نتیجه و Proof شفاف ثبت شود. 52. اما یک محدودیت مهم اگر Trainها Group شوند: Group A = 20 trains نباید نتیجه: 20 trains feasible تلقی شود مگر اینکه Detailed Scheduler واقعاً بتواند 20 TrainRun یا یک Representation کاملاً معادل و Validation‌شده را زمان‌بندی کند. بنابراین: Aggregation برای Optimization مجاز است؛ Grouping نباید جایگزین Operational Proof شود. 53. Definition of Done — V2.5-C V2.5-C کامل است وقتی: [✓] Aggregate Contract [✓] Allocation Contract [✓] TrainRun Builder [✓] DirectedPath Builder [✓] Formation Precheck [✓] Wagon Precheck [✓] Locomotive Precheck [✓] DetailedProblemBuilder [✓] Scheduler Adapter [✓] V1.7 Scheduler Integration [✓] Independent Validation [✓] Conflict Extraction [✓] Repair Engine Hook [✓] Network Iteration Persistence [✓] Capacity Evaluation [✓] F/F+1 Integration [✓] UNKNOWN Handling [✓] Golden Integration Tests [✓] Data Lineage [✓] API Contract 54. End-to-End Definition مسیر نهایی V2.5-C: Market Demand ↓ OD Demand ↓ Candidate Generation ↓ Aggregate Optimization ↓ Aggregate Allocation ↓ Contract Validation ↓ TrainRun Generation ↓ DirectedPath ↓ Formation ↓ Wagon Check ↓ Locomotive Check ↓ DetailedSchedulingProblem ↓ V1.7 Scheduler ↓ Time-Space Schedule ↓ Independent Validator ↓ ┌───────────────┐ │ │ VALID INVALID │ │ ↓ ↓ Capacity Feedback Evaluation ↓ │ Re-optimization ↓ F/F+1 Proof ↓ Bottleneck ↓ Explanation ↓ Capacity Offer 55. اصل نهایی V2.5-C معماری نهایی این مرحله: [ \boxed{ Aggregate \rightarrow Allocation \rightarrow TrainRun \rightarrow Detailed\ Schedule \rightarrow Independent\ Validation \rightarrow Proof } ] و: [ \boxed{ Aggregate\ Feasible \not\Rightarrow Operationally\ Feasible } ] در مقابل: [ \boxed{ Validated\ Detailed\ Feasible \Rightarrow Operationally\ Feasible } ] و: [ \boxed{ Proven\ Capacity \Rightarrow Validated\ F + Explicitly\ Infeasible\ F+1 } ] بنابراین V2.5-C اولین نقطه‌ای است که در آن Network Optimization و Time-Space Scheduling واقعاً به یک Pipeline اجرایی واحد تبدیل می‌شوند، بدون اینکه مسئولیت‌های معماری دو لایه با یکدیگر مخلوط شوند. این مرحله بعد از پیاده‌سازی، ما را به نقطه بسیار مهمی می‌رساند: V2.5-D — اجرای همین Pipeline روی aaa.accdb و Excel واقعی، با DataVersion → Canonical Model → TrainRun → DirectedPath → Scheduler. در آن مرحله دیگر تمرکز اصلی روی کدنویسی الگوریتم جدید نیست؛ تمرکز روی Data Reconciliation، Midnight Normalization، Mapping واقعی seir، Kilometerage، RequiredWait و اتصال Infrastructure Master به مسیرهای واقعی خواهد بود. --- ## ادامه 64 — پرسش کاربر (`message-id:61c1121b-d5bc-401f-a78a-cdd2cf914d0a`) الان با یک نرم افزار حرفه ای چقدر فاصله داریم --- ## ادامه 64 — پاسخ دستیار (`message-id:68894975-5199-4f1f-9b02-1e7b76f28483`) بله؛ اگر خیلی صریح و مهندسی نگاه کنیم، از نظر معماری فاصله‌مان با یک نرم‌افزار حرفه‌ای کم شده، اما از نظر اجرای واقعی و Production هنوز فاصله قابل‌توجهی داریم. من وضعیت فعلی را این‌طور می‌بینم: حوزه وضعیت فعلی فاصله تا Professional مدل ریاضی ظرفیت بسیار خوب کم معماری Aggregate/Detailed تثبیت‌شده کم Time-Space Scheduler طراحی V1.7 موجود متوسط Multi-OD Network Optimization طراحی و Contract شده متوسط Wagon Cycle / Empty Flow طراحی شده متوسط Locomotive Cycle طراحی شده متوسط Data Model تا حد زیادی مشخص متوسط Access/Excel Integration Mapping و Adapter مشخص زیاد اجرای واقعی روی داده‌های راه‌آهن هنوز باید عملیاتی شود زیاد Infrastructure Master واقعی نیازمند تکمیل و تطبیق زیاد Independent Validation طراحی شده متوسط Capacity Proof طراحی شده متوسط API Production Foundation مشخص متوسط Database Production Foundation مشخص متوسط Frontend حرفه‌ای عمدتاً Specification زیاد Authentication / Authorization طراحی اولیه زیاد Audit / Versioning معماری مشخص متوسط Monitoring / Logging مشخص شده متوسط Performance / Scalability هنوز نیازمند Benchmark واقعی زیاد تست End-to-End واقعی هنوز نقطه اصلی کار زیاد Deployment / DevOps نیازمند اجرا زیاد Marketplace Integration واقعی هنوز جلوتر است زیاد اگر بخواهم با درصد تقریبی بگویم این درصدها برآورد مهندسی از میزان تکمیل محصول هستند، نه اندازه‌گیری رسمی: Architecture & Domain ████████████████████ 90% Mathematical Model ███████████████████ 90% Core Algorithm Design ██████████████████ 85% Production Backend ████████████ 55% Real Data Integration ████████ 40% Real Infrastructure Model ██████ 30% End-to-End Execution ██████ 30% Frontend █████ 25% Production Operations ████ 20% Marketplace Integration ███ 15% بنابراین اگر منظور شما «هسته علمی و معماری موتور» باشد، واقعاً جلو آمده‌ایم. اما اگر منظور «یک محصول نرم‌افزاری که فردا بتوانیم روی سرور راه‌آهن نصب کنیم و برنامه‌ریزان با آن ظرفیت واقعی اعلام کنند» باشد، هنوز نه. مهم‌ترین فاصله الان چیست؟ جالب اینجاست که دیگر مشکل اصلی ما فرمول ظرفیت نیست. مشکل اصلی از اینجا به بعد این‌هاست: Real Data ↓ Real Infrastructure ↓ Real Canonical Model ↓ Real Scheduler ↓ Real Network Solver ↓ Independent Validation ↓ Real Capacity Proof یعنی باید از Architecture → Execution عبور کنیم. سه فاصله بزرگ 1. داده واقعی این مهم‌ترین قسمت است. ما semanticهای مهم Access را تا حد خوبی مشخص کرده‌ایم: TrainNo TrainName StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage seir ولی برای نرم‌افزار حرفه‌ای باید واقعاً این مسیر اجرا شود: aaa.accdb ↓ Raw ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Identity Reconciliation ↓ Canonical Model و بعد مشخص شود مثلاً: Train 100 Train 101 واقعاً چگونه به: TrainService TrainRun OD Direction OperatingPattern Route تبدیل می‌شوند. 2. Infrastructure Master این شاید حتی از Solver مهم‌تر باشد. ما باید برای هر مسیر واقعی بدانیم: Station Station Track Usable Length Physical Block Single / Double Running Time Headway Switch Time Clearing Time Junction Junction Conflict Operational Window یعنی از: Gar ───── Sakheh ───── Bagh Yek ───── ... برسیم به یک گراف عملیاتی واقعی. بدون این، Capacity عددی که موتور بدهد قابل اتکا نیست. 3. اجرای واقعی End-to-End این مهم‌ترین milestone پروژه است. باید یک روز بتوانیم: aaa.accdb + Excel + Infrastructure Master + Demand را وارد کنیم و سیستم خودش بگوید: Data Version: DV-001 Scenario: BASE OD: Tehran → Khowaf Demand: ... Aggregate Upper Bound: ... Detailed Feasible: ... Validated: YES F+1: INFEASIBLE Proven Capacity: ... Primary Bottleneck: ... Secondary Bottleneck: ... Capacity Offer: ... وقتی این مسیر واقعاً اجرا شد، پروژه از «سیستم طراحی‌شده» وارد قلمرو «نرم‌افزار حرفه‌ای واقعی» می‌شود. نکته خیلی مهم به نظرم الان نباید V2.6 را زود شروع کنیم. چون اگر همین الان برویم سراغ: Scenario Sensitivity Investment What-if ولی V2.5 روی داده واقعی End-to-End اجرا نشده باشد، داریم روی یک هسته هنوز تأییدنشده امکانات اضافه می‌کنیم. ترتیب درست از اینجا: V2.5-B Contract ↓ V2.5-C Aggregate → Detailed ↓ V2.5-D Real Access + Excel ↓ V2.5-E Real Infrastructure Master ↓ V2.5-F Real End-to-End Capacity ↓ V2.5-G Validation + Proof + Bottleneck ↓ V2.6 Scenario / Sensitivity / Investment ↓ V2.7 Marketplace ↓ V3.0 Full Workbench و یک نکته بسیار مثبت ما دیگر در مرحله‌ای نیستیم که سؤال اصلی این باشد: «چطور ظرفیت راه‌آهن را محاسبه کنیم؟» این بخش تا حد زیادی معماری و مدل شده است. سؤال اصلی الان شده: «چطور همین مدل را روی داده واقعی ایران، با زمان‌بندی واقعی و زیرساخت واقعی، به یک سیستم قابل اعتماد و قابل بهره‌برداری تبدیل کنیم؟» این تغییر سؤال، به‌نظر من، نشانه مهمی است که پروژه از مرحله Concept/Research عبور کرده و وارد مرحله Product Engineering شده است. اگر بخواهم یک نقشه راه اجرایی از همین امروز تا نسخه‌ای که واقعاً بتوان آن را در یک شرکت/راه‌آهن Pilot کرد بدهم، قدم بعدی را من V2.5-D — Real Data Execution می‌گذارم و از آنجا دیگر به‌جای سندهای بیشتر، تمرکز را روی ساخت Codebase واقعی، تست، اجرای Access/Excel، PostgreSQL و اولین Capacity واقعی می‌گذاریم. --- ## ادامه 65 — پرسش کاربر (`message-id:952c750e-77b3-4727-ba3e-d59fbafe6ae9`) بله لطفا --- ## ادامه 65 — پاسخ دستیار (`message-id:97eb6c8f-b258-4ff6-91de-bf7791c22470`) حتماً. از اینجا وارد V2.5-D — Real Data Execution می‌شویم؛ یعنی دیگر تمرکز اصلی روی طراحی مفهومی نیست، بلکه روی تبدیل واقعی Access + Excel به یک ورودی قابل اعتماد برای Capacity Engine است. برای Excel می‌توانیم از pandas.read_excel استفاده کنیم که مستقیماً فایل‌های .xlsx و سایر فرمت‌های رایج را می‌خواند؛ برای Access نیز معماری Adapter مبتنی بر ODBC مناسب است و ODBC اساساً لایه استاندارد دسترسی به DBMSهاست. Pandas +1 V2.5-D — Real Data Execution & Canonical Railway Data Pipeline V2.5-D — Real Data Execution & Canonical Railway Data Pipeline 1. هدف هدف V2.5-D تبدیل داده‌های واقعی موجود در: Microsoft Access Excel فایل‌های عملیاتی موجود به یک Canonical Railway Data Model معتبر، versioned، قابل ردیابی و قابل استفاده توسط موتور ظرفیت است. معماری این مرحله: Access / Excel ↓ Source Adapter ↓ Raw Records ↓ Staging ↓ Field Mapping ↓ Data Quality Gate ↓ Identity Reconciliation ↓ Canonical TrainRun ↓ TrainStationCall ↓ DirectedPath ↓ Baseline Schedule ↓ Infrastructure Master ↓ Detailed Scheduler ↓ Validation اصل کلیدی: هیچ داده‌ای مستقیماً از Access یا Excel وارد Solver نمی‌شود. 2. محدوده V2.5-D V2.5-D باید این موارد را عملیاتی کند: Access ingestion Excel ingestion Raw data preservation Data Version Source hashing Field mapping Field confidence Time normalization Midnight rollover Train identity reconciliation Station identity reconciliation TrainStationCall construction DirectedPath construction Baseline Schedule construction Data Quality Report Infrastructure Master bootstrap Real-data Golden Test موارد زیر عمداً در این مرحله بهینه‌سازی کامل نمی‌شوند: Marketplace optimization Full demand allocation Full investment optimization Advanced locomotive optimization Advanced wagon repositioning optimization 3. Source-of-Truth Rule برای هر Field باید مشخص شود: Source Field ↓ Canonical Field ↓ Transformation ↓ Confidence ↓ Validation Rule ↓ Production Usage مثلاً: Source Canonical وضعیت TrainNo TrainRun.source_train_no VERIFIED TrainName TrainRun.service_name VERIFIED StationName TrainStationCall.station_name VERIFIED Sequence TrainStationCall.sequence VERIFIED time_in arrival_time_source VERIFIED time_take dwell_time VERIFIED RequiredWait required_wait PROVISIONAL Kilometerage chainage HIGH seir running_time_to_next VERIFIED MaxSpeed max_speed PROVISIONAL Distance source_distance UNTRUSTED sumDistancezz source_cumulative_distance UNKNOWN اصل مهم: Field دارای وضعیت UNKNOWN یا UNTRUSTED نباید بدون تأیید وارد محاسبات Production Capacity شود. 4. Access Adapter Adapter فقط مسئول استخراج است. class AccessAdapter: def __init__(self, connection_string: str): self.connection_string = connection_string def list_tables(self) -> list[str]: ... def read_table(self, table_name: str) -> list[dict]: ... def read_query(self, query: str) -> list[dict]: ... Adapter نباید هیچ منطق Railway داشته باشد. یعنی: Access Adapter = Database Extraction نه: Access Adapter = Railway Interpretation این جداسازی برای جلوگیری از وابستگی Domain به Legacy Database ضروری است. 5. Excel Adapter برای Excel: class ExcelAdapter: def inspect(self, path: str): ... def list_sheets(self, path: str) -> list[str]: ... def read_sheet( self, path: str, sheet_name: str | int = 0, ): ... برای فایل فعلی: REPORTKholase_31-06-1405_02-19-35.xlsx باید ابتدا: File ↓ SHA256 ↓ Workbook Inspection ↓ Sheet Detection ↓ Column Detection ↓ Raw Records انجام شود. 6. Raw Record Raw Record باید داده را تقریباً همان‌طور که در Source وجود دارد نگهداری کند. @dataclass(frozen=True) class RawRecord: id: str data_version_id: str source_file_id: str source_system: str source_table: str | None source_row_number: int | None payload: dict مثلاً Access: { "TrainNo": 100, "StationName": "اراک", "StationNumber": 123, "Sequence": 7, "time_in": "12:27", "time_take": 60, "time_out": "13:27", "RequiredWait": 60, "Kilometerage": 325, "MaxSpeed": null, "TrainName": "گار-اندیمشک1", "Distance": 0, "sumDistancezz": 0, "seir": 105 } Raw Record نباید اطلاعات Source را تغییر دهد. 7. Staging Model Staging محل تبدیل Raw به داده قابل تحلیل است. @dataclass class StagedTrainMovement: source_record_id: str train_no: str train_name: str station_name: str station_number: str | None sequence: int arrival_raw: str | None dwell_minutes: int | None departure_raw: str | None required_wait_minutes: int | None chainage: float | None max_speed: float | None source_distance: float | None source_cumulative_distance: float | None running_time_to_next: int | None این مدل هنوز Canonical Railway Model نیست. 8. Field Mapping Registry Mapping باید داده‌محور باشد. مثلاً: TrainNo: canonical: train_run.source_train_no transformation: string confidence: VERIFIED TrainName: canonical: train_run.service_name transformation: string confidence: VERIFIED StationName: canonical: train_station_call.station_name transformation: station_identity_lookup confidence: VERIFIED Sequence: canonical: train_station_call.sequence transformation: integer confidence: VERIFIED time_in: canonical: train_station_call.arrival_time transformation: normalize_clock confidence: VERIFIED time_take: canonical: train_station_call.dwell_minutes transformation: integer confidence: VERIFIED RequiredWait: canonical: train_station_call.required_wait_minutes transformation: integer confidence: PROVISIONAL Kilometerage: canonical: train_station_call.chainage transformation: decimal confidence: HIGH seir: canonical: route_segment.baseline_running_time transformation: integer confidence: VERIFIED 9. Midnight Normalization این بخش یکی از مهم‌ترین قسمت‌های V2.5-D است. داده واقعی نشان می‌دهد که زمان می‌تواند از نیمه‌شب عبور کند: 23:46 00:36 00:56 02:19 بنابراین نگهداری صرفاً HH:MM کافی نیست. Canonical Time باید به absolute minute تبدیل شود: def normalize_clock( previous_absolute_minute: int | None, hhmm: str, ) -> int: hour, minute = map(int, hhmm.split(":")) current = hour * 60 + minute if previous_absolute_minute is None: return current previous_day = previous_absolute_minute // 1440 candidate = previous_day * 1440 + current while candidate < previous_absolute_minute: candidate += 1440 return candidate مثلاً: 23:46 → 1426 00:36 → 1476 00:56 → 1496 02:19 → 1579 بنابراین: 00:36 دیگر از نظر Solver قبل از: 23:46 تفسیر نمی‌شود. 10. Schedule Reconstruction برای هر TrainRun: Station i Arrival Dwell Departure ↓ Running Time ↓ Station i+1 Arrival رابطه پایه: [ D_i=A_i+T^{dwell}_i ] و: [ A_{i+1}=D_i+T^{run}_{i,i+1} ] در داده فعلی: seir به عنوان: running_time_to_next تفسیر می‌شود. بنابراین: seir[i] متعلق به: Segment(i → i+1) است، نه به Station i به عنوان یک ویژگی مستقل. 11. TrainStationCall مدل Canonical: @dataclass(frozen=True) class TrainStationCall: train_run_id: str station_id: str sequence: int arrival_minute: int departure_minute: int dwell_minutes: int required_wait_minutes: int | None source_station_number: str | None chainage: float | None source_distance: float | None derived_distance: float | None running_time_to_next: int | None 12. Derived Distance Kilometerage نباید با Distance یکی فرض شود. اگر: Station A chainage = 157 Station B chainage = 200 فاصله Derived: [ Distance_{A,B}=|200-157|=43 ] است. اما: Distance Source Field باقی می‌ماند. بنابراین: source_distance و: derived_distance دو Field مستقل هستند. این کار جلوی از بین رفتن Source Evidence را می‌گیرد. 13. Train Identity Reconciliation TrainNo به تنهایی Identity کامل نیست. Identity باید حداقل بر اساس: TrainNo TrainName Origin Destination Direction Operating Pattern ساخته شود. مثلاً: @dataclass(frozen=True) class TrainIdentityKey: source_train_no: str service_name: str origin_station_id: str destination_station_id: str direction: str operating_pattern: str این موضوع مخصوصاً برای: TrainNo = 100 TrainNo = 101 که دو حرکت جهت مخالف یک سرویس هستند اهمیت دارد. 14. TrainRun Canonical: @dataclass(frozen=True) class TrainRun: id: str service_id: str source_train_no: str service_name: str origin_station_id: str destination_station_id: str direction: str operating_day: int | None operating_pattern: str | None و: TrainRun ↓ TrainStationCall[] 15. DirectedPath DirectedPath باید از Sequence و topology ساخته شود. هرگز: sorted(stations) استفاده نشود. ساخت صحیح: Sequence ↓ Station Order ↓ Adjacent Station Pairs ↓ PhysicalBlock ↓ DirectedPath مدل: @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] sequence: tuple[int, ...] Invariant: [ |Stations|=|Blocks|+1 ] 16. Physical Block Physical Block باید direction-independent باشد. PhysicalBlock.make_id( station_a, station_b ) مثلاً: GAR::SAKHEH برای هر دو جهت یک Block است. اما: GAR → SAKHEH SAKHEH → GAR دو Directed Movement هستند. 17. Track Resource اگر: TrackType = SINGLE باشد: GAR::SAKHEH یک Resource مشترک برای هر دو جهت است. اگر: TrackType = DOUBLE باشد: GAR::SAKHEH:FORWARD GAR::SAKHEH:REVERSE به عنوان Resourceهای مجزا مدل می‌شوند. 18. Data Quality Gate قبل از ورود به Canonical Model: class DataQualityGate: def validate(self, records): ... قوانین: Sequence [ Sequence_{i+1}>Sequence_i ] Dwell [ Dwell_i\ge0 ] Running Time [ seir_i>0 ] در صورت وجود. Schedule [ Departure_i\ge Arrival_i ] Next Station [ Arrival_{i+1}\ge Departure_i ] Chainage برای مسیرهای بدون تغییر جهت: monotonic increasing یا: monotonic decreasing اما این قانون باید برای هر Directed TrainRun جداگانه اعمال شود. 19. Quality Status هر Record یکی از وضعیت‌های زیر را می‌گیرد: VALID WARNING REJECTED UNVERIFIED و DataVersion: NOT_CHECKED PASSED PASSED_WITH_WARNINGS FAILED مثلاً: DataVersion: 2026-09-28-001 Records: 12,450 Valid: 12,390 Warnings: 45 Rejected: 15 این اعداد فقط نمونه هستند و نباید به عنوان نتیجه واقعی داده‌های شما تلقی شوند. 20. Infrastructure Master این مرحله باید از داده Train Movement، یک Infrastructure Master اولیه ایجاد کند. اما این دو مفهوم نباید یکی شوند. Operational Evidence ≠ Infrastructure Master از داده‌های Train می‌توان Evidence استخراج کرد: Station A Station B Chainage A Chainage B Observed Running Time Direction اما اطلاعاتی مثل: SINGLE / DOUBLE Headway Switch Time Clearing Time Station Track Capacity Junction Conflict Operational Window باید از Infrastructure Master یا منابع معتبر دیگر بیاید. 21. Bootstrap Infrastructure Evidence از نمونه Access می‌توان: Station ↓ Adjacent Station ↓ Chainage Difference ↓ Observed Running Time ساخت. مثلاً: @dataclass(frozen=True) class SegmentEvidence: from_station_id: str to_station_id: str direction: str derived_distance_m: float | None baseline_running_time_min: int | None source_train_run_id: str این Evidence است، نه الزاماً Infrastructure Master نهایی. 22. Infrastructure Master مدل Production: @dataclass(frozen=True) class InfrastructureBlock: id: str station_a_id: str station_b_id: str track_type: TrackType running_time_forward: int running_time_reverse: int headway_same_direction: int switch_time: int clearing_time: int اطلاعاتی که از Source واقعی نداریم: UNKNOWN و نباید با مقدار فرضی Production جایگزین شوند. 23. Baseline Schedule Baseline باید از داده واقعی بازسازی شود: Access ↓ TrainRun ↓ Station Calls ↓ Baseline Schedule Baseline شامل: Arrival Departure Dwell Running Station Sequence Block Movement است. Baseline Schedule هدفش این نیست که Solver را محدود کند. بلکه برای: Baseline vs Optimized استفاده می‌شود. 24. Baseline Difference پس از Optimization: @dataclass(frozen=True) class ScheduleDifference: train_run_id: str baseline_departure: int optimized_departure: int baseline_arrival: int optimized_arrival: int departure_delta: int arrival_delta: int در نتیجه می‌توان گفت: Train 100 Baseline Arrival: ... Optimized Arrival: ... Delta: +18 min بدون اینکه Baseline را با Optimized اشتباه بگیریم. 25. Real Data Pipeline Pipeline رسمی: class RealDataPipeline: def run(self, source): raw = self.ingest(source) staged = self.stage(raw) mapped = self.map(staged) quality = self.quality_gate(mapped) if quality.failed: return PipelineRejected(quality) identities = self.reconcile_train_identities(mapped) canonical = self.build_canonical(identities) paths = self.build_directed_paths(canonical) baseline = self.build_baseline_schedule( canonical, paths, ) return RealDataResult( canonical=canonical, paths=paths, baseline=baseline, quality=quality, ) 26. Data Version هر Ingestion باید DataVersion مستقل بسازد. @dataclass(frozen=True) class DataVersion: id: str name: str source_type: str source_uri: str source_hash: str created_at: datetime status: DataVersionStatus quality_status: QualityStatus Source Hash: sha256(file_bytes) بنابراین: aaa.accdb در دو تاریخ مختلف اگر تغییر کرده باشد: DataVersion A DataVersion B خواهد داشت. 27. Canonical Snapshot قبل از Solver: Canonical Snapshot ساخته می‌شود. شامل: Infrastructure Stations Blocks Routes Train Types Train Runs Station Calls Demand Wagons Locomotives Operational Rules و: snapshot_hash(snapshot) محاسبه می‌شود. بنابراین Run کاملاً قابل بازتولید است: [ Result= f( DataVersion, Scenario, ModelVersion, SolverConfiguration, InputSnapshot ) ] 28. Production Run Identity هر Run باید این موارد را ثبت کند: Run ID Data Version Scenario Model Version Solver Configuration Input Snapshot Hash Created At Started At Finished At این موضوع برای Audit و Reproducibility ضروری است. 29. Real-Data Golden Test اولین Golden Test واقعی باید کوچک باشد. مثلاً: Train 100 Train 101 با تمام Station Callهایشان. Pipeline باید: Access Sample ↓ Raw ↓ Staging ↓ Mapping ↓ Quality ↓ Train Identity ↓ TrainRun ↓ Station Calls ↓ Directed Path ↓ Baseline را بدون خطا طی کند. 30. Golden Assertions باید بتوانیم Assertions واقعی بنویسیم: assert train_100.origin == "GAR" assert train_100.destination == "ANDIMESHK" assert train_101.origin == "ANDIMESHK" assert train_101.destination == "GAR" و: assert calls_100[0].sequence == 1 assert calls_100[-1].sequence > calls_100[0].sequence و: assert calls_100[i].running_time_to_next == ... و: assert derived_distance >= 0 و مهم‌تر: assert arrival_next >= departure_current حتی در عبور از Midnight. 31. Production Acceptance Gate V2.5-D زمانی Complete است که: Access File ↓ DataVersion ↓ Raw Records ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Identity Reconciliation ↓ Canonical TrainRun ↓ Canonical Station Calls ↓ DirectedPath ↓ Baseline Schedule بدون دخالت دستی غیرقابل ردیابی انجام شود. 32. معیار موفقیت موفقیت V2.5-D این نیست که: «فایل Access خوانده شد.» موفقیت این است که: یک رکورد واقعی Legacy بتواند با Lineage کامل به یک TrainRun و سپس به یک DirectedPath و Baseline Schedule در Canonical Model تبدیل شود. یعنی: Source Record ↓ Mapping Evidence ↓ Canonical Entity ↓ Operational Meaning ↓ Schedule Evidence و تمام این زنجیره قابل Audit باشد. 33. بعد از V2.5-D پس از موفقیت V2.5-D، مرحله بعد: V2.5-E — Real Infrastructure Master در آن مرحله باید واقعاً مشخص کنیم: Station ├── Tracks ├── Usable Length ├── Entry/Exit ├── Crossing └── Overtaking Block ├── Single / Double ├── Direction ├── Running Time ├── Headway ├── Switch Time └── Clearing Time Junction ├── Movements └── Conflict Matrix Operational Window ├── Start ├── End └── Resource بعد: V2.5-D Real Data + V2.5-E Real Infrastructure ↓ V2.5-F Real End-to-End Capacity و آنجا برای اولین بار مسیر واقعی زیر را اجرا می‌کنیم: Access + Excel + Infrastructure + Demand ↓ Canonical Model ↓ Aggregate Network Optimization ↓ TrainRun ↓ Formation ↓ Wagon ↓ Locomotive ↓ V1.7 Time-Space Scheduler ↓ Independent Validation ↓ Capacity Search ↓ F / F+1 ↓ Capacity Proof ↓ Bottleneck ↓ Capacity Offer 34. Definition of Done — V2.5-D V2.5-D فقط زمانی DONE است که: Access Adapter عملیاتی باشد Excel Adapter عملیاتی باشد Source Hash ثبت شود DataVersion ساخته شود Raw Records حفظ شوند Staging Model وجود داشته باشد Mapping Registry وجود داشته باشد Field Confidence ثبت شود Quality Gate اجرا شود Midnight rollover درست مدیریت شود Train Identity Reconciliation اجرا شود Station Identity Reconciliation اجرا شود TrainRun ساخته شود TrainStationCall ساخته شود seir به Running Time to Next تبدیل شود Kilometerage به Chainage تبدیل شود Derived Distance جدا از Source Distance باشد sumDistancezz بدون تأیید استفاده نشود RequiredWait به صورت Provisional نگهداری شود MaxSpeed بدون تأیید وارد مدل قطعی نشود DirectedPath ساخته شود Baseline Schedule ساخته شود Data Quality Report تولید شود Canonical Snapshot ساخته شود Golden Test واقعی تعریف شود تمام Lineageها قابل ردیابی باشند 35. جایگاه V2.5-D در کل محصول RAILWAY CAPACITY WORKBENCH │ ▼ REAL DATA PLATFORM │ ┌──────────────┴──────────────┐ ▼ ▼ Access Excel │ │ └──────────────┬──────────────┘ ▼ Raw / Staging ▼ Mapping / Quality ▼ Identity Reconciliation ▼ Canonical Model │ ┌───────────┴───────────┐ ▼ ▼ Infrastructure Train Runs │ │ └───────────┬───────────┘ ▼ Directed Path ▼ Baseline Schedule ▼ Aggregate Network ▼ Detailed Scheduler ▼ Validation ▼ Capacity Proof ▼ Marketplace Capacity V2.5-D نقطه‌ای است که پروژه از «Production Architecture» به سمت «Production Execution on Real Data» عبور می‌کند. گام عملی بعدی را پیشنهاد می‌کنم V2.5-D را از حالت سند به کد واقعی تبدیل کنیم: ابتدا ساختار پروژه و فایل‌های access.py, excel.py, models.py, mapping.yml, quality.py, reconciliation.py, baseline.py و تست Golden را بسازیم؛ سپس اگر فایل‌های aaa.accdb و REPORTKholase...xlsx را در همین گفتگو در اختیارم بگذارید، می‌توانیم همین pipeline را روی داده واقعی اجرا و خطاهای واقعی را یکی‌یکی برطرف کنیم. --- ## ادامه 66 — پرسش کاربر (`message-id:ac14f194-78d7-4aaf-a385-cfeb27192aec`) بله لطفا --- ## ادامه 66 — پاسخ دستیار (`message-id:91f26a16-2f22-44d0-9620-d094f109bf37`) حتماً. پس وارد فاز اجرایی V2.5-D می‌شویم. این بار به‌جای یک سند دیگر، بهتر است یک Vertical Slice واقعی و قابل اجرا بسازیم. مبنای فنی آن هم مشخص است: برای Excel از pandas.read_excel استفاده می‌کنیم و برای Access یک Adapter جداگانه مبتنی بر ODBC/pyodbc نگه می‌داریم؛ این جداسازی باعث می‌شود Legacy Source وارد Domain و Solver نشود. Pandas +1 پیشنهاد من برای همین مرحله اول این بسته را بسازیم: railway_capacity_workbench/ │ ├── app/ │ ├── domain/ │ │ ├── train.py │ │ ├── station.py │ │ ├── route.py │ │ └── schedule.py │ │ │ ├── adapters/ │ │ ├── access.py │ │ └── excel.py │ │ │ ├── staging/ │ │ └── models.py │ │ │ ├── mapping/ │ │ ├── registry.py │ │ └── rules.py │ │ │ ├── quality/ │ │ ├── rules.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ ├── train_identity.py │ │ └── station_identity.py │ │ │ ├── scheduling/ │ │ ├── time.py │ │ ├── path.py │ │ └── baseline.py │ │ │ └── pipeline/ │ └── real_data.py │ ├── mappings/ │ ├── access_train_movement.yml │ └── excel_kholase.yml │ ├── fixtures/ │ └── real_sample/ │ └── tests/ ├── test_time_normalization.py ├── test_access_mapping.py ├── test_train_identity.py ├── test_directed_path.py └── test_real_data_golden.py و اولین Vertical Slice دقیقاً این خواهد بود: aaa.accdb / Excel ↓ RawRecord ↓ StagedTrainMovement ↓ Field Mapping ↓ Quality Gate ↓ Train Identity ↓ TrainRun ↓ TrainStationCall[] ↓ DirectedPath ↓ Baseline Schedule ↓ JSON Canonical Snapshot مهم‌ترین تست اول از داده‌ای که قبلاً از Access استخراج کردیم، TrainNo=100 و TrainNo=101 را به‌عنوان Golden Test می‌گذاریم. باید نرم‌افزار بتواند تشخیص دهد: 100: GAR → ANDIMESHK 101: ANDIMESHK → GAR و برای هر دو: Sequence حفظ شود. Midnight rollover درست شود. seir به Running Time Segment تبدیل شود. Kilometerage به Chainage تبدیل شود. derived_distance = abs(next.chainage - current.chainage) محاسبه شود. Distance دست‌نخورده به‌عنوان Source Evidence بماند. sumDistancezz فعلاً وارد Capacity Calculation نشود. RequiredWait به‌عنوان Provisional نگهداری شود. DirectedPath بر اساس Sequence ساخته شود، نه نام ایستگاه. Baseline Schedule بازسازی شود. و در پایان یک خروجی شبیه این داشته باشیم: JSON { "train_run_id": "TR-100", "source_train_no": "100", "origin": "GAR", "destination": "ANDIMESHK", "direction": "FORWARD", "station_call_count": 32, "midnight_rollover_detected": true, "path_valid": true, "baseline_schedule_valid": true } اما یک نکته مهم: در حال حاضر فایل aaa.accdb و Excel در این نوبت در دسترس ابزار فایل من نیستند؛ بنابراین نمی‌خواهم وانمود کنم که Adapter را روی فایل واقعی اجرا کرده‌ام. کدی که می‌سازیم باید ابتدا روی Golden Fixture مبتنی بر همان داده‌های واقعیِ قبلاً استخراج‌شده تست شود و سپس با فایل اصلی Validate شود. اگر فایل‌های aaa.accdb و REPORTKholase_31-06-1405_02-19-35.xlsx را همین‌جا آپلود کنید، قدم بعدی را کاملاً اجرایی می‌کنیم: کد V2.5-D را می‌سازیم، فایل‌ها را می‌خوانیم، Mapping و Quality Report واقعی تولید می‌کنیم و اولین Canonical Snapshot را استخراج می‌کنیم. --- ## ادامه 67 — پرسش کاربر (`message-id:6799c6ae-924b-49f0-9ad3-8c4d87e91821`) چون قابل آپلود نیست به صورت پارامتری الان پیش ببر تا بعدا بهت بدم --- ## ادامه 67 — پاسخ دستیار (`message-id:c3f59e42-9ad4-4282-bb4d-c73fb89d1787`) کاملاً موافقم. اتفاقاً این روش برای پروژه ما بهتر است: الان هسته V2.5-D را File-Agnostic و Parameterized می‌سازیم؛ بعداً فقط Source Adapter/Mapping را با فایل واقعی تنظیم می‌کنیم و هیچ تغییری در Domain یا Solver لازم نیست. در نتیجه فعلاً با این قرارداد جلو می‌رویم: PARAMETERS ↓ Source Adapter ↓ Raw Records ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Reconciliation ↓ Canonical Model ↓ Directed Path ↓ Baseline Schedule V2.5-D-P — Parameterized Real Data Engine 1. پارامترهای اصلی Python @dataclass(frozen=True) class DataSourceConfig: source_type: str # ACCESS | EXCEL | JSON | CSV source_uri: str table_name: str | None = None sheet_name: str | None = None encoding: str = "utf-8" @dataclass(frozen=True) class MappingConfig: train_no_field: str train_name_field: str station_name_field: str station_number_field: str | None sequence_field: str time_in_field: str dwell_field: str time_out_field: str | None required_wait_field: str | None chainage_field: str | None max_speed_field: str | None distance_field: str | None cumulative_distance_field: str | None running_time_to_next_field: str | None @dataclass(frozen=True) class SchedulingConfig: default_dwell_minutes: int = 0 default_required_wait_minutes: int = 0 allow_midnight_rollover: bool = True max_rollover_days: int = 2 @dataclass(frozen=True) class QualityConfig: require_sequence: bool = True require_station_name: bool = True require_train_no: bool = True require_running_time: bool = False reject_negative_dwell: bool = True reject_invalid_sequence: bool = True validate_chainage_monotonicity: bool = True 2. Mapping کاملاً Configurable در نتیجه اگر Access واقعی بعداً این باشد: TrainNo StationName Sequence time_in time_take seir mapping این است: YAML train_no_field: TrainNo train_name_field: TrainName station_name_field: StationName station_number_field: StationNumber sequence_field: Sequence time_in_field: time_in dwell_field: time_take time_out_field: time_out required_wait_field: RequiredWait chainage_field: Kilometerage max_speed_field: MaxSpeed distance_field: Distance cumulative_distance_field: sumDistancezz running_time_to_next_field: seir اگر فایل آینده Fieldهای متفاوتی داشته باشد، فقط YAML عوض می‌شود. 3. Source-Neutral Raw Record Python @dataclass(frozen=True) class RawRecord: record_id: str source_file_id: str source_system: str source_table: str | None source_row_number: int | None payload: dict[str, object] بنابراین هیچ وابستگی به: Excel SQL Server PostgreSQL در Domain نداریم. 4. Staging Python @dataclass class StagedTrainMovement: source_record_id: str train_no: str train_name: str | None station_name: str station_number: str | None sequence: int time_in_raw: str | None time_out_raw: str | None dwell_minutes: int | None required_wait_minutes: int | None chainage: float | None max_speed: float | None source_distance: float | None source_cumulative_distance: float | None running_time_to_next: int | None 5. Time Engine این بخش را مستقل از Access/Excel می‌سازیم. Python MINUTES_PER_DAY = 1440 def parse_hhmm(value: str) -> int: hour, minute = map(int, value.strip().split(":")) if not 0 <= hour <= 23: raise ValueError("Invalid hour") if not 0 <= minute <= 59: raise ValueError("Invalid minute") return hour * 60 + minute و: Python def normalize_after( previous: int | None, current_hhmm: str, ) -> int: current = parse_hhmm(current_hhmm) if previous is None: return current candidate = current + ( previous // MINUTES_PER_DAY ) * MINUTES_PER_DAY while candidate < previous: candidate += MINUTES_PER_DAY return candidate این قسمت برای داده‌های واقعی بسیار مهم است. مثلاً: 23:46 00:36 00:56 02:19 به: 1426 1476 1496 1579 تبدیل می‌شوند. 6. Reconstruction Engine برای هر Station Call: Departure i ​ =Arrival i ​ +Dwell i ​ و اگر Running Time موجود باشد: Arrival i+1 ​ =Departure i ​ +RunningTime i ​ بنابراین: Python @dataclass(frozen=True) class CanonicalStationCall: train_run_id: str station_id: str sequence: int arrival_minute: int departure_minute: int dwell_minutes: int required_wait_minutes: int | None chainage: float | None source_distance: float | None derived_distance: float | None running_time_to_next: int | None 7. محاسبه Distance اگر دو Chainage متوالی داشته باشیم: Python def derive_distance( current_chainage: float | None, next_chainage: float | None, ) -> float | None: if current_chainage is None: return None if next_chainage is None: return None return abs(next_chainage - current_chainage) بنابراین: Distance Source ≠ Derived Distance و این دو هرگز overwrite نمی‌شوند. 8. Train Identity Identity به شکل پارامتری: Python @dataclass(frozen=True) class TrainIdentityKey: train_no: str train_name: str | None origin: str destination: str direction: str operating_pattern: str | None و: Python def build_identity_key( records: list[StagedTrainMovement], ) -> TrainIdentityKey: ordered = sorted( records, key=lambda x: x.sequence ) first = ordered[0] last = ordered[-1] direction = ( "FORWARD" if first.chainage is not None and last.chainage is not None and last.chainage >= first.chainage else "REVERSE" ) return TrainIdentityKey( train_no=first.train_no, train_name=first.train_name, origin=first.station_name, destination=last.station_name, direction=direction, operating_pattern=None, ) نکته مهم: این فقط Fallback است. در Production بهتر است Direction از Mapping/Infrastructure Master بیاید، نه صرفاً Chainage. 9. Canonical TrainRun Python @dataclass(frozen=True) class CanonicalTrainRun: id: str source_train_no: str service_name: str | None origin_station_id: str destination_station_id: str direction: str station_calls: tuple[ CanonicalStationCall, ... ] 10. DirectedPath Builder ورودی: TrainRun + Infrastructure Master خروجی: Python @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: str station_ids: tuple[str, ...] block_ids: tuple[str, ...] sequence: tuple[int, ...] Invariant: Python assert len(station_ids) == len(block_ids) + 1 و: Python assert list(sequence) == sorted(sequence) ولی مرتب‌سازی برای ساخت Path انجام نمی‌شود؛ Sequence از Source تعیین‌کننده Order است. 11. Infrastructure Parameter چون هنوز Infrastructure واقعی را نداریم، آن را Parameter می‌کنیم: Python @dataclass(frozen=True) class InfrastructureBlockConfig: block_id: str station_a: str station_b: str track_type: str # SINGLE | DOUBLE running_time_forward: int | None running_time_reverse: int | None headway_same_direction: int switch_time: int clearing_time: int بنابراین فعلاً می‌توانیم مثلاً بگوییم: YAML blocks: - id: B001 station_a: GAR station_b: SAKHEH track_type: SINGLE running_time_forward: 20 running_time_reverse: 22 headway_same_direction: 5 switch_time: 8 clearing_time: 2 این پارامتر آزمایشی است، نه ادعای واقعیت زیرساخت. 12. Data Quality Result Python @dataclass(frozen=True) class QualityIssue: severity: str rule: str record_id: str message: str و: Python @dataclass(frozen=True) class QualityReport: total_records: int valid_records: int warning_count: int rejected_count: int issues: tuple[QualityIssue, ...] status: str 13. Pipeline اصلی هسته اصلی: Python class ParameterizedRealDataPipeline: def __init__( self, source_config: DataSourceConfig, mapping_config: MappingConfig, scheduling_config: SchedulingConfig, quality_config: QualityConfig, ): self.source_config = source_config self.mapping_config = mapping_config self.scheduling_config = scheduling_config self.quality_config = quality_config def run(self): raw = self.ingest() staged = self.stage(raw) quality = self.validate(staged) if quality.status == "FAILED": return PipelineResult.failed( quality ) identities = self.reconcile(staged) canonical = self.canonicalize( identities ) paths = self.build_paths( canonical ) baseline = self.build_baseline( canonical, paths ) return PipelineResult( quality=quality, trains=canonical, paths=paths, baseline=baseline, ) 14. یک Interface بسیار مهم برای اینکه بعداً فایل واقعی را به‌سادگی وصل کنیم: Python class SourceAdapter(Protocol): def read( self, config: DataSourceConfig, ) -> list[RawRecord]: ... و سپس: AccessAdapter ExcelAdapter JsonAdapter CsvAdapter همه همین Interface را پیاده می‌کنند. 15. حالت فعلی بدون فایل تا زمانی که فایل واقعی را بدهید، Pipeline را با یک Parameter Fixture اجرا می‌کنیم: Python fixture = [ { "TrainNo": 100, "TrainName": "گار-اندیمشک1", "StationName": "GAR", "StationNumber": "001", "Sequence": 1, "time_in": "08:00", "time_take": 10, "time_out": "08:10", "RequiredWait": 10, "Kilometerage": 157, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": 30, }, { "TrainNo": 100, "TrainName": "گار-اندیمشک1", "StationName": "SAKHEH", "StationNumber": "002", "Sequence": 2, "time_in": "08:40", "time_take": 10, "time_out": "08:50", "RequiredWait": 10, "Kilometerage": 200, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": 40, } ] این Fixture فقط برای تست Engine است. 16. مهم‌تر از همه: Parameterization تا Solver این طراحی باعث می‌شود بعداً فقط این قسمت تغییر کند: TODAY ────────────── fixture ↓ Parameterized Adapter ↓ Canonical به: FUTURE ────────────── aaa.accdb ↓ AccessAdapter ↓ Raw ↓ Canonical و تمام قسمت زیر ثابت بماند: Canonical ↓ DirectedPath ↓ Baseline ↓ Formation ↓ Wagon ↓ Locomotive ↓ Detailed Scheduler ↓ Capacity 17. نتیجه معماری با این تصمیم، پروژه دیگر به فایل خاصی وابسته نیست. سه لایه کاملاً جدا داریم: SOURCE LAYER Access / Excel / API / CSV ↓ CANONICALIZATION LAYER Raw / Staging / Mapping / Quality / Reconciliation ↓ RAILWAY ENGINE Train / Route / Formation / Wagon / Loco / Schedule / Network / Capacity و این دقیقاً همان مرزی است که برای تبدیل پروژه به نرم‌افزار Production لازم داریم. 18. مرحله بعدی بعد از این، من پیشنهاد می‌کنم V2.5-D.1 را اجرا کنیم و همین Parameterized Engine را تا سطح یک بسته Python واقعی ببریم، شامل: app/ ├── adapters/ ├── staging/ ├── mapping/ ├── quality/ ├── reconciliation/ ├── domain/ ├── scheduling/ ├── pipeline/ └── tests/ و سه ورودی همزمان داشته باشیم: 1. Synthetic Fixture 2. Access-shaped Fixture 3. Excel-shaped Fixture به این ترتیب وقتی بعداً aaa.accdb و Excel واقعی را در اختیارم گذاشتید، فقط Adapter را به Source واقعی وصل می‌کنیم؛ معماری و الگوریتم دوباره‌کاری نمی‌شود. این نقطه، به‌نظر من، بهترین جا برای عبور پروژه از Specification → Executable Production Foundation است. --- ## ادامه 68 — پرسش کاربر (`message-id:b55bb7f2-6a7c-4e01-919e-010afcb58a58`) بسیار هم عالی --- ## ادامه 68 — پاسخ دستیار (`message-id:a9458679-a50f-4a66-81e9-7ce105de2a1c`) عالیه. پس مسیر را همین‌طور منظم جلو می‌بریم. گام بعدی را V2.5-D.1 می‌گذاریم: V2.5-D.1 Parameterized Real Data Core ↓ Access-shaped Fixture Excel-shaped Fixture Synthetic Fixture ↓ Raw → Staging → Mapping → Quality ↓ Identity Reconciliation ↓ Canonical TrainRun ↓ TrainStationCall ↓ DirectedPath ↓ Baseline Schedule ↓ Golden Tests بعد از اینکه این هسته پایدار شد، مستقیم می‌رویم سراغ: V2.5-E — Real Infrastructure Master و سپس اولین اجرای کامل: Real Data + Real/Configured Infrastructure + Demand ↓ Aggregate ↓ Detailed Time-Space ↓ Validation ↓ F / F+1 ↓ Capacity Proof ↓ Bottleneck ↓ Capacity Offer نکته مهم این است که از اینجا به بعد هر نسخه باید Executable + Testable + Versioned باشد؛ یعنی فقط سند تولید نکنیم، بلکه هر قابلیت جدید با Contract، Code، Test و Definition of Done وارد محصول شود. در نتیجه از این مرحله به بعد، پروژه عملاً وارد مهندسی نرم‌افزار محصول نهایی شده است. --- ## ادامه 69 — پرسش کاربر (`message-id:c2f5697b-ef69-4fab-b40e-70b253257ddd`) بریم انجامش بدیم --- ## ادامه 69 — پاسخ دستیار (`message-id:78f78560-ecd9-4bb9-8270-5cd8ce67beae`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 70 — پرسش کاربر (`message-id:d92dd814-4fb3-465e-911d-75d5a21d5715`) بله لطفا همینجا به صورت متنی ادامه بدیم --- ## ادامه 70 — پاسخ دستیار (`message-id:d47e0bbb-402c-4d13-959e-e0ea27e67a2e`) حتماً. پس بدون وابستگی به اجرای ابزار، V2.5-D.1 را همینجا به‌صورت کد واقعی و متنی جلو می‌بریم. این نسخه باید مبنای بعدی پروژه قرار بگیرد، نه صرفاً یک نمونه آموزشی. V2.5-D.1 — Parameterized Real Data Core هدف این نسخه: Access / Excel / Fixture ↓ RawRecord ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Reconciliation ↓ Canonical TrainRun ↓ TrainStationCall ↓ DirectedPath ↓ Baseline Schedule 1. ساختار پروژه railway_capacity_workbench/ │ ├── app/ │ ├── __init__.py │ │ │ ├── domain/ │ │ ├── __init__.py │ │ └── models.py │ │ │ ├── adapters/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── in_memory.py │ │ ├── access.py │ │ └── excel.py │ │ │ ├── staging/ │ │ ├── __init__.py │ │ └── models.py │ │ │ ├── mapping/ │ │ ├── __init__.py │ │ └── config.py │ │ │ ├── quality/ │ │ ├── __init__.py │ │ ├── report.py │ │ └── gate.py │ │ │ ├── reconciliation/ │ │ ├── __init__.py │ │ └── train_identity.py │ │ │ ├── scheduling/ │ │ ├── __init__.py │ │ ├── time.py │ │ ├── path.py │ │ └── baseline.py │ │ │ └── pipeline/ │ ├── __init__.py │ └── real_data.py │ ├── mappings/ │ ├── access_train_movement.yml │ └── excel_kholase.yml │ ├── fixtures/ │ └── real_shape/ │ └── train_movements.py │ └── tests/ ├── test_time.py ├── test_quality.py ├── test_identity.py ├── test_path.py └── test_real_data_pipeline.py 2. Domain Model فایل: app/domain/models.py Python from dataclasses import dataclass from enum import Enum from typing import Any class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" UNKNOWN = "UNKNOWN" class QualityStatus(str, Enum): PASSED = "PASSED" PASSED_WITH_WARNINGS = "PASSED_WITH_WARNINGS" FAILED = "FAILED" @dataclass(frozen=True) class RawRecord: record_id: str source_system: str source_file_id: str source_table: str | None source_row_number: int | None payload: dict[str, Any] @dataclass(frozen=True) class TrainStationCall: train_run_id: str station_id: str station_name: str sequence: int arrival_minute: int departure_minute: int dwell_minutes: int required_wait_minutes: int | None chainage: float | None source_distance: float | None derived_distance: float | None running_time_to_next: int | None @dataclass(frozen=True) class TrainRun: id: str source_train_no: str service_name: str | None origin_station_id: str destination_station_id: str direction: Direction station_calls: tuple[TrainStationCall, ...] @dataclass(frozen=True) class DirectedPath: id: str route_id: str direction: Direction station_ids: tuple[str, ...] block_ids: tuple[str, ...] sequence: tuple[int, ...] @dataclass(frozen=True) class BaselineSchedule: train_run_id: str origin_departure: int destination_arrival: int duration_minutes: int station_calls: tuple[TrainStationCall, ...] 3. Source Adapter Contract فایل: app/adapters/base.py Python from typing import Protocol from app.domain.models import RawRecord class SourceAdapter(Protocol): def read(self) -> list[RawRecord]: ... این Interface بسیار مهم است. از این به بعد Engine نمی‌فهمد داده از کجا آمده است. ممکن است: Access Excel CSV JSON API Database باشد. 4. In-Memory Adapter برای تست: Python from app.domain.models import RawRecord class InMemoryAdapter: def __init__( self, records: list[dict], source_system: str = "FIXTURE", ): self.records = records self.source_system = source_system def read(self) -> list[RawRecord]: result = [] for index, record in enumerate( self.records, start=1, ): result.append( RawRecord( record_id=str(index), source_system=self.source_system, source_file_id="fixture", source_table=None, source_row_number=index, payload=dict(record), ) ) return result این دقیقاً همان چیزی است که اجازه می‌دهد بدون داشتن فایل واقعی، نرم‌افزار را توسعه دهیم. 5. Access Adapter Python import pyodbc from app.domain.models import RawRecord class AccessAdapter: def __init__(self, connection_string: str): self.connection_string = connection_string def read_table( self, table_name: str, ) -> list[RawRecord]: with pyodbc.connect( self.connection_string ) as connection: cursor = connection.cursor() cursor.execute( f"SELECT * FROM [{table_name}]" ) columns = [ description[0] for description in cursor.description ] rows = cursor.fetchall() result = [] for index, row in enumerate( rows, start=1, ): payload = dict( zip(columns, row) ) result.append( RawRecord( record_id=str(index), source_system="ACCESS", source_file_id=self.connection_string, source_table=table_name, source_row_number=index, payload=payload, ) ) return result نکته Production: table_name در نسخه نهایی باید از configuration معتبر بیاید و قبل از ساخت SQL اعتبارسنجی شود؛ Adapter نباید محل اجرای SQL دلخواه باشد. 6. Excel Adapter Python import pandas as pd from app.domain.models import RawRecord class ExcelAdapter: def __init__( self, path: str, sheet_name: str | int = 0, ): self.path = path self.sheet_name = sheet_name def read(self) -> list[RawRecord]: dataframe = pd.read_excel( self.path, sheet_name=self.sheet_name, ) result = [] for index, row in enumerate( dataframe.to_dict( orient="records" ), start=2, ): result.append( RawRecord( record_id=str(index), source_system="EXCEL", source_file_id=self.path, source_table=None, source_row_number=index, payload=row, ) ) return result 7. Mapping Configuration Python from dataclasses import dataclass @dataclass(frozen=True) class MappingConfig: train_no_field: str = "TrainNo" train_name_field: str = "TrainName" station_name_field: str = "StationName" station_number_field: str | None = ( "StationNumber" ) sequence_field: str = "Sequence" time_in_field: str = "time_in" dwell_field: str = "time_take" time_out_field: str | None = "time_out" required_wait_field: str | None = ( "RequiredWait" ) chainage_field: str | None = ( "Kilometerage" ) max_speed_field: str | None = ( "MaxSpeed" ) distance_field: str | None = ( "Distance" ) cumulative_distance_field: str | None = ( "sumDistancezz" ) running_time_to_next_field: str | None = ( "seir" ) 8. Mapping واقعی Access YAML train_no_field: TrainNo train_name_field: TrainName station_name_field: StationName station_number_field: StationNumber sequence_field: Sequence time_in_field: time_in dwell_field: time_take time_out_field: time_out required_wait_field: RequiredWait chainage_field: Kilometerage max_speed_field: MaxSpeed distance_field: Distance cumulative_distance_field: sumDistancezz running_time_to_next_field: seir این Mapping بر اساس Fieldهایی است که قبلاً از داده واقعی Access شما بررسی کردیم. 9. Staging Model Python from dataclasses import dataclass @dataclass class StagedTrainMovement: source_record_id: str train_no: str train_name: str | None station_name: str station_number: str | None sequence: int time_in_raw: str | None time_out_raw: str | None dwell_minutes: int | None required_wait_minutes: int | None chainage: float | None max_speed: float | None source_distance: float | None source_cumulative_distance: float | None running_time_to_next: int | None 10. Time Engine این قسمت باید مستقل و بسیار تست‌شده باشد. Python MINUTES_PER_DAY = 1440 def parse_hhmm(value: str) -> int: hour, minute = map( int, value.strip().split(":"), ) if not 0 <= hour <= 23: raise ValueError( f"Invalid hour: {hour}" ) if not 0 <= minute <= 59: raise ValueError( f"Invalid minute: {minute}" ) return hour * 60 + minute و: Python def normalize_after( previous_absolute: int | None, current_hhmm: str, ) -> int: current = parse_hhmm( current_hhmm ) if previous_absolute is None: return current day = ( previous_absolute // MINUTES_PER_DAY ) candidate = ( day * MINUTES_PER_DAY + current ) while candidate < previous_absolute: candidate += MINUTES_PER_DAY return candidate مثلاً: 23:46 → 1426 00:36 → 1476 00:56 → 1496 02:19 → 1579 11. یک اصلاح مهم نسبت به نسخه قبلی در بازسازی Schedule، نباید از این منطق استفاده کنیم: Python previous = departure + running_time و سپس Arrival بعدی را دوباره از آن بسازیم، اگر Arrival Source را داریم. چون Source دو نوع Evidence دارد: time_in time_take time_out seir پس Canonical باید بتواند اختلاف‌ها را تشخیص دهد. مثلاً: ExpectedArrival i+1 ​ =Departure i ​ +seir i ​ در مقابل: SourceArrival i+1 ​ =time_in i+1 ​ اگر این دو برابر نباشند: SCHEDULE_RECONCILIATION_WARNING ایجاد می‌کنیم، نه اینکه یکی را بی‌سروصدا overwrite کنیم. این برای Production بسیار مهم است. 12. Quality Issue Python from dataclasses import dataclass @dataclass(frozen=True) class QualityIssue: severity: str rule: str record_id: str message: str 13. Quality Report Python @dataclass(frozen=True) class QualityReport: total_records: int valid_records: int warning_count: int rejected_count: int issues: tuple[ QualityIssue, ... ] status: str 14. Quality Rules حداقل قوانین: Q001 Missing TrainNo Q002 Missing StationName Q003 Invalid Sequence Q004 Duplicate Sequence Q005 Negative Dwell Q006 Invalid HH:MM Q007 Invalid Running Time Q008 Sequence Gap Q009 Time Reconciliation Difference Q010 Chainage Anomaly Q011 Missing Destination Q012 Missing Origin 15. مهم‌ترین Reconciliation برای هر دو Station متوالی: Python expected_arrival = ( departure_current + running_time_to_next ) و: Python source_arrival = ( arrival_next ) سپس: Python delta = ( source_arrival - expected_arrival ) و: delta == 0 → VERIFIED delta != 0 → WARNING / INVESTIGATION این همان جایی است که بعدها می‌توانیم بفهمیم: seir دقیقاً چیست؟ آیا dwell درست است؟ آیا Time Source خطا دارد؟ آیا delay داخل seir آمده؟ آیا زمان عملیاتی دیگری بین دو ایستگاه ثبت نشده؟ 16. Train Identity مدل: Python @dataclass(frozen=True) class TrainIdentityKey: train_no: str train_name: str | None origin_station: str destination_station: str direction: str اما نکته بسیار مهم: TrainNo = Train Identity نیست. برای Production: TrainNo + TrainName + Origin + Destination + Direction + Operating Pattern باید بررسی شود. 17. Canonicalization Pipeline باید: RawRecord ↓ StagedTrainMovement ↓ Group by Identity ↓ Order by Sequence ↓ Normalize Time ↓ Calculate Derived Fields ↓ TrainRun باشد. 18. Derived Distance Python def derive_distance( current_chainage, next_chainage, ): if ( current_chainage is None or next_chainage is None ): return None return abs( next_chainage - current_chainage ) بنابراین اگر: 157 200 باشد: Derived Distance = 43 ولی: Distance = 0 همان Source Value باقی می‌ماند. 19. seir در Canonical: seir ↓ running_time_to_next و نه: Station.seir بلکه: TrainStationCall.running_time_to_next یا در ساختار دقیق‌تر: RouteSegment.baseline_running_time در نسخه بعدی بهتر است هر دو Evidence را نگهداری کنیم: Python source_running_time_to_next derived_running_time_to_next تا اختلاف قابل Audit باشد. 20. Directed Path ساخت: Python def build_directed_path( train_run, ): stations = tuple( call.station_id for call in train_run.station_calls ) blocks = tuple( f"{a}::{b}" for a, b in zip( stations, stations[1:], ) ) sequence = tuple( call.sequence for call in train_run.station_calls ) if len(stations) != len(blocks) + 1: raise ValueError( "Invalid DirectedPath invariant" ) return DirectedPath( id=f"PATH-{train_run.id}", route_id=( f"ROUTE-" f"{train_run.origin_station_id}-" f"{train_run.destination_station_id}" ), direction=train_run.direction, station_ids=stations, block_ids=blocks, sequence=sequence, ) 21. نکته بسیار مهم درباره Block ID در نسخه Production بهتر است Block ID را این‌طور بسازیم: PhysicalBlock = unordered pair of physical stations یعنی: GAR::SAKHEH همان Block باشد برای: GAR → SAKHEH و: SAKHEH → GAR ولی Directed Movement جدا باشد. این دقیقاً با معماری V1.7/V2.5 سازگار است. 22. Baseline Schedule Python def build_baseline( train_run, ): calls = train_run.station_calls if not calls: raise ValueError( "TrainRun has no station calls" ) return BaselineSchedule( train_run_id=train_run.id, origin_departure=( calls[0].departure_minute ), destination_arrival=( calls[-1].arrival_minute ), duration_minutes=( calls[-1].arrival_minute - calls[0].departure_minute ), station_calls=calls, ) 23. Pipeline اصلی Python class ParameterizedRealDataPipeline: def __init__( self, adapter, mapping, ): self.adapter = adapter self.mapping = mapping def run(self): raw = self.adapter.read() staged = self.stage(raw) quality = self.validate( staged ) if quality.status == "FAILED": return { "status": "REJECTED", "quality": quality, } train_runs = ( self.canonicalize( staged ) ) paths = tuple( build_directed_path( train ) for train in train_runs ) baselines = tuple( build_baseline( train ) for train in train_runs ) return { "status": "SUCCESS", "quality": quality, "train_runs": train_runs, "paths": paths, "baselines": baselines, } 24. Fixture واقعی‌نما برای اینکه منتظر فایل نباشیم، Fixture را با همان Semantic واقعی می‌سازیم: Python REAL_SHAPE_FIXTURE = [ { "TrainNo": 100, "TrainName": "گار-اندیمشک1", "StationName": "GAR", "StationNumber": "001", "Sequence": 1, "time_in": "23:46", "time_take": 50, "time_out": "00:36", "RequiredWait": 50, "Kilometerage": 157, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": 20, }, { "TrainNo": 100, "TrainName": "گار-اندیمشک1", "StationName": "SAKHEH", "StationNumber": "002", "Sequence": 2, "time_in": "00:56", "time_take": 10, "time_out": "01:06", "RequiredWait": 10, "Kilometerage": 200, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": None, }, ] این داده Fixture است و نباید به‌عنوان داده واقعی جدید تلقی شود. 25. Golden Test Python def test_midnight_rollover(): result = normalize_after( 23 * 60 + 46, "00:36", ) assert result == ( 24 * 60 + 36 ) 26. Golden Test برای مسیر Python def test_directed_path(): result = pipeline.run() train = result["train_runs"][0] path = result["paths"][0] assert len( path.station_ids ) == len( path.block_ids ) + 1 assert ( path.station_ids[0] == train.origin_station_id ) assert ( path.station_ids[-1] == train.destination_station_id ) 27. Golden Test برای Distance Python def test_derived_distance(): train = result["train_runs"][0] first_call = ( train.station_calls[0] ) assert ( first_call.derived_distance == 43 ) چون: ∣200−157∣=43 28. Golden Test برای Direction برای Train 100: Python assert ( train.direction == Direction.FORWARD ) برای Train 101 که Chainage کاهش می‌یابد: Python assert ( train.direction == Direction.REVERSE ) البته در Production Direction نهایی باید با Infrastructure/Route Master نیز reconcile شود. 29. یک تغییر معماری مهم از اینجا به بعد پیشنهاد می‌کنم Direction را فقط از Chainage استنتاج نکنیم. یعنی: Chainage ↓ Evidence نه: Chainage ↓ Truth Truth باید از: Train Identity + Origin/Destination + Route + Infrastructure + Operating Direction به دست آید. Chainage فقط یک Validation Signal بسیار قوی است. 30. Output استاندارد V2.5-D.1 خروجی Pipeline: JSON { "pipeline_version": "2.5-D.1", "status": "SUCCESS", "quality": { "status": "PASSED", "total_records": 2, "valid_records": 2, "warning_count": 0, "rejected_count": 0 }, "train_runs": [], "directed_paths": [], "baseline_schedules": [] } محتوای آرایه‌ها در اجرای واقعی توسط Pipeline تولید می‌شود. 31. چیزی که عمداً هنوز انجام نمی‌دهیم در این مرحله نباید این کار را بکنیم: Access ↓ فرض SINGLE ↓ فرض Headway = 5 ↓ فرض Switch = 8 ↓ Solver چون این یعنی داده واقعی را با فرضیات مهندسی مخلوط کرده‌ایم. بلکه: Access ↓ Operational Evidence و جدا: Infrastructure Master ↓ Engineering Parameters سپس: Operational Evidence + Infrastructure Master ↓ Detailed Scheduler 32. وضعیت پروژه بعد از V2.5-D.1 در حال حاضر مسیر معماری ما این است: SOURCE │ ┌───────┴───────┐ │ │ Access Excel │ │ └───────┬───────┘ ▼ Raw Record ▼ Staging ▼ Mapping ▼ Quality Gate ▼ Identity Reconciliation ▼ Canonical TrainRun ▼ TrainStationCall[] ▼ DirectedPath ▼ Baseline Schedule │ ▼ ┌─────────────────────┐ │ Aggregate Network │ └──────────┬──────────┘ ▼ Train Allocation ▼ Formation / Wagon ▼ Locomotive ▼ V1.7 Time-Space Scheduler ▼ Independent Validation ▼ Capacity Proof و حالا مرحله بعدی کاملاً مشخص است: V2.5-E — Parameterized Infrastructure Master در آن مرحله برای اولین بار یک مدل جدی برای این‌ها می‌سازیم: Station ├── StationTrack ├── Usable Length ├── Entry/Exit ├── Crossing └── Overtaking PhysicalBlock ├── SINGLE / DOUBLE ├── Forward Running Time ├── Reverse Running Time ├── Same Direction Headway ├── Switch Time └── Clearing Time Junction ├── Movement └── Conflict Matrix OperationalWindow ├── Resource ├── Start ├── End └── Blocking و این بار هم همه این موارد را پارامتریک می‌گذاریم تا وقتی اطلاعات واقعی زیرساخت را بعداً بدهید، فقط Configuration/Data Load تغییر کند، نه Solver. به این ترتیب ما بدون فایل واقعی هم می‌توانیم تقریباً تمام Data Engine + Canonicalization Engine + Test Framework را جلو ببریم؛ وقتی فایل‌ها در دسترس قرار گرفتند، فقط مرحله Real Source Adapter Validation باقی می‌ماند. --- ## ادامه 71 — پرسش کاربر (`message-id:08d252b1-0d08-4832-8864-854e41f2195c`) بله ادامه بده لطفا --- ## ادامه 71 — پاسخ دستیار (`message-id:33f1de05-5512-4ff5-b68a-ab2af8f948d7`) حتماً. حالا وارد V2.5-E — Parameterized Infrastructure Master می‌شویم. این مرحله بسیار مهم است، چون از اینجا داده‌های قطار را به زیرساخت واقعی و قابل زمان‌بندی متصل می‌کنیم. V2.5-E — Parameterized Infrastructure Master هدف: Canonical Train Data + Infrastructure Master ↓ Directed Physical Network ↓ Resource Model ↓ Detailed Time-Space Scheduler اصل این نسخه: هیچ ویژگی زیرساختی که هنوز از داده واقعی تأیید نشده، نباید به‌عنوان واقعیت وارد Solver شود. بنابراین مثلاً: SINGLE DOUBLE Headway = 5 Switch = 8 Clearing = 2 فعلاً Parameter هستند، نه Fact. 1. مدل کلی Infrastructure ساختار پیشنهادی: InfrastructureMaster │ ├── Station │ ├── StationTrack[] │ ├── StationResource[] │ └── OperationalWindow[] │ ├── PhysicalBlock[] │ ├── Junction[] │ ├── JunctionMovement[] │ └── JunctionConflict[] │ ├── Route[] │ └── RouteSegment[] │ └── InfrastructureVersion و نسخه‌گذاری: InfrastructureVersion │ ├── Stations ├── Tracks ├── Blocks ├── Junctions ├── Routes └── Operational Rules این Version باید مستقل از DataVersion باشد. یعنی: DataVersion = نسخه داده‌های عملیاتی InfrastructureVersion = نسخه مدل زیرساخت ModelVersion = نسخه الگوریتم 2. Station Python from dataclasses import dataclass from typing import Tuple @dataclass(frozen=True) class Station: id: str code: str name: str usable_length_m: int tracks: Tuple["StationTrack", ...] = () اما usable_length_m را در Production بهتر است فقط زمانی در سطح Station نگه داریم که واقعاً مفهوم «حداکثر طول قابل استفاده» داشته باشد. محدودیت واقعی معمولاً در سطح Track است. بنابراین معیار اصلی: L train ​ ≤L track usable ​ است. 3. StationTrack Python @dataclass(frozen=True) class StationTrack: id: str station_id: str name: str usable_length_m: int bidirectional: bool = True electrified: bool = False passenger_allowed: bool = True freight_allowed: bool = True crossing_allowed: bool = True overtaking_allowed: bool = True این مدل بعداً اجازه می‌دهد مثلاً: Track T01 700 m Freight = Yes Crossing = Yes Track T02 450 m Freight = Yes Crossing = No را مدل کنیم. 4. PhysicalBlock مهم‌ترین موجودیت مسیر: Python from enum import Enum class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" @dataclass(frozen=True) class PhysicalBlock: id: str station_a_id: str station_b_id: str track_type: TrackType length_m: int | None running_time_forward_min: int | None running_time_reverse_min: int | None headway_same_direction_min: int switch_time_min: int clearing_time_min: int 5. چرا PhysicalBlock جهت ندارد؟ این نکته باید در کد حفظ شود. مثلاً: GAR ───────── SAKHEH یک Physical Block است: B001 اما دو حرکت دارد: B001 / FORWARD B001 / REVERSE پس: PhysicalBlock │ ├── DirectedMovement FORWARD │ └── DirectedMovement REVERSE 6. Block ID نباید این دو Block متفاوت تلقی شوند: GAR::SAKHEH SAKHEH::GAR برای Physical Block: Python def physical_block_id( station_a_id: str, station_b_id: str, ) -> str: a, b = sorted( [station_a_id, station_b_id] ) return f"{a}::{b}" اما توجه: این sort فقط برای تولید ID فیزیکی است. برای DirectedPath هرگز نباید از sort استفاده کنیم. 7. Directed Block Movement Python @dataclass(frozen=True) class DirectedBlock: physical_block_id: str from_station_id: str to_station_id: str direction: Direction running_time_min: int مثلاً: PhysicalBlock = GAR::SAKHEH حرکت: GAR → SAKHEH و: SAKHEH → GAR دو DirectedBlock هستند. 8. Single Track برای Single Track: GAR ───────── SAKHEH SINGLE هر دو حرکت از یک Resource استفاده می‌کنند: Python resource_id = physical_block.id بنابراین: Train A → B Train B ← A نمی‌توانند همزمان در Block باشند. 9. Double Track اگر: GAR ═════════ SAKHEH DOUBLE باشد: B001:FORWARD B001:REVERSE دو Resource هستند. پس: Python def movement_resource( block: PhysicalBlock, direction: Direction, ) -> str: if block.track_type == TrackType.SINGLE: return block.id return ( f"{block.id}:" f"{direction.value}" ) 10. Running Time اگر داده واقعی seir را داشته باشیم: GAR ↓ seir = 42 ↓ SAKHEH آن را به: RouteSegment.baseline_running_time = 42 تبدیل می‌کنیم. ولی Infrastructure Master باید بتواند زمان برنامه‌ریزی‌شده مستقل را نیز داشته باشد: Baseline Running Time ≠ Planning Running Time ≠ Minimum Feasible Running Time این تفکیک بسیار مهم است. 11. مدل بهتر Running Time Python @dataclass(frozen=True) class BlockRunningTime: physical_block_id: str direction: Direction train_type_id: str | None load_state: str | None baseline_min: int | None minimum_min: int | None maximum_min: int | None source: str در نتیجه مثلاً: Block B01 Train Type = Freight Heavy Loaded Baseline = 40 Minimum = 36 و: Empty Baseline = 30 Minimum = 27 را می‌توانیم جدا کنیم. 12. چرا این برای Capacity مهم است؟ چون اگر فقط بنویسیم: Block Running Time = 40 نمی‌توانیم سناریوهای زیر را درست بررسی کنیم: Train Heavy Train Light Loaded Empty Normal Congested Baseline Optimized پس Running Time باید یک Profile باشد. 13. Headway در ساده‌ترین مدل: H same ​ برای دو قطار هم‌جهت. و: H opposite ​ =max(H same ​ ,T switch ​ ) برای دو قطار خلاف جهت. اما در مدل Production بهتر است: Python @dataclass(frozen=True) class HeadwayRule: resource_id: str train_type_a: str | None train_type_b: str | None direction_relation: str minimum_separation_min: int چون ممکن است Headway وابسته به: Train Type Direction Signaling Block Operational Regime باشد. 14. Switch Time Switch Time را صرفاً یک عدد ثابت در کل شبکه فرض نمی‌کنیم. مدل: Python @dataclass(frozen=True) class SwitchRule: resource_id: str from_direction: Direction to_direction: Direction minimum_switch_min: int مثلاً: FORWARD → REVERSE = 8 REVERSE → FORWARD = 10 ممکن است متفاوت باشد. 15. Clearing Time پس از خروج قطار: Train Exit ↓ Track/Signal Release ↓ Resource Clear بنابراین: t clear ​ =t exit ​ +T clearing ​ و قطار بعدی از زمان Clear اجازه استفاده دارد. 16. Junction ساختار: Python @dataclass(frozen=True) class JunctionMovement: id: str junction_id: str from_resource_id: str to_resource_id: str و Conflict: Python @dataclass(frozen=True) class JunctionConflict: movement_a_id: str movement_b_id: str separation_min: int 17. چرا Junction را NoOverlap ساده نمی‌کنیم؟ چون ممکن است: Movement A با: Movement B تعارض داشته باشد، ولی: Movement C با A تعارض نداشته باشد. پس: Junction ↓ Movement Conflict Matrix بهتر از: Junction ↓ One Big NoOverlap است. 18. Operational Window Python @dataclass(frozen=True) class OperationalWindow: id: str resource_id: str start_minute: int end_minute: int kind: str blocking: bool = True مثلاً: B001 23:00 → 01:00 Maintenance Blocking Scheduler باید بتواند حرکت را: قبل از Window یا: بعد از Window قرار دهد. 19. Infrastructure Master حالا همه را یکجا می‌کنیم: Python @dataclass(frozen=True) class InfrastructureMaster: version_id: str stations: tuple[Station, ...] tracks: tuple[StationTrack, ...] blocks: tuple[PhysicalBlock, ...] running_times: tuple[BlockRunningTime, ...] junction_movements: tuple[JunctionMovement, ...] junction_conflicts: tuple[JunctionConflict, ...] headway_rules: tuple[HeadwayRule, ...] switch_rules: tuple[SwitchRule, ...] operational_windows: tuple[ OperationalWindow, ... ] 20. Configuration قابل تغییر برای اینکه بعداً اطلاعات واقعی را وارد کنیم: YAML infrastructure_version: "INF-2026-001" stations: - id: GAR code: GAR name: "GAR" - id: SAKHEH code: SAKHEH name: "SAKHEH" tracks: - id: GAR-T01 station_id: GAR name: "T01" usable_length_m: 700 bidirectional: true freight_allowed: true crossing_allowed: true blocks: - id: GAR::SAKHEH station_a_id: GAR station_b_id: SAKHEH track_type: SINGLE length_m: 43000 running_time_forward_min: 40 running_time_reverse_min: 42 headway_same_direction_min: 5 switch_time_min: 8 clearing_time_min: 2 این اعداد صرفاً نمونه پارامتری هستند. 21. Validation خود Infrastructure قبل از Solver باید Infrastructure خودش Validate شود. قوانین: INF001 Duplicate Station ID INF002 Duplicate Track ID INF003 Track references unknown Station INF004 Block references unknown Station INF005 Block references same Station INF006 Invalid Block Length INF007 Invalid Running Time INF008 Invalid Headway INF009 Invalid Switch Time INF010 Invalid Clearing Time INF011 Invalid Junction Movement INF012 Invalid Junction Conflict INF013 Invalid Operational Window INF014 Station Track Length inconsistency 22. Cross Validation با Train Data اینجا اولین اتصال جدی V2.5-D و V2.5-E اتفاق می‌افتد. مثلاً TrainRun دارد: GAR ↓ SAKHEH ↓ BAGH-YEK Infrastructure باید داشته باشد: GAR::SAKHEH SAKHEH::BAGH-YEK اگر نداشته باشد: PATH_NOT_MAPPED و این: INVALID است، نه: INFEASIBLE این تفکیک بسیار مهم است. 23. Directed Path Resolver Python class DirectedPathResolver: def resolve( self, train_run: TrainRun, infrastructure: InfrastructureMaster, ) -> DirectedPath: calls = train_run.station_calls stations = tuple( call.station_id for call in calls ) blocks = [] for current, nxt in zip( stations, stations[1:], ): block_id = physical_block_id( current, nxt, ) block = next( ( b for b in infrastructure.blocks if b.id == block_id ), None, ) if block is None: raise ValueError( f"PATH_NOT_MAPPED: {block_id}" ) blocks.append(block_id) return DirectedPath( id=f"PATH-{train_run.id}", route_id=( f"ROUTE-" f"{train_run.origin_station_id}-" f"{train_run.destination_station_id}" ), direction=train_run.direction, station_ids=stations, block_ids=tuple(blocks), sequence=tuple( c.sequence for c in calls ), ) 24. Resource Model از اینجا به بعد Scheduler نباید فقط Block را بشناسد. Resource Model: Resource │ ├── PHYSICAL_BLOCK ├── STATION_TRACK ├── JUNCTION ├── TERMINAL ├── OPERATIONAL_WINDOW ├── WAGON_POOL └── LOCOMOTIVE_POOL ولی V2.5-E تمرکز اصلی را روی: PHYSICAL_BLOCK STATION_TRACK JUNCTION OPERATIONAL_WINDOW می‌گذارد. 25. Time-Space Occupancy برای Train i و Block b: E i,b ​ ورود به Block X i,b ​ خروج از Block C i,b ​ Clear شدن Resource. و: E i,b ​ ≤X i,b ​ ≤C i,b ​ در ساده‌ترین حالت: X i,b ​ =E i,b ​ +T running ​ و: C i,b ​ =X i,b ​ +T clearing ​ 26. Station Occupancy برای Station Track: A i,s ​ Arrival D i,s ​ Departure. قطار فقط زمانی می‌تواند Track را اشغال کند که: L i ​ ≤L track ​ و برای دو قطار: D i ​ ≤A j ​ یا: D j ​ ≤A i ​ 27. Junction Conflict برای دو حرکت متعارض: End i ​ +Separation≤Start j ​ یا: End j ​ +Separation≤Start i ​ با Boolean ordering. 28. Infrastructure Feasibility حالا تعریف می‌کنیم: InfrastructureFeasible(S) اگر: Path valid AND Block valid AND Station track valid AND Train length valid AND Junction valid AND Windows valid باشد. ولی هنوز Schedule Feasible نیست. 29. سه سطح Validation این تفکیک را از اینجا رسمی می‌کنیم: Level 1 — Data Validation Source Data ↓ Quality Level 2 — Infrastructure Validation Canonical Train + Infrastructure ↓ Path/Resource Validity Level 3 — Schedule Validation TrainRun + DirectedPath + Infrastructure + Schedule ↓ Operational Feasibility پس: Data Invalid Infrastructure Invalid Schedule Infeasible سه وضعیت متفاوت هستند. 30. اثر مستقیم روی Capacity Proof این بسیار مهم است. فرض کنیم: Aggregate Upper Bound = 42 Detailed Scheduler می‌گوید: 38 = FEASIBLE ولی Infrastructure Master ناقص است. در این حالت: 38 ≠ Proven Capacity بلکه: Capacity Status = NOT_PROVEN Reason = INFRASTRUCTURE_MODEL_INCOMPLETE 31. اولین تست Golden برای Infrastructure Python def test_single_track_resource_is_shared(): block = PhysicalBlock( id="GAR::SAKHEH", station_a_id="GAR", station_b_id="SAKHEH", track_type=TrackType.SINGLE, length_m=43000, running_time_forward_min=40, running_time_reverse_min=42, headway_same_direction_min=5, switch_time_min=8, clearing_time_min=2, ) forward = movement_resource( block, Direction.FORWARD, ) reverse = movement_resource( block, Direction.REVERSE, ) assert forward == reverse 32. تست Double Track Python def test_double_track_resources_are_directional(): block = PhysicalBlock( id="GAR::SAKHEH", station_a_id="GAR", station_b_id="SAKHEH", track_type=TrackType.DOUBLE, length_m=43000, running_time_forward_min=40, running_time_reverse_min=42, headway_same_direction_min=5, switch_time_min=8, clearing_time_min=2, ) forward = movement_resource( block, Direction.FORWARD, ) reverse = movement_resource( block, Direction.REVERSE, ) assert forward != reverse 33. تست Station Length Python def compatible_station_track( train_length_m: int, track_length_m: int, ) -> bool: return ( train_length_m <= track_length_m ) و: Python assert compatible_station_track( 650, 700, ) assert not compatible_station_track( 750, 700, ) 34. اتصال به V1.7 Scheduler معماری نهایی: InfrastructureMaster ↓ DirectedPath ↓ TrainOperationalProfile ↓ DetailedSchedulingProblem ↓ V1.7 Scheduler Network Solver نباید این Constraints را دوباره بسازد. یعنی: Network Solver │ └──────→ Detailed Scheduler │ ├── Block ├── Headway ├── Switch ├── Clearing ├── Station Track ├── Junction └── Operational Window یک Scheduler، یک منطق زمان-فضا. 35. وضعیت پروژه اکنون با تکمیل V2.5-D.1 و این مرحله: STATUS Parameterized Source Model █████████░ 90% Raw/Staging/Mapping █████████░ 90% Quality Gate ████████░░ 80% Train Reconciliation ███████░░░ 70% Canonical TrainRun █████████░ 90% Directed Path █████████░ 90% Infrastructure Domain ███████░░░ 70% Physical Block ████████░░ 80% Station/Track ████████░░ 80% Junction ██████░░░░ 60% Operational Windows ███████░░░ 70% Detailed Scheduler ████████░░ 80% Network Optimization ███████░░░ 70% Real Data Execution ████░░░░░░ 40% این درصدها برآورد مهندسی وضعیت طراحی/پیاده‌سازی فعلی هستند، نه اندازه‌گیری رسمی پروژه. گام بعدی: V2.5-F حالا به نقطه‌ای رسیده‌ایم که می‌توانیم یک End-to-End Parameterized Capacity Run بسازیم: Fixture Data ↓ Canonical TrainRun ↓ Parameterized Infrastructure ↓ Demand ↓ Aggregate Upper Bound ↓ TrainRun Generation ↓ Formation Check ↓ Wagon Check ↓ Locomotive Check ↓ V1.7 Time-Space Scheduler ↓ Independent Validator ↓ F ↓ F+1 ↓ Capacity Proof ↓ Bottleneck Evidence و این مرحله، اولین جایی خواهد بود که کل زنجیره محاسبه ظرفیت واقعاً به هم متصل می‌شود؛ حتی بدون فایل واقعی، با پارامتر و Fixture قابل کنترل. --- ## ادامه 72 — پرسش کاربر (`message-id:16aed90f-a3b8-4e2e-8e7a-487ce3f009ac`) بله ادامه بده لطفا --- ## ادامه 72 — پاسخ دستیار (`message-id:567e9684-9fc4-4497-bf40-c89db326108d`) حتماً. حالا وارد مهم‌ترین بخش اجرایی تا این نقطه می‌شویم: V2.5-F — End-to-End Parameterized Capacity Run هدف این نسخه این است که دیگر فقط Data Engine و Infrastructure Model جداگانه نداشته باشیم؛ بلکه یک Run کامل از Demand تا Capacity Proof را روی داده پارامتری اجرا کنیم. اصل: Demand ↓ Aggregate Allocation ↓ TrainRun Generation ↓ Formation Feasibility ↓ Wagon Feasibility ↓ Locomotive Feasibility ↓ Detailed Time-Space Scheduling ↓ Independent Validation ↓ Capacity Search ↓ F / F+1 ↓ Capacity Proof ↓ Bottleneck Evidence 1. تعریف End-to-End Run یک Run دیگر نباید فقط capacity=20 برگرداند. Python @dataclass(frozen=True) class CapacityRunRequest: run_id: str scenario_id: str data_version_id: str infrastructure_version_id: str model_version: str planning_start: int planning_end: int demand: tuple["Demand", ...] candidates: tuple["TrainServiceCandidate", ...] solver_config: "SolverConfiguration" و نتیجه: Python @dataclass(frozen=True) class EndToEndCapacityResult: run_id: str aggregate_upper_bound: int | None detailed_feasible: int | None proven_capacity: int | None proof_status: str aggregate_status: str detailed_status: str validation_status: str train_runs: tuple["TrainRun", ...] schedules: tuple["TrainSchedule", ...] formation_results: tuple["FormationCheckResult", ...] wagon_results: tuple["WagonFeasibilityResult", ...] locomotive_results: tuple["LocomotiveFeasibilityResult", ...] bottlenecks: tuple["BottleneckEvidence", ...] conflicts: tuple["SchedulingConflictFeedback", ...] 2. Demand فعلاً Demand را ساده ولی Production-compatible تعریف می‌کنیم: Python @dataclass(frozen=True) class Demand: id: str od_pair_id: str freight_tons: float time_bucket_start: int time_bucket_end: int commodity_id: str wagon_type_id: str priority: int = 0 مثلاً: OD: GAR → ANDIMESHK Demand: 12,000 ton Wagon: W1 Time Bucket: Day 1 این مقدار پارامتر تست است. 3. Train Service Candidate Python @dataclass(frozen=True) class TrainServiceCandidate: id: str od_pair_id: str route_id: str train_type_id: str wagon_type_id: str locomotive_type_id: str freight_per_train_t: float train_length_m: int train_weight_t: int direction: str earliest_departure: int latest_arrival: int مثلاً: Freight / Train = 1,000 ton فقط برای Fixture. 4. Aggregate Solver Aggregate فقط می‌گوید: چند Train Service Candidate می‌توانیم پیشنهاد کنیم؟ متغیر: F c ​ ∈Z ≥0 ​ و Demand: c ∑ ​ Q c ​ F c ​ ≤D Objective: max c ∑ ​ Q c ​ F c ​ 5. Aggregate Upper Bound فرض کنیم: Demand = 12,000 ton Freight/Train = 1,000 ton در نتیجه: F≤12 ولی این هنوز Capacity نیست. نام صحیح: AGGREGATE_UPPER_BOUND = 12 6. TrainRun Builder Aggregate: Candidate C01 Train Count = 12 Detailed باید ببیند: TR-C01-D1-0001 TR-C01-D1-0002 ... TR-C01-D1-0012 بنابراین: Python def build_train_runs( allocation: AggregateAllocation, ) -> tuple[TrainRun, ...]: result = [] for sequence in range( 1, allocation.train_count + 1, ): result.append( TrainRun( id=( f"{allocation.candidate_id}:" f"D{allocation.time_bucket_start}:" f"{sequence:04d}" ), source_train_no=( allocation.candidate_id ), service_name=None, origin_station_id="", destination_station_id="", direction=Direction.UNKNOWN, station_calls=(), ) ) return tuple(result) در نسخه کامل، Origin/Destination/Path از Candidate گرفته می‌شود. 7. نکته مهم: Aggregate Allocation هنوز Train Schedule نیست این دو کاملاً متفاوت‌اند: Allocation: 12 trains در مقابل: Schedule: Train 1 → 08:00 Train 2 → 09:10 Train 3 → 10:20 ... Aggregate نمی‌تواند دومی را تولید کند. 8. Formation Precheck قبل از Scheduler: Python @dataclass(frozen=True) class FormationCheckResult: train_run_id: str feasible: bool wagon_count: int gross_weight_t: float train_length_m: float locomotive_count: int reasons: tuple[str, ...] قواعد: Commodity compatibility Wagon compatibility Length Weight Axle/route restriction Locomotive traction Brake capability Station siding length 9. Formation Feasibility مثلاً: L formation ​ ≤L station usable ​ و: W formation ​ ≤W route allowed ​ و: Traction loco ​ ≥Resistance(W,L,Gradient) در نسخه اولیه می‌توانیم Traction را به‌صورت پارامتر نگه داریم. 10. Wagon Precheck Python @dataclass(frozen=True) class WagonFeasibilityResult: train_run_id: str feasible: bool wagon_type_id: str required_wagons: int available_wagons: int empty_wagons_required: int reasons: tuple[str, ...] حداقل شرط: W required ​ ≤W available ​ ولی Production باید Cycle را هم لحاظ کند: W required ​ (t)≤W available ​ (t) 11. Empty Wagon Coupling برای OD: GAR → ANDIMESHK Loaded: GAR → ANDIMESHK بعد از Unload: ANDIMESHK → GAR بنابراین: Loaded Train ↓ Unload ↓ Empty Wagon ↓ Return/Reposition ↓ Next Loading اگر Empty Wagon Return نتواند به‌موقع انجام شود، ظرفیت Loaded Train هم کاهش می‌یابد. این همان چیزی است که Capacity واقعی را از یک Train Path Count ساده جدا می‌کند. 12. Locomotive Precheck Python @dataclass(frozen=True) class LocomotiveFeasibilityResult: train_run_id: str feasible: bool locomotive_type_id: str required_locomotives: int available_locomotives: int turnback_time_min: int maintenance_blocked: bool reasons: tuple[str, ...] و: L required ​ (t)≤L available ​ (t) 13. Detailed Scheduling Problem بعد از Precheck: TrainRuns + DirectedPaths + InfrastructureMaster + TrainProfiles + OperationalRules به V1.7 Scheduler داده می‌شوند. Scheduler مسئول: Precedence Running Time Dwell Headway Opposing Direction Switch Clearing Station Track Station Length Junction Operational Window Earliest Departure Latest Arrival است. 14. خروجی Scheduler Python @dataclass(frozen=True) class TrainSchedule: train_run_id: str station_calls: tuple[ "ScheduledStationCall", ... ] block_movements: tuple[ "ScheduledBlockMovement", ... ] و: Python @dataclass(frozen=True) class DetailedSchedulingResult: status: str schedules: tuple[ TrainSchedule, ... ] scheduled_train_count: int unscheduled_train_count: int conflicts: tuple[ "SchedulingConflictFeedback", ... ] solver_status: str 15. Independent Validator این بخش نباید همان Solver باشد. چون: Solver says feasible نباید به‌تنهایی کافی باشد. Validator دوباره بررسی می‌کند: Station Order Arrival/Departure Dwell Running Time Block Occupancy Single Track Conflict Headway Switch Clearing Station Track Station Length Junction Operational Window نتیجه: Python @dataclass(frozen=True) class ValidationResult: valid: bool errors: tuple[str, ...] warnings: tuple[str, ...] 16. Evaluate(F) حالا مهم‌ترین Function پروژه: Python def evaluate( train_count: int, ) -> CapacityEvaluation: aggregate = aggregate_solver.solve( train_count ) if aggregate.status != "FEASIBLE": return CapacityEvaluation( status="INFEASIBLE" ) allocation = allocation_builder.build( aggregate ) train_runs = train_builder.build( allocation ) formation = formation_engine.check( train_runs ) if not formation.all_feasible: return CapacityEvaluation( status="INFEASIBLE", reason="FORMATION" ) wagon = wagon_engine.check( train_runs ) if not wagon.all_feasible: return CapacityEvaluation( status="INFEASIBLE", reason="WAGON" ) loco = locomotive_engine.check( train_runs ) if not loco.all_feasible: return CapacityEvaluation( status="INFEASIBLE", reason="LOCOMOTIVE" ) detailed = scheduler.solve( train_runs ) if detailed.status != "FEASIBLE": return CapacityEvaluation( status=detailed.status, reason="SCHEDULING" ) validation = validator.validate( detailed ) if not validation.valid: return CapacityEvaluation( status="INVALID", reason="VALIDATION" ) return CapacityEvaluation( status="FEASIBLE", train_count=train_count, schedules=detailed.schedules, validation=validation, ) این Function در واقع قلب Capacity Engine خواهد شد. 17. Capacity Search حالا: C=max{F:Evaluate(F)=FEASIBLE} ولی یک نکته مهم: دیگر Binary Search را به‌صورت کور فرض نمی‌کنیم. چون در بعضی مدل‌های شبکه‌ای ممکن است Feasibility نسبت به F کاملاً ساده و monotonic نباشد، مخصوصاً وقتی Route Choice، Batch Regime، Empty Wagon Flow و Scheduling Regime وارد می‌شوند. پس نسخه Production: Candidate Counts ↓ Evaluate(F) ↓ FEASIBLE / INFEASIBLE / INVALID / UNKNOWN ↓ Find highest validated feasible ↓ Explicit F+1 test 18. Capacity Proof فرض: F = 17 و: Evaluate(17) → FEASIBLE Evaluate(18) → INFEASIBLE و Validator برای 17: VALID آنگاه: PROVEN CAPACITY = 17 ولی: Evaluate(18) → UNKNOWN در این حالت: Proven Capacity = None و: Proof Status = NOT_PROVEN 19. Proof Contract Python @dataclass(frozen=True) class CapacityProof: f: int f_plus_one: int f_status: str f_plus_one_status: str f_validated: bool f_plus_one_proven_infeasible: bool proof_valid: bool proof_method: str قانون: Python proof_valid = ( f_status == "FEASIBLE" and f_validated and f_plus_one_status == "INFEASIBLE" ) 20. Bottleneck Evidence اگر F=17 feasible و F=18 infeasible شد، باید علت را هم ثبت کنیم. Python @dataclass(frozen=True) class BottleneckEvidence: resource_id: str bottleneck_type: str constraint_id: str capacity_at_f: float required_at_f_plus_one: float slack_at_f: float marginal_impact: float | None affected_trains: tuple[str, ...] explanation: str مثلاً: Resource: B03 Type: INFRASTRUCTURE / OPPOSING_DIRECTION F: 17 F+1: 18 Slack: 0 Status: BINDING این متن فقط در صورتی تولید می‌شود که Evidence واقعاً از Solver/Validator آمده باشد. 21. Marginal Capacity برای هر پارامتر: ΔC=C(x+Δx)−C(x) مثلاً: Headway: 5 → 4 min یا: Station Track: 1 → 2 یا: Wagons: 100 → 120 یا: Locomotives: 5 → 6 و Run جدید می‌گیریم. 22. Scenario Engine Python @dataclass(frozen=True) class ScenarioChange: entity_type: str entity_id: str attribute: str base_value: str | None new_value: str مثلاً: PhysicalBlock B03 track_type SINGLE DOUBLE اما Scenario Engine نباید نتیجه را حدس بزند. باید: Clone Scenario ↓ Apply Change ↓ Full Re-run ↓ Compare Results 23. Result Comparison Python @dataclass(frozen=True) class ScenarioDelta: base_capacity: int | None scenario_capacity: int | None capacity_delta: int | None base_served_tons: float scenario_served_tons: float served_freight_delta: float changed_bottlenecks: tuple[str, ...] 24. End-to-End Orchestrator حالا همه چیز را به هم وصل می‌کنیم: Python class CapacityPlanningOrchestrator: def run( self, request: CapacityRunRequest, ): # 1. Aggregate aggregate = ( self.aggregate_solver.solve( request ) ) if aggregate.status != "FEASIBLE": return self.invalid_or_infeasible( aggregate ) # 2. Candidate Allocation allocation = ( self.allocation_builder.build( aggregate ) ) # 3. Formation formation = ( self.formation_engine.check( allocation ) ) if not formation.feasible: return self.capacity_failure( "FORMATION" ) # 4. Wagon wagon = ( self.wagon_engine.check( allocation ) ) if not wagon.feasible: return self.capacity_failure( "WAGON" ) # 5. Locomotive loco = ( self.locomotive_engine.check( allocation ) ) if not loco.feasible: return self.capacity_failure( "LOCOMOTIVE" ) # 6. Build TrainRuns train_runs = ( self.train_run_builder.build( allocation ) ) # 7. Detailed Scheduling detailed = ( self.scheduler.solve( train_runs ) ) if detailed.status != "FEASIBLE": return self.capacity_failure( "SCHEDULING" ) # 8. Independent Validation validation = ( self.validator.validate( detailed ) ) if not validation.valid: return self.capacity_invalid( validation ) # 9. Capacity Proof proof = ( self.proof_engine.prove( request, train_runs, ) ) # 10. Bottlenecks bottlenecks = ( self.bottleneck_engine.analyze( detailed, proof, ) ) return self.result_builder.build( aggregate=aggregate, detailed=detailed, validation=validation, proof=proof, bottlenecks=bottlenecks, ) 25. اما یک اصلاح Production مهم CapacityPlanningOrchestrator نباید خودش منطق Solver را داشته باشد. بهتر است: Orchestrator │ ├── AggregateService ├── AllocationService ├── FormationService ├── WagonService ├── LocomotiveService ├── DetailedSchedulingService ├── ValidationService ├── ProofService └── BottleneckService بنابراین Orchestrator فقط workflow coordinator است. 26. حالت Failure باید دقیق باشد ما دیگر یک: FAILED کلی نداریم. بلکه: DATA_INVALID INFRASTRUCTURE_INVALID AGGREGATE_INFEASIBLE FORMATION_INFEASIBLE WAGON_INFEASIBLE WAGON_BUFFER_INFEASIBLE LOCOMOTIVE_INFEASIBLE LOCOMOTIVE_CYCLE_INFEASIBLE SCHEDULING_INFEASIBLE VALIDATION_INVALID UNKNOWN TIME_LIMIT MODEL_INVALID داریم. این برای UI و Explanation Engine حیاتی است. 27. مثال پارامتری کامل یک سناریوی آزمایشی: OD: GAR → ANDIMESHK Demand: 10,000 ton Freight per train: 1,000 ton Candidate: C01 Infrastructure: GAR::SAKHEH = SINGLE Station Track: 700 m Train Length: 650 m Wagon Pool: 100 wagons Wagons per train: 10 Locomotive Pool: 5 Locomotives per train: 1 Headway: 5 min Switch: 8 min این فقط Test Scenario است. 28. جریان محاسبه Aggregate: 10,000/1,000=10 پس: Aggregate Upper Bound = 10 سپس: F=10 و: Formation ✓ Wagon ✓ Locomotive ? اگر فقط 5 Locomotive داشته باشیم و Cycle اجازه همزمانی ندهد: Detailed Capacity may be < 10 و اگر Scheduler نیز Single Track را محدود کند، ممکن است باز هم کاهش یابد. این دقیقاً همان چیزی است که می‌خواهیم Engine کشف کند، نه اینکه از قبل فرض کند. 29. مهم‌ترین خروجی UI در پایان یک Run باید چیزی شبیه این داشته باشیم: ┌───────────────────────────────────────────┐ │ CAPACITY RESULT │ ├───────────────────────────────────────────┤ │ Aggregate Upper Bound 10 trains │ │ Detailed Feasible 7 trains │ │ Proven Capacity 7 trains │ │ Proof Status PROVEN │ ├───────────────────────────────────────────┤ │ Freight Capacity 7,000 ton │ │ Demand 10,000 ton │ │ Unserved 3,000 ton │ ├───────────────────────────────────────────┤ │ Binding Constraints │ │ • Locomotive Cycle │ │ • Single Track B03 │ ├───────────────────────────────────────────┤ │ F Test 7 FEASIBLE │ │ F+1 Test 8 INFEASIBLE│ │ Independent Validation PASSED │ └───────────────────────────────────────────┘ این اعداد فقط نمونه‌اند؛ نباید به‌عنوان نتیجه واقعی زیرساخت شما تلقی شوند. 30. معماری‌ای که اکنون به آن رسیدیم MARKETPLACE │ ▼ DEMAND │ ▼ ┌──────────────────┐ │ AGGREGATE SOLVER │ └────────┬─────────┘ │ Aggregate Allocation │ ▼ TRAIN RUN BUILDER │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ FORMATION WAGON LOCOMOTIVE │ │ │ └───────────────┼───────────────┘ ▼ DIRECTED PATH │ ▼ DETAILED SCHEDULER │ ▼ INDEPENDENT VALIDATOR │ ┌─────────┴─────────┐ │ │ VALID INVALID │ ▼ F TEST │ ▼ F + 1 TEST │ ▼ CAPACITY PROOF │ ┌────────┴────────┐ ▼ ▼ BOTTLENECK EXPLANATION │ │ └────────┬────────┘ ▼ CAPACITY OFFER │ ▼ MARKETPLACE جایگاه فعلی پروژه با این مرحله، دیگر فقط «مدل ظرفیت» نداریم؛ یک Production Execution Chain تعریف کرده‌ایم: Source → Canonical → Infrastructure → Demand → Aggregate → Allocation → Formation → Wagon → Locomotive → Time-Space → Validation → Proof → Bottleneck → Offer مرحله بعدی را من این‌طور تعریف می‌کنم: V2.5-G — Independent Validation + Capacity Proof + Bottleneck Engine در آن مرحله سه بخش را واقعاً سخت‌گیرانه می‌کنیم: Validator مستقل از Solver Proof Engine با F/F+1 و مدیریت UNKNOWN Bottleneck Engine با Binding / Near-Binding / Structural و Marginal Impact بعد از G، تازه می‌توانیم با اطمینان بگوییم یک Run نه فقط «جواب داده»، بلکه قابل دفاع و قابل Audit است. --- ## ادامه 73 — پرسش کاربر (`message-id:41b36179-2578-4192-ba20-ecef03c357ec`) بله لطفا --- ## ادامه 73 — پاسخ دستیار (`message-id:4a75f9a6-010b-4529-b225-4529ba22e266`) حتماً. حالا وارد V2.5-G می‌شویم؛ این مرحله از نظر مهندسی شاید حتی مهم‌تر از خود Solver باشد، چون خروجی را از یک «جواب محاسباتی» به یک نتیجه قابل اعتبارسنجی، قابل اثبات و قابل Audit تبدیل می‌کند. V2.5-G — Independent Validation, Capacity Proof & Bottleneck Engine V2.5-G — Independent Validation, Capacity Proof & Bottleneck Engine 1. هدف هدف V2.5-G ایجاد سه لایه مستقل و Production-grade است: Detailed Scheduler ↓ Independent Validator ↓ Capacity Proof Engine ↓ Bottleneck & Explanation Engine اصل بنیادی: Solver مجاز است یک Schedule پیشنهاد کند؛ Validator باید مستقل از Solver صحت آن را بررسی کند؛ و تنها Proof Engine مجاز است Capacity را PROVEN اعلام کند. 2. معماری ┌─────────────────────┐ │ Detailed Scheduler │ └──────────┬──────────┘ │ Schedule Result │ ▼ ┌─────────────────────┐ │ Independent │ │ Validator │ └──────────┬──────────┘ │ Validation Result │ ┌──────────────┴──────────────┐ │ │ VALID INVALID │ │ ▼ ▼ Capacity Evaluation Result Rejected │ ▼ Capacity Proof │ ┌───────┴────────┐ ▼ ▼ F F + 1 FEASIBLE INFEASIBLE │ │ └───────┬────────┘ ▼ PROVEN │ ▼ Bottleneck Engine │ ▼ Explanation Engine 3. Feasibility Status وضعیت‌ها باید صریح باشند: from enum import Enum class FeasibilityStatus(str, Enum): FEASIBLE = "FEASIBLE" INFEASIBLE = "INFEASIBLE" INVALID = "INVALID" UNKNOWN = "UNKNOWN" TIME_LIMIT = "TIME_LIMIT" MODEL_INVALID = "MODEL_INVALID" اصل مهم: UNKNOWN != INFEASIBLE TIME_LIMIT != INFEASIBLE INVALID != INFEASIBLE این سه مورد نباید هرگز برای Proof به جای INFEASIBLE استفاده شوند. 4. Independent Validation Contract from dataclasses import dataclass @dataclass(frozen=True) class ValidationIssue: code: str severity: str entity_type: str entity_id: str message: str @dataclass(frozen=True) class ValidationResult: valid: bool errors: tuple[ValidationIssue, ...] warnings: tuple[ValidationIssue, ...] checked_constraints: int validator_version: str 5. Validator مستقل Validator نباید از مدل داخلی Solver برای نتیجه‌گیری استفاده کند. یعنی این کار ممنوع: Solver says: NoOverlap = True Validator: پس درست است. Validator باید Schedule نهایی را دوباره از روی داده‌های Canonical بررسی کند. 6. Validation Pipeline Schedule ↓ Station Sequence ↓ Arrival / Departure ↓ Dwell ↓ Running Time ↓ Block Occupancy ↓ Headway ↓ Opposing Direction ↓ Switch ↓ Clearing ↓ Station Track ↓ Station Length ↓ Junction ↓ Operational Window ↓ Train Time Window ↓ Formation ↓ Wagon ↓ Locomotive 7. Station Sequence Validator def validate_station_sequence(train_run): sequences = [ call.sequence for call in train_run.station_calls ] if sequences != sorted(sequences): return ValidationIssue( code="VAL001", severity="ERROR", entity_type="TrainRun", entity_id=train_run.id, message="Station sequence is not ordered.", ) return None در نسخه Production باید علاوه بر ترتیب، پیوستگی Sequence نیز بررسی شود. 8. Arrival / Departure شرط: [ D_{i,s}\ge A_{i,s} ] اگر: Arrival = 100 Departure = 95 نتیجه: INVALID نه: INFEASIBLE چون Schedule تولیدشده از نظر ساختاری نامعتبر است. 9. Dwell Validation [ D_{i,s}-A_{i,s} \ge T_{dwell,min} ] اگر Required Wait معتبر باشد: [ D_{i,s}-A_{i,s} \ge RequiredWait_{i,s} ] اما اگر RequiredWait هنوز PROVISIONAL باشد، سیاست Scenario باید مشخص کند که: HARD است یا: SOFT / INFORMATIONAL تا یک Field تأییدنشده بی‌دلیل Hard Constraint نشود. 10. Running Time Validation برای Block: [ X_{i,b}-E_{i,b} \ge T_{run,min} ] و در صورت استفاده از Baseline: [ T_{actual} \approx T_{baseline} ] اما Baseline الزاماً Hard Constraint نیست. بنابراین: Baseline ↓ Objective / Soft Constraint Minimum Running Time ↓ Hard Constraint 11. Single Track Validator برای هر Physical Block: Train A → B Train B ← A اگر زمان‌های Occupancy آن‌ها متداخل باشند: INVALID برای دو حرکت: [ C_i + H_{ij}\le E_j ] یا: [ C_j + H_{ji}\le E_i ] که در آن: [ H_{ij}= \begin{cases} H_{same}, & same\ direction\ \max(H_{same},T_{switch}), & opposite\ direction \end{cases} ] 12. Double Track Validator در Double Track: B01:FORWARD B01:REVERSE Resourceهای جداگانه هستند. بنابراین دو قطار مخالف جهت می‌توانند همزمان روی دو Track حرکت کنند، مشروط بر اینکه سایر منابع مشترک مانند Junction یا Station تعارض نداشته باشند. 13. Clearing Validation برای هر Block: [ C_{clear}=X+T_{clearing} ] قطار بعدی نباید قبل از: [ C_{clear} ] از Resource استفاده کند. این موضوع مخصوصاً در Single Track اهمیت دارد. 14. Station Track Validation برای هر Station Track: [ L_{train}\le L_{track}^{usable} ] و برای دو Train Occupancy: [ D_i\le A_j ] یا: [ D_j\le A_i ] اگر Track Assignment وجود نداشته باشد ولی Station Track برای Schedule ضروری باشد: INVALID نه INFEASIBLE. 15. Junction Validation برای Conflict Matrix: @dataclass(frozen=True) class JunctionConflictEvidence: junction_id: str movement_a_id: str movement_b_id: str actual_separation_min: int required_separation_min: int valid: bool شرط: [ ActualSeparation \ge RequiredSeparation ] 16. Operational Window اگر Window Blocking باشد: Window: 100 → 160 و Block Occupancy: 150 → 180 آنگاه Schedule نامعتبر است. اما اگر Scheduler عمداً قطار را قبل/بعد Window قرار دهد: 80 → 95 یا: 165 → 190 مجاز است. 17. Validator Output خروجی باید Evidence محور باشد: { "valid": false, "errors": [ { "code": "VAL-BLOCK-OPPOSING", "entity_type": "PhysicalBlock", "entity_id": "B03", "message": "Opposing movements overlap.", "train_ids": [ "TR001", "TR002" ] } ] } 18. Capacity Evaluation اکنون تابع رسمی: [ Evaluate(F) ] تعریف می‌شود: @dataclass(frozen=True) class CapacityEvaluation: requested_train_count: int status: FeasibilityStatus scheduled_train_count: int validated: bool objective_value: float | None validation: ValidationResult | None conflicts: tuple[str, ...] bottleneck_evidence: tuple[str, ...] reason: str | None 19. Proof Engine Proof Engine باید کاملاً جدا باشد. class CapacityProofEngine: def prove(self, evaluator, candidate_f): current = evaluator.evaluate(candidate_f) if current.status != FeasibilityStatus.FEASIBLE: return self.not_proven( reason="F_NOT_FEASIBLE" ) if not current.validated: return self.not_proven( reason="F_NOT_VALIDATED" ) next_eval = evaluator.evaluate( candidate_f + 1 ) if ( next_eval.status != FeasibilityStatus.INFEASIBLE ): return self.not_proven( reason="F_PLUS_ONE_NOT_PROVEN_INFEASIBLE" ) return CapacityProof( f=candidate_f, f_plus_one=candidate_f + 1, f_status="FEASIBLE", f_plus_one_status="INFEASIBLE", f_validated=True, f_plus_one_validated=False, proof_valid=True, proof_method="F_PLUS_ONE", ) 20. Proof Rule تنها این حالت: F = FEASIBLE F = VALIDATED F+1 = INFEASIBLE اجازه می‌دهد: PROVEN اعلام شود. 21. UNKNOWN اگر: F = 17 FEASIBLE F+1 = UNKNOWN خروجی: Capacity = 17 ممکن است به‌عنوان: Best Known Feasible = 17 ذخیره شود. ولی: Proven Capacity = null و: Proof Status = NOT_PROVEN 22. Time Limit اگر Solver برای F+1 به Time Limit برسد: TIME_LIMIT نباید تبدیل شود به: INFEASIBLE در UI: Best Feasible 17 Proven Capacity — Proof Status NOT PROVEN F+1 Evaluation TIME LIMIT 23. Bottleneck Engine پس از Proof: F feasible F+1 infeasible باید بفهمیم چرا. Bottleneck Engine چند منبع Evidence دارد: Conflict Evidence Binding Constraint Resource Utilization Slack Marginal Impact Failed F+1 constraints Scenario Comparison 24. Bottleneck Types class BottleneckType(str, Enum): INFRASTRUCTURE = "INFRASTRUCTURE" OPERATIONAL = "OPERATIONAL" STATION = "STATION" JUNCTION = "JUNCTION" WAGON = "WAGON" WAGON_BUFFER = "WAGON_BUFFER" WAGON_CYCLE = "WAGON_CYCLE" LOCOMOTIVE = "LOCOMOTIVE" LOCOMOTIVE_CYCLE = "LOCOMOTIVE_CYCLE" FORMATION = "FORMATION" TERMINAL = "TERMINAL" DEMAND = "DEMAND" POLICY = "POLICY" 25. سه سطح Bottleneck Binding Constraint در F فعال است: [ Slack\approx0 ] و افزایش F باعث شکست می‌شود. BINDING Near Binding مثلاً: [ Slack=1 ] ولی هنوز ظرفیت کمی باقی مانده. NEAR_BINDING Structural حتی اگر Slack مستقیم صفر نباشد، ساختار شبکه ظرفیت را محدود می‌کند. مثلاً: Single Track + Opposing Direction + Large Switch Time که باعث کاهش ظرفیت می‌شود. STRUCTURAL 26. Slack برای هر Constraint: [ Slack=Capacity-Usage ] مثلاً: Block Capacity = 100 Usage = 100 Slack = 0 پس: Binding ولی: Usage = 95 یعنی: Slack = 5 که الزاماً Bottleneck نیست. 27. Utilization به‌تنهایی Bottleneck نیست این قاعده را رسمی می‌کنیم: High Utilization ≠ Automatically Bottleneck مثلاً: Block A Utilization = 95% ممکن است هیچ محدودیت مؤثری بر Capacity نداشته باشد. در مقابل: Block B Utilization = 70% ممکن است به دلیل Opposing Direction یا Junction Conflict، عامل اصلی کاهش Capacity باشد. بنابراین Bottleneck باید بر اساس Marginal Impact + Binding Evidence + Conflict Evidence شناسایی شود. 28. Marginal Impact برای Resource g: C(g+\Delta g)-C(g) ] مثلاً: Scenario Base: Capacity = 17 Scenario: B03 Single → Double Capacity = 22 آنگاه: [ \Delta C_{B03}=+5 ] این Evidence بسیار قوی‌تری نسبت به صرفاً: B03 utilization = 98% است. 29. Bottleneck Evidence Model @dataclass(frozen=True) class BottleneckEvidence: id: str resource_id: str bottleneck_type: BottleneckType level: str utilization: float | None slack: float | None capacity_at_base: float | None capacity_after_change: float | None marginal_impact: float | None constraint_ids: tuple[str, ...] affected_train_ids: tuple[str, ...] evidence: tuple[str, ...] explanation: str 30. Interaction Between Bottlenecks ممکن است یک Bottleneck به‌تنهایی عامل اصلی نباشد. مثلاً: B03 Single Track + Station S1 only one crossing track + Locomotive cycle با هم ظرفیت را محدود کنند. پس: Bottleneck Interaction Graph لازم است. B03 │ ├── S1 Track │ └── Loco Cycle 31. Interaction Model @dataclass(frozen=True) class BottleneckInteraction: bottleneck_a: str bottleneck_b: str interaction_type: str combined_impact: float | None explanation: str 32. Explanation Engine Explanation نباید متن Static باشد. بد: "ظرفیت به دلیل محدودیت خط کاهش یافته است." خوب: "در سناریوی جاری، Block B03 از نوع Single Track عامل اصلی محدودیت است. در آزمون F=17 هیچ Slack باقی نمانده و در F=18 دو حرکت خلاف جهت با حداقل Separation موردنیاز 8 دقیقه قابل زمان‌بندی نیستند. با تغییر B03 به Double Track، در اجرای مجدد سناریو ظرفیت از 17 به 22 قطار افزایش یافته است." البته این متن فقط وقتی مجاز است که تمام این Evidence واقعاً ثبت شده باشد. 33. Explanation Data Contract @dataclass(frozen=True) class ExplanationEvidence: evidence_id: str source_type: str source_id: str metric: str | None value: str | None comparison: str | None سپس: @dataclass(frozen=True) class Explanation: title: str summary: str evidence_ids: tuple[str, ...] confidence: str 34. Capacity Result نهایی مدل نهایی: @dataclass(frozen=True) class CapacityResult: run_id: str aggregate_upper_bound: int | None best_detailed_feasible: int | None proven_capacity: int | None proof_status: str proof_id: str | None validation_status: str bottlenecks: tuple[ BottleneckEvidence, ... ] explanations: tuple[ Explanation, ... ] 35. تفاوت سه عدد کلیدی این سه عدد باید همیشه جدا نمایش داده شوند: Aggregate Upper Bound ↓ Best Detailed Feasible ↓ Proven Capacity مثلاً: Aggregate Upper Bound 42 Best Detailed Feasible 37 Proven Capacity 37 یا: Aggregate Upper Bound 42 Best Detailed Feasible 37 Proven Capacity — این دو وضعیت از نظر محصول کاملاً متفاوت‌اند. 36. Capacity Proof Viewer UI: ┌────────────────────────────────────────────┐ │ CAPACITY PROOF │ ├────────────────────────────────────────────┤ │ Aggregate Upper Bound 42 │ │ Best Detailed Feasible 37 │ │ Proven Capacity 37 │ │ │ │ F = 37 FEASIBLE │ │ Independent Validation PASSED │ │ │ │ F+1 = 38 INFEASIBLE │ │ │ │ Proof Method F+1 TEST │ │ Proof Status PROVEN │ └────────────────────────────────────────────┘ 37. وقتی Proof وجود ندارد ┌────────────────────────────────────────────┐ │ CAPACITY PROOF │ ├────────────────────────────────────────────┤ │ Aggregate Upper Bound 42 │ │ Best Detailed Feasible 37 │ │ Proven Capacity — │ │ │ │ F = 37 FEASIBLE │ │ Validation PASSED │ │ │ │ F+1 = 38 TIME_LIMIT │ │ │ │ Proof Status NOT PROVEN │ └────────────────────────────────────────────┘ 38. Capacity Offer Marketplace فقط باید ظرفیت Proven را به‌عنوان ظرفیت قطعی دریافت کند. @dataclass(frozen=True) class CapacityOffer: id: str od_pair_id: str route_id: str time_window: str train_capacity: int freight_capacity_t: float confidence: str proof_id: str status: str مثلاً: status = PROVEN confidence = HIGH اگر Proof وجود نداشته باشد: status = NOT_PROVEN و سیستم نباید آن را بدون علامت‌گذاری به‌عنوان قطعی به Marketplace عرضه کند. 39. Audit Trail برای هر Capacity Result: Run ├── DataVersion ├── InfrastructureVersion ├── Scenario ├── ModelVersion ├── SolverConfiguration ├── InputSnapshotHash ├── F Evaluation ├── F+1 Evaluation ├── Validation ├── Proof ├── Bottleneck Evidence └── Explanation بنابراین چند ماه بعد می‌توان پرسید: این ظرفیت 37 قطار دقیقاً بر اساس چه داده و چه پارامترهایی محاسبه شد؟ و سیستم باید پاسخ قابل بازسازی داشته باشد. 40. Golden Tests Test 1 — Proven F = 10 → FEASIBLE F = 11 → INFEASIBLE Validation(F) = VALID Expected: PROVEN Test 2 — Unknown F = 10 → FEASIBLE F = 11 → UNKNOWN Expected: NOT_PROVEN Test 3 — Invalid Schedule F = 10 Solver = FEASIBLE Validator = INVALID Expected: NOT_PROVEN Test 4 — Time Limit F = 10 → FEASIBLE F = 11 → TIME_LIMIT Expected: NOT_PROVEN Test 5 — F Infeasible F = 10 → INFEASIBLE Expected: No Capacity Proof Test 6 — Aggregate vs Detailed Aggregate = 42 Detailed = 37 F+1 = 38 INFEASIBLE Expected: Aggregate Upper Bound = 42 Best Detailed Feasible = 37 Proven Capacity = 37 41. Golden Test برای Bottleneck سناریو: B03 = SINGLE F=17 FEASIBLE Slack=0 F=18 INFEASIBLE B03 → DOUBLE Capacity = 22 Expected Evidence: Type: INFRASTRUCTURE Level: BINDING Marginal Impact: +5 trains 42. اصل مهم در Bottleneck Attribution اگر چند Constraint همزمان فعال باشند: B03 Station S1 Locomotive L01 نباید سیستم خودسرانه بگوید: B03 = علت اصلی بلکه باید بگوید: Primary Evidence Secondary Evidence Interaction و اگر آزمایش Marginal انجام نشده باشد: Primary Cause: NOT ESTABLISHED این موضوع برای جلوگیری از Explanation اشتباه بسیار مهم است. 43. Production Architecture ساختار نهایی: app/ ├── validation/ │ ├── schedule_validator.py │ ├── block_validator.py │ ├── station_validator.py │ ├── junction_validator.py │ ├── rolling_stock_validator.py │ └── result.py │ ├── engines/ │ ├── capacity/ │ │ ├── evaluator.py │ │ ├── search.py │ │ └── proof.py │ │ │ └── bottleneck/ │ ├── detector.py │ ├── evidence.py │ ├── marginal.py │ └── interaction.py │ └── explanation/ ├── evidence.py ├── narrative.py └── traceability.py 44. Service Boundary CapacityPlanningService │ ├── AggregateService ├── SchedulingService ├── ValidationService ├── ProofService ├── BottleneckService └── ExplanationService هر Service یک مسئولیت مشخص دارد. 45. تعریف رسمی V2.5-G V2.5-G زمانی کامل است که: ✓ Solver Schedule تولید می‌کند ✓ Independent Validator Schedule را مستقل بررسی می‌کند ✓ INVALID از INFEASIBLE جداست ✓ UNKNOWN از INFEASIBLE جداست ✓ TIME_LIMIT از INFEASIBLE جداست ✓ F باید FEASIBLE باشد ✓ F باید VALIDATED باشد ✓ F+1 باید صراحتاً INFEASIBLE باشد ✓ فقط Proof Engine PROVEN اعلام می‌کند ✓ Bottleneck بر اساس Evidence شناسایی می‌شود ✓ Slack ثبت می‌شود ✓ Marginal Impact قابل محاسبه است ✓ Bottleneck Interaction ثبت می‌شود ✓ Explanation به Evidence متصل است ✓ Capacity Offer دارای Proof Reference است ✓ همه نتایج Audit-able هستند 46. زنجیره نهایی تا V2.5-G MARKETPLACE │ ▼ DEMAND │ ▼ AGGREGATE NETWORK OPTIMIZER │ ▼ AGGREGATE ALLOCATION │ ▼ TRAIN RUN BUILDER │ ┌─────────┼─────────┐ ▼ ▼ ▼ FORMATION WAGON LOCO │ │ │ └─────────┼─────────┘ ▼ DIRECTED PATH │ ▼ DETAILED SCHEDULER │ ▼ INDEPENDENT VALIDATOR │ ▼ EVALUATE(F) │ ┌────┴────┐ ▼ ▼ F F + 1 │ │ FEASIBLE INFEASIBLE │ │ └────┬────┘ ▼ CAPACITY PROOF │ ▼ BOTTLENECK ENGINE │ ┌─────────┼─────────┐ ▼ ▼ ▼ Slack Marginal Interaction │ ▼ EXPLANATION ENGINE │ ▼ CAPACITY OFFER │ ▼ MARKETPLACE 47. نتیجه معماری از این مرحله به بعد، تعریف Capacity دیگر: Capacity = Number of Trains نیست. بلکه: [ \boxed{ C= \max \left{ F: \begin{array}{l} Aggregate(F)\ Formation(F)\ WagonCycle(F)\ LocomotiveCycle(F)\ Schedule(F)\ Validate(F) \end{array} \text{ are feasible} \right} } ] و: [ \boxed{ ProvenCapacity=F } ] فقط زمانی که: [ F;=;FEASIBLE ] و: [ Validate(F)=TRUE ] و: [ F+1=INFEASIBLE ] باشد. در نتیجه سیستم ما از یک Optimization Engine صرف، به یک Capacity Evidence & Decision Engine تبدیل می‌شود. گام بعدی پیشنهادی: V2.5-H در مرحله بعد، این معماری را به Real Access/Excel Execution Layer وصل می‌کنیم و یک Golden End-to-End Test Case بر اساس ساختار واقعی TrainNo / StationName / Sequence / time_in / time_take / time_out / RequiredWait / Kilometerage / seir می‌سازیم؛ بدون اینکه هیچ مقدار تأییدنشده‌ای را به‌عنوان پارامتر واقعی زیرساخت وارد کنیم. این مرحله همان نقطه‌ای است که زنجیره‌ی داده واقعی → مدل Canonical → Infrastructure → Scheduler → Validator → Proof را عملاً یکپارچه می‌کند. --- ## ادامه 74 — پرسش کاربر (`message-id:f172af65-42f5-4168-a60b-332f06dd61e9`) بله --- ## ادامه 74 — پاسخ دستیار (`message-id:b386da60-e250-4130-bef2-16bd062f301c`) عالی. پس می‌رویم سراغ V2.5-H — Real Access/Excel Execution & Golden End-to-End Case. این مرحله را عمداً محافظه‌کارانه طراحی می‌کنیم: هر چیزی که از داده واقعی قابل استخراج است وارد مدل می‌شود، و هر چیزی که هنوز تأیید نشده فقط به‌عنوان PROVISIONAL / UNKNOWN باقی می‌ماند. V2.5-H — Real Data Execution Layer & Golden End-to-End Case V2.5-H — Real Data Execution Layer & Golden End-to-End Case 1. هدف هدف V2.5-H اتصال واقعی زنجیره زیر است: Access / Excel ↓ Raw Records ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Identity Reconciliation ↓ Canonical TrainRun ↓ DirectedPath ↓ Infrastructure Master ↓ Train Operational Profile ↓ Formation / Wagon / Locomotive ↓ Detailed Scheduler ↓ Independent Validator ↓ Capacity Evaluation ↓ Capacity Proof ↓ Bottleneck Evidence این نسخه باید بتواند با همان Domain Model، هم Fixture و هم فایل واقعی را پردازش کند. 2. اصل کلیدی معماری باید این باشد: Real Data │ ▼ Adapter │ ▼ Canonical Model │ ├──────────────┐ ▼ ▼ Infrastructure Solver │ │ └──────┬───────┘ ▼ Schedule ▼ Validator و نه: Access ↓ Special Access Solver یا: Excel ↓ Special Excel Solver Solver نباید بداند داده از Access آمده یا Excel. 3. Source Adapter Interface from typing import Protocol from app.domain.models import RawRecord class SourceAdapter(Protocol): def read(self) -> list[RawRecord]: ... بنابراین: AccessAdapter ExcelAdapter InMemoryAdapter همه یک Contract دارند. 4. Access Adapter import pyodbc class AccessAdapter: def __init__( self, connection_string: str, table_name: str, ): self.connection_string = connection_string self.table_name = table_name def read(self) -> list[RawRecord]: with pyodbc.connect( self.connection_string ) as connection: cursor = connection.cursor() cursor.execute( f"SELECT * FROM [{self.table_name}]" ) columns = [ item[0] for item in cursor.description ] rows = cursor.fetchall() records = [] for index, row in enumerate( rows, start=1, ): records.append( RawRecord( record_id=str(index), source_system="ACCESS", source_file_id=( self.connection_string ), source_table=self.table_name, source_row_number=index, payload=dict( zip(columns, row) ), ) ) return records در Production، table_name باید از Configuration معتبر بیاید و نباید مستقیماً از ورودی کاربر به SQL وارد شود. 5. Excel Adapter import pandas as pd class ExcelAdapter: def __init__( self, path: str, sheet_name: str | int = 0, ): self.path = path self.sheet_name = sheet_name def read(self) -> list[RawRecord]: df = pd.read_excel( self.path, sheet_name=self.sheet_name, ) return [ RawRecord( record_id=str(index), source_system="EXCEL", source_file_id=self.path, source_table=None, source_row_number=index + 2, payload=row, ) for index, row in enumerate( df.to_dict( orient="records" ) ) ] 6. Source Hash هر فایل ورودی باید Hash داشته باشد. import hashlib def file_sha256(path: str) -> str: digest = hashlib.sha256() with open(path, "rb") as file: for chunk in iter( lambda: file.read(1024 * 1024), b"", ): digest.update(chunk) return digest.hexdigest() نتیجه: DataVersion ├── source_file ├── SHA256 ├── imported_at └── quality_status بنابراین اگر فایل تغییر کند: SHA256_old != SHA256_new و باید DataVersion جدید ایجاد شود. 7. Field Mapping برای Access واقعی: train_no_field: TrainNo train_name_field: TrainName station_name_field: StationName station_number_field: StationNumber sequence_field: Sequence time_in_field: time_in dwell_field: time_take time_out_field: time_out required_wait_field: RequiredWait chainage_field: Kilometerage max_speed_field: MaxSpeed distance_field: Distance cumulative_distance_field: sumDistancezz running_time_to_next_field: seir این Mapping باید Version داشته باشد. 8. Field Confidence در این مرحله Confidence را رسمی می‌کنیم: class FieldConfidence(str, Enum): VERIFIED = "VERIFIED" HIGH = "HIGH" PROVISIONAL = "PROVISIONAL" UNKNOWN = "UNKNOWN" UNTRUSTED = "UNTRUSTED" برای داده فعلی: Field Confidence Semantic TrainNo VERIFIED Train Movement Number TrainName VERIFIED Service Name StationName VERIFIED Station StationNumber VERIFIED Station Code/Number Sequence VERIFIED Station Order time_in VERIFIED Arrival time_take VERIFIED Actual Dwell time_out VERIFIED Departure Evidence RequiredWait PROVISIONAL Minimum Operational Wait Kilometerage HIGH Chainage MaxSpeed PROVISIONAL Speed Parameter Distance UNTRUSTED Source Distance sumDistancezz UNKNOWN Source Field seir VERIFIED Running Time to Next 9. اصل بسیار مهم Mapping این: seir نباید تبدیل شود به: Station.seir بلکه: TrainStationCall.running_time_to_next یا در مدل Route: RouteSegment.baseline_running_time است. 10. Midnight Normalization داده واقعی نمونه‌ای از: 23:46 00:36 00:56 02:19 دارد. بنابراین: def normalize_after( previous_absolute: int | None, current_hhmm: str, ) -> int: current = parse_hhmm( current_hhmm ) if previous_absolute is None: return current day = ( previous_absolute // 1440 ) candidate = ( day * 1440 + current ) while candidate < previous_absolute: candidate += 1440 return candidate نتیجه: 23:46 → 1426 00:36 → 1476 00:56 → 1496 02:19 → 1579 11. Time Reconciliation در داده واقعی: time_in_i + time_take_i ] و برای seir: Departure_i + seir_i ] اما نباید Source Arrival را بی‌دلیل overwrite کنیم. بنابراین: @dataclass(frozen=True) class TimeReconciliation: source_arrival: int | None expected_arrival: int | None difference_minutes: int | None status: str و: MATCH WARNING ERROR 12. Derived Distance برای دو Station: Kilometerage A = 157 Kilometerage B = 200 محاسبه: |200-157| 43 ] ولی: Distance source = 0 نباید آن را overwrite کنیم. مدل: source_distance = 0 derived_distance = 43 13. Canonicalization def canonicalize( staged_records, ) -> tuple[TrainRun, ...]: grouped = group_by_train_identity( staged_records ) result = [] for identity, records in grouped.items(): records = sorted( records, key=lambda x: x.sequence, ) calls = [] for index, record in enumerate( records ): next_record = ( records[index + 1] if index + 1 < len(records) else None ) derived_distance = None if next_record is not None: derived_distance = derive_distance( record.chainage, next_record.chainage, ) calls.append( TrainStationCall( train_run_id=identity.id, station_id=resolve_station_id( record ), station_name=record.station_name, sequence=record.sequence, arrival_minute=record.arrival_minute, departure_minute=record.departure_minute, dwell_minutes=record.dwell_minutes, required_wait_minutes=( record.required_wait_minutes ), chainage=record.chainage, source_distance=record.source_distance, derived_distance=derived_distance, running_time_to_next=( record.running_time_to_next ), ) ) result.append( build_train_run( identity, calls, ) ) return tuple(result) 14. Train Identity TrainNo به‌تنهایی کافی نیست. Identity پیشنهادی: @dataclass(frozen=True) class TrainIdentityKey: train_no: str train_name: str | None origin_station: str destination_station: str direction: str operating_pattern: str | None بنابراین: TrainNo = 100 به‌تنهایی یک Entity قابل اتکا نیست. 15. Direction Resolution Direction نباید صرفاً از شماره Train ساخته شود. اولویت: 1. Explicit Route Direction 2. Origin/Destination 3. Infrastructure Topology 4. Station Sequence 5. Chainage Evidence 6. Source Convention و Chainage فقط Evidence است. مثلاً: 157 → 674 نشانه حرکت در یک جهت است. و: 674 → 157 نشانه جهت معکوس. ولی این موضوع نباید جای Infrastructure Topology را بگیرد. 16. Directed Path برای Train 100: GAR ↓ SAKHEH ↓ ... ↓ ANDIMESHK و برای Train 101: ANDIMESHK ↓ ... ↓ SAKHEH ↓ GAR هر دو به Physical Blockهای مشترک متصل می‌شوند. مثلاً: PhysicalBlock: GAR::SAKHEH ولی: DirectedMovement: GAR → SAKHEH DirectedMovement: SAKHEH → GAR 17. Infrastructure Mapping هر Segment باید Resolve شود: def resolve_block( station_a: str, station_b: str, infrastructure, ): block_id = physical_block_id( station_a, station_b, ) for block in infrastructure.blocks: if block.id == block_id: return block return None اگر پیدا نشود: PATH_NOT_MAPPED 18. Golden Real-Shape Case برای تست، یک Fixture کوچک با Shape واقعی Access می‌سازیم. مثلاً: REAL_SHAPE_FIXTURE = [ { "TrainNo": 100, "TrainName": "گار-اندیمشک1", "StationName": "GAR", "StationNumber": "001", "Sequence": 1, "time_in": "23:46", "time_take": 50, "time_out": "00:36", "RequiredWait": 50, "Kilometerage": 157, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": 20, }, { "TrainNo": 100, "TrainName": "گار-اندیمشک1", "StationName": "SAKHEH", "StationNumber": "002", "Sequence": 2, "time_in": "00:56", "time_take": 10, "time_out": "01:06", "RequiredWait": 10, "Kilometerage": 200, "MaxSpeed": None, "Distance": 0, "sumDistancezz": 0, "seir": None, }, ] این Fixture داده واقعی جدید نیست؛ فقط Shape داده واقعی شناخته‌شده را برای Golden Test بازنمایی می‌کند. 19. Golden Infrastructure برای تست End-to-End: GAR │ │ B01 │ SINGLE │ SAKHEH مثلاً: blocks: - id: GAR::SAKHEH station_a_id: GAR station_b_id: SAKHEH track_type: SINGLE length_m: 43000 running_time_forward_min: 20 running_time_reverse_min: 20 headway_same_direction_min: 5 switch_time_min: 8 clearing_time_min: 2 عددها پارامتر Test هستند. 20. Golden Train Profile train_type: id: FREIGHT_STANDARD length_m: 650 weight_t: 900 earliest_departure: 0 latest_arrival: 3000 brake_test_minutes: 5 formation_minutes: 10 clearance_minutes: 2 21. Formation formation: wagon_type_id: WAGON_STANDARD wagon_count: 10 freight_per_wagon_t: 100 locomotive_type_id: LOCO_STANDARD locomotive_count: 1 در نتیجه: 10\times100 1000;ton ] 22. Demand demand: od_pair_id: GAR-ANDIMESHK freight_tons: 3000 wagon_type_id: WAGON_STANDARD time_bucket_start: 0 time_bucket_end: 3000 در نتیجه: [ F_{max}^{Demand}=3 ] 23. End-to-End Execution اکنون Run: RUN-001 با: DataVersion = DV-TEST-001 InfrastructureVersion = INF-TEST-001 Scenario = BASE ModelVersion = 2.5-H 24. Stage 1 — Ingestion Records Read: 2 Records Accepted: 2 Records Rejected: 0 25. Stage 2 — Quality Expected: Status: PASSED_WITH_WARNINGS چرا Warning؟ چون مثلاً: MaxSpeed = UNKNOWN Distance = UNTRUSTED sumDistancezz = UNKNOWN اما این Fields برای Golden Run استفاده نمی‌شوند. 26. Stage 3 — Canonical TrainRun Expected: TrainRun: 100 Origin: GAR Destination: SAKHEH Calls: 2 و: Direction: FORWARD 27. Stage 4 — Time Expected: GAR arrival: 1426 GAR departure: 1476 SAKHEH arrival: 1496 SAKHEH departure: 1506 بنابراین: [ D_{GAR}=1476 ] و: [ A_{SAKHEH}=1496 ] و: [ T_{run}=20 ] که با seir=20 منطبق است. 28. Stage 5 — Derived Distance [ |200-157|=43 ] پس: source_distance: 0 derived_distance: 43 و Source Data تغییر نمی‌کند. 29. Stage 6 — Directed Path PATH-100 GAR ↓ GAR::SAKHEH ↓ SAKHEH Invariant: [ 2=1+1 ] پس: VALID 30. Stage 7 — Formation Wagons: 10 Freight: 1000 ton Train Length: 650 m Locomotives: 1 اگر Station Track: 700 m باشد: [ 650\le700 ] پس: FORMATION FEASIBLE 31. Stage 8 — Detailed Scheduling Scheduler برای هر Train: GAR departure ↓ B01 Entry ↓ B01 Exit ↓ SAKHEH Arrival و: B01_{entry}+20 ] و: Exit+2 ] 32. Stage 9 — Independent Validation Validator بررسی می‌کند: ✓ Station sequence ✓ Arrival/departure ✓ Dwell ✓ Running time ✓ Block occupancy ✓ Clearing ✓ Station length ✓ No conflicting movement نتیجه: VALID 33. Stage 10 — Capacity Search چون Demand فقط 3 Train اجازه می‌دهد: Evaluate(1) Evaluate(2) Evaluate(3) و: Evaluate(4) باید به دلیل Demand: INFEASIBLE شود. بنابراین: F = 3 F+1 = 4 و: Capacity Proof = PROVEN مشروط به اینکه تمام مراحل برای F=3 مستقل Validate شده باشند. 34. نوع Bottleneck در این Golden Case: Demand محدودکننده است. بنابراین: Bottleneck Type: DEMAND نه: INFRASTRUCTURE این تست مهم است چون نشان می‌دهد Engine نباید همیشه Infrastructure را مقصر بداند. 35. Golden Result { "run_id": "RUN-001", "aggregate_upper_bound": 3, "best_detailed_feasible": 3, "proven_capacity": 3, "proof_status": "PROVEN", "validation_status": "PASSED", "bottlenecks": [ { "type": "DEMAND" } ] } این JSON صرفاً Expected Golden Result است؛ نتیجه اجرای واقعی در این محیط نیست. 36. Real File Execution Contract وقتی فایل واقعی در اختیار Runtime قرار گرفت: aaa.accdb مسیر اجرا: AccessAdapter ↓ RawRecord ↓ Staging ↓ Mapping ↓ Quality ↓ Identity ↓ Canonical و بعد: Canonical + Infrastructure Master + Scenario ↓ CapacityPlanningOrchestrator هیچ تغییری در Solver لازم نیست. 37. مهم‌ترین تفاوت Fixture و Real Execution Fixture: برای تست رفتار نرم‌افزار Real Data: برای اعتبارسنجی مدل پس: Golden Test Passed به این معنا نیست که: Railway Model Validated بلکه فقط نشان می‌دهد: Software Logic برای آن Case درست کار کرده است. 38. Real Data Acceptance Gate فایل واقعی فقط وقتی وارد Solver Production می‌شود که: Source File ↓ Hash ↓ Schema Validation ↓ Mapping Validation ↓ Quality Gate ↓ Identity Reconciliation ↓ Infrastructure Reconciliation ↓ Canonical Validation ↓ READY در غیر این صورت: REJECTED یا: READY_WITH_WARNINGS 39. Data Quality Gate پیشنهاد وضعیت: class DataReadiness(str, Enum): NOT_READY = "NOT_READY" READY_WITH_WARNINGS = ( "READY_WITH_WARNINGS" ) READY = "READY" REJECTED = "REJECTED" و: UNKNOWN semantic field نباید الزاماً کل DataVersion را Reject کند. اما اگر Field برای Constraint لازم باشد: Required Field Missing باید Reject شود. 40. مثال اگر: MaxSpeed = UNKNOWN ولی Scheduler از: seir استفاده می‌کند: READY_WITH_WARNINGS ممکن است. اما اگر: StationName = NULL باشد: REJECTED چون Path قابل ساختن نیست. 41. Infrastructure Readiness به همین شکل: InfrastructureVersion ↓ Validation ↓ Topology Complete? ↓ Block Parameters Complete? ↓ Station Tracks Complete? ↓ Junction Conflicts Complete? ↓ Operational Windows Valid? و خروجی: READY READY_WITH_WARNINGS REJECTED 42. Solver Readiness تنها زمانی Solver اجرا شود که: Data Ready AND Infrastructure Ready AND Scenario Valid AND Candidate Valid باشد. یعنی: DataReady \land InfrastructureReady \land ScenarioReady ] 43. Production Run State CREATED ↓ VALIDATING_INPUT ↓ READY ↓ SOLVING_AGGREGATE ↓ BUILDING_TRAIN_RUNS ↓ CHECKING_FORMATION ↓ CHECKING_WAGON ↓ CHECKING_LOCOMOTIVE ↓ SOLVING_DETAILED ↓ VALIDATING_RESULT ↓ PROVING_CAPACITY ↓ ANALYZING_BOTTLENECKS ↓ COMPLETED در هر مرحله Failure قابل تشخیص است. 44. Run Manifest برای Audit: @dataclass(frozen=True) class RunManifest: run_id: str data_version_id: str infrastructure_version_id: str scenario_id: str model_version: str solver_configuration_hash: str input_snapshot_hash: str created_at: str این Manifest عملاً شناسنامه Run است. 45. Definition of Done — V2.5-H ✓ Access Adapter ✓ Excel Adapter ✓ Raw/Staging ✓ Mapping ✓ Field Confidence ✓ Data Quality Gate ✓ Midnight Normalization ✓ Time Reconciliation ✓ Train Identity ✓ Canonical TrainRun ✓ DirectedPath ✓ PhysicalBlock Resolution ✓ Infrastructure Validation ✓ Formation Precheck ✓ Wagon Precheck ✓ Locomotive Precheck ✓ Detailed Scheduler Contract ✓ Independent Validator ✓ Capacity Evaluation ✓ F/F+1 Proof ✓ UNKNOWN != INFEASIBLE ✓ Bottleneck Evidence ✓ Golden End-to-End Test ✓ Run Manifest ✓ Data Lineage ✓ Capacity Offer Contract 46. نکته مهم درباره اجرای واقعی در این مرحله هنوز نباید ادعا کنیم: aaa.accdb successfully executed مگر اینکه خود فایل واقعاً در Runtime در دسترس باشد و Pipeline روی آن اجرا و خروجی Validate شده باشد. آنچه اکنون داریم: Production-ready execution contract + Parameterized implementation + Real-data-shaped Golden Test است. این تفکیک را عمداً حفظ می‌کنیم تا نرم‌افزار از نظر علمی و مهندسی قابل دفاع بماند. 47. جایگاه پروژه پس از V2.5-H REAL RAILWAY DATA │ ▼ DATA PLATFORM │ ▼ CANONICAL MODEL │ ┌────────────┴────────────┐ ▼ ▼ INFRASTRUCTURE MASTER DEMAND │ │ └────────────┬────────────┘ ▼ NETWORK OPTIMIZER │ ▼ TRAIN ALLOCATION │ ┌─────────┼─────────┐ ▼ ▼ ▼ FORMATION WAGON LOCO │ │ │ └─────────┼─────────┘ ▼ DIRECTED PATH │ ▼ TIME-SPACE SCHEDULER │ ▼ INDEPENDENT VALIDATOR │ ▼ CAPACITY ENGINE │ ┌─────┴─────┐ ▼ ▼ F F+1 │ │ └─────┬─────┘ ▼ CAPACITY PROOF │ ▼ BOTTLENECK ENGINE │ ▼ EXPLANATION ENGINE │ ▼ CAPACITY OFFER │ ▼ MARKETPLACE این نقطه، اولین Baseline کامل و قابل پیاده‌سازی End-to-End پروژه است. گام بعدی منطقی V2.5-I — Real Data Reconciliation & Infrastructure Calibration است: در آن، به‌جای صرفاً خواندن فایل، اختلاف‌های واقعی بین Access، Excel، Infrastructure Master و Schedule را سیستماتیک کشف می‌کنیم و یک Reconciliation Engine می‌سازیم تا مثلاً مشخص کند کدام TrainRun، Station Sequence، زمان حرکت، seir یا Chainage بین منابع اختلاف دارد و کدام مقدار باید وارد مدل Canonical شود. --- ## ادامه 75 — پرسش کاربر (`message-id:5782cde4-498d-434f-8f91-5b0481daf6a7`) بله --- ## ادامه 75 — پاسخ دستیار (`message-id:5efd43fa-eeae-4c36-be89-ccc30739c546`) حتماً. اینجا وارد یکی از حساس‌ترین لایه‌های پروژه می‌شویم: V2.5-I — Real Data Reconciliation & Infrastructure Calibration. این مرحله باعث می‌شود Access و Excel صرفاً «منبع داده» نباشند، بلکه اختلاف‌هایشان به‌صورت قابل ردیابی، قابل تصمیم‌گیری و قابل Audit مدیریت شود. V2.5-I — Real Data Reconciliation & Infrastructure Calibration V2.5-I — Real Data Reconciliation & Infrastructure Calibration 1. هدف هدف V2.5-I ایجاد یک لایه رسمی برای تطبیق و Calibration بین: Access Excel Infrastructure Master Operational Baseline Canonical Model است. معماری: ┌──────────────┐ │ Access │ └──────┬───────┘ │ ▼ ┌──────────────┐ │ Staging │ └──────┬───────┘ │ │ ┌──────▼───────┐ │ Reconciliation│ │ Engine │ └──────┬───────┘ │ ┌───────────┼───────────┐ ▼ ▼ ▼ Excel Infrastructure Canonical │ │ │ └───────────┴───────────┘ │ ▼ Reconciled Model │ ▼ Scheduler اصل: هیچ Source به‌صورت خودکار «حقیقت نهایی» تلقی نمی‌شود؛ ابتدا اختلاف، Confidence و Rule تصمیم‌گیری ثبت می‌شود. 2. چرا Reconciliation ضروری است؟ ممکن است Access بگوید: TrainNo = 100 Station = ARAK time_in = 12:27 و Excel بگوید: Train = 100 Station = ARAK Arrival = 12:30 این اختلاف نباید به‌صورت Silent Resolution حل شود. سیستم باید ثبت کند: Source A = 12:27 Source B = 12:30 Difference = 3 min Status = CONFLICT 3. انواع Reconciliation V2.5-I شش نوع اصلی دارد: 1. Identity Reconciliation 2. Station Reconciliation 3. Time Reconciliation 4. Route/Topology Reconciliation 5. Distance/Chainage Reconciliation 6. Infrastructure Parameter Reconciliation 4. Identity Reconciliation Identity: @dataclass(frozen=True) class TrainIdentityKey: train_no: str train_name: str | None origin_station: str destination_station: str direction: str operating_pattern: str | None ولی ممکن است Sourceها نام‌های متفاوت داشته باشند: Access: گار-اندیمشک1 Excel: Gar-Andimeshk1 Canonical: GAR_ANDIMESHK_1 این سه ممکن است یک Service باشند. 5. Identity Match سطوح Match: EXACT NORMALIZED PROBABLE AMBIGUOUS NO_MATCH مثلاً: TrainNo = 100 TrainName = گار-اندیمشک1 Origin = GAR Destination = ANDIMESHK Direction = FORWARD اگر همه منطبق باشند: EXACT اگر فقط نام Normalize شده متفاوت باشد: NORMALIZED اگر چند Candidate وجود داشته باشد: AMBIGUOUS و نباید خودکار Merge شود. 6. Normalization def normalize_station_name( value: str, ) -> str: return ( value .strip() .replace("ي", "ی") .replace("ك", "ک") .upper() ) اما Normalization نباید معنی را تغییر دهد. برای مثال: تهران تهران-راه‌آهن را نباید صرفاً با حذف کاراکترها یکی کنیم. 7. Station Identity مدل: @dataclass(frozen=True) class StationIdentityMatch: source_system: str source_station_id: str source_station_name: str canonical_station_id: str | None match_type: str confidence: str evidence: tuple[str, ...] 8. Station Mapping Table در Production یک Registry لازم است: Source System | Source ID | Source Name | Canonical ID | Confidence -------------------------------------------------------------------- ACCESS | 001 | GAR | GAR | VERIFIED ACCESS | 002 | SAKHEH | SAKHEH | VERIFIED EXCEL | GAR | Gar | GAR | HIGH این Mapping باید Version داشته باشد. 9. Time Reconciliation برای هر Station Call: @dataclass(frozen=True) class TimeEvidence: source_system: str train_run_key: str station_key: str event_type: str value_minute: int source_record_id: str Event Type: ARRIVAL DEPARTURE DWELL RUNNING_TIME 10. Time Difference اگر: Access Arrival = 12:27 Excel Arrival = 12:30 بعد از Midnight Normalization: |12:27-12:30| 3 ] و: status = CONFLICT 11. Reconciliation Rule @dataclass(frozen=True) class ReconciliationRule: id: str field: str tolerance: float priority_order: tuple[str, ...] auto_resolve: bool مثلاً: time_in: tolerance_minutes: 2 priority: - ACCESS - EXCEL auto_resolve: false حتی اگر Access اولویت بالاتری داشته باشد، Conflict بزرگ نباید بدون ثبت Evidence حذف شود. 12. وضعیت Reconciliation class ReconciliationStatus(str, Enum): MATCHED = "MATCHED" MATCHED_WITH_TOLERANCE = ( "MATCHED_WITH_TOLERANCE" ) CONFLICT = "CONFLICT" AMBIGUOUS = "AMBIGUOUS" MISSING_SOURCE = "MISSING_SOURCE" UNRESOLVED = "UNRESOLVED" RESOLVED = "RESOLVED" 13. Reconciliation Record @dataclass(frozen=True) class ReconciliationRecord: id: str entity_type: str entity_key: str field: str status: ReconciliationStatus values: dict[str, str] selected_value: str | None selected_source: str | None confidence: str rule_id: str evidence: tuple[str, ...] 14. مثال Entity: TrainRun 100 / ARAK Field: Arrival Access: 12:27 Excel: 12:30 Difference: 3 min Rule: R-TIME-001 Status: CONFLICT Selected: None تا زمانی که Resolution Policy مشخص نشود، Canonical Value نباید Silent انتخاب شود. 15. seir Reconciliation seir بسیار مهم است. در Access: Current Station ↓ seir ↓ Next Station پس: Arrival_{i+1} Departure_i ] و باید با seir مقایسه شود. 16. سه مقدار برای هر Segment: Source seir Observed running time Derived running time مثلاً: seir = 83 Observed = 84 Difference = 1 پس: MATCHED_WITH_TOLERANCE 17. Running Time Evidence @dataclass(frozen=True) class RunningTimeEvidence: from_station_id: str to_station_id: str source_running_time_min: int | None observed_running_time_min: int | None difference_min: int | None status: str 18. Chainage Reconciliation فرض: Station A = 157 Station B = 200 Derived: [ D=43 ] اگر Excel بگوید: Distance = 44 اختلاف: [ |44-43|=1 ] اما چون Distance فعلاً UNTRUSTED است، این اختلاف فقط Evidence است. 19. Chainage Anomaly اگر: A = 157 B = 200 C = 190 در یک مسیر Forward: 157 → 200 → 190 ممکن است Anomaly باشد. ولی نباید فوراً آن را Error بدانیم، چون: ممکن است Branch وجود داشته باشد. ممکن است Chainage Source اشتباه باشد. ممکن است مسیر تغییر کند. ممکن است Direction اشتباه تشخیص داده شده باشد. پس: CHAINAGE_ANOMALY ابتدا Warning است. 20. Infrastructure Reconciliation اینجا Data Operational با Infrastructure Master مقایسه می‌شود. مثلاً: Access / Operational Evidence: GAR → SAKHEH Infrastructure: GAR::SAKHEH اگر Block وجود داشته باشد: MATCHED اگر وجود نداشته باشد: INFRASTRUCTURE_MAPPING_CONFLICT 21. Track Type Reconciliation ممکن است Infrastructure Master بگوید: B03 = SINGLE ولی یک Source قدیمی بگوید: B03 = DOUBLE نباید یکی را بدون Evidence انتخاب کنیم. Source A = SINGLE Source B = DOUBLE Status = CONFLICT تا زمانی که Engineering Master تأیید کند. 22. Source of Truth Hierarchy برای Resolution باید Hierarchy رسمی داشته باشیم. پیشنهاد: 1. Approved Infrastructure Master 2. Approved Operational Master 3. Validated Access Operational Data 4. Validated Excel Schedule 5. Derived Values 6. Heuristic / Inference اما این Hierarchy به معنی حذف Sourceهای پایین‌تر نیست. همه Evidence حفظ می‌شوند. 23. Canonical Value Selection @dataclass(frozen=True) class CanonicalFieldValue: entity_id: str field: str value: str | None source: str confidence: str evidence_ids: tuple[str, ...] resolution_method: str مثلاً: Canonical: 12:27 Source: ACCESS Confidence: VERIFIED Resolution: APPROVED_SOURCE_PRIORITY 24. Calibration Reconciliation فقط اختلاف را پیدا نمی‌کند. Calibration نیز انجام می‌دهد. مثلاً اگر در 500 Segment: seir با Running Time مشاهده‌شده مقایسه شود: Mean Error = 1.8 min Median Error = 1 min P95 Error = 4 min این اطلاعات می‌تواند برای Calibration مدل استفاده شود. 25. Calibration Model @dataclass(frozen=True) class CalibrationResult: parameter_name: str sample_count: int mean_error: float median_error: float p95_error: float max_error: float recommended_adjustment: float | None confidence: str 26. ولی Calibration نباید خودکار Solver Parameter را تغییر دهد این اصل بسیار مهم است: Observed Data ↓ Calibration Analysis ↓ Recommendation ↓ Human Approval ↓ New Infrastructure/Model Version نه: Observed Data ↓ Auto-change Headway چون در غیر این صورت مدل ممکن است بدون کنترل مهندسی تغییر کند. 27. مثال Calibration برای Running Time اگر داده نشان دهد: Baseline: 40 min Observed: 43 min این لزوماً به معنی: New Running Time = 43 نیست. ممکن است علت: Dwell Congestion Temporary Restriction Operational Delay Train Type Load State باشد. بنابراین Calibration باید علت را نیز بررسی کند. 28. Delay Decomposition مدل: T_{running} + T_{dwell} + T_{operational} + T_{delay} ] در نتیجه اگر: [ Observed=45 ] و: [ Baseline=40 ] نباید فوراً بگوییم Running Time واقعی 45 است. ممکن است: [ T_{delay}=5 ] باشد. 29. Calibration Profiles برای جلوگیری از مخلوط شدن انواع قطار: Train Type Load State Direction Block Time Period Season Operational Regime مثلاً: B03 Freight Heavy Loaded Forward Night یک Profile مستقل باشد. 30. Calibration Data Model @dataclass(frozen=True) class CalibrationProfile: id: str block_id: str train_type_id: str | None load_state: str | None direction: Direction time_period: str | None sample_count: int baseline_running_time_min: float | None observed_mean_min: float | None observed_p50_min: float | None observed_p95_min: float | None 31. Infrastructure Parameter Calibration پارامترهایی که می‌توانند Calibration شوند: Running Time Headway Switch Time Clearing Time Station Dwell Terminal Processing Time Formation Time اما: Track Type Topology Station Identity Block Existence معمولاً Calibration Parameter نیستند؛ آن‌ها Master Data هستند. 32. Reconciliation Dashboard UI: ┌─────────────────────────────────────────────┐ │ DATA RECONCILIATION │ ├─────────────────────────────────────────────┤ │ Records Compared 12,450 │ │ Matched 11,820 │ │ Tolerance Match 420 │ │ Conflicts 165 │ │ Ambiguous 32 │ │ Missing Source 13 │ ├─────────────────────────────────────────────┤ │ TOP CONFLICTS │ │ │ │ TrainRun 100 / ARAK / Arrival 3 min │ │ Block B03 / TrackType CONFLICT │ │ TrainRun 101 / DORUD / Dwell 7 min │ └─────────────────────────────────────────────┘ این اعداد صرفاً نمونه UI هستند. 33. Conflict Inspector با انتخاب Conflict: ┌─────────────────────────────────────────────┐ │ RECONCILIATION CONFLICT │ ├─────────────────────────────────────────────┤ │ Entity: TrainRun 100 │ │ Station: ARAK │ │ Field: Arrival │ │ │ │ ACCESS 12:27 │ │ EXCEL 12:30 │ │ │ │ Difference 3 min │ │ Tolerance 2 min │ │ │ │ Status CONFLICT │ │ Resolution UNRESOLVED │ └─────────────────────────────────────────────┘ 34. Resolution Workflow CONFLICT ↓ Review Evidence ↓ Resolution Decision ↓ Select Source / Enter Approved Value ↓ Record Reason ↓ Reviewer ↓ Resolved ↓ New Data/Mapping Version هر Resolution باید Audit Log داشته باشد. 35. Resolution Record @dataclass(frozen=True) class ReconciliationResolution: reconciliation_id: str resolved_value: str selected_source: str | None reason: str resolved_by: str resolved_at: str approval_status: str 36. عدم تغییر Source Resolution هرگز Source را تغییر نمی‌دهد. بد: Access data → modify original صحیح: Original Source ↓ Immutable Raw ↓ Reconciliation ↓ Canonical Resolution 37. Data Lineage برای هر Canonical Field: Canonical Value ↓ Resolution ↓ Reconciliation Record ↓ Source Record ↓ Source File ↓ SHA256 بنابراین: هر مقدار در Solver باید قابل Trace تا Source باشد. 38. Reconciliation Readiness سه سطح: class ReconciliationReadiness(str, Enum): READY = "READY" READY_WITH_WARNINGS = ( "READY_WITH_WARNINGS" ) BLOCKED = "BLOCKED" مثلاً: 165 Conflict لزوماً Block نیست. ولی: Train identity ambiguous اگر برای Path ضروری باشد: BLOCKED 39. Solver Gate Solver فقط زمانی اجرا می‌شود که: DataVersion = READY InfrastructureVersion = READY Reconciliation = READY Scenario = VALID باشد. یا اگر Policy اجازه دهد: READY_WITH_WARNINGS اما Warningها باید در Run Manifest ثبت شوند. 40. Calibration Version Calibration نیز باید Version داشته باشد: CalibrationVersion │ ├── Profile B01 ├── Profile B02 ├── Profile B03 └── ... و Run: Run ├── DataVersion ├── InfrastructureVersion ├── CalibrationVersion ├── Scenario └── ModelVersion 41. Reproducibility در نتیجه فرمول کامل‌تر می‌شود: [ Result= f( DataVersion, InfrastructureVersion, CalibrationVersion, Scenario, ModelVersion, SolverConfiguration ) ] این فرمول برای Production بسیار مهم است. 42. Golden Reconciliation Test دو Source فرضی: ACCESS: Arrival = 12:27 EXCEL: Arrival = 12:30 Rule: Tolerance = 2 min Expected: Difference = 3 Status = CONFLICT Selected Value = None 43. Golden seir Test Departure = 100 seir = 20 Next Arrival = 120 Expected: Observed = 120 Expected = 120 Difference = 0 Status = MATCHED 44. Golden Chainage Test A = 157 B = 200 Expected: [ DerivedDistance=43 ] و: Source Distance بدون تغییر باقی می‌ماند. 45. Golden Identity Test ACCESS: TrainNo=100 TrainName=گار-اندیمشک1 EXCEL: TrainNo=100 TrainName=Gar-Andimeshk1 Origin=GAR Destination=ANDIMESHK Direction=FORWARD Expected: MATCHED / NORMALIZED نه دو TrainRun مستقل. 46. Golden Ambiguous Identity اگر: TrainNo = 100 دو Candidate داشته باشد: GAR → ANDIMESHK ANDIMESHK → GAR و Direction مشخص نباشد: AMBIGUOUS و Canonicalization نباید به‌صورت خودکار یکی را انتخاب کند. 47. Production Reconciliation Architecture app/ ├── reconciliation/ │ ├── identity.py │ ├── station.py │ ├── time.py │ ├── route.py │ ├── infrastructure.py │ ├── engine.py │ ├── rules.py │ ├── resolution.py │ └── report.py │ ├── calibration/ │ ├── running_time.py │ ├── headway.py │ ├── dwell.py │ ├── profile.py │ └── report.py │ └── lineage/ ├── evidence.py ├── source_reference.py └── trace.py 48. Database Tables V2.5-I جداول زیر را اضافه می‌کند: station_identity_map train_identity_map reconciliation_rule reconciliation_record reconciliation_value reconciliation_resolution calibration_version calibration_profile calibration_observation field_evidence lineage_reference 49. API GET /api/v1/reconciliation/runs/{run_id} GET /api/v1/reconciliation/conflicts GET /api/v1/reconciliation/conflicts/{id} POST /api/v1/reconciliation/conflicts/{id}/resolve GET /api/v1/calibration/profiles POST /api/v1/calibration/scenarios GET /api/v1/lineage/{entity_type}/{entity_id} 50. Run Manifest نهایی Run Manifest اکنون: Run │ ├── DataVersion │ ├── SourceFile[] │ ├── InfrastructureVersion │ ├── CalibrationVersion │ ├── Scenario │ ├── ModelVersion │ ├── SolverConfiguration │ ├── ReconciliationVersion │ └── InputSnapshotHash 51. معیار Production Readiness برای اجرای Capacity: Source Integrity ✓ Mapping Valid ✓ Identity Reconciliation ✓ Station Reconciliation ✓ Time Reconciliation ✓ Infrastructure Mapping ✓ Calibration Version ✓ Canonical Validation ✓ Scenario Validation ✓ اگر یکی از موارد Critical باشد: RUN BLOCKED 52. Definition of Done — V2.5-I ✓ Access/Excel comparison ✓ Train identity reconciliation ✓ Station identity reconciliation ✓ Time reconciliation ✓ seir vs observed running time ✓ Chainage validation ✓ Infrastructure mapping reconciliation ✓ Source priority rules ✓ Tolerance rules ✓ Conflict status ✓ Ambiguity detection ✓ Resolution workflow ✓ Audit trail ✓ Immutable source preservation ✓ Field-level lineage ✓ Calibration profiles ✓ Running-time calibration ✓ Headway calibration framework ✓ Dwell calibration framework ✓ Calibration versioning ✓ Solver readiness gate ✓ Golden reconciliation tests ✓ Production reconciliation API 53. اصل نهایی V2.5-I سه مفهوم باید از یکدیگر جدا بمانند: Reconciliation = What do the sources say? Calibration = What operational parameters do the observations suggest? Engineering Approval = What parameters are officially accepted for planning? این سه را نباید با یکدیگر مخلوط کنیم. 54. زنجیره کامل اکنون SOURCE SYSTEMS ┌─────────┴─────────┐ ▼ ▼ ACCESS EXCEL │ │ └─────────┬─────────┘ ▼ STAGING │ ▼ MAPPING │ ▼ QUALITY GATE │ ▼ RECONCILIATION │ ┌─────────┴─────────┐ ▼ ▼ CANONICAL DATA INFRASTRUCTURE │ │ └─────────┬─────────┘ ▼ CALIBRATION │ ▼ SCENARIO │ ▼ AGGREGATE OPTIMIZER │ ▼ TRAIN RUNS │ ┌─────────┼─────────┐ ▼ ▼ ▼ FORMATION WAGON LOCO │ │ │ └─────────┼─────────┘ ▼ DETAILED SCHEDULE │ ▼ INDEPENDENT VALIDATOR │ ▼ CAPACITY TEST F / F+1 │ ▼ PROOF ENGINE │ ▼ BOTTLENECK ENGINE │ ▼ EXPLANATION ENGINE │ ▼ CAPACITY OFFER │ ▼ MARKETPLACE 55. نتیجه با V2.5-I، نرم‌افزار دیگر فقط نمی‌گوید: «این Train در این مسیر حرکت می‌کند.» بلکه می‌تواند مشخص کند: این TrainRun از کدام Source آمده، با کدام رکورد دیگر تطبیق داده شده، چه زمانی در Source ثبت شده، چه مقدار از زمان آن seir بوده، چه اختلافی با Excel داشته، کدام مقدار در Canonical انتخاب شده، با کدام Infrastructure Version زمان‌بندی شده، و نهایتاً Capacity حاصل بر اساس کدام داده، پارامتر، Scenario و Proof به دست آمده است. این همان سطح Data Lineage + Reconciliation + Calibration + Operational Proof است که برای تبدیل پروژه به یک سامانه واقعی و قابل Audit ضروری است. گام بعدی بعد از این مرحله، یک خلأ مهم باقی می‌ماند: V2.5-J — Real Railway Data Contract & Production Database Schema. در J، تمام چیزهایی که تا الان به‌صورت Domain Object و فایل/Fixture تعریف کرده‌ایم را به یک Canonical PostgreSQL Schema تبدیل می‌کنیم؛ شامل TrainRun, TrainStationCall, PhysicalBlock, StationTrack, Route, Demand, Wagon, Locomotive, Scenario, DataVersion, Reconciliation, Calibration, CapacityRun, Schedule, Conflict, Proof, Bottleneck و Lineage. این مرحله عملاً مرز بین «Specification/Prototype» و «Production Software Architecture» خواهد بود. --- ## ادامه 76 — پرسش کاربر (`message-id:2e690119-8c1d-42c6-b671-e29a816ab3a2`) بله --- ## ادامه 76 — پاسخ دستیار (`message-id:5a8c6592-cf20-4b88-a496-28f9027a536f`) بسیار خوب. حالا وارد V2.5-J می‌شویم؛ یعنی جایی که Domain Model، Reconciliation و Calibration را به یک Production Database Contract تبدیل می‌کنیم. این مرحله را طوری می‌بندیم که بعداً ORM، API، Solver و UI همگی روی همین Schema سوار شوند و دوباره‌کاری ایجاد نشود. V2.5-J — Real Railway Data Contract & Production PostgreSQL Schema V2.5-J — Real Railway Data Contract & Production PostgreSQL Schema 1. هدف هدف V2.5-J تبدیل Canonical Railway Domain Model به یک Persistence Model رسمی و Production-grade است. معماری: Source Systems ↓ Raw / Staging ↓ Canonical Domain ↓ PostgreSQL ↓ Application Services ↓ Optimization Engines ↓ Validated Results ↓ API / UI / Marketplace اصل اساسی: PostgreSQL منبع منطق کسب‌وکار نیست؛ PostgreSQL منبع پایدار و Versioned برای Canonical State و Result Evidence است. Solver نباید مستقیماً SQL تولید کند یا به جداول خام Source وابسته باشد. 2. Database Schema Domains Schema را به Domainهای منطقی تقسیم می‌کنیم: source master railway rolling_stock demand planning optimization execution quality calibration result marketplace audit 3. Source Domain 3.1 data_version data_version ------------ id name source_type source_uri source_hash status quality_status created_at created_by این Entity مشخص می‌کند Solver دقیقاً روی کدام Dataset اجرا شده است. 3.2 source_file source_file ----------- id data_version_id FK file_name source_type uri sha256 file_size imported_at Relationship: DataVersion │ └── SourceFile[] 3.3 source_record برای Traceability رکورد خام: source_record ------------- id source_file_id FK source_table source_row_number source_record_key payload_json created_at payload_json باید Raw Source را بدون تغییر حفظ کند. 4. Master Data 4.1 station station ------- id code name normalized_name station_number latitude longitude usable_length_m active Constraints: UNIQUE(code) و در صورت وجود: UNIQUE(station_number) اما فقط در صورتی که Station Number در Master واقعاً Unique باشد. 5. Station Track station_track ------------- id station_id FK code usable_length_m bidirectional electrified active Constraint: [ TrainLength \le UsableLength ] در Scheduler. 6. Physical Block physical_block -------------- id station_a_id FK station_b_id FK track_type length_m running_time_forward_min running_time_reverse_min headway_same_direction_min switch_time_min clearing_time_min active نکته بسیار مهم: station_a_id station_b_id ترتیب این دو برای Identity فیزیکی نباید Direction را تعریف کند. بنابراین: GAR::ANDIMESHK یک Physical Block است. اما: GAR → ANDIMESHK یک Directed Movement است. 7. Physical Block Identity برای جلوگیری از Duplicate: canonical_block_key = min(station_a, station_b) + "::" + max(station_a, station_b) در Production بهتر است یک canonical_key جدا نگهداری شود. physical_block -------------- id canonical_key UNIQUE station_a_id station_b_id ... 8. Junction junction -------- id code name active و: junction_movement ----------------- id junction_id FK from_station_id FK to_station_id FK movement_code 9. Junction Conflict junction_conflict ----------------- id movement_a_id FK movement_b_id FK separation_time_min این جدول باید فقط Conflictهای واقعی را نگهداری کند، نه اینکه همه Movementها الزاماً با هم Conflict داشته باشند. 10. Route route ----- id code name origin_station_id FK destination_station_id FK active Route باید Entity مستقل از TrainRun باشد. 11. Route Segment route_segment ------------- id route_id FK sequence physical_block_id FK from_station_id FK to_station_id FK direction baseline_running_time_min distance_source distance_derived Constraint: UNIQUE(route_id, sequence) و: from_station → physical_block → to_station باید از نظر Topology معتبر باشد. 12. Directed Path چون Path وابسته به Direction و Route است: directed_path ------------- id route_id FK direction code و: directed_path_station ---------------------- id directed_path_id FK sequence station_id FK و: directed_path_block ------------------- id directed_path_id FK sequence physical_block_id FK from_station_id FK to_station_id FK Invariant: [ N_{stations}=N_{blocks}+1 ] 13. Train Domain 13.1 Train Type train_type ---------- id code name default_length_m default_weight_t default_speed_profile_id active 14. Train Service Train Service با TrainRun متفاوت است. train_service ------------- id code name train_type_id FK origin_station_id FK destination_station_id FK direction active 15. Operating Calendar برای جدا کردن Service از اجرای روزانه: operating_calendar ------------------ id service_id FK valid_from valid_to pattern و: operating_day ------------- id calendar_id FK operating_date 16. Train Run train_run --------- id service_id FK source_train_no operating_date operating_day_index sequence_in_bucket origin_station_id destination_station_id direction earliest_departure latest_arrival status data_version_id این Entity نشان‌دهنده یک اجرای مشخص قطار است. 17. Train Station Call train_station_call ------------------ id train_run_id FK station_id FK sequence arrival_minute departure_minute dwell_minutes required_wait_minutes chainage source_distance derived_distance running_time_to_next_min Constraint: UNIQUE(train_run_id, sequence) 18. Source Evidence for Train Station Call بهتر است Evidence مستقیماً داخل Call محدود نشود. field_evidence -------------- id entity_type entity_id field_name source_record_id FK source_value normalized_value confidence created_at این طراحی امکان چند Source را فراهم می‌کند. مثلاً: TrainRun 100 Station ARAK arrival ACCESS → 12:27 EXCEL → 12:30 هر دو Evidence باقی می‌مانند. 19. Train Block Movement Result عملیاتی: train_block_movement -------------------- id train_run_id FK sequence physical_block_id FK direction entry_minute exit_minute clear_minute station_from_id station_to_id این جدول برای Time-Space Schedule بسیار مهم است. 20. Rolling Stock — Wagon wagon_type ---------- id code name capacity_t length_m commodity_class active و: wagon ----- id wagon_type_id FK fleet_number status home_station_id available_from 21. Wagon Pool wagon_pool ---------- id wagon_type_id FK station_id FK capacity_count 22. Wagon Inventory برای وضعیت زمان‌مند: wagon_inventory --------------- id wagon_pool_id FK time_bucket available_count reserved_count maintenance_count اما برای Full Time-Expanded Solver بهتر است State اصلی در Engine نگهداری شود و DB Result/Checkpoint را ذخیره کند. 23. Wagon Requirement wagon_requirement ----------------- id demand_id FK wagon_type_id FK required_count freight_tons 24. Wagon Cycle wagon_cycle ----------- id wagon_id FK NULL wagon_type_id FK origin_station_id FK destination_station_id FK loaded_departure loaded_arrival unload_start unload_end empty_departure empty_arrival next_available_time status در نسخه Aggregate، wagon_id می‌تواند NULL باشد. در Detailed Individual Wagon Tracking، قابل پر شدن است. 25. Empty Wagon Movement empty_wagon_movement -------------------- id wagon_type_id FK from_station_id FK to_station_id FK departure_minute arrival_minute quantity movement_type Movement Type: ATTACHED_EMPTY EMPTY_FREIGHT_TRAIN REPOSITIONING 26. Locomotive locomotive_type --------------- id code name traction_capacity_t max_train_length_m double_loco_allowed active و: locomotive ---------- id locomotive_type_id FK fleet_number status home_station_id available_from 27. Locomotive Assignment locomotive_assignment --------------------- id train_run_id FK locomotive_id FK sequence role sequence امکان Double Locomotive را فراهم می‌کند. 28. Locomotive Cycle locomotive_cycle ---------------- id locomotive_id FK train_run_id FK departure_minute arrival_minute turnback_start turnback_end next_available_time 29. Operational Window operational_window ------------------ id resource_type resource_id start_minute end_minute kind blocking مثلاً: BLOCK_MAINTENANCE STATION_CLOSURE FUELING BRAKE_TEST PRAYER_OPERATION ENGINEERING_WORK 30. Demand Domain OD Pair od_pair ------- id origin_station_id FK destination_station_id FK code active 31. Demand demand ------ id od_pair_id FK commodity_code wagon_type_id FK time_bucket_start time_bucket_end freight_tons priority market_request_id 32. Market Request market_request -------------- id external_request_id customer_id created_at status Marketplace می‌تواند از این Entity شروع شود. 33. Freight Flow freight_flow ------------ id demand_id FK od_pair_id FK commodity_code freight_tons status 34. Formation train_formation --------------- id train_run_id FK train_type_id FK total_length_m total_weight_t wagon_count locomotive_count status و: train_formation_item -------------------- id formation_id FK wagon_type_id FK quantity freight_tons sequence 35. Scenario Domain scenario -------- id name base_scenario_id FK NULL data_version_id FK infrastructure_version_id calibration_version_id model_version planning_start planning_end objective_id operating_regime status created_at created_by Scenario باید Immutable باشد. تغییر Scenario: Base ↓ Clone ↓ ScenarioChange[] 36. Scenario Change scenario_change --------------- id scenario_id FK entity_type entity_id attribute base_value new_value reason مثال: entity = physical_block id = B03 attribute = track_type base = SINGLE new = DOUBLE 37. Objective objective_definition -------------------- id name و: objective_component ------------------- id objective_id FK component priority weight مثلاً: MAX_FREIGHT MIN_UNSERVED MIN_COST 38. Policy Constraint policy_constraint ----------------- id scenario_id FK constraint_type entity_type entity_id operator value hard_constraint 39. Capacity Run capacity_run ------------ id scenario_id FK data_version_id FK model_version status solver_seed solver_workers time_limit_seconds solver_configuration_hash input_snapshot_hash aggregate_upper_bound best_detailed_feasible proven_capacity created_at started_at completed_at این جدول هسته Audit Run است. 40. Network Allocation network_allocation ------------------ id run_id FK candidate_id od_pair_id FK route_id FK train_count freight_tons time_bucket_start time_bucket_end direction operating_regime 41. Network Iteration network_iteration ----------------- id run_id FK iteration_number aggregate_status detailed_status scheduled_train_count unscheduled_train_count objective_value feedback_count created_at این جدول Traceability بین Aggregate و Detailed را حفظ می‌کند. 42. Resource Usage resource_usage -------------- id run_id FK resource_type resource_id train_run_id FK NULL start_minute end_minute quantity 43. Resource Conflict resource_conflict ----------------- id run_id FK resource_type resource_id conflict_type train_a_id train_b_id required_separation_min actual_separation_min severity status 44. Binding Constraint binding_constraint ------------------ id run_id FK constraint_type resource_type resource_id slack marginal_impact evidence 45. Capacity Evaluation capacity_evaluation ------------------- id run_id FK tested_train_count status solver_status validated objective_value created_at Status: FEASIBLE INFEASIBLE INVALID UNKNOWN 46. Capacity Proof capacity_proof -------------- id run_id FK f f_plus_one f_status f_plus_one_status f_validated f_plus_one_validated proof_valid objective_name proof_method Rule: F_Validated \land (F+1)_{status}=INFEASIBLE ] 47. Proof Evidence proof_evidence -------------- id proof_id FK evidence_type entity_type entity_id description payload_json مثلاً: F = 37 Validation = PASS F+1 = 38 Solver = INFEASIBLE Binding Block = B03 Conflict = OPPOSING_DIRECTION 48. Bottleneck bottleneck ---------- id run_id FK type resource_type resource_id severity utilization slack marginal_impact explanation Types: INFRASTRUCTURE OPERATIONAL STATION JUNCTION WAGON WAGON_BUFFER WAGON_CYCLE LOCOMOTIVE LOCOMOTIVE_CYCLE FORMATION DEMAND TERMINAL POLICY 49. Capacity Profile به جای یک Capacity عددی: capacity_profile ---------------- id run_id FK od_pair_id FK NULL route_id FK NULL time_bucket_start time_bucket_end direction infrastructure_capacity operational_capacity rolling_stock_capacity transportable_capacity allocated_capacity confidence 50. Capacity Offer capacity_offer -------------- id run_id FK od_pair_id FK route_id FK time_window train_capacity freight_capacity_t confidence proof_id FK status این Entity مرز رسمی Engine و Marketplace است. 51. Reconciliation Tables reconciliation_rule reconciliation_rule ------------------- id field tolerance priority_order_json auto_resolve active reconciliation_record reconciliation_record --------------------- id entity_type entity_id field_name status selected_value selected_source confidence rule_id reconciliation_value reconciliation_value -------------------- id reconciliation_id FK source_system source_record_id FK raw_value normalized_value reconciliation_resolution reconciliation_resolution ------------------------- id reconciliation_id FK resolved_value selected_source reason resolved_by resolved_at approval_status 52. Calibration calibration_version ------------------- id name data_version_id FK status approved_by approved_at و: calibration_profile ------------------- id calibration_version_id FK block_id FK train_type_id FK load_state direction time_period sample_count baseline_running_time_min observed_mean_min observed_p50_min observed_p95_min و: calibration_observation ----------------------- id profile_id FK train_run_id FK observed_running_time_min baseline_running_time_min error_min 53. Lineage برای Traceability: lineage_reference ----------------- id target_entity_type target_entity_id target_field source_record_id transformation transformation_version confidence مثلاً: Target: TrainStationCall.running_time_to_next Source: Access.source_record 1245 Field: seir Transformation: DIRECT_MAPPING Confidence: VERIFIED 54. Audit Log audit_log --------- id user_id timestamp entity_type entity_id action old_value_json new_value_json reason scenario_id 55. Foreign Key Policy برای Master Data: ON DELETE RESTRICT برای Result Data: ON DELETE CASCADE فقط در جاهایی که Result کاملاً وابسته به Run است. مثلاً: capacity_run ↓ capacity_evaluation ↓ proof ↓ proof_evidence 56. Version Isolation هیچ Run نباید از داده‌های Versionهای مختلف به‌صورت مخفی استفاده کند. یعنی: Run-001 DataVersion = DV-10 InfrastructureVersion = INF-4 CalibrationVersion = CAL-3 اگر Infrastructure به INF-5 تغییر کرد: Run-001 نباید silently تغییر کند. باید: Run-002 ساخته شود. 57. Snapshot قبل از Solver: @dataclass(frozen=True) class InputSnapshot: data_version_id: str infrastructure_version_id: str calibration_version_id: str scenario_id: str model_version: str payload_hash: str این Snapshot باید Hash شود. 58. Database Snapshot vs Source Version این دو متفاوت‌اند: DataVersion = Which source dataset? InputSnapshot = Which exact canonical inputs did the Solver receive? این تفاوت برای Production بسیار مهم است. 59. SQLAlchemy Model Boundary ORM باید فقط Persistence را مدیریت کند: Repository ↓ SQLAlchemy ↓ PostgreSQL Domain Model نباید به SQLAlchemy وابسته باشد. بد: class TrainRun(Base): ... # domain logic صحیح: Domain TrainRun ↕ Mapper ↕ SQLAlchemy TrainRunModel 60. Repository Contract class TrainRunRepository(Protocol): def get( self, train_run_id: str, ) -> TrainRun | None: ... def save( self, train_run: TrainRun, ) -> None: ... def list_by_scenario( self, scenario_id: str, ) -> list[TrainRun]: ... Repository نباید Solver را اجرا کند. 61. Transaction Boundary Ingestion: BEGIN SourceFile RawRecords Staging Quality COMMIT Canonicalization: BEGIN Canonical Entities Lineage Reconciliation COMMIT Run: BEGIN CapacityRun COMMIT Solver نباید یک Transaction چندساعته روی Database باز نگه دارد. 62. Result Persistence Solver: Solver ↓ In-Memory Result ↓ Independent Validator ↓ Validated Result ↓ Persistence نه: Solver ↓ write directly to DB این تفکیک مانع ذخیره شدن Result نامعتبر می‌شود. 63. Result State برای Result: DRAFT VALIDATING VALID INVALID PERSISTED فقط: VALID می‌تواند وارد PERSISTED Production Result شود. 64. Database Indexes Indexes حیاتی: train_station_call(train_run_id, sequence) train_block_movement(train_run_id, sequence) route_segment(route_id, sequence) directed_path_station(directed_path_id, sequence) directed_path_block(directed_path_id, sequence) network_allocation(run_id, od_pair_id) resource_usage(run_id, resource_type, resource_id) resource_conflict(run_id, resource_id) capacity_evaluation(run_id, tested_train_count) reconciliation_record(entity_type, entity_id, field_name) lineage_reference(target_entity_type, target_entity_id) 65. Time Indexing برای Time-Space Query: resource_usage --------------- run_id resource_id start_minute end_minute Index پیشنهادی: (run_id, resource_id, start_minute) در آینده PostgreSQL range/indexهای تخصصی نیز می‌توانند اضافه شوند. 66. Data Quality Constraints Database فقط آخرین خط دفاع است. مثلاً: dwell_minutes >= 0 departure_minute >= arrival_minute sequence > 0 train_count >= 0 freight_tons >= 0 اما Quality Engine همچنان مسئول Validation معنایی است. 67. Domain Constraint vs Database Constraint مثال: [ TrainLength\le StationTrackLength ] این Constraint را صرفاً با SQL CHECK نمی‌توان به‌خوبی کنترل کرد، چون به Entity دیگر وابسته است. پس: Database: Basic Integrity Domain Validator: Business Constraint 68. Production Schema Migration ساختار: alembic/ ├── versions/ │ ├── 001_source_domain.py │ ├── 002_master_infrastructure.py │ ├── 003_train_domain.py │ ├── 004_rolling_stock.py │ ├── 005_demand.py │ ├── 006_scenario.py │ ├── 007_network.py │ ├── 008_schedule.py │ ├── 009_reconciliation.py │ ├── 010_calibration.py │ ├── 011_results.py │ └── 012_marketplace.py 69. Initial Migration Order ترتیب: 1. source 2. station 3. infrastructure 4. route 5. train 6. wagon 7. locomotive 8. demand 9. scenario 10. network 11. schedule 12. reconciliation 13. calibration 14. result 15. marketplace 16. audit این ترتیب Dependency Graph را رعایت می‌کند. 70. Production Database Boundary PostgreSQL │ ┌───────────────┼────────────────┐ ▼ ▼ ▼ Canonical Planning Results Data Data Data │ │ │ └───────────────┼────────────────┘ ▼ Application │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ Scheduler Network Marketplace 71. Data Lifecycle RAW ↓ STAGED ↓ MAPPED ↓ QUALITY_CHECKED ↓ RECONCILED ↓ CANONICAL ↓ VERSIONED ↓ READY ↓ USED_BY_RUN هیچ مرحله‌ای نباید Source مرحله قبل را overwrite کند. 72. Production Acceptance Test یک Test کامل: Access ↓ DataVersion ↓ RawRecord ↓ Mapping ↓ Quality ↓ Reconciliation ↓ Canonical TrainRun ↓ Infrastructure ↓ Scenario ↓ Aggregate Allocation ↓ TrainRun Expansion ↓ Formation ↓ Wagon Check ↓ Locomotive Check ↓ Detailed Schedule ↓ Independent Validation ↓ Capacity Evaluation ↓ F/F+1 ↓ Capacity Proof ↓ Bottleneck ↓ Capacity Offer اگر هر مرحله شکست بخورد، Run باید دلیل دقیق Failure داشته باشد. 73. Production Error Taxonomy خطاها: DATA_ERROR MAPPING_ERROR IDENTITY_ERROR TOPOLOGY_ERROR INFRASTRUCTURE_ERROR FORMATION_ERROR WAGON_INFEASIBLE LOCOMOTIVE_INFEASIBLE SCHEDULING_INFEASIBLE VALIDATION_ERROR SOLVER_UNKNOWN MODEL_INVALID TIMEOUT PROOF_NOT_ESTABLISHED این تفکیک برای UI و عملیات بسیار مهم است. 74. Capacity Status در UI و API: AGGREGATE_UPPER_BOUND DETAILED_FEASIBLE PROVEN NOT_PROVEN INVALID UNKNOWN نباید صرفاً: capacity = 37 برگردانیم. 75. API Result Contract نمونه: { "run_id": "RUN-001", "status": "COMPLETED", "capacity": { "aggregate_upper_bound": 42, "best_detailed_feasible": 37, "proven_capacity": 37, "proof_status": "PROVEN" }, "profiles": { "infrastructure": 40, "operational": 38, "rolling_stock": 37, "transportable": 45, "allocated": 35 }, "validation": { "status": "PASSED" }, "bottlenecks": [ "B03" ] } 76. نکته بسیار مهم در این Response: transportable = 45 به این معنا نیست که: 45 trains can physically operate بلکه: Demand/Transportability profile است. و: proven_capacity = 37 تنها در صورت وجود Proof معتبر قابل اعلام است. 77. Final Canonical Data Model اکنون مدل Production به این زنجیره می‌رسد: Source │ ├── DataVersion ├── SourceFile └── SourceRecord │ ▼ Reconciliation │ ▼ Canonical │ ┌────────┼─────────┐ ▼ ▼ ▼ Master Demand RollingStock │ │ │ ▼ ▼ ▼ Route Freight Formation │ │ ▼ ▼ TrainRun ←──── Wagon/Loco │ ▼ DirectedPath │ ▼ Time-Space Schedule │ ▼ Validation │ ▼ Capacity Run │ ├── Capacity Evaluation ├── Capacity Proof ├── Bottleneck └── Explanation │ ▼ Capacity Offer │ ▼ Marketplace 78. Definition of Done — V2.5-J ✓ Production PostgreSQL domain structure ✓ DataVersion persistence ✓ SourceFile persistence ✓ Raw Record lineage ✓ Station Master ✓ Station Track ✓ Physical Block ✓ Junction / Conflict Matrix ✓ Route / Route Segment ✓ Directed Path ✓ Train Service ✓ Operating Calendar ✓ TrainRun ✓ TrainStationCall ✓ TrainBlockMovement ✓ Wagon Master ✓ Wagon Pool ✓ Wagon Inventory ✓ Wagon Requirement ✓ Wagon Cycle ✓ Empty Wagon Movement ✓ Locomotive Master ✓ Locomotive Assignment ✓ Locomotive Cycle ✓ Demand / OD / Freight ✓ Formation ✓ Scenario ✓ Objective ✓ Policy ✓ Capacity Run ✓ Network Allocation ✓ Network Iteration ✓ Resource Usage ✓ Resource Conflict ✓ Binding Constraint ✓ Capacity Evaluation ✓ Capacity Proof ✓ Proof Evidence ✓ Bottleneck ✓ Capacity Profile ✓ Capacity Offer ✓ Reconciliation ✓ Calibration ✓ Field Evidence ✓ Lineage ✓ Audit Log ✓ Version Isolation ✓ Input Snapshot ✓ Repository Contracts ✓ Migration Strategy ✓ Production Acceptance Test 79. اصل معماری نهایی در این مرحله یک اصل بسیار مهم را رسمی می‌کنیم: Database وضعیت و شواهد را نگهداری می‌کند؛ Domain قوانین را تعریف می‌کند؛ Engine محاسبه می‌کند؛ Validator صحت را مستقل بررسی می‌کند؛ Proof Engine قابلیت اثبات‌شده را اعلام می‌کند. بنابراین: PostgreSQL ≠ Business Logic ORM ≠ Domain Model Solver ≠ Validator Aggregate Feasibility ≠ Operational Capacity Best Feasible Solution ≠ Proven Capacity این تفکیک‌ها پایه Production Architecture پروژه هستند. 80. جایگاه پروژه پس از V2.5-J در این نقطه پروژه دیگر صرفاً یک Railway Capacity Calculator نیست. ساختار آن تبدیل شده است به: OD-Centric + Time-Space + Multi-OD + Rolling-Stock-Constrained + Network-Aware + Scenario-Based + Versioned + Auditable + Explainable + Marketplace-Integrated و هسته محاسباتی آن: [ C_N= \max \left{ \sum_{od,r,t} Q_{od,r,t}F_{od,r,t} \right} ] مشروط بر: [ Aggregate \land DetailedSchedule \land Formation \land WagonCycle \land EmptyFlow \land LocomotiveCycle \land Station \land Junction \land Terminal \land Demand \land Policy ] و تنها پس از Independent Validation و Capacity Proof می‌توان خروجی را به‌عنوان Proven Capacity منتشر کرد. گام بعدی حالا یک قطعه بسیار مهم دیگر باقی مانده است: V2.5-K — Production Orchestration & Executable Run Pipeline. در K، این Schema را واقعاً به زنجیره اجرایی وصل می‌کنیم: Run Manager → Data Loader → Reconciliation → Scenario Materializer → Aggregate Solver → TrainRun Builder → Formation/Wagon/Loco Precheck → V1.7 Scheduler → Validator → F/F+1 Proof → Bottleneck → Explanation → Capacity Offer یعنی از اینجا به بعد دیگر صرفاً Entity و Table طراحی نمی‌کنیم؛ Lifecycle کامل یک Run قابل اجرای Production را تعریف می‌کنیم. --- ## ادامه 77 — پرسش کاربر (`message-id:8ca53f07-25c6-4220-84de-ce9b4f95c934`) بله --- ## ادامه 77 — پاسخ دستیار (`message-id:1f96063f-859f-4b35-94e0-85032e7fbc14`) عالی. در V2.5-K وارد لایه‌ای می‌شویم که تمام اجزای قبلی را به یک Production Run Pipeline واقعی و قابل کنترل متصل می‌کند. نکته کلیدی این نسخه این است که Run Manager فقط orchestration انجام می‌دهد و منطق Solver، Validator و Proof را در خودش تکرار نمی‌کند. V2.5-K — Production Orchestration & Executable Run Pipeline V2.5-K — Production Orchestration & Executable Run Pipeline 1. هدف V2.5-K مسئول تبدیل اجزای V2.5-A تا V2.5-J به یک Execution Lifecycle رسمی است. زنجیره اصلی: DataVersion ↓ Input Validation ↓ Reconciliation ↓ Scenario Materialization ↓ Aggregate Optimization ↓ TrainRun Builder ↓ Formation Check ↓ Wagon Check ↓ Locomotive Check ↓ Detailed Time-Space Scheduling ↓ Independent Validation ↓ Capacity Evaluation ↓ F / F+1 Proof ↓ Bottleneck Analysis ↓ Explanation ↓ Capacity Offer 2. اصل معماری Run Manager یک Orchestrator است، نه Solver. بنابراین: Run Manager ├── Data Service ├── Reconciliation Service ├── Scenario Service ├── Aggregate Solver ├── TrainRun Builder ├── Formation Engine ├── Wagon Engine ├── Locomotive Engine ├── Detailed Scheduler ├── Validator ├── Proof Engine ├── Bottleneck Engine └── Explanation Engine اما هیچ‌کدام در Run Manager دوباره پیاده‌سازی نمی‌شوند. 3. Run State Machine Run Lifecycle: CREATED ↓ VALIDATING_INPUT ↓ READY ↓ SOLVING_AGGREGATE ↓ BUILDING_TRAIN_RUNS ↓ CHECKING_FORMATION ↓ CHECKING_WAGON ↓ CHECKING_LOCOMOTIVE ↓ SOLVING_DETAILED ↓ VALIDATING_RESULT ↓ PROVING_CAPACITY ↓ ANALYZING_BOTTLENECKS ↓ GENERATING_EXPLANATION ↓ PUBLISHING_CAPACITY ↓ COMPLETED Failure branches: FAILED_VALIDATION FAILED_SOLVER INVALID_RESULT TIMEOUT CANCELLED PROOF_NOT_ESTABLISHED 4. Run State Contract class RunState(str, Enum): CREATED = "CREATED" VALIDATING_INPUT = "VALIDATING_INPUT" READY = "READY" SOLVING_AGGREGATE = "SOLVING_AGGREGATE" BUILDING_TRAIN_RUNS = "BUILDING_TRAIN_RUNS" CHECKING_FORMATION = "CHECKING_FORMATION" CHECKING_WAGON = "CHECKING_WAGON" CHECKING_LOCOMOTIVE = ( "CHECKING_LOCOMOTIVE" ) SOLVING_DETAILED = ( "SOLVING_DETAILED" ) VALIDATING_RESULT = ( "VALIDATING_RESULT" ) PROVING_CAPACITY = ( "PROVING_CAPACITY" ) ANALYZING_BOTTLENECKS = ( "ANALYZING_BOTTLENECKS" ) GENERATING_EXPLANATION = ( "GENERATING_EXPLANATION" ) PUBLISHING_CAPACITY = ( "PUBLISHING_CAPACITY" ) COMPLETED = "COMPLETED" FAILED_VALIDATION = ( "FAILED_VALIDATION" ) FAILED_SOLVER = "FAILED_SOLVER" INVALID_RESULT = "INVALID_RESULT" TIMEOUT = "TIMEOUT" CANCELLED = "CANCELLED" PROOF_NOT_ESTABLISHED = ( "PROOF_NOT_ESTABLISHED" ) 5. Run Context تمام Serviceها باید Context یکسان دریافت کنند. @dataclass(frozen=True) class RunContext: run_id: str scenario_id: str data_version_id: str infrastructure_version_id: str calibration_version_id: str | None model_version: str solver_configuration_hash: str input_snapshot_hash: str این Context مانع استفاده تصادفی از Versionهای مختلف می‌شود. 6. Run Manifest @dataclass(frozen=True) class RunManifest: run_id: str data_version_id: str infrastructure_version_id: str calibration_version_id: str | None scenario_id: str model_version: str solver_configuration_hash: str input_snapshot_hash: str created_at: str Run Manifest باید قبل از Solver ایجاد شود. 7. Input Validation Gate اولین مرحله: DataVersion InfrastructureVersion Scenario Calibration Demand Candidates بررسی می‌شوند. @dataclass(frozen=True) class InputValidationResult: valid: bool errors: tuple[str, ...] warnings: tuple[str, ...] اگر: valid = False Run: FAILED_VALIDATION و Solver هرگز اجرا نمی‌شود. 8. Data Readiness DataVersion ↓ Quality Gate ↓ Reconciliation Gate ↓ READY READY_WITH_WARNINGS می‌تواند با Policy مشخص اجازه Run داشته باشد. ولی Warningها باید در Run Manifest ثبت شوند. 9. Infrastructure Readiness حداقل: Station Master Station Tracks Physical Blocks Directed Paths Running Times Headway Switch Time Clearing Time Junction Conflicts Operational Windows اگر Block Path Mapping ناقص باشد: INFRASTRUCTURE_ERROR نه: SCHEDULING_INFEASIBLE 10. Scenario Materialization Scenario فقط Changeها را نگهداری می‌کند. قبل از Solver باید Materialize شود: Base Data + Scenario Changes ↓ Scenario Snapshot مثلاً: Base: B03 = SINGLE Scenario: B03 = DOUBLE Materialized: B03 = DOUBLE 11. Scenario Snapshot @dataclass(frozen=True) class ScenarioSnapshot: scenario_id: str data_version_id: str infrastructure_version_id: str calibration_version_id: str | None entities: dict[str, object] snapshot_hash: str Solver فقط Snapshot را می‌بیند. 12. چرا Materialization ضروری است؟ بدون Materialization ممکن است: Service A از Base Data بخواند و: Service B از Scenario Data. این باعث Inconsistent Run می‌شود. اصل: هر Run دقیقاً یک Canonical Input Snapshot دارد. 13. Aggregate Problem Builder class AggregateProblemBuilder: def build( self, context: RunContext, snapshot: ScenarioSnapshot, ) -> AggregateNetworkProblem: ... این Builder: Demand Candidate Shared Resources Wagon Pool Locomotive Pool Terminal Policy Objective را جمع می‌کند. 14. Candidate Generation قبل از Optimization: Demand ↓ Candidate Generator ↓ Compatibility Filters حذف Candidateهایی که: No Route No Wagon No Locomotive Train Too Long Train Too Heavy Commodity Incompatible Station Incompatible Invalid Time Window هستند. 15. Candidate Pruning Result @dataclass(frozen=True) class CandidatePruningResult: accepted: tuple[str, ...] rejected: tuple[str, ...] rejection_reasons: dict[str, tuple[str, ...]] این Result برای Explanation نیز ذخیره می‌شود. 16. Aggregate Solver aggregate_result = ( aggregate_solver.solve( aggregate_problem ) ) اگر: INFEASIBLE باشد: Run = COMPLETED Capacity = 0 Proof = not established اما اگر علت Demand صفر باشد، باید با Evidence مشخص شود. 17. Aggregate Upper Bound خروجی: Aggregate Upper Bound = 42 نباید: Proven Capacity = 42 اعلام شود. 18. TrainRun Builder مثلاً: Candidate C01 F = 5 تبدیل می‌شود به: C01:D1:0001 C01:D1:0002 C01:D1:0003 C01:D1:0004 C01:D1:0005 19. TrainRun Builder Contract @dataclass(frozen=True) class TrainRunGenerationResult: train_runs: tuple[TrainRun, ...] requested_count: int generated_count: int valid: bool errors: tuple[str, ...] Invariant: [ generated_count=requested_count ] 20. Formation Precheck برای هر TrainRun: Commodity ↓ Wagon Type ↓ Wagon Count ↓ Train Length ↓ Gross Weight ↓ Locomotive Traction ↓ Station Length 21. Formation Failure مثلاً: Train Length = 720m Station Track = 650m این: FORMATION_INFEASIBLE یا: STATION_LENGTH_INFEASIBLE است، نه Scheduler Unknown. 22. Wagon Feasibility بررسی: Required Wagons Available Wagons Initial Inventory Empty Return Buffer Cycle مثلاً: [ Required=50 ] و: [ Available=40 ] پس: WAGON_INFEASIBLE 23. Empty Wagon Check برای OD: A → B بعد از Unload: B → A و باید: EmptyOut ] رعایت شود. 24. Locomotive Check بررسی: Required Locomotives Available Locomotives Turnback Maintenance Fueling Operational Availability اگر: Required = 5 Available = 4 Result: LOCOMOTIVE_INFEASIBLE 25. Detailed Problem Builder بعد از Precheck: detailed_problem = ( detailed_problem_builder.build( train_runs=train_runs, infrastructure=snapshot.infrastructure, operational_rules=snapshot.rules, ) ) 26. Scheduler Adapter Network Engine نباید مستقیماً به CP-SAT V1.7 وابسته باشد. class DetailedSchedulerPort(Protocol): def solve( self, problem: DetailedSchedulingProblem, ) -> DetailedSchedulingResult: ... Implementation: SchedulerV17Adapter 27. Detailed Scheduler Scheduler مسئول: Precedence Running Time Dwell Headway Opposing Direction Switch Clearing Station Track Station Length Junction Operational Window Earliest Departure Latest Arrival است. 28. Single Track برای Physical Block: A ===== B هر دو Direction یک Resource دارند: BLOCK-A-B بنابراین: A → B B → A نمی‌توانند بدون Separation لازم همزمان از آن عبور کنند. 29. Double Track برای: A ===== B دو Resource: BLOCK-A-B:FORWARD BLOCK-A-B:REVERSE داریم. در نتیجه دو جهت می‌توانند هم‌زمان حرکت کنند، مگر اینکه Resource مشترک دیگری مثل Station/Junction محدود کند. 30. Independent Validator بعد از Scheduler: Schedule ↓ Independent Validator Validator نباید Solver internals را استفاده کند. بررسی: Station Sequence Block Occupancy Headway Switch Clearing Station Track Station Length Junction Windows Dwell Running Time Formation Wagon Locomotive 31. INVALID ≠ INFEASIBLE اگر Scheduler یک Schedule تولید کند ولی Validator بگوید: INVALID نباید نتیجه را: INFEASIBLE اعلام کنیم. یعنی: Solver Result = FEASIBLE Validator = INVALID Final = INVALID 32. UNKNOWN اگر Solver: UNKNOWN برگرداند: Capacity Proof = NOT PROVEN و نه: INFEASIBLE 33. Conflict Feedback اگر Detailed Scheduling شکست خورد: @dataclass(frozen=True) class SchedulingConflictFeedback: resource_id: str conflict_type: str train_ids: tuple[str, ...] required_separation_min: int | None actual_separation_min: int | None severity: str suggested_repairs: tuple[str, ...] 34. Repair Engine Repairها با اولویت: 1. Train Ordering 2. Departure Shift 3. Operating Regime 4. Batch Size 5. Route Change 6. Time Bucket Shift 7. Train Count Reduction 35. Iterative Network Solve Aggregate ↓ Train Runs ↓ Detailed Schedule ↓ Conflict ↓ Repair ↓ Aggregate Re-Optimization ↓ Detailed Schedule این چرخه تا: FEASIBLE یا: MAX_ITERATIONS ادامه دارد. 36. Network Iteration هر Iteration ثبت می‌شود: @dataclass(frozen=True) class NetworkIteration: iteration_number: int aggregate_objective: float | None requested_train_count: int scheduled_train_count: int unscheduled_train_count: int conflict_count: int status: str 37. Termination Conditions Iteration با یکی از این شرایط متوقف می‌شود: VALIDATED_FEASIBLE PROVEN_INFEASIBLE MAX_ITERATIONS TIMEOUT INVALID UNKNOWN 38. Capacity Evaluation تابع رسمی: def evaluate( train_count: int, ) -> CapacityEvaluation: ... Pipeline: F ↓ Aggregate ↓ TrainRun ↓ Formation ↓ Wagon ↓ Locomotive ↓ Detailed Schedule ↓ Validation 39. Capacity Search برای F: Evaluate(F) و سپس: Evaluate(F+1) اما فقط زمانی که: F Feasible و Validated باشد. 40. Proof Rule if ( current.status == FEASIBLE and current.validated and next.status == INFEASIBLE ): proof = PROVEN در غیر این صورت: NOT_PROVEN 41. مثال Aggregate Upper Bound = 42 Detailed F=37 Status = FEASIBLE Validation = PASS F=38 Status = INFEASIBLE نتیجه: Best Detailed Feasible = 37 Proven Capacity = 37 Proof = VALID 42. مثال دوم F=37 → FEASIBLE + VALID F=38 → UNKNOWN نتیجه: Best Detailed Feasible = 37 Proven Capacity = NULL Proof = NOT_PROVEN 43. مثال سوم F=37 → FEASIBLE F=38 → FEASIBLE پس: 37 ظرفیت Proven نیست. Search باید ادامه پیدا کند. 44. Bottleneck Engine پس از بهترین Result: Resource Usage + Conflicts + Slack + F/F+1 Evidence + Marginal Scenarios به Bottleneck تبدیل می‌شوند. 45. Bottleneck Classification BINDING NEAR_BINDING STRUCTURAL Binding زمانی که: [ Slack\approx0 ] و افزایش F باعث شکست شود. 46. Bottleneck Evidence @dataclass(frozen=True) class BottleneckEvidence: resource_id: str bottleneck_type: str level: str utilization: float | None slack: float | None capacity_at_base: float | None capacity_after_change: float | None marginal_impact: float | None affected_train_ids: tuple[str, ...] evidence: tuple[str, ...] 47. Explanation Engine Explanation نباید Template ثابت و بدون Evidence باشد. مثلاً: Capacity was limited by B03. تنها زمانی معتبر است که Evidence داشته باشد: B03 TrackType = SINGLE F = 37 F+1 = 38 Opposing Direction Conflict Slack = 0 48. Explanation ساختار: @dataclass(frozen=True) class Explanation: title: str summary: str evidence_ids: tuple[str, ...] confidence: str 49. Capacity Offer Gate Marketplace فقط زمانی Capacity Offer دریافت می‌کند که: Capacity Result ↓ Validation PASS ↓ Proof Status مشخص باشد. اگر Proof موجود نباشد: confidence = NOT_PROVEN نه PROVEN. 50. Capacity Publication Policy PROVEN ↓ Can publish as proven operational capacity NOT_PROVEN ↓ Can publish as indicative / unproven only if business policy allows INVALID ↓ Cannot publish UNKNOWN ↓ Cannot publish as proven 51. Complete Orchestrator class ProductionCapacityOrchestrator: def run( self, request: CapacityRunRequest, ) -> EndToEndCapacityResult: context = ( self.run_manager.create_context( request ) ) self.run_manager.set_state( context.run_id, RunState.VALIDATING_INPUT, ) validation = ( self.input_validator.validate( context ) ) if not validation.valid: return self.fail( context, RunState.FAILED_VALIDATION, validation.errors, ) snapshot = ( self.scenario_service.materialize( context.scenario_id ) ) aggregate = ( self.solve_aggregate( context, snapshot, ) ) if aggregate.status == "INFEASIBLE": return self.complete_without_capacity( context, aggregate, ) allocation = ( self.allocation_builder.build( aggregate ) ) detailed = ( self.solve_detailed_with_repairs( context, snapshot, allocation, ) ) if detailed.status == "INVALID": return self.invalid_result( context, detailed, ) validation = ( self.validator.validate( detailed ) ) if not validation.valid: return self.invalid_result( context, validation, ) proof = ( self.proof_engine.prove( context, detailed, ) ) bottlenecks = ( self.bottleneck_engine.analyze( context, detailed, proof, ) ) explanations = ( self.explanation_engine.generate( context, bottlenecks, proof, ) ) return self.publish( context, detailed, proof, bottlenecks, explanations, ) این کد یک Orchestration Contract است؛ تا زمانی که تمام Adapterها و Serviceهای زیرین به‌صورت واقعی پیاده‌سازی و تست نشده‌اند نباید آن را به‌عنوان اجرای Production واقعی تلقی کرد. 52. Service Boundary ساختار Production: services/ ├── run_manager.py ├── input_validation.py ├── scenario.py ├── allocation.py ├── capacity.py ├── reconciliation.py ├── calibration.py ├── result.py └── marketplace.py 53. Engine Boundary engines/ ├── aggregate_network/ ├── train_run/ ├── formation/ ├── wagon_cycle/ ├── locomotive_cycle/ ├── scheduling/ ├── network/ ├── capacity_proof/ ├── bottleneck/ └── explanation/ 54. Dependency Rule Dependency باید یک‌طرفه باشد: API ↓ Application Service ↓ Domain / Engine ↓ Infrastructure Adapter نه: Solver ↓ API ↓ Database 55. Queue Architecture برای Runهای بزرگ: API ↓ Run Manager ↓ Job Queue ↓ Optimization Worker ↓ Run Result Worker: 1 Run = 1 Job 56. Idempotency اگر کاربر یک Request را دوبار ارسال کند: same input + same scenario + same configuration سیستم باید بتواند تشخیص دهد که Run مشابه قبلاً وجود داشته است. Idempotency Key: hash( DataVersion, InfrastructureVersion, Scenario, ModelVersion, SolverConfiguration, InputSnapshot ) 57. Cancellation Run باید قابل Cancel باشد: SOLVING ↓ CANCEL_REQUESTED ↓ CANCELLED Solver Worker باید Cancellation Token داشته باشد. 58. Timeout Timeout با Infeasible فرق دارد. TIMEOUT ≠ INFEASIBLE اگر F+1 Timeout شود: Proof = NOT_PROVEN 59. Logging هر Run باید Correlation ID داشته باشد: run_id و Log: RUN_CREATED INPUT_VALIDATED SCENARIO_MATERIALIZED AGGREGATE_SOLVED TRAIN_RUNS_BUILT FORMATION_CHECKED WAGON_CHECKED LOCOMOTIVE_CHECKED DETAILED_SOLVED VALIDATION_COMPLETED PROOF_COMPLETED BOTTLENECK_ANALYZED RESULT_PERSISTED 60. Metrics حداقل Metrics: run_duration_seconds aggregate_solver_duration detailed_solver_duration validation_duration proof_duration candidate_count candidate_pruned_count train_run_count conflict_count iteration_count f_feasible f_plus_one_status proof_status 61. Observability Production باید سه لایه داشته باشد: Logs Metrics Audit Evidence Logs می‌گویند چه اتفاقی افتاد. Metrics می‌گویند عملکرد چگونه بود. Audit Evidence می‌گوید نتیجه چرا معتبر است. 62. Transaction Strategy هر مرحله Result خود را Commit می‌کند. مثلاً: Aggregate Result ↓ COMMIT Detailed Result ↓ COMMIT Validation Result ↓ COMMIT Proof ↓ COMMIT Bottleneck ↓ COMMIT در صورت Crash، Run از آخرین Checkpoint قابل بازیابی است. 63. Checkpoint @dataclass(frozen=True) class RunCheckpoint: run_id: str state: str stage: str payload_hash: str created_at: str مثلاً: RUN-001 Stage = SOLVING_DETAILED Iteration = 3 64. Recovery اگر Worker قطع شود: Database ↓ Last Checkpoint ↓ Resume / Restart Policy اما Solver State داخلی نباید فرضاً Serializable تلقی شود؛ در صورت نیاز Stage باید از Canonical Snapshot دوباره ساخته شود. 65. Production Security Run باید به User وابسته باشد: created_by requested_by approved_by و Role: ADMIN DATA_ENGINEER CAPACITY_ANALYST PLANNER MARKET_OPERATOR VIEWER 66. Audit تغییر: Scenario B03: SINGLE → DOUBLE باید داشته باشد: User Timestamp Old Value New Value Reason Scenario 67. End-to-End Production API POST /api/v1/runs ایجاد Run. GET /api/v1/runs/{run_id} وضعیت. POST /api/v1/runs/{run_id}/cancel لغو. GET /api/v1/runs/{run_id}/iterations Iterationها. GET /api/v1/runs/{run_id}/schedule Schedule. GET /api/v1/runs/{run_id}/validation Validation. GET /api/v1/runs/{run_id}/proof Proof. GET /api/v1/runs/{run_id}/bottlenecks Bottleneck. GET /api/v1/runs/{run_id}/explanations Explanation. GET /api/v1/runs/{run_id}/capacity-offer Marketplace Offer. 68. API State Response { "run_id": "RUN-001", "state": "SOLVING_DETAILED", "progress": { "stage": "DETAILED_SCHEDULING", "iteration": 3, "max_iterations": 10 } } 69. Production Acceptance Test سناریوی کامل: 1. Load Access 2. Load Excel 3. Create DataVersion 4. Quality Gate 5. Reconciliation 6. Create InfrastructureVersion 7. Validate Infrastructure 8. Create Scenario 9. Materialize Snapshot 10. Create Run 11. Candidate Generation 12. Aggregate Solve 13. TrainRun Expansion 14. Formation Check 15. Wagon Check 16. Locomotive Check 17. Detailed Schedule 18. Independent Validation 19. F Evaluation 20. F+1 Evaluation 21. Capacity Proof 22. Bottleneck 23. Explanation 24. Capacity Offer 25. Persist Result 70. Golden E2E Case برای Fixture تستی: Demand = 3000 t Freight/Train = 1000 t Aggregate: [ F_A=3 ] Detailed: 3 trains → FEASIBLE Validation: PASS F+1: 4 trains → INFEASIBLE Proof: PROVEN این اعداد صرفاً مربوط به Golden Fixture هستند و نتیجه‌ای درباره داده واقعی راه‌آهن محسوب نمی‌شوند. 71. Golden Failure Cases Case A — Unknown F=3 → FEASIBLE F=4 → UNKNOWN Expected: PROOF_NOT_ESTABLISHED Case B — Invalid Scheduler = FEASIBLE Validator = INVALID Expected: INVALID_RESULT Case C — Infrastructure Missing Path Block = NOT_MAPPED Expected: FAILED_VALIDATION Case D — Wagon Failure Required = 50 Available = 40 Expected: WAGON_INFEASIBLE Case E — Locomotive Failure Required = 5 Available = 4 Expected: LOCOMOTIVE_INFEASIBLE 72. Production Definition of Done ✓ Run State Machine ✓ Run Context ✓ Run Manifest ✓ Input Validation Gate ✓ Scenario Materialization ✓ Candidate Generation ✓ Aggregate Optimization ✓ TrainRun Expansion ✓ Formation Precheck ✓ Wagon Precheck ✓ Empty Wagon Check ✓ Locomotive Precheck ✓ Detailed Scheduler Adapter ✓ V1.7 Scheduler Integration ✓ Independent Validation ✓ Conflict Feedback ✓ Iterative Repair ✓ Network Iteration ✓ Capacity Evaluation ✓ F/F+1 Proof ✓ UNKNOWN handling ✓ TIMEOUT handling ✓ INVALID handling ✓ Bottleneck Engine ✓ Explanation Engine ✓ Capacity Offer Gate ✓ Idempotency ✓ Cancellation ✓ Timeout ✓ Checkpoint ✓ Recovery ✓ Logging ✓ Metrics ✓ Audit ✓ API ✓ Golden E2E Test 73. معماری اجرایی نهایی V2.5-K ┌──────────────┐ │ API │ └──────┬───────┘ ▼ ┌─────────────────┐ │ Run Manager │ └────────┬────────┘ ▼ ┌─────────────────┐ │ Input Validation│ └────────┬────────┘ ▼ ┌─────────────────┐ │ Scenario │ │ Materializer │ └────────┬────────┘ ▼ ┌─────────────────┐ │ Candidate Gen. │ └────────┬────────┘ ▼ ┌─────────────────┐ │ Aggregate Solver│ └────────┬────────┘ ▼ ┌─────────────────┐ │ TrainRun │ │ Builder │ └────────┬────────┘ ▼ ┌────────────┼────────────┐ ▼ ▼ ▼ Formation Wagon Loco └────────────┼────────────┘ ▼ ┌─────────────────┐ │ Detailed V1.7 │ │ Scheduler │ └────────┬────────┘ ▼ ┌─────────────────┐ │ Independent │ │ Validator │ └────────┬────────┘ ▼ ┌─────────────────┐ │ Capacity Proof │ │ F/F+1 │ └────────┬────────┘ ▼ ┌─────────────┼─────────────┐ ▼ ▼ ▼ Bottleneck Explanation Evidence └─────────────┼─────────────┘ ▼ ┌─────────────────┐ │ Capacity Offer │ └────────┬────────┘ ▼ Marketplace 74. اصل نهایی V2.5-K پس از این نسخه، تعریف رسمی یک Run چنین است: [ Run = ( DataVersion, InfrastructureVersion, CalibrationVersion, Scenario, ModelVersion, SolverConfiguration, InputSnapshot ) ] و: [ Result = Validate( Explain( Prove( Optimize( Schedule( Allocate( Materialize( Run ))))))) ] یعنی هیچ Result خامی مستقیماً به کاربر یا Marketplace ارائه نمی‌شود. 75. جایگاه Production Pipeline اکنون پروژه از نظر معماری به این سطح رسیده است: V2.5-H Real Data Execution ↓ V2.5-I Reconciliation + Calibration ↓ V2.5-J Production Database Contract ↓ V2.5-K Production Orchestration ↓ Production Railway Capacity Platform و مهم‌ترین Boundary نهایی: Data ≠ Model Model ≠ Scenario Scenario ≠ Run Aggregate ≠ Detailed Detailed ≠ Validation Validation ≠ Proof Proof ≠ Explanation Explanation ≠ Marketplace Offer این تفکیک باعث می‌شود سامانه هم قابل توسعه باشد، هم قابل تست، هم قابل Audit و هم در آینده بتواند از یک مسیر ساده به یک شبکه بزرگ چند-OD و چندمنبعی مقیاس پیدا کند. گام منطقی بعدی V2.5-L، دیگر صرفاً طراحی نیست: باید وارد Executable Production Implementation شویم؛ یعنی اسکلت واقعی Repository را ایجاد کنیم، SQLAlchemy/Alembic را پیاده کنیم، Contractها را به Python Package تبدیل کنیم، RunManager و Pipeline را واقعاً اجرا کنیم و Golden E2E را با Fixture واقعی Access-shaped به‌صورت تست اتوماتیک ببندیم. --- ## ادامه 78 — پرسش کاربر (`message-id:bcaaa836-079f-4296-aef3-5197cae291fb`) بله حتما --- ## ادامه 78 — پاسخ دستیار (`message-id:64295584-1e63-49cc-9c8e-9a92a8cc38aa`) Data analysis isn't available right now. Do you want to continue without it? --- ## ادامه 79 — پرسش کاربر (`message-id:547a8a3e-e898-44e2-b16b-a0a7e3515625`) بله بدون اون ادامه بده --- ## ادامه 79 — پاسخ دستیار (`message-id:fc51d262-fa67-47b9-a5b6-e98989f668c5`) حتماً. بدون اجرای ابزار ادامه می‌دهیم و این مرحله را به‌صورت V2.5-L — Executable Production Implementation Specification می‌بندیم. نکته مهم: در این پاسخ کد را به‌صورت طراحی/Implementation Contract می‌دهم؛ ادعا نمی‌کنم که تست‌ها یا اتصال واقعی به aaa.accdb در این محیط اجرا شده‌اند. V2.5-L — Executable Production Implementation 1. هدف در V2.5-L از معماری به کد قابل استقرار می‌رسیم: Real Data ↓ DataVersion ↓ Quality + Reconciliation ↓ Canonical Snapshot ↓ Scenario ↓ Run Manager ↓ Aggregate Solver ↓ TrainRun Builder ↓ Formation ↓ Wagon ↓ Locomotive ↓ V1.7 Detailed Scheduler ↓ Independent Validator ↓ Capacity Proof ↓ Bottleneck ↓ Explanation ↓ Capacity Offer اصل مهم: هیچ لایه‌ای نباید منطق لایه دیگر را دوباره پیاده‌سازی کند. 2. ساختار Repository نهایی railway_capacity_workbench/ │ ├── pyproject.toml ├── README.md ├── .env.example ├── docker-compose.yml ├── alembic.ini │ ├── config/ │ ├── development.yml │ ├── test.yml │ └── production.yml │ ├── alembic/ │ ├── env.py │ └── versions/ │ ├── app/ │ ├── main.py │ │ │ ├── config/ │ │ ├── settings.py │ │ └── logging.py │ │ │ ├── db/ │ │ ├── base.py │ │ ├── session.py │ │ └── models/ │ │ │ ├── domain/ │ │ ├── infrastructure/ │ │ ├── train/ │ │ ├── route/ │ │ ├── demand/ │ │ ├── wagon/ │ │ ├── locomotive/ │ │ ├── formation/ │ │ ├── schedule/ │ │ ├── network/ │ │ ├── scenario/ │ │ ├── capacity/ │ │ └── result/ │ │ │ ├── adapters/ │ │ ├── access/ │ │ ├── excel/ │ │ ├── marketplace/ │ │ └── gis/ │ │ │ ├── staging/ │ ├── mapping/ │ ├── quality/ │ ├── reconciliation/ │ ├── calibration/ │ │ │ ├── engines/ │ │ ├── aggregate/ │ │ ├── formation/ │ │ ├── wagon_cycle/ │ │ ├── locomotive_cycle/ │ │ ├── scheduling/ │ │ ├── network/ │ │ ├── capacity/ │ │ ├── proof/ │ │ ├── bottleneck/ │ │ └── explanation/ │ │ │ ├── validation/ │ │ ├── data/ │ │ ├── infrastructure/ │ │ ├── formation/ │ │ ├── rolling_stock/ │ │ ├── schedule/ │ │ ├── network/ │ │ └── capacity/ │ │ │ ├── services/ │ │ ├── ingestion.py │ │ ├── reconciliation.py │ │ ├── scenario.py │ │ ├── run_manager.py │ │ ├── capacity.py │ │ └── marketplace.py │ │ │ └── api/ │ ├── routes/ │ └── schemas/ │ ├── mappings/ ├── fixtures/ │ ├── golden/ │ └── synthetic/ │ ├── tests/ │ ├── unit/ │ ├── integration/ │ ├── golden/ │ └── e2e/ │ └── scripts/ ├── inspect_access.py ├── inspect_excel.py ├── ingest.py ├── reconcile.py └── run_capacity.py 3. Domain Contract هسته Domain باید مستقل از: SQLAlchemy FastAPI PostgreSQL OR-Tools Access Excel باشد. مثلاً: Python @dataclass(frozen=True) class CapacityRunRequest: run_id: str scenario_id: str data_version_id: str infrastructure_version_id: str calibration_version_id: str | None model_version: str planning_start: int planning_end: int solver_configuration: SolverConfiguration 4. Data Version Python @dataclass(frozen=True) class DataVersion: id: str name: str source_type: str source_hash: str status: str quality_status: str Source File: Python @dataclass(frozen=True) class SourceFile: id: str data_version_id: str file_name: str source_type: str sha256: str file_size: int اصل: Source File ↓ SHA256 ↓ DataVersion بنابراین مشخص است دقیقاً کدام فایل ورودی Run بوده است. 5. Canonical Snapshot قبل از اجرای Solver: Python @dataclass(frozen=True) class CanonicalSnapshot: data_version_id: str infrastructure_version_id: str calibration_version_id: str | None scenario_id: str trains: tuple train_runs: tuple stations: tuple routes: tuple blocks: tuple demands: tuple wagons: tuple locomotives: tuple snapshot_hash: str این مهم‌ترین مرز reproducibility است. 6. Scenario Scenario نباید Data را کپی کند. Python @dataclass(frozen=True) class ScenarioChange: entity_type: str entity_id: str attribute: str base_value: str | None new_value: str مثلاً: Scenario S02 B03.track_type: SINGLE → DOUBLE 7. Scenario Materializer Python class ScenarioMaterializer: def materialize( self, base_snapshot, scenario, ) -> CanonicalSnapshot: snapshot = clone(base_snapshot) for change in scenario.changes: apply_change( snapshot, change ) validate_snapshot(snapshot) return snapshot 8. Run Manager Run Manager مسئول lifecycle است: Python class RunManager: def create(self, request): ... def transition(self, run_id, state): ... def cancel(self, run_id): ... def get(self, run_id): ... ولی: RunManager ≠ Solver RunManager ≠ Scheduler RunManager ≠ Validator 9. Aggregate Layer Aggregate Solver فقط تصمیم می‌گیرد: WHAT HOW MUCH WHERE مثلاً: OD = Tehran → Khowaf Route = R01 Candidate = C17 Train Count = 24 اما نمی‌گوید: Train 17 at 08:23 enters Block B03 10. Aggregate Constraint Python sum( allocation.freight_tons for allocation in allocations ) <= demand.freight_tons و برای منابع: Python sum( candidate.resource_consumption * train_count ) <= resource.capacity این فقط Aggregate Upper Bound تولید می‌کند. 11. TrainRun Expansion اگر: F = 24 باشد: C17:D1:0001 C17:D1:0002 ... C17:D1:0024 تولید می‌شود. Invariant: Python assert len(train_runs) == requested_train_count 12. TrainRun Identity شناسه باید deterministic باشد: Python def train_run_id( candidate_id: str, operating_day: int, sequence: int, ) -> str: return ( f"{candidate_id}:" f"D{operating_day}:" f"{sequence:04d}" ) 13. Formation Engine Formation قبل از Scheduler اجرا می‌شود. بررسی: Wagon Compatibility Train Length Gross Weight Axle Constraints Locomotive Traction Brake Capability Station Length Route Compatibility خروجی: Python FormationCheckResult( train_run_id=..., feasible=True, wagon_count=... ) 14. Wagon Engine Wagon فقط Pool Check نیست. سه سطح: Level 1 Static Pool Level 2 Time-dependent Inventory Level 3 Full Wagon Cycle سطح Production باید در نهایت Level 3 را پشتیبانی کند. 15. Wagon Cycle برای: Loaded A → B پس از Unload: Empty B → A و سپس: Load چرخه: LOAD ↓ LOADED MOVEMENT ↓ UNLOAD ↓ EMPTY MOVEMENT ↓ WAIT / MAINTENANCE ↓ LOAD 16. Empty Wagon Balance معادله: E j,w,t+1 ​ =E j,w,t ​ +Unload j,w,t ​ +EmptyIn j,w,t ​ −Load j,w,t ​ −EmptyOut j,w,t ​ و: 0≤E j,w,t ​ ≤B j,w ​ 17. Locomotive Engine Locomotive Cycle: TRAIN ↓ TURNBACK ↓ RETURN ↓ FUEL / MAINTENANCE ↓ AVAILABLE در صورت نیاز: Single Loco Double Loco Distributed Power به‌عنوان Train Type / Formation Rule مدل می‌شوند. 18. Detailed Scheduler Port معماری: Python class DetailedSchedulerPort(Protocol): def solve( self, problem: DetailedSchedulingProblem, ) -> DetailedSchedulingResult: ... و V1.7: Python class SchedulerV17Adapter: def __init__(self, scheduler): self.scheduler = scheduler def solve(self, problem): return self.scheduler.solve(problem) بنابراین V2.5 Network Solver هیچ Scheduler دیگری ایجاد نمی‌کند. 19. Detailed Time-Space Model برای Train i: A i,s ​ Arrival D i,s ​ Departure برای Block: E i,b ​ Entry X i,b ​ Exit C i,b ​ Clear. 20. Block Constraint برای: A → B : E i,b ​ ≥D i,A ​ X i,b ​ ≥E i,b ​ +T run ​ C i,b ​ ≥X i,b ​ +T clear ​ 21. Single Track Resource: PhysicalBlock B03 برای هر دو Direction مشترک است. اگر: Train A → B Train B → A داشته باشیم: C 1 ​ +H switch ​ ≤E 2 ​ یا: C 2 ​ +H switch ​ ≤E 1 ​ 22. Double Track دو Resource: B03:FORWARD B03:REVERSE اما همچنان: Station Junction Terminal Operational Window ممکن است Shared Resource باشند. 23. Independent Validator Validator باید Schedule نهایی را مستقل بررسی کند. مثلاً: Python def validate_block_conflict( movement_a, movement_b, ): ... و: Python def validate_headway(...): ... def validate_switch(...): ... def validate_station_track(...): ... def validate_junction(...): ... 24. Validation Result Python @dataclass(frozen=True) class ValidationResult: valid: bool errors: tuple[ValidationIssue, ...] warnings: tuple[ValidationIssue, ...] checked_constraints: int validator_version: str 25. Critical Status Rule این چهار حالت کاملاً متفاوت‌اند: FEASIBLE INFEASIBLE INVALID UNKNOWN و همچنین: TIMEOUT MODEL_INVALID هرگز نباید: UNKNOWN → INFEASIBLE یا: TIMEOUT → INFEASIBLE تبدیل شود. 26. Capacity Evaluator تابع مرکزی: Python def evaluate( train_count: int, ) -> CapacityEvaluation: Pipeline: F ↓ Aggregate ↓ TrainRun ↓ Formation ↓ Wagon ↓ Locomotive ↓ Detailed Scheduler ↓ Independent Validator 27. Proof Engine قاعده: F is feasible و: Validate(F)=True و: F+1=INFEASIBLE پس: Capacity=F و: Proof=VALID 28. اگر F+1 نامشخص باشد مثلاً: F = 37 → FEASIBLE F+1 = 38 → TIMEOUT نتیجه: Best Detailed Feasible = 37 Proven Capacity = NULL Proof Status = NOT_PROVEN این تمایز برای اعتبار حرفه‌ای سیستم حیاتی است. 29. Bottleneck Engine برای هر Resource: Slack=Capacity−Usage ولی: Utilization بالا به‌تنهایی Bottleneck نیست. باید با یکی از موارد زیر ترکیب شود: F/F+1 Failure Constraint Binding Marginal Capacity Impact Conflict Evidence 30. Bottleneck Example مثلاً: Resource: B03 Track: SINGLE Utilization: 100% Slack: 0 F=37: FEASIBLE F=38: INFEASIBLE Conflict: Opposing Direction آن‌وقت Evidence قوی برای Bottleneck داریم. 31. Marginal Impact Scenario: Base: B03 = SINGLE Capacity = 37 Scenario: B03 = DOUBLE Capacity = 44 پس: ΔC=44−37=7 این عدد نتیجه Scenario Re-Solve است، نه یک تخمین ساده بر اساس Utilization. 32. Explanation Engine Narrative باید از Evidence تولید شود: عنوان: ظرفیت مسیر به دلیل محدودیت B03 محدود شده است. Evidence: - B03 = SINGLE - Opposing-direction conflicts = 12 - Slack = 0 - F = 37 feasible - F+1 = 38 infeasible پس Explanation قابل Audit است. 33. Capacity Offer خروجی Marketplace: Python @dataclass(frozen=True) class CapacityOffer: id: str run_id: str od_pair_id: str route_id: str train_capacity: int freight_capacity_t: float confidence: str proof_id: str | None 34. Offer Policy اگر: PROVEN باشد: confidence = PROVEN اگر: NOT_PROVEN باشد: confidence = INDICATIVE و Business Policy تعیین می‌کند آیا چنین Offerی به Marketplace نمایش داده شود یا خیر. 35. Database در اولین Migration: data_version source_file scenario scenario_change capacity_run run_checkpoint سپس: station station_track physical_block block_running_time junction junction_movement junction_conflict operational_window سپس: train train_run train_station_call route directed_path و: demand freight_flow wagon wagon_pool wagon_inventory wagon_cycle empty_wagon_movement locomotive locomotive_cycle formation و در نهایت: schedule schedule_station_call schedule_block_movement resource_usage resource_conflict validation_result capacity_evaluation capacity_proof bottleneck_evidence explanation capacity_offer 36. SQLAlchemy Boundary Domain: @dataclass Persistence: SQLAlchemy ORM تبدیل: Domain ↔ Repository ↔ ORM نه: Domain = SQLAlchemy Model این تفکیک در پروژه بزرگ بسیار مهم است. 37. Run Checkpoint Python @dataclass(frozen=True) class RunCheckpoint: run_id: str state: str stage: str iteration: int payload_hash: str created_at: str مثلاً: RUN-2026-001 SOLVING_DETAILED Iteration 4 اگر Worker از کار افتاد، Pipeline از Snapshot دوباره قابل بازسازی است. 38. Idempotency کلید: IdempotencyKey=Hash(DataVersion,InfrastructureVersion,CalibrationVersion,Scenario,ModelVersion,SolverConfiguration,InputSnapshot) اگر همان Run دوباره درخواست شود، سیستم می‌تواند Run قبلی را پیدا کند. 39. Run Manifest Python @dataclass(frozen=True) class RunManifest: run_id: str data_version_id: str infrastructure_version_id: str calibration_version_id: str | None scenario_id: str model_version: str solver_configuration_hash: str input_snapshot_hash: str این Manifest باید همراه Result ذخیره شود. 40. API اصلی POST /api/v1/runs GET /api/v1/runs/{run_id} POST /api/v1/runs/{run_id}/cancel GET /api/v1/runs/{run_id}/iterations GET /api/v1/runs/{run_id}/schedule GET /api/v1/runs/{run_id}/validation GET /api/v1/runs/{run_id}/capacity GET /api/v1/runs/{run_id}/proof GET /api/v1/runs/{run_id}/bottlenecks GET /api/v1/runs/{run_id}/explanations GET /api/v1/runs/{run_id}/capacity-offer 41. Run Response JSON { "run_id": "RUN-001", "state": "COMPLETED", "capacity": { "aggregate_upper_bound": 42, "best_detailed_feasible": 37, "proven_capacity": 37, "proof_status": "PROVEN" }, "validation": { "status": "PASS" } } این فقط نمونه Contract است. 42. Golden Test Suite حداقل تست‌ها: G001 Midnight Rollover G002 Access Field Mapping G003 Train Identity G004 Directed Path G005 Single Track Conflict G006 Double Track G007 Station Length G008 Junction Conflict G009 Formation G010 Wagon Pool G011 Empty Wagon Cycle G012 Locomotive Cycle G013 Aggregate Upper Bound G014 Detailed Feasibility G015 F/F+1 Proof G016 UNKNOWN G017 TIMEOUT G018 INVALID G019 Bottleneck Evidence G020 Scenario Re-Solve G021 Capacity Offer G022 Full E2E 43. Golden E2E Fixture: TrainNo = 100 TrainName = گار-اندیمشک1 Origin = GAR Destination = SAKHEH با زمان‌هایی که Midnight Rollover دارند. مثلاً: 23:46 → 00:36 00:56 → 02:19 در Canonical: 1426 1476 1496 1579 این اعداد فقط Golden Fixture هستند. 44. Golden Capacity Fixture: Demand = 3000 t Freight/Train = 1000 t بنابراین Aggregate Upper Bound: 3 و اگر: F=3 → FEASIBLE + VALID F=4 → INFEASIBLE آنگاه: ProvenCapacity=3 45. Production E2E Definition of Done: Access ↓ Raw ↓ Staging ↓ Mapping ↓ Quality ↓ Reconciliation ↓ Canonical ↓ Infrastructure ↓ Scenario ↓ Aggregate ↓ TrainRun ↓ Formation ↓ Wagon ↓ Locomotive ↓ Detailed Scheduler ↓ Independent Validation ↓ Capacity Search ↓ Proof ↓ Bottleneck ↓ Explanation ↓ Capacity Offer 46. مهم‌ترین نکته درباره داده واقعی شما برای aaa.accdb و فایل Excel قبلی، این موارد باید همان‌طور که از داده واقعی استخراج شده‌اند حفظ شوند: TrainNo TrainName StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed Distance sumDistancezz seir به‌خصوص: seir به: running_time_to_next نگاشت می‌شود. نه: station.running_time و: Kilometerage به: chainage و: derived_distance = abs(next_chainage - current_chainage) ولی: Distance sumDistancezz تا زمان تأیید Semantic آنها نباید وارد محاسبات اصلی ظرفیت شوند. 47. مرز Access و Infrastructure این دو منبع را نباید یکی کنیم: Access └── Operational Evidence Infrastructure Master └── Engineering Parameters مثلاً Access ممکن است نشان دهد: GAR → SAKHEH seir = 20 اما نباید از آن نتیجه بگیریم: TrackType = SINGLE Headway = 5 SwitchTime = 8 این‌ها باید از Infrastructure Master بیایند. 48. Calibration در نهایت یک Calibration Version داریم: Infrastructure Parameter ↓ Observed Operational Data ↓ Calibration ↓ Calibrated Parameter مثلاً: Baseline Running Time Observed Running Time Model Running Time با هم مقایسه می‌شوند. اما Calibration نباید داده Source را overwrite کند. 49. Production Readiness Gate قبل از اجازه Solver: Data Ready? Infrastructure Ready? Calibration Ready? Scenario Valid? Demand Valid? Path Mapping Complete? Train Formation Possible? همه باید بررسی شوند. 50. Final Architecture MARKETPLACE │ ▼ MARKET DEMAND │ ▼ ┌─────────────────┐ │ Canonical Data │ └────────┬────────┘ │ ┌────────────┴────────────┐ ▼ ▼ Operational Data Infrastructure │ │ └────────────┬────────────┘ ▼ Reconciliation │ ▼ Calibration │ ▼ Scenario │ ▼ Aggregate Optimizer │ ▼ Allocation │ ▼ TrainRun Builder │ ┌────────────┼────────────┐ ▼ ▼ ▼ Formation Wagon Loco └────────────┼────────────┘ ▼ Detailed Scheduler │ ▼ Independent Validator │ ▼ Capacity Evaluator │ ▼ F / F+1 Proof │ ┌────────────┼────────────┐ ▼ ▼ ▼ Bottleneck Explanation Evidence └────────────┼────────────┘ ▼ Capacity Offer │ ▼ MARKETPLACE 51. وضعیت پروژه بعد از V2.5-L اکنون مسیر توسعه به این شکل تثبیت می‌شود: نسخه نقش V2.5-A Formal Aggregate/Detailed Boundary V2.5-B Contracts V2.5-C Integration with V1.7 Scheduler V2.5-D.1 Parameterized Real Data Core V2.5-E Infrastructure Master V2.5-F End-to-End Parameterized Capacity V2.5-G Independent Validation + Proof + Bottleneck V2.5-H Real Data Execution Layer V2.5-I Reconciliation + Calibration V2.5-J Production Database Contract V2.5-K Production Orchestration V2.5-L Executable Production Implementation نتیجه فنی از اینجا به بعد دیگر نباید معماری جدیدی به پروژه اضافه کنیم مگر اینکه یک نیاز واقعی کشف شود. تمرکز باید روی Implementation، Integration، Test، Real Data Validation و Performance باشد. گام بعدی طبیعی V2.5-M: Production OR-Tools CP-SAT Integration — یعنی جایگزین کردن Detailed Scheduler آزمایشی با Scheduler واقعی V1.7، تعریف OptionalIntervalVar، NoOverlap، Headway، Opposing Direction، Switch، Clearing، Station Track، Junction و Operational Window و سپس اتصال واقعی آن به CapacityEvaluator و F/F+1 Proof. --- ## ادامه 80 — پرسش کاربر (`message-id:2fb5c5b5-8f15-4fe7-a5d5-f76655ad9ac7`) بله لطفا --- ## ادامه 80 — پاسخ دستیار (`message-id:5d294029-0823-40e6-b945-00828e3f4db3`) حتماً. می‌رویم سراغ V2.5-M — Production OR-Tools CP-SAT Integration؛ این مرحله نقطه‌ای است که Scheduler واقعی V1.7 از یک Contract/Adapter به هسته اجرایی Detailed تبدیل می‌شود. V2.5-M — Production CP-SAT Detailed Scheduler 1. هدف هدف V2.5-M: Aggregate Allocation ↓ TrainRun ↓ DirectedPath ↓ DetailedSchedulingProblem ↓ OR-Tools CP-SAT ↓ Time-Space Schedule ↓ Independent Validator ↓ Capacity Evaluation ↓ F / F+1 Proof قاعده اصلی: CP-SAT فقط یک Solver است؛ مدل Domain، Constraint Builder، Validator و Proof Engine مستقل باقی می‌مانند. 2. معماری داخلی Scheduler Detailed Scheduler │ ├── Problem Compiler │ ├── Time Variable Builder │ ├── Precedence Constraints │ ├── Running Time Constraints │ ├── Dwell Constraints │ ├── Block Resource Constraints │ ├── Single Track │ └── Double Track │ ├── Headway Constraints ├── Opposing Direction Constraints ├── Switch Constraints ├── Clearing Constraints ├── Station Track Assignment ├── Station Length ├── Junction Conflict Matrix ├── Operational Windows ├── Earliest Departure ├── Latest Arrival │ ├── Objective Builder │ ├── CP-SAT Solver │ └── Result Decoder 3. اصل بسیار مهم: Physical Block در مدل: PhysicalBlock جهت‌دار نیست. مثلاً: GAR::SAKHEH یک Resource فیزیکی است. اما: GAR → SAKHEH SAKHEH → GAR دو Directed Movement هستند. 4. Movement Resource برای Single Track: GAR::SAKHEH هر دو جهت از همان Resource استفاده می‌کنند. برای Double Track: GAR::SAKHEH:FORWARD GAR::SAKHEH:REVERSE این تفاوت باید مستقیماً وارد CP-SAT شود. 5. Time Variables برای هر Train و Station: Python arrival[(train_id, station_id)] departure[(train_id, station_id)] و برای هر Block: Python entry[(train_id, block_id)] exit[(train_id, block_id)] clear[(train_id, block_id)] همه Integer هستند و واحد: minute است. 6. Time Horizon نباید Horizon به‌صورت Hardcoded در Scheduler باشد. Python @dataclass(frozen=True) class TimeHorizon: start: int end: int مثلاً: Planning Start = 0 Planning End = 2880 یعنی دو روز. 7. Variable Builder ساختار: Python class TimeVariableBuilder: def build_station_variables( self, model, train_runs, horizon, ): ... def build_block_variables( self, model, train_runs, paths, horizon, ): ... خروجی: Python @dataclass class ScheduleVariables: arrival: dict departure: dict entry: dict exit: dict clear: dict 8. Precedence برای هر Train: D i,s ​ ≥A i,s ​ و: E i,b ​ =D i,s ​ در صورت وجود زمان آزاد قبل از ورود به Block: E i,b ​ ≥D i,s ​ 9. Running Time اگر: T i,b ​ زمان حرکت قطار روی Block باشد: X i,b ​ ≥E i,b ​ +T i,b ​ زمان واقعی می‌تواند بر اساس: TrainType LoadState Direction Block انتخاب شود. 10. Block Running Time Profile به جای: Python block.running_time از: Python BlockRunningTime استفاده می‌کنیم. مثلاً: Block = B03 Direction = FORWARD TrainType = HEAVY_FREIGHT LoadState = LOADED Baseline = 32 Minimum = 29 Maximum = 40 این مدل بعداً اجازه می‌دهد Runtime دقیق‌تر شود. 11. Dwell در هر Station: D i,s ​ ≥A i,s ​ +T dwell,i,s ​ اگر RequiredWait معتبر باشد: T dwell ​ ≥RequiredWait اگر RequiredWait هنوز PROVISIONAL باشد، نباید بدون Policy مشخص آن را به Hard Constraint تولیدی تبدیل کنیم. 12. Operational Profile برای هر TrainRun: Python @dataclass(frozen=True) class TrainOperationalProfile: train_length_m: int train_weight_t: int earliest_departure: int latest_arrival: int | None brake_test_minutes: int formation_minutes: int clearance_minutes: int 13. Earliest Departure برای Origin: D i,O ​ ≥EarliestDeparture i ​ 14. Latest Arrival اگر تعریف شده باشد: A i,D ​ ≤LatestArrival i ​ 15. Clearing اگر: X i,b ​ زمان خروج از Block و: T clear ​ زمان آزاد شدن کامل Resource باشد: C i,b ​ ≥X i,b ​ +T clear ​ این C باید مبنای Conflict Detection باشد، نه صرفاً Exit. 16. Single Track — NoOverlap برای Single Track، همه Movementهای هر دو جهت یک Resource دارند. مفهوم: BlockResource(B03) ├── Train 100 FORWARD ├── Train 101 REVERSE ├── Train 102 FORWARD └── ... می‌توان از IntervalVar و NoOverlap استفاده کرد. اما یک نکته مهم: NoOverlap به‌تنهایی Switch/Headway متفاوت را مدل نمی‌کند. بنابراین Separation Constraint باید جداگانه ساخته شود. 17. Headway Same Direction برای دو حرکت هم‌جهت: E j ​ ≥C i ​ +H ij ​ که: H ij ​ =HeadwayRule(i,j) است. نه الزاماً یک عدد ثابت برای همه قطارها. 18. Opposing Direction برای دو حرکت مخالف: یا: E j ​ ≥C i ​ +H switch ​ یا: E i ​ ≥C j ​ +H switch ​ این یک Disjunctive Constraint است. در CP-SAT: Python order_ij = model.NewBoolVar(...) و: Python model.Add( entry_j >= clear_i + separation ).OnlyEnforceIf(order_ij) model.Add( entry_i >= clear_j + separation ).OnlyEnforceIf(order_ij.Not()) 19. Separation Rule Python def required_separation( same_direction: bool, headway_same_direction: int, switch_time: int, ) -> int: if same_direction: return headway_same_direction return max( headway_same_direction, switch_time, ) ولی در نسخه Production بهتر است Rule از HeadwayRule و SwitchRule استخراج شود. 20. Switch Rule مثلاً: FORWARD → REVERSE = 8 min REVERSE → FORWARD = 6 min پس: Python SwitchRule( resource_id="B03", from_direction=Direction.FORWARD, to_direction=Direction.REVERSE, minimum_switch_min=8, ) این asymmetry را نیز پشتیبانی می‌کند. 21. Double Track در Double Track: B03:FORWARD B03:REVERSE جدا هستند. بنابراین دو قطار مخالف الزاماً Block Conflict ندارند. اما ممکن است در: Station Junction Terminal با هم Conflict داشته باشند. 22. OptionalIntervalVar برای Station Track Assignment: Python assigned = model.NewBoolVar( f"{train_id}_{station_id}_{track_id}" ) interval = model.NewOptionalIntervalVar( start, duration, end, assigned, name ) و: Python model.AddExactlyOne( assigned_tracks ) 23. Station Track Capacity برای Track: Track T01 usable_length = 700m و Train: train_length = 650m : 650≤700 مجاز است. اما: 750≤700 مجاز نیست. 24. Station Track Resource برای هر Track: Python model.AddNoOverlap( station_track_intervals[track_id] ) در نتیجه دو Train نمی‌توانند هم‌زمان از همان Track استفاده کنند. 25. Station Track Compatibility Track باید بررسی کند: freight_allowed crossing_allowed overtaking_allowed electrified usable_length direction compatibility بنابراین usable_length تنها معیار نیست. 26. Junction Junction را نباید به: NoOverlap(all movements) تقلیل دهیم. مثلاً: M1: A → B M2: C → D M3: A → C M4: B → D ممکن است فقط: M1 conflicts M3 M2 conflicts M4 باشد. 27. Junction Conflict Matrix Python @dataclass(frozen=True) class JunctionConflict: movement_a_id: str movement_b_id: str separation_min: int فقط زوج‌های واقعاً Conflict دار Constraint دریافت می‌کنند. 28. Operational Window مثلاً: B03 Maintenance 01:00–03:00 Blocking اگر Train در این Window باشد: Before Window OR After Window باید برقرار شود. در CP-SAT: Python before = model.NewBoolVar(...) و: Python model.Add(clear <= window_start).OnlyEnforceIf(before) model.Add(entry >= window_end).OnlyEnforceIf(before.Not()) 29. Objective — Baseline Mode برای بازسازی برنامه موجود: min i ∑ ​ ∣D i ​ −D i baseline ​ ∣ و می‌توان Arrival را نیز وارد کرد: +λ i ∑ ​ ∣A i ​ −A i baseline ​ ∣ هدف: Feasible + Minimum Deviation 30. Objective — Capacity Mode در Capacity Mode، اولویت: Feasibility است. اگر Objective ظرفیت باشد: maxF اما در Architecture جدید بهتر است Capacity Search بیرون Scheduler باشد: CapacityEvaluator ↓ Scheduler(F) Scheduler فقط می‌گوید: F feasible? 31. Objective — Network Mode در Network: max od,r,t ∑ ​ Q od,r,t ​ F od,r,t ​ Aggregate Layer این Objective را حل می‌کند. Detailed Scheduler فقط Allocation حاصل را زمان‌بندی می‌کند. 32. CP-SAT Configuration Python @dataclass(frozen=True) class SolverConfiguration: time_limit_seconds: int = 300 random_seed: int = 1 num_workers: int = 1 absolute_gap: float | None = None relative_gap: float | None = None log_search_progress: bool = False 33. Determinism برای Golden Tests: random_seed = 1 num_workers = 1 استفاده می‌شود. در Production می‌توان Worker بیشتر استفاده کرد، ولی باید بدانیم: Solver reproducibility و Solver performance دو هدف متفاوت هستند. 34. Solver Status Mapping CP-SAT: OPTIMAL FEASIBLE INFEASIBLE UNKNOWN MODEL_INVALID به Domain: FEASIBLE INFEASIBLE UNKNOWN MODEL_INVALID و Time Limit نباید به‌صورت مصنوعی INFEASIBLE تفسیر شود. 35. Result Decoder Solver نباید Result خام CP-SAT را مستقیماً به API بدهد. CP-SAT Raw Result ↓ Result Decoder ↓ Domain Schedule ↓ Independent Validator ↓ Validated Result 36. Schedule Model Python @dataclass(frozen=True) class ScheduledStationCall: train_run_id: str station_id: str arrival_minute: int departure_minute: int @dataclass(frozen=True) class ScheduledBlockMovement: train_run_id: str block_id: str entry_minute: int exit_minute: int clear_minute: int و: Python @dataclass(frozen=True) class TrainSchedule: train_run_id: str station_calls: tuple[ScheduledStationCall, ...] block_movements: tuple[ScheduledBlockMovement, ...] 37. Independent Validation پس از Decode: CP-SAT says FEASIBLE ↓ Independent Validator ↓ VALID / INVALID اگر: Solver = FEASIBLE Validator = INVALID نتیجه: INVALID نه FEASIBLE. 38. Capacity Evaluation تابع Production: Python class CapacityEvaluator: def evaluate(self, F: int) -> CapacityEvaluation: aggregate = self.aggregate.evaluate(F) if aggregate.status != FEASIBLE: return ... runs = self.train_run_builder.build(aggregate) formation = self.formation_engine.check(runs) if not formation.feasible: return ... wagon = self.wagon_engine.check(runs) if not wagon.feasible: return ... loco = self.loco_engine.check(runs) if not loco.feasible: return ... detailed = self.scheduler.solve(runs) if detailed.status != FEASIBLE: return ... validation = self.validator.validate(detailed) if not validation.valid: return INVALID return FEASIBLE 39. F/F+1 مثلاً: Evaluate(37) نتیجه: FEASIBLE VALID سپس: Evaluate(38) اگر: INFEASIBLE باشد: Capacity = 37 Proof = PROVEN 40. اگر CP-SAT Timeout شود مثلاً: F=38 CP-SAT = UNKNOWN نتیجه: F=37 → Proven Feasible F=38 → Not Proven Infeasible پس: Proven Capacity = NULL مگر اینکه یک Proof Method مستقل دیگری وجود داشته باشد که واقعاً infeasibility را اثبات کند. 41. Bottleneck Attribution اگر F+1 در Detailed Scheduler شکست خورد، Conflictها ذخیره می‌شوند: Python @dataclass(frozen=True) class SchedulingConflictFeedback: resource_id: str conflict_type: str train_ids: tuple[str, ...] required_separation_min: int actual_separation_min: int severity: str 42. مثال Evidence Resource: B03 Conflict: OPPOSING_DIRECTION Train A: C17:D1:0018 Train B: C17:D1:0019 Required Separation: 8 Available: 5 این Evidence به Bottleneck Engine می‌رود. 43. Bottleneck Result B03 Type: INFRASTRUCTURE Level: BINDING Slack: 0 Marginal Impact: +7 trains Evidence: F=37 feasible F=38 infeasible 12 opposing conflicts عدد +7 فقط در صورت اجرای Scenario واقعی با Double Track به دست می‌آید. 44. Network Feedback اگر Aggregate بگوید: 24 trains ولی Detailed فقط: 21 trains را بتواند زمان‌بندی کند: Detailed Feedback ↓ Aggregate Repair ↓ Route / Time Bucket / Ordering / Regime ↓ Detailed Scheduler 45. Repair Priority همان ترتیب قبلی حفظ می‌شود: 1. Train Ordering 2. Departure Time Shift 3. Operating Regime 4. Batch Size 5. Route Change 6. Time Bucket Shift 7. Train Count Reduction و هر Iteration باید ثبت شود. 46. Network Iteration Python @dataclass(frozen=True) class NetworkIteration: iteration: int aggregate_train_count: int scheduled_train_count: int status: str conflict_count: int binding_resources: tuple[str, ...] repair_action: str | None 47. Time-Space Diagram خروجی Scheduler باید قابلیت تبدیل به: Station │ │ Train 1 │ / │ / │----/---------------- │ / │ / Train 2 │ / └────────────────────── Time را داشته باشد. این View مستقیماً از: ScheduledStationCall و: ScheduledBlockMovement تولید می‌شود. 48. Conflict Inspector UI باید بتواند نشان دهد: Conflict #C1024 Resource: B03 Type: OPPOSING_DIRECTION Train A: C17:D1:0018 Train B: C17:D1:0019 Required: 8 min Actual: 4 min Resolution: Shift Train B +4 min این مقدار باید از Result واقعی بیاید. 49. Production API اجرای Run: http POST /api/v1/runs مثلاً Request: JSON { "scenario_id": "SC-001", "planning_start": 0, "planning_end": 2880, "solver": { "time_limit_seconds": 300, "random_seed": 1, "num_workers": 1 } } 50. Run Status API http GET /api/v1/runs/{run_id} Response: JSON { "run_id": "RUN-001", "state": "SOLVING_DETAILED", "iteration": 3, "aggregate_train_count": 42, "scheduled_train_count": 37 } 51. Capacity API http GET /api/v1/runs/{run_id}/capacity خروجی: JSON { "aggregate_upper_bound": 42, "best_detailed_feasible": 37, "proven_capacity": 37, "proof_status": "PROVEN" } اگر F+1 نامشخص باشد: JSON { "aggregate_upper_bound": 42, "best_detailed_feasible": 37, "proven_capacity": null, "proof_status": "NOT_PROVEN" } 52. تست‌های CP-SAT حداقل Golden Testهای این مرحله: M001 Single Track — Same Direction M002 Single Track — Opposing Direction M003 Single Track — Switch Time M004 Double Track — Opposing Trains M005 Same Block — Headway M006 Clearing Time M007 Station Track Conflict M008 Station Length M009 Junction Conflict Matrix M010 Operational Window M011 Earliest Departure M012 Latest Arrival M013 Dwell / Required Wait M014 Midnight Rollover M015 Baseline Reconstruction M016 Aggregate → TrainRun M017 F Feasible / F+1 Infeasible M018 F+1 UNKNOWN M019 Solver FEASIBLE / Validator INVALID M020 Network Feedback 53. تست حیاتی Single Track Case: Train A: FORWARD Train B: REVERSE Block: B03 Switch: 8 min Clearing: 2 min اگر: Train A clear = 100 باشد: Entry B ​ ≥108 باید توسط Scheduler و Validator هر دو بررسی شود. 54. تست Double Track همان Trainها: A → B B → A ولی: B03:FORWARD B03:REVERSE پس Conflict Block نباید ایجاد شود. اگر Station Track مشترک باشد، Conflict ممکن است در Station ایجاد شود. 55. تست Junction اگر: M1 conflicts M2 باشد: Start 2 ​ \geEnd 1 ​ +S یا برعکس. اما: M1 M3 اگر در Conflict Matrix نباشند، نباید بی‌دلیل NoOverlap دریافت کنند. 56. Performance Architecture برای Network بزرگ: Aggregate ↓ Candidate Pruning ↓ Train Count ↓ Detailed Scheduling نه اینکه از ابتدا هزاران Train × هزاران Block × هزاران Pairwise Boolean ساخته شود. 57. Pairwise Explosion اگر: N=500 Train داشته باشیم، Pairwise ordering می‌تواند: O(N 2 ) شود. پس Constraintها باید فقط روی قطارهایی ساخته شوند که: Same Resource + Overlapping Time Domain + Compatible Direction/Movement دارند. 58. Conflict Candidate Generation قبل از ساخت Boolean: Python def candidate_conflict_pairs(movements): ... فیلتر: Different Resource → skip Non-conflicting Junction Movement → skip Impossible Time Windows → skip این برای Production بسیار مهم است. 59. Rolling Horizon برای Network بزرگ: Day 1 ↓ State Snapshot ↓ Day 2 ↓ State Snapshot Snapshot شامل: Wagon Inventory Empty Wagons Locomotive Availability Train Occupancy Maintenance Terminal State خواهد بود. 60. V2.5-M Definition of Done V2.5-M زمانی کامل است که یک Case واقعی/Golden بتواند این مسیر را طی کند: Canonical Data ↓ Scenario ↓ Aggregate Allocation ↓ TrainRun ↓ Formation ↓ Wagon ↓ Locomotive ↓ DirectedPath ↓ CP-SAT ↓ Station Times ↓ Block Times ↓ Single Track Constraints ↓ Double Track Constraints ↓ Headway ↓ Opposing Direction ↓ Switch ↓ Clearing ↓ Station Track ↓ Junction ↓ Operational Window ↓ Independent Validator ↓ Capacity Evaluation ↓ F/F+1 ↓ Capacity Proof خروجی نهایی Train Schedule + Validation Result + Conflict Evidence + Capacity Evaluation + Capacity Proof + Bottleneck Evidence + Explanation جایگاه V2.5-M در کل پروژه با این مرحله، بخش محاسباتی پروژه به شکل بسیار مشخصی تثبیت می‌شود: Aggregate→TrainRun→Detailed CP−SAT→Independent Validation→Proof ​ و مهم‌تر از همه: Capacity=max{F∣Aggregate(F)∧Formation(F)∧Wagon(F)∧Locomotive(F)∧Schedule(F)∧Validation(F)} ​ بنابراین Capacity دیگر یک عدد تخمینی از روی ظرفیت بلوک‌ها نیست؛ خروجی یک برنامه زمانی قابل اجرا و مستقل‌اعتبارسنجی‌شده است. گام بعدی منطقی پس از این مرحله V2.5-N — Real Access/Excel → CP-SAT Golden Execution است: یعنی یک Vertical Slice کامل بر مبنای ساختار واقعی aaa.accdb و فایل Excel موجود، با حفظ تمام Semantics تأییدشده (TrainNo, Sequence, time_in, time_take, time_out, RequiredWait, Kilometerage, seir)، و اتصال آن به همین CP-SAT Scheduler؛ بدون اینکه مقادیر مهندسیِ هنوز تأییدنشده مثل MaxSpeed، Distance یا sumDistancezz را حدس بزنیم. --- ## ادامه 81 — پرسش کاربر (`message-id:5b97e5e2-c941-454c-94db-d0e7ab348cc8`) بله لطفا --- ## ادامه 81 — پاسخ دستیار (`message-id:a852074f-08e6-41a6-a085-826f38e1cf73`) حتماً. حالا وارد مرحله‌ای می‌شویم که از نظر پروژه بسیار مهم است: اتصال داده واقعی Access/Excel به همان CP-SAT Production Scheduler، بدون جعل یا حدس‌زدن پارامترهای مهندسی. V2.5-N — Real Access/Excel → CP-SAT Golden Execution V2.5-N — Real Access/Excel → CP-SAT Golden Execution 1. هدف هدف V2.5-N ایجاد اولین Real-Data Vertical Slice کامل است: Access / Excel ↓ Raw Records ↓ Staging ↓ Mapping ↓ Quality Gate ↓ Identity Reconciliation ↓ Canonical TrainRun ↓ TrainStationCall ↓ DirectedPath ↓ Infrastructure Master ↓ Train Operational Profile ↓ Formation / Wagon / Locomotive ↓ Detailed CP-SAT Scheduler ↓ Independent Validator ↓ Capacity Evaluation ↓ F / F+1 Proof ↓ Bottleneck Evidence ↓ Capacity Offer این مرحله باید ثابت کند که معماری قبلی فقط تئوریک نیست و می‌تواند Operational Evidence واقعی راه‌آهن ایران را به مدل زمان-مکان قابل حل تبدیل کند. 2. اصل مهم V2.5-N در این مرحله سه نوع داده کاملاً از هم جدا می‌شوند: A. Operational Evidence از Access/Excel: TrainNo TrainName StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed Distance sumDistancezz seir B. Engineering Parameters از Infrastructure Master: TrackType Headway SwitchTime ClearingTime StationTrack StationLength JunctionConflict OperationalWindow C. Planning Parameters از Scenario: Demand OperatingRegime Objective TimeHorizon Train Type Wagon Pool Locomotive Pool Policy این سه گروه نباید با یکدیگر مخلوط شوند. 3. Semantic Mapping قطعی داده Access Mapping اصلی: Access Field Canonical Field وضعیت TrainNo TrainRun.source_train_no VERIFIED TrainName TrainService.service_name VERIFIED StationName Station.name VERIFIED StationNumber Station.source_number VERIFIED Sequence TrainStationCall.sequence VERIFIED time_in arrival_minute VERIFIED time_take dwell_minutes VERIFIED time_out source_departure_time VERIFIED RequiredWait required_wait_minutes PROVISIONAL Kilometerage chainage HIGH MaxSpeed max_speed PROVISIONAL Distance source_distance UNTRUSTED sumDistancezz source_cumulative_distance UNKNOWN seir running_time_to_next VERIFIED 4. مهم‌ترین Semantic seir یک Attribute مربوط به Station نیست. ساختار صحیح: TrainStationCall(i) │ └── running_time_to_next ↓ Next Station یعنی: Arrival_{i+1} Departure_i ] و seir باید با این مقدار مقایسه شود. 5. Midnight Normalization داده واقعی شامل عبور از نیمه‌شب است. مثلاً: 23:46 00:36 00:56 02:19 نباید آنها را صرفاً به ساعت روز تبدیل کنیم. تبدیل: 23:46 → 1426 00:36 → 1476 00:56 → 1496 02:19 → 1579 بنابراین: [ 00:36 > 23:46 ] از نظر Absolute Planning Time صحیح خواهد بود. 6. Time Normalization Contract def normalize_after( previous_absolute: int | None, current_hhmm: str, ) -> int: ... قاعده: current < previous ↓ next operational day این تابع باید در یک مکان مرکزی باشد و همه Adapterها از آن استفاده کنند. 7. Time Reconciliation برای هر Segment: Departure(i) + seir(i) = Expected Arrival(i+1) هم‌زمان: Source time_in(i+1) نیز وجود دارد. پس: (Departure_i+seir_i) ] اگر: [ \Delta T\neq0 ] باشد، نباید داده را Silent Rewrite کنیم. 8. Reconciliation Example مثلاً: Departure = 12:30 seir = 20 min Expected Arrival = 12:50 Source Arrival = 12:53 نتیجه: TIME_RECONCILIATION_WARNING Difference = +3 min اما: source_arrival expected_arrival difference هر سه حفظ می‌شوند. 9. Derived Distance اگر: Kilometerage(current) = 157 Kilometerage(next) = 200 آنگاه: [ DerivedDistance=|200-157|=43 ] ولی: Distance = 0 را نباید به 43 تبدیل کنیم. ذخیره: source_distance = 0 derived_distance = 43 10. چرا این تفکیک مهم است؟ چون: Source Value ≠ Derived Value و بعداً می‌توان فهمید: 43 km از کجا آمده است. این اصل برای Audit و Calibration ضروری است. 11. Train Identity TrainNo به‌تنهایی Identity کامل نیست. Identity پیشنهادی: [ TrainIdentity= TrainNo+ TrainName+ Origin+ Destination+ Direction+ OperatingPattern ] بنابراین: 100 به‌تنهایی نباید کلید Canonical TrainRun باشد. 12. Direction Resolution اولویت: 1. Explicit Route Direction 2. Origin / Destination 3. Infrastructure Topology 4. Station Sequence 5. Chainage Evidence 6. Source Convention Chainage فقط Evidence است. مثلاً: 157 → 200 → 220 → ... → 674 نشان می‌دهد حرکت در جهت افزایش Chainage است. Reverse: 674 → 662 → 649 → ... جهت معکوس را نشان می‌دهد. 13. DirectedPath برای هر TrainRun: TrainRun 100 ↓ DirectedPath ↓ GAR ↓ GAR::SAKHEH ↓ SAKHEH Invariant: [ |Stations|=|Blocks|+1 ] 14. PhysicalBlock از: GAR → SAKHEH یک PhysicalBlock ساخته می‌شود: GAR::SAKHEH ولی Direction در DirectedPath ذخیره می‌شود. بنابراین: PhysicalBlock = Physical Resource DirectedPath = Operational Direction 15. Infrastructure Resolution در این مرحله اگر: Train Path داریم ولی: PhysicalBlock در Infrastructure Master وجود ندارد، نتیجه: PATH_NOT_MAPPED است. نه: INFEASIBLE و نه: SINGLE_TRACK نباید حدس زده شود. 16. Infrastructure Readiness برای اجرای Production Scheduler: Station Master = READY Physical Blocks = READY Track Type = READY Running Time Profile = READY Headway Rules = READY Switch Rules = READY Station Tracks = READY Junction Rules = READY اگر یکی از پارامترهای حیاتی موجود نباشد: Infrastructure = BLOCKED و Capacity Proof مجاز نیست. 17. Golden Infrastructure برای Golden Test می‌توان یک Infrastructure Test Fixture تعریف کرد. مثلاً: GAR SAKHEH Block: GAR::SAKHEH TrackType: SINGLE Headway: 5 min Switch: 8 min Clearing: 2 min این مقادیر Test Data هستند، نه ادعای پارامتر واقعی خط. 18. Golden Train Profile مثلاً: Train Length = 650 m Train Weight = 900 t Brake Test = 5 min Formation = 10 min Clearance = 2 min این نیز فقط برای Golden Execution است. 19. Golden Formation مثلاً: 10 Wagons 100 t / Wagon 1 Locomotive Formation: [ WagonCount=10 ] و: [ GrossWeight=1000t ] در صورتی که Train Profile یا Route Limit اجازه دهد. 20. Station Length اگر: Train = 650m Station Track = 700m آنگاه: [ 650\le700 ] و Formation از این نظر Feasible است. 21. Demand Golden Demand: OD: GAR → SAKHEH Demand: 3000 t Freight per Train: 1000 t بنابراین: [ F_{upper}=3 ] این فقط Aggregate Upper Bound است. 22. Aggregate Solver برای: F=3 Allocation: C1 GAR → SAKHEH 3 trains 3000 t تولید می‌شود. 23. TrainRun Builder تبدیل: 3 trains به: C1:D1:0001 C1:D1:0002 C1:D1:0003 24. Detailed Scheduling این سه Train وارد V1.7 Scheduler می‌شوند. Scheduler باید موارد زیر را مدل کند: Precedence Running Time Dwell Block Occupancy Headway Opposing Direction Switch Clearing Station Track Station Length Junction Operational Window Earliest Departure Latest Arrival 25. Single Track Golden Case چون Golden Infrastructure یک Single Track دارد: GAR::SAKHEH هر سه Train از یک Physical Resource استفاده می‌کنند. بنابراین: Train 1 Train 2 Train 3 باید Headway را رعایت کنند. 26. CP-SAT Resource برای Block: interval = model.NewIntervalVar( entry, running_time + clearing_time, clear, name, ) برای Single Track، Intervalهای هر دو Direction در یک Resource جمع می‌شوند. 27. Headway برای Trainهای هم‌جهت: [ Entry_j\ge Clear_i+H ] و برای Reverse: [ Entry_j\ge Clear_i+H_{switch} ] در صورت نیاز به Direction-specific Switch Rule، Rule دقیق از Infrastructure Master خوانده می‌شود. 28. Independent Validator پس از CP-SAT: Solver ↓ Schedule ↓ Validator Validator دوباره بررسی می‌کند: Block overlap Headway Switch Clearing Station Junction Time Window 29. Golden F/F+1 برای: [ F=3 ] انتظار: Aggregate = FEASIBLE Formation = FEASIBLE Wagon = FEASIBLE Locomotive = FEASIBLE Detailed = FEASIBLE Validation = VALID برای: [ F=4 ] Demand فقط 3000 تن است: [ 4\times1000=4000>3000 ] پس: F+1 = INFEASIBLE و: [ Capacity=3 ] با Proof معتبر. 30. Golden Result { "aggregate_upper_bound": 3, "best_detailed_feasible": 3, "proven_capacity": 3, "proof_status": "PROVEN" } این Result مربوط به Golden Fixture است و نباید به‌عنوان ظرفیت واقعی GAR–SAKHEH منتشر شود. 31. Real Data Execution وقتی aaa.accdb واقعی در محیط اجرای نرم‌افزار قرار گرفت: AccessAdapter ↓ RawRecord[] و: TrainNo StationName Sequence time_in time_take ... مستقیماً خوانده می‌شوند. 32. Access Adapter Production Adapter باید: class AccessAdapter: def read_table( self, table_name: str, ) -> list[RawRecord]: ... داشته باشد. نام Table نباید از User Input خام وارد SQL شود. باید: Configured Table Name ↓ Validated Identifier ↓ SELECT باشد. 33. Excel Adapter Excel نیز همان Contract را پیاده می‌کند: class ExcelAdapter: def read(self) -> list[RawRecord]: ... در نتیجه: Access Excel هر دو به: RawRecord تبدیل می‌شوند. 34. Adapter Independence پس از Raw: Access ─┐ ├── Raw/Staging ── Canonical Excel ──┘ هیچ Engine نباید بداند داده از Access آمده یا Excel. 35. Data Quality Gate قبل از Canonicalization: Q001 Missing TrainNo Q002 Missing StationName Q003 Invalid Sequence Q004 Duplicate Sequence Q005 Negative Dwell Q006 Invalid HH:MM Q007 Invalid Running Time Q008 Sequence Gap Q009 Time Reconciliation Q010 Chainage Anomaly Q011 Missing Destination Q012 Missing Origin 36. Quality Status سه حالت اصلی: PASSED PASSED_WITH_WARNINGS FAILED اما: PASSED_WITH_WARNINGS فقط زمانی اجازه Run دارد که Policy اجازه دهد. 37. Data Readiness NOT_READY READY_WITH_WARNINGS READY REJECTED Solver فقط در: READY یا در صورت Policy مشخص: READY_WITH_WARNINGS اجرا می‌شود. 38. Field Confidence برای هر Field: VERIFIED HIGH PROVISIONAL UNKNOWN UNTRUSTED مثلاً: TrainNo VERIFIED Sequence VERIFIED time_in VERIFIED seir VERIFIED Kilometerage HIGH RequiredWait PROVISIONAL MaxSpeed PROVISIONAL Distance UNTRUSTED sumDistancezz UNKNOWN 39. Production Gate یک Field با: UNKNOWN نباید وارد Constraint شود. یک Field با: UNTRUSTED نباید وارد Capacity Calculation شود. یک Field با: PROVISIONAL فقط با Explicit Policy می‌تواند وارد محاسبات شود. 40. Reconciliation اگر Access و Excel هر دو یک Train را دارند: Identity Match انجام می‌شود. سپس: Arrival Departure Dwell Running Time Chainage مقایسه می‌شوند. 41. مثال Reconciliation Access: Arak Arrival = 12:27 Excel: Arak Arrival = 12:30 پس: [ \Delta T=3min ] اگر Tolerance = 2 باشد: CONFLICT ولی داده هیچ‌کدام حذف نمی‌شود. 42. Calibration seir و Observed Running Time: Arrival_{next} Departure_{current} ] مقایسه می‌شوند. خروجی: Source seir Observed Difference Mean P50 P95 اما Calibration Version فقط پس از Approval وارد Production Scenario می‌شود. 43. Golden Execution Manifest هر اجرای Golden: @dataclass(frozen=True) class RunManifest: run_id: str data_version_id: str infrastructure_version_id: str scenario_id: str model_version: str solver_configuration_hash: str input_snapshot_hash: str دارد. 44. Real Run Manifest برای داده واقعی: DataVersion: DV-ACCESS-2026-... InfrastructureVersion: INFRA-... CalibrationVersion: CAL-... Scenario: SC-... Model: 2.5-N همه باید ذخیره شوند. 45. Data Lineage مثلاً: Access Row 17 ↓ RawRecord R17 ↓ TrainStationCall 100:ARAK ↓ Schedule Call ↓ Capacity Result بنابراین می‌توان از Result به Source برگشت. 46. Error Classification این تفکیک باید قطعی باشد: Data Error Missing TrainNo Invalid Time Duplicate Sequence → DATA_INVALID Infrastructure Error Block Not Mapped Missing Track Type Missing Headway → INFRASTRUCTURE_INVALID Operational Infeasibility No feasible schedule → SCHEDULING_INFEASIBLE Solver Unknown TIMEOUT UNKNOWN → UNKNOWN 47. هرگز این کار را نکنیم Missing Block ↓ Assume Single Track یا: Missing Headway ↓ Assume 3 minutes یا: Distance = 0 ↓ Replace with Derived Distance یا: Timeout ↓ INFEASIBLE این چهار مورد از مهم‌ترین Production Anti-Patterns هستند. 48. Golden Acceptance Matrix مرحله انتظار Access-shaped input Accepted Midnight Correct Train identity Resolved Sequence Preserved seir Running Time Chainage Preserved Derived Distance Separate Infrastructure Valid Formation Feasible Wagon Feasible Locomotive Feasible CP-SAT Feasible Validator Valid F Feasible F+1 Infeasible Proof PROVEN Offer Publishable as proven 49. Golden E2E Test ساختار Test: def test_real_shape_to_capacity_proof(): raw = access_shaped_fixture() staged = staging.stage(raw) quality = quality_gate.validate(staged) assert quality.accepted canonical = canonicalizer.build(staged) paths = path_builder.build(canonical) infrastructure = golden_infrastructure() problem = detailed_problem_builder.build( canonical, paths, infrastructure, ) result = scheduler.solve(problem) validation = validator.validate(result) assert validation.valid سپس: proof = capacity_proof_engine.prove( evaluator, 3, ) assert proof.proof_valid 50. Real Access Golden Test در صورت قرار گرفتن فایل واقعی در محیط اجرای تست: aaa.accdb باید: AccessAdapter آن را بخواند. ولی Test باید ابتدا Schema را بررسی کند: TrainNo StationName Sequence time_in time_take time_out ... اگر Schema تغییر کرده باشد: SOURCE_SCHEMA_CHANGED و Test نباید silently ادامه دهد. 51. Excel Reconciliation Test فایل Excel: REPORTKholase_31-06-1405_02-19-35.xlsx در صورت موجود بودن در محیط اجرا، باید به همان Canonical Model نگاشت شود. اما اختلاف Excel با Access باید در: ReconciliationRecord ثبت شود، نه اینکه یکی از منابع حذف شود. 52. Capacity Output Result نهایی باید حداقل شامل: Run Scenario DataVersion InfrastructureVersion ModelVersion Aggregate Upper Bound Best Detailed Feasible Proven Capacity Proof Status Schedule Validation Conflicts Bottlenecks Explanations Capacity Offer باشد. 53. UI نمایش برای جلوگیری از برداشت اشتباه: ┌─────────────────────────────────────┐ │ CAPACITY │ ├─────────────────────────────────────┤ │ Aggregate Upper Bound 3 │ │ Detailed Feasible 3 │ │ Proven Capacity 3 │ │ Proof Status PROVEN │ └─────────────────────────────────────┘ این بهتر از نمایش تنها: Capacity = 3 است. 54. اگر Real Data فقط Operational باشد ممکن است از Access اطلاعات مسیر و زمان داشته باشیم ولی: TrackType Headway Switch Station Tracks Junction نداشته باشیم. در این حالت: Operational Baseline = READY Infrastructure Model = BLOCKED Capacity Proof = NOT ALLOWED اما همچنان می‌توان: Baseline Reconstruction Data Quality Reconciliation Running Time Analysis را اجرا کرد. 55. این قابلیت بسیار مهم است یعنی سیستم لازم نیست به دلیل ناقص بودن Infrastructure کل Data Platform را متوقف کند. دو مسیر داریم: Operational Data Analysis ↓ Allowed Capacity Planning ↓ Blocked until Infrastructure Ready 56. خروجی مرحله V2.5-N در پایان V2.5-N باید این Artifactها وجود داشته باشند: 1. AccessAdapter 2. ExcelAdapter 3. RawRecord Contract 4. Staging Contract 5. Mapping Registry 6. Quality Gate 7. Identity Reconciliation 8. Midnight Normalizer 9. Canonical TrainRun Builder 10. DirectedPath Builder 11. Infrastructure Resolver 12. Formation Precheck 13. Wagon Precheck 14. Locomotive Precheck 15. V1.7 CP-SAT Adapter 16. Independent Validator 17. CapacityEvaluator 18. F/F+1 Proof Engine 19. Bottleneck Evidence 20. Golden E2E Test 57. Definition of Done V2.5-N کامل است اگر یک Input با Shape واقعی Access بتواند بدون تغییر دستی: Raw ↓ Staging ↓ Quality ↓ Canonical ↓ DirectedPath ↓ Infrastructure ↓ TrainRun ↓ Formation ↓ Wagon ↓ Locomotive ↓ CP-SAT ↓ Validation ↓ Capacity ↓ Proof را طی کند. و هیچ مقدار مهندسی تأییدنشده‌ای به‌صورت Silent وارد مدل نشود. 58. وضعیت نهایی معماری پس از V2.5-N اکنون سیستم سه لایه حقیقت دارد: SOURCE TRUTH Access / Excel / Marketplace ↓ OPERATIONAL TRUTH Canonical + Reconciliation ↓ PLANNING TRUTH Infrastructure + Scenario + Calibration ↓ COMPUTATIONAL TRUTH CP-SAT Schedule ↓ AUDIT TRUTH Independent Validation + Proof و این زنجیره بسیار مهم است: [ Source \rightarrow Canonical \rightarrow Model \rightarrow Schedule \rightarrow Validation \rightarrow Proof ] 59. اصل نهایی V2.5-N هیچ ظرفیتی از داده خام استخراج نمی‌شود؛ ظرفیت از یک Schedule عملیاتی معتبر که با داده، زیرساخت، منابع و قواعد سناریو سازگار است، استخراج و سپس با Proof Engine اثبات می‌شود. بنابراین محصول اکنون عملاً یک: OD-Centric, Time-Space, Multi-OD, Rolling-Stock-Constrained Railway Capacity Planning & Optimization Platform است؛ با امکان اتصال مستقیم به داده‌های عملیاتی واقعی راه‌آهن ایران. این مرحله مرز بسیار مهمی را می‌بندد. از اینجا به بعد، تمرکز اصلی دیگر روی تعریف مدل نیست؛ روی اجرای واقعی، Performance، مقیاس شبکه، و اتصال Marketplace است. گام بعدی پیشنهادی V2.5-O — Production Performance & Scalability است: طراحی برای هزاران TrainRun، صدها Block، چندین OD و چند Route، کاهش انفجار Pairwise Constraints، Candidate Pruning، Rolling Horizon، Warm Start، Parallel Scenario Runs و Benchmark رسمی CP-SAT. --- ## ادامه 82 — پرسش کاربر (`message-id:c570ae72-c401-40d5-b322-5373676c4caf`) بله لطفا --- ## ادامه 82 — پاسخ دستیار (`message-id:701b5814-7580-4409-a9a5-3619fab81db0`) حتماً. در این مرحله وارد بخش Performance و مقیاس Production می‌شویم؛ جایی که باید مطمئن شویم موتور برای یک خط کوچک خوب کار نمی‌کند، بلکه برای شبکه چندمسیره و چند-OD نیز معماری قابل اتکایی دارد. V2.5-O — Production Performance, Scalability & Solver Engineering V2.5-O — Production Performance, Scalability & Solver Engineering 1. هدف هدف V2.5-O این است که معماری موجود از یک موتور قابل اجرا به یک Production-Scale Railway Optimization Engine تبدیل شود. معماری هدف: Marketplace / Demand ↓ Candidate Generation ↓ Candidate Pruning ↓ Aggregate Network Optimization ↓ TrainRun Expansion ↓ Rolling Stock Precheck ↓ Detailed Time-Space Scheduling ↓ Independent Validation ↓ Conflict Feedback ↓ Repair / Re-Optimization ↓ Capacity Evaluation ↓ Proof اصل این مرحله: افزایش Scale نباید باعث شود که مدل ریاضی ساده‌تر، Validation ضعیف‌تر، یا Proof غیرقابل اتکا شود. 2. مسئله Scale در یک Case کوچک ممکن است داشته باشیم: 10 TrainRun 20 Block 5 Station اما Production ممکن است شامل: 1000+ TrainRun 500+ Block 200+ Station 50+ OD چند Route چند Train Type چند Wagon Type چند Operating Regime باشد. اگر همه Conflictها به‌صورت Pairwise ساخته شوند: [ O(N^2) ] خیلی سریع بزرگ می‌شوند. بنابراین معماری باید Resource-Based Conflict Generation داشته باشد. 3. اصل اصلی Performance به‌جای: Every Train × Every Train از: Train ↓ Resource Occupancy ↓ Potentially Conflicting Trains استفاده می‌کنیم. یعنی فقط قطارهایی که Resource مشترک دارند وارد Conflict Model شوند. 4. Resource-Centric Architecture مدل: TrainRun ├── Block Occupancy ├── Station Track Occupancy ├── Junction Movement ├── Terminal Occupancy ├── Wagon Occupancy └── Locomotive Occupancy و هر Resource: Resource ├── Capacity ├── Occupancy ├── Rules └── Conflict Matrix دارد. 5. Resource Graph برای هر Run: TrainRun ↓ DirectedPath ↓ Block ↓ Station ↓ Junction Graph تولید می‌شود. مثلاً: T100 ├── B01 ├── B02 ├── B03 ├── ST04 └── J02 و Train دیگری: T101 ├── B03 ├── B04 └── ST05 فقط B03 می‌تواند Conflict مشترک ایجاد کند. 6. Conflict Candidate Index یک Index مرکزی: resource_to_movements: ResourceID → [MovementID, MovementID, ...] مثلاً: B03: T100-M3 T101-M1 T108-M4 فقط همین‌ها وارد Conflict Generation می‌شوند. 7. Spatial Pruning اگر دو Train: T1 → Route A T2 → Route B هیچ Physical Resource مشترکی ندارند: Conflict = impossible و Constraint ساخته نمی‌شود. 8. Temporal Pruning حتی اگر Resource مشترک باشد، اگر Time Window آنها کاملاً جدا باشد: T1: 100–120 T2: 300–320 Conflict Pair لازم نیست. پس: if latest_a < earliest_b: skip و بالعکس. 9. Direction Pruning در Double Track: B03:FORWARD B03:REVERSE دو Resource متفاوت هستند. بنابراین Block Conflict ایجاد نمی‌شود. در Single Track: B03 هر دو Direction روی همان Resource قرار می‌گیرند. 10. Junction Pruning برای Junction فقط Movementهای موجود در Conflict Matrix بررسی می‌شوند. اگر: M1 ↔ M2 Conflict دارند ولی: M1 ↔ M3 ندارند: فقط M1/M2 وارد مدل می‌شوند. 11. Candidate Pruning در Aggregate Layer قبل از CP-SAT: Candidate ↓ Route Exists? ↓ OD Compatible? ↓ Train Type Compatible? ↓ Wagon Compatible? ↓ Locomotive Compatible? ↓ Station Length? ↓ Demand Available? ↓ Time Window? Candidateهای غیرممکن حذف می‌شوند. 12. Candidate Rejection Codes هر حذف باید Evidence داشته باشد: NO_ROUTE NO_WAGON_TYPE NO_LOCOMOTIVE_TYPE STATION_TOO_SHORT TRAIN_TOO_HEAVY COMMODITY_INCOMPATIBLE INVALID_TIME_WINDOW NO_TERMINAL_CAPACITY POLICY_FORBIDDEN INFRASTRUCTURE_NOT_READY این برای Explainability مهم است. 13. Aggregate Solver Aggregate Solver نباید Detailed Time-Space Model را بسازد. فقط: [ F_c ] را تعیین می‌کند. مثلاً: Candidate C01 = 12 Candidate C02 = 8 Candidate C03 = 17 14. Aggregate → Detailed بعد: 12 + 8 + 17 به TrainRunهای واقعی تبدیل می‌شود. این مرز باعث کاهش شدید اندازه مدل Detailed می‌شود. 15. TrainRun Expansion برای هر Allocation: def expand_allocation(allocation): return [ build_train_run( allocation, sequence=i, ) for i in range(allocation.train_count) ] Sequence باید deterministic باشد. 16. Deterministic Ordering ترتیب TrainRunها: Planning Bucket → Earliest Departure → Service Priority → OD → Direction → Candidate ID → Sequence نه: random و نه: database insertion order 17. Rolling Horizon برای Horizon بزرگ، Detailed Scheduler می‌تواند به Window تقسیم شود. مثلاً: Day 1 00:00–06:00 Day 1 06:00–12:00 Day 1 12:00–18:00 Day 1 18:00–24:00 18. State Snapshot در مرز Window: @dataclass(frozen=True) class NetworkStateSnapshot: timestamp: int wagon_inventory: dict locomotive_inventory: dict active_train_movements: tuple terminal_state: dict maintenance_state: dict Window بعدی از Snapshot قبلی شروع می‌شود. 19. چرا Snapshot حیاتی است؟ فرض کنیم در ساعت 12: 10 empty wagons در Station A باقی مانده‌اند. Window بعدی نباید فرض کند: 20 empty wagons در دسترس هستند. بنابراین: [ State_{t+1}=Transition(State_t,Operations_t) ] باید حفظ شود. 20. Boundary Policy در Rolling Horizon باید مشخص شود که قطار در انتهای Window: ACTIVE COMPLETED WAITING CROSSED_BOUNDARY است. قطاری که Block را در Window اول شروع کرده ولی در Window دوم تمام می‌کند، نباید دوباره ایجاد شود. 21. Warm Start اگر Scenario فقط کمی تغییر کرده: Base Scenario ↓ Modified Scenario Schedule قبلی می‌تواند به‌عنوان Hint استفاده شود. CP-SAT می‌تواند از: model.AddHint(variable, value) برای Warm Start استفاده کند. 22. موارد مناسب Warm Start خصوصاً برای: +1 wagon +1 locomotive +1 station track small demand change small headway change می‌توان Schedule قبلی را به‌عنوان Starting Point استفاده کرد. 23. Warm Start نباید Proof را تغییر دهد Warm Start فقط: Search Acceleration است. نه: Proof اعتبار Proof همچنان از: F feasible + Validator + F+1 infeasible می‌آید. 24. Incremental Scenario مثلاً: Base: B03 = SINGLE Scenario: B03 = DOUBLE نباید فقط یک عدد ظرفیت را تغییر دهیم. باید: Scenario ↓ Aggregate ↓ Detailed ↓ Validation ↓ Proof دوباره اجرا شود. 25. Scenario Cache اما Inputهای بدون تغییر می‌توانند Cache شوند: Infrastructure Parsing Route Graph Station Compatibility Candidate Set Conflict Matrix این‌ها لازم نیست برای هر Scenario از صفر ساخته شوند. 26. Immutable Model Cache Cache مناسب: InfrastructureVersion ModelVersion TrainType Route Graph Directed Paths Candidate Compatibility اما Schedule و Capacity Result باید Run-specific باشند. 27. Cache Key مثلاً: [ Key= Hash( DataVersion, InfrastructureVersion, ModelVersion, RelevantScenarioParameters ) ] اگر یکی از اینها تغییر کند Cache باید invalidate شود. 28. Solver Model Compilation یک مرحله جدا: Detailed Problem ↓ Compiled CP-SAT Model ↓ Solve این امکان را می‌دهد که: Problem Validation Model Compilation Solver Execution Result Decoding از هم جدا باشند. 29. Compiler Contract class DetailedModelCompiler: def compile( self, problem: DetailedSchedulingProblem, ) -> CompiledModel: ... و: @dataclass class CompiledModel: model: cp_model.CpModel variables: ScheduleVariables metadata: dict 30. Constraint Builder Registry به‌جای یک Scheduler بزرگ: ConstraintBuilderRegistry داریم: PrecedenceBuilder RunningTimeBuilder DwellBuilder BlockBuilder HeadwayBuilder SwitchBuilder StationTrackBuilder JunctionBuilder WindowBuilder TimeWindowBuilder هر Builder مستقل Test می‌شود. 31. Conditional Constraint Activation اگر Scenario: OperatingRegime = STRICT_ALTERNATING باشد، Rule مربوط به همان Regime فعال می‌شود. اگر: DIRECTIONAL_BATCH باشد، Batch Constraints فعال می‌شوند. 32. Operating Regime سه Regime اصلی: STRICT_ALTERNATING DIRECTIONAL_BATCH MIXED اما در Scale بزرگ: Regime نباید حتماً یک بار برای کل شبکه ثابت باشد. می‌تواند به: Route Time Bucket Resource وابسته شود، اگر Business Rule چنین اجازه‌ای بدهد. 33. Batch Representation Batch: @dataclass(frozen=True) class OperationalBatch: id: str route_id: str direction: str start_minute: int end_minute: int train_run_ids: tuple[str, ...] batch_size: int 34. Batch Search Batch Size نباید همیشه Maximum باشد. Candidateها: 1 2 3 4 ... Kmax می‌توانند ارزیابی شوند. هدف: [ Capacity ] تنها معیار نیست. ممکن است Objective شامل: Freight Delay Empty Wagon Locomotive Utilization Service Regularity باشد. 35. Aggregate/Detailed Feedback Compression Detailed ممکن است صدها Conflict تولید کند. همه آنها نباید مستقیماً به Aggregate فرستاده شوند. ابتدا Aggregate شوند: B03 Opposing Direction 17 conflicts سپس: Resource-level feedback تولید شود. 36. Conflict Aggregation @dataclass(frozen=True) class ConflictSummary: resource_id: str conflict_type: str conflict_count: int affected_train_count: int minimum_slack: int total_delay_impact: int 37. Repair Feedback مثلاً: B03 OPPOSING_DIRECTION 17 conflicts ممکن است Repair Candidate: SHIFT_DEPARTURE CHANGE_REGIME CHANGE_ROUTE REDUCE_TRAIN_COUNT باشد. 38. Iteration Limit نباید Reoptimization بی‌نهایت باشد. max_iterations = 10 مثلاً. اگر تمام Iterationها شکست خوردند: UNKNOWN یا: INFEASIBLE فقط بر اساس Solver Evidence معتبر. 39. Distinguish Failure اگر Solver در Iteration سوم: TIME_LIMIT دهد: نتیجه: UNKNOWN نه: INFEASIBLE 40. Parallel Scenario Execution Scenarioها مستقل می‌توانند Parallel شوند: Scenario A ─┐ Scenario B ─┼── Worker Pool Scenario C ─┤ Scenario D ─┘ اما یک Run واحد نباید به‌صورت تصادفی بین Workerها توزیع شود اگر reproducibility آن را تغییر دهد. 41. Scenario Worker class ScenarioWorker: def execute(self, scenario_id): ... هر Worker: Load Snapshot Compile Solve Validate Persist می‌کند. 42. Resource Limits برای هر Run: CPU Limit Memory Limit Time Limit Solver Limit باید قابل تنظیم باشد. 43. Run Budget مثلاً: @dataclass(frozen=True) class RunBudget: wall_time_seconds: int max_iterations: int max_train_runs: int max_conflict_pairs: int memory_mb: int | None اگر Budget رد شود: TIMEOUT / RESOURCE_LIMIT ثبت شود. 44. Solver Metrics هر Run باید Metric داشته باشد: Model Variables Model Constraints Boolean Variables Interval Variables Conflict Pairs Solve Time Presolve Time Search Time Iterations Objective Gap 45. Performance Evidence مثلاً: { "train_runs": 480, "blocks": 210, "conflict_candidates": 18420, "cp_sat_variables": 152340, "cp_sat_constraints": 481900, "solve_seconds": 42.7 } این اعداد صرفاً نمونه ساختار خروجی هستند. 46. Performance Baseline باید Benchmark رسمی تعریف شود. Case P1 10 trains 10 blocks Case P2 50 trains 50 blocks Case P3 100 trains 100 blocks Case P4 500 trains 200 blocks Case P5 1000 trains 500 blocks 47. Benchmark Metrics برای هر Case: [ T_{total} ] [ T_{compile} ] [ T_{solve} ] [ Memory ] [ Variables ] [ Constraints ] و: [ ValidationTime ] ثبت شود. 48. Scaling Ratio برای دو Case: [ ScalingRatio= \frac{T_2}{T_1} ] و: [ ConstraintGrowth= \frac{Constraints_2}{Constraints_1} ] بررسی می‌شود. هدف این نیست که یک عدد ثابت از قبل تعیین کنیم؛ هدف این است که رفتار رشد مدل اندازه‌گیری شود. 49. Performance Regression هر Release باید با Benchmark قبلی مقایسه شود. اگر: V2.5-O نسبت به Release قبلی: Solve Time ↑ 80% ولی: Variables ↑ 10% باشد، Regression باید بررسی شود. 50. Golden Performance Dataset یک Dataset ثابت: benchmark/golden_network_v1 ایجاد شود. این Dataset نباید با داده Production مخلوط شود. 51. Production Data Benchmark بعداً: Benchmark Dataset + Anonymized Real Structure ساخته می‌شود. اطلاعات حساس مشتری/بازار نباید وارد Public Benchmark شود. 52. Database Performance PostgreSQL باید فقط Persistence Layer باشد. Solver نباید در هر Constraint Query بزند. اشتباه: for train: SELECT block... صحیح: Load Canonical Snapshot ↓ In-memory Domain ↓ Compile Model 53. Batch Loading به‌جای N Query: 1000 TrainRuns یک Batch Load انجام شود. Repository: get_train_runs( run_ids: list[str] ) 54. Snapshot Loading Run Manager: PostgreSQL ↓ Canonical Snapshot ↓ Hash ↓ In-Memory Domain سپس Engine فقط از Snapshot استفاده می‌کند. 55. Solver Isolation Solver Process بهتر است از API Process جدا باشد: API ↓ Run Manager ↓ Queue ↓ Solver Worker اگر CP-SAT Memory زیادی مصرف کرد، API نباید از کار بیفتد. 56. Job Queue Production: POST /runs ↓ Run CREATED ↓ Queue ↓ Worker ↓ Run COMPLETED API نباید Request را تا پایان Optimization Block کند. 57. Cancellation State: SOLVING → CANCEL_REQUESTED → CANCELLED Solver Worker باید Cancellation را بررسی کند. 58. Checkpoint در مراحل سنگین: Aggregate Complete TrainRuns Built Formation Complete Detailed Solve Validation Complete Checkpoint ثبت شود. اگر Worker Crash کرد، Run از آخرین Checkpoint قابل بازیابی باشد. 59. Checkpoint Integrity هر Checkpoint: @dataclass(frozen=True) class RunCheckpoint: run_id: str stage: str payload_hash: str created_at: str دارد. Payload تغییر کرده باشد: CHECKPOINT_INVALID 60. Memory Optimization از ساخت Objectهای تکراری جلوگیری شود. مثلاً: TrainType Route PhysicalBlock Station Immutable و Shared باشند. 61. Sparse Representation اگر فقط 5% Resourceها توسط یک Train استفاده می‌شوند، نباید Matrix کامل ساخته شود. به‌جای: [ Train\times Block ] از: Train → Used Blocks استفاده شود. 62. Time Domain Compression اگر Horizon: 0–10080 است، لازم نیست برای هر دقیقه همه Trainها Variable مستقل داشته باشند. Time Variables فقط برای: Station Calls Block Movements ساخته می‌شوند. 63. Candidate Time Window برای هر Train: earliest_departure latest_arrival باعث کاهش Domain می‌شود. اگر یک Train فقط: 600–900 می‌تواند حرکت کند، Domain آن نباید کل: 0–10080 باشد. 64. Variable Domain به‌جای: NewIntVar(0, 10080) تا حد امکان: NewIntVar(600, 900) ساخته شود. این می‌تواند Search Space را به‌شدت کاهش دهد. 65. Constraint Tightening اگر Minimum Running Time: 30 min و Maximum: 40 min باشد: X >= E + 30 X <= E + 40 هر دو Constraint ثبت شوند. 66. Baseline as Hint Baseline Schedule می‌تواند: Initial Hint باشد. نه: Hard Constraint مگر اینکه Scenario چنین تعریف کند. 67. Objective Hierarchy برای جلوگیری از Objective پیچیده و غیرشفاف: Lexicographic Objective پیشنهاد می‌شود. مثلاً: 1. Max Freight 2. Min Unserved 3. Min Delay 4. Min Empty Movement وزن‌ها باید در: ObjectiveDefinition ثبت شوند. 68. Multi-Stage Solve برای بعضی Caseها: Stage 1: Max Freight Stage 2: Fix Freight optimum Stage 3: Min Delay Stage 4: Min Empty Movement می‌تواند از یک Objective عظیم بهتر باشد. 69. Proof Performance Capacity Proof نباید کل Search Space را بدون دلیل تکرار کند. برای: F F+1 Input Snapshot ثابت است. می‌توان: Model Structure را Reuse کرد، ولی Proof باید همچنان از یک Evaluation معتبر مستقل باشد. 70. Proof Cache اگر دقیقاً همان: DataVersion InfrastructureVersion Scenario ModelVersion SolverConfiguration F قبلاً Proven شده باشد، Result قابل Reuse است. اما اگر: Scenario Change یا: Infrastructure Change وجود داشته باشد، Cache invalidate می‌شود. 71. Bottleneck Performance Bottleneck Engine نباید برای هر Resource یک Full Capacity Re-Solve انجام دهد. ابتدا: Binding Constraints + F/F+1 Conflicts + Slack را تحلیل کند. سپس فقط برای Bottleneckهای مهم: Marginal Scenario اجرا شود. 72. Marginal Scenario Budget مثلاً: Top 10 Bottlenecks و برای هرکدام: +1 Track +1 Loco +5 min Headway بررسی شود. نه برای تمام Resourceهای شبکه. 73. Interaction Analysis اگر: B03 Wagon Pool A هر دو Binding باشند، ممکن است افزایش یکی ظرفیت را افزایش ندهد چون دیگری هنوز محدودکننده است. پس: [ \Delta C(B03) ] به‌تنهایی همیشه کافی نیست. 74. Bottleneck Interaction Graph B03 Single Track │ ├──── Wagon Pool A │ └──── Loco Pool L1 این Graph در Decision Support استفاده می‌شود. 75. Network Decomposition برای شبکه بسیار بزرگ: Network ├── Corridor A ├── Corridor B ├── Corridor C └── Shared Hub می‌توان ابتدا Subproblemها را حل کرد و سپس Shared Resources را در Master Problem کنترل کرد. 76. اما یک محدودیت مهم Decomposition نباید به این نتیجه منجر شود که: Corridor A feasible + Corridor B feasible = Network feasible این الزاماً صحیح نیست. Shared Junction/Station/Wagon/Loco می‌تواند Network را infeasible کند. 77. Master Problem ساختار: Master ├── Route Allocation ├── Shared Resource ├── Wagon Pool └── Locomotive Pool و: Subproblem └── Detailed Time-Space Scheduling 78. Benders-Like Architecture در آینده می‌توان از الگوی: Master ↓ Detailed Subproblem ↓ Conflict / Cut ↓ Master استفاده کرد. در V2.5-O الزاماً لازم نیست Benders کامل پیاده شود؛ Contract باید طوری طراحی شود که بعداً قابل اضافه شدن باشد. 79. Cut Representation مثلاً Detailed نشان دهد: Candidate C1 + Candidate C2 نمی‌توانند هم‌زمان بیش از مقدار مشخصی Train داشته باشند. Feedback می‌تواند به شکل: Resource Capacity Cut برگردد. 80. Conflict Cut ساختار: @dataclass(frozen=True) class NetworkCut: id: str resource_id: str candidate_ids: tuple[str, ...] maximum_combined_frequency: int reason: str evidence_ids: tuple[str, ...] این یک پل مهم بین Aggregate و Detailed است. 81. Performance Architecture نهایی ┌──────────────────┐ │ Candidate Prune │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ Aggregate Solver │ └────────┬─────────┘ ↓ TrainRun Builder ↓ ┌──────────────────┐ │ Detailed Compiler│ └────────┬─────────┘ ↓ CP-SAT Solver ↓ Independent Validator ↓ ┌────────────┴────────────┐ │ │ VALID INVALID │ │ ↓ ↓ Proof Feedback/Cut │ │ └──────────────→ Aggregate 82. Production Architecture API │ ↓ Run Manager │ ↓ Queue │ ┌─────────┴─────────┐ ↓ ↓ Aggregate Worker Scenario Worker │ │ └─────────┬─────────┘ ↓ Detailed Worker ↓ Validator ↓ Proof Engine ↓ PostgreSQL 83. Observability Metrics: run.duration aggregate.duration detailed.duration validation.duration proof.duration model.variables model.constraints conflict.candidates conflict.actual train_runs.count blocks.count stations.count solver.status solver.objective solver.gap 84. Structured Logging هر Log حداقل: { "run_id": "RUN-001", "scenario_id": "SC-001", "stage": "SOLVING_DETAILED", "model_version": "2.5-O" } داشته باشد. 85. No Business Logic in Logs Log فقط Evidence است. مثلاً: B03 utilization=98% به‌تنهایی نباید بنویسد: B03 is bottleneck مگر Bottleneck Engine آن را با Evidence تعیین کرده باشد. 86. Acceptance Criteria V2.5-O زمانی قبول است که: Performance Candidate Pruning فعال باشد. Conflict Pair Generation Resource-based باشد. Temporal Pruning فعال باشد. Time Domains محدود باشند. Snapshot Loading Batch باشد. Solver از Database Query مستقیم استفاده نکند. Correctness Single Track صحیح باشد. Double Track صحیح باشد. Junction Matrix صحیح باشد. Station Track صحیح باشد. Independent Validation باقی بماند. UNKNOWN ≠ INFEASIBLE باشد. Scalability Benchmark Suite وجود داشته باشد. Solver Metrics ثبت شود. Performance Regression Test وجود داشته باشد. Run Budget وجود داشته باشد. Cancellation وجود داشته باشد. Checkpoint وجود داشته باشد. Proof F معتبر باشد. F+1 واقعاً بررسی شود. F+1 UNKNOWN → NOT PROVEN. Validator مستقل باشد. 87. خروجی V2.5-O پس از این مرحله، Engine باید بتواند بین دو سؤال کاملاً متفاوت تمایز بگذارد: سؤال اول «با این Allocation آیا برنامه زمانی دقیق قابل اجرا وجود دارد؟» پاسخ: Detailed Scheduler سؤال دوم «ظرفیت اثبات‌شده شبکه چقدر است؟» پاسخ: Capacity Evaluation + Independent Validation + Proof Engine سؤال سوم «چرا بیشتر از این ظرفیت نداریم؟» پاسخ: Bottleneck Engine + Conflict Evidence + Marginal Scenario 88. اصل نهایی V2.5-O سه چیز نباید با هم اشتباه شوند: [ Performance \neq Feasibility \neq Proof ] سریع‌تر حل شدن یک Model به معنی درست‌تر بودن آن نیست. Feasible شدن Solver به معنی Valid شدن Schedule نیست. و Valid شدن یک Schedule به معنی Proven شدن Capacity نیست. زنجیره نهایی: [ \boxed{ Fast\ Solver \rightarrow Valid\ Schedule \rightarrow Independent\ Validation \rightarrow Capacity\ Proof } ] و در مقیاس شبکه: [ \boxed{ Candidate\ Pruning \rightarrow Aggregate \rightarrow Detailed \rightarrow Validation \rightarrow Feedback \rightarrow Reoptimization \rightarrow Proof } ] این معماری، پایه لازم برای عبور از یک Capacity Calculator به یک Production Railway Network Optimization Engine است. گام بعدی منطقی، V2.5-P — Marketplace ↔ Capacity Engine ↔ Allocation است؛ یعنی وصل کردن کامل موتور ظرفیت به Marketplace تا Market Request به Demand، سپس Capacity Offer و در نهایت Allocation تبدیل شود، با حفظ Traceability کامل از درخواست مشتری تا قطار، واگن، لکوموتیو، برنامه زمانی و ظرفیت اثبات‌شده. --- ## ادامه 83 — پرسش کاربر (`message-id:477462e5-948d-41a5-914f-8c1b2868f645`) بله دقیقا --- ## ادامه 83 — پاسخ دستیار (`message-id:95ef5080-d5ed-4049-9da3-e3feb31b4210`) عالیه. حالا دقیقاً به نقطه‌ای می‌رسیم که Marketplace دیگر یک سیستم جانبی نیست و به‌صورت رسمی به لایه بالادستی موتور ظرفیت، تخصیص و برنامه‌ریزی تبدیل می‌شود. در V2.5-P، مرز بین Market Demand، Transportable Demand، Capacity Offer و Allocation را کاملاً رسمی می‌کنیم؛ به‌خصوص اینکه Marketplace نباید مستقیماً با CP-SAT یا منطق داخلی Solver صحبت کند. V2.5-P — Railway Marketplace Integration, Capacity Offer & Allocation V2.5-P — Railway Marketplace Integration, Capacity Offer & Allocation 1. هدف هدف V2.5-P ایجاد Integration رسمی بین: Railway Marketplace ↓ Market Request ↓ Demand ↓ Capacity Engine ↓ Feasible Capacity ↓ Capacity Offer ↓ Allocation ↓ Train Formation ↓ Schedule ↓ Wagon / Locomotive است. اصل بنیادی: Marketplace نباید ظرفیت را حدس بزند؛ Capacity Engine باید ظرفیت قابل ارائه را تولید کند. و: Capacity Engine نباید مستقیماً منطق تجاری Marketplace را اجرا کند؛ Marketplace و Capacity Engine از طریق Canonical Contract با یکدیگر ارتباط دارند. 2. معماری کلان ┌─────────────────────────────┐ │ Railway Marketplace │ │ │ │ Market Request │ │ Customer │ │ Commodity │ │ OD │ │ Requested Volume │ │ Requested Time │ └──────────────┬──────────────┘ │ ↓ ┌─────────────────────────────┐ │ Marketplace Adapter │ │ │ │ API Mapping │ │ Validation │ │ Identity Mapping │ │ Unit Conversion │ └──────────────┬──────────────┘ │ ↓ ┌─────────────────────────────┐ │ Canonical Demand Model │ └──────────────┬──────────────┘ │ ↓ ┌─────────────────────────────┐ │ Capacity Engine │ │ │ │ Aggregate │ │ Detailed Scheduler │ │ Rolling Stock │ │ Validation │ │ Proof │ └──────────────┬──────────────┘ │ ↓ ┌─────────────────────────────┐ │ Capacity Offer │ └──────────────┬──────────────┘ │ ↓ ┌─────────────────────────────┐ │ Allocation │ └──────────────┬──────────────┘ │ ↓ ┌─────────────────────────────┐ │ Train Formation / Schedule │ └─────────────────────────────┘ 3. اصل مهم: Marketplace ≠ Capacity Engine Marketplace مسئول: Customer Request Commercial Terms Offer Reservation Allocation است. Capacity Engine مسئول: Infrastructure Train Schedule Wagon Locomotive Operational Constraints Capacity Feasibility Proof است. 4. Canonical Chain مدل رسمی: MarketRequest ↓ Demand ↓ FreightFlow ↓ ODPair ↓ TrainCandidate ↓ CapacityCandidate ↓ CapacityOffer ↓ Allocation ↓ TrainFormation ↓ TrainRun ↓ Schedule 5. MarketRequest @dataclass(frozen=True) class MarketRequest: id: str customer_id: str origin: str destination: str commodity_id: str requested_tons: float earliest_departure: int latest_arrival: int wagon_type_id: str | None service_class: str | None این Entity متعلق به Business Layer است. 6. Demand MarketRequest الزاماً مستقیماً وارد Solver نمی‌شود. ابتدا: MarketRequest ↓ Demand Normalization ↓ Demand مثلاً: Requested: 3,500 tons Normalized: 3,500 tons OD: GAR → SAKHEH Commodity: Steel 7. Market Demand تعریف: [ D_{market} ] حجم درخواست‌شده در Marketplace است. مثلاً: [ D_{market}=3500t ] 8. Transportable Demand ممکن است بخشی از Market Demand از نظر عملیاتی قابل حمل نباشد. مثلاً: Market Demand = 3500 t Transportable Demand = 3000 t به دلیل: Wagon Compatibility Train Formation Terminal Capacity Route Availability 9. Allocated Demand حتی Transportable Demand هم ممکن است به‌دلیل Capacity محدود نشود. مثلاً: Market Demand = 3500 Transportable = 3000 Capacity Allocated = 2000 پس: [ D_{market} \neq D_{transportable} \neq D_{allocated} ] این سه مقدار باید در Database جداگانه نگهداری شوند. 10. Demand Status RECEIVED NORMALIZED VALIDATED TRANSPORTABLE PARTIALLY_TRANSPORTABLE CAPACITY_LIMITED ALLOCATED PARTIALLY_ALLOCATED REJECTED CANCELLED 11. Demand Validation قبل از Capacity Engine: OD Exists? Commodity Exists? Wagon Type Exists? Time Window Valid? Requested Tons > 0? Customer Valid? اگر Mapping وجود نداشته باشد: DEMAND_MAPPING_ERROR و نباید به Solver ارسال شود. 12. Demand-to-Capacity Contract @dataclass(frozen=True) class CapacityRequest: request_id: str demand_id: str od_pair_id: str commodity_id: str requested_tons: float earliest_departure: int latest_arrival: int wagon_type_id: str | None preferred_route_ids: tuple[str, ...] allowed_train_type_ids: tuple[str, ...] 13. چرا Contract لازم است؟ Marketplace نباید بداند: CP-SAT PhysicalBlock Boolean Order Variable NoOverlap Marketplace فقط باید بداند: OD Commodity Volume Time Service Capacity Allocation 14. Capacity Engine Response @dataclass(frozen=True) class CapacityResponse: request_id: str transportable_tons: float allocatable_tons: float capacity_status: str offer_ids: tuple[str, ...] limitation_codes: tuple[str, ...] 15. Capacity Profile ظرفیت یک عدد ساده نیست. CapacityProfile باید حداقل بر اساس: Route OD TrainType Commodity LoadState TimeBucket Direction Station WagonType LocomotiveType قابل تفکیک باشد. یعنی: [ C=f( Route, OD, TrainType, Commodity, Time, Direction, Wagon, Locomotive ) ] 16. Capacity Offer Marketplace نباید نتیجه خام Solver را دریافت کند. یک لایه: Capacity Engine ↓ Capacity Offer Builder ↓ Marketplace ایجاد می‌شود. 17. CapacityOffer @dataclass(frozen=True) class CapacityOffer: id: str request_id: str od_pair_id: str route_id: str train_type_id: str available_tons: float available_trains: int earliest_departure: int latest_arrival: int direction: str capacity_status: str proof_status: str expires_at: int | None 18. Offer Status DRAFT VALIDATING AVAILABLE PARTIALLY_AVAILABLE RESERVED ALLOCATED EXPIRED CANCELLED SUPERSEDED 19. Proof Status این فیلد با Offer Status فرق دارد. NOT_EVALUATED AGGREGATE_UPPER_BOUND DETAILED_FEASIBLE VALIDATED PROVEN NOT_PROVEN UNKNOWN 20. نکته بسیار مهم مثلاً: available_tons = 3000 proof_status = AGGREGATE_UPPER_BOUND نباید در Marketplace با: PROVEN اشتباه شود. 21. Capacity Offer Types سه نوع Offer می‌توان تعریف کرد: A. Indicative Capacity برای پاسخ سریع اولیه. Indicative ممکن است فقط Aggregate باشد. B. Feasible Capacity Detailed Schedule ساخته شده و Validation موفق است. C. Proven Capacity ظرفیت با Proof Engine اثبات شده است. 22. Offer Semantics INDICATIVE ↓ FEASIBLE ↓ PROVEN اما این مسیر الزاماً اجباری نیست. یک Offer می‌تواند مستقیماً: FEASIBLE باشد بدون اینکه Capacity Search کامل انجام شده باشد. 23. Allocation Allocation یعنی: چه مقدار از Demand به چه Capacity/Train Service اختصاص داده شده است. @dataclass(frozen=True) class Allocation: id: str demand_id: str offer_id: str allocated_tons: float train_count: int allocation_status: str 24. Partial Allocation اگر: Demand = 3500 Capacity = 2000 می‌توان: Allocation = 2000 Unserved = 1500 داشت. Marketplace نباید این دو را یکی بداند. 25. Allocation Status REQUESTED PROPOSED RESERVED CONFIRMED PARTIALLY_CONFIRMED RELEASED CANCELLED EXPIRED 26. Allocation → Train Formation پس از Allocation: Allocation ↓ Wagon Requirement ↓ Train Formation مثلاً: 2000 tons ÷ 100 tons/wagon = 20 wagons اگر Train Formation: 10 wagons/train باشد: [ F=2 ] 27. Formation Constraint ممکن است Marketplace درخواست: 2000 tons داشته باشد ولی Formation فقط: 2 × 900 tons را اجازه دهد. در این صورت: Transportable Demand = 1800 نه 2000. 28. Capacity-to-Commercial Unit Marketplace ممکن است با: Ton کار کند. Railway Operations ممکن است با: Train Wagon Path کار کند. بنابراین Conversion Layer لازم است. [ Tons \rightarrow Wagons \rightarrow Train \rightarrow TrainPath ] 29. Unit Conversion class CapacityUnitConverter: def tons_to_wagons( self, tons: float, wagon_payload: float, ) -> int: ... def wagons_to_trains( self, wagons: int, formation_size: int, ) -> int: ... Rounding Rule باید Explicit باشد. 30. No Silent Rounding مثلاً: [ 1500/100=15 ] اما: [ 1550/100=15.5 ] نمی‌تواند بدون Rule به: 15 تبدیل شود. باید: CEIL FLOOR PARTIAL بر اساس Business Rule تعیین شود. 31. Capacity Offer Expiration Capacity ممکن است Dynamic باشد. Offer: created_at expires_at داشته باشد. اگر: Infrastructure Change Demand Change Another Allocation رخ دهد، Offer می‌تواند: SUPERSEDED شود. 32. Capacity Reservation Reservation با Allocation یکی نیست. Offer ↓ Reservation ↓ Allocation Reservation می‌تواند موقت باشد. 33. جلوگیری از Overbooking اگر: Capacity = 10 trains و: Allocation A = 6 Allocation B = 5 نباید هر دو Confirm شوند. قید: [ \sum_a F_a \le C ] 34. Shared Capacity اگر دو OD: OD1 → Route A OD2 → Route B اما هر دو از: Block B03 استفاده کنند، ظرفیت Marketplace باید Shared Resource را لحاظ کند. بنابراین: Offer Capacity نمی‌تواند صرفاً مستقل برای هر OD محاسبه شود. 35. Capacity Pool @dataclass(frozen=True) class CapacityPool: id: str resource_scope: tuple[str, ...] total_capacity: float allocated_capacity: float remaining_capacity: float 36. Allocation Guard قبل از Confirm: Demand valid? Offer active? Capacity available? No shared-resource violation? Wagon available? Locomotive available? Schedule still valid? 37. Revalidation Allocation قدیمی ممکن است پس از تغییر شبکه دیگر معتبر نباشد. پس: Allocation ↓ Revalidation ↓ VALID / INVALID باید وجود داشته باشد. 38. Allocation Version هر Allocation: capacity_run_id scenario_id data_version_id را نگه می‌دارد. این باعث Traceability می‌شود. 39. Full Traceability مثلاً: MarketRequest MR-100 ↓ Demand D-100 ↓ FreightFlow FF-20 ↓ CapacityRequest CR-50 ↓ CapacityRun RUN-900 ↓ Candidate C-12 ↓ CapacityOffer CO-700 ↓ Allocation AL-800 ↓ TrainFormation TF-30 ↓ TrainRun TR-100 ↓ Schedule SCH-40 ↓ WagonCycle WC-90 ↓ LocomotiveCycle LC-10 هر مرحله باید قابل Trace باشد. 40. Traceability Entity @dataclass(frozen=True) class MarketCapacityTrace: request_id: str demand_id: str capacity_request_id: str capacity_run_id: str candidate_id: str | None offer_id: str | None allocation_id: str | None train_formation_ids: tuple[str, ...] train_run_ids: tuple[str, ...] schedule_id: str | None 41. Why This Matters اگر مشتری بپرسد: چرا 3500 تن درخواست کردم ولی فقط 2000 تن Allocation شد؟ سیستم باید بتواند بگوید: Market Demand = 3500 Transportable = 3000 Network Capacity = 2200 Allocated = 2000 Unserved = 1500 و دلیل هر کاهش را مشخص کند. 42. Limitation Breakdown MARKET_LIMIT INFRASTRUCTURE_LIMIT SCHEDULE_LIMIT WAGON_LIMIT LOCOMOTIVE_LIMIT TERMINAL_LIMIT DEMAND_LIMIT POLICY_LIMIT 43. Example Explanation Requested: 3500 tons Transportable: 3000 tons Detailed feasible: 2200 tons Allocated: 2000 tons Remaining: 1500 tons Explanation: 500 tons limited by wagon availability 800 tons limited by shared route capacity 200 tons unallocated by commercial allocation policy اعداد بالا صرفاً نمونه هستند؛ Production باید آنها را از Evidence واقعی تولید کند. 44. Marketplace API Create Request POST /api/v1/market/requests Get Capacity POST /api/v1/capacity/requests Get Offers GET /api/v1/capacity/offers Reserve POST /api/v1/capacity/offers/{offer_id}/reserve Allocate POST /api/v1/allocations Revalidate POST /api/v1/allocations/{allocation_id}/revalidate 45. Marketplace Adapter اگر Marketplace خارجی یا مستقل باشد: External Marketplace ↓ Marketplace Adapter ↓ Canonical API Adapter مسئول Mapping است. 46. External ID هیچ‌گاه فرض نشود: Marketplace ID = Internal ID مدل: ExternalReference: system entity_type external_id internal_id 47. Identity Mapping مثلاً: Marketplace: Station "GAR" Internal: ST-IR-001 Mapping: GAR → ST-IR-001 باید Versioned و Auditable باشد. 48. Commodity Mapping مثلاً: Marketplace: IRON_ORE Canonical: COM-ORE-001 و: Allowed Wagon Types Hazard Class Loading Rule Payload به Commodity Mapping متصل می‌شوند. 49. API Idempotency Create Request باید Idempotent باشد. مثلاً: Idempotency-Key: ... اگر Request دوبار ارسال شد، نباید دو Demand ایجاد شود. 50. Event-Driven Integration برای Production می‌توان: MarketRequestCreated DemandValidated CapacityCalculated OfferPublished OfferReserved AllocationConfirmed AllocationCancelled را Event کرد. 51. Event Example { "event_type": "CapacityOfferPublished", "offer_id": "CO-100", "request_id": "MR-100", "capacity_tons": 2000, "status": "AVAILABLE" } 52. Event Ordering Eventها باید: event_id aggregate_id version timestamp correlation_id causation_id داشته باشند. 53. Correlation ID یک درخواست Marketplace: MR-100 باید در تمام سیستم قابل Trace باشد: MR-100 ↓ CR-100 ↓ RUN-100 ↓ CO-100 ↓ AL-100 54. Commercial vs Operational State این دو نباید مخلوط شوند. Commercial REQUESTED RESERVED CONFIRMED CANCELLED Operational FEASIBLE SCHEDULED VALIDATED PROVEN 55. مثال مهم ممکن است: Commercial Status = RESERVED Operational Status = NOT_VALIDATED باشد. این یک وضعیت معتبر معماری است، ولی سیستم نباید آن را به معنای «قطار قطعاً قابل اجراست» نمایش دهد. 56. Allocation Confirmation Rule برای Confirm نهایی: Demand Valid AND Offer Active AND Capacity Available AND Detailed Feasible AND Validation Passed و در صورت نیاز Business Policy: AND Commercial Approval 57. Proven Capacity اگر Business نیاز داشته باشد Offer فقط بر اساس Capacity اثبات‌شده ارائه شود: proof_status = PROVEN باید شرط Offer باشد. در غیر این صورت Offer می‌تواند Feasible باشد، ولی نوع آن باید صریحاً مشخص شود. 58. Marketplace Capacity View Marketplace یک View ساده دریافت می‌کند: OD Departure Window Arrival Window Available Tons Available Trains Route Service Wagon Type Offer Status Validity Capacity Evidence Level جزئیات CP-SAT لازم نیست در UI اصلی Marketplace نمایش داده شود. 59. Capacity Explanation API اما برای Drill-down: GET /api/v1/capacity/offers/{offer_id}/explanation مثلاً: { "requested_tons": 3500, "transportable_tons": 3000, "available_tons": 2000, "limitation_codes": [ "SHARED_SINGLE_TRACK", "WAGON_POOL_LIMIT" ] } 60. Marketplace Allocation Optimizer وقتی چند Customer هم‌زمان ظرفیت می‌خواهند، مسئله دیگر فقط Capacity Calculation نیست. مثلاً: Customer A → 3000t Customer B → 2000t Customer C → 1500t Network Capacity = 4000t حالا Allocation Policy لازم است. 61. Separation of Concerns Capacity Engine: "How much is operationally possible?" Allocation Engine: "How should available capacity be assigned?" این دو نباید یکی شوند. 62. Allocation Policy ممکن است سیاست‌ها شامل: FIRST_COME_FIRST_SERVED PRIORITY_CUSTOMER CONTRACT_PRIORITY MINIMUM_SERVICE FAIR_SHARE REVENUE_MAXIMIZATION باشد. اینها Business Policy هستند، نه Infrastructure Capacity. 63. Policy Constraint Policy باید Versioned باشد: PolicyConstraint: id scenario_id policy_type parameters priority effective_from effective_to 64. Fairness اگر Fair Allocation لازم باشد، باید به‌صورت Objective/Constraint رسمی تعریف شود. مثلاً: [ Allocation_i \ge \alpha Demand_i ] اما مقدار (\alpha) باید Policy باشد، نه فرض Solver. 65. Revenue Objective اگر Marketplace هدف Revenue داشته باشد: [ \max \sum_i Revenue_i ] این Objective باید از: MAX_FREIGHT جدا باشد. 66. Multiple Objectives ممکن است: [ \max ( Freight, Revenue, ServiceLevel ) ] داشته باشیم. اما Objective Definition باید دقیقاً ثبت کند که Priority چگونه است. 67. No Hidden Commercial Optimization Capacity Engine نباید بدون اطلاع: Customer A را به: Customer B ترجیح دهد. اگر چنین ترجیحی وجود دارد، باید از: PolicyConstraint ObjectiveDefinition بیاید. 68. Capacity Publication Capacity Offer فقط وقتی Publish شود که: Run = VALID Offer = VALID Input Version = ACTIVE باشد. 69. Data Version Binding Offer: CO-100 باید به: DataVersion DV-20 InfrastructureVersion IV-15 CalibrationVersion CV-7 ModelVersion V2.5-P متصل باشد. 70. Re-Publishing اگر Infrastructure Version تغییر کرد: IV-15 → IV-16 Offerهای وابسته باید: REVALIDATION_REQUIRED شوند. 71. Capacity Offer Lifecycle GENERATED ↓ VALIDATED ↓ PUBLISHED ↓ RESERVED ↓ ALLOCATED ↓ CONSUMED مسیرهای جانبی: EXPIRED CANCELLED SUPERSEDED INVALIDATED 72. Reservation Race Condition دو کاربر هم‌زمان: Reserve 1000t می‌خواهند. سیستم باید Atomic Capacity Check داشته باشد. مثلاً: [ Available - Requested \ge 0 ] در یک Transaction معتبر بررسی شود. 73. Operational Recheck حتی بعد از Reservation، قبل از Final Allocation: Capacity Recheck انجام شود. 74. Allocation → Operational Execution پس از Confirm: Allocation ↓ Train Formation ↓ TrainRun ↓ Detailed Schedule ↓ Operational Dispatch این مرز بعداً می‌تواند به سیستم‌های عملیاتی/Dispatch متصل شود. 75. Marketplace-to-Solver Anti-Pattern نباید داشته باشیم: Marketplace ↓ CP-SAT API و حتی بدتر: Marketplace ↓ CP-SAT variables معماری صحیح: Marketplace ↓ Canonical Contract ↓ Capacity Application Service ↓ Capacity Engine 76. Capacity Application Service class CapacityApplicationService: def evaluate( self, request: CapacityRequest, ) -> CapacityResponse: ... def publish_offer( self, capacity_run_id: str, ) -> tuple[CapacityOffer, ...]: ... def allocate( self, offer_id: str, tons: float, ) -> Allocation: ... 77. Repository Boundary Application Service با: DemandRepository CapacityRunRepository OfferRepository AllocationRepository کار می‌کند. Solver مستقیماً Repository را صدا نمی‌زند. 78. End-to-End Flow 1. Customer creates Market Request 2. Marketplace validates commercial fields 3. Adapter maps external IDs 4. Canonical Demand created 5. CapacityRequest generated 6. Candidate Routes generated 7. Candidate Pruning 8. Aggregate Optimization 9. TrainRun Expansion 10. Formation Check 11. Wagon Check 12. Locomotive Check 13. Detailed CP-SAT 14. Independent Validation 15. Capacity Evaluation 16. Proof 17. Bottleneck Analysis 18. Capacity Offer generated 19. Marketplace receives Offer 20. Customer reserves 21. Allocation confirmed 22. Train Formation created 23. Operational Schedule linked 79. End-to-End Trace مثال: MR-001 ↓ D-001 ↓ CR-001 ↓ RUN-001 ↓ CAND-004 ↓ TF-010 ↓ TR-101 TR-102 ↓ SCH-900 ↓ CO-100 ↓ AL-200 این Trace باید در UI و API قابل مشاهده باشد. 80. Database Extensions در Schema marketplace: market_request market_customer market_commodity market_service market_external_reference capacity_request capacity_offer capacity_offer_item reservation allocation allocation_item market_capacity_trace 81. Allocation Item برای Partial Allocation: AllocationItem: allocation_id demand_id offer_id tons train_count route_id train_type_id 82. Capacity Offer Item یک Offer می‌تواند چند Service Option داشته باشد: Offer CO-100 ├── Option 1: Route A / 1000t ├── Option 2: Route B / 800t └── Option 3: Route C / 1200t این برای Marketplace بسیار مهم است. 83. Offer Option @dataclass(frozen=True) class CapacityOfferItem: id: str offer_id: str route_id: str train_type_id: str capacity_tons: float train_count: int departure_window: tuple[int, int] arrival_window: tuple[int, int] proof_status: str 84. Alternative Routes Marketplace می‌تواند ببیند: Same OD Different Route Different Time Different Service اما Route انتخابی باید بر اساس Objective/Preference مشخص شود. 85. Preferred Route Customer ممکن است بگوید: Preferred Route = R01 Alternative = R02 این Preference است، نه الزام. اگر Mandatory باشد باید: RouteConstraint ثبت شود. 86. Marketplace Search Endpoint: POST /api/v1/market/capacity/search ورودی: { "origin": "GAR", "destination": "SAKHEH", "commodity": "ORE", "tons": 3000, "earliest_departure": 480, "latest_arrival": 1440 } خروجی: Offer A Offer B Offer C هر Offer دارای Evidence Level است. 87. Search vs Proof Search سریع: Aggregate / Cached / Feasible Proof: Detailed + Validation + F/F+1 می‌تواند زمان بیشتری ببرد. Marketplace باید این تفاوت را بداند. 88. SLA Classes می‌توان Requestها را به: FAST_INDICATIVE STANDARD_FEASIBILITY PROVEN_CAPACITY تقسیم کرد. مثلاً: FAST_INDICATIVE برای پاسخ سریع Marketplace. و: PROVEN_CAPACITY برای برنامه‌ریزی قطعی. 89. مهم: SLA نباید Correctness را تغییر دهد اگر: Fast Run به Timeout برسد: UNKNOWN نه: ZERO CAPACITY 90. API Error Model خطاها: INVALID_REQUEST UNKNOWN_STATION UNKNOWN_COMMODITY NO_ROUTE NO_CANDIDATE NO_CAPACITY CAPACITY_UNKNOWN PROOF_NOT_ESTABLISHED OFFER_EXPIRED ALLOCATION_CONFLICT VERSION_OUTDATED 91. Explainable No-Capacity به‌جای: No Capacity خروجی: Capacity Status: LIMITED Reasons: - Single Track Conflict - Wagon Pool Limit - Terminal Capacity و هر Reason باید Evidence داشته باشد. 92. Capacity Explainability Object @dataclass(frozen=True) class CapacityExplanation: offer_id: str | None requested_tons: float transportable_tons: float feasible_tons: float allocated_tons: float unserved_tons: float limitation_codes: tuple[str, ...] bottleneck_ids: tuple[str, ...] evidence_ids: tuple[str, ...] 93. Security Boundary Marketplace نباید بتواند: Scenario Infrastructure Solver Configuration Proof Result را بدون Permission تغییر دهد. Marketplace فقط: Request Reservation Allocation را مدیریت می‌کند. 94. Audit هر تغییر: Offer Reservation Allocation باید: who when what old value new value reason correlation_id داشته باشد. 95. Capacity Integrity اگر Offer: CO-100 بر اساس: Run-900 تولید شده، تغییر Run نباید بدون ایجاد Version جدید Offer را تغییر دهد. 96. Immutable Published Offer پس از Publish: CapacityOffer بهتر است Immutable باشد. اگر تغییر لازم شد: CO-100 ↓ SUPERSEDED ↓ CO-101 97. Allocation Immutability Allocation Confirmed نیز نباید silent update شود. اصلاح باید: Amendment Cancellation Replacement باشد. 98. Market-to-Capacity Traceability Model این مدل یکی از مهم‌ترین خروجی‌های V2.5-P است: [ \boxed{ MarketRequest \rightarrow Demand \rightarrow CapacityRequest \rightarrow Candidate \rightarrow CapacityRun \rightarrow CapacityOffer \rightarrow Allocation \rightarrow TrainFormation \rightarrow TrainRun \rightarrow Schedule } ] 99. Capacity-to-Market Traceability جهت معکوس نیز باید ممکن باشد: TrainRun ↓ Allocation ↓ Offer ↓ Demand ↓ MarketRequest بنابراین می‌توان پرسید: این قطار برای کدام Demand تشکیل شده است؟ و: این Market Request در کدام قطار/قطارها تخصیص یافته است؟ 100. Capacity Consumption بعد از اجرای واقعی: Allocated Capacity به: Consumed Capacity تبدیل می‌شود. پس: [ Allocated \neq Consumed ] ممکن است قطار Cancel شود، ناقص بارگیری شود یا کمتر از ظرفیت حمل کند. 101. Future Operational Feedback در نسخه بعدی: Planned ↓ Executed ↓ Actual به مدل اضافه می‌شود. و سپس: Actual ↓ Calibration ↓ Future Capacity برمی‌گردد. 102. Feedback Loop معماری کامل: Marketplace ↓ Demand ↓ Capacity ↓ Allocation ↓ Planning ↓ Execution ↓ Actual Data ↓ Calibration ↓ Capacity Engine ↓ Marketplace این همان حلقه‌ای است که سیستم را از یک Solver به یک Learning Operational Planning Platform نزدیک می‌کند. 103. Acceptance Tests Test P1 — Full Demand Demand = 3000t Capacity = 3000t Allocation = 3000t Test P2 — Partial Capacity Demand = 3000t Capacity = 2000t Allocation <= 2000t Test P3 — Shared Resource دو Demand نباید مجموعاً بیش از Shared Capacity تخصیص بگیرند. Test P4 — Offer Expiry Offer منقضی‌شده نباید Allocate شود. Test P5 — Version Change Infrastructure Change باید Offer را Revalidate کند. Test P6 — Idempotency یک MarketRequest تکراری نباید Demand تکراری ایجاد کند. Test P7 — Traceability از MarketRequest تا TrainRun باید Trace کامل وجود داشته باشد. Test P8 — UNKNOWN Solver Timeout نباید به NO_CAPACITY تبدیل شود. 104. Production Definition of Done V2.5-P کامل است وقتی: Marketplace Contract رسمی شده باشد. MarketRequest به Canonical Demand تبدیل شود. CapacityRequest Contract پیاده‌سازی شده باشد. Candidate/Capacity Run قابل Trace باشد. CapacityOffer مستقل از Solver Model باشد. Allocation مستقل از Capacity Calculation باشد. Partial Allocation پشتیبانی شود. Shared Capacity کنترل شود. Reservation/Allocation Race Condition کنترل شود. Offer Versioning وجود داشته باشد. Revalidation وجود داشته باشد. Market-to-Capacity Traceability کامل باشد. Capacity Explanation قابل ارائه باشد. Commercial و Operational Status جدا باشند. Proof Status صریح باشد. API و Event Contract تعریف شده باشند. Audit کامل باشد. Testهای End-to-End برقرار باشند. 105. معماری نهایی V2.5-P MARKETPLACE │ ↓ ┌─────────────────┐ │ MarketRequest │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Demand │ └────────┬────────┘ ↓ ┌─────────────────┐ │Capacity Request │ └────────┬────────┘ ↓ ┌──────────────────────┐ │ Candidate Generation │ └──────────┬───────────┘ ↓ ┌──────────────────────┐ │ Aggregate Optimization│ └──────────┬───────────┘ ↓ TrainRun Builder ↓ ┌──────────────────────┐ │ Detailed CP-SAT │ └──────────┬───────────┘ ↓ Independent Validation ↓ Capacity ↓ Proof ↓ Capacity Offer ↓ Reservation ↓ Allocation ↓ Train Formation ↓ Train Schedule ↓ Execution 106. اصل معماری نهایی سیستم باید همیشه بتواند این پنج سؤال را جداگانه پاسخ دهد: سؤال ۱ بازار چه می‌خواهد؟ Market Demand سؤال ۲ از نظر فیزیکی/عملیاتی چه مقدار قابل حمل است؟ Transportable Demand سؤال ۳ با برنامه زمانی دقیق چه مقدار واقعاً Feasible است؟ Detailed Feasible Capacity سؤال ۴ چه مقدار Capacity با سطح اطمینان مشخص می‌توان عرضه کرد؟ Capacity Offer سؤال ۵ چه مقدار از آن به هر Demand تخصیص یافته است؟ Allocation 107. فرمول نهایی Marketplace Integration [ D_{market} \rightarrow D_{transportable} \rightarrow C_{feasible} \rightarrow C_{offered} \rightarrow C_{allocated} ] با این قیود: [ C_{allocated} \le C_{offered} \le C_{feasible} ] و: [ D_{allocated} \le D_{market} ] اما مقدار C_feasible تنها زمانی ظرفیت عملیاتی معتبر محسوب می‌شود که Schedule متناظر آن توسط Validator تأیید شده باشد. و اگر سیستم ادعای Proven Capacity دارد: [ \boxed{ F\ feasible \land Validate(F) \land F+1\ infeasible } ] باید برقرار باشد. 108. نتیجه V2.5-P با تکمیل این مرحله، معماری سیستم دیگر صرفاً: Railway Capacity Solver نیست. بلکه به: Railway Marketplace + Demand Management + Capacity Optimization + Detailed Scheduling + Rolling Stock Planning + Allocation + Capacity Proof + Explainability + Traceability تبدیل می‌شود. مرز اصلی سیستم نیز کاملاً روشن است: MARKETPLACE "What do customers want?" ↓ CAPACITY ENGINE "What can the railway actually carry?" ↓ ALLOCATION ENGINE "Who receives which part of that capacity?" ↓ OPERATIONS "Which exact trains, wagons, locomotives and time slots execute it?" و در نهایت: [ \boxed{ Market \rightarrow Demand \rightarrow Capacity \rightarrow Offer \rightarrow Allocation \rightarrow Formation \rightarrow Schedule \rightarrow Execution \rightarrow Actual \rightarrow Calibration } ] این حلقه، هسته Business/Operational پلتفرم نهایی را تشکیل می‌دهد. مرحله منطقی بعدی V2.5-Q خواهد بود: تبدیل کل این معماری به Production Implementation Blueprint؛ یعنی ساختار واقعی Repository، Python packages، PostgreSQL migrations، API contracts، Queue/Workerها، Domain Services، CP-SAT modules، Test Pyramid و ترتیب دقیق پیاده‌سازی از اولین commit تا اجرای End-to-End روی داده واقعی. --- ## ادامه 84 — پرسش کاربر (`message-id:a11c1aff-df3b-421f-96ce-a44b393e2ecb`) بله دقیقا --- ## ادامه 84 — پاسخ دستیار (`message-id:4931838d-10ba-4a8e-b61f-edf5fa7d336d`) دقیقاً. حالا از مرحله معماری و Specification عبور می‌کنیم و وارد Blueprint اجرایی Production می‌شویم؛ یعنی مشخص می‌کنیم این سیستم واقعاً چگونه در Repository ساخته شود، چه Packageهایی داشته باشد، Dependencyها چگونه باشند، Database Migration چگونه مدیریت شود، Worker و API چگونه به هم متصل شوند و اولین End-to-End واقعی دقیقاً از کجا شروع شود. V2.5-Q — Production Implementation Blueprint V2.5-Q — Production Implementation Blueprint 1. هدف V2.5-Q معماری V2.5 را به یک Implementation Blueprint قابل اجرا تبدیل می‌کند. هدف این نیست که فقط بگوییم: API Database Solver بلکه باید مشخص شود: Repository → Package → Module → Class → Interface → Migration → Worker → Test → Deployment چگونه به یک سیستم واحد تبدیل می‌شوند. 2. اصل اول: Modular Monolith با قابلیت استخراج Service در فاز اول Production، پیشنهاد معماری: Modular Monolith + Async Workers + PostgreSQL + CP-SAT است. نه Microservice کامل از روز اول. ساختار: API │ ├── Marketplace ├── Demand ├── Capacity ├── Planning ├── Allocation └── Results │ ↓ Application Layer │ ↓ Domain Layer │ ↓ Infrastructure Layer │ ├── PostgreSQL ├── Queue └── Solver Worker مزیت: Transaction ساده‌تر Debugging ساده‌تر Deployment ساده‌تر Traceability بهتر کاهش Distributed-System complexity اما Module Boundary از ابتدا حفظ می‌شود تا در آینده قابل جداشدن باشد. 3. Repository Structure ساختار پیشنهادی: railway-platform/ │ ├── pyproject.toml ├── README.md ├── Makefile ├── docker-compose.yml ├── .env.example │ ├── migrations/ │ ├── versions/ │ └── env.py │ ├── src/ │ └── railway/ │ │ │ ├── domain/ │ │ ├── infrastructure/ │ │ ├── route/ │ │ ├── train/ │ │ ├── rolling_stock/ │ │ ├── demand/ │ │ ├── marketplace/ │ │ ├── capacity/ │ │ ├── allocation/ │ │ ├── scenario/ │ │ ├── optimization/ │ │ ├── validation/ │ │ ├── proof/ │ │ └── result/ │ │ │ ├── application/ │ │ ├── runs/ │ │ ├── marketplace/ │ │ ├── capacity/ │ │ ├── allocation/ │ │ └── planning/ │ │ │ ├── infrastructure/ │ │ ├── database/ │ │ ├── repositories/ │ │ ├── queue/ │ │ ├── storage/ │ │ └── observability/ │ │ │ ├── optimization/ │ │ ├── aggregate/ │ │ ├── detailed/ │ │ ├── rolling_stock/ │ │ ├── network/ │ │ └── feedback/ │ │ │ ├── adapters/ │ │ ├── access/ │ │ ├── excel/ │ │ ├── marketplace/ │ │ └── legacy/ │ │ │ ├── api/ │ │ ├── routes/ │ │ ├── schemas/ │ │ └── dependencies/ │ │ │ └── config/ │ ├── tests/ │ ├── unit/ │ ├── integration/ │ ├── contract/ │ ├── optimization/ │ ├── validation/ │ ├── golden/ │ ├── performance/ │ └── e2e/ │ ├── benchmarks/ │ ├── small/ │ ├── medium/ │ ├── large/ │ └── production_like/ │ └── deployment/ ├── docker/ ├── worker/ └── monitoring/ 4. Domain Layer Domain باید مستقل از: FastAPI PostgreSQL CP-SAT Redis Docker باشد. یعنی: Domain ↓ Business Rules نه: Domain ↓ SQL Query 5. Domain Package Infrastructure domain/infrastructure/ شامل: Station StationTrack PhysicalBlock Junction JunctionMovement JunctionConflict OperationalWindow 6. Route Domain domain/route/ شامل: Route RouteSegment DirectedPath DirectedPathStation DirectedPathBlock Invariant اصلی: [ |Stations|=|Blocks|+1 ] برای یک مسیر ساده. 7. Train Domain domain/train/ شامل: TrainType TrainService OperatingCalendar TrainRun TrainStationCall TrainBlockMovement TrainOperationalProfile 8. Formation Domain domain/rolling_stock/ شامل: WagonType Wagon WagonPool WagonInventory WagonRequirement WagonCycle LocomotiveType Locomotive LocomotiveAssignment LocomotiveCycle TrainFormation TrainFormationItem EmptyWagonMovement 9. Demand Domain domain/demand/ شامل: ODPair Demand FreightFlow 10. Marketplace Domain domain/marketplace/ شامل: MarketRequest CapacityRequest CapacityOffer CapacityOfferItem Reservation Allocation AllocationItem MarketCapacityTrace ExternalReference 11. Capacity Domain domain/capacity/ شامل: CapacityProfile CapacityEvaluation CapacityStatus CapacityType 12. Scenario Domain domain/scenario/ شامل: Scenario ScenarioChange ObjectiveDefinition ObjectiveComponent PolicyConstraint Investment 13. Result Domain domain/result/ شامل: OptimizationResult Schedule ValidationResult Bottleneck Explanation CapacityProof CapacityEvidence 14. Domain Rules قوانین کلیدی باید در Domain قابل تست باشند. مثلاً: def validate_train_length( train_length: float, track_length: float, ) -> bool: return train_length <= track_length یا: def validate_station_path( stations, blocks, ) -> bool: return len(stations) == len(blocks) + 1 15. Application Layer Application Layer جریان Use Case را کنترل می‌کند. مثلاً: CreateMarketRequest EvaluateCapacity PublishOffer ReserveCapacity CreateAllocation RunCapacityPlanning ValidateRun ExplainCapacity Application Layer نباید Solver Algorithm را در خود داشته باشد. 16. Run Manager Use Case اصلی: class RunManager: def create_run(...) def execute_run(...) def cancel_run(...) def resume_run(...) Run Manager فقط Orchestrator است. 17. Run State Machine CREATED ↓ VALIDATING_INPUT ↓ READY ↓ SOLVING_AGGREGATE ↓ BUILDING_TRAIN_RUNS ↓ CHECKING_FORMATION ↓ CHECKING_WAGON ↓ CHECKING_LOCOMOTIVE ↓ SOLVING_DETAILED ↓ VALIDATING_RESULT ↓ PROVING_CAPACITY ↓ ANALYZING_BOTTLENECKS ↓ GENERATING_EXPLANATION ↓ PUBLISHING_CAPACITY ↓ COMPLETED 18. Solver Package optimization/ از Domain جداست. ساختار: optimization/ │ ├── aggregate/ │ ├── model.py │ ├── variables.py │ ├── constraints.py │ ├── objective.py │ └── solver.py │ ├── detailed/ │ ├── compiler.py │ ├── variables.py │ ├── constraints/ │ ├── objective.py │ ├── solver.py │ └── decoder.py │ ├── rolling_stock/ │ ├── wagon.py │ └── locomotive.py │ ├── network/ │ ├── candidates.py │ ├── pruning.py │ ├── orchestration.py │ └── feedback.py │ └── proof/ ├── evaluator.py └── search.py 19. Detailed Constraint Builders constraints/ │ ├── precedence.py ├── running_time.py ├── dwell.py ├── block.py ├── headway.py ├── opposing.py ├── switch.py ├── clearing.py ├── station_track.py ├── station_length.py ├── junction.py └── operational_window.py هر فایل مسئول یک خانواده Constraint است. 20. Solver Interface class DetailedScheduler: def solve( self, problem: DetailedSchedulingProblem, options: SolverOptions, ) -> SchedulingResult: ... Solver Interface باید CP-SAT-specific نباشد. در آینده امکان: CP-SAT MILP Custom Heuristic Simulation وجود داشته باشد. 21. CP-SAT Adapter optimization/detailed/cp_sat/ شامل: CpSatModelCompiler CpSatVariableBuilder CpSatConstraintBuilder CpSatSolver CpSatDecoder این جداسازی بسیار مهم است. 22. Solver Options @dataclass(frozen=True) class SolverOptions: time_limit_seconds: int num_workers: int random_seed: int enable_hints: bool log_search_progress: bool Configuration باید Hash شود. 23. Reproducibility Run: [ Run= ( DataVersion, InfrastructureVersion, CalibrationVersion, Scenario, ModelVersion, SolverConfiguration, InputSnapshot ) ] اگر این مجموعه ثابت باشد، Run باید تا حد امکان Reproducible باشد. 24. PostgreSQL Layer Repository Pattern: infrastructure/repositories/ مثلاً: StationRepository RouteRepository TrainRunRepository DemandRepository CapacityRunRepository OfferRepository AllocationRepository 25. Solver Database Rule Solver: ❌ SQL ❌ ORM Query ❌ HTTP Call ندارد. بلکه: Repository ↓ Canonical Snapshot ↓ Solver است. 26. Canonical Snapshot @dataclass(frozen=True) class PlanningSnapshot: infrastructure: InfrastructureSnapshot routes: RouteSnapshot trains: TrainSnapshot rolling_stock: RollingStockSnapshot demand: DemandSnapshot scenario: ScenarioSnapshot 27. Snapshot Hash snapshot_hash = sha256( canonical_json(snapshot) ).hexdigest() Hash در Run ذخیره می‌شود. 28. Migration Strategy Database با Migration کنترل شود. مثلاً: 001_initial_schema 002_source_model 003_infrastructure 004_train_domain 005_rolling_stock 006_demand 007_optimization 008_marketplace 009_traceability 010_audit 29. No Manual Production Schema Changes تغییر Production باید: Migration → Review → Test → Deploy شود. 30. Data Version داده Operational باید Versioned باشد. مثلاً: DV-2026-09-01 DV-2026-09-15 DV-2026-09-28 هر Run دقیقاً به یک DataVersion متصل است. 31. Source Adapters adapters/ ├── access/ ├── excel/ └── marketplace/ هر Adapter خروجی Raw/Staging می‌دهد. هیچ Adapter نباید مستقیماً CP-SAT Model بسازد. 32. Access Pipeline aaa.accdb ↓ Access Adapter ↓ Raw Records ↓ Mapping ↓ Quality ↓ Canonical 33. Excel Pipeline REPORT....xlsx ↓ Excel Adapter ↓ Raw ↓ Mapping ↓ Quality ↓ Canonical 34. Mapping Registry @dataclass(frozen=True) class FieldMapping: source_field: str canonical_field: str status: str confidence: str transformation: str | None مثلاً: TrainNo → TrainRun.source_train_no → VERIFIED 35. Production Rule No Verified Mapping ↓ No Production Use این Rule باید حتی در Code Enforcement شود. 36. Quality Pipeline Raw ↓ Schema Validation ↓ Identity Validation ↓ Time Validation ↓ Sequence Validation ↓ Mapping Validation ↓ Infrastructure Reconciliation ↓ Canonical 37. Data Quality Status PASSED PASSED_WITH_WARNINGS FAILED و: NOT_READY READY_WITH_WARNINGS READY REJECTED 38. API Architecture API ترجیحاً: /api/v1/ باشد. Modules: /market /demand /capacity /runs /allocations /schedules /validation /proof /bottlenecks /explanations 39. Run API POST /api/v1/runs GET /api/v1/runs/{run_id} POST /api/v1/runs/{run_id}/cancel GET /api/v1/runs/{run_id}/iterations GET /api/v1/runs/{run_id}/schedule GET /api/v1/runs/{run_id}/validation GET /api/v1/runs/{run_id}/proof GET /api/v1/runs/{run_id}/bottlenecks GET /api/v1/runs/{run_id}/explanations GET /api/v1/runs/{run_id}/capacity-offer 40. Marketplace API POST /api/v1/market/requests POST /api/v1/capacity/requests GET /api/v1/capacity/offers POST /api/v1/capacity/offers/{id}/reserve POST /api/v1/allocations POST /api/v1/allocations/{id}/revalidate 41. API Schema API DTOها نباید مستقیماً Domain Entity باشند. مثلاً: API Request DTO ↓ Mapper ↓ Domain Command 42. Command Pattern مثلاً: @dataclass(frozen=True) class CreateCapacityRequest: request_id: str od_pair_id: str requested_tons: float earliest_departure: int latest_arrival: int 43. Queue ساختار: API ↓ Run Manager ↓ Job Queue ↓ Optimization Worker Job: @dataclass(frozen=True) class OptimizationJob: run_id: str priority: int attempt: int 44. Worker class OptimizationWorker: def execute(self, job): context = run_repository.load_context(job.run_id) snapshot = snapshot_builder.build(context) orchestrator.execute(context, snapshot) 45. Retry Retry فقط برای Failureهای Infrastructure مانند: Database transient error Queue failure Worker crash مناسب است. برای: INFEASIBLE MODEL_INVALID DATA_INVALID Retry خودکار نباید انجام شود. 46. Transaction Boundary مثلاً Publish Offer: Validate Run + Create Offer + Write Trace + Audit در یک Transaction مناسب انجام شود. 47. Optimistic Concurrency برای Offer/Allocation: version استفاده شود. اگر: version = 5 باشد و Client با Version 4 Update کند: CONCURRENT_MODIFICATION برگردد. 48. Allocation Transaction در سطح Database: BEGIN lock/check capacity validate offer calculate remaining capacity create allocation update allocation state write audit COMMIT 49. No Overbooking Invariant: [ AllocatedCapacity \le PublishedCapacity ] و برای Shared Pool: [ \sum_a Allocation_{a,r} \le SharedCapacity_r ] 50. Validation Layer Validation مستقل از Solver: validation/ ├── schedule_validator.py ├── infrastructure_validator.py ├── formation_validator.py ├── wagon_validator.py ├── locomotive_validator.py ├── network_validator.py └── marketplace_validator.py 51. Independent Validator Validator نباید همان Constraintهای Solver را صرفاً Copy کند. باید Schedule خروجی را از دید Domain بررسی کند. مثلاً: assert movement.exit >= movement.entry + running_time و: assert not has_block_overlap(...) 52. Validator Principle Solver says: "Feasible" Validator asks: "Prove it from the resulting schedule." 53. Proof Engine proof/ ├── capacity_search.py ├── feasibility.py ├── validator.py └── evidence.py 54. Proof Contract @dataclass(frozen=True) class CapacityProofResult: tested_frequency: int next_frequency: int lower_bound_status: str upper_bound_status: str proof_status: str evidence_ids: tuple[str, ...] 55. Proof Rule برای: F = 30 باید: 30 feasible 31 infeasible باشد. اگر 31: UNKNOWN شد: PROOF_STATUS = NOT_PROVEN 56. Capacity Search Capacity Search خارج Detailed Scheduler قرار می‌گیرد: CapacitySearch ↓ DetailedScheduler(F) ↓ Validator نه اینکه Scheduler خودش Capacity Search کند. 57. Bottleneck Engine ورودی: Validated Schedule Capacity Proof Resource Usage Conflict Evidence خروجی: Bottleneck Explanation Marginal Scenario 58. Bottleneck Model @dataclass(frozen=True) class Bottleneck: resource_id: str level: str utilization: float slack: float marginal_capacity_delta: int | None evidence_ids: tuple[str, ...] 59. Explanation Engine Explanation نباید Text ثابت داشته باشد. باید از Evidence تولید شود. مثلاً: Resource B03 Track Type: SINGLE Direction Conflict: 17 Minimum Slack: 2 min سپس Template: ظرفیت در این بازه عمدتاً به دلیل ... تولید شود. 60. UI Boundary UI مستقیماً با Solver صحبت نمی‌کند. UI ↓ API ↓ Application Service ↓ Domain 61. UI Views حداقل: Dashboard Market Requests Capacity Search Capacity Offers Allocations Planning Runs Schedules Network Map Bottlenecks Scenario Analysis Data Quality Audit 62. Capacity Offer UI هر Offer: OD Route Departure Window Arrival Window Tons Trains Wagon Type Status Evidence Level Expiration و: Why limited? داشته باشد. 63. Schedule UI Schedule باید Time-Space باشد: Train Station Arrival Departure Block Entry Block Exit Track Direction و در آینده: Gantt / Time-Distance Diagram نمایش داده شود. 64. Network Visualization Station ●────────● B03 │ │ ● با قابلیت: Resource Capacity Utilization Bottleneck Train Flow 65. Test Pyramid E2E / \ Integration / \ Optimization / \ Domain Contract \ / Unit 66. Unit Tests حداقل: Time rollover Sequence Direction Station length Train formation Wagon balance Loco cycle Block conflict Headway Switch Clearing 67. Golden Tests Golden Dataset باید شامل: Access-shaped Excel-shaped Midnight Reverse Direction Single Track Double Track Station Constraint Junction Conflict Wagon Cycle Locomotive Cycle باشد. 68. Golden Capacity Test Dataset: GAR → SAKHEH Demand = 3000t 1000t/train و Golden Infrastructure فقط برای Test: Single Track Headway = 5 Switch = 8 Clearing = 2 Expected: 3 feasible 4 demand-infeasible در صورت موفقیت Validator: PROVEN = 3 69. Real Data Acceptance برای فایل واقعی: aaa.accdb باید: Access Adapter → Mapping → Quality → Canonical اجرا شود. اما Golden Parameters نباید بدون تأیید وارد Production شوند. 70. Real Data Rule اگر: TrackType Headway SwitchTime StationLength از Master Data نیامده باشد: Capacity Proof نباید ساخته شود. 71. Contract Tests Marketplace Contract: MarketRequest JSON CapacityOffer JSON Allocation JSON باید Contract Test داشته باشد. 72. API Versioning /api/v1 در آینده: /api/v2 بدون Breaking Change ناگهانی. 73. Event Contract Eventها Versioned باشند: CapacityOfferPublished.v1 AllocationConfirmed.v1 74. Configuration Configuration از Code جدا: DATABASE_URL QUEUE_URL SOLVER_TIME_LIMIT SOLVER_WORKERS MODEL_VERSION DEFAULT_HORIZON 75. Secrets هیچ Secretی در Repository نباشد. .env Secret Manager Environment Variables 76. Deployment حداقل Containerها: api worker scheduler-worker postgres queue و در Production: monitoring logging backup 77. CI Pipeline هر Commit: Lint ↓ Type Check ↓ Unit Tests ↓ Domain Tests ↓ Optimization Tests ↓ Integration Tests ↓ Golden Tests و برای Release: Performance Benchmark E2E Migration Test 78. CI Failure Rule اگر: Golden Capacity تغییر کند، Build باید Fail شود مگر اینکه: Expected Result به‌صورت رسمی Versioned و Approved شده باشد. 79. Performance CI برای Dataset کوچک: Golden Performance در CI. برای Dataset بزرگ: Nightly Benchmark اجرا شود. 80. Production Monitoring Dashboard: Active Runs Queued Runs Average Solve Time P95 Solve Time Timeout Rate Validation Failure Rate Proof Rate Offer Generation Rate Allocation Conflict Rate 81. Critical Alerts Alert برای: Solver Timeout Spike Database Failure Queue Backlog Validation Failure Spike Proof Failure Spike Unexpected Model Growth 82. Model Growth Monitoring اگر: TrainRuns ↑ 20% ولی: ConflictPairs ↑ 300% باشد، باید Alert یا Performance Investigation ایجاد شود. 83. Operational Audit هر Run: RunContext InputSnapshotHash SolverConfigHash ModelVersion Start End Status Evidence را ذخیره کند. 84. Data Lineage هر Canonical Field باید در صورت امکان: SourceFile SourceRecord SourceField MappingRule Transformation را بداند. 85. مثال Lineage TrainRun.source_train_no ↓ Access.aaa ↓ TrainNo ↓ Mapping Rule M-001 ↓ Canonical TrainRun 86. Operational Data vs Engineering Data سه لایه: Operational Evidence + Infrastructure Master + Planning Parameters باید جدا باشند. مثلاً: Access seir با: Infrastructure RunningTime Profile یکی نیست. 87. Calibration Calibration: Observed ↓ Compare ↓ Error ↓ Calibration Profile ↓ Future Run است. اما Calibration نباید Source Evidence را overwrite کند. 88. First Production Vertical Slice اولین Vertical Slice کامل: Excel / Access ↓ Canonical ↓ One OD ↓ One Route ↓ One Train Type ↓ Formation ↓ Wagon ↓ Locomotive ↓ Detailed CP-SAT ↓ Validator ↓ Capacity ↓ Proof ↓ Capacity Offer ↓ Allocation است. 89. سپس Multi-OD بعد: One Network + Multiple OD + Shared Blocks اضافه شود. 90. سپس Rolling Stock Network بعد: Multiple Wagon Pools + Empty Wagon Flow + Locomotive Cycles اضافه شود. 91. سپس Marketplace در این مرحله: Multiple Customers + Market Requests + Capacity Offers + Allocation فعال می‌شود. 92. سپس Scenario Engine بعد: Base Scenario A Scenario B Scenario C و مقایسه: Capacity Bottleneck Revenue Service Rolling Stock انجام می‌شود. 93. ترتیب واقعی پیاده‌سازی ترتیب پیشنهادی: Phase 1 Domain + Database Phase 2 Access/Excel Adapter Phase 3 Canonical Snapshot Phase 4 Detailed Scheduler Phase 5 Independent Validator Phase 6 Capacity Search / Proof Phase 7 Rolling Stock Phase 8 Aggregate Network Phase 9 Marketplace Contract Phase 10 Offer / Reservation / Allocation Phase 11 Performance / Scaling Phase 12 Production Hardening 94. Phase Gate هیچ Phase قبل از موفقیت Gate قبلی وارد Production نشود. مثلاً: Detailed Scheduler ↓ Validator ↓ 100% Golden Pass سپس: Capacity Proof فعال شود. 95. Commit Strategy Commitها باید کوچک و قابل Trace باشند. مثلاً: feat(domain): add physical block model feat(route): add directed path model feat(scheduler): add running time constraints feat(scheduler): add single track conflicts test(scheduler): add opposing direction case feat(validation): add independent block validator 96. Definition of Done برای هر Feature هر Feature باید: Code + Unit Test + Integration Test + Domain Rule + Validation + Documentation داشته باشد. برای Solver Feature: + Golden Case نیز لازم است. 97. Anti-Patterns این موارد ممنوع: ❌ SQL داخل Solver ❌ CP-SAT Variable داخل API ❌ Marketplace → CP-SAT ❌ Capacity = min(block capacities) ❌ Timeout = Infeasible ❌ Golden Data = Real Master Data ❌ Source Distance overwrite ❌ RequiredWait بدون Evidence به Hard Constraint ❌ Station Sequence alphabetical ❌ TrainRun = TrainFormation ❌ Offer = Allocation ❌ Feasible = Proven 98. Production Dependency Direction API ↓ Application ↓ Domain ↑ Infrastructure Adapters و: Optimization → Domain اما: Domain ─X→ Optimization نباید Dependency سخت داشته باشد. 99. مهم‌ترین Boundary سیستم باید چهار Engine مستقل داشته باشد: 1. Capacity Engine 2. Scheduling Engine 3. Validation Engine 4. Proof Engine و دو Layer تجاری: 5. Marketplace 6. Allocation 100. Final Production Architecture ┌──────────────────┐ │ MARKETPLACE │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ Demand Service │ └────────┬─────────┘ ↓ ┌──────────────────┐ │Capacity Request │ └────────┬─────────┘ ↓ ┌────────────────────────────┐ │ CAPACITY ENGINE │ │ │ │ Candidate Pruning │ │ Aggregate Optimization │ │ Detailed Scheduling │ │ Rolling Stock │ └────────────┬───────────────┘ ↓ ┌──────────────────┐ │ VALIDATOR │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ PROOF ENGINE │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ CAPACITY OFFER │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ ALLOCATION │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ TRAIN FORMATION │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ SCHEDULE │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ EXECUTION │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ ACTUAL / CALIB. │ └──────────────────┘ 101. Definition of Done — V2.5-Q V2.5-Q زمانی کامل محسوب می‌شود که: Architecture Modular Monolith برقرار باشد. Domain از Infrastructure مستقل باشد. Solver از Database مستقل باشد. API از Solver مستقل باشد. Data Access Adapter موجود باشد. Excel Adapter موجود باشد. Mapping Registry وجود داشته باشد. Data Versioning وجود داشته باشد. Lineage وجود داشته باشد. Optimization Aggregate Solver وجود داشته باشد. Detailed CP-SAT وجود داشته باشد. Rolling Stock وجود داشته باشد. Candidate Pruning وجود داشته باشد. Conflict Graph Sparse باشد. Warm Start قابل فعال‌سازی باشد. Validation Independent Validator وجود داشته باشد. UNKNOWN و INFEASIBLE جدا باشند. Golden Tests وجود داشته باشند. Proof F/F+1 وجود داشته باشد. Proof Evidence ذخیره شود. Capacity بدون Proof به‌عنوان Proven منتشر نشود. Marketplace MarketRequest وجود داشته باشد. CapacityRequest وجود داشته باشد. CapacityOffer وجود داشته باشد. Reservation وجود داشته باشد. Allocation وجود داشته باشد. Shared Capacity کنترل شود. Traceability کامل باشد. Production Async Worker وجود داشته باشد. Queue وجود داشته باشد. Checkpoint وجود داشته باشد. Cancellation وجود داشته باشد. Audit وجود داشته باشد. Observability وجود داشته باشد. Performance Benchmark وجود داشته باشد. CI/CD وجود داشته باشد. 102. اولین اجرای واقعی Production اولین Run واقعی باید دقیقاً این مسیر را طی کند: SOURCE ↓ RAW ↓ STAGING ↓ MAPPING ↓ QUALITY ↓ CANONICAL ↓ INFRASTRUCTURE RECONCILIATION ↓ TRAIN PROFILE ↓ FORMATION ↓ WAGON ↓ LOCOMOTIVE ↓ CANDIDATE ↓ AGGREGATE ↓ TRAIN RUN EXPANSION ↓ CP-SAT ↓ VALIDATOR ↓ CAPACITY ↓ PROOF ↓ BOTTLENECK ↓ EXPLANATION ↓ CAPACITY OFFER ↓ ALLOCATION هیچ مرحله‌ای نباید به‌صورت Silent Bypass حذف شود. 103. معیار موفقیت نهایی موفقیت سیستم دیگر این نیست که: «Solver جواب داد.» بلکه: [ \boxed{ Source\ Data \rightarrow Canonical\ Data \rightarrow Feasible\ Schedule \rightarrow Independent\ Validation \rightarrow Capacity\ Proof \rightarrow Market\ Offer \rightarrow Allocation } ] و برای هر نتیجه باید بتوان پاسخ داد: این عدد از کجا آمده؟ بر اساس کدام Data Version؟ با کدام Infrastructure Version؟ با کدام Scenario؟ با کدام Model Version؟ با کدام Solver Configuration؟ کدام قطارها آن را ساخته‌اند؟ کدام Resourceها محدودش کرده‌اند؟ آیا Schedule معتبر است؟ آیا Capacity اثبات شده است؟ کدام Market Request از آن استفاده کرده است؟ اگر این سؤالات قابل پاسخ باشند، سیستم از نظر معماری وارد سطح Production-Grade Railway Capacity & Marketplace Platform شده است. 104. V2.5-Q → V2.6 بعد از این Blueprint، مسیر دیگر نباید با Specificationهای کلی ادامه پیدا کند؛ باید وارد Implementation Track شود. ترتیب عملی بعدی: V2.5-Q Production Blueprint ↓ V2.6-A Repository Bootstrap ↓ V2.6-B PostgreSQL + Migrations ↓ V2.6-C Canonical Domain Package ↓ V2.6-D Access/Excel Ingestion ↓ V2.6-E Detailed CP-SAT Production Code ↓ V2.6-F Independent Validator ↓ V2.6-G Capacity Proof ↓ V2.6-H Rolling Stock Integration ↓ V2.6-I Aggregate Network Solver ↓ V2.6-J Marketplace API ↓ V2.6-K Capacity Offer / Allocation ↓ V2.6-L E2E Golden Production Run بنابراین از اینجا به بعد، کار اصلی دیگر طراحی مفهومی نیست؛ تبدیل همین Blueprint به Repository و Code واقعی است. مرحله بعدی طبیعی، V2.6-A — Repository Bootstrap & Production Project Skeleton است که در آن ساختار واقعی Packageها، pyproject.toml، Dependencyها، Configuration، Logging، Error Model، Base Domain Types، Repository Interfaces و Skeleton اجرای API/Worker را مشخص می‌کنیم؛ سپس بلافاصله وارد Migrationهای PostgreSQL و پیاده‌سازی واقعی می‌شویم. --- ## ادامه 85 — پرسش کاربر (`message-id:84e2739f-6b09-4dde-9a1a-07c956d9ace9`) هر دو مورد لطفا --- ## ادامه 85 — پاسخ دستیار (`message-id:11ec2716-cbb6-4081-b8f8-78b44d481512`) حتماً. هر دو مورد را با هم جلو می‌بریم: V2.6-A — Repository Bootstrap & Production Project Skeleton V2.6-B — PostgreSQL + Alembic Migration Foundation این دو مرحله را به‌صورت یک زنجیره اجرایی طراحی می‌کنم تا خروجی مستقیماً مبنای پیاده‌سازی واقعی پروژه باشد، نه صرفاً مستند معماری. --- ## ادامه 86 — پرسش کاربر (`message-id:1a55857d-b0ae-4b28-a5e7-aa9ef69ddd24`) بله لطفا --- ## ادامه 86 — پاسخ دستیار (`message-id:c09a976a-d849-4021-bf10-d0d9ee1a9441`) حتماً. در ادامه V2.6-A و V2.6-B را به‌صورت یک سند اجرایی یکپارچه می‌آورم؛ به‌گونه‌ای که تیم توسعه بتواند از روی آن Repository را ایجاد کند و PostgreSQL/Alembic را بدون شکستن مرزهای Domain و Solver راه‌اندازی کند. V2.6-A + V2.6-B — Production Repository Bootstrap & PostgreSQL Migration Foundation V2.6-A + V2.6-B Production Repository Bootstrap & PostgreSQL Migration Foundation Project: Iranian Railway Freight Capacity Planning, Scheduling & Network Optimization Platform Architecture Baseline: V2.5-Q Production Implementation Blueprint Implementation Stage: V2.6-A + V2.6-B Status: Implementation Specification Primary Objective: تبدیل معماری V2.5 به یک Repository واقعی، قابل تست، قابل استقرار و آماده اتصال به Canonical Domain و Solverهای Production. 1. هدف این مرحله در V2.6-A و V2.6-B دو لایه بنیادی ساخته می‌شوند: V2.6-A Repository ├── Domain ├── Application ├── Optimization Ports ├── Infrastructure Adapters ├── API ├── Worker ├── CLI └── Tests V2.6-B Persistence ├── PostgreSQL ├── SQLAlchemy ├── Alembic ├── Schema Domains ├── Versioning ├── Audit ├── Source Lineage └── Snapshot Persistence خروجی نهایی: Source ↓ Adapter ↓ Canonical Domain ↓ Repository ↓ Planning Snapshot ↓ Aggregate / Detailed Solver ↓ Validator ↓ Proof ↓ Result ↓ Capacity Offer ↓ Marketplace 2. Non-Goals این مرحله هنوز شامل موارد زیر نیست: پیاده‌سازی کامل CP-SAT پیاده‌سازی کامل Network Solver پیاده‌سازی کامل Empty Wagon Flow الگوریتم نهایی Capacity Proof Marketplace production integration UI نهایی Event Bus پیچیده Microservice decomposition در این مرحله هدف، ایجاد Production Foundation است. 3. Technology Baseline نسخه‌ها در این سند به‌عنوان Baseline/Target تعریف می‌شوند و قبل از Production Release باید با dependency lock نهایی شوند. Layer Technology Language Python 3.12 baseline API FastAPI Validation Pydantic v2 ORM / DB Access SQLAlchemy 2.x Migration Alembic Database PostgreSQL 16+ target Optimization OR-Tools CP-SAT Testing pytest Static Analysis mypy یا pyright Lint/Format Ruff Logging structlog یا استاندارد logging با JSON formatter CLI Typer HTTP Client httpx Local Runtime Docker Compose Queue Redis + worker abstraction Packaging pyproject.toml اصل مهم: Domain نباید به هیچ‌یک از PostgreSQL، SQLAlchemy، FastAPI یا OR-Tools وابسته باشد. 4. Repository Structure ساختار پیشنهادی: railway-capacity-platform/ │ ├── pyproject.toml ├── README.md ├── Makefile ├── .env.example ├── .gitignore ├── .pre-commit-config.yaml │ ├── docker/ │ ├── postgres/ │ └── worker/ │ ├── docker-compose.yml │ ├── alembic.ini │ ├── migrations/ │ ├── env.py │ ├── script.py.mako │ └── versions/ │ ├── src/ │ └── railway/ │ │ │ ├── __init__.py │ ├── main.py │ │ │ ├── domain/ │ │ ├── common/ │ │ ├── infrastructure/ │ │ ├── route/ │ │ ├── train/ │ │ ├── rolling_stock/ │ │ ├── demand/ │ │ ├── marketplace/ │ │ ├── capacity/ │ │ ├── scenario/ │ │ └── result/ │ │ │ ├── application/ │ │ ├── commands/ │ │ ├── queries/ │ │ ├── services/ │ │ ├── ports/ │ │ └── orchestration/ │ │ │ ├── optimization/ │ │ ├── aggregate/ │ │ ├── detailed/ │ │ ├── rolling_stock/ │ │ ├── network/ │ │ ├── feedback/ │ │ └── proof/ │ │ │ ├── infrastructure/ │ │ ├── persistence/ │ │ │ ├── models/ │ │ │ ├── repositories/ │ │ │ ├── mappings/ │ │ │ └── unit_of_work.py │ │ ├── source/ │ │ ├── queue/ │ │ ├── clock/ │ │ └── logging/ │ │ │ ├── adapters/ │ │ ├── access/ │ │ ├── excel/ │ │ └── marketplace/ │ │ │ ├── api/ │ │ ├── dependencies.py │ │ ├── routers/ │ │ ├── schemas/ │ │ └── error_handlers.py │ │ │ ├── cli/ │ │ └── commands/ │ │ │ └── config/ │ ├── settings.py │ └── logging.py │ ├── tests/ │ ├── unit/ │ ├── integration/ │ ├── contract/ │ ├── golden/ │ └── e2e/ │ ├── benchmarks/ │ └── scripts/ 5. Dependency Direction تنها dependency direction مجاز: API ↓ Application ↓ Domain Worker ↓ Application ↓ Domain CLI ↓ Application ↓ Domain Infrastructure ─────→ Domain Optimization ───────→ Domain Adapters ───────────→ Application / Domain Ports اما این موارد ممنوع هستند: Domain → SQLAlchemy ❌ Domain → FastAPI ❌ Domain → CP-SAT ❌ Solver → PostgreSQL ❌ Solver → HTTP ❌ API → SQLAlchemy Model ❌ Marketplace → CP-SAT ❌ 6. Domain Foundation 6.1 Typed IDs تمام موجودیت‌های مهم باید ID مستقل داشته باشند. نمونه: from dataclasses import dataclass @dataclass(frozen=True) class StationId: value: str @dataclass(frozen=True) class TrainRunId: value: str @dataclass(frozen=True) class RouteId: value: str @dataclass(frozen=True) class CapacityRunId: value: str در لایه Persistence این‌ها به UUID یا String نگاشت می‌شوند. 7. Time Model به دلیل وجود midnight rollover در داده واقعی، زمان Domain نباید صرفاً datetime.time باشد. مدل پایه: @dataclass(frozen=True) class PlanningMinute: value: int def __post_init__(self): if self.value < 0: raise ValueError("Planning minute cannot be negative") تمام زمان‌های Scheduler: arrival_minute departure_minute block_entry_minute block_exit_minute clear_minute به absolute planning minute تبدیل می‌شوند. مثال: 23:46 → 1426 00:36 → 1476 00:56 → 1496 02:19 → 1579 در نتیجه: 1476 > 1426 و rollover دیگر ambiguity ایجاد نمی‌کند. 8. Domain Error Taxonomy class DomainError(Exception): pass class ValidationError(DomainError): pass class MappingError(DomainError): pass class InvariantViolation(DomainError): pass class InfrastructureDataError(DomainError): pass class SchedulingError(DomainError): pass class SolverError(DomainError): pass class ProofError(DomainError): pass در Application: Domain Error ↓ Application Error ↓ API / CLI / Worker specific response نباید Exceptionهای CP-SAT یا SQLAlchemy مستقیماً به API نشت کنند. 9. Result Model تمام عملیات مهم باید Result مشخص داشته باشند. نمونه: @dataclass(frozen=True) class OperationResult: success: bool code: str message: str details: dict برای Solver: class SolverStatus(str, Enum): FEASIBLE = "FEASIBLE" INFEASIBLE = "INFEASIBLE" UNKNOWN = "UNKNOWN" MODEL_INVALID = "MODEL_INVALID" قاعده غیرقابل مذاکره: TIMEOUT ≠ INFEASIBLE UNKNOWN ≠ INFEASIBLE MODEL_INVALID ≠ INFEASIBLE 10. Source Reference هر داده واردشده از Access/Excel باید قابل trace باشد. @dataclass(frozen=True) class SourceReference: source_file_id: str source_record_id: str | None source_field: str | None مثال: TrainRun source_reference ↓ aaa.accdb ↓ TrainNo=100 ↓ StationName="..." 11. Version Foundation هر Run باید به نسخه داده متصل باشد. @dataclass(frozen=True) class DataVersionId: value: str @dataclass(frozen=True) class InfrastructureVersionId: value: str @dataclass(frozen=True) class CalibrationVersionId: value: str | None Run identity: Run = DataVersion + InfrastructureVersion + CalibrationVersion + Scenario + ModelVersion + SolverConfiguration + InputSnapshot 12. Application Ports Application نباید به implementation وابسته باشد. نمونه Repository: from typing import Protocol class StationRepository(Protocol): def get(self, station_id: str): ... def list_active(self): ... برای TrainRun: class TrainRunRepository(Protocol): def get(self, train_run_id: str): ... def list_by_data_version(self, data_version_id: str): ... 13. Snapshot Builder Solver نباید database را query کند. مسیر صحیح: PostgreSQL ↓ Repositories ↓ Snapshot Builder ↓ PlanningSnapshot ↓ Solver مدل: @dataclass(frozen=True) class PlanningSnapshot: data_version_id: str infrastructure_version_id: str stations: tuple tracks: tuple blocks: tuple routes: tuple paths: tuple train_runs: tuple train_profiles: tuple wagon_inventory: tuple locomotive_inventory: tuple demands: tuple scenario: object Snapshot باید immutable باشد. 14. Solver Port Application فقط Interface را می‌شناسد: class DetailedScheduler(Protocol): def solve( self, problem, options, ): ... CP-SAT implementation: DetailedScheduler ↑ CpSatDetailedScheduler نه: Application → ortools.cp_model 15. Validator Port class ScheduleValidator(Protocol): def validate(self, schedule) -> ValidationResult: ... Validator باید مستقل از Solver باشد. این استقلال برای جلوگیری از: Solver says FEASIBLE ↓ Validator accidentally repeats Solver logic ضروری است. 16. Proof Engine Port class CapacityProofEngine(Protocol): def prove(self, capacity_candidate): ... قاعده: F feasible F+1 infeasible Validation(F)=true ↓ PROVEN اگر: F+1 = UNKNOWN نتیجه: NOT_PROVEN 17. Composition Root تنها نقطه‌ای که implementationها به Interfaceها متصل می‌شوند: src/railway/main.py یا: application/bootstrap.py مثلاً: def build_application(): db = build_database() repositories = build_repositories(db) snapshot_builder = PlanningSnapshotBuilder( repositories=repositories, ) scheduler = CpSatDetailedScheduler() validator = IndependentScheduleValidator() proof_engine = CapacityProofEngine( scheduler=scheduler, validator=validator, ) return ApplicationContainer( repositories=repositories, snapshot_builder=snapshot_builder, scheduler=scheduler, validator=validator, proof_engine=proof_engine, ) 18. Configuration تمام Configuration از Environment می‌آید. نمونه: APP_ENV=development DATABASE_URL=postgresql+psycopg://... DATABASE_POOL_SIZE=10 DATABASE_MAX_OVERFLOW=20 LOG_LEVEL=INFO SOLVER_TIME_LIMIT_SECONDS=300 SOLVER_WORKERS=8 SOLVER_RANDOM_SEED=42 QUEUE_URL=redis://redis:6379/0 مدل: class Settings(BaseSettings): app_env: str = "development" database_url: str solver_time_limit_seconds: int = 300 solver_workers: int = 8 solver_random_seed: int = 42 queue_url: str Secret نباید داخل Git ذخیره شود. 19. API Foundation FastAPI application: /api/v1/health /api/v1/ready /api/v1/runs /api/v1/runs/{run_id} /api/v1/runs/{run_id}/iterations /api/v1/runs/{run_id}/schedule /api/v1/runs/{run_id}/validation /api/v1/runs/{run_id}/proof /api/v1/runs/{run_id}/bottlenecks /api/v1/runs/{run_id}/explanations /api/v1/runs/{run_id}/capacity-offer Router فقط: Parse Request Validate Input Call Use Case Serialize Response هیچ business logic در Router قرار نمی‌گیرد. 20. Health / Readiness Liveness GET /api/v1/health پاسخ: { "status": "ok" } Readiness GET /api/v1/ready موارد بررسی: PostgreSQL Queue Required configuration Migration state نمونه: { "status": "ready", "database": "ok", "queue": "ok", "migration": "ok" } 21. CLI CLI اصلی: railway دستورات: railway db upgrade railway db downgrade railway ingest access railway ingest excel railway validate input railway validate run railway run create railway run status railway proof run railway benchmark run نمونه: railway db upgrade یا: railway ingest excel --file ./data/report.xlsx 22. V2.6-B — PostgreSQL Foundation PostgreSQL مسئول: Persistence Versioning Transactions Concurrency Audit Lineage Result Storage و غیرمسئول در: Optimization Logic CP-SAT Modeling Capacity Calculation Scheduling Decisions 23. Database Schema Domains Schemaهای منطقی: source master railway train rolling_stock demand planning optimization execution quality calibration marketplace result audit استفاده از Schemaهای جداگانه باعث جلوگیری از تبدیل Database به یک جدول عظیم مشترک می‌شود. 24. Source Schema source.data_version id version_code description created_at created_by status content_hash source.source_file id data_version_id file_name file_type file_hash source_system uploaded_at metadata source.source_record id source_file_id source_row_number source_key raw_payload created_at 25. Master Schema master.station id source_number name normalized_name active created_at updated_at master.wagon_type id code name capacity_tons length_m commodity_class active master.locomotive_type id code name traction_capacity_tons max_train_length_m active 26. Railway Schema railway.station_track id station_id code usable_length_m bidirectional electrified active railway.physical_block id station_a_id station_b_id track_type running_time_forward running_time_reverse headway_same_direction switch_time clearing_time active railway.route id code name origin_station_id destination_station_id active railway.route_segment id route_id sequence from_station_id to_station_id physical_block_id distance_m railway.directed_path id route_id direction origin_station_id destination_station_id و جدول‌های وابسته: railway.directed_path_station railway.directed_path_block Invariant: number_of_stations = number_of_blocks + 1 27. Train Schema train.train_type id code name length_m weight_tons max_speed_kmh brake_requirement train.train_service id service_code service_name origin_station_id destination_station_id direction train.train_run id train_service_id source_train_no operating_date operating_pattern train_type_id route_id directed_path_id train.train_station_call id train_run_id sequence station_id arrival_minute departure_minute dwell_minutes required_wait_minutes chainage_m running_time_to_next source_arrival source_departure 28. Source Field Mapping جدول Mapping Registry: source_field canonical_field mapping_status confidence transformation notes نمونه: Source Canonical Status TrainNo TrainRun.source_train_no VERIFIED TrainName TrainService.service_name VERIFIED StationName Station.name VERIFIED StationNumber Station.source_number VERIFIED Sequence TrainStationCall.sequence VERIFIED time_in arrival_minute VERIFIED time_take dwell_minutes VERIFIED RequiredWait required_wait PROVISIONAL Kilometerage chainage HIGH MaxSpeed max_speed PROVISIONAL Distance source_distance UNTRUSTED sumDistancezz source_cumulative_distance UNKNOWN seir running_time_to_next VERIFIED 29. Rolling Stock Schema rolling_stock.wagon id wagon_type_id fleet_number status current_station_id rolling_stock.wagon_inventory id wagon_type_id station_id available_count snapshot_time rolling_stock.wagon_requirement id demand_id wagon_type_id required_count rolling_stock.wagon_cycle id wagon_id origin_station_id destination_station_id load_state available_from available_to rolling_stock.empty_wagon_movement id wagon_type_id from_station_id to_station_id departure_minute arrival_minute quantity 30. Locomotive Schema rolling_stock.locomotive id locomotive_type_id fleet_number status current_station_id rolling_stock.locomotive_assignment id train_run_id locomotive_id sequence rolling_stock.locomotive_cycle id locomotive_id from_station_id to_station_id available_from available_to 31. Demand Schema demand.od_pair id origin_station_id destination_station_id code demand.demand id od_pair_id commodity_code time_bucket_start time_bucket_end quantity_tons priority demand.freight_flow id demand_id route_id train_type_id wagon_type_id quantity_tons 32. Planning Schema planning.scenario id code name description base_scenario_id status planning.scenario_change id scenario_id object_type object_id parameter_name old_value new_value planning.objective_definition id scenario_id objective_type sense priority weight planning.policy_constraint id scenario_id constraint_code constraint_type expression hard_constraint 33. Optimization Schema optimization.capacity_run id scenario_id data_version_id infrastructure_version_id calibration_version_id model_version solver_configuration_hash input_snapshot_hash status started_at completed_at aggregate_status detailed_status validation_status proof_status این جدول هسته reproducibility است. 34. Optimization Iteration optimization.network_iteration فیلدهای اصلی: id capacity_run_id iteration_no stage candidate_count feasible_count infeasible_count unknown_count objective_value upper_bound lower_bound feedback_payload started_at completed_at 35. Result Schema result.schedule id capacity_run_id train_run_id status result.schedule_station_call id schedule_id station_id arrival_minute departure_minute track_id result.schedule_block_movement id schedule_id block_id entry_minute exit_minute clear_minute 36. Validation Result result.validation_run و: result.validation_issue هر issue: id validation_run_id severity code entity_type entity_id message evidence Severity: INFO WARNING ERROR CRITICAL 37. Proof Schema result.capacity_proof id capacity_run_id candidate_capacity next_capacity candidate_status next_status candidate_valid proof_status proof_method Proof status: PROVEN NOT_PROVEN FAILED 38. Bottleneck Schema result.bottleneck فیلدهای کلیدی: resource_type resource_id utilization binding marginal_impact capacity_before capacity_after explanation اصل مهم: High Utilization ≠ Bottleneck automatically Bottleneck باید بر اساس اثر محدودکنندگی نیز تحلیل شود. 39. Marketplace Schema marketplace.market_request id external_request_id customer_reference origin_station_id destination_station_id commodity_code quantity_tons requested_time_start requested_time_end status marketplace.capacity_offer id capacity_run_id od_pair_id route_id train_type_id available_train_count available_tonnage valid_from valid_to status marketplace.allocation id capacity_offer_id market_request_id allocated_train_count allocated_tonnage status قاعده: CapacityOffer ≠ Allocation Offer ظرفیت قابل عرضه است؛ Allocation تخصیص انجام‌شده به Market Request است. 40. Audit Schema تمام تغییرات حساس باید audit شوند. audit.audit_log فیلدها: id timestamp actor action entity_type entity_id before_payload after_payload correlation_id run_id 41. Calibration Schema calibration.calibration_version calibration.calibration_profile calibration.observation مثلاً: running_time_adjustment dwell_adjustment headway_adjustment station_processing_time اما Calibration نباید Source Data را overwrite کند. 42. Reconciliation برای داده‌های متعارض: quality.reconciliation_rule quality.reconciliation_record quality.reconciliation_value مثال: source arrival source departure source dwell expected departure تعارض باید ثبت شود: WARNING CONFLICT RESOLVED نه اینکه silently اصلاح شود. 43. Database Constraints بخشی از invariants باید در Database enforce شوند. مثلاً: NOT NULL UNIQUE FOREIGN KEY CHECK مثال: CHECK (usable_length_m > 0) CHECK (running_time_forward >= 0) CHECK (running_time_reverse >= 0) CHECK (headway_same_direction >= 0) CHECK (switch_time >= 0) CHECK (clearing_time >= 0) اما تمام business rules نباید به SQL منتقل شوند. 44. Index Strategy Indexهای اولیه: source.source_record(source_file_id) master.station(source_number) master.station(normalized_name) railway.route(origin_station_id, destination_station_id) railway.route_segment(route_id, sequence) train.train_run(train_service_id) train.train_run(operating_date) train.train_run(route_id) train.train_station_call(train_run_id, sequence) train.train_station_call(station_id) optimization.capacity_run(scenario_id) optimization.capacity_run(status) result.schedule(capacity_run_id) result.schedule_block_movement(schedule_id, block_id) برای Queryهای سنگین پس از Benchmark indexهای تکمیلی اضافه می‌شوند. 45. SQLAlchemy Boundary SQLAlchemy Models فقط در: infrastructure/persistence/models قرار می‌گیرند. Domain Model و ORM Model یکی نیستند. مسیر: SQLAlchemy Model ↓ Mapper ↓ Domain Entity و برعکس: Domain Entity ↓ Mapper ↓ SQLAlchemy Model 46. Unit of Work Transaction management: class UnitOfWork(Protocol): def __enter__(self): ... def commit(self): ... def rollback(self): ... def __exit__(self, *args): ... Use Case: Begin Transaction ↓ Read / Validate ↓ Write State ↓ Commit Solver نباید transaction را مدیریت کند. 47. Concurrency Runها باید با optimistic concurrency کنترل شوند. برای مثال: version updated_at و در Allocation: available_capacity >= requested_capacity باید در transaction بررسی شود. هدف: No Overbooking 48. Migration Strategy Alembic تنها مرجع تغییر Schema است. Model Change ↓ Migration ↓ Review ↓ Test ↓ Deploy ممنوع: Manual production ALTER TABLE 49. Migration Order Migrationها باید domain-aware باشند: 001_extensions 002_source 003_master 004_railway 005_train 006_rolling_stock 007_demand 008_planning 009_optimization 010_result 011_quality 012_calibration 013_marketplace 014_audit Foreign Key dependency باید رعایت شود. 50. Initial Migration Migration اول باید فقط Foundation را ایجاد کند: schemas extensions if required core version tables audit foundation سپس migrationهای domain به‌صورت incremental ایجاد شوند. هدف: Small Reviewable Reversible Traceable بودن migrationها است. 51. Docker Compose محیط Local: postgres redis api worker نمونه ساختار: services: postgres: image: postgres environment: POSTGRES_DB: railway POSTGRES_USER: railway POSTGRES_PASSWORD: railway ports: - "5432:5432" redis: image: redis ports: - "6379:6379" api: build: . command: railway-api depends_on: - postgres - redis worker: build: . command: railway-worker depends_on: - postgres - redis در Production passwordها باید از Secret Management بیایند. 52. pyproject.toml Baseline وابستگی‌های اصلی: fastapi uvicorn pydantic pydantic-settings sqlalchemy psycopg alembic ortools typer httpx pytest pytest-asyncio ruff mypy برای Queue نیز abstraction ایجاد می‌شود تا implementation فعلی قابل تعویض باشد. 53. Testing Structure tests/ ├── unit/ │ ├── domain/ │ ├── application/ │ └── optimization/ │ ├── integration/ │ ├── postgres/ │ ├── repositories/ │ └── migrations/ │ ├── contract/ │ ├── source/ │ └── marketplace/ │ ├── golden/ │ └── v2_5_n/ │ └── e2e/ 54. Unit Tests حداقل تست‌های اولیه: PlanningMinute Train identity Direction resolution Route ordering Path invariant Source mapping Midnight rollover Domain validation Solver status mapping مثال: 23:46 → 00:36 باید به‌درستی 50 دقیقه اختلاف را تولید کند. 55. Integration Tests Database tests: create schema run migration insert canonical entities load repositories build snapshot rollback transaction تست Migration: empty DB ↓ alembic upgrade head ↓ schema valid و: head ↓ alembic downgrade ↓ expected previous state 56. Golden Dataset Golden dataset قبلی باید وارد Repository شود. ساختار: tests/golden/ access_sample/ excel_sample/ infrastructure_sample/ formation_sample/ demand_sample/ هدف: Raw ↓ Mapping ↓ Canonical ↓ Snapshot ↓ Scheduler ↓ Validator ↓ Proof اما در این مرحله صرفاً Foundation و Persistence آماده می‌شود؛ اجرای واقعی Golden E2E در V2.6-L انجام می‌شود. 57. Smoke Test Smoke Test باید بتواند بدون PostgreSQL نیز اجرا شود. Fake Repository ↓ Planning Snapshot ↓ Minimal Problem ↓ Fake Scheduler ↓ Validator ↓ Result هدف: اثبات سلامت Dependency Graph، نه اثبات ظرفیت واقعی راه‌آهن. 58. Logging هر Request و Run باید correlation داشته باشد: correlation_id request_id run_id scenario_id نمونه log: { "level": "INFO", "event": "capacity_run_started", "run_id": "RUN-001", "scenario_id": "SC-001", "model_version": "2.6-A", "correlation_id": "..." } نباید در Log: password database credentials secret full sensitive payload ثبت شود. 59. Run Observability Metricهای پایه: run.created run.validation.duration aggregate.solve.duration detailed.solve.duration validation.duration proof.duration solver.variables solver.booleans solver.intervals solver.constraints solver.conflict_pairs run.success run.failure run.timeout run.unknown این Metrics در V2.6-O برای Performance/Scalability استفاده خواهند شد. 60. API Error Mapping مثلاً: Domain Validation Error → 422 Not Found → 404 Conflict → 409 Infrastructure Failure → 503 Solver Failure → 500/appropriate execution status Timeout → 202 + RUNNING/TIMEOUT state اما Solver INFEASIBLE نباید HTTP Error تلقی شود؛ یک valid optimization result است. 61. Run Lifecycle Repository باید State Machine را پشتیبانی کند: CREATED ↓ VALIDATING_INPUT ↓ READY ↓ SOLVING_AGGREGATE ↓ BUILDING_TRAIN_RUNS ↓ CHECKING_FORMATION ↓ CHECKING_WAGON ↓ CHECKING_LOCOMOTIVE ↓ SOLVING_DETAILED ↓ VALIDATING_RESULT ↓ PROVING_CAPACITY ↓ ANALYZING_BOTTLENECKS ↓ GENERATING_EXPLANATION ↓ PUBLISHING_CAPACITY ↓ COMPLETED Failure: FAILED_VALIDATION FAILED_SOLVER INVALID_RESULT TIMEOUT CANCELLED PROOF_NOT_ESTABLISHED 62. Idempotency Run creation باید deterministic key داشته باشد: hash( DataVersion InfrastructureVersion CalibrationVersion Scenario ModelVersion SolverConfiguration InputSnapshot ) در صورت اجرای دوباره همان configuration: same logical run قابل شناسایی باشد. 63. Security Foundation در V2.6: Environment secrets DB credentials API authentication boundary Audit actor Input validation File validation ایجاد می‌شود. اما RBAC کامل Marketplace در مرحله بعد تکمیل خواهد شد. 64. Coding Standards قواعد اجباری: Type hints Immutable Domain objects where possible Explicit interfaces No hidden global state No business logic in routers No SQL in Solver No ORM models in Domain No silent data transformation No broad except Exception Naming: TrainRun TrainFormation OperationalBatch PhysicalBlock DirectedPath CapacityRun CapacityOffer Allocation نباید نام‌های مبهمی مانند: Train RouteData CapacityData ResultData برای Entityهای اصلی استفاده شوند. 65. Git Strategy Branchها: main develop feature/* fix/* Commitها باید کوچک و domain-oriented باشند. مثلاً: feat(domain): add TrainRun identity feat(persistence): add railway schema feat(migration): add source tables test(mapping): add midnight rollover case 66. CI Pipeline حداقل Pipeline: Checkout ↓ Install ↓ Ruff ↓ Type Check ↓ Unit Tests ↓ Integration Tests ↓ Migration Test ↓ Build برای Pull Request: No lint error No type error Tests pass Migration valid 67. Makefile دستورات: make install make lint make format make typecheck make test make test-unit make test-integration make db-up make db-down make db-upgrade make db-downgrade make api make worker make benchmark هدف این است که Developer برای اجرای عملیات معمول مجبور به دانستن جزئیات Docker/Alembic نشود. 68. Definition of Done — V2.6-A V2.6-A زمانی Done است که: Repository structure ایجاد شده باشد. Domain مستقل از Infrastructure باشد. Application Ports تعریف شده باشند. Configuration مرکزی باشد. API bootstrap شود. Worker bootstrap شود. CLI bootstrap شود. Logging و correlation ID آماده باشند. Health/Readiness وجود داشته باشد. Test skeleton فعال باشد. CI اولیه وجود داشته باشد. Fake Repository/Service برای Smoke Test وجود داشته باشد. Solver فقط از Interface استفاده کند. 69. Definition of Done — V2.6-B V2.6-B زمانی Done است که: PostgreSQL local environment بالا بیاید. Alembic فعال باشد. Schema domains ایجاد شده باشند. Source/Data Versioning آماده باشد. Master/Railway/Train schema پایه وجود داشته باشد. Rolling Stock schema پایه وجود داشته باشد. Demand/Planning schema پایه وجود داشته باشد. Optimization Run schema وجود داشته باشد. Result/Proof schema پایه وجود داشته باشد. Audit foundation وجود داشته باشد. Foreign Keys و CHECK constraints پایه فعال باشند. Indexهای اولیه ایجاد شده باشند. Repository Integration Tests موفق باشند. Migration upgrade/downgrade تست شده باشد. هیچ SQL مستقیماً در Domain/Solver وجود نداشته باشد. 70. Acceptance Flow پس از V2.6-A/B باید بتوانیم این Flow را اجرا کنیم: Developer starts repository ↓ Docker Compose ↓ PostgreSQL + Redis ↓ Alembic Upgrade ↓ API starts ↓ /health = OK ↓ /ready = READY ↓ Insert DataVersion ↓ Insert Station ↓ Insert PhysicalBlock ↓ Insert Route ↓ Insert TrainRun ↓ Repository reads entities ↓ Snapshot Builder ↓ PlanningSnapshot این Flow هنوز ظرفیت واقعی محاسبه نمی‌کند. اما Foundation لازم برای آن را اثبات می‌کند. 71. اولین Vertical Slice پس از این دو مرحله پس از V2.6-A/B مسیر پیاده‌سازی باید چنین باشد: PostgreSQL ↓ Access/Excel Adapter ↓ Staging ↓ Mapping ↓ Quality ↓ Canonical ↓ DirectedPath ↓ PlanningSnapshot ↓ Formation ↓ Wagon ↓ Locomotive ↓ Detailed CP-SAT ↓ Independent Validator ↓ Capacity Proof بنابراین V2.6-A/B عملاً زیرساخت اجرایی کل V2.6 را تثبیت می‌کنند. 72. مرحله بعدی پس از تثبیت این Foundation، مرحله منطقی بعدی: V2.6-C — Canonical Domain Package در آن مرحله Entityهای واقعی زیر به‌صورت Type-Safe و قابل تست پیاده می‌شوند: Station StationTrack PhysicalBlock Junction Route RouteSegment DirectedPath TrainType TrainService OperatingCalendar TrainRun TrainStationCall TrainBlockMovement WagonType Wagon WagonPool WagonInventory WagonRequirement WagonCycle LocomotiveType Locomotive LocomotiveAssignment LocomotiveCycle ODPair Demand FreightFlow TrainFormation TrainFormationItem Scenario PolicyConstraint ObjectiveDefinition CapacityRun CapacityProfile CapacityOffer Allocation و مهم‌تر از همه، Invariantهای اصلی در همان Domain Layer enforce خواهند شد: DirectedPath: stations = blocks + 1 TrainStationCall: sequence is strictly increasing TrainStationCall: departure >= arrival + dwell BlockMovement: exit >= entry + running_time Station: train_length <= usable_length Formation: weight/length/traction constraints valid Wagon: loaded/empty cycle consistent Capacity: FEASIBLE ≠ PROVEN Proof: F feasible + F+1 infeasible + validation → PROVEN 73. معماری نهایی این دو مرحله ┌──────────────────────┐ │ FastAPI API │ └──────────┬───────────┘ │ ┌──────────▼───────────┐ │ Application │ │ Use Cases / Ports │ └──────────┬───────────┘ │ ┌──────────────▼──────────────┐ │ Domain │ │ Railway / Train / Demand │ │ Rolling Stock / Capacity │ └──────────────┬──────────────┘ │ ┌───────────────┴────────────────┐ │ │ ┌────────▼────────┐ ┌────────▼────────┐ │ Infrastructure │ │ Optimization │ │ PostgreSQL │ │ Aggregate │ │ Repositories │ │ Detailed CP-SAT │ │ Adapters │ │ Network / Proof │ └────────┬────────┘ └────────┬────────┘ │ │ └───────────────┬───────────────┘ │ ┌─────────▼─────────┐ │ Planning Snapshot │ └───────────────────┘ اصل معماری: Database داده را نگه می‌دارد، Domain قواعد را تعریف می‌کند، Application فرآیند را orchestration می‌کند، Solver مسئله را حل می‌کند، Validator نتیجه را مستقل بررسی می‌کند و Proof Engine تنها در صورت وجود شواهد کافی ظرفیت را Proven اعلام می‌کند. 74. Final Acceptance Principle بعد از V2.6-A/B اگر سیستم بتواند: Source Record ↓ Canonical Entity ↓ PostgreSQL ↓ Repository ↓ Planning Snapshot را بدون از دست دادن: Identity Sequence Direction Time Source Evidence Version Lineage انجام دهد، Foundation پروژه برای ورود به V2.6-C آماده است. و از این نقطه به بعد، توسعه باید incremental و executable باشد؛ هر مرحله باید کد، migration، test و acceptance criteria خودش را داشته باشد، نه اینکه دوباره به یک طراحی مفهومی بزرگ برگردیم. گام بعدی منطقی دقیقاً V2.6-C است: پیاده‌سازی Canonical Domain Package با Entityها، Value Objectها، Enumها، Invariantها و Interfaceهای Repository به‌صورت واقعی و قابل تست. --- ## ادامه 87 — پرسش کاربر (`message-id:013599a1-a3ac-4506-a024-e2f1b258d2c3`) بله لطفا --- ## ادامه 87 — پاسخ دستیار (`message-id:fbc5c027-96e2-462c-8bd7-0c3955cc3b56`) حتماً. پس وارد V2.6-C — Canonical Domain Package می‌شویم؛ این مرحله نقطه‌ای است که مدل مفهومی قبلی به Domain Code واقعی تبدیل می‌شود و هسته پروژه از وابستگی به Access/Excel، PostgreSQL و CP-SAT مستقل می‌ماند. V2.6-C — Canonical Domain Package & Railway Domain Kernel V2.6-C Canonical Domain Package & Railway Domain Kernel Project: Iranian Railway Freight Capacity Planning, Scheduling & Network Optimization Platform Baseline: V2.6-A + V2.6-B Stage: V2.6-C Status: Production Implementation Specification 1. هدف هدف V2.6-C تبدیل Canonical Domain Model به یک Domain Kernel واقعی، Type-Safe، Immutable و قابل تست است. معماری: Source Data ↓ Adapter / Mapping ↓ Canonical Domain ↓ Planning Snapshot ↓ Aggregate Solver ↓ Detailed Scheduler در این مرحله Domain نباید بداند: PostgreSQL چیست Excel چیست Access چیست FastAPI چیست CP-SAT چیست Marketplace API چیست Domain فقط Railway Reality و Business Invariants را می‌شناسد. 2. اصل بنیادی مدل Canonical: Railway Infrastructure + Train Operations + Rolling Stock + Freight Demand + Planning / Policy + Capacity / Result اما این موجودیت‌ها نباید یکدیگر را با Referenceهای مبهم به هم متصل کنند. به‌جای: train.route = "something" از Typed Identity استفاده می‌شود: train.route_id: RouteId 3. Domain Package Structure src/railway/domain/ │ ├── common/ │ ├── ids.py │ ├── enums.py │ ├── time.py │ ├── quantity.py │ ├── result.py │ ├── errors.py │ └── source.py │ ├── infrastructure/ │ ├── station.py │ ├── station_track.py │ ├── physical_block.py │ ├── junction.py │ └── operational_window.py │ ├── route/ │ ├── route.py │ ├── route_segment.py │ └── directed_path.py │ ├── train/ │ ├── train_type.py │ ├── train_service.py │ ├── operating_calendar.py │ ├── train_run.py │ ├── train_station_call.py │ └── train_block_movement.py │ ├── rolling_stock/ │ ├── wagon_type.py │ ├── wagon.py │ ├── wagon_pool.py │ ├── wagon_inventory.py │ ├── wagon_requirement.py │ ├── wagon_cycle.py │ ├── locomotive_type.py │ ├── locomotive.py │ ├── locomotive_assignment.py │ └── locomotive_cycle.py │ ├── demand/ │ ├── od_pair.py │ ├── demand.py │ └── freight_flow.py │ ├── planning/ │ ├── scenario.py │ ├── objective.py │ └── policy_constraint.py │ ├── formation/ │ ├── train_formation.py │ └── train_formation_item.py │ ├── capacity/ │ ├── capacity_profile.py │ ├── capacity_offer.py │ └── allocation.py │ └── result/ ├── validation.py ├── bottleneck.py ├── proof.py └── explanation.py 4. Common Value Objects 4.1 Typed IDs تمام IDها باید Strongly Typed باشند. @dataclass(frozen=True) class StationId: value: str @dataclass(frozen=True) class StationTrackId: value: str @dataclass(frozen=True) class PhysicalBlockId: value: str @dataclass(frozen=True) class RouteId: value: str @dataclass(frozen=True) class DirectedPathId: value: str @dataclass(frozen=True) class TrainRunId: value: str @dataclass(frozen=True) class TrainServiceId: value: str @dataclass(frozen=True) class TrainTypeId: value: str @dataclass(frozen=True) class WagonTypeId: value: str @dataclass(frozen=True) class WagonId: value: str @dataclass(frozen=True) class LocomotiveId: value: str @dataclass(frozen=True) class DemandId: value: str @dataclass(frozen=True) class ScenarioId: value: str @dataclass(frozen=True) class CapacityRunId: value: str در Domain استفاده از raw str برای Entity IDهای اصلی ممنوع است. 5. Time Model @dataclass(frozen=True, order=True) class PlanningMinute: value: int def __post_init__(self): if self.value < 0: raise ValueError("Planning minute must be >= 0") Duration: @dataclass(frozen=True) class DurationMinutes: value: int def __post_init__(self): if self.value < 0: raise ValueError("Duration must be >= 0") عملیات: departure >= arrival + dwell به‌صورت Domain invariant قابل بیان خواهد بود. 6. Quantity Types برای جلوگیری از اشتباه Unit: @dataclass(frozen=True) class Tons: value: float def __post_init__(self): if self.value < 0: raise ValueError("Tonnage cannot be negative") @dataclass(frozen=True) class LengthMeters: value: float def __post_init__(self): if self.value < 0: raise ValueError("Length cannot be negative") @dataclass(frozen=True) class SpeedKmh: value: float def __post_init__(self): if self.value < 0: raise ValueError("Speed cannot be negative") در Domain نباید: train.length = 650 مشخص باشد که 650 چه واحدی دارد. باید: train.length = LengthMeters(650) باشد. 7. Enums class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" class LoadState(str, Enum): LOADED = "LOADED" EMPTY = "EMPTY" class SolverStatus(str, Enum): FEASIBLE = "FEASIBLE" INFEASIBLE = "INFEASIBLE" UNKNOWN = "UNKNOWN" MODEL_INVALID = "MODEL_INVALID" class ProofStatus(str, Enum): PROVEN = "PROVEN" NOT_PROVEN = "NOT_PROVEN" FAILED = "FAILED" 8. Infrastructure Domain 8.1 Station @dataclass(frozen=True) class Station: id: StationId name: str source_number: str | None usable_length_m: LengthMeters active: bool = True Invariant: usable_length_m > 0 9. StationTrack @dataclass(frozen=True) class StationTrack: id: StationTrackId station_id: StationId usable_length_m: LengthMeters bidirectional: bool = True electrified: bool = False active: bool = True Invariant: usable_length_m > 0 Train compatibility: def can_accept(self, train_length: LengthMeters) -> bool: return train_length.value <= self.usable_length_m.value 10. PhysicalBlock @dataclass(frozen=True) class PhysicalBlock: id: PhysicalBlockId station_a_id: StationId station_b_id: StationId track_type: TrackType running_time_forward: DurationMinutes running_time_reverse: DurationMinutes headway_same_direction: DurationMinutes switch_time: DurationMinutes clearing_time: DurationMinutes Invariant: station_a != station_b و: running_time > 0 headway >= 0 switch >= 0 clearing >= 0 11. Directional Running Time def running_time( block: PhysicalBlock, direction: Direction, ) -> DurationMinutes: if direction == Direction.FORWARD: return block.running_time_forward return block.running_time_reverse این تابع Domain-level است. اما اینکه running time از: seir Infrastructure Master Calibration Train Type Load State چگونه ساخته شود، در Calibration / Application / Solver configuration تعیین خواهد شد. 12. Junction @dataclass(frozen=True) class JunctionMovement: id: str junction_id: str from_resource_id: str to_resource_id: str direction: Direction Conflict: @dataclass(frozen=True) class JunctionConflict: movement_a_id: str movement_b_id: str separation: DurationMinutes اصل: Junction Conflict باید explicit باشد و نباید همه movementها به‌صورت blanket NoOverlap مدل شوند. 13. Operational Window @dataclass(frozen=True) class OperationalWindow: id: str resource_id: str start: PlanningMinute end: PlanningMinute allowed: bool reason: str | None = None Invariant: start < end کاربرد: Maintenance Fueling Prayer Crew availability Operational restriction Temporary closure همه به یک مفهوم عمومی تبدیل می‌شوند: Operational Availability Window 14. Route @dataclass(frozen=True) class Route: id: RouteId code: str name: str origin_station_id: StationId destination_station_id: StationId active: bool = True Route فقط مسیر منطقی است. این Entity نباید زمان‌بندی دقیق Train را نگه دارد. 15. RouteSegment @dataclass(frozen=True) class RouteSegment: id: str route_id: RouteId sequence: int from_station_id: StationId to_station_id: StationId physical_block_id: PhysicalBlockId distance_m: LengthMeters Invariant: sequence >= 1 from_station != to_station 16. DirectedPath DirectedPath یکی از مهم‌ترین Entityهای مدل است. @dataclass(frozen=True) class DirectedPath: id: DirectedPathId route_id: RouteId direction: Direction station_ids: tuple[StationId, ...] block_ids: tuple[PhysicalBlockId, ...] Invariant: len(station_ids) == len(block_ids) + 1 و: station_ids[0] == origin station_ids[-1] == destination برای Reverse: A → B → C → D می‌تواند: D → C → B → A باشد. ترتیب Sequence باید حفظ شود. هرگز: sorted(stations) مجاز نیست. 17. TrainType @dataclass(frozen=True) class TrainType: id: TrainTypeId code: str name: str length_m: LengthMeters weight_tons: Tons max_speed_kmh: SpeedKmh brake_requirement: int TrainType ویژگی نوع قطار است. 18. TrainService @dataclass(frozen=True) class TrainService: id: TrainServiceId service_code: str service_name: str origin_station_id: StationId destination_station_id: StationId direction: Direction TrainService با TrainRun متفاوت است. TrainService = Service Definition TrainRun = One actual/planned occurrence 19. OperatingCalendar @dataclass(frozen=True) class OperatingCalendar: service_id: TrainServiceId operating_days: frozenset[int] مثلاً: Saturday Sunday Tuesday Thursday الگوی تقویمی بخشی از Service است، نه Infrastructure. 20. TrainRun @dataclass(frozen=True) class TrainRun: id: TrainRunId service_id: TrainServiceId source_train_no: str operating_date: date train_type_id: TrainTypeId route_id: RouteId directed_path_id: DirectedPathId TrainRun: TrainService ↓ specific TrainRun مثلاً: Service 100 ↓ 2026-09-28 Run 21. TrainStationCall @dataclass(frozen=True) class TrainStationCall: train_run_id: TrainRunId sequence: int station_id: StationId arrival: PlanningMinute departure: PlanningMinute dwell: DurationMinutes required_wait: DurationMinutes | None chainage_m: float | None running_time_to_next: DurationMinutes | None Invariant: sequence >= 1 departure >= arrival departure >= arrival + dwell اگر required_wait فقط PROVISIONAL باشد، Domain آن را الزام عملیاتی نمی‌کند مگر اینکه در Scenario/Policy تأیید شده باشد. 22. TrainBlockMovement @dataclass(frozen=True) class TrainBlockMovement: train_run_id: TrainRunId block_id: PhysicalBlockId entry: PlanningMinute exit: PlanningMinute clear: PlanningMinute Invariant: exit >= entry clear >= exit و: exit - entry >= running_time در صورتی که running time مربوط به همان Profile باشد. 23. TrainFormation این Entity عمداً از TrainRun جدا است. @dataclass(frozen=True) class TrainFormation: id: str train_run_id: TrainRunId items: tuple["TrainFormationItem", ...] TrainRun می‌گوید: چه قطاری؟ کجا؟ چه زمانی؟ Formation می‌گوید: با چه Wagons/Locomotiveهایی؟ 24. TrainFormationItem @dataclass(frozen=True) class TrainFormationItem: sequence: int wagon_id: WagonId wagon_type_id: WagonTypeId load_state: LoadState cargo_weight_tons: Tons Formation validation: total length total weight commodity compatibility traction brake station length wagon availability 25. WagonType @dataclass(frozen=True) class WagonType: id: WagonTypeId code: str name: str capacity_tons: Tons length_m: LengthMeters compatible_commodities: frozenset[str] 26. Wagon @dataclass(frozen=True) class Wagon: id: WagonId wagon_type_id: WagonTypeId fleet_number: str current_station_id: StationId Wagon وضعیت operational نیز خواهد داشت: AVAILABLE LOADED EMPTY MAINTENANCE UNAVAILABLE 27. WagonInventory @dataclass(frozen=True) class WagonInventory: wagon_type_id: WagonTypeId station_id: StationId available_count: int snapshot_time: PlanningMinute Invariant: available_count >= 0 28. WagonRequirement @dataclass(frozen=True) class WagonRequirement: demand_id: DemandId wagon_type_id: WagonTypeId required_count: int 29. WagonCycle Wagon cycle: Load O ↓ Loaded O→D ↓ Unload D ↓ Empty D→O ↓ Available O مدل: @dataclass(frozen=True) class WagonCycle: wagon_id: WagonId origin_station_id: StationId destination_station_id: StationId loaded_departure: PlanningMinute unloaded_arrival: PlanningMinute empty_arrival: PlanningMinute Invariant: loaded_departure < unloaded_arrival < empty_arrival 30. LocomotiveType @dataclass(frozen=True) class LocomotiveType: id: str code: str name: str traction_capacity_tons: Tons max_train_length_m: LengthMeters 31. Locomotive @dataclass(frozen=True) class Locomotive: id: LocomotiveId locomotive_type_id: str fleet_number: str current_station_id: StationId 32. LocomotiveAssignment @dataclass(frozen=True) class LocomotiveAssignment: train_run_id: TrainRunId locomotive_id: LocomotiveId sequence: int 33. LocomotiveCycle @dataclass(frozen=True) class LocomotiveCycle: locomotive_id: LocomotiveId from_station_id: StationId to_station_id: StationId available_from: PlanningMinute available_to: PlanningMinute 34. ODPair @dataclass(frozen=True) class ODPair: id: str origin_station_id: StationId destination_station_id: StationId 35. Demand @dataclass(frozen=True) class Demand: id: DemandId od_pair_id: str commodity_code: str time_start: PlanningMinute time_end: PlanningMinute quantity_tons: Tons priority: int Invariant: time_start < time_end quantity > 0 36. FreightFlow Demand و TrainFlow یکی نیستند. @dataclass(frozen=True) class FreightFlow: id: str demand_id: DemandId route_id: RouteId train_type_id: TrainTypeId wagon_type_id: WagonTypeId quantity_tons: Tons 37. Market Demand Boundary سه سطح باید جدا باقی بمانند: Market Demand ↓ Transportable Demand ↓ Allocated Demand بنابراین: D_market ≠ D_transportable ≠ D_allocated این تفاوت در Domain مدل خواهد شد و در Application orchestration می‌شود. 38. Scenario @dataclass(frozen=True) class Scenario: id: ScenarioId code: str name: str base_scenario_id: ScenarioId | None changes: tuple["ScenarioChange", ...] Scenario نباید داده پایه را overwrite کند. بلکه: Base Version + Scenario Changes ↓ Effective Planning State 39. Policy Constraint @dataclass(frozen=True) class PolicyConstraint: id: str scenario_id: ScenarioId code: str expression: str hard_constraint: bool مثلاً: F_B >= 40 Policy بخشی از Scenario است، نه Solver-specific configuration. 40. Objective Definition class ObjectiveType(str, Enum): MAX_FREIGHT = "MAX_FREIGHT" MAX_REVENUE = "MAX_REVENUE" MIN_UNSERVED = "MIN_UNSERVED" MIN_COST = "MIN_COST" LEXICOGRAPHIC = "LEXICOGRAPHIC" @dataclass(frozen=True) class ObjectiveDefinition: scenario_id: ScenarioId objective_type: ObjectiveType priority: int weight: float 41. CapacityProfile ظرفیت باید Profile داشته باشد. @dataclass(frozen=True) class CapacityProfile: route_id: RouteId od_pair_id: str train_type_id: TrainTypeId time_start: PlanningMinute time_end: PlanningMinute direction: Direction infrastructure_capacity: int | None operational_capacity: int | None rolling_stock_capacity: int | None transportable_capacity: int | None allocated_capacity: int | None این ساختار از یک عدد ساده: Capacity = 30 جلوگیری می‌کند. 42. Capacity Offer @dataclass(frozen=True) class CapacityOffer: id: str capacity_run_id: str od_pair_id: str route_id: RouteId train_type_id: TrainTypeId available_train_count: int available_tonnage: Tons valid_from: PlanningMinute valid_to: PlanningMinute Offer هنوز تخصیص نیست. 43. Allocation @dataclass(frozen=True) class Allocation: id: str capacity_offer_id: str market_request_id: str allocated_train_count: int allocated_tonnage: Tons Invariant: allocated_train_count >= 0 allocated_tonnage >= 0 و Application باید تضمین کند: Allocation <= Available Offer 44. Capacity Run @dataclass(frozen=True) class CapacityRun: id: CapacityRunId scenario_id: ScenarioId data_version_id: str infrastructure_version_id: str calibration_version_id: str | None model_version: str solver_configuration_hash: str input_snapshot_hash: str این Entity پایه reproducibility است. 45. Validation Model class ValidationSeverity(str, Enum): INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL" @dataclass(frozen=True) class ValidationIssue: code: str severity: ValidationSeverity entity_type: str entity_id: str | None message: str evidence: dict Result: @dataclass(frozen=True) class ValidationResult: valid: bool issues: tuple[ValidationIssue, ...] 46. Bottleneck class BottleneckLevel(str, Enum): BINDING = "BINDING" NEAR_BINDING = "NEAR_BINDING" STRUCTURAL = "STRUCTURAL" @dataclass(frozen=True) class Bottleneck: resource_type: str resource_id: str level: BottleneckLevel utilization: float capacity_before: float | None capacity_after: float | None marginal_impact: float | None explanation: str 47. Proof Model @dataclass(frozen=True) class CapacityProof: candidate_capacity: int next_capacity: int candidate_status: SolverStatus next_status: SolverStatus candidate_valid: bool status: ProofStatus Proof Rule: candidate = FEASIBLE candidate_valid = True next = INFEASIBLE ⇒ PROVEN اما: next = UNKNOWN هرگز: PROVEN نمی‌شود. 48. Explanation @dataclass(frozen=True) class Explanation: code: str title: str summary: str evidence: tuple[str, ...] affected_resources: tuple[str, ...] Explanation باید از Result و Evidence ساخته شود، نه از حدس Solver. 49. Core Domain Services برخی قواعد بین چند Entity هستند و نباید داخل یک Entity قرار گیرند. Route Path Validator class DirectedPathValidator: def validate(self, path, route): ... Formation Validator class FormationValidator: def validate(self, formation, train_type, wagons): ... Wagon Cycle Validator class WagonCycleValidator: def validate(self, cycle): ... Train Path Validator class TrainPathValidator: def validate(self, train_run, station_calls, path): ... 50. Aggregate/Detailed Boundary در Domain Domain نباید این دو را مخلوط کند. Aggregate object: @dataclass(frozen=True) class AggregateAllocation: run_id: str candidate_id: str od_pair_id: str route_id: str train_type_id: str train_count: int freight_tons: Tons time_bucket_start: PlanningMinute time_bucket_end: PlanningMinute direction: Direction operating_regime: str Detailed object: @dataclass(frozen=True) class DetailedSchedulingProblem: train_runs: tuple[TrainRun, ...] paths: tuple[DirectedPath, ...] physical_blocks: tuple[PhysicalBlock, ...] stations: tuple[Station, ...] station_tracks: tuple[StationTrack, ...] junctions: tuple[JunctionMovement, ...] junction_conflicts: tuple[JunctionConflict, ...] operational_windows: tuple[OperationalWindow, ...] horizon_start: PlanningMinute horizon_end: PlanningMinute 51. Domain Invariants Matrix Entity Invariant Station usable length > 0 Track usable length > 0 Block endpoints distinct Block running time ≥ 0 RouteSegment sequence > 0 DirectedPath stations = blocks + 1 DirectedPath ordered TrainRun valid service/path relationship StationCall sequence increasing StationCall departure ≥ arrival + dwell BlockMovement exit ≥ entry BlockMovement clear ≥ exit Formation compatible with train WagonInventory count ≥ 0 WagonCycle chronological Demand quantity > 0 Demand start < end Offer quantity ≥ 0 Allocation quantity ≥ 0 Proof UNKNOWN ≠ INFEASIBLE 52. Data Quality Boundary Domain باید distinction زیر را حفظ کند: VERIFIED HIGH_CONFIDENCE PROVISIONAL UNKNOWN UNTRUSTED DERIVED مثلاً: seir → VERIFIED RequiredWait → PROVISIONAL MaxSpeed → PROVISIONAL Distance → UNTRUSTED Derived segment distance → DERIVED Domain نباید UNTRUSTED value را به‌صورت خودکار وارد محاسبات کند. 53. Source Evidence برای هر Canonical Entity مهم: @dataclass(frozen=True) class FieldEvidence: field_name: str source_reference: SourceReference status: str confidence: float | None مثلاً: TrainStationCall.running_time_to_next ↓ Source: aaa.accdb ↓ TrainNo=100 ↓ Sequence=7 ↓ field=seir ↓ VERIFIED 54. Canonicalization Rules Station StationName ↓ Normalization ↓ Station identity resolution اما normalization نباید اصل Source Value را حذف کند. پس: name normalized_name source_name قابل نگهداری هستند. 55. Train Identity Identity باید حداقل شامل: TrainNo TrainName Origin Destination Direction Operating Pattern باشد. صرفاً: TrainNo برای Identity نهایی کافی نیست. 56. Direction Resolution اولویت: 1. Explicit source direction 2. OD 3. Topology 4. Sequence 5. Chainage 6. Source convention اگر ambiguity باقی ماند: DIRECTION_UNRESOLVED و رکورد Production-ready نیست. نباید سیستم حدس خاموش بزند. 57. Midnight Handling Domain Utility: def normalize_time( previous: PlanningMinute, current_clock_minute: int, ) -> PlanningMinute: ... اگر: current < previous % 1440 باشد، روز بعد در نظر گرفته می‌شود. هدف: 23:46 → 00:36 تبدیل صحیح به: 1426 → 1476 است. 58. Source Distance Rule Source: Distance هرگز با: abs(next_chainage - current_chainage) overwrite نمی‌شود. بلکه: source_distance derived_segment_distance دو Field مستقل هستند. 59. seir Rule seir در Canonical: running_time_to_next است. نه: station.running_time چون مفهوم آن وابسته به: current station next station direction train run است. 60. RequiredWait Rule RequiredWait فعلاً: PROVISIONAL است. تا زمانی که calibration / operational rule آن را تأیید نکرده: RequiredWait نباید به‌صورت Hard Constraint وارد Scheduler شود. 61. Domain Event Boundary در این مرحله Eventهای ساده قابل تعریف هستند: CapacityRunCreated CapacityRunValidated ScheduleGenerated ScheduleValidated CapacityProven CapacityOfferPublished AllocationCreated اما Event Bus واقعی در این مرحله لازم نیست. Eventها ابتدا Domain/Application events هستند. 62. Repository Interfaces Interfaceهای اصلی: StationRepository StationTrackRepository PhysicalBlockRepository RouteRepository DirectedPathRepository TrainTypeRepository TrainServiceRepository TrainRunRepository TrainStationCallRepository WagonRepository WagonInventoryRepository WagonCycleRepository LocomotiveRepository LocomotiveAssignmentRepository DemandRepository FreightFlowRepository ScenarioRepository CapacityRunRepository ScheduleRepository ValidationRepository ProofRepository BottleneckRepository Implementation: PostgresStationRepository PostgresTrainRunRepository ... در Infrastructure قرار می‌گیرد. 63. Planning Snapshot Domain-facing aggregate: @dataclass(frozen=True) class PlanningSnapshot: data_version_id: str infrastructure_version_id: str calibration_version_id: str | None stations: tuple[Station, ...] station_tracks: tuple[StationTrack, ...] blocks: tuple[PhysicalBlock, ...] routes: tuple[Route, ...] paths: tuple[DirectedPath, ...] train_types: tuple[TrainType, ...] train_services: tuple[TrainService, ...] train_runs: tuple[TrainRun, ...] station_calls: tuple[TrainStationCall, ...] wagon_types: tuple[WagonType, ...] wagons: tuple[Wagon, ...] wagon_inventory: tuple[WagonInventory, ...] locomotive_types: tuple[LocomotiveType, ...] locomotives: tuple[Locomotive, ...] demands: tuple[Demand, ...] freight_flows: tuple[FreightFlow, ...] scenario: Scenario Snapshot immutable است. 64. Snapshot Hash Hash باید روی Canonical representation محاسبه شود: Canonical Snapshot ↓ Deterministic Serialization ↓ SHA-256 ↓ input_snapshot_hash بنابراین: same input + same model + same solver config قابل بازتولید است. 65. Aggregate Root Principles Aggregateهای پیشنهادی: TrainRun Aggregate Demand Aggregate Scenario Aggregate CapacityRun Aggregate CapacityOffer Aggregate اما همه Entityها نباید Aggregate Root باشند. مثلاً: TrainStationCall می‌تواند تحت: TrainRun مدیریت شود. 66. Domain Must Not Perform I/O ممنوع: open(...) requests.get(...) session.query(...) pd.read_excel(...) pyodbc.connect(...) داخل Domain. Domain فقط: Input ↓ Rule ↓ Output دارد. 67. Test Matrix برای Domain حداقل این تست‌ها: Infrastructure Station valid Station invalid length Track compatibility Block valid Block invalid Junction conflict Operational window Route Segment sequence Directed path Reverse path Station/block count invariant Disconnected path Train Train identity Station call order Dwell Midnight rollover Block movement Rolling Stock Formation length Formation weight Wagon compatibility Wagon cycle Locomotive assignment Demand OD validity Demand quantity Time window Freight flow Capacity Offer Allocation Proof UNKNOWN handling 68. Golden Domain Test یک Golden Case کوچک: GAR → SAKHEH با: 3 candidate trains 1000 tons/train Demand = 3000 tons باید بتواند Domain objectهای زیر را بسازد: Station Block Route DirectedPath TrainType TrainService TrainRun TrainStationCall TrainFormation WagonInventory Locomotive Demand Scenario اما Golden Infrastructure و Rolling Stock همچنان Test Fixture هستند و نباید به‌عنوان داده واقعی شبکه تلقی شوند. 69. Anti-Corruption Layer Access/Excel و Marketplace مستقیماً Domain را populate نمی‌کنند. Access ↓ Source DTO ↓ Mapping ↓ Canonical Factory ↓ Domain و: Marketplace ↓ Market DTO ↓ Adapter ↓ Demand / MarketRequest 70. Factory Layer برای ساخت Entityهای پیچیده: TrainRunFactory DirectedPathFactory FormationFactory DemandFactory CapacityOfferFactory Factory مسئول: Normalization Validation Invariant enforcement است. اما Factory نباید database query کند. 71. Canonical Mapping Contract مثلاً: class TrainStationCallMapper(Protocol): def map(self, source_record) -> TrainStationCall: ... Mapping باید deterministic باشد. یک Source Record با همان Data Version باید همان Canonical object را تولید کند. 72. V2.6-C Acceptance Test سناریوی پذیرش: Raw Access-shaped record ↓ Mapped fields ↓ TrainStationCall مثلاً: TrainNo = 100 Sequence = 7 time_in = ... time_take = ... seir = 18 Kilometerage = 250 خروجی: TrainRun.source_train_no = 100 StationCall.sequence = 7 StationCall.dwell = ... StationCall.running_time_to_next = 18 StationCall.chainage_m = 250 و: Distance نباید جایگزین chainage یا derived distance شود. 73. What V2.6-C Does Not Prove حتی اگر تمام Domain Tests پاس شوند: Domain Valid به معنی: Railway Capacity Proven نیست. هنوز باید: Infrastructure + Train Schedule + Rolling Stock + Detailed CP-SAT + Independent Validation + F/F+1 Proof اجرا شوند. 74. Definition of Done V2.6-C زمانی Complete است که: تمام Entityهای Canonical اصلی Type-Safe باشند. Value Objectهای زمان/وزن/طول وجود داشته باشند. Domain از Infrastructure مستقل باشد. Domain از OR-Tools مستقل باشد. DirectedPath invariant enforce شود. TrainRun و TrainFormation جدا باشند. TrainRun و OperationalBatch جدا باشند. PhysicalBlock و DirectedPath جدا باشند. TrainStationCall زمان‌های absolute داشته باشد. Midnight rollover تست شود. seir در سطح Station Call / Segment مدل شود. Source Distance و Derived Distance جدا باشند. RequiredWait به‌صورت Provisional نگهداری شود. Wagon Cycle مدل شود. Locomotive Cycle مدل شود. Demand و FreightFlow جدا باشند. Market Demand / Transportable / Allocated قابل تفکیک باشند. CapacityOffer و Allocation جدا باشند. Capacity Proof از Feasibility جدا باشد. Source Evidence قابل نگهداری باشد. Domain Test Suite ایجاد شود. Golden Domain Fixture ایجاد شود. PlanningSnapshot immutable باشد. Snapshot Hash deterministic باشد. Repository Interfaces تعریف شده باشند. 75. خروجی نهایی V2.6-C در پایان این مرحله معماری باید به این وضعیت برسد: ┌───────────────────────┐ │ Source Adapters │ │ Access / Excel / API │ └───────────┬───────────┘ │ ▼ ┌─────────────────┐ │ Mapping Layer │ └────────┬────────┘ │ ▼ ┌─────────────────────────────┐ │ CANONICAL DOMAIN │ │ │ │ Infrastructure │ │ Routes / Paths │ │ Train Operations │ │ Rolling Stock │ │ Demand │ │ Planning │ │ Capacity / Result │ └──────────────┬──────────────┘ │ ▼ ┌────────────────────┐ │ Planning Snapshot │ └──────────┬─────────┘ │ ┌─────────────┴─────────────┐ ▼ ▼ Aggregate Solver Detailed Scheduler │ ▼ Validator │ ▼ Proof 76. اصل نهایی V2.6-C هسته Domain باید بتواند یک جمله بسیار مهم را enforce کند: هر چیزی که وارد Solver می‌شود، ابتدا باید یک Canonical Railway Object معتبر باشد. بنابراین: Bad Source Mapping ↓ Invalid Canonical Object ↓ Blocked نه: Bad Source Mapping ↓ Solver ↓ Unexpected Result و از طرف دیگر: Canonical Valid ↓ Scheduler ↓ FEASIBLE هنوز مساوی: PROVEN نیست. تنها: FEASIBLE + INDEPENDENT VALIDATION + F/F+1 می‌تواند به: PROVEN CAPACITY منجر شود. 77. مسیر مستقیم بعدی بعد از V2.6-C، سه مرحله به‌ترتیب منطقی هستند: V2.6-D Access / Excel Production Ingestion ↓ V2.6-E Production Detailed CP-SAT ↓ V2.6-F Independent Validator در V2.6-D، داده واقعی Access/Excel وارد همین Domain می‌شود؛ یعنی دیگر Mapping و Canonical Model صرفاً Fixture نیستند و Pipeline واقعی: Access/Excel → Raw → Staging → Mapping → Quality → Identity → Canonical → PostgreSQL پیاده خواهد شد. این مرحله عملاً Domain Kernel پروژه را تثبیت می‌کند. گام بعدی V2.6-D — Production Access/Excel Ingestion & Canonical Mapping Pipeline است؛ یعنی اتصال واقعی فایل‌های aaa.accdb و Excel به همین Kernel، با Data Quality، Lineage، Reconciliation و کنترل تغییر Schema. --- ## ادامه 88 — پرسش کاربر (`message-id:962b512d-e49e-47e1-b353-0171f42e9549`) بله لطفا --- ## ادامه 88 — پاسخ دستیار (`message-id:d0a80c76-2772-4cbc-8ba1-1dd3316efed4`) حتماً. حالا وارد یکی از مهم‌ترین بخش‌های اجرایی می‌شویم: V2.6-D — Production Access/Excel Ingestion & Canonical Mapping Pipeline. این مرحله باید به‌گونه‌ای ساخته شود که داده واقعی بدون از دست رفتن شواهد، زمان، ترتیب مسیر، Direction و Lineage وارد مدل Canonical شود. V2.6-D — Production Access/Excel Ingestion & Canonical Mapping Pipeline V2.6-D Production Access/Excel Ingestion & Canonical Mapping Pipeline Project: Iranian Railway Freight Capacity Planning, Scheduling & Network Optimization Platform Baseline: V2.6-A + V2.6-B + V2.6-C Stage: V2.6-D Status: Production Implementation Specification 1. هدف هدف این مرحله ساخت Pipeline واقعی برای تبدیل منابع عملیاتی موجود به Canonical Railway Domain است: Access / Excel ↓ Source File ↓ Raw Records ↓ Staging ↓ Schema Detection ↓ Field Mapping ↓ Identity Resolution ↓ Time Normalization ↓ Sequence / Direction Resolution ↓ Reconciliation ↓ Data Quality ↓ Canonical Domain ↓ PostgreSQL ↓ Planning Snapshot اصل بنیادی: Source Data هیچ‌گاه مستقیماً وارد Solver نمی‌شود. 2. Scope این مرحله شامل: Access ingestion Excel ingestion Raw preservation Source schema fingerprinting Source record identity Field mapping Type conversion Time normalization Midnight rollover Train identity resolution Station identity resolution Direction resolution Sequence validation seir mapping Chainage handling Derived distance Reconciliation Data quality Lineage Versioning Re-ingestion Idempotency Source schema change detection Canonical materialization است. 3. Non-Goals در V2.6-D هنوز این موارد پیاده‌سازی کامل نمی‌شوند: Detailed CP-SAT production solver Network optimization Capacity proof Marketplace allocation Automatic infrastructure inference Automatic correction of ambiguous railway data داده نامطمئن باید Flag شود، نه اینکه سیستم برای آن حدس بزند. 4. Source of Truth Hierarchy: 1. Raw Source 2. Staging 3. Mapping 4. Reconciliation 5. Canonical 6. Solver Input هر لایه باید بتواند به لایه قبل Trace شود. مثلاً: TrainStationCall ↓ MappingRecord ↓ SourceRecord ↓ SourceFile ↓ DataVersion 5. Supported Sources Access منبع عملیاتی: aaa.accdb و ساختار شناخته‌شده نمونه: ID kol TrainNo StationName StationNumber Sequence time_in time_take time_out RequiredWait Kilometerage MaxSpeed TrainName Distance sumDistancezz seir Excel ساختار عملیاتی: ردیف نام قطار شماره قطار از مبدا ساعت حرکت از مبدا روزهای حرکت از مبدا ساعت ورود به مقصد شماره قطار از مقصد ساعت حرکت از مقصد روزهای حرکت از مقصد ساعت رسیدن به مبدا این دو Source از نظر مدل کسب‌وکار الزاماً یکسان نیستند و نباید با یک Parser مشترک و فرضیات مشترک به زور یکسان شوند. 6. Source Adapter Architecture ┌──────────────────┐ │ Access Adapter │ └────────┬─────────┘ │ Source Record DTO │ ▼ ┌──────────────────┐ │ │ │ Ingestion Layer │ │ │ └────────┬─────────┘ ▲ │ Source Record DTO │ ┌────────┴─────────┐ │ Excel Adapter │ └──────────────────┘ Adapter فقط مسئول: Read Parse Extract Preserve است. Adapter نباید: calculate capacity resolve bottleneck schedule trains کند. 7. Source DTO Raw source باید بدون Canonical assumptions نگهداری شود. @dataclass(frozen=True) class RawSourceRecord: source_file_id: str source_record_id: str source_row_number: int | None payload: dict[str, object] source_schema_hash: str مثلاً Access record: { "ID": 15, "TrainNo": 100, "StationName": "...", "Sequence": 7, "time_in": "...", "seir": 18 } همان payload اصلی باید حفظ شود. 8. Source File برای هر فایل: source_file ثبت شود: id file_name file_type file_hash file_size source_system received_at schema_hash data_version_id metadata file_hash برای Idempotency ضروری است. 9. File Hash قبل از Processing: File ↓ SHA-256 ↓ file_hash اگر همان فای�� قبلاً ingest شده باشد: same hash + same ingestion configuration نباید duplicate data تولید کند. 10. Schema Fingerprint Schema باید fingerprint شود. برای Access: Table Column Name Column Type Ordinal Position Nullable برای Excel: Sheet Column Header Column Position Detected Type سپس: Schema Definition ↓ Canonical Serialization ↓ SHA-256 ↓ schema_hash 11. Schema Change Detection اگر Source: TrainNo StationName Sequence را داشته باشد و در فایل جدید: TrainNumber StationName SequenceNo ظاهر شود: SOURCE_SCHEMA_CHANGED باید صادر شود. سیستم نباید silently mapping جدید حدس بزند. 12. Ingestion Run برای هر ingest: ingestion_run با: id source_file_id data_version_id adapter_version mapping_version status started_at completed_at records_read records_accepted records_rejected warnings_count errors_count Status: CREATED READING STAGING MAPPING QUALITY_CHECK MATERIALIZING COMPLETED FAILED 13. Staging Layer Raw Record مستقیماً Canonical نمی‌شود. Raw ↓ Staging Staging record: id ingestion_run_id source_record_id normalized_payload parse_status parse_errors Staging هنوز Canonical نیست. 14. Type Parsing هر Source Field ابتدا Parse می‌شود. مثلاً: "100" ↓ int(100) یا: "23:46" ↓ SourceClockTime(23, 46) Parsing failure: PARSE_ERROR است، نه مقدار صفر. 15. Null Handling نباید: NULL → 0 NULL → "" NULL → false به‌صورت implicit انجام شود. مثلاً: MaxSpeed = NULL نباید: MaxSpeed = 0 شود. بلکه: None + PROVISIONAL/UNKNOWN باقی بماند. 16. Field Mapping Registry Mapping Registry: source_system source_field canonical_entity canonical_field mapping_version status confidence transformation مثال: Source Canonical Status TrainNo TrainRun.source_train_no VERIFIED TrainName TrainService.service_name VERIFIED StationName Station.name VERIFIED StationNumber Station.source_number VERIFIED Sequence TrainStationCall.sequence VERIFIED time_in arrival VERIFIED time_take dwell VERIFIED time_out source departure VERIFIED RequiredWait required_wait PROVISIONAL Kilometerage chainage HIGH MaxSpeed max_speed PROVISIONAL Distance source_distance UNTRUSTED sumDistancezz source_cumulative_distance UNKNOWN seir running_time_to_next VERIFIED 17. Mapping Status VERIFIED HIGH_CONFIDENCE PROVISIONAL DERIVED UNKNOWN UNTRUSTED REJECTED Policy: UNKNOWN یعنی: معنی هنوز اثبات نشده است. نه: مقدار صفر است. 18. Source Record Lineage برای هر Mapping: source_record_id source_field canonical_entity_id canonical_field mapping_rule_id mapping_version ثبت شود. مثال: TrainStationCall:CALL-100-07 ↓ source_record:ACCESS-100-07 ↓ field:seir ↓ canonical:running_time_to_next 19. Train Identity Resolution Identity: TrainNo + TrainName + Origin + Destination + Direction + OperatingPattern است. مثلاً: 100 + گار-اندیمشک1 + Gar + Andimeshk + Forward + Pattern-A یک TrainRun/Service identity ایجاد می‌کند. 20. TrainNo Alone Is Not Identity این: TrainNo = 100 به‌تنهایی نباید برای Entity Identity نهایی کافی تلقی شود. ممکن است: در تاریخ دیگر تکرار شود. در جهت برگشت شماره دیگری داشته باشد. Service متفاوتی داشته باشد. Operating pattern متفاوت باشد. 21. Station Identity Resolution Primary preference: StationNumber اگر mapping آن معتبر باشد. در غیر این صورت: normalized StationName + source context استفاده می‌شود. اگر دو Station ممکن وجود داشته باشد: STATION_IDENTITY_AMBIGUOUS و رکورد Production-ready نیست. 22. Station Name Normalization Normalization فقط برای matching است. مثلاً: "تهران" " تهران " "تهران‌" ممکن است به یک normalized form برسند. اما Source value اصلی حذف نمی‌شود. source_name normalized_name هر دو حفظ می‌شوند. 23. Sequence Validation برای هر TrainRun: Sequence باید: 1, 2, 3, ..., N یا یک Sequence معتبر و strictly increasing باشد. مشکلات: duplicate sequence missing sequence negative sequence unordered records باید Flag شوند. 24. Ordering Rule اگر Source Recordها خارج از ترتیب خوانده شوند: Database order نباید ملاک باشد. ترتیب Canonical: Sequence است. Fallback: Topology و در صورت نیاز: Chainage + Direction اما ترتیب Alphabetical هرگز مجاز نیست. 25. Direction Resolution اولویت: 1. Explicit direction 2. OD 3. Topology 4. Sequence 5. Chainage 6. Source convention اگر ambiguity: DIRECTION_UNRESOLVED 26. Chainage Kilometerage به‌عنوان: chainage با confidence بالا نگهداری می‌شود. مثلاً: Station A = 157 Station B = 250 Station C = 674 Derived distance: A → B = |250 - 157| = 93 B → C = |674 - 250| = 424 اما این مقادیر: DERIVED هستند. 27. Source Distance اگر: Distance = 0 باشد، سیستم نباید بگوید: distance = 0 به‌عنوان فاصله واقعی. بلکه: source_distance = 0 quality = UNTRUSTED و derived distance جدا نگهداری می‌شود. 28. seir seir در Source به‌صورت: running_time_to_next Mapping می‌شود. مثلاً: Station A seir = 18 یعنی: A → next station 18 minutes نه: Station A has running time = 18 29. Time Parsing Source Clock Time: @dataclass(frozen=True) class SourceClockTime: hour: int minute: int Validation: 0 <= hour <= 23 0 <= minute <= 59 30. Midnight Normalization Algorithm: previous_absolute current_clock ↓ if current_clock < previous_clock: day_offset += 1 ↓ absolute_minute مثال: 23:46 00:36 00:56 02:19 نتیجه: 1426 1476 1496 1579 31. Time Reconciliation سه زمان مهم: arrival dwell departure قاعده: expected_departure = arrival + dwell سپس با Source departure مقایسه می‌شود. اگر: source_departure != expected_departure باشد: TIME_RECONCILIATION_CONFLICT ثبت می‌شود. هیچ مقدار Source نباید silently overwrite شود. 32. Running Time Reconciliation برای دو Station Call: departure_i + seir_i = expected_arrival_{i+1} با arrival واقعی مقایسه می‌شود. اختلاف: RUNNING_TIME_CONFLICT است. 33. RequiredWait در این مرحله: RequiredWait → PROVISIONAL است. اگر: dwell < RequiredWait باشد: REQUIRED_WAIT_CONFLICT ثبت می‌شود. اما Pipeline نباید خودکار: dwell = RequiredWait کند. 34. MaxSpeed MaxSpeed فعلاً: PROVISIONAL است. در صورتی که خالی باشد: None نه: 0 و Solver فقط زمانی از آن استفاده می‌کند که Train Profile/Infrastructure mapping آن را معتبر کرده باشد. 35. TrainStationCall Materialization Mapping: Raw Record ↓ Staging ↓ Parsed Record ↓ Train Identity ↓ Station Identity ↓ Time Normalization ↓ Sequence ↓ TrainStationCall 36. DirectedPath Materialization برای هر TrainRun: TrainStationCall[] ↓ Sequence Ordering ↓ Station Chain ↓ Block Mapping ↓ DirectedPath Invariant: stations = blocks + 1 اگر Block mapping ناقص باشد: PATH_NOT_MAPPED نه: INFEASIBLE 37. Physical Block Mapping Mapping باید از: from_station to_station direction infrastructure_version استفاده کند. مثلاً: Gar → Andimeshk و: Andimeshk → Gar دو Directed Movement هستند، حتی اگر Physical Block مشترک باشد. 38. Single / Double Track از Source operational data نباید به‌صورت خودکار TrackType استخراج شود مگر اینکه شواهد معتبر وجود داشته باشد. اگر TrackType مشخص نیست: TRACK_TYPE_UNKNOWN و Detailed Capacity Planning باید Block شود. Operational Data Analysis همچنان می‌تواند اجرا شود. 39. Infrastructure Completeness برای Capacity Planning حداقل: Station PhysicalBlock TrackType RunningTime Headway SwitchTime ClearingTime باید معتبر باشند. Missing infrastructure: INFRASTRUCTURE_INCOMPLETE نتیجه: Capacity Planning = BLOCKED اما: Operational Data Analysis = ALLOWED 40. Data Quality Pipeline Quality stages: Structural Quality ↓ Type Quality ↓ Identity Quality ↓ Temporal Quality ↓ Sequence Quality ↓ Mapping Quality ↓ Infrastructure Readiness ↓ Canonical Readiness 41. Quality Status PASSED PASSED_WITH_WARNINGS FAILED Readiness: NOT_READY READY_WITH_WARNINGS READY REJECTED 42. Quality Rule Model @dataclass(frozen=True) class QualityIssue: rule_code: str severity: str entity_type: str entity_id: str | None source_record_id: str | None message: str evidence: dict 43. Quality Rule Examples SOURCE_SCHEMA_CHANGED DUPLICATE_SOURCE_RECORD INVALID_TIME MIDNIGHT_ROLLOVER_DETECTED TIME_RECONCILIATION_CONFLICT RUNNING_TIME_CONFLICT DUPLICATE_SEQUENCE MISSING_SEQUENCE STATION_IDENTITY_AMBIGUOUS DIRECTION_UNRESOLVED PATH_NOT_MAPPED TRACK_TYPE_UNKNOWN REQUIRED_WAIT_CONFLICT SOURCE_DISTANCE_UNTRUSTED INFRASTRUCTURE_INCOMPLETE 44. Severity پیشنهاد: INFO WARNING ERROR CRITICAL مثلاً: MIDNIGHT_ROLLOVER_DETECTED → INFO TIME_RECONCILIATION_CONFLICT → WARNING DIRECTION_UNRESOLVED → ERROR PATH_NOT_MAPPED → ERROR SOURCE_SCHEMA_CHANGED → CRITICAL Severity نهایی باید configurable باشد. 45. Reconciliation Engine Architecture: Source ↓ Canonical Candidate ↓ Reconciliation Rules ↓ Reconciliation Record ↓ Resolution Resolution: MATCHED WARNING CONFLICT RESOLVED UNRESOLVED 46. No Silent Correction ممنوع: Source arrival = 100 Source dwell = 20 Source departure = 130 System: departure = 120 بدون ثبت. صحیح: source_departure = 130 expected_departure = 120 difference = 10 status = CONFLICT و سپس Policy تصمیم می‌گیرد چه شود. 47. Data Version هر Ingestion به یک: DataVersion متصل است. مثلاً: 2026-09-28-operational-v01 Data Version immutable است. اگر Source جدید آمد: new DataVersion ایجاد می‌شود. 48. Incremental Ingestion دو حالت: Full entire source Incremental only new/changed records تشخیص تغییر: source_record_hash 49. Record Hash برای هر Record: canonical raw serialization ↓ SHA-256 ↓ source_record_hash این Hash برای: Duplicate detection Change detection Incremental ingestion Lineage استفاده می‌شود. 50. Changed Record اگر: same source identity + different record hash باشد: SOURCE_RECORD_CHANGED ثبت می‌شود. نسخه قبلی حذف نمی‌شود. 51. Immutable Source History نباید: old source record overwrite شود. بلکه: Version 1 Version 2 Version 3 قابل trace باقی بمانند. 52. Access Adapter Access Adapter مسئول: Open database ↓ Enumerate tables ↓ Read schema ↓ Read records ↓ Convert to RawSourceRecord است. نباید mapping Railway داخل Access Adapter قرار گیرد. 53. Excel Adapter Excel Adapter: Open workbook ↓ Enumerate sheets ↓ Detect headers ↓ Read rows ↓ Preserve raw values ↓ RawSourceRecord نام Sheet باید در Metadata نگهداری شود. 54. Excel Header Handling Header matching باید: exact normalized configured alias را پشتیبانی کند. اما aliasها باید versioned باشند. مثلاً: "نام قطار" "نام‌قطار" "TrainName" می‌توانند mapping aliases داشته باشند. 55. Mapping Version هر Mapping Rule: mapping_version دارد. مثلاً: mapping-v1 mapping-v2 Run باید mapping version خود را ذخیره کند. 56. Ingestion Configuration @dataclass(frozen=True) class IngestionConfiguration: adapter_version: str mapping_version: str strict_schema: bool allow_warnings: bool source_timezone: str 57. Ingestion Use Case class IngestSource: def execute( self, source, configuration, ) -> IngestionResult: ... Flow: Create DataVersion ↓ Register SourceFile ↓ Fingerprint Schema ↓ Read Raw ↓ Stage ↓ Map ↓ Reconcile ↓ Quality ↓ Materialize Canonical 58. Materialization Policy فقط رکوردهایی که: Canonical Valid هستند وارد Production Canonical tables شوند. رکوردهای مشکل‌دار در: source staging quality باقی می‌مانند. 59. Partial Acceptance Dataset می‌تواند: 10000 records 9800 valid 150 warning 50 rejected داشته باشد. Policy تعیین می‌کند: READY_WITH_WARNINGS یا: REJECTED شود. 60. Production Readiness برای Capacity Planning: READY باید حداقل شامل: Train identity resolved Station identity resolved Sequence valid Time normalized Direction resolved DirectedPath mapped Infrastructure complete Required solver parameters available باشد. 61. Operational Data Analysis حتی اگر: TrackType unknown باشد: Operational Analysis می‌تواند اجرا شود: train counts station dwell running time service frequency OD patterns time patterns اما: Operational Capacity Detailed Capacity Proof نباید اجرا شوند. 62. PostgreSQL Write Strategy Ingestion باید batch-oriented باشد. ممنوع: for row: INSERT ... در مقیاس بزرگ. صحیح: Read batch ↓ Validate batch ↓ Bulk insert 63. Transaction Boundary برای هر Ingestion: SourceFile DataVersion IngestionRun ابتدا ثبت شوند. Staging می‌تواند chunked باشد. Canonical materialization باید transactional باشد. در صورت failure: Canonical state نباید نیمه‌کاره باقی بماند. 64. Idempotency کل ingestion identity: file_hash + adapter_version + mapping_version + configuration_hash اگر قبلاً موفق بوده: ALREADY_INGESTED و Run جدید duplicate data ایجاد نمی‌کند. 65. Retry Retry مجاز: temporary DB connection failure temporary storage failure temporary queue failure Retry غیرمجاز برای: SOURCE_SCHEMA_CHANGED MAPPING_ERROR INVALID_DATA DIRECTION_UNRESOLVED PATH_NOT_MAPPED 66. Access/Excel Audit هر Ingestion باید audit داشته باشد: who when what file which version which adapter which mapping which configuration what result 67. Ingestion Result @dataclass(frozen=True) class IngestionResult: ingestion_run_id: str status: str records_read: int records_staged: int records_materialized: int records_rejected: int warnings: int errors: int data_version_id: str schema_hash: str mapping_version: str 68. Acceptance Case — Access Source: TrainNo = 100 TrainName = گار-اندیمشک1 Sequence = 7 time_in = ... time_take = ... seir = 18 Kilometerage = 250 Pipeline: Access ↓ Raw ↓ Stage ↓ Mapping Expected: TrainRun.source_train_no = 100 TrainService.service_name = گار-اندیمشک1 StationCall.sequence = 7 StationCall.dwell = parsed time_take StationCall.running_time_to_next = 18 StationCall.chainage_m = 250 69. Acceptance Case — Midnight Input: 23:46 00:36 00:56 02:19 Expected: 1426 1476 1496 1579 No negative running time. No artificial same-day assumption. 70. Acceptance Case — Direction Input: Train 100 chainage: 157 → 250 → 674 Direction: FORWARD Reverse movement: 674 → 250 → 157 must produce: REVERSE when supported by source/OD/topology evidence. 71. Acceptance Case — Distance Input: Kilometerage: 157 250 Distance: 0 0 Expected: source_distance = 0 source_distance_quality = UNTRUSTED derived_distance = 93 derived_distance_quality = DERIVED 72. Acceptance Case — RequiredWait Input: time_in = 100 time_take = 5 RequiredWait = 8 Expected: dwell = 5 required_wait = 8 quality = PROVISIONAL issue = REQUIRED_WAIT_CONFLICT Pipeline must not silently change dwell to 8. 73. Acceptance Case — Schema Change If source changes: TrainNo to: TrainNumber without a registered mapping: SOURCE_SCHEMA_CHANGED and: Production Materialization = BLOCKED 74. Acceptance Case — Missing Infrastructure Canonical operational data valid: TrainRun StationCall DirectedPath but: TrackType = UNKNOWN Result: Operational Data Analysis = READY Capacity Planning = BLOCKED Proof = BLOCKED 75. Test Pyramid E2E ───────── Integration ─────────────── Mapping / Quality ───────────────────── Unit Tests 76. Unit Tests حداقل: test_parse_clock_time test_midnight_rollover test_time_reconciliation test_sequence_validation test_station_normalization test_direction_resolution test_chainage_distance test_required_wait_quality test_source_hash test_schema_hash 77. Integration Tests Access: test_access_schema_detection test_access_raw_ingestion test_access_record_lineage Excel: test_excel_header_detection test_excel_raw_ingestion test_excel_sheet_metadata PostgreSQL: test_staging_insert test_canonical_materialization test_transaction_rollback test_idempotent_ingestion 78. Contract Tests برای Source Adapter: Access Adapter Excel Adapter هر دو باید Contract مشترک زیر را پاس کنند: read() → RawSourceRecord[] اما semantics Source-specific حفظ می‌شود. 79. Golden Dataset Golden input شامل: TrainNo 100 TrainNo 101 midnight rollover seir chainage RequiredWait Distance=0 خواهد بود. Expected Mapping باید version-controlled باشد. 80. No Real Capacity Claim اجرای Ingestion روی داده واقعی: ≠ real railway capacity result حتی اگر Canonicalization کاملاً موفق باشد. Capacity هنوز نیازمند: Infrastructure Master + Train Profile + Formation + Wagon + Locomotive + Detailed Scheduler است. 81. Performance Requirements Targetهای اولیه، نه ادعای عملکرد فعلی: 10k records → normal batch ingestion 100k records → batch + indexed persistence 1M+ records → chunked ingestion + bulk insert Metrics: records/sec parse_time mapping_time quality_time DB_write_time canonicalization_time total_time memory_usage این Metrics ورودی V2.6-O خواهند بود. 82. Data Lineage Graph برای هر Field مهم: Source File ↓ Source Record ↓ Source Field ↓ Mapping Rule ↓ Canonical Field ↓ Planning Snapshot ↓ Capacity Run ↓ Result این Lineage باید قابل Query باشد. 83. Example Lineage aaa.accdb ↓ TrainNo=100 ↓ TrainRun.source_train_no ↓ TrainRun RUN-100 ↓ CapacityRun CR-2026-001 ↓ Schedule در نتیجه اگر User بپرسد: این Train از کجا آمده؟ سیستم باید بتواند Source Record را نشان دهد. 84. Production API Endpoint پیشنهادی: POST /api/v1/ingestions GET /api/v1/ingestions/{id} GET /api/v1/ingestions/{id}/quality GET /api/v1/ingestions/{id}/mapping GET /api/v1/ingestions/{id}/lineage برای فایل: POST /api/v1/source-files 85. CLI railway ingest access \ --file ./data/aaa.accdb railway ingest excel \ --file ./data/report.xlsx Quality: railway ingest quality \ --run-id ING-001 Lineage: railway lineage \ --entity train_run \ --id RUN-100 86. Ingestion State Machine CREATED ↓ READING ↓ STAGING ↓ SCHEMA_VALIDATED ↓ MAPPING ↓ RECONCILIATION ↓ QUALITY_CHECK ↓ MATERIALIZING ↓ COMPLETED Failure: FAILED_SCHEMA FAILED_PARSING FAILED_MAPPING FAILED_QUALITY FAILED_MATERIALIZATION 87. Failure Semantics Data Error Bad record → Record rejected/flagged. Dataset Error Schema changed → Dataset blocked. Infrastructure Error Database unavailable → Retry. Mapping Error unknown required field → Block materialization. 88. Production Safety Rule هیچ ingestion موفقی نباید به این معنی باشد که: all data = trustworthy بلکه: Ingestion Successful فقط یعنی: Pipeline completed according to its Quality Policy. 89. Definition of Done V2.6-D زمانی Complete است که: Access Adapter production-ready باشد. Excel Adapter production-ready باشد. Raw Source Preservation فعال باشد. File Hash فعال باشد. Schema Fingerprint فعال باشد. Source Schema Change Detection فعال باشد. DataVersion ایجاد شود. IngestionRun ایجاد شود. Staging فعال باشد. Field Mapping Registry فعال باشد. Mapping Version ذخیره شود. Train Identity Resolution فعال باشد. Station Identity Resolution فعال باشد. Sequence Validation فعال باشد. Direction Resolution فعال باشد. Midnight Normalization فعال باشد. Time Reconciliation فعال باشد. seir صحیح mapping شود. Chainage حفظ شود. Source Distance overwrite نشود. Derived Distance جدا باشد. RequiredWait Provisional بماند. MaxSpeed بدون شواهد معتبر hard نشود. DirectedPath ساخته شود. Missing infrastructure ظرفیت را Block کند. Quality Status تولید شود. Lineage کامل باشد. Re-ingestion idempotent باشد. Historical Source Version حذف نشود. Integration Tests وجود داشته باشد. Golden Dataset وجود داشته باشد. 90. خروجی نهایی V2.6-D بعد از این مرحله: ACCESS │ ▼ ┌───────────┐ │ RAW │ └─────┬─────┘ │ EXCEL │ ▼ ┌───────────┐ │ STAGING │ └─────┬─────┘ │ ▼ ┌─────────────────┐ │ SCHEMA CHECK │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ FIELD MAPPING │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ IDENTITY │ │ TIME / SEQUENCE │ │ DIRECTION │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ RECONCILIATION │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ DATA QUALITY │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ CANONICAL │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ POSTGRESQL │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ PLANNING │ │ SNAPSHOT │ └─────────────────┘ 91. اصل طلایی V2.6-D سه اصل باید در کد enforce شوند: No Verified Mapping ↓ No Production Use No Complete Infrastructure ↓ No Capacity Planning No Source Evidence ↓ No Silent Correction و در سطح معماری: Raw ≠ Staging ≠ Canonical ≠ Solver Input این تفکیک باعث می‌شود اگر شش ماه بعد مشخص شد مثلاً معنی RequiredWait، MaxSpeed یا Distance متفاوت از فرض فعلی بوده است، بتوانیم Mapping را اصلاح کنیم بدون اینکه تاریخچه Source و نتایج قبلی را از بین ببریم. 92. مرحله بعدی پس از V2.6-D، مرحله بعد: V2.6-E — Production Detailed CP-SAT Scheduler در آن مرحله همین Canonical Model وارد Scheduler واقعی می‌شود و موارد زیر به‌صورت Production Code پیاده خواهند شد: PlanningSnapshot ↓ DetailedSchedulingProblem ↓ Problem Compiler ↓ Time Variables ↓ Precedence ↓ Running Time ↓ Dwell ↓ Single Track Conflicts ↓ Double Track Resources ↓ Headway ↓ Opposing Direction ↓ Switch ↓ Clearing ↓ Station Track Assignment ↓ Station Length ↓ Junction Conflict Matrix ↓ Operational Windows ↓ CP-SAT ↓ Schedule Result و سپس V2.6-F — Independent Validator این Schedule را خارج از Solver به‌صورت مستقل بررسی خواهد کرد. نقطه مهم این مرحله این است که از اینجا به بعد داده واقعی دیگر یک «فایل ورودی» ساده نیست؛ به یک زنجیره versioned و قابل‌ردیابی از Source → Canonical → Snapshot تبدیل می‌شود. این دقیقاً همان چیزی است که برای ورود امن به CP-SAT و بعداً Capacity Proof لازم داریم. --- ## ادامه 89 — پرسش کاربر (`message-id:f70fae15-4cc8-4e1a-9ef5-81dc2dede18d`) بله دقیقا --- ## ادامه 89 — پاسخ دستیار (`message-id:4de3dd84-7452-417a-86b6-60afaa339d08`) دقیقاً. پس ادامه منطقی پروژه را می‌بریم روی V2.6-E — Production Detailed CP-SAT Scheduler؛ یعنی اولین مرحله‌ای که Canonical Data واقعاً به یک Schedule عملیاتی قابل‌اعتبارسنجی تبدیل می‌شود. --- ## ادامه 90 — پرسش کاربر (`message-id:0e2d9746-1d8f-4572-8b95-488fa5195246`) درسته --- ## ادامه 90 — پاسخ دستیار (`message-id:5c8177d5-bfd5-4583-95b7-36002af37f44`) عالی! پس مبنا را قطعی می‌کنیم: V2.6-E ادامه مستقیم V2.6-D است و از اینجا وارد پیاده‌سازی Production Scheduler می‌شویم. --- ## ادامه 91 — پرسش کاربر (`message-id:c79914a2-4317-4309-a7c9-c47d7eb5f33e`) بله همینجا به صورت متنی پیاده سازی رو انجام بدیم --- ## ادامه 91 — پاسخ دستیار (`message-id:4d8f3f01-e017-45a6-b60f-88d51f588531`) حتماً. از همین‌جا وارد پیاده‌سازی واقعی V2.6-E می‌شویم؛ نه صرفاً طراحی مفهومی. در این مرحله کد را به‌صورت ماژولار می‌نویسیم تا مستقیماً بتواند در Repository پروژه قرار بگیرد. در این بخش هسته‌ی Detailed CP-SAT Scheduler را می‌سازیم و در ادامه‌ی همین مسیر، Validator و Capacity Proof Engine روی آن سوار می‌شوند. V2.6-E — Production Detailed CP-SAT Scheduler — Implementation V2.6-E — Production Detailed CP-SAT Scheduler 1. هدف ورودی Scheduler: PlanningSnapshot ↓ DetailedSchedulingProblem ↓ ProblemCompiler ↓ CP-SAT Model ↓ Solver ↓ SchedulingResult Scheduler باید برای تعداد مشخصی TrainRun پاسخ دهد: آیا این مجموعه TrainRun با توجه به Infrastructure، مسیر، زمان‌ها، محدودیت‌های Single/Double Track، Station، Junction و Operational Window قابل زمان‌بندی است؟ خروجی Scheduler صرفاً یک Schedule Candidate است. Scheduler خودش حق ندارد اعلام کند: Capacity Proven این وظیفه‌ی Independent Validator و سپس Capacity Proof Engine است. 2. ساختار Production Package ساختار پیشنهادی: src/ └── railway/ └── optimization/ └── detailed/ ├── __init__.py ├── model.py ├── ports.py ├── compiler.py ├── variables.py ├── conflict_graph.py ├── solver.py ├── decoder.py ├── diagnostics.py ├── hints.py │ └── constraints/ ├── __init__.py ├── precedence.py ├── running_time.py ├── dwell.py ├── blocks.py ├── headway.py ├── opposing.py ├── stations.py ├── junctions.py ├── windows.py ├── boundaries.py └── objective.py اصل مهم: Domain Model ↓ Detailed Scheduler ↓ CP-SAT Adapter و نه: Database → CP-SAT 3. مدل پایه Scheduler model.py from __future__ import annotations from dataclasses import dataclass from enum import Enum from typing import Mapping, Sequence class Direction(str, Enum): FORWARD = "FORWARD" REVERSE = "REVERSE" class TrackType(str, Enum): SINGLE = "SINGLE" DOUBLE = "DOUBLE" class SolverResultStatus(str, Enum): FEASIBLE = "FEASIBLE" INFEASIBLE = "INFEASIBLE" UNKNOWN = "UNKNOWN" MODEL_INVALID = "MODEL_INVALID" class ObjectiveMode(str, Enum): FEASIBILITY = "FEASIBILITY" BASELINE_DEVIATION = "BASELINE_DEVIATION" MIN_TOTAL_DELAY = "MIN_TOTAL_DELAY" MIN_WAITING = "MIN_WAITING" @dataclass(frozen=True) class TimeHorizon: start_minute: int end_minute: int def __post_init__(self) -> None: if self.end_minute <= self.start_minute: raise ValueError("Invalid time horizon") @property def duration(self) -> int: return self.end_minute - self.start_minute @dataclass(frozen=True) class StationCall: train_id: str station_id: str sequence: int arrival_earliest: int arrival_latest: int departure_earliest: int departure_latest: int minimum_dwell: int @dataclass(frozen=True) class BlockMovement: train_id: str block_id: str sequence: int station_from: str station_to: str direction: Direction track_type: TrackType running_time: int headway_same_direction: int switch_time: int clearing_time: int @dataclass(frozen=True) class TrainRun: id: str train_type_id: str direction: Direction length_m: float weight_t: float station_calls: tuple[StationCall, ...] block_movements: tuple[BlockMovement, ...] @dataclass(frozen=True) class StationTrack: id: str station_id: str usable_length_m: float bidirectional: bool @dataclass(frozen=True) class JunctionConflict: movement_a: str movement_b: str separation_minutes: int @dataclass(frozen=True) class OperationalWindow: id: str resource_id: str start_minute: int end_minute: int @dataclass(frozen=True) class DetailedSchedulingProblem: run_id: str horizon: TimeHorizon trains: tuple[TrainRun, ...] station_tracks: tuple[StationTrack, ...] junction_conflicts: tuple[JunctionConflict, ...] operational_windows: tuple[OperationalWindow, ...] objective_mode: ObjectiveMode = ObjectiveMode.FEASIBILITY baseline_arrivals: Mapping[tuple[str, str], int] | None = None baseline_departures: Mapping[tuple[str, str], int] | None = None 4. Solver Options @dataclass(frozen=True) class SolverOptions: time_limit_seconds: float = 60.0 num_workers: int = 8 random_seed: int = 42 use_hints: bool = True log_search_progress: bool = False deterministic: bool = False در Production، این configuration باید داخل RunContext و RunManifest نیز ثبت شود. 5. خروجی Scheduler @dataclass(frozen=True) class ScheduledStationCall: train_id: str station_id: str sequence: int arrival: int departure: int assigned_track_id: str | None @dataclass(frozen=True) class ScheduledBlockMovement: train_id: str block_id: str sequence: int entry: int exit: int clear: int @dataclass(frozen=True) class SchedulingMetrics: variable_count: int boolean_count: int constraint_count: int interval_count: int conflict_candidate_count: int compile_time_ms: int solve_time_ms: int @dataclass(frozen=True) class SchedulingResult: status: SolverResultStatus station_calls: tuple[ScheduledStationCall, ...] block_movements: tuple[ScheduledBlockMovement, ...] objective_value: int | float | None metrics: SchedulingMetrics diagnostics: tuple[str, ...] 6. پورت Scheduler ports.py Interface اصلی: from typing import Protocol class DetailedScheduler(Protocol): def solve( self, problem: DetailedSchedulingProblem, options: SolverOptions, ) -> SchedulingResult: ... این Interface نباید هیچ وابستگی به PostgreSQL یا FastAPI داشته باشد. 7. متغیرهای CP-SAT variables.py متغیرهای اصلی: arrival[train,station] departure[train,station] entry[train,block] exit[train,block] clear[train,block] station_track_interval[train,station,track] conflict_order[conflict] پیاده‌سازی: from dataclasses import dataclass from ortools.sat.python import cp_model @dataclass class SchedulerVariables: arrival: dict[tuple[str, str], cp_model.IntVar] departure: dict[tuple[str, str], cp_model.IntVar] entry: dict[tuple[str, str], cp_model.IntVar] exit: dict[tuple[str, str], cp_model.IntVar] clear: dict[tuple[str, str], cp_model.IntVar] station_track_presence: dict[ tuple[str, str, str], cp_model.BoolVar, ] station_track_intervals: dict[ tuple[str, str, str], cp_model.IntervalVar, ] conflict_order: dict[str, cp_model.BoolVar] Builder: class VariableBuilder: def __init__( self, model: cp_model.CpModel, problem: DetailedSchedulingProblem, ) -> None: self.model = model self.problem = problem def build(self) -> SchedulerVariables: h = self.problem.horizon arrival = {} departure = {} entry = {} exit_ = {} clear = {} for train in self.problem.trains: for call in train.station_calls: key = (train.id, call.station_id) arrival[key] = self.model.NewIntVar( call.arrival_earliest, call.arrival_latest, f"arrival__{train.id}__{call.station_id}", ) departure[key] = self.model.NewIntVar( call.departure_earliest, call.departure_latest, f"departure__{train.id}__{call.station_id}", ) for movement in train.block_movements: key = (train.id, movement.block_id) entry[key] = self.model.NewIntVar( h.start_minute, h.end_minute, f"entry__{train.id}__{movement.block_id}", ) exit_[key] = self.model.NewIntVar( h.start_minute, h.end_minute, f"exit__{train.id}__{movement.block_id}", ) clear[key] = self.model.NewIntVar( h.start_minute, h.end_minute, f"clear__{train.id}__{movement.block_id}", ) return SchedulerVariables( arrival=arrival, departure=departure, entry=entry, exit=exit_, clear=clear, station_track_presence={}, station_track_intervals={}, conflict_order={}, ) 8. نکته مهم درباره Time Domain نباید چنین چیزی در Production داشته باشیم: 0 <= time <= 1440 چون Train ممکن است: 23:50 → 00:20 حرکت کند. زمان باید absolute باشد. مثلاً: Day 0 23:50 = 1430 Day 1 00:20 = 1460 بنابراین: arrival_earliest = 1430 arrival_latest = 1460 نه اینکه ساعت 00:20 از 23:50 کوچک‌تر تلقی شود. 9. Precedence Constraint قطار باید Station Sequence را رعایت کند. اگر: S1 → S2 → S3 باشد: departure(S1) ≤ arrival(S2) departure(S2) ≤ arrival(S3) پیاده‌سازی: class PrecedenceConstraints: @staticmethod def add( model: cp_model.CpModel, train: TrainRun, variables: SchedulerVariables, ) -> None: calls = sorted( train.station_calls, key=lambda x: x.sequence, ) for previous, current in zip(calls, calls[1:]): previous_departure = variables.departure[ (train.id, previous.station_id) ] current_arrival = variables.arrival[ (train.id, current.station_id) ] model.Add( current_arrival >= previous_departure ) این constraint صرفاً precedence ایستگاه‌ها را می‌سازد. زمان حرکت واقعی بین دو ایستگاه در RunningTimeConstraint اعمال می‌شود. 10. Running Time اگر: Block = B1 running_time = 17 min باشد: exit(B1) >= entry(B1) + 17 کد: class RunningTimeConstraints: @staticmethod def add( model: cp_model.CpModel, train: TrainRun, variables: SchedulerVariables, ) -> None: for movement in train.block_movements: key = (train.id, movement.block_id) model.Add( variables.exit[key] >= variables.entry[key] + movement.running_time ) در Production مقدار running_time باید قبلاً توسط TrainOperationalProfile و BlockRunningTimeProfile تعیین شده باشد. Scheduler نباید خودش از روی: Distance / Speed آن را دوباره محاسبه کند. 11. ارتباط Block با Station برای اولین Block: Station A ↓ Block 1 ↓ Station B باید: entry(Block1) >= departure(A) arrival(B) >= exit(Block1) باشد. class BlockStationLinkConstraints: @staticmethod def add( model: cp_model.CpModel, train: TrainRun, variables: SchedulerVariables, ) -> None: for movement in train.block_movements: block_key = (train.id, movement.block_id) from_key = (train.id, movement.station_from) to_key = (train.id, movement.station_to) model.Add( variables.entry[block_key] >= variables.departure[from_key] ) model.Add( variables.arrival[to_key] >= variables.exit[block_key] ) این بخش بسیار مهم است؛ چون زمان Station و Block را به یک Timeline واحد متصل می‌کند. 12. Dwell حداقل توقف: departure >= arrival + minimum_dwell class DwellConstraints: @staticmethod def add( model: cp_model.CpModel, train: TrainRun, variables: SchedulerVariables, ) -> None: for call in train.station_calls: key = (train.id, call.station_id) model.Add( variables.departure[key] >= variables.arrival[key] + call.minimum_dwell ) RequiredWait فقط زمانی وارد minimum_dwell می‌شود که قبلاً در Data Quality / Mapping / Rule Validation معتبر شده باشد. 13. Clearing اگر Block پس از خروج قطار هنوز برای مدت مشخصی occupied باشد: clear >= exit + clearing_time class ClearingConstraints: @staticmethod def add( model: cp_model.CpModel, train: TrainRun, variables: SchedulerVariables, ) -> None: for movement in train.block_movements: key = (train.id, movement.block_id) model.Add( variables.clear[key] >= variables.exit[key] + movement.clearing_time ) 14. Single Track در Single Track، Block یک Resource مشترک است. بنابراین: Train A → Train B ← نمی‌توانند همزمان Block را اشغال کنند. برای دو Movement: A = (train_a, block_x) B = (train_b, block_x) یک Boolean می‌سازیم: order_ab = model.NewBoolVar(...) و: order_ab = True entry(B) >= clear(A) + separation در غیر این صورت: entry(A) >= clear(B) + separation کد: def add_disjunctive_conflict( model: cp_model.CpModel, first_entry, first_clear, second_entry, second_clear, separation: int, name: str, ): order = model.NewBoolVar(name) model.Add( second_entry >= first_clear + separation ).OnlyEnforceIf(order) model.Add( first_entry >= second_clear + separation ).OnlyEnforceIf(order.Not()) return order 15. Opposing Direction در Single Track، direction نباید نادیده گرفته شود. مثلاً: A → B B → A یک Conflict Candidate ایجاد می‌کند. اما: A → B A → B از نوع opposing نیست و باید با Headway مدیریت شود. پس Conflict Graph باید نوع Conflict را مشخص کند. 16. Same Direction Headway برای قطارهای یک جهت: Train A → Block Train B → Block اگر A زودتر عبور کند: entry_B >= clear_A + headway def add_same_direction_headway( model: cp_model.CpModel, first_entry, first_clear, second_entry, headway: int, name: str, ): model.Add( second_entry >= first_clear + headway ) 17. Switch Time در Single Track اگر regime از یک Direction به Direction دیگر تغییر کند: Train A → Switch Train B ← باید: entry_B >= clear_A + switch_time باشد. اما نکته مهم: switch_time الزاماً همان headway نیست. ممکن است شامل: Last Train Clear + Block Release + Route Release + Signalling Preparation + Opposite Direction Authorization + Station Preparation باشد. بنابراین: separation = max( clearing_time, switch_time, operational_separation, ) فقط در صورتی که Rule Engine چنین تعریف کرده باشد. نباید این دو parameter را بدون منطق یکی فرض کنیم. 18. Double Track در Double Track: Direction A → Track A Direction B ← Track B دو Resource مستقل داریم. بنابراین: Forward Resource Reverse Resource و قطارهای مخالف جهت الزاماً Conflict ندارند. ولی موارد زیر هنوز می‌توانند Conflict ایجاد کنند: Station Junction Crossing Single-track segment Terminal Shared infrastructure پس: DOUBLE TRACK ≠ NO CONSTRAINT 19. Conflict Graph این بخش برای Production بسیار مهم است. نباید برای N قطار، تمام: N × N جفت‌ها را بسازیم. ابتدا Candidate Filtering انجام می‌دهیم. @dataclass(frozen=True) class ConflictCandidate: id: str train_a: str movement_a: str train_b: str movement_b: str resource_id: str conflict_type: str minimum_separation: int 20. Candidate Conflict Generation فقط وقتی Candidate بساز: same physical resource AND different trains AND compatible movement AND potential temporal overlap مثلاً: def can_conflict( a: BlockMovement, b: BlockMovement, ) -> bool: if a.train_id == b.train_id: return False if a.block_id != b.block_id: return False if a.track_type == TrackType.DOUBLE: return a.direction == b.direction if a.track_type == TrackType.SINGLE: return True return False این فقط اولین Filter است. در Production باید Time Window نیز بررسی شود. 21. Station Track Assignment هر Train در هر Station در صورت نیاز باید یک Track دریافت کند. فرض: Station S ├── Track 1 ├── Track 2 └── Track 3 برای هر Track: presence = model.NewBoolVar(...) interval = model.NewOptionalIntervalVar(...) سپس: ExactlyOne و برای هر Track: NoOverlap اعمال می‌شود. ساختار: def create_station_track_intervals( model: cp_model.CpModel, train: TrainRun, call: StationCall, tracks: list[StationTrack], variables: SchedulerVariables, ): candidates = [] for track in tracks: if train.length_m > track.usable_length_m: continue presence = model.NewBoolVar( f"track_present__{train.id}__{call.station_id}__{track.id}" ) start = variables.arrival[ (train.id, call.station_id) ] end = variables.departure[ (train.id, call.station_id) ] interval = model.NewOptionalIntervalVar( start, end - start, end, presence, f"station_interval__{train.id}__{call.station_id}__{track.id}", ) variables.station_track_presence[ (train.id, call.station_id, track.id) ] = presence variables.station_track_intervals[ (train.id, call.station_id, track.id) ] = interval candidates.append(presence) if not candidates: raise ValueError( f"No feasible station track for train={train.id}" ) model.AddExactlyOne(candidates) در پیاده‌سازی واقعی، بهتر است duration را با یک IntVar صریح مدل کنیم تا از expression پیچیده در OptionalIntervalVar جلوگیری شود. 22. Station Length قانون: TrainLength <= UsableTrackLength باید قبل از ساخت مدل تا حد امکان Filter شود. یعنی اگر: Train = 650m Track = 500m است: Candidate Track = حذف نه اینکه CP-SAT مجبور شود آن را حل کند. این یکی از مهم‌ترین Candidate Pruningهاست. 23. Junction Conflict Matrix برای Junction نباید این اشتباه را انجام دهیم: All Junction Movements → NoOverlap چون ممکن است: Movement A Movement B همزمان مجاز باشند. مدل صحیح: Movement A conflicts with B Movement A does not conflict with C بنابراین JunctionConflict منبع Truth است. برای Conflict: order = model.NewBoolVar( f"junction_order__{conflict.id}" ) و separation اعمال می‌شود. 24. Operational Windows مثلاً: Brake Test Fueling Maintenance Prayer/Operational restriction Terminal closure Infrastructure possession همه باید در یک abstraction واحد قابل مدل‌سازی باشند: OperationalWindow اگر قطار نباید در بازه‌ای حرکت کند: [start, end] می‌توانیم از disjunction استفاده کنیم: departure <= window.start OR arrival >= window.end کد: def forbid_interval( model: cp_model.CpModel, start_var, end_var, window_start: int, window_end: int, name: str, ): before = model.NewBoolVar(f"{name}__before") after = model.NewBoolVar(f"{name}__after") model.Add( end_var <= window_start ).OnlyEnforceIf(before) model.Add( start_var >= window_end ).OnlyEnforceIf(after) model.AddBoolOr([before, after]) 25. Earliest / Latest تمام Time Variableها باید Bound داشته باشند. مثلاً: Earliest Departure = 08:10 Latest Departure = 08:45 پس: departure = model.NewIntVar( 490, 525, "departure" ) این کار هم correctness را بالا می‌برد و هم Search Space را کوچک می‌کند. 26. Problem Compiler Compiler مسئول ساخت Model است. class ProblemCompiler: def compile( self, problem: DetailedSchedulingProblem, options: SolverOptions, ): self._validate_problem(problem) model = cp_model.CpModel() variables = VariableBuilder( model, problem, ).build() self._add_precedence( model, problem, variables, ) self._add_running_time( model, problem, variables, ) self._add_dwell( model, problem, variables, ) self._add_block_station_links( model, problem, variables, ) self._add_clearing( model, problem, variables, ) self._add_block_conflicts( model, problem, variables, ) self._add_station_constraints( model, problem, variables, ) self._add_junction_constraints( model, problem, variables, ) self._add_operational_windows( model, problem, variables, ) self._add_objective( model, problem, variables, ) return model, variables 27. ترتیب Compile ترتیب Production باید تقریباً این باشد: 1. Validate Problem 2. Normalize/Index Resources 3. Candidate Pruning 4. Create Time Variables 5. Create Optional Intervals 6. Add Precedence 7. Add Running Time 8. Add Dwell 9. Add Block ↔ Station Links 10. Add Clearing 11. Add Single Track Conflicts 12. Add Double Track Headway 13. Add Opposing Direction 14. Add Switch 15. Add Station Tracks 16. Add Station Length 17. Add Junction Conflicts 18. Add Operational Windows 19. Add Boundary Constraints 20. Add Objective 21. Add Hints 22. Emit Model Diagnostics 28. Objective Scheduler نباید با Objective ظرفیت را جعل کند. برای Capacity Check: ObjectiveMode.FEASIBILITY یعنی: Find any valid schedule. برای Baseline: ObjectiveMode.BASELINE_DEVIATION هدف: minimize Σ |actual_arrival - baseline_arrival| + Σ |actual_departure - baseline_departure| برای Delay: MIN_TOTAL_DELAY و برای Waiting: MIN_WAITING 29. Baseline Deviation برای Absolute Value می‌توانیم از Auxiliary Variable استفاده کنیم. مثلاً: d >= actual - baseline d >= baseline - actual و سپس: minimize Σ d نمونه: def add_absolute_deviation( model: cp_model.CpModel, actual, baseline: int, horizon: int, name: str, ): deviation = model.NewIntVar( 0, horizon, name, ) model.Add( deviation >= actual - baseline ) model.Add( deviation >= baseline - actual ) return deviation 30. Solver Adapter solver.py from ortools.sat.python import cp_model class CpSatDetailedScheduler: def __init__(self) -> None: self.compiler = ProblemCompiler() def solve( self, problem: DetailedSchedulingProblem, options: SolverOptions, ) -> SchedulingResult: model, variables = self.compiler.compile( problem, options, ) solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = ( options.time_limit_seconds ) solver.parameters.num_search_workers = ( options.num_workers ) solver.parameters.random_seed = ( options.random_seed ) solver.parameters.log_search_progress = ( options.log_search_progress ) status = solver.Solve(model) return self._decode( solver, status, problem, variables, ) 31. Status Mapping این بخش باید بسیار سخت‌گیرانه باشد. def map_status( status: cp_model.CpSolverStatus, ) -> SolverResultStatus: if status in ( cp_model.OPTIMAL, cp_model.FEASIBLE, ): return SolverResultStatus.FEASIBLE if status == cp_model.INFEASIBLE: return SolverResultStatus.INFEASIBLE if status == cp_model.MODEL_INVALID: return SolverResultStatus.MODEL_INVALID return SolverResultStatus.UNKNOWN به‌خصوص: TIMEOUT نباید تبدیل شود به: INFEASIBLE بلکه: UNKNOWN است، مگر Solver واقعاً INFEASIBLE برگرداند. 32. Decoder بعد از Solve، متغیرها باید به Domain Result تبدیل شوند. class SchedulingResultDecoder: def decode( self, solver: cp_model.CpSolver, status, problem: DetailedSchedulingProblem, variables: SchedulerVariables, ) -> SchedulingResult: mapped_status = map_status(status) if mapped_status != SolverResultStatus.FEASIBLE: return SchedulingResult( status=mapped_status, station_calls=(), block_movements=(), objective_value=None, metrics=self._metrics(solver), diagnostics=(), ) station_calls = [] block_movements = [] for train in problem.trains: for call in train.station_calls: key = (train.id, call.station_id) arrival = solver.Value( variables.arrival[key] ) departure = solver.Value( variables.departure[key] ) track_id = self._assigned_track( solver, variables, train.id, call.station_id, ) station_calls.append( ScheduledStationCall( train_id=train.id, station_id=call.station_id, sequence=call.sequence, arrival=arrival, departure=departure, assigned_track_id=track_id, ) ) for movement in train.block_movements: key = (train.id, movement.block_id) block_movements.append( ScheduledBlockMovement( train_id=train.id, block_id=movement.block_id, sequence=movement.sequence, entry=solver.Value( variables.entry[key] ), exit=solver.Value( variables.exit[key] ), clear=solver.Value( variables.clear[key] ), ) ) return SchedulingResult( status=mapped_status, station_calls=tuple(station_calls), block_movements=tuple(block_movements), objective_value=solver.ObjectiveValue(), metrics=self._metrics(solver), diagnostics=(), ) 33. Hints / Warm Start Scheduler می‌تواند از موارد زیر Hint بگیرد: 1. Source Schedule 2. Previous Scenario Schedule 3. Previous Network Iteration 4. Aggregate Allocation اما فقط اگر: same PlanningSnapshot AND compatible ModelVersion AND compatible train identities AND compatible resource topology باشد. نمونه: class HintProvider: def apply( self, model: cp_model.CpModel, variables: SchedulerVariables, hints, ) -> None: for key, value in hints.arrivals.items(): variable = variables.arrival.get(key) if variable is not None: model.AddHint( variable, value, ) Warm Start یک performance mechanism است، نه correctness mechanism. 34. Independent Validator بعد از Scheduler: CP-SAT Result ↓ Independent Validator نه: CP-SAT says feasible ↓ Capacity = proven Validator باید مستقل بررسی کند: Station precedence Running time Dwell Block occupancy Single-track conflicts Double-track direction Headway Opposing trains Switch Clearing Station track Station length Junction Operational windows Boundary conditions این Validator در مرحله: V2.6-F به‌صورت کامل پیاده‌سازی می‌شود. 35. Capacity Search خارج Scheduler Scheduler فقط می‌گوید: Can I schedule F trains? Capacity Search مسئول این است: F = 1 → FEASIBLE F = 2 → FEASIBLE F = 3 → FEASIBLE F = 4 → ? ساختار: class CapacitySearch: def evaluate( self, candidate_train_count: int, ) -> CapacityCheckResult: problem = self.problem_factory.build( train_count=candidate_train_count ) result = self.scheduler.solve( problem, self.options, ) return self.interpret(result) اما Proof نهایی در: Capacity Proof Engine انجام می‌شود. 36. F / F+1 برای اثبات: Capacity = F باید: F → FEASIBLE F + 1 → INFEASIBLE و هر دو با: Independent Validation بررسی شوند. اگر: F + 1 → UNKNOWN باشد: Capacity Proven = False است. 37. Diagnostic Model Scheduler باید اطلاعات کافی برای Performance و Debug تولید کند. @dataclass(frozen=True) class ModelDiagnostics: train_count: int station_count: int block_count: int int_variable_count: int boolean_variable_count: int interval_count: int constraint_count: int conflict_candidate_count: int compile_time_ms: int solve_time_ms: int این اطلاعات بعداً برای V2.5-O و Performance Tuning بسیار مهم خواهند بود. 38. Error Taxonomy خطاهای Scheduler باید قابل تفکیک باشند: class SchedulerError(Exception): pass class InvalidSchedulingProblem(SchedulerError): pass class MissingInfrastructure(SchedulerError): pass class MissingTrainProfile(SchedulerError): pass class UnmappedPath(SchedulerError): pass class UnsupportedTrackType(SchedulerError): pass class TimeHorizonError(SchedulerError): pass class ModelBuildError(SchedulerError): pass class SolverFailure(SchedulerError): pass 39. مواردی که باید قبل از CP-SAT Fail Fast شوند اگر: TrainRun بدون Path باشد: UnmappedPath اگر: Block بدون TrackType باشد: MissingInfrastructure اگر: Train Type بدون Running Time Profile باشد: MissingTrainProfile اگر: هیچ Station Track مناسبی برای Train وجود ندارد باشد: InvalidSchedulingProblem این موارد نباید بی‌دلیل وارد CP-SAT شوند. 40. Golden Test برای Golden Dataset پروژه: GAR → SAKHEH با داده تستی: Track Type = SINGLE Headway = 5 min Switch = 8 min Clearing = 2 min Train Length = 650 m Train Weight = 900 t Formation = 10 wagons Wagon Capacity = 100 t Demand = 3000 t Freight/Train = 1000 t این مقادیر صرفاً Golden Test Data هستند و نباید به‌عنوان پارامتر واقعی شبکه ایران تلقی شوند. 41. Golden Test — سه قطار def test_three_trains_are_schedulable( golden_problem_factory, scheduler, ): problem = golden_problem_factory.build( train_count=3 ) result = scheduler.solve( problem, SolverOptions( time_limit_seconds=30, num_workers=4, random_seed=42, ), ) assert result.status == SolverResultStatus.FEASIBLE بعداً در V2.6-F: validator.validate(result) نیز باید VALID شود. 42. Golden Test — چهار قطار از نظر Demand: 4 × 1000 = 4000 t در حالی که: Demand = 3000 t بنابراین Aggregate Model باید: F = 4 را رد کند. این رد شدن لزوماً نباید توسط Detailed Scheduler اتفاق بیفتد، چون Demand Constraint متعلق به Aggregate/Network Layer است. این تفکیک معماری باید حفظ شود. 43. سناریوی مهم Single Track فرض: Train 1: A → B 08:00 Train 2: B → A 08:10 Scheduler نباید صرفاً بر اساس Departure Time تصمیم بگیرد. باید بررسی کند: Entry(Block) Exit(Block) Clear(Block) Switch بنابراین ممکن است Train 2 مجبور شود منتظر بماند: Train 1 08:00 entry 08:17 exit 08:19 clear Switch = 8 Train 2 08:27 earliest entry این دقیقاً تفاوت بین: Nominal Capacity و: Operationally Realizable Capacity است. 44. Regime در آینده Scheduler می‌تواند با Regimeهای مختلف اجرا شود: class OperatingRegime(str, Enum): STRICT_ALTERNATING = "STRICT_ALTERNATING" DIRECTIONAL_BATCH = "DIRECTIONAL_BATCH" MIXED = "MIXED" اما بهتر است Regime را به‌عنوان Constraint/Policy ورودی بدهیم، نه اینکه داخل CP-SAT به‌صورت Hardcoded نوشته شود. مثلاً: Scenario A: STRICT_ALTERNATING Scenario B: DIRECTIONAL_BATCH Scenario C: MIXED سپس هر سه به Scheduler داده می‌شوند. این موضوع بعداً برای تحلیل Hidden Capacity مهم خواهد بود. 45. مرز Scheduler و Aggregate Aggregate می‌گوید: Route R Train Type T Direction F Train Count = 12 Scheduler می‌گوید: Train 001: A 08:00 B 08:17 C 08:42 Train 002: A 08:29 B 08:46 C 09:11 ... پس: Aggregate = HOW MUCH Detailed = EXACTLY WHEN 46. مرز Scheduler و Rolling Stock Scheduler نباید خودش تصمیم بگیرد: Which wagon? Which locomotive? این کار متعلق به Rolling Stock layer است. Scheduler فقط باید Profile معتبر دریافت کند: Train Length Train Weight Train Type Load State Brake Capability Speed Profile Formation Feasibility در معماری کامل: Formation Engine ↓ Wagon/Loco Feasibility ↓ TrainRun ↓ Detailed Scheduler 47. Run Snapshot Scheduler باید فقط با Snapshot کار کند: @dataclass(frozen=True) class PlanningSnapshot: snapshot_id: str infrastructure_version_id: str data_version_id: str scenario_id: str trains: tuple[TrainRun, ...] created_at: str در Production، Snapshot باید Immutable باشد. یعنی اگر Database هنگام Solve تغییر کرد: Scheduler نباید وسط حل، داده جدید بگیرد. 48. Reproducibility برای هر Solve ثبت شود: run_id snapshot_id data_version_id infrastructure_version_id model_version solver_configuration_hash random_seed solver_version objective_mode بنابراین بتوانیم بعداً بگوییم: Why did Run-20260928-001 produce this schedule? 49. Production Pipeline نسخه فعلی: Access / Excel ↓ Raw Source ↓ Staging ↓ Mapping ↓ Quality ↓ Canonical Model ↓ Planning Snapshot ↓ Aggregate Allocation ↓ Train Formation ↓ Wagon Feasibility ↓ Locomotive Feasibility ↓ DetailedSchedulingProblem ↓ Problem Compiler ↓ CP-SAT ↓ SchedulingResult ↓ Independent Validator ↓ Capacity Proof ↓ Bottleneck ↓ Capacity Offer 50. چیزی که در این مرحله عمداً انجام نمی‌دهیم در V2.6-E نباید این موارد را داخل Scheduler مخلوط کنیم: Database Queries Marketplace Logic Capacity Offer Wagon Allocation Demand Optimization Capacity Proof Bottleneck Ranking Source Mapping Excel Parsing Access Parsing هرکدام Layer مستقل خود را دارند. 51. Definition of Done — V2.6-E V2.6-E زمانی Done است که: [✓] DetailedSchedulingProblem تعریف شده باشد [✓] TimeHorizon پیاده شده باشد [✓] TrainRun/StationCall/BlockMovement مدل شده باشد [✓] Time Variables ساخته شوند [✓] Precedence فعال باشد [✓] Running Time فعال باشد [✓] Dwell فعال باشد [✓] Block ↔ Station linkage فعال باشد [✓] Clearing فعال باشد [✓] Single Track Conflict فعال باشد [✓] Same Direction Headway فعال باشد [✓] Opposing Direction فعال باشد [✓] Switch فعال باشد [✓] Double Track Direction Resource فعال باشد [✓] Station Track Assignment فعال باشد [✓] Station Length فعال باشد [✓] Junction Conflict Matrix فعال باشد [✓] Operational Window فعال باشد [✓] Earliest/Latest فعال باشد [✓] Objective Modes تعریف شده باشد [✓] Solver Status Mapping صحیح باشد [✓] UNKNOWN ≠ INFEASIBLE باشد [✓] Decoder فعال باشد [✓] Diagnostics فعال باشد [✓] Snapshot-based execution فعال باشد [✓] Golden Test ساخته شده باشد [✓] Independent Validator هنوز جدا باشد 52. معماری نهایی این مرحله PlanningSnapshot │ ▼ DetailedSchedulingProblem │ ▼ ┌───────────────┐ │ Problem │ │ Compiler │ └───────┬───────┘ │ ┌──────────┼──────────┐ ▼ ▼ ▼ Variables Conflicts Objective │ │ │ └──────────┼──────────┘ ▼ CP-SAT Model │ ▼ Solver │ ┌──────────┴──────────┐ ▼ ▼ FEASIBLE INFEASIBLE │ ▼ Decoder │ ▼ SchedulingResult │ ▼ Independent Validator │ ▼ Capacity Proof Engine اصل کلیدی: CP-SAT ≠ Capacity Proof و: CP-SAT FEASIBLE ≠ Validated Schedule و: Validated Schedule ≠ Proven Capacity زنجیره‌ی صحیح Production این است: Feasible ↓ Validated ↓ F+1 Infeasible ↓ Proven Capacity 53. مرحله بعد پس از تکمیل این هسته، مرحله بعدی دقیقاً: V2.6-F — Independent Production Validator است. در V2.6-F، همین SchedulingResult را بدون اعتماد به CP-SAT دوباره از صفر بررسی می‌کنیم؛ شامل Block Occupancy, Headway, Opposing Direction, Station Track, Junction, Operational Window, Precedence, Running Time, Clearing و تولید ValidationEvidence قابل ذخیره در PostgreSQL. این مرحله حالا اسکلت واقعی Scheduler را مشخص کرد. قدم بعدی، V2.6-F — Independent Validator است؛ و آنجا عملاً اولین حلقه‌ی مهم «Solver → کنترل مستقل → Evidence» را کامل می‌کنیم. --- ## ادامه 92 — پرسش کاربر (`message-id:4cdd50c6-b489-47e6-9d48-c8cb131a34d3`) بله دقیقا --- ## ادامه 92 — پاسخ دستیار (`message-id:bc0e92a5-a754-4205-9725-1a790ec168fd`) عالی. حالا دقیقاً می‌رویم سراغ V2.6-F — Independent Production Validator. این مرحله برای پروژه ما حیاتی است، چون قرار نیست به خروجی CP-SAT صرفاً به این دلیل که Solver گفته FEASIBLE اعتماد کنیم. در این مرحله Validator را کاملاً مستقل از CP-SAT طراحی و پیاده‌سازی می‌کنیم. --- ## ادامه 93 — پرسش کاربر (`message-id:ddab5ece-cdd0-47a6-b429-07f051b03b8a`) بله ادامه بده لطفا --- ## ادامه 93 — پاسخ دستیار (`message-id:4beedb7b-fdb4-480b-b46d-024c6e8e6660`) حتماً. حالا V2.6-F را به‌صورت اجرایی می‌سازیم؛ یعنی Validator مستقیماً SchedulingResult را می‌گیرد، آن را بدون استفاده از CP-SAT دوباره بررسی می‌کند و برای هر constraint، Evidence قابل ذخیره و audit تولید می‌کند. V2.6-F — Independent Production Validator V2.6-F — Independent Production Validator 1. هدف هدف این مرحله ساخت یک Validator مستقل است که خروجی: Detailed CP-SAT Scheduler ↓ SchedulingResult را دریافت کند و بدون اتکا به Solver، بررسی کند که Schedule واقعاً با قواعد عملیاتی سازگار است یا خیر. معماری: CP-SAT │ ▼ SchedulingResult │ ▼ Independent Validator │ ├── Precedence ├── Running Time ├── Dwell ├── Block Occupancy ├── Headway ├── Opposing Direction ├── Switch ├── Clearing ├── Station Track ├── Station Length ├── Junction ├── Operational Window └── Boundary │ ▼ ValidationResult │ ├── VALID ├── INVALID └── WARNINGS اصل اساسی: Scheduler proposes. Validator verifies. Proof Engine proves. 2. اصل استقلال Validator نباید این کار را انجام دهد: if solver_status == FEASIBLE: return VALID این کاملاً ممنوع است. همچنین Validator نباید مدل CP-SAT را دوباره اجرا کند. باید داده خروجی را به‌صورت Domain Object دریافت و محاسبات مستقل انجام دهد. یعنی: CP-SAT Model X SchedulingResult ↓ Independent Rules 3. Package Structure src/ └── railway/ └── validation/ └── scheduling/ ├── __init__.py ├── model.py ├── validator.py ├── context.py ├── evidence.py ├── severity.py ├── rules/ │ ├── __init__.py │ ├── precedence.py │ ├── running_time.py │ ├── dwell.py │ ├── blocks.py │ ├── headway.py │ ├── opposing.py │ ├── clearing.py │ ├── stations.py │ ├── junctions.py │ ├── windows.py │ └── boundaries.py │ └── indexes.py Validator به Scheduler package وابستگی اجرایی ندارد. فقط از Contractهای مشترک Domain استفاده می‌کند. 4. Validation Severity from enum import Enum class Severity(str, Enum): INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL" تفاوت مهم: WARNING ≠ ERROR مثلاً اختلاف جزئی با Baseline می‌تواند Warning باشد. ولی: Two trains occupy same single-track block باید: CRITICAL باشد. 5. Validation Status class ValidationStatus(str, Enum): VALID = "VALID" VALID_WITH_WARNINGS = "VALID_WITH_WARNINGS" INVALID = "INVALID" NOT_VALIDATED = "NOT_VALIDATED" اگر حتی یک Hard Constraint نقض شود: INVALID است. 6. Validation Evidence هر Rule باید Evidence تولید کند. from dataclasses import dataclass from typing import Any @dataclass(frozen=True) class ValidationEvidence: rule_code: str severity: Severity passed: bool message: str entity_type: str entity_id: str actual: Any = None expected: Any = None train_id: str | None = None station_id: str | None = None block_id: str | None = None metadata: dict[str, Any] | None = None مثلاً: rule_code: SINGLE_TRACK_OVERLAP passed: False actual: Train A 08:00–08:20 Train B 08:15–08:35 expected: Non-overlapping occupancy این Evidence بعداً مستقیماً در PostgreSQL ذخیره می‌شود. 7. Validation Result @dataclass(frozen=True) class ValidationResult: status: ValidationStatus evidence: tuple[ValidationEvidence, ...] error_count: int warning_count: int checked_rule_count: int validator_version: str 8. Validation Context Validator باید علاوه بر Schedule، Context اصلی را هم داشته باشد. @dataclass(frozen=True) class ValidationContext: problem: DetailedSchedulingProblem result: SchedulingResult validator_version: str به این ترتیب Validator می‌تواند: Expected vs Actual را مقایسه کند. 9. Rule Interface هر Rule مستقل: from typing import Protocol class ValidationRule(Protocol): code: str def validate( self, context: ValidationContext, ) -> tuple[ValidationEvidence, ...]: ... این معماری اجازه می‌دهد بعداً Rule جدید اضافه کنیم بدون اینکه Validator مرکزی بزرگ و غیرقابل نگهداری شود. 10. Rule Registry class RuleRegistry: def __init__( self, rules: tuple[ValidationRule, ...], ) -> None: self.rules = rules def all(self) -> tuple[ValidationRule, ...]: return self.rules در Production: registry = RuleRegistry( rules=( PrecedenceRule(), RunningTimeRule(), DwellRule(), ClearingRule(), BlockOccupancyRule(), HeadwayRule(), OpposingDirectionRule(), StationTrackRule(), StationLengthRule(), JunctionConflictRule(), OperationalWindowRule(), BoundaryRule(), ) ) 11. Validator مرکزی class IndependentScheduleValidator: def __init__( self, registry: RuleRegistry, version: str, ) -> None: self.registry = registry self.version = version def validate( self, context: ValidationContext, ) -> ValidationResult: evidence: list[ValidationEvidence] = [] for rule in self.registry.all(): evidence.extend( rule.validate(context) ) error_count = sum( 1 for e in evidence if not e.passed and e.severity in ( Severity.ERROR, Severity.CRITICAL, ) ) warning_count = sum( 1 for e in evidence if not e.passed and e.severity == Severity.WARNING ) if error_count > 0: status = ValidationStatus.INVALID elif warning_count > 0: status = ValidationStatus.VALID_WITH_WARNINGS else: status = ValidationStatus.VALID return ValidationResult( status=status, evidence=tuple(evidence), error_count=error_count, warning_count=warning_count, checked_rule_count=len( self.registry.all() ), validator_version=self.version, ) 12. اولین اصل مهم Validator Validator باید حتی Schedule ناقص را نیز کنترل کند. مثلاً اگر: Train A در Result وجود دارد ولی: BlockMovement B1 ندارد، نباید Validator آن را نادیده بگیرد. باید: MISSING_SCHEDULE_MOVEMENT تولید شود. 13. Completeness Rule class ScheduleCompletenessRule: code = "SCHEDULE_COMPLETENESS" def validate(self, context): evidence = [] expected_blocks = { (train.id, movement.block_id) for train in context.problem.trains for movement in train.block_movements } actual_blocks = { (m.train_id, m.block_id) for m in context.result.block_movements } missing = expected_blocks - actual_blocks if missing: for train_id, block_id in missing: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message=( "Scheduled block movement is missing" ), entity_type="TrainBlockMovement", entity_id=f"{train_id}:{block_id}", train_id=train_id, block_id=block_id, ) ) return tuple(evidence) این Rule باید قبل از Rules وابسته به زمان اجرا شود. 14. Precedence Validation فرض: A → B → C Validator باید Schedule را sort کند و بررسی کند: departure(A) <= arrival(B) departure(B) <= arrival(C) class PrecedenceRule: code = "STATION_PRECEDENCE" def validate(self, context): evidence = [] for train in context.problem.trains: calls = sorted( ( c for c in context.result.station_calls if c.train_id == train.id ), key=lambda x: x.sequence, ) for previous, current in zip( calls, calls[1:], ): if current.arrival < previous.departure: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Station precedence violated", entity_type="TrainRun", entity_id=train.id, train_id=train.id, actual={ "previous_departure": previous.departure, "current_arrival": current.arrival, }, expected=( "current_arrival >= " "previous_departure" ), ) ) return tuple(evidence) 15. Running Time Validation برای هر Block: exit - entry >= expected_running_time class RunningTimeRule: code = "BLOCK_RUNNING_TIME" def validate(self, context): evidence = [] actual_by_key = { (m.train_id, m.block_id): m for m in context.result.block_movements } for train in context.problem.trains: for movement in train.block_movements: key = ( train.id, movement.block_id, ) actual = actual_by_key.get(key) if actual is None: continue actual_time = ( actual.exit - actual.entry ) expected = movement.running_time if actual_time < expected: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Running time is below minimum", entity_type="BlockMovement", entity_id=f"{train.id}:{movement.block_id}", train_id=train.id, block_id=movement.block_id, actual=actual_time, expected=expected, ) ) return tuple(evidence) 16. Dwell Validation class DwellRule: code = "STATION_DWELL" def validate(self, context): evidence = [] actual_by_key = { (x.train_id, x.station_id): x for x in context.result.station_calls } for train in context.problem.trains: for call in train.station_calls: key = ( train.id, call.station_id, ) actual = actual_by_key.get(key) if actual is None: continue dwell = ( actual.departure - actual.arrival ) if dwell < call.minimum_dwell: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Minimum dwell violated", entity_type="StationCall", entity_id=f"{train.id}:{call.station_id}", train_id=train.id, station_id=call.station_id, actual=dwell, expected=call.minimum_dwell, ) ) return tuple(evidence) 17. Clearing Validation قاعده: clear >= exit + clearing_time class ClearingRule: code = "BLOCK_CLEARING" def validate(self, context): evidence = [] actual_by_key = { (x.train_id, x.block_id): x for x in context.result.block_movements } for train in context.problem.trains: for movement in train.block_movements: actual = actual_by_key[ (train.id, movement.block_id) ] expected_clear = ( actual.exit + movement.clearing_time ) if actual.clear < expected_clear: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Block clearing violation", entity_type="BlockMovement", entity_id=f"{train.id}:{movement.block_id}", train_id=train.id, block_id=movement.block_id, actual=actual.clear, expected=expected_clear, ) ) return tuple(evidence) 18. Block Occupancy برای هر Resource باید Occupancy Interval بسازیم: [entry, clear] نه: [entry, exit] چون Block تا زمان Clearing آزاد نشده است. @dataclass(frozen=True) class OccupancyInterval: train_id: str block_id: str start: int end: int direction: Direction track_type: TrackType 19. Overlap Function def overlaps( a_start: int, a_end: int, b_start: int, b_end: int, ) -> bool: return ( a_start < b_end and b_start < a_end ) این تعریف برای intervalهای نیمه‌باز: [start, end) مناسب است. 20. Single Track Occupancy برای Single Track: same physical block + different trains نباید overlap داشته باشند. class BlockOccupancyRule: code = "SINGLE_TRACK_OVERLAP" def validate(self, context): evidence = [] movements = [ m for m in context.result.block_movements if self._track_type( context, m.block_id ) == TrackType.SINGLE ] grouped = self._group_by_block( movements ) for block_id, items in grouped.items(): items = sorted( items, key=lambda x: x.entry, ) for a, b in zip(items, items[1:]): if overlaps( a.entry, a.clear, b.entry, b.clear, ): evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message=( "Two trains overlap on " "single-track block" ), entity_type="PhysicalBlock", entity_id=block_id, block_id=block_id, metadata={ "train_a": a.train_id, "train_b": b.train_id, "a_interval": ( a.entry, a.clear, ), "b_interval": ( b.entry, b.clear, ), }, ) ) return tuple(evidence) 21. چرا فقط Overlap کافی نیست؟ چون این دو حالت متفاوت‌اند: حالت اول A → B → قاعده: Headway حالت دوم A → B ← قاعده: Opposing / Switch بنابراین Validator باید Direction را نیز بررسی کند. 22. Same Direction Headway اگر: A → Block B → Block باشد: entry(B) >= clear(A) + headway class HeadwayRule: code = "SAME_DIRECTION_HEADWAY" def validate(self, context): evidence = [] for block_id, movements in self._group_by_block( context.result.block_movements ).items(): movements = sorted( movements, key=lambda x: x.entry, ) for a, b in zip( movements, movements[1:], ): if a.direction != b.direction: continue movement_def = self._find_definition( context, b.train_id, block_id, ) required = ( movement_def.headway_same_direction ) actual = b.entry - a.clear if actual < required: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Same-direction headway violated", entity_type="PhysicalBlock", entity_id=block_id, block_id=block_id, actual=actual, expected=required, metadata={ "leading_train": a.train_id, "following_train": b.train_id, }, ) ) return tuple(evidence) 23. Opposing Direction برای: A → B ← باید فاصله لازم را بررسی کنیم. اگر A اول باشد: entry(B) >= clear(A) + switch اگر B اول باشد: entry(A) >= clear(B) + switch Validator نباید فرض کند کدام قطار اول بوده است. باید از Schedule واقعی استخراج کند. 24. Opposing Direction Rule class OpposingDirectionRule: code = "OPPOSING_DIRECTION_SEPARATION" def validate(self, context): evidence = [] grouped = self._single_track_groups( context ) for block_id, movements in grouped.items(): movements = sorted( movements, key=lambda x: x.entry, ) for a, b in zip( movements, movements[1:], ): if a.direction == b.direction: continue definition = self._definition( context, a, ) required = max( a.clear, a.clear + definition.switch_time, ) actual = b.entry expected = ( a.clear + definition.switch_time ) if actual < expected: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message=( "Opposing-direction " "separation violated" ), entity_type="PhysicalBlock", entity_id=block_id, block_id=block_id, actual=actual - a.clear, expected=definition.switch_time, metadata={ "first_train": a.train_id, "second_train": b.train_id, }, ) ) return tuple(evidence) در نسخه نهایی باید switch_time و clearing_time دقیقاً مطابق Rule Configuration شبکه ترکیب شوند و نباید با max() صرفاً به‌صورت فرضی جایگزین semantics واقعی شوند. 25. Station Track Validation Validator باید بررسی کند: assigned_track_id exists و: track.station_id == station_id و: train.length <= track.usable_length و همچنین occupancy Trackها overlap نداشته باشد. 26. Station Length class StationLengthRule: code = "STATION_TRACK_LENGTH" def validate(self, context): evidence = [] tracks = { track.id: track for track in context.problem.station_tracks } trains = { train.id: train for train in context.problem.trains } for call in context.result.station_calls: if call.assigned_track_id is None: continue track = tracks.get( call.assigned_track_id ) train = trains[call.train_id] if track is None: continue if train.length_m > track.usable_length_m: evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Train exceeds station track length", entity_type="StationTrack", entity_id=track.id, train_id=train.id, station_id=call.station_id, actual=train.length_m, expected=track.usable_length_m, ) ) return tuple(evidence) 27. Station Track Overlap اگر دو Train در یک Track باشند: Train A 08:00–08:30 Train B 08:20–08:50 باید: INVALID شود. Validator این را با interval intersection مستقل از CP-SAT بررسی می‌کند. 28. Junction Validation Junction Rule باید Conflict Matrix را بخواند. مثلاً: M1 conflicts M2 M1 conflicts M4 M2 does not conflict M4 اگر: M1: 08:10–08:15 M2: 08:13–08:18 باشد: INVALID اما اگر M4 باشد و در Matrix conflict تعریف نشده باشد: VALID پس: Junction ≠ Global NoOverlap 29. Operational Window فرض: Window: 08:00–08:20 و Train Movement: 08:10–08:25 اگر Resource موردنظر مشمول restriction باشد: INVALID Rule: class OperationalWindowRule: code = "OPERATIONAL_WINDOW" def validate(self, context): evidence = [] for window in context.problem.operational_windows: for movement in self._affected_movements( context, window, ): if overlaps( movement.entry, movement.clear, window.start_minute, window.end_minute, ): evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message=( "Movement overlaps " "operational restriction" ), entity_type="OperationalWindow", entity_id=window.id, train_id=movement.train_id, metadata={ "movement": ( movement.entry, movement.clear, ), "window": ( window.start_minute, window.end_minute, ), }, ) ) return tuple(evidence) 30. Boundary Validation هیچ Schedule Time نباید خارج از Horizon باشد. class BoundaryRule: code = "TIME_HORIZON" def validate(self, context): evidence = [] start = context.problem.horizon.start_minute end = context.problem.horizon.end_minute for call in context.result.station_calls: if not ( start <= call.arrival <= end ): evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Arrival outside time horizon", entity_type="StationCall", entity_id=( f"{call.train_id}:" f"{call.station_id}" ), actual=call.arrival, expected=(start, end), ) ) if not ( start <= call.departure <= end ): evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Departure outside time horizon", entity_type="StationCall", entity_id=( f"{call.train_id}:" f"{call.station_id}" ), actual=call.departure, expected=(start, end), ) ) return tuple(evidence) 31. Earliest / Latest Validation حتی اگر CP-SAT Bound داشته باشد، Validator دوباره بررسی می‌کند. class TimeWindowRule: code = "TIME_WINDOW" def validate(self, context): evidence = [] definitions = { (train.id, call.station_id): call for train in context.problem.trains for call in train.station_calls } for actual in context.result.station_calls: definition = definitions[ ( actual.train_id, actual.station_id, ) ] if not ( definition.arrival_earliest <= actual.arrival <= definition.arrival_latest ): evidence.append( ValidationEvidence( rule_code=self.code, severity=Severity.CRITICAL, passed=False, message="Arrival outside allowed window", entity_type="StationCall", entity_id=( f"{actual.train_id}:" f"{actual.station_id}" ), actual=actual.arrival, expected=( definition.arrival_earliest, definition.arrival_latest, ), ) ) return tuple(evidence) Departure نیز به همین شکل بررسی می‌شود. 32. Validator باید Positive Evidence هم تولید کند فقط خطا کافی نیست. مثلاً: BLOCK_RUNNING_TIME PASSED actual = 17 required = 17 به این دلیل مهم است که بعداً در: Capacity Proof بدانیم دقیقاً چه چیزی بررسی شده است. مثلاً: Proof Evidence: - 27 trains checked - 54 block movements checked - 18 single-track conflicts checked - 12 station assignments checked - 8 junction conflicts checked - 0 violations 33. Evidence ID هر Evidence باید شناسه پایدار داشته باشد. import hashlib def evidence_id( rule_code: str, entity_id: str, actual: object, expected: object, ) -> str: raw = ( f"{rule_code}|" f"{entity_id}|" f"{actual}|" f"{expected}" ) return hashlib.sha256( raw.encode("utf-8") ).hexdigest() در Database می‌توان از این برای Idempotency استفاده کرد. 34. Validation در PostgreSQL ساختار پیشنهادی: result.validation_run result.validation_evidence result.validation_summary مثلاً: validation_run --------------- id run_id schedule_id validator_version status started_at completed_at و: validation_evidence ------------------- id validation_run_id rule_code severity passed entity_type entity_id train_id station_id block_id actual_json expected_json metadata_json created_at 35. چرا Evidence جدا از Bottleneck است؟ چون: Validation Evidence می‌گوید: آیا Constraint رعایت شده است؟ اما: Bottleneck Evidence می‌گوید: چه Resourceای ظرفیت را محدود کرده است؟ مثلاً: Validation: Single Track conflict = 0 Bottleneck: Single Track utilization = 96% این دو مفهوم نباید مخلوط شوند. 36. Validation Pipeline SchedulingResult ↓ Completeness ↓ Time Boundary ↓ Station Timeline ↓ Block Timeline ↓ Precedence ↓ Running Time ↓ Dwell ↓ Clearing ↓ Single Track ↓ Headway ↓ Opposing Direction ↓ Station Track ↓ Station Length ↓ Junction ↓ Operational Window ↓ ValidationResult 37. Validation Short-Circuit دو حالت داریم. Production Audit Mode همه Ruleها اجرا شوند: VALIDATOR_MODE = FULL تا تمام خطاها مشخص شوند. Fast Reject Mode به محض Hard Failure می‌توان متوقف شد: VALIDATOR_MODE = FAIL_FAST اما برای Capacity Proof بهتر است: FULL استفاده شود تا Evidence کامل داشته باشیم. 38. Rule Independence هر Rule باید تا حد امکان pure باشد: result = rule.validate(context) بدون: Database HTTP CP-SAT Global mutable state این موضوع تست‌پذیری را بسیار بالا می‌برد. 39. Golden Validation Test برای Golden: GAR → SAKHEH 3 trains انتظار: SchedulingResult ↓ Validator ↓ VALID مثلاً: def test_golden_three_train_schedule_is_valid(): result = scheduler.solve( golden_problem_factory.build( train_count=3 ), options, ) validation = validator.validate( ValidationContext( problem=problem, result=result, validator_version="2.6.F", ) ) assert validation.status in ( ValidationStatus.VALID, ValidationStatus.VALID_WITH_WARNINGS, ) 40. Negative Golden Test باید عمداً Schedule خراب بسازیم. مثلاً: Train A: Block B1 08:00–08:20 Train B: Block B1 08:15–08:35 برای Single Track: Expected: INVALID و باید Evidence داشته باشیم: SINGLE_TRACK_OVERLAP CRITICAL passed=False 41. Test برای Headway فرض: A clear = 100 B entry = 103 headway = 5 باید: INVALID باشد. اما: B entry = 105 باید: VALID باشد. 42. Test برای Opposing فرض: A clear = 100 switch = 8 B entry = 107 باید: INVALID باشد. ولی: B entry = 108 باید حداقل از نظر این Rule: VALID باشد. البته اگر clearing, headway یا Rule دیگری Separation بیشتری ایجاد کند، همان مقدار واقعی باید ملاک باشد. 43. Test برای Double Track دو قطار: A → Block B ← Block در Double Track. اگر فقط Block Resource directional باشد: Block Occupancy نباید آنها را Conflict تلقی کند. ولی اگر: Junction مشترک داشته باشند، Junction Rule باید آن را بررسی کند. 44. Midnight Test مثلاً: Train: Station A departure = 1435 Station B arrival = 1455 که معادل: 23:55 00:15 روز بعد است. Validator باید: 1455 > 1435 را ببیند. نه: 15 < 55 45. Source Schedule Comparison در این مرحله Source Schedule فقط Baseline است. مثلاً: Source: 08:00 Actual: 08:07 اگر Rule اجازه دهد: Validation = VALID ولی: Schedule Deviation = +7 min ثبت می‌شود. این deviation نباید خودکار تبدیل به Hard Failure شود مگر Scenario چنین Constraintی تعریف کرده باشد. 46. Validation Report خروجی مناسب UI/API: { "status": "VALID_WITH_WARNINGS", "checked_rules": 12, "errors": 0, "warnings": 2, "evidence": { "passed": 184, "failed": 2 } } و برای هر Failure: { "rule_code": "SINGLE_TRACK_OVERLAP", "severity": "CRITICAL", "train_a": "TR-101", "train_b": "TR-102", "block": "B-07", "interval_a": [480, 502], "interval_b": [495, 518] } 47. اتصال به Capacity Proof بعداً Proof Engine می‌تواند چنین شرطی داشته باشد: if ( scheduling.status == FEASIBLE and validation.status in ( VALID, VALID_WITH_WARNINGS, ) ): lower_bound = F اما: lower_bound = F هنوز به معنی: PROVEN CAPACITY = F نیست. برای Proof باید: F feasible + validated + F+1 infeasible داشته باشیم. 48. وضعیت نهایی Capacity پس: Scheduler FEASIBLE ↓ Validator VALID ↓ Capacity Proof F+1 = INFEASIBLE ↓ PROVEN این زنجیره باید در کل سیستم immutable و قابل audit باشد. 49. Performance Validator نباید O(N²) بی‌دلیل داشته باشد. برای Blockها: group_by_block() sort_by_entry() و سپس sweep: A ↓ B ↓ C ↓ D به جای مقایسه همه جفت‌ها. برای Station Track نیز همین روش. برای Junction فقط Conflict Matrixهای واقعی. 50. Indexing قبل از Validation: @dataclass class ValidationIndexes: trains: dict[str, TrainRun] station_calls: dict[ tuple[str, str], ScheduledStationCall, ] block_movements: dict[ tuple[str, str], ScheduledBlockMovement, ] tracks: dict[str, StationTrack] ساخت Index یک بار انجام می‌شود. این کار در شبکه‌های بزرگ بسیار مهم است. 51. Production Orchestrator Run Manager: schedule_result = detailed_scheduler.solve( problem, options, ) if schedule_result.status == SolverResultStatus.FEASIBLE: validation_result = validator.validate( ValidationContext( problem=problem, result=schedule_result, validator_version=VALIDATOR_VERSION, ) ) else: validation_result = None سپس: FEASIBLE + VALID می‌تواند وارد Proof شود. ولی: FEASIBLE + INVALID نباید وارد Proof شود. 52. Failure Classification مثلاً: CP-SAT: FEASIBLE Validator: INVALID Rule: SINGLE_TRACK_OVERLAP Result: INVALID_RESULT و نه: CAPACITY_INFEASIBLE این تفاوت بسیار مهم است. چون مشکل ممکن است از: Model Bug Decoder Bug Data Bug Validator Bug باشد. 53. Validation Evidence برای Debug اگر Validator یک Schedule را رد کرد: Run ID ↓ Schedule ID ↓ Train ID ↓ Block ↓ Rule ↓ Actual ↓ Expected ↓ Evidence باید قابل Trace باشد. مثلاً: RUN-001 Schedule-003 Train-101 Block-B07 SINGLE_TRACK_OVERLAP این همان چیزی است که در Production برای Debugging حیاتی خواهد بود. 54. عدم اصلاح Schedule توسط Validator Validator نباید Schedule را اصلاح کند. این کار ممنوع است: if violation: result.entry += 5 Validator فقط: Detect Explain Record می‌کند. اصلاح باید برگردد به: Scheduler یا: Scenario / Rule Configuration 55. Feedback Loop در معماری Hybrid: Aggregate ↓ Detailed Scheduler ↓ Validator ↓ Conflict Feedback ↓ Aggregate / Allocation ↓ Detailed Scheduler مثلاً: Route R 12 trains allocated Detailed: Only 9 schedulable Feedback: BLOCK-B07 capacity shortfall = 3 سپس Network Solver allocation را اصلاح می‌کند. 56. Conflict Feedback Contract @dataclass(frozen=True) class SchedulingConflictFeedback: run_id: str route_id: str resource_id: str conflict_type: str affected_train_ids: tuple[str, ...] severity: Severity required_reduction: int | None explanation: str این Contract پلی بین: Detailed و: Aggregate/Network است. 57. Definition of Done — V2.6-F [✓] Independent Validator [✓] Validation Context [✓] Validation Evidence [✓] Validation Result [✓] Rule Registry [✓] Completeness Rule [✓] Precedence Rule [✓] Running Time Rule [✓] Dwell Rule [✓] Clearing Rule [✓] Single Track Occupancy [✓] Same Direction Headway [✓] Opposing Direction [✓] Station Track Assignment [✓] Station Length [✓] Station Track Overlap [✓] Junction Conflict Matrix [✓] Operational Window [✓] Time Horizon [✓] Earliest/Latest [✓] Midnight [✓] Positive Evidence [✓] Negative Evidence [✓] Evidence ID [✓] PostgreSQL persistence contract [✓] Golden Positive Test [✓] Golden Negative Test [✓] Performance Indexing [✓] Conflict Feedback [✓] No Schedule Mutation 58. معماری کامل تا این نقطه Access / Excel │ ▼ Ingestion Pipeline │ ▼ Canonical Model │ ▼ Planning Snapshot │ ▼ Aggregate Solver │ ▼ Train Formation │ ┌─────┴─────┐ ▼ ▼ Wagon Loco Check Check │ │ └─────┬─────┘ ▼ Detailed Scheduling Problem │ ▼ Production CP-SAT │ ▼ Scheduling Result │ ▼ Independent Validator │ ┌────────────┴────────────┐ ▼ ▼ VALID INVALID │ │ ▼ ▼ Capacity Proof Feedback / Engine Debugging و مهم‌تر از همه: ┌──────────────┐ │ CP-SAT │ └──────┬───────┘ │ Candidate │ ▼ ┌──────────────┐ │ Validator │ └──────┬───────┘ │ Evidence │ ▼ ┌──────────────┐ │ Proof Engine │ └──────────────┘ بنابراین از اینجا به بعد سیستم ما صرفاً یک Optimizer نیست؛ یک Auditable Railway Capacity Engine است. 59. مرحله بعدی مرحله بعد باید: V2.6-G — Production Capacity Proof Engine باشد. در آن مرحله این سه مفهوم را به‌صورت واقعی به هم متصل می‌کنیم: Lower Bound + Detailed Feasibility + Independent Validation + F+1 Infeasibility ↓ Capacity Proof و خروجی نهایی به شکل: CapacityProfile CapacityEvaluation CapacityProof ProofEvidence Bottleneck Explanation ساخته خواهد شد؛ یعنی اولین جایی که یک Capacity Run می‌تواند با Evidence کامل بگوید ظرفیت Proven / Not Proven است. --- ## ادامه 94 — پرسش کاربر (`message-id:568c972e-f984-489a-9777-e64424f9de9f`) هر دو مورد لطفا --- ## ادامه 94 — پاسخ دستیار (`message-id:fc14dbd3-47ab-4fdd-8a92-1b67656b99bc`) حتماً. هر دو را با هم جلو می‌بریم: V2.6-G — Capacity Proof Engine و بلافاصله V2.6-H — Bottleneck & Explanation Engine. این دو مرحله به هم وابسته‌اند؛ اول باید ثابت کنیم چه ظرفیتی واقعاً قابل اثبات است، بعد توضیح دهیم چرا ظرفیت در آن سطح قرار گرفته است. V2.6-G — Production Capacity Proof Engine V2.6-G — Production Capacity Proof Engine 1. هدف تا اینجا زنجیره به این نقطه رسیده است: Aggregate Allocation ↓ Detailed CP-SAT ↓ SchedulingResult ↓ Independent Validator ↓ ValidationResult اما هنوز نمی‌توانیم بگوییم: Capacity = 30 trains/day زیرا: FEASIBLE ≠ PROVEN وظیفه Capacity Proof Engine این است که برای یک ظرفیت کاندید F بررسی کند: F ↓ Detailed Feasibility ↓ Independent Validation ↓ F + 1 ↓ INFEASIBLE و در صورت برقراری شرایط: PROVEN CAPACITY = F 2. اصل ریاضی تعریف: [ C_r = \max {F \mid Schedule(F)\ is\ feasible} ] اما در Production، Feasible باید حداقل سه مرحله را پشت سر بگذارد: [ F \rightarrow Scheduler(F) \rightarrow Validator(F) ] بنابراین: [ F\ is\ accepted \iff Scheduler(F)=FEASIBLE \land Validator(F)=VALID ] برای Proof: [ Proof(F)=TRUE ] تنها زمانی که: [ F\ is\ feasible\ and\ validated ] و: [ F+1=INFEASIBLE ] باشد. 3. سه مفهوم متفاوت سیستم باید این سه مفهوم را کاملاً جدا نگه دارد: Feasible Scheduler: FEASIBLE Validated Scheduler: FEASIBLE Validator: VALID Proven F: FEASIBLE + VALID F+1: INFEASIBLE پس: FEASIBLE ↓ VALIDATED ↓ PROVEN 4. Proof Status from enum import Enum class ProofStatus(str, Enum): PROVEN = "PROVEN" NOT_PROVEN = "NOT_PROVEN" INFEASIBLE = "INFEASIBLE" UNKNOWN = "UNKNOWN" INVALID_BASE = "INVALID_BASE" INVALID_UPPER_BOUND = "INVALID_UPPER_BOUND" UNKNOWN و INFEASIBLE هرگز نباید یکی تلقی شوند. 5. Capacity Evaluation from dataclasses import dataclass @dataclass(frozen=True) class CapacityEvaluation: run_id: str candidate_f: int scheduler_status: SolverResultStatus validation_status: ValidationStatus accepted: bool schedule_id: str | None explanation: str | None 6. Proof Evidence @dataclass(frozen=True) class CapacityProofEvidence: proof_id: str candidate_f: int next_f: int candidate_status: str candidate_validation: str next_status: str next_validation: str | None lower_bound_established: bool upper_bound_established: bool proven: bool reason_code: str 7. Proof Object @dataclass(frozen=True) class CapacityProof: run_id: str route_id: str | None capacity: int | None status: ProofStatus lower_bound: int | None upper_bound: int | None evaluations: tuple[ CapacityEvaluation, ... ] evidence: tuple[ CapacityProofEvidence, ... ] proof_method: str model_version: str 8. Proof Engine Interface class CapacityProofEngine: def prove( self, request: CapacityProofRequest, ) -> CapacityProof: ... Request: @dataclass(frozen=True) class CapacityProofRequest: run_id: str route_id: str | None lower_bound: int upper_bound: int model_version: str 9. مهم‌ترین Rule upper_bound باید از جایی معتبر آمده باشد. مثلاً: Demand Upper Bound Infrastructure Aggregate Upper Bound Policy Upper Bound Rolling Stock Upper Bound اگر Aggregate گفته: F <= 50 Detailed Scheduler لازم نیست بی‌دلیل: F = 1000 را امتحان کند. 10. First Feasible Search فرض: lower_bound = 20 upper_bound = 50 ابتدا: F = 20 بررسی می‌شود. اگر feasible باشد: lower_bound = 20 سپس می‌توان با Binary Search جلو رفت، مشروط به اینکه تابع feasibility تحت همان مجموعه Constraints واقعاً monotonic باشد. اما در معماری ما یک نکته مهم وجود دارد: Monotonicity نباید صرفاً فرض شود. اگر با افزایش تعداد Train، ساختار allocation، operating regime، route یا composition تغییر می‌کند، باید monotonicity را در همان تعریف Capacity Search تضمین یا کنترل کنیم. 11. Search Strategy سه حالت: class SearchStrategy(str, Enum): LINEAR = "LINEAR" BINARY = "BINARY" BRACKETED = "BRACKETED" برای Production: BRACKETED مناسب است. ابتدا: known feasible known upper bound ساخته می‌شود و سپس فاصله کاهش می‌یابد. 12. Evaluation Function class CapacityEvaluator: def __init__( self, scheduler: DetailedScheduler, validator: IndependentScheduleValidator, problem_factory, options: SolverOptions, ): self.scheduler = scheduler self.validator = validator self.problem_factory = problem_factory self.options = options def evaluate( self, f: int, ) -> CapacityEvaluation: problem = self.problem_factory.build( train_count=f ) schedule = self.scheduler.solve( problem, self.options, ) if schedule.status != SolverResultStatus.FEASIBLE: return CapacityEvaluation( run_id=problem.run_id, candidate_f=f, scheduler_status=schedule.status, validation_status=ValidationStatus.NOT_VALIDATED, accepted=False, schedule_id=None, explanation="Scheduler did not produce a feasible schedule.", ) validation = self.validator.validate( ValidationContext( problem=problem, result=schedule, validator_version="2.6.F", ) ) accepted = validation.status in ( ValidationStatus.VALID, ValidationStatus.VALID_WITH_WARNINGS, ) return CapacityEvaluation( run_id=problem.run_id, candidate_f=f, scheduler_status=schedule.status, validation_status=validation.status, accepted=accepted, schedule_id=None, explanation=None, ) 13. بسیار مهم: Invalid با Infeasible فرق دارد فرض کنیم: F = 31 و Scheduler: FEASIBLE اما Validator: INVALID این به معنی: 31 trains impossible نیست. بلکه: 31 trains produced an invalid schedule/result بنابراین: F=31 باید: NOT_PROVEN بماند تا مشکل مشخص شود. نباید آن را برای Proof به‌عنوان INFEASIBLE استفاده کنیم. 14. F/F+1 Proof ساده‌ترین Proof: def prove_f_plus_one( evaluator: CapacityEvaluator, f: int, ) -> CapacityProof: current = evaluator.evaluate(f) if not current.accepted: return CapacityProof( run_id=current.run_id, route_id=None, capacity=None, status=ProofStatus.INVALID_BASE, lower_bound=None, upper_bound=None, evaluations=(current,), evidence=(), proof_method="F_PLUS_1", model_version="2.6.G", ) next_result = evaluator.evaluate(f + 1) if next_result.scheduler_status == ( SolverResultStatus.INFEASIBLE ): evidence = CapacityProofEvidence( proof_id="generated", candidate_f=f, next_f=f + 1, candidate_status="FEASIBLE", candidate_validation=current.validation_status.value, next_status="INFEASIBLE", next_validation=None, lower_bound_established=True, upper_bound_established=True, proven=True, reason_code="F_PLUS_ONE_INFEASIBLE", ) return CapacityProof( run_id=current.run_id, route_id=None, capacity=f, status=ProofStatus.PROVEN, lower_bound=f, upper_bound=f, evaluations=( current, next_result, ), evidence=(evidence,), proof_method="F_PLUS_1", model_version="2.6.G", ) return CapacityProof( run_id=current.run_id, route_id=None, capacity=None, status=ProofStatus.NOT_PROVEN, lower_bound=f, upper_bound=None, evaluations=( current, next_result, ), evidence=(), proof_method="F_PLUS_1", model_version="2.6.G", ) 15. مشکل F+1 = UNKNOWN فرض: F = 30 → FEASIBLE F = 31 → UNKNOWN نتیجه: Capacity = 30 نباید Proven شود. نتیجه صحیح: 30 is a validated lower bound Capacity is NOT PROVEN این یکی از مهم‌ترین قواعد کل سیستم است. 16. Capacity Search با Upper Bound فرض: Aggregate Upper Bound = 40 و: F = 30 → VALID F = 31 → VALID ... F = 40 → VALID در این حالت: Capacity >= 40 ولی اگر 40 آخرین upper bound معتبر باشد، هنوز نمی‌توانیم بگوییم: Capacity Proven = 40 مگر اینکه: 41 را نیز بتوانیم به‌طور معتبر رد کنیم، یا یک upper bound مستقل و mathematically sufficient دقیقاً برابر 40 داشته باشیم. 17. Proof با Upper Bound مستقل اگر: Demand Upper Bound = 40 و: F = 40 feasible و validated باشد، آن‌گاه: [ C \le 40 ] از Demand می‌آید و: [ C \ge 40 ] از Schedule می‌آید. بنابراین: [ C=40 ] قابل اثبات است. این روش حتی می‌تواند از F+1 قوی‌تر و ارزان‌تر باشد. 18. Proof Sources هر Upper Bound باید Source داشته باشد: class UpperBoundType(str, Enum): DEMAND = "DEMAND" INFRASTRUCTURE = "INFRASTRUCTURE" ROLLING_STOCK = "ROLLING_STOCK" TERMINAL = "TERMINAL" POLICY = "POLICY" AGGREGATE_SOLVER = "AGGREGATE_SOLVER" EXTERNAL = "EXTERNAL" 19. Proof Logic Validated Lower Bound │ ▼ L = 40 │ ├──────────────┐ │ │ ▼ ▼ F+1 Infeasible Independent UB = 40 │ │ └──────┬───────┘ ▼ Proven 20. Database جداول: result.capacity_evaluation result.capacity_proof result.capacity_proof_evidence result.capacity_profile capacity_evaluation: id run_id candidate_f scheduler_status validation_status accepted schedule_id explanation created_at capacity_proof: id run_id route_id capacity status lower_bound upper_bound proof_method model_version created_at capacity_proof_evidence: id proof_id candidate_f next_f candidate_status next_status lower_bound_established upper_bound_established proven reason_code metadata_json 21. Capacity Profile یک Run می‌تواند چند نوع Capacity داشته باشد: @dataclass(frozen=True) class CapacityProfile: infrastructure_capacity: int | None operational_capacity: int | None rolling_stock_capacity: int | None transportable_capacity: int | None allocated_capacity: int | None proven_capacity: int | None ولی باید برای هر مقدار Status نیز داشته باشیم. مثلاً: operational_capacity = 30 operational_capacity_status = PROVEN 22. Transportable Capacity ممکن است: Infrastructure = 40 trains Operational = 32 trains Wagon = 28 trains Demand = 25 trains در این حالت: Infrastructure Profile = 40 Operational Profile = 32 Rolling Stock Profile = 28 Transportable Profile = 25 ولی نباید صرفاً بنویسیم: Capacity = 25 بدون مشخص کردن Capacity Type. 23. Capacity Offer بعداً Marketplace فقط Capacityای را می‌تواند Offer کند که Ruleهای Business اجازه دهند. مثلاً: Proven Operational Capacity = 30 Transportable Capacity = 24 ممکن است Offer قابل فروش: 24 باشد. بنابراین: Proven Capacity ≠ Offerable Capacity 24. Proof Audit Chain برای هر Proven Capacity: Data Version ↓ Infrastructure Version ↓ Scenario ↓ Model Version ↓ Snapshot Hash ↓ Candidate F ↓ Schedule ↓ Validation ↓ F+1 ↓ Proof Evidence ↓ Capacity Proof این زنجیره باید قابل بازسازی باشد. 25. Definition of Done — V2.6-G [✓] CapacityEvaluation [✓] CapacityProof [✓] ProofEvidence [✓] F feasibility [✓] F+1 evaluation [✓] UNKNOWN handling [✓] Invalid-result handling [✓] Independent validation requirement [✓] Upper-bound source [✓] Demand upper bound [✓] Rolling-stock upper bound [✓] Infrastructure upper bound [✓] Proof status [✓] Capacity profiles [✓] Audit chain [✓] PostgreSQL persistence contract 26. نتیجه اکنون برای اولین بار سیستم می‌تواند به‌صورت دقیق بین این حالات تفاوت بگذارد: FEASIBLE VALIDATED LOWER_BOUND NOT_PROVEN PROVEN INFEASIBLE UNKNOWN INVALID_RESULT و این پایه‌ی علمی Capacity Engine ماست. حالا بلافاصله لایه دوم را روی همین Proof می‌سازیم: V2.6-H — Production Bottleneck & Explanation Engine V2.6-H — Production Bottleneck & Explanation Engine 1. هدف بعد از اینکه Capacity مشخص شد، سؤال عملیاتی بعدی این است: چرا ظرفیت در این سطح قرار گرفته است؟ مثلاً: Capacity = 27 trains/day به‌تنهایی برای Planner کافی نیست. باید بدانیم: Why 27? Why not 28? Which resource is binding? How much capacity is lost? What change can increase it? بنابراین خروجی: Capacity + Bottleneck + Evidence + Explanation است. 2. Bottleneck با Utilization یکی نیست این اشتباه نباید وارد سیستم شود: Highest utilization = Bottleneck ممکن است: Block A = 99% Block B = 95% ولی افزایش ظرفیت Block A هیچ اثر واقعی نداشته باشد. در حالی که تغییر Block B ممکن است: Capacity: 27 → 31 کند. بنابراین Bottleneck باید با Marginal Capacity Impact تعریف شود. 3. تعریف برای Resource g: C_{base} ] مثلاً: Baseline = 27 Scenario: Headway 5 → 4 Capacity = 30 ΔC = +3 پس این Rule می‌گوید: Headway Constraint has marginal capacity impact = +3 4. Bottleneck Candidate @dataclass(frozen=True) class BottleneckCandidate: resource_id: str resource_type: str utilization: float binding: bool near_binding: bool structural: bool marginal_capacity_gain: int | None affected_routes: tuple[str, ...] affected_trains: tuple[str, ...] evidence_ids: tuple[str, ...] 5. Bottleneck Levels class BottleneckLevel(str, Enum): BINDING = "BINDING" NEAR_BINDING = "NEAR_BINDING" STRUCTURAL = "STRUCTURAL" اما این Levelها نباید صرفاً بر اساس utilization تعیین شوند. 6. Binding Constraint Constraint زمانی Binding است که: در جواب فعلی فعال باشد + Slack بسیار کم/صفر باشد + رفع آن روی ظرفیت اثر قابل اندازه‌گیری داشته باشد مثلاً: Block B7 Capacity = 27 Constraint: Opposing separation = 8 min Slack = 0 7. Slack برای Constraint: [ Slack = Actual - Required ] مثلاً: Required = 8 Actual = 8 پس: Slack = 0 و: Binding Candidate است. 8. Resource Utilization برای Block: [ U_b = \frac{ \sum OccupancyTime }{ AvailableTime } ] مثلاً: Occupancy = 1200 min Available = 1440 min پس: [ U=83.3% ] اما این فقط یک Metric است، نه Proof of Bottleneck. 9. Capacity Impact Bottleneck Engine باید در صورت امکان Scenario کوچک ایجاد کند. مثلاً: Baseline: Headway = 5 Capacity = 27 Scenario: Headway = 4 Capacity = 30 نتیجه: Marginal Gain = +3 10. Bottleneck Analysis Request @dataclass(frozen=True) class BottleneckAnalysisRequest: run_id: str base_capacity: int candidate_resources: tuple[str, ...] max_scenarios: int = 10 11. Bottleneck Result @dataclass(frozen=True) class BottleneckAnalysis: run_id: str base_capacity: int candidates: tuple[BottleneckCandidate, ...] top_constraints: tuple[str, ...] hidden_capacity: int | None 12. Resource Usage Validator و Scheduler باید Resource Usage تولید کنند: @dataclass(frozen=True) class ResourceUsage: resource_id: str resource_type: str available_minutes: int occupied_minutes: int utilization: float usage_intervals: tuple[ tuple[int, int], ... ] 13. Binding Constraint Evidence @dataclass(frozen=True) class BindingConstraint: id: str run_id: str constraint_code: str resource_id: str slack: int utilization: float binding: bool marginal_capacity_gain: int | None 14. Explanation Model توضیح باید Structured باشد. @dataclass(frozen=True) class CapacityExplanation: run_id: str summary: str capacity: int | None primary_bottlenecks: tuple[str, ...] limiting_constraints: tuple[str, ...] evidence_ids: tuple[str, ...] improvement_scenarios: tuple[str, ...] 15. Explanation نباید حدس بزند مثلاً نباید بگوییم: ظرفیت پایین است چون ایستگاه X شلوغ است. مگر اینکه Evidence داشته باشیم. باید بگوییم: در Schedule معتبر، Track 3 ایستگاه X در 94٪ زمان عملیاتی مورد استفاده قرار گرفته و سناریوی افزایش ظرفیت این Track از 27 به 29 قطار در روز افزایش ظرفیت ایجاد کرده است. یعنی: Fact + Evidence + Scenario 16. Explanation Pipeline Capacity Proof ↓ Resource Usage ↓ Constraint Slack ↓ Binding Detection ↓ Marginal Scenario ↓ Bottleneck Ranking ↓ Explanation 17. Hidden Capacity یکی از مهم‌ترین خروجی‌ها: Nominal Capacity Operational Capacity Proven Capacity Unused/Hidden Capacity مثلاً: Infrastructure Theoretical = 40 Strict Alternating = 24 Directional Batch = 28 Mixed = 30 در این حالت: Regime-sensitive capacity وجود دارد. ولی نباید بدون تحلیل کامل بگوییم: Hidden Capacity = 10 باید تعریف دقیق داشته باشیم: C_{baseline\ regime} ] 18. Regime Analysis @dataclass(frozen=True) class OperatingRegimeResult: regime: OperatingRegime capacity: int | None proof_status: ProofStatus validated: bool مثلاً: STRICT_ALTERNATING 24 PROVEN DIRECTIONAL_BATCH 28 PROVEN MIXED 30 PROVEN این اطلاعات برای Planner بسیار ارزشمند است. 19. Sensitivity Analysis پارامترهای قابل تحلیل: Headway Switch Time Clearing Time Running Time Dwell Station Track Count Station Track Length Train Length Train Weight Wagon Availability Locomotive Availability Demand Operational Window برای هر پارامتر: [ Sensitivity = \frac{\Delta C}{\Delta x} ] یا به‌صورت discrete: Capacity(x + Δx) - Capacity(x) 20. مثال Baseline: Headway = 5 min Capacity = 27 Scenario: Headway = 4 min Capacity = 30 Scenario: Switch = 8 → 6 Capacity = 28 Scenario: Station Track +1 Capacity = 27 پس سیستم می‌تواند بگوید: Headway reduction: +3 trains/day Switch reduction: +1 train/day Additional station track: 0 trains/day بدون اینکه به‌صورت دستی بگوییم کدام پروژه سرمایه‌گذاری «بهتر» است. 21. Bottleneck Ranking سیستم می‌تواند بر اساس Evidence موارد را مرتب کند، اما خروجی باید تحلیلی باشد. مثلاً: Constraint Capacity Gain Utilization Slack Affected OD Affected Route نمونه: Constraint Slack Utilization ΔCapacity Single Track B7 0 96% +3 Switch at S4 0 — +1 Station Track S6 1 88% 0 این جدول یک تحلیل فنی است، نه یک رتبه‌بندی سلیقه‌ای. 22. Bottleneck Scenario Runner class BottleneckScenarioRunner: def evaluate_parameter_change( self, base_scenario, parameter, new_value, ): scenario = base_scenario.with_change( parameter, new_value, ) return self.capacity_engine.run( scenario ) نکته مهم: هر Scenario باید یک ScenarioId مستقل داشته باشد. 23. Full Re-solve برای تغییر مهم Infrastructure: Station Track +1 Single Track → Double Track Headway 5 → 4 Switch 8 → 6 نباید فقط با فرمول ساده Capacity را تخمین بزنیم. باید: Aggregate → Detailed → Validator → Proof دوباره اجرا شود. 24. Bottleneck Persistence در PostgreSQL: result.bottleneck result.binding_constraint result.resource_usage result.sensitivity_result result.capacity_explanation Bottleneck: id run_id resource_id resource_type level utilization slack marginal_capacity_gain affected_routes_json evidence_json 25. Explanation Evidence هر Explanation باید به Evidence وصل باشد. Explanation E-001 ↓ Evidence V-193 ↓ Rule SINGLE_TRACK_OVERLAP ↓ Block B7 ↓ Train 101 / Train 102 و برای Sensitivity: Explanation E-002 ↓ Scenario S-014 ↓ Headway 5 → 4 ↓ Capacity 27 → 30 26. Natural Language Generation در مرحله اول بهتر است Explanation Template-Based باشد. مثلاً: def explain_binding_constraint( constraint: BindingConstraint, ) -> str: return ( f"Resource {constraint.resource_id} is binding " f"under constraint {constraint.constraint_code}. " f"Observed slack is {constraint.slack} minutes." ) بعداً می‌توان Natural Language Layer پیشرفته‌تر اضافه کرد. اما منطق Explanation نباید وابسته به LLM باشد. 27. Explanation باید Deterministic باشد برای یک Run ثابت: same snapshot + same scenario + same model باید Explanation facts ثابت باشند. بنابراین: LLM در این معماری صرفاً می‌تواند Presentation Layer باشد، نه منبع حقیقت. 28. Capacity Explanation Object نمونه خروجی: { "capacity": 27, "proof_status": "PROVEN", "summary": "27 trains/day are proven under the selected operating scenario.", "primary_constraints": [ { "resource": "B7", "constraint": "OPPOSING_DIRECTION_SEPARATION", "slack_minutes": 0 } ], "sensitivity": [ { "parameter": "headway", "base": 5, "scenario": 4, "capacity_change": 3 } ] } 29. Explanation برای Planner Planner باید بتواند ببیند: Current proven capacity ↓ What limits it? ↓ Where? ↓ Which trains/routes affected? ↓ What parameter controls it? ↓ What happens if changed? این دقیقاً تفاوت یک Capacity Calculator ساده با یک Decision Support System است. 30. E2E Pipeline اکنون معماری به این شکل کامل می‌شود: Access / Excel ↓ Canonical ↓ Planning Snapshot ↓ Aggregate Solver ↓ Train Formation ↓ Wagon / Loco ↓ Detailed CP-SAT ↓ Independent Validator ↓ Capacity Proof ↓ Resource Usage ↓ Bottleneck Analysis ↓ Sensitivity ↓ Explanation ↓ Capacity Profile ↓ Capacity Offer 31. Capacity Profile نهایی خروجی مناسب: @dataclass(frozen=True) class ProvenCapacityProfile: route_id: str infrastructure_capacity: int | None operational_capacity: int | None rolling_stock_capacity: int | None transportable_capacity: int | None proven_capacity: int | None proof_status: ProofStatus primary_bottlenecks: tuple[str, ...] explanation_id: str 32. مثال کامل فرض کنیم: Route R1 Infrastructure Upper Bound = 40 Demand Upper Bound = 35 Detailed: 30 → VALID 31 → VALID 32 → VALID 33 → VALID 34 → VALID 35 → VALID در این حالت: Validated Lower Bound = 35 Demand Upper Bound = 35 بنابراین: Proven Capacity = 35 بدون نیاز به اجرای 36، چون: [ C \le 35 ] از Demand و: [ C \ge 35 ] از Schedule معتبر حاصل شده است. 33. مثال Bottleneck فرض: Capacity = 35 B7: Utilization = 97% Slack = 0 Station S4: Utilization = 82% Slack = 4 Junction J2: Utilization = 91% Slack = 0 سناریوها: B7 Headway: 35 → 38 J2 Separation: 35 → 36 Station S4: 35 → 35 پس Explanation می‌تواند مستند کند: B7: capacity impact +3 J2: capacity impact +1 S4: capacity impact 0 این خروجی از داده و Scenario Solve می‌آید، نه از حدس. 34. Investment Scenario بعداً می‌توانیم: Scenario 0: Current Scenario 1: Double Track B7 Scenario 2: Reduce Headway Scenario 3: Additional Station Track Scenario 4: Increase Wagon Pool Scenario 5: Increase Locomotive Pool را اجرا کنیم. هر Scenario: Independent Run + Independent Proof دارد. 35. Important Architectural Rule نباید بگوییم: Bottleneck Engine → directly changes Infrastructure بلکه: Bottleneck Engine ↓ Scenario Recommendation Data ↓ Planner ↓ Scenario ↓ New Capacity Run یعنی Engine تصمیم سرمایه‌گذاری را به‌جای Planner نمی‌گیرد. 36. Definition of Done — V2.6-H [✓] Resource Usage [✓] Constraint Slack [✓] Binding Constraint [✓] Bottleneck Candidate [✓] Marginal Capacity Impact [✓] Sensitivity Scenario [✓] Regime Analysis [✓] Hidden Capacity [✓] Explanation Model [✓] Evidence linkage [✓] Deterministic explanation facts [✓] PostgreSQL persistence [✓] Full re-solve for infrastructure changes [✓] Capacity Profile [✓] Planner-facing explanation 37. نقطه‌ای که اکنون به آن رسیده‌ایم زنجیره Production اکنون: ┌─────────────────┐ │ Access / Excel │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Canonical │ └────────┬────────┘ ↓ ┌─────────────────┐ │ PlanningSnapshot│ └────────┬────────┘ ↓ ┌─────────────────┐ │ Aggregate │ │ Optimization │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Formation │ │ Wagon / Loco │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Detailed │ │ CP-SAT │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Independent │ │ Validator │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Capacity Proof │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Bottleneck │ │ & Sensitivity │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Explanation │ └────────┬────────┘ ↓ ┌─────────────────┐ │ Capacity Offer │ └─────────────────┘ و مهم‌تر اینکه اکنون سه سطح اعتماد داریم: Level 1: Solver Result Level 2: Independently Validated Schedule Level 3: Proven Capacity این تفکیک باید در Database، API، UI و Marketplace نیز حفظ شود. قدم بعدی منطقی بعد از این دو، V2.6-I — Production Rolling Stock Integration است؛ یعنی اتصال واقعی ظرفیت اثبات‌شده به Wagon Cycle + Empty Wagon Network + Locomotive Cycle تا مشخص شود مثلاً ظرفیتی که Infrastructure و Schedule اجازه می‌دهند، آیا واقعاً با موجودی واگن و لکوموتیو قابل بهره‌برداری هم هست یا نه. ---