Cuong2004 commited on
Commit
b010f1b
·
0 Parent(s):

Initial commit: REMB - AI-Powered Industrial Estate Master Plan Optimization Engine

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +29 -0
  2. .gitignore +86 -0
  3. README.md +0 -0
  4. config/__init__.py +1 -0
  5. config/regulations.yaml +61 -0
  6. config/settings.py +50 -0
  7. docs/Core_document.md +40 -0
  8. docs/MVP-24h.md +1783 -0
  9. docs/MVP-STATUS.md +120 -0
  10. docs/Proposal_ AI-Powered Industrial Estate Master Plan Optimization Engine.md +0 -0
  11. docs/Requirement.md +8 -0
  12. examples/api-cw750-details.dxf +0 -0
  13. frontend/.gitignore +24 -0
  14. frontend/README.md +73 -0
  15. frontend/eslint.config.js +23 -0
  16. frontend/index.html +13 -0
  17. frontend/package-lock.json +0 -0
  18. frontend/package.json +34 -0
  19. frontend/public/vite.svg +1 -0
  20. frontend/src/App.css +724 -0
  21. frontend/src/App.tsx +333 -0
  22. frontend/src/assets/react.svg +1 -0
  23. frontend/src/components/ChatInterface.tsx +127 -0
  24. frontend/src/components/ExportPanel.tsx +35 -0
  25. frontend/src/components/FileUploadPanel.tsx +75 -0
  26. frontend/src/components/LayoutOptionsPanel.tsx +91 -0
  27. frontend/src/components/Map2DPlotter.tsx +274 -0
  28. frontend/src/components/index.ts +6 -0
  29. frontend/src/index.css +26 -0
  30. frontend/src/main.tsx +10 -0
  31. frontend/src/services/api.ts +102 -0
  32. frontend/src/types/index.ts +86 -0
  33. frontend/tsconfig.app.json +28 -0
  34. frontend/tsconfig.json +7 -0
  35. frontend/tsconfig.node.json +26 -0
  36. frontend/vite.config.ts +7 -0
  37. requirements.txt +41 -0
  38. src/__init__.py +7 -0
  39. src/algorithms/__init__.py +1 -0
  40. src/algorithms/ga_optimizer.py +405 -0
  41. src/algorithms/milp_solver.py +659 -0
  42. src/algorithms/nsga2_optimizer.py +336 -0
  43. src/algorithms/regulation_checker.py +329 -0
  44. src/api/__init__.py +1 -0
  45. src/api/main.py +182 -0
  46. src/api/mvp_api.py +677 -0
  47. src/core/__init__.py +2 -0
  48. src/core/orchestrator.py +662 -0
  49. src/export/__init__.py +2 -0
  50. src/export/dxf_exporter.py +444 -0
.env.example ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment Configuration for REMB
2
+
3
+ # Database Configuration
4
+ POSTGRES_USER=remb_user
5
+ POSTGRES_PASSWORD=remb_password
6
+ POSTGRES_SERVER=localhost
7
+ POSTGRES_PORT=5432
8
+ POSTGRES_DB=remb_db
9
+
10
+ # API Configuration
11
+ API_V1_STR=/api/v1
12
+ PROJECT_NAME="REMB - Industrial Estate Master Planning Engine"
13
+ VERSION=0.1.0
14
+
15
+ # Optimization Settings
16
+ NSGA2_POPULATION_SIZE=100
17
+ NSGA2_GENERATIONS=200
18
+ NSGA2_CROSSOVER_RATE=0.9
19
+ NSGA2_MUTATION_RATE=0.1
20
+
21
+ # MILP Solver Settings
22
+ MILP_TIME_LIMIT_SECONDS=3600
23
+ MILP_SOLVER=SCIP
24
+
25
+ # File Upload Settings
26
+ MAX_UPLOAD_SIZE_MB=50
27
+
28
+ # Processing Settings
29
+ MAX_CONCURRENT_OPTIMIZATIONS=2
.gitignore ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+ *.manifest
25
+ *.spec
26
+ pip-log.txt
27
+ pip-delete-this-directory.txt
28
+
29
+ # Virtual Environment
30
+ venv/
31
+ ENV/
32
+ env/
33
+ .venv
34
+
35
+ # IDEs
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+ *.swo
40
+ *~
41
+ .DS_Store
42
+
43
+ # Environment variables
44
+ .env
45
+ .env.local
46
+ .env.*.local
47
+
48
+ # Testing
49
+ .pytest_cache/
50
+ .coverage
51
+ htmlcov/
52
+ .tox/
53
+ .nox/
54
+
55
+ # Jupyter Notebook
56
+ .ipynb_checkpoints
57
+
58
+ # Frontend - Node
59
+ frontend/node_modules/
60
+ frontend/dist/
61
+ frontend/.vite/
62
+ frontend/.cache/
63
+
64
+ # Logs
65
+ *.log
66
+ logs/
67
+ npm-debug.log*
68
+ yarn-debug.log*
69
+ yarn-error.log*
70
+ pnpm-debug.log*
71
+ lerna-debug.log*
72
+
73
+ # Output files
74
+ output/*.dxf
75
+ output/*.pdf
76
+ output/*.png
77
+ !output/.gitkeep
78
+
79
+ # OS
80
+ Thumbs.db
81
+ .DS_Store
82
+
83
+ # Temporary files
84
+ *.tmp
85
+ *.temp
86
+ .cache/
README.md ADDED
Binary file (7.26 kB). View file
 
config/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Config package"""
config/regulations.yaml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vietnamese Industrial Estate Regulations
2
+ # Quy định khu công nghiệp Việt Nam
3
+
4
+ # Setbacks (khoảng cách lùi ranh giới) - in meters
5
+ setbacks:
6
+ boundary_minimum: 50 # Khoảng cách tối thiểu từ ranh giới
7
+ fire_safety_distance: 30 # Khoảng cách an toàn PCCC
8
+ waterway_buffer: 100 # Vùng đệm sông, suối
9
+ highway_buffer: 50 # Vùng đệm đường cao tốc
10
+ residential_buffer: 300 # Khoảng cách từ khu dân cư
11
+
12
+ # Floor Area Ratio (Tỷ lệ diện tích sàn)
13
+ far:
14
+ maximum: 0.7 # Hệ số sử dụng đất tối đa
15
+ minimum: 0.3 # Hệ số sử dụng đất tối thiểu
16
+
17
+ # Green Space Requirements (Yêu cầu cây xanh)
18
+ green_space:
19
+ minimum_percentage: 0.15 # 15% diện tích tối thiểu
20
+ preferred_percentage: 0.20 # 20% diện tích khuyến khích
21
+
22
+ # Plot Requirements (Yêu cầu lô đất)
23
+ plot:
24
+ minimum_area_sqm: 1000 # Diện tích lô tối thiểu (m²)
25
+ maximum_area_sqm: 50000 # Diện tích lô tối đa (m²)
26
+ minimum_width_m: 20 # Chiều rộng tối thiểu (m)
27
+ minimum_frontage_m: 15 # Mặt tiền tối thiểu (m)
28
+
29
+ # Road Network (Mạng lưới đường)
30
+ roads:
31
+ primary_width_m: 24 # Đường chính
32
+ secondary_width_m: 16 # Đường nhánh
33
+ tertiary_width_m: 12 # Đường nội bộ
34
+ minimum_turning_radius_m: 12 # Bán kính quay đầu tối thiểu
35
+ maximum_distance_to_road_m: 200 # Khoảng cách tối đa đến đường
36
+
37
+ # Hazard Zones (Khu vực nguy hiểm)
38
+ hazard_zones:
39
+ chemical_storage_buffer_m: 200 # Vùng đệm kho hóa chất
40
+ fuel_storage_buffer_m: 150 # Vùng đệm kho nhiên liệu
41
+ warehouse_buffer_m: 50 # Vùng đệm kho hàng thường
42
+ utility_corridor_width_m: 10 # Hành lang kỹ thuật
43
+
44
+ # Infrastructure (Hạ tầng)
45
+ infrastructure:
46
+ utility_corridor_percentage: 0.05 # 5% diện tích cho hành lang kỹ thuật
47
+ drainage_slope_minimum: 0.003 # Độ dốc thoát nước tối thiểu (0.3%)
48
+
49
+ # Land Use Distribution (Phân bổ đất đai khuyến nghị)
50
+ land_use:
51
+ industrial_plots: 0.65 # 65% đất công nghiệp
52
+ roads_and_infrastructure: 0.20 # 20% đường và hạ tầng
53
+ green_space: 0.15 # 15% cây xanh
54
+
55
+ # Optimization Objectives (Mục tiêu tối ưu)
56
+ objectives:
57
+ weights:
58
+ sellable_area: 0.5 # Trọng số diện tích có thể bán
59
+ green_space: 0.2 # Trọng số cây xanh
60
+ road_efficiency: 0.15 # Trọng số hiệu quả đường
61
+ regulatory_compliance: 0.15 # Trọng số tuân thủ quy định
config/settings.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for REMB Optimization Engine
3
+ """
4
+ from pydantic_settings import BaseSettings
5
+ from typing import Optional
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ """Application settings"""
10
+
11
+ # API Settings
12
+ API_V1_STR: str = "/api/v1"
13
+ PROJECT_NAME: str = "REMB - Industrial Estate Master Planning Engine"
14
+ VERSION: str = "0.1.0"
15
+
16
+ # Database Settings
17
+ POSTGRES_USER: str = "remb_user"
18
+ POSTGRES_PASSWORD: str = "remb_password"
19
+ POSTGRES_SERVER: str = "localhost"
20
+ POSTGRES_PORT: str = "5432"
21
+ POSTGRES_DB: str = "remb_db"
22
+
23
+ @property
24
+ def DATABASE_URL(self) -> str:
25
+ return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
26
+
27
+ # Optimization Settings
28
+ NSGA2_POPULATION_SIZE: int = 100
29
+ NSGA2_GENERATIONS: int = 200
30
+ NSGA2_CROSSOVER_RATE: float = 0.9
31
+ NSGA2_MUTATION_RATE: float = 0.1
32
+
33
+ # MILP Solver Settings
34
+ MILP_TIME_LIMIT_SECONDS: int = 3600 # 1 hour
35
+ MILP_SOLVER: str = "SCIP" # OR-Tools solver
36
+
37
+ # File Upload Settings
38
+ MAX_UPLOAD_SIZE_MB: int = 50
39
+ ALLOWED_EXTENSIONS: list = [".shp", ".dxf", ".geojson"]
40
+
41
+ # Processing Settings
42
+ MAX_CONCURRENT_OPTIMIZATIONS: int = 2
43
+ CELERY_BROKER_URL: Optional[str] = None
44
+
45
+ class Config:
46
+ env_file = ".env"
47
+ case_sensitive = True
48
+
49
+
50
+ settings = Settings()
docs/Core_document.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Cơ chế hoạt động của phần Core Engine theo mô hình "Nhạc trưởng & Kỹ sư" (Orchestrator-Solver Model):
2
+
3
+ 1. Phân chia vai trò
4
+ 🧠 LLM (The Brain/Nhạc trưởng): Chịu trách nhiệm hiểu ngữ nghĩa, điều phối luồng đi, xử lý logic nghiệp vụ và giao tiếp với con người. LLM không trực tiếp tính toán hình học.
5
+
6
+ ⚙️ CP Module (The Muscle/Kỹ sư): Chịu trách nhiệm tính toán toán học, giải quyết các ràng buộc hình học (Geometry) và tối ưu hóa (Optimization) chính xác tuyệt đối. CP hoạt động như một "hộp đen" (black-box) xử lý dữ liệu.
7
+
8
+ 2. Quy trình "Bắt tay" (The Handshake Loop)
9
+ Quy trình diễn ra theo vòng lặp khép kín 4 bước:
10
+
11
+ Dịch (Translation):
12
+
13
+ LLM nhận yêu cầu tự nhiên (VD: "Tránh kho xăng 200m").
14
+
15
+ LLM chuyển đổi yêu cầu thành tham số kỹ thuật chuẩn (JSON) để gọi hàm (Function Calling).
16
+
17
+ Giải (Execution):
18
+
19
+ CP Module nhận JSON, chạy thuật toán (MILP/GeoPandas).
20
+
21
+ CP trả về kết quả thô (số liệu, tọa độ) hoặc trạng thái lỗi (nếu bài toán vô nghiệm).
22
+
23
+ Hiểu (Interpretation):
24
+
25
+ LLM đọc kết quả thô từ CP.
26
+
27
+ LLM so sánh với yêu cầu ban đầu để đánh giá: Thành công hay Thất bại.
28
+
29
+ Quyết định (Reasoning & Action):
30
+
31
+ Nếu thành công: LLM ra lệnh xuất bản vẽ và báo cáo cho người dùng.
32
+
33
+ Nếu thất bại (Xung đột): LLM tự động suy luận nguyên nhân (VD: "Quá chật chội"), đề xuất phương án nới lỏng ràng buộc và hỏi ý kiến người dùng.
34
+
35
+ 3. Ưu điểm cốt lõi
36
+ Cơ chế này khắc phục được điểm yếu chết người của từng công nghệ riêng lẻ:
37
+
38
+ Loại bỏ sự "ảo giác" (hallucination) của AI vì mọi con số đều do CP tính.
39
+
40
+ Loại bỏ sự "cứng nhắc" của thuật toán truyền thống vì LLM giúp linh hoạt trong việc nhập liệu và xử lý tình huống phát sinh.
docs/MVP-24h.md ADDED
@@ -0,0 +1,1783 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # **AIOptimize™ COMPLETE ARCHITECTURE, WORKFLOW & IMPLEMENTATION GUIDE**
2
+
3
+ ## **Enterprise-Ready System Design Architecture & Strategy**
4
+
5
+ **Complete technical architecture, workflow diagrams, technology stack, and implementation strategy**
6
+
7
+ ---
8
+
9
+ # **🎯 EXECUTIVE OVERVIEW**
10
+
11
+ ## **What This System Does**
12
+
13
+ AIOptimize™ is an AI-powered industrial estate planning engine that: 1\. Analyzes site boundaries 2\. Generates multiple optimized layout options 3\. Explains optimization choices via AI 4\. Exports professional CAD files
14
+
15
+ ## **System Maturity Levels**
16
+
17
+ ### **Level 1: MVP (6 hours)**
18
+
19
+ * Basic UI for file upload
20
+
21
+ * GeoJSON parsing
22
+
23
+ * Simple visualization
24
+
25
+ * No optimization
26
+
27
+ ### **Level 2: Smart Demo (12 hours)**
28
+
29
+ * Real genetic algorithm optimization
30
+
31
+ * Multiple intelligent layout options
32
+
33
+ * Hardcoded AI chat explanations
34
+
35
+ * Professional 2D visualization
36
+
37
+ ### **Level 2+: Enterprise (24 hours)**
38
+
39
+ * Real Gemini Flash 2.0 AI (replaces hardcoded)
40
+
41
+ * Professional DXF CAD export
42
+
43
+ * Complete error handling
44
+
45
+ * Production-ready deployment
46
+
47
+ ---
48
+
49
+ # **📊 COMPLETE SYSTEM ARCHITECTURE**
50
+
51
+ ## **High-Level System Design**
52
+
53
+ ┌─────────────────────────────────────────────────────────────┐
54
+ │ USER INTERFACE (Browser) │
55
+ │ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │
56
+ │ │ Upload UI │ │ 2D Visualizer │ │ Chat Panel │ │
57
+ │ │ (React) │ │ (Konva.js) │ │ (React) │ │
58
+ │ └──────────────┘ └────────────────┘ └──────────────┘ │
59
+ │ │ │ │
60
+ │ ┌─────────────────────┴──────────────────────┘ │
61
+ │ │ Metrics Display │ Export Buttons (DXF/ZIP) │
62
+ └──┼──────────────────────────────────────────────────────────┘
63
+ │ REST API (HTTP/JSON)
64
+
65
+ ┌─────────────────────────────────────────────────────────────┐
66
+ │ APPLICATION BACKEND │
67
+ │ (FastAPI \- Python) │
68
+ │ │
69
+ │ ┌──────────────────────────────────────────────────────┐ │
70
+ │ │ API Layer (REST Endpoints) │ │
71
+ │ │ • /upload-boundary (POST) │ │
72
+ │ │ • /generate-layouts (POST) │ │
73
+ │ │ • /chat (POST) │ │
74
+ │ │ • /export-dxf (POST) │ │
75
+ │ │ • /export-all-dxf (POST) │ │
76
+ │ │ • /health (GET) │ │
77
+ │ └──────────────────────────────────────────────────────┘ │
78
+ │ │ │
79
+ │ ┌──────────────────────┴───────────────────────────────┐ │
80
+ │ │ Service Layer (Business Logic) │ │
81
+ │ │ ┌──────────────────┐ ┌──────────────────────┐ │ │
82
+ │ │ │ Geometry Service │ │ GA Optimization │ │ │
83
+ │ │ │ (Shapely) │ │ (Genetic Algorithm) │ │ │
84
+ │ │ └──────────────────┘ └──────────────────────┘ │ │
85
+ │ │ ┌──────────────────┐ ┌──────────────────────┐ │ │
86
+ │ │ │ Chat Service │ │ Gemini LLM Service │ │ │
87
+ │ │ │ (Hardcoded) │ │ (Real AI) │ │ │
88
+ │ │ └──────────────────┘ └──────────────────────┘ │ │
89
+ │ │ ���──────────────────┐ ┌──────────────────────┐ │ │
90
+ │ │ │ DXF Export │ │ Session Management │ │ │
91
+ │ │ │ (ezdxf) │ │ (In-memory store) │ │ │
92
+ │ │ └──────────────────┘ └──────────────────────┘ │ │
93
+ │ └──────────────────────────────────────────────────────┘ │
94
+ │ │
95
+ │ ┌──────────────────────────────────────────────────────┐ │
96
+ │ │ Data Layer │ │
97
+ │ │ • Session storage (UUID → site data) │ │
98
+ │ │ • Geometry data (Shapely Polygon objects) │ │
99
+ │ │ • Layout options (plot coordinates, metrics) │ │
100
+ │ │ • Export cache (temporary DXF files) │ │
101
+ │ └──────────────────────────────────────────────────────┘ │
102
+ └─────────────────────────────────────────────────────────────┘
103
+ │ External API Calls
104
+ ├─→ Google Gemini API (AI Chat)
105
+ ├─→ GeoJSON parsing (geospatial data)
106
+ └─→ File system (export storage)
107
+
108
+ ---
109
+
110
+ # **🏗️ DETAILED SYSTEM COMPONENTS**
111
+
112
+ ## **Component 1: Frontend Application Layer**
113
+
114
+ ### **Purpose**
115
+
116
+ Interactive user interface for site analysis and visualization.
117
+
118
+ ### **Responsibilities**
119
+
120
+ * File upload handling
121
+
122
+ * 2D site visualization
123
+
124
+ * Layout options display
125
+
126
+ * Chat interface
127
+
128
+ * Export button management
129
+
130
+ * Real-time state management
131
+
132
+ ### **Technology Choices & Why**
133
+
134
+ * **React** (instead of Vue/Angular)
135
+
136
+ * Large ecosystem (tools, libraries, components)
137
+
138
+ * TypeScript support (type safety)
139
+
140
+ * Easier learning curve for full-stack developers
141
+
142
+ * Better component reusability
143
+
144
+ * **TypeScript** (instead of plain JavaScript)
145
+
146
+ * Catches errors at compile time
147
+
148
+ * Better IDE support and autocomplete
149
+
150
+ * Self-documenting code
151
+
152
+ * Enterprise standard
153
+
154
+ * **Konva.js** (instead of Canvas API / D3 / Three.js)
155
+
156
+ * Specifically built for 2D graphics
157
+
158
+ * Simpler API than raw Canvas
159
+
160
+ * Built-in event handling
161
+
162
+ * Better performance for GIS visualization
163
+
164
+ * **Axios** (instead of Fetch API / GraphQL)
165
+
166
+ * Simpler request/response handling
167
+
168
+ * Built-in interceptors (error handling, logging)
169
+
170
+ * Request/response transformation
171
+
172
+ * Backward compatible
173
+
174
+ * **Lucide React** (for icons)
175
+
176
+ * Lightweight icon library
177
+
178
+ * Consistent icon set
179
+
180
+ * Simple integration with React
181
+
182
+ ### **Data Flow (Frontend)**
183
+
184
+ User Action (Upload/Generate/Chat)
185
+
186
+ React State Update
187
+
188
+ API Call via Axios
189
+
190
+ Wait for Response
191
+
192
+ Update UI Components
193
+
194
+ Display Results
195
+
196
+ ### **Component Hierarchy**
197
+
198
+ \<App\> (Main)
199
+ ├─ Header (Logo, Title)
200
+ ├─ FileUploadPanel
201
+ │ ├─ Upload Button
202
+ │ └─ Sample Data Button
203
+ ├─ MainContent (60% width)
204
+ │ ├─ Map2DPlotter (Konva Stage)
205
+ │ │ ├─ Boundary Polygon
206
+ │ │ ├─ Setback Zone
207
+ │ │ └─ Grid/Reference Lines
208
+ │ ├─ LayoutOptionsPanel
209
+ │ │ ├─ OptionCard 1
210
+ │ │ ├─ OptionCard 2
211
+ │ │ └─ OptionCard 3
212
+ │ └─ ExportPanel
213
+ │ ├─ Export Individual Buttons
214
+ │ └─ Export All Zip Button
215
+ └─ ChatSidebar (40% width)
216
+ └─ ChatInterface
217
+ ├─ Message History
218
+ ├─ Message Input
219
+ └─ Send Button
220
+
221
+ ---
222
+
223
+ ## **Component 2: Backend Application Layer**
224
+
225
+ ### **Purpose**
226
+
227
+ Business logic, optimization algorithms, and data processing.
228
+
229
+ ### **Responsibilities**
230
+
231
+ * REST API endpoint management
232
+
233
+ * GeoJSON parsing and validation
234
+
235
+ * Session management
236
+
237
+ * Genetic algorithm execution
238
+
239
+ * AI response generation
240
+
241
+ * DXF file creation
242
+
243
+ * Error handling and logging
244
+
245
+ ### **Technology Choices & Why**
246
+
247
+ * **FastAPI** (instead of Flask / Django / Starlette)
248
+
249
+ * Automatic OpenAPI documentation
250
+
251
+ * Built-in request validation (Pydantic)
252
+
253
+ * Asynchronous support (async/await)
254
+
255
+ * Type hints integration
256
+
257
+ * Very fast (ASGI-based)
258
+
259
+ * Smaller learning curve than Django
260
+
261
+ * **Python 3.8+** (language choice)
262
+
263
+ * Large scientific computing ecosystem
264
+
265
+ * Quick prototyping and development
266
+
267
+ * Strong geospatial libraries (Shapely, GeoPandas)
268
+
269
+ * Good AI/ML library support
270
+
271
+ * Easy deployment
272
+
273
+ * **Shapely** (instead of GDAL / PostGIS / Turf.js)
274
+
275
+ * Pure Python (no native dependencies)
276
+
277
+ * Simple geometry operations
278
+
279
+ * Built-in validation
280
+
281
+ * Good performance for 2D operations
282
+
283
+ * Well-documented
284
+
285
+ * **NumPy** (for numerical operations)
286
+
287
+ * Industry standard for numerical computing
288
+
289
+ * Fast matrix/array operations
290
+
291
+ * Genetic algorithm fitness calculations
292
+
293
+ * Statistical functions
294
+
295
+ * **google-generativeai** (for Gemini API)
296
+
297
+ * Official Google library
298
+
299
+ * Maintained and updated
300
+
301
+ * Simple API for chat completion
302
+
303
+ * Free tier available
304
+
305
+ * **ezdxf** (instead of pyDXF / cadquery / LibreCAD)
306
+
307
+ * Comprehensive DXF support (R2010 standard)
308
+
309
+ * Specific for CAD file creation
310
+
311
+ * Active maintenance
312
+
313
+ * Good layer/attribute support
314
+
315
+ * Works with all CAD software
316
+
317
+ ### **Service Architecture**
318
+
319
+ API Layer (REST Endpoints)
320
+
321
+ ├─ Authentication/Validation
322
+ ├─ Request Routing
323
+ └─ Response Formatting
324
+
325
+ Service Layer (Business Logic)
326
+ ├─ GeometryService
327
+ │ ├─ Parse GeoJSON → Polygon
328
+ │ ├─ Calculate setback zones
329
+ │ ├─ Validate boundaries
330
+ │ └─ Compute metrics
331
+
332
+ ├─ OptimizationService
333
+ │ ├─ Genetic Algorithm
334
+ │ ├─ Population management
335
+ │ ├─ Fitness evaluation
336
+ │ └─ Layout generation
337
+
338
+ ├─ ChatService
339
+ │ ├─ Message analysis
340
+ │ ├─ Response generation
341
+ │ └─ Context management
342
+
343
+ ├─ GeminiService
344
+ │ ├─ API communication
345
+ │ ├─ Prompt engineering
346
+ │ └─ Error handling
347
+
348
+ └─ ExportService
349
+ ├─ DXF document creation
350
+ ├─ Layer management
351
+ └─ File export
352
+
353
+
354
+ Data Layer (Storage & Access)
355
+ ├─ Session Store (In-memory)
356
+ ├─ File System (Export cache)
357
+ └─ External APIs (Gemini, etc.)
358
+
359
+ ---
360
+
361
+ ## **Component 3: Optimization Algorithm (Genetic Algorithm)**
362
+
363
+ ### **Purpose**
364
+
365
+ Generate multiple intelligent layout options that maximize different objectives.
366
+
367
+ ### **How It Works (Conceptual)**
368
+
369
+ **Phase 1: Initialization** \- Create 10 random layout candidates \- Each layout has 8 plots positioned randomly within boundary \- Each plot respects 50m setback rule
370
+
371
+ **Phase 2: Evaluation** \- Calculate fitness score for each layout \- Fitness \= (Profit Score × 0.5) \+ (Compliance Score × 0.3) \+ (Space Efficiency × 0.2) \- Profit \= total plot area (more area \= higher profit) \- Compliance \= 1.0 if all setback rules met, 0.8 if violated \- Space Efficiency \= (used area / total boundary area)
372
+
373
+ **Phase 3: Selection** \- Keep top 3 best performers (elitism) \- Discard bottom 7 layouts
374
+
375
+ **Phase 4: Reproduction** \- Create 7 new layouts from the elite 3 \- New layouts are mutations of elite layouts \- 30% of plots in each new layout are randomly repositioned
376
+
377
+ **Phase 5: Mutation** \- Randomly adjust plot positions (±30 meters) \- Small probability of adding/removing plots \- Ensures genetic diversity
378
+
379
+ **Phase 6: Repeat** \- Run phases 2-5 for 20 generations \- Track best solution from each generation \- Stop if improvement plateaus
380
+
381
+ **Result: Top 3 layouts with different strategies** \- Option 1: Maximum profit (most plots) \- Option 2: Balanced (medium plots, more space) \- Option 3: Premium (fewer plots, larger sizes)
382
+
383
+ ### **Why Genetic Algorithm?**
384
+
385
+ | Approach | Pros | Cons | Used Here? |
386
+ | :---- | :---- | :---- | :---- |
387
+ | Random Search | Simple | Very slow (1000s of tries) | ❌ |
388
+ | Greedy Algorithm | Fast | Gets stuck in local optimum | ❌ |
389
+ | Simulated Annealing | Good for some problems | Limited diversity | ❌ |
390
+ | **Genetic Algorithm** | **Finds diverse solutions** | **Reasonable time** | ✅ |
391
+ | Linear Programming | Optimal solutions | Complex setup | ❌ |
392
+
393
+ ### **Algorithm Parameters (Tuned for Demo)**
394
+
395
+ | Parameter | Value | Reason |
396
+ | :---- | :---- | :---- |
397
+ | Population Size | 10 | Balance speed vs. diversity |
398
+ | Generations | 20 | Enough iterations for convergence |
399
+ | Elite Size | 3 | Keep best performers |
400
+ | Mutation Rate | 0.3 (30%) | Enough randomness for diversity |
401
+ | Target Plots | 8 | Realistic for industrial estates |
402
+ | Setback Distance | 50m | Typical zoning requirement |
403
+
404
+ ---
405
+
406
+ ## **Component 4: AI Chat System**
407
+
408
+ ### **Level 2: Hardcoded Responses**
409
+
410
+ **How it works:** 1\. User asks question 2\. Analyze question keywords 3\. Match to predefined category 4\. Return scripted response
411
+
412
+ **Categories:** \- Layout differences → Explain trade-offs \- Best option → Recommend based on fitness scores \- Compliance questions → Explain setback rules \- Metrics questions → Define each metric \- Algorithm questions → Explain GA process \- Default → Generic helpful response
413
+
414
+ **Advantages:** \- Fast response (no API latency) \- Completely free \- Predictable behavior \- Works offline
415
+
416
+ **Disadvantages:** \- Rigid responses (no true understanding) \- Limited conversational ability \- Can’t handle new question types
417
+
418
+ ### **Level 2+: Real Gemini LLM**
419
+
420
+ **How it works:** 1\. User asks question 2\. Build context from current layouts 3\. Send to Google Gemini API 4\. Get intelligent response 5\. Return response to user 6\. Fall back to hardcoded if API fails
421
+
422
+ **Advantages:** \- Real AI understanding \- Handles unlimited question variations \- Context-aware responses \- Professional appearance
423
+
424
+ **Disadvantages:** \- API latency (1-2 seconds) \- Requires internet connection \- Rate-limited free tier \- Costs money at scale
425
+
426
+ **Gemini Choice Rationale:**
427
+
428
+ | Provider | Cost | Speed | Quality | Integration | Chosen? |
429
+ | :---- | :---- | :---- | :---- | :---- | :---- |
430
+ | OpenAI GPT-4 | $$$ (expensive) | Fast | Best | Simple | ❌ || Anthropic Claude | $$ (moderate) | Slower | Very Good | Simple | ❌ |
431
+ | **Google Gemini** | **FREE** | **Fast** | **Good** | **Simple** | ✅ |
432
+ | Open Source (LLaMA) | Free | Slow | OK | Complex | ❌ |
433
+ | Self-hosted LLM | Free | Slow | OK | Complex | ❌ |
434
+
435
+ **Why Gemini Flash 2.0 specifically:** \- Free tier: 15 requests/minute, 1.5M tokens/day \- Fast: \<1 second response time \- Latest: Up-to-date training (knowledge cutoff 2024\) \- Reliable: Google infrastructure \- Easy: Simple Python library
436
+
437
+ ---
438
+
439
+ ## **Component 5: CAD Export System**
440
+
441
+ ### **Purpose**
442
+
443
+ Create professional, industry-standard CAD files for architects and planners.
444
+
445
+ ### **DXF Format Choice Rationale**
446
+
447
+ | Format | Use Case | Pros | Cons | Chosen? |
448
+ | :---- | :---- | :---- | :---- | :---- |
449
+ | PDF | Drawings, reports | Universal | Not editable | ❌ |
450
+ | **DXF** | **CAD software** | **Universal, editable** | **Old format** | ✅ |
451
+ | SVG | Web graphics | Modern, scalable | Limited CAD support | ❌ |
452
+ | GeoJSON | Geospatial data | Standard, portable | Not CAD format | ❌ |
453
+ | AutoCAD DWG | Professional | Industry standard | Proprietary, paid | ❌ |
454
+
455
+ **DXF Advantages:** \- Open standard (40+ years old) \- Works with all CAD software (AutoCAD, LibreCAD, DraftSight, etc.) \- Works with free online viewers \- Professional appearance \- Contains all necessary information
456
+
457
+ ### **DXF File Structure**
458
+
459
+ DXF Document
460
+ ├─ Header (Version, units, etc.)
461
+ ├─ Layers (Organizational hierarchy)
462
+ │ ├─ BOUNDARY (Site edge \- black, solid)
463
+ │ ├─ SETBACK (50m buffer zone \- red, dashed)
464
+ │ ├─ PLOTS (Individual plots \- cyan, solid)
465
+ │ ├─ LABELS (Plot names P1,P2,etc. \- white)
466
+ │ ├─ ANNOTATIONS (Area labels 1200m² \- yellow)
467
+ │ └─ TITLEBLOCK (Metadata \- black)
468
+ ├─ Entities (Drawing elements)
469
+ │ ├─ Polylines (Plot boundaries)
470
+ │ ├─ Circles (Reference points)
471
+ │ ├─ Text (Labels and annotations)
472
+ │ └─ Lines (Grid, dimensions)
473
+ └─ Blocks (Reusable components)
474
+
475
+ ### **Export Options**
476
+
477
+ **Option 1: Single Layout Export** \- Download individual DXF file \- Filename: option\_1\_20251204\_123456.dxf \- \~50-100KB file size \- Immediate download
478
+
479
+ **Option 2: All Layouts ZIP** \- Download ZIP containing 3 DXF files \- Filename: layouts\_20251204\_123456.zip \- \~150-300KB total \- Immediate download \- User extracts to get individual files
480
+
481
+ ### **Why ezdxf Library?**
482
+
483
+ | Library | Purpose | Pros | Cons | Chosen? |
484
+ | :---- | :---- | :---- | :---- | :---- |
485
+ | **ezdxf** | **DXF creation** | **Complete, Python native** | **Not for plotting** | ✅ |
486
+ | pyDXF | DXF creation | Lightweight | Limited features | ❌ |
487
+ | CADQuery | CAD design | Parametric | Heavy (depends on OpenCASCADE) | ❌ |
488
+ | GDAL | Geospatial I/O | Comprehensive | Complex | ❌ |
489
+ | Inkscape | Vector graphics | GUI-based | Not programmable | ❌ |
490
+
491
+ **ezdxf Advantages:** \- Pure Python (no native dependencies) \- Complete DXF R2010 support \- Easy layer management \- Good performance \- Well-documented \- Active maintenance
492
+
493
+ ---
494
+
495
+ # **🔄 COMPLETE WORKFLOW (USER PERSPECTIVE)**
496
+
497
+ ## **User Journey \- Step by Step**
498
+
499
+ ### **Step 1: Upload Site Boundary**
500
+
501
+ User Action: Click "Upload" button and select GeoJSON file
502
+
503
+ Frontend: Read file using FileReader API
504
+
505
+ Frontend: Send to backend /api/upload-boundary endpoint
506
+
507
+ Backend: Parse GeoJSON
508
+ \- Extract coordinates
509
+ \- Create Shapely Polygon
510
+ \- Validate geometry (is\_valid check)
511
+ \- Create session with UUID
512
+ \- Store in memory
513
+
514
+ Backend: Return session\_id \+ boundary coordinates \+ metadata
515
+
516
+ Frontend: Store session\_id in React state
517
+
518
+ Frontend: Extract boundary coordinates
519
+
520
+ Frontend: Render on 2D canvas using Konva
521
+ \- Create Polygon shape
522
+ \- Set scale to fit canvas
523
+ \- Render with black line (1px)
524
+ \- Add reference grid
525
+
526
+ User Sees: 2D plot of site boundary with dimensions (area, perimeter)
527
+
528
+ ### **Step 2: Generate Optimized Layouts**
529
+
530
+ User Action: Click "Generate Layouts" button
531
+
532
+ Frontend: Make POST request to /api/generate-layouts with session\_id
533
+
534
+ Backend: Retrieve session using session\_id
535
+
536
+ Backend: Initialize Genetic Algorithm
537
+ \- Create population of 10 random layouts
538
+ \- Each layout has 8 plots
539
+ \- Each plot respects 50m setback
540
+
541
+ Backend: Run GA evolution loop (20 generations)
542
+ For each generation:
543
+ 1\. Evaluate fitness of all 10 layouts
544
+ 2\. Select top 3 (elitism)
545
+ 3\. Create 7 new layouts from elite (mutation)
546
+ 4\. Replace population
547
+
548
+ Backend: Extract top 3 final layouts
549
+
550
+ Backend: Calculate metrics for each layout
551
+ \- Total plots count
552
+ \- Total area (sum of plot areas)
553
+ \- Average plot size
554
+ \- Fitness score
555
+
556
+ Backend: Return options array with plot data \+ metrics
557
+
558
+ Frontend: Receive options data
559
+
560
+ Frontend: Render 3 option cards
561
+ \- Each card shows option name (Option 1/2/3)
562
+ \- Display icon (💰/⚖️/🏢)
563
+ \- Show metrics (plots, area, avg, fitness)
564
+ \- Show compliance status (PASS)
565
+
566
+ User Sees: 3 layout options with different characteristics
567
+
568
+ ### **Step 3: Ask Chat Questions**
569
+
570
+ User Action: Type question in chat input, press Enter
571
+
572
+ Frontend: Add user message to message history
573
+
574
+ Frontend: Send POST to /api/chat with session\_id \+ message
575
+
576
+ Backend: Receive question \+ session data
577
+
578
+ Backend: Check if Gemini API available
579
+ ├─ YES: Call GeminiService
580
+ │ \- Build context from current layouts
581
+ │ \- Create prompt with system instructions
582
+ │ \- Send to Google Gemini API
583
+ │ \- Get response
584
+ │ \- Return with model="gemini-2.0-flash"
585
+
586
+ └─ NO: Use fallback ChatService
587
+ \- Analyze question keywords
588
+ \- Match to category
589
+ \- Return scripted response
590
+ \- Return with model="fallback"
591
+
592
+ Frontend: Receive response \+ model type
593
+
594
+ Frontend: Add assistant message to chat
595
+
596
+ Frontend: Display model indicator badge
597
+ \- "🤖 Powered by Gemini" if real AI
598
+ \- "💬 Fallback Mode" if hardcoded
599
+
600
+ Frontend: Auto-scroll to show latest message
601
+
602
+ User Sees: AI response explaining the layouts
603
+
604
+ ### **Step 4: Export to CAD**
605
+
606
+ User Action: Click "Option 1 DXF" button
607
+
608
+ Frontend: Make POST to /api/export-dxf with session\_id \+ option\_id
609
+
610
+ Backend: Retrieve layout from session
611
+
612
+ Backend: Call DXFExportService
613
+ ├─ Create new DXF document
614
+ ├─ Setup layers (BOUNDARY, SETBACK, PLOTS, etc.)
615
+ ├─ Draw site boundary polygon
616
+ ├─ Draw 50m setback zone
617
+ ├─ Draw each plot rectangle
618
+ ├─ Add plot labels (P1, P2, etc.)
619
+ ├─ Add area annotations
620
+ ├─ Add title block with metadata
621
+ └─ Save to temporary file
622
+
623
+ Backend: Stream DXF file to frontend as blob
624
+
625
+ Frontend: Create blob from response
626
+
627
+ Frontend: Create temporary download link
628
+
629
+ Frontend: Trigger browser download
630
+ \- Filename: option\_1\_20251204\_123456.dxf
631
+ \- MIME type: application/x-autocad-dxf
632
+
633
+ Browser: Downloads file to user's Downloads folder
634
+
635
+ User Can: Open in AutoCAD, LibreCAD, or online viewers
636
+
637
+ ### **Step 5: Export All as ZIP**
638
+
639
+ User Action: Click "Export All as ZIP" button
640
+
641
+ Frontend: Make POST to /api/export-all-dxf with session\_id
642
+
643
+ Backend: Get all 3 layouts from session
644
+
645
+ Backend: For each layout:
646
+ \- Call DXFExportService
647
+ \- Generate DXF file
648
+ \- Add to ZIP archive
649
+
650
+ Backend: Create ZIP file containing 3 DXF files
651
+
652
+ Backend: Stream ZIP to frontend
653
+
654
+ Frontend: Trigger browser download
655
+ \- Filename: layouts\_20251204\_123456.zip
656
+ \- MIME type: application/zip
657
+
658
+ User Can: Unzip and open each DXF in CAD software
659
+
660
+ ---
661
+
662
+ # **🛠️ COMPLETE TECHNOLOGY STACK**
663
+
664
+ ## **Frontend Stack**
665
+
666
+ ### **Core Framework**
667
+
668
+ * **React 18** \- UI library
669
+
670
+ * Component-based architecture
671
+
672
+ * Virtual DOM optimization
673
+
674
+ * Hooks for state management
675
+
676
+ * Functional components
677
+
678
+ * **TypeScript 5** \- Type safety
679
+
680
+ * Type checking at compile time
681
+
682
+ * Better IDE support
683
+
684
+ * Self-documenting
685
+
686
+ * Catches errors early
687
+
688
+ ### **UI & Visualization**
689
+
690
+ * **Konva.js** \- 2D Canvas library
691
+
692
+ * Stage (canvas container)
693
+
694
+ * Layers (grouping elements)
695
+
696
+ * Shapes (Polygon, Rect, Text)
697
+
698
+ * Event handling
699
+
700
+ * Performance optimization
701
+
702
+ * **Lucide React** \- Icon library
703
+
704
+ * Upload, Download, Zap, MessageCircle icons
705
+
706
+ * Lightweight (SVG-based)
707
+
708
+ * Consistent styling
709
+
710
+ * **CSS/Styling**
711
+
712
+ * Inline styles (React style objects)
713
+
714
+ * Tailwind CSS (optional)
715
+
716
+ * CSS Flexbox/Grid for layout
717
+
718
+ * Responsive design media queries
719
+
720
+ ### **Data & Communication**
721
+
722
+ * **Axios** \- HTTP client
723
+
724
+ * REST API calls
725
+
726
+ * Request/response handling
727
+
728
+ * Error handling
729
+
730
+ * Request interceptors
731
+
732
+ * **React Hooks** \- State management
733
+
734
+ * useState (component state)
735
+
736
+ * useEffect (side effects)
737
+
738
+ * useRef (direct DOM access)
739
+
740
+ * useCallback (memoization)
741
+
742
+ ### **Build & Development**
743
+
744
+ * **Create React App** \- Build tool
745
+
746
+ * Webpack configuration
747
+
748
+ * Babel transpiling
749
+
750
+ * Development server
751
+
752
+ * Production optimization
753
+
754
+ * **npm** \- Package manager
755
+
756
+ * Dependency management
757
+
758
+ * Version control
759
+
760
+ * Scripts execution
761
+
762
+ ### **Browser APIs Used**
763
+
764
+ * **FileReader API** \- File upload handling
765
+
766
+ * **Fetch API** / Axios \- HTTP requests
767
+
768
+ * **Blob API** \- File downloads
769
+
770
+ * **LocalStorage** \- Session persistence (optional)
771
+
772
+ ---
773
+
774
+ ## **Backend Stack**
775
+
776
+ ### **Core Framework**
777
+
778
+ * **FastAPI** \- Web framework
779
+
780
+ * ASGI (async support)
781
+
782
+ * Automatic API documentation
783
+
784
+ * Request validation (Pydantic)
785
+
786
+ * Type hints integration
787
+
788
+ * Middleware support
789
+
790
+ * **Python 3.8+** \- Language
791
+
792
+ * Type hints
793
+
794
+ * Async/await support
795
+
796
+ * Rich ecosystem
797
+
798
+ * Easy deployment
799
+
800
+ ### **Geospatial & Geometry**
801
+
802
+ * **Shapely** \- Geometry operations
803
+
804
+ * Polygon creation from coordinates
805
+
806
+ * Buffer operations (setback zones)
807
+
808
+ * Geometry validation
809
+
810
+ * Intersection/containment checks
811
+
812
+ * Distance calculations
813
+
814
+ * **NumPy** \- Numerical computing
815
+
816
+ * Array operations
817
+
818
+ * Mathematical functions
819
+
820
+ * Random number generation
821
+
822
+ * Statistical calculations
823
+
824
+ * **GeoJSON** \- Data format
825
+
826
+ * Standard geospatial format
827
+
828
+ * JSON-based
829
+
830
+ * Supported by most GIS tools
831
+
832
+ * Web-friendly
833
+
834
+ ### **Optimization**
835
+
836
+ * **Genetic Algorithm** (custom implementation)
837
+
838
+ * Population management
839
+
840
+ * Fitness calculation
841
+
842
+ * Selection operators
843
+
844
+ * Crossover/mutation
845
+
846
+ * Convergence detection
847
+
848
+ ### **AI & LLM**
849
+
850
+ * **google-generativeai** \- Gemini API client
851
+
852
+ * Chat completion
853
+
854
+ * Context window management
855
+
856
+ * Token counting
857
+
858
+ * Error handling
859
+
860
+ ### **CAD & Export**
861
+
862
+ * **ezdxf** \- DXF file creation
863
+
864
+ * Document creation
865
+
866
+ * Layer management
867
+
868
+ * Entity creation (polylines, text)
869
+
870
+ * Attributes and styling
871
+
872
+ * File output
873
+
874
+ ### **Utilities**
875
+
876
+ * **python-multipart** \- File upload handling
877
+
878
+ * **python-dotenv** \- Environment variables (.env)
879
+
880
+ * **uvicorn** \- ASGI server
881
+
882
+ * Production-ready
883
+
884
+ * Hot reload (development)
885
+
886
+ * Multiple worker support
887
+
888
+ ### **Package Management**
889
+
890
+ * **pip** \- Python package manager
891
+
892
+ * **requirements.txt** \- Dependency specification
893
+
894
+ * **Virtual environment** \- Isolation
895
+
896
+ ---
897
+
898
+ ## **DevOps & Infrastructure**
899
+
900
+ ### **Development**
901
+
902
+ * **Local Development Server**
903
+
904
+ * Backend: uvicorn (localhost:8000)
905
+
906
+ * Frontend: npm (localhost:3000)
907
+
908
+ * CORS enabled for local testing
909
+
910
+ ### **Version Control**
911
+
912
+ * **Git** \- Source control
913
+
914
+ * Code tracking
915
+
916
+ * Collaboration
917
+
918
+ * Version history
919
+
920
+ * **GitHub** \- Repository hosting
921
+
922
+ * Remote backup
923
+
924
+ * CI/CD integration
925
+
926
+ * Collaboration features
927
+
928
+ ### **Deployment Targets**
929
+
930
+ #### *Frontend Deployment*
931
+
932
+ * **Vercel** (recommended for React)
933
+
934
+ * Git integration
935
+
936
+ * Automatic deployments
937
+
938
+ * Global CDN
939
+
940
+ * Environment variables
941
+
942
+ * Free tier available
943
+
944
+ * **Netlify** (alternative)
945
+
946
+ * Similar features
947
+
948
+ * Lambda functions (optional)
949
+
950
+ * Form handling
951
+
952
+ #### *Backend Deployment*
953
+
954
+ * **Railway** (recommended for Python)
955
+
956
+ * Docker support
957
+
958
+ * Git integration
959
+
960
+ * Automatic deployments
961
+
962
+ * PostgreSQL addon available
963
+
964
+ * Free tier available
965
+
966
+ * **Heroku** (alternative)
967
+
968
+ * Python support
969
+
970
+ * Addons (database, etc.)
971
+
972
+ * Procfile configuration
973
+
974
+ * Paid only
975
+
976
+ * **AWS / Google Cloud / Azure**
977
+
978
+ * More complex setup
979
+
980
+ * More control
981
+
982
+ * Pay-as-you-go pricing
983
+
984
+ * Enterprise scale
985
+
986
+ ### **Database (Future Enhancement)**
987
+
988
+ * **PostgreSQL** \- Relational database
989
+
990
+ * Project persistence
991
+
992
+ * User data storage
993
+
994
+ * PostGIS extension (geospatial queries)
995
+
996
+ * **Redis** \- Caching (optional)
997
+
998
+ * Session caching
999
+
1000
+ * Job queue
1001
+
1002
+ * Rate limiting
1003
+
1004
+ ---
1005
+
1006
+ # **📋 SYSTEM REQUIREMENTS & SPECIFICATIONS**
1007
+
1008
+ ## **Frontend Requirements**
1009
+
1010
+ ### **Browser Compatibility**
1011
+
1012
+ * Chrome 90+
1013
+
1014
+ * Firefox 88+
1015
+
1016
+ * Safari 14+
1017
+
1018
+ * Edge 90+
1019
+
1020
+ ### **Minimum System Specs**
1021
+
1022
+ * 1GB RAM
1023
+
1024
+ * Modern CPU (2010+)
1025
+
1026
+ * 50MB disk space
1027
+
1028
+ * Broadband internet (2+ Mbps)
1029
+
1030
+ ### **Screen Resolutions Supported**
1031
+
1032
+ * Desktop: 1024x768 minimum (1920x1080 optimal)
1033
+
1034
+ * Tablet: 768x1024 minimum
1035
+
1036
+ * Mobile: 320x480 (basic support)
1037
+
1038
+ ### **Network Requirements**
1039
+
1040
+ * HTTPS for production
1041
+
1042
+ * CORS enabled
1043
+
1044
+ * WebSocket support (optional, for future features)
1045
+
1046
+ ---
1047
+
1048
+ ## **Backend Requirements**
1049
+
1050
+ ### **Server Specs (Minimum)**
1051
+
1052
+ * **CPU**: 1 core
1053
+
1054
+ * **RAM**: 512MB
1055
+
1056
+ * **Disk**: 2GB
1057
+
1058
+ * **Network**: Broadband (10+ Mbps)
1059
+
1060
+ ### **Python Version**
1061
+
1062
+ * 3.8+ required
1063
+
1064
+ * 3.10+ recommended
1065
+
1066
+ ### **Operating System**
1067
+
1068
+ * Linux (production)
1069
+
1070
+ * macOS (development)
1071
+
1072
+ * Windows 10+ (development)
1073
+
1074
+ ### **Dependencies**
1075
+
1076
+ * FastAPI
1077
+
1078
+ * Shapely
1079
+
1080
+ * NumPy
1081
+
1082
+ * google-generativeai
1083
+
1084
+ * ezdxf
1085
+
1086
+ * python-multipart
1087
+
1088
+ * python-dotenv
1089
+
1090
+ * uvicorn
1091
+
1092
+ ---
1093
+
1094
+ # **🔐 SECURITY CONSIDERATIONS**
1095
+
1096
+ ## **Frontend Security**
1097
+
1098
+ ### **File Upload Security**
1099
+
1100
+ * Validate file type (only .geojson, .json)
1101
+
1102
+ * Limit file size (5MB maximum)
1103
+
1104
+ * No executable file types
1105
+
1106
+ * Scan for malicious content (optional)
1107
+
1108
+ ### **API Communication**
1109
+
1110
+ * Use HTTPS only
1111
+
1112
+ * CORS validation
1113
+
1114
+ * Input validation before sending
1115
+
1116
+ * Sanitize displayed content
1117
+
1118
+ ### **Data Privacy**
1119
+
1120
+ * No sensitive data stored locally
1121
+
1122
+ * Use httpOnly cookies (if session tokens used)
1123
+
1124
+ * Clear session on logout
1125
+
1126
+ * Implement CSP headers
1127
+
1128
+ ---
1129
+
1130
+ ## **Backend Security**
1131
+
1132
+ ### **Input Validation**
1133
+
1134
+ * Validate GeoJSON format
1135
+
1136
+ * Check coordinate bounds
1137
+
1138
+ * Validate session IDs
1139
+
1140
+ * Check file paths (prevent directory traversal)
1141
+
1142
+ ### **API Security**
1143
+
1144
+ * Rate limiting (to prevent abuse)
1145
+
1146
+ * CORS restrictions (whitelist allowed origins)
1147
+
1148
+ * Input sanitization
1149
+
1150
+ * Error handling (no sensitive info in errors)
1151
+
1152
+ ### **Authentication (Future)**
1153
+
1154
+ * API keys for external access
1155
+
1156
+ * User authentication (OAuth2/JWT)
1157
+
1158
+ * Role-based access control
1159
+
1160
+ * Audit logging
1161
+
1162
+ ### **AI API Security**
1163
+
1164
+ * Store GEMINI\_API\_KEY in environment variables
1165
+
1166
+ * Never commit keys to Git
1167
+
1168
+ * Rotate keys periodically
1169
+
1170
+ * Monitor API usage
1171
+
1172
+ * Set spending limits
1173
+
1174
+ ### **File Handling**
1175
+
1176
+ * Validate DXF file paths
1177
+
1178
+ * Use secure temporary directories
1179
+
1180
+ * Auto-delete old export files
1181
+
1182
+ * Limit export directory size
1183
+
1184
+ ---
1185
+
1186
+ # **⚡ PERFORMANCE OPTIMIZATION STRATEGIES**
1187
+
1188
+ ## **Frontend Optimization**
1189
+
1190
+ ### **Code Splitting**
1191
+
1192
+ * Lazy load components
1193
+
1194
+ * Code splitting by route
1195
+
1196
+ * Dynamic imports for heavy libraries
1197
+
1198
+ ### **Asset Optimization**
1199
+
1200
+ * Minify JavaScript/CSS
1201
+
1202
+ * Compress images
1203
+
1204
+ * Use WebP format
1205
+
1206
+ * Cache static assets
1207
+
1208
+ ### **Rendering Optimization**
1209
+
1210
+ * Memoize expensive components
1211
+
1212
+ * Virtual scrolling for large lists
1213
+
1214
+ * Debounce resize events
1215
+
1216
+ * Optimize Konva rendering
1217
+
1218
+ ### **Bundle Size**
1219
+
1220
+ * Tree-shaking unused code
1221
+
1222
+ * Remove development dependencies
1223
+
1224
+ * Use production builds
1225
+
1226
+ * Monitor with webpack-bundle-analyzer
1227
+
1228
+ ---
1229
+
1230
+ ## **Backend Optimization**
1231
+
1232
+ ### **Algorithm Optimization**
1233
+
1234
+ * GA parameters tuned for performance
1235
+
1236
+ * Early termination if converged
1237
+
1238
+ * Parallel population evaluation (optional)
1239
+
1240
+ * Caching of fitness calculations
1241
+
1242
+ ### **API Optimization**
1243
+
1244
+ * Pagination for large responses
1245
+
1246
+ * Compression (gzip)
1247
+
1248
+ * Caching headers
1249
+
1250
+ * Connection pooling
1251
+
1252
+ ### **Memory Optimization**
1253
+
1254
+ * Session cleanup (remove old sessions)
1255
+
1256
+ * Stream large file downloads
1257
+
1258
+ * Limit file size
1259
+
1260
+ * Garbage collection tuning
1261
+
1262
+ ### **Database Optimization (Future)**
1263
+
1264
+ * Indexes on frequently queried fields
1265
+
1266
+ * Query optimization
1267
+
1268
+ * Connection pooling
1269
+
1270
+ * Replication for redundancy
1271
+
1272
+ ---
1273
+
1274
+ # **🔄 DATA FLOW & STATE MANAGEMENT**
1275
+
1276
+ ## **Frontend State Management**
1277
+
1278
+ ### **React State Hierarchy**
1279
+
1280
+ App (Root)
1281
+ ├─ sessionId (string, UUID)
1282
+ ├─ boundary (GeoJSON polygon)
1283
+ ├─ options (array of layout options)
1284
+ ├─ siteMetadata (object: area, perimeter)
1285
+ ├─ messages (array of chat messages)
1286
+ ├─ loading (boolean, for loading states)
1287
+ └─ errors (array of error messages)
1288
+
1289
+ ### **State Updates**
1290
+
1291
+ User Action
1292
+
1293
+ Event Handler (onClick, onChange, etc.)
1294
+
1295
+ Call setState or useReducer
1296
+
1297
+ Trigger re-render of affected components
1298
+
1299
+ Virtual DOM diff
1300
+
1301
+ Update actual DOM
1302
+
1303
+ Display changes to user
1304
+
1305
+ ---
1306
+
1307
+ ## **Backend Session Management**
1308
+
1309
+ ### **Session Lifecycle**
1310
+
1311
+ User Uploads File
1312
+
1313
+ Backend creates Session object
1314
+ \- Generate UUID
1315
+ \- Store in memory dictionary
1316
+ \- Initialize with empty data
1317
+
1318
+ Return session\_id to frontend
1319
+
1320
+ Frontend stores session\_id in state
1321
+
1322
+ All subsequent requests include session\_id
1323
+
1324
+ Backend retrieves session from dictionary
1325
+
1326
+ Add/update session data (layouts, metadata)
1327
+
1328
+ Session remains available for 24 hours (optional cleanup)
1329
+
1330
+ User closes browser/session expires
1331
+
1332
+ Backend periodically cleans up old sessions
1333
+
1334
+ ---
1335
+
1336
+ # **📊 INTEGRATION POINTS & DEPENDENCIES**
1337
+
1338
+ ## **External Services**
1339
+
1340
+ ### **Google Gemini API**
1341
+
1342
+ * **Purpose**: Real AI chat responses
1343
+
1344
+ * **Integration**: google-generativeai Python library
1345
+
1346
+ * **Authentication**: API key in environment variable
1347
+
1348
+ * **Rate Limits**: 15 requests/minute (free tier)
1349
+
1350
+ * **Fallback**: Use hardcoded responses if unavailable
1351
+
1352
+ ### **GeoJSON Input**
1353
+
1354
+ * **Source**: User file upload
1355
+
1356
+ * **Format**: RFC 7946 standard
1357
+
1358
+ * **Validation**: Shapely geometry checks
1359
+
1360
+ * **Expected Data**: Polygon geometry (site boundary)
1361
+
1362
+ ### **File System**
1363
+
1364
+ * **Purpose**: Store temporary export files
1365
+
1366
+ * **Location**: backend/exports/ directory
1367
+
1368
+ * **Cleanup**: Remove files older than 24 hours
1369
+
1370
+ * **Permissions**: Read/write/delete
1371
+
1372
+ ---
1373
+
1374
+ # **📈 SCALABILITY & GROWTH PATH**
1375
+
1376
+ ## **Current System (Single Server)**
1377
+
1378
+ Frontend (Vercel CDN)
1379
+ ↓ HTTPS
1380
+ Backend (Single Railway container)
1381
+ └─ All processing
1382
+ └─ In-memory session storage
1383
+ └─ Temporary file storage
1384
+
1385
+ ### **Limitations**
1386
+
1387
+ * \~100 concurrent sessions
1388
+
1389
+ * \~1000 requests/minute
1390
+
1391
+ * Data lost on restart
1392
+
1393
+ * No redundancy
1394
+
1395
+ ---
1396
+
1397
+ ## **Future: Scalable Architecture**
1398
+
1399
+ External
1400
+ Services
1401
+ (Gemini)
1402
+
1403
+ User (Browser) │
1404
+ ↓ HTTPS │
1405
+ └─ Vercel CDN ←──────────────────┬─┘
1406
+
1407
+ Load Balancer
1408
+
1409
+ ┌───────────┼───────────┐
1410
+ ↓ ↓ ↓
1411
+ Backend Backend Backend
1412
+ Container Container Container
1413
+ ↓ ↓ ↓
1414
+ └───────────┼───────────┘
1415
+
1416
+ PostgreSQL
1417
+ (Persistent)
1418
+
1419
+ Redis Cache
1420
+ (Session, GA cache)
1421
+
1422
+ ### **Improvements**
1423
+
1424
+ * Horizontal scaling (multiple backend containers)
1425
+
1426
+ * Database persistence (PostgreSQL)
1427
+
1428
+ * Session caching (Redis)
1429
+
1430
+ * Load balancing
1431
+
1432
+ * Monitoring & logging (Datadog, New Relic)
1433
+
1434
+ * CDN for static files
1435
+
1436
+ * API gateway
1437
+
1438
+ ---
1439
+
1440
+ # **🎯 DEPLOYMENT STRATEGY**
1441
+
1442
+ ## **Development Environment**
1443
+
1444
+ Local Machine
1445
+ ├─ Backend: localhost:8000 (uvicorn \--reload)
1446
+ ├─ Frontend: localhost:3000 (npm start)
1447
+ ├─ CORS: Localhost only
1448
+ ├─ Database: None (in-memory)
1449
+ └─ Logging: Console
1450
+
1451
+ ## **Staging Environment**
1452
+
1453
+ Staging Server (Railway/AWS)
1454
+ ├─ Backend: staging-api.aioptimize.com
1455
+ ├─ Frontend: staging.aioptimize.com
1456
+ ├─ CORS: Staging domain only
1457
+ ├─ Database: PostgreSQL (optional)
1458
+ └─ Logging: Structured logging service
1459
+
1460
+ ## **Production Environment**
1461
+
1462
+ Production Server (Railway/AWS/Google Cloud)
1463
+ ├─ Backend: api.aioptimize.com
1464
+ ├─ Frontend: app.aioptimize.com (Vercel CDN)
1465
+ ├─ CORS: Production domains only
1466
+ ├─ Database: PostgreSQL with backups
1467
+ ├─ Logging: Enterprise logging (Datadog)
1468
+ ├─ Monitoring: Performance monitoring
1469
+ ├─ Alerting: Email/Slack notifications
1470
+ └─ Backup: Daily automated backups
1471
+
1472
+ ---
1473
+
1474
+ # **📊 MONITORING & OBSERVABILITY**
1475
+
1476
+ ## **Metrics to Track (Current)**
1477
+
1478
+ * API response times
1479
+
1480
+ * Error rates by endpoint
1481
+
1482
+ * Session count
1483
+
1484
+ * Gemini API latency
1485
+
1486
+ * File export success rate
1487
+
1488
+ * User download counts
1489
+
1490
+ ## **Metrics to Track (Future)**
1491
+
1492
+ * User engagement
1493
+
1494
+ * Conversion funnel
1495
+
1496
+ * Cost per session
1497
+
1498
+ * GA optimization efficiency
1499
+
1500
+ * User satisfaction (feedback)
1501
+
1502
+ ## **Logging Strategy**
1503
+
1504
+ * Info level: Major operations
1505
+
1506
+ * Warning level: Non-critical errors
1507
+
1508
+ * Error level: Critical failures
1509
+
1510
+ * Debug level: Development only
1511
+
1512
+ ---
1513
+
1514
+ # **🔍 ERROR HANDLING & RESILIENCE**
1515
+
1516
+ ## **Error Categories & Handling**
1517
+
1518
+ ### **User Input Errors**
1519
+
1520
+ * Invalid GeoJSON → User-friendly message
1521
+
1522
+ * Missing file → Prompt to upload
1523
+
1524
+ * Invalid coordinates → Suggest bounds
1525
+
1526
+ ### **API Errors**
1527
+
1528
+ * Gemini API timeout → Use fallback chat
1529
+
1530
+ * Rate limit exceeded → Queue message or inform user
1531
+
1532
+ * Network error → Retry with exponential backoff
1533
+
1534
+ ### **System Errors**
1535
+
1536
+ * Out of memory → Reject large file
1537
+
1538
+ * File system full → Clean up old exports
1539
+
1540
+ * Database connection → Use in-memory fallback
1541
+
1542
+ ### **Graceful Degradation**
1543
+
1544
+ Gemini AI Available?
1545
+ ├─ YES → Use real AI
1546
+ └─ NO → Use hardcoded responses (system still works)
1547
+
1548
+ DXF Export Available?
1549
+ ├─ YES → Generate professional CAD
1550
+ └─ NO → Return JSON alternative
1551
+
1552
+ Database Available?
1553
+ ├─ YES → Persist to database
1554
+ └─ NO → Use in-memory storage
1555
+
1556
+ ---
1557
+
1558
+ # **✅ QUALITY ASSURANCE STRATEGY**
1559
+
1560
+ ## **Testing Levels**
1561
+
1562
+ ### **Unit Testing**
1563
+
1564
+ * Test individual functions
1565
+
1566
+ * Test geometry operations
1567
+
1568
+ * Test GA fitness calculations
1569
+
1570
+ * Test response generation logic
1571
+
1572
+ ### **Integration Testing**
1573
+
1574
+ * Test API endpoints
1575
+
1576
+ * Test frontend-backend communication
1577
+
1578
+ * Test file upload flow
1579
+
1580
+ * Test export generation
1581
+
1582
+ ### **End-to-End Testing**
1583
+
1584
+ * Complete user workflows
1585
+
1586
+ * Multi-step scenarios
1587
+
1588
+ * Error recovery
1589
+
1590
+ * Performance under load
1591
+
1592
+ ### **Performance Testing**
1593
+
1594
+ * Load testing (concurrent users)
1595
+
1596
+ * Stress testing (resource limits)
1597
+
1598
+ * Latency testing (response times)
1599
+
1600
+ * Scalability testing
1601
+
1602
+ ---
1603
+
1604
+ # **📋 IMPLEMENTATION CHECKLIST**
1605
+
1606
+ ## **Phase 1: Level 2 Smart Demo (12 hours)**
1607
+
1608
+ ### **Frontend**
1609
+
1610
+ * ☐ React \+ TypeScript setup
1611
+
1612
+ * ☐ Component architecture designed
1613
+
1614
+ * ☐ File upload UI implemented
1615
+
1616
+ * ☐ 2D Konva canvas integrated
1617
+
1618
+ * ☐ Layout options display created
1619
+
1620
+ * ☐ Chat UI built
1621
+
1622
+ * ☐ Styling finalized
1623
+
1624
+ ### **Backend**
1625
+
1626
+ * ☐ FastAPI project initialized
1627
+
1628
+ * ☐ Virtual environment created
1629
+
1630
+ * ☐ Dependencies installed
1631
+
1632
+ * ☐ API endpoints designed
1633
+
1634
+ * ☐ GeoJSON parsing implemented
1635
+
1636
+ * ☐ Genetic algorithm coded
1637
+
1638
+ * ☐ Chat logic implemented
1639
+
1640
+ * ☐ Error handling added
1641
+
1642
+ ### **Integration**
1643
+
1644
+ * ☐ CORS enabled
1645
+
1646
+ * ☐ Frontend connects to backend
1647
+
1648
+ * ☐ File upload works end-to-end
1649
+
1650
+ * ☐ Layout generation works
1651
+
1652
+ * ☐ Chat responds
1653
+
1654
+ * ☐ No console errors
1655
+
1656
+ ### **Testing**
1657
+
1658
+ * ☐ Manual workflow testing
1659
+
1660
+ * ☐ Error case testing
1661
+
1662
+ * ☐ Performance verified
1663
+
1664
+ * ☐ Browser compatibility checked
1665
+
1666
+ ---
1667
+
1668
+ ## **Phase 2: Level 2+ Enhancements (12 hours)**
1669
+
1670
+ ### **Gemini Integration**
1671
+
1672
+ * ☐ API key obtained
1673
+
1674
+ * ☐ google-generativeai library installed
1675
+
1676
+ * ☐ GeminiService class created
1677
+
1678
+ * ☐ Context building implemented
1679
+
1680
+ * ☐ Fallback mechanism tested
1681
+
1682
+ * ☐ Badge indicator added
1683
+
1684
+ * ☐ Real responses verified
1685
+
1686
+ ### **DXF Export**
1687
+
1688
+ * ☐ ezdxf library installed
1689
+
1690
+ * ☐ DXFExportService class created
1691
+
1692
+ * ☐ Layer setup implemented
1693
+
1694
+ * ☐ Geometry drawing implemented
1695
+
1696
+ * ☐ Title block added
1697
+
1698
+ * ☐ Export endpoints created
1699
+
1700
+ * ☐ UI buttons added
1701
+
1702
+ * ☐ Download mechanism tested
1703
+
1704
+ * ☐ ZIP creation implemented
1705
+
1706
+ * ☐ File opening verified (CAD software)
1707
+
1708
+ ### **Final Testing**
1709
+
1710
+ * ☐ Both features working
1711
+
1712
+ * ☐ No console errors
1713
+
1714
+ * ☐ No backend errors
1715
+
1716
+ * ☐ Complete workflows tested
1717
+
1718
+ * ☐ Performance acceptable
1719
+
1720
+ * ☐ Documentation complete
1721
+
1722
+ * ☐ Code committed to Git
1723
+
1724
+ * ☐ Ready for production
1725
+
1726
+ ---
1727
+
1728
+ # **🚀 DEPLOYMENT READINESS CHECKLIST**
1729
+
1730
+ ## **Pre-Deployment**
1731
+
1732
+ * ☐ All tests passing
1733
+
1734
+ * ☐ Code reviewed
1735
+
1736
+ * ☐ Documentation complete
1737
+
1738
+ * ☐ Security audit done
1739
+
1740
+ * ☐ Performance baseline established
1741
+
1742
+ * ☐ Backup strategy defined
1743
+
1744
+ * ☐ Monitoring setup
1745
+
1746
+ * ☐ Alert rules defined
1747
+
1748
+ ## **Deployment**
1749
+
1750
+ * ☐ Frontend deployed (Vercel)
1751
+
1752
+ * ☐ Backend deployed (Railway)
1753
+
1754
+ * ☐ Environment variables set
1755
+
1756
+ * ☐ API keys secured
1757
+
1758
+ * ☐ CORS configured
1759
+
1760
+ * ☐ HTTPS enforced
1761
+
1762
+ * ☐ DNS configured
1763
+
1764
+ ## **Post-Deployment**
1765
+
1766
+ * ☐ Smoke tests pass
1767
+
1768
+ * ☐ Performance monitoring active
1769
+
1770
+ * ☐ User feedback collected
1771
+
1772
+ * ☐ Error tracking enabled
1773
+
1774
+ * ☐ Incident response plan ready
1775
+
1776
+ * ☐ Runbooks documented
1777
+
1778
+ ---
1779
+
1780
+ **This architecture is battle-tested, production-ready, and designed for rapid iteration and scaling.**
1781
+
1782
+ **Everything is documented, realistic, and achievable.**
1783
+
docs/MVP-STATUS.md ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AIOptimize™ MVP - Final Status Report
2
+
3
+ ## Tổng quan hoàn thành
4
+
5
+ | Component | Status | Details |
6
+ |-----------|--------|---------|
7
+ | **Backend API** | ✅ 100% | 10 endpoints |
8
+ | **Frontend UI** | ✅ 100% | React + Konva |
9
+ | **DXF Input** | ✅ NEW | Parse LWPOLYLINE |
10
+ | **DXF Output** | ✅ Fixed | Export layouts |
11
+ | **Gemini AI** | ✅ Ready | API key configured |
12
+ | **Zoom/Pan** | ✅ NEW | Mouse wheel + drag |
13
+
14
+ ---
15
+
16
+ ## Những gì đã tối ưu
17
+
18
+ ### Frontend
19
+ 1. **Map2DPlotter hoàn toàn mới**
20
+ - Zoom: mouse wheel + buttons
21
+ - Pan: kéo thả canvas
22
+ - Canvas size: 720x500px
23
+ - Transform coordinates chính xác
24
+
25
+ 2. **Layout tối ưu**
26
+ - Left panel: `max-width: 800px`
27
+ - Map section chiếm phần lớn
28
+ - Controls: zoom in/out/reset
29
+
30
+ 3. **CSS**
31
+ - Map controls styling
32
+ - Zoom level indicator
33
+ - Grab cursor khi drag
34
+
35
+ ### Backend
36
+ 1. **Endpoint mới `/api/upload-dxf`**
37
+ - Parse file DXF
38
+ - Extract LWPOLYLINE/POLYLINE
39
+ - Tự động chọn polygon lớn nhất
40
+
41
+ 2. **Gemini API**
42
+ - API key từ `.env`
43
+ - Model: `gemini-2.0-flash-exp`
44
+ - Fallback responses nếu lỗi
45
+
46
+ ---
47
+
48
+ ## Files đã sửa
49
+
50
+ | File | Changes |
51
+ |------|---------|
52
+ | `src/api/mvp_api.py` | +100 lines (upload-dxf) |
53
+ | `frontend/src/components/Map2DPlotter.tsx` | Rewritten (zoom/pan) |
54
+ | `frontend/src/App.tsx` | Map size 720x500 |
55
+ | `frontend/src/App.css` | Map controls CSS |
56
+ | `frontend/src/services/api.ts` | DXF routing |
57
+
58
+ ---
59
+
60
+ ## Build Status
61
+
62
+ ```
63
+ Frontend:
64
+ ✓ 1818 modules transformed
65
+ ✓ built in 4.83s
66
+ - index.js: 562KB (gzip: 178KB)
67
+ - index.css: 9.5KB (gzip: 2.5KB)
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Cách test
73
+
74
+ ```bash
75
+ # Terminal 1 - Backend
76
+ cd d:\Workspace\Project\REMB
77
+ .\venv\Scripts\activate
78
+ uvicorn src.api.mvp_api:app --reload --port 8000
79
+
80
+ # Terminal 2 - Frontend
81
+ cd frontend
82
+ npm run dev
83
+ ```
84
+
85
+ **Test DXF upload:**
86
+ 1. Tạo file DXF với LWPOLYLINE
87
+ 2. Upload qua UI
88
+ 3. Verify boundary hiển thị
89
+ 4. Generate layouts
90
+ 5. Export DXF
91
+
92
+ ---
93
+
94
+ ## API Endpoints
95
+
96
+ | Method | Endpoint | Description |
97
+ |--------|----------|-------------|
98
+ | GET | `/api/health` | Health check |
99
+ | GET | `/api/sample-data` | Sample GeoJSON |
100
+ | POST | `/api/upload-dxf` | **NEW** DXF input |
101
+ | POST | `/api/upload-boundary-json` | JSON input |
102
+ | POST | `/api/generate-layouts` | GA optimizer |
103
+ | POST | `/api/chat` | Gemini AI |
104
+ | POST | `/api/export-dxf` | Single DXF |
105
+ | POST | `/api/export-all-dxf` | ZIP all layouts |
106
+
107
+ ---
108
+
109
+ ## Gemini API
110
+
111
+ ```
112
+ Model: gemini-2.0-flash-exp
113
+ Key: Configured in .env
114
+ Status: Ready
115
+ ```
116
+
117
+ Test chat với các câu hỏi:
118
+ - "Which option is best?"
119
+ - "Compare the layouts"
120
+ - "How does the algorithm work?"
docs/Proposal_ AI-Powered Industrial Estate Master Plan Optimization Engine.md ADDED
The diff for this file is too large to render. See raw diff
 
docs/Requirement.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Tóm tắt "Tech Stack" mã nguồn mở cho MVP khuyến nghị:
2
+
3
+ Ngôn ngữ: Python 3.10+
4
+ Tối ưu hóa (AI): Pymoo (NSGA-II) + Google OR-Tools (dùng solver HiGHS cho MILP).
5
+ Xử lý hình học: GeoPandas + Shapely.
6
+ Xuất bản vẽ: ezdxf.
7
+ Backend/API: FastAPI.
8
+ Database: PostgreSQL + PostGIS.
examples/api-cw750-details.dxf ADDED
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
frontend/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.13.2",
14
+ "konva": "^10.0.12",
15
+ "lucide-react": "^0.555.0",
16
+ "react": "^19.2.0",
17
+ "react-dom": "^19.2.0",
18
+ "react-konva": "^19.2.1"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.39.1",
22
+ "@types/node": "^24.10.1",
23
+ "@types/react": "^19.2.5",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^5.1.1",
26
+ "eslint": "^9.39.1",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.4.24",
29
+ "globals": "^16.5.0",
30
+ "typescript": "~5.9.3",
31
+ "typescript-eslint": "^8.46.4",
32
+ "vite": "^7.2.4"
33
+ }
34
+ }
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AIOptimize™ MVP - Styles
3
+ * Modern, premium design system
4
+ */
5
+
6
+ /* === CSS Variables === */
7
+ :root {
8
+ --primary: #6366f1;
9
+ --primary-dark: #4f46e5;
10
+ --primary-light: #818cf8;
11
+ --secondary: #10b981;
12
+ --accent: #f59e0b;
13
+ --danger: #ef4444;
14
+
15
+ --bg-dark: #0f172a;
16
+ --bg-card: #1e293b;
17
+ --bg-hover: #334155;
18
+ --bg-light: #f8fafc;
19
+
20
+ --text-primary: #f8fafc;
21
+ --text-secondary: #94a3b8;
22
+ --text-muted: #64748b;
23
+
24
+ --border: #334155;
25
+ --border-light: #475569;
26
+
27
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
28
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
29
+
30
+ --radius: 12px;
31
+ --radius-sm: 8px;
32
+
33
+ --transition: all 0.2s ease;
34
+ }
35
+
36
+ /* === Base Styles === */
37
+ * {
38
+ box-sizing: border-box;
39
+ margin: 0;
40
+ padding: 0;
41
+ }
42
+
43
+ body {
44
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1f35 100%);
46
+ color: var(--text-primary);
47
+ min-height: 100vh;
48
+ line-height: 1.5;
49
+ }
50
+
51
+ /* === Layout === */
52
+ .app {
53
+ display: flex;
54
+ flex-direction: column;
55
+ min-height: 100vh;
56
+ }
57
+
58
+ .app-header {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ padding: 1rem 2rem;
63
+ background: rgba(15, 23, 42, 0.8);
64
+ backdrop-filter: blur(10px);
65
+ border-bottom: 1px solid var(--border);
66
+ }
67
+
68
+ .logo {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 0.75rem;
72
+ }
73
+
74
+ .logo svg {
75
+ color: var(--primary);
76
+ }
77
+
78
+ .logo h1 {
79
+ font-size: 1.5rem;
80
+ font-weight: 700;
81
+ background: linear-gradient(135deg, var(--primary-light), var(--secondary));
82
+ -webkit-background-clip: text;
83
+ -webkit-text-fill-color: transparent;
84
+ background-clip: text;
85
+ }
86
+
87
+ .tagline {
88
+ color: var(--text-secondary);
89
+ font-size: 0.875rem;
90
+ }
91
+
92
+ .app-main {
93
+ display: flex;
94
+ flex: 1;
95
+ padding: 1.5rem;
96
+ gap: 1.5rem;
97
+ max-width: 1600px;
98
+ margin: 0 auto;
99
+ width: 100%;
100
+ }
101
+
102
+ .left-panel {
103
+ flex: 1;
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 1.5rem;
107
+ max-width: 800px;
108
+ }
109
+
110
+ .right-panel {
111
+ width: 400px;
112
+ flex-shrink: 0;
113
+ }
114
+
115
+ .app-footer {
116
+ padding: 1rem 2rem;
117
+ text-align: center;
118
+ color: var(--text-muted);
119
+ font-size: 0.75rem;
120
+ border-top: 1px solid var(--border);
121
+ }
122
+
123
+ /* === Buttons === */
124
+ .btn {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ gap: 0.5rem;
129
+ padding: 0.75rem 1.25rem;
130
+ border-radius: var(--radius-sm);
131
+ font-weight: 600;
132
+ font-size: 0.875rem;
133
+ cursor: pointer;
134
+ border: none;
135
+ transition: var(--transition);
136
+ }
137
+
138
+ .btn:disabled {
139
+ opacity: 0.5;
140
+ cursor: not-allowed;
141
+ }
142
+
143
+ .btn-primary {
144
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
145
+ color: white;
146
+ }
147
+
148
+ .btn-primary:hover:not(:disabled) {
149
+ transform: translateY(-1px);
150
+ box-shadow: var(--shadow-lg);
151
+ }
152
+
153
+ .btn-secondary {
154
+ background: var(--bg-card);
155
+ color: var(--text-primary);
156
+ border: 1px solid var(--border);
157
+ }
158
+
159
+ .btn-secondary:hover:not(:disabled) {
160
+ background: var(--bg-hover);
161
+ }
162
+
163
+ .btn-sm {
164
+ padding: 0.5rem 0.75rem;
165
+ font-size: 0.75rem;
166
+ }
167
+
168
+ .btn-export {
169
+ background: var(--secondary);
170
+ color: white;
171
+ }
172
+
173
+ .btn-export:hover:not(:disabled) {
174
+ background: #059669;
175
+ }
176
+
177
+ .btn-generate {
178
+ width: 100%;
179
+ padding: 1rem;
180
+ font-size: 1rem;
181
+ }
182
+
183
+ .btn-export-all {
184
+ width: 100%;
185
+ }
186
+
187
+ .btn-send {
188
+ padding: 0.75rem;
189
+ background: var(--primary);
190
+ color: white;
191
+ border-radius: var(--radius-sm);
192
+ }
193
+
194
+ .spin {
195
+ animation: spin 1s linear infinite;
196
+ }
197
+
198
+ @keyframes spin {
199
+ from {
200
+ transform: rotate(0deg);
201
+ }
202
+
203
+ to {
204
+ transform: rotate(360deg);
205
+ }
206
+ }
207
+
208
+ /* === Upload Panel === */
209
+ .upload-panel {
210
+ background: var(--bg-card);
211
+ border-radius: var(--radius);
212
+ padding: 1.5rem;
213
+ border: 1px solid var(--border);
214
+ }
215
+
216
+ .upload-header {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 0.75rem;
220
+ margin-bottom: 1rem;
221
+ }
222
+
223
+ .upload-header svg {
224
+ color: var(--primary);
225
+ }
226
+
227
+ .upload-header h3 {
228
+ font-size: 1rem;
229
+ font-weight: 600;
230
+ }
231
+
232
+ .upload-buttons {
233
+ display: flex;
234
+ gap: 0.75rem;
235
+ }
236
+
237
+ .upload-status {
238
+ margin-top: 1rem;
239
+ padding: 0.5rem 0.75rem;
240
+ background: rgba(16, 185, 129, 0.1);
241
+ border-radius: var(--radius-sm);
242
+ color: var(--secondary);
243
+ font-size: 0.875rem;
244
+ }
245
+
246
+ /* === Generate Section === */
247
+ .generate-section {
248
+ background: var(--bg-card);
249
+ border-radius: var(--radius);
250
+ padding: 1rem;
251
+ border: 1px solid var(--border);
252
+ }
253
+
254
+ /* === Map Container === */
255
+ .map-section {
256
+ background: var(--bg-card);
257
+ border-radius: var(--radius);
258
+ padding: 1rem;
259
+ border: 1px solid var(--border);
260
+ }
261
+
262
+ .map-wrapper {
263
+ position: relative;
264
+ }
265
+
266
+ .map-controls {
267
+ position: absolute;
268
+ top: 10px;
269
+ right: 10px;
270
+ display: flex;
271
+ gap: 4px;
272
+ z-index: 10;
273
+ background: white;
274
+ padding: 4px;
275
+ border-radius: var(--radius-sm);
276
+ box-shadow: var(--shadow);
277
+ }
278
+
279
+ .map-controls button {
280
+ width: 32px;
281
+ height: 32px;
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ background: #f8fafc;
286
+ border: 1px solid #e2e8f0;
287
+ border-radius: 6px;
288
+ cursor: pointer;
289
+ color: #475569;
290
+ transition: var(--transition);
291
+ }
292
+
293
+ .map-controls button:hover {
294
+ background: #e2e8f0;
295
+ color: #1e293b;
296
+ }
297
+
298
+ .map-controls .zoom-level {
299
+ font-size: 11px;
300
+ color: #64748b;
301
+ padding: 0 8px;
302
+ display: flex;
303
+ align-items: center;
304
+ }
305
+
306
+ .map-hint {
307
+ position: absolute;
308
+ bottom: 8px;
309
+ left: 50%;
310
+ transform: translateX(-50%);
311
+ font-size: 11px;
312
+ color: #94a3b8;
313
+ background: rgba(255, 255, 255, 0.9);
314
+ padding: 4px 12px;
315
+ border-radius: 20px;
316
+ display: flex;
317
+ align-items: center;
318
+ gap: 6px;
319
+ }
320
+
321
+ .map-container {
322
+ background: white;
323
+ border-radius: var(--radius-sm);
324
+ overflow: hidden;
325
+ cursor: grab;
326
+ }
327
+
328
+ .map-container:active {
329
+ cursor: grabbing;
330
+ }
331
+
332
+ /* === Options Panel === */
333
+ .options-panel {
334
+ background: var(--bg-card);
335
+ border-radius: var(--radius);
336
+ padding: 1.5rem;
337
+ border: 1px solid var(--border);
338
+ }
339
+
340
+ .options-panel h3 {
341
+ font-size: 1rem;
342
+ font-weight: 600;
343
+ margin-bottom: 1rem;
344
+ }
345
+
346
+ .options-panel.empty {
347
+ text-align: center;
348
+ color: var(--text-muted);
349
+ padding: 2rem;
350
+ }
351
+
352
+ .options-grid {
353
+ display: flex;
354
+ flex-direction: column;
355
+ gap: 1rem;
356
+ }
357
+
358
+ .option-card {
359
+ background: var(--bg-dark);
360
+ border-radius: var(--radius-sm);
361
+ padding: 1rem;
362
+ border: 2px solid var(--border);
363
+ cursor: pointer;
364
+ transition: var(--transition);
365
+ }
366
+
367
+ .option-card:hover {
368
+ border-color: var(--border-light);
369
+ background: var(--bg-hover);
370
+ }
371
+
372
+ .option-card.selected {
373
+ border-color: var(--primary);
374
+ background: rgba(99, 102, 241, 0.1);
375
+ }
376
+
377
+ .option-header {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 0.75rem;
381
+ margin-bottom: 0.5rem;
382
+ }
383
+
384
+ .option-icon {
385
+ font-size: 1.5rem;
386
+ }
387
+
388
+ .option-header h4 {
389
+ flex: 1;
390
+ font-size: 1rem;
391
+ font-weight: 600;
392
+ }
393
+
394
+ .selected-check {
395
+ color: var(--primary);
396
+ }
397
+
398
+ .option-description {
399
+ color: var(--text-secondary);
400
+ font-size: 0.75rem;
401
+ margin-bottom: 1rem;
402
+ }
403
+
404
+ .option-metrics {
405
+ display: grid;
406
+ grid-template-columns: repeat(4, 1fr);
407
+ gap: 0.75rem;
408
+ margin-bottom: 1rem;
409
+ }
410
+
411
+ .metric {
412
+ text-align: center;
413
+ }
414
+
415
+ .metric-label {
416
+ display: block;
417
+ font-size: 0.625rem;
418
+ color: var(--text-muted);
419
+ text-transform: uppercase;
420
+ letter-spacing: 0.05em;
421
+ margin-bottom: 0.25rem;
422
+ }
423
+
424
+ .metric-value {
425
+ font-size: 0.875rem;
426
+ font-weight: 600;
427
+ color: var(--text-primary);
428
+ }
429
+
430
+ .metric-value.fitness {
431
+ color: var(--secondary);
432
+ }
433
+
434
+ .option-footer {
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: space-between;
438
+ }
439
+
440
+ .compliance {
441
+ font-size: 0.75rem;
442
+ padding: 0.25rem 0.5rem;
443
+ border-radius: 4px;
444
+ }
445
+
446
+ .compliance.pass {
447
+ background: rgba(16, 185, 129, 0.1);
448
+ color: var(--secondary);
449
+ }
450
+
451
+ .compliance.fail {
452
+ background: rgba(239, 68, 68, 0.1);
453
+ color: var(--danger);
454
+ }
455
+
456
+ /* === Export Panel === */
457
+ .export-panel {
458
+ background: var(--bg-card);
459
+ border-radius: var(--radius);
460
+ padding: 1rem;
461
+ border: 1px solid var(--border);
462
+ }
463
+
464
+ /* === Chat Interface === */
465
+ .chat-interface {
466
+ background: var(--bg-card);
467
+ border-radius: var(--radius);
468
+ border: 1px solid var(--border);
469
+ display: flex;
470
+ flex-direction: column;
471
+ height: calc(100vh - 180px);
472
+ max-height: 700px;
473
+ }
474
+
475
+ .chat-header {
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 0.75rem;
479
+ padding: 1rem 1.25rem;
480
+ border-bottom: 1px solid var(--border);
481
+ }
482
+
483
+ .chat-header svg {
484
+ color: var(--primary);
485
+ }
486
+
487
+ .chat-header .sparkle {
488
+ color: var(--accent);
489
+ }
490
+
491
+ .chat-header h3 {
492
+ flex: 1;
493
+ font-size: 1rem;
494
+ font-weight: 600;
495
+ }
496
+
497
+ .chat-messages {
498
+ flex: 1;
499
+ overflow-y: auto;
500
+ padding: 1rem;
501
+ }
502
+
503
+ .chat-welcome {
504
+ text-align: center;
505
+ padding: 2rem 1rem;
506
+ color: var(--text-secondary);
507
+ }
508
+
509
+ .welcome-icon {
510
+ color: var(--primary);
511
+ margin-bottom: 1rem;
512
+ }
513
+
514
+ .chat-welcome h4 {
515
+ font-size: 1.125rem;
516
+ color: var(--text-primary);
517
+ margin-bottom: 0.5rem;
518
+ }
519
+
520
+ .chat-welcome p {
521
+ font-size: 0.875rem;
522
+ margin-bottom: 1.5rem;
523
+ }
524
+
525
+ .suggested-questions {
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: 0.5rem;
529
+ }
530
+
531
+ .suggested-btn {
532
+ background: var(--bg-dark);
533
+ border: 1px solid var(--border);
534
+ color: var(--text-secondary);
535
+ padding: 0.75rem 1rem;
536
+ border-radius: var(--radius-sm);
537
+ font-size: 0.75rem;
538
+ cursor: pointer;
539
+ text-align: left;
540
+ transition: var(--transition);
541
+ }
542
+
543
+ .suggested-btn:hover:not(:disabled) {
544
+ background: var(--bg-hover);
545
+ color: var(--text-primary);
546
+ border-color: var(--primary);
547
+ }
548
+
549
+ .message {
550
+ display: flex;
551
+ gap: 0.75rem;
552
+ margin-bottom: 1rem;
553
+ }
554
+
555
+ .message-icon {
556
+ width: 28px;
557
+ height: 28px;
558
+ border-radius: 50%;
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ flex-shrink: 0;
563
+ }
564
+
565
+ .message.user .message-icon {
566
+ background: var(--primary);
567
+ color: white;
568
+ }
569
+
570
+ .message.assistant .message-icon {
571
+ background: var(--secondary);
572
+ color: white;
573
+ }
574
+
575
+ .message-content {
576
+ flex: 1;
577
+ background: var(--bg-dark);
578
+ padding: 0.75rem 1rem;
579
+ border-radius: var(--radius-sm);
580
+ font-size: 0.875rem;
581
+ }
582
+
583
+ .message.user .message-content {
584
+ background: rgba(99, 102, 241, 0.1);
585
+ }
586
+
587
+ .message-text {
588
+ white-space: pre-wrap;
589
+ line-height: 1.6;
590
+ }
591
+
592
+ .model-badge {
593
+ margin-top: 0.5rem;
594
+ font-size: 0.625rem;
595
+ color: var(--text-muted);
596
+ }
597
+
598
+ .model-badge.gemini {
599
+ color: var(--secondary);
600
+ }
601
+
602
+ .typing-indicator {
603
+ display: flex;
604
+ gap: 4px;
605
+ }
606
+
607
+ .typing-indicator span {
608
+ width: 6px;
609
+ height: 6px;
610
+ background: var(--text-muted);
611
+ border-radius: 50%;
612
+ animation: bounce 1.4s infinite ease-in-out;
613
+ }
614
+
615
+ .typing-indicator span:nth-child(2) {
616
+ animation-delay: 0.2s;
617
+ }
618
+
619
+ .typing-indicator span:nth-child(3) {
620
+ animation-delay: 0.4s;
621
+ }
622
+
623
+ @keyframes bounce {
624
+
625
+ 0%,
626
+ 80%,
627
+ 100% {
628
+ transform: translateY(0);
629
+ }
630
+
631
+ 40% {
632
+ transform: translateY(-6px);
633
+ }
634
+ }
635
+
636
+ .chat-input-form {
637
+ display: flex;
638
+ gap: 0.75rem;
639
+ padding: 1rem;
640
+ border-top: 1px solid var(--border);
641
+ }
642
+
643
+ .chat-input {
644
+ flex: 1;
645
+ background: var(--bg-dark);
646
+ border: 1px solid var(--border);
647
+ border-radius: var(--radius-sm);
648
+ padding: 0.75rem 1rem;
649
+ color: var(--text-primary);
650
+ font-size: 0.875rem;
651
+ }
652
+
653
+ .chat-input:focus {
654
+ outline: none;
655
+ border-color: var(--primary);
656
+ }
657
+
658
+ .chat-input:disabled {
659
+ opacity: 0.5;
660
+ }
661
+
662
+ /* === Error Banner === */
663
+ .error-banner {
664
+ display: flex;
665
+ align-items: center;
666
+ gap: 0.75rem;
667
+ padding: 0.75rem 1.5rem;
668
+ background: rgba(239, 68, 68, 0.1);
669
+ border-bottom: 1px solid var(--danger);
670
+ color: var(--danger);
671
+ }
672
+
673
+ .error-banner svg {
674
+ flex-shrink: 0;
675
+ }
676
+
677
+ .error-banner span {
678
+ flex: 1;
679
+ font-size: 0.875rem;
680
+ }
681
+
682
+ .error-banner button {
683
+ background: none;
684
+ border: none;
685
+ color: var(--danger);
686
+ font-size: 1.25rem;
687
+ cursor: pointer;
688
+ padding: 0 0.5rem;
689
+ }
690
+
691
+ /* === Responsive === */
692
+ @media (max-width: 1200px) {
693
+ .app-main {
694
+ flex-direction: column;
695
+ }
696
+
697
+ .left-panel {
698
+ max-width: 100%;
699
+ }
700
+
701
+ .right-panel {
702
+ width: 100%;
703
+ }
704
+
705
+ .chat-interface {
706
+ height: 400px;
707
+ }
708
+ }
709
+
710
+ @media (max-width: 768px) {
711
+ .app-header {
712
+ flex-direction: column;
713
+ gap: 0.5rem;
714
+ text-align: center;
715
+ }
716
+
717
+ .upload-buttons {
718
+ flex-direction: column;
719
+ }
720
+
721
+ .option-metrics {
722
+ grid-template-columns: repeat(2, 1fr);
723
+ }
724
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AIOptimize™ MVP - Main Application
3
+ * Industrial Estate Planning Optimization
4
+ */
5
+ import { useState, useCallback, useEffect } from 'react';
6
+ import { Layers, Zap, AlertCircle, Loader2 } from 'lucide-react';
7
+ import { FileUploadPanel } from './components/FileUploadPanel';
8
+ import { Map2DPlotter } from './components/Map2DPlotter';
9
+ import { LayoutOptionsPanel } from './components/LayoutOptionsPanel';
10
+ import { ChatInterface } from './components/ChatInterface';
11
+ import { ExportPanel } from './components/ExportPanel';
12
+ import { apiService } from './services/api';
13
+ import type { AppState, ChatMessage, GeoJSONFeature } from './types';
14
+ import './App.css';
15
+
16
+ function App() {
17
+ // Application state
18
+ const [state, setState] = useState<AppState>({
19
+ sessionId: null,
20
+ boundary: null,
21
+ boundaryCoords: null,
22
+ metadata: null,
23
+ options: [],
24
+ selectedOption: null,
25
+ messages: [],
26
+ loading: false,
27
+ error: null,
28
+ });
29
+
30
+ // Loading states
31
+ const [uploadLoading, setUploadLoading] = useState(false);
32
+ const [generateLoading, setGenerateLoading] = useState(false);
33
+ const [chatLoading, setChatLoading] = useState(false);
34
+ const [exportLoading, setExportLoading] = useState(false);
35
+
36
+ // Debug: monitor options changes
37
+ useEffect(() => {
38
+ console.log('State options changed:', state.options.length, state.options);
39
+ }, [state.options]);
40
+
41
+ // Extract boundary coords from GeoJSON
42
+ const extractCoords = (geojson: GeoJSONFeature): number[][] => {
43
+ if (geojson.type === 'Feature' && geojson.geometry) {
44
+ return geojson.geometry.coordinates[0];
45
+ }
46
+ return [];
47
+ };
48
+
49
+ // Handle file upload
50
+ const handleUpload = useCallback(async (file: File) => {
51
+ setUploadLoading(true);
52
+ setState(prev => ({ ...prev, error: null }));
53
+
54
+ try {
55
+ const response = await apiService.uploadBoundaryFile(file);
56
+ const coords = extractCoords(response.boundary);
57
+
58
+ setState(prev => ({
59
+ ...prev,
60
+ sessionId: response.session_id,
61
+ boundary: response.boundary,
62
+ boundaryCoords: coords,
63
+ metadata: response.metadata,
64
+ options: [],
65
+ selectedOption: null,
66
+ }));
67
+ } catch (err) {
68
+ setState(prev => ({
69
+ ...prev,
70
+ error: err instanceof Error ? err.message : 'Upload failed'
71
+ }));
72
+ } finally {
73
+ setUploadLoading(false);
74
+ }
75
+ }, []);
76
+
77
+ // Handle sample data
78
+ const handleSampleData = useCallback(async () => {
79
+ setUploadLoading(true);
80
+ setState(prev => ({ ...prev, error: null }));
81
+
82
+ try {
83
+ const sampleData = await apiService.getSampleData();
84
+ const response = await apiService.uploadBoundary(sampleData);
85
+ const coords = extractCoords(response.boundary);
86
+
87
+ setState(prev => ({
88
+ ...prev,
89
+ sessionId: response.session_id,
90
+ boundary: response.boundary,
91
+ boundaryCoords: coords,
92
+ metadata: response.metadata,
93
+ options: [],
94
+ selectedOption: null,
95
+ }));
96
+ } catch (err) {
97
+ setState(prev => ({
98
+ ...prev,
99
+ error: err instanceof Error ? err.message : 'Failed to load sample data'
100
+ }));
101
+ } finally {
102
+ setUploadLoading(false);
103
+ }
104
+ }, []);
105
+
106
+ // Generate layouts
107
+ const handleGenerate = useCallback(async () => {
108
+ if (!state.sessionId) return;
109
+
110
+ setGenerateLoading(true);
111
+ setState(prev => ({ ...prev, error: null }));
112
+
113
+ try {
114
+ const response = await apiService.generateLayouts(state.sessionId);
115
+ console.log('Generate response:', response);
116
+
117
+ if (response.options && response.options.length > 0) {
118
+ setState(prev => ({
119
+ ...prev,
120
+ options: response.options,
121
+ selectedOption: response.options[0]?.id || null,
122
+ }));
123
+ } else {
124
+ setState(prev => ({
125
+ ...prev,
126
+ error: 'No layout options returned'
127
+ }));
128
+ }
129
+ } catch (err) {
130
+ console.error('Generate error:', err);
131
+ setState(prev => ({
132
+ ...prev,
133
+ error: err instanceof Error ? err.message : 'Generation failed'
134
+ }));
135
+ } finally {
136
+ setGenerateLoading(false);
137
+ }
138
+ }, [state.sessionId]);
139
+
140
+ // Select layout option
141
+ const handleSelectOption = useCallback((optionId: number) => {
142
+ setState(prev => ({ ...prev, selectedOption: optionId }));
143
+ }, []);
144
+
145
+ // Export single DXF
146
+ const handleExportDxf = useCallback(async (optionId: number) => {
147
+ if (!state.sessionId) return;
148
+
149
+ setExportLoading(true);
150
+
151
+ try {
152
+ const blob = await apiService.exportDxf(state.sessionId, optionId);
153
+
154
+ const url = window.URL.createObjectURL(blob);
155
+ const a = document.createElement('a');
156
+ a.href = url;
157
+ a.download = `layout_option_${optionId}.dxf`;
158
+ document.body.appendChild(a);
159
+ a.click();
160
+ document.body.removeChild(a);
161
+ window.URL.revokeObjectURL(url);
162
+ } catch (err) {
163
+ setState(prev => ({
164
+ ...prev,
165
+ error: err instanceof Error ? err.message : 'Export failed'
166
+ }));
167
+ } finally {
168
+ setExportLoading(false);
169
+ }
170
+ }, [state.sessionId]);
171
+
172
+ // Export all as ZIP
173
+ const handleExportAll = useCallback(async () => {
174
+ if (!state.sessionId) return;
175
+
176
+ setExportLoading(true);
177
+
178
+ try {
179
+ const blob = await apiService.exportAllDxf(state.sessionId);
180
+
181
+ const url = window.URL.createObjectURL(blob);
182
+ const a = document.createElement('a');
183
+ a.href = url;
184
+ a.download = 'all_layouts.zip';
185
+ document.body.appendChild(a);
186
+ a.click();
187
+ document.body.removeChild(a);
188
+ window.URL.revokeObjectURL(url);
189
+ } catch (err) {
190
+ setState(prev => ({
191
+ ...prev,
192
+ error: err instanceof Error ? err.message : 'Export failed'
193
+ }));
194
+ } finally {
195
+ setExportLoading(false);
196
+ }
197
+ }, [state.sessionId]);
198
+
199
+ // Send chat message
200
+ const handleChat = useCallback(async (message: string) => {
201
+ if (!state.sessionId) return;
202
+
203
+ const userMessage: ChatMessage = { role: 'user', content: message };
204
+ setState(prev => ({
205
+ ...prev,
206
+ messages: [...prev.messages, userMessage],
207
+ }));
208
+
209
+ setChatLoading(true);
210
+
211
+ try {
212
+ const response = await apiService.chat(state.sessionId, message);
213
+
214
+ const assistantMessage: ChatMessage = {
215
+ role: 'assistant',
216
+ content: response.message,
217
+ model: response.model,
218
+ };
219
+
220
+ setState(prev => ({
221
+ ...prev,
222
+ messages: [...prev.messages, assistantMessage],
223
+ }));
224
+ } catch (err) {
225
+ const errorMessage: ChatMessage = {
226
+ role: 'assistant',
227
+ content: 'Sorry, I encountered an error. Please try again.',
228
+ model: 'fallback',
229
+ };
230
+ setState(prev => ({
231
+ ...prev,
232
+ messages: [...prev.messages, errorMessage],
233
+ }));
234
+ } finally {
235
+ setChatLoading(false);
236
+ }
237
+ }, [state.sessionId]);
238
+
239
+ // Get selected option object
240
+ const selectedOptionData = state.options.find(o => o.id === state.selectedOption) || null;
241
+
242
+ return (
243
+ <div className="app">
244
+ <header className="app-header">
245
+ <div className="logo">
246
+ <Layers size={28} />
247
+ <h1>AIOptimize™</h1>
248
+ </div>
249
+ <p className="tagline">AI-Powered Industrial Estate Planning</p>
250
+ </header>
251
+
252
+ {state.error && (
253
+ <div className="error-banner">
254
+ <AlertCircle size={18} />
255
+ <span>{state.error}</span>
256
+ <button onClick={() => setState(prev => ({ ...prev, error: null }))}>×</button>
257
+ </div>
258
+ )}
259
+
260
+ <main className="app-main">
261
+ <div className="left-panel">
262
+ <FileUploadPanel
263
+ onUpload={handleUpload}
264
+ onSampleData={handleSampleData}
265
+ loading={uploadLoading}
266
+ hasData={!!state.boundary}
267
+ />
268
+
269
+ {state.boundary && (
270
+ <div className="generate-section">
271
+ <button
272
+ className="btn btn-primary btn-generate"
273
+ onClick={handleGenerate}
274
+ disabled={generateLoading}
275
+ >
276
+ {generateLoading ? (
277
+ <>
278
+ <Loader2 size={18} className="spin" />
279
+ Optimizing...
280
+ </>
281
+ ) : (
282
+ <>
283
+ <Zap size={18} />
284
+ Generate Layouts
285
+ </>
286
+ )}
287
+ </button>
288
+ </div>
289
+ )}
290
+
291
+ <div className="map-section">
292
+ <Map2DPlotter
293
+ boundaryCoords={state.boundaryCoords}
294
+ metadata={state.metadata}
295
+ selectedOption={selectedOptionData}
296
+ width={720}
297
+ height={500}
298
+ />
299
+ </div>
300
+
301
+ <LayoutOptionsPanel
302
+ options={state.options}
303
+ selectedOptionId={state.selectedOption}
304
+ onSelect={handleSelectOption}
305
+ onExport={handleExportDxf}
306
+ loading={exportLoading}
307
+ />
308
+
309
+ <ExportPanel
310
+ hasLayouts={state.options.length > 0}
311
+ onExportAll={handleExportAll}
312
+ loading={exportLoading}
313
+ />
314
+ </div>
315
+
316
+ <div className="right-panel">
317
+ <ChatInterface
318
+ messages={state.messages}
319
+ onSendMessage={handleChat}
320
+ loading={chatLoading}
321
+ disabled={!state.sessionId}
322
+ />
323
+ </div>
324
+ </main>
325
+
326
+ <footer className="app-footer">
327
+ <p>AIOptimize™ MVP • Built with React + FastAPI + Genetic Algorithm</p>
328
+ </footer>
329
+ </div>
330
+ );
331
+ }
332
+
333
+ export default App;
frontend/src/assets/react.svg ADDED
frontend/src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ChatInterface Component - AI Chat for layout questions
2
+ import React, { useState, useRef, useEffect } from 'react';
3
+ import { Send, Bot, User, Sparkles } from 'lucide-react';
4
+ import type { ChatMessage } from '../types';
5
+
6
+ interface ChatInterfaceProps {
7
+ messages: ChatMessage[];
8
+ onSendMessage: (message: string) => void;
9
+ loading: boolean;
10
+ disabled: boolean;
11
+ }
12
+
13
+ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
14
+ messages,
15
+ onSendMessage,
16
+ loading,
17
+ disabled,
18
+ }) => {
19
+ const [input, setInput] = useState('');
20
+ const messagesEndRef = useRef<HTMLDivElement>(null);
21
+
22
+ // Auto-scroll to bottom on new messages
23
+ useEffect(() => {
24
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
25
+ }, [messages]);
26
+
27
+ const handleSubmit = (e: React.FormEvent) => {
28
+ e.preventDefault();
29
+ if (input.trim() && !loading && !disabled) {
30
+ onSendMessage(input.trim());
31
+ setInput('');
32
+ }
33
+ };
34
+
35
+ const suggestedQuestions = [
36
+ "What's the difference between the options?",
37
+ "Which layout do you recommend?",
38
+ "How does the optimization work?",
39
+ "What are the compliance requirements?",
40
+ ];
41
+
42
+ return (
43
+ <div className="chat-interface">
44
+ <div className="chat-header">
45
+ <Bot size={20} />
46
+ <h3>AI Assistant</h3>
47
+ <Sparkles size={16} className="sparkle" />
48
+ </div>
49
+
50
+ <div className="chat-messages">
51
+ {messages.length === 0 ? (
52
+ <div className="chat-welcome">
53
+ <Bot size={40} className="welcome-icon" />
54
+ <h4>Ask about your layouts</h4>
55
+ <p>I can help you understand optimization options, metrics, and compliance.</p>
56
+
57
+ <div className="suggested-questions">
58
+ {suggestedQuestions.map((q, i) => (
59
+ <button
60
+ key={i}
61
+ className="suggested-btn"
62
+ onClick={() => onSendMessage(q)}
63
+ disabled={disabled || loading}
64
+ >
65
+ {q}
66
+ </button>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ ) : (
71
+ messages.map((msg, index) => (
72
+ <div key={index} className={`message ${msg.role}`}>
73
+ <div className="message-icon">
74
+ {msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
75
+ </div>
76
+ <div className="message-content">
77
+ <div className="message-text">{msg.content}</div>
78
+ {msg.role === 'assistant' && msg.model && (
79
+ <div className={`model-badge ${msg.model === 'gemini-2.0-flash' ? 'gemini' : 'fallback'}`}>
80
+ {msg.model === 'gemini-2.0-flash' ? '🤖 Powered by Gemini' : '💬 Fallback Mode'}
81
+ </div>
82
+ )}
83
+ </div>
84
+ </div>
85
+ ))
86
+ )}
87
+
88
+ {loading && (
89
+ <div className="message assistant loading">
90
+ <div className="message-icon">
91
+ <Bot size={16} />
92
+ </div>
93
+ <div className="message-content">
94
+ <div className="typing-indicator">
95
+ <span></span>
96
+ <span></span>
97
+ <span></span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ )}
102
+
103
+ <div ref={messagesEndRef} />
104
+ </div>
105
+
106
+ <form className="chat-input-form" onSubmit={handleSubmit}>
107
+ <input
108
+ type="text"
109
+ value={input}
110
+ onChange={(e) => setInput(e.target.value)}
111
+ placeholder={disabled ? "Upload a site first..." : "Ask about your layouts..."}
112
+ disabled={loading || disabled}
113
+ className="chat-input"
114
+ />
115
+ <button
116
+ type="submit"
117
+ disabled={!input.trim() || loading || disabled}
118
+ className="btn btn-send"
119
+ >
120
+ <Send size={18} />
121
+ </button>
122
+ </form>
123
+ </div>
124
+ );
125
+ };
126
+
127
+ export default ChatInterface;
frontend/src/components/ExportPanel.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ExportPanel Component - Export buttons for DXF download
2
+ import React from 'react';
3
+ import { Download, Package } from 'lucide-react';
4
+
5
+ interface ExportPanelProps {
6
+ hasLayouts: boolean;
7
+ onExportAll: () => void;
8
+ loading: boolean;
9
+ }
10
+
11
+ export const ExportPanel: React.FC<ExportPanelProps> = ({
12
+ hasLayouts,
13
+ onExportAll,
14
+ loading,
15
+ }) => {
16
+ if (!hasLayouts) {
17
+ return null;
18
+ }
19
+
20
+ return (
21
+ <div className="export-panel">
22
+ <button
23
+ className="btn btn-primary btn-export-all"
24
+ onClick={onExportAll}
25
+ disabled={loading}
26
+ >
27
+ <Package size={18} />
28
+ Export All as ZIP
29
+ <Download size={16} />
30
+ </button>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default ExportPanel;
frontend/src/components/FileUploadPanel.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // FileUploadPanel Component
2
+ import React, { useRef } from 'react';
3
+ import { Upload, FileJson, Zap } from 'lucide-react';
4
+
5
+ interface FileUploadPanelProps {
6
+ onUpload: (file: File) => void;
7
+ onSampleData: () => void;
8
+ loading: boolean;
9
+ hasData: boolean;
10
+ }
11
+
12
+ export const FileUploadPanel: React.FC<FileUploadPanelProps> = ({
13
+ onUpload,
14
+ onSampleData,
15
+ loading,
16
+ hasData,
17
+ }) => {
18
+ const fileInputRef = useRef<HTMLInputElement>(null);
19
+
20
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
21
+ const file = e.target.files?.[0];
22
+ if (file) {
23
+ onUpload(file);
24
+ }
25
+ };
26
+
27
+ const handleClick = () => {
28
+ fileInputRef.current?.click();
29
+ };
30
+
31
+ return (
32
+ <div className="upload-panel">
33
+ <div className="upload-header">
34
+ <FileJson size={24} />
35
+ <h3>Site Boundary</h3>
36
+ </div>
37
+
38
+ <div className="upload-buttons">
39
+ <button
40
+ className="btn btn-primary"
41
+ onClick={handleClick}
42
+ disabled={loading}
43
+ >
44
+ <Upload size={18} />
45
+ {hasData ? 'Replace Boundary' : 'Upload DXF / GeoJSON'}
46
+ </button>
47
+
48
+ <button
49
+ className="btn btn-secondary"
50
+ onClick={onSampleData}
51
+ disabled={loading}
52
+ >
53
+ <Zap size={18} />
54
+ Use Sample Data
55
+ </button>
56
+ </div>
57
+
58
+ <input
59
+ ref={fileInputRef}
60
+ type="file"
61
+ accept=".dxf,.geojson,.json"
62
+ onChange={handleFileChange}
63
+ style={{ display: 'none' }}
64
+ />
65
+
66
+ {hasData && (
67
+ <div className="upload-status">
68
+ ✅ Boundary loaded
69
+ </div>
70
+ )}
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export default FileUploadPanel;
frontend/src/components/LayoutOptionsPanel.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // LayoutOptionsPanel - Display 3 layout options with metrics
2
+ import React from 'react';
3
+ import { Download, Check } from 'lucide-react';
4
+ import type { LayoutOption } from '../types';
5
+
6
+ interface LayoutOptionsPanelProps {
7
+ options: LayoutOption[];
8
+ selectedOptionId: number | null;
9
+ onSelect: (optionId: number) => void;
10
+ onExport: (optionId: number) => void;
11
+ loading: boolean;
12
+ }
13
+
14
+ export const LayoutOptionsPanel: React.FC<LayoutOptionsPanelProps> = ({
15
+ options,
16
+ selectedOptionId,
17
+ onSelect,
18
+ onExport,
19
+ loading,
20
+ }) => {
21
+ if (options.length === 0) {
22
+ return (
23
+ <div className="options-panel empty">
24
+ <p>Generate layouts to see optimization options</p>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <div className="options-panel">
31
+ <h3>Layout Options</h3>
32
+ <div className="options-grid">
33
+ {options.map((option) => (
34
+ <div
35
+ key={option.id}
36
+ className={`option-card ${selectedOptionId === option.id ? 'selected' : ''}`}
37
+ onClick={() => onSelect(option.id)}
38
+ >
39
+ <div className="option-header">
40
+ <span className="option-icon">{option.icon}</span>
41
+ <h4>{option.name}</h4>
42
+ {selectedOptionId === option.id && (
43
+ <Check size={18} className="selected-check" />
44
+ )}
45
+ </div>
46
+
47
+ <p className="option-description">{option.description}</p>
48
+
49
+ <div className="option-metrics">
50
+ <div className="metric">
51
+ <span className="metric-label">Plots</span>
52
+ <span className="metric-value">{option.metrics.total_plots}</span>
53
+ </div>
54
+ <div className="metric">
55
+ <span className="metric-label">Total Area</span>
56
+ <span className="metric-value">{option.metrics.total_area.toLocaleString()} m²</span>
57
+ </div>
58
+ <div className="metric">
59
+ <span className="metric-label">Avg Size</span>
60
+ <span className="metric-value">{Math.round(option.metrics.avg_size).toLocaleString()} m²</span>
61
+ </div>
62
+ <div className="metric">
63
+ <span className="metric-label">Fitness</span>
64
+ <span className="metric-value fitness">{(option.metrics.fitness * 100).toFixed(0)}%</span>
65
+ </div>
66
+ </div>
67
+
68
+ <div className="option-footer">
69
+ <span className={`compliance ${option.metrics.compliance === 'PASS' ? 'pass' : 'fail'}`}>
70
+ {option.metrics.compliance === 'PASS' ? '✓ Compliant' : '✗ Issues'}
71
+ </span>
72
+ <button
73
+ className="btn btn-sm btn-export"
74
+ onClick={(e) => {
75
+ e.stopPropagation();
76
+ onExport(option.id);
77
+ }}
78
+ disabled={loading}
79
+ >
80
+ <Download size={14} />
81
+ DXF
82
+ </button>
83
+ </div>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ export default LayoutOptionsPanel;
frontend/src/components/Map2DPlotter.tsx ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Map2DPlotter Component - Konva.js canvas with zoom/pan for site visualization
2
+ import React, { useMemo, useState, useRef, useCallback } from 'react';
3
+ import { Stage, Layer, Line, Rect, Text, Group } from 'react-konva';
4
+ import { ZoomIn, ZoomOut, Move, Maximize2 } from 'lucide-react';
5
+ import type { LayoutOption, SiteMetadata } from '../types';
6
+ import type Konva from 'konva';
7
+
8
+ interface Map2DPlotterProps {
9
+ boundaryCoords: number[][] | null;
10
+ metadata: SiteMetadata | null;
11
+ selectedOption: LayoutOption | null;
12
+ width?: number;
13
+ height?: number;
14
+ }
15
+
16
+ export const Map2DPlotter: React.FC<Map2DPlotterProps> = ({
17
+ boundaryCoords,
18
+ metadata,
19
+ selectedOption,
20
+ width = 800,
21
+ height = 600,
22
+ }) => {
23
+ // Zoom and pan state
24
+ const [scale, setScale] = useState(1);
25
+ const [position, setPosition] = useState({ x: 0, y: 0 });
26
+ const stageRef = useRef<Konva.Stage>(null);
27
+
28
+ // Calculate base transform to fit boundary in canvas
29
+ const baseTransform = useMemo(() => {
30
+ if (!boundaryCoords || boundaryCoords.length === 0) {
31
+ return { scale: 1, offsetX: 0, offsetY: 0, minX: 0, minY: 0 };
32
+ }
33
+
34
+ const xs = boundaryCoords.map(c => c[0]);
35
+ const ys = boundaryCoords.map(c => c[1]);
36
+
37
+ const minX = Math.min(...xs);
38
+ const maxX = Math.max(...xs);
39
+ const minY = Math.min(...ys);
40
+ const maxY = Math.max(...ys);
41
+
42
+ const dataWidth = maxX - minX;
43
+ const dataHeight = maxY - minY;
44
+
45
+ const padding = 60;
46
+ const scaleX = (width - padding * 2) / dataWidth;
47
+ const scaleY = (height - padding * 2) / dataHeight;
48
+ const baseScale = Math.min(scaleX, scaleY) * 0.85;
49
+
50
+ const offsetX = padding + (width - padding * 2 - dataWidth * baseScale) / 2 - minX * baseScale;
51
+ const offsetY = padding + (height - padding * 2 - dataHeight * baseScale) / 2 + maxY * baseScale;
52
+
53
+ return { scale: baseScale, offsetX, offsetY, minX, minY, maxY };
54
+ }, [boundaryCoords, width, height]);
55
+
56
+ // Transform coordinates (flip Y for screen coords)
57
+ const transformPoint = useCallback((x: number, y: number): [number, number] => {
58
+ return [
59
+ x * baseTransform.scale + baseTransform.offsetX,
60
+ baseTransform.offsetY - y * baseTransform.scale,
61
+ ];
62
+ }, [baseTransform]);
63
+
64
+ // Flatten boundary coords for Konva Line
65
+ const boundaryPoints = useMemo(() => {
66
+ if (!boundaryCoords) return [];
67
+ return boundaryCoords.flatMap(([x, y]) => transformPoint(x, y));
68
+ }, [boundaryCoords, transformPoint]);
69
+
70
+ // Calculate setback boundary (50m inside)
71
+ const setbackPoints = useMemo(() => {
72
+ if (!boundaryCoords || boundaryCoords.length < 3) return [];
73
+
74
+ const xs = boundaryCoords.map(c => c[0]);
75
+ const ys = boundaryCoords.map(c => c[1]);
76
+ const centerX = xs.reduce((a, b) => a + b, 0) / xs.length;
77
+ const centerY = ys.reduce((a, b) => a + b, 0) / ys.length;
78
+
79
+ const shrinkFactor = 0.82;
80
+ const shrunkCoords = boundaryCoords.map(([x, y]) => [
81
+ centerX + (x - centerX) * shrinkFactor,
82
+ centerY + (y - centerY) * shrinkFactor,
83
+ ]);
84
+
85
+ return shrunkCoords.flatMap(([x, y]) => transformPoint(x, y));
86
+ }, [boundaryCoords, transformPoint]);
87
+
88
+ // Handle zoom
89
+ const handleZoom = (delta: number) => {
90
+ const newScale = Math.min(Math.max(scale + delta, 0.5), 3);
91
+ setScale(newScale);
92
+ };
93
+
94
+ // Handle wheel zoom
95
+ const handleWheel = (e: Konva.KonvaEventObject<WheelEvent>) => {
96
+ e.evt.preventDefault();
97
+ const delta = e.evt.deltaY > 0 ? -0.1 : 0.1;
98
+ handleZoom(delta);
99
+ };
100
+
101
+ // Reset view
102
+ const resetView = () => {
103
+ setScale(1);
104
+ setPosition({ x: 0, y: 0 });
105
+ };
106
+
107
+ // Render plots from selected option
108
+ const renderPlots = () => {
109
+ if (!selectedOption?.plots) return null;
110
+
111
+ return selectedOption.plots.map((plot, index) => {
112
+ // Transform plot position
113
+ const [x, y] = transformPoint(plot.x, plot.y + plot.height);
114
+ const w = plot.width * baseTransform.scale;
115
+ const h = plot.height * baseTransform.scale;
116
+
117
+ // Color based on index
118
+ const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
119
+ const color = colors[index % colors.length];
120
+
121
+ return (
122
+ <Group key={index}>
123
+ <Rect
124
+ x={x}
125
+ y={y}
126
+ width={w}
127
+ height={h}
128
+ fill={`${color}33`}
129
+ stroke={color}
130
+ strokeWidth={2}
131
+ cornerRadius={2}
132
+ />
133
+ <Text
134
+ x={x + w / 2 - 12}
135
+ y={y + h / 2 - 10}
136
+ text={`P${index + 1}`}
137
+ fontSize={14}
138
+ fill={color}
139
+ fontStyle="bold"
140
+ />
141
+ <Text
142
+ x={x + w / 2 - 25}
143
+ y={y + h / 2 + 6}
144
+ text={`${Math.round(plot.area)}m²`}
145
+ fontSize={10}
146
+ fill="#666"
147
+ />
148
+ </Group>
149
+ );
150
+ });
151
+ };
152
+
153
+ return (
154
+ <div className="map-wrapper">
155
+ {/* Zoom controls */}
156
+ <div className="map-controls">
157
+ <button onClick={() => handleZoom(0.2)} title="Zoom In">
158
+ <ZoomIn size={16} />
159
+ </button>
160
+ <button onClick={() => handleZoom(-0.2)} title="Zoom Out">
161
+ <ZoomOut size={16} />
162
+ </button>
163
+ <button onClick={resetView} title="Reset View">
164
+ <Maximize2 size={16} />
165
+ </button>
166
+ <span className="zoom-level">{Math.round(scale * 100)}%</span>
167
+ </div>
168
+
169
+ <div className="map-container">
170
+ <Stage
171
+ ref={stageRef}
172
+ width={width}
173
+ height={height}
174
+ scaleX={scale}
175
+ scaleY={scale}
176
+ x={position.x}
177
+ y={position.y}
178
+ draggable
179
+ onWheel={handleWheel}
180
+ onDragEnd={(e) => setPosition({ x: e.target.x(), y: e.target.y() })}
181
+ >
182
+ <Layer>
183
+ {/* Background grid */}
184
+ {[...Array(25)].map((_, i) => (
185
+ <Line
186
+ key={`grid-h-${i}`}
187
+ points={[0, i * (height / 25), width, i * (height / 25)]}
188
+ stroke="#f0f0f0"
189
+ strokeWidth={1}
190
+ />
191
+ ))}
192
+ {[...Array(25)].map((_, i) => (
193
+ <Line
194
+ key={`grid-v-${i}`}
195
+ points={[i * (width / 25), 0, i * (width / 25), height]}
196
+ stroke="#f0f0f0"
197
+ strokeWidth={1}
198
+ />
199
+ ))}
200
+
201
+ {/* Site boundary */}
202
+ {boundaryPoints.length > 0 && (
203
+ <Line
204
+ points={boundaryPoints}
205
+ closed
206
+ stroke="#1e293b"
207
+ strokeWidth={3}
208
+ fill="rgba(226, 232, 240, 0.3)"
209
+ />
210
+ )}
211
+
212
+ {/* Setback zone */}
213
+ {setbackPoints.length > 0 && (
214
+ <Line
215
+ points={setbackPoints}
216
+ closed
217
+ stroke="#ef4444"
218
+ strokeWidth={2}
219
+ dash={[10, 5]}
220
+ fill="rgba(239, 68, 68, 0.05)"
221
+ />
222
+ )}
223
+
224
+ {/* Plots */}
225
+ {renderPlots()}
226
+
227
+ {/* Legend */}
228
+ <Group x={10} y={10}>
229
+ <Rect x={0} y={0} width={170} height={90} fill="white" opacity={0.95} cornerRadius={8} shadowBlur={5} shadowColor="rgba(0,0,0,0.1)" />
230
+ <Text x={10} y={8} text="Legend" fontSize={12} fontStyle="bold" fill="#333" />
231
+ <Line points={[10, 30, 35, 30]} stroke="#1e293b" strokeWidth={3} />
232
+ <Text x={45} y={24} text="Site Boundary" fontSize={11} fill="#666" />
233
+ <Line points={[10, 50, 35, 50]} stroke="#ef4444" strokeWidth={2} dash={[5, 3]} />
234
+ <Text x={45} y={44} text="Setback (50m)" fontSize={11} fill="#666" />
235
+ <Rect x={10} y={62} width={25} height={18} fill="rgba(59, 130, 246, 0.2)" stroke="#3B82F6" strokeWidth={2} />
236
+ <Text x={45} y={66} text="Industrial Plots" fontSize={11} fill="#666" />
237
+ </Group>
238
+
239
+ {/* Metadata */}
240
+ {metadata && (
241
+ <Group x={width - 160} y={10}>
242
+ <Rect x={0} y={0} width={150} height={60} fill="white" opacity={0.95} cornerRadius={8} shadowBlur={5} shadowColor="rgba(0,0,0,0.1)" />
243
+ <Text x={10} y={8} text="Site Info" fontSize={12} fontStyle="bold" fill="#333" />
244
+ <Text x={10} y={26} text={`Area: ${(metadata.area / 10000).toFixed(2)} ha`} fontSize={11} fill="#666" />
245
+ <Text x={10} y={42} text={`Perimeter: ${metadata.perimeter.toFixed(0)} m`} fontSize={11} fill="#666" />
246
+ </Group>
247
+ )}
248
+
249
+ {/* Empty state */}
250
+ {!boundaryCoords && (
251
+ <Group>
252
+ <Rect x={width / 2 - 120} y={height / 2 - 30} width={240} height={60} fill="#f8fafc" cornerRadius={8} />
253
+ <Text
254
+ x={width / 2 - 100}
255
+ y={height / 2 - 10}
256
+ text="Upload DXF or GeoJSON to start"
257
+ fontSize={14}
258
+ fill="#94a3b8"
259
+ />
260
+ </Group>
261
+ )}
262
+ </Layer>
263
+ </Stage>
264
+ </div>
265
+
266
+ {/* Drag hint */}
267
+ <div className="map-hint">
268
+ <Move size={12} /> Drag to pan • Scroll to zoom
269
+ </div>
270
+ </div>
271
+ );
272
+ };
273
+
274
+ export default Map2DPlotter;
frontend/src/components/index.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // Components index
2
+ export { FileUploadPanel } from './FileUploadPanel';
3
+ export { Map2DPlotter } from './Map2DPlotter';
4
+ export { LayoutOptionsPanel } from './LayoutOptionsPanel';
5
+ export { ChatInterface } from './ChatInterface';
6
+ export { ExportPanel } from './ExportPanel';
frontend/src/index.css ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Base styles - reset default Vite styles */
2
+ :root {
3
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
4
+ line-height: 1.5;
5
+ font-weight: 400;
6
+ font-synthesis: none;
7
+ text-rendering: optimizeLegibility;
8
+ -webkit-font-smoothing: antialiased;
9
+ -moz-osx-font-smoothing: grayscale;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ min-width: 320px;
21
+ min-height: 100vh;
22
+ }
23
+
24
+ #root {
25
+ min-height: 100vh;
26
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/services/api.ts ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API service for AIOptimize
2
+
3
+ import axios from 'axios';
4
+ import type {
5
+ UploadResponse,
6
+ GenerateResponse,
7
+ ChatResponse,
8
+ GeoJSONFeature
9
+ } from '../types';
10
+
11
+ const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
12
+
13
+ const api = axios.create({
14
+ baseURL: API_BASE,
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ });
19
+
20
+ export const apiService = {
21
+ // Health check
22
+ async health() {
23
+ const response = await api.get('/api/health');
24
+ return response.data;
25
+ },
26
+
27
+ // Get sample data
28
+ async getSampleData(): Promise<GeoJSONFeature> {
29
+ const response = await api.get('/api/sample-data');
30
+ return response.data;
31
+ },
32
+
33
+ // Upload boundary (JSON)
34
+ async uploadBoundary(geojson: GeoJSONFeature): Promise<UploadResponse> {
35
+ const response = await api.post('/api/upload-boundary-json', {
36
+ geojson: geojson,
37
+ });
38
+ return response.data;
39
+ },
40
+
41
+ // Upload boundary file (DXF or GeoJSON)
42
+ async uploadBoundaryFile(file: File): Promise<UploadResponse> {
43
+ const formData = new FormData();
44
+ formData.append('file', file);
45
+
46
+ // Use DXF endpoint for .dxf files
47
+ const isDxf = file.name.toLowerCase().endsWith('.dxf');
48
+ const endpoint = isDxf ? '/api/upload-dxf' : '/api/upload-boundary';
49
+
50
+ const response = await api.post(endpoint, formData, {
51
+ headers: { 'Content-Type': 'multipart/form-data' },
52
+ });
53
+ return response.data;
54
+ },
55
+
56
+ // Generate layouts
57
+ async generateLayouts(
58
+ sessionId: string,
59
+ targetPlots: number = 8,
60
+ setback: number = 50
61
+ ): Promise<GenerateResponse> {
62
+ const response = await api.post('/api/generate-layouts', {
63
+ session_id: sessionId,
64
+ target_plots: targetPlots,
65
+ setback: setback,
66
+ });
67
+ return response.data;
68
+ },
69
+
70
+ // Chat
71
+ async chat(sessionId: string, message: string): Promise<ChatResponse> {
72
+ const response = await api.post('/api/chat', {
73
+ session_id: sessionId,
74
+ message: message,
75
+ });
76
+ return response.data;
77
+ },
78
+
79
+ // Export DXF
80
+ async exportDxf(sessionId: string, optionId: number): Promise<Blob> {
81
+ const response = await api.post('/api/export-dxf', {
82
+ session_id: sessionId,
83
+ option_id: optionId,
84
+ }, {
85
+ responseType: 'blob',
86
+ });
87
+ return response.data;
88
+ },
89
+
90
+ // Export all as ZIP
91
+ async exportAllDxf(sessionId: string): Promise<Blob> {
92
+ const formData = new FormData();
93
+ formData.append('session_id', sessionId);
94
+
95
+ const response = await api.post('/api/export-all-dxf', formData, {
96
+ responseType: 'blob',
97
+ });
98
+ return response.data;
99
+ },
100
+ };
101
+
102
+ export default apiService;
frontend/src/types/index.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Type definitions for AIOptimize MVP
2
+
3
+ export interface Coordinates {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ export interface PlotData {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ area: number;
14
+ coords: number[][];
15
+ }
16
+
17
+ export interface LayoutMetrics {
18
+ total_plots: number;
19
+ total_area: number;
20
+ avg_size: number;
21
+ fitness: number;
22
+ compliance: string;
23
+ }
24
+
25
+ export interface LayoutOption {
26
+ id: number;
27
+ name: string;
28
+ icon: string;
29
+ description: string;
30
+ plots: PlotData[];
31
+ metrics: LayoutMetrics;
32
+ }
33
+
34
+ export interface SiteMetadata {
35
+ area: number;
36
+ perimeter: number;
37
+ bounds: number[];
38
+ centroid: number[];
39
+ }
40
+
41
+ export interface GeoJSONGeometry {
42
+ type: string;
43
+ coordinates: number[][][];
44
+ }
45
+
46
+ export interface GeoJSONFeature {
47
+ type: string;
48
+ geometry: GeoJSONGeometry;
49
+ properties?: Record<string, unknown>;
50
+ }
51
+
52
+ export interface ChatMessage {
53
+ role: 'user' | 'assistant';
54
+ content: string;
55
+ model?: string;
56
+ timestamp?: string;
57
+ }
58
+
59
+ export interface UploadResponse {
60
+ session_id: string;
61
+ boundary: GeoJSONFeature;
62
+ metadata: SiteMetadata;
63
+ }
64
+
65
+ export interface GenerateResponse {
66
+ session_id: string;
67
+ options: LayoutOption[];
68
+ count: number;
69
+ }
70
+
71
+ export interface ChatResponse {
72
+ message: string;
73
+ model: 'gemini-2.0-flash' | 'fallback';
74
+ }
75
+
76
+ export interface AppState {
77
+ sessionId: string | null;
78
+ boundary: GeoJSONFeature | null;
79
+ boundaryCoords: number[][] | null;
80
+ metadata: SiteMetadata | null;
81
+ options: LayoutOption[];
82
+ selectedOption: number | null;
83
+ messages: ChatMessage[];
84
+ loading: boolean;
85
+ error: string | null;
86
+ }
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ },
27
+ "include": ["src"]
28
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
requirements.txt ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # REMB Dependencies - Industrial Estate Master Planning Optimization Engine
2
+
3
+ # Core Optimization
4
+ pymoo>=0.6.0
5
+ ortools>=9.7.2876
6
+
7
+ # Geometry Processing
8
+ geopandas>=0.14.0
9
+ shapely>=2.0.0
10
+ ezdxf>=1.1.0
11
+ pyproj>=3.6.0
12
+
13
+ # API Framework
14
+ fastapi>=0.104.0
15
+ uvicorn[standard]>=0.24.0
16
+ pydantic>=2.4.0
17
+ pydantic-settings>=2.1.0
18
+ python-multipart>=0.0.6
19
+
20
+ # Database
21
+ sqlalchemy>=2.0.0
22
+ psycopg2-binary>=2.9.0
23
+ geoalchemy2>=0.14.0
24
+ alembic>=1.12.0
25
+
26
+ # Data Processing
27
+ numpy>=1.24.0
28
+ pandas>=2.0.0
29
+ pyyaml>=6.0
30
+
31
+ # Utilities
32
+ python-dotenv>=1.0.0
33
+ pydantic>=2.4.0
34
+
35
+ # Development & Testing
36
+ pytest>=7.4.0
37
+ pytest-asyncio>=0.21.0
38
+ pytest-cov>=4.1.0
39
+ black>=23.10.0
40
+ flake8>=6.1.0
41
+ mypy>=1.6.0
src/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ REMB - Industrial Estate Master Planning Optimization Engine
3
+ Main package initialization
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __author__ = "PiXerse.AI Team"
src/algorithms/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Algorithms package"""
src/algorithms/ga_optimizer.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple Genetic Algorithm Optimizer
3
+ Generates 3 diverse layout options for industrial estate planning
4
+ Following MVP-24h.md specification
5
+ """
6
+ import random
7
+ import math
8
+ from typing import List, Dict, Tuple, Any
9
+ from dataclasses import dataclass, field
10
+ from shapely.geometry import Polygon, box, Point
11
+ from shapely.ops import unary_union
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class PlotConfig:
19
+ """Plot configuration"""
20
+ x: float
21
+ y: float
22
+ width: float
23
+ height: float
24
+
25
+ @property
26
+ def area(self) -> float:
27
+ return self.width * self.height
28
+
29
+ @property
30
+ def geometry(self) -> Polygon:
31
+ return box(self.x, self.y, self.x + self.width, self.y + self.height)
32
+
33
+ def to_dict(self) -> Dict:
34
+ return {
35
+ "x": self.x,
36
+ "y": self.y,
37
+ "width": self.width,
38
+ "height": self.height,
39
+ "area": self.area,
40
+ "coords": list(self.geometry.exterior.coords)
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class LayoutCandidate:
46
+ """A layout candidate in the GA population"""
47
+ plots: List[PlotConfig] = field(default_factory=list)
48
+ fitness: float = 0.0
49
+
50
+ @property
51
+ def total_area(self) -> float:
52
+ return sum(p.area for p in self.plots)
53
+
54
+ @property
55
+ def avg_plot_size(self) -> float:
56
+ return self.total_area / len(self.plots) if self.plots else 0
57
+
58
+ def to_dict(self) -> Dict:
59
+ return {
60
+ "plots": [p.to_dict() for p in self.plots],
61
+ "total_plots": len(self.plots),
62
+ "total_area": self.total_area,
63
+ "avg_size": self.avg_plot_size,
64
+ "fitness": self.fitness
65
+ }
66
+
67
+
68
+ class SimpleGAOptimizer:
69
+ """
70
+ Simple Genetic Algorithm for layout optimization
71
+
72
+ Per MVP-24h.md:
73
+ - Population: 10 layouts
74
+ - Generations: 20
75
+ - Elite: 3 best
76
+ - Mutation rate: 30%
77
+ - Output: 3 diverse options
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ population_size: int = 10,
83
+ n_generations: int = 20,
84
+ elite_size: int = 3,
85
+ mutation_rate: float = 0.3,
86
+ setback: float = 50.0,
87
+ target_plots: int = 8
88
+ ):
89
+ self.population_size = population_size
90
+ self.n_generations = n_generations
91
+ self.elite_size = elite_size
92
+ self.mutation_rate = mutation_rate
93
+ self.setback = setback
94
+ self.target_plots = target_plots
95
+
96
+ # Plot size ranges
97
+ self.min_plot_width = 30
98
+ self.max_plot_width = 80
99
+ self.min_plot_height = 40
100
+ self.max_plot_height = 100
101
+
102
+ def optimize(self, boundary_coords: List[List[float]]) -> List[Dict]:
103
+ """
104
+ Run GA optimization and return 3 diverse layout options
105
+
106
+ Args:
107
+ boundary_coords: List of [x, y] coordinate pairs
108
+
109
+ Returns:
110
+ List of 3 layout options with different strategies
111
+ """
112
+ logger.info("Starting GA optimization")
113
+
114
+ # Create boundary polygon
115
+ boundary = Polygon(boundary_coords)
116
+ if not boundary.is_valid:
117
+ boundary = boundary.buffer(0)
118
+
119
+ # Get buildable area (after setback)
120
+ buildable = boundary.buffer(-self.setback)
121
+ if buildable.is_empty or not buildable.is_valid:
122
+ logger.warning("Buildable area too small, reducing setback")
123
+ buildable = boundary.buffer(-self.setback / 2)
124
+
125
+ bounds = buildable.bounds # (minx, miny, maxx, maxy)
126
+
127
+ # Initialize population
128
+ population = self._initialize_population(buildable, bounds)
129
+
130
+ # Evolution loop
131
+ for gen in range(self.n_generations):
132
+ # Evaluate fitness
133
+ for candidate in population:
134
+ candidate.fitness = self._evaluate_fitness(candidate, buildable, boundary)
135
+
136
+ # Sort by fitness
137
+ population.sort(key=lambda x: x.fitness, reverse=True)
138
+
139
+ # Keep elite
140
+ elite = population[:self.elite_size]
141
+
142
+ # Create new population from elite
143
+ new_population = elite.copy()
144
+
145
+ while len(new_population) < self.population_size:
146
+ parent = random.choice(elite)
147
+ child = self._mutate(parent, bounds, buildable)
148
+ new_population.append(child)
149
+
150
+ population = new_population
151
+
152
+ # Final sort
153
+ for candidate in population:
154
+ candidate.fitness = self._evaluate_fitness(candidate, buildable, boundary)
155
+ population.sort(key=lambda x: x.fitness, reverse=True)
156
+
157
+ # Create 3 diverse options
158
+ options = self._create_diverse_options(population, buildable, bounds, boundary)
159
+
160
+ logger.info(f"GA complete: {len(options)} options generated")
161
+ return options
162
+
163
+ def _initialize_population(self, buildable: Polygon, bounds: Tuple) -> List[LayoutCandidate]:
164
+ """Create initial random population"""
165
+ population = []
166
+ minx, miny, maxx, maxy = bounds
167
+
168
+ for _ in range(self.population_size):
169
+ candidate = LayoutCandidate()
170
+ placed = []
171
+
172
+ for _ in range(self.target_plots):
173
+ # Random plot dimensions
174
+ width = random.uniform(self.min_plot_width, self.max_plot_width)
175
+ height = random.uniform(self.min_plot_height, self.max_plot_height)
176
+
177
+ # Random position
178
+ for attempt in range(20):
179
+ x = random.uniform(minx, maxx - width)
180
+ y = random.uniform(miny, maxy - height)
181
+
182
+ plot = PlotConfig(x=x, y=y, width=width, height=height)
183
+
184
+ # Check if within buildable and no overlap
185
+ if buildable.contains(plot.geometry):
186
+ overlaps = False
187
+ for existing in placed:
188
+ if plot.geometry.intersects(existing.geometry):
189
+ overlaps = True
190
+ break
191
+
192
+ if not overlaps:
193
+ placed.append(plot)
194
+ break
195
+
196
+ candidate.plots = placed
197
+ population.append(candidate)
198
+
199
+ return population
200
+
201
+ def _evaluate_fitness(self, candidate: LayoutCandidate, buildable: Polygon, boundary: Polygon) -> float:
202
+ """
203
+ Evaluate fitness of a layout candidate
204
+
205
+ Fitness = (Profit × 0.5) + (Compliance × 0.3) + (Efficiency × 0.2)
206
+ """
207
+ if not candidate.plots:
208
+ return 0.0
209
+
210
+ # Profit score (normalized total area)
211
+ max_area = buildable.area * 0.6 # Max 60% coverage
212
+ profit = min(candidate.total_area / max_area, 1.0)
213
+
214
+ # Compliance score (all plots within setback)
215
+ compliant = sum(1 for p in candidate.plots if buildable.contains(p.geometry))
216
+ compliance = compliant / len(candidate.plots)
217
+
218
+ # Efficiency score (plot count vs target)
219
+ efficiency = min(len(candidate.plots) / self.target_plots, 1.0)
220
+
221
+ fitness = (profit * 0.5) + (compliance * 0.3) + (efficiency * 0.2)
222
+ return round(fitness, 4)
223
+
224
+ def _mutate(self, parent: LayoutCandidate, bounds: Tuple, buildable: Polygon) -> LayoutCandidate:
225
+ """Create mutated child from parent"""
226
+ child = LayoutCandidate()
227
+ minx, miny, maxx, maxy = bounds
228
+
229
+ for plot in parent.plots:
230
+ if random.random() < self.mutation_rate:
231
+ # Mutate position (±30m)
232
+ new_x = plot.x + random.uniform(-30, 30)
233
+ new_y = plot.y + random.uniform(-30, 30)
234
+
235
+ # Keep within bounds
236
+ new_x = max(minx, min(new_x, maxx - plot.width))
237
+ new_y = max(miny, min(new_y, maxy - plot.height))
238
+
239
+ new_plot = PlotConfig(x=new_x, y=new_y, width=plot.width, height=plot.height)
240
+
241
+ if buildable.contains(new_plot.geometry):
242
+ child.plots.append(new_plot)
243
+ else:
244
+ child.plots.append(plot)
245
+ else:
246
+ child.plots.append(plot)
247
+
248
+ return child
249
+
250
+ def _create_diverse_options(
251
+ self,
252
+ population: List[LayoutCandidate],
253
+ buildable: Polygon,
254
+ bounds: Tuple,
255
+ boundary: Polygon
256
+ ) -> List[Dict]:
257
+ """
258
+ Create 3 diverse layout options:
259
+ 1. Maximum Profit (most plots)
260
+ 2. Balanced (medium density)
261
+ 3. Premium (fewer, larger plots)
262
+ """
263
+ options = []
264
+
265
+ # Option 1: Maximum Profit (best fitness from GA)
266
+ if population:
267
+ best = population[0]
268
+ options.append({
269
+ "id": 1,
270
+ "name": "Maximum Profit",
271
+ "icon": "💰",
272
+ "description": "Maximizes sellable area with more plots",
273
+ "plots": [p.to_dict() for p in best.plots],
274
+ "metrics": {
275
+ "total_plots": len(best.plots),
276
+ "total_area": round(best.total_area, 2),
277
+ "avg_size": round(best.avg_plot_size, 2),
278
+ "fitness": best.fitness,
279
+ "compliance": "PASS"
280
+ }
281
+ })
282
+
283
+ # Option 2: Balanced - Generate with medium density
284
+ balanced = self._generate_balanced_layout(buildable, bounds)
285
+ options.append({
286
+ "id": 2,
287
+ "name": "Balanced",
288
+ "icon": "⚖️",
289
+ "description": "Balanced approach with medium-sized plots",
290
+ "plots": [p.to_dict() for p in balanced.plots],
291
+ "metrics": {
292
+ "total_plots": len(balanced.plots),
293
+ "total_area": round(balanced.total_area, 2),
294
+ "avg_size": round(balanced.avg_plot_size, 2),
295
+ "fitness": self._evaluate_fitness(balanced, buildable, boundary),
296
+ "compliance": "PASS"
297
+ }
298
+ })
299
+
300
+ # Option 3: Premium - Fewer, larger plots
301
+ premium = self._generate_premium_layout(buildable, bounds)
302
+ options.append({
303
+ "id": 3,
304
+ "name": "Premium",
305
+ "icon": "🏢",
306
+ "description": "Premium layout with fewer, larger plots",
307
+ "plots": [p.to_dict() for p in premium.plots],
308
+ "metrics": {
309
+ "total_plots": len(premium.plots),
310
+ "total_area": round(premium.total_area, 2),
311
+ "avg_size": round(premium.avg_plot_size, 2),
312
+ "fitness": self._evaluate_fitness(premium, buildable, boundary),
313
+ "compliance": "PASS"
314
+ }
315
+ })
316
+
317
+ return options
318
+
319
+ def _generate_balanced_layout(self, buildable: Polygon, bounds: Tuple) -> LayoutCandidate:
320
+ """Generate balanced layout with medium plot sizes"""
321
+ candidate = LayoutCandidate()
322
+ minx, miny, maxx, maxy = bounds
323
+
324
+ # Medium plot size
325
+ plot_width = 50
326
+ plot_height = 70
327
+ spacing = 20
328
+
329
+ placed = []
330
+ y = miny + spacing
331
+
332
+ while y + plot_height < maxy:
333
+ x = minx + spacing
334
+ while x + plot_width < maxx:
335
+ plot = PlotConfig(x=x, y=y, width=plot_width, height=plot_height)
336
+
337
+ if buildable.contains(plot.geometry):
338
+ overlaps = False
339
+ for existing in placed:
340
+ if plot.geometry.intersects(existing.geometry):
341
+ overlaps = True
342
+ break
343
+
344
+ if not overlaps:
345
+ placed.append(plot)
346
+
347
+ x += plot_width + spacing
348
+ y += plot_height + spacing
349
+
350
+ candidate.plots = placed[:8] # Limit to 8 plots
351
+ return candidate
352
+
353
+ def _generate_premium_layout(self, buildable: Polygon, bounds: Tuple) -> LayoutCandidate:
354
+ """Generate premium layout with fewer, larger plots"""
355
+ candidate = LayoutCandidate()
356
+ minx, miny, maxx, maxy = bounds
357
+
358
+ # Large plot size
359
+ plot_width = 80
360
+ plot_height = 100
361
+ spacing = 30
362
+
363
+ placed = []
364
+ y = miny + spacing
365
+
366
+ while y + plot_height < maxy and len(placed) < 4:
367
+ x = minx + spacing
368
+ while x + plot_width < maxx and len(placed) < 4:
369
+ plot = PlotConfig(x=x, y=y, width=plot_width, height=plot_height)
370
+
371
+ if buildable.contains(plot.geometry):
372
+ overlaps = False
373
+ for existing in placed:
374
+ if plot.geometry.intersects(existing.geometry):
375
+ overlaps = True
376
+ break
377
+
378
+ if not overlaps:
379
+ placed.append(plot)
380
+
381
+ x += plot_width + spacing
382
+ y += plot_height + spacing
383
+
384
+ candidate.plots = placed
385
+ return candidate
386
+
387
+
388
+ # Example usage
389
+ if __name__ == "__main__":
390
+ logging.basicConfig(level=logging.INFO)
391
+
392
+ # Sample boundary (simple rectangle)
393
+ boundary = [
394
+ [0, 0], [500, 0], [500, 400], [0, 400], [0, 0]
395
+ ]
396
+
397
+ optimizer = SimpleGAOptimizer()
398
+ options = optimizer.optimize(boundary)
399
+
400
+ for opt in options:
401
+ print(f"\n{opt['icon']} {opt['name']}")
402
+ print(f" Plots: {opt['metrics']['total_plots']}")
403
+ print(f" Area: {opt['metrics']['total_area']} m²")
404
+ print(f" Avg: {opt['metrics']['avg_size']} m²")
405
+ print(f" Fitness: {opt['metrics']['fitness']}")
src/algorithms/milp_solver.py ADDED
@@ -0,0 +1,659 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MILP Solver - Module B: The Engineer (CP Module)
3
+ Mixed-Integer Linear Programming solver for precision constraints using OR-Tools
4
+ Đảm bảo tính hợp lệ toán học tuyệt đối - loại bỏ "hallucination"
5
+ """
6
+ import numpy as np
7
+ from ortools.linear_solver import pywraplp
8
+ from ortools.sat.python import cp_model
9
+ from typing import List, Tuple, Dict, Optional, Any
10
+ from dataclasses import dataclass, field
11
+ import json
12
+ import logging
13
+
14
+ from src.models.domain import Layout, Plot, PlotType, SiteBoundary, RoadNetwork
15
+ from shapely.geometry import Polygon, box, LineString, MultiLineString
16
+ from shapely.ops import unary_union
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class MILPResult:
23
+ """Result from MILP solver"""
24
+ status: str # 'OPTIMAL', 'FEASIBLE', 'INFEASIBLE', 'TIMEOUT'
25
+ objective_value: float = 0.0
26
+ solve_time_seconds: float = 0.0
27
+ plots: List[Dict[str, Any]] = field(default_factory=list)
28
+ error_message: Optional[str] = None
29
+
30
+ def is_success(self) -> bool:
31
+ return self.status in ['OPTIMAL', 'FEASIBLE']
32
+
33
+ def to_json(self) -> str:
34
+ """Export result as JSON for LLM interpretation"""
35
+ return json.dumps({
36
+ 'status': self.status,
37
+ 'objective_value': self.objective_value,
38
+ 'solve_time_seconds': self.solve_time_seconds,
39
+ 'num_plots': len(self.plots),
40
+ 'plots': self.plots,
41
+ 'error_message': self.error_message
42
+ }, indent=2)
43
+
44
+
45
+ class MILPSolver:
46
+ """
47
+ Mixed-Integer Linear Programming Solver - The "Muscle/Kỹ sư"
48
+
49
+ Responsibilities:
50
+ - Enforce non-overlapping constraints mathematically
51
+ - Ensure road connectivity for all plots
52
+ - Guarantee geometric closure and snapping
53
+ - Provide exact numerical solutions (no hallucination)
54
+
55
+ This is a "black-box" that receives JSON parameters and returns raw results.
56
+ """
57
+
58
+ def __init__(self, time_limit_seconds: int = 3600, solver_type: str = "SCIP"):
59
+ """
60
+ Initialize MILP Solver
61
+
62
+ Args:
63
+ time_limit_seconds: Maximum solve time
64
+ solver_type: Solver backend ('SCIP', 'CBC', 'GLOP', 'SAT')
65
+ """
66
+ self.time_limit_seconds = time_limit_seconds
67
+ self.solver_type = solver_type
68
+ self.logger = logging.getLogger(__name__)
69
+
70
+ # Try to find an available solver
71
+ self._available_solver = self._find_available_solver()
72
+
73
+ def _find_available_solver(self) -> str:
74
+ """Find an available solver"""
75
+ solvers_to_try = [self.solver_type, 'SCIP', 'CBC', 'GLOP', 'SAT']
76
+
77
+ for solver_name in solvers_to_try:
78
+ solver = pywraplp.Solver.CreateSolver(solver_name)
79
+ if solver:
80
+ self.logger.info(f"Using solver: {solver_name}")
81
+ return solver_name
82
+
83
+ self.logger.warning("No LP solver available, will use CP-SAT only")
84
+ return None
85
+
86
+ def validate_and_refine(self, layout: Layout) -> Tuple[Layout, MILPResult]:
87
+ """
88
+ Validate and refine a layout using MILP constraints
89
+
90
+ This is the main entry point - receives layout, returns refined layout with
91
+ mathematically guaranteed validity.
92
+
93
+ Args:
94
+ layout: Layout to validate and refine
95
+
96
+ Returns:
97
+ Tuple of (refined_layout, result)
98
+ """
99
+ self.logger.info(f"MILP validation for layout {layout.id}")
100
+ import time
101
+ start_time = time.time()
102
+
103
+ # Step 1: Check for overlaps and fix
104
+ overlap_result = self._resolve_overlaps(layout)
105
+ if not overlap_result.is_success():
106
+ return layout, overlap_result
107
+
108
+ # Step 2: Ensure road connectivity
109
+ connectivity_result = self._ensure_road_connectivity(layout)
110
+ if not connectivity_result.is_success():
111
+ return layout, connectivity_result
112
+
113
+ # Step 3: Snap geometries to grid
114
+ self._snap_geometries(layout)
115
+
116
+ # Step 4: Validate final geometry closure
117
+ closure_result = self._validate_geometry_closure(layout)
118
+
119
+ solve_time = time.time() - start_time
120
+
121
+ result = MILPResult(
122
+ status='OPTIMAL' if closure_result else 'FEASIBLE',
123
+ objective_value=layout.metrics.sellable_area_sqm,
124
+ solve_time_seconds=solve_time,
125
+ plots=[{
126
+ 'id': p.id,
127
+ 'area_sqm': p.area_sqm,
128
+ 'type': p.type.value,
129
+ 'has_road_access': p.has_road_access
130
+ } for p in layout.plots]
131
+ )
132
+
133
+ self.logger.info(f"MILP validation complete: {result.status} in {solve_time:.2f}s")
134
+
135
+ return layout, result
136
+
137
+ def _resolve_overlaps(self, layout: Layout) -> MILPResult:
138
+ """
139
+ Resolve plot overlaps using constraint programming
140
+
141
+ Uses OR-Tools CP-SAT solver to find non-overlapping placement
142
+ """
143
+ industrial_plots = [p for p in layout.plots if p.type == PlotType.INDUSTRIAL]
144
+
145
+ if len(industrial_plots) < 2:
146
+ return MILPResult(status='OPTIMAL')
147
+
148
+ # Check for overlaps
149
+ overlaps_found = []
150
+ for i, p1 in enumerate(industrial_plots):
151
+ for p2 in industrial_plots[i+1:]:
152
+ if p1.geometry and p2.geometry:
153
+ if p1.geometry.intersects(p2.geometry):
154
+ intersection = p1.geometry.intersection(p2.geometry)
155
+ if intersection.area > 1.0: # 1 sqm tolerance
156
+ overlaps_found.append((p1.id, p2.id, intersection.area))
157
+
158
+ if not overlaps_found:
159
+ return MILPResult(status='OPTIMAL')
160
+
161
+ self.logger.warning(f"Found {len(overlaps_found)} overlapping plot pairs")
162
+
163
+ # Use CP-SAT to resolve overlaps
164
+ model = cp_model.CpModel()
165
+
166
+ # Get site bounds
167
+ minx, miny, maxx, maxy = layout.site_boundary.geometry.bounds
168
+ site_width = int(maxx - minx)
169
+ site_height = int(maxy - miny)
170
+
171
+ # Decision variables: position of each plot
172
+ plot_vars = {}
173
+ for plot in industrial_plots:
174
+ width = int(plot.width_m) if plot.width_m > 0 else 50
175
+ height = int(plot.depth_m) if plot.depth_m > 0 else 50
176
+
177
+ # X and Y positions
178
+ x = model.NewIntVar(0, site_width - width, f'x_{plot.id}')
179
+ y = model.NewIntVar(0, site_height - height, f'y_{plot.id}')
180
+
181
+ plot_vars[plot.id] = {
182
+ 'x': x,
183
+ 'y': y,
184
+ 'width': width,
185
+ 'height': height,
186
+ 'plot': plot
187
+ }
188
+
189
+ # Non-overlap constraints (using interval variables)
190
+ x_intervals = []
191
+ y_intervals = []
192
+
193
+ for plot_id, vars in plot_vars.items():
194
+ x_interval = model.NewIntervalVar(
195
+ vars['x'],
196
+ vars['width'],
197
+ vars['x'] + vars['width'],
198
+ f'x_interval_{plot_id}'
199
+ )
200
+ y_interval = model.NewIntervalVar(
201
+ vars['y'],
202
+ vars['height'],
203
+ vars['y'] + vars['height'],
204
+ f'y_interval_{plot_id}'
205
+ )
206
+ x_intervals.append(x_interval)
207
+ y_intervals.append(y_interval)
208
+
209
+ # Add 2D no-overlap constraint
210
+ model.AddNoOverlap2D(x_intervals, y_intervals)
211
+
212
+ # Solve
213
+ solver = cp_model.CpSolver()
214
+ solver.parameters.max_time_in_seconds = min(60, self.time_limit_seconds)
215
+
216
+ status = solver.Solve(model)
217
+
218
+ if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
219
+ # Update plot positions
220
+ for plot_id, vars in plot_vars.items():
221
+ new_x = solver.Value(vars['x']) + minx
222
+ new_y = solver.Value(vars['y']) + miny
223
+ width = vars['width']
224
+ height = vars['height']
225
+
226
+ # Update plot geometry
227
+ plot = vars['plot']
228
+ plot.geometry = box(new_x, new_y, new_x + width, new_y + height)
229
+ plot.area_sqm = plot.geometry.area
230
+
231
+ return MILPResult(
232
+ status='OPTIMAL' if status == cp_model.OPTIMAL else 'FEASIBLE',
233
+ solve_time_seconds=solver.WallTime()
234
+ )
235
+ else:
236
+ return MILPResult(
237
+ status='INFEASIBLE',
238
+ error_message='Cannot resolve overlaps - site may be too constrained'
239
+ )
240
+
241
+ def _ensure_road_connectivity(self, layout: Layout) -> MILPResult:
242
+ """
243
+ Ensure all industrial plots have road access
244
+
245
+ Uses simple distance-based connectivity check
246
+ """
247
+ max_distance = 200 # meters (from regulations)
248
+
249
+ # If no road network, create a simple grid
250
+ if not layout.road_network or not layout.road_network.primary_roads:
251
+ self._generate_simple_road_network(layout)
252
+
253
+ # Check connectivity for each industrial plot
254
+ disconnected_plots = []
255
+ all_roads = []
256
+
257
+ if layout.road_network:
258
+ if layout.road_network.primary_roads:
259
+ all_roads.extend(layout.road_network.primary_roads.geoms if hasattr(layout.road_network.primary_roads, 'geoms') else [layout.road_network.primary_roads])
260
+ if layout.road_network.secondary_roads:
261
+ all_roads.extend(layout.road_network.secondary_roads.geoms if hasattr(layout.road_network.secondary_roads, 'geoms') else [layout.road_network.secondary_roads])
262
+
263
+ for plot in layout.plots:
264
+ if plot.type == PlotType.INDUSTRIAL and plot.geometry:
265
+ # Find minimum distance to any road
266
+ min_distance = float('inf')
267
+ for road in all_roads:
268
+ dist = plot.geometry.distance(road)
269
+ min_distance = min(min_distance, dist)
270
+
271
+ if min_distance <= max_distance:
272
+ plot.has_road_access = True
273
+ else:
274
+ plot.has_road_access = False
275
+ disconnected_plots.append(plot.id)
276
+
277
+ if disconnected_plots:
278
+ self.logger.warning(f"Plots without road access: {disconnected_plots}")
279
+ return MILPResult(
280
+ status='FEASIBLE',
281
+ error_message=f'Plots {disconnected_plots} exceed {max_distance}m from road'
282
+ )
283
+
284
+ return MILPResult(status='OPTIMAL')
285
+
286
+ def _generate_simple_road_network(self, layout: Layout):
287
+ """Generate a simple grid road network"""
288
+ bounds = layout.site_boundary.geometry.bounds
289
+ minx, miny, maxx, maxy = bounds
290
+
291
+ # Create primary roads (cross pattern)
292
+ center_x = (minx + maxx) / 2
293
+ center_y = (miny + maxy) / 2
294
+
295
+ horizontal = LineString([(minx, center_y), (maxx, center_y)])
296
+ vertical = LineString([(center_x, miny), (center_x, maxy)])
297
+
298
+ layout.road_network = RoadNetwork(
299
+ primary_roads=MultiLineString([horizontal, vertical]),
300
+ total_length_m=horizontal.length + vertical.length
301
+ )
302
+
303
+ # Calculate road area (assume 24m width for primary roads)
304
+ road_width = 24
305
+ road_area = layout.road_network.total_length_m * road_width
306
+ layout.road_network.total_area_sqm = road_area
307
+
308
+ def _snap_geometries(self, layout: Layout, grid_size: float = 1.0):
309
+ """
310
+ Snap all geometries to a grid for clean coordinates
311
+
312
+ Args:
313
+ layout: Layout to snap
314
+ grid_size: Grid size in meters (default 1m)
315
+ """
316
+ for plot in layout.plots:
317
+ if plot.geometry:
318
+ coords = list(plot.geometry.exterior.coords)
319
+ snapped_coords = [
320
+ (round(x / grid_size) * grid_size, round(y / grid_size) * grid_size)
321
+ for x, y in coords
322
+ ]
323
+ try:
324
+ plot.geometry = Polygon(snapped_coords)
325
+ plot.area_sqm = plot.geometry.area
326
+ except Exception as e:
327
+ self.logger.warning(f"Failed to snap plot {plot.id}: {e}")
328
+
329
+ def _validate_geometry_closure(self, layout: Layout) -> bool:
330
+ """
331
+ Validate that all geometries are properly closed
332
+
333
+ Returns:
334
+ True if all geometries are valid
335
+ """
336
+ all_valid = True
337
+
338
+ for plot in layout.plots:
339
+ if plot.geometry:
340
+ if not plot.geometry.is_valid:
341
+ self.logger.warning(f"Plot {plot.id} has invalid geometry")
342
+ # Try to fix
343
+ plot.geometry = plot.geometry.buffer(0)
344
+ if not plot.geometry.is_valid:
345
+ all_valid = False
346
+
347
+ return all_valid
348
+
349
+ def solve_plot_placement(
350
+ self,
351
+ site_boundary: SiteBoundary,
352
+ num_plots: int,
353
+ min_plot_size: float = 1000,
354
+ max_plot_size: float = 10000,
355
+ setback: float = 50
356
+ ) -> MILPResult:
357
+ """
358
+ Solve optimal plot placement from scratch using MILP
359
+
360
+ This is the "black-box" function that LLM can call via Function Calling.
361
+ Receives JSON-like parameters, returns raw numerical results.
362
+
363
+ Args:
364
+ site_boundary: Site boundary polygon
365
+ num_plots: Target number of plots
366
+ min_plot_size: Minimum plot area in sqm
367
+ max_plot_size: Maximum plot area in sqm
368
+ setback: Boundary setback in meters
369
+
370
+ Returns:
371
+ MILPResult with plot placements
372
+ """
373
+ self.logger.info(f"MILP solving for {num_plots} plots")
374
+ import time
375
+ start_time = time.time()
376
+
377
+ # Create solver - use available solver or fallback to CP-SAT
378
+ solver_to_use = self._available_solver or 'SAT'
379
+ solver = pywraplp.Solver.CreateSolver(solver_to_use)
380
+ if not solver:
381
+ # Fallback: Use CP-SAT based approach
382
+ return self._solve_with_cpsat(site_boundary, num_plots, min_plot_size, max_plot_size, setback)
383
+
384
+ solver.SetTimeLimit(self.time_limit_seconds * 1000) # Convert to ms
385
+
386
+ # Get buildable area (after setback)
387
+ buildable = site_boundary.geometry.buffer(-setback)
388
+ if buildable.is_empty:
389
+ return MILPResult(
390
+ status='INFEASIBLE',
391
+ error_message=f'Site too small for {setback}m setback'
392
+ )
393
+
394
+ bounds = buildable.bounds
395
+ minx, miny, maxx, maxy = bounds
396
+ width = maxx - minx
397
+ height = maxy - miny
398
+
399
+ # Decision variables
400
+ infinity = solver.infinity()
401
+
402
+ # For each plot: x, y, w, h (continuous), active (binary)
403
+ plots_vars = []
404
+ for i in range(num_plots):
405
+ x = solver.NumVar(0, width, f'x_{i}')
406
+ y = solver.NumVar(0, height, f'y_{i}')
407
+ w = solver.NumVar(20, 200, f'w_{i}') # 20-200m width
408
+ h = solver.NumVar(20, 200, f'h_{i}') # 20-200m height
409
+ active = solver.IntVar(0, 1, f'active_{i}')
410
+
411
+ plots_vars.append({
412
+ 'x': x, 'y': y, 'w': w, 'h': h,
413
+ 'active': active, 'index': i
414
+ })
415
+
416
+ # Boundary constraints
417
+ solver.Add(x + w <= width)
418
+ solver.Add(y + h <= height)
419
+
420
+ # Size constraints
421
+ solver.Add(w * h >= min_plot_size * active)
422
+ solver.Add(w * h <= max_plot_size)
423
+
424
+ # Objective: Maximize total active plot area
425
+ objective = solver.Objective()
426
+ for pv in plots_vars:
427
+ # Approximate area (linearization)
428
+ objective.SetCoefficient(pv['w'], 100)
429
+ objective.SetCoefficient(pv['h'], 100)
430
+ objective.SetCoefficient(pv['active'], min_plot_size)
431
+ objective.SetMaximization()
432
+
433
+ # Solve
434
+ status = solver.Solve()
435
+
436
+ solve_time = time.time() - start_time
437
+
438
+ # Parse results
439
+ if status in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]:
440
+ result_plots = []
441
+ for pv in plots_vars:
442
+ if pv['active'].solution_value() > 0.5:
443
+ x = pv['x'].solution_value() + minx + setback
444
+ y = pv['y'].solution_value() + miny + setback
445
+ w = pv['w'].solution_value()
446
+ h = pv['h'].solution_value()
447
+
448
+ result_plots.append({
449
+ 'id': f'plot_{pv["index"]}',
450
+ 'x': x,
451
+ 'y': y,
452
+ 'width': w,
453
+ 'height': h,
454
+ 'area_sqm': w * h,
455
+ 'type': 'industrial'
456
+ })
457
+
458
+ return MILPResult(
459
+ status='OPTIMAL' if status == pywraplp.Solver.OPTIMAL else 'FEASIBLE',
460
+ objective_value=solver.Objective().Value(),
461
+ solve_time_seconds=solve_time,
462
+ plots=result_plots
463
+ )
464
+ else:
465
+ status_map = {
466
+ pywraplp.Solver.INFEASIBLE: 'INFEASIBLE',
467
+ pywraplp.Solver.UNBOUNDED: 'UNBOUNDED',
468
+ pywraplp.Solver.NOT_SOLVED: 'TIMEOUT'
469
+ }
470
+ return MILPResult(
471
+ status=status_map.get(status, 'ERROR'),
472
+ solve_time_seconds=solve_time,
473
+ error_message='Could not find valid plot placement'
474
+ )
475
+
476
+ def _solve_with_cpsat(
477
+ self,
478
+ site_boundary: SiteBoundary,
479
+ num_plots: int,
480
+ min_plot_size: float,
481
+ max_plot_size: float,
482
+ setback: float
483
+ ) -> MILPResult:
484
+ """
485
+ Fallback solver using CP-SAT for plot placement
486
+ """
487
+ import time
488
+ start_time = time.time()
489
+
490
+ # Get buildable area
491
+ buildable = site_boundary.geometry.buffer(-setback)
492
+ if buildable.is_empty:
493
+ return MILPResult(
494
+ status='INFEASIBLE',
495
+ error_message=f'Site too small for {setback}m setback'
496
+ )
497
+
498
+ minx, miny, maxx, maxy = buildable.bounds
499
+ width = int(maxx - minx)
500
+ height = int(maxy - miny)
501
+
502
+ # Use CP-SAT
503
+ model = cp_model.CpModel()
504
+
505
+ # Fixed plot dimensions for simplicity
506
+ plot_width = int(min_plot_size ** 0.5) # Square plots
507
+ plot_height = plot_width
508
+
509
+ # Create plot variables
510
+ plot_vars = []
511
+ x_intervals = []
512
+ y_intervals = []
513
+
514
+ for i in range(num_plots):
515
+ x = model.NewIntVar(0, max(0, width - plot_width), f'x_{i}')
516
+ y = model.NewIntVar(0, max(0, height - plot_height), f'y_{i}')
517
+
518
+ x_interval = model.NewIntervalVar(x, plot_width, x + plot_width, f'x_int_{i}')
519
+ y_interval = model.NewIntervalVar(y, plot_height, y + plot_height, f'y_int_{i}')
520
+
521
+ plot_vars.append({'x': x, 'y': y, 'width': plot_width, 'height': plot_height})
522
+ x_intervals.append(x_interval)
523
+ y_intervals.append(y_interval)
524
+
525
+ # No overlap constraint
526
+ model.AddNoOverlap2D(x_intervals, y_intervals)
527
+
528
+ # Solve
529
+ solver = cp_model.CpSolver()
530
+ solver.parameters.max_time_in_seconds = min(30, self.time_limit_seconds)
531
+ status = solver.Solve(model)
532
+
533
+ solve_time = time.time() - start_time
534
+
535
+ if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
536
+ result_plots = []
537
+ for i, pv in enumerate(plot_vars):
538
+ x = solver.Value(pv['x']) + minx + setback
539
+ y = solver.Value(pv['y']) + miny + setback
540
+
541
+ result_plots.append({
542
+ 'id': f'plot_{i}',
543
+ 'x': x,
544
+ 'y': y,
545
+ 'width': pv['width'],
546
+ 'height': pv['height'],
547
+ 'area_sqm': pv['width'] * pv['height'],
548
+ 'type': 'industrial'
549
+ })
550
+
551
+ return MILPResult(
552
+ status='OPTIMAL' if status == cp_model.OPTIMAL else 'FEASIBLE',
553
+ objective_value=len(result_plots) * plot_width * plot_height,
554
+ solve_time_seconds=solve_time,
555
+ plots=result_plots
556
+ )
557
+ else:
558
+ return MILPResult(
559
+ status='INFEASIBLE',
560
+ solve_time_seconds=solve_time,
561
+ error_message='Could not place plots without overlap'
562
+ )
563
+
564
+ def to_json_interface(self, request: Dict[str, Any]) -> str:
565
+ """
566
+ JSON interface for LLM Function Calling
567
+
568
+ This is the standardized interface that LLM uses to call the CP Module.
569
+
570
+ Input format:
571
+ {
572
+ "action": "solve_placement" | "validate_layout",
573
+ "parameters": {...}
574
+ }
575
+
576
+ Output format:
577
+ {
578
+ "status": "OPTIMAL" | "FEASIBLE" | "INFEASIBLE" | "ERROR",
579
+ "result": {...}
580
+ }
581
+ """
582
+ action = request.get('action')
583
+ params = request.get('parameters', {})
584
+
585
+ try:
586
+ if action == 'solve_placement':
587
+ # Create site boundary from params
588
+ bounds = params.get('bounds', [0, 0, 500, 500])
589
+ site_geom = box(*bounds)
590
+ site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
591
+
592
+ result = self.solve_plot_placement(
593
+ site_boundary=site,
594
+ num_plots=params.get('num_plots', 10),
595
+ min_plot_size=params.get('min_plot_size', 1000),
596
+ max_plot_size=params.get('max_plot_size', 10000),
597
+ setback=params.get('setback', 50)
598
+ )
599
+
600
+ elif action == 'validate_layout':
601
+ # Would need to deserialize layout from JSON
602
+ return json.dumps({
603
+ 'status': 'ERROR',
604
+ 'error_message': 'validate_layout requires Layout object'
605
+ })
606
+ else:
607
+ return json.dumps({
608
+ 'status': 'ERROR',
609
+ 'error_message': f'Unknown action: {action}'
610
+ })
611
+
612
+ return result.to_json()
613
+
614
+ except Exception as e:
615
+ return json.dumps({
616
+ 'status': 'ERROR',
617
+ 'error_message': str(e)
618
+ })
619
+
620
+
621
+ # Example usage
622
+ if __name__ == "__main__":
623
+ from shapely.geometry import box as shapely_box
624
+
625
+ # Create test site
626
+ site_geom = shapely_box(0, 0, 500, 500)
627
+ site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
628
+ site.buildable_area_sqm = site.area_sqm
629
+
630
+ # Test MILP solver
631
+ solver = MILPSolver(time_limit_seconds=60)
632
+
633
+ # Test plot placement
634
+ result = solver.solve_plot_placement(
635
+ site_boundary=site,
636
+ num_plots=10,
637
+ min_plot_size=1000,
638
+ max_plot_size=5000,
639
+ setback=50
640
+ )
641
+
642
+ print(f"Status: {result.status}")
643
+ print(f"Solve time: {result.solve_time_seconds:.2f}s")
644
+ print(f"Number of plots: {len(result.plots)}")
645
+
646
+ # Test JSON interface
647
+ json_request = {
648
+ "action": "solve_placement",
649
+ "parameters": {
650
+ "bounds": [0, 0, 500, 500],
651
+ "num_plots": 10,
652
+ "min_plot_size": 1000,
653
+ "setback": 50
654
+ }
655
+ }
656
+
657
+ json_response = solver.to_json_interface(json_request)
658
+ print("\nJSON Response:")
659
+ print(json_response)
src/algorithms/nsga2_optimizer.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NSGA-II Optimizer - Module A: The Architect
3
+ Multi-objective genetic algorithm for industrial estate layout optimization
4
+ """
5
+ import numpy as np
6
+ from pymoo.algorithms.moo.nsga2 import NSGA2
7
+ from pymoo.core.problem import Problem
8
+ from pymoo.optimize import minimize
9
+ from pymoo.operators.crossover.sbx import SBX
10
+ from pymoo.operators.mutation.pm import PM
11
+ from pymoo.operators.sampling.rnd import FloatRandomSampling
12
+ from typing import List, Tuple
13
+ import yaml
14
+ from pathlib import Path
15
+
16
+ from src.models.domain import Layout, SiteBoundary, Plot, PlotType, ParetoFront, RoadNetwork
17
+ from shapely.geometry import Polygon, box
18
+ import logging
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class IndustrialEstateProblem(Problem):
24
+ """
25
+ Multi-objective optimization problem for industrial estate layout
26
+
27
+ Objectives:
28
+ 1. Maximize sellable area
29
+ 2. Maximize green space
30
+ 3. Minimize road network length
31
+ 4. Maximize regulatory compliance score
32
+ """
33
+
34
+ def __init__(self, site_boundary: SiteBoundary, regulations: dict, n_plots: int = 20):
35
+ """
36
+ Initialize optimization problem
37
+
38
+ Args:
39
+ site_boundary: Site boundary with constraints
40
+ regulations: Regulatory requirements from YAML
41
+ n_plots: Target number of industrial plots
42
+ """
43
+ self.site_boundary = site_boundary
44
+ self.regulations = regulations
45
+ self.n_plots = n_plots
46
+
47
+ # Decision variables: [x1, y1, width1, height1, orientation1, ..., xN, yN, widthN, heightN, orientationN]
48
+ # 5 variables per plot: x, y position (normalized), width, height (meters), orientation (0-360)
49
+ n_var = n_plots * 5
50
+
51
+ # Variable bounds
52
+ xl = np.array([0, 0, 20, 20, 0] * n_plots) # Lower bounds
53
+ xu = np.array([1, 1, 200, 200, 360] * n_plots) # Upper bounds
54
+
55
+ super().__init__(
56
+ n_var=n_var,
57
+ n_obj=4, # 4 objectives
58
+ n_constr=0, # Constraints handled via penalties
59
+ xl=xl,
60
+ xu=xu
61
+ )
62
+
63
+ def _evaluate(self, X, out, *args, **kwargs):
64
+ """
65
+ Evaluate population
66
+
67
+ Args:
68
+ X: Population matrix (n_individuals x n_variables)
69
+ out: Output dictionary
70
+ """
71
+ n_individuals = X.shape[0]
72
+
73
+ # Initialize objective arrays
74
+ f1_sellable = np.zeros(n_individuals) # Maximize (will negate)
75
+ f2_green = np.zeros(n_individuals) # Maximize (will negate)
76
+ f3_road_length = np.zeros(n_individuals) # Minimize
77
+ f4_compliance = np.zeros(n_individuals) # Maximize (will negate)
78
+
79
+ for i in range(n_individuals):
80
+ layout = self._decode_solution(X[i])
81
+
82
+ # Calculate objectives
83
+ metrics = layout.calculate_metrics()
84
+
85
+ # F1: Maximize sellable area (negate for minimization)
86
+ f1_sellable[i] = -metrics.sellable_area_sqm
87
+
88
+ # F2: Maximize green space (negate for minimization)
89
+ f2_green[i] = -metrics.green_space_area_sqm
90
+
91
+ # F3: Minimize road network length
92
+ if layout.road_network:
93
+ f3_road_length[i] = layout.road_network.total_length_m
94
+ else:
95
+ f3_road_length[i] = 1e6 # Penalty for no road
96
+
97
+ # F4: Regulatory compliance score (0-1, higher is better)
98
+ compliance_score = self._calculate_compliance_score(layout)
99
+ f4_compliance[i] = -compliance_score # Negate for minimization
100
+
101
+ # Set objectives
102
+ out["F"] = np.column_stack([f1_sellable, f2_green, f3_road_length, f4_compliance])
103
+
104
+ def _decode_solution(self, x: np.ndarray) -> Layout:
105
+ """
106
+ Decode decision variables into a Layout
107
+
108
+ Args:
109
+ x: Decision variables array
110
+
111
+ Returns:
112
+ Layout object
113
+ """
114
+ layout = Layout(site_boundary=self.site_boundary)
115
+
116
+ # Get site bounds for denormalization
117
+ minx, miny, maxx, maxy = self.site_boundary.geometry.bounds
118
+ site_width = maxx - minx
119
+ site_height = maxy - miny
120
+
121
+ plots = []
122
+ for i in range(self.n_plots):
123
+ idx = i * 5
124
+
125
+ # Denormalize position
126
+ x_norm, y_norm = x[idx], x[idx + 1]
127
+ x_pos = minx + x_norm * site_width
128
+ y_pos = miny + y_norm * site_height
129
+
130
+ width, height = x[idx + 2], x[idx + 3]
131
+ orientation = x[idx + 4]
132
+
133
+ # Create simple rectangular plot
134
+ plot_geom = box(x_pos, y_pos, x_pos + width, y_pos + height)
135
+
136
+ # Check if plot is within buildable area
137
+ if not self.site_boundary.geometry.contains(plot_geom):
138
+ continue # Skip invalid plots
139
+
140
+ plot = Plot(
141
+ geometry=plot_geom,
142
+ area_sqm=plot_geom.area,
143
+ type=PlotType.INDUSTRIAL,
144
+ width_m=width,
145
+ depth_m=height,
146
+ orientation_degrees=orientation
147
+ )
148
+ plots.append(plot)
149
+
150
+ # Add green space (simplified: use remaining area)
151
+ # In practice, this would be more sophisticated
152
+ green_area_target = self.site_boundary.buildable_area_sqm * 0.15 # 15% minimum
153
+
154
+ layout.plots = plots
155
+ layout.road_network = RoadNetwork() # Simplified
156
+
157
+ return layout
158
+
159
+ def _calculate_compliance_score(self, layout: Layout) -> float:
160
+ """
161
+ Calculate regulatory compliance score (0-1)
162
+
163
+ Args:
164
+ layout: Layout to evaluate
165
+
166
+ Returns:
167
+ Compliance score (1.0 = fully compliant)
168
+ """
169
+ score = 1.0
170
+ penalties = 0
171
+
172
+ metrics = layout.metrics
173
+
174
+ # Check green space requirement
175
+ min_green = self.regulations.get('green_space', {}).get('minimum_percentage', 0.15)
176
+ if metrics.green_space_ratio < min_green:
177
+ penalties += 0.3
178
+
179
+ # Check FAR
180
+ max_far = self.regulations.get('far', {}).get('maximum', 0.7)
181
+ if metrics.far_value > max_far:
182
+ penalties += 0.3
183
+
184
+ # Check plot sizes
185
+ min_plot_size = self.regulations.get('plot', {}).get('minimum_area_sqm', 1000)
186
+ for plot in layout.plots:
187
+ if plot.type == PlotType.INDUSTRIAL and plot.area_sqm < min_plot_size:
188
+ penalties += 0.1
189
+ break
190
+
191
+ score = max(0.0, score - penalties)
192
+ return score
193
+
194
+
195
+ class NSGA2Optimizer:
196
+ """
197
+ NSGA-II based multi-objective optimizer for industrial estate layouts
198
+ """
199
+
200
+ def __init__(self, config_path: str = "config/regulations.yaml"):
201
+ """
202
+ Initialize optimizer
203
+
204
+ Args:
205
+ config_path: Path to regulations YAML file
206
+ """
207
+ self.config_path = Path(config_path)
208
+ self.regulations = self._load_regulations()
209
+ self.logger = logging.getLogger(__name__)
210
+
211
+ def _load_regulations(self) -> dict:
212
+ """Load regulations from YAML file"""
213
+ if not self.config_path.exists():
214
+ self.logger.warning(f"Regulations file not found: {self.config_path}")
215
+ return {}
216
+
217
+ with open(self.config_path, 'r', encoding='utf-8') as f:
218
+ return yaml.safe_load(f)
219
+
220
+ def optimize(
221
+ self,
222
+ site_boundary: SiteBoundary,
223
+ population_size: int = 100,
224
+ n_generations: int = 200,
225
+ n_plots: int = 20
226
+ ) -> ParetoFront:
227
+ """
228
+ Run NSGA-II optimization
229
+
230
+ Args:
231
+ site_boundary: Site boundary with constraints
232
+ population_size: NSGA-II population size
233
+ n_generations: Number of generations
234
+ n_plots: Target number of plots
235
+
236
+ Returns:
237
+ ParetoFront with optimal solutions
238
+ """
239
+ import time
240
+ start_time = time.time()
241
+
242
+ self.logger.info(f"Starting NSGA-II optimization: pop={population_size}, gen={n_generations}")
243
+
244
+ # Define problem
245
+ problem = IndustrialEstateProblem(
246
+ site_boundary=site_boundary,
247
+ regulations=self.regulations,
248
+ n_plots=n_plots
249
+ )
250
+
251
+ # Define algorithm
252
+ algorithm = NSGA2(
253
+ pop_size=population_size,
254
+ sampling=FloatRandomSampling(),
255
+ crossover=SBX(prob=0.9, eta=15),
256
+ mutation=PM(eta=20),
257
+ eliminate_duplicates=True
258
+ )
259
+
260
+ # Run optimization
261
+ result = minimize(
262
+ problem,
263
+ algorithm,
264
+ ('n_gen', n_generations),
265
+ seed=42,
266
+ verbose=True
267
+ )
268
+
269
+ # Extract Pareto front
270
+ pareto_front = ParetoFront()
271
+
272
+ if result.X is not None:
273
+ # result.X can be 1D or 2D depending on number of solutions
274
+ if len(result.X.shape) == 1:
275
+ solutions = [result.X]
276
+ else:
277
+ solutions = result.X
278
+
279
+ for i, x in enumerate(solutions):
280
+ layout = problem._decode_solution(x)
281
+ layout.pareto_rank = i
282
+ layout.calculate_metrics()
283
+
284
+ # Store fitness scores
285
+ if len(result.F.shape) == 1:
286
+ f = result.F
287
+ else:
288
+ f = result.F[i]
289
+
290
+ layout.fitness_scores = {
291
+ 'sellable_area': -f[0], # Negate back
292
+ 'green_space': -f[1],
293
+ 'road_length': f[2],
294
+ 'compliance': -f[3]
295
+ }
296
+
297
+ pareto_front.layouts.append(layout)
298
+
299
+ pareto_front.generation_time_seconds = time.time() - start_time
300
+
301
+ self.logger.info(f"Optimization complete: {len(pareto_front.layouts)} solutions in {pareto_front.generation_time_seconds:.2f}s")
302
+
303
+ return pareto_front
304
+
305
+
306
+ # Example usage
307
+ if __name__ == "__main__":
308
+ # Example: Create a simple rectangular site
309
+ from shapely.geometry import box
310
+
311
+ site_geom = box(0, 0, 500, 500) # 500m x 500m site
312
+ site = SiteBoundary(
313
+ geometry=site_geom,
314
+ area_sqm=site_geom.area
315
+ )
316
+ site.buildable_area_sqm = site.area_sqm
317
+
318
+ # Run optimization
319
+ optimizer = NSGA2Optimizer()
320
+ pareto_front = optimizer.optimize(
321
+ site_boundary=site,
322
+ population_size=50,
323
+ n_generations=100,
324
+ n_plots=15
325
+ )
326
+
327
+ print(f"Generated {len(pareto_front.layouts)} Pareto-optimal layouts")
328
+
329
+ # Show best layouts
330
+ max_sellable = pareto_front.get_max_sellable_layout()
331
+ if max_sellable:
332
+ print(f"Max sellable area: {max_sellable.metrics.sellable_area_sqm:.2f} m²")
333
+
334
+ max_green = pareto_front.get_max_green_layout()
335
+ if max_green:
336
+ print(f"Max green space: {max_green.metrics.green_space_area_sqm:.2f} m²")
src/algorithms/regulation_checker.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Regulation Checker - Module C: The Inspector
3
+ Rule-based expert system for Vietnamese industrial estate regulatory compliance
4
+ """
5
+ import yaml
6
+ from pathlib import Path
7
+ from typing import List, Dict
8
+ import logging
9
+ from shapely.geometry import Polygon, MultiPolygon, Point
10
+ from shapely.ops import unary_union
11
+ import geopandas as gpd
12
+
13
+ from src.models.domain import Layout, Plot, PlotType, ComplianceReport, Constraint, ConstraintType
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class RegulationChecker:
19
+ """
20
+ Automated regulatory compliance checker for industrial estate layouts
21
+ Based on Vietnamese industrial estate regulations
22
+ """
23
+
24
+ def __init__(self, regulations_path: str = "config/regulations.yaml"):
25
+ """
26
+ Initialize regulation checker
27
+
28
+ Args:
29
+ regulations_path: Path to regulations YAML configuration
30
+ """
31
+ self.regulations_path = Path(regulations_path)
32
+ self.regulations = self._load_regulations()
33
+ self.logger = logging.getLogger(__name__)
34
+
35
+ def _load_regulations(self) -> dict:
36
+ """Load regulations from YAML file"""
37
+ if not self.regulations_path.exists():
38
+ self.logger.warning(f"Regulations file not found: {self.regulations_path}")
39
+ return self._get_default_regulations()
40
+
41
+ with open(self.regulations_path, 'r', encoding='utf-8') as f:
42
+ return yaml.safe_load(f)
43
+
44
+ def _get_default_regulations(self) -> dict:
45
+ """Get default regulations if file not found"""
46
+ return {
47
+ 'setbacks': {
48
+ 'boundary_minimum': 50,
49
+ 'fire_safety_distance': 30,
50
+ 'waterway_buffer': 100
51
+ },
52
+ 'far': {
53
+ 'maximum': 0.7,
54
+ 'minimum': 0.3
55
+ },
56
+ 'green_space': {
57
+ 'minimum_percentage': 0.15
58
+ },
59
+ 'plot': {
60
+ 'minimum_area_sqm': 1000,
61
+ 'minimum_width_m': 20
62
+ },
63
+ 'roads': {
64
+ 'maximum_distance_to_road_m': 200
65
+ }
66
+ }
67
+
68
+ def validate_compliance(self, layout: Layout) -> ComplianceReport:
69
+ """
70
+ Comprehensive regulatory compliance validation
71
+
72
+ Args:
73
+ layout: Layout to validate
74
+
75
+ Returns:
76
+ ComplianceReport with violations and warnings
77
+ """
78
+ report = ComplianceReport(
79
+ layout_id=layout.id,
80
+ is_compliant=True
81
+ )
82
+
83
+ self.logger.info(f"Validating layout {layout.id} against Vietnamese regulations")
84
+
85
+ # Run all checks
86
+ self._check_boundary_setbacks(layout, report)
87
+ self._check_far_compliance(layout, report)
88
+ self._check_green_space_requirements(layout, report)
89
+ self._check_plot_dimensions(layout, report)
90
+ self._check_road_accessibility(layout, report)
91
+ self._check_fire_safety_distances(layout, report)
92
+ self._check_no_overlaps(layout, report)
93
+
94
+ # Final determination
95
+ if len(report.violations) == 0:
96
+ report.is_compliant = True
97
+ self.logger.info(f"Layout {layout.id} is COMPLIANT")
98
+ else:
99
+ report.is_compliant = False
100
+ self.logger.warning(f"Layout {layout.id} has {len(report.violations)} violations")
101
+
102
+ return report
103
+
104
+ def _check_boundary_setbacks(self, layout: Layout, report: ComplianceReport):
105
+ """Check minimum setback from site boundary"""
106
+ min_setback = self.regulations['setbacks']['boundary_minimum']
107
+
108
+ # Create buffer zone inside boundary
109
+ boundary = layout.site_boundary.geometry
110
+ setback_zone = boundary.buffer(-min_setback)
111
+
112
+ for plot in layout.plots:
113
+ if plot.type == PlotType.INDUSTRIAL:
114
+ if not setback_zone.contains(plot.geometry):
115
+ report.add_violation(
116
+ f"Plot {plot.id} violates {min_setback}m boundary setback requirement"
117
+ )
118
+ return
119
+
120
+ report.add_pass("Boundary setback compliance")
121
+
122
+ def _check_far_compliance(self, layout: Layout, report: ComplianceReport):
123
+ """Check Floor Area Ratio (FAR) compliance"""
124
+ max_far = self.regulations['far']['maximum']
125
+ min_far = self.regulations['far'].get('minimum', 0.0)
126
+
127
+ metrics = layout.metrics
128
+
129
+ # FAR = Total floor area / Land area
130
+ # Simplified: assuming single story, FAR = Building area / Land area
131
+ if metrics.sellable_ratio > max_far:
132
+ report.add_violation(
133
+ f"FAR {metrics.sellable_ratio:.2f} exceeds maximum {max_far}"
134
+ )
135
+ elif metrics.sellable_ratio < min_far:
136
+ report.add_warning(
137
+ f"FAR {metrics.sellable_ratio:.2f} below recommended minimum {min_far}"
138
+ )
139
+ else:
140
+ report.add_pass(f"FAR compliance ({metrics.sellable_ratio:.2f})")
141
+
142
+ def _check_green_space_requirements(self, layout: Layout, report: ComplianceReport):
143
+ """Check minimum green space requirement"""
144
+ min_green = self.regulations['green_space']['minimum_percentage']
145
+
146
+ metrics = layout.metrics
147
+
148
+ if metrics.green_space_ratio < min_green:
149
+ deficit = (min_green - metrics.green_space_ratio) * 100
150
+ report.add_violation(
151
+ f"Green space {metrics.green_space_ratio*100:.1f}% is below minimum {min_green*100}% "
152
+ f"(deficit: {deficit:.1f}%)"
153
+ )
154
+ else:
155
+ report.add_pass(f"Green space compliance ({metrics.green_space_ratio*100:.1f}%)")
156
+
157
+ def _check_plot_dimensions(self, layout: Layout, report: ComplianceReport):
158
+ """Check plot minimum dimensions"""
159
+ min_area = self.regulations['plot']['minimum_area_sqm']
160
+ min_width = self.regulations['plot']['minimum_width_m']
161
+
162
+ violations = []
163
+ for plot in layout.plots:
164
+ if plot.type == PlotType.INDUSTRIAL:
165
+ if plot.area_sqm < min_area:
166
+ violations.append(
167
+ f"Plot {plot.id} area {plot.area_sqm:.0f}m² below minimum {min_area}m²"
168
+ )
169
+
170
+ if plot.width_m < min_width:
171
+ violations.append(
172
+ f"Plot {plot.id} width {plot.width_m:.1f}m below minimum {min_width}m"
173
+ )
174
+
175
+ if violations:
176
+ for v in violations:
177
+ report.add_violation(v)
178
+ else:
179
+ report.add_pass("Plot dimension compliance")
180
+
181
+ def _check_road_accessibility(self, layout: Layout, report: ComplianceReport):
182
+ """Check that all plots have road access within maximum distance"""
183
+ max_distance = self.regulations['roads']['maximum_distance_to_road_m']
184
+
185
+ if not layout.road_network or not layout.road_network.primary_roads:
186
+ report.add_warning("No road network defined for accessibility check")
187
+ return
188
+
189
+ # Simplified check: ensure plots are within max_distance of roads
190
+ # In practice, would check actual road connectivity
191
+
192
+ violations = []
193
+ for plot in layout.plots:
194
+ if plot.type == PlotType.INDUSTRIAL:
195
+ if not plot.has_road_access:
196
+ violations.append(f"Plot {plot.id} lacks road access")
197
+
198
+ if violations:
199
+ for v in violations:
200
+ report.add_violation(v)
201
+ else:
202
+ report.add_pass("Road accessibility compliance")
203
+
204
+ def _check_fire_safety_distances(self, layout: Layout, report: ComplianceReport):
205
+ """Check fire safety distance requirements"""
206
+ fire_distance = self.regulations['setbacks']['fire_safety_distance']
207
+
208
+ # Check spacing between industrial plots
209
+ industrial_plots = [p for p in layout.plots if p.type == PlotType.INDUSTRIAL]
210
+
211
+ for i, plot1 in enumerate(industrial_plots):
212
+ for plot2 in industrial_plots[i+1:]:
213
+ distance = plot1.geometry.distance(plot2.geometry)
214
+ if distance < fire_distance:
215
+ report.add_violation(
216
+ f"Plots {plot1.id} and {plot2.id} violate {fire_distance}m fire safety distance "
217
+ f"(actual: {distance:.1f}m)"
218
+ )
219
+ return
220
+
221
+ report.add_pass("Fire safety distance compliance")
222
+
223
+ def _check_no_overlaps(self, layout: Layout, report: ComplianceReport):
224
+ """Check that no plots overlap"""
225
+ plots = layout.plots
226
+
227
+ for i, plot1 in enumerate(plots):
228
+ for plot2 in plots[i+1:]:
229
+ if plot1.geometry.intersects(plot2.geometry):
230
+ intersection = plot1.geometry.intersection(plot2.geometry)
231
+ if intersection.area > 0.01: # Small tolerance for numerical errors
232
+ report.add_violation(
233
+ f"Plots {plot1.id} and {plot2.id} overlap by {intersection.area:.2f}m²"
234
+ )
235
+ return
236
+
237
+ report.add_pass("No plot overlaps")
238
+
239
+ def check_constraint_compliance(self, layout: Layout, constraints: List[Constraint]) -> ComplianceReport:
240
+ """
241
+ Check compliance against specific spatial constraints
242
+
243
+ Args:
244
+ layout: Layout to check
245
+ constraints: List of spatial constraints
246
+
247
+ Returns:
248
+ ComplianceReport
249
+ """
250
+ report = ComplianceReport(
251
+ layout_id=layout.id,
252
+ is_compliant=True
253
+ )
254
+
255
+ for constraint in constraints:
256
+ violations = self._check_single_constraint(layout, constraint)
257
+ for v in violations:
258
+ report.add_violation(v)
259
+
260
+ return report
261
+
262
+ def _check_single_constraint(self, layout: Layout, constraint: Constraint) -> List[str]:
263
+ """Check a single constraint"""
264
+ violations = []
265
+
266
+ for plot in layout.plots:
267
+ if plot.type == PlotType.INDUSTRIAL:
268
+ if plot.geometry.intersects(constraint.geometry):
269
+ violations.append(
270
+ f"Plot {plot.id} violates {constraint.type.value} constraint: {constraint.description}"
271
+ )
272
+
273
+ return violations
274
+
275
+
276
+ # Example usage
277
+ if __name__ == "__main__":
278
+ from shapely.geometry import box
279
+ from src.models.domain import SiteBoundary
280
+
281
+ # Create example site and layout
282
+ site_geom = box(0, 0, 500, 500)
283
+ site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
284
+ site.buildable_area_sqm = site.area_sqm
285
+
286
+ layout = Layout(site_boundary=site)
287
+
288
+ # Add some example plots
289
+ plot1 = Plot(
290
+ geometry=box(60, 60, 150, 150),
291
+ area_sqm=8100,
292
+ type=PlotType.INDUSTRIAL,
293
+ width_m=90,
294
+ depth_m=90,
295
+ has_road_access=True
296
+ )
297
+
298
+ plot2 = Plot(
299
+ geometry=box(200, 60, 290, 150),
300
+ area_sqm=8100,
301
+ type=PlotType.INDUSTRIAL,
302
+ width_m=90,
303
+ depth_m=90,
304
+ has_road_access=True
305
+ )
306
+
307
+ green_plot = Plot(
308
+ geometry=box(60, 200, 150, 290),
309
+ area_sqm=8100,
310
+ type=PlotType.GREEN_SPACE
311
+ )
312
+
313
+ layout.plots = [plot1, plot2, green_plot]
314
+ layout.calculate_metrics()
315
+
316
+ # Check compliance
317
+ checker = RegulationChecker()
318
+ report = checker.validate_compliance(layout)
319
+
320
+ print(f"Compliance: {report.is_compliant}")
321
+ print(f"Violations: {len(report.violations)}")
322
+ for v in report.violations:
323
+ print(f" - {v}")
324
+ print(f"Warnings: {len(report.warnings)}")
325
+ for w in report.warnings:
326
+ print(f" - {w}")
327
+ print(f"Checks passed: {len(report.checks_passed)}")
328
+ for c in report.checks_passed:
329
+ print(f" ✓ {c}")
src/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API package"""
src/api/main.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Main Application - REMB Optimization Engine
3
+ """
4
+ from fastapi import FastAPI, UploadFile, File, HTTPException
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from pydantic import BaseModel
7
+ from typing import Optional, List
8
+ import logging
9
+
10
+ from config.settings import settings
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Create FastAPI app
17
+ app = FastAPI(
18
+ title=settings.PROJECT_NAME,
19
+ version=settings.VERSION,
20
+ description="AI-Powered Industrial Estate Master Planning Optimization Engine"
21
+ )
22
+
23
+ # CORS middleware
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"],
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+
33
+ # Pydantic models for API
34
+ class OptimizationRequest(BaseModel):
35
+ """Request model for optimization"""
36
+ site_id: str
37
+ population_size: int = 100
38
+ n_generations: int = 200
39
+ n_plots: int = 20
40
+
41
+
42
+ class OptimizationResponse(BaseModel):
43
+ """Response model for optimization"""
44
+ optimization_id: str
45
+ status: str
46
+ n_solutions: int
47
+ generation_time_seconds: float
48
+
49
+
50
+ class LayoutMetricsResponse(BaseModel):
51
+ """Layout metrics response"""
52
+ layout_id: str
53
+ total_area_sqm: float
54
+ sellable_area_sqm: float
55
+ green_space_area_sqm: float
56
+ sellable_ratio: float
57
+ green_space_ratio: float
58
+ is_compliant: bool
59
+
60
+
61
+ @app.get("/")
62
+ async def root():
63
+ """Root endpoint"""
64
+ return {
65
+ "message": "REMB Industrial Estate Master Planning Optimization Engine",
66
+ "version": settings.VERSION,
67
+ "status": "operational"
68
+ }
69
+
70
+
71
+ @app.get("/api/v1/health")
72
+ async def health_check():
73
+ """Health check endpoint"""
74
+ return {
75
+ "status": "healthy",
76
+ "version": settings.VERSION
77
+ }
78
+
79
+
80
+ @app.post("/api/v1/sites/upload")
81
+ async def upload_site_boundary(file: UploadFile = File(...)):
82
+ """
83
+ Upload site boundary file (Shapefile or DXF)
84
+
85
+ Args:
86
+ file: Shapefile (.shp) or DXF file
87
+
88
+ Returns:
89
+ Site ID and metadata
90
+ """
91
+ if not file.filename:
92
+ raise HTTPException(status_code=400, detail="No file provided")
93
+
94
+ # Check file extension
95
+ extension = file.filename.split('.')[-1].lower()
96
+ if f".{extension}" not in settings.ALLOWED_EXTENSIONS:
97
+ raise HTTPException(
98
+ status_code=400,
99
+ detail=f"File type .{extension} not allowed. Allowed: {settings.ALLOWED_EXTENSIONS}"
100
+ )
101
+
102
+ # TODO: Implement actual file processing
103
+ site_id = "site_123" # Placeholder
104
+
105
+ return {
106
+ "site_id": site_id,
107
+ "filename": file.filename,
108
+ "status": "uploaded",
109
+ "message": "Site boundary uploaded successfully"
110
+ }
111
+
112
+
113
+ @app.post("/api/v1/sites/{site_id}/optimize", response_model=OptimizationResponse)
114
+ async def optimize_site(site_id: str, request: OptimizationRequest):
115
+ """
116
+ Run optimization for a site
117
+
118
+ Args:
119
+ site_id: Site identifier
120
+ request: Optimization parameters
121
+
122
+ Returns:
123
+ Optimization results with Pareto front
124
+ """
125
+ logger.info(f"Starting optimization for site {site_id}")
126
+
127
+ # TODO: Implement actual optimization
128
+ # This would call NSGA2Optimizer and return results
129
+
130
+ return OptimizationResponse(
131
+ optimization_id="opt_123",
132
+ status="completed",
133
+ n_solutions=8,
134
+ generation_time_seconds=120.5
135
+ )
136
+
137
+
138
+ @app.get("/api/v1/layouts/{layout_id}/metrics", response_model=LayoutMetricsResponse)
139
+ async def get_layout_metrics(layout_id: str):
140
+ """
141
+ Get metrics for a specific layout
142
+
143
+ Args:
144
+ layout_id: Layout identifier
145
+
146
+ Returns:
147
+ Layout metrics
148
+ """
149
+ # TODO: Retrieve from database
150
+ return LayoutMetricsResponse(
151
+ layout_id=layout_id,
152
+ total_area_sqm=250000.0,
153
+ sellable_area_sqm=162500.0,
154
+ green_space_area_sqm=37500.0,
155
+ sellable_ratio=0.65,
156
+ green_space_ratio=0.15,
157
+ is_compliant=True
158
+ )
159
+
160
+
161
+ @app.get("/api/v1/layouts/{layout_id}/export")
162
+ async def export_layout_dxf(layout_id: str):
163
+ """
164
+ Export layout as DXF file
165
+
166
+ Args:
167
+ layout_id: Layout identifier
168
+
169
+ Returns:
170
+ DXF file download
171
+ """
172
+ # TODO: Implement DXF export
173
+ return {
174
+ "layout_id": layout_id,
175
+ "status": "exported",
176
+ "download_url": f"/downloads/{layout_id}.dxf"
177
+ }
178
+
179
+
180
+ if __name__ == "__main__":
181
+ import uvicorn
182
+ uvicorn.run(app, host="0.0.0.0", port=8000)
src/api/mvp_api.py ADDED
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AIOptimize™ MVP API
3
+ FastAPI backend for industrial estate planning optimization
4
+ Per MVP-24h.md specification
5
+ """
6
+ import os
7
+ import io
8
+ import json
9
+ import zipfile
10
+ from datetime import datetime
11
+ from typing import Optional, List, Dict, Any
12
+ from pathlib import Path
13
+
14
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import StreamingResponse, JSONResponse
17
+ from pydantic import BaseModel
18
+ from shapely.geometry import Polygon, shape
19
+
20
+ # Import services
21
+ from src.services.session_manager import session_manager
22
+ from src.services.gemini_service import gemini_service
23
+ from src.algorithms.ga_optimizer import SimpleGAOptimizer
24
+ from src.export.dxf_exporter import DXFExporter
25
+ from src.models.domain import Layout, Plot, PlotType, SiteBoundary, LayoutMetrics
26
+
27
+ # Sample data
28
+ SAMPLE_BOUNDARY = {
29
+ "type": "Feature",
30
+ "geometry": {
31
+ "type": "Polygon",
32
+ "coordinates": [[
33
+ [0, 0], [500, 0], [500, 400], [0, 400], [0, 0]
34
+ ]]
35
+ },
36
+ "properties": {"name": "Sample Industrial Site"}
37
+ }
38
+
39
+
40
+ # === Pydantic Models ===
41
+
42
+ class UploadResponse(BaseModel):
43
+ session_id: str
44
+ boundary: Dict[str, Any]
45
+ metadata: Dict[str, Any]
46
+
47
+
48
+ class GenerateRequest(BaseModel):
49
+ session_id: str
50
+ target_plots: int = 8
51
+ setback: float = 50.0
52
+
53
+
54
+ class ChatRequest(BaseModel):
55
+ session_id: str
56
+ message: str
57
+
58
+
59
+ class ChatResponse(BaseModel):
60
+ message: str
61
+ model: str
62
+
63
+
64
+ class ExportRequest(BaseModel):
65
+ session_id: str
66
+ option_id: int
67
+
68
+
69
+ class HealthResponse(BaseModel):
70
+ status: str
71
+ version: str
72
+ gemini_available: bool
73
+
74
+
75
+ # === FastAPI App ===
76
+
77
+ app = FastAPI(
78
+ title="AIOptimize™ API",
79
+ description="AI-Powered Industrial Estate Planning Engine",
80
+ version="1.0.0"
81
+ )
82
+
83
+ # CORS for frontend
84
+ app.add_middleware(
85
+ CORSMiddleware,
86
+ allow_origins=["*"], # Allow all for development
87
+ allow_credentials=True,
88
+ allow_methods=["*"],
89
+ allow_headers=["*"],
90
+ )
91
+
92
+ # Initialize optimizer and exporter
93
+ ga_optimizer = SimpleGAOptimizer()
94
+ dxf_exporter = DXFExporter()
95
+
96
+
97
+ # === API Endpoints ===
98
+
99
+ @app.get("/api/health", response_model=HealthResponse)
100
+ async def health_check():
101
+ """Health check endpoint"""
102
+ return HealthResponse(
103
+ status="healthy",
104
+ version="1.0.0",
105
+ gemini_available=gemini_service.is_available
106
+ )
107
+
108
+
109
+ @app.get("/api/sample-data")
110
+ async def get_sample_data():
111
+ """Get sample GeoJSON boundary data"""
112
+ return SAMPLE_BOUNDARY
113
+
114
+
115
+ @app.post("/api/upload-boundary", response_model=UploadResponse)
116
+ async def upload_boundary(file: UploadFile = File(None), geojson: str = Form(None)):
117
+ """
118
+ Upload site boundary (GeoJSON)
119
+
120
+ Accepts either file upload or JSON string
121
+ """
122
+ try:
123
+ # Get GeoJSON data
124
+ if file and file.filename:
125
+ content = await file.read()
126
+ geojson_data = json.loads(content)
127
+ elif geojson:
128
+ geojson_data = json.loads(geojson)
129
+ else:
130
+ raise HTTPException(400, "No boundary data provided")
131
+
132
+ # Extract coordinates from GeoJSON
133
+ if geojson_data.get("type") == "Feature":
134
+ geometry = geojson_data.get("geometry", {})
135
+ elif geojson_data.get("type") == "FeatureCollection":
136
+ features = geojson_data.get("features", [])
137
+ if features:
138
+ geometry = features[0].get("geometry", {})
139
+ else:
140
+ raise HTTPException(400, "No features in FeatureCollection")
141
+ elif geojson_data.get("type") == "Polygon":
142
+ geometry = geojson_data
143
+ else:
144
+ raise HTTPException(400, "Invalid GeoJSON format")
145
+
146
+ # Get coordinates
147
+ coords = geometry.get("coordinates", [[]])[0]
148
+ if not coords:
149
+ raise HTTPException(400, "No coordinates found")
150
+
151
+ # Create Shapely polygon and validate
152
+ polygon = Polygon(coords)
153
+ if not polygon.is_valid:
154
+ polygon = polygon.buffer(0)
155
+
156
+ # Calculate metadata
157
+ metadata = {
158
+ "area": polygon.area,
159
+ "perimeter": polygon.length,
160
+ "bounds": list(polygon.bounds),
161
+ "centroid": [polygon.centroid.x, polygon.centroid.y]
162
+ }
163
+
164
+ # Create session and store data
165
+ session = session_manager.create_session()
166
+ session_manager.set_boundary(
167
+ session.id,
168
+ boundary=geojson_data,
169
+ coords=coords,
170
+ metadata=metadata
171
+ )
172
+
173
+ return UploadResponse(
174
+ session_id=session.id,
175
+ boundary=geojson_data,
176
+ metadata=metadata
177
+ )
178
+
179
+ except json.JSONDecodeError as e:
180
+ raise HTTPException(400, f"Invalid JSON format: {str(e)}")
181
+ except HTTPException:
182
+ raise
183
+ except Exception as e:
184
+ import traceback
185
+ traceback.print_exc()
186
+ raise HTTPException(500, f"Error processing boundary: {str(e)}")
187
+
188
+
189
+ # Alternative JSON endpoint for easier frontend integration
190
+ class UploadBoundaryRequest(BaseModel):
191
+ geojson: Dict[str, Any]
192
+
193
+
194
+ @app.post("/api/upload-boundary-json", response_model=UploadResponse)
195
+ async def upload_boundary_json(request: UploadBoundaryRequest):
196
+ """Upload site boundary via JSON body"""
197
+ try:
198
+ geojson_data = request.geojson
199
+
200
+ # Extract coordinates from GeoJSON
201
+ if geojson_data.get("type") == "Feature":
202
+ geometry = geojson_data.get("geometry", {})
203
+ elif geojson_data.get("type") == "FeatureCollection":
204
+ features = geojson_data.get("features", [])
205
+ if features:
206
+ geometry = features[0].get("geometry", {})
207
+ else:
208
+ raise HTTPException(400, "No features in FeatureCollection")
209
+ elif geojson_data.get("type") == "Polygon":
210
+ geometry = geojson_data
211
+ else:
212
+ raise HTTPException(400, "Invalid GeoJSON format")
213
+
214
+ coords = geometry.get("coordinates", [[]])[0]
215
+ if not coords:
216
+ raise HTTPException(400, "No coordinates found")
217
+
218
+ polygon = Polygon(coords)
219
+ if not polygon.is_valid:
220
+ polygon = polygon.buffer(0)
221
+
222
+ metadata = {
223
+ "area": polygon.area,
224
+ "perimeter": polygon.length,
225
+ "bounds": list(polygon.bounds),
226
+ "centroid": [polygon.centroid.x, polygon.centroid.y]
227
+ }
228
+
229
+ session = session_manager.create_session()
230
+ session_manager.set_boundary(
231
+ session.id,
232
+ boundary=geojson_data,
233
+ coords=coords,
234
+ metadata=metadata
235
+ )
236
+
237
+ return UploadResponse(
238
+ session_id=session.id,
239
+ boundary=geojson_data,
240
+ metadata=metadata
241
+ )
242
+
243
+ except HTTPException:
244
+ raise
245
+ except Exception as e:
246
+ import traceback
247
+ traceback.print_exc()
248
+ raise HTTPException(500, f"Error: {str(e)}")
249
+
250
+
251
+ @app.post("/api/upload-dxf", response_model=UploadResponse)
252
+ async def upload_dxf(file: UploadFile = File(...)):
253
+ """
254
+ Upload site boundary from DXF file
255
+
256
+ Parses LWPOLYLINE entities to extract site boundary polygon
257
+ """
258
+ import ezdxf
259
+ import tempfile
260
+
261
+ if not file.filename or not file.filename.lower().endswith('.dxf'):
262
+ raise HTTPException(400, "Please upload a valid .dxf file")
263
+
264
+ try:
265
+ # Save to temp file for ezdxf to read
266
+ content = await file.read()
267
+ with tempfile.NamedTemporaryFile(suffix='.dxf', delete=False) as tmp:
268
+ tmp.write(content)
269
+ tmp_path = tmp.name
270
+
271
+ # Parse DXF
272
+ doc = ezdxf.readfile(tmp_path)
273
+ msp = doc.modelspace()
274
+
275
+ # Find closed polylines
276
+ polygons = []
277
+ for entity in msp:
278
+ if entity.dxftype() == 'LWPOLYLINE':
279
+ if entity.closed:
280
+ points = list(entity.get_points())
281
+ if len(points) >= 3:
282
+ coords = [(p[0], p[1]) for p in points]
283
+ coords.append(coords[0]) # Close polygon
284
+ poly = Polygon(coords)
285
+ if poly.is_valid:
286
+ polygons.append((poly, coords))
287
+ elif entity.dxftype() == 'POLYLINE':
288
+ if entity.is_closed:
289
+ points = list(entity.points())
290
+ if len(points) >= 3:
291
+ coords = [(p[0], p[1]) for p in points]
292
+ coords.append(coords[0])
293
+ poly = Polygon(coords)
294
+ if poly.is_valid:
295
+ polygons.append((poly, coords))
296
+
297
+ # Clean up temp file
298
+ os.unlink(tmp_path)
299
+
300
+ if not polygons:
301
+ raise HTTPException(400, "No closed polygons found in DXF file")
302
+
303
+ # Get largest polygon as site boundary
304
+ polygon, coords = max(polygons, key=lambda x: x[0].area)
305
+
306
+ # Create GeoJSON boundary
307
+ geojson_data = {
308
+ "type": "Feature",
309
+ "geometry": {
310
+ "type": "Polygon",
311
+ "coordinates": [coords]
312
+ },
313
+ "properties": {"source": file.filename}
314
+ }
315
+
316
+ # Calculate metadata
317
+ metadata = {
318
+ "area": polygon.area,
319
+ "perimeter": polygon.length,
320
+ "bounds": list(polygon.bounds),
321
+ "centroid": [polygon.centroid.x, polygon.centroid.y],
322
+ "dxf_source": file.filename
323
+ }
324
+
325
+ # Create session
326
+ session = session_manager.create_session()
327
+ session_manager.set_boundary(
328
+ session.id,
329
+ boundary=geojson_data,
330
+ coords=coords,
331
+ metadata=metadata
332
+ )
333
+
334
+ print(f"[DXF] Parsed {file.filename}: {len(coords)-1} vertices, area={polygon.area:.0f}m²")
335
+
336
+ return UploadResponse(
337
+ session_id=session.id,
338
+ boundary=geojson_data,
339
+ metadata=metadata
340
+ )
341
+
342
+ except HTTPException:
343
+ raise
344
+ except Exception as e:
345
+ import traceback
346
+ traceback.print_exc()
347
+ raise HTTPException(500, f"DXF parsing error: {str(e)}")
348
+
349
+
350
+ @app.post("/api/generate-layouts")
351
+ async def generate_layouts(request: GenerateRequest):
352
+ """
353
+ Generate optimized layout options using Genetic Algorithm
354
+
355
+ Returns 3 diverse layout options:
356
+ 1. Maximum Profit
357
+ 2. Balanced
358
+ 3. Premium
359
+ """
360
+ # Get session
361
+ session = session_manager.get_session(request.session_id)
362
+ if not session:
363
+ raise HTTPException(404, "Session not found")
364
+
365
+ if not session.boundary_coords:
366
+ raise HTTPException(400, "No boundary uploaded for this session")
367
+
368
+ try:
369
+ # Configure and run optimizer
370
+ optimizer = SimpleGAOptimizer(
371
+ setback=request.setback,
372
+ target_plots=request.target_plots
373
+ )
374
+
375
+ options = optimizer.optimize(session.boundary_coords)
376
+
377
+ # Store in session
378
+ session_manager.set_layouts(request.session_id, options)
379
+
380
+ return {
381
+ "session_id": request.session_id,
382
+ "options": options,
383
+ "count": len(options)
384
+ }
385
+
386
+ except Exception as e:
387
+ raise HTTPException(500, f"Optimization failed: {str(e)}")
388
+
389
+
390
+ @app.post("/api/chat", response_model=ChatResponse)
391
+ async def chat(request: ChatRequest):
392
+ """
393
+ Chat with AI about layout options
394
+
395
+ Uses Gemini Flash 2.0 if available, otherwise falls back to hardcoded responses
396
+ """
397
+ # Get session
398
+ session = session_manager.get_session(request.session_id)
399
+ if not session:
400
+ raise HTTPException(404, "Session not found")
401
+
402
+ # Add user message to history
403
+ session_manager.add_chat_message(request.session_id, "user", request.message)
404
+
405
+ # Generate response
406
+ response = gemini_service.chat(
407
+ message=request.message,
408
+ layouts=session.layouts,
409
+ boundary_metadata=session.metadata
410
+ )
411
+
412
+ # Add assistant message to history
413
+ session_manager.add_chat_message(
414
+ request.session_id,
415
+ "assistant",
416
+ response["message"],
417
+ response["model"]
418
+ )
419
+
420
+ return ChatResponse(**response)
421
+
422
+
423
+ @app.post("/api/export-dxf")
424
+ async def export_dxf(request: ExportRequest):
425
+ """
426
+ Export single layout option to DXF
427
+
428
+ Returns DXF file as download
429
+ """
430
+ # Get session
431
+ session = session_manager.get_session(request.session_id)
432
+ if not session:
433
+ raise HTTPException(404, "Session not found")
434
+
435
+ if not session.layouts:
436
+ raise HTTPException(400, "No layouts generated")
437
+
438
+ # Find requested option
439
+ option = None
440
+ for layout in session.layouts:
441
+ if layout.get("id") == request.option_id:
442
+ option = layout
443
+ break
444
+
445
+ if not option:
446
+ raise HTTPException(404, f"Option {request.option_id} not found")
447
+
448
+ try:
449
+ # Create Layout object for exporter
450
+ layout_obj = _create_layout_from_option(option, session)
451
+
452
+ # Generate DXF to bytes
453
+ dxf_bytes = _export_layout_to_bytes(layout_obj, option)
454
+
455
+ # Create filename
456
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
457
+ filename = f"option_{request.option_id}_{timestamp}.dxf"
458
+
459
+ return StreamingResponse(
460
+ io.BytesIO(dxf_bytes),
461
+ media_type="application/x-autocad-dxf",
462
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
463
+ )
464
+
465
+ except Exception as e:
466
+ raise HTTPException(500, f"Export failed: {str(e)}")
467
+
468
+
469
+ @app.post("/api/export-all-dxf")
470
+ async def export_all_dxf(session_id: str = Form(...)):
471
+ """
472
+ Export all layout options as ZIP file
473
+
474
+ Returns ZIP containing 3 DXF files
475
+ """
476
+ # Get session
477
+ session = session_manager.get_session(session_id)
478
+ if not session:
479
+ raise HTTPException(404, "Session not found")
480
+
481
+ if not session.layouts:
482
+ raise HTTPException(400, "No layouts generated")
483
+
484
+ try:
485
+ # Create ZIP in memory
486
+ zip_buffer = io.BytesIO()
487
+
488
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
489
+ for option in session.layouts:
490
+ # Create Layout object
491
+ layout_obj = _create_layout_from_option(option, session)
492
+
493
+ # Generate DXF
494
+ dxf_bytes = _export_layout_to_bytes(layout_obj, option)
495
+
496
+ # Add to ZIP
497
+ filename = f"option_{option.get('id', 0)}_{option.get('name', 'layout').replace(' ', '_')}.dxf"
498
+ zf.writestr(filename, dxf_bytes)
499
+
500
+ zip_buffer.seek(0)
501
+
502
+ # Create filename
503
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
504
+ filename = f"layouts_{timestamp}.zip"
505
+
506
+ return StreamingResponse(
507
+ zip_buffer,
508
+ media_type="application/zip",
509
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
510
+ )
511
+
512
+ except Exception as e:
513
+ raise HTTPException(500, f"Export failed: {str(e)}")
514
+
515
+
516
+ @app.get("/api/session/{session_id}")
517
+ async def get_session(session_id: str):
518
+ """Get session info"""
519
+ session = session_manager.get_session(session_id)
520
+ if not session:
521
+ raise HTTPException(404, "Session not found")
522
+
523
+ return session.to_dict()
524
+
525
+
526
+ # === Helper Functions ===
527
+
528
+ def _create_layout_from_option(option: Dict, session) -> Layout:
529
+ """Convert GA option to Layout object for DXF export"""
530
+ from shapely.geometry import box, Polygon
531
+
532
+ # Create site boundary
533
+ boundary_poly = Polygon(session.boundary_coords)
534
+ site = SiteBoundary(
535
+ geometry=boundary_poly,
536
+ area_sqm=boundary_poly.area
537
+ )
538
+ site.buildable_area_sqm = boundary_poly.buffer(-50).area
539
+
540
+ # Create layout
541
+ layout = Layout(site_boundary=site)
542
+
543
+ # Add plots
544
+ plots = []
545
+ for i, plot_data in enumerate(option.get("plots", [])):
546
+ coords = plot_data.get("coords", [])
547
+ if coords:
548
+ plot_geom = Polygon(coords)
549
+ else:
550
+ plot_geom = box(
551
+ plot_data["x"],
552
+ plot_data["y"],
553
+ plot_data["x"] + plot_data["width"],
554
+ plot_data["y"] + plot_data["height"]
555
+ )
556
+
557
+ plot = Plot(
558
+ id=f"P{i+1}",
559
+ geometry=plot_geom,
560
+ area_sqm=plot_data.get("area", plot_geom.area),
561
+ type=PlotType.INDUSTRIAL,
562
+ width_m=plot_data.get("width", 50),
563
+ depth_m=plot_data.get("height", 50)
564
+ )
565
+ plots.append(plot)
566
+
567
+ layout.plots = plots
568
+
569
+ # Set metrics
570
+ metrics = option.get("metrics", {})
571
+ layout.metrics = LayoutMetrics(
572
+ total_area_sqm=site.area_sqm,
573
+ sellable_area_sqm=metrics.get("total_area", 0),
574
+ green_space_area_sqm=0,
575
+ road_area_sqm=0,
576
+ num_plots=metrics.get("total_plots", len(plots)),
577
+ is_compliant=True
578
+ )
579
+ layout.metrics.sellable_ratio = layout.metrics.sellable_area_sqm / layout.metrics.total_area_sqm if layout.metrics.total_area_sqm > 0 else 0
580
+
581
+ return layout
582
+
583
+
584
+ def _export_layout_to_bytes(layout: Layout, option: Dict) -> bytes:
585
+ """Export layout to DXF bytes"""
586
+ import ezdxf
587
+ from ezdxf.enums import TextEntityAlignment
588
+
589
+ # Create DXF document
590
+ doc = ezdxf.new(dxfversion="R2010")
591
+ msp = doc.modelspace()
592
+
593
+ # Setup layers
594
+ layers = {
595
+ 'BOUNDARY': {'color': 7}, # White
596
+ 'SETBACK': {'color': 1}, # Red
597
+ 'PLOTS': {'color': 5}, # Blue
598
+ 'LABELS': {'color': 7}, # White
599
+ 'ANNOTATIONS': {'color': 2}, # Yellow
600
+ 'TITLEBLOCK': {'color': 7} # White
601
+ }
602
+
603
+ for name, props in layers.items():
604
+ doc.layers.add(name, color=props['color'])
605
+
606
+ # Draw boundary
607
+ if layout.site_boundary and layout.site_boundary.geometry:
608
+ coords = list(layout.site_boundary.geometry.exterior.coords)
609
+ msp.add_lwpolyline(coords, dxfattribs={'layer': 'BOUNDARY', 'closed': True})
610
+
611
+ # Draw setback zone
612
+ setback = layout.site_boundary.geometry.buffer(-50)
613
+ if not setback.is_empty:
614
+ setback_coords = list(setback.exterior.coords)
615
+ msp.add_lwpolyline(setback_coords, dxfattribs={'layer': 'SETBACK', 'closed': True})
616
+
617
+ # Draw plots
618
+ for plot in layout.plots:
619
+ if plot.geometry:
620
+ coords = list(plot.geometry.exterior.coords)
621
+ msp.add_lwpolyline(coords, dxfattribs={'layer': 'PLOTS', 'closed': True})
622
+
623
+ # Add label
624
+ centroid = plot.geometry.centroid
625
+ msp.add_text(
626
+ plot.id,
627
+ dxfattribs={
628
+ 'layer': 'LABELS',
629
+ 'height': 5,
630
+ 'insert': (centroid.x, centroid.y)
631
+ }
632
+ )
633
+
634
+ # Add area annotation
635
+ msp.add_text(
636
+ f"{plot.area_sqm:.0f}m²",
637
+ dxfattribs={
638
+ 'layer': 'ANNOTATIONS',
639
+ 'height': 3,
640
+ 'insert': (centroid.x, centroid.y - 8)
641
+ }
642
+ )
643
+
644
+ # Add title block
645
+ if layout.site_boundary:
646
+ bounds = layout.site_boundary.geometry.bounds
647
+ minx, miny = bounds[0], bounds[1]
648
+
649
+ title_lines = [
650
+ f"AIOptimize™ - {option.get('name', 'Layout')}",
651
+ f"Plots: {option.get('metrics', {}).get('total_plots', 0)}",
652
+ f"Total Area: {option.get('metrics', {}).get('total_area', 0):.0f} m²",
653
+ f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
654
+ ]
655
+
656
+ y = miny - 20
657
+ for line in title_lines:
658
+ msp.add_text(
659
+ line,
660
+ dxfattribs={
661
+ 'layer': 'TITLEBLOCK',
662
+ 'height': 4,
663
+ 'insert': (minx, y)
664
+ }
665
+ )
666
+ y -= 8
667
+
668
+ # Save to bytes
669
+ stream = io.StringIO()
670
+ doc.write(stream)
671
+ return stream.getvalue().encode('utf-8')
672
+
673
+
674
+ # Run with: uvicorn src.api.mvp_api:app --reload --port 8000
675
+ if __name__ == "__main__":
676
+ import uvicorn
677
+ uvicorn.run(app, host="0.0.0.0", port=8000)
src/core/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Core package - Orchestrator and central coordination"""
2
+ from src.core.orchestrator import CoreOrchestrator, OrchestrationResult, OrchestrationStatus
src/core/orchestrator.py ADDED
@@ -0,0 +1,662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core Orchestrator - The Brain/Nhạc trưởng
3
+ Coordinates between LLM understanding and CP Module execution
4
+ Following the "Handshake Loop" pattern from Core_document.md
5
+ """
6
+ import json
7
+ from typing import Dict, Any, Optional, List, Tuple
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ import logging
11
+ from pathlib import Path
12
+
13
+ from src.models.domain import Layout, SiteBoundary, Plot, ParetoFront, ComplianceReport
14
+ from src.algorithms.nsga2_optimizer import NSGA2Optimizer
15
+ from src.algorithms.milp_solver import MILPSolver, MILPResult
16
+ from src.algorithms.regulation_checker import RegulationChecker
17
+ from src.geometry.site_processor import SiteProcessor
18
+ from src.geometry.road_network import RoadNetworkGenerator
19
+ from src.geometry.plot_generator import PlotGenerator
20
+ from src.export.dxf_exporter import DXFExporter
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class OrchestrationStatus(str, Enum):
26
+ """Status of orchestration step"""
27
+ SUCCESS = "success"
28
+ FAILURE = "failure"
29
+ CONFLICT = "conflict"
30
+ PENDING = "pending"
31
+
32
+
33
+ @dataclass
34
+ class OrchestrationResult:
35
+ """Result from orchestration step"""
36
+ status: OrchestrationStatus
37
+ message: str
38
+ data: Dict[str, Any] = field(default_factory=dict)
39
+ suggestions: List[str] = field(default_factory=list) # For conflict resolution
40
+
41
+ def to_json(self) -> str:
42
+ """Convert to JSON for LLM interpretation"""
43
+ return json.dumps({
44
+ 'status': self.status.value,
45
+ 'message': self.message,
46
+ 'data': self.data,
47
+ 'suggestions': self.suggestions
48
+ }, indent=2, default=str)
49
+
50
+
51
+ class CoreOrchestrator:
52
+ """
53
+ Core Orchestrator - The central coordinator
54
+
55
+ Implements the "Handshake Loop" pattern:
56
+ 1. Dịch (Translation): Convert natural language to technical parameters
57
+ 2. Giải (Execution): Run CP Module algorithms
58
+ 3. Hiểu (Interpretation): Evaluate results against requirements
59
+ 4. Quyết định (Reasoning & Action): Report success or propose alternatives
60
+
61
+ Key principle: LLM handles semantics, CP handles math
62
+ - All numbers come from CP (no hallucination)
63
+ - LLM provides flexibility in input/output
64
+ """
65
+
66
+ def __init__(self, regulations_path: str = "config/regulations.yaml"):
67
+ """
68
+ Initialize orchestrator with all modules
69
+
70
+ Args:
71
+ regulations_path: Path to regulations YAML
72
+ """
73
+ self.regulations_path = regulations_path
74
+
75
+ # Initialize all modules
76
+ self.site_processor = SiteProcessor(regulations_path)
77
+ self.road_generator = RoadNetworkGenerator(regulations_path)
78
+ self.plot_generator = PlotGenerator(regulations_path)
79
+ self.nsga2_optimizer = NSGA2Optimizer(regulations_path)
80
+ self.milp_solver = MILPSolver()
81
+ self.regulation_checker = RegulationChecker(regulations_path)
82
+ self.dxf_exporter = DXFExporter()
83
+
84
+ self.logger = logging.getLogger(__name__)
85
+
86
+ # Current session state
87
+ self.current_site: Optional[SiteBoundary] = None
88
+ self.current_layouts: List[Layout] = []
89
+ self.current_pareto: Optional[ParetoFront] = None
90
+
91
+ # =========================================================================
92
+ # STAGE 1: Digital Twin Initialization (Dịch + Giải)
93
+ # =========================================================================
94
+
95
+ def initialize_site(
96
+ self,
97
+ source: str,
98
+ source_type: str = "coordinates"
99
+ ) -> OrchestrationResult:
100
+ """
101
+ Initialize site from various sources
102
+
103
+ Args:
104
+ source: File path or coordinate string
105
+ source_type: 'shapefile', 'geojson', 'dxf', 'coordinates'
106
+
107
+ Returns:
108
+ OrchestrationResult
109
+ """
110
+ self.logger.info(f"Initializing site from {source_type}")
111
+
112
+ try:
113
+ # Dịch (Translation): Parse input based on type
114
+ if source_type == "shapefile":
115
+ self.current_site = self.site_processor.import_from_shapefile(source)
116
+ elif source_type == "geojson":
117
+ self.current_site = self.site_processor.import_from_geojson(source)
118
+ elif source_type == "dxf":
119
+ self.current_site = self.site_processor.import_from_dxf(source)
120
+ elif source_type == "coordinates":
121
+ # Parse coordinates from string or list
122
+ if isinstance(source, str):
123
+ coords = json.loads(source)
124
+ else:
125
+ coords = source
126
+ self.current_site = self.site_processor.import_from_coordinates(coords)
127
+ else:
128
+ return OrchestrationResult(
129
+ status=OrchestrationStatus.FAILURE,
130
+ message=f"Unknown source type: {source_type}"
131
+ )
132
+
133
+ # Giải (Execution): Site processor has calculated buildable area
134
+
135
+ # Hiểu (Interpretation): Check if site is valid
136
+ if self.current_site.buildable_area_sqm <= 0:
137
+ return OrchestrationResult(
138
+ status=OrchestrationStatus.CONFLICT,
139
+ message="Site too small after applying setbacks",
140
+ suggestions=[
141
+ "Reduce boundary setback requirement",
142
+ "Choose a larger site",
143
+ "Apply for variance permit"
144
+ ]
145
+ )
146
+
147
+ # Quyết định: Success
148
+ return OrchestrationResult(
149
+ status=OrchestrationStatus.SUCCESS,
150
+ message="Site initialized successfully",
151
+ data={
152
+ 'site_id': self.current_site.id,
153
+ 'total_area_sqm': self.current_site.area_sqm,
154
+ 'buildable_area_sqm': self.current_site.buildable_area_sqm,
155
+ 'buildable_ratio': self.current_site.buildable_area_sqm / self.current_site.area_sqm,
156
+ 'num_constraints': len(self.current_site.constraints)
157
+ }
158
+ )
159
+
160
+ except Exception as e:
161
+ self.logger.error(f"Site initialization failed: {e}")
162
+ return OrchestrationResult(
163
+ status=OrchestrationStatus.FAILURE,
164
+ message=f"Failed to initialize site: {str(e)}"
165
+ )
166
+
167
+ # =========================================================================
168
+ # STAGE 2: Infrastructure Skeleton (Dịch + Giải)
169
+ # =========================================================================
170
+
171
+ def generate_road_network(
172
+ self,
173
+ pattern: str = "grid",
174
+ primary_spacing: float = 200,
175
+ secondary_spacing: float = 100
176
+ ) -> OrchestrationResult:
177
+ """
178
+ Generate road network
179
+
180
+ Args:
181
+ pattern: 'grid' or 'spine'
182
+ primary_spacing: Primary road spacing
183
+ secondary_spacing: Secondary road spacing
184
+
185
+ Returns:
186
+ OrchestrationResult
187
+ """
188
+ if not self.current_site:
189
+ return OrchestrationResult(
190
+ status=OrchestrationStatus.FAILURE,
191
+ message="No site initialized. Call initialize_site first."
192
+ )
193
+
194
+ self.logger.info(f"Generating {pattern} road network")
195
+
196
+ try:
197
+ # Giải (Execution): Generate road network
198
+ if pattern == "spine":
199
+ road_network = self.road_generator.generate_spine_network(self.current_site)
200
+ else:
201
+ road_network = self.road_generator.generate_grid_network(
202
+ self.current_site,
203
+ primary_spacing=primary_spacing,
204
+ secondary_spacing=secondary_spacing
205
+ )
206
+
207
+ # Check for dead zones
208
+ dead_zones = self.road_generator.identify_dead_zones(
209
+ self.current_site, road_network
210
+ )
211
+
212
+ # Hiểu (Interpretation)
213
+ if dead_zones:
214
+ dead_area = sum(z.area for z in dead_zones)
215
+ if dead_area > self.current_site.buildable_area_sqm * 0.1:
216
+ return OrchestrationResult(
217
+ status=OrchestrationStatus.CONFLICT,
218
+ message=f"Road network leaves {len(dead_zones)} dead zones ({dead_area:.0f}m²)",
219
+ data={'road_network': road_network, 'dead_zones': len(dead_zones)},
220
+ suggestions=[
221
+ f"Try pattern='grid' with smaller spacing",
222
+ f"Add more secondary roads",
223
+ f"Accept dead zones as green space"
224
+ ]
225
+ )
226
+
227
+ # Store road network in site for later use
228
+ self._current_road_network = road_network
229
+
230
+ return OrchestrationResult(
231
+ status=OrchestrationStatus.SUCCESS,
232
+ message="Road network generated successfully",
233
+ data={
234
+ 'total_length_m': road_network.total_length_m,
235
+ 'total_area_sqm': road_network.total_area_sqm,
236
+ 'dead_zones': len(dead_zones) if dead_zones else 0,
237
+ 'pattern': pattern
238
+ }
239
+ )
240
+
241
+ except Exception as e:
242
+ self.logger.error(f"Road network generation failed: {e}")
243
+ return OrchestrationResult(
244
+ status=OrchestrationStatus.FAILURE,
245
+ message=f"Failed to generate road network: {str(e)}"
246
+ )
247
+
248
+ # =========================================================================
249
+ # STAGE 3: Constraint Mapping (Dịch + Giải)
250
+ # =========================================================================
251
+
252
+ def add_constraint(
253
+ self,
254
+ constraint_type: str,
255
+ description: str,
256
+ geometry: Any,
257
+ buffer_m: float = 0
258
+ ) -> OrchestrationResult:
259
+ """
260
+ Add a constraint to the site
261
+
262
+ Natural language examples that LLM would translate:
263
+ - "Tránh kho xăng 200m" -> constraint_type="hazard", buffer_m=200
264
+ - "Cách sông 100m" -> constraint_type="waterway", buffer_m=100
265
+
266
+ Args:
267
+ constraint_type: Type of constraint
268
+ description: Human-readable description
269
+ geometry: Constraint geometry (coordinates or polygon)
270
+ buffer_m: Buffer distance in meters
271
+
272
+ Returns:
273
+ OrchestrationResult
274
+ """
275
+ if not self.current_site:
276
+ return OrchestrationResult(
277
+ status=OrchestrationStatus.FAILURE,
278
+ message="No site initialized"
279
+ )
280
+
281
+ try:
282
+ from src.models.domain import ConstraintType
283
+
284
+ # Dịch (Translation): Map string to enum
285
+ type_map = {
286
+ 'setback': ConstraintType.SETBACK,
287
+ 'fire_safety': ConstraintType.FIRE_SAFETY,
288
+ 'waterway': ConstraintType.WATERWAY,
289
+ 'hazard': ConstraintType.HAZARD_ZONE,
290
+ 'no_build': ConstraintType.NO_BUILD
291
+ }
292
+
293
+ constraint_enum = type_map.get(constraint_type.lower(), ConstraintType.NO_BUILD)
294
+
295
+ # Giải (Execution)
296
+ constraint = self.site_processor.add_constraint(
297
+ self.current_site,
298
+ constraint_enum,
299
+ geometry,
300
+ buffer_distance=buffer_m,
301
+ description=description
302
+ )
303
+
304
+ # Hiểu (Interpretation)
305
+ if self.current_site.buildable_area_sqm <= 0:
306
+ return OrchestrationResult(
307
+ status=OrchestrationStatus.CONFLICT,
308
+ message="Site no longer has buildable area after constraint",
309
+ suggestions=[
310
+ "Reduce buffer distance",
311
+ "Move constraint location",
312
+ "Request exemption"
313
+ ]
314
+ )
315
+
316
+ return OrchestrationResult(
317
+ status=OrchestrationStatus.SUCCESS,
318
+ message=f"Constraint added: {description}",
319
+ data={
320
+ 'constraint_type': constraint_type,
321
+ 'buffer_m': buffer_m,
322
+ 'remaining_buildable_sqm': self.current_site.buildable_area_sqm
323
+ }
324
+ )
325
+
326
+ except Exception as e:
327
+ return OrchestrationResult(
328
+ status=OrchestrationStatus.FAILURE,
329
+ message=f"Failed to add constraint: {str(e)}"
330
+ )
331
+
332
+ # =========================================================================
333
+ # STAGE 4: Automated Optimization (Hybrid AI: GA + MILP)
334
+ # =========================================================================
335
+
336
+ def run_optimization(
337
+ self,
338
+ population_size: int = 100,
339
+ n_generations: int = 200,
340
+ n_plots: int = 20
341
+ ) -> OrchestrationResult:
342
+ """
343
+ Run full optimization pipeline (NSGA-II + MILP + Compliance)
344
+
345
+ This is the core of the "Handshake Loop" - multiple iterations
346
+ between GA exploration and MILP validation.
347
+
348
+ Args:
349
+ population_size: NSGA-II population size
350
+ n_generations: Number of GA generations
351
+ n_plots: Target number of plots
352
+
353
+ Returns:
354
+ OrchestrationResult with Pareto front
355
+ """
356
+ if not self.current_site:
357
+ return OrchestrationResult(
358
+ status=OrchestrationStatus.FAILURE,
359
+ message="No site initialized"
360
+ )
361
+
362
+ self.logger.info("Starting optimization pipeline")
363
+
364
+ try:
365
+ # STEP 1: NSGA-II Exploration (Giải)
366
+ self.logger.info("Step 1: NSGA-II multi-objective optimization")
367
+ pareto_front = self.nsga2_optimizer.optimize(
368
+ site_boundary=self.current_site,
369
+ population_size=population_size,
370
+ n_generations=n_generations,
371
+ n_plots=n_plots
372
+ )
373
+
374
+ if not pareto_front.layouts:
375
+ return OrchestrationResult(
376
+ status=OrchestrationStatus.FAILURE,
377
+ message="NSGA-II failed to generate valid layouts",
378
+ suggestions=[
379
+ "Reduce number of plots",
380
+ "Relax constraints",
381
+ "Check site configuration"
382
+ ]
383
+ )
384
+
385
+ # STEP 2: MILP Validation for each layout (Giải)
386
+ self.logger.info("Step 2: MILP validation")
387
+ validated_layouts = []
388
+
389
+ for layout in pareto_front.layouts:
390
+ refined_layout, milp_result = self.milp_solver.validate_and_refine(layout)
391
+
392
+ if milp_result.is_success():
393
+ validated_layouts.append(refined_layout)
394
+
395
+ if not validated_layouts:
396
+ return OrchestrationResult(
397
+ status=OrchestrationStatus.CONFLICT,
398
+ message="No layouts passed MILP validation",
399
+ data={'original_count': len(pareto_front.layouts)},
400
+ suggestions=[
401
+ "Reduce plot density",
402
+ "Increase road network",
403
+ "Review constraint compatibility"
404
+ ]
405
+ )
406
+
407
+ # STEP 3: Regulatory Compliance Check (Giải)
408
+ self.logger.info("Step 3: Regulatory compliance check")
409
+ compliant_layouts = []
410
+ violations_summary = []
411
+
412
+ for layout in validated_layouts:
413
+ report = self.regulation_checker.validate_compliance(layout)
414
+ layout.metrics.is_compliant = report.is_compliant
415
+ layout.metrics.compliance_violations = report.violations
416
+
417
+ if report.is_compliant:
418
+ compliant_layouts.append(layout)
419
+ else:
420
+ violations_summary.extend(report.violations[:2]) # Top 2 violations
421
+
422
+ # Hiểu (Interpretation) + Quyết định (Reasoning)
423
+ if not compliant_layouts:
424
+ return OrchestrationResult(
425
+ status=OrchestrationStatus.CONFLICT,
426
+ message="No layouts meet regulatory compliance",
427
+ data={
428
+ 'validated_count': len(validated_layouts),
429
+ 'sample_violations': violations_summary[:5]
430
+ },
431
+ suggestions=[
432
+ "Increase green space ratio",
433
+ "Review boundary setbacks",
434
+ "Reduce plot density"
435
+ ]
436
+ )
437
+
438
+ # Success - store Pareto front
439
+ self.current_pareto = ParetoFront(
440
+ layouts=compliant_layouts,
441
+ generation_time_seconds=pareto_front.generation_time_seconds
442
+ )
443
+ self.current_layouts = compliant_layouts
444
+
445
+ # Generate summary
446
+ max_sellable = self.current_pareto.get_max_sellable_layout()
447
+ max_green = self.current_pareto.get_max_green_layout()
448
+ balanced = self.current_pareto.get_balanced_layout()
449
+
450
+ return OrchestrationResult(
451
+ status=OrchestrationStatus.SUCCESS,
452
+ message=f"Generated {len(compliant_layouts)} compliant layouts",
453
+ data={
454
+ 'num_layouts': len(compliant_layouts),
455
+ 'generation_time_seconds': pareto_front.generation_time_seconds,
456
+ 'max_sellable_area': max_sellable.metrics.sellable_area_sqm if max_sellable else 0,
457
+ 'max_green_ratio': max_green.metrics.green_space_ratio if max_green else 0,
458
+ 'balanced_sellable': balanced.metrics.sellable_area_sqm if balanced else 0,
459
+ 'scenarios': [
460
+ {
461
+ 'name': 'Max Sellable',
462
+ 'id': max_sellable.id if max_sellable else None,
463
+ 'sellable_sqm': max_sellable.metrics.sellable_area_sqm if max_sellable else 0
464
+ },
465
+ {
466
+ 'name': 'Max Green',
467
+ 'id': max_green.id if max_green else None,
468
+ 'green_ratio': max_green.metrics.green_space_ratio if max_green else 0
469
+ },
470
+ {
471
+ 'name': 'Balanced',
472
+ 'id': balanced.id if balanced else None
473
+ }
474
+ ]
475
+ }
476
+ )
477
+
478
+ except Exception as e:
479
+ self.logger.error(f"Optimization failed: {e}")
480
+ return OrchestrationResult(
481
+ status=OrchestrationStatus.FAILURE,
482
+ message=f"Optimization pipeline failed: {str(e)}"
483
+ )
484
+
485
+ # =========================================================================
486
+ # STAGE 5: Engineering Delivery (Giải + Output)
487
+ # =========================================================================
488
+
489
+ def export_layout(
490
+ self,
491
+ layout_id: str,
492
+ output_path: str,
493
+ format: str = "dxf"
494
+ ) -> OrchestrationResult:
495
+ """
496
+ Export a specific layout to file
497
+
498
+ Args:
499
+ layout_id: Layout ID to export
500
+ output_path: Output file path
501
+ format: Export format ('dxf')
502
+
503
+ Returns:
504
+ OrchestrationResult
505
+ """
506
+ # Find layout
507
+ layout = None
508
+ for l in self.current_layouts:
509
+ if l.id == layout_id:
510
+ layout = l
511
+ break
512
+
513
+ if not layout:
514
+ return OrchestrationResult(
515
+ status=OrchestrationStatus.FAILURE,
516
+ message=f"Layout not found: {layout_id}"
517
+ )
518
+
519
+ try:
520
+ if format.lower() == "dxf":
521
+ filepath = self.dxf_exporter.export(layout, output_path)
522
+ return OrchestrationResult(
523
+ status=OrchestrationStatus.SUCCESS,
524
+ message=f"Layout exported to {filepath}",
525
+ data={'filepath': filepath, 'format': 'DXF'}
526
+ )
527
+ else:
528
+ return OrchestrationResult(
529
+ status=OrchestrationStatus.FAILURE,
530
+ message=f"Unsupported format: {format}"
531
+ )
532
+
533
+ except Exception as e:
534
+ return OrchestrationResult(
535
+ status=OrchestrationStatus.FAILURE,
536
+ message=f"Export failed: {str(e)}"
537
+ )
538
+
539
+ def export_all_layouts(self, output_dir: str) -> OrchestrationResult:
540
+ """
541
+ Export all layouts in Pareto front
542
+
543
+ Args:
544
+ output_dir: Output directory
545
+
546
+ Returns:
547
+ OrchestrationResult
548
+ """
549
+ if not self.current_pareto:
550
+ return OrchestrationResult(
551
+ status=OrchestrationStatus.FAILURE,
552
+ message="No optimization results available"
553
+ )
554
+
555
+ try:
556
+ files = self.dxf_exporter.export_pareto_front(
557
+ self.current_pareto,
558
+ output_dir,
559
+ prefix="layout"
560
+ )
561
+
562
+ return OrchestrationResult(
563
+ status=OrchestrationStatus.SUCCESS,
564
+ message=f"Exported {len(files)} layouts",
565
+ data={'files': files}
566
+ )
567
+
568
+ except Exception as e:
569
+ return OrchestrationResult(
570
+ status=OrchestrationStatus.FAILURE,
571
+ message=f"Export failed: {str(e)}"
572
+ )
573
+
574
+ # =========================================================================
575
+ # JSON Interface for LLM Function Calling
576
+ # =========================================================================
577
+
578
+ def execute_command(self, command_json: str) -> str:
579
+ """
580
+ Execute a command from LLM via JSON
581
+
582
+ This is the standardized interface for LLM → Orchestrator communication.
583
+
584
+ Input format:
585
+ {
586
+ "action": "initialize_site" | "generate_roads" | "add_constraint" | "optimize" | "export",
587
+ "parameters": {...}
588
+ }
589
+
590
+ Args:
591
+ command_json: JSON command string
592
+
593
+ Returns:
594
+ JSON response string
595
+ """
596
+ try:
597
+ command = json.loads(command_json)
598
+ action = command.get('action')
599
+ params = command.get('parameters', {})
600
+
601
+ if action == 'initialize_site':
602
+ result = self.initialize_site(**params)
603
+ elif action == 'generate_roads':
604
+ result = self.generate_road_network(**params)
605
+ elif action == 'add_constraint':
606
+ result = self.add_constraint(**params)
607
+ elif action == 'optimize':
608
+ result = self.run_optimization(**params)
609
+ elif action == 'export':
610
+ result = self.export_layout(**params)
611
+ elif action == 'export_all':
612
+ result = self.export_all_layouts(**params)
613
+ else:
614
+ result = OrchestrationResult(
615
+ status=OrchestrationStatus.FAILURE,
616
+ message=f"Unknown action: {action}"
617
+ )
618
+
619
+ return result.to_json()
620
+
621
+ except json.JSONDecodeError as e:
622
+ return json.dumps({
623
+ 'status': 'failure',
624
+ 'message': f'Invalid JSON: {str(e)}'
625
+ })
626
+ except Exception as e:
627
+ return json.dumps({
628
+ 'status': 'failure',
629
+ 'message': f'Execution error: {str(e)}'
630
+ })
631
+
632
+
633
+ # Example usage
634
+ if __name__ == "__main__":
635
+ logging.basicConfig(level=logging.INFO)
636
+
637
+ # Initialize orchestrator
638
+ orchestrator = CoreOrchestrator()
639
+
640
+ # Stage 1: Initialize site
641
+ coords = [(0, 0), (500, 0), (500, 500), (0, 500), (0, 0)]
642
+ result = orchestrator.initialize_site(coords, source_type="coordinates")
643
+ print(f"Site init: {result.status.value}")
644
+ print(result.to_json())
645
+
646
+ # Stage 2: Generate roads
647
+ result = orchestrator.generate_road_network(pattern="grid", primary_spacing=150)
648
+ print(f"\nRoad gen: {result.status.value}")
649
+
650
+ # Stage 4: Run optimization
651
+ result = orchestrator.run_optimization(
652
+ population_size=50,
653
+ n_generations=50,
654
+ n_plots=10
655
+ )
656
+ print(f"\nOptimization: {result.status.value}")
657
+ print(result.to_json())
658
+
659
+ # Stage 5: Export
660
+ if result.status == OrchestrationStatus.SUCCESS:
661
+ result = orchestrator.export_all_layouts("output/")
662
+ print(f"\nExport: {result.status.value}")
src/export/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Export package"""
2
+ from src.export.dxf_exporter import DXFExporter
src/export/dxf_exporter.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DXF Exporter - Engineering Delivery
3
+ Export layouts to AutoCAD DXF format with proper layering
4
+ """
5
+ import ezdxf
6
+ from ezdxf.enums import TextEntityAlignment
7
+ from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString
8
+ from typing import List, Optional, Dict, Tuple
9
+ from pathlib import Path
10
+ import logging
11
+ from datetime import datetime
12
+
13
+ from src.models.domain import Layout, Plot, PlotType, SiteBoundary, RoadNetwork
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class DXFExporter:
19
+ """
20
+ AutoCAD DXF file exporter
21
+
22
+ Exports industrial estate layouts with proper layering:
23
+ - SITE_BOUNDARY: Site boundary polygon
24
+ - ROADS_PRIMARY: Primary road network
25
+ - ROADS_SECONDARY: Secondary road network
26
+ - PLOTS_INDUSTRIAL: Industrial plots
27
+ - PLOTS_GREEN: Green space plots
28
+ - UTILITIES: Utility corridors
29
+ - ANNOTATIONS: Text annotations and dimensions
30
+ """
31
+
32
+ # Layer configuration
33
+ LAYERS = {
34
+ 'SITE_BOUNDARY': {'color': 7, 'linetype': 'CONTINUOUS'}, # White
35
+ 'ROADS_PRIMARY': {'color': 1, 'linetype': 'CONTINUOUS'}, # Red
36
+ 'ROADS_SECONDARY': {'color': 3, 'linetype': 'DASHED'}, # Green
37
+ 'ROADS_TERTIARY': {'color': 4, 'linetype': 'DASHED'}, # Cyan
38
+ 'PLOTS_INDUSTRIAL': {'color': 5, 'linetype': 'CONTINUOUS'}, # Blue
39
+ 'PLOTS_GREEN': {'color': 3, 'linetype': 'CONTINUOUS'}, # Green
40
+ 'PLOTS_UTILITY': {'color': 6, 'linetype': 'CONTINUOUS'}, # Magenta
41
+ 'CONSTRAINTS': {'color': 1, 'linetype': 'PHANTOM'}, # Red dashed
42
+ 'ANNOTATIONS': {'color': 7, 'linetype': 'CONTINUOUS'}, # White
43
+ 'DIMENSIONS': {'color': 2, 'linetype': 'CONTINUOUS'}, # Yellow
44
+ }
45
+
46
+ def __init__(self, version: str = "R2010"):
47
+ """
48
+ Initialize DXF exporter
49
+
50
+ Args:
51
+ version: DXF version ('R12', 'R2000', 'R2004', 'R2007', 'R2010', 'R2013', 'R2018')
52
+ """
53
+ self.version = version
54
+ self.logger = logging.getLogger(__name__)
55
+
56
+ def export(
57
+ self,
58
+ layout: Layout,
59
+ filepath: str,
60
+ include_annotations: bool = True,
61
+ include_dimensions: bool = True
62
+ ) -> str:
63
+ """
64
+ Export layout to DXF file
65
+
66
+ Args:
67
+ layout: Layout to export
68
+ filepath: Output file path
69
+ include_annotations: Include text annotations
70
+ include_dimensions: Include dimensions
71
+
72
+ Returns:
73
+ Path to created file
74
+ """
75
+ self.logger.info(f"Exporting layout {layout.id} to DXF: {filepath}")
76
+
77
+ # Create new DXF document with setup=True to include standard linetypes (DASHED, PHANTOM, etc.)
78
+ doc = ezdxf.new(dxfversion=self.version, setup=True)
79
+ msp = doc.modelspace()
80
+
81
+ # Setup layers and dimension style
82
+ self._setup_layers(doc)
83
+ self._setup_dimension_style(doc)
84
+
85
+ # Export site boundary
86
+ if layout.site_boundary and layout.site_boundary.geometry:
87
+ self._export_site_boundary(msp, layout.site_boundary)
88
+
89
+ # Export road network
90
+ if layout.road_network:
91
+ self._export_road_network(msp, layout.road_network)
92
+
93
+ # Export plots
94
+ for plot in layout.plots:
95
+ self._export_plot(msp, plot)
96
+
97
+ # Export constraints
98
+ if layout.site_boundary:
99
+ for constraint in layout.site_boundary.constraints:
100
+ self._export_constraint(msp, constraint)
101
+
102
+ # Add annotations
103
+ if include_annotations:
104
+ self._add_annotations(msp, layout)
105
+
106
+ # Add dimensions
107
+ if include_dimensions:
108
+ self._add_dimensions(msp, layout)
109
+
110
+ # Add title block
111
+ self._add_title_block(msp, layout)
112
+
113
+ # Save file with error handling for file locking
114
+ output_path = Path(filepath)
115
+ output_path.parent.mkdir(parents=True, exist_ok=True)
116
+
117
+ try:
118
+ doc.saveas(str(filepath))
119
+ except PermissionError:
120
+ # File is locked (e.g., open in AutoCAD)
121
+ self.logger.warning(f"File is locked: {filepath}, trying alternate name")
122
+ timestamp = datetime.now().strftime('%H%M%S')
123
+ alt_path = output_path.with_stem(f"{output_path.stem}_{timestamp}")
124
+ doc.saveas(str(alt_path))
125
+ self.logger.info(f"DXF exported to alternate path: {alt_path}")
126
+ return str(alt_path)
127
+
128
+ self.logger.info(f"DXF exported successfully: {filepath}")
129
+ return str(filepath)
130
+
131
+ def _setup_layers(self, doc):
132
+ """Setup DXF layers with colors and linetypes"""
133
+ for layer_name, config in self.LAYERS.items():
134
+ linetype = config.get('linetype', 'CONTINUOUS')
135
+ # Ensure linetype exists (setup=True should provide DASHED, PHANTOM, etc.)
136
+ if linetype not in doc.linetypes:
137
+ linetype = 'CONTINUOUS'
138
+ doc.layers.add(
139
+ layer_name,
140
+ color=config['color'],
141
+ linetype=linetype
142
+ )
143
+
144
+ def _setup_dimension_style(self, doc):
145
+ """
146
+ Setup custom dimension style for stability
147
+ Prevents crashes from malformed default dimension styles
148
+ """
149
+ # Create custom engineering dimension style
150
+ if 'ENG_DIM' not in doc.dimstyles:
151
+ dim_style = doc.dimstyles.new('ENG_DIM')
152
+ dim_style.dxf.dimtxt = 2.5 # Text height
153
+ dim_style.dxf.dimasz = 2.5 # Arrow size
154
+ dim_style.dxf.dimexe = 1.5 # Extension line extension
155
+ dim_style.dxf.dimexo = 0.625 # Extension line offset
156
+ dim_style.dxf.dimgap = 0.625 # Gap from dimension line
157
+ dim_style.dxf.dimdec = 2 # Decimal places
158
+ dim_style.dxf.dimtad = 1 # Text above dimension line
159
+ dim_style.dxf.dimclrd = 2 # Dimension line color (yellow)
160
+ dim_style.dxf.dimclre = 2 # Extension line color (yellow)
161
+ dim_style.dxf.dimclrt = 7 # Text color (white)
162
+
163
+ def _export_site_boundary(self, msp, site: SiteBoundary):
164
+ """Export site boundary polygon"""
165
+ if site.geometry:
166
+ coords = self._polygon_to_coords(site.geometry)
167
+ msp.add_lwpolyline(
168
+ coords,
169
+ dxfattribs={'layer': 'SITE_BOUNDARY', 'closed': True}
170
+ )
171
+
172
+ def _export_road_network(self, msp, road_network: RoadNetwork):
173
+ """Export road network lines"""
174
+ # Primary roads
175
+ if road_network.primary_roads:
176
+ self._export_multilinestring(
177
+ msp, road_network.primary_roads, 'ROADS_PRIMARY'
178
+ )
179
+
180
+ # Secondary roads
181
+ if road_network.secondary_roads:
182
+ self._export_multilinestring(
183
+ msp, road_network.secondary_roads, 'ROADS_SECONDARY'
184
+ )
185
+
186
+ # Tertiary roads
187
+ if road_network.tertiary_roads:
188
+ self._export_multilinestring(
189
+ msp, road_network.tertiary_roads, 'ROADS_TERTIARY'
190
+ )
191
+
192
+ def _export_multilinestring(self, msp, geometry, layer: str):
193
+ """Export MultiLineString or LineString to DXF"""
194
+ if hasattr(geometry, 'geoms'):
195
+ lines = geometry.geoms
196
+ else:
197
+ lines = [geometry]
198
+
199
+ for line in lines:
200
+ if isinstance(line, LineString):
201
+ points = [(p[0], p[1]) for p in line.coords]
202
+ msp.add_lwpolyline(
203
+ points,
204
+ dxfattribs={'layer': layer}
205
+ )
206
+
207
+ def _export_plot(self, msp, plot: Plot):
208
+ """Export a plot polygon"""
209
+ if not plot.geometry:
210
+ return
211
+
212
+ # Determine layer based on plot type
213
+ layer_map = {
214
+ PlotType.INDUSTRIAL: 'PLOTS_INDUSTRIAL',
215
+ PlotType.GREEN_SPACE: 'PLOTS_GREEN',
216
+ PlotType.UTILITY: 'PLOTS_UTILITY',
217
+ PlotType.ROAD: 'ROADS_TERTIARY',
218
+ PlotType.BUFFER: 'CONSTRAINTS'
219
+ }
220
+ layer = layer_map.get(plot.type, 'PLOTS_INDUSTRIAL')
221
+
222
+ # Export polygon
223
+ coords = self._polygon_to_coords(plot.geometry)
224
+ msp.add_lwpolyline(
225
+ coords,
226
+ dxfattribs={'layer': layer, 'closed': True}
227
+ )
228
+
229
+ # Add plot ID label at centroid
230
+ centroid = plot.geometry.centroid
231
+ msp.add_text(
232
+ plot.id,
233
+ dxfattribs={
234
+ 'layer': 'ANNOTATIONS',
235
+ 'height': 2,
236
+ 'insert': (centroid.x, centroid.y)
237
+ }
238
+ )
239
+
240
+ def _export_constraint(self, msp, constraint):
241
+ """Export constraint zone"""
242
+ if not constraint.geometry:
243
+ return
244
+
245
+ if isinstance(constraint.geometry, Polygon):
246
+ coords = self._polygon_to_coords(constraint.geometry)
247
+ msp.add_lwpolyline(
248
+ coords,
249
+ dxfattribs={'layer': 'CONSTRAINTS', 'closed': True}
250
+ )
251
+
252
+ def _add_annotations(self, msp, layout: Layout):
253
+ """Add text annotations"""
254
+ # Summary annotation at top-right
255
+ if layout.site_boundary:
256
+ bounds = layout.site_boundary.geometry.bounds
257
+ maxx, maxy = bounds[2], bounds[3]
258
+
259
+ # Create summary text
260
+ metrics = layout.metrics
261
+ summary_lines = [
262
+ f"LAYOUT SUMMARY",
263
+ f"----------------",
264
+ f"Total Area: {metrics.total_area_sqm:.0f} m²",
265
+ f"Sellable: {metrics.sellable_area_sqm:.0f} m² ({metrics.sellable_ratio*100:.1f}%)",
266
+ f"Green: {metrics.green_space_area_sqm:.0f} m² ({metrics.green_space_ratio*100:.1f}%)",
267
+ f"Roads: {metrics.road_area_sqm:.0f} m²",
268
+ f"Num Plots: {metrics.num_plots}",
269
+ f"Compliant: {'Yes' if metrics.is_compliant else 'No'}"
270
+ ]
271
+
272
+ y_offset = maxy + 20
273
+ for line in summary_lines:
274
+ msp.add_text(
275
+ line,
276
+ dxfattribs={
277
+ 'layer': 'ANNOTATIONS',
278
+ 'height': 3,
279
+ 'insert': (maxx + 20, y_offset)
280
+ }
281
+ )
282
+ y_offset -= 5
283
+
284
+ def _add_dimensions(self, msp, layout: Layout):
285
+ """Add dimension annotations"""
286
+ if layout.site_boundary and layout.site_boundary.geometry:
287
+ bounds = layout.site_boundary.geometry.bounds
288
+ minx, miny, maxx, maxy = bounds
289
+
290
+ # Add site dimensions using custom dimension style
291
+ # Width dimension
292
+ msp.add_linear_dim(
293
+ base=(minx, miny - 10),
294
+ p1=(minx, miny),
295
+ p2=(maxx, miny),
296
+ dimstyle='ENG_DIM',
297
+ dxfattribs={'layer': 'DIMENSIONS'}
298
+ ).render()
299
+
300
+ # Height dimension
301
+ msp.add_linear_dim(
302
+ base=(maxx + 10, miny),
303
+ p1=(maxx, miny),
304
+ p2=(maxx, maxy),
305
+ angle=90,
306
+ dimstyle='ENG_DIM',
307
+ dxfattribs={'layer': 'DIMENSIONS'}
308
+ ).render()
309
+
310
+ def _add_title_block(self, msp, layout: Layout):
311
+ """Add title block with project info"""
312
+ if not layout.site_boundary:
313
+ return
314
+
315
+ bounds = layout.site_boundary.geometry.bounds
316
+ minx, miny = bounds[0], bounds[1]
317
+
318
+ # Title block at bottom-left
319
+ title_lines = [
320
+ "INDUSTRIAL ESTATE MASTER PLAN",
321
+ f"Layout ID: {layout.id}",
322
+ f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
323
+ "REMB Optimization Engine v0.1.0",
324
+ "PiXerse.AI"
325
+ ]
326
+
327
+ y_offset = miny - 30
328
+ for i, line in enumerate(title_lines):
329
+ msp.add_text(
330
+ line,
331
+ dxfattribs={
332
+ 'layer': 'ANNOTATIONS',
333
+ 'height': 4 if i == 0 else 2.5,
334
+ 'insert': (minx, y_offset)
335
+ }
336
+ )
337
+ y_offset -= 6
338
+
339
+ def _polygon_to_coords(self, polygon: Polygon) -> List[Tuple[float, float]]:
340
+ """Convert Shapely polygon to coordinate list"""
341
+ return [(p[0], p[1]) for p in polygon.exterior.coords]
342
+
343
+ def export_pareto_front(
344
+ self,
345
+ pareto_front,
346
+ output_dir: str,
347
+ prefix: str = "layout"
348
+ ) -> List[str]:
349
+ """
350
+ Export all layouts in a Pareto front
351
+
352
+ Args:
353
+ pareto_front: ParetoFront object
354
+ output_dir: Output directory
355
+ prefix: Filename prefix
356
+
357
+ Returns:
358
+ List of created file paths
359
+ """
360
+ output_path = Path(output_dir)
361
+ output_path.mkdir(parents=True, exist_ok=True)
362
+
363
+ files = []
364
+ for i, layout in enumerate(pareto_front.layouts):
365
+ filename = f"{prefix}_{i:02d}_{layout.id[:8]}.dxf"
366
+ filepath = output_path / filename
367
+ self.export(layout, str(filepath))
368
+ files.append(str(filepath))
369
+
370
+ return files
371
+
372
+
373
+ # Example usage
374
+ if __name__ == "__main__":
375
+ from shapely.geometry import box
376
+ from src.models.domain import Layout, Plot, PlotType, SiteBoundary, RoadNetwork, LayoutMetrics
377
+ from shapely.geometry import LineString, MultiLineString
378
+
379
+ # Create example layout
380
+ site_geom = box(0, 0, 500, 500)
381
+ site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
382
+ site.buildable_area_sqm = site.area_sqm
383
+
384
+ layout = Layout(site_boundary=site)
385
+
386
+ # Add some plots
387
+ layout.plots = [
388
+ Plot(
389
+ id="plot_001",
390
+ geometry=box(60, 60, 160, 160),
391
+ area_sqm=10000,
392
+ type=PlotType.INDUSTRIAL,
393
+ width_m=100,
394
+ depth_m=100
395
+ ),
396
+ Plot(
397
+ id="plot_002",
398
+ geometry=box(200, 60, 300, 160),
399
+ area_sqm=10000,
400
+ type=PlotType.INDUSTRIAL,
401
+ width_m=100,
402
+ depth_m=100
403
+ ),
404
+ Plot(
405
+ id="green_001",
406
+ geometry=box(60, 200, 160, 300),
407
+ area_sqm=10000,
408
+ type=PlotType.GREEN_SPACE,
409
+ width_m=100,
410
+ depth_m=100
411
+ )
412
+ ]
413
+
414
+ # Add road network
415
+ layout.road_network = RoadNetwork(
416
+ primary_roads=MultiLineString([
417
+ LineString([(0, 250), (500, 250)]),
418
+ LineString([(250, 0), (250, 500)])
419
+ ]),
420
+ total_length_m=1000
421
+ )
422
+
423
+ # Calculate metrics
424
+ layout.metrics = LayoutMetrics(
425
+ total_area_sqm=250000,
426
+ sellable_area_sqm=20000,
427
+ green_space_area_sqm=10000,
428
+ road_area_sqm=24000,
429
+ sellable_ratio=0.65,
430
+ green_space_ratio=0.15,
431
+ num_plots=2,
432
+ is_compliant=True
433
+ )
434
+
435
+ # Export
436
+ exporter = DXFExporter()
437
+ output_file = exporter.export(
438
+ layout,
439
+ "output/test_layout.dxf",
440
+ include_annotations=True,
441
+ include_dimensions=True
442
+ )
443
+
444
+ print(f"Exported to: {output_file}")