Files
POPS/app/estimation.py
Martin Hohenberg 51a7791461 Scale material factor by model size (smaller=denser, larger=fluffier)
Walls are fixed thickness regardless of model size, so small models are
wall-dominated (high material factor) while large models are infill-dominated.

_effective_factor() uses log-linear interpolation:
  factor = MATERIAL_FACTOR - log10(volume / 25 cm³) × 0.15

Example curve:
    0.5 cm³ → 0.76    1 cm³ → 0.71   10 cm³ → 0.56
     26 cm³ → 0.50   100 cm³ → 0.41  500 cm³ → 0.31

MATERIAL_FACTOR env var is now the anchor at 25 cm³ (validated reference).
The card header shows the effective (size-adjusted) factor for the specific job.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:50:32 +02:00

77 lines
2.8 KiB
Python
Raw 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))
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
net_price = after_misprint * 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),
"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,
}