Skip to content

Commit 4f593ed

Browse files
authored
Merge pull request #8 from halcwb/repo-assist/improve-gensolver-profiling-script-978639214f427955
[Repo Assist] feat(gensolver): add Profile.fsx benchmarking script for W2 review
2 parents 23ddb08 + 24d8b76 commit 4f593ed

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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

Comments
 (0)