[Racket] Ferti hydroponic nutrient solver, redux.
Update NNLS unit tests.
services/nnls.rkt
@@ -180,27 +180,161 @@
180
180
181
181
(define test-date "2025-01-01")
182
182
183
Removed:
(run-tests (test-suite "Nutrient measurement model"
184
Removed:
#:before (λ ()
185
Removed:
(connect! #:path 'memory)
186
Removed:
;; (connect! #:path "test.sqlite3")
187
Removed:
(migrate-all!)
183
Added:
(run-tests (test-suite "NNLS"
184
Added:
(test-case "Build fertilizer product matrix"
185
Added:
(connect! #:path 'memory)
186
Added:
(migrate-all!)
188
187
189
Removed:
(define nitrogen (create-nutrient! "Nitrogen" "N"))
190
Removed:
(define phosphorus (create-nutrient! "Phosphorus" "P"))
188
Added:
(define n1 (create-nutrient! "N1" "N1"))
189
Added:
(define n2 (create-nutrient! "N2" "N2"))
190
Added:
(define nutrients (list n1 n2))
191
191
192
Removed:
(create-nutrient-measurement! test-date (hash nitrogen 0 phosphorus 0))
193
Removed:
(create-nutrient-target! test-date (hash nitrogen 100 phosphorus 50))
192
Added:
(define f1 (create-fertilizer-product! "F1" "F1" (hash n1 10 n2 20)))
193
Added:
(define f2 (create-fertilizer-product! "F2" "F2" (hash n1 30 n2 5)))
194
Added:
(define fertilizers (list f1 f2))
194
195
195
Removed:
(create-fertilizer-product! "Nitrogen" "King Nitrogen" (hash nitrogen 100))
196
Removed:
(create-fertilizer-product! "Phosphorus"
197
Removed:
"Phosphorescent Baboon"
198
Removed:
(hash nitrogen 10 phosphorus 100))
199
Removed:
(create-fertilizer-product! "Diluted phosphorus"
200
Removed:
"John's Phosphorus"
201
Removed:
(hash nitrogen 3 phosphorus 30)))
202
Removed:
#:after (λ () (disconnect!))
196
Added:
(define matrix (get-fertilizer-product-matrix nutrients fertilizers))
203
197
204
Removed:
(test-case "Solve for NNLS"
205
Removed:
(displayln (format "Final solution for fertilizers is combination ~a"
206
Removed:
(find-ferti-recipe)))))))
198
Added:
(check-= (matrix-ref matrix 0 0) 10 0 "N1 in F1")
199
Added:
(check-= (matrix-ref matrix 0 1) 30 0 "N1 in F2")
200
Added:
(check-= (matrix-ref matrix 1 0) 20 0 "N2 in F1")
201
Added:
(check-= (matrix-ref matrix 1 1) 5 0 "N2 in F2")
202
Added:
203
Added:
(disconnect!))
204
Added:
205
Added:
(test-case "Single nutrient, single fertilizer"
206
Added:
(define A (matrix [[2]]))
207
Added:
(define y (col-matrix [10]))
208
Added:
(define ε 1e-6)
209
Added:
210
Added:
(define result (lawson-hanson-1974 A y ε))
211
Added:
212
Added:
(check-= (matrix-ref result 0 0) 5.0 ε "Should give x = 5 since 2*5 = 10"))
213
Added:
214
Added:
(test-case "Two variables, known solution"
215
Added:
(define A (matrix [[1 0] [0 1]]))
216
Added:
(define y (col-matrix [3 4]))
217
Added:
(define epsilon 1e-6)
218
Added:
219
Added:
(define result (lawson-hanson-1974 A y epsilon))
220
Added:
221
Added:
(check-= (matrix-ref result 0 0) 3.0 epsilon "x1 should be 3")
222
Added:
(check-= (matrix-ref result 1 0) 4.0 epsilon "x2 should be 4"))
223
Added:
224
Added:
(test-case "Overdetermined system"
225
Added:
(define A (matrix [[1 1] [2 1] [1 2]]))
226
Added:
(define y (col-matrix [3 5 5]))
227
Added:
(define ε 1e-4)
228
Added:
229
Added:
(define result (lawson-hanson-1974 A y ε))
230
Added:
231
Added:
;; Solution should be approximately [1.636, 1.636] (least squares fit)
232
Added:
(check-= (matrix-ref result 0 0) 1.636 0.01 "x1 approximately 1.636")
233
Added:
(check-= (matrix-ref result 1 0) 1.636 0.01 "x2 approximately 1.636"))
234
Added:
235
Added:
(test-case "Non-negativity enforcement"
236
Added:
(define A (matrix [[1 -1] [1 1]]))
237
Added:
(define y (col-matrix [1 3]))
238
Added:
(define epsilon 1e-6)
239
Added:
240
Added:
(define result (lawson-hanson-1974 A y epsilon))
241
Added:
242
Added:
;; All results should be non-negative
243
Added:
(check-true (>= (matrix-ref result 0 0) 0) "x1 should be non-negative")
244
Added:
(check-true (>= (matrix-ref result 1 0) 0) "x2 should be non-negative"))
245
Added:
246
Added:
(test-case "Zero target"
247
Added:
(define A (matrix [[1 2] [3 4]]))
248
Added:
(define y (col-matrix [0 0]))
249
Added:
(define ε 1e-6)
250
Added:
251
Added:
(define result (lawson-hanson-1974 A y ε))
252
Added:
253
Added:
(check-= (matrix-ref result 0 0) 0.0 ε "x1 should be 0")
254
Added:
(check-= (matrix-ref result 1 0) 0.0 ε "x2 should be 0"))
255
Added:
256
Added:
(test-case "Ferti recipe"
257
Added:
(connect! #:path 'memory)
258
Added:
(migrate-all!)
259
Added:
260
Added:
(define nitrogen (create-nutrient! "Nitrogen" "N"))
261
Added:
(define phosphorus (create-nutrient! "Phosphorus" "P"))
262
Added:
263
Added:
(create-nutrient-measurement! test-date (hash nitrogen 0 phosphorus 0))
264
Added:
(create-nutrient-target! test-date (hash nitrogen 100 phosphorus 50))
265
Added:
266
Added:
(create-fertilizer-product! "Nitrogen" "King Nitrogen" (hash nitrogen 100))
267
Added:
(create-fertilizer-product! "Phosphorus"
268
Added:
"Phosphorescent Baboon"
269
Added:
(hash nitrogen 10 phosphorus 100))
270
Added:
(create-fertilizer-product! "Diluted phosphorus"
271
Added:
"John's Phosphorus"
272
Added:
(hash nitrogen 3 phosphorus 30))
273
Added:
274
Added:
(define recipe (find-ferti-recipe))
275
Added:
276
Added:
(check-equal? (length recipe) 3 "Should have 3 fertilizer products")
277
Added:
278
Added:
(for ([pair recipe])
279
Added:
(check-true (>= (cdr pair) 0) "Fertilizer quantity should be non-negative"))
280
Added:
281
Added:
(disconnect!))
282
Added:
283
Added:
;; Test deficit calculation edge cases
284
Added:
(test-case "Deficit calculation with missing data"
285
Added:
(connect! #:path 'memory)
286
Added:
(migrate-all!)
287
Added:
288
Added:
(define n (create-nutrient! "TestNutrient" "TN"))
289
Added:
290
Added:
;; No measurement, no target
291
Added:
(check-false (get-latest-nutrient-measurement-value n))
292
Added:
(check-false (get-latest-nutrient-target-value n))
293
Added:
294
Added:
;; Add only target
295
Added:
(create-nutrient-target! test-date (hash n 100))
296
Added:
(check-= (get-latest-nutrient-target-value n) 100 0)
297
Added:
298
Added:
;; Add measurement
299
Added:
(create-nutrient-measurement! test-date (hash n 50))
300
Added:
(define measured (get-latest-nutrient-measurement-value n))
301
Added:
(define targeted (get-latest-nutrient-target-value n))
302
Added:
303
Added:
;; Deficit should be 100% (from 50 to 100)
304
Added:
(define deficit (* 100 (/ (- targeted measured) measured)))
305
Added:
(check-= deficit 100.0 0.01 "Deficit should be 100%")
306
Added:
307
Added:
(disconnect!))
308
Added:
309
Added:
;; Test recipe with realistic constraints
310
Added:
(test-case "Recipe calculation with real-world scenario"
311
Added:
(connect! #:path 'memory)
312
Added:
(migrate-all!)
313
Added:
314
Added:
(define n (create-nutrient! "N" "N"))
315
Added:
(define p (create-nutrient! "P" "P"))
316
Added:
(define k (create-nutrient! "K" "K"))
317
Added:
318
Added:
;; Current levels
319
Added:
(create-nutrient-measurement! test-date (hash n 50 p 10 k 100))
320
Added:
321
Added:
;; Target levels
322
Added:
(create-nutrient-target! test-date (hash n 150 p 30 k 200))
323
Added:
324
Added:
;; Fertilizers with different NPK ratios
325
Added:
(create-fertilizer-product! "" "Balanced" (hash n 100 p 100 k 100))
326
Added:
(create-fertilizer-product! "Nitrogen blend" "High-N" (hash n 200 p 50 k 50))
327
Added:
(create-fertilizer-product! "Phosphorus blend" "High-P" (hash n 50 p 200 k 50))
328
Added:
(create-fertilizer-product! "Potassium blend" "High-K" (hash n 50 p 50 k 200))
329
Added:
330
Added:
(define recipe (find-ferti-recipe))
331
Added:
332
Added:
(check-equal? (length recipe) 4 "Should have 4 fertilizer options")
333
Added:
334
Added:
;; Verify solution is non-negative
335
Added:
(for ([pair recipe])
336
Added:
(check-true (>= (cdr pair) 0)
337
Added:
(format "~a quantity must be non-negative"
338
Added:
(fertilizer-name (car pair)))))
339
Added:
340
Added:
(disconnect!)))))