fermi

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

f_simple.go (9602B)


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