f.go (11517B)
1 package main 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "git.nunosempere.com/NunoSempere/fermi/pretty" 8 "git.nunosempere.com/NunoSempere/fermi/sample" 9 "math" 10 "os" 11 "sort" 12 "strings" 13 ) 14 15 /* Types and interfaces */ 16 type Stack struct { 17 old_dist Dist 18 vars map[string]Dist 19 } 20 21 type Dist interface { 22 Samples() []float64 23 } 24 25 type Scalar float64 26 27 type Lognormal struct { 28 low float64 29 high float64 30 } 31 32 type Beta struct { 33 a float64 34 b float64 35 } 36 37 type FilledSamples struct { 38 xs []float64 39 } 40 41 /* Dist interface functions */ 42 // https://go.dev/tour/methods/9 43 func (p Scalar) Samples() []float64 { 44 xs := make([]float64, N_SAMPLES) 45 for i := 0; i < N_SAMPLES; i++ { 46 xs[i] = float64(p) 47 } 48 return xs 49 } 50 51 func (ln Lognormal) Samples() []float64 { 52 sampler := func(r sample.Src) float64 { return sample.Sample_to(ln.low, ln.high, r) } 53 // Can't do parallel because then I'd have to await throughout the code 54 return sample.Sample_serially(sampler, N_SAMPLES) 55 } 56 57 func (beta Beta) Samples() []float64 { 58 sampler := func(r sample.Src) float64 { return sample.Sample_beta(beta.a, beta.b, r) } 59 return sample.Sample_serially(sampler, N_SAMPLES) 60 } 61 62 func (fs FilledSamples) Samples() []float64 { 63 return fs.xs 64 } 65 66 /* Constants */ 67 const HELP_MSG = " Operation | Variable assignment | Special\n" + 68 " Operation: operator operand\n" + 69 " operator: (empty) | * | / | + | -\n" + 70 " operand: scalar | lognormal | beta | variable\n" + 71 " lognormal: low high\n" + 72 " beta: beta alpha beta\n" + 73 " Variable assignment: =: variable_name\n" + 74 " Variable assignment and clear stack: =. variable_name\n" + 75 " Special: \n" + 76 " Comment: # this is a comment\n" + 77 " Clear stack: clear | c | .\n" + 78 " Print debug info: debug | d\n" + 79 " Print help message: help | h\n" + 80 " Start additional stack: operator (\n" + 81 " Return from additional stack )\n" + 82 " Exit: exit | e\n" + 83 " Examples: \n" + 84 " + 2\n" + 85 " / 2.5\n" + 86 " * 1 10 (interpreted as lognormal)\n" + 87 " + 1 10\n" + 88 " * beta 1 10\n" + 89 " 1 10 (multiplication taken as default operation)\n" + 90 " =: x\n" + 91 " .\n" + 92 " 1 100\n" + 93 " + x\n" + 94 " # this is a comment\n" + 95 " * 1 12 # this is an operation followed by a comment\n" + 96 " * (\n" + 97 " 1 10\n" + 98 " + beta 1 100\n" + 99 " )\n" + 100 " exit\n" 101 102 const NORMAL90CONFIDENCE = 1.6448536269514727 103 const INIT_DIST Scalar = Scalar(1) 104 const N_SAMPLES = 1_000_000 105 106 /* Printers */ 107 func prettyPrintDist(dist Dist) { 108 switch v := dist.(type) { 109 case Lognormal: 110 fmt.Printf("=> ") 111 pretty.PrettyPrint2Floats(v.low, v.high) 112 fmt.Println() 113 case Beta: 114 fmt.Printf("=> beta ") 115 pretty.PrettyPrint2Floats(v.a, v.b) 116 fmt.Println() 117 case Scalar: 118 fmt.Printf("=> scalar ") 119 w := float64(v) 120 pretty.PrettyPrintFloat(w) 121 fmt.Println() 122 case FilledSamples: 123 sorted_xs := make([]float64, N_SAMPLES) 124 copy(sorted_xs, v.xs) 125 sort.Slice(sorted_xs, func(i, j int) bool { 126 return sorted_xs[i] < sorted_xs[j] 127 }) 128 129 low := sorted_xs[N_SAMPLES/20] 130 high := sorted_xs[N_SAMPLES*19/20] 131 fmt.Printf("=> ") 132 pretty.PrettyPrint2Floats(low, high) 133 134 fmt.Printf(" (") 135 pretty.PrettyPrintInt(N_SAMPLES) 136 fmt.Printf(" samples)") 137 fmt.Println() 138 default: 139 fmt.Printf("%v\n", v) 140 } 141 } 142 143 func printAndReturnErr(err_msg string) error { 144 fmt.Println(err_msg) 145 fmt.Println(HELP_MSG) 146 return errors.New(err_msg) 147 } 148 149 /* Operations */ 150 // Generic operations with samples 151 func operateDistsAsSamples(dist1 Dist, dist2 Dist, op string) (Dist, error) { 152 153 xs := dist1.Samples() 154 ys := dist2.Samples() 155 zs := make([]float64, N_SAMPLES) 156 157 for i := 0; i < N_SAMPLES; i++ { 158 switch op { 159 case "*": 160 zs[i] = xs[i] * ys[i] 161 case "/": 162 if ys[0] != 0 { 163 zs[i] = xs[i] / ys[i] 164 } else { 165 fmt.Println("Error: When dividing as samples, division by zero") 166 return nil, errors.New("Division by zero") 167 } 168 case "+": 169 zs[i] = xs[i] + ys[i] 170 case "-": 171 zs[i] = xs[i] - ys[i] 172 } 173 } 174 175 return FilledSamples{xs: zs}, nil 176 } 177 178 // Multiplication 179 func multiplyLogDists(l1 Lognormal, l2 Lognormal) Lognormal { 180 logmean1 := (math.Log(l1.high) + math.Log(l1.low)) / 2.0 181 logstd1 := (math.Log(l1.high) - math.Log(l1.low)) / (2.0 * NORMAL90CONFIDENCE) 182 183 logmean2 := (math.Log(l2.high) + math.Log(l2.low)) / 2.0 184 logstd2 := (math.Log(l2.high) - math.Log(l2.low)) / (2.0 * NORMAL90CONFIDENCE) 185 186 logmean_product := logmean1 + logmean2 187 logstd_product := math.Sqrt(logstd1*logstd1 + logstd2*logstd2) 188 189 h := logstd_product * NORMAL90CONFIDENCE 190 loglow := logmean_product - h 191 loghigh := logmean_product + h 192 return Lognormal{low: math.Exp(loglow), high: math.Exp(loghigh)} 193 194 } 195 196 func multiplyBetaDists(beta1 Beta, beta2 Beta) Beta { 197 return Beta{a: beta1.a + beta2.a, b: beta1.b + beta2.b} 198 } 199 200 func multiplyDists(old_dist Dist, new_dist Dist) (Dist, error) { 201 202 switch o := old_dist.(type) { 203 case Lognormal: 204 { 205 switch n := new_dist.(type) { 206 case Lognormal: 207 return multiplyLogDists(o, n), nil 208 case Scalar: 209 return multiplyLogDists(o, Lognormal{low: float64(n), high: float64(n)}), nil 210 default: 211 return operateDistsAsSamples(old_dist, new_dist, "*") 212 } 213 } 214 case Scalar: 215 { 216 if o == 1 { 217 return new_dist, nil 218 } 219 switch n := new_dist.(type) { 220 case Lognormal: 221 return multiplyLogDists(Lognormal{low: float64(o), high: float64(o)}, n), nil 222 case Scalar: 223 return Scalar(float64(o) * float64(n)), nil 224 default: 225 return operateDistsAsSamples(old_dist, new_dist, "*") 226 } 227 } 228 case Beta: 229 switch n := new_dist.(type) { 230 case Beta: 231 return multiplyBetaDists(o, n), nil 232 default: 233 return operateDistsAsSamples(old_dist, new_dist, "*") 234 } 235 default: 236 return operateDistsAsSamples(old_dist, new_dist, "*") 237 } 238 } 239 240 func divideDists(old_dist Dist, new_dist Dist) (Dist, error) { 241 242 switch o := old_dist.(type) { 243 case Lognormal: 244 { 245 switch n := new_dist.(type) { 246 case Lognormal: 247 if n.high == 0 || n.low == 0 { 248 fmt.Println("Error: Can't divide by 0.0") 249 return nil, errors.New("Error: division by zero") 250 } 251 return multiplyLogDists(o, Lognormal{low: 1.0 / n.high, high: 1.0 / n.low}), nil 252 case Scalar: 253 if n == 0.0 { 254 fmt.Println("Error: Can't divide by 0.0") 255 return nil, errors.New("Error: division by zero scalar") 256 } 257 return multiplyLogDists(o, Lognormal{low: 1.0 / float64(n), high: 1.0 / float64(n)}), nil 258 default: 259 return operateDistsAsSamples(old_dist, new_dist, "/") 260 } 261 } 262 case Scalar: 263 { 264 switch n := new_dist.(type) { 265 case Lognormal: 266 return multiplyLogDists(Lognormal{low: float64(o), high: float64(o)}, Lognormal{low: 1.0 / n.high, high: 1.0 / n.low}), nil 267 case Scalar: 268 if n == 0.0 { 269 fmt.Println("Error: Can't divide by 0.0") 270 return nil, errors.New("Error: division by zero scalar") 271 } 272 return Scalar(float64(o) / float64(n)), nil 273 default: 274 return operateDistsAsSamples(old_dist, new_dist, "/") 275 } 276 } 277 default: 278 return operateDistsAsSamples(old_dist, new_dist, "/") 279 } 280 } 281 282 // Generic distribution operations 283 func operateDists(old_dist Dist, new_dist Dist, op string) (Dist, error) { 284 switch op { 285 case "*": 286 return multiplyDists(old_dist, new_dist) 287 case "/": 288 return divideDists(old_dist, new_dist) 289 case "+": 290 return operateDistsAsSamples(old_dist, new_dist, "+") 291 case "-": 292 return operateDistsAsSamples(old_dist, new_dist, "-") 293 default: 294 return nil, printAndReturnErr("Can't combine distributions in this way") 295 } 296 } 297 298 /* Parser and repl */ 299 func parseWordsErr(err_msg string) (string, Dist, error) { 300 return "", nil, printAndReturnErr(err_msg) 301 } 302 func parseWordsIntoOpAndDist(words []string, vars map[string]Dist) (string, Dist, error) { 303 304 op := "" 305 var dist Dist 306 307 switch words[0] { 308 case "*", "/", "+", "-": 309 op = words[0] 310 words = words[1:] 311 default: 312 op = "*" // later, change the below to 313 } 314 315 switch len(words) { 316 case 0: 317 return parseWordsErr("Operator must have operand; can't operate on nothing") 318 case 1: 319 var_word, var_word_exists := vars[words[0]] 320 single_float, err1 := pretty.ParseFloat(words[0]) // abstract this away to search for K/M/B/T/etc. 321 switch { 322 case var_word_exists: 323 dist = var_word 324 case err1 == nil: 325 dist = Scalar(single_float) 326 case err1 != nil && !var_word_exists: 327 return parseWordsErr("Trying to operate on a scalar, but scalar is neither a float nor an assigned variable") 328 } 329 case 2: 330 new_low, err1 := pretty.ParseFloat(words[0]) 331 new_high, err2 := pretty.ParseFloat(words[1]) 332 if err1 != nil || err2 != nil { 333 return parseWordsErr("Trying to operate by a distribution, but distribution is not specified as two floats") 334 } 335 dist = Lognormal{low: new_low, high: new_high} 336 case 3: 337 if words[0] == "beta" || words[0] == "b" { 338 a, err1 := pretty.ParseFloat(words[1]) 339 b, err2 := pretty.ParseFloat(words[2]) 340 if err1 != nil || err2 != nil { 341 return parseWordsErr("Trying to specify a beta distribution? Try beta 1 2") 342 } 343 dist = Beta{a: a, b: b} 344 } else { 345 return parseWordsErr("Input not understood or not implemented yet") 346 } 347 default: 348 return parseWordsErr("Input not understood or not implemented yet") 349 } 350 return op, dist, nil 351 } 352 353 /* Combine old dist and new line */ 354 // We want this as a function (rather than just be in main) 355 // to be able to have parenthesis/recusion, possibly functions 356 357 func runRepl(stack Stack, reader *bufio.Reader) Stack { 358 replForLoop: 359 for { 360 new_line, _ := reader.ReadString('\n') 361 new_line_before_comments, _, _ := strings.Cut(new_line, "#") 362 new_line_trimmed := strings.TrimSpace(new_line_before_comments) 363 words := strings.Split(new_line_trimmed, " ") 364 365 switch { 366 case strings.TrimSpace(new_line_trimmed) == "": /* Empty line case */ 367 continue replForLoop 368 /* Parenthesis */ 369 case len(words) == 2 && (words[0] == "*" || words[0] == "+" || words[0] == "-" || words[0] == "/") && words[1] == "(": 370 new_stack := runRepl(Stack{old_dist: INIT_DIST, vars: stack.vars}, reader) 371 combined_dist, err := operateDists(stack.old_dist, new_stack.old_dist, words[0]) 372 if err == nil { 373 stack.old_dist = combined_dist 374 } 375 case len(words) == 1 && words[0] == ")": 376 return stack 377 /* Special operations */ 378 case words[0] == "exit" || words[0] == "e": 379 os.Exit(0) 380 case words[0] == "help" || words[0] == "h": 381 fmt.Println(HELP_MSG) 382 case words[0] == "debug" || words[0] == "d": 383 fmt.Printf("%v", stack) 384 case words[0] == "clear" || words[0] == "c" || words[0] == ".": 385 stack.old_dist = INIT_DIST 386 fmt.Println() 387 /* Variable assignment */ 388 case words[0] == "=:" && len(words) == 2: 389 stack.vars[words[1]] = stack.old_dist 390 fmt.Printf("%s ", words[1]) 391 case words[0] == "=." && len(words) == 2: 392 stack.vars[words[1]] = stack.old_dist 393 fmt.Printf("%s ", words[1]) 394 prettyPrintDist(stack.old_dist) 395 stack.old_dist = INIT_DIST 396 default: 397 op, new_dist, err := parseWordsIntoOpAndDist(words, stack.vars) 398 if err != nil { 399 continue replForLoop 400 } 401 combined_dist, err := operateDists(stack.old_dist, new_dist, op) 402 if err == nil { 403 stack.old_dist = combined_dist 404 } 405 } 406 prettyPrintDist(stack.old_dist) 407 } 408 } 409 410 func main() { 411 reader := bufio.NewReader(os.Stdin) 412 stack := Stack{old_dist: INIT_DIST, vars: make(map[string]Dist)} 413 runRepl(stack, reader) 414 }