NNLS module now calculates Ferti recipe for a given day.

Commit
71c22e872b1b4c24cf7ce0ea86451930cc9cbd83
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
services/nnls.rkt
index abc76fe2..a5800f36 100644..100644
@@ -5,30 +5,41 @@
5 5 (require math/array
6 6 math/matrix
7 7 "../models/nutrient.rkt"
8 Added: "../models/fertilizer-product.rkt"
8 9 "../models/nutrient-measurement.rkt"
9 Removed: "../models/nutrient-target.rkt"
10 Removed: "../models/fertilizer-product.rkt")
10 Added: "../models/crop-rotation.rkt"
11 Added: "../models/crop-requirement.rkt")
11 12
12 Removed: (define (find-ferti-recipe)
13 Added: (define (find-ferti-recipe date-string)
13 14 (define fertilizers (get-fertilizer-products))
14 Removed: (define solution-array (solve-nnls fertilizers))
15 Removed: (for/list ([fertilizer (in-list fertilizers)]
15 Added: (define measurement-values
16 Added: (nutrient-measurement-values
17 Added: (or (get-nutrient-measurement #:date date-string)
18 Added: (error 'nnls
19 Added: "Can't compute the Ferti recipe (missing nutrient measurement for ~a)"
20 Added: date-string))))
21 Added: (define rotation-values
22 Added: (average-crop-requirement-nutrient-values
23 Added: (crop-rotation-requirements
24 Added: (or
25 Added: (get-crop-rotation #:date date-string)
26 Added: (error 'nnls "Can't compute the Ferti recipe (missing crop rotation for ~a)" date-string)))))
27 Added: (define solution-array (solve-nnls fertilizers measurement-values rotation-values))
28 Added: (for/hash ([fertilizer (in-list fertilizers)]
16 29 [quantity (in-array solution-array)])
17 Removed: (cons fertilizer quantity)))
30 Added: (values fertilizer quantity)))
18 31
19 Removed: (define (solve-nnls fertilizers)
32 Added: (define (solve-nnls fertilizers measurement-values rotation-values)
20 33 (define nutrients (get-nutrients))
21 34 (define fertilizer-product-matrix (get-fertilizer-product-matrix nutrients fertilizers))
22 35 (define deficits
23 36 (->col-matrix (for/list ([n nutrients])
24 Removed: (define latest-measurement (get-latest-nutrient-measurement-value n))
25 Removed: (define latest-target (get-latest-nutrient-target-value n))
37 Added: (define measured (hash-ref measurement-values n 0))
38 Added: (define required (hash-ref rotation-values n 0))
26 39 (define deficit
27 Removed: (cond
28 Removed: [(false? latest-target) 0]
29 Removed: [(or (false? latest-measurement) (zero? latest-measurement)) latest-target]
30 Removed: [(and (number? latest-measurement) (number? latest-target))
31 Removed: (* 100 (/ (- latest-target latest-measurement) latest-measurement))]))
40 Added: (if (zero? required)
41 Added: 0
42 Added: (* 100 (/ (- required measured) required))))
32 43 deficit)))
33 44 (define error-threshold 10e-4)
34 45 (lawson-hanson-1974 fertilizer-product-matrix deficits error-threshold))
@@ -178,19 +189,23 @@
178 189 "../db/conn.rkt"
179 190 "../db/migrations.rkt")
180 191
181 Removed: (define test-date "2025-01-01")
182 Removed:
183 192 (run-tests (test-suite "NNLS"
193 Added: #:before (λ ()
194 Added: (connect! #:path 'memory)
195 Added: (migrate-all!)
196 Added: (create-nutrient! "Nitrogen" "Azote" "N")
197 Added: (create-nutrient! "Phosphorus" "Phosphore" "P")
198 Added: (create-nutrient! "Potassium" "Potassium" "K"))
199 Added: #:after (λ () (disconnect!))
200 Added:
184 201 (test-case "Build fertilizer product matrix"
185 Removed: (connect! #:path 'memory)
186 Removed: (migrate-all!)
202 Added: (define n (get-nutrient #:name "Nitrogen"))
203 Added: (define p (get-nutrient #:name "Phosphorus"))
187 204
188 Removed: (define n1 (create-nutrient! "N1" "" "N1"))
189 Removed: (define n2 (create-nutrient! "N2" "" "N2"))
190 Removed: (define nutrients (list n1 n2))
205 Added: (define nutrients (list n p))
191 206
192 Removed: (define f1 (create-fertilizer-product! "F1" "F1" (hash n1 10 n2 20)))
193 Removed: (define f2 (create-fertilizer-product! "F2" "F2" (hash n1 30 n2 5)))
207 Added: (define f1 (create-fertilizer-product! "F1" "F1" (hash n 10 p 20)))
208 Added: (define f2 (create-fertilizer-product! "F2" "F2" (hash n 30 p 5)))
194 209 (define fertilizers (list f1 f2))
195 210
196 211 (define matrix (get-fertilizer-product-matrix nutrients fertilizers))
@@ -198,10 +213,8 @@
198 213 (check-= (matrix-ref matrix 0 0) 10 0 "N1 in F1")
199 214 (check-= (matrix-ref matrix 0 1) 30 0 "N1 in F2")
200 215 (check-= (matrix-ref matrix 1 0) 20 0 "N2 in F1")
201 Removed: (check-= (matrix-ref matrix 1 1) 5 0 "N2 in F2")
216 Added: (check-= (matrix-ref matrix 1 1) 5 0 "N2 in F2"))
202 217
203 Removed: (disconnect!))
204 Removed:
205 218 (test-case "Single nutrient, single fertilizer"
206 219 (define A (matrix [[2]]))
207 220 (define y (col-matrix [10]))
@@ -254,72 +267,40 @@
254 267 (check-= (matrix-ref result 1 0) 0.0 ε "x2 should be 0"))
255 268
256 269 (test-case "Ferti recipe"
257 Removed: (connect! #:path 'memory)
258 Removed: (migrate-all!)
270 Added: (define test-date "2025-01-01")
271 Added: (define n (get-nutrient #:name "Nitrogen"))
272 Added: (define p (get-nutrient #:name "Phosphorus"))
273 Added: (define k (get-nutrient #:name "Potassium"))
259 274
260 Removed: (define nitrogen (create-nutrient! "Nitrogen" "" "N"))
261 Removed: (define phosphorus (create-nutrient! "Phosphorus" "" "P"))
275 Added: (create-nutrient-measurement! test-date (hash n 0 p 0))
276 Added: (define test-crop-requirement
277 Added: (create-crop-requirement! "Test requirement" (hash n 100 p 50)))
278 Added: (create-crop-rotation! test-date (hash test-crop-requirement 100))
262 279
263 Removed: (create-nutrient-measurement! test-date (hash nitrogen 0 phosphorus 0))
264 Removed: (create-nutrient-target! test-date (hash nitrogen 100 phosphorus 50))
280 Added: (create-fertilizer-product! "Nitrogen" "King Nitrogen" (hash n 100))
281 Added: (create-fertilizer-product! "Phosphorus" "Phosphorescent Baboon" (hash n 10 p 100))
282 Added: (create-fertilizer-product! "Diluted phosphorus" "John's Phosphorus" (hash n 3 p 30))
265 283
266 Removed: (create-fertilizer-product! "Nitrogen" "King Nitrogen" (hash nitrogen 100))
267 Removed: (create-fertilizer-product! "Phosphorus"
268 Removed: "Phosphorescent Baboon"
269 Removed: (hash nitrogen 10 phosphorus 100))
270 Removed: (create-fertilizer-product! "Diluted phosphorus"
271 Removed: "John's Phosphorus"
272 Removed: (hash nitrogen 3 phosphorus 30))
284 Added: (define recipe (find-ferti-recipe test-date))
273 285
274 Removed: (define recipe (find-ferti-recipe))
286 Added: (check-equal? (hash-count recipe) 5 "Should have 5 fertilizer products")
275 287
276 Removed: (check-equal? (length recipe) 3 "Should have 3 fertilizer products")
288 Added: (for ([(fertilizer quantity) (in-hash recipe)])
289 Added: (check-true (>= quantity 0) "Fertilizer quantity should be non-negative")))
277 290
278 Removed: (for ([pair recipe])
279 Removed: (check-true (>= (cdr pair) 0) "Fertilizer quantity should be non-negative"))
280 Removed:
281 Removed: (disconnect!))
282 Removed:
283 Removed: ;; Test deficit calculation edge cases
284 Removed: (test-case "Deficit calculation with missing data"
285 Removed: (connect! #:path 'memory)
286 Removed: (migrate-all!)
287 Removed:
288 Removed: (define n (create-nutrient! "TestNutrient" "" "TN"))
289 Removed:
290 Removed: ;; No measurement, no target
291 Removed: (check-false (get-latest-nutrient-measurement-value n))
292 Removed: (check-false (get-latest-nutrient-target-value n))
293 Removed:
294 Removed: ;; Add only target
295 Removed: (create-nutrient-target! test-date (hash n 100))
296 Removed: (check-= (get-latest-nutrient-target-value n) 100 0)
297 Removed:
298 Removed: ;; Add measurement
299 Removed: (create-nutrient-measurement! test-date (hash n 50))
300 Removed: (define measured (get-latest-nutrient-measurement-value n))
301 Removed: (define targeted (get-latest-nutrient-target-value n))
302 Removed:
303 Removed: ;; Deficit should be 100% (from 50 to 100)
304 Removed: (define deficit (* 100 (/ (- targeted measured) measured)))
305 Removed: (check-= deficit 100.0 0.01 "Deficit should be 100%")
306 Removed:
307 Removed: (disconnect!))
308 Removed:
309 Removed: ;; Test recipe with realistic constraints
310 291 (test-case "Recipe calculation with real-world scenario"
311 Removed: (connect! #:path 'memory)
312 Removed: (migrate-all!)
292 Added: (define test-date "2025-01-02")
293 Added: (define n (get-nutrient #:name "Nitrogen"))
294 Added: (define p (get-nutrient #:name "Phosphorus"))
295 Added: (define k (get-nutrient #:name "Potassium"))
313 296
314 Removed: (define n (create-nutrient! "N" "" "N"))
315 Removed: (define p (create-nutrient! "P" "" "P"))
316 Removed: (define k (create-nutrient! "K" "" "K"))
317 Removed:
318 297 ;; Current levels
319 298 (create-nutrient-measurement! test-date (hash n 50 p 10 k 100))
320 299
321 300 ;; Target levels
322 Removed: (create-nutrient-target! test-date (hash n 150 p 30 k 200))
301 Added: (define test-crop-requirement
302 Added: (create-crop-requirement! "Test requirement 2" (hash n 150 p 30 k 200)))
303 Added: (create-crop-rotation! test-date (hash test-crop-requirement 100))
323 304
324 305 ;; Fertilizers with different NPK ratios
325 306 (create-fertilizer-product! "" "Balanced" (hash n 100 p 100 k 100))
@@ -327,14 +308,12 @@
327 308 (create-fertilizer-product! "Phosphorus blend" "High-P" (hash n 50 p 200 k 50))
328 309 (create-fertilizer-product! "Potassium blend" "High-K" (hash n 50 p 50 k 200))
329 310
330 Removed: (define recipe (find-ferti-recipe))
311 Added: (define recipe (find-ferti-recipe test-date))
331 312
332 Removed: (check-equal? (length recipe) 4 "Should have 4 fertilizer options")
313 Added: (check-equal? (hash-count recipe) 9 "Recipe should have 9 fertilizers")
333 314
334 315 ;; Verify solution is non-negative
335 Removed: (for ([pair recipe])
336 Removed: (check-true (>= (cdr pair) 0)
316 Added: (for ([(fertilizer quantity) (in-hash recipe)])
317 Added: (check-true (>= quantity 0)
337 318 (format "~a quantity must be non-negative"
338 Removed: (fertilizer-name (car pair)))))
339 Removed:
340 Removed: (disconnect!)))))
319 Added: (fertilizer-product-name fertilizer))))))))