From 6e9b3f1b071dd12588ae5950e191e204b2111f39 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:59:18 +1100 Subject: [PATCH 01/14] Reorganis the package structure to use subdirectories cleanly --- pycufsm/Jupyter_Notebooks/gui_widgets.py | 19 ++- pycufsm/examples/example_1.py | 2 +- pycufsm/examples/example_2.py | 2 +- pycufsm/fsm.py | 28 ++-- pycufsm/helpers.py | 44 ------- pycufsm/post/helpers.py | 45 +++++++ pycufsm/{ => post}/plotters.py | 2 +- pycufsm/{ => pre}/cutwp.py | 0 pycufsm/pre/forces.py | 144 +++++++++++++++++++++ pycufsm/{ => pre}/geometry.py | 2 + pycufsm/{preprocess.py => pre/template.py} | 131 ------------------- pycufsm/{ => solve}/analysis.py | 0 pycufsm/{ => solve}/analysis_c.pyx | 0 pycufsm/{ => solve}/analysis_p.py | 0 pycufsm/{ => solve}/cfsm.py | 0 tests/fixtures/e2e_fixtures.py | 4 +- tests/test_e2e.py | 3 +- 17 files changed, 222 insertions(+), 204 deletions(-) create mode 100644 pycufsm/post/helpers.py rename pycufsm/{ => post}/plotters.py (99%) rename pycufsm/{ => pre}/cutwp.py (100%) create mode 100644 pycufsm/pre/forces.py rename pycufsm/{ => pre}/geometry.py (99%) rename pycufsm/{preprocess.py => pre/template.py} (73%) rename pycufsm/{ => solve}/analysis.py (100%) rename pycufsm/{ => solve}/analysis_c.pyx (100%) rename pycufsm/{ => solve}/analysis_p.py (100%) rename pycufsm/{ => solve}/cfsm.py (100%) diff --git a/pycufsm/Jupyter_Notebooks/gui_widgets.py b/pycufsm/Jupyter_Notebooks/gui_widgets.py index 2d74a16..c414eba 100644 --- a/pycufsm/Jupyter_Notebooks/gui_widgets.py +++ b/pycufsm/Jupyter_Notebooks/gui_widgets.py @@ -3,7 +3,7 @@ import ipywidgets as widgets import numpy as np -import pycufsm.plotters as crossect +import pycufsm.post.plotters as plotters def prevals() -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, List[int]]: @@ -255,7 +255,6 @@ def Assemble(self, page, rowm, rnode, relem, rflag, cs, rBC): self.page.close() del self.page self.page = widgets.VBox([self.row0, self.row, self.relem]) - display(self.page) def add_material(self, b): self.m = self.m + 1 @@ -270,7 +269,7 @@ def add_material(self, b): self.rowm, self.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.props, self.m) 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.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) def del_material(self, b): @@ -283,7 +282,7 @@ def del_material(self, b): self.rowm, self.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.props, self.m) 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.Assemble(self.page, self.rowm, self.rnode, self.relem, self.rflag, self.cs, self.rBC) def add_node(self, b): @@ -299,7 +298,7 @@ def add_node(self, b): self.rnode, self.ADDNODE, self.DELNODE, 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) + plotters.cross_sect(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) def del_node(self, b): @@ -314,7 +313,7 @@ def del_node(self, b): self.rnode, self.ADDNODE, self.DELNODE, 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) + plotters.cross_sect(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) def add_elem(self, b): @@ -330,7 +329,7 @@ def add_elem(self, b): self.relem, self.ADDELEM, self.DELELEM, 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) + plotters.cross_sect(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) def del_elem(self, b): @@ -343,7 +342,7 @@ def del_elem(self, b): self.relem, self.ADDELEM, self.DELELEM, 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) + plotters.cross_sect(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) def submit(self, b): @@ -371,7 +370,7 @@ def submit(self, b): self.rflag, self.Submit, 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) + plotters.cross_sect(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) def run(self, m, n, e, props, nodes, elements, springs, constraints, flag): @@ -391,7 +390,7 @@ def run(self, m, n, e, props, nodes, elements, springs, constraints, flag): self.rBC, self.bc_widget, self.neigs = self.wBound_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) return self.props, self.nodes, self.elements diff --git a/pycufsm/examples/example_1.py b/pycufsm/examples/example_1.py index df2addd..33dd2bc 100644 --- a/pycufsm/examples/example_1.py +++ b/pycufsm/examples/example_1.py @@ -3,7 +3,7 @@ import numpy as np from pycufsm.fsm import strip -from pycufsm.preprocess import stress_gen +from pycufsm.pre.forces import stress_gen from pycufsm.types import BC, GBT_Con, Sect_Props # This example presents a very simple Cee section, diff --git a/pycufsm/examples/example_2.py b/pycufsm/examples/example_2.py index 5e4ba86..9e8e1ef 100644 --- a/pycufsm/examples/example_2.py +++ b/pycufsm/examples/example_2.py @@ -3,7 +3,7 @@ import numpy as np from pycufsm.fsm import strip -from pycufsm.preprocess import stress_gen +from pycufsm.pre.forces import stress_gen from pycufsm.types import BC, GBT_Con, Sect_Props # This example presents a very simple Zed section, diff --git a/pycufsm/fsm.py b/pycufsm/fsm.py index 2a3799c..ecc63b2 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -4,10 +4,10 @@ import numpy as np from scipy import linalg as spla # type: ignore -import pycufsm.cfsm -from pycufsm.analysis import analysis +import pycufsm.pre.forces as forces +import pycufsm.solve.cfsm as cfsm from pycufsm.helpers import inputs_new_to_old, lengths_recommend -from pycufsm.preprocess import stress_gen, yield_mp +from pycufsm.solve.analysis import analysis from pycufsm.types import ( BC, Analysis_Config, @@ -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, @@ -704,7 +704,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 = forces.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 +720,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 = forces.stress_gen(nodes=nodes_old, forces=forces, sect_props=sect_props) elif np.shape(nodes)[1] == 3: nodes_stressed = nodes_old else: @@ -866,7 +868,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..a597c1f 100644 --- a/pycufsm/helpers.py +++ b/pycufsm/helpers.py @@ -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/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 474b781..50d8000 100644 --- a/pycufsm/plotters.py +++ b/pycufsm/post/plotters.py @@ -5,7 +5,7 @@ from matplotlib.collections import PatchCollection from matplotlib.patches import Polygon -from pycufsm import helpers +import pycufsm.post.helpers as helpers from pycufsm.analysis import analysis diff --git a/pycufsm/cutwp.py b/pycufsm/pre/cutwp.py similarity index 100% rename from pycufsm/cutwp.py rename to pycufsm/pre/cutwp.py diff --git a/pycufsm/pre/forces.py b/pycufsm/pre/forces.py new file mode 100644 index 0000000..529e688 --- /dev/null +++ b/pycufsm/pre/forces.py @@ -0,0 +1,144 @@ +from typing import Optional, Tuple, Union + +import numpy as np +from scipy import linalg as spla # type: ignore + +from pycufsm.types import Forces, Sect_Geom, 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/geometry.py b/pycufsm/pre/geometry.py similarity index 99% rename from pycufsm/geometry.py rename to pycufsm/pre/geometry.py index 835f4f5..18ff335 100644 --- a/pycufsm/geometry.py +++ b/pycufsm/pre/geometry.py @@ -3,6 +3,8 @@ import numpy as np +# 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/preprocess.py b/pycufsm/pre/template.py similarity index 73% rename from pycufsm/preprocess.py rename to pycufsm/pre/template.py index 7bc67cd..14be89e 100644 --- a/pycufsm/preprocess.py +++ b/pycufsm/pre/template.py @@ -340,137 +340,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/analysis.py b/pycufsm/solve/analysis.py similarity index 100% rename from pycufsm/analysis.py rename to pycufsm/solve/analysis.py 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 100% rename from pycufsm/cfsm.py rename to pycufsm/solve/cfsm.py 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 From 2bc87ce66296d630c0a91c5f621cf80da559c999 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:08:29 +1100 Subject: [PATCH 02/14] typing --- pycufsm/pre/geometry.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pycufsm/pre/geometry.py b/pycufsm/pre/geometry.py index f891d53..4ef6c0d 100644 --- a/pycufsm/pre/geometry.py +++ b/pycufsm/pre/geometry.py @@ -3,17 +3,19 @@ import numpy as np +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 def mesh_nodes( - centerline_coords: np.ndarray, + centerline_coords: ArrayLike, corner_radius: float, mesh_corner_deg: Optional[float] = 22.5, mesh_side_len: Optional[float] = 0.0, -): +) -> np.ndarray: """ Convert centerline coordinates to node coordinates for a cross-section. Note that all meshing parameters are interpreted as maximum values; meshing will always be performed evenly over each curved or straight segment. @@ -37,7 +39,9 @@ def mesh_nodes( """ # Initialize parameters centerline_coords = np.array(centerline_coords) - mesh_corner_rad = mesh_corner_deg * np.pi / 180 # Convert mesh corner angle from degrees to radians + mesh_corner_rad = ( + mesh_corner_deg * np.pi / 180 if mesh_corner_deg is not None else None + ) # Convert mesh corner angle from degrees to radians if mesh_side_len is not None and mesh_side_len <= 0.0: # Default mesh side length to the maximum outer dimension divided by 4 max_dim = np.max(np.ptp(centerline_coords, axis=0)) @@ -106,7 +110,7 @@ def c_section( r_inner: float = 0.0, mesh_corner_deg: Optional[float] = 22.5, mesh_side_len: Optional[float] = 0.0, -): +) -> np.ndarray: """ Convert cross-section outer dimensions of a "C" section to meshed nodes. @@ -176,7 +180,7 @@ def z_section( r_inner: float = 0.0, mesh_corner_deg: Optional[float] = 22.5, mesh_side_len: Optional[float] = 0.0, -): +) -> np.ndarray: """ Convert cross-section outer dimensions of a "Z" section to centerline coordinates. This function does not mesh the cross-section, and does not account for corner radii. If these are needed, run this function's output through @@ -247,7 +251,7 @@ def f_section( r_inner: float = 0.0, mesh_corner_deg: Optional[float] = 22.5, mesh_side_len: Optional[float] = 0.0, -): +) -> np.ndarray: """ Convert cross-section outer dimensions of a "F" section to centerline coordinates. This function does not mesh the cross-section, and does not account for corner radii. If these are needed, run this function's output through @@ -339,8 +343,8 @@ def _sfia_thickness_and_radius(designation: int, thickness_type: Literal["minimu raise ValueError(f"Designation {int(designation)} not found in thickness table.") row = thickness_table[int(designation)] - t = row["t_min"] if thickness_type == "minimum" else row["t_design"] - r = row["r_inner"] + t / 2 # Convert inner radius to centerline radius + t: float = row["t_min"] if thickness_type == "minimum" else row["t_design"] + r: float = row["r_inner"] + t / 2 # Convert inner radius to centerline radius row["t"] = t row["r"] = r return row @@ -392,7 +396,7 @@ def sfia_section( try: depth, section_type, flange_width, thickness_mils = re.match( r"(\d+)([STUFCZ])(\d+)-(\d+)", designation - ).groups() + ).groups() # type: ignore (this is a type failure because the regex match could fail, but we catch that with the except block below) except AttributeError as exc: raise ValueError(f"Invalid SFIA designation format: '{designation}'.") from exc From ceae6ce76e8c82f869989919c1517aa78dc362bd Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:53:02 +1100 Subject: [PATCH 03/14] Make sect_props optional in strip_new(), emphasise force inputs --- pycufsm/fsm.py | 94 ++++++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/pycufsm/fsm.py b/pycufsm/fsm.py index ecc63b2..6e2a517 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -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: From c1b230f3f8055f5556804408532e5db2aff06f37 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 19:40:23 +1000 Subject: [PATCH 04/14] Remove greetings workflow, which is too argumentative --- .github/workflows/greetings.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/greetings.yml 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!" From 9d1674c172eeb2df47f036845b2dd82dd14841b9 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 19:44:04 +1000 Subject: [PATCH 05/14] Add __init__'s --- pycufsm/Jupyter_Notebooks/__init__.py | 0 pycufsm/examples/__init__.py | 0 pycufsm/post/__init__.py | 0 pycufsm/pre/__init__.py | 0 pycufsm/solve/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pycufsm/Jupyter_Notebooks/__init__.py create mode 100644 pycufsm/examples/__init__.py create mode 100644 pycufsm/post/__init__.py create mode 100644 pycufsm/pre/__init__.py create mode 100644 pycufsm/solve/__init__.py 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/examples/__init__.py b/pycufsm/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/post/__init__.py b/pycufsm/post/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/pre/__init__.py b/pycufsm/pre/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycufsm/solve/__init__.py b/pycufsm/solve/__init__.py new file mode 100644 index 0000000..e69de29 From 0768159fa6cf525fddcb3b26a7325cf5be83147c Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 19:50:05 +1000 Subject: [PATCH 06/14] Fix install issues --- pycufsm/_types.py | 215 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 pycufsm/_types.py diff --git a/pycufsm/_types.py b/pycufsm/_types.py new file mode 100644 index 0000000..543047b --- /dev/null +++ b/pycufsm/_types.py @@ -0,0 +1,215 @@ +from typing import Any, List, Literal, Optional, Union + +import numpy as np + +try: + from typing import TypedDict # Python 3.12+ +except ImportError: + from typing_extensions import TypedDict # Python <3.12 + +__all__ = [ + "ArrayLike", + "BC", + "Directions", + "ForceTypes", + "Analysis_Config", + "Cfsm_Config", + "Cufsm_MAT_File", + "Forces", + "GBT_Con", + "New_Constraint", + "New_Element", + "New_Node_Props", + "New_Props", + "New_Props_min", + "New_Spring", + "PyCufsm_Input", + "Sect_Geom", + "Sect_Props", + "Yield_Force", +] + +ArrayLike = Union[np.ndarray, list, tuple] + +BC = Literal["S-S", "C-C", "S-C", "C-F", "C-G"] + +Directions = Literal["Pos", "Neg", "+", "-"] + +ForceTypes = Literal["Mxx", "Myy", "M11", "M22", "P"] + +Analysis_Config = TypedDict( + "Analysis_Config", + { + "B_C": BC, # boundary condition type + "n_eigs": int, # number of eigenvalues to consider + }, +) + +Cfsm_Config = TypedDict( + "Cfsm_Config", + { + "glob_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) + "dist_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) + "local_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) + "other_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) + "null_space": Literal["ST", "K_global", "Kg_global", "vector"], + "normalization": Literal["none", "vector", "strain_energy", "work"], + "coupled": bool, # coupled basis vs uncoupled basis for general B.C. + "orthogonality": Literal["natural", "modal_axial", "modal_load"], # natural or modal basis + }, +) + +Cufsm_MAT_File = TypedDict( + "Cufsm_MAT_File", + { + "node": list, + "elem": list, + "lengths": list, + "prop": list, + "constraints": list, + "springs": list, + "curve": list, + "GBTcon": Any, # this is some special structure with string dictionaries but a dtype attribute + "shapes": list, + "clas": str, + }, +) + +Forces = TypedDict( + "Forces", + { + "P": float, + "Mxx": float, + "Myy": float, + "M11": float, + "M22": float, + "restrain": bool, + "offset": Optional[ArrayLike], + }, +) + +GBT_Con = TypedDict( + "GBT_Con", + { + "glob": List[Literal[0, 1]], + "dist": List[Literal[0, 1]], + "local": List[Literal[0, 1]], + "other": List[Literal[0, 1]], + "o_space": int, + "couple": int, + "orth": int, + "norm": int, + }, +) + +New_Constraint = TypedDict( + "New_Constraint", + { + "elim_node": int, # node # + "elim_dof": Literal["x", "y", "z", "q"], # "q" is the twist dof + "coeff": float, # elim_dof = coeff * keep_dof + "keep_node": int, # node # + "keep_dof": Literal["x", "y", "z", "q"], # "q" is the twist dof + }, +) + +New_Element = TypedDict( + "New_Element", + { + "nodes": Union[Literal["all"], List[int]], # "all" or [node1, node2, node3, ...] + "t": float, # thickness + "mat": str, # "mat_name" + }, +) + +New_Node_Props = TypedDict( + "New_Node_Props", + { + "dof_x": bool, # defaults to True + "dof_y": bool, # defaults to True + "dof_z": bool, # defaults to True + "dof_q": bool, # defaults to True + }, +) + +New_Props = TypedDict("New_Props", {"E_x": float, "E_y": float, "nu_x": float, "nu_y": float, "G_bulk": float}) + +New_Props_min = TypedDict("New_Props_min", {"E": float, "nu": float, "f_y": float}) + +New_Spring = TypedDict( + "New_Spring", + { + "node": int, # node # + "k_x": float, # x stiffness + "k_y": float, # y stiffness + "k_z": float, # z stiffness + "k_q": float, # q stiffness + "k_type": Literal["foundation", "total", "node_pair"], # "foundation"|"total"|"node_pair" - stiffness type, + "node_pair": int, # node number to which to pair (if relevant) + "discrete": bool, + "y": float, # location of discrete spring + }, +) + +PyCufsm_Input = TypedDict( + "PyCufsm_Input", + { + "nodes": np.ndarray, + "elements": np.ndarray, + "lengths": np.ndarray, + "props": np.ndarray, + "constraints": np.ndarray, + "springs": np.ndarray, + "curve": np.ndarray, + "shapes": np.ndarray, + "clas": str, + "GBTcon": GBT_Con, + }, +) + +Sect_Geom = TypedDict( + "Sect_Geom", + { + "n_d": int, + "n_b1": int, + "n_b2": int, + "n_l1": int, + "n_l2": int, + "n_r": int, + "type": str, + "b_1": float, + "b_2": float, + "t": float, + "r_out": float, + "d": float, + "l_1": float, + "l_2": float, + }, +) + +Sect_Props = TypedDict( + "Sect_Props", + { + "A": float, + "cx": float, + "cy": float, + "Ixx": float, + "Iyy": float, + "Ixy": float, + "phi": float, + "I11": float, + "I22": float, + "J": Optional[float], + "x0": Optional[float], + "y0": Optional[float], + "Cw": Optional[float], + "B1": Optional[float], + "B2": Optional[float], + "wn": Optional[np.ndarray], + }, +) + +Yield_Force = TypedDict( + "Yield_Force", + {"force": ForceTypes, "direction": Directions, "f_y": float, "restrain": bool, "offset": Optional[ArrayLike]}, +) From eeb49160143f7b542135ee24705baf6a4809a8d3 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 19:50:18 +1000 Subject: [PATCH 07/14] Fix install issues --- build_cython_ext.py | 4 +- pycufsm/post/plotters.py | 2 +- pycufsm/solve/analysis.py | 6 +- pycufsm/solve/cfsm.py | 4 +- pycufsm/types.py | 215 -------------------------------------- 5 files changed, 8 insertions(+), 223 deletions(-) delete mode 100644 pycufsm/types.py 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/post/plotters.py b/pycufsm/post/plotters.py index d928326..043785c 100644 --- a/pycufsm/post/plotters.py +++ b/pycufsm/post/plotters.py @@ -6,7 +6,7 @@ from matplotlib.patches import Polygon import pycufsm.post.helpers as helpers -from pycufsm.analysis import analysis +from pycufsm.solve.analysis import analysis def cross_sect( diff --git a/pycufsm/solve/analysis.py b/pycufsm/solve/analysis.py index 3622217..8e0203a 100644 --- a/pycufsm/solve/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/solve/cfsm.py b/pycufsm/solve/cfsm.py index 4cb18ce..f10e6a8 100644 --- a/pycufsm/solve/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/pycufsm/types.py b/pycufsm/types.py deleted file mode 100644 index 543047b..0000000 --- a/pycufsm/types.py +++ /dev/null @@ -1,215 +0,0 @@ -from typing import Any, List, Literal, Optional, Union - -import numpy as np - -try: - from typing import TypedDict # Python 3.12+ -except ImportError: - from typing_extensions import TypedDict # Python <3.12 - -__all__ = [ - "ArrayLike", - "BC", - "Directions", - "ForceTypes", - "Analysis_Config", - "Cfsm_Config", - "Cufsm_MAT_File", - "Forces", - "GBT_Con", - "New_Constraint", - "New_Element", - "New_Node_Props", - "New_Props", - "New_Props_min", - "New_Spring", - "PyCufsm_Input", - "Sect_Geom", - "Sect_Props", - "Yield_Force", -] - -ArrayLike = Union[np.ndarray, list, tuple] - -BC = Literal["S-S", "C-C", "S-C", "C-F", "C-G"] - -Directions = Literal["Pos", "Neg", "+", "-"] - -ForceTypes = Literal["Mxx", "Myy", "M11", "M22", "P"] - -Analysis_Config = TypedDict( - "Analysis_Config", - { - "B_C": BC, # boundary condition type - "n_eigs": int, # number of eigenvalues to consider - }, -) - -Cfsm_Config = TypedDict( - "Cfsm_Config", - { - "glob_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) - "dist_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) - "local_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) - "other_modes": List[Literal[0, 1]], # list of 1's (inclusion) and 0's (exclusion) - "null_space": Literal["ST", "K_global", "Kg_global", "vector"], - "normalization": Literal["none", "vector", "strain_energy", "work"], - "coupled": bool, # coupled basis vs uncoupled basis for general B.C. - "orthogonality": Literal["natural", "modal_axial", "modal_load"], # natural or modal basis - }, -) - -Cufsm_MAT_File = TypedDict( - "Cufsm_MAT_File", - { - "node": list, - "elem": list, - "lengths": list, - "prop": list, - "constraints": list, - "springs": list, - "curve": list, - "GBTcon": Any, # this is some special structure with string dictionaries but a dtype attribute - "shapes": list, - "clas": str, - }, -) - -Forces = TypedDict( - "Forces", - { - "P": float, - "Mxx": float, - "Myy": float, - "M11": float, - "M22": float, - "restrain": bool, - "offset": Optional[ArrayLike], - }, -) - -GBT_Con = TypedDict( - "GBT_Con", - { - "glob": List[Literal[0, 1]], - "dist": List[Literal[0, 1]], - "local": List[Literal[0, 1]], - "other": List[Literal[0, 1]], - "o_space": int, - "couple": int, - "orth": int, - "norm": int, - }, -) - -New_Constraint = TypedDict( - "New_Constraint", - { - "elim_node": int, # node # - "elim_dof": Literal["x", "y", "z", "q"], # "q" is the twist dof - "coeff": float, # elim_dof = coeff * keep_dof - "keep_node": int, # node # - "keep_dof": Literal["x", "y", "z", "q"], # "q" is the twist dof - }, -) - -New_Element = TypedDict( - "New_Element", - { - "nodes": Union[Literal["all"], List[int]], # "all" or [node1, node2, node3, ...] - "t": float, # thickness - "mat": str, # "mat_name" - }, -) - -New_Node_Props = TypedDict( - "New_Node_Props", - { - "dof_x": bool, # defaults to True - "dof_y": bool, # defaults to True - "dof_z": bool, # defaults to True - "dof_q": bool, # defaults to True - }, -) - -New_Props = TypedDict("New_Props", {"E_x": float, "E_y": float, "nu_x": float, "nu_y": float, "G_bulk": float}) - -New_Props_min = TypedDict("New_Props_min", {"E": float, "nu": float, "f_y": float}) - -New_Spring = TypedDict( - "New_Spring", - { - "node": int, # node # - "k_x": float, # x stiffness - "k_y": float, # y stiffness - "k_z": float, # z stiffness - "k_q": float, # q stiffness - "k_type": Literal["foundation", "total", "node_pair"], # "foundation"|"total"|"node_pair" - stiffness type, - "node_pair": int, # node number to which to pair (if relevant) - "discrete": bool, - "y": float, # location of discrete spring - }, -) - -PyCufsm_Input = TypedDict( - "PyCufsm_Input", - { - "nodes": np.ndarray, - "elements": np.ndarray, - "lengths": np.ndarray, - "props": np.ndarray, - "constraints": np.ndarray, - "springs": np.ndarray, - "curve": np.ndarray, - "shapes": np.ndarray, - "clas": str, - "GBTcon": GBT_Con, - }, -) - -Sect_Geom = TypedDict( - "Sect_Geom", - { - "n_d": int, - "n_b1": int, - "n_b2": int, - "n_l1": int, - "n_l2": int, - "n_r": int, - "type": str, - "b_1": float, - "b_2": float, - "t": float, - "r_out": float, - "d": float, - "l_1": float, - "l_2": float, - }, -) - -Sect_Props = TypedDict( - "Sect_Props", - { - "A": float, - "cx": float, - "cy": float, - "Ixx": float, - "Iyy": float, - "Ixy": float, - "phi": float, - "I11": float, - "I22": float, - "J": Optional[float], - "x0": Optional[float], - "y0": Optional[float], - "Cw": Optional[float], - "B1": Optional[float], - "B2": Optional[float], - "wn": Optional[np.ndarray], - }, -) - -Yield_Force = TypedDict( - "Yield_Force", - {"force": ForceTypes, "direction": Directions, "f_y": float, "restrain": bool, "offset": Optional[ArrayLike]}, -) From 6e516c64edc0c18d39dff77e9780fc5f930832ab Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 19:54:07 +1000 Subject: [PATCH 08/14] Fix some imports --- pycufsm/examples/example_1.py | 2 +- pycufsm/examples/example_1_new.py | 2 +- pycufsm/examples/example_2.py | 2 +- pycufsm/fsm.py | 6 +++--- pycufsm/helpers.py | 2 +- pycufsm/pre/cutwp.py | 2 +- pycufsm/pre/forces.py | 2 +- pycufsm/pre/geometry.py | 2 +- pycufsm/pre/template.py | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pycufsm/examples/example_1.py b/pycufsm/examples/example_1.py index 33dd2bc..d8cec9d 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.pre.forces import stress_gen -from pycufsm.types import BC, GBT_Con, Sect_Props # 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 9e8e1ef..2bd2454 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.pre.forces import stress_gen -from pycufsm.types import BC, GBT_Con, Sect_Props # 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 6e2a517..1c9ae79 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -6,9 +6,7 @@ import pycufsm.pre.forces as forces import pycufsm.solve.cfsm as cfsm -from pycufsm.helpers import inputs_new_to_old, lengths_recommend -from pycufsm.solve.analysis import analysis -from pycufsm.types import ( +from pycufsm._types import ( BC, Analysis_Config, ArrayLike, @@ -22,6 +20,8 @@ Sect_Props, Yield_Force, ) +from pycufsm.helpers import inputs_new_to_old, lengths_recommend +from pycufsm.solve.analysis import analysis # from scipy.sparse.linalg import eigs # Originally developed for MATLAB by Benjamin Schafer PhD et al diff --git a/pycufsm/helpers.py b/pycufsm/helpers.py index a597c1f..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, diff --git a/pycufsm/pre/cutwp.py b/pycufsm/pre/cutwp.py index 6377377..7666d17 100644 --- a/pycufsm/pre/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/pre/forces.py b/pycufsm/pre/forces.py index 529e688..c5081f0 100644 --- a/pycufsm/pre/forces.py +++ b/pycufsm/pre/forces.py @@ -3,7 +3,7 @@ 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 Forces, Sect_Geom, Sect_Props # Originally developed for MATLAB by Benjamin Schafer PhD et al # Ported to Python by Brooks Smith MEng, PE diff --git a/pycufsm/pre/geometry.py b/pycufsm/pre/geometry.py index 81b440e..c2798a5 100644 --- a/pycufsm/pre/geometry.py +++ b/pycufsm/pre/geometry.py @@ -3,7 +3,7 @@ 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. diff --git a/pycufsm/pre/template.py b/pycufsm/pre/template.py index 14be89e..5c34330 100644 --- a/pycufsm/pre/template.py +++ b/pycufsm/pre/template.py @@ -3,7 +3,7 @@ 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 Forces, Sect_Geom, Sect_Props # Originally developed for MATLAB by Benjamin Schafer PhD et al # Ported to Python by Brooks Smith MEng, PE From 61a8ba17f703d465ae68d3e927cf672ed541fe2e Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 20:01:27 +1000 Subject: [PATCH 09/14] Fix name collision --- pycufsm/examples/example_1.py | 2 +- pycufsm/examples/example_2.py | 2 +- pycufsm/fsm.py | 26 ++++++++------------------ pycufsm/pre/{forces.py => stresses.py} | 0 4 files changed, 10 insertions(+), 20 deletions(-) rename pycufsm/pre/{forces.py => stresses.py} (100%) diff --git a/pycufsm/examples/example_1.py b/pycufsm/examples/example_1.py index d8cec9d..1d23c2f 100644 --- a/pycufsm/examples/example_1.py +++ b/pycufsm/examples/example_1.py @@ -4,7 +4,7 @@ from pycufsm._types import BC, GBT_Con, Sect_Props from pycufsm.fsm import strip -from pycufsm.pre.forces import stress_gen +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_2.py b/pycufsm/examples/example_2.py index 2bd2454..7d9b164 100644 --- a/pycufsm/examples/example_2.py +++ b/pycufsm/examples/example_2.py @@ -4,7 +4,7 @@ from pycufsm._types import BC, GBT_Con, Sect_Props from pycufsm.fsm import strip -from pycufsm.pre.forces import stress_gen +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 1c9ae79..2be30de 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -4,23 +4,13 @@ import numpy as np from scipy import linalg as spla # type: ignore -import pycufsm.pre.forces as forces -import pycufsm.solve.cfsm as cfsm -from pycufsm._types import ( - BC, - Analysis_Config, - ArrayLike, - Cfsm_Config, - Forces, - GBT_Con, - New_Constraint, - New_Element, - New_Node_Props, - New_Spring, - Sect_Props, - Yield_Force, -) +from pycufsm._types import (BC, Analysis_Config, ArrayLike, Cfsm_Config, + Forces, GBT_Con, New_Constraint, New_Element, + New_Node_Props, New_Spring, Sect_Props, + Yield_Force) from pycufsm.helpers import inputs_new_to_old, lengths_recommend +from pycufsm.pre import stresses +from pycufsm.solve import cfsm from pycufsm.solve.analysis import analysis # from scipy.sparse.linalg import eigs @@ -708,7 +698,7 @@ 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 = forces.yield_mp( + 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} @@ -724,7 +714,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 = forces.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: diff --git a/pycufsm/pre/forces.py b/pycufsm/pre/stresses.py similarity index 100% rename from pycufsm/pre/forces.py rename to pycufsm/pre/stresses.py From ebc649e36448f955eac6ca7706495a7f549f7c97 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 20:15:13 +1000 Subject: [PATCH 10/14] Linting --- pycufsm/Jupyter_Notebooks/gui_widgets.py | 79 +++++++++++++++++------- pycufsm/post/plotters.py | 2 +- pycufsm/pre/geometry.py | 2 +- pycufsm/pre/stresses.py | 4 +- pycufsm/pre/template.py | 8 ++- 5 files changed, 66 insertions(+), 29 deletions(-) diff --git a/pycufsm/Jupyter_Notebooks/gui_widgets.py b/pycufsm/Jupyter_Notebooks/gui_widgets.py index c414eba..86d5efb 100644 --- a/pycufsm/Jupyter_Notebooks/gui_widgets.py +++ b/pycufsm/Jupyter_Notebooks/gui_widgets.py @@ -3,7 +3,7 @@ import ipywidgets as widgets import numpy as np -import pycufsm.post.plotters as plotters +from pycufsm.post import plotters def prevals() -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, List[int]]: @@ -53,7 +53,42 @@ def prevals() -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarra class Preprocess: - def wprops(self, props: np.ndarray, m: int) -> Tuple[widgets.VBox, widgets.Button, widgets.Button, list]: + def __init__(self): + 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.ADDMAT = None + self.DELMAT = None + self.ADDNODE = None + self.DELNODE = None + self.ADDELEM = None + self.DELELEM = None + self.Submit = None + self.bc_widget = None + self.neigs = None + self.rowm = None + self.rnode = None + self.relem = None + self.rflag = None + self.rBC = 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]: self.m = m matTitle = widgets.Label(value="Material Properties") mattext = ["mat#", "Ex", "Ey", "vx", "vy", "G"] @@ -241,7 +276,7 @@ def wBound_Cond(self): self.rBC = widgets.VBox([self.bc_widget, self.neigs], layout=widgets.Layout(width="50%")) return self.rBC, self.bc_widget, self.neigs - def Assemble(self, page, rowm, rnode, relem, rflag, cs, rBC): + def Assemble(self): 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%")) @@ -256,7 +291,7 @@ def Assemble(self, page, rowm, rnode, relem, rflag, cs, rBC): del self.page self.page = widgets.VBox([self.row0, self.row, self.relem]) - def add_material(self, b): + def add_material(self): self.m = self.m + 1 self.props = [[] for i in range(self.m)] for i in range(self.m): @@ -266,26 +301,26 @@ 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.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.m) self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() - def del_material(self, b): + def del_material(self): 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.ADDMAT, self.DELMAT, self.mitems = self.wprops(self.m) self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() - def add_node(self, b): + def add_node(self): self.n = self.n + 1 self.nodes = [[] for i in range(self.n)] for i in range(self.n): @@ -299,12 +334,12 @@ def add_node(self, b): self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() - def del_node(self, b): + def del_node(self): 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): @@ -314,9 +349,9 @@ def del_node(self, b): self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() - def add_elem(self, b): + def add_elem(self): self.e = self.e + 1 self.elements = [[] for i in range(self.e)] for i in range(self.e): @@ -330,9 +365,9 @@ def add_elem(self, b): self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() - def del_elem(self, b): + def del_elem(self): self.e = self.e - 1 self.elements = [[] for i in range(self.e)] for i in range(self.e): @@ -343,9 +378,9 @@ def del_elem(self, b): self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() - def submit(self, b): + def submit(self): self.props = [[] for i in range(self.m)] for i in range(self.m): for j in range(6): @@ -371,7 +406,7 @@ def submit(self, b): self.cs = widgets.Output() with self.cs: plotters.cross_sect(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) + self.Assemble() def run(self, m, n, e, props, nodes, elements, springs, constraints, flag): self.m = m @@ -383,7 +418,7 @@ 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.rowm, self.ADDMAT, self.DELMAT, self.mitems = self.wprops(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) @@ -392,5 +427,5 @@ def run(self, m, n, e, props, nodes, elements, springs, constraints, flag): with self.cs: 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/post/plotters.py b/pycufsm/post/plotters.py index 043785c..c4895bc 100644 --- a/pycufsm/post/plotters.py +++ b/pycufsm/post/plotters.py @@ -5,7 +5,7 @@ from matplotlib.collections import PatchCollection from matplotlib.patches import Polygon -import pycufsm.post.helpers as helpers +from pycufsm.post import helpers from pycufsm.solve.analysis import analysis diff --git a/pycufsm/pre/geometry.py b/pycufsm/pre/geometry.py index c2798a5..f078c21 100644 --- a/pycufsm/pre/geometry.py +++ b/pycufsm/pre/geometry.py @@ -5,7 +5,7 @@ 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. +# 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 index c5081f0..c7178d0 100644 --- a/pycufsm/pre/stresses.py +++ b/pycufsm/pre/stresses.py @@ -1,9 +1,9 @@ -from typing import Optional, Tuple, Union +from typing import Union 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 Forces, Sect_Props # Originally developed for MATLAB by Benjamin Schafer PhD et al # Ported to Python by Brooks Smith MEng, PE diff --git a/pycufsm/pre/template.py b/pycufsm/pre/template.py index 5c34330..7a90f67 100644 --- a/pycufsm/pre/template.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 From 83620bfe6fcb01065470c50aa9f737c256a05847 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 20:26:55 +1000 Subject: [PATCH 11/14] Fix remaining linting issues in gui_widgets --- pycufsm/Jupyter_Notebooks/gui_widgets.py | 207 +++++++++++++++-------- 1 file changed, 135 insertions(+), 72 deletions(-) diff --git a/pycufsm/Jupyter_Notebooks/gui_widgets.py b/pycufsm/Jupyter_Notebooks/gui_widgets.py index 86d5efb..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 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,8 +57,11 @@ def prevals() -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarra # return tab -class Preprocess: +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 @@ -66,36 +74,44 @@ def __init__(self): self.mitems = [] self.nitems = [] self.eitems = [] - self.ADDMAT = None - self.DELMAT = None - self.ADDNODE = None - self.DELNODE = None - self.ADDELEM = None - self.DELELEM = None - self.Submit = None + 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.rBC = 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.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"), ) @@ -126,21 +142,30 @@ def wprops(self, m: int) -> Tuple[widgets.VBox, widgets.Button, widgets.Button, 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): @@ -162,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"), ) @@ -215,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", @@ -237,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", @@ -273,25 +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): + 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]) 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): @@ -301,26 +346,28 @@ def add_material(self): 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.m) + self.rowm, self.add_mat_btn, self.del_mat_btn, self.mitems = self.wprops(self.m) self.cs = widgets.Output() with self.cs: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + self.assemble() 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.m) + self.rowm, self.add_mat_btn, self.del_mat_btn, self.mitems = self.wprops(self.m) self.cs = widgets.Output() with self.cs: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + self.assemble() 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): @@ -330,13 +377,14 @@ def add_node(self): 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: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + self.assemble() 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() @@ -345,13 +393,14 @@ def del_node(self): 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: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + self.assemble() 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): @@ -361,26 +410,28 @@ def add_elem(self): 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: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + self.assemble() 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: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + self.assemble() 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): @@ -396,19 +447,31 @@ def submit(self): 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: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) - self.Assemble() + 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 @@ -418,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(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: plotters.cross_sect(self.nodes, self.elements, self.springs, self.constraints, self.flag) self.page = widgets.FloatText(value=1) - self.Assemble() + self.assemble() return self.props, self.nodes, self.elements From b93012a395cd9e831d2af2db8c3eae024732a52d Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 20:27:43 +1000 Subject: [PATCH 12/14] Linting of module name --- pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/Validation.ipynb | 0 pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/__init__.py | 0 pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/gui_widgets.py | 0 pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/pyCUFSM.ipynb | 0 .../{Jupyter_Notebooks => jupyter_notebooks}/pyCUFSM_GUI.ipynb | 0 .../pyCUFSM_load_mat.ipynb | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/Validation.ipynb (100%) rename pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/__init__.py (100%) rename pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/gui_widgets.py (100%) rename pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/pyCUFSM.ipynb (100%) rename pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/pyCUFSM_GUI.ipynb (100%) rename pycufsm/{Jupyter_Notebooks => jupyter_notebooks}/pyCUFSM_load_mat.ipynb (100%) 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 similarity index 100% rename from pycufsm/Jupyter_Notebooks/__init__.py rename to pycufsm/jupyter_notebooks/__init__.py diff --git a/pycufsm/Jupyter_Notebooks/gui_widgets.py b/pycufsm/jupyter_notebooks/gui_widgets.py similarity index 100% rename from pycufsm/Jupyter_Notebooks/gui_widgets.py rename to pycufsm/jupyter_notebooks/gui_widgets.py 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 From 22aa9c6ac831fe37eda0e7bd046f9887404b9905 Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 20:30:22 +1000 Subject: [PATCH 13/14] Missed autoformat for some reason --- pycufsm/fsm.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pycufsm/fsm.py b/pycufsm/fsm.py index 2be30de..71e6d1a 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -4,10 +4,20 @@ import numpy as np from scipy import linalg as spla # type: ignore -from pycufsm._types import (BC, Analysis_Config, ArrayLike, Cfsm_Config, - Forces, GBT_Con, New_Constraint, New_Element, - New_Node_Props, New_Spring, Sect_Props, - Yield_Force) +from pycufsm._types import ( + BC, + Analysis_Config, + ArrayLike, + Cfsm_Config, + Forces, + GBT_Con, + New_Constraint, + New_Element, + New_Node_Props, + New_Spring, + Sect_Props, + Yield_Force, +) from pycufsm.helpers import inputs_new_to_old, lengths_recommend from pycufsm.pre import stresses from pycufsm.solve import cfsm From 28ad5e8df7a34f23a20d1d5d0fc94c36b58578ec Mon Sep 17 00:00:00 2001 From: Brooks Smith <42363318+smith120bh@users.noreply.github.com> Date: Sat, 30 May 2026 20:37:11 +1000 Subject: [PATCH 14/14] Missing handling of None sect_props --- pycufsm/fsm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pycufsm/fsm.py b/pycufsm/fsm.py index 71e6d1a..8bce874 100644 --- a/pycufsm/fsm.py +++ b/pycufsm/fsm.py @@ -19,7 +19,7 @@ Yield_Force, ) from pycufsm.helpers import inputs_new_to_old, lengths_recommend -from pycufsm.pre import stresses +from pycufsm.pre import cutwp, stresses from pycufsm.solve import cfsm from pycufsm.solve.analysis import analysis @@ -682,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,