This is a basic example of using OOP in Maple to implement an ode solver. There is a base module called ode_base_class (I will be using class instead of module, as this is more common in OOP)
This program will for now support first order and second order ode’s only.
The base ode class will contain the basic operations and data which is common to both first order and second order ode’s.
Next, we will make a first order ode class, and second order ode class. Both of these will extend the base ode class.
Next, we will have more ode classes. For example, for first order ode, there will be linear first order ode class, and separable first order ode class, and Bernoulli ode class and so on. Each one these classes will extend the first order ode class which in turn extends the base ode class.
Same for second order ode’s. There will be second order constant coefficients ode class, and second order variable coefficient ode class and so on. Each one of these classes will extend the second order ode class which in turn extends the base ode class.
Let base_ode_class be the base of an ode class which will contain all the necessary
methods that are generic and applicable to an ode of any order and type.
These can be the ode expression itself, its order and any other data and operations which applicable to any ode type.
Now we want to create a first order class. This will have its own operations and private variables that are specific and make sense only to any first order ode.
Then these will be the first order separable ode class, which has methods that implement solving the ode using separable method and has other methods which makes sense only for first order separable ode. The following diagram is partial illustration of the is-A relation among possible classes.
First we define the base ode class and define private variables and method that are common to any ode type.
restart; module base_ode_class() option object; local the_ode; local the_order::integer; export get_ode::static:=proc(_self,$) RETURN(_self:-the_ode); end proc; export get_order::static:=proc(_self,$) RETURN(_self:-the_order); end proc; end module;
Note that the base ode class does not have constructor. Since it is meant to be extended only.
The following is the first order ode class.
module first_order_ode_class() option object(base_ode_class); local initial_conditions; local solution_found; export get_IC::static:=proc(_self,$) RETURN(_self:-initial_conditions); end proc; export get_solution::static:=proc(_self,$) RETURN(_self:-solution_found); end proc; export verify_solution::static:=proc(_self,$)::truefalse; #code to verify if the solution found is valid or not #using odetest() end proc; end module;
The following is the first order separable ode class which extends the above first order ode class.
module first_order_separable_ode_class() option object(first_order_ode_class); local f,g; #ode of form y'=f(x)*g(y) export ModuleCopy::static:= proc(_self,proto::first_order_separable_ode_class, ode, IC,$) _self:-the_ode := ode; _self:-initial_conditions := IC; end proc; export dsolve::static:=proc(_self,$) #solve the ode for now we will use Maple but in my code #I have my own solver ofcourse. _self:-solution_found:= :-dsolve([_self:-the_ode, _self:-initial_conditions]); end proc; end module;
In the above, when we create a instance of first_order_separable_ode_class then
it now have the whole chain of classes into one. i.e. first order separable class
extending the first order class which in turn extends the base ode class. For
example
o:=Object(first_order_separable_ode_class,diff(y(x),x)=3*sin(x)*y(x),y(0)=1) o:-dsolve(); o:-get_solution() #y(x) = exp(3)*exp(-3*cos(x)) o:-get_ode() #diff(y(x), x) = 3*sin(x)*y(x)
The above calls will all work, even though the first order separable class has no
method in it called get_ode but it extends a class which does, hence it works as
it.
Now we will do the same for second order ode’s.
Advantage of this design, is that methods in base classes that are being extended can be reused by any class which extends them. Only methods that applies and specific to the lower level classes need to be implemented.
As we add more specific solvers, we just have to extend the base classes and the new
solvers just need to implement its own specific dsolve and any specific methods and data
that it needs itself.
Ofcourse in practice the above design is not complete as is. The user should not have to specify which class to instantiate, as user does not care what the ode type or class it is. They just want to to do
To solve this problem we have to make a factory method which is called to make the correct instance of the class and return that to the user. The factory method figures out the type of ode and it creates the correct instance of the correct class and returns that. So the call will become
The function make_ode_object above is the main interface the user will call to make an
ode object.
This will be explained next with examples. One possibility is to make the factory function a global function or better, a public method in a utility module. For now, it is given here as standalone function for illustration. The user calls this method to make an object of the correct instance of the ode. Here is complete implementation of all the above including the factory method.
#factory method. Makes objects for users make_ode_object:=proc(ode::`=`,func::function(name)) local x,y,the_order; y:=op(0,func); x:=op(1,func); the_order := PDEtools:-difforder(ode,x); if the_order=1 then RETURN(first_order_ode_class:-make_ode_object(ode,func)); elif the_order=2 then #RETURN(second_order_ode_class:-make_ode_object(ode,func)); #implement later NULL; else error "Only first and second order ode's are currently supported"; fi; end proc: ######################### module base_ode_class() option object; local the_ode; local the_order::integer; #methods bound to the object export get_ode::static:=proc(_self,$) RETURN(_self:-the_ode); end proc; export get_order::static:=proc(_self,$) RETURN(_self:-the_order); end proc; end module: ################## module first_order_ode_class() option object(base_ode_class); local initial_conditions; local solution_found; #public factory method not bound to the object. export make_ode_object:=proc(ode::`=`,func::function(name)) local x,y,ode_type::string; y:=op(0,func); x:=op(1,func); ode_type:="separable"; #code here which determined first order ode type if ode_type="separable" then RETURN( Object(first_order_separable_ode_class,ode,func)); elif ode_type="linear" then RETURN( Object(first_order_linear_ode_class,ode,func)); fi; #more ode types added here end proc; #methods bound to the object export get_IC::static:=proc(_self,$) RETURN(_self:-initial_conditions); end proc; export get_solution::static:=proc(_self,$) RETURN(_self:-solution_found); end proc; export verify_solution::static:=proc(_self,$)::truefalse; #code to verify if the solution found is valid or not #using odetest() end proc; end module: ################## module first_order_separable_ode_class() option object(first_order_ode_class); local f,g; #ode of form y'=f(x)*g(y) export ModuleCopy::static:= proc(_self,proto::first_order_separable_ode_class,ode,func::function(name),$) _self:-the_ode := ode; end proc; export dsolve::static:=proc(_self,$) #print("Enter first_order_separable_ode_class:-dsolve"); #solve the ode for now we will use Maple but in my code #I have my own solver ofcourse. _self:-solution_found:= :-dsolve(_self:-the_ode); NULL; end proc; end module:
It is used as follows