diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index e3bf4f3..0000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Greetings - -on: [pull_request_target] - -jobs: - greeting: - name: Greet First-Time Contributors - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/first-interaction@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue_message: "Thank you for reporting your first issue to the open-source pyCUFSM project! A maintainer should be along shortly to review your issue." - pr_message: "Thank you so much for contributing to the open-source pyCUFSM project! Your contribution will help thousands of engineers work more efficiently and accurately.\n\nNow that you've created your first pull request here, please don't go away; take a look at the bottom of this page for the automated checks that should already be running. If they pass, great! If not, please click on 'Details' and see if you can fix the problem they've identified. Keep in mind that this repository uses the `black` autoformatter, `pylint` linter, and `mypy` type-checking; the most common problems can be fixed by making sure you've installed and run those systems. A maintainer should be along shortly to review your pull request and help get it added to pyCUFSM!" diff --git a/build_cython_ext.py b/build_cython_ext.py index 51c7b00..9478cb1 100644 --- a/build_cython_ext.py +++ b/build_cython_ext.py @@ -8,8 +8,8 @@ ext_modules = [ Extension( - "pycufsm.analysis_c", - sources=["pycufsm/analysis_c.pyx"], + "pycufsm.solve.analysis_c", + sources=["pycufsm/solve/analysis_c.pyx"], define_macros=define_macros, include_dirs=[numpy.get_include()], ), diff --git a/pycufsm/types.py b/pycufsm/_types.py similarity index 100% rename from pycufsm/types.py rename to pycufsm/_types.py diff --git a/pycufsm/examples/__init__.py b/pycufsm/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/examples/example_1.py b/pycufsm/examples/example_1.py index df2addd..1d23c2f 100644 --- a/pycufsm/examples/example_1.py +++ b/pycufsm/examples/example_1.py @@ -2,9 +2,9 @@ import numpy as np +from pycufsm._types import BC, GBT_Con, Sect_Props from pycufsm.fsm import strip -from pycufsm.preprocess import stress_gen -from pycufsm.types import BC, GBT_Con, Sect_Props +from pycufsm.pre.stresses import stress_gen # This example presents a very simple Cee section, # solved for pure compression, diff --git a/pycufsm/examples/example_1_new.py b/pycufsm/examples/example_1_new.py index 532464c..9c9dd5e 100644 --- a/pycufsm/examples/example_1_new.py +++ b/pycufsm/examples/example_1_new.py @@ -2,8 +2,8 @@ import numpy as np +from pycufsm._types import Analysis_Config, New_Element, Sect_Props from pycufsm.fsm import strip_new -from pycufsm.types import Analysis_Config, New_Element, Sect_Props # This example presents a very simple Cee section, # solved for pure compression, diff --git a/pycufsm/examples/example_2.py b/pycufsm/examples/example_2.py index 5e4ba86..7d9b164 100644 --- a/pycufsm/examples/example_2.py +++ b/pycufsm/examples/example_2.py @@ -2,9 +2,9 @@ import numpy as np +from pycufsm._types import BC, GBT_Con, Sect_Props from pycufsm.fsm import strip -from pycufsm.preprocess import stress_gen -from pycufsm.types import BC, GBT_Con, Sect_Props +from pycufsm.pre.stresses import stress_gen # This example presents a very simple Zed section, # solved for pure bending about the X-axis, diff --git a/pycufsm/fsm.py b/pycufsm/fsm.py index 2a3799c..8bce874 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -4,11 +4,7 @@ import numpy as np from scipy import linalg as spla # type: ignore -import pycufsm.cfsm -from pycufsm.analysis import analysis -from pycufsm.helpers import inputs_new_to_old, lengths_recommend -from pycufsm.preprocess import stress_gen, yield_mp -from pycufsm.types import ( +from pycufsm._types import ( BC, Analysis_Config, ArrayLike, @@ -22,6 +18,10 @@ Sect_Props, Yield_Force, ) +from pycufsm.helpers import inputs_new_to_old, lengths_recommend +from pycufsm.pre import cutwp, stresses +from pycufsm.solve import cfsm +from pycufsm.solve.analysis import analysis # from scipy.sparse.linalg import eigs # Originally developed for MATLAB by Benjamin Schafer PhD et al @@ -181,15 +181,15 @@ def strip( n_dist_modes, n_local_modes, dof_perm, - ] = pycufsm.cfsm.base_properties(nodes=nodes_base, elements=elements) - [r_x, r_z, r_yd, r_ys, r_ud] = pycufsm.cfsm.mode_constr( + ] = cfsm.base_properties(nodes=nodes_base, elements=elements) + [r_x, r_z, r_yd, r_ys, r_ud] = cfsm.mode_constr( nodes=nodes_base, elements=elements, node_props=node_props, main_nodes=main_nodes, meta_elements=meta_elements, ) - [d_y, n_global_modes] = pycufsm.cfsm.y_dofs( + [d_y, n_global_modes] = cfsm.y_dofs( nodes=nodes_base, elements=elements, main_nodes=main_nodes, @@ -215,7 +215,7 @@ def strip( # SET SWITCH AND PREPARE BASE VECTORS (R_matrix) FOR cFSM ANALYSIS if cfsm_analysis == 1: # generate natural base vectors for axial compression loading - b_v_l = pycufsm.cfsm.base_column( + b_v_l = cfsm.base_column( nodes_base=nodes_base, elements=elements, props=props, @@ -294,7 +294,7 @@ def strip( # size boundary conditions and user constraints for use in R_matrix format # d_constrained=r_user*d_unconstrained, d=nodal DOF vector (note by # BWS June 5 2006) - r_user = pycufsm.cfsm.constr_user(nodes=nodes, constraints=constraints, m_a=m_a) + r_user = cfsm.constr_user(nodes=nodes, constraints=constraints, m_a=m_a) R_u0_matrix = spla.null_space(r_user.conj().T) # Number of boundary conditions and user defined constraints = nu0 nu0 = len(R_u0_matrix[0]) @@ -302,7 +302,7 @@ def strip( # GENERATION OF cFSM CONSTRAINT MATRIX if cfsm_analysis == 1: # PERFORM ORTHOGONALIZATION IF GBT-LIKE MODES ARE ENFORCED - b_v = pycufsm.cfsm.base_update( + b_v = cfsm.base_update( GBT_con=GBT_con, b_v_l=b_v_l, length=length, @@ -318,7 +318,7 @@ def strip( ) # no normalization is enforced: 0: m # assign base vectors to constraints - b_v = pycufsm.cfsm.mode_select( + b_v = cfsm.mode_select( b_v=b_v, n_global_modes=n_global_modes, n_dist_modes=n_dist_modes, @@ -478,15 +478,15 @@ def strip_new( props: Dict[str, Dict[str, float]], nodes: ArrayLike, elements: Sequence[New_Element], - sect_props: Sect_Props, + yield_force: Optional[Yield_Force] = None, + forces: Optional[Forces] = None, + sect_props: Optional[Sect_Props] = None, lengths: Optional[Union[ArrayLike, set, Dict[float, ArrayLike]]] = None, node_props: Optional[Dict[Union[Literal["all"], int], New_Node_Props]] = None, springs: Optional[Sequence[New_Spring]] = None, constraints: Optional[Sequence[New_Constraint]] = None, analysis_config: Optional[Analysis_Config] = None, cfsm_config: Optional[Cfsm_Config] = None, - forces: Optional[Forces] = None, - yield_force: Optional[Yield_Force] = None, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Converts new format of inputs to old (original CUFSM) format @@ -517,7 +517,45 @@ def strip_new( Material thickness elements["mat"]: material name as a string - sect_props (Sect_Props): Section properties + yield_force (Optional[Yield_Force]): Single yield force to apply to section. + | { + | force: "Mxx"|"Myy"|"M11"|"M22"|"P", + | direction: "Pos"|"Neg"|"+"|"-", + | f_y: float, + | restrain: bool, + | offset: ArrayLike + | } + Either this or 'forces' must be set, or stresses must be set manually in `nodes`. + yield_force["restrain"]: + Note that 'restrain' only affects "Mxx" or "Myy" forces, and then only for sections + in which the principal axes are no aligned with the geometric axes (such as Z + sections) + yield_force["offset"]: + | `[x_offset, y_offset]` + Offset from the (0,0) coordinate used in calculating section properties and the + (0,0) coordinate used to define the nodal coordinates. This may commonly differ + by thickness/2, for example, if an external section properties calculator is used + forces (Optional[Forces]): Specific forces to apply to the section. + | { + | 'P': float, + | 'Mxx': float, + | 'Myy': float, + | 'M11': float, + | 'M22': float, + | 'restrain': bool, + | 'offset': ArrayLike + | } + forces["restrain"]: + Note that 'restrain' only affects "Mxx" or "Myy" forces, and then only for sections + in which the principal axes are no aligned with the geometric axes (such as Z + sections) + forces["offset"]: + | `[x_offset, y_offset]` + Offset from the (0,0) coordinate used in calculating section properties and the + (0,0) coordinate used to define the nodal coordinates. This may commonly differ + by thickness/2, for example, if an external section properties calculator is used + Either this or 'yield_force' must be set, or stresses must be set manually in `nodes`. + sect_props (Optional[Sect_Props]): Section properties | { | "A": float, | "cx": float, @@ -536,15 +574,18 @@ def strip_new( | "B2": float, | "wn": np.ndarray | } - Dictionary of section properties of cross-section - lengths (Union[ArrayLike, set, Dict[float, ArrayLike]): Half-wavelengths for analysis + Dictionary of section properties of cross-section. If None, section properties will be calculated using + the included CUTWP based on the geometry defined by `nodes` and `elements`, and the material properties + defined in `props`. + lengths (Optional[Union[ArrayLike, set, Dict[float, ArrayLike]]]): Half-wavelengths for analysis | `[length1, length2, ...]` | or | `{length1: List[int], length2: List[int], ...} If given as a simple array, then the longitudinal m term will be taken as `[1]` for each half-wavelength (which is normally what you want for a signature curve analysis). If given as a dictionary, then the longitudinal m terms must be set to an array with - appropriate values + appropriate values. If None, then lengths will be taken as the recommended lengths based on + the geometry of the section, and the longitudinal m terms will be taken as `[1]` for each length. node_props (Optional[Dict[Union[Literal["all", int], New_Node_Props]]): Nodal DOF inclusion | { | node_#|"all": { @@ -621,44 +662,7 @@ def strip_new( | "natural": natural basis | "modal_axial": modal basis, axial orthogonality | "modal_load": modal basis, load dependent orthogonality - yield_force (Optional[Yield_Force]): Single yield force to apply to section. - | { - | force: "Mxx"|"Myy"|"M11"|"M22"|"P", - | direction: "Pos"|"Neg"|"+"|"-", - | f_y: float, - | restrain: bool, - | offset: ArrayLike - | } - Either this or 'forces' must be set, or stresses must be set manually in `nodes`. - yield_force["restrain"]: - Note that 'restrain' only affects "Mxx" or "Myy" forces, and then only for sections - in which the principal axes are no aligned with the geometric axes (such as Z - sections) - yield_force["offset"]: - | `[x_offset, y_offset]` - Offset from the (0,0) coordinate used in calculating section properties and the - (0,0) coordinate used to define the nodal coordinates. This may commonly differ - by thickness/2, for example, if an external section properties calculator is used - forces (Optional[Forces]): Specific forces to apply to the section. - | { - | 'P': float, - | 'Mxx': float, - | 'Myy': float, - | 'M11': float, - | 'M22': float, - | 'restrain': bool, - | 'offset': ArrayLike - | } - forces["restrain"]: - Note that 'restrain' only affects "Mxx" or "Myy" forces, and then only for sections - in which the principal axes are no aligned with the geometric axes (such as Z - sections) - forces["offset"]: - | `[x_offset, y_offset]` - Offset from the (0,0) coordinate used in calculating section properties and the - (0,0) coordinate used to define the nodal coordinates. This may commonly differ - by thickness/2, for example, if an external section properties calculator is used - Either this or 'yield_force' must be set, or stresses must be set manually in `nodes`. + Returns: @@ -678,6 +682,12 @@ def strip_new( n_lengths = lengths if isinstance(lengths, int) else 50 lengths = [] + if sect_props is None: + sect_props = cutwp.prop2_new( + nodes=nodes, + elements=elements, + ) + ( props_old, nodes_old, @@ -704,7 +714,9 @@ def strip_new( if forces is None and yield_force is not None: restrained = yield_force["restrain"] if "restrain" in yield_force else False offset = yield_force["offset"] if "offset" in yield_force and yield_force["offset"] is not None else [0, 0] - all_yields = yield_mp(nodes=nodes_old, f_y=yield_force["f_y"], sect_props=sect_props, restrained=restrained) + all_yields = stresses.yield_mp( + nodes=nodes_old, f_y=yield_force["f_y"], sect_props=sect_props, restrained=restrained + ) forces = {"Mxx": 0, "Myy": 0, "M11": 0, "M22": 0, "P": 0, "restrain": restrained, "offset": offset} if yield_force["direction"] == "-" or yield_force["direction"] == "Neg": multiplier = -1 @@ -718,7 +730,7 @@ def strip_new( "Either 'forces' or 'yield_force' must be set, " + "or stress must be set manually for each node" ) if forces is not None: - nodes_stressed = stress_gen(nodes=nodes_old, forces=forces, sect_props=sect_props) + nodes_stressed = stresses.stress_gen(nodes=nodes_old, forces=forces, sect_props=sect_props) elif np.shape(nodes)[1] == 3: nodes_stressed = nodes_old else: @@ -866,7 +878,7 @@ def m_recommend( if load2 < load1 and load2 <= load3: local_minima.append(curve_signature[i + 1, 0]) - _, _, _, _, _, _, n_dist_modes, n_local_modes, _ = pycufsm.cfsm.base_properties(nodes=nodes, elements=elements) + _, _, _, _, _, _, n_dist_modes, n_local_modes, _ = cfsm.base_properties(nodes=nodes, elements=elements) n_global_modes = 4 n_other_modes = 2 * (len(nodes) - 1) diff --git a/pycufsm/helpers.py b/pycufsm/helpers.py index 81f1df1..9361f0e 100644 --- a/pycufsm/helpers.py +++ b/pycufsm/helpers.py @@ -4,7 +4,7 @@ import numpy as np from scipy.io import loadmat # type: ignore -from pycufsm.types import ( +from pycufsm._types import ( BC, Analysis_Config, ArrayLike, @@ -27,50 +27,6 @@ # change history, have been generally retained unaltered -def gammait2(phi: float, disp_local: np.ndarray) -> np.ndarray: - """transform local displacements into global displacements - - Args: - phi (float): angle - disp_local (np.ndarray): local displacements - - Returns: - np.ndarray: global displacements - - BWS, 1998 - """ - gamma = np.array([[np.cos(phi), 0, -np.sin(phi)], [0, 1, 0], [np.sin(phi), 0, np.cos(phi)]]) - return np.dot(np.linalg.inv(gamma), disp_local) # type: ignore - - -def shapef(links: int, disp: np.ndarray, length: float) -> np.ndarray: - """Apply displacements using shape function - - Args: - links (int): the number of additional line segments used to show the disp shape - disp (np.ndarray): the vector of nodal displacements - length (float): the actual length of the element - - Returns: - np.ndarray: applied displacements - - BWS, 1998 - """ - inc = 1 / (links) - x_disps = np.linspace(inc, 1 - inc, links - 1) - disp_local = np.zeros((3, len(x_disps))) - for i, x_d in enumerate(x_disps): - n_1 = 1 - 3 * x_d * x_d + 2 * x_d * x_d * x_d - n_2 = x_d * length * (1 - 2 * x_d + x_d**2) - n_3 = 3 * x_d**2 - 2 * x_d**3 - n_4 = x_d * length * (x_d**2 - x_d) - n_matrix = np.array( - [[(1 - x_d), 0, x_d, 0, 0, 0, 0, 0], [0, (1 - x_d), 0, x_d, 0, 0, 0, 0], [0, 0, 0, 0, n_1, n_2, n_3, n_4]] - ) - disp_local[:, i] = np.dot(n_matrix, disp).reshape(3) - return disp_local - - def lengths_recommend( nodes: np.ndarray, elements: np.ndarray, length_append: Optional[float] = None, n_lengths: int = 50 ) -> np.ndarray: diff --git a/pycufsm/Jupyter_Notebooks/Validation.ipynb b/pycufsm/jupyter_notebooks/Validation.ipynb similarity index 100% rename from pycufsm/Jupyter_Notebooks/Validation.ipynb rename to pycufsm/jupyter_notebooks/Validation.ipynb diff --git a/pycufsm/jupyter_notebooks/__init__.py b/pycufsm/jupyter_notebooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/Jupyter_Notebooks/gui_widgets.py b/pycufsm/jupyter_notebooks/gui_widgets.py similarity index 59% rename from pycufsm/Jupyter_Notebooks/gui_widgets.py rename to pycufsm/jupyter_notebooks/gui_widgets.py index 2d74a16..bf189e9 100644 --- a/pycufsm/Jupyter_Notebooks/gui_widgets.py +++ b/pycufsm/jupyter_notebooks/gui_widgets.py @@ -1,12 +1,17 @@ from typing import List, Tuple -import ipywidgets as widgets +import ipywidgets as widgets # pylint: disable=import-error import numpy as np -import pycufsm.plotters as crossect +from pycufsm.post import plotters def prevals() -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, List[int]]: + """Returns default material, node, element, spring, constraint, and flag values. + + Returns: + Tuple of (props, nodes, elements, springs, constraints, flag). + """ springs = np.array([]) constraints = np.array([]) flag = [1, 0, 0, 0, 1, 0, 0, 0, 0, 0] @@ -52,15 +57,61 @@ def prevals() -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarra # return tab -class Preprocess: - def wprops(self, props: np.ndarray, m: int) -> Tuple[widgets.VBox, widgets.Button, widgets.Button, list]: +class Preprocess: # pylint: disable=too-many-instance-attributes + """Widget-based preprocessor for cross-section geometry input in Jupyter notebooks.""" + + def __init__(self): + """Initializes all instance attributes to default values.""" + self.m = 0 + self.n = 0 + self.e = 0 + self.nodes = np.array([]) + self.elements = np.array([]) + self.props = np.array([]) + self.springs = np.array([]) + self.constraints = np.array([]) + self.flag = [] + self.mitems = [] + self.nitems = [] + self.eitems = [] + self.add_mat_btn = None + self.del_mat_btn = None + self.add_node_btn = None + self.del_node_btn = None + self.add_elem_btn = None + self.del_elem_btn = None + self.submit_btn = None + self.bc_widget = None + self.neigs = None + self.rowm = None + self.rnode = None + self.relem = None + self.rflag = None + self.r_bc = None + self.cs = None + self.page = None + self.row1 = None + self.row = None + self.row0 = None + self.b_c = None + self.flags = None + + def wprops(self, m: int) -> Tuple[widgets.VBox, widgets.Button, widgets.Button, list]: + """Builds the material properties widget panel. + + Args: + m: Number of materials. + + Returns: + Tuple of (panel widget, add button, delete button, material item list). + """ self.m = m matTitle = widgets.Label(value="Material Properties") mattext = ["mat#", "Ex", "Ey", "vx", "vy", "G"] prop: List[widgets.GridBox] = [[] for i in range(self.m)] self.mitems: List[widgets.FloatText] = [[] for i in range(self.m)] - self.ADDMAT = widgets.Button(description="Add Material", layout=widgets.Layout(border="solid 1px black")) - self.DELMAT = widgets.Button( + self.add_mat_btn = widgets.Button(description="Add Material", layout=widgets.Layout(border="solid 1px black")) + self.del_mat_btn = widgets.Button( description="Remove Material", layout=widgets.Layout(border="solid 1px black"), ) @@ -91,21 +142,30 @@ def wprops(self, props: np.ndarray, m: int) -> Tuple[widgets.VBox, widgets.Butto layout=widgets.Layout(grid_template_columns="repeat(6, 50px[col-start])"), ) matr = widgets.VBox([prop[j] for j in range(m)]) - brow = widgets.HBox([self.ADDMAT, self.DELMAT]) + brow = widgets.HBox([self.add_mat_btn, self.del_mat_btn]) self.rowm = widgets.VBox( [matTitle, matlabel, matr, brow], layout=widgets.Layout(border="solid 1px black", width="35%"), ) - return self.rowm, self.ADDMAT, self.DELMAT, self.mitems + return self.rowm, self.add_mat_btn, self.del_mat_btn, self.mitems def wnodes(self, nodes, n): + """Builds the nodes widget panel. + + Args: + nodes: Array of node data. + n: Number of nodes. + + Returns: + Tuple of (panel widget, add button, delete button, node item list). + """ self.nodes = nodes nodTitle = widgets.Label(value="Nodes") nodetext = ["Node#", "x", "y", "xdof", "zdof", "ydof", "qdof", "stress"] node = [[] for i in range(self.n)] self.nitems = [[] for i in range(self.n)] - self.ADDNODE = widgets.Button(description="Add Node", layout=widgets.Layout(border="solid 1px black")) - self.DELNODE = widgets.Button(description="Remove Node", layout=widgets.Layout(border="solid 1px black")) + self.add_node_btn = widgets.Button(description="Add Node", layout=widgets.Layout(border="solid 1px black")) + self.del_node_btn = widgets.Button(description="Remove Node", layout=widgets.Layout(border="solid 1px black")) nlabel = widgets.HBox([widgets.Label(value=nodetext[j], layout=widgets.Layout(width="55px")) for j in range(8)]) for i in range(self.n): if i < len(self.nodes): @@ -127,24 +187,33 @@ def wnodes(self, nodes, n): ) node[i] = widgets.HBox(self.nitems[i], layout=widgets.Layout(width="490px", height="30px")) noder0 = widgets.VBox([node[j] for j in range(n)]) - brow = widgets.HBox([self.ADDNODE, self.DELNODE]) + brow = widgets.HBox([self.add_node_btn, self.del_node_btn]) self.rnode = widgets.VBox( [nodTitle, nlabel, noder0, brow], layout=widgets.Layout(border="solid 1px black"), ) - return self.rnode, self.ADDNODE, self.DELNODE, self.nitems + return self.rnode, self.add_node_btn, self.del_node_btn, self.nitems def welems(self, elements, e): + """Builds the elements widget panel. + + Args: + elements: Array of element data. + e: Number of elements. + + Returns: + Tuple of (panel widget, add button, delete button, element item list). + """ self.elements = elements elTitle = widgets.Label(value="Elements") elemtext = ["Element#", "Nodei", "Nodej", "t", "Mat#"] elem = [[] for i in range(self.e)] self.eitems = [[] for i in range(self.e)] - self.ADDELEM = widgets.Button( + self.add_elem_btn = widgets.Button( description="Add Element", layout=widgets.Layout(border="solid 1px black", width="120px"), ) - self.DELELEM = widgets.Button( + self.del_elem_btn = widgets.Button( description="Remove Element", layout=widgets.Layout(border="solid 1px black", width="120px"), ) @@ -180,14 +249,22 @@ def welems(self, elements, e): ) elem[i] = widgets.GridBox(self.eitems[i], layout=widgets.Layout(height="30px")) elemr = widgets.VBox([elem[j] for j in range(e)]) - brow = widgets.HBox([self.ADDELEM, self.DELELEM], layout=widgets.Layout(width="30%")) + brow = widgets.HBox([self.add_elem_btn, self.del_elem_btn], layout=widgets.Layout(width="30%")) self.relem = widgets.VBox( [elTitle, elabel, elemr, brow], layout=widgets.Layout(border="solid 1px black"), ) - return self.relem, self.ADDELEM, self.DELELEM, self.eitems + return self.relem, self.add_elem_btn, self.del_elem_btn, self.eitems def wflag(self, flag): + """Builds the plot options widget panel. + + Args: + flag: List of flag values (0 or 1) for each plot option. + + Returns: + Tuple of (panel widget, submit button, flag checkbox list). + """ FlagTitle = widgets.Label(value="Plot Options") Flagtext = [ "node", @@ -202,29 +279,30 @@ def wflag(self, flag): "propaxis", ] self.flags = [] - for i in range(len(Flagtext)): - if flag[i] == 1: - flagv = True - else: - flagv = False + for i, label in enumerate(Flagtext): self.flags.append( widgets.Checkbox( - description=Flagtext[i], - value=flagv, + description=label, + value=flag[i] == 1, indent=False, layout=widgets.Layout(width="150px"), ) ) flag0 = widgets.VBox([self.flags[j] for j in range(10)]) - self.Submit = widgets.Button( + self.submit_btn = widgets.Button( description="Plot", layout=widgets.Layout(border="solid 1px black", width="150px"), ) - self.rflag = widgets.VBox([FlagTitle, flag0, self.Submit]) - return self.rflag, self.Submit, self.flags + self.rflag = widgets.VBox([FlagTitle, flag0, self.submit_btn]) + return self.rflag, self.submit_btn, self.flags + + def w_bound_cond(self): + """Builds the boundary conditions widget panel. - def wBound_Cond(self): - self.B_C = ["S-S", "C-C", "S-C", "C-F", "C-G"] + Returns: + Tuple of (panel widget, dropdown widget, eigenvalue count widget). + """ + self.b_c = ["S-S", "C-C", "S-C", "C-F", "C-G"] BCtext = [ "simple-simple", "clamped-clamped", @@ -238,26 +316,27 @@ def wBound_Cond(self): description="Boundary Conditions", ) self.neigs = widgets.IntText(value=20, description="Number of eignevalues") - self.rBC = widgets.VBox([self.bc_widget, self.neigs], layout=widgets.Layout(width="50%")) - return self.rBC, self.bc_widget, self.neigs + self.r_bc = widgets.VBox([self.bc_widget, self.neigs], layout=widgets.Layout(width="50%")) + return self.r_bc, self.bc_widget, self.neigs - def Assemble(self, page, rowm, rnode, relem, rflag, cs, rBC): + def assemble(self): + """Assembles all sub-panels into the main page widget and wires button callbacks.""" self.row1 = widgets.HBox([self.cs, self.rflag]) self.row = widgets.HBox([self.rnode, self.row1]) - self.row0 = widgets.HBox([self.rowm, self.rBC], layout=widgets.Layout(width="100%")) - self.ADDMAT.on_click(self.add_material) - self.ADDNODE.on_click(self.add_node) - self.ADDELEM.on_click(self.add_elem) - self.Submit.on_click(self.submit) - self.DELMAT.on_click(self.del_material) - self.DELNODE.on_click(self.del_node) - self.DELELEM.on_click(self.del_elem) + self.row0 = widgets.HBox([self.rowm, self.r_bc], layout=widgets.Layout(width="100%")) + self.add_mat_btn.on_click(self.add_material) + self.add_node_btn.on_click(self.add_node) + self.add_elem_btn.on_click(self.add_elem) + self.submit_btn.on_click(self.submit) + self.del_mat_btn.on_click(self.del_material) + self.del_node_btn.on_click(self.del_node) + self.del_elem_btn.on_click(self.del_elem) self.page.close() del self.page self.page = widgets.VBox([self.row0, self.row, self.relem]) - display(self.page) - def add_material(self, b): + def add_material(self): + """Adds a new material row to the materials panel.""" self.m = self.m + 1 self.props = [[] for i in range(self.m)] for i in range(self.m): @@ -267,26 +346,28 @@ def add_material(self, b): else: self.props[i].append(self.mitems[i][j].value) self.props = np.array(self.props) - self.rowm, self.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.props, self.m) + self.rowm, self.add_mat_btn, self.del_mat_btn, self.mitems = self.wprops(self.m) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() - def del_material(self, b): + def del_material(self): + """Removes the last material row from the materials panel.""" self.m = self.m - 1 self.props = [[] for i in range(self.m)] for i in range(self.m): for j in range(6): self.props[i].append(self.mitems[i][j].value) self.props = np.array(self.props) - self.rowm, self.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.props, self.m) + self.rowm, self.add_mat_btn, self.del_mat_btn, self.mitems = self.wprops(self.m) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() - def add_node(self, b): + def add_node(self): + """Adds a new node row to the nodes panel.""" self.n = self.n + 1 self.nodes = [[] for i in range(self.n)] for i in range(self.n): @@ -296,28 +377,30 @@ def add_node(self, b): else: self.nodes[i].append(self.nitems[i][j].value) self.nodes = np.array(self.nodes) - self.rnode, self.ADDNODE, self.DELNODE, self.nitems = self.wnodes(self.nodes, self.n) + self.rnode, self.add_node_btn, self.del_node_btn, self.nitems = self.wnodes(self.nodes, self.n) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() - def del_node(self, b): + def del_node(self): + """Removes the last node row from the nodes panel.""" self.n = self.n - 1 if self.n == len(self.eitems): - self.del_elem(b) + self.del_elem() self.nodes = [[] for i in range(self.n)] for i in range(self.n): for j in range(8): self.nodes[i].append(self.nitems[i][j].value) self.nodes = np.array(self.nodes) - self.rnode, self.ADDNODE, self.DELNODE, self.nitems = self.wnodes(self.nodes, self.n) + self.rnode, self.add_node_btn, self.del_node_btn, self.nitems = self.wnodes(self.nodes, self.n) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() - def add_elem(self, b): + def add_elem(self): + """Adds a new element row to the elements panel.""" self.e = self.e + 1 self.elements = [[] for i in range(self.e)] for i in range(self.e): @@ -327,26 +410,28 @@ def add_elem(self, b): else: self.elements[i].append(self.eitems[i][j].value) self.elements = np.array(self.elements) - self.relem, self.ADDELEM, self.DELELEM, self.eitems = self.welems(self.elements, self.e) + self.relem, self.add_elem_btn, self.del_elem_btn, self.eitems = self.welems(self.elements, self.e) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() - def del_elem(self, b): + def del_elem(self): + """Removes the last element row from the elements panel.""" self.e = self.e - 1 self.elements = [[] for i in range(self.e)] for i in range(self.e): for j in range(5): self.elements[i].append(self.eitems[i][j].value) self.elements = np.array(self.elements) - self.relem, self.ADDELEM, self.DELELEM, self.eitems = self.welems(self.elements, self.e) + self.relem, self.add_elem_btn, self.del_elem_btn, self.eitems = self.welems(self.elements, self.e) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() - def submit(self, b): + def submit(self): + """Reads all widget values, updates stored arrays, and redraws the cross-section.""" self.props = [[] for i in range(self.m)] for i in range(self.m): for j in range(6): @@ -362,19 +447,31 @@ def submit(self, b): for j in range(5): self.elements[i].append(self.eitems[i][j].value) self.elements = np.array(self.elements) - for i in range(len(self.flags)): - if self.flags[i].value == True: - self.flag[i] = 1 - else: - self.flag[i] = 0 - - self.rflag, self.Submit, self.flags = self.wflag(self.flag) + for i, flag_widget in enumerate(self.flags): + self.flag[i] = 1 if flag_widget.value else 0 + self.rflag, self.submit_btn, self.flags = self.wflag(self.flag) self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + self.assemble() def run(self, m, n, e, props, nodes, elements, springs, constraints, flag): + """Initializes and displays the full preprocessor UI. + + Args: + m: Number of materials. + n: Number of nodes. + e: Number of elements. + props: Array of material properties. + nodes: Array of node data. + elements: Array of element data. + springs: Array of spring data. + constraints: Array of constraint data. + flag: List of plot option flags. + + Returns: + Tuple of updated (props, nodes, elements). + """ self.m = m self.n = n self.e = e @@ -384,14 +481,14 @@ def run(self, m, n, e, props, nodes, elements, springs, constraints, flag): self.springs = springs self.constraints = constraints self.flag = flag - self.rowm, self.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.props, len(self.props)) - self.rnode, self.ADDNODE, self.DELNODE, self.nitems = self.wnodes(self.nodes, len(self.nodes)) - self.relem, self.ADDELEM, self.DELELEM, self.eitems = self.welems(self.elements, len(self.elements)) - self.rflag, self.Submit, self.flags = self.wflag(self.flag) - self.rBC, self.bc_widget, self.neigs = self.wBound_Cond() + self.rowm, self.add_mat_btn, self.del_mat_btn, self.mitems = self.wprops(len(self.props)) + self.rnode, self.add_node_btn, self.del_node_btn, self.nitems = self.wnodes(self.nodes, len(self.nodes)) + self.relem, self.add_elem_btn, self.del_elem_btn, self.eitems = self.welems(self.elements, len(self.elements)) + self.rflag, self.submit_btn, self.flags = self.wflag(self.flag) + self.r_bc, self.bc_widget, self.neigs = self.w_bound_cond() self.cs = widgets.Output() with self.cs: - crossect.crossect(self.nodes, self.elements, self.springs, self.constraints, self.flag) + plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) self.page = widgets.FloatText(value=1) - self.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) + self.assemble() return self.props, self.nodes, self.elements diff --git a/pycufsm/Jupyter_Notebooks/pyCUFSM.ipynb b/pycufsm/jupyter_notebooks/pyCUFSM.ipynb similarity index 100% rename from pycufsm/Jupyter_Notebooks/pyCUFSM.ipynb rename to pycufsm/jupyter_notebooks/pyCUFSM.ipynb diff --git a/pycufsm/Jupyter_Notebooks/pyCUFSM_GUI.ipynb b/pycufsm/jupyter_notebooks/pyCUFSM_GUI.ipynb similarity index 100% rename from pycufsm/Jupyter_Notebooks/pyCUFSM_GUI.ipynb rename to pycufsm/jupyter_notebooks/pyCUFSM_GUI.ipynb diff --git a/pycufsm/Jupyter_Notebooks/pyCUFSM_load_mat.ipynb b/pycufsm/jupyter_notebooks/pyCUFSM_load_mat.ipynb similarity index 100% rename from pycufsm/Jupyter_Notebooks/pyCUFSM_load_mat.ipynb rename to pycufsm/jupyter_notebooks/pyCUFSM_load_mat.ipynb diff --git a/pycufsm/post/__init__.py b/pycufsm/post/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/post/helpers.py b/pycufsm/post/helpers.py new file mode 100644 index 0000000..558a1df --- /dev/null +++ b/pycufsm/post/helpers.py @@ -0,0 +1,45 @@ +import numpy as np + + +def gammait2(phi: float, disp_local: np.ndarray) -> np.ndarray: + """transform local displacements into global displacements + + Args: + phi (float): angle + disp_local (np.ndarray): local displacements + + Returns: + np.ndarray: global displacements + + BWS, 1998 + """ + gamma = np.array([[np.cos(phi), 0, -np.sin(phi)], [0, 1, 0], [np.sin(phi), 0, np.cos(phi)]]) + return np.dot(np.linalg.inv(gamma), disp_local) # type: ignore + + +def shapef(links: int, disp: np.ndarray, length: float) -> np.ndarray: + """Apply displacements using shape function + + Args: + links (int): the number of additional line segments used to show the disp shape + disp (np.ndarray): the vector of nodal displacements + length (float): the actual length of the element + + Returns: + np.ndarray: applied displacements + + BWS, 1998 + """ + inc = 1 / (links) + x_disps = np.linspace(inc, 1 - inc, links - 1) + disp_local = np.zeros((3, len(x_disps))) + for i, x_d in enumerate(x_disps): + n_1 = 1 - 3 * x_d * x_d + 2 * x_d * x_d * x_d + n_2 = x_d * length * (1 - 2 * x_d + x_d**2) + n_3 = 3 * x_d**2 - 2 * x_d**3 + n_4 = x_d * length * (x_d**2 - x_d) + n_matrix = np.array( + [[(1 - x_d), 0, x_d, 0, 0, 0, 0, 0], [0, (1 - x_d), 0, x_d, 0, 0, 0, 0], [0, 0, 0, 0, n_1, n_2, n_3, n_4]] + ) + disp_local[:, i] = np.dot(n_matrix, disp).reshape(3) + return disp_local diff --git a/pycufsm/plotters.py b/pycufsm/post/plotters.py similarity index 99% rename from pycufsm/plotters.py rename to pycufsm/post/plotters.py index 1e43d7a..c4895bc 100644 --- a/pycufsm/plotters.py +++ b/pycufsm/post/plotters.py @@ -5,8 +5,8 @@ from matplotlib.collections import PatchCollection from matplotlib.patches import Polygon -from pycufsm import helpers -from pycufsm.analysis import analysis +from pycufsm.post import helpers +from pycufsm.solve.analysis import analysis def cross_sect( diff --git a/pycufsm/pre/__init__.py b/pycufsm/pre/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/cutwp.py b/pycufsm/pre/cutwp.py similarity index 99% rename from pycufsm/cutwp.py rename to pycufsm/pre/cutwp.py index 6377377..7666d17 100644 --- a/pycufsm/cutwp.py +++ b/pycufsm/pre/cutwp.py @@ -2,7 +2,7 @@ import numpy as np -from pycufsm.types import ArrayLike, New_Element, Sect_Props +from pycufsm._types import ArrayLike, New_Element, Sect_Props def prop2_new(nodes: ArrayLike, elements: Sequence[New_Element]) -> Sect_Props: diff --git a/pycufsm/geometry.py b/pycufsm/pre/geometry.py similarity index 99% rename from pycufsm/geometry.py rename to pycufsm/pre/geometry.py index 19bbd19..f078c21 100644 --- a/pycufsm/geometry.py +++ b/pycufsm/pre/geometry.py @@ -3,7 +3,9 @@ import numpy as np -from pycufsm.types import ArrayLike +from pycufsm._types import ArrayLike + +# Note that this file is entirely new to pyCUFSM, and similar functions will NOT be found in the MATLAB version of CUFSM ANGLE_TOLERANCE = np.radians(5) # Tolerance for detecting a corner in radians diff --git a/pycufsm/pre/stresses.py b/pycufsm/pre/stresses.py new file mode 100644 index 0000000..c7178d0 --- /dev/null +++ b/pycufsm/pre/stresses.py @@ -0,0 +1,144 @@ +from typing import Union + +import numpy as np +from scipy import linalg as spla # type: ignore + +from pycufsm._types import Forces, Sect_Props + +# Originally developed for MATLAB by Benjamin Schafer PhD et al +# Ported to Python by Brooks Smith MEng, PE +# +# Each function within this file was originally its own separate file. +# Original MATLAB comments, especially those retaining to authorship or +# change history, have been generally retained unaltered + + +def yield_mp(nodes: np.ndarray, f_y: float, sect_props: Sect_Props, restrained: bool = False) -> Forces: + """Determine yield strengths in bending and axial loading + + Args: + nodes (np.ndarray): _description_ + f_y (float): _description_ + sect_props (Sect_Props): _description_ + restrained (bool, optional): _description_. Defaults to False. + + Returns: + forces (Forces): Yield bending and axial strengths + {Py,Mxx_y,Mzz_y,M11_y,M22_y} + + BWS, Aug 2000 + BWS, May 2019 trap nan when flat plate or other properites are zero + """ + f_yield: Forces = {"P": 0, "Mxx": 0, "Myy": 0, "M11": 0, "M22": 0, "restrain": restrained, "offset": [0, 0]} + + f_yield["P"] = f_y * sect_props["A"] + + # account for the possibility of restrained bending vs. unrestrained bending + if restrained is False: + sect_props["Ixy"] = 0 + # Calculate stress at every point based on m_xx=1 + m_xx = 1 + m_yy = 0 + stress1: np.ndarray = np.zeros((1, len(nodes))) + stress1 = stress1 - ( + (m_yy * sect_props["Ixx"] + m_xx * sect_props["Ixy"]) * (nodes[:, 1] - sect_props["cx"]) + - (m_yy * sect_props["Ixy"] + m_xx * sect_props["Iyy"]) * (nodes[:, 2] - sect_props["cy"]) + ) / (sect_props["Iyy"] * sect_props["Ixx"] - sect_props["Ixy"] ** 2) + if np.max(abs(stress1)) == 0: + f_yield["Mxx"] = 0 + else: + f_yield["Mxx"] = f_y / np.max(abs(stress1)) + # Calculate stress at every point based on m_yy=1 + m_xx = 0 + m_yy = 1 + stress1 = np.zeros((1, len(nodes))) + stress1 = stress1 - ( + (m_yy * sect_props["Ixx"] + m_xx * sect_props["Ixy"]) * (nodes[:, 1] - sect_props["cx"]) + - (m_yy * sect_props["Ixy"] + m_xx * sect_props["Iyy"]) * (nodes[:, 2] - sect_props["cy"]) + ) / (sect_props["Iyy"] * sect_props["Ixx"] - sect_props["Ixy"] ** 2) + if np.max(abs(stress1)) == 0: + f_yield["Myy"] = 0 + else: + f_yield["Myy"] = f_y / np.max(abs(stress1)) + # %M11_y, M22_y + # %transform coordinates of nodes into principal coordinates + phi = sect_props["phi"] + transform = np.array([[np.cos(phi), -np.sin(phi)], [np.sin(phi), np.cos(phi)]]) + cent_coord = np.array([nodes[:, 1] - sect_props["cx"], nodes[:, 2] - sect_props["cy"]]) + prin_coord = np.transpose(spla.inv(transform) @ cent_coord) + f_yield["M11"] = 1 + stress1 = np.zeros((1, len(nodes))) + stress1 = stress1 - f_yield["M11"] * prin_coord[:, 1] / sect_props["I11"] + if np.max(abs(stress1)) == 0: + f_yield["M11"] = 0 + else: + f_yield["M11"] = f_y / np.max(abs(stress1)) * f_yield["M11"] + + f_yield["M22"] = 1 + stress1 = np.zeros((1, len(nodes))) + stress1 = stress1 - f_yield["M22"] * prin_coord[:, 0] / sect_props["I22"] + if np.max(abs(stress1)) == 0: + f_yield["M22"] = 0 + else: + f_yield["M22"] = f_y / np.max(abs(stress1)) * f_yield["M22"] + return f_yield + + +def stress_gen( + nodes: np.ndarray, + forces: Forces, + sect_props: Sect_Props, + restrained: bool = False, + offset_basis: Union[int, list] = 0, +) -> np.ndarray: + """Generates stresses on nodes based upon applied loadings + + Args: + nodes (np.ndarray): _description_ + forces (Forces): _description_ + sect_props (Sect_Props): _description_ + restrained (bool, optional): _description_. Defaults to False. + offset_basis (Union[int, list], optional): offset_basis compensates for section properties + that are based upon coordinate + [0, 0] being something other than the centreline of elements. For example, + if section properties are based upon the outer perimeter, then + offset_basis=[-thickness/2, -thickness/2]. Defaults to 0. + + Returns: + np.ndarray: _description_ + + BWS, 1998 + B Smith, Aug 2020 + """ + if "restrain" in forces: + restrained = forces["restrain"] + if "offset" in forces and forces["offset"] is not None: + offset_basis = list(forces["offset"]) + if isinstance(offset_basis, (float, int)): + offset_basis = [offset_basis, offset_basis] + + stress: np.ndarray = np.zeros((1, len(nodes))) + stress = stress + forces["P"] / sect_props["A"] + if restrained: + stress = stress - ( + (forces["Myy"] * sect_props["Ixx"]) * (nodes[:, 1] - sect_props["cx"] - offset_basis[0]) + - (forces["Mxx"] * sect_props["Iyy"]) * (nodes[:, 2] - sect_props["cy"] - offset_basis[1]) + ) / (sect_props["Iyy"] * sect_props["Ixx"]) + else: + stress = stress - ( + (forces["Myy"] * sect_props["Ixx"] + forces["Mxx"] * sect_props["Ixy"]) + * (nodes[:, 1] - sect_props["cx"] - offset_basis[0]) + - (forces["Myy"] * sect_props["Ixy"] + forces["Mxx"] * sect_props["Iyy"]) + * (nodes[:, 2] - sect_props["cy"] - offset_basis[1]) + ) / (sect_props["Iyy"] * sect_props["Ixx"] - sect_props["Ixy"] ** 2) + phi = sect_props["phi"] * np.pi / 180 + transform = np.array([[np.cos(phi), -np.sin(phi)], [np.sin(phi), np.cos(phi)]]) + cent_coord = np.array( + [nodes[:, 1] - sect_props["cx"] - offset_basis[0], nodes[:, 2] - sect_props["cy"] - offset_basis[1]] + ) + prin_coord = np.transpose(spla.inv(transform) @ cent_coord) + stress = stress - forces["M11"] * prin_coord[:, 1] / sect_props["I11"] + + stress = stress - forces["M22"] * prin_coord[:, 0] / sect_props["I22"] + nodes[:, 7] = stress.flatten() + return nodes diff --git a/pycufsm/preprocess.py b/pycufsm/pre/template.py similarity index 72% rename from pycufsm/preprocess.py rename to pycufsm/pre/template.py index 7bc67cd..7a90f67 100644 --- a/pycufsm/preprocess.py +++ b/pycufsm/pre/template.py @@ -1,9 +1,11 @@ -from typing import Optional, Tuple, Union +from typing import Optional, Tuple import numpy as np -from scipy import linalg as spla # type: ignore -from pycufsm.types import Forces, Sect_Geom, Sect_Props +from pycufsm._types import Sect_Geom + +# from scipy import linalg as spla # type: ignore + # Originally developed for MATLAB by Benjamin Schafer PhD et al # Ported to Python by Brooks Smith MEng, PE @@ -340,137 +342,6 @@ def template_out_to_in(sect: Sect_Geom) -> list: return [depth, b_1, l_1, b_2, l_2, rad, thick] -def yield_mp(nodes: np.ndarray, f_y: float, sect_props: Sect_Props, restrained: bool = False) -> Forces: - """Determine yield strengths in bending and axial loading - - Args: - nodes (np.ndarray): _description_ - f_y (float): _description_ - sect_props (Sect_Props): _description_ - restrained (bool, optional): _description_. Defaults to False. - - Returns: - forces (Forces): Yield bending and axial strengths - {Py,Mxx_y,Mzz_y,M11_y,M22_y} - - BWS, Aug 2000 - BWS, May 2019 trap nan when flat plate or other properites are zero - """ - f_yield: Forces = {"P": 0, "Mxx": 0, "Myy": 0, "M11": 0, "M22": 0, "restrain": restrained, "offset": [0, 0]} - - f_yield["P"] = f_y * sect_props["A"] - - # account for the possibility of restrained bending vs. unrestrained bending - if restrained is False: - sect_props["Ixy"] = 0 - # Calculate stress at every point based on m_xx=1 - m_xx = 1 - m_yy = 0 - stress1: np.ndarray = np.zeros((1, len(nodes))) - stress1 = stress1 - ( - (m_yy * sect_props["Ixx"] + m_xx * sect_props["Ixy"]) * (nodes[:, 1] - sect_props["cx"]) - - (m_yy * sect_props["Ixy"] + m_xx * sect_props["Iyy"]) * (nodes[:, 2] - sect_props["cy"]) - ) / (sect_props["Iyy"] * sect_props["Ixx"] - sect_props["Ixy"] ** 2) - if np.max(abs(stress1)) == 0: - f_yield["Mxx"] = 0 - else: - f_yield["Mxx"] = f_y / np.max(abs(stress1)) - # Calculate stress at every point based on m_yy=1 - m_xx = 0 - m_yy = 1 - stress1 = np.zeros((1, len(nodes))) - stress1 = stress1 - ( - (m_yy * sect_props["Ixx"] + m_xx * sect_props["Ixy"]) * (nodes[:, 1] - sect_props["cx"]) - - (m_yy * sect_props["Ixy"] + m_xx * sect_props["Iyy"]) * (nodes[:, 2] - sect_props["cy"]) - ) / (sect_props["Iyy"] * sect_props["Ixx"] - sect_props["Ixy"] ** 2) - if np.max(abs(stress1)) == 0: - f_yield["Myy"] = 0 - else: - f_yield["Myy"] = f_y / np.max(abs(stress1)) - # %M11_y, M22_y - # %transform coordinates of nodes into principal coordinates - phi = sect_props["phi"] - transform = np.array([[np.cos(phi), -np.sin(phi)], [np.sin(phi), np.cos(phi)]]) - cent_coord = np.array([nodes[:, 1] - sect_props["cx"], nodes[:, 2] - sect_props["cy"]]) - prin_coord = np.transpose(spla.inv(transform) @ cent_coord) - f_yield["M11"] = 1 - stress1 = np.zeros((1, len(nodes))) - stress1 = stress1 - f_yield["M11"] * prin_coord[:, 1] / sect_props["I11"] - if np.max(abs(stress1)) == 0: - f_yield["M11"] = 0 - else: - f_yield["M11"] = f_y / np.max(abs(stress1)) * f_yield["M11"] - - f_yield["M22"] = 1 - stress1 = np.zeros((1, len(nodes))) - stress1 = stress1 - f_yield["M22"] * prin_coord[:, 0] / sect_props["I22"] - if np.max(abs(stress1)) == 0: - f_yield["M22"] = 0 - else: - f_yield["M22"] = f_y / np.max(abs(stress1)) * f_yield["M22"] - return f_yield - - -def stress_gen( - nodes: np.ndarray, - forces: Forces, - sect_props: Sect_Props, - restrained: bool = False, - offset_basis: Union[int, list] = 0, -) -> np.ndarray: - """Generates stresses on nodes based upon applied loadings - - Args: - nodes (np.ndarray): _description_ - forces (Forces): _description_ - sect_props (Sect_Props): _description_ - restrained (bool, optional): _description_. Defaults to False. - offset_basis (Union[int, list], optional): offset_basis compensates for section properties - that are based upon coordinate - [0, 0] being something other than the centreline of elements. For example, - if section properties are based upon the outer perimeter, then - offset_basis=[-thickness/2, -thickness/2]. Defaults to 0. - - Returns: - np.ndarray: _description_ - - BWS, 1998 - B Smith, Aug 2020 - """ - if "restrain" in forces: - restrained = forces["restrain"] - if "offset" in forces and forces["offset"] is not None: - offset_basis = list(forces["offset"]) - if isinstance(offset_basis, (float, int)): - offset_basis = [offset_basis, offset_basis] - - stress: np.ndarray = np.zeros((1, len(nodes))) - stress = stress + forces["P"] / sect_props["A"] - if restrained: - stress = stress - ( - (forces["Myy"] * sect_props["Ixx"]) * (nodes[:, 1] - sect_props["cx"] - offset_basis[0]) - - (forces["Mxx"] * sect_props["Iyy"]) * (nodes[:, 2] - sect_props["cy"] - offset_basis[1]) - ) / (sect_props["Iyy"] * sect_props["Ixx"]) - else: - stress = stress - ( - (forces["Myy"] * sect_props["Ixx"] + forces["Mxx"] * sect_props["Ixy"]) - * (nodes[:, 1] - sect_props["cx"] - offset_basis[0]) - - (forces["Myy"] * sect_props["Ixy"] + forces["Mxx"] * sect_props["Iyy"]) - * (nodes[:, 2] - sect_props["cy"] - offset_basis[1]) - ) / (sect_props["Iyy"] * sect_props["Ixx"] - sect_props["Ixy"] ** 2) - phi = sect_props["phi"] * np.pi / 180 - transform = np.array([[np.cos(phi), -np.sin(phi)], [np.sin(phi), np.cos(phi)]]) - cent_coord = np.array( - [nodes[:, 1] - sect_props["cx"] - offset_basis[0], nodes[:, 2] - sect_props["cy"] - offset_basis[1]] - ) - prin_coord = np.transpose(spla.inv(transform) @ cent_coord) - stress = stress - forces["M11"] * prin_coord[:, 1] / sect_props["I11"] - - stress = stress - forces["M22"] * prin_coord[:, 0] / sect_props["I22"] - nodes[:, 7] = stress.flatten() - return nodes - - def doubler(nodes: np.ndarray, elements: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """A function to double the number of elements to help out the discretization of the member somewhat. diff --git a/pycufsm/solve/__init__.py b/pycufsm/solve/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/analysis.py b/pycufsm/solve/analysis.py similarity index 63% rename from pycufsm/analysis.py rename to pycufsm/solve/analysis.py index 3622217..8e0203a 100644 --- a/pycufsm/analysis.py +++ b/pycufsm/solve/analysis.py @@ -3,7 +3,7 @@ try: try: # if the cython module is already built, use it - import pycufsm.analysis_c as analysis # pylint:disable=unused-import + import pycufsm.solve.analysis_c as analysis # pylint:disable=unused-import except ImportError: # if we can build the cython module, build and use it @@ -15,8 +15,8 @@ reload_support=True, setup_args={"include_dirs": np.get_include()}, ) - import pycufsm.analysis_c as analysis # type: ignore # pylint:disable=ungrouped-imports,unused-import + import pycufsm.solve.analysis_c as analysis # type: ignore # pylint:disable=ungrouped-imports,unused-import except ImportError: # if cython just fails entirely, then use the pure python module - import pycufsm.analysis_p as analysis # pylint:disable=unused-import + import pycufsm.solve.analysis_p as analysis # pylint:disable=unused-import diff --git a/pycufsm/analysis_c.pyx b/pycufsm/solve/analysis_c.pyx similarity index 100% rename from pycufsm/analysis_c.pyx rename to pycufsm/solve/analysis_c.pyx diff --git a/pycufsm/analysis_p.py b/pycufsm/solve/analysis_p.py similarity index 100% rename from pycufsm/analysis_p.py rename to pycufsm/solve/analysis_p.py diff --git a/pycufsm/cfsm.py b/pycufsm/solve/cfsm.py similarity index 99% rename from pycufsm/cfsm.py rename to pycufsm/solve/cfsm.py index 4cb18ce..f10e6a8 100644 --- a/pycufsm/cfsm.py +++ b/pycufsm/solve/cfsm.py @@ -4,8 +4,8 @@ import numpy as np from scipy import linalg as spla # type: ignore -from pycufsm.analysis import analysis -from pycufsm.types import GBT_Con, Sect_Props +from pycufsm._types import GBT_Con, Sect_Props +from pycufsm.solve.analysis import analysis # Originally developed for MATLAB by Benjamin Schafer PhD et al # Ported to Python by Brooks Smith MEng, PE, CPEng diff --git a/tests/fixtures/e2e_fixtures.py b/tests/fixtures/e2e_fixtures.py index 53c2a92..228757c 100644 --- a/tests/fixtures/e2e_fixtures.py +++ b/tests/fixtures/e2e_fixtures.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from pycufsm import fsm, helpers, preprocess +from pycufsm import fsm # import pycufsm.examples.example_1 as ex1_main @@ -187,7 +187,7 @@ def offset_basis(thickness): @pytest.fixture def stress_gen(nodes, forces, sect_props, offset_basis): - return preprocess.stress_gen(nodes=nodes, forces=forces, sect_props=sect_props, offset_basis=offset_basis) + return forces.stress_gen(nodes=nodes, forces=forces, sect_props=sect_props, offset_basis=offset_basis) @pytest.fixture diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 9a350b6..3bc963a 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -4,7 +4,8 @@ import pycufsm.examples.example_1 as example_1 import pycufsm.examples.example_1_new as example_1_new -from pycufsm import cutwp, fsm, helpers +from pycufsm import fsm, helpers +from pycufsm.pre import cutwp from .fixtures.e2e_fixtures import * from .utils import pspec_context