starwing / amoeba

a Cassowary constraint solving algorithm implements in pure C.
MIT License
177 stars 24 forks source link

Swift wrapper #5

Open hfossli opened 7 years ago

hfossli commented 7 years ago

I am looking into creating a wrapper around this api in Swift. I'm facing some difficult choices.

All variables are created using a solver and the solvers am_Allocf. This makes it hard to create an object oriented model as they are tied to the solver pretty closely. This is seen throughout the API as well. Example:

AM_API int  am_add    (am_Constraint *cons)

To me I now think it is not feasible to create an object oriented swift model around Amoeba unless variables, terms and constraints can be created independently from the solver.

Methods like

AM_API int  am_add    (am_Constraint *cons)

would have to be rewritten to

AM_API int  am_add    (am_Constraint *cons, am_Solver *solver)

This is not a request to change anything. I'm just thinking loudly :) amoeba.lua is not a wrapper, but the whole implementation written in Lua if I understand correctly?

starwing commented 7 years ago

I'm writing a C++11 wrapper, too. And I also find this issue.

the amoeba.lua is a pure Lua implement, but lua_amoeba.c is real a Lua binding for C implement of amoeba. In this binding, I still need a solver to create variable/constraint.

this API is intend to solve the pure C memory management issue. all objects are under the same memory pool. so a solver is likely a arena. you could allocate any object you want in this arena, and after am_delsolver(), all objects are freed.

So, in my C++11 solution, I'm trying to construct constraint in pure C++ way, recording all variable and terms it used, and when add to solver, it will be convert into amoeba object. In this way, you could create object apart from solver itself.

Another way is always using the amoeba object, something like this:

Solver solver;
Variable xl(solver), xm(solver), xr(solver);

solver.add((xl + 10 < xr) / amoeba::Required);

notice that you could add a solver pointer into variable, so creating a constraint object is easy. this is the way of the Lua binding.

I don't know which is better, amoeba is a pure C library, means I must manage all memory it use. So I could not create objects part from it (or you had call am_del\ on any object you created, just like other C libraries).

We could work out some better solution, any feedback are welcome :-)

starwing commented 7 years ago

A idea, if I add some routines to copy/assignment data across solver, maybe we could make a static solver for allocate temporary objects, and copy it into the solver it will be added.

hfossli commented 7 years ago

So, in my C++11 solution, I'm trying to construct constraint in pure C++ way, recording all variable and terms it used, and when add to solver, it will be convert into amoeba object. In this way, you could create object apart from solver itself.

Yep, trying that out now. Seems promising.

hfossli commented 7 years ago

Can you explain int am_mergeconstraint(am_Constraint *cons, am_Constraint *other, double multiplier)? Does it just add all terms from another constraint?

What happens when a constraint already is added to the solver and you change the constraint? Example: add a new term after the constraint has been added.

starwing commented 7 years ago
  1. yes, it merge two constraint from same solver. merge(a > 10, a > 20) == 2*a > 30 if you treat constraint as expression, merge means add two expression.
  2. it will return AM_FAILED, after constraint added to solver, all changes to it will fail and return failure.
hfossli commented 7 years ago
  1. I like it. This is different from Kiwi, but I like it!
  2. Brilliant!
starwing commented 7 years ago

constraint is just like expression, e.g. x >= 10 means x - 10 >= 0, so constraint only have two state (kiwi has three state, >=, <= and ==)

for example, if you want spec a + b <= 10 + c, it become this:

newconstant() ==> 0.0 >= 0.0
addterm(a, 1.0) ==> a >= 0.0
addterm(b, 1.0) ==> a + b >= 0.0
setrelation(<=) ==> -a -b >= 0.0
addconstant(10.0) ==> -a -b + 10.0 >= 0.0
addconstant(c, 1.0) ==> -a -b + 10.0 + c >= 0.0

notice that if you set relation to <= or ==, all terms before this will be negative, and following terms are positive. and if you set relation to >=, all terms before this remaining position, and following added terms will be negative when you add.

so, the constraint always in form term >= 0 or term == 0, this makes merge constraint possible.

hfossli commented 7 years ago

Thanks for that thorough explanation! Very useful!

I am making progress here. This is f***ing amazing! This is done using only pointers and no wrappers (except for the solver)

        do {
            let solver = AMSolver() // solver is a swift solver class
            let x = solver.createVariable() // x is a `am_Variable *`
            let c = solver.createConstraint(strength: Strength.required) // c is a `am_Constraint *`
            try c.setRelation(.eq)
            try c.addTerm(variable: x, multiplier: 1.0)
            try c.addConstant(-10)
            try c.addToSolver()
            print("value of x: \(x.value)")
        } catch let error {
            print("Error \(error)")
        }

Outputs

value of x: 10.0

I also tested creating wrappers around variable, term and constraint

        do {
            let solver = Solver() // solver is a swift `Solver` class
            let x = Variable() // x is a swift `Variable` class
            let c = Constraint() // c is a swift `Constraint` class
            c.relation = .eq
            c.strength = Strength.required
            c.add(term: Term(variable: x, multiplier: 1.0))
            c.add(constant: -10)
            try solver.add(constraint: c)
            print("value of x: \(solver.value(x))")
        } catch let error {
            print("Error \(error)")
        }

Outputs

value of x: 10.0

I will check more closely wether I need this object oriented approach or if I kind just extend the pointers in Swift. I kind of like the first alternative. It is the least memory intensive, but I have a couple of questions. What happens if someone uses c or `x´ after the solver has been deleted?

I'm impressed how easy it is to use this c api's like this from Swift.

starwing commented 7 years ago

you can't.

the solver is just like a arena, all object allocated by it. after delete solver, all object relate with it (variables, constraints, etc) are all freed, if you still have these pointer, they will become wild pointer.

so in Lua/C++11 wrapper, I make a weak-referenced mapping between object and solver, if delete the solver, I will clear all pointers in object.

hfossli commented 7 years ago

I see! But what if I call am_usevariable() and friends? Still wild pointers?

hfossli commented 7 years ago

notice that if you set relation to <= or ==, all terms before this will be negative, and following terms are positive. and if you set relation to >=, all terms before this remaining position, and following added terms will be negative when you add.

This logic will need to be duplicated/added to a pure swift constraint I guess. Very useful that you took the time to explain it.

starwing commented 7 years ago

yes, even if you call usevariable.

And you needn't duplicate this logic, even needn't do e.g. Merge the same variable. I just recorded all expressions using a vector, and leave things when add constraints into solver.

hfossli commented 7 years ago

Good idea. Can I see the preliminary c++ code?

starwing commented 7 years ago

Can swift override operators? If it can't,you just need bind the current interface, and if it can, you could record the expressions, yet. Because operators always create new object, amoeba objects are cheap, but swift objects may cheaper.

hfossli commented 7 years ago

Yep, I'm creating operator overloads as we speak :)

starwing commented 7 years ago

I'm still working on it(and changing API when necessary), after finished I will upload it. And if you need new functionality I can add them for you. :-)

starwing commented 7 years ago

You could do some benchmarks to determine whether swift or amoeba object cheaper, and deciding how to implement operator overrides

hfossli commented 7 years ago

I'm currently at this point

let solver = Solver()
let v1 = Variable()
let v2 = Variable()
try solver.add(v1 / 2 == v2 * 3)
try solver.add(v1 + v1 - v1 == 8)
print("value of v1: \(solver.value(v1))")
print("value of v2: \(solver.value(v2))")
value of v1: -8.0
value of v2: -1.33333333333333

http://i.giphy.com/l3E6qj3wFoUiXbxuw.gif

In native swift classes I have now Solver, Variable, Term, Expression, Constraint. I had to create Expression in order to compile time enforce that it isn't possible to say a == b <= 3 etc.

starwing commented 7 years ago

the correct way to avoid a == b <= 3 is using typing:

Expression = Variable
Expression = constant
Expression = Expression + Expression
Expression = Expression - Expresxsion
Expression = Expression * constant
Expression = constant * Expression
Expression = Expression / constant
Constraint = Expression >= Expression
Constraint = Expression <= Expression
Constraint = Expression == Expression
Constraint = Constraint / constant       (set strength for constraint)

this is how kiwi and my wrapper done.

hfossli commented 7 years ago

Yep, that's what Rhea did as well. søn. 20. nov. 2016 kl. 14.11 skrev Xavier Wang notifications@github.com:

the correct way to avoid a == b <= 3 is using typing:

Expression = Variable Expression = Expression + Expression Expression = Expression - Expresxsion Expression = Expression * constant Expression = Expression / constant Constraint = Expression >= Expression Constraint = Expression <= Expression Constraint = Expression == Expression

this is how kiwi and my wrapper done.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/starwing/amoeba/issues/5#issuecomment-261777532, or mute the thread https://github.com/notifications/unsubscribe-auth/ADe7602Bsb8Va7TB9aMKGxS87oL9M8Uzks5rAEbkgaJpZM4K3P9q .

hfossli commented 7 years ago

Currently

Should we keep all in one repo or should it be in separate? I'm in favor of keeping them all together if the API is similar. My swift wrapper may support string parsing etc as well, but I don't know if that's too much.

Any thoughts on this?

starwing commented 7 years ago

Yes, I prefer together, too. should we build separate folders for different bindings? pull requests welcome :-)

hfossli commented 7 years ago

Sounds good. I'm packing quite a lot into the swift wrapper. So I think I will divide my code as following

It may be necessary to place a amoeba.podspec file on root and maybe also several additional files. If that's a deal breaker then I fully understand.

starwing commented 7 years ago

yes, that seems good.

hfossli commented 7 years ago

How's the c++ project going btw?

starwing commented 7 years ago

because C++ project only one header, so I plan to put it direct into this project :-)

I have not much time for the end of year :-( so I will put the source later, as soon as I have time, sorry.

hfossli commented 7 years ago

I see. How big is that file (line count)?