CBx_estimator / calculations.py
AccruedInnovation's picture
Upload 2 files
8d505d1 verified
import param
class CannabinoidCalculations(param.Parameterized):
# --- Input Parameters ---
kg_processed_per_hour = param.Number(
default=150.0,
bounds=(0, 3000),
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 ($)",
)
weekly_rent = param.Number(
default=2000.0, bounds=(0, 10000), step=1.0, label="Weekly rent ($)"
)
non_production_electricity_cost_weekly = param.Number(
default=100.0, bounds=(0, 2000), step=1.0, label="Weekly Non-production Electricity Cost ($)"
)
property_insurance_weekly = param.Number(
default=100.0, bounds=(0, 2000), step=1.0, label="Weekly Property Insurance ($)"
)
general_liability_insurance_weekly = param.Number(
default=100.0, bounds=(0, 2000), step=1.0, label="Weekly General Liability Insurance ($)"
)
product_recall_insurance_weekly = param.Number(
default=100.0, bounds=(0, 2000), step=1.0, label="Weekly Product Recall Insurance ($)"
)
workers_per_shift = param.Number(
default=9.0, bounds=(1, 20), step=1.0, label="Workers per shift"
)
worker_base_pay_rate = param.Number(
default=5.0, bounds=(0.25, 50), step=0.25, label="Worker base pay rate ($/hr)"
)
managers_per_shift = param.Number(
default=1.0, bounds=(1, 10), step=1.0, label="Supervisors per shift"
)
manager_base_pay_rate = param.Number(
default=10.0,
bounds=(5.0, 50),
step=0.25,
label="Supervisor base pay rate ($/hr)",
)
direct_cost_pct = param.Number(
default=33.0,
bounds=(0, 200),
step=0.1,
label="Direct Costs (% of Base Pay)",
)
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"
)
batch_frequency = param.String(default="Day", label="New batch frequency")
# --- Calculated Attributes ---
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
batch_test_cost_per_shift = 0.0
def __init__(self, **params):
super().__init__(**params)
# Initial calculation can be triggered here if desired,
# or by the class that instantiates it (like the GUI or a financial model).
# For now, the GUI class calls _update_calculations() after super().__init__
# and its own _create_sliders(), which is fine.
@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",
"batch_frequency",
"weekly_rent",
"non_production_electricity_cost_weekly",
"property_insurance_weekly",
"general_liability_insurance_weekly",
"product_recall_insurance_weekly",
"workers_per_shift",
"worker_base_pay_rate",
"managers_per_shift",
"manager_base_pay_rate",
"direct_cost_pct",
"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: # Avoid division by zero
self.shifts_per_week = (
1e-9 # A very small number to avoid errors, or handle differently
)
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._post_calculation_update() # Hook for subclasses
def _post_calculation_update(self):
"""Placeholder for any actions needed after calculations are updated.
Can be overridden by subclasses (like the GUI class).
"""
pass
def _calc_cogs(self):
worker_total_comp_rate = self.worker_base_pay_rate * (
1 + self.direct_cost_pct / 100.0
)
manager_total_comp_rate = self.manager_base_pay_rate * (
1 + self.direct_cost_pct / 100.0
)
worker_cost = self.workers_per_shift * worker_total_comp_rate
manager_cost = self.managers_per_shift * manager_total_comp_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
)
total_fixed_overhead_per_week = (
self.weekly_rent
+ self.non_production_electricity_cost_weekly
+ self.property_insurance_weekly
+ self.general_liability_insurance_weekly
+ self.product_recall_insurance_weekly
)
self.overhead_cost_per_shift = (
total_fixed_overhead_per_week / self.shifts_per_week
if self.shifts_per_week > 0 # Ensure shifts_per_week is positive
else 0.0
)
self.batch_test_cost_per_shift = 0.0
if self.batch_frequency == "Shift":
self.batch_test_cost_per_shift = self.batch_test_cost
elif self.batch_frequency == "Day":
if self.shifts_per_day > 0:
self.batch_test_cost_per_shift = (
self.batch_test_cost / self.shifts_per_day
)
else:
self.batch_test_cost_per_shift = 0.0
elif self.batch_frequency == "Week":
if self.shifts_per_week > 0:
self.batch_test_cost_per_shift = (
self.batch_test_cost / self.shifts_per_week
)
else:
self.batch_test_cost_per_shift = 0.0
shift_cogs_before_output_specific = (
self.labour_cost_per_shift
+ self.variable_cost_per_shift
+ self.overhead_cost_per_shift
+ self.batch_test_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 # and self.biomass_kg_per_saleable_kg is not None
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 # and self.biomass_kg_per_saleable_kg is not None
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
)