Source code for kf.waves
"""Generators to simulate wave system dynamics. The viewer classes can be found in QtWaves."""
################################################################
# Written in 2003-2019 by Garth Zeglin <garthz@cmu.edu>
# To the extent possible under law, the author has dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
# You should have received a copy of the CC0 Public Domain Dedication along with this software.
# If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
################################################################
import math, logging
import numpy as np
# set up logger for module
log = logging.getLogger('waves')
# filter out most logging; the default is NOTSET which passes along everything
# log.setLevel(logging.WARNING)
################################################################
[docs]class Waves1D(object):
"""Simulation of a set of one-dimensional wave systems.
:param channels: number of wave systems to simulate
:param samples: number of sample points in each system
"""
def __init__(self, channels=5, samples=20):
# scalar constants
self.W = channels # Number of waves.
self.N = samples # Number of sample points for each wave.
self.k = 200.0*np.ones((self.W)) # Spring rate for node coupling for each wave.
self.b = 0.5*np.ones((self.W)) # Node damping factor for each wave.
self.k = self.k / (1 + np.arange(self.W))
self.period = 8.0*np.ones((self.W)) # Period of the default excitation for each wave.
self.duty = 0.25*np.ones((self.W)) # Duty cycle of the default excitation for each wave.
self.magnitude = 2.0*np.ones((self.W)) # Magnitude of the default excitation for each wave.
self.qE = np.zeros((self.W)) # Position of the driven node for each wave.
self.inputE = np.zeros((self.W), dtype=int) # Index of the driven node for each wave.
# q is a (W,N) matrix of generalized wave coordinates (positions)
# qd is a (W,N) matrix of generalized wave derivatives (velocities)
self.q = np.zeros((self.W, self.N))
self.qd = np.zeros((self.W, self.N))
# Threshold flags are used to detect events.
self.rising = np.zeros((self.W, self.N), dtype=bool)
self.rising_edge_listener = None
# elapsed integrator time
self.t = 0.0
return
[docs] def set_all_damping(self, b):
self.b = b*np.ones((self.W))
[docs] def set_all_period(self, period):
self.period = period*np.ones((self.W))
[docs] def get_positions(self):
"""Return a (W, N) matrix of node position values where W is the number of waves and N is the number of samples."""
return self.q.copy()
[docs] def update_for_interval(self, interval):
"""Run the simulator for the given interval, which may include one or more integration steps."""
while interval > 0.0:
dt = min(interval, 0.001)
interval -= dt
self._step(dt)
[docs] def connect_rising_edge_listener(self, callback):
self.rising_edge_listener = callback
# ================================================================
def _step(self, dt):
# calculate the excitation
if self.t < 0:
self.qE = np.zeros((self.W))
else:
# phase ramps from [0,1) in a sawtooth
phase = np.mod(self.t, self.period) / self.period
# create a pulse train with the specified duty cycle
self.qE = (phase < self.duty) * self.magnitude
# create an accumulator for forces on all nodes
force = np.ndarray((self.W, self.N))
# calculate coupling spring forces on all interior nodes
# k (W, 1) * dQ (W, N-2)
force[:,1:-1] = self.k.reshape((self.W, 1)) * (0.5*(self.q[:, 2:] + self.q[:, 0:-2]) - self.q[:,1:-1])
# Calculate coupling spring forces on boundary nodes. This assumes an open boundary condition at each end.
force[:, 0] = self.k * (self.q[:, 1] - self.q[:, 0])
force[:,-1] = self.k * (self.q[:, -2] - self.q[:,-1])
# calculate damping on all nodes
force -= self.b.reshape((self.W, 1)) * self.qd
# apply the drive condition as an extra node coupling spring and damper
force[:, self.inputE] += 4*self.k * (self.qE - self.q[:, self.inputE]) - 20*self.b * (self.qd[:,self.inputE])
# detect threshold events
rising = self.q > (0.5 * self.qE.reshape((self.W, 1)))
new_rising = np.logical_and(rising, np.logical_not(self.rising))
self.rising = rising
if self.rising_edge_listener is not None:
# iterate over the set of indicies of true elements
for wave, particle in zip(*np.nonzero(new_rising)):
self.rising_edge_listener(wave, particle)
# calculate the node dynamics with forward Euler integration
self.q += self.qd * dt
self.qd += force * dt
self.t += dt
return
################################################################
if __name__ == "__main__":
obj = Waves1D()
obj.update_for_interval(0.040)