summaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
authorMarius Peter <dev@marius-peter.com>2025-11-22 18:38:50 +0100
committerMarius Peter <dev@marius-peter.com>2025-11-22 18:38:50 +0100
commit708f0e0ba2fd7e7592ea20b1be35b7932b94f7aa (patch)
treecf0d7abcdf501dab161f36ed76763ce44de5d80e /services
parent7af921010521b1108144c85259dae4e435a78262 (diff)
Update NNLS unit tests.
Diffstat (limited to 'services')
-rw-r--r--services/nnls.rkt182
1 files changed, 158 insertions, 24 deletions
diff --git a/services/nnls.rkt b/services/nnls.rkt
index 90acb41..8fdccc8 100644
--- a/services/nnls.rkt
+++ b/services/nnls.rkt
@@ -180,27 +180,161 @@
(define test-date "2025-01-01")
- (run-tests (test-suite "Nutrient measurement model"
- #:before (λ ()
- (connect! #:path 'memory)
- ;; (connect! #:path "test.sqlite3")
- (migrate-all!)
-
- (define nitrogen (create-nutrient! "Nitrogen" "N"))
- (define phosphorus (create-nutrient! "Phosphorus" "P"))
-
- (create-nutrient-measurement! test-date (hash nitrogen 0 phosphorus 0))
- (create-nutrient-target! test-date (hash nitrogen 100 phosphorus 50))
-
- (create-fertilizer-product! "Nitrogen" "King Nitrogen" (hash nitrogen 100))
- (create-fertilizer-product! "Phosphorus"
- "Phosphorescent Baboon"
- (hash nitrogen 10 phosphorus 100))
- (create-fertilizer-product! "Diluted phosphorus"
- "John's Phosphorus"
- (hash nitrogen 3 phosphorus 30)))
- #:after (λ () (disconnect!))
-
- (test-case "Solve for NNLS"
- (displayln (format "Final solution for fertilizers is combination ~a"
- (find-ferti-recipe)))))))
+ (run-tests (test-suite "NNLS"
+ (test-case "Build fertilizer product matrix"
+ (connect! #:path 'memory)
+ (migrate-all!)
+
+ (define n1 (create-nutrient! "N1" "N1"))
+ (define n2 (create-nutrient! "N2" "N2"))
+ (define nutrients (list n1 n2))
+
+ (define f1 (create-fertilizer-product! "F1" "F1" (hash n1 10 n2 20)))
+ (define f2 (create-fertilizer-product! "F2" "F2" (hash n1 30 n2 5)))
+ (define fertilizers (list f1 f2))
+
+ (define matrix (get-fertilizer-product-matrix nutrients fertilizers))
+
+ (check-= (matrix-ref matrix 0 0) 10 0 "N1 in F1")
+ (check-= (matrix-ref matrix 0 1) 30 0 "N1 in F2")
+ (check-= (matrix-ref matrix 1 0) 20 0 "N2 in F1")
+ (check-= (matrix-ref matrix 1 1) 5 0 "N2 in F2")
+
+ (disconnect!))
+
+ (test-case "Single nutrient, single fertilizer"
+ (define A (matrix [[2]]))
+ (define y (col-matrix [10]))
+ (define ε 1e-6)
+
+ (define result (lawson-hanson-1974 A y ε))
+
+ (check-= (matrix-ref result 0 0) 5.0 ε "Should give x = 5 since 2*5 = 10"))
+
+ (test-case "Two variables, known solution"
+ (define A (matrix [[1 0] [0 1]]))
+ (define y (col-matrix [3 4]))
+ (define epsilon 1e-6)
+
+ (define result (lawson-hanson-1974 A y epsilon))
+
+ (check-= (matrix-ref result 0 0) 3.0 epsilon "x1 should be 3")
+ (check-= (matrix-ref result 1 0) 4.0 epsilon "x2 should be 4"))
+
+ (test-case "Overdetermined system"
+ (define A (matrix [[1 1] [2 1] [1 2]]))
+ (define y (col-matrix [3 5 5]))
+ (define ε 1e-4)
+
+ (define result (lawson-hanson-1974 A y ε))
+
+ ;; Solution should be approximately [1.636, 1.636] (least squares fit)
+ (check-= (matrix-ref result 0 0) 1.636 0.01 "x1 approximately 1.636")
+ (check-= (matrix-ref result 1 0) 1.636 0.01 "x2 approximately 1.636"))
+
+ (test-case "Non-negativity enforcement"
+ (define A (matrix [[1 -1] [1 1]]))
+ (define y (col-matrix [1 3]))
+ (define epsilon 1e-6)
+
+ (define result (lawson-hanson-1974 A y epsilon))
+
+ ;; All results should be non-negative
+ (check-true (>= (matrix-ref result 0 0) 0) "x1 should be non-negative")
+ (check-true (>= (matrix-ref result 1 0) 0) "x2 should be non-negative"))
+
+ (test-case "Zero target"
+ (define A (matrix [[1 2] [3 4]]))
+ (define y (col-matrix [0 0]))
+ (define ε 1e-6)
+
+ (define result (lawson-hanson-1974 A y ε))
+
+ (check-= (matrix-ref result 0 0) 0.0 ε "x1 should be 0")
+ (check-= (matrix-ref result 1 0) 0.0 ε "x2 should be 0"))
+
+ (test-case "Ferti recipe"
+ (connect! #:path 'memory)
+ (migrate-all!)
+
+ (define nitrogen (create-nutrient! "Nitrogen" "N"))
+ (define phosphorus (create-nutrient! "Phosphorus" "P"))
+
+ (create-nutrient-measurement! test-date (hash nitrogen 0 phosphorus 0))
+ (create-nutrient-target! test-date (hash nitrogen 100 phosphorus 50))
+
+ (create-fertilizer-product! "Nitrogen" "King Nitrogen" (hash nitrogen 100))
+ (create-fertilizer-product! "Phosphorus"
+ "Phosphorescent Baboon"
+ (hash nitrogen 10 phosphorus 100))
+ (create-fertilizer-product! "Diluted phosphorus"
+ "John's Phosphorus"
+ (hash nitrogen 3 phosphorus 30))
+
+ (define recipe (find-ferti-recipe))
+
+ (check-equal? (length recipe) 3 "Should have 3 fertilizer products")
+
+ (for ([pair recipe])
+ (check-true (>= (cdr pair) 0) "Fertilizer quantity should be non-negative"))
+
+ (disconnect!))
+
+ ;; Test deficit calculation edge cases
+ (test-case "Deficit calculation with missing data"
+ (connect! #:path 'memory)
+ (migrate-all!)
+
+ (define n (create-nutrient! "TestNutrient" "TN"))
+
+ ;; No measurement, no target
+ (check-false (get-latest-nutrient-measurement-value n))
+ (check-false (get-latest-nutrient-target-value n))
+
+ ;; Add only target
+ (create-nutrient-target! test-date (hash n 100))
+ (check-= (get-latest-nutrient-target-value n) 100 0)
+
+ ;; Add measurement
+ (create-nutrient-measurement! test-date (hash n 50))
+ (define measured (get-latest-nutrient-measurement-value n))
+ (define targeted (get-latest-nutrient-target-value n))
+
+ ;; Deficit should be 100% (from 50 to 100)
+ (define deficit (* 100 (/ (- targeted measured) measured)))
+ (check-= deficit 100.0 0.01 "Deficit should be 100%")
+
+ (disconnect!))
+
+ ;; Test recipe with realistic constraints
+ (test-case "Recipe calculation with real-world scenario"
+ (connect! #:path 'memory)
+ (migrate-all!)
+
+ (define n (create-nutrient! "N" "N"))
+ (define p (create-nutrient! "P" "P"))
+ (define k (create-nutrient! "K" "K"))
+
+ ;; Current levels
+ (create-nutrient-measurement! test-date (hash n 50 p 10 k 100))
+
+ ;; Target levels
+ (create-nutrient-target! test-date (hash n 150 p 30 k 200))
+
+ ;; Fertilizers with different NPK ratios
+ (create-fertilizer-product! "" "Balanced" (hash n 100 p 100 k 100))
+ (create-fertilizer-product! "Nitrogen blend" "High-N" (hash n 200 p 50 k 50))
+ (create-fertilizer-product! "Phosphorus blend" "High-P" (hash n 50 p 200 k 50))
+ (create-fertilizer-product! "Potassium blend" "High-K" (hash n 50 p 50 k 200))
+
+ (define recipe (find-ferti-recipe))
+
+ (check-equal? (length recipe) 4 "Should have 4 fertilizer options")
+
+ ;; Verify solution is non-negative
+ (for ([pair recipe])
+ (check-true (>= (cdr pair) 0)
+ (format "~a quantity must be non-negative"
+ (fertilizer-name (car pair)))))
+
+ (disconnect!)))))
Copyright 2019--2026 Marius PETER