Spaces:
Running
Running
import panel as pn | |
import pandas as pd | |
import param | |
from bokeh.models.formatters import PrintfTickFormatter | |
from calculations import CannabinoidCalculations | |
from config import slider_design, slider_style, slider_stylesheet, get_formatter | |
class CannabinoidEstimatorGUI(CannabinoidCalculations): | |
# DataFrame params for tables | |
money_data_unit_df = param.DataFrame( | |
pd.DataFrame(), | |
precedence=-1, # precedence to hide from param pane if shown | |
) | |
money_data_time_df = param.DataFrame(pd.DataFrame(), precedence=-1) | |
profit_data_df = param.DataFrame(pd.DataFrame(), precedence=-1) | |
processing_data_df = param.DataFrame(pd.DataFrame(), precedence=-1) | |
def __init__(self, **params): | |
super().__init__(**params) | |
self._create_sliders() | |
self._create_tables_and_indicators() | |
self._update_calculations() # Initial calculation and table update | |
def _create_sliders(self): | |
self.batch_frequency_radio = pn.widgets.RadioButtonGroup.from_param( | |
self.param.batch_frequency, | |
name=self.param.batch_frequency.label, | |
options=["Shift", "Day", "Week"], | |
button_type="primary", | |
) | |
def _create_tables_and_indicators(self): | |
# Table for $/kg Biomass and $/kg Output | |
self.money_unit_table = pn.widgets.Tabulator( | |
self.money_data_unit_df, # Initial empty or pre-filled df | |
formatters={ | |
"$/kg Biomass": get_formatter("$%.02f"), | |
"$/kg Output": get_formatter("$%.02f"), | |
}, | |
disabled=True, | |
layout="fit_data", | |
sizing_mode="fixed", | |
align="center", | |
show_index=False, | |
text_align={ | |
" ": "right", | |
"$/kg Biomass": "center", | |
"$/kg Output": "center", | |
}, | |
) | |
# Table for Per Shift, Per Day, Per Week | |
self.money_time_table = pn.widgets.Tabulator( | |
self.money_data_time_df, # Initial empty or pre-filled df | |
formatters={ | |
"Per Shift": get_formatter("$%.02f"), | |
"Per Day": get_formatter("$%.02f"), | |
"Per Week": get_formatter("$%.02f"), | |
}, | |
disabled=True, | |
layout="fit_data", | |
sizing_mode="fixed", | |
align="center", | |
show_index=False, | |
text_align={ | |
" ": "right", | |
"Per Shift": "center", | |
"Per Day": "center", | |
"Per Week": "center", | |
}, | |
) | |
self.profit_table = pn.widgets.Tabulator( | |
self.profit_data_df, # Initial empty or pre-filled df | |
disabled=True, | |
layout="fit_data_table", | |
sizing_mode="fixed", | |
align="center", | |
show_index=False, | |
text_align={"Metric": "right", "Value": "center"}, | |
) | |
self.processing_table = pn.widgets.Tabulator( | |
self.processing_data_df, # Initial empty or pre-filled df | |
formatters={}, | |
disabled=True, | |
layout="fit_data_table", | |
sizing_mode="fixed", | |
align="center", | |
show_index=False, | |
text_align={"Metric (Per Shift)": "right", "Value": "center"}, | |
) | |
self.profit_weekly = pn.indicators.Number( | |
name="Weekly Profit", | |
value=0, | |
format="$0 k", | |
default_color="green", | |
align="center", | |
) | |
self.profit_pct = pn.indicators.Number( | |
name="Operating Profit", | |
value=0, | |
format="0.00%", | |
default_color="green", | |
align="center", | |
) | |
def _update_processing_hours_slider_constraints(self): | |
new_max_processing_hours = self.labour_hours_per_shift | |
# Ensure min bound is not greater than new max bound | |
current_min_processing_hours = min( | |
self.param.processing_hours_per_shift.bounds[0], new_max_processing_hours | |
) | |
self.param.processing_hours_per_shift.bounds = ( | |
current_min_processing_hours, | |
new_max_processing_hours, | |
) | |
# Check if processing_hours_per_shift_slider exists before trying to update it | |
if hasattr(self, "processing_hours_per_shift_slider"): | |
self.processing_hours_per_shift_slider.end = new_max_processing_hours | |
if self.processing_hours_per_shift > new_max_processing_hours: | |
self.processing_hours_per_shift = new_max_processing_hours | |
# Also update start if it's now greater than end | |
if self.processing_hours_per_shift_slider.start > new_max_processing_hours: | |
self.processing_hours_per_shift_slider.start = ( | |
current_min_processing_hours # or new_max_processing_hours | |
) | |
def _post_calculation_update(self): | |
"""Overrides the base class method to update GUI elements.""" | |
super()._post_calculation_update() # Call base class method if it has any logic | |
self._update_tables_data() | |
def _update_tables_data(self): | |
metric_names = [ | |
"Biomass cost", | |
"Processing cost", | |
"Gross Revenue", | |
"Net Revenue", | |
] | |
money_data_unit_dict = { | |
" ": metric_names, | |
"$/kg Biomass": [ | |
self.bio_cost, | |
self.internal_cogs_per_kg_bio, | |
self.gross_rev_per_kg_bio, | |
self.net_rev_per_kg_bio, | |
], | |
"$/kg Output": [ | |
self.biomass_cost_per_kg_output, | |
self.internal_cogs_per_kg_output, | |
self.wholesale_cbx_price, | |
self.net_rev_per_kg_output, | |
], | |
} | |
self.money_data_unit_df = pd.DataFrame(money_data_unit_dict) | |
if hasattr(self, "money_unit_table"): | |
self.money_unit_table.value = self.money_data_unit_df | |
money_data_time_dict = { | |
" ": metric_names, | |
"Per Shift": [ | |
self.biomass_cost_per_shift, | |
self.internal_cogs_per_shift, | |
self.gross_rev_per_shift, | |
self.net_rev_per_shift, | |
], | |
"Per Day": [ | |
self.biomass_cost_per_day, | |
self.internal_cogs_per_day, | |
self.gross_rev_per_day, | |
self.net_rev_per_day, | |
], | |
"Per Week": [ | |
self.biomass_cost_per_week, | |
self.internal_cogs_per_week, | |
self.gross_rev_per_week, | |
self.net_rev_per_week, | |
], | |
} | |
self.money_data_time_df = pd.DataFrame(money_data_time_dict) | |
if hasattr(self, "money_time_table"): | |
self.money_time_table.value = self.money_data_time_df | |
profit_data_dict = { | |
"Metric": ["Operating Profit", "Resin Spread"], | |
"Value": [ | |
f"{self.operating_profit_pct * 100.0:.2f}%", | |
f"{self.resin_spread_pct * 100.0:.2f}%", | |
], | |
} | |
self.profit_data_df = pd.DataFrame(profit_data_dict) | |
if hasattr(self, "profit_table"): | |
self.profit_table.value = self.profit_data_df | |
processing_values_formatted_shift = [ | |
f"{self.kg_processed_per_shift:,.0f}", | |
f"{self.saleable_kg_per_shift:,.0f}", | |
f"${self.labour_cost_per_shift:,.2f}", | |
f"${self.variable_cost_per_shift:,.2f}", | |
f"${self.overhead_cost_per_shift:,.2f}", | |
] | |
processing_values_formatted_day = [ | |
f"{self.kg_processed_per_shift * self.shifts_per_day:,.0f}", | |
f"{self.saleable_kg_per_day:,.0f}", | |
f"${self.labour_cost_per_shift * self.shifts_per_day:,.2f}", | |
f"${self.variable_cost_per_shift * self.shifts_per_day:,.2f}", | |
f"${self.overhead_cost_per_shift * self.shifts_per_day:,.2f}", | |
] | |
processing_values_formatted_week = [ | |
f"{self.kg_processed_per_shift * self.shifts_per_week:,.0f}", | |
f"{self.saleable_kg_per_week:,.0f}", | |
f"${self.labour_cost_per_shift * self.shifts_per_week:,.2f}", | |
f"${self.variable_cost_per_shift * self.shifts_per_week:,.2f}", | |
f"${self.overhead_cost_per_shift * self.shifts_per_week:,.2f}", | |
] | |
processing_data_dict = { | |
"Metric Per": [ | |
"Kilograms Extracted", | |
"Kg CBx Produced", | |
"Labour Cost", | |
"Variable Cost", | |
"Overhead", | |
], | |
"Shift": processing_values_formatted_shift, | |
"Day": processing_values_formatted_day, | |
"Week": processing_values_formatted_week, | |
} | |
self.processing_data_df = pd.DataFrame(processing_data_dict) | |
if hasattr(self, "processing_table"): | |
self.processing_table.value = self.processing_data_df | |
if hasattr(self, "profit_weekly"): | |
self.profit_weekly.value = self.net_rev_per_week | |
# Ensure format updates if value changes significantly (e.g. from 0 to large number) | |
self.profit_weekly.format = ( | |
f"${self.net_rev_per_week / 1000:.0f} k" | |
if self.net_rev_per_week != 0 | |
else "$0 k" | |
) | |
if hasattr(self, "profit_pct"): | |
self.profit_pct.value = self.operating_profit_pct | |
self.profit_pct.format = f"{self.operating_profit_pct * 100.0:.2f}%" | |
def view(self): | |
input_col_max_width = 400 | |
extractionCol = pn.Column( | |
"### Extraction", | |
self.param.kg_processed_per_hour, | |
self.param.finished_product_yield_pct, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
biomassCol = pn.Column( | |
pn.pane.Markdown("### Biomass parameters", margin=0), | |
self.param.bio_cbx_pct, | |
self.param.bio_cost, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
consumableCol = pn.Column( | |
pn.pane.Markdown("### Consumable rates", margin=0), | |
self.param.kwh_rate, | |
self.param.water_cost_per_1000l, | |
self.param.consumables_per_kg_bio_rate, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
wholesaleCol = pn.Column( | |
pn.pane.Markdown("### Wholesale details", margin=0), | |
self.param.wholesale_cbx_price, | |
self.param.wholesale_cbx_pct, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
variableCol = pn.Column( | |
pn.pane.Markdown("### Variable processing costs", margin=0), | |
self.param.kwh_per_kg_bio, | |
self.param.water_liters_consumed_per_kg_bio, | |
self.param.consumables_per_kg_output, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
complianceBatchCol = pn.Column( | |
pn.pane.Markdown("### Compliance", margin=0), | |
self.param.batch_test_cost, | |
pn.pane.Markdown("New Batch Every:", margin=0), | |
self.batch_frequency_radio, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
leechCol = pn.Column( | |
pn.pane.Markdown("### Weekly Rent & Fixed Overheads", margin=0), | |
self.param.weekly_rent, | |
self.param.non_production_electricity_cost_weekly, | |
self.param.property_insurance_weekly, | |
self.param.general_liability_insurance_weekly, | |
self.param.product_recall_insurance_weekly, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
workerCol = pn.Column( | |
pn.pane.Markdown("### Worker Details", margin=0), | |
self.param.workers_per_shift, | |
self.param.worker_base_pay_rate, | |
self.param.managers_per_shift, | |
self.param.manager_base_pay_rate, | |
self.param.direct_cost_pct, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
shiftCol = pn.Column( | |
pn.pane.Markdown("### Shift details", margin=0), | |
self.param.labour_hours_per_shift, | |
self.param.processing_hours_per_shift, | |
self.param.shifts_per_day, | |
self.param.shifts_per_week, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
input_grid = pn.FlexBox( | |
extractionCol, | |
biomassCol, | |
consumableCol, | |
wholesaleCol, | |
variableCol, | |
complianceBatchCol, | |
workerCol, | |
shiftCol, | |
leechCol, | |
align_content="flex-start", | |
align_items="flex-start", | |
# valid options include: '[stretch, flex-start, flex-end, center, baseline, first baseline, last baseline, start, end, self-start, self-end]' | |
flex_wrap="wrap", | |
) # Added flex_wrap | |
money_unit_table_display = pn.Column( | |
pn.pane.Markdown( | |
"### Financial Summary (Per Unit)", styles={"text-align": "center"} | |
), | |
self.money_unit_table, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width + 50, | |
) | |
money_time_table_display = pn.Column( | |
pn.pane.Markdown( | |
"### Financial Summary (Aggregated)", styles={"text-align": "center"} | |
), | |
self.money_time_table, | |
sizing_mode="stretch_width", | |
max_width=500, | |
) | |
profit_table_display = pn.Column( | |
pn.pane.Markdown("### Profitability", styles={"text-align": "center"}), | |
self.profit_table, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
processing_table_display = pn.Column( | |
pn.pane.Markdown("### Processing Summary", styles={"text-align": "center"}), | |
self.processing_table, | |
sizing_mode="stretch_width", | |
max_width=input_col_max_width, | |
) | |
table_grid = pn.FlexBox( | |
self.profit_weekly, | |
self.profit_pct, | |
processing_table_display, | |
profit_table_display, | |
money_unit_table_display, | |
money_time_table_display, | |
align_content="normal", | |
flex_wrap="wrap", | |
) | |
main_layout = pn.Column( | |
pn.Accordion(("Knobs & Dials",input_grid)), | |
pn.layout.Divider(margin=(10, 0)), | |
table_grid, | |
styles={"margin": "0px 10px"}, | |
) | |
return main_layout |