Spaces:
Sleeping
Sleeping
| 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=2.75, 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=5.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. | |
| 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 | |
| ) |