|
7 | 7 | from dataclasses import dataclass, field, fields |
8 | 8 |
|
9 | 9 | import numpy as np |
10 | | -from numpy.typing import ArrayLike |
| 10 | +from numpy.typing import ArrayLike, NDArray |
11 | 11 | from shapely import Point |
12 | 12 |
|
13 | 13 |
|
@@ -800,6 +800,184 @@ def get_point_stress( |
800 | 800 | ) |
801 | 801 |
|
802 | 802 |
|
| 803 | +@dataclass(slots=True) |
| 804 | +class StrainProfileResult: |
| 805 | + """Class for storing the results from calculate_strain_profile method.""" |
| 806 | + |
| 807 | + # Solved generalized strains |
| 808 | + eps_a: float = 0.0 # the axial strain at 0, 0 |
| 809 | + chi_y: float = 0.0 # the curvature respect y axes |
| 810 | + chi_z: float = 0.0 # the curvature respect z axes |
| 811 | + |
| 812 | + # target external loads assigned by user |
| 813 | + n_ext: float = 0.0 # Target axial load acting at 0, 0 |
| 814 | + m_y_ext: float = 0.0 # Target moment My |
| 815 | + m_z_ext: float = 0.0 # Target moment Mz |
| 816 | + |
| 817 | + # Actual integrated loads at the converged strain state |
| 818 | + n: float = 0.0 # Axial load acting at 0, 0 |
| 819 | + m_y: float = 0.0 # Bending moment My |
| 820 | + m_z: float = 0.0 # Bending moment Mz |
| 821 | + |
| 822 | + # Solver settings and diagnostics |
| 823 | + tolerance: float = 0.0 |
| 824 | + max_iter: int = 0 |
| 825 | + used_initial_tangent: bool = False |
| 826 | + iterations: int = 0 |
| 827 | + converged: bool = False |
| 828 | + residual: NDArray[np.float64] = field( |
| 829 | + default_factory=lambda: np.zeros(3, dtype=float) |
| 830 | + ) |
| 831 | + |
| 832 | + # Iteration history |
| 833 | + residual_history: list[NDArray[np.float64]] = field(default_factory=list) |
| 834 | + strain_history: list[NDArray[np.float64]] = field(default_factory=list) |
| 835 | + |
| 836 | + # For context store the section |
| 837 | + section: t.Any = None # Note for future: if I want to type this I also have problem of circular import? #noqa E501 |
| 838 | + # The detailed result data structure |
| 839 | + detailed_result: SectionDetailedResultState = None |
| 840 | + |
| 841 | + @property |
| 842 | + def residual_norm_history(self) -> t.List: |
| 843 | + """Returns the history of residual norm.""" |
| 844 | + return [float(np.linalg.norm(x)) for x in self.residual_history] |
| 845 | + |
| 846 | + @property |
| 847 | + def delta_strain_history(self) -> t.List: |
| 848 | + """Returns as a list the history of delta_strain.""" |
| 849 | + return [ |
| 850 | + self.strain_history[i] - self.strain_history[i - 1] |
| 851 | + for i in range(1, len(self.strain_history)) |
| 852 | + ] |
| 853 | + |
| 854 | + @property |
| 855 | + def delta_strain_norm_history(self) -> t.List: |
| 856 | + """Returns as a list the history of norm of delta_strain.""" |
| 857 | + return [float(np.linalg.norm(x)) for x in self.delta_strain_history] |
| 858 | + |
| 859 | + @property |
| 860 | + def response_history(self) -> t.List: |
| 861 | + """Returns as a list the response (i.e. internal forces) history.""" |
| 862 | + loads = np.array([self.n_ext, self.m_y_ext, self.m_z_ext]) |
| 863 | + return [(loads - x) for x in self.residual_history] |
| 864 | + |
| 865 | + @property |
| 866 | + def strain_plane(self) -> NDArray[np.float64]: |
| 867 | + """Returns the strain profile as a numpy array.""" |
| 868 | + return np.array([self.eps_a, self.chi_y, self.chi_z]) |
| 869 | + |
| 870 | + @property |
| 871 | + def residual_norm(self) -> float: |
| 872 | + """Returns the norm of the residual at last iteration.""" |
| 873 | + return float(np.linalg.norm(self.residual)) |
| 874 | + |
| 875 | + def to_list(self) -> t.List: |
| 876 | + """Returns the strain profile coefficients in a list.""" |
| 877 | + return [self.eps_a, self.chi_y, self.chi_z] |
| 878 | + |
| 879 | + def create_detailed_result(self, num_points=1000): |
| 880 | + """Create the detailed result object. |
| 881 | +
|
| 882 | + Arguments: |
| 883 | + num_points (int): Number of random points to sample for each |
| 884 | + surface geometry (default = 1000). |
| 885 | + """ |
| 886 | + self.detailed_result = SectionDetailedResultState( |
| 887 | + section=self.section, |
| 888 | + eps_a=self.eps_a, |
| 889 | + chi_y=self.chi_y, |
| 890 | + chi_z=self.chi_z, |
| 891 | + n=self.n, |
| 892 | + m_y=self.m_y, |
| 893 | + m_z=self.m_z, |
| 894 | + num_points=num_points, |
| 895 | + ) |
| 896 | + |
| 897 | + def get_point_strain( |
| 898 | + self, |
| 899 | + y: float, |
| 900 | + z: float, |
| 901 | + name: t.Optional[str] = None, |
| 902 | + group_label: t.Optional[str] = None, |
| 903 | + case_sensitive: bool = True, |
| 904 | + all_results: bool = False, |
| 905 | + ) -> float: |
| 906 | + """Return the strain at a given point (y,z). |
| 907 | +
|
| 908 | + Arguments: |
| 909 | + y (float): The y-coordinate of the point. |
| 910 | + z (float): The z-coordinate of the point. |
| 911 | + name (str, optional): The name of the surface geometry to check. |
| 912 | + group_label (str, optional): The group label of the surface |
| 913 | + geometry to check. |
| 914 | + case_sensitive (bool, optional): If True (default) the matching is |
| 915 | + case sensitive. |
| 916 | + all_results (bool): If True, return the strain for all geometries |
| 917 | + that matches the filters, otherwise return the strain for the |
| 918 | + first geometry that matches the filters (default False). |
| 919 | +
|
| 920 | + Returns: |
| 921 | + float: The strain at the given point, or None if the point is not |
| 922 | + within any of the geometries that match the filters. |
| 923 | + """ |
| 924 | + return _get_point_response( |
| 925 | + section=self.section, |
| 926 | + eps_a=self.eps_a, |
| 927 | + chi_y=self.chi_y, |
| 928 | + chi_z=self.chi_z, |
| 929 | + y=y, |
| 930 | + z=z, |
| 931 | + response_type='strain', |
| 932 | + name=name, |
| 933 | + group_label=group_label, |
| 934 | + case_sensitive=case_sensitive, |
| 935 | + all_results=all_results, |
| 936 | + ) |
| 937 | + |
| 938 | + def get_point_stress( |
| 939 | + self, |
| 940 | + y: float, |
| 941 | + z: float, |
| 942 | + name: t.Optional[str] = None, |
| 943 | + group_label: t.Optional[str] = None, |
| 944 | + case_sensitive: bool = True, |
| 945 | + all_results: bool = False, |
| 946 | + ) -> float: |
| 947 | + """Return the stress at a given point (y,z). |
| 948 | +
|
| 949 | + Arguments: |
| 950 | + y (float): The y-coordinate of the point. |
| 951 | + z (float): The z-coordinate of the point. |
| 952 | + name (str, optional): The pattern for filtering the geometries by |
| 953 | + their name. |
| 954 | + group_label (str, optional): The pattern for filtering the |
| 955 | + geometries by their group_label. |
| 956 | + case_sensitive (bool, optional): If True (default) the matching is |
| 957 | + case sensitive. |
| 958 | + all_results (bool): If True, return the stress for all geometries |
| 959 | + that matches the filters, otherwise return the stress for the |
| 960 | + first geometry that matches the filters (default False). |
| 961 | +
|
| 962 | + Returns: |
| 963 | + float: The strain at the given point, or None if the point is not |
| 964 | + within any of the geometries that match the filters. |
| 965 | + """ |
| 966 | + return _get_point_response( |
| 967 | + section=self.section, |
| 968 | + eps_a=self.eps_a, |
| 969 | + chi_y=self.chi_y, |
| 970 | + chi_z=self.chi_z, |
| 971 | + y=y, |
| 972 | + z=z, |
| 973 | + response_type='stress', |
| 974 | + name=name, |
| 975 | + group_label=group_label, |
| 976 | + case_sensitive=case_sensitive, |
| 977 | + all_results=all_results, |
| 978 | + ) |
| 979 | + |
| 980 | + |
803 | 981 | @dataclass |
804 | 982 | class InteractionDomain: |
805 | 983 | """Class for storing common data on all interaction domain results. |
|
0 commit comments