CBx_estimator / gui.py
AccruedInnovation's picture
Upload 2 files
8d505d1 verified
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",
)
@param.depends("labour_hours_per_shift", watch=True)
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