Here, I will start making a complete small OOP ode solver in Maple.
At each step more classes are added and enhanced until we get a fully working small ode solver based on OOP design that solves a first and second order ode, this is to show how it all works. Another solvers can be added later by simply extending the base class.
The base class is called Base_ode_class. There will be Second_order_ode_class and
First_order_ode_class and these classes extend the base ode class. We can later add
higher_order_ode_class.
Next, there are different classes which extend these. There is First_order_linear_ode_class
and First_order_separable_ode_class and so on, and these extend the
First_order_ode_class.
For example, if a user wanted to solve a first order ode which happened to be say
separable, then object of class First_order_separable_ode_class will be created and
used.
Since the user does not know and should not know what object to create, then the factory class will be used. The factory class is what the user initially calls to make the ode object.
It is the factory class which determines which type of class to instantiate based on the ode given.
The factory class is singleton (standard module in Maple, not of type object), which has
the make_ode method which is called by the user. This method parses the ode
and determines its order and then based on the order determine which subclass
to use, and then instantiate this and returns the correct object to the user to
use.
This object will have the dsolve method and other methods the user can use on the
object.
The make_ode method in the factory module accepts only the ode itself the function such
as \(y(x)\). A typical use is given below
ode := ODE_factoy_class:-make_ode( diff(y(x),x)=sin(x), y(x) ); ode:-set_IC(....); ode:-set_hint("the hint"); ..... ode:-dsolve(); #solves the ode ode:-is_solved(); #checks if ode was successfully solved ode:-verify_sol(); #verifies the solution using maple odetest() ode:-is_sol_verified(); #asks if solution is verified ode:-get_number_of_sol(); #returns number of solutions, 1 or 2 etc... ode:-get_sol(); #returns the solutions found in list #and more method ...
Examples at the end will show how all the above works on actual odes’s.
The initial call to make an ode does not have initial conditions, or hint and any other parameters. This is so to keep the call simple. As the factory method only makes the concrete ode object.
Additional methods are then used to add more information if needed by using the returned object itself, such as initial conditions, and hint and so on before calling the dsolve method on the object.
Here is a very basic setup which include the base ode class and extended to first order two sub classes for now.
restart; ODE_factory_class :=module() #notice, normal module. No option object. export make_ode:=proc(ode::`=`,func::function(name),$) local dep_variables_found::list,item; local y::symbol; local x::symbol; local ode_order::integer; if nops(func)<>1 then error("Parsing error, dependent variable must contain one argument, found ", func); fi; y:=op(0,func); x:=op(1,func); if not has(ode,y) then error ("Supplied ode ",ode," has no ",y); fi; if not has(ode,x) then error ("Supplied ode ",ode," has no ",x); fi; if not has(ode,func) then error ("Supplied ode ",ode," has no ",func); fi; ode_order := PDEtools:-difforder(ode,x); #this will check that the dependent variable will show with #SAME argument in the ode. i.e. if y(x) and y(t) show up in same ode, it #will throw exception, which is what we want. try dep_variables_found := PDEtools:-Library:-GetDepVars([y],ode); catch: error lastexception; end try; #now go over dep_variables_found and check the independent #variable is same as x i.e. ode can be y'(z)+y(z)=0 but function is y(x). for item in dep_variables_found do if not type(item,function) then error("Parsing error. Expected ",func," found ",item," in ode"); else if op(1,item) <> x then error("Parsing error. Expected ",func," found ",item," in ode"); fi; fi; od; #now go over all indents in ode and check that y only shows as y(x) and not as just y #as the PDEtools:-Library:-GetDepVars([_self:-y],ode) code above does not detect this. #i.e. it does not check y'(x)+y=0 if numelems(indets(ode,identical(y))) > 0 then error("Parsing error, Can not have ",y," with no argument inside ",ode); fi; if ode_order=1 then RETURN(make_first_order_ode(ode,y,x)); elif ode_order=2 then RETURN(make_second_order_ode(ode,y,x)); else RETURN(make_higher_order_ode(ode,y,x)); fi; end proc; local make_first_order_ode:=proc(ode::`=`,y::symbol,x::symbol) #decide on what specific type the ode is, and make instant of it if first_order_ode_quadrature_class:-is_quadrature(ode,y,x) then RETURN(Object(first_order_ode_quadrature_class,ode,y,x)); elif first_order_ode_linear_class:-is_linear(ode,y,x) then RETURN(Object(first_order_ode_linear_class,ode,y,x)); fi; #and so on end proc; local make_second_order_ode:=proc(ode::`=`,y::symbol,x::symbol) #decide on what specific type the ode is, and make instant of it #same as for first order end proc; end module; #------------------------- module solution_class() option object; local the_solution; local is_verified_solution::truefalse:=false; local is_implicit_solution::truefalse:=false; export ModuleCopy::static:= proc(_self,proto::solution_class,the_solution::`=`,is_implicit_solution::truefalse,$) _self:-the_solution:=the_solution; _self:-is_implicit_solution:=is_implicit_solution; end proc; export get_solution::static:=proc(_self,$) RETURN(_self:-the_solution); end proc; export is_verified::static:=proc(_self,$) RETURN(_self:-is_verified_solution); end proc; export is_implicit::static:=proc(_self,$) RETURN(_self:-is_implicit_solution); end proc; export is_explicit::static:=proc(_self,$) RETURN(not(_self:-is_implicit_solution)); end proc; export verify_solution::static:= overload( [ proc(_self, ode::`=`,$) option overload; local stat; stat:= odetest(_self:-the_solution,ode); if stat=0 then _self:-is_verified_solution:=true; else if simplify(stat)=0 then _self:-is_verified_solution:=true; else _self:-is_verified_solution:=false; fi; fi; end, proc(_self, ode::`=`,IC::list,$) option overload; local stat; stat:= odetest([_self:-the_solution,IC],ode); if stat=[0,0] then _self:-is_verified_solution:=true; else if simplify(stat)=[0,0] then _self:-is_verified_solution:=true; else _self:-is_verified_solution:=false; fi; fi; end ]); end module: #------------------------- module ODE_base_class() option object; local y::symbol; local x::symbol; local func::function(name); #y(x) local ode::`=`; local ode_order::posint; local IC::list:=[]; local parsed_IC::list:=[]; local the_hint::string:=""; local solutions_found::list(solution_class):=[]; #exported getters methods export get_ode::static:=proc(_self,$) RETURN(_self:-ode); end proc; export get_x::static:=proc(_self,$) RETURN(_self:-x); end proc; export get_y::static:=proc(_self,$) RETURN(_self:-y); end proc; export get_ode_order::static:=proc(_self,$) RETURN(_self:-ode_order); end proc; export get_IC::static:=proc(_self,$) RETURN(_self:-IC); end proc; export get_parsed_IC::static:=proc(_self,$) RETURN(_self:-parsed_IC); end proc; export get_sol::static:=proc(_self,$) local L:=Array(1..0): local sol; for sol in _self:-solutions_found do L ,= sol:-get_solution(); od; RETURN(convert(L,list)); end proc; #exported setters methods export set_hint::static:=proc(_self,hint::string,$) #add code to check if hint is valid _self:-the_hint:=hint; end proc; end module; #------------------------- module first_order_ode_quadrature_class() option object(ODE_base_class); local f,g; #ode of form y'=f(x)*g(y) #this method is not an object method. It is part of the module but does #not have _self. It is called by the factory class to find if the ode #is of this type first export is_quadrature:=proc(ode::`=`,y::symbol,x::symbol)::truefalse; RETURN(true); #for now end proc; export ModuleCopy::static:= proc(_self,proto::first_order_ode_quadrature_class,ode::`=`,y::symbol,x::symbol,$) _self:-ode := ode; _self:-y := y; _self:-x := x; _self:-func := _self:-y(_self:-x); _self:-ode_order :=1; end proc; export dsolve::static:=proc(_self,$) local sol,o; #print("Enter first_order_ode_quadrature_class:-dsolve"); #solve the ode for now we will use Maple but in my code #I have my own solver ofcourse. sol:= :-dsolve(_self:-ode,_self:-func); o:=Object(solution_class,sol,false); _self:-solutions_found:= [o]; NULL; end proc; end module: #------------------------- module first_order_ode_linear_class() option object(ODE_base_class); local f,g; #ode of form y'=f(x)*g(y) #this method is not an object method. It is part of the module but does #not have _self. It is called by the factory class to find if the ode #is of this type first export is_linear:=proc(ode::`=`,y::symbol,x::symbol)::truefalse; RETURN(true); #for now end proc; export ModuleCopy::static:= proc(_self,proto::first_order_ode_linear_class,ode::`=`,y::symbol,x::symbol,$) _self:-ode := ode; _self:-y := y; _self:-x := x; _self:-func := _self:-y(_self:-x); _self:-ode_order :=1; end proc; export dsolve::static:=proc(_self,$) local sol,o; sol:= :-dsolve(_self:-ode,_self:-func); o:=Object(solution_class,sol,false); _self:-solutions_found[1]:= [o]: end proc: end module:
Example usage is