Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Advice in designing objects which interact with each other using Fortran

Tags:

oop

fortran

I have been banging my head against a wall all afternoon trying to figure out this problem, so I hope someone can help me.

I have an abstract base class, called base_model (say), which in Fortran2003 looks like:

type, abstract :: base_model
contains
  procedure(initMe), pass(this), deferred :: init ! constructor
  procedure(delMe), pass(this), deferred :: delete ! destructor
  procedure(solveMe), pass(this), deferred :: solve
end type base_model

where, obviously, the abstract procedures initMe, delMe and solveMe are defined using an abstract interface block. I then have three derived classes, called model1, model2 and model3 (say):

type, extends(base_model) :: model1
  double precision :: x,y
contains
  procedure :: init => init_model1
  procedure :: delete => delete_model1
  procedure :: solve => solve_model1
end type model1

type, extends(base_model) :: model2
contains
  procedure :: init => init_model2
  procedure :: delete => delete_model2
  procedure :: solve => solve_model2
end type model2

type, extends(base_model) :: model3
contains
  procedure :: init => init_model3
  procedure :: delete => delete_model3
  procedure :: solve => solve_model3
end type model3

I then have a "controlling" object, called control (say), which extends an abstract base_control:

type, abstract :: base_control
  class(base_model), allocatable :: m1
  class(base_model), allocatable :: m2
  class(base_model), allocatable :: m3
contains
  procedure(initMe), pass(self), deferred :: init
  procedure(delMe), pass(self), deferred :: delete
  procedure(runMe), pass(self), deferred :: run
end type base_control

type, extends(base_control) :: control
contains
  procedure :: init => init_control
  procedure :: delete => delete_control
  procedure :: run => run_control
end type control

The objects m1, m2 and m3 can be allocated into any of the models: model1, model2 or model3, and are "solved" in any particular order depending on which "control" is requested by the user.

The three allocatable objects (m1, m2 and m3) need to pass data between them. Given that they are members of a "controlling" object, I can define a "getter" for each model which then passes the required data into each model. However, the specific models are not known at compile time and therefore, the "control" object does not know what data to get and indeed, the models don't know what data to receive!

For example, if I allocate(model1::m1) (that is, allocate m1 to be of type model1) then it will contain two bits of data double precision :: x,y. Then if m2 is allocated to be of type model2 (allocate(model2::m2)), it could require x but if it is allocated to be of type model3 (allocate(model3::m2)) then it may require y from m1. Therefore, given that the "controlling" object cannot know what type m2 is allocated to be, how can it get the necessary data from m1 to pass into m2?

An additional complication is that the interactions between the models are in general, circular. That is, m1 requires data from m2, m2 requires data from m1 and so-on. Also, the data required is in general not only specific to the models but also variable in both type and quantity.

Unfortunately, the data x and y are not members of base_model and therefore, passing m1 into m2 as an argument would not work either.

So I have the following questions:

  1. Is there a better way to design these objects so that I can pass data between them easily? Looking around on here, there have been some suggestions that the best thing to do is re-design the objects so that the interaction between them is not circular. However, that is kind of necessary here!

  2. Do I have to write a "getter" for each piece of data that might be shared between the objects? That seems like a lot of coding (I have a lot of data which might be shared). However, that also seems rather complicated because the "getter" (specific to a piece of data) would also have to satisfy an abstract interface.

In higher level languages like Python, this would be easy as we could simply create a new data type as a composite of the models but that's not possible, as far as I am aware, in Fortran.

Thanks in advance. Any help is greatly appreciated.

Edit: Following the discussion with francescalus below, select type is an option. Indeed, in the simple example given above, select type would be a good choice. However, in my actual code, this would result in large, nested select types and so if there is a way to do it without using select type I would prefer it. Thanks to francescalus for pointing out my error regarding select type.

like image 220
crispyninja Avatar asked Feb 22 '18 17:02

crispyninja


2 Answers

To answer your two questions:

Is there a better way to design?

I don't quite know why you have so many contraints in your design, but in short yes. You can use a context manager for your models. I recommend you check out this answer: Context class pattern

Do you have to write a getter method on every model?

Not quite, if you use a context strategy on this specific issue, the only thing you need to implement on every model is a setter method that will share the data between models.

I implemented a working solution for this scenario in Python. Code speaks louder than words. I've avoided using any special features of Python to provide you a clear understanding of how to use a context in this case.

from abc import ABC, abstractmethod
import random

class BaseModel(ABC):
    def __init__(self, ctx):
        super().__init__()
        self.ctx = ctx
        print("BaseModel initializing with context id:", ctx.getId())

    @abstractmethod
    def solveMe():
        pass

class BaseControl(object):
    # m1 - m3 could be replaced here with *args
    def __init__(self, m1, m2, m3):
        super().__init__()
        self.models = [m1, m2, m3]


class Control(BaseControl):
    def __init__(self, m1, m2, m3):
        super().__init__(m1, m2, m3)

    def run(self):
        print("Now Solving..")
        for m in self.models:
            print("Class: {} reports value: {}".format(type(m).__name__, m.solveMe()))


class Model1(BaseModel):
    def __init__(self, x, y, ctx):
        super().__init__(ctx)
        self.x = x
        self.y = y
        ctx.setVal("x", x)
        ctx.setVal("y", y)

    def solveMe(self):
        return self.x * self.y

class Model2(BaseModel):
    def __init__(self, z, ctx):
        super().__init__(ctx)
        self.z = z
        ctx.setVal("z", z)

    def solveMe(self):
        return self.z * self.ctx.getVal("x")

class Model3(BaseModel):
    def __init__(self, z, ctx):
        super().__init__(ctx)
        self.z = z
        ctx.setVal("z", z)

    def solveMe(self):
        return self.z * self.ctx.getVal("y")

class Context(object):
    def __init__(self):
        self.modelData = {}
        self.ctxId = random.getrandbits(32)

    def getVal(self, key):
        return self.modelData[key]

    def setVal(self, key, val):
        self.modelData[key] = val

    def getId(self):
        return self.ctxId


ctx = Context()

m1 = Model1(1,2, ctx)
m2 = Model2(4, ctx)
m3 = Model3(6, ctx)

# note that the order in the arguments to control defines behavior
control = Control(m1, m2, m3)
control.run()

Output

python context.py
BaseModel initializing with context id: 1236512420
BaseModel initializing with context id: 1236512420
BaseModel initializing with context id: 1236512420
Now Solving..
Class: Model1 reports value: 2
Class: Model2 reports value: 4
Class: Model3 reports value: 12

Explanation

In short, we create a context class which has a dictionary that can be shared across the different models. This implementation is very specific to the primitive data types you provided (I.E. x, y, z). If you need to compute the data before it is shared across the models you can still use this pattern by replacing the return of solveMe() with a deferred promise.

like image 199
Josue Alexander Ibarra Avatar answered Nov 01 '22 17:11

Josue Alexander Ibarra


FWIW, below is a similar attempt to access fields of another objects based on key/values (*). For simplicity, the main program gets one integer from a child1_t object and sets a complex value to a child2_t object (both of which are extended types of parent_t).

parent.f90:

module parent_m
    implicit none

    type, abstract :: parent_t   !(abstract is optional)
    contains
        procedure :: set
        procedure :: get
        procedure :: show
    endtype

    type composite_t
        class(parent_t), allocatable :: pa, pb
    endtype

contains
    subroutine set( this, key, val )  ! key-based setter
        class(parent_t), intent(inout) :: this
        character(*),   intent(in)     :: key
        integer,        intent(in)     :: val
    endsubroutine

    subroutine get( this, key, val )  ! key-based getter
        class(parent_t), intent(in)  :: this
        character(*),    intent(in)  :: key
        integer,         intent(out) :: val
    endsubroutine

    subroutine show( this )   ! print contents
        class(parent_t), intent(in) :: this
    endsubroutine
end module

child.f90:

module child_m
    use parent_m, only: parent_t
    implicit none

    type, extends(parent_t) :: child1_t
        integer :: n1 = 777   ! some property
    contains
        procedure :: get  => child1_get
        procedure :: show => child1_show
    endtype

    type, extends(parent_t) :: child2_t
        complex :: c2 = ( 0.0, 0.0 )   ! another property
    contains
        procedure :: set  => child2_set
        procedure :: show => child2_show
    endtype

contains

    subroutine child1_get( this, key, val )
        class(child1_t), intent(in)  :: this
        character(*),    intent(in)  :: key
        integer,         intent(out) :: val

        select case( key )
            case ( "num", "n1" ) ; val = this % n1  ! get n1
            case default ; stop "invalid key"
        end select
    end subroutine

    subroutine child1_show( this )
        class(child1_t), intent(in) :: this
        print *, "[child1] ", this % n1
    endsubroutine

    subroutine child2_set( this, key, val )
        class(child2_t), intent(inout) :: this
        character(*),    intent(in)    :: key
        integer,         intent(in)    :: val

        select case( key )
            case ( "coeff", "c2" ) ; this % c2 = cmplx( real(val), 0.0 )  ! set c2
            case default ; stop "invalid key"
        end select
    end subroutine

    subroutine child2_show( this )
        class(child2_t), intent(in) :: this
        print *, "[child2] ", this % c2
    endsubroutine

end module

main.f90:

program main
    use parent_m, only: composite_t
    use child_m,  only: child1_t, child2_t
    implicit none
    type(composite_t) :: c
    integer itmp

    allocate( child1_t :: c % pa )
    allocate( child2_t :: c % pb )

    print *, "initial state:"
    call c % pa % show()
    call c % pb % show()

    call c % pa % get( "num",  itmp )   ! get an integer value from pa
    call c % pb % set( "coeff", itmp )  ! set a complex value to pb

    print *, "modified state:"
    call c % pa % show()
    call c % pb % show()
end

Compile & results:

 $ gfortran parent.f90 child.f90 main.f90

 initial state:
 [child1]          777
 [child2]              (0.00000000,0.00000000)
 modified state:
 [child1]          777
 [child2]              (777.000000,0.00000000)

Though the above code deals with "data transfer" for only integer and complex, other kind of data may be added similaly in the select case construct (without adding new getter/setter methods for each). Further, if necessary, we could overload get() and set() for different types of value (e.g. set_int() and set_real()) via generic keyword (in the parent_t type), and override them in the extended types. The same also holds for value of array type, maybe...

If the transfer (copy) of data between extended types are expensive (e.g., large arrays), I guess the getter could return pointers to them so that childX_t's can communicate in a tightly coupled way (without knowing their implementation).

(But I guess there may be a simpler approach than doing a thing like the above, including Point 1 in the question (e.g., re-design of the program itself). Also, if we have some dictionary type in hand, the use of dictionaries (as in the other answer) seems more appealing to me.)

like image 27
roygvib Avatar answered Nov 01 '22 17:11

roygvib