use fnv::FnvHashMap;
use std::fmt::Debug;
use std::hash::Hash;
use crate::Model;
use crate::{Error, Result};
use grb_sys2 as ffi;
mod private_traits {
use super::*;
pub trait ModelObjectPrivate: Sized + Hash + Eq + Copy {
fn from_raw(id: u32, model_id: u32) -> Self;
fn idx_manager_mut(model: &mut Model) -> &mut IdxManager<Self>;
fn idx_manager(model: &Model) -> &IdxManager<Self>;
unsafe fn gurobi_remove(m: *mut ffi::GRBmodel, inds: &[i32]) -> ffi::c_int;
fn model_id(&self) -> u32;
}
}
use private_traits::ModelObjectPrivate;
pub trait ModelObject: ModelObjectPrivate + Debug {
fn id(&self) -> u32;
}
macro_rules! create_model_obj_ty {
($t:ident, $model_attr:ident, $delfunc:path, $doc:literal) => {
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[doc = $doc]
pub struct $t {
pub(crate) id : u32,
pub(crate) model_id: u32,
}
impl ModelObjectPrivate for $t {
fn from_raw(id: u32, model_id: u32) -> $t {
Self{ id, model_id }
}
fn idx_manager_mut(model: &mut Model) -> &mut IdxManager<$t> {
&mut model.$model_attr
}
fn idx_manager(model: &Model) -> &IdxManager<$t> {
&model.$model_attr
}
unsafe fn gurobi_remove(m: *mut ffi::GRBmodel, inds: &[i32]) -> ffi::c_int {
$delfunc(m, inds.len() as i32, inds.as_ptr())
}
fn model_id(&self) -> u32 { self.model_id }
}
impl ModelObject for $t {
fn id(&self) -> u32 { self.id }
}
};
}
create_model_obj_ty!(Var, vars, ffi::GRBdelvars,
"A Gurobi variable.
To interact with the attributes of a variable, use [`Model::get_obj_attr`] and [`Model::set_obj_attr`]"
);
create_model_obj_ty!(Constr, constrs, ffi::GRBdelconstrs,
"A linear constraint added to a [`Model`]
To interact with the attributes of a constraint, use [`Model::get_obj_attr`] and [`Model::set_obj_attr`]"
);
create_model_obj_ty!(QConstr, qconstrs, ffi::GRBdelqconstrs,
"A quadratic constraint added to a [`Model`]
To interact with the attributes of a constraint, use [`Model::get_obj_attr`] and [`Model::set_obj_attr`]"
);
create_model_obj_ty!(SOS, sos, ffi::GRBdelsos,
"An SOS constraint added to a [`Model`]
To interact with the attributes of a constraint, use [`Model::get_obj_attr`] and [`Model::set_obj_attr`]"
);
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum IdxState {
Present(i32),
Build(i32),
Pending,
Removed(i32), }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
enum UpdateAction {
Noop = 0,
Fix = 1,
Rebuild = 2, }
#[derive(Debug)]
#[doc(hidden)]
pub struct IdxManager<T: Hash + Eq> {
update_model: bool,
update_action: UpdateAction,
next_id: u32,
model_id: u32,
order: Vec<T>,
lookup: FnvHashMap<T, IdxState>,
}
impl<T: ModelObject> IdxManager<T> {
pub(crate) fn new_with_existing_obj(model_id: u32, nobj: usize) -> IdxManager<T> {
let mut im = IdxManager::new(model_id);
for id in 0..nobj {
let v = T::from_raw(id as u32, model_id);
im.order.push(v);
im.lookup.insert(v, IdxState::Present(id as i32));
}
im.next_id = nobj as u32;
im
}
pub(crate) fn new(model_id: u32) -> IdxManager<T> {
let order = Vec::new();
let lookup = FnvHashMap::default();
IdxManager {
order,
lookup,
model_id,
next_id: 0,
update_action: UpdateAction::Noop,
update_model: false,
}
}
fn mark_update_action(&mut self, a: UpdateAction) {
if a > self.update_action {
self.update_action = a
}
}
pub(crate) fn get_index(&self, o: &T) -> Result<i32> {
if let Some(state) = self.lookup.get(o) {
match *state {
IdxState::Removed(_) => Err(Error::ModelObjectRemoved),
IdxState::Pending | IdxState::Build(_) => Err(Error::ModelObjectPending),
IdxState::Present(idx) => Ok(idx),
}
} else if o.model_id() == self.model_id {
Err(Error::ModelObjectRemoved)
} else {
Err(Error::ModelObjectMismatch)
}
}
pub(crate) fn get_index_build(&self, o: &T) -> Result<i32> {
if let Some(state) = self.lookup.get(o) {
match *state {
IdxState::Pending => Err(Error::ModelObjectPending),
IdxState::Present(idx) | IdxState::Build(idx) | IdxState::Removed(idx) => Ok(idx),
}
} else if o.model_id() == self.model_id {
Err(Error::ModelObjectRemoved)
} else {
Err(Error::ModelObjectMismatch)
}
}
pub(crate) fn model_update_needed(&self) -> bool {
self.update_model
}
pub(crate) fn objects(&self) -> &[T] {
assert!(!self.update_model);
self.order.as_slice()
}
pub(crate) fn remove(&mut self, o: T, _update_lazy: bool) -> Result<()> {
if o.model_id() != self.model_id {
return Err(Error::ModelObjectMismatch);
}
let state = self.lookup.get_mut(&o).ok_or(Error::ModelObjectRemoved)?;
match *state {
IdxState::Build(_) | IdxState::Pending => return Err(Error::ModelObjectPending),
IdxState::Present(idx) => *state = IdxState::Removed(idx),
IdxState::Removed(_) => return Err(Error::ModelObjectRemoved),
}
self.update_model = true;
self.mark_update_action(UpdateAction::Rebuild);
debug_assert_eq!(self.lookup.len(), self.order.len());
Ok(())
}
pub fn add_new(&mut self, update_lazy: bool) -> T {
debug_assert_eq!(self.lookup.len(), self.order.len());
let o = T::from_raw(self.next_id, self.model_id);
self.next_id += 1;
self.mark_update_action(UpdateAction::Fix);
let state = if update_lazy {
IdxState::Pending
} else {
IdxState::Build(self.lookup.len() as i32)
};
self.update_model = true;
#[cfg(debug_assertions)]
{
if let Some(other) = self.order.last() {
let s = self.lookup[other];
if s != IdxState::Pending {
assert_ne!(s, state);
}
}
}
self.lookup.insert(o, state);
self.order.push(o);
o
}
#[cfg(debug_assertions)]
#[allow(dead_code)]
fn print_vars(&self) {
println!("----------------------------------------------------");
for o in &self.order {
print!("{:?} ", self.lookup[o]);
}
println!();
}
pub(crate) fn update(&mut self) {
debug_assert_eq!(self.lookup.len(), self.order.len());
use std::collections::hash_map::Entry;
match self.update_action {
UpdateAction::Noop => {}
UpdateAction::Fix => {
let mut k = self.order.len() as i32 - 1;
for var in self.order.iter().rev() {
let state = self.lookup.get_mut(var).unwrap();
match *state {
IdxState::Removed(_) => unreachable!(),
IdxState::Pending => {
*state = IdxState::Present(k);
}
IdxState::Build(idx) => {
debug_assert_eq!(idx, k);
*state = IdxState::Present(k);
}
IdxState::Present(_) => break,
};
k -= 1;
}
}
UpdateAction::Rebuild => {
let mut k = 0i32;
let order = &mut self.order;
let lookup = &mut self.lookup;
order.retain(|&o| match lookup.entry(o) {
Entry::Vacant(_) => unreachable!("bug, should always have an entry in lookup"),
Entry::Occupied(mut e) => {
let state = *e.get();
match state {
IdxState::Present(_) | IdxState::Build(_) | IdxState::Pending => {
e.insert(IdxState::Present(k));
k += 1;
true
}
IdxState::Removed(_) => {
e.remove();
false
}
}
}
});
debug_assert_eq!(k as usize, self.lookup.len());
}
}
debug_assert_eq!(self.lookup.len(), self.lookup.len());
self.update_model = false;
self.update_action = UpdateAction::Noop;
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Action {
SwitchUpdateMode,
AddVar(u8),
RemoveVar(u8),
}
fn action_strat(num: usize) -> impl Strategy<Value = Vec<Action>> {
let s = prop_oneof![
Just(Action::SwitchUpdateMode),
any::<u8>().prop_map(Action::AddVar),
any::<u8>().prop_map(Action::RemoveVar),
];
proptest::collection::vec(s, ..num)
}
fn state_machine(actions: Vec<Action>) {
let mut update_mode_lazy = true;
let mut vars: Vec<Option<Var>> = vec![None; u8::MAX as usize + 1];
let mut idx_manager = IdxManager::new(0);
for a in actions {
match a {
Action::SwitchUpdateMode => {
update_mode_lazy = !update_mode_lazy;
if !update_mode_lazy {
idx_manager.update(); }
}
Action::AddVar(i) => {
let i = i as usize;
let v = vars[i];
match v {
Some(v) => {
if !update_mode_lazy {
idx_manager.get_index_build(&v).unwrap();
}
}
None => {
vars[i] = Some(idx_manager.add_new(update_mode_lazy));
}
}
}
Action::RemoveVar(i) => {
let i = i as usize;
let v = vars[i];
match v {
Some(v) => match idx_manager.remove(v, update_mode_lazy) {
Ok(()) => vars[i] = None,
Err(e) => assert_eq!(e, Error::ModelObjectPending),
},
None => {}
}
}
}
}
}
proptest! {
#[test]
fn fuzz(actions in action_strat(100)) {
state_machine(actions);
}
}
#[test]
fn regressions() {
use Action::*;
state_machine(vec![AddVar(4), SwitchUpdateMode, AddVar(4)]);
state_machine(vec![
AddVar(0),
AddVar(1),
AddVar(2),
RemoveVar(1),
SwitchUpdateMode,
AddVar(1),
]);
}
}