AccruedInnovation commited on
Commit
d72cf5b
·
verified ·
1 Parent(s): c43f411

Upload app.py

Browse files
Files changed (1) hide show
  1. code/app.py +873 -0
code/app.py ADDED
@@ -0,0 +1,873 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import panel as pn
2
+ import pandas as pd
3
+ import param
4
+ from bokeh.models.formatters import PrintfTickFormatter
5
+
6
+ # Initialize Panel extension for Tabulator and set a global sizing mode
7
+ pn.extension(
8
+ "tabulator",
9
+ sizing_mode="stretch_width",
10
+ template="fast",
11
+ # theme = 'dark',
12
+ )
13
+
14
+ # --- Styling Placeholders (as per user instruction) ---
15
+ slider_design = {}
16
+ slider_style = {}
17
+ slider_stylesheet = []
18
+
19
+
20
+ # --- Helper for NumberFormatters ---
21
+ def get_formatter(format_str):
22
+ if format_str == "%i":
23
+ return pn.widgets.tables.NumberFormatter(format="0")
24
+ elif format_str == "%.1f":
25
+ return pn.widgets.tables.NumberFormatter(format="0.0")
26
+ elif format_str == "%.2f":
27
+ return pn.widgets.tables.NumberFormatter(format="0.00")
28
+ elif format_str == "%.4f":
29
+ return pn.widgets.tables.NumberFormatter(format="0.0000")
30
+ elif format_str == "$%.02f":
31
+ return pn.widgets.tables.NumberFormatter(format="$0,0.00")
32
+ return format_str
33
+
34
+
35
+ class CannabinoidEstimator(param.Parameterized):
36
+ # --- Input Parameters ---
37
+ kg_processed_per_hour = param.Number(
38
+ default=150.0,
39
+ bounds=(0, 2000),
40
+ step=1.0,
41
+ label="Biomass processed per hour (kg)",
42
+ )
43
+ finished_product_yield_pct = param.Number(
44
+ default=60.0,
45
+ bounds=(0.01, 100),
46
+ step=0.01,
47
+ label="Product yield: CBx Weight Output / Weight Input (%)",
48
+ )
49
+ kwh_rate = param.Number(
50
+ default=0.25, bounds=(0.01, 5), step=0.01, label="Power rate ($ per kWh)"
51
+ )
52
+ water_cost_per_1000l = param.Number(
53
+ default=2.50,
54
+ bounds=(0.01, 10),
55
+ step=0.01,
56
+ label="Water rate ($ per 1000L / m3)",
57
+ )
58
+ consumables_per_kg_bio_rate = param.Number(
59
+ default=0.0032,
60
+ bounds=(0, 10),
61
+ step=0.0001,
62
+ label="Other Consumables rate ($ per kg biomass)",
63
+ )
64
+ kwh_per_kg_bio = param.Number(
65
+ default=0.25,
66
+ bounds=(0.05, 15),
67
+ step=0.01,
68
+ label="Power consumption (kWh per kg biomass)",
69
+ )
70
+ water_liters_consumed_per_kg_bio = param.Number(
71
+ default=3.0,
72
+ bounds=(0.1, 100),
73
+ step=0.1,
74
+ label="Water consumption (liters per kg biomass)",
75
+ )
76
+ consumables_per_kg_output = param.Number(
77
+ default=10.0,
78
+ bounds=(0, 100),
79
+ step=0.01,
80
+ label="Consumables per kg finished product ($)",
81
+ )
82
+ bio_cbx_pct = param.Number(
83
+ default=10.0, bounds=(0, 30), step=0.1, label="Cannabinoid (CBx) in biomass (%)"
84
+ )
85
+ bio_cost = param.Number(
86
+ default=3.0,
87
+ bounds=(0, 200),
88
+ step=0.25,
89
+ label="Biomass purchase cost ($ per kg)",
90
+ )
91
+ wholesale_cbx_price = param.Number(
92
+ default=220.0,
93
+ bounds=(25, 6000),
94
+ step=5.0,
95
+ label="Gross revenue ($ per kg output)",
96
+ )
97
+ wholesale_cbx_pct = param.Number(
98
+ default=99.9, bounds=(0, 100), step=0.01, label="CBx in finished product (%)"
99
+ )
100
+ batch_test_cost = param.Number(
101
+ default=1300.0,
102
+ bounds=(100, 5000),
103
+ step=25.0,
104
+ label="Per-batch testing/compliance costs ($)",
105
+ )
106
+ fixed_overhead_per_week = param.Number(
107
+ default=2000.0, bounds=(0, 10000), step=1.0, label="Weekly fixed costs ($)"
108
+ )
109
+ workers_per_shift = param.Number(
110
+ default=9.0, bounds=(1, 20), step=1.0, label="Workers per shift"
111
+ )
112
+ worker_hourly_rate = param.Number(
113
+ default=5.0, bounds=(0.25, 50), step=0.25, label="Worker loaded pay rate ($/hr)"
114
+ )
115
+ managers_per_shift = param.Number(
116
+ default=1.0, bounds=(1, 10), step=1.0, label="Supervisors per shift"
117
+ )
118
+ manager_hourly_rate = param.Number(
119
+ default=10.0,
120
+ bounds=(5.0, 50),
121
+ step=0.25,
122
+ label="Supervisor loaded pay rate ($/hr)",
123
+ )
124
+ processing_hours_per_shift = param.Number(
125
+ default=7.0, bounds=(0.25, 8.0), step=0.25, label="Processing hours per shift"
126
+ )
127
+ labour_hours_per_shift = param.Number(
128
+ default=8.0, bounds=(6.0, 12), step=0.25, label="Labor hours per shift"
129
+ )
130
+ shifts_per_day = param.Number(
131
+ default=3.0, bounds=(1, 10), step=1.0, label="Shifts per day"
132
+ )
133
+ shifts_per_week = param.Number(
134
+ default=21.0, bounds=(1, 28), step=1.0, label="Shifts per week"
135
+ )
136
+
137
+ kg_processed_per_shift = 0.0
138
+ labour_cost_per_shift = 0.0
139
+ variable_cost_per_shift = 0.0
140
+ overhead_cost_per_shift = 0.0
141
+ saleable_kg_per_kg_bio = 0.0
142
+ saleable_kg_per_shift = 0.0
143
+ saleable_kg_per_day = 0.0
144
+ saleable_kg_per_week = 0.0
145
+ biomass_kg_per_saleable_kg = 0.0
146
+ internal_cogs_per_kg_bio = 0.0
147
+ internal_cogs_per_shift = 0.0
148
+ internal_cogs_per_day = 0.0
149
+ internal_cogs_per_week = 0.0
150
+ internal_cogs_per_kg_output = 0.0
151
+ biomass_cost_per_shift = 0.0
152
+ biomass_cost_per_day = 0.0
153
+ biomass_cost_per_week = 0.0
154
+ biomass_cost_per_kg_output = 0.0
155
+ gross_rev_per_kg_bio = 0.0
156
+ gross_rev_per_shift = 0.0
157
+ gross_rev_per_day = 0.0
158
+ gross_rev_per_week = 0.0
159
+ net_rev_per_kg_bio = 0.0
160
+ net_rev_per_shift = 0.0
161
+ net_rev_per_day = 0.0
162
+ net_rev_per_week = 0.0
163
+ net_rev_per_kg_output = 0.0
164
+ operating_profit_pct = 0.0
165
+ resin_spread_pct = 0.0
166
+
167
+ money_data_df = param.DataFrame(pd.DataFrame())
168
+ profit_data_df = param.DataFrame(pd.DataFrame())
169
+ processing_data_df = param.DataFrame(pd.DataFrame())
170
+
171
+ def __init__(self, **params):
172
+ super().__init__(**params)
173
+ self._create_sliders()
174
+ self.money_table = pn.widgets.Tabulator(
175
+ self.money_data_df,
176
+ formatters=self._get_money_formatters(),
177
+ disabled=True,
178
+ layout="fit_data_table",
179
+ sizing_mode="fixed",
180
+ align="center",
181
+ show_index=False, # Hide index column
182
+ )
183
+ self.profit_table = pn.widgets.Tabulator(
184
+ self.profit_data_df,
185
+ disabled=True,
186
+ layout="fit_data_table",
187
+ sizing_mode="fixed",
188
+ align="center",
189
+ show_index=False, # Hide index column
190
+ )
191
+ self.processing_table = pn.widgets.Tabulator(
192
+ self.processing_data_df,
193
+ formatters={},
194
+ disabled=True,
195
+ layout="fit_data_table",
196
+ sizing_mode="fixed",
197
+ align="center",
198
+ show_index=False, # Hide index column
199
+ )
200
+ self._update_calculations()
201
+
202
+ def _create_sliders(self):
203
+ self.kg_processed_per_hour_slider = pn.widgets.EditableFloatSlider.from_param(
204
+ self.param.kg_processed_per_hour,
205
+ name=self.param.kg_processed_per_hour.label, # Updated
206
+ fixed_start=self.param.kg_processed_per_hour.bounds[0],
207
+ fixed_end=self.param.kg_processed_per_hour.bounds[1],
208
+ design=slider_design,
209
+ styles=slider_style,
210
+ stylesheets=slider_stylesheet,
211
+ # format="0",
212
+ format=PrintfTickFormatter(format="%i kg"),
213
+ )
214
+ self.finished_product_yield_pct_slider = (
215
+ pn.widgets.EditableFloatSlider.from_param(
216
+ self.param.finished_product_yield_pct,
217
+ name=self.param.finished_product_yield_pct.label, # Updated
218
+ fixed_start=self.param.finished_product_yield_pct.bounds[0],
219
+ fixed_end=self.param.finished_product_yield_pct.bounds[1],
220
+ design=slider_design,
221
+ styles=slider_style,
222
+ stylesheets=slider_stylesheet,
223
+ format="0.00",
224
+ )
225
+ )
226
+ self.kwh_rate_slider = pn.widgets.EditableFloatSlider.from_param(
227
+ self.param.kwh_rate,
228
+ name=self.param.kwh_rate.label, # Updated
229
+ fixed_start=self.param.kwh_rate.bounds[0],
230
+ fixed_end=self.param.kwh_rate.bounds[1],
231
+ design=slider_design,
232
+ styles=slider_style,
233
+ stylesheets=slider_stylesheet,
234
+ format="0.00",
235
+ # format=PrintfTickFormatter(format='%.2f per kWh'),
236
+ )
237
+ self.water_cost_per_1000l_slider = pn.widgets.EditableFloatSlider.from_param(
238
+ self.param.water_cost_per_1000l,
239
+ name=self.param.water_cost_per_1000l.label, # Updated
240
+ fixed_start=self.param.water_cost_per_1000l.bounds[0],
241
+ fixed_end=self.param.water_cost_per_1000l.bounds[1],
242
+ design=slider_design,
243
+ styles=slider_style,
244
+ stylesheets=slider_stylesheet,
245
+ format="0.00",
246
+ )
247
+ self.consumables_per_kg_bio_rate_slider = (
248
+ pn.widgets.EditableFloatSlider.from_param(
249
+ self.param.consumables_per_kg_bio_rate,
250
+ name=self.param.consumables_per_kg_bio_rate.label, # Updated
251
+ fixed_start=self.param.consumables_per_kg_bio_rate.bounds[0],
252
+ fixed_end=self.param.consumables_per_kg_bio_rate.bounds[1],
253
+ design=slider_design,
254
+ styles=slider_style,
255
+ stylesheets=slider_stylesheet,
256
+ format="0.0000",
257
+ )
258
+ )
259
+ self.kwh_per_kg_bio_slider = pn.widgets.EditableFloatSlider.from_param(
260
+ self.param.kwh_per_kg_bio,
261
+ name=self.param.kwh_per_kg_bio.label, # Updated
262
+ fixed_start=self.param.kwh_per_kg_bio.bounds[0],
263
+ fixed_end=self.param.kwh_per_kg_bio.bounds[1],
264
+ design=slider_design,
265
+ styles=slider_style,
266
+ stylesheets=slider_stylesheet,
267
+ format="0.00",
268
+ )
269
+ self.water_liters_consumed_per_kg_bio_slider = (
270
+ pn.widgets.EditableFloatSlider.from_param(
271
+ self.param.water_liters_consumed_per_kg_bio,
272
+ name=self.param.water_liters_consumed_per_kg_bio.label, # Updated
273
+ fixed_start=self.param.water_liters_consumed_per_kg_bio.bounds[0],
274
+ fixed_end=self.param.water_liters_consumed_per_kg_bio.bounds[1],
275
+ design=slider_design,
276
+ styles=slider_style,
277
+ stylesheets=slider_stylesheet,
278
+ format="0.0",
279
+ )
280
+ )
281
+ self.consumables_per_kg_output_slider = (
282
+ pn.widgets.EditableFloatSlider.from_param(
283
+ self.param.consumables_per_kg_output,
284
+ name=self.param.consumables_per_kg_output.label, # Updated
285
+ fixed_start=self.param.consumables_per_kg_output.bounds[0],
286
+ fixed_end=self.param.consumables_per_kg_output.bounds[1],
287
+ design=slider_design,
288
+ styles=slider_style,
289
+ stylesheets=slider_stylesheet,
290
+ format="0.00",
291
+ )
292
+ )
293
+ self.bio_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param(
294
+ self.param.bio_cbx_pct,
295
+ name=self.param.bio_cbx_pct.label, # Updated
296
+ fixed_start=self.param.bio_cbx_pct.bounds[0],
297
+ fixed_end=self.param.bio_cbx_pct.bounds[1],
298
+ design=slider_design,
299
+ styles=slider_style,
300
+ stylesheets=slider_stylesheet,
301
+ format="0.0",
302
+ )
303
+ self.bio_cost_slider = pn.widgets.EditableFloatSlider.from_param(
304
+ self.param.bio_cost,
305
+ name=self.param.bio_cost.label, # Updated
306
+ fixed_start=self.param.bio_cost.bounds[0],
307
+ fixed_end=self.param.bio_cost.bounds[1],
308
+ design=slider_design,
309
+ styles=slider_style,
310
+ stylesheets=slider_stylesheet,
311
+ format="0.00",
312
+ )
313
+ self.wholesale_cbx_price_slider = pn.widgets.EditableFloatSlider.from_param(
314
+ self.param.wholesale_cbx_price,
315
+ name=self.param.wholesale_cbx_price.label, # Updated
316
+ fixed_start=self.param.wholesale_cbx_price.bounds[0],
317
+ fixed_end=self.param.wholesale_cbx_price.bounds[1],
318
+ design=slider_design,
319
+ styles=slider_style,
320
+ stylesheets=slider_stylesheet,
321
+ format="0",
322
+ )
323
+ self.wholesale_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param(
324
+ self.param.wholesale_cbx_pct,
325
+ name=self.param.wholesale_cbx_pct.label, # Updated
326
+ fixed_start=self.param.wholesale_cbx_pct.bounds[0],
327
+ fixed_end=self.param.wholesale_cbx_pct.bounds[1],
328
+ design=slider_design,
329
+ styles=slider_style,
330
+ stylesheets=slider_stylesheet,
331
+ format="0.00",
332
+ )
333
+ self.batch_test_cost_slider = pn.widgets.EditableFloatSlider.from_param(
334
+ self.param.batch_test_cost,
335
+ name=self.param.batch_test_cost.label, # Updated
336
+ fixed_start=self.param.batch_test_cost.bounds[0],
337
+ fixed_end=self.param.batch_test_cost.bounds[1],
338
+ design=slider_design,
339
+ styles=slider_style,
340
+ stylesheets=slider_stylesheet,
341
+ format="0",
342
+ )
343
+ self.fixed_overhead_per_week_slider = pn.widgets.EditableFloatSlider.from_param(
344
+ self.param.fixed_overhead_per_week,
345
+ name=self.param.fixed_overhead_per_week.label, # Updated
346
+ fixed_start=self.param.fixed_overhead_per_week.bounds[0],
347
+ fixed_end=self.param.fixed_overhead_per_week.bounds[1],
348
+ design=slider_design,
349
+ styles=slider_style,
350
+ stylesheets=slider_stylesheet,
351
+ format="0",
352
+ )
353
+ self.workers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param(
354
+ self.param.workers_per_shift,
355
+ name=self.param.workers_per_shift.label, # Updated
356
+ fixed_start=self.param.workers_per_shift.bounds[0],
357
+ fixed_end=self.param.workers_per_shift.bounds[1],
358
+ design=slider_design,
359
+ styles=slider_style,
360
+ stylesheets=slider_stylesheet,
361
+ format="0",
362
+ )
363
+ self.worker_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param(
364
+ self.param.worker_hourly_rate,
365
+ name=self.param.worker_hourly_rate.label, # Updated
366
+ fixed_start=self.param.worker_hourly_rate.bounds[0],
367
+ fixed_end=self.param.worker_hourly_rate.bounds[1],
368
+ design=slider_design,
369
+ styles=slider_style,
370
+ stylesheets=slider_stylesheet,
371
+ format="0.00",
372
+ )
373
+ self.managers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param(
374
+ self.param.managers_per_shift,
375
+ name=self.param.managers_per_shift.label, # Updated
376
+ fixed_start=self.param.managers_per_shift.bounds[0],
377
+ fixed_end=self.param.managers_per_shift.bounds[1],
378
+ design=slider_design,
379
+ styles=slider_style,
380
+ stylesheets=slider_stylesheet,
381
+ format="0",
382
+ )
383
+ self.manager_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param(
384
+ self.param.manager_hourly_rate,
385
+ name=self.param.manager_hourly_rate.label, # Updated
386
+ fixed_start=self.param.worker_hourly_rate.default, # Keeping original logic as per file
387
+ fixed_end=self.param.manager_hourly_rate.bounds[1],
388
+ design=slider_design,
389
+ styles=slider_style,
390
+ stylesheets=slider_stylesheet,
391
+ format="0.00",
392
+ )
393
+
394
+ # Updated based on previous request
395
+ self.labour_hours_per_shift_slider = pn.widgets.EditableFloatSlider.from_param(
396
+ self.param.labour_hours_per_shift,
397
+ name=self.param.labour_hours_per_shift.label, # Updated
398
+ fixed_start=self.param.labour_hours_per_shift.bounds[
399
+ 0
400
+ ], # Changed in previous request
401
+ fixed_end=self.param.labour_hours_per_shift.bounds[1],
402
+ design=slider_design,
403
+ styles=slider_style,
404
+ stylesheets=slider_stylesheet,
405
+ format="0.00",
406
+ )
407
+
408
+ # Updated based on previous request
409
+ self.processing_hours_per_shift_slider = (
410
+ pn.widgets.EditableFloatSlider.from_param(
411
+ self.param.processing_hours_per_shift,
412
+ name=self.param.processing_hours_per_shift.label, # Updated
413
+ fixed_start=self.param.processing_hours_per_shift.bounds[0],
414
+ fixed_end=self.labour_hours_per_shift, # Changed in previous request
415
+ design=slider_design,
416
+ styles=slider_style,
417
+ stylesheets=slider_stylesheet,
418
+ format="0.00",
419
+ )
420
+ )
421
+
422
+ self.shifts_per_day_slider = pn.widgets.EditableFloatSlider.from_param(
423
+ self.param.shifts_per_day,
424
+ name=self.param.shifts_per_day.label, # Updated
425
+ fixed_start=self.param.shifts_per_day.bounds[0],
426
+ fixed_end=self.param.shifts_per_day.bounds[1],
427
+ design=slider_design,
428
+ styles=slider_style,
429
+ stylesheets=slider_stylesheet,
430
+ format="0",
431
+ )
432
+ self.shifts_per_week_slider = pn.widgets.EditableFloatSlider.from_param(
433
+ self.param.shifts_per_week,
434
+ name=self.param.shifts_per_week.label, # Updated
435
+ fixed_start=self.param.shifts_per_week.bounds[0],
436
+ fixed_end=self.param.shifts_per_week.bounds[1],
437
+ design=slider_design,
438
+ styles=slider_style,
439
+ stylesheets=slider_stylesheet,
440
+ format="0",
441
+ )
442
+
443
+ @param.depends(
444
+ "kg_processed_per_hour",
445
+ "finished_product_yield_pct",
446
+ "kwh_rate",
447
+ "water_cost_per_1000l",
448
+ "consumables_per_kg_bio_rate",
449
+ "kwh_per_kg_bio",
450
+ "water_liters_consumed_per_kg_bio",
451
+ "consumables_per_kg_output",
452
+ "bio_cbx_pct",
453
+ "bio_cost",
454
+ "wholesale_cbx_price",
455
+ "wholesale_cbx_pct",
456
+ "batch_test_cost",
457
+ "fixed_overhead_per_week",
458
+ "workers_per_shift",
459
+ "worker_hourly_rate",
460
+ "managers_per_shift",
461
+ "manager_hourly_rate",
462
+ "labour_hours_per_shift",
463
+ "processing_hours_per_shift",
464
+ "shifts_per_day",
465
+ "shifts_per_week",
466
+ watch=True,
467
+ )
468
+ def _update_calculations(self, *events):
469
+ self.kg_processed_per_shift = (
470
+ self.processing_hours_per_shift * self.kg_processed_per_hour
471
+ )
472
+ if self.shifts_per_week == 0:
473
+ self.shifts_per_week = 1
474
+
475
+ self._calc_saleable_kg()
476
+ self._calc_biomass_cost()
477
+ self._calc_cogs()
478
+ self._calc_gross_revenue()
479
+ self._calc_net_revenue()
480
+
481
+ self.operating_profit_pct = (
482
+ (self.net_rev_per_kg_bio / self.gross_rev_per_kg_bio)
483
+ if self.gross_rev_per_kg_bio
484
+ else 0.0
485
+ )
486
+ self.resin_spread_pct = (
487
+ ((self.gross_rev_per_kg_bio - self.bio_cost) / self.bio_cost)
488
+ if self.bio_cost
489
+ else 0.0
490
+ )
491
+
492
+ self._update_tables_data()
493
+
494
+ @param.depends("labour_hours_per_shift", watch=True)
495
+ def _update_processing_hours_slider_constraints(self):
496
+ new_max_processing_hours = self.labour_hours_per_shift
497
+
498
+ # Get the current lower bound of the processing_hours_per_shift parameter
499
+ current_min_processing_hours = self.param.processing_hours_per_shift.bounds[0]
500
+
501
+ # Update the bounds of the underlying param.Number object for processing_hours_per_shift
502
+ # This allows the parameter to accept values up to the new maximum
503
+ self.param.processing_hours_per_shift.bounds = (
504
+ current_min_processing_hours,
505
+ new_max_processing_hours,
506
+ )
507
+
508
+ # Ensure the slider widget has been created before trying to access it
509
+ if hasattr(self, "processing_hours_per_shift_slider"):
510
+ # Update the 'end' property of the slider widget
511
+ self.processing_hours_per_shift_slider.end = new_max_processing_hours
512
+
513
+ # If the current value of processing_hours_per_shift is now greater than
514
+ # the new maximum, adjust it to be the new maximum.
515
+ if self.processing_hours_per_shift > new_max_processing_hours:
516
+ self.processing_hours_per_shift = new_max_processing_hours
517
+
518
+ def _calc_cogs(self):
519
+ worker_cost = self.workers_per_shift * self.worker_hourly_rate
520
+ manager_cost = self.managers_per_shift * self.manager_hourly_rate
521
+ self.labour_cost_per_shift = (
522
+ worker_cost + manager_cost
523
+ ) * self.labour_hours_per_shift
524
+
525
+ power_cost_per_kg = self.kwh_rate * self.kwh_per_kg_bio
526
+ water_cost_per_kg = (
527
+ self.water_cost_per_1000l / 1000.0
528
+ ) * self.water_liters_consumed_per_kg_bio
529
+ total_variable_consumable_cost_per_kg = (
530
+ self.consumables_per_kg_bio_rate + power_cost_per_kg + water_cost_per_kg
531
+ )
532
+ self.variable_cost_per_shift = (
533
+ total_variable_consumable_cost_per_kg * self.kg_processed_per_shift
534
+ )
535
+
536
+ self.overhead_cost_per_shift = (
537
+ self.fixed_overhead_per_week / self.shifts_per_week
538
+ if self.shifts_per_week > 0
539
+ else 0.0
540
+ )
541
+
542
+ shift_cogs_before_output_specific = (
543
+ self.labour_cost_per_shift
544
+ + self.variable_cost_per_shift
545
+ + self.overhead_cost_per_shift
546
+ )
547
+ shift_output_specific_cogs = (
548
+ self.consumables_per_kg_output * self.saleable_kg_per_shift
549
+ )
550
+
551
+ self.internal_cogs_per_shift = (
552
+ shift_cogs_before_output_specific + shift_output_specific_cogs
553
+ )
554
+ self.internal_cogs_per_kg_bio = (
555
+ self.internal_cogs_per_shift / self.kg_processed_per_shift
556
+ if self.kg_processed_per_shift > 0
557
+ else 0.0
558
+ )
559
+ self.internal_cogs_per_day = self.internal_cogs_per_shift * self.shifts_per_day
560
+ self.internal_cogs_per_week = (
561
+ self.internal_cogs_per_shift * self.shifts_per_week
562
+ )
563
+ self.internal_cogs_per_kg_output = (
564
+ (self.internal_cogs_per_kg_bio * self.biomass_kg_per_saleable_kg)
565
+ if self.biomass_kg_per_saleable_kg != 0
566
+ else 0.0
567
+ )
568
+
569
+ def _calc_gross_revenue(self):
570
+ self.gross_rev_per_kg_bio = (
571
+ self.saleable_kg_per_kg_bio * self.wholesale_cbx_price
572
+ )
573
+ self.gross_rev_per_shift = (
574
+ self.gross_rev_per_kg_bio * self.kg_processed_per_shift
575
+ )
576
+ self.gross_rev_per_day = self.gross_rev_per_shift * self.shifts_per_day
577
+ self.gross_rev_per_week = self.gross_rev_per_shift * self.shifts_per_week
578
+
579
+ def _calc_net_revenue(self):
580
+ self.net_rev_per_kg_bio = (
581
+ self.gross_rev_per_kg_bio - self.internal_cogs_per_kg_bio - self.bio_cost
582
+ )
583
+ self.net_rev_per_shift = self.net_rev_per_kg_bio * self.kg_processed_per_shift
584
+ self.net_rev_per_day = self.net_rev_per_shift * self.shifts_per_day
585
+ self.net_rev_per_week = self.net_rev_per_shift * self.shifts_per_week
586
+ self.net_rev_per_kg_output = (
587
+ (self.biomass_kg_per_saleable_kg * self.net_rev_per_kg_bio)
588
+ if self.biomass_kg_per_saleable_kg != 0
589
+ else 0.0
590
+ )
591
+
592
+ def _calc_biomass_cost(self):
593
+ self.biomass_cost_per_shift = self.kg_processed_per_shift * self.bio_cost
594
+ self.biomass_cost_per_day = self.biomass_cost_per_shift * self.shifts_per_day
595
+ self.biomass_cost_per_week = self.biomass_cost_per_shift * self.shifts_per_week
596
+
597
+ def _calc_saleable_kg(self):
598
+ if self.wholesale_cbx_pct == 0:
599
+ self.saleable_kg_per_kg_bio = 0.0
600
+ else:
601
+ self.saleable_kg_per_kg_bio = (
602
+ (self.bio_cbx_pct / 100.0)
603
+ * (self.finished_product_yield_pct / 100.0)
604
+ / (self.wholesale_cbx_pct / 100.0)
605
+ )
606
+ self.saleable_kg_per_shift = (
607
+ self.saleable_kg_per_kg_bio * self.kg_processed_per_shift
608
+ )
609
+ self.saleable_kg_per_day = self.saleable_kg_per_shift * self.shifts_per_day
610
+ self.saleable_kg_per_week = self.saleable_kg_per_shift * self.shifts_per_week
611
+ self.biomass_kg_per_saleable_kg = (
612
+ 1 / self.saleable_kg_per_kg_bio if self.saleable_kg_per_kg_bio > 0 else 0.0
613
+ )
614
+ self.biomass_cost_per_kg_output = (
615
+ self.biomass_kg_per_saleable_kg * self.bio_cost
616
+ )
617
+
618
+ def _update_tables_data(self):
619
+ money_data_dict = {
620
+ " ": ["Biomass cost", "Processing cost", "Gross Revenue", "Net Revenue"],
621
+ "Per Kilogram of Biomass": [
622
+ self.bio_cost,
623
+ self.internal_cogs_per_kg_bio,
624
+ self.gross_rev_per_kg_bio,
625
+ self.net_rev_per_kg_bio,
626
+ ],
627
+ "Per Kilogram of Output": [
628
+ self.biomass_cost_per_kg_output,
629
+ self.internal_cogs_per_kg_output,
630
+ self.wholesale_cbx_price,
631
+ self.net_rev_per_kg_output,
632
+ ],
633
+ "Per Shift": [
634
+ self.biomass_cost_per_shift,
635
+ self.internal_cogs_per_shift,
636
+ self.gross_rev_per_shift,
637
+ self.net_rev_per_shift,
638
+ ],
639
+ "Per Day": [
640
+ self.biomass_cost_per_day,
641
+ self.internal_cogs_per_day,
642
+ self.gross_rev_per_day,
643
+ self.net_rev_per_day,
644
+ ],
645
+ "Per Week": [
646
+ self.biomass_cost_per_week,
647
+ self.internal_cogs_per_week,
648
+ self.gross_rev_per_week,
649
+ self.net_rev_per_week,
650
+ ],
651
+ }
652
+ self.money_data_df = pd.DataFrame(money_data_dict)
653
+ if hasattr(self, "money_table"):
654
+ self.money_table.value = self.money_data_df
655
+
656
+ profit_data_dict = {
657
+ "Metric": ["Operating Profit", "Resin Spread"],
658
+ "Value": [
659
+ f"{self.operating_profit_pct * 100.0:.2f}%",
660
+ f"{self.resin_spread_pct * 100.0:.2f}%",
661
+ ],
662
+ }
663
+ self.profit_data_df = pd.DataFrame(profit_data_dict)
664
+ if hasattr(self, "profit_table"):
665
+ self.profit_table.value = self.profit_data_df
666
+
667
+ processing_values_formatted = [
668
+ f"{self.kg_processed_per_shift:,.0f}",
669
+ f"${self.labour_cost_per_shift:,.2f}",
670
+ f"${self.variable_cost_per_shift:,.2f}",
671
+ f"${self.overhead_cost_per_shift:,.2f}",
672
+ ]
673
+ processing_data_dict = {
674
+ "Metric": [
675
+ "Kilograms Extracted per Shift",
676
+ "Labour Cost per Shift",
677
+ "Variable Cost per Shift",
678
+ "Overhead per Shift",
679
+ ],
680
+ "Value": processing_values_formatted,
681
+ }
682
+ self.processing_data_df = pd.DataFrame(processing_data_dict)
683
+ if hasattr(self, "processing_table"):
684
+ self.processing_table.value = self.processing_data_df
685
+
686
+ def _get_money_formatters(self):
687
+ return {
688
+ "Per Kilogram of Biomass": get_formatter("$%.02f"),
689
+ "Per Kilogram of Output": get_formatter("$%.02f"),
690
+ "Per Shift": get_formatter("$%.02f"),
691
+ "Per Day": get_formatter("$%.02f"),
692
+ "Per Week": get_formatter("$%.02f"),
693
+ }
694
+
695
+ def view(self):
696
+ col1 = pn.Column(
697
+ pn.pane.Markdown("## Extraction"),
698
+ pn.Row(
699
+ self.kg_processed_per_hour_slider, # Updated
700
+ ),
701
+ pn.Row(
702
+ self.finished_product_yield_pct_slider, # Updated
703
+ ),
704
+ pn.pane.Markdown("## Biomass parameters"),
705
+ pn.Row(
706
+ self.bio_cbx_pct_slider, # Updated
707
+ ),
708
+ pn.Row(
709
+ self.bio_cost_slider, # Updated
710
+ ),
711
+ sizing_mode="stretch_width",
712
+ )
713
+ col2 = pn.Column(
714
+ pn.pane.Markdown("## Consumable rates"),
715
+ pn.Row(
716
+ self.kwh_rate_slider, # Updated
717
+ ),
718
+ pn.Row(
719
+ self.water_cost_per_1000l_slider, # Updated
720
+ ),
721
+ pn.Row(
722
+ self.consumables_per_kg_bio_rate_slider, # Updated
723
+ ),
724
+ pn.pane.Markdown("## Wholesale details"),
725
+ pn.Row(
726
+ self.wholesale_cbx_price_slider, # Updated
727
+ ),
728
+ pn.Row(
729
+ self.wholesale_cbx_pct_slider, # Updated
730
+ ),
731
+ sizing_mode="stretch_width",
732
+ )
733
+ col3 = pn.Column(
734
+ pn.pane.Markdown("## Variable costs"),
735
+ pn.Row(
736
+ self.kwh_per_kg_bio_slider, # Updated
737
+ ),
738
+ pn.Row(
739
+ self.water_liters_consumed_per_kg_bio_slider, # Updated
740
+ ),
741
+ pn.Row(
742
+ self.consumables_per_kg_output_slider, # Updated
743
+ ),
744
+ pn.pane.Markdown("## Compliance"),
745
+ pn.Row(
746
+ self.batch_test_cost_slider, # Updated
747
+ ),
748
+ pn.pane.Markdown("## Overhead"),
749
+ pn.Row(
750
+ self.fixed_overhead_per_week_slider, # Updated
751
+ ),
752
+ sizing_mode="stretch_width",
753
+ )
754
+ col4 = pn.Column(
755
+ pn.pane.Markdown("## Worker Details"),
756
+ pn.Row(
757
+ self.workers_per_shift_slider, # Updated
758
+ ),
759
+ pn.Row(
760
+ self.worker_hourly_rate_slider, # Updated
761
+ ),
762
+ pn.Row(
763
+ self.managers_per_shift_slider, # Updated
764
+ ),
765
+ pn.Row(
766
+ self.manager_hourly_rate_slider, # Updated
767
+ ),
768
+ pn.pane.Markdown("## Shift details"),
769
+ pn.Row(
770
+ self.labour_hours_per_shift_slider, # Updated
771
+ ),
772
+ pn.Row(
773
+ self.processing_hours_per_shift_slider, # Updated
774
+ ),
775
+ pn.Row(
776
+ self.shifts_per_day_slider, # Updated
777
+ ),
778
+ pn.Row(
779
+ self.shifts_per_week_slider, # Updated
780
+ ),
781
+ sizing_mode="stretch_width",
782
+ )
783
+
784
+ input_grid = pn.GridSpec(sizing_mode="stretch_width", max_width=1800, margin=10)
785
+ input_grid[0, 0] = col1
786
+ input_grid[0, 1] = col2
787
+ input_grid[0, 2] = col3
788
+ input_grid[0, 3] = col4
789
+
790
+ money_table_display = pn.Column(
791
+ pn.pane.Markdown("### Financial Summary", styles={"text-align": "center"}),
792
+ self.money_table,
793
+ )
794
+
795
+ profit_table_display = pn.Column(
796
+ pn.pane.Markdown(
797
+ "### Profitability Metrics", styles={"text-align": "center"}
798
+ ),
799
+ self.profit_table,
800
+ )
801
+
802
+ processing_table_display = pn.Column(
803
+ pn.pane.Markdown("### Processing Summary", styles={"text-align": "center"}),
804
+ self.processing_table,
805
+ )
806
+
807
+ tables_layout = pn.Column(
808
+ money_table_display,
809
+ pn.Row(
810
+ processing_table_display,
811
+ profit_table_display,
812
+ align="center",
813
+ ),
814
+ #sizing_mode="stretch_width",
815
+ #max_width=1800,
816
+ margin=10,
817
+ align="center",
818
+ )
819
+
820
+ profit_weekly = pn.indicators.Number(
821
+ name="Weekly Profit",
822
+ value=self.net_rev_per_week,
823
+ format=f"${self.net_rev_per_week / 1000:.0f} k",
824
+ default_color="green",
825
+ align="center",
826
+ )
827
+
828
+ profit_pct = pn.indicators.Number(
829
+ name="Operating Profit",
830
+ value=self.operating_profit_pct,
831
+ format=f"{self.operating_profit_pct * 100.0:.2f}%",
832
+ default_color="green",
833
+ align="center",
834
+ )
835
+
836
+ indicator_layout = pn.Column(profit_pct, profit_weekly, align="center")
837
+
838
+ table_grid = pn.GridSpec(sizing_mode="stretch_width", max_width=1800, margin=10)
839
+ table_grid[:, 0:2] = tables_layout
840
+ table_grid[:, 2] = indicator_layout
841
+
842
+ main_layout = pn.Column(
843
+ input_grid,
844
+ pn.layout.Divider(margin=(10, 0)),
845
+ table_grid,
846
+ styles={"margin": "0px 10px"},
847
+ )
848
+
849
+ return main_layout
850
+
851
+
852
+ estimator_app = CannabinoidEstimator()
853
+ # To run in a Panel server:
854
+ # pn.config.raw_css = custom_themes.get_base_css(custom_themes.DARK_THEME_VARS)
855
+ estimator_app.view().servable(title="CBx Revenue Estimator")
856
+
857
+ # Instantiate the template with widgets displayed in the sidebar
858
+ # template = pn.template.FastListTemplate(
859
+ # title="CBx Revenue Estimator (FastList Panel)",
860
+ # #theme = custom_themes.DarkTheme,
861
+ # #sidebar=[freq, phase],
862
+ # )
863
+
864
+ # template.main.append(estimator_app.view())
865
+ # template.servable()
866
+
867
+ if __name__ == "__main__":
868
+ pn.serve(
869
+ estimator_app.view(),
870
+ title="CBx Revenue Estimator (Panel)",
871
+ show=True,
872
+ port=5007,
873
+ )