Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype System class. #81

Merged
merged 18 commits into from
Aug 7, 2014
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions examples/double_pendulum/double_pendulum.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
KM = KanesMethod(N, q_ind=[q1, q2], u_ind=[u1, u2], kd_eqs=kd)


(fr, frstar) = KM.kanes_equations(FL, BL)
kdd = KM.kindiffdict()
mass_matrix = KM.mass_matrix_full
forcing_vector = KM.forcing_full
qudots = mass_matrix.inv() * forcing_vector
qudots = qudots.subs(kdd)
qudots.simplify()

KM.kanes_equations(FL, BL)
#kdd = KM.kindiffdict()
#mass_matrix = KM.mass_matrix_full
#forcing_vector = KM.forcing_full
#qudots = mass_matrix.inv() * forcing_vector
#qudots = qudots.subs(kdd)
#qudots.simplify()
#
39 changes: 6 additions & 33 deletions examples/double_pendulum/simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,17 @@

from double_pendulum import *

# List the symbolic arguments
# ===========================

# Constants
# ---------

constants = {l: 10.0, m: 10.0, g: 9.81}

# Time-varying
# ------------

coordinates = [q1, q2]

speeds = [u1, u2]


# Generate function that returns state derivatives
# ================================================

xdot_function = generate_ode_function(mass_matrix, forcing_vector,
constants.keys(), coordinates, speeds)
initial_conditions = {q1: 1.0, q2: 0.0, u1: 0.0, u2: 0.0}


# Specify numerical quantities
# ============================

initial_coordinates = [1.0, 0.0]
initial_speeds = [0.0, 0.0]
x0 = concatenate((initial_coordinates, initial_speeds), axis=1)

args = {'constants': constants.values()}


# Simulate
# ========
sys = System(KM, constants=constants,
initial_conditions=initial_conditions)

frames_per_sec = 60
final_time = 5.0

t = linspace(0.0, final_time, final_time * frames_per_sec)
x = odeint(xdot_function, x0, t, args=(args,))

x = sys.integrate(times)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I had been implying for re-integrating Scene visualization stuff.
So that we supply times and sys to the Scene class, and is easier to handle than the existing structure.
I am ready to work on integrating this code with Scene class(after tomorrow).

But also there will be some backwards incompatible changes(in Scene class as well), when this is integrated into Scene.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is why we are creating this, so that you can have something easier to work with in your viz code. It won't be ready tomorrow, but please help contribute to this so we can get it done more quickly.


3 changes: 1 addition & 2 deletions examples/double_pendulum/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
# Create the visualization
# ========================

scene.generate_visualization_json(coordinates + speeds, constants.keys(), x,
constants.values())
scene.generate_visualization_json(sys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better, if we initialize Scene with a System instance, rather than passing it to a method.

Ideally it should be on the lines:

s = Scene(sys, ...)
s.generate_visualization_json(over_time_data = time_data)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. You're right. Also, I think the times should be modifiable from within the visualizer so that the user is not even passing it around in python unless they want to override some default.


scene.display()
9 changes: 6 additions & 3 deletions pydy/codegen/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sympy.physics.mechanics as me


def generate_mass_spring_damper_equations_of_motion(external_force=True):
def generate_mass_spring_damper_equations_of_motion(external_force=True, kane=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just have these models output a System in the future.

"""Returns the symbolic equations of motion and associated variables for a
simple one degree of freedom mass, spring, damper system with gravity
and an optional external specified force.
Expand Down Expand Up @@ -90,8 +90,11 @@ def generate_mass_spring_damper_equations_of_motion(external_force=True):
else:
specified = None

return (mass_matrix, forcing_vector, constants, coordinates, speeds,
specified)
if kane:
return kane
else:
return (mass_matrix, forcing_vector, constants, coordinates, speeds,
specified)


def generate_n_link_pendulum_on_cart_equations_of_motion(n, cart_force=True,
Expand Down
105 changes: 105 additions & 0 deletions pydy/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

from .codegen.code import generate_ode_function
from scipy.integrate import odeint

class System(object):
"""Manages the simulation (integration) of a system whose equations are
given by KanesMethod or LagrangesMethod.

All attributes can be set directly. With the exception of `method`, the
attributes can also be set via keyword arguments to the constructor.

Attributes
----------
method : sympy.physics.mechanics.KanesMethod or
sympy.physics.mechanics.LagrangesMethod
The method used to generate the equations of motion.
constants : dict, optional (default: all 1.0)
Numerical values for the constants in the problem. Keys are the symbols
for the constants, and values are floats. Constants that are not
specified in this dict are given a default value of 1.0.
specified_symbols : iterable of symbols, optional
The symbols for the quantities whose numerical values you want to
specify. The order of these must match the order of the
`specified_values`. If not provided, we get all the specified symbols
from the `method`, and set their numerical value to 0. You can also
provide a subset of symbols for this attribute, in which case the
remaining symbols still have their numerical value set to 0.
specified_values : iterable of floats or functions, optional
The numerical values of the `specified_symbols`, or functions (which
take the same arguments as f, above) that can generate the numerical
values. If not provided, we get all the specified symbols
from the `method`, and set their numerical value to 0. You can also
provide a subset of symbols for this attribute, in which case the
remaining symbols still have their numerical value set to 0.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between the constants dictionary and specified_symbols/specified_values? If I were to do: dict(zip(specified_symbols, specified_values)) would this be the same as constants?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Reading further on in the code I can see these are dynamicsymbols, not symbols. Perhaps a better name for these parameters? fixed_dynsyms? I think a dictionary for specifying these would be cleaner as well, as they are relational (can't have one iterable longer than the other). If a user has iterables, could just run dict(zip(*)) to make the dict, and then pass it to the method themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jcrist. This is something that @moorepants and I discussed for a bit but we are still not completely satisfied. We agree with your point about how they are relational. However, Jason was saying that the specified_values could be a function that returns more than one number, to define multiple specified_symbols. That is, you may have symbols [a, b, c] and values lambda x, t: np.zeros(3) to define all 3 of those symbols. If we were to use a dict, we'd have to give each symbol its value separately.

Can you think of a good solution that (1) allows simulatenously defining a numerical value for more than one specified symbol at a time, and (2) enforces the relational (key, value) aspect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yeah good point, the documentation for distinguishing symbols and specified's could be better. specifieds is also kinda weird b/c it's an adjective, not a noun.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like something more complicated that should be done outside the class. Suppose I have an iterable of symbols: [a, b, c, d, e, f], and an iterable of functions [f1, f2]. f1 generates the value for a, b, and f2 generates for c - f. What if the code for f1 changed to now return 3 things? Now you have an off by one error all symbols after b. I'm not sure how you can resolve this in a robust way with multiple returns from a single function. Unless you plan on dynamically calling these functions in a systematic manner during integration/simulation, I don't think this use is necessary. The user can still just as easily call the functions themselves, and generate the dictionary to pass to System.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may work. I like it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I want to go that route but it may cause issues when passing self.specifieds.keys() to pydy.codegen.code.generate_ode_function. Does anyone have insight as to what'll happen here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moorepants I don't know enough about how generate_ode_function parses the list of functions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can update generate_ode_function too with this I guess. I guess generate_ode_function could take a system as an argument now.

Right now the rhs function requires the specified to be in a certain form. This shows what it needs to be: https://github.com/pydy/pydy/blob/master/pydy/codegen/code.py#L441

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah shucks. What I have now does not conform. I'll commit it and maybe we can work on making it conform.

code_gen_backend : str, optional (default: 'lambdify')
The backend used to generate the ode function.
See the documentation of pydy.codegen.code.generate_ode_function`.
ode_solver : function, optional (default: scipy.integrate.odeint)
A function that performs forward integration. It must have the same
signature as odeint, which is::

x_history = ode_solver(f, x0, t, args=(args,))

where f is a function f(x, t), x0 are the initial conditions, x_history
is the history, and args is a keyword argument takes arguments that are
then passed to f. TODO
initial_conditions : dict, optional (default: all zero)
Initial conditions for all coordinates and speeds. Keys are the symbols
for the coordinates and speeds, and values are floats. Coordinates or
speeds that are not specified in this dict are given a default value of
zero.

"""
def __init__(self, method, **kwargs):
self.method = method
for k, v in kwargs:
setattr(self, k, v)
if self.constants != None:
self.constants = self._find_constants()
if self.specified_symbols != None and self.specified_values != None:
self._check_specified()
# TODO
pass
if self.code_gen_backend != None:
self.code_gen_backend = 'lambdify'
if ode_solver != None:
self.ode_solver = odeint

def _find_constants(self):
othersymbols = method._find_othersymbols()
return zip(othersymbols, len(othersymbols) * [1.0])

def _find_specified_symbols(self):
specified_symbols = method._find_dynamicsymbols()
return zip

def generate_ode_function(self):
"""Calls `pydy. TODO

"""
self.rhs = generate_ode_function(
self.method.mass_matrix_full, self.method.forcing_full,
self.constants.keys(),
self.method._q, self.method._u)



#for k, v in kwargs:
# setattr(self, k, v)
#sys.generate_ode_function(backend=...)
#sys.code_gen_backend =
#sys.ode_solver = odeint
#sys.specified_symbols =
#sys.specified_values =
#sys.initial_conditions =
#sys.constants =
#
#class Sys:
# self.initial_conditions = zero(
# def integrate(self, times):
# # make rhs
# sys.ode_solver(self.rhs, self.initial_conditions, times,
# args=(self.con)
#

Empty file added pydy/tests/__init__.py
Empty file.
116 changes: 116 additions & 0 deletions pydy/tests/test_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@

import numpy as np
from numpy import testing
from sympy import symbols
from sympy.physics.mechanics import dynamicsymbols
from scipy.integrate import odeint

from ..system import System
from ..codegen.tests.models import \
generate_mass_spring_damper_equations_of_motion as mass_spring_damper


class TestSystem():

def setup(self):

self.kane = mass_spring_damper(kane=True)
self.specified_symbol = dynamicsymbols('F')
self.constant_map = dict(zip(symbols('m, k, c, g'),
[2.0, 1.5, 0.5, 9.8]))
self.sys = System(self.kane,
code_gen_backend='lambdify',
ode_solver=odeint,
specified_symbols=(self.specified_symbol,),
specified_values=np.ones(1),
initial_conditions=np.zeros(2),
constants=self.constant_map)

def test_init(self):

sys = System(self.kane)

assert sys.method is self.kane
assert sys.code_gen_backend == 'lambdify'
assert sys.specified_symbols is dynamicsymbols('F')
testing.assert_allclose(sys.specified_values, np.zeros(1))
testing.assert_allclose(sys.initial_conditions, np.zeros(2))
assert sys.constants == dict(zip(symbols('m, k, c, g'),
[1.0, 1.0, 1.0, 1.0]))

sys = System(self.kane,
code_gen_backend='lambdify',
ode_solver=odeint,
specified_symbols=(self.specified_symbol,),
specified_values=np.ones(1),
initial_conditions=np.zeros(2),
constants=self.constant_map)

assert sys.method is self.kane
assert sys.code_gen_backend == 'lambdify'
assert sys.specified_symbols is dynamicsymbols('F')
testing.assert_allclose(sys.specified_values, np.ones(1))
testing.assert_allclose(sys.initial_conditions, np.zeros(2))
assert sys.constants == self.constant_map

sys.backend == 'cython'

# You must set both specified symbols and values.
assert_raises(ValueError,
System(self.kane,
specified_symbols=(self.specified_symbol)))

assert_raises(ValueError,
System(self.kane,
specified_values=(self.specified_symbol)))

# Check to make sure the specified function returns the correct
# number of values.
assert_raises(ValueError,
System(self.kane,
specified_symbols=(self.specified_symbol),
specified_values=lambda x, t: np.ones(10))


# The specified symbol must exist in the equations of motion and not
# be a state.
assert_raises(ValueError,
System(self.kane,
specified_symbols=(dynamicsymbols('G')),
specified_values=np.ones(1)))

# The same number of specified symbols and values are needed.
assert_raises(ValueError,
System(self.kane,
specified_symbols=(self.specified_symbol),
specified_values=np.ones(2))


def test_generate_ode_function(self):

rhs = self.sys.generate_ode_function()

assert rhs is self.rhs

args = {'constants': self.constant_map.values,
'specified': np.zeros(1)}

actual = rhs(np.ones(2), 0.0, args=(args,))

testing.assert_allclose(actual,
np.array([ , ]))

def find_constants(self):

constant_dict = self.sys._find_constants()

assert constant_dict = self.constant_map

def find_specified_symbols(self):

specified_dict = self.sys._find_specified_symbols()

assert specified_dict = {self.dynamic_symbol: 0.0}

def test_integrate(self):