Retired as of 4.1Appendix IIa:Priority SimulationsThe (now deprecated) rotation simulation code is available on the
matlabadin project page, as are the
priority models used.
The simulation works by simulating a limited combat environment. It keeps track of ability cooldowns, holy power, and the GCD, iterating in user-definable time steps. At each time step, it checks the priority queue in order and casts the first thing for which the conditionals are true.
While it's capable of incorporating haste effects (by working in time steps of 0.1s or less), that leads to some weird situations. For example, since Holy Wrath is a spell it has a shorter GCD than a melee attack like Crusader Strike. Thus, you can end up in a situation that looks like this:
CS-HW-????
If your Holy Wrath GCD is shorter than 1.5 seconds by at least one full timestep, CS still won't be off of cooldown. However, if something else is (Judgement perhaps, or Consecration) it'll try and cast that instead. That pushes CS back by almost a full GCD, inevitably causing a massive drop in DPS.
Because of this, I've simulated in time steps of 0.5 seconds, which is essentially throwing out haste as far as our spellcast choices are concerned. Since we are unlikely to want to deviate from casting CS on every alternate cooldown, this shouldn't be an issue.
If you're looking at old queues, there are some non-standard abbreviations and things to note:
- SD stands for Sacred Duty, and means that in a case where you have 3 Holy Power, no Sacred Duty buff, and Judgement is off of cooldown, you prioritize Judgement (instead of just casting SotR) to "fish" for a Sacred Duty proc.
- J2 stands for Judgement in the 2nd "filler" slot before a finisher. It's probably irrelevant, but it was something I was toying with as a way to eke out more SD/SotR damage. It's only a weak theoretical increase though.
- SotR# stands for an #-point SotR. If the number is omitted, it's assumed to be a 3-pointer.
- SDSotR# stands for an #-point SotR if and only if Sacred Duty is up. If # is omitted, it's assumed to be 3.
- Inq is coded in such a way that it will only refresh if the duration is <1 second. This means that Inq overlap time is essentially 0, because this condition forces a refresh at 8 GCDs instead of 6 GCDs. Just like SotR#, Inq# would stand for an #-point Inq.
- An asterisk after any damage-dealing ability (i.e. SotR*) indicates the conditional "if and only if Inquisition is Active."
- Inq* is the exception to its own rule. Inq* stands for forcing an Inq cast at 3 HP, regardless of remaining duration. For casts at lower HoPo, I'll use Inq#* just like SotR.
- CS+ means "CS if HoPo<3," and is a way to put CS at the top of the queue without allowing the sim to be stupid.
The simulation is run for 30k GCDs, or roughly 12 and a half
hours of combat. That's a long time, but random procs are random, and it's still entirely possible to get variations of 30-50 DPS or more from sim to sim. That's around 0.4% or less of variation, but I'd say that a more reasonable threshold for significance is around 0.5% (or around 75 DPS). In other words, if the difference between two sims is less than around 75 DPS, they're roughly the same as far as we're concerned.
I've also included some information that helps us interpret the simulations. "Empty" is the number of empty GCDs, and "E%" is simply the percentage of empties (Empty/30k). "SotR miss" is the number of missed SotRs in the parse, while "AS cast" is the number of Avenger's Shields cast. SotR miss and AS cast give you some idea whether those factors were a significant source of variation in a given sim. If rotation A had 1500 SotR misses, and rotation B had only 1000, we might expect B to perform better even if A is better (or equal) on paper. Similarly, if B had many more AS casts, it might've had more favorable luck with Grand Crusader procs, which could give it an "unfair" advantage.
Appendix IIb: Numerical Rotation ModelThe
old analytical model we've been using for 4.0.3a no longer works for the new Holy Power generation mechanics, so I've archived it in the appendix. Before proceeding with the new method, I want to briefly discuss the reasons we had to abandon the analytical approach.
Out with the old...The primary issue is that there are factors that affect the cast sequences and damage values in the rotation. The primary one is hit and expertise, but talents like Sacred Duty, Grand Crusader, and Eternal Glory also cause problems. With guaranteed Holy Power generation, we were able to ignore a lot of these effects, and simply wrap them into the average damage done by each ability. We then needed small correction terms for the talented effects as described in the Appendix. We also needed to correct for the one hit/exp effect that did affect the cast sequence (SotR misses), which is also described in the Appendix.
However, the change to Holy Power generation in 4.0.6 threw a wrench into this. Now the result of a CS attack
does affect the rest of the rotation in a very significant and hard to model way. It may delay SotR, which then causes cooldown clashes with Judgement or AS. That means that the cast probabilities of J and AS change, which then collaterally affects the other fillers. It also makes all of those probabilities harder to calculate, because the availability of J and AS suddenly depends on what happened in the previous cycle. This makes everything much more sensitively dependent on hit and expertise, because you're not just missing abilities, you're also changing the entire cast composition of the rotation.
I tried to work out the analytical problem for a single "block" of CS-X- to determine the probability of each filler spell being in slot X at an arbitrary spot in the rotation, taking into account all of these effects. In the end though, I failed - the problem became too complex too quickly, and none of my attempts gave me anything that I felt was consistent enough to be an accurate model.
One alternative concept that we considered (and tlitp worked very hard on, at least as hard as I did on the analytical approach) was breaking things down into independent "macroblocks" that represent the casts between one SotR and the next. In other words, if you start with 0 Holy Power and succeed on all three CS casts, your macroblock would be CS-X-CS-X-CS-SotR. If we represent that in a shorthand that just represents CS successes as 1 and CS failures as 0, that macroblock is 111. The macroblocks with 1 unsuccessful CS cast and 3 successful ones are 1011, 1101, and 0111.
This method has some advantages - for one thing, we can explicitly and easily calculate the probability of each CS macroblock. The fillers are still difficult though, but we may have been able to be calculated recursively. However, the big disadvantage is that, if we limit ourselves to considering macroblocks of length 7 or less (since we would want to refresh Holy Shield by that point), there are
63 different macroblocks for CS alone, all of which needed to be coded by hand. And we still didn't have a reliable way to handle fillers!
I even suggested a method where we generate a string of N random numbers to represent the CS casts, which would then quickly be broken down into a string of macroblocks. We'd then fill in the empty spots with SotR, then J, then AS. This might be faster than the priority sim code, but is fundamentally no different, and has most of the same disadvantages.
Despite all the hard work, none of these seemed to be reasonable approaches. I punted on the analytical approach, and after further scrutiny it made little sense to go with the macroblock approach, which was half-numerical and half-analytical. If we're going to go numerical, why go half-hog and make the entire thing very rigid and complicated? Why not go whole-hog numerical, and take advantage of all the work we had put into the priority simulations? So that's exactly what we did.
... and in with the newWe could continue with a brute-force method, namely to run the priority simulation code every time we wanted to calculate DPS. This has several disadvantages, the biggest being that it's very slow. Running the simulation long enough to get ~1% or better accuracy in the results takes time, and doing that many times for a single calculation ends up being prohibitive. In addition, we'll still have variations on the order of ~1% that make exact values hard to come by, since 1% is around 100 DPS or more. In many cases, this would mean that the noise threshold is larger than the results of our calculations.
Instead, we've taken a slightly more intelligent approach. Rather than run the simulation code every time we want a value, we run the code a number of times under certain configurations and generate curve fits for the results. We can store the coefficients of these curve fits in a database, and then when we want the results of a calculation we can access the appropriate portion of the database to extract the fit coefficients. From that data, we can accurately reproduce the results of any given simulation in under a second.
This has a lot of pros and very few cons.
- The biggest pro is speed - accessing the database and generating results is as quick or quicker than the analytical model was. It takes several hours (up to 6 or so on my i7-2600K running at 4GHz) to generate the database, which is a con, but we only have to do that once, or at least once every time we make significant changes, and it's all done automatically in the code so it can be performed unattended.
- Another pro is consistency - with the numerical model, we're essentially evaluating a function F for certain inputs F(x,y,z). Since the function doesn't change, we will always get the same outputs for the same inputs. This means that we won't see 50-100 DPS variations between runs.
- We also get consistency in interpolation. For example, if we know that the function increases monotonically with x, then we know that if we feed the function F(x+a,y,z), it will always give a larger result than F(x,y,z). In the priority sim, variations could lead to one sim erroneously suggesting F(x+a,y,z) is lower than F(x,y,z).
- Generality - One problem with analytical models is that they tend to be very specific. This means that one little change (like, oh, say, only granting Holy Power on successful CS, or a chance to get Holy Power from AS) disrupts the entire model, and requires reformulation. Despite spending weeks working on a new analytical model for 4.0.6, we still couldn't iron out enough of the bugs to make it work right.
In contrast, this method is very general, and can accommodate anything the priority sim can handle (which is more or less everything). Blizz could completely overhaul the rotation next week and it wouldn't matter. We'd just have to make minor adjustments to the priority code, regenerate the database, and we'd be ready to go. From a time-saving point of view, I'd rather let the computer spend 6 hours overnight regenerating the database than spend 10-20 hours or more trying to model the new mechanics analytically.
- Since the priority code already handles Inquisition, seal procs, and talents like Grand Crusader or Sacred Duty properly, we no longer have to use adjustment factors to approximate their effects.
The code is more or less finished, and can be seen here:
rotation_db_gen.mrotation_db_data.mrotation_db_fit.mrotation_db.mand of course, the updated
rotation_model.m, which turns the database entries into CPS/DPS/HPS/TPS values.
Since I value transparency and community scrutiny in this code, I want to briefly describe the process flow so that people can check my work.
rotation_db_gen.m is the master script that creates the "rotation database." It does so by calling
rotation_db_data.m to generate data sets at different hit/exp values with the
prio_sim() function. It then calls
rotation_db_fit.m to take the output of the simulations and fits them with 5th-order polynomials (i.e. a*x^5+b*x^4+c*x^3+d*x^2+e*x+f), where x is your chance to hit with SotR (mdf.mehit in the code). It then stores the constants a-f and generates output code in a text file, which we copy/paste into
rotation_db.m. The output code is written such that when rotation_db is run, it creates structures containing the constants a-f.
There's an additional complication to all of this, which is that talents like Grand Crusader, Sacred Duty, and Eternal Glory have a significant effect. Thus, we can't just generate function of hit/expertise, we also need those functions to depend on these talents. In other words, y=F(x) won't cut it, we need y=F(x,GC,SD,EG). In practice, we do this by calculating F(x) for every ability and every possible configuration of GC, SD, and EG, so that we have a 5-dimensional matrix that gives us a-f for any choice of the four parameters (ability,GC,SD,EG).
To illustrate how this works, here's an example graph from a test run that shows how this works. This is a very short run for demonstration purposes (3 runs of N=100 for each hit value), so the accuracy is very low, but it does a good job of illustrating the idea. For the actual database, we run 30 runs of N=1000, or 30k GCDs
for each value of mdf.mehit.

The blue and red traces are data generated by prio_sim for the "coefficient weight" and "casts per second" of SotR, respectively. The black line with circles represents the fit to the data, which is what we extract the coefficients a-f from. The line at 0 is for inquisition uptime modeling - in this rotation, we don't use Inq at all, and the fit reflects that by giving us 0 for a-f.
There are a few subtleties left to discuss. GC, SD, and EG are
not the only three external factors that affect the rotation. An obvious one is the Consecration glyph, which would affect the availability of Cons as a filler. However, the variation due to that is very small, and not worth the additional computation time of extending the code to incorporate a fourth external parameter. I've run everything with the Cons glyph enabled just in case, but it's unlikely that disabling it would result in a significant DPS
increase. And in the current environment, we may not have mana to cast Consecration frequently enough for it to matter anyway.
A less obvious one is the double-latency penalty on CS. Since CS has a cooldown, you can't use the ability queue system to queue it up ahead of time. Thus, you're limited to mashing the button so that it fires as soon as the cooldown is up. What this means in practice is that the effective cooldown on CS is not 1.5s+latency like other spells, but 1.5s+2*latency. The code doesn't attempt to model this at all (and never did). If we decide to include it, the more convenient way to do so is to simply treat the GCD as 1.5+2*latency seconds rather than add complexity to the database generation.