Spaces:
Sleeping
Sleeping
| import panel as pn | |
| import pandas as pd | |
| import param | |
| from bokeh.models.formatters import PrintfTickFormatter | |
| # from custom_themes import AIDefaultTheme, AIDarkTheme | |
| # Initialize Panel extension for Tabulator and set a global sizing mode | |
| pn.extension( | |
| "tabulator", | |
| sizing_mode="stretch_width", | |
| template="fast", | |
| # theme = AIDarkTheme, | |
| ) | |
| # --- Styling Placeholders (as per user instruction) --- | |
| slider_design = {} | |
| slider_style = {} | |
| slider_stylesheet = [] | |
| # --- Helper for NumberFormatters --- | |
| def get_formatter(format_str): | |
| if format_str == "%i": | |
| return pn.widgets.tables.NumberFormatter(format="0") | |
| elif format_str == "%.1f": | |
| return pn.widgets.tables.NumberFormatter(format="0.0") | |
| elif format_str == "%.2f": | |
| return pn.widgets.tables.NumberFormatter(format="0.00") | |
| elif format_str == "%.4f": | |
| return pn.widgets.tables.NumberFormatter(format="0.0000") | |
| elif format_str == "$%.02f": | |
| return pn.widgets.tables.NumberFormatter(format="$0,0.00") | |
| return format_str | |
| class CannabinoidEstimator(param.Parameterized): | |
| # --- Input Parameters --- | |
| kg_processed_per_hour = param.Number( | |
| default=150.0, | |
| bounds=(0, 2000), | |
| step=1.0, | |
| label="Biomass processed per hour (kg)", | |
| ) | |
| finished_product_yield_pct = param.Number( | |
| default=60.0, | |
| bounds=(0.01, 100), | |
| step=0.01, | |
| label="Product yield: CBx Weight Output / Weight Input (%)", | |
| ) | |
| kwh_rate = param.Number( | |
| default=0.25, bounds=(0.01, 5), step=0.01, label="Power rate ($ per kWh)" | |
| ) | |
| water_cost_per_1000l = param.Number( | |
| default=2.50, | |
| bounds=(0.01, 10), | |
| step=0.01, | |
| label="Water rate ($ per 1000L / m3)", | |
| ) | |
| consumables_per_kg_bio_rate = param.Number( | |
| default=0.0032, | |
| bounds=(0, 10), | |
| step=0.0001, | |
| label="Other Consumables rate ($ per kg biomass)", | |
| ) | |
| kwh_per_kg_bio = param.Number( | |
| default=0.25, | |
| bounds=(0.05, 15), | |
| step=0.01, | |
| label="Power consumption (kWh per kg biomass)", | |
| ) | |
| water_liters_consumed_per_kg_bio = param.Number( | |
| default=3.0, | |
| bounds=(0.1, 100), | |
| step=0.1, | |
| label="Water consumption (liters per kg biomass)", | |
| ) | |
| consumables_per_kg_output = param.Number( | |
| default=10.0, | |
| bounds=(0, 100), | |
| step=0.01, | |
| label="Consumables per kg finished product ($)", | |
| ) | |
| bio_cbx_pct = param.Number( | |
| default=10.0, bounds=(0, 30), step=0.1, label="Cannabinoid (CBx) in biomass (%)" | |
| ) | |
| bio_cost = param.Number( | |
| default=3.0, | |
| bounds=(0, 200), | |
| step=0.25, | |
| label="Biomass purchase cost ($ per kg)", | |
| ) | |
| wholesale_cbx_price = param.Number( | |
| default=220.0, | |
| bounds=(25, 6000), | |
| step=5.0, | |
| label="Gross revenue ($ per kg output)", | |
| ) | |
| wholesale_cbx_pct = param.Number( | |
| default=99.9, bounds=(0, 100), step=0.01, label="CBx in finished product (%)" | |
| ) | |
| batch_test_cost = param.Number( | |
| default=1300.0, | |
| bounds=(100, 5000), | |
| step=25.0, | |
| label="Per-batch testing/compliance costs ($)", | |
| ) | |
| fixed_overhead_per_week = param.Number( | |
| default=2000.0, bounds=(0, 10000), step=1.0, label="Weekly fixed costs ($)" | |
| ) | |
| workers_per_shift = param.Number( | |
| default=9.0, bounds=(1, 20), step=1.0, label="Workers per shift" | |
| ) | |
| worker_hourly_rate = param.Number( | |
| default=5.0, bounds=(0.25, 50), step=0.25, label="Worker loaded pay rate ($/hr)" | |
| ) | |
| managers_per_shift = param.Number( | |
| default=1.0, bounds=(1, 10), step=1.0, label="Supervisors per shift" | |
| ) | |
| manager_hourly_rate = param.Number( | |
| default=10.0, | |
| bounds=(5.0, 50), | |
| step=0.25, | |
| label="Supervisor loaded pay rate ($/hr)", | |
| ) | |
| processing_hours_per_shift = param.Number( | |
| default=7.0, bounds=(0.25, 8.0), step=0.25, label="Processing hours per shift" | |
| ) | |
| labour_hours_per_shift = param.Number( | |
| default=8.0, bounds=(6.0, 12), step=0.25, label="Labor hours per shift" | |
| ) | |
| shifts_per_day = param.Number( | |
| default=3.0, bounds=(1, 10), step=1.0, label="Shifts per day" | |
| ) | |
| shifts_per_week = param.Number( | |
| default=21.0, bounds=(1, 28), step=1.0, label="Shifts per week" | |
| ) | |
| kg_processed_per_shift = 0.0 | |
| labour_cost_per_shift = 0.0 | |
| variable_cost_per_shift = 0.0 | |
| overhead_cost_per_shift = 0.0 | |
| saleable_kg_per_kg_bio = 0.0 | |
| saleable_kg_per_shift = 0.0 | |
| saleable_kg_per_day = 0.0 | |
| saleable_kg_per_week = 0.0 | |
| biomass_kg_per_saleable_kg = 0.0 | |
| internal_cogs_per_kg_bio = 0.0 | |
| internal_cogs_per_shift = 0.0 | |
| internal_cogs_per_day = 0.0 | |
| internal_cogs_per_week = 0.0 | |
| internal_cogs_per_kg_output = 0.0 | |
| biomass_cost_per_shift = 0.0 | |
| biomass_cost_per_day = 0.0 | |
| biomass_cost_per_week = 0.0 | |
| biomass_cost_per_kg_output = 0.0 | |
| gross_rev_per_kg_bio = 0.0 | |
| gross_rev_per_shift = 0.0 | |
| gross_rev_per_day = 0.0 | |
| gross_rev_per_week = 0.0 | |
| net_rev_per_kg_bio = 0.0 | |
| net_rev_per_shift = 0.0 | |
| net_rev_per_day = 0.0 | |
| net_rev_per_week = 0.0 | |
| net_rev_per_kg_output = 0.0 | |
| operating_profit_pct = 0.0 | |
| resin_spread_pct = 0.0 | |
| money_data_df = param.DataFrame(pd.DataFrame()) | |
| profit_data_df = param.DataFrame(pd.DataFrame()) | |
| processing_data_df = param.DataFrame(pd.DataFrame()) | |
| def __init__(self, **params): | |
| super().__init__(**params) | |
| self._create_sliders() | |
| self.money_table = pn.widgets.Tabulator( | |
| self.money_data_df, | |
| formatters=self._get_money_formatters(), | |
| disabled=True, | |
| layout="fit_data", | |
| sizing_mode="fixed", | |
| align="center", | |
| show_index=False, # Hide index column | |
| text_align={ | |
| " ": "right", | |
| "$/kg Biomass": "center", | |
| "$/kg Output": "center", | |
| "Per Shift": "center", | |
| "Per Day": "center", | |
| "Per Week": "center", | |
| }, | |
| ) | |
| self.profit_table = pn.widgets.Tabulator( | |
| self.profit_data_df, | |
| disabled=True, | |
| layout="fit_data_table", | |
| sizing_mode="fixed", | |
| align="center", | |
| show_index=False, # Hide index column | |
| text_align={ | |
| "Metric": "right", | |
| "Value": "center", | |
| }, | |
| ) | |
| self.processing_table = pn.widgets.Tabulator( | |
| self.processing_data_df, | |
| formatters={}, | |
| disabled=True, | |
| layout="fit_data_table", | |
| sizing_mode="fixed", | |
| align="center", | |
| show_index=False, # Hide index column | |
| text_align={ | |
| "Metric (Per Shift)": "right", | |
| "Value": "center", | |
| }, | |
| ) | |
| self._update_calculations() | |
| def _create_sliders(self): | |
| self.kg_processed_per_hour_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.kg_processed_per_hour, | |
| name=self.param.kg_processed_per_hour.label, | |
| fixed_start=self.param.kg_processed_per_hour.bounds[0], | |
| fixed_end=self.param.kg_processed_per_hour.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| # format="0", | |
| format=PrintfTickFormatter(format="%i kg"), | |
| ) | |
| self.finished_product_yield_pct_slider = ( | |
| pn.widgets.EditableFloatSlider.from_param( | |
| self.param.finished_product_yield_pct, | |
| name=self.param.finished_product_yield_pct.label, | |
| fixed_start=self.param.finished_product_yield_pct.bounds[0], | |
| fixed_end=self.param.finished_product_yield_pct.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| ) | |
| self.kwh_rate_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.kwh_rate, | |
| name=self.param.kwh_rate.label, | |
| fixed_start=self.param.kwh_rate.bounds[0], | |
| fixed_end=self.param.kwh_rate.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| # format=PrintfTickFormatter(format='%.2f per kWh'), | |
| ) | |
| self.water_cost_per_1000l_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.water_cost_per_1000l, | |
| name=self.param.water_cost_per_1000l.label, | |
| fixed_start=self.param.water_cost_per_1000l.bounds[0], | |
| fixed_end=self.param.water_cost_per_1000l.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.consumables_per_kg_bio_rate_slider = ( | |
| pn.widgets.EditableFloatSlider.from_param( | |
| self.param.consumables_per_kg_bio_rate, | |
| name=self.param.consumables_per_kg_bio_rate.label, | |
| fixed_start=self.param.consumables_per_kg_bio_rate.bounds[0], | |
| fixed_end=self.param.consumables_per_kg_bio_rate.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.0000", | |
| ) | |
| ) | |
| self.kwh_per_kg_bio_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.kwh_per_kg_bio, | |
| name=self.param.kwh_per_kg_bio.label, | |
| fixed_start=self.param.kwh_per_kg_bio.bounds[0], | |
| fixed_end=self.param.kwh_per_kg_bio.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.water_liters_consumed_per_kg_bio_slider = ( | |
| pn.widgets.EditableFloatSlider.from_param( | |
| self.param.water_liters_consumed_per_kg_bio, | |
| name=self.param.water_liters_consumed_per_kg_bio.label, | |
| fixed_start=self.param.water_liters_consumed_per_kg_bio.bounds[0], | |
| fixed_end=self.param.water_liters_consumed_per_kg_bio.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.0", | |
| ) | |
| ) | |
| self.consumables_per_kg_output_slider = ( | |
| pn.widgets.EditableFloatSlider.from_param( | |
| self.param.consumables_per_kg_output, | |
| name=self.param.consumables_per_kg_output.label, | |
| fixed_start=self.param.consumables_per_kg_output.bounds[0], | |
| fixed_end=self.param.consumables_per_kg_output.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| ) | |
| self.bio_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.bio_cbx_pct, | |
| name=self.param.bio_cbx_pct.label, | |
| fixed_start=self.param.bio_cbx_pct.bounds[0], | |
| fixed_end=self.param.bio_cbx_pct.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.0", | |
| ) | |
| self.bio_cost_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.bio_cost, | |
| name=self.param.bio_cost.label, | |
| fixed_start=self.param.bio_cost.bounds[0], | |
| fixed_end=self.param.bio_cost.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.wholesale_cbx_price_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.wholesale_cbx_price, | |
| name=self.param.wholesale_cbx_price.label, | |
| fixed_start=self.param.wholesale_cbx_price.bounds[0], | |
| fixed_end=self.param.wholesale_cbx_price.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| self.wholesale_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.wholesale_cbx_pct, | |
| name=self.param.wholesale_cbx_pct.label, | |
| fixed_start=self.param.wholesale_cbx_pct.bounds[0], | |
| fixed_end=self.param.wholesale_cbx_pct.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.batch_test_cost_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.batch_test_cost, | |
| name=self.param.batch_test_cost.label, | |
| fixed_start=self.param.batch_test_cost.bounds[0], | |
| fixed_end=self.param.batch_test_cost.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| self.fixed_overhead_per_week_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.fixed_overhead_per_week, | |
| name=self.param.fixed_overhead_per_week.label, | |
| fixed_start=self.param.fixed_overhead_per_week.bounds[0], | |
| fixed_end=self.param.fixed_overhead_per_week.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| self.workers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.workers_per_shift, | |
| name=self.param.workers_per_shift.label, | |
| fixed_start=self.param.workers_per_shift.bounds[0], | |
| fixed_end=self.param.workers_per_shift.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| self.worker_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.worker_hourly_rate, | |
| name=self.param.worker_hourly_rate.label, | |
| fixed_start=self.param.worker_hourly_rate.bounds[0], | |
| fixed_end=self.param.worker_hourly_rate.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.managers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.managers_per_shift, | |
| name=self.param.managers_per_shift.label, | |
| fixed_start=self.param.managers_per_shift.bounds[0], | |
| fixed_end=self.param.managers_per_shift.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| self.manager_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.manager_hourly_rate, | |
| name=self.param.manager_hourly_rate.label, | |
| fixed_start=self.param.worker_hourly_rate.default, # Keeping original logic as per file | |
| fixed_end=self.param.manager_hourly_rate.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.labour_hours_per_shift_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.labour_hours_per_shift, | |
| name=self.param.labour_hours_per_shift.label, | |
| fixed_start=self.param.labour_hours_per_shift.bounds[ | |
| 0 | |
| ], # Changed in previous request | |
| fixed_end=self.param.labour_hours_per_shift.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| self.processing_hours_per_shift_slider = ( | |
| pn.widgets.EditableFloatSlider.from_param( | |
| self.param.processing_hours_per_shift, | |
| name=self.param.processing_hours_per_shift.label, | |
| fixed_start=self.param.processing_hours_per_shift.bounds[0], | |
| fixed_end=self.labour_hours_per_shift, # Changed in previous request | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0.00", | |
| ) | |
| ) | |
| self.shifts_per_day_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.shifts_per_day, | |
| name=self.param.shifts_per_day.label, | |
| fixed_start=self.param.shifts_per_day.bounds[0], | |
| fixed_end=self.param.shifts_per_day.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| self.shifts_per_week_slider = pn.widgets.EditableFloatSlider.from_param( | |
| self.param.shifts_per_week, | |
| name=self.param.shifts_per_week.label, | |
| fixed_start=self.param.shifts_per_week.bounds[0], | |
| fixed_end=self.param.shifts_per_week.bounds[1], | |
| design=slider_design, | |
| styles=slider_style, | |
| stylesheets=slider_stylesheet, | |
| format="0", | |
| ) | |
| def _update_calculations(self, *events): | |
| self.kg_processed_per_shift = ( | |
| self.processing_hours_per_shift * self.kg_processed_per_hour | |
| ) | |
| if self.shifts_per_week == 0: | |
| self.shifts_per_week = 1 | |
| self._calc_saleable_kg() | |
| self._calc_biomass_cost() | |
| self._calc_cogs() | |
| self._calc_gross_revenue() | |
| self._calc_net_revenue() | |
| self.operating_profit_pct = ( | |
| (self.net_rev_per_kg_bio / self.gross_rev_per_kg_bio) | |
| if self.gross_rev_per_kg_bio | |
| else 0.0 | |
| ) | |
| self.resin_spread_pct = ( | |
| ((self.gross_rev_per_kg_bio - self.bio_cost) / self.bio_cost) | |
| if self.bio_cost | |
| else 0.0 | |
| ) | |
| self._update_tables_data() | |
| def _update_processing_hours_slider_constraints(self): | |
| new_max_processing_hours = self.labour_hours_per_shift | |
| # Get the current lower bound of the processing_hours_per_shift parameter | |
| current_min_processing_hours = self.param.processing_hours_per_shift.bounds[0] | |
| # Update the bounds of the underlying param.Number object for processing_hours_per_shift | |
| # This allows the parameter to accept values up to the new maximum | |
| self.param.processing_hours_per_shift.bounds = ( | |
| current_min_processing_hours, | |
| new_max_processing_hours, | |
| ) | |
| # Ensure the slider widget has been created before trying to access it | |
| if hasattr(self, "processing_hours_per_shift_slider"): | |
| # Update the 'end' property of the slider widget | |
| self.processing_hours_per_shift_slider.end = new_max_processing_hours | |
| # If the current value of processing_hours_per_shift is now greater than | |
| # the new maximum, adjust it to be the new maximum. | |
| if self.processing_hours_per_shift > new_max_processing_hours: | |
| self.processing_hours_per_shift = new_max_processing_hours | |
| def _calc_cogs(self): | |
| worker_cost = self.workers_per_shift * self.worker_hourly_rate | |
| manager_cost = self.managers_per_shift * self.manager_hourly_rate | |
| self.labour_cost_per_shift = ( | |
| worker_cost + manager_cost | |
| ) * self.labour_hours_per_shift | |
| power_cost_per_kg = self.kwh_rate * self.kwh_per_kg_bio | |
| water_cost_per_kg = ( | |
| self.water_cost_per_1000l / 1000.0 | |
| ) * self.water_liters_consumed_per_kg_bio | |
| total_variable_consumable_cost_per_kg = ( | |
| self.consumables_per_kg_bio_rate + power_cost_per_kg + water_cost_per_kg | |
| ) | |
| self.variable_cost_per_shift = ( | |
| total_variable_consumable_cost_per_kg * self.kg_processed_per_shift | |
| ) | |
| self.overhead_cost_per_shift = ( | |
| self.fixed_overhead_per_week / self.shifts_per_week | |
| if self.shifts_per_week > 0 | |
| else 0.0 | |
| ) | |
| shift_cogs_before_output_specific = ( | |
| self.labour_cost_per_shift | |
| + self.variable_cost_per_shift | |
| + self.overhead_cost_per_shift | |
| ) | |
| shift_output_specific_cogs = ( | |
| self.consumables_per_kg_output * self.saleable_kg_per_shift | |
| ) | |
| self.internal_cogs_per_shift = ( | |
| shift_cogs_before_output_specific + shift_output_specific_cogs | |
| ) | |
| self.internal_cogs_per_kg_bio = ( | |
| self.internal_cogs_per_shift / self.kg_processed_per_shift | |
| if self.kg_processed_per_shift > 0 | |
| else 0.0 | |
| ) | |
| self.internal_cogs_per_day = self.internal_cogs_per_shift * self.shifts_per_day | |
| self.internal_cogs_per_week = ( | |
| self.internal_cogs_per_shift * self.shifts_per_week | |
| ) | |
| self.internal_cogs_per_kg_output = ( | |
| (self.internal_cogs_per_kg_bio * self.biomass_kg_per_saleable_kg) | |
| if self.biomass_kg_per_saleable_kg != 0 | |
| else 0.0 | |
| ) | |
| def _calc_gross_revenue(self): | |
| self.gross_rev_per_kg_bio = ( | |
| self.saleable_kg_per_kg_bio * self.wholesale_cbx_price | |
| ) | |
| self.gross_rev_per_shift = ( | |
| self.gross_rev_per_kg_bio * self.kg_processed_per_shift | |
| ) | |
| self.gross_rev_per_day = self.gross_rev_per_shift * self.shifts_per_day | |
| self.gross_rev_per_week = self.gross_rev_per_shift * self.shifts_per_week | |
| def _calc_net_revenue(self): | |
| self.net_rev_per_kg_bio = ( | |
| self.gross_rev_per_kg_bio - self.internal_cogs_per_kg_bio - self.bio_cost | |
| ) | |
| self.net_rev_per_shift = self.net_rev_per_kg_bio * self.kg_processed_per_shift | |
| self.net_rev_per_day = self.net_rev_per_shift * self.shifts_per_day | |
| self.net_rev_per_week = self.net_rev_per_shift * self.shifts_per_week | |
| self.net_rev_per_kg_output = ( | |
| (self.biomass_kg_per_saleable_kg * self.net_rev_per_kg_bio) | |
| if self.biomass_kg_per_saleable_kg != 0 | |
| else 0.0 | |
| ) | |
| def _calc_biomass_cost(self): | |
| self.biomass_cost_per_shift = self.kg_processed_per_shift * self.bio_cost | |
| self.biomass_cost_per_day = self.biomass_cost_per_shift * self.shifts_per_day | |
| self.biomass_cost_per_week = self.biomass_cost_per_shift * self.shifts_per_week | |
| def _calc_saleable_kg(self): | |
| if self.wholesale_cbx_pct == 0: | |
| self.saleable_kg_per_kg_bio = 0.0 | |
| else: | |
| self.saleable_kg_per_kg_bio = ( | |
| (self.bio_cbx_pct / 100.0) | |
| * (self.finished_product_yield_pct / 100.0) | |
| / (self.wholesale_cbx_pct / 100.0) | |
| ) | |
| self.saleable_kg_per_shift = ( | |
| self.saleable_kg_per_kg_bio * self.kg_processed_per_shift | |
| ) | |
| self.saleable_kg_per_day = self.saleable_kg_per_shift * self.shifts_per_day | |
| self.saleable_kg_per_week = self.saleable_kg_per_shift * self.shifts_per_week | |
| self.biomass_kg_per_saleable_kg = ( | |
| 1 / self.saleable_kg_per_kg_bio if self.saleable_kg_per_kg_bio > 0 else 0.0 | |
| ) | |
| self.biomass_cost_per_kg_output = ( | |
| self.biomass_kg_per_saleable_kg * self.bio_cost | |
| ) | |
| def _update_tables_data(self): | |
| money_data_dict = { | |
| " ": ["Biomass cost", "Processing cost", "Gross Revenue", "Net Revenue"], | |
| "$/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, | |
| ], | |
| "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_df = pd.DataFrame(money_data_dict) | |
| if hasattr(self, "money_table"): | |
| self.money_table.value = self.money_data_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 = [ | |
| f"{self.kg_processed_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_data_dict = { | |
| "Metric (Per Shift)": [ | |
| "Kilograms Extracted", | |
| "Labour Cost", | |
| "Variable Cost", | |
| "Overhead", | |
| ], | |
| "Value": processing_values_formatted, | |
| } | |
| self.processing_data_df = pd.DataFrame(processing_data_dict) | |
| if hasattr(self, "processing_table"): | |
| self.processing_table.value = self.processing_data_df | |
| def _get_money_formatters(self): | |
| return { | |
| "$/kg Biomass": get_formatter("$%.02f"), | |
| "$/kg Output": get_formatter("$%.02f"), | |
| "Per Shift": get_formatter("$%.02f"), | |
| "Per Day": get_formatter("$%.02f"), | |
| "Per Week": get_formatter("$%.02f"), | |
| } | |
| def view(self): | |
| input_col_max_width = 400 | |
| col1 = pn.Column( | |
| "### Extraction", | |
| self.kg_processed_per_hour_slider, | |
| self.finished_product_yield_pct_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col2 = pn.Column( | |
| pn.pane.Markdown("### Biomass parameters"), | |
| self.bio_cbx_pct_slider, | |
| self.bio_cost_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col3 = pn.Column( | |
| pn.pane.Markdown("### Consumable rates"), | |
| self.kwh_rate_slider, | |
| self.water_cost_per_1000l_slider, | |
| self.consumables_per_kg_bio_rate_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col4 = pn.Column( | |
| pn.pane.Markdown("### Wholesale details"), | |
| self.wholesale_cbx_price_slider, | |
| self.wholesale_cbx_pct_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col5 = pn.Column( | |
| pn.pane.Markdown("### Variable costs"), | |
| self.kwh_per_kg_bio_slider, | |
| self.water_liters_consumed_per_kg_bio_slider, | |
| self.consumables_per_kg_output_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col6 = pn.Column( | |
| pn.pane.Markdown("### Compliance"), | |
| self.batch_test_cost_slider, | |
| pn.pane.Markdown("### Overhead"), | |
| self.fixed_overhead_per_week_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col8 = pn.Column( | |
| pn.pane.Markdown("### Worker Details"), | |
| self.workers_per_shift_slider, | |
| self.worker_hourly_rate_slider, | |
| self.managers_per_shift_slider, | |
| self.manager_hourly_rate_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| col9 = pn.Column( | |
| pn.pane.Markdown("### Shift details"), | |
| self.labour_hours_per_shift_slider, | |
| self.processing_hours_per_shift_slider, | |
| self.shifts_per_day_slider, | |
| self.shifts_per_week_slider, | |
| sizing_mode="stretch_width", | |
| max_width=input_col_max_width, | |
| ) | |
| input_grid = pn.FlexBox( | |
| col1, col2, col3, col4, col5, col8, col9, col6, align_content="normal" | |
| ) | |
| money_table_display = pn.Column( | |
| pn.pane.Markdown("### Financial Summary", styles={"text-align": "center"}), | |
| self.money_table, | |
| sizing_mode="stretch_width", | |
| max_width=700, | |
| ) | |
| 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, | |
| ) | |
| profit_weekly = pn.indicators.Number( | |
| name="Weekly Profit", | |
| value=self.net_rev_per_week, | |
| format=f"${self.net_rev_per_week / 1000:.0f} k", | |
| default_color="green", | |
| align="center", | |
| ) | |
| profit_pct = pn.indicators.Number( | |
| name="Operating Profit", | |
| value=self.operating_profit_pct, | |
| format=f"{self.operating_profit_pct * 100.0:.2f}%", | |
| default_color="green", | |
| align="center", | |
| ) | |
| table_grid = pn.FlexBox( | |
| profit_weekly, | |
| profit_pct, | |
| processing_table_display, | |
| profit_table_display, | |
| money_table_display, | |
| align_content="normal", | |
| ) | |
| main_layout = pn.Column( | |
| input_grid, | |
| pn.layout.Divider(margin=(10, 0)), | |
| table_grid, | |
| styles={"margin": "0px 10px"}, | |
| ) | |
| return main_layout | |
| estimator_app = CannabinoidEstimator() | |
| # To run in a Panel server: | |
| # pn.config.raw_css = custom_themes.get_base_css(custom_themes.DARK_THEME_VARS) | |
| estimator_app.view().servable(title="CBx Revenue Estimator") | |
| if __name__ == "__main__": | |
| pn.serve( | |
| estimator_app.view(), | |
| title="CBx Revenue Estimator (Panel)", | |
| show=True, | |
| port=5007, | |
| ) | |