import math from typing import Optional from .inventory_safety_stock_data import Z_SCORES from .inventory_safety_stock_types import SafetyStockInput def _amount(name: str, value: Optional[float], method: str) -> float: if value is None: raise ValueError("%s needs %s" % (method, name)) if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0: raise ValueError("%s must be a finite number, not negative, received %r" % (name, value)) return float(value) def _whole_units_up(x: float) -> int: # Six decimal places, half away from zero, then up to whole units: float # noise such as 55.00000000000001 must not cost a whole extra unit. y = x * 1e6 micro = math.floor(y) if y - micro >= 0.5: micro += 1 return (micro + 999999) // 1000000 def safety_stock(input: SafetyStockInput) -> int: """Safety stock in whole units, by the service-level or max-minus-average method.""" lead_time = _amount("leadTime", input.lead_time, input.method) if input.method == "service-level": level = input.service_level_basis_points if level is None: raise ValueError("service-level needs serviceLevelBasisPoints") row = next((r for r in Z_SCORES if r.service_level_basis_points == level), None) if row is None: raise ValueError( "no z-score for a service level of %s basis points: use one of %s" % (level, ", ".join(str(r.service_level_basis_points) for r in Z_SCORES)) ) sd = _amount("demandStdDev", input.demand_std_dev, input.method) variance = lead_time * sd * sd sl = _amount("leadTimeStdDev", 0.0 if input.lead_time_std_dev is None else input.lead_time_std_dev, input.method) if sl > 0: d = _amount("averageDemand", input.average_demand, "service-level with a varying lead time") variance = variance + d * d * sl * sl return _whole_units_up((row.z_ten_thousandths / 10000) * math.sqrt(variance)) if input.method == "max-minus-average": max_demand = _amount("maxDemand", input.max_demand, input.method) max_lead_time = _amount("maxLeadTime", input.max_lead_time, input.method) average_demand = _amount("averageDemand", input.average_demand, input.method) worst = max_demand * max_lead_time usual = average_demand * lead_time if worst < usual: raise ValueError( "maxDemand x maxLeadTime (%s) must not be less than averageDemand x leadTime (%s)" % (worst, usual) ) return _whole_units_up(worst - usual) raise ValueError('unknown safety stock method "%s"' % (input.method,))