Files
POPS/app/estimation.py
Martin Hohenberg 0569cd4f02 Add printer cost (€0.35/h) to price estimation
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>
2026-06-19 07:28:46 +02:00

81 lines
3.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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,
}