private static IAddInjectionCost <T> AddInjectWithdrawRanges <T>( IAddInjectWithdrawConstraints <T> builder, IEnumerable <InjectWithdrawRangeByInventoryAndPeriod <T> > injectWithdrawRanges, Func <InjectWithdrawRangeByInventory[], IInjectWithdrawConstraint> constraintFactory) where T : ITimePeriod <T> { var injectWithdrawSortedList = new SortedList <T, IInjectWithdrawConstraint>(); var inventoryRangeList = new List <InventoryRange>(); foreach ((T period, IEnumerable <InjectWithdrawRangeByInventory> injectWithdrawRange) in injectWithdrawRanges) { if (period == null) { throw new ArgumentException("Null Period in collection.", nameof(injectWithdrawRanges)); } if (injectWithdrawRange == null) { throw new ArgumentException("Null InjectWithdrawRanges in collection.", nameof(injectWithdrawRange)); } InjectWithdrawRangeByInventory[] injectWithdrawRangeArray = injectWithdrawRange.ToArray(); if (injectWithdrawRangeArray.Length < 2) { throw new ArgumentException($"Period {period} contains less than 2 inject/withdraw/inventory constraints.", nameof(injectWithdrawRanges)); } IInjectWithdrawConstraint constraint; if (injectWithdrawRangeArray.Length == 2 && injectWithdrawRangeArray[0].InjectWithdrawRange.MinInjectWithdrawRate.AlmostEqual( injectWithdrawRangeArray[1].InjectWithdrawRange.MinInjectWithdrawRate, double.Epsilon) && injectWithdrawRangeArray[0].InjectWithdrawRange.MaxInjectWithdrawRate.AlmostEqual( injectWithdrawRangeArray[1].InjectWithdrawRange.MaxInjectWithdrawRate, double.Epsilon)) { // Two rows which represent constant inject/withdraw constraints over all inventories constraint = new ConstantInjectWithdrawConstraint(injectWithdrawRangeArray[0].InjectWithdrawRange); } else { constraint = constraintFactory(injectWithdrawRangeArray); } double minInventory = injectWithdrawRangeArray.Min(inventoryRange => inventoryRange.Inventory); double maxInventory = injectWithdrawRangeArray.Max(inventoryRange => inventoryRange.Inventory); try { injectWithdrawSortedList.Add(period, constraint); inventoryRangeList.Add(new InventoryRange(minInventory, maxInventory)); } catch (ArgumentException) // TODO unit test { throw new ArgumentException("Repeated periods found in inject/withdraw ranges.", nameof(injectWithdrawRanges)); } } if (injectWithdrawSortedList.Count == 0) { throw new ArgumentException("No inject/withdraw constrains provided.", nameof(injectWithdrawRanges)); } // TODO create helper method (in Cmdty.TimeSeries) to create TimeSeries from piecewise data? T firstPeriod = injectWithdrawSortedList.Keys[0]; T lastPeriod = injectWithdrawSortedList.Keys[injectWithdrawSortedList.Count - 1]; int numPeriods = lastPeriod.OffsetFrom(firstPeriod) + 1; var timeSeriesInjectWithdrawValues = new IInjectWithdrawConstraint[numPeriods]; var timeSeriesInventoryRangeValues = new InventoryRange[numPeriods]; T periodLoop = firstPeriod; IInjectWithdrawConstraint constraintLoop = injectWithdrawSortedList.Values[0]; InventoryRange inventoryRangeLoop = inventoryRangeList[0]; int arrayCounter = 0; int sortedListCounter = 0; do { if (periodLoop.Equals(injectWithdrawSortedList.Keys[sortedListCounter])) { constraintLoop = injectWithdrawSortedList.Values[sortedListCounter]; inventoryRangeLoop = inventoryRangeList[sortedListCounter]; sortedListCounter++; } timeSeriesInjectWithdrawValues[arrayCounter] = constraintLoop; timeSeriesInventoryRangeValues[arrayCounter] = inventoryRangeLoop; periodLoop = periodLoop.Offset(1); arrayCounter++; } while (periodLoop.CompareTo(lastPeriod) <= 0); var injectWithdrawTimeSeries = new TimeSeries <T, IInjectWithdrawConstraint>(firstPeriod, timeSeriesInjectWithdrawValues); var inventoryRangeTimeSeries = new TimeSeries <T, InventoryRange>(firstPeriod, timeSeriesInventoryRangeValues); IInjectWithdrawConstraint GetInjectWithdrawConstraint(T period) { if (period.CompareTo(injectWithdrawTimeSeries.End) > 0) { return(injectWithdrawTimeSeries[injectWithdrawTimeSeries.End]); } return(injectWithdrawTimeSeries[period]); } IAddMinInventory <T> addMinInventory = builder.WithInjectWithdrawConstraint(GetInjectWithdrawConstraint); double GetMinInventory(T period) { if (period.CompareTo(inventoryRangeTimeSeries.End) > 0) { return(inventoryRangeTimeSeries[inventoryRangeTimeSeries.End].MinInventory); } return(inventoryRangeTimeSeries[period].MinInventory); } IAddMaxInventory <T> addMaxInventory = addMinInventory.WithMinInventory(GetMinInventory); double GetMaxInventory(T period) { if (period.CompareTo(inventoryRangeTimeSeries.End) > 0) { return(inventoryRangeTimeSeries[inventoryRangeTimeSeries.End].MaxInventory); } return(inventoryRangeTimeSeries[period].MaxInventory); } IAddInjectionCost <T> addInjectionCost = addMaxInventory.WithMaxInventory(GetMaxInventory); return(addInjectionCost); }
public static TimeSeries <T, InventoryRange> CalculateInventorySpace <T>(ICmdtyStorage <T> storage, double startingInventory, T currentPeriod) where T : ITimePeriod <T> { if (currentPeriod.CompareTo(storage.EndPeriod) > 0) // TODO should condition be >= 0? { throw new ArgumentException("Storage has expired"); // TODO change to return empty TimeSeries? } T startActiveStorage = storage.StartPeriod.CompareTo(currentPeriod) > 0 ? storage.StartPeriod : currentPeriod; int numPeriods = storage.EndPeriod.OffsetFrom(startActiveStorage); // Calculate the inventory space range going forward var forwardCalcMaxInventory = new double[numPeriods]; var forwardCalcMinInventory = new double[numPeriods]; double minInventoryForwardCalc = startingInventory; double maxInventoryForwardCalc = startingInventory; for (int i = 0; i < numPeriods; i++) { T periodLoop = startActiveStorage.Offset(i); T nextPeriod = periodLoop.Offset(1); double inventoryPercentLoss = storage.CmdtyInventoryPercentLoss(periodLoop); double injectWithdrawMin = storage.GetInjectWithdrawRange(periodLoop, minInventoryForwardCalc).MinInjectWithdrawRate; double inventoryLossAtMin = inventoryPercentLoss * minInventoryForwardCalc; double storageMin = storage.MinInventory(nextPeriod); minInventoryForwardCalc = Math.Max(minInventoryForwardCalc - inventoryLossAtMin + injectWithdrawMin, storageMin); forwardCalcMinInventory[i] = minInventoryForwardCalc; double injectWithdrawMax = storage.GetInjectWithdrawRange(periodLoop, maxInventoryForwardCalc).MaxInjectWithdrawRate; double inventoryLossAtMax = inventoryPercentLoss * maxInventoryForwardCalc; double storageMax = storage.MaxInventory(nextPeriod); maxInventoryForwardCalc = Math.Min(maxInventoryForwardCalc - inventoryLossAtMax + injectWithdrawMax, storageMax); forwardCalcMaxInventory[i] = maxInventoryForwardCalc; } // Calculate the inventory space range going backwards var backwardCalcMaxInventory = new double[numPeriods]; var backwardCalcMinInventory = new double[numPeriods]; T periodBackLoop = storage.EndPeriod; backwardCalcMaxInventory[numPeriods - 1] = storage.MustBeEmptyAtEnd ? 0 : storage.MaxInventory(storage.EndPeriod); backwardCalcMinInventory[numPeriods - 1] = storage.MustBeEmptyAtEnd ? 0 : storage.MinInventory(storage.EndPeriod); for (int i = numPeriods - 2; i >= 0; i--) { periodBackLoop = periodBackLoop.Offset(-1); backwardCalcMaxInventory[i] = storage.InventorySpaceUpperBound(periodBackLoop, backwardCalcMinInventory[i + 1], backwardCalcMaxInventory[i + 1]); backwardCalcMinInventory[i] = storage.InventorySpaceLowerBound(periodBackLoop, backwardCalcMinInventory[i + 1], backwardCalcMaxInventory[i + 1]); } // Calculate overall inventory space and check for consistency var inventoryRanges = new InventoryRange[numPeriods]; for (int i = 0; i < numPeriods; i++) { double inventorySpaceMax = Math.Min(forwardCalcMaxInventory[i], backwardCalcMaxInventory[i]); double inventorySpaceMin = Math.Max(forwardCalcMinInventory[i], backwardCalcMinInventory[i]); if (inventorySpaceMin > inventorySpaceMax) { throw new InventoryConstraintsCannotBeFulfilledException(); } inventoryRanges[i] = new InventoryRange(inventorySpaceMin, inventorySpaceMax); } return(new TimeSeries <T, InventoryRange>(startActiveStorage.Offset(1), inventoryRanges)); }