private static IntrinsicStorageValuationResults <T> Calculate(T currentPeriod, double startingInventory,
                                                                      TimeSeries <T, double> forwardCurve, ICmdtyStorage <T> storage, Func <T, Day> settleDateRule,
                                                                      Func <Day, Day, double> discountFactors, Func <ICmdtyStorage <T>, IDoubleStateSpaceGridCalc> gridCalcFactory,
                                                                      IInterpolatorFactory interpolatorFactory, double numericalTolerance)
        {
            if (startingInventory < 0)
            {
                throw new ArgumentException("Inventory cannot be negative.", nameof(startingInventory));
            }

            if (currentPeriod.CompareTo(storage.EndPeriod) > 0)
            {
                return(new IntrinsicStorageValuationResults <T>(0.0, TimeSeries <T, StorageProfile> .Empty));
            }

            if (currentPeriod.Equals(storage.EndPeriod))
            {
                if (storage.MustBeEmptyAtEnd)
                {
                    if (startingInventory > 0) // TODO allow some tolerance for floating point numerical error?
                    {
                        throw new InventoryConstraintsCannotBeFulfilledException("Storage must be empty at end, but inventory is greater than zero.");
                    }
                    return(new IntrinsicStorageValuationResults <T>(0.0, TimeSeries <T, StorageProfile> .Empty));
                }

                double terminalMinInventory = storage.MinInventory(storage.EndPeriod);
                double terminalMaxInventory = storage.MaxInventory(storage.EndPeriod);

                if (startingInventory < terminalMinInventory)
                {
                    throw new InventoryConstraintsCannotBeFulfilledException("Current inventory is lower than the minimum allowed in the end period.");
                }

                if (startingInventory > terminalMaxInventory)
                {
                    throw new InventoryConstraintsCannotBeFulfilledException("Current inventory is greater than the maximum allowed in the end period.");
                }

                double cmdtyPrice = forwardCurve[storage.EndPeriod];
                double npv        = storage.TerminalStorageNpv(cmdtyPrice, startingInventory);
                return(new IntrinsicStorageValuationResults <T>(npv, TimeSeries <T, StorageProfile> .Empty));
            }

            TimeSeries <T, InventoryRange> inventorySpace = StorageHelper.CalculateInventorySpace(storage, startingInventory, currentPeriod);

            // TODO think of method to put in TimeSeries class to perform the validation check below in one line
            if (forwardCurve.IsEmpty)
            {
                throw new ArgumentException("Forward curve cannot be empty.", nameof(forwardCurve));
            }

            if (forwardCurve.Start.CompareTo(inventorySpace.Start) > 0)
            {
                throw new ArgumentException("Forward curve starts too late.", nameof(forwardCurve));
            }

            if (forwardCurve.End.CompareTo(inventorySpace.End) < 0)
            {
                throw new ArgumentException("Forward curve does not extend until storage end period.", nameof(forwardCurve));
            }

            // Calculate discount factor function
            Day dayToDiscountTo = currentPeriod.First <Day>(); // TODO IMPORTANT, this needs to change

            // Memoize the discount factor
            var discountFactorCache = new Dictionary <Day, double>(); // TODO do this in more elegant way and share with Tree calc

            double DiscountToCurrentDay(Day cashFlowDate)
            {
                if (!discountFactorCache.TryGetValue(cashFlowDate, out double discountFactor))
                {
                    discountFactor = discountFactors(dayToDiscountTo, cashFlowDate);
                    discountFactorCache[cashFlowDate] = discountFactor;
                }
                return(discountFactor);
            }

            // Perform backward induction
            var storageValueByInventory = new Func <double, double> [inventorySpace.Count];

            double cmdtyPriceAtEnd = forwardCurve[storage.EndPeriod];

            storageValueByInventory[inventorySpace.Count - 1] =
                finalInventory => storage.TerminalStorageNpv(cmdtyPriceAtEnd, finalInventory);

            int backCounter = inventorySpace.Count - 2;
            IDoubleStateSpaceGridCalc gridCalc = gridCalcFactory(storage);

            foreach (T periodLoop in inventorySpace.Indices.Reverse().Skip(1))
            {
                (double inventorySpaceMin, double inventorySpaceMax) = inventorySpace[periodLoop];
                double[] inventorySpaceGrid = gridCalc.GetGridPoints(inventorySpaceMin, inventorySpaceMax)
                                              .ToArray();
                var storageValuesGrid = new double[inventorySpaceGrid.Length];

                double cmdtyPrice = forwardCurve[periodLoop];
                Func <double, double> continuationValueByInventory = storageValueByInventory[backCounter + 1];

                Day    cmdtySettlementDate = settleDateRule(periodLoop);
                double discountFactorFromCmdtySettlement = DiscountToCurrentDay(cmdtySettlementDate);

                (double nextStepInventorySpaceMin, double nextStepInventorySpaceMax) = inventorySpace[periodLoop.Offset(1)];
                for (int i = 0; i < inventorySpaceGrid.Length; i++)
                {
                    double inventory = inventorySpaceGrid[i];
                    storageValuesGrid[i] = OptimalDecisionAndValue(storage, periodLoop, inventory, nextStepInventorySpaceMin,
                                                                   nextStepInventorySpaceMax, cmdtyPrice, continuationValueByInventory,
                                                                   discountFactorFromCmdtySettlement, DiscountToCurrentDay, numericalTolerance).StorageNpv;
                }

                storageValueByInventory[backCounter] =
                    interpolatorFactory.CreateInterpolator(inventorySpaceGrid, storageValuesGrid);
                backCounter--;
            }

            // Loop forward from start inventory choosing optimal decisions
            int numStorageProfiles = inventorySpace.Count + 1;
            var storageProfiles    = new StorageProfile[numStorageProfiles];
            var periods            = new T[numStorageProfiles];

            double inventoryLoop      = startingInventory;
            T      startActiveStorage = inventorySpace.Start.Offset(-1);

            for (int i = 0; i < numStorageProfiles; i++)
            {
                T              periodLoop = startActiveStorage.Offset(i);
                double         spotPrice  = forwardCurve[periodLoop];
                StorageProfile storageProfile;
                if (periodLoop.Equals(storage.EndPeriod))
                {
                    double endPeriodNpv = storage.MustBeEmptyAtEnd ? 0.0 : storage.TerminalStorageNpv(spotPrice, inventoryLoop);
                    storageProfile = new StorageProfile(inventoryLoop, 0.0, 0.0, 0.0, 0.0, endPeriodNpv);
                }
                else
                {
                    Day    cmdtySettlementDate = settleDateRule(periodLoop);
                    double discountFactorFromCmdtySettlement = DiscountToCurrentDay(cmdtySettlementDate);

                    Func <double, double> continuationValueByInventory = storageValueByInventory[i];
                    (double nextStepInventorySpaceMin, double nextStepInventorySpaceMax) = inventorySpace[periodLoop.Offset(1)];
                    (double _, double optimalInjectWithdraw, double cmdtyConsumedOnAction, double inventoryLoss, double optimalPeriodPv) =
                        OptimalDecisionAndValue(storage, periodLoop, inventoryLoop, nextStepInventorySpaceMin,
                                                nextStepInventorySpaceMax, spotPrice, continuationValueByInventory, discountFactorFromCmdtySettlement,
                                                DiscountToCurrentDay, numericalTolerance);

                    inventoryLoop += optimalInjectWithdraw - inventoryLoss;

                    double netVolume = -optimalInjectWithdraw - cmdtyConsumedOnAction;
                    storageProfile = new StorageProfile(inventoryLoop, optimalInjectWithdraw, cmdtyConsumedOnAction, inventoryLoss, netVolume, optimalPeriodPv);
                }
                storageProfiles[i] = storageProfile;
                periods[i]         = periodLoop;
            }

            double storageNpv = storageProfiles.Sum(profile => profile.PeriodPv);

            return(new IntrinsicStorageValuationResults <T>(storageNpv, new TimeSeries <T, StorageProfile>(periods, storageProfiles)));
        }