Adds depreciation/electricity as a separate line item in the production cost breakdown. Included in the markup base so it contributes to margin. Configurable via PRINTER_COST_PER_HOUR env var (default 0.35). Bracket gross total updates automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
3.2 KiB
Python
81 lines
3.2 KiB
Python
"""Job cost and time estimation from STL geometry."""
|
||
import math
|
||
import os
|
||
|
||
# g/cm³ per material
|
||
_DENSITY: dict[str, float] = {
|
||
"PLA": 1.24,
|
||
"PETG": 1.27,
|
||
"ABS": 1.04,
|
||
"ASA": 1.07,
|
||
"TPU": 1.20,
|
||
"Nylon": 1.14,
|
||
"PC": 1.20,
|
||
}
|
||
_DEFAULT_DENSITY = 1.24
|
||
|
||
# Volume (cm³) at which MATERIAL_FACTOR applies exactly.
|
||
# Calibrated to Bambu Lab Studio: 26 cm³ lid → 50 % factor → 16.2 g (actual 16.24 g).
|
||
_REF_VOLUME = 25.0
|
||
|
||
# Factor change per decade of volume.
|
||
# Each 10× increase in volume → factor drops by this amount (models get fluffier).
|
||
# Each 10× decrease → factor rises by this amount (models get denser).
|
||
# 0.15 gives: 1 cm³ → +0.21, 250 cm³ → −0.15 relative to base.
|
||
_SIZE_SLOPE = 0.15
|
||
|
||
|
||
def _effective_factor(volume_ccm: float, base: float) -> float:
|
||
"""Scale material factor by model size using log-linear interpolation."""
|
||
factor = base - math.log10(volume_ccm / _REF_VOLUME) * _SIZE_SLOPE
|
||
return round(max(0.25, min(0.85, factor)), 3)
|
||
|
||
|
||
def estimate(volume_ccm: float, material: str | None) -> dict:
|
||
"""
|
||
Returns a cost/time estimate dict for one job.
|
||
|
||
MATERIAL_FACTOR (env, default 0.50):
|
||
Base fraction of model volume that becomes filament, calibrated at
|
||
25 cm³. Scaled up for small models (wall-dominated) and down for large
|
||
models (infill-dominated). Approximate scale:
|
||
~1 cm³ → ×1.4 (tiny clip, almost all walls)
|
||
~25 cm³ → ×1.0 (reference — validated against slicer)
|
||
~250 cm³ → ×0.7 (large box, infill dominates)
|
||
|
||
PRINT_SPEED_G_PER_HOUR (env, default 32):
|
||
~32 g/h matches Bambu Lab at normal quality; use ~20 for slower printers.
|
||
"""
|
||
vat_rate = float(os.environ.get("VAT_RATE", 19))
|
||
price_per_kg = float(os.environ.get("FILAMENT_PRICE_PER_KG", 15))
|
||
base_factor = float(os.environ.get("MATERIAL_FACTOR", 0.50))
|
||
print_speed = float(os.environ.get("PRINT_SPEED_G_PER_HOUR", 32))
|
||
printer_cost_per_hour = float(os.environ.get("PRINTER_COST_PER_HOUR", 0.35))
|
||
density = _DENSITY.get(material or "", _DEFAULT_DENSITY)
|
||
|
||
eff_factor = _effective_factor(volume_ccm, base_factor)
|
||
material_ccm = volume_ccm * eff_factor
|
||
weight_g = material_ccm * density
|
||
print_minutes = round((weight_g / print_speed) * 60)
|
||
|
||
filament_cost = weight_g / 1000 * price_per_kg
|
||
after_misprint = filament_cost * 1.20
|
||
printer_cost = (print_minutes / 60) * printer_cost_per_hour
|
||
net_price = (after_misprint + printer_cost) * 1.30
|
||
gross_price = net_price * (1 + vat_rate / 100)
|
||
|
||
return {
|
||
"material_ccm": round(material_ccm, 2),
|
||
"weight_g": round(weight_g, 1),
|
||
"print_minutes": print_minutes,
|
||
"filament_cost": round(filament_cost, 2),
|
||
"printer_cost": round(printer_cost, 2),
|
||
"net_price": round(net_price, 2),
|
||
"gross_price": round(gross_price, 2),
|
||
"vat_rate": vat_rate,
|
||
"price_per_kg": price_per_kg,
|
||
"material_factor": eff_factor,
|
||
"print_speed": print_speed,
|
||
"printer_cost_per_hour": printer_cost_per_hour,
|
||
}
|