monte 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +65 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/monte +18 -0
- data/lib/monte.rb +6 -0
- data/lib/monte/cli.rb +33 -0
- data/lib/monte/command.rb +131 -0
- data/lib/monte/commands/.gitkeep +1 -0
- data/lib/monte/commands/carlo.rb +54 -0
- data/lib/monte/simulation.rb +42 -0
- data/lib/monte/version.rb +3 -0
- data/monte.gemspec +31 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fe28c6cadcd331c236ef5dd53e43b9d32d18b9585fbcc7debc820e1abcb28394
|
4
|
+
data.tar.gz: 998b21dbf4105649811833eef488e9ad858876759f9bb974f5805937655bacad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 26eb9d0115fad3fc93ecf4dd3d007d35573da5e0468ca6fa04c91fedc85499a79edda481a7af08a1be643e0a14229ce5b5a23d8e5388d61e652f63ea55b177b8
|
7
|
+
data.tar.gz: 8c1bc63cefcde03c48e2d2217415b20469ed9937d75c428b9ad701c12bff625ff59040025ae2954597f6fe3e7ad434980a39180c4cbcd3cdcdff3c4d97c01f16
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Andrew Werner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Monte
|
2
|
+
|
3
|
+
Monte is a simple gem designed to help software engineers and developers answer
|
4
|
+
the question 'When will it be done?'.
|
5
|
+
|
6
|
+
The tool uses the Monte Carlo method to provide a reasonable forecast of the
|
7
|
+
future based on statistically relevant data from the past.
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
You must have ruby installed to use this tool.
|
11
|
+
|
12
|
+
start by installing the gem.
|
13
|
+
|
14
|
+
```sh
|
15
|
+
$ gem install monte
|
16
|
+
```
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
The following is the simplest version of Monte which should be used when you
|
21
|
+
have little previous data with which to build your forecast.
|
22
|
+
|
23
|
+
```sh
|
24
|
+
$ monte carlo
|
25
|
+
>
|
26
|
+
> __ __ _
|
27
|
+
> | \/ | ___ _ __ | |_ ___
|
28
|
+
> | |\/| | / _ \ | '_ \ | __| / _ \
|
29
|
+
> | | | | | (_) | | | | | | |_ | __/
|
30
|
+
> |_| |_| \___/ |_| |_| \__| \___|
|
31
|
+
>
|
32
|
+
> Please answer the following:
|
33
|
+
>
|
34
|
+
> How many items do you have in your backlog? 40
|
35
|
+
> How certain are you with regard to the scope of the work? medium
|
36
|
+
> When will you start work (e.g. 28/04/2021) 2020-11-15
|
37
|
+
> What is the smallest number of tasks/tickets you have completed in a week? 2
|
38
|
+
> What is the largest number of tasks/tickets you have completed in a week? 6
|
39
|
+
> How many simulations would you like to run 10000
|
40
|
+
>
|
41
|
+
>
|
42
|
+
> Your Results
|
43
|
+
>
|
44
|
+
> ┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
|
45
|
+
> │ 5% │ 15% │ 30% │ 50% │ 70% │ 85% │ 95% │
|
46
|
+
> ├──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
|
47
|
+
> │2021-02-14│2021-02-21│2021-02-28│2021-02-28│2021-03-07│2021-03-14│2021-03-21│
|
48
|
+
> └──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
|
49
|
+
```
|
50
|
+
|
51
|
+
## Development
|
52
|
+
|
53
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
54
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
55
|
+
prompt that will allow you to experiment.
|
56
|
+
|
57
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
58
|
+
release a new version, update the version number in `version.rb`, and then run
|
59
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
60
|
+
git commits and tags, and push the `.gem` file to
|
61
|
+
[rubygems.org](https://rubygems.org).
|
62
|
+
|
63
|
+
## Copyright
|
64
|
+
|
65
|
+
Copyright (c) 2020 Andrew Werner. See [MIT License](LICENSE.txt) for further details.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "monte"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/monte
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib_path = File.expand_path('../lib', __dir__)
|
5
|
+
$:.unshift(lib_path) if !$:.include?(lib_path)
|
6
|
+
require 'monte/cli'
|
7
|
+
|
8
|
+
Signal.trap('INT') do
|
9
|
+
warn("\n#{caller.join("\n")}: interrupted")
|
10
|
+
exit(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
Monte::CLI.start
|
15
|
+
rescue Monte::CLI::Error => err
|
16
|
+
puts "ERROR: #{err.message}"
|
17
|
+
exit 1
|
18
|
+
end
|
data/lib/monte.rb
ADDED
data/lib/monte/cli.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module Monte
|
6
|
+
# Handle the application command line parsing
|
7
|
+
# and the dispatch to various command objects
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class CLI < Thor
|
11
|
+
# Error raised by this runner
|
12
|
+
Error = Class.new(StandardError)
|
13
|
+
|
14
|
+
desc 'version', 'monte version'
|
15
|
+
def version
|
16
|
+
require_relative 'version'
|
17
|
+
puts "v#{Monte::VERSION}"
|
18
|
+
end
|
19
|
+
map %w[--version -v] => :version
|
20
|
+
|
21
|
+
desc 'carlo', 'Runs through a set of questions to generate a forecast for project completion'
|
22
|
+
method_option :help, aliases: '-h', type: :boolean,
|
23
|
+
desc: 'Display usage information'
|
24
|
+
def carlo
|
25
|
+
if options[:help]
|
26
|
+
invoke :help, ['carlo']
|
27
|
+
else
|
28
|
+
require_relative 'commands/carlo'
|
29
|
+
Monte::Commands::Carlo.new(options).execute
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Monte
|
6
|
+
class Command
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :command, :run
|
10
|
+
|
11
|
+
# Execute this command
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def execute(*)
|
15
|
+
raise(
|
16
|
+
NotImplementedError,
|
17
|
+
"#{self.class}##{__method__} must be implemented"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The external commands runner
|
22
|
+
#
|
23
|
+
# @see http://www.rubydoc.info/gems/tty-command
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def command(**options)
|
27
|
+
require 'tty-command'
|
28
|
+
TTY::Command.new(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
# The cursor movement
|
32
|
+
#
|
33
|
+
# @see http://www.rubydoc.info/gems/tty-cursor
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def cursor
|
37
|
+
require 'tty-cursor'
|
38
|
+
TTY::Cursor
|
39
|
+
end
|
40
|
+
|
41
|
+
# Open a file or text in the user's preferred editor
|
42
|
+
#
|
43
|
+
# @see http://www.rubydoc.info/gems/tty-editor
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
def editor
|
47
|
+
require 'tty-editor'
|
48
|
+
TTY::Editor
|
49
|
+
end
|
50
|
+
|
51
|
+
# File manipulation utility methods
|
52
|
+
#
|
53
|
+
# @see http://www.rubydoc.info/gems/tty-file
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def generator
|
57
|
+
require 'tty-file'
|
58
|
+
TTY::File
|
59
|
+
end
|
60
|
+
|
61
|
+
# Terminal output paging
|
62
|
+
#
|
63
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def pager(**options)
|
67
|
+
require 'tty-pager'
|
68
|
+
TTY::Pager.new(options)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Terminal platform and OS properties
|
72
|
+
#
|
73
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def platform
|
77
|
+
require 'tty-platform'
|
78
|
+
TTY::Platform.new
|
79
|
+
end
|
80
|
+
|
81
|
+
# The interactive prompt
|
82
|
+
#
|
83
|
+
# @see http://www.rubydoc.info/gems/tty-prompt
|
84
|
+
#
|
85
|
+
# @api public
|
86
|
+
def prompt(**options)
|
87
|
+
require 'tty-prompt'
|
88
|
+
TTY::Prompt.new(options)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get terminal screen properties
|
92
|
+
#
|
93
|
+
# @see http://www.rubydoc.info/gems/tty-screen
|
94
|
+
#
|
95
|
+
# @api public
|
96
|
+
def screen
|
97
|
+
require 'tty-screen'
|
98
|
+
TTY::Screen
|
99
|
+
end
|
100
|
+
|
101
|
+
# The unix which utility
|
102
|
+
#
|
103
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
104
|
+
#
|
105
|
+
# @api public
|
106
|
+
def which(*args)
|
107
|
+
require 'tty-which'
|
108
|
+
TTY::Which.which(*args)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Check if executable exists
|
112
|
+
#
|
113
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
def exec_exist?(*args)
|
117
|
+
require 'tty-which'
|
118
|
+
TTY::Which.exist?(*args)
|
119
|
+
end
|
120
|
+
|
121
|
+
def table(headers, rows)
|
122
|
+
require 'tty-table'
|
123
|
+
TTY::Table.new(headers, rows)
|
124
|
+
end
|
125
|
+
|
126
|
+
def large_title(title)
|
127
|
+
require 'tty-font'
|
128
|
+
TTY::Font.new(:standard).write(title)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require_relative '../command'
|
5
|
+
require_relative '../simulation'
|
6
|
+
|
7
|
+
module Monte
|
8
|
+
module Commands
|
9
|
+
# Runs Monte Carlo Simulation to estimate how long a piece of work will take
|
10
|
+
class Carlo < Monte::Command
|
11
|
+
include Simulation
|
12
|
+
CERTAINTY = { 'low' => 1.8, 'medium' => 1.5, 'high' => 1.2 }.freeze
|
13
|
+
RUNS = { '10000' => 10_000, '1000' => 1000, '500' => 500 }.freeze
|
14
|
+
HEADERS = ['5%', '15%', '30%', '50%', '70%', '85%', '95%'].freeze
|
15
|
+
PERCENTILES = [0.05, 0.15, 0.3, 0.5, 0.7, 0.85, 0.95].freeze
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute(output: $stdout)
|
22
|
+
output.puts(create_header)
|
23
|
+
output.puts("Please answer the following:\n\n")
|
24
|
+
user_input = ask_questions!
|
25
|
+
results = percentiles(user_input)
|
26
|
+
output.puts("\n\nYour Results\n\n")
|
27
|
+
output.puts(create_table(results))
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_table(rows)
|
31
|
+
table(HEADERS, [rows]).render(:unicode, alignment: [:center])
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_header
|
35
|
+
large_title('Monte')
|
36
|
+
end
|
37
|
+
|
38
|
+
def ask_questions!
|
39
|
+
prompt.collect do
|
40
|
+
key(:backlog).ask('How many items do you have in your backlog?', convert: :int)
|
41
|
+
key(:split_factor).select('How certain are you with regard to the scope of the work?', CERTAINTY)
|
42
|
+
key(:start_date).ask('When will you start work (e.g. 28/04/2021)') do |q|
|
43
|
+
q.default Date.today
|
44
|
+
q.convert ->(input) { Date.parse(input.to_s) }
|
45
|
+
end
|
46
|
+
key(:low).ask('What is the smallest number of tasks/tickets you have completed in a week?', convert: :int)
|
47
|
+
key(:high).ask('What is the largest number of tasks/tickets you have completed in a week?', convert: :int)
|
48
|
+
key(:runs).select('How many simulations would you like to run', RUNS)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The code to run n monte carlo simulations
|
4
|
+
# args is a hash that must contain the following:
|
5
|
+
# :backlog
|
6
|
+
# :split_factor
|
7
|
+
# :runs
|
8
|
+
# :low
|
9
|
+
# :high
|
10
|
+
module Simulation
|
11
|
+
PERCENTILES = [0.05, 0.15, 0.3, 0.5, 0.7, 0.85, 0.95].freeze
|
12
|
+
|
13
|
+
def percentiles(args)
|
14
|
+
results = run_simulations(args).sort
|
15
|
+
PERCENTILES.map do |percentile|
|
16
|
+
index = args[:runs] * (percentile - 1)
|
17
|
+
results[index]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_simulations(args)
|
22
|
+
estimated_backlog = args[:backlog] * args[:split_factor]
|
23
|
+
Array.new(args[:runs]) do |_|
|
24
|
+
args[:start_date] + simulate(
|
25
|
+
estimated_backlog,
|
26
|
+
args[:low],
|
27
|
+
args[:high]
|
28
|
+
) * 7
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def simulate(backlog, low, high, result = 0)
|
33
|
+
return result if backlog <= 0
|
34
|
+
|
35
|
+
simulate(
|
36
|
+
backlog - rand(low..high),
|
37
|
+
low,
|
38
|
+
high,
|
39
|
+
result + 1
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
data/monte.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/monte/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'monte'
|
7
|
+
spec.license = 'MIT'
|
8
|
+
spec.version = Monte::VERSION
|
9
|
+
spec.authors = ['Andrew Werner']
|
10
|
+
spec.email = ['awerner1@googlemail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Monte Carlo forecasting for engineering projects'
|
13
|
+
spec.description = "If you are an engineer who is being asked, 'When will
|
14
|
+
it be done?' then Monte can help by using the Monte Carlo method to provide
|
15
|
+
you with plausile forecasts based on historic data."
|
16
|
+
spec.homepage = 'https://github.com/ALRW/monte'
|
17
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/ALRW/monte.git'
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/ALRW/monte/blob/master/CHANGELOG.md'
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: monte
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Werner
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |-
|
14
|
+
If you are an engineer who is being asked, 'When will
|
15
|
+
it be done?' then Monte can help by using the Monte Carlo method to provide
|
16
|
+
you with plausile forecasts based on historic data.
|
17
|
+
email:
|
18
|
+
- awerner1@googlemail.com
|
19
|
+
executables:
|
20
|
+
- monte
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- ".gitignore"
|
25
|
+
- ".rspec"
|
26
|
+
- ".travis.yml"
|
27
|
+
- CHANGELOG.md
|
28
|
+
- Gemfile
|
29
|
+
- LICENSE.txt
|
30
|
+
- README.md
|
31
|
+
- Rakefile
|
32
|
+
- bin/console
|
33
|
+
- bin/setup
|
34
|
+
- exe/monte
|
35
|
+
- lib/monte.rb
|
36
|
+
- lib/monte/cli.rb
|
37
|
+
- lib/monte/command.rb
|
38
|
+
- lib/monte/commands/.gitkeep
|
39
|
+
- lib/monte/commands/carlo.rb
|
40
|
+
- lib/monte/simulation.rb
|
41
|
+
- lib/monte/version.rb
|
42
|
+
- monte.gemspec
|
43
|
+
homepage: https://github.com/ALRW/monte
|
44
|
+
licenses:
|
45
|
+
- MIT
|
46
|
+
metadata:
|
47
|
+
homepage_uri: https://github.com/ALRW/monte
|
48
|
+
source_code_uri: https://github.com/ALRW/monte.git
|
49
|
+
changelog_uri: https://github.com/ALRW/monte/blob/master/CHANGELOG.md
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: 2.3.0
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
requirements: []
|
65
|
+
rubygems_version: 3.1.2
|
66
|
+
signing_key:
|
67
|
+
specification_version: 4
|
68
|
+
summary: Monte Carlo forecasting for engineering projects
|
69
|
+
test_files: []
|