-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
IPOPT duplicated history handling #252
Conversation
Codecov Report
@@ Coverage Diff @@
## master #252 +/- ##
===========================================
- Coverage 83.44% 72.61% -10.84%
===========================================
Files 22 22
Lines 3250 3268 +18
===========================================
- Hits 2712 2373 -339
- Misses 538 895 +357
Continue to review full report at Codecov.
|
I don't know why flake8 is failing, looks like F821 is |
Don't worry about |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some comments, the main thing I would like to fix is how the callCounter
loop is done in History
. A few other things:
- We definitely want to add a test for this, but given that I am working on refactoring the tests in parallel maybe that needs to happen in a separate PR later.
- The docs page
history.rst
need to be updated, to add theiter
key to the ASCII diagram showing history file structure.
pyoptsparse/pyOpt_history.py
Outdated
@@ -630,29 +630,32 @@ def getValues(self, names=None, callCounters=None, major=True, scale=False, stac | |||
callCounters.append(self.read("last")) | |||
callCounters.remove("last") | |||
|
|||
self._ipoptIterCounter = -1 # track iteration, only relevant for IPOPT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of this approach here. Can we go through a pre-processing step to determine the "suitable" callCounters
to loop over in the main loop instead? We should be able to come up with an approach that does not depend on checking the optimizer, and is generally applicable to all history files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current method should work for other optimizers as well (will double check), I just put IPOPT in the variable/method names so that what I intend here is clear. But I will rename those.
I don't know if we want to have another pre-processing loop, as we are already doing some callCounter
validation in the main loop (func vs funcSense, major, fail)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was thinking of creating a loop before we parse the file, where we generate the list of callCounters given all the various input flags. Then we only loop over those. But maybe that would decrease the efficiency so maybe it's not worth it. I'm okay with keeping it as is, but maybe some minor refactor would be helpful:
- lump all the checks into a single function that validates the callCounter against things like
funcs
, major flags etc - make it general for all optimizers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored
I don't have a good idea for the test other than comparing the final |
I think it's fine to do that for one test and for just IPOPT. We can have another one to test that |
Ready for another review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think? Would the code still be readable?
pyoptsparse/pyOpt_history.py
Outdated
@@ -630,40 +630,26 @@ def getValues(self, names=None, callCounters=None, major=True, scale=False, stac | |||
callCounters.append(self.read("last")) | |||
callCounters.remove("last") | |||
|
|||
self._ipoptIterCounter = -1 # track iteration, only relevant for IPOPT | |||
# get a list of valid call counters | |||
validCallCounters = self._generateValidCallCounters(callCounters, user_specified_callCounter, allowSens, major) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I am looking at this again, I think this may have a significant performance penalty since we are looping over the database twice. Could we have a function that checks if the current callCounter is valid? We would call that at every iteration, and only proceed if valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated, yeah I think this is better now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, just some minor comments. I will wait for others to take a look too. Thanks for this work!
pyoptsparse/pyOpt_history.py
Outdated
return data | ||
|
||
def _readValidCallCounter(self, i, user_specified_flag, allowSens_flag, major_flag): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add docstrings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every time I look into pyOptSparse I learn something new that I completely ignored before - i.e. the duplicated func and sens calls that are managed within the History() class.
So, if I get it right (aside the warnings for "old" hst files) you added the iter
key to facilitate the process of picking the "right" stored iter - namely the one with the function evaluation - by checking against the cached value of the DV vector. This cached value is currently used only to ensure we are not calling the function evaluation unnecessarily within the masterFunc() method. A few comments:
- I am getting a bit confused with the major/minor iteration definition as I am used to SNOPT, here we are just talking about func eval vs fcon/sens eval?
- for @nwu63: I don't get (even in our current approach) how we squeeze all the info into the major iteration. Are we doing it at all - i.e. saving the sensitivities values within a major iter - or are we just discarding that info? I might be confused again by SNOPT which has the
optimality
key for every iter - looks like
_readValidCallCounter
mostly overlaps with Pointexist(), maybe we can get rid of that? - I have further questions about some low-level machinery in
Optimizer()
now that we scratched it with this cache thing, but that is a separate discussion
Thanks a lot for putting this together! Questions aside, I would also feel safer if @sseraj takes a look at this too
Yes, and
Minor iterations are internal iterations for solving the QP subproblems I believe.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good
Yeah this seems an improvement beyond the IPOPT issue itself
Exactly, here the difference is between major (i.e. new
Good point on the method being public. The snippet itself is pretty small so I agree that touching it would be pretty unnecessary right now |
I think once Sabet's comments are addressed this is good to go! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we merge #251 first?
Yes I think that'd be best, then we can add some new tests without causing merge conflicts. |
@kanekosh we just merged the test refactoring PR. The only thing missing for this PR to be merged is the additional test for IPOPT stored values, do you think you can address this soon? |
will do this week
|
test/test_hs015.py
Outdated
|
||
# Check iteration counters | ||
hist = History(self.histFileName, flag="r") | ||
data_init = hist.getValues(names=["iter"], callCounters=[0], allowSens=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a similar test via the read
function? I want to make sure that the history file is "correct" and the read
function should be used instead of getValues
since it does a bunch of other things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced getValues
with read
for the iter
checks.
I didn't replace it for the second part (where I loop over the iteration and check the consecutive objective values) because we do want to test getValues
there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep looks good to me
self.assertEqual(0, data_init["iter"]) | ||
data_last = hist.getValues(names=["iter"], callCounters=["last"], allowSens=True) | ||
data_last = hist.read(hist.read("last")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this duplicated? you just get the int
counter with the first call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean? hist.read("last")
returns the last call counter (integer)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to go! Do we want to address the issues with getValues()
before a new release?
Purpose
Closes #182. I added
iter
entries to the history data at every call counter. Other than that the hist file is unchanged.Then the duplicated entries are removed in
OptView_baseclasses
andHistory.getValues()
.When we read the old hist files which don't have the
iter
entries, OptView andgetValues()
raise a warning.Type of change
What types of change is it?
Select the appropriate type(s) that describe this PR
Testing
The history file and OptView plot for
test_hs015.py.
IPOPT.out says it had 12 function evaluations (= iter).Checklist
Put an
x
in the boxes that apply.flake8
andblack
to make sure the code adheres to PEP-8 and is consistently formatted