|
| 1 | +// Profile.fsx |
| 2 | +// GenSOLVER performance profiling script |
| 3 | +// Part of the W2 Core Architecture Review |
| 4 | +// |
| 5 | +// Run from GenSOLVER Scripts dir: |
| 6 | +// dotnet fsi Profile.fsx |
| 7 | +// |
| 8 | +// Or load in FSI: |
| 9 | +// #I "/path/to/Scripts" |
| 10 | +// #load "Profile.fsx" |
| 11 | + |
| 12 | +#load "load.fsx" |
| 13 | + |
| 14 | +#time |
| 15 | + |
| 16 | +open System |
| 17 | +open System.Diagnostics |
| 18 | + |
| 19 | +open Informedica.GenUnits.Lib |
| 20 | +open Informedica.GenSolver.Lib |
| 21 | + |
| 22 | +module Name = Variable.Name |
| 23 | +module ValueRange = Variable.ValueRange |
| 24 | +module Minimum = ValueRange.Minimum |
| 25 | +module Maximum = ValueRange.Maximum |
| 26 | +module Increment = ValueRange.Increment |
| 27 | +module ValueSet = ValueRange.ValueSet |
| 28 | + |
| 29 | +Environment.CurrentDirectory <- __SOURCE_DIRECTORY__ |
| 30 | + |
| 31 | + |
| 32 | +// ────────────────────────────────────────────── |
| 33 | +// Profiling helpers |
| 34 | +// ────────────────────────────────────────────── |
| 35 | + |
| 36 | +/// Silent logger — discards all solver trace messages during benchmarks |
| 37 | +let silentLogger = SolverLogging.create (fun _ -> ()) |
| 38 | + |
| 39 | +/// Time a thunk and return (result, elapsed ms) |
| 40 | +let timed (f: unit -> 'a) = |
| 41 | + let sw = Stopwatch.StartNew() |
| 42 | + let r = f () |
| 43 | + sw.Stop() |
| 44 | + r, sw.Elapsed.TotalMilliseconds |
| 45 | + |
| 46 | +/// Print a benchmark row |
| 47 | +let reportMs label ms = |
| 48 | + printfn $" %-45s{label} %8.2f ms" label ms |
| 49 | + |
| 50 | +/// Print a section heading |
| 51 | +let section title = |
| 52 | + printfn "" |
| 53 | + printfn $"=== {title} ===" |
| 54 | + |
| 55 | + |
| 56 | +// ────────────────────────────────────────────── |
| 57 | +// Setup helpers (mirrors Solver.fsx / Tests.fsx) |
| 58 | +// ────────────────────────────────────────────── |
| 59 | + |
| 60 | +let create c u v = |
| 61 | + [| v |] |
| 62 | + |> ValueUnit.create u |
| 63 | + |> c |
| 64 | + |
| 65 | +let createMinIncl = create (Minimum.create true) |
| 66 | +let createMaxIncl = create (Maximum.create true) |
| 67 | +let createIncr = create Increment.create |
| 68 | + |
| 69 | +let createValSet u vs = |
| 70 | + vs |
| 71 | + |> Array.ofSeq |
| 72 | + |> ValueUnit.create u |
| 73 | + |> ValueSet.create |
| 74 | + |
| 75 | +let setMinIncl u n min eqs = |
| 76 | + let n = n |> Name.createExc |
| 77 | + let p = min |> createMinIncl u |> MinProp |
| 78 | + match eqs |> Api.setVariableValues n p with |
| 79 | + | Some var -> eqs |> List.map (Equation.replace var) |
| 80 | + | None -> eqs |
| 81 | + |
| 82 | +let setMaxIncl u n max eqs = |
| 83 | + let n = n |> Name.createExc |
| 84 | + let p = max |> createMaxIncl u |> MaxProp |
| 85 | + match eqs |> Api.setVariableValues n p with |
| 86 | + | Some var -> eqs |> List.map (Equation.replace var) |
| 87 | + | None -> eqs |
| 88 | + |
| 89 | +let setIncr u n incr eqs = |
| 90 | + let n = n |> Name.createExc |
| 91 | + let p = incr |> createIncr u |> IncrProp |
| 92 | + match eqs |> Api.setVariableValues n p with |
| 93 | + | Some var -> eqs |> List.map (Equation.replace var) |
| 94 | + | None -> eqs |
| 95 | + |
| 96 | +let setValues u n vs eqs = |
| 97 | + let n = n |> Name.createExc |
| 98 | + let p = vs |> createValSet u |> ValsProp |
| 99 | + match eqs |> Api.setVariableValues n p with |
| 100 | + | Some var -> eqs |> List.map (Equation.replace var) |
| 101 | + | None -> eqs |
| 102 | + |
| 103 | +let solveAll eqs = |
| 104 | + eqs |
| 105 | + |> Api.solveAll false silentLogger |
| 106 | + |> function |
| 107 | + | Ok solved -> solved |
| 108 | + | Error _ -> eqs |
| 109 | + |
| 110 | +let solveMinMax eqs = |
| 111 | + eqs |
| 112 | + |> Api.solveAll true silentLogger |
| 113 | + |> function |
| 114 | + | Ok solved -> solved |
| 115 | + | Error _ -> eqs |
| 116 | + |
| 117 | +/// Report how many solved values each variable holds |
| 118 | +let countValues eqs = |
| 119 | + eqs |
| 120 | + |> List.sumBy (fun eq -> |
| 121 | + eq |
| 122 | + |> Equation.toVars |
| 123 | + |> List.sumBy Variable.count |
| 124 | + ) |
| 125 | + |
| 126 | + |
| 127 | +// ────────────────────────────────────────────── |
| 128 | +// Scenario 1 — Simple product equation (dose = wt * dosePerKg) |
| 129 | +// Min/max only — no value enumeration |
| 130 | +// ────────────────────────────────────────────── |
| 131 | + |
| 132 | +section "Scenario 1: single product eq, min/max constraints (solveMinMax)" |
| 133 | + |
| 134 | +let sc1 () = |
| 135 | + // dose = weight * dosePerKg |
| 136 | + [ "dose = weight * dpkg" ] |
| 137 | + |> Api.init |
| 138 | + |> Api.nonZeroNegative |
| 139 | + |> setMinIncl Units.Weight.kiloGram "weight" 1N |
| 140 | + |> setMaxIncl Units.Weight.kiloGram "weight" 70N |
| 141 | + |> setMinIncl Units.Mass.milliGram "dpkg" 5N |
| 142 | + |> setMaxIncl Units.Mass.milliGram "dpkg" 15N |
| 143 | + |> solveMinMax |
| 144 | + |
| 145 | +let _, ms1 = timed sc1 |
| 146 | +reportMs "dose = weight * dpkg (min/max only)" ms1 |
| 147 | + |
| 148 | + |
| 149 | +// ────────────────────────────────────────────── |
| 150 | +// Scenario 2 — Single product eq with increment |
| 151 | +// Enumerates candidate values for the product |
| 152 | +// ────────────────────────────────────────────── |
| 153 | + |
| 154 | +section "Scenario 2: single product eq, increment + min/max (solveAll)" |
| 155 | + |
| 156 | +let sc2 () = |
| 157 | + [ "dose = weight * dpkg" ] |
| 158 | + |> Api.init |
| 159 | + |> Api.nonZeroNegative |
| 160 | + |> setMinIncl Units.Weight.kiloGram "weight" 1N |
| 161 | + |> setMaxIncl Units.Weight.kiloGram "weight" 70N |
| 162 | + |> setIncr Units.Weight.kiloGram "weight" 1N |
| 163 | + |> setMinIncl Units.Mass.milliGram "dpkg" 5N |
| 164 | + |> setMaxIncl Units.Mass.milliGram "dpkg" 15N |
| 165 | + |> setIncr Units.Mass.milliGram "dpkg" 1N |
| 166 | + |> solveAll |
| 167 | + |
| 168 | +let solved2, ms2 = timed sc2 |
| 169 | +reportMs "dose = weight * dpkg (incr 1 kg, 1 mg)" ms2 |
| 170 | +printfn $" → total solved values across equations: {countValues solved2}" |
| 171 | + |
| 172 | + |
| 173 | +// ────────────────────────────────────────────── |
| 174 | +// Scenario 3 — Chained product equations |
| 175 | +// dose = weight * dpkg; dose = freq * dosePerTime |
| 176 | +// ────────────────────────────────────────────── |
| 177 | + |
| 178 | +section "Scenario 3: two chained product eqs, increment constraints (solveAll)" |
| 179 | + |
| 180 | +let sc3 () = |
| 181 | + [ "dose = weight * dpkg" |
| 182 | + "totaldose = dose * freq" ] |
| 183 | + |> Api.init |
| 184 | + |> Api.nonZeroNegative |
| 185 | + |> setMinIncl Units.Weight.kiloGram "weight" 1N |
| 186 | + |> setMaxIncl Units.Weight.kiloGram "weight" 70N |
| 187 | + |> setIncr Units.Weight.kiloGram "weight" 1N |
| 188 | + |> setMinIncl Units.Mass.milliGram "dpkg" 5N |
| 189 | + |> setMaxIncl Units.Mass.milliGram "dpkg" 15N |
| 190 | + |> setIncr Units.Mass.milliGram "dpkg" 1N |
| 191 | + |> setValues Units.Count.times "freq" [| 1N; 2N; 3N; 4N |] |
| 192 | + |> solveAll |
| 193 | + |
| 194 | +let solved3, ms3 = timed sc3 |
| 195 | +reportMs "chained: dose = wt*dpkg ; totaldose = dose*freq" ms3 |
| 196 | +printfn $" → total solved values across equations: {countValues solved3}" |
| 197 | + |
| 198 | + |
| 199 | +// ────────────────────────────────────────────── |
| 200 | +// Scenario 4 — Value-set scale: how does solve time |
| 201 | +// grow as the value-set cardinality increases? |
| 202 | +// This probes the ValueSet overflow threshold. |
| 203 | +// ────────────────────────────────────────────── |
| 204 | + |
| 205 | +section "Scenario 4: value-set scaling (product eq, setValues on both vars)" |
| 206 | + |
| 207 | +let MAX_CALC_COUNT = 500 // mirrors Utils.Constants.MAX_CALC_COUNT |
| 208 | + |
| 209 | +for n in [ 5; 10; 20; 50; 100; 200; 400; 499 ] do |
| 210 | + let vals = Array.init n (fun i -> BigRational.FromInt (i + 1)) |
| 211 | + let run () = |
| 212 | + [ "result = a * b" ] |
| 213 | + |> Api.init |
| 214 | + |> Api.nonZeroNegative |
| 215 | + |> setValues Units.Count.times "a" vals |
| 216 | + |> setValues Units.Count.times "b" vals |
| 217 | + |> solveAll |
| 218 | + let solved, ms = timed run |
| 219 | + let resultCount = |
| 220 | + solved |
| 221 | + |> List.collect Equation.toVars |
| 222 | + |> List.tryFind (fun v -> v |> Variable.getName |> Name.toString = "result") |
| 223 | + |> Option.map (Variable.getValueRange >> Variable.ValueRange.count) |
| 224 | + |> Option.defaultValue 0 |
| 225 | + let overflow = if n >= MAX_CALC_COUNT then "⚠ near overflow" else "" |
| 226 | + printfn $" n={n,4} ({n}×{n}={n*n,7} combos) solved in {ms,8:F2} ms → result has {resultCount,6} values {overflow}" |
| 227 | + |
| 228 | + |
| 229 | +// ────────────────────────────────────────────── |
| 230 | +// Scenario 5 — Sum equation (concentration = amount / volume) |
| 231 | +// ────────────────────────────────────────────── |
| 232 | + |
| 233 | +section "Scenario 5: sum/division scenario with increment" |
| 234 | + |
| 235 | +let sc5 () = |
| 236 | + // total = bolus + continuous (typical infusion scenario) |
| 237 | + [ "total = bolus + continuous" ] |
| 238 | + |> Api.init |
| 239 | + |> Api.nonZeroNegative |
| 240 | + |> setMinIncl Units.Volume.milliLiter "bolus" 0N |
| 241 | + |> setMaxIncl Units.Volume.milliLiter "bolus" 500N |
| 242 | + |> setIncr Units.Volume.milliLiter "bolus" 5N |
| 243 | + |> setMinIncl Units.Volume.milliLiter "continuous" 0N |
| 244 | + |> setMaxIncl Units.Volume.milliLiter "continuous" 500N |
| 245 | + |> setIncr Units.Volume.milliLiter "continuous" 5N |
| 246 | + |> solveAll |
| 247 | + |
| 248 | +let solved5, ms5 = timed sc5 |
| 249 | +reportMs "total = bolus + continuous (incr 5 mL each)" ms5 |
| 250 | +printfn $" → total solved values across equations: {countValues solved5}" |
| 251 | + |
| 252 | + |
| 253 | +// ────────────────────────────────────────────── |
| 254 | +// Summary |
| 255 | +// ────────────────────────────────────────────── |
| 256 | + |
| 257 | +section "Summary" |
| 258 | +printfn "" |
| 259 | +printfn " Scenario 1 (min/max only) : %8.2f ms" ms1 |
| 260 | +printfn " Scenario 2 (single eq + incr) : %8.2f ms" ms2 |
| 261 | +printfn " Scenario 3 (chained eqs + incr) : %8.2f ms" ms3 |
| 262 | +printfn " Scenario 5 (sum eq + incr 5 mL) : %8.2f ms" ms5 |
| 263 | +printfn "" |
| 264 | +printfn " Constants: MAX_CALC_COUNT=%d MAX_LOOP_COUNT=20 PRUNE=4" MAX_CALC_COUNT |
| 265 | +printfn " ValueSet overflow threshold: %d × %d = %d values" MAX_CALC_COUNT MAX_CALC_COUNT (MAX_CALC_COUNT * MAX_CALC_COUNT) |
| 266 | +printfn "" |
| 267 | +printfn "Profiling complete." |
0 commit comments