diff --git a/doc/readthedocs/hysteresis/Backups/baseClass.py b/doc/readthedocs/hysteresis/Backups/baseClass.py index 24e1346..8732762 100644 --- a/doc/readthedocs/hysteresis/Backups/baseClass.py +++ b/doc/readthedocs/hysteresis/Backups/baseClass.py @@ -13,6 +13,10 @@ # Curve objects # ============================================================================= + + + + class CurveBase: """ @@ -62,21 +66,38 @@ def __init__(self, XYData, xunit = '', yunit = ''): self.xunit = xunit self.yunit = yunit - + def __len__(self): return len(self.xy[:,0]) - # def __truediv__(self, x): - # return self.xy[:,1] / x + def _getInstance(self): + return type(self) + + def _convertToCurve(self, otherCurve): + """ + Converts non-hysteresis datatypes + """ + if isinstance(otherCurve, np.ndarray): + return CurveBase(otherCurve) + # else: + # otherType = type(otherCurve) + # raise Exception(f'multiplication not supported for {otherType}.') + - # def __rtruediv__(self, x): - # return x / self.xy[:,1] - # def __mul__(self, x): - # y = self.xy[:,1]*x - # xy = np.column_stack([x,y]) + def __mul__(self, curve): + """ + I need a way of saving the hysteresis state and copying it over... + """ - # return self.xy[:,1]*x + # Get the current instance of curve + Instance = self._getInstance() + operand = _getOperand(curve) + + x = self.xy[:,0] + y = self.xy[:,1]*operand + + return Instance(np.column_stack([x,y])) # def __add__(self, x): # return self.xy[:,1] + x @@ -164,17 +185,19 @@ def setPeaks(self, peakDist = 2, peakWidth = None, peakProminence = None): self.minIndexes = peakIndexes[1::2] self.maxIndexes = peakIndexes[0::2] - def plot(self, plotCycles = False, plotPeaks = False, labelCycles = []): + def plot(self, plotCycles = False, plotPeaks = False, labelCycles = [], + **kwargs): """ Plots the base curve """ x = self.xy[:,0] y = self.xy[:,1] - return self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles) + return self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles, + **kwargs) def plotVsIndex(self, plotCycles = False, plotPeaks = False, - labelCycles = []): + labelCycles = [], **kwargs): """ Plots the base curve against index (as opposed to X values) """ @@ -182,9 +205,9 @@ def plotVsIndex(self, plotCycles = False, plotPeaks = False, x = np.arange(0,len(self.xy[:,0])) y = self.xy[:,0] - self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles) + self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles, **kwargs) - def plotLoadProtocol(self, comparisonProtocol = []): + def plotLoadProtocol(self, comparisonProtocol = [], **kwargs): """ Plots the peak x values for each cycle in acurve. """ @@ -194,39 +217,59 @@ def plotLoadProtocol(self, comparisonProtocol = []): y = self.loadProtocol x = np.arange(0,len(y)) - self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles) + self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles, **kwargs) if len(comparisonProtocol) != 0: plt.plot(comparisonProtocol) def plotSlope(self, plotCycles = False, plotPeaks = False, - labelCycles = []): + labelCycles = [], **kwargs): x = self.xy[:,0] y = self.slope - self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles) + self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles, **kwargs) - def plotArea(self, plotCycles = False, plotPeaks = False, labelCycles = []): + def plotArea(self, plotCycles = False, plotPeaks = False, labelCycles = [], **kwargs): x = self.xy[:,0] y = self.area - self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles) + self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles, **kwargs) - def plotCumArea(self, plotCycles = False, plotPeaks = False, labelCycles = []): + def plotCumArea(self, plotCycles = False, plotPeaks = False, labelCycles = [], **kwargs): # We get the cumulative displacement and area x = self.getCumDisp() y = self.getCumArea() - self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles) + self.plotFunction(self, x ,y, plotCycles, plotPeaks, labelCycles, **kwargs) def initFig(self, xlims = [], ylims = []): return self.initializeFig(xlims, ylims) +def _getOperand(curve): + """ + Gets the operand (what data the function acts on) for operation functions. + The data used depands on the input given + + In the future, a strategy pattern could be used to replace this. + """ + if not hasattr(curve, '__len__'): # assume scalar if no length + operand = curve + elif hasattr(curve, 'xy'): # use the xy if it's a hysteresis curve + operand = curve.xy[:,1] + elif isinstance(curve, np.ndarray): + if 1 == len(curve.shape): + operand = curve + elif 2 == len(curve.shape) and curve.shape[-1] == 2: # if 2D array + operand = curve[:,1] + else: # if 1D array + raise Exception(f'{curve.shape[-1]}D curve give, only 1 or 2D supported') + return operand + # TODO: # Make the Hysteresis object take in the optional arguements as well. This # Curretnly will not work for non-basic funcitons. @@ -376,8 +419,11 @@ def plotCycles(self, Cycles = [], plotCycles = False, plotPeaks = False, def recalculateCycles(self, revDist = 2, revWidth = None, revProminence = None, **kwargs): """ - Calcualtes the cycles again, using the input parameters for distance, - width, and prominence. Peaks are calculated using scipy's find_peaks function. + Calcualtes the cycles again, using the input parameters for distance + (number of indexes), width (distance on the x axis), and prominence + (distance in the y axis). + + Peaks are calculated using scipy's find_peaks function. Parameters ---------- @@ -442,9 +488,13 @@ def recalculateCycles_like(self, sampleHysteresis): def recalculateCycles_dist(self, revDist = 2, revWidth = None, revProminence = None, **kwargs): """ - Recalulates the reversals of one hysteresis using another the reversal - propreties from another hysteresis. Peaks are calculated using scipy's - find_peaks function. + Calcualtes the cycles again, using the input parameters for distance + (number of indexes), width (distance on the x axis), and prominence + (distance in the y axis). + + The instead of the xy curve, the secant length between each point on + the curve is used to find revesal indexes. + Peaks are calculated using scipy's find_peaks function. Parameters ---------- diff --git a/doc/readthedocs/hysteresis/Backups/baseFuncs.py b/doc/readthedocs/hysteresis/Backups/baseFuncs.py index 58ee4fc..32414e2 100644 --- a/doc/readthedocs/hysteresis/Backups/baseFuncs.py +++ b/doc/readthedocs/hysteresis/Backups/baseFuncs.py @@ -155,4 +155,10 @@ def removeNegative(Curve): Output = _RemoveNeg(x, y, direction) - return Output \ No newline at end of file + return Output + + + + +# def get + diff --git a/doc/readthedocs/hysteresis/Backups/defaultPlotFuncs.py b/doc/readthedocs/hysteresis/Backups/defaultPlotFuncs.py index 18b1562..400900d 100644 --- a/doc/readthedocs/hysteresis/Backups/defaultPlotFuncs.py +++ b/doc/readthedocs/hysteresis/Backups/defaultPlotFuncs.py @@ -108,7 +108,7 @@ def defaultShowCycles(self, x, y, plotCycles, plotPeaks, labelCycles = [], Cycle # Annotate = plt.annotate(int(Cycle), xy=(ReversalX[ii], ReversalY[ii]), xytext=(-1, 5), textcoords = 'offset points', fontsize=12) # Annotate = plt.annotate(int(ii), xy=(ReversalX[ii], ReversalY[ii])) -def defaultPlotFunction(self, x, y, plotCycles, plotPeaks, labelCycles = []): +def defaultPlotFunction(self, x, y, plotCycles, plotPeaks, labelCycles = [], **kwargs): """ Parameters ---------- @@ -132,7 +132,7 @@ def defaultPlotFunction(self, x, y, plotCycles, plotPeaks, labelCycles = []): # fig, ax = initializeFig(xlim, ylim) - line, = plt.plot(x, y) + line, = plt.plot(x, y, **kwargs) defaultShowCycles(self, x, y, plotCycles, plotPeaks, labelCycles) diff --git a/doc/readthedocs/hysteresis/Backups/envelope.py b/doc/readthedocs/hysteresis/Backups/envelope.py index d5d8df5..f1a60e1 100644 --- a/doc/readthedocs/hysteresis/Backups/envelope.py +++ b/doc/readthedocs/hysteresis/Backups/envelope.py @@ -319,7 +319,7 @@ def fitEEEP(backbone): """ Fits a backbone curve with a equivalent elastic perfectly plastic curve using the ASTM E2126 methodology. - The cirve has equivalent area to the input backone curve. + The curve has equivalent area to the input backone curve. http://www.materialstandard.com/wp-content/uploads/2019/10/E2126-11.pdf diff --git a/doc/readthedocs/hysteresis/Backups/plotSpecial/animate.py b/doc/readthedocs/hysteresis/Backups/plotSpecial/animate.py index 9788516..511af8a 100644 --- a/doc/readthedocs/hysteresis/Backups/plotSpecial/animate.py +++ b/doc/readthedocs/hysteresis/Backups/plotSpecial/animate.py @@ -3,17 +3,21 @@ Created on Fri May 7 20:15:19 2021 @author: Christian + + +TODO: + Add arrow key functionality? + Add bliting + Make ABC and make update an abstract method. """ -import hysteresis as hys +# import hysteresis as hys import matplotlib.pyplot as plt -import matplotlib.animation as animation -from matplotlib.widgets import Button - -# from matplotlib.animation import FuncAnimation +from matplotlib.animation import FuncAnimation +# from dataclasses import dataclass +from matplotlib.widgets import Button, Slider import numpy as np - # Add this function to somewhere else def init_Animation(): fig, ax = plt.subplots() @@ -27,13 +31,29 @@ def getAnixy(rawFrames, skipFrames): return xyAni -def getAniFrames(x, targetdx): +def getAniFrames(x:list, targetdx:float): """ + given a input array of positive nu x, return a new array + Returns a frame every target dx. Can be useful for pre-processing data if input data has a variable timestep. No linearl inerpolatin is used for intermediate frames. + + Parameters + ---------- + x : list + DESCRIPTION. + targetdx : float + DESCRIPTION. + + Returns + ------- + TYPE + DESCRIPTION. + """ + NFrames = len(x) NframesOut = [] @@ -50,160 +70,299 @@ def getAniFrames(x, targetdx): class AnimationBase: - def __init__(self): - self.play = True + def initAnimation(self): + self.isPlaying = True + self.fig, self.ax = init_Animation() + + def connectWidgets(self): + + """ + Sets up the canvas for wigits. This includes the bottom slider, + as well as connecting press and click events. + """ + plt.subplots_adjust(bottom=0.25) - # Replace with imported function - fig, ax = init_Animation() - # Connect clicking - # fig.canvas.mpl_connect('button_press_event', self.on_click) - fig.canvas.mpl_connect('button_press_event', self.toggle_pause) + self.fig.canvas.mpl_connect('button_press_event', self.on_click) + self.fig.canvas.mpl_connect('key_press_event', self.on_press) - self.fig = fig - self.ax = ax - - - - - def togglePlay(self): - self.play = self.play == False + # set up the slider axisd + self.Sax = plt.axes([0.20, 0.1, 0.7, 0.03]) + self.slide = Slider(self.Sax, "Index", 0, self.Nframes, valinit=0, + valstep = 1, valfmt = "%i", color="green") + self.fig.canvas.draw() + # connect the update function to run when the slider changes + self.slide.on_changed(self.update_line_slider) - def on_click(self, event): + # self.canvas.draw_idle() + def on_press(self, event): + print(event.key) + + def togglePlay(self): + """ Turns on or off the animation """ + self.isPlaying = self.isPlaying == False + + # def toggle_pause(self, event, *args, **kwargs): + def toggle(self, event): + """ + Toggles playing on or off + """ + if self.isPlaying == True: + # self.ani.pause() + self.ani.event_source.stop() + self.fig.canvas.draw_idle() + else: + # self.ani.resume() + self.ani.event_source.start() + self.togglePlay() + + def getClickXY(self, event): xclick = event.x yclick = event.y - return xclick, yclick - # print(xclick, yclick) - # To be overwritten - def toggle_pause(self,event, *args, **kwargs): + def stepForward(self, event): + pass + def stepBack(self, event): + event.key + def getFrame(self, x): pass - - # # Check where the click happened - # (xm,ym),(xM,yM) = plotSlider.label.clipbox.get_points() - # if xm < event.x < xM and ym < event.y < yM: - # # Event happened within the slider, ignore since it is handled in update_slider - # return - # else: - # # Toggle on off based on clicking - # global is_paused - # if is_paused == True: - # is_paused=False - # elif is_paused == False: - # is_paused=True - - - -# class FrameHelper(): + + def on_click(self, event): + """ + Toggles on or off playing if a click event happened within the main + graph. + + Parameters + ---------- + event : matplotlib event + The matplotlib click event class. + """ + # Check where the click happened + (xm,ym),(xM,yM) = self.slide.label.clipbox.get_points() + if xm < event.x < xM and ym < event.y < yM: + # Event happened within the slider, ignore since it is handled in update_slider + return + else: + self.toggle(event) + + + # Define the update function + def update_line_slider(self, currentFrame): + """ + Converts changes in the slider to a frame that can be passed to the + update function. + """ + + self.aniArtists = self.update(currentFrame) + self.fig.canvas.draw_idle() -# def __init__(self, pointsPerFrame = 1, skipFrames = 1, skipStart = 0, skipEnd = 0): -# self.pointsPerFrame = pointsPerFrame + def _get_next_frame(self, currentFrame): + currentFrame += 1 + if currentFrame >= self.frames[-1]: + currentFrame = 0 + return currentFrame + + def update_slider_widget(self, frame): + + """ + Updates the plot based on the current value of the slider. + Returns a set artists for the animations + """ + # Find the close timeStep and plot that + # CurrentFrame = int(np.floor(plotSlider.val)) + # convert the slider value to a time step + currentTime = self.slide.val + currentFrame = int(currentTime) + # CurrentFrame = (np.abs(timeSteps - CurrentTime)).argmin() + + aniframe = self._get_next_frame(currentFrame) + + self.slide.set_val(aniframe) + + @staticmethod + def update(self, frame): + """updates the plot""" + pass + + def animate(self): + self.initAnimation() + # set the update function + if self.widgets == True: + self.connectWidgets() + update = self.update_slider_widget + else: + update = self.update + + self.ani = FuncAnimation(self.fig, update, self.frames, + interval=self.interval, blit=False) +# @dataclass class Animation(AnimationBase): - - def __init__(self, Curve, pointsPerFrame = 1, skipFrames = 1, skipStart = 0, skipEnd = 0, interval = 50): - - super().__init__() + def __init__(self, Curve, pointsPerFrame = 1, skipFrames = 1, + skipStart = 0, skipEnd = 0, interval = 50, widgets = True): + """ + Creates a animation of the input curve object + + Parameters + ---------- + Curve : Hysteresis Curve + The curve to animate. + pointsPerFrame : int, optional + The number of data points to draw per frame. The default is 1. + skipFrames : TYPE, optional + THe number of animation frames to skip per input. This reduces + can be used to reduce the size of large data arrays. The default is + 1, which shows all frames. + skipStart : int, optional + Allows the user to skip this many frames at the start. + Skipped frames are applied after the other frame filters are applied. + The default is 0, which skips no start frames. + skipEnd : TYPE, optional + Allows the user to skip this many frames at the start. + Skipped frames are applied after the other frame filters are applied. + The default is 0, which skips no start frames. + interval : int, optional + The target time in ms the frame will be dispalyed for. + The default is 50ms. + widgets : Boolean, optional + A toggle that allows the user to turn on or off widgets. + The default is True, which has the widgets on. + + """ + self.Curve = Curve + self.pointsPerFrame = pointsPerFrame + self.skipStart = skipStart + self.skipEnd = skipEnd + self.interval = interval + self.widgets = widgets self.xy = Curve.xy - self.pointsPerFrame = pointsPerFrame - self.interval = interval + xyAni = getAnixy(self.xy, skipFrames) + self.xyAni = self.skipStartEnd(xyAni, skipStart, skipEnd) - xyAni = getAnixy(Curve.xy, skipFrames) - - self.xyAni = xyAni - # self.xyAni = self.xy self.Nframes = int(len(self.xyAni) / pointsPerFrame) - self.frames = np.arange(self.Nframes) - - self.lines = [] - - - # self.fig.canvas.mpl_connect('button_press_event', self.toggle_pause) + self.frames = np.arange(self.Nframes) + def validateData(self): + pass - # def setAniXY() - def initfunc(self): - line = plt.plot(self.xyAni[:,0], self.xyAni[:,1])[0] - self.lines.append(line) # def initAnimation(self): + def skipStartEnd(self,xyAni, skipStart, skipEnd): - return line, + if skipEnd == 0: + return xyAni[skipStart:, :] + else: + skipEnd *= -1 + return xyAni[skipStart:skipEnd, :] + + def update(self, frame): - - # for ii in range() + """ + Updates the canvas at the given frame. + + """ points = int(frame*self.pointsPerFrame) - - newXY = self.xyAni[:points,:] - line = self.lines[0] + newXY = self.xyAni[:points,:] + line = self.lines[0] line.set_data(newXY[:,0], newXY[:,1]) - - return line, + # self.fig.canvas.draw_idle() + return [line] - def Animate(self): - self.ani = animation.FuncAnimation(self.fig, self.update, self.frames, self.initfunc, - interval=self.interval, blit=True) + def initAnimation(self): + super().initAnimation() + line = plt.plot(self.xyAni[:,0], self.xyAni[:,1])[0] + self.lines = [line] - # def toggle_pause(self, *args, **kwargs): - # self.togglePlay() - - # if self.play: - # self.ani.resume() - # else: - # self.ani.pause() - class JointAnimation(AnimationBase): - def __init__(self, Curves, pointsPerFrame = 1, skipFrames = 1, skipStart = 0, skipEnd = 0, interval = 50): - + def __init__(self, curves, pointsPerFrame = 1, skipFrames = 1, + skipStart = 0, skipEnd = 0, interval = 50, widgets = True): + """ + Animates several plots on the same graph. All input graphs must have + the same number of data points. + + Parameters + ---------- + Curves : list + The list of hysteresis curves to animate. + pointsPerFrame : int, optional + The number of data points to draw per frame. The default is 1. + skipFrames : TYPE, optional + THe number of animation frames to skip per input. This reduces + can be used to reduce the size of large data arrays. The default is + 1, which shows all frames. + skipStart : int, optional + Allows the user to skip this many frames at the start. + Skipped frames are applied after the other frame filters are applied. + The default is 0, which skips no start frames. + skipEnd : TYPE, optional + Allows the user to skip this many frames at the start. + Skipped frames are applied after the other frame filters are applied. + The default is 0, which skips no start frames. + interval : int, optional + The target time in ms the frame will be dispalyed for. + The default is 50ms. + widgets : Boolean, optional + A toggle that allows the user to turn on or off widgets. + The default is True, which has the widgets on. + + Returns + ------- + None. + + """ + + super().__init__() - self.pointsPerFrame = pointsPerFrame - self.interval = interval - - self.Curves = Curves - self.Ncurves = len(Curves) - - xyAni = getAnixy(Curves[0].xy, skipFrames) - self.Nframes = int(len(xyAni) / pointsPerFrame) - self.frames = np.arange(self.Nframes) + self._validateCurves(curves) - self.xyCurves = [None]*self.Ncurves - self.lines = [] - - for ii in range(self.Ncurves): - curve = self.Curves[ii] - xy = curve.xy - - xyAni = getAnixy(xy, skipFrames) - self.xyCurves[ii] = xyAni - - # self.xyAni = xyAni + self.pointsPerFrame = pointsPerFrame + self.skipFrames = skipFrames + self.skipStart = skipStart + self.skipEnd = skipEnd + self.interval = interval + self.widgets = widgets + self.Curves = curves + self.Ncurves = len(curves) - # self.fig.canvas.mpl_connect('button_press_event', self.toggle_pause) - + xyAni = getAnixy(curves[0].xy, skipFrames) + self.Nframes = int(len(xyAni) / pointsPerFrame) + self.frames = np.arange(self.Nframes) + + def _validateCurves(self, curves): + Lcurve = len(curves[0]) + ii = 1 + for curve in curves[1:]: + if len(curve) != Lcurve: + raise Exception('Curves must all have the same number of datapoints.') + ii +=1 + + + def initAnimation(self): + super().initAnimation() - # def setAniXY() - def initfunc(self): - + self.lines = [] + self.xyCurves = [None]*self.Ncurves for ii in range(self.Ncurves): - tempXY = self.xyCurves[ii] - line = plt.plot(tempXY[:,0], tempXY[:,1])[0] - self.lines.append(line) # def initAnimation(self): - - return self.lines - + xy = self.Curves[ii].xy + xyAni = getAnixy(xy, self.skipFrames) + + self.xyCurves[ii] = xyAni + line = plt.plot(xyAni[:,0], xyAni[:,1])[0] + self.lines.append(line) # def initAnimation(self): + def update(self, frame): # print(frame) @@ -219,113 +378,28 @@ def update(self, frame): line = self.lines[ii] line.set_data(newXY[:,0], newXY[:,1]) lines[ii] = line - - + + self.aniArtists = lines + # self.aniArtists # lines[ii] = line # lines = self.lines - return lines - - def Animate(self): - self.ani = animation.FuncAnimation(self.fig, self.update, self.frames, self.initfunc, - interval=50, blit=True) - - + + # return lines + return self.aniArtists + # def animate(self): + # self.initAnimation() + # if self.widgets == True: + # self.connectWidgets() + # update = self.update_slider_widget + # else: + # update = self.update + # self.ani = FuncAnimation(self.fig, update, self.frames, + # interval=50, blit=False) - - - # axSlider = plt.axes([0.25, .03, 0.50, 0.02]) - # plotSlider = Slider(axSlider, 'Time', framesTime[FrameStart], framesTime[FrameEnd], valinit=framesTime[FrameStart]) - - - - # # Slider Location and size relative to plot - # # [x, y, xsize, ysize] - # axSlider = plt.axes([0.25, .03, 0.50, 0.02]) - # plotSlider = Slider(axSlider, 'Time', framesTime[FrameStart], framesTime[FrameEnd], valinit=framesTime[FrameStart]) - # # Animation controls - # global is_paused - # is_paused = False # True if user has taken control of the animation - # def on_click(event): - # # Check where the click happened - # (xm,ym),(xM,yM) = plotSlider.label.clipbox.get_points() - # if xm < event.x < xM and ym < event.y < yM: - # # Event happened within the slider, ignore since it is handled in update_slider - # return - # else: - # # Toggle on off based on clicking - # global is_paused - # if is_paused == True: - # is_paused=False - # elif is_paused == False: - # is_paused=True - - # def animate2D_slider(Time): - # """ - # The slider value is liked with the plot - we update the plot by updating - # the slider. - # """ - # global is_paused - # is_paused=True - # # Convert time to frame - # TimeStep = (np.abs(framesTime - Time)).argmin() - - # # The current node coordinants in (x,y) or (x,y,z) - # CurrentNodeCoords = nodes[:,1:] + Disp[TimeStep,:,:] - # # Update Plots - - # # update node locations - # EqfigNodes.set_xdata(CurrentNodeCoords[:,0]) - # EqfigNodes.set_ydata(CurrentNodeCoords[:,1]) - - # # Get new node mapping - # # I don't like doing this loop every time - there has to be a faster way - # xy_labels = {} - # for jj in range(Nnodes): - # xy_labels[nodeLabels[jj]] = CurrentNodeCoords[jj,:] - - # # Define the surface - # SurfCounter = 0 - - # # update element locations - # for jj in range(Nele): - # # Get the node number for the first and second node connected by the element - # TempNodes = elements[jj][1:] - # # This is the xy coordinates of each node in the group - # TempNodeCoords = [xy_labels[node] for node in TempNodes] - # coords_x = [xy[0] for xy in TempNodeCoords] - # coords_y = [xy[1] for xy in TempNodeCoords] - - # # Update element lines - # EqfigLines[jj].set_xdata(coords_x) - # EqfigLines[jj].set_ydata(coords_y) - # # print('loop start') - # # Update the surface if necessary - # if 2 < len(TempNodes): - # tempxy = np.column_stack([coords_x, coords_y]) - # EqfigSurfaces[SurfCounter].xy = tempxy - # SurfCounter += 1 - - # # update time Text - # # time_text.set_text("Time= "+'%.2f' % time[TimeStep]+ " s") - - # # redraw canvas while idle - # fig.canvas.draw_idle() - - # return EqfigNodes, EqfigLines, EqfigSurfaces, EqfigText - - - - # Saving - # if Movie != "none": - # MovefileName = Movie + '.mp4' - # ODBdir = Model+"_ODB" # ODB Dir name - # Movfile = os.path.join(ODBdir, LoadCase, MovefileName) - # print("Saving the animation movie as "+MovefileName+" in "+ODBdir+"->"+LoadCase+" folder") - # ani.save(Movfile, writer='ffmpeg') - + \ No newline at end of file diff --git a/doc/readthedocs/hysteresis/baseClass.py b/doc/readthedocs/hysteresis/baseClass.py index 8732762..e4abd93 100644 --- a/doc/readthedocs/hysteresis/baseClass.py +++ b/doc/readthedocs/hysteresis/baseClass.py @@ -15,9 +15,9 @@ - - class CurveBase: + + __array_ufunc__ = None """ The curve base object represents a generic xy curve with no limitations. @@ -35,25 +35,20 @@ def __init__(self, XYData, xunit = '', yunit = ''): Parameters ---------- XYData : array - The input array of XY date for the curve. - areaFunction : function, optional - The function to be used to calcualte area. - The default is defaultareaFunction. - slopeFunction : function, optional - The function to be used to calcualte slope. - The default is defaultareaFunction. - plotFunction : function, optional - The function to be used to plot the curve. - The default is defaultPlotFunction. + The input array of XY date for the curve. A list [x,y] can also be + passed into the function. xunit : str, optional The units on the x axis. The default is ''. yunit : str, optional The units on the y axis. The default is ''. """ - self.xy = XYData + + + self.xy = self._parseXY(XYData) self.Npoints = len(XYData[:,0]) + # The function to be used to calcualte area. These can be overwritten self.areaFunction = env.environment.fArea self.slopeFunction = env.environment.fslope self.lengthFunction = env.environment.flength @@ -67,44 +62,86 @@ def __init__(self, XYData, xunit = '', yunit = ''): self.xunit = xunit self.yunit = yunit + def _parseXY(self, xy): + if isinstance(xy, list): + xy = np.column_stack(xy) + return xy + def __len__(self): return len(self.xy[:,0]) def _getInstance(self): return type(self) - def _convertToCurve(self, otherCurve): + def _convertToCurve(self, other): """ Converts non-hysteresis datatypes """ - if isinstance(otherCurve, np.ndarray): - return CurveBase(otherCurve) + if isinstance(other, np.ndarray): + return CurveBase(other) # else: # otherType = type(otherCurve) # raise Exception(f'multiplication not supported for {otherType}.') - + def __add__(self, other): + """ Enables addition of curve x values""" + Instance = self._getInstance() + operand = _getOperand(other) + x = self.xy[:,0] + y = self.xy[:,1]+operand + + return Instance(np.column_stack([x,y])) + + def __sub__(self, other): + Instance = self._getInstance() + operand = _getOperand(other) + x = self.xy[:,0] + y = self.xy[:,1] - operand + + return Instance(np.column_stack([x,y])) + + def __rsub__(self, other): + Instance = self._getInstance() + operand = _getOperand(other) + x = self.xy[:,0] + y = operand - self.xy[:,1] + + return Instance(np.column_stack([x,y])) - def __mul__(self, curve): + def __mul__(self, other): """ - I need a way of saving the hysteresis state and copying it over... + It would be useful of having a hystresis base state, then copying that over. """ - # Get the current instance of curve Instance = self._getInstance() - operand = _getOperand(curve) - + operand = _getOperand(other) x = self.xy[:,0] y = self.xy[:,1]*operand return Instance(np.column_stack([x,y])) - # def __add__(self, x): - # return self.xy[:,1] + x - - # def __sub__(self, x): - # return self.xy[:,1] - x + + def __truediv__(self, other): + # Get the current instance of curve + Instance = self._getInstance() + operand = _getOperand(other) + x = self.xy[:,0] + y = self.xy[:,1] / operand + return Instance(np.column_stack([x,y])) + + + def __rtruediv__(self, other): + # Get the current instance of curve + Instance = self._getInstance() + operand = _getOperand(other) + x = self.xy[:,0] + y = operand / self.xy[:,1] + + return Instance(np.column_stack([x,y])) + + + def setArea(self): """ sets the area under each point of the curve using the area function""" self.area = self.areaFunction(self.xy) @@ -184,7 +221,11 @@ def setPeaks(self, peakDist = 2, peakWidth = None, peakProminence = None): else: self.minIndexes = peakIndexes[1::2] self.maxIndexes = peakIndexes[0::2] - + + + + + def plot(self, plotCycles = False, plotPeaks = False, labelCycles = [], **kwargs): """ @@ -259,9 +300,9 @@ def _getOperand(curve): """ if not hasattr(curve, '__len__'): # assume scalar if no length operand = curve - elif hasattr(curve, 'xy'): # use the xy if it's a hysteresis curve + elif hasattr(curve, 'xy'): # use the xy if it's a curve from the hysteresis module. operand = curve.xy[:,1] - elif isinstance(curve, np.ndarray): + elif isinstance(curve, np.ndarray): # If a numpy array, do stuff depending on array dimension. if 1 == len(curve.shape): operand = curve elif 2 == len(curve.shape) and curve.shape[-1] == 2: # if 2D array @@ -285,7 +326,7 @@ def _getOperand(curve): class Hysteresis(CurveBase): """ Hysteresis objects are those that have at least one reversal point in - the x direction of the data. + their data. Reversal points are those where the x axis changes direction. The hysteresis object has a number of functions that help find the reversal points in the x data of the curve. @@ -293,15 +334,39 @@ class Hysteresis(CurveBase): The hysteresis object also stores each each half-cycle (where there is no change in direction) as a SimpleCycle object. + + Parameters + ---------- + XYData : array + The input array of XY date for the curve. A list [x,y] can also be + passed into the function. + revDist : int, optional + Used to filter reversal points based on the minimal horizontal distance + (>= 1) between neighbouring peaks in the x axis. Smaller peaks are + removed first until the condition is fulfilled for all remaining peaks. + The default is 2. + revWidth : int, optional + Used to filter reversal points using the approximate width in number of + samples of each peak at half it's prominence. Peaks that occur very + abruptly have a small width, while those that occur gradually have + a big width. + The default is None, which results in no filtering. + revProminence : number, optional + Used to filter reversal points that aren't sufficently high. Prominence + is the desired difference in height between peaks and their + neighbouring peaks. + The default is None, which results in no filtering. + """ - revDist = 2 - revWidth = None - revProminence = None + def __init__(self, XYData, revDist = 2, revWidth = None, revProminence = None, setCycles = True, setArea = True, setSlope = True, **kwargs): + CurveBase.__init__(self, XYData, **kwargs) + self.setReversalPropreties(revDist, revWidth, revProminence) + #TODO Create warning if cycles don't make sense. if setCycles == True: @@ -537,7 +602,35 @@ def RemoveCycles(): class SimpleCycle(CurveBase): """ A curve that doesn't change direction on the X axis, but can change - Y direction. + Y direction. The data has a number of peaks, each of which is the largest + y value in relation to other points on the curve. This point canbe + + + Parameters + ---------- + XYData : array + The input array of XY date for the curve. A list [x,y] can also be + passed into the function. + peakDist : int, optional + Used to filter peaks based on the minimal horizontal distance in number + of samples (>= 1) between neighbouring peaks in the y axis. + Smaller peaks are removed first until the condition is fulfilled for + all remaining peaks. + The default is 2. + peakWidth : int, optional + Used to filter peaks points using the approximate width in number of + samples of each peak at half it's prominence. Peaks that occur very + abruptly have a small width, while those that occur gradually have + a big width. + The default is None, which results in no filtering. + peakProminence : number, optional + Used to filter peaks points that aren't sufficently high. Prominence + is the desired difference in height between peaks and their + neighbouring peaks. + The default is None, which results in no filtering. + + + """ def __init__(self, XYData, findPeaks = False, setSlope = False, setArea = False, @@ -673,6 +766,12 @@ def recalculatePeaks(self, peakDist = 2, peakWidth = None, peakProminence = None self.setArea() class MonotonicCurve(CurveBase): + """ + A curve that has no changes in it's x axis direction, as well as no changes + in it's y axis. + """ + + def __init__(self, XYData): CurveBase.__init__(self, XYData) diff --git a/doc/readthedocs/hysteresis/data.py b/doc/readthedocs/hysteresis/data.py index 02e2cc3..b05eab9 100644 --- a/doc/readthedocs/hysteresis/data.py +++ b/doc/readthedocs/hysteresis/data.py @@ -97,6 +97,7 @@ def getCycleIndicies(VectorX, peakDist = 2, peakWidth = None, """ This function finds the index where there is areversal in the XY data. You may need to adjust the find peaks factor to get satisfactory results. + Built on the scipy find peaks funciton Parameters @@ -107,8 +108,19 @@ def getCycleIndicies(VectorX, peakDist = 2, peakWidth = None, Input Y Vector. This is only used if we want to plot the values. CreatePlot : Boolean This switch specifies whether or not to display the output.plot - peakDist : TYPE, optional - The sampling distance used to find peaks. The default is 2. + peakDist : int, optional + Required minimal horizontal distance (>= 1) in samples between + neighbouring peaks. Smaller peaks are removed first until the condition + is fulfilled for all remaining peaks. + The default is 2. + peakWidth : int, optional + The approximate width in number of samples of the peak at half it's prominence. + Peaks that occur very abruptly have a small width, while those that occur + gradually have a big width. + peakProminence : number, optional + Used to filter out peaks that aren't sufficently high. Prominence is + the desired difference in height between peaks and their neighbouring peaks. + The default is None, which results in no filtering. Returns ------- diff --git a/doc/readthedocs/source/examples/02 Basic Usage/2.5 Utility Features/2.5.1 Operations.py b/doc/readthedocs/source/examples/02 Basic Usage/2.5 Utility Features/2.5.1 Operations.py new file mode 100644 index 0000000..fcaab89 --- /dev/null +++ b/doc/readthedocs/source/examples/02 Basic Usage/2.5 Utility Features/2.5.1 Operations.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +@author: Christian + +Curves in the hysteresis module are compatible with basic operations, i.e. +addition, subtraction, multiplication, and + + +""" + +import numpy as np +import hysteresis as hys + +x = np.linspace(0,4,1001) +y = x**3 - 3*x**2 + 3 + +xy = np.column_stack([x,y]) +dataHys = hys.Hysteresis(xy) + + + +""" +The it's possible to perform standard operations using scalars, numpy arrays +of equal size, and other hysteresis curves to each hysteresis. +These operations are applied to the y data of the curve respectivly. + +""" +examples = [] +examples.append( dataHys + 1 ) +examples.append( dataHys + 2*x ) +examples.append( 2 - dataHys/2 + 2*x ) +examples.append( dataHys * dataHys / (y+50) ) + +dataHys.plot() +for curve in examples: + curve.plot(linewidth = 2) + + + + + + diff --git a/doc/readthedocs/source/examples/02 Basic Usage/Readme.md b/doc/readthedocs/source/examples/02 Basic Usage/Readme.md index 682c877..a04bb47 100644 --- a/doc/readthedocs/source/examples/02 Basic Usage/Readme.md +++ b/doc/readthedocs/source/examples/02 Basic Usage/Readme.md @@ -7,6 +7,7 @@ The following examples show basic usage of the Hysteresis objects: * 2.2 Basic Seismic Damper: an example working with 'messy' experimental data, as well as comparing two hystereses. * 2.3 Seismic Damper 1: A hands off example showing how to get cumulative energy in a seismic damper * 2.4 Seismic Damper 2: A hands off example showing how to get cumulative energy in a seismic damper +* 2.5 Utility Features: An example of some of the miscellaneous that can be done with hystesis curves. diff --git a/doc/readthedocs/source/rst/Ex-02-2.5-Utility-Features.rst b/doc/readthedocs/source/rst/Ex-02-2.5-Utility-Features.rst new file mode 100644 index 0000000..0e5a989 --- /dev/null +++ b/doc/readthedocs/source/rst/Ex-02-2.5-Utility-Features.rst @@ -0,0 +1,10 @@ +Utility Features +======== + +An example of operations can be used with hysteresis curves. + +.. literalinclude:: /examples/02 Basic Usage/2.5 Utility Features/2.5.1 Operations.py + + + + diff --git a/doc/readthedocs/source/rst/Ex-02-BasicUsage.rst b/doc/readthedocs/source/rst/Ex-02-BasicUsage.rst index ea2e3b7..9daf462 100644 --- a/doc/readthedocs/source/rst/Ex-02-BasicUsage.rst +++ b/doc/readthedocs/source/rst/Ex-02-BasicUsage.rst @@ -8,6 +8,7 @@ Examples are broken into the following categories #. :doc:`Ex-02-2.2-Hysteresis-with-Noisey-Data` #. :doc:`Ex-02-2.3-Seismic-Damper-1` #. :doc:`Ex-02-2.4-Seismic-Damper-2` +#. :doc:`Ex-02-2.5-Utility-Features` .. toctree:: :maxdepth: 2 @@ -17,6 +18,8 @@ Examples are broken into the following categories Ex-02-2.2-Hysteresis-with-Noisey-Data.rst Ex-02-2.3-Seismic-Damper-1.rst Ex-02-2.4-Seismic-Damper-2.rst + Ex-02-2.5-Utility-Features.rst + diff --git a/examples/02 Basic Usage/Readme.md b/examples/02 Basic Usage/Readme.md index 682c877..a04bb47 100644 --- a/examples/02 Basic Usage/Readme.md +++ b/examples/02 Basic Usage/Readme.md @@ -7,6 +7,7 @@ The following examples show basic usage of the Hysteresis objects: * 2.2 Basic Seismic Damper: an example working with 'messy' experimental data, as well as comparing two hystereses. * 2.3 Seismic Damper 1: A hands off example showing how to get cumulative energy in a seismic damper * 2.4 Seismic Damper 2: A hands off example showing how to get cumulative energy in a seismic damper +* 2.5 Utility Features: An example of some of the miscellaneous that can be done with hystesis curves.