Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -16,8 +16,8 @@ import tempfile
|
|
| 16 |
|
| 17 |
class AdvancedGridOptimizer:
|
| 18 |
def __init__(self):
|
| 19 |
-
#
|
| 20 |
-
self.
|
| 21 |
8.5: {"depths": [21, 25, 28], "type": "SLHC", "squares": "11-16"},
|
| 22 |
10.5: {"depths": [21, 25, 28, 32, 35], "type": "SLHC", "squares": "13-21.5"},
|
| 23 |
12.5: {"depths": [21, 25, 28, 30, 32], "type": "Standard", "squares": "16-24"},
|
|
@@ -31,17 +31,39 @@ class AdvancedGridOptimizer:
|
|
| 31 |
16.8: {"depths": [30, 32], "type": "Corner-Premium", "squares": "26-32"}
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
self.slhc_widths = [8.5, 10.5]
|
| 35 |
self.standard_widths = [12.5, 14.0]
|
| 36 |
self.premium_widths = [16.0, 18.0]
|
| 37 |
self.corner_specific = [11.0, 13.3, 14.8, 16.8]
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Define corner_widths as all widths suitable for corners
|
| 40 |
self.corner_widths = self.corner_specific + [14.0, 16.0, 18.0]
|
| 41 |
|
| 42 |
# Enhanced color palette with RPM brand colors
|
| 43 |
self.color_schemes = {
|
| 44 |
'rpm_primary': {
|
|
|
|
| 45 |
8.5: '#802B2B', # Burgundy for SLHC
|
| 46 |
10.5: '#AB3838', # Burgundy 75%
|
| 47 |
12.5: '#216767', # Teal
|
|
@@ -51,9 +73,16 @@ class AdvancedGridOptimizer:
|
|
| 51 |
11.0: '#4F8585', # Teal 75%
|
| 52 |
13.3: '#545D51', # RPM Green 75%
|
| 53 |
14.8: '#697687', # Blue 75%
|
| 54 |
-
16.8: '#FFCF6D'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
'rpm_contrast': {
|
|
|
|
| 57 |
8.5: '#D69C9C', # Burgundy 50%
|
| 58 |
10.5: '#E2C1B7', # Burgundy 25%
|
| 59 |
12.5: '#95B5B5', # Teal 50%
|
|
@@ -63,9 +92,16 @@ class AdvancedGridOptimizer:
|
|
| 63 |
11.0: '#D6E3E3', # Teal 25%
|
| 64 |
13.3: '#B6B8B2', # RPM Green 25%
|
| 65 |
14.8: '#CCD7E4', # Blue 25%
|
| 66 |
-
16.8: '#FFEFCE'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
},
|
| 68 |
'rpm_monochrome': {
|
|
|
|
| 69 |
8.5: '#2E3E2F', # RPM Green 100%
|
| 70 |
10.5: '#545D51', # RPM Green 75%
|
| 71 |
12.5: '#80857B', # RPM Green 50%
|
|
@@ -75,16 +111,40 @@ class AdvancedGridOptimizer:
|
|
| 75 |
11.0: '#D1D3D4', # Black 25%
|
| 76 |
13.3: '#216767', # Teal (accent)
|
| 77 |
14.8: '#415B6E', # Blue (accent)
|
| 78 |
-
16.8: '#FF8E3C'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
}
|
| 81 |
|
| 82 |
self.current_scheme = 'rpm_primary'
|
| 83 |
self.current_solution = None # Store current AI solution
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
def create_enhanced_visualization(self, solution, stage_width, stage_depth=32, title="Premium Grid Layout", show_variance=None):
|
| 86 |
-
"""Create a clean 2D visualization with corner splays"""
|
| 87 |
-
|
|
|
|
|
|
|
| 88 |
facecolor='#2E3E2F')
|
| 89 |
|
| 90 |
# Main visualization
|
|
@@ -95,20 +155,27 @@ class AdvancedGridOptimizer:
|
|
| 95 |
|
| 96 |
# Set up main plot with RPM green background
|
| 97 |
ax1.set_xlim(-5, stage_width + 5)
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
ax1.set_facecolor('#2E3E2F')
|
| 100 |
|
| 101 |
# Add title with variance if provided
|
| 102 |
if show_variance is not None:
|
| 103 |
variance_color = '#216767' if abs(show_variance) < 0.001 else '#802B2B'
|
| 104 |
-
|
|
|
|
|
|
|
| 105 |
ax1.set_title(title_text, fontsize=28, fontweight='bold', pad=25, color='white')
|
| 106 |
else:
|
| 107 |
ax1.set_title(title, fontsize=28, fontweight='bold', pad=25, color='white')
|
| 108 |
|
| 109 |
# Add subtle gradient background
|
| 110 |
gradient = np.linspace(0.3, 0.1, 100).reshape(1, -1)
|
| 111 |
-
|
|
|
|
| 112 |
cmap='Greys', alpha=0.3, zorder=0)
|
| 113 |
|
| 114 |
# Add street with label
|
|
@@ -119,17 +186,45 @@ class AdvancedGridOptimizer:
|
|
| 119 |
ax1.text(stage_width/2, -2, 'STREET', ha='center', va='center',
|
| 120 |
fontsize=20, color='white', fontweight='bold')
|
| 121 |
|
| 122 |
-
# Draw lots with corner splays
|
| 123 |
splay_size = 3 # 3m corner splay
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
for i, (width, lot_type) in enumerate(solution):
|
| 127 |
# Get base color
|
| 128 |
if width in colors:
|
| 129 |
base_color = colors[width]
|
| 130 |
else:
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
# Check position
|
| 135 |
is_corner = (i == 0 or i == len(solution) - 1)
|
|
@@ -139,14 +234,14 @@ class AdvancedGridOptimizer:
|
|
| 139 |
edge_color = 'white'
|
| 140 |
linewidth = 4.0 if is_corner else 3.0
|
| 141 |
|
| 142 |
-
# Create lot shape with
|
| 143 |
-
if is_corner:
|
| 144 |
-
# Corner lot with splay
|
| 145 |
if i == 0: # First corner
|
| 146 |
vertices = [
|
| 147 |
(x_pos + splay_size, 8), # Start after splay
|
| 148 |
(x_pos + width, 8),
|
| 149 |
-
(x_pos + width, 8 + lot_height),
|
| 150 |
(x_pos, 8 + lot_height), # Straight rear
|
| 151 |
(x_pos, 8 + splay_size) # Splay corner
|
| 152 |
]
|
|
@@ -155,7 +250,7 @@ class AdvancedGridOptimizer:
|
|
| 155 |
(x_pos, 8),
|
| 156 |
(x_pos + width - splay_size, 8),
|
| 157 |
(x_pos + width, 8 + splay_size), # Splay corner
|
| 158 |
-
(x_pos + width, 8 + lot_height),
|
| 159 |
(x_pos, 8 + lot_height)
|
| 160 |
]
|
| 161 |
|
|
@@ -175,7 +270,7 @@ class AdvancedGridOptimizer:
|
|
| 175 |
ax1.plot([x_pos + width - splay_size, x_pos + width],
|
| 176 |
[8, 8 + splay_size], 'white', linewidth=2, alpha=0.8)
|
| 177 |
else:
|
| 178 |
-
# Regular lot
|
| 179 |
lot = FancyBboxPatch((x_pos, 8), width, lot_height,
|
| 180 |
boxstyle="round,pad=0.1",
|
| 181 |
facecolor=face_color,
|
|
@@ -184,7 +279,7 @@ class AdvancedGridOptimizer:
|
|
| 184 |
zorder=3)
|
| 185 |
ax1.add_patch(lot)
|
| 186 |
|
| 187 |
-
# Add subtle glow
|
| 188 |
glow = FancyBboxPatch((x_pos - 0.2, 7.8), width + 0.4, lot_height + 0.4,
|
| 189 |
boxstyle="round,pad=0.15",
|
| 190 |
facecolor='none',
|
|
@@ -194,16 +289,13 @@ class AdvancedGridOptimizer:
|
|
| 194 |
zorder=2)
|
| 195 |
ax1.add_patch(glow)
|
| 196 |
|
| 197 |
-
# Add rear alignment line to emphasize equal depth
|
| 198 |
-
rear_y = 8 + lot_height
|
| 199 |
-
ax1.plot([x_pos, x_pos + width], [rear_y, rear_y],
|
| 200 |
-
color=edge_color, linewidth=1, alpha=0.3, linestyle='--')
|
| 201 |
-
|
| 202 |
# Add lot information (positioned consistently)
|
| 203 |
-
|
|
|
|
|
|
|
| 204 |
ha='center', va='center', fontsize=16, fontweight='bold', color='white')
|
| 205 |
|
| 206 |
-
ax1.text(x_pos + width/2,
|
| 207 |
ha='center', va='center', fontsize=14, fontweight='bold', color='white')
|
| 208 |
|
| 209 |
# Lot type
|
|
@@ -218,10 +310,14 @@ class AdvancedGridOptimizer:
|
|
| 218 |
spec = {**spec, 'type': 'Custom'}
|
| 219 |
|
| 220 |
lot_type_text = spec['type']
|
| 221 |
-
if is_corner:
|
| 222 |
lot_type_text = "CORNER"
|
| 223 |
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
ha='center', va='center', fontsize=11,
|
| 226 |
bbox=dict(boxstyle="round,pad=0.3", facecolor='#545D51',
|
| 227 |
edgecolor='white', alpha=0.9), color='white')
|
|
@@ -231,16 +327,27 @@ class AdvancedGridOptimizer:
|
|
| 231 |
ax1.plot([x_pos, x_pos], [10, 14], 'w-', linewidth=1, alpha=0.3)
|
| 232 |
ax1.plot([x_pos + width, x_pos + width], [10, 14], 'w-', linewidth=1, alpha=0.3)
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
x_pos += width
|
| 235 |
lot_num += 1
|
| 236 |
|
| 237 |
# Add rear alignment line across all lots
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
| 244 |
|
| 245 |
# Add stage dimensions
|
| 246 |
arrow_props = dict(arrowstyle='<->', color='white', lw=3)
|
|
@@ -263,33 +370,48 @@ class AdvancedGridOptimizer:
|
|
| 263 |
unique_widths = len(set(w for w, _ in solution))
|
| 264 |
diversity_score = unique_widths / len(set(self.lot_specifications.keys()))
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
# Calculate actual total width and variance
|
| 277 |
total_width = sum(w for w, _ in solution)
|
| 278 |
variance = total_width - stage_width
|
| 279 |
efficiency = "100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
metrics_lines = [
|
| 282 |
f"📊 TOTAL LOTS: {total_lots}",
|
| 283 |
f"📐 LAND EFFICIENCY: {efficiency}",
|
| 284 |
f"🎯 DIVERSITY: {diversity_score:.0%} ({unique_widths} types)",
|
| 285 |
f"📏 GRID VARIANCE: {variance:+.2f}m",
|
| 286 |
"",
|
| 287 |
-
f"SLHC (≤10.5m): {slhc_count} lots",
|
| 288 |
-
f"Standard (11-14m): {standard_count} lots",
|
| 289 |
-
f"Premium (>14m): {premium_count} lots",
|
| 290 |
"",
|
| 291 |
-
f"🚗 SLHC Pairs: {slhc_pairs}",
|
| 292 |
-
|
| 293 |
]
|
| 294 |
|
| 295 |
col1_text = '\n'.join(metrics_lines[:5])
|
|
@@ -358,12 +480,14 @@ class AdvancedGridOptimizer:
|
|
| 358 |
feedback = f"⚠️ Grid is {-variance:.2f}m too narrow. Add {-variance:.2f}m total width."
|
| 359 |
|
| 360 |
# Add suggestions if not perfect
|
|
|
|
|
|
|
| 361 |
if abs(variance) > 0.001:
|
| 362 |
if variance > 0:
|
| 363 |
# Suggest which lots could be reduced
|
| 364 |
suggestions = []
|
| 365 |
for i, w in enumerate(widths):
|
| 366 |
-
if w - variance >=
|
| 367 |
suggestions.append(f"L{i+1}: reduce from {w:.1f}m to {w-variance:.1f}m")
|
| 368 |
if suggestions:
|
| 369 |
feedback += f"\n\nSuggestions:\n" + "\n".join(suggestions[:3])
|
|
@@ -381,54 +505,19 @@ class AdvancedGridOptimizer:
|
|
| 381 |
return ""
|
| 382 |
return ", ".join([f"{w:.1f}" for w, _ in solution])
|
| 383 |
|
| 384 |
-
def parse_manual_input(self, manual_text):
|
| 385 |
-
"""Parse manual input into structured data"""
|
| 386 |
-
try:
|
| 387 |
-
if not manual_text:
|
| 388 |
-
return {}
|
| 389 |
-
|
| 390 |
-
# Try JSON format first
|
| 391 |
-
if manual_text.strip().startswith('{'):
|
| 392 |
-
return json.loads(manual_text)
|
| 393 |
-
|
| 394 |
-
# Otherwise parse line by line
|
| 395 |
-
result = {}
|
| 396 |
-
for line in manual_text.strip().split('\n'):
|
| 397 |
-
line = line.strip()
|
| 398 |
-
if not line:
|
| 399 |
-
continue
|
| 400 |
-
|
| 401 |
-
if '=' in line:
|
| 402 |
-
parts = line.split('=')
|
| 403 |
-
width_str = parts[0].strip().replace('m', '')
|
| 404 |
-
count_str = parts[1].strip()
|
| 405 |
-
try:
|
| 406 |
-
width_val = float(width_str)
|
| 407 |
-
result[width_val] = int(count_str)
|
| 408 |
-
except:
|
| 409 |
-
pass
|
| 410 |
-
elif ':' in line:
|
| 411 |
-
parts = line.split(':')
|
| 412 |
-
width_str = parts[0].strip().replace('m', '')
|
| 413 |
-
count_str = parts[1].strip()
|
| 414 |
-
try:
|
| 415 |
-
width_val = float(width_str)
|
| 416 |
-
result[width_val] = int(count_str)
|
| 417 |
-
except:
|
| 418 |
-
pass
|
| 419 |
-
return result
|
| 420 |
-
except Exception as e:
|
| 421 |
-
print(f"Error parsing manual input: {e}")
|
| 422 |
-
return {}
|
| 423 |
-
|
| 424 |
def find_optimal_custom_corners(self, stage_width, internal_widths, base_corner_width, tolerance=0.5):
|
| 425 |
"""Find optimal corner widths that can vary slightly from base width"""
|
| 426 |
best_solution = None
|
| 427 |
best_fitness = -float('inf')
|
| 428 |
|
| 429 |
-
#
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
|
| 433 |
# Try variations of corner widths within tolerance
|
| 434 |
variations = np.arange(min_corner_width,
|
|
@@ -446,10 +535,11 @@ class AdvancedGridOptimizer:
|
|
| 446 |
internal_solution = self.find_exact_solution_with_diversity(internal_width, internal_widths)
|
| 447 |
|
| 448 |
if internal_solution:
|
| 449 |
-
#
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
| 453 |
|
| 454 |
# Build complete solution
|
| 455 |
solution = [(round(corner1, 1), 'corner')]
|
|
@@ -484,8 +574,14 @@ class AdvancedGridOptimizer:
|
|
| 484 |
|
| 485 |
# Strategy 2: Try flexible corners if enabled
|
| 486 |
if allow_custom_corners and standard_internal:
|
| 487 |
-
#
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
if any(abs(w - base_width) < 2 for w in enabled_widths):
|
| 490 |
custom_solution = self.find_optimal_custom_corners(
|
| 491 |
stage_width, standard_internal, base_width, tolerance=0.5
|
|
@@ -503,10 +599,15 @@ class AdvancedGridOptimizer:
|
|
| 503 |
|
| 504 |
# Separate widths by size
|
| 505 |
all_widths = sorted(enabled_widths)
|
| 506 |
-
min_internal_width = min(all_widths)
|
| 507 |
|
| 508 |
-
|
| 509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
best_solution = None
|
| 512 |
best_fitness = -float('inf')
|
|
@@ -528,10 +629,11 @@ class AdvancedGridOptimizer:
|
|
| 528 |
)
|
| 529 |
|
| 530 |
for internal_widths in internal_solutions:
|
| 531 |
-
#
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
|
|
|
| 535 |
|
| 536 |
# Build complete solution
|
| 537 |
solution = [(corner1, 'corner')]
|
|
@@ -539,31 +641,34 @@ class AdvancedGridOptimizer:
|
|
| 539 |
solution.append((corner2, 'corner'))
|
| 540 |
|
| 541 |
# Optimize arrangement
|
| 542 |
-
optimized = self.
|
| 543 |
fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
|
| 544 |
|
| 545 |
if fitness > best_fitness:
|
| 546 |
best_fitness = fitness
|
| 547 |
best_solution = optimized
|
| 548 |
|
| 549 |
-
# If no good solution, try without strict corner rules
|
| 550 |
if not best_solution:
|
| 551 |
all_solutions = []
|
| 552 |
self.find_all_combinations_recursive(stage_width, sorted(enabled_widths),
|
| 553 |
[], all_solutions, 20)
|
| 554 |
|
| 555 |
for widths in all_solutions[:50]:
|
| 556 |
-
#
|
| 557 |
-
|
| 558 |
-
if len(sorted_widths) >= 2:
|
| 559 |
-
# Put two largest widths at corners
|
| 560 |
-
solution = [(sorted_widths[-1], 'corner')] # Largest
|
| 561 |
-
solution.extend([(w, 'standard') for w in sorted_widths[:-2]])
|
| 562 |
-
solution.append((sorted_widths[-2], 'corner')) # Second largest
|
| 563 |
-
else:
|
| 564 |
solution = [(w, 'standard') for w in widths]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
|
| 566 |
-
optimized = self.
|
| 567 |
fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
|
| 568 |
|
| 569 |
if fitness > best_fitness:
|
|
@@ -572,6 +677,52 @@ class AdvancedGridOptimizer:
|
|
| 572 |
|
| 573 |
return best_solution
|
| 574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
def find_diverse_combinations(self, target_width, available_widths, max_solutions=20):
|
| 576 |
"""Find combinations that maximize diversity"""
|
| 577 |
all_solutions = []
|
|
@@ -678,7 +829,7 @@ class AdvancedGridOptimizer:
|
|
| 678 |
current.pop()
|
| 679 |
|
| 680 |
def optimize_slhc_grouping(self, lots):
|
| 681 |
-
"""Optimize lot arrangement with sophisticated rules"""
|
| 682 |
if not lots or len(lots) <= 1:
|
| 683 |
return lots
|
| 684 |
|
|
@@ -812,43 +963,56 @@ class AdvancedGridOptimizer:
|
|
| 812 |
fitness -= max_repetition * 500 # Penalty for too many of same width
|
| 813 |
fitness += diversity_ratio * 3000 # Bonus for good diversity ratio
|
| 814 |
|
| 815 |
-
#
|
| 816 |
-
if
|
| 817 |
-
|
| 818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
-
#
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
fitness -= 2000
|
| 825 |
|
| 826 |
-
#
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
|
| 832 |
-
#
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
fitness += 1500 # Perfect match
|
| 836 |
-
elif corner_diff <= 1.0:
|
| 837 |
-
fitness += 1000 # Very good
|
| 838 |
-
elif corner_diff <= 2.0:
|
| 839 |
-
fitness += 500 # Good
|
| 840 |
-
else:
|
| 841 |
-
fitness -= 500 # Poor balance
|
| 842 |
-
|
| 843 |
-
# SLHC grouping bonus
|
| 844 |
-
for i in range(len(solution) - 1):
|
| 845 |
-
if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5:
|
| 846 |
-
fitness += 300 # Adjacent SLHC bonus
|
| 847 |
-
|
| 848 |
-
# Penalize corner-specific widths used internally
|
| 849 |
-
for i in range(1, len(solution) - 1):
|
| 850 |
-
if solution[i][0] in self.corner_specific:
|
| 851 |
-
fitness -= 200
|
| 852 |
|
| 853 |
return fitness
|
| 854 |
|
|
@@ -873,11 +1037,16 @@ class AdvancedGridOptimizer:
|
|
| 873 |
total_width = sum(w for w, _ in solution)
|
| 874 |
variance = total_width - stage_width
|
| 875 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
report = f"""
|
| 877 |
-
# SUBDIVISION OPTIMIZATION REPORT
|
| 878 |
## Project Analysis for {stage_width}m × {stage_depth}m Stage
|
| 879 |
|
| 880 |
### EXECUTIVE SUMMARY
|
|
|
|
| 881 |
- **Total Lots**: {len(solution)}
|
| 882 |
- **Unique Lot Types**: {unique_widths}
|
| 883 |
- **Land Efficiency**: {"100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"}
|
|
@@ -885,32 +1054,49 @@ class AdvancedGridOptimizer:
|
|
| 885 |
- **Stage Dimensions**: {stage_width}m × {stage_depth}m
|
| 886 |
- **Total Area**: {stage_width * stage_depth}m²
|
| 887 |
{f"- **Custom Widths Used**: {', '.join(custom_widths)}" if custom_widths else ""}
|
| 888 |
-
|
| 889 |
-
### LOT DIVERSITY ANALYSIS
|
| 890 |
"""
|
| 891 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
# Sort by count to show distribution
|
| 893 |
sorted_widths = sorted(width_counts.items(), key=lambda x: x[1], reverse=True)
|
| 894 |
for width, count in sorted_widths:
|
| 895 |
percentage = (count / len(solution)) * 100
|
| 896 |
if width in self.lot_specifications:
|
| 897 |
spec = self.lot_specifications[width]
|
| 898 |
-
|
|
|
|
| 899 |
else:
|
| 900 |
report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): Custom Width\n"
|
| 901 |
|
| 902 |
-
# Corner analysis
|
| 903 |
-
if len(solution) >= 2:
|
| 904 |
report += f"\n### CORNER ANALYSIS\n"
|
| 905 |
report += f"- **Front Corner**: {solution[0][0]:.1f}m with 3m × 3m splay\n"
|
| 906 |
report += f"- **Rear Corner**: {solution[-1][0]:.1f}m with 3m × 3m splay\n"
|
| 907 |
report += f"- **Balance**: {abs(solution[0][0] - solution[-1][0]):.1f}m difference\n"
|
| 908 |
|
| 909 |
report += f"\n### DESIGN FEATURES\n"
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
|
| 915 |
report += f"\n---\n*Report generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"
|
| 916 |
|
|
@@ -929,30 +1115,68 @@ class AdvancedGridOptimizer:
|
|
| 929 |
def create_advanced_app():
|
| 930 |
optimizer = AdvancedGridOptimizer()
|
| 931 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 932 |
def optimize_grid(
|
| 933 |
stage_width,
|
| 934 |
stage_depth,
|
|
|
|
|
|
|
|
|
|
| 935 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 936 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
allow_custom_corners, color_scheme
|
| 938 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
# Update color scheme
|
| 940 |
optimizer.current_scheme = color_scheme
|
| 941 |
|
| 942 |
-
# Collect enabled widths
|
| 943 |
enabled_widths = []
|
| 944 |
-
|
| 945 |
-
if
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
if
|
| 954 |
-
|
| 955 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
|
| 957 |
if not enabled_widths:
|
| 958 |
return None, None, pd.DataFrame(), "Please select at least one lot width!", "", ""
|
|
@@ -974,23 +1198,33 @@ def create_advanced_app():
|
|
| 974 |
|
| 975 |
# Verify solution
|
| 976 |
if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001:
|
| 977 |
-
# Provide suggestions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
return None, pd.DataFrame(), f"""
|
| 979 |
### ❌ Cannot achieve 100% usage with selected widths
|
| 980 |
|
| 981 |
**Stage Width**: {stage_width}m
|
|
|
|
| 982 |
**Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])}
|
| 983 |
|
| 984 |
**Try:**
|
| 985 |
1. Enable more lot types for flexibility
|
| 986 |
2. Enable "Custom Corners" option
|
| 987 |
-
3. Try common stage widths:
|
|
|
|
| 988 |
""", "", ""
|
| 989 |
|
| 990 |
# Create visualizations with variance indicator
|
|
|
|
| 991 |
fig_2d = optimizer.create_enhanced_visualization(
|
| 992 |
optimized_solution, stage_width, stage_depth,
|
| 993 |
-
|
| 994 |
show_variance=variance
|
| 995 |
)
|
| 996 |
|
|
@@ -1017,19 +1251,23 @@ def create_advanced_app():
|
|
| 1017 |
'count': 1,
|
| 1018 |
'type': spec.get('type', 'Custom'),
|
| 1019 |
'squares': spec.get('squares', 'N/A'),
|
| 1020 |
-
'area': width * stage_depth
|
|
|
|
| 1021 |
}
|
| 1022 |
|
| 1023 |
results_data = []
|
| 1024 |
for width, info in sorted(width_counts.items()):
|
| 1025 |
-
|
| 1026 |
'Lot Width': width,
|
| 1027 |
'Count': info['count'],
|
| 1028 |
'Type': info['type'],
|
| 1029 |
'Area Each': f"{info['area']:.0f}m²",
|
| 1030 |
'Total Width': f"{float(width[:-1]) * info['count']:.1f}m",
|
| 1031 |
'Total Area': f"{info['area'] * info['count']:.0f}m²"
|
| 1032 |
-
}
|
|
|
|
|
|
|
|
|
|
| 1033 |
|
| 1034 |
results_df = pd.DataFrame(results_data)
|
| 1035 |
|
|
@@ -1040,31 +1278,30 @@ def create_advanced_app():
|
|
| 1040 |
total_lots = len(optimized_solution)
|
| 1041 |
unique_widths = len(set(w for w, _ in optimized_solution))
|
| 1042 |
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
# Analyze corners
|
| 1048 |
-
corner_info = "N/A"
|
| 1049 |
-
if len(optimized_solution) >= 2:
|
| 1050 |
-
first = optimized_solution[0][0]
|
| 1051 |
-
last = optimized_solution[-1][0]
|
| 1052 |
-
diff = abs(first - last)
|
| 1053 |
|
| 1054 |
-
|
| 1055 |
-
corner_info = f"✨ PERFECT ({first:.1f}m × 2)"
|
| 1056 |
-
elif diff <= 1.0:
|
| 1057 |
-
corner_info = f"✅ Excellent ({first:.1f}m + {last:.1f}m)"
|
| 1058 |
-
elif diff <= 2.0:
|
| 1059 |
-
corner_info = f"👍 Good ({first:.1f}m + {last:.1f}m)"
|
| 1060 |
-
else:
|
| 1061 |
-
corner_info = f"⚠️ Unbalanced ({first:.1f}m + {last:.1f}m)"
|
| 1062 |
-
|
| 1063 |
-
summary = f"""
|
| 1064 |
**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m²
|
|
|
|
| 1065 |
**Total Lots**: {total_lots}
|
|
|
|
| 1066 |
**Unique Lot Types**: {unique_widths}
|
| 1067 |
**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
"""
|
| 1069 |
|
| 1070 |
# Convert solution to string for manual editing
|
|
@@ -1072,8 +1309,14 @@ def create_advanced_app():
|
|
| 1072 |
|
| 1073 |
return fig_2d, results_df, summary, report, manual_edit_string
|
| 1074 |
|
| 1075 |
-
def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme):
|
| 1076 |
"""Update visualization based on manual adjustment"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
optimizer.current_scheme = color_scheme
|
| 1078 |
|
| 1079 |
# Parse manual widths
|
|
@@ -1160,7 +1403,23 @@ def create_advanced_app():
|
|
| 1160 |
with gr.Row():
|
| 1161 |
with gr.Column(scale=1):
|
| 1162 |
with gr.Group():
|
| 1163 |
-
gr.Markdown("### 📐 Stage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1164 |
stage_width = gr.Number(
|
| 1165 |
label="Stage Width (m)",
|
| 1166 |
value=105.0,
|
|
@@ -1174,7 +1433,8 @@ def create_advanced_app():
|
|
| 1174 |
|
| 1175 |
gr.Markdown("### 📏 Lot Width Options")
|
| 1176 |
|
| 1177 |
-
|
|
|
|
| 1178 |
gr.Markdown("**Standard Widths**")
|
| 1179 |
with gr.Row():
|
| 1180 |
enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True)
|
|
@@ -1184,8 +1444,7 @@ def create_advanced_app():
|
|
| 1184 |
enable_14 = gr.Checkbox(label="14.0m", value=True)
|
| 1185 |
enable_16 = gr.Checkbox(label="16.0m", value=True)
|
| 1186 |
enable_18 = gr.Checkbox(label="18.0m", value=False)
|
| 1187 |
-
|
| 1188 |
-
with gr.Group():
|
| 1189 |
enable_corners = gr.Checkbox(
|
| 1190 |
label="Enable Corner-Specific Widths",
|
| 1191 |
value=True,
|
|
@@ -1197,6 +1456,21 @@ def create_advanced_app():
|
|
| 1197 |
with gr.Row():
|
| 1198 |
enable_14_8 = gr.Checkbox(label="14.8m", value=True)
|
| 1199 |
enable_16_8 = gr.Checkbox(label="16.8m", value=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
|
| 1201 |
with gr.Column(scale=1):
|
| 1202 |
gr.Markdown("### ⚙️ Settings")
|
|
@@ -1223,14 +1497,14 @@ def create_advanced_app():
|
|
| 1223 |
|
| 1224 |
gr.Markdown("""
|
| 1225 |
### 💡 Quick Tips:
|
| 1226 |
-
- **
|
|
|
|
|
|
|
| 1227 |
- **Grid Variance**: Shows if layout is perfect (0.0m)
|
| 1228 |
-
- **Manual Adjust**: Edit the result below after optimization
|
| 1229 |
-
- **Diversity Focus**: Maximizes lot variety
|
| 1230 |
""")
|
| 1231 |
|
| 1232 |
with gr.Row():
|
| 1233 |
-
plot_2d = gr.Plot(label="2D Layout
|
| 1234 |
|
| 1235 |
# Manual adjustment section
|
| 1236 |
gr.Markdown("### ✏️ Fine-Tune Result")
|
|
@@ -1258,14 +1532,46 @@ def create_advanced_app():
|
|
| 1258 |
with gr.Column():
|
| 1259 |
report_output = gr.Markdown(label="Professional Report")
|
| 1260 |
|
| 1261 |
-
# Wire up
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1262 |
optimize_btn.click(
|
| 1263 |
optimize_grid,
|
| 1264 |
inputs=[
|
| 1265 |
stage_width,
|
| 1266 |
stage_depth,
|
|
|
|
|
|
|
|
|
|
| 1267 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 1268 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1269 |
allow_custom_corners, color_scheme
|
| 1270 |
],
|
| 1271 |
outputs=[plot_2d, results_table, summary_output, report_output, manual_widths]
|
|
@@ -1273,7 +1579,7 @@ def create_advanced_app():
|
|
| 1273 |
|
| 1274 |
update_btn.click(
|
| 1275 |
update_manual_adjustment,
|
| 1276 |
-
inputs=[manual_widths, stage_width, stage_depth, color_scheme],
|
| 1277 |
outputs=[plot_2d, adjustment_feedback]
|
| 1278 |
)
|
| 1279 |
|
|
|
|
| 16 |
|
| 17 |
class AdvancedGridOptimizer:
|
| 18 |
def __init__(self):
|
| 19 |
+
# Conventional lot widths and their typical depths
|
| 20 |
+
self.conventional_lot_specifications = {
|
| 21 |
8.5: {"depths": [21, 25, 28], "type": "SLHC", "squares": "11-16"},
|
| 22 |
10.5: {"depths": [21, 25, 28, 32, 35], "type": "SLHC", "squares": "13-21.5"},
|
| 23 |
12.5: {"depths": [21, 25, 28, 30, 32], "type": "Standard", "squares": "16-24"},
|
|
|
|
| 31 |
16.8: {"depths": [30, 32], "type": "Corner-Premium", "squares": "26-32"}
|
| 32 |
}
|
| 33 |
|
| 34 |
+
# Medium Density lot specifications
|
| 35 |
+
self.md_rear_loaded_specifications = {
|
| 36 |
+
4.5: {"depths": [19, 25, 28], "type": "MD-Rear Load", "squares": "85.5-126", "build": "2/2/1"},
|
| 37 |
+
6.0: {"depths": [19, 25, 28], "type": "MD-Rear Load", "squares": "114-168", "build": "3/2/2"},
|
| 38 |
+
7.5: {"depths": [25, 28], "type": "MD-Rear Load", "squares": "187.5-210", "build": "3-4/2/2"}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
self.md_front_loaded_specifications = {
|
| 42 |
+
7.0: {"depths": [21], "type": "MD-Front Load", "squares": "147", "build": "3/2/1"},
|
| 43 |
+
8.0: {"depths": [21], "type": "MD-Front Load", "squares": "168", "build": "3-4/2/2"},
|
| 44 |
+
8.5: {"depths": [16], "type": "MD-Front Load", "squares": "136", "build": "3/2/1"},
|
| 45 |
+
10.5: {"depths": [16], "type": "MD-Front Load", "squares": "168", "build": "3-4/2/2"}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Set initial lot specifications to conventional
|
| 49 |
+
self.lot_specifications = self.conventional_lot_specifications
|
| 50 |
+
|
| 51 |
self.slhc_widths = [8.5, 10.5]
|
| 52 |
self.standard_widths = [12.5, 14.0]
|
| 53 |
self.premium_widths = [16.0, 18.0]
|
| 54 |
self.corner_specific = [11.0, 13.3, 14.8, 16.8]
|
| 55 |
|
| 56 |
+
# Medium density categories
|
| 57 |
+
self.md_rear_widths = [4.5, 6.0, 7.5]
|
| 58 |
+
self.md_front_widths = [7.0, 8.0, 8.5, 10.5]
|
| 59 |
+
|
| 60 |
# Define corner_widths as all widths suitable for corners
|
| 61 |
self.corner_widths = self.corner_specific + [14.0, 16.0, 18.0]
|
| 62 |
|
| 63 |
# Enhanced color palette with RPM brand colors
|
| 64 |
self.color_schemes = {
|
| 65 |
'rpm_primary': {
|
| 66 |
+
# Conventional colors
|
| 67 |
8.5: '#802B2B', # Burgundy for SLHC
|
| 68 |
10.5: '#AB3838', # Burgundy 75%
|
| 69 |
12.5: '#216767', # Teal
|
|
|
|
| 73 |
11.0: '#4F8585', # Teal 75%
|
| 74 |
13.3: '#545D51', # RPM Green 75%
|
| 75 |
14.8: '#697687', # Blue 75%
|
| 76 |
+
16.8: '#FFCF6D', # Yellow 75%
|
| 77 |
+
# Medium Density colors
|
| 78 |
+
4.5: '#6B4C8A', # Purple for MD
|
| 79 |
+
6.0: '#8A6BB3', # Purple 75%
|
| 80 |
+
7.5: '#9F85C7', # Purple 50%
|
| 81 |
+
7.0: '#4A7C7E', # Teal-Blue for MD Front
|
| 82 |
+
8.0: '#5A9A9C' # Teal-Blue 75%
|
| 83 |
},
|
| 84 |
'rpm_contrast': {
|
| 85 |
+
# Conventional colors
|
| 86 |
8.5: '#D69C9C', # Burgundy 50%
|
| 87 |
10.5: '#E2C1B7', # Burgundy 25%
|
| 88 |
12.5: '#95B5B5', # Teal 50%
|
|
|
|
| 92 |
11.0: '#D6E3E3', # Teal 25%
|
| 93 |
13.3: '#B6B8B2', # RPM Green 25%
|
| 94 |
14.8: '#CCD7E4', # Blue 25%
|
| 95 |
+
16.8: '#FFEFCE', # Yellow 25%
|
| 96 |
+
# Medium Density colors
|
| 97 |
+
4.5: '#B5A6C5', # Purple 50%
|
| 98 |
+
6.0: '#C7BDD6', # Purple 25%
|
| 99 |
+
7.5: '#DDD6E8', # Purple 15%
|
| 100 |
+
7.0: '#8FB8BA', # Teal-Blue 50%
|
| 101 |
+
8.0: '#B3D0D2' # Teal-Blue 25%
|
| 102 |
},
|
| 103 |
'rpm_monochrome': {
|
| 104 |
+
# All widths use grayscale
|
| 105 |
8.5: '#2E3E2F', # RPM Green 100%
|
| 106 |
10.5: '#545D51', # RPM Green 75%
|
| 107 |
12.5: '#80857B', # RPM Green 50%
|
|
|
|
| 111 |
11.0: '#D1D3D4', # Black 25%
|
| 112 |
13.3: '#216767', # Teal (accent)
|
| 113 |
14.8: '#415B6E', # Blue (accent)
|
| 114 |
+
16.8: '#FF8E3C', # Yellow (accent)
|
| 115 |
+
# Medium Density
|
| 116 |
+
4.5: '#4A4B4D', # Dark gray
|
| 117 |
+
6.0: '#6B6C6E', # Medium gray
|
| 118 |
+
7.5: '#8C8D8F', # Light gray
|
| 119 |
+
7.0: '#5C5D5F', # Gray
|
| 120 |
+
8.0: '#7D7E80' # Light gray
|
| 121 |
}
|
| 122 |
}
|
| 123 |
|
| 124 |
self.current_scheme = 'rpm_primary'
|
| 125 |
self.current_solution = None # Store current AI solution
|
| 126 |
+
self.development_mode = 'conventional' # conventional or medium_density
|
| 127 |
+
self.md_load_type = 'front' # front or rear
|
| 128 |
+
|
| 129 |
+
def set_development_mode(self, mode, load_type=None):
|
| 130 |
+
"""Set the development mode and update lot specifications"""
|
| 131 |
+
self.development_mode = mode
|
| 132 |
+
if mode == 'medium_density':
|
| 133 |
+
if load_type == 'rear':
|
| 134 |
+
self.lot_specifications = self.md_rear_loaded_specifications
|
| 135 |
+
self.md_load_type = 'rear'
|
| 136 |
+
else:
|
| 137 |
+
self.lot_specifications = self.md_front_loaded_specifications
|
| 138 |
+
self.md_load_type = 'front'
|
| 139 |
+
else:
|
| 140 |
+
self.lot_specifications = self.conventional_lot_specifications
|
| 141 |
+
self.md_load_type = None
|
| 142 |
|
| 143 |
def create_enhanced_visualization(self, solution, stage_width, stage_depth=32, title="Premium Grid Layout", show_variance=None):
|
| 144 |
+
"""Create a clean 2D visualization with corner splays and optional laneway"""
|
| 145 |
+
# Adjust figure size for laneway if needed
|
| 146 |
+
fig_height = 14 if self.md_load_type == 'rear' else 12
|
| 147 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(18, fig_height), gridspec_kw={'height_ratios': [3, 1]},
|
| 148 |
facecolor='#2E3E2F')
|
| 149 |
|
| 150 |
# Main visualization
|
|
|
|
| 155 |
|
| 156 |
# Set up main plot with RPM green background
|
| 157 |
ax1.set_xlim(-5, stage_width + 5)
|
| 158 |
+
# Adjust y-limits for rear laneway
|
| 159 |
+
if self.md_load_type == 'rear':
|
| 160 |
+
ax1.set_ylim(-10, 60) # Extended for laneway
|
| 161 |
+
else:
|
| 162 |
+
ax1.set_ylim(-10, 50)
|
| 163 |
ax1.set_facecolor('#2E3E2F')
|
| 164 |
|
| 165 |
# Add title with variance if provided
|
| 166 |
if show_variance is not None:
|
| 167 |
variance_color = '#216767' if abs(show_variance) < 0.001 else '#802B2B'
|
| 168 |
+
mode_text = "MD " if self.development_mode == 'medium_density' else ""
|
| 169 |
+
load_text = f"({self.md_load_type.title()} Loaded) " if self.md_load_type else ""
|
| 170 |
+
title_text = f"{mode_text}{load_text}{title}\nGrid Variance: {show_variance:+.1f}m"
|
| 171 |
ax1.set_title(title_text, fontsize=28, fontweight='bold', pad=25, color='white')
|
| 172 |
else:
|
| 173 |
ax1.set_title(title, fontsize=28, fontweight='bold', pad=25, color='white')
|
| 174 |
|
| 175 |
# Add subtle gradient background
|
| 176 |
gradient = np.linspace(0.3, 0.1, 100).reshape(1, -1)
|
| 177 |
+
y_max = 60 if self.md_load_type == 'rear' else 50
|
| 178 |
+
ax1.imshow(gradient, extent=[-5, stage_width + 5, -10, y_max], aspect='auto',
|
| 179 |
cmap='Greys', alpha=0.3, zorder=0)
|
| 180 |
|
| 181 |
# Add street with label
|
|
|
|
| 186 |
ax1.text(stage_width/2, -2, 'STREET', ha='center', va='center',
|
| 187 |
fontsize=20, color='white', fontweight='bold')
|
| 188 |
|
| 189 |
+
# Draw lots with corner splays
|
| 190 |
splay_size = 3 # 3m corner splay
|
| 191 |
+
|
| 192 |
+
# Get appropriate depth for current mode
|
| 193 |
+
if self.development_mode == 'medium_density':
|
| 194 |
+
# Use first available depth for MD lots
|
| 195 |
+
lot_height = 28 # Default
|
| 196 |
+
for width, _ in solution:
|
| 197 |
+
if width in self.lot_specifications:
|
| 198 |
+
lot_height = self.lot_specifications[width]['depths'][0]
|
| 199 |
+
break
|
| 200 |
+
else:
|
| 201 |
+
lot_height = 28 # Standard height for conventional
|
| 202 |
+
|
| 203 |
+
# Add rear laneway if rear loaded MD
|
| 204 |
+
if self.md_load_type == 'rear':
|
| 205 |
+
laneway_y = 8 + lot_height
|
| 206 |
+
laneway = Rectangle((-5, laneway_y), stage_width + 10, 7,
|
| 207 |
+
facecolor='#3A3A3A', alpha=0.9, zorder=1,
|
| 208 |
+
edgecolor='#FFCF6D', linewidth=2, linestyle='--')
|
| 209 |
+
ax1.add_patch(laneway)
|
| 210 |
+
ax1.text(stage_width/2, laneway_y + 3.5, 'REAR LANEWAY (7m)',
|
| 211 |
+
ha='center', va='center', fontsize=16, color='#FFCF6D',
|
| 212 |
+
fontweight='bold', alpha=0.9)
|
| 213 |
|
| 214 |
for i, (width, lot_type) in enumerate(solution):
|
| 215 |
# Get base color
|
| 216 |
if width in colors:
|
| 217 |
base_color = colors[width]
|
| 218 |
else:
|
| 219 |
+
# Use MD colors if in MD mode
|
| 220 |
+
if self.development_mode == 'medium_density':
|
| 221 |
+
if width <= 6.0:
|
| 222 |
+
base_color = colors.get(4.5, '#6B4C8A')
|
| 223 |
+
else:
|
| 224 |
+
base_color = colors.get(7.0, '#4A7C7E')
|
| 225 |
+
else:
|
| 226 |
+
closest_width = min(colors.keys(), key=lambda x: abs(x - width))
|
| 227 |
+
base_color = colors[closest_width]
|
| 228 |
|
| 229 |
# Check position
|
| 230 |
is_corner = (i == 0 or i == len(solution) - 1)
|
|
|
|
| 234 |
edge_color = 'white'
|
| 235 |
linewidth = 4.0 if is_corner else 3.0
|
| 236 |
|
| 237 |
+
# Create lot shape with appropriate height
|
| 238 |
+
if is_corner and self.development_mode == 'conventional':
|
| 239 |
+
# Corner lot with splay for conventional only
|
| 240 |
if i == 0: # First corner
|
| 241 |
vertices = [
|
| 242 |
(x_pos + splay_size, 8), # Start after splay
|
| 243 |
(x_pos + width, 8),
|
| 244 |
+
(x_pos + width, 8 + lot_height),
|
| 245 |
(x_pos, 8 + lot_height), # Straight rear
|
| 246 |
(x_pos, 8 + splay_size) # Splay corner
|
| 247 |
]
|
|
|
|
| 250 |
(x_pos, 8),
|
| 251 |
(x_pos + width - splay_size, 8),
|
| 252 |
(x_pos + width, 8 + splay_size), # Splay corner
|
| 253 |
+
(x_pos + width, 8 + lot_height),
|
| 254 |
(x_pos, 8 + lot_height)
|
| 255 |
]
|
| 256 |
|
|
|
|
| 270 |
ax1.plot([x_pos + width - splay_size, x_pos + width],
|
| 271 |
[8, 8 + splay_size], 'white', linewidth=2, alpha=0.8)
|
| 272 |
else:
|
| 273 |
+
# Regular lot (or MD corner without splay)
|
| 274 |
lot = FancyBboxPatch((x_pos, 8), width, lot_height,
|
| 275 |
boxstyle="round,pad=0.1",
|
| 276 |
facecolor=face_color,
|
|
|
|
| 279 |
zorder=3)
|
| 280 |
ax1.add_patch(lot)
|
| 281 |
|
| 282 |
+
# Add subtle glow
|
| 283 |
glow = FancyBboxPatch((x_pos - 0.2, 7.8), width + 0.4, lot_height + 0.4,
|
| 284 |
boxstyle="round,pad=0.15",
|
| 285 |
facecolor='none',
|
|
|
|
| 289 |
zorder=2)
|
| 290 |
ax1.add_patch(glow)
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
# Add lot information (positioned consistently)
|
| 293 |
+
info_y_base = 48 if self.md_load_type == 'rear' else 40
|
| 294 |
+
|
| 295 |
+
ax1.text(x_pos + width/2, info_y_base, f'L{lot_num}',
|
| 296 |
ha='center', va='center', fontsize=16, fontweight='bold', color='white')
|
| 297 |
|
| 298 |
+
ax1.text(x_pos + width/2, info_y_base - 5, f'{width:.1f}m',
|
| 299 |
ha='center', va='center', fontsize=14, fontweight='bold', color='white')
|
| 300 |
|
| 301 |
# Lot type
|
|
|
|
| 310 |
spec = {**spec, 'type': 'Custom'}
|
| 311 |
|
| 312 |
lot_type_text = spec['type']
|
| 313 |
+
if is_corner and self.development_mode == 'conventional':
|
| 314 |
lot_type_text = "CORNER"
|
| 315 |
|
| 316 |
+
# Add build type for MD
|
| 317 |
+
if self.development_mode == 'medium_density' and 'build' in spec:
|
| 318 |
+
lot_type_text += f"\n{spec['build']}"
|
| 319 |
+
|
| 320 |
+
ax1.text(x_pos + width/2, info_y_base - 17, lot_type_text,
|
| 321 |
ha='center', va='center', fontsize=11,
|
| 322 |
bbox=dict(boxstyle="round,pad=0.3", facecolor='#545D51',
|
| 323 |
edgecolor='white', alpha=0.9), color='white')
|
|
|
|
| 327 |
ax1.plot([x_pos, x_pos], [10, 14], 'w-', linewidth=1, alpha=0.3)
|
| 328 |
ax1.plot([x_pos + width, x_pos + width], [10, 14], 'w-', linewidth=1, alpha=0.3)
|
| 329 |
|
| 330 |
+
# Add garage indicators for rear loaded
|
| 331 |
+
if self.md_load_type == 'rear':
|
| 332 |
+
# Small garage icon at rear
|
| 333 |
+
garage_y = 8 + lot_height - 6
|
| 334 |
+
garage = Rectangle((x_pos + width/2 - 1.5, garage_y), 3, 5,
|
| 335 |
+
facecolor='#636466', edgecolor='white',
|
| 336 |
+
linewidth=1, alpha=0.8, zorder=4)
|
| 337 |
+
ax1.add_patch(garage)
|
| 338 |
+
|
| 339 |
x_pos += width
|
| 340 |
lot_num += 1
|
| 341 |
|
| 342 |
# Add rear alignment line across all lots
|
| 343 |
+
rear_y = 8 + lot_height
|
| 344 |
+
if self.md_load_type != 'rear': # Don't show if laneway present
|
| 345 |
+
ax1.plot([0, stage_width], [rear_y, rear_y],
|
| 346 |
+
'#216767', linewidth=2, alpha=0.8, linestyle='-')
|
| 347 |
+
ax1.text(stage_width/2, rear_y + 1, 'REAR ALIGNMENT LINE',
|
| 348 |
+
ha='center', va='bottom', fontsize=12, color='#216767', alpha=0.8,
|
| 349 |
+
bbox=dict(boxstyle="round,pad=0.3", facecolor='#2E3E2F',
|
| 350 |
+
edgecolor='#216767', alpha=0.8))
|
| 351 |
|
| 352 |
# Add stage dimensions
|
| 353 |
arrow_props = dict(arrowstyle='<->', color='white', lw=3)
|
|
|
|
| 370 |
unique_widths = len(set(w for w, _ in solution))
|
| 371 |
diversity_score = unique_widths / len(set(self.lot_specifications.keys()))
|
| 372 |
|
| 373 |
+
if self.development_mode == 'conventional':
|
| 374 |
+
slhc_count = sum(1 for w, _ in solution if w <= 10.5)
|
| 375 |
+
standard_count = sum(1 for w, _ in solution if 10.5 < w <= 14)
|
| 376 |
+
premium_count = sum(1 for w, _ in solution if w > 14)
|
| 377 |
+
|
| 378 |
+
# SLHC pairs
|
| 379 |
+
slhc_pairs = 0
|
| 380 |
+
for i in range(len(solution) - 1):
|
| 381 |
+
if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5:
|
| 382 |
+
slhc_pairs += 1
|
| 383 |
+
else:
|
| 384 |
+
# MD metrics
|
| 385 |
+
narrow_count = sum(1 for w, _ in solution if w <= 6.0)
|
| 386 |
+
standard_count = sum(1 for w, _ in solution if 6.0 < w <= 8.0)
|
| 387 |
+
wide_count = sum(1 for w, _ in solution if w > 8.0)
|
| 388 |
+
slhc_pairs = 0 # Not applicable for MD
|
| 389 |
|
| 390 |
# Calculate actual total width and variance
|
| 391 |
total_width = sum(w for w, _ in solution)
|
| 392 |
variance = total_width - stage_width
|
| 393 |
efficiency = "100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"
|
| 394 |
|
| 395 |
+
# Calculate yield
|
| 396 |
+
if self.development_mode == 'medium_density':
|
| 397 |
+
# Assume potential for duplex on lots ≥ 7m
|
| 398 |
+
potential_dwellings = sum(2 if w >= 7.0 else 1 for w, _ in solution)
|
| 399 |
+
yield_text = f"🏘️ Potential Dwellings: {potential_dwellings}"
|
| 400 |
+
else:
|
| 401 |
+
yield_text = f"💰 Revenue: ${total_lots * 0.5:.1f}M - ${total_lots * 1.2:.1f}M"
|
| 402 |
+
|
| 403 |
metrics_lines = [
|
| 404 |
f"📊 TOTAL LOTS: {total_lots}",
|
| 405 |
f"📐 LAND EFFICIENCY: {efficiency}",
|
| 406 |
f"🎯 DIVERSITY: {diversity_score:.0%} ({unique_widths} types)",
|
| 407 |
f"📏 GRID VARIANCE: {variance:+.2f}m",
|
| 408 |
"",
|
| 409 |
+
f"{'Narrow (≤6m)' if self.development_mode == 'medium_density' else 'SLHC (≤10.5m)'}: {narrow_count if self.development_mode == 'medium_density' else slhc_count} lots",
|
| 410 |
+
f"{'Standard (6-8m)' if self.development_mode == 'medium_density' else 'Standard (11-14m)'}: {standard_count} lots",
|
| 411 |
+
f"{'Wide (>8m)' if self.development_mode == 'medium_density' else 'Premium (>14m)'}: {wide_count if self.development_mode == 'medium_density' else premium_count} lots",
|
| 412 |
"",
|
| 413 |
+
f"{'🚗 Access: ' + ('Rear Laneway' if self.md_load_type == 'rear' else 'Front Loaded') if self.development_mode == 'medium_density' else f'🚗 SLHC Pairs: {slhc_pairs}'}",
|
| 414 |
+
yield_text
|
| 415 |
]
|
| 416 |
|
| 417 |
col1_text = '\n'.join(metrics_lines[:5])
|
|
|
|
| 480 |
feedback = f"⚠️ Grid is {-variance:.2f}m too narrow. Add {-variance:.2f}m total width."
|
| 481 |
|
| 482 |
# Add suggestions if not perfect
|
| 483 |
+
min_width = 4.5 if self.development_mode == 'medium_density' else 8.5
|
| 484 |
+
|
| 485 |
if abs(variance) > 0.001:
|
| 486 |
if variance > 0:
|
| 487 |
# Suggest which lots could be reduced
|
| 488 |
suggestions = []
|
| 489 |
for i, w in enumerate(widths):
|
| 490 |
+
if w - variance >= min_width: # Minimum viable width
|
| 491 |
suggestions.append(f"L{i+1}: reduce from {w:.1f}m to {w-variance:.1f}m")
|
| 492 |
if suggestions:
|
| 493 |
feedback += f"\n\nSuggestions:\n" + "\n".join(suggestions[:3])
|
|
|
|
| 505 |
return ""
|
| 506 |
return ", ".join([f"{w:.1f}" for w, _ in solution])
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
def find_optimal_custom_corners(self, stage_width, internal_widths, base_corner_width, tolerance=0.5):
|
| 509 |
"""Find optimal corner widths that can vary slightly from base width"""
|
| 510 |
best_solution = None
|
| 511 |
best_fitness = -float('inf')
|
| 512 |
|
| 513 |
+
# For MD, corners don't need to be wider than internal
|
| 514 |
+
if self.development_mode == 'medium_density':
|
| 515 |
+
min_internal = min(internal_widths) if internal_widths else 4.5
|
| 516 |
+
min_corner_width = base_corner_width - tolerance
|
| 517 |
+
else:
|
| 518 |
+
# Ensure corners are at least as wide as smallest internal lot
|
| 519 |
+
min_internal = min(internal_widths) if internal_widths else 8.5
|
| 520 |
+
min_corner_width = max(base_corner_width - tolerance, min_internal)
|
| 521 |
|
| 522 |
# Try variations of corner widths within tolerance
|
| 523 |
variations = np.arange(min_corner_width,
|
|
|
|
| 535 |
internal_solution = self.find_exact_solution_with_diversity(internal_width, internal_widths)
|
| 536 |
|
| 537 |
if internal_solution:
|
| 538 |
+
# For conventional, verify no internal lot is wider than corners
|
| 539 |
+
if self.development_mode == 'conventional':
|
| 540 |
+
max_internal = max(internal_solution) if internal_solution else 0
|
| 541 |
+
if max_internal > min(corner1, corner2):
|
| 542 |
+
continue
|
| 543 |
|
| 544 |
# Build complete solution
|
| 545 |
solution = [(round(corner1, 1), 'corner')]
|
|
|
|
| 574 |
|
| 575 |
# Strategy 2: Try flexible corners if enabled
|
| 576 |
if allow_custom_corners and standard_internal:
|
| 577 |
+
# For MD, try all widths as potential corners
|
| 578 |
+
if self.development_mode == 'medium_density':
|
| 579 |
+
corner_bases = list(enabled_widths)
|
| 580 |
+
else:
|
| 581 |
+
# For conventional, use traditional corner widths
|
| 582 |
+
corner_bases = [11.0, 13.3, 14.8, 16.8, 14.0, 16.0]
|
| 583 |
+
|
| 584 |
+
for base_width in corner_bases:
|
| 585 |
if any(abs(w - base_width) < 2 for w in enabled_widths):
|
| 586 |
custom_solution = self.find_optimal_custom_corners(
|
| 587 |
stage_width, standard_internal, base_width, tolerance=0.5
|
|
|
|
| 599 |
|
| 600 |
# Separate widths by size
|
| 601 |
all_widths = sorted(enabled_widths)
|
|
|
|
| 602 |
|
| 603 |
+
if self.development_mode == 'medium_density':
|
| 604 |
+
# For MD, any width can be a corner
|
| 605 |
+
corner_options = all_widths
|
| 606 |
+
min_internal_width = min(all_widths) if all_widths else 4.5
|
| 607 |
+
else:
|
| 608 |
+
# For conventional, maintain hierarchy
|
| 609 |
+
min_internal_width = min(all_widths)
|
| 610 |
+
corner_options = [w for w in enabled_widths if w >= max(11.0, min_internal_width)]
|
| 611 |
|
| 612 |
best_solution = None
|
| 613 |
best_fitness = -float('inf')
|
|
|
|
| 629 |
)
|
| 630 |
|
| 631 |
for internal_widths in internal_solutions:
|
| 632 |
+
# For conventional, verify no internal lot is wider than corners
|
| 633 |
+
if self.development_mode == 'conventional':
|
| 634 |
+
max_internal = max(internal_widths) if internal_widths else 0
|
| 635 |
+
if max_internal > min(corner1, corner2):
|
| 636 |
+
continue
|
| 637 |
|
| 638 |
# Build complete solution
|
| 639 |
solution = [(corner1, 'corner')]
|
|
|
|
| 641 |
solution.append((corner2, 'corner'))
|
| 642 |
|
| 643 |
# Optimize arrangement
|
| 644 |
+
optimized = self.optimize_lot_grouping(solution)
|
| 645 |
fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
|
| 646 |
|
| 647 |
if fitness > best_fitness:
|
| 648 |
best_fitness = fitness
|
| 649 |
best_solution = optimized
|
| 650 |
|
| 651 |
+
# If no good solution, try without strict corner rules
|
| 652 |
if not best_solution:
|
| 653 |
all_solutions = []
|
| 654 |
self.find_all_combinations_recursive(stage_width, sorted(enabled_widths),
|
| 655 |
[], all_solutions, 20)
|
| 656 |
|
| 657 |
for widths in all_solutions[:50]:
|
| 658 |
+
# For MD, just use the solution as-is
|
| 659 |
+
if self.development_mode == 'medium_density':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
solution = [(w, 'standard') for w in widths]
|
| 661 |
+
else:
|
| 662 |
+
# For conventional, ensure corners are among the largest lots
|
| 663 |
+
sorted_widths = sorted(widths)
|
| 664 |
+
if len(sorted_widths) >= 2:
|
| 665 |
+
solution = [(sorted_widths[-1], 'corner')]
|
| 666 |
+
solution.extend([(w, 'standard') for w in sorted_widths[:-2]])
|
| 667 |
+
solution.append((sorted_widths[-2], 'corner'))
|
| 668 |
+
else:
|
| 669 |
+
solution = [(w, 'standard') for w in widths]
|
| 670 |
|
| 671 |
+
optimized = self.optimize_lot_grouping(solution)
|
| 672 |
fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
|
| 673 |
|
| 674 |
if fitness > best_fitness:
|
|
|
|
| 677 |
|
| 678 |
return best_solution
|
| 679 |
|
| 680 |
+
def optimize_lot_grouping(self, lots):
|
| 681 |
+
"""Optimize lot arrangement based on development mode"""
|
| 682 |
+
if self.development_mode == 'medium_density':
|
| 683 |
+
return self.optimize_md_grouping(lots)
|
| 684 |
+
else:
|
| 685 |
+
return self.optimize_slhc_grouping(lots)
|
| 686 |
+
|
| 687 |
+
def optimize_md_grouping(self, lots):
|
| 688 |
+
"""Optimize lot arrangement for medium density"""
|
| 689 |
+
if not lots or len(lots) <= 1:
|
| 690 |
+
return lots
|
| 691 |
+
|
| 692 |
+
# Separate lots by width
|
| 693 |
+
narrow_lots = [] # 4.5-6m
|
| 694 |
+
medium_lots = [] # 7-8m
|
| 695 |
+
wide_lots = [] # >8m
|
| 696 |
+
|
| 697 |
+
for width, lot_type in lots:
|
| 698 |
+
if width <= 6.0:
|
| 699 |
+
narrow_lots.append((width, lot_type))
|
| 700 |
+
elif width <= 8.0:
|
| 701 |
+
medium_lots.append((width, lot_type))
|
| 702 |
+
else:
|
| 703 |
+
wide_lots.append((width, lot_type))
|
| 704 |
+
|
| 705 |
+
# Build optimized layout
|
| 706 |
+
optimized = []
|
| 707 |
+
|
| 708 |
+
# For rear loaded, group similar widths for efficient laneway access
|
| 709 |
+
if self.md_load_type == 'rear':
|
| 710 |
+
# Group narrow lots together
|
| 711 |
+
optimized.extend(narrow_lots)
|
| 712 |
+
optimized.extend(medium_lots)
|
| 713 |
+
optimized.extend(wide_lots)
|
| 714 |
+
else:
|
| 715 |
+
# For front loaded, alternate sizes for variety
|
| 716 |
+
while narrow_lots or medium_lots or wide_lots:
|
| 717 |
+
if wide_lots:
|
| 718 |
+
optimized.append(wide_lots.pop(0))
|
| 719 |
+
if narrow_lots:
|
| 720 |
+
optimized.append(narrow_lots.pop(0))
|
| 721 |
+
if medium_lots:
|
| 722 |
+
optimized.append(medium_lots.pop(0))
|
| 723 |
+
|
| 724 |
+
return optimized
|
| 725 |
+
|
| 726 |
def find_diverse_combinations(self, target_width, available_widths, max_solutions=20):
|
| 727 |
"""Find combinations that maximize diversity"""
|
| 728 |
all_solutions = []
|
|
|
|
| 829 |
current.pop()
|
| 830 |
|
| 831 |
def optimize_slhc_grouping(self, lots):
|
| 832 |
+
"""Optimize lot arrangement with sophisticated rules for conventional"""
|
| 833 |
if not lots or len(lots) <= 1:
|
| 834 |
return lots
|
| 835 |
|
|
|
|
| 963 |
fitness -= max_repetition * 500 # Penalty for too many of same width
|
| 964 |
fitness += diversity_ratio * 3000 # Bonus for good diversity ratio
|
| 965 |
|
| 966 |
+
# Mode-specific evaluation
|
| 967 |
+
if self.development_mode == 'conventional':
|
| 968 |
+
# Corner evaluation for conventional
|
| 969 |
+
if len(solution) >= 2:
|
| 970 |
+
first_width = solution[0][0]
|
| 971 |
+
last_width = solution[-1][0]
|
| 972 |
+
|
| 973 |
+
# Penalty for SLHC on corners
|
| 974 |
+
if first_width <= 10.5:
|
| 975 |
+
fitness -= 2000
|
| 976 |
+
if last_width <= 10.5:
|
| 977 |
+
fitness -= 2000
|
| 978 |
+
|
| 979 |
+
# Bonus for good corners
|
| 980 |
+
if first_width >= 11.0:
|
| 981 |
+
fitness += 1000
|
| 982 |
+
if last_width >= 11.0:
|
| 983 |
+
fitness += 1000
|
| 984 |
+
|
| 985 |
+
# Balance bonus
|
| 986 |
+
corner_diff = abs(first_width - last_width)
|
| 987 |
+
if corner_diff < 0.1:
|
| 988 |
+
fitness += 1500 # Perfect match
|
| 989 |
+
elif corner_diff <= 1.0:
|
| 990 |
+
fitness += 1000 # Very good
|
| 991 |
+
elif corner_diff <= 2.0:
|
| 992 |
+
fitness += 500 # Good
|
| 993 |
+
else:
|
| 994 |
+
fitness -= 500 # Poor balance
|
| 995 |
|
| 996 |
+
# SLHC grouping bonus
|
| 997 |
+
for i in range(len(solution) - 1):
|
| 998 |
+
if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5:
|
| 999 |
+
fitness += 300 # Adjacent SLHC bonus
|
|
|
|
| 1000 |
|
| 1001 |
+
# Penalize corner-specific widths used internally
|
| 1002 |
+
for i in range(1, len(solution) - 1):
|
| 1003 |
+
if solution[i][0] in self.corner_specific:
|
| 1004 |
+
fitness -= 200
|
| 1005 |
+
else:
|
| 1006 |
+
# MD-specific bonuses
|
| 1007 |
+
if self.md_load_type == 'rear':
|
| 1008 |
+
# Bonus for grouping similar widths (efficient laneway access)
|
| 1009 |
+
for i in range(len(solution) - 1):
|
| 1010 |
+
if abs(solution[i][0] - solution[i+1][0]) < 1.5:
|
| 1011 |
+
fitness += 200
|
| 1012 |
|
| 1013 |
+
# Bonus for potential duplex lots (≥7m)
|
| 1014 |
+
duplex_count = sum(1 for w, _ in solution if w >= 7.0)
|
| 1015 |
+
fitness += duplex_count * 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
|
| 1017 |
return fitness
|
| 1018 |
|
|
|
|
| 1037 |
total_width = sum(w for w, _ in solution)
|
| 1038 |
variance = total_width - stage_width
|
| 1039 |
|
| 1040 |
+
# Mode-specific title
|
| 1041 |
+
mode_text = "MEDIUM DENSITY " if self.development_mode == 'medium_density' else ""
|
| 1042 |
+
load_text = f"({self.md_load_type.upper()} LOADED) " if self.md_load_type else ""
|
| 1043 |
+
|
| 1044 |
report = f"""
|
| 1045 |
+
# {mode_text}{load_text}SUBDIVISION OPTIMIZATION REPORT
|
| 1046 |
## Project Analysis for {stage_width}m × {stage_depth}m Stage
|
| 1047 |
|
| 1048 |
### EXECUTIVE SUMMARY
|
| 1049 |
+
- **Development Type**: {self.development_mode.replace('_', ' ').title()}
|
| 1050 |
- **Total Lots**: {len(solution)}
|
| 1051 |
- **Unique Lot Types**: {unique_widths}
|
| 1052 |
- **Land Efficiency**: {"100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"}
|
|
|
|
| 1054 |
- **Stage Dimensions**: {stage_width}m × {stage_depth}m
|
| 1055 |
- **Total Area**: {stage_width * stage_depth}m²
|
| 1056 |
{f"- **Custom Widths Used**: {', '.join(custom_widths)}" if custom_widths else ""}
|
|
|
|
|
|
|
| 1057 |
"""
|
| 1058 |
|
| 1059 |
+
# Add MD-specific info
|
| 1060 |
+
if self.development_mode == 'medium_density':
|
| 1061 |
+
potential_dwellings = sum(2 if w >= 7.0 else 1 for w, _ in solution)
|
| 1062 |
+
density = potential_dwellings / (stage_width * stage_depth / 10000) # per hectare
|
| 1063 |
+
report += f"- **Potential Dwellings**: {potential_dwellings} ({density:.0f} dwellings/ha)\n"
|
| 1064 |
+
report += f"- **Access Type**: {'Rear Laneway (7m)' if self.md_load_type == 'rear' else 'Front Loaded'}\n"
|
| 1065 |
+
|
| 1066 |
+
report += f"\n### LOT DIVERSITY ANALYSIS\n"
|
| 1067 |
+
|
| 1068 |
# Sort by count to show distribution
|
| 1069 |
sorted_widths = sorted(width_counts.items(), key=lambda x: x[1], reverse=True)
|
| 1070 |
for width, count in sorted_widths:
|
| 1071 |
percentage = (count / len(solution)) * 100
|
| 1072 |
if width in self.lot_specifications:
|
| 1073 |
spec = self.lot_specifications[width]
|
| 1074 |
+
build_info = f" [{spec.get('build', 'N/A')}]" if 'build' in spec else ""
|
| 1075 |
+
report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): {spec['type']}{build_info}\n"
|
| 1076 |
else:
|
| 1077 |
report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): Custom Width\n"
|
| 1078 |
|
| 1079 |
+
# Corner analysis (only for conventional)
|
| 1080 |
+
if self.development_mode == 'conventional' and len(solution) >= 2:
|
| 1081 |
report += f"\n### CORNER ANALYSIS\n"
|
| 1082 |
report += f"- **Front Corner**: {solution[0][0]:.1f}m with 3m × 3m splay\n"
|
| 1083 |
report += f"- **Rear Corner**: {solution[-1][0]:.1f}m with 3m × 3m splay\n"
|
| 1084 |
report += f"- **Balance**: {abs(solution[0][0] - solution[-1][0]):.1f}m difference\n"
|
| 1085 |
|
| 1086 |
report += f"\n### DESIGN FEATURES\n"
|
| 1087 |
+
if self.development_mode == 'medium_density':
|
| 1088 |
+
if self.md_load_type == 'rear':
|
| 1089 |
+
report += f"- 7m rear laneway provides vehicle access and services\n"
|
| 1090 |
+
report += f"- Garages positioned at rear for better street presentation\n"
|
| 1091 |
+
else:
|
| 1092 |
+
report += f"- Front loaded design with integrated garages\n"
|
| 1093 |
+
report += f"- Compact lots maximize dwelling yield\n"
|
| 1094 |
+
report += f"- Potential for duplex/triplex on wider lots (≥7m)\n"
|
| 1095 |
+
else:
|
| 1096 |
+
report += f"- Corner splays provide safe sight lines at intersections\n"
|
| 1097 |
+
report += f"- All lots have identical rear alignment for visual consistency\n"
|
| 1098 |
+
report += f"- Diverse lot mix ensures varied streetscape\n"
|
| 1099 |
+
report += f"- SLHC lots grouped for efficient garbage collection\n"
|
| 1100 |
|
| 1101 |
report += f"\n---\n*Report generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"
|
| 1102 |
|
|
|
|
| 1115 |
def create_advanced_app():
|
| 1116 |
optimizer = AdvancedGridOptimizer()
|
| 1117 |
|
| 1118 |
+
def update_available_widths(development_mode, md_load_type):
|
| 1119 |
+
"""Update the available width options based on development mode"""
|
| 1120 |
+
if development_mode == "Medium Density":
|
| 1121 |
+
if md_load_type == "Rear Loaded":
|
| 1122 |
+
# Rear loaded MD widths
|
| 1123 |
+
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
|
| 1124 |
+
else:
|
| 1125 |
+
# Front loaded MD widths
|
| 1126 |
+
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
|
| 1127 |
+
else:
|
| 1128 |
+
# Conventional widths
|
| 1129 |
+
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
|
| 1130 |
+
|
| 1131 |
def optimize_grid(
|
| 1132 |
stage_width,
|
| 1133 |
stage_depth,
|
| 1134 |
+
development_mode,
|
| 1135 |
+
md_load_type,
|
| 1136 |
+
# Conventional widths
|
| 1137 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 1138 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
| 1139 |
+
# MD rear widths
|
| 1140 |
+
enable_4_5, enable_6_0, enable_7_5,
|
| 1141 |
+
# MD front widths
|
| 1142 |
+
enable_7_0, enable_8_0, enable_md_8_5, enable_md_10_5,
|
| 1143 |
allow_custom_corners, color_scheme
|
| 1144 |
):
|
| 1145 |
+
# Update optimizer mode
|
| 1146 |
+
if development_mode == "Medium Density":
|
| 1147 |
+
optimizer.set_development_mode('medium_density', 'rear' if md_load_type == "Rear Loaded" else 'front')
|
| 1148 |
+
else:
|
| 1149 |
+
optimizer.set_development_mode('conventional')
|
| 1150 |
+
|
| 1151 |
# Update color scheme
|
| 1152 |
optimizer.current_scheme = color_scheme
|
| 1153 |
|
| 1154 |
+
# Collect enabled widths based on mode
|
| 1155 |
enabled_widths = []
|
| 1156 |
+
|
| 1157 |
+
if development_mode == "Conventional Land":
|
| 1158 |
+
if enable_8_5: enabled_widths.append(8.5)
|
| 1159 |
+
if enable_10_5: enabled_widths.append(10.5)
|
| 1160 |
+
if enable_12_5: enabled_widths.append(12.5)
|
| 1161 |
+
if enable_14: enabled_widths.append(14.0)
|
| 1162 |
+
if enable_16: enabled_widths.append(16.0)
|
| 1163 |
+
if enable_18: enabled_widths.append(18.0)
|
| 1164 |
+
|
| 1165 |
+
if enable_corners:
|
| 1166 |
+
if enable_11: enabled_widths.append(11.0)
|
| 1167 |
+
if enable_13_3: enabled_widths.append(13.3)
|
| 1168 |
+
if enable_14_8: enabled_widths.append(14.8)
|
| 1169 |
+
if enable_16_8: enabled_widths.append(16.8)
|
| 1170 |
+
else:
|
| 1171 |
+
if md_load_type == "Rear Loaded":
|
| 1172 |
+
if enable_4_5: enabled_widths.append(4.5)
|
| 1173 |
+
if enable_6_0: enabled_widths.append(6.0)
|
| 1174 |
+
if enable_7_5: enabled_widths.append(7.5)
|
| 1175 |
+
else:
|
| 1176 |
+
if enable_7_0: enabled_widths.append(7.0)
|
| 1177 |
+
if enable_8_0: enabled_widths.append(8.0)
|
| 1178 |
+
if enable_md_8_5: enabled_widths.append(8.5)
|
| 1179 |
+
if enable_md_10_5: enabled_widths.append(10.5)
|
| 1180 |
|
| 1181 |
if not enabled_widths:
|
| 1182 |
return None, None, pd.DataFrame(), "Please select at least one lot width!", "", ""
|
|
|
|
| 1198 |
|
| 1199 |
# Verify solution
|
| 1200 |
if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001:
|
| 1201 |
+
# Provide mode-specific suggestions
|
| 1202 |
+
if development_mode == "Medium Density":
|
| 1203 |
+
width_suggestions = "4.5m, 6m, 7.5m" if md_load_type == "Rear Loaded" else "7m, 8m, 8.5m, 10.5m"
|
| 1204 |
+
stage_suggestions = "54m, 72m, 90m"
|
| 1205 |
+
else:
|
| 1206 |
+
width_suggestions = "8.5m-18m plus corner widths"
|
| 1207 |
+
stage_suggestions = "84m, 105m, 126m"
|
| 1208 |
+
|
| 1209 |
return None, pd.DataFrame(), f"""
|
| 1210 |
### ❌ Cannot achieve 100% usage with selected widths
|
| 1211 |
|
| 1212 |
**Stage Width**: {stage_width}m
|
| 1213 |
+
**Mode**: {development_mode} {f'({md_load_type})' if development_mode == 'Medium Density' else ''}
|
| 1214 |
**Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])}
|
| 1215 |
|
| 1216 |
**Try:**
|
| 1217 |
1. Enable more lot types for flexibility
|
| 1218 |
2. Enable "Custom Corners" option
|
| 1219 |
+
3. Try common stage widths: {stage_suggestions}
|
| 1220 |
+
4. Available widths: {width_suggestions}
|
| 1221 |
""", "", ""
|
| 1222 |
|
| 1223 |
# Create visualizations with variance indicator
|
| 1224 |
+
title = f"{'MD ' if development_mode == 'Medium Density' else ''}Grid Cut Optimization"
|
| 1225 |
fig_2d = optimizer.create_enhanced_visualization(
|
| 1226 |
optimized_solution, stage_width, stage_depth,
|
| 1227 |
+
title,
|
| 1228 |
show_variance=variance
|
| 1229 |
)
|
| 1230 |
|
|
|
|
| 1251 |
'count': 1,
|
| 1252 |
'type': spec.get('type', 'Custom'),
|
| 1253 |
'squares': spec.get('squares', 'N/A'),
|
| 1254 |
+
'area': width * stage_depth,
|
| 1255 |
+
'build': spec.get('build', 'N/A')
|
| 1256 |
}
|
| 1257 |
|
| 1258 |
results_data = []
|
| 1259 |
for width, info in sorted(width_counts.items()):
|
| 1260 |
+
row_data = {
|
| 1261 |
'Lot Width': width,
|
| 1262 |
'Count': info['count'],
|
| 1263 |
'Type': info['type'],
|
| 1264 |
'Area Each': f"{info['area']:.0f}m²",
|
| 1265 |
'Total Width': f"{float(width[:-1]) * info['count']:.1f}m",
|
| 1266 |
'Total Area': f"{info['area'] * info['count']:.0f}m²"
|
| 1267 |
+
}
|
| 1268 |
+
if development_mode == "Medium Density":
|
| 1269 |
+
row_data['Build Type'] = info['build']
|
| 1270 |
+
results_data.append(row_data)
|
| 1271 |
|
| 1272 |
results_df = pd.DataFrame(results_data)
|
| 1273 |
|
|
|
|
| 1278 |
total_lots = len(optimized_solution)
|
| 1279 |
unique_widths = len(set(w for w, _ in optimized_solution))
|
| 1280 |
|
| 1281 |
+
if development_mode == "Medium Density":
|
| 1282 |
+
# MD specific metrics
|
| 1283 |
+
potential_dwellings = sum(2 if w >= 7.0 else 1 for w, _ in optimized_solution)
|
| 1284 |
+
density = potential_dwellings / (stage_width * stage_depth / 10000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1285 |
|
| 1286 |
+
summary = f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1287 |
**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m²
|
| 1288 |
+
**Development**: {development_mode} ({md_load_type})
|
| 1289 |
**Total Lots**: {total_lots}
|
| 1290 |
+
**Potential Dwellings**: {potential_dwellings} ({density:.0f}/ha)
|
| 1291 |
**Unique Lot Types**: {unique_widths}
|
| 1292 |
**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"}
|
| 1293 |
+
"""
|
| 1294 |
+
else:
|
| 1295 |
+
# Conventional metrics
|
| 1296 |
+
slhc_pairs = sum(1 for i in range(len(optimized_solution) - 1)
|
| 1297 |
+
if optimized_solution[i][0] <= 10.5 and optimized_solution[i+1][0] <= 10.5)
|
| 1298 |
+
|
| 1299 |
+
summary = f"""
|
| 1300 |
+
**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m²
|
| 1301 |
+
**Total Lots**: {total_lots}
|
| 1302 |
+
**Unique Lot Types**: {unique_widths}
|
| 1303 |
+
**SLHC Pairs**: {slhc_pairs}
|
| 1304 |
+
**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"}
|
| 1305 |
"""
|
| 1306 |
|
| 1307 |
# Convert solution to string for manual editing
|
|
|
|
| 1309 |
|
| 1310 |
return fig_2d, results_df, summary, report, manual_edit_string
|
| 1311 |
|
| 1312 |
+
def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, development_mode, md_load_type, color_scheme):
|
| 1313 |
"""Update visualization based on manual adjustment"""
|
| 1314 |
+
# Set mode
|
| 1315 |
+
if development_mode == "Medium Density":
|
| 1316 |
+
optimizer.set_development_mode('medium_density', 'rear' if md_load_type == "Rear Loaded" else 'front')
|
| 1317 |
+
else:
|
| 1318 |
+
optimizer.set_development_mode('conventional')
|
| 1319 |
+
|
| 1320 |
optimizer.current_scheme = color_scheme
|
| 1321 |
|
| 1322 |
# Parse manual widths
|
|
|
|
| 1403 |
with gr.Row():
|
| 1404 |
with gr.Column(scale=1):
|
| 1405 |
with gr.Group():
|
| 1406 |
+
gr.Markdown("### 📐 Stage Configuration")
|
| 1407 |
+
|
| 1408 |
+
development_mode = gr.Radio(
|
| 1409 |
+
["Conventional Land", "Medium Density"],
|
| 1410 |
+
label="🏘️ Development Mode",
|
| 1411 |
+
value="Conventional Land",
|
| 1412 |
+
info="Select the type of development"
|
| 1413 |
+
)
|
| 1414 |
+
|
| 1415 |
+
md_load_type = gr.Radio(
|
| 1416 |
+
["Front Loaded", "Rear Loaded"],
|
| 1417 |
+
label="🚗 MD Access Type",
|
| 1418 |
+
value="Front Loaded",
|
| 1419 |
+
visible=False,
|
| 1420 |
+
info="Rear loaded includes 7m laneway"
|
| 1421 |
+
)
|
| 1422 |
+
|
| 1423 |
stage_width = gr.Number(
|
| 1424 |
label="Stage Width (m)",
|
| 1425 |
value=105.0,
|
|
|
|
| 1433 |
|
| 1434 |
gr.Markdown("### 📏 Lot Width Options")
|
| 1435 |
|
| 1436 |
+
# Conventional widths group
|
| 1437 |
+
with gr.Group(visible=True) as conventional_group:
|
| 1438 |
gr.Markdown("**Standard Widths**")
|
| 1439 |
with gr.Row():
|
| 1440 |
enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True)
|
|
|
|
| 1444 |
enable_14 = gr.Checkbox(label="14.0m", value=True)
|
| 1445 |
enable_16 = gr.Checkbox(label="16.0m", value=True)
|
| 1446 |
enable_18 = gr.Checkbox(label="18.0m", value=False)
|
| 1447 |
+
|
|
|
|
| 1448 |
enable_corners = gr.Checkbox(
|
| 1449 |
label="Enable Corner-Specific Widths",
|
| 1450 |
value=True,
|
|
|
|
| 1456 |
with gr.Row():
|
| 1457 |
enable_14_8 = gr.Checkbox(label="14.8m", value=True)
|
| 1458 |
enable_16_8 = gr.Checkbox(label="16.8m", value=True)
|
| 1459 |
+
|
| 1460 |
+
# MD Rear Loaded widths
|
| 1461 |
+
with gr.Group(visible=False) as md_rear_group:
|
| 1462 |
+
gr.Markdown("**MD Rear Loaded Widths**")
|
| 1463 |
+
enable_4_5 = gr.Checkbox(label="4.5m (2/2/1)", value=True)
|
| 1464 |
+
enable_6_0 = gr.Checkbox(label="6.0m (3/2/2)", value=True)
|
| 1465 |
+
enable_7_5 = gr.Checkbox(label="7.5m (3-4/2/2)", value=True)
|
| 1466 |
+
|
| 1467 |
+
# MD Front Loaded widths
|
| 1468 |
+
with gr.Group(visible=False) as md_front_group:
|
| 1469 |
+
gr.Markdown("**MD Front Loaded Widths**")
|
| 1470 |
+
enable_7_0 = gr.Checkbox(label="7.0m (3/2/1)", value=True)
|
| 1471 |
+
enable_8_0 = gr.Checkbox(label="8.0m (3-4/2/2)", value=True)
|
| 1472 |
+
enable_md_8_5 = gr.Checkbox(label="8.5m (3/2/1)", value=True)
|
| 1473 |
+
enable_md_10_5 = gr.Checkbox(label="10.5m (3-4/2/2)", value=True)
|
| 1474 |
|
| 1475 |
with gr.Column(scale=1):
|
| 1476 |
gr.Markdown("### ⚙️ Settings")
|
|
|
|
| 1497 |
|
| 1498 |
gr.Markdown("""
|
| 1499 |
### 💡 Quick Tips:
|
| 1500 |
+
- **Conventional**: Traditional lots with corner splays
|
| 1501 |
+
- **Medium Density**: Compact lots for higher yield
|
| 1502 |
+
- **Rear Loaded**: Includes 7m laneway visualization
|
| 1503 |
- **Grid Variance**: Shows if layout is perfect (0.0m)
|
|
|
|
|
|
|
| 1504 |
""")
|
| 1505 |
|
| 1506 |
with gr.Row():
|
| 1507 |
+
plot_2d = gr.Plot(label="2D Layout Visualization")
|
| 1508 |
|
| 1509 |
# Manual adjustment section
|
| 1510 |
gr.Markdown("### ✏️ Fine-Tune Result")
|
|
|
|
| 1532 |
with gr.Column():
|
| 1533 |
report_output = gr.Markdown(label="Professional Report")
|
| 1534 |
|
| 1535 |
+
# Wire up development mode changes
|
| 1536 |
+
def handle_mode_change(mode):
|
| 1537 |
+
if mode == "Medium Density":
|
| 1538 |
+
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
|
| 1539 |
+
else:
|
| 1540 |
+
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
|
| 1541 |
+
|
| 1542 |
+
def handle_md_type_change(md_type):
|
| 1543 |
+
if md_type == "Rear Loaded":
|
| 1544 |
+
return gr.update(visible=True), gr.update(visible=False)
|
| 1545 |
+
else:
|
| 1546 |
+
return gr.update(visible=False), gr.update(visible=True)
|
| 1547 |
+
|
| 1548 |
+
development_mode.change(
|
| 1549 |
+
handle_mode_change,
|
| 1550 |
+
inputs=[development_mode],
|
| 1551 |
+
outputs=[md_load_type, conventional_group, md_rear_group, md_front_group]
|
| 1552 |
+
)
|
| 1553 |
+
|
| 1554 |
+
md_load_type.change(
|
| 1555 |
+
handle_md_type_change,
|
| 1556 |
+
inputs=[md_load_type],
|
| 1557 |
+
outputs=[md_rear_group, md_front_group]
|
| 1558 |
+
)
|
| 1559 |
+
|
| 1560 |
+
# Wire up the optimize button
|
| 1561 |
optimize_btn.click(
|
| 1562 |
optimize_grid,
|
| 1563 |
inputs=[
|
| 1564 |
stage_width,
|
| 1565 |
stage_depth,
|
| 1566 |
+
development_mode,
|
| 1567 |
+
md_load_type,
|
| 1568 |
+
# Conventional
|
| 1569 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 1570 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
| 1571 |
+
# MD Rear
|
| 1572 |
+
enable_4_5, enable_6_0, enable_7_5,
|
| 1573 |
+
# MD Front
|
| 1574 |
+
enable_7_0, enable_8_0, enable_md_8_5, enable_md_10_5,
|
| 1575 |
allow_custom_corners, color_scheme
|
| 1576 |
],
|
| 1577 |
outputs=[plot_2d, results_table, summary_output, report_output, manual_widths]
|
|
|
|
| 1579 |
|
| 1580 |
update_btn.click(
|
| 1581 |
update_manual_adjustment,
|
| 1582 |
+
inputs=[manual_widths, stage_width, stage_depth, development_mode, md_load_type, color_scheme],
|
| 1583 |
outputs=[plot_2d, adjustment_feedback]
|
| 1584 |
)
|
| 1585 |
|