public int[,] UCPSearch( MGInput m, float[] p_target, Action <float> progressCallback) { // Exhaustive Search is very expensive so we use a coarser step. // But we'll need to translate from the coarse step to the // orginal step so we'll store the translation coefficient // in stepSize. int stepCount = options.stepCount; int stepSize = m.tCount / stepCount; int taskCount = options.taskCount; // Get the total number of possible combinations. long nCombinations = (long)1 << (m.genCount * stepCount); // Out fitness variable. float lowestCost = Mathf.Infinity; // Pre-calculate fuel costs at max power because we'll use them // later to calculate a presumptive total cost. float[] cost_thr_at_max = ThermalGenerator.GetCostAtMaxPower( m.genCount, m.p_thr_max, m.thr_c_a, m.thr_c_b, m.thr_c_c, 60.0f); // We only need to verify min uptime/downtime constraints if our // step size is smaller than the the biggest min u/dtime. Otherwise // our step is large enough that the algorithm will never be in a // situation where it's trying to switch a generator that can't be // switched due to min downtime/uptime constraints. bool[] isValidationNeeded = IsMinTimeVerificationNeeded( m.genCount, stepSize, m.thr_min_dtime, m.thr_min_utime); // 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] = Tuple.Create( iStep * stepSize, (iStep + 1) * stepSize - 1); } // 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. Dictionary <StateStep, float> purchaseCostDict = ConstructCostDictionary(m, stepCount, stepSize, 400, p_target); // Running the search in threads makes pruning high cost // combinations happen earlier and more often because a low cost // variant is discovered earlier on. long bestComb = -1; var tasks = new Task[taskCount]; long combSegmentSize = nCombinations / taskCount; var taskParams = new TaskParams( p_target, stepCount, stepSize, stepInterval, isValidationNeeded, cost_thr_at_max, purchaseCostDict); var taskProgress = new float[taskCount]; void progressCallbackInner(int id, float completion) { taskProgress[id] = completion; float totalProgress = 0; for (int i = 0; i < taskProgress.Length; i++) { totalProgress += taskProgress[i]; } progressCallback(totalProgress / taskCount); } for (int iTask = 0; iTask < taskCount; iTask++) { taskProgress[iTask] = 0.0f; long searchStart = iTask * combSegmentSize; long searchEnd = (iTask + 1) * combSegmentSize; int taskID = iTask; var task = Task.Run(() => SearchTask( m, searchStart, searchEnd, ref bestComb, ref lowestCost, taskParams, progressCallbackInner, taskID)); tasks[iTask] = task; } var waiter = Task.WhenAll(tasks.ToArray()); waiter.Wait(); // Write out the step based state array int[,] u_thr_best_step = new int[m.genCount, m.tCount]; for (int iStep = 0; iStep < stepCount; iStep++) { for (int iGen = m.genCount - 1; iGen >= 0; iGen--) { u_thr_best_step[iGen, iStep] = (int)(bestComb % 2); bestComb /= 2; } } // Translate step-based state array to original time based // state array. int[,] u_thr_best = new int[m.genCount, m.tCount]; for (int t = 0; t < m.tCount; t++) { int currStep = t / stepSize; for (int iGen = 0; iGen < m.genCount; iGen++) { u_thr_best[iGen, t] = u_thr_best_step[iGen, currStep]; } } return(u_thr_best); }
/// <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); }