MRC-CSO-SPHSU / LoneParentsModel.jl

An initial implementation of an ABM for social and child care
0 stars 4 forks source link

better modularity? #48

Open mhinsch opened 2 years ago

mhinsch commented 2 years ago

Mixers.jl makes it possible to directly "assemble" a struct from parts:

@mix struct Bla1
    x :: Int
end

@mix struct Bla2
    y :: Float64
end

@Bla1 @Bla2 struct Meep
    z :: String
end

Meep will then have three fields, x, y and z (with the corresponding types). This would make the implementation of modular agent types a lot more lightweight as the combined type can be treated like a plain struct with all variables as direct members.

On the other hand we would lose the clear separation between modules, so it would be easier to for example accidentally access a BasicInfo property in Family.

Anyway, I don't have a clear opinion on this yet, but I thought it might be worth thinking about.

mhinsch commented 2 years ago

Another advantage of the Mixers approach - since all fields are inlined in the combined agent type (as opposed to separated into sub-structs) we get much less memory fragmentation (which can substantially affect performance).

AtiyahElsheikh commented 2 years ago

Looks good! Just a question. With respect to the clear separation, may be it is convenient to setup good naming convention, e.g. Bla1Bla2Meep Is there any constraints, e.g. user-defined constructors?

mhinsch commented 2 years ago

Good question re constructors. That might actually be a drawback of Mixers.jl, but I'll have to look into it.

AtiyahElsheikh commented 2 years ago

I believe that can be useful. But I can imagine situations where it is nice to have hierarchies of structs (rather than having all so flat)

[mutable] struct Meep
   bla1::Bla1
   bla2::Bla2
   z::string 
end 

or

[mutable] struct MeepBla1
   bla1::Bla1
   meep::Meep 
end

So probably, mixture of both style would be relevant.

mhinsch commented 2 years ago

Ok, no provision at all for constructors it seems, so the constructor for the combined type would have to forward to functions that set the respective properties. In the end that's not that much of a difference to the delegate system we are using now, though, as we now also have to forward to the component constructors (in the constructor of Person for example).

mhinsch commented 2 years ago

We have a few issues with similar topics (issues #90, #73, #52), but this one is possibly closest to the fundamentals.

The following is a bit lengthy and mostly just a way to sort my thoughts. I don't think we should do anything immediately (finishing the translation has higher priority), but it's maybe something worth thinking about now and then.

On the technical level a simulation consists of several parts:

Ideally we would like to keep the simulation modular, so that we can easily add and remove (model) processes (e.g. add social care or remove income). The most common and methodologically most straightforward, but least flexible way to do this is to make the model as generic as possible and parameterise all effects. A model without social care would then for example simply be the same model (code), but with effect of social care on everything else set to 0.

A more elegant and flexible approach would be to try and keep the code itself modular in such a way that pieces can be combined with very little effort to produce a more complicated model that includes several different processes. Ideally these pieces could also be implemented independently, without having to know too much about other parts of the model.

For independent processes this can be as easy as 'first apply transition A, then apply transition B' where A and B live in separate modules with separate state objects (agents), parameters and setup routines. In reality it's usually more complicated than that, however.

Put differently - adding or removing a process usually requires:

On the face of it this makes it look as if every combination of model parts will have to be an ad-hoc, manual process. However, I think there might be a way to do this mostly automatically.

I think it should be possible to come up with a set of macros so that we could combine parts like this (or similar):

agents2d.jl

@model_part Agents2D begin

  @params begin
    start_x :: Int
    start_y :: Int
  end

  @agent begin
    x :: Int
    y :: Int
    energy :: Float64
    age :: Int
  end
end

#...
# functions

agents3d.jl

@model_part Agents3D begin

  @params begin
    start_x :: Int
    start_y :: Int
    start_z :: Int
  end

  @agent begin
    x :: Int
    y :: Int
    z :: Int
  end
end

#...
# functions

model.jl

include("agents2d.jl")
include("agents3d.jl")

@model Agents2D3D [Agents2D, Agents3D]

The @model macro would automatically declare agent and parameter types with overlapping properties removed. The functions would still have to be done somewhat manually, but with judicious use of Julia's overloading the effort could be kept minimal, I think.