2.10 How to make a complete OOP ode solver in Maple?

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

o:=ODE_factory_class:-make_ode(diff(y(x),x)=x,y(x)) 
o:-get_ode() 
                           d 
                          --- y(x) = x 
                           dx 
 
o:-dsolve(); 
o:-get_sol() 
                      [       1  2       ] 
                      [y(x) = - x  + c__1] 
                      [       2          ]