def smd(self, f0=0.05, f1=0.75): """Compute the strong motion duration. Parameters ---------- f0 : float, optional Fraction of Arias intensity at the beginning of the strong motion duration. Default 0.05. f1 : float, optional Fraction of Arias intensity at the end of the strong motion duration. Default 0.75. Returns ------- tuple A tuple with three floats: 0. `smd` - the strong motion duration (``smd = t1 - t0``) 1. `t0` - time when the Arias intensity reached `f0`. 2. `t1` - time when the Arias intensity reached `f1`. """ if self.ordinate != 'a': config.vprint( 'WARNING: class method smd() is intended for acceleration time histories' ) Ea = cumtrapz(self.data**2, self.time, initial=0) t0, t1 = np.interp([f0 * Ea[-1], f1 * Ea[-1]], Ea, self.time) smd = t1 - t0 return (smd, t0, t1)
def differentiate(self, v=False): """Differentiate the time history with respect to time. This function uses :func:`numpy.gradient()` to perform the operation. Set ``v = True`` for verbose confirmations.""" if v: config.vprint( 'WARNING: differentiating a time history can ' 'degrade the information contained in the signal and lead to ' 'inaccurate results.') if self.dt_fixed: self.data = np.gradient(self.data, self.dt) else: self.data = np.gradient(self.data, self.time) if self.ordinate == 'd': if v: config.vprint('Time history {} has been differentiated and ' 'now represents velocity.'.format(self.label)) self.ordinate = 'v' elif self.ordinate == 'v': if v: config.vprint('Time history {} has been differentiated and ' 'now represents acceleration.'.format( self.label)) self.ordinate = 'a' elif self.ordinate == 'a': config.vprint('WARNING: you are differentiating an acceleration ' 'time history.') self.ordinate = 'n/a' return self
def harmonic(dt, f=1.0, Td=None, A=1.0, ordinate='a'): """ Create a sinusoidal time history. Parameters ---------- dt : float The time step. f : float, optional The frequency of the time history (in Hz). Td : float, optional The duration of the time history. The default value is ``Td = 100*dt``. A : float, optional The amplitude of the time history. ordinate : {'d', 'v', 'a'}, optional The physical quality of the data generated by this function, with 'd' = displacement, 'v' = velocity, 'a' = acceleration. Returns ------- th : an instance of class TimeHistory Notes ----- If ``Td/dt`` is not a whole number, then the duration will be increased such that ``Td = ceil(Td/dt)*dt``. """ if Td is None: Td = 100 * dt n = ceil(Td / dt) Tdn = n * dt if Tdn != Td: config.vprint('WARNING: the duration has been increased to {:6.2f} ' 'seconds to accommodate {} timesteps'.format(Tdn, n)) Td = Tdn w = 2 * pi * f time = np.linspace(0., Tdn, n) data = A * np.sin(w * time) th = TimeHistory(time, data, ordinate) th.setdt(dt, True, 1 / (2 * dt)) return th
def integrate(self, v=False): """Integrate the time history with respect to time. Set ``v = True`` for verbose confirmations.""" self.data = cumtrapz(self.data, self.time, initial=0) if self.ordinate == 'd': config.vprint('WARNING: you are integrating a displacement time ' 'history.') self.ordinate = 'n/a' elif self.ordinate == 'v': if v: config.vprint('Time history {} has been integrated and now ' 'represents displacement.'.format(self.label)) self.ordinate = 'd' elif self.ordinate == 'a': if v: config.vprint('Time history {} has been integrated and now ' 'represents velocity.'.format(self.label)) self.ordinate = 'v' return self
def directS2S(ND, OD, z0, method='Jiang (2015)', zmin=0.001, INRES=True, **kwargs): """Compute an in-structure response spectrum (ISRS) using a direct spectrum-to-spectrum method. Parameters ---------- ND : int, {1, 2, 3} Number of excitation directions to take into account. OD : int, {0, 1, 2} Direction of in-structure response ('output direction'). z0 : float Damping ratio of the secondary system. method : str, optional The numerical method used to compute the ISRS. Valid options are 'Jiang (2015)' (default) and 'Der Kiureghian (1981)'. zmin : float, optional Lower bound modal damping value of the primary system. Useful if some primary modes have zero or unrealistically low damping. Default 0.001. INRES : bool, optional Include residual response in the computation. Default `True`. See DISRS documentation for further information. wd : str, optional Path to working directory. If this is not provided, `wd` will default to the current working directory. rslist : a list of lists of response spectra, optional Input spectra for ISRS computation. If `rslist` is not provided, the function will look for a file named `SpectralData.txt` in the working directory. The length of `rslist` must satisfy: ``len(rslist) >= ND``. The length of ``rslist[0]`` determines the number of damping values (`Nzb`). If ``rslist[1]`` is provided, then ``len(rslist[1]) == len(rslist[0])``. If ``rslist[2]`` is provided, then ``len(rslist[2]) == len(rslist[0])``. mdlist : a list of NumPy arrays, optional. This parameter contains the modal properties of the primary system, where: * ``mdlist[0]`` is a 1D array with natural frequencies (with ``Np = len(mdlist[0])``); * ``mdlist[1]`` is a 1D array with modal damping ratios; * ``mdlist[2]`` is a 2D array with participation factors (the shape of this array must be (`Np`,`ND`); * ``mdlist[3]`` is a 1D or 2D array with modal displacements (the shape of this array must be (`Np`,`ND`) or just (`Np`,) if `ND` = 1). Other parameters ---------------- The following named parameters are used in the Jiang (2015) method: fc : tuple of floats, required Corner frequencies. See the DirectS2S Documentation. GT : str, optional Ground type ('H' for hard, 'S' for soft). Only relevant for vertical excitation. Default 'H'. The following named parameters are used in the Der Kiureghian (1981) method: m0 : float, optional Mass of secondary system. Default value 0. Returns ------- rs : an instance of class ResponseSpectrum The in-structure response spectrum for response in direction `OD`. Notes ----- A more thorough DirectS2S documentation is available as a PDF document: EN/NU/TECH/TGN/031, Direct Generation of In-Structure Response Spectra. """ # Time the execution start = time.perf_counter() config.vprint( 'Computing in-structure response spectrum with method: {}'.format( method)) # Check if a path has been specified; if not, assume current working directory if 'wd' in kwargs: wd = Path(kwargs['wd']) else: wd = Path.cwd() # Retrieve modal information (from textfile if not supplied in **kwargs) if 'mdlist' in kwargs: mdlist = kwargs['mdlist'] fp = mdlist[0] zp = np.fmax(mdlist[1], zmin) gam = mdlist[2] phi = mdlist[3] else: MD = np.loadtxt(wd / 'ModalData.txt') fp = MD[:, 0] zp = np.fmax(MD[:, 1], zmin) gam = MD[:, 2:(2 + ND)] phi = MD[:, (2 + ND + OD)] # Retrieve response spectra at the base of the primary system. # If rslist is not provided, read spectral input data from file if 'rslist' in kwargs: rslist = kwargs['rslist'] assert len(rslist) >= ND, ( 'The length of rslist is insufficient to ' 'compute response over {} directions'.format(ND)) Nfb = rslist[0][0].ndat Nzb = len(rslist[0]) fb = rslist[0][0].f zb = np.empty(Nzb) SAb = np.empty((Nfb, Nzb, ND)) for i in range(Nzb): zb[i] = rslist[0][i].xi for j in range(ND): SAb[:, i, j] = rslist[j][i].sa else: SDb = np.loadtxt(wd / 'SpectralData.txt') Nfb = np.size(SDb, axis=0) - 1 Nzb = (np.size(SDb, axis=1) - 1) // ND fb = SDb[1:Nfb + 1, 0] zb = SDb[0, 1:Nzb + 1] SAb = np.empty((Nfb, Nzb, ND)) for d in range(ND): SAb[:, :, d] = SDb[1:Nfb + 1, d * Nzb + 1:(d + 1) * Nzb + 1] if method == 'Jiang (2015)': if 'fc' in kwargs: fc = kwargs['fc'] else: raise ValueError('With method=\'Jiang (2015)\', the parameter fc ' 'must be defined and provided.') if 'GT' in kwargs: GT = kwargs['GT'] else: GT = 'H' SA_ISRS = dsm.j15_main(SAb, fb, zb, fp, zp, gam, phi, z0, OD, ND, INRES, fc, GT) elif method == 'Der Kiureghian (1981)': if 'm0' in kwargs: m0 = kwargs['m0'] else: m0 = 0. SA_ISRS = dsm.dk81_main(SAb, fb, zb, fp, zp, gam, phi, m0, z0, OD, ND, False) else: raise ValueError('Method {} is not supported.'.format(method)) rs = ResponseSpectrum(fb, SA_ISRS, xi=z0, label='ISRS {}% Dir {}'.format(100 * z0, OD)) # Write output to file outfil = Path(wd) / 'DirectS2S_Output_{}.txt'.format(OD) localtime = time.asctime(time.localtime(time.time())) head1 = ('# In-structure response spectrum computed by Qtools ' 'v. {} on {}'.format(config.version, localtime)) head2 = ('# Method = {}, direction = {}, damping ratio = {}, ' 'residual response {}'.format( method, OD, z0, 'included' if INRES else 'excluded')) head3 = '#' if method == 'Der Kiureghian (1981)': head3 = '# Secondary system mass = {}'.format(m0) header = '\n'.join([head1, head2, head3]) np.savetxt(outfil, np.array([rs.f, rs.sa]).T, header=header) # End timing stop = time.perf_counter() config.vprint('Time to execute (min): {:6.2f}'.format((stop - start) / 60)) return rs
def dk81_main(SAb, fb, zb, fp, zp, gam, phi, me, ze, OD, ND, INRES, Mi=None): """ Main function for the direct spectrum-to-spectrum method developed by Der Kiureghian et al. (1981). Parameters ---------- SAb : 3D NumPy array Spectral accelerations at the base of the primary structure (the input spectrum), with ``SAb[i,j,d]`` returning the spectral acceleration at frequency ``fb[i]`` and damping ratio ``zb[j]`` in direction `d`. fb : 1D NumPy array Frequencies corresponding to the 1st axis in `SAb`. zb : 1D NumPy array Damping ratios corresponding to the 2nd axis in `SAb`. fp : 1D NumPy array Modal frequencies of the primary system. zp : 1D NumPy array Damping ratios of the primary system. gam : 2D NumPy array Participation factors, with ``gam[i,d]`` returning the participation factor for mode `i` in direction `d`. phi : 1D NumPy array Modal displacements for DOF `k` (the point where the secondary system is attached). me : float Mass of secondary system. ze : float Damping ratio of secondary system. OD : int Output direction. ND : int Number of directions to take into account. INRES : bool If True, include the residual response. Otherwise ignore it. Mi : 1D NumPy array, optional Modal masses. The default is None, in which case it is assumed that all modes have been normalised to the mass matrix (in other words, it is assumed that ``Mi = np.ones_like(phi)``). Returns ------- 1D NumPy array The in-structure spectral accelerations at the frequencies defined in `fb`. """ with open('debug.txt', 'w') as outfil: # If Mi is None, then all modes have been normalised such that Mi = 1 if Mi is None: Mi = np.ones_like(phi) # Determine lengths of input arrays Nfb = len(fb) Np = len(fp) # Initialise arrays pkis = np.empty(Np + 1) pnis = np.empty(Np + 1) fps = np.empty(Np + 1) zps = np.empty(Np + 1) Mis = np.empty(Np + 1) Gis = np.empty(Np + 1) Rt = np.empty(Np + 1) SA_ISRS = np.zeros(Nfb) # Loop over directions for d in range(ND): config.vprint( 'Computing ISRS for excitation in direction {}'.format(d)) # DEBUG start outfil.write('Direction {}\n'.format(d)) # DEBUG end # Loop over frequency range: for i in range(Nfb): # Parameters bi = (fp**2 - fb[i]**2) / fb[i]**2 gi = me * phi**2 / Mi redInd = np.argwhere(np.isclose(bi, 0) & np.isclose(gi, 0)) if len(redInd) > 0: gi[redInd] = 1e-3 for i in redInd[:, 0]: config.vprint( 'WARNING: increased equipment mass in mode ' '{} to avoid numerical instability. New mass value: {}' .format(i, gi[i] * Mi[i] / phi[i]**2)) A1 = 1 + (bi + gi) / 2 - np.sqrt((1 + (bi + gi) / 2)**2 - (1 + bi)) A2 = 1 + (bi + gi) / 2 + np.sqrt((1 + (bi + gi) / 2)**2 - (1 + bi)) denom = np.where(bi < 0, A1 - 1, A2 - 1) ai = -1 / denom sag = np.sum(ai * gi) sa2g = np.sum(ai**2 * gi) ci = np.sqrt(1 + bi) l = np.argmin(np.fabs(bi)) rn = 1 if d == OD else 0 # New modal frequencies fps[0] = sqrt(1 + sag) * fb[i] fps[1:] = fp * np.where(bi < 0, np.sqrt(A1 / (1 + bi)), np.sqrt(A2 / (1 + bi))) # New modal damping ratios zps[0] = (np.sum(ci * ai**2 * gi * zp) + (1 + sag)**2 * ze) / (1 + sa2g) zps[1:] = (ci * zp + (1 - ai)**2 * gi * ze) / (ci * (1 + ai**2 * gi)) # New modal displacement (DOF k) pkis[0] = -sag pkis[1:] = phi pkis[l + 1] = sag - ai[l] * gi[l] - 1 / ai[l] # New modal displacement (DOF n+1) pnis[0] = 1 pnis[1:] = ai * pkis[1:] pnis[l + 1] = -1 # New modal masses Mis[0] = (1 + sa2g) * me Mis[1:] = (1 + ai**2 * gi) * Mi a2gl = ai[l]**2 * gi[l] Mis[l + 1] = (1 + a2gl * (1 + sa2g - a2gl)) * Mi[l] / (ai[l] * phi[l])**2 # New participation factors Gis[0] = -(np.sum(ai * phi * gam[:, d]) - rn) / (1 + sa2g) Gis[1:] = (gam[:, d] + ai * gi * rn / phi) / (1 + ai**2 * gi) Gis[l + 1] = -((ai[l] * phi[l] * gam[l, d] - ai[l]**2 * gi[l] * (np.sum(ai * phi * gam[:, d]) - ai[l] * phi[l] * gam[l, d] - rn)) / (1 + ai[l] * gi[l] * (1 + sa2g - a2gl))) # Compute spectral accelerations at new frequencies SAp = SAfun(SAb[:, :, d], fb, zb, fps, zps) # Modal responses Rt = Gis * (pnis - pkis) * SAp / fps**2 # Compute the double sum for direction d and add the result DS = doublesum(Rt, fps, zps) SA_ISRS[i] += fb[i]**4 * DS # Complete the SRSS combination and return the ISRS return np.sqrt(SA_ISRS)
def plotps(*args, **kwargs): """Function for plotting instances of class PowerSpectrum and FourierSpectrum. Parameters ---------- *args Any number of power spectra or Fourier spectra. All spectra must be of the same type. **kwargs Optional parameters (see below under **Other parameters**) Other parameters ---------------- style : str Plot style. See `Matplotlib style sheets reference <https:// matplotlib.org/stable/gallery/style_sheets/ style_sheets_reference.html>`_ for available styles. Default `default`. xscale : {'log', 'lin'} Specifies the scale on the x-axis (logarithmic or linear). Default 'lin'. legend : dict (or bool) The keys in this dictionary will be used to set the arguments in a call to :func:`matplotlib.pyplot.legend`. If this parameter is not provided, the legend will not be shown. This parameter can also be set to True to show the legend with default parameters ``{'loc': 'best'}`` filename : str If given, save plot to file using `filename` as file name. The file name should include the desired extension (e.g. 'png' or 'svg'), from which the file format will be determined as per :func:`matplotlib.pyplot.savefig`. dpi : int Dots per inch to use if plot is saved to file. Default None. right : float Sets the upper limit on the x-axis. left : float Sets the lower limit on the x-axis. top : float Sets the upper limit on the y-axis. bottom : float Sets the lower limit on the y-axis. grid : dict The keys in this dictionary will be used to set the arguments in a call to :func:`matplotlib.pyplot.grid`. Default: ``{'which': 'major', 'color': '0.75'}`` Notes ----- The line format can be set in the 'fmt' attribute of a spectrum. The line format is passed directly to :func:`matplotlib.pyplot.plot` as 'fmt'. Examples -------- See :func:`qtools.plotrs`. """ style = kwargs.get('style', 'default') xscale = kwargs.get('xscale', 'lin') if 'legend' in kwargs: show_legend = True legend = {'loc': 'best'} if isinstance(kwargs['legend'], dict): legend = kwargs['legend'] else: show_legend = False filename = kwargs.get('filename', '') dpi = kwargs.get('dpi', None) grid = kwargs.get('grid', {'which': 'major', 'color': '0.75'}) plt.style.use(style) # Get the axes of a new plot fig, ax = plt.subplots() # Set the appropriate type of plot if xscale == 'log': plot = ax.semilogx elif xscale == 'lin': plot = ax.plot ax.set_xlabel('Frequency [Hz]') # Check the validity of the arguments and set the labels on the y-axis if all([isinstance(ps, FourierSpectrum) for ps in args]): ax.set_ylabel('Fourier amplitude [{}]'.format(args[0].unit)) yaxis = 'X' elif all([isinstance(ps, PowerSpectrum) for ps in args]): ax.set_ylabel('Spectral power density [{}]'.format(args[0].unit)) yaxis = 'Wf' else: raise ValueError( 'The arguments provided to plotps are not all of the' ' same type (either FourierSpectrum or PowerSpectrum)') if len(set([ps.unit for ps in args])) > 1: config.vprint('WARNING: in plotps, it seems that some spectra have' ' different units.') # Create the plots for ps in args: if yaxis == 'X': y = ps.abs() else: y = ps.Wf if ps.fmt == '_default_': plot(ps.f, y, label=ps.label) else: plot(ps.f, y, ps.fmt, label=ps.label) # Set upper limit on x-axis if specified if 'right' in kwargs: ax.set_xlim(right=kwargs['right']) # Set lower limit on x-axis if specified if 'left' in kwargs: ax.set_xlim(left=kwargs['left']) # Set upper limit on y-axis if specified if 'top' in kwargs: ax.set_ylim(top=kwargs['top']) # Set lower limit on y-axis if specified if 'bottom' in kwargs: ax.set_ylim(bottom=kwargs['bottom']) if show_legend: ax.legend(**legend) ax.grid(**grid) if len(filename) > 0: plt.savefig(filename, dpi=dpi, bbox_inches='tight') plt.show()
def loadth(sfile, ordinate='a', dt=-1.0, factor=1.0, delimiter=None, comments='#', skiprows=0): """ Create a time history from a text file. Parameters ---------- sfile : str The path and name of the input file. ordinate : {'d', 'v', 'a'}, optional The physical type of the data imported from `sfile`, with 'd' = displacement, 'v' = velocity, 'a' = acceleration. dt : float, optional The time step. A positive value forces a constant time step, in which case `sfile` must not contain any time values. A negative value (e.g. ``dt = -1.0``) signifies that the time step is specified in the input file. factor : float, optional This argument can be used to factor the input data. Useful for converting from g units to |m/s2|. delimiter : str, optional The character used to separate values. The default is whitespace. comments : string or sequence of strings, optional The characters or list of characters used to indicate the start of a comment. None implies no comments. The default is '#'. skiprows : int, optional Skip the first `skiprows` lines. Default 0. Returns ------- th : an instance of class TimeHistory Notes ----- The Nyquist frequency is calculated as: .. math:: f_{Nyq} = \\frac{1}{2\\Delta t_{max}} where :math:`\Delta t_{max}` is the longest time step in the input file (when the function is called with `dt` < 0) or simply equal to `dt` (when the function is called with `dt` > 0). The time step can be specified in the input file by including a comment with the time step value. This comment must contain 'dt =' followed by the value of the time step in seconds. See example A below. The input file can have multiple data points per line. If the time step has been specified through `dt` or through a comment in the input file, it is assumed that the data in the input file are ordinates (displacements, velocities, accelerations) only; if the time step has not been specified, it is assumed that the data are given as pairs of time and ordinate. Examples -------- *Example A*. Fixed time step specified in a comment using white space as delimiter with 5 data points per line:: # dt = 0.005 a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 ... *Example B*. Time points defined directly in input file with 4 data points per line (resulting in 8 columns of data) using comma as delimiter:: t0, a0, t1, a1, t2, a2, t3, a3 t4, a4, t5, a5, t6, a6, t7, a7 ... """ if type(sfile) != str: raise TypeError('The first argument to loadth() must be a string.') if dt > 0.0: dt_fixed = True else: dt_fixed = False if not dt_fixed: # Look for the time step definition in the input file with open(sfile, 'r') as fil: for line in fil.readlines(): if 'dt =' in line: met = line.partition('=')[2].lstrip() try: dt = float(met) except: pass else: dt_fixed = True break # Now read the data from the specified file rawdata = np.loadtxt(sfile, delimiter=delimiter, comments=comments, skiprows=skiprows) ndat = np.size(rawdata) if dt_fixed: # Constant time step - the input file contains just ordinates fNyq = 1 / (2 * dt) time = np.linspace(0, dt * (ndat - 1), num=ndat) data = np.reshape(rawdata, ndat) else: # The input file is defined as pairs of (time, data) if (np.size(rawdata[0]) % 2 != 0): # There is an odd number of entries in the first row. # This format is not supported. raise IndexError( 'In loadth(): when the time points are defined in ' 'the input file, an odd number of columns in the input file is not supported.' ) time = np.reshape(rawdata[:, 0::2], np.size(rawdata[:, 0::2])) data = np.reshape(rawdata[:, 1::2], np.size(rawdata[:, 1::2])) ndat //= 2 # Find the highest frequency that can be represented by the time history # and check for fixed time step dt0 = time[1] - time[0] ddt = 0.0 fNyq = 1.E6 for i in range(ndat - 1): dt1 = time[i + 1] - time[i] ddt = max(ddt, abs(dt1 - dt0)) fNyq = min(1 / (2 * dt1), fNyq) # Check whether time step really is fixed if ddt < 1E-8: dt_fixed = True dt = dt0 # Factor input data if factor != 1.0: data *= factor # Create instance of TimeHistory th = TimeHistory(time, data, ordinate) th.setdt(dt, dt_fixed, round(fNyq, 2)) # Set label (removing the file path and the extension, if any) th.setLabel(Path(sfile).stem) # Output information config.vprint('Time history successfully read from file {}.'.format(sfile)) if th.dt_fixed: config.vprint( 'Constant time step = {:6.4f} seconds will be used.'.format(th.dt)) else: config.vprint('Variable time step will be used.') config.vprint('The duration is {:5.2f} seconds.'.format(th.Td)) config.vprint( 'The estimated Nyquist frequency of the time history is {:5.1f} Hz.'. format(th.fNyq)) if th.fNyq < 100.0: config.vprint( 'It is recommended to interpolate the time history using ' 'method interpft(k) with k >= {} for accuracy up to 100 Hz, ' 'depending on the frequency contents of the time history.'.format( ceil(100 / th.fNyq))) config.vprint('There are', th.ndat, 'points in the time history.') if ordinate == 'a': config.vprint('The PGA is {:4.2f} g.'.format(th.pga / 9.81)) config.vprint('------------------------------------------') return th
def interpft(self, k): """Interpolate the time history via a discrete Fourier transform of the data points in the time history. The result is a time history with a time step ``self.dt = self.dt/k``. Parameters ---------- k : int Divisor on the time step. If `k` is not an integer, it will be converted to one. Notes ----- The time history must have fixed time step. If ``self.dt_fixed is False``, no action is taken by invoking this method. The parameter `k` must be a positive integer. A value ``k = 1`` results in the same time history. This method uses :func:`numpy.fft.rfft` and :func:`numpy.fft.irfft` to perform the interpolation. Example ------- The following example shows how to use this method:: import copy from math import pi # Create a sample time hitory t0, dt = np.linspace(0,3*pi,20,retstep=True) g0 = np.sin(t0)**2*np.cos(t0) th0 = qt.arrayth(g0,time=t0,fmt='o') # Make a copy of th0 for later use th1 = copy.deepcopy(th0) th1.setLineFormat('-') # Interpolate and plot the two time histories th1.interpft(8) qt.plotth(th0,th1) """ k = int(k) if k < 1: raise ValueError( 'The parameter k must be greater than or equal to 1.') if self.dt_fixed: n = self.ndat m = k * n cn = 1 / n * np.fft.rfft(self.data) self.data = np.fft.irfft(m * cn, m)[0:(n - 1) * k + 1] t0 = self.time[0] Td = self.Td self.time, self.dt = np.linspace(t0, t0 + Td, (n - 1) * k + 1, retstep=True) self.ndat = (n - 1) * k + 1 self.k = 0 self.fNyq = 1 / (2 * self.dt) config.vprint('Interpolating time history {}.'.format(self.label)) config.vprint('The new time history has {} data points and a time ' 'step of {} sec.'.format(self.ndat, self.dt)) config.vprint('------------------------------------------') else: config.vprint('WARNING: calling interpft() on a time history with ' 'variable time step has no effect.')
def j15_main(SAb, fb, zb, fp, zp, gam, phi, z0, OD, ND, INRES, fc, GT): """ Main function for the direct spectrum-to-spectrum method developed by Jiang et al. (2015). Parameters ---------- SAb : 3D NumPy array Spectral accelerations at the base of the primary structure (the input spectrum), with ``SAb[i,j,d]`` returning the spectral acceleration at frequency ``fb[i]`` and damping ratio ``zb[j]`` in direction `d`. fb : 1D NumPy array Frequencies corresponding to the 1st axis in `SAb`. zb : 1D NumPy array Damping ratios corresponding to the 2nd axis in `SAb`. fp : 1D NumPy array Modal frequencies of the primary system. zp : 1D NumPy array Damping ratios of the primary system. gam : 2D NumPy array Participation factors, with ``gam[i,d]`` returning the participation factor for mode `i` in direction `d`. phi : 1D NumPy array Modal displacements for DOF `k` (the point where the secondary system is attached). z0 : float Damping ratio of secondary system. OD : int, {0, 1, 2} Output direction. ND : int, {1, 2, 3} Number of directions to take into account. INRES : bool If True, include the residual response. Otherwise ignore it. fc : tuple of 2 floats Corner frequencies. See the DirectS2S Documentation. GT : str Ground type ('H' for hard, 'S' for soft). Only relevant for vertical excitation. Default 'H'. Returns ------- 1D NumPy array The in-structure spectral accelerations at the frequencies defined in `fb`. """ Nfb = len(fb) # Initialise in-structure spectral acceleration array SA_ISRS = np.zeros_like(SAb[:, 0, 0]) # Loop over directions for d in range(ND): config.vprint( 'Computing ISRS for excitation in direction {}'.format(d)) # Compute spectral accelerations at structural frequencies and damping ratios SAp = SAfun(SAb[:, :, d], fb, zb, fp, zp) # Compute spectral acceleration at the frequency of the secondary system # (note: the effort is unnecessary when z0 equals one of the predefined # damping ratios in zb, but for general purpose, we need to to do this) SA0 = SAfun(SAb[:, :, d], fb, zb, fb, z0) # Compute peak displacements due to excitation in direction d u = gam[:, d] * phi # Add residual response if INRES and d == OD: # Append the residual modal displacement to the u array ur = 1 - np.sum(u) if ur < 0 or ur >= 1: config.vprint( 'WARNING: with ur = {}, the residual response is outside ' 'the expected range (0 <= ur < 1)'.format(ur)) config.vprint('The residual response is ignored (ur = 0)') ur = 0 else: config.vprint('The residual response is ur = {}'.format(ur)) u_r = np.append(u, ur) # Append appropriate values to other arrays # (note: the values appended to fp and zp are somewhat arbitrary) SAp_r = np.append(SAp, SAp[-1]) fp_r = np.append(fp, 200) zp_r = np.append(zp, z0) # Loop over frequency range: for i in range(Nfb): if INRES and d == OD: # Compute the response vector R = resp(SAp_r, SA0[i], u_r, fb[i], fp_r, z0, zp_r, fc, d, GT, INRES) # Compute the double sum for direction d and add the result DS = doublesum(R, fp_r, fb[i], zp_r, z0) else: # Compute the response vector R = resp(SAp, SA0[i], u, fb[i], fp, z0, zp, fc, d, GT, INRES) # Compute the double sum for direction d and add the result DS = doublesum(R, fp, fb[i], zp, z0) SA_ISRS[i] += DS # Complete the SRSS combination and return the ISRS return np.sqrt(SA_ISRS)