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

lexicographic (hierachical) multi-objective optimization #304

Open
jeffreyhanson opened this issue Aug 30, 2023 · 5 comments
Open

lexicographic (hierachical) multi-objective optimization #304

jeffreyhanson opened this issue Aug 30, 2023 · 5 comments

Comments

@jeffreyhanson
Copy link
Contributor

jeffreyhanson commented Aug 30, 2023

I've been starting to use lexicographic (hierachical) multi-objective optimization a lot more in my own work, and I think it could potentially be useful to formally add this to prioritizr. For example, one of my projects involves doing a multi-objective prioritization that first uses the min shortfall objective and then minimizes cost. Although the calibrating trade-offs vignette shows how to do this manually, I wonder if there would be some way we could provide a more convienent API to do this?

I imagine one approach for implementing this could be to update all the add_*_penalties() functions so that users could specify whether the penalty should be treated as a blended or lexicographic objective for optimization. However, this would mean adding in extra parameters to the functions that may, or may not, have an effect depending on other parameters. For example, if we had new blended parameter in add_boundary_penalties(), such that blended = FALSE means that the penalty should be treated as a lexicographic objective (instead of a blended objective), then the penalty parameter would have no effect. However, I worry this added complexity would confuse users?

Alternatively, another approach for implementing this could be to create a new family of functions to handle lexicographic optimization. So, if users wanted to account for total boundary length using a blended optimization approach, they could still use add_boundary_penalties(). And if they wanted to account for total boundary length using a lexicographic optimization approach, they could use a new function (e.g., add_boundary_${insert_something_clever}())?

Or perhaps you can think of a better way to do this? Also, does anyone else think this would be useful? If you think the second option sounds better, can you think of what a new family of functions for lexicographic optimization should be called (i.e., the ${insert_something_clever} in the function above)? I'm pretty busy at the moment so don't have time to implement this in the near future, but I thought it might be useful getting input from other people?

@ricschuster
Copy link
Member

I like this idea Jeff!

Personally, I like the first approach of adding a parameter. Maybe instead of blended we could add hierarchical and set it to FALSE by default?

This way current code would still work as the behavior wouldn't change. At least I think so. Would it break existing code to add another parameter like this?

I don't think users would be too confused as long as the documentation explains things well. Maybe I'm off though. Would be great to get some feedback from users on this too.

@jeffreyhanson
Copy link
Contributor Author

jeffreyhanson commented Aug 31, 2023

Yeah, I agree the first approach has its benefits. For example, (1) we don't need to introduce a new family of functions, (2) we don't need to update the print() method for problem() objects to accomodate a new family of functions, (3) it provides an easy way for users to play around with hierachical optimization, and (4) it reduces the conceptual load/burden for using the package (e.g., users don't have to learn about "penalties" vs. "${something else}" for blended vs. hierachical optimization, and instead users only need to think about "penalties" and if they should be optimized using blended vs. hierachical approaches).

One other thing to think about is that we might want to also add a parameter to allow the optimization process to degrade the primary/main objective by a relative amount. E.g., you might havbe a main objective of minimizing cost and a secondary objective of minimizing boundary length. In some cases, you might not want to accept any degradation (meaning that you want to reduce boundary length as much as possible, but don't make it any more costly). In other cases, you might want to accept a small amount of degredation (meaning that you want to reduce boundary length as much as possible, and you're happy for the cost to increase slightly [e.g. 5%]). So, if we wanted to add this, we would need to add an extra parameter too - so I don't know if/how that would influence our choice of API?

@ricschuster
Copy link
Member

Great points!

Interesting idea about the additional parameter. I think that's a good idea and I don't think it influences the choice of API at least for me.

What about you?

@jeffreyhanson
Copy link
Contributor Author

jeffreyhanson commented Sep 3, 2023

Yeah, I worry that such a function could be confusing, because some parameters would only have an effect when other parameters are NULL or TRUE/FALSE. E.g., at the moment we have this as a function definition

add_boundary_penalties(
  x,
  penalty, 
  edge_factor = rep(0.5, number_of_zones(x), 
  zones = diag(number_of_zones(x)), 
  data = NULL
)

This would turn into something like (where reltol and hierachical are new parameters for hierachical optimization). Note that zones and daa parameters are always last for consistency with other functions in prioritizr.

add_boundary_penalties(
  x,
  penalty, 
  edge_factor = rep(0.5, number_of_zones(x), 
  reltol = NULL,
  hierachical = FALSE,
  zones = diag(number_of_zones(x)), 
  data = NULL,
)

So, to use blended optimization, it would be the same as usual, e.g.:

p %>% add_boundary_penalties(penalty = 0.001)

And for hierachical optimization, it would be something like this:

p %>% add_boundary_penalties(penalty = NULL, reltol = 0.05, hierachicial = TRUE)

If we had the hierachical stuff in a different family of functions, then it would look something like this instead:

# blended
p %>% add_boundary_b_penalties(penalty = 0.001)

# hierachical
p %>% add_boundary_h_penalties(reltol = 0.05)

However, I'm not 100% happy with _h_ and _b_ penalty types, because add_asym_connectivity_penalties() is already long enough, and I would prefer not to make this function name even longer.

@ricschuster
Copy link
Member

Hmmm, the way you describe things here makes me reconsider my initial preference. Given the potential for confusion of the approach to keep the functions and adding parameters, I am now favoring the _h_ and _b_ approach.

Its still hard to say how much appetite for the _h_ functions is out there though. I have no idea about that one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants