From 584c12c764e17381369a4957031229f00e368fb8 Mon Sep 17 00:00:00 2001 From: robbievanleeuwen Date: Thu, 11 Dec 2025 13:42:14 +1100 Subject: [PATCH] Add display transformed results, update docs --- .../materials/composite_analysis.ipynb | 34 +- docs/examples/results/display_results.ipynb | 7 +- docs/examples/results/get_results.ipynb | 20 +- docs/user_guide/results.rst | 10 +- src/sectionproperties/analysis/section.py | 30 ++ src/sectionproperties/post/post.py | 296 ++++++++++++++++++ 6 files changed, 384 insertions(+), 13 deletions(-) diff --git a/docs/examples/materials/composite_analysis.ipynb b/docs/examples/materials/composite_analysis.ipynb index 6df52e08..feaf750c 100644 --- a/docs/examples/materials/composite_analysis.ipynb +++ b/docs/examples/materials/composite_analysis.ipynb @@ -345,7 +345,7 @@ "id": "29", "metadata": {}, "source": [ - "A plastic analysis for composite sections will calculate plastic moments rather than plastic section moduli. The plastic moment assumes all geometry fibres reach the yield strength." + "Further, we can display the transformed results with respect to an elastic modulus by calling the `display_transformed_results()` method. Here we print the transformed section properties with respect to the timber." ] }, { @@ -354,6 +354,24 @@ "id": "30", "metadata": {}, "outputs": [], + "source": [ + "sec.display_transformed_results(e_ref=timber)" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "A plastic analysis for composite sections will calculate plastic moments rather than plastic section moduli. The plastic moment assumes all geometry fibres reach the yield strength." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], "source": [ "mp_xx, _ = sec.get_mp()\n", "print(f\"Mp = {mp_xx / 1e6:.1f} kN.m\")" @@ -361,7 +379,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "33", "metadata": {}, "source": [ "### Stress Analysis" @@ -370,7 +388,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -380,7 +398,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -390,7 +408,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -399,7 +417,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "37", "metadata": {}, "source": [ "We can plot only a specific list of materials by including the `material_list` argument. In the above plot it is difficult to see the stress in the timber so we set `material_list=[timber]`." @@ -408,7 +426,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -432,7 +450,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.17" + "version": "3.14.0" } }, "nbformat": 4, diff --git a/docs/examples/results/display_results.ipynb b/docs/examples/results/display_results.ipynb index 38186918..26c25099 100644 --- a/docs/examples/results/display_results.ipynb +++ b/docs/examples/results/display_results.ipynb @@ -114,7 +114,7 @@ "id": "11", "metadata": {}, "source": [ - "Because we have not specified any material properties, the displayed properties are purely geometric. If we assign a steel material to the CHS, we will see some results change to material property weighted values (see [here](../../user_guide/results.rst#how-material-properties-affect-results) for more information on how material properties affect results)." + "Because we have not specified any material properties, the displayed properties are purely geometric. If we assign a steel material to the CHS, we will see some results change to material property weighted values (see [here](../../user_guide/results.rst#how-material-properties-affect-results) for more information on how material properties affect results). We can also print the transformed results using the `display_transformed_results()` method." ] }, { @@ -145,7 +145,8 @@ "sec.calculate_geometric_properties()\n", "sec.calculate_warping_properties()\n", "sec.calculate_plastic_properties()\n", - "sec.display_results(fmt=\".3e\")" + "sec.display_results(fmt=\".3e\")\n", + "sec.display_transformed_results(e_ref=steel, fmt=\".3e\")" ] } ], @@ -165,7 +166,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.17" + "version": "3.14.0" } }, "nbformat": 4, diff --git a/docs/examples/results/get_results.ipynb b/docs/examples/results/get_results.ipynb index 5dad5786..d93407fc 100644 --- a/docs/examples/results/get_results.ipynb +++ b/docs/examples/results/get_results.ipynb @@ -270,6 +270,24 @@ "source": [ "print(f\"I_rect = {300 * 600**3 / 12:.3e} mm4\")" ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "Finally, we can print the transformed section properties using the `display_transformed_results()` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "sec.display_transformed_results(e_ref=concrete, fmt=\".3e\")" + ] } ], "metadata": { @@ -288,7 +306,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.17" + "version": "3.14.0" } }, "nbformat": 4, diff --git a/docs/user_guide/results.rst b/docs/user_guide/results.rst index 20cbbc1a..59354248 100644 --- a/docs/user_guide/results.rst +++ b/docs/user_guide/results.rst @@ -7,11 +7,19 @@ Displaying the Results A list of cross-section properties that have been calculated by the performed analyses can be printed to the terminal using the :meth:`~sectionproperties.analysis.section.Section.display_results` method that belongs -to every :class:`~sectionproperties.analysis.section.Section` object. +to every :class:`~sectionproperties.analysis.section.Section` object. .. automethod:: sectionproperties.analysis.section.Section.display_results :noindex: +If a composite analysis has been performed, the transformed properties can be printed to +the terminal using the +:meth:`~sectionproperties.analysis.section.Section.display_transformed_results` method +that belongs to every :class:`~sectionproperties.analysis.section.Section` object. + +.. automethod:: sectionproperties.analysis.section.Section.display_transformed_results + :noindex: + Retrieving Section Properties ----------------------------- diff --git a/src/sectionproperties/analysis/section.py b/src/sectionproperties/analysis/section.py index 1ce58e53..cee7965a 100644 --- a/src/sectionproperties/analysis/section.py +++ b/src/sectionproperties/analysis/section.py @@ -1873,6 +1873,36 @@ def display_results( """ post.print_results(section=self, fmt=fmt) + def display_transformed_results( + self, + e_ref: float | pre.Material, + fmt: str = "8.6e", + ) -> None: + """Prints the transformed results that have been calculated to the terminal. + + All results that are scaled by the elastic modulus in ``display_results()`` + (e.g. ``e.ixx_c``) are divided by ``e_ref``. + + This is a composite only method, as such this can only be called if material + properties have been applied to the cross-section. + + Args: + e_ref: Reference elastic modulus or material property by which to transform + the results. + fmt: Number formatting string, see + https://docs.python.org/3/library/string.html. Defaults to ``"8.6e"``. + + Raises: + RuntimeError: If material properties have *not* been applied + """ + if not self.is_composite(): + msg = "Attempting to call a composite only method for a geometric analysis" + msg += " (material properties have not been applied). Consider using" + msg += " display_results()." + raise RuntimeError(msg) + + post.print_transformed_results(section=self, e_ref=e_ref, fmt=fmt) + def is_composite(self) -> bool: """Returns whether or not a composite section is being analysed. diff --git a/src/sectionproperties/post/post.py b/src/sectionproperties/post/post.py index 5dd6ecad..8eb65718 100644 --- a/src/sectionproperties/post/post.py +++ b/src/sectionproperties/post/post.py @@ -13,6 +13,7 @@ from rich.table import Table import sectionproperties.analysis.fea as fea +import sectionproperties.pre.pre as pre if TYPE_CHECKING: from collections.abc import Generator @@ -935,3 +936,298 @@ def print_results( console = Console() console.print(table) print("") + + +def print_transformed_results( + section: Section, + e_ref: float | pre.Material, + fmt: str, +) -> None: + """Prints the results that have been calculated to the terminal. + + Args: + section: Section object + e_ref: Reference elastic modulus or material property by which to transform + the results. + fmt: Number formatting string + """ + # assign reference elastic modulus + er = e_ref.elastic_modulus if isinstance(e_ref, pre.Material) else e_ref + + table = Table(title="Transformed Section Properties") + table.add_column("Property", justify="left", style="cyan", no_wrap=True) + table.add_column("Value", justify="right", style="green") + + # print geometric area + try: + area = section.get_area() + table.add_row("geom area", f"{area:>{fmt}}") + except RuntimeError: + pass + + # print cross-section perimeter + try: + perimeter = section.get_perimeter() + table.add_row("perimeter", f"{perimeter:>{fmt}}") + except RuntimeError: + pass + + # print cross-section mass + try: + mass = section.get_mass() + table.add_row("mass", f"{mass:>{fmt}}") + except RuntimeError: + pass + + # print e_ref + table.add_row("e_ref", f"{er:>{fmt}}") + + # print cross-section ea/e_ref + try: + ea = section.get_ea() / er + table.add_row("a", f"{ea:>{fmt}}") + except RuntimeError: + pass + + # print cross-section eq/e_ref + try: + eqx, eqy = section.get_eq() + table.add_row("qx", f"{eqx / er:>{fmt}}") + table.add_row("qy", f"{eqy / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section ig/e_ref + try: + eixx_g, eiyy_g, eixy_g = section.get_eig() + table.add_row("ixx_g", f"{eixx_g / er:>{fmt}}") + table.add_row("iyy_g", f"{eiyy_g / er:>{fmt}}") + table.add_row("ixy_g", f"{eixy_g / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section centroid + try: + cx, cy = section.get_c() + table.add_row("cx", f"{cx:>{fmt}}") + table.add_row("cy", f"{cy:>{fmt}}") + except RuntimeError: + pass + + # print cross-section ic/e_ref + try: + eixx_c, eiyy_c, eixy_c = section.get_eic() + table.add_row("ixx_c", f"{eixx_c / er:>{fmt}}") + table.add_row("iyy_c", f"{eiyy_c / er:>{fmt}}") + table.add_row("ixy_c", f"{eixy_c / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section z/e_ref + try: + ezxx_plus, ezxx_minus, ezyy_plus, ezyy_minus = section.get_ez() + table.add_row("zxx+", f"{ezxx_plus / er:>{fmt}}") + table.add_row("zxx-", f"{ezxx_minus / er:>{fmt}}") + table.add_row("zyy+", f"{ezyy_plus / er:>{fmt}}") + table.add_row("zyy-", f"{ezyy_minus / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section my + try: + my_xx, my_yy = section.get_my() + table.add_row("my_xx", f"{my_xx:>{fmt}}") + table.add_row("my_yy", f"{my_yy:>{fmt}}") + except RuntimeError: + pass + + # print cross-section rc + try: + rx, ry = section.get_rc() + table.add_row("rx", f"{rx:>{fmt}}") + table.add_row("ry", f"{ry:>{fmt}}") + except RuntimeError: + pass + + # print cross-section ip/e_ref + try: + ei11_c, ei22_c = section.get_eip() + table.add_row("i11_c", f"{ei11_c / er:>{fmt}}") + table.add_row("i22_c", f"{ei22_c / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section phi + try: + phi = section.get_phi() + table.add_row("phi", f"{phi:>{fmt}}") + except RuntimeError: + pass + + # print cross-section zp/e_ref + try: + ez11_plus, ez11_minus, ez22_plus, ez22_minus = section.get_ezp() + table.add_row("z11+", f"{ez11_plus / er:>{fmt}}") + table.add_row("z11-", f"{ez11_minus / er:>{fmt}}") + table.add_row("z22+", f"{ez22_plus / er:>{fmt}}") + table.add_row("z22-", f"{ez22_minus / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section my_p + try: + my_11, my_22 = section.get_my_p() + table.add_row("my_11", f"{my_11:>{fmt}}") + table.add_row("my_22", f"{my_22:>{fmt}}") + except RuntimeError: + pass + + # print cross-section rp + try: + r11, r22 = section.get_rp() + table.add_row("r11", f"{r11:>{fmt}}") + table.add_row("r22", f"{r22:>{fmt}}") + except RuntimeError: + pass + + # print effective material properties + try: + e_eff = section.get_e_eff() + g_eff = section.get_g_eff() + nu_eff = section.get_nu_eff() + table.add_row("e_eff", f"{e_eff:>{fmt}}") + table.add_row("g_eff", f"{g_eff:>{fmt}}") + table.add_row("nu_eff", f"{nu_eff:>{fmt}}") + except RuntimeError: + pass + + # print cross-section j/e_ref + try: + ej = section.get_ej() + table.add_row("j", f"{ej / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section sc + try: + x_se, y_se = section.get_sc() + table.add_row("x_se", f"{x_se:>{fmt}}") + table.add_row("y_se", f"{y_se:>{fmt}}") + except RuntimeError: + pass + + # print cross-section sc_p + try: + x1_se, y2_se = section.get_sc_p() + table.add_row("x1_se", f"{x1_se:>{fmt}}") + table.add_row("y2_se", f"{y2_se:>{fmt}}") + except RuntimeError: + pass + + # print cross-section sc_t + try: + x_st, y_st = section.get_sc_t() + table.add_row("x_st", f"{x_st:>{fmt}}") + table.add_row("y_st", f"{y_st:>{fmt}}") + except RuntimeError: + pass + + # print cross-section gamma/e_ref + try: + egamma = section.get_egamma() + table.add_row("gamma", f"{egamma / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section as/e_ref + try: + ea_sx, ea_sy = section.get_eas() + table.add_row("a_sx", f"{ea_sx / er:>{fmt}}") + table.add_row("a_sy", f"{ea_sy / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section as_p/e_ref + try: + ea_s11, ea_s22 = section.get_eas_p() + table.add_row("a_s11", f"{ea_s11 / er:>{fmt}}") + table.add_row("a_s22", f"{ea_s22 / er:>{fmt}}") + except RuntimeError: + pass + + # print cross-section beta + try: + beta_x_plus, beta_x_minus, beta_y_plus, beta_y_minus = section.get_beta() + table.add_row("beta_x+", f"{beta_x_plus:>{fmt}}") + table.add_row("beta_x-", f"{beta_x_minus:>{fmt}}") + table.add_row("beta_y+", f"{beta_y_plus:>{fmt}}") + table.add_row("beta_y-", f"{beta_y_minus:>{fmt}}") + except RuntimeError: + pass + + # print cross-section beta_p + try: + beta_11_plus, beta_11_minus, beta_22_plus, beta_22_minus = section.get_beta_p() + table.add_row("beta_11+", f"{beta_11_plus:>{fmt}}") + table.add_row("beta_11-", f"{beta_11_minus:>{fmt}}") + table.add_row("beta_22+", f"{beta_22_plus:>{fmt}}") + table.add_row("beta_22-", f"{beta_22_minus:>{fmt}}") + except RuntimeError: + pass + + # print cross-section pc + try: + x_pc, y_pc = section.get_pc() + table.add_row("x_pc", f"{x_pc:>{fmt}}") + table.add_row("y_pc", f"{y_pc:>{fmt}}") + except RuntimeError: + pass + + # print cross-section pc_p + try: + x11_pc, y22_pc = section.get_pc_p() + table.add_row("x11_pc", f"{x11_pc:>{fmt}}") + table.add_row("y22_pc", f"{y22_pc:>{fmt}}") + except RuntimeError: + pass + + # print cross-section s/mp + try: + mp_xx, mp_yy = section.get_mp() + table.add_row("mp_xx", f"{mp_xx:>{fmt}}") + table.add_row("mp_yy", f"{mp_yy:>{fmt}}") + + except RuntimeError: + pass + + # print cross-section sp/mp_p + try: + mp_11, mp_22 = section.get_mp_p() + table.add_row("mp_11", f"{mp_11:>{fmt}}") + table.add_row("mp_22", f"{mp_22:>{fmt}}") + except RuntimeError: + pass + + # print cross-section sf + try: + sf_xx_plus, sf_xx_minus, sf_yy_plus, sf_yy_minus = section.get_sf() + table.add_row("sf_xx+", f"{sf_xx_plus:>{fmt}}") + table.add_row("sf_xx-", f"{sf_xx_minus:>{fmt}}") + table.add_row("sf_yy+", f"{sf_yy_plus:>{fmt}}") + table.add_row("sf_yy-", f"{sf_yy_minus:>{fmt}}") + except RuntimeError: + pass + + # print cross-section sf_p + try: + sf_11_plus, sf_11_minus, sf_22_plus, sf_22_minus = section.get_sf_p() + table.add_row("sf_11+", f"{sf_11_plus:>{fmt}}") + table.add_row("sf_11-", f"{sf_11_minus:>{fmt}}") + table.add_row("sf_22+", f"{sf_22_plus:>{fmt}}") + table.add_row("sf_22-", f"{sf_22_minus:>{fmt}}") + except RuntimeError: + pass + + console = Console() + console.print(table) + print("")