Spaces:
Sleeping
Sleeping
| 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", | |
| ) | |
| knobs = pn.Accordion(("Knobs & Dials",input_grid)) | |
| knobs.active = [0] | |
| main_layout = pn.Column( | |
| knobs, | |
| pn.layout.Divider(margin=(10, 0)), | |
| table_grid, | |
| styles={"margin": "0px 10px"}, | |
| ) | |
| return main_layout |