HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonModerate

Visual solution of the Newtonial differential equation with Python

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thevisualwithnewtonialdifferentialpythonsolutionequation

Problem

I wrote this Python code that plots the movement of an object under the effect of a given force function in 2D by solving the Newton's movement equation numerically. One can add other force functions, or even parameters to the draw_path function. I tried to make it as readable as I could.
I would really appreciate if you could tell me what did I wrong, and what would have you done diferently.

Since I learned programming only by tutorials and codes, never from proper lessons/courses, and this is my first finished code, I probably did some weird things. Since I am not a native speaker, I probably wrote some weird comments. Sorry.

```
#!/usr/bin/env python

# movement_equation_2.0.py
#
# Copyright 2016 Nagy Gergely
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# :Author: Nagy Gergely
# :Version: 0.2.1 beta
# :Status: Prototype
# :Date: 2016.01.05

"Non-physics functions"

from PIL import Image, ImageDraw

#dimensions for vectors
x = 0
y = 1

def plot_base(xmin, xmax, ymin, ymax):
"""
Creates a base for the plot: a white picture with two orthogonal lines, the x
and y axis, according to the given minimum and maximum coordinates in pixels.
"""
base_color = 'white'
axis_color = 'grey'

xsize = abs(xmax - xmin)
ysize = abs(ymax - ymin)
plot = Image.new('RGB', (xsize, ysize), base_color)
draw = ImageDraw.Draw(plot)

#draw x axis if shown:
if ymin 0:
draw.line((0, ymax, xsize, ymax), axis_color)
elif ymax > 0:
draw.line((0, ysize, xsize, ysize), axi

Solution

This is pretty good work for a self-taught programmer. I'd like to make a few suggestions to improve readability.

First, I would define a 2D vector class. That would allow you to simplify expressions where you are doing the same operation to both the x and the y coordinate, such as r = (r[x] + v[x]dt + a[x]/2dt2 , r[y] + v[y]dt + a[y]/2dt2), into

r += (v * dt) + (a * dt**2 / 2)


To achieve that, I would use namedtuple, so that you can have vec.x and vec.y members instead of vec[x] and vec[y].

from collections import namedtuple
import math
from PIL import Image, ImageDraw

class Vec2(namedtuple('Vec2', 'x y')):
    def __add__(self, other):
        return Vec2(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vec2(self.x - other.x, self.y - other.y)

    def __mul__(self, scale):
        return Vec2(self.x * scale, self.y * scale)

    def __truediv__(self, scale):
        return Vec2(self.x / scale, self.y / scale)

    def rotate(self, angle):
        sin, cos = math.sin(angle), math.cos(angle)
        return Vec2(self.x * cos - self.y * sin, self.x * sin + self.y * cos)


The Vec2 class would also let the code express your mathematical intentions — for example, your entire eight-line v_dep() function could be written as…

###############################################################################
# FORCE FUNCTIONS
###############################################################################
# Force functions for the movement plotter. They should take the paramaters as
# keyword arguments (use **kwargs to be compatible with more parameters in the
# future), and return the coordinates of the force vector at these parameters
# as a 2-tuple.  Possible variables at the moment: m, r, v
###############################################################################

def v_dep(**variables):
    """force depends on the velocity vector
       (i.e. charged particle in magnetic field)"""
    return variables['v'].rotate(math.pi / 2)


The other change would be to disentangle the physics from the plotting. In particular, I'd prefer not to have code like pix_pos_x = round(r[x]/resolution) - plot_pixsize[0] in your draw_path loop, which is already complicated as it is.

I suggest defining a Canvas class to contain the image, the bounds information, and handle the physical-to-pixel coordinate translation.

class Canvas:
    def __init__(self, bounds, resolution, base_color='white'):
        """
        :param bounds: positions of the upper-left and lower-right corners, each as a Vec2
        :param resolution: resolution of the result plot image (meter/pixel)
        """
        self._nw_corner, self._se_corner = bounds
        self._resolution = resolution

        width = round((self._se_corner.x - self._nw_corner.x) / resolution)
        height = round((self._nw_corner.y - self._se_corner.y) / resolution)
        self._plot = Image.new('RGB', (width, height), base_color)
        self._draw = ImageDraw.Draw(self._plot)

        self._pos = None
        self.color = 'black'

    def draw_axes(self, axis_color='grey'):
        #draw x axis
        self._draw.line([
            self._phys_to_draw(Vec2(self._nw_corner.x, 0)),
            self._phys_to_draw(Vec2(self._se_corner.x, 0))
        ], axis_color)

        #draw y axis
        self._draw.line([
            self._phys_to_draw(Vec2(0, self._nw_corner.y)),
            self._phys_to_draw(Vec2(0, self._se_corner.y))
        ], axis_color)

    def draw_to(self, r):
        """Draw a line from the previous position (if any) to the specified position"""
        self._prev_pos, self._pos = self._pos, self._phys_to_draw(r)
        if self._prev_pos is not None:
            self._draw.line((self._prev_pos, self._pos), fill=self.color)

    def show(self):
        self._plot.show()

    def _phys_to_draw(self, r):
        """Translate physical coordinates to image coordinates"""
        return (
            round((r.x - self._nw_corner.x) / self._resolution),
            round((self._nw_corner.y - r.y) / self._resolution)
        )


Here's the rest of the code:

```
def draw_path(canvas, m, r0, v0, force_function, duration, dt):
"""
Draws the path of an object with weight m starting from r0 with velocity v0,
according to the force function given in the force_function method.

:param m: the mass of the object
:param r0: the coordinates of the object at t=0 in meters (2-tuple)
:param v0: the coordinates of the initial velocity vector (2-tuple)
:param force_function: the function of the force applied to the object, described below
:param duration: the duration of the movement
:param dt: time elapsed between two calculated points: the bigger it is, the faster but less accurate is the result
"""
r = r0
v = v0

canvas.color = (255, 0, 0)
canvas.draw_to(r)
for _ in range(int(duration / dt)):
F = force_function(m

Code Snippets

r += (v * dt) + (a * dt**2 / 2)
from collections import namedtuple
import math
from PIL import Image, ImageDraw

class Vec2(namedtuple('Vec2', 'x y')):
    def __add__(self, other):
        return Vec2(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vec2(self.x - other.x, self.y - other.y)

    def __mul__(self, scale):
        return Vec2(self.x * scale, self.y * scale)

    def __truediv__(self, scale):
        return Vec2(self.x / scale, self.y / scale)

    def rotate(self, angle):
        sin, cos = math.sin(angle), math.cos(angle)
        return Vec2(self.x * cos - self.y * sin, self.x * sin + self.y * cos)
###############################################################################
# FORCE FUNCTIONS
###############################################################################
# Force functions for the movement plotter. They should take the paramaters as
# keyword arguments (use **kwargs to be compatible with more parameters in the
# future), and return the coordinates of the force vector at these parameters
# as a 2-tuple.  Possible variables at the moment: m, r, v
###############################################################################

def v_dep(**variables):
    """force depends on the velocity vector
       (i.e. charged particle in magnetic field)"""
    return variables['v'].rotate(math.pi / 2)
class Canvas:
    def __init__(self, bounds, resolution, base_color='white'):
        """
        :param bounds: positions of the upper-left and lower-right corners, each as a Vec2
        :param resolution: resolution of the result plot image (meter/pixel)
        """
        self._nw_corner, self._se_corner = bounds
        self._resolution = resolution

        width = round((self._se_corner.x - self._nw_corner.x) / resolution)
        height = round((self._nw_corner.y - self._se_corner.y) / resolution)
        self._plot = Image.new('RGB', (width, height), base_color)
        self._draw = ImageDraw.Draw(self._plot)

        self._pos = None
        self.color = 'black'

    def draw_axes(self, axis_color='grey'):
        #draw x axis
        self._draw.line([
            self._phys_to_draw(Vec2(self._nw_corner.x, 0)),
            self._phys_to_draw(Vec2(self._se_corner.x, 0))
        ], axis_color)

        #draw y axis
        self._draw.line([
            self._phys_to_draw(Vec2(0, self._nw_corner.y)),
            self._phys_to_draw(Vec2(0, self._se_corner.y))
        ], axis_color)

    def draw_to(self, r):
        """Draw a line from the previous position (if any) to the specified position"""
        self._prev_pos, self._pos = self._pos, self._phys_to_draw(r)
        if self._prev_pos is not None:
            self._draw.line((self._prev_pos, self._pos), fill=self.color)

    def show(self):
        self._plot.show()

    def _phys_to_draw(self, r):
        """Translate physical coordinates to image coordinates"""
        return (
            round((r.x - self._nw_corner.x) / self._resolution),
            round((self._nw_corner.y - r.y) / self._resolution)
        )
def draw_path(canvas, m, r0, v0, force_function, duration, dt):
    """
    Draws the path of an object with weight m starting from r0 with velocity v0,
    according to the force function given in the force_function method.

    :param m: the mass of the object
    :param r0: the coordinates of the object at t=0 in meters (2-tuple)
    :param v0: the coordinates of the initial velocity vector (2-tuple)
    :param force_function: the function of the force applied to the object, described below
    :param duration: the duration of the movement
    :param dt: time elapsed between two calculated points: the bigger it is, the faster but less accurate is the result
    """
    r = r0
    v = v0

    canvas.color = (255, 0, 0)
    canvas.draw_to(r)
    for _ in range(int(duration / dt)):
        F = force_function(m=m, r=r, v=v)
        a = F / m
        v += a * dt
        r += (v * dt) + (a * dt**2 / 2)
        canvas.draw_to(r)




###############################################################################
# MAIN MOVEMENT DRAWER FUNCTION
###############################################################################

def main(arg):
    canvas = Canvas(
        bounds=(Vec2(-10, 10), Vec2(10, -10)),
        resolution=0.01,                #meters per pixel on the plot
    )
    canvas.draw_axes()
    draw_path(
        canvas=canvas,
        m=0.1,                          #mass of the object in kilograms
        r0=Vec2(0, 0),                  #initial coordinates in meters
        v0=Vec2(30, 0),                 #initial velocity vector in m/s
        force_function=v_dep,           #force function from above
        duration=10,                    #time of movement to draw in secs
        dt=1/70000,                     #time between steps in secs, determines accuracy and running time
    )
    canvas.show()



if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

Context

StackExchange Code Review Q#115916, answer score: 11

Revisions (0)

No revisions yet.