AccruedInnovation commited on
Commit
89bf277
·
verified ·
1 Parent(s): 95a856c

Upload 3 files

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