Scaling issue with RES-type constraints in the transportation sector
I am using RES-type constraints to implement policies such as Renewable Portfolio Standards, Zero Emission Vehicle targets, and LED market penetration targets. These have worked very well for all applications except those in the transportation sector ... where their success has been sporadic. It has been difficult to track down the issue, but I believe I've done so.
The root cause seems to be that that the unit conversions required in GCAM's transportation sector can yield RES target values that are very small... e.g., on the order of 1e-6 to 1e-8. However, the "checks" that occur in preconditioner.cpp (in solve) and solver_library.cpp (in getRelativeED) compare the excess demand, abs(demand-supply), to a floor value that is only on the order of 1e-3 or 1e-4. If the excess demand is less than the floor (which it sometimes ... but not always... is in the transportation sector because of the unit conversions), the market is assumed to be solved.... and (I believe) the price is just set to 1 or 0. But, in many instances, the market isn't solved.
I've tried a number of solutions, with varying degrees of success.... Here's one that addresses the problem in many instances, but not all (see 1st comment for more info). This is a replacement for getRelativeED() in solution_info.cpp. This handles RES-type constraints differently, multiplying the solution floor by 0.01.
double SolutionInfo::getRelativeED() const { string name = this->getName(); double rtnVal = 0.0;
if (this->getTypeName() != "RES") {
rtnVal = SolverLibrary::getRelativeED(getED(), getDemand(), mSolutionFloor);
}
else {
double excess_demand = abs(getED());
double demand = abs(getDemand());
if (demand < util::getSmallNumber()) {
demand = util::getSmallNumber();
}
double floor = mSolutionFloor;
double relative_excess_demand = abs(excess_demand / demand);
double tempFloor = mSolutionFloor;
if (demand < mSolutionFloor) tempFloor = mSolutionFloor * 0.01;
if (abs(excess_demand) <= tempFloor) {
rtnVal = 0.0;
}
else {
rtnVal = relative_excess_demand;
}
}
return rtnVal;
}
While this fix seems to work for most applications, I'm certain a more robust solution is likely possible. Maybe those with more experience with the solver (@pralitp) may have some ideas. I'm happy to provide additional information if I can.
Test-RES-75x50-Sales-Cars-Global-Ea.zip
configuration_Test-RES-75x50-Sales-Global-Ea.zip
I've attached an example policy file and configuration file. The policy file forces a minimum BEV sales share in the "Car" category that increases from 5% in 2020 to 75% in 2050. You'll see output-ratios on the order of 10-9 and pMultipliers of 1e9. I think it is these conversions - unique to transportation - that yield problems when excess demands are compared to the solution floor in the getRelativeEd() method. They are so small that they trigger the determination that some of the markets are solved even when they arent' solved.
With the downloaded gcam.exe (prior to the hack), using my policy file, 12 of the 32 regions met the constraint (75% of Car sales must be BEVs by 2050) and another 2 regions were close. Interestingly, those 12 that met the constraint were 12 of the 13 largest regions wrt total travel demand met by cars. It was the smaller regions that were failing to meet the constraint.
My hack in my submitted "issue" resulted in 25 of the 32 global regions meeting the constraint, with another 4 regions that were within a percent or two. Those regions that didn't meet or come close to the constraint had very low demand for Cars (e.g., Africa_Southern, South Africa, and Taiwan). The market price reported for the constraint was 1.0 1975$s/GW for the years that it does not appear to work. Those markets are not reported as being "unsolved unsolvable" or "unsolved solvable" in the main_log.txt.
When I apply a similar constraint for US states in GCAM-USA 5.4, I get a similar result... The constraint works for nearly all states, but not for a handful of the smallest ones (e.g., DC and VT).
In the code hack above, I have also tried modifying the 0.01 multiplier to 0.001 or making the floor a fraction of the demand (e.g., demand * 0.01), but those changes seem to introduce additional market solution issues. Furthermore, I notice that there is a call to solver_library::getRelativeED(...) in solver_library::isWithinTolerance(...). The call from isWithinTolerance method is not benefiting from my hack, but my attempts to introduce changes to this file as well were not successful.
I don't think my code change should be considered a "fix" since it doesn't work for small markets. That said, hopefully it is helpful in understanding the problem.
Follow-up... I have also tried several work-arounds that do not involve code changes... One trial involved decreasing the "solution-floor" in the cal_broyden_config.xml file. I've also tried modifying the solution for only for specific types of markets, e.g.:
<solution-info-param-parser> <solution-floor fillout="1" good="LDV-EV-4W_New_Reg_Ea-2020" market-type="RES" period="4">1e-8</solution-floor> </solution-info-param-parser>
On their own, neither change worked particularly well (and the former introduced additional solution issues in other markets that previously were solving ok). I eventually was able to deduce that there is some interplay among the following settings:
- solution-floor (1e-4 by default)
- the ftol of the preconditioner-solver-component (1e-2 by default)
- the ftol of the broyden-solver-component (5e-4 by default)
- the "SMALL_NUM" constant (1e-6 by default) (and possibly "VERY_SMALL_NUM", 1e-8 by default, and "LARGE_NUM", 1e6 by default) in the util.h file
It seems that if modifications aren't done in a way that takes this interplay into account, solution issues arise. For example, you can have a small market pop in and out of the solvable list, triggering the floor in one place but not another, and thus never gets solved.
[Aside 1: A colleague had an state-specific RPS constraint (implemented via a RES-type market) work on its own, but not when it was paired with a CO2 constraint. I think this indicates that the issue I was having with RES constraints is not limited to the transportation sector.]
[Aside 2: For the LARGE_NUM constant, it is important to note that I am getting some RES-type market prices that are on the order of 1e6, so it seems that re-thinking the default value 1e6 may be in order. I suspect this might have been what was happening with my colleague and the RPS & CO2 constraints, but I haven't checked the logs to verify]
Based on all of this, I came up with another work-around. It definitely involves more changes than the one in my 1st comment, but also seems to work better. And perhaps it will provide some additional data points if anyone takes a deep dive into the solver parameterization in the future.
My approach:
- changed the solution-floor to 1e-8 (I also tried changing it just for the transportation RES markets, but the results were nearly the same as applying to all markets)
- changed the ftol of the preconditioner-solver-component to 1e-8
- changed the ftol of the broyden-solver-component to 1e-9
- changed SMALL_NUM, VERY_SMALL_NUM, and LARGE_NUM in util.h to 1e-9, 1e-12, and 1e+9, respectively, and recompiled gcam.exe
I just tested this setup on a GCAM-USA run with state-level light duty sales constraints (applied to each state) that transitioned from 5% EVs in 2020 to 100% in 2050. It worked for ALL the states. The solution process seemed to go well, with no problematic time periods (2020 required 2800 iterations, but the rest of the periods required 600-900 iterations). I was worried that runtime might be affected by modifying the solution tolerances and the constants. On my computer, this run took ~45 minutes, which is typical for a run with a number of policies and no major solution issues.
[Aside 3: I just tested this parameterization for a run with a US 80x50 CO2 cap, and for another that combined the CO2 cap and the light-duty EV sales target. Both scenarios worked as intended and I didn't run into any solution issues]
Now that I have a solution that works for me, I suspect that I will stop exploring this topic. However, perhaps future solver development work could consider the following:
- changing the SMALL_NUMBER, VERY_SMALL_NUMBER, and LARGE_NUMBER constants to better support more widely-varying market sizes
- and/or linking the various solver floors and FTOLS in a way that accounts for how these parameters interact with each other in the solution process
- and/or introducing some form of scaling that adjusts the various ceilings and floors based on the nature of the market (e.g., the floor may need to be adjusted for a market where the supply/demand values across a range of prices are very small)
[Aside 4: The preconditioner code includes some hard-coded values, and I'm curious whether it would be better to modify the code in a more scalable manner. And example is the following "newprice=ub-0.1".... so the new price is 0.1 less than the upper bound price. However, markets can differ greatly in the magnitude of prices. Would it be better to code this a "newprice=ub*0.9" or some other value such as a fraction of the range between the upper bound and lower bound?]
I want to thank Yang Ou and Matthew Binstead, who both provided useful suggestions and feedback.
Pralit and Yang made a suggestion for working around this numerical issue. I have implemented the suggestion and it appears to work. The gist is to use the price-unit-conversion to adjust the units to not be so small.
Here is an example of a heavy duty EV market share constraint (83% of sales in 2045) before implementing the suggestion...
<period year="2045">
<minicam-energy-input name="HDV-EV_New_Reg24-2045">
<ajusted-coef year="2045">0.83</adjusted-coef>
<price-unit-conversion>1.0</price-unit-conversion> [default, not showing in regular files]
</minicam-energy-input>
<res-secondary-output name="HDV-EV_New_Reg24-2045">
<output-ratio>7.377622377622378E-10</output-ratio>
<pMultiplier>1.0e9</pMultiplier>
</res-secondary-output>
</period>
And here is a version that incorporates the price-unit-conversion. Notice that in addition to the new price-unit-conversion there are changed needed to the adjusted coefficient, output-ratio, and pMultiplier.
<period year="2045">
<minicam-energy-input name="HDV-EV_New_Reg24-2045">
<adjusted-coef year="2045">830</adjusted-coef>
<price-unit-conversion>1.0e-3</price-unit-conversion>
</minicam-energy-input>
<res-secondary-output name="HDV-EV_New_Reg24-2045">
<output-ratio>7.377622377622378E-7</output-ratio>
<pMultiplier>1.0e6</pMultiplier>
</res-secondary-output>
</period>