diff --git a/.gitignore b/.gitignore index b260abc..ba7b17c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ *.log *.lp .vscode/ + +# Ignore the cgo helper files and gurobi passthrough files +***cgoHelper.go +*gurobi_passthrough.h \ No newline at end of file diff --git a/README.md b/README.md index 2f0f768..b071a72 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,35 @@ +# NOTE: This repository has now been replaced by the organization [MatProGo](https://github.com/MatProGo-dev)'s project: [Gurobi.go](https://github.com/MatProGo-dev/Gurobi.go/tree/main) + # Gurobi Interfaces for Golang Unofficial Gurobi Interfaces for Golang. -(inspired from https://github.com/JuliaOpt/Gurobi.jl) +(inspired by https://github.com/JuliaOpt/Gurobi.jl) + +## Installation + +Warning: The setup script is designed to only work on Mac OS X. If you are interested in using this on a Windows machine, then there are no guarantees that it will work. + +### Installation in Module + +1. Use a "-d" `go get -d github.com/kwesiRutledge/gurobi.go/gurobi`. Pay attention to which version appears in your terminal output. +2. Enter Go's internal installation of gurobi.go. For example, run `cd ~/go/pkg/mod/github.com/kwesi\!rutledge/gurobi.go@v0.0.0-20220103225839-e6367b1d0f27` where the suffix is the version number from the previous output. +3. Run go generate with sudo privileges from this installation. `sudo go generate`. + +### Development Installation + +1. Clone the library using `git clone github.com/kwesiRutledge/gurobi.go ` +2. Run the setup script from inside the cloned repository: `go generate`. + + ## Usage + +See the `testing` directory for some examples of how to use this. + Works in progress... + + ## LICENSE See [LICENSE](LICENSE). diff --git a/examples/qp.go b/examples/qp.go index c8c9bb0..2c5143a 100644 --- a/examples/qp.go +++ b/examples/qp.go @@ -1,7 +1,10 @@ package main -import "../gurobi" -import "fmt" +import ( + "fmt" + + gurobi "../gurobi" +) func main() { // Create environment. diff --git a/gen.go b/gen.go new file mode 100644 index 0000000..a38d74d --- /dev/null +++ b/gen.go @@ -0,0 +1,9 @@ +package gurobi + +/* +gen.go +Description: + Includes a go:generate statement. This will be called when the user runs go generate. +*/ + +//go:generate go run utils/setup.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a57931d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/kwesiRutledge/gurobi.go + +go 1.16 diff --git a/gurobi/env.go b/gurobi/env.go index 9221576..df0f15f 100644 --- a/gurobi/env.go +++ b/gurobi/env.go @@ -1,8 +1,11 @@ package gurobi -// #include +// #include import "C" -import "errors" +import ( + "errors" + "fmt" +) type Env struct { env *C.GRBenv @@ -25,3 +28,129 @@ func (env *Env) Free() { C.GRBfreeenv(env.env) } } + +/* +SetTimeLimit +Description: + This member function is meant to set the time limit of the environment that has + been created in Gurobi. +*/ +func (env *Env) SetTimeLimit(limitIn float64) error { + // Constants + paramName := "TimeLimit" + + // Algorithm + + if env == nil { + return fmt.Errorf("The input env variable to SetTimeLimit() was nil!") + } + + errcode := int(C.GRBsetdblparam(env.env, C.CString(paramName), C.double(limitIn))) + if errcode != 0 { + return fmt.Errorf("There was an error running GRBsetdblparam(): Error code %v", errcode) + } + + // If everything was successful, then return nil. + return nil + +} + +/* +GetTimeLimit +Description: + This member function is meant to set the time limit of the environment that has + been created in Gurobi. +*/ +func (env *Env) GetTimeLimit() (float64, error) { + // Constants + paramName := "TimeLimit" + + // Algorithm + + if env == nil { + return -1, fmt.Errorf("The input env variable to SetTimeLimit() was nil!") + } + + var limitOut C.double + errcode := int(C.GRBgetdblparam(env.env, C.CString(paramName), &limitOut)) + if errcode != 0 { + return -1, fmt.Errorf("There was an error running GRBsetdblparam(): Error code %v", errcode) + } + + // If everything was successful, then return nil. + return float64(limitOut), nil + +} + +/* +SetDBLParam() +Description: + Mirrors the functionality of the GRBsetdblattr() function from the C api. + Sets the parameter of the solver that has name paramName with value val. +*/ +func (env *Env) SetDBLParam(paramName string, val float64) error { + // Check that attribute is actually a scalar double attribute. + if !IsValidDBLParam(paramName) { + return fmt.Errorf("The input attribute name (%v) is not considered a valid attribute.", paramName) + } + + // Check that the env object is not nil. + if env == nil { + return fmt.Errorf("The input env variable to SetTimeLimit() was nil!") + } + + // Set Attribute + errcode := int(C.GRBsetdblparam(env.env, C.CString(paramName), C.double(val))) + if errcode != 0 { + return fmt.Errorf("There was an error running GRBsetdblparam(), errcode %v", errcode) + } + + // If everything was successful, then return nil. + return nil + +} + +/* +GetDBLParam() +Description: + Mirrors the functionality of the GRBgetdblattr() function from the C api. + Gets the parameter of the model with the name paramName if it exists. +*/ +func (env *Env) GetDBLParam(paramName string) (float64, error) { + // Check the paramName to make sure it is valid + if !IsValidDBLParam(paramName) { + return -1, fmt.Errorf("The input attribute name (%v) is not considered a valid attribute.", paramName) + } + + // Check environment input + if env == nil { + return -1, fmt.Errorf("The input env variable to SetTimeLimit() was nil!") + } + + // Use GRBgetdblparam + var valOut C.double + errcode := int(C.GRBgetdblparam(env.env, C.CString(paramName), &valOut)) + if errcode != 0 { + return -1, fmt.Errorf("There was an error running GRBgetdblparam(). Errorcode %v", errcode) + } + + // If everything was successful, then return nil. + return float64(valOut), nil +} + +func IsValidDBLParam(paramName string) bool { + // All param names + var scalarDoubleAttributes []string = []string{"TimeLimit", "Cutoff", "BestObjStop"} + + // Check that attribute is actually a scalar double attribute. + paramNameIsValid := false + + for _, validName := range scalarDoubleAttributes { + if validName == paramName { + paramNameIsValid = true + break + } + } + + return paramNameIsValid +} diff --git a/gurobi/env_test.go b/gurobi/env_test.go new file mode 100644 index 0000000..6dfa7d2 --- /dev/null +++ b/gurobi/env_test.go @@ -0,0 +1,121 @@ +package gurobi + +import "testing" + +/* +TestEnv_NewEnv1 +Description: + Verifies that NewEnv() correctly creates a new environment with log file given. +*/ +func TestEnv_NewEnv1(t *testing.T) { + // Constants + logfilename1 := "thomTide.log" + + // Algorithm + env, err := NewEnv(logfilename1) + if err != nil { + t.Errorf("There was an issue creating the new Env variable: %v", err) + } + defer env.Free() + +} + +/* +TestEnv_SetTimeLimit1 +Description: + Verifies that NewEnv() correctly creates a new environment with log file given. +*/ +func TestEnv_SetTimeLimit1(t *testing.T) { + // Constants + logfilename1 := "thomTide.log" + var newTimeLimit float64 = 132 + + // Algorithm + env, err := NewEnv(logfilename1) + if err != nil { + t.Errorf("There was an issue creating the new Env variable: %v", err) + } + defer env.Free() + + err = env.SetTimeLimit(newTimeLimit) + if err != nil { + t.Errorf("There was an error setting the time limit of the environment! %v", err) + } + + detectedTimeLimit, err := env.GetTimeLimit() + if err != nil { + t.Errorf("There was an error getting the time limit of the environment! %v", err) + } + + if detectedTimeLimit != newTimeLimit { + t.Errorf("The detected time limit (%v) was not equal to the expected time limit (%v s).", detectedTimeLimit, newTimeLimit) + } + +} + +/* +TestEnv_SetDBLParam1 +Description: + Verifies that we can set the value of 'TimeLimit' in current model. +*/ +func TestEnv_SetDBLParam1(t *testing.T) { + // Constants + logfilename1 := "thomTide.log" + var newTimeLimit float64 = 132 + + // Algorithm + env, err := NewEnv(logfilename1) + if err != nil { + t.Errorf("There was an issue creating the new Env variable: %v", err) + } + defer env.Free() + + err = env.SetDBLParam("TimeLimit", newTimeLimit) + if err != nil { + t.Errorf("There was an error setting the time limit of the environment! %v", err) + } + + detectedTimeLimit, err := env.GetDBLParam("TimeLimit") + if err != nil { + t.Errorf("There was an error getting the time limit of the environment! %v", err) + } + + if detectedTimeLimit != newTimeLimit { + t.Errorf("The detected time limit (%v) was not equal to the expected time limit (%v s).", detectedTimeLimit, newTimeLimit) + } + +} + +/* +TestEnv_SetDBLParam2 +Description: + Verifies that we can set the value of 'BestObjStop' in current model. +*/ +func TestEnv_SetDBLParam2(t *testing.T) { + // Constants + logfilename1 := "thomTide.log" + var newVal float64 = 132 + var paramToModify string = "BestObjStop" + + // Algorithm + env, err := NewEnv(logfilename1) + if err != nil { + t.Errorf("There was an issue creating the new Env variable: %v", err) + } + defer env.Free() + + err = env.SetDBLParam(paramToModify, newVal) + if err != nil { + t.Errorf("There was an error setting the time limit of the environment! %v", err) + } + + detectedVal, err := env.GetDBLParam(paramToModify) + if err != nil { + t.Errorf("There was an error getting the time limit of the environment! %v", err) + } + + if detectedVal != newVal { + t.Errorf("The detected %v (%v) was not equal to the expected %v (%v).", paramToModify, detectedVal, paramToModify, newVal) + } + +} diff --git a/gurobi/error.go b/gurobi/error.go index cb3c932..4852be3 100644 --- a/gurobi/error.go +++ b/gurobi/error.go @@ -1,6 +1,6 @@ package gurobi -// #include +// #include import "C" import "errors" @@ -27,5 +27,5 @@ func (env *Env) makeError(errcode C.int) error { } func (model *Model) makeError(errcode C.int) error { - return model.env.makeError(errcode) + return model.Env.makeError(errcode) } diff --git a/gurobi/gurobi.go b/gurobi/gurobi.go index 1ec12f0..4cef401 100644 --- a/gurobi/gurobi.go +++ b/gurobi/gurobi.go @@ -1,7 +1,6 @@ package gurobi -// #include -// #cgo LDFLAGS: -lgurobi65 +// #include import "C" const DBL_ATTR_OBJ = C.GRB_DBL_ATTR_OBJ diff --git a/gurobi/model.go b/gurobi/model.go index 7cfb65b..7be35bc 100644 --- a/gurobi/model.go +++ b/gurobi/model.go @@ -1,16 +1,18 @@ package gurobi -// #include +// TODO: Change the names of all instances of Model so that they are exported. + +// #include import "C" import "errors" // Model ... // Gurobi model object type Model struct { - model *C.GRBmodel - env Env - vars []Var - constrs []Constr + AsGRBModel *C.GRBmodel + Env Env + Variables []Var + Constraints []Constr } // NewModel ... @@ -31,7 +33,7 @@ func NewModel(modelname string, env *Env) (*Model, error) { return nil, errors.New("Failed retrieve the environment") } - return &Model{model: model, env: Env{newenv}}, nil + return &Model{AsGRBModel: model, Env: Env{newenv}}, nil } // Free ... @@ -40,11 +42,21 @@ func (model *Model) Free() { if model == nil { return } - C.GRBfreemodel(model.model) + C.GRBfreemodel(model.AsGRBModel) } -// AddVar ... -// create a variable to the model +/* +AddVar +Description: + Create a variable to the model + This includes inputs for: + - obj = Linear coefficient applied to this variable in the objective function (i.e. objective = ... + obj * newvar + ...) + - lb = Lower Bound + - ub = Upper Bound + - name = Name of the variable +Links: + +*/ func (model *Model) AddVar(vtype int8, obj float64, lb float64, ub float64, name string, constrs []*Constr, columns []float64) (*Var, error) { if model == nil { return nil, errors.New("model is not initialized") @@ -69,7 +81,7 @@ func (model *Model) AddVar(vtype int8, obj float64, lb float64, ub float64, name pval = (*C.double)(&columns[0]) } - err := C.GRBaddvar(model.model, C.int(len(constrs)), pind, pval, C.double(obj), C.double(lb), C.double(ub), C.char(vtype), C.CString(name)) + err := C.GRBaddvar(model.AsGRBModel, C.int(len(constrs)), pind, pval, C.double(obj), C.double(lb), C.double(ub), C.char(vtype), C.CString(name)) if err != 0 { return nil, model.makeError(err) } @@ -78,8 +90,8 @@ func (model *Model) AddVar(vtype int8, obj float64, lb float64, ub float64, name return nil, err } - model.vars = append(model.vars, Var{model, int32(len(model.vars))}) - return &model.vars[len(model.vars)-1], nil + model.Variables = append(model.Variables, Var{model, int32(len(model.Variables))}) + return &model.Variables[len(model.Variables)-1], nil } // AddVars ... @@ -146,7 +158,7 @@ func (model *Model) AddVars(vtype []int8, obj []float64, lb []float64, ub []floa pname = (**C.char)(&vname[0]) } - err := C.GRBaddvars(model.model, C.int(len(vtype)), C.int(numnz), pbeg, pind, pval, pobj, plb, pub, pvtype, pname) + err := C.GRBaddvars(model.AsGRBModel, C.int(len(vtype)), C.int(numnz), pbeg, pind, pval, pobj, plb, pub, pvtype, pname) if err != 0 { return nil, model.makeError(err) } @@ -156,16 +168,28 @@ func (model *Model) AddVars(vtype []int8, obj []float64, lb []float64, ub []floa } vars := make([]*Var, len(vtype), 0) - xcols := len(model.vars) + xcols := len(model.Variables) for i := xcols; i < xcols+len(vtype); i++ { - model.vars = append(model.vars, Var{model, int32(i)}) - vars[i] = &model.vars[len(model.vars)-1] + model.Variables = append(model.Variables, Var{model, int32(i)}) + vars[i] = &model.Variables[len(model.Variables)-1] } return vars, nil } -// AddConstr ... -// add a constraint into the model. +/* +AddConstr +Description: + Add a Linear constraint into the model. + Uses the GRBaddconstr() method from the C api. +Inputs: + - vars: A slice of variable arrays which provide the indices for the gurobi model's variables. + - val: A slice of float values which are used as coefficients for the variables in the linear constraint. + - sense: A flag which determines if this is an equality, less than equal or greater than or equal constraint. + - rhs: A float value which determines the constant which is on the other side of the constraint. + - constrname: An optional name for the constraint. +Link: + https://www.gurobi.com/documentation/9.1/refman/c_addconstr.html +*/ func (model *Model) AddConstr(vars []*Var, val []float64, sense int8, rhs float64, constrname string) (*Constr, error) { if model == nil { return nil, errors.New("") @@ -173,10 +197,10 @@ func (model *Model) AddConstr(vars []*Var, val []float64, sense int8, rhs float6 ind := make([]int32, len(vars)) for i, v := range vars { - if v.idx < 0 { + if v.Index < 0 { return nil, errors.New("Invalid vars") } - ind[i] = v.idx + ind[i] = v.Index } pind := (*C.int)(nil) @@ -186,7 +210,7 @@ func (model *Model) AddConstr(vars []*Var, val []float64, sense int8, rhs float6 pval = (*C.double)(&val[0]) } - err := C.GRBaddconstr(model.model, C.int(len(ind)), pind, pval, C.char(sense), C.double(rhs), C.CString(constrname)) + err := C.GRBaddconstr(model.AsGRBModel, C.int(len(ind)), pind, pval, C.char(sense), C.double(rhs), C.CString(constrname)) if err != 0 { return nil, model.makeError(err) } @@ -195,8 +219,8 @@ func (model *Model) AddConstr(vars []*Var, val []float64, sense int8, rhs float6 return nil, err } - model.constrs = append(model.constrs, Constr{model, int32(len(model.constrs))}) - return &model.constrs[len(model.constrs)-1], nil + model.Constraints = append(model.Constraints, Constr{model, int32(len(model.Constraints))}) + return &model.Constraints[len(model.Constraints)-1], nil } func (model *Model) AddConstrs(vars [][]*Var, val [][]float64, sense []int8, rhs []float64, constrname []string) ([]*Constr, error) { @@ -204,7 +228,7 @@ func (model *Model) AddConstrs(vars [][]*Var, val [][]float64, sense []int8, rhs return nil, errors.New("") } - if len(vars) != len(val) || len(val) != len(sense) || len(sense) != len(rhs) || len(rhs) != len(constrname) { + if len(vars) != len(val) || len(val) != len(sense) || len(sense) != len(rhs) || len(rhs) != len(constrname) { return nil, errors.New("") } @@ -223,7 +247,7 @@ func (model *Model) AddConstrs(vars [][]*Var, val [][]float64, sense []int8, rhs } for j := 0; j < len(vars[i]); j++ { - idx := vars[i][j].idx + idx := vars[i][j].Index if idx < 0 { return nil, errors.New("") } @@ -258,7 +282,7 @@ func (model *Model) AddConstrs(vars [][]*Var, val [][]float64, sense []int8, rhs pname = (**C.char)(&name[0]) } - err := C.GRBaddconstrs(model.model, C.int(len(constrname)), C.int(numnz), pbeg, pind, pval, psense, prhs, pname) + err := C.GRBaddconstrs(model.AsGRBModel, C.int(len(constrname)), C.int(numnz), pbeg, pind, pval, psense, prhs, pname) if err != 0 { return nil, model.makeError(err) } @@ -268,23 +292,84 @@ func (model *Model) AddConstrs(vars [][]*Var, val [][]float64, sense []int8, rhs } constrs := make([]*Constr, len(constrname)) - xrows := len(model.constrs) + xrows := len(model.Constraints) for i := xrows; i < xrows+len(constrname); i++ { - model.constrs = append(model.constrs, Constr{model, int32(i)}) - constrs[i] = &model.constrs[len(model.constrs)-1] + model.Constraints = append(model.Constraints, Constr{model, int32(i)}) + constrs[i] = &model.Constraints[len(model.Constraints)-1] } return constrs, nil } - // SetObjective ... -func (model *Model) SetObjective(expr *QuadExpr, sense int32) error { - if err := C.GRBdelq(model.model); err != 0 { +func (model *Model) SetObjective(objectiveExpr interface{}, sense int32) error { + + // Clear Out All Previous Quadratic Objective Terms + if err := C.GRBdelq(model.AsGRBModel); err != 0 { return model.makeError(err) } + + // Detect the Type of Objective We Have + switch objectiveExpr.(type) { + case *LinExpr: + le := objectiveExpr.(*LinExpr) + if err := model.SetLinearObjective(le, sense); err != nil { + return err + } + case *QuadExpr: + qe := objectiveExpr.(*QuadExpr) + if err := model.SetQuadraticObjective(qe, sense); err != nil { + return err + } + default: + return errors.New("Unexpected objective expression type!") + } + + return nil +} + +/* +SetLinearObjective +Description: + Adds a linear objective to the model. +*/ +func (model *Model) SetLinearObjective(expr *LinExpr, sense int32) error { + // Constants + + // Algorithm + for tempIndex, tempVar := range expr.ind { + // Add Each to the objective, by modifying the obj attribute + if err := tempVar.SetObj(expr.val[tempIndex]); err != nil { + return err + } + } + + // if err := model.SetDoubleAttrVars(C.GRB_DBL_ATTR_OBJ, expr.lind, expr.lval); err != nil { + // return err + // } + if err := model.SetDoubleAttr(C.GRB_DBL_ATTR_OBJCON, expr.offset); err != nil { + return err + } + if err := model.SetIntAttr(C.GRB_INT_ATTR_MODELSENSE, sense); err != nil { + return err + } + + // If you successfully complete all steps, then return no errors. + return nil +} + +/* +SetQuadraticObjective +Description: + Adds a quadratic objective to the model. +*/ +func (model *Model) SetQuadraticObjective(expr *QuadExpr, sense int32) error { + // Constants + + // Algorithm if err := model.addQPTerms(expr.qrow, expr.qcol, expr.qval); err != nil { return err } + if err := model.SetDoubleAttrVars(C.GRB_DBL_ATTR_OBJ, expr.lind, expr.lval); err != nil { return err } @@ -294,6 +379,8 @@ func (model *Model) SetObjective(expr *QuadExpr, sense int32) error { if err := model.SetIntAttr(C.GRB_INT_ATTR_MODELSENSE, sense); err != nil { return err } + + // If you successfully complete all steps, then return no errors. return nil } @@ -309,15 +396,15 @@ func (model *Model) addQPTerms(qrow []*Var, qcol []*Var, qval []float64) error { _qrow := make([]int32, len(qrow)) _qcol := make([]int32, len(qcol)) for i := 0; i < len(qrow); i++ { - if qrow[i].idx < 0 { + if qrow[i].Index < 0 { return errors.New("") } - if qcol[i].idx < 0 { + if qcol[i].Index < 0 { return errors.New("") } - _qrow[i] = qrow[i].idx - _qcol[i] = qcol[i].idx + _qrow[i] = qrow[i].Index + _qcol[i] = qcol[i].Index } pqrow := (*C.int)(nil) @@ -329,7 +416,7 @@ func (model *Model) addQPTerms(qrow []*Var, qcol []*Var, qval []float64) error { pqval = (*C.double)(&qval[0]) } - err := C.GRBaddqpterms(model.model, C.int(len(qrow)), pqrow, pqcol, pqval) + err := C.GRBaddqpterms(model.AsGRBModel, C.int(len(qrow)), pqrow, pqcol, pqval) if err != 0 { return model.makeError(err) } @@ -342,7 +429,7 @@ func (model *Model) Update() error { if model == nil { return errors.New("") } - err := C.GRBupdatemodel(model.model) + err := C.GRBupdatemodel(model.AsGRBModel) if err != 0 { return model.makeError(err) } @@ -354,7 +441,7 @@ func (model *Model) Optimize() error { if model == nil { return errors.New("") } - err := C.GRBoptimize(model.model) + err := C.GRBoptimize(model.AsGRBModel) if err != 0 { return model.makeError(err) } @@ -366,7 +453,7 @@ func (model *Model) Write(filename string) error { if model == nil { return errors.New("") } - err := C.GRBwrite(model.model, C.CString(filename)) + err := C.GRBwrite(model.AsGRBModel, C.CString(filename)) if err != 0 { return model.makeError(err) } @@ -379,7 +466,7 @@ func (model *Model) GetIntAttr(attrname string) (int32, error) { return 0, errors.New("") } var attr int32 - err := C.GRBgetintattr(model.model, C.CString(attrname), (*C.int)(&attr)) + err := C.GRBgetintattr(model.AsGRBModel, C.CString(attrname), (*C.int)(&attr)) if err != 0 { return 0, model.makeError(err) } @@ -392,7 +479,7 @@ func (model *Model) GetDoubleAttr(attrname string) (float64, error) { return 0, errors.New("") } var attr float64 - err := C.GRBgetdblattr(model.model, C.CString(attrname), (*C.double)(&attr)) + err := C.GRBgetdblattr(model.AsGRBModel, C.CString(attrname), (*C.double)(&attr)) if err != 0 { return 0, model.makeError(err) } @@ -405,7 +492,7 @@ func (model *Model) GetStringAttr(attrname string) (string, error) { return "", errors.New("") } var attr *C.char - err := C.GRBgetstrattr(model.model, C.CString(attrname), (**C.char)(&attr)) + err := C.GRBgetstrattr(model.AsGRBModel, C.CString(attrname), (**C.char)(&attr)) if err != 0 { return "", model.makeError(err) } @@ -417,7 +504,7 @@ func (model *Model) SetIntAttr(attrname string, value int32) error { if model == nil { return errors.New("") } - err := C.GRBsetintattr(model.model, C.CString(attrname), C.int(value)) + err := C.GRBsetintattr(model.AsGRBModel, C.CString(attrname), C.int(value)) if err != 0 { return model.makeError(err) } @@ -429,7 +516,7 @@ func (model *Model) SetDoubleAttr(attrname string, value float64) error { if model == nil { return errors.New("") } - err := C.GRBsetdblattr(model.model, C.CString(attrname), C.double(value)) + err := C.GRBsetdblattr(model.AsGRBModel, C.CString(attrname), C.double(value)) if err != 0 { return model.makeError(err) } @@ -441,7 +528,7 @@ func (model *Model) SetStringAttr(attrname string, value string) error { if model == nil { return errors.New("") } - err := C.GRBsetstrattr(model.model, C.CString(attrname), C.CString(value)) + err := C.GRBsetstrattr(model.AsGRBModel, C.CString(attrname), C.CString(value)) if err != 0 { return model.makeError(err) } @@ -452,10 +539,10 @@ func (model *Model) SetStringAttr(attrname string, value string) error { func (model *Model) GetDoubleAttrVars(attrname string, vars []*Var) ([]float64, error) { ind := make([]int32, len(vars)) for i, v := range vars { - if v.idx < 0 { + if v.Index < 0 { return []float64{}, errors.New("") } - ind[i] = v.idx + ind[i] = v.Index } return model.getDoubleAttrList(attrname, ind) } @@ -464,10 +551,10 @@ func (model *Model) GetDoubleAttrVars(attrname string, vars []*Var) ([]float64, func (model *Model) SetDoubleAttrVars(attrname string, vars []*Var, value []float64) error { ind := make([]int32, len(vars)) for i, v := range vars { - if v.idx < 0 { + if v.Index < 0 { return errors.New("") } - ind[i] = v.idx + ind[i] = v.Index } return model.setDoubleAttrList(attrname, ind, value) } @@ -477,7 +564,7 @@ func (model *Model) getIntAttrElement(attr string, ind int32) (int32, error) { return 0.0, errors.New("") } var value int32 - err := C.GRBgetintattrelement(model.model, C.CString(attr), C.int(ind), (*C.int)(&value)) + err := C.GRBgetintattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), (*C.int)(&value)) if err != 0 { return 0, model.makeError(err) } @@ -489,7 +576,7 @@ func (model *Model) getCharAttrElement(attr string, ind int32) (int8, error) { return 0, errors.New("") } var value int8 - err := C.GRBgetcharattrelement(model.model, C.CString(attr), C.int(ind), (*C.char)(&value)) + err := C.GRBgetcharattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), (*C.char)(&value)) if err != 0 { return 0, model.makeError(err) } @@ -501,7 +588,7 @@ func (model *Model) getDoubleAttrElement(attr string, ind int32) (float64, error return 0, errors.New("") } var value float64 - err := C.GRBgetdblattrelement(model.model, C.CString(attr), C.int(ind), (*C.double)(&value)) + err := C.GRBgetdblattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), (*C.double)(&value)) if err != 0 { return 0, model.makeError(err) } @@ -513,7 +600,7 @@ func (model *Model) getStringAttrElement(attr string, ind int32) (string, error) return "", errors.New("") } var value *C.char - err := C.GRBgetstrattrelement(model.model, C.CString(attr), C.int(ind), (**C.char)(&value)) + err := C.GRBgetstrattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), (**C.char)(&value)) if err != 0 { return "", model.makeError(err) } @@ -524,7 +611,7 @@ func (model *Model) setIntAttrElement(attr string, ind int32, value int32) error if model == nil { return errors.New("") } - err := C.GRBsetintattrelement(model.model, C.CString(attr), C.int(ind), C.int(value)) + err := C.GRBsetintattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), C.int(value)) if err != 0 { return model.makeError(err) } @@ -535,7 +622,7 @@ func (model *Model) setCharAttrElement(attr string, ind int32, value int8) error if model == nil { return errors.New("") } - err := C.GRBsetcharattrelement(model.model, C.CString(attr), C.int(ind), C.char(value)) + err := C.GRBsetcharattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), C.char(value)) if err != 0 { return model.makeError(err) } @@ -546,7 +633,7 @@ func (model *Model) setDoubleAttrElement(attr string, ind int32, value float64) if model == nil { return errors.New("") } - err := C.GRBsetdblattrelement(model.model, C.CString(attr), C.int(ind), C.double(value)) + err := C.GRBsetdblattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), C.double(value)) if err != 0 { return model.makeError(err) } @@ -557,7 +644,7 @@ func (model *Model) setStringAttrElement(attr string, ind int32, value string) e if model == nil { return errors.New("") } - err := C.GRBsetstrattrelement(model.model, C.CString(attr), C.int(ind), C.CString(value)) + err := C.GRBsetstrattrelement(model.AsGRBModel, C.CString(attr), C.int(ind), C.CString(value)) if err != 0 { return model.makeError(err) } @@ -572,7 +659,7 @@ func (model *Model) getDoubleAttrList(attrname string, ind []int32) ([]float64, return []float64{}, nil } value := make([]float64, len(ind)) - err := C.GRBgetdblattrlist(model.model, C.CString(attrname), C.int(len(ind)), (*C.int)(&ind[0]), (*C.double)(&value[0])) + err := C.GRBgetdblattrlist(model.AsGRBModel, C.CString(attrname), C.int(len(ind)), (*C.int)(&ind[0]), (*C.double)(&value[0])) if err != 0 { return []float64{}, model.makeError(err) } @@ -589,9 +676,15 @@ func (model *Model) setDoubleAttrList(attrname string, ind []int32, value []floa if len(ind) == 0 { return nil } - err := C.GRBsetdblattrlist(model.model, C.CString(attrname), C.int(len(ind)), (*C.int)(&ind[0]), (*C.double)(&value[0])) + err := C.GRBsetdblattrlist(model.AsGRBModel, C.CString(attrname), C.int(len(ind)), (*C.int)(&ind[0]), (*C.double)(&value[0])) if err != 0 { return model.makeError(err) } return nil } + +/* +GetVarByName +Description: + Collects the GRBVar which has the given gurobi variable by name. +*/ diff --git a/gurobi/var.go b/gurobi/var.go index c7ba305..c3ba337 100644 --- a/gurobi/var.go +++ b/gurobi/var.go @@ -2,38 +2,48 @@ package gurobi // Gurobi variable object type Var struct { - model *Model - idx int32 + Model *Model + Index int32 } func (v *Var) GetInt(attr string) (int32, error) { - return v.model.getIntAttrElement(attr, v.idx) + return v.Model.getIntAttrElement(attr, v.Index) } func (v *Var) GetChar(attr string) (int8, error) { - return v.model.getCharAttrElement(attr, v.idx) + return v.Model.getCharAttrElement(attr, v.Index) } func (v *Var) GetDouble(attr string) (float64, error) { - return v.model.getDoubleAttrElement(attr, v.idx) + return v.Model.getDoubleAttrElement(attr, v.Index) } func (v *Var) GetString(attr string) (string, error) { - return v.model.getStringAttrElement(attr, v.idx) + return v.Model.getStringAttrElement(attr, v.Index) } func (v *Var) SetInt(attr string, value int32) error { - return v.model.setIntAttrElement(attr, v.idx, value) + return v.Model.setIntAttrElement(attr, v.Index, value) } func (v *Var) SetChar(attr string, value int8) error { - return v.model.setCharAttrElement(attr, v.idx, value) + return v.Model.setCharAttrElement(attr, v.Index, value) } func (v *Var) SetDouble(attr string, value float64) error { - return v.model.setDoubleAttrElement(attr, v.idx, value) + return v.Model.setDoubleAttrElement(attr, v.Index, value) } func (v *Var) SetString(attr string, value string) error { - return v.model.setStringAttrElement(attr, v.idx, value) + return v.Model.setStringAttrElement(attr, v.Index, value) +} + +func (v *Var) SetObj(value float64) error { + err := v.Model.setDoubleAttrElement("Obj", v.Index, value) + if err != nil { + return err + } + + // Update model and return + return v.Model.Update() } diff --git a/testing/lp_test.go b/testing/lp_test.go new file mode 100644 index 0000000..069c676 --- /dev/null +++ b/testing/lp_test.go @@ -0,0 +1,106 @@ +package testing + +import ( + "fmt" + "testing" + + gurobi "github.com/kwesiRutledge/gurobi.go/gurobi" +) + +func Test_LP1(t *testing.T) { + // Create environment. + env, err := gurobi.NewEnv("lp.log") + if err != nil { + panic(err.Error()) + } + defer env.Free() + + // Create an empty model. + model, err := gurobi.NewModel("lp-hypercube1", env) + if err != nil { + panic(err.Error()) + } + defer model.Free() + + // Add varibles + x, err := model.AddVar(gurobi.CONTINUOUS, 0.0, -gurobi.INFINITY, gurobi.INFINITY, "x", []*gurobi.Constr{}, []float64{}) + if err != nil { + panic(err.Error()) + } + y, err := model.AddVar(gurobi.CONTINUOUS, 0.0, -gurobi.INFINITY, gurobi.INFINITY, "y", []*gurobi.Constr{}, []float64{}) + if err != nil { + panic(err.Error()) + } + + // Integrate new variables. + if err := model.Update(); err != nil { + panic(err.Error()) + } + + // Set Objective function + expr := gurobi.LinExpr{} + expr.AddTerm(x, 1).AddTerm(y, 1) + if err := model.SetObjective(&expr, gurobi.MINIMIZE); err != nil { + panic(err.Error()) + } + + // Add box constraints (four of them) + if _, err = model.AddConstr([]*gurobi.Var{x}, []float64{1}, '<', 1.0, "x_upper_bound"); err != nil { + panic(err.Error()) + } + if _, err = model.AddConstr([]*gurobi.Var{x}, []float64{-1}, '<', 1.0, "x_lower_bound"); err != nil { + panic(err.Error()) + } + if _, err = model.AddConstr([]*gurobi.Var{y}, []float64{1}, '<', 1.0, "y_upper_bound"); err != nil { + panic(err.Error()) + } + if _, err = model.AddConstr([]*gurobi.Var{y}, []float64{-1}, '<', 1.0, "y_lower_bound"); err != nil { + panic(err.Error()) + } + + // Optimize model + if err := model.Optimize(); err != nil { + panic(err.Error()) + } + + // Write model to 'lp-hyperbox1.lp'. + if err := model.Write("lp-hyperbox1.lp"); err != nil { + panic(err.Error()) + } + + // Capture solution information + optimstatus, err := model.GetIntAttr(gurobi.INT_ATTR_STATUS) + if err != nil { + panic(err.Error()) + } + + objval, err := model.GetDoubleAttr(gurobi.DBL_ATTR_OBJVAL) + if err != nil { + panic(err.Error()) + } + + sol, err := model.GetDoubleAttrVars(gurobi.DBL_ATTR_X, []*gurobi.Var{x, y}) + if err != nil { + panic(err.Error()) + } + + fmt.Printf("\nOptimization complete\n") + if optimstatus == gurobi.OPTIMAL { + fmt.Printf("Optimal objective: %.4e\n", objval) + fmt.Printf(" x=%.4f, y=%.4f\n", sol[0], sol[1]) + } else if optimstatus == gurobi.INF_OR_UNBD { + fmt.Printf("Model is infeasible or unbounded\n") + } else { + fmt.Printf("Optimization was stopped early\n") + } + + // Checks + if sol[0] != -1.0 { + t.Errorf("The value of x is %v, not -1.0.", sol[0]) + } + + if sol[1] != -1.0 { + t.Errorf("The value of y is %v, not -1.0.", sol[1]) + } + +} diff --git a/testing/qp_test.go b/testing/qp_test.go new file mode 100644 index 0000000..1f9faa1 --- /dev/null +++ b/testing/qp_test.go @@ -0,0 +1,96 @@ +package testing + +import ( + "fmt" + "testing" + + gurobi "github.com/kwesiRutledge/gurobi.go/gurobi" +) + +func TestQP1(t *testing.T) { + // Create environment. + env, err := gurobi.NewEnv("qp.log") + if err != nil { + panic(err.Error()) + } + defer env.Free() + + // Create an empty model. + model, err := gurobi.NewModel("qp", env) + if err != nil { + panic(err.Error()) + } + defer model.Free() + + // Add varibles + x, err := model.AddVar(gurobi.CONTINUOUS, 0.0, 0.0, gurobi.INFINITY, "x", []*gurobi.Constr{}, []float64{}) + if err != nil { + panic(err.Error()) + } + y, err := model.AddVar(gurobi.CONTINUOUS, 0.0, 0.0, gurobi.INFINITY, "y", []*gurobi.Constr{}, []float64{}) + if err != nil { + panic(err.Error()) + } + z, err := model.AddVar(gurobi.CONTINUOUS, 0.0, 0.0, gurobi.INFINITY, "z", []*gurobi.Constr{}, []float64{}) + if err != nil { + panic(err.Error()) + } + + // Integrate new variables. + if err := model.Update(); err != nil { + panic(err.Error()) + } + + // Set Objective function + expr := gurobi.QuadExpr{} + expr.AddTerm(x, 2).AddQTerm(x, x, 1).AddQTerm(x, y, 1).AddQTerm(y, y, 1).AddQTerm(y, z, 1).AddQTerm(z, z, 1) + if err := model.SetObjective(&expr, gurobi.MINIMIZE); err != nil { + panic(err.Error()) + } + + // First constraint + if _, err = model.AddConstr([]*gurobi.Var{x, y, z}, []float64{1, 2, 3}, '>', 4.0, "c0"); err != nil { + panic(err.Error()) + } + + // Second constraint + if _, err = model.AddConstr([]*gurobi.Var{x, y, z}, []float64{1, 1, 1}, '>', 1.0, "c1"); err != nil { + panic(err.Error()) + } + + // Optimize model + if err := model.Optimize(); err != nil { + panic(err.Error()) + } + + // Write model to 'qp.lp'. + if err := model.Write("qp.lp"); err != nil { + panic(err.Error()) + } + + // Capture solution information + optimstatus, err := model.GetIntAttr(gurobi.INT_ATTR_STATUS) + if err != nil { + panic(err.Error()) + } + + objval, err := model.GetDoubleAttr(gurobi.DBL_ATTR_OBJVAL) + if err != nil { + panic(err.Error()) + } + + sol, err := model.GetDoubleAttrVars(gurobi.DBL_ATTR_X, []*gurobi.Var{x, y, z}) + if err != nil { + panic(err.Error()) + } + + fmt.Printf("\nOptimization complete\n") + if optimstatus == gurobi.OPTIMAL { + fmt.Printf("Optimal objective: %.4e\n", objval) + fmt.Printf(" x=%.4f, y=%.4f, z=%.4f\n", sol[0], sol[1], sol[2]) + } else if optimstatus == gurobi.INF_OR_UNBD { + fmt.Printf("Model is infeasible or unbounded\n") + } else { + fmt.Printf("Optimization was stopped early\n") + } +} diff --git a/testing/var_test.go b/testing/var_test.go new file mode 100644 index 0000000..4100a20 --- /dev/null +++ b/testing/var_test.go @@ -0,0 +1,88 @@ +package testing + +import ( + "testing" + + gurobi "github.com/kwesiRutledge/gurobi.go/gurobi" +) + +/* +TestVar_GetDouble1 +Description: + Tests how we can get the obj value of a given gurobi variable declared using the standard AddVar function. +*/ +func TestVar_GetDouble1(t *testing.T) { + // Create environment. + env, err := gurobi.NewEnv("setobj1.log") + if err != nil { + t.Errorf("There was an issue creating the new Env: %v", err) + } + defer env.Free() + + // Create an empty model. + model, err := gurobi.NewModel("setobj1", env) + if err != nil { + t.Errorf("There was an issue creating the new model: %v", err) + } + defer model.Free() + + // Add varibles + x, err := model.AddVar(gurobi.CONTINUOUS, 0.0, -gurobi.INFINITY, gurobi.INFINITY, "x", []*gurobi.Constr{}, []float64{}) + if err != nil { + t.Errorf("There was an issue adding a variable to the old model: %v", err) + } + + // Test current value of var (0) + initialObjVal, err := x.GetDouble("obj") + if err != nil { + t.Errorf("There was an issue getting the obj value: %v", err.Error()) + } + + if initialObjVal != 0.0 { + t.Errorf("The initial obj value was %v; expected %v", initialObjVal, 0.0) + } +} + +/* +TestVar_SetObj1 +Description: + Tests how we can set the obj value of a given gurobi variable using the var object. +*/ +func TestVar_SetObj1(t *testing.T) { + // Create environment. + env, err := gurobi.NewEnv("setobj2.log") + if err != nil { + t.Errorf("There was an issue creating the new Env: %v", err) + } + defer env.Free() + + // Create an empty model. + model, err := gurobi.NewModel("setobj1", env) + if err != nil { + t.Errorf("There was an issue creating the new model: %v", err) + } + defer model.Free() + + // Add varibles + x, err := model.AddVar(gurobi.CONTINUOUS, 0.0, -gurobi.INFINITY, gurobi.INFINITY, "x", []*gurobi.Constr{}, []float64{}) + if err != nil { + t.Errorf("There was an issue adding a variable to the old model: %v", err) + } + + // Set value of var + + err = x.SetObj(1.0) + if err != nil { + t.Errorf("There was an issue setting the obj value of the variable x: %v", err) + } + + // Retrieve and compare the new obj value of x + newObjVal, err := x.GetDouble("obj") + if err != nil { + t.Errorf("There was an issue getting the obj value: %v", err.Error()) + } + + if newObjVal != 1.0 { + t.Errorf("The new obj value was %v; expected %v", newObjVal, 1.0) + } +} diff --git a/utils/setup.go b/utils/setup.go new file mode 100644 index 0000000..01efd3d --- /dev/null +++ b/utils/setup.go @@ -0,0 +1,443 @@ +/* +setup.go +Description: + An implementation of the file local_lib_info.go written entirely in go. +*/ + +package main + +import ( + "errors" + "fmt" + "os" + "runtime" + "strconv" + "strings" +) + +/* +Constants +*/ +const GoLibraryFilename string = "gurobi/cgoHelper.go" +const CppHeaderFilename string = "gurobi/gurobi_passthrough.h" + +/* +Type Definitions +*/ + +type SetupFlags struct { + GurobiHome string // Directory where Gurobi is installed + GoFilename string // Name of the Go File to + HeaderFilename string // Name of the Headerfile to Create + PackageName string // Name of the package +} + +type GurobiVersionInfo struct { + MajorVersion int + MinorVersion int + TertiaryVersion int +} + +/* +CreateGurobiHomeDirectory +Description: + + Creates the Gurobi Home directory where your gurobi program and helper files should be installed when you run + the Gurobi Installer. +*/ +func CreateGurobiHomeDirectory(versionInfo GurobiVersionInfo) (string, error) { + // Create Base Home Directory + gurobiHome := fmt.Sprintf("/Library/gurobi%v%v%v", versionInfo.MajorVersion, versionInfo.MinorVersion, versionInfo.TertiaryVersion) + + // Create + switch runtime.GOOS { + case "darwin": + // Decide on if we are on intel processors or apple silicon. + switch runtime.GOARCH { + case "amd64": + gurobiHome := fmt.Sprintf("%v/mac64", gurobiHome) + return gurobiHome, nil + case "arm64": + gurobiHome := fmt.Sprintf("%v/macos_universal2", gurobiHome) + return gurobiHome, nil + default: + return "", fmt.Errorf("There was an error finding the architecture on your mac! What is \"%v\"? Send an issue to the gurobi.go repository.", runtime.GOARCH) + } + default: + return "", fmt.Errorf("The operating system that you are using is not recognized: \"%v\".", runtime.GOOS) + } + +} + +func GetDefaultSetupFlags() (SetupFlags, error) { + // Create Default Struct + defaultGurobiVersion := GurobiVersionInfo{9, 0, 3} + defaultHome, err := CreateGurobiHomeDirectory(defaultGurobiVersion) + if err != nil { + return SetupFlags{}, err + } + + mlf := SetupFlags{ + GurobiHome: defaultHome, + GoFilename: GoLibraryFilename, + HeaderFilename: CppHeaderFilename, + PackageName: "gurobi", + } + + // Search through Mac Library for all instances of Gurobi + libraryContents, err := os.ReadDir("/Library") + if err != nil { + return mlf, err + } + gurobiDirectories := []string{} + for _, content := range libraryContents { + if content.IsDir() && strings.Contains(content.Name(), "gurobi") { + fmt.Println(content.Name()) + gurobiDirectories = append(gurobiDirectories, content.Name()) + } + } + + // Convert Directories into Gurobi Version Info + gurobiVersionList, err := StringsToGurobiVersionInfoList(gurobiDirectories) + if err != nil { + return mlf, err + } + + highestVersion, err := FindHighestVersion(gurobiVersionList) + if err != nil { + return mlf, err + } + + // Write the highest version's directory into the GurobiHome variable + mlf.GurobiHome, err = CreateGurobiHomeDirectory(highestVersion) + if err != nil { + return mlf, err + } + + return mlf, nil + +} + +/* +StringToGurobiVersionInfo +Assumptions: + + Assumes that a valid gurobi name is given. +*/ +func StringToGurobiVersionInfo(gurobiDirectoryName string) (GurobiVersionInfo, error) { + //Locate major and minor version indices in gurobi directory name + majorVersionAsString := string(gurobiDirectoryName[len("gurobi")]) + minorVersionAsString := string(gurobiDirectoryName[len("gurobi")+1]) + tertiaryVersionAsString := string(gurobiDirectoryName[len("gurobi")+2]) + + // Convert using strconv to integers + majorVersion, err := strconv.Atoi(majorVersionAsString) + if err != nil { + return GurobiVersionInfo{}, err + } + + minorVersion, err := strconv.Atoi(minorVersionAsString) + if err != nil { + return GurobiVersionInfo{}, err + } + + tertiaryVersion, err := strconv.Atoi(tertiaryVersionAsString) + if err != nil { + return GurobiVersionInfo{}, err + } + + return GurobiVersionInfo{ + MajorVersion: majorVersion, + MinorVersion: minorVersion, + TertiaryVersion: tertiaryVersion, + }, nil + +} + +/* +StringsToGurobiVersionInfoList +Description: + + Receives a set of strings which should be in the format of valid gurobi installation directories + and returns a list of GurobiVersionInfo objects. + +Assumptions: + + Assumes that a valid gurobi name is given. +*/ +func StringsToGurobiVersionInfoList(gurobiDirectoryNames []string) ([]GurobiVersionInfo, error) { + + // Convert Directories into Gurobi Version Info + gurobiVersionList := []GurobiVersionInfo{} + for _, directory := range gurobiDirectoryNames { + tempGVI, err := StringToGurobiVersionInfo(directory) + if err != nil { + return gurobiVersionList, err + } + gurobiVersionList = append(gurobiVersionList, tempGVI) + } + // fmt.Println(gurobiVersionList) + + return gurobiVersionList, nil + +} + +/* +// Iterate through all versions in gurobiVersionList and find the one with the largest major or minor version. +*/ +func FindHighestVersion(gurobiVersionList []GurobiVersionInfo) (GurobiVersionInfo, error) { + + // Input Checking + if len(gurobiVersionList) == 0 { + return GurobiVersionInfo{}, errors.New("No gurobi versions were provided to FindHighestVersion().") + } + + // Perform search + highestVersion := gurobiVersionList[0] + if len(gurobiVersionList) == 1 { + return highestVersion, nil + } + + for _, gvi := range gurobiVersionList { + // Compare Major version numbers + if gvi.MajorVersion > highestVersion.MajorVersion { + highestVersion = gvi + continue + } + + // Compare minor version numbers + if gvi.MinorVersion > highestVersion.MinorVersion { + highestVersion = gvi + continue + } + + // Compare tertiary version numbers + if gvi.TertiaryVersion > highestVersion.TertiaryVersion { + highestVersion = gvi + continue + } + } + + return highestVersion, nil + +} + +func ParseMakeLibArguments(sfIn SetupFlags) (SetupFlags, error) { + // Iterate through any arguments with mlfIn as the default + sfOut := sfIn + + // Input Processing + argIndex := 1 // Skip entry 0 + for argIndex < len(os.Args) { + // Share parsing data + fmt.Println("- Parsed input: %v", os.Args[argIndex]) + + // Parse Inputs + switch { + case os.Args[argIndex] == "--gurobi-home": + sfOut.GurobiHome = os.Args[argIndex+1] + argIndex += 2 + case os.Args[argIndex] == "--go-fname": + sfOut.GoFilename = os.Args[argIndex+1] + argIndex += 2 + case os.Args[argIndex] == "--pkg": + sfOut.PackageName = os.Args[argIndex+1] + argIndex += 2 + default: + fmt.Printf("Unrecognized input: %v", os.Args[argIndex]) + argIndex++ + } + + } + + return sfOut, nil +} + +/* +CreateCXXFlagsDirective +Description: + + Creates the CXX Flags directive in the file that we will use in lib.go. +*/ +func CreateCXXFlagsDirective(sf SetupFlags) (string, error) { + // Create Statement + + gurobiCXXFlagsString := fmt.Sprintf("// #cgo CXXFLAGS: --std=c++11 -I%v/include \n", sf.GurobiHome) + //lpSolveCXXFlagsString := "// #cgo CXXFLAGS: -I/usr/local/opt/lp_solve/include\n" // Works as long as lp_solve was installed with Homebrew + + return gurobiCXXFlagsString, nil +} + +/* +CreatePackageLine +Description: + + Creates the "package" directive in the file that we will use in lib.go. +*/ +func CreatePackageLine(sf SetupFlags) (string, error) { + + return fmt.Sprintf("package %v\n\n", sf.PackageName), nil +} + +/* +CreateLDFlagsDirective +Description: + + Creates the LD_FLAGS directive in the file that we will use in lib.go. +*/ +func CreateLDFlagsDirective(sf SetupFlags) (string, error) { + // Constants + AsGVI, err := sf.ToGurobiVersionInfo() + if err != nil { + return "", err + } + + // Locate the desired files for mac in the directory. + // libContent, err := os.ReadDir(mlfIn.GurobiHome) + // if err != nil { + // return "", err + // } + + ldFlagsDirective := fmt.Sprintf("// #cgo LDFLAGS: -L%v/lib", sf.GurobiHome) + + targetedFilenames := []string{"gurobi_c++", fmt.Sprintf("gurobi%v%v", AsGVI.MajorVersion, AsGVI.MinorVersion)} + + for _, target := range targetedFilenames { + ldFlagsDirective = fmt.Sprintf("%v -l%v", ldFlagsDirective, target) + } + ldFlagsDirective = fmt.Sprintf("%v \n", ldFlagsDirective) + + return ldFlagsDirective, nil +} + +func (mlf *SetupFlags) ToGurobiVersionInfo() (GurobiVersionInfo, error) { + // Split the GurobiHome variable by the name gurobi + GurobiWordIndexStart := strings.Index(mlf.GurobiHome, "gurobi") + GurobiDirNameIndexEnd := len(mlf.GurobiHome) - len("/mac64") - 1 + + return StringToGurobiVersionInfo(string(mlf.GurobiHome[GurobiWordIndexStart : GurobiDirNameIndexEnd+1])) + +} + +func GetAHeaderFilenameFrom(dirName string) (string, error) { + // Constants + + // Algorithm + + // Search through dirName directory for all instances of .a files + libraryContents, err := os.ReadDir(dirName) + if err != nil { + return "", err + } + headerNames := []string{} + for _, content := range libraryContents { + if content.Type().IsRegular() && strings.Contains(content.Name(), ".a") { + fmt.Println(content.Name()) + headerNames = append(headerNames, content.Name()) + } + } + + return headerNames[0], nil + +} + +/* +WriteLibGo +Description: + + Creates the library file which imports the proper libraries for cgo. + By default this is named according to GoLibraryFilename. +*/ +func WriteLibGo(sf SetupFlags) error { + // Constants + + // Algorithm + + // First Create all Strings that we would like to write to lib.go + // 1. Create package definition + packageDirective, err := CreatePackageLine(sf) + if err != nil { + return err + } + + // 2. Create CXX_FLAGS argument + cxxDirective, err := CreateCXXFlagsDirective(sf) + if err != nil { + return err + } + + // 3. Create LDFLAGS Argument + ldflagsDirective, err := CreateLDFlagsDirective(sf) + if err != nil { + return err + } + + // Now Write to File + f, err := os.Create(sf.GoFilename) + if err != nil { + return err + } + defer f.Close() + + // Write all directives to file + _, err = f.WriteString(fmt.Sprintf("%v%v%v import \"C\"\n", packageDirective, cxxDirective, ldflagsDirective)) + if err != nil { + return err + } + + return nil + +} + +/* +WriteHeaderFile +Description: + + This script writes the C++ header file which goes in gurobi.go but references + the true gurobi_c.h file. +*/ +func WriteHeaderFile(sf SetupFlags) error { + // Constants + + // Algorithm + + // Now Write to File + f, err := os.Create(sf.HeaderFilename) + if err != nil { + return err + } + defer f.Close() + + // Write a small comment + import + simpleComment := fmt.Sprintf("// This header file was created by setup.go \n// It simply connects gurobi.go to the local distribution (along with the cgo directives in %v\n\n", GoLibraryFilename) + simpleImport := fmt.Sprintf("#include <%v/include/gurobi_c.h>\n", sf.GurobiHome) + + // Write all directives to file + _, err = f.WriteString(fmt.Sprintf("%v%v", simpleComment, simpleImport)) + if err != nil { + return err + } + + // Return nil if everything went well. + return nil +} + +func main() { + + sf, err := GetDefaultSetupFlags() + + // Next, parse the arguments to make_lib and assign values to the mlf appropriately + sf, err = ParseMakeLibArguments(sf) + + fmt.Println(sf) + fmt.Println(err) + + // Write File + err = WriteLibGo(sf) + if err != nil { + fmt.Println(err) + } + + err = WriteHeaderFile(sf) + +}