Installation
You can install the library via pip:
pip install kiwicalc
Importing
There are several ways to import the library.
Either import the whole thing and abbreviate it:
import kiwicalc as kw
Or import specific elements from the library:
from kiwicalc import ...
Or import everything like that:
from kiwicalc import *
Solving Equations
You can solve a variety of equations using our library by using
methods or classes in order to solve them. Creating new objects will enable more
features, but it's usually less memory performant than using methods.
Linear Equations
The quickest way to solve a linear equation is to use the solve_linear
method.
The method accepts a string that represents the equation, and optionally also a collection
of
the
variables_dict in the equation. For example:
from kiwicalc import solve_linear
linear_equation = "3x + 7 = -x -5"
result = solve_linear(linear_equation)
print(result)
If you want to speed up this method, specify the variables that appear in it:
result = solve_linear(linear_equation, ('x',))
You can also use the LinearEquation
class, which offers
more features.
class LinearEquation
class LinearEquation(Equation):
Properties
equation
- the string that represents the linear equation
variables
- a list of the variables that appear in the equation
num_of_variables
- the number of variables that appear in the equation
first_side
- a string that represents the first side of the equation
second_side
- a string that represents the second side of the equation
solution
- the solution of the equation
variables_dict
- An internal dictionary that represents the coefficients
of the variables in the equation, when all moved to the first side.
Create a new LinearEquation
object
def __init__(self, equation: str, variables: Iterable = None, calc_now: bool = False):
You have to enter the equation as a string. For instance: "3x+5 = 8x-4"
.
In addition, you can specify the variables that appear in the equation, so we won't have
to figure it out ourselves. This results in a significant runtime improvement (even though
it's pretty fast regardless).
You can also set the calc_now
parameter to True
if you want
that the equation will be solved when the LinearEquation
object is created.
For instance:
my_equation = LinearEquation("3x + 5 = 8", variables=('x',))
Fetching the solutions of the equation
You can either get the solutions with the solve()
method, or the
solution
property. For example:
my_equation = LinearEquation("3x + 5 = 8", variables=('x',))
print(my_equation.solution)
print(my_equation.solve())
Solve while printing the steps to the solution
my_equation = LinearEquation("3x - 7 + 2x + 6 = 4x - 4 + 6x")
print(my_equation.show_steps())
Output:
This method is not well optimized yet, but it works pretty well!
Simplifying the equation
You can simplify the linear equation (without necessarily solving it) via the
simplify()
method. For instance:
my_equation = LinearEquation("4 + 3x + 2y + 5x + 6 = 2 + x - y", variables=('x', 'y'))
my_equation.simplify()
print(my_equation)
Plot the solution
If the equation indeed has a solution, you can plot it as the intersection of two linear
functions. Here is the signature of the method:
def plot_solution(self, start: float = -10, stop: float = 10, step: float = 0.01, ymin: float = -10,
ymax: float = 10, show_axis=True, show=True, title: str = None, with_legend=True):
my_equation = LinearEquation("3x - 7 + 2x + 6 = 4x - 4 + 6x")
print(my_equation.plot_solution())
Output:
Polynomial Equations
Short sum up
If you don't know the degree of the polynomial equation ( linear, quadratic, cubic ... ),
and
you just want it solved,
you can use the solve_polynomial() method.
The method accepts either the
coefficients of the polynomials or a string that represents the equation
and returns a set of solutions.
The method picks the method of solving according to the length of the coefficients.
For instance, the coefficients `[1,6,8]` represent the equation `x^2 + 6x + 8` and therefore
the quadratic formula will be used. Pay attention that entering the coefficients directly
will be faster than entering a string, if speed is a significant consideration for you.
Here is the signature of the method:
def solve_polynomial(coefficients, epsilon: float = 0.000001, nmax: int = 10_000):
For example:
print(solve_polynomial([1, 0, 0, 0, 0, 0, 2, 0, 0, -18]))
print(solve_polynomial("x^2 + 3x^3 + 12x + 5 = 2x -4 + x^2"))
You can also enter a string that represents a polynomial (and not an equation) as well:
print(solve_polynomial("x^2 + 6x + 8"))
Alternatively, you can create a PolyEquation object, and call the solve()
method
or the solution property.
This can be much simpler, but might be a bit less efficient.
For example:
# Solving with PolyEquation objects
from kiwicalc import PolyEquation
poly_equation = PolyEquation("x^2 - 8x = 15")
print(poly_equation.solution)
However, if you know for instance that the equation is of degree 2,3 or 4, then you should
use the appropriate methods for each degree, which are explained below.
Quadratic Equations
In order to solve quadratic equations you can use the solve_quadratic()
method.
The method accepts 3 parameters: `a`, `b`, and `c` which represent the coefficients in
the formula: `ax^2 + bx +c`. The method will compute 2 solutions, whether they are real or
complex.
If you are only interested in the real solutions, you can use the
solve_quadratic_real()
method. Of course, the method internally uses the known quadratic formula:
`x = (-b +- sqrt(b^2-4ac))/(2a) .`
For example, lets solve the equation `x^2 + 6x + 8` and `x^2 + x + 2` with both of the
methods
solutions = solve_quadratic(1,6,8)
real_solutions = solve_quadratic_real(1,6,8)
print(solutions)
print(real_solutions)
And now lets solve the equation `x^2 + x + 1` with both of the methods:
solutions2 = solve_quadratic(1,1,2)
real_solutions2 = solve_quadratic_real(1,1,2)
print(solutions2)
print(real_solutions2)
You can also solve quadratic equations with parameters via the
solve_quadratic_params
.
For instance:
a, b, c = Var('a'), Var('b'), Var('c')
results = solve_quadratic_params(a, b, c)
print(result[0])
print(result[1])
It's also possible to use the
QuadraticEquation
class for a wider variety of
operations on quadratic equations.
class QuadraticEquation
class QuadraticEquation(Equation):
Properties
The QuadraticEquation
class has the same properties as the
LinearEquation
class, since both of them inherit from the Equation
class.
create a new QuadraticEquation
object
You can create a new QuadraticEquation
object by entering a string that
represents the equation, and optionally you can also enter variables that appear in the
equation. For instance:
my_equation = QuadraticEquation("x^2 + 6x + 8 = 0")
Solving the equation
You can solve the quadratic equation with solve()
method.
Here is the signature of the method:
def solve(self, mode='complex'):
You can choose how to solve the quadratic equation via the mode
parameter:
'complex'
- solve for all the solutions, including complex solutions
'real'
- solve only for the real solutions
For instance:
my_equation = QuadraticEquation("x^2 + 6x + 8 = 0")
solution = my_equation.solve()
print(solution)
my_equation = QuadraticEquation("x^2 + 6x + 8 = 0")
solution = my_equation.solve('real')
print(solution)
Get a simplified equation
You can simplify quadratic expressions via the simplified_str()
method.
Currently, it's only available for quadratic equations with 1 variable. For instance:
my_equation = QuadraticEquation("x^2 + 2x + 4x + 8 = 0")
print(my_equation)
print(my_equation.simplified_str())
Get the coefficients of the equation
You can get the coefficients of the equation via the coefficients()
method.
For instance:
my_equation = QuadraticEquation("x^2 + 6x + 8 = 0")
print(my_equation.coefficients())
Generate a random quadratic equation
You can generate a random quadratic equation via the static method
random()
. The method accepts several optional parameters:
values
- the range of the coefficients in the equation. Default:
(-15, 15)
digits_after
-
the number of digits after the decimal point of the solutions. Default: 0
variable
- a string that represents the variable which appears in the
equation.
strict_syntax
- a boolean value that determines whether the equation
will be created in the traditional form of `ax^2 + bx + c = 0`.
The default is True
.
get_solutions
- if set to True, the function will return both
the equation and the solutions of the equation as a tuple of (equation,
solutions)
.
For example:
print(QuadraticEquation.random())
print(QuadraticEquation.random(digits_after=1, variable='y'))
Export to PDF worksheets
You can use the random_worksheet()
method to create a PDF document with
1 page of equations, and optionally also an additional page of solutions.
Here is the signature of the method:
@staticmethod
def random_worksheet(path:str=None, title="Quadratic Equations Worksheet", num_of_equations=20,
solutions_range=(-15, 15), digits_after: int = 0, get_solutions=True):
For instance:
print(QuadraticEquation.random_worksheet("worksheet1.pdf", title="Example"))
You can also create a PDF worksheets with several pages via the
random_worksheets()
method. Here is the signature of the method:
@staticmethod
def random_worksheets(path=None, num_of_pages=2, equations_per_page=20, titles=None,
solutions_range=(-15, 15), digits_after: int = 0, get_solutions=False):
For instance:
print(QuadraticEquation.random_worksheets("worksheet2.pdf", num_of_pages=5))
Cubic Equations
Lets move to cubic equations, namely, equations in the form `ax^3+bx^2+cx+d` where
`a!=0`. You can solve cubic equations with the solve_cubic()
method. The method
accepts four real coefficients: `a`, `b`, `c`, `d`, and returns a tuple of the 3 solutions.
If you are only interested in the real solutions, you can use the
solve_cubic_real
method.
For example, lets solve the equation `x^3 + 3x^2 - 4x - 8`.
solutions = solve_cubic(1, 3, -4, -8)
real_solutions = solve_cubic_real(1, 3, -4, -8)
print(solutions)
print(real_solutions)
You can also use the CubicEquation
class, which offers the same methods
in the QuadraticEquation
class. For instance:
my_equation = CubicEquation("x^3 + 3x^2 - 4x - 8 = 0")
print(my_equation.solution)
print(my_equation.coefficients())
print(my_equation.random())
Quartic Equations
You can also solve quartic equations, namely, equations in the form `ax^4 + bx^3 + cx^2 + dx
+ e`
with the solve_quartic()
method.
The method accepts the 5 coefficients:
`a`,
`b`,
`c`, `d`, `e`, and returns the 5 solutions ( either real or complex ). For example, lets solve
the equation `x^4 + 3x^2 - 6x + 1`:
solution = solve_quartic(1, 0, 0, 0, -16) # x^2 - 16 = 0
print(solution)
# output:
# [(2+0j), 2j, -2j, (-2+0j)]
Source: http://www.1728.org/quartic2.htm
You can also use the QuarticEquation
class, which offers the same methods
in the QuadraticEquation
class. For instance:
my_equation = QuarticEquation("x^4 - 16 = 0")
print(my_equation.solution)
print(my_equation.coefficients())
print(my_equation.random())
According to Abel's impossibility theorem, a general formula for solving equations of degree
5 or
higher does not exist. Therefore, in order to solve such equations, we can use use numerical
methods.
These methods are iterative - they generate closer approximations to the solutions on each
iteration, so after a certain number of iterations the approximations converge to the
solutions.
Hopefully this general explanation of numerical root-finding methods was clear enough for
you to
understand. If not, that's ok, since you don't
have to know how the library works in order to use it.
Polynomial equations of degree 5 or more
You can solve these kind of equations with the numerical root-finding methods which were
implemented in the library, so I recommend you look at the
numerical methods section of this documentation.
In short, if you wish to find only 1 root, you can use the
newton_raphson() and its
counterparts, but if you wish to find all of the roots, you can use the
durand_kerner() method or the aberth_method() method.
# Add examples here
Solving A System Of Equations
As to this version, you can only solve systems of linear and polynomial equations.
Other systems of equations might be available as a beta feature in the next version or
update.
System of linear equations
You can solve a system of linear equations via the
solve_linear_system()
method. The method accepts a collection of strings
where each string is a linear equation, and optionally also a collection of the variables
that appear in the equation. By entering the variables yourself instead
of letting the method to deduce them, you save runtime. Here is the signature of method:
def solve_linear_system(equations, variables=None):
The method will return a dictionary with the
variables as keys and the solutions as the values. Internally, the
Gaussian Elimination method is used to solve the system.
For example:
solutions = solve_linear_system(("3x - 4y + 3 = -z + 9", "-3x + 5 -2z = 2y - 9 + x", "2x + 4y - z = 4"))
print(solutions)
class LinearSystem
You can also use the LinearSystem
class. Creating a LinearSystem
object will be a bit slower, but will provide with some helpful features, like converting the
equation
system to a matrix. For more on the LinearSystem class, go to the
LinearSystem section. For instance, lets declare the LinearSystem
object.
Properties
variables_dict
- a list of all of the variables_dict in the
equations
- a list of all of the equations(list of strings) in the system.
linear_system = LinearSystem(("3x - 4y + 3 = -z + 9", "-3x + 5 -2z = 2y - 9 + x", "2x + 4y - z = 4"))
print(linear_system.get_solutions())
# linear_system.solutions - will also work
Systems of polynomial equations
You can solve a system of polynomial equations using the
solve_poly_system
method. Further elaboration on the method is written on the
numerical methods section of this documentation. Here is an example of using the method to find
the solutions of the system:
$$
\left\{
\begin{array}{c}
x^2 + y^2 = 25 \\
2x + 3y = 18 \\
\end{array}
\right.
$$
with the initial guesses `X_0 = [[2],[1]]`
# Solving systems of polynomial equations via KiwiCalc
solutions = solve_poly_system(["x^2 + y^2 = 25", "2x + 3y = 18"], {'x': 2, 'y': 1})
print(solutions)
# output: '{'x': 3.0000001628514434, 'y': 3.999999891432371}'
Plotting Methods
The module contains a set of plotting functions with
matplotlib. These methods are extremely easy to use -
you don't have to engage with the relatively sophisticated technical details involving
matplotlib's interface. Although many classes in KiwiCalc provide plotting abilities,
you don't have to learn any of them to use these methods.
scatter_dots()
You can scatter dots in 2D via the scatter_dots()
method.
Here is the signature of the method:
def scatter_dots(x_values, y_values, title: str = "", ymin:float=-10, ymax:float=10,show_axis=True,
show=True):
For example:
scatter_dots([1,2,3,4],[2,4,6,8], title="Matplotlib is awesome")
Output:
scatter_dots_3d()
You can scatter dots in 3D via the scatter_dots_3d()
method.
Here is the signature of the method:
def scatter_dots_3d(x_values, y_values, z_values, title: str = "", fig=None,
ax=None, show_axis=True, show=True):
For example:
scatter_dots_3d([4, 2, 8, -5, 3, 8, 9, -6, 1], [1, 9, -2, 3, 4, 5, 1, 6, 7], [1, -1, 3, 4, 8, 1, 4, 2, 6])
Output:
plot_function()
You can plot functions with only variable on a 2D axis system via the
plot_function()
method. Here is the signature of the method:
def plot_function(func: Union[Callable, str], start: float = -10, stop: float = 10,
step: float = 0.01, ymin: float = -10, ymax: float = 10, title=None, show_axis=True, show=True,
fig=None, ax=None, formatText=True, values=None):
Parameters
func
- A function to plot. We offer a variety of ways to enter this function.
You can enter any matching callable object, such as lambda expressions and Function objects.
You can even enter a string that represents the function. This is as simple as it gets...
start
- Where to start plotting
stop
- Where to stop plotting
step
- Interval between each `x`
ymin
- Smallest `y` in the scope
ymax
- Biggest `y` in the scope
title
- The title of the graph
show_axis
- Whether to show the `x` and `y` axis or not
( True
/ False
)
show
- Whether to show the graph or not
( True
/ False
)
fig
- An existing matplotlib figure to plot in. In order to use
this parameter, you must also the ax parameter.
ax
- An existing axis in matplotlib. In order to use
this parameter, you must also the fig parameter.
formatText
- An experimental feature: whether to try formatting the text
into latex or not. Currently it's slow and therefore the default is False
.
However, in later versions, this might be more advanced.
values
- Instead of specifying a range via the start
,
stop
and step
parameters, you can specify the `x` values yourself.
For instance:
plot_function("f(x) = x^2")
Output:
plot_function(Function("f(x) = sin(x)"))
plot_function(lambda x: 2*x)
plot_function_3d()
You can plot functions with 2 variables on a 3D axis system via the
plot_function_3d()
method. Here is the signature of the method:
def plot_function_3d(given_function:"Union[Callable, str]", start: float = -3, stop: float = 3, step: float = 0.1, xlabel: str = "X Values",
ylabel: str = "Y Values", zlabel: str = "Z Values"):
For instance:
plot_function_3d("f(x,y) = sin(x)*cos(y)")
Output:
scatter_function()
You can scatter functions with only 1 variables on a 2D axis system via the
scatter_function()
method. Here is the signature of the method:
def scatter_function(func: Union[Callable, str], start: float = -10, stop: float = 10,
step: float = 0.5, ymin: float = -10, ymax: float = 10, title="", show_axis=True, show=True):
For example:
scatter_function("f(x) = sin(x)")
Output:
scatter_function(lambda x:x**2)
scatter_function(Function("f(x) = 2x"))
scatter_function_3d()
You can scatter functions with 2 variables on a 3D axis system via the
scatter_function_3d()
method. Here is the signature of the method:
def scatter_function_3d(func: "Union[Function, str]", start: float = -10, stop: float = 10,
step: float = 0.5, title=None, show=True):
For example:
scatter_function_3d("f(x,y) = x + y")
import math
scatter_function_3d(lambda x, y: math.sin(x)*math.cos(y))
scatter_function_3d(Function("f(x,y) = xy"), title="Hi there!")
plot_functions()
You can plot several functions with only 1 variable on a 2D axis system via the
plot_functions()
method. Here is the signature of the method:
def plot_functions(functions, start: float = -10, stop: float = 10, step: float = 0.01, ymin: float = -10,
ymax: float = 10, text: str = None,
show_axis: bool = True, show: bool = True):
For example
plot_functions(["f(x) = 2x", "f(x) = x^2", "f(x) = sin(x)"])
plot_functions_3d()
You can plot several functions with 2 variables on a 3D axis system via the
plot_functions_3d()
method. Here is the signature of the method:
def plot_functions_3d(functions: "Iterable[Union[Callable, str, IExpression]]", start: float = -5, stop: float = 5,
step: float = 0.1,
xlabel: str = "X Values",
ylabel: str = "Y Values", zlabel: str = "Z Values"):
For instance:
plot_functions_3d(["f(x,y) = sin(x)*cos(y)", "f(x,y) = sin(x)*ln(y)"])
Output:
If the plot is too laggy, try increasing the step
parameter.
plot_multiple()
You can plot several functions on different figures via the plot_multiple()
method. Here is the signature of the method:
def plot_multiple(funcs, shape: Tuple[int, int] = None, start: float = -10, stop: float = 10,
step: float = 0.01, ymin: float = -10, ymax: float = 10, title=None, show_axis=True, show=True,
values=None):
The method accepts parameters:
funcs
- a collection of functions - you can enter strings, lambda expressions,
Function
objects, algebraic expressions, etc.
shape(optional)
A tuple of two integers that represents the shape of the
figures - for instance, a grid of 3x3 figures.
If not specified, the best fitting shape will be chosen automatically.
start(optional)
Where to start plotting from - float
.
Default is -10
stop(optional)
Where to end plotting - float
.
The default is 10
step(optional)
- The distance between each x value in the plotting range, of
type float
. The default is 0.01. Smaller steps result in higher accuracy
but it makes the plotting process much slower and often cause lags.
ymin(optional)
- the smallest `y` value that will be visible in the scope
of the plot, of type float
. The default is -10.
ymax(optional)
- the highest `y` value that will be visible in the scope, of
type float
. The default is 10.
title(optional)
- The title of the plot.
show_axis(optional)
- Whether to show the axis or not in the plot.
The default is True
.
show(optional)
- Whether to show the plot. The default is True.
values(optional)
- Default is None
.
If this parameter is not None, its value will be used as the range of x values to plot in
instead of the start
, stop
, and step
parameters.
For instance:
plot_multiple(["f(x) = 4x", "f(x) = x^2", "f(x) = x^3", "f(x)= 8", "f(x)=ln(x)", "f(x)=e^x", "f(x)=|x|", "f(x)=sin(x)", "f(x)=cos(x)"])
Output:
plot_complex()
You can plot a complex number or several complex numbers via the plot_complex()
method. Here is the signature of the method:
def plot_complex(*numbers: complex, title:str="", show=True):
Parameters
*numbers
- complex numbers to plot
title
- specify a title for the plot
show
- whether to show the plot or not
( True
/ False
)
For example:
plot_complex(complex(5, 4), complex(3, -2))
Output:
Class Function
You can quickly declare mathematical functions, execute them, and plot them via the Function
class. You can also find the roots of the function numerically and also its minimum and maximum
points depending on the type of the function, Support for derivatives, partial derivatives,
and integrals is supported or developed for certain types of functions.
Properties
function_string
- A string that represents the function. For instance,
"f(x) = x^2"
function_signature
- The signature of a function is the function declaration on the
function_string
property. For instance, "f(x)"
, "g(x,y)"
.
function_expression
- A string that represents only the expression of the
function;
For instance, the expression of the function "f(x) = x^2"
lambda_expression
- A lambda expression that represents the function.
If one cannot be generated, it will be set to None
.
variables_dict
- A list of all of the variables_dict that appear in the
function. For
instance,
it will be ['x','y']
for the function "f(x,y) = x+y"
.
num_of_variables
- the number of variables_dict that appear in the function.
-
classification
- Experimental feature - classifying functions.
The classification is presented via the class Function.Classification
.
Namely, the Classification class is nested within the Function class, and inherits from
Enum
.
class Classification(Enum):
linear = 1,
quadratic = 2,
polynomial = 3,
trigonometric = 4,
logarithmic = 5,
exponent = 6,
constant = 7,
command = 8,
linear_several_parameters = 8,
non_linear_several_parameters = 9,
exponent_several_parameters = 10,
predicate = 11
# The __init__ method's signature
def __init__(self, func=None):
There are several ways to create a new Function object:
-
Entering a string in mathematical format
The string representation of the function must follow this syntax:
"function_name(a,b,c...) = ......"
For example:
# Creating a new Function object, with a string in math syntax
sine = Function("f(x)=sin(x)")
-
Entering a string in a lambda syntax
You can also define your function with a lambda-like syntax.
Either in a more pythonic syntax:
# Creating a Function object via a string in python-like lambda expression syntax.
sine = Function("lambda x:sin(x)")
-
or in a more C# or Javascript like manner:
sine = Function("x => sin(x)")
You can also enter strings that represent lambdas with multiple parameters, separated
by
commas.
For instance, a function that takes three parameters, and returns their sum:
three_sum = Function("x, y, z => x + y + z")
-
Entering a Mono or Poly expression
You can also create a function by entering a monomial (Mono) and polynomial(Poly).
These two classes are documented later in this documentation.
For example:
x = Var('x')
func = Function(-x**2 + 6*x + 7)
Keep in mind
that while this method is rather simple, it's a bit slower, since extra steps are being
taken for
the conversions.
-
Entering a lambda expression
Entering a lambda expression as a parameter is not highly recommended, however, it will
work
rather
fast if, and only if, you declare the lambda expression inside the constructor, and not
in
previous
lines.
For example:
from math import sin,pi
sine = Function(lambda x:sin(x))
print(sine(pi/2))
# output:
# 1.0
def __call__(self, *parameters):
Parameters
*parameters - an unlimited number of parameters.
these parameters will be assigned respectively to
the Function's object variables_dict.
For example:
parabola = Function("f(x)=x**2")
print(parabola(5))
Output:
25.0
three_sum = Function("g(a,b,c)=a+b+c")
print(three_sum(6,5,4))
# output:
#15.0
# Creating functions that return True or False
equality = Function("f(a,b) = a==b")
print(equality(5,5))
print(equality(4,6))
# output:
# True
# False
from math import pi
trigo_op = Function("f(x) = -sin(x) + 2cos(2x)")
print(trigo_op(math.pi/2))
# output:
# -3.0
Getting the variables that appear in the function
functions can contain several variables, and sometimes it is necessary to find out what
variables_dict does a function contain. For that, you can either use the
variables
property, or the square brackets operator.
If you choose to use the square brackets operator, you can enter an index to the function
or use list slicing. If an index is given to the function the variable name (type str) in
the
specified index will be returned. In case of list slicing, a list of all function's
variables_dict between the specified indices will be returned.
For example:
# Retrieving the variables appearing in a function
three_sum = Function("g(a,b,c)=a+b+c")
print(three_sum.variables)
print(three_sum[0])
print(three_sum[1:3])
Derivative of a function
As for this version, only linear and polynomial derivatives are fully implemented.
Trigonometric derivatives are also available, but the support is still basic and needs to be
extended. A larger variety of derivatives will be supported in the future.
# The method's signature
def derivative(self):
The method accepts no parameters and returns a Function object that represent the derivative
of the original function.
for example:
func = Function("f(x)=x**2+2x-5")
print(func.derivative())
Partial Derivatives
As for now, partial derivatives are only supported in linear and polynomial functions.
The support is expected to be extended in the next versions.
func = Function("f(x,y) = x^2 * y^2")
print(func.partial_derivative('x'))
print(func.partial_derivative('y'))
print(func.partial_derivative('xy'))
Plotting
You can plot Function objects with the plot()
method.
The plotting is done behind the scenes with the python library
matplotlib, as for this version. Functions with two variables_dict
will automatically be plotted in 3D space.
def plot(self, start: float = -10, end: float = 10, step: float = 0.01, ymin: float = -10, ymax: float = 10,
others: "Optional[Iterable[Function]]" = None, show_axis=True, show=True):
Parameters:
You can adjust the graph by modifying these settings:
Name |
Type |
Default Value |
Meaning |
start |
float |
`-10` |
The `x` value that plotting starts from |
end |
float |
`10` |
The `x` value in which the plotting ends |
step |
float |
`0.05` |
The distance between each computed point in the `x` axis |
ymin |
float |
`-10` |
The lowest y value that is shown (without scrolling down the graph) |
ymax |
float |
`10` |
The highest y value that is shown (without scrolling up the graph) |
show_axis |
True |
True |
Whether to draw an axis system along with the graph, or not |
show |
True |
True |
Whether to show the graph or not. |
- start: type float, the number to start plotting from. Default value is -10.
- end: type float, Plotting from start to end
- step:type float, the interval between each dot in the graph. Default value is 0.01.
-
scatter: type bool, if set to True, the graph will be scattered and not plotted. Default
value: False
- ymin: type float, the minimum value of y in the graph's perspective.
- ymax: type float, the maximum value of y in the graph's perspective.
- others: other functions that you wish to plot together with the current function
- show_axis: whether to show the axis or not
- show - whether to show the graph, or not
For example, lets plot the function `f(x) = x^2` when `-10 <= x <= 10`. And lets customize
the interval between each x value to `0.1`, by setting the step parameter.
Smaller distances between each dot will result in better accuracy of the function, but it
will
also take more time.
# Plotting ( via matplotlib )
fn = Function("f(x) = x^2")
fn.plot(start=-10, stop=10, step=0.1)
You can also define and plot some more sophisticated and versatile functions.
For example, lets plot the function
`f(x) = xe^{sin(x)} - 3ln(abs(4x))`
# Plotting ( via matplotlib )
example_function = Function("f(x)=xe^sin(x)-3ln(|4x|)")
example_function.plot(start=-10, stop=10)
Scattering
You can scatter functions via the scatter()
, scatter2d()
and
scatter3d()
methods. The scatter method chooses whether to use
the scatter2d()
method or the scatter3d()
method, depending on
the number of variables. If you already know whether to scatter in 2d or 3d, you should
use the scatter2d()
and scatter3d()
methods, as they are
more explicit and offer more parameters. Here are the signatures of the three methods:
def scatter(self, start: float = -15, end: float = 15, step: float = 0.1, ymin=-15, ymax=15, show_axis=True,show=True):
def scatter2d(self, start: float = -15, stop: float = 15, step: float = 0.3, ymin=-15, ymax=15, show_axis=True,
show=True, basic=True):
def scatter3d(self, start: float = -3, stop: float = 3,
step: float = 0.3,
xlabel: str = "X Values",
ylabel: str = "Y Values", zlabel: str = "Z Values", show=True, fig=None, ax=None,
write_labels=True, meshgrid=None, title=""):
For example:
fn = Function("f(x) = x^2")
fn.scatter2d()
fn1 = Function("f(x,y) = sin(x) * cos(y)")
fn1.scatter3d()
Additional Experimental Feature:
If the basic
parameter in the scatter2d()
is set to
False
, the points will contain labels. This may be more convenient, but it's
also slower. For instance:
fn2 = Function("f(x) = x^2")
fn2.scatter2d(basic=False)
Context Manager
You can use a context manager for a Function object, namely, using the with
keyword.
That way, the Function object will be deleted once the code goes out of the scope of the
with segment.
with Function("f(x) = x^2") as fn:
pass # Do some stuff here
class FunctionCollection
The FunctionCollection
class is used in order to represent a collection of
functions, namely, a collection of Function
objects.
Properties
functions
- returns a list of the Function
objects
in the collection.
num_of_functions
- returns the number of the Function
objects in
the collection.
__init__()
You can create a new instance FunctionCollection
by entering
Function
objects or strings that represent the functions.
Here is the signature of the method:
def __init__(self, *functions, gen_copies=False):
In addition, if gen_copies
is set to True
, the collection will
take the copies of the items instead of the originals. Here are some examples:
functions = FunctionCollection(Function("f(x) = x^2"), Function("g(x) = sin(x)"), Function("h(x) = 2x"))
print(functions)
functions = FunctionCollection("f(x) = x^2", "g(x) = sin(x)", "h(x) = 2x")
functions = FunctionCollection(Function("f(x) =x^2"), "g(x) = sin(x)", Function("h(x) = 2x"))
The output will be the same in all of these ways:
1. f(x)=x**2
2. g(x)=sin(x)
3. h(x)=2x
add_function()
You can add new functions to the collection after initialization via the
add_function
method. You can either enter a Function
object or a
string that represent a function.For instance:
functions.add_function(Function("f(x) = 2x"))
functions.add_function("f(x) = 2x")
extend()
You can add a collection of functions via the extend()
method.
The method takes a collection of either Function
objects or strings.
Here is the signature of the method:
def extend(self, functions: Iterable[Union[Function, str]]):
For example:
functions.extend(["f(x,y) = x+y", "g(x,y) = cos(x)+cos(y)", "h(x,y) = sin(x)*sin(y)"])
clear()
Pretty straightforward, you can use the clear()
method to clear the collection
of functions. After that, the collection will considered empty.
is_empty()
The function will return true
if the collection is empty, otherwise,
False
. For example:
functions = FunctionCollection()
print(functions.is_empty())
functions.add_function("f(x) = ln(x)")
print(functions.is_empty())
The output is:
True
False
functions = FunctionCollection(Function("f(x) =x^2"), "g(x) = sin(x)", Function("h(x) = 2x"))
functions.clear()
print(functions.is_empty())
The output is
True
random_function()
Fetch a random Function
object from the collection. For example:
functions = FunctionCollection(Function("f(x) =x^2"), "g(x) = sin(x)", Function("h(x) = 2x"))
print(functions.random_function())
And the output is:
h(x)=2x
random_value()
Fetch a random value from a random function in the collection.
Parameters:
a
- lower bound of the range of numbers to pick the parameters from.
b
- higher bound of the range of numbers
mode
- if mode is "int"
, integer parameters will be randomly
picked via random.randint(a, b)
, if mode is "float"
,
parameters of type float
will be randomly chosen via the
random.uniform(a, b)
method. The default is "int"
.
For example:
functions = FunctionCollection(Function("f(x) =x^2"), "g(x) = sin(x)", Function("h(x) = 2x"))
print(functions.random_value(1, 10))
functions = FunctionCollection("f(x,y) = x + y", "g(x,y) = x - y", "h(x,y) = sin(x) * cos(y)")
print(functions.random_value(1, 10))
plot()
You can plot all of the functions on a shared axis system via the plot()
method. Here is the signature of the method:
def plot(self, start: float = -10, stop: float = 10,
step: float = 0.01, ymin: float = -10, ymax: float = 10, text=None, show_axis=True, show=True):
For example:
functions = FunctionCollection("f(x,y) = x + y", "g(x,y) = x - y", "h(x,y) = sin(x) * cos(y)")
functions.plot()
scatter()
You can scatter all of the functions on a shared axis system via the plot()
method. Here is the signature of the method:
def scatter(self, start: float = -10, stop: float = 10,
step: float = 0.01, ymin: float = -10, ymax: float = 10, text=None, show_axis=True, show=True):
For instance:
functions = FunctionCollection("f(x,y) = x + y", "g(x,y) = x - y", "h(x,y) = sin(x) * cos(y)")
functions.scatter()
__len__()
You can also fetch the number of functions in the collection via the len()
method. For instance:
functions = FunctionCollection("f(x) = 2x", "g(x) = x^2", "h(x) = sin(x)")
print(len(functions))
Output:
3
class FunctionChain
You can execute several functions on the parameters via the FunctionChain
class.
For instance, lets define some functions: `f(x) = 2x`, `g(x) = x^2`, and `h(x) = x-2`.
They will be executed one after another, where each function's input is the last function's
output: `h(g(f(x)))`. For instance, for `x = 4`, `f(4) = 8`, `g(8) = 64` and `h(64) = 62`.
Therefore, the final output will be `62`.
The FunctionChain
class inherits from FunctionCollection
, since
it's a special collection of functions. Therefore, you can also use methods from
FunctionCollection
here.
Creating a new instance
You can create a new instance of FunctionChain
via the
__init__()
method by entering functions, exactly the same like creating
a FunctionCollection
object.
Here is the signature of the method:
def __init__(self, *functions):
For example:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5", "h(x) = sin(x)")
In addition, you can also create new instances via the chain()
method in
the Function
class. For instance:
function_chain = Function("f(x) = x^2").chain(Function("f(x)=x+5")).chain(Function("h(x) = sin(x)"))
Execute
You can call all of the functions via the execute_all()
method, or the built
in __call__() method.
For example:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5", "h(x) = sin(x)")
print(function_chain.execute_all(1))
print(function_chain(1))
Output:
0.8414709848078965
0.8414709848078965
Customized Execution
You can execute the functions in a reverse order via the execute_reverse()
method.In addition, you can execute only certain functions from the collection via the
execute_indices()
method. The method accepts a collection of indices
in the collection, and execute the functions in these indices by the specified order. For
instance:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5", "h(x) = sin(x)")
print(function_chain.execute_reverse()) # execute in reverse order
print(function_chain.execute_indices([3, 1, 0, 2]))
Plotting
You can plot the FunctionChain
object via the plot()
method,
Here is the signature of method:
def plot(self, start: float = -10, stop: float = 10,
step: float = 0.01, ymin: float = -10, ymax: float = 10, text=None, show_axis=True, show=True,
values=None):
For example:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5", "h(x) = sin(x)")
function_chain.plot()
Scattering
You can also scatter the functions via the scatter()
method. For instance:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5", "h(x) = sin(x)")
function_chain.plot()
chain()
You can chain another function to the collection by using the chain()
method. For instance:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5")
function_chain.chain("h(x) = sin(x)")
Indexers
You can fetch the item by its index via the []
operator.
For instance:
function_chain = FunctionChain("f(x) = x^2", "g(x) = x+5")
print(function_chain[0])
PDF Worksheets
You can generate PDF worksheets in different mathematical subjects and languages
either via the worksheet()
method or via a system of classes.
These classes allow you to customize each exercise so you can create worksheets
with exercises of different topics and languages. Also, they offer a wider variety of
exercises, while you can only generate equations with the worksheet()
method as for this version.
worksheet()
You can create a PDF worksheet document with exercises of the same topic via the
worksheet()
method. You also get an additional pages of final solutions as default.
Here are the parameters of the method:
path
- a string that represents the path of the file. Default is
None
,
so a path is generated automatically if not specified.
dtype
- A string that represents the type of the equations. The default is
'linear'
.
The possible Values are:
'linear'
'quadratic'
'cubic'
'quartic'
'polynomial'
'trigo'
'log'
num_of_pages
- the number of pages in the document
(not including the solutions). Default is 1.
equations_per_page
- The number of equations per page. The default is 20.
get_solutions
- Whether to add pages with final solutions. Default is
True
.
digits_after
- The maximum amount of digits after the decimal points of the
solutions.
titles
- a list of strings that represent the titles of the pages
(including the solutions). Default is None
.
For example:
worksheet()
worksheet("linearWorkheet.pdf", num_of_pages=10)
worksheet("myWorksheet.pdf", dtype='quadratic')
Now lets cover the classes.
PDFWorksheet
class PDFWorksheet:
The PDFWorksheet
class represents the whole PDF document.
Properties
pages
- a list of the pages in the document of type PDFPage
.
num_of_pages
- the number of pages in the document
(including the solutions)
current_page
- the current page that we're working on, of type
PDFPage
Create a new PDFWorksheet
object
You can create a new PDFWorksheet
object via the __init__()
method. Here are the parameters of the method:
title
- a string for the title of the first page. Default is
"Worksheet"
.
ordered
- whether to number the exercises and the solutions.
Default is True
.
Just pay attention that when you create a new document, the first page is created
automatically as well, so you don't have to add the first page.
add_exercise()
The method accepts an exercise of type PDFExercise
or one of its
subclasses, and adds it to the current page.
end_page()
Ending the current page. This will make sure to add a page of solutions if needed
next_page()
Add a new page, and move to the new page. After calling it, a new blank page is added
and we can add exercises to it.
create()
creates the PDF file in the given path.
If the path is not given, the PDF file will be generated in a random path in the current
working directory.
PDFPage
class PDFPage:
This class represents a single page in the PDF document.
Usually, you won't have to access it and modify it directly.
Properties
exercises
- a list of the exercises in the page.
title
- the title of the page
Create a new PDFPage
Parameters to the constructor:
title
- the title of the page. Default is "Worksheet"
exercises
- a list of exercises in the page. Default is None
Iterate on the page
You can iterate on the list of the exercises in the page directly. For instance:
for exercise in page:
....
PDFExercise
class PDFExercise:
This class represents an exercise in a PDF page.
Properties:
exercise
- A string that represents the exercise.
number
- The serial number of the exercise in the document.
dtype
- The datatype of the exercise (of type str)
solution
- A string that represents the solution of the exercise.
has_solution
- Whether the exercise comes with a solution or not.
lang
- the language of the exercise and the solution ( if there is one).
Creating a new exercise
Usually, you'll be creating new exercises via the subclasses.
Here are the available exercises as for this version:
PDFLinearFunction
- Analyze a linear function.
Here is the signature of the constructor:
def __init__(self, with_solution: bool = True, lang: str = 'en'):
For instance:
exercise = PDFLinearFunction()
PDFLinearIntersection
- Find intersection between two linear functions.
Here is the signature of the constructor:
def __init__(self, with_solution=True, lang='en'):
PDFLinearSystem
- Solve a linear system of equations.
Here is the signature of the constructor:
def __init__(self, with_solution=True, lang='en', num_of_equations=None, digits_after: int = 0):
For instance:
exercise = exercise = PDFLinearSystem()
PDFLinearFromPoints
- Find a linear function from two points.
Here is the signature of the constructor:
def __init__(self, with_solution: bool = True, lang: str = "en"):
For instance:
exercise = PDFLinearFromPoints()
PDFLinearFromPointSlope
- Find a linear function from a point and
a given slope. Here is the signature of the constructor:
def __init__(self, with_solution: bool = True, lang: str = "en"):
For instance:
exercise = PDFLinearFromPointAndSlope()
PDFPolyFunction
- Analyze a polynomial function.
Here is the signature of the method:
def __init__(self, with_solution: bool = True, degree: int = None, lang: str = 'en'):
For instance:
exercise = PDFPolyFunction()
PDFQuadraticFunction
- Analyze a quadratic function.
Here is the signature of the method:
def __init__(self, with_solution: bool = True, lang: str = "en"):
For instance:
exercise = PDFQuadraticFunction()
PDFCubicFunction
- Analyze a cubic function.
Here is the signature of the constructor:
def __init__(self, with_solution: bool = True, lang: str = "en"):
For instance:
exercise = PDFCubicFunction()
PDFQuarticFunction
- Analyze a quartic function.
Here is the signature of the constructor:
def __init__(self, with_solution: bool = True, lang: str = "en"):
For instance:
exercise = PDFQuarticFunction()
PDFLinearEquation
- Solve a linear equation.
Here is the signature of the constructor:
def __init__(self, with_solution=True, number: int = None):
For instance:
exercise = PDFLinearEquation()
PDFQuadraticEquation
- Solve a quadratic equation.
Here is the signature of the constructor:
def __init__(self, with_solution=True, number: int = None):
For instance:
exercise = PDFQuadraticEquation()
PDFCubicEquation
- Solve a cubic equation.
Here is the signature of the constructor:
def __init__(self, with_solution=True, number: int = None):
For instance:
exercise = PDFCubicEquation()
PDFQuarticEquation
- Solve a quartic equation.
Here is the signature of the constructor:
def __init__(self, with_solution=True, number: int = None):
For instance:
exercise = PDFQuarticEquation()
PDFPolyEquation
- Solve a polynomial equation.
Here is the signature of the constructor:
def __init__(self, with_solution=True, number: int = None):
For instance:
exercise = PDFPolyEquation()
Here is a full example of creating a worksheet.
pdf_worksheet = PDFWorksheet("Functions")
pdf_worksheet.add_exercise(PDFCubicFunction(lang='es'))
pdf_worksheet.add_exercise(PDFQuadraticFunction())
pdf_worksheet.add_exercise(PDFLinearFromPointAndSlope())
pdf_worksheet.end_page()
pdf_worksheet.next_page("Equations")
for _ in range(10):
pdf_worksheet.add_exercise(PDFLinearEquation())
pdf_worksheet.add_exercise(PDFQuadraticEquation())
pdf_worksheet.end_page()
pdf_worksheet.create("worksheetExample.pdf")
Class IExpression
IExpression
is an abstract class that all of the algebraic expressions
in this library inherit from. Therefore, knowing this interface is imperative for a deeper
understanding of the library.
Abstract methods
assign()
- assigning a value to an expression. For example,
the function call assign(x=5)
means that x will be replaced with 5
every time it appears in the expression. You can assign multiple variables at
the same time. For instance: assign(x=5, y=4)
try_evaluate()
- Try to evaluate the algebraic expression
to int
or float
. If not possible, None
will be returned.
variables
- An abstract property. Each algebraic expression must have
this property, that returns a collection of the variables that appear in it
(a list of strings).
__iadd__()
- the +=
operator
__isub__()
- the -=
operator.
__imul__()
- the *=
operator.
__ipow__()
- the **=
operator.
__itruediv__()
- the /=
operator.
__neg__()
- the negative sign operator. For instance: `-x`
__copy__()
- Copying the object.
__str__()
- A string representation of the expression.
simplify()
- try to simplify the expression.
to_dict()
- convert the object to a dictionary
from_dict()
(static method) - create a new expression from a matching
dictionary.
__eq__()
- the ==
operator.
__ne__()
- the !=
operator.
Methods
when()
- assign values, but on a copy of the original expression.
__add__()
- the +
operator - applying +=
on a copy of the expression.
__radd__()
- Same as __add__()
__sub__()
- the -
operator - applying -=
on a copy of the expression.
__mul__()
- the *
operator - applying *=
on a copy of the expression.
__rmul__()
- the *
operator - applying *=
on a copy of the expression.
__pow__()
- the **=
operator - applying **=
on a copy of the expression.
__abs__()
- the abs()
built in method. An Abs
object will be returned as default.
reinman()
- Reinman's method for finding integrals numerically.
Here is the signature of the method:
def reinman(self, a: float, b: float, N: int):
trapz()
- The Trapezoid method for finding integrals numerically.
Here is the signature of the method:
def trapz(self, a: float, b: float, N: int):
simpson()
- Simpson's method for finding integrals numerically.
Here is the signature of the method:
def simpson(self, a: float, b: float, N: int):
secant()
- The Secant method for finding a root of the expression.
Here is the signature of the method:
def secant(self, n_0: float, n_1: float, epsilon: float = 0.00001, nmax: int = 10_000:
bisection()
- The bisection method for finding a root of the expression.
Here is the signature of the method:
def bisection(self, a: float, b: float, epsilon: float = 0.00001, nmax=100000):
plot()
- Plotting an expression in 2D or 3D.
scatter()
- Scattering an expression in 2D or 3D.
to_json()
- convert the expression to a string in JSON format.
This is done via the to_dict()
method.
Here is the signature of the method:
export_json()
- Save the expression's JSON representation in a JSON
file in the specified path. Here is the signature of the method:
def export_json(self, path: str):
to_Function()
- Try to convert the expression to a Function
object. If not successful, None
will be returned.
class ExpressionSum
class ExpressionSum(IExpression, IPlottable, IScatterable):
The ExpressionSum
class is responsible for handling with collection of
expressions with different types ( even though all inherit from IExpression
).
Usually, you won't have to create the ExpressionSum
object manually, as it
will be generated automatically as a result of arithmetic operations between expressions
of different types, if needed. This interface doesn't do some fancy stuff like
advanced derivatives ( yet 😉 ), but it supports arithmetic operations, assigning values,
plotting and scattering in 2D/3D, and more. It was developed to give an answer to edge cases
that aren't included in the other classes. Quite interestingly, the ExpressionSum
class actually also implements the IExpression
interface, as it's an algebraic
expression too. For instance:
expressions = 3*x + Ln(x) + Sin(x)
other_expressions = ExpressionSum([3*x, Ln(x), Sin(x)])
Addition
You can add ExpressionSum
objects via the +
operator.
x = Var('x')
expressions = Sin(x) + Ln(x)
other_expressions = Cos(x) + 2*x
print(expressions+other_expressions)
Subtraction
You can subtract ExpressionSum
objects via the -
operator.
x = Var('x')
expressions = Sin(x) - Ln(x)
expressions -= Cos(x)
Multiplication
You can multiply ExpressionSum
objects via the +
operators.
x, y = Var('x'), Var('y')
one = Sin(x) + Cos(x)
two = 3*x
print(one * two)
Division
You can divide ExpressionSum
objects via the /
operator.
For instance:
x = Var('x')
print((x ** 2 + Sin(x)) / x)
Power
You can raise the ExpressionSum
object via the **
operator.
For example:
x = Var('x')
print((x+Sin(x))**2)
Assign values
Evaluate to int
or float
You can try to evaluate the ExpressionSum
object into float
or int
via the try_evaluate()
method. For instance:
x = Var('x')
my_expressions = Sin(x) + Ln(x) + 4
my_expressions.assign(x=5)
to_lambda()
You can generate a lambda expression from the ExpressionSum
object via
the to_lambda()
method. For instance:
x = Var('x')
my_expressions = Sin(x) + Ln(x) + 4
print(my_expressions.to_lambda())
Plot
You can plot an ExpressionSum
object by using the plot()
method.
Here is the signature of the method:
def plot(self, start: float = -6, stop: float = 6, step: float = 0.3, ymin: float = -10,
ymax: float = 10, title: str = None, formatText: bool = False,
show_axis: bool = True, show: bool = True, fig=None, ax=None, values=None):
Here is an example in 2D:
x = Var('x')
result = Sin(x)*Cos(Ln(x**3 - 6)) + Root(2*x-5)
result.plot()
And here is an example in 3D:
x = Var('x')
y = Var('y')
result = Sin(x) + Cos(y) * Ln(x)
result.plot()
This is how it actually looks in matplotlib:
Scatter
You can also scatter an ExpressionSum
object in 2D/3D
via the scatter()
method.
For instance:
x = Var('x')
y = Var('y')
result = Sin(x) + Cos(y) * Ln(x)
result.scatter()
class Mono
This class is used for representing a single polynomial expression, such as `3x^2`, `6x`, etc..
In order to use this class, you must import it first:
from kiwicalc import Mono
Creating A New Mono object
There are several main ways to create a Mono object:
The first approach:
You need to enter `2` parameters:
- The coefficient of the expression, should be a float
- A dictionary that contains the variables_dict' name and exponents. Each key must be
unique!
For example:
expression = Mono(5, {'x': 2, 'y': 3})
print(expression)
# output:
# 5x^2*y^3
The second approach:
You can also enter a string, such as "3x^2*y^4".
This approach is more intuitive, however, it is slower in performance.
expression = Mono("3x^2*y^4")
print(expression)
# output:
# '3x^2*y^4'
The third approach:
If you wish to just create a free number, like `5`, `9.6`, etc, enter it as the coefficient:
four = Mono(4)
print(four)
# output:
# 4
The fourth approach ( shorter, but slower )
You can actually create a Mono
object using the Var
class.
For example:
x = Var('x')
mono_expression = 3*x**2
In the example below we defined a variable, and created a single expression `3x^2`
This expression will be automatically evaluated into a Mono object.
This can be done with as many variables_dict as you wish, and will be covered more thoroughly in
the section regarding the Var
class.
This approach is quite intuitive and simple, but also more costly in runtime.
This is because `x` is first brought to power by `2`, which creates a new copy of Mono object,
and then it's multiplied by `3`, which creates another object.
So here instead of creating one object, we created 3 objects : `x`, `x^2`, and `3x^2`.
Hopefully, that was a clear enough explanation about the inside mechanism, which I will
hopefully
modify in future versions.
Arithmetic Operators
As it will be further elaborated in the parts regarding the
Var
and
Poly
classes,
you can perform addition,subtraction,multiplication and division, using their corresponding
operators in python.
Lets define two
Mono
objects:
first_mono, second_mono = Mono("3x^2"), Mono("2x^2")
print(first_mono+second_mono)
print(first_mono - second_mono)
print(first_mono * second_mono)
print(first_mono / second_mono)
Assigning values
Suppose you have the monomial
3x^2
, and you wish to assign
x=5
to it.
After x is assigned,
the expression turns to
75
.
You can assign numbers to variables_dict by using the
assign()
method from an
instance
of a Mono object. In order to use this method, you must enter keyword arguments.
For example:
m = Mono(coefficient=3, variables_dict = {'x':2, 'y':1}) # 3x^2*y
m.assign(x=4)
print(m)
m.assign(y=3)
print(m)
# output:
# 48y
# 144
You can also assign several variables_dict in the same time:
m.assign(x=4, y=3)
If you want to assign a variable to the expression and get the result without changing
the original expression, you can use the
when()
method.
For example:
m = Mono(coefficient=3, variables_dict = {'x':2, 'y':1}) # creating the expression
assigned_expression = m.when(x=4, y=3) # saving the assigned expression without modifying the original.
print(assigned_expression)
print(m)
output:
144
3x^2*y
Evaluation into int or float
Sometimes the monomials we deal with will only represent numbers, So we might want to convert
them
to a float number.
In order to do that, we can use the
try_evaluate()
method.
This is the signature of the method - it accepts no parameters, and returns None
if the expression can't be evaluated to a number.
def try_evaluate(self) -> Optional[float]:
For instance,
m = Mono(5)
print(m.try_evaluate())
Another example: lets create an expression, assign a value to it so it becomes a number, and
then fetch the float value with the aforementioned method.
m = Mono(coefficient=2, variables_dict = {'x':4}) # creating 2*x^4
assigned_expression = m.when(x=3) # assigning x=3 without changing the original object
print(f"Value is {assigned_expression} and the type is {type(assigned_expression)}")
evaluated_number = assigned_expression.try_evaluate()
print(f"Value is {evaluated_number} and type is {type(evaluated_number)}")
# output:
# Value is 162 and the type is <class '__main__.Mono'>
# Value is 162 and type is <class 'int'>
From the output of the program, you can understand that to get an integer or a float out of
the
object, we must call try_evaluate()
.
Plot
You can plot Mono
objects via the plot()
method. For a a deeper dive into the method, go to
the relevant section in the IExpression
class.
For example:
my_mono = x ** 2
my_mono.plot()
Scatter
You can scatter Mono
objects via the scatter()
r
method. For a a deeper dive into the method, go to
the relevant section in the IExpression
class.
For example:
my_mono = x ** 2
my_mono.scatter()
The var class enables you to define variables and then use arithmetic operations on them
without ever changing the original variable.
Var
inherits from
Mono
, and in order to use it, you must import it
first:
from kiwicalc import Var
Creating a new variable
Creating a new Var object is extremely simple, and requires only the name of the variable.
For example
x = Var('x')
y = Var('y')
z = Var('z')
Yes! As simple as that
Wasn't that hard, huh?
Just pay attention, that it's highly recommended to use variables_dict with
only `1` character, like `x`, `y` etc.
Calculations with Var
Since
Var
inherits from
Mono
,
You can use the `+` , `-` , * , `/` , ** operators for calculating algebraic expressions,
similar to how it's done in the Mono class.
Example 2 - Basic calculations with Var
x = Var('x')
y = Var('y')
print((x-5)**2)
print((x+y)*(x-y))
print((x+y)**2)
print((x+y+5)**3)
# output:
# x^2-10x+25
# x^2-(y^2)
# x^2+2x*y+y^2
# x^3+3x^2*y+15x^2+3x*y^2+30x*y+75x+y^3+15y^2+75y+125
While here the examples show the use of 2 variables_dict (x and y),
you can use as many variables_dict as you'd like.
That way you can simplify sophisticated polynomials in an instant, which could have taken
hours
by hand.
Note
when calculating with Var, the results will usually be of type Mono or Poly,
rather than Var, Since Var is basically only a subclass of Mono which helps you to write
more concisely
In the next chapters, we will learn how to integrate Var with
other classes such as Sin and Log.
Derivatives and Integrals
You can use Var
to compute the polynomial's derivatives and integrals,
(it evaluates to Mono
or Poly
object).
Note: This feature is only possible with one variable. For example:
print((3*x**2).derivative())
print((6*x).integral())
# output:
# 6x
# 3x^2
Important To Mention
In the integral function, if the name of your variable isn't 'x', specify your custom
name as a parameter.
This kind of behavior applies to all of the classes that inherit from
You can call derivative() and integral() from a Mono
, Var
and
Poly
objects.
Checking if two expressions are equal
You can equate algebraic expressions using the '==' operator, or the method
__eq__()
.
You could also check if two algebraic expressions aren't equal, using the '!='
operator or the __ne__()
method.
For example:
print(3*y == 2*x+6)
print((2*x + 2*y) / 2 == y + (2* x**2) / (2*x))
print(2 * x + 5 != 3*y*x**2 - 4)
# output:
# False
# True
# True
Pay Attention
Just like in math, you need to make sure to use parenthesis when necessary,
since the lack of it might
change the order of operations and hence also the result.
So if your result seems off, try adding a pair of extra parenthesis
class Poly
Polynomials are mainly supported via the Poly
class.
This class represents a polynomial, namely, a collection of monomials.
Since a monomial is represented by the Mono class, each Poly object contains a collection of
Mono objects. In order to use the class, you must import it first:
from kiwicalc import Poly
Creating A New Polynomial
There are several ways to create a Poly object:
-
Entering a collection of Mono
or Var
objects
A given collection of Mono objects will be evaluated and inserted into a new
Poly object. In addition, ints, floats and valid strings will also be accepted
as items in the collection. The easiest way to do that is to use the Var class.
For Example:
x, y = Var('x'), Var('y')
polynomial = Poly((3*x**2,2.6,"4y^2",7*x**4 * y**5))
You can also create the Mono
objects on the spot. For instance:
polynomial = Poly((Mono(2,{'x':3,'y':4}),Mono("4xy^3")))
-
Create a Poly
object with Var
object arithmetics
You can actually construct Poly
objects in an even simpler manner using
the Var
class.
However, this approach might be slower, as more objects are created during the
Var
objects arithmetic.
For example:
x = Var('x')
polynomial = x**2 + 3*x + 6
The expression on the right actually computes to a Poly
object, so the
polynomial is generated for us automatically.
-
Create a Poly
object from a string
Just enter a string in the right format, and a corresponding Poly object
will be generated soon enough! This approach is rather simple, however, it requires
string
manipulation from the class, and thus it might be a bit more expensive on runtime
than the first approach.
Despite the obvious simplicity of this approach, as for
this version, the strings must follow specific rules and limitations:
-
The variable name represented by a single English letter, i.e: a-z , A-Z.
-
Use of parenthesis isn't supported yet.
-
Division operator isn't supported yet.
For instance:
expression = Poly("8 + 3x^2 + 6x + 9 + 2x^3 + 2x^2")
print(expression)
# output:
# '2x^3+5x^2+6x+17'
-
Create a new Poly
object by copying an existing one
You can also create a new Poly object from an existing one.
Alternatively, you can copy Poly objects with the __copy__()
method.
For example:
x = Var('x')
existing_polynomial = 3*x**2- 6*x + 7
new_polynomial = Poly(existing_polynomial)
Arithmetic Operators for polynomials
You can perform arithmetic operations between polynomials with the corresponding operators:
- Addition is done by the
+
operator
- Subtraction is done by the
-
operator
- Multiplication is done by the
*
operator
- Division is done by the
/
operator.
- Power is done by the
**
operator. (polynomial **
number)
Polynomial Addition
You can easily add up polynomials with the +
operator.
Example 5 - Polynomial Addition
x = Var('x')
y = Var('y')
first = 2*x + 3
second = 3*y - 4*x + 5
print(first+second)
# output:
# -2x+8+3y
Polynomial Subtraction
You can also subtract polynomials using the -
operator.
Example 6 - Polynomial Subtraction
x = Var('x')
first = 3*x + 5
second = 2*x - 1
print(first-second)
# output:
# x+6
Polynomial Multiplication
You can use the *
operator in order to multiply between:
-
A polynomial and another polynomial.
-
A polynomial and any
IExpression
object.
-
A polynomial and a float or an int.
For example:
x = Var('x')
y = Var('y')
print((x + 5) * (x - 2))
print( (x + y) * (x - y))
# output:
# x^2+3x-10
# x^2-y^2
Polynomial Division
You can use the built in /
operator in order to divide between
polynomials. For example:
x = Var('x')
first = x**2 + 6*x + 8
second = x + 4
division_result = first / second
print(division_result)
Additional feature
You can also divide by a string that represents a polynomial, for the sake of simplicity.
Raising a polynomial by a power
You can raise a polynomial by an integer or float power with the **
operator.
For example:
a, b = Var('a'), Var('b')
print((a+b)**2)
# output:
# a^2+2a*b+b^2
Assigning values
You can assign values to variables_dict using the assign()
method from a Poly
object.
For example, after assigning `x=5` for the expression `2x-4` It will
represent `6`:
x = Var('x')
poly = 2*x - 4
poly.assign(x=5)
print(poly)
# output:
# 6
Keep In Mind
The Poly object stays of type Poly
even after we assign values to it.
If the polynomial represents a free number and you wish to extract the number as
an int
or float
, use the try_evaluate()
method.
This kind of behavior applies to all of the classes that inherit from
the IExpression
interface. For more details, visit its
section.
Sometimes we want to see what happens when we assign a certain value to a polynomial,
but we want to leave the original polynomial unchanged. For that, we can use the
when()
method. For example:
y = Var('y') # Declaring a variable
original = 3*y**2 + 6*y + 7 # Creating the original polynomial
assigned = original.when(y=-2) # Saving the assigned polynomial
print(f"Original is {original}")
print(f"Assigned is {assigned}")
# output:
# Original is 3y^2+6y+7
# Assigned is 7
Evaluating a polynomial to a number
Sometimes when a polynomial represents a free number, we want to extract its value as an
integer.
For that, we can use the try_evaluate()
method. The method will return the int
or
float
value if the polynomial represents a free number,
and None
otherwise.
For example,
x = Var('x') # Declaring a variable named x
poly = 3*x - 2 # Creating a polynomial
poly.assign(x=4) # Assigning a value to it, so it will hold a free number
number = poly.try_evaluate() # Extract the free number from the polynomial
print(f"poly represents {poly} and its type is {type(poly)}")
print(f"number is {number} and its type is {type(number)}")
# output:
# poly represents 10 and its type is <class '__main__.Poly'>
# number is 10 and its type is <class 'int'>
Plot Polynomials
You can plot univariate polynomials (polynomials with only one variable) in a 2D axis
system via the plot()
method. Here is the signature of the method:
def plot(self, start: float = -10, stop: float = 10,step: float = 0.01, ymin: float = -10, ymax: float = 10, text=None, fig=None, ax=None, show_axis=True,show=True):
For example:
x = Var('x')
poly = x**2 + 6*x + 8
poly.plot()
Scatter polynomials
You can scatter univariate polynomials (polynomials with only one variable) on a 2D axis.
For example:
x = Var('x')
poly = x**2 + 6*x + 8
poly.scatter()
Derivatives
You can find a derivative of a polynomial via the derivative()
method.
The method will return a corresponding Poly
or Mono
object that
represents the derivative. For example:
x = Var('x')
poly = 2*x**3 - 6*x + 7
print(poly.derivative())
Partial Derivatives
You can find the partial derivatives in respect to a variable with the
partial_derivative()
method. The method accepts a string that represent the
corresponding variable/s. For instance, the partial derivative of the polynomial function
`f(x,y)=x^2+y^2` in respect to `x` can be denoted as `f'_x` and it is `2x`.
For example:
x, y = Var('x'), Var('y')
f = x**2 + y**2
f_x = f.partial_derivative('x')
print(f_x)
You can also do several partial derivatives. For instance, let `f(x) = x^2 + 2xy + y^2`.
In order to find `f'_{xy}` we have to first derive for `x`:
`\frac(∂f)(∂x) = 2x + 2y` , and then for `y`, and therefore `\frac(∂f)(∂x ∂y) = 2`.
It's important to mention that the order in which the partial derivatives are done doesn't
alter the final result, hence, `f'_{xy} = f'_{yx}`
For example:
x, y = Var('x'), Var('y')
f = x**2 + 2*x*y + y**2
f_xy = f.partial_derivative('xy')
Class FastPoly
*experimental new feature
The FastPoly
class serves as an alternative to the Poly
class
when handling with polynomials. For most simple cases, it should be considerably faster and
memory performant, but it's still rather limited.
Advantages
There are several advantages of using the FastPoly
class instead of the
Poly
class in certain cases.
- Faster - faster root-finding, addition, and subtraction when the polynomial is of
a relatively small degree.
- More memory performant - For most simple cases, the
FastPoly
class
uses less memory than the Poly
class. That stems from the difference between
the storage of the two ways: a Poly
object contains a collection
of Mono
objects, while a FastPoly
object stores the coefficients
of the different variables.
In addition, Poly
objects tend to store large amounts of memory when containing
a large number of expressions, while FastPoly
expressions will store lower
amounts of memory, as long the degree of of the polynomial will be low.
Disadvantages
-
Slower and less memory performant on polynomials with higher degrees.
As mentioned earlier, PolyFast
stores collections of
coefficients, so a polynomial of degree 1000 will be represented with a 1000
coefficients. Storing such big amounts of memory is inefficient and counterproductive.
- Still partially implemented - As for this version,
FastPoly
still doesn't support multiplying and dividing polynomials. Also, there is no support
yet for powers with the **
operator.
- Incompatible with
IExpression
- Most expressions in KiwiCalc
inherit from the IExpression
interface. That way, they are more compatible
with each other, and able to interact with each other. However, FastPoly
isn't fully compatible with it yet.
- No Support for mixed expressions - Currently, the
FastPoly
class doesn't support expressions with expressions such as `xy`. For instance,
you could represent the expression `x^2 + 2xy + y^2` via the Poly
class, but not with the FastPoly
class.
Conclusion
While FastPoly
tends to be much faster and performant on simple polynomials
and polynomials with many expressions than Poly
, it takes a lot of of memory
for polynomials with higher degrees, and it is still quite limited with its features.
Therefore, it would be best to use FastPoly on polynomials with lower degrees when
not needing sophisticated features and compatibility with other types of expressions.
Properties
variables
(list) - a copy of a list of the variables that appear in the
expression. For instance, the list will be ['x', 'y']
for the expression
`x^2 + 2y - 7`.
num_of_variables
(int) - get the number of variables in the expression
without fetching a copy of the list of variables.
variables_dict
(dict) - The polynomial is represented internally in
a dictionary.
degree(Union[float, dict])
- The highest power in the expression.
If the polynomial contains only 1 variable, the result will be integer.
For instance, for the polynomial `x^2 + 2x + 1` the result would be 2
.
For expressions with several variables, a dictionary with the highest powers of each
variable will be returned. For example, for the polynomial `2x^3 - 2y + 5y^2 + 1`,
the result would be {'x':[2,0,0], 'y':[5,-2], 'free':1}
.
Creating a FastPoly
object.
There are several ways to create a PolyFast
object.
- Entering a string- The most prominent and easiest method to create a new
PolyFast
object is by entering a string that represents a polynomial.
The string will be parsed internally into a dictionary and will be stored in the object.
For example:
# creating a PolyFast object
fast_poly = FastPoly("3x^5 - 2y^2 + 3x + 14")
You can also specify the variables in the expression to speed us the
parsing of the string.
fast_poly = FastPoly("3x^5 - 2y^2 + 3x + 14", variables=('x', 'y'))
- Entering the parsed dictionary directly- As mentioned before,
the polynomial is represented internally via a dictionary.
Each key-value pair in the dictionary is a variable that appears in the expression
and a list of its coefficients. In addition, a special key
'free'
is added to the dictionary to represent the free number. For instance, the dictionary for
the expression `x^2 + 2x + 6` will be
{'x':[1,2], 'free':6}
And the dictionary for the expression `2x^3 + y^3 + 7` will be
{'x':[2,0,0], 'y':[1,0,0], 'free':7}
You can enter a dictionary in this syntax in order to create a PolyFast
object in a shorter time. For example, this is how we can create the expression
`2n^4 - 32`:
FastPoly({'n':[2,0,0,0], 'free':-32})
- Entering a
list
or tuple
of coefficients-
You can create a polynomial with 1 variable by entering its coefficients and the name
of the variable. If the name of the variable isn't given, the default is 'x'.
For instance, the coefficients of the expression `n^2 + 2n + 1` are
[1,2,1]
, so you can create the expression in the following way:
fast_poly = FastPoly([1,2,1], variables=('n',))
Adding and Subtracting
You can add and subtract FastPoly
objects via the `+` and `-` operators.
For example:
poly1 = FastPoly("2x^3 + 5x -7")
poly2 = FastPoly("x^2 + 4y^2 - x^3 + 5x + 6")
print(poly1+poly2)
#output: 'x^3+x^2+10x+4y^2-1'
poly1 = FastPoly("2x^3 + 6x^2 + 5")
poly2 = FastPoly("x^4 + x^3 - 5x^2 + 6")
print(poly1-poly2)
#output: '-x^4+x^3+11x^2-1'
Finding the roots of the polynomial
If the polynomial has only 1 variable, you can use the roots()
method to find
its roots. For instance:
fast_poly = FastPoly("x^4+8x^3+11x^2-20x")
print(fast_poly.roots())
# output: [0j, (1+0j), (-5+0j), (-4+0j)]
assign()
You can assign values to variables via the assign()
method.
For example:
my_poly = FastPoly("x^2 + y^2")
my_poly.assign(x=5)
print(my_poly)
# output: 'y^2+25'
when()
You can an assigned copy of the object via the when()
method.
For example:
my_poly = FastPoly("x^2 + y^2")
print(my_poly.when(x=5))
print(my_poly)
# output: 'y^2+25'
# 'x^2 + y^2'
try_evaluate()
You can try to evaluate a polynomial into an int or float via the try_evaluate()
method. If not possible,
None
will be created. For instance:
poly1 = FastPoly("5")
poly2 = FastPoly("x^2 + 6x + 8")
print(poly1.try_evaluate())
print(poly2.try_evaluate())
#output: '5.0'
#'None'
plot()
You can plot FastPoly
object with 1 or 2 variables in 2D or 3D respectively
via the plot()
method.
Here is a detailed explanation about the method. For example:
fast_poly = FastPoly("x^3 - 2x + 1")
fast_poly.plot()
scatter()
you can scatter FastPoly
objects with 1 or two variables in 2D or 3D
respectively via the scatter()
method. For instance:
fast_poly = FastPoly("x^3 - 2x + 1")
fast_poly.scatter()
class Log
*experimental feature
class Log(IExpression, IPlottable, IScatterable):
You can handle with logarithms via the Log
class.
You can apply arithmetic operations to the logarithms, and integrate them with other
types of expressions.
Each Log object represent an expression of the form `nlog_m(b)^k`,
where `n` is the coefficient, `m` is the base, `b` is the internal expression
and the power is `k`.
The interface is designed to be quite flexible - for instance, you can
represent logarithms of both numbers and algebraic expressions, including
polynomials, trigonometric expressions, other logarithms, and even factorials.
However, this interface still requires further polishing, and therefore further support
will be added in next versions. As for now, it is considered an experimental feature.
Creating a new instance
Parameters
-
expression
- the internal expression inside the logarithm.
You can enter here a string, an IExpression
object, or a number.
Read below about the different ways to use this parameter.
-
base
(optional). The type of the base is float
or int
.
The default is `10`.
-
coefficient
(optional) - The coefficient of the expression.
The type of the coefficient is IExpression
and the default is Mono(1)
You can specify the internal expression in several ways:
- Entering a string - You can enter a string that represents the logarithm.
The string will be internally parsed and processed. However, you have to specify the type of
the expression inside the logarithm. For example:
my_log = Log("x^2 + 6x + 8", base=2, dtype='poly')
other_log = Log("sin(x) + cos(2x)", base=10, dtype='trigo')
- Entering an
IExpression
- you can also enter an algebraic
expression. For instance:
x = Var('x')
my_log = Log(3*x+5, base=2)
other_log = Log(Sin(x), base=5)
-
Entering a number - You can also enter an int
or a float
. For instance:
my_log = Log(100, base=10)
print(my_log.try_evaluate())
Addition
You can add Log
objects, or other expressions via the +
operator.
When adding Log objects with the same base, the result will be evaluated via the formula
`log_a(x) + log_a(y) = log_a(xy)`. For example:
x = Var('x')
my_log = Log(2*x)
other_log = Log(x)
print(my_log+other_log)
Subtraction
You can subtract Log
objects, or other algebraic expressions via the
-
operator. When subtracting Log objects with the same base, the result
will be evaluated via the formula `log_a(x) - log_a(y) = log_a(\frac(x)(y))`. For example:
x = Var('x')
my_log = Log(x**2 + 6*x + 8)
other_log = Log(x+4)
print(my_log-other_log)
Multiplication
You can multiply Log
objects by other expressions via the *
operator.
n = Var('n')
my_log = Log(n+5, base=2)
my_log *= 3
my_log *= 2*n**2
print(my_log)
Division
You can divide logarithms via the /
operator. For instance:
x = Var('x')
my_log = 3 * Log(x) ** 2
other_log = 2*Log(x)
print(my_log / other_log)
Power
You can raise a Log
object by a power via the **
operator.
For example:
x = Var('x')
print(Log(x) ** 2)
Equating logarithms
You can check the equality of Log
objects via the ==
operator.
Currently, the algorithm does not cover every case, and therefore there might be some
false negatives in abnormal cases.
x = Var('x')
my_log = Log(2*x+10, base=2)
other_log = Log(2*x+10, base=2)
print(my_log == other_log)
Evaluate int
or float
You can try to evaluate a Log
object to int
or float
via the try_evaluate()
method. If not possible, None
will be returned.
For example:
x = Var('x')
my_log = Log(2*x, base=2)
print(my_log.try_evaluate())
my_log.assign(x=4)
print(my_log.try_evaluate())
class TrigoExpr
*experimental feature
class TrigoExpr(IExpression, IPlottable, IScatterable):
The TrigoExpr
class represents a trigonometric expression. It is still under
development, but it already supports relatively advanced features with ease. For instance,
it integrates relatively well with other types of expressions, it supports basic
arithmetic operations, it can compute simple cases of derivatives and some common
trigonometric identities. This feature will be heavily extended in the future or modified,
to be even more robust.
Creating a new instance
There are several ways to create a TrigoExpr
object.
- Entering a string. For example:
print(TrigoExpr('sin(3x)', dtype='poly'))
print(TrigoExpr('sin(log(2x+5))', dtype='log'))
Output:
sin(3x)
sin(log10(2x+5))
Pay Attention
Make sure you separate trigonometric methods with the *
operator when multiplying them. For instance, the string
sin(x)cos(x)
isn't valid, while the string
sin(x)*cos(x)
is valid.
In summary, follow the syntax or get errors.
- Using subclasses -
TrigoExpr
has many subclasses that make it more
intuitive to create new trigonometric expressions:
Sin
Cos
Tan
Cot
Sec
Csc
Asin
Acos
Atan
You have two main ways to create new objects via these subclasses: Entering a string
that represents the internal expression and its type, or alternatively entering
the algebraic expression directly. For instance:
my_sin = Sin('2x', dtype='poly')
my_cos = Cos('log(3x)', dtype='log')
x = Var('x')
my_sin = Sin(2*x)
my_cos = Sin(Log(3*x))
Addition
You can add TrigoExpr
objects with the +
operator. For instance:
import math
x = Var('x')
print(Sin(x) + Sin(x))
print(Sin(x) + Cos(x))
print(Sin(math.pi/2) + 4)
Subtraction
You can subtract TrigoExpr
objects with the -
operator. For instance:
import math
x = Var('x')
print(Cos(x) - 2*Sin(x))
print(2*Sin(x) - Sin(x))
print(3*Sin(math.pi/2) - 2)
Multiplication
You can multiply TrigoExpr
objects via the *
operator.
For instance:
x = Var('x')
print(Sin(x)*Cos(x))
print(5*Sin(2*x))
print(3*x**2 * Tan(Log(x)))
Output:
sin(x)*cos(x)
5sin(2x)
1*(3x^2)*tan(log10(x))
Division
You can divide TrigoExpr
objects via the /
operator.
For example:
x = Var('x')
print(Sin(x)/Cos(x))
print(2*Sin(x)*Cos(x) / Sin(2*x) )
print((3*x*Sin(x)) / Log(x))
Power
Equating
You can check equality between two TrigoExpr
expressions via the ==
operator. However, checking equality between trigonometric expressions is highly problematic,
considering trigonometric identities. For instance, `cos(x) = cos(x+2\pi)`, `sin(2x) =
2sin(x)cos(x)`
The algorithm
for this method doesn't take into consideration all of the relevant identities, and
therefore there might be false negatives. However, there is little to none chance of
false positives occurring. Here are some examples:
x = Var('x')
print(Sin(2*x) == Sin(2*x))
print(Sin(x) == Sin(x+2*math.pi))
print(2*Sin(x)*Cos(x) == Sin(2*x))
print(Sin(x) == Sin(x+5))
Simplify
This method is used mainly internally, but can also be used manually by the user, in order
to clean the expression from negligible expressions. For instance, the expression
`sin(x)^2*cos(2x)^0` can be simplified to `sin(x)^2`.
to_lambda()
You can generate an executable lambda expression from the TrigoExpr
object
via the to_lambda()
method. For instance:
import math
x = Var('x')
my_lambda = Sin(x).to_lambda()
print(my_lambda(math.pi/2))
Output:
1.0
Evaluate to int
or float
You can try to evaluate the expression into int
or float
via the try_evaluate()
method. For instance, the expression `sin(\frac(\pi)(2))`
can be evaluated into `1`. If the expression can't be evaluated, None
will be returned. For example:
import math
x = Var('x')
my_trigo = Sin(math.pi)
my_eval = my_trigo.try_evaluate()
if my_eval is not None:
print("the expression could be evaluated")
else:
print("the expression couldn't be evaluated")
newton
You can use newton's method in order to find a root of the trigonometric expression.
For example:
x = Var('x')
print(Sin(x).newton(initial_value=2))
reinman()
You can use the Reinman Sum Method with via the reinman()
method in order
to find the finite integral of the trigonometric function. For instance:
import math
x = Var('x')
print(Sin(x).reinman(0, math.pi, 20))
1.9954413183201944
trapz()
You can also use the trapezoid rule via the trapz()
method
in order to find the finite integral of the trigonometric function. For example:
import math
x = Var('x')
print(Sin(x).trapz(0, math.pi, 20))
Output:
1.9958859727087144
Here we also see that `\int_0^\pi sin(x)\dx\ ~~ 2`
simpson()
You can also use Simpson's method in order to find the finite integral:
import math
x = Var('x')
print(Sin(x).simpson(0, math.pi, 20))
Output:
1.9863695787474476
class TrigoExprs
*experimental feature
class TrigoExprs(ExpressionSum, IPlottable, IScatterable):
The TrigoExprs
class is used in order to represent a collection of
TrigoExpr
objects. For instance, the expression `sin(x)*cos(y) + tan(x)` is composed of 2
TrigoExprs
objects.
Creating a new instance
You have several ways to create a new TrigoExprs
object.
-
As a result of of an operation - you can actually generate
TrigoExprs
objects without explicitly calling the constructor,
but by executing operations on TrigoExpr
objects. For example:
x = Var('x')
result = Sin(x) + Cos(x)
print(type(result))
The output:
Entering a string - you can choose to enter a string that represents the
trigonometric expressions. You also need to enter the datatype of the expressions inside the
trigonometric functions, so they will be processed accordingly. The default datatype is
'poly'
, and hence if the dtype
parameter is not specified,
the internal expressions will be processed as polynomials. It is possible that in
next versions the strings will be classified and processed without any help from the user.
Here are some examples for entering a string to the constructor:
print(TrigoExprs("3sin(x) - 2cos(x)"))
Addition
You can add TrigoExprs
objects via the +
operator.
For example:
first = TrigoExprs("sin(x)^2 + 5cos(y) + 7")
second = TrigoExprs("4cos(y) + 5tan(2x)")
print(first+second)
Subtraction
You can subtract TrigoExprs
objects via the -
operator.
For example:
first = TrigoExprs("3sin(x) + 4cos(x)")
second = TrigoExprs("2sin(x) - cos(x) + 4")
print(first-second)
Multiplication
You can multiply TrigoExprs
objects via the *
operator.
For example:
x= Var('x')
first = Sin(x) + Cos(x)
second = Sin(x) - Cos(x)
print(first * second)
Division
You can divide TrigoExprs
objects via the /
operator.
For example:
x = Var('x')
first = 3*Sin(x)**2 + 3*Cos(x)*Sin(x)
second = Sin(x)
print(first / second)
Power
You can use the **
operator for raising a trigonometric expression by a power.
For instance:
x = Var('x')
print((Sin(x)+Cos(x))**2)
print(Sin(x)+Cos(x)**(x+5))
Evaluate to int
or float
You can try to evaluate the expression to int
or float
via
the try_evaluate()
method.
If not possible, None
will be returned. For example:
import math
my_trigo = Sin(math.pi/2) + Cos(math.pi/2)
print(my_trigo.try_evaluate())
Assign values
You can assign values to variables in the expression via the assign()
method.
For example:
import math
x = Var('x')
my_trigo = Sin(x) + Cos(2*x)
print(my_trigo.assign(x=math.pi/2))
to_lambda()
You can generate a lambda expression from the TrigoExprs
object via the to_lambda()
method. For example:
import math
x, y = Var('x'), Var('y')
my_trigo = Sin(x)*Cos(y) + Sin(y)*Cos(x)
my_lambda = my_trigo.to_lambda()
print(my_lambda(math.pi/4, math.pi/3))
import math
x = Var('x')
my_trigo = Sin(x) + Cos(x)
my_lambda = my_trigo.to_lambda()
print(my_lambda(math.pi/2))
Plot
You can plot the TrigoExprs
object in 2D or 3D via the plot()
method. For example:
x = Var('x')
my_trigo= Sin(x) + Cos(x)
my_trigo.plot()
x = Var('x')
y = Var('y')
my_trigo = Sin(x) * Cos(y)
my_trigo.plot()
Scatter
You can scatter the TrigoExprs
object in 2D or 3D via the plot()
method. For example:
x = Var('x')
my_trigo= Sin(x) + Cos(x)
my_trigo.scatter()
x = Var('x')
y = Var('y')
my_trigo = Sin(x) * Cos(y)
my_trigo.scatter()
class Root
*experimental feature
class Root(IExpression, IPlottable, IScatterable):
You can create algebraic expressions with roots via the Root
class.
Each Root
object represents an expression in the form `a\root(n)(x)`
This interface is still experimental, and it will be extended in the future for better
support of arithmetic operations, derivatives, integrals, etc. However, it still offers
a decent set of features for handling roots, such as assigning values, plotting, and
scattering.
Properties
coefficient
- the coefficient of the expression. Either algebraic expression or
number
inside
- the internal expression inside the root. Either algebraic expression
or number
root
- The root. default is 2.
variables
- The variables that appear in the expression.
Creating a new instance
You can create a new instance of Root
via the __init__()
method. Here is the signature of the method:
def __init__(self, inside: Union[IExpression, float, int], root_by: Union[IExpression, float, int] = 2
, coefficient: Union[int, float, IExpression] = Mono(1)):
For example, this is how you can create the expression `\root(3)(2x+5)`:
x = Var('x')
my_root = Root(2*x+5, 3)
print(my_root)
You can also use the subclass Sqrt
to create Square roots in a more
explicit way:
x = Var('x')
my_root = Sqrt(x**2 + 6*x + 8)
This is equivalent to:
x = Var('x')
my_root = Root(x**2 + 6*x + 8, 2)
Addition
You can add Root
objects via the +
operator. For instance:
print(Sqrt(2 * x) + Sqrt(2 * x))
print(Sqrt(4) + Sqrt(6))
Subtraction
You can subtract Root
objects via the -
operator
print(2*Sqrt(x) - Sqrt(x))
print(Sqrt(6) - Sqrt(4))
Multiplication
You can multiply Root
objects via the *
operator
print(2 * Sqrt(x) * Sqrt(x))
print(Sqrt(6) * Sqrt(4))
Division
You can divide Root
objects via the /
operator
print(2 * Sqrt(x) / Sqrt(x))
print(Sqrt(16) / Sqrt(4))
Power
You can raise a root by a power (and even cancel the root) by the **
operator. For instance:
print(Sqrt(x) ** 2)
print(Sqrt(5) ** 2)
Assign values
You can assign values to the expression via the assign()
method.
If you don't want to change the original expression, you can use the when()
method. For example:
my_root = Sqrt(x)
print(my_root.when(x=5))
my_root.assign(x=4)
print(my_root)
Evaluate to int
or float
You can try to evaluate Root
objects to int
or float
via the try_evaluate()
method. For instance:
print(Sqrt(25).try_evaluate())
print(Sqrt(x).try_evaluate())
Plotting
You can plot and scatter Root objects via the plot()
and
scatter()
methods. For instance:
Sqrt(x).plot()
Sqrt(x).scatter()
For more information about this method, view the relevant section in class
IExpression
.
class Abs
*experimental feature
class Abs(IExpression, IPlottable, IScatterable):
Creating a new instance
You have several ways to create a new Abs via the __init__()
method.
Here is the signature of the constructor:
def __init__(self, expression: Union[IExpression, int, float], power: Union[int, float, IExpression] = 1,
coefficient: Union[int, float, IExpression] = 1, gen_copies=True):
For example:
x = Var('x')
print(Abs(x))
print(Abs(-x))
print(Abs(5))
print(Abs(expression=x, power=2, coefficient=3))
Addition
You can add Abs
objects via the +
operator. For example:
x = Var('x')
print(2*Abs(3*x) + Abs(3*x))
print(Abs(5) + Abs(-5))
Subtraction
You can subtract Abs
objects via the -
operator. For instance:
x = Var('x')
print(2 * Abs(3 * x) - Abs(3 * x))
print(Abs(5) - Abs(-5))
Multiplication
You can multiply Abs
objects via the *
operator. For example:
x = Var('x')
print(2 * Abs(3 * x) * Abs(3 * x))
print(Abs(5) * Abs(-5))
Division
You can divide Abs
objects via the /
operator. For instance:
x = Var('x')
print(2 * Abs(3 * x) / Abs(3 * x))
print(Abs(x) / 5)
print(Abs(5) / Abs(-5))
Power
You can raise Abs objects by a power via the **
operator. For example:
print(Abs(x) ** 2)
Assign values
You can assign values with the assign()
method, or with the when()
method if you don't want to change the original object. For instance:
my_abs = Abs(x)
print(my_abs.when(x=-5))
my_abs.assign(x=4)
print(my_abs)
Evaluate to int
or float
You can try to evaluate the Abs
object to int
or float
via the try_evaluate()
method. For example:
my_abs = Abs(-5)
print(my_abs.try_evaluate())
to_lambda()
You can generate a lambda expression from the Abs
object via the
to_lambda()
method. For instance:
my_abs = Abs(x)
print(my_abs.to_lambda())
Plot
You can plot Abs
objects via the plot()
method. For example:
x = Var('x')
my_abs = Abs(x)
my_abs.plot()
x, y = Var('x'), Var('y')
my_abs = Abs(x + y)
my_abs.plot(step=0.3)
Scatter
You can scatter Abs
objects via the scatter()
method.
For instance:
x = Var('x')
my_abs = Abs(x)
my_abs.scatter()
x, y = Var('x'), Var('y')
my_abs = Abs(x + y)
my_abs.scatter(step=0.3)
class Exponent
*experimental feature
class Exponent(IExpression, IPlottable, IScatterable):
Creating a new instance
You have several ways to create exponent. For instance:
x = Var('x')
exponent1 = 3*Exponent(x, x)
exponent2 = 3 *x ** x
exponent3 = Exponent(base=x, power=x, coefficient=3)
Arithmetic Operators
You can apply arithmetic operators to the Exponent
object.
For example:
x = Var('x')
my_exponent = x ** x
print(my_exponent + x ** x)
print(my_exponent - x ** x)
print(my_exponent * 2)
print(my_exponent / 2)
Assign values
You can assign values to the Exponent
objects via the assign()
method, or via the when()
method if you don't want to modify the original object.
For instance:
x, y = Var('x'), Var('y')
my_exponent = x ** x
print(my_exponent.when(x=2))
other_exponent = x ** y
print(my_exponent.when(y=2))
Evaluate to int
or float
You can try to evaluate the Exponent
object via the try_evaluate()
method. For instance:
x = Var('x')
my_exponent = Exponent(2, 2)
print(my_exponent.try_evaluate())
to_lambda()
You can generate a lambda expression from the exponent object via the
to_lambda()
method. For example:
x = Var('x')
print((x**x).to_lambda())
Plotting
You can plot and scatter Exponent
objects via the plot()
and scatter()
method. For instance:
x, y = Var('x'), Var('y')
(x**x).plot()
(x**x).scatter()
(x**y).plot()
(x**y).scatter()
class Factorial
*experimental feature
You can represent factorial expressions in the form `a(x!)^n`
via the Factorial
class.
Properties
coefficient
- the coefficient of the expression (algebraic expression or
number)
expression
- the internal expression that the factorial is applied to
(algebraic expression or number)
power
- algebraic expression or number.
Creating a new instance
You can create a new Factorial expression in two main ways via the __init__()
method:
def __init__(self, expression: Optional[Union[IExpression, int, float, str]],
coefficient: Union[IExpression, int, float] = Mono(1),
power: Union[IExpression, int, float] = Mono(1), dtype='poly'):
- Enter the parameters directly - You can specify 3 parameters:
the internal expression, the coefficient of the expression, and the power.
Both the coefficient and the power will be 1 if you don't specify them, but you have
to specify the internal expression. For example:
x = Var('x')
my_factorial = Factorial(x**2 + 6*x + 8, coefficient = Sin(x), power = 2)
print(my_factorial)
The output is:
((x^2+6x+8)!)**2
- Enter a string - you can enter a string that represents the expression. You
also need to specify the datatype of the expressions inside. The default is
'poly'
. For example:
my_factorial = Factorial('(3x+5)!', dtype='poly')
Addition
You can add Factorial
objects via the +
operator. For instance:
x = Var('x')
print(Factorial(x) + Factorial(x))
print(Factorial(5)+Factorial(4))
Subtraction
You can subtract Factorial
objects via the -
operator.
For instance:
x = Var('x')
print(Factorial(x) - 0.5*Factorial(x))
print(Factorial(5)-Factorial(4))
Multiplication
You can multiply Factorial
objects via the *
operator.
For instance:
x = Var('x')
print(Factorial(x)*Factorial(2*x))
print(2*x*Factorial(Sin(x)))
print(5*Factorial(1))
Division
You can divide Factorial objects via the /
operator. For instance:
x = Var('x')
print(2*Factorial(x)/Factorial(x))
print(Factorial(3*x+4)/Factorial(2*x-1))
Power
x = Var('x')
print(Factorial(x) ** 2)
Evaluate to float or int
You can evaluate the expression to int
or float
via the
try_evaluate()
method. If not possible, None
will be
returned. For example:
my_factorial = Factorial(5)
my_eval = my_factorial.try_evaluate()
print(f"the evaluation is {my_eval} and its type is {type(my_eval)}")
Assign values
You can assign values via the assign()
method. For example:
x = Var('x')
my_factorial = Factorial(x+2)
my_factorial.assign(x=2)
print(my_factorial)
Plot
You can plot the Factorial
object in 2D or 3D via the plot()
method. For instance:
x = Var('x')
my_factorial = Factorial(Sin(x))
my_factorial.plot(start=-5, stop=5, step=0.01)
Scatter
You can also scatter the Factorial
object. For example:
x = Var('x')
my_factorial = Factorial(Sin(x))
my_factorial.plot(start=-5, stop=5, step=0.01)
Numerical methods
Finite Integrals
A finite integral of a function is an integral of the form
`\int_a^b f(x)\dx\ `, which evaluates to `F(b) - F(a)` when `F(x)` is the antiderivative of
`f(x)`.
Sometimes it is difficult or impossible to integrate `f(x)` in order to get to `F(x)`, and
thus the finite integral must be computed numerically.
We currently support three numerical methods that compute the finite integral of a given
function:
Reinman's sum, Trapezoid Method, and Simpson's Method.
Reinman's Sum
Reinman's Sum is the most known method to approximate the finite integral numerically.
The method approximates the sum of areas ofthe rectangles that are trapped between the
graph and the `x` axis. For example:
import math
reinman(lambda x:sin(x), 0, math.pi, 20)
Trapezoid Method
The Trapezoid Rule is a type of Reinman Sum, where you sum the the areas
of the trapped trapezoid - namely, it computes the finite integral of a function by dividing the
space trapped between the function and the `x` axis, into trapezoids, and summing their
areas. Trapezoids under the `x` axis will have a negative "area".
In order to do that, we choose the range of x values: `a` and `b`, that represent the
limits in the integral `\int_a^b f(x)\dx\ `. We also need to choose an integer `N`,
that determines the number of intervals and the number of trapezoids.
we also compute the length of each interval( denoted as `\Delta x` ) using the following
formula: `\frac(b-a)(N)`.
The formula for the entire thing is:
`\int_a^b f(x)\dx\ \approx \frac(\Delta x)(2) \sum_{k=1}^n f(x_k) + f(x_k-1)`.
When `N->\infty`, the expression tends to the finite integral.
However, while extremely big `N` values lead to better accuracy, they also result in a slower
runtime.
The Trapezoid Method is available via the trapz()
method.
Here is how the method is implemented under the hood:
def trapz(f: Callable, a, b, N: int):
if N == 0:
raise ValueError("Trapz(): N cannot be 0")
dx = (b - a) / N
return 0.5 * dx * sum((f(a + i * dx) + f(a + (i - 1) * dx)) for i in range(1, int(N) + 1))
As you can see, the method is very lightweight and simple.
Sources:
Simpson's method
Simpson's method is another approach for computing finite integrals.
Here is the signature of the method:
def simpson(f: Callable, a, b, N: int):
For instance:
print(simpson(lambda x: sin(x), 0, pi, 11))
Output:
2.0001095173150043
Single-Root Algorithms
Some numerical root-finding methods are only destined to find only 1 solution, depending on
the given input. Amongst them, is the well known Newton-Raphson method, Halley's method,
Steffensen's method, etc. Each of these methods requires different parameters, and
different numbers of iterations, and has pros and cons.
Newton-Raphson method
The Newton-Raphson method is one of the most known root-finding algorithms. Its formula
is also rather simple:
`x_{n+1} = x_{n} - \frac(f(x))(f'(x))`.
The process of this method, seen here in the formula is quite simple to comprehend:
First, we choose the initial value - an arbitrary number, preferably close to the root.
Then, the next item will be equal to the previous item, minus the division between the
function
and its derivative
(with the previous number).
Then, we repeat the process, until the value of the function with our item is very close to
0.
For more details visit
the wikipedia page about Newton's method
Newton's method is implemented in this library via the newton_raphson()
method.
You can import it directly, like this:
from kiwicalc import newton_raphson
This is the signature of the method:
def newton_raphson(f_0: Callable, f_1: Callable, initial_value: float = 0, epsilon=0.00001) -> float:
The method accepts a function `f_0`, a derivative `f_1` , and an initial value.
You can also change the default epsilon value, which is `0.00001`. Epsilon represents
how close a point be close to the x axis to be considered a root.
For example, lets find a root of the polynomial `2x^3 -5x^2 -23x - 10`.
For that, we can express the function its derivative via lambda expressions, and choose
an initial guess for the result.
origin_function = lambda x: 2 * x ** 3 - 5 * x ** 2 - 23 * x - 10
first_derivative = lambda x: 6 * x ** 2 - 10 * x - 23
initial_value = 8
print(newton_raphson(origin_function, first_derivative, initial_value))
# output:
# 5.0
Thus we know that `x = 5` is one of the roots of the function!
Lets try using a different initial value, `-10` for instance:
other_solution = newton_raphson(origin_function, first_derivative, -10)
print(other_solution)
# output:
# -2.0
We discovered another root! `x = -2.0`.
Example 4 - Integrating the Newton-Raphson method with the Function class
Say we defined the same function from the previous example:
origin_function = Function("f(x) = 2x^3 -5x^2 -23x - 10")
Now we can get its derivative (as a Function object):
first_derivative:Function = origin_function.derivative()
And use the newton_raphson method the same as eariler:
solution = newton_raphson(origin_function,first_derivative,9)
print(solution)
# output:
# 5.0
You could also shorten this process, by calling to newton_raphson()
from inside the Function
class:
origin_function = Function("f(x) = 2x^3 -5x^2 -23x - 10")
print(origin_function.newton(7))
# output:
# 5.0
That way, you only need to pass the initial value as a parameter ( in this case, 7).
Similarly, you can integrate it with the classes regarding algebraic expressions in this
project:
Example 2 - Integrating the Newton-Raphson method to find the roots of Algebraic
expressions
x = Var('x')
print((2*x**3 - 5*x**2 - 23*x - 10).newton(5))
Halley's Method
Halley's method, named after the British Mathematician
Halley Edmund
`(1656 - 1742)` is
another method for finding a single root of a function. Unlike the aforementioned Newton's
method, Halley's method also requires the second derivative of a function.
However, it converges cubically to the solution, compared to Newton's method which converges
quadratically, and hence it will take less iterations to find the solution.
This is the method's formula (this step is returned until convergence with the solution):
`x_{n+1} = x_n - \frac(2f(x_n)f'(x_n))(2[f'(x_n)]^2 - f(x_n)f''(x_n))`
It's considered a good practice to use Halley's method instead of Newton's method when it's
easy
to find the derivatives of a function.
This is the signature of the implementation of halley's method:
def halleys_method(f_0: Callable, f_1: Callable, f_2: Callable, initial_value: float, epsilon: float = 0.00001,
nmax:int=100000):
Here are some examples for different approaches to using
Halley's method:
f_0 = lambda n: 2 * n ** 3 - 5 * n ** 2 - 23 * n - 10 # function
f_1 = lambda n: 6 * n ** 2 - 10 * n - 23 # first derivative
f_2 = lambda n: 12 * n - 10 # second derivative
initial_value = 0 # initial approximation ( doesn't have to be 0 obviously )
print(halleys_method(f_0, f_1, f_2, initial_value))
# output:
# -0.49999999999999994
Therefore, we know that `x = -0.5` is a solution. You can round up the result to -0.5 via
the
round_decimal()
method, if that bothers you, or if you need to present the
result
to the user.
Chebychev's method
`x_{n+1} = x_n - \frac(f(x_n))(f'(x_n)) * (1+\frac(f(x_n)*f''(x_n))(2(f'(x_n))^2))`
Chebychev's Method is named by the 19th century Russian Mathematician Pafnuty Chebyshev. It
shares
many characteristics with Halley's method's ;
Both methods are used to find a single root, both a have 3rd order of convergence, and both
require
the function, its derivative, its second derivative
and an initial value.
Some researchers have managed to
optimize chebychev's method by some modifications,
but these newer versions are not currently supported in this version.
In order to use this method, you must import it beforehand from the library:
from kiwicalc import chebychevs_method
Here are some examples of using Chebychev's method:
f_0 = lambda n: 2 * n ** 3 - 5 * n ** 2 - 23 * n - 10
f_1 = lambda n: 6 * n ** 2 - 10 * n - 23
f_2 = lambda n: 12 * n - 10
initial_value = 0 # It doesn't have to be zero obviously
print(chebychevs_method(f_0, f_1, f_2, initial_value))
# output:
# -0.4999999999999998
Steffensen's Method
Steffensen's method is another single root-finding method. It differs from the previous two in
that it only requires a function and an initial value, compared to Newton's method which also
requires the first derivative
and Halley's method which also requires the first and second derivative.
It is considered a good practice to use Steffensen's method when you wish to find one root of a
function, but
differentiating it isn't possible, or too costly in time or memory.
In order to use the method you must import it first:
from kiwicalc import steffensen_method
Lets test out Steffensen's Method.
As mentioned before, it takes two parameters, a function and an initial approximation.
So lets apply it to the function `2x^3 -5*x - 7` and the initial value `8`.
print(steffensen_method(lambda x: 2 * x ** 3 - 5 * x - 7, 8))
# output:
# 2.050976417088196
Therefore, we know that `x=2.0501` is approximately the root of the function.
Bisection Method
The bisection method is a single root-finding algorithm which only applies for
continuous functions. Unlike the aforementioned methods, the bisection method requires
two x values that their corresponding y values are of opposite signs, in addition to the
function of course. This ensures that a root is found between these two dots
(remember that the function needs to be continuous). Knowing that, the method will
perform a "binary search", namely, the method will split the interval between the two
dots into 2 in each iteration. It's considered a rather easy and intuitive method,
but it's also quite slow compared to some other numeric algorithms.
In order to use this method you must import it first:
from kiwicalc import bisection_method
Here is the signature of the method:
def bisection_method(f: Callable, a: float, b: float, epsilon: float = 0.00001, nmax: int = 10000):
Here are some examples of using the method:
Consider the function `f(x) = x^2 - 5x`.
Let's graph it:
We see that the function has two roots on `x = 0` and `x = 5`.
We can also see that when `x<5`, then `y<0`, and when `x>5` or `x<0`, then `y>0`.
Thus, for instance, we know that `f(2) < 0` and that `f(9) > 0`. Since they have opposite y
values,
when we enter `2` and `9` to the bisection method, it will converge to the root between
them,
which
is `x=5` in this case.
parabola = lambda x: x ** 2 - 5 * x # creating the function
print(bisection_method(parabola, 2, 9))
# output:
# 4.999995231628418
As you can see, we got a pretty good approximation of the root.
If that's not enough for you , you can always round the result with the
round_decimal()
method or decrease the epsilon parameter of the function.
However, keep in mind that decreasing the epsilon parameter would also lead to more
iterations,
and consequently to a slower execution.
Multi-Root Algorithms
Multi-root algorithms are algorithms that find several roots of a function.
Durand-Kerner Method
The Durand-Kerner method, also known as the Weierstrass method is an iterative approach for
finding all of the real and complex roots of a polynomial. It was first discovered by the
German
mathematician Karl Weierstrass in 1891,
and was later discovered by Durand(1960) and Kerner (1966).
This method requires the function and a collection of its coefficients.
Here are some examples:
func = lambda x: x ** 4 - 16
coefficients = [1, 0, 0, 0, -16]
print(durand_kerner(func, coefficients))
Output:
{(2+0j), -2j, 2j, (-2+0j)}
You can also use the durand_kerner2()
method if you only have the coefficients.
For instance:
print(durand_kerner2([1, 0, 0, 0, -16]))
You can also run it from polynomial
# EXAMPLES HERE WITH A POLYEXPR AND VAR
Aberth-Ehrlich Method
Aberth's method is another method for simultaneous calculations of multiple roots of a
polynomial function.
It was first developed in `1967` and it's named after the mathematicians Oliver Aberth and
Louis
W. Ehrlich.
Overall, in most implementations it is considered faster than the Durand-Kerner method, as
it converges faster to the roots, and thus less iterations are performed. This
implementation of
the method consists of several steps:
-
creating `n` complex approximations for the n roots, each one will convert to a
different
root.
These approximations will be placed evenly on a circle on the complex plane that its
center
will be `(0,0)`, and its radius will be determined by the following formula:
`R = ^n\sqrt[abs(\frac(p_0)(p_n))]`, where `n` is the highest power in the expression,
`p_0` is the coefficient of the free number of the expression, and `p_n` is the
coefficient
of the highest power in the expression.
-
For each approximation to the root, we will compute an offset, by the following formula:
`w_k = \frac(\frac(p(z_k))(p'(z_k)))(1-\frac(p(z_k))(p'(z_k)) * \sum_{j!=k}(\frac(1)(z_k-z_j)))`
-
Subtract each offset from its corresponding root. This will bring the approximations
closer
to the roots.
-
Repeat steps `2` and `3` until all the approximations have converged to the solutions.
The method requires the original function, its derivative, and the coefficients of the
function.It will return a set of the complex approximations of the roots.
Here is the signature of the method:
def aberth_method(f_0: Callable, f_1: Callable, coefficients, epsilon: float = 0.000001, nmax: int = 100000) -> set:
Here are some examples of using this method:
func = lambda x: 5 * x ** 4 - 1
derivative = lambda x: 20 * x ** 3
coefficients = [5, 0, 0, 0, -1]
print(aberth_method(func, derivative, coefficients))
# output:
# {(0.66874+0j), 0.66874j, -0.66874j, (-0.66874+0j)}
Generalized Newton
The generalized newton's method is a numerical method for finding the solutions of systems of
nonlinear equations. Here are the steps of the underlying algorithm:
- Creating `X` - a vector of initial guesses to the solutions
- Computing `J(X)` - a corresponding jacobian matrix from the system of equations
- If all of the guesses have converged to the appropriate solutions, return them and exit
- Otherwise, modify the next set of solutions: `X_{n+1} = X_n - J(X_n)^{-1}F(X_n)`
- Repeat phases 3 and 4 until the guesses have converged to the solutions.
For example, given this system of equations:
$$
\left\{
\begin{array}{c}
x^2 + y^2 = 25 \\
2x + 3y = 18 \\
\end{array}
\right.
$$
We can extract `F(X)`, a system of functions:
$$
\left\{
\begin{array}{c}
f_1(x,y) = x^2 + y^2 - 25 \\
f_2(x,y) = 2x + 3y - 18 \\
\end{array}
\right.
$$
Hence the jacobian matrix `J(X)` will be:
`[[\frac(∂f_1)(∂x), \frac(∂f_1)(∂y) ],[\frac(∂f_2)(∂x), \frac(∂f_2)(∂y)]]`
Lets compute the partial_derivatives:
`\frac(∂f_1)(∂x) = 2x`,
`\frac(∂f_1)(∂y) = 2y`,
`\frac(∂f_2)(∂x) = 2`,
`\frac(∂f_2)(∂y) = 3`
And therefore the `J(X)` will be:
`J(X) = [[2x, 2y ],[2, 3]]`
Now we need to choose two guesses for the solutions for `x` and `y`:
`X_0 = [[1],[2]]`
We can check whether the current guesses are accurate by checking if `F(X_0) ~~ [[0],[0]]`.
However, ` F(X_0) = F([[1],[2]]) = [[-20],[-10]]`, and therefore we have to keep iterating.
In order to update the guesses and find `X_1`, we need to apply
`X_1 = X_0 - J(X_0)^{-1}F(X_0)`, And for that we need to compute `J(X_0)` and
`J(X_0)^{-1}`.
`J(X_0) = [[2,4],[2,3]]`
`J(X_0)^{-1} = [[-1.5, 2],[1,-1]]`
And therefore `X_1 = [[1],[2]] - [[-1.5, 2],[1,-1]] * [[-20],[-10]] = [[-9],[12]]`
After several iterations, we will reach the solutions `x ~~ 2.5384` and `y ~~ 4.3077`.
However, for different initial guesses, we might get different sets of solutions. For instance,
for `X_0 = [[2],[1]]`, we will actually get `x = 3` and `y = 4`.
Currently, you can use this method only to solve systems of polynomial equations, via the
solve_poly_system()
method.
Parameters
equations
- a collection of equations(type str) or polynomials (type Poly)
initial_vals
- a dictionary that represents the initial guesses. For instance:
{'x':2, 'y':1}
will be interpreted as `X_0 = [[2],[1]]`.
epsilon
Determines the negligible difference. Default is `0.00001`
nmax
- The maximum number of iterations. Default is `10000`
For instance, this is how we would solve the aforementioned example:
# Solving systems of polynomial equations via KiwiCalc
solutions = solve_poly_system(["x^2 + y^2 = 25", "2x + 3y = 18"], {'x': 2, 'y': 1})
print(solutions)
# output: '{'x': 3.0000001628514434, 'y': 3.999999891432371}'
Machine Learning
The library currently contains only several fundamental machine learning methods. More methods
will hopefully be added in the future.
Gradient Descent & Ascent
The gradient descent and ascent methods are numerical methods for finding the minimum
and maximum points of a function, respectively.
Linear Regression
Linear Regression is a popular method for describing a linear connection between two variables
or more. Currently, you can use the linear_regression()
method for finding a
trend-line from points in a 2D axis system. The method accepts a collection of x values
and a corresponding list of y values, and returns either a lambda expression in the form
lambda x: a*x+b
or `a` and `b` directly. Just to clarify, `a` and `b`
represent the coefficients of the known equation `y = ax + b`.
Parameters
axes
- A collection of x values
y_values
- A collection of y values
get_values
- Whether to get the `a` and `b` values in the linear
equation `y=ax+b`. Default is False
, so only the lambda expression of the
linear
equation is returned.
Here is the signature of the method:
def linear_regression(axes, y_values, get_values:bool=False):
For example:
ages = (43, 21, 25, 42, 57, 59)
glucose_levels = (99, 65, 79, 75, 87, 81)
print(linear_regression(ages, glucose_levels, get_values=True))
Mean Absolute Value
The Mean Absolute Value (MAV) is a method for calculating the error between two functions in
a given range. The method accepts two functions and range of numbers to check in. and sums the
absolute value of difference of the y values for each value.
This is the formula for `N` values:
$$ \frac{1}{n} \sum\limits_{i = 1}^n {|\hat{y_i} - y_i|} $$
You can use this method via the mav()
method. Here is the signature of the method:
def mav(func1: Callable, func2: Callable, start: float, stop: float, step: float):
Mean Square Value
The Mean Square Value (MSV) is another function for computing the error between two given
functions.The method accepts two functions and range of numbers to check in and sums the
difference of the squared y values for each value.
This is the formula for `N` values:
$$ \frac{1}{n} \sum\limits_{i = 1}^n {(\hat{y_n} - y_n)^2} $$
You can use this method via the msv()
method. Here is the signature of the method:
def msv(func1: Callable, func2: Callable, start: float, stop: float, step: float):
Mean Root Value
The Mean Root Value (MRV) is another error function. Here is the formula:
$$ \frac{1}{n} \sum\limits_{i = 1}^n {\sqrt{(| \hat{y_n} - y_n) |}} $$
You can use this method via the mrv()
method. Here is the signature of the method:
def mrv(func1: Callable, func2: Callable, start: float, stop: float, step: float):
class Point
class Point:
The Point
class represents a point - namely, a collection of coordinates.
This class also has two subclasses - Point2D
and Point3D
specifically for points in 2d and 3d space.
You can create a new Point
objects by entering a collection of coordinates,
or by using the aforementioned subclasses.
The coordinates of a point can be either numbers or
algebraic expressions, but pay attention that points with algebraic coordinates cannot be
plotted. You can technically also create points with more than 2 or 3 coordinates, but
they cannot be plotted as well.
Here are some examples of creating new points:
Properties
coordinates
- a list of the coordinates
dimensions
- the number of coordinates
Point2D
objects have 2 additional properties: x
, and y
.
Point3D
objects have 3 additional properties: x
, y
and z
.
x, y = Var('x'), Var('y')
print(Point2D(3, 5))
print(Point((3, 5)))
print(Point3D(7, 2, 1))
print(Point((7, 2, 1)))
print(Point((6, 4, 7, 3)))
print(Point2D(x+5, y-4))
print(Point3D(x-3, 4, Sin(y)))
Arithmetic Operations
You can add and subtract points with equal number of coordinates via the +
and -
operators. For example:
print(Point2D(4, 1) + Point2D(5, -3))
print(Point3D(2, 1, 7) - Point3D(-7, 4, 9))
print(Point((4, -2, -1, 6)) + Point((-3, 4, 8, 1)))
Max and min coordinates
You can get the biggest and smallest coordinates via the max__coord()
and min_coord()
methods. For example:
print(Point((4,5,1,8)).max_coord())
print(Point((7,5)).min_coord())
sum()
You can calculate the sum of the coordinates of the point via the sum()
method. For example:
x = Var('x')
print(Point((5, 4, 1, 3)).sum())
print(Point3D(x, 2*x, x+6).sum())
class PointCollection
The PointCollection
class represents a collection of points. Namely,
a collection of Point
objects.
Properties
points
- a list of points.
Create a new PointCollection
object
The constructor accepts a collection of points. Each point can be represented
via a Point
object, or other collections such as list
,
tuple
,set
, etc. For instance:
my_points = PointCollection([Point((2, 6)), [2, 4], Point2D(9, 3)])
print(my_points)
The points don't have
to be in the same dimensions, but it makes much more sense
that way. In addition, some features won't be available if the points have different dimensions.
The PointCollection
class has several subclasses:
Point1DCollection
, Point2DCollection
, Point3DCollection
and Point4DCollection
. You should use them if you can, even if
we're not using them in some examples for the sake of simplicity.
add_point()
You can add a new point to the collection via the add_point()
method.
For instance:
my_points = PointCollection([Point((2, 6)), [2, 4], Point2D(9, 3)])
my_points.add_point(Point((5, 3)))
print(my_points)
remove_point()
You can also remove points by their index. For example:
my_points = PointCollection([Point((2, 6)), [2, 4], Point2D(9, 3)])
my_points.remove_point(0) # remove the first point
print(my_points)
Max and min distances
You can find the longest and shortest distances between any 2 points in the collection
via the longest_distance()
and shortest_distance()
. In other words,
this methods find the distance of the furthest and closest pair of points.
However, this will only work if all of the points in the collection are in the same
dimensions, namely, have the same number of coordinates.
For instance:
my_points = PointCollection([[1, -4, 5], [7, -5, -2], [5, 3, 9], [-2, 6, 4]])
print(my_points.longest_distance())
print(my_points.shortest_distance())
You can also fetch the pairs of points:
my_points = PointCollection([[1, -4, 5], [7, -5, -2], [5, 3, 9], [-2, 6, 4]])
longest_distance, longest_pair = my_points.longest_distance(get_points=True)
print(F"Longest Distance: {longest_distance}, Longest Pair: {longest_pair}")
shortest_distance, shortest_pair = my_points.shortest_distance(get_points=True)
print(F"Shortest Distance: {shortest_distance}, Shortest Pair: {shortest_pair}")
Scatter points
You can scatter the points in 1D, 2D, 3D, and 4D. For example:
# scatter in 1D
points = PointCollection([[1], [3], [5], [6]])
points.scatter()
Output:
# scatter in 2D
points = Point2DCollection([[1, 2], [6, -4], [-3, 1], [4, 2], [7, -5], [4, -3], [-2, 1], [-3, 4], [5, 2], [1, -5]])
points.scatter()
Output:
# scatter in 3D
points = Point3DCollection([[random.randint(1, 100), random.randint(1, 100), random.randint(1, 100)] for _ in range(100)])
points.scatter()
Output:
# Scatter in 4D
points = Point4DCollection(
[[random.randint(1, 100), random.randint(1, 100), random.randint(1, 100), random.randint(1, 100)] for _ in
range(100)])
points.scatter()
Output:
class Point2DCollection
As mentioned before, the Point2DCollection
class is a subclass of the
PointCollection
class, specifically designed to handle collections of points in
the 2d space. You can create and handle Point2DCollection
objects just like
PointCollection
objects, only that you get several extra features.
Properties
This class has 2 additional properties to increase the simplicity of the interface:
x_values
- a list of the `x` coordinates of the points in the collection
y_values
- a l ist of the `y` coordinates of the points in the collection
linear_regression()
You can use the linear_regression()
method in order to find a linear function
in the form `y = ax + b`. By default, a lambda expression that represents
the linear function will be returned. However, if the get_tuple
parameter is set to True
, a tuple of the `a` and `b` coefficients
of the equation `y=ax+b` will be returned instead.
For instance:
my_dots = Point2DCollection([(1, 3), (2, 5), (3, 7), (4, 9), (5, 11), (6, 13)])
print(my_dots.linear_regression())
print(my_dots.linear_regression(get_tuple=True))
Output:
<function linear_regression<locals>.<lambda> at 0x0000026216697F70>
(2.0, 1.0)
plot_regression
You can plot the linear-regression function via the plot_regression()
function. For instance:
my_dots = Point2DCollection([(1, 3), (2, 5), (3, 7), (4, 9), (5, 11), (6, 13)])
print(my_dots.plot_regression())
Output:
scatter_with_regression()
You can scatter the points together with the linear-regression line. For instance:
points = Point2DCollection.random(100, (-10, 10))
points.scatter_with_regression()
Output:
class Graph2D
The Graph2D
class enables you to plot many functions, expressions and geometric
shapes
extremely easily in matplotlib. You just create a Graph2D
object,
add the items you wish to plot, and plot them together. You can also scatter these items
as well.
Properties
items
- a list of all of the Plottable items that were added to the
Graph2D
object
Create a new Graph2D
object
The Graph2D constructor accepts a collection of objects to be plotted in 2D axis system.
Here is the signature of the __init__()
method:
def __init__(self, objs: Iterable[IPlottable] = ()):
As you can see,
Graph2D
can only accept objects that inherit from
IPlottable
, namely, they have a matching
plot()
method.
Here is a list of the objects currently implementing this interface:
FastPoly
Poly
Log
Ln
Function
FunctionCollection
FunctionChain
Point2D
Circle
Vector2D
For example:
my_graph = Graph2D((Function("f(x) = 2x"),Poly("x^3 - 3")))
In addition, you can also initialize the Graph2D
object without anything:
my_graph = Graph2D()
Until plottable items are added, this Graph2D
object will be considered empty
and can't be plotted.
add()
You can add plottable objects to the graph via the add()
method. For instance:
my_graph = Graph2D()
my_graph.add(Function("f(x) = x^3-3"))
my_graph.add(Poly("2x^4 - 3x + 5"))
my_graph.add(Circle(5, (0, 0)))
plot()
You can plot all of the items via the plot()
method. The method takes many optional
parameters so you can customize your graph conveniently, however, you can also call the method
without any parameters(the default values will be used). Here is the signature of the method:
def plot(self, start: float = -10, stop: float = 10,
step: float = 0.01, ymin: float = -10, ymax: float = 10, text=None, show_axis=True, show=True,
formatText=False, values=None):
Here are the parameters:
start
- initial x to plot from. Default: -10
stop
- last x value in the plot. Default: 10
step
- the interval between each x. Default: 0.01
ymin
- lowest y value in the initial scope Default: -10
ymax
- highest y value in the initial scope. Default: 10
text
- customize the graph's title
formatText
- try to format the text to latex (currently experimental).
Default: False
values
- instead of start
, stop
and
step
- you can just insert a list of x values to plot in. Default:
None
For instance:
my_graph = Graph2D()
my_graph.add(Function("f(x) = x^3-3"))
my_graph.add(Poly("2x^4 - 3x + 5"))
my_graph.add(Circle(5, (0, 0)))
my_graph.plot()
Output:
Class Graph3D
class Graph3D(Graph)
The Graph3D
enables you to plot expressions and functions in 3D conveniently.
Class Vector
Vectors are something that has both magnitude and direction. For instance, the vector
`(2, 3)` represents moving moving `2` units on the `x` axis, and `3` units on the `y` axis.
Vectors are essential to a plethora of math fields, including Linear Algebra, and therefore
they are supported in this library.
You can create and work with vectors via the Vector
class.
While you can create vectors with any dimension, you can only plot vectors with `2` or `3`
elements,
in 2D and 3D, respectively.
The Vector
class also has 2 subclasses: Vector2D
and Vector3D
. You should use them instead if you're working with 2D and 3D vectors.
Properties
Each vector has the following properties:
direction_vector
- returns the direction vector which also end_coordinate
minus
start_coordinate
- returns the start coordinate, the origin of the vector
end_coordinate
- returns the end coordinate, the end of the vector
Creating a new Vector
object
There are several ways to create a vector object:
-
Enter the start_coordinate and the end_coordinate
Vector(start_coordinate=(1,2),end_coordinate=(4,6))
-
Enter the start_coordinate and the direction_vector
Vector(direction_vector=(3,4),start_coordinate=(1,2))
-
Enter the end_coordinate and the direction_vector
Vector(direction_vector=(3,4),end_coordinate=(4,6))
-
Enter only the direction vector, start_coordinate will be (0,0...)
as default.
Vector(3,4))
print(Vector(start_coordinate=(1,2),end_coordinate=(4,6)))
print(Vector(direction_vector=(3,4),start_coordinate=(1,2)))
print(Vector(direction_vector=(3,4),end_coordinate=(4,6)))
print(Vector(direction_vector=(3,4)))
# output:
# 'start: [1, 2] end: [4, 6] direction: [3, 4]'
# 'start: [1, 2] end: [4, 6] direction: [3, 4]'
# 'start: [1, 2] end: [4, 6] direction: [3, 4]'
# 'start: [0, 0] end: [3, 4] direction: [3, 4]'
Creating a copy of a vector
You can create a copy of a Object
via the __copy__()
method.
For instance:
vec = Vector(start_coordinate=(7, 8, 5), direction_vector=(2, 5, 4))
print(vec.__copy__())
# output:
# start: [7, 8, 5] end: [9, 13, 9] direction: [2, 5, 4]
Adding Vectors
Where adding vectors, we'll actually be adding up the direction_vectors,
without considering the start and the end of each vector.
Using __add__()
and __radd__()
allows us to use the `+` operator
to
add up vectors. For example:
vec1 = Vector((1, 1, 1))
vec2 = Vector((2, 2, 2))
vec3 = Vector((3, 3, 3))
print(vec1 + vec2 + vec3)
# output:
# 'start: [0, 0, 0] end: [6, 6, 6] direction: [6, 6, 6]'
Subtracting Vectors
You can subtract vectors too using the `-` operator, thanks to the built-in method
__sub__()
. For instance:
vec1 = Vector((1, 1, 1))
vec2 = Vector((2, 2, 2))
vec3 = Vector((3, 3, 3))
print(vec3 - vec2 - vec1)
# output:
# 'start: [0, 0, 0] end: [0, 0, 0] direction: [0, 0, 0]'
Scalar Multiplication Of Vectors
The result of the product of two vectors is a number.
The multiplication of 2 vectors is equal to the sum of
the products of the matching coordinates from both vectors.
For example, `(6, 2, 5) * (3, 4 ,2) = 6 * 3 + 2 * 4+ 5 * 2 = 36.`
With __mul__()
, you can use the * ( asterisk ) operator
to multiply vectors. For example:
vec1 = Vector((6, 2, 5))
vec2 = Vector((3, 4, 2))
print(vec1*vec2)
# output:
# 36
Flipping a vector's direction
In order to flip the direction of a vector, you change the sign
of each of its coordinates. With __neg__ ()
, this can be done with the
subtraction operator. For example:
vec = Vector((-1, 2, 3))
print(vec)
print(-vec)
or
print(vec.__neg__())
# output:
# start: [0, 0, 0] end: [-1, 2, 3] direction: [-1, 2, 3]
# start: [-1, 2, 3] end: [0, 0, 0] direction: [1, -2, -3]
Getting The Vector's Length
There are several approaches to get the vector's length, but they are
are all basically different sides of the same coin. For example:
vec = Vector((3, 4))
print(len(vec)) # first approach using len()
print(vec.__len__()) # second approach using __len__()
print(vec.length()) # third approach using length()
# output:
# 5
# 5
# 5
Checking whether vectors are equal
Vectors are considered equal if they have the same direction vector.
You can check equality with the ==
operator. For instance:
print(Vector((3, 4, 8)) == Vector((3, 4, 8)))
Output:
True
Equating directions
You can check whether vectors have the same direction via the
equal_direction_ratio()
method. Namely, whether there's a ratio between their
direction vectors. For instance:
print(Vector((1, 2, 4)).equal_direction_ratio(Vector((2, 4, 8))))
print(Vector((x, 2 * x, 4 * x)).equal_direction_ratio(Vector((2 * x, 4 * x, 8 * x))))
print(Vector((x, 2 * x, 4 * x)).equal_direction_ratio(Vector((2 * x ** 2, 4 * x ** 2, 8 * x ** 2))))
Output:
True
True
True
Class VectorCollection
Sometimes, you want to apply certain processes to a group of vectors - in that case
you can use the VectorCollection
class.
The VectorCollection
class, as its name suggests, represents a collection of
vectors.
By defining a VectorCollection rather than a simple list of Vector objects, you can plot
all the vectors in a single axis system, instead of separately, and use other basic analysis
tools.
How To Create A New Vector Collection
def __init__(self, *vectors):
In order to create a VectorCollection, you could enter as much vectors as you wish as
parameters. Each vector should be represented by a Vector object, However, tuples and lists
which are entered to the constructor will be converted automatically to Vector objects and added
to the collection.
For Example:
# You can enter Vector objects, lists, or tuples.
vectors = VectorCollection(Vector((7, 5, 3)), (8, 1, 2), [9, 4, 7])
# Or if you already have an existing list of vectors:
lst_of_vectors = [Vector((7, 5, 3)), (8, 1, 2), [9, 4, 7]]
other_vectors = VectorCollection(*lst_of_vectors)
Properties:
vectors
- you can get and set the list of vectors of the collection with
this
property.
-
num_of_vectors
- you can get the number of vectors in the collection,
namely,
the
length of the vectors list.
How to plot the vectors ?
The vectors will be displayed via the matplotlib library,
using the plot_in_range(start=0,end=None )
method.
This method accepts two optional parameters:
-
start
- where the plotting starts. default value is `0`.
-
end
- where the plotting ends. default value is None. If left unchanged,
it will be set automatically to a proper value.
you can use the plot_all()
method to call plot_in_range()
with its default values in a more succinct way. plot()
doesn't accept any
parameters.
For example, plotting vectors in 2D and 3D:
vectors = VectorCollection(Vector(start_coordinate=(7, 5), end_coordinate=(3, 4)), Vector((1, 9)))
vectors.append(Vector(start_coordinate=(2, 2), end_coordinate=(7, 7)))
vectors.plot()
vectors = VectorCollection(Vector((7, 5, 3)), Vector((8, 1, 2)), Vector((9, 4, 7)))
vectors.plot()
# shortly after, a matplotlib graph will be displayed.
Finding the vectors with the max and min lengths in the collection
You can find the vector with the maximum length with longest( )
and
the vector with the shortest length with shortest( ),
as shown below:
# creating the vectors and printing the initial collection
vectors = VectorCollection(Vector((2, 1, 6)), Vector((8, 1, 2)), Vector((9, 4, 7)))
# fetching both the index of the longest vector in the collection, and the longest vector itself.
index, longest_vector = vectors.longest(get_index=True)
print(f"Vector {longest_vector} is in index {index}")
# fetching both the index of the shortest vector and the shortest vector, but this time, also removing it from
# the collection.
index, shortest_vector = vectors.shortest(get_index=True, remove=True)
print(f"removed the vector {shortest_vector} from index {index}")
There are also two optional parameters for both of the functions:
get_index
( of type bool
: if set to True, the functions will
return a
tuple in the form: (index, vector),
Namely, a tuple which consists of the vector's index in the collection and the vector
itself,
will
be returned.
The default value is False
remove ( of type bool
): If set to True, the vector that will be returned, will
also
be removed from
the collection.
The default value is True
.
For example:
# UPDATE ALL OF THE EXAMPLES ON THE VECTOR CLASS !@!!!!
# creating the vectors and printing the initial collection
vectors = VectorCollection(Vector((2, 1, 6)), Vector((8, 1, 2)), Vector((9, 4, 7)))
print(f"The original collection: {[vector.get_direction() for vector in vectors]}")
# fetching both the index of the longest vector in the collection, and the longest vector itself.
index, longest_vector = vectors.longest(get_index=True)
print(f"The vector {longest_vector.get_direction()} is located in index {index}.")
# fetching both the index of the shortest vector and the shortest vector, but this time, also removing it from
# the collection.
index, shortest_vector = vectors.shortest(get_index=True, remove=True)
print(f"The vector {shortest_vector.get_direction()} was removed from index {index}.")
# printing the modified expression
print(f"The modified collection: {[vector.get_direction() for vector in vectors]}")
# output:
# 'The original collection: [[2, 1, 6], [8, 1, 2], [9, 4, 7]]'
# 'The vector [9, 4, 7] is located in index 2.'
# 'The vector [2, 1, 6] was removed from index 0.'
# 'The modified collection: [[8, 1, 2], [9, 4, 7]]'
class Circle
class Circle(IPlottable):
The Circle
class enables you to represent circles of the equations of the form
`(x-a)^2 + (y-b)^2 = R^2`, where `(a, b)` is the center of the circle and `R` is its radius.
Properties
radius
- the radius of the circle.
diameter
- the diameter of the circle
center
- the center of the circle.
center_x
- the x coordinate of the center point
center_y
- the y coordinate of the center point.
left_edge
- the left edge of the circle.
right_edge
- the right edge of the circle.
top_edge
- the top edge of the circle
bottom_edge
- the bottom edge of the circle.
Create a new circle object
You can create a a new circle object by entering the radius of the circle and its center.
For instance:
x = Var('x')
my_circle = Circle(5, (3, 3))
other_circle = Circle(x+5, (Cos(x), Sin(x)))
Area
The area of a circle is calculated via the `\pir^2`. You can compute the area of a circle
via the area()
method. For example:
r = Var('r')
print(Circle(5, (3,1)).perimeter())
print(Circle(r, (1, -4)).perimeter())
Perimeter
The perimeter of a circle is calculated via the `2\pir`. You can find the perimeter of
a circle via the perimeter()
method.
r = Var('r')
print(Circle(5, (3, 1)).perimeter())
print(Circle(r, (1, -4)).perimeter())
point_inside()
Check whether the given point is located inside the circle. You can enter a Point object or
any iterable object as a point.
my_circle = Circle(5, (0, 0))
print(my_circle.point_inside((1, 1)))
is_inside()
Check whether the circle is inside another circle.
small_circle = Circle(5, (0, 0))
big_circle = Circle(10, (0, 0))
plot()
Plot the circle on an axis system. For instance:
my_circle = Circle(5, (0, 0))
my_circle.plot()
Matrices are one of the key foundations for Linear Algebra.
They have countless mathematical and scientific applications. We offer a basic
implementation
of a matrix, via the Matrix
class. The advantage of using this class,
is that it's able to integrate with other classes in this module ; for instance, you can create
matrices
with polynomials, trigonometric expressions, logarithms, and much more. In the future,
further support for Linear Algebra operations will be added, speed optimizations will be
conducted
via C or C++, and compatibility with NumPy will be added as well.
properties
- num_of_rows(int) - the number of num_of_rows in the matrix. -
- num_of_columns (int) - the number of num_of_columns in each row.
- matrix - a list of lists that represent the matrix.
def __init__(self, dimensions=None, matrix=None):
Parameters for initializing
- dimensions(Iterable) - you can create a matrix filled with zeroes in the given
dimensions,
in the syntax
(rows, columns)
.
For example: (5, 6)
, [5, 6]
, "5x6"
,
"5,6"
- matrix - if you want to create a matrix from an existing matrix, you can enter it
here ( list
of lists ).
Here are a few examples of creating new matrices:
mat = Matrix(dimensions=(3, 3))
mat1 = Matrix(dimensions="2x2")
mat2 = Matrix(dimensions="1,3")
mat3 = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(mat)
print(mat1)
print(mat2)
print(mat3)
Swapping Rows
you can swap num_of_rows with the replace_lines
method. Here is the method's
signature
def replace_lines(self, line1:int, line2:int):
line1
is the index of the first row,
and line2
is the index of the second row. For example:
mat = Matrix(matrix=[[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(mat)
mat.replace_rows(0, 1)
print(mat)
Multiplying a row
You can multiply a row in the matrix by an expression via the
multiply_row()
method.
For example:
mat = Matrix(matrix=[[1, 2, 3], [4, 5, 6], [7, 8, 9]])
mat.multiply_row(2,row=1)
print(mat)
Dividing a row
You could divide a row by a scalar using the divide_row( )
method.
method, which does not accept division by `0` .
you can also divide the row using the multiply_row(
)
method
by multiplying by the reciprocal of the scalar. For example, multiplying by `\frac(1)(3)` is
the
same
as dividing by `3`.
my_matrix = Matrix([[1,2,3],[4,5,6]])
my_matrix.divide_row(2,row=0)
Finding the min and max
While you can find the smallest and biggest values of a matrix yourself just by
yourself by iteration, you can just use the min()
and max()
from the Matrix
object. For example:
mat = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f'the maximum is {mat.max()}')
print(f'the minimum is {mat.min()}')
output:
the maximum is 9
the minimum is 1
Finding the sum of the matrix
You can get the sum of all the elements in the matrix, by using the sum()
method from the Matrix
object.
# Add an example with IExpression expressions !!!!!!!!!!!!
mat = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(mat.sum())
# output:
# 45
Equating Matrices
You can check whether two matrices are equal via the __eq__()
method, or the
`==` operator ( It's the same thing ). For example:
mat = Matrix([[5, 3, 2], [8, 7, 1]])
mat1 = Matrix([[6, 4, 7], [9, 1, 3]])
if mat == mat1:
print("The matrices are equal")
else:
print("The matrices are not equal")
# output:
# 'The matrices are not equal'
Iterating on a matrix
There are several approaches to iterate over a Matrix object.
1. Iterate on the matrix property
Each Matrix object has a matrix property, which is just a list of lists.
You can iterate on it just like any other list. For example:
mat = Matrix(((1, 2, 3), (4, 5, 6), (7, 8, 9)))
for row in mat.matrix:
for item in row:
print(item)
2.Iterate directly on the object
You actually don't have to use the matrix property, since the Matrix class implements
the __ iter __ ()
and __ next __ ()
methods, and hence a matrix object
is considered iterable.
Example 8 - Iterating directly on the Matrix object
mat = Matrix(((1, 2, 3), (4, 5, 6), (7, 8, 9)))
for row in mat:
for item in row:
print(item)
Iterate on the indices
Since a matrix is composed of a list of lists, and lists follow indices, you can access elements
in the matrix by two indices, the index of the row(starting from 0) and the index of the column
(starting from 0). This can be done by calling the range() method from the Matrix object.
This method will yield the row index and the column index. For example:
mat = Matrix(matrix=((1, 2, 3), (4, 5, 6), (7, 8, 9)))
for i, j in mat.range():
print(f"The item in row {i} and column {j} is {mat[i][j]}")
Adding Matrices
You can add matrices with the same dimensions. Each item in the new matrix will be
the sum of the corresponding elements in the two matrix. For instance, here is how to add
matrices with the dimensions `2xx2`:
`[[x_1,x_2],[x_3,x_4]] + [[y_1,y_2],[y_3,y_4]] = [[x_1+y_1, x_2+y_2],[x_3+y_3,x_4+y_4]]`
Here is an example with some easy numbers to clarify:
`[[1,5],[3,2],[4,6]] + [[7,4],[6,1],[2,3]] = [[8,9],[9,3],[6,9]] `
You can add up Matrix
objects by using the `+` operator. For example:
mat1 = Matrix([[1,3],[7,6]])
mat2 = Matrix([[5,8],[4,2]])
print(mat1+mat2)
Subtracting Matrices
Just like adding matrices, you can only subtract matrices with the same dimensions, namely,
the same number of rows and columns. For instance:
`[[2,4],[1,7],[8,3]] - [[5,5],[1,4],[3,2]] = [[-3,-1],[0,3],[5,1]] `
You can subtract matrices via the `-` operator. For example:
mat1 = Matrix([[2,4],[1,7],[8,3]])
mat2 = Matrix([[5,5],[1,4],[3,2]])
print(mat1-mat2)
Multiplying Matrices
2 Matrices can be multiplied if the number of columns in one of the matrices is equal
to the number of rows in the other matrix. Namely, the two matrices need to be in the dimensions
`mxxn` and `nxxp`.
An element in the row `i` and the column `j` in the new matrix will be equal
to the dot product of the line at index `i` in the first matrix and the column at index `j` in
the
second matrix. For instance:
`[[3,2],[7,4],[9,1]] * [[2,4,1],[6,3,2]] = [[(3,2)*(2,6),(3,2)*(4,3),(3,2)*(1,2)],
[(7,4)*(2,6),(7,4)*(4,3),(7,4)*(1,2)],[(9,1)*(2,6),(9,1)*(4,3),(9,1)*(1,2)]] =
[[18,18,7],[38,40,15],[24,39,11]]`
You can multiply Matrix
objects with the @ operator. For example:
mat1 = Matrix([[3,2],[7,4],[9,1]])
mat2 = Matrix([[2,4,1],[6,3,2]])
print(mat1@mat2)
Determinants
You can find the determinant of a square matrix via the determinant()
method.
Useful to know, if the columns of the matrix are linearly dependant, the determinant will always
be 0. Also, if the determinant of a matrix is 0, we can deduce that it does not have an inverse.
mat = Matrix([[1,2,3],[4,5,6],[7,8,9]])
print(mat.determinant())
For a deeper dive into determinants, I highly recommend watching
3Blue1Brown's video:
Transpose
The transpose of a matrix `A` is denoted `A^T`, and it's a new matrix where the rows become
the columns, and the rows become the columns. Therefore, for a matrix `A` with dimensions `mxxn`
the transpose `A^T` has the dimensions `nxxm`. For instance:
`A = [[1,2],[3,4],[5,6]]`, `A^T = [[1,3,5],[2,4,6]]`
You can get the transpose of a Matrix object via the transpose
method. For example:
mat = Matrix([[1,2],[3,4],[5,6]])
print(mat.transpose())
Inverse
You can find the inverse of a matrix via the inverse()
method. If the matrix
doesn't have an inverse the method will return None
. For example:
mat = Matrix([[1,2,3],[4,5,6],[7,8,9]])
print(mat.inverse())
mat = Matrix([[1,2],[3,4]])
print(mat.inverse())
class Surface
Surface
are used to represents surfaces with the equation
`ax + by + cz + d = 0`
Properties
a
- the `a` coefficient in the equation.
b
- the `b` coefficient in the equation.
c
- the `c` coefficient in the equation.
d
- the `d` coefficient in the equation.
Creating a new Surface
object
You can create a new Surface
object by entering a string that represents the
equation or with a collection of the coefficients `a`, `b`, `c`, and `d`.
You can plot it too later. For instance:
my_surface = Surface("7x + 3y + z + 9 = 0")
my_surface.plot()
my_surface = Surface([7, 3, 1, 9])
my_surface.plot()
Output:
Checking Surface
Equality
You can check whether Surface
objects with the ==
operator.
For example:
first_surface = Surface("7x + 3y + z + 9 = 0")
second_surface = Surface([7, 1, 3, 9])
print(first_surface == second_surface)
class ArithmeticProg
In an arithmetic progression, there is a constant difference between each two
consecutive items in the sequence. You can represent this kind of sequences via the
ArithmeticProg
class.
class ArithmeticProg(Sequence):
Properties
difference
- the difference between each pair of items in the sequence.
first
- the first element.
Creating a new ArithmeticProg
object
The constructor takes 2 parameters:
first_numbers
- a collection of the first numbers
difference
- the ratio of the series.
You have two ways to use the constructor:
- Enter the first element and the difference of the series.
For example:
my_sequence = ArithmeticProg([3], 2)
- Enter at least 2 of the first elements of the series. Then the difference
will calculated internally. For example:
my_sequence = ArithmeticProg([3, 5])
print(my_sequence)
in_index()
You can fetch an item in the sequence by its index via the in_index()
method.
Just pay attention that indices of sequences start from 1 and not from 0.
For instance:
my_sequence = ArithmeticProg([3], 2)
print(my_sequence.in_index(2))
sum_first_n()
You can find the sum of the first `n` elements in the series via the
sum_first_n()
method. The sum is calculated via the formula
`S = \frac(n)(2)(2a_1 + d(n-1))`.
For example:
my_sequence = ArithmeticProg([3], 2)
print(my_sequence.sum_first_n(3))
index_of()
Get the index of a given number in the sequence. If the given number is not in the sequence,
-1 will be returned.
For example:
my_sequence = ArithmeticProg([3], 2)
print(my_sequence.index_of(7))
print(my_sequence.index_of(10))
GeometricSeq
Geometric sequences are sequences where there's a constant ratio between each pair of
consecutive items. For instance: `2, 4, 8, 16 ...`.
You can create geometric sequences via the GeometricSeq
class.
Properties
first
- the first item in the sequence
ratio
- the ratio between every two consecutive items in the series.
Create a new GeometricSeq
object
The constructor takes two parameters:
first_numbers
- a collection of the first items in the sequence
ratio
- the ratio of the sequence
You have two ways to create a new geometric sequence:
- Enter the first item of the sequence, and the ratio. For instance:
my_sequence = GeometricSeq([2], 2)
print(my_sequence)
- Enter at least two of the first items in the sequence.
my_sequence = GeometricSeq([2,4])
print(my_sequence)
in_index()
You can find an element in a specified index via the in_index()
method. For example:
my_sequence = GeometricSeq([2, 4])
print(my_sequence.in_index(3))
index_of()
You can find the index of an element in the sequence via the in_index()
method.
If the element doesn't exist in the sequence, -1 will be returned. For instance:
my_sequence = GeometricSeq([2], 2)
print(my_sequence.index_of(8))
If you only need to know if an item exists in the sequence, you should you use the
in
operator. For example:
my_sequence = GeometricSeq([2], 2)
print(8 in my_sequence)
print(7 in my_sequence)
plot()
You can also plot the sequence via the plot()
method from the Sequence
base class. For example:
my_sequence = GeometricSeq([2], 2)
my_sequence.plot(start=1, stop=10, step=1)
Class RecursiveSeq
Recursive sequences are sequences where each item is recursively defined by previous items.
The most known example of a recursive sequence is the Fibonacci sequence, named after the famous
italian mathematician Fibonacci (11-12th centuries). The first two items in the Fibonacci
sequence are `1, 1` and each element later is equal to the sum of the two previous items:
`1, 1, 2, 3, 5, 8, 13, 21, 34...`
The sequence has been discovered to possess many intriguing qualities, and even take place in
natural phenomena.
Using this class, one may declare his own Fibonacci sequence, and actually almost any recursive
sequence he/she had in mind. You can define and work with the RecursiveSeq
class.
How to create a new recursive sequences
The constructor for the RecursiveSeq
class requires a string and an Iterable
of the first
elements of the sequence.
The string should represent the recursive sequence,
and must follow a specific syntax; The current value of the sequence is written as
a_n
, previous items can be written as a_{n-1}, a_{n-2} ...
and the
next items can be written as a_{n+1}, a_{n+2} ...
A recursive sequence is defined by the the definition of the a_n
.
Therefore, the string must always begin with the definition of a_n
:
"a_n = ....."
For example, this string represents the Fibonacci sequence,
where the initial values of the sequence are `(1 ,1, 2)`:
"a_n = a_{n-1} + a_{n-2}"
And this string is a recursive definition for
factorial, where the first elements are
`(1,
2)`.
a_n = a_{n-1} * n
There is one key concept one has to remember when entering the first elements of a recursive
sequence to this interface; For each previous item you refer to in the string,
you have to add one more initial value. For example, if only refer to
a_n
in the
string, you need
to enter at least 1 initial value,
but if you also refer to
a_{n-1}
, you need to enter at least 2 initial values, and so
on.
That's why factorial requires 2 starting values, and Fibonacci takes 3 starting values.
Here is how these sequences will actually look in the code:
fibonacci = RecursiveSeq("a_n = a_{n-1} + a_{n-2}", (1, 1, 2))
my_factorial = RecursiveSeq("a_n = a_{n-1} * n", (1, 2))
You can also use more advanced calculations in order to define your own recursive sequence.
Here are some examples:
# An example of a custom recursive method.
custom_recursion = RecursiveSeq("a_n = a_{n-1}^2 + 0.5*ln(a_{n-2})", [e, 1, 1.5])
print(f"The custom recursive method is {custom_recursion.at_n(4)} at place 4")
Note
in later versions some syntax limitations might be removed, and
new features might be added.
Contact us via E-mail and GitHub on
suggestions
and bug fixes related to the Sequence
class.
Fetching the item on the `n`th place
Declaring the sequence is the most difficult part. Once you manage to that, you can use all
the
features effortlessly. One of the most important of these features is finding the item on
given
place.
The only thing that one needs to remember here is that unlike arrays, sequences start from
the index `1`. Thus, index `1` represents the first item, index of `2` to represents the second,
etc.
There are two syntactic approaches to fetch the item in the `n`th place
-
Use the method
at_n()
-
Use indexers ([ ])
Iterating on a sequence
You can iterate on elements of a sequence within a given range using a
for
loop, in
two
main approaches.
-
Use the
range()
method.
-
Use slicing, in a
([start:stop:step])
format
For example, lets represent a simple scenario where all of the student take a test. 40% will
pass the test, while 60% will fail. Out of the 40% who passes the test, 10% will ace it.
If the probability to pass the test is `p_k`, and the the probability to ace it out of those
who passed it is `p_n`, the probability of a random student to ace the test is
`p_f = p_k * p_n`. We know that `p_k = 0.4`, and `p_n = 0.1`, and therefore `p_f = 0.4 * 0.1 =
0.04`.
So finally, we know that only `4%` of the students aced the test, which means that either the
teacher
is terrible at his job, or the students are stupid.
In case these last two paragraphs haven't been clear, here is how you would visualize the tree:
Each node in the json representation of the tree must contain
three keys: parent ( the parent node ), identifier ( the unique name of the node )
and the chance of the occurrence, as shown below:
The first node will always be the root node. Its parent
attribute will be set
to None.
As mentioned earlier, each node in the tree has a unique string, called identifier
.
You can fetch a node by its identifier with the get_node_by_id()
method.
Here is the signature of the method:
The path of a node in the tree gives us a sense of its location, similar to a system of files.
We will also disregard the root element in the path. We can fetch the path of a node with the
get_node_path()
method: