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=8.0, bounds=(0, 30), step=0.1, label="Cannabinoid (CBx) in biomass (%)" ) bio_cost = param.Number( default=20.0, bounds=(0, 200), step=0.25, label="Biomass purchase cost ($ per kg)", ) wholesale_cbx_price = param.Number( default=800.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=1250.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=3.0, bounds=(1, 20), step=1.0, label="Workers per shift" ) worker_base_pay_rate = param.Number( default=30.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=40.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=1.0, bounds=(1, 10), step=1.0, label="Shifts per day" ) shifts_per_week = param.Number( default=2.0, bounds=(1, 28), step=1.0, label="Shifts per week" ) batch_frequency = param.String(default="Week", 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 )