diff --git a/.gitignore b/.gitignore index 2e0c9c38b..3472db188 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ Output_PDF/* !license-dependencies.txt convert_imports.py fix_relative_imports.py -verify_imports.py \ No newline at end of file +verify_imports.py +# Internal development notes — not for version control +references/ diff --git a/src/osdag/gui/ui_template.py b/src/osdag/gui/ui_template.py index 6d5634987..535ab695a 100644 --- a/src/osdag/gui/ui_template.py +++ b/src/osdag/gui/ui_template.py @@ -1930,6 +1930,8 @@ def design_fn(self, op_list, data_list, main): print(f"design_fn input_type = {input_type}\n") print(f"design_fn input_list = {input_list}\n") print(f"design_fn tab = {tab}\n") + if tab is None: + continue for key_name in input_list: key = tab.findChild(QtWidgets.QWidget, key_name) if key is None: diff --git a/src/osdag/gui/ui_template_for_mac.py b/src/osdag/gui/ui_template_for_mac.py index 81c6ecce8..48738ed05 100644 --- a/src/osdag/gui/ui_template_for_mac.py +++ b/src/osdag/gui/ui_template_for_mac.py @@ -1768,6 +1768,8 @@ def design_fn(self, op_list, data_list, main): input_type = des_pref[1] input_list = des_pref[2] tab = self.designPrefDialog.ui.findChild(QtWidgets.QWidget, tab_name) + if tab is None: + continue for key_name in input_list: key = tab.findChild(QtWidgets.QWidget, key_name) if input_type == TYPE_TEXTBOX: diff --git a/src/osdag_core/design_type/connection/butt_joint_bolted.py b/src/osdag_core/design_type/connection/butt_joint_bolted.py index cb4644930..e6c735451 100644 --- a/src/osdag_core/design_type/connection/butt_joint_bolted.py +++ b/src/osdag_core/design_type/connection/butt_joint_bolted.py @@ -65,6 +65,10 @@ def tab_value_changed(self): def edit_tabs(self): return [] # Keep original empty implementation + def refresh_input_dock(self): + # Butt joint has no section-designation fields; skip Connection's default refresh + return [] + def input_dictionary_design_pref(self): design_input = [] @@ -77,7 +81,8 @@ def input_dictionary_design_pref(self): # Detailing preferences design_input.append(("Detailing", TYPE_COMBOBOX, [ - KEY_DP_DETAILING_EDGE_TYPE # For edge preparation method + KEY_DP_DETAILING_EDGE_TYPE, # For edge preparation method + KEY_DP_DETAILING_PACKING_PLATE # Yes/No; applies β_pkg per IS 800:2007 Cl. 10.3.3.3 ])) return design_input @@ -109,7 +114,7 @@ def get_values_for_design_pref(self, key, design_dictionary): def detailing_values(self, input_dictionary): values = { KEY_DP_DETAILING_EDGE_TYPE: 'Sheared or hand flame cut', - # KEY_DP_DETAILING_PACKING_PLATE: 'Yes', # Commented out packing plate preference + KEY_DP_DETAILING_PACKING_PLATE: 'Yes', } for key in values.keys(): @@ -118,16 +123,16 @@ def detailing_values(self, input_dictionary): detailing = [] - # Edge preparation method as per Cl. 10.2.4 of IS:800:2007 + # Edge preparation method; affects min edge/end distances per IS 800:2007 Cl. 10.2.4.2 t1 = (KEY_DP_DETAILING_EDGE_TYPE, KEY_DISP_DP_DETAILING_EDGE_TYPE, TYPE_COMBOBOX, ['Sheared or hand flame cut', 'Rolled, machine-flame cut, sawn and planed'], values[KEY_DP_DETAILING_EDGE_TYPE]) detailing.append(t1) - # Commented out packing plate design preference - # t3 = (KEY_DP_DETAILING_PACKING_PLATE, KEY_DISP_DP_DETAILING_PACKING_PLATE, TYPE_COMBOBOX, - # ['Yes', 'No'], values[KEY_DP_DETAILING_PACKING_PLATE]) - # detailing.append(t3) + # Packing plate required when plates of different thickness are joined (IS 800:2007 Cl. 10.3.3.3) + t3 = (KEY_DP_DETAILING_PACKING_PLATE, KEY_DISP_DP_DETAILING_PACKING_PLATE, TYPE_COMBOBOX, + ['Yes', 'No'], values[KEY_DP_DETAILING_PACKING_PLATE]) + detailing.append(t3) t4 = ("textBrowser", "", TYPE_TEXT_BROWSER, DETAILING_DESCRIPTION_LAPJOINT, None) detailing.append(t4) @@ -388,6 +393,10 @@ def output_values(self, flag): self.len_conn if flag else '', True) out_list.append(t20) + t28 = (KEY_OUT_LENGTH_COVER_PLATE, KEY_OUT_DISP_LENGTH_COVER_PLATE, TYPE_TEXTBOX, + self.cover_plate_length if flag else '', True) + out_list.append(t28) + # Populate Hover Dict (Butt Joint Bolted) self.hover_dict["Plate 1"] = ( f"Plate 1
" @@ -403,6 +412,7 @@ def output_values(self, flag): self.hover_dict["Cover Plate"] = ( f"Cover Plate
" + f"Length: {round(float(self.platec.length), 2) if flag and self.platec.length else ''} mm
" f"Width: {round(float(self.platec.height), 2) if flag else ''} mm
" f"Thickness: {round(float(self.platec.thickness_provided), 2) if flag and self.platec.thickness_provided else ''} mm" ) @@ -615,21 +625,23 @@ def set_input_values(self, design_dictionary): cover_plate_type_str = design_dictionary[KEY_COVER_PLATE] self.cover_plate_type = cover_plate_type_str # Store for CAD generation - # Cover plate and packing plate logic as per documentation available_thicknesses = [float(thk) for thk in PLATE_THICKNESS_SAIL] if "double" in cover_plate_type_str.lower(): self.planes = 2 - Tcp = math.ceil((9.0 / 8.0) * Tmin) # Double cover plate thickness as per Eq. 3.2 + # Double cover plate: each plate area ≥ A_main/2 (load shared equally); 5/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + Tcp = math.ceil((5.0 / 8.0) * Tmin) self.calculated_cover_plate_thickness = min( [thk for thk in available_thicknesses if thk >= Tcp], default=Tcp ) - # Packing plate logic as per Cl. 10.3.3.2 - if abs(plate1_thk - plate2_thk) > 0.001: + # Packing plate as per IS 800:2007 Cl. 10.3.3.3; only when user preference is 'Yes' + use_packing = design_dictionary.get(KEY_DP_DETAILING_PACKING_PLATE, 'Yes') == 'Yes' + if use_packing and abs(plate1_thk - plate2_thk) > 0.001: self.packing_plate_thickness = abs(plate1_thk - plate2_thk) if self.packing_plate_thickness > 6.0: - # βpkg calculation as per Eq. 3.3 + # β_pkg = 1 - 0.0125*t_pkg per IS 800:2007 Cl. 10.3.3.3 self.beta_pkg = (1.0 - 0.0125 * self.packing_plate_thickness) else: self.beta_pkg = 1.0 @@ -639,7 +651,9 @@ def set_input_values(self, design_dictionary): elif "single" in cover_plate_type_str.lower(): self.planes = 1 - Tcp = math.ceil((5.0 / 8.0) * Tmin) # Single cover plate thickness as per Eq. 3.1 + # Single cover plate: plate area ≥ A_main (full load alone); 9/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + Tcp = math.ceil((9.0 / 8.0) * Tmin) self.calculated_cover_plate_thickness = min( [thk for thk in available_thicknesses if thk >= Tcp], default=Tcp @@ -906,11 +920,18 @@ def number_r_c_bolts(self,design_dictionary,count=0,hit=0): self.number_bolts = self.rows * self.cols # Calculate connection length (determined by columns along pitch direction) + # Connection length represents the physical length of the bolt group on one side of the joint line. + # Reference: Symmetrical layout rules. N. Subramanian, Design of Steel Structures, Sec. 3.8 / Fig 3.20. if self.cols > 1: self.len_conn = (self.cols - 1) * self.bolt.min_pitch_round + 2 * self.bolt.min_end_dist_round else: self.len_conn = 2 * self.bolt.min_end_dist_round + # Cover plate length spans symmetrically across the joint. + # Formula: L_cp = 2 * L_conn + # Reference: Symmetrical cover plate design. N. Subramanian, Design of Steel Structures, Sec. 3.8 / Fig 3.20. + self.cover_plate_length = 2 * self.len_conn + if self.number_bolts >= 2 and count == 0: self.design_status = True self.check_capacity_reduction_1(design_dictionary) @@ -933,12 +954,12 @@ def check_capacity_reduction_1(self,design_dictionary): if self.bij >= 0.75 and self.bij <= 1.0: self.cap_red = True - self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * self.bij if self.bolt.bolt_type == TYP_BEARING: + self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * self.bij self.bolt.bolt_capacity = min(self.bolt.bolt_shear_capacity, self.bolt.bolt_bearing_capacity) else: - self.slip_res = self.bolt.bolt_shear_capacity - self.bolt.bolt_capacity = self.slip_res + self.bolt.bolt_capacity = self.bolt.bolt_capacity * self.bij + self.slip_res = self.bolt.bolt_capacity self.design_status = True self.check_capacity_reduction_2(design_dictionary) @@ -955,12 +976,12 @@ def check_capacity_reduction_2(self,design_dictionary): if self.blg < self.bij and self.blg != 0: self.cap_red = True - self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * self.blg if self.bolt.bolt_type == TYP_BEARING: + self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * self.blg self.bolt.bolt_capacity = min(self.bolt.bolt_shear_capacity, self.bolt.bolt_bearing_capacity) else: - self.slip_res = self.bolt.bolt_shear_capacity - self.bolt.bolt_capacity = self.slip_res + self.bolt.bolt_capacity = self.bolt.bolt_capacity * self.blg + self.slip_res = self.bolt.bolt_capacity # Continue design with reduced capacity - recursion limit handled in number_r_c_bolts self.number_r_c_bolts(design_dictionary,1,0) @@ -1001,6 +1022,7 @@ def final_formatting(self,design_dictionary): self.final_end_dist = self.bolt.min_end_dist_round # Recalculate connection length for single row self.len_conn = (self.cols - 1) * self.bolt.min_pitch_round + 2 * self.bolt.min_end_dist_round + self.cover_plate_length = 2 * self.len_conn self.design_status = True # Check maximum gauge as per Cl. 10.2.3.1 elif gauge_dist > self.max_gauge_round: @@ -1077,7 +1099,11 @@ def _format_util(value, decimals=3): # Check if utilization ratio is less than 1 for valid design if overall_util >= 1: self.design_status = False + governing = "bolt" if bolt_util >= base_util else "plate (base metal)" + self.design_error = f"Utilization ratio >= 1.0; governed by insufficient {governing} capacity." self.logger.error(": Utilization ratio is greater than or equal to 1. Design is not safe.") + self.logger.error(f": Design unsafe — governed by insufficient {governing} capacity " + f"(UR_bolt = {_format_util(bolt_util)}, UR_plate = {_format_util(base_util)}).") self.logger.info(" :=========End Of design===========") return @@ -1105,8 +1131,8 @@ def _format_util(value, decimals=3): self.plate2.height = plate_width self.plate2.thickness_provided = float(self.plate2thk) - # Cover plate dimensions (same as main plates) - self.platec.length = plate_length + # Cover plate dimensions + self.platec.length = self.cover_plate_length self.platec.height = plate_width self.platec.thickness_provided = float(self.calculated_cover_plate_thickness) @@ -1143,6 +1169,8 @@ def check_base_metal_strength(self): else: n_holes = max(self.cols, 1) hole_dia = self.bolt.dia_hole if hasattr(self.bolt, 'dia_hole') else 0.0 + self.n_holes = n_holes # number of bolt-hole columns; for area report + self.bolt_hole_dia = hole_dia # hole diameter used for net area; for area report net_width = float(self.width) - n_holes * hole_dia if net_width <= 0: @@ -1152,10 +1180,10 @@ def check_base_metal_strength(self): return False self.A_n = plate_thk_min * net_width - shear_lag_factor = 0.7 # IS 800:2007 Cl.6.3.3 for butt joints T_dg = self.A_g * fy / self.gamma_m0 - T_dn = 0.9 * self.A_n * fu * shear_lag_factor / self.gamma_m1 + # Plates: full cross-section connected via cover plates → no shear lag (IS 800:2007 Cl. 6.3) + T_dn = 0.9 * self.A_n * fu / self.gamma_m1 self.T_dg = T_dg self.T_dn = T_dn self.T_db = min(T_dg, T_dn) @@ -1188,7 +1216,7 @@ def check_base_metal_strength(self): def save_design(self, popup_summary): """ - Generate the LaTeX design report for Lap Joint Bolted Connection (Tension/Compression) + Generate the LaTeX design report for Butt Joint Bolted Connection (Tension/Compression) per IS 800:2007. """ try: @@ -1260,6 +1288,7 @@ def as_int(x, default=0): pitch = f2(g('final_pitch', 0.0), 0.0) gauge = f2(g('final_gauge', 0.0), 0.0) e_dist = f2(g('final_edge_dist', 0.0), 0.0) + end_d = f2(g('final_end_dist', 0.0), 0.0) # end distance along load/pitch direction t_fu_fy_list = getattr(self, 'bolt_conn_plates_t_fu_fy', []) if t_fu_fy_list and len(t_fu_fy_list) > 0: @@ -1271,10 +1300,15 @@ def as_int(x, default=0): base_metal_capacity_kN = f2(g('base_metal_capacity_kN', 0.0), 0.0) - A_g = f2(g('A_g', 0.0), 0.0) - T_dg = f2(g('T_dg', 0.0), 0.0) - T_dn = f2(g('T_dn', 0.0), 0.0) - T_db = f2(g('T_db', 0.0), 0.0) + A_g = f2(g('A_g', 0.0), 0.0) + A_n = f2(g('A_n', 0.0), 0.0) + n_holes_disp = int(g('n_holes', max(cols, 1))) + d_hole_disp = f2(g('bolt_hole_dia', 0.0), 0.0) + # self.T_dg/T_dn/T_db are stored in Newtons by check_base_metal_strength(); + # convert to kN for the report (base_metal_capacity_kN is already kN). + T_dg = f2(g('T_dg', 0.0) / 1000.0, 0.0) + T_dn = f2(g('T_dn', 0.0) / 1000.0, 0.0) + T_db = f2(g('T_db', 0.0) / 1000.0, 0.0) overall_ur = round(g('utilization_ratio', 0.0), 3) @@ -1291,11 +1325,14 @@ def as_int(x, default=0): "Type *": bolt_type, f"{'Tensile' if not is_comp else 'Axial'} Force (kN) *": axial_kN, "Additional inputs": "TITLE", - "Bolt Hole Type": getattr(self.bolt, 'boltholetype', 'Standard'), - "Slip Factor (μf)": getattr(self.bolt, 'mu_f', 'N/A'), + "Bolt Hole Type": getattr(self.bolt, 'bolt_hole_type', 'Standard'), "Edge Preparation Method": edge_type } + # Slip factor is only meaningful for friction-grip (HSFG) bolts; omit for bearing bolts. + if bolt_type != "Bearing Bolt": + self.report_input["Slip Factor (μf)"] = getattr(self.bolt, 'mu_f', 'N/A') + self.report_check = [] #============================================================= @@ -1313,18 +1350,26 @@ def as_int(x, default=0): cp_req.append(NoEscape(r'\begin{aligned}')) if "double" in cover_plate_type.lower(): - t_req = math.ceil(1.125 * t_min) # 9/8 * t_min + # Double cover plate: each plate area ≥ A_main/2 (load shared equally); 5/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + t_req = math.ceil(0.625 * t_min) # 5/8 * t_min cp_req.append(NoEscape(r'\text{For Double Cover Plates:}\\')) - cp_req.append(NoEscape(r't_{cp, req} &= \frac{9}{8} \cdot t_{\min}\\')) - cp_req.append(NoEscape(r'&= \frac{9}{8} \times ' + str(t_min) + r'\\')) - cp_req.append(NoEscape(r'&= ' + str(t_req) + r' \text{ mm}\\')) - else: # Single Cover Plate - t_req = math.ceil(0.625 * t_min) # 5/8 * t_min - cp_req.append(NoEscape(r'\text{For Single Cover Plate:}\\')) cp_req.append(NoEscape(r't_{cp, req} &= \frac{5}{8} \cdot t_{\min}\\')) cp_req.append(NoEscape(r'&= \frac{5}{8} \times ' + str(t_min) + r'\\')) cp_req.append(NoEscape(r'&= ' + str(t_req) + r' \text{ mm}\\')) + else: # Single Cover Plate + # Single cover plate: plate area ≥ A_main (full load alone); 9/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + t_req = math.ceil(1.125 * t_min) # 9/8 * t_min + cp_req.append(NoEscape(r'\text{For Single Cover Plate:}\\')) + cp_req.append(NoEscape(r't_{cp, req} &= \frac{9}{8} \cdot t_{\min}\\')) + cp_req.append(NoEscape(r'&= \frac{9}{8} \times ' + str(t_min) + r'\\')) + cp_req.append(NoEscape(r'&= ' + str(t_req) + r' \text{ mm}\\')) + # IS 800:2007 has no explicit cover-plate-thickness clause; this rule is from + # standard design practice (reviewer requested the source be shown in the report). + cp_req.append(NoEscape(r'&[\text{Not in IS 800:2007; ref. N. Subramanian,}\\')) + cp_req.append(NoEscape(r'&\ \text{Design of Steel Structures / IBR practice}]\\')) cp_req.append(NoEscape(r'\end{aligned}')) t_cp_prov = float(self.calculated_cover_plate_thickness) if hasattr(self, 'calculated_cover_plate_thickness') else t_req @@ -1334,7 +1379,22 @@ def as_int(x, default=0): cp_status = "PASS" if t_cp_prov >= t_req else "FAIL" self.report_check.append(["Cover Plate Thickness", cp_req, cp_prov, cp_status]) - # 1.2 Packing Plate (Cl. 10.3.3.2) + # 1.2 Cover Plate Length + # Reference: Symmetrical layout rules. N. Subramanian, Design of Steel Structures, Sec. 3.8. + cp_len_req = Math(inline=True) + cp_len_req.append(NoEscape(r'\begin{aligned}')) + cp_len_req.append(NoEscape(r'L_{cp} &= 2 \cdot L_{conn}\\')) + cp_len_req.append(NoEscape(r'&= 2 \times ' + f'{self.len_conn:.1f}' + r'\\')) + cp_len_req.append(NoEscape(r'&= ' + f'{self.cover_plate_length:.1f}' + r' \text{ mm}\\')) + cp_len_req.append(NoEscape(r'&[\text{Ref. N. Subramanian, Sec. 3.8}]')) + cp_len_req.append(NoEscape(r'\end{aligned}')) + + cp_len_prov = Math(inline=True) + cp_len_prov.append(NoEscape(r'L_{cp, prov} = ' + f'{self.cover_plate_length:.1f}' + r' \text{ mm}')) + + self.report_check.append(["Cover Plate Length", cp_len_req, cp_len_prov, "PASS"]) + + # 1.3 Packing Plate (Cl. 10.3.3.2) packing_thk = float(getattr(self, 'packing_plate_thickness', 0.0)) if packing_thk > 0: pack_req = Math(inline=True) @@ -1359,7 +1419,10 @@ def as_int(x, default=0): d = float(self.bolt.bolt_diameter_provided) bolt_grade = float(self.bolt.bolt_grade_provided) - f_ub = int(bolt_grade * 100) + # fub = first digit of property class x 100 (e.g. 4.6 -> 400, 8.8 -> 800). + # Use the solver's DB value (IS1367_Part3_2002.get_bolt_fu_fy) as source of truth; + # fall back to int(grade)*100 (NOT int(grade*100), which wrongly gives 460 for 4.6). + f_ub = int(getattr(self.bolt, 'bolt_fu', None) or int(bolt_grade) * 100) plate1_thk_raw = float(self.plate1.thickness[0]) if isinstance(self.plate1.thickness, list) else float(self.plate1.thickness) plate2_thk_raw = float(self.plate2.thickness[0]) if isinstance(self.plate2.thickness, list) else float(self.plate2.thickness) @@ -1416,42 +1479,31 @@ def as_int(x, default=0): else: # Bearing Bolt # ========== SHEAR CAPACITY (Cl. 10.3.3) ========== - # Strategy: Use the Solver's final Shear Capacity (bolt_shear_kN) as the source of truth to ensure Report matches Dock. - # Back-calculate the Effective Area (A_eff) that yields this capacity, then display it in the formula. - # This handles cases where Solver uses different Area assumptions (e.g. shank vs net) or different reduction factors. - - V_dsb_kN = bolt_shear_kN - V_nsb_val = V_dsb_kN * gamma_mb - - try: - vals = str(bolt_grade_prov).split('.') - if len(vals) >= 2: - f_ub_val = int(vals[0]) * 100 - else: - f_ub_val = 400 - except (ValueError, TypeError, IndexError): - f_ub_val = 400 - + # Forward calculation matching the solver (component.Bolt.calculate_bolt_capacity): + # V_nsb = f_ub/sqrt(3) * (n_n*A_nb + n_s*A_sb) [Cl. 10.3.3] + # V_dsb(unreduced) = V_nsb / gamma_mb + # V_dsb = beta_lj * beta_lg * V_dsb(unreduced) [Cl. 10.3.3.1 / 10.3.3.2] + # The solver's reduced value (bolt_shear_kN) is the source of truth shown last. n_n = self.planes if hasattr(self, 'planes') else 1 - - # Back-calculate effective area per bolt per plane (forcing n_s=0 for display simplicity) - # V_nsb = (f_ub / sqrt(3)) * (n_n * A_eff) - if n_n > 0 and f_ub_val > 0: - A_eff = (V_nsb_val * 1000.0 * math.sqrt(3.0)) / (f_ub_val * n_n) - else: - A_eff = 0.0 + n_s = 0 + A_nb = f2(getattr(self.bolt, 'bolt_net_area', bolt_net_area), bolt_net_area) + + V_nsb_kN = (f_ub / math.sqrt(3.0)) * (n_n * A_nb + n_s * bolt_shank_area) / 1000.0 + V_dsb_unreduced_kN = V_nsb_kN / gamma_mb + V_dsb_kN = bolt_shear_kN # final, reduced by beta_lj/beta_lg (source of truth) shear_req = Math(inline=True) shear_req.append(NoEscape(r'\begin{aligned}\\')) - shear_req.append(NoEscape(r'V_{dsb} &= \frac{V_{nsb}}{\gamma_{mb}}\\\\')) shear_req.append(NoEscape(r'V_{nsb} &= \frac{f_{ub}}{\sqrt{3}} \cdot (n_n \cdot A_{nb} + n_s \cdot A_{sb})\\')) - shear_req.append(NoEscape(r'&= \frac{' + str(f_ub_val) + r'}{\sqrt{3}} \times (' + str(n_n) + r' \times ' + f'{A_eff:.2f}' + r' + 0)\\')) - shear_req.append(NoEscape(r'&= ' + f'{V_nsb_val:.2f}' + r' \text{ kN}\\\\')) - shear_req.append(NoEscape(r'V_{dsb} &= \frac{' + f'{V_nsb_val:.2f}' + r'}{' + str(gamma_mb) + r'}\\')) + shear_req.append(NoEscape(r'&= \frac{' + str(f_ub) + r'}{\sqrt{3}} \times (' + str(n_n) + r' \times ' + f'{A_nb:.2f}' + r')\\')) + shear_req.append(NoEscape(r'&= ' + f'{V_nsb_kN:.2f}' + r' \text{ kN}\\\\')) + shear_req.append(NoEscape(r'V_{dsb} &= \frac{V_{nsb}}{\gamma_{mb}} = \frac{' + f'{V_nsb_kN:.2f}' + r'}{' + f'{gamma_mb:.2f}' + r'}\\')) + shear_req.append(NoEscape(r'&= ' + f'{V_dsb_unreduced_kN:.2f}' + r' \text{ kN}\\\\')) + shear_req.append(NoEscape(r'V_{dsb,\,red} &= \beta_{lj} \cdot \beta_{lg} \cdot V_{dsb}\\')) shear_req.append(NoEscape(r'&= ' + f'{V_dsb_kN:.2f}' + r' \text{ kN}\\')) - shear_req.append(NoEscape(r'&[\text{Ref. Cl. 10.3.3}]')) + shear_req.append(NoEscape(r'&[\text{Ref. Cl. 10.3.3; reductions below}]')) shear_req.append(NoEscape(r'\end{aligned}')) - + self.report_check.append(["Shear Capacity", "", shear_req, ""]) # ========== BEARING CAPACITY (Cl. 10.3.4) ========== @@ -1733,9 +1785,30 @@ def as_int(x, default=0): ]) self.report_check.append([ - "Bolt Pattern", "2", f"Arrangement: {rows} rows × {cols} columns", "" + "Bolt Pattern", f"{n_bolts}", f"Arrangement: {rows} rows × {cols} columns", "" ]) + # 2.5.2 Connection Length + # Reference: Symmetrical layout rules. N. Subramanian, Design of Steel Structures, Sec. 3.8 / Fig 3.20. + conn_len_req = Math(inline=True) + conn_len_req.append(NoEscape(r'\begin{aligned}')) + if cols > 1: + conn_len_req.append(NoEscape(r'L_{conn} &= (n_c - 1) \cdot p + 2 \cdot e_{end}\\')) + conn_len_req.append(NoEscape(r'&= (' + str(cols) + r' - 1) \times ' + str(pitch) + r' + 2 \times ' + str(end_d) + r'\\')) + else: + conn_len_req.append(NoEscape(r'L_{conn} &= 2 \cdot e_{end}\\')) + conn_len_req.append(NoEscape(r'&= 2 \times ' + str(end_d) + r'\\')) + conn_len_req.append(NoEscape(r'&= ' + f'{self.len_conn:.1f}' + r' \text{ mm}\\')) + conn_len_req.append(NoEscape(r'&\text{(extreme-bolt distance along load} + 2 e_{end};\\')) + conn_len_req.append(NoEscape(r'&\ \text{distinct from } l_j \text{ used for long-joint reduction)}\\')) + conn_len_req.append(NoEscape(r'&[\text{Ref. N. Subramanian, Sec. 3.8}]')) + conn_len_req.append(NoEscape(r'\end{aligned}')) + + conn_len_prov = Math(inline=True) + conn_len_prov.append(NoEscape(r'L_{conn, prov} = ' + f'{self.len_conn:.1f}' + r' \text{ mm}')) + + self.report_check.append(["Connection Length", conn_len_req, conn_len_prov, "PASS"]) + #================================ # Section 2.6: Base Metal Strength #================================ @@ -1743,6 +1816,23 @@ def as_int(x, default=0): "SubSection", "Base Metal Strength", "|p{4cm}|p{5cm}|p{5.5cm}|p{1.5cm}|" ]) + # IS 800:2007 Cl. 6.2: gross section area = width × t_min + area_calc = Math(inline=True) + area_calc.append(NoEscape(r'\begin{aligned}')) + area_calc.append(NoEscape(r'A_g &= b \times t_{\min}\\')) + area_calc.append(NoEscape( + r'&= ' + str(width) + r' \times ' + str(plate_thk_min) + + r' = ' + str(A_g) + r' \text{ mm}^2')) + if not is_comp: + # IS 800:2007 Cl. 6.3: net area deducts bolt holes from gross width + area_calc.append(NoEscape(r'\\A_n &= (b - n_h \cdot d_0) \times t_{\min}\\')) + area_calc.append(NoEscape( + r'&= (' + str(width) + r' - ' + str(n_holes_disp) + + r' \times ' + str(d_hole_disp) + r') \times ' + + str(plate_thk_min) + r' = ' + str(A_n) + r' \text{ mm}^2')) + area_calc.append(NoEscape(r'\end{aligned}')) + self.report_check.append(["Gross / Net Area", "", area_calc, ""]) + if is_comp: base_req = Math(inline=True) base_req.append(NoEscape(r'\begin{aligned}\\')) @@ -1758,25 +1848,21 @@ def as_int(x, default=0): # 1. Gross Section Yielding yield_req = Math(inline=True) yield_req.append(NoEscape(r'\begin{aligned}\\')) - yield_req.append(NoEscape(r'T_{dg} &= \frac{A_g \cdot f_y}{\gamma_{m0}}\\\\')) - yield_req.append(NoEscape(r'&= \frac{' + str(A_g) + r' \times ' + str(fy) + r'}{1.10}\\\\')) + yield_req.append(NoEscape(r'T_{dg} &= \frac{A_g \cdot f_y}{\gamma_{m0} \times 1000}\\\\')) + yield_req.append(NoEscape(r'&= \frac{' + str(A_g) + r' \times ' + str(fy) + r'}{1.10 \times 1000}\\\\')) yield_req.append(NoEscape(r'&= ' + f'{T_dg:.2f}' + r' \text{ kN}\\')) yield_req.append(NoEscape(r'&[\text{Ref. Cl. 6.2}]')) yield_req.append(NoEscape(r'\end{aligned}')) self.report_check.append(["Gross Section Yield", "", yield_req, ""]) # 2. Net Section Rupture - # Back calculate An for display accuracy - # T_dn = 0.9 * An * fu / 1.25 (in kN) - if fu > 0: - An_disp = (T_dn * 1000.0 * 1.25) / (0.9 * fu) - else: - An_disp = 0.0 - + # Plates: full cross-section connected via cover plates → no shear lag (IS 800:2007 Cl. 6.3) rup_req = Math(inline=True) rup_req.append(NoEscape(r'\begin{aligned}\\')) - rup_req.append(NoEscape(r'T_{dn} &= \frac{0.9 A_n f_u}{\gamma_{m1}}\\')) - rup_req.append(NoEscape(r'&= \frac{0.9 \times ' + f'{An_disp:.2f}' + r' \times ' + str(fu) + r'}{1.25}\\')) + rup_req.append(NoEscape(r'T_{dn} &= \frac{0.9 \cdot A_n \cdot f_u}{\gamma_{m1} \times 1000}\\')) + rup_req.append(NoEscape( + r'&= \frac{0.9 \times ' + + str(A_n) + r' \times ' + str(fu) + r'}{1.25 \times 1000}\\')) rup_req.append(NoEscape(r'&= ' + f'{T_dn:.2f}' + r' \text{ kN}\\')) rup_req.append(NoEscape(r'&[\text{Ref. Cl. 6.3}]')) rup_req.append(NoEscape(r'\end{aligned}')) @@ -1834,13 +1920,32 @@ def as_int(x, default=0): bolt_capacity_total = f2(bolt_final_cap * n_bolts, 0.0) bolt_ur = axial_kN / bolt_capacity_total if bolt_capacity_total > 0 else 999.0 - + plate_ur = axial_kN / base_metal_capacity_kN if base_metal_capacity_kN > 0 else 999.0 - + # Overall UR is max of both overall_ur_val = max(bolt_ur, plate_ur) overall_ur = round(overall_ur_val, 3) + # Total bolt-group capacity, shown with intermediate substitution. + # V_db already embeds the long-joint (beta_lj) and large-grip (beta_lg) reductions. + grp_req = Math(inline=True) + grp_req.append(NoEscape(r'\begin{aligned}\\')) + grp_req.append(NoEscape(r'V_{group} &= n \cdot V_{db}\\')) + grp_req.append(NoEscape(r'&\text{(}V_{db}\text{ already includes }\beta_{lj},\beta_{lg}\text{)}\\')) + grp_req.append(NoEscape(r'&= ' + str(n_bolts) + r' \times ' + str(bolt_final_cap) + r'\\')) + grp_req.append(NoEscape(r'&= ' + str(bolt_capacity_total) + r' \text{ kN}')) + grp_req.append(NoEscape(r'\end{aligned}')) + self.report_check.append(["Total Bolt Group Capacity", "", grp_req, ""]) + + # Explicit governing component (which check controls the design). + governing = "Bolt" if bolt_ur >= plate_ur else "Plate" + self.report_check.append([ + "Governing Component", "", + f"{governing} (UR_bolt = {bolt_ur:.3f}, UR_plate = {plate_ur:.3f})", + "" + ]) + ur_req = Math(inline=True) ur_req.append(NoEscape(r'\begin{aligned}\\')) ur_req.append(NoEscape(r'\text{Bolt Capacity} &= ' + str(bolt_capacity_total) + r' \text{ kN}\\')) @@ -1858,10 +1963,10 @@ def as_int(x, default=0): Disp_2d_image = [] Disp_3D_image = "/ResourceFiles/images/3d.png" rel_path = os.path.abspath(".").replace("\\", "/") - fname_no_ext = popup_summary.get("filename", "LapJointBoltedReport") + fname_no_ext = popup_summary.get("filename", "ButtJointBoltedReport") folder = popup_summary.get('folder', './reports') os.makedirs(folder, exist_ok=True) - + CreateLatex.save_latex( CreateLatex(), self.report_input, self.report_check, popup_summary, fname_no_ext, rel_path, Disp_2d_image, Disp_3D_image, diff --git a/src/osdag_core/design_type/connection/butt_joint_welded.py b/src/osdag_core/design_type/connection/butt_joint_welded.py index e806b3c04..e50d752b6 100644 --- a/src/osdag_core/design_type/connection/butt_joint_welded.py +++ b/src/osdag_core/design_type/connection/butt_joint_welded.py @@ -70,6 +70,10 @@ def tab_value_changed(self): def edit_tabs(self): return [] # Keep original empty implementation + def refresh_input_dock(self): + # Butt joint has no section-designation fields; skip Connection's default refresh + return [] + def input_dictionary_design_pref(self): design_input = [] design_input.append(("Weld", TYPE_COMBOBOX, [ @@ -385,7 +389,7 @@ def output_values(self, flag): out_list.append(t38) t28 = (KEY_OUT_LENGTH_COVER_PLATE, KEY_OUT_DISP_LENGTH_COVER_PLATE, TYPE_TEXTBOX, - round(self.weld_length_provided, 1) if flag else '', True) + round(self.cover_plate_length, 1) if flag and hasattr(self, 'cover_plate_length') else '', True) out_list.append(t28) t47 = (KEY_OUT_THICKNESS_COVER_PLATE, KEY_OUT_DISP_THICKNESS_COVER_PLATE, TYPE_TEXTBOX, @@ -419,7 +423,7 @@ def output_values(self, flag): out_list.append(t26) t27 = (KEY_OUT_BOLT_CONN_LEN, KEY_OUT_DISP_BOLT_CONN_LEN, TYPE_TEXTBOX, - round(self.weld_length_provided, 1) if flag else '', True) + round(self.connection_length, 1) if flag and hasattr(self, 'connection_length') else '', True) out_list.append(t27) t29 = (KEY_OUT_DESIGN_FOR, KEY_OUT_DISP_DESIGN_FOR, TYPE_TEXTBOX, @@ -427,7 +431,7 @@ def output_values(self, flag): out_list.append(t29) # Populate Hover Dict (Butt Joint Welded) with actual dimensions - plate_length = getattr(self, 'weld_length_provided', 0) + plate_length = getattr(self, 'cover_plate_length', 0) plate_width = getattr(self, 'plates_width', 0) plate1_thk = float(self.plate1.thickness[0]) if hasattr( self, 'plate1') and self.plate1 and self.plate1.thickness else 0 @@ -650,6 +654,8 @@ def set_input_values(self, design_dictionary): fabrication=design_dictionary.get(KEY_DP_FAB_SHOP, KEY_DP_FAB_SHOP)) # Set weld size after creating the weld object self.weld.size = design_dictionary[KEY_WELD_SIZE] + # Store for report; save_design() reads self.edgetype via getattr(self, 'edgetype', ...) + self.edgetype = design_dictionary.get(KEY_DP_DETAILING_EDGE_TYPE, 'Sheared or hand flame cut') # Start design process print("input values are set. Doing preliminary member checks") self.member_design_status = False @@ -665,27 +671,29 @@ def set_input_values(self, design_dictionary): cover_plate_type_str = design_dictionary[KEY_COVER_PLATE] self.cover_plate_type = cover_plate_type_str # Store for CAD generation - # Cover plate and packing plate logic as per documentation available_thicknesses = [float(thk) for thk in PLATE_THICKNESS_SAIL] if "double" in cover_plate_type_str.lower(): self.planes = 2 - # Double cover plate thickness as per Eq. 3.2 - Tcp = math.ceil((9.0 / 16.0) * Tmin) + # Double cover plate: each plate area ≥ A_main/2 (load shared equally); 5/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + Tcp = math.ceil((5.0 / 8.0) * Tmin) self.calculated_cover_plate_thickness = min( [thk for thk in available_thicknesses if thk >= Tcp], default=Tcp ) - # Packing plate logic as per Cl. 10.3.3.2 - if abs(plate1_thk - plate2_thk) > 0.001: + # Packing plate as per IS 800:2007 Cl. 10.3.3.3; only when user preference is 'Yes' + use_packing = design_dictionary.get(KEY_DP_DETAILING_PACKING_PLATE, 'Yes') == 'Yes' + if use_packing and abs(plate1_thk - plate2_thk) > 0.001: self.packing_plate_thickness = abs(plate1_thk - plate2_thk) else: self.packing_plate_thickness = 0.0 elif "single" in cover_plate_type_str.lower(): self.planes = 1 - # Single cover plate thickness as per Eq. 3.1 - Tcp = math.ceil((5.0 / 8.0) * Tmin) + # Single cover plate: plate area ≥ A_main (full load alone); 9/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + Tcp = math.ceil((9.0 / 8.0) * Tmin) self.calculated_cover_plate_thickness = min( [thk for thk in available_thicknesses if thk >= Tcp], default=Tcp @@ -875,7 +883,8 @@ def weld_length(self, design_dictionary): self.logger.info( ": Straight weld will be provided as required length is less than plate width") self.weld_length_provided = self.plates_width - self.weld_length_effective = self.weld_length_provided + # Apply 2s deduction for end craters per IS 800:2007 Cl. 10.5.4 + self.weld_length_effective = self.weld_length_provided - (2 * self.weld_size) self.weld_angle = 0 self.side_weld_length = 0 @@ -919,6 +928,21 @@ def weld_length(self, design_dictionary): self.logger.info( ": Skewed weld will be provided with angle {:.2f} degrees".format(self.weld_angle)) + # Overlap length of cover plate on each side + # Reference: IS 800:2007 Clause 10.5.1.2 (for lap/overlap length) and Clause 10.5.10 (for return welds) / N. Subramanian, Design of Steel Structures, Sec. 3.10. + self.overlap = max(4 * self.calculated_cover_plate_thickness, 40.0) + if self.side_weld_length > 0: + self.overlap = max(self.overlap, self.side_weld_length + 2 * self.weld_size) + + # Cover plate length spans symmetrically on both sides of the joint line + # Formula: L_cp = 2 * overlap + # Reference: Symmetrical welded splice plate design. N. Subramanian, Design of Steel Structures, Sec. 3.10. + self.cover_plate_length = 2 * self.overlap + + # Connection length represents the length of the weld group along the force direction (longitudinal direction) + # Reference: IS 800:2007 Cl. 10.5.1.2 / N. Subramanian, Design of Steel Structures, Sec. 3.10. + self.connection_length = self.side_weld_length + # Update output values for UI display self.output_values_dict[KEY_OUT_WELD_LENGTH] = self.weld_length_effective @@ -1262,15 +1286,16 @@ def f2(x, default=0.0): "SubSection", "Cover Plate Design", "|p{4cm}|p{4cm}|p{6.5cm}|p{1.5cm}|" ]) - # FIXED ISSUE 1: Correct fraction display - if N_f == 2: # Double cover - tcp_numerator = 9 - tcp_denominator = 16 - tcp_req = f2((9.0 / 16.0) * plate_thk_min, 0.0) - else: # Single cover + # Cover plate thickness: double = 5/8 × Tmin, single = 9/8 × Tmin + # Reference: Indian Machine Design practice (IBR); validate against IS 800:2007 Cl. 10.1 + if N_f == 2: # Double cover: each plate area ≥ 1.05 × A_main/2 tcp_numerator = 5 tcp_denominator = 8 tcp_req = f2((5.0 / 8.0) * plate_thk_min, 0.0) + else: # Single cover: plate area ≥ 1.05 × A_main + tcp_numerator = 9 + tcp_denominator = 8 + tcp_req = f2((9.0 / 8.0) * plate_thk_min, 0.0) tcp_calc = Math(inline=True) tcp_calc.append(NoEscape(r'\begin{aligned}')) @@ -1290,6 +1315,21 @@ def f2(x, default=0.0): self.report_check.append( ["Cover Plate Thickness", tcp_calc, tcp_prov, tcp_status]) + # Cover Plate Length row + # Reference: Symmetrical welded splice plate design. N. Subramanian, Design of Steel Structures, Sec. 3.10. + cp_len_req = Math(inline=True) + cp_len_req.append(NoEscape(r'\begin{aligned}')) + cp_len_req.append(NoEscape(r'L_{cp} &= 2 \cdot l_{lap}\\')) + cp_len_req.append(NoEscape(r'&= 2 \times ' + f'{self.overlap:.1f}' + r'\\')) + cp_len_req.append(NoEscape(r'&= ' + f'{self.cover_plate_length:.1f}' + r' \text{ mm}\\')) + cp_len_req.append(NoEscape(r'&[\text{Ref. N. Subramanian, Sec. 3.10}]')) + cp_len_req.append(NoEscape(r'\end{aligned}')) + + cp_len_prov = Math(inline=True) + cp_len_prov.append(NoEscape(r'L_{cp, prov} = ' + f'{self.cover_plate_length:.1f}' + r' \text{ mm}')) + + self.report_check.append(["Cover Plate Length", cp_len_req, cp_len_prov, 'Pass']) + # Packing plate requirement (if applicable) if abs(plate1_thk - plate2_thk) > 0.001 and N_f == 2: packing_calc = Math(inline=True) @@ -1509,10 +1549,11 @@ def f2(x, default=0.0): # Effective length calculation (Section 3.5, Step 1) eff_len_calc_detail = Math(inline=True) eff_len_calc_detail.append(NoEscape(r'\begin{aligned}')) + # IS 800:2007 Cl. 10.5.4: effective length deducts 2 × weld size for end craters eff_len_calc_detail.append( - NoEscape(r'L_{eff} &= L_{provided\_line} - 2a\\')) + NoEscape(r'L_{eff} &= L_{provided\_line} - 2s \quad [\text{IS 800:2007 Cl. 10.5.4}]\\')) eff_len_calc_detail.append(NoEscape( - r'&= ' + str(L_provided_line) + r' - 2 \times ' + str(effective_throat) + r'\\')) + r'&= ' + str(L_provided_line) + r' - 2 \times ' + str(weld_size) + r'\\')) eff_len_calc_detail.append( NoEscape(r'&= ' + str(L_eff_provided) + r' \text{ mm}\\')) eff_len_calc_detail.append(NoEscape(r'\end{aligned}')) @@ -1520,6 +1561,24 @@ def f2(x, default=0.0): self.report_check.append( ["Effective Length", "", eff_len_calc_detail, ""]) + # Connection Length row + # Reference: IS 800:2007 Cl. 10.5.1.2 / N. Subramanian, Design of Steel Structures, Sec. 3.10. + conn_len_req = Math(inline=True) + conn_len_req.append(NoEscape(r'\begin{aligned}')) + if self.side_weld_length > 0: + conn_len_req.append(NoEscape(r'L_{conn} &= L_{side}\\')) + conn_len_req.append(NoEscape(r'&= ' + f'{self.side_weld_length:.1f}' + r' \text{ mm}\\')) + conn_len_req.append(NoEscape(r'&[\text{Ref. Side weld length}]')) + else: + conn_len_req.append(NoEscape(r'L_{conn} &= 0 \text{ mm}\\')) + conn_len_req.append(NoEscape(r'&[\text{Ref. Transverse weld only}]')) + conn_len_req.append(NoEscape(r'\end{aligned}')) + + conn_len_prov = Math(inline=True) + conn_len_prov.append(NoEscape(r'L_{conn, prov} = ' + f'{self.connection_length:.1f}' + r' \text{ mm}')) + + self.report_check.append(["Connection Length", conn_len_req, conn_len_prov, 'Pass']) + # ========================================================================== # SECTION 3.6: LONG JOINT REDUCTION FACTOR # ========================================================================== @@ -1594,6 +1653,21 @@ def f2(x, default=0.0): "SubSection", "Base Metal Strength Check", "|p{4cm}|p{4cm}|p{6.5cm}|p{1.5cm}|" ]) + # IS 800:2007 Cl. 6.2: gross section area = width × t_min + area_calc = Math(inline=True) + area_calc.append(NoEscape(r'\begin{aligned}')) + area_calc.append(NoEscape(r'A_g &= b \times t_{\min}\\')) + area_calc.append(NoEscape( + r'&= ' + str(width) + r' \times ' + str(plate_thk_min) + + r' = ' + f'{Ag:.1f}' + r' \text{ mm}^2\\')) + # IS 800:2007 Cl. 6.3: no hole deduction for welded joints + area_calc.append(NoEscape(r'A_n &= A_g\\')) + area_calc.append(NoEscape( + r'&= ' + f'{Ag:.1f}' + + r' \text{ mm}^2 \quad \text{(no bolt holes in welded joint)}')) + area_calc.append(NoEscape(r'\end{aligned}')) + self.report_check.append(["Gross / Net Area", "", area_calc, ""]) + if is_comp: # For compression - only yielding check comp_calc_req = Math(inline=True) diff --git a/src/osdag_core/design_type/connection/lap_joint_bolted.py b/src/osdag_core/design_type/connection/lap_joint_bolted.py index e4385fc29..86eed0bc2 100644 --- a/src/osdag_core/design_type/connection/lap_joint_bolted.py +++ b/src/osdag_core/design_type/connection/lap_joint_bolted.py @@ -474,9 +474,9 @@ def set_input_values(self, design_dictionary): material_grade=design_dictionary[KEY_MATERIAL], width=self.width) self.bolt = Bolt(grade=design_dictionary[KEY_GRD], diameter=design_dictionary[KEY_D], bolt_type=design_dictionary[KEY_TYP], - bolt_hole_type=design_dictionary[KEY_DP_BOLT_HOLE_TYPE], - edge_type=design_dictionary[KEY_DP_DETAILING_EDGE_TYPE], - mu_f=design_dictionary.get(KEY_DP_BOLT_SLIP_FACTOR, None), + bolt_hole_type=design_dictionary.get(KEY_DP_BOLT_HOLE_TYPE, "Standard"), + edge_type=design_dictionary.get(KEY_DP_DETAILING_EDGE_TYPE, "Sheared or hand flame cut"), + mu_f=design_dictionary.get(KEY_DP_BOLT_SLIP_FACTOR, "0.3"), ) self.planes = 1 self.count = 0 @@ -742,43 +742,48 @@ def number_r_c_bolts(self, design_dictionary, count=0, hit=0): def check_capacity_reduction_1(self,design_dictionary): # print("Capacity red check 1") + self.bij = 1.0 if self.number_bolts > 2: - lg = (self.rows - 1)*self.bolt.min_pitch_round + lg = (self.cols - 1)*self.bolt.min_pitch_round if lg > 15 * self.bolt.bolt_diameter_provided: self.bij = 1.075 - (lg / (200 * self.bolt.bolt_diameter_provided)) - if self.bij >= 0.75 and self.bij <= 1.0: - self.cap_red = True - # print("1 cap red") - self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * self.bij - if self.bolt.bolt_type == 'Bearing Bolt': - self.bolt.bolt_capacity = min(self.bolt.bolt_shear_capacity, self.bolt.bolt_bearing_capacity) - else: - self.slip_res = self.bolt.bolt_shear_capacity - self.bolt.bolt_capacity = self.slip_res - + if self.bij > 1.0: self.bij = 1.0 + if self.bij < 0.75: self.bij = 0.75 self.design_status = True self.check_capacity_reduction_2(design_dictionary) def check_capacity_reduction_2(self,design_dictionary): self.cap_red = False + self.blg = 1.0 # print("Capacity red check 2") - if self.plate1thk + self.plate2thk > 5 * self.bolt.bolt_diameter_provided: - self.blg = 8 / (3 + (self.plate1thk + self.plate2thk / self.bolt.bolt_diameter_provided)) - if self.blg < self.bij and self.blg != 0: + total_thk = float(self.plate1thk) + float(self.plate2thk) + if total_thk > 8 * self.bolt.bolt_diameter_provided: + self.logger.error(": Grip length exceeds 8 times the nominal diameter of the bolt. [Cl. 10.3.3.2]") + self.design_status = False + self.design_error = "Grip length too large (Cl. 10.3.3.2)" + return + + if total_thk > 5 * self.bolt.bolt_diameter_provided: + self.blg = 8 / (3 + (total_thk / self.bolt.bolt_diameter_provided)) + + if self.blg > self.bij: + self.blg = self.bij + + beta = min(self.bij, self.blg) + + if beta < 1.0: self.cap_red = True - # print("blg",self.blg) - # print("2 cap red") - self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * self.blg + # print("cap red") if self.bolt.bolt_type == 'Bearing Bolt': + self.bolt.bolt_shear_capacity = self.bolt.bolt_shear_capacity * beta self.bolt.bolt_capacity = min(self.bolt.bolt_shear_capacity, self.bolt.bolt_bearing_capacity) else: - self.slip_res = self.bolt.bolt_shear_capacity + self.slip_res = self.slip_res * beta self.bolt.bolt_capacity = self.slip_res self.number_r_c_bolts(design_dictionary,1,0) - - if self.cap_red == False: + else: self.design_status = True # print("Going to formatting") # print("After checks 2 numbolts",self.number_bolts) @@ -840,6 +845,8 @@ def final_formatting(self,design_dictionary): self.final_end_dist = self.bolt.min_end_dist_round self.design_status = True + self.number_bolts = self.rows * self.cols + if self.bolt.bolt_type == 'Bearing Bolt': self.bolt.bolt_shear_capacity = round(self.bolt.bolt_shear_capacity / 1000, 2) self.bolt.bolt_bearing_capacity = round(self.bolt.bolt_bearing_capacity / 1000, 2) @@ -881,19 +888,19 @@ def _format_util(value, decimals=3): if math.isinf(overall_util) or math.isnan(overall_util): self.utilization_ratio = 'Inf' + self.design_status = False + self.design_error = "Utilization ratio is undefined or infinite." else: self.utilization_ratio = round(overall_util, 2) + if overall_util > 1.0: + self.design_status = False + self.design_error = "Utilization ratio exceeds 1.0" self.final_gauge = round(self.final_gauge, 0) self.final_pitch = round(self.final_pitch, 0) self.final_end_dist = round(self.final_end_dist, 0) self.final_edge_dist = round(self.final_edge_dist, 0) - print("FINAL FINAL", self.bolt) - print("Final Edge/End/Gauge/Pitch", self.final_edge_dist, self.final_end_dist, self.final_gauge, self.final_pitch) - print("Max and min end edge dist ", self.bolt.max_end_dist_round, self.bolt.min_end_dist_round, self.bolt.max_edge_dist_round, self.bolt.min_edge_dist_round) - print("Max min gauge pitch dist", self.max_gauge_round, self.bolt.min_gauge_round, self.max_pitch_round, self.bolt.min_pitch_round) - # Set plate dimensions for hover_dict display # plate length = connection length (along the bolt pitch direction) # plate height = plate width (perpendicular to pitch direction) @@ -949,10 +956,9 @@ def check_base_metal_strength(self): return False self.A_n = plate_thk_min * net_width - shear_lag_factor = 0.7 # IS 800:2007 Cl.6.3.3 for lap joints T_dg = self.A_g * fy / self.gamma_m0 - T_dn = 0.9 * self.A_n * fu * shear_lag_factor / self.gamma_m1 + T_dn = 0.9 * self.A_n * fu / self.gamma_m1 self.T_dg = T_dg self.T_dn = T_dn self.T_db = min(T_dg, T_dn) @@ -965,7 +971,7 @@ def check_base_metal_strength(self): T_db_block = IS800_2007.cl_6_4_1_block_shear_strength(A_vg, A_vn, A_tg, A_tn, fu, fy) self.T_db = min(self.T_db, T_db_block) - self.logger.info(f": Design strength of plate in tension = {self.T_db / 1000:.2f} kN [Cl.6.2.2, 6.2.3, 6.3.3]") + self.logger.info(f": Design strength of plate in tension = {self.T_db / 1000:.2f} kN [Cl.6.2, 6.3.1, 6.4.1]") if self.T_db <= 0: self.logger.error(": Plate design strength is non-positive. Check input dimensions/material.") @@ -1120,9 +1126,9 @@ def as_int(x, default=0): base_metal_capacity_kN = f2(g('base_metal_capacity_kN', 0.0), 0.0) A_g = f2(g('A_g', 0.0), 0.0) - T_dg = f2(g('T_dg', 0.0), 0.0) - T_dn = f2(g('T_dn', 0.0), 0.0) - T_db = f2(g('T_db', 0.0), 0.0) + T_dg = f2(g('T_dg', 0.0) / 1000.0, 0.0) + T_dn = f2(g('T_dn', 0.0) / 1000.0, 0.0) + T_db = f2(g('T_db', 0.0) / 1000.0, 0.0) overall_ur = round(g('utilization_ratio', 0.0), 3) @@ -1155,17 +1161,18 @@ def as_int(x, default=0): d = float(self.bolt.bolt_diameter_provided) bolt_grade = float(self.bolt.bolt_grade_provided) - f_ub = int(bolt_grade * 100) + f_ub = as_int(getattr(self.bolt, 'bolt_fu', None), int(bolt_grade) * 100) plate1_thk_raw = float(self.plate1.thickness[0]) if isinstance(self.plate1.thickness, list) else float(self.plate1.thickness) plate2_thk_raw = float(self.plate2.thickness[0]) if isinstance(self.plate2.thickness, list) else float(self.plate2.thickness) bolt_shank_area = f2(math.pi * d**2 / 4, 0.0) - if hasattr(self.bolt, 'bolt_net_area_provided'): - bolt_net_area = f2(self.bolt.bolt_net_area_provided, 0.0) + # Get net tensile stress area A_nb from bolt object or use standard approximation. + if hasattr(self.bolt, 'bolt_net_area') and self.bolt.bolt_net_area: + bolt_net_area = f2(self.bolt.bolt_net_area, 0.0) else: - bolt_net_area = f2(math.pi * (d - 0.9382 * math.sqrt(d))**2 / 4, 0.0) + bolt_net_area = f2(0.78 * math.pi * d**2 / 4, 0.0) gamma_mb = 1.25 @@ -1212,23 +1219,28 @@ def as_int(x, default=0): else: # Bearing Bolt # ========== SHEAR CAPACITY (Cl. 10.3.3) ========== - V_dsb_kN = bolt_shear_kN - V_nsb = V_dsb_kN * gamma_mb - n_n = 1 # Threads intercepting shear plane n_s = 0 # No threads without shear - + + # Nominal & design shear per Cl. 10.3.3, BEFORE long-joint/long-grip reductions. + V_nsb_kN = (f_ub / math.sqrt(3)) * (n_n * bolt_net_area + n_s * bolt_shank_area) / 1000.0 + V_dsb_unreduced_kN = V_nsb_kN / gamma_mb + + # Design shear capacity actually used in the design (reduced by beta_lj / beta_lg). + V_dsb_kN = bolt_shear_kN + shear_req = Math(inline=True) shear_req.append(NoEscape(r'\begin{aligned}\\')) - shear_req.append(NoEscape(r'V_{dsb} &= \frac{V_{nsb}}{\gamma_{mb}}\\\\')) shear_req.append(NoEscape(r'V_{nsb} &= \frac{f_{ub}}{\sqrt{3}} \cdot (n_n \cdot A_{nb} + n_s \cdot A_{sb})\\')) shear_req.append(NoEscape(r'&= \frac{' + str(f_ub) + r'}{\sqrt{3}} \times (1 \times ' + f'{bolt_net_area:.2f}' + r')\\')) - shear_req.append(NoEscape(r'&= ' + f'{V_nsb:.2f}' + r' \text{ kN}\\\\')) - shear_req.append(NoEscape(r'V_{dsb} &= \frac{' + f'{V_nsb:.2f}' + r'}{' + str(gamma_mb) + r'}\\')) + shear_req.append(NoEscape(r'&= ' + f'{V_nsb_kN:.2f}' + r' \text{ kN}\\\\')) + shear_req.append(NoEscape(r'V_{dsb} &= \frac{V_{nsb}}{\gamma_{mb}} = \frac{' + f'{V_nsb_kN:.2f}' + r'}{' + f'{gamma_mb:.2f}' + r'}\\')) + shear_req.append(NoEscape(r'&= ' + f'{V_dsb_unreduced_kN:.2f}' + r' \text{ kN}\\\\')) + shear_req.append(NoEscape(r'V_{dsb,\,red} &= \beta_{lj} \cdot \beta_{lg} \cdot V_{dsb}\\')) shear_req.append(NoEscape(r'&= ' + f'{V_dsb_kN:.2f}' + r' \text{ kN}\\')) - shear_req.append(NoEscape(r'&[\text{Ref. Cl. 10.3.3}]')) + shear_req.append(NoEscape(r'&[\text{Ref. Cl. 10.3.3; reductions below}]')) shear_req.append(NoEscape(r'\end{aligned}')) - + self.report_check.append(["Shear Capacity", "", shear_req, ""]) # ========== BEARING CAPACITY (Cl. 10.3.4) ========== @@ -1308,7 +1320,7 @@ def as_int(x, default=0): "SubSection", "Reduction Factors", "|p{4cm}|p{5cm}|p{5.5cm}|p{1.5cm}|" ]) - l_j = (self.rows - 1) * self.final_pitch if self.rows > 1 else 0 + l_j = (self.cols - 1) * self.final_pitch if self.cols > 1 else 0 d = self.bolt.bolt_diameter_provided lj_req = Math(inline=True) @@ -1496,7 +1508,7 @@ def as_int(x, default=0): ]) self.report_check.append([ - "Bolt Pattern", "2", f"Arrangement: {rows} rows × {cols} columns", "" + "Bolt Pattern", f"{n_bolts}", f"Arrangement: {rows} rows × {cols} columns", "" ]) #================================ @@ -1521,8 +1533,8 @@ def as_int(x, default=0): # 1. Gross Section Yielding yield_req = Math(inline=True) yield_req.append(NoEscape(r'\begin{aligned}\\')) - yield_req.append(NoEscape(r'T_{dg} &= \frac{A_g \cdot f_y}{\gamma_{m0}}\\\\')) - yield_req.append(NoEscape(r'&= \frac{' + str(A_g) + r' \times ' + str(fy) + r'}{1.10}\\\\')) + yield_req.append(NoEscape(r'T_{dg} &= \frac{A_g \cdot f_y}{\gamma_{m0} \times 1000}\\\\')) + yield_req.append(NoEscape(r'&= \frac{' + str(A_g) + r' \times ' + str(fy) + r'}{1.10 \times 1000}\\\\')) yield_req.append(NoEscape(r'&= ' + f'{T_dg:.2f}' + r' \text{ kN}\\')) yield_req.append(NoEscape(r'&[\text{Ref. Cl. 6.2}]')) yield_req.append(NoEscape(r'\end{aligned}')) @@ -1531,9 +1543,9 @@ def as_int(x, default=0): # 2. Net Section Rupture rup_req = Math(inline=True) rup_req.append(NoEscape(r'\begin{aligned}')) - rup_req.append(NoEscape(r'T_{dn} &= \frac{0.9 A_n f_u}{\gamma_{m1}}\\')) + rup_req.append(NoEscape(r'T_{dn} &= \frac{0.9 \cdot A_n \cdot f_u}{\gamma_{m1} \times 1000}\\')) rup_req.append(NoEscape(r'&= ' + f'{T_dn:.2f}' + r' \text{ kN}\\')) - rup_req.append(NoEscape(r'&[\text{Ref. Cl. 6.3}]')) + rup_req.append(NoEscape(r'&[\text{Ref. Cl. 6.3.1}]')) rup_req.append(NoEscape(r'\end{aligned}')) self.report_check.append(["Net Section Rupture", "", rup_req, ""]) diff --git a/src/osdag_core/design_type/connection/lap_joint_welded.py b/src/osdag_core/design_type/connection/lap_joint_welded.py index b14b6d6c6..6becd99e6 100644 --- a/src/osdag_core/design_type/connection/lap_joint_welded.py +++ b/src/osdag_core/design_type/connection/lap_joint_welded.py @@ -543,18 +543,12 @@ def calculate_weld_length(self): # Required effective weld length (Cl.10.5.4.1) self.weld_length_required = self.tensile_force / (2 * self.fillet_weld_design_strength) self.leff_min = max(4 * self.weld_size, 40) # Cl.10.5.4.1 - self.leff_max = 70 * self.weld_size # Cl.10.5.4.1 self.logger.info(f": Required effective weld length = {self.weld_length_required:.2f} mm") self.logger.info(f": Minimum effective weld length = {self.leff_min} mm [Cl.10.5.4.1]") - self.logger.info(f": Maximum effective weld length = {self.leff_max} mm [Cl.10.5.4.1]") - # Check min/max + # Check min if self.weld_length_required < self.leff_min: self.l_eff = self.leff_min self.logger.warning(f": Required length is less than minimum, using l_eff = {self.l_eff} mm [Cl.10.5.4.1]") - elif self.weld_length_required > self.leff_max: - self.logger.error(": Required weld length exceeds maximum allowed. Increase weld size. [Cl.10.5.4.1]") - self.design_status = False - return False # Design fails - let GUI show error via logs else: self.l_eff = self.weld_length_required self.logger.info(": Required weld length is within limits (Pass)") @@ -577,10 +571,6 @@ def check_long_joint(self): if l_req_modified < self.leff_min: self.logger.warning(f": Modified required weld length {l_req_modified:.2f} mm is less than minimum effective length {self.leff_min} mm [Cl.10.5.4.1]") self.l_eff = self.leff_min - elif l_req_modified > self.leff_max: - self.logger.error(": Modified required weld length exceeds maximum allowed. Increase weld size. [Cl.10.5.4.1]") - self.design_status = False - return False # Design fails - let GUI show error via logs else: self.l_eff = l_req_modified # End return length (Cl.10.5.4.5): min(2*s, 12mm) @@ -617,13 +607,12 @@ def check_base_metal_strength(self, design_dictionary): self.T_db = self.A_g * self.plate1.fy / self.gamma_m0 self.logger.info(f": Design strength of plate in compression = {self.T_db/1000:.2f} kN [Cl.7.1.2]") else: - # Tension: yielding and rupture, take minimum (Cl.6.2.2, 6.2.3, 6.3.3) - # Shear lag factor (Cl.6.3.3): For lap joints, net section efficiency = 0.7 - shear_lag_factor = 0.7 + # Tension: yielding and rupture, take minimum (Cl.6.2.2, 6.3.1) + # For welded flat plates, A_n = A_g and no shear lag factor applies. T_dg = self.A_g * self.plate1.fy / self.gamma_m0 # Gross section yielding (Cl.6.2.2) - T_dn = 0.9 * self.A_g * self.plate1.fu * shear_lag_factor / self.gamma_m1 # Net section rupture (Cl.6.2.3, 6.3.3) + T_dn = 0.9 * self.A_g * self.plate1.fu / self.gamma_m1 # Net section rupture (Cl.6.3.1) self.T_db = min(T_dg, T_dn) - self.logger.info(f": Design strength of plate in tension = {self.T_db/1000:.2f} kN [Cl.6.2.2, 6.2.3, 6.3.3]") + self.logger.info(f": Design strength of plate in tension = {self.T_db/1000:.2f} kN [Cl.6.2.2, 6.3.1]") self.utilization_ratios['base_metal'] = self.axial_force / self.T_db if self.T_db > 0 else float('inf') @@ -708,7 +697,6 @@ def f2(x, default=0.0): # Weld lengths l_eff = f2(g('l_eff', g('weld_length_effective', g('weldlengtheffective', 0.0))), 0.0) l_eff_min = f2(max(4 * weld_size, 40), 0.0) - l_eff_max = f2(70 * weld_size, 0.0) # Long joint reduction factor beta_lw = f2(g('beta_lw', g('betalw', 1.0)), 1.0) @@ -731,7 +719,7 @@ def f2(x, default=0.0): base_metal_capacity_kN = f2((Ag * fy / gamma_m0) / 1000, 0.0) else: Tdg = (Ag * fy / gamma_m0) / 1000 - Tdn = (0.9 * Ag * fu * 0.7 / gamma_m1) / 1000 + Tdn = (0.9 * Ag * fu / gamma_m1) / 1000 base_metal_capacity_kN = f2(min(Tdg, Tdn), 0.0) # Retrieve calculated unit design strengths to match Dock @@ -881,16 +869,13 @@ def f2(x, default=0.0): eff_len_req.append(NoEscape(r'l_{\text{eff,min}} &= \max(4s, 40)\\')) # Step 1: Formula eff_len_req.append(NoEscape(r'&= \max(4 \times ' + str(weld_size) + r', 40)\\')) # Step 2: Substitution eff_len_req.append(NoEscape(r'&= ' + str(l_eff_min) + r' \text{ mm}\\')) # Step 3: Result - eff_len_req.append(NoEscape(r'l_{\text{eff,max}} &= 70s\\')) - eff_len_req.append(NoEscape(r'&= 70 \times ' + str(weld_size) + r'\\')) - eff_len_req.append(NoEscape(r'&= ' + str(l_eff_max) + r' \text{ mm}\\')) eff_len_req.append(NoEscape(r'&[\text{Ref. Cl. 10.5.3}]')) eff_len_req.append(NoEscape(r'\end{aligned}')) eff_len_prov = Math(inline=True) eff_len_prov.append(NoEscape(r'l_{\text{eff}} = ' + str(l_eff) + r' \text{ mm}')) - eff_status = "PASS" if (l_eff_min <= l_eff <= l_eff_max) else "FAIL" + eff_status = "PASS" if (l_eff_min <= l_eff) else "FAIL" self.report_check.append(["Length Limits", eff_len_req, eff_len_prov, eff_status]) # 3.2.3 End Returns @@ -976,7 +961,9 @@ def f2(x, default=0.0): ten_req = Math(inline=True) ten_req.append(NoEscape(r'\begin{aligned}')) ten_req.append(NoEscape(r'T_{dg} &= \frac{A_g f_y}{\gamma_{m0}} = ' + f'{Tdg:.2f}' + r' \text{ kN}\\')) - ten_req.append(NoEscape(r'T_{dn} &= \frac{0.9 A_g f_u \beta}{\gamma_{m1}} = ' + f'{Tdn:.2f}' + r' \text{ kN}\\')) + ten_req.append(NoEscape(r'T_{dn} &= \frac{0.9 A_n f_u}{\gamma_{m1}}\text{ (where } A_n = A_g)\\\\')) + ten_req.append(NoEscape(r'&= \frac{0.9 \times ' + str(Ag) + r' \times ' + str(fu) + r'}{' + str(gamma_m1) + r'}\\\\')) + ten_req.append(NoEscape(r'&= ' + f'{Tdn:.2f}' + r' \text{ kN}\\\\')) ten_req.append(NoEscape(r'T_d &= \min(T_{dg}, T_{dn}) = ' + str(base_metal_capacity_kN) + r' \text{ kN}\\')) ten_req.append(NoEscape(r'&[\text{Ref. Cl. 6.2, 6.3}]')) ten_req.append(NoEscape(r'\end{aligned}')) diff --git a/src/osdag_core/design_type/main.py b/src/osdag_core/design_type/main.py index a9022ac0a..038725bdd 100644 --- a/src/osdag_core/design_type/main.py +++ b/src/osdag_core/design_type/main.py @@ -39,7 +39,7 @@ def refresh_designation_additional_inputs(self, table): def bolt_values(self, input_dictionary): - if not input_dictionary or input_dictionary[KEY_TYP] == 'Bearing Bolt': + if not input_dictionary or input_dictionary.get(KEY_TYP, 'Bearing Bolt') == 'Bearing Bolt': bolt_tension_type = 'Non pre-tensioned' else: bolt_tension_type = 'Pre-tensioned' @@ -89,7 +89,7 @@ def weld_values(self, input_dictionary): values = {KEY_DP_WELD_FAB: KEY_DP_FAB_SHOP, KEY_DP_WELD_MATERIAL_G_O: ''} - if not input_dictionary or input_dictionary[KEY_MATERIAL] == 'Select Material': + if not input_dictionary or input_dictionary.get(KEY_MATERIAL, 'Select Material') == 'Select Material': pass else: values[KEY_DP_WELD_MATERIAL_G_O] = Material(input_dictionary[KEY_MATERIAL]).fu diff --git a/src/osdag_core/utils/common/component.py b/src/osdag_core/utils/common/component.py index ee7b03626..44e37e577 100644 --- a/src/osdag_core/utils/common/component.py +++ b/src/osdag_core/utils/common/component.py @@ -30,7 +30,10 @@ def __init__(self, grade=None, diameter=None, bolt_type="", bolt_hole_type="Stan self.bolt_type = bolt_type self.bolt_hole_type = bolt_hole_type self.edge_type = edge_type - self.mu_f = float(mu_f) + try: + self.mu_f = float(mu_f) if mu_f not in (None, '') else 0.3 + except (TypeError, ValueError): + self.mu_f = 0.3 if bolt_type == "Bearing Bolt": bolt_tensioning = 'Non pre-tensioned' diff --git a/src/osdag_gui/ui/windows/template_page.py b/src/osdag_gui/ui/windows/template_page.py index 64882b35c..f5ec5dace 100644 --- a/src/osdag_gui/ui/windows/template_page.py +++ b/src/osdag_gui/ui/windows/template_page.py @@ -1781,6 +1781,8 @@ def design_fn(self, op_list, data_list, main): # print(f"design_fn input_type = {input_type}\n") # print(f"design_fn input_list = {input_list}\n") # print(f"design_fn tab = {tab}\n") + if tab is None: + continue for key_name in input_list: key = tab.findChild(QWidget, key_name) if key is None: @@ -2011,8 +2013,11 @@ def design_preferences(self): master_widget = self.input_dock.input_widget.findChild(QWidget, master_key) if master_widget and master_widget.currentText() not in value: continue + _widget = self.input_dock.input_widget.findChild(QWidget, key_name) + if _widget is None: + continue self.refresh_section( - prev=self.input_dock.input_widget.findChild(QWidget, key_name).currentText(), + prev=_widget.currentText(), key_name=key_name, key_type=key_type, tab_key=tab_key,