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>
This commit is contained in:
Martin Hohenberg
2026-06-18 22:50:32 +02:00
parent 908cfbc981
commit 51a7791461

View File

@@ -1,4 +1,5 @@
"""Job cost and time estimation from STL geometry."""
import math
import os
# g/cm³ per material
@@ -13,29 +14,46 @@ _DENSITY: dict[str, float] = {
}
_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):
Fraction of model volume that becomes actual filament.
'20 % infill' in the slicer does NOT mean 20 % of volume is printed —
walls, top/bottom solid layers, and shells are 100 % solid and typically
dominate. Empirically ~50 % of model volume is printed for typical FDM
parts (validated against Bambu Lab Studio output).
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):
Average material throughput in g/h. ~32 g/h matches Bambu Lab printers
at normal quality; slower printers (~20 g/h) can be set here.
~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))
material_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)
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)
material_ccm = volume_ccm * material_factor
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)
@@ -45,14 +63,14 @@ def estimate(volume_ccm: float, material: str | None) -> dict:
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": material_factor,
"print_speed": print_speed,
"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,
}