farm-layout-model / DESIGN_LOGIC.md
spacedout-bits's picture
Change 1: Implement global lateral orientation with compute_farm_axis
8050706
# Drip Irrigation Design Logic & Accuracy Guide
## Architecture Overview
The design pipeline has **3 independent but sequential stages**:
```
Input (farm boundary + crop zones + pump)
↓
[1] VALVE PLACEMENT ENGINE (valve_engine.py)
β€’ Decides: how many valves, where, why
↓
[2] DRIP LAYOUT GENERATOR (drip_engine.py)
β€’ For each valve zone: generate main + laterals
↓
[3] BOM & SUMMARY (drip_engine.py)
β€’ Compute costs, material counts, flow rates
↓
Output (GeoJSON with valves, zones, drips, BOM)
```
Each stage can be improved independently. This doc explains the logic, current assumptions, and where accuracy hurts most.
---
## Stage 1: Valve Placement (THE MOST CRITICAL STAGE)
### Decision Matrix (4-layer hierarchy)
When to split the farm into multiple zones? The engine applies these rules **in order**, taking the **maximum** zone count that satisfies all constraints:
```python
num_zones = max(
num_zones_capacity, # [1] Pump can't deliver all emitter demand at once
num_zones_crop, # [2] Different crops in different zones
num_zones_area, # [3] Area-based minimum (crop density floor)
num_zones_topo, # [4] Elevation split
)
```
### Layer 1: Capacity Constraint
**Logic:**
```python
total_emitter_flow_lph = sum(area_m2 * emitter_density * emitter_flow_lph per crop)
pump_flow_lph = HP_TO_LPH[pump_hp] # lookup table
num_zones_capacity = ceil(total_emitter_flow_lph / pump_flow_lph)
```
**Current assumptions:**
- Emitter density derived from `lateral_spacing Γ— emitter_spacing` (see `CROP_FLOW_PARAMS` in `valve_engine.py:47-54`)
- Tomato: 0.5m rows, 0.3m emitter spacing β†’ 4.17 emitters/mΒ²
- Lettuce: 0.4m rows, 0.2m spacing β†’ 12.5 emitters/mΒ² (intensive)
- Orchard: 2.0m rows, 1.0m spacing β†’ 0.5 emitters/mΒ² (sparse)
- Pump HP β†’ L/h curve is linear interpolation between discrete values (`HP_TO_LPH` dict)
**Accuracy issues:**
- **Emitter density is naive**: assumes rows are perpendicular to flow, which isn't always true on irregular plots.
- **Pump curve is a lookup**: real pumps have pressure-dependent flow; we ignore head loss over long laterals.
- **No derating for pressure drop**: a 200m lateral with 4.17 emitters/mΒ² may have 30%+ flow drop by the end; we ignore this.
**When to improve:**
- If you're seeing valve zones with highly uneven emitter counts, increase `CROP_FLOW_PARAMS[crop]["emitter_density_m2"]` for intensive crops.
- If the pump can't deliver enough flow, you likely **underestimated density**.
### Layer 2: Crop Type Constraint
**Logic:**
```python
num_zones_crop = len(set(z["crop"] for z in crop_zones))
```
**Current behavior:**
- Each **unique crop name** gets its own zone (minimum).
- Example: if you have tomato in plot_1 and lettuce in plot_2, you'll get β‰₯2 zones.
**Accuracy issues:**
- This is **exact** β€” no assumptions.
- But it forces fragmentation if you have many small plots with different crops, even if their water needs are similar.
**When to improve:**
- If you want to group similar crops (e.g., tomato + pepper both ~0.5m spacing), add a crop "family" system.
### Layer 3: Area-Density Floor (THE MOST OPINIONATED)
**Logic:**
```python
density_valves_per_ha = VALVE_DENSITY_PER_HA[primary_crop] # e.g., 6 for tomato
num_zones_area = ceil(farm_area_ha * density)
```
**Current densities (`VALVE_DENSITY_PER_HA`):**
```python
"tomato": 6, # ~0.17 ha/valve = 1700 mΒ² per valve
"pepper": 6,
"lettuce": 7,
"cucumber": 4,
"orchard": 2,
"generic": 5, # β‰ˆ2 valves/acre, standard default
```
**Why this matters:**
- A 1-hectare tomato field gets **at least 6 zones**, even if the pump could handle it in 1.
- This is **not** capacity-driven; it's **operational best practice**: narrower zones β†’ shorter laterals β†’ less pressure drop β†’ more uniform water.
**Current assumption:**
- Rule of thumb from FAO irrigation guides: 1 valve per 1500–2000 mΒ² for row crops.
- But these are **empirical, not physics-based**.
**Accuracy issues:**
- **These numbers are guessed**, not calibrated to your farms.
- Real farms might benefit from 3 valves/ha or 10 valves/ha depending on topography, soil, crop sensitivity.
- **No feedback loop**: the engine doesn't learn from actual irrigation records.
**When to improve (HIGH IMPACT):**
1. **Collect data**: run 5-10 farms and measure which density gives the most uniform soil moisture or yield.
2. **Stratify by local conditions**: clay soils may tolerate coarser zones (lower density); sandy soils need more (higher density).
3. **Make it configurable**: add `valve_density_override` to the API so users can tune.
### Layer 4: Topography Constraint
**Logic:**
```python
should_split_topo = (max_elevation_m - min_elevation_m) > ELEVATION_DELTA_THRESHOLD_M # 5m
if should_split_topo:
num_zones_required += 1 # Just adds 1 zone; not sophisticated
```
**Current assumption:**
- If elevation differs by >5m across the farm, you need separate high/low zones.
- This avoids excessive pressure imbalance (5m β‰ˆ 0.5 bar).
**Accuracy issues:**
- **Very coarse**: if your field has 20m elevation span, splitting into high/low is crude.
- **No secondary valve placement logic**: we don't intelligently place the "high zone" valve uphill.
- **Ignores soil variability**: even flat farms have water-retention differences.
**When to improve:**
- Implement a real topographic split: use Voronoi diagram based on elevation contours.
---
## Stage 2: Drip Layout Generation (PURELY GEOMETRIC)
### Main Line Selection
**Logic:**
```python
edges = polygon.exterior edges (between consecutive vertices)
edge_lengths = [distance(v1, v2) for each edge]
main_idx = argmax(edge_lengths) # or argmin if main_line_edge="shortest"
main_line = edges[main_idx]
```
**Current assumption:**
- The longest edge is the best place for the main pipe (supplies all laterals perpendicular to it).
- This works well for **rectangular fields**; poor for **irregular/triangular** fields.
**Accuracy issues:**
- **Doesn't optimize for minimal piping**: placing the main along the longest edge doesn't minimize total pipe (main + laterals).
- **Doesn't consider pump location**: if pump is far from the longest edge, the main should be closer to the pump.
**When to improve:**
- Implement a "cost-optimal main placement" algorithm:
```
for each edge:
cost = main_length + avg_lateral_length_if_main_on_this_edge
best_edge = min(cost)
```
- Result: main placement adapts to both field shape and pump location.
### Lateral Generation & Clipping
**Logic:**
```python
# Generate parallel lines perpendicular to main, spaced at crop-specific intervals
spacing = params["lateral_spacing_m"] # e.g., 0.5m for tomato
num_laterals = ceil(main_length / spacing)
for i in range(num_laterals):
point_on_main = main.interpolate(i * spacing)
lateral = perpendicular_line(point_on_main, direction=perp)
clipped_lateral = lateral.intersection(polygon) # Clip to field boundary
```
**Current assumption:**
- Equally-spaced laterals perpendicular to the main.
- Works perfectly for **rectangles**, OK for **gentle polygons**, poor for **irregular shapes** (some laterals clip to very short lengths, wasting water).
**Accuracy issues:**
- **Uneven emitter distribution**: clipping can leave some laterals very short (e.g., 10m) while others are 100m, causing huge flow imbalance.
- **Dead zones**: if the field is very irregular (e.g., L-shaped), the corners near the short clipped laterals will be under-irrigated.
**When to improve:**
1. **Adaptive lateral spacing**: instead of fixed spacing, space laterals so each has ~similar length (within 20% variation).
2. **Lateral grouping**: group short laterals and feed them from a single branch main, not from the primary main.
3. **Visualization**: highlight in the output which laterals are "problematic" (too short, too long).
### Headland Buffer
**Logic:**
```python
buffered_polygon = polygon.buffer(-headland_buffer_m)
```
**Current assumption:**
- Shrink the field inward by a fixed distance.
- Avoids putting drip on field edges (where machinery turns).
**Accuracy issues:**
- **All headland is the same**: real farms have varied headland: one side might be a road (no drip), other side a boundary (need drip).
- **No input for headland shape**: assume rectangular; doesn't account for irregular field edges.
**When to improve:**
- Accept a **per-edge headland map**: `{ "north": 1.5, "east": 1.0, "south": 2.0, "west": 0.5 }`.
---
## Stage 3: BOM & Summary (COST ESTIMATION)
### Current Cost Model
**Logic:**
```python
main_pipe_cost = main_length_m * PIPE_COSTS["main_line_16mm"]
drip_tape_cost = drip_length_m * PIPE_COSTS["drip_tape_16mm"]
emitter_cost = emitter_count * PIPE_COSTS["emitter_inline"]
valve_cost = num_valves * 15 # Fixed $ per valve
total_cost = main + drip + emitter + valve
```
**Current assumption:**
- All pipes are 16mm.
- All emitters are $0.08 each.
- All valves are $15 each.
- These are **order-of-magnitude guesses** from generic sourcing.
**Accuracy issues:**
- **No regional pricing**: India vs. Kenya vs. Brazil have very different pipe costs.
- **No volume discounts**: a 1-hectare vs. 100-hectare farm have different unit costs.
- **No installation labor**: only materials, no digging, trenching, connection labor.
- **No waste allowance**: we assume 100% of pipe is used; real installations have ~5-10% waste.
**When to improve:**
1. **Collect regional pricing data**: build a cost table by country/region.
2. **Add waste factor**: multiply final quantities by 1.10 (10% waste).
3. **Surface cost per hectare & cost per emitter**: help users compare.
---
## Summary: Accuracy Levers (in order of impact)
| Lever | Current | Improvement | Impact |
|-------|---------|-------------|--------|
| **Valve density (Layer 3)** | Fixed 6 valves/ha for tomato | Calibrate to local data + user input | πŸ”΄ HIGHEST β€” wrong density = under/over-designed |
| **Lateral spacing uniformity** | Clipped equally-spaced lines | Adaptive spacing by target lateral length | πŸ”΄ HIGH β€” short laterals = dead zones |
| **Main line placement** | Longest edge | Cost-optimal (main + laterals) | 🟑 MEDIUM β€” 10-20% improvement typical |
| **Pump head loss** | Ignored | Model pressure drop over lateral length | 🟑 MEDIUM β€” matters >100m laterals |
| **Headland map** | Fixed inward buffer | Per-edge buffer (N/E/S/W) | 🟑 MEDIUM β€” irregular fields |
| **Cost calibration** | Guessed $ per unit | Regional/seasonal sourcing data | 🟑 MEDIUM β€” users need local confidence |
| **Topography split** | Elevation delta > 5m β†’ +1 zone | Voronoi split + contour-aware placement | 🟒 LOW β€” only needed for >10m deltas |
---
## How to Test & Validate Your Improvements
### 1. Unit Tests
- Each improvement should have a test in `test_drip_engine.py` or `test_valve_engine.py`.
- Example: if you improve lateral spacing uniformity, add a test that checks `max(lateral_lengths) / min(lateral_lengths) < 1.5` (no more than 50% variation).
### 2. Visual Inspection
- Use the Gradio UI or REST API to generate designs for **known farms**.
- Compare your design to hand-drawn designs or existing irrigation schemes.
- Look for:
- Balanced lateral lengths βœ“
- Valves placed logically βœ“
- No huge dead zones βœ“
### 3. Field Metrics
- Once you have real farm data, collect:
- Soil moisture at 0-30cm depth (3-5 spots per field)
- Before / after irrigation water use
- Crop yield uniformity
- Correlate with design metrics:
- `avg_lateral_length`, `lateral_length_std_dev`
- `emitters_per_zone`
- Compare uniform designs vs. non-uniform designs
### 4. Sensitivity Analysis
- For each improvement, run the design with:
- `-10% parameter` β†’ output
- `nominal parameter` β†’ output
- `+10% parameter` β†’ output
- Measure sensitivity: `(output_+10% - output_-10%) / output_nominal`
- Example: "Reducing valve density from 6β†’5.4 /ha increases avg_lateral_length by 8%; this is acceptable for loose clay soils but risky for sandy soils."
---
## Next Steps
1. **Identify your priority**:
- Cost accuracy? β†’ Fix regional pricing (Step 3).
- Design uniformity? β†’ Adaptive lateral spacing (Step 2).
- Operational realism? β†’ Tune valve density + add headland map (Step 1).
2. **Collect data**: 5-10 real farms (geometry, crops, pump, existing irrigation scheme if any).
3. **Iterate**:
- Improve one lever at a time.
- Validate against your test farms.
- Document assumptions.
4. **Share improvements**: push back to the repo so the next user benefits.