AccruedInnovation commited on
Commit
95a856c
·
verified ·
1 Parent(s): 03070aa

Update app.py

Browse files

Cleaning up responsiveness for mobile

Files changed (1) hide show
  1. app.py +289 -334
app.py CHANGED
@@ -34,7 +34,7 @@ def get_formatter(format_str):
34
  return format_str
35
 
36
 
37
- class CannabinoidEstimator(param.Parameterized):
38
  # --- Input Parameters ---
39
  kg_processed_per_hour = param.Number(
40
  default=150.0,
@@ -135,10 +135,9 @@ class CannabinoidEstimator(param.Parameterized):
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(
139
- default = "Day", label="New batch frequency"
140
- )
141
 
 
142
  kg_processed_per_shift = 0.0
143
  labour_cost_per_shift = 0.0
144
  variable_cost_per_shift = 0.0
@@ -168,42 +167,248 @@ class CannabinoidEstimator(param.Parameterized):
168
  net_rev_per_kg_output = 0.0
169
  operating_profit_pct = 0.0
170
  resin_spread_pct = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- money_data_df = param.DataFrame(pd.DataFrame())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  profit_data_df = param.DataFrame(pd.DataFrame())
174
  processing_data_df = param.DataFrame(pd.DataFrame())
175
 
176
  def __init__(self, **params):
177
  super().__init__(**params)
178
  self._create_sliders()
179
- self.money_table = pn.widgets.Tabulator(
180
- self.money_data_df,
181
- formatters=self._get_money_formatters(),
 
 
 
 
 
182
  disabled=True,
183
  layout="fit_data",
184
  sizing_mode="fixed",
185
  align="center",
186
- show_index=False, # Hide index column
187
  text_align={
188
  " ": "right",
189
  "$/kg Biomass": "center",
190
  "$/kg Output": "center",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  "Per Shift": "center",
192
  "Per Day": "center",
193
  "Per Week": "center",
194
  },
195
  )
 
196
  self.profit_table = pn.widgets.Tabulator(
197
  self.profit_data_df,
198
  disabled=True,
199
  layout="fit_data_table",
200
  sizing_mode="fixed",
201
  align="center",
202
- show_index=False, # Hide index column
203
- text_align={
204
- "Metric": "right",
205
- "Value": "center",
206
- },
207
  )
208
  self.processing_table = pn.widgets.Tabulator(
209
  self.processing_data_df,
@@ -212,46 +417,39 @@ class CannabinoidEstimator(param.Parameterized):
212
  layout="fit_data_table",
213
  sizing_mode="fixed",
214
  align="center",
215
- show_index=False, # Hide index column
216
- text_align={
217
- "Metric (Per Shift)": "right",
218
- "Value": "center",
219
- },
220
  )
221
  self.profit_weekly = pn.indicators.Number(
222
  name="Weekly Profit",
223
- value=self.net_rev_per_week,
224
- format=f"${self.net_rev_per_week / 1000:.0f} k",
225
  default_color="green",
226
  align="center",
227
  )
228
  self.profit_pct = pn.indicators.Number(
229
  name="Operating Profit",
230
- value=self.operating_profit_pct,
231
- format=f"{self.operating_profit_pct * 100.0:.2f}%",
232
  default_color="green",
233
  align="center",
234
  )
 
235
  self._update_calculations()
236
 
237
  def _create_sliders(self):
238
  self.kg_processed_per_hour_slider = pn.widgets.EditableFloatSlider.from_param(
239
  self.param.kg_processed_per_hour,
240
  name=self.param.kg_processed_per_hour.label,
241
- fixed_start=self.param.kg_processed_per_hour.bounds[0],
242
- fixed_end=self.param.kg_processed_per_hour.bounds[1],
243
  design=slider_design,
244
  styles=slider_style,
245
  stylesheets=slider_stylesheet,
246
- # format="0",
247
  format=PrintfTickFormatter(format="%i kg"),
248
  )
249
  self.finished_product_yield_pct_slider = (
250
  pn.widgets.EditableFloatSlider.from_param(
251
  self.param.finished_product_yield_pct,
252
  name=self.param.finished_product_yield_pct.label,
253
- fixed_start=self.param.finished_product_yield_pct.bounds[0],
254
- fixed_end=self.param.finished_product_yield_pct.bounds[1],
255
  design=slider_design,
256
  styles=slider_style,
257
  stylesheets=slider_stylesheet,
@@ -261,19 +459,14 @@ class CannabinoidEstimator(param.Parameterized):
261
  self.kwh_rate_slider = pn.widgets.EditableFloatSlider.from_param(
262
  self.param.kwh_rate,
263
  name=self.param.kwh_rate.label,
264
- fixed_start=self.param.kwh_rate.bounds[0],
265
- fixed_end=self.param.kwh_rate.bounds[1],
266
  design=slider_design,
267
  styles=slider_style,
268
  stylesheets=slider_stylesheet,
269
  format="0.00",
270
- # format=PrintfTickFormatter(format='%.2f per kWh'),
271
  )
272
  self.water_cost_per_1000l_slider = pn.widgets.EditableFloatSlider.from_param(
273
  self.param.water_cost_per_1000l,
274
  name=self.param.water_cost_per_1000l.label,
275
- fixed_start=self.param.water_cost_per_1000l.bounds[0],
276
- fixed_end=self.param.water_cost_per_1000l.bounds[1],
277
  design=slider_design,
278
  styles=slider_style,
279
  stylesheets=slider_stylesheet,
@@ -283,8 +476,6 @@ class CannabinoidEstimator(param.Parameterized):
283
  pn.widgets.EditableFloatSlider.from_param(
284
  self.param.consumables_per_kg_bio_rate,
285
  name=self.param.consumables_per_kg_bio_rate.label,
286
- fixed_start=self.param.consumables_per_kg_bio_rate.bounds[0],
287
- fixed_end=self.param.consumables_per_kg_bio_rate.bounds[1],
288
  design=slider_design,
289
  styles=slider_style,
290
  stylesheets=slider_stylesheet,
@@ -294,8 +485,6 @@ class CannabinoidEstimator(param.Parameterized):
294
  self.kwh_per_kg_bio_slider = pn.widgets.EditableFloatSlider.from_param(
295
  self.param.kwh_per_kg_bio,
296
  name=self.param.kwh_per_kg_bio.label,
297
- fixed_start=self.param.kwh_per_kg_bio.bounds[0],
298
- fixed_end=self.param.kwh_per_kg_bio.bounds[1],
299
  design=slider_design,
300
  styles=slider_style,
301
  stylesheets=slider_stylesheet,
@@ -305,8 +494,6 @@ class CannabinoidEstimator(param.Parameterized):
305
  pn.widgets.EditableFloatSlider.from_param(
306
  self.param.water_liters_consumed_per_kg_bio,
307
  name=self.param.water_liters_consumed_per_kg_bio.label,
308
- fixed_start=self.param.water_liters_consumed_per_kg_bio.bounds[0],
309
- fixed_end=self.param.water_liters_consumed_per_kg_bio.bounds[1],
310
  design=slider_design,
311
  styles=slider_style,
312
  stylesheets=slider_stylesheet,
@@ -317,8 +504,6 @@ class CannabinoidEstimator(param.Parameterized):
317
  pn.widgets.EditableFloatSlider.from_param(
318
  self.param.consumables_per_kg_output,
319
  name=self.param.consumables_per_kg_output.label,
320
- fixed_start=self.param.consumables_per_kg_output.bounds[0],
321
- fixed_end=self.param.consumables_per_kg_output.bounds[1],
322
  design=slider_design,
323
  styles=slider_style,
324
  stylesheets=slider_stylesheet,
@@ -328,8 +513,6 @@ class CannabinoidEstimator(param.Parameterized):
328
  self.bio_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param(
329
  self.param.bio_cbx_pct,
330
  name=self.param.bio_cbx_pct.label,
331
- fixed_start=self.param.bio_cbx_pct.bounds[0],
332
- fixed_end=self.param.bio_cbx_pct.bounds[1],
333
  design=slider_design,
334
  styles=slider_style,
335
  stylesheets=slider_stylesheet,
@@ -338,8 +521,6 @@ class CannabinoidEstimator(param.Parameterized):
338
  self.bio_cost_slider = pn.widgets.EditableFloatSlider.from_param(
339
  self.param.bio_cost,
340
  name=self.param.bio_cost.label,
341
- fixed_start=self.param.bio_cost.bounds[0],
342
- fixed_end=self.param.bio_cost.bounds[1],
343
  design=slider_design,
344
  styles=slider_style,
345
  stylesheets=slider_stylesheet,
@@ -348,8 +529,6 @@ class CannabinoidEstimator(param.Parameterized):
348
  self.wholesale_cbx_price_slider = pn.widgets.EditableFloatSlider.from_param(
349
  self.param.wholesale_cbx_price,
350
  name=self.param.wholesale_cbx_price.label,
351
- fixed_start=self.param.wholesale_cbx_price.bounds[0],
352
- fixed_end=self.param.wholesale_cbx_price.bounds[1],
353
  design=slider_design,
354
  styles=slider_style,
355
  stylesheets=slider_stylesheet,
@@ -358,8 +537,6 @@ class CannabinoidEstimator(param.Parameterized):
358
  self.wholesale_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param(
359
  self.param.wholesale_cbx_pct,
360
  name=self.param.wholesale_cbx_pct.label,
361
- fixed_start=self.param.wholesale_cbx_pct.bounds[0],
362
- fixed_end=self.param.wholesale_cbx_pct.bounds[1],
363
  design=slider_design,
364
  styles=slider_style,
365
  stylesheets=slider_stylesheet,
@@ -368,8 +545,6 @@ class CannabinoidEstimator(param.Parameterized):
368
  self.batch_test_cost_slider = pn.widgets.EditableFloatSlider.from_param(
369
  self.param.batch_test_cost,
370
  name=self.param.batch_test_cost.label,
371
- fixed_start=self.param.batch_test_cost.bounds[0],
372
- fixed_end=self.param.batch_test_cost.bounds[1],
373
  design=slider_design,
374
  styles=slider_style,
375
  stylesheets=slider_stylesheet,
@@ -378,8 +553,6 @@ class CannabinoidEstimator(param.Parameterized):
378
  self.fixed_overhead_per_week_slider = pn.widgets.EditableFloatSlider.from_param(
379
  self.param.fixed_overhead_per_week,
380
  name=self.param.fixed_overhead_per_week.label,
381
- fixed_start=self.param.fixed_overhead_per_week.bounds[0],
382
- fixed_end=self.param.fixed_overhead_per_week.bounds[1],
383
  design=slider_design,
384
  styles=slider_style,
385
  stylesheets=slider_stylesheet,
@@ -388,8 +561,6 @@ class CannabinoidEstimator(param.Parameterized):
388
  self.workers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param(
389
  self.param.workers_per_shift,
390
  name=self.param.workers_per_shift.label,
391
- fixed_start=self.param.workers_per_shift.bounds[0],
392
- fixed_end=self.param.workers_per_shift.bounds[1],
393
  design=slider_design,
394
  styles=slider_style,
395
  stylesheets=slider_stylesheet,
@@ -398,8 +569,6 @@ class CannabinoidEstimator(param.Parameterized):
398
  self.worker_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param(
399
  self.param.worker_hourly_rate,
400
  name=self.param.worker_hourly_rate.label,
401
- fixed_start=self.param.worker_hourly_rate.bounds[0],
402
- fixed_end=self.param.worker_hourly_rate.bounds[1],
403
  design=slider_design,
404
  styles=slider_style,
405
  stylesheets=slider_stylesheet,
@@ -408,8 +577,6 @@ class CannabinoidEstimator(param.Parameterized):
408
  self.managers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param(
409
  self.param.managers_per_shift,
410
  name=self.param.managers_per_shift.label,
411
- fixed_start=self.param.managers_per_shift.bounds[0],
412
- fixed_end=self.param.managers_per_shift.bounds[1],
413
  design=slider_design,
414
  styles=slider_style,
415
  stylesheets=slider_stylesheet,
@@ -418,45 +585,32 @@ class CannabinoidEstimator(param.Parameterized):
418
  self.manager_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param(
419
  self.param.manager_hourly_rate,
420
  name=self.param.manager_hourly_rate.label,
421
- fixed_start=self.param.worker_hourly_rate.default, # Keeping original logic as per file
422
- fixed_end=self.param.manager_hourly_rate.bounds[1],
423
  design=slider_design,
424
  styles=slider_style,
425
  stylesheets=slider_stylesheet,
426
  format="0.00",
427
  )
428
-
429
  self.labour_hours_per_shift_slider = pn.widgets.EditableFloatSlider.from_param(
430
  self.param.labour_hours_per_shift,
431
  name=self.param.labour_hours_per_shift.label,
432
- fixed_start=self.param.labour_hours_per_shift.bounds[
433
- 0
434
- ], # Changed in previous request
435
- fixed_end=self.param.labour_hours_per_shift.bounds[1],
436
  design=slider_design,
437
  styles=slider_style,
438
  stylesheets=slider_stylesheet,
439
  format="0.00",
440
  )
441
-
442
  self.processing_hours_per_shift_slider = (
443
  pn.widgets.EditableFloatSlider.from_param(
444
  self.param.processing_hours_per_shift,
445
  name=self.param.processing_hours_per_shift.label,
446
- fixed_start=self.param.processing_hours_per_shift.bounds[0],
447
- fixed_end=self.labour_hours_per_shift, # Changed in previous request
448
  design=slider_design,
449
  styles=slider_style,
450
  stylesheets=slider_stylesheet,
451
  format="0.00",
452
  )
453
  )
454
-
455
  self.shifts_per_day_slider = pn.widgets.EditableFloatSlider.from_param(
456
  self.param.shifts_per_day,
457
  name=self.param.shifts_per_day.label,
458
- fixed_start=self.param.shifts_per_day.bounds[0],
459
- fixed_end=self.param.shifts_per_day.bounds[1],
460
  design=slider_design,
461
  styles=slider_style,
462
  stylesheets=slider_stylesheet,
@@ -465,14 +619,11 @@ class CannabinoidEstimator(param.Parameterized):
465
  self.shifts_per_week_slider = pn.widgets.EditableFloatSlider.from_param(
466
  self.param.shifts_per_week,
467
  name=self.param.shifts_per_week.label,
468
- fixed_start=self.param.shifts_per_week.bounds[0],
469
- fixed_end=self.param.shifts_per_week.bounds[1],
470
  design=slider_design,
471
  styles=slider_style,
472
  stylesheets=slider_stylesheet,
473
  format="0",
474
  )
475
-
476
  self.batch_frequency_radio = pn.widgets.RadioButtonGroup.from_param(
477
  self.param.batch_frequency,
478
  name=self.param.batch_frequency.label,
@@ -480,209 +631,33 @@ class CannabinoidEstimator(param.Parameterized):
480
  button_type="primary",
481
  )
482
 
483
- @param.depends(
484
- "kg_processed_per_hour",
485
- "finished_product_yield_pct",
486
- "kwh_rate",
487
- "water_cost_per_1000l",
488
- "consumables_per_kg_bio_rate",
489
- "kwh_per_kg_bio",
490
- "water_liters_consumed_per_kg_bio",
491
- "consumables_per_kg_output",
492
- "bio_cbx_pct",
493
- "bio_cost",
494
- "wholesale_cbx_price",
495
- "wholesale_cbx_pct",
496
- "batch_test_cost",
497
- "batch_frequency",
498
- "fixed_overhead_per_week",
499
- "workers_per_shift",
500
- "worker_hourly_rate",
501
- "managers_per_shift",
502
- "manager_hourly_rate",
503
- "labour_hours_per_shift",
504
- "processing_hours_per_shift",
505
- "shifts_per_day",
506
- "shifts_per_week",
507
- watch=True,
508
- )
509
- def _update_calculations(self, *events):
510
- self.kg_processed_per_shift = (
511
- self.processing_hours_per_shift * self.kg_processed_per_hour
512
- )
513
- if self.shifts_per_week == 0:
514
- self.shifts_per_week = 1
515
-
516
- self._calc_saleable_kg()
517
- self._calc_biomass_cost()
518
- self._calc_cogs()
519
- self._calc_gross_revenue()
520
- self._calc_net_revenue()
521
-
522
- self.operating_profit_pct = (
523
- (self.net_rev_per_kg_bio / self.gross_rev_per_kg_bio)
524
- if self.gross_rev_per_kg_bio
525
- else 0.0
526
- )
527
- self.resin_spread_pct = (
528
- ((self.gross_rev_per_kg_bio - self.bio_cost) / self.bio_cost)
529
- if self.bio_cost
530
- else 0.0
531
- )
532
-
533
- self._update_tables_data()
534
-
535
  @param.depends("labour_hours_per_shift", watch=True)
536
  def _update_processing_hours_slider_constraints(self):
537
  new_max_processing_hours = self.labour_hours_per_shift
538
-
539
- # Get the current lower bound of the processing_hours_per_shift parameter
540
  current_min_processing_hours = self.param.processing_hours_per_shift.bounds[0]
541
-
542
- # Update the bounds of the underlying param.Number object for processing_hours_per_shift
543
- # This allows the parameter to accept values up to the new maximum
544
  self.param.processing_hours_per_shift.bounds = (
545
  current_min_processing_hours,
546
  new_max_processing_hours,
547
  )
548
-
549
- # Ensure the slider widget has been created before trying to access it
550
  if hasattr(self, "processing_hours_per_shift_slider"):
551
- # Update the 'end' property of the slider widget
552
  self.processing_hours_per_shift_slider.end = new_max_processing_hours
553
-
554
- # If the current value of processing_hours_per_shift is now greater than
555
- # the new maximum, adjust it to be the new maximum.
556
  if self.processing_hours_per_shift > new_max_processing_hours:
557
  self.processing_hours_per_shift = new_max_processing_hours
558
 
559
- def _calc_cogs(self):
560
- worker_cost = self.workers_per_shift * self.worker_hourly_rate
561
- manager_cost = self.managers_per_shift * self.manager_hourly_rate
562
- self.labour_cost_per_shift = (
563
- worker_cost + manager_cost
564
- ) * self.labour_hours_per_shift
565
-
566
- power_cost_per_kg = self.kwh_rate * self.kwh_per_kg_bio
567
- water_cost_per_kg = (
568
- self.water_cost_per_1000l / 1000.0
569
- ) * self.water_liters_consumed_per_kg_bio
570
- total_variable_consumable_cost_per_kg = (
571
- self.consumables_per_kg_bio_rate + power_cost_per_kg + water_cost_per_kg
572
- )
573
- self.variable_cost_per_shift = (
574
- total_variable_consumable_cost_per_kg * self.kg_processed_per_shift
575
- )
576
-
577
- self.overhead_cost_per_shift = (
578
- self.fixed_overhead_per_week / self.shifts_per_week
579
- if self.shifts_per_week > 0
580
- else 0.0
581
- )
582
-
583
- # Calculate batch_test_cost_per_shift based on batch_frequency
584
- self.batch_test_cost_per_shift = 0.0
585
- if self.batch_frequency == "Shift":
586
- self.batch_test_cost_per_shift = self.batch_test_cost
587
- elif self.batch_frequency == "Day":
588
- # Ensure self.shifts_per_day is defined and positive
589
- if hasattr(self, 'shifts_per_day') and self.shifts_per_day > 0:
590
- self.batch_test_cost_per_shift = self.batch_test_cost / self.shifts_per_day
591
- else:
592
- # If no shifts per day, or attribute not defined, cost per shift is 0
593
- # Or, this could be an error condition if shifts_per_day is expected
594
- self.batch_test_cost_per_shift = 0.0
595
- elif self.batch_frequency == "Week":
596
- # self.shifts_per_week is used above, so it should be available
597
- if self.shifts_per_week > 0:
598
- self.batch_test_cost_per_shift = self.batch_test_cost / self.shifts_per_week
599
- else:
600
- # If no shifts per week, cost per shift is 0
601
- self.batch_test_cost_per_shift = 0.0
602
- # else: # Optional: handle invalid self.batch_frequency value
603
- # For example, raise ValueError or log a warning
604
- # print(f"Warning: Unknown batch_frequency: {self.batch_frequency}")
605
-
606
- shift_cogs_before_output_specific = (
607
- self.labour_cost_per_shift
608
- + self.variable_cost_per_shift
609
- + self.overhead_cost_per_shift
610
- + self.batch_test_cost_per_shift # Added batch test cost
611
- )
612
- shift_output_specific_cogs = (
613
- self.consumables_per_kg_output * self.saleable_kg_per_shift
614
- )
615
-
616
- self.internal_cogs_per_shift = (
617
- shift_cogs_before_output_specific + shift_output_specific_cogs
618
- )
619
- self.internal_cogs_per_kg_bio = (
620
- self.internal_cogs_per_shift / self.kg_processed_per_shift
621
- if self.kg_processed_per_shift > 0
622
- else 0.0
623
- )
624
- self.internal_cogs_per_day = self.internal_cogs_per_shift * self.shifts_per_day
625
- self.internal_cogs_per_week = (
626
- self.internal_cogs_per_shift * self.shifts_per_week
627
- )
628
- self.internal_cogs_per_kg_output = (
629
- (self.internal_cogs_per_kg_bio * self.biomass_kg_per_saleable_kg)
630
- if self.biomass_kg_per_saleable_kg != 0
631
- else 0.0
632
- )
633
-
634
- def _calc_gross_revenue(self):
635
- self.gross_rev_per_kg_bio = (
636
- self.saleable_kg_per_kg_bio * self.wholesale_cbx_price
637
- )
638
- self.gross_rev_per_shift = (
639
- self.gross_rev_per_kg_bio * self.kg_processed_per_shift
640
- )
641
- self.gross_rev_per_day = self.gross_rev_per_shift * self.shifts_per_day
642
- self.gross_rev_per_week = self.gross_rev_per_shift * self.shifts_per_week
643
-
644
- def _calc_net_revenue(self):
645
- self.net_rev_per_kg_bio = (
646
- self.gross_rev_per_kg_bio - self.internal_cogs_per_kg_bio - self.bio_cost
647
- )
648
- self.net_rev_per_shift = self.net_rev_per_kg_bio * self.kg_processed_per_shift
649
- self.net_rev_per_day = self.net_rev_per_shift * self.shifts_per_day
650
- self.net_rev_per_week = self.net_rev_per_shift * self.shifts_per_week
651
- self.net_rev_per_kg_output = (
652
- (self.biomass_kg_per_saleable_kg * self.net_rev_per_kg_bio)
653
- if self.biomass_kg_per_saleable_kg != 0
654
- else 0.0
655
- )
656
-
657
- def _calc_biomass_cost(self):
658
- self.biomass_cost_per_shift = self.kg_processed_per_shift * self.bio_cost
659
- self.biomass_cost_per_day = self.biomass_cost_per_shift * self.shifts_per_day
660
- self.biomass_cost_per_week = self.biomass_cost_per_shift * self.shifts_per_week
661
-
662
- def _calc_saleable_kg(self):
663
- if self.wholesale_cbx_pct == 0:
664
- self.saleable_kg_per_kg_bio = 0.0
665
- else:
666
- self.saleable_kg_per_kg_bio = (
667
- (self.bio_cbx_pct / 100.0)
668
- * (self.finished_product_yield_pct / 100.0)
669
- / (self.wholesale_cbx_pct / 100.0)
670
- )
671
- self.saleable_kg_per_shift = (
672
- self.saleable_kg_per_kg_bio * self.kg_processed_per_shift
673
- )
674
- self.saleable_kg_per_day = self.saleable_kg_per_shift * self.shifts_per_day
675
- self.saleable_kg_per_week = self.saleable_kg_per_shift * self.shifts_per_week
676
- self.biomass_kg_per_saleable_kg = (
677
- 1 / self.saleable_kg_per_kg_bio if self.saleable_kg_per_kg_bio > 0 else 0.0
678
- )
679
- self.biomass_cost_per_kg_output = (
680
- self.biomass_kg_per_saleable_kg * self.bio_cost
681
- )
682
 
683
  def _update_tables_data(self):
684
- money_data_dict = {
685
- " ": ["Biomass cost", "Processing cost", "Gross Revenue", "Net Revenue"],
 
 
 
 
 
 
 
 
686
  "$/kg Biomass": [
687
  self.bio_cost,
688
  self.internal_cogs_per_kg_bio,
@@ -695,6 +670,14 @@ class CannabinoidEstimator(param.Parameterized):
695
  self.wholesale_cbx_price,
696
  self.net_rev_per_kg_output,
697
  ],
 
 
 
 
 
 
 
 
698
  "Per Shift": [
699
  self.biomass_cost_per_shift,
700
  self.internal_cogs_per_shift,
@@ -714,9 +697,9 @@ class CannabinoidEstimator(param.Parameterized):
714
  self.net_rev_per_week,
715
  ],
716
  }
717
- self.money_data_df = pd.DataFrame(money_data_dict)
718
- if hasattr(self, "money_table"):
719
- self.money_table.value = self.money_data_df
720
 
721
  profit_data_dict = {
722
  "Metric": ["Operating Profit", "Resin Spread"],
@@ -747,41 +730,32 @@ class CannabinoidEstimator(param.Parameterized):
747
  self.processing_data_df = pd.DataFrame(processing_data_dict)
748
  if hasattr(self, "processing_table"):
749
  self.processing_table.value = self.processing_data_df
750
-
751
  if hasattr(self, "profit_weekly"):
752
  self.profit_weekly.value = self.net_rev_per_week
753
  self.profit_weekly.format = f"${self.net_rev_per_week / 1000:.0f} k"
754
-
755
  if hasattr(self, "profit_pct"):
756
  self.profit_pct.value = self.operating_profit_pct
757
- self.profit_pct.format=f"{self.operating_profit_pct * 100.0:.2f}%"
758
-
759
- def _get_money_formatters(self):
760
- return {
761
- "$/kg Biomass": get_formatter("$%.02f"),
762
- "$/kg Output": get_formatter("$%.02f"),
763
- "Per Shift": get_formatter("$%.02f"),
764
- "Per Day": get_formatter("$%.02f"),
765
- "Per Week": get_formatter("$%.02f"),
766
- }
767
 
768
  def view(self):
769
  input_col_max_width = 400
770
- col1 = pn.Column(
771
  "### Extraction",
772
  self.kg_processed_per_hour_slider,
773
  self.finished_product_yield_pct_slider,
774
  sizing_mode="stretch_width",
775
  max_width=input_col_max_width,
776
  )
777
- col2 = pn.Column(
778
  pn.pane.Markdown("### Biomass parameters", margin=0),
779
  self.bio_cbx_pct_slider,
780
  self.bio_cost_slider,
781
  sizing_mode="stretch_width",
782
  max_width=input_col_max_width,
783
  )
784
- col3 = pn.Column(
785
  pn.pane.Markdown("### Consumable rates", margin=0),
786
  self.kwh_rate_slider,
787
  self.water_cost_per_1000l_slider,
@@ -789,22 +763,22 @@ class CannabinoidEstimator(param.Parameterized):
789
  sizing_mode="stretch_width",
790
  max_width=input_col_max_width,
791
  )
792
- col4 = pn.Column(
793
  pn.pane.Markdown("### Wholesale details", margin=0),
794
  self.wholesale_cbx_price_slider,
795
  self.wholesale_cbx_pct_slider,
796
  sizing_mode="stretch_width",
797
  max_width=input_col_max_width,
798
  )
799
- col5 = pn.Column(
800
- pn.pane.Markdown("### Variable costs", margin=0),
801
  self.kwh_per_kg_bio_slider,
802
  self.water_liters_consumed_per_kg_bio_slider,
803
  self.consumables_per_kg_output_slider,
804
  sizing_mode="stretch_width",
805
  max_width=input_col_max_width,
806
  )
807
- col6 = pn.Column(
808
  pn.pane.Markdown("### Compliance", margin=0),
809
  self.batch_test_cost_slider,
810
  pn.pane.Markdown("New Batch Every:", margin=0),
@@ -814,7 +788,7 @@ class CannabinoidEstimator(param.Parameterized):
814
  sizing_mode="stretch_width",
815
  max_width=input_col_max_width,
816
  )
817
- col8 = pn.Column(
818
  pn.pane.Markdown("### Worker Details", margin=0),
819
  self.workers_per_shift_slider,
820
  self.worker_hourly_rate_slider,
@@ -823,7 +797,7 @@ class CannabinoidEstimator(param.Parameterized):
823
  sizing_mode="stretch_width",
824
  max_width=input_col_max_width,
825
  )
826
- col9 = pn.Column(
827
  pn.pane.Markdown("### Shift details", margin=0),
828
  self.labour_hours_per_shift_slider,
829
  self.processing_hours_per_shift_slider,
@@ -833,20 +807,34 @@ class CannabinoidEstimator(param.Parameterized):
833
  max_width=input_col_max_width,
834
  )
835
 
836
- # input_grid = pn.GridSpec(sizing_mode="stretch_width", max_width=1800, margin=10)
837
- # input_grid[0, 0] = col1
838
- # input_grid[0, 1] = col2
839
- # input_grid[0, 2] = col3
840
- # input_grid[0, 3] = col4
841
  input_grid = pn.FlexBox(
842
- col1, col2, col3, col4, col5, col8, col9, col6, align_content="normal"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  )
844
 
845
- money_table_display = pn.Column(
846
- pn.pane.Markdown("### Financial Summary", styles={"text-align": "center"}),
847
- self.money_table,
 
 
848
  sizing_mode="stretch_width",
849
- max_width=700,
850
  )
851
 
852
  profit_table_display = pn.Column(
@@ -855,7 +843,6 @@ class CannabinoidEstimator(param.Parameterized):
855
  sizing_mode="stretch_width",
856
  max_width=input_col_max_width,
857
  )
858
-
859
  processing_table_display = pn.Column(
860
  pn.pane.Markdown("### Processing Summary", styles={"text-align": "center"}),
861
  self.processing_table,
@@ -863,34 +850,15 @@ class CannabinoidEstimator(param.Parameterized):
863
  max_width=input_col_max_width,
864
  )
865
 
866
- # profit_weekly = pn.indicators.Number(
867
- # name="Weekly Profit",
868
- # value=self.net_rev_per_week,
869
- # format=f"${self.net_rev_per_week / 1000:.0f} k",
870
- # default_color="green",
871
- # align="center",
872
- # )
873
-
874
- # profit_pct = pn.indicators.Number(
875
- # name="Operating Profit",
876
- # value=self.operating_profit_pct,
877
- # format=f"{self.operating_profit_pct * 100.0:.2f}%",
878
- # default_color="green",
879
- # align="center",
880
- # )
881
-
882
- # indicator_layout = pn.Column(profit_pct, profit_weekly, align="center")
883
-
884
- # table_grid = pn.GridSpec(sizing_mode="stretch_width", max_width=1800, margin=10)
885
- # table_grid[:, 0:2] = tables_layout
886
- # table_grid[:, 2] = indicator_layout
887
  table_grid = pn.FlexBox(
888
  self.profit_weekly,
889
  self.profit_pct,
890
  processing_table_display,
891
  profit_table_display,
892
- money_table_display,
 
893
  align_content="normal",
 
894
  )
895
 
896
  main_layout = pn.Column(
@@ -899,25 +867,12 @@ class CannabinoidEstimator(param.Parameterized):
899
  table_grid,
900
  styles={"margin": "0px 10px"},
901
  )
902
-
903
  return main_layout
904
 
905
 
906
- estimator_app = CannabinoidEstimator()
907
- # To run in a Panel server:
908
- # pn.config.raw_css = custom_themes.get_base_css(custom_themes.DARK_THEME_VARS)
909
  estimator_app.view().servable(title="CBx Revenue Estimator")
910
 
911
- # Instantiate the template with widgets displayed in the sidebar
912
- # template = pn.template.FastListTemplate(
913
- # title="CBx Revenue Estimator (FastList Panel)",
914
- # #theme = custom_themes.DarkTheme,
915
- # #sidebar=[freq, phase],
916
- # )
917
-
918
- # template.main.append(estimator_app.view())
919
- # template.servable()
920
-
921
  if __name__ == "__main__":
922
  pn.serve(
923
  estimator_app.view(),
 
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,
 
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
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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,
 
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"],
 
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,
 
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),
 
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,
 
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,
 
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(
 
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,
 
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(
 
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(),