Source code for opstool.anlys._sec_analysis

from typing import Union
from warnings import warn

import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
from scipy.integrate import trapezoid

from ..utils import get_opensees_module
from ._smart_analyze import SmartAnalyze

ops = get_opensees_module()


[docs] class MomentCurvature: """Moment-Curvature Analysis for Fiber Section in OpenSeesPy. Parameters ---------- sec_tag : int, The previously defined section Tag. axial_force : float, optional Axial load, compression is negative, by default 0 """ def __init__(self, sec_tag: int, axial_force: float = 0) -> None: self.P = axial_force self.sec_tag = sec_tag self.phi, self.M, self.FiberData = None, None, None self.cycle_path = None # self.phi_cycle, self.M_cycle, self.FiberDataCycle = None, None, None
[docs] def analyze( self, axis: str = "y", max_phi: float = 0.5, incr_phi: float = 1e-4, limit_peak_ratio: float = 0.8, cycle_analyze: bool = False, smart_analyze: bool = True, debug: bool = False, ): """Performing Moment-Curvature Analysis. Parameters ---------- axis : str, optional, "y" or "z" The axis of the section to be analyzed, by default "y". max_phi : float, optional The maximum curvature to analyze, by default 0.5. incr_phi : float, optional Curvature analysis increment, by default 1e-4. limit_peak_ratio : float, optional A ratio of the moment intensity after the peak used to stop the analysis., by default 0.8, i.e., a 20% drop after peak. cycle_analyze : bool, optional Whether to perform cyclic analysis, by default False. smart_analyze : bool, optional Whether to use smart analysis options, by default True. debug: bool, optional Whether to use debug mode when smart analysis is True, by default, False. .. Note:: The termination of the analysis depends on whichever reaches `max_phi` or `post_peak_ratio` first. """ self.phi, self.M, self.FiberData = _analyze( sec_tag=self.sec_tag, P=self.P, axis=axis, max_phi=max_phi, incr_phi=incr_phi, stop_ratio=limit_peak_ratio, cycle=cycle_analyze, cycle_path=self.cycle_path, smart_analyze=smart_analyze, debug=debug, ) print("MomentCurvature: 🎉 Successfully finished! 🎉")
[docs] def set_cycle_path(self, max_phi: float, n_cycle: int = 20, n_hold: int = 1): """set a deformation cycle path. Parameters ---------- max_phi : float Peak of the path. n_cycle : int, optional Number of cycles, by default 20 n_hold : int, optional The number of repetitions for each cycle., by default 1 .. Note:: The total number of cycles is ``n_cycle * n_hold``. Returns ------- 1D Arraylike. Displacement path sequence """ max_phi = abs(max_phi) upper_envelope = np.linspace(0, max_phi, n_cycle)[1:] below_envelope = np.linspace(0, -max_phi, n_cycle)[1:] pattern = [0.0] for upper, below in zip(upper_envelope, below_envelope): upper, below = float(upper), float(below) for _ in range(n_hold): pattern.extend([upper, below]) pattern.append(0.0) # # mesh by step size # data = [0.0] # for i in range(len(pattern) - 1): # a, b = pattern[i], pattern[i + 1] # n = int(np.abs(b - a) / step_size) # n = 2 if n < 2 else n # data.extend(np.linspace(a, b, n)[1:]) self.cycle_path = pattern return pattern
[docs] def plot_M_phi(self, ax=None): """Plot the moment-curvature relationship. Parameters ------------ ax : matplotlib.axes.Axes, optional The axes to plot the moment-curvature relationship, by default None. """ lw = 1.0 if self.cycle_path else 2.5 if ax is None: _, ax = plt.subplots(1, 1, figsize=(10, 10 * 0.618)) ax.plot(self.phi, self.M, lw=lw, c="blue") ax.set_title("$M-\\phi$", fontsize=28) ax.set_xlabel("$\\phi$", fontsize=25) ax.set_ylabel("$M$", fontsize=25) plt.xticks(fontsize=15) plt.yticks(fontsize=15) for loc in ["bottom", "left", "right", "top"]: ax.spines[loc].set_linewidth(1.0) plt.gcf().subplots_adjust(bottom=0.15)
[docs] def plot_fiber_responses(self, return_ax: bool = False): """Plot the fiber responses. Parameters ----------- return_ax: bool, default=False If True, return the axes for the plot of matplotlib. """ fiber_data = self.FiberData matTags = np.unique(fiber_data[-1][:, 3]) _, axs = plt.subplots(len(matTags), 1, figsize=(8, 3 * len(matTags))) for mat, ax in zip(matTags, axs): idxs = np.argwhere(np.abs(fiber_data[-1][:, 3] - mat) < 1e-6) strain = fiber_data[:, idxs, -1] stress = fiber_data[:, idxs, -2] for i in range(len(idxs)): ax.plot( strain[:, i, :], stress[:, i, :], lw=1, ) ax.set_title(f"matTag = {mat:.0f}", fontsize=15) ax.tick_params(labelsize=12) ax.set_ylabel("stress", fontsize=16) ax.set_xlabel("strain", fontsize=16) plt.subplots_adjust(wspace=0.02, hspace=0.4) plt.tight_layout() if return_ax: return axs
[docs] def get_phi(self): """Get the curvature array. Returns ------- phi: 1D array-like Curvature array. """ return self.phi
[docs] def get_curvature(self): """Get the curvature array. Returns ------- phi: 1D array-like Curvature array. """ return self.get_phi()
[docs] def get_M(self): """Get the moment array. Returns ------- m: 1D array-like Moment array. """ return self.M
[docs] def get_moment(self): """Get the moment array. Returns ------- m: 1D array-like Moment array. """ return self.get_M()
[docs] def get_M_phi(self): """Get the moment and curvature array. Returns ------- (1D array-like, 1D array-like) (Curvature array, Moment array) """ return self.phi, self.M
[docs] def get_fiber_data(self) -> xr.DataArray: """All fiber data. Returns ------- FiberData: xr.DataArray All fiber data. "Steps" is the number of steps in the analysis. "Fibers" is the number of fibers in the section. "Properties" is the properties of the fibers, including "yloc", "zloc", "area", "mat", "stress", "strain". """ data = xr.DataArray( self.FiberData, coords={ "Steps": np.arange(len(self.FiberData)), "Fibers": np.arange(len(self.FiberData[0])), "Properties": ["yloc", "zloc", "area", "mat", "stress", "strain"], }, dims=("Steps", "Fibers", "Properties"), name="FiberData", ) return data
[docs] def get_limit_state( self, matTag: Union[list[int], int] = 1, threshold: Union[list[float], float] = 0.0, peak_drop: Union[float, bool] = False, ): """Get the curvature and moment corresponding to a certain limit state. Parameters ---------- matTag : Union[list[int], int] The OpenSeesPy material Tag used to determine the limit state., by default 1 threshold : Union[list[float], float] The ``strain threshold`` used to determine the limit state by material `matTag`, by default 0. The positive and negative signs are meaningful for tension and compression. .. Note:: If ``matTag`` is a list, the length of `matTag` and `threshold` should be the same. As long as any material reaches its corresponding threshold, it will be set to the limit state. peak_drop : Union[float, bool], optional, by default False. If True, A default 20% drop from the peak value of the moment will be used as the limit state; If float in [0, 1], this means that the ratio of ultimate strength to peak value is specified by this value, for example, peak_drop = 0.3, the ultimate strength = 0.7 * peak. `matTag` and `threshold` are not needed. .. Note:: When using ``peak_drop``, matTag and strain threshold will be ignored! Returns ------- (float, float) (Limit Curvature, Limit Moment) Examples --------- >>> sec = MomentCurvature(sec_tag=1) >>> sec.analyze(axis="y", max_phi=1.0, incr_phi=1e-4, limit_peak_ratio=0.8) >>> # Get the limit state by material Tag 1 and strain threshold 0.002 >>> sec.get_limit_state(matTag=1, threshold=0.002) >>> sec.get_limit_state(peak_drop=0.20) """ phi = self.phi M = self.M fiber_data = self.FiberData if peak_drop: ratio_ = 0.8 if peak_drop is True else 1 - peak_drop idx = np.argmax(M) au = np.argwhere(M[idx:] <= np.max(M) * ratio_) if au.size == 0: warn( f"Peak strength does not drop {1 - ratio_}, please increase target ductility ratio! " f"The last value is used as the limit state.", stacklevel=2, ) bu = -1 else: bu = np.min(au) + idx - 1 else: mat_tags = np.atleast_1d(matTag) thresholds = np.atleast_1d(threshold) if len(mat_tags) != len(thresholds): raise ValueError("The length of matTag and threshold should be the same!") # noqa: TRY003 bus = [] for matTag, threshold in zip(mat_tags, thresholds): matTag = int(matTag) idxu = np.argwhere(np.abs(fiber_data[-1][:, 3] - matTag) < 1e-6) eu = threshold if eu >= 0: strain = np.array([np.max(data[idxu, -1]) for data in fiber_data]) au = np.argwhere(strain >= eu) else: strain = np.array([np.min(data[idxu, -1]) for data in fiber_data]) au = np.argwhere(strain < eu) if len(au) == 0: warn( "The ultimate strain is not reached, please increase target ductility ratio! " "The last value is used as the limit state.", stacklevel=2, ) bu = len(phi) - 1 else: bu = np.min(au) bus.append(bu) bu = int(np.min(bus)) Phi_u = phi[bu] M_u = M[bu] return Phi_u, M_u
[docs] def bilinearize(self, phiy: float, My: float, phiu: float, plot: bool = False, ax=None): """Bilinear Approximation of Moment-Curvature Relation. Parameters ---------- phiy : float The initial yield curvature. My : float The initial yield moment. phiu : float The limit curvature. plot : bool, optional If True, plot the bilinear approximation, by default, False. ax : matplotlib.axes.Axes, optional The axes to plot the bilinear approximation, by default None. Returns ------- (float, float) (Equivalent Curvature, Equivalent Moment) """ phi = self.phi M = self.M bu = np.argmin(np.abs(phiu - phi)) Q = trapezoid(M[: bu + 1], phi[: bu + 1]) k = My / phiy Phi_eq = (k * phiu - np.sqrt((k * phiu) ** 2 - 2 * k * Q)) / k M_eq = k * Phi_eq M_new = np.insert(M[0 : bu + 1], 0, 0, axis=None) Phi_new = np.insert(phi[0 : bu + 1], 0, 0, axis=None) if plot: if ax is None: _, ax = plt.subplots(1, 1, figsize=(10, 10 * 0.618)) ax.plot(Phi_new, M_new, lw=1.5, c="#2529d8") ax.plot([0, phiy, Phi_eq, phiu], [0, My, M_eq, M_eq], lw=1.5, c="#de0f17") ax.plot( phiy, My, "o", ms=12, mec="black", mfc="#0099e5", label="Initial Yield ($\\phi_y$,$M_y$)", ) ax.plot( Phi_eq, M_eq, "*", ms=15, mec="black", mfc="#ff4c4c", label="Equivalent Yield ($\\phi_{{eq}}$,$M_{{eq}}$)", ) maxy = np.max(ax.get_yticks()) ax.vlines(phiu, 0, maxy, colors="#34bf49", linestyles="dashed", lw=0.75) txt = ( f"$\\phi_y$={phiy:.3E}, $M_y$={My:.3E}\n" f"$\\phi_{{eq}}$={Phi_eq:.3E}, $M_{{eq}}$={M_eq:.3E}\n" f"$\\phi_{{u}}$={phiu:.3E}, $M_{{u}}$={M[bu]:.3E}" ) ax.text( 0.5, 0.4, txt, fontsize=15, ha="center", va="bottom", transform=ax.transAxes, ) ax.set_title( "Moment-Curvature", fontsize=22, ) ax.set_xlabel("$\\phi$", fontsize=20) ax.set_ylabel("$M$", fontsize=20) plt.xticks(fontsize=15) plt.yticks(fontsize=15) # ax.set_xlim(0, np.max(ax.get_xticks())) ax.set_ylim(0, maxy) for loc in ["bottom", "left", "right", "top"]: ax.spines[loc].set_linewidth(1.0) ax.legend(loc="lower center", fontsize=15) plt.gcf().subplots_adjust(bottom=0.15) return Phi_eq, M_eq
def _create_model(sec_tag): ops.model("basic", "-ndm", 3, "-ndf", 6) ops.node(1, 0.0, 0.0, 0.0) ops.node(2, 0.0, 0.0, 0.0) ops.fix(1, 1, 1, 1, 1, 1, 1) ops.fix(2, 0, 1, 1, 1, 0, 0) ops.element("zeroLengthSection", 1, 1, 2, sec_tag) def _create_axial_resp(p): ops.timeSeries("Linear", 1) ops.pattern("Plain", 1, 1) ops.load(2, p, 0, 0, 0, 0, 0) # nd FX, FY, Fz, MX, MY, MZ ops.wipeAnalysis() ops.system("BandGeneral") ops.constraints("Plain") ops.numberer("Plain") ops.test("NormDispIncr", 1.0e-10, 10, 3) ops.algorithm("Newton") ops.integrator("LoadControl", 1 / 10) ops.analysis("Static") ops.analyze(10) ops.loadConst("-time", 0.0) def _analyze( sec_tag, P=0.0, axis="y", max_phi=0.5, incr_phi=1e-5, stop_ratio=0.8, smart_analyze=True, cycle=False, cycle_path=None, debug=False, ): _create_model(sec_tag) if P != 0: _create_axial_resp(P) _setup_pushover_load(axis) if cycle and cycle_path is not None: max_phi = np.max(np.abs(cycle_path)) protocol = _build_protocol(max_phi, incr_phi, cycle, cycle_path) dof = _get_dof(axis) results = _run_protocol_segments(protocol, dof, stop_ratio, smart_analyze, cycle_path, max_phi, debug) return results def _setup_pushover_load(axis): dof = _get_dof(axis) load = [0.0] * 6 load[dof - 1] = 1.0 ops.timeSeries("Linear", 2) ops.pattern("Plain", 2, 2) ops.load(2, *load) def _get_dof(axis): axis_map = {"y": 5, "z": 6} try: return axis_map[axis.lower()] except KeyError: print("Only supported axis = y or z!") def _build_protocol(max_phi, incr_phi, cycle, cycle_path): if cycle and cycle_path is not None: protocol = [] for i in range(len(cycle_path) - 1): diff = cycle_path[i + 1] - cycle_path[i] steps = max(1, int(abs(diff / incr_phi))) protocol += [diff / steps] * steps return protocol else: steps = max(1, int(abs(max_phi / incr_phi))) return [max_phi / steps] * steps def _run_protocol_segments(protocol, dof, stop_ratio, smart_analyze, cycle_path, max_phi, debug): M, PHI, RESP = [0.0], [0.0], [0.0] def record(): phi = ops.nodeDisp(2, dof) moment = ops.getLoadFactor(2) PHI.append(phi) M.append(moment) RESP.append(_get_fiber_sec_data(ele_tag=1)) return phi, moment def should_stop(): max_moment = np.max(np.abs(M)) flip = (M[-2] - M[-1]) * (PHI[-2] - PHI[-1]) < 0 decay = abs(M[-1]) <= max_moment * stop_ratio return (flip and decay) or abs(PHI[-1]) >= max_phi if smart_analyze: analysis = SmartAnalyze( analysis_type="Static", testType="NormDispIncr", testTol=1e-12, tryAddTestTimes=True, testIterTimes=100, testIterTimesMore=[250, 500, 1000], tryAlterAlgoTypes=False, algoTypes=[40], relaxation=0.5, minStep=1e-12, printPer=1e10, debugMode=debug, ) for seg in analysis.static_split(cycle_path if cycle_path else [max_phi], maxStep=protocol[0]): ok = analysis.StaticAnalyze(2, dof, seg) record() if should_stop(): analysis.close() break if ok < 0: analysis.close() raise RuntimeError("Analysis failed") # noqa: TRY003 else: for step in protocol: ops.integrator("DisplacementControl", 2, dof, step) ok = ops.analyze(1) record() if should_stop(): break if ok < 0: raise RuntimeError("Analysis failed") # noqa: TRY003 RESP[0] = RESP[1] * 0 return np.array(PHI), np.array(M), np.array(RESP) def _get_fiber_sec_data(ele_tag: int): fiber_data = ops.eleResponse(ele_tag, "section", "fiberData2") # From column 1 to 6: "yCoord", "zCoord", "area", 'mat', "stress", "strain" fiber_data = np.array(fiber_data).reshape((-1, 6)) return fiber_data def _get_center(ys, zs, areas): yo = np.sum(ys * areas) / np.sum(areas) zo = np.sum(zs * areas) / np.sum(areas) return yo, zo