from typing import Dict, List, Sequence from .manufacturing_capacity_types import CapacityLoad, RoutedLoad, WorkCentre from .math_rational import compare_rational, divide_rational, multiply_rational, rational, rational_to_integer MAX_SAFE = 2**53 - 1 def _is_whole(value: int) -> bool: return not isinstance(value, bool) and isinstance(value, int) and -MAX_SAFE <= value <= MAX_SAFE def _minutes(what: str, value: int) -> None: if not _is_whole(value) or value < 0: raise ValueError("%s must be a whole number of minutes, not negative, received %r" % (what, value)) def work_centre_load(work_centres: Sequence[WorkCentre], loads: Sequence[RoutedLoad]) -> List[CapacityLoad]: """Capacity, load and overload per work centre. Capacity is kept as an exact fraction, so a load a fraction of a minute over it is still an overload. """ totals: Dict[str, int] = {} for wc in work_centres: if wc.id in totals: raise ValueError('duplicate work centre "%s"' % (wc.id,)) _minutes('availableMinutes of "%s"' % (wc.id,), wc.available_minutes) if not _is_whole(wc.efficiency_basis_points) or wc.efficiency_basis_points < 0: raise ValueError( 'efficiencyBasisPoints of "%s" must be a whole number, not negative, received %r' % (wc.id, wc.efficiency_basis_points) ) if not _is_whole(wc.utilisation_basis_points) or not 0 <= wc.utilisation_basis_points <= 10000: raise ValueError( 'utilisationBasisPoints of "%s" must be a whole number from 0 to 10000, received %r' % (wc.id, wc.utilisation_basis_points) ) totals[wc.id] = 0 for load in loads: if load.work_centre not in totals: raise ValueError('a load names work centre "%s", which is not in the list' % (load.work_centre,)) _minutes('a load on "%s"' % (load.work_centre,), load.minutes) total = totals[load.work_centre] + load.minutes if total > MAX_SAFE: raise ValueError('the load on "%s" exceeds 2^53 - 1 minutes' % (load.work_centre,)) totals[load.work_centre] = total result: List[CapacityLoad] = [] for wc in work_centres: capacity = multiply_rational( rational(wc.available_minutes, 1), multiply_rational(rational(wc.efficiency_basis_points, 10000), rational(wc.utilisation_basis_points, 10000)), ) load_minutes = totals[wc.id] load = rational(load_minutes, 1) capacity_minutes = rational_to_integer(capacity, "down") result.append( CapacityLoad( work_centre=wc.id, capacity_minutes=capacity_minutes, load_minutes=load_minutes, spare_minutes=capacity_minutes - load_minutes, load_basis_points=None if capacity.numerator == 0 else rational_to_integer(multiply_rational(divide_rational(load, capacity), rational(10000, 1)), "half-up"), overloaded=compare_rational(load, capacity) > 0, ) ) return result