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 1 commit
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
Prev Previous commit
Next Next commit
Restore the fast way of parsing specifieds.
  • Loading branch information
chrisdembia committed Aug 7, 2014
commit fd981e761e4b8c045231c0752c24021280efa100
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ and specified quantities. Here, we specify sinusoidal forcing::

sys = System(kane,
constants={mass: 1.0, stiffness: 1.0, damping: 0.2, gravity: 9.8},
specified={force: lambda x, t: sin(t)},
specified={'symbols': [force], 'values': lambda x, t: sin(t)},
Copy link
Member

Choose a reason for hiding this comment

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

How do you make sure that your list of specifieds is the same order as those found from the kane object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't. If the user is using the symbols/values format, they are in control of the order of symbols. See https://github.com/pydy/pydy/pull/81/files#diff-105304ef1b8479ef4d5d9e41b786cc34R383

initial_conditions=array([0.1, -1.0]))

Now generate the function needed for numerical evaluation of the ODEs. The
Expand Down
68 changes: 48 additions & 20 deletions pydy/codegen/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,20 +428,31 @@ def evaluate_ode(x, t, args):
A dictionary that maps the constant symbols to floats. The
dictionary must contain these keys:
{constant_list}
specified : dictionary
A dictionary that maps the specified functions of time to
floats, ndarrays, or functions that produce ndarrays. The
keys can be a single specified symbolic function of time or
a tuple of symbols. The total number of symbols must be
equal to {num_specified}. If the value is a function it must
specified : dictionary; ndarray, shape({num_specified},); function
There are three options for this dictionary. (1) is more flexible
but (2) and (3) are much more efficient.
(1) A dictionary that maps the specified functions of time to
floats, ndarrays, or functions that produce ndarrays.
The keys can be a single specified symbolic function of
time or a tuple of symbols. The total number of symbols must
be equal to {num_specified}. If the value is a function it must
be of the form f(x, t), where x is the current state vector
ndarray and t is the current time float and it must return
an ndarray of the correct shape. For example:
ndarray and t is the current time float and it must return an
ndarray of the correct shape. For example:
args['specified'] = {{a: 1.0,
(d, b) : np.array([1.0, 2.0]),
(e, f) : lambda x, t: np.array(x[0], x[1]),
c: lambda x, t: np.array(x[2])}}
The dictionary must contian these functions of time:

(2) ndarray: The specified values in the same order as the
symbols given to `generate_ode_function()`, and must be the
correct shape.
(3) function: It must be of the form f(x, t), where x is the
current state vector and t is the current time and it must
return an ndarray of the correct shape (an ndarray like for
(2)).

The dictionary must contain these functions of time:
{specified_list}

Returns
Expand All @@ -465,19 +476,36 @@ def evaluate_ode(x, t, args):

if specified is not None:

specified_values = np.zeros(len(specified))
if isinstance(args['specified'], dict):

specified_values = np.zeros(len(specified))

for k, v in args['specified'].items():
# TODO : Not sure if this is the best check here.
if isinstance(type(k), UndefinedFunction):
k = (k,)
idx = [specified.index(symmy) for symmy in k]
try:
specified_values[idx] = v(x, t)
except TypeError: # not callable
# If not callable, then it should be a float, ndarray,
# or indexable.
specified_values[idx] = v

else:
# More efficient.

try:
specified_values = args['specified'](x, t)
except TypeError: # not callable.
# If not callable, then it should be a float or ndarray.
specified_values = args['specified']

for k, v in args['specified'].items():
# TODO : Not sure if this is the best check here.
if isinstance(type(k), UndefinedFunction):
k = (k,)
idx = [specified.index(symmy) for symmy in k]
# If the value is just a float, then convert to a 1D array.
try:
specified_values[idx] = v(x, t)
except TypeError: # not callable
# If not callable, then it should be a float, ndarray,
# or indexable.
specified_values[idx] = v
len(specified_values)
except TypeError:
specified_values = np.asarray([specified_values])

segmented.append(specified_values)

Expand Down
9 changes: 9 additions & 0 deletions pydy/codegen/tests/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ def test_rhs_args(self):
testing.assert_allclose(xd_01, xd_02)
testing.assert_allclose(xd_01, xd_03)

# Test old and efficient RHS args.
args['specified'] = np.array([1.0, 2.0, 3.0, 4.0])
xd_04 = rhs(x, 0.0, args)
testing.assert_allclose(xd_01, xd_04)

args['specified'] = lambda x, t: np.array([1.0, 2.0, 3.0, 4.0])
xd_05 = rhs(x, 0.0, args)
testing.assert_allclose(xd_01, xd_05)

class TestCode():

def test_generate_ode_function(self):
Expand Down
112 changes: 83 additions & 29 deletions pydy/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,24 +174,46 @@ def _constants_padded_with_defaults(self):
def specifieds(self):
"""A dict that provides numerical values for the specified quantities
in the problem (all dynamicsymbols that are not defined by the
equations of motion). Keys are the symbols for the specified
quantities, or a tuple of symbols, and values are the floats, arrays of
floats, or functions that generate the values. If a dictionary value is
a function, it must have the same signature as ``f(x, t)``, the ode
right-hand-side function (see the documentation for the ``ode_solver``
attribute). You needn't provide values for all specified symbols. Those
for which you do not give a value will default to 0.0.
equations of motion). There are two possible formats. (1) is more
flexible, but (2) is more efficient (by a factor of 3).

(1) Keys are the symbols for the specified quantities, or a tuple of
symbols, and values are the floats, arrays of floats, or functions that
generate the values. If a dictionary value is a function, it must have
the same signature as ``f(x, t)``, the ode right-hand-side function
(see the documentation for the ``ode_solver`` attribute). You needn't
provide values for all specified symbols. Those for which you do not
give a value will default to 0.0.

(2) There are two keys: 'symbols' and 'values'. The value for 'symbols'
is an iterable of *all* the specified quantities in the order that you
have provided them in 'values'. Values is an ndarray, whose length is
`len(sys.specifieds_symbols)`, or a function of x and t that returns an
ndarray (also of length `len(sys.specifieds_symbols)`). NOTE: You must
provide values for all specified symbols. In this case, we do *not*
provide default values.

NOTE: If you switch formats with the same instance of System, you
*must* call `generate_ode_function()` before calling `integrate()`
again.

Examples
--------
Keys can be individual symbols, or a tuple of symbols. Length of a
value must match the length of the corresponding key. Values can be
functions that return iterables::
Here are examples for (1). Keys can be individual symbols, or a tuple
of symbols. Length of a value must match the length of the
corresponding key. Values can be functions that return iterables::

sys = System(km)
sys.specifieds = {(a, b, c): np.ones(3), d: lambda x, t: -3 * x[0]}
sys.specifieds = {(a, b, c): lambda x, t: np.ones(3)}

Here are examples for (2):

sys.specifieds = {'symbols': (a, b, c, d),
'values': np.ones(4)}
sys.specifieds = {'symbols': (a, b, c, d),
'values': lambda x, t: np.ones(4)}

"""
return self._specifieds

Expand All @@ -216,29 +238,55 @@ def _assert_symbol_appears_multiple_times(self, symbol, symbols_so_far):
raise ValueError("Symbol {} appears more than once.".format(
symbol))

def _specifieds_are_in_format_2(self, specifieds):
keys = specifieds.keys()
if ('symbols' in keys and 'values' in keys):
return True
else:
return False

def _check_specifieds(self, specifieds):
symbols = self.specifieds_symbols

symbols_so_far = list()

for k, v in specifieds.items():
if self._specifieds_are_in_format_2(specifieds):

# The symbols must be specifieds.
if isinstance(k, tuple):
for ki in k:
self._assert_is_specified_symbol(ki, symbols)
else:
self._assert_is_specified_symbol(k, symbols)
for sym in specifieds['symbols']:
self._assert_is_specified_symbol(sym, symbols)

# Each specified symbol can appear only once.
if isinstance(k, tuple):
for ki in k:
self._assert_symbol_appears_multiple_times(ki,
symbols_so_far)
symbols_so_far.append(ki)
else:
self._assert_symbol_appears_multiple_times(k, symbols_so_far)
symbols_so_far.append(k)
for sym in specifieds['symbols']:
self._assert_symbol_appears_multiple_times(sym, symbols_so_far)
symbols_so_far.append(sym)

# Must have provided all specifieds.
for sym in self.specifieds_symbols:
if sym not in specifieds['symbols']:
raise ValueError(
"Specified symbol {} is not provided.".format(sym))

else:

for k, v in specifieds.items():

# The symbols must be specifieds.
if isinstance(k, tuple):
for ki in k:
self._assert_is_specified_symbol(ki, symbols)
else:
self._assert_is_specified_symbol(k, symbols)

# Each specified symbol can appear only once.
if isinstance(k, tuple):
for ki in k:
self._assert_symbol_appears_multiple_times(ki,
symbols_so_far)
symbols_so_far.append(ki)
else:
self._assert_symbol_appears_multiple_times(k, symbols_so_far)
symbols_so_far.append(k)

def _symbol_is_in_specifieds_dict(self, symbol, specifieds_dict):
for k in specifieds_dict.keys():
Expand Down Expand Up @@ -331,14 +379,18 @@ def generate_ode_function(self, generator='lambdify', **kwargs):
A function which evaluates the derivaties of the states.

"""
if self._specifieds_are_in_format_2(self.specifieds):
specified_value = self.specifieds['symbols']
else:
specified_value = self.specifieds_symbols
self._evaluate_ode_function = generate_ode_function(
# args:
self.eom_method.mass_matrix_full,
self.eom_method.forcing_full,
self.constants_symbols,
self.coordinates, self.speeds,
# kwargs:
specified=self.specifieds_symbols,
specified=specified_value,
generator=generator,
**kwargs
)
Expand Down Expand Up @@ -378,23 +430,25 @@ def integrate(self, times):
initial_conditions_in_proper_order = \
[init_conds_dict[k] for k in self.states]

if self._specifieds_are_in_format_2(self.specifieds):
specified_value = self.specifieds['values']
else:
specified_value = self._specifieds_padded_with_defaults()

return self.ode_solver(
self.evaluate_ode_function,
initial_conditions_in_proper_order,
times,
args=({
'constants': self._constants_padded_with_defaults(),
'specified': self._specifieds_padded_with_defaults(),
'specified': specified_value,
},)
)

def _Kane_inlist_insyms(self):
"""TODO temporary."""
uaux = self.eom_method._uaux
uauxdot = [diff(i, t) for i in uaux]
Copy link
Member

Choose a reason for hiding this comment

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

Seems like this should fail as diff isn't imported anywhere.

# dictionary of auxiliary speeds & derivatives which are equal to zero
subdict = dict(
list(zip(uaux + uauxdot, [0] * (len(uaux) + len(uauxdot)))))

# Checking for dynamic symbols outside the dynamic differential
# equations; throws error if there is.
Expand Down
45 changes: 45 additions & 0 deletions pydy/tests/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ def test_init(self):
testing.assert_allclose(sys.constants.values(),
self.constant_map.values())

# Use old specifieds.
# -------------------
sys = System(self.kane,
ode_solver=odeint,
specifieds={'symbols': [self.specified_symbol],
'values': np.ones(1)},
initial_conditions=ic,
constants=self.constant_map)

def test_coordinates(self):
assert self.sys.coordinates == self.kane._q

Expand Down Expand Up @@ -183,6 +192,42 @@ def test_specifieds(self):
sys.specifieds.pop(spec_syms[0])
sys.integrate(times)

# Test old way of providing specifieds.
# -------------------------------------
sys = System(self.kane_nlink)
spec_syms = sys.specifieds_symbols
# Get numbers using the new way.
sys.specifieds = dict(zip(spec_syms, [1.0, 2.0, 3.0, 4.0]))
x_01 = sys.integrate(times)

# Now use the old way.
sys.specifieds = {'symbols': spec_syms,
'values': [1.0, 2.0, 3.0, 4.0]}
x_02 = sys.integrate(times)
testing.assert_allclose(x_01, x_02)

# Error checks for the new way.
# -----------------------------
with testing.assert_raises(ValueError):
sys.specifieds = {'symbols': [symbols('m1')], 'values': [1.0]}
with testing.assert_raises(ValueError):
sys.specifieds = {'symbols': [symbols('T2, T2')], 'values': [1, 2]}
with testing.assert_raises(ValueError):
sys.specifieds = {'symbols': [dynamicsymbols('T2')], 'values':
[1.0]}

# Reordering causes issues!
# -------------------------
sys.specifieds = {'symbols':
[spec_syms[1], spec_syms[0], spec_syms[2], spec_syms[3]],
'values': [2.0, 1.0, 3.0, 4.0]}
x_03 = sys.integrate(times)
# I tested: x_01 is not allclose to x_03.

sys.generate_ode_function()
x_04 = sys.integrate(times)
testing.assert_allclose(x_01, x_04)


def test_ode_solver(self):

Expand Down