Skip to content
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

Allow calling fix! twice in a row #299

Merged
merged 3 commits into from
Jun 15, 2019
Merged

Conversation

ericphanson
Copy link
Collaborator

Currently, if you fix! twice and then free!, you end up with a variable with AffineVexity but :fixed, because free! only removes the last :fixed and still changes the convexity.

On release and on master with ECOS, the test I added segfaults (which seems strange), and on release and master with SCS, the third test simply fails (the solver returns NaN).

src/variable.jl Outdated
@@ -127,8 +127,7 @@ end
fix!(x::Variable, v::Number) = fix!(x, fill(v, (1, 1)))

function free!(x::Variable)
# TODO this won't work if :fixed appears other than at the end of x.sets
x.sets[end] == :fixed && pop!(x.sets)
deleteat!(x.sets, findall(==(:fixed), x.sets))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with the fix!/free! stuff, but don't we only want to delete the last occurrence of :fixed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding was :fixed should be basically a boolean toggle, either the variable is :fixed or it isn't: if it has :fixed in its sets field, then it should also have ConstVexity() and should be effectively treated like a constant. So if you fix!(x,3.0) and then fix!(x, 2.0), you are just changing the fixed value of x, but if you free!(x), you're saying it's just a regular variable again (and so it shouldn't have :fixed in it's .sets field anymore).

In fact, I think we don't need the concept of :fixed at all: for a Variable x, :fixed should be in x.sets if and only if x.vexity == ConstVexity() (and otherwise x.vexity == AffineVexity()). So we actually have two redundant ways to express x being fixed to a constant, and the bug that causes the segfault or NaN result with that test I added is what happens when these two concepts get out of sync: by calling fix! twice and then free!, the vexity gets reset to AffineVexity(), but it still has :fixed in its .sets field. Besides in fix! and free!, :fixed only shows up once in Convex.jl's source code, which is in conic_form!(x::Variable,...), which has the lines

 if :fixed in x.sets
            # do exactly what we would for a constant
            ...

This causes the twice fix!'d then free!'d x to hit this code path despite having AffineVexity(). So in part of the problem formulation x is treated like a variable, but here it is treated like a constant. And I think that's what causes the bug that this PR's test checks.

I'll push another commit that actually removes :fixed altogether, so we just have 1 toggle to mean constant or not: ConstVexity() or AffineVexity(), which would hopefully remove the possibility for this kind of bug.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So all the tests pass locally with this change (removing :fixed altogether). Maybe there is a usage of fix! and free! that is not covered by the tests (which are quite sparse regarding fix! and free!) which I don't understand where you don't want to remove all instances of :fixed though. I certainly don't want to be pushing for changes that will break people's code. To me, fixed or not as a boolean makes sense (either the variable is constant or it isn't), but maybe there is some more complicated usage.

However, I somewhat doubtful that there are lots of active users of fix! and free! currently, since until #294 (which hasn't made it into a release yet), the functionality was completely broken on any code hitting that path (e.g. #298). So me, it makes sense to take the opportunity to have a simple semantic (treated like a constant or treated like a variable; double fix!ing just updates the value of the constant, free!ing makes it a regular variable again).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the very detailed and thoughtful analysis here. I think you're right, doing everything in terms of the vexity instead of a Symbol-based toggle makes more sense. I feel a little uncomfortable accepting the change unilaterally only because I have so little knowledge of the fix!/free! stuff, but then again, not many people really look at this package nowadays...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Yeah, makes sense.

Copy link
Contributor

@ararslan ararslan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks good to me and the change has been well justified. I'll give this a bit of time in case anyone wants to chime in but then I'll go forward with merging.

@@ -127,8 +126,6 @@ end
fix!(x::Variable, v::Number) = fix!(x, fill(v, (1, 1)))

function free!(x::Variable)
# TODO this won't work if :fixed appears other than at the end of x.sets
x.sets[end] == :fixed && pop!(x.sets)
x.vexity = AffineVexity()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly unrelated to the PR, but I noticed something: isn't it possible that x could have been something other than affine before fixing it as a constant? If that can occur, in such cases x would not actually have its vexity reset by freeing but rather changed entirely.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean, but I think Variable's only have ConstVexity() or AffineVexity(), where ConstVexity() corresponds to the fixed case. I think other vexities don't quite make sense for a Variable (but instead an expression involving a variable might have a different vexity, e.g. log(x) would be concave). To double check this, I searched the source for .vexity, since that would be the way to set a Variable to another vexity (since also searching for field and property doesn't show anything relevant), and only saw .vexity show up in a getter, in free!, in a show method, and in the line I changed,

        if x.vexity == ConstVexity()
            # do exactly what we would for a constant
            ...

so I think indeed, Variable's vexities are not set to anything but AffineVexity() (and ConstVexity() in fix!).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to say, I realized that actually I probably should have written vexity(x) instead of x.vexity in that if condition (we only need direct access for setting it, not accessing), so I pushed one (hopefully last) commit to change that.

@ararslan
Copy link
Contributor

Clearly no one else is going to look at this, so yolo

@ararslan ararslan merged commit 164b302 into jump-dev:master Jun 15, 2019
ericphanson added a commit to ericphanson/Convex.jl that referenced this pull request Jul 22, 2019
* Allow calling `fix!` twice without a `free!`

* Remove `:fixed` and just use `ConstVexity`

* Use getter instead of direct field access
@ericphanson ericphanson deleted the fixfix branch August 13, 2019 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants