kansim 0.1.0__tar.gz
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.
- kansim-0.1.0/.gitignore +13 -0
- kansim-0.1.0/PKG-INFO +239 -0
- kansim-0.1.0/README.md +226 -0
- kansim-0.1.0/examples/poker_hand_equity.py +57 -0
- kansim-0.1.0/examples/startup_runway.py +34 -0
- kansim-0.1.0/examples/water_treatment_plant.py +20 -0
- kansim-0.1.0/kansim/__init__.py +242 -0
- kansim-0.1.0/kansim.egg-info/PKG-INFO +239 -0
- kansim-0.1.0/kansim.egg-info/SOURCES.txt +13 -0
- kansim-0.1.0/kansim.egg-info/dependency_links.txt +1 -0
- kansim-0.1.0/kansim.egg-info/requires.txt +4 -0
- kansim-0.1.0/kansim.egg-info/top_level.txt +1 -0
- kansim-0.1.0/pyproject.toml +36 -0
- kansim-0.1.0/setup.cfg +4 -0
- kansim-0.1.0/uv.lock +227 -0
kansim-0.1.0/.gitignore
ADDED
kansim-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kansim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Project-URL: Homepage, https://github.com/kdqed/kansim
|
|
6
|
+
Project-URL: Issues, https://github.com/kdqed/kansim/issues
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: numpy>=2.4.3
|
|
10
|
+
Requires-Dist: pandas>=3.0.1
|
|
11
|
+
Requires-Dist: plotly-express>=0.4.1
|
|
12
|
+
Requires-Dist: scipy>=1.17.1
|
|
13
|
+
|
|
14
|
+
# kansim
|
|
15
|
+
|
|
16
|
+
**Monte Carlo simulations as code. Free, open, and AI-ready.**
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why simulation software should be free and open
|
|
21
|
+
|
|
22
|
+
Simulation models inform some of the most consequential decisions in engineering, finance, environment, and public health. Yet the tools used to build them are often expensive, proprietary, and locked behind GUIs that make models opaque and hard to reproduce.
|
|
23
|
+
|
|
24
|
+
This creates a problem: **a model you cannot read is a model you cannot trust.**
|
|
25
|
+
|
|
26
|
+
When simulation is code:
|
|
27
|
+
|
|
28
|
+
- **It is auditable.** Anyone can read, review, and challenge the assumptions.
|
|
29
|
+
- **It is reproducible.** Run the same model anywhere, any time, and get the same results.
|
|
30
|
+
- **It can be version-controlled.** Every change is tracked. You can see who changed what and why.
|
|
31
|
+
- **It is composable.** Models can import each other, share logic, and be tested like software.
|
|
32
|
+
- **It is free.** No license fees. No vendor lock-in. No expiry dates.
|
|
33
|
+
|
|
34
|
+
Science and engineering move faster when knowledge is shared. Simulation tooling should be no different.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Why code beats a GUI
|
|
39
|
+
|
|
40
|
+
GUI simulation tools are approachable but they impose a ceiling. As models grow in complexity, the visual interface becomes the bottleneck: hard to navigate, impossible to diff, and painful to automate.
|
|
41
|
+
|
|
42
|
+
Code has no such ceiling.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from kansim import Simulation, Normal, Triangular, LogNormal, Boolean
|
|
46
|
+
|
|
47
|
+
class StartupRunway(Simulation):
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.monthly_burn = Normal(mean=45_000, std=8_000)
|
|
51
|
+
self.monthly_revenue = LogNormal(mean=20_000, std=12_000)
|
|
52
|
+
self.raised_bridge = Boolean(p=0.3)
|
|
53
|
+
self.bridge_amount = Normal(mean=150_000, std=50_000) if self.raised_bridge else 0
|
|
54
|
+
|
|
55
|
+
net_burn = self.monthly_burn - self.monthly_revenue
|
|
56
|
+
self.runway_months = (500_000 + self.bridge_amount) / net_burn if net_burn > 0 else 999
|
|
57
|
+
self.default_risk = self.runway_months < 12
|
|
58
|
+
|
|
59
|
+
result = StartupRunway.run(n=10_000, seed=42)
|
|
60
|
+
print(result.df.describe())
|
|
61
|
+
result.plot_tornado(target="runway_months")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is the entire model. It fits in a code review. It runs in CI. It can be parameterized, tested, and shipped as a package.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install kansim
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv add kansim
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or clone and run examples directly with [uv](https://github.com/astral-sh/uv):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/kansim/kansim
|
|
84
|
+
cd kansim
|
|
85
|
+
uv run examples/startup_runway.py
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## How it works
|
|
91
|
+
|
|
92
|
+
Subclass `Simulation`. Assign inputs using distribution functions and compute outputs — all inside `__init__`. Call `.run(n=)` to get a `SimulationResult`.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from kansim import Simulation, Triangular, Normal
|
|
96
|
+
|
|
97
|
+
class BridgeFatigue(Simulation):
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
self.daily_load = Normal(mean=500, std=80) # tonnes
|
|
101
|
+
self.material_life = Triangular(min=40, most_likely=60, max=90) # years
|
|
102
|
+
self.cycles = self.daily_load * 365
|
|
103
|
+
self.failure_risk = self.cycles > self.material_life * 150_000
|
|
104
|
+
|
|
105
|
+
result = BridgeFatigue.run(n=10_000, seed=42)
|
|
106
|
+
result.plot_histogram("material_life")
|
|
107
|
+
result.plot_tornado(target="failure_risk")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
All results write to `results/<timestamp>/`. Pass `save="file.png"` to any plot to save instead of show.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
result.result_id # "2024-03-10_143022"
|
|
114
|
+
result.df # pandas DataFrame — filter and pass back to any plot
|
|
115
|
+
result.save_csv()
|
|
116
|
+
result.save_parquet()
|
|
117
|
+
result.plot_histogram("col", save="hist.png")
|
|
118
|
+
result.plot_cdf("col")
|
|
119
|
+
result.plot_scatter("x", "y")
|
|
120
|
+
result.plot_tornado(target="col")
|
|
121
|
+
|
|
122
|
+
# filtered plots
|
|
123
|
+
passing = result.df[result.df.failure_risk == False]
|
|
124
|
+
result.plot_histogram("material_life", df=passing, save="safe_only.png")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Distributions
|
|
130
|
+
|
|
131
|
+
| Function | Parameters |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `Normal` | `mean, std, min=None, max=None` |
|
|
134
|
+
| `LogNormal` | `mean, std, min=None, max=None` |
|
|
135
|
+
| `Uniform` | `min, max, log=False` |
|
|
136
|
+
| `Triangular` | `min, most_likely, max` |
|
|
137
|
+
| `BetaPERT` | `min, most_likely, max` |
|
|
138
|
+
| `Beta` | `successes, failures` → 0–1 range |
|
|
139
|
+
| `GeneralizedBeta` | `mean, std, min, max` |
|
|
140
|
+
| `Exponential` | `mean` |
|
|
141
|
+
| `Gamma` | `mean, std, min=None, max=None` |
|
|
142
|
+
| `Weibull` | `scale, shape, min=0, max=None` |
|
|
143
|
+
| `Pareto` | `shape, mode, max=None` |
|
|
144
|
+
| `Poisson` | `expected` |
|
|
145
|
+
| `Binomial` | `n, p` |
|
|
146
|
+
| `NegativeBinomial` | `successes, p` |
|
|
147
|
+
| `StudentT` | `df` |
|
|
148
|
+
| `PearsonIII` | `location, scale, shape` |
|
|
149
|
+
| `ExtremeValue` | `location, scale` |
|
|
150
|
+
| `Discrete` | `values=[], probs=[]` |
|
|
151
|
+
| `Cumulative` | `values=[], probs=[]` |
|
|
152
|
+
| `SampledResults` | `values=[]` |
|
|
153
|
+
| `Boolean` | `p` |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Using AI to generate simulations
|
|
158
|
+
|
|
159
|
+
kansim is designed to be written by humans or AI agents. The model structure is simple enough that a language model can draft a complete, runnable simulation from a plain English description.
|
|
160
|
+
|
|
161
|
+
### Instructions for an AI agent
|
|
162
|
+
|
|
163
|
+
Paste the following into your system prompt or alongside your request:
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
> Use the kansim framework by subclassing `Simulation` and assigning all variables in `__init__`.
|
|
168
|
+
> Inputs use distribution functions, outputs are computed from them. Everything assigned to `self` becomes a DataFrame column.
|
|
169
|
+
>
|
|
170
|
+
> ```python
|
|
171
|
+
> from kansim import Simulation, Normal, Triangular, Beta
|
|
172
|
+
>
|
|
173
|
+
> class MyModel(Simulation):
|
|
174
|
+
> def __init__(self):
|
|
175
|
+
> self.input_a = Normal(mean=100, std=10)
|
|
176
|
+
> self.input_b = Triangular(min=1, most_likely=2, max=5)
|
|
177
|
+
> self.output_x = self.input_a * self.input_b
|
|
178
|
+
>
|
|
179
|
+
> result = MyModel.run(n=10_000, seed=42)
|
|
180
|
+
> ```
|
|
181
|
+
>
|
|
182
|
+
> `run()` returns a `SimulationResult` with:
|
|
183
|
+
> - `result.result_id` — human readable timestamp e.g. `"2024-03-10_143022"`
|
|
184
|
+
> - `result.df` — pandas DataFrame of all runs
|
|
185
|
+
> - `result.save_csv()`, `result.save_parquet()` — saves to `results/<result_id>/`
|
|
186
|
+
> - `result.plot_histogram(col, df=None, save=None)`
|
|
187
|
+
> - `result.plot_cdf(col, df=None, save=None)`
|
|
188
|
+
> - `result.plot_scatter(x, y, df=None, save=None)`
|
|
189
|
+
> - `result.plot_tornado(target, df=None, save=None)`
|
|
190
|
+
>
|
|
191
|
+
> All plots accept an optional `df` argument for filtered/modified DataFrames.
|
|
192
|
+
> All saves write to `results/<result_id>/` — pass `save="filename.png"` to save instead of show.
|
|
193
|
+
>
|
|
194
|
+
> Available distributions:
|
|
195
|
+
> `Normal(mean, std, min=None, max=None)`,
|
|
196
|
+
> `LogNormal(mean, std, min=None, max=None)`,
|
|
197
|
+
> `Uniform(min, max, log=False)`,
|
|
198
|
+
> `Triangular(min, most_likely, max)`,
|
|
199
|
+
> `BetaPERT(min, most_likely, max)`,
|
|
200
|
+
> `Beta(successes, failures)`,
|
|
201
|
+
> `GeneralizedBeta(mean, std, min, max)`,
|
|
202
|
+
> `Exponential(mean)`,
|
|
203
|
+
> `Gamma(mean, std, min=None, max=None)`,
|
|
204
|
+
> `Weibull(scale, shape, min=0, max=None)`,
|
|
205
|
+
> `Pareto(shape, mode, max=None)`,
|
|
206
|
+
> `Poisson(expected)`,
|
|
207
|
+
> `Binomial(n, p)`,
|
|
208
|
+
> `NegativeBinomial(successes, p)`,
|
|
209
|
+
> `StudentT(df)`,
|
|
210
|
+
> `PearsonIII(location, scale, shape)`,
|
|
211
|
+
> `ExtremeValue(location, scale)`,
|
|
212
|
+
> `Discrete(values=[], probs=[])`,
|
|
213
|
+
> `Cumulative(values=[], probs=[])`,
|
|
214
|
+
> `SampledResults(values=[])`,
|
|
215
|
+
> `Boolean(p)`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Example prompt
|
|
220
|
+
|
|
221
|
+
> Using kansim, simulate the cost overrun risk for a construction project. Inputs should include labor cost, material cost, and weather delay days. Output should include total cost and whether the project exceeds budget.
|
|
222
|
+
|
|
223
|
+
The AI will return a complete, runnable Python file.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Examples
|
|
228
|
+
|
|
229
|
+
| File | Description |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `examples/startup_runway.py` | Startup cash runway under uncertain growth and events |
|
|
232
|
+
| `examples/poker_hand_equity.py` | Heads-up poker equity over 10,000 deals |
|
|
233
|
+
| `examples/water_treatment_plant.py` | Water treatment plant throughput and cost |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
kansim-0.1.0/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# kansim
|
|
2
|
+
|
|
3
|
+
**Monte Carlo simulations as code. Free, open, and AI-ready.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why simulation software should be free and open
|
|
8
|
+
|
|
9
|
+
Simulation models inform some of the most consequential decisions in engineering, finance, environment, and public health. Yet the tools used to build them are often expensive, proprietary, and locked behind GUIs that make models opaque and hard to reproduce.
|
|
10
|
+
|
|
11
|
+
This creates a problem: **a model you cannot read is a model you cannot trust.**
|
|
12
|
+
|
|
13
|
+
When simulation is code:
|
|
14
|
+
|
|
15
|
+
- **It is auditable.** Anyone can read, review, and challenge the assumptions.
|
|
16
|
+
- **It is reproducible.** Run the same model anywhere, any time, and get the same results.
|
|
17
|
+
- **It can be version-controlled.** Every change is tracked. You can see who changed what and why.
|
|
18
|
+
- **It is composable.** Models can import each other, share logic, and be tested like software.
|
|
19
|
+
- **It is free.** No license fees. No vendor lock-in. No expiry dates.
|
|
20
|
+
|
|
21
|
+
Science and engineering move faster when knowledge is shared. Simulation tooling should be no different.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why code beats a GUI
|
|
26
|
+
|
|
27
|
+
GUI simulation tools are approachable but they impose a ceiling. As models grow in complexity, the visual interface becomes the bottleneck: hard to navigate, impossible to diff, and painful to automate.
|
|
28
|
+
|
|
29
|
+
Code has no such ceiling.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from kansim import Simulation, Normal, Triangular, LogNormal, Boolean
|
|
33
|
+
|
|
34
|
+
class StartupRunway(Simulation):
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.monthly_burn = Normal(mean=45_000, std=8_000)
|
|
38
|
+
self.monthly_revenue = LogNormal(mean=20_000, std=12_000)
|
|
39
|
+
self.raised_bridge = Boolean(p=0.3)
|
|
40
|
+
self.bridge_amount = Normal(mean=150_000, std=50_000) if self.raised_bridge else 0
|
|
41
|
+
|
|
42
|
+
net_burn = self.monthly_burn - self.monthly_revenue
|
|
43
|
+
self.runway_months = (500_000 + self.bridge_amount) / net_burn if net_burn > 0 else 999
|
|
44
|
+
self.default_risk = self.runway_months < 12
|
|
45
|
+
|
|
46
|
+
result = StartupRunway.run(n=10_000, seed=42)
|
|
47
|
+
print(result.df.describe())
|
|
48
|
+
result.plot_tornado(target="runway_months")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This is the entire model. It fits in a code review. It runs in CI. It can be parameterized, tested, and shipped as a package.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install kansim
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv add kansim
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or clone and run examples directly with [uv](https://github.com/astral-sh/uv):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/kansim/kansim
|
|
71
|
+
cd kansim
|
|
72
|
+
uv run examples/startup_runway.py
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
Subclass `Simulation`. Assign inputs using distribution functions and compute outputs — all inside `__init__`. Call `.run(n=)` to get a `SimulationResult`.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from kansim import Simulation, Triangular, Normal
|
|
83
|
+
|
|
84
|
+
class BridgeFatigue(Simulation):
|
|
85
|
+
|
|
86
|
+
def __init__(self):
|
|
87
|
+
self.daily_load = Normal(mean=500, std=80) # tonnes
|
|
88
|
+
self.material_life = Triangular(min=40, most_likely=60, max=90) # years
|
|
89
|
+
self.cycles = self.daily_load * 365
|
|
90
|
+
self.failure_risk = self.cycles > self.material_life * 150_000
|
|
91
|
+
|
|
92
|
+
result = BridgeFatigue.run(n=10_000, seed=42)
|
|
93
|
+
result.plot_histogram("material_life")
|
|
94
|
+
result.plot_tornado(target="failure_risk")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
All results write to `results/<timestamp>/`. Pass `save="file.png"` to any plot to save instead of show.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
result.result_id # "2024-03-10_143022"
|
|
101
|
+
result.df # pandas DataFrame — filter and pass back to any plot
|
|
102
|
+
result.save_csv()
|
|
103
|
+
result.save_parquet()
|
|
104
|
+
result.plot_histogram("col", save="hist.png")
|
|
105
|
+
result.plot_cdf("col")
|
|
106
|
+
result.plot_scatter("x", "y")
|
|
107
|
+
result.plot_tornado(target="col")
|
|
108
|
+
|
|
109
|
+
# filtered plots
|
|
110
|
+
passing = result.df[result.df.failure_risk == False]
|
|
111
|
+
result.plot_histogram("material_life", df=passing, save="safe_only.png")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Distributions
|
|
117
|
+
|
|
118
|
+
| Function | Parameters |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `Normal` | `mean, std, min=None, max=None` |
|
|
121
|
+
| `LogNormal` | `mean, std, min=None, max=None` |
|
|
122
|
+
| `Uniform` | `min, max, log=False` |
|
|
123
|
+
| `Triangular` | `min, most_likely, max` |
|
|
124
|
+
| `BetaPERT` | `min, most_likely, max` |
|
|
125
|
+
| `Beta` | `successes, failures` → 0–1 range |
|
|
126
|
+
| `GeneralizedBeta` | `mean, std, min, max` |
|
|
127
|
+
| `Exponential` | `mean` |
|
|
128
|
+
| `Gamma` | `mean, std, min=None, max=None` |
|
|
129
|
+
| `Weibull` | `scale, shape, min=0, max=None` |
|
|
130
|
+
| `Pareto` | `shape, mode, max=None` |
|
|
131
|
+
| `Poisson` | `expected` |
|
|
132
|
+
| `Binomial` | `n, p` |
|
|
133
|
+
| `NegativeBinomial` | `successes, p` |
|
|
134
|
+
| `StudentT` | `df` |
|
|
135
|
+
| `PearsonIII` | `location, scale, shape` |
|
|
136
|
+
| `ExtremeValue` | `location, scale` |
|
|
137
|
+
| `Discrete` | `values=[], probs=[]` |
|
|
138
|
+
| `Cumulative` | `values=[], probs=[]` |
|
|
139
|
+
| `SampledResults` | `values=[]` |
|
|
140
|
+
| `Boolean` | `p` |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Using AI to generate simulations
|
|
145
|
+
|
|
146
|
+
kansim is designed to be written by humans or AI agents. The model structure is simple enough that a language model can draft a complete, runnable simulation from a plain English description.
|
|
147
|
+
|
|
148
|
+
### Instructions for an AI agent
|
|
149
|
+
|
|
150
|
+
Paste the following into your system prompt or alongside your request:
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
> Use the kansim framework by subclassing `Simulation` and assigning all variables in `__init__`.
|
|
155
|
+
> Inputs use distribution functions, outputs are computed from them. Everything assigned to `self` becomes a DataFrame column.
|
|
156
|
+
>
|
|
157
|
+
> ```python
|
|
158
|
+
> from kansim import Simulation, Normal, Triangular, Beta
|
|
159
|
+
>
|
|
160
|
+
> class MyModel(Simulation):
|
|
161
|
+
> def __init__(self):
|
|
162
|
+
> self.input_a = Normal(mean=100, std=10)
|
|
163
|
+
> self.input_b = Triangular(min=1, most_likely=2, max=5)
|
|
164
|
+
> self.output_x = self.input_a * self.input_b
|
|
165
|
+
>
|
|
166
|
+
> result = MyModel.run(n=10_000, seed=42)
|
|
167
|
+
> ```
|
|
168
|
+
>
|
|
169
|
+
> `run()` returns a `SimulationResult` with:
|
|
170
|
+
> - `result.result_id` — human readable timestamp e.g. `"2024-03-10_143022"`
|
|
171
|
+
> - `result.df` — pandas DataFrame of all runs
|
|
172
|
+
> - `result.save_csv()`, `result.save_parquet()` — saves to `results/<result_id>/`
|
|
173
|
+
> - `result.plot_histogram(col, df=None, save=None)`
|
|
174
|
+
> - `result.plot_cdf(col, df=None, save=None)`
|
|
175
|
+
> - `result.plot_scatter(x, y, df=None, save=None)`
|
|
176
|
+
> - `result.plot_tornado(target, df=None, save=None)`
|
|
177
|
+
>
|
|
178
|
+
> All plots accept an optional `df` argument for filtered/modified DataFrames.
|
|
179
|
+
> All saves write to `results/<result_id>/` — pass `save="filename.png"` to save instead of show.
|
|
180
|
+
>
|
|
181
|
+
> Available distributions:
|
|
182
|
+
> `Normal(mean, std, min=None, max=None)`,
|
|
183
|
+
> `LogNormal(mean, std, min=None, max=None)`,
|
|
184
|
+
> `Uniform(min, max, log=False)`,
|
|
185
|
+
> `Triangular(min, most_likely, max)`,
|
|
186
|
+
> `BetaPERT(min, most_likely, max)`,
|
|
187
|
+
> `Beta(successes, failures)`,
|
|
188
|
+
> `GeneralizedBeta(mean, std, min, max)`,
|
|
189
|
+
> `Exponential(mean)`,
|
|
190
|
+
> `Gamma(mean, std, min=None, max=None)`,
|
|
191
|
+
> `Weibull(scale, shape, min=0, max=None)`,
|
|
192
|
+
> `Pareto(shape, mode, max=None)`,
|
|
193
|
+
> `Poisson(expected)`,
|
|
194
|
+
> `Binomial(n, p)`,
|
|
195
|
+
> `NegativeBinomial(successes, p)`,
|
|
196
|
+
> `StudentT(df)`,
|
|
197
|
+
> `PearsonIII(location, scale, shape)`,
|
|
198
|
+
> `ExtremeValue(location, scale)`,
|
|
199
|
+
> `Discrete(values=[], probs=[])`,
|
|
200
|
+
> `Cumulative(values=[], probs=[])`,
|
|
201
|
+
> `SampledResults(values=[])`,
|
|
202
|
+
> `Boolean(p)`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Example prompt
|
|
207
|
+
|
|
208
|
+
> Using kansim, simulate the cost overrun risk for a construction project. Inputs should include labor cost, material cost, and weather delay days. Output should include total cost and whether the project exceeds budget.
|
|
209
|
+
|
|
210
|
+
The AI will return a complete, runnable Python file.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Examples
|
|
215
|
+
|
|
216
|
+
| File | Description |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `examples/startup_runway.py` | Startup cash runway under uncertain growth and events |
|
|
219
|
+
| `examples/poker_hand_equity.py` | Heads-up poker equity over 10,000 deals |
|
|
220
|
+
| `examples/water_treatment_plant.py` | Water treatment plant throughput and cost |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from kansim import Simulation, Boolean
|
|
3
|
+
|
|
4
|
+
RANKS = "23456789TJQKA"
|
|
5
|
+
SUITS = "cdhs"
|
|
6
|
+
DECK = [r + s for r in RANKS for s in SUITS]
|
|
7
|
+
|
|
8
|
+
RANK_ORDER = {r: i for i, r in enumerate(RANKS)}
|
|
9
|
+
|
|
10
|
+
def deal(deck, n):
|
|
11
|
+
hand = random.sample(deck, n)
|
|
12
|
+
for c in hand:
|
|
13
|
+
deck.remove(c)
|
|
14
|
+
return hand
|
|
15
|
+
|
|
16
|
+
def best_hand_rank(cards):
|
|
17
|
+
"""Returns a comparable tuple representing the best 5-card hand rank."""
|
|
18
|
+
from itertools import combinations
|
|
19
|
+
return max(_hand_rank(h) for h in combinations(cards, 5))
|
|
20
|
+
|
|
21
|
+
def _hand_rank(hand):
|
|
22
|
+
ranks = sorted([RANK_ORDER[c[0]] for c in hand], reverse=True)
|
|
23
|
+
suits = [c[1] for c in hand]
|
|
24
|
+
flush = len(set(suits)) == 1
|
|
25
|
+
straight = (ranks[0] - ranks[4] == 4 and len(set(ranks)) == 5) or ranks == [12, 3, 2, 1, 0]
|
|
26
|
+
counts = sorted([ranks.count(r) for r in set(ranks)], reverse=True)
|
|
27
|
+
if straight and flush: return (8, ranks)
|
|
28
|
+
if counts[0] == 4: return (7, ranks)
|
|
29
|
+
if counts[:2] == [3,2]: return (6, ranks)
|
|
30
|
+
if flush: return (5, ranks)
|
|
31
|
+
if straight: return (4, ranks)
|
|
32
|
+
if counts[0] == 3: return (3, ranks)
|
|
33
|
+
if counts[:2] == [2,2]: return (2, ranks)
|
|
34
|
+
if counts[0] == 2: return (1, ranks)
|
|
35
|
+
return (0, ranks)
|
|
36
|
+
|
|
37
|
+
class PokerHandEquity(Simulation):
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
deck = DECK.copy()
|
|
41
|
+
|
|
42
|
+
self.hole1, self.hole2 = deal(deck, 2), deal(deck, 2)
|
|
43
|
+
community = deal(deck, 5)
|
|
44
|
+
|
|
45
|
+
best1 = best_hand_rank(self.hole1 + community)
|
|
46
|
+
best2 = best_hand_rank(self.hole2 + community)
|
|
47
|
+
|
|
48
|
+
self.player1_wins = best1 > best2
|
|
49
|
+
self.player2_wins = best2 > best1
|
|
50
|
+
self.split = best1 == best2
|
|
51
|
+
|
|
52
|
+
self.hole1 = " ".join(self.hole1)
|
|
53
|
+
self.hole2 = " ".join(self.hole2)
|
|
54
|
+
|
|
55
|
+
result = PokerHandEquity.run(n=10_000, seed=42)
|
|
56
|
+
print(result.df[["player1_wins", "player2_wins", "split"]].mean().round(3))
|
|
57
|
+
result.plot_histogram("player1_wins")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from kansim import Simulation, Normal, Triangular, LogNormal, Boolean
|
|
2
|
+
|
|
3
|
+
class StartupRunway(Simulation):
|
|
4
|
+
|
|
5
|
+
def __init__(self):
|
|
6
|
+
# Financials
|
|
7
|
+
self.initial_cash = 500_000
|
|
8
|
+
self.monthly_burn = Normal(mean=45_000, std=8_000)
|
|
9
|
+
self.monthly_revenue = LogNormal(mean=20_000, std=12_000)
|
|
10
|
+
|
|
11
|
+
# Growth
|
|
12
|
+
self.revenue_growth_rate = Triangular(min=0.02, most_likely=0.08, max=0.20)
|
|
13
|
+
self.churn_rate = Triangular(min=0.02, most_likely=0.06, max=0.15)
|
|
14
|
+
|
|
15
|
+
# Events
|
|
16
|
+
self.raised_bridge_round = Boolean(p=0.3)
|
|
17
|
+
self.bridge_amount = Normal(mean=150_000, std=50_000) if self.raised_bridge_round else 0
|
|
18
|
+
self.lost_anchor_customer = Boolean(p=0.2)
|
|
19
|
+
self.anchor_revenue_loss = Normal(mean=8_000, std=2_000) if self.lost_anchor_customer else 0
|
|
20
|
+
|
|
21
|
+
# Derived
|
|
22
|
+
net_revenue = self.monthly_revenue - self.anchor_revenue_loss
|
|
23
|
+
net_monthly_burn = self.monthly_burn - net_revenue
|
|
24
|
+
total_cash = self.initial_cash + self.bridge_amount
|
|
25
|
+
|
|
26
|
+
self.runway_months = total_cash / net_monthly_burn if net_monthly_burn > 0 else 999
|
|
27
|
+
self.default_risk = self.runway_months < 12
|
|
28
|
+
self.healthy = self.runway_months > 18
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__=="__main__":
|
|
32
|
+
result = StartupRunway.run(n=10_000, seed=42)
|
|
33
|
+
result.plot_histogram("runway_months")
|
|
34
|
+
result.plot_tornado(target="runway_months")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from kansim import *
|
|
2
|
+
|
|
3
|
+
class WaterTreatmentPlant(Simulation):
|
|
4
|
+
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.flow_rate = Normal(mean=1000, std=50)
|
|
7
|
+
self.removal_efficiency = Beta(successes=9, failures=1)
|
|
8
|
+
self.operating_cost = Triangular(min=0.10, most_likely=0.15, max=0.25)
|
|
9
|
+
|
|
10
|
+
treated = self.flow_rate * self.removal_efficiency
|
|
11
|
+
self.treated_volume = treated
|
|
12
|
+
self.daily_cost = treated * self.operating_cost
|
|
13
|
+
self.pass_rate = treated > 900
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
result = WaterTreatmentPlant.run(n=10_000, seed=42)
|
|
18
|
+
print(result)
|
|
19
|
+
print(result.result_id)
|
|
20
|
+
print(result.df.describe())
|