import numpy as np import collections import sys, os import pyqtgraph as pg from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.parametertree import Parameter, ParameterTree from pyqtgraph.parametertree import types as pTypes import pyqtgraph.configfile from pyqtgraph.python2_3 import xrange from time import perf_counter class RelativityGUI(QtGui.QWidget): def __init__(self): QtGui.QWidget.__init__(self) self.animations = [] self.animTimer = QtCore.QTimer() self.animTimer.timeout.connect(self.stepAnimation) self.animTime = 0 self.animDt = .016 self.lastAnimTime = 0 self.setupGUI() self.objectGroup = ObjectGroupParam() self.params = Parameter.create(name='params', type='group', children=[ dict(name='Load Preset..', type='list', values=[]), #dict(name='Unit System', type='list', values=['', 'MKS']), dict(name='Duration', type='float', value=10.0, step=0.1, limits=[0.1, None]), dict(name='Reference Frame', type='list', values=[]), dict(name='Animate', type='bool', value=True), dict(name='Animation Speed', type='float', value=1.0, dec=True, step=0.1, limits=[0.0001, None]), dict(name='Recalculate Worldlines', type='action'), dict(name='Save', type='action'), dict(name='Load', type='action'), self.objectGroup, ]) self.tree.setParameters(self.params, showTop=False) self.params.param('Recalculate Worldlines').sigActivated.connect(self.recalculate) self.params.param('Save').sigActivated.connect(self.save) self.params.param('Load').sigActivated.connect(self.load) self.params.param('Load Preset..').sigValueChanged.connect(self.loadPreset) self.params.sigTreeStateChanged.connect(self.treeChanged) ## read list of preset configs presetDir = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'presets') if os.path.exists(presetDir): presets = [os.path.splitext(p)[0] for p in os.listdir(presetDir)] self.params.param('Load Preset..').setLimits(['']+presets) def setupGUI(self): self.layout = QtGui.QVBoxLayout() self.layout.setContentsMargins(0,0,0,0) self.setLayout(self.layout) self.splitter = QtGui.QSplitter() self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal) self.layout.addWidget(self.splitter) self.tree = ParameterTree(showHeader=False) self.splitter.addWidget(self.tree) self.splitter2 = QtGui.QSplitter() self.splitter2.setOrientation(QtCore.Qt.Orientation.Vertical) self.splitter.addWidget(self.splitter2) self.worldlinePlots = pg.GraphicsLayoutWidget() self.splitter2.addWidget(self.worldlinePlots) self.animationPlots = pg.GraphicsLayoutWidget() self.splitter2.addWidget(self.animationPlots) self.splitter2.setSizes([int(self.height()*0.8), int(self.height()*0.2)]) self.inertWorldlinePlot = self.worldlinePlots.addPlot() self.refWorldlinePlot = self.worldlinePlots.addPlot() self.inertAnimationPlot = self.animationPlots.addPlot() self.inertAnimationPlot.setAspectLocked(1) self.refAnimationPlot = self.animationPlots.addPlot() self.refAnimationPlot.setAspectLocked(1) self.inertAnimationPlot.setXLink(self.inertWorldlinePlot) self.refAnimationPlot.setXLink(self.refWorldlinePlot) def recalculate(self): ## build 2 sets of clocks clocks1 = collections.OrderedDict() clocks2 = collections.OrderedDict() for cl in self.params.param('Objects'): clocks1.update(cl.buildClocks()) clocks2.update(cl.buildClocks()) ## Inertial simulation dt = self.animDt * self.params['Animation Speed'] sim1 = Simulation(clocks1, ref=None, duration=self.params['Duration'], dt=dt) sim1.run() sim1.plot(self.inertWorldlinePlot) self.inertWorldlinePlot.autoRange(padding=0.1) ## reference simulation ref = self.params['Reference Frame'] dur = clocks1[ref].refData['pt'][-1] ## decide how long to run the reference simulation sim2 = Simulation(clocks2, ref=clocks2[ref], duration=dur, dt=dt) sim2.run() sim2.plot(self.refWorldlinePlot) self.refWorldlinePlot.autoRange(padding=0.1) ## create animations self.refAnimationPlot.clear() self.inertAnimationPlot.clear() self.animTime = 0 self.animations = [Animation(sim1), Animation(sim2)] self.inertAnimationPlot.addItem(self.animations[0]) self.refAnimationPlot.addItem(self.animations[1]) ## create lines representing all that is visible to a particular reference #self.inertSpaceline = Spaceline(sim1, ref) #self.refSpaceline = Spaceline(sim2) self.inertWorldlinePlot.addItem(self.animations[0].items[ref].spaceline()) self.refWorldlinePlot.addItem(self.animations[1].items[ref].spaceline()) def setAnimation(self, a): if a: self.lastAnimTime = perf_counter() self.animTimer.start(int(self.animDt*1000)) else: self.animTimer.stop() def stepAnimation(self): now = perf_counter() dt = (now-self.lastAnimTime) * self.params['Animation Speed'] self.lastAnimTime = now self.animTime += dt if self.animTime > self.params['Duration']: self.animTime = 0 for a in self.animations: a.restart() for a in self.animations: a.stepTo(self.animTime) def treeChanged(self, *args): clocks = [] for c in self.params.param('Objects'): clocks.extend(c.clockNames()) #for param, change, data in args[1]: #if change == 'childAdded': self.params.param('Reference Frame').setLimits(clocks) self.setAnimation(self.params['Animate']) def save(self): filename = pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)") if isinstance(filename, tuple): filename = filename[0] # Qt4/5 API difference if filename == '': return state = self.params.saveState() pg.configfile.writeConfigFile(state, str(filename)) def load(self): filename = pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)") if isinstance(filename, tuple): filename = filename[0] # Qt4/5 API difference if filename == '': return state = pg.configfile.readConfigFile(str(filename)) self.loadState(state) def loadPreset(self, param, preset): if preset == '': return path = os.path.abspath(os.path.dirname(__file__)) fn = os.path.join(path, 'presets', preset+".cfg") state = pg.configfile.readConfigFile(fn) self.loadState(state) def loadState(self, state): if 'Load Preset..' in state['children']: del state['children']['Load Preset..']['limits'] del state['children']['Load Preset..']['value'] self.params.param('Objects').clearChildren() self.params.restoreState(state, removeChildren=False) self.recalculate() class ObjectGroupParam(pTypes.GroupParameter): def __init__(self): pTypes.GroupParameter.__init__(self, name="Objects", addText="Add New..", addList=['Clock', 'Grid']) def addNew(self, typ): if typ == 'Clock': self.addChild(ClockParam()) elif typ == 'Grid': self.addChild(GridParam()) class ClockParam(pTypes.GroupParameter): def __init__(self, **kwds): defs = dict(name="Clock", autoIncrementName=True, renamable=True, removable=True, children=[ dict(name='Initial Position', type='float', value=0.0, step=0.1), #dict(name='V0', type='float', value=0.0, step=0.1), AccelerationGroup(), dict(name='Rest Mass', type='float', value=1.0, step=0.1, limits=[1e-9, None]), dict(name='Color', type='color', value=(100,100,150)), dict(name='Size', type='float', value=0.5), dict(name='Vertical Position', type='float', value=0.0, step=0.1), ]) #defs.update(kwds) pTypes.GroupParameter.__init__(self, **defs) self.restoreState(kwds, removeChildren=False) def buildClocks(self): x0 = self['Initial Position'] y0 = self['Vertical Position'] color = self['Color'] m = self['Rest Mass'] size = self['Size'] prog = self.param('Acceleration').generate() c = Clock(x0=x0, m0=m, y0=y0, color=color, prog=prog, size=size) return {self.name(): c} def clockNames(self): return [self.name()] pTypes.registerParameterType('Clock', ClockParam) class GridParam(pTypes.GroupParameter): def __init__(self, **kwds): defs = dict(name="Grid", autoIncrementName=True, renamable=True, removable=True, children=[ dict(name='Number of Clocks', type='int', value=5, limits=[1, None]), dict(name='Spacing', type='float', value=1.0, step=0.1), ClockParam(name='ClockTemplate'), ]) #defs.update(kwds) pTypes.GroupParameter.__init__(self, **defs) self.restoreState(kwds, removeChildren=False) def buildClocks(self): clocks = {} template = self.param('ClockTemplate') spacing = self['Spacing'] for i in range(self['Number of Clocks']): c = list(template.buildClocks().values())[0] c.x0 += i * spacing clocks[self.name() + '%02d' % i] = c return clocks def clockNames(self): return [self.name() + '%02d' % i for i in range(self['Number of Clocks'])] pTypes.registerParameterType('Grid', GridParam) class AccelerationGroup(pTypes.GroupParameter): def __init__(self, **kwds): defs = dict(name="Acceleration", addText="Add Command..") pTypes.GroupParameter.__init__(self, **defs) self.restoreState(kwds, removeChildren=False) def addNew(self): nextTime = 0.0 if self.hasChildren(): nextTime = self.children()[-1]['Proper Time'] + 1 self.addChild(Parameter.create(name='Command', autoIncrementName=True, type=None, renamable=True, removable=True, children=[ dict(name='Proper Time', type='float', value=nextTime), dict(name='Acceleration', type='float', value=0.0, step=0.1), ])) def generate(self): prog = [] for cmd in self: prog.append((cmd['Proper Time'], cmd['Acceleration'])) return prog pTypes.registerParameterType('AccelerationGroup', AccelerationGroup) class Clock(object): nClocks = 0 def __init__(self, x0=0.0, y0=0.0, m0=1.0, v0=0.0, t0=0.0, color=None, prog=None, size=0.5): Clock.nClocks += 1 self.pen = pg.mkPen(color) self.brush = pg.mkBrush(color) self.y0 = y0 self.x0 = x0 self.v0 = v0 self.m0 = m0 self.t0 = t0 self.prog = prog self.size = size def init(self, nPts): ## Keep records of object from inertial frame as well as reference frame self.inertData = np.empty(nPts, dtype=[('x', float), ('t', float), ('v', float), ('pt', float), ('m', float), ('f', float)]) self.refData = np.empty(nPts, dtype=[('x', float), ('t', float), ('v', float), ('pt', float), ('m', float), ('f', float)]) ## Inertial frame variables self.x = self.x0 self.v = self.v0 self.m = self.m0 self.t = 0.0 ## reference clock always starts at 0 self.pt = self.t0 ## proper time starts at t0 ## reference frame variables self.refx = None self.refv = None self.refm = None self.reft = None self.recordFrame(0) def recordFrame(self, i): f = self.force() self.inertData[i] = (self.x, self.t, self.v, self.pt, self.m, f) self.refData[i] = (self.refx, self.reft, self.refv, self.pt, self.refm, f) def force(self, t=None): if len(self.prog) == 0: return 0.0 if t is None: t = self.pt ret = 0.0 for t1,f in self.prog: if t >= t1: ret = f return ret def acceleration(self, t=None): return self.force(t) / self.m0 def accelLimits(self): ## return the proper time values which bound the current acceleration command if len(self.prog) == 0: return -np.inf, np.inf t = self.pt ind = -1 for i, v in enumerate(self.prog): t1,f = v if t >= t1: ind = i if ind == -1: return -np.inf, self.prog[0][0] elif ind == len(self.prog)-1: return self.prog[-1][0], np.inf else: return self.prog[ind][0], self.prog[ind+1][0] def getCurve(self, ref=True): if ref is False: data = self.inertData else: data = self.refData[1:] x = data['x'] y = data['t'] curve = pg.PlotCurveItem(x=x, y=y, pen=self.pen) #x = self.data['x'] - ref.data['x'] #y = self.data['t'] step = 1.0 #mod = self.data['pt'] % step #inds = np.argwhere(abs(mod[1:] - mod[:-1]) > step*0.9) inds = [0] pt = data['pt'] for i in range(1,len(pt)): diff = pt[i] - pt[inds[-1]] if abs(diff) >= step: inds.append(i) inds = np.array(inds) #t = self.data['t'][inds] #x = self.data['x'][inds] pts = [] for i in inds: x = data['x'][i] y = data['t'][i] if i+1 < len(data): dpt = data['pt'][i+1]-data['pt'][i] dt = data['t'][i+1]-data['t'][i] else: dpt = 1 if dpt > 0: c = pg.mkBrush((0,0,0)) else: c = pg.mkBrush((200,200,200)) pts.append({'pos': (x, y), 'brush': c}) points = pg.ScatterPlotItem(pts, pen=self.pen, size=7) return curve, points class Simulation: def __init__(self, clocks, ref, duration, dt): self.clocks = clocks self.ref = ref self.duration = duration self.dt = dt @staticmethod def hypTStep(dt, v0, x0, tau0, g): ## Hyperbolic step. ## If an object has proper acceleration g and starts at position x0 with speed v0 and proper time tau0 ## as seen from an inertial frame, then return the new v, x, tau after time dt has elapsed. if g == 0: return v0, x0 + v0*dt, tau0 + dt * (1. - v0**2)**0.5 v02 = v0**2 g2 = g**2 tinit = v0 / (g * (1 - v02)**0.5) B = (1 + (g2 * (dt+tinit)**2))**0.5 v1 = g * (dt+tinit) / B dtau = (np.arcsinh(g * (dt+tinit)) - np.arcsinh(g * tinit)) / g tau1 = tau0 + dtau x1 = x0 + (1.0 / g) * ( B - 1. / (1.-v02)**0.5 ) return v1, x1, tau1 @staticmethod def tStep(dt, v0, x0, tau0, g): ## Linear step. ## Probably not as accurate as hyperbolic step, but certainly much faster. gamma = (1. - v0**2)**-0.5 dtau = dt / gamma return v0 + dtau * g, x0 + v0*dt, tau0 + dtau @staticmethod def tauStep(dtau, v0, x0, t0, g): ## linear step in proper time of clock. ## If an object has proper acceleration g and starts at position x0 with speed v0 at time t0 ## as seen from an inertial frame, then return the new v, x, t after proper time dtau has elapsed. ## Compute how much t will change given a proper-time step of dtau gamma = (1. - v0**2)**-0.5 if g == 0: dt = dtau * gamma else: v0g = v0 * gamma dt = (np.sinh(dtau * g + np.arcsinh(v0g)) - v0g) / g #return v0 + dtau * g, x0 + v0*dt, t0 + dt v1, x1, t1 = Simulation.hypTStep(dt, v0, x0, t0, g) return v1, x1, t0+dt @staticmethod def hypIntersect(x0r, t0r, vr, x0, t0, v0, g): ## given a reference clock (seen from inertial frame) has rx, rt, and rv, ## and another clock starts at x0, t0, and v0, with acceleration g, ## compute the intersection time of the object clock's hyperbolic path with ## the reference plane. ## I'm sure we can simplify this... if g == 0: ## no acceleration, path is linear (and hyperbola is undefined) #(-t0r + t0 v0 vr - vr x0 + vr x0r)/(-1 + v0 vr) t = (-t0r + t0 *v0 *vr - vr *x0 + vr *x0r)/(-1 + v0 *vr) return t gamma = (1.0-v0**2)**-0.5 sel = (1 if g>0 else 0) + (1 if vr<0 else 0) sel = sel%2 if sel == 0: #(1/(g^2 (-1 + vr^2)))(-g^2 t0r + g gamma vr + g^2 t0 vr^2 - #g gamma v0 vr^2 - g^2 vr x0 + #g^2 vr x0r + \[Sqrt](g^2 vr^2 (1 + gamma^2 (v0 - vr)^2 - vr^2 + #2 g gamma (v0 - vr) (-t0 + t0r + vr (x0 - x0r)) + #g^2 (t0 - t0r + vr (-x0 + x0r))^2))) t = (1./(g**2 *(-1. + vr**2)))*(-g**2 *t0r + g *gamma *vr + g**2 *t0 *vr**2 - g *gamma *v0 *vr**2 - g**2 *vr *x0 + g**2 *vr *x0r + np.sqrt(g**2 *vr**2 *(1. + gamma**2 *(v0 - vr)**2 - vr**2 + 2 *g *gamma *(v0 - vr)* (-t0 + t0r + vr *(x0 - x0r)) + g**2 *(t0 - t0r + vr* (-x0 + x0r))**2))) else: #-(1/(g^2 (-1 + vr^2)))(g^2 t0r - g gamma vr - g^2 t0 vr^2 + #g gamma v0 vr^2 + g^2 vr x0 - #g^2 vr x0r + \[Sqrt](g^2 vr^2 (1 + gamma^2 (v0 - vr)^2 - vr^2 + #2 g gamma (v0 - vr) (-t0 + t0r + vr (x0 - x0r)) + #g^2 (t0 - t0r + vr (-x0 + x0r))^2))) t = -(1./(g**2 *(-1. + vr**2)))*(g**2 *t0r - g *gamma* vr - g**2 *t0 *vr**2 + g *gamma *v0 *vr**2 + g**2* vr* x0 - g**2 *vr *x0r + np.sqrt(g**2* vr**2 *(1. + gamma**2 *(v0 - vr)**2 - vr**2 + 2 *g *gamma *(v0 - vr) *(-t0 + t0r + vr *(x0 - x0r)) + g**2 *(t0 - t0r + vr *(-x0 + x0r))**2))) return t def run(self): nPts = int(self.duration/self.dt)+1 for cl in self.clocks.values(): cl.init(nPts) if self.ref is None: self.runInertial(nPts) else: self.runReference(nPts) def runInertial(self, nPts): clocks = self.clocks dt = self.dt tVals = np.linspace(0, dt*(nPts-1), nPts) for cl in self.clocks.values(): for i in xrange(1,nPts): nextT = tVals[i] while True: tau1, tau2 = cl.accelLimits() x = cl.x v = cl.v tau = cl.pt g = cl.acceleration() v1, x1, tau1 = self.hypTStep(dt, v, x, tau, g) if tau1 > tau2: dtau = tau2-tau cl.v, cl.x, cl.t = self.tauStep(dtau, v, x, cl.t, g) cl.pt = tau2 else: cl.v, cl.x, cl.pt = v1, x1, tau1 cl.t += dt if cl.t >= nextT: cl.refx = cl.x cl.refv = cl.v cl.reft = cl.t cl.recordFrame(i) break def runReference(self, nPts): clocks = self.clocks ref = self.ref dt = self.dt dur = self.duration ## make sure reference clock is not present in the list of clocks--this will be handled separately. clocks = clocks.copy() for k,v in clocks.items(): if v is ref: del clocks[k] break ref.refx = 0 ref.refv = 0 ref.refm = ref.m0 ## These are the set of proper times (in the reference frame) that will be simulated ptVals = np.linspace(ref.pt, ref.pt + dt*(nPts-1), nPts) for i in xrange(1,nPts): ## step reference clock ahead one time step in its proper time nextPt = ptVals[i] ## this is where (when) we want to end up while True: tau1, tau2 = ref.accelLimits() dtau = min(nextPt-ref.pt, tau2-ref.pt) ## do not step past the next command boundary g = ref.acceleration() v, x, t = Simulation.tauStep(dtau, ref.v, ref.x, ref.t, g) ref.pt += dtau ref.v = v ref.x = x ref.t = t ref.reft = ref.pt if ref.pt >= nextPt: break #else: #print "Stepped to", tau2, "instead of", nextPt ref.recordFrame(i) ## determine plane visible to reference clock ## this plane goes through the point ref.x, ref.t and has slope = ref.v ## update all other clocks for cl in clocks.values(): while True: g = cl.acceleration() tau1, tau2 = cl.accelLimits() ##Given current position / speed of clock, determine where it will intersect reference plane #t1 = (ref.v * (cl.x - cl.v * cl.t) + (ref.t - ref.v * ref.x)) / (1. - cl.v) t1 = Simulation.hypIntersect(ref.x, ref.t, ref.v, cl.x, cl.t, cl.v, g) dt1 = t1 - cl.t ## advance clock by correct time step v, x, tau = Simulation.hypTStep(dt1, cl.v, cl.x, cl.pt, g) ## check to see whether we have gone past an acceleration command boundary. ## if so, we must instead advance the clock to the boundary and start again if tau < tau1: dtau = tau1 - cl.pt cl.v, cl.x, cl.t = Simulation.tauStep(dtau, cl.v, cl.x, cl.t, g) cl.pt = tau1-0.000001 continue if tau > tau2: dtau = tau2 - cl.pt cl.v, cl.x, cl.t = Simulation.tauStep(dtau, cl.v, cl.x, cl.t, g) cl.pt = tau2 continue ## Otherwise, record the new values and exit the loop cl.v = v cl.x = x cl.pt = tau cl.t = t1 cl.m = None break ## transform position into reference frame x = cl.x - ref.x t = cl.t - ref.t gamma = (1.0 - ref.v**2) ** -0.5 vg = -ref.v * gamma cl.refx = gamma * (x - ref.v * t) cl.reft = ref.pt # + gamma * (t - ref.v * x) # this term belongs here, but it should always be equal to 0. cl.refv = (cl.v - ref.v) / (1.0 - cl.v * ref.v) cl.refm = None cl.recordFrame(i) t += dt def plot(self, plot): plot.clear() for cl in self.clocks.values(): c, p = cl.getCurve() plot.addItem(c) plot.addItem(p) class Animation(pg.ItemGroup): def __init__(self, sim): pg.ItemGroup.__init__(self) self.sim = sim self.clocks = sim.clocks self.items = {} for name, cl in self.clocks.items(): item = ClockItem(cl) self.addItem(item) self.items[name] = item def restart(self): for cl in self.items.values(): cl.reset() def stepTo(self, t): for i in self.items.values(): i.stepTo(t) class ClockItem(pg.ItemGroup): def __init__(self, clock): pg.ItemGroup.__init__(self) self.size = clock.size self.item = QtGui.QGraphicsEllipseItem(QtCore.QRectF(0, 0, self.size, self.size)) tr = QtGui.QTransform.fromTranslate(-self.size*0.5, -self.size*0.5) self.item.setTransform(tr) self.item.setPen(pg.mkPen(100,100,100)) self.item.setBrush(clock.brush) self.hand = QtGui.QGraphicsLineItem(0, 0, 0, self.size*0.5) self.hand.setPen(pg.mkPen('w')) self.hand.setZValue(10) self.flare = QtGui.QGraphicsPolygonItem(QtGui.QPolygonF([ QtCore.QPointF(0, -self.size*0.25), QtCore.QPointF(0, self.size*0.25), QtCore.QPointF(self.size*1.5, 0), QtCore.QPointF(0, -self.size*0.25), ])) self.flare.setPen(pg.mkPen('y')) self.flare.setBrush(pg.mkBrush(255,150,0)) self.flare.setZValue(-10) self.addItem(self.hand) self.addItem(self.item) self.addItem(self.flare) self.clock = clock self.i = 1 self._spaceline = None def spaceline(self): if self._spaceline is None: self._spaceline = pg.InfiniteLine() self._spaceline.setPen(self.clock.pen) return self._spaceline def stepTo(self, t): data = self.clock.refData while self.i < len(data)-1 and data['t'][self.i] < t: self.i += 1 while self.i > 1 and data['t'][self.i-1] >= t: self.i -= 1 self.setPos(data['x'][self.i], self.clock.y0) t = data['pt'][self.i] self.hand.setRotation(-0.25 * t * 360.) v = data['v'][self.i] gam = (1.0 - v**2)**0.5 self.setTransform(QtGui.QTransform.fromScale(gam, 1.0)) f = data['f'][self.i] tr = QtGui.QTransform() if f < 0: tr.translate(self.size*0.4, 0) else: tr.translate(-self.size*0.4, 0) tr.scale(-f * (0.5+np.random.random()*0.1), 1.0) self.flare.setTransform(tr) if self._spaceline is not None: self._spaceline.setPos(pg.Point(data['x'][self.i], data['t'][self.i])) self._spaceline.setAngle(data['v'][self.i] * 45.) def reset(self): self.i = 1 #class Spaceline(pg.InfiniteLine): #def __init__(self, sim, frame): #self.sim = sim #self.frame = frame #pg.InfiniteLine.__init__(self) #self.setPen(sim.clocks[frame].pen) #def stepTo(self, t): #self.setAngle(0) #pass if __name__ == '__main__': app = pg.mkQApp() #import pyqtgraph.console #cw = pyqtgraph.console.ConsoleWidget() #cw.show() #cw.catchNextException() win = RelativityGUI() win.setWindowTitle("Relativity!") win.show() win.resize(1100,700) pg.exec()