/// <summary> /// Precomputes the cost of each possible set of generator states at /// every time step. /// </summary> /// <param name="m">Microgrid input data.</param> /// <param name="genCount">Number of generators.</param> /// <param name="stepCount">The number of time steps.</param> /// <param name="stepSize">The size of each time step.</param> /// <param name="p_target">The amount of power the system needs to supply at a given time step.</param> /// <returns>A dictionary mapping a generator state and time step pair /// to the cost of purchasing the amount of power necessary to fulfill /// remaining demand for that specific pair.</returns> public static Dictionary <StateStep, float> ConstructCostDictionary( MGInput m, int stepCount, int stepSize, float penalty, float[] p_target) { // It's expensive to compute the cost of the amount of power the // microgrid needs to purchase inside the main loop. There are only // 2^n * stepCount combinations possible which end up being // redundantly recalculated a lot if done in the main loop. // So we compute all of them ahead of time and store them in a // dictionary. // t1 and t2 define the interval of time this step occupies. var stepInterval = new Tuple <int, int> [stepCount]; for (int iStep = 0; iStep < stepCount; iStep++) { stepInterval[iStep] = new Tuple <int, int>( iStep * stepSize, (iStep + 1) * stepSize - 1); } int nSqr = 1 << m.genCount; //Bitwise squaring. int nPurchaseCombs = nSqr * stepCount; var purchaseCostDict = new Dictionary <StateStep, float>(nPurchaseCombs); var indexToCost = new Dictionary <int, float>(m.genCount); float[] p_thr = new float[m.genCount]; for (int i = 0; i < m.genCount; i++) { // EUR for 1 hour of functioning float cost = ThermalGenerator.GetCost( m.p_thr_max[i], m.thr_c_a[i], m.thr_c_b[i], m.thr_c_c[i]); // EUR/MWh float alpha = cost / m.p_thr_max[i]; indexToCost.Add(i, alpha); } float[] e_thr_remaining = new float[stepSize]; float[] p_remaining = new float[stepSize]; var orderedByCost = indexToCost.OrderBy(x => x.Value).ToDictionary(x => x.Key, y => y.Value); for (int iState = 0; iState < nSqr; iState++) { for (int iStep = 0; iStep < stepCount; iStep++) { float c_total = 0.0f; int t1 = stepInterval[iStep].Item1; int t2 = stepInterval[iStep].Item2; for (int t = t1; t < t2; t++) { e_thr_remaining[t - t1] = p_target[t] / 60.0f; p_remaining[t - t1] = p_target[t]; foreach (KeyValuePair <int, float> kvp in orderedByCost) { int iGen = kvp.Key; float alpha = kvp.Value; var bitValue = iState & (1 << m.genCount - iGen - 1); var state = ((bitValue | (~bitValue + 1)) >> 31) & 1; if (state == 1) { if (alpha < m.price[t]) { if (m.canSell) { // Since we can sell and our generator // makes cheaper energy than the market price, // just produce at max power. p_thr[iGen] = m.p_thr_max[iGen]; } else { // Since we can't sell but our generator // makes cheaper energy than market price, // we'll produce enough to cover local demand. p_thr[iGen] = Mathf.Clamp(m.p_thr_max[iGen], 0.0f, p_remaining[t - t1]); } } else { if (m.canBuy) { // Since we can buy and our generator // makes more expensive energy than // the market price, produce no power // and buy. p_thr[iGen] = 0.0f; } else { // Since we can't buy, produce enough // power to cover local demand, regardless // of market price. p_thr[iGen] = Mathf.Clamp(m.p_thr_max[iGen], 0.0f, p_remaining[t - t1]); } } c_total += ThermalGenerator.GetCost( p_thr[iGen], m.thr_c_a[iGen], m.thr_c_b[iGen], m.thr_c_c[iGen]) / 60.0f; p_remaining[t - t1] -= p_thr[iGen]; e_thr_remaining[t - t1] -= p_thr[iGen] / 60.0f; } else { p_thr[iGen] = 0.0f; } } if (e_thr_remaining[t - t1] < 0.0f) { if (m.canSell) { c_total += e_thr_remaining[t - t1] * m.price[t]; } } else { if (m.canBuy) { c_total += e_thr_remaining[t - t1] * m.price[t]; } else { c_total += e_thr_remaining[t - t1] * penalty; } } } var id = new StateStep(iState, iStep); #if UNITY_EDITOR || DEVELOPMENT_BUILD if (purchaseCostDict.ContainsKey(id) && purchaseCostDict[id] != c_total) { throw new InvalidProgramException( $@"IDs and purchase costs need to have a 1 to 1 mapping. There can't be an ID with a different purchase cost. If that happens, the code is wrong somewhere. UID: {id}. Existing Value: {purchaseCostDict[id]}. New Value: {c_total}."); } #endif purchaseCostDict[id] = c_total; } } return(purchaseCostDict); }