diff --git a/src/osdag_core/Common.py b/src/osdag_core/Common.py index 32b277ca6..ef61a8c74 100644 --- a/src/osdag_core/Common.py +++ b/src/osdag_core/Common.py @@ -874,7 +874,7 @@ def is_valid_custom(self): VALUES_SUPPORTING_OPTIONS_RS = ['Profiled Metal sheeting', 'Plastered sheeting'] VALUES_SUPPORTING_OPTIONS_GNT = ['Crane'] VALUES_SUPPORTING_OPTIONS_FRC = ['Elements not susceptible to cracking', 'Element susceptible to cracking'] -VALUES_SUPPORTING_OPTIONS_DEF = ['NA'] +VALUES_SUPPORTING_OPTIONS_DEF = ['Not Applicable'] KEY_MAX_DEFL = 'Deflection.Max' KEY_DISP_MAX_DEFL = 'Maximum Deflection' VALUES_MAX_DEFL = ['Span/600','Span/800','Span/400','Span/300','Span/360','Span/150','Span/180','Span/240','Span/120','Span/500','Span/750','Span/1000'] diff --git a/src/osdag_core/cad/common_logic.py b/src/osdag_core/cad/common_logic.py index 77cd9a551..96441a22d 100644 --- a/src/osdag_core/cad/common_logic.py +++ b/src/osdag_core/cad/common_logic.py @@ -2065,9 +2065,16 @@ def createPlateGirderCAD(self): # Stiffener Logic include_intermediate_stiffeners = True stiffener_spacing_val = getattr(Conn, 'c', 'N/A') - + + # Web philosophy is the single source of truth: 'Thick Web without ITS' has no + # intermediate transverse stiffeners, so never draw them regardless of self.c. + web_philosophy = getattr(Conn, 'web_philosophy', '') + # Check if stiffener spacing is valid - if stiffener_spacing_val in ['NA', 'N/A', None, '', '0'] or isinstance(stiffener_spacing_val, str) and not stiffener_spacing_val.replace('.', '', 1).isdigit(): + if web_philosophy == 'Thick Web without ITS': + include_intermediate_stiffeners = False + stiffener_spacing = 750.0 + elif stiffener_spacing_val in ['NA', 'N/A', None, '', '0'] or isinstance(stiffener_spacing_val, str) and not stiffener_spacing_val.replace('.', '', 1).isdigit(): include_intermediate_stiffeners = False stiffener_spacing = 750.0 # Default for creating the spacing if needed (though skipped) else: diff --git a/src/osdag_core/design_type/member.py b/src/osdag_core/design_type/member.py index f39a66e9f..a4d3574d6 100644 --- a/src/osdag_core/design_type/member.py +++ b/src/osdag_core/design_type/member.py @@ -2884,32 +2884,19 @@ def optimization_tab_welded_plate_girder_design(self, input_dictionary): optimum = [] - t2 = ( - KEY_EFFECTIVE_AREA_PARA, KEY_DISP_EFFECTIVE_AREA_PARA, TYPE_TEXTBOX, None, values[KEY_EFFECTIVE_AREA_PARA]) - optimum.append(t2) - t1 = (KEY_ALLOW_CLASS, KEY_DISP_CLASS, TYPE_COMBOBOX, ['Yes', 'No'], values[KEY_ALLOW_CLASS]) optimum.append(t1) t1 = (KEY_LOAD, KEY_DISP_LOAD, TYPE_COMBOBOX, KEY_DISP_LOAD_list, values[KEY_LOAD]) optimum.append(t1) - t2 = ( - KEY_LENGTH_OVERWRITE, KEY_DISPP_LENGTH_OVERWRITE, TYPE_TEXTBOX, None, values[KEY_LENGTH_OVERWRITE]) - optimum.append(t2) - doc_text = """ -

Effective Area Parameter is the parameter used to define the reduction in the area of the section due to - connection detailing and other such requirements. The default value of this parameter is set at 1.0, which means - that the effective area is 100% of the gross area for Plastic, Compact and Semi-compact sections.

-

For Slender sections, the initial area will be computed based on the recommendations in Fig.2B of the National - Building Code (2016). The value of the parameter should be defined in terms of the effective area to be considered for - design simulation after deducting the area lost.

-

The maximum value of the parameter is 1.0 (effective area is 100% of the gross area) with a minimum value of 0.1.

-
-

Effective Length is the parameter used to overwrite the length multiplier. The default value of this ratio is set at NA. - The value can be re-defined for any particular design session with a minimum of 0.1. If an invalid value is given, it is set to NA or 1.0.

-

For simply supported beams of overall depth D and span length L, the effective length LLT is given by the table below.

+

Allow Class selects whether the optimiser may consider sections of the given + classification. Type of Load selects the loading condition used when deriving the + lateral-torsional-buckling effective length for the welded plate girder.

+

The effective length of the plate girder is derived automatically from the end + restraints (torsional/warping) and the loading condition; no manual length or area + override is required for this module.

""" t9 = ("textBrowser", "", TYPE_TEXT_BROWSER, doc_text , None) @@ -2921,7 +2908,7 @@ def Stiffener_design(self, input_dictionary): optimum = [] values = {KEY_IntermediateStiffener:'Yes', KEY_IntermediateStiffener_spacing:'NA', - KEY_LongitudnalStiffener:'Yes', + KEY_LongitudnalStiffener:'No', KEY_IntermediateStiffener_thickness:'6', KEY_LongitudnalStiffener_thickness:'6', KEY_ShearBucklingOption : KEY_DISP_SB_Option[0] @@ -2936,7 +2923,7 @@ def Stiffener_design(self, input_dictionary): t8 = (KEY_IntermediateStiffener_spacing, KEY_DISP_IntermediateStiffener_spacing, TYPE_TEXTBOX, None, values[KEY_IntermediateStiffener_spacing]) optimum.append(t8) - t9 = (KEY_LongitudnalStiffener, KEY_DISP_LongitudnalStiffener, TYPE_COMBOBOX, ['Yes','No'], values[KEY_LongitudnalStiffener]) + t9 = (KEY_LongitudnalStiffener, KEY_DISP_LongitudnalStiffener, TYPE_COMBOBOX, ['No', 'Yes and 1 stiffener', 'Yes and 2 stiffeners'], values[KEY_LongitudnalStiffener]) optimum.append(t9) t10 = (KEY_IntermediateStiffener_thickness, KEY_DISP_IntermediateStiffener_thickness, TYPE_COMBOBOX, ['All','Customized'], values[KEY_IntermediateStiffener_thickness]) optimum.append(t10) @@ -3041,8 +3028,16 @@ def tab_girder_sec(self, input_dictionary): web_thickness = str(web_thk_list[0]) thickness_for_mat = float(web_thk_list[0]) else: - web_thickness = str(web_thk_list) if web_thk_list else '10' - thickness_for_mat = float(web_thk_list) if web_thk_list else 20 + try: + thickness_for_mat = float(web_thk_list) + web_thickness = str(web_thk_list) + except (TypeError, ValueError): + # Non-numeric marker such as 'Customized' (Optimized mode). The real + # customized thickness list is consumed by the optimizer through its + # own path; use a default here so the design-preference preview can + # still estimate the section Fy instead of crashing on float(). + web_thickness = '10' + thickness_for_mat = 20 else: web_thickness = '10' thickness_for_mat = 20 @@ -3075,7 +3070,12 @@ def tab_girder_sec(self, input_dictionary): if isinstance(tf_top_list, list) and len(tf_top_list) > 0: top_flange_thickness = str(tf_top_list[0]) else: - top_flange_thickness = str(tf_top_list) if tf_top_list else '15' + try: + float(tf_top_list) + top_flange_thickness = str(tf_top_list) + except (TypeError, ValueError): + # 'Customized' / other non-numeric marker -> use default for preview + top_flange_thickness = '15' else: top_flange_thickness = '15' @@ -3093,7 +3093,12 @@ def tab_girder_sec(self, input_dictionary): if isinstance(tf_bot_list, list) and len(tf_bot_list) > 0: bottom_flange_thickness = str(tf_bot_list[0]) else: - bottom_flange_thickness = str(tf_bot_list) if tf_bot_list else '20' + try: + float(tf_bot_list) + bottom_flange_thickness = str(tf_bot_list) + except (TypeError, ValueError): + # 'Customized' / other non-numeric marker -> use default for preview + bottom_flange_thickness = '20' else: bottom_flange_thickness = '20' diff --git a/src/osdag_core/design_type/plate_girder/checks/deflection.py b/src/osdag_core/design_type/plate_girder/checks/deflection.py index 3ead32836..daae2929f 100644 --- a/src/osdag_core/design_type/plate_girder/checks/deflection.py +++ b/src/osdag_core/design_type/plate_girder/checks/deflection.py @@ -125,7 +125,14 @@ def evaluate_deflection_kNm_mm(M_kNm, L, E, case, criteria, total_depth, top_fla # For now raising error to be caught or debugged if format is weird. raise ValueError(f"Invalid deflection criteria format: {criteria}") else: - n = float(criteria) + try: + n = float(criteria) + except (TypeError, ValueError): + # Guard against non-numeric criteria (e.g. 'NA', '') reaching the + # check via the optimization/PSO path, which bypasses the interactive + # validation in func_for_validation. Fail cleanly instead of with an + # opaque float() traceback. + raise ValueError(f"Invalid deflection criteria: {criteria}") allowable = L / n # mm # Check serviceability diff --git a/src/osdag_core/design_type/plate_girder/checks/shear.py b/src/osdag_core/design_type/plate_girder/checks/shear.py index dbf7b74ff..138dff25f 100644 --- a/src/osdag_core/design_type/plate_girder/checks/shear.py +++ b/src/osdag_core/design_type/plate_girder/checks/shear.py @@ -267,14 +267,11 @@ def tension_field_end_stiffener(d, tw, fyw, shear_force, moment, c, web_philosop bottom_flange_thickness, Nf, gamma_m0, A_vg, tau_b) - V_tf= result[8] - V_dp = (d * tw * fyw * math.sqrt(3)) - denom = V_tf - V_cr - if denom == 0: denom = 1e-6 # Avoid division by zero - rad = 1.0 - (V_cr - V_dp) / denom - if rad < 0: - return False, 0, 0, 0, 0, 0, IntStiffnerwidth, 0 # Fail - H_q = (shear_force - V_cr) / denom + V_tf = result[8] + V_dp = (d * tw * fyw) / math.sqrt(3) + rad = 1.0 - (V_cr / V_dp) if V_dp > 0 else 0 + if rad < 0: rad = 0 + H_q = 1.5 * V_dp * math.sqrt(rad) R_tf = H_q / 2 A_v= d * tw V_n= (fyw * A_v) /( math.sqrt(3) * gamma_m0) @@ -458,13 +455,12 @@ def end_panel_stiffener_calc(Bf_top, Bf_bot, tw, tq, fy, gamma_m0, d, tf_top, to V_tf = (A_vg * tau_b + 0.9 * w_tf * tw * fv * sinφ) V_p = d * tw * fy / (math.sqrt(3) * gamma_m0) V_tf = min(V_tf, V_p) - V_dp = (d * tw * fy / math.sqrt(3)) + V_dp = (d * tw * fy) / math.sqrt(3) - rad = 1.0 - (V_cr / V_dp) - if rad < 0: - return False, end_stiffwidth, end_stiffthickness, 0, 0, 0, 0 # Fail + rad = 1.0 - (V_cr / V_dp) if V_dp > 0 else 0 + if rad < 0: rad = 0 - H_q = 1.25 * V_dp * math.sqrt(rad) + H_q = 1.5 * V_dp * math.sqrt(rad) R_tf = H_q / 2 A_v = d * tw V_n = (fy * A_v) / (math.sqrt(3) * gamma_m0) @@ -577,11 +573,14 @@ def check_longitudinal_stiffener_required(d, tw, c, epsilon, debug=False): """ import math - # Handle c value - if c is None or c == 'NA' or c == 0: + # Handle c value (may be 'NA'/'N/A' for thick web, or non-numeric user text) + if c is None or c in ('NA', 'N/A') or c == 0: c_val = 3.0 * d # Conservative large value if undefined else: - c_val = float(c) + try: + c_val = float(c) + except (ValueError, TypeError): + c_val = 3.0 * d d_tw_ratio = d / tw diff --git a/src/osdag_core/design_type/plate_girder/checks/web_crippling.py b/src/osdag_core/design_type/plate_girder/checks/web_crippling.py index 6cc37ad61..e174d58cb 100644 --- a/src/osdag_core/design_type/plate_girder/checks/web_crippling.py +++ b/src/osdag_core/design_type/plate_girder/checks/web_crippling.py @@ -50,23 +50,26 @@ def check_web_crippling_IS800(shear_force, b1, tw, fy, d, tf, gamma_m0, logger, # DDCL Eq 2.45: P_crip = (b1 + n2) * tw * fyw / gamma_m0 P_crip = (b1 + n2) * tw * fy / gamma_m0 - - # Additional slenderness warning - if d/tw > 200: - logger.warning("Web slenderness ratio (d/tw) exceeds 200. Additional stiffening may be required.") - + + # NOTE: The web-slenderness (d/tw > 200) warning is intentionally NOT logged here. + # Like the crippling-shortfall warning below, this function runs once per particle + # per PSO iteration; logging here floods the design log. It is logged once per run + # in PlateGirderWelded.set_input_values instead. + # Safety check is_safe = P_crip >= shear_force - + if debug: print(f"[DEBUG] Web Crippling (IS 800 Cl. 8.7.4):") print(f"[DEBUG] b1={b1:.2f}, n2={n2:.2f}, tw={tw:.2f}, fy={fy:.2f}") print(f"[DEBUG] P_crip = ({b1:.2f} + {n2:.2f}) * {tw:.2f} * {fy:.2f} / {gamma_m0} = {P_crip:.2f} N") print(f"[DEBUG] Applied={shear_force:.2f} N, Ratio={shear_force/P_crip if P_crip > 0 else 100:.4f}") - - if not is_safe: - logger.warning(f"Web crippling resistance ({P_crip:.2f} N) is less than factored load ({shear_force:.2f} N)") - + + # NOTE: The "resistance less than factored load" warning is intentionally NOT logged here. + # This function is evaluated once per particle per iteration during PSO optimization, which + # would flood the design log with identical messages. The caller logs it once for the final + # design (see PlateGirderWelded.design_check / web_crippling_warning_logged). + return is_safe, P_crip except Exception as e: diff --git a/src/osdag_core/design_type/plate_girder/core/plate_girder.py b/src/osdag_core/design_type/plate_girder/core/plate_girder.py index 4e2945591..3975b290f 100644 --- a/src/osdag_core/design_type/plate_girder/core/plate_girder.py +++ b/src/osdag_core/design_type/plate_girder/core/plate_girder.py @@ -153,7 +153,7 @@ def tab_value_changed(self): t9 = ("Deflection", [KEY_STR_TYPE], [KEY_MEMBER_OPTIONS], TYPE_COMBOBOX, self.member_options_change) change_tab.append(t9) - t9 = ("Deflection", [KEY_MEMBER_OPTIONS], [KEY_SUPPORTING_OPTIONS], TYPE_COMBOBOX, self.supp_options_change) + t9 = ("Deflection", [KEY_STR_TYPE, KEY_MEMBER_OPTIONS], [KEY_SUPPORTING_OPTIONS], TYPE_COMBOBOX, self.supp_options_change) change_tab.append(t9) t9 = ("Deflection", [KEY_STR_TYPE,KEY_DESIGN_LOAD,KEY_MEMBER_OPTIONS,KEY_SUPPORTING_OPTIONS], [KEY_MAX_DEFL], TYPE_TEXTBOX, self.max_defl_change) change_tab.append(t9) @@ -296,7 +296,7 @@ def get_values_for_design_pref(self, key, design_dictionary): KEY_STR_TYPE:'Highway Bridge', KEY_DESIGN_LOAD:'Live Load', KEY_MEMBER_OPTIONS :'Simple Span', - KEY_SUPPORTING_OPTIONS: 'NA', + KEY_SUPPORTING_OPTIONS: 'Not Applicable', KEY_MAX_DEFL : 600, KEY_IntermediateStiffener_thickness_val : VALUES_STIFFENER_THICKNESS, KEY_LongitudnalStiffener_thickness_val : VALUES_STIFFENER_THICKNESS @@ -312,13 +312,22 @@ def member_options_change(self, arg): return {KEY_MEMBER_OPTIONS : VALUES_MEMBER_OPTIONS[0]} def supp_options_change(self, arg): - if arg[0] in ['Purlin and Girts', 'Simple span', 'Cantilever span']: + # arg[0] = structure type, arg[1] = member option. + # Cladding-based supporting options (IS 800:2007 Table 6) apply to + # industrial/other buildings only. Bridge deflection is a direct span + # ratio, so Supporting Options is genuinely Not Applicable for bridges. + structure_type = arg[0] if arg and arg[0] else "" + member_option = arg[1] if len(arg) > 1 and arg[1] else "" + if structure_type in [KEY_DISP_STR_TYP1, KEY_DISP_STR_TYP2]: + return {KEY_SUPPORTING_OPTIONS : VALUES_SUPPORTING_OPTIONS_DEF} + member_lower = member_option.lower() + if member_lower in ['purlin and girts', 'simple span', 'cantilever span']: return {KEY_SUPPORTING_OPTIONS : VALUES_SUPPORTING_OPTIONS_PSC} - elif arg[0] == 'Rafter Supporting': + elif member_lower == 'rafter supporting': return {KEY_SUPPORTING_OPTIONS : VALUES_SUPPORTING_OPTIONS_RS} - elif arg[0] == 'Gantry': + elif member_lower == 'gantry': return {KEY_SUPPORTING_OPTIONS : VALUES_SUPPORTING_OPTIONS_GNT} - elif arg[0] in ['Floor and roof', 'Cantilever']: + elif member_lower in ['floor and roof', 'cantilever']: return {KEY_SUPPORTING_OPTIONS : VALUES_SUPPORTING_OPTIONS_FRC} else: return {KEY_SUPPORTING_OPTIONS : VALUES_SUPPORTING_OPTIONS_DEF} @@ -374,13 +383,19 @@ def max_defl_change(self, arg): return {KEY_MAX_DEFL : VALUES_MAX_DEFL[6]} else: return {KEY_MAX_DEFL : VALUES_MAX_DEFL[7]} - elif arg[2] == 'Gantry' and arg[1] == 'Live load': + elif arg[2] == 'Gantry': + # Gantry deflection is governed by the crane design load + # (arg[1]), not a 'Live load' option. IS 800:2007 Table 6: + # manual -> Span/500, electric <=50t -> Span/750, + # electric >50t -> Span/1000. if arg[1] == 'Crane Load(Manual operation)': return {KEY_MAX_DEFL : VALUES_MAX_DEFL[9]} elif arg[1] == 'Crane load(Electric operation up to 50t)': return {KEY_MAX_DEFL : VALUES_MAX_DEFL[10]} - else: + elif arg[1] == 'Crane load(Electric operation over 50t)': return {KEY_MAX_DEFL : VALUES_MAX_DEFL[11]} + else: + return {KEY_MAX_DEFL : 'NA'} else: return {KEY_MAX_DEFL : 'NA'} @@ -717,7 +732,7 @@ def output_values(self, flag): t0 = (None, DISP_TITLE_MOMENT_DESIGN, TYPE_TITLE, None, True) out_list.append(t0) - t_beta = (KEY_betab_constatnt, KEY_DISP_betab_constatnt, TYPE_TEXTBOX, + t_beta = (KEY_betab_constatnt, 'βb', TYPE_TEXTBOX, self.betab if flag else '', True) out_list.append(t_beta) @@ -914,9 +929,42 @@ def func_for_validation(self, design_dictionary): print(f"DEBUG: Validation flags -> flag(fields):{flag}, flag1(length):{flag1}, flag2(shear):{flag2}, flag3(moment):{flag3}") if flag and flag1 and flag2 and flag3: - if self.debug: - print("DEBUG: Validation PASSED - Calling set_input_values") - self.set_input_values(design_dictionary) + max_defl_val = str(design_dictionary.get(KEY_MAX_DEFL, 'Span/600')) + denom_str = max_defl_val.split('/')[-1] if '/' in max_defl_val else max_defl_val + try: + float(denom_str) + design_dictionary[KEY_MAX_DEFL] = f"Span/{denom_str}" + except ValueError: + all_errors.append("Maximum Deflection must be a numeric value") + flag = False + + web_phil = design_dictionary.get(KEY_WEB_PHILOSOPHY, "") + int_stiff = design_dictionary.get(KEY_IntermediateStiffener, "") + if web_phil == KEY_DISP_PHILO1 and int_stiff == "No": + all_errors.append("Intermediate stiffeners are mandatory for thin webs") + flag = False + + # Intermediate stiffener spacing (Additional Inputs) is a free textbox with no + # validator, so guard against non-numeric input (e.g. "a12") here instead of + # letting float() crash downstream in design_check. + int_stiff_spacing = str(design_dictionary.get(KEY_IntermediateStiffener_spacing, "")).strip() + if int_stiff_spacing and int_stiff_spacing.upper() not in ("NA", "N/A"): + try: + if float(int_stiff_spacing) <= 0: + all_errors.append("Intermediate Stiffener Spacing must be a positive number") + flag = False + except ValueError: + all_errors.append("Intermediate Stiffener Spacing must be a numeric value (or leave as NA)") + flag = False + + if flag: + if self.debug: + print("DEBUG: Validation PASSED - Calling set_input_values") + self.set_input_values(design_dictionary) + else: + if self.debug: + print(f"DEBUG: Validation FAILED - Errors: {all_errors}") + return all_errors else: if self.debug: print(f"DEBUG: Validation FAILED - Errors: {all_errors}") @@ -974,6 +1022,9 @@ def set_input_values(self, design_dictionary): print(f" {key}: {design_dictionary[key]}") print("="*60 + "\n") + # Reset the once-per-run web-crippling warning guard for this design run + self.web_crippling_warning_logged = False + self.module = design_dictionary[KEY_MODULE] self.mainmodule = 'PLATE GIRDER' self.design_type = design_dictionary[KEY_OVERALL_DEPTH_PG_TYPE] @@ -1030,6 +1081,10 @@ def set_input_values(self, design_dictionary): thickness_for_mat = max(self.web_thickness,self.top_flange_thickness, self.bottom_flange_thickness) self.eff_depth = self.total_depth - self.top_flange_thickness - self.bottom_flange_thickness + # Log the web-slenderness warning once per design run (the per-particle web-crippling + # check no longer logs it, to keep the design log readable during optimization). + if getattr(self, 'logger', None) and self.web_thickness and self.eff_depth / self.web_thickness > 200: + self.logger.warning("Web slenderness ratio (d/tw) exceeds 200. Additional stiffening may be required.") self.IntStiffnerwidth = min(self.top_flange_width,self.bottom_flange_width) - self.web_thickness/2 - 10 self.material = Material(design_dictionary[KEY_MATERIAL],thickness_for_mat) if self.debug: @@ -1068,12 +1123,17 @@ def set_input_values(self, design_dictionary): self.deflection_criteria= design_dictionary[KEY_MAX_DEFL] self.support_condition = 'Simply Supported' self.loading_case = design_dictionary[KEY_BENDING_MOMENT_SHAPE] + self.moment_diagram_type = self.loading_case self.shear_type = None self.support_type = design_dictionary[KEY_DESIGN_TYPE_FLEXURE] self.loading_condition = design_dictionary[KEY_LOAD] self.torsional_res = design_dictionary[KEY_TORSIONAL_RES] self.warping = design_dictionary[KEY_WARPING_RES] self.length = float(design_dictionary[KEY_LENGTH]) + self.structure_type = design_dictionary.get(KEY_STR_TYPE, KEY_DISP_STR_TYP3) + self.design_load_type = design_dictionary.get(KEY_DESIGN_LOAD, VALUE_DESIGN_LOAD_list[0]) + self.member_option = design_dictionary.get(KEY_MEMBER_OPTIONS, VALUES_MEMBER_OPTIONS[0][0]) + self.supporting_option = design_dictionary.get(KEY_SUPPORTING_OPTIONS, VALUES_SUPPORTING_OPTIONS_DEF[0]) # Calculate effective length for lateral-torsional buckling # LLT = k * L + d_mult * D per IS 800:2007 Table 15 @@ -1091,6 +1151,7 @@ def set_input_values(self, design_dictionary): self.effective_length = self.length * self.lefactor + self.d_offset_mult * self.total_depth self.allow_class = design_dictionary[KEY_ALLOW_CLASS] self.loading_case = design_dictionary[KEY_BENDING_MOMENT_SHAPE] + self.moment_diagram_type = self.loading_case self.beta_b_lt = None self.web_philosophy = design_dictionary[KEY_WEB_PHILOSOPHY] self.epsilon = math.sqrt(250 / self.material.fy) # IS 800:2007: ε = √(250/fy), fy in MPa @@ -1104,6 +1165,7 @@ def set_input_values(self, design_dictionary): self.V_cr = None self.V_d = None self.V_tf = None + self.intermediate_stiffener = design_dictionary[KEY_IntermediateStiffener] self.long_Stiffner = design_dictionary[KEY_LongitudnalStiffener] self.load = Load(shear_force=design_dictionary[KEY_SHEAR],axial_force="",moment=design_dictionary[KEY_MOMENT],unit_kNm=True,) # Imperfection factor per IS 800:2007 Table 10 for welded I-sections @@ -1252,6 +1314,20 @@ def section_classification(self,design_dictionary): ) return is_valid + def _warn_web_crippling_once(self): + """Log the web-crippling shortfall warning at most once per design run. + + check_web_crippling_IS800() no longer logs this itself because it is evaluated + once per particle per PSO iteration; logging there floods the design log with + identical lines. This consolidates it to a single message for the final design. + """ + if not getattr(self, 'web_crippling_warning_logged', False): + self.logger.warning( + f"Web crippling resistance ({self.F_q:.2f} N) is less than factored " + f"load ({self.load.shear_force:.2f} N)" + ) + self.web_crippling_warning_logged = True + def design_check(self,design_dictionary): if self.debug: print("\n" + "="*50) @@ -1413,7 +1489,8 @@ def design_check(self,design_dictionary): self.logger.error("Web Buckling Check failed") web_height = self.total_depth - self.top_flange_thickness - self.bottom_flange_thickness - is_safe, self.F_q = check_web_crippling(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, self.gamma_m0, self.logger, debug=self.debug) + tf_used = min(self.top_flange_thickness, self.bottom_flange_thickness) if self.top_flange_thickness > 0 and self.bottom_flange_thickness > 0 else (self.top_flange_thickness or self.bottom_flange_thickness) + is_safe, self.F_q = check_web_crippling_IS800(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, tf_used, self.gamma_m0, self.logger, debug=self.debug) if self.debug: print(f"Crippling Resistance (F_q): {self.F_q:.2f} N") if is_safe: @@ -1422,7 +1499,8 @@ def design_check(self,design_dictionary): else: self.shearflag3 = False self.logger.error("Web Crippling Check failed") - + self._warn_web_crippling_once() + if self.shearflag1 == True and self.shearflag2 == True and self.shearflag3 == True: self.shearchecks = True else: @@ -1504,8 +1582,12 @@ def design_check(self,design_dictionary): self.c = self.calculate_stiffener_spacing_IS800() self.logger.info(f"Calculated stiffener spacing (c) per IS 800:2007: {self.c:.2f} mm") else: - self.c = float(self.c) - + try: + self.c = float(self.c) + except (ValueError, TypeError): + self.logger.warning(f"Invalid intermediate stiffener spacing '{self.c}'; using calculated value") + self.c = self.calculate_stiffener_spacing_IS800() + self.design_flag2 = min_web_thickness_thick_web(self.eff_depth,self.web_thickness,self.epsilon,self.stiffener_type,self.c, debug=self.debug) if self.design_flag2 == True: self.x= design_dictionary[KEY_ShearBucklingOption] @@ -1562,13 +1644,15 @@ def design_check(self,design_dictionary): # Web Crippling Check (Added for Thin Web with ITS/Simple Post Critical) web_height = self.total_depth - self.top_flange_thickness - self.bottom_flange_thickness - is_safe_crip, self.F_q = check_web_crippling(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, self.gamma_m0, self.logger, debug=self.debug) + tf_used = min(self.top_flange_thickness, self.bottom_flange_thickness) if self.top_flange_thickness > 0 and self.bottom_flange_thickness > 0 else (self.top_flange_thickness or self.bottom_flange_thickness) + is_safe_crip, self.F_q = check_web_crippling_IS800(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, tf_used, self.gamma_m0, self.logger, debug=self.debug) if is_safe_crip: self.shearflag3 = True self.logger.info("Web Crippling Check passed") else: self.shearflag3 = False self.logger.error("Web Crippling Check failed") + self._warn_web_crippling_once() @@ -1619,13 +1703,15 @@ def design_check(self,design_dictionary): # Web Crippling Check (Added for Thin Web with ITS/Tension Field) web_height = self.total_depth - self.top_flange_thickness - self.bottom_flange_thickness - is_safe_crip, self.F_q = check_web_crippling(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, self.gamma_m0, self.logger, debug=self.debug) + tf_used = min(self.top_flange_thickness, self.bottom_flange_thickness) if self.top_flange_thickness > 0 and self.bottom_flange_thickness > 0 else (self.top_flange_thickness or self.bottom_flange_thickness) + is_safe_crip, self.F_q = check_web_crippling_IS800(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, tf_used, self.gamma_m0, self.logger, debug=self.debug) if is_safe_crip: self.shearflag3 = True self.logger.info("Web Crippling Check passed") else: self.shearflag3 = False self.logger.error("Web Crippling Check failed") + self._warn_web_crippling_once() if self.shearflag1 == True and self.shearflag2 == True and self.shearflag3 == True: self.shearchecks = True @@ -2002,7 +2088,8 @@ def design_check_optimized_version(self,design_dictionary): #web crippling check web_height = self.total_depth - self.top_flange_thickness - self.bottom_flange_thickness - is_safe, self.F_q = check_web_crippling(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, self.gamma_m0, self.logger, debug=self.debug) + tf_used = min(self.top_flange_thickness, self.bottom_flange_thickness) if self.top_flange_thickness > 0 and self.bottom_flange_thickness > 0 else (self.top_flange_thickness or self.bottom_flange_thickness) + is_safe, self.F_q = check_web_crippling_IS800(self.load.shear_force, self.b1, self.web_thickness, self.material.fy, web_height, tf_used, self.gamma_m0, self.logger, debug=self.debug) if is_safe: self.shearflag3 = True # Fixed from False to True # self.logger.info("Web Crippling Check passed") @@ -2061,7 +2148,11 @@ def design_check_optimized_version(self,design_dictionary): # Calculate c per IS 800:2007 when not provided self.c = self.calculate_stiffener_spacing_IS800() else: - self.c = float(self.c) + try: + self.c = float(self.c) + except (ValueError, TypeError): + self.logger.warning(f"Invalid intermediate stiffener spacing '{self.c}'; using calculated value") + self.c = self.calculate_stiffener_spacing_IS800() self.design_flag2 = min_web_thickness_thick_web(self.eff_depth,self.web_thickness,self.epsilon,self.stiffener_type,self.c, debug=self.debug) print('DESIGN FLAG2',self.design_flag2) if self.design_flag2 == True: @@ -2130,9 +2221,10 @@ def design_check_optimized_version(self,design_dictionary): # Web Crippling Check for thin web (same as thick web path) web_height = self.total_depth - self.top_flange_thickness - self.bottom_flange_thickness - is_safe_crip, self.F_q = check_web_crippling( + tf_used = min(self.top_flange_thickness, self.bottom_flange_thickness) if self.top_flange_thickness > 0 and self.bottom_flange_thickness > 0 else (self.top_flange_thickness or self.bottom_flange_thickness) + is_safe_crip, self.F_q = check_web_crippling_IS800( self.load.shear_force, self.b1, self.web_thickness, - self.material.fy, web_height, self.gamma_m0, self.logger + self.material.fy, web_height, tf_used, self.gamma_m0, self.logger ) self.shearflag3 = is_safe_crip @@ -2485,6 +2577,10 @@ def final_format(self,design_dictionary): self.end_panel_stiffener_thickness = "N/A" self.x1 = "N/A" self.x2 = "N/A" + # Reset the raw stiffener spacing too: the CAD builder gates on self.c, so + # leaving a numeric value here would draw intermediate stiffeners the design + # says do not exist (thick web has no ITS). + self.c = "N/A" else: self.intstiffener_thk = self.IntStiffThickness self.longstiffener_thk = self.LongStiffThickness diff --git a/src/osdag_core/design_type/plate_girder/gui/pso_ui_manager.py b/src/osdag_core/design_type/plate_girder/gui/pso_ui_manager.py index 810c916dd..69cf7b1f2 100644 --- a/src/osdag_core/design_type/plate_girder/gui/pso_ui_manager.py +++ b/src/osdag_core/design_type/plate_girder/gui/pso_ui_manager.py @@ -50,6 +50,7 @@ def __init__(self, parent: 'TemplatePage'): self._pso_viz_widget: Optional[QWidget] = None self._last_pso_iter: int = -1 self._pso_data_for_replay = None + self._render_pending = False def is_plate_girder_optimized(self) -> bool: """Check if current module is Plate Girder with Optimized design type. @@ -180,9 +181,12 @@ def viz_callback(depth, ur, weight, iteration, particle_idx, position=None, vari # Disable input dock during optimization if hasattr(self.parent, 'input_dock'): self.parent.input_dock.setEnabled(False) - + # Output dock stays visible throughout PSO - no hide/show needed - + + # Mark that _close_popup_and_render will handle the 3D render + self._render_pending = True + # Run design SYNCHRONOUSLY on main thread (required for OpenGL safety) # The viz_callback includes processEvents() to update UI in real-time try: @@ -283,7 +287,9 @@ def _close_popup_and_render(self): self._hidden_pso_widget = self.pso_viz self.pso_viz.hide() self.pso_viz = None - + + self._render_pending = False + # Trigger 3D model generation if hasattr(self.parent, '_render_3d_result'): self.parent._render_3d_result(self.parent.backend.design_status, self.parent.backend) diff --git a/src/osdag_core/design_type/plate_girder/report/latex_report.py b/src/osdag_core/design_type/plate_girder/report/latex_report.py index c7b0a8949..d429d9a41 100644 --- a/src/osdag_core/design_type/plate_girder/report/latex_report.py +++ b/src/osdag_core/design_type/plate_girder/report/latex_report.py @@ -15,6 +15,7 @@ import math from ..checks.shear import tension_field_unequal_I_corrected, calc_K_v from ....utils.common.is800_2007 import IS800_2007 +from ..core.section import shear_stress_unsym_I def save_design(popup_summary): """Generate LaTeX design report for Plate Girder module""" @@ -94,17 +95,34 @@ def prepare_report_input(pg_obj, logger): # Material material = getattr(pg_obj, 'material', None) - report_input['Material Grade'] = getattr(material, 'designation', 'E 250') if material else 'E 250' + report_input['Material Grade'] = getattr(material, 'material', 'E 250') if material else 'E 250' # Structure Type # Try to get from attributes, fallback to reasonable default if missing report_input['Type of Structure'] = getattr(pg_obj, 'structure_type', 'Industrial Structure') + # Normalize design load type (e.g., "Live load" -> "Live Load") + design_load = getattr(pg_obj, 'design_load_type', 'Dead load') + if design_load.lower() == 'live load': + design_load = 'Live Load' + elif design_load.lower() == 'dead load': + design_load = 'Dead Load' + report_input['Design Load'] = design_load + + report_input['Member Options'] = getattr(pg_obj, 'member_option', 'Simple span') + + # Replace 'NA' with 'Not Applicable' for supporting options + supp_opt = getattr(pg_obj, 'supporting_option', 'Not Applicable') + if supp_opt.upper() == 'NA': + supp_opt = 'Not Applicable' + report_input['Supporting Options'] = supp_opt + + # Torsional Restraint - report_input['Torsional Restraint'] = getattr(pg_obj, 'torsional_restraint', 'Fully Restrained') + report_input['Torsional Restraint'] = getattr(pg_obj, 'torsional_res', 'Fully Restrained') # Warping Restraint - report_input['Warping Restraint'] = getattr(pg_obj, 'warping_restraint', 'No Restraint') + report_input['Warping Restraint'] = getattr(pg_obj, 'warping', 'No Restraint') # Span report_input['Span (mm)'] = round(getattr(pg_obj, 'length', 0), 1) @@ -118,7 +136,7 @@ def prepare_report_input(pg_obj, logger): # Material Properties if material: - girder_props['Material'] = getattr(material, 'designation', 'E 250') + girder_props['Material'] = getattr(material, 'material', 'E 250') girder_props['Ultimate Strength (MPa)'] = getattr(material, 'fu', 410) girder_props['Yield Strength (MPa)'] = getattr(material, 'fy', 250) E = getattr(material, 'modulus_of_elasticity', 200000) @@ -147,17 +165,17 @@ def prepare_report_input(pg_obj, logger): girder_props['Mass (kg/m)'] = Unsymmetrical_I_Section_Properties.calc_mass(D, bf_top, bf_bot, tw, tf_top, tf_bot) girder_props['Area (cm$^2$)' ] = round(Unsymmetrical_I_Section_Properties.calc_area(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 100, 2) - girder_props['Moment of Area, Iz (cm$^4$)' ] = round(Unsymmetrical_I_Section_Properties.calc_MomentOfAreaZ(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10000, 2) - girder_props['Moment of Area, Iy (cm$^4$)' ] = round(Unsymmetrical_I_Section_Properties.calc_MomentOfAreaY(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10000, 2) + girder_props['Moment of Area, $I_z$ (cm$^4$)' ] = round(Unsymmetrical_I_Section_Properties.calc_MomentOfAreaZ(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10000, 2) + girder_props['Moment of Area, $I_y$ (cm$^4$)' ] = round(Unsymmetrical_I_Section_Properties.calc_MomentOfAreaY(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10000, 2) - girder_props['Radius of Gyration, rz (cm)'] = round(Unsymmetrical_I_Section_Properties.calc_RadiusOfGyrationZ(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10, 2) - girder_props['Radius of Gyration, ry (cm)'] = round(Unsymmetrical_I_Section_Properties.calc_RadiusOfGyrationY(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10, 2) + girder_props['Radius of Gyration, $r_z$ (cm)'] = round(Unsymmetrical_I_Section_Properties.calc_RadiusOfGyrationZ(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10, 2) + girder_props['Radius of Gyration, $r_y$ (cm)'] = round(Unsymmetrical_I_Section_Properties.calc_RadiusOfGyrationY(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 10, 2) - girder_props['Elastic Modulus, Zez (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_ElasticModulusZz(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) - girder_props['Elastic Modulus, Zey (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_ElasticModulusZy(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) + girder_props['Elastic Modulus, $Z_{ez}$ (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_ElasticModulusZz(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) + girder_props['Elastic Modulus, $Z_{ey}$ (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_ElasticModulusZy(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) - girder_props['Plastic Modulus, Zpz (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_PlasticModulusZ(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) - girder_props['Plastic Modulus, Zpy (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_PlasticModulusY(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) + girder_props['Plastic Modulus, $Z_{pz}$ (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_PlasticModulusZ(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) + girder_props['Plastic Modulus, $Z_{py}$ (cm$^3$)' ] = round(Unsymmetrical_I_Section_Properties.calc_PlasticModulusY(D, bf_top, bf_bot, tw, tf_top, tf_bot) / 1000, 2) report_input['Girder Properties'] = girder_props @@ -188,7 +206,7 @@ def prepare_report_input(pg_obj, logger): report_input['Support Condition Inputs'] = 'TITLE' report_input['Support Condition'] = getattr(pg_obj, 'support_type', 'Major Laterally Supported') - report_input['Bearing length (mm)'] = round(getattr(pg_obj, 'bearing_length', 0), 1) + report_input['Bearing length (mm)'] = round(getattr(pg_obj, 'b1', 0), 1) # ==================== 5. Web Philosophy Inputs ==================== report_input['Web Philosophy Inputs'] = 'TITLE' @@ -252,6 +270,22 @@ def prepare_design_checks(pg_obj, logger): moment_checks = getattr(pg_obj, 'momentchecks', False) defl_check = getattr(pg_obj, 'defl_check', False) web_philosophy = getattr(pg_obj, 'web_philosophy', '') + # Intermediate stiffener spacing is referenced by several later subsections + # (Longitudinal Stiffener, Stiffener Design Summary). Extract it up front with a + # safe default so those subsections never fail with NameError when the + # Intermediate Stiffener subsection is skipped for a given design case. + c = getattr(pg_obj, 'c', 0) + # pg_obj.c may be a non-numeric marker (e.g. 'NA') when no intermediate stiffener + # spacing applies; coerce to a float so the subsections that use it in arithmetic + # (Intermediate/Longitudinal Stiffener) never fail with a type error. + try: + c = float(c) + except (TypeError, ValueError): + c = 0 + # Deflection limit ratio is computed in the Deflection subsection but also read + # in the Overall Design Check subsection. Initialise it up front so the latter + # never fails with UnboundLocalError if the Deflection subsection is skipped. + defl_limit_ratio = 600 # ==================== SECTION CLASSIFICATION ==================== report_check.append(['SubSection', 'Section Classification', table_format]) @@ -341,6 +375,21 @@ def prepare_design_checks(pg_obj, logger): V_d_N = getattr(pg_obj, 'V_d', 0) V_d = round(V_d_N / 1000, 2) if V_d_N else 0 + # Effective depth + d_eq = Math(inline=True) + d_eq.append(NoEscape(r'\begin{aligned}\\')) + d_eq.append(NoEscape(r'd &= D - t_{f,top} - t_{f,bottom}\\\\')) + d_eq.append(NoEscape(rf'&= {D:.1f} - {tf_top:.1f} - {tf_bot:.1f}\\\\')) + d_eq.append(NoEscape(rf'&= {d:.1f} \text{{ mm}}\\')) + d_eq.append(NoEscape(r'\end{aligned}')) + + report_check.append([ + NoEscape(r'Effective web depth ($d$)'), + '', + d_eq, + '' + ]) + # Shear area Avw = round(d * tw, 2) @@ -417,6 +466,22 @@ def prepare_design_checks(pg_obj, logger): # (replicating shear.py logic for consistency) mu = 0.3 tau_crc = IS800_2007.cl_8_4_2_2_tau_crc_Simple_postcritical(kv, E, mu, d_eff, tw) + + tau_cre_eq = Math(inline=True) + tau_cre_eq.append(NoEscape(r'\begin{aligned}\\')) + tau_cre_eq.append(NoEscape(r'\tau_{cr,e} &= \dfrac{k_v \pi^2 E}{12(1 - \mu^2)(d/t_w)^2}\\\\')) + tau_cre_eq.append(NoEscape(rf'&= \dfrac{{{kv:.3f} \times \pi^2 \times {E}}}{{12(1 - 0.3^2)({d_eff:.1f}/{tw:.1f})^2}}\\\\')) + tau_cre_eq.append(NoEscape(rf'&= {tau_crc:.2f} \text{{ MPa}}\\')) + tau_cre_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2]}\\')) + tau_cre_eq.append(NoEscape(r'\end{aligned}')) + + report_check.append([ + NoEscape(r'Elastic Critical Shear Stress'), + '', + tau_cre_eq, + '' + ]) + lambda_w = IS800_2007.cl_8_4_2_2_lambda_w_Simple_postcritical(fy, tau_crc) tau_b = IS800_2007.cl_8_4_2_2_tau_b_Simple_postcritical(lambda_w, fy) @@ -471,14 +536,20 @@ def prepare_design_checks(pg_obj, logger): '' ]) - V_cr_val = round(Avw * tau_b / 1000, 2) - + # Source V_cr from the design's authoritative value (pg_obj.V_cr, in N) + # so this row matches the Vertical Anchor Force row and the design-pref + # textbox. Recomputing round(Avw*tau_b/1000, 2) here uses the already- + # rounded display factors and drifts by ~0.01 kN. Fall back to the + # recomputed value if V_cr was never set (report without a shear check). + _v_cr_pg = getattr(pg_obj, 'V_cr', 0) or 0 + V_cr_val = round(_v_cr_pg / 1000, 2) if _v_cr_pg else round(Avw * tau_b / 1000, 2) + Vcr_eq = Math(inline=True) Vcr_eq.append(NoEscape(r'\begin{aligned}\\')) Vcr_eq.append(NoEscape(r'V_{cr} &= A_{vw} \times \tau_b\\\\')) Vcr_eq.append(NoEscape(rf'&= {Avw:.2f} \times {tau_b:.2f}\\\\')) Vcr_eq.append(NoEscape(rf'&= {V_cr_val:.2f} \text{{ kN}}\\\\')) - Vcr_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.1]}\\')) + Vcr_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2]}\\')) Vcr_eq.append(NoEscape(r'\end{aligned}')) report_check.append([ @@ -595,17 +666,11 @@ def prepare_design_checks(pg_obj, logger): fq_eq.append(NoEscape(r'\begin{aligned}\\')) # Calculate n2 for display purpose if not explicitly available, based on Fq equation - # Fq = (b1 + n2) * tw * fy / gamma_m0 - # (b1 + n2) = Fq * gamma_m0 / (tw * fy) - # n2 = [Fq * 1000 * gamma_m0 / (tw * fy)] - b1 - # Note: Fq in pg_obj is in N. b1 in mm. + tf_used = min(tf_top, tf_bot) if tf_top > 0 and tf_bot > 0 else (tf_top or tf_bot) + n2_disp = 2.5 * tf_used - n2_disp = 0 - if tw > 0 and fy > 0: - try: - n2_disp = (pg_obj.F_q * gamma_m0) / (tw * fy) - b1 - except Exception: - n2_disp = 0 + fq_eq.append(NoEscape(r'n_2 &= 2.5 \times t_f\\\\')) + fq_eq.append(NoEscape(rf'&= 2.5 \times {tf_used:.1f} = {n2_disp:.1f} \text{{ mm}}\\\\')) fq_eq.append(NoEscape(r'F_q &= \dfrac{(b_1 + n_2) t_w f_y}{\gamma_{m0}}\\\\')) fq_eq.append(NoEscape(rf'&= \dfrac{{({b1:.1f} + {n2_disp:.1f}) \times {tw:.1f} \times {fy:.1f}}}{{{gamma_m0}}}\\\\')) @@ -635,15 +700,83 @@ def prepare_design_checks(pg_obj, logger): Ze_val = getattr(pg_obj, 'elast_sec_mod_z', 0) Ze = round(Ze_val * 1e-3, 2) if Ze_val and Ze_val > 0 else 0 - beta_b_val = getattr(pg_obj, 'betab', 1.0) - beta_b = round(beta_b_val, 3) if beta_b_val else 1.0 + # beta_b per IS 800:2007 Cl.8.2.1.2 — mirrors corrected_design_bending_strength() + # and moment_capacity_laterally_unsupported() in checks/moment.py: + # Plastic/Compact -> 1.0 ; Semi-Compact -> Ze/Zp. + # (The old code read a non-existent 'betab' attribute and always fell back to 1.0.) + if section_class in ['Plastic', 'Compact']: + beta_b = 1.0 + else: + beta_b = round(Ze_val / Zp_val, 3) if Zp_val else 1.0 if section_class in ['Plastic', 'Compact']: Z_used = Zp - Z_label = 'Z_p' + Z_label = 'Z_{pz}' else: Z_used = Ze - Z_label = 'Z_e' + Z_label = 'Z_{ez}' + + # Zp Derivation + if section_class in ['Plastic', 'Compact']: + zp_eq = Math(inline=True) + zp_eq.append(NoEscape(r'\begin{aligned}\\')) + + # Plastic neutral axis from the bottom face (divides the section into two + # equal areas). Computed inline here — Unsymmetrical_I_Section_Properties + # exposes only calc_PlasticModulusZ, not a standalone PNA method. + _h_w = D - tf_top - tf_bot + _A_top = bf_top * tf_top + _A_bot = bf_bot * tf_bot + _A_web = _h_w * tw + _A_total = _A_top + _A_bot + _A_web + _half_area = _A_total / 2.0 + if _A_bot >= _half_area: + y_p = _half_area / bf_bot if bf_bot else 0 + elif _A_bot + _A_web >= _half_area: + y_p = tf_bot + (_half_area - _A_bot) / tw if tw else tf_bot + else: + y_p = D - (_A_total - _half_area) / bf_top if bf_top else D + zp_eq.append(NoEscape(r'y_p &= \text{Plastic Neutral Axis from bottom}\\\\')) + zp_eq.append(NoEscape(rf'&= {y_p:.2f} \text{{ mm}}\\\\')) + + # Full derivation: Z_pz = sum of (area x distance from PNA to its centroid). + # For the common case where the PNA lies in the web, the section splits into + # four parts about y_p (bottom flange, web-below, web-above, top flange). + if tf_bot <= y_p <= (D - tf_top): + _y_bar_bf = tf_bot / 2.0 # bottom flange centroid from bottom + _y_bar_tf = D - tf_top / 2.0 # top flange centroid from bottom + _A_web_below = (y_p - tf_bot) * tw + _A_web_above = (D - tf_top - y_p) * tw + _lever_bf = abs(y_p - _y_bar_bf) + _lever_wb = (y_p - tf_bot) / 2.0 + _lever_wa = (D - tf_top - y_p) / 2.0 + _lever_tf = abs(_y_bar_tf - y_p) + # contributions in x10^3 mm^3 (consistent with the printed Zp) + _c_bf = _A_bot * _lever_bf / 1e3 + _c_wb = _A_web_below * _lever_wb / 1e3 + _c_wa = _A_web_above * _lever_wa / 1e3 + _c_tf = _A_top * _lever_tf / 1e3 + zp_eq.append(NoEscape(r'Z_{pz} &= \sum A_i \, |\bar{y}_i - y_p| \quad (\text{about PNA})\\\\')) + zp_eq.append(NoEscape( + rf'&= \underset{{\text{{bot fl.}}}}{{{_A_bot:.0f} \times {_lever_bf:.1f}}}' + rf' + \underset{{\text{{web below}}}}{{{_A_web_below:.0f} \times {_lever_wb:.1f}}}\\\\')) + zp_eq.append(NoEscape( + rf'&\quad + \underset{{\text{{web above}}}}{{{_A_web_above:.0f} \times {_lever_wa:.1f}}}' + rf' + \underset{{\text{{top fl.}}}}{{{_A_top:.0f} \times {_lever_tf:.1f}}}\\\\')) + zp_eq.append(NoEscape( + rf'&= {_c_bf:.2f} + {_c_wb:.2f} + {_c_wa:.2f} + {_c_tf:.2f}\\\\')) + zp_eq.append(NoEscape(rf'&= {Zp:.2f} \times 10^3 \text{{ mm}}^3\\\\')) + else: + zp_eq.append(NoEscape(r'Z_{pz} &= \text{Plastic Modulus about major axis}\\\\')) + zp_eq.append(NoEscape(rf'&= {Zp:.2f} \times 10^3 \text{{ mm}}^3\\\\')) + zp_eq.append(NoEscape(r'\end{aligned}')) + + report_check.append([ + NoEscape(r'Plastic Section Modulus'), + '', + zp_eq, + '' + ]) # Beta_b beta_eq = Math(inline=True) @@ -652,30 +785,64 @@ def prepare_design_checks(pg_obj, logger): beta_eq.append(NoEscape(r'\end{aligned}')) report_check.append([ - NoEscape(r'Betab Factor'), + NoEscape(r'$\beta_b$'), '', beta_eq, '' ]) - # Design moment capacity - md_eq = Math(inline=True) - md_eq.append(NoEscape(r'\begin{aligned}\\')) - + # Design moment capacity — reconstruct the governing case so the shown steps + # reproduce pg_obj.Md (the value printed on the final line). Mirrors + # checks/moment.py: laterally-supported uses beta_b*Zp*fy/gm0 capped at + # cap*Ze*fy/gm0; laterally-unsupported uses Z*fbd (no beta_b, IS Cl.8.2.2); + # high shear (V>0.6Vd) reduces to Mdv (Cl.9.2.2). support_type = getattr(pg_obj, 'support_type', '') - if support_type == 'Major Laterally Unsupported': - md_eq.append(NoEscape(r'M_d &= \beta_b Z_p f_{bd}\\\\')) - md_eq.append(NoEscape(rf'&= {beta_b} \times {Z_used:.2f} \times f_{{bd}}\\\\')) - else: - md_eq.append(NoEscape(r'M_d &= \dfrac{\beta_b Z_p f_y}{\gamma_{m0}}\\')) - md_eq.append(NoEscape(rf'&= \dfrac{{{beta_b} \times {Z_used:.2f} \times {fy}}}{{{gamma_m0}}}\\\\')) + support_condition = getattr(pg_obj, 'support_condition', 'Simply Supported') + cap_factor = 1.5 if support_condition == 'Cantilever' else 1.2 + # The Mdv high-shear check in checks/moment.py uses the PLASTIC shear capacity + # V_d = A_vg*fy/(sqrt(3)*gm0), NOT the shear-buckling resistance stored in + # pg_obj.V_d (which is overwritten by the shear check). Recompute it here so the + # displayed beta and Mdv reproduce pg_obj.Md. + _Vd_pl_kN = ((D - tf_top - tf_bot) * tw * fy) / (math.sqrt(3) * gamma_m0) / 1e3 + high_shear = _Vd_pl_kN > 0 and V_applied > 0.6 * _Vd_pl_kN - md_eq.append(NoEscape(rf'&= {Md:.2f} \text{{ kN-m}}\\\\')) + md_eq = Math(inline=True) + md_eq.append(NoEscape(r'\begin{aligned}\\')) - if support_type == 'Major Laterally Unsupported': + if 'Unsupported' in support_type: + # Laterally unsupported: Md = Z * f_bd (Z = Zp for plastic/compact, Ze otherwise) + fbd_lt = getattr(pg_obj, 'fbd_lt', 0) + md_eq.append(NoEscape(rf'M_d &= {Z_label}\, f_{{bd}}\\\\')) + md_eq.append(NoEscape( + rf'&= {Z_used:.2f}\times 10^3 \times {fbd_lt:.2f} \times 10^{{-6}}\\\\')) + if high_shear: + md_eq.append(NoEscape(r'&\text{(reduced for high shear, } V>0.6V_d\text{)}\\\\')) + md_eq.append(NoEscape(rf'&= {Md:.2f} \text{{ kN-m}}\\\\')) md_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.2.2]}\\')) + elif high_shear: + # Laterally supported, high shear: Mdv per Cl.9.2.2 + _Aw = (D - tf_top - tf_bot) * tw + _Zfd = Zp_val - (_Aw * D / 4.0) # mm^3 + _beta_hs = (2 * V_applied / _Vd_pl_kN - 1) ** 2 + _Mfd = _Zfd * fy / gamma_m0 * 1e-6 # kN-m + _Md_pl = Zp_val * fy / gamma_m0 * 1e-6 # kN-m (full plastic) + md_eq.append(NoEscape(r'&\text{High shear } (V > 0.6\, V_d):\\\\')) + md_eq.append(NoEscape(r'M_{dv} &= M_d - \beta (M_d - M_{fd})\\\\')) + md_eq.append(NoEscape( + rf'&= {_Md_pl:.2f} - {_beta_hs:.3f}\,({_Md_pl:.2f} - {_Mfd:.2f})\\\\')) + md_eq.append(NoEscape(rf'&= {Md:.2f} \text{{ kN-m}}\\\\')) + md_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.9.2.2]}\\')) else: - md_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.2.1]}\\')) + # Laterally supported, low shear: Md = min(beta_b*Zp*fy/gm0, cap*Ze*fy/gm0) + _Md_base = beta_b * Zp_val * fy / gamma_m0 * 1e-6 # kN-m + _Md_limit = cap_factor * Ze_val * fy / gamma_m0 * 1e-6 # kN-m + md_eq.append(NoEscape(rf'M_d &= \dfrac{{\beta_b\, Z_{{pz}}\, f_y}}{{\gamma_{{m0}}}}' + rf' = \dfrac{{{beta_b} \times {Zp:.2f}\times10^3 \times {fy}}}{{{gamma_m0}}}\times10^{{-6}}\\\\')) + md_eq.append(NoEscape(rf'&= {_Md_base:.2f} \text{{ kN-m}}\\\\')) + md_eq.append(NoEscape(rf'M_{{d,lim}} &= \dfrac{{{cap_factor}\, Z_{{ez}}\, f_y}}{{\gamma_{{m0}}}}' + rf' = {_Md_limit:.2f} \text{{ kN-m}}\\\\')) + md_eq.append(NoEscape(rf'M_d &= \min(M_d,\, M_{{d,lim}}) = {Md:.2f} \text{{ kN-m}}\\\\')) + md_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.2.1.2]}\\')) md_eq.append(NoEscape(r'\end{aligned}')) @@ -830,20 +997,28 @@ def prepare_design_checks(pg_obj, logger): except: Iz_mm4 = 1 - # Calculate w (N/mm) from M_applied (kNm) assuming UDL - # M = w L^2 / 8 => w = 8 M / L^2 - if L > 0: - M_Nmm = M_applied * 1e6 - w_udl = 8 * M_Nmm / (L**2) - else: - w_udl = 0 + # Determine deflection formula based on load case + load_case = getattr(pg_obj, 'loading_case', 'Uniform Loading with pinned-pinned support') + M_Nmm = M_applied * 1e6 + if load_case == 'Uniform Loading with fixed-fixed support': + formula_sym = r'\delta &= \dfrac{M L^2}{32 E I_{z}}' + formula_val = rf'&= \dfrac{{{M_applied:.2f} \times 10^6 \times {L:.1f}^2}}{{32 \times {E} \times {Iz_mm4:.2e}}}' + elif load_case == 'Concentrate Load with pinned-pinned support': + formula_sym = r'\delta &= \dfrac{M L^2}{12 E I_{z}}' + formula_val = rf'&= \dfrac{{{M_applied:.2f} \times 10^6 \times {L:.1f}^2}}{{12 \times {E} \times {Iz_mm4:.2e}}}' + elif load_case == 'Concentrate load with fixed-fixed support': + formula_sym = r'\delta &= \dfrac{M L^2}{24 E I_{z}}' + formula_val = rf'&= \dfrac{{{M_applied:.2f} \times 10^6 \times {L:.1f}^2}}{{24 \times {E} \times {Iz_mm4:.2e}}}' + else: # Default UDL pinned-pinned + formula_sym = r'\delta &= \dfrac{5 M L^2}{48 E I_{z}}' + formula_val = rf'&= \dfrac{{5 \times {M_applied:.2f} \times 10^6 \times {L:.1f}^2}}{{48 \times {E} \times {Iz_mm4:.2e}}}' actual_eq = Math(inline=True) actual_eq.append(NoEscape(r'\begin{aligned}\\')) - actual_eq.append(NoEscape(r'\delta &= \dfrac{5 w L^4}{384 E I_{z}}\\\\')) - actual_eq.append(NoEscape(rf'&= \dfrac{{5 \times {w_udl:.2f} \times {L:.1f}^4}}{{384 \times {E} \times {Iz_mm4:.2e}}}\\\\')) + actual_eq.append(NoEscape(formula_sym + r'\\\\')) + actual_eq.append(NoEscape(formula_val + r'\\\\')) actual_eq.append(NoEscape(rf'&= {delta_actual:.2f} \text{{ mm}}\\\\')) - actual_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Table 6]}')) + actual_eq.append(NoEscape(r'&\text{[Standard beam deflection formula]}')) actual_eq.append(NoEscape(r'\end{aligned}')) defl_status = 'Pass' if defl_check else 'Fail' @@ -859,6 +1034,68 @@ def prepare_design_checks(pg_obj, logger): # ==================== WELD DESIGN ==================== report_check.append(['SubSection', 'Weld Design', table_format]) + # Calculate fwd + fwd = fu / (math.sqrt(3) * 1.25) + fwd_eq = Math(inline=True) + fwd_eq.append(NoEscape(r'\begin{aligned}\\')) + fwd_eq.append(NoEscape(r'f_{wd} &= \dfrac{f_u}{\sqrt{3} \times \gamma_{mw}}\\\\')) + fwd_eq.append(NoEscape(rf'&= \dfrac{{{fu}}}{{\sqrt{{3}} \times 1.25}}\\\\')) + fwd_eq.append(NoEscape(rf'&= {fwd:.2f} \text{{ MPa}}\\\\')) + fwd_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.10.5.7.1.1]}\\')) + fwd_eq.append(NoEscape(r'\end{aligned}')) + + report_check.append([ + 'Weld Design Stress', + '', + fwd_eq, + '' + ]) + + V_N = V_applied * 1e3 + sf = shear_stress_unsym_I(V_N, bf_top, tf_top, bf_bot, tf_bot, tw, D - tf_top - tf_bot) + # shear_stress_unsym_I returns shear flow in the same force unit it is given. + # V_N is in Newtons, so the returned value is already N/mm (the dict key name is + # misleading). Do NOT multiply by 1e3 again or the weld leg inflates 1000x. + q_top = sf['q_top_kN_per_mm'] # N/mm + t_req_top = q_top / fwd + a_req_top = t_req_top * math.sqrt(2) + + A_top = bf_top * tf_top + y_bar_top = (D - tf_top / 2.0) - sf['y_na_mm'] + Iz_sf = sf['I_z_mm4'] + + sf_eq = Math(inline=True) + sf_eq.append(NoEscape(r'\begin{aligned}\\')) + sf_eq.append(NoEscape(r'q_{top} &= \dfrac{V A y_{bar}}{I_z}\\\\')) + sf_eq.append(NoEscape( + rf'&= \dfrac{{{V_N:.2e} \times {A_top:.2f} \times {y_bar_top:.2f}}}' + rf'{{{Iz_sf:.2e}}}\\\\')) + sf_eq.append(NoEscape(rf'&= {q_top:.2f} \text{{ N/mm}}\\\\')) + sf_eq.append(NoEscape(r'\end{aligned}')) + + report_check.append([ + 'Shear Flow (Top Flange)', + '', + sf_eq, + '' + ]) + + req_weld_eq = Math(inline=True) + req_weld_eq.append(NoEscape(r'\begin{aligned}\\')) + req_weld_eq.append(NoEscape(r't_{req} &= \dfrac{q_{top}}{f_{wd}}\\\\')) + req_weld_eq.append(NoEscape(rf'&= \dfrac{{{q_top:.2f}}}{{{fwd:.2f}}}\\\\')) + req_weld_eq.append(NoEscape(rf'&= {t_req_top:.2f} \text{{ mm}}\\\\')) + req_weld_eq.append(NoEscape(r's_{req} &= t_{req} \times \sqrt{2}\\\\')) + req_weld_eq.append(NoEscape(rf'&= {a_req_top:.2f} \text{{ mm}}\\\\')) + req_weld_eq.append(NoEscape(r'\end{aligned}')) + + report_check.append([ + 'Required Weld Leg (Top)', + '', + req_weld_eq, + '' + ]) + weld_top = getattr(pg_obj, 'atop', 0) weld_bot = getattr(pg_obj, 'abot', 0) t_min = min(tw, tf_top, tf_bot) if tw > 0 and tf_top > 0 and tf_bot > 0 else 0 @@ -931,126 +1168,155 @@ def prepare_design_checks(pg_obj, logger): # ==================== INTERMEDIATE STIFFENER - SECTION 1.5.1 ==================== - report_check.append(['SubSection', 'Intermediate Stiffener', table_format]) - - c = getattr(pg_obj, 'c', 0) - - if d > 0: - c_d_ratio = round(c / d, 3) - sqrt_2 = 1.414 + # Web philosophy is the single source of truth for intermediate stiffeners: + # 'Thick Web without ITS' -> no intermediate stiffener section at all. + show_int_stiff = web_philosophy != 'Thick Web without ITS' + if show_int_stiff: + report_check.append(['SubSection', 'Intermediate Stiffener', table_format]) + + if d > 0: + c_d_ratio = round(c / d, 3) + sqrt_2 = 1.414 - if c_d_ratio >= sqrt_2: - I_s_min = round(0.75 * d * (tw**3), 2) + if c_d_ratio >= sqrt_2: + I_s_min = round(0.75 * d * (tw**3), 2) - I_s_eq = Math(inline=True) - I_s_eq.append(NoEscape(r'\begin{aligned}\\')) - I_s_eq.append(NoEscape(rf'\text{{Since }} \dfrac{{c}}{{d}} &= {c_d_ratio:.3f} \geq \sqrt{{2}}\\\\')) - I_s_eq.append(NoEscape(r'I_s &\geq 0.75 \, d \, t_w^3\\\\')) - I_s_eq.append(NoEscape(rf'&\geq 0.75 \times {d:.1f} \times ({tw:.1f})^3\\\\')) - I_s_eq.append(NoEscape(rf'&\geq {I_s_min:.2f} \text{{ mm}}^4\\\\')) - I_s_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.7.1.2, Eq. 1.17]}\\')) - I_s_eq.append(NoEscape(r'\end{aligned}')) - else: - # condition_status = rf'Since \dfrac{{c}}{{d}} = {c_d_ratio:.3f} < \sqrt{{2}}' - I_s_min = round((1.5 * (d**3) * (tw**3)) / (c**2), 2) + I_s_eq = Math(inline=True) + I_s_eq.append(NoEscape(r'\begin{aligned}\\')) + I_s_eq.append(NoEscape(rf'\text{{Since }} \dfrac{{c}}{{d}} &= {c_d_ratio:.3f} \geq \sqrt{{2}}\\\\')) + I_s_eq.append(NoEscape(r'I_s &\geq 0.75 \, d \, t_w^3\\\\')) + I_s_eq.append(NoEscape(rf'&\geq 0.75 \times {d:.1f} \times ({tw:.1f})^3\\\\')) + I_s_eq.append(NoEscape(rf'&\geq {I_s_min:.2f} \text{{ mm}}^4\\\\')) + I_s_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.7.1.2, Eq. 1.17]}\\')) + I_s_eq.append(NoEscape(r'\end{aligned}')) + else: + # condition_status = rf'Since \dfrac{{c}}{{d}} = {c_d_ratio:.3f} < \sqrt{{2}}' + # I_s >= 1.5 d^3 tw^3 / c^2 (IS 800:2007 Cl.8.7.1.2). Guard against a + # zero stiffener spacing (no intermediate stiffener) to avoid ZeroDivisionError. + I_s_min = round((1.5 * (d**3) * (tw**3)) / (c**2), 2) if c > 0 else 0 - I_s_eq = Math(inline=True) - I_s_eq.append(NoEscape(r'\begin{aligned}\\')) - I_s_eq.append(NoEscape(rf'\text{{Since }} \dfrac{{c}}{{d}} &= {c_d_ratio:.3f} < \sqrt{{2}}\\\\')) - I_s_eq.append(NoEscape(r'I_s &\geq 1.5 \, \dfrac{d^3 t_w^3}{c^2}\\\\')) - I_s_eq.append(NoEscape(rf'&\geq 1.5 \times \dfrac{{({d:.1f})^3 \times ({tw:.1f})^3}}{{({c:.1f})^2}}\\\\')) - I_s_eq.append(NoEscape(rf'&\geq {I_s_min:.2f} \text{{ mm}}^4\\\\')) - I_s_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.7.1.2, Eq. 1.18]}\\')) - I_s_eq.append(NoEscape(r'\end{aligned}')) + I_s_eq = Math(inline=True) + I_s_eq.append(NoEscape(r'\begin{aligned}\\')) + I_s_eq.append(NoEscape(rf'\text{{Since }} \dfrac{{c}}{{d}} &= {c_d_ratio:.3f} < \sqrt{{2}}\\\\')) + I_s_eq.append(NoEscape(r'I_s &\geq 1.5 \, \dfrac{d^3 t_w^3}{c^2}\\\\')) + I_s_eq.append(NoEscape(rf'&\geq 1.5 \times \dfrac{{({d:.1f})^3 \times ({tw:.1f})^3}}{{({c:.1f})^2}}\\\\')) + I_s_eq.append(NoEscape(rf'&\geq {I_s_min:.2f} \text{{ mm}}^4\\\\')) + I_s_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.7.1.2, Eq. 1.18]}\\')) + I_s_eq.append(NoEscape(r'\end{aligned}')) - report_check.append([ - 'Minimum Moment of Inertia', - '', - I_s_eq, - '' - ]) + report_check.append([ + 'Minimum Moment of Inertia', + '', + I_s_eq, + '' + ]) - mu = 0.3 - tau_crc = round((kv * (3.14159**2) * E) / (12 * (1 - mu**2) * ((d/tw)**2)), 2) + mu = 0.3 + tau_crc = round((kv * (3.14159**2) * E) / (12 * (1 - mu**2) * ((d/tw)**2)), 2) - tau_crc_eq = Math(inline=True) - tau_crc_eq.append(NoEscape(r'\begin{aligned}\\')) - tau_crc_eq.append(NoEscape(r'\tau_{cr,e} &= \dfrac{K_v \pi^2 E}{12(1-\mu^2)(d/t_w)^2}\\\\')) - tau_crc_eq.append(NoEscape(rf'&= \dfrac{{{kv:.3f} \times \pi^2 \times {E}}}{{12(1-{mu}^2)({d:.1f}/{tw:.1f})^2}}\\\\')) - tau_crc_eq.append(NoEscape(rf'&= {tau_crc:.2f} \text{{ MPa}}\\\\')) - tau_crc_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2, Eq. 1.23]}\\')) - tau_crc_eq.append(NoEscape(r'\end{aligned}')) + tau_crc_eq = Math(inline=True) + tau_crc_eq.append(NoEscape(r'\begin{aligned}\\')) + tau_crc_eq.append(NoEscape(r'\tau_{cr,e} &= \dfrac{K_v \pi^2 E}{12(1-\mu^2)(d/t_w)^2}\\\\')) + tau_crc_eq.append(NoEscape(rf'&= \dfrac{{{kv:.3f} \times \pi^2 \times {E}}}{{12(1-{mu}^2)({d:.1f}/{tw:.1f})^2}}\\\\')) + tau_crc_eq.append(NoEscape(rf'&= {tau_crc:.2f} \text{{ MPa}}\\\\')) + tau_crc_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2, Eq. 1.23]}\\')) + tau_crc_eq.append(NoEscape(r'\end{aligned}')) - report_check.append([ - 'Critical Buckling Stres', - NoEscape(rf'$\mu = {mu}$'), - tau_crc_eq, - '' - ]) + report_check.append([ + 'Critical Buckling Stres', + NoEscape(rf'$\mu = {mu}$'), + tau_crc_eq, + '' + ]) # ==================== END PANEL STIFFENER - SECTION 1.5.2 ==================== - report_check.append(['SubSection', 'End Panel Stiffener', table_format]) + # End-panel anchor force / tension field is a thin-web concept; omit the whole + # subsection for 'Thick Web without ITS'. + show_end_panel = web_philosophy != 'Thick Web without ITS' + if show_end_panel: + report_check.append(['SubSection', 'End Panel Stiffener', table_format]) + + # Anchor Force Hq + # Both Vcr (pg_obj.V_cr) and Vp are in Newtons, so the ratio is Vcr/Vp directly. + Vcr = getattr(pg_obj, 'V_cr', 0) + Vp = (d * tw * fy) / math.sqrt(3) + tension_field_developed = Vp > 0 and Vcr < Vp + if tension_field_developed: + rad = max(0, 1 - (Vcr / Vp)) + Hq = 1.5 * Vp * math.sqrt(rad) + else: + # Vcr >= Vp: the web reaches its plastic shear capacity before buckling, + # so no tension field develops. The term (1 - Vcr/Vp) would be negative, + # hence Hq is taken as 0 (IS 800:2007, Cl.8.5.3). + Hq = 0 - # Vertical Anchor Force - Vp = round((d * tw * fy) / (3**0.5), 2) + Hq_kN = round(Hq / 1000, 2) Vp_kN = round(Vp / 1000, 2) + Vcr_kN = round(Vcr / 1000, 2) Vp_eq = Math(inline=True) Vp_eq.append(NoEscape(r'\begin{aligned}\\')) - Vp_eq.append(NoEscape(r'V_p &= \dfrac{d \cdot t_w \cdot f_y}{\sqrt{3}}\\\\')) - Vp_eq.append(NoEscape(rf'&= \dfrac{{{d:.1f} \times {tw:.1f} \times {fy:.1f}}}{{\sqrt{{3}}}}\\\\')) - Vp_eq.append(NoEscape(rf'&= {Vp_kN:.2f} \text{{ kN}}\\\\')) - Vp_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2]}\\')) + Vp_eq.append(NoEscape(r'H_q &= 1.5 V_p \left(1 - \dfrac{V_{cr}}{V_p}\right)^{1/2}\\\\')) + Vp_eq.append(NoEscape(rf'&= 1.5 \times {Vp_kN:.2f} \left(1 - \dfrac{{{Vcr_kN:.2f}}}{{{Vp_kN:.2f}}}\right)^{{1/2}}\\\\')) + Vp_eq.append(NoEscape(rf'&= {Hq_kN:.2f} \text{{ kN}}\\\\')) + if not tension_field_developed: + Vp_eq.append(NoEscape(r'&\text{Since } V_{cr} \geq V_p,\ H_q \text{ is taken as 0}\\\\')) + Vp_eq.append(NoEscape(r'&\text{(no tension field develops)}\\\\')) + Vp_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.5.3]}\\')) Vp_eq.append(NoEscape(r'\end{aligned}')) - report_check.append([ - 'Vertical Anchor Force', - '', - Vp_eq, - '' - ]) + if show_end_panel: + report_check.append([ + 'Vertical Anchor Force', + '', + Vp_eq, + '' + ]) # Tension Flange Reaction - Rtf = round(Vp_kN / 2, 2) + Rtf = round(Hq_kN / 2, 2) Rtf_eq = Math(inline=True) Rtf_eq.append(NoEscape(r'\begin{aligned}\\')) - Rtf_eq.append(NoEscape(r'R_{tf} &= \dfrac{V_p}{2}\\\\')) - Rtf_eq.append(NoEscape(rf'&= \dfrac{{{Vp_kN:.2f}}}{{2}}\\\\')) + Rtf_eq.append(NoEscape(r'R_{tf} &= \dfrac{H_q}{2}\\\\')) + Rtf_eq.append(NoEscape(rf'&= \dfrac{{{Hq_kN:.2f}}}{{2}}\\\\')) Rtf_eq.append(NoEscape(rf'&= {Rtf:.2f} \text{{ kN}}\\\\')) - Rtf_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2]}\\')) + Rtf_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.5.3]}\\')) Rtf_eq.append(NoEscape(r'\end{aligned}')) - report_check.append([ - 'Tension Flange Reaction', - '', - Rtf_eq, - '' - ]) + if show_end_panel: + report_check.append([ + 'Tension Flange Reaction', + '', + Rtf_eq, + '' + ]) # Tension Flange Moment - Mtf = round(Vp_kN * d / 10, 2) + Mtf = round(Hq_kN * d / 10, 2) Mtf_eq = Math(inline=True) Mtf_eq.append(NoEscape(r'\begin{aligned}\\')) - Mtf_eq.append(NoEscape(r'M_{tf} &= \dfrac{V_p \cdot d}{10}\\\\')) - Mtf_eq.append(NoEscape(rf'&= \dfrac{{{Vp_kN:.2f} \times {d:.1f}}}{{10}}\\\\')) + Mtf_eq.append(NoEscape(r'M_{tf} &= \dfrac{H_q \cdot d}{10}\\\\')) + Mtf_eq.append(NoEscape(rf'&= \dfrac{{{Hq_kN:.2f} \times {d:.1f}}}{{10}}\\\\')) Mtf_eq.append(NoEscape(rf'&= {Mtf:.2f} \text{{ kN-mm}}\\\\')) - Mtf_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.4.2.2]}\\')) + Mtf_eq.append(NoEscape(r'&\text{[Ref: IS 800:2007, Cl.8.5.3]}\\')) Mtf_eq.append(NoEscape(r'\end{aligned}')) - report_check.append([ - 'Tension Flange Moment', - '', - Mtf_eq, - '' - ]) + if show_end_panel: + report_check.append([ + 'Tension Flange Moment', + '', + Mtf_eq, + '' + ]) if web_philosophy != "Thick Web without ITS": # Calculate end panel stiffener thickness based on tension flange reaction - Vp = (d * tw * fy) / math.sqrt(3) - Rtf = Vp / 2 # Tension flange reaction + # Design stiffener for Rtf (which is Hq/2) + # Rtf is calculated above as Hq_kN / 2. We convert back to N for stress checks. + Rtf_N = Hq / 2 # Design stiffener for Rtf # Assume stiffener is a pair on both sides of web @@ -1062,7 +1328,7 @@ def prepare_design_checks(pg_obj, logger): # Solving for t_stiff: t_stiff ≥ (Rtf × γm0) / (2 × stiff_width × fy) if stiff_width > 0: - t_req = (Rtf * gamma_m0) / (2 * stiff_width * fy) + t_req = (Rtf_N * gamma_m0) / (2 * stiff_width * fy) # Use thickness from pg_obj if available (Optimization source of truth) t_provided_obj = getattr(pg_obj, 'end_stiffthickness', 0) @@ -1092,6 +1358,18 @@ def prepare_design_checks(pg_obj, logger): pg_obj.endpanelstiffenerthickness = "NA" pg_obj.endstiffthickness = 0 + except Exception as e: + # A failure while building the primary subsections (Classification, Shear, + # Moment, Deflection, Weld, Intermediate/End-Panel Stiffener) must NOT drop the + # stiffener sections below. Log and fall through to the guaranteed block. + logger.error(f"Error preparing primary design-check sections: {str(e)}") + import traceback + logger.error(f"Traceback: {traceback.format_exc()}") + + # The Longitudinal Stiffener, Stiffener Design Summary and Overall Design Check + # subsections are built in a separate guarded block so they are always emitted, + # giving the report a consistent structure regardless of upstream failures. + try: # ==================== LONGITUDINAL STIFFENER - SECTION 1.5.3 ==================== report_check.append(['SubSection', 'Longitudinal Stiffener', table_format]) @@ -1233,9 +1511,10 @@ def prepare_design_checks(pg_obj, logger): t_end_stiff = getattr(pg_obj, 'endstiffthickness', 0) # Determine Longitudinal Stiffener values based on requirement - if long_stiff_required: - t_long_stiff = getattr(pg_obj, 'longstiffenerthk', 'NA') - num_long = getattr(pg_obj, 'longstiffenerno', 'Not Required') + user_long_stiff = getattr(pg_obj, 'long_Stiffner', 'No') in ['Yes and 1 stiffener', 'Yes and 2 stiffeners'] + if long_stiff_required or user_long_stiff: + t_long_stiff = getattr(pg_obj, 'longstiffener_thk', 'NA') + num_long = getattr(pg_obj, 'longstiffener_no', 'Not Required') stiff_1_pos = getattr(pg_obj, 'x1', 'Not Required') stiff_2_pos = getattr(pg_obj, 'x2', 'Not Required') else: @@ -1371,8 +1650,22 @@ def prepare_design_checks(pg_obj, logger): logger.info("DDCL-compliant design checks prepared successfully") except Exception as e: - logger.error(f"Error preparing design checks: {str(e)}") + logger.error(f"Error preparing stiffener/overall design-check sections: {str(e)}") import traceback logger.error(f"Traceback: {traceback.format_exc()}") + # Guarantee a consistent report structure: the stiffener sections must always be + # present. If an exception prevented either header from being appended, add it now + # with an explanatory placeholder row instead of omitting the section entirely. + existing_subsections = {row[1] for row in report_check + if len(row) >= 2 and row[0] == 'SubSection'} + if 'Longitudinal Stiffener' not in existing_subsections: + report_check.append(['SubSection', 'Longitudinal Stiffener', table_format]) + report_check.append(['Longitudinal Stiffener', '', + 'Not evaluated for this design case (see design log)', '']) + if 'Stiffener Design Summary' not in existing_subsections: + report_check.append(['SubSection', 'Stiffener Design Summary', table_format]) + report_check.append(['Stiffener Design Summary', '', + 'Not evaluated for this design case (see design log)', '']) + return report_check \ No newline at end of file diff --git a/src/osdag_gui/ui/components/docks/input_dock.py b/src/osdag_gui/ui/components/docks/input_dock.py index d3b61df0b..19b3c648d 100644 --- a/src/osdag_gui/ui/components/docks/input_dock.py +++ b/src/osdag_gui/ui/components/docks/input_dock.py @@ -505,20 +505,17 @@ def show_lock_tooltip(self): self.tooltip_timer.start(3000) def toggle_lock(self, set_locked_state=False): - if set_locked_state: - self.state_locked = True - self.lock_btn.setChecked(True) - self.input_widget.setDisabled(True) - self.update_lock_icon() - else: - if self.state_locked: - # Use CleanupCoordinator for redesign cleanup - coordinator = get_cleanup_coordinator() - coordinator.cleanup_for_redesign(self.parent) - self.state_locked = not self.state_locked - self.lock_btn.setChecked(self.state_locked) - self.input_widget.setDisabled(self.state_locked) - self.update_lock_icon() + if set_locked_state == self.state_locked: + return + + if self.state_locked and not set_locked_state: + coordinator = get_cleanup_coordinator() + coordinator.cleanup_for_redesign(self.parent) + + self.state_locked = set_locked_state + self.lock_btn.setChecked(self.state_locked) + self.input_widget.setDisabled(self.state_locked) + self.update_lock_icon() def update_lock_icon(self): if self.state_locked: diff --git a/src/osdag_gui/ui/windows/additional_inputs.py b/src/osdag_gui/ui/windows/additional_inputs.py index 789daff9f..fc5537345 100644 --- a/src/osdag_gui/ui/windows/additional_inputs.py +++ b/src/osdag_gui/ui/windows/additional_inputs.py @@ -366,6 +366,29 @@ def initUI(self, main, input_dictionary): combo.model().item(2).setEnabled(False) if input_dictionary: combo.setCurrentText(str(element[4])) + # Web philosophy is the single source of truth for intermediate + # stiffeners: auto-sync the (now redundant) Yes/No control to match + # and disable it so it cannot contradict the selected philosophy. + if element[0] == KEY_IntermediateStiffener: + wp = input_dictionary.get(KEY_WEB_PHILOSOPHY, '') if input_dictionary else '' + if wp == KEY_DISP_PHILO2: # Thick Web without ITS + combo.setCurrentText('No') + combo.setEnabled(False) + combo.setToolTip("Not Applicable: 'Thick Web without ITS' has no intermediate stiffeners") + elif wp == KEY_DISP_PHILO1: # Thin Web with ITS + combo.setCurrentText('Yes') + combo.setEnabled(False) + combo.setToolTip("Intermediate stiffeners are mandatory for 'Thin Web with ITS'") + # Deflection 'Supporting Options': disable + explain when the only + # available option is 'Not Applicable' for the selected member option. + # Show the message inline in the field body (not just as a tooltip), + # widening the field so the full text is readable. + if element[0] == KEY_SUPPORTING_OPTIONS and list(element[3]) == list(VALUES_SUPPORTING_OPTIONS_DEF): + combo.setEnabled(False) + combo.clear() + combo.addItem("Not Applicable for the selected member option") + combo.setFixedSize(280, 22) + combo.setToolTip("Not Applicable for the selected member option") if element[0] in self.save_changes_list: self.connect_widget_for_change(combo) r += 1 diff --git a/src/osdag_gui/ui/windows/template_page.py b/src/osdag_gui/ui/windows/template_page.py index 64882b35c..a0db3fdde 100644 --- a/src/osdag_gui/ui/windows/template_page.py +++ b/src/osdag_gui/ui/windows/template_page.py @@ -1646,14 +1646,12 @@ def common_function_for_save_and_design(self, main, data, trigger_type): # They force OCC wrapper cleanup in arbitrary order, causing heap corruption. # The OCC memory manager and Qt event loop handle cleanup safely. - # CRITICAL FIX: Only generate 3D model if CAD widget is visible and parented. - # If running PSO optimization, CAD widget is hidden/detached, and generating - # 3D model here causes a crash (double free / list corruption). - # In that case, pso_ui_manager will trigger rendering after restoring the widget. - if self.cad_widget.isVisible() and self.cad_widget.parent() is not None: + # Skip 3D render when PSO timer will handle it + pso_pending = self._pso_manager is not None and self._pso_manager._render_pending + if not pso_pending and self.cad_widget.isVisible() and self.cad_widget.parent() is not None: self._render_3d_result(status, main) else: - print("[DEBUG] Skipping 3D model generation (CAD widget hidden/detached)") + print("[DEBUG] Skipping 3D model generation (PSO active or CAD widget hidden)") def _render_3d_result(self, status, main): """Helper to render 3D model and update UI state. @@ -1911,6 +1909,19 @@ def tab_change(self, key, tab, new, main): k2.clear() for values in val[k2_key_name]: k2.addItem(str(values)) + # If the repopulated list is the 'Not Applicable' fallback, disable + # the field and show the message inline in the field body (widened + # so it is readable) instead of a bare selectable "NA". + if list(val[k2_key_name]) == list(VALUES_SUPPORTING_OPTIONS_DEF): + k2.setEnabled(False) + k2.clear() + k2.addItem("Not Applicable for the selected member option") + k2.setFixedSize(280, 22) + k2.setToolTip("Not Applicable for the selected member option") + else: + k2.setEnabled(True) + k2.setFixedSize(120, 22) + k2.setToolTip("") if isinstance(k2, QLineEdit): k2.setText(str(val[k2_key_name])) if isinstance(k2, QLabel): @@ -2043,7 +2054,47 @@ def capture_design_pref_values(self): self.design_pref_inputs[key_name] = key.text() elif isinstance(key, QComboBox): self.design_pref_inputs[key_name] = key.currentText() - + + self._sync_section_material_to_input_dock() + + def _sync_section_material_to_input_dock(self): + """Propagate the section/girder material chosen in Additional Inputs back to the + main input-dock Material field. + + The design consumes ``KEY_MATERIAL`` (the input-dock material), not the + design-preference ``KEY_SEC_MATERIAL``. Without this sync, editing the material in + Additional Inputs (girder properties) has no effect on the design and the input + dock keeps showing the old grade. Only applies to modules that expose a single + ``KEY_MATERIAL`` combobox in the input dock (findChild returns None otherwise). + """ + sec_material = self.design_pref_inputs.get(KEY_SEC_MATERIAL) + if not sec_material or str(sec_material) in ['', 'Select Material']: + return + + input_dock_material = self.input_dock.input_widget.findChild(QWidget, KEY_MATERIAL) + if not isinstance(input_dock_material, QComboBox): + return + + if input_dock_material.currentText() == str(sec_material): + return + + validator = MaterialValidator(sec_material) + if validator.is_already_in_db(): + pass + elif validator.is_format_custom() and validator.is_valid_custom(): + # Register the new custom grade in the DB and refresh the combo items + self.update_material_db(grade=sec_material, material=validator) + input_dock_material.clear() + for item in connectdb("Material"): + input_dock_material.addItem(item) + else: + # Unknown / invalid grade - do not touch the input dock + return + + if input_dock_material.findText(str(sec_material)) == -1: + input_dock_material.addItem(str(sec_material)) + input_dock_material.setCurrentText(str(sec_material)) + # ============================= Additional Inputs Connectors Ends ========================== # This is to save Design as Project if Design is already done