[Racket] Ferti hydroponic nutrient solver, redux.
NNLS module now calculates Ferti recipe for a given day.
services/nnls.rkt
@@ -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))))))))