fermi

A minimalist calculator for estimating with distributions
Log | Files | Refs | README

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 }