CBx_estimator / app.py
AccruedInnovation's picture
Upload app.py
c43f411 verified
raw
history blame
33.8 kB
import panel as pn
import pandas as pd
import param
from bokeh.models.formatters import PrintfTickFormatter
# Initialize Panel extension for Tabulator and set a global sizing mode
pn.extension(
"tabulator",
sizing_mode="stretch_width",
template="fast",
# theme = 'dark',
)
# --- 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_table",
sizing_mode="fixed",
align="center",
show_index=False, # Hide index column
)
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
)
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
)
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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, # Updated
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",
)
# Updated based on previous request
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, # Updated
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",
)
# Updated based on previous request
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, # Updated
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, # Updated
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, # Updated
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",
)
@param.depends(
"kg_processed_per_hour",
"finished_product_yield_pct",
"kwh_rate",
"water_cost_per_1000l",
"consumables_per_kg_bio_rate",
"kwh_per_kg_bio",
"water_liters_consumed_per_kg_bio",
"consumables_per_kg_output",
"bio_cbx_pct",
"bio_cost",
"wholesale_cbx_price",
"wholesale_cbx_pct",
"batch_test_cost",
"fixed_overhead_per_week",
"workers_per_shift",
"worker_hourly_rate",
"managers_per_shift",
"manager_hourly_rate",
"labour_hours_per_shift",
"processing_hours_per_shift",
"shifts_per_day",
"shifts_per_week",
watch=True,
)
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()
@param.depends("labour_hours_per_shift", watch=True)
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"],
"Per Kilogram of Biomass": [
self.bio_cost,
self.internal_cogs_per_kg_bio,
self.gross_rev_per_kg_bio,
self.net_rev_per_kg_bio,
],
"Per Kilogram of 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": [
"Kilograms Extracted per Shift",
"Labour Cost per Shift",
"Variable Cost per Shift",
"Overhead per Shift",
],
"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 {
"Per Kilogram of Biomass": get_formatter("$%.02f"),
"Per Kilogram of Output": get_formatter("$%.02f"),
"Per Shift": get_formatter("$%.02f"),
"Per Day": get_formatter("$%.02f"),
"Per Week": get_formatter("$%.02f"),
}
def view(self):
col1 = pn.Column(
pn.pane.Markdown("## Extraction"),
pn.Row(
self.kg_processed_per_hour_slider, # Updated
),
pn.Row(
self.finished_product_yield_pct_slider, # Updated
),
pn.pane.Markdown("## Biomass parameters"),
pn.Row(
self.bio_cbx_pct_slider, # Updated
),
pn.Row(
self.bio_cost_slider, # Updated
),
sizing_mode="stretch_width",
)
col2 = pn.Column(
pn.pane.Markdown("## Consumable rates"),
pn.Row(
self.kwh_rate_slider, # Updated
),
pn.Row(
self.water_cost_per_1000l_slider, # Updated
),
pn.Row(
self.consumables_per_kg_bio_rate_slider, # Updated
),
pn.pane.Markdown("## Wholesale details"),
pn.Row(
self.wholesale_cbx_price_slider, # Updated
),
pn.Row(
self.wholesale_cbx_pct_slider, # Updated
),
sizing_mode="stretch_width",
)
col3 = pn.Column(
pn.pane.Markdown("## Variable costs"),
pn.Row(
self.kwh_per_kg_bio_slider, # Updated
),
pn.Row(
self.water_liters_consumed_per_kg_bio_slider, # Updated
),
pn.Row(
self.consumables_per_kg_output_slider, # Updated
),
pn.pane.Markdown("## Compliance"),
pn.Row(
self.batch_test_cost_slider, # Updated
),
pn.pane.Markdown("## Overhead"),
pn.Row(
self.fixed_overhead_per_week_slider, # Updated
),
sizing_mode="stretch_width",
)
col4 = pn.Column(
pn.pane.Markdown("## Worker Details"),
pn.Row(
self.workers_per_shift_slider, # Updated
),
pn.Row(
self.worker_hourly_rate_slider, # Updated
),
pn.Row(
self.managers_per_shift_slider, # Updated
),
pn.Row(
self.manager_hourly_rate_slider, # Updated
),
pn.pane.Markdown("## Shift details"),
pn.Row(
self.labour_hours_per_shift_slider, # Updated
),
pn.Row(
self.processing_hours_per_shift_slider, # Updated
),
pn.Row(
self.shifts_per_day_slider, # Updated
),
pn.Row(
self.shifts_per_week_slider, # Updated
),
sizing_mode="stretch_width",
)
input_grid = pn.GridSpec(sizing_mode="stretch_width", max_width=1800, margin=10)
input_grid[0, 0] = col1
input_grid[0, 1] = col2
input_grid[0, 2] = col3
input_grid[0, 3] = col4
money_table_display = pn.Column(
pn.pane.Markdown("### Financial Summary", styles={"text-align": "center"}),
self.money_table,
)
profit_table_display = pn.Column(
pn.pane.Markdown(
"### Profitability Metrics", styles={"text-align": "center"}
),
self.profit_table,
)
processing_table_display = pn.Column(
pn.pane.Markdown("### Processing Summary", styles={"text-align": "center"}),
self.processing_table,
)
tables_layout = pn.Column(
money_table_display,
pn.Row(
processing_table_display,
profit_table_display,
align="center",
),
#sizing_mode="stretch_width",
#max_width=1800,
margin=10,
align="center",
)
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",
)
indicator_layout = pn.Column(profit_pct, profit_weekly, align="center")
table_grid = pn.GridSpec(sizing_mode="stretch_width", max_width=1800, margin=10)
table_grid[:, 0:2] = tables_layout
table_grid[:, 2] = indicator_layout
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")
# Instantiate the template with widgets displayed in the sidebar
# template = pn.template.FastListTemplate(
# title="CBx Revenue Estimator (FastList Panel)",
# #theme = custom_themes.DarkTheme,
# #sidebar=[freq, phase],
# )
# template.main.append(estimator_app.view())
# template.servable()
if __name__ == "__main__":
pn.serve(
estimator_app.view(),
title="CBx Revenue Estimator (Panel)",
show=True,
port=5007,
)