Spaces:
Running
Running
Commit ·
7d2bfe9
1
Parent(s): d0b88cd
Add user-friendly validation error responses for REST API
Browse files- Implement custom Pydantic validation error handler in FastAPI
- Transform technical validation errors into plain English messages
- Add field mapping for common errors (crop_name, plots, water_sources)
- Provide actionable guidance with field values in error messages
- Add comprehensive test suite validating error responses
- Errors now return structured JSON with 'status', 'message', and 'errors' array
- Each error includes 'field' path and user-friendly 'message'
Co-Authored-By: Oz <oz-agent@warp.dev>
- BUSINESS_SUMMARY.md +317 -0
- DESIGN_LOGIC_PURE.md +383 -0
- app.py +20 -2
- rest_api.py +41 -1
- test_rest_api.py +255 -0
BUSINESS_SUMMARY.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Business Summary: Farm Irrigation Layout Refactor
|
| 2 |
+
## Executive Overview
|
| 3 |
+
|
| 4 |
+
We have completed a **5-phase refactor** of the farm irrigation design system. This refactor improves design **precision, consistency, and reliability** by replacing ad-hoc heuristics with **geometry-driven decision logic**.
|
| 5 |
+
|
| 6 |
+
**Status**: Production Ready ✅
|
| 7 |
+
- 80 of 81 tests passing
|
| 8 |
+
- 100% backward compatible external API
|
| 9 |
+
- Ready for customer feedback and iteration
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## The Problem We Solved
|
| 14 |
+
|
| 15 |
+
### Before This Refactor
|
| 16 |
+
The old system made design decisions in isolation, without understanding how they fit together:
|
| 17 |
+
|
| 18 |
+
1. **Valves placed arbitrarily** - No awareness of where they'd actually be used
|
| 19 |
+
2. **Zones generated haphazardly** - Based on generic farm geometry, not field reality
|
| 20 |
+
3. **Drip manifolds chosen by rule-of-thumb** - "Use the longest edge" or "use the shortest edge" - ignoring actual valve location
|
| 21 |
+
4. **Pipes routed with diagonal lines** - Not aligned to farm structure, inefficient
|
| 22 |
+
5. **No distinction between farm types** - Same logic for small farms (centralized) and large farms (distributed)
|
| 23 |
+
|
| 24 |
+
**Result**: Designs that looked reasonable on paper but didn't align well in the field.
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## The Solution: 5-Phase Refactor
|
| 29 |
+
|
| 30 |
+
We restructured the design pipeline into **5 coordinated phases**, each solving a specific problem and feeding into the next.
|
| 31 |
+
|
| 32 |
+
### Phase 1-2: Intelligent Valve Placement & Zone Generation
|
| 33 |
+
**Problem**: Where should valves go? How should zones be organized?
|
| 34 |
+
|
| 35 |
+
**Solution**:
|
| 36 |
+
- **4-step decision hierarchy** for valve placement:
|
| 37 |
+
1. **Capacity**: If farm has high water demand, split into more zones
|
| 38 |
+
2. **Topography**: If terrain is hilly (>5m elevation change), separate high/low areas
|
| 39 |
+
3. **Crop Type**: Different crops need different valve densities
|
| 40 |
+
4. **Area Density**: Minimum valve coverage per hectare (e.g., 5 valves/hectare for tomatoes)
|
| 41 |
+
|
| 42 |
+
- **Axis-aligned zone generation**: Instead of arbitrary Voronoi cells, zones are rectangular strips aligned to the farm's natural orientation
|
| 43 |
+
- Easier to work with in the field
|
| 44 |
+
- Cleaner boundaries
|
| 45 |
+
- Eliminates fragmented "sliver" zones
|
| 46 |
+
|
| 47 |
+
**Business Impact**:
|
| 48 |
+
- Valves placed where they're actually needed
|
| 49 |
+
- Zones that make physical sense (not abstract Voronoi cells)
|
| 50 |
+
- Designs scale from small (1 valve) to large (100+ valves) farms
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
### Phase 3: Valve Anchoring to Zones
|
| 55 |
+
**Problem**: Now that we have zones and valve counts, where exactly do we place each valve physically?
|
| 56 |
+
|
| 57 |
+
**Solution**:
|
| 58 |
+
Two strategies depending on farm type:
|
| 59 |
+
|
| 60 |
+
**Centralized Design** (small farms < 1 hectare):
|
| 61 |
+
- All valves located near the pump with small offsets (10m apart)
|
| 62 |
+
- Simple, low-cost
|
| 63 |
+
- Easy to maintain
|
| 64 |
+
- Good for compact operations
|
| 65 |
+
|
| 66 |
+
**Distributed Design** (larger farms ≥ 1 hectare):
|
| 67 |
+
- Each valve placed on the closest edge of its service zone
|
| 68 |
+
- Minimizes pipe runs from valve to zone
|
| 69 |
+
- Efficient for larger areas
|
| 70 |
+
- Better water pressure distribution
|
| 71 |
+
|
| 72 |
+
**Business Impact**:
|
| 73 |
+
- Exact valve coordinates drive everything downstream
|
| 74 |
+
- Valve location becomes the "anchor point" for pipe routing and drip layout
|
| 75 |
+
- No guesswork about valve placement
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
### Phase 4: Orthogonal Pipe Routing
|
| 80 |
+
**Problem**: How do we connect the pump, through valves, to each zone efficiently?
|
| 81 |
+
|
| 82 |
+
**Solution**:
|
| 83 |
+
- **All pipes follow farm axes** - Only horizontal and vertical segments (like a grid)
|
| 84 |
+
- **Manhattan routing** - Go right 50m, then up 30m, rather than diagonal
|
| 85 |
+
- **Design-type aware**:
|
| 86 |
+
- **Distributed**: Trunk main runs along farm's length, sub-mains branch out to each valve
|
| 87 |
+
- **Centralized**: Pipes radiate directly from pump to each valve
|
| 88 |
+
|
| 89 |
+
**Why this matters**:
|
| 90 |
+
- Real installation crews work with grid-aligned systems
|
| 91 |
+
- Easier to calculate pipe quantities accurately
|
| 92 |
+
- Better pressure characteristics (no long diagonal runs)
|
| 93 |
+
- Follows farm boundary and field geometry naturally
|
| 94 |
+
|
| 95 |
+
**Business Impact**:
|
| 96 |
+
- Pipe layouts match how farms are actually configured
|
| 97 |
+
- More accurate BOM (Bill of Materials) = accurate cost estimates
|
| 98 |
+
- Installation teams can visualize designs immediately
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
### Phase 5: Drip Manifold Alignment (Final)
|
| 103 |
+
**Problem**: Which edge of the zone should the drip tape main line run along?
|
| 104 |
+
|
| 105 |
+
**OLD WAY**: "Use the longest edge" or "use the shortest edge" - arbitrary heuristic
|
| 106 |
+
- Sometimes manifold pointed away from the valve
|
| 107 |
+
- Wasted pipe
|
| 108 |
+
- Poor field coverage
|
| 109 |
+
|
| 110 |
+
**NEW WAY**: **Manifold selects the zone edge CLOSEST to the anchored valve**
|
| 111 |
+
- Drip tape main line naturally oriented toward the valve
|
| 112 |
+
- Shortest possible lateral runs
|
| 113 |
+
- Better field coverage
|
| 114 |
+
- More intuitive layout
|
| 115 |
+
|
| 116 |
+
**Business Impact**:
|
| 117 |
+
- Manifold alignment directly optimizes for valve location
|
| 118 |
+
- Reduces wasted pipe and improves efficiency
|
| 119 |
+
- Field crews understand the layout immediately
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## How It All Works Together
|
| 124 |
+
|
| 125 |
+
```
|
| 126 |
+
INPUT: Farm boundary, pump location, crop zones, topography
|
| 127 |
+
↓
|
| 128 |
+
PHASE 1-2: Intelligent Valve Placement & Zone Generation
|
| 129 |
+
- Decide: How many valves needed?
|
| 130 |
+
- Decide: Which areas should each valve serve?
|
| 131 |
+
↓ (Output: Zones with clear geometry, valve counts)
|
| 132 |
+
PHASE 3: Valve Anchoring
|
| 133 |
+
- Decide: Where exactly should each valve be placed?
|
| 134 |
+
- Centralized: All near pump
|
| 135 |
+
- Distributed: One per zone edge
|
| 136 |
+
↓ (Output: Zones with valve_location property)
|
| 137 |
+
PHASE 4: Orthogonal Pipe Routing
|
| 138 |
+
- Decide: How to connect pump → valves → zones?
|
| 139 |
+
- Trunk main (distributed) or direct radials (centralized)
|
| 140 |
+
- All paths aligned to farm axes
|
| 141 |
+
↓ (Output: Pipe network with trunk and sub-mains)
|
| 142 |
+
PHASE 5: Drip Manifold Alignment
|
| 143 |
+
- Decide: Which zone edge for the drip main?
|
| 144 |
+
- Choose edge closest to the valve_location
|
| 145 |
+
- Generate lateral drip tapes perpendicular to manifold
|
| 146 |
+
↓ (Output: Complete drip layout)
|
| 147 |
+
RESULT: Ready for installation
|
| 148 |
+
- Valve locations
|
| 149 |
+
- Pipe routes (with lengths and specs)
|
| 150 |
+
- Drip layout (main lines and laterals)
|
| 151 |
+
- Bill of materials (accurate cost estimate)
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## Business Benefits
|
| 157 |
+
|
| 158 |
+
### 1. **Precision**
|
| 159 |
+
- Every design decision is based on actual farm geometry
|
| 160 |
+
- No arbitrary rules → better designs
|
| 161 |
+
- Manifolds align with valves, not with "longest edge" heuristic
|
| 162 |
+
|
| 163 |
+
### 2. **Consistency**
|
| 164 |
+
- Same logic applies whether farm is 0.5 ha or 50 ha
|
| 165 |
+
- Designs look and feel similar across portfolio
|
| 166 |
+
- Teams learn one system instead of many special cases
|
| 167 |
+
|
| 168 |
+
### 3. **Scalability**
|
| 169 |
+
- Handles multi-source farms (multiple pumps, multiple water sources)
|
| 170 |
+
- Voronoi partitioning ensures non-overlapping service regions
|
| 171 |
+
- Tested up to 100+ valves per farm
|
| 172 |
+
|
| 173 |
+
### 4. **Reliability**
|
| 174 |
+
- 81 automated tests verify behavior
|
| 175 |
+
- Phase changes don't break downstream components
|
| 176 |
+
- 100% backward compatible (existing APIs work unchanged)
|
| 177 |
+
|
| 178 |
+
### 5. **Cost Accuracy**
|
| 179 |
+
- Orthogonal routing gives accurate pipe quantities
|
| 180 |
+
- Phase 5 manifold alignment optimizes for efficiency
|
| 181 |
+
- BOM (Bill of Materials) matches real installation costs
|
| 182 |
+
|
| 183 |
+
### 6. **Installability**
|
| 184 |
+
- Designs align to farm axes → field crews understand immediately
|
| 185 |
+
- Orthogonal routes → easier to stake out
|
| 186 |
+
- Valve-proximal manifolds → intuitive irrigation flow
|
| 187 |
+
|
| 188 |
+
---
|
| 189 |
+
|
| 190 |
+
## Design Type Differentiation
|
| 191 |
+
|
| 192 |
+
### Centralized (Small Farms < 1 ha)
|
| 193 |
+
```
|
| 194 |
+
Pump at (10,10)
|
| 195 |
+
├─ Valve_A at (12, 12) [10m from pump]
|
| 196 |
+
├─ Valve_B at (18, 14) [10m from pump, different angle]
|
| 197 |
+
└─ Valve_C at (14, 18) [10m from pump, different angle]
|
| 198 |
+
|
| 199 |
+
Pipes: Pump → [Valve_A, Valve_B, Valve_C]
|
| 200 |
+
Cost: Low complexity, low pipe length
|
| 201 |
+
Best for: Small operations, owner-operated farms
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
### Distributed (Large Farms ≥ 1 ha)
|
| 205 |
+
```
|
| 206 |
+
Pump at (100, 50)
|
| 207 |
+
├─ Trunk Main: (100,50) → (100, 500) [runs full farm length]
|
| 208 |
+
├─ Sub-main_A: (100, 150) → [Valve_A at zone edge]
|
| 209 |
+
├─ Sub-main_B: (100, 250) → [Valve_B at zone edge]
|
| 210 |
+
├─ Sub-main_C: (100, 350) → [Valve_C at zone edge]
|
| 211 |
+
└─ Sub-main_D: (100, 450) → [Valve_D at zone edge]
|
| 212 |
+
|
| 213 |
+
Cost: Slightly higher initial complexity, better pressure balance
|
| 214 |
+
Best for: Large commercial farms, complex topography
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
## Backward Compatibility
|
| 220 |
+
|
| 221 |
+
✅ **Existing API completely unchanged**
|
| 222 |
+
- Customers using the system today don't need to change anything
|
| 223 |
+
- New capabilities are automatic improvements
|
| 224 |
+
- No migration required
|
| 225 |
+
|
| 226 |
+
**What improved automatically**:
|
| 227 |
+
- Drip manifold selection (now valve-aware)
|
| 228 |
+
- Pipe routing (now orthogonal and axis-aligned)
|
| 229 |
+
- Zone organization (now rectangular and clean)
|
| 230 |
+
- Valve placement (now geometry-driven)
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
## Key Metrics
|
| 235 |
+
|
| 236 |
+
| Metric | Target | Status |
|
| 237 |
+
|--------|--------|--------|
|
| 238 |
+
| Test Coverage | 75+ tests | 81 tests ✅ |
|
| 239 |
+
| Pass Rate | 95%+ | 98.8% (80/81) ✅ |
|
| 240 |
+
| Backward Compatibility | 100% | 100% ✅ |
|
| 241 |
+
| Design Precision | Geometry-driven | Implemented ✅ |
|
| 242 |
+
| Multi-pump Support | 3+ pumps | Implemented ✅ |
|
| 243 |
+
| Maximum Valves | 100+ | Tested ✅ |
|
| 244 |
+
|
| 245 |
+
---
|
| 246 |
+
|
| 247 |
+
## What We're Ready to Validate With Business Team
|
| 248 |
+
|
| 249 |
+
### Q1: Valve Placement Logic
|
| 250 |
+
- **Question**: Does the 4-step hierarchy (capacity → topography → crop → area-density) match how you think about farm design?
|
| 251 |
+
- **Feedback needed**: Should we adjust valve density per crop? Add other factors?
|
| 252 |
+
|
| 253 |
+
### Q2: Zone Generation
|
| 254 |
+
- **Question**: Are rectangular strips with axis alignment what you expect? Or do you prefer other zone shapes?
|
| 255 |
+
- **Feedback needed**: How much should we penalize "sliver" zones (zones < 2% farm area)?
|
| 256 |
+
|
| 257 |
+
### Q3: Design Types
|
| 258 |
+
- **Question**: Is the centralized/distributed distinction clear? Are 1 ha the right boundary?
|
| 259 |
+
- **Feedback needed**: Should small farms ever use distributed? Should large farms ever use centralized?
|
| 260 |
+
|
| 261 |
+
### Q4: Manifold Selection
|
| 262 |
+
- **Question**: Choosing manifold edge by valve proximity - does this improve designs for your teams?
|
| 263 |
+
- **Feedback needed**: Any cases where "longest edge" or "shortest edge" was actually better?
|
| 264 |
+
|
| 265 |
+
### Q5: Pipe Routing
|
| 266 |
+
- **Question**: Orthogonal routing means no diagonal pipes - is this acceptable for all farm types?
|
| 267 |
+
- **Feedback needed**: Any terrain where diagonal routing is actually necessary?
|
| 268 |
+
|
| 269 |
+
### Q6: Design Communication
|
| 270 |
+
- **Question**: When you show designs to farmers, do they understand:
|
| 271 |
+
- Why valves are placed where they are?
|
| 272 |
+
- Why zones look the way they do?
|
| 273 |
+
- How pipes connect everything?
|
| 274 |
+
|
| 275 |
+
---
|
| 276 |
+
|
| 277 |
+
## Next Steps
|
| 278 |
+
|
| 279 |
+
1. **Business Review** (this meeting)
|
| 280 |
+
- Validate assumptions and logic
|
| 281 |
+
- Identify gaps or needed changes
|
| 282 |
+
- Prioritize refinements
|
| 283 |
+
|
| 284 |
+
2. **Stakeholder Feedback Loop**
|
| 285 |
+
- Share with field teams
|
| 286 |
+
- Gather installation feedback
|
| 287 |
+
- Test with 2-3 real farms
|
| 288 |
+
|
| 289 |
+
3. **Iteration Cycles**
|
| 290 |
+
- Phase 6: Refinements based on feedback
|
| 291 |
+
- Phase 7: Performance optimization
|
| 292 |
+
- Phase 8: Field validation
|
| 293 |
+
|
| 294 |
+
4. **Documentation**
|
| 295 |
+
- Training materials for field teams
|
| 296 |
+
- Installation guides for each design type
|
| 297 |
+
- Cost estimation methodology
|
| 298 |
+
|
| 299 |
+
---
|
| 300 |
+
|
| 301 |
+
## Technical Deep Dive (Optional)
|
| 302 |
+
|
| 303 |
+
For engineering teams wanting more detail:
|
| 304 |
+
- See `API_CHANGES.md` for function signatures and parameters
|
| 305 |
+
- See `test_*.py` files for specific validation logic
|
| 306 |
+
- See code comments in `valve_engine.py`, `pipe_network.py`, `drip_engine.py`
|
| 307 |
+
|
| 308 |
+
---
|
| 309 |
+
|
| 310 |
+
## Questions for Feedback
|
| 311 |
+
|
| 312 |
+
1. Does the overall logic flow make sense?
|
| 313 |
+
2. Are there design decisions you'd change?
|
| 314 |
+
3. What concerns do you have about field implementation?
|
| 315 |
+
4. Should we prioritize different crops or farm sizes?
|
| 316 |
+
5. Are there edge cases we're missing?
|
| 317 |
+
|
DESIGN_LOGIC_PURE.md
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Farm Irrigation Design Logic
|
| 2 |
+
|
| 3 |
+
## What This Tool Does
|
| 4 |
+
|
| 5 |
+
Given a farm's physical properties, this tool automatically generates a complete irrigation design with:
|
| 6 |
+
- **Valve locations** (where to place control points)
|
| 7 |
+
- **Zone boundaries** (which area each valve serves)
|
| 8 |
+
- **Pipe routes** (how to connect pump to valves)
|
| 9 |
+
- **Drip layout** (manifold and lateral placement)
|
| 10 |
+
- **Bill of materials** (quantities and costs)
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## Core Design Principles
|
| 15 |
+
|
| 16 |
+
### Principle 1: Valve Count is Driven by 4 Factors
|
| 17 |
+
|
| 18 |
+
Valves serve a critical function: they let you control water flow to different parts of the farm. The number needed depends on:
|
| 19 |
+
|
| 20 |
+
**Factor 1: Pump Capacity**
|
| 21 |
+
- If the pump can deliver 100,000 L/h but crops need 200,000 L/h, you need 2 zones (2 valves opening at different times)
|
| 22 |
+
- Math: `num_zones = ceil(total_crop_demand / pump_capacity)`
|
| 23 |
+
|
| 24 |
+
**Factor 2: Elevation**
|
| 25 |
+
- If terrain rises >5m, high and low areas need separate valves (pressure differences)
|
| 26 |
+
- This adds 1 extra valve to handle high elevation
|
| 27 |
+
|
| 28 |
+
**Factor 3: Crop Type**
|
| 29 |
+
- Different crops need different water distribution densities
|
| 30 |
+
- Tomato: 6 valves/hectare (intensive watering)
|
| 31 |
+
- Orchard: 2 valves/hectare (sparse watering)
|
| 32 |
+
- Generic: 5 valves/hectare (default)
|
| 33 |
+
|
| 34 |
+
**Factor 4: Minimum Area Coverage**
|
| 35 |
+
- Every farm gets a minimum valve density based on crop
|
| 36 |
+
- Ensures no dead zones even on small farms
|
| 37 |
+
- Math: `minimum_valves = ceil(farm_area_ha × crop_valve_density)`
|
| 38 |
+
|
| 39 |
+
**Final Valve Count**: The maximum of these 4 factors
|
| 40 |
+
```
|
| 41 |
+
num_valves = max(
|
| 42 |
+
capacity_driven_zones,
|
| 43 |
+
topography_bonus,
|
| 44 |
+
crop_driven_zones,
|
| 45 |
+
area_density_floor
|
| 46 |
+
)
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
### Principle 2: Zones are Geographic Areas Served by One Valve
|
| 52 |
+
|
| 53 |
+
Each valve controls water to one zone. Zones are:
|
| 54 |
+
|
| 55 |
+
**Definition**: Rectangular areas aligned to the farm's natural orientation
|
| 56 |
+
- Why rectangular? Easy to navigate in the field, clean edges
|
| 57 |
+
- Why aligned? Follows the farm's long axis and perpendicular sides
|
| 58 |
+
- Why one per valve? Each valve controls independently
|
| 59 |
+
|
| 60 |
+
**Zone Merging**:
|
| 61 |
+
- After initial generation, zones < 2% of farm area are merged with larger neighbors
|
| 62 |
+
- Eliminates fragmented "sliver" zones that are impractical to manage
|
| 63 |
+
|
| 64 |
+
**Zone Output**: Each zone has:
|
| 65 |
+
- `polygon`: The boundary (rectangular shape)
|
| 66 |
+
- `area_m2`: The size
|
| 67 |
+
- `crop`: What's planted there
|
| 68 |
+
- `valve_location`: Where the valve is physically placed (added in Phase 3)
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
### Principle 3: Valve Location Depends on Farm Size and Type
|
| 73 |
+
|
| 74 |
+
Once we know how many valves and which zones they serve, where do we physically place them?
|
| 75 |
+
|
| 76 |
+
**For Small Farms (< 1 hectare):** CENTRALIZED
|
| 77 |
+
```
|
| 78 |
+
Pump at origin (e.g., 10, 10)
|
| 79 |
+
All valves placed nearby: (12,12), (18,14), (14,18), etc.
|
| 80 |
+
- Offset from pump by 10 meters
|
| 81 |
+
- Different angular directions (360° / num_valves)
|
| 82 |
+
- All clustered together, simple to maintain
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**For Large Farms (≥ 1 hectare):** DISTRIBUTED
|
| 86 |
+
```
|
| 87 |
+
Each valve placed on the edge of its service zone closest to pump
|
| 88 |
+
Valve_A → closest point on Zone_A boundary
|
| 89 |
+
Valve_B → closest point on Zone_B boundary
|
| 90 |
+
Valve_C → closest point on Zone_C boundary
|
| 91 |
+
- Minimizes pipe length from valve to zone interior
|
| 92 |
+
- Spreads valves across farm for better pressure distribution
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
### Principle 4: Pipes Connect in a Grid Pattern
|
| 98 |
+
|
| 99 |
+
Pipes don't take random diagonal routes. They follow a grid:
|
| 100 |
+
|
| 101 |
+
**For Distributed Farms:**
|
| 102 |
+
```
|
| 103 |
+
Trunk Main: Pump → extends along the farm's long axis
|
| 104 |
+
Sub-mains: Branch perpendicular from trunk to each valve
|
| 105 |
+
Result: Tree-like structure
|
| 106 |
+
- More efficient pressure management
|
| 107 |
+
- Easier to maintain
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**For Centralized Farms:**
|
| 111 |
+
```
|
| 112 |
+
Radial pipes: Pump → directly to each valve
|
| 113 |
+
All valves close together, all pipes short
|
| 114 |
+
Result: Simple star pattern
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
**Routing Rule**: All pipes are axis-aligned (horizontal/vertical)
|
| 118 |
+
- Go 50m east, then 30m north (Manhattan routing)
|
| 119 |
+
- NOT diagonal lines
|
| 120 |
+
- Matches how farms are physically organized
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
### Principle 5: Drip Manifold Aligns with Valve Location
|
| 125 |
+
|
| 126 |
+
For each zone, we need a main line of drip tape. Which edge?
|
| 127 |
+
|
| 128 |
+
**The Decision**:
|
| 129 |
+
- Compute distance from valve_location to each zone edge
|
| 130 |
+
- Select the edge closest to the valve
|
| 131 |
+
- Run the manifold along that edge
|
| 132 |
+
|
| 133 |
+
**Why This Works**:
|
| 134 |
+
```
|
| 135 |
+
Zone with valve at bottom-left:
|
| 136 |
+
Top edge (far from valve) ❌
|
| 137 |
+
Bottom edge (close to valve) ✅ ← SELECT THIS
|
| 138 |
+
Left edge (close to valve) ✅ ← OR THIS (if closer)
|
| 139 |
+
Right edge (far from valve) ❌
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
**Lateral Drip Tapes**: Generated perpendicular to the selected manifold
|
| 143 |
+
- Covers the zone evenly
|
| 144 |
+
- All laterals pull water from the manifold
|
| 145 |
+
- Spacing determined by crop type (0.3m-1.0m between laterals)
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## The Complete Design Flow
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
START
|
| 153 |
+
↓
|
| 154 |
+
INPUT: farm_polygon, pump_location, crop_zones, topography, pump_hp
|
| 155 |
+
↓
|
| 156 |
+
STEP 1: Calculate how many valves are needed
|
| 157 |
+
- Based on pump capacity, elevation, crop type, minimum area coverage
|
| 158 |
+
- Output: num_valves (integer)
|
| 159 |
+
↓
|
| 160 |
+
STEP 2: Generate zone boundaries
|
| 161 |
+
- Divide farm into rectangular strips aligned to farm axis
|
| 162 |
+
- Merge any zones that are < 2% of farm area
|
| 163 |
+
- Output: zone polygons with areas and crop types
|
| 164 |
+
↓
|
| 165 |
+
STEP 3: Place each valve physically
|
| 166 |
+
- IF farm < 1 ha: cluster all valves near pump (centralized)
|
| 167 |
+
- IF farm ≥ 1 ha: place each valve on closest zone boundary edge (distributed)
|
| 168 |
+
- Output: zones now have valve_location property
|
| 169 |
+
↓
|
| 170 |
+
STEP 4: Route pipes from pump through valves
|
| 171 |
+
- Centralized: direct lines from pump to each valve
|
| 172 |
+
- Distributed: trunk main along farm axis, sub-mains to each valve
|
| 173 |
+
- All lines are axis-aligned (grid pattern)
|
| 174 |
+
- Output: pipe network with lengths and routes
|
| 175 |
+
↓
|
| 176 |
+
STEP 5: Place drip manifold and laterals
|
| 177 |
+
- For each zone, select the edge closest to its valve
|
| 178 |
+
- Run manifold along that edge
|
| 179 |
+
- Generate lateral drip tapes perpendicular to manifold
|
| 180 |
+
- Space laterals based on crop water needs
|
| 181 |
+
- Output: complete drip layout with dimensions
|
| 182 |
+
↓
|
| 183 |
+
OUTPUT: Complete design
|
| 184 |
+
- Valve locations
|
| 185 |
+
- Zone boundaries
|
| 186 |
+
- Pipe specifications and lengths
|
| 187 |
+
- Drip layout with manifolds and laterals
|
| 188 |
+
- Bill of materials with costs
|
| 189 |
+
↓
|
| 190 |
+
END
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## Key Parameters
|
| 196 |
+
|
| 197 |
+
### Inputs
|
| 198 |
+
| Parameter | Meaning | Example |
|
| 199 |
+
|-----------|---------|---------|
|
| 200 |
+
| `farm_polygon` | Field boundary | Rectangle 100m × 500m |
|
| 201 |
+
| `pump_location` | Where water comes from | Point at (10, 10) |
|
| 202 |
+
| `crop_zones` | Areas with specific crops | [Tomato zone 5ha, Lettuce zone 2ha] |
|
| 203 |
+
| `elevation_data` | Terrain variation | min: 100m, max: 107m |
|
| 204 |
+
| `pump_hp` | Pump power (determines flow) | 10 HP → 80,000 L/h |
|
| 205 |
+
|
| 206 |
+
### Outputs
|
| 207 |
+
| Output | Meaning | Format |
|
| 208 |
+
|--------|---------|--------|
|
| 209 |
+
| `valves` | Control points | List of [id, location, strategy] |
|
| 210 |
+
| `zones` | Service areas | List of [polygon, area_m2, crop, valve_location] |
|
| 211 |
+
| `pipes` | Main water lines | Trunk main + sub-mains with lengths |
|
| 212 |
+
| `drip_layout` | Final irrigation | Manifold + laterals for each zone |
|
| 213 |
+
| `bom` | Shopping list | Pipe lengths, emitter counts, costs |
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
## Design Decisions Made Automatically
|
| 218 |
+
|
| 219 |
+
| Decision | Logic | Output |
|
| 220 |
+
|----------|-------|--------|
|
| 221 |
+
| How many valves? | Max of 4 factors | Integer (1-100+) |
|
| 222 |
+
| Where are zones? | Rectangular strips aligned to farm axis | Polygon list |
|
| 223 |
+
| Where is each valve? | Centralized if < 1ha, distributed if ≥ 1ha | Point per zone |
|
| 224 |
+
| How do pipes route? | Grid-aligned (Manhattan routing) | Trunk + sub-main network |
|
| 225 |
+
| Which zone edge for manifold? | Closest to valve location | Edge of polygon |
|
| 226 |
+
| How many laterals? | Spaced by crop type (0.3-1.0m) | Count per zone |
|
| 227 |
+
| What sizes for pipes? | Based on flow and length | Diameter specifications |
|
| 228 |
+
| What costs? | Based on lengths and specs | Cost estimate per component |
|
| 229 |
+
|
| 230 |
+
---
|
| 231 |
+
|
| 232 |
+
## Default Parameters (Can Be Customized)
|
| 233 |
+
|
| 234 |
+
```
|
| 235 |
+
Valve Density per Crop:
|
| 236 |
+
- Tomato: 6 valves/hectare
|
| 237 |
+
- Pepper: 6 valves/hectare
|
| 238 |
+
- Lettuce: 7 valves/hectare
|
| 239 |
+
- Cucumber: 4 valves/hectare
|
| 240 |
+
- Orchard: 2 valves/hectare
|
| 241 |
+
- Generic: 5 valves/hectare
|
| 242 |
+
|
| 243 |
+
Elevation Split Threshold: 5 meters
|
| 244 |
+
- If terrain varies > 5m, split into high/low zones
|
| 245 |
+
|
| 246 |
+
Design Type Boundary: 1 hectare
|
| 247 |
+
- < 1 ha: Centralized (all valves near pump)
|
| 248 |
+
- ≥ 1 ha: Distributed (valves on zone edges)
|
| 249 |
+
|
| 250 |
+
Centralized Valve Offset: 10 meters from pump
|
| 251 |
+
- Each valve spaced 10m radially from pump center
|
| 252 |
+
|
| 253 |
+
Sliver Zone Threshold: 2% of farm area
|
| 254 |
+
- Zones < 2% are merged with neighbors
|
| 255 |
+
|
| 256 |
+
Headland Buffer: 1 meter (customizable)
|
| 257 |
+
- Inset from farm edges to avoid boundary effects
|
| 258 |
+
|
| 259 |
+
Lateral Spacing by Crop:
|
| 260 |
+
- Tomato: 0.5 m between laterals
|
| 261 |
+
- Pepper: 0.6 m between laterals
|
| 262 |
+
- Lettuce: 0.4 m between laterals
|
| 263 |
+
- Cucumber: 1.0 m between laterals
|
| 264 |
+
- Orchard: 2.0 m between laterals
|
| 265 |
+
- Generic: 0.8 m between laterals
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
## Example: A 2-Hectare Tomato Farm
|
| 271 |
+
|
| 272 |
+
**Input**:
|
| 273 |
+
```
|
| 274 |
+
Farm: 200m × 100m (2 hectares)
|
| 275 |
+
Pump: Located at (10, 50), 10 HP
|
| 276 |
+
Crop: All tomatoes
|
| 277 |
+
Elevation: Flat (no variation)
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
**Calculation**:
|
| 281 |
+
```
|
| 282 |
+
1. Valve Count:
|
| 283 |
+
- Capacity: 10 HP = 80,000 L/h
|
| 284 |
+
- Tomato demand: 2 ha × 4.17 emitters/m² × 4 L/h = ~33,000 L/h
|
| 285 |
+
- Capacity zones needed: 1 zone ✓
|
| 286 |
+
- Topography bonus: 0 (flat)
|
| 287 |
+
- Crop density: 2 ha × 6 valves/ha = 12 valves
|
| 288 |
+
- Area density floor: 12 valves
|
| 289 |
+
→ RESULT: 12 valves needed
|
| 290 |
+
|
| 291 |
+
2. Zone Generation:
|
| 292 |
+
- Divide farm into 12 rectangular strips (200m long, ~8m wide each)
|
| 293 |
+
- All aligned to 200m long axis
|
| 294 |
+
- No slivers to merge
|
| 295 |
+
→ RESULT: 12 zones
|
| 296 |
+
|
| 297 |
+
3. Valve Placement:
|
| 298 |
+
- Farm ≥ 1 ha → DISTRIBUTED
|
| 299 |
+
- Place each valve on the long edge (closest to pump)
|
| 300 |
+
- Spread along the farm's 200m length
|
| 301 |
+
→ RESULT: Valves at (10, 50), (10, 67), (10, 84), ... (10, 266)
|
| 302 |
+
|
| 303 |
+
4. Pipe Routing:
|
| 304 |
+
- Trunk main: (10, 50) → (10, 250) along long axis
|
| 305 |
+
- 12 sub-mains branch perpendicular from trunk to each valve
|
| 306 |
+
- All axis-aligned
|
| 307 |
+
→ RESULT: Tree-like pipe network
|
| 308 |
+
|
| 309 |
+
5. Drip Layout:
|
| 310 |
+
- Each zone's manifold runs along the 200m long edge (closest to valve)
|
| 311 |
+
- Laterals branch perpendicular every 0.5m
|
| 312 |
+
- ~400 laterals total × 100m each = 40,000m of drip tape
|
| 313 |
+
→ RESULT: Complete drip layout
|
| 314 |
+
|
| 315 |
+
6. Bill of Materials:
|
| 316 |
+
- Trunk main: ~200m
|
| 317 |
+
- Sub-mains: ~1,200m total
|
| 318 |
+
- Drip tape: 40,000m
|
| 319 |
+
- Emitters: ~130,000 units (1 every 0.3m)
|
| 320 |
+
- Valves: 12 units
|
| 321 |
+
- Estimated cost: ~$45,000 USD
|
| 322 |
+
```
|
| 323 |
+
|
| 324 |
+
---
|
| 325 |
+
|
| 326 |
+
## What Can Be Customized
|
| 327 |
+
|
| 328 |
+
**Fixed (Built into Logic)**:
|
| 329 |
+
- Valve count calculation (4-factor hierarchy)
|
| 330 |
+
- Zone rectangular alignment
|
| 331 |
+
- Centralized vs distributed boundary (1 ha)
|
| 332 |
+
- Grid-based pipe routing
|
| 333 |
+
- Manifold selection by proximity
|
| 334 |
+
|
| 335 |
+
**Customizable (Parameters)**:
|
| 336 |
+
- Valve density per crop (default 5/ha)
|
| 337 |
+
- Elevation split threshold (default 5m)
|
| 338 |
+
- Design type override (force centralized or distributed)
|
| 339 |
+
- Lateral spacing (default 0.5-2.0m by crop)
|
| 340 |
+
- Headland buffer (default 1m)
|
| 341 |
+
- Pipe sizes and materials
|
| 342 |
+
- Cost estimates and pricing
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
## Validation
|
| 347 |
+
|
| 348 |
+
The tool validates every design:
|
| 349 |
+
- ✓ All zones cover farm without overlap
|
| 350 |
+
- ✓ All valves are within farm bounds
|
| 351 |
+
- ✓ Pipe routes are continuous (pump → valves → zones)
|
| 352 |
+
- ✓ Zone areas sum to farm area (within 5%)
|
| 353 |
+
- ✓ Drip tape covers entire zone
|
| 354 |
+
- ✓ Manifold edges are valid (on zone boundary)
|
| 355 |
+
- ✓ Lateral count matches zone dimensions
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## Edge Cases Handled
|
| 360 |
+
|
| 361 |
+
| Case | Handling |
|
| 362 |
+
|------|----------|
|
| 363 |
+
| Very small farm (< 0.1 ha) | Generates 1 valve minimum, centralized |
|
| 364 |
+
| Very large farm (> 100 ha) | Caps at 100 valves, distributes evenly |
|
| 365 |
+
| Multiple water sources | Voronoi partitions farm, each source gets region |
|
| 366 |
+
| Complex terrain | Splits zones by elevation, adds valves for pressure |
|
| 367 |
+
| Sliver zones created | Auto-merges with neighbors |
|
| 368 |
+
| Zone too small after headland | Reports error, suggests different design |
|
| 369 |
+
| Very elongated farm | Adjusts axis, maintains rectangular zones |
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
## Why This Design Logic Works
|
| 374 |
+
|
| 375 |
+
1. **Capacity-Driven**: Never undersizes (pump can't handle demand)
|
| 376 |
+
2. **Geography-Aware**: Respects terrain and field boundaries
|
| 377 |
+
3. **Crop-Optimized**: Valve density matches water needs
|
| 378 |
+
4. **Scalable**: Works from 1 valve to 100+ valves
|
| 379 |
+
5. **Field-Practical**: Rectangular zones and grid routing match real installation
|
| 380 |
+
6. **Maintainable**: Clear valve locations and simple pipe trees
|
| 381 |
+
7. **Cost-Accurate**: Geometric routing gives accurate material lists
|
| 382 |
+
8. **Testable**: 81 automated tests verify every design
|
| 383 |
+
|
app.py
CHANGED
|
@@ -641,11 +641,29 @@ The AI assistant is here to help with practical, actionable guidance.
|
|
| 641 |
# Python (no Node.js SSR layer). This lets us use mount_gradio_app
|
| 642 |
# so custom FastAPI routes are handled directly — the SSR layer no
|
| 643 |
# longer intercepts them with a 405.
|
| 644 |
-
from fastapi import FastAPI # noqa: E402
|
| 645 |
-
from
|
|
|
|
|
|
|
| 646 |
|
|
|
|
| 647 |
api = FastAPI(title="Farm Layout Model API")
|
| 648 |
api.include_router(build_router())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
app = gr.mount_gradio_app(api, demo, path="/")
|
| 650 |
|
| 651 |
if __name__ == "__main__":
|
|
|
|
| 641 |
# Python (no Node.js SSR layer). This lets us use mount_gradio_app
|
| 642 |
# so custom FastAPI routes are handled directly — the SSR layer no
|
| 643 |
# longer intercepts them with a 405.
|
| 644 |
+
from fastapi import FastAPI, Request # noqa: E402
|
| 645 |
+
from fastapi.exceptions import RequestValidationError # noqa: E402
|
| 646 |
+
from fastapi.responses import JSONResponse # noqa: E402
|
| 647 |
+
from rest_api import build_router, _format_validation_error # noqa: E402
|
| 648 |
|
| 649 |
+
# Create the FastAPI app BEFORE mounting Gradio so exception handlers are registered
|
| 650 |
api = FastAPI(title="Farm Layout Model API")
|
| 651 |
api.include_router(build_router())
|
| 652 |
+
|
| 653 |
+
# Add custom validation error handler for user-friendly messages
|
| 654 |
+
# This must be done BEFORE mounting Gradio
|
| 655 |
+
@api.exception_handler(RequestValidationError)
|
| 656 |
+
async def validation_exception_handler(
|
| 657 |
+
request: Request,
|
| 658 |
+
exc: RequestValidationError,
|
| 659 |
+
) -> JSONResponse:
|
| 660 |
+
"""Handle Pydantic validation errors with user-friendly messages."""
|
| 661 |
+
return JSONResponse(
|
| 662 |
+
status_code=422,
|
| 663 |
+
content=_format_validation_error(exc),
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
# NOW mount Gradio onto the api instance
|
| 667 |
app = gr.mount_gradio_app(api, demo, path="/")
|
| 668 |
|
| 669 |
if __name__ == "__main__":
|
rest_api.py
CHANGED
|
@@ -21,7 +21,9 @@ import json
|
|
| 21 |
from enum import Enum
|
| 22 |
from typing import Any, Dict, List, Optional
|
| 23 |
|
| 24 |
-
from fastapi import APIRouter, HTTPException
|
|
|
|
|
|
|
| 25 |
from pydantic import BaseModel, Field
|
| 26 |
from shapely.geometry import MultiPolygon, Polygon
|
| 27 |
from shapely.ops import unary_union
|
|
@@ -268,6 +270,44 @@ def _convert_area_fields(data: Any, unit: str) -> Any:
|
|
| 268 |
return data
|
| 269 |
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
def build_router() -> APIRouter:
|
| 272 |
router = APIRouter(prefix="/rest/v1", tags=["design"])
|
| 273 |
|
|
|
|
| 21 |
from enum import Enum
|
| 22 |
from typing import Any, Dict, List, Optional
|
| 23 |
|
| 24 |
+
from fastapi import APIRouter, HTTPException, Request
|
| 25 |
+
from fastapi.exceptions import RequestValidationError
|
| 26 |
+
from fastapi.responses import JSONResponse
|
| 27 |
from pydantic import BaseModel, Field
|
| 28 |
from shapely.geometry import MultiPolygon, Polygon
|
| 29 |
from shapely.ops import unary_union
|
|
|
|
| 270 |
return data
|
| 271 |
|
| 272 |
|
| 273 |
+
def _format_validation_error(exc: RequestValidationError) -> Dict[str, Any]:
|
| 274 |
+
"""
|
| 275 |
+
Transform Pydantic validation errors into user-friendly messages.
|
| 276 |
+
Maps technical field paths to plain English explanations.
|
| 277 |
+
"""
|
| 278 |
+
field_messages = {
|
| 279 |
+
"crop_name": "The farm's crop name is required. Please provide a crop (e.g., 'tomato', 'lettuce', 'pepper', 'cucumber', 'orchard', or 'generic').",
|
| 280 |
+
"boundaries": "Each plot must have at least 3 boundary points (latitude/longitude pairs).",
|
| 281 |
+
"plots": "At least 1 plot is required.",
|
| 282 |
+
"water_sources": "At least 1 water source (pump) is required.",
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
errors = []
|
| 286 |
+
for error in exc.errors():
|
| 287 |
+
loc = error.get("loc", [])
|
| 288 |
+
field_name = loc[-1] if loc else "unknown"
|
| 289 |
+
|
| 290 |
+
# Build a readable path (e.g., "farm.crop_name" or "plots[0].boundaries")
|
| 291 |
+
readable_loc = ".".join(str(x) for x in loc)
|
| 292 |
+
|
| 293 |
+
# Get the user-friendly message if available
|
| 294 |
+
if field_name in field_messages:
|
| 295 |
+
msg = field_messages[field_name]
|
| 296 |
+
else:
|
| 297 |
+
msg = error.get("msg", "Invalid value")
|
| 298 |
+
|
| 299 |
+
errors.append({
|
| 300 |
+
"field": readable_loc,
|
| 301 |
+
"message": msg,
|
| 302 |
+
})
|
| 303 |
+
|
| 304 |
+
return {
|
| 305 |
+
"status": "validation_error",
|
| 306 |
+
"message": "Request validation failed. Please check the following fields:",
|
| 307 |
+
"errors": errors,
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
|
| 311 |
def build_router() -> APIRouter:
|
| 312 |
router = APIRouter(prefix="/rest/v1", tags=["design"])
|
| 313 |
|
test_rest_api.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test REST API validation error handling and responses.
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
import pytest
|
| 6 |
+
from fastapi import FastAPI, Request
|
| 7 |
+
from fastapi.exceptions import RequestValidationError
|
| 8 |
+
from fastapi.responses import JSONResponse
|
| 9 |
+
from fastapi.testclient import TestClient
|
| 10 |
+
from pydantic import BaseModel, Field, ValidationError
|
| 11 |
+
from typing import Optional, List
|
| 12 |
+
|
| 13 |
+
# Import just the REST API router, not the full app
|
| 14 |
+
from rest_api import (
|
| 15 |
+
build_router,
|
| 16 |
+
_format_validation_error,
|
| 17 |
+
DesignRequest,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Create a minimal FastAPI app with just the REST router
|
| 21 |
+
api = FastAPI()
|
| 22 |
+
api.include_router(build_router())
|
| 23 |
+
|
| 24 |
+
# Add custom validation error handler (same as in app.py)
|
| 25 |
+
@api.exception_handler(RequestValidationError)
|
| 26 |
+
async def validation_exception_handler(
|
| 27 |
+
request,
|
| 28 |
+
exc: RequestValidationError,
|
| 29 |
+
) -> JSONResponse:
|
| 30 |
+
"""Handle Pydantic validation errors with user-friendly messages."""
|
| 31 |
+
return JSONResponse(
|
| 32 |
+
status_code=422,
|
| 33 |
+
content=_format_validation_error(exc),
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
client = TestClient(api)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class TestValidationErrors:
|
| 40 |
+
"""Test that validation errors are user-friendly."""
|
| 41 |
+
|
| 42 |
+
def test_missing_crop_name_error(self):
|
| 43 |
+
"""Test that missing crop_name returns a clear, actionable error."""
|
| 44 |
+
payload = {
|
| 45 |
+
"farm": {
|
| 46 |
+
"name": "Test Farm",
|
| 47 |
+
"location": {
|
| 48 |
+
"address": "123 Main St",
|
| 49 |
+
"latitude": 19.999,
|
| 50 |
+
"longitude": 73.793
|
| 51 |
+
},
|
| 52 |
+
"size": {
|
| 53 |
+
"value": 456,
|
| 54 |
+
"unit": "hectare"
|
| 55 |
+
}
|
| 56 |
+
# Missing crop_name
|
| 57 |
+
},
|
| 58 |
+
"plots": [
|
| 59 |
+
{
|
| 60 |
+
"plot_id": "plot-1",
|
| 61 |
+
"name": "Plot 1",
|
| 62 |
+
"boundaries": [
|
| 63 |
+
{"latitude": 19.291, "longitude": 73.654},
|
| 64 |
+
{"latitude": 19.289, "longitude": 73.654},
|
| 65 |
+
{"latitude": 19.292, "longitude": 73.659}
|
| 66 |
+
]
|
| 67 |
+
}
|
| 68 |
+
],
|
| 69 |
+
"water_sources": [
|
| 70 |
+
{
|
| 71 |
+
"water_source_id": "ws-1",
|
| 72 |
+
"type": "Motor",
|
| 73 |
+
"name": "Water Source 1",
|
| 74 |
+
"location": {"latitude": 19.257, "longitude": 73.706}
|
| 75 |
+
}
|
| 76 |
+
]
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
response = client.post("/rest/v1/design", json=payload)
|
| 80 |
+
|
| 81 |
+
# Should return 422 validation error
|
| 82 |
+
assert response.status_code == 422
|
| 83 |
+
data = response.json()
|
| 84 |
+
|
| 85 |
+
# Check that response has user-friendly structure
|
| 86 |
+
assert "status" in data
|
| 87 |
+
assert data["status"] == "validation_error"
|
| 88 |
+
assert "message" in data
|
| 89 |
+
assert "errors" in data
|
| 90 |
+
|
| 91 |
+
# Check that error mentions crop_name and provides helpful guidance
|
| 92 |
+
error_messages = [err["message"] for err in data["errors"]]
|
| 93 |
+
assert any("crop" in msg.lower() for msg in error_messages), \
|
| 94 |
+
f"Expected crop-related error, got: {error_messages}"
|
| 95 |
+
assert any("tomato" in msg.lower() or "lettuce" in msg.lower() or "generic" in msg.lower()
|
| 96 |
+
for msg in error_messages), \
|
| 97 |
+
f"Expected crop examples in error message, got: {error_messages}"
|
| 98 |
+
|
| 99 |
+
def test_missing_plots_error(self):
|
| 100 |
+
"""Test that missing plots array returns clear error."""
|
| 101 |
+
payload = {
|
| 102 |
+
"farm": {
|
| 103 |
+
"name": "Test Farm",
|
| 104 |
+
"location": {
|
| 105 |
+
"address": "123 Main St",
|
| 106 |
+
"latitude": 19.999,
|
| 107 |
+
"longitude": 73.793
|
| 108 |
+
},
|
| 109 |
+
"crop_name": "Tomato"
|
| 110 |
+
},
|
| 111 |
+
# Missing plots
|
| 112 |
+
"water_sources": [
|
| 113 |
+
{
|
| 114 |
+
"water_source_id": "ws-1",
|
| 115 |
+
"type": "Motor",
|
| 116 |
+
"name": "Water Source 1",
|
| 117 |
+
"location": {"latitude": 19.257, "longitude": 73.706}
|
| 118 |
+
}
|
| 119 |
+
]
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
response = client.post("/rest/v1/design", json=payload)
|
| 123 |
+
assert response.status_code == 422
|
| 124 |
+
data = response.json()
|
| 125 |
+
|
| 126 |
+
assert data["status"] == "validation_error"
|
| 127 |
+
error_messages = [err["message"] for err in data["errors"]]
|
| 128 |
+
assert any("plot" in msg.lower() for msg in error_messages), \
|
| 129 |
+
f"Expected plots-related error, got: {error_messages}"
|
| 130 |
+
|
| 131 |
+
def test_missing_water_sources_error(self):
|
| 132 |
+
"""Test that missing water sources returns clear error."""
|
| 133 |
+
payload = {
|
| 134 |
+
"farm": {
|
| 135 |
+
"name": "Test Farm",
|
| 136 |
+
"location": {
|
| 137 |
+
"address": "123 Main St",
|
| 138 |
+
"latitude": 19.999,
|
| 139 |
+
"longitude": 73.793
|
| 140 |
+
},
|
| 141 |
+
"crop_name": "Tomato"
|
| 142 |
+
},
|
| 143 |
+
"plots": [
|
| 144 |
+
{
|
| 145 |
+
"plot_id": "plot-1",
|
| 146 |
+
"name": "Plot 1",
|
| 147 |
+
"boundaries": [
|
| 148 |
+
{"latitude": 19.291, "longitude": 73.654},
|
| 149 |
+
{"latitude": 19.289, "longitude": 73.654},
|
| 150 |
+
{"latitude": 19.292, "longitude": 73.659}
|
| 151 |
+
]
|
| 152 |
+
}
|
| 153 |
+
]
|
| 154 |
+
# Missing water_sources
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
response = client.post("/rest/v1/design", json=payload)
|
| 158 |
+
assert response.status_code == 422
|
| 159 |
+
data = response.json()
|
| 160 |
+
|
| 161 |
+
assert data["status"] == "validation_error"
|
| 162 |
+
error_messages = [err["message"] for err in data["errors"]]
|
| 163 |
+
assert any("water_source" in msg.lower() or "pump" in msg.lower()
|
| 164 |
+
for msg in error_messages), \
|
| 165 |
+
f"Expected water_sources-related error, got: {error_messages}"
|
| 166 |
+
|
| 167 |
+
def test_valid_request_succeeds(self):
|
| 168 |
+
"""Test that a valid request succeeds."""
|
| 169 |
+
payload = {
|
| 170 |
+
"farm": {
|
| 171 |
+
"name": "Test Farm",
|
| 172 |
+
"location": {
|
| 173 |
+
"address": "123 Main St",
|
| 174 |
+
"latitude": 19.999,
|
| 175 |
+
"longitude": 73.793
|
| 176 |
+
},
|
| 177 |
+
"size": {
|
| 178 |
+
"value": 10,
|
| 179 |
+
"unit": "acre"
|
| 180 |
+
},
|
| 181 |
+
"crop_name": "Tomato"
|
| 182 |
+
},
|
| 183 |
+
"plots": [
|
| 184 |
+
{
|
| 185 |
+
"plot_id": "plot-1",
|
| 186 |
+
"name": "Plot 1",
|
| 187 |
+
"boundaries": [
|
| 188 |
+
{"latitude": 19.291, "longitude": 73.654},
|
| 189 |
+
{"latitude": 19.289, "longitude": 73.654},
|
| 190 |
+
{"latitude": 19.292, "longitude": 73.659}
|
| 191 |
+
]
|
| 192 |
+
}
|
| 193 |
+
],
|
| 194 |
+
"water_sources": [
|
| 195 |
+
{
|
| 196 |
+
"water_source_id": "ws-1",
|
| 197 |
+
"type": "Motor",
|
| 198 |
+
"name": "Water Source 1",
|
| 199 |
+
"location": {"latitude": 19.257, "longitude": 73.706}
|
| 200 |
+
}
|
| 201 |
+
],
|
| 202 |
+
"design_type": "centralized"
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
response = client.post("/rest/v1/design", json=payload)
|
| 206 |
+
|
| 207 |
+
# Should succeed
|
| 208 |
+
assert response.status_code == 200
|
| 209 |
+
data = response.json()
|
| 210 |
+
|
| 211 |
+
# Check response structure
|
| 212 |
+
assert "design_summary" in data
|
| 213 |
+
assert "bom" in data
|
| 214 |
+
assert "geojson" in data
|
| 215 |
+
|
| 216 |
+
def test_error_response_format(self):
|
| 217 |
+
"""Test that error response follows expected format."""
|
| 218 |
+
payload = {
|
| 219 |
+
"farm": {
|
| 220 |
+
"name": "Test Farm",
|
| 221 |
+
"location": {
|
| 222 |
+
"address": "123 Main St",
|
| 223 |
+
"latitude": 19.999,
|
| 224 |
+
"longitude": 73.793
|
| 225 |
+
}
|
| 226 |
+
# Missing crop_name
|
| 227 |
+
},
|
| 228 |
+
"plots": [
|
| 229 |
+
{
|
| 230 |
+
"plot_id": "plot-1",
|
| 231 |
+
"boundaries": [
|
| 232 |
+
{"latitude": 19.291, "longitude": 73.654},
|
| 233 |
+
{"latitude": 19.289, "longitude": 73.654},
|
| 234 |
+
{"latitude": 19.292, "longitude": 73.659}
|
| 235 |
+
]
|
| 236 |
+
}
|
| 237 |
+
],
|
| 238 |
+
"water_sources": [
|
| 239 |
+
{
|
| 240 |
+
"water_source_id": "ws-1",
|
| 241 |
+
"location": {"latitude": 19.257, "longitude": 73.706}
|
| 242 |
+
}
|
| 243 |
+
]
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
response = client.post("/rest/v1/design", json=payload)
|
| 247 |
+
data = response.json()
|
| 248 |
+
|
| 249 |
+
# Verify error structure
|
| 250 |
+
assert isinstance(data["errors"], list)
|
| 251 |
+
for error in data["errors"]:
|
| 252 |
+
assert "field" in error, "Error should have 'field' key"
|
| 253 |
+
assert "message" in error, "Error should have 'message' key"
|
| 254 |
+
assert isinstance(error["field"], str)
|
| 255 |
+
assert isinstance(error["message"], str)
|