diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..6a50dfd8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,48 @@ +name: Tests + +on: + push: + branches: + - modern_dev + pull_request: + branches: + - modern_dev + +jobs: + pytest: + name: Python tests on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest] + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-dev.txt + python -m pip install -e . + + - name: Import package + run: python -c "import anystruct; print('anystruct import OK')" + + - name: Run pytest + run: python -m pytest + + - name: Run GUI smoke on Windows + if: runner.os == 'Windows' + run: python -m tests.gui_automatic_run + + - name: Run GUI smoke on Linux + if: runner.os == 'Linux' + run: xvfb-run -a python -m tests.gui_automatic_run diff --git a/.gitignore b/.gitignore index 0e82e245..983c3b32 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +.pytest_tmp/ cover/ # Translations @@ -161,4 +162,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/.idea/ANYstructure.iml b/.idea/ANYstructure.iml index aee9ef2d..79ac038b 100644 --- a/.idea/ANYstructure.iml +++ b/.idea/ANYstructure.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 481181c0..a0d21cbd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -27,43 +27,43 @@ include anystruct\ship_section_example.txt include anystruct\ANYstructure_documentation.pdf include anystruct\sections.csv include anystruct\bulb_anglebar_tbar_flatbar.csv -include anystruct\ml_files\CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_CSR-Tank_req_cl_predictor.pickle -include anystruct\ml_files\CL_CSR-Tank_req_cl_scaler.pickle -include anystruct\ml_files\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle -include anystruct\ml_files\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle -include anystruct\ml_files\CL_CSR-Tank_req_cl_predictor.pickle -include anystruct\ml_files\CL_CSR-Tank_req_cl_scaler.pickle -include anystruct\ml_files\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle -include anystruct\ml_files\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle \ No newline at end of file +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_CSR-Tank_req_cl_predictor.pickle +include anystruct\ml_files\CLPIPE_CL_CSR-Tank_req_cl_scaler.pickle +include anystruct\ml_files\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle +include anystruct\ml_files\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle +include anystruct\ml_files\CLPIPE_CL_CSR-Tank_req_cl_predictor.pickle +include anystruct\ml_files\CLPIPE_CL_CSR-Tank_req_cl_scaler.pickle +include anystruct\ml_files\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle +include anystruct\ml_files\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle \ No newline at end of file diff --git a/README.md b/README.md index add7b042..3ac29d65 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,62 @@ +![picture](https://github.com/audunarn/ANYstructure/blob/master/anystruct/images/ANYstructure_logo.jpg) + # ANYstructure # ANYstructure is the ultimate steel structure design tool for plate fields and cylinders! Weight optimization for all structures with machine learning capabilities. Calculations are based on DNV standards and rules -### What's new in 5.3.1 ### -* Force to stress to force conversion fix for cylinders. -### What's new in 5.3.0 ### -* Fixed critical caluculation bug in column buckling -* Some minor bug fixing -* Changed some text -### What's new in 5.2.0 ### -* Support for python 3.13 -* Some bug fixing -### What's new in 5.1.4 ### -* Excel input option -* Add load factor 1.1 for Machine Learning buckling (PULS based) -* Continuous update of Utilization Factor when updating pressure loads -### What's new in 5.0.2 ### -* API introduced -* API documenatation at readthedocs (anystructure.readthedocs.io) -* GUI modifications -* Import of python package set to "anystruct" -### What's new in 4.10 ### -* Corrected minor bug on membrane stresses for unstiffened cylinder. -### What's new in 4.9.1 ### -* Corrected bug in loading old save files -* Corrected error on buckling flat plate calculation -### What's new in 4.8 ### -* Reporting table on cylinders. -* Color coding on come cylinder properties. -* Corrected error on additional hoop stress input for cylinders. -### What's new in 4.7 ### -* Corrected error on girder caluculation for cylinder buckling. -* Added 1.10 load factor option for cylinder buckling. -* Better compability with linux. -* Python 3.11 based. -### What's new in 4.4 ### -* Backup and restore feature added. -### What's new in 4.3 ### -* General stability. -* User friendliness. -### What's new in 4.2 ### -* Bug fixing. -* Ukraininan theme. -### What's new in 4.0 ### -* Cylinder design and optimization! -* Flat plate prescriptive buckling improved. Girder calculation added. -* Updated GUI with color themes. -### What's new in 3.3 ### -* Extremely efficient Machine Learning version of PULS called ML-CL. Implemented for all optimizer options. -* Calculation of Center of Gravity and Center of Buoyancy. -* Reporting of weights and COG. -* Lots of bug fixes. +### What's new in 6.0.0 ### +* New buckling method: Semi-analytical buckling for flat plates. +* Updated ML buckling to numerical instead of classification. Improved accurancy of the neural network. +* Updated color coding option. +* Implemented extended testing regime. +* Started work on separating calculation code from the GUI. ------------------------------------------------------------------------ -For documnetation: +For documentation: https://anystructure.readthedocs.io/en/latest/ +## Development setup ## + +ANYstructure is currently maintained as a Python package named `anystruct`. The GUI can still be launched through the `ANYstructure` console command after an editable install. + +Recommended local setup: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -r requirements-dev.txt +python -m pip install -e . +python -m pytest +``` + +Launch the desktop app after installation: + +```powershell +ANYstructure +``` + +Dependency groups are also available for focused installs: + +```powershell +python -m pip install -r requirements-core.txt +python -m pip install -r requirements-ml.txt +python -m pip install -r requirements-excel.txt +``` + +Equivalent package extras are exposed as `core`, `ml`, `excel`, `dev`, and `all`. The default package install still includes Excel and ML dependencies for backwards compatibility. + +Excel project import requires a local Excel installation and is not expected to run in basic automated tests. +The external Excel-sheet DNV PULS calculation workflow has been removed from this release; ML-CL remains available. + ## The following is calculated: ## * Minimum plate thickness (DNV-OS-C101) * Minimum section modulus of stiffener/plate (DNVGL-OS-C101) * Minimum shear area (DNVGL-OS-C101) -* Buckling (DNVGL-RP-C201)or PULS (licenced DNV software) +* Buckling (DNVGL-RP-C201) * Buckling strength of shells DNV-RP-C202 -* PULS buckling (DNV license needed) * Machine learning buckling, PULS based * Fatigue for plate/stiffener connection (DNVGL-RP-C203) @@ -83,4 +75,4 @@ Please like, share or comment on LinkedIn: https://www.linkedin.com/in/audun-arn Screenshot (this example can be loaded from file "ship_section_example.txt"): -![picture](https://docs.google.com/uc?id=1HJeT50bNJTLJbcHTfRke4iySV8zNOAl_) +![picture](https://github.com/audunarn/ANYstructure/blob/master/anystruct/images/ANYstructure%20screenshot.png) diff --git a/__init__.py b/__init__.py index 6d63338c..5bcde4ec 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ # __init__.py # Project information. __title__ = 'ANYstructure' -__version__ = '5.3.3' +__version__ = '6.0.0' __author__ = 'Audun Arnesen Nyhus' __license__ = 'MIT' \ No newline at end of file diff --git a/__main__.py b/__main__.py index cdea46e4..6680ea43 100644 --- a/__main__.py +++ b/__main__.py @@ -1,23 +1,5 @@ -import sys, multiprocessing, ctypes -import tkinter as tk -from anystruct.main_application import Application +from anystruct.__main__ import main -def main(args=None): - """The main routine.""" - if args is None: - args = sys.argv[1:] - - multiprocessing.freeze_support() - errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2) - root = tk.Tk() - width = root.winfo_screenwidth() - height = root.winfo_screenheight() - root.geometry(f'{width}x{height}') - my_app = Application(root) - root.mainloop() - - # Do argument parsing here (eg. with argparse) and anything else - # you want your project to do. if __name__ == "__main__": main() diff --git a/anystruct/ANYstructure_documentation.pdf b/anystruct/ANYstructure_documentation.pdf index f82970db..49d30dde 100644 Binary files a/anystruct/ANYstructure_documentation.pdf and b/anystruct/ANYstructure_documentation.pdf differ diff --git a/anystruct/__main__.py b/anystruct/__main__.py index 7369713d..7c98dd0c 100644 --- a/anystruct/__main__.py +++ b/anystruct/__main__.py @@ -1,24 +1,36 @@ -import sys, multiprocessing, ctypes, os +import ctypes +import multiprocessing +import sys import tkinter as tk + from anystruct.main_application import Application -def main(args=None): - """The main routine.""" +def _set_windows_dpi_awareness(): + if sys.platform != 'win32': + return + + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) + except (AttributeError, OSError): + pass + + +def main(args=None): + """Launch the ANYstructure Tkinter application.""" if args is None: args = sys.argv[1:] multiprocessing.freeze_support() - errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2) + _set_windows_dpi_awareness() + root = tk.Tk() width = root.winfo_screenwidth() height = root.winfo_screenheight() root.geometry(f'{width}x{height}') - my_app = Application(root) + Application(root) root.mainloop() - # Do argument parsing here (eg. with argparse) and anything else - # you want your project to do. if __name__ == "__main__": main() diff --git a/anystruct/api.py b/anystruct/api.py index 4b06de0a..c864d97a 100644 --- a/anystruct/api.py +++ b/anystruct/api.py @@ -14,6 +14,16 @@ import anystruct.stresses_window as stress import anystruct.fatigue_window as fatigue import anystruct.load_factor_window as load_factors + import anystruct.api_helpers as api_helpers + from anystruct.project_application import ( + ProjectFileCodec, + ProjectHydrationDefaults, + ProjectOpenService, + ProjectPersistenceService, + ProjectSaveInput, + ProjectSaveService, + ) + from anystruct.project_state import ProjectState from anystruct.report_generator import LetterMaker import anystruct.sesam_interface as sesam except ModuleNotFoundError: @@ -33,10 +43,42 @@ import ANYstructure.anystruct.stresses_window as stress import ANYstructure.anystruct.fatigue_window as fatigue import ANYstructure.anystruct.load_factor_window as load_factors + import ANYstructure.anystruct.api_helpers as api_helpers + from ANYstructure.anystruct.project_application import ( + ProjectFileCodec, + ProjectHydrationDefaults, + ProjectOpenService, + ProjectPersistenceService, + ProjectSaveInput, + ProjectSaveService, + ) + from ANYstructure.anystruct.project_state import ProjectState from ANYstructure.anystruct.report_generator import LetterMaker import ANYstructure.anystruct.sesam_interface as sesam +def load_project_state(path): + """Load an ANYstructure project file into canonical project state.""" + return ProjectPersistenceService.load_state_from_path(path) + + +def save_project_state(project_state, path): + """Save canonical project state using the supported project-file codec.""" + return ProjectPersistenceService.save_state_to_path(project_state, path) + + +def open_project(path, hydration_defaults=None): + """Load and hydrate an ANYstructure project through the application service facade.""" + if hydration_defaults is None: + hydration_defaults = ProjectHydrationDefaults() + return ProjectOpenService.open_path(path, hydration_defaults) + + +def save_project(path, save_input): + """Create and save project state from a public project save input.""" + return ProjectSaveService.save_path(path, save_input) + + class FlatStru(): ''' API class for all flat plates.\n @@ -52,12 +94,7 @@ def __init__(self, calculation_domain: str = None): :type calculation_domain: str ''' super().__init__() - assert calculation_domain in ["Flat plate, unstiffened", "Flat plate, stiffened", - "Flat plate, stiffened with girder"], ('Calculation domain missing!\n ' - 'Alternatives:' - '\n "Flat plate, unstiffened"' - '\n "Flat plate, stiffened"' - '\n "Flat plate, stiffened with girder"') + api_helpers.assert_choice(calculation_domain, api_helpers.FLAT_STRUCTURE_DOMAINS, 'calculation_domain') self._Plate = CalcScantlings() self._Stiffeners = CalcScantlings() @@ -108,15 +145,15 @@ def set_material(self, mat_yield = 355, emodule = 210000, material_factor = 1.15 :return: :rtype: ''' - self._FlatStructure.mat_yield = mat_yield*1e6 - self._FlatStructure.E = emodule*1e6 + self._FlatStructure.mat_yield = api_helpers.mpa_to_pa(mat_yield) + self._FlatStructure.E = api_helpers.mpa_to_pa(emodule) self._FlatStructure.v = poisson self._FlatStructure.mat_factor = material_factor for sub_cls in [self.Plate, self.Stiffeners, self.Girder]: if sub_cls is not None: - sub_cls.mat_yield = mat_yield * 1e6 - sub_cls.E = emodule * 1e6 + sub_cls.mat_yield = api_helpers.mpa_to_pa(mat_yield) + sub_cls.E = api_helpers.mpa_to_pa(emodule) sub_cls.v = poisson sub_cls.mat_factor = material_factor @@ -181,7 +218,7 @@ def set_plate_geometry(self, spacing: float = 700, thickness: float = 20, span: self._FlatStructure.Plate.t = thickness self._FlatStructure.Plate.spacing = spacing - self._FlatStructure.Plate.span = span/1000 + self._FlatStructure.Plate.span = api_helpers.mm_to_m(span) self._FlatStructure.Plate.girder_lg = 10 # placeholder value @@ -246,7 +283,7 @@ def set_stiffener(self, hw: float = 260, tw: float = 12, bf: float = 49, self._FlatStructure.Stiffener.tw = tw self._FlatStructure.Stiffener.b = bf self._FlatStructure.Stiffener.tf = tf - self._FlatStructure.Stiffener.stiffener_type = 'L-bulb' if stf_type in ['hp HP HP-bulb bulb'] else stf_type + self._FlatStructure.Stiffener.stiffener_type = api_helpers.normalize_bulb_stiffener_type(stf_type) self._FlatStructure.Stiffener.spacing = spacing self._FlatStructure.Stiffener.girder_lg = 10 self._FlatStructure.Stiffener.t = self._FlatStructure.Plate.t @@ -293,9 +330,10 @@ def set_buckling_parameters(self, calculation_method: str= None, buckling_accept Various buckling realted parameters are set here. For details, see\n DNV-RP-C201 Buckling strength of plated structures.\n - :param calculation_method: 'DNV-RP-C201 - prescriptive', 'ML-CL (PULS based)' + :param calculation_method: 'DNV-RP-C201 - prescriptive', 'SemiAnalytical S3/U3', + or 'ML-Numeric (PULS based)' :type calculation_method: str - :param buckling_acceptance: for ML-CL calculations, either 'buckling' or 'ultimate' + :param buckling_acceptance: selected UF family, either 'buckling' or 'ultimate' :type buckling_acceptance: str :param stiffened_plate_effective_aginst_sigy: :type stiffened_plate_effective_aginst_sigy: @@ -318,12 +356,10 @@ def set_buckling_parameters(self, calculation_method: str= None, buckling_accept :return: :rtype: ''' - assert calculation_method in ['DNV-RP-C201 - prescriptive', 'ML-CL (PULS based)'] - assert buckling_acceptance in ['buckling', 'ultimate'] - assert stiffener_support in ['Continuous', 'Sniped'] - assert girder_support in ['Continuous', 'Sniped'] - if calculation_method == 'ML-CL (PULS based)': - raise NotImplementedError('ML-CL (PULS based) not yet implemented') + api_helpers.assert_choice(calculation_method, api_helpers.BUCKLING_CALCULATION_METHODS, 'calculation_method') + api_helpers.assert_choice(buckling_acceptance, api_helpers.BUCKLING_ACCEPTANCE_TYPES, 'buckling_acceptance') + api_helpers.assert_choice(stiffener_support, api_helpers.SUPPORT_TYPES, 'stiffener_support') + api_helpers.assert_choice(girder_support, api_helpers.SUPPORT_TYPES, 'girder_support') sigy_mapper = {True: 'Stf. pl. effective against sigma y', False:'All sigma y to girder'} self._FlatStructure._stiffened_plate_effective_aginst_sigy = sigy_mapper[stiffened_plate_effective_aginst_sigy] self._FlatStructure.method = buckling_acceptance @@ -375,10 +411,7 @@ class CylStru(): 8. 'Orthogonally Stiffened panel'\n ''' - geotypes = ['Unstiffened shell', 'Unstiffened panel', - 'Longitudinal Stiffened shell', 'Longitudinal Stiffened panel', - 'Ring Stiffened shell', 'Ring Stiffened panel', - 'Orthogonally Stiffened shell', 'Orthogonally Stiffened panel'] + geotypes = api_helpers.CYLINDER_STRUCTURE_DOMAINS def __init__(self, calculation_domain: str = 'Unstiffened shell'): ''' :param calculation_domain: calculation domain, 'Unstiffened shell', 'Unstiffened panel', @@ -387,12 +420,12 @@ def __init__(self, calculation_domain: str = 'Unstiffened shell'): :type calculation_domain: str ''' super().__init__() - assert calculation_domain in self.geotypes, 'Geometry type must be either of: ' + str(self.geotypes) - self._load_type = 'Stress' if 'panel' in calculation_domain else 'Force' + api_helpers.assert_choice(calculation_domain, self.geotypes, 'calculation_domain') + self._load_type = api_helpers.cylinder_input_mode(calculation_domain) - self._calculation_domain = calculation_domain + ' (' + self._load_type + ' input)' + self._calculation_domain = api_helpers.cylinder_domain_with_input_mode(calculation_domain) self._CylinderMain = CylinderAndCurvedPlate() - self._CylinderMain.geometry = CylinderAndCurvedPlate.geomeries_map_no_input_spec[calculation_domain] + self._CylinderMain.geometry = api_helpers.geometry_id_for_domain(self._calculation_domain) self._CylinderMain.ShellObj = Shell() if calculation_domain in ['Unstiffened shell', 'Unstiffened panel']: self._CylinderMain.LongStfObj = None @@ -432,12 +465,12 @@ def set_stresses(self, sasd = 0, smsd = 0, tTsd = 0, tQsd = 0, psd = 0, shsd = 0 :rtype: ''' - self._CylinderMain.sasd = sasd*1e6 - self._CylinderMain.smsd = smsd*1e6 - self._CylinderMain.tTsd = abs(tTsd*1e6) - self._CylinderMain.tQsd = abs(tQsd*1e6) - self._CylinderMain.psd = psd*1e6 - self._CylinderMain.shsd = shsd*1e6 + self._CylinderMain.sasd = api_helpers.mpa_to_pa(sasd) + self._CylinderMain.smsd = api_helpers.mpa_to_pa(smsd) + self._CylinderMain.tTsd = abs(api_helpers.mpa_to_pa(tTsd)) + self._CylinderMain.tQsd = abs(api_helpers.mpa_to_pa(tQsd)) + self._CylinderMain.psd = api_helpers.mpa_to_pa(psd) + self._CylinderMain.shsd = api_helpers.mpa_to_pa(shsd) def set_forces(self, Nsd: float = 0, Msd: float = 0, Tsd: float = 0, Qsd: float = 0, psd: float = 0): ''' @@ -452,16 +485,7 @@ def set_forces(self, Nsd: float = 0, Msd: float = 0, Tsd: float = 0, Qsd: float :return: ''' - geomeries = {11: 'Flat plate, stiffened', 10: 'Flat plate, unstiffened', - 12: 'Flat plate, stiffened with girder', - 1: 'Unstiffened shell (Force input)', 2: 'Unstiffened panel (Stress input)', - 3: 'Longitudinal Stiffened shell (Force input)', 4: 'Longitudinal Stiffened panel (Stress input)', - 5: 'Ring Stiffened shell (Force input)', 6: 'Ring Stiffened panel (Stress input)', - 7: 'Orthogonally Stiffened shell (Force input)', 8: 'Orthogonally Stiffened panel (Stress input)'} - geomeries_map = dict() - for key, value in geomeries.items(): - geomeries_map[value] = key - geometry = geomeries_map[self._calculation_domain] + geometry = api_helpers.geometry_id_for_domain(self._calculation_domain) forces = [Nsd, Msd, Tsd, Qsd] sasd, smsd, tTsd, tQsd, shsd = hlp.helper_cylinder_stress_to_force_to_stress( stresses=None, forces=forces, geometry=geometry, shell_t=self._CylinderMain.ShellObj.thk, @@ -496,8 +520,8 @@ def set_material(self, mat_yield = 355, emodule = 210000, material_factor = 1.15 :return: :rtype: ''' - self._CylinderMain.mat_yield = mat_yield*1e6 - self._CylinderMain.E = emodule*1e6 + self._CylinderMain.mat_yield = api_helpers.mpa_to_pa(mat_yield) + self._CylinderMain.E = api_helpers.mpa_to_pa(emodule) self._CylinderMain.v = poisson self._CylinderMain.mat_factor = material_factor def set_imperfection(self, delta_0 = 0.005): @@ -523,8 +547,8 @@ def set_fabrication_method(self, stiffener: str = 'Fabricated', girder: str = ' :return: :rtype: ''' - options = ['Fabricated', 'Cold formed'] - assert stiffener in options, 'Method must be either of: ' + str(options) + api_helpers.assert_choice(stiffener, api_helpers.FABRICATION_METHODS, 'stiffener fabrication method') + api_helpers.assert_choice(girder, api_helpers.FABRICATION_METHODS, 'girder fabrication method') self._CylinderMain.fab_method_ring_stf = stiffener self._CylinderMain.fab_method_ring_girder = girder def set_end_cap_pressure_included_in_stress(self, is_included: bool = True): @@ -549,7 +573,7 @@ def set_uls_or_als(self, kind = 'ULS'): :return: :rtype: ''' - assert kind in ['ULS', 'ALS'], 'Must be either of: ' + str(kind) + api_helpers.assert_choice(kind, api_helpers.LIMIT_STATE_TYPES, 'limit state') self._CylinderMain.uls_or_als = kind def set_exclude_ring_stiffener(self, is_excluded: bool = True): ''' @@ -593,7 +617,7 @@ def set_panel_spacing(self, val: float = 0): :return: :rtype: ''' - self._CylinderMain.panel_spacing = val/1000 + self._CylinderMain.panel_spacing = api_helpers.mm_to_m(val) def set_shell_geometry(self, radius: float = 0, thickness: float = 0,distance_between_rings: float = 0, tot_length_of_shell: float = 0): @@ -612,15 +636,15 @@ def set_shell_geometry(self, radius: float = 0, thickness: float = 0,distance_be :rtype: ''' - self._CylinderMain.ShellObj.radius = radius/1000 - self._CylinderMain.ShellObj.thk = thickness/1000 - self._CylinderMain.ShellObj.dist_between_rings = distance_between_rings/1000 + self._CylinderMain.ShellObj.radius = api_helpers.mm_to_m(radius) + self._CylinderMain.ShellObj.thk = api_helpers.mm_to_m(thickness) + self._CylinderMain.ShellObj.dist_between_rings = api_helpers.mm_to_m(distance_between_rings) if tot_length_of_shell == 0: # Setting a default. - self._CylinderMain.ShellObj.length_of_shell = distance_between_rings * 10/1000 - self._CylinderMain.ShellObj.tot_cyl_length = distance_between_rings * 10/1000 + self._CylinderMain.ShellObj.length_of_shell = api_helpers.mm_to_m(distance_between_rings * 10) + self._CylinderMain.ShellObj.tot_cyl_length = api_helpers.mm_to_m(distance_between_rings * 10) else: - self._CylinderMain.ShellObj.tot_cyl_length = tot_length_of_shell/1000 + self._CylinderMain.ShellObj.tot_cyl_length = api_helpers.mm_to_m(tot_length_of_shell) def set_shell_buckling_parmeters(self, eff_buckling_length_factor: float = 1.0): ''' @@ -656,7 +680,7 @@ def set_longitudinal_stiffener(self, hw: float = 260, tw: float = 12, bf: float self._CylinderMain.LongStfObj.tw = tw self._CylinderMain.LongStfObj.b = bf self._CylinderMain.LongStfObj.tf = tf - self._CylinderMain.LongStfObj.stiffener_type = 'L-bulb' if stf_type in ['hp HP HP-bulb bulb'] else stf_type + self._CylinderMain.LongStfObj.stiffener_type = api_helpers.normalize_bulb_stiffener_type(stf_type) self._CylinderMain.LongStfObj.spacing = spacing self._CylinderMain.LongStfObj.t = self._CylinderMain.ShellObj.thk @@ -686,7 +710,7 @@ def set_ring_stiffener(self, hw: float = 260, tw: float = 12, bf: float = 49, self._CylinderMain.RingStfObj.tw = tw self._CylinderMain.RingStfObj.b = bf self._CylinderMain.RingStfObj.tf = tf - self._CylinderMain.RingStfObj.stiffener_type = 'L-bulb' if stf_type in ['hp HP HP-bulb bulb'] else stf_type + self._CylinderMain.RingStfObj.stiffener_type = api_helpers.normalize_bulb_stiffener_type(stf_type) self._CylinderMain.RingStfObj.s = spacing self._CylinderMain.RingStfObj.t = self._CylinderMain.ShellObj.thk @@ -715,7 +739,7 @@ def set_ring_girder(self, hw: float = 500, tw: float = 15, bf: float = 200, self._CylinderMain.RingFrameObj.tw = tw self._CylinderMain.RingFrameObj.b = bf self._CylinderMain.RingFrameObj.tf = tf - self._CylinderMain.RingFrameObj.stiffener_type = 'L-bulb' if stf_type in ['hp HP HP-bulb bulb'] else stf_type + self._CylinderMain.RingFrameObj.stiffener_type = api_helpers.normalize_bulb_stiffener_type(stf_type) self._CylinderMain.RingFrameObj.s = spacing self._CylinderMain.RingFrameObj.t = self._CylinderMain.ShellObj.thk @@ -767,6 +791,3 @@ def get_buckling_results(self): - - - diff --git a/anystruct/api_helpers.py b/anystruct/api_helpers.py new file mode 100644 index 00000000..0d17e837 --- /dev/null +++ b/anystruct/api_helpers.py @@ -0,0 +1,120 @@ +FLAT_STRUCTURE_DOMAINS = ( + "Flat plate, unstiffened", + "Flat plate, stiffened", + "Flat plate, stiffened with girder", +) + +CYLINDER_STRUCTURE_DOMAINS = ( + "Unstiffened shell", + "Unstiffened panel", + "Longitudinal Stiffened shell", + "Longitudinal Stiffened panel", + "Ring Stiffened shell", + "Ring Stiffened panel", + "Orthogonally Stiffened shell", + "Orthogonally Stiffened panel", +) + +BUCKLING_CALCULATION_METHODS = ( + "DNV-RP-C201 - prescriptive", + "SemiAnalytical S3/U3", + "ML-Numeric (PULS based)", +) + +BUCKLING_ACCEPTANCE_TYPES = ( + "buckling", + "ultimate", +) + +SUPPORT_TYPES = ( + "Continuous", + "Sniped", +) + +FABRICATION_METHODS = ( + "Fabricated", + "Cold formed", +) + +LIMIT_STATE_TYPES = ( + "ULS", + "ALS", +) + +FLAT_GEOMETRY_IDS = { + "Flat plate, unstiffened": 10, + "Flat plate, stiffened": 11, + "Flat plate, stiffened with girder": 12, +} + +CYLINDER_GEOMETRY_IDS = { + "Unstiffened shell (Force input)": 1, + "Unstiffened panel (Stress input)": 2, + "Longitudinal Stiffened shell (Force input)": 3, + "Longitudinal Stiffened panel (Stress input)": 4, + "Ring Stiffened shell (Force input)": 5, + "Ring Stiffened panel (Stress input)": 6, + "Orthogonally Stiffened shell (Force input)": 7, + "Orthogonally Stiffened panel (Stress input)": 8, +} + +CYLINDER_STRUCTURE_DOMAINS_WITH_INPUT = tuple(CYLINDER_GEOMETRY_IDS.keys()) + +GEOMETRY_IDS = { + **FLAT_GEOMETRY_IDS, + **CYLINDER_GEOMETRY_IDS, +} + +GEOMETRY_DOMAINS = {value: key for key, value in GEOMETRY_IDS.items()} + + +def normalize_domain_string(calculation_domain): + return " ".join(str(calculation_domain).split()) + + +def assert_choice(value, choices, label): + assert value in choices, f"{label} must be one of: {list(choices)}" + + +def cylinder_input_mode(calculation_domain): + calculation_domain = normalize_domain_string(calculation_domain) + assert_choice(calculation_domain, CYLINDER_STRUCTURE_DOMAINS, "calculation_domain") + return "Stress" if "panel" in calculation_domain.lower() else "Force" + + +def cylinder_domain_with_input_mode(calculation_domain): + calculation_domain = normalize_domain_string(calculation_domain) + input_mode = cylinder_input_mode(calculation_domain) + return f"{calculation_domain} ({input_mode} input)" + + +def geometry_id_for_domain(calculation_domain): + calculation_domain = normalize_domain_string(calculation_domain) + + if calculation_domain in GEOMETRY_IDS: + return GEOMETRY_IDS[calculation_domain] + + if calculation_domain in CYLINDER_STRUCTURE_DOMAINS: + calculation_domain = cylinder_domain_with_input_mode(calculation_domain) + return GEOMETRY_IDS[calculation_domain] + + raise KeyError( + f"Unknown calculation domain: {calculation_domain!r}. " + f"Expected one of: {list(GEOMETRY_IDS)} or {list(CYLINDER_STRUCTURE_DOMAINS)}" + ) + + +def domain_for_geometry_id(geometry_id): + return GEOMETRY_DOMAINS[geometry_id] + + +def mpa_to_pa(value): + return value * 1e6 + + +def mm_to_m(value): + return value / 1000 + + +def normalize_bulb_stiffener_type(stiffener_type): + return "L-bulb" if stiffener_type in ["hp", "HP", "HP-bulb", "bulb"] else stiffener_type diff --git a/anystruct/calc_structure.py b/anystruct/calc_structure.py index 6d5ed02f..1b0b3fc9 100644 --- a/anystruct/calc_structure.py +++ b/anystruct/calc_structure.py @@ -2619,16 +2619,16 @@ def local_buckling(self, optimizing = False): def get_one_line_string_mixed(self): ''' Returning a one line string. ''' - return 'pl_'+str(round(self.Plate.s, 1))+'x'+str(round(self.Plate.t,1))+' stf_'+\ + return 'pl_'+str(round(self.Plate.get_s(), 1))+'x'+str(round(self.Plate.get_pl_thk(),1))+' stf_'+\ self.Stiffener.get_stiffener_type()+\ - str(round(self.Stiffener.hw,1))+'x'+str(round(self.Stiffener.tw,1))+'+'\ - +str(round(self.Stiffener.b,1))+'x'+\ - str(round(self.Stiffener.tf,1)) + str(round(self.Stiffener.get_web_h(),1))+'x'+str(round(self.Stiffener.get_web_thk(),1))+'+'\ + +str(round(self.Stiffener.get_fl_w(),1))+'x'+\ + str(round(self.Stiffener.get_fl_thk(),1)) def get_extended_string_mixed(self): ''' Some more information returned. ''' - return 'span: '+str(round(self.Plate.span,4))+' structure type: '+ self.Stiffener.get_structure_type() + ' stf. type: ' + \ - self.Stiffener.get_stiffener_type() + ' pressure side: ' + self.Plate.overpressure_side + return 'span: '+str(round(self.Plate.get_span(),4))+' structure type: '+ self.Stiffener.get_structure_type() + ' stf. type: ' + \ + self.Stiffener.get_stiffener_type() + ' pressure side: ' + self.overpressure_side class Shell(): ''' diff --git a/anystruct/calculate_semianalytical.py b/anystruct/calculate_semianalytical.py new file mode 100644 index 00000000..34486f01 --- /dev/null +++ b/anystruct/calculate_semianalytical.py @@ -0,0 +1,4200 @@ +"""Reduced semi-analytical PULS S3/U3 panel calculations. + +This module is a first physics milestone for regular stiffened S3 panels and +unstiffened U3 panels. It keeps the PULS CSV files as benchmark data and does +not fit corrections from them. The implementation follows the direct +semi-analytical shape described in DNV-CG-0128 Sec.4: + +* panel deflections are represented with Rayleigh-Ritz sine modes, +* a nonlinear equilibrium residual is traced along a proportional in-plane + load path while lateral pressure remains fixed, +* elastic mode factors and a major-yield collapse check are reported as + usage factors. + +The production PULS code uses a richer element library and element validity +manual than the public guideline. The result diagnostics therefore name the +covered assumptions instead of presenting this reduced model as PULS parity. +""" + +from __future__ import annotations + +import argparse +import csv +import json +import math +import random +import statistics +from dataclasses import asdict, dataclass, field, replace +from pathlib import Path +from typing import Any, Iterable, Iterator, Mapping, Sequence + +import numpy as np + + +EPS = 1.0e-12 +SUPPORTED_IN_PLANE_SUPPORTS = { + "Integrated": "method-a", + "Girder - long": "method-b-long", + "Girder - trans": "method-b-trans", +} +SUPPORTED_STIFFENER_TYPES = {"T-bar", "L-bulb", "Angle", "Flatbar"} +SUPPORTED_STIFFENER_BOUNDARIES = {"Cont", "Sniped"} +SUPPORTED_ROTATIONAL_SUPPORTS = {"SS", "CL", "FS", ""} +CSV_SAMPLE_METHOD_FIRST_N = "first-n" +CSV_SAMPLE_METHOD_STRATIFIED = "stratified-shuffle" +CSV_SAMPLE_METHOD_FULL = "full" +DEFAULT_CSV_SAMPLE_SIZE = 1200 +DEFAULT_CSV_FIXTURE_SAMPLE_SIZE = 64 +DEFAULT_CSV_SAMPLE_SEED = 128 +DEFAULT_CSV_MODE_CONVERGENCE_SAMPLE_SIZE = 200 +CSV_STRATIFICATION_KEYS = ( + "target_validity", + "stiffener_type", + "support", + "stiffener_boundary", + "pressure_state", + "pressure_band", + "usage_region", +) +DEFAULT_SHIP_SECTION_INPUTS = ( + r"C:\Github\ANYstructure\anystruct\ship_section_example.txt", + r"C:\Users\AudunArnesenNyhus\OneDrive - Cefront\Documents\OKEA side section.txt", + r"C:\Users\AudunArnesenNyhus\OneDrive - Cefront\Documents\OKEA mid section.txt", + r"C:\Users\AudunArnesenNyhus\OneDrive - Cefront\Documents\OKEA transversal section.txt", +) +DEFAULT_RELIABILITY_BASELINE = Path("reports/puls_reliability_baseline.json") +PULS_MANUAL_S3_LIMITS = { + "source": r"C:\Program Files\DNV\NauticusHull 20.36.2508\Manuals\UserManual PULS.pdf", + "manual_file_date": "2025-08-05", + "plate_slenderness_max": 200.0, + "aspect_ratio_min": 0.17, + "aspect_ratio_max": 20.0, + "flatbar_web_slenderness_max": 35.0, + "open_profile_web_slenderness_max": 90.0, + "free_flange_slenderness_max": 15.0, + "min_flange_width_to_web_height": 0.22, +} +PULS_MANUAL_U3_LIMITS = { + "source": PULS_MANUAL_S3_LIMITS["source"], + "manual_file_date": PULS_MANUAL_S3_LIMITS["manual_file_date"], + "plate_slenderness_max": 200.0, + "long_to_short_aspect_ratio_max": 20.0, +} + + +@dataclass(frozen=True) +class S3PanelInput: + """Input surface for the regular stiffened S3 panel milestone. + + Geometric dimensions are millimetres and stresses/pressure are MPa. CSV + PULS exports use positive in-plane normal stress for compression; the + helper functions in this module preserve that sign convention. + """ + + length: float + stiffener_spacing: float + plate_thickness: float + stiffener_type: str + stiffener_boundary: str + stiffener_height: float + web_thickness: float + flange_width: float + flange_thickness: float + yield_stress_plate: float + yield_stress_stiffener: float + axial_stress: float + transverse_stress_1: float + transverse_stress_2: float + shear_stress: float + pressure: float + in_plane_support: str + elastic_modulus: float = 210000.0 + poisson_ratio: float = 0.3 + + @property + def width(self) -> float: + return self.stiffener_spacing + + @property + def mean_transverse_stress(self) -> float: + return 0.5 * (self.transverse_stress_1 + self.transverse_stress_2) + + +@dataclass(frozen=True) +class U3PanelInput: + """Input surface for the regular unstiffened U3 plate milestone. + + Geometric dimensions are millimetres and stresses/pressure are MPa. The + ANYstructure/PULS exports use two end values for longitudinal and + transverse stress; the current U3 Ritz path uses their linear plate-field + interpolation in yield checks and their mean values in elastic buckling. + """ + + length: float + width: float + plate_thickness: float + yield_stress_plate: float + axial_stress_1: float + axial_stress_2: float + transverse_stress_1: float + transverse_stress_2: float + shear_stress: float + pressure: float + in_plane_support: str + rotational_support_1: str = "SS" + rotational_support_2: str = "SS" + elastic_modulus: float = 210000.0 + poisson_ratio: float = 0.3 + + @property + def axial_stress(self) -> float: + return 0.5 * (self.axial_stress_1 + self.axial_stress_2) + + @property + def mean_transverse_stress(self) -> float: + return 0.5 * (self.transverse_stress_1 + self.transverse_stress_2) + + +@dataclass(frozen=True) +class S3SolverConfig: + """Numerical and covered-domain controls for the reduced S3 solver.""" + + longitudinal_modes: tuple[int, ...] = (1, 2, 3) + transverse_modes: tuple[int, ...] = (1, 2) + web_longitudinal_modes: tuple[int, ...] = (1, 2, 3) + web_depth_modes: tuple[int, ...] = (1, 2) + initial_imperfection_ratio: float = 0.001 + initial_imperfection_floor_mm: float = 0.02 + nonlinear_membrane_factor: float = 0.75 + local_global_coupling_floor: float = 0.25 + local_global_coupling_gain: float = 1.0 + global_stiffened_strip_capacity_factor: float | None = None + web_shear_interaction_exponent: float = 1.0 + local_plate_web_interaction_exponent: float = 2.0 + flanged_local_plate_restraint_factor: float = 1.10 + s3_shear_buckling_capacity_factor: float = 0.75 + use_effective_stiffener_width: bool = False + sniped_eccentricity_factor: float = 1.2 + web_local_sniped_eccentricity_factor: float = 1.0 + torsional_imperfection_scale: float = 1.0 + torsional_restraint_factor: float = 0.65 + pressure_local_share: float = 0.0 + pressure_global_share: float = 1.0 + use_separate_s3_pressure_modes: bool = True + s3_pressure_mode_stiffness_factor: float = 5.0 + include_pressure_dominated_yield_in_buckling_strength: bool = False + include_lateral_deformation_in_ultimate_yield: bool = False + include_global_curvature_in_plate_yield: bool = False + pressure_dominated_yield_preload_ratio: float = 0.05 + s3_major_yield_reserve_factor: float = 1.10 + yield_utilization_limit: float = 1.0 + pressure_yield_limit: float = 1.0 + max_load_factor: float = 100.0 + initial_load_step: float = 0.05 + load_step_growth: float = 1.08 + max_load_step: float = 1.5 + min_load_step: float = 1.0e-4 + load_step_cutback: float = 0.5 + max_load_step_cutbacks: int = 12 + newton_max_iterations: int = 40 + newton_tolerance: float = 1.0e-7 + min_aspect_ratio: float = 0.15 + max_aspect_ratio: float = 12.0 + max_plate_slenderness: float = 250.0 + max_web_slenderness: float = 180.0 + max_flange_slenderness: float = 45.0 + max_web_to_flange_ratio: float = 5.0 + hot_spot_grid: tuple[float, ...] = (0.125, 0.25, 0.5, 0.75, 0.875) + check_mode_convergence: bool = False + medium_longitudinal_modes: tuple[int, ...] = (1, 2, 3, 4) + medium_transverse_modes: tuple[int, ...] = (1, 2, 3) + high_longitudinal_modes: tuple[int, ...] = (1, 2, 3, 4, 5) + high_transverse_modes: tuple[int, ...] = (1, 2, 3, 4) + high_confidence_drift_limit: float = 0.05 + medium_confidence_drift_limit: float = 0.12 + + +@dataclass(frozen=True) +class S3SectionProperties: + area: float + centroid_from_plate_midplane: float + inertia_x: float + top_distance: float + bottom_distance: float + plate_area: float + stiffener_area: float + + @property + def section_modulus(self) -> float: + return self.inertia_x / max(self.top_distance, self.bottom_distance, EPS) + + @property + def top_section_modulus(self) -> float: + return self.inertia_x / max(self.top_distance, EPS) + + @property + def attached_plate_section_modulus(self) -> float: + return self.inertia_x / max(self.bottom_distance, EPS) + + +@dataclass(frozen=True) +class OrthotropicStiffness: + d11: float + d12: float + d22: float + d66: float + membrane_thickness: float + + +@dataclass(frozen=True) +class RitzMode: + family: str + m: int + n: int + kx: float + ky: float + linear_stiffness: float + geometric_stiffness: float + pressure_force: float + imperfection: float + nonlinear_stiffness: float + + @property + def label(self) -> str: + return f"{self.family}:{self.m},{self.n}" + + +@dataclass +class S3Result: + buckling_usage_factor: float | None + ultimate_usage_factor: float | None + valid: bool + elastic_buckling_usage_factor: float | None = None + invalid_reason: str | None = None + diagnostics: dict[str, Any] = field(default_factory=dict) + covered_domain_notes: list[str] = field(default_factory=list) + confidence: str = "low" + confidence_reasons: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "buckling_usage_factor": self.buckling_usage_factor, + "ultimate_usage_factor": self.ultimate_usage_factor, + "elastic_buckling_usage_factor": self.elastic_buckling_usage_factor, + "valid": self.valid, + "invalid_reason": self.invalid_reason, + "diagnostics": self.diagnostics, + "covered_domain_notes": list(self.covered_domain_notes), + "confidence": self.confidence, + "confidence_reasons": list(self.confidence_reasons), + } + + +@dataclass(frozen=True) +class _Rectangle: + area: float + centroid: float + height: float + + @property + def local_inertia_x(self) -> float: + return self.area * self.height * self.height / 12.0 + + +def _float_from_row(row: Mapping[str, Any], key: str) -> float: + value = row.get(key, "") + if value is None: + raise ValueError(f"Missing numeric value for {key}") + text = str(value).strip() + if text == "": + raise ValueError(f"Missing numeric value for {key}") + return float(text) + + +def _optional_float(value: Any) -> float | None: + if value is None: + return None + text = str(value).strip() + if not text: + return None + try: + parsed = float(text) + except ValueError: + return None + if math.isnan(parsed): + return None + return parsed + + +def row_to_s3_input(row: Mapping[str, Any]) -> S3PanelInput: + """Map a PULS stiffened-panel CSV row into the solver input type.""" + + elastic_modulus = _optional_float(row.get("Modulus of elasticity")) + poisson_ratio = _optional_float(row.get("Poisson's ratio")) + if poisson_ratio is None: + poisson_ratio = _optional_float(row.get("Poisson ratio")) + return S3PanelInput( + length=_float_from_row(row, "Length of panel"), + stiffener_spacing=_float_from_row(row, "Stiffener spacing"), + plate_thickness=_float_from_row(row, "Plate thick."), + stiffener_type=str(row.get("Stiffener type", "")).strip(), + stiffener_boundary=str(row.get("Stiffener boundary", "")).strip(), + stiffener_height=_float_from_row(row, "Stiff. Height"), + web_thickness=_float_from_row(row, "Web thick."), + flange_width=_float_from_row(row, "Flange width"), + flange_thickness=_float_from_row(row, "Flange thick."), + yield_stress_plate=_float_from_row(row, "Yield stress plate"), + yield_stress_stiffener=_float_from_row(row, "Yield stress stiffener"), + axial_stress=_float_from_row(row, "Axial stress"), + transverse_stress_1=_float_from_row(row, "Trans. stress 1"), + transverse_stress_2=_float_from_row(row, "Trans. stress 2"), + shear_stress=_float_from_row(row, "Shear stress"), + pressure=_float_from_row(row, "Pressure (fixed)"), + in_plane_support=str(row.get("In-plane support", "")).strip(), + elastic_modulus=( + elastic_modulus + if elastic_modulus is not None + else S3PanelInput.__dataclass_fields__["elastic_modulus"].default + ), + poisson_ratio=( + poisson_ratio + if poisson_ratio is not None + else S3PanelInput.__dataclass_fields__["poisson_ratio"].default + ), + ) + + +def row_to_u3_input(row: Mapping[str, Any]) -> U3PanelInput: + """Map a PULS unstiffened-panel row/export into the solver input type.""" + + elastic_modulus = _optional_float(row.get("Modulus of elasticity")) + poisson_ratio = _optional_float(row.get("Poisson's ratio")) + if poisson_ratio is None: + poisson_ratio = _optional_float(row.get("Poisson ratio")) + axial_1 = _float_from_row(row, "Axial stress") + axial_2 = _optional_float(row.get("Axial stress 2")) + transverse_1 = _optional_float(row.get("Trans. stress 1")) + if transverse_1 is None: + transverse_1 = _optional_float(row.get("Trans. Stress")) + if transverse_1 is None: + transverse_1 = _optional_float(row.get("Trans. stress")) + if transverse_1 is None: + raise ValueError("Missing numeric value for Trans. stress 1") + transverse_2 = _optional_float(row.get("Trans. stress 2")) + if transverse_2 is None: + transverse_2 = _optional_float(row.get("Trans. Stress 2")) + if transverse_2 is None: + transverse_2 = transverse_1 + return U3PanelInput( + length=_float_from_row(row, "Plate length"), + width=_float_from_row(row, "Plate width"), + plate_thickness=_float_from_row(row, "Plate thick."), + yield_stress_plate=_float_from_row(row, "Yield stress plate"), + axial_stress_1=axial_1, + axial_stress_2=axial_1 if axial_2 is None else axial_2, + transverse_stress_1=transverse_1, + transverse_stress_2=transverse_2, + shear_stress=_float_from_row(row, "Shear stress"), + pressure=_float_from_row(row, "Pressure (fixed)"), + in_plane_support=str(row.get("In-plane support", "")).strip(), + rotational_support_1=str(row.get("Rotational support", "SS") or "SS").strip(), + rotational_support_2=str(row.get("Rotational support 2", "SS") or "SS").strip(), + elastic_modulus=( + elastic_modulus + if elastic_modulus is not None + else U3PanelInput.__dataclass_fields__["elastic_modulus"].default + ), + poisson_ratio=( + poisson_ratio + if poisson_ratio is not None + else U3PanelInput.__dataclass_fields__["poisson_ratio"].default + ), + ) + + +_MISSING = object() + + +def _ship_section_value(section: Mapping[str, Any], key: str, default: Any = _MISSING) -> Any: + if key in section: + value = section[key] + elif default is not _MISSING: + value = default + else: + value = section[key] + if isinstance(value, (list, tuple)): + return value[0] + return value + + +def _ship_section_optional_value( + section: Mapping[str, Any], + *keys: str, + default: Any = "", +) -> Any: + for key in keys: + if key in section: + return _ship_section_value(section, key) + return default + + +def ship_section_record_to_csv_row(record: Mapping[str, Any]) -> dict[str, Any]: + """Flatten one ANYstructure ship-section PULS result record to CSV-like S3 fields.""" + + plate = record["Plate geometry"] + stiffener = record["Primary stiffeners"] + material = record["Material"] + loads = record["Applied loads"] + support = record.get("Bound cond.", {}) + buckling = record.get("Buckling strength", {}) + ultimate = record.get("Ultimate capacity", {}) + global_elastic = record.get("Global elastic buckling", {}) + local_elastic = record.get("Local elastic buckling", {}) + failure_modes = record.get("Failure modes", {}) + transverse_stress = _ship_section_value(loads, "Trans. stress") + + return { + "_ship_line": record.get("Identification", ""), + "Length of panel": _ship_section_value(plate, "Length of panel"), + "Stiffener spacing": _ship_section_value(plate, "Stiffener spacing"), + "Plate thick.": _ship_section_value(plate, "Plate thick."), + "Stiffener type": _ship_section_value(stiffener, "Stiffener type"), + "Stiffener boundary": _ship_section_value(stiffener, "Stiffener boundary"), + "Stiff. Height": _ship_section_value(stiffener, "Stiff. Height"), + "Web thick.": _ship_section_value(stiffener, "Web thick."), + "Flange width": _ship_section_value(stiffener, "Flange width"), + "Flange thick.": _ship_section_value(stiffener, "Flange thick."), + "Yield stress plate": _ship_section_value(material, "Yield stress plate"), + "Yield stress stiffener": _ship_section_value(material, "Yield stress stiffener"), + "Modulus of elasticity": _ship_section_value(material, "Modulus of elasticity"), + "Poisson's ratio": _ship_section_value(material, "Poisson's ratio"), + "Axial stress": _ship_section_value(loads, "Axial stress"), + "Trans. stress 1": transverse_stress, + "Trans. stress 2": _ship_section_value(loads, "Trans. stress 2", transverse_stress), + "Shear stress": _ship_section_value(loads, "Shear stress"), + "Pressure (fixed)": _ship_section_value(loads, "Pressure (fixed)"), + "In-plane support": _ship_section_value(support, "In-plane support") if support else "", + "Buckling Actual usage Factor inc NaN": ( + _ship_section_value(buckling, "Actual usage Factor") if buckling else "" + ), + "Ultimate Actual usage Factor inc NaN": ( + _ship_section_value(ultimate, "Actual usage Factor") if ultimate else "" + ), + "output cl str buc": _ship_section_value(buckling, "Status") if buckling else "", + "PULS global axial stress": _ship_section_optional_value( + global_elastic, + "Axial stress", + ), + "PULS global trans stress": _ship_section_optional_value( + global_elastic, + "Trans. Stress", + "Trans. stress", + ), + "PULS global shear stress": _ship_section_optional_value( + global_elastic, + "Shear stress", + ), + "PULS local axial stress": _ship_section_optional_value( + local_elastic, + "Axial stress", + ), + "PULS local trans stress": _ship_section_optional_value( + local_elastic, + "Trans. Stress", + "Trans. stress", + ), + "PULS local shear stress": _ship_section_optional_value( + local_elastic, + "Shear stress", + ), + "PULS failure plate buckling percent": _ship_section_optional_value( + failure_modes, + "Plate buckling", + ), + "PULS failure global stiffener buckling percent": _ship_section_optional_value( + failure_modes, + "Global stiffener buckling", + ), + "PULS failure torsional stiffener buckling percent": _ship_section_optional_value( + failure_modes, + "Torsional stiffener buckling", + ), + "PULS failure web stiffener buckling percent": _ship_section_optional_value( + failure_modes, + "Web stiffener buckling", + ), + } + + +def ship_section_record_to_u3_row(record: Mapping[str, Any]) -> dict[str, Any]: + """Flatten one ANYstructure ship-section PULS result record to CSV-like U3 fields.""" + + plate = record["Geometry"] + material = record["Material"] + loads = record["Applied loads"] + support = record.get("Boundary conditions", record.get("Bound cond.", {})) + buckling = record.get("Buckling strength", {}) + ultimate = record.get("Ultimate capacity", {}) + transverse_stress = _ship_section_value(loads, "Trans. Stress", _ship_section_value(loads, "Trans. stress", 0.0)) + + return { + "_ship_line": record.get("Identification", ""), + "Panel family": "U3", + "Plate length": _ship_section_value(plate, "Plate length"), + "Plate width": _ship_section_value(plate, "Plate width"), + "Plate thick.": _ship_section_value(plate, "Plate thick."), + "Yield stress plate": _ship_section_value(material, "Yield st. plate", _ship_section_value(material, "Yield stress plate", "")), + "Modulus of elasticity": _ship_section_value(material, "Modulus of elasticity"), + "Poisson's ratio": _ship_section_value(material, "Poisson's ratio"), + "Axial stress": _ship_section_value(loads, "Axial stress"), + "Axial stress 2": _ship_section_value(loads, "Axial stress 2", _ship_section_value(loads, "Axial stress")), + "Trans. stress 1": transverse_stress, + "Trans. stress 2": _ship_section_value(loads, "Trans. Stress 2", transverse_stress), + "Shear stress": _ship_section_value(loads, "Shear stress"), + "Pressure (fixed)": _ship_section_value(loads, "Pressure (fixed)"), + "In-plane support": _ship_section_value(support, "In-plane support") if support else "", + "Rotational support": _ship_section_value(support, "Rotational support", "SS") if support else "SS", + "Rotational support 2": _ship_section_value(support, "Rotational support 2", "SS") if support else "SS", + "Buckling Actual usage Factor inc NaN": ( + _ship_section_value(buckling, "Actual usage Factor") if buckling else "" + ), + "Ultimate Actual usage Factor inc NaN": ( + _ship_section_value(ultimate, "Actual usage Factor") if ultimate else "" + ), + "output cl str buc": _ship_section_value(buckling, "Status") if buckling else "", + } + + +def _anystructure_float(value: Any, default: float = 0.0) -> float: + try: + if value is None: + return default + return float(value) + except Exception: + return default + + +def _anystructure_stiffener_type(value: Any) -> str: + return { + "T": "T-bar", + "T-bar": "T-bar", + "L": "Angle", + "Angle": "Angle", + "L-bulb": "L-bulb", + "FB": "Flatbar", + "F": "Flatbar", + "Flatbar": "Flatbar", + }.get(str(value), str(value)) + + +def _anystructure_stiffener_boundary(value: Any) -> str: + return { + "C": "Cont", + "Cont": "Cont", + "Continuous": "Cont", + "S": "Sniped", + "Sniped": "Sniped", + }.get(str(value), str(value)) + + +def _anystructure_in_plane_support(value: Any) -> str: + return { + "Int": "Integrated", + "Integrated": "Integrated", + "GL": "Girder - long", + "Girder - long": "Girder - long", + "GT": "Girder - trans", + "Girder - trans": "Girder - trans", + }.get(str(value), str(value)) + + +def _anystructure_rotational_supports(value: Any) -> tuple[str, str]: + text = str(value or "SSSS").strip().upper().replace("-", "") + if text in {"CCCC", "CLCL", "CC", "CL"}: + return "CL", "CL" + if text in {"FSFS", "FFFF", "FS"}: + return "FS", "FS" + return "SS", "SS" + + +def _anystructure_selected_method(value: Any) -> str: + text = str(value).strip().lower() + if text in {"1", "buckling"}: + return "buckling" + if text in {"2", "ultimate"}: + return "ultimate" + return text + + +def _anystructure_material_factor(all_structure: Any) -> float | None: + for candidate in ( + getattr(all_structure, "Plate", None), + getattr(all_structure, "Stiffener", None), + all_structure, + ): + if candidate is None: + continue + for name in ("mat_factor", "_mat_factor"): + try: + value = getattr(candidate, name) + except Exception: + continue + parsed = _optional_float(value) + if parsed is not None and parsed > EPS: + return parsed + return None + + +def _anystructure_axial_design_stress(sigma_x1: float, sigma_x2: float) -> float: + if sigma_x1 * sigma_x2 >= 0: + return sigma_x1 if abs(sigma_x1) > abs(sigma_x2) else sigma_x2 + return max(sigma_x1, sigma_x2) + + +def anystructure_panel_input(calc_object: Any, lat_press: float = 0.0) -> S3PanelInput | U3PanelInput | None: + """Build an S3/U3 input from an ANYstructure AllStructure-like object. + + This is intentionally duck-typed so ANYstructure can call it without making + this repository import ANYstructure. The `lat_press` convention follows + ANYstructure optimization: kPa in, MPa on the solver input surface. + """ + + all_structure = calc_object[0] if isinstance(calc_object, (list, tuple)) else calc_object + plate = getattr(all_structure, "Plate", None) + stiffener = getattr(all_structure, "Stiffener", None) + if plate is None: + return None + + sp_or_up = str(plate.get_puls_sp_or_up()).strip().upper() + pressure_mpa = _anystructure_float(lat_press) / 1000.0 + elastic_modulus = _anystructure_float(getattr(all_structure, "E", None), 210000.0e6) / 1.0e6 + poisson_ratio = _anystructure_float(getattr(all_structure, "v", None), 0.3) + + if sp_or_up == "SP" and stiffener is not None: + puls_boundary = stiffener.get_puls_boundary() + sigxd = _anystructure_axial_design_stress(stiffener.sigma_x1, stiffener.sigma_x2) + return S3PanelInput( + length=stiffener.span * 1000.0, + stiffener_spacing=stiffener.spacing, + plate_thickness=stiffener.t, + stiffener_type=_anystructure_stiffener_type(stiffener.get_stiffener_type()), + stiffener_boundary=_anystructure_stiffener_boundary(stiffener.get_puls_stf_end()), + stiffener_height=stiffener.hw, + web_thickness=stiffener.tw, + flange_width=stiffener.b, + flange_thickness=stiffener.tf, + yield_stress_plate=stiffener.mat_yield / 1.0e6, + yield_stress_stiffener=stiffener.mat_yield / 1.0e6, + axial_stress=0.0 if puls_boundary == "GT" else sigxd, + transverse_stress_1=0.0 if puls_boundary == "GL" else stiffener.sigma_y1, + transverse_stress_2=0.0 if puls_boundary == "GL" else stiffener.sigma_y2, + shear_stress=stiffener.tau_xy, + pressure=pressure_mpa, + in_plane_support=_anystructure_in_plane_support(puls_boundary), + elastic_modulus=elastic_modulus, + poisson_ratio=poisson_ratio, + ) + + up_boundary = plate.get_puls_up_boundary() if hasattr(plate, "get_puls_up_boundary") else "SSSS" + rotational_1, rotational_2 = _anystructure_rotational_supports(up_boundary) + puls_boundary = plate.get_puls_boundary() if hasattr(plate, "get_puls_boundary") else "GL" + return U3PanelInput( + length=plate.span * 1000.0, + width=plate.spacing, + plate_thickness=plate.t, + yield_stress_plate=plate.mat_yield / 1.0e6, + axial_stress_1=plate.sigma_x1, + axial_stress_2=plate.sigma_x2, + transverse_stress_1=plate.sigma_y1, + transverse_stress_2=plate.sigma_y2, + shear_stress=plate.tau_xy, + pressure=pressure_mpa, + in_plane_support=_anystructure_in_plane_support(puls_boundary), + rotational_support_1=rotational_1, + rotational_support_2=rotational_2, + elastic_modulus=elastic_modulus, + poisson_ratio=poisson_ratio, + ) + + +def solve_anystructure_panel( + calc_object: Any, + lat_press: float = 0.0, + config: S3SolverConfig | None = None, +) -> dict[str, Any]: + """Return an ANYstructure-friendly SemiAnalytical result dictionary.""" + + config = config or S3SolverConfig() + all_structure = calc_object[0] if isinstance(calc_object, (list, tuple)) else calc_object + material_factor = _anystructure_material_factor(all_structure) + acceptance_limit = None if material_factor is None else 1.0 / material_factor + panel = anystructure_panel_input(calc_object, lat_press) + if panel is None: + return { + "panel_family": None, + "buckling_usage_factor": None, + "ultimate_usage_factor": None, + "material_factor": material_factor, + "acceptance_limit": acceptance_limit, + "valid": False, + "available": False, + "valid_prediction": 0, + "valid_label": "SemiAnalytical S3/U3 unsupported or invalid", + "invalid_reason": "unsupported-anystructure-input", + "confidence": "low", + "confidence_reasons": ["unsupported-anystructure-input"], + "diagnostics": {}, + } + + panel_family = "S3" if isinstance(panel, S3PanelInput) else "U3" + solved = solve_s3_panel(panel, config) if panel_family == "S3" else solve_u3_panel(panel, config) + valid_prediction = ( + solved.valid + and solved.buckling_usage_factor is not None + and solved.ultimate_usage_factor is not None + ) + valid_label = ( + f"valid SemiAnalytical {panel_family} UF predicted ({solved.confidence} confidence)" + if valid_prediction + else f"SemiAnalytical {panel_family} unsupported or invalid" + ) + return { + "panel_family": panel_family, + "buckling_usage_factor": solved.buckling_usage_factor, + "ultimate_usage_factor": solved.ultimate_usage_factor, + "elastic_buckling_usage_factor": solved.elastic_buckling_usage_factor, + "material_factor": material_factor, + "acceptance_limit": acceptance_limit, + "valid": solved.valid, + "available": bool(valid_prediction), + "valid_prediction": 1 if valid_prediction else 0, + "valid_label": valid_label, + "invalid_reason": solved.invalid_reason, + "confidence": solved.confidence, + "confidence_reasons": list(solved.confidence_reasons), + "diagnostics": solved.diagnostics, + "result": solved.to_dict(), + } + + +def predict_anystructure_uf( + calc_object: Any, + lat_press: float = 0.0, + config: S3SolverConfig | None = None, +) -> np.ndarray: + """Return legacy ANYstructure vector [buckling UF, ultimate UF, valid]. + + The factors are intentionally returned un-factored. ANYstructure compares + them against a separate PULS acceptance limit, typically 1 / material factor. + """ + + result = solve_anystructure_panel(calc_object, lat_press, config) + if result["available"]: + return np.array( + [ + float(result["buckling_usage_factor"]), + float(result["ultimate_usage_factor"]), + 1.0, + ], + dtype=float, + ) + return np.array([float("inf"), float("inf"), 0.0], dtype=float) + + +def predict_anystructure_uf_with_acceptance( + calc_object: Any, + lat_press: float = 0.0, + config: S3SolverConfig | None = None, + default_acceptance: float = 0.87, +) -> np.ndarray: + """Return [buckling UF, ultimate UF, valid, acceptance limit] for ANYstructure.""" + + result = solve_anystructure_panel(calc_object, lat_press, config) + acceptance = _optional_float(result.get("acceptance_limit")) + if acceptance is None: + acceptance = float(default_acceptance) + vector = np.array([float("inf"), float("inf"), 0.0, acceptance], dtype=float) + if result["available"]: + vector[0] = float(result["buckling_usage_factor"]) + vector[1] = float(result["ultimate_usage_factor"]) + vector[2] = 1.0 + return vector + + +def normalized_load_components(panel: S3PanelInput) -> dict[str, float]: + """Return stress resultants and destabilizing load components. + + Positive values in the `compression_*` fields are compression resultants + per unit length. Negative normal stresses still enter the yield checks + with their signed values, but are not treated as elastic buckling drivers. + """ + + plate_t = panel.plate_thickness + return { + "signed_axial_stress": panel.axial_stress, + "signed_transverse_stress": panel.mean_transverse_stress, + "signed_shear_stress": panel.shear_stress, + "compression_nx": max(panel.axial_stress, 0.0) * plate_t, + "compression_ny": max(panel.mean_transverse_stress, 0.0) * plate_t, + "shear_nxy": abs(panel.shear_stress) * plate_t, + "pressure": max(panel.pressure, 0.0), + } + + +def build_section_properties( + panel: S3PanelInput, + attached_plate_width: float | None = None, + stiffener_web_thickness: float | None = None, +) -> S3SectionProperties: + """Return strip section properties about the plate/stiffener axis.""" + + plate_t = panel.plate_thickness + web_h = panel.stiffener_height + web_thickness = panel.web_thickness if stiffener_web_thickness is None else stiffener_web_thickness + plate_width = panel.width if attached_plate_width is None else min(attached_plate_width, panel.width) + rectangles = [ + _Rectangle( + area=max(plate_width, EPS) * plate_t, + centroid=0.0, + height=plate_t, + ), + _Rectangle( + area=web_h * max(web_thickness, EPS), + centroid=0.5 * plate_t + 0.5 * web_h, + height=web_h, + ), + ] + + if panel.stiffener_type != "Flatbar" and panel.flange_width > 0.0 and panel.flange_thickness > 0.0: + rectangles.append( + _Rectangle( + area=panel.flange_width * panel.flange_thickness, + centroid=0.5 * plate_t + web_h + 0.5 * panel.flange_thickness, + height=panel.flange_thickness, + ) + ) + + total_area = sum(rectangle.area for rectangle in rectangles) + centroid = sum(rectangle.area * rectangle.centroid for rectangle in rectangles) / max(total_area, EPS) + inertia = sum( + rectangle.local_inertia_x + rectangle.area * (rectangle.centroid - centroid) ** 2 + for rectangle in rectangles + ) + + top_coordinate = max( + 0.5 * plate_t, + 0.5 * plate_t + web_h, + 0.5 * plate_t + web_h + max(panel.flange_thickness, 0.0), + ) + bottom_coordinate = -0.5 * plate_t + plate_area = rectangles[0].area + return S3SectionProperties( + area=total_area, + centroid_from_plate_midplane=centroid, + inertia_x=inertia, + top_distance=max(top_coordinate - centroid, EPS), + bottom_distance=max(centroid - bottom_coordinate, EPS), + plate_area=plate_area, + stiffener_area=max(total_area - plate_area, 0.0), + ) + + +def effective_stiffener_plate_width(panel: S3PanelInput) -> dict[str, float]: + """Return the shear-lag effective width used by stiffener checks. + + IACS S35 and the matching DNV stiffener text reduce attached plating by an + effective-width coefficient based on stiffener effective length. The S3 CSV + surface has a regular strip width but not separate plate reduction factors + for the two sides of a stiffener, so this helper applies the length-based + coefficient only and records that width in diagnostics. + """ + + effective_length = ( + panel.length / math.sqrt(3.0) + if panel.stiffener_boundary == "Cont" + else panel.length + ) + length_ratio = effective_length / max(panel.width, EPS) + if length_ratio >= 1.0: + coefficient = 1.12 / (1.0 + 1.75 * length_ratio**1.6) + coefficient = min(coefficient, 1.0) + else: + coefficient = 0.407 * length_ratio + coefficient = min(1.0, max(coefficient, EPS)) + return { + "width": coefficient * panel.width, + "coefficient": coefficient, + "effective_length": effective_length, + "length_ratio": length_ratio, + } + + +def effective_flatbar_web_thickness( + panel: S3PanelInput, + attached_plate_width: float, +) -> dict[str, float]: + """Return the effective flat-bar web thickness for stiffener checks.""" + + if panel.stiffener_type != "Flatbar": + return { + "gross_thickness": panel.web_thickness, + "effective_thickness": panel.web_thickness, + "reduction_factor": 1.0, + "attached_plate_width": attached_plate_width, + } + + width_ratio = min(max(attached_plate_width / max(panel.width, EPS), 0.0), 1.0) + reduction_factor = ( + 1.0 + - 2.0 + * math.pi**2 + / 3.0 + * (panel.stiffener_height / max(panel.width, EPS)) ** 2 + * (1.0 - width_ratio) + ) + reduction_factor = min(1.0, max(reduction_factor, EPS)) + return { + "gross_thickness": panel.web_thickness, + "effective_thickness": reduction_factor * panel.web_thickness, + "reduction_factor": reduction_factor, + "attached_plate_width": attached_plate_width, + } + + +def build_effective_stiffener_section( + panel: S3PanelInput, + config: S3SolverConfig, +) -> tuple[S3SectionProperties, dict[str, Any]]: + effective_width = effective_stiffener_plate_width(panel) + attached_width = effective_width["width"] if config.use_effective_stiffener_width else panel.width + flatbar_web = effective_flatbar_web_thickness(panel, attached_width) + if not config.use_effective_stiffener_width: + effective_width = {**effective_width, "applied_width": panel.width} + else: + effective_width = {**effective_width, "applied_width": attached_width} + effective_width = {**effective_width, "flatbar_web_thickness": flatbar_web} + return ( + build_section_properties( + panel, + attached_plate_width=attached_width, + stiffener_web_thickness=flatbar_web["effective_thickness"], + ), + effective_width, + ) + + +def _plate_bending_rigidity(panel: S3PanelInput) -> float: + return panel.elastic_modulus * panel.plate_thickness**3 / ( + 12.0 * (1.0 - panel.poisson_ratio**2) + ) + + +def build_orthotropic_stiffness( + panel: S3PanelInput, + section: S3SectionProperties, + family: str, + config: S3SolverConfig | None = None, +) -> OrthotropicStiffness: + """Build local-plate or distributed stiffened-strip stiffness terms.""" + + config = config or S3SolverConfig() + plate_d = _plate_bending_rigidity(panel) + if family == "local": + restraint_factor = ( + max(config.flanged_local_plate_restraint_factor, EPS) + if panel.stiffener_type != "Flatbar" + else 1.0 + ) + d11 = plate_d * restraint_factor + d22 = plate_d * restraint_factor + membrane_thickness = panel.plate_thickness + elif family == "global": + d11 = max(panel.elastic_modulus * section.inertia_x / panel.width, plate_d) + d22 = plate_d + membrane_thickness = max(section.area / panel.width, panel.plate_thickness) + else: + raise ValueError(f"Unknown stiffness family: {family}") + + d12 = panel.poisson_ratio * math.sqrt(d11 * d22) + d66 = 0.5 * (1.0 - panel.poisson_ratio) * math.sqrt(d11 * d22) + return OrthotropicStiffness( + d11=d11, + d12=d12, + d22=d22, + d66=d66, + membrane_thickness=membrane_thickness, + ) + + +def _with_longitudinal_stiffness_scale( + stiffness: OrthotropicStiffness, + scale: float, +) -> OrthotropicStiffness: + scale = max(scale, EPS) + d11 = stiffness.d11 * scale + d22 = stiffness.d22 + return OrthotropicStiffness( + d11=d11, + d12=math.sqrt(scale) * stiffness.d12, + d22=d22, + d66=math.sqrt(scale) * stiffness.d66, + membrane_thickness=stiffness.membrane_thickness, + ) + + +def _support_membrane_factor(panel: S3PanelInput) -> float: + if panel.in_plane_support == "Integrated": + return 1.0 + if panel.in_plane_support == "Girder - long": + return 0.90 + if panel.in_plane_support == "Girder - trans": + return 0.90 + return 1.0 + + +def _load_family(panel: S3PanelInput | U3PanelInput) -> str: + components = [] + if panel.axial_stress > EPS: + components.append("axial-compression") + elif panel.axial_stress < -EPS: + components.append("axial-tension") + if panel.mean_transverse_stress > EPS: + components.append("transverse-compression") + elif panel.mean_transverse_stress < -EPS: + components.append("transverse-tension") + if abs(panel.shear_stress) > EPS: + components.append("shear") + if panel.pressure > EPS: + components.append("pressure") + return "+".join(components) if components else "zero-variable-load" + + +def _limit_check(value: float | None, *, maximum: float | None = None, minimum: float | None = None) -> bool | None: + if value is None: + return None + if maximum is not None and value > maximum: + return False + if minimum is not None and value < minimum: + return False + return True + + +def _puls_manual_reference_domain(panel: S3PanelInput | U3PanelInput) -> dict[str, Any]: + """Return source-backed PULS manual limit diagnostics without gating solves.""" + + if isinstance(panel, S3PanelInput): + limits = PULS_MANUAL_S3_LIMITS + aspect_ratio = panel.length / panel.width if panel.width > EPS else None + plate_slenderness = panel.width / panel.plate_thickness if panel.plate_thickness > EPS else None + web_slenderness = ( + panel.stiffener_height / panel.web_thickness + if panel.web_thickness > EPS + else None + ) + web_limit = ( + limits["flatbar_web_slenderness_max"] + if panel.stiffener_type == "Flatbar" + else limits["open_profile_web_slenderness_max"] + ) + flange_slenderness = None + flange_width_to_web_height = None + if panel.stiffener_type != "Flatbar" and panel.flange_width > EPS and panel.flange_thickness > EPS: + flange_slenderness = panel.flange_width / panel.flange_thickness + flange_width_to_web_height = panel.flange_width / max(panel.stiffener_height, EPS) + checks = { + "plate_slenderness": _limit_check(plate_slenderness, maximum=limits["plate_slenderness_max"]), + "aspect_ratio": _limit_check( + aspect_ratio, + minimum=limits["aspect_ratio_min"], + maximum=limits["aspect_ratio_max"], + ), + "web_slenderness": _limit_check(web_slenderness, maximum=web_limit), + "free_flange_slenderness": ( + True + if panel.stiffener_type == "Flatbar" + else _limit_check(flange_slenderness, maximum=limits["free_flange_slenderness_max"]) + ), + "flange_width_to_web_height": ( + True + if panel.stiffener_type == "Flatbar" + else _limit_check( + flange_width_to_web_height, + minimum=limits["min_flange_width_to_web_height"], + ) + ), + } + failed = [name for name, passed in checks.items() if passed is False] + unknown = [name for name, passed in checks.items() if passed is None] + return { + "source": limits["source"], + "manual_file_date": limits["manual_file_date"], + "panel_family": "S3", + "note": "manual reference limits are diagnostic only; solver covered-domain gates are unchanged", + "limits": {key: value for key, value in limits.items() if key not in {"source", "manual_file_date"}}, + "values": { + "aspect_ratio": aspect_ratio, + "plate_slenderness": plate_slenderness, + "web_slenderness": web_slenderness, + "web_slenderness_limit": web_limit, + "free_flange_slenderness": flange_slenderness, + "flange_width_to_web_height": flange_width_to_web_height, + }, + "checks": checks, + "failed": failed, + "unknown": unknown, + "within_manual_limits": not failed and not unknown, + } + + limits = PULS_MANUAL_U3_LIMITS + short_side = min(panel.length, panel.width) + long_side = max(panel.length, panel.width) + long_to_short = long_side / max(short_side, EPS) + plate_slenderness = short_side / panel.plate_thickness if panel.plate_thickness > EPS else None + checks = { + "plate_slenderness": _limit_check(plate_slenderness, maximum=limits["plate_slenderness_max"]), + "long_to_short_aspect_ratio": _limit_check( + long_to_short, + maximum=limits["long_to_short_aspect_ratio_max"], + ), + } + failed = [name for name, passed in checks.items() if passed is False] + unknown = [name for name, passed in checks.items() if passed is None] + return { + "source": limits["source"], + "manual_file_date": limits["manual_file_date"], + "panel_family": "U3", + "note": "manual reference limits are diagnostic only; solver covered-domain gates are unchanged", + "limits": {key: value for key, value in limits.items() if key not in {"source", "manual_file_date"}}, + "values": { + "long_to_short_aspect_ratio": long_to_short, + "plate_slenderness": plate_slenderness, + }, + "checks": checks, + "failed": failed, + "unknown": unknown, + "within_manual_limits": not failed and not unknown, + } + + +def _validation_domain( + panel: S3PanelInput | U3PanelInput, + config: S3SolverConfig, + panel_family: str, + reasons: Sequence[str] | None = None, +) -> dict[str, Any]: + width = panel.width + plate_slenderness = width / panel.plate_thickness if panel.plate_thickness > EPS else None + aspect_ratio = panel.length / width if width > EPS else None + domain: dict[str, Any] = { + "panel_family": panel_family, + "aspect_ratio": aspect_ratio, + "aspect_ratio_limits": [config.min_aspect_ratio, config.max_aspect_ratio], + "plate_slenderness": plate_slenderness, + "max_plate_slenderness": config.max_plate_slenderness, + "in_plane_support": panel.in_plane_support, + "support_model": SUPPORTED_IN_PLANE_SUPPORTS.get(panel.in_plane_support), + "pressure": panel.pressure, + "pressure_category": "nonzero" if panel.pressure > EPS else "zero", + "load_family": _load_family(panel), + "reasons": list(reasons or ()), + "puls_manual_reference": _puls_manual_reference_domain(panel), + } + if isinstance(panel, S3PanelInput): + domain.update( + { + "stiffener_type": panel.stiffener_type, + "stiffener_boundary": panel.stiffener_boundary, + "web_slenderness": ( + panel.stiffener_height / panel.web_thickness + if panel.web_thickness > EPS + else None + ), + "max_web_slenderness": config.max_web_slenderness, + "flange_slenderness": ( + panel.flange_width / panel.flange_thickness + if panel.flange_thickness > EPS + else None + ), + "max_flange_slenderness": config.max_flange_slenderness, + "web_to_flange_ratio": ( + panel.stiffener_height / max(panel.flange_width, panel.web_thickness) + if max(panel.flange_width, panel.web_thickness) > EPS + else None + ), + "max_web_to_flange_ratio": config.max_web_to_flange_ratio, + } + ) + else: + domain.update( + { + "rotational_support": { + "x_edges": panel.rotational_support_1, + "y_edges": panel.rotational_support_2, + }, + } + ) + return domain + + +def collect_s3_validation_reasons(panel: S3PanelInput, config: S3SolverConfig) -> list[str]: + """Return all explicit S3 domain/validity reasons in stable first-reason order.""" + + numeric_fields = { + "length": panel.length, + "stiffener_spacing": panel.stiffener_spacing, + "plate_thickness": panel.plate_thickness, + "stiffener_height": panel.stiffener_height, + "web_thickness": panel.web_thickness, + "flange_width": panel.flange_width, + "flange_thickness": panel.flange_thickness, + "yield_stress_plate": panel.yield_stress_plate, + "yield_stress_stiffener": panel.yield_stress_stiffener, + "elastic_modulus": panel.elastic_modulus, + "poisson_ratio": panel.poisson_ratio, + "axial_stress": panel.axial_stress, + "transverse_stress_1": panel.transverse_stress_1, + "transverse_stress_2": panel.transverse_stress_2, + "shear_stress": panel.shear_stress, + "pressure": panel.pressure, + } + reasons: list[str] = [] + if any(not math.isfinite(value) for value in numeric_fields.values()): + return ["non-finite-input"] + if panel.poisson_ratio < 0.0 or panel.poisson_ratio >= 0.5: + reasons.append("unsupported-material") + positive_fields = { + "length": panel.length, + "stiffener_spacing": panel.stiffener_spacing, + "plate_thickness": panel.plate_thickness, + "stiffener_height": panel.stiffener_height, + "web_thickness": panel.web_thickness, + "yield_stress_plate": panel.yield_stress_plate, + "yield_stress_stiffener": panel.yield_stress_stiffener, + "elastic_modulus": panel.elastic_modulus, + } + if any(value <= 0.0 for value in positive_fields.values()): + reasons.append("non-positive-geometry-or-material") + if panel.pressure < 0.0: + reasons.append("negative-pressure-unsupported") + if panel.stiffener_type not in SUPPORTED_STIFFENER_TYPES: + reasons.append("unsupported-stiffener-type") + if panel.stiffener_boundary not in SUPPORTED_STIFFENER_BOUNDARIES: + reasons.append("unsupported-stiffener-boundary") + if panel.in_plane_support not in SUPPORTED_IN_PLANE_SUPPORTS: + reasons.append("unsupported-in-plane-support") + if panel.width > EPS: + aspect_ratio = panel.length / panel.width + if aspect_ratio < config.min_aspect_ratio or aspect_ratio > config.max_aspect_ratio: + reasons.append("aspect ratio") + if panel.plate_thickness > EPS and panel.width / panel.plate_thickness > config.max_plate_slenderness: + reasons.append("slenderness") + if panel.web_thickness > EPS and panel.stiffener_height / panel.web_thickness > config.max_web_slenderness: + reasons.append("slenderness") + if panel.stiffener_type != "Flatbar": + if panel.flange_width <= 0.0 or panel.flange_thickness <= 0.0: + reasons.append("web-flange-ratio") + elif panel.flange_width / panel.flange_thickness > config.max_flange_slenderness: + reasons.append("web-flange-ratio") + if ( + panel.stiffener_type in {"Angle", "T-bar"} + and panel.stiffener_height / max(panel.flange_width, panel.web_thickness, EPS) + > config.max_web_to_flange_ratio + ): + reasons.append("web-flange-ratio") + return list(dict.fromkeys(reasons)) + + +def collect_u3_validation_reasons(panel: U3PanelInput, config: S3SolverConfig) -> list[str]: + """Return all explicit U3 domain/validity reasons in stable first-reason order.""" + + numeric_fields = { + "length": panel.length, + "width": panel.width, + "plate_thickness": panel.plate_thickness, + "yield_stress_plate": panel.yield_stress_plate, + "elastic_modulus": panel.elastic_modulus, + "poisson_ratio": panel.poisson_ratio, + "axial_stress_1": panel.axial_stress_1, + "axial_stress_2": panel.axial_stress_2, + "transverse_stress_1": panel.transverse_stress_1, + "transverse_stress_2": panel.transverse_stress_2, + "shear_stress": panel.shear_stress, + "pressure": panel.pressure, + } + reasons: list[str] = [] + if any(not math.isfinite(value) for value in numeric_fields.values()): + return ["non-finite-input"] + if panel.poisson_ratio < 0.0 or panel.poisson_ratio >= 0.5: + reasons.append("unsupported-material") + positive_fields = { + "length": panel.length, + "width": panel.width, + "plate_thickness": panel.plate_thickness, + "yield_stress_plate": panel.yield_stress_plate, + "elastic_modulus": panel.elastic_modulus, + } + if any(value <= 0.0 for value in positive_fields.values()): + reasons.append("non-positive-geometry-or-material") + if panel.pressure < 0.0: + reasons.append("negative-pressure-unsupported") + if panel.in_plane_support not in SUPPORTED_IN_PLANE_SUPPORTS: + reasons.append("unsupported-in-plane-support") + if panel.rotational_support_1 not in SUPPORTED_ROTATIONAL_SUPPORTS: + reasons.append("unsupported-rotational-support") + if panel.rotational_support_2 not in SUPPORTED_ROTATIONAL_SUPPORTS: + reasons.append("unsupported-rotational-support") + if panel.width > EPS: + aspect_ratio = panel.length / panel.width + if aspect_ratio < config.min_aspect_ratio or aspect_ratio > config.max_aspect_ratio: + reasons.append("aspect ratio") + if panel.plate_thickness > EPS and panel.width / panel.plate_thickness > config.max_plate_slenderness: + reasons.append("slenderness") + return list(dict.fromkeys(reasons)) + + +def validate_s3_input(panel: S3PanelInput, config: S3SolverConfig) -> str | None: + """Validate the explicit covered domain before solving.""" + + reasons = collect_s3_validation_reasons(panel, config) + return reasons[0] if reasons else None + + +def validate_u3_input(panel: U3PanelInput, config: S3SolverConfig) -> str | None: + """Validate the explicit U3 covered domain before solving.""" + + reasons = collect_u3_validation_reasons(panel, config) + return reasons[0] if reasons else None + + +def _pressure_generalized_force(panel: S3PanelInput, m: int, n: int, share: float) -> float: + if panel.pressure <= 0.0 or m % 2 == 0 or n % 2 == 0: + return 0.0 + return share * panel.pressure * 4.0 * panel.length * panel.width / (m * n * math.pi**2) + + +def _mode_linear_terms( + panel: S3PanelInput, + stiffness: OrthotropicStiffness, + config: S3SolverConfig, + m: int, + n: int, +) -> tuple[float, float, float, float]: + kx = m * math.pi / panel.length + ky = n * math.pi / panel.width + area_factor = panel.length * panel.width / 4.0 + bending = ( + stiffness.d11 * kx**4 + + 2.0 * (stiffness.d12 + 2.0 * stiffness.d66) * kx * kx * ky * ky + + stiffness.d22 * ky**4 + ) + + loads = normalized_load_components(panel) + geometric = ( + loads["compression_nx"] * kx * kx + + loads["compression_ny"] * ky * ky + ) + linear_stiffness = area_factor * bending + geometric_stiffness = area_factor * geometric + wave_norm = kx * kx + ky * ky + nonlinear_stiffness = ( + config.nonlinear_membrane_factor + * _support_membrane_factor(panel) + * panel.elastic_modulus + * stiffness.membrane_thickness + * panel.length + * panel.width + * wave_norm + * wave_norm + / 16.0 + ) + return kx, ky, linear_stiffness, max(geometric_stiffness, 0.0), nonlinear_stiffness + + +def _sin_cos_integral(sine_mode: int, cosine_mode: int, length: float) -> float: + """Return the Fourier integral int_0^L sin(p*pi*x/L) cos(q*pi*x/L) dx.""" + + def harmonic_integral(harmonic: int) -> float: + if harmonic == 0 or harmonic % 2 == 0: + return 0.0 + return 2.0 / harmonic + + return ( + 0.5 + * length + / math.pi + * ( + harmonic_integral(sine_mode + cosine_mode) + + harmonic_integral(sine_mode - cosine_mode) + ) + ) + + +def _rectangular_mode_shear_geometric_integral( + length: float, + width: float, + first: RitzMode, + second: RitzMode, +) -> float: + """Return symmetric unit-Nxy geometric coupling between Ritz modes.""" + + first_x_second_y = ( + first.kx + * second.ky + * _sin_cos_integral(second.m, first.m, length) + * _sin_cos_integral(first.n, second.n, width) + ) + second_x_first_y = ( + second.kx + * first.ky + * _sin_cos_integral(first.m, second.m, length) + * _sin_cos_integral(second.n, first.n, width) + ) + return first_x_second_y + second_x_first_y + + +def _mode_shear_geometric_integral( + panel: S3PanelInput, + first: RitzMode, + second: RitzMode, +) -> float: + return _rectangular_mode_shear_geometric_integral( + panel.length, + panel.width, + first, + second, + ) + + +def build_ritz_modes( + panel: S3PanelInput, + section: S3SectionProperties, + config: S3SolverConfig, + global_stiffness_scale: float = 1.0, +) -> list[RitzMode]: + modes: list[RitzMode] = [] + imperfection = max( + config.initial_imperfection_floor_mm, + config.initial_imperfection_ratio * min(panel.length, panel.width), + ) + global_pressure_share = ( + 0.0 + if config.use_separate_s3_pressure_modes + else config.pressure_global_share + ) + for family, pressure_share in ( + ("local", config.pressure_local_share), + ("global", global_pressure_share), + ): + stiffness = build_orthotropic_stiffness(panel, section, family, config) + if family == "global": + stiffness = _with_longitudinal_stiffness_scale(stiffness, global_stiffness_scale) + for m in config.longitudinal_modes: + for n in config.transverse_modes: + kx, ky, linear, geometric, nonlinear = _mode_linear_terms( + panel, + stiffness, + config, + m, + n, + ) + modes.append( + RitzMode( + family=family, + m=m, + n=n, + kx=kx, + ky=ky, + linear_stiffness=linear, + geometric_stiffness=geometric, + pressure_force=_pressure_generalized_force(panel, m, n, pressure_share), + imperfection=imperfection, + nonlinear_stiffness=max(nonlinear, EPS), + ) + ) + if config.use_separate_s3_pressure_modes and config.pressure_global_share > EPS: + stiffness = build_orthotropic_stiffness(panel, section, "global", config) + stiffness = _with_longitudinal_stiffness_scale(stiffness, global_stiffness_scale) + stiffness = OrthotropicStiffness( + d11=stiffness.d11 * max(config.s3_pressure_mode_stiffness_factor, EPS), + d12=stiffness.d12 * max(config.s3_pressure_mode_stiffness_factor, EPS), + d22=stiffness.d22 * max(config.s3_pressure_mode_stiffness_factor, EPS), + d66=stiffness.d66 * max(config.s3_pressure_mode_stiffness_factor, EPS), + membrane_thickness=stiffness.membrane_thickness, + ) + m = config.longitudinal_modes[0] if config.longitudinal_modes else 1 + n = config.transverse_modes[0] if config.transverse_modes else 1 + kx, ky, linear, _, nonlinear = _mode_linear_terms(panel, stiffness, config, m, n) + modes.append( + RitzMode( + family="global-pressure", + m=m, + n=n, + kx=kx, + ky=ky, + linear_stiffness=linear, + geometric_stiffness=0.0, + pressure_force=_pressure_generalized_force( + panel, + m, + n, + config.pressure_global_share, + ), + imperfection=0.0, + nonlinear_stiffness=max(nonlinear, EPS), + ) + ) + return modes + + +def _isotropic_plate_stiffness(panel: U3PanelInput | S3PanelInput) -> OrthotropicStiffness: + d = _plate_bending_rigidity(panel) + return OrthotropicStiffness( + d11=d, + d12=panel.poisson_ratio * d, + d22=d, + d66=0.5 * (1.0 - panel.poisson_ratio) * d, + membrane_thickness=panel.plate_thickness, + ) + + +def build_u3_ritz_modes(panel: U3PanelInput, config: S3SolverConfig) -> list[RitzMode]: + """Return the plate-only Rayleigh-Ritz modes used by the U3 solver.""" + + modes: list[RitzMode] = [] + imperfection = max( + config.initial_imperfection_floor_mm, + config.initial_imperfection_ratio * min(panel.length, panel.width), + ) + stiffness = _isotropic_plate_stiffness(panel) + for m in config.longitudinal_modes: + for n in config.transverse_modes: + kx, ky, linear, geometric, nonlinear = _mode_linear_terms( + panel, + stiffness, + config, + m, + n, + ) + modes.append( + RitzMode( + family="plate", + m=m, + n=n, + kx=kx, + ky=ky, + linear_stiffness=linear, + geometric_stiffness=geometric, + pressure_force=_pressure_generalized_force(panel, m, n, 1.0), + imperfection=imperfection, + nonlinear_stiffness=max(nonlinear, EPS), + ) + ) + return modes + + +def ritz_combined_buckling_factor( + panel: S3PanelInput, + modes: Sequence[RitzMode], + family: str, +) -> dict[str, Any] | None: + """Return a coupled linear Ritz factor for compression plus panel shear. + + Normal compression is diagonal in the sine basis. Panel shear couples + opposite-parity Fourier terms and is the mechanism that lets a double + Fourier expansion describe inclined elastic shear shapes. This truncated + candidate makes that interaction explicit without replacing the classical + pure-shear plate diagnostic retained for a narrow reduced basis. + """ + + family_modes = [mode for mode in modes if mode.family == family] + if len(family_modes) < 2: + return None + + linear = np.diag([mode.linear_stiffness for mode in family_modes]) + geometric, coupling = _ritz_geometric_matrix(panel, family_modes) + if coupling["coupled_terms"] == 0: + return None + + linear_diagonal = np.diag(linear) + inverse_sqrt_linear = np.diag(1.0 / np.sqrt(np.maximum(linear_diagonal, EPS))) + transformed = inverse_sqrt_linear @ geometric @ inverse_sqrt_linear + eigenvalues, eigenvectors = np.linalg.eigh(transformed) + positive_indices = [ + index + for index, eigenvalue in enumerate(eigenvalues) + if eigenvalue > EPS and math.isfinite(float(eigenvalue)) + ] + if not positive_indices: + return None + + critical_index = max(positive_indices, key=lambda index: float(eigenvalues[index])) + eigenvalue = float(eigenvalues[critical_index]) + transformed_vector = eigenvectors[:, critical_index] + physical_vector = inverse_sqrt_linear @ transformed_vector + modal_weights = np.abs(physical_vector) + weight_sum = float(np.sum(modal_weights)) + composition = [] + if weight_sum > EPS: + composition = [ + { + "mode": mode.label, + "weight": float(weight / weight_sum), + } + for mode, weight in sorted( + zip(family_modes, modal_weights), + key=lambda item: float(item[1]), + reverse=True, + ) + if weight > EPS + ] + return { + "factor": 1.0 / eigenvalue, + "eigenvalue": eigenvalue, + "family": family, + "signed_shear_resultant": coupling["signed_shear_resultant"], + "coupled_terms": coupling["coupled_terms"], + "mode_composition": composition[:4], + } + + +def _ritz_geometric_matrix( + panel: S3PanelInput, + modes: Sequence[RitzMode], +) -> tuple[np.ndarray, dict[str, Any]]: + """Return normal-plus-shear geometric stiffness for Ritz modes. + + The local and distributed-global families share the trigonometric panel + coordinates but represent different reduced S3 displacement fields. Shear + coupling is therefore kept inside each family block. + """ + + geometric = np.diag([mode.geometric_stiffness for mode in modes]) + signed_shear_resultant = panel.shear_stress * panel.plate_thickness + family_terms: dict[str, int] = {} + if abs(signed_shear_resultant) <= EPS or len(modes) < 2: + return geometric, { + "signed_shear_resultant": signed_shear_resultant, + "coupled_terms": 0, + "family_terms": family_terms, + } + + coupled_terms = 0 + for row, first in enumerate(modes): + for column in range(row + 1, len(modes)): + second = modes[column] + if first.family != second.family: + continue + coupling = ( + signed_shear_resultant + * _mode_shear_geometric_integral(panel, first, second) + ) + if abs(coupling) <= EPS: + continue + geometric[row, column] += coupling + geometric[column, row] += coupling + coupled_terms += 1 + family_terms[first.family] = family_terms.get(first.family, 0) + 1 + return geometric, { + "signed_shear_resultant": signed_shear_resultant, + "coupled_terms": coupled_terms, + "family_terms": family_terms, + } + + +def _local_amplitude_ratio(panel: S3PanelInput, modes: Sequence[RitzMode], amplitudes: Sequence[float]) -> float: + local_amplitude = sum(abs(amplitude) for mode, amplitude in zip(modes, amplitudes) if mode.family == "local") + local_span = max(min(panel.length, panel.width), EPS) + return local_amplitude / local_span + + +def local_global_stiffness_scale( + panel: S3PanelInput, + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + load_factor: float, + config: S3SolverConfig, +) -> dict[str, float]: + """Return reduced global longitudinal stiffness from local response. + + The Byklum model family uses local response to supply anisotropic stiffness + to a global orthotropic plate model. This reduced pass applies a scalar + longitudinal degradation driven by local-family elastic utilization so the + interaction is visible and configurable. Local amplitude is still + reported, but not used as a degradation driver because pressure deflection + in the reduced basis makes that scalar too noisy. + """ + + local_mode_factors = [ + mode.linear_stiffness / mode.geometric_stiffness + for mode in modes + if mode.family == "local" and mode.geometric_stiffness > EPS + ] + local_elastic_utilization = ( + load_factor / max(min(local_mode_factors), EPS) + if local_mode_factors + else 0.0 + ) + amplitude_ratio = _local_amplitude_ratio(panel, modes, amplitudes) + amplitude_utilization = amplitude_ratio / max(config.initial_imperfection_ratio, EPS) + interaction_driver = local_elastic_utilization + scale = 1.0 / (1.0 + config.local_global_coupling_gain * max(interaction_driver - 1.0, 0.0)) + scale = min(1.0, max(config.local_global_coupling_floor, scale)) + return { + "scale": scale, + "local_elastic_utilization": local_elastic_utilization, + "local_amplitude_ratio": amplitude_ratio, + "amplitude_utilization": amplitude_utilization, + "interaction_driver": interaction_driver, + } + + +def _stiffener_column_factor(panel: S3PanelInput, section: S3SectionProperties) -> float | None: + compression = max(panel.axial_stress, 0.0) + if compression <= EPS: + return None + effective_length = panel.length * (0.70 if panel.stiffener_boundary == "Cont" else 1.0) + euler_force = math.pi**2 * panel.elastic_modulus * section.inertia_x / max(effective_length**2, EPS) + reference_force = compression * section.area + if reference_force <= EPS: + return None + return euler_force / reference_force + + +def _plate_strip_shear_buckling( + panel: S3PanelInput | U3PanelInput, + length: float, + width: float, + thickness: float, + shear_stress: float, + capacity_factor: float = 1.0, +) -> dict[str, float] | None: + """Return the elastic shear buckling factor for a plate strip. + + Notes + ----- + This uses the classical simply-supported plate shear buckling coefficient + + k_tau = 5.34 + 4 / alpha^2 + + where alpha = long_side / short_side >= 1. + + The classical elastic shear buckling stress is calculated directly as + + tau_cr = k_tau * pi^2 * E / (12 * (1 - nu^2)) * (t / short_side)^2 + + The returned critical stress is then multiplied by the explicit + `capacity_factor`. S3 uses this as a visible reduced shear-interaction + control; U3 keeps the default factor of 1.0. + """ + + shear = abs(shear_stress) + if shear <= EPS: + return None + + short_side = min(length, width) + long_side = max(length, width) + alpha = long_side / max(short_side, EPS) + + shear_coefficient = 5.34 + 4.0 / max(alpha * alpha, EPS) + + elastic_reference = ( + math.pi**2 + * panel.elastic_modulus + / (12.0 * (1.0 - panel.poisson_ratio**2)) + * (thickness / max(short_side, EPS)) ** 2 + ) + + applied_capacity_factor = max(float(capacity_factor), EPS) + classical_critical_stress = shear_coefficient * elastic_reference + critical_stress = applied_capacity_factor * classical_critical_stress + + return { + "factor": critical_stress / shear, + "critical_stress": critical_stress, + "classical_critical_stress": classical_critical_stress, + "coefficient": shear_coefficient, + "capacity_factor": applied_capacity_factor, + "aspect_ratio": alpha, + "short_side": short_side, + "long_side": long_side, + } + + +def _local_plate_shear_buckling( + panel: S3PanelInput, + config: S3SolverConfig, +) -> dict[str, float] | None: + """Return the classical local plate shear factor for the unit bay. + + Same-mode diagonal sine terms are not a sound representation of plate shear + buckling. The reduced solver therefore reports a separate simply + supported plate shear candidate while the nonlinear path keeps shear in + the yield stress state. + """ + + return _plate_strip_shear_buckling( + panel, + panel.length, + panel.width, + panel.plate_thickness, + panel.shear_stress, + config.s3_shear_buckling_capacity_factor, + ) + + +def _web_ritz_modes( + panel: S3PanelInput, + config: S3SolverConfig, + compression_stress: float, +) -> list[RitzMode]: + """Return a bounded isotropic Ritz surface for open stiffener webs.""" + + bending_rigidity = panel.elastic_modulus * panel.web_thickness**3 / ( + 12.0 * (1.0 - panel.poisson_ratio**2) + ) + compression_resultant = max(compression_stress, 0.0) * panel.web_thickness + area_factor = panel.length * panel.stiffener_height / 4.0 + modes: list[RitzMode] = [] + for m in config.web_longitudinal_modes: + for n in config.web_depth_modes: + kx = m * math.pi / panel.length + ky = n * math.pi / panel.stiffener_height + wave_norm = kx * kx + ky * ky + modes.append( + RitzMode( + family="web", + m=m, + n=n, + kx=kx, + ky=ky, + linear_stiffness=area_factor * bending_rigidity * wave_norm * wave_norm, + geometric_stiffness=area_factor * compression_resultant * kx * kx, + pressure_force=0.0, + imperfection=0.0, + nonlinear_stiffness=EPS, + ) + ) + return modes + + +def _ritz_factor_summary( + linear: np.ndarray, + geometric: np.ndarray, + modes: Sequence[RitzMode], + metadata: Mapping[str, Any], +) -> dict[str, Any] | None: + """Return the positive critical factor for a Ritz geometric matrix.""" + + linear_diagonal = np.diag(linear) + inverse_sqrt_linear = np.diag(1.0 / np.sqrt(np.maximum(linear_diagonal, EPS))) + transformed = inverse_sqrt_linear @ geometric @ inverse_sqrt_linear + eigenvalues, eigenvectors = np.linalg.eigh(transformed) + positive_indices = [ + index + for index, eigenvalue in enumerate(eigenvalues) + if eigenvalue > EPS and math.isfinite(float(eigenvalue)) + ] + if not positive_indices: + return None + + critical_index = max(positive_indices, key=lambda index: float(eigenvalues[index])) + eigenvalue = float(eigenvalues[critical_index]) + physical_vector = inverse_sqrt_linear @ eigenvectors[:, critical_index] + modal_weights = np.abs(physical_vector) + weight_sum = float(np.sum(modal_weights)) + composition = [] + if weight_sum > EPS: + composition = [ + { + "mode": mode.label, + "weight": float(weight / weight_sum), + } + for mode, weight in sorted( + zip(modes, modal_weights), + key=lambda item: float(item[1]), + reverse=True, + ) + if weight > EPS + ] + return { + "factor": 1.0 / eigenvalue, + "eigenvalue": eigenvalue, + "mode_composition": composition[:4], + **metadata, + } + + +def _web_ritz_buckling( + panel: S3PanelInput, + config: S3SolverConfig, + compression_demand: Mapping[str, Any], +) -> dict[str, Any] | None: + """Return a web-surface Ritz factor for axial web compression and shear.""" + + modes = _web_ritz_modes(panel, config, float(compression_demand["stress"])) + if not modes: + return None + + linear = np.diag([mode.linear_stiffness for mode in modes]) + geometric = np.diag([mode.geometric_stiffness for mode in modes]) + signed_shear_resultant = panel.shear_stress * panel.web_thickness + coupled_terms = 0 + if abs(signed_shear_resultant) > EPS: + for row, first in enumerate(modes): + for column in range(row + 1, len(modes)): + second = modes[column] + coupling = ( + signed_shear_resultant + * _rectangular_mode_shear_geometric_integral( + panel.length, + panel.stiffener_height, + first, + second, + ) + ) + if abs(coupling) <= EPS: + continue + geometric[row, column] += coupling + geometric[column, row] += coupling + coupled_terms += 1 + + return _ritz_factor_summary( + linear, + geometric, + modes, + { + "family": "web", + "signed_shear_resultant": signed_shear_resultant, + "coupled_terms": coupled_terms, + "mode_count": len(modes), + "longitudinal_modes": list(config.web_longitudinal_modes), + "depth_modes": list(config.web_depth_modes), + "surface": { + "length": panel.length, + "depth": panel.stiffener_height, + "thickness": panel.web_thickness, + }, + "compression_demand": compression_demand, + }, + ) + + +def _stiffener_web_reference_compression( + panel: S3PanelInput, + gross_section: S3SectionProperties, + stiffener_section: S3SectionProperties, + config: S3SolverConfig, +) -> dict[str, Any]: + """Return reference web-edge compression for the local web check. + + The panel axial stress is the base proportional compression driver. The + SI/PI yield branches keep the public sniped-stiffener axial eccentricity + moment. The reduced web branch carries that contribution as an explicit + sensitivity factor so the assumption remains visible in diagnostics and + can be disabled for comparison without changing the SI/PI yield path. + """ + + effective_axial = _stiffener_effective_axial_stress( + panel, + gross_section, + stiffener_section, + 1.0, + ) + axial_compression = max(float(effective_axial["stress"]), 0.0) + sniped = _sniped_stiffener_eccentricity_moments( + panel, + gross_section, + stiffener_section, + 1.0, + config, + ) + web_coordinates = { + "root": 0.5 * panel.plate_thickness, + "tip": 0.5 * panel.plate_thickness + panel.stiffener_height, + } + raw_absolute_moment = abs(float(sniped["absolute"])) + web_local_sniped_factor = max(float(config.web_local_sniped_eccentricity_factor), 0.0) + absolute_moment = raw_absolute_moment * web_local_sniped_factor + edge_bending = { + edge: absolute_moment + * abs(coordinate - stiffener_section.centroid_from_plate_midplane) + / max(stiffener_section.inertia_x, EPS) + for edge, coordinate in web_coordinates.items() + } + edge_compression = { + edge: axial_compression + bending + for edge, bending in edge_bending.items() + } + controlling_edge = max(edge_compression, key=edge_compression.get) + return { + "stress": edge_compression[controlling_edge], + "source": "stiffener-section-axial-plus-configured-sniped-web-edge-envelope", + "controlling_edge": controlling_edge, + "edge_compression": edge_compression, + "edge_bending_stress": edge_bending, + "effective_axial_stress": effective_axial, + "sniped_eccentricity_moment": sniped, + "web_local_sniped_eccentricity_factor": web_local_sniped_factor, + "applied_sniped_eccentricity_moment": absolute_moment, + } + + +def _stiffener_web_local_buckling( + panel: S3PanelInput, + gross_section: S3SectionProperties, + stiffener_section: S3SectionProperties, + config: S3SolverConfig, +) -> dict[str, Any] | None: + """Return an open-profile web compression-shear candidate. + + The web is treated as a long simply supported plate strip under a reference + web-edge compression envelope and panel shear. The candidate keeps the + full PULS web/stiffener stress redistribution out of scope, but the explicit + interaction avoids treating tall loaded webs as compression-only strips. + """ + + compression_demand = _stiffener_web_reference_compression( + panel, + gross_section, + stiffener_section, + config, + ) + compression = float(compression_demand["stress"]) + compression_factor = None + compression_critical_stress = None + compression_coefficient = 4.0 + if compression > EPS: + elastic_reference = ( + math.pi**2 + * panel.elastic_modulus + / (12.0 * (1.0 - panel.poisson_ratio**2)) + * (panel.web_thickness / panel.stiffener_height) ** 2 + ) + compression_critical_stress = compression_coefficient * elastic_reference + compression_factor = compression_critical_stress / compression + + shear = _plate_strip_shear_buckling( + panel, + panel.length, + panel.stiffener_height, + panel.web_thickness, + panel.shear_stress, + config.s3_shear_buckling_capacity_factor, + ) + web_ritz = _web_ritz_buckling(panel, config, compression_demand) + factor_rows = [ + factor + for factor in ( + compression_factor, + None if shear is None else shear["factor"], + ) + if factor is not None and factor > EPS and math.isfinite(factor) + ] + if not factor_rows: + return None + + exponent = max(config.web_shear_interaction_exponent, EPS) + interaction_usage = sum((1.0 / factor) ** exponent for factor in factor_rows) ** (1.0 / exponent) + approximation_notes = [ + "web modeled as an isolated simply supported compression-shear strip", + "S3 web buckling load shedding with plate, flange, and torsional displacement fields is not reproduced", + ] + return { + "factor": 1.0 / max(interaction_usage, EPS), + "critical_stress": compression_critical_stress, + "coefficient": compression_coefficient, + "compression_demand": compression_demand, + "compression_factor": compression_factor, + "shear_factor": None if shear is None else shear["factor"], + "shear_critical_stress": None if shear is None else shear["critical_stress"], + "shear_coefficient": None if shear is None else shear["coefficient"], + "interaction_exponent": exponent, + "factor_source": "strip-compression-shear-interaction", + "web_ritz": web_ritz, + "coverage": "reduced-strip-approximation", + "approximation_notes": approximation_notes, + } + + +def _local_plate_web_interaction( + plate_shear: Mapping[str, Any] | None, + web_local: Mapping[str, Any] | None, + config: S3SolverConfig, +) -> dict[str, Any] | None: + """Return a reduced mixed local interaction for plate and web response. + + The public S3 description treats panel failure as a mixed local stiffened + response with load shedding between plating and primary stiffeners. The + first reduced solver still has separate plate-shear and web-local + candidates, so this explicit interaction candidate keeps those concurrent + local shear usages visible while the full coupled displacement family is + not yet implemented. + """ + + if plate_shear is None or web_local is None: + return None + plate_factor = _optional_float(plate_shear.get("factor")) + web_factor = _optional_float(web_local.get("factor")) + if ( + plate_factor is None + or web_factor is None + or plate_factor <= EPS + or web_factor <= EPS + ): + return None + + exponent = max(config.local_plate_web_interaction_exponent, EPS) + plate_usage = 1.0 / plate_factor + web_usage = 1.0 / web_factor + interaction_usage = ( + plate_usage**exponent + web_usage**exponent + ) ** (1.0 / exponent) + return { + "factor": 1.0 / max(interaction_usage, EPS), + "interaction_usage": interaction_usage, + "interaction_exponent": exponent, + "plate_shear_factor": plate_factor, + "plate_shear_usage": plate_usage, + "web_local_factor": web_factor, + "web_local_usage": web_usage, + "factor_source": "plate-shear-web-local-usage-interaction", + "coverage": "reduced-local-interaction", + "approximation_notes": [ + "mixed local interaction keeps separate plate-shear and web-local reduced candidates", + "full S3 plate, web, flange, and torsional local displacement coupling is not reproduced", + ], + } + + +def _stiffener_torsional_buckling( + panel: S3PanelInput, + config: S3SolverConfig | None = None, +) -> dict[str, float] | None: + """Return a reduced open-profile tripping/torsional stress candidate. + + DNV-CG-0128 uses a torsional reference stress based on St. Venant torsion, + polar inertia, sectorial inertia, the stiffener span, and attachment + restraint. The reduced S3 solver uses the same ingredients in a + gross-section estimate about the web root with an explicit restraint + factor. It is exposed as an approximate candidate in diagnostics. + """ + + config = config or S3SolverConfig() + compression = max(panel.axial_stress, 0.0) + if compression <= EPS: + return None + + web_height = panel.stiffener_height + web_thickness = panel.web_thickness + flange_width = 0.0 if panel.stiffener_type == "Flatbar" else max(panel.flange_width, 0.0) + flange_thickness = 0.0 if panel.stiffener_type == "Flatbar" else max(panel.flange_thickness, 0.0) + web_area = web_height * web_thickness + flange_area = flange_width * flange_thickness + shear_modulus = panel.elastic_modulus / (2.0 * (1.0 + panel.poisson_ratio)) + + torsion_constant = web_height * web_thickness**3 / 3.0 + if flange_area > 0.0: + torsion_constant += flange_width * flange_thickness**3 / 3.0 + + polar_inertia = web_area * web_height**2 / 3.0 + flange_offset = web_height + 0.5 * flange_thickness + if flange_area > 0.0: + polar_inertia += flange_area * (flange_offset**2 + flange_width**2 / 12.0) + polar_inertia = max(polar_inertia, EPS) + + sectorial_inertia = 0.0 + if flange_area > 0.0: + sectorial_inertia = flange_area * flange_width**2 * flange_offset**2 / 12.0 + + effective_length = panel.length * (0.70 if panel.stiffener_boundary == "Cont" else 1.0) + wave_number = math.pi / max(effective_length, EPS) + critical_stress = config.torsional_restraint_factor * ( + shear_modulus * torsion_constant + + panel.elastic_modulus * sectorial_inertia * wave_number**2 + ) / polar_inertia + return { + "factor": critical_stress / compression, + "critical_stress": critical_stress, + "torsion_constant": torsion_constant, + "polar_inertia": polar_inertia, + "sectorial_inertia": sectorial_inertia, + "effective_length": effective_length, + "half_waves": 1.0, + "restraint_factor": config.torsional_restraint_factor, + } + + +def _global_stiffened_strip_capacity_adjustment( + panel: S3PanelInput, + section: S3SectionProperties, + raw_factor: float, + local_reference_factor: float | None, + config: S3SolverConfig, +) -> dict[str, Any]: + """Return the reduced global-strip capacity adjustment and diagnostics.""" + + plate_reference_inertia = panel.width * panel.plate_thickness**3 / 12.0 + section_inertia_ratio = section.inertia_x / max(plate_reference_inertia, EPS) + section_area_ratio = section.area / max(panel.width * panel.plate_thickness, EPS) + aspect_ratio = panel.length / max(panel.width, EPS) + plate_slenderness = panel.width / max(panel.plate_thickness, EPS) + local_interaction_ratio = ( + raw_factor / max(local_reference_factor, EPS) + if local_reference_factor is not None and local_reference_factor > EPS + else None + ) + load_family = _load_family(panel) + + if config.global_stiffened_strip_capacity_factor is not None: + fixed_factor = max(float(config.global_stiffened_strip_capacity_factor), EPS) + return { + "raw_factor": raw_factor, + "local_reference_factor": local_reference_factor, + "local_interaction_ratio": local_interaction_ratio, + "capacity_factor": fixed_factor, + "mode": "fixed-override", + "section": { + "inertia_ratio": section_inertia_ratio, + "area_ratio": section_area_ratio, + "plate_slenderness": plate_slenderness, + "aspect_ratio": aspect_ratio, + }, + "modifiers": { + "base": fixed_factor, + "support": 1.0, + "pressure": 1.0, + "load": 1.0, + "aspect": 1.0, + "slenderness": 1.0, + "section": 1.0, + "local_interaction": 1.0, + }, + "notes": ["fixed global-stiffened-strip capacity override"], + } + + notes = ["computed reduced global-stiffened-strip capacity adjustment"] + base = 0.55 + support_modifier = { + "Integrated": 0.88, + "Girder - long": 0.95, + "Girder - trans": 0.85, + }.get(panel.in_plane_support, 1.0) + pressure_modifier = 0.92 if panel.pressure > EPS else 1.0 + + load_modifier = 1.0 + if "shear" in load_family: + load_modifier *= 0.95 + if "axial-tension" in load_family and "transverse-compression" in load_family: + load_modifier *= 0.95 + if "pressure" in load_family and "shear" not in load_family: + load_modifier *= 0.95 + + if aspect_ratio >= 6.0: + aspect_modifier = 0.90 + elif aspect_ratio >= 3.0: + aspect_modifier = 0.96 + else: + aspect_modifier = 1.05 + + if plate_slenderness >= 80.0: + slenderness_modifier = 1.15 + notes.append("very slender plate: global-strip reduction relaxed") + elif plate_slenderness >= 50.0: + slenderness_modifier = 1.00 + else: + slenderness_modifier = 0.95 + + if section_inertia_ratio >= 1.0e5: + section_modifier = 0.95 + elif section_inertia_ratio >= 1.0e4: + section_modifier = 0.98 + else: + section_modifier = 0.92 + + if local_interaction_ratio is None: + local_modifier = 1.0 + elif local_interaction_ratio >= 4.0: + local_modifier = 0.90 + elif local_interaction_ratio >= 2.0: + local_modifier = 0.95 + else: + local_modifier = 1.0 + + capacity_factor = ( + base + * support_modifier + * pressure_modifier + * load_modifier + * aspect_modifier + * slenderness_modifier + * section_modifier + * local_modifier + ) + capacity_factor = min(0.95, max(0.30, capacity_factor)) + return { + "raw_factor": raw_factor, + "local_reference_factor": local_reference_factor, + "local_interaction_ratio": local_interaction_ratio, + "capacity_factor": capacity_factor, + "mode": "computed", + "section": { + "inertia_ratio": section_inertia_ratio, + "area_ratio": section_area_ratio, + "plate_slenderness": plate_slenderness, + "aspect_ratio": aspect_ratio, + }, + "modifiers": { + "base": base, + "support": support_modifier, + "pressure": pressure_modifier, + "load": load_modifier, + "aspect": aspect_modifier, + "slenderness": slenderness_modifier, + "section": section_modifier, + "local_interaction": local_modifier, + }, + "notes": notes, + } + + +def elastic_buckling_factors( + panel: S3PanelInput, + section: S3SectionProperties, + modes: Sequence[RitzMode], + config: S3SolverConfig | None = None, + stiffener_section: S3SectionProperties | None = None, +) -> dict[str, Any]: + config = config or S3SolverConfig() + stiffener_section = stiffener_section or section + factor_rows = [] + for mode in modes: + if mode.geometric_stiffness <= EPS: + continue + raw_factor = mode.linear_stiffness / mode.geometric_stiffness + if mode.family == "global": + capacity_factor = 1.0 + factor = raw_factor + failure_family = "global-stiffened-strip" + else: + capacity_factor = 1.0 + factor = raw_factor + failure_family = "plate" + factor_rows.append( + { + "factor": factor, + "raw_factor": raw_factor, + "capacity_factor": capacity_factor, + "label": mode.label, + "family": mode.family, + "failure_family": failure_family, + } + ) + column_factor = _stiffener_column_factor(panel, stiffener_section) + if column_factor is not None: + factor_rows.append( + { + "factor": column_factor, + "label": "stiffener-column", + "family": "stiffener-column", + "failure_family": "global-stiffener-cutoff", + } + ) + shear_factor = _local_plate_shear_buckling(panel, config) + if shear_factor is not None: + factor_rows.append( + { + "factor": shear_factor["factor"], + "label": "local-plate-shear", + "family": "local-shear", + "failure_family": "plate-shear", + } + ) + coupled_shear_factors = { + family: ritz_combined_buckling_factor(panel, modes, family) + for family in ("local", "global") + } + for family, factor in coupled_shear_factors.items(): + if factor is None: + continue + factor_rows.append( + { + "factor": factor["factor"], + "raw_factor": factor["factor"], + "capacity_factor": 1.0, + "label": f"{family}-ritz-combined-shear", + "family": family, + "failure_family": "plate-shear" if family == "local" else "global-stiffened-strip", + } + ) + web_factor = _stiffener_web_local_buckling(panel, section, stiffener_section, config) + if web_factor is not None: + factor_rows.append( + { + "factor": web_factor["factor"], + "label": "stiffener-web-local", + "family": "stiffener-web", + "failure_family": "web-local", + } + ) + local_interaction = _local_plate_web_interaction(shear_factor, web_factor, config) + if local_interaction is not None: + factor_rows.append( + { + "factor": local_interaction["factor"], + "label": "local-plate-web-interaction", + "family": "local-interaction", + "failure_family": "plate-web-local-interaction", + } + ) + torsional_factor = _stiffener_torsional_buckling(panel, config) + if torsional_factor is not None: + factor_rows.append( + { + "factor": torsional_factor["factor"], + "label": "stiffener-torsional", + "family": "stiffener-torsional", + "failure_family": "torsional-stiffener", + } + ) + factor_rows = [ + row + for row in factor_rows + if row["factor"] > EPS and math.isfinite(row["factor"]) + ] + local_plate_rows = [ + row + for row in factor_rows + if row["failure_family"] in {"plate", "plate-shear"} + ] + local_reference_factor = ( + min(float(row["factor"]) for row in local_plate_rows) + if local_plate_rows + else None + ) + elastic_global_coupling_rows: list[dict[str, float | str]] = [] + if local_reference_factor is not None: + for row in factor_rows: + if row["failure_family"] != "global-stiffened-strip": + continue + raw_factor = float(row.get("raw_factor", row["factor"])) + adjustment = _global_stiffened_strip_capacity_adjustment( + panel, + section, + raw_factor, + local_reference_factor, + config, + ) + capacity_factor = float(adjustment["capacity_factor"]) + uncoupled_factor = raw_factor * capacity_factor + interaction_driver = uncoupled_factor / max(local_reference_factor, EPS) + scale = 1.0 / ( + 1.0 + + config.local_global_coupling_gain * max(interaction_driver - 1.0, 0.0) + ) + scale = min(1.0, max(config.local_global_coupling_floor, scale)) + row["raw_factor"] = raw_factor + row["capacity_factor"] = capacity_factor + row["global_capacity_adjustment"] = adjustment + row["uncoupled_factor"] = uncoupled_factor + row["elastic_coupling_scale"] = scale + row["factor"] = uncoupled_factor * scale + elastic_global_coupling_rows.append( + { + "mode": str(row["label"]), + "raw_factor": raw_factor, + "capacity_factor": capacity_factor, + "capacity_adjustment": adjustment, + "uncoupled_factor": uncoupled_factor, + "coupled_factor": float(row["factor"]), + "scale": scale, + "interaction_driver": interaction_driver, + } + ) + if not factor_rows: + return { + "critical_factor": None, + "critical_mode": "no-compressive-or-shear-buckling-driver", + "critical_failure_family": "none", + "stiffener_column_factor": column_factor, + "local_plate_shear": shear_factor, + "ritz_combined_shear": coupled_shear_factors, + "stiffener_web_local": web_factor, + "local_plate_web_interaction": local_interaction, + "stiffener_torsional": torsional_factor, + "elastic_global_coupling": { + "local_reference_factor": local_reference_factor, + "global_modes": elastic_global_coupling_rows, + }, + "modeled_failure_families": {}, + "approximate_failure_families": [ + "torsional-stiffener", + "web-local", + "plate-web-local-interaction", + ], + "unmodeled_failure_families": ["stiffener-local-global-coupling"], + } + + family_minima: dict[str, dict[str, Any]] = {} + for row in factor_rows: + failure_family = str(row["failure_family"]) + current = family_minima.get(failure_family) + if current is None or row["factor"] < current["factor"]: + family_minima[failure_family] = dict(row) + + family_usages = { + name: 1.0 / max(float(row["factor"]), EPS) + for name, row in family_minima.items() + } + usage_total = sum(family_usages.values()) + modeled_failure_families = {} + for name, row in sorted(family_minima.items()): + family_summary = { + "critical_factor": row["factor"], + "critical_mode": row["label"], + "usage_share_percent": 100.0 * family_usages[name] / max(usage_total, EPS), + } + if "uncoupled_factor" in row: + family_summary["uncoupled_factor"] = row["uncoupled_factor"] + family_summary["elastic_coupling_scale"] = row["elastic_coupling_scale"] + if "global_capacity_adjustment" in row: + family_summary["raw_factor"] = row["raw_factor"] + family_summary["capacity_factor"] = row["capacity_factor"] + family_summary["global_capacity_adjustment"] = row["global_capacity_adjustment"] + modeled_failure_families[name] = family_summary + critical = min(factor_rows, key=lambda item: item["factor"]) + return { + "critical_factor": critical["factor"], + "critical_mode": critical["label"], + "critical_failure_family": critical["failure_family"], + "stiffener_column_factor": column_factor, + "local_plate_shear": shear_factor, + "ritz_combined_shear": coupled_shear_factors, + "stiffener_web_local": web_factor, + "local_plate_web_interaction": local_interaction, + "stiffener_torsional": torsional_factor, + "elastic_global_coupling": { + "local_reference_factor": local_reference_factor, + "global_modes": elastic_global_coupling_rows, + }, + "modeled_failure_families": modeled_failure_families, + "approximate_failure_families": [ + "torsional-stiffener", + "web-local", + "plate-web-local-interaction", + ], + "unmodeled_failure_families": ["stiffener-local-global-coupling"], + } + + +def elastic_u3_buckling_factors( + panel: U3PanelInput, + modes: Sequence[RitzMode], + config: S3SolverConfig | None = None, +) -> dict[str, Any]: + """Return elastic buckling candidates for the U3 unstiffened plate.""" + + config = config or S3SolverConfig() + factor_rows = [ + { + "factor": mode.linear_stiffness / mode.geometric_stiffness, + "label": mode.label, + "family": mode.family, + "failure_family": "plate", + } + for mode in modes + if mode.geometric_stiffness > EPS + ] + shear_factor = _plate_strip_shear_buckling( + panel, + panel.length, + panel.width, + panel.plate_thickness, + panel.shear_stress, + ) + if shear_factor is not None: + factor_rows.append( + { + "factor": shear_factor["factor"], + "label": "plate-shear", + "family": "plate-shear", + "failure_family": "plate-shear", + } + ) + coupled_shear = {"plate": ritz_combined_buckling_factor(panel, modes, "plate")} + if coupled_shear["plate"] is not None: + factor_rows.append( + { + "factor": coupled_shear["plate"]["factor"], + "label": "plate-ritz-combined-shear", + "family": "plate", + "failure_family": "plate-shear", + } + ) + factor_rows = [ + row + for row in factor_rows + if row["factor"] > EPS and math.isfinite(row["factor"]) + ] + if not factor_rows: + return { + "critical_factor": None, + "critical_mode": "no-compressive-or-shear-buckling-driver", + "critical_failure_family": "none", + "local_plate_shear": shear_factor, + "ritz_combined_shear": coupled_shear, + "modeled_failure_families": {}, + "approximate_failure_families": [], + "unmodeled_failure_families": ["production-U3-elasto-plastic-postbuckling-calibration"], + } + + family_minima: dict[str, dict[str, Any]] = {} + for row in factor_rows: + failure_family = str(row["failure_family"]) + current = family_minima.get(failure_family) + if current is None or row["factor"] < current["factor"]: + family_minima[failure_family] = dict(row) + + family_usages = { + name: 1.0 / max(float(row["factor"]), EPS) + for name, row in family_minima.items() + } + usage_total = sum(family_usages.values()) + modeled_failure_families = { + name: { + "critical_factor": row["factor"], + "critical_mode": row["label"], + "usage_share_percent": 100.0 * family_usages[name] / max(usage_total, EPS), + } + for name, row in sorted(family_minima.items()) + } + critical = min(factor_rows, key=lambda item: item["factor"]) + return { + "critical_factor": critical["factor"], + "critical_mode": critical["label"], + "critical_failure_family": critical["failure_family"], + "local_plate_shear": shear_factor, + "ritz_combined_shear": coupled_shear, + "modeled_failure_families": modeled_failure_families, + "approximate_failure_families": [], + "unmodeled_failure_families": ["production-U3-elasto-plastic-postbuckling-calibration"], + } + + +def _limit_newton_delta( + modes: Sequence[RitzMode], + amplitudes: np.ndarray, + delta: np.ndarray, +) -> np.ndarray: + limited = delta.copy() + for index, mode in enumerate(modes): + max_delta = max( + 1.0, + 0.5 * abs(float(amplitudes[index])), + 0.10 / max(mode.kx, mode.ky, EPS), + ) + if abs(float(limited[index])) > max_delta: + limited[index] = math.copysign(max_delta, float(limited[index])) + return limited + + +def solve_equilibrium_amplitudes( + panel: S3PanelInput, + modes: Sequence[RitzMode], + load_factor: float, + previous_amplitudes: Sequence[float], + config: S3SolverConfig, +) -> tuple[list[float], bool, int]: + """Solve the coupled reduced Ritz continuation equilibrium. + + Normal resultants keep diagonal geometric terms in this basis. Panel shear + contributes off-diagonal geometric coupling between opposite-parity modes, + so the load-path residual and Newton tangent are assembled as vectors and + matrices instead of solving each amplitude independently. + """ + + if not modes: + return [], True, 0 + + q = np.asarray(previous_amplitudes, dtype=float) + if q.shape != (len(modes),): + q = np.zeros(len(modes), dtype=float) + + linear = np.diag([mode.linear_stiffness for mode in modes]) + geometric, _ = _ritz_geometric_matrix(panel, modes) + nonlinear = np.asarray([mode.nonlinear_stiffness for mode in modes], dtype=float) + pressure = np.asarray([mode.pressure_force for mode in modes], dtype=float) + imperfection = np.asarray([mode.imperfection for mode in modes], dtype=float) + force = pressure + load_factor * (geometric @ imperfection) + tangent_linear = linear - load_factor * geometric + if np.max(np.abs(force)) <= EPS and np.max(np.abs(q)) <= EPS: + return [0.0 for _ in modes], True, 0 + + for iteration in range(1, config.newton_max_iterations + 1): + nonlinear_response = nonlinear * q**3 + residual = tangent_linear @ q + nonlinear_response - force + scale = max( + float(np.max(np.abs(force))), + float(np.max(np.abs(tangent_linear @ q))), + float(np.max(np.abs(nonlinear_response))), + 1.0, + ) + if float(np.max(np.abs(residual))) <= config.newton_tolerance * scale: + return q.tolist(), True, iteration + + tangent = tangent_linear + np.diag(3.0 * nonlinear * q * q) + try: + delta = np.linalg.solve(tangent, -residual) + except np.linalg.LinAlgError: + delta = np.linalg.lstsq(tangent, -residual, rcond=None)[0] + delta = _limit_newton_delta(modes, q, delta) + q += delta + if not np.all(np.isfinite(q)): + return list(previous_amplitudes), False, iteration + + return q.tolist(), False, config.newton_max_iterations + + +def _mode_amplitude_summary( + modes: Sequence[RitzMode], + amplitudes: Sequence[float], +) -> dict[str, Any]: + family_maxima: dict[str, dict[str, float | str]] = {} + max_amplitude = 0.0 + for mode, amplitude in zip(modes, amplitudes): + magnitude = abs(amplitude) + max_amplitude = max(max_amplitude, magnitude) + current = family_maxima.get(mode.family) + if current is None or magnitude > float(current["amplitude"]): + family_maxima[mode.family] = { + "mode": mode.label, + "amplitude": magnitude, + "signed_amplitude": amplitude, + } + return { + "max_amplitude": max_amplitude, + "families": family_maxima, + } + + +def _stress_von_mises(sigma_x: float, sigma_y: float, tau_xy: float) -> float: + return math.sqrt(max(sigma_x**2 - sigma_x * sigma_y + sigma_y**2 + 3.0 * tau_xy**2, 0.0)) + + +def _mode_curvatures( + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + x: float, + y: float, + family: str | None = None, +) -> tuple[float, float, float]: + d2x = 0.0 + d2y = 0.0 + dxy = 0.0 + for mode, amplitude in zip(modes, amplitudes): + if family is not None and mode.family != family: + continue + sin_x = math.sin(mode.kx * x) + sin_y = math.sin(mode.ky * y) + cos_x = math.cos(mode.kx * x) + cos_y = math.cos(mode.ky * y) + d2x -= amplitude * mode.kx * mode.kx * sin_x * sin_y + d2y -= amplitude * mode.ky * mode.ky * sin_x * sin_y + dxy += amplitude * mode.kx * mode.ky * cos_x * cos_y + return d2x, d2y, dxy + + +def _pressure_stiffener_bending_moment(panel: S3PanelInput) -> float: + if panel.pressure <= 0.0: + return 0.0 + span_factor = 12.0 if panel.stiffener_boundary == "Cont" else 8.0 + return panel.pressure * panel.width * panel.length**2 / span_factor + + +def _sniped_stiffener_eccentricity_moments( + panel: S3PanelInput, + gross_section: S3SectionProperties, + stiffener_section: S3SectionProperties, + load_factor: float, + config: S3SolverConfig, +) -> dict[str, float]: + """Return branch-signed axial eccentricity moments for sniped stiffeners.""" + + compression = max(load_factor * panel.axial_stress, 0.0) + if panel.stiffener_boundary != "Sniped" or compression <= EPS: + return { + "absolute": 0.0, + "stiffener_induced": 0.0, + "plate_induced": 0.0, + } + moment = ( + config.sniped_eccentricity_factor + * abs(stiffener_section.centroid_from_plate_midplane) + * compression + * gross_section.area + ) + return { + "absolute": moment, + "stiffener_induced": -moment, + "plate_induced": moment, + } + + +def _stiffener_torsional_edge_distance( + panel: S3PanelInput, + stiffener_section: S3SectionProperties, +) -> float: + """Return a reduced free-edge distance for SI torsional deformation.""" + + if panel.stiffener_type == "Flatbar": + return 0.5 * panel.web_thickness + if panel.stiffener_type == "T-bar": + return 0.5 * panel.flange_width + + stiffener_area = max(stiffener_section.stiffener_area, EPS) + edge_distance = panel.flange_width - ( + panel.stiffener_height * panel.web_thickness**2 + + panel.flange_thickness * panel.flange_width**2 + ) / ( + 2.0 * stiffener_area + ) + return max(edge_distance, EPS) + + +def _stiffener_torsional_deformation_stress( + panel: S3PanelInput, + stiffener_section: S3SectionProperties, + effective_axial_stress: float, + config: S3SolverConfig, +) -> dict[str, float]: + """Return the SI-only torsional deformation stress used in stiffener yield. + + Public stiffener interaction rules add a stress amplification term for + stiffener-induced failure when the axial stress approaches the torsional + reference stress. This reduced pass uses the existing gross-section + torsional reference stress and one half-wave along the supported span. + """ + + axial_compression = max(effective_axial_stress, 0.0) + torsional = _stiffener_torsional_buckling(panel, config) + if axial_compression <= EPS or torsional is None: + return { + "stress": 0.0, + "edge_distance": 0.0, + "imperfection_rotation": 0.0, + "reference_stress": 0.0, + "stress_ratio": 0.0, + } + + reference_stress = torsional["critical_stress"] + stress_ratio = min(axial_compression / max(reference_stress, EPS), 1.0 - 1.0e-6) + edge_distance = _stiffener_torsional_edge_distance(panel, stiffener_section) + half_waves = torsional["half_waves"] + effective_length = torsional["effective_length"] + imperfection_rotation = ( + config.torsional_imperfection_scale + * effective_length + / max(half_waves * panel.stiffener_height, EPS) + * 1.0e-4 + ) + wave_number = half_waves * math.pi / max(effective_length, EPS) + stress = ( + panel.elastic_modulus + * edge_distance + * imperfection_rotation + * wave_number**2 + * (1.0 / max(1.0 - stress_ratio, EPS) - 1.0) + ) + return { + "stress": stress, + "edge_distance": edge_distance, + "imperfection_rotation": imperfection_rotation, + "reference_stress": reference_stress, + "stress_ratio": stress_ratio, + } + + +def _plate_yield_ratio( + panel: S3PanelInput, + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + load_factor: float, + config: S3SolverConfig, +) -> float: + modulus = panel.elastic_modulus / (1.0 - panel.poisson_ratio**2) + shear_modulus = panel.elastic_modulus / (2.0 * (1.0 + panel.poisson_ratio)) + z_values = (-0.5 * panel.plate_thickness, 0.5 * panel.plate_thickness) + max_ratio = 0.0 + curvature_family = None if config.include_global_curvature_in_plate_yield else "local" + + for x_fraction in config.hot_spot_grid: + x = panel.length * x_fraction + for y_fraction in config.hot_spot_grid: + y = panel.width * y_fraction + d2x, d2y, dxy = _mode_curvatures( + modes, + amplitudes, + x, + y, + family=curvature_family, + ) + transverse_stress = ( + panel.transverse_stress_1 + + (panel.transverse_stress_2 - panel.transverse_stress_1) * y_fraction + ) + for z in z_values: + bending_x = -modulus * z * (d2x + panel.poisson_ratio * d2y) + bending_y = -modulus * z * (d2y + panel.poisson_ratio * d2x) + bending_tau = -2.0 * shear_modulus * z * dxy + vm = _stress_von_mises( + load_factor * panel.axial_stress + bending_x, + load_factor * transverse_stress + bending_y, + load_factor * panel.shear_stress + bending_tau, + ) + max_ratio = max(max_ratio, vm / panel.yield_stress_plate) + return max_ratio + + +def _u3_plate_yield_ratio( + panel: U3PanelInput, + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + load_factor: float, + config: S3SolverConfig, +) -> float: + modulus = panel.elastic_modulus / (1.0 - panel.poisson_ratio**2) + shear_modulus = panel.elastic_modulus / (2.0 * (1.0 + panel.poisson_ratio)) + z_values = (-0.5 * panel.plate_thickness, 0.5 * panel.plate_thickness) + max_ratio = 0.0 + + for x_fraction in config.hot_spot_grid: + x = panel.length * x_fraction + axial_stress = ( + panel.axial_stress_1 + + (panel.axial_stress_2 - panel.axial_stress_1) * x_fraction + ) + for y_fraction in config.hot_spot_grid: + y = panel.width * y_fraction + d2x, d2y, dxy = _mode_curvatures(modes, amplitudes, x, y) + transverse_stress = ( + panel.transverse_stress_1 + + (panel.transverse_stress_2 - panel.transverse_stress_1) * y_fraction + ) + for z in z_values: + bending_x = -modulus * z * (d2x + panel.poisson_ratio * d2y) + bending_y = -modulus * z * (d2y + panel.poisson_ratio * d2x) + bending_tau = -2.0 * shear_modulus * z * dxy + vm = _stress_von_mises( + load_factor * axial_stress + bending_x, + load_factor * transverse_stress + bending_y, + load_factor * panel.shear_stress + bending_tau, + ) + max_ratio = max(max_ratio, vm / panel.yield_stress_plate) + return max_ratio + + +def u3_yield_utilization( + panel: U3PanelInput, + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + load_factor: float, + config: S3SolverConfig, +) -> dict[str, Any]: + plate_ratio = _u3_plate_yield_ratio(panel, modes, amplitudes, load_factor, config) + return { + "max": plate_ratio, + "plate": plate_ratio, + "stiffener": None, + "stiffener_induced": None, + "plate_induced": None, + } + + +def _stiffener_branch_stress_ratio( + axial_stress: float, + signed_bending_stress: float, + yield_stress: float, +) -> dict[str, float]: + stress = axial_stress + signed_bending_stress + return { + "stress": stress, + "signed_bending_stress": signed_bending_stress, + "ratio": abs(stress) / max(yield_stress, EPS), + } + + +def _stiffener_effective_axial_stress( + panel: S3PanelInput, + gross_section: S3SectionProperties, + stiffener_section: S3SectionProperties, + load_factor: float, +) -> dict[str, float]: + """Return attached-plating effective axial stress for stiffener checks.""" + + nominal_stress = load_factor * panel.axial_stress + area_factor = gross_section.area / max(stiffener_section.area, EPS) + return { + "stress": nominal_stress * area_factor, + "nominal_stress": nominal_stress, + "area_factor": area_factor, + } + + +def _global_slenderness_reduction( + panel: S3PanelInput, + global_elastic_factor: float | None, +) -> dict[str, float | None]: + """Return the public stiffener lateral-deformation reduction terms.""" + + if global_elastic_factor is None or global_elastic_factor <= EPS: + return { + "gamma_reh": None, + "global_elastic_factor": global_elastic_factor, + "slenderness": None, + "reduction_factor": 0.0, + } + + reference_stress = _stress_von_mises( + panel.axial_stress, + panel.mean_transverse_stress, + panel.shear_stress, + ) + if reference_stress <= EPS: + return { + "gamma_reh": None, + "global_elastic_factor": global_elastic_factor, + "slenderness": None, + "reduction_factor": 0.0, + } + + gamma_reh = min(panel.yield_stress_plate, panel.yield_stress_stiffener) / reference_stress + slenderness = math.sqrt(max(gamma_reh / global_elastic_factor, 0.0)) + if slenderness <= 1.56: + reduction = 1.0 - slenderness**4 / 12.0 + else: + reduction = 3.0 / max(4.0 * slenderness, EPS) + return { + "gamma_reh": gamma_reh, + "global_elastic_factor": global_elastic_factor, + "slenderness": slenderness, + "reduction_factor": min(1.0, max(reduction, 0.0)), + } + + +def _stiffener_lateral_deformation_moment( + panel: S3PanelInput, + stiffener_section: S3SectionProperties, + load_factor: float, + global_elastic_factor: float | None, +) -> dict[str, float | None]: + """Return guide-style stiffener moment from lateral deformation.""" + + slenderness = _global_slenderness_reduction(panel, global_elastic_factor) + if ( + load_factor <= EPS + or global_elastic_factor is None + or global_elastic_factor <= load_factor + EPS + or slenderness["reduction_factor"] <= EPS + ): + return { + "moment": 0.0, + "ideal_elastic_buckling_force": 0.0, + "assumed_imperfection": panel.length / 1000.0, + "amplification": 0.0, + **slenderness, + } + + effective_length = panel.length + ideal_elastic_force = ( + math.pi**2 + * panel.elastic_modulus + * stiffener_section.inertia_x + / max(effective_length**2, EPS) + ) + amplification = load_factor / max(global_elastic_factor - load_factor, EPS) + assumed_imperfection = effective_length / 1000.0 + moment = ( + ideal_elastic_force + * float(slenderness["reduction_factor"]) + * amplification + * assumed_imperfection + ) + return { + "moment": moment, + "ideal_elastic_buckling_force": ideal_elastic_force, + "assumed_imperfection": assumed_imperfection, + "amplification": amplification, + **slenderness, + } + + +def _stiffener_yield_ratios( + panel: S3PanelInput, + gross_section: S3SectionProperties, + stiffener_section: S3SectionProperties, + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + load_factor: float, + config: S3SolverConfig, + global_elastic_factor: float | None = None, +) -> dict[str, Any]: + pressure_moment = _pressure_stiffener_bending_moment(panel) + sniped_moments = _sniped_stiffener_eccentricity_moments( + panel, + gross_section, + stiffener_section, + load_factor, + config, + ) + effective_axial = _stiffener_effective_axial_stress( + panel, + gross_section, + stiffener_section, + load_factor, + ) + torsional_deformation = _stiffener_torsional_deformation_stress( + panel, + stiffener_section, + effective_axial["stress"], + config, + ) + lateral_deformation = _stiffener_lateral_deformation_moment( + panel, + stiffener_section, + load_factor, + global_elastic_factor, + ) + max_curvature = 0.0 + for x_fraction in config.hot_spot_grid: + d2x, _, _ = _mode_curvatures( + modes, + amplitudes, + panel.length * x_fraction, + 0.5 * panel.width, + family="global", + ) + max_curvature = max(max_curvature, abs(d2x)) + si_deflection_stress = panel.elastic_modulus * stiffener_section.top_distance * max_curvature + pi_deflection_stress = panel.elastic_modulus * stiffener_section.bottom_distance * max_curvature + axial_stress = effective_axial["stress"] + common_moment = pressure_moment + float(lateral_deformation["moment"]) + si_moment_stress = ( + common_moment + sniped_moments["stiffener_induced"] + ) / max(stiffener_section.top_section_modulus, EPS) + pi_moment_stress = ( + -common_moment + sniped_moments["plate_induced"] + ) / max(stiffener_section.attached_plate_section_modulus, EPS) + si_bending_stress = si_moment_stress + torsional_deformation["stress"] + pi_bending_stress = pi_moment_stress + si_stress = _stiffener_branch_stress_ratio( + axial_stress, + si_bending_stress, + panel.yield_stress_stiffener, + ) + pi_stress = _stiffener_branch_stress_ratio( + axial_stress, + pi_bending_stress, + panel.yield_stress_plate, + ) + si_ratio = si_stress["ratio"] + pi_ratio = pi_stress["ratio"] + return { + "max": max(si_ratio, pi_ratio), + "stiffener_induced": si_ratio, + "plate_induced": pi_ratio, + "effective_axial_stress": effective_axial, + "stiffener_induced_stress": si_stress, + "plate_induced_stress": pi_stress, + "signed_bending_stress": { + "stiffener_moment": si_moment_stress, + "plate_moment": pi_moment_stress, + "ritz_stiffener_deflection": si_deflection_stress, + "ritz_plate_deflection": -pi_deflection_stress, + }, + "pressure_moment": pressure_moment, + "sniped_eccentricity_moment": sniped_moments, + "lateral_deformation_moment": lateral_deformation, + "torsional_deformation_stress": torsional_deformation["stress"], + "torsional_deformation": torsional_deformation, + } + + +def yield_utilization( + panel: S3PanelInput, + section: S3SectionProperties, + stiffener_section: S3SectionProperties, + modes: Sequence[RitzMode], + amplitudes: Sequence[float], + load_factor: float, + config: S3SolverConfig, + global_elastic_factor: float | None = None, +) -> dict[str, Any]: + plate_ratio = _plate_yield_ratio(panel, modes, amplitudes, load_factor, config) + stiffener_ratios = _stiffener_yield_ratios( + panel, + section, + stiffener_section, + modes, + amplitudes, + load_factor, + config, + global_elastic_factor, + ) + return { + "max": max(plate_ratio, stiffener_ratios["max"]), + "plate": plate_ratio, + "stiffener": stiffener_ratios["max"], + "stiffener_induced": stiffener_ratios["stiffener_induced"], + "plate_induced": stiffener_ratios["plate_induced"], + "effective_axial_stress": stiffener_ratios["effective_axial_stress"], + "stiffener_induced_stress": stiffener_ratios["stiffener_induced_stress"], + "plate_induced_stress": stiffener_ratios["plate_induced_stress"], + "signed_bending_stress": stiffener_ratios["signed_bending_stress"], + "pressure_moment": stiffener_ratios["pressure_moment"], + "sniped_eccentricity_moment": stiffener_ratios["sniped_eccentricity_moment"], + "lateral_deformation_moment": stiffener_ratios["lateral_deformation_moment"], + "torsional_deformation_stress": stiffener_ratios["torsional_deformation_stress"], + "torsional_deformation": stiffener_ratios["torsional_deformation"], + } + + +def _continuation_summary( + accepted_steps: int, + rejected_steps: int, + cutbacks: int, + last_accepted_load_factor: float, + min_accepted_step: float | None, + max_accepted_step: float | None, + current_step: float, + config: S3SolverConfig, +) -> dict[str, Any]: + return { + "accepted_steps": accepted_steps, + "rejected_steps": rejected_steps, + "cutbacks": cutbacks, + "last_accepted_load_factor": last_accepted_load_factor, + "min_accepted_step": min_accepted_step, + "max_accepted_step": max_accepted_step, + "current_step": current_step, + "configured_initial_step": config.initial_load_step, + "configured_max_step": config.max_load_step, + "configured_min_step": config.min_load_step, + "configured_cutback": config.load_step_cutback, + "configured_max_cutbacks": config.max_load_step_cutbacks, + } + + +def _interpolate_capacity( + previous_load: float, + previous_ratio: float, + load_factor: float, + ratio: float, + limit: float, +) -> float: + if ratio <= previous_ratio + EPS: + return load_factor + fraction = (limit - previous_ratio) / (ratio - previous_ratio) + fraction = min(max(fraction, 0.0), 1.0) + return previous_load + fraction * (load_factor - previous_load) + + +def _notes() -> list[str]: + return [ + "regular S3 unit strip only; T1, K3, corrugation and FRP are outside this milestone", + "positive PULS CSV normal stress is compression; signed stresses scale while lateral pressure remains fixed", + "Rayleigh-Ritz sine modes use a reduced local/global strip basis, not the full production PULS basis", + "buckling usage is the reduced buckling-strength envelope over ultimate capacity and elastic local/global buckling limits; fixed-pressure material preload contribution is reported in ultimate diagnostics but excluded from buckling-strength control by default", + "shear-normal Ritz coupling is truncated in elastic and continuation checks; classical local plate shear remains a fallback candidate", + "web-local compression-shear uses a reduced stiffener-section web-edge compression envelope and a reduced local plate-web interaction; torsional stiffener remains a reduced gross-section estimate", + "stiffener yield exposes SI/PI section branches, lateral-deformation and sniped bending, SI-only torsional stress, and effective attached plate width", + "global longitudinal strip stiffness degrades from local elastic utilization on the nonlinear load path", + "PULS user manual S3 limits are reported as diagnostics; covered-domain gates remain controlled by solver config", + ] + + +def _u3_notes() -> list[str]: + return [ + "regular U3 unstiffened rectangular panels only; T1, K3, corrugation and FRP are outside this milestone", + "positive PULS export normal stress is compression; signed stresses scale while lateral pressure remains fixed", + "Rayleigh-Ritz sine modes use an isotropic plate basis with simply-supported trigonometric shapes", + "buckling usage is the reduced buckling-strength envelope over first-yield capacity and elastic plate/shear buckling limits", + "U3 end stresses are interpolated in yield checks and averaged in elastic buckling checks", + "PULS user manual U3 limits are reported as diagnostics; covered-domain gates remain controlled by solver config", + ] + + +def _invalid_result(reason: str, diagnostics: dict[str, Any] | None = None) -> S3Result: + return S3Result( + buckling_usage_factor=None, + ultimate_usage_factor=None, + valid=False, + elastic_buckling_usage_factor=None, + invalid_reason=reason, + diagnostics=diagnostics or {}, + covered_domain_notes=_notes(), + ) + + +def _invalid_u3_result(reason: str, diagnostics: dict[str, Any] | None = None) -> S3Result: + return S3Result( + buckling_usage_factor=None, + ultimate_usage_factor=None, + valid=False, + elastic_buckling_usage_factor=None, + invalid_reason=reason, + diagnostics=diagnostics or {}, + covered_domain_notes=_u3_notes(), + ) + + +def _pressure_dominated_yield_limit( + panel: S3PanelInput, + pressure_yield: Mapping[str, Any], + final_yield: Mapping[str, Any], + config: S3SolverConfig, +) -> bool: + if panel.pressure <= EPS: + return False + preload_max = float(pressure_yield.get("max") or 0.0) + if preload_max <= EPS: + return False + preload_share = preload_max / max(_s3_major_yield_utilization_limit(config), EPS) + return preload_share >= config.pressure_dominated_yield_preload_ratio + + +def _s3_major_yield_utilization_limit(config: S3SolverConfig) -> float: + """Return the S3 utilization target for major yield/collapse detection.""" + + reserve = max(config.s3_major_yield_reserve_factor, EPS) + return max(config.yield_utilization_limit, EPS) * reserve + + +def _relative_drift(reference: float | None, candidate: float | None) -> float | None: + if reference is None or candidate is None: + return None + return abs(candidate - reference) / max(abs(reference), EPS) + + +def _mode_convergence_config( + config: S3SolverConfig, + longitudinal_modes: tuple[int, ...], + transverse_modes: tuple[int, ...], +) -> S3SolverConfig: + return replace( + config, + longitudinal_modes=longitudinal_modes, + transverse_modes=transverse_modes, + check_mode_convergence=False, + ) + + +def _summarize_mode_convergence( + base_result: S3Result, + medium_result: S3Result, + high_result: S3Result, + config: S3SolverConfig, +) -> dict[str, Any]: + medium_buckling = _relative_drift( + base_result.buckling_usage_factor, + medium_result.buckling_usage_factor, + ) + medium_ultimate = _relative_drift( + base_result.ultimate_usage_factor, + medium_result.ultimate_usage_factor, + ) + high_buckling = _relative_drift( + base_result.buckling_usage_factor, + high_result.buckling_usage_factor, + ) + high_ultimate = _relative_drift( + base_result.ultimate_usage_factor, + high_result.ultimate_usage_factor, + ) + finite_drifts = [ + value + for value in ( + medium_buckling, + medium_ultimate, + high_buckling, + high_ultimate, + ) + if value is not None and math.isfinite(value) + ] + return { + "enabled": True, + "medium_basis": { + "longitudinal_modes": list(config.medium_longitudinal_modes), + "transverse_modes": list(config.medium_transverse_modes), + "valid": medium_result.valid, + "buckling_usage_factor": medium_result.buckling_usage_factor, + "ultimate_usage_factor": medium_result.ultimate_usage_factor, + "buckling_relative_drift": medium_buckling, + "ultimate_relative_drift": medium_ultimate, + }, + "high_basis": { + "longitudinal_modes": list(config.high_longitudinal_modes), + "transverse_modes": list(config.high_transverse_modes), + "valid": high_result.valid, + "buckling_usage_factor": high_result.buckling_usage_factor, + "ultimate_usage_factor": high_result.ultimate_usage_factor, + "buckling_relative_drift": high_buckling, + "ultimate_relative_drift": high_ultimate, + }, + "max_relative_drift": max(finite_drifts) if finite_drifts else None, + "high_confidence_drift_limit": config.high_confidence_drift_limit, + "medium_confidence_drift_limit": config.medium_confidence_drift_limit, + } + + +def _classify_confidence( + result: S3Result, + validation_domain: Mapping[str, Any], + mode_convergence: Mapping[str, Any] | None, + config: S3SolverConfig, +) -> tuple[str, list[str]]: + if not result.valid: + return "low", [f"invalid:{result.invalid_reason or 'unknown'}"] + + reasons: list[str] = [] + domain_reasons = list(validation_domain.get("reasons") or []) + if domain_reasons: + reasons.extend(f"domain:{reason}" for reason in domain_reasons) + + if result.ultimate_usage_factor is not None and result.buckling_usage_factor is not None: + if result.ultimate_usage_factor > result.buckling_usage_factor + EPS: + reasons.append("usage-order:ultimate-exceeds-buckling") + + if mode_convergence is None or not mode_convergence.get("enabled"): + reasons.append("mode-convergence:not-run") + return ("low" if any(reason.startswith("usage-order:") for reason in reasons) else "medium", reasons) + + max_drift = _optional_float(mode_convergence.get("max_relative_drift")) + medium = mode_convergence.get("medium_basis", {}) + high = mode_convergence.get("high_basis", {}) + if not medium.get("valid") or not high.get("valid"): + reasons.append("mode-convergence:non-valid-refined-basis") + return "low", reasons + if max_drift is None: + reasons.append("mode-convergence:unavailable") + return "low", reasons + reasons.append(f"mode-convergence:max-drift={max_drift:.6g}") + if max_drift <= config.high_confidence_drift_limit: + return "high", reasons + if max_drift <= config.medium_confidence_drift_limit: + return "medium", reasons + return "low", reasons + + +def _attach_reliability( + result: S3Result, + panel: S3PanelInput | U3PanelInput, + config: S3SolverConfig, + panel_family: str, + solver: Any, + validation_reasons: Sequence[str] | None = None, +) -> S3Result: + validation_domain = _validation_domain(panel, config, panel_family, validation_reasons) + diagnostics = dict(result.diagnostics) + diagnostics["validation_domain"] = validation_domain + mode_convergence = diagnostics.get("mode_convergence") + if result.valid and config.check_mode_convergence: + medium_result = solver( + panel, + _mode_convergence_config( + config, + config.medium_longitudinal_modes, + config.medium_transverse_modes, + ), + ) + high_result = solver( + panel, + _mode_convergence_config( + config, + config.high_longitudinal_modes, + config.high_transverse_modes, + ), + ) + mode_convergence = _summarize_mode_convergence( + result, + medium_result, + high_result, + config, + ) + elif mode_convergence is None: + mode_convergence = {"enabled": False} + diagnostics["mode_convergence"] = mode_convergence + confidence, confidence_reasons = _classify_confidence( + result, + validation_domain, + mode_convergence, + config, + ) + diagnostics["confidence"] = confidence + diagnostics["confidence_reasons"] = confidence_reasons + return S3Result( + buckling_usage_factor=result.buckling_usage_factor, + ultimate_usage_factor=result.ultimate_usage_factor, + elastic_buckling_usage_factor=result.elastic_buckling_usage_factor, + valid=result.valid, + invalid_reason=result.invalid_reason, + diagnostics=diagnostics, + covered_domain_notes=result.covered_domain_notes, + confidence=confidence, + confidence_reasons=confidence_reasons, + ) + + +def solve_s3_panel(panel: S3PanelInput, config: S3SolverConfig | None = None) -> S3Result: + """Solve the reduced S3 load path and return usage-factor diagnostics.""" + + config = config or S3SolverConfig() + validation_reasons = collect_s3_validation_reasons(panel, config) + validation_error = validation_reasons[0] if validation_reasons else None + if validation_error is not None: + return _attach_reliability( + _invalid_result(validation_error), + panel, + config, + "S3", + solve_s3_panel, + validation_reasons, + ) + + if all( + abs(value) <= EPS + for value in ( + panel.axial_stress, + panel.mean_transverse_stress, + panel.shear_stress, + ) + ): + return _attach_reliability( + _invalid_result("zero-variable-load"), + panel, + config, + "S3", + solve_s3_panel, + ["zero-variable-load"], + ) + + section = build_section_properties(panel) + stiffener_section, effective_width = build_effective_stiffener_section(panel, config) + modes = build_ritz_modes(panel, section, config) + amplitudes = [0.0 for _ in modes] + amplitudes, pressure_converged, pressure_iterations = solve_equilibrium_amplitudes( + panel, + modes, + 0.0, + amplitudes, + config, + ) + if not pressure_converged: + return _attach_reliability( + _invalid_result( + "non-convergence", + { + "stage": "pressure-preload", + "iterations": pressure_iterations, + }, + ), + panel, + config, + "S3", + solve_s3_panel, + validation_reasons, + ) + + pressure_yield = yield_utilization(panel, section, stiffener_section, modes, amplitudes, 0.0, config) + pressure_preload_response = { + "iterations": pressure_iterations, + "amplitudes": _mode_amplitude_summary(modes, amplitudes), + "yield_utilization": pressure_yield, + "controlling_yield_branch": max( + ("plate", "stiffener_induced", "plate_induced"), + key=lambda branch: float(pressure_yield[branch]), + ), + "pressure_mode_model": { + "separate_symmetric_pressure_modes": config.use_separate_s3_pressure_modes, + "pressure_family": ( + "global-pressure" + if config.use_separate_s3_pressure_modes + else "global" + ), + "pressure_mode_stiffness_factor": ( + config.s3_pressure_mode_stiffness_factor + if config.use_separate_s3_pressure_modes + else 1.0 + ), + "manual_basis": ( + "S3 user manual separates symmetric/clamped pressure modes from " + "asymmetric simply supported buckling modes" + ), + }, + } + if pressure_yield["max"] >= config.pressure_yield_limit: + return _attach_reliability( + _invalid_result( + "pressure", + { + "stage": "pressure-preload", + "yield_utilization": pressure_yield, + "pressure_preload_response": pressure_preload_response, + "pressure_iterations": pressure_iterations, + }, + ), + panel, + config, + "S3", + solve_s3_panel, + validation_reasons, + ) + + buckling = elastic_buckling_factors(panel, section, modes, config, stiffener_section) + buckling_factor = buckling["critical_factor"] + elastic_buckling_usage = None if buckling_factor is None else 1.0 / max(buckling_factor, EPS) + column_factor = buckling["stiffener_column_factor"] + global_family = buckling["modeled_failure_families"].get("global-stiffened-strip", {}) + global_elastic_cutoff_factor = _optional_float(global_family.get("critical_factor")) + s3_major_yield_limit = _s3_major_yield_utilization_limit(config) + + previous_load = 0.0 + previous_yield = pressure_yield["max"] + yield_capacity_factor: float | None = None + max_iterations = pressure_iterations + collapse_state = "major-yield" + final_yield = pressure_yield + local_global_coupling = local_global_stiffness_scale(panel, modes, amplitudes, 0.0, config) + + accepted_steps = 0 + rejected_steps = 0 + cutbacks = 0 + min_accepted_step: float | None = None + max_accepted_step: float | None = None + current_step = min( + max(config.initial_load_step, config.min_load_step, EPS), + max(config.max_load_step, config.min_load_step, EPS), + ) + cutback_factor = min(max(config.load_step_cutback, EPS), 0.95) + + while previous_load < config.max_load_factor - EPS: + load_factor = min(previous_load + current_step, config.max_load_factor) + attempted_step = load_factor - previous_load + local_global_coupling = local_global_stiffness_scale(panel, modes, amplitudes, load_factor, config) + if local_global_coupling["scale"] < 1.0: + modes = build_ritz_modes( + panel, + section, + config, + global_stiffness_scale=local_global_coupling["scale"], + ) + trial_amplitudes, converged, iterations = solve_equilibrium_amplitudes( + panel, + modes, + load_factor, + amplitudes, + config, + ) + max_iterations = max(max_iterations, iterations) + if not converged: + rejected_steps += 1 + next_step = current_step * cutback_factor + if ( + next_step < max(config.min_load_step, EPS) + or cutbacks >= config.max_load_step_cutbacks + ): + continuation = _continuation_summary( + accepted_steps, + rejected_steps, + cutbacks, + previous_load, + min_accepted_step, + max_accepted_step, + current_step, + config, + ) + continuation["attempted_load_factor"] = load_factor + continuation["attempted_step"] = attempted_step + continuation["next_cutback_step"] = next_step + continuation["newton_iterations"] = iterations + continuation["cutback_exhausted"] = True + return _attach_reliability( + _invalid_result( + "non-convergence", + { + "stage": "in-plane-continuation", + "load_factor": load_factor, + "iterations": iterations, + "buckling": buckling, + "continuation": continuation, + }, + ), + panel, + config, + "S3", + solve_s3_panel, + validation_reasons, + ) + current_step = next_step + cutbacks += 1 + continue + + amplitudes = trial_amplitudes + accepted_steps += 1 + min_accepted_step = ( + attempted_step + if min_accepted_step is None + else min(min_accepted_step, attempted_step) + ) + max_accepted_step = ( + attempted_step + if max_accepted_step is None + else max(max_accepted_step, attempted_step) + ) + ultimate_yield_global_factor = ( + global_elastic_cutoff_factor + if config.include_lateral_deformation_in_ultimate_yield + else None + ) + final_yield = yield_utilization( + panel, + section, + stiffener_section, + modes, + amplitudes, + load_factor, + config, + ultimate_yield_global_factor, + ) + if final_yield["max"] >= s3_major_yield_limit: + yield_capacity_factor = _interpolate_capacity( + previous_load, + previous_yield, + load_factor, + final_yield["max"], + s3_major_yield_limit, + ) + previous_load = load_factor + break + previous_load = load_factor + previous_yield = final_yield["max"] + if previous_load >= 2.0: + current_step = min(current_step * config.load_step_growth, config.max_load_step) + + continuation = _continuation_summary( + accepted_steps, + rejected_steps, + cutbacks, + previous_load, + min_accepted_step, + max_accepted_step, + current_step, + config, + ) + + ultimate_capacity_factor = yield_capacity_factor + if ultimate_capacity_factor is None: + if global_elastic_cutoff_factor is not None: + ultimate_capacity_factor = global_elastic_cutoff_factor + collapse_state = "global-elastic-cutoff" + if column_factor is not None and ( + ultimate_capacity_factor is None or column_factor < ultimate_capacity_factor + ): + ultimate_capacity_factor = column_factor + collapse_state = "stiffener-column-cutoff" + + if ultimate_capacity_factor is None or ultimate_capacity_factor <= EPS: + return _attach_reliability( + _invalid_result( + "no-collapse-within-load-range", + { + "max_load_factor": config.max_load_factor, + "buckling": buckling, + "yield_utilization": final_yield, + }, + ), + panel, + config, + "S3", + solve_s3_panel, + validation_reasons, + ) + + pressure_dominated_yield = _pressure_dominated_yield_limit( + panel, + pressure_yield, + final_yield, + config, + ) + include_ultimate_in_buckling_strength = ( + config.include_pressure_dominated_yield_in_buckling_strength + or not pressure_dominated_yield + ) + buckling_strength_limits = {} + excluded_buckling_strength_limits = {} + if include_ultimate_in_buckling_strength: + buckling_strength_limits["ultimate_capacity"] = ultimate_capacity_factor + else: + excluded_buckling_strength_limits[ + "ultimate_capacity" + ] = "pressure-dominated-fixed-preload-yield" + if buckling_factor is not None: + buckling_strength_limits["elastic_buckling_envelope"] = buckling_factor + if not buckling_strength_limits: + buckling_strength_limits["ultimate_capacity"] = ultimate_capacity_factor + buckling_strength_control, buckling_strength_capacity_factor = min( + buckling_strength_limits.items(), + key=lambda item: float(item[1]), + ) + raw_ultimate_capacity_factor = ultimate_capacity_factor + reported_ultimate_capacity_factor = max( + raw_ultimate_capacity_factor, + buckling_strength_capacity_factor, + ) + ultimate_lifted_to_buckling_strength = ( + reported_ultimate_capacity_factor > raw_ultimate_capacity_factor + EPS + ) + buckling_strength = { + "capacity_factor": buckling_strength_capacity_factor, + "usage_factor": 1.0 / max(buckling_strength_capacity_factor, EPS), + "controlling_limit": buckling_strength_control, + "component_capacity_factors": buckling_strength_limits, + "excluded_component_capacity_factors": excluded_buckling_strength_limits, + "elastic_usage_factor": elastic_buckling_usage, + "ultimate_usage_factor": 1.0 / max(reported_ultimate_capacity_factor, EPS), + "raw_ultimate_usage_factor": 1.0 / max(raw_ultimate_capacity_factor, EPS), + "raw_ultimate_capacity_factor": raw_ultimate_capacity_factor, + "reported_ultimate_capacity_factor": reported_ultimate_capacity_factor, + "ultimate_lifted_to_buckling_strength": ultimate_lifted_to_buckling_strength, + "pressure_dominated_yield_limit": pressure_dominated_yield, + "ultimate_included": include_ultimate_in_buckling_strength, + } + diagnostics = { + "collapse_state": collapse_state, + "capacity_factor": reported_ultimate_capacity_factor, + "raw_capacity_factor": raw_ultimate_capacity_factor, + "yield_capacity_factor": yield_capacity_factor, + "s3_major_yield_utilization_limit": s3_major_yield_limit, + "s3_major_yield_reserve_factor": config.s3_major_yield_reserve_factor, + "ultimate_yield_includes_lateral_deformation": ( + config.include_lateral_deformation_in_ultimate_yield + ), + "global_elastic_cutoff_factor": global_elastic_cutoff_factor, + "buckling": buckling, + "pressure_preload_yield_utilization": pressure_yield, + "pressure_preload_response": pressure_preload_response, + "final_yield_utilization": final_yield, + "max_newton_iterations": max_iterations, + "mode_count": len(modes), + "section": asdict(section), + "stiffener_section": asdict(stiffener_section), + "effective_stiffener_plate_width": effective_width, + "load_components": normalized_load_components(panel), + "support_model": SUPPORTED_IN_PLANE_SUPPORTS[panel.in_plane_support], + "local_global_coupling": local_global_coupling, + "continuation_geometric_coupling": _ritz_geometric_matrix(panel, modes)[1], + "continuation": continuation, + "buckling_strength": buckling_strength, + } + return _attach_reliability( + S3Result( + buckling_usage_factor=buckling_strength["usage_factor"], + ultimate_usage_factor=buckling_strength["ultimate_usage_factor"], + elastic_buckling_usage_factor=elastic_buckling_usage, + valid=True, + invalid_reason=None, + diagnostics=diagnostics, + covered_domain_notes=_notes(), + ), + panel, + config, + "S3", + solve_s3_panel, + validation_reasons, + ) + + +def solve_u3_panel(panel: U3PanelInput, config: S3SolverConfig | None = None) -> S3Result: + """Solve the reduced U3 load path and return usage-factor diagnostics.""" + + config = config or S3SolverConfig() + validation_reasons = collect_u3_validation_reasons(panel, config) + validation_error = validation_reasons[0] if validation_reasons else None + if validation_error is not None: + return _attach_reliability( + _invalid_u3_result(validation_error), + panel, + config, + "U3", + solve_u3_panel, + validation_reasons, + ) + + if all( + abs(value) <= EPS + for value in ( + panel.axial_stress, + panel.mean_transverse_stress, + panel.shear_stress, + ) + ): + return _attach_reliability( + _invalid_u3_result("zero-variable-load"), + panel, + config, + "U3", + solve_u3_panel, + ["zero-variable-load"], + ) + + modes = build_u3_ritz_modes(panel, config) + amplitudes = [0.0 for _ in modes] + amplitudes, pressure_converged, pressure_iterations = solve_equilibrium_amplitudes( + panel, + modes, + 0.0, + amplitudes, + config, + ) + if not pressure_converged: + return _attach_reliability( + _invalid_u3_result( + "non-convergence", + { + "stage": "pressure-preload", + "iterations": pressure_iterations, + }, + ), + panel, + config, + "U3", + solve_u3_panel, + validation_reasons, + ) + + pressure_yield = u3_yield_utilization(panel, modes, amplitudes, 0.0, config) + pressure_preload_response = { + "iterations": pressure_iterations, + "amplitudes": _mode_amplitude_summary(modes, amplitudes), + "yield_utilization": pressure_yield, + "controlling_yield_branch": "plate", + } + if pressure_yield["max"] >= config.pressure_yield_limit: + return _attach_reliability( + _invalid_u3_result( + "pressure", + { + "stage": "pressure-preload", + "yield_utilization": pressure_yield, + "pressure_preload_response": pressure_preload_response, + "pressure_iterations": pressure_iterations, + }, + ), + panel, + config, + "U3", + solve_u3_panel, + validation_reasons, + ) + + buckling = elastic_u3_buckling_factors(panel, modes, config) + buckling_factor = buckling["critical_factor"] + elastic_buckling_usage = None if buckling_factor is None else 1.0 / max(buckling_factor, EPS) + + previous_load = 0.0 + previous_yield = pressure_yield["max"] + yield_capacity_factor: float | None = None + max_iterations = pressure_iterations + collapse_state = "first-yield" + final_yield = pressure_yield + + accepted_steps = 0 + rejected_steps = 0 + cutbacks = 0 + min_accepted_step: float | None = None + max_accepted_step: float | None = None + current_step = min( + max(config.initial_load_step, config.min_load_step, EPS), + max(config.max_load_step, config.min_load_step, EPS), + ) + cutback_factor = min(max(config.load_step_cutback, EPS), 0.95) + + while previous_load < config.max_load_factor - EPS: + load_factor = min(previous_load + current_step, config.max_load_factor) + attempted_step = load_factor - previous_load + trial_amplitudes, converged, iterations = solve_equilibrium_amplitudes( + panel, + modes, + load_factor, + amplitudes, + config, + ) + max_iterations = max(max_iterations, iterations) + if not converged: + rejected_steps += 1 + next_step = current_step * cutback_factor + if ( + next_step < max(config.min_load_step, EPS) + or cutbacks >= config.max_load_step_cutbacks + ): + continuation = _continuation_summary( + accepted_steps, + rejected_steps, + cutbacks, + previous_load, + min_accepted_step, + max_accepted_step, + current_step, + config, + ) + continuation["attempted_load_factor"] = load_factor + continuation["attempted_step"] = attempted_step + continuation["next_cutback_step"] = next_step + continuation["newton_iterations"] = iterations + continuation["cutback_exhausted"] = True + return _attach_reliability( + _invalid_u3_result( + "non-convergence", + { + "stage": "in-plane-continuation", + "load_factor": load_factor, + "iterations": iterations, + "buckling": buckling, + "continuation": continuation, + }, + ), + panel, + config, + "U3", + solve_u3_panel, + validation_reasons, + ) + current_step = next_step + cutbacks += 1 + continue + + amplitudes = trial_amplitudes + accepted_steps += 1 + min_accepted_step = ( + attempted_step + if min_accepted_step is None + else min(min_accepted_step, attempted_step) + ) + max_accepted_step = ( + attempted_step + if max_accepted_step is None + else max(max_accepted_step, attempted_step) + ) + final_yield = u3_yield_utilization(panel, modes, amplitudes, load_factor, config) + if final_yield["max"] >= config.yield_utilization_limit: + yield_capacity_factor = _interpolate_capacity( + previous_load, + previous_yield, + load_factor, + final_yield["max"], + config.yield_utilization_limit, + ) + previous_load = load_factor + break + previous_load = load_factor + previous_yield = final_yield["max"] + if previous_load >= 2.0: + current_step = min(current_step * config.load_step_growth, config.max_load_step) + + continuation = _continuation_summary( + accepted_steps, + rejected_steps, + cutbacks, + previous_load, + min_accepted_step, + max_accepted_step, + current_step, + config, + ) + + if yield_capacity_factor is None or yield_capacity_factor <= EPS: + return _attach_reliability( + _invalid_u3_result( + "no-collapse-within-load-range", + { + "max_load_factor": config.max_load_factor, + "buckling": buckling, + "yield_utilization": final_yield, + }, + ), + panel, + config, + "U3", + solve_u3_panel, + validation_reasons, + ) + + buckling_strength_limits = {"ultimate_capacity": yield_capacity_factor} + if buckling_factor is not None: + buckling_strength_limits["elastic_buckling_envelope"] = buckling_factor + buckling_strength_control, buckling_strength_capacity_factor = min( + buckling_strength_limits.items(), + key=lambda item: float(item[1]), + ) + raw_ultimate_capacity_factor = yield_capacity_factor + reported_ultimate_capacity_factor = max( + raw_ultimate_capacity_factor, + buckling_strength_capacity_factor, + ) + ultimate_lifted_to_buckling_strength = ( + reported_ultimate_capacity_factor > raw_ultimate_capacity_factor + EPS + ) + buckling_strength = { + "capacity_factor": buckling_strength_capacity_factor, + "usage_factor": 1.0 / max(buckling_strength_capacity_factor, EPS), + "controlling_limit": buckling_strength_control, + "component_capacity_factors": buckling_strength_limits, + "excluded_component_capacity_factors": {}, + "elastic_usage_factor": elastic_buckling_usage, + "ultimate_usage_factor": 1.0 / max(reported_ultimate_capacity_factor, EPS), + "raw_ultimate_usage_factor": 1.0 / max(raw_ultimate_capacity_factor, EPS), + "raw_ultimate_capacity_factor": raw_ultimate_capacity_factor, + "reported_ultimate_capacity_factor": reported_ultimate_capacity_factor, + "ultimate_lifted_to_buckling_strength": ultimate_lifted_to_buckling_strength, + "pressure_dominated_yield_limit": False, + "ultimate_included": True, + } + diagnostics = { + "panel_family": "U3", + "collapse_state": collapse_state, + "capacity_factor": reported_ultimate_capacity_factor, + "raw_capacity_factor": raw_ultimate_capacity_factor, + "yield_capacity_factor": yield_capacity_factor, + "buckling": buckling, + "pressure_preload_yield_utilization": pressure_yield, + "pressure_preload_response": pressure_preload_response, + "final_yield_utilization": final_yield, + "max_newton_iterations": max_iterations, + "mode_count": len(modes), + "load_components": normalized_load_components(panel), + "support_model": SUPPORTED_IN_PLANE_SUPPORTS[panel.in_plane_support], + "rotational_support": { + "x_edges": panel.rotational_support_1, + "y_edges": panel.rotational_support_2, + }, + "continuation_geometric_coupling": _ritz_geometric_matrix(panel, modes)[1], + "continuation": continuation, + "buckling_strength": buckling_strength, + } + return _attach_reliability( + S3Result( + buckling_usage_factor=buckling_strength["usage_factor"], + ultimate_usage_factor=buckling_strength["ultimate_usage_factor"], + elastic_buckling_usage_factor=elastic_buckling_usage, + valid=True, + invalid_reason=None, + diagnostics=diagnostics, + covered_domain_notes=_u3_notes(), + ), + panel, + config, + "U3", + solve_u3_panel, + validation_reasons, + ) + + + diff --git a/anystruct/current_comps.png b/anystruct/current_comps.png index e5b5a222..df439658 100644 Binary files a/anystruct/current_comps.png and b/anystruct/current_comps.png differ diff --git a/anystruct/example_data.py b/anystruct/example_data.py index 8fdad194..500813b4 100644 --- a/anystruct/example_data.py +++ b/anystruct/example_data.py @@ -519,7 +519,10 @@ def get_grid_coord_from_points_coords(point_coord): def get_section_list(): ''' Returning a section list. ''' - import pl_stf_window as plstf + try: + import anystruct.pl_stf_window as plstf + except ModuleNotFoundError: + import ANYstructure.anystruct.pl_stf_window as plstf return [plstf.Section(obj_dict), plstf.Section(obj_dict2), plstf.Section(obj_dict_L)] diff --git a/anystruct/fatigue_window.py b/anystruct/fatigue_window.py index 46eab5b4..cfe97143 100644 --- a/anystruct/fatigue_window.py +++ b/anystruct/fatigue_window.py @@ -3,9 +3,11 @@ try: import anystruct.example_data as test + import anystruct.line_structure as line_structure import anystruct.SN_curve_parameters as sn except ModuleNotFoundError: import ANYstructure.anystruct.example_data as test + import ANYstructure.anystruct.line_structure as line_structure import ANYstructure.anystruct.SN_curve_parameters as sn @@ -27,21 +29,22 @@ def __init__(self, master, app=None): self._initial_fatigue_obj = test.get_fatigue_object() else: - if app._line_to_struc[app._active_line][0] is None: + active_bundle = app._line_to_struc[app._active_line] + if line_structure.structure(active_bundle) is None: return - elif app._line_to_struc[app._active_line][0].Stiffener is None: + elif not line_structure.has_stiffener(active_bundle): return self.app = app self.active_line = app._active_line points = app._line_dict[self.active_line] coords = (app._point_dict['point'+str(points[0])], app._point_dict['point'+str(points[1])]) self.pressure_coords = self.get_pressure_point_coord_from_two_points(coords[0], coords[1]) - self._initial_structure_obj = app._line_to_struc[app._active_line][0].Stiffener - self.load_objects = app._line_to_struc[app._active_line][3] + self._initial_structure_obj = line_structure.stiffener(active_bundle) + self.load_objects = line_structure.loads(active_bundle) self.comp_objects = [app._tank_dict['comp'+str(comp_i)] for comp_i in app.get_compartments_for_line(app._active_line)] - self._initial_fatigue_obj = app._line_to_struc[self.active_line][2] + self._initial_fatigue_obj = line_structure.fatigue(active_bundle) self._frame = master @@ -349,4 +352,4 @@ def save_and_close(self): if __name__ == '__main__': root = tk.Tk() my_app = CreateFatigueWindow(root,app=None) - root.mainloop() \ No newline at end of file + root.mainloop() diff --git a/anystruct/gui.py b/anystruct/gui.py index f133ad2d..6680ea43 100644 --- a/anystruct/gui.py +++ b/anystruct/gui.py @@ -1,24 +1,5 @@ -import sys, multiprocessing, ctypes, os -import tkinter as tk -from anystruct.main_application import Application +from anystruct.__main__ import main -def main(args=None): - """The main routine.""" - - if args is None: - args = sys.argv[1:] - - multiprocessing.freeze_support() - errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2) - root = tk.Tk() - width = root.winfo_screenwidth() - height = root.winfo_screenheight() - root.geometry(f'{width}x{height}') - my_app = Application(root) - root.mainloop() - - # Do argument parsing here (eg. with argparse) and anything else - # you want your project to do. if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/anystruct/line_structure.py b/anystruct/line_structure.py new file mode 100644 index 00000000..7a963582 --- /dev/null +++ b/anystruct/line_structure.py @@ -0,0 +1,96 @@ +"""Named accessors for the existing line-to-structure bundle format.""" + +import copy +from dataclasses import dataclass, field +from typing import Any + + +STRUCTURE = 0 +LEGACY_CALC_OBJECT = 1 +FATIGUE = 2 +LOADS = 3 +LOAD_COMBINATIONS = 4 +CYLINDER = 5 + + +@dataclass +class LineStructureBundle: + """Typed adapter for the legacy line-to-structure list representation.""" + + line_structure: Any = None + legacy_calc_object: Any = None + fatigue: Any = None + loads: list[Any] = field(default_factory=list) + load_combinations: dict[str, Any] = field(default_factory=dict) + cylinder: Any = None + legacy_length: int = CYLINDER + 1 + + @classmethod + def from_legacy_bundle(cls, bundle): + return cls( + line_structure=None if len(bundle) <= STRUCTURE else bundle[STRUCTURE], + legacy_calc_object=None if len(bundle) <= LEGACY_CALC_OBJECT else bundle[LEGACY_CALC_OBJECT], + fatigue=None if len(bundle) <= FATIGUE else bundle[FATIGUE], + loads=[] if len(bundle) <= LOADS else bundle[LOADS], + load_combinations={} if len(bundle) <= LOAD_COMBINATIONS else bundle[LOAD_COMBINATIONS], + cylinder=None if len(bundle) <= CYLINDER else bundle[CYLINDER], + legacy_length=len(bundle), + ) + + def to_legacy_bundle(self): + bundle = [ + self.line_structure, + self.legacy_calc_object, + self.fatigue, + self.loads, + self.load_combinations, + self.cylinder, + ] + return bundle[:self.legacy_length] + + +def structure(bundle): + return LineStructureBundle.from_legacy_bundle(bundle).line_structure + + +def plate(bundle): + line_structure = structure(bundle) + return None if line_structure is None else line_structure.Plate + + +def stiffener(bundle): + line_structure = structure(bundle) + return None if line_structure is None else line_structure.Stiffener + + +def girder(bundle): + line_structure = structure(bundle) + return None if line_structure is None else line_structure.Girder + + +def fatigue(bundle): + return LineStructureBundle.from_legacy_bundle(bundle).fatigue + + +def loads(bundle): + return LineStructureBundle.from_legacy_bundle(bundle).loads + + +def load_combinations(bundle): + return LineStructureBundle.from_legacy_bundle(bundle).load_combinations + + +def cylinder(bundle): + return LineStructureBundle.from_legacy_bundle(bundle).cylinder + + +def has_cylinder(bundle): + return cylinder(bundle) is not None + + +def has_stiffener(bundle): + return stiffener(bundle) is not None + + +def copy_bundle(bundle): + return [copy.deepcopy(item) if item is not None else None for item in bundle] diff --git a/anystruct/main_application.py b/anystruct/main_application.py index 2199fabd..56f88792 100644 --- a/anystruct/main_application.py +++ b/anystruct/main_application.py @@ -5,14 +5,12 @@ from tkinter import ttk from tkinter import filedialog from tkinter import messagebox -import decimal, pickle +import decimal from _tkinter import TclError import multiprocessing import ctypes from matplotlib import pyplot as plt import matplotlib -from reportlab.lib.pagesizes import letter, landscape -from reportlab.platypus import SimpleDocTemplate from sklearn.neural_network import MLPClassifier from sklearn.preprocessing import StandardScaler from sklearn.metrics._pairwise_distances_reduction import _datasets_pair,_middle_term_computer @@ -34,9 +32,10 @@ import anystruct.stresses_window as stress import anystruct.fatigue_window as fatigue import anystruct.load_factor_window as load_factors - from anystruct.report_generator import LetterMaker - import anystruct.sesam_interface as sesam - import anystruct.excel_inteface as excel_interface + import anystruct.api_helpers as api_helpers + import anystruct.ml_models as ml_models + import anystruct.project_application as project_application + import anystruct.project_services as project_services except ModuleNotFoundError: # This is due to pyinstaller issues. from ANYstructure.anystruct.calc_structure import * @@ -54,9 +53,10 @@ import ANYstructure.anystruct.stresses_window as stress import ANYstructure.anystruct.fatigue_window as fatigue import ANYstructure.anystruct.load_factor_window as load_factors - from ANYstructure.anystruct.report_generator import LetterMaker - import ANYstructure.anystruct.sesam_interface as sesam - import ANYstructure.anystruct.excel_inteface as excel_interface + import ANYstructure.anystruct.api_helpers as api_helpers + import ANYstructure.anystruct.ml_models as ml_models + import ANYstructure.anystruct.project_application as project_application + import ANYstructure.anystruct.project_services as project_services class Application(): ''' @@ -189,8 +189,6 @@ def __init__(self, parent): sub_sesam = tk.Menu(menu) menu.add_cascade(label = 'Interfaces', menu = sub_sesam) sub_sesam.add_command(label = 'Export geometry to SESAM GeniE JS', command = self.export_to_js) - sub_sesam.add_command(label='Run all PULS lines', command=self.puls_run_all_lines) - sub_sesam.add_command(label='Delete all PULS results', command=self.puls_delete_all) sub_sesam.add_command(label='Import excel file', command=self.open_excel_file) sub_help = tk.Menu(menu) @@ -327,58 +325,11 @@ def __init__(self, parent): self.__previous_load_data = None # Used to compare loads before and after. self.__copied_line_prop = None # Used to copy line properties to another. - self._PULS_results = None # If a puls run is avaliable, it is stored here. self._center_of_buoyancy = dict() # Center of buoyancy for all and for carious static drafts # Example {8: (5,20), 22: (12,20), 'all': (16,20)} - self._ML_buckling = {1.1 : dict(), 1.15: dict()}# Buckling machine learning algorithm - for mat_fac in [1.1, 1.15]: - for name, file_base in zip(['cl SP buc int predictor', 'cl SP buc int scaler', - 'cl SP ult int predictor', 'cl SP ult int scaler', - 'cl SP buc GLGT predictor', 'cl SP buc GLGT scaler', - 'cl SP ult GLGT predictor', 'cl SP ult GLGT scaler', - 'cl UP buc int predictor', 'cl UP buc int scaler', - 'cl UP ult int predictor', 'cl UP ult int scaler', - 'cl UP buc GLGT predictor', 'cl UP buc GLGT scaler', - 'cl UP ult GLGT predictor', 'cl UP ult GLGT scaler', - 'CSR predictor UP', 'CSR scaler UP', - 'CSR predictor SP', 'CSR scaler SP' - ], - ["ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_CSR-Tank_req_cl_predictor", - "ml_files\\CL_CSR-Tank_req_cl_scaler", - "ml_files\\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", - "ml_files\\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler"]): - - mat_fac_str = [str(round(mat_fac,2)).replace('.','')+'0'][0][0:3] - self._ML_buckling[mat_fac][name] = None - file_base = file_base.replace('XXX', mat_fac_str) - if os.path.isfile(file_base + '.pickle'): - file = open(file_base + '.pickle', 'rb') - self._ML_buckling[mat_fac][name] = pickle.load(file) - file.close() - else: - #file = open(self._root_dir +'\\' + file_base + '.pickle', 'rb') - - ml_file = os.path.join(self._root_dir, file_base + '.pickle') - file = open(ml_file, 'rb') - self._ML_buckling[mat_fac][name] = pickle.load(file) - file.close() + self._ML_buckling = ml_models.load_buckling_models((self._root_dir,)) + self._ML_classes = ml_models.default_ml_class_messages() # Used to select parameter self._stuctural_definition = ['mat_yield','mat_factor', 'span', 'spacing', 'plate_thk', 'stf_web_height', @@ -950,14 +901,16 @@ def show_message(): self._ent_structure_type] self._new_buckling_method = tk.StringVar() - options = ['DNV-RP-C201 - prescriptive','DNV PULS','ML-CL (PULS based)'] + options = [ + 'DNV-RP-C201 - prescriptive', + 'ML-Numeric (PULS based)', + 'SemiAnalytical S3/U3', + ] self._lab_buckling_method = ttk.Label(self._tab_prop, text='Set buckling method') self._buckling_method = ttk.OptionMenu(self._tab_prop, self._new_buckling_method, options[0], *options, command=self.update_frame) - # PULS interface - self._puls_run_all = ttk.Button(self._tab_prop, text='Run PULS -\nupdate results', - command=self.puls_run_all_lines) + # SemiAnalytical and ML-Numeric share the historic buckling panel input parameters below. self._ent_puls_uf = ttk.Entry(self._tab_prop, textvariable=self._new_puls_uf, width=int(ent_width * 1)) self._new_puls_uf.trace('w', self.trace_acceptance_change) @@ -1017,12 +970,12 @@ def show_message(): ttk.Entry(self._tab_prop, textvariable=self._new_buckling_lf_stresses, width=int(ent_width * 1))] - self._lab_puls_acceptance= ttk.Label(self._tab_prop, text='PULS acceptance') - self._lab_puls_uf = ttk.Label(self._tab_prop, text='PULS utilization factor:') - self._lab_puls_int_gt = ttk.Label(self._tab_prop, text='PULS Int-integrated GL-free left/right GT-free top/bottom') + self._lab_puls_acceptance= ttk.Label(self._tab_prop, text='Buckling acceptance') + self._lab_puls_uf = ttk.Label(self._tab_prop, text='Utilization factor:') + self._lab_puls_int_gt = ttk.Label(self._tab_prop, text='Int-integrated GL-free left/right GT-free top/bottom') self._lab_puls_cont_sniped = ttk.Label(self._tab_prop, text='Continous or Sniped', font = self._text_size['Text 8']) - self._lab_puls_up_supp = ttk.Label(self._tab_prop, text='PULS UP support - left,right,upper,lower\n' + self._lab_puls_up_supp = ttk.Label(self._tab_prop, text='UP support - left,right,upper,lower\n' 'S: simply supported C: Continuous', font = self._text_size['Text 8']) # self._zstar_label = ttk.Label(self._tab_prop, text='z* optimization (buckling RP-C201)', @@ -1064,7 +1017,7 @@ def show_message(): chk_deltax = 0.1 chk_deltay = 0.025 - (ttk.Label(self._tab_information, text='Labelling options. For non-cylinder structure only. ', font="Text 9") + (ttk.Label(self._tab_information, text='Labelling and color code options ', font="Text 9") .place(relx=0.02, rely=2*chk_deltay)) self._information_gui_chk_structure = [ ttk.Checkbutton(self._tab_information, @@ -1470,8 +1423,12 @@ def show_message(): prop_vert_start = 0.01 types_start = 0.005208333 - options = list(CylinderAndCurvedPlate.geomeries.values()) # Shell geometry selection [string] - self._shell_geometries_map = CylinderAndCurvedPlate.geomeries_map # Shell geometry selection string : int + options = ['Flat plate, stiffened', 'Flat plate, unstiffened', 'Flat plate, stiffened with girder'] + \ + list(api_helpers.CYLINDER_STRUCTURE_DOMAINS_WITH_INPUT) + self._shell_geometries_map = { + **api_helpers.FLAT_GEOMETRY_IDS, + **api_helpers.CYLINDER_GEOMETRY_IDS, + } self._current_calculation_domain = 'Flat plate, stiffened' self._unit_informations_dimensions = list() @@ -1763,17 +1720,19 @@ def show_message(): ttk.Button(self._main_fr, text='Load factors', command=self.on_open_load_factor_window,style = "Bold.TButton")\ .place(relx=0.8225,rely=0.7, relwidth = 0.05) - # PULS result information - self._puls_information_button = ttk.Button(self._main_fr, text='PULS results for line', - command=self.on_puls_results_for_line,style = "Bold.TButton") - self._puls_information_button.place(relx=0.875,rely=0.7, relwidth = 0.075) - # Wight developement plot self._weight_button = ttk.Button(self._main_fr, text='Weights', command=self.on_plot_cog_dev,style = "Bold.TButton") - self._weight_button.place(relx=0.9525,rely=0.7, relwidth = 0.038) + self._weight_button.place(relx=0.875,rely=0.7, relwidth = 0.038) self.gui_structural_properties() # Initiating the flat panel structural properties self.set_colors('default') # Setting colors theme + + # Minimum practical size for the current Tkinter layout + parent.minsize(1200, 750) + + self._resize_after_id = None + self._last_resize_size = (0, 0) + # self._current_theme = 'default' def set_colors(self, theme): @@ -1855,8 +1814,14 @@ def gui_structural_properties(self, flat_panel_stf_girder = False, flat_unstf = vert_start = 0.04 hor_start = 0.02 - delta_y = 0.024 - delta_x = 0.13 + width = max(self._parent.winfo_width(), 1) + + if width < 1350: + delta_y = 0.028 + delta_x = 0.12 + else: + delta_y = 0.024 + delta_x = 0.13 ent_relx = hor_start + 6*delta_x @@ -1922,7 +1887,7 @@ def gui_structural_properties(self, flat_panel_stf_girder = False, flat_unstf = idx += 1 self._flat_btn_load_info.place(relx=hor_start + 5 * delta_x, rely=vert_start + (idx+1) * delta_y) - self._flat_btn_fixation_info.place(relx=hor_start + 5 * delta_x, + self._flat_btn_fixation_info.place(relx=hor_start + 6 * delta_x, rely=vert_start+ (idx-7.5) * delta_y) self._button_str_type.place(relx=hor_start + 5 * delta_x, rely=vert_start + (idx+3) * delta_y) @@ -1966,8 +1931,6 @@ def gui_structural_properties(self, flat_panel_stf_girder = False, flat_unstf = buckling_ent.place(relx=hor_start + 5 * delta_x, rely=vert_start + idx * delta_y) idx += 1 - self._puls_run_all.place(relx=hor_start + 6 * delta_x, rely=vert_start + (idx-2) * delta_y) - # optimize buttons for dom in ['Flat plate, unstiffened', 'Flat plate, stiffened', 'Flat plate, stiffened with girder', @@ -2161,7 +2124,7 @@ def gui_structural_properties(self, flat_panel_stf_girder = False, flat_unstf = rely=end_y + delta_y*other_count, relwidth=geo_ent_width*1.9) other_count += 1 - if self._shell_geometries_map[self._new_calculation_domain.get()] in [1,5]: + if api_helpers.geometry_id_for_domain(self._new_calculation_domain.get()) in [1,5]: self._lab_shell_en_cap_pressure.place(relx=hor_start, rely= end_y + delta_y*other_count) self._ent_shell_end_cap_pressure_included.place(relx=3 * delta_x, @@ -2185,12 +2148,12 @@ def gui_structural_properties(self, flat_panel_stf_girder = False, flat_unstf = def calculation_domain_selected(self, event = None): ''' ['Stiffened panel, flat', 'Unstiffened shell (Force input)', 'Unstiffened panel (Stress input)', - 'Longitudinal Stiffened shell (Force input)', 'Longitudinal Stiffened panel (Stress input)', + 'Longitudinal Stiffened shell (Force input)', 'Longitudinal Stiffened panel (Stress input)', 'Ring Stiffened shell (Force input)', 'Ring Stiffened panel (Stress input)', 'Orthogonally Stiffened shell (Force input)', 'Orthogonally Stiffened panel (Stress input)'] ''' - to_process = [self._puls_run_all, self._lab_buckling_method, + to_process = [self._lab_buckling_method, self._buckling_method, self._lab_yield, self._lab_mat_fac,self._structure_types_label, self._button_str_type, self._ent_structure_type, self._lab_structure_type, self._lab_kpp, self._lab_kps, self._lab_km1, self._lab_km2, @@ -2222,7 +2185,7 @@ def calculation_domain_selected(self, event = None): ''' geomeries = {1:'Unstiffened shell (Force input)', 2:'Unstiffened panel (Stress input)', - 3:'Longitudinal Stiffened shell (Force input)', + 3:'Longitudinal Stiffened shell (Force input)', 4:'Longitudinal Stiffened panel (Stress input)', 5:'Ring Stiffened shell (Force input)', 6:'Ring Stiffened panel (Stress input)', @@ -2246,7 +2209,7 @@ def calculation_domain_selected(self, event = None): 'Unstiffened panel (Stress input)']: self.gui_structural_properties(flat_unstf=False, flat_stf = False, flat_panel_stf_girder = False, shell=True, long_stf=False, ring_stf=False, ring_frame=False) - elif self._new_calculation_domain.get() in ['Longitudinal Stiffened shell (Force input)', + elif self._new_calculation_domain.get() in ['Longitudinal Stiffened shell (Force input)', 'Longitudinal Stiffened panel (Stress input)']: self.gui_structural_properties(flat_unstf=False, flat_stf = False, flat_panel_stf_girder = False, shell=True, long_stf=True, ring_stf=False, ring_frame=False) @@ -2294,69 +2257,6 @@ def calculation_domain_selected(self, event = None): self._current_calculation_domain = self._new_calculation_domain.get() # Setting the correct optmization buttons - def puls_run_all_lines(self, line_given = None): - progress = ttk.Progressbar(self._tab_prop, mode='indeterminate') - progress.place(relx = 0.85, rely = 0.9, relwidth = 0.1) - progress.start() - if self._PULS_results is None: - self._PULS_results = PULSpanel() - if self._PULS_results.puls_sheet_location is None or not os.path.isfile(self._PULS_results.puls_sheet_location): - tk.messagebox.showerror('No PULS excel sheet located', 'Set location of PULS excel sheet.\n' - 'Note that PULS excel may require 32 bit ' - 'office.\n\n' - 'A sheet may be provided but does not exist' - ' in :\n' - + str(self._PULS_results.puls_sheet_location) + - '\n\n A file dialogue will pop up after this message.') - self._PULS_results.puls_sheet_location= \ - tk.filedialog.askopenfilename(parent=self._main_fr,title='Set location of PULS excel sheet.') - if self._PULS_results.puls_sheet_location == '': - tk.messagebox.showerror('No valid PULS sheet', 'No excel sheet was provided. Cannot run PULS.\n' - 'Note that PULS excel may require 32 bit office.') - return - - dict_to_run = {} - result_lines = list(self._PULS_results.get_run_results().keys()) - - if line_given == None: - current_button = self._puls_run_all - for line, data in self._line_to_struc.items(): - if line not in result_lines: - data[0].Plate.hw = 0 if data[0].Stiffener is None else data[0].Stiffener.hw - data[0].Plate.tw = 0 if data[0].Stiffener is None else data[0].Stiffener.tw - data[0].Plate.b = 0 if data[0].Stiffener is None else data[0].Stiffener.b - data[0].Plate.tf = 0 if data[0].Stiffener is None else data[0].Stiffener.tf - dict_to_run[line] = data[0].Plate.get_puls_input() - dict_to_run[line]['Identification'] = line - dict_to_run[line]['Pressure (fixed)'] = self.get_highest_pressure(line)['normal']/1e6 - else: - current_button = self._puls_run_one - if line_given == '': - return - if line_given not in result_lines: - dict_to_run[line_given] = self._line_to_struc[line_given][0].Plate.get_puls_input() - dict_to_run[line_given]['Identification'] = line_given - dict_to_run[line_given]['Pressure (fixed)'] = self.get_highest_pressure(line_given)['normal'] / 1e6 - - - if len(dict_to_run) > 0: - - #current_button.config(relief = 'sunken') - self._PULS_results.set_all_to_run(dict_to_run) - self._PULS_results.run_all() - #current_button.config(relief='raised') - current_button.config(text='PULS run or\nupdate all lines' if line_given == None else 'PULS\nRun one line') - #current_button.config(bg=self._button_bg_color) - for key, value in self._line_to_struc.items(): - value[0].need_recalc = True - else: - tk.messagebox.showinfo('Results avaliable', 'PULS results is already avaliable for this line or no ' - 'lines need update.') - - progress.stop() - progress.destroy() - self.update_frame() - def stress_information_notebooks(self, info_type = 'shell'): ''' Shows stress information ''' text_m = tk.Toplevel(self._parent, background=self._general_color) @@ -2416,10 +2316,6 @@ def stress_information_notebooks(self, info_type = 'shell'): pass text_widget.image_create('current', image=photo) - def trace_puls_uf(self, *args): - if self._PULS_results is not None: - pass - def trace_material_factor(self, *args): try: self._new_puls_uf.set(1/self._new_material_factor.get()) @@ -2444,36 +2340,108 @@ def trace_puls_up_or_sp(self, event = None): else: self._ent_puls_up_boundary.place_forget() - def puls_run_one_line(self): - self.puls_run_all_lines(self._active_line) - self.update_frame() + def resize(self, event=None): + """ + Responsive GUI scaling. - def puls_delete_all(self): - ''' - Deletes all existing PULS results - ''' - if self._PULS_results is not None: - for key, val in self._line_to_struc.items(): - self._PULS_results.result_changed(key) - val[0].need_recalc = True + The old implementation kept text_scale = 1 and did not call update_frame(). + On small laptop screens, relative widget placement shrinks the available + widget area while the text remains full size, causing overlap. + """ - self.update_frame() + if event is not None and event.widget is not self._parent: + return - def resize(self, event): - self.text_scale = 1#self._main_fr.winfo_width()/1920 - self._text_size = {'Text 14 bold': 'Verdana '+str(int(14*self.text_scale))+' bold', - 'Text 16 bold': 'Verdana ' + str(int(16 * self.text_scale)) + ' bold', - 'Text 18 bold': 'Verdana ' + str(int(18 * self.text_scale)) + ' bold', - 'Text 12 bold': 'Verdana ' + str(int(12 * self.text_scale)) + ' bold', - 'Text 10 bold': 'Verdana '+str(int(10*self.text_scale))+' bold', - 'Text 9 bold': 'Verdana ' + str(int(9 * self.text_scale)) + ' bold', - 'Text 8 bold': 'Verdana ' + str(int(8 * self.text_scale)) + ' bold', - 'Text 8': 'Verdana ' + str(int(8 * self.text_scale)), - 'Text 9': 'Verdana ' + str(int(8 * self.text_scale)), - 'Text 7': 'Verdana ' + str(int(7 * self.text_scale)), - 'Text 10': 'Verdana ' + str(int(10 * self.text_scale)), - 'Text 7 bold': 'Verdana ' + str(int(7 * self.text_scale)) + ' bold'} - #self.update_frame() + width = max(self._parent.winfo_width(), 1) + height = max(self._parent.winfo_height(), 1) + + if (width, height) == self._last_resize_size: + return + + self._last_resize_size = (width, height) + + # Scale against a comfortable reference layout. + # Clamp to avoid unreadably small or unnecessarily large fonts. + scale_w = width / 1600.0 + scale_h = height / 900.0 + self.text_scale = max(0.78, min(1.05, min(scale_w, scale_h))) + + self._text_size = { + 'Text 14 bold': 'Verdana ' + str(max(9, int(14 * self.text_scale))) + ' bold', + 'Text 16 bold': 'Verdana ' + str(max(10, int(16 * self.text_scale))) + ' bold', + 'Text 18 bold': 'Verdana ' + str(max(11, int(18 * self.text_scale))) + ' bold', + 'Text 12 bold': 'Verdana ' + str(max(8, int(12 * self.text_scale))) + ' bold', + 'Text 10 bold': 'Verdana ' + str(max(7, int(10 * self.text_scale))) + ' bold', + 'Text 9 bold': 'Verdana ' + str(max(7, int(9 * self.text_scale))) + ' bold', + 'Text 8 bold': 'Verdana ' + str(max(6, int(8 * self.text_scale))) + ' bold', + 'Text 8': 'Verdana ' + str(max(6, int(8 * self.text_scale))), + 'Text 9': 'Verdana ' + str(max(6, int(9 * self.text_scale))), + 'Text 7': 'Verdana ' + str(max(6, int(7 * self.text_scale))), + 'Text 10': 'Verdana ' + str(max(7, int(10 * self.text_scale))), + 'Text 7 bold': 'Verdana ' + str(max(6, int(7 * self.text_scale))) + ' bold', + 'Text 6 bold': 'Verdana ' + str(max(6, int(6 * self.text_scale))) + ' bold', + } + + self._apply_responsive_main_layout(width, height) + + # Throttle redraws while user drags/resizes the window. + if self._resize_after_id is not None: + self._parent.after_cancel(self._resize_after_id) + + self._resize_after_id = self._parent.after(150, self._finish_resize) + + def _finish_resize(self): + self._resize_after_id = None + + try: + self.update_frame() + except Exception: + # Avoid crashing during early initialization. + pass + + def _apply_responsive_main_layout(self, width, height): + """ + Adjust the major GUI regions for smaller laptop screens. + + The old layout used: + tab width = 0.2585 + main canvas start x = 0.26 + + That leaves too little room for the input notebook on small screens. + """ + + if width < 1350: + left_width = 0.34 + elif width < 1600: + left_width = 0.30 + else: + left_width = 0.2585 + + x_canvas_place = left_width + 0.005 + remaining_width = 1.0 - x_canvas_place + + self._tabControl.place(relwidth=left_width, relheight=1) + + self._main_canvas.place( + relx=x_canvas_place, + rely=0, + relwidth=remaining_width * 0.70, + relheight=0.73, + ) + + self._prop_canvas.place( + relx=x_canvas_place, + rely=0.73, + relwidth=remaining_width * 0.52, + relheight=0.27, + ) + + self._result_canvas.place( + relx=x_canvas_place + remaining_width * 0.52, + rely=0.73, + relwidth=remaining_width * 0.48, + relheight=0.27, + ) def toggle_select_multiple(self, event = None): if self._toggle_btn.config('relief')[-1] == 'sunken': @@ -2846,7 +2814,7 @@ def add_to_combinations_dict(self,line): def trace_update_load(self, *args): try: - self._line_to_struc[self._active_line][0].need_recalc = True + project_services.mark_line_for_recalculation(self._line_to_struc, self._active_line) except BaseException as error: pass @@ -2859,8 +2827,7 @@ def trace_shift_change(self, *args): def trace_acceptance_change(self, *args): try: self.update_frame() - for key, val in self._line_to_struc.items(): - val[0].need_recalc = True + project_services.mark_lines_for_recalculation(self._line_to_struc) except (TclError, ZeroDivisionError): pass @@ -2874,14 +2841,181 @@ def update_frame(self, event = None, *args): return state - def get_color_and_calc_state(self, current_line = None, active_line_only = False): + def _ml_model_exists(self, mat_fac, key): + return ( + mat_fac in self._ML_buckling + and key in self._ML_buckling[mat_fac] + and self._ML_buckling[mat_fac][key] is not None + ) + + def _predict_numeric_uf_pipeline(self, mat_fac, sp_or_up, boundary_type, buckling_ml_input): + """ + Predict raw numeric buckling/ultimate UF using the numeric UF pipeline. + + Important: + The numeric model is trained on Buckling/Ultimate UF at material factor = 1.0. + Material factor scaling is applied later in get_color_and_calc_state(). + """ + if sp_or_up == 'SP': + prefix = 'num SP int' if boundary_type == 'Int' else 'num SP GLGT' + else: + prefix = 'num UP int' if boundary_type == 'Int' else 'num UP GLGT' + + valid_predictor_key = f'{prefix} validity predictor' + valid_xscaler_key = f'{prefix} validity xscaler' + reg_predictor_key = f'{prefix} UF reg predictor' + reg_xscaler_key = f'{prefix} UF reg xscaler' + reg_yscaler_key = f'{prefix} UF reg yscaler' + + required_keys = [ + valid_predictor_key, + valid_xscaler_key, + reg_predictor_key, + reg_xscaler_key, + reg_yscaler_key, + ] + + missing = [key for key in required_keys if not self._ml_model_exists(mat_fac, key)] + if missing: + return { + 'available': False, + 'valid_prediction': None, + 'valid_label': 'numeric ML unavailable', + 'buckling_uf': float('inf'), + 'ultimate_uf': float('inf'), + 'buckling_uf_raw': float('inf'), + 'ultimate_uf_raw': float('inf'), + 'material_factor': mat_fac, + 'error': 'Missing numeric ML model(s): ' + ', '.join(missing), + } + + try: + x_valid = self._ML_buckling[mat_fac][valid_xscaler_key].transform( + buckling_ml_input + ) + valid_pred = self._ML_buckling[mat_fac][valid_predictor_key].predict( + x_valid + )[0] + valid_pred_int = int(valid_pred) + + if valid_pred_int != 1: + return { + 'available': True, + 'valid_prediction': valid_pred_int, + 'valid_label': 'invalid/NaN UF predicted', + 'buckling_uf': float('inf'), + 'ultimate_uf': float('inf'), + 'buckling_uf_raw': float('inf'), + 'ultimate_uf_raw': float('inf'), + 'material_factor': mat_fac, + 'error': '', + } + + x_reg = self._ML_buckling[mat_fac][reg_xscaler_key].transform( + buckling_ml_input + ) + y_scaled = self._ML_buckling[mat_fac][reg_predictor_key].predict( + x_reg + ) + y_numeric = self._ML_buckling[mat_fac][reg_yscaler_key].inverse_transform( + y_scaled + )[0] + + buckling_uf_raw = float(y_numeric[0]) + ultimate_uf_raw = float(y_numeric[1]) + + return { + 'available': True, + 'valid_prediction': valid_pred_int, + 'valid_label': 'valid numeric UF predicted', + 'buckling_uf': buckling_uf_raw, + 'ultimate_uf': ultimate_uf_raw, + 'buckling_uf_raw': buckling_uf_raw, + 'ultimate_uf_raw': ultimate_uf_raw, + 'material_factor': mat_fac, + 'error': '', + } + + except Exception as e: + return { + 'available': False, + 'valid_prediction': None, + 'valid_label': 'numeric ML error', + 'buckling_uf': float('inf'), + 'ultimate_uf': float('inf'), + 'buckling_uf_raw': float('inf'), + 'ultimate_uf_raw': float('inf'), + 'material_factor': mat_fac, + 'error': str(e), + } + + def _numeric_uf_color(self, uf, mat_fac=None): + """ + Numeric UF is material-factored in get_color_and_calc_state(). + Therefore the acceptance limit is 1.0. + """ + try: + return 'green' if float(uf) <= 1.0 else 'red' + except Exception: + return 'red' + + def _semi_analytical_uf_color(self, uf): + """SemiAnalytical UF is material-factored before pass/fail coloring.""" + try: + return 'green' if float(uf) <= 1.0 else 'red' + except Exception: + return 'red' + + @staticmethod + def _cylinder_buckling_uf(cylinder_results): + """Return the governing cylinder buckling UF used for GUI color coding.""" + uf_values = [] + for key in ['Unstiffened shell', 'Longitudinal stiffened shell', + 'Ring stiffened shell', 'Heavy ring frame']: + try: + if cylinder_results.get(key, None) is not None: + uf_values.append(float(cylinder_results[key])) + except Exception: + pass + + try: + if cylinder_results.get('Need to check column buckling', False) is True and \ + cylinder_results.get('Column stability UF', None) is not None: + uf_values.append(float(cylinder_results['Column stability UF'])) + except Exception: + pass + + return max(uf_values) if uf_values else 0.0 + + def get_color_and_calc_state(self, current_line=None, active_line_only=False): ''' Return calculations and colors for line and results. ''' - return_dict = {'colors': {}, 'section_modulus': {}, 'thickness': {}, 'shear_area': {}, 'buckling': {}, - 'fatigue': {}, 'pressure_uls': {}, 'pressure_fls': {}, - 'all_obj': {}, 'scant_calc_obj': {}, 'fatigue_obj': {}, 'utilization': {}, 'slamming': {}, - 'color code': {}, 'PULS colors': {}, 'ML buckling colors' : {}, 'ML buckling class' : {}, - 'weights': {}, 'cylinder': {}} + return_dict = { + 'colors': {}, + 'section_modulus': {}, + 'thickness': {}, + 'shear_area': {}, + 'buckling': {}, + 'fatigue': {}, + 'pressure_uls': {}, + 'pressure_fls': {}, + 'all_obj': {}, + 'scant_calc_obj': {}, + 'fatigue_obj': {}, + 'utilization': {}, + 'slamming': {}, + 'color code': {}, + 'ML buckling colors': {}, + 'ML buckling class': {}, + 'ML buckling numeric': {}, + 'ML buckling numeric valid': {}, + 'ML buckling numeric colors': {}, + 'SemiAnalytical': {}, + 'SemiAnalytical valid': {}, + 'SemiAnalytical colors': {}, + 'weights': {}, + 'cylinder': {}, + } return_dict['slamming'][current_line] = {} @@ -2908,40 +3042,52 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False all_cyl_thk = np.sort(all_cyl_thk) for current_line in line_iterator: - rec_for_color[current_line] = {} + rec_for_color[current_line] = {} slamming_pressure = 0 if current_line in self._line_to_struc.keys(): - if self._line_to_struc[current_line][5] is not None: cyl_obj = self._line_to_struc[current_line][5] - # cyl_radius = round(cyl_obj.ShellObj.radius * 1000, 2) - # cyl_thickness = round(cyl_obj.ShellObj.thk * 1000, 2) - # cyl_long_str = cyl_obj.LongStfObj.get_beam_string() - # cyl_ring_stf = cyl_obj.LongStfObj.get_beam_string() - # cyl_heavy_ring = cyl_obj.LongStfObj.get_beam_string() - # cyl_span = round(cyl_obj.ShellObj.dist_between_rings, 1) - # cyl_tot_length = round(cyl_obj.ShellObj.length_of_shell, 1) - # cyl_tot_cyl = round(cyl_obj.ShellObj.tot_cyl_length, 1) - # cyl_sigma_axial = cyl_obj.sasd / 1e6 - # cyl_sigma_bend = cyl_obj.smsd / 1e6 - # cyl_sigma_tors = cyl_obj.tTsd / 1e6 - # cyl_tau_xy = cyl_obj.tQsd / 1e6 - # cyl_lat_press = cyl_obj.psd / 1e6 - # cyl_sigma_hoop = cyl_obj.shsd / 1e6 cyl_results = cyl_obj.get_utilization_factors() else: cyl_thickness = 0 all_obj = self._line_to_struc[current_line][0] - obj_scnt_calc_pl = all_obj.Plate #self._line_to_struc[current_line][1] + obj_scnt_calc_pl = all_obj.Plate # self._line_to_struc[current_line][1] obj_scnt_calc_stf = all_obj.Stiffener # self._line_to_struc[current_line][1] obj_scnt_calc_girder = all_obj.Girder # self._line_to_struc[current_line][1] return_dict['all_obj'][current_line] = all_obj if all_obj.need_recalc is False: - return self._state_logger[current_line] + # Cached state may be stale when the selected ML material factor changes. + # Reuse cache only when the cached numeric ML material factor matches + # the current GUI-selected material factor. + cached_state = self._state_logger.get(current_line, None) + try: + selected_mat_fac = float(self._new_material_factor.get()) + except Exception: + selected_mat_fac = obj_scnt_calc_pl.mat_factor + + cached_mat_fac = None + cached_has_semi_analytical = False + if cached_state is not None: + cached_mat_fac = ( + cached_state + .get('ML buckling numeric', {}) + .get(current_line, {}) + .get('material factor', None) + ) + cached_has_semi_analytical = current_line in cached_state.get('SemiAnalytical', {}) + + try: + cached_mat_fac = None if cached_mat_fac is None else float(cached_mat_fac) + except Exception: + cached_mat_fac = None + + if cached_state is not None and cached_mat_fac == selected_mat_fac and cached_has_semi_analytical: + return cached_state + # Otherwise continue and recalculate this line so numeric UF is updated. try: norm_and_slam = self.get_highest_pressure(current_line) design_pressure = norm_and_slam['normal'] / 1000 @@ -2958,7 +3104,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False min_thk = obj_scnt_calc_pl.get_dnv_min_thickness(design_pressure) color_thk = 'green' if obj_scnt_calc_pl.is_acceptable_pl_thk(design_pressure) else 'red' rec_for_color[current_line]['plate thickness'] = (min_thk / 1000) / obj_scnt_calc_pl.get_pl_thk() - all_obj.lat_press = design_pressure/1000 + all_obj.lat_press = design_pressure / 1000 buckling = all_obj.plate_buckling() all_buckling_uf_list = list() @@ -2977,7 +3123,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False min_shear = obj_scnt_calc_stf.get_minimum_shear_area(design_pressure) min_sec_mod = obj_scnt_calc_stf.get_dnv_min_section_modulus(design_pressure) rec_for_color[current_line]['section modulus'] = min_sec_mod / min(sec_mod) - rec_for_color[current_line]['shear'] = min_shear/shear_area + rec_for_color[current_line]['shear'] = min_shear / shear_area return_dict['slamming'][current_line] = dict() if slamming_pressure is not None and slamming_pressure > 0: return_dict['slamming'][current_line]['state'] = True @@ -3003,20 +3149,20 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False color_shear = 'green' if obj_scnt_calc_stf.is_acceptable_shear_area(shear_area, design_pressure) \ else 'red' else: - sec_mod = [0,0] + sec_mod = [0, 0] rec_for_color[current_line]['section modulus'] = 0.0 rec_for_color[current_line]['shear'] = 0 return_dict['slamming'][current_line] = dict() fatigue_obj, p_int, p_ext, damage, dff = [None for dummy in range(5)] color_sec = 'green' if all_obj.Stiffener is None else 'black' - color_shear = 'green' if all_obj.Stiffener is None else'black' + color_shear = 'green' if all_obj.Stiffener is None else 'black' return_dict['slamming'][current_line]['state'] = False if slamming_pressure is not None and slamming_pressure > 0 and obj_scnt_calc_stf is not None: slamming_res = obj_scnt_calc_stf.calculate_slamming_stiffener(slamming_pressure, - red_fac=slamming_red_fac_pl) + red_fac=slamming_red_fac_pl) min_pl_slamming = obj_scnt_calc_stf.calculate_slamming_plate(slamming_pressure, - red_fac=slamming_red_fac_stf) + red_fac=slamming_red_fac_stf) if slamming_res['Zp_req'] is not None: zpl = obj_scnt_calc_stf.get_net_effective_plastic_section_modulus() @@ -3027,10 +3173,10 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False zpl_req = None color_sec = 'red' - color_shear = 'green' if round(obj_scnt_calc_stf.get_web_thk()* 1000,1) >= \ - round(slamming_res['tw_req'],1) else 'red' - color_thk = 'green' if round(obj_scnt_calc_stf.get_pl_thk() * 1000,1) >= \ - round(min_pl_slamming,1) else 'red' + color_shear = 'green' if round(obj_scnt_calc_stf.get_web_thk() * 1000, 1) >= \ + round(slamming_res['tw_req'], 1) else 'red' + color_thk = 'green' if round(obj_scnt_calc_stf.get_pl_thk() * 1000, 1) >= \ + round(min_pl_slamming, 1) else 'red' return_dict['slamming'][current_line]['zpl'] = zpl return_dict['slamming'][current_line]['zpl_req'] = zpl_req @@ -3046,156 +3192,489 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False if self._line_to_struc[current_line][5] is not None: return_dict['cylinder'][current_line] = cyl_results - - ''' - PULS calculations ''' - if self._PULS_results != None: - res = self._PULS_results.get_puls_line_results(current_line) - if res is not None: - geo_problem = False - if type(res['Ultimate capacity']['Actual usage Factor'][0]) != str: - ufnum = res['Ultimate capacity']['Actual usage Factor'][0] / self._new_puls_uf.get() - rec_for_color[current_line]['PULS ultimate']=ufnum - col_ult = 'green' if ufnum < 1 else 'red' - else: - geo_problem = True - col_ult = 'red' - if res['Buckling strength']['Actual usage Factor'][0] is not None: - bnum = res['Buckling strength']['Actual usage Factor'][0] / self._new_puls_uf.get() - rec_for_color[current_line]['PULS buckling'] = bnum - col_buc = 'green' if bnum < 1 else 'red' - else: - col_buc = 'red' - if geo_problem: - loc_geom = 'red' - else: - if obj_scnt_calc_stf is None: - loc_label = 'Geom. Req (PULS validity limits)' - else: - loc_label = 'Local geom req (PULS validity limits)' if \ - obj_scnt_calc_stf.get_puls_sp_or_up() == 'SP' else 'Geom. Req (PULS validity limits)' - loc_geom = 'green' if all([val[0] == 'Ok' for val in res[loc_label].values()]) else 'red' - if obj_scnt_calc_stf is None: - csr_label = 'CSR-Tank req' - else: - csr_label = 'CSR-Tank requirements (primary stiffeners)' if \ - obj_scnt_calc_stf.get_puls_sp_or_up() == 'SP' else'CSR-Tank req' + Machine learning buckling. - csr_geom = 'green' if all([val[0] in ['Ok', '-'] for val in res[csr_label].values()]) else 'red' - return_dict['PULS colors'][current_line] = {'ultimate': col_ult, 'buckling': col_buc, - 'local geometry': loc_geom, 'csr': csr_geom} - else: - return_dict['PULS colors'][current_line] = {'ultimate': 'black', 'buckling': 'black', - 'local geometry': 'black', 'csr': 'black'} - else: - return_dict['PULS colors'][current_line] = {'ultimate': 'black', 'buckling': 'black', - 'local geometry': 'black', 'csr': 'black'} + Classification pipeline: + return_dict['ML buckling class'] + return_dict['ML buckling colors'] + Numeric UF pipeline: + return_dict['ML buckling numeric'] + return_dict['ML buckling numeric valid'] + return_dict['ML buckling numeric colors'] ''' - Machine learning buckling - ['cl SP buc int predictor', 'cl SP buc int scaler', - 'cl SP ult int predictor', 'cl SP ult int scaler', - 'cl SP buc GLGT predictor', 'cl SP buc GLGT scaler', - 'cl SP ult GLGT predictor', 'cl SP ult GLGT scaler'] - ''' + mat_fac_error = '' + + # ----------------------------------------------------------------------------- + # Helper defaults in case ML cannot be evaluated + # ----------------------------------------------------------------------------- + default_numeric_pred = { + 'available': False, + 'valid_prediction': None, + 'valid_label': 'numeric ML not available', + 'buckling_uf': float('inf'), + 'ultimate_uf': float('inf'), + 'buckling_uf_raw': float('inf'), + 'ultimate_uf_raw': float('inf'), + 'material_factor': None, + 'error': '', + } + + def _apply_material_factor_to_numeric_pred(numeric_pred, mat_fac): + """ + Numeric UF model predicts UF at material factor = 1.0. + + The displayed/check UF shall be: + UF = predicted_UF * material_factor + """ + + numeric_pred = numeric_pred.copy() + + if numeric_pred.get('valid_prediction', None) == 1: + buckling_uf_raw = float(numeric_pred.get('buckling_uf', float('inf'))) + ultimate_uf_raw = float(numeric_pred.get('ultimate_uf', float('inf'))) + + numeric_pred['buckling_uf_raw'] = buckling_uf_raw + numeric_pred['ultimate_uf_raw'] = ultimate_uf_raw + + numeric_pred['buckling_uf'] = buckling_uf_raw * mat_fac + numeric_pred['ultimate_uf'] = ultimate_uf_raw * mat_fac + numeric_pred['material_factor'] = mat_fac + else: + numeric_pred['buckling_uf_raw'] = float('inf') + numeric_pred['ultimate_uf_raw'] = float('inf') + numeric_pred['material_factor'] = mat_fac + + return numeric_pred + if obj_scnt_calc_pl.get_puls_sp_or_up() == 'UP': - buckling_ml_input = obj_scnt_calc_pl.get_buckling_ml_input(design_lat_press=design_pressure) - mat_fac = obj_scnt_calc_pl.mat_factor + # ========================================================================= + # UP / unstiffened panel ML + # ========================================================================= + buckling_ml_input = obj_scnt_calc_pl.get_buckling_ml_input( + design_lat_press=design_pressure + ) + + try: + mat_fac = float(self._new_material_factor.get()) + except Exception: + mat_fac = obj_scnt_calc_pl.mat_factor mat_fac_error = '' + if mat_fac not in [1.1, 1.15]: mat_fac_error = ' MATERIAL FACTOR MUST BE 1.1 or 1.15 -> using 1.15' mat_fac = 1.15 - if obj_scnt_calc_pl.get_puls_boundary() == 'Int': - if self._ML_buckling[mat_fac]['cl UP buc int predictor'] != None: - x_buc = self._ML_buckling[mat_fac]['cl UP buc int scaler'].transform(buckling_ml_input) - y_pred_buc = self._ML_buckling[mat_fac]['cl UP buc int predictor'].predict(x_buc)[0] + + boundary_type = obj_scnt_calc_pl.get_puls_boundary() + + # ------------------------------------------------------------------------- + # Classification ML prediction + # ------------------------------------------------------------------------- + if boundary_type == 'Int': + if self._ML_buckling[mat_fac]['cl UP buc int predictor'] is not None: + x_buc = self._ML_buckling[mat_fac]['cl UP buc int scaler'].transform( + buckling_ml_input + ) + y_pred_buc = self._ML_buckling[mat_fac]['cl UP buc int predictor'].predict( + x_buc + )[0] else: - y_pred_buc = 0 - if self._ML_buckling[mat_fac]['cl UP ult int predictor'] != None: - x_ult = self._ML_buckling[mat_fac]['cl UP ult int scaler'].transform(buckling_ml_input) - y_pred_ult = self._ML_buckling[mat_fac]['cl UP ult int predictor'].predict(x_ult)[0] + y_pred_buc = 'ML buckling model missing' + + if self._ML_buckling[mat_fac]['cl UP ult int predictor'] is not None: + x_ult = self._ML_buckling[mat_fac]['cl UP ult int scaler'].transform( + buckling_ml_input + ) + y_pred_ult = self._ML_buckling[mat_fac]['cl UP ult int predictor'].predict( + x_ult + )[0] else: - y_pred_ult = 0 + y_pred_ult = 'ML ultimate model missing' + else: - if self._ML_buckling[mat_fac]['cl UP buc GLGT predictor'] != None: - x_buc = self._ML_buckling[mat_fac]['cl UP buc GLGT scaler'].transform(buckling_ml_input) - y_pred_buc = self._ML_buckling[mat_fac]['cl UP buc GLGT predictor'].predict(x_buc)[0] - else: - y_pred_buc = 0 - if self._ML_buckling[mat_fac]['cl UP ult GLGT predictor'] != None: - x_ult = self._ML_buckling[mat_fac]['cl UP ult GLGT scaler'].transform(buckling_ml_input) - y_pred_ult = self._ML_buckling[mat_fac]['cl UP ult GLGT predictor'].predict(x_ult)[0] + if self._ML_buckling[mat_fac]['cl UP buc GLGT predictor'] is not None: + x_buc = self._ML_buckling[mat_fac]['cl UP buc GLGT scaler'].transform( + buckling_ml_input + ) + y_pred_buc = self._ML_buckling[mat_fac]['cl UP buc GLGT predictor'].predict( + x_buc + )[0] else: - y_pred_ult = 0 + y_pred_buc = 'ML buckling model missing' - x_csr = obj_scnt_calc_pl.get_buckling_ml_input(design_lat_press=design_pressure, csr = True) - x_csr = self._ML_buckling[mat_fac]['CSR scaler UP'].transform(x_csr) - csr_pl = self._ML_buckling[mat_fac]['CSR predictor UP'].predict(x_csr)[0] + if self._ML_buckling[mat_fac]['cl UP ult GLGT predictor'] is not None: + x_ult = self._ML_buckling[mat_fac]['cl UP ult GLGT scaler'].transform( + buckling_ml_input + ) + y_pred_ult = self._ML_buckling[mat_fac]['cl UP ult GLGT predictor'].predict( + x_ult + )[0] + else: + y_pred_ult = 'ML ultimate model missing' - if self._new_material_factor.get() == 1.1: + # ------------------------------------------------------------------------- + # Numeric UF pipeline + # ------------------------------------------------------------------------- + try: + numeric_pred = self._predict_numeric_uf_pipeline( + mat_fac=mat_fac, + sp_or_up='UP', + boundary_type=boundary_type, + buckling_ml_input=buckling_ml_input, + ) + except Exception as e: + numeric_pred = default_numeric_pred.copy() + numeric_pred['valid_label'] = 'numeric ML error' + numeric_pred['error'] = str(e) + + numeric_pred = _apply_material_factor_to_numeric_pred(numeric_pred, mat_fac) + + # ------------------------------------------------------------------------- + # CSR prediction + # ------------------------------------------------------------------------- + try: + x_csr = obj_scnt_calc_pl.get_buckling_ml_input( + design_lat_press=design_pressure, + csr=True, + ) + x_csr = self._ML_buckling[mat_fac]['CSR scaler UP'].transform(x_csr) + csr_pl = self._ML_buckling[mat_fac]['CSR predictor UP'].predict(x_csr)[0] + except Exception: + csr_pl = 0 + + if mat_fac == 1.1: accept = 'below or equal 0.91' else: accept = 'below or equal 0.87' - return_dict['ML buckling colors'][current_line] = \ - {'buckling': 'green' if y_pred_buc == accept else 'red', - 'ultimate': 'green' if y_pred_ult == accept else 'red', - 'CSR requirement': 'green' if csr_pl == 1 else 'red'} + return_dict['ML buckling colors'][current_line] = { + 'buckling': 'green' if y_pred_buc == accept else 'red', + 'ultimate': 'green' if y_pred_ult == accept else 'red', + 'CSR requirement': 'green' if csr_pl == 1 else 'red', + } + + return_dict['ML buckling class'][current_line] = { + 'buckling': str(y_pred_buc) + mat_fac_error, + 'ultimate': str(y_pred_ult) + mat_fac_error, + 'CSR': [csr_pl, float('inf'), float('inf'), float('inf')], + } + + # ------------------------------------------------------------------------- + # Numeric UF color handling + # ------------------------------------------------------------------------- + if numeric_pred.get('valid_prediction', None) == 1: + numeric_buc_color = self._numeric_uf_color( + numeric_pred['buckling_uf'], + mat_fac, + ) + numeric_ult_color = self._numeric_uf_color( + numeric_pred['ultimate_uf'], + mat_fac, + ) + else: + numeric_buc_color = 'red' + numeric_ult_color = 'red' + + return_dict['ML buckling numeric'][current_line] = { + 'buckling UF': numeric_pred['buckling_uf'], + 'ultimate UF': numeric_pred['ultimate_uf'], + 'buckling UF raw': numeric_pred.get('buckling_uf_raw', float('inf')), + 'ultimate UF raw': numeric_pred.get('ultimate_uf_raw', float('inf')), + 'material factor': numeric_pred.get('material_factor', mat_fac), + 'error': numeric_pred['error'], + } + + return_dict['ML buckling numeric valid'][current_line] = { + 'available': numeric_pred['available'], + 'valid prediction': numeric_pred['valid_prediction'], + 'valid label': numeric_pred['valid_label'], + } + + return_dict['ML buckling numeric colors'][current_line] = { + 'buckling': numeric_buc_color, + 'ultimate': numeric_ult_color, + } - return_dict['ML buckling class'][current_line] = {'buckling': y_pred_buc + mat_fac_error, - 'ultimate': y_pred_ult + mat_fac_error, - 'CSR': [csr_pl, float('inf'), - float('inf'), float('inf')]} else: - buckling_ml_input = obj_scnt_calc_stf.get_buckling_ml_input(design_lat_press=design_pressure) - mat_fac = obj_scnt_calc_pl.mat_factor - mat_fac_error = '' - if mat_fac not in [1.1, 1.15]: - mat_fac = 1.15 - mat_fac_error = ' MATERIAL FACTOR MUST BE 1.1 or 1.15 -> using 1.15' - if obj_scnt_calc_stf.get_puls_boundary() == 'Int': - if self._ML_buckling[mat_fac]['cl SP buc int predictor'] != None: - x_buc = self._ML_buckling[mat_fac]['cl SP buc int scaler'].transform(buckling_ml_input) - y_pred_buc = self._ML_buckling[mat_fac]['cl SP buc int predictor'].predict(x_buc)[0] + # ========================================================================= + # SP / stiffened panel ML + # ========================================================================= + + # Defensive guard: SP ML requires a stiffener object. + if obj_scnt_calc_stf is None: + try: + mat_fac = float(self._new_material_factor.get()) + except Exception: + mat_fac = obj_scnt_calc_pl.mat_factor + + if mat_fac not in [1.1, 1.15]: + mat_fac_error = ' MATERIAL FACTOR MUST BE 1.1 or 1.15 -> using 1.15' + mat_fac = 1.15 else: - y_pred_buc = 0 - if self._ML_buckling[mat_fac]['cl SP ult int predictor'] != None: - x_ult = self._ML_buckling[mat_fac]['cl SP ult int scaler'].transform(buckling_ml_input) - y_pred_ult = self._ML_buckling[mat_fac]['cl SP ult int predictor'].predict(x_ult)[0] - else: - y_pred_ult = 0 + mat_fac_error = '' + + return_dict['ML buckling colors'][current_line] = { + 'buckling': 'red', + 'ultimate': 'red', + 'CSR requirement': 'red', + } + + return_dict['ML buckling class'][current_line] = { + 'buckling': 'No stiffener - ML SP not available' + mat_fac_error, + 'ultimate': 'No stiffener - ML SP not available' + mat_fac_error, + 'CSR': [0, 0, 0, 0], + } + + return_dict['ML buckling numeric'][current_line] = { + 'buckling UF': float('inf'), + 'ultimate UF': float('inf'), + 'buckling UF raw': float('inf'), + 'ultimate UF raw': float('inf'), + 'material factor': mat_fac, + 'error': 'No stiffener - numeric ML SP not available', + } + + return_dict['ML buckling numeric valid'][current_line] = { + 'available': False, + 'valid prediction': None, + 'valid label': 'No stiffener - numeric ML SP not available', + } + + return_dict['ML buckling numeric colors'][current_line] = { + 'buckling': 'red', + 'ultimate': 'red', + } + else: - if self._ML_buckling[mat_fac]['cl SP buc GLGT predictor'] != None: - x_buc = self._ML_buckling[mat_fac]['cl SP buc GLGT scaler'].transform(buckling_ml_input) - y_pred_buc = self._ML_buckling[mat_fac]['cl SP buc GLGT predictor'].predict(x_buc)[0] - else: - y_pred_buc = 0 - if self._ML_buckling[mat_fac]['cl SP ult GLGT predictor'] != None: - x_ult = self._ML_buckling[mat_fac]['cl SP ult GLGT scaler'].transform(buckling_ml_input) - y_pred_ult = self._ML_buckling[mat_fac]['cl SP ult GLGT predictor'].predict(x_ult)[0] - else: - y_pred_ult = 0 + buckling_ml_input = obj_scnt_calc_stf.get_buckling_ml_input( + design_lat_press=design_pressure + ) + + try: + mat_fac = float(self._new_material_factor.get()) + except Exception: + mat_fac = obj_scnt_calc_pl.mat_factor + mat_fac_error = '' + + if mat_fac not in [1.1, 1.15]: + mat_fac = 1.15 + mat_fac_error = ' MATERIAL FACTOR MUST BE 1.1 or 1.15 -> using 1.15' + + boundary_type = obj_scnt_calc_stf.get_puls_boundary() + + # --------------------------------------------------------------------- + # Classification ML prediction + # --------------------------------------------------------------------- + if boundary_type == 'Int': + if self._ML_buckling[mat_fac]['cl SP buc int predictor'] is not None: + x_buc = self._ML_buckling[mat_fac]['cl SP buc int scaler'].transform( + buckling_ml_input + ) + y_pred_buc = self._ML_buckling[mat_fac]['cl SP buc int predictor'].predict( + x_buc + )[0] + else: + y_pred_buc = 'ML buckling model missing' + + if self._ML_buckling[mat_fac]['cl SP ult int predictor'] is not None: + x_ult = self._ML_buckling[mat_fac]['cl SP ult int scaler'].transform( + buckling_ml_input + ) + y_pred_ult = self._ML_buckling[mat_fac]['cl SP ult int predictor'].predict( + x_ult + )[0] + else: + y_pred_ult = 'ML ultimate model missing' - x_csr = obj_scnt_calc_stf.get_buckling_ml_input(design_lat_press=design_pressure, csr = True) + else: + if self._ML_buckling[mat_fac]['cl SP buc GLGT predictor'] is not None: + x_buc = self._ML_buckling[mat_fac]['cl SP buc GLGT scaler'].transform( + buckling_ml_input + ) + y_pred_buc = self._ML_buckling[mat_fac]['cl SP buc GLGT predictor'].predict( + x_buc + )[0] + else: + y_pred_buc = 'ML buckling model missing' + + if self._ML_buckling[mat_fac]['cl SP ult GLGT predictor'] is not None: + x_ult = self._ML_buckling[mat_fac]['cl SP ult GLGT scaler'].transform( + buckling_ml_input + ) + y_pred_ult = self._ML_buckling[mat_fac]['cl SP ult GLGT predictor'].predict( + x_ult + )[0] + else: + y_pred_ult = 'ML ultimate model missing' + + # --------------------------------------------------------------------- + # Numeric UF pipeline + # --------------------------------------------------------------------- + try: + numeric_pred = self._predict_numeric_uf_pipeline( + mat_fac=mat_fac, + sp_or_up='SP', + boundary_type=boundary_type, + buckling_ml_input=buckling_ml_input, + ) + except Exception as e: + numeric_pred = default_numeric_pred.copy() + numeric_pred['valid_label'] = 'numeric ML error' + numeric_pred['error'] = str(e) + + numeric_pred = _apply_material_factor_to_numeric_pred(numeric_pred, mat_fac) + + # --------------------------------------------------------------------- + # CSR prediction + # --------------------------------------------------------------------- + try: + x_csr = obj_scnt_calc_stf.get_buckling_ml_input( + design_lat_press=design_pressure, + csr=True, + ) - x_csr = self._ML_buckling[mat_fac]['CSR scaler SP'].transform(x_csr) - csr_pl, csr_web, csr_web_fl, csr_fl = self._ML_buckling[mat_fac]['CSR predictor SP'].predict(x_csr)[0] - if obj_scnt_calc_pl.mat_factor == 1.1: - accept = 'below or equal 0.91' - else: - accept = 'below or equal 0.87' + x_csr = self._ML_buckling[mat_fac]['CSR scaler SP'].transform(x_csr) + csr_pl, csr_web, csr_web_fl, csr_fl = self._ML_buckling[mat_fac][ + 'CSR predictor SP' + ].predict(x_csr)[0] + except Exception: + csr_pl, csr_web, csr_web_fl, csr_fl = 0, 0, 0, 0 + if mat_fac == 1.1: + accept = 'below or equal 0.91' + else: + accept = 'below or equal 0.87' + + return_dict['ML buckling colors'][current_line] = { + 'buckling': 'green' if y_pred_buc == accept else 'red', + 'ultimate': 'green' if y_pred_ult == accept else 'red', + 'CSR requirement': 'green' if all( + [csr_pl == 1, csr_web == 1, csr_web_fl == 1, csr_fl == 1] + ) else 'red', + } + + return_dict['ML buckling class'][current_line] = { + 'buckling': str(y_pred_buc) + mat_fac_error, + 'ultimate': str(y_pred_ult) + mat_fac_error, + 'CSR': [csr_pl, csr_web, csr_web_fl, csr_fl], + } + + # --------------------------------------------------------------------- + # Numeric UF color handling + # --------------------------------------------------------------------- + if numeric_pred.get('valid_prediction', None) == 1: + numeric_buc_color = self._numeric_uf_color( + numeric_pred['buckling_uf'], + mat_fac, + ) + numeric_ult_color = self._numeric_uf_color( + numeric_pred['ultimate_uf'], + mat_fac, + ) + else: + numeric_buc_color = 'red' + numeric_ult_color = 'red' + + return_dict['ML buckling numeric'][current_line] = { + 'buckling UF': numeric_pred['buckling_uf'], + 'ultimate UF': numeric_pred['ultimate_uf'], + 'buckling UF raw': numeric_pred.get('buckling_uf_raw', float('inf')), + 'ultimate UF raw': numeric_pred.get('ultimate_uf_raw', float('inf')), + 'material factor': numeric_pred.get('material_factor', mat_fac), + 'error': numeric_pred['error'], + } + + return_dict['ML buckling numeric valid'][current_line] = { + 'available': numeric_pred['available'], + 'valid prediction': numeric_pred['valid_prediction'], + 'valid label': numeric_pred['valid_label'], + } + + return_dict['ML buckling numeric colors'][current_line] = { + 'buckling': numeric_buc_color, + 'ultimate': numeric_ult_color, + } + + # ------------------------------------------------------------------------- + # SemiAnalytical solver. + # + # Store both raw solver UF and material-factored UF. The factored UF is used + # for display, colors, and utilization checks, matching ML-Numeric behavior. + # ------------------------------------------------------------------------- + try: + semi_analytical_mat_fac = float(self._new_material_factor.get()) + except Exception: + semi_analytical_mat_fac = obj_scnt_calc_pl.mat_factor - return_dict['ML buckling colors'][current_line] = \ - {'buckling': 'green' if y_pred_buc == accept else 'red', - 'ultimate': 'green' if y_pred_ult == accept else 'red', - 'CSR requirement': 'green' if - all([csr_pl == 1, csr_web == 1, csr_web_fl == 1, csr_fl == 1]) else 'red'} - return_dict['ML buckling class'][current_line] = {'buckling': y_pred_buc + mat_fac_error , - 'ultimate': y_pred_ult + mat_fac_error, - 'CSR': [csr_pl, csr_web, csr_web_fl, csr_fl]} + try: + semi_analytical_result = op.semi_analytical.solve_anystructure_panel( + [all_obj, None], + design_pressure, + ) + semi_analytical_valid = int(semi_analytical_result.get('valid_prediction', 0)) == 1 + except Exception as e: + semi_analytical_result = { + 'buckling_usage_factor': float('inf'), + 'ultimate_usage_factor': float('inf'), + 'panel_family': None, + 'confidence': 'low', + } + semi_analytical_valid = False + semi_analytical_error = str(e) + else: + semi_analytical_error = semi_analytical_result.get('invalid_reason') or '' + + semi_analytical_buc_raw = ( + float(semi_analytical_result.get('buckling_usage_factor', float('inf'))) + if semi_analytical_valid else float('inf') + ) + semi_analytical_ult_raw = ( + float(semi_analytical_result.get('ultimate_usage_factor', float('inf'))) + if semi_analytical_valid else float('inf') + ) + semi_analytical_buc_uf = semi_analytical_buc_raw * semi_analytical_mat_fac + semi_analytical_ult_uf = semi_analytical_ult_raw * semi_analytical_mat_fac + semi_analytical_acceptance = 1.0 + + if semi_analytical_valid: + semi_analytical_buc_color = self._semi_analytical_uf_color(semi_analytical_buc_uf) + semi_analytical_ult_color = self._semi_analytical_uf_color(semi_analytical_ult_uf) + semi_analytical_valid_label = semi_analytical_result.get( + 'valid_label', + 'valid SemiAnalytical UF predicted', + ) + else: + semi_analytical_buc_color = 'red' + semi_analytical_ult_color = 'red' + semi_analytical_valid_label = semi_analytical_result.get( + 'valid_label', + 'SemiAnalytical S3/U3 unsupported or invalid', + ) + + return_dict['SemiAnalytical'][current_line] = { + 'buckling UF': semi_analytical_buc_uf, + 'ultimate UF': semi_analytical_ult_uf, + 'buckling UF raw': semi_analytical_buc_raw, + 'ultimate UF raw': semi_analytical_ult_raw, + 'material factor': semi_analytical_mat_fac, + 'acceptance': semi_analytical_acceptance, + 'panel family': semi_analytical_result.get('panel_family', None), + 'confidence': semi_analytical_result.get('confidence', None), + 'error': semi_analytical_error, + } + + return_dict['SemiAnalytical valid'][current_line] = { + 'available': semi_analytical_valid, + 'valid prediction': 1 if semi_analytical_valid else 0, + 'valid label': semi_analytical_valid_label, + } + + return_dict['SemiAnalytical colors'][current_line] = { + 'buckling': semi_analytical_buc_color, + 'ultimate': semi_analytical_ult_color, + } ''' Weight calculations for line. @@ -3212,7 +3691,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False p1 = self._point_dict['point' + str(points[0])] p2 = self._point_dict['point' + str(points[1])] - mid_coord = [(p1[0]+p2[0])/2, (p1[1]+p2[1])/2] + mid_coord = [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2] return_dict['weights'][current_line] = {'line weight': line_weight, 'mid_coord': mid_coord} @@ -3226,7 +3705,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False return_dict['section_modulus'][current_line] = {'sec_mod': sec_mod, 'min_sec_mod': 0} if \ obj_scnt_calc_stf is None else {'sec_mod': sec_mod, 'min_sec_mod': min_sec_mod} return_dict['shear_area'][current_line] = {'shear_area': 0, 'min_shear_area': 0} if \ - obj_scnt_calc_stf is None else{'shear_area': shear_area, 'min_shear_area': min_shear} + obj_scnt_calc_stf is None else {'shear_area': shear_area, 'min_shear_area': min_shear} return_dict['thickness'][current_line] = {'thk': obj_scnt_calc_pl.get_pl_thk(), 'min_thk': min_thk} return_dict['fatigue_obj'][current_line] = fatigue_obj return_dict['color code'][current_line] = {} @@ -3234,7 +3713,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False if fatigue_obj is not None: return_dict['fatigue'][current_line] = {'damage': damage, 'dff': dff, 'curve': fatigue_obj.get_sn_curve()} - rec_for_color[current_line]['fatigue'] = damage*dff + rec_for_color[current_line]['fatigue'] = damage * dff else: return_dict['fatigue'][current_line] = {'damage': None, 'dff': None, 'curve': None} rec_for_color[current_line]['fatigue'] = 0 @@ -3245,21 +3724,44 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False sec_util = 0 if min(sec_mod) == 0 else min_sec_mod / min(sec_mod) buc_util = 1 if float('inf') in buckling else max(all_buckling_uf_list) rec_for_color[current_line]['rp buckling'] = max(all_buckling_uf_list) + selected_buckling_method = self._new_buckling_method.get() + semi_analytical_util = buc_util + if return_dict['SemiAnalytical valid'][current_line].get('valid prediction', None) == 1: + if op._puls_selected_method(obj_scnt_calc_pl.get_puls_method()) == 'ultimate': + semi_analytical_util = return_dict['SemiAnalytical'][current_line]['ultimate UF'] + else: + semi_analytical_util = return_dict['SemiAnalytical'][current_line]['buckling UF'] + + numeric_util = buc_util + if return_dict['ML buckling numeric valid'][current_line].get('valid prediction', None) == 1: + if op._puls_selected_method(obj_scnt_calc_pl.get_puls_method()) == 'ultimate': + numeric_util = return_dict['ML buckling numeric'][current_line]['ultimate UF'] + else: + numeric_util = return_dict['ML buckling numeric'][current_line]['buckling UF'] + + active_buckling_util = buc_util + if selected_buckling_method == 'SemiAnalytical S3/U3': + active_buckling_util = semi_analytical_util + elif selected_buckling_method == 'ML-Numeric (PULS based)': + active_buckling_util = numeric_util + return_dict['utilization'][current_line] = {'buckling': buc_util, - 'PULS buckling': buc_util, + 'SemiAnalytical buckling': semi_analytical_util, + 'ML-Numeric buckling': numeric_util, + 'active buckling': active_buckling_util, 'fatigue': fat_util, 'section': sec_util, 'shear': shear_util, 'thickness': thk_util} # Color coding state - self._state_logger[current_line] = return_dict # Logging the current state of the line. + self._state_logger[current_line] = return_dict # Logging the current state of the line. self._line_to_struc[current_line][0].need_recalc = False else: pass - sec_in_model, idx, recorded_sections = dict(), 0, list() - cyl_sec_in_model, idx_cyl, recorded_cyl_sections = dict(), 0, list() + sec_in_model, idx, recorded_sections = dict(), 0, list() + cyl_sec_in_model, idx_cyl, recorded_cyl_sections = dict(), 0, list() for data in self._line_to_struc.values(): if data[0].Stiffener is not None: @@ -3270,7 +3772,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False if data[5] is not None: if data[5].LongStfObj is not None: if data[5].LongStfObj.get_beam_string() not in recorded_cyl_sections: - cyl_sec_in_model[ data[5].LongStfObj.get_beam_string()] = idx_cyl + cyl_sec_in_model[data[5].LongStfObj.get_beam_string()] = idx_cyl recorded_cyl_sections.append(data[5].LongStfObj.get_beam_string()) idx_cyl += 1 @@ -3278,17 +3780,16 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False cyl_sec_in_model['length'] = len(recorded_cyl_sections) if self._line_to_struc != {}: - sec_mod_map = np.arange(0,1.1,0.1) - fat_map = np.arange(0,1.1,0.1) + sec_mod_map = np.arange(0, 1.1, 0.1) + fat_map = np.arange(0, 1.1, 0.1) all_thicknesses = [round(objs[0].Plate.get_pl_thk(), 5) for objs in self._line_to_struc.values()] all_thicknesses = np.unique(all_thicknesses).tolist() - - + thickest_plate = max(all_thicknesses) if len(all_thicknesses) > 1: thk_map = np.arange(min(all_thicknesses), max(all_thicknesses) + (max(all_thicknesses) - min(all_thicknesses)) / 10, - (max(all_thicknesses) - min(all_thicknesses)) / 10) + (max(all_thicknesses) - min(all_thicknesses)) / 10) else: thk_map = all_thicknesses @@ -3303,7 +3804,6 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False # else: # thk_map_cyl = [1,] - try: all_pressures = sorted([self.get_highest_pressure(line)['normal'] for line in list(self._line_dict.keys())]) @@ -3321,27 +3821,26 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False else: press_map = all_pressures - all_utils = [max(list(return_dict['utilization'][line].values())) - for line in self._line_to_struc.keys()] + all_utils = [] + for line in self._line_to_struc.keys(): + if self._line_to_struc[line][5] is not None: + all_utils.append(self._cylinder_buckling_uf(return_dict['cylinder'].get(line, {}))) + else: + all_utils.append( + return_dict['utilization'][line].get( + 'active buckling', + max(list(return_dict['utilization'][line].values())), + ) + ) all_utils = np.unique(all_utils).tolist() - if len(all_utils) >1: - util_map = np.arange(0, 1.1, 0.1) + if len(all_utils) > 1: + util_map = np.arange(0, 1.1, 0.1) else: util_map = all_utils - if self._PULS_results is not None: - #puls_util_map = self._PULS_results.all_uf - puls_util_map = list() - for key, val in self._line_to_struc.items(): - puls_util_map.append(self._PULS_results.get_utilization(key, val[0].Plate.get_puls_method(), - acceptance = self._new_puls_uf.get())) - puls_util_map = np.arange(0, 1.1, 0.1) - else: - puls_util_map = None - sig_x = np.unique([self._line_to_struc[line][0].Plate.sigma_x1 for line in self._line_to_struc.keys()]).tolist() - if len(sig_x) > 1: # TODO color coding when using sig_x1 and sig_x2 (23.12.2021) + if len(sig_x) > 1: # TODO color coding when using sig_x1 and sig_x2 (23.12.2021) sig_x_map = np.arange(min(sig_x), max(sig_x) + (max(sig_x) - min(sig_x)) / 10, (max(sig_x) - min(sig_x)) / 10) else: @@ -3383,9 +3882,9 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False 'section modulus map': sec_mod_map, 'fatigue map': fat_map, 'highest pressure': highest_pressure, 'lowest pressure': lowest_pressure, - 'pressure map': press_map, 'all pressures':all_pressures, + 'pressure map': press_map, 'all pressures': all_pressures, + 'buckling method': self._new_buckling_method.get(), 'all utilizations': all_utils, 'utilization map': util_map, - 'PULS utilization map': puls_util_map, 'max sigma x': max(sig_x), 'min sigma x': min(sig_x), 'sigma x map': sig_x_map, 'max sigma y1': max(sig_y1), 'min sigma y1': min(sig_y1), 'sigma y1 map': sig_y1_map, @@ -3396,28 +3895,17 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False 'sections in model': sec_in_model, 'cyl sections in model': cyl_sec_in_model, 'recorded sections': recorded_sections, - 'recorded cylinder long sections' : recorded_cyl_sections, + 'recorded cylinder long sections': recorded_cyl_sections, 'spacings': spacing, 'max spacing': max(spacing), 'min spacing': min(spacing)} - line_color_coding, puls_method_map, puls_sp_or_up_map = \ - {}, {None: 0, 'buckling': 0.5, 'ultimate': 1}, {None:0, 'SP': 0.5, 'UP': 1} + line_color_coding = {} cmap_sections = plt.get_cmap('jet') thk_sort_unique = return_dict['color code']['all thicknesses'] spacing_sort_unique = return_dict['color code']['spacings'] structure_type_unique = return_dict['color code']['structure types map'] - tot_weight, weight_mult_dist_x, weight_mult_dist_y = 0, 0,0 + tot_weight, weight_mult_dist_x, weight_mult_dist_y = 0, 0, 0 for line, line_data in self._line_to_struc.items(): - if self._PULS_results is None: - puls_color, buc_uf, puls_uf, puls_method, puls_sp_or_up = 'black', 0, 0, None, None - elif self._PULS_results.get_utilization(line, self._line_to_struc[line][0].Plate.get_puls_method(), - self._new_puls_uf.get()) == None: - puls_color, buc_uf, puls_uf, puls_method, puls_sp_or_up = 'black', 0,0, None, None - else: - puls_method = self._line_to_struc[line][0].Plate.get_puls_method() - puls_uf = self._PULS_results.get_utilization( - line, puls_method, - self._new_puls_uf.get()) - puls_color = matplotlib.colors.rgb2hex(cmap_sections(puls_uf)) - puls_sp_or_up = self._line_to_struc[line][0].Plate.get_puls_sp_or_up() + puls_method = line_data[0].Plate.get_puls_method() + puls_sp_or_up = line_data[0].Plate.get_puls_sp_or_up() # Cylinders if self._line_to_struc[line][5] is not None: @@ -3441,14 +3929,7 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False # cyl_sigma_hoop = cyl_obj.shsd / 1e6 cyl_results = cyl_obj.get_utilization_factors() - cyl_uf = max([round(0 if cyl_results['Unstiffened shell'] is None else - cyl_results['Unstiffened shell'],2), - round(0 if cyl_results['Longitudinal stiffened shell'] is None else - cyl_results['Longitudinal stiffened shell'],2), - round(0 if cyl_results['Ring stiffened shell'] is None else - cyl_results['Ring stiffened shell'],2), - round(0 if cyl_results['Heavy ring frame'] is None else - cyl_results['Heavy ring frame'],2)]) + cyl_uf = self._cylinder_buckling_uf(cyl_results) else: cyl_uf = 0 cyl_long_str = None @@ -3459,9 +3940,6 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False tot_uf_rp = max([rec_for_color[line]['fatigue'], rp_uf, rec_for_color[line]['section modulus'], rec_for_color[line]['shear'], rec_for_color[line]['plate thickness']]) - tot_uf_puls = max([rec_for_color[line]['fatigue'], puls_uf, - rec_for_color[line]['section modulus'], rec_for_color[line]['shear'], - rec_for_color[line]['plate thickness']]) try: this_pressure = self.get_highest_pressure(line)['normal'] except KeyError: @@ -3471,82 +3949,79 @@ def get_color_and_calc_state(self, current_line = None, active_line_only = False res = list() for stress_list, this_stress in zip([sig_x, sig_y1, sig_y2, tau_xy], - [line_data[0].Plate.sigma_x1, line_data[0].Plate.sigma_y1, - line_data[0].Plate.sigma_y2, line_data[0].Plate.tau_xy]): - if type(stress_list)==float: + [line_data[0].Plate.sigma_x1, line_data[0].Plate.sigma_y1, + line_data[0].Plate.sigma_y2, line_data[0].Plate.tau_xy]): + if type(stress_list) == float: res.append(1) elif len(stress_list) == 1: res.append(1) elif max(stress_list) == 0 and min(stress_list) == 0: res.append(0) elif this_stress < 0: - res.append(this_stress /min(stress_list)) + res.append(this_stress / min(stress_list)) elif this_stress >= 0: - res.append(this_stress/ max(stress_list)) + res.append(this_stress / max(stress_list)) - sig_x_uf, sig_y1_uf, sig_y2_uf , tau_xy_uf = res + sig_x_uf, sig_y1_uf, sig_y2_uf, tau_xy_uf = res if type(all_cyl_thk) is not list: all_cyl_thk = all_cyl_thk.tolist() line_color_coding[line] = {'plate': matplotlib.colors.rgb2hex(cmap_sections( - thk_sort_unique.index(round(line_data[0].Plate.get_pl_thk(),10))/len(thk_sort_unique))), - 'spacing': 'black' if line_data[0].Stiffener is None else matplotlib.colors.rgb2hex( - cmap_sections(spacing_sort_unique.index(round(line_data[0].Stiffener - .get_s(), 10)) / len( - spacing_sort_unique))), - 'section': 'black' if line_data[0].Stiffener is None else - matplotlib.colors.rgb2hex(cmap_sections(sec_in_model[line_data[0] - .Stiffener.get_beam_string()]/ - len(list(recorded_sections)))), - 'section cyl': 'black' if cyl_long_str is None else - matplotlib.colors.rgb2hex(cmap_sections(cyl_sec_in_model[cyl_long_str] / - len(list(recorded_cyl_sections)))), - 'structure type': matplotlib.colors.rgb2hex( - cmap_sections(structure_type_unique.index(line_data[0].Plate.get_structure_type()) - /len(structure_type_unique))), - 'pressure color': 'black' if all_pressures in [[0],[0,1]] else matplotlib.colors.rgb2hex(cmap_sections( - this_pressure/highest_pressure)), - 'pressure': this_pressure, - 'rp uf color': matplotlib.colors.rgb2hex(cmap_sections(rp_util)), - 'rp uf': rp_util, - 'PULS method': puls_method, - 'PULS sp or up':puls_sp_or_up, - 'section modulus color': matplotlib.colors.rgb2hex( - cmap_sections(rec_for_color[line]['section modulus'])), - 'fatigue color': matplotlib.colors.rgb2hex( - cmap_sections(rec_for_color[line]['fatigue'])), - 'Total uf color rp' : matplotlib.colors.rgb2hex( - cmap_sections(tot_uf_rp)), - 'Total uf rp': tot_uf_rp, - 'Total uf color puls': matplotlib.colors.rgb2hex( - cmap_sections(tot_uf_puls)), - 'Total uf puls': tot_uf_puls, - 'PULS uf': round(puls_uf,2), - 'PULS uf color': puls_color, - 'fatigue uf' : rec_for_color[line]['fatigue'], - 'section uf' : rec_for_color[line]['section modulus'], - 'sigma x': matplotlib.colors.rgb2hex(cmap_sections(sig_x_uf)), - 'sigma y1': matplotlib.colors.rgb2hex(cmap_sections(sig_y1_uf)), - 'sigma y2': matplotlib.colors.rgb2hex(cmap_sections(sig_y2_uf)), - 'tau xy':matplotlib.colors.rgb2hex(cmap_sections(tau_xy_uf)), + thk_sort_unique.index(round(line_data[0].Plate.get_pl_thk(), 10)) / len(thk_sort_unique))), + 'spacing': 'black' if line_data[0].Stiffener is None else matplotlib.colors.rgb2hex( + cmap_sections(spacing_sort_unique.index(round(line_data[0].Stiffener + .get_s(), 10)) / len( + spacing_sort_unique))), + 'section': 'black' if line_data[0].Stiffener is None else + matplotlib.colors.rgb2hex(cmap_sections(sec_in_model[line_data[0] + .Stiffener.get_beam_string()] / + len(list(recorded_sections)))), + 'section cyl': 'black' if cyl_long_str is None else + matplotlib.colors.rgb2hex(cmap_sections(cyl_sec_in_model[cyl_long_str] / + len(list(recorded_cyl_sections)))), + 'structure type': matplotlib.colors.rgb2hex( + cmap_sections(structure_type_unique.index(line_data[0].Plate.get_structure_type()) + / len(structure_type_unique))), + 'pressure color': 'black' if all_pressures in [[0], [0, 1]] else matplotlib.colors.rgb2hex( + cmap_sections( + this_pressure / highest_pressure)), + 'pressure': this_pressure, + 'rp uf color': matplotlib.colors.rgb2hex(cmap_sections(rp_util)), + 'rp uf': rp_util, + 'Buckling method': puls_method, + 'Buckling SP/UP': puls_sp_or_up, + 'section modulus color': matplotlib.colors.rgb2hex( + cmap_sections(rec_for_color[line]['section modulus'])), + 'fatigue color': matplotlib.colors.rgb2hex( + cmap_sections(rec_for_color[line]['fatigue'])), + 'Total uf color rp': matplotlib.colors.rgb2hex( + cmap_sections(tot_uf_rp)), + 'Total uf rp': tot_uf_rp, + 'fatigue uf': rec_for_color[line]['fatigue'], + 'section uf': rec_for_color[line]['section modulus'], + 'sigma x': matplotlib.colors.rgb2hex(cmap_sections(sig_x_uf)), + 'sigma y1': matplotlib.colors.rgb2hex(cmap_sections(sig_y1_uf)), + 'sigma y2': matplotlib.colors.rgb2hex(cmap_sections(sig_y2_uf)), + 'tau xy': matplotlib.colors.rgb2hex(cmap_sections(tau_xy_uf)), 'cylinder uf': matplotlib.colors.rgb2hex(cmap_sections(cyl_uf)), - 'cylinder plate' : matplotlib.colors.rgb2hex - (cmap_sections(0 if cyl_thickness is None else all_cyl_thk.index(cyl_thickness)/len(all_cyl_thk))) + 'cylinder uf value': cyl_uf, + 'cylinder plate': matplotlib.colors.rgb2hex + (cmap_sections(0 if cyl_thickness is None else all_cyl_thk.index(cyl_thickness) / len(all_cyl_thk))) - } + } return_dict['color code']['lines'] = line_color_coding # COG calculations # Steel tot_weight += return_dict['weights'][line]['line weight'] - weight_mult_dist_x += return_dict['weights'][line]['line weight']\ - *return_dict['weights'][line]['mid_coord'][0] - weight_mult_dist_y += return_dict['weights'][line]['line weight']\ - *return_dict['weights'][line]['mid_coord'][1] + weight_mult_dist_x += return_dict['weights'][line]['line weight'] \ + * return_dict['weights'][line]['mid_coord'][0] + weight_mult_dist_y += return_dict['weights'][line]['line weight'] \ + * return_dict['weights'][line]['mid_coord'][1] - tot_cog = [weight_mult_dist_x/tot_weight, weight_mult_dist_y/tot_weight] + tot_cog = [weight_mult_dist_x / tot_weight, weight_mult_dist_y / tot_weight] else: - tot_cog = [0,0] + tot_cog = [0, 0] tot_weight = 0 return_dict['COG'] = tot_cog @@ -3720,41 +4195,56 @@ def draw_canvas(self, state = None, event = None): all_cyl_chks.append(stf_val) color = 'green' if all(all_cyl_chks) else 'red' - elif self._new_buckling_method.get() == 'DNV PULS': - if 'black' in state['PULS colors'][line].values(): - color = 'black' - else: - col1, col2 = state['PULS colors'][line]['buckling'], \ - state['PULS colors'][line]['ultimate'] - - if self._line_to_struc[line][0].Plate.get_puls_method() == 'buckling': - color = 'red' if any([col1 == 'red', col2 == 'red']) else 'green' - else: - color = col2 - - if color == 'green': - color = 'green' if all([state['colors'][line][key] == 'green' for key in - ['fatigue', 'section', 'shear','thickness']]) else 'red' - elif self._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': color = 'red' if 'red' in state['colors'][line].values() else 'green' - elif self._new_buckling_method.get() == 'ML-CL (PULS based)': - if 'black' in state['ML buckling colors'][line].values(): + elif self._new_buckling_method.get() == 'SemiAnalytical S3/U3': + semi_analytical_valid = state.get('SemiAnalytical valid', {}).get(line, {}) + semi_analytical_colors = state.get('SemiAnalytical colors', {}).get(line, {}) + + if semi_analytical_valid.get('valid prediction', None) != 1: + color = 'red' + else: + puls_method = op._puls_selected_method( + self._line_to_struc[line][0].Plate.get_puls_method() + ) + if puls_method == 'ultimate': + color = semi_analytical_colors.get('ultimate', 'red') + else: + color = semi_analytical_colors.get('buckling', 'red') + + if color == 'green': + color = 'green' if all([ + state['colors'][line][key] == 'green' + for key in ['fatigue', 'section', 'shear', 'thickness'] + ]) else 'red' + elif self._new_buckling_method.get() in [ + 'ML-Numeric (PULS based)', + ]: + ml_color_dict = state.get('ML buckling numeric colors', {}).get(line, {}) + + if ml_color_dict == {}: + color = 'black' + elif 'black' in ml_color_dict.values(): color = 'black' else: - col1, col2 = state['ML buckling colors'][line]['buckling'], \ - state['ML buckling colors'][line]['ultimate'] + col_buc = ml_color_dict.get('buckling', 'black') + col_ult = ml_color_dict.get('ultimate', 'black') if self._line_to_struc[line][0].Plate.get_puls_method() == 'buckling': - color = col1 + color = col_buc else: - color = col2 + color = col_ult + # If the selected ML check is green, keep the existing + # fatigue/section/shear/thickness checks as additional + # requirements for the displayed line color. if color == 'green': - color = 'green' if all([state['colors'][line][key] == 'green' for key in - ['fatigue', 'section', 'shear','thickness']]) else 'red' + color = 'green' if all([ + state['colors'][line][key] == 'green' + for key in ['fatigue', 'section', 'shear', 'thickness'] + ]) else 'red' - except (KeyError, TypeError): + except (KeyError, TypeError, AttributeError): color = 'black' elif chk_box_active and state != None and self._line_to_struc != {}: color = self.color_code_line(state, line, coord1, [coord2[0] - coord1[0], coord2[1] - coord1[1]]) @@ -3890,21 +4380,19 @@ def color_code_text(self, state): fill=matplotlib.colors.rgb2hex(cmap_sections(0 if highest_pressure == 0 else press/highest_pressure)), anchor="nw") - elif all([self._new_colorcode_utilization.get() == True, - self._line_to_struc != {}, self._new_buckling_method.get() != 'DNV PULS']): + elif self._new_colorcode_utilization.get() == True and self._line_to_struc != {}: all_utils = cc_state['utilization map'] - for idx, uf in enumerate(cc_state['utilization map']): - self._main_canvas.create_text(11, start_text_shift + 20 * idx, text=str('UF = ' +str(round(uf,1))), - font=self._text_size["Text 10 bold"], - fill='black', - anchor="nw") - self._main_canvas.create_text(10, start_text + 20 * idx, text=str('UF = ' +str(round(uf,1))), - font=self._text_size["Text 10 bold"], - fill=matplotlib.colors.rgb2hex(cmap_sections(uf/max(all_utils))), - anchor="nw") - elif all([self._new_colorcode_utilization.get() == True, - self._line_to_struc != {}, self._new_buckling_method == 'DNV PULS']): - all_utils = cc_state['PULS utilization map'] + method_text = cc_state.get('buckling method', self._new_buckling_method.get()) + if method_text == 'SemiAnalytical S3/U3': + title_text = 'SemiAnalytical UF' + elif method_text == 'ML-Numeric (PULS based)': + title_text = 'ML-Numeric UF' + else: + title_text = 'Buckling UF' + self._main_canvas.create_text(10, start_text - 20, text=title_text, + font=self._text_size["Text 10 bold"], + fill=self._color_text, + anchor="nw") for idx, uf in enumerate(cc_state['utilization map']): self._main_canvas.create_text(11, start_text_shift + 20 * idx, text=str('UF = ' +str(round(uf,1))), font=self._text_size["Text 10 bold"], @@ -4021,6 +4509,16 @@ def color_code_text(self, state): def color_code_line(self, state, line, coord1, vector): cc_state = state['color code'] + cmap_sections = plt.get_cmap('jet') + + def utilization_color(uf): + try: + uf = float(uf) + max_uf = max(cc_state.get('utilization map', [uf, 1.0])) + max_uf = 1.0 if max_uf == 0 else float(max_uf) + return matplotlib.colors.rgb2hex(cmap_sections(uf / max_uf)) + except Exception: + return 'black' if line not in state['color code']['lines'].keys(): return 'black' @@ -4089,15 +4587,10 @@ def color_code_line(self, state, line, coord1, vector): color = 'black' this_text = 'N/A' elif self._line_to_struc[line][5] is not None: - cyl_obj = self._line_to_struc[line][5] - results = cyl_obj.get_utilization_factors() - ufs = [round(0 if results['Unstiffened shell'] is None else results['Unstiffened shell'], 2), - round(0 if results['Longitudinal stiffened shell'] is None else results['Longitudinal stiffened shell'],2), - round(0 if results['Ring stiffened shell'] is None else results['Ring stiffened shell'], 2), - round(0 if results['Heavy ring frame'] is None else results['Heavy ring frame'], 2)] - - color = state['color code']['lines'][line]['cylinder uf'] - this_text = str(max(ufs)) + results = state.get('cylinder', {}).get(line, self._line_to_struc[line][5].get_utilization_factors()) + cyl_uf = self._cylinder_buckling_uf(results) + color = utilization_color(cyl_uf) + this_text = str(round(cyl_uf, 3)) if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, text=this_text) @@ -4107,22 +4600,69 @@ def color_code_line(self, state, line, coord1, vector): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, text=round(state['color code']['lines'][line]['rp uf'],2)) - elif self._new_colorcode_utilization.get() == True and self._new_buckling_method.get() == 'DNV PULS': - if self._line_to_struc[line][5] is not None: - color = 'grey' - this_text = 'N/A' + elif self._new_colorcode_utilization.get() == True and self._new_buckling_method.get() == 'SemiAnalytical S3/U3': + if line in self._line_to_struc and self._line_to_struc[line][5] is not None: + results = state.get('cylinder', {}).get(line, self._line_to_struc[line][5].get_utilization_factors()) + cyl_uf = self._cylinder_buckling_uf(results) + color = utilization_color(cyl_uf) + this_text = str(round(cyl_uf, 3)) else: - color = state['color code']['lines'][line]['PULS uf color'] - this_text = round(state['color code']['lines'][line]['PULS uf'],2) + semi_analytical = state.get('SemiAnalytical', {}).get(line, {}) + semi_analytical_valid = state.get('SemiAnalytical valid', {}).get(line, {}) + + if semi_analytical_valid.get('valid prediction', None) == 1: + puls_method = state['color code']['lines'][line].get('Buckling method', None) + puls_method = op._puls_selected_method(puls_method) + + if puls_method == 'ultimate': + uf = semi_analytical.get('ultimate UF', float('inf')) + color = utilization_color(uf) + this_text = 'ult UF=' + str(round(uf, 3)) + else: + uf = semi_analytical.get('buckling UF', float('inf')) + color = utilization_color(uf) + this_text = 'buc UF=' + str(round(uf, 3)) + else: + color = 'red' + this_text = semi_analytical_valid.get('valid label', 'invalid') + if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, text=this_text) + elif self._new_colorcode_utilization.get() == True and self._new_buckling_method.get() == 'ML-Numeric (PULS based)': + if line in self._line_to_struc and self._line_to_struc[line][5] is not None: + results = state.get('cylinder', {}).get(line, self._line_to_struc[line][5].get_utilization_factors()) + cyl_uf = self._cylinder_buckling_uf(results) + color = utilization_color(cyl_uf) + this_text = str(round(cyl_uf, 3)) + else: + numeric = state.get('ML buckling numeric', {}).get(line, {}) + numeric_valid = state.get('ML buckling numeric valid', {}).get(line, {}) + + if numeric_valid.get('valid prediction', None) == 1: + puls_method = state['color code']['lines'][line].get('Buckling method', None) + + if puls_method == 'buckling': + uf = numeric.get('buckling UF', float('inf')) + color = utilization_color(uf) + this_text = 'buc UF=' + str(round(uf, 3)) + elif puls_method == 'ultimate': + uf = numeric.get('ultimate UF', float('inf')) + color = utilization_color(uf) + this_text = 'ult UF=' + str(round(uf, 3)) + else: + buc_uf = numeric.get('buckling UF', float('inf')) + ult_uf = numeric.get('ultimate UF', float('inf')) + uf = max(buc_uf, ult_uf) + color = utilization_color(uf) + this_text = 'UF=' + str(round(uf, 3)) + else: + color = 'red' + this_text = numeric_valid.get('valid label', 'invalid/NaN') - elif self._new_colorcode_utilization.get() == True and self._new_buckling_method.get() == 'ML-CL (PULS based)': - color = 'black' if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, - text='N/A') + text=this_text) elif self._new_colorcode_sigmax.get() == True: if self._line_to_struc[line][5] is not None: @@ -4212,255 +4752,109 @@ def color_code_line(self, state, line, coord1, vector): elif self._new_colorcode_total.get() == True: if self._line_to_struc[line][5] is not None: - cyl_obj = self._line_to_struc[line][5] - results = cyl_obj.get_utilization_factors() - ufs = [round(0 if results['Unstiffened shell'] is None else results['Unstiffened shell'], 2), - round(0 if results['Longitudinal stiffened shell'] is None else results['Longitudinal stiffened shell'],2), - round(0 if results['Ring stiffened shell'] is None else results['Ring stiffened shell'], 2), - round(0 if results['Heavy ring frame'] is None else results['Heavy ring frame'], 2)] - - color = state['color code']['lines'][line]['cylinder uf'] - this_text = str(max(ufs)) + results = state.get('cylinder', {}).get(line, self._line_to_struc[line][5].get_utilization_factors()) + cyl_uf = self._cylinder_buckling_uf(results) + color = utilization_color(cyl_uf) + this_text = str(round(cyl_uf, 3)) if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, text=this_text) - elif self._new_buckling_method.get() == 'DNV PULS': - color = state['color code']['lines'][line]['Total uf color puls'] - if self._new_label_color_coding.get(): - self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, - text=round(state['color code']['lines'][line]['Total uf puls'],2)) elif self._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': color = state['color code']['lines'][line]['Total uf color rp'] if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, text=round(state['color code']['lines'][line]['Total uf rp'],2)) - elif self._new_buckling_method.get() == 'ML-CL (PULS based)': - color = 'black' + elif self._new_buckling_method.get() == 'SemiAnalytical S3/U3': + semi_analytical = state.get('SemiAnalytical', {}).get(line, {}) + semi_analytical_valid = state.get('SemiAnalytical valid', {}).get(line, {}) + semi_analytical_colors = state.get('SemiAnalytical colors', {}).get(line, {}) + + if semi_analytical_valid.get('valid prediction', None) == 1: + buc_uf = semi_analytical.get('buckling UF', float('inf')) + ult_uf = semi_analytical.get('ultimate UF', float('inf')) + total_uf = max(buc_uf, ult_uf) + + color = 'green' if all([ + semi_analytical_colors.get('buckling', 'red') == 'green', + semi_analytical_colors.get('ultimate', 'red') == 'green' + ]) else 'red' + this_text = 'max UF=' + str(round(total_uf, 3)) + else: + color = 'red' + this_text = semi_analytical_valid.get('valid label', 'invalid') + + if self._new_label_color_coding.get(): + self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, + text=this_text) + elif self._new_buckling_method.get() == 'ML-Numeric (PULS based)': + numeric = state.get('ML buckling numeric', {}).get(line, {}) + numeric_valid = state.get('ML buckling numeric valid', {}).get(line, {}) + numeric_colors = state.get('ML buckling numeric colors', {}).get(line, {}) + + if numeric_valid.get('valid prediction', None) == 1: + buc_uf = numeric.get('buckling UF', float('inf')) + ult_uf = numeric.get('ultimate UF', float('inf')) + total_uf = max(buc_uf, ult_uf) + + color = 'green' if all([ + numeric_colors.get('buckling', 'red') == 'green', + numeric_colors.get('ultimate', 'red') == 'green' + ]) else 'red' + this_text = 'max UF=' + str(round(total_uf, 3)) + else: + color = 'red' + this_text = numeric_valid.get('valid label', 'invalid/NaN') + if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, - text='N/A') + text=this_text) elif self._new_colorcode_puls_acceptance.get(): - if state['color code']['lines'][line]['PULS method'] == None: + buckling_method = state['color code']['lines'][line].get('Buckling method', None) + if buckling_method == None: color = 'black' else: - color = 'blue' if state['color code']['lines'][line]['PULS method'] == 'ultimate' else 'red' + color = 'blue' if buckling_method == 'ultimate' else 'red' if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, - text=state['color code']['lines'][line]['PULS method']) + text=buckling_method) elif self._new_colorcode_puls_sp_or_up.get(): - if state['color code']['lines'][line]['PULS sp or up'] == None: + buckling_sp_or_up = state['color code']['lines'][line].get('Buckling SP/UP', None) + if buckling_sp_or_up == None: color = 'black' else: - color = 'blue' if state['color code']['lines'][line]['PULS sp or up'] == 'SP' else 'red' + color = 'blue' if buckling_sp_or_up == 'SP' else 'red' if self._new_label_color_coding.get(): self._main_canvas.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 - 10, - text=state['color code']['lines'][line]['PULS sp or up']) + text=buckling_sp_or_up) else: color = 'black' return color - def draw_prop(self, event = None): + def draw_results(self, state = None): + ''' + The properties canvas is created here. + state = {'colors': {}, 'section_modulus': {}, 'thickness': {}, 'shear_area': {}, 'buckling': {}, + 'fatigue': {}, 'pressure_uls': {}, 'pressure_fls': {}, + 'all_obj': {}, 'scant_calc_obj': {}, 'fatigue_obj': {}} + :return: ''' - Prints the properties of the selected line to the bottom canvas. - properties for line dicitonary: + self._result_canvas.delete('all') - name of line : [ Structure class, calc scantling class, calc fatigue class, [load classes] ] + if state is None or self._active_line not in state['all_obj'].keys(): + return - ''' + if self._line_is_active: + x, y, dx, dy = 0, 5, 15, 17 - self._prop_canvas.delete('all') - canvas_width = self._prop_canvas.winfo_width() - canvas_height = self._prop_canvas.winfo_height() + if self._active_line in self._line_to_struc and self._line_to_struc[self._active_line][5] is None: - def checkered(line_distance, canvas): - ''' - Grid lines in the properties canvas. - :param line_distance: - :return: - ''' - # vertical lines at an interval of "line_distance" pixel - for x in range(line_distance, canvas_width, line_distance): - canvas.create_line(x, 0, x, canvas_width, stipple='gray50', activestipple='gray75') - # horizontal lines at an interval of "line_distance" pixel - for y in range(line_distance, canvas_height, line_distance): - canvas.create_line(0, y, canvas_width, y, stipple='gray50', activestipple='gray75') - - if self._active_line in self._line_to_struc: - self.set_selected_variables(self._active_line) - # printing the properties to the active line - if self._line_is_active and self._line_to_struc[self._active_line][5] is None: - #checkered(10, self._prop_canvas) - self._prop_canvas.create_text([canvas_width/2-canvas_width/20, canvas_height/20], - text ='SELECTED: '+str(self._active_line), - font=self._text_size["Text 10 bold"], fill='red') - if all([self._line_to_struc[self._active_line][0].Stiffener is None, - self._line_to_struc[self._active_line][0].Girder is None]): - structure_obj = self._line_to_struc[self._active_line][0].Plate - spacing = structure_obj.get_s() * self._prop_canvas_scale * 3 - plate_thk = structure_obj.get_pl_thk() * self._prop_canvas_scale * 3 - startx = 20 - starty = 225 - self._prop_canvas.create_text([startx + 100, 50], - text='Plate with thickness ' + - str(structure_obj.get_pl_thk()*1000) + ' mm' , - font=self._text_size["Text 10 bold"], fill='Black') - self._prop_canvas.create_rectangle(startx + spacing, - starty, - startx + spacing + spacing, - starty - plate_thk, - fill='grey', activefill='yellow') - - for idx, structure_obj in enumerate([self._line_to_struc[self._active_line][0].Stiffener, - self._line_to_struc[self._active_line][0].Girder]): - mult = 1 if self._line_to_struc[self._active_line][0].Girder is not None else 2 # *(400/max_web) - thk_mult = 2 # *(400/max_web) - startx = 100 + 300 * idx - starty = 225 - - if structure_obj is not None: - self._prop_canvas.create_text([startx +60, 50], - text='Stiffener\n' +structure_obj.get_beam_string() - if idx == 0 else 'Girder\n' + structure_obj.get_beam_string(), - font=self._text_size["Text 9 bold"], fill='Black') - if structure_obj is not None: - self._prop_canvas.create_text([100, 20], - text='Thickness scale x 2', - font=self._text_size["Text 10 bold"], fill='grey') - # drawing stiffener - spacing = structure_obj.get_s()*self._prop_canvas_scale * mult - stf_web_height = structure_obj.get_web_h()*self._prop_canvas_scale * mult - stf_flange_width = structure_obj.get_fl_w() *self._prop_canvas_scale * mult - plate_thk = structure_obj.get_pl_thk()*self._prop_canvas_scale*thk_mult * mult - stf_web_thk = structure_obj.get_web_thk()*self._prop_canvas_scale*thk_mult * mult - stf_flange_thk = structure_obj.get_fl_thk()*self._prop_canvas_scale*thk_mult * mult - - for count in [0,1,2] if idx == 0 else [0,]: - - self._prop_canvas.create_rectangle(startx + count*spacing, - starty, - startx+spacing+ count*spacing, - starty- plate_thk , - fill = 'grey', activefill = 'yellow') - self._prop_canvas.create_rectangle(startx+spacing*0.5+ count*spacing - stf_web_thk/2, - starty - plate_thk, - startx+spacing*0.5+ count*spacing + stf_web_thk/2, - starty - stf_web_height - plate_thk, - fill = 'grey', activefill = 'yellow') - - if structure_obj.get_stiffener_type() not in ['L', 'L-bulb']: - - self._prop_canvas.create_rectangle(startx+spacing*0.5-stf_flange_width/2+ count*spacing, - starty - stf_web_height - plate_thk, - startx + spacing * 0.5 + stf_flange_width / 2+ count*spacing, - starty - stf_web_height - plate_thk- stf_flange_thk, - fill = 'grey', activefill = 'yellow') - else: - self._prop_canvas.create_rectangle(startx+spacing*0.5-stf_web_thk/2+ count*spacing, - starty-stf_web_height - plate_thk, - startx + spacing * 0.5 + stf_flange_width + count*spacing, - starty - stf_web_height - plate_thk - stf_flange_thk, - fill = 'grey', - activefill = 'yellow') - - - elif self._line_is_active and self._line_to_struc[self._active_line][5] is not None: - self.draw_cylinder(canvas = self._prop_canvas,CylObj = self._line_to_struc[self._active_line][5], - height = 200, radius = 150, start_x_cyl = 500,start_y_cyl = 20, - text_color= self._color_text) - - else: - pass - - @staticmethod - def draw_cylinder(text_size = None, canvas = None, CylObj: CylinderAndCurvedPlate = None, - height = 150, radius = 150, - start_x_cyl = 500,start_y_cyl = 20, acceptance_color = False, text_x = 200, text_y = 130, - text_color = 'black'): - - canvas_width = canvas.winfo_width() - canvas_height = canvas.winfo_height() - if text_size == None: - text_size = 'Verdana 8' - - canvas.create_text([text_x, text_y], text=CylObj, font=text_size,fill = text_color) - # setting the input field to active line properties - #self.set_selected_variables(self._active_line) - - offset_oval = 30 - - coord1 = start_x_cyl, start_y_cyl, start_x_cyl + radius, start_y_cyl+offset_oval - coord2 = start_x_cyl, start_y_cyl + height, start_x_cyl + radius,start_y_cyl+ offset_oval + height - - arc_1 = canvas.create_oval(coord1, width=5, fill='grey90') - arc_2 = canvas.create_arc(coord2, extent=180, start=180, style=tk.ARC, width=3) - - line1 = canvas.create_line(coord1[0], coord1[1] + offset_oval / 2, - coord1[0], coord1[1] + height + offset_oval / 2, - width=3) - line2 = canvas.create_line(coord1[0] + radius, coord1[1] + offset_oval / 2, - coord1[0] + radius, coord1[1] + height + offset_oval / 2, - width=3) - if CylObj.LongStfObj is not None: - long_obj = CylObj.LongStfObj - - num_stf = int(1000 * 2*math.pi*CylObj.ShellObj.radius / long_obj.spacing / 2) - for line_num in range(1, num_stf, 1): - angle = 180 - 180 / (num_stf) * line_num - arc_x, arc_y = 1 * math.cos(math.radians(angle)), 0.5 * math.sin(math.radians(angle)) - arc_x = (arc_x + 1) / 2 - - line1 = canvas.create_line(coord1[0] + radius * arc_x, - coord1[1] + 1 * arc_y * offset_oval+offset_oval/2, - coord1[0] + radius * arc_x, - coord1[1] + height + 1 * arc_y * offset_oval+offset_oval/2, - fill='blue') - if CylObj.RingStfObj is not None: - num_ring_stiff = CylObj.ShellObj.length_of_shell / \ - CylObj.ShellObj._dist_between_rings - num_ring_stiff = int(num_ring_stiff) - - for ring_stf in range(1, num_ring_stiff+1, 1): - coord3 = coord1[0], coord1[1] + (height / (num_ring_stiff + 1)) * ring_stf, \ - start_x_cyl + radius, coord1[3] + (height / (num_ring_stiff + 1)) * ring_stf, - arc_2 = canvas.create_arc(coord3, extent=180, start=180, style=tk.ARC, width=2, - fill='orange', - outline='orange') - if CylObj.RingFrameObj is not None: - num_ring_girder = CylObj.ShellObj.length_of_shell / \ - CylObj.length_between_girders - num_ring_girder = int(num_ring_girder) - for ring_girder in range(1, num_ring_girder + 1, 1): - coord3 = coord1[0], coord1[1] + (height / (num_ring_girder + 1)) * ring_girder, \ - start_x_cyl + radius, coord1[3] + (height / (num_ring_girder + 1)) * ring_girder, - arc_2 = canvas.create_arc(coord3, extent=180, start=180, style=tk.ARC, width=4, - fill='grey', outline='grey') - - def draw_results(self, state = None): - ''' - The properties canvas is created here. - state = {'colors': {}, 'section_modulus': {}, 'thickness': {}, 'shear_area': {}, 'buckling': {}, - 'fatigue': {}, 'pressure_uls': {}, 'pressure_fls': {}, - 'all_obj': {}, 'scant_calc_obj': {}, 'fatigue_obj': {}} - :return: - ''' - - self._result_canvas.delete('all') - - if state is None or self._active_line not in state['all_obj'].keys(): - return - - if self._line_is_active: - x, y, dx, dy = 0, 5, 15, 17 - - if self._active_line in self._line_to_struc and self._line_to_struc[self._active_line][5] is None: - - m3_to_mm3 = float(math.pow(1000,3)) - m2_to_mm2 = float(math.pow(1000, 2)) + m3_to_mm3 = float(math.pow(1000,3)) + m2_to_mm2 = float(math.pow(1000, 2)) current_line = self._active_line @@ -4588,71 +4982,8 @@ def draw_results(self, state = None): # buckling results start_y, y = 5, 10 - if self._PULS_results != None and self._new_buckling_method.get() == 'DNV PULS': - line_results = state['PULS colors'][self._active_line] - puls_res = self._PULS_results.get_puls_line_results(self._active_line) - if puls_res != None: - geo_problem = False - if type(puls_res['Ultimate capacity']['Actual usage Factor'][0]) != str: - ult_text = 'Ultimate capacity usage factor: ' + str(puls_res['Ultimate capacity'] - ['Actual usage Factor'][ - 0] / self._new_puls_uf.get()) - else: - geo_problem = True - ult_text = puls_res['Ultimate capacity']['Actual usage Factor'][0] - if puls_res['Buckling strength']['Actual usage Factor'][0] != None: - buc_text = 'Buckling capacity usage factor: ' + str(puls_res['Buckling strength'] - ['Actual usage Factor'][ - 0] / self._new_puls_uf.get()) - else: - buc_text = 'Buckling capacity usage factor: None - geometric issue' - - loc_label = 'Local geom req (PULS validity limits)' if \ - obj_scnt_calc_pl.get_puls_sp_or_up() == 'SP' else 'Geom. Req (PULS validity limits)' - csr_label = 'CSR-Tank requirements (primary stiffeners)' if \ - obj_scnt_calc_pl.get_puls_sp_or_up() == 'SP' else 'CSR-Tank req' - if geo_problem: - loc_geom = 'Not ok: ' - for key, value in puls_res[loc_label].items(): - if value[0] == 'Not ok': - loc_geom += key + ' ' - else: - loc_geom = 'Ok' if all( - [val[0] == 'Ok' for val in puls_res[loc_label] - .values()]) else 'Not ok' - csr_geom = 'Ok' if all( - [val[0] in ['Ok', '-'] for val in puls_res[csr_label] - .values()]) else 'Not ok' - loc_geom = loc_label + ': ' + loc_geom - csr_geom = csr_label+': ' + csr_geom - self._result_canvas.create_text([x * 1, y + (start_y+0) * dy], text='PULS results', - font=self._text_size['Text 9 bold'], - anchor='nw', - fill = self._color_text) - self._result_canvas.create_text([x * 1, y + (start_y+1) * dy], text=buc_text, - font=self._text_size['Text 9 bold'], - anchor='nw', - fill=line_results['buckling']) - self._result_canvas.create_text([x * 1, y + (start_y+2) * dy], text=ult_text, - font=self._text_size['Text 9 bold'], - anchor='nw', - fill=line_results['ultimate']) - self._result_canvas.create_text([x * 1, y + (start_y+3) * dy], text=loc_geom, - font=self._text_size['Text 9 bold'], - anchor='nw', - fill=line_results['local geometry']) - self._result_canvas.create_text([x * 1, y + (start_y+4) * dy], text=csr_geom, - font=self._text_size['Text 9 bold'], - anchor='nw', - fill=line_results['csr']) - else: - self._result_canvas.create_text([x * 1, y + (start_y+0) * dy], - text='PULS results not avaliable for this line.\n' - 'Run or update lines.', - font=self._text_size['Text 9 bold'], - anchor='nw', - fill='Orange') - elif self._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': + fatigue_start_offset = 8 + if self._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': ''' return {'Plate': {'Plate buckling': up_buckling}, 'Stiffener': {'Overpressure plate side': stf_buckling_pl_side, 'Overpressure stiffener side': stf_buckling_stf_side, @@ -4774,45 +5105,269 @@ def draw_results(self, state = None): # anchor='nw',fill=color_buckling) - elif self._new_buckling_method.get() == 'ML-CL (PULS based)': + elif self._new_buckling_method.get() in [ + 'SemiAnalytical S3/U3', + 'ML-Numeric (PULS based)', + ]: + + print_semi_analytical_results = self._new_buckling_method.get() == 'SemiAnalytical S3/U3' + print_class_results = False + print_numeric_results = self._new_buckling_method.get() == 'ML-Numeric (PULS based)' + + self._result_canvas.create_text( + [x * 1, (y + (start_y + 0) * dy) * 1], + text='Buckling results ANYstructure SemiAnalytical/ML algorithm:', + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=self._color_text, + ) + + line_offset = 1 + + # ------------------------------------------------------------------------- + # Built-in semi-analytical replacement result + # ------------------------------------------------------------------------- + if print_semi_analytical_results: + semi_analytical = state.get('SemiAnalytical', {}).get(current_line, None) + semi_analytical_valid = state.get('SemiAnalytical valid', {}).get(current_line, {}) + semi_analytical_colors = state.get('SemiAnalytical colors', {}).get(current_line, {}) + + if semi_analytical is not None: + semi_analytical_is_valid = semi_analytical_valid.get('valid prediction', None) == 1 + if semi_analytical_is_valid: + buckling_uf = semi_analytical.get('buckling UF', float('inf')) + ultimate_uf = semi_analytical.get('ultimate UF', float('inf')) + buckling_uf_raw = semi_analytical.get('buckling UF raw', None) + ultimate_uf_raw = semi_analytical.get('ultimate UF raw', None) + semi_analytical_mat_fac = semi_analytical.get( + 'material factor', + self._new_material_factor.get(), + ) + + buckling_uf_txt = f"{buckling_uf:.3f}" + ultimate_uf_txt = f"{ultimate_uf:.3f}" + + if buckling_uf_raw is not None: + buckling_uf_txt += f" ({buckling_uf_raw:.3f} x {semi_analytical_mat_fac:.2f})" + + if ultimate_uf_raw is not None: + ultimate_uf_txt += f" ({ultimate_uf_raw:.3f} x {semi_analytical_mat_fac:.2f})" + else: + buckling_uf_txt = 'invalid/unsupported' + ultimate_uf_txt = 'invalid/unsupported' + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Buckling UF SemiAnalytical S3/U3: ' + buckling_uf_txt, + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=semi_analytical_colors.get('buckling', 'red'), + ) + line_offset += 1 + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Ultimate UF SemiAnalytical S3/U3: ' + ultimate_uf_txt, + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=semi_analytical_colors.get('ultimate', 'red'), + ) + line_offset += 1 + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='SemiAnalytical UF acceptance limit: <= 1.00', + font=self._text_size["Text 9"], + anchor='nw', + fill=self._color_text, + ) + line_offset += 1 + + semi_analytical_status = semi_analytical_valid.get('valid label', '') + if semi_analytical.get('error', ''): + semi_analytical_status += ' | ' + semi_analytical.get('error', '') + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='SemiAnalytical S3/U3 status: ' + semi_analytical_status, + font=self._text_size["Text 9"], + anchor='nw', + fill=self._color_text if semi_analytical_is_valid else 'red', + ) + line_offset += 1 - self._result_canvas.create_text([x * 1, (y+(start_y+0)*dy) * 1], - text='Buckling results ANYstructure ML algorithm:', - font=self._text_size["Text 9 bold"], anchor='nw', - fill = self._color_text) - self._result_canvas.create_text([x * 1, (y+(start_y+1)*dy) * 1], - text='Buckling: ' + state['ML buckling class'][current_line]['buckling'], - font=self._text_size["Text 9 bold"], - anchor='nw', fill=state['ML buckling colors'][current_line]['buckling']) - self._result_canvas.create_text([x * 1, (y+(start_y+2)*dy) * 1], - text='Ultimate: ' +state['ML buckling class'][current_line]['ultimate'], - font=self._text_size["Text 9 bold"], - anchor='nw', fill=state['ML buckling colors'][current_line]['ultimate']) - if obj_scnt_calc_pl.get_puls_sp_or_up() == 'SP': - csr = state['ML buckling class'][current_line]['CSR'] - csr_str = ['Ok' if csr[0] == 1 else 'Not ok', 'Ok' if csr[1] == 1 else 'Not ok', - 'Ok' if csr[2] == 1 else 'Not ok', 'Ok' if csr[3] == 1 else 'Not ok'] - self._result_canvas.create_text([x * 1, (y+(start_y+3)*dy) * 1], - text='CSR requirements (stiffener): plate-'+ csr_str[0]+ ' web-'+ - csr_str[1] + ' web/flange ratio-'+ csr_str[2] + - ' flange-'+ csr_str[3] , - font=self._text_size["Text 9"], - anchor='nw', - fill=state['ML buckling colors'][current_line]['CSR requirement']) - else: - csr = state['ML buckling class'][current_line]['CSR'] - csr_str = 'Ok' if csr[0] == 1 else 'Not ok' - self._result_canvas.create_text([x * 1, (y+(start_y+3)*dy) * 1], - text='CSR requirements (stiffener): Plate slenderness -'+ - csr_str, - font=self._text_size["Text 9"], - anchor='nw', - fill=state['ML buckling colors'][current_line]['CSR requirement']) + else: + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='SemiAnalytical S3/U3: not available', + font=self._text_size["Text 9"], + anchor='nw', + fill=self._color_text, + ) + line_offset += 1 + + # ------------------------------------------------------------------------- + # Classification pipeline result + # ------------------------------------------------------------------------- + if print_class_results: + ml_class = state.get('ML buckling class', {}).get(current_line, {}) + ml_colors = state.get('ML buckling colors', {}).get(current_line, {}) + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Buckling class: ' + str(ml_class.get('buckling', 'N/A')), + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=ml_colors.get('buckling', 'red'), + ) + line_offset += 1 + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Ultimate class: ' + str(ml_class.get('ultimate', 'N/A')), + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=ml_colors.get('ultimate', 'red'), + ) + line_offset += 1 + + # ------------------------------------------------------------------------- + # Numeric UF pipeline result + # + # The numeric UF values stored in state are already material-factored + # in get_color_and_calc_state(). The raw values are the model output + # for material factor = 1.0. + # ------------------------------------------------------------------------- + if print_numeric_results: + numeric = state.get('ML buckling numeric', {}).get(current_line, None) + numeric_valid = state.get('ML buckling numeric valid', {}).get(current_line, {}) + numeric_colors = state.get('ML buckling numeric colors', {}).get(current_line, {}) + + if numeric is not None: + numeric_is_valid = numeric_valid.get('valid prediction', None) == 1 + + if numeric_is_valid: + buckling_uf = numeric.get('buckling UF', float('inf')) + ultimate_uf = numeric.get('ultimate UF', float('inf')) + + buckling_uf_raw = numeric.get('buckling UF raw', None) + ultimate_uf_raw = numeric.get('ultimate UF raw', None) + + # Use the currently selected GUI material factor for display. + # The value in state['ML buckling numeric'] should already be + # multiplied by the material factor in get_color_and_calc_state(). + numeric_mat_fac = self._new_material_factor.get() + buckling_uf_txt = f"{buckling_uf:.3f}" + ultimate_uf_txt = f"{ultimate_uf:.3f}" + + if buckling_uf_raw is not None: + buckling_uf_txt += f" ({buckling_uf_raw:.3f} x {numeric_mat_fac:.2f})" + + if ultimate_uf_raw is not None: + ultimate_uf_txt += f" ({ultimate_uf_raw:.3f} x {numeric_mat_fac:.2f})" + + else: + buckling_uf_txt = 'invalid/NaN' + ultimate_uf_txt = 'invalid/NaN' + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Buckling UF numeric: ' + buckling_uf_txt, + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=numeric_colors.get('buckling', 'red'), + ) + line_offset += 1 + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Ultimate UF numeric: ' + ultimate_uf_txt, + font=self._text_size["Text 9 bold"], + anchor='nw', + fill=numeric_colors.get('ultimate', 'red'), + ) + line_offset += 1 + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Numeric UF acceptance limit: <= 1.00', + font=self._text_size["Text 9"], + anchor='nw', + fill=self._color_text, + ) + line_offset += 1 + + numeric_status = numeric_valid.get('valid label', '') + if numeric.get('error', ''): + numeric_status += ' | ' + numeric.get('error', '') + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Numeric UF status: ' + numeric_status, + font=self._text_size["Text 9"], + anchor='nw', + fill=self._color_text if numeric_is_valid else 'red', + ) + line_offset += 1 + + else: + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='Numeric UF: not available', + font=self._text_size["Text 9"], + anchor='nw', + fill=self._color_text, + ) + line_offset += 1 + + # ------------------------------------------------------------------------- + # CSR requirement result, shown for both ML classification and numeric modes + # ------------------------------------------------------------------------- + ml_class = state.get('ML buckling class', {}).get(current_line, {}) + ml_colors = state.get('ML buckling colors', {}).get(current_line, {}) + csr = ml_class.get('CSR', None) + + if csr is not None: + if obj_scnt_calc_pl.get_puls_sp_or_up() == 'SP': + csr_str = [ + 'Ok' if csr[0] == 1 else 'Not ok', + 'Ok' if csr[1] == 1 else 'Not ok', + 'Ok' if csr[2] == 1 else 'Not ok', + 'Ok' if csr[3] == 1 else 'Not ok', + ] + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text=( + 'CSR requirements (stiffener): plate-' + csr_str[0] + + ' web-' + csr_str[1] + + ' web/flange ratio-' + csr_str[2] + + ' flange-' + csr_str[3] + ), + font=self._text_size["Text 9"], + anchor='nw', + fill=ml_colors.get('CSR requirement', 'red'), + ) + line_offset += 1 + else: + csr_str = 'Ok' if csr[0] == 1 else 'Not ok' + + self._result_canvas.create_text( + [x * 1, (y + (start_y + line_offset) * dy) * 1], + text='CSR requirements (plate): Plate slenderness - ' + csr_str, + font=self._text_size["Text 9"], + anchor='nw', + fill=ml_colors.get('CSR requirement', 'red'), + ) + line_offset += 1 + + # Keep fatigue below the ML result block, with the old default as a minimum. + fatigue_start_offset = max(8, line_offset + 1) # fatigue results - self._result_canvas.create_text([x * 1, (y+(start_y+8)*dy) * 1], + self._result_canvas.create_text([x * 1, (y+(start_y+fatigue_start_offset)*dy) * 1], text='Fatigue results (DNVGL-RP-C203): ', font=self._text_size["Text 9 bold"], anchor='nw', fill = self._color_text) @@ -4820,19 +5375,19 @@ def draw_results(self, state = None): if state['fatigue'][current_line]['damage'] is not None: damage = state['fatigue'][current_line]['damage'] dff = state['fatigue'][current_line]['dff'] - self._result_canvas.create_text([x * 1, (y + (start_y+9) * dy) * 1], + self._result_canvas.create_text([x * 1, (y + (start_y+fatigue_start_offset+1) * dy) * 1], text='Total damage (DFF not included): '+str(round(damage,3)) + ' | With DFF = '+str(dff)+' --> Damage: '+ str(round(damage*dff,3)), font=self._text_size["Text 9 bold"], anchor='nw', fill=color_fatigue) else: - self._result_canvas.create_text([x * 1, (y + (start_y+9) * dy) * 1], + self._result_canvas.create_text([x * 1, (y + (start_y+fatigue_start_offset+1) * dy) * 1], text='Total damage: NO RESULTS ', font=self._text_size["Text 9 bold"], anchor='nw', fill = self._color_text) else: - self._result_canvas.create_text([x * 1, (y + (start_y+9) * dy) * 1], + self._result_canvas.create_text([x * 1, (y + (start_y+fatigue_start_offset+1) * dy) * 1], text='Total damage: NO RESULTS ', font=self._text_size["Text 9 bold"], anchor='nw', fill = self._color_text) @@ -4910,45 +5465,244 @@ def draw_results(self, state = None): fill=uf_col) elif key == 'Stiffener check': - if value is not None: - y_location +=1 - self._result_canvas.create_text([x, dy*y_location], - text='Stiffener requirement checks:', - font=self._text_size['Text 10 bold'], - anchor='nw', - fill = self._color_text) - y_location += 1 - idx_y, idx_x = 0, 0 + if value is not None: + y_location +=1 + self._result_canvas.create_text([x, dy*y_location], + text='Stiffener requirement checks:', + font=self._text_size['Text 10 bold'], + anchor='nw', + fill = self._color_text) + y_location += 1 + idx_y, idx_x = 0, 0 + + for stf_type, chk_bool in value.items(): + stf_text = stf_type + if stf_type == 'ring frame': + continue + + chk_text = 'OK' if chk_bool == True else 'failed' if chk_bool == False else 'N/A' + + self._result_canvas.create_text([15*dx*idx_x, dy*y_location], + text=stf_text, font=self._text_size['Text 10 bold'], + anchor='nw', + fill=self._color_text if not value else 'black') + + self._result_canvas.create_text([15*dx*idx_x, y + (y_location+1)*dy], + text=chk_text, font=self._text_size['Text 10 bold'], + anchor='nw', + fill='green' if chk_bool == True else 'red' if + chk_bool == False else self._color_text) + + self._result_canvas.create_text([15*dx*idx_x, y + (y_location+2)*dy], + text=results['Stiffener check detailed'][stf_type], + font=self._text_size['Text 10'], + anchor='nw', + fill='green' if chk_bool == True else 'red' if + chk_bool == False else self._color_text) + idx_y += 1 + idx_x += 1 + + + y_location += 1 + + def draw_prop(self, event = None): + ''' + Prints the properties of the selected line to the bottom canvas. + + properties for line dicitonary: + + name of line : [ Structure class, calc scantling class, calc fatigue class, [load classes] ] + + ''' + + self._prop_canvas.delete('all') + canvas_width = self._prop_canvas.winfo_width() + canvas_height = self._prop_canvas.winfo_height() + + def checkered(line_distance, canvas): + ''' + Grid lines in the properties canvas. + :param line_distance: + :return: + ''' + # vertical lines at an interval of "line_distance" pixel + for x in range(line_distance, canvas_width, line_distance): + canvas.create_line(x, 0, x, canvas_width, stipple='gray50', activestipple='gray75') + # horizontal lines at an interval of "line_distance" pixel + for y in range(line_distance, canvas_height, line_distance): + canvas.create_line(0, y, canvas_width, y, stipple='gray50', activestipple='gray75') + + if self._active_line in self._line_to_struc: + self.set_selected_variables(self._active_line) + # printing the properties to the active line + if self._line_is_active and self._line_to_struc[self._active_line][5] is None: + #checkered(10, self._prop_canvas) + self._prop_canvas.create_text([canvas_width/2-canvas_width/20, canvas_height/20], + text ='SELECTED: '+str(self._active_line), + font=self._text_size["Text 10 bold"], fill='red') + if all([self._line_to_struc[self._active_line][0].Stiffener is None, + self._line_to_struc[self._active_line][0].Girder is None]): + structure_obj = self._line_to_struc[self._active_line][0].Plate + spacing = structure_obj.get_s() * self._prop_canvas_scale * 3 + plate_thk = structure_obj.get_pl_thk() * self._prop_canvas_scale * 3 + startx = 20 + starty = 225 + self._prop_canvas.create_text([startx + 100, 50], + text='Plate with thickness ' + + str(structure_obj.get_pl_thk()*1000) + ' mm' , + font=self._text_size["Text 10 bold"], fill='Black') + self._prop_canvas.create_rectangle(startx + spacing, + starty, + startx + spacing + spacing, + starty - plate_thk, + fill='grey', activefill='yellow') + + for idx, structure_obj in enumerate([self._line_to_struc[self._active_line][0].Stiffener, + self._line_to_struc[self._active_line][0].Girder]): + mult = 1 if self._line_to_struc[self._active_line][0].Girder is not None else 2 # *(400/max_web) + thk_mult = 2 # *(400/max_web) + startx = 100 + 300 * idx + starty = 225 + + if structure_obj is not None: + self._prop_canvas.create_text([startx +60, 50], + text='Stiffener\n' +structure_obj.get_beam_string() + if idx == 0 else 'Girder\n' + structure_obj.get_beam_string(), + font=self._text_size["Text 9 bold"], fill='Black') + if structure_obj is not None: + self._prop_canvas.create_text([100, 20], + text='Thickness scale x 2', + font=self._text_size["Text 10 bold"], fill='grey') + # drawing stiffener + spacing = structure_obj.get_s()*self._prop_canvas_scale * mult + stf_web_height = structure_obj.get_web_h()*self._prop_canvas_scale * mult + stf_flange_width = structure_obj.get_fl_w() *self._prop_canvas_scale * mult + plate_thk = structure_obj.get_pl_thk()*self._prop_canvas_scale*thk_mult * mult + stf_web_thk = structure_obj.get_web_thk()*self._prop_canvas_scale*thk_mult * mult + stf_flange_thk = structure_obj.get_fl_thk()*self._prop_canvas_scale*thk_mult * mult + + for count in [0,1,2] if idx == 0 else [0,]: + + self._prop_canvas.create_rectangle(startx + count*spacing, + starty, + startx+spacing+ count*spacing, + starty- plate_thk , + fill = 'grey', activefill = 'yellow') + self._prop_canvas.create_rectangle(startx+spacing*0.5+ count*spacing - stf_web_thk/2, + starty - plate_thk, + startx+spacing*0.5+ count*spacing + stf_web_thk/2, + starty - stf_web_height - plate_thk, + fill = 'grey', activefill = 'yellow') + + if structure_obj.get_stiffener_type() not in ['L', 'L-bulb']: + + self._prop_canvas.create_rectangle(startx+spacing*0.5-stf_flange_width/2+ count*spacing, + starty - stf_web_height - plate_thk, + startx + spacing * 0.5 + stf_flange_width / 2+ count*spacing, + starty - stf_web_height - plate_thk- stf_flange_thk, + fill = 'grey', activefill = 'yellow') + else: + self._prop_canvas.create_rectangle(startx+spacing*0.5-stf_web_thk/2+ count*spacing, + starty-stf_web_height - plate_thk, + startx + spacing * 0.5 + stf_flange_width + count*spacing, + starty - stf_web_height - plate_thk - stf_flange_thk, + fill = 'grey', + activefill = 'yellow') + + + elif self._line_is_active and self._line_to_struc[self._active_line][5] is not None: + self.draw_cylinder(canvas = self._prop_canvas,CylObj = self._line_to_struc[self._active_line][5], + height = 200, radius = 150, start_x_cyl = 500,start_y_cyl = 20, + text_color= self._color_text) + + else: + pass + + + @staticmethod + def draw_cylinder(text_size = None, canvas = None, CylObj: CylinderAndCurvedPlate = None, + height = 150, radius = 150, + start_x_cyl = 500,start_y_cyl = 20, acceptance_color = False, text_x = 200, text_y = 130, + text_color = 'black'): + + canvas_width = canvas.winfo_width() + canvas_height = canvas.winfo_height() + if text_size == None: + text_size = 'Verdana 8' + + canvas.create_text([text_x, text_y], text=CylObj, font=text_size,fill = text_color) + # setting the input field to active line properties + #self.set_selected_variables(self._active_line) + + offset_oval = 30 + + coord1 = start_x_cyl, start_y_cyl, start_x_cyl + radius, start_y_cyl+offset_oval + coord2 = start_x_cyl, start_y_cyl + height, start_x_cyl + radius,start_y_cyl+ offset_oval + height - for stf_type, chk_bool in value.items(): - stf_text = stf_type - if stf_type == 'ring frame': - continue + arc_1 = canvas.create_oval(coord1, width=5, fill='grey90') + arc_2 = canvas.create_arc(coord2, extent=180, start=180, style=tk.ARC, width=3) - chk_text = 'OK' if chk_bool == True else 'failed' if chk_bool == False else 'N/A' + line1 = canvas.create_line(coord1[0], coord1[1] + offset_oval / 2, + coord1[0], coord1[1] + height + offset_oval / 2, + width=3) + line2 = canvas.create_line(coord1[0] + radius, coord1[1] + offset_oval / 2, + coord1[0] + radius, coord1[1] + height + offset_oval / 2, + width=3) + if CylObj.LongStfObj is not None: + long_obj = CylObj.LongStfObj - self._result_canvas.create_text([15*dx*idx_x, dy*y_location], - text=stf_text, font=self._text_size['Text 10 bold'], - anchor='nw', - fill=self._color_text if not value else 'black') + num_stf = int(1000 * 2*math.pi*CylObj.ShellObj.radius / long_obj.spacing / 2) + for line_num in range(1, num_stf, 1): + angle = 180 - 180 / (num_stf) * line_num + arc_x, arc_y = 1 * math.cos(math.radians(angle)), 0.5 * math.sin(math.radians(angle)) + arc_x = (arc_x + 1) / 2 - self._result_canvas.create_text([15*dx*idx_x, y + (y_location+1)*dy], - text=chk_text, font=self._text_size['Text 10 bold'], - anchor='nw', - fill='green' if chk_bool == True else 'red' if - chk_bool == False else self._color_text) + line1 = canvas.create_line(coord1[0] + radius * arc_x, + coord1[1] + 1 * arc_y * offset_oval+offset_oval/2, + coord1[0] + radius * arc_x, + coord1[1] + height + 1 * arc_y * offset_oval+offset_oval/2, + fill='blue') + if CylObj.RingStfObj is not None: + num_ring_stiff = CylObj.ShellObj.length_of_shell / \ + CylObj.ShellObj._dist_between_rings + num_ring_stiff = int(num_ring_stiff) - self._result_canvas.create_text([15*dx*idx_x, y + (y_location+2)*dy], - text=results['Stiffener check detailed'][stf_type], - font=self._text_size['Text 10'], - anchor='nw', - fill='green' if chk_bool == True else 'red' if - chk_bool == False else self._color_text) - idx_y += 1 - idx_x += 1 + for ring_stf in range(1, num_ring_stiff+1, 1): + coord3 = coord1[0], coord1[1] + (height / (num_ring_stiff + 1)) * ring_stf, \ + start_x_cyl + radius, coord1[3] + (height / (num_ring_stiff + 1)) * ring_stf, + arc_2 = canvas.create_arc(coord3, extent=180, start=180, style=tk.ARC, width=2, + fill='orange', + outline='orange') + if CylObj.RingFrameObj is not None: + num_ring_girder = CylObj.ShellObj.length_of_shell / \ + CylObj.length_between_girders + num_ring_girder = int(num_ring_girder) + for ring_girder in range(1, num_ring_girder + 1, 1): + coord3 = coord1[0], coord1[1] + (height / (num_ring_girder + 1)) * ring_girder, \ + start_x_cyl + radius, coord1[3] + (height / (num_ring_girder + 1)) * ring_girder, + arc_2 = canvas.create_arc(coord3, extent=180, start=180, style=tk.ARC, width=4, + fill='grey', outline='grey') - y_location += 1 + def _get_ml_classes(self): + return getattr(self, '_ML_classes', ml_models.default_ml_class_messages()) + + def _build_report_data_snapshot(self): + return project_services.ReportDataSnapshot( + project_information=self._project_information.get('1.0', tk.END), + buckling_method=self._new_buckling_method.get(), + points=self._point_dict, + lines=self._line_dict, + line_bundles=self._line_to_struc, + tanks=self._tank_dict, + loads=self._load_dict, + result_state=self.get_color_and_calc_state(), + highest_pressures={ + line_name: self.get_highest_pressure(line_name) + for line_name in self._line_to_struc + }, + ml_classes=self._get_ml_classes(), + ) def report_generate(self, autosave = False): ''' @@ -4957,10 +5711,12 @@ def report_generate(self, autosave = False): ''' if not autosave: - save_file = filedialog.asksaveasfile(mode="w", defaultextension=".pdf") - filename = save_file.name - if save_file is None: # ask saveasfile return `None` if dialog closed with "cancel". + target = project_application.ProjectFileDialogService.selected_output_target( + filedialog.asksaveasfilename(defaultextension=".pdf") + ) + if target is None: return + filename = target.path else: filename = '../testrun.pdf' @@ -4974,9 +5730,9 @@ def report_generate(self, autosave = False): else: self.grid_display_tanks(save=True) - doc = LetterMaker(filename, "Section results", 10, self) - doc.createDocument() - doc.savePDF() + project_services.ReportRequestService.create_pdf( + project_services.ReportRequest(str(filename), "Section results", 10, self._build_report_data_snapshot()), + ) try: os.startfile(filename) except FileNotFoundError: @@ -4989,10 +5745,12 @@ def report_generate(self, autosave = False): def table_generate(self, autosave = False): if not autosave: - save_file = filedialog.asksaveasfile(mode="w", defaultextension=".pdf") - filename = save_file.name - if save_file is None: # ask saveasfile return `None` if dialog closed with "cancel". + target = project_application.ProjectFileDialogService.selected_output_target( + filedialog.asksaveasfilename(defaultextension=".pdf") + ) + if target is None: return + filename = target.path else: filename = '../testrun.pdf' @@ -5000,10 +5758,9 @@ def table_generate(self, autosave = False): tk.messagebox.showerror('No lines', 'No lines defined. Cannot make report.') return - doc_dat = LetterMaker(filename, "Section results", 10, self) - doc = SimpleDocTemplate(filename, pagesize=landscape(letter)) - elements = doc_dat.createTable() - doc.build(elements) + project_services.ReportRequestService.create_table( + project_services.ReportRequest(str(filename), "Section results", 10, self._build_report_data_snapshot()), + ) try: os.startfile(filename) except FileNotFoundError: @@ -5027,8 +5784,7 @@ def create_accelerations(self): for tank, data in self._tank_dict.items(): data.set_acceleration(self._accelerations_dict) - for line, obj in self._line_to_struc.items(): - obj[0].need_recalc = True + project_services.mark_lines_for_recalculation(self._line_to_struc) except TclError: messagebox.showinfo(title='Input error', message='Input must be a number. Dots used not comma.') @@ -5049,29 +5805,19 @@ def new_point(self,copy=False,move=False, redo = None): x_coord = (self._new_point_x.get() / 1000) y_coord = (self._new_point_y.get() / 1000) + project_editor = project_services.ProjectEditService(self._point_dict, self._line_dict) # Finding name of the new point - current_point = '' if move: current_point, current_coords = self._active_point, self._point_dict[self._active_point] else: - found_name = False - if len(self._point_dict) == 0: - current_point = 'point1' - found_name = True - else: - counter = 1 - while not found_name: - current_point = 'point'+str(counter) - if current_point not in self._point_dict.keys(): - found_name = True - else: - counter += 1 + current_point = project_editor.next_point_name() self._new_line_p1.set(get_num(current_point)) # Creating the point # No point is created if another point is already there - if [x_coord,y_coord] not in self._point_dict.values(): - self._point_dict[current_point] = [x_coord, y_coord] + point_record = project_editor.move_point(current_point, (x_coord, y_coord)) if move else \ + project_editor.add_point(current_point, (x_coord, y_coord)) + if point_record is not None: self._active_point = current_point if move: self.logger(point=current_point, move_coords=(current_coords,[x_coord, y_coord])) @@ -5101,24 +5847,22 @@ def move_point(self, event = None, redo = None): ''' if self._point_is_active: self.new_point(move=True, redo=redo) # doing the actual moving - for line,data in self._line_dict.items(): + project_editor = project_services.ProjectEditService(self._point_dict, self._line_dict) + for line in project_editor.connected_line_names(self._active_point): + data = self._line_dict[line] # updating the span and deleting compartments (if not WT) - if get_num(self._active_point) in data: - coord1 = self._point_dict['point'+str(data[0])] - coord2 = self._point_dict['point'+str(data[1])] - if line in self._line_to_struc.keys(): - self._line_to_struc[line][0].Plate.set_span(dist(coord1,coord2)) - self._line_to_struc[line][0].Plate.set_span(dist(coord1, coord2)) - if self._PULS_results is not None: - self._PULS_results.result_changed(line) - if self._line_to_struc[line][0].Plate.get_structure_type() not in ['GENERAL_INTERNAL_NONWT', - 'FRAME']: - self._tank_dict = {} - self._main_grid.clear() - self._compartments_listbox.delete(0, 'end') - - for line, obj in self._line_to_struc.items(): - obj[0].need_recalc = True + coord1 = self._point_dict['point'+str(data[0])] + coord2 = self._point_dict['point'+str(data[1])] + if line in self._line_to_struc.keys(): + self._line_to_struc[line][0].Plate.set_span(dist(coord1,coord2)) + self._line_to_struc[line][0].Plate.set_span(dist(coord1, coord2)) + if self._line_to_struc[line][0].Plate.get_structure_type() not in ['GENERAL_INTERNAL_NONWT', + 'FRAME']: + self._tank_dict = {} + self._main_grid.clear() + self._compartments_listbox.delete(0, 'end') + + project_services.mark_lines_for_recalculation(self._line_to_struc) self.update_frame() else: messagebox.showinfo(title='Input error', message='A point must be selected (right click).') @@ -5138,6 +5882,7 @@ def new_line(self, event = None, redo = None): Adds line to line dictionary. Type is 'line1' = [p1,p2] ''' + current_name = None try: # if's ensure that the new line does not exist already and that the point input is not an invalid point. if redo is None: @@ -5145,36 +5890,571 @@ def new_line(self, event = None, redo = None): 'point' + str(self._new_line_p2.get()) else: first_point, second_point = redo - first_point_num, second_point_num = get_num(first_point), get_num(second_point) - - if first_point in self._point_dict.keys() and second_point in self._point_dict.keys() \ - and first_point != second_point: - line_str, line_str_rev = self.make_point_point_line_string(first_point_num, second_point_num) - - if line_str and line_str_rev not in self._line_point_to_point_string: - name = False - counter = 1 - while not name: - current_name = 'line' + str(counter) - if current_name not in self._line_dict.keys(): - name = True - counter += 1 - - self._line_dict[current_name] = [first_point_num, second_point_num] - - self.update_frame() - self.logger(line=[current_name, redo]) - - # making stings from two points difining the lines, e.g. for line 1 string could be 'p1p2' and 'p2p1' - self._line_point_to_point_string.append(line_str) - self._line_point_to_point_string.append(line_str_rev) - self.add_to_combinations_dict(current_name) - for line, obj in self._line_to_struc.items(): - obj[0].need_recalc = True + project_editor = project_services.ProjectEditService(self._point_dict, self._line_dict) + line_record = project_editor.add_line(first_point, second_point) + + if line_record is not None: + current_name = line_record.name + self.update_frame() + self.logger(line=[current_name, redo]) + + # Keep the legacy point-to-point index synchronized while the GUI still reads it. + self._line_point_to_point_string.extend(line_record.endpoint_keys) + self.add_to_combinations_dict(current_name) + project_services.mark_lines_for_recalculation(self._line_to_struc) except TclError: messagebox.showinfo(title='Input error', message='Input must be a line number.') return current_name + def _is_flat_calculation_domain(self, calculation_domain): + return calculation_domain in api_helpers.FLAT_STRUCTURE_DOMAINS + + def _build_flat_structure_property_request(self): + return project_services.FlatStructurePropertyRequest( + calculation_domain=self._new_calculation_domain.get(), + base_values={ + 'material': self._new_material.get(), + 'material_factor': self._new_material_factor.get(), + 'span': self._new_field_len.get(), + 'spacing': self._new_stf_spacing.get(), + 'plate_thk': self._new_plate_thk.get(), + 'stf_web_h': self._new_stf_web_h.get(), + 'stf_web_t': self._new_stf_web_t.get(), + 'stf_fl_w': self._new_stf_fl_w.get(), + 'stf_fl_t': self._new_stf_fl_t.get(), + 'structure_type': self._new_stucture_type.get(), + 'stf_type': self._new_stf_type.get(), + 'sigma_y1': self._new_sigma_y1.get(), + 'sigma_y2': self._new_sigma_y2.get(), + 'sigma_x1': self._new_sigma_x1.get(), + 'sigma_x2': self._new_sigma_x2.get(), + 'tau_xy': self._new_tauxy.get(), + 'plate_kpp': self._new_plate_kpp.get(), + 'stf_kps': self._new_stf_kps.get(), + 'stf_km1': self._new_stf_km1.get(), + 'stf_km2': self._new_stf_km2.get(), + 'stf_km3': self._new_stf_km3.get(), + 'pressure_side': self._new_pressure_side.get(), + 'zstar_optimization': self._new_zstar_optimization.get(), + 'puls_method': self._new_puls_method.get(), + 'puls_boundary': self._new_puls_panel_boundary.get(), + 'puls_stiffener_end': self._new_buckling_stf_end_support.get(), + 'puls_sp_or_up': self._new_puls_sp_or_up.get(), + 'puls_up_boundary': self._new_puls_up_boundary.get(), + 'panel_or_shell': self._new_panel_or_shell.get(), + 'girder_lg': self._new_girder_length_LG.get(), + }, + girder_values={ + 'web_h': self._new_girder_web_h.get(), + 'web_t': self._new_girder_web_t.get(), + 'fl_w': self._new_girder_fl_w.get(), + 'fl_t': self._new_girder_fl_t.get(), + 'type': self._new_girder_type.get(), + }, + buckling_values={ + 'min_pressure_adjacent_spans': self._new_buckling_min_press_adj_spans.get(), + 'load_factor_stresses': self._new_buckling_lf_stresses.get(), + 'stiffener_end_support': self._new_buckling_stf_end_support.get(), + 'girder_end_support': self._new_buckling_girder_end_support.get(), + 'tension_field': self._new_buckling_tension_field.get(), + 'plate_effective_against_sigy': self._new_buckling_effective_against_sigy.get(), + 'buckling_length_factor_stf': self._new_buckling_length_factor_stf.get(), + 'buckling_length_factor_girder': self._new_buckling_length_factor_stf.get(), + 'km3': self._new_buckling_km3.get(), + 'km2': self._new_buckling_km2.get(), + 'girder_dist_lateral_support': self._new_buckling_girder_dist_bet_lat_supp.get(), + 'stiffener_dist_lateral_support': self._new_buckling_stf_dist_bet_lat_supp.get(), + 'panel_length': self._new_panel_length_Lp.get(), + 'fabrication_method_stiffener': self._new_buckling_fab_method_stf.get(), + 'fabrication_method_girder': self._new_buckling_fab_method_girder.get(), + }, + structure_types=self._structure_types, + ) + + def _build_flat_structure_properties(self): + return project_services.FlatStructurePropertyService.build( + self._build_flat_structure_property_request() + ) + + def _build_flat_structure_property_request_from_excel_record(self, import_record, span): + plate_thk, spacing, web_h, web_t, flange_w, flange_t = import_record.plate_values + sigma_x1, sigma_x2, sigma_y1, sigma_y2, tau_xy = import_record.stress_values + girder_length, girder_web_h, girder_web_t, girder_fl_w, girder_fl_t, girder_type = \ + import_record.girder_values + overpressure, structure_type, stf_end, girder_end, fab_stf, fab_girder, length_stf, length_girder, \ + stf_lateral_support, girder_lateral_support, tension_field, effective_against_sigy = \ + import_record.buckling_values + + return project_services.FlatStructurePropertyRequest( + calculation_domain=import_record.calculation_domain, + base_values={ + 'material': self._new_material.get(), + 'material_factor': self._new_material_factor.get(), + 'span': span, + 'spacing': spacing, + 'plate_thk': plate_thk, + 'stf_web_h': web_h, + 'stf_web_t': web_t, + 'stf_fl_w': flange_w, + 'stf_fl_t': flange_t, + 'structure_type': structure_type if structure_type is not None else self._new_stucture_type.get(), + 'stf_type': self._new_stf_type.get(), + 'sigma_y1': sigma_y1, + 'sigma_y2': sigma_y2, + 'sigma_x1': sigma_x1, + 'sigma_x2': sigma_x2, + 'tau_xy': tau_xy, + 'plate_kpp': self._new_plate_kpp.get(), + 'stf_kps': self._new_stf_kps.get(), + 'stf_km1': self._new_stf_km1.get(), + 'stf_km2': self._new_stf_km2.get(), + 'stf_km3': self._new_stf_km3.get(), + 'pressure_side': self._new_pressure_side.get(), + 'zstar_optimization': self._new_zstar_optimization.get(), + 'puls_method': self._new_puls_method.get(), + 'puls_boundary': self._new_puls_panel_boundary.get(), + 'puls_stiffener_end': stf_end if stf_end is not None else self._new_buckling_stf_end_support.get(), + 'puls_sp_or_up': self._new_puls_sp_or_up.get(), + 'puls_up_boundary': self._new_puls_up_boundary.get(), + 'panel_or_shell': self._new_panel_or_shell.get(), + 'girder_lg': girder_length, + }, + girder_values={ + 'web_h': girder_web_h, + 'web_t': girder_web_t, + 'fl_w': girder_fl_w, + 'fl_t': girder_fl_t, + 'type': girder_type if girder_type is not None else self._new_girder_type.get(), + }, + buckling_values={ + 'min_pressure_adjacent_spans': self._new_buckling_min_press_adj_spans.get(), + 'load_factor_stresses': self._new_buckling_lf_stresses.get(), + 'stiffener_end_support': stf_end if stf_end is not None else self._new_buckling_stf_end_support.get(), + 'girder_end_support': girder_end if girder_end is not None + else self._new_buckling_girder_end_support.get(), + 'tension_field': tension_field if tension_field is not None else self._new_buckling_tension_field.get(), + 'plate_effective_against_sigy': effective_against_sigy if effective_against_sigy is not None + else self._new_buckling_effective_against_sigy.get(), + 'buckling_length_factor_stf': length_stf if length_stf is not None + else self._new_buckling_length_factor_stf.get(), + 'buckling_length_factor_girder': length_girder if length_girder is not None + else self._new_buckling_length_factor_stf.get(), + 'km3': self._new_buckling_km3.get(), + 'km2': self._new_buckling_km2.get(), + 'girder_dist_lateral_support': girder_lateral_support if girder_lateral_support is not None + else self._new_buckling_girder_dist_bet_lat_supp.get(), + 'stiffener_dist_lateral_support': stf_lateral_support if stf_lateral_support is not None + else self._new_buckling_stf_dist_bet_lat_supp.get(), + 'panel_length': self._new_panel_length_Lp.get(), + 'fabrication_method_stiffener': fab_stf if fab_stf is not None + else self._new_buckling_fab_method_stf.get(), + 'fabrication_method_girder': fab_girder if fab_girder is not None + else self._new_buckling_fab_method_girder.get(), + }, + structure_types=self._structure_types, + ), overpressure + + def _build_flat_structure_property_request_from_cylinder_excel_record(self, import_record): + shell_thk = import_record.shell_values[0] + long_web_h, long_web_t, long_fl_w, long_fl_t, panel_spacing, long_type = \ + import_record.longitudinal_values + + return project_services.FlatStructurePropertyRequest( + calculation_domain=import_record.calculation_domain, + base_values={ + 'material': self._new_material.get(), + 'material_factor': self._new_material_factor.get(), + 'span': hlp.dist(import_record.first_point, import_record.second_point), + 'spacing': panel_spacing, + 'plate_thk': shell_thk, + 'stf_web_h': long_web_h, + 'stf_web_t': long_web_t, + 'stf_fl_w': long_fl_w, + 'stf_fl_t': long_fl_t, + 'structure_type': self._new_stucture_type.get(), + 'stf_type': long_type, + 'sigma_y1': self._new_sigma_y1.get(), + 'sigma_y2': self._new_sigma_y2.get(), + 'sigma_x1': self._new_sigma_x1.get(), + 'sigma_x2': self._new_sigma_x2.get(), + 'tau_xy': self._new_tauxy.get(), + 'plate_kpp': self._new_plate_kpp.get(), + 'stf_kps': self._new_stf_kps.get(), + 'stf_km1': self._new_stf_km1.get(), + 'stf_km2': self._new_stf_km2.get(), + 'stf_km3': self._new_stf_km3.get(), + 'pressure_side': self._new_pressure_side.get(), + 'zstar_optimization': self._new_zstar_optimization.get(), + 'puls_method': self._new_puls_method.get(), + 'puls_boundary': self._new_puls_panel_boundary.get(), + 'puls_stiffener_end': self._new_buckling_stf_end_support.get(), + 'puls_sp_or_up': self._new_puls_sp_or_up.get(), + 'puls_up_boundary': self._new_puls_up_boundary.get(), + 'panel_or_shell': self._new_panel_or_shell.get(), + 'girder_lg': self._new_girder_length_LG.get(), + }, + girder_values={ + 'web_h': self._new_girder_web_h.get(), + 'web_t': self._new_girder_web_t.get(), + 'fl_w': self._new_girder_fl_w.get(), + 'fl_t': self._new_girder_fl_t.get(), + 'type': self._new_girder_type.get(), + }, + buckling_values={ + 'min_pressure_adjacent_spans': self._new_buckling_min_press_adj_spans.get(), + 'load_factor_stresses': self._new_buckling_lf_stresses.get(), + 'stiffener_end_support': self._new_buckling_stf_end_support.get(), + 'girder_end_support': self._new_buckling_girder_end_support.get(), + 'tension_field': self._new_buckling_tension_field.get(), + 'plate_effective_against_sigy': self._new_buckling_effective_against_sigy.get(), + 'buckling_length_factor_stf': self._new_buckling_length_factor_stf.get(), + 'buckling_length_factor_girder': self._new_buckling_length_factor_stf.get(), + 'km3': self._new_buckling_km3.get(), + 'km2': self._new_buckling_km2.get(), + 'girder_dist_lateral_support': self._new_buckling_girder_dist_bet_lat_supp.get(), + 'stiffener_dist_lateral_support': self._new_buckling_stf_dist_bet_lat_supp.get(), + 'panel_length': self._new_panel_length_Lp.get(), + 'fabrication_method_stiffener': self._new_buckling_fab_method_stf.get(), + 'fabrication_method_girder': self._new_buckling_fab_method_girder.get(), + }, + structure_types=self._structure_types, + ) + + def _build_cylinder_structure_property_request(self): + return project_services.CylinderStructurePropertyRequest( + calculation_domain=self._new_calculation_domain.get(), + dummy_values={ + 'span': self._new_field_len.get(), + 'plate_thk': self._new_plate_thk.get(), + 'structure_type': self._new_stucture_type.get(), + 'sigma_y1': self._new_sigma_y1.get(), + 'sigma_y2': self._new_sigma_y2.get(), + 'sigma_x1': self._new_sigma_x1.get(), + 'sigma_x2': self._new_sigma_x2.get(), + 'tau_xy': self._new_tauxy.get(), + 'plate_kpp': self._new_plate_kpp.get(), + 'stf_kps': self._new_stf_kps.get(), + 'stf_km1': self._new_stf_km1.get(), + 'stf_km2': self._new_stf_km2.get(), + 'stf_km3': self._new_stf_km3.get(), + 'pressure_side': self._new_pressure_side.get(), + 'zstar_optimization': self._new_zstar_optimization.get(), + 'puls_method': self._new_puls_method.get(), + 'puls_boundary': self._new_puls_panel_boundary.get(), + 'puls_stiffener_end': self._new_buckling_stf_end_support.get(), + 'puls_sp_or_up': self._new_puls_sp_or_up.get(), + 'puls_up_boundary': self._new_puls_up_boundary.get(), + 'panel_or_shell': self._new_panel_or_shell.get(), + 'material_factor': self._new_material_factor.get(), + 'spacing': self._new_stf_spacing.get(), + }, + shell_values={ + 'thickness': self._new_shell_thk.get(), + 'radius': self._new_shell_radius.get(), + 'distance_between_rings': self._new_shell_dist_rings.get(), + 'length': self._new_shell_length.get(), + 'total_length': self._new_shell_tot_length.get(), + 'k_factor': self._new_shell_k_factor.get(), + }, + longitudinal_values={ + 'spacing': self._new_stf_spacing.get(), + 'web_h': self._new_stf_web_h.get(), + 'web_t': self._new_stf_web_t.get(), + 'fl_w': self._new_stf_fl_w.get(), + 'fl_t': self._new_stf_fl_t.get(), + 'type': self._new_stf_type.get(), + }, + ring_stiffener_values={ + 'web_h': self._new_shell_ring_stf_hw.get(), + 'web_t': self._new_shell_ring_stf_tw.get(), + 'fl_w': self._new_shell_ring_stf_b.get(), + 'fl_t': self._new_shell_ring_stf_tf.get(), + 'type': self._new_shell_ring_stf_type.get(), + }, + ring_frame_values={ + 'web_h': self._new_shell_ring_frame_hw.get(), + 'web_t': self._new_shell_ring_frame_tw.get(), + 'fl_w': self._new_shell_ring_frame_b.get(), + 'fl_t': self._new_shell_ring_frame_tf.get(), + 'type': self._new_shell_ring_frame_type.get(), + }, + load_input={ + 'mode': self._new_shell_stress_or_force.get(), + 'Nsd': self._new_shell_Nsd.get(), + 'Msd': self._new_shell_Msd.get(), + 'Tsd': self._new_shell_Tsd.get(), + 'Qsd': self._new_shell_Qsd.get(), + 'sasd': self._new_shell_sasd.get(), + 'smsd': self._new_shell_smsd.get(), + 'tTsd': self._new_shell_tTsd.get(), + 'tQsd': self._new_shell_tQsd.get(), + 'psd': self._new_shell_psd.get(), + 'shsd': self._new_shell_shsd.get(), + }, + main_values={ + 'material_factor': self._new_shell_mat_factor.get(), + 'fab_method_ring_stiffener': self._new_shell_ring_stf_fab_method.get(), + 'fab_method_ring_frame': self._new_shell_ring_frame_fab_method.get(), + 'e_module': self._new_shell_e_module.get(), + 'poisson': self._new_shell_poisson.get(), + 'yield': self._new_shell_yield.get(), + 'length_between_girders': self._new_shell_ring_frame_length_between_girders.get(), + 'panel_spacing': self._new_shell_panel_spacing.get(), + 'ring_stiffener_excluded': self._new_shell_exclude_ring_stf.get(), + 'ring_frame_excluded': self._new_shell_exclude_ring_frame.get(), + 'uls_or_als': self._new_shell_uls_or_als.get(), + 'end_cap_pressure': self._new_shell_end_cap_pressure_included.get(), + }, + structure_types=self._structure_types, + ) + + def _build_cylinder_excel_import_defaults(self): + return project_services.CylinderExcelImportDefaults( + plate_thk=self._new_plate_thk.get(), + structure_type=self._new_stucture_type.get(), + sigma_y1=self._new_sigma_y1.get(), + sigma_y2=self._new_sigma_y2.get(), + sigma_x1=self._new_sigma_x1.get(), + sigma_x2=self._new_sigma_x2.get(), + tau_xy=self._new_tauxy.get(), + plate_kpp=self._new_plate_kpp.get(), + stf_kps=self._new_stf_kps.get(), + stf_km1=self._new_stf_km1.get(), + stf_km2=self._new_stf_km2.get(), + stf_km3=self._new_stf_km3.get(), + pressure_side=self._new_pressure_side.get(), + zstar_optimization=self._new_zstar_optimization.get(), + puls_method=self._new_puls_method.get(), + puls_boundary=self._new_puls_panel_boundary.get(), + puls_stiffener_end=self._new_buckling_stf_end_support.get(), + puls_sp_or_up=self._new_puls_sp_or_up.get(), + puls_up_boundary=self._new_puls_up_boundary.get(), + panel_or_shell=self._new_panel_or_shell.get(), + material_factor=self._new_material_factor.get(), + design_pressure=self._new_shell_psd.get(), + shear_stress=self._new_shell_shsd.get(), + e_module=self._new_shell_e_module.get(), + poisson=self._new_shell_poisson.get(), + length_between_girders=self._new_shell_ring_frame_length_between_girders.get(), + fab_method_ring_stiffener=self._new_shell_ring_stf_fab_method.get(), + fab_method_ring_frame=self._new_shell_ring_frame_fab_method.get(), + end_cap_pressure=self._new_shell_end_cap_pressure_included.get(), + structure_types=self._structure_types, + ring_stiffener_type=self._new_shell_ring_stf_type.get(), + ring_frame_type=self._new_shell_ring_frame_type.get(), + ) + + def _create_cylinder_structure_from_property_result(self, result): + return self._create_cylinder_structure_from_properties( + result.main_dict, + result.shell_dict, + result.longitudinal_dict, + result.ring_stiffener_dict, + result.ring_frame_dict, + result.geometry, + ) + + def _build_cylinder_structure_properties(self): + result = project_services.CylinderStructurePropertyService.build( + self._build_cylinder_structure_property_request() + ) + + sasd, smsd, tTsd, tQsd, _ = result.derived_stresses + Nsd, Msd, Tsd, Qsd = result.derived_forces + if self._new_shell_stress_or_force.get() == 1: + self._new_shell_sasd.set(sasd) + self._new_shell_smsd.set(smsd) + self._new_shell_tTsd.set(tTsd) + self._new_shell_tQsd.set(tQsd) + else: + self._new_shell_Nsd.set(Nsd) + self._new_shell_Msd.set(Msd) + self._new_shell_Tsd.set(Tsd) + self._new_shell_Qsd.set(Qsd) + + cylinder_obj = self._create_cylinder_structure_from_property_result(result) + + return ( + cylinder_obj, + result.main_dict, + result.shell_dict, + result.longitudinal_dict, + result.ring_stiffener_dict, + result.ring_frame_dict, + result.geometry, + ) + + def _structure_input_is_missing(self): + if not self._is_flat_calculation_domain(self._new_calculation_domain.get()): + return False + return any([self._new_stf_spacing.get() == 0, self._new_plate_thk.get() == 0, + self._new_stf_web_h.get() == 0, self._new_stf_web_t.get() == 0]) + + @staticmethod + def _show_missing_structure_input_warning(): + tk.messagebox.showwarning('No propertied defined', 'No properties is defined for the line!\n' + 'Define spacing, web height, web thickness etc.\n' + 'Either press button with stiffener or input manually.', type='ok') + + def _create_all_structure_from_properties(self, prop_dict): + calculation_domain = self._new_calculation_domain.get() + return AllStructure(Plate=CalcScantlings(prop_dict['Plate']), + Stiffener=None if calculation_domain == 'Flat plate, unstiffened' + else CalcScantlings(prop_dict['Stiffener']), + Girder=None if calculation_domain in ['Flat plate, unstiffened', + 'Flat plate, stiffened'] + else CalcScantlings(prop_dict['Girder']), + main_dict=prop_dict['main dict']) + + def _create_cylinder_structure_from_properties(self, main_dict_cyl, shell_dict, long_dict, ring_stf_dict, + ring_frame_dict, geometry): + ring_stf_excluded = main_dict_cyl.get('ring stf excluded', [self._new_shell_exclude_ring_stf.get()])[0] + ring_frame_excluded = main_dict_cyl.get('ring frame excluded', [self._new_shell_exclude_ring_frame.get()])[0] + return CylinderAndCurvedPlate(main_dict_cyl, Shell(shell_dict), + long_stf=None if geometry in [1, 2, 5, 6] + else Structure(long_dict), + ring_stf=None if any([geometry in [1, 2, 3, 4], + ring_stf_excluded]) + else Structure(ring_stf_dict), + ring_frame=None if any([geometry in [1, 2, 3, 4], + ring_frame_excluded]) + else Structure(ring_frame_dict)) + + def _clear_tanks_and_grid(self): + self._tank_dict = {} + self._main_grid.clear() + self._compartments_listbox.delete(0, 'end') + + def _refresh_after_structure_change(self, suspend_recalc): + if not suspend_recalc: + # when changing multiple parameters, recalculations are suspended. + project_services.mark_lines_for_recalculation(self._line_to_struc) + state = self.update_frame() + if state != None and self._line_is_active: + self._weight_logger['new structure']['COG'].append(self.get_color_and_calc_state()['COG']) + self._weight_logger['new structure']['weight'].append(self.get_color_and_calc_state()['Total weight']) + self._weight_logger['new structure']['time'].append(time.time()) + self.cylinder_gui_mods() + + self.get_unique_plates_and_beams() + + def _resolve_new_structure_properties(self, pasted_structure=None, multi_return=None, toggle_multi=None, + cylinder_return=None): + CylinderObj = None + obj_dict_stf = None + main_dict_cyl = shell_dict = long_dict = ring_stf_dict = ring_frame_dict = geometry = None + + if multi_return is not None: + prop_dict = multi_return[0].get_main_properties() # From optimizer. + elif isinstance(toggle_multi, tuple): + prop_dict, obj_dict_stf = toggle_multi + elif toggle_multi is not None: + prop_dict = toggle_multi + elif pasted_structure is None: + prop_dict, obj_dict_stf = self._build_flat_structure_properties() + + if not self._is_flat_calculation_domain(self._new_calculation_domain.get()) and cylinder_return is None: + CylinderObj, main_dict_cyl, shell_dict, long_dict, ring_stf_dict, ring_frame_dict, geometry = \ + self._build_cylinder_structure_properties() + elif cylinder_return is not None: + main_dict_cyl, shell_dict, long_dict, ring_stf_dict, ring_frame_dict = \ + cylinder_return.get_all_properties() + geometry = main_dict_cyl['geometry'][0] + else: + prop_dict = pasted_structure.get_main_properties() + + if cylinder_return is not None: + CylinderObj = cylinder_return + main_dict_cyl, shell_dict, long_dict, ring_stf_dict, ring_frame_dict = \ + cylinder_return.get_all_properties() + geometry = main_dict_cyl['geometry'][0] + + if obj_dict_stf is None and isinstance(prop_dict, dict): + obj_dict_stf = prop_dict.get('Stiffener') + + return (prop_dict, obj_dict_stf, CylinderObj, main_dict_cyl, shell_dict, long_dict, ring_stf_dict, + ring_frame_dict, geometry) + + def _add_structure_to_active_line(self, prop_dict, obj_dict_stf, CylinderObj, main_dict_cyl, shell_dict, + long_dict, ring_stf_dict, ring_frame_dict, geometry): + All = self._create_all_structure_from_properties(prop_dict) + line_structures = project_services.LineStructureService(self._line_to_struc) + line_structures.assign_structure(self._active_line, All, cylinder=CylinderObj) + + self._sections = add_new_section(self._sections, struc.Section(obj_dict_stf)) # TODO error when pasting + if line_structures.structure(self._active_line).Plate.get_structure_type() not in \ + self._structure_types['non-wt']: + self._clear_tanks_and_grid() + if not self._is_flat_calculation_domain(self._new_calculation_domain.get()): + if CylinderObj is None: + CylinderObj = self._create_cylinder_structure_from_properties( + main_dict_cyl, shell_dict, long_dict, ring_stf_dict, ring_frame_dict, geometry) + line_structures.set_cylinder(self._active_line, CylinderObj) + + def _scale_existing_flat_structure_if_needed(self, prev_all_obj): + line_structures = project_services.LineStructureService(self._line_to_struc) + if self._new_scale_stresses.get() and prev_all_obj.get_main_properties() != \ + line_structures.structure(self._active_line).get_main_properties(): + if prev_all_obj.Stiffener is not None: + plate = line_structures.structure(self._active_line).Plate + stiffener = line_structures.structure(self._active_line).Stiffener + girder = line_structures.structure(self._active_line).Girder + calc_tup = (plate.get_s(), plate.get_pl_thk(), stiffener.get_web_h(), stiffener.get_web_thk(), + stiffener.get_fl_w(), + stiffener.get_fl_thk(), plate.span, stiffener.girder_lg if girder is None else + girder.girder_lg, stiffener.stiffener_type) + else: + calc_tup = line_structures.structure(self._active_line).Plate.get_tuple() + line_structures.replace_structure( + self._active_line, + op.create_new_calc_obj(prev_all_obj, calc_tup, fup=self._new_fup.get(), fdwn=self._new_fdwn.get())[0], + ) + + def _sync_fatigue_object_after_structure_update(self, prop_dict): + project_services.LineStructureService(self._line_to_struc).sync_fatigue_after_structure_update( + self._active_line, prop_dict) + + def _sync_cylinder_object_after_structure_update(self, CylinderObj, cylinder_return): + line_structures = project_services.LineStructureService(self._line_to_struc) + if all([CylinderObj is None, cylinder_return is None, + line_structures.cylinder(self._active_line) is not None]): + line_structures.set_cylinder(self._active_line, None) + elif CylinderObj is not None: + if line_structures.cylinder(self._active_line) is not None and self._new_scale_stresses.get(): + NewCylinderObj = op.create_new_cylinder_obj(line_structures.cylinder(self._active_line), + CylinderObj.get_x_opt()) + NewCylinderObj.LongStfObj = None if CylinderObj.LongStfObj is None \ + else NewCylinderObj.LongStfObj + NewCylinderObj.RingStfObj = None if CylinderObj.RingStfObj is None \ + else NewCylinderObj.RingStfObj + NewCylinderObj.RingFrameObj = None if CylinderObj.RingFrameObj is None \ + else NewCylinderObj.RingFrameObj + line_structures.set_cylinder(self._active_line, CylinderObj) + elif cylinder_return is not None: + line_structures.set_cylinder(self._active_line, cylinder_return) + + def _update_existing_active_line_structure(self, prop_dict, CylinderObj, cylinder_return): + line_structures = project_services.LineStructureService(self._line_to_struc) + prev_type = line_structures.structure(self._active_line).Plate.get_structure_type() + prev_all_obj = copy.deepcopy(line_structures.structure(self._active_line)) + line_structures.update_structure_properties(self._active_line, prop_dict) + + self._scale_existing_flat_structure_if_needed(prev_all_obj) + self._sync_fatigue_object_after_structure_update(prop_dict) + + if prev_type in self._structure_types['non-wt'] and prop_dict['Plate']['structure_type'][0] in \ + self._structure_types['internals'] + self._structure_types['horizontal'] + \ + self._structure_types['vertical']: + self._clear_tanks_and_grid() + + self._sync_cylinder_object_after_structure_update(CylinderObj, cylinder_return) + + def _calculate_load_combinations_after_structure_update(self): + try: + self.calculate_all_load_combinations_for_line_all_lines() + except (KeyError, AttributeError): + pass + def new_structure(self, event = None, pasted_structure = None, multi_return = None, toggle_multi = None, suspend_recalc = False, cylinder_return = None): ''' @@ -5193,368 +6473,30 @@ def new_structure(self, event = None, pasted_structure = None, multi_return = No if multi_return is None: self.save_no_dialogue(backup=True) #keeping a backup - if all([pasted_structure == None, multi_return == None]): - missing_input = False - if self._new_calculation_domain.get() in ['Flat plate, stiffened', 'Flat plate, unstiffened', - 'Flat plate, stiffened with girder']: - if any([self._new_stf_spacing.get()==0, self._new_plate_thk.get()==0, self._new_stf_web_h.get()==0, - self._new_stf_web_t.get()==0]): # TODO must account for calculation domain - missing_input = True - - if missing_input: - mess = tk.messagebox.showwarning('No propertied defined', 'No properties is defined for the line!\n' - 'Define spacing, web height, web thickness etc.\n' - 'Either press button with stiffener or input ' - 'manually.', type='ok') + if all([pasted_structure == None, multi_return == None, toggle_multi == None]): + if self._structure_input_is_missing(): + self._show_missing_structure_input_warning() return if self._line_is_active or multi_return != None: # structure dictionary: name of line : [ 0.Structure class, 1.calc scantling class, # 2.calc fatigue class, 3.load object, 4.load combinations result ] - CylinderObj = None - if multi_return is not None: - prop_dict = multi_return[0].get_main_properties() # From optimizer. - elif toggle_multi is not None: - prop_dict = toggle_multi - elif pasted_structure is None: - calc_dom = self._new_calculation_domain.get() - - obj_dict = {'mat_yield': [self._new_material.get()*1e6, 'Pa'], - 'mat_factor': [self._new_material_factor.get(), ''], - 'span': [self._new_field_len.get()/1000, 'm'], - 'spacing': [self._new_stf_spacing.get()/1000, 'm'], - 'plate_thk': [self._new_plate_thk.get()/1000, 'm'], - 'stf_web_height': [self._new_stf_web_h.get()/1000, 'm'], - 'stf_web_thk': [self._new_stf_web_t.get()/1000, 'm'], - 'stf_flange_width': [self._new_stf_fl_w.get()/1000, 'm'], - 'stf_flange_thk': [self._new_stf_fl_t.get()/1000, 'm'], - 'structure_type': [self._new_stucture_type.get(), ''], - 'stf_type': [self._new_stf_type.get(), ''], - 'sigma_y1': [self._new_sigma_y1.get(), 'MPa'], - 'sigma_y2': [self._new_sigma_y2.get(), 'MPa'], - 'sigma_x1': [self._new_sigma_x1.get(), 'MPa'], - 'sigma_x2': [self._new_sigma_x2.get(), 'MPa'], - 'tau_xy': [self._new_tauxy.get(), 'MPa'], - 'plate_kpp': [self._new_plate_kpp.get(), ''], - 'stf_kps': [self._new_stf_kps.get(), ''], - 'stf_km1': [self._new_stf_km1.get(), ''], - 'stf_km2': [self._new_stf_km2.get(), ''], - 'stf_km3': [self._new_stf_km3.get(), ''], - 'press_side': [self._new_pressure_side.get(), ''], - 'structure_types':[self._structure_types, ''], - 'zstar_optimization': [self._new_zstar_optimization.get(), ''], - 'puls buckling method': [self._new_puls_method.get(), ''], - 'puls boundary': [self._new_puls_panel_boundary.get(), ''], - 'puls stiffener end': [self._new_buckling_stf_end_support.get(), ''], - 'puls sp or up': [self._new_puls_sp_or_up.get(), ''], - 'puls up boundary': [self._new_puls_up_boundary.get(), ''], - 'panel or shell': [self._new_panel_or_shell.get(), ''], - 'girder_lg': [self._new_girder_length_LG.get()/1000, '']} - - obj_dict_pl = copy.copy(obj_dict) - obj_dict_stf = copy.copy(obj_dict) - obj_dict_girder = copy.copy(obj_dict) - - obj_dict_girder['stf_web_height'] = [self._new_girder_web_h.get()/1000, 'm'] - obj_dict_girder['stf_web_thk'] = [self._new_girder_web_t.get() / 1000, 'm'] - obj_dict_girder['stf_flange_width'] = [self._new_girder_fl_w.get() / 1000, 'm'] - obj_dict_girder['stf_flange_thk'] = [self._new_girder_fl_t.get() / 1000, 'm'] - obj_dict_girder['stf_type'] = [self._new_girder_type.get(), ''] - - main_dict = dict() - main_dict['minimum pressure in adjacent spans'] = [self._new_buckling_min_press_adj_spans.get(), ''] - main_dict['material yield'] = [self._new_material.get()*1e6, 'Pa'] - main_dict['load factor on stresses'] = [self._new_buckling_lf_stresses.get(), ''] - main_dict['load factor on pressure'] = [1, ''] - main_dict['buckling method'] = [self._new_puls_method.get(), ''] - main_dict['stiffener end support'] =[self._new_buckling_stf_end_support.get(), ''] # 'Continuous' - main_dict['girder end support'] = [self._new_buckling_girder_end_support.get(), ''] # 'Continuous' - main_dict['tension field'] = [self._new_buckling_tension_field.get(), ''] # 'not allowed' - main_dict['plate effective agains sigy'] = [self._new_buckling_effective_against_sigy.get(), ''] # True - main_dict['buckling length factor stf'] = [self._new_buckling_length_factor_stf.get(), ''] - main_dict['buckling length factor girder'] = [self._new_buckling_length_factor_stf.get(), ''] - main_dict['km3'] = [self._new_buckling_km3.get(), ''] # 12 - main_dict['km2'] = [self._new_buckling_km2.get(), ''] # 24 - main_dict['girder distance between lateral support'] = [self._new_buckling_girder_dist_bet_lat_supp.get(), ''] - main_dict['stiffener distance between lateral support'] = [self._new_buckling_stf_dist_bet_lat_supp.get(), ''] - main_dict['panel length, Lp'] = [self._new_panel_length_Lp.get(), ''] - main_dict['pressure side'] = [self._new_pressure_side.get(), ''] # either 'stiffener', 'plate', 'both' - main_dict['fabrication method stiffener'] = [self._new_buckling_fab_method_stf.get(), ''] - main_dict['fabrication method girder'] = [self._new_buckling_fab_method_girder.get(), ''] - main_dict['calculation domain'] = [self._new_calculation_domain.get(), ''] - - - prop_dict = {'main dict': main_dict, - 'Plate': obj_dict_pl, - 'Stiffener': None if calc_dom == 'Flat plate, unstiffened' else obj_dict_stf, - 'Girder': None if calc_dom in ['Flat plate, unstiffened', 'Flat plate, stiffened'] - else obj_dict_girder} - - if self._new_calculation_domain.get() not in ['Flat plate, stiffened','Flat plate, unstiffened', - 'Flat plate, stiffened with girder'] and cylinder_return is None: - ''' - Shell structure. - 0:'Stiffened panel, flat', 1:'Unstiffened shell (Force input)', 2:'Unstiffened panel (Stress input)', - 3:'Longitudinal Stiffened shell (Force input)', 4:'Longitudinal Stiffened panel (Stress input)', - 5:'Ring Stiffened shell (Force input)', 6:'Ring Stiffened panel (Stress input)', - 7:'Orthogonally Stiffened shell (Force input)', 8:'Orthogonally Stiffened panel (Stress input)' - ''' - domain_string = self._new_calculation_domain.get() - domain_int = self._shell_geometries_map[domain_string] - - dummy_data = {'span': [self._new_field_len.get()/1000, 'm'], - 'plate_thk': [self._new_plate_thk.get()/1000, 'm'], - 'structure_type': [self._new_stucture_type.get(), ''], - 'sigma_y1': [self._new_sigma_y1.get(), 'MPa'], - 'sigma_y2': [self._new_sigma_y2.get(), 'MPa'], - 'sigma_x1': [self._new_sigma_x1.get(), 'MPa'], - 'sigma_x2': [self._new_sigma_x2.get(), 'MPa'], - 'tau_xy': [self._new_tauxy.get(), 'MPa'], - 'plate_kpp': [self._new_plate_kpp.get(), ''], - 'stf_kps': [self._new_stf_kps.get(), ''], - 'stf_km1': [self._new_stf_km1.get(), ''], - 'stf_km2': [self._new_stf_km2.get(), ''], - 'stf_km3': [self._new_stf_km3.get(), ''], - 'press_side': [self._new_pressure_side.get(), ''], - 'structure_types':[self._structure_types, ''], - 'zstar_optimization': [self._new_zstar_optimization.get(), ''], - 'puls buckling method': [self._new_puls_method.get(), ''], - 'puls boundary': [self._new_puls_panel_boundary.get(), ''], - 'puls stiffener end': [self._new_buckling_stf_end_support.get(), ''], - 'puls sp or up': [self._new_puls_sp_or_up.get(), ''], - 'puls up boundary': [self._new_puls_up_boundary.get(), ''], - 'panel or shell': [self._new_panel_or_shell.get(), ''], - 'mat_factor': [self._new_material_factor.get(), '',], - 'spacing': [self._new_stf_spacing.get()/1000, 'm'],} - - # Main class input - - # Shell data input - shell_dict = {'plate_thk': [self._new_shell_thk.get() / 1000, 'm'], - 'radius': [self._new_shell_radius.get() / 1000, 'm'], - 'distance between rings, l': [self._new_shell_dist_rings.get() / 1000, 'm'], - 'length of shell, L': [self._new_shell_length.get() / 1000, 'm'], - 'tot cyl length, Lc': [self._new_shell_tot_length.get() / 1000, 'm'], - 'eff. buckling lenght factor': [self._new_shell_k_factor.get(), ''], - 'mat_yield': [self._new_shell_yield.get() * 1e6, 'Pa'], - } - # Longitudinal stiffener input - long_dict = {'spacing': [self._new_stf_spacing.get() / 1000, 'm'], - 'stf_web_height': [self._new_stf_web_h.get() / 1000, 'm'], - 'stf_web_thk': [self._new_stf_web_t.get() / 1000, 'm'], - 'stf_flange_width': [self._new_stf_fl_w.get() / 1000, 'm'], - 'stf_flange_thk': [self._new_stf_fl_t.get() / 1000, 'm'], - 'stf_type': [self._new_stf_type.get(), ''], - 'span': [self._new_field_len.get()/1000, 'm'], - 'mat_yield': [self._new_shell_yield.get() * 1e6, 'Pa'], - 'panel or shell': ['shell', '']} - ring_stf_dict = {'stf_web_height': [self._new_shell_ring_stf_hw.get() / 1000, 'm'], - 'stf_web_thk': [self._new_shell_ring_stf_tw.get() / 1000, 'm'], - 'stf_flange_width': [self._new_shell_ring_stf_b.get() / 1000, 'm'], - 'stf_flange_thk': [self._new_shell_ring_stf_tf.get() / 1000, 'm'], - 'stf_type': [self._new_shell_ring_stf_type.get(), ''], - 'mat_yield': [self._new_shell_yield.get() * 1e6, 'Pa'], - 'panel or shell': ['shell', '']} - ring_frame_dict = {'stf_web_height': [self._new_shell_ring_frame_hw.get() / 1000, 'm'], - 'stf_web_thk': [self._new_shell_ring_frame_tw.get() / 1000, 'm'], - 'stf_flange_width': [self._new_shell_ring_frame_b.get() / 1000, 'm'], - 'stf_flange_thk': [self._new_shell_ring_frame_tf.get() / 1000, 'm'], - 'stf_type': [self._new_shell_ring_frame_type.get(), ''], - 'span': [self._new_field_len.get()/1000, 'm'], - 'mat_yield': [self._new_shell_yield.get() * 1e6, 'Pa'], - 'panel or shell': ['shell', '']} - - geometry = self._shell_geometries_map[self._new_calculation_domain.get()] - - if self._new_shell_stress_or_force.get() == 1: - forces = [self._new_shell_Nsd.get(), self._new_shell_Msd.get(), - self._new_shell_Tsd.get(), self._new_shell_Qsd.get()] - sasd, smsd, tTsd, tQsd, shsd = hlp.helper_cylinder_stress_to_force_to_stress( - stresses=None, forces=forces, geometry=geometry, shell_t=self._new_shell_thk.get(), - shell_radius=self._new_shell_radius.get(), shell_spacing=self._new_stf_spacing.get(), - hw=self._new_stf_web_h.get(), tw=self._new_stf_web_t.get(), b=self._new_stf_fl_w.get(), - tf=self._new_stf_fl_t.get(), CylinderAndCurvedPlate=CylinderAndCurvedPlate) - self._new_shell_sasd.set(sasd) - self._new_shell_smsd.set(smsd) - self._new_shell_tTsd.set(tTsd) - self._new_shell_tQsd.set(tQsd) - #self._new_shell_shsd.set(0) - else: - stresses = [self._new_shell_sasd.get(), self._new_shell_smsd.get(), - abs(self._new_shell_tTsd.get()), - self._new_shell_tQsd.get(), self._new_shell_shsd.get()] - sasd, smsd, tTsd, tQsd, shsd = stresses - Nsd, Msd, Tsd, Qsd, shsd = hlp.helper_cylinder_stress_to_force_to_stress( - stresses=stresses, geometry=geometry, shell_t=self._new_shell_thk.get(), - shell_radius=self._new_shell_radius.get(), shell_spacing=self._new_stf_spacing.get(), - hw=self._new_stf_web_h.get(), tw=self._new_stf_web_t.get(), b=self._new_stf_fl_w.get(), - tf=self._new_stf_fl_t.get(), CylinderAndCurvedPlate=CylinderAndCurvedPlate) - self._new_shell_Nsd.set(Nsd) - self._new_shell_Msd.set(Msd) - self._new_shell_Tsd.set(Tsd) - self._new_shell_Qsd.set(Qsd) - - main_dict_cyl = {'sasd': [sasd*1e6, 'Pa'], - 'smsd': [smsd*1e6, 'Pa'], - 'tTsd': [tTsd*1e6, 'Pa'], - 'tQsd': [tQsd*1e6, 'Pa'], - 'psd': [self._new_shell_psd.get() *1e6, 'Pa'], - 'shsd': [shsd *1e6, 'Pa'], - 'geometry': [self._shell_geometries_map[self._new_calculation_domain.get()], ''], - 'material factor': [self._new_shell_mat_factor.get(), ''], - 'delta0': [0.005, ''], - 'fab method ring stf': [self._new_shell_ring_stf_fab_method.get(), ''], - 'fab method ring girder': [self._new_shell_ring_frame_fab_method.get(), ''], - 'E-module': [self._new_shell_e_module.get(), 'Pa'], - 'poisson': [self._new_shell_poisson.get(), ''], - 'mat_yield': [self._new_shell_yield.get() *1e6, 'Pa'], - 'length between girders': [self._new_shell_ring_frame_length_between_girders.get()/1000, 'm'], - 'panel spacing, s': [self._new_shell_panel_spacing.get()/1000, 'm'], - 'ring stf excluded': [self._new_shell_exclude_ring_stf.get(), ''], - 'ring frame excluded': [self._new_shell_exclude_ring_frame.get(), '',], - 'ULS or ALS': [self._new_shell_uls_or_als.get(), '',], - 'end cap pressure': [self._new_shell_end_cap_pressure_included.get(), ''] - } - - for key, value in dummy_data.items(): - if key not in long_dict.keys(): - long_dict[key] = value - if key not in ring_stf_dict.keys(): - ring_stf_dict[key] = value - if key not in ring_frame_dict.keys(): - ring_frame_dict[key] = value - - CylinderObj = CylinderAndCurvedPlate(main_dict_cyl, Shell(shell_dict), - long_stf=None if geometry in [1,2,5,6] - else Structure(long_dict), - ring_stf=None if any([geometry in [1,2,3,4], - self._new_shell_exclude_ring_stf.get()]) - else Structure(ring_stf_dict), - ring_frame=None if any([geometry in [1,2,3,4], - self._new_shell_exclude_ring_frame.get()]) - else Structure(ring_frame_dict)) - elif cylinder_return is not None: - main_dict_cyl, shell_dict, long_dict, ring_stf_dict, ring_frame_dict = \ - cylinder_return.get_all_properties() - else: - prop_dict = pasted_structure.get_main_properties() - + prop_dict, obj_dict_stf, CylinderObj, main_dict_cyl, shell_dict, long_dict, ring_stf_dict, \ + ring_frame_dict, geometry = self._resolve_new_structure_properties( + pasted_structure=pasted_structure, multi_return=multi_return, toggle_multi=toggle_multi, + cylinder_return=cylinder_return) if self._active_line not in self._line_to_struc.keys() : - self._line_to_struc[self._active_line] = [None, None, None, [None], {}, None] - # First entry - # Flat plate domains: 'Flat plate, stiffened with girder', 'Flat plate, stiffened', Flat plate, unstiffened' - cdom = self._new_calculation_domain.get() - All = AllStructure(Plate=CalcScantlings(prop_dict['Plate']), - Stiffener=None if cdom == 'Flat plate, unstiffened' - else CalcScantlings(prop_dict['Stiffener']), - Girder=None if cdom in ['Flat plate, unstiffened', 'Flat plate, stiffened'] - else CalcScantlings(prop_dict['Girder']), - main_dict=prop_dict['main dict']) - - self._sections = add_new_section(self._sections, struc.Section(obj_dict_stf)) # TODO error when pasting - self._line_to_struc[self._active_line][0] = All - self._line_to_struc[self._active_line][5] = CylinderObj - if self._line_to_struc[self._active_line][0].Plate.get_structure_type() not in \ - self._structure_types['non-wt']: - self._tank_dict = {} - self._main_grid.clear() - self._compartments_listbox.delete(0, 'end') - if self._new_calculation_domain.get() not in ['Flat plate, stiffened','Flat plate, unstiffened', - 'Flat plate, stiffened with girder']: - CylinderObj = CylinderAndCurvedPlate(main_dict_cyl, Shell(shell_dict), - long_stf=None if geometry in [1,2,5,6] - else Structure(long_dict), - ring_stf=None if any([geometry in [1,2,3,4], - self._new_shell_exclude_ring_stf.get()]) - else Structure(ring_stf_dict), - ring_frame=None if any([geometry in [1,2,3,4], - self._new_shell_exclude_ring_frame.get()]) - else Structure(ring_frame_dict)) - - self._line_to_struc[self._active_line][5] = CylinderObj - + self._add_structure_to_active_line(prop_dict, obj_dict_stf, CylinderObj, main_dict_cyl, shell_dict, + long_dict, ring_stf_dict, ring_frame_dict, geometry) else: - # if self._new_calculation_domain.get() in ['Flat plate, stiffened','Flat plate, unstiffened', - # 'Flat plate, stiffened with girder'] and \ - # self._line_to_struc[self._active_line][5] is not None: - # self._line_to_struc[self._active_line][5] = None - - prev_type = self._line_to_struc[self._active_line][0].Plate.get_structure_type() - prev_all_obj = copy.deepcopy(self._line_to_struc[self._active_line][0]) - self._line_to_struc[self._active_line][0].set_main_properties(prop_dict) - - if self._new_scale_stresses.get() and prev_all_obj.get_main_properties() != \ - self._line_to_struc[self._active_line][0].get_main_properties(): - if prev_all_obj.Stiffener is not None: - plate = self._line_to_struc[self._active_line][0].Plate - stiffener = self._line_to_struc[self._active_line][0].Stiffener - girder = self._line_to_struc[self._active_line][0].Girder - calc_tup = (plate.get_s(), plate.get_pl_thk(), stiffener.get_web_h(), stiffener.get_web_thk(), - stiffener.get_fl_w(), - stiffener.get_fl_thk(), plate.span, stiffener.girder_lg if girder is None else - girder.girder_lg, stiffener.stiffener_type) - else: - calc_tup = self._line_to_struc[self._active_line][0].Plate.get_tuple() - self._line_to_struc[self._active_line][0] = op.create_new_calc_obj(prev_all_obj, calc_tup, - fup=self._new_fup.get(), - fdwn=self._new_fdwn.get())[0] - - self._line_to_struc[self._active_line][0].need_recalc = True - - if self._line_to_struc[self._active_line][2] is not None: - calc_dom = self._line_to_struc[self._active_line][0].calculation_domain - if calc_dom == 'Flat plate, unstiffened': - self._line_to_struc[self._active_line][2] = None - else: - self._line_to_struc[self._active_line][2].set_main_properties(prop_dict['Stiffener']) - - if prev_type in self._structure_types['non-wt'] and prop_dict['Plate']['structure_type'][0] in \ - self._structure_types['internals'] + self._structure_types['horizontal'] + \ - self._structure_types['vertical']: - self._tank_dict = {} - self._main_grid.clear() - self._compartments_listbox.delete(0, 'end') - - if all([CylinderObj is None, cylinder_return is None, - self._line_to_struc[self._active_line][5] is not None]): - self._line_to_struc[self._active_line][5] = None - elif CylinderObj is not None: - if self._line_to_struc[self._active_line][5] is not None and self._new_scale_stresses.get(): - NewCylinderObj = op.create_new_cylinder_obj(self._line_to_struc[self._active_line][5], - CylinderObj.get_x_opt()) - NewCylinderObj.LongStfObj = None if CylinderObj.LongStfObj is None \ - else NewCylinderObj.LongStfObj - NewCylinderObj.RingStfObj = None if CylinderObj.RingStfObj is None \ - else NewCylinderObj.RingStfObj - NewCylinderObj.RingFrameObj = None if CylinderObj.RingFrameObj is None \ - else NewCylinderObj.RingFrameObj - self._line_to_struc[self._active_line][5] = CylinderObj - elif cylinder_return is not None: - self._line_to_struc[self._active_line][5] = cylinder_return - try: - self.calculate_all_load_combinations_for_line_all_lines() - except (KeyError, AttributeError): - pass + self._update_existing_active_line_structure(prop_dict, CylinderObj, cylinder_return) + self._calculate_load_combinations_after_structure_update() else: pass - if self._PULS_results != None: - self._PULS_results.result_changed(self._active_line) - - if not suspend_recalc: - # when changing multiple parameters, recalculations are suspended. - for line, obj in self._line_to_struc.items(): - obj[0].need_recalc = True - state = self.update_frame() - if state != None and self._line_is_active: - self._weight_logger['new structure']['COG'].append(self.get_color_and_calc_state()['COG']) - self._weight_logger['new structure']['weight'].append(self.get_color_and_calc_state()['Total weight']) - self._weight_logger['new structure']['time'].append(time.time()) - self.cylinder_gui_mods() - - self.get_unique_plates_and_beams() + self._refresh_after_structure_change(suspend_recalc) def option_meny_structure_type_trace(self, event): ''' Updating of the values in the structure type option menu. ''' @@ -5639,42 +6581,11 @@ def calculate_all_load_combinations_for_line(self, line, limit_state = 'ULS', ge self._load_conditions = ['loaded', 'ballast','tanktest'] :return: ''' - if limit_state == 'FLS': - return - results = {} #dict - dnva/dnvb/tanktest/manual - load_info = [] - # calculating for DNV a and DNV b - - - for dnv_ab in ['dnva', 'dnvb']: #, load_factors in self._load_factors_dict.items(): - results[dnv_ab] = [] - for load_condition in self._load_conditions[0:2]: - returned = self.calculate_one_load_combination(line, dnv_ab, load_condition) - if returned != None: - results[dnv_ab].append(returned[0]) - [load_info.append(val) for val in returned[1]] - - # calculating for tank test condition - results['tanktest'] = [] - res_val = self.calculate_one_load_combination(line, "tanktest", 'tanktest') - results['tanktest'].append(res_val[0]) - [load_info.append(val) for val in res_val[1]] - - - # calculating for manual condition - results['manual'] = [] - res_val = self.calculate_one_load_combination(line, 'manual', 'manual') - results['manual'].append(res_val[0]) - [load_info.append(val) for val in res_val[1]] - - results['slamming'] = [] - res_val = self.calculate_one_load_combination(line, 'slamming', 'slamming') - results['slamming'].append(res_val[0]) - [load_info.append(val) for val in res_val[1]] - - if get_load_info: - return load_info - return results + return project_services.LinePressureService.calculate_combinations( + self._build_line_pressure_input(line), + limit_state=limit_state, + get_load_info=get_load_info, + ) def calculate_one_load_combination(self, current_line, comb_name, load_condition): ''' @@ -5684,36 +6595,29 @@ def calculate_one_load_combination(self, current_line, comb_name, load_condition #load combination dictionary (comb,line,load) : [stat - DoubleVar(), dyn - DoubleVar], on/off - IntVar()] :return: ''' + return project_services.LinePressureService.calculate_one( + self._build_line_pressure_input(current_line), + comb_name, + load_condition, + ) - defined_loads = [] - for load_obj in self._line_to_struc[current_line][3]: - if load_obj is not None: - if load_obj.get_limit_state() != 'FLS': - defined_loads.append(load_obj) + def _build_line_pressure_input(self, current_line): if self._tank_dict == {}: - defined_tanks = [] - else: - defined_tanks = [['comp'+str(int(tank_num)), self._tank_dict['comp'+str(int(tank_num))]] - for tank_num in self.get_compartments_for_line_duplicates(current_line)] - - coord = (self.get_line_radial_mid(current_line), self.get_line_low_elevation(current_line)) - - if load_condition not in ['tanktest','manual','slamming']: - acc = (self._accelerations_dict['static'], self._accelerations_dict['dyn_'+str(load_condition)]) - else: - acc = (self._accelerations_dict['static'], 0) - - load_factors_all = self._new_load_comb_dict - - current_line_obj = [current_line, self._line_to_struc[current_line][0].Plate] - - if self._line_to_struc[current_line][0].Plate.get_structure_type() in ['', 'FRAME','GENERAL_INTERNAL_NONWT']: - return [0, ''] + defined_tanks = () else: - return_value = one_load_combination(current_line_obj, coord, defined_loads, load_condition, - defined_tanks, comb_name, acc, load_factors_all) - - return return_value + defined_tanks = tuple( + ('comp' + str(int(tank_num)), self._tank_dict['comp' + str(int(tank_num))]) + for tank_num in self.get_compartments_for_line_duplicates(current_line) + ) + + return project_services.LinePressureInput( + line_name=current_line, + line_bundle=self._line_to_struc[current_line], + coordinate=(self.get_line_radial_mid(current_line), self.get_line_low_elevation(current_line)), + defined_tanks=defined_tanks, + accelerations=self._accelerations_dict, + load_factors=project_services.load_factor_records(self._new_load_comb_dict), + ) def run_optimizer_for_line(self,line,goal,constrains): ''' @@ -5737,10 +6641,7 @@ def update_tank(self): current_tank.set_content(self._new_content_type.get()) current_tank.set_acceleration(self._accelerations_dict) current_tank.set_density(self._new_density.get()) - for line, obj in self._line_to_struc.items(): - obj[0].need_recalc = True - if self._compartments_listbox.get('active') in self.get_compartments_for_line(line): - self._PULS_results.result_changed(line) + project_services.mark_lines_for_recalculation(self._line_to_struc) def delete_line(self, event = None, undo = None, line = None): ''' @@ -5755,19 +6656,19 @@ def delete_line(self, event = None, undo = None, line = None): if line in self._line_dict.keys() or undo is not None: line = line if undo is None else undo - point_str = 'p' + str(self._line_dict[line][0]) + 'p' + str(self._line_dict[line][1]) - point_str_rev = 'p' + str(self._line_dict[line][1]) + 'p' + str(self._line_dict[line][0]) + project_editor = project_services.ProjectEditService(self._point_dict, self._line_dict) + line_record = project_editor.line(line) if line in self._line_dict.keys(): if line in self._line_to_struc.keys(): if self._line_to_struc[line][0].Plate.get_structure_type() not in self._structure_types['non-wt']: self.delete_properties_pressed() self.delete_all_tanks() - self._line_dict.pop(line) + project_editor.remove_line(line) if line in self._line_to_struc.keys(): self._line_to_struc.pop(line) - self._line_point_to_point_string.pop(self._line_point_to_point_string.index(point_str)) - self._line_point_to_point_string.pop(self._line_point_to_point_string.index(point_str_rev)) + for endpoint_key in line_record.endpoint_keys: + self._line_point_to_point_string.pop(self._line_point_to_point_string.index(endpoint_key)) self._active_line = '' # Removing from load dict @@ -5776,10 +6677,6 @@ def delete_line(self, event = None, undo = None, line = None): for load in loads: if line in self._load_dict[load][1]: self._load_dict[load][1].pop(self._load_dict[load][1].index(line)) - # Removing from puls results - if self._PULS_results is not None: - self._PULS_results.result_changed(line) - self.update_frame() else: messagebox.showinfo(title='No line.', message='Input line does noe exist.') @@ -5796,11 +6693,8 @@ def delete_point(self, event = None, undo = None, point = None): point = 'point' + str(self._ent_delete_point.get()) if undo is None else undo if point in self._point_dict.keys(): - line_to_delete = [] - # finding the lines that needs to be deleted - for line, points in self._line_dict.items(): - if int(self._ent_delete_point.get()) in points: - line_to_delete.append(line) + project_editor = project_services.ProjectEditService(self._point_dict, self._line_dict) + line_to_delete = project_editor.connected_line_names(point) # deleting the lines and the connected properties. also deleting point to point string list items. for line in list(line_to_delete): self.delete_line(line = line) @@ -5813,7 +6707,7 @@ def delete_point(self, event = None, undo = None, point = None): # if line in self._line_to_struc.keys(): # self._line_to_struc.pop(line) # at the en, the points is deleted from the point dict. - self._point_dict.pop(point) + project_editor.remove_point(point) self._active_point = '' else: messagebox.showinfo(title='No point.', message='Input point does not exist.') @@ -5869,8 +6763,7 @@ def delete_properties_pressed(self, event = None, line = None): action_taken = True if action_taken: - for line, obj in self._line_to_struc.items(): - obj[0].need_recalc = True + project_services.mark_lines_for_recalculation(self._line_to_struc) self.update_frame() def delete_all_tanks(self): @@ -5985,7 +6878,7 @@ def set_selected_variables(self, line): self._new_shell_psd.set(main_dict_cyl['psd'][0]/1e6) self._new_shell_shsd.set(main_dict_cyl['shsd'][0]/1e6) - self._new_calculation_domain.set(CylinderAndCurvedPlate.geomeries[main_dict_cyl['geometry'][0]]) + self._new_calculation_domain.set(api_helpers.domain_for_geometry_id(main_dict_cyl['geometry'][0])) self._new_shell_mat_factor.set(main_dict_cyl['material factor'][0]) self._new_shell_ring_stf_fab_method.set(main_dict_cyl['fab method ring stf'][0]) self._new_shell_ring_frame_fab_method.set(main_dict_cyl['fab method ring girder'][0]) @@ -6035,29 +6928,12 @@ def get_highest_pressure(self, line, limit_state = 'ULS'): Returning the highest pressure of a line. :return: ''' - all_press = list() - if limit_state == 'ULS': - pressures = self.calculate_all_load_combinations_for_line(line) - slm_red, psl, slm_red_pl, slm_red_stf = 1, 0, 1, 1 - for key, value in pressures.items(): - if key != 'slamming': - all_press.append(max(value)) - else: - if value is not None: - for load in self._line_to_struc[line][3]: - if load is not None: - if load.get_load_condition() == 'slamming': - slm_red_pl = load.get_slamming_reduction_plate() - slm_red_stf = load.get_slamming_reduction_stf() - psl = max(value) - - - return {'normal':max(all_press), 'slamming': psl, 'slamming plate reduction factor': slm_red_pl, - 'slamming stf reduction factor': slm_red_stf} - elif limit_state == 'FLS': - pass - else: - return {'normal':0, 'slamming': 0} + if limit_state == 'FLS': + return + return project_services.LinePressureService.highest_pressure( + self._build_line_pressure_input(line), + limit_state=limit_state, + ) def get_fatigue_pressures(self, line, accelerations = (0, 0, 0)): ''' Retruning a dictionary of internal and external pressures. ''' @@ -6324,7 +7200,6 @@ def reset(self): self._line_point_to_point_string = [] # This one ensures that a line is not created on top of a line self._accelerations_dict = {'static':9.81, 'dyn_loaded':0, 'dyn_ballast':0} self._multiselect_lines = [] - self._PULS_results = None self.update_frame() # Initsializing the calculation grid used for tank definition @@ -6535,8 +7410,8 @@ def cylinder_gui_mods(self): if self._active_line in self._line_to_struc.keys(): if self._line_to_struc[self._active_line][5] is not None: - self._new_calculation_domain.set(CylinderAndCurvedPlate - .geomeries[self._line_to_struc[self._active_line][5].geometry]) + self._new_calculation_domain.set( + api_helpers.domain_for_geometry_id(self._line_to_struc[self._active_line][5].geometry)) self._new_shell_exclude_ring_stf.set(self._line_to_struc[self._active_line][5]._ring_stiffener_excluded) self._new_shell_exclude_ring_frame.set(self._line_to_struc[self._active_line][5]._ring_frame_excluded) self.calculation_domain_selected() @@ -6653,12 +7528,15 @@ def draw_point_frame(self): def save_no_dialogue(self, event = None, backup = False): if backup: - self.savefile(filename=os.path.join(self._root_dir, '../backup.txt'), backup = backup) + target = project_application.ProjectFileDialogService.backup_save_target(self._root_dir) + self.savefile(filename=target.path, backup=backup) return - if self.__last_save_file is not None: - self.savefile(filename=self.__last_save_file) - else: + + target = project_application.ProjectFileDialogService.remembered_save_target(self.__last_save_file) + if target is None: tk.messagebox.showerror('Save error', 'No saves in this session yet.') + return + self.savefile(filename=target.path) def savefile(self, filename = None, backup = False): ''' @@ -6666,327 +7544,130 @@ def savefile(self, filename = None, backup = False): ''' if filename is None: - save_file = filedialog.asksaveasfile(mode="w", defaultextension=".txt") - if save_file is None: # ask saveasfile return `None` if dialog closed with "cancel". + filename = filedialog.asksaveasfilename(defaultextension=".txt") + if not filename: return - if not backup: - self.__last_save_file = save_file.name - else: - try: - save_file = open(filename, mode='w') - except FileNotFoundError: - save_file = open(filename.replace('',''), mode='w') - - structure_properties = {} - shell_structure_properties = {} - for key, value in self._line_to_struc.items(): - structure_properties[key] = value[0].get_main_properties() - shell_structure_properties[key] = None if value[5] is None else value[5].get_all_properties() - - fatigue_properties = {} - for key, value in self._line_to_struc.items(): - if value[2] != None: - try: - fatigue_properties[key] = value[2].get_fatigue_properties() - except AttributeError: - fatigue_properties[key] = None - else: - fatigue_properties[key] = None - - load_properties = {} - for load, data in self._load_dict.items(): - load_properties[load] = [data[0].get_load_parmeters(), data[1]] + save_target = project_application.ProjectFileDialogService.selected_save_target(filename, backup=backup) + if save_target.remember_as_last_save: + self.__last_save_file = str(save_target.path) - tank_properties = {} - tank_properties['grid'] = self._main_grid.export_grid() - tank_properties['search_data'] = self._main_grid.bfs_search_data - for tank,data in self._tank_dict.items(): - tank_properties[tank] = data.get_parameters() + try: + save_result = project_application.ProjectSaveService.save_path( + save_target.path, + self._build_project_save_input(), + ) + except project_application.ProjectPersistenceError as error: + tk.messagebox.showerror('Save error', str(error)) + return - load_combiantions = {} - counter = 0 - for name, data in self._new_load_comb_dict.items(): - load_combiantions[counter] = [name,data[0].get(),data[1].get(),data[2].get()] - counter+=1 - - export_all = {} - - export_all['project information'] = self._project_information.get('1.0', tk.END) - export_all['theme'] = self._current_theme - export_all['point_dict'] = self._point_dict - export_all['line_dict'] = self._line_dict - export_all['structure_properties'] = structure_properties - export_all['shell structure properties'] = shell_structure_properties - export_all['load_properties'] = load_properties - export_all['accelerations_dict'] = self._accelerations_dict - export_all['load_combinations'] = load_combiantions - export_all['tank_properties'] = tank_properties - export_all['fatigue_properties'] = fatigue_properties - #export_all['buckling type'] = self._new_buckling_slider.get() - - export_all['buckling method'] = self._new_buckling_method.get() - - if self._PULS_results is not None: - export_all['PULS results'] = self._PULS_results.get_run_results() - export_all['PULS results']['sheet location'] = self._PULS_results.puls_sheet_location - export_all['shifting'] = {'shifted checked': self._new_shifted_coords.get(), - 'shift hor': self._new_shift_viz_coord_hor.get(), - 'shift ver': self._new_shift_viz_coord_ver.get()} - - export_all['Weight and COG'] = self._weight_logger - - json.dump(export_all, save_file)#, sort_keys=True, indent=4) - save_file.close() if not backup: - self._parent.wm_title('| ANYstructure | ' + save_file.name) + self._parent.wm_title('| ANYstructure | ' + str(save_result.path)) #self.update_frame() - def openfile(self, defined = None, alone = False): - ''' - Opens a file with data (JSON). - ''' - - if defined == None: - imp_file = filedialog.askopenfile(mode='r', defaultextension=".txt") - if imp_file is None: # asksaveasfile return `None` if dialog closed with "cancel". - return - else: - imp_file = open(defined,'r') - - imported = json.load(imp_file) - - self.reset() - if 'project information' in imported.keys(): - self._project_information.delete("1.0", tk.END) - self._project_information.insert(1.0, imported['project information']) + def _build_project_save_input(self): + load_combinations = [ + project_application.LoadCombinationRecord(name, data[0].get(), data[1].get(), data[2].get()) + for name, data in self._new_load_comb_dict.items() + ] + return project_application.ProjectSaveInput( + project_information=self._project_information.get('1.0', tk.END), + theme=self._current_theme, + points=self._point_dict, + lines=self._line_dict, + line_bundles=self._line_to_struc, + load_assignments=self._load_dict, + accelerations=self._accelerations_dict, + load_combinations=load_combinations, + tanks=self._tank_dict, + tank_grid=self._main_grid.export_grid(), + tank_search_data=self._main_grid.bfs_search_data, + buckling_method=self._new_buckling_method.get(), + shifting={'shifted checked': self._new_shifted_coords.get(), + 'shift hor': self._new_shift_viz_coord_hor.get(), + 'shift ver': self._new_shift_viz_coord_ver.get()}, + weight_and_cog=self._weight_logger, + ) + + def _build_project_hydration_defaults(self): + return project_application.ProjectHydrationDefaults( + structure_types=self._structure_types, + zstar_optimization=self._new_zstar_optimization.get(), + puls_buckling_method=self._new_puls_method.get(), + puls_boundary=self._new_puls_panel_boundary.get(), + puls_stiffener_end=self._new_buckling_stf_end_support.get(), + puls_sp_or_up=self._new_puls_sp_or_up.get(), + puls_up_boundary=self._new_puls_up_boundary.get(), + material_factor=self._new_material_factor.get(), + ) + + def _apply_open_project_text_and_theme(self, open_transfer): + self._project_information.delete("1.0", tk.END) + if open_transfer.project_information: + self._project_information.insert(1.0, open_transfer.project_information) else: - self._project_information.delete("1.0", tk.END) self._project_information.insert(1.0, 'No information on project provided. Input here.') - if 'shifting' in imported.keys(): - self._new_shifted_coords.set(imported['shifting']['shifted checked']) - self._new_shift_viz_coord_hor.set(imported['shifting']['shift hor']) - self._new_shift_viz_coord_ver.set(imported['shifting']['shift ver']) - else: - pass - - if 'theme' in imported.keys(): - self.set_colors(imported['theme']) + if open_transfer.shifting: + self._new_shifted_coords.set(open_transfer.shifting.get('shifted checked', False)) + self._new_shift_viz_coord_hor.set(open_transfer.shifting.get('shift hor', 0)) + self._new_shift_viz_coord_ver.set(open_transfer.shifting.get('shift ver', 0)) - self._point_dict = imported['point_dict'] - self._line_dict = imported['line_dict'] - struc_prop = imported['structure_properties'] - old_save_file = False + self.set_colors(open_transfer.theme) - for line, lines_prop in struc_prop.items(): + def _apply_open_project_geometry_and_objects(self, open_transfer, hydration): + self._point_dict = open_transfer.points + self._line_dict = open_transfer.lines + self._line_to_struc = hydration.line_bundles + self._load_dict = hydration.load_assignments - if len(lines_prop) > 10: - # Loading a file (pre 3.4) - old_save_file = True - - self._line_to_struc[line] = [None, None, None, [], {}, None] + for line in self._line_to_struc: self._line_point_to_point_string.append( self.make_point_point_line_string(self._line_dict[line][0], self._line_dict[line][1])[0]) self._line_point_to_point_string.append( self.make_point_point_line_string(self._line_dict[line][0], self._line_dict[line][1])[1]) + for section_properties in hydration.section_properties: + self._sections = add_new_section(self._sections, struc.Section(section_properties)) - if 'structure_types' not in lines_prop.keys(): - lines_prop['structure_types'] = [self._structure_types, ' '] - if 'zstar_optimization' not in lines_prop.keys(): - lines_prop['zstar_optimization'] = [self._new_zstar_optimization.get(), ''] - if 'puls buckling method' not in lines_prop.keys(): - lines_prop['puls buckling method'] = [self._new_puls_method.get(), ''] - if 'puls boundary' not in lines_prop.keys(): - lines_prop['puls boundary'] = [self._new_puls_panel_boundary.get(), ''] - if 'puls stiffener end' not in lines_prop.keys(): - lines_prop['puls stiffener end'] = [self._new_buckling_stf_end_support.get(), ''] - if 'puls sp or up' not in lines_prop.keys(): - lines_prop['puls sp or up'] = [self._new_puls_sp_or_up.get(), ''] - if 'puls up boundary' not in lines_prop.keys(): - lines_prop['puls up boundary'] = [self._new_puls_up_boundary.get(), ''] - if 'mat_factor' not in lines_prop.keys(): - lines_prop['mat_factor'] = [self._new_material_factor.get(), ''] - - # Sigma x1/x2 is missing before 3.4 - if 'sigma_x' in lines_prop.keys(): - lines_prop['sigma_x1'] = lines_prop['sigma_x'] - lines_prop['sigma_x2'] = lines_prop['sigma_x'] - lines_prop.pop('sigma_x') - - if old_save_file: #need to get some basic information - # Import issues - try: - import example_data as ex - except ModuleNotFoundError: - # This is due to pyinstaller issues. - import anystruct.example_data as ex - #import ANYstructure.anystruct.example_data as ex - - main_dict = ex.prescriptive_main_dict - map_end = {'C': 'Continuous', 'S': 'Sniped'} - lines_prop['puls stiffener end'] = [map_end[lines_prop['puls stiffener end'][0]], - lines_prop['puls stiffener end'][1]] - main_dict['material yield'] = [355e6, 'Pa'] - main_dict['load factor on stresses'] = [1, ''] - main_dict['load factor on pressure'] = [1, ''] - main_dict['buckling method'] = [lines_prop['puls buckling method'], ''] - main_dict['stiffener end support'] = lines_prop['puls stiffener end'] # 'Continuous' - main_dict['girder end support'] = ['Continuous', ''] # 'Continuous' - dom = 'Flat plate, stiffened' if lines_prop['puls sp or up'][0] == 'SP' else 'Flat plate, unstiffened' - - main_dict['calculation domain'] = [dom, ''] - map_side = {'p': 'plate side', 's': 'stiffener side'} - if 'press_side' in lines_prop.keys(): - lines_prop['press_side'] = [map_side[lines_prop['press_side'][0]], ''] - else: - lines_prop['press_side'] = 'both sides' - lines_prop['panel or shell'] = 'panel' - #lines_prop['tension field'] = 'allowed' - self._line_to_struc[line][0] = AllStructure(Plate=CalcScantlings(lines_prop), - Stiffener=None if dom == 'Flat plate, unstiffened' - else CalcScantlings(lines_prop), - Girder=None, main_dict=main_dict) - - if imported['fatigue_properties'][line] is not None: - self._line_to_struc[line][2] = CalcFatigue(lines_prop, - imported['fatigue_properties'][line]) - else: - self._line_to_struc[line][2] = None - # Recording sections. - self._sections = add_new_section(self._sections, struc.Section(lines_prop)) - - else: - self._line_to_struc[line][0] = AllStructure(Plate=None if lines_prop['Plate'] is None - else CalcScantlings(lines_prop['Plate']), - Stiffener=None if lines_prop['Stiffener'] is None - else CalcScantlings(lines_prop['Stiffener']), - Girder=None if lines_prop['Girder'] is None - else CalcScantlings(lines_prop['Girder']), - main_dict=lines_prop['main dict']) - - if imported['fatigue_properties'][line] is not None: - self._line_to_struc[line][2] = CalcFatigue(lines_prop['Stiffener'], - imported['fatigue_properties'][line]) - else: - self._line_to_struc[line][2] = None - # Recording sections. - if self._line_to_struc[line][0].Stiffener is not None: - self._sections = add_new_section(self._sections, struc.Section(lines_prop['Stiffener'])) - - if 'shell structure properties' in imported.keys(): - if imported['shell structure properties'][line] is not None: - # need to correct the calcuation domain. - #self._new_calculation_domain.set(imported_dict['Main class'][CylinderAndCurvedPlate.geomeries]) - imported_dict = imported['shell structure properties'][line] - ''' - all_data = {'Main class': self.get_main_properties(), - 'Shell': self._Shell.get_main_properties(), - 'Long. stf.': self._LongStf.get_structure_prop(), - 'Ring stf.': self.RingStfObj.get_structure_prop(), - 'Ring frame': self._RingFrame.get_structure_prop()} - ''' - for stuc_type in ['Long. stf.', 'Ring stf.', 'Ring frame']: - if imported_dict[stuc_type] is not None: - if 'sigma_x' in imported_dict[stuc_type].keys(): - imported_dict[stuc_type]['sigma_x1'] = imported_dict[stuc_type]['sigma_x'] - imported_dict[stuc_type]['sigma_x2'] = imported_dict[stuc_type]['sigma_x'] - imported_dict[stuc_type].pop('sigma_x') - self._line_to_struc[line][5] = \ - CylinderAndCurvedPlate(imported_dict['Main class'], shell=None if imported_dict['Shell'] is None - else Shell(imported_dict['Shell']), long_stf=None if imported_dict['Long. stf.'] is None - else Structure(imported_dict['Long. stf.']), ring_stf=None if imported_dict['Ring stf.'] is None - else Structure(imported_dict['Ring stf.']), ring_frame=None if imported_dict['Ring frame'] - is None - else Structure(imported_dict['Ring frame'])) - - - # opening the loads - variables = ['poly_third','poly_second', 'poly_first', 'poly_const', 'load_condition', - 'structure_type', 'man_press', 'static_draft', 'name_of_load', 'limit_state', - 'slamming mult pl', 'slamming mult stf'] - - if len(imported['load_properties']) != 0: - for load, data in imported['load_properties'].items(): - temp_dict = {} - count_i = 0 - values = data[0] - if len(values) != len(variables): - # Adding slamming multiplication factors - values.append(1) - values.append(1) - for value in values: - temp_dict[variables[count_i]]= value - count_i += 1 - self._load_dict[load] = [Loads(temp_dict), data[1]] - - if len(data[1]) != 0: - for main_line in self._line_dict.keys(): - if main_line in data[1]: - self._line_to_struc[main_line][3].append(self._load_dict[load][0]) - - try: - self._accelerations_dict = imported['accelerations_dict'] - except IndexError: - self._accelerations_dict = {'static':9.81, 'dyn_loaded':0, 'dyn_ballast':0} - + def _apply_open_project_accelerations(self, open_transfer): + self._accelerations_dict = open_transfer.accelerations self._new_static_acc.set(self._accelerations_dict['static']) self._new_dyn_acc_loaded.set(self._accelerations_dict['dyn_loaded']) self._new_dyn_acc_ballast.set(self._accelerations_dict['dyn_ballast']) - try: - for data in imported['load_combinations'].values(): - name = tuple(data[0]) - self._new_load_comb_dict[name] = [tk.DoubleVar(),tk.DoubleVar(),tk.IntVar()] - self._new_load_comb_dict[name][0].set(data[1]), self._new_load_comb_dict[name][1].set(data[2]) - self._new_load_comb_dict[name][2].set(data[3]) - except IndexError: - for data in imported['load_combinations'].values(): - name = tuple(data[0]) - self._new_load_comb_dict[name] = [tk.DoubleVar(),tk.IntVar()] - self._new_load_comb_dict[name][0].set(data[1]), self._new_load_comb_dict[name][1].set(data[2]) + def _apply_open_project_load_combinations(self, open_transfer): + for load_combination in open_transfer.load_combinations: + name = load_combination.name + if load_combination.has_include: + self._new_load_comb_dict[name] = [tk.DoubleVar(), tk.DoubleVar(), tk.IntVar()] + self._new_load_comb_dict[name][0].set(load_combination.static_factor) + self._new_load_comb_dict[name][1].set(load_combination.dynamic_factor) + self._new_load_comb_dict[name][2].set(load_combination.include) + else: + self._new_load_comb_dict[name] = [tk.DoubleVar(), tk.IntVar()] + self._new_load_comb_dict[name][0].set(load_combination.static_factor) + self._new_load_comb_dict[name][1].set(load_combination.dynamic_factor) + def _apply_open_project_tanks(self, open_transfer): try: - self._main_grid.import_grid(imported['tank_properties']['grid']) + self._main_grid.import_grid(open_transfer.tank_grid) self._grid_calc = grid_window.CreateGridWindow(self._main_grid, self._canvas_dim, self._pending_grid_draw, self._canvas_base_origo) - tank_inp = dict() - if 'search_data' in imported['tank_properties'].keys(): - try: - for key, value in imported['tank_properties']['search_data'].items(): - tank_inp[int(key)] = value - self._main_grid.bfs_search_data = tank_inp - self._grid_calc.bfs_search_data = tank_inp - except AttributeError: - self._main_grid.bfs_search_data = None - self._grid_calc.bfs_search_data = None - else: - self._main_grid.bfs_search_data = None - self._grid_calc.bfs_search_data = None + self._main_grid.bfs_search_data = open_transfer.tank_search_data + self._grid_calc.bfs_search_data = open_transfer.tank_search_data - for comp_no in range(2, int(self._main_grid.get_highest_number_in_grid())+1): - self._compartments_listbox.insert('end',comp_no) - self._tank_dict['comp' + str(comp_no)] = Tanks(imported['tank_properties']['comp' + str(comp_no)]) + for comp_no in range(2, int(self._main_grid.get_highest_number_in_grid()) + 1): + self._compartments_listbox.insert('end', comp_no) + tank_name = 'comp' + str(comp_no) + self._tank_dict[tank_name] = Tanks(open_transfer.tank_properties[tank_name]) except IndexError: for line_name, point_no in self._line_dict.items(): point_coord_x = self._canvas_base_origo[0] + self._point_dict[point_no][0] * self._canvas_scale point_coord_y = self._canvas_base_origo[1] - self._point_dict[point_no][1] * self._canvas_scale - self.grid_operations(line_name, [point_coord_x,point_coord_y]) - - if 'PULS results' in list(imported.keys()): - self._PULS_results = PULSpanel() - if 'sheet location' in imported['PULS results'].keys(): - self._PULS_results.puls_sheet_location = imported['PULS results']['sheet location'] - imported['PULS results'].pop('sheet location') - self._PULS_results.set_run_results(imported['PULS results']) - - if 'buckling method' in list(imported.keys()): - #options = ['DNV-RP-C201 - prescriptive', 'DNV PULS', 'ML-CL (PULS based)'] - self._new_buckling_method.set(imported['buckling method']) - - # Setting the scale of the canvas + self.grid_operations(line_name, [point_coord_x, point_coord_y]) + def _apply_open_project_canvas_scale(self): points = self._point_dict if len(points) != 0: highest_y = max([coord[1] for coord in points.values()]) @@ -6997,236 +7678,143 @@ def openfile(self, defined = None, alone = False): if not any([highest_x == 0, highest_y == 0]): self._canvas_scale = min(800 / highest_y, 800 / highest_x, 15) - # if 'buckling type' in imported.keys(): - # self._new_buckling_slider.set(imported['buckling type']) - # self._buckling_slider.set(imported['buckling type']) - - if 'Weight and COG' in imported.keys(): - self._weight_logger = imported['Weight and COG'] - + def _finalize_open_project(self, open_transfer, filename): + self._new_buckling_method.set(open_transfer.buckling_method) + self._weight_logger = open_transfer.weight_and_cog self.get_cob() - imp_file.close() - self._parent.wm_title('| ANYstructure | ' + imp_file.name) + self._parent.wm_title('| ANYstructure | ' + str(filename)) self.update_frame() + def openfile(self, defined = None, alone = False): + ''' + Opens a file with data (JSON). + ''' + + if defined == None: + target = project_application.ProjectFileDialogService.selected_open_target( + filedialog.askopenfilename(defaultextension=".txt") + ) + else: + target = project_application.ProjectFileDialogService.selected_open_target(defined) + if target is None: + return + + try: + opened_project = project_application.ProjectOpenService.open_path( + target.path, + self._build_project_hydration_defaults(), + ) + except project_application.ProjectPersistenceError as error: + tk.messagebox.showerror('Open error', str(error)) + return + + open_transfer = opened_project.transfer + hydration = opened_project.hydration + + self.reset() + self._apply_open_project_text_and_theme(open_transfer) + self._apply_open_project_geometry_and_objects(open_transfer, hydration) + self._apply_open_project_accelerations(open_transfer) + self._apply_open_project_load_combinations(open_transfer) + self._apply_open_project_tanks(open_transfer) + self._apply_open_project_canvas_scale() + self._finalize_open_project(open_transfer, target.path) + def restore_previous(self): - if os.path.isfile(os.path.join(self._root_dir, '../backup.txt')): - self.openfile(defined=os.path.join(self._root_dir, '../backup.txt')) + target = project_application.ProjectFileDialogService.restore_target(self._root_dir) + if target is not None: + self.openfile(defined=target.path) def open_example(self, file_name = 'ship_section_example.txt'): ''' Open the example file. To be used in help menu. ''' - if os.path.isfile(file_name) : - self.openfile(defined = file_name) - else: - self.openfile(defined= self._root_dir + '/' + file_name) + target = project_application.ProjectFileDialogService.example_open_target(file_name, self._root_dir) + self.openfile(defined=target.path) def open_example_excel_file(self): file_name = 'excel_input_example.xlsx' + target = project_application.ProjectFileDialogService.example_open_target(file_name, self._root_dir) + project_services.ExcelProjectImportService.open_example_path(target.path) - if os.path.isfile(file_name) : - XLB = excel_interface.ExcelInterface(file_name, visible=True, read_only=True) - else: - XLB = excel_interface.ExcelInterface(self._root_dir + '/' + file_name, visible=True, read_only=True) + def _sync_excel_import_geometry(self, geometry_import): + for point in geometry_import.created_points: + self._active_point = point.name + self.logger(point=point.name, move_coords=None) + + for imported_line in geometry_import.imported_lines: + self._line_point_to_point_string.extend(imported_line.line.endpoint_keys) + self.add_to_combinations_dict(imported_line.line.name) + self.logger(line=[imported_line.line.name, None]) def open_excel_file(self): ''' Open an excel file with data to read into ANYstructure ''' - imp_file = filedialog.askopenfile(mode='r', defaultextension=".xlsx") - if imp_file is None: # asksaveasfile return `None` if dialog closed with "cancel". + target = project_application.ProjectFileDialogService.selected_open_target( + filedialog.askopenfilename(defaultextension=".xlsx") + ) + if target is None: return - data_wb = excel_interface.ExcelInterface(imp_file.name, visible=False, read_only=True) - # if os.path.isfile(imp_file) : #kj - # data_wb = excel_interface.ExcelInterface(imp_file, visible=False, read_only=True) - # else: - # return - data_flat = data_wb.get_sheet_data('flat_plate') - data_cyl = data_wb.get_sheet_data('cylinder') - data_wb.close_book() + import_data = project_services.ExcelProjectImportService.read_path(target.path) + flat_plate_records = import_data.flat_plate_records + cylinder_records = import_data.cylinder_records + + flat_geometry = project_services.ExcelProjectGeometryImportService.add_records( + self._point_dict, + self._line_dict, + flat_plate_records, + ) + cylinder_geometry = project_services.ExcelProjectGeometryImportService.add_records( + self._point_dict, + self._line_dict, + cylinder_records, + ) + self._sync_excel_import_geometry(flat_geometry) + self._sync_excel_import_geometry(cylinder_geometry) + self.update_frame() # Flat #-------------------------------------------------------------------------------------------------------------- - for row_data in data_flat[1:]: - l1x, l1y, l2x, l2y = row_data[1:5] - self._new_point_x.set(l1x) - self._new_point_y.set(l1y) - self.new_point() - self._new_point_x.set(l2x) - self._new_point_y.set(l2y) - self.new_point() - for row_data in data_cyl[1:]: - l1x, l1y, l2x, l2y = row_data[1:5] - self._new_point_x.set(l1x) - self._new_point_y.set(l1y) - self.new_point() - self._new_point_x.set(l2x) - self._new_point_y.set(l2y) - self.new_point() - # Creating lines - all_points = self.get_points() - for row_data in data_flat[1:]: - l1x, l1y, l2x, l2y = row_data[1:5] - p1_found = False - p2_found = False - for key, value in all_points.items(): - if l1x/1000 == value[0] and l1y/1000 == value[1]: - p1 = get_num(key) - p1_found = True - if l2x/1000 == value[0] and l2y/1000 == value[1]: - p2 = get_num(key) - p2_found = True - if p1_found and p2_found: - self._new_line_p1.set(p1) - self._new_line_p2.set(p2) - this_line = self.new_line() - self._active_line = this_line - self._line_is_active = True - self._new_calculation_domain.set(row_data[0]) - self._new_field_len.set(hlp.dist((l1x, l1y), (l2x, l2y))) - self._new_plate_thk.set(row_data[5]) - self._new_stf_spacing.set(row_data[6]) - self._new_stf_web_h.set(row_data[7]) - self._new_stf_web_t.set(row_data[8]) - self._new_stf_fl_w.set(row_data[9]) - self._new_stf_fl_t.set(row_data[10]) - sig_start = 12 - self._new_sigma_x1.set(row_data[sig_start]) - self._new_sigma_x2.set(row_data[sig_start+1]) - self._new_sigma_y1.set(row_data[sig_start+2]) - self._new_sigma_y2.set(row_data[sig_start+3]) - self._new_tauxy.set(row_data[sig_start+4]) - gird_start = 18 - if row_data[0] == 'Flat plate, stiffened with girder': - self._new_girder_length_LG.set(row_data[gird_start+0]) - self._new_girder_web_h.set(row_data[gird_start+1]) - self._new_girder_web_t.set(row_data[gird_start + 2]) - self._new_girder_fl_w.set(row_data[gird_start + 3]) - self._new_girder_fl_t.set(row_data[gird_start + 4]) - self._new_girder_type.set(row_data[gird_start + 5]) - - end_data_start = 24 - for idx, var in enumerate([self._new_overpresure,self._new_stucture_type, self._new_buckling_stf_end_support, - self._new_buckling_girder_end_support, self._new_buckling_fab_method_stf, - self._new_buckling_fab_method_girder, self._new_buckling_length_factor_stf, - self._new_buckling_length_factor_girder, self._new_buckling_stf_dist_bet_lat_supp, - self._new_buckling_girder_dist_bet_lat_supp, self._new_buckling_tension_field, - self._new_buckling_effective_against_sigy]): - if row_data[end_data_start + idx] is not None: - var.set(row_data[end_data_start + idx]) - - - - # self._new_overpresure.set(row_data[end_data_start + 0]) - # self._new_stucture_type.set(row_data[end_data_start + 1]) - # self._new_buckling_stf_end_support.set(row_data[end_data_start + 2]) - # self._new_buckling_girder_end_support.set(row_data[end_data_start + 3]) - # self._new_buckling_fab_method_stf.set(row_data[end_data_start + 4]) - # self._new_buckling_fab_method_girder.set(row_data[end_data_start + 5]) - # self._new_buckling_length_factor_stf.set(row_data[end_data_start + 6]) - # self._new_buckling_length_factor_girder.set(row_data[end_data_start + 7]) - # self._new_buckling_stf_dist_bet_lat_supp.set(row_data[end_data_start + 8]) - # self._new_buckling_girder_dist_bet_lat_supp.set(row_data[end_data_start + 9]) - # self._new_buckling_tension_field.set(row_data[end_data_start + 10]) - # self._new_buckling_effective_against_sigy.set(row_data[end_data_start + 11]) - - self.new_structure() - self._new_load_comb_dict[('manual', this_line, 'manual')][0].set(row_data[16]) - self._new_load_comb_dict[('manual', this_line, 'manual')][1].set(1) - self._new_load_comb_dict[('manual', this_line, 'manual')][2].set(1) - self._line_to_struc[this_line][0].need_recalc = True + for imported_line in flat_geometry.imported_lines: + import_record = imported_line.record + this_line = imported_line.line.name + l1x, l1y = import_record.first_point + l2x, l2y = import_record.second_point + self._active_line = this_line + self._line_is_active = True + self._new_calculation_domain.set(import_record.calculation_domain) + flat_request, overpressure = self._build_flat_structure_property_request_from_excel_record( + import_record, + hlp.dist((l1x, l1y), (l2x, l2y)), + ) + if overpressure is not None: + self._new_overpresure.set(overpressure) + self.new_structure( + toggle_multi=project_services.FlatStructurePropertyService.build(flat_request) + ) + self._new_load_comb_dict[('manual', this_line, 'manual')][0].set(import_record.manual_pressure) + self._new_load_comb_dict[('manual', this_line, 'manual')][1].set(1) + self._new_load_comb_dict[('manual', this_line, 'manual')][2].set(1) + self._line_to_struc[this_line][0].need_recalc = True # Cylinders # ------------------------------------------------------------------------------------------------------------ - for row_data in data_cyl[1:]: - - l1x, l1y, l2x, l2y = row_data[1:5] - p1_found = False - p2_found = False - for key, value in all_points.items(): - if l1x/1000 == value[0] and l1y/1000 == value[1]: - p1 = get_num(key) - p1_found = True - if l2x/1000 == value[0] and l2y/1000 == value[1]: - p2 = get_num(key) - p2_found = True - - if p1_found and p2_found: - self._new_line_p1.set(p1) - self._new_line_p2.set(p2) - this_line = self.new_line() - self._active_line = this_line - self._line_is_active = True - self._new_calculation_domain.set(row_data[0]) - main_start = 5 - self._new_shell_thk.set(row_data[main_start + 0]) - self._new_shell_radius.set(row_data[main_start + 1]) - self._new_shell_dist_rings.set(row_data[main_start + 2]) - self._new_shell_length.set(row_data[main_start + 3]) - self._new_shell_tot_length.set(row_data[main_start + 4]) - self._new_shell_k_factor.set(row_data[main_start + 5]) - self._new_shell_mat_factor.set(row_data[main_start + 6]) - - long_start = 12 - self._new_stf_web_h.set(row_data[long_start + 0]) - self._new_stf_web_t.set(row_data[long_start + 1]) - self._new_stf_fl_w.set(row_data[long_start + 2]) - self._new_stf_fl_t.set(row_data[long_start + 3]) - self._new_shell_panel_spacing.set(row_data[long_start + 4]) - self._new_stf_type.set(row_data[long_start + 5]) - - ring_stf_start = 18 - if row_data[ring_stf_start] is None: - self._new_shell_exclude_ring_stf.set(True) - else: - self._new_shell_exclude_ring_stf.set(False) - self._new_shell_ring_stf_hw.set(row_data[ring_stf_start + 0]) - self._new_shell_ring_stf_tw.set(row_data[ring_stf_start + 1]) - self._new_shell_ring_stf_b.set(row_data[ring_stf_start + 2]) - self._new_shell_ring_stf_tf.set(row_data[ring_stf_start + 3]) - self._new_shell_ring_stf_type.set(row_data[ring_stf_start + 4]) - - ring_frame = 23 - if row_data[ring_frame] is None: - self._new_shell_exclude_ring_frame.set(True) - else: - self._new_shell_exclude_ring_frame.set(False) - self._new_shell_ring_frame_hw.set(row_data[ring_frame + 0]) - self._new_shell_ring_frame_tw.set(row_data[ring_frame + 1]) - self._new_shell_ring_frame_b.set(row_data[ring_frame + 2]) - self._new_shell_ring_frame_tf.set(row_data[ring_frame + 3]) - self._new_shell_ring_frame_length_between_girders.set(row_data[ring_frame + 4]) - self._new_shell_ring_frame_type.set(row_data[ring_frame + 5]) - - stress_start = 29 - if row_data[stress_start] is None: - pass - else: - #mapper ={1: 'Force', 2: 'Stress'} - self._new_shell_stress_or_force.set(2) - self._new_shell_sasd.set(row_data[stress_start + 0]) - self._new_shell_smsd.set(row_data[stress_start + 1]) - self._new_shell_tTsd.set(row_data[stress_start + 2]) - self._new_shell_tQsd.set(row_data[stress_start + 3]) - self._new_shell_psd.set(row_data[stress_start + 4]) - self._new_shell_shsd.set(row_data[stress_start + 5]) - - force_start = 35 - if row_data[force_start] is None: - pass - else: - self._new_shell_stress_or_force.set(1) - self._new_shell_Nsd.set(row_data[force_start + 0]) - self._new_shell_Msd.set(row_data[force_start + 1]) - self._new_shell_Tsd.set(row_data[force_start + 2]) - self._new_shell_Qsd.set(row_data[force_start + 3]) - self._new_shell_psd.set(row_data[stress_start + 4]) - end_data_start = 39 - self._new_shell_uls_or_als.set(row_data[end_data_start + 0]) - self._new_shell_yield.set(row_data[end_data_start + 1]) - self._new_shell_end_cap_pressure_included.set(row_data[end_data_start + 1]) - self._new_shell_fab_ring_stf.set(row_data[end_data_start + 2]) - self._new_shell_fab_ring_frame.set(row_data[end_data_start + 3]) - self.new_structure() + for imported_line in cylinder_geometry.imported_lines: + import_record = imported_line.record + this_line = imported_line.line.name + self._active_line = this_line + self._line_is_active = True + self._new_calculation_domain.set(import_record.calculation_domain) + cylinder_request = project_services.CylinderExcelImportPropertyService.build_request( + import_record, + self._build_cylinder_excel_import_defaults(), + ) + cylinder_result = project_services.CylinderStructurePropertyService.build(cylinder_request) + cylinder_obj = self._create_cylinder_structure_from_property_result(cylinder_result) + flat_request = self._build_flat_structure_property_request_from_cylinder_excel_record(import_record) + self.new_structure( + toggle_multi=project_services.FlatStructurePropertyService.build(flat_request), + cylinder_return=cylinder_obj, + ) def button_load_info_click(self, event = None): ''' Get the load information for one line.''' @@ -7332,43 +7920,6 @@ def on_open_load_factor_window(self): lf_tkinter = tk.Toplevel(self._parent, background=self._general_color) load_factors.CreateLoadFactorWindow(lf_tkinter, self) - def on_puls_results_for_line(self): - if not self._line_is_active: - return - if self._PULS_results is None: - return - elif self._PULS_results.get_puls_line_results(self._active_line) is None: - return - # if self._puls_information_button.config('relief')[-1] == 'sunken': - # self.text_widget.forget() - # self._puls_information_button.config(relief='raised') - this_result = self._PULS_results.get_puls_line_results(self._active_line) - this_string = '' - for key, value in this_result.items(): - if type(value) == list: - this_string += key + ' : ' + str(value[0]) + ' ' + str(value[1]) + '\n' - elif type(value) == str: - this_string += key + ' : ' + value + '\n' - elif type(value) == dict: - this_string += key + '\n' - for subk, subv in value.items(): - this_string += ' ' + subk + ' : ' + str(subv[0]) + ' ' + str(subv[1] if subv[1] != None else '') + '\n' - - text_m = tk.Toplevel(self._parent, background=self._general_color) - # Create the text widget - text_widget = tk.Text(text_m , height=60, width=100) - # Create a scrollbar - scroll_bar = ttk.Scrollbar(text_m) - # Pack the scroll bar - # Place it to the right side, using tk.RIGHT - scroll_bar.pack(side=tk.RIGHT) - # Pack it into our tkinter application - # Place the text widget to the left side - text_widget.pack(side=tk.LEFT) - long_text = this_string - # Insert text into the text widget - text_widget.insert(tk.END, long_text) - def on_show_loads(self): ''' User can open a new window to specify loads @@ -7521,32 +8072,12 @@ def on_close_load_window(self, returned_loads, counter, load_comb_dict): self._new_load_comb_dict = load_comb_dict temp_load = self.__previous_load_data if len(returned_loads) != 0: - need_to_recalc_puls = {} for load, data in returned_loads.items(): #creating the loads objects dictionary self._load_dict[load] = data - # adding values to the line dictionary. resetting first. - for key, value in self._line_to_struc.items(): - self._line_to_struc[key][3] = [] - self._line_to_struc[key][0].need_recalc = True # All lines need recalculations. - - for main_line in self._line_dict.keys(): - for load_obj, load_line in self._load_dict.values(): - if main_line in self._line_to_struc.keys(): - if load_obj.get_name() in temp_load.keys(): - if any([load_obj.__str__() != temp_load[load_obj.get_name()][0].__str__() and main_line in \ - load_line+temp_load[load_obj.get_name()][1], - main_line in list(set(temp_load[load_obj.get_name()][1]).symmetric_difference(set(load_line)))]) : - # The load has changed for this line. - if self._PULS_results is not None: - self._PULS_results.result_changed(main_line) - elif main_line in load_line: - # This is a new load for this line. - if self._PULS_results is not None: - self._PULS_results.result_changed(main_line) - if main_line in load_line and main_line in self._line_to_struc.keys(): - self._line_to_struc[main_line][3].append(load_obj) + project_services.LineLoadService(self._line_to_struc).rebuild_line_loads( + self._line_dict.keys(), self._load_dict, temp_load) # Storing the the returned data to temporary variable. self.__returned_load_data = [returned_loads, counter, load_comb_dict] @@ -7831,11 +8362,11 @@ def open_about(self): Open a about messagebox. :return: ''' - messagebox.showinfo(title='Information', message='ANYstructure 5.x.x (Stable/Production)' + messagebox.showinfo(title='Information', message='ANYstructure 6.x.x (Stable/Production)' '\n' '\n' 'By Audun Arnesen Nyhus \n' - '2023/2024\n\n' + '2026\n\n' 'All technical calculation based on \n' 'DNV RPs and standards') @@ -7844,19 +8375,20 @@ def export_to_js(self): Printing to a js file :return: ''' - save_file = filedialog.asksaveasfile(mode="w", defaultextension=".js") - if save_file is None: # ask saveasfile return `None` if dialog closed with "cancel". + target = project_application.ProjectFileDialogService.selected_output_target( + filedialog.asksaveasfilename(defaultextension=".js") + ) + if target is None: return - # Setting up interface class. - JS = sesam.JSfile(self._point_dict, self._line_dict, self._sections, self._line_to_struc) - - JS.write_points() - JS.write_lines() - JS.write_sections() - JS.write_beams() - - save_file.writelines(JS.output_lines) - save_file.close() + project_services.SesamExportService.write_js_path( + project_services.SesamExportRequest( + points=self._point_dict, + lines=self._line_dict, + sections=self._sections, + line_bundles=self._line_to_struc, + ), + target.path, + ) if __name__ == '__main__': @@ -7932,4 +8464,4 @@ def export_to_js(self): my_app = Application(root) root.mainloop() - #Application.openfile(r'C:\Github\ANYstructure\ANYstructure\ship_section_example.txt', alone=True) \ No newline at end of file + #Application.openfile(r'C:\Github\ANYstructure\ANYstructure\ship_section_example.txt', alone=True) diff --git a/anystruct/ml_files/CLPIPE_CL_CSR-Tank_req_cl_predictor_UP.pickle b/anystruct/ml_files/CLPIPE_CL_CSR-Tank_req_cl_predictor_UP.pickle new file mode 100644 index 00000000..f33d1872 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_CSR-Tank_req_cl_predictor_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_CSR-Tank_req_cl_scaler_UP.pickle b/anystruct/ml_files/CLPIPE_CL_CSR-Tank_req_cl_scaler_UP.pickle new file mode 100644 index 00000000..e06c3541 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_CSR-Tank_req_cl_scaler_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor_SP.pickle b/anystruct/ml_files/CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor_SP.pickle new file mode 100644 index 00000000..58d7c140 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler_SP.pickle b/anystruct/ml_files/CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler_SP.pickle new file mode 100644 index 00000000..bb42b952 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..6a7b60e4 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..3495c5b2 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..7e743c85 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..f4e505ee Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..5f9a8597 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..b48f8a0e Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..f3d47448 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..ebef42e8 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..444964c5 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..3d8b1907 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..64bc8dd8 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..578b2ce6 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..5f9a8597 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..b48f8a0e Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..f3d47448 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..ebef42e8 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..d286b666 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..273dd08a Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..8c1ed61f Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..7e282ea9 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..5f9a8597 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..b48f8a0e Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..f3d47448 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..ebef42e8 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..f467c50e Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..6172dbd1 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..f4a2db7d Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..176e7d2e Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle new file mode 100644 index 00000000..5f9a8597 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle new file mode 100644 index 00000000..b48f8a0e Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle new file mode 100644 index 00000000..f3d47448 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle differ diff --git a/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle new file mode 100644 index 00000000..ebef42e8 Binary files /dev/null and b/anystruct/ml_files/CLPIPE_CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle differ diff --git a/anystruct/ml_files/CL_CSR-Tank_req_cl_predictor.pickle b/anystruct/ml_files/CL_CSR-Tank_req_cl_predictor.pickle deleted file mode 100644 index a2de2479..00000000 Binary files a/anystruct/ml_files/CL_CSR-Tank_req_cl_predictor.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_CSR-Tank_req_cl_scaler.pickle b/anystruct/ml_files/CL_CSR-Tank_req_cl_scaler.pickle deleted file mode 100644 index c84aa64a..00000000 Binary files a/anystruct/ml_files/CL_CSR-Tank_req_cl_scaler.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle b/anystruct/ml_files/CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle deleted file mode 100644 index 0e4d1148..00000000 Binary files a/anystruct/ml_files/CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle b/anystruct/ml_files/CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle deleted file mode 100644 index 23b286b7..00000000 Binary files a/anystruct/ml_files/CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index cf66382c..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index 956507ea..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index f23818a8..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index bc21e32b..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_predictor_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index 53d37fe9..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index d62314aa..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index 40f0b3d0..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index 3dfb1cbc..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_110_scaler_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index f9c237d3..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index 230af97b..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index 9167b313..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index 639f468b..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_predictor_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index 53d37fe9..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index d62314aa..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index 40f0b3d0..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index 3dfb1cbc..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_buc_115_scaler_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index 0f0f7d35..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index 16ada80e..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index b3cddf18..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index e6697373..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_predictor_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index 53d37fe9..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index d62314aa..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index 40f0b3d0..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index 3dfb1cbc..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_110_scaler_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index 7c4d4d0b..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index 87c69f3a..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index 1dcbf6ad..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index 88310e6b..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_predictor_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle deleted file mode 100644 index 53d37fe9..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle deleted file mode 100644 index d62314aa..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_1_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle deleted file mode 100644 index 40f0b3d0..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_SP.pickle and /dev/null differ diff --git a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle b/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle deleted file mode 100644 index 3dfb1cbc..00000000 Binary files a/anystruct/ml_files/CL_output_cl_str_ult_115_scaler_In-plane_support_cl_2,_3_UP.pickle and /dev/null differ diff --git a/anystruct/ml_files/NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..693ec895 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..1e62cbaf Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..135c435d Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..147605bc Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..a31f9771 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..5074999c Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..3bbf489b Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..ad9a6527 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..35b391c8 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..a1e9a995 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..eaa91f16 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..772d1b67 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..043e66a7 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..577e78a9 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..5b4d11d9 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..1458089b Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..30328368 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..50d2df8d Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1.pickle b/anystruct/ml_files/NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1.pickle new file mode 100644 index 00000000..84547ce7 Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1.pickle differ diff --git a/anystruct/ml_files/NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle b/anystruct/ml_files/NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle new file mode 100644 index 00000000..062ad36a Binary files /dev/null and b/anystruct/ml_files/NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3.pickle differ diff --git a/anystruct/ml_models.py b/anystruct/ml_models.py new file mode 100644 index 00000000..d5c1a387 --- /dev/null +++ b/anystruct/ml_models.py @@ -0,0 +1,236 @@ +"""Machine-learning model file resolution and loading helpers.""" + +import pickle +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + + +MATERIAL_FACTORS = (1.1, 1.15) +ML_CLASS_MESSAGES = { + 0: "N/A", + 1: "A negative utilisation factor is found.", + 2: "At least one of the in-plane loads must be non-zero.", + 3: "Division by zero", + 4: "Overflow", + 5: "The aspect ratio exceeds the PULS code limit", + 6: "The global slenderness exceeds 4. Please reduce stiffener span or increase stiffener height.", + 7: "The applied pressure is too high for this plate field.", + 8: "web-flange-ratio", + 9: "UF below or equal 0.87", + 10: "UF between 0.87 and 1.0", + 11: "UF above 1.0", +} + + +def default_ml_class_messages(): + """Return the ML classifier result messages used by GUI/report surfaces.""" + return dict(ML_CLASS_MESSAGES) + + +@dataclass(frozen=True) +class ModelFileSpec: + """Named model with current and compatible legacy file locations.""" + + name: str + file_bases: tuple[str, ...] + + +BUCKLING_MODEL_SPECS = ( + # ------------------------------------------------------------------------- + # Classification pipeline - existing CLPIPE models + # ------------------------------------------------------------------------- + ModelFileSpec("cl SP buc int predictor", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_SP",)), + ModelFileSpec("cl SP buc int scaler", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_SP",)), + ModelFileSpec("cl SP ult int predictor", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_SP",)), + ModelFileSpec("cl SP ult int scaler", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_SP",)), + + ModelFileSpec("cl SP buc GLGT predictor", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_SP",)), + ModelFileSpec("cl SP buc GLGT scaler", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_SP",)), + ModelFileSpec("cl SP ult GLGT predictor", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_SP",)), + ModelFileSpec("cl SP ult GLGT scaler", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_SP",)), + + ModelFileSpec("cl UP buc int predictor", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_UP",)), + ModelFileSpec("cl UP buc int scaler", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_UP",)), + ModelFileSpec("cl UP ult int predictor", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_UP",)), + ModelFileSpec("cl UP ult int scaler", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_UP",)), + + ModelFileSpec("cl UP buc GLGT predictor", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_UP",)), + ModelFileSpec("cl UP buc GLGT scaler", ("ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_UP",)), + ModelFileSpec("cl UP ult GLGT predictor", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_UP",)), + ModelFileSpec("cl UP ult GLGT scaler", ("ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_UP",)), + + ModelFileSpec( + "CSR predictor UP", + ( + "ml_files\\CLPIPE_CL_CSR-Tank_req_cl_predictor_UP", + "ml_files\\CLPIPE_CL_CSR-Tank_req_cl_predictor", + ), + ), + ModelFileSpec( + "CSR scaler UP", + ( + "ml_files\\CLPIPE_CL_CSR-Tank_req_cl_scaler_UP", + "ml_files\\CLPIPE_CL_CSR-Tank_req_cl_scaler", + ), + ), + ModelFileSpec( + "CSR predictor SP", + ( + "ml_files\\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor_SP", + "ml_files\\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", + ), + ), + ModelFileSpec( + "CSR scaler SP", + ( + "ml_files\\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler_SP", + "ml_files\\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler", + ), + ), + + # ------------------------------------------------------------------------- + # Numeric UF pipeline - validity classifier + two-output UF regressor + # ------------------------------------------------------------------------- + # SP, integrated support + ModelFileSpec( + "num SP int validity predictor", + ("ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num SP int validity xscaler", + ("ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num SP int UF reg predictor", + ("ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num SP int UF reg xscaler", + ("ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num SP int UF reg yscaler", + ("ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1",), + ), + + # SP, GL/GT support + ModelFileSpec( + "num SP GLGT validity predictor", + ("ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num SP GLGT validity xscaler", + ("ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num SP GLGT UF reg predictor", + ("ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num SP GLGT UF reg xscaler", + ("ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num SP GLGT UF reg yscaler", + ("ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3",), + ), + + # UP, integrated support + ModelFileSpec( + "num UP int validity predictor", + ("ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num UP int validity xscaler", + ("ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num UP int UF reg predictor", + ("ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num UP int UF reg xscaler", + ("ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1",), + ), + ModelFileSpec( + "num UP int UF reg yscaler", + ("ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1",), + ), + + # UP, GL/GT support + ModelFileSpec( + "num UP GLGT validity predictor", + ("ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num UP GLGT validity xscaler", + ("ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num UP GLGT UF reg predictor", + ("ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num UP GLGT UF reg xscaler", + ("ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3",), + ), + ModelFileSpec( + "num UP GLGT UF reg yscaler", + ("ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3",), + ), +) + + +def material_factor_token(material_factor: float) -> str: + """Return the token embedded in material-factor-specific model filenames.""" + return (str(round(material_factor, 2)).replace(".", "") + "0")[:3] + + +def resolve_model_pickle( + spec: ModelFileSpec, + search_roots: Iterable[str | Path], + material_factor: float, +) -> Path: + """Find the first available current or legacy pickle for a model spec.""" + attempted = [] + roots = tuple(Path(root) for root in search_roots) + for file_base in spec.file_bases: + relative_path = _pickle_path(file_base, material_factor) + for candidate in _candidate_paths(relative_path, roots): + attempted.append(candidate) + if candidate.is_file(): + return candidate + + searched = ", ".join(str(path) for path in attempted) + raise FileNotFoundError(f"Could not find ML model {spec.name!r}. Checked: {searched}") + + +def load_buckling_models( + search_roots: Iterable[str | Path], + material_factors: Iterable[float] = MATERIAL_FACTORS, +) -> dict[float, dict[str, Any]]: + """Load the buckling and CSR classifier models used by the main workspace.""" + roots = tuple(search_roots) + models = {} + for material_factor in material_factors: + models[material_factor] = {} + for spec in BUCKLING_MODEL_SPECS: + model_path = resolve_model_pickle(spec, roots, material_factor) + with model_path.open("rb") as model_file: + models[material_factor][spec.name] = pickle.load(model_file) + return models + + +def _pickle_path(file_base: str, material_factor: float) -> Path: + path = file_base.replace("XXX", material_factor_token(material_factor)).replace("\\", "/") + return Path(path + ".pickle") + + +def _candidate_paths(relative_path: Path, roots: tuple[Path, ...]) -> tuple[Path, ...]: + candidates = [relative_path] + for root in roots: + candidates.append(root / relative_path) + if relative_path.parts and relative_path.parts[0] == "ml_files": + candidates.append(root / Path(*relative_path.parts[1:])) + return tuple(dict.fromkeys(candidates)) diff --git a/anystruct/optimize.py b/anystruct/optimize.py index 9935e167..ddf38b5d 100644 --- a/anystruct/optimize.py +++ b/anystruct/optimize.py @@ -1,4 +1,4 @@ -#Optimize structure +# Optimize structure import numpy as np import itertools as it @@ -15,20 +15,64 @@ try: import anystruct.calc_structure as calc import anystruct.helper as hlp + import anystruct.calculate_semianalytical as semi_analytical except ModuleNotFoundError: import ANYstructure.anystruct.calc_structure as calc import ANYstructure.anystruct.helper as hlp + import ANYstructure.anystruct.calculate_semianalytical as semi_analytical + + +def _set_material_factor_on_structure(obj, material_factor): + """ + Apply selected material factor to Plate/Stiffener/Girder objects. + + This is important for the ML-Numeric optimizer because the numeric UF + is converted as: + UF_checked = UF_predicted * material_factor + and get_filtered_results reads the material factor from init_stuc_obj.Plate. + + obj may be a single AllStructure-like object or a list/tuple of such objects + for geometric optimization. + """ + if material_factor is None: + return obj + + try: + mat_fac = float(material_factor) + except Exception: + return obj + + def _apply(one_obj): + for attr_name in ('Plate', 'Stiffener', 'Girder'): + try: + part = getattr(one_obj, attr_name) + except Exception: + part = None + if part is not None: + try: + part.mat_factor = mat_fac + except Exception: + pass + return one_obj + + if isinstance(obj, (list, tuple)): + for one_obj in obj: + _apply(one_obj) + else: + _apply(obj) + + return obj def run_optmizataion(initial_structure_obj=None, min_var=None, max_var=None, lateral_pressure=None, deltas=None, algorithm='anysmart', trials=30000, side='p', - const_chk = (True,True,True,True,True,True, True, False, False, False), - pso_options = (100,0.5,0.5,0.5,100,1e-8,1e-8), is_geometric=False, fatigue_obj = None , - fat_press_ext_int = None, - min_max_span = (2,6), tot_len = None, frame_height = 2.5, frame_distance = None, - slamming_press = 0, predefined_stiffener_iter = None, processes = None, use_weight_filter = True, - load_pre = False, opt_girder_prop = None, puls_sheet = None, puls_acceptance = 0.87, - fdwn = 1, fup = 0.5, ml_algo = None, cylinder = False): + const_chk=(True, True, True, True, True, True, True, False, False, False), + pso_options=(100, 0.5, 0.5, 0.5, 100, 1e-8, 1e-8), is_geometric=False, fatigue_obj=None, + fat_press_ext_int=None, + min_max_span=(2, 6), tot_len=None, frame_height=2.5, frame_distance=None, + slamming_press=0, predefined_stiffener_iter=None, processes=None, use_weight_filter=True, + load_pre=False, opt_girder_prop=None, puls_sheet=None, puls_acceptance=0.87, + fdwn=1, fup=0.5, ml_algo=None, cylinder=False, material_factor=None): ''' The optimazation is initiated here. It is called from optimize_window. :param initial_structure_obj: @@ -41,6 +85,16 @@ def run_optmizataion(initial_structure_obj=None, min_var=None, max_var=None, lat :param pso_options: :return: ''' + if puls_sheet is not None: + raise NotImplementedError( + "External Excel-sheet PULS optimization was removed. Use the built-in SemiAnalytical replacement, " + "prescriptive, or ML-Numeric buckling checks." + ) + + # Make material factor explicit for all optimizer variants. + # Single optimization receives one structure object; geometric optimization receives a list. + initial_structure_obj = _set_material_factor_on_structure(initial_structure_obj, material_factor) + init_filter_weight = float('inf') if is_geometric: @@ -58,44 +112,44 @@ def run_optmizataion(initial_structure_obj=None, min_var=None, max_var=None, lat init_filter_weight = get_initial_weight(obj=initial_structure_obj, lat_press=lateral_pressure, min_var=min_var, max_var=max_var, deltas=deltas, - trials= 30000 if predefined_stiffener_iter is None else + trials=30000 if predefined_stiffener_iter is None else len(predefined_stiffener_iter), fat_dict=fat_dict, fat_press=None if fat_press_ext_int is None else fat_press_ext_int, - predefined_stiffener_iter = predefined_stiffener_iter, - slamming_press=slamming_press, fdwn = fdwn, fup = fup, - ml_algo = ml_algo) + predefined_stiffener_iter=predefined_stiffener_iter, + slamming_press=slamming_press, fdwn=fdwn, fup=fup, + ml_algo=ml_algo) if cylinder: to_return = any_smart_loop_cylinder(min_var=min_var, max_var=max_var, deltas=deltas, initial_structure_obj=initial_structure_obj, - use_weight_filter = use_weight_filter, + use_weight_filter=use_weight_filter, predefiened_stiffener_iter=predefined_stiffener_iter) return to_return elif algorithm == 'anysmart' and not is_geometric: to_return = any_smart_loop(min_var, max_var, deltas, initial_structure_obj, lateral_pressure, init_filter_weight, side=side, const_chk=const_chk, fat_dict=fat_dict, - fat_press=fat_press_ext_int,slamming_press=slamming_press, - predefiened_stiffener_iter=predefined_stiffener_iter, puls_sheet = puls_sheet, - puls_acceptance = puls_acceptance, fdwn = fdwn, fup = fup, ml_algo=ml_algo) + fat_press=fat_press_ext_int, slamming_press=slamming_press, + predefiened_stiffener_iter=predefined_stiffener_iter, puls_sheet=puls_sheet, + puls_acceptance=puls_acceptance, fdwn=fdwn, fup=fup, ml_algo=ml_algo) return to_return elif algorithm == 'anysmart' and is_geometric: - return geometric_summary_search(min_var= min_var, max_var=max_var, deltas= deltas, - initial_structure_obj= initial_structure_obj, lateral_pressure=lateral_pressure, - init_filter= init_filter_weight, side= side, const_chk= const_chk, - fat_obj= fatigue_obj, fat_press= fat_press_ext_int, min_max_span= min_max_span, - tot_len= tot_len, frame_distance = frame_distance, - algorithm= 'anysmart', predefiened_stiffener_iter=predefined_stiffener_iter, - slamming_press = slamming_press, load_pre = load_pre, - opt_girder_prop = opt_girder_prop, ml_algo=ml_algo) + return geometric_summary_search(min_var=min_var, max_var=max_var, deltas=deltas, + initial_structure_obj=initial_structure_obj, lateral_pressure=lateral_pressure, + init_filter=init_filter_weight, side=side, const_chk=const_chk, + fat_obj=fatigue_obj, fat_press=fat_press_ext_int, min_max_span=min_max_span, + tot_len=tot_len, frame_distance=frame_distance, + algorithm='anysmart', predefiened_stiffener_iter=predefined_stiffener_iter, + slamming_press=slamming_press, load_pre=load_pre, + opt_girder_prop=opt_girder_prop, ml_algo=ml_algo) elif algorithm == 'anydetail' and not is_geometric: - return any_optimize_loop(min_var, max_var, deltas, initial_structure_obj, lateral_pressure,init_filter_weight, + return any_optimize_loop(min_var, max_var, deltas, initial_structure_obj, lateral_pressure, init_filter_weight, side=side, const_chk=const_chk, fat_dict=fat_dict, fat_press=fat_press_ext_int, slamming_press=slamming_press) elif algorithm == 'random' and not is_geometric: - return get_random_result(initial_structure_obj,lateral_pressure,min_var,max_var,deltas,trials=trials, - side=side, const_chk=const_chk, fat_dict=fat_dict,fat_press=fat_press_ext_int) + return get_random_result(initial_structure_obj, lateral_pressure, min_var, max_var, deltas, trials=trials, + side=side, const_chk=const_chk, fat_dict=fat_dict, fat_press=fat_press_ext_int) elif algorithm == 'random_no_delta' and not is_geometric: return get_random_result_no_bounds(initial_structure_obj, lateral_pressure, min_var, max_var, trials=trials, side=side, const_chk=const_chk) @@ -107,9 +161,10 @@ def run_optmizataion(initial_structure_obj=None, min_var=None, max_var=None, lat else: return None -def any_optimize_loop(min_var,max_var,deltas,initial_structure_obj,lateral_pressure, init_filter = float('inf'), - side='p',const_chk=(True,True,True,True,True,False), fat_dict = None, fat_press = None, - slamming_press = 0): + +def any_optimize_loop(min_var, max_var, deltas, initial_structure_obj, lateral_pressure, init_filter=float('inf'), + side='p', const_chk=(True, True, True, True, True, False), fat_dict=None, fat_press=None, + slamming_press=0): ''' Calulating initial values. :param min: @@ -117,7 +172,7 @@ def any_optimize_loop(min_var,max_var,deltas,initial_structure_obj,lateral_press :return: ''' ass_var = [] - plot_x,plot_y = [],[] + plot_x, plot_y = [], [] plt.xlabel('#') plt.ylabel('weigth [kg]') plt.title('ANYdetail brute force results') @@ -126,22 +181,22 @@ def any_optimize_loop(min_var,max_var,deltas,initial_structure_obj,lateral_press iter_count = 0 min_weight = init_filter main_fail = list() - for spacing in np.arange(min_var[0],max_var[0]+deltas[0],deltas[0]): - for plate_thk in np.arange(min_var[1],max_var[1]+deltas[1],deltas[1]): - for stf_web_h in np.arange(min_var[2],max_var[2]+deltas[2],deltas[2]): - for stf_web_thk in np.arange(min_var[3],max_var[3]+deltas[3],deltas[3]): - for stf_flange_width in np.arange(min_var[4],max_var[4]+deltas[4],deltas[4]): - for stf_flange_thk in np.arange(min_var[5],max_var[5]+deltas[5],deltas[5]): + for spacing in np.arange(min_var[0], max_var[0] + deltas[0], deltas[0]): + for plate_thk in np.arange(min_var[1], max_var[1] + deltas[1], deltas[1]): + for stf_web_h in np.arange(min_var[2], max_var[2] + deltas[2], deltas[2]): + for stf_web_thk in np.arange(min_var[3], max_var[3] + deltas[3], deltas[3]): + for stf_flange_width in np.arange(min_var[4], max_var[4] + deltas[4], deltas[4]): + for stf_flange_thk in np.arange(min_var[5], max_var[5] + deltas[5], deltas[5]): var_x = np.array([spacing, plate_thk, stf_web_h, stf_web_thk, stf_flange_width, - stf_flange_thk,min_var[6],min_var[7]]) - check = any_constraints_all(var_x,initial_structure_obj,lat_press=lateral_pressure, - init_weight=min_weight,side=side,chk=const_chk, - fat_dict = fat_dict, fat_press = fat_press, + stf_flange_thk, min_var[6], min_var[7]]) + check = any_constraints_all(var_x, initial_structure_obj, lat_press=lateral_pressure, + init_weight=min_weight, side=side, chk=const_chk, + fat_dict=fat_dict, fat_press=fat_press, slamming_press=slamming_press) if check[0] is not False: current_weight = calc_weight(var_x) if current_weight <= min_weight: - iter_count+=1 + iter_count += 1 min_weight = current_weight ass_var = var_x main_fail.append(check) @@ -150,16 +205,17 @@ def any_optimize_loop(min_var,max_var,deltas,initial_structure_obj,lateral_press if ass_var is None: return None, None, None, False, main_fail - new_struc_obj = create_new_structure_obj(initial_structure_obj,[item for item in ass_var]) - new_calc_obj = create_new_calc_obj(initial_structure_obj,[item for item in ass_var])[0] + new_struc_obj = create_new_structure_obj(initial_structure_obj, [item for item in ass_var]) + new_calc_obj = create_new_calc_obj(initial_structure_obj, [item for item in ass_var])[0] return new_struc_obj, new_calc_obj, fat_dict, True, main_fail -def any_smart_loop(min_var,max_var,deltas,initial_structure_obj,lateral_pressure, init_filter = float('inf'), - side='p',const_chk=(True,True,True,True,True,True,True, False, False,False), fat_dict = None, - fat_press = None, - slamming_press = 0, predefiened_stiffener_iter = None, processes = None, - puls_sheet = None, puls_acceptance = 0.87, fdwn = 1, fup = 0.5, ml_algo = None): + +def any_smart_loop(min_var, max_var, deltas, initial_structure_obj, lateral_pressure, init_filter=float('inf'), + side='p', const_chk=(True, True, True, True, True, True, True, False, False, False), fat_dict=None, + fat_press=None, + slamming_press=0, predefiened_stiffener_iter=None, processes=None, + puls_sheet=None, puls_acceptance=0.87, fdwn=1, fup=0.5, ml_algo=None): ''' Trying to be smart :param min_var: @@ -172,18 +228,19 @@ def any_smart_loop(min_var,max_var,deltas,initial_structure_obj,lateral_pressure if predefiened_stiffener_iter is None: structure_to_check = any_get_all_combs(min_var, max_var, deltas) else: - structure_to_check = any_get_all_combs(min_var, max_var, deltas,predef_stiffeners=[item.get_tuple() for item - in predefiened_stiffener_iter]) + structure_to_check = any_get_all_combs(min_var, max_var, deltas, predef_stiffeners=[item.get_tuple() for item + in + predefiened_stiffener_iter]) - main_result = get_filtered_results(structure_to_check, initial_structure_obj,lateral_pressure, - init_filter_weight=init_filter, side=side,chk=const_chk, fat_dict=fat_dict, + main_result = get_filtered_results(structure_to_check, initial_structure_obj, lateral_pressure, + init_filter_weight=init_filter, side=side, chk=const_chk, fat_dict=fat_dict, fat_press=fat_press, slamming_press=slamming_press, processes=processes, - puls_sheet = puls_sheet, puls_acceptance = puls_acceptance, ml_algo=ml_algo) + puls_sheet=puls_sheet, puls_acceptance=puls_acceptance, ml_algo=ml_algo) main_iter = main_result[0] main_fail = main_result[1] - ass_var=None + ass_var = None current_weight = float('inf') for item in main_iter: main_fail.append(item) @@ -207,9 +264,9 @@ def any_smart_loop(min_var,max_var,deltas,initial_structure_obj,lateral_pressure calc_object_stf = None if initial_structure_obj.Stiffener is None \ else create_new_calc_obj(initial_structure_obj.Stiffener, ass_var, - fat_dict, fdwn=fdwn, fup=fup) + fat_dict, fdwn=fdwn, fup=fup) calc_object_pl = create_new_calc_obj(initial_structure_obj.Plate, ass_var, fat_dict, - fdwn=fdwn, fup=fup) + fdwn=fdwn, fup=fup) calc_object = calc.AllStructure(Plate=calc_object_pl[0], Stiffener=None if initial_structure_obj.Stiffener is None else calc_object_stf[0], Girder=None, @@ -218,18 +275,19 @@ def any_smart_loop(min_var,max_var,deltas,initial_structure_obj,lateral_pressure return calc_object, fat_dict, True, main_fail -def any_smart_loop_cylinder(min_var,max_var,deltas,initial_structure_obj,lateral_pressure = None, - init_filter = float('inf'), - side='p',const_chk=(True,True,True,True,True,True,True, False, False,False), fat_dict = None, - fat_press = None, slamming_press = 0, predefiened_stiffener_iter = None, processes = None, - fdwn = 1, fup = 0.5, ml_algo = None, use_weight_filter = True): +def any_smart_loop_cylinder(min_var, max_var, deltas, initial_structure_obj, lateral_pressure=None, + init_filter=float('inf'), + side='p', const_chk=(True, True, True, True, True, True, True, False, False, False), + fat_dict=None, + fat_press=None, slamming_press=0, predefiened_stiffener_iter=None, processes=None, + fdwn=1, fup=0.5, ml_algo=None, use_weight_filter=True): combs = list() # TODO first optmize for long then ring components. Find the overall smallest weight. # Creating the individual combinations for Shell, LongStf, RingStf and RingFrame for idx, str_type in enumerate(range(len(min_var))): if sum(min_var[idx]) == 0: - structure_to_check = [(0, 0, 0, 0, 0, 0, 0, 0),] + structure_to_check = [(0, 0, 0, 0, 0, 0, 0, 0), ] else: if any([predefiened_stiffener_iter is None, idx == 0]): initial_structure_obj.LongStfObj.stiffener_type = 'T' @@ -237,8 +295,8 @@ def any_smart_loop_cylinder(min_var,max_var,deltas,initial_structure_obj,lateral else: structure_to_check = any_get_all_combs(min_var[idx], max_var[idx], deltas[idx], - predef_stiffeners= [item.get_tuple() for item in - predefiened_stiffener_iter]) + predef_stiffeners=[item.get_tuple() for item in + predefiened_stiffener_iter]) # TODO add stifffener type # [list(item).append('T') for item in structure_to_check] @@ -260,7 +318,7 @@ def any_smart_loop_cylinder(min_var,max_var,deltas,initial_structure_obj,lateral to_check = [random.choice(final_comb) + [float('inf')] for dummy in range(10000)] with Pool(processes=max(cpu_count() - 1, 1)) as my_process: res_pre = my_process.starmap(any_constraints_cylinder, to_check) - for chk_res in res_pre : + for chk_res in res_pre: if chk_res[0]: current_weight = calc_weight_cylinder(chk_res[2]) if current_weight < min_weight: @@ -273,7 +331,7 @@ def any_smart_loop_cylinder(min_var,max_var,deltas,initial_structure_obj,lateral final_comb_inc_weight.append(val + [min_weight]) t1 = time.time() - with Pool(processes = max(cpu_count()-1,1)) as my_process: + with Pool(processes=max(cpu_count() - 1, 1)) as my_process: res_pre = my_process.starmap(any_constraints_cylinder, final_comb_inc_weight) check_ok, check_not_ok = list(), list() @@ -302,31 +360,32 @@ def any_smart_loop_cylinder(min_var,max_var,deltas,initial_structure_obj,lateral # Checking ring stiffeners and frames - #return new_struc_obj, new_calc_obj, fat_dict, True, main_fail + # return new_struc_obj, new_calc_obj, fat_dict, True, main_fail return new_cylinder_obj, main_fail -def any_smart_loop_geometric(min_var,max_var,deltas,initial_structure_obj,lateral_pressure, init_filter = float('inf'), - side='p',const_chk=(True,True,True,True,True,True), fat_obj = None, fat_press = None, - slamming_press = None, predefiened_stiffener_iter=None, processes = None, ml_algo = None): +def any_smart_loop_geometric(min_var, max_var, deltas, initial_structure_obj, lateral_pressure, + init_filter=float('inf'), + side='p', const_chk=(True, True, True, True, True, True), fat_obj=None, fat_press=None, + slamming_press=None, predefiened_stiffener_iter=None, processes=None, ml_algo=None): ''' Searching multiple sections using the smart loop. ''' all_obj = [] idx = 0 for struc_obj, lat_press, fatigue_obj, fatigue_press, slam_press in zip(initial_structure_obj, lateral_pressure, fat_obj, fat_press, slamming_press): - #print(predefiened_stiffener_iter) + # print(predefiened_stiffener_iter) if predefiened_stiffener_iter is not None: - this_predefiened_objects = hlp.helper_read_section_file(predefiened_stiffener_iter, struc_obj) + this_predefiened_objects = hlp.helper_read_section_file(predefiened_stiffener_iter, struc_obj.Stiffener) else: this_predefiened_objects = None - opt_obj = any_smart_loop(min_var = min_var,max_var = max_var,deltas = deltas,initial_structure_obj = struc_obj, - lateral_pressure = lat_press, init_filter = init_filter, side=side, + opt_obj = any_smart_loop(min_var=min_var, max_var=max_var, deltas=deltas, initial_structure_obj=struc_obj, + lateral_pressure=lat_press, init_filter=init_filter, side=side, const_chk=const_chk, - fat_dict = None if fatigue_obj is None else fatigue_obj.get_fatigue_properties(), - fat_press = None if fatigue_press is None else fatigue_press, - slamming_press = 0 if slam_press is None else slam_press, + fat_dict=None if fatigue_obj is None else fatigue_obj.get_fatigue_properties(), + fat_press=None if fatigue_press is None else fatigue_press, + slamming_press=0 if slam_press is None else slam_press, predefiened_stiffener_iter=this_predefiened_objects, processes=processes, ml_algo=ml_algo) @@ -335,24 +394,24 @@ def any_smart_loop_geometric(min_var,max_var,deltas,initial_structure_obj,latera return all_obj -def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_structure_obj=None,lateral_pressure=None, - init_filter = float('inf'),side='p',const_chk=(True,True,True,True, True, True), - pso_options=(100,0.5,0.5,0.5,100,1e-8,1e-8), fat_obj = None, fat_press = None, - min_max_span = (2,6), tot_len = None, frame_distance = None, - algorithm = 'anysmart', predefiened_stiffener_iter=None, reiterate = True, - processes = None, slamming_press = None, load_pre = False, opt_girder_prop = None, - ml_algo = None): +def geometric_summary_search(min_var=None, max_var=None, deltas=None, initial_structure_obj=None, lateral_pressure=None, + init_filter=float('inf'), side='p', const_chk=(True, True, True, True, True, True), + pso_options=(100, 0.5, 0.5, 0.5, 100, 1e-8, 1e-8), fat_obj=None, fat_press=None, + min_max_span=(2, 6), tot_len=None, frame_distance=None, + algorithm='anysmart', predefiened_stiffener_iter=None, reiterate=True, + processes=None, slamming_press=None, load_pre=False, opt_girder_prop=None, + ml_algo=None): '''Geometric optimization of all relevant sections. ''' # Checking the number of initial objects and adding if number of fraction is to be changed. # print('Min/max span is', min_max_span) found_max, found_min = False, False - for frames in range(1,100): + for frames in range(1, 100): frame_count = frames - if tot_len/frames <= min_max_span[1] and found_min is False: + if tot_len / frames <= min_max_span[1] and found_min is False: min_frame_count = frame_count - 1 found_min = True - if tot_len/frames <= min_max_span[0] and found_max is False: + if tot_len / frames <= min_max_span[0] and found_max is False: max_frame_count = frame_count - 1 found_max = True if found_min and found_max: @@ -368,13 +427,13 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st working_fatigue_press = {} working_slamming = {} - for no_of_fractions in range(min_frame_count+1, max_frame_count+1): + for no_of_fractions in range(min_frame_count + 1, max_frame_count + 1): # Create fraction varables - frac_var,min_frac,max_frac = [], [], [] + frac_var, min_frac, max_frac = [], [], [] for var in range(no_of_fractions): # Frame height is a interpolation between height at start and end. - frac_var.append(1/no_of_fractions) + frac_var.append(1 / no_of_fractions) working_objects[no_of_fractions] = list(initial_structure_obj) working_lateral[no_of_fractions] = list(lateral_pressure) working_fatigue[no_of_fractions] = list(fat_obj) @@ -383,9 +442,9 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st similar_count = len(working_objects[no_of_fractions]) tick_tock = True - while similar_count != no_of_fractions*2: + while similar_count != no_of_fractions * 2: - if similar_count > no_of_fractions*2: + if similar_count > no_of_fractions * 2: for var_dict in [working_objects, working_lateral, working_fatigue, working_fatigue_press, working_slamming]: if tick_tock: @@ -403,25 +462,25 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st else: if tick_tock: lower_idx = 0 - upper_idx = int(len(working_objects[no_of_fractions])/2) + upper_idx = int(len(working_objects[no_of_fractions]) / 2) tick_tock = False else: - lower_idx = int(len(working_objects[no_of_fractions])/2) - 1 + lower_idx = int(len(working_objects[no_of_fractions]) / 2) - 1 upper_idx = -1 tick_tock = True - #print(no_of_fractions, int(ceil(len(working_objects[no_of_fractions])/2))) + # print(no_of_fractions, int(ceil(len(working_objects[no_of_fractions])/2))) - obj_start, obj_stop = copy.deepcopy(working_objects[no_of_fractions][lower_idx]),\ - copy.deepcopy(working_objects[no_of_fractions][upper_idx]) + obj_start, obj_stop = copy.deepcopy(working_objects[no_of_fractions][lower_idx]), \ + copy.deepcopy(working_objects[no_of_fractions][upper_idx]) fat_obj_start, fat_obj_stop = copy.deepcopy(working_fatigue[no_of_fractions][lower_idx]), \ - copy.deepcopy(working_fatigue[no_of_fractions][upper_idx]) + copy.deepcopy(working_fatigue[no_of_fractions][upper_idx]) lat_start, lat_stop = working_lateral[no_of_fractions][lower_idx], \ - working_lateral[no_of_fractions][upper_idx] + working_lateral[no_of_fractions][upper_idx] fat_press_start, fat_press_stop = working_fatigue_press[no_of_fractions][lower_idx], \ - working_fatigue_press[no_of_fractions][upper_idx] + working_fatigue_press[no_of_fractions][upper_idx] slam_start, slam_stop = working_slamming[no_of_fractions][lower_idx], \ - working_slamming[no_of_fractions][upper_idx] + working_slamming[no_of_fractions][upper_idx] # if no_of_fractions == 11: # print('Tick/tock', tick_tock, 'lower/opper idx', lower_idx, upper_idx) @@ -447,7 +506,7 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st # print('') for no_of_fractions, struc_objects in working_objects.items(): for struc_obj in struc_objects: - struc_obj.Plate.set_span(tot_len/no_of_fractions) + struc_obj.Plate.set_span(tot_len / no_of_fractions) struc_obj.Stiffener.set_span(tot_len / no_of_fractions) solution_found, iterations = False, 0 @@ -455,8 +514,8 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st while not solution_found: iterations += 1 if iterations != 1: - min_var[0:6] += deltas/2 - max_var[0:6] -= deltas/2 + min_var[0:6] += deltas / 2 + max_var[0:6] -= deltas / 2 if algorithm == 'anysmart': if load_pre: @@ -465,62 +524,61 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st opt_objects = pickle.load(file)[no_of_fractions][1] else: - opt_objects = any_smart_loop_geometric(min_var=min_var,max_var=max_var,deltas=deltas, + opt_objects = any_smart_loop_geometric(min_var=min_var, max_var=max_var, deltas=deltas, initial_structure_obj=working_objects[no_of_fractions], lateral_pressure=working_lateral[no_of_fractions], - init_filter = init_filter,side=side,const_chk=const_chk, - fat_obj = working_fatigue[no_of_fractions], - slamming_press = working_slamming[no_of_fractions], + init_filter=init_filter, side=side, const_chk=const_chk, + fat_obj=working_fatigue[no_of_fractions], + slamming_press=working_slamming[no_of_fractions], fat_press=working_fatigue_press[no_of_fractions], - predefiened_stiffener_iter = predefiened_stiffener_iter, + predefiened_stiffener_iter=predefiened_stiffener_iter, ml_algo=ml_algo) - # Finding weight of this solution. tot_weight, frame_spacings, valid, width, weight_details = 0, [None for dummy in range(len(opt_objects))], \ - True, 10, {'frames': list(), 'objects': list(), - 'scales': list()} + True, 10, {'frames': list(), 'objects': list(), + 'scales': list()} - #print('Weight for', no_of_fractions) + # print('Weight for', no_of_fractions) for count, opt in enumerate(opt_objects): obj = opt[0] if opt[3]: - weigth_to_add = calc_weight((obj.Plate.get_s(),obj.Plate.get_pl_thk(),obj.Stiffener.get_web_h(), + weigth_to_add = calc_weight((obj.Plate.get_s(), obj.Plate.get_pl_thk(), obj.Stiffener.get_web_h(), obj.Stiffener.get_web_thk(), - obj.Stiffener.get_fl_w(),obj.Stiffener.get_fl_thk(), - obj.Plate.span,width), prt=False) + obj.Stiffener.get_fl_w(), obj.Stiffener.get_fl_thk(), + obj.Plate.span, width), prt=False) tot_weight += weigth_to_add weight_details['objects'].append(weigth_to_add) if frame_spacings[count // 2] is None: frame_spacings[count // 2] = obj.Plate.get_s() - #print('added normal weight', weigth_to_add) + # print('added normal weight', weigth_to_add) else: # In this case there are no applicable solutions found in the specified dimension ranges. tot_weight += float('inf') valid = False if valid: - #print(frame_distance) - for frame in range(no_of_fractions-1): + # print(frame_distance) + for frame in range(no_of_fractions - 1): frame_height = 2.5 if frame_distance is None else frame_distance['start_dist'] + \ - (frame_distance['stop_dist']- + (frame_distance['stop_dist'] - frame_distance['start_dist']) * \ - ((frame+1)/no_of_fractions) + ((frame + 1) / no_of_fractions) - #pl_area, stf_area = 0.018 * width, 0.25 * 0.015 * (width//frame_spacings[frame]) + # pl_area, stf_area = 0.018 * width, 0.25 * 0.015 * (width//frame_spacings[frame]) this_x = (frame_spacings[frame], opt_girder_prop[0], opt_girder_prop[1], opt_girder_prop[2], opt_girder_prop[3], opt_girder_prop[4], None, width) - this_weight = sum(get_field_tot_area(this_x))* frame_height * 7850 + this_weight = sum(get_field_tot_area(this_x)) * frame_height * 7850 scale_max, scale_min = opt_girder_prop[5], opt_girder_prop[6] - this_scale = scale_min + (scale_max-scale_min) * (abs((max_frame_count-(count+1)/2))/ - (max_frame_count-min_frame_count)) - #print('Number of fractions', no_of_fractions, 'Scale', this_scale) + this_scale = scale_min + (scale_max - scale_min) * (abs((max_frame_count - (count + 1) / 2)) / + (max_frame_count - min_frame_count)) + # print('Number of fractions', no_of_fractions, 'Scale', this_scale) tot_weight += this_weight * this_scale solution_found = True - #print('added frame weight', this_weight * this_scale) + # print('added frame weight', this_weight * this_scale) weight_details['frames'].append(this_weight * this_scale) weight_details['scales'].append(this_scale) elif iterations == 2: @@ -535,6 +593,7 @@ def geometric_summary_search(min_var=None,max_var=None,deltas = None, initial_st # print(val) return results + def any_find_min_weight_var(var): ''' Find the minimum weight of the inpu @@ -545,10 +604,11 @@ def any_find_min_weight_var(var): return min(map(calc_weight)) -def any_constraints_cylinder(x,obj: calc.CylinderAndCurvedPlate,init_weight, lat_press = None,side='p', - chk=(True,True,True,True, True, True, True, False, False, False), - fat_dict = None, fat_press = None, slamming_press = 0,fdwn = 1, fup = 0.5, - ml_results = None): + +def any_constraints_cylinder(x, obj: calc.CylinderAndCurvedPlate, init_weight, lat_press=None, side='p', + chk=(True, True, True, True, True, True, True, False, False, False), + fat_dict=None, fat_press=None, slamming_press=0, fdwn=1, fup=0.5, + ml_results=None): ''' Checking all constraints defined. @@ -558,9 +618,9 @@ def any_constraints_cylinder(x,obj: calc.CylinderAndCurvedPlate,init_weight, lat :return: ''' - all_checks = [0,0,0,0,0,0,0,0] - check_map = {'weight': 0, 'UF unstiffened': 1, 'Column stability': 2, 'UF longitudinal stiffeners':3, - 'Stiffener check': 4, 'UF ring stiffeners':5, 'UF ring frame': 6, 'Check OK': 7} + all_checks = [0, 0, 0, 0, 0, 0, 0, 0] + check_map = {'weight': 0, 'UF unstiffened': 1, 'Column stability': 2, 'UF longitudinal stiffeners': 3, + 'Stiffener check': 4, 'UF ring stiffeners': 5, 'UF ring frame': 6, 'Check OK': 7} calc_obj = create_new_cylinder_obj(obj, x) @@ -569,13 +629,13 @@ def any_constraints_cylinder(x,obj: calc.CylinderAndCurvedPlate,init_weight, lat if init_weight != False: this_weight = calc_weight_cylinder(x) if this_weight > init_weight: - results = calc_obj.get_utilization_factors(optimizing=optimizing, empty_result_dict = True) + results = calc_obj.get_utilization_factors(optimizing=optimizing, empty_result_dict=True) results['Weight'] = this_weight all_checks[0] += 1 return False, 'Weight filter', x, all_checks, calc_obj if chk[0]: - results = calc_obj.get_utilization_factors(optimizing = optimizing) + results = calc_obj.get_utilization_factors(optimizing=optimizing) if results[0]: all_checks[check_map[results[1]]] += 1 return True, results[1], x, all_checks, calc_obj @@ -584,59 +644,357 @@ def any_constraints_cylinder(x,obj: calc.CylinderAndCurvedPlate,init_weight, lat return False, results[1], x, all_checks, calc_obj -def any_constraints_all(x,obj,lat_press,init_weight,side='p',chk=(True,True,True,True, True, True, True, False, - False, False), - fat_dict = None, fat_press = None, slamming_press = 0, PULSrun: calc.PULSpanel = None, - print_result = False, fdwn = 1, fup = 0.5, ml_results = None, random_result_return = False): +def _get_ml_input_for_optimization(calc_object, lat_press): + """ + Return the correct ML input row for an optimization candidate. + + UP models use the plate object input. + SP models use the stiffener object input, because SP numeric/regression models + were trained with stiffener geometry included. + """ + all_structure = calc_object[0] if isinstance(calc_object, (list, tuple)) else calc_object + + if all_structure.Plate.get_puls_sp_or_up() == 'UP': + return all_structure.Plate.get_buckling_ml_input(lat_press, alone=False) + + if all_structure.Stiffener is not None: + return all_structure.Stiffener.get_buckling_ml_input(lat_press, alone=False) + + # Defensive fallback. SP without stiffener should normally not occur. + return all_structure.Plate.get_buckling_ml_input(lat_press, alone=False) + + +def _get_numeric_pipeline_prefix(calc_object): + """Return numeric ML pipeline prefix for this optimization candidate.""" + all_structure = calc_object[0] if isinstance(calc_object, (list, tuple)) else calc_object + + sp_or_up = all_structure.Plate.get_puls_sp_or_up() + + if sp_or_up == 'UP': + boundary = all_structure.Plate.get_puls_boundary() + return 'num UP int' if boundary == 'Int' else 'num UP GLGT' + + if all_structure.Stiffener is not None: + boundary = all_structure.Stiffener.get_puls_boundary() + else: + boundary = all_structure.Plate.get_puls_boundary() + + return 'num SP int' if boundary == 'Int' else 'num SP GLGT' + + +def _predict_numeric_uf_group(ml_algo, input_rows, prefix, mat_fac): + """ + Batch-predict numeric UF for optimization. + + Returns an array with columns: + [buckling_uf_material_factored, ultimate_uf_material_factored, valid_prediction] + + The numeric regressor predicts UF at material factor = 1.0. + For optimization checks, the values are converted using: + UF = predicted_UF * mat_fac + and then compared against 1.0. + """ + result = np.full((len(input_rows), 3), np.inf, dtype=float) + + if len(input_rows) == 0: + return result + + result[:, 2] = 0.0 + + required_keys = [ + f'{prefix} validity predictor', + f'{prefix} validity xscaler', + f'{prefix} UF reg predictor', + f'{prefix} UF reg xscaler', + f'{prefix} UF reg yscaler', + ] + + for key in required_keys: + if ml_algo is None or key not in ml_algo or ml_algo[key] is None: + return result + + try: + x_valid = ml_algo[f'{prefix} validity xscaler'].transform(input_rows) + valid_pred = ml_algo[f'{prefix} validity predictor'].predict(x_valid) + valid_pred = np.asarray(valid_pred, dtype=float).ravel() + result[:, 2] = valid_pred + + valid_idx = np.where(valid_pred == 1)[0] + if len(valid_idx) == 0: + return result + + valid_inputs = [input_rows[idx] for idx in valid_idx] + x_reg = ml_algo[f'{prefix} UF reg xscaler'].transform(valid_inputs) + y_scaled = ml_algo[f'{prefix} UF reg predictor'].predict(x_reg) + y_raw = ml_algo[f'{prefix} UF reg yscaler'].inverse_transform(y_scaled) + + # Material factor correction. The regressor output is for mat. factor = 1.0. + y_factored = y_raw * float(mat_fac) + + result[valid_idx, 0] = y_factored[:, 0] + result[valid_idx, 1] = y_factored[:, 1] + + return result + + except Exception: + # If numeric prediction fails, mark all as invalid so optimization rejects them. + return result + + +def _semi_analytical_float(value, default=0.0): + try: + if value is None: + return default + return float(value) + except Exception: + return default + + +def _semi_analytical_stiffener_type(value): + return { + 'T': 'T-bar', + 'T-bar': 'T-bar', + 'L': 'Angle', + 'Angle': 'Angle', + 'L-bulb': 'L-bulb', + 'FB': 'Flatbar', + 'F': 'Flatbar', + 'Flatbar': 'Flatbar', + }.get(value, value) + + +def _semi_analytical_stiffener_boundary(value): + return { + 'C': 'Cont', + 'Cont': 'Cont', + 'Continuous': 'Cont', + 'S': 'Sniped', + 'Sniped': 'Sniped', + }.get(value, value) + + +def _semi_analytical_in_plane_support(value): + return { + 'Int': 'Integrated', + 'Integrated': 'Integrated', + 'GL': 'Girder - long', + 'Girder - long': 'Girder - long', + 'GT': 'Girder - trans', + 'Girder - trans': 'Girder - trans', + }.get(value, value) + + +def _puls_selected_method(value): + text = str(value).strip().lower() + if text in ('1', 'buckling'): + return 'buckling' + if text in ('2', 'ultimate'): + return 'ultimate' + return text + + +def _get_semi_analytical_input_for_optimization(calc_object, lat_press): + """ + Build the reduced SemiAnalytical input from an optimization candidate. + + The replacement currently covers regular stiffened SP panels. It follows + the same candidate object and pressure convention as the ML-Numeric SP + path: geometry/stresses are in the candidate object, and lateral pressure + is converted from kPa to MPa for the PULS-style input surface. + """ + all_structure = calc_object[0] if isinstance(calc_object, (list, tuple)) else calc_object + + if all_structure.Plate.get_puls_sp_or_up() != 'SP': + return None + if all_structure.Stiffener is None: + return None + + stiffener = all_structure.Stiffener + puls_boundary = stiffener.get_puls_boundary() + sig_x1 = stiffener.sigma_x1 + sig_x2 = stiffener.sigma_x2 + if sig_x1 * sig_x2 >= 0: + sigxd = sig_x1 if abs(sig_x1) > abs(sig_x2) else sig_x2 + else: + sigxd = max(sig_x1, sig_x2) + + pressure_mpa = _semi_analytical_float(lat_press) / 1000.0 + return semi_analytical.S3PanelInput( + length=stiffener.span * 1000.0, + stiffener_spacing=stiffener.spacing, + plate_thickness=stiffener.t, + stiffener_type=_semi_analytical_stiffener_type(stiffener.get_stiffener_type()), + stiffener_boundary=_semi_analytical_stiffener_boundary(stiffener.get_puls_stf_end()), + stiffener_height=stiffener.hw, + web_thickness=stiffener.tw, + flange_width=stiffener.b, + flange_thickness=stiffener.tf, + yield_stress_plate=stiffener.mat_yield / 1e6, + yield_stress_stiffener=stiffener.mat_yield / 1e6, + axial_stress=0.0 if puls_boundary == 'GT' else sigxd, + transverse_stress_1=0.0 if puls_boundary == 'GL' else stiffener.sigma_y1, + transverse_stress_2=0.0 if puls_boundary == 'GL' else stiffener.sigma_y2, + shear_stress=stiffener.tau_xy, + pressure=pressure_mpa, + in_plane_support=_semi_analytical_in_plane_support(puls_boundary), + elastic_modulus=210000.0, + poisson_ratio=0.3, + ) + + +def _predict_semi_analytical_uf(calc_object, lat_press, default_acceptance=0.87): + """ + Return [buckling_uf, ultimate_uf, valid_prediction, acceptance_limit]. + """ + result = np.array([float('inf'), float('inf'), 0.0, float(default_acceptance)], dtype=float) + + try: + if hasattr(semi_analytical, 'predict_anystructure_uf_with_acceptance'): + return semi_analytical.predict_anystructure_uf_with_acceptance( + calc_object, + lat_press, + default_acceptance=default_acceptance, + ) + if hasattr(semi_analytical, 'predict_anystructure_uf'): + result[0:3] = semi_analytical.predict_anystructure_uf(calc_object, lat_press) + return result + + panel = _get_semi_analytical_input_for_optimization(calc_object, lat_press) + if panel is None: + return result + + solved = semi_analytical.solve_s3_panel(panel) + if ( + solved.valid + and solved.buckling_usage_factor is not None + and solved.ultimate_usage_factor is not None + ): + result[0] = float(solved.buckling_usage_factor) + result[1] = float(solved.ultimate_usage_factor) + result[2] = 1.0 + except Exception: + pass + + return result + + +def any_constraints_all(x, obj, lat_press, init_weight, side='p', chk=(True, True, True, True, True, True, True, False, + False, False), + fat_dict=None, fat_press=None, slamming_press=0, PULSrun: calc.PULSpanel = None, + print_result=False, fdwn=1, fup=0.5, ml_results=None, random_result_return=False): ''' Checking all constraints defined. - iter_var = ((item,init_stuc_obj,lat_press,init_filter_weight,side,chk,fat_dict,fat_press,slamming_press, PULSrun) - for item in iterable_all) - :param x: - :return: + ml_results is used for both ML pipelines: + chk[7] SemiAnalytical replacement: + ml_results[0] = buckling UF + ml_results[1] = ultimate UF + ml_results[2] = valid prediction flag, 1 = valid, 0 = invalid/unsupported + ml_results[3] = SemiAnalytical acceptance limit + accepted if selected UF / acceptance < 1.0 + + chk[8] ML-CL: + deactivated; ML-Numeric has replaced this classifier path + + chk[9] ML-Numeric: + ml_results[0] = material-factored buckling UF + ml_results[1] = material-factored ultimate UF + ml_results[2] = valid numeric prediction flag, 1 = valid, 0 = invalid/NaN + accepted if selected UF <= 1.0 ''' if random_result_return: # Skip all calculations - if random.choice([True,False,False,False,False,False,False]): - return True, 'Check OK', x, [0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5] + if random.choice([True, False, False, False, False, False, False]): + return True, 'Check OK', x, [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] else: - return False, 'Random result', x, [1.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5] - all_checks = [0,0,0,0,0,0,0,0,0,0,0] + return False, 'Random result', x, [1.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] + + all_checks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] print_result = False - calc_object_stf = None if obj.Stiffener is None else create_new_calc_obj(obj.Stiffener, x, fat_dict, fdwn = fdwn, fup = fup) + + calc_object_stf = None if obj.Stiffener is None else create_new_calc_obj(obj.Stiffener, x, fat_dict, fdwn=fdwn, + fup=fup) calc_object_pl = create_new_calc_obj(obj.Plate, x, fat_dict, fdwn=fdwn, fup=fup) calc_object = [calc.AllStructure(Plate=calc_object_pl[0], - Stiffener=None if obj.Stiffener is None else calc_object_stf[0], Girder=None, + Stiffener=None if obj.Stiffener is None else calc_object_stf[0], + Girder=None, main_dict=obj.get_main_properties()['main dict']), calc_object_pl[1]] calc_object[0].lat_press = lat_press - # PULS buckling check - if chk[7] and PULSrun is not None: - x_id = x_to_string(x) - if calc_object[0].Plate.get_puls_method() == 'buckling': - puls_uf = PULSrun.get_puls_line_results(x_id)["Buckling strength"]["Actual usage Factor"][0] - elif calc_object[0].Plate.get_puls_method() == 'ultimate': - puls_uf = PULSrun.get_puls_line_results(x_id)["Ultimate capacity"]["Actual usage Factor"][0] + # SemiAnalytical buckling check + if chk[7]: + if ml_results is not None: + try: + valid_prediction = int(ml_results[2]) if len(ml_results) > 2 else 0 + puls_acceptance = float(ml_results[3]) if len(ml_results) > 3 else 0.87 + + if valid_prediction != 1: + return False, 'SemiAnalytical', x, all_checks + + puls_method = _puls_selected_method(calc_object[0].Plate.get_puls_method()) + if puls_method == 'buckling': + puls_uf = float(ml_results[0]) + elif puls_method == 'ultimate': + puls_uf = float(ml_results[1]) + else: + puls_uf = None + except Exception: + return False, 'SemiAnalytical', x, all_checks + elif PULSrun is not None: + x_id = x_to_string(x) + puls_acceptance = PULSrun.puls_acceptance + puls_method = _puls_selected_method(calc_object[0].Plate.get_puls_method()) + if puls_method == 'buckling': + puls_uf = PULSrun.get_puls_line_results(x_id)["Buckling strength"]["Actual usage Factor"][0] + elif puls_method == 'ultimate': + puls_uf = PULSrun.get_puls_line_results(x_id)["Ultimate capacity"]["Actual usage Factor"][0] + else: + puls_uf = None + else: + return False, 'SemiAnalytical', x, all_checks + if type(puls_uf) == str or puls_uf is None: - return False, 'PULS', x, all_checks - all_checks[8] = puls_uf/PULSrun.puls_acceptance - if puls_uf/PULSrun.puls_acceptance >= 1: + return False, 'SemiAnalytical', x, all_checks + + all_checks[8] = puls_uf / puls_acceptance + if puls_uf / puls_acceptance >= 1: if print_result: - print('PULS', calc_object[0].get_one_line_string(), False) - return False, 'PULS', x, all_checks + print('SemiAnalytical', calc_object[0].get_one_line_string(), False) + return False, 'SemiAnalytical', x, all_checks - # Buckling ml-cl if chk[8]: - if any([calc_object[0].Plate.get_puls_method() == 'buckling' and ml_results[0] != 9, - calc_object[0].Plate.get_puls_method() == 'ultimate' and ml_results[1] != 9]): - if print_result: - print('Buckling ML-CL', calc_object[0].Stiffener.get_one_line_string(), False) - return False, 'Buckling ML-CL', x, all_checks + return False, 'Buckling ML-CL deactivated', x, all_checks - # Buckling ml-reg + # Buckling ML-Numeric if chk[9]: - pass + numeric_ok = False + numeric_uf = float('inf') + + try: + valid_prediction = int(ml_results[2]) if ml_results is not None and len(ml_results) > 2 else 0 + + if valid_prediction == 1: + puls_method = _puls_selected_method(calc_object[0].Plate.get_puls_method()) + if puls_method == 'buckling': + numeric_uf = float(ml_results[0]) + elif puls_method == 'ultimate': + numeric_uf = float(ml_results[1]) + else: + numeric_uf = float('inf') + + numeric_ok = numeric_uf <= 1.0 + except Exception: + numeric_ok = False + numeric_uf = float('inf') + + all_checks[10] = numeric_uf + + if not numeric_ok: + if print_result: + stf_text = calc_object[0].Stiffener.get_one_line_string() if calc_object[ + 0].Stiffener is not None else 'No stiffener' + print('Buckling ML-Numeric', stf_text, False) + return False, 'Buckling ML-Numeric', x, all_checks this_weight = calc_weight(x) @@ -644,120 +1002,97 @@ def any_constraints_all(x,obj,lat_press,init_weight,side='p',chk=(True,True,True weigt_frac = this_weight / init_weight if print_result: pass - # print('Weights', calc_weight(x), ' > ', init_weight, - # calc_object[0].get_one_line_string(), init_weight, False) all_checks[0] = weigt_frac return False, 'Weight filter', x, all_checks # Section modulus if chk[0] and calc_object[0].Stiffener is not None: section_modulus = min(calc_object[0].Stiffener.get_section_modulus()) - min_section_modulus = calc_object[0].Stiffener.get_dnv_min_section_modulus(lat_press*1000) + min_section_modulus = calc_object[0].Stiffener.get_dnv_min_section_modulus(lat_press * 1000) section_frac = section_modulus / min_section_modulus - #print(section_modulus, min_section_modulus, section_frac, lat_press) all_checks[1] = section_frac - if not section_modulus > min_section_modulus : + if not section_modulus > min_section_modulus: if print_result: - print('Section modulus',calc_object[0].get_one_line_string(), False) + print('Section modulus', calc_object[0].get_one_line_string(), False) return False, 'Section modulus', x, all_checks - # Local stiffener buckling if chk[6] and calc_object[0].Stiffener is not None: buckling_local = calc_object[0].local_buckling(optimizing=True) check = all([buckling_local['Stiffener'][0] < calc_object[0].Stiffener.hw, buckling_local['Stiffener'][1] < calc_object[0].Stiffener.b]) all_checks[2] = max([0 if buckling_local['Stiffener'][0] == 0 else - calc_object[0].Stiffener.hw/buckling_local['Stiffener'][0], - 0 if buckling_local['Stiffener'][1] == 0 else - calc_object[0].Stiffener.b/buckling_local['Stiffener'][1]]) + calc_object[0].Stiffener.hw / buckling_local['Stiffener'][0], + 0 if buckling_local['Stiffener'][1] == 0 else + calc_object[0].Stiffener.b / buckling_local['Stiffener'][1]]) if not check: if print_result: - print('Local stiffener buckling',calc_object[0].get_one_line_string(), False) + print('Local stiffener buckling', calc_object[0].get_one_line_string(), False) return False, 'Local stiffener buckling', x, all_checks # Buckling if chk[3]: - ''' - {'Plate': {'Plate buckling': up_buckling}, 'Stiffener': {'Overpressure plate side': stf_buckling_pl_side, - 'Overpressure stiffener side': stf_buckling_stf_side, - 'Resistance between stiffeners': stf_plate_resistance, - 'Shear capacity': stf_shear_capacity}, - 'Girder': {'Overpressure plate side': girder_buckling_pl_side, - 'Overpressure girder side': girder_buckling_girder_side, - 'Shear capacity': girder_shear_capacity}, - 'Local buckling': local_buckling} - ''' buckling_results = calc_object[0].plate_buckling(optimizing=True) - res = [buckling_results['Plate']['Plate buckling'],] + res = [buckling_results['Plate']['Plate buckling'], ] for val in buckling_results['Stiffener'].values(): res.append(val) - # for val in buckling_results['Girder'].values(): - # res.append(val) buckling_results = res - # print(buckling_results) all_checks[3] = max(buckling_results) - if not all([uf<=1 for uf in buckling_results]): + if not all([uf <= 1 for uf in buckling_results]): if print_result: - print('Buckling',calc_object[0].get_one_line_string(), False) + print('Buckling', calc_object[0].get_one_line_string(), False) return False, 'Buckling', x, all_checks - # Minimum plate thickness if chk[1]: act_pl_thk = calc_object[0].Plate.get_pl_thk() - min_pl_thk = calc_object[0].Plate.get_dnv_min_thickness(lat_press*1000)/1000 + min_pl_thk = calc_object[0].Plate.get_dnv_min_thickness(lat_press * 1000) / 1000 plate_frac = min_pl_thk / act_pl_thk all_checks[4] = plate_frac if not act_pl_thk > min_pl_thk: if print_result: - print('Minimum plate thickeness',calc_object[0].get_one_line_string(), False) + print('Minimum plate thickeness', calc_object[0].get_one_line_string(), False) return False, 'Minimum plate thickness', x, all_checks # Shear area if chk[2]: pass - # calc_shear_area = calc_object[0].Stiffener.get_shear_area() - # min_shear_area = calc_object[0].Stiffener.get_minimum_shear_area(lat_press) - # shear_frac = min_shear_area / calc_shear_area - # all_checks[5] = shear_frac - # if not calc_shear_area > min_shear_area: - # if print_result: - # print('Shear area',calc_object[0].Stiffener.get_one_line_string(), False) - # return False, 'Shear area', x, all_checks # Fatigue if chk[4] and fat_dict is not None and fat_press is not None: - fatigue_uf = calc_object[1].get_total_damage(ext_press=fat_press[0], - int_press=fat_press[1])*calc_object[1].get_dff() + int_press=fat_press[1]) * calc_object[1].get_dff() all_checks[6] = fatigue_uf if fatigue_uf > 1: if print_result: - print('Fatigue',calc_object[0].Stiffener.get_one_line_string(), False) + print('Fatigue', calc_object[0].Stiffener.get_one_line_string(), False) return False, 'Fatigue', x, all_checks # Slamming - if chk[5] and slamming_press != 0 and calc_object[0].Stiffener is not None: slam_check = calc_object[0].Stiffener.check_all_slamming(slamming_press) all_checks[7] = slam_check[1] if slam_check[0] is False: if print_result: - print('Slamming',calc_object[0].Stiffener.get_one_line_string(), False) + print('Slamming', calc_object[0].Stiffener.get_one_line_string(), False) return False, 'Slamming', x, all_checks if print_result: - print('OK Section', calc_object[0].Stiffener.get_one_line_string(), True) + stf_text = calc_object[0].Stiffener.get_one_line_string() if calc_object[ + 0].Stiffener is not None else 'No stiffener' + print('OK Section', stf_text, True) return True, 'Check OK', x, all_checks + def constraint_geometric(fractions, *args): return sum(fractions) == 1 -def pso_constraint_geometric(x,*args): + +def pso_constraint_geometric(x, *args): ''' The sum of the fractions must be 1.''' - return 1-sum(x) + return 1 - sum(x) + def create_new_cylinder_obj(init_obj, x_new): ''' @@ -775,20 +1110,20 @@ def create_new_cylinder_obj(init_obj, x_new): t1, r1, s2, hw2, tw2, b2, tf2 = x2 ''' - x_old = shell_obj.thk, shell_obj.radius, \ - init_obj.panel_spacing if long_obj is None else long_obj.s/1000, \ - 0 if long_obj is None else long_obj.hw/1000, \ - 0 if long_obj is None else long_obj.tw/1000,\ - 0 if long_obj is None else long_obj.b/1000,\ - 0 if long_obj is None else long_obj.tf/1000, + x_old = shell_obj.thk, shell_obj.radius, \ + init_obj.panel_spacing if long_obj is None else long_obj.s / 1000, \ + 0 if long_obj is None else long_obj.hw / 1000, \ + 0 if long_obj is None else long_obj.tw / 1000, \ + 0 if long_obj is None else long_obj.b / 1000, \ + 0 if long_obj is None else long_obj.tf / 1000, x_new_stress_scaling = x_new[0][0] if not np.isnan(x_new[0][0]) else shell_obj.thk, \ - x_new[0][1] if not np.isnan(x_new[0][1]) else shell_obj.radius,\ - x_new[0][5] if long_obj is None else x_new[1][0], \ - 0 if long_obj is None else x_new[1][2], \ - 0 if long_obj is None else x_new[1][3],\ - 0 if long_obj is None else x_new[1][4],\ - 0 if long_obj is None else x_new[1][5] + x_new[0][1] if not np.isnan(x_new[0][1]) else shell_obj.radius, \ + x_new[0][5] if long_obj is None else x_new[1][0], \ + 0 if long_obj is None else x_new[1][2], \ + 0 if long_obj is None else x_new[1][3], \ + 0 if long_obj is None else x_new[1][4], \ + 0 if long_obj is None else x_new[1][5] new_stresses = stress_scaling_cylinder(x_old, x_new_stress_scaling, stress_press) new_obj = copy.deepcopy(init_obj) @@ -798,17 +1133,18 @@ def create_new_cylinder_obj(init_obj, x_new): if long_obj is None: new_obj.panel_spacing = x_new[0][5] else: - new_obj.LongStfObj.s = x_new[1][0]*1000 - new_obj.LongStfObj.hw = x_new[1][2]*1000 - new_obj.LongStfObj.tw = x_new[1][3]*1000 - new_obj.LongStfObj.b = x_new[1][4]*1000 - new_obj.LongStfObj.tf = x_new[1][5]*1000 + new_obj.LongStfObj.s = x_new[1][0] * 1000 + new_obj.LongStfObj.hw = x_new[1][2] * 1000 + new_obj.LongStfObj.tw = x_new[1][3] * 1000 + new_obj.LongStfObj.b = x_new[1][4] * 1000 + new_obj.LongStfObj.tf = x_new[1][5] * 1000 - #new_obj.LongStfObj.stiffener_type = x_new[1][7] # TODO should be 8 + # new_obj.LongStfObj.stiffener_type = x_new[1][7] # TODO should be 8 return new_obj -def create_new_calc_obj(init_obj,x, fat_dict=None, fdwn = 1, fup = 0.5): + +def create_new_calc_obj(init_obj, x, fat_dict=None, fdwn=1, fup=0.5): ''' Returns a new calculation object to be used in optimization :param init_obj: @@ -822,43 +1158,44 @@ def create_new_calc_obj(init_obj,x, fat_dict=None, fdwn = 1, fup = 0.5): stiffener = init_obj.Stiffener girder = init_obj.Girder x_old = (plate.get_s(), plate.get_pl_thk(), stiffener.get_web_h(), stiffener.get_web_thk(), - stiffener.get_fl_w(), - stiffener.get_fl_thk(), plate.span, stiffener.girder_lg if girder is None else - girder.girder_lg, stiffener.stiffener_type) + stiffener.get_fl_w(), + stiffener.get_fl_thk(), plate.span, stiffener.girder_lg if girder is None else + girder.girder_lg, stiffener.stiffener_type) else: x_old = init_obj.Plate.get_tuple() - - sigma_y1_new = stress_scaling(init_obj.Plate.sigma_y1, init_obj.Plate.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) - sigma_y2_new = stress_scaling(init_obj.Plate.sigma_y2, init_obj.Plate.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) - tau_xy_new = stress_scaling(init_obj.Plate.tau_xy, init_obj.Plate.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) + + sigma_y1_new = stress_scaling(init_obj.Plate.sigma_y1, init_obj.Plate.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + sigma_y2_new = stress_scaling(init_obj.Plate.sigma_y2, init_obj.Plate.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + tau_xy_new = stress_scaling(init_obj.Plate.tau_xy, init_obj.Plate.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) sigma_x1_new = stress_scaling_area(init_obj.Plate.sigma_x1, - sum(get_field_tot_area(x_old)), - sum(get_field_tot_area(x)), fdwn = fdwn, fup = fup) + sum(get_field_tot_area(x_old)), + sum(get_field_tot_area(x)), fdwn=fdwn, fup=fup) sigma_x2_new = stress_scaling_area(init_obj.Plate.sigma_x2, - sum(get_field_tot_area(x_old)), - sum(get_field_tot_area(x)), fdwn = fdwn, fup = fup) + sum(get_field_tot_area(x_old)), + sum(get_field_tot_area(x)), fdwn=fdwn, fup=fup) try: stf_type = x[8] except IndexError: stf_type = init_obj.plate.get_stiffener_type() - - main_dict = {'mat_yield': [init_obj.Plate.get_fy(), 'Pa'],'mat_factor': [init_obj.Plate.mat_factor, ''], + + main_dict = {'mat_yield': [init_obj.Plate.get_fy(), 'Pa'], 'mat_factor': [init_obj.Plate.mat_factor, ''], 'span': [init_obj.Plate.span, 'm'], - 'spacing': [x[0], 'm'],'plate_thk': [x[1], 'm'],'stf_web_height':[ x[2], 'm'], - 'stf_web_thk': [x[3], 'm'],'stf_flange_width': [x[4], 'm'], - 'stf_flange_thk': [x[5], 'm'],'structure_type': [init_obj.Plate.get_structure_type(), ''], - 'stf_type': [stf_type, ''],'sigma_y1': [sigma_y1_new, 'MPa'], - 'sigma_y2': [sigma_y2_new, 'MPa'],'sigma_x1': [sigma_x1_new, 'MPa'],'sigma_x2': [sigma_x2_new, 'MPa'], - 'tau_xy': [tau_xy_new, 'MPa'],'plate_kpp': [init_obj.Plate.get_kpp(), ''], - 'stf_kps': [init_obj.Plate.get_kps(), ''],'stf_km1': [init_obj.Plate.get_km1(), ''], - 'stf_km2': [init_obj.Plate.get_km2(), ''],'stf_km3': [init_obj.Plate.get_km3(), ''], - 'structure_types':[init_obj.Plate.get_structure_types(), ''], + 'spacing': [x[0], 'm'], 'plate_thk': [x[1], 'm'], 'stf_web_height': [x[2], 'm'], + 'stf_web_thk': [x[3], 'm'], 'stf_flange_width': [x[4], 'm'], + 'stf_flange_thk': [x[5], 'm'], 'structure_type': [init_obj.Plate.get_structure_type(), ''], + 'stf_type': [stf_type, ''], 'sigma_y1': [sigma_y1_new, 'MPa'], + 'sigma_y2': [sigma_y2_new, 'MPa'], 'sigma_x1': [sigma_x1_new, 'MPa'], + 'sigma_x2': [sigma_x2_new, 'MPa'], + 'tau_xy': [tau_xy_new, 'MPa'], 'plate_kpp': [init_obj.Plate.get_kpp(), ''], + 'stf_kps': [init_obj.Plate.get_kps(), ''], 'stf_km1': [init_obj.Plate.get_km1(), ''], + 'stf_km2': [init_obj.Plate.get_km2(), ''], 'stf_km3': [init_obj.Plate.get_km3(), ''], + 'structure_types': [init_obj.Plate.get_structure_types(), ''], 'zstar_optimization': [init_obj.Plate.get_z_opt(), ''], - 'puls buckling method':[init_obj.Plate.get_puls_method(),''], - 'puls boundary':[init_obj.Plate.get_puls_boundary(),''], - 'puls stiffener end':[init_obj.Plate.get_puls_stf_end(),''], - 'puls sp or up':[init_obj.Plate.get_puls_sp_or_up(),''], - 'puls up boundary':[init_obj.Plate.get_puls_up_boundary(),''], + 'puls buckling method': [init_obj.Plate.get_puls_method(), ''], + 'puls boundary': [init_obj.Plate.get_puls_boundary(), ''], + 'puls stiffener end': [init_obj.Plate.get_puls_stf_end(), ''], + 'puls sp or up': [init_obj.Plate.get_puls_sp_or_up(), ''], + 'puls up boundary': [init_obj.Plate.get_puls_up_boundary(), ''], 'panel or shell': [init_obj.Plate.panel_or_shell, '']} all_dict = init_obj.get_main_properties() all_dict['Plate'] = main_dict @@ -867,78 +1204,80 @@ def create_new_calc_obj(init_obj,x, fat_dict=None, fdwn = 1, fup = 0.5): if fat_dict == None: return calc.AllStructure(Plate=None if all_dict['Plate'] is None - else calc.CalcScantlings(all_dict['Plate']), - Stiffener=None if all_dict['Stiffener'] is None - else calc.CalcScantlings(all_dict['Stiffener']), - Girder=None if all_dict['Girder'] is None - else calc.CalcScantlings(all_dict['Girder']), - main_dict=all_dict['main dict']), None + else calc.CalcScantlings(all_dict['Plate']), + Stiffener=None if all_dict['Stiffener'] is None + else calc.CalcScantlings(all_dict['Stiffener']), + Girder=None if all_dict['Girder'] is None + else calc.CalcScantlings(all_dict['Girder']), + main_dict=all_dict['main dict']), None else: return calc.AllStructure(Plate=None if all_dict['Plate'] is None - else calc.CalcScantlings(all_dict['Plate']), - Stiffener=None if all_dict['Stiffener'] is None - else calc.CalcScantlings(all_dict['Stiffener']), - Girder=None if all_dict['Girder'] is None - else calc.CalcScantlings(all_dict['Girder']), - main_dict=all_dict['main dict']), \ - calc.CalcFatigue(main_dict, fat_dict) + else calc.CalcScantlings(all_dict['Plate']), + Stiffener=None if all_dict['Stiffener'] is None + else calc.CalcScantlings(all_dict['Stiffener']), + Girder=None if all_dict['Girder'] is None + else calc.CalcScantlings(all_dict['Girder']), + main_dict=all_dict['main dict']), \ + calc.CalcFatigue(main_dict, fat_dict) else: - x_old = [init_obj.get_s(), init_obj.get_pl_thk(), init_obj.get_web_h() , init_obj.get_web_thk(), - init_obj.get_fl_w(),init_obj.get_fl_thk(), init_obj.span, init_obj.girder_lg] + x_old = [init_obj.get_s(), init_obj.get_pl_thk(), init_obj.get_web_h(), init_obj.get_web_thk(), + init_obj.get_fl_w(), init_obj.get_fl_thk(), init_obj.span, init_obj.girder_lg] - sigma_y1_new = stress_scaling(init_obj.sigma_y1, init_obj.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) - sigma_y2_new = stress_scaling(init_obj.sigma_y2, init_obj.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) - tau_xy_new = stress_scaling(init_obj.tau_xy, init_obj.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) + sigma_y1_new = stress_scaling(init_obj.sigma_y1, init_obj.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + sigma_y2_new = stress_scaling(init_obj.sigma_y2, init_obj.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + tau_xy_new = stress_scaling(init_obj.tau_xy, init_obj.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) sigma_x1_new = stress_scaling_area(init_obj.sigma_x1, - sum(get_field_tot_area(x_old)), - sum(get_field_tot_area(x)), fdwn = fdwn, fup = fup) + sum(get_field_tot_area(x_old)), + sum(get_field_tot_area(x)), fdwn=fdwn, fup=fup) sigma_x2_new = stress_scaling_area(init_obj.sigma_x2, - sum(get_field_tot_area(x_old)), - sum(get_field_tot_area(x)), fdwn = fdwn, fup = fup) + sum(get_field_tot_area(x_old)), + sum(get_field_tot_area(x)), fdwn=fdwn, fup=fup) try: stf_type = x[8] except IndexError: stf_type = init_obj.get_stiffener_type() - main_dict = {'mat_yield': [init_obj.get_fy(), 'Pa'],'mat_factor': [init_obj.mat_factor, ''], + main_dict = {'mat_yield': [init_obj.get_fy(), 'Pa'], 'mat_factor': [init_obj.mat_factor, ''], 'span': [init_obj.span, 'm'], - 'spacing': [x[0], 'm'],'plate_thk': [x[1], 'm'],'stf_web_height':[ x[2], 'm'], - 'stf_web_thk': [x[3], 'm'],'stf_flange_width': [x[4], 'm'], - 'stf_flange_thk': [x[5], 'm'],'structure_type': [init_obj.get_structure_type(), ''], - 'stf_type': [stf_type, ''],'sigma_y1': [sigma_y1_new, 'MPa'], - 'sigma_y2': [sigma_y2_new, 'MPa'],'sigma_x1': [sigma_x1_new, 'MPa'],'sigma_x2': [sigma_x2_new, 'MPa'], - 'tau_xy': [tau_xy_new, 'MPa'],'plate_kpp': [init_obj.get_kpp(), ''], - 'stf_kps': [init_obj.get_kps(), ''],'stf_km1': [init_obj.get_km1(), ''], - 'stf_km2': [init_obj.get_km2(), ''],'stf_km3': [init_obj.get_km3(), ''], - 'structure_types':[init_obj.get_structure_types(), ''], + 'spacing': [x[0], 'm'], 'plate_thk': [x[1], 'm'], 'stf_web_height': [x[2], 'm'], + 'stf_web_thk': [x[3], 'm'], 'stf_flange_width': [x[4], 'm'], + 'stf_flange_thk': [x[5], 'm'], 'structure_type': [init_obj.get_structure_type(), ''], + 'stf_type': [stf_type, ''], 'sigma_y1': [sigma_y1_new, 'MPa'], + 'sigma_y2': [sigma_y2_new, 'MPa'], 'sigma_x1': [sigma_x1_new, 'MPa'], + 'sigma_x2': [sigma_x2_new, 'MPa'], + 'tau_xy': [tau_xy_new, 'MPa'], 'plate_kpp': [init_obj.get_kpp(), ''], + 'stf_kps': [init_obj.get_kps(), ''], 'stf_km1': [init_obj.get_km1(), ''], + 'stf_km2': [init_obj.get_km2(), ''], 'stf_km3': [init_obj.get_km3(), ''], + 'structure_types': [init_obj.get_structure_types(), ''], 'zstar_optimization': [init_obj.get_z_opt(), ''], - 'puls buckling method':[init_obj.get_puls_method(),''], - 'puls boundary':[init_obj.get_puls_boundary(),''], - 'puls stiffener end':[init_obj.get_puls_stf_end(),''], - 'puls sp or up':[init_obj.get_puls_sp_or_up(),''], - 'puls up boundary':[init_obj.get_puls_up_boundary(),''], + 'puls buckling method': [init_obj.get_puls_method(), ''], + 'puls boundary': [init_obj.get_puls_boundary(), ''], + 'puls stiffener end': [init_obj.get_puls_stf_end(), ''], + 'puls sp or up': [init_obj.get_puls_sp_or_up(), ''], + 'puls up boundary': [init_obj.get_puls_up_boundary(), ''], 'panel or shell': [init_obj.panel_or_shell, '']} if fat_dict == None: return calc.CalcScantlings(main_dict), None else: return calc.CalcScantlings(main_dict), calc.CalcFatigue(main_dict, fat_dict) -def create_new_structure_obj(init_obj, x, fat_dict=None, fdwn = 1, fup = 0.5): + +def create_new_structure_obj(init_obj, x, fat_dict=None, fdwn=1, fup=0.5): ''' Returns a new calculation object to be used in optimization :param init_obj: :return: ''' - x_old = [init_obj.get_s(), init_obj.get_pl_thk(), init_obj.get_web_h() , init_obj.get_web_thk(), - init_obj.get_fl_w() ,init_obj.get_fl_thk(), init_obj.span, init_obj.girder_lg] + x_old = [init_obj.get_s(), init_obj.get_pl_thk(), init_obj.get_web_h(), init_obj.get_web_thk(), + init_obj.get_fl_w(), init_obj.get_fl_thk(), init_obj.span, init_obj.girder_lg] - sigma_y1_new = stress_scaling(init_obj.sigma_y1, init_obj.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) - sigma_y2_new = stress_scaling(init_obj.sigma_y2, init_obj.get_pl_thk(), x[1], fdwn = fdwn, fup = fup) - tau_xy_new = stress_scaling(init_obj.tau_xy, init_obj.get_pl_thk(), x[1],fdwn = fdwn, fup = fup) - sigma_x1_new = stress_scaling_area(init_obj.sigma_x1,sum(get_field_tot_area(x_old)),sum(get_field_tot_area(x)), - fdwn = fdwn, fup = fup) - sigma_x2_new = stress_scaling_area(init_obj.sigma_x2,sum(get_field_tot_area(x_old)),sum(get_field_tot_area(x)), - fdwn = fdwn, fup = fup) + sigma_y1_new = stress_scaling(init_obj.sigma_y1, init_obj.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + sigma_y2_new = stress_scaling(init_obj.sigma_y2, init_obj.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + tau_xy_new = stress_scaling(init_obj.tau_xy, init_obj.get_pl_thk(), x[1], fdwn=fdwn, fup=fup) + sigma_x1_new = stress_scaling_area(init_obj.sigma_x1, sum(get_field_tot_area(x_old)), sum(get_field_tot_area(x)), + fdwn=fdwn, fup=fup) + sigma_x2_new = stress_scaling_area(init_obj.sigma_x2, sum(get_field_tot_area(x_old)), sum(get_field_tot_area(x)), + fdwn=fdwn, fup=fup) try: stf_type = x[8] @@ -947,14 +1286,15 @@ def create_new_structure_obj(init_obj, x, fat_dict=None, fdwn = 1, fup = 0.5): main_dict = {'mat_yield': [init_obj.get_fy(), 'Pa'], 'span': [init_obj.span, 'm'], 'mat_factor': [init_obj.Plate.mat_factor, ''], - 'spacing': [x[0], 'm'], 'plate_thk': [x[1], 'm'], 'stf_web_height': [x[2], 'm'], - 'stf_web_thk': [x[3], 'm'], 'stf_flange_width': [x[4], 'm'], - 'stf_flange_thk': [x[5], 'm'], 'structure_type': [init_obj.get_structure_type(), ''], - 'stf_type': [stf_type, ''], 'sigma_y1': [sigma_y1_new, 'MPa'], - 'sigma_y2': [sigma_y2_new, 'MPa'], 'sigma_x1': [sigma_x1_new, 'MPa'],'sigma_x2': [sigma_x2_new, 'MPa'], - 'tau_xy': [tau_xy_new, 'MPa'], 'plate_kpp': [init_obj.get_kpp(), ''], - 'stf_kps': [init_obj.get_kps(), ''], 'stf_km1': [init_obj.get_km1(), ''], - 'stf_km2': [init_obj.get_km2(), ''], 'stf_km3': [init_obj.get_km3(), ''], + 'spacing': [x[0], 'm'], 'plate_thk': [x[1], 'm'], 'stf_web_height': [x[2], 'm'], + 'stf_web_thk': [x[3], 'm'], 'stf_flange_width': [x[4], 'm'], + 'stf_flange_thk': [x[5], 'm'], 'structure_type': [init_obj.get_structure_type(), ''], + 'stf_type': [stf_type, ''], 'sigma_y1': [sigma_y1_new, 'MPa'], + 'sigma_y2': [sigma_y2_new, 'MPa'], 'sigma_x1': [sigma_x1_new, 'MPa'], + 'sigma_x2': [sigma_x2_new, 'MPa'], + 'tau_xy': [tau_xy_new, 'MPa'], 'plate_kpp': [init_obj.get_kpp(), ''], + 'stf_kps': [init_obj.get_kps(), ''], 'stf_km1': [init_obj.get_km1(), ''], + 'stf_km2': [init_obj.get_km2(), ''], 'stf_km3': [init_obj.get_km3(), ''], 'structure_types': [init_obj.get_structure_types(), ''], 'zstar_optimization': [init_obj.get_z_opt(), ''], 'puls buckling method': [init_obj.get_puls_method(), ''], @@ -964,9 +1304,10 @@ def create_new_structure_obj(init_obj, x, fat_dict=None, fdwn = 1, fup = 0.5): 'puls up boundary': [init_obj.get_puls_up_boundary(), ''], } - #if fat_dict == None: + # if fat_dict == None: return calc.Structure(main_dict) + def get_field_tot_area(x): ''' Total area of a plate field. ''' @@ -974,12 +1315,13 @@ def get_field_tot_area(x): width = 10 else: width = x[7] - plate_area = width*x[1] - stiff_area = (x[2] * x[3]+ x[4] * x[5]) * (width//x[0]) + plate_area = width * x[1] + stiff_area = (x[2] * x[3] + x[4] * x[5]) * (width // x[0]) return plate_area, stiff_area -def calc_weight(x, prt = False): + +def calc_weight(x, prt=False): ''' Calculating the current weight :param current_dict: @@ -993,7 +1335,8 @@ def calc_weight(x, prt = False): span * 7850 * (plate_area + stiff_area)) return span * 7850 * (plate_area + stiff_area) -def calc_weight_pso(x,*args): + +def calc_weight_pso(x, *args): ''' Calculating the current weight :param current_dict: @@ -1003,16 +1346,17 @@ def calc_weight_pso(x,*args): width = args[5] span = args[6] - plate_area = width*x[1] - stiff_area = (x[2] * x[3]+ x[4] * x[5]) * (width//x[0]) + plate_area = width * x[1] + stiff_area = (x[2] * x[3] + x[4] * x[5]) * (width // x[0]) return span * 7850 * (plate_area + stiff_area) -def calc_weight_pso_section(x,*args): + +def calc_weight_pso_section(x, *args): ''' Calculating the weight of a complete section. - :param x: - :param args: - :return: + :param x: + :param args: + :return: ''' stru_objects = args[1] tot_length = args[2] @@ -1022,17 +1366,18 @@ def calc_weight_pso_section(x,*args): tot_weight = 0 for dummy_i in range(len(stru_objects)): - tot_weight += frame_section_area*frame_height*7850 + tot_weight += frame_section_area * frame_height * 7850 count = 0 for stru_object in stru_objects: - span = tot_length*x[count] + span = tot_length * x[count] stru_object.Plate.set_span(span) stru_object.Stiffener.set_span(span) tot_weight += stru_object.Stiffener.get_weight_width_lg() return tot_weight + def calc_weight_cylinder(x): ''' Calculation of total weigth. @@ -1043,29 +1388,30 @@ def calc_weight_cylinder(x): ring (nan, nan, 0.7, 0.02, 0.2, 0.02, nan, nan)] ''' if sum(x[1][0:8]) != 0: - num_long_stf = 2*math.pi*x[0][1]/x[1][0] - long_stf_area = x[1][2]*x[1][3]+x[1][4]*x[1][5] + num_long_stf = 2 * math.pi * x[0][1] / x[1][0] + long_stf_area = x[1][2] * x[1][3] + x[1][4] * x[1][5] long_stf_volume = long_stf_area * x[0][4] * num_long_stf else: long_stf_volume = 0 if sum(x[2][0:8]) != 0: num_ring_stf = x[0][4] / x[0][2] - ring_stf_volume = math.pi*(math.pow(x[0][1],2)-math.pow(x[0][1]-x[2][2],2))*x[2][3] + \ - 2*math.pi*(x[0][1]-x[2][2]) * x[2][4] * x[2][5] + ring_stf_volume = math.pi * (math.pow(x[0][1], 2) - math.pow(x[0][1] - x[2][2], 2)) * x[2][3] + \ + 2 * math.pi * (x[0][1] - x[2][2]) * x[2][4] * x[2][5] ring_stf_tot_vol = ring_stf_volume * num_ring_stf else: ring_stf_tot_vol = 0 if sum(x[3][0:8]) != 0: num_ring_girder = x[0][4] / x[0][3] - ring_frame_volume = math.pi*(math.pow(x[0][1],2)-math.pow(x[0][1]-x[3][2],2))*x[3][3] + \ - 2*math.pi*(x[0][1]-x[3][2])*x[3][4]*x[3][5] - tot_ring_frame_vol = ring_frame_volume*num_ring_girder + ring_frame_volume = math.pi * (math.pow(x[0][1], 2) - math.pow(x[0][1] - x[3][2], 2)) * x[3][3] + \ + 2 * math.pi * (x[0][1] - x[3][2]) * x[3][4] * x[3][5] + tot_ring_frame_vol = ring_frame_volume * num_ring_girder else: tot_ring_frame_vol = 0 shell_volume = 2 * math.pi * x[0][1] * x[0][0] * x[0][4] - return (long_stf_volume+ring_stf_tot_vol+tot_ring_frame_vol+shell_volume)*7850 + return (long_stf_volume + ring_stf_tot_vol + tot_ring_frame_vol + shell_volume) * 7850 + def stress_scaling_cylinder(x1, x2, stress1): ''' @@ -1083,7 +1429,7 @@ def stress_scaling_cylinder(x1, x2, stress1): t1, r1, s1, hw1, tw1, b1, tf1 = x1 t2, r2, s2, hw2, tw2, b2, tf2 = x2 - + sasd1, smsd1, tTsd1, tQsd1, shsd1 = stress1 A1 = hw1 * tw1 + b1 * tf1 @@ -1100,81 +1446,73 @@ def stress_scaling_cylinder(x1, x2, stress1): # Torsional, shear and hoop changes by cylinder thickness. - return sasd1*(thk_eq1/thk_eq2), smsd1*(Itot1/Itot2), tTsd1*(t1/t2), tQsd1*(t1/t2), shsd1*(t1/t2) + return sasd1 * (thk_eq1 / thk_eq2), smsd1 * (Itot1 / Itot2), tTsd1 * (t1 / t2), tQsd1 * (t1 / t2), shsd1 * (t1 / t2) -def stress_scaling(sigma_old,t_old,t_new, fdwn = 1, fup = 0.5): - if t_new <= t_old: #decreasing the thickness - sigma_new = sigma_old*(t_old/(t_old-fdwn*abs((t_old-t_new)))) +def stress_scaling(sigma_old, t_old, t_new, fdwn=1, fup=0.5): + if t_new <= t_old: # decreasing the thickness + sigma_new = sigma_old * (t_old / (t_old - fdwn * abs((t_old - t_new)))) # assert sigma_new >= sigma_old, 'ERROR no stress increase: \n' \ # 't_old '+str(t_old)+' sigma_old '+str(sigma_old)+ \ # '\nt_new '+str(t_new)+' sigma_new '+str(sigma_new) - else: #increasing the thickness + else: # increasing the thickness - sigma_new = sigma_old*(t_old/(t_old+fup*abs((t_old-t_new)))) + sigma_new = sigma_old * (t_old / (t_old + fup * abs((t_old - t_new)))) # assert sigma_new <= sigma_old, 'ERROR no stress reduction: \n' \ # 't_old '+str(t_old)+' sigma_old '+str(sigma_old)+ \ # '\nt_new '+str(t_new)+' sigma_new '+str(sigma_new) return sigma_new -def stress_scaling_area(sigma_old,a_old,a_new, fdwn = 1, fup = 0.5): + +def stress_scaling_area(sigma_old, a_old, a_new, fdwn=1, fup=0.5): ''' Scale stresses using input area ''' - if a_new <= a_old: #decreasing the thickness - sigma_new = sigma_old*(a_old/(a_old-fdwn*abs((a_old-a_new)))) + if a_new <= a_old: # decreasing the thickness + sigma_new = sigma_old * (a_old / (a_old - fdwn * abs((a_old - a_new)))) # assert sigma_new >= sigma_old, 'ERROR no stress increase: \n' \ # 't_old '+str(a_old)+' sigma_old '+str(sigma_old)+ \ # '\nt_new '+str(a_new)+' sigma_new '+str(sigma_new) - else: #increasing the thickness - sigma_new = sigma_old*(a_old/(a_old+fup*abs((a_old-a_new)))) + else: # increasing the thickness + sigma_new = sigma_old * (a_old / (a_old + fup * abs((a_old - a_new)))) # assert sigma_new <= sigma_old, 'ERROR no stress reduction: \n' \ # 't_old '+str(a_old)+' sigma_old '+str(sigma_old)+ \ # '\nt_new '+str(a_new)+' sigma_new '+str(sigma_new) - #print('a_old', a_old, 'sigma_old', sigma_old, '|', 'a_new', a_new, 'sigma_new',sigma_new) + # print('a_old', a_old, 'sigma_old', sigma_old, '|', 'a_new', a_new, 'sigma_new',sigma_new) return sigma_new + def x_to_string(x): ret = '' for val in x: ret += str(val) + '_' return ret -def get_filtered_results(iterable_all,init_stuc_obj,lat_press,init_filter_weight,side='p', - chk=(True,True,True,True,True,True,True, False),fat_dict = None, fat_press = None, - slamming_press=None, processes = None, puls_sheet = None, puls_acceptance = 0.87, - fdwn = 1, fup = 0.5, ml_algo = None): + +def get_filtered_results(iterable_all, init_stuc_obj, lat_press, init_filter_weight, side='p', + chk=(True, True, True, True, True, True, True, False), fat_dict=None, fat_press=None, + slamming_press=None, processes=None, puls_sheet=None, puls_acceptance=0.87, + fdwn=1, fup=0.5, ml_algo=None): ''' Using multiprocessing to return list of applicable results. - :param iterable_all: - :param init_stuc_obj: - :param lat_press: - :param init_filter_weight: - :param side: - :param chk: - :return: - ''' - #print('Init filter weight', init_filter_weight) - ''' - x,obj,lat_press,init_weight,side='p',chk=(True,True,True,True, True, True, True, False), - fat_dict = None, fat_press = None, slamming_press = 0, , puls_results = None, print_result = False + Supports: + chk[7] = built-in SemiAnalytical replacement + chk[8] = ML-CL classification pipeline, deactivated + chk[9] = ML-Numeric UF pipeline ''' + if len(chk) > 8 and chk[8]: + raise NotImplementedError('ML-CL buckling is deactivated. Use ML-Numeric or SemiAnalytical.') + if chk[7]: - # PULS to be used. - #calc.PULSpanel - ''' - dict_to_run[line_given] = self._line_to_struc[line_given][1].get_puls_input() - dict_to_run[line_given]['Identification'] = line_given - dict_to_run[line_given]['Pressure (fixed)'] = self.get_highest_pressure(line_given)['normal'] / 1e6 - ''' - - dict_to_run = {} - for x in iterable_all: - x_id = x_to_string(x) - # calc_object = create_new_calc_obj(init_stuc_obj, x, fat_dict, fdwn = fdwn, fup = fup) + # Built-in SemiAnalytical replacement. Columns: buckling UF, ultimate UF, + # valid prediction, acceptance limit. + sort_again = np.full([len(iterable_all), 4], np.inf, dtype=float) + sort_again[:, 2] = 0.0 + sort_again[:, 3] = float(puls_acceptance) + for idx, x in enumerate(iterable_all): calc_object_stf = None if init_stuc_obj.Stiffener is None else create_new_calc_obj(init_stuc_obj.Stiffener, x, fat_dict, fdwn=fdwn, fup=fup) @@ -1185,27 +1523,16 @@ def get_filtered_results(iterable_all,init_stuc_obj,lat_press,init_filter_weight main_dict=init_stuc_obj.get_main_properties()['main dict']), calc_object_pl[1]] - dict_to_run[x_id] = calc_object[0].Plate.get_puls_input() - dict_to_run[x_id]['Identification'] = x_id - dict_to_run[x_id]['Pressure (fixed)'] = lat_press # PULS sheet to have pressure in MPa + sort_again[idx, 0:4] = _predict_semi_analytical_uf(calc_object, lat_press, puls_acceptance) + + PULSrun = None - PULSrun = calc.PULSpanel(dict_to_run, puls_sheet_location=puls_sheet, puls_acceptance=puls_acceptance) - PULSrun.run_all() - sort_again = None elif chk[8]: + # ML-CL is deactivated. + sp_int, sp_gl_gt, up_int, up_gl_gt = list(), list(), list(), list() + sp_int_idx, sp_gl_gt_idx, up_int_idx, up_gl_gt_idx = list(), list(), list(), list() - # ML-CL to be used. - # Buckling ml-cl - sp_int, sp_gl_gt, up_int, up_gl_gt, \ - sp_int_idx, sp_gl_gt_idx, up_int_idx, up_gl_gt_idx = \ - list(), list(), list(),list(),list(), list(), list(),list() - - # Create iterator - idx_count = 0 for idx, x in enumerate(iterable_all): - idx_count += 1 - # calc_object = create_new_calc_obj(init_stuc_obj, x, fat_dict, fdwn=fdwn, fup=fup) - calc_object_stf = None if init_stuc_obj.Stiffener is None else create_new_calc_obj(init_stuc_obj.Stiffener, x, fat_dict, fdwn=fdwn, fup=fup) @@ -1213,79 +1540,138 @@ def get_filtered_results(iterable_all,init_stuc_obj,lat_press,init_filter_weight calc_object = [calc.AllStructure(Plate=calc_object_pl[0], Stiffener=None if init_stuc_obj.Stiffener is None else calc_object_stf[0], Girder=None, - main_dict=init_stuc_obj.get_main_properties()['main dict']), calc_object_pl[1]] + main_dict=init_stuc_obj.get_main_properties()['main dict']), + calc_object_pl[1]] if calc_object[0].Plate.get_puls_sp_or_up() == 'UP': if calc_object[0].Plate.get_puls_boundary() == 'Int': - up_int.append(calc_object[0].Plate.get_buckling_ml_input(lat_press, alone = False)) + up_int.append(calc_object[0].Plate.get_buckling_ml_input(lat_press, alone=False)) up_int_idx.append(idx) else: - up_gl_gt.append(calc_object[0].Plate.get_buckling_ml_input(lat_press, alone = False)) + up_gl_gt.append(calc_object[0].Plate.get_buckling_ml_input(lat_press, alone=False)) up_gl_gt_idx.append(idx) else: + ml_input = _get_ml_input_for_optimization(calc_object, lat_press) if calc_object[0].Plate.get_puls_boundary() == 'Int': - sp_int.append(calc_object[0].Plate.get_buckling_ml_input(lat_press, alone = False)) + sp_int.append(ml_input) sp_int_idx.append(idx) else: - sp_gl_gt.append(calc_object[0].Plate.get_buckling_ml_input(lat_press, alone = False)) + sp_gl_gt.append(ml_input) sp_gl_gt_idx.append(idx) - # Predict - sort_again = np.zeros([len(iterable_all),2]) + # Existing classification result container. + sort_again = np.zeros([len(iterable_all), 2]) if len(sp_int) != 0: sp_int_res = [ml_algo['cl SP buc int predictor'].predict(ml_algo['cl SP buc int scaler'] .transform(sp_int)), - ml_algo['cl SP ult int predictor'].predict(ml_algo['cl SP buc int scaler'] + ml_algo['cl SP ult int predictor'].predict(ml_algo['cl SP ult int scaler'] .transform(sp_int))] - for idx, res_buc, res_ult in zip(sp_int_idx, sp_int_res[0],sp_int_res[1]): + for idx, res_buc, res_ult in zip(sp_int_idx, sp_int_res[0], sp_int_res[1]): sort_again[idx] = [res_buc, res_ult] if len(sp_gl_gt) != 0: sp_gl_gt_res = [ml_algo['cl SP buc GLGT predictor'].predict(ml_algo['cl SP buc GLGT scaler'] .transform(sp_gl_gt)), - ml_algo['cl SP buc GLGT predictor'].predict(ml_algo['cl SP buc GLGT scaler'] - .transform(sp_gl_gt))] - for idx, res_buc, res_ult in zip(sp_gl_gt_idx, sp_gl_gt_res[0],sp_gl_gt_res[1]): + ml_algo['cl SP ult GLGT predictor'].predict(ml_algo['cl SP ult GLGT scaler'] + .transform(sp_gl_gt))] + for idx, res_buc, res_ult in zip(sp_gl_gt_idx, sp_gl_gt_res[0], sp_gl_gt_res[1]): sort_again[idx] = [res_buc, res_ult] + if len(up_int) != 0: up_int_res = [ml_algo['cl UP buc int predictor'].predict(ml_algo['cl UP buc int scaler'] .transform(up_int)), - ml_algo['cl UP ult int predictor'].predict(ml_algo['cl UP buc int scaler'] + ml_algo['cl UP ult int predictor'].predict(ml_algo['cl UP ult int scaler'] .transform(up_int))] - for idx, res_buc, res_ult in zip(up_int_idx, up_int_res[0],up_int_res[1]): + for idx, res_buc, res_ult in zip(up_int_idx, up_int_res[0], up_int_res[1]): sort_again[idx] = [res_buc, res_ult] + if len(up_gl_gt) != 0: - up_gl_gt_res =[ml_algo['cl UP buc GLGT predictor'].predict(ml_algo['cl UP buc GLGT scaler'] + up_gl_gt_res = [ml_algo['cl UP buc GLGT predictor'].predict(ml_algo['cl UP buc GLGT scaler'] .transform(up_gl_gt)), - ml_algo['cl UP buc GLGT predictor'].predict(ml_algo['cl UP buc GLGT scaler'] - .transform(up_gl_gt))] - for idx, res_buc, res_ult in zip(up_gl_gt_idx, up_gl_gt_res[0],up_gl_gt_res[1]): + ml_algo['cl UP ult GLGT predictor'].predict(ml_algo['cl UP ult GLGT scaler'] + .transform(up_gl_gt))] + for idx, res_buc, res_ult in zip(up_gl_gt_idx, up_gl_gt_res[0], up_gl_gt_res[1]): sort_again[idx] = [res_buc, res_ult] + PULSrun = None + + elif chk[9]: + # ML-Numeric UF to be used. + numeric_groups = { + 'num SP int': {'inputs': [], 'indices': []}, + 'num SP GLGT': {'inputs': [], 'indices': []}, + 'num UP int': {'inputs': [], 'indices': []}, + 'num UP GLGT': {'inputs': [], 'indices': []}, + } + + for idx, x in enumerate(iterable_all): + calc_object_stf = None if init_stuc_obj.Stiffener is None else create_new_calc_obj(init_stuc_obj.Stiffener, + x, fat_dict, + fdwn=fdwn, fup=fup) + calc_object_pl = create_new_calc_obj(init_stuc_obj.Plate, x, fat_dict, fdwn=fdwn, fup=fup) + calc_object = [calc.AllStructure(Plate=calc_object_pl[0], + Stiffener=None if init_stuc_obj.Stiffener is None else calc_object_stf[0], + Girder=None, + main_dict=init_stuc_obj.get_main_properties()['main dict']), + calc_object_pl[1]] + + prefix = _get_numeric_pipeline_prefix(calc_object) + ml_input = _get_ml_input_for_optimization(calc_object, lat_press) + numeric_groups[prefix]['inputs'].append(ml_input) + numeric_groups[prefix]['indices'].append(idx) + + # columns: buckling UF, ultimate UF, valid prediction + sort_again = np.full([len(iterable_all), 3], np.inf, dtype=float) + sort_again[:, 2] = 0.0 + + try: + mat_fac = float(init_stuc_obj.Plate.mat_factor) + except Exception: + mat_fac = 1.15 + + for prefix, group in numeric_groups.items(): + if len(group['inputs']) == 0: + continue + + group_res = _predict_numeric_uf_group( + ml_algo=ml_algo, + input_rows=group['inputs'], + prefix=prefix, + mat_fac=mat_fac, + ) + + for idx, res in zip(group['indices'], group_res): + sort_again[idx] = res + + PULSrun = None + else: PULSrun = None - idx_count = 0 - for x in iterable_all: - idx_count += 1 + for _ in iterable_all: + pass sort_again = None iter_var = list() - for idx,item in enumerate(iterable_all): - iter_var.append((item,init_stuc_obj,lat_press,init_filter_weight,side,chk,fat_dict,fat_press,slamming_press, - PULSrun, False,fdwn, fup, sort_again[idx] if chk[8] == True else None)) + for idx, item in enumerate(iterable_all): + if chk[7] or chk[8] or chk[9]: + this_ml_result = sort_again[idx] + else: + this_ml_result = None + + iter_var.append((item, init_stuc_obj, lat_press, init_filter_weight, side, chk, fat_dict, fat_press, + slamming_press, PULSrun, False, fdwn, fup, this_ml_result)) iter_var = tuple(iter_var) - #res_pre = it.starmap(any_constraints_all, iter_var) if processes is None: - processes = max(cpu_count()-1,1) + processes = max(cpu_count() - 1, 1) - with Pool(processes) as my_process: - # res_pre = m - # y_process.starmap_async(any_constraints_all, iter_var).get() - # print('Done calculating') - res_pre = my_process.starmap(any_constraints_all, iter_var) + if processes == 1: + res_pre = [any_constraints_all(*args) for args in iter_var] + else: + with Pool(processes) as my_process: + res_pre = my_process.starmap(any_constraints_all, iter_var) check_ok, check_not_ok = list(), list() for item in res_pre: @@ -1296,7 +1682,8 @@ def get_filtered_results(iterable_all,init_stuc_obj,lat_press,init_filter_weight return check_ok, check_not_ok -def any_get_all_combs(min_var, max_var,deltas, init_weight = float('inf'), predef_stiffeners = None, stf_type = None): + +def any_get_all_combs(min_var, max_var, deltas, init_weight=float('inf'), predef_stiffeners=None, stf_type=None): ''' Calulating initial values. :param min: @@ -1321,14 +1708,14 @@ def any_get_all_combs(min_var, max_var,deltas, init_weight = float('inf'), prede ring_frame_lower_bounds = np.array( [None, None, 0.5, 0.02, 0.2, 0.020]) ''' if min_var[0] is not None: - spacing_array = (np.arange(min_var[0], max_var[0]+ deltas[0], deltas[0])) if min_var[0] != max_var[0] \ + spacing_array = (np.arange(min_var[0], max_var[0] + deltas[0], deltas[0])) if min_var[0] != max_var[0] \ else np.array([min_var[0]]) spacing_array = spacing_array[spacing_array <= max_var[0]] else: spacing_array = np.array([np.nan]) if min_var[1] is not None: - pl_thk_array = (np.arange(min_var[1], max_var[1]+ deltas[1], deltas[1])) if min_var[1] != max_var[1] \ + pl_thk_array = (np.arange(min_var[1], max_var[1] + deltas[1], deltas[1])) if min_var[1] != max_var[1] \ else np.array([min_var[1]]) pl_thk_array = pl_thk_array[pl_thk_array <= max_var[1]] else: @@ -1347,21 +1734,20 @@ def any_get_all_combs(min_var, max_var,deltas, init_weight = float('inf'), prede return predef_iterable - - web_h_array = (np.arange(min_var[2], max_var[2]+ deltas[2], deltas[2])) if min_var[2] != max_var[2] \ + web_h_array = (np.arange(min_var[2], max_var[2] + deltas[2], deltas[2])) if min_var[2] != max_var[2] \ else np.array([min_var[2]]) web_h_array = web_h_array[web_h_array <= max_var[2]] - web_thk_array = (np.arange(min_var[3], max_var[3]+ deltas[3], deltas[3])) if min_var[3] != max_var[3] \ + web_thk_array = (np.arange(min_var[3], max_var[3] + deltas[3], deltas[3])) if min_var[3] != max_var[3] \ else np.array([min_var[3]]) web_thk_array = web_thk_array[web_thk_array <= max_var[3]] - flange_w_array = (np.arange(min_var[4], max_var[4]+ deltas[4], deltas[4])) if min_var[4] != max_var[4] \ + flange_w_array = (np.arange(min_var[4], max_var[4] + deltas[4], deltas[4])) if min_var[4] != max_var[4] \ else np.array([min_var[4]]) flange_w_array = flange_w_array[flange_w_array <= max_var[4]] if min_var[5] is not None: - flange_thk_array = (np.arange(min_var[5], max_var[5]+ deltas[5], deltas[5])) if min_var[5] != max_var[5] \ + flange_thk_array = (np.arange(min_var[5], max_var[5] + deltas[5], deltas[5])) if min_var[5] != max_var[5] \ else np.array([min_var[5]]) flange_thk_array = flange_thk_array[flange_thk_array <= max_var[5]] else: @@ -1380,12 +1766,13 @@ def any_get_all_combs(min_var, max_var,deltas, init_weight = float('inf'), prede girder_array = np.array([np.nan]) comb = it.product(spacing_array, pl_thk_array, web_h_array, web_thk_array, flange_w_array, flange_thk_array, - span_array,girder_array) + span_array, girder_array) return list(comb) -def get_initial_weight(obj,lat_press,min_var,max_var,deltas,trials,fat_dict,fat_press, predefined_stiffener_iter, - slamming_press, fdwn = 1, fup = 0.5, ml_algo = None): + +def get_initial_weight(obj, lat_press, min_var, max_var, deltas, trials, fat_dict, fat_press, predefined_stiffener_iter, + slamming_press, fdwn=1, fup=0.5, ml_algo=None): ''' Return a guess of the initial weight used to filter the constraints. Only aim is to reduce running time of the algorithm. @@ -1395,21 +1782,23 @@ def get_initial_weight(obj,lat_press,min_var,max_var,deltas,trials,fat_dict,fat_ if predefined_stiffener_iter is None: combs = any_get_all_combs(min_var, max_var, deltas) else: - combs = any_get_all_combs(min_var, max_var, deltas,predef_stiffeners=[item.get_tuple() for item in - predefined_stiffener_iter]) + combs = any_get_all_combs(min_var, max_var, deltas, predef_stiffeners=[item.get_tuple() for item in + predefined_stiffener_iter]) trial_selection = random_product(combs, repeat=trials) obj.lat_press = lat_press for x in trial_selection: - if any_constraints_all(x=x,obj=obj,lat_press=lat_press,init_weight=min_weight, - fat_dict=fat_dict,fat_press = fat_press,slamming_press=slamming_press, - fdwn = fdwn, fup = fup)[0]: + if any_constraints_all(x=x, obj=obj, lat_press=lat_press, init_weight=min_weight, + fat_dict=fat_dict, fat_press=fat_press, slamming_press=slamming_press, + fdwn=fdwn, fup=fup)[0]: current_weight = calc_weight(x) if current_weight < min_weight: min_weight = current_weight return min_weight -def get_random_result(obj,lat_press,min_var,max_var,deltas,trials=10000,side='p',const_chk=(True,True,True,True,True), + +def get_random_result(obj, lat_press, min_var, max_var, deltas, trials=10000, side='p', + const_chk=(True, True, True, True, True), fat_dict=None, fat_press=None): ''' Return random results @@ -1417,10 +1806,10 @@ def get_random_result(obj,lat_press,min_var,max_var,deltas,trials=10000,side='p' min_weight = float('inf') ass_var = None combs = any_get_all_combs(min_var, max_var, deltas) - trial_selection = random_product(combs,repeat=trials) + trial_selection = random_product(combs, repeat=trials) for x in trial_selection: - if any_constraints_all(x=x,obj=obj,lat_press=lat_press,init_weight=min_weight,side=side,chk=const_chk, - fat_dict = fat_dict, fat_press = fat_press)[0] is not False: + if any_constraints_all(x=x, obj=obj, lat_press=lat_press, init_weight=min_weight, side=side, chk=const_chk, + fat_dict=fat_dict, fat_press=fat_press)[0] is not False: current_weight = calc_weight(x) if current_weight < min_weight: min_weight = current_weight @@ -1428,9 +1817,11 @@ def get_random_result(obj,lat_press,min_var,max_var,deltas,trials=10000,side='p' if ass_var == None: return ass_var return create_new_structure_obj(obj, [round(item, 5) for item in ass_var]), \ - create_new_calc_obj(obj, [round(item, 5) for item in ass_var])[0] + create_new_calc_obj(obj, [round(item, 5) for item in ass_var])[0] + -def get_random_result_no_bounds(obj,lat_press,min_var,max_var,trials=10000,side='p',const_chk=(True,True,True,True,True) +def get_random_result_no_bounds(obj, lat_press, min_var, max_var, trials=10000, side='p', + const_chk=(True, True, True, True, True) , fat_dict=None, fat_press=None): ''' Return random results, ignoring the deltas @@ -1438,15 +1829,15 @@ def get_random_result_no_bounds(obj,lat_press,min_var,max_var,trials=10000,side= min_weight = float('inf') ass_var = None for trial in range(trials): - spacing = random.randrange(int(min_var[0]*1000),int(max_var[0]*1000),1)/1000 - pl_thk = random.randrange(int(min_var[1]*1000),int(max_var[1]*1000),1)/1000 - web_h = random.randrange(int(min_var[2]*1000),int(max_var[2]*1000),1)/1000 - web_thk = random.randrange(int(min_var[3]*1000),int(max_var[3]*1000),1)/1000 - fl_w = random.randrange(int(min_var[4]*1000),int(max_var[4]*1000),1)/1000 - fl_thk = random.randrange(int(min_var[5]*1000),int(max_var[5]*1000),1)/1000 - x = (spacing,pl_thk,web_h,web_thk,fl_w,fl_thk,min_var[6],min_var[7]) - if any_constraints_all(x=x,obj=obj,lat_press=lat_press,init_weight=min_weight,side=side,chk=const_chk, - fat_dict = fat_dict, fat_press = fat_press)[0]: + spacing = random.randrange(int(min_var[0] * 1000), int(max_var[0] * 1000), 1) / 1000 + pl_thk = random.randrange(int(min_var[1] * 1000), int(max_var[1] * 1000), 1) / 1000 + web_h = random.randrange(int(min_var[2] * 1000), int(max_var[2] * 1000), 1) / 1000 + web_thk = random.randrange(int(min_var[3] * 1000), int(max_var[3] * 1000), 1) / 1000 + fl_w = random.randrange(int(min_var[4] * 1000), int(max_var[4] * 1000), 1) / 1000 + fl_thk = random.randrange(int(min_var[5] * 1000), int(max_var[5] * 1000), 1) / 1000 + x = (spacing, pl_thk, web_h, web_thk, fl_w, fl_thk, min_var[6], min_var[7]) + if any_constraints_all(x=x, obj=obj, lat_press=lat_press, init_weight=min_weight, side=side, chk=const_chk, + fat_dict=fat_dict, fat_press=fat_press)[0]: current_weight = calc_weight(x) if current_weight < min_weight: min_weight = current_weight @@ -1454,31 +1845,33 @@ def get_random_result_no_bounds(obj,lat_press,min_var,max_var,trials=10000,side= if ass_var == None: return ass_var return create_new_structure_obj(obj, [round(item, 5) for item in ass_var]), \ - create_new_calc_obj(obj, [round(item, 5) for item in ass_var])[0] + create_new_calc_obj(obj, [round(item, 5) for item in ass_var])[0] + def random_product(*args, repeat=1): "Random selection from itertools.product(*args, **kwds)" pools = [tuple(pool) for pool in args] * repeat return tuple(random.choice(pool) for pool in pools) -def product_any(*args, repeat=1,weight=float('inf')): + +def product_any(*args, repeat=1, weight=float('inf')): # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 pools = [tuple(pool) for pool in args] * repeat result = [[]] for pool in pools: - result = [x+[y] for x in result for y in pool] + result = [x + [y] for x in result for y in pool] for prod in result: if calc_weight(prod) < weight: yield tuple(prod) -def plot_optimization_results(results, multiple = False): +def plot_optimization_results(results, multiple=False): check_ok_array, check_array, section_array = list(), list(), list() save_to_csv = asksaveasfilename() if save_to_csv != '': - csv_file = open(save_to_csv,'w', newline='') + csv_file = open(save_to_csv, 'w', newline='') csv_writer = csv.writer(csv_file) csv_writer.writerow(['Is OK', 'Check info', 'pl b', 'pl thk', 'web h', 'web thk', 'fl b', 'fl thk', 'span', 'girder width', 'stiffener type', 'uf weight', 'uf sec mod', 'uf loc stf buc', @@ -1493,7 +1886,7 @@ def plot_optimization_results(results, multiple = False): to_write.append(check_ok) to_write.append(check) [to_write.append(item) for item in section] - if(len(section) == 8): + if (len(section) == 8): to_write.append('T') [to_write.append(item) for item in ufres] csv_writer.writerow(to_write) @@ -1502,8 +1895,8 @@ def plot_optimization_results(results, multiple = False): csv_file.close() check_ok_array, check_array, section_array = np.array(check_ok_array), \ - np.array(check_array), \ - np.array(section_array) + np.array(check_array), \ + np.array(section_array) x_label = np.unique(check_array) y = [np.count_nonzero(check_array == item) for item in np.unique(check_array)] @@ -1518,38 +1911,39 @@ def plot_optimization_results(results, multiple = False): plt.show() + if __name__ == '__main__': import example_data as ex from calc_structure import CylinderAndCurvedPlate, Structure, Shell shell_main_dict = ex.shell_main_dict shell_main_dict['geometry'] = [7, ''] - #Structure(ex.obj_dict_cyl_ring) - #Structure(ex.obj_dict_cyl_heavy_ring) + # Structure(ex.obj_dict_cyl_ring) + # Structure(ex.obj_dict_cyl_heavy_ring) # my_cyl = CylinderAndCurvedPlate(main_dict = ex.shell_main_dict, shell= Shell(ex.shell_dict), # long_stf= Structure(ex.obj_dict_cyl_long2), # ring_stf = Structure(ex.obj_dict_cyl_ring2), # ring_frame= Structure(ex.obj_dict_cyl_heavy_ring2)) - my_cyl = CylinderAndCurvedPlate(main_dict = ex.shell_main_dict, shell= Shell(ex.shell_dict), - long_stf= Structure(ex.obj_dict_cyl_long2), - ring_stf = None,# Structure(ex.obj_dict_cyl_ring2), - ring_frame= None)#Structure(ex.obj_dict_cyl_heavy_ring2)) + my_cyl = CylinderAndCurvedPlate(main_dict=ex.shell_main_dict, shell=Shell(ex.shell_dict), + long_stf=Structure(ex.obj_dict_cyl_long2), + ring_stf=None, # Structure(ex.obj_dict_cyl_ring2), + ring_frame=None) # Structure(ex.obj_dict_cyl_heavy_ring2)) - shell_upper_bounds = np.array( [0.03, 5, 5, 5, 10, None, None, None]) - shell_deltas = np.array( [0.005, 0.5, 1, 0.1,1, None, None, None]) - shell_lower_bounds = np.array( [0.02, 5, 5, 5, 10, None, None, None]) + shell_upper_bounds = np.array([0.03, 5, 5, 5, 10, None, None, None]) + shell_deltas = np.array([0.005, 0.5, 1, 0.1, 1, None, None, None]) + shell_lower_bounds = np.array([0.02, 5, 5, 5, 10, None, None, None]) - long_upper_bounds = np.array( [0.8, None, 0.5, 0.02, 0.2, 0.03, None, None]) - long_deltas = np.array( [0.1, None, 0.1, 0.01, 0.1, 0.01, None, None]) - long_lower_bounds = np.array( [0.7, None, 0.3, 0.01, 0.1, 0.01, None, None]) + long_upper_bounds = np.array([0.8, None, 0.5, 0.02, 0.2, 0.03, None, None]) + long_deltas = np.array([0.1, None, 0.1, 0.01, 0.1, 0.01, None, None]) + long_lower_bounds = np.array([0.7, None, 0.3, 0.01, 0.1, 0.01, None, None]) - ring_stf_upper_bounds = np.array( [None, None, 0.5, 0.018, 0.2, 0.03, None, None]) - ring_stf_deltas = np.array( [None, None, 0.1, 0.004, 0.1, 0.01, None, None]) - ring_stf_lower_bounds = np.array( [None, None, 0.3, 0.010, 0.1, 0.010, None, None]) + ring_stf_upper_bounds = np.array([None, None, 0.5, 0.018, 0.2, 0.03, None, None]) + ring_stf_deltas = np.array([None, None, 0.1, 0.004, 0.1, 0.01, None, None]) + ring_stf_lower_bounds = np.array([None, None, 0.3, 0.010, 0.1, 0.010, None, None]) - ring_frame_upper_bounds = np.array( [None, None, 0.9, 0.04, 0.3, 0.04, None, None]) - ring_frame_deltas = np.array( [None, None, 0.2, 0.01, 0.1, 0.01, None, None]) - ring_frame_lower_bounds = np.array( [None, None, 0.7, 0.02, 0.2, 0.02, None, None]) + ring_frame_upper_bounds = np.array([None, None, 0.9, 0.04, 0.3, 0.04, None, None]) + ring_frame_deltas = np.array([None, None, 0.2, 0.01, 0.1, 0.01, None, None]) + ring_frame_lower_bounds = np.array([None, None, 0.7, 0.02, 0.2, 0.02, None, None]) max_var = [shell_upper_bounds, long_upper_bounds, ring_stf_upper_bounds, ring_frame_upper_bounds] deltas = [shell_deltas, long_deltas, ring_stf_deltas, ring_frame_deltas] @@ -1557,9 +1951,12 @@ def plot_optimization_results(results, multiple = False): results = run_optmizataion(initial_structure_obj=my_cyl, min_var=min_var, max_var=max_var, deltas=deltas, cylinder=True, use_weight_filter=True) - shell = ['Shell thk. [mm]', 'Shell radius [mm]' , 'l rings [mm]', 'L shell [mm]', 'L tot. [mm]', 'N/A - future', 'N/A - future', 'N/A - future'] - stf_long = ['Spacing [mm]', 'Plate thk. [mm]', 'Web height [mm]', 'Web thk. [mm]', 'Flange width [mm]', 'Flange thk. [mm]', 'N/A - future', 'N/A - future'] - stf_ring = ['N/A', 'Plate thk. [mm]', 'Web height [mm]', 'Web thk. [mm]', 'Flange width [mm]', 'Flange thk. [mm]', 'N/A - future', 'N/A - future'] + shell = ['Shell thk. [mm]', 'Shell radius [mm]', 'l rings [mm]', 'L shell [mm]', 'L tot. [mm]', 'N/A - future', + 'N/A - future', 'N/A - future'] + stf_long = ['Spacing [mm]', 'Plate thk. [mm]', 'Web height [mm]', 'Web thk. [mm]', 'Flange width [mm]', + 'Flange thk. [mm]', 'N/A - future', 'N/A - future'] + stf_ring = ['N/A', 'Plate thk. [mm]', 'Web height [mm]', 'Web thk. [mm]', 'Flange width [mm]', 'Flange thk. [mm]', + 'N/A - future', 'N/A - future'] # obj_dict = ex.obj_dict_sec_error # fat_obj = ex.get_fatigue_object_problematic() @@ -1640,8 +2037,6 @@ def plot_optimization_results(results, multiple = False): # plt.legend(loc=2) # plt.show() - - # for swarm_size in [100, 1000, 10000, 100000, 1000000]: # t1 = time.time() # @@ -1678,3 +2073,4 @@ def plot_optimization_results(results, multiple = False): + diff --git a/anystruct/optimize_cylinder.py b/anystruct/optimize_cylinder.py index d1f8cee3..340601d2 100644 --- a/anystruct/optimize_cylinder.py +++ b/anystruct/optimize_cylinder.py @@ -12,11 +12,13 @@ import anystruct.optimize as op import anystruct.example_data as test import anystruct.helper as hlp + import anystruct.line_structure as line_structure except ModuleNotFoundError: import ANYstructure.anystruct.main_application as main_application import ANYstructure.anystruct.optimize as op import ANYstructure.anystruct.example_data as test import ANYstructure.anystruct.helper as hlp + import ANYstructure.anystruct.line_structure as line_structure class CreateOptimizeCylinderWindow(): ''' @@ -34,45 +36,128 @@ def __init__(self,master,app=None): self._fatigue_pressure = test.get_fatigue_pressures() self._slamming_pressure = test.get_slamming_pressure() image_dir = os.path.dirname(__file__)+'\\images\\' - self._PULS_object = None - self._puls_acceptance = 0.87 - self._initial_cylinder_obj = calc.CylinderAndCurvedPlate(main_dict=test.shell_main_dict, shell=calc.Shell(test.shell_dict), long_stf=calc.Structure(test.obj_dict_cyl_long2), ring_stf=None,#calc.Structure(test.obj_dict_cyl_ring2), ring_frame=None)#calc.Structure(test.obj_dict_cyl_heavy_ring2)) - self._ML_buckling = dict() # Buckling machine learning algorithm - for name, file_base in zip(['cl SP buc int predictor', 'cl SP buc int scaler', - 'cl SP ult int predictor', 'cl SP ult int scaler', - 'cl SP buc GLGT predictor', 'cl SP buc GLGT scaler', - 'cl SP ult GLGT predictor', 'cl SP ult GLGT scaler', - 'cl UP buc int predictor', 'cl UP buc int scaler', - 'cl UP ult int predictor', 'cl UP ult int scaler', - 'cl UP buc GLGT predictor', 'cl UP buc GLGT scaler', - 'cl UP ult GLGT predictor', 'cl UP ult GLGT scaler' - ], - ["ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_CSR-Tank_req_cl_predictor", - "ml_files\\CL_CSR-Tank_req_cl_UP_scaler", - "ml_files\\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", - "ml_files\\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_SP_scaler"]): + self._ML_buckling = dict() # Buckling machine learning algorithms + + for name, file_base in zip( + [ + # --------------------------------------------------------------------- + # Classification pipeline + # --------------------------------------------------------------------- + 'cl SP buc int predictor', + 'cl SP buc int scaler', + 'cl SP ult int predictor', + 'cl SP ult int scaler', + + 'cl SP buc GLGT predictor', + 'cl SP buc GLGT scaler', + 'cl SP ult GLGT predictor', + 'cl SP ult GLGT scaler', + + 'cl UP buc int predictor', + 'cl UP buc int scaler', + 'cl UP ult int predictor', + 'cl UP ult int scaler', + + 'cl UP buc GLGT predictor', + 'cl UP buc GLGT scaler', + 'cl UP ult GLGT predictor', + 'cl UP ult GLGT scaler', + + 'CSR predictor UP', + 'CSR scaler UP', + 'CSR predictor SP', + 'CSR scaler SP', + + # --------------------------------------------------------------------- + # Numeric UF pipeline + # --------------------------------------------------------------------- + 'num SP int validity predictor', + 'num SP int validity xscaler', + 'num SP int UF reg predictor', + 'num SP int UF reg xscaler', + 'num SP int UF reg yscaler', + + 'num SP GLGT validity predictor', + 'num SP GLGT validity xscaler', + 'num SP GLGT UF reg predictor', + 'num SP GLGT UF reg xscaler', + 'num SP GLGT UF reg yscaler', + + 'num UP int validity predictor', + 'num UP int validity xscaler', + 'num UP int UF reg predictor', + 'num UP int UF reg xscaler', + 'num UP int UF reg yscaler', + + 'num UP GLGT validity predictor', + 'num UP GLGT validity xscaler', + 'num UP GLGT UF reg predictor', + 'num UP GLGT UF reg xscaler', + 'num UP GLGT UF reg yscaler', + ], + [ + # --------------------------------------------------------------------- + # Classification pipeline + # --------------------------------------------------------------------- + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_SP", + + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_SP", + + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_UP", + + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_UP", + + "ml_files\\CLPIPE_CL_CSR-Tank_req_cl_predictor_UP", + "ml_files\\CLPIPE_CL_CSR-Tank_req_cl_scaler_UP", + "ml_files\\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor_SP", + "ml_files\\CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler_SP", + + # --------------------------------------------------------------------- + # Numeric UF pipeline + # --------------------------------------------------------------------- + "ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1", + + "ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3", + + "ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1", + "ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1", + + "ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3", + "ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3", + ], + ): self._ML_buckling[name] = None if os.path.isfile(file_base + '.pickle'): file = open(file_base + '.pickle', 'rb') @@ -90,10 +175,11 @@ def __init__(self,master,app=None): 9: 'UF below or equal 0.87', 10: 'UF between 0.87 and 1.0', 11: 'UF above 1.0'} else: self.app = app - self._initial_structure_obj = app._line_to_struc[app._active_line][0] - self._initial_calc_obj = app._line_to_struc[app._active_line][1] - self._initial_cylinder_obj = app._line_to_struc[app._active_line][5] - self._fatigue_object = app._line_to_struc[app._active_line][2] + active_bundle = app._line_to_struc[app._active_line] + self._initial_structure_obj = line_structure.structure(active_bundle) + self._initial_calc_obj = line_structure.structure(active_bundle) + self._initial_cylinder_obj = line_structure.cylinder(active_bundle) + self._fatigue_object = line_structure.fatigue(active_bundle) try: self._fatigue_pressure = app.get_fatigue_pressures(app._active_line, self._fatigue_object.get_accelerations()) @@ -113,9 +199,6 @@ def __init__(self,master,app=None): self._slamming_pressure = 0 image_dir = app._root_dir +'\\images\\' self._root_dir = app._root_dir - self._PULS_object = app._PULS_results - self._puls_acceptance = self.app._new_puls_uf.get() - self._ML_buckling = app._ML_buckling diff --git a/anystruct/optimize_geometry.py b/anystruct/optimize_geometry.py index de52cbfd..741f3801 100644 --- a/anystruct/optimize_geometry.py +++ b/anystruct/optimize_geometry.py @@ -4,6 +4,10 @@ from tkinter.ttk import Progressbar from tkinter import messagebox import pickle +import os +import time +import copy +import numpy as np from tkinter.filedialog import askopenfilenames from multiprocessing import cpu_count from tkinter import filedialog @@ -13,6 +17,7 @@ import anystruct.main_application import anystruct.optimize as op import anystruct.example_data as test + import anystruct.line_structure as line_structure from anystruct.calc_structure import * import anystruct.calc_structure from anystruct.helper import * @@ -20,6 +25,7 @@ import ANYstructure.anystruct.main_application import ANYstructure.anystruct.optimize as op import ANYstructure.anystruct.example_data as test + import ANYstructure.anystruct.line_structure as line_structure from ANYstructure.anystruct.calc_structure import * import ANYstructure.anystruct.calc_structure from ANYstructure.anystruct.helper import * @@ -30,6 +36,76 @@ class CreateOptGeoWindow(): This class initiates the MultiOpt window. ''' + def _get_selected_ml_buckling(self): + """Return the flat ML model dictionary expected by optimize.py.""" + ml_algo = self._ML_buckling + + # In the main app this may be stored as {1.1: {...}, 1.15: {...}}. + # Optimization needs the flat dictionary for the selected/current material factor. + if isinstance(ml_algo, dict): + try: + mat_fac = float(self.app._new_material_factor.get()) + except Exception: + try: + mat_fac = float(self._initial_calc_obj.Plate.mat_factor) + except Exception: + mat_fac = None + + if mat_fac in ml_algo and isinstance(ml_algo[mat_fac], dict): + return ml_algo[mat_fac] + + # Fallback: if only one nested material-factor dictionary exists, use it. + nested_keys = [ + key for key, value in ml_algo.items() + if isinstance(value, dict) + ] + if len(nested_keys) == 1: + return ml_algo[nested_keys[0]] + + return ml_algo + + def _get_selected_material_factor(self): + """Return selected material factor from main app or fallback to 1.15.""" + try: + return float(self.app._new_material_factor.get()) + except Exception: + pass + try: + return float(self._initial_calc_obj.Plate.mat_factor) + except Exception: + return 1.15 + + def _apply_material_factor_to_structure(self, obj, mat_fac): + """Apply selected material factor to Plate/Stiffener/Girder before optimization.""" + try: + mat_fac = float(mat_fac) + except Exception: + return obj + + for attr_name in ('Plate', 'Stiffener', 'Girder'): + try: + part = getattr(obj, attr_name) + except Exception: + part = None + if part is not None: + try: + part.mat_factor = mat_fac + except Exception: + pass + return obj + + def _load_pickle_first_existing(self, file_bases): + """Load the first existing pickle from one or more base filenames.""" + if isinstance(file_bases, str): + file_bases = (file_bases,) + + for file_base in file_bases: + file_name = file_base if file_base.endswith('.pickle') else file_base + '.pickle' + if os.path.isfile(file_name): + with open(file_name, 'rb') as file: + return pickle.load(file) + return None + def __init__(self, master, app=None): super(CreateOptGeoWindow, self).__init__() if __name__ == '__main__': @@ -40,11 +116,10 @@ def __init__(self, master, app=None): self._point_dict = test.get_point_dict() self._canvas_scale = 20 - self._line_to_struc = test.get_line_to_struc() self._opt_frames = {} - self._active_points = ['point1','point4','point8','point5'] + self._active_points = ['point1', 'point4', 'point8', 'point5'] self._ML_buckling = dict() # Buckling machine learning algorithm for name, file_base in zip(['cl SP buc int predictor', 'cl SP buc int scaler', 'cl SP ult int predictor', 'cl SP ult int scaler', @@ -56,31 +131,121 @@ def __init__(self, master, app=None): 'cl UP ult GLGT predictor', 'cl UP ult GLGT scaler', 'CSR predictor', 'CSR scaler' ], - ["ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_UP", - "CL_CSR-Tank_req_cl_predictor", - "CL_CSR-Tank_req_cl_UP_scaler", - "CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", - "CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_SP_scaler"]): - self._ML_buckling[name] = None - if os.path.isfile(file_base + '.pickle'): - file = open(file_base + '.pickle', 'rb') - self._ML_buckling[name] = pickle.load(file) - file.close() + ["ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_UP", + "CLPIPE_CL_CSR-Tank_req_cl_predictor", + "CLPIPE_CL_CSR-Tank_req_cl_UP_scaler", + "CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", + "CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_SP_scaler"]): + self._ML_buckling[name] = None + if os.path.isfile(file_base + '.pickle'): + file = open(file_base + '.pickle', 'rb') + self._ML_buckling[name] = pickle.load(file) + file.close() + + # Numeric UF pipeline models. These are optional in standalone mode. + numeric_model_specs = ( + ('num SP int validity predictor', ( + 'ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num SP int validity xscaler', ( + 'ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num SP int UF reg predictor', ( + 'ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num SP int UF reg xscaler', ( + 'ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num SP int UF reg yscaler', ( + 'ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + + ('num SP GLGT validity predictor', ( + 'ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num SP GLGT validity xscaler', ( + 'ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num SP GLGT UF reg predictor', ( + 'ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num SP GLGT UF reg xscaler', ( + 'ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num SP GLGT UF reg yscaler', ( + 'ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + + ('num UP int validity predictor', ( + 'ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num UP int validity xscaler', ( + 'ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num UP int UF reg predictor', ( + 'ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num UP int UF reg xscaler', ( + 'ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + ('num UP int UF reg yscaler', ( + 'ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1', + 'ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1', + )), + + ('num UP GLGT validity predictor', ( + 'ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num UP GLGT validity xscaler', ( + 'ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num UP GLGT UF reg predictor', ( + 'ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num UP GLGT UF reg xscaler', ( + 'ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ('num UP GLGT UF reg yscaler', ( + 'ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3', + 'ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3', + )), + ) + + for name, file_bases in numeric_model_specs: + self._ML_buckling[name] = self._load_pickle_first_existing(file_bases) else: self.app = app self._load_objects = app._load_dict @@ -95,7 +260,6 @@ def __init__(self, master, app=None): self._root_dir = app._root_dir self._ML_buckling = app._ML_buckling - self._opt_structure = {} self._opt_frames_obj = [] self._frame = master @@ -103,7 +267,7 @@ def __init__(self, master, app=None): self._frame.geometry('1800x950') self._frame.grab_set() self._canvas_origo = (50, 720 - 50) - + self._canvas_base_origo = self._canvas_origo self._canvas_draw_origo = list(self._canvas_base_origo) self._previous_drag_mouse = list(self._canvas_draw_origo) @@ -123,12 +287,11 @@ def __init__(self, master, app=None): tk.Frame(self._frame, width=770, height=5, bg="grey", colormap="new").place(x=20, y=95) tk.Frame(self._frame, width=770, height=5, bg="grey", colormap="new").place(x=20, y=135) - algorithms = ('anysmart',' ') + algorithms = ('anysmart', ' ') tk.Label(self._frame, text='-- Plate field span optimizer for plate fields separated by frames. --', font='Verdana 15 bold').place(x=10, y=10) - # upper and lower bounds for optimization # [0.6, 0.012, 0.3, 0.01, 0.1, 0.01] self._new_spacing_upper = tk.DoubleVar() @@ -173,7 +336,6 @@ def __init__(self, master, app=None): self._new_option_fraction = tk.IntVar() self._new_option_panel = tk.IntVar() - ent_w = 10 self._ent_spacing_upper = tk.Entry(self._frame, textvariable=self._new_spacing_upper, width=ent_w) self._ent_spacing_lower = tk.Entry(self._frame, textvariable=self._new_spacing_lower, width=ent_w) @@ -219,21 +381,20 @@ def __init__(self, master, app=None): width=ent_w) self._ent_opt_girder_scale_high = tk.Entry(self._frame, textvariable=self._new_opt_girder_scale_high, - width=int(ent_w/2)) + width=int(ent_w / 2)) self._ent_opt_girder_scale_low = tk.Entry(self._frame, textvariable=self._new_opt_girder_scale_low, - width=int(ent_w/2)) + width=int(ent_w / 2)) self._ent_opt_max_span = tk.Entry(self._frame, textvariable=self._new_opt_span_max, - width=int(ent_w/2)) + width=int(ent_w / 2)) self._ent_opt_min_span = tk.Entry(self._frame, textvariable=self._new_opt_span_min, - width=int(ent_w/2)) - + width=int(ent_w / 2)) start_x, start_y, dx, dy = 20, 70, 100, 40 - tk.Label(self._frame, text='Processes\n (CPUs)', font='Verdana 9 bold', bg = 'silver')\ + tk.Label(self._frame, text='Processes\n (CPUs)', font='Verdana 9 bold', bg='silver') \ .place(x=start_x + 8 * dx, y=start_y + 0.5 * dy) - tk.Entry(self._frame, textvariable=self._new_processes, width = 12, bg = 'silver')\ + tk.Entry(self._frame, textvariable=self._new_processes, width=12, bg='silver') \ .place(x=start_x + 8 * dx, y=start_y + 1.4 * dy) self._prop_canvas_dim = (500, 450) @@ -273,8 +434,8 @@ def __init__(self, master, app=None): y=start_y - 0.6 * dy) tk.Label(self._frame, text='Estimated running time for algorithm not calculated.', font='Verdana 9 bold').place(x=start_x, y=start_y + 2.8 * dy) - tk.Label(self._frame, text='- Harmonize stiffener spacing for section.', font='Verdana 9 bold')\ - .place(x=start_x + 5*dx, y=start_y + 2.8 * dy) + tk.Label(self._frame, text='- Harmonize stiffener spacing for section.', font='Verdana 9 bold') \ + .place(x=start_x + 5 * dx, y=start_y + 2.8 * dy) # self._runnig_time_label = tk.Label(self._frame, text='', font='Verdana 9 bold') # self._runnig_time_label.place(x=start_x + 2.7 * dx, y=start_y + 2.8 * dy) # tk.Label(self._frame, text='seconds ', font='Verdana 9 bold').place(x=start_x + 3.3 * dx, y=start_y + 2.8 * dy) @@ -363,9 +524,8 @@ def __init__(self, master, app=None): # self._new_algorithm_random_trials.trace('w', self.update_running_time) # self._new_algorithm.trace('w', self.update_running_time) - self.running_time_per_item = 4e-05 - #self._runnig_time_label.config(text=str(self.get_running_time())) + # self._runnig_time_label.config(text=str(self.get_running_time())) self._ent_algorithm.place(x=start_x + dx * 10, y=start_y + dy) self.algorithm_random_label = tk.Label(self._frame, text='Number of trials') tk.Button(self._frame, text='algorith information', command=self.algorithm_info, bg='white') \ @@ -379,10 +539,9 @@ def __init__(self, master, app=None): # command=self.save_and_close, bg='green', font='Verdana 10 bold', fg='yellow') # self.close_and_save.place(x=start_x + dx * 10, y=10) - tk.Button(self._frame, text='Open predefined stiffeners example', - command=self.open_example_file, bg='white', font='Verdana 10')\ - .place(x=start_x+dx*10,y=10) + command=self.open_example_file, bg='white', font='Verdana 10') \ + .place(x=start_x + dx * 10, y=10) # Selection of constraints self._new_check_sec_mod = tk.BooleanVar() @@ -393,7 +552,9 @@ def __init__(self, master, app=None): self._new_check_slamming = tk.BooleanVar() self._new_check_local_buckling = tk.BooleanVar() self._new_harmonize_spacing = tk.BooleanVar() + self._new_check_buckling_semi_analytical = tk.BooleanVar() self._new_check_buckling_ml_cl = tk.BooleanVar() + self._new_check_buckling_ml_numeric = tk.BooleanVar() self._new_check_sec_mod.set(True) self._new_check_min_pl_thk.set(True) @@ -405,32 +566,40 @@ def __init__(self, master, app=None): self._new_option_fraction.set(None) self._new_option_panel.set(None) self._new_harmonize_spacing.set(False) + self._new_check_buckling_semi_analytical.set(False) self._new_check_buckling_ml_cl.set(False) + self._new_check_buckling_ml_numeric.set(False) + self._new_check_buckling_semi_analytical.trace('w', self.update_running_time) self._new_check_buckling_ml_cl.trace('w', self.update_running_time) + self._new_check_buckling_ml_numeric.trace('w', self.update_running_time) - start_y, start_x, dy = 570, 100, 25 - tk.Label(self._frame,text='Check for minimum section modulus').place(x=start_x+dx*9.7,y=start_y+4*dy) - tk.Label(self._frame, text='Check for minimum plate thk.').place(x=start_x+dx*9.7,y=start_y+5*dy) - tk.Label(self._frame, text='Check for minimum shear area').place(x=start_x+dx*9.7,y=start_y+6*dy) - tk.Label(self._frame, text='Check for buckling (RP-C201)').place(x=start_x+dx*9.7,y=start_y+7*dy) + start_y, start_x, dy = 570, 100, 25 + tk.Label(self._frame, text='Check for minimum section modulus').place(x=start_x + dx * 9.7, y=start_y + 4 * dy) + tk.Label(self._frame, text='Check for minimum plate thk.').place(x=start_x + dx * 9.7, y=start_y + 5 * dy) + tk.Label(self._frame, text='Check for minimum shear area').place(x=start_x + dx * 9.7, y=start_y + 6 * dy) + tk.Label(self._frame, text='Check for buckling (RP-C201)').place(x=start_x + dx * 9.7, y=start_y + 7 * dy) tk.Label(self._frame, text='Check for fatigue (RP-C203)').place(x=start_x + dx * 9.7, y=start_y + 8 * dy) tk.Label(self._frame, text='Check for bow slamming').place(x=start_x + dx * 9.7, y=start_y + 9 * dy) tk.Label(self._frame, text='Check for local stf. buckling').place(x=start_x + dx * 9.7, y=start_y + 10 * dy) - tk.Label(self._frame, text='Check for buckling, ML-CL').place(x=start_x + dx * 9.7, y=start_y + 11 * dy) + tk.Label(self._frame, text='Check for buckling, SemiAnalytical S3/U3').place(x=start_x + dx * 9.7, + y=start_y + 11 * dy) + tk.Label(self._frame, text='Check for buckling, ML-CL deactivated').place(x=start_x + dx * 9.7, y=start_y + 12 * dy) + tk.Label(self._frame, text='Check for buckling, ML-Numeric').place(x=start_x + dx * 9.7, + y=start_y + 13 * dy) - tk.Label(self._frame, text='Frame (girder data) for weight calculation:', font = 'Verdana 9 bold')\ + tk.Label(self._frame, text='Frame (girder data) for weight calculation:', font='Verdana 9 bold') \ .place(x=start_x + dx * 13, - y=start_y + 4 * dy) + y=start_y + 4 * dy) tk.Label(self._frame, text='Girder thickness').place(x=start_x + dx * 13, y=start_y + 5 * dy) tk.Label(self._frame, text='Stiffener height').place(x=start_x + dx * 13, y=start_y + 6 * dy) tk.Label(self._frame, text='Stiffener thickness').place(x=start_x + dx * 13, y=start_y + 7 * dy) tk.Label(self._frame, text='Stf. flange width').place(x=start_x + dx * 13, y=start_y + 8 * dy) tk.Label(self._frame, text='Stf. flange thickenss').place(x=start_x + dx * 13, y=start_y + 9 * dy) - tk.Label(self._frame, text='For weight calculation of girder: Max span mult / Min span mult')\ - .place(x=start_x + dx * 13,y=start_y + 10 * dy) - tk.Label(self._frame, text='Maximum span / Minimum span ->')\ - .place(x=start_x + dx * 13,y=start_y + 12 * dy) + tk.Label(self._frame, text='For weight calculation of girder: Max span mult / Min span mult') \ + .place(x=start_x + dx * 13, y=start_y + 10 * dy) + tk.Label(self._frame, text='Maximum span / Minimum span ->') \ + .place(x=start_x + dx * 13, y=start_y + 14 * dy) self._ent_opt_girder_thk.place(x=start_x + dx * 15, y=start_y + 5 * dy) self._ent_opt_girder_stf_web_h.place(x=start_x + dx * 15, y=start_y + 6 * dy) @@ -442,19 +611,24 @@ def __init__(self, master, app=None): self._ent_opt_max_span.place(x=start_x + dx * 15, y=start_y + 12 * dy) self._ent_opt_min_span.place(x=start_x + dx * 15.5, y=start_y + 12 * dy) - tk.Checkbutton(self._frame,variable=self._new_check_sec_mod).place(x=start_x+dx*12,y=start_y+4*dy) - tk.Checkbutton(self._frame, variable=self._new_check_min_pl_thk).place(x=start_x+dx*12,y=start_y+5*dy) - tk.Checkbutton(self._frame, variable=self._new_check_shear_area).place(x=start_x+dx*12,y=start_y+6*dy) - tk.Checkbutton(self._frame, variable=self._new_check_buckling).place(x=start_x+dx*12,y=start_y+7*dy) + tk.Checkbutton(self._frame, variable=self._new_check_sec_mod).place(x=start_x + dx * 12, y=start_y + 4 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_min_pl_thk).place(x=start_x + dx * 12, y=start_y + 5 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_shear_area).place(x=start_x + dx * 12, y=start_y + 6 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_buckling).place(x=start_x + dx * 12, y=start_y + 7 * dy) tk.Checkbutton(self._frame, variable=self._new_check_fatigue).place(x=start_x + dx * 12, y=start_y + 8 * dy) tk.Checkbutton(self._frame, variable=self._new_check_slamming).place(x=start_x + dx * 12, y=start_y + 9 * dy) tk.Checkbutton(self._frame, variable=self._new_check_local_buckling).place(x=start_x + dx * 12, y=start_y + 10 * dy) - tk.Checkbutton(self._frame, variable=self._new_check_buckling_ml_cl).place(x=start_x + dx * 12, - y=start_y + 11 * dy) - + tk.Checkbutton(self._frame, variable=self._new_check_buckling_semi_analytical).place(x=start_x + dx * 12, + y=start_y + 11 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_buckling_ml_cl, state='disabled').place( + x=start_x + dx * 12, + y=start_y + 12 * dy, + ) + tk.Checkbutton(self._frame, variable=self._new_check_buckling_ml_numeric).place(x=start_x + dx * 12, + y=start_y + 13 * dy) - tk.Checkbutton(self._frame, variable=self._new_harmonize_spacing).place(x=start_x + 3.9*dx, y=180) + tk.Checkbutton(self._frame, variable=self._new_harmonize_spacing).place(x=start_x + 3.9 * dx, y=180) # Stress scaling self._new_fup = tk.DoubleVar() @@ -462,49 +636,48 @@ def __init__(self, master, app=None): self._new_fdwn = tk.DoubleVar() self._new_fdwn.set(1) - tk.Label(self._frame, text='Factor when scaling stresses up, fup')\ - .place(x=start_x + dx * 9.7, y=start_y + 12 * dy) - ent_fup = tk.Entry(self._frame, textvariable=self._new_fup, width = 5) - ent_fup.place(x=start_x + dx * 12, y=start_y + 12 * dy) - tk.Label(self._frame, text='Factor when scaling stresses up, fdown')\ - .place(x=start_x + dx * 9.7, y=start_y + 13 * dy) - ent_fdwn = tk.Entry(self._frame, textvariable=self._new_fdwn, width = 5) - ent_fdwn.place(x=start_x + dx * 12, y=start_y + 13 * dy) + tk.Label(self._frame, text='Factor when scaling stresses up, fup') \ + .place(x=start_x + dx * 9.7, y=start_y + 14 * dy) + ent_fup = tk.Entry(self._frame, textvariable=self._new_fup, width=5) + ent_fup.place(x=start_x + dx * 12, y=start_y + 14 * dy) + tk.Label(self._frame, text='Factor when scaling stresses up, fdown') \ + .place(x=start_x + dx * 9.7, y=start_y + 15 * dy) + ent_fdwn = tk.Entry(self._frame, textvariable=self._new_fdwn, width=5) + ent_fdwn.place(x=start_x + dx * 12, y=start_y + 15 * dy) self._toggle_btn = tk.Button(self._frame, text="Iterate predefiened stiffeners", relief="raised", - command=self.toggle, bg = 'salmon') - self._toggle_btn.place(x=start_x+dx*10.5, y=start_y - dy * 16.8) + command=self.toggle, bg='salmon') + self._toggle_btn.place(x=start_x + dx * 10.5, y=start_y - dy * 16.8) self._toggle_object, self._filez = None, None - self._options_fractions = (None, ) - self._options_panels = (None, ) - tk.Label(self._frame, text='Select number of panels:').place(x=start_x+dx*12, y=start_y - dy * 20.5) - tk.Label(self._frame, text='Select panel to plot: ').place(x=start_x+dx*12, y=start_y - dy * 19.5) + self._options_fractions = (None,) + self._options_panels = (None,) + tk.Label(self._frame, text='Select number of panels:').place(x=start_x + dx * 12, y=start_y - dy * 20.5) + tk.Label(self._frame, text='Select panel to plot: ').place(x=start_x + dx * 12, y=start_y - dy * 19.5) self._ent_option_fractions = tk.OptionMenu(self._frame, self._new_option_fraction, *self._options_fractions, command=self.get_plate_field_options) self._ent_option_field = tk.OptionMenu(self._frame, self._new_option_panel, *self._options_panels, command=self.get_plate_field_options) - self._option_fractions_place = [start_x+dx*13.5, start_y - dy * 20.5] - self._options_panels_place = [start_x+dx*13.5, start_y - dy * 19.5] + self._option_fractions_place = [start_x + dx * 13.5, start_y - dy * 20.5] + self._options_panels_place = [start_x + dx * 13.5, start_y - dy * 19.5] self._ent_option_fractions.place(x=self._option_fractions_place[0], y=self._option_fractions_place[1]) self._ent_option_field.place(x=self._options_panels_place[0], y=self._options_panels_place[1]) - self.run_results = tk.Button(self._frame,text='show calculated', command=self.plot_results, bg='white', - font='Verdana 10',fg='black') - self.run_results.place(x=start_x+dx*13, y=start_y - dy * 18) - - self.run_results_prev = tk.Button(self._frame,text='Show previous\n' - 'results', command=self.show_previous_results, bg='white', - font='Verdana 10',fg='black') - self.run_results_prev.place(x=start_x+dx*15, y=start_y - dy * 20) + self.run_results = tk.Button(self._frame, text='show calculated', command=self.plot_results, bg='white', + font='Verdana 10', fg='black') + self.run_results.place(x=start_x + dx * 13, y=start_y - dy * 18) + self.run_results_prev = tk.Button(self._frame, text='Show previous\n' + 'results', command=self.show_previous_results, bg='white', + font='Verdana 10', fg='black') + self.run_results_prev.place(x=start_x + dx * 15, y=start_y - dy * 20) # ----------------------------------END OF OPTIMIZE SINGLE COPY----------------------------------------------- self.progress_count = tk.IntVar() self.progress_count.set(0) - self.progress_bar = Progressbar(self._frame, orient="horizontal",length=200, mode="determinate", + self.progress_bar = Progressbar(self._frame, orient="horizontal", length=200, mode="determinate", variable=self.progress_count) - #self.progress_bar.place(x=start_x+dx*10.5,y=start_y-dy*16.5) + # self.progress_bar.place(x=start_x+dx*10.5,y=start_y-dy*16.5) self._active_lines = [] self.controls() @@ -589,7 +762,7 @@ def show_previous_results(self): # return self.draw_select_canvas(opt_results=self._geo_results) - def run_optimizaion(self, load_pre = False, save_results = False, harmonize = False): + def run_optimizaion(self, load_pre=False, save_results=False, harmonize=False): ''' Function when pressing the optimization botton inside this window. :return: @@ -602,34 +775,39 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa contraints = (self._new_check_sec_mod.get(), self._new_check_min_pl_thk.get(), self._new_check_shear_area.get(), self._new_check_buckling.get(), self._new_check_fatigue.get(), self._new_check_slamming.get(), - self._new_check_local_buckling.get(), False, self._new_check_buckling_ml_cl.get(), False) + self._new_check_local_buckling.get(), self._new_check_buckling_semi_analytical.get(), + False, + self._new_check_buckling_ml_numeric.get()) + + selected_ml_algo = self._get_selected_ml_buckling() + selected_mat_fac = self._get_selected_material_factor() self.pso_parameters = (self._new_swarm_size.get(), self._new_omega.get(), self._new_phip.get(), - self._new_phig.get(),self._new_maxiter.get(), self._new_minstep.get(), + self._new_phig.get(), self._new_maxiter.get(), self._new_minstep.get(), self._new_minfunc.get()) opt_girder_prop = (self._new_opt_girder_thk.get(), self._new_opt_girder_stf_web_h.get(), self._new_opt_girder_stf_web_thk.get(), self._new_opt_girder_stf_flange_b.get(), - self._new_opt_girder_stf_flange_thk.get(),self._new_opt_girder_scale_high.get(), + self._new_opt_girder_stf_flange_thk.get(), self._new_opt_girder_scale_high.get(), self._new_opt_girder_scale_low.get()) min_max_span = (self._new_opt_span_min.get(), self._new_opt_span_max.get()) - init_objects, fatigue_objects, fat_press_ext_int, slamming_pressures, lateral_press, fatigue_objects, \ - slamming_press = [list() for dummy in range(7)] + slamming_press = [list() for dummy in range(7)] broke = False pressure_side = 'both sides' # default value - for line,coord in self._opt_structure.items(): + for line, coord in self._opt_structure.items(): if self.opt_create_struc_obj(self._opt_structure[line]) is None: broke = True break else: - init_objects.append(self.opt_create_struc_obj(self._opt_structure[line])[0]) + init_obj_single = self.opt_create_struc_obj(self._opt_structure[line])[0] + self._apply_material_factor_to_structure(init_obj_single, selected_mat_fac) + init_objects.append(init_obj_single) fat_obj_single = self.opt_create_struc_obj(self._opt_structure[line])[2] fatigue_objects.append(fat_obj_single) - if __name__ == '__main__': import example_data as ex lateral_press.append(0.2) # for testing @@ -645,17 +823,18 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa else: p1, p2 = self._opt_structure[line] # Check if line is horizontal or vertical - if p2[0] == p1[0]: # Vertical - to_find = [p2[0], min(p2[1], p1[1]) + abs((p2[1]-p1[1])*0.5)] - elif p2[1] == p1[1]: # Horizontal - to_find = [min(p2[0], p1[0])+ (p2[0]-p1[0])*0.5, p2[1]] - else: # Other orientations - to_find = [min(p2[0],p1[0])+abs((p2[0]-p1[0])*0.5), min(p2[1]-p1[1])+abs((p2[1]-p1[1])*0.5)] + if p2[0] == p1[0]: # Vertical + to_find = [p2[0], min(p2[1], p1[1]) + abs((p2[1] - p1[1]) * 0.5)] + elif p2[1] == p1[1]: # Horizontal + to_find = [min(p2[0], p1[0]) + (p2[0] - p1[0]) * 0.5, p2[1]] + else: # Other orientations + to_find = [min(p2[0], p1[0]) + abs((p2[0] - p1[0]) * 0.5), + min(p2[1] - p1[1]) + abs((p2[1] - p1[1]) * 0.5)] # Taking properites from the closest line. closet_line = self.opt_find_closest_orig_line(to_find) - pressure_side = self._line_to_struc[closet_line].overpressure_side - #print('Closest line', closet_line, p1, p2, to_find) + pressure_side = self._line_overpressure_side(closet_line) + # print('Closest line', closet_line, p1, p2, to_find) gotten_lat_press = self.app.get_highest_pressure(closet_line) lateral_press.append(gotten_lat_press['normal'] / 1e6) slamming_press.append(gotten_lat_press['slamming']) @@ -667,7 +846,7 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa fat_press_single['p_int']['part'])) fat_press_ext_int.append(fat_press_tuple) else: - fat_press_ext_int.append(((0, 0, 0), (0, 0,0))) + fat_press_ext_int.append(((0, 0, 0), (0, 0, 0))) # except AttributeError: # print('AttributeError') @@ -685,7 +864,6 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa deltas[0]) resulting_geo = list() - if self._new_harmonize_spacing.get(): geo_results = dict() for spacing in spacings: @@ -694,29 +872,30 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa this_max_var = copy.deepcopy(max_var) this_max_var[0] = spacing - geo_results = op.run_optmizataion(initial_structure_obj=init_objects,min_var=this_min_var, - max_var=this_max_var,lateral_pressure=lateral_press, - deltas=self.get_deltas(), algorithm='anysmart',side=pressure_side, - const_chk = contraints,pso_options = self.pso_parameters, - is_geometric=True,fatigue_obj= fatigue_objects, + geo_results = op.run_optmizataion(initial_structure_obj=init_objects, min_var=this_min_var, + max_var=this_max_var, lateral_pressure=lateral_press, + deltas=self.get_deltas(), algorithm='anysmart', + side=pressure_side, + const_chk=contraints, pso_options=self.pso_parameters, + is_geometric=True, fatigue_obj=fatigue_objects, fat_press_ext_int=fat_press_ext_int, min_max_span=min_max_span, tot_len=self.opt_get_length(), - frame_height=self.opt_get_distance(), frame_distance = distances, + frame_height=self.opt_get_distance(), frame_distance=distances, predefined_stiffener_iter=self._filez, - processes = self._new_processes.get(), + processes=self._new_processes.get(), slamming_press=slamming_press, opt_girder_prop=opt_girder_prop, - fdwn = self._new_fdwn.get(), fup = self._new_fdwn.get(), - ml_algo=self._ML_buckling) + fdwn=self._new_fdwn.get(), fup=self._new_fup.get(), + ml_algo=selected_ml_algo, + material_factor=selected_mat_fac) resulting_geo.append(geo_results) - #need to find the lowest + # need to find the lowest for fraction in resulting_geo[0].keys(): weight = float('inf') best_idx = None for idx, geo_res in enumerate(resulting_geo): this_sub_fraction_weight = geo_res[fraction][0] if this_sub_fraction_weight < weight: - best_idx = idx weight = this_sub_fraction_weight @@ -734,18 +913,20 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa predefined_stiffener_iter=self._filez, processes=self._new_processes.get(), slamming_press=slamming_press, opt_girder_prop=opt_girder_prop, - fdwn=self._new_fdwn.get(), fup=self._new_fdwn.get()) + fdwn=self._new_fdwn.get(), fup=self._new_fup.get(), + ml_algo=selected_ml_algo, + material_factor=selected_mat_fac) self._geo_results = geo_results - if len([val*2 for val in self._geo_results.keys()]) != 0: + if len([val * 2 for val in self._geo_results.keys()]) != 0: self._ent_option_fractions.destroy() self._ent_option_fractions = tk.OptionMenu(self._frame, self._new_option_fraction, - *tuple([val*2 for val in self._geo_results.keys()]), + *tuple([val * 2 for val in self._geo_results.keys()]), command=self.get_plate_field_options) self._ent_option_fractions.place(x=self._option_fractions_place[0], y=self._option_fractions_place[1]) - #SAVING RESULTS + # SAVING RESULTS if save_results: with open('geo_opt_2.pickle', 'wb') as file: pickle.dump(geo_results, file) @@ -755,23 +936,23 @@ def run_optimizaion(self, load_pre = False, save_results = False, harmonize = Fa self._ent_option_fractions.destroy() self._ent_option_fractions = tk.OptionMenu(self._frame, self._new_option_fraction, - *tuple([val*2 for val in self._geo_results.keys()]), + *tuple([val * 2 for val in self._geo_results.keys()]), command=self.get_plate_field_options) self._ent_option_fractions.place(x=self._option_fractions_place[0], y=self._option_fractions_place[1]) save_file, filename = None, None if save_results: - save_file = filedialog.asksaveasfile(mode="w", defaultextension=".txt", title = 'Save results to file') + save_file = filedialog.asksaveasfile(mode="w", defaultextension=".txt", title='Save results to file') if save_file is None: # ask saveasfile return `None` if dialog closed with "cancel". filename = None else: filename = save_file.name save_file, xplot, yplot = self.draw_result_text(self._geo_results, save_to_file=filename) - self.draw_select_canvas(opt_results=self._geo_results, save_file = save_file) + self.draw_select_canvas(opt_results=self._geo_results, save_file=save_file) plt.axes(facecolor='lightslategray') - plt.plot(xplot, yplot,color='yellow', linestyle='solid', marker='o',markerfacecolor='white', markersize=6) + plt.plot(xplot, yplot, color='yellow', linestyle='solid', marker='o', markerfacecolor='white', markersize=6) plt.xlabel('Length of plate fields [m]') plt.ylabel('Weight / max weight') plt.title('Length of plate fields vs. total weight') @@ -788,45 +969,44 @@ def opt_get_fractions(self): break to_return = [] for dummy_i in range(init_fractions): - to_return.append(1/init_fractions) + to_return.append(1 / init_fractions) return to_return - def opt_create_struc_obj(self,opt_line): - ''' Creating preliminary stucture object from selected optimized line. + def opt_create_struc_obj(self, opt_line): + ''' Creating preliminary stucture object from selected optimized line. The properties of the new line oto be optimized is taken from the closest original line.''' pt1 = opt_line[0] pt2 = opt_line[1] vector = [pt2[0] - pt1[0], pt2[1] - pt1[1]] - point = [pt1[0]+vector[0]*0.5, pt1[1]+vector[1]*0.5] + point = [pt1[0] + vector[0] * 0.5, pt1[1] + vector[1] * 0.5] if self.opt_find_closest_orig_line(point) == None: return None - objects = [copy.deepcopy(x) if x != None else None for x in - self._line_to_struc[self.opt_find_closest_orig_line(point)]] - objects[0].Plate.set_span(dist(pt1,pt2)) + objects = self._copy_line_structure_bundle(self.opt_find_closest_orig_line(point)) + objects[0].Plate.set_span(dist(pt1, pt2)) objects[0].Stiffener.set_span(dist(pt1, pt2)) return objects - def opt_find_closest_orig_line(self,coord): + def opt_find_closest_orig_line(self, coord): ''' Find the closest original line to the optimized line. Used to create initial structure objects. ''' - for key,value in self._line_dict.items(): + for key, value in self._line_dict.items(): - pt1 = list(self._point_dict['point'+str(value[0])]) - pt2 = list(self._point_dict['point'+str(value[1])]) - distance = dist(pt2,pt1) - vector = [pt2[0]-pt1[0],pt2[1]-pt1[1]] - current = list(self._point_dict['point'+str(value[0])]) + pt1 = list(self._point_dict['point' + str(value[0])]) + pt2 = list(self._point_dict['point' + str(value[1])]) + distance = dist(pt2, pt1) + vector = [pt2[0] - pt1[0], pt2[1] - pt1[1]] + current = list(self._point_dict['point' + str(value[0])]) for dummy_i in range(1000): - delta = distance/1000 - current[0] += (vector[0]/distance) * delta - current[1] += (vector[1]/distance) * delta - if dist(coord,current) <= 0.1: - if self._line_to_struc[key][0].Plate.get_structure_type() not in ('GENERAL_INTERNAL_NONWT', 'FRAME'): + delta = distance / 1000 + current[0] += (vector[0] / distance) * delta + current[1] += (vector[1] / distance) * delta + if dist(coord, current) <= 0.1: + if self._line_structure_type(key) not in ('GENERAL_INTERNAL_NONWT', 'FRAME'): return key else: return None @@ -834,20 +1014,20 @@ def opt_find_closest_orig_line(self,coord): def opt_get_distance(self): ''' Getting the largest disctance between the two lines to be optimized. ''' if len(self._active_points) == 4: - return dist(self._point_dict[self._active_points[0]],self._point_dict[self._active_points[2]]) + return dist(self._point_dict[self._active_points[0]], self._point_dict[self._active_points[2]]) else: return None def opt_get_length(self): ''' Getting the length of the lines to be optimized. ''' - if len(self._active_points)==4: - return dist(self._point_dict[self._active_points[0]],self._point_dict[self._active_points[1]]) + if len(self._active_points) == 4: + return dist(self._point_dict[self._active_points[0]], self._point_dict[self._active_points[1]]) - def opt_get_fraction_bounds(self, max_len = 6, min_len = 2): + def opt_get_fraction_bounds(self, max_len=6, min_len=2): ''' Return the fraction bounds(basis upper/lower) to be considered. ''' - return int(self.opt_get_length()/max_len), int(self.opt_get_length()/min_len) + return int(self.opt_get_length() / max_len), int(self.opt_get_length() / min_len) - def opt_create_frames(self,fractions): + def opt_create_frames(self, fractions): ''' Creating frames between the the two lines to be optimized. ''' count = 1 @@ -861,73 +1041,73 @@ def opt_create_frames(self,fractions): [self._point_dict[self._active_points[3]][0], self._point_dict[self._active_points[3]][1]]] - start = 0 + start = 0 for fraction in fractions: start += fraction if start != 1: - self._opt_frames['opt_frame'+str(count)] = [[self._point_dict[self._active_points[0]][0] + - round(self.opt_get_length()*start,5), - self._point_dict[self._active_points[0]][1]], - [self._point_dict[self._active_points[2]][0] + - round(self.opt_get_length() * start,5), - self._point_dict[self._active_points[2]][1]]] - count+=1 + self._opt_frames['opt_frame' + str(count)] = [[self._point_dict[self._active_points[0]][0] + + round(self.opt_get_length() * start, 5), + self._point_dict[self._active_points[0]][1]], + [self._point_dict[self._active_points[2]][0] + + round(self.opt_get_length() * start, 5), + self._point_dict[self._active_points[2]][1]]] + count += 1 distances = {'start_dist': dist(self._opt_frames['opt_frame_start'][0], self._opt_frames['opt_frame_start'][1]), 'stop_dist': dist(self._opt_frames['opt_frame_stop'][0], self._opt_frames['opt_frame_stop'][1])} return self._opt_frames, distances - def opt_create_main_structure(self,frames,start1,stop1,start2,stop2): + def opt_create_main_structure(self, frames, start1, stop1, start2, stop2): ''' This creates line definition for the new structure objects. The scipt searches the line to find frames.''' - line1_coord = self._point_dict[start1],self._point_dict[stop1] - line2_coord = self._point_dict[start2],self._point_dict[stop2] + line1_coord = self._point_dict[start1], self._point_dict[stop1] + line2_coord = self._point_dict[start2], self._point_dict[stop2] structure = {} - - p1_low,p1_high = list(line1_coord[0]),list(line2_coord[0]) - p2_low,p2_high = list(line1_coord[1]),list(line2_coord[1]) - vector_low,vector_high = [p2_low[0]-p1_low[0],p2_low[1]-p1_low[1]],[p2_high[0]-p1_high[0],p2_high[1]-p1_high[1]] + p1_low, p1_high = list(line1_coord[0]), list(line2_coord[0]) + p2_low, p2_high = list(line1_coord[1]), list(line2_coord[1]) + vector_low, vector_high = [p2_low[0] - p1_low[0], p2_low[1] - p1_low[1]], [p2_high[0] - p1_high[0], + p2_high[1] - p1_high[1]] # Starting search on the lower or inner line count = 1 - tmp_struc = [p1_low] # starting point defined. + tmp_struc = [p1_low] # starting point defined. found = None for frame, coords in frames.items(): current = list(p1_low) - if frame!='opt_frame_start' and frame!='opt_frame_stop': + if frame != 'opt_frame_start' and frame != 'opt_frame_stop': for jump in range(100): current[0] += vector_low[0] / 100 current[1] += vector_low[1] / 100 - if dist(current,coords[0]) < 0.1 and frame != found: + if dist(current, coords[0]) < 0.1 and frame != found: found = frame tmp_struc.append(coords[0]) - self._opt_structure['opt_struc'+str(count)] = tmp_struc # adding found line + self._opt_structure['opt_struc' + str(count)] = tmp_struc # adding found line tmp_struc = [coords[0]] count += 1 tmp_struc.append(p2_low) - self._opt_structure['opt_struc'+str(count)] = tmp_struc # adding found line (end) + self._opt_structure['opt_struc' + str(count)] = tmp_struc # adding found line (end) count += 1 # Starting search of upper or outer line. - tmp_struc = [p1_high] # starting point defined. + tmp_struc = [p1_high] # starting point defined. found = None for frame, coords in frames.items(): current = list(p1_high) - if frame!='opt_frame_start' and frame!='opt_frame_stop': + if frame != 'opt_frame_start' and frame != 'opt_frame_stop': for jump in range(100): current[0] += vector_high[0] / 100 current[1] += vector_high[1] / 100 - if dist(current,coords[1]) < 0.1 and frame != found: + if dist(current, coords[1]) < 0.1 and frame != found: found = frame tmp_struc.append(coords[1]) - self._opt_structure['opt_struc'+str(count)] = tmp_struc # adding found line + self._opt_structure['opt_struc' + str(count)] = tmp_struc # adding found line tmp_struc = [coords[1]] count += 1 tmp_struc.append(p2_high) - self._opt_structure['opt_struc'+str(count)] = tmp_struc # adding found line (end) + self._opt_structure['opt_struc' + str(count)] = tmp_struc # adding found line (end) return self._opt_structure @@ -978,7 +1158,27 @@ def update_running_time(self, *args): # except ZeroDivisionError: # pass # _tkinter.TclError: pass - if self._new_check_buckling_ml_cl.get(): + selected_buckling_checks = [ + self._new_check_buckling.get(), + self._new_check_buckling_semi_analytical.get(), + False, + self._new_check_buckling_ml_numeric.get(), + ] + + if selected_buckling_checks.count(True) > 1: + messagebox.showerror('You can only select one buckling type. Reselect.') + + if self._new_check_buckling.get(): + self._new_check_buckling.set(False) + self._new_check_local_buckling.set(False) + + if self._new_check_buckling_semi_analytical.get(): + self._new_check_buckling_semi_analytical.set(False) + + if self._new_check_buckling_ml_numeric.get(): + self._new_check_buckling_ml_numeric.set(False) + + elif (self._new_check_buckling_semi_analytical.get() or self._new_check_buckling_ml_numeric.get()): self._new_check_buckling.set(False) self._new_check_local_buckling.set(False) @@ -1062,14 +1262,14 @@ def draw_properties(self, init_obj=None, opt_obj=None, line=None): ctr_y - m * (init_obj.get_pl_thk() + init_obj.get_web_h()), ctr_x + m * init_obj.get_fl_w() / 2, ctr_y - m * ( - init_obj.get_pl_thk() + init_obj.get_web_h() + init_obj.get_fl_thk()), + init_obj.get_pl_thk() + init_obj.get_web_h() + init_obj.get_fl_thk()), fill=init_color, stipple=init_stipple) else: self._canvas_opt.create_rectangle(ctr_x - m * init_obj.get_web_thk() / 2, ctr_y - m * (init_obj.get_pl_thk() + init_obj.get_web_h()), ctr_x + m * init_obj.get_fl_w(), ctr_y - m * ( - init_obj.get_pl_thk() + init_obj.get_web_h() + init_obj.get_fl_thk()), + init_obj.get_pl_thk() + init_obj.get_web_h() + init_obj.get_fl_thk()), fill=init_color, stipple=init_stipple) if opt_obj != None: @@ -1082,35 +1282,35 @@ def draw_properties(self, init_obj=None, opt_obj=None, line=None): m * opt_obj.get_pl_thk(), ctr_x + m * opt_obj.get_web_thk() / 2, ctr_y - m * ( - opt_obj.get_web_h() + opt_obj.get_pl_thk()) + opt_obj.get_web_h() + opt_obj.get_pl_thk()) , fill=opt_color, stipple=opt_stippe) if init_obj.get_stiffener_type() not in ['L', 'L-bulb']: self._canvas_opt.create_rectangle(ctr_x - m * opt_obj.get_fl_w() / 2, ctr_y - m * ( - opt_obj.get_pl_thk() + opt_obj.get_web_h()), + opt_obj.get_pl_thk() + opt_obj.get_web_h()), ctr_x + m * opt_obj.get_fl_w() / 2, ctr_y - m * ( - opt_obj.get_pl_thk() + opt_obj.get_web_h() + - opt_obj.get_fl_thk()), + opt_obj.get_pl_thk() + opt_obj.get_web_h() + + opt_obj.get_fl_thk()), fill=opt_color, stipple=opt_stippe) else: self._canvas_opt.create_rectangle(ctr_x - m * opt_obj.get_web_thk() / 2, ctr_y - m * ( - opt_obj.get_pl_thk() + opt_obj.get_web_h()), + opt_obj.get_pl_thk() + opt_obj.get_web_h()), ctr_x + m * opt_obj.get_fl_w(), ctr_y - m * ( - opt_obj.get_pl_thk() + opt_obj.get_web_h() + - opt_obj.get_fl_thk()), + opt_obj.get_pl_thk() + opt_obj.get_web_h() + + opt_obj.get_fl_thk()), fill=opt_color, stipple=opt_stippe) self._canvas_opt.create_line(10, 50, 30, 50, fill=opt_color, width=5) self._canvas_opt.create_text(270, 50, - text='Optimized - Pl.: ' + str(round(opt_obj.get_s() * 1000,1)) + 'x' + - str(round(opt_obj.get_pl_thk() * 1000,1)) + ' Stf.: ' - + str(round(opt_obj.get_web_h() * 1000,1)) + - 'x' + str(round(opt_obj.get_web_thk() * 1000,1)) + '+' + - str(round(opt_obj.get_fl_w() * 1000,1)) + - 'x' + str(round(opt_obj.get_fl_thk() * 1000,1)), + text='Optimized - Pl.: ' + str(round(opt_obj.get_s() * 1000, 1)) + 'x' + + str(round(opt_obj.get_pl_thk() * 1000, 1)) + ' Stf.: ' + + str(round(opt_obj.get_web_h() * 1000, 1)) + + 'x' + str(round(opt_obj.get_web_thk() * 1000, 1)) + '+' + + str(round(opt_obj.get_fl_w() * 1000, 1)) + + 'x' + str(round(opt_obj.get_fl_thk() * 1000, 1)), font='Verdana 8', fill=opt_color) self._canvas_opt.create_text(120, 70, text='Weight (per Lg width): ' + str(int(op.calc_weight([opt_obj.get_s(), @@ -1129,12 +1329,12 @@ def draw_properties(self, init_obj=None, opt_obj=None, line=None): if __name__ == '__main__': lateral_press = 0.2 # for testing else: - lateral_press = self.app.get_highest_pressure(line)['normal'] /1e6 + lateral_press = self.app.get_highest_pressure(line)['normal'] / 1e6 self._canvas_opt.create_text(250, self._prop_canvas_dim[1] - 10, text='Lateral pressure: ' + str(lateral_press) + ' kPa', font='Verdana 10 bold', fill='red') - def draw_select_canvas(self, opt_results = None, save_file = None): + def draw_select_canvas(self, opt_results=None, save_file=None): ''' Making the lines canvas. :return: @@ -1145,9 +1345,11 @@ def draw_select_canvas(self, opt_results = None, save_file = None): if opt_results is None: # stippled lines and text. - self._canvas_select.create_line(self._canvas_draw_origo[0], 0, self._canvas_draw_origo[0], self._select_canvas_dim[1], + self._canvas_select.create_line(self._canvas_draw_origo[0], 0, self._canvas_draw_origo[0], + self._select_canvas_dim[1], stipple='gray50') - self._canvas_select.create_line(0, self._canvas_draw_origo[1], self._select_canvas_dim[0], self._canvas_draw_origo[1], + self._canvas_select.create_line(0, self._canvas_draw_origo[1], self._select_canvas_dim[0], + self._canvas_draw_origo[1], stipple='gray50') self._canvas_select.create_text(self._canvas_draw_origo[0] - 30, self._canvas_draw_origo[1] + 20, text='(0,0)', @@ -1168,21 +1370,24 @@ def draw_select_canvas(self, opt_results = None, save_file = None): coord2 = self.get_point_canvas_coord('point' + str(value[1])) vector = [coord2[0] - coord1[0], coord2[1] - coord1[1]] # drawing a bold line if it is selected - if self._line_to_struc[line][0].Plate.get_structure_type() not in ('GENERAL_INTERNAL_NONWT','FRAME'): + if self._line_structure_type(line) not in ('GENERAL_INTERNAL_NONWT', 'FRAME'): if line in self._active_lines: - self._canvas_select.create_line(coord1, coord2, width=6, fill=color,stipple='gray50') - self._canvas_select.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 + 10, + self._canvas_select.create_line(coord1, coord2, width=6, fill=color, stipple='gray50') + self._canvas_select.create_text(coord1[0] + vector[0] / 2 + 5, + coord1[1] + vector[1] / 2 + 10, text='Line ' + str(get_num(line)), font='Verdand 10 bold', fill='red') else: - self._canvas_select.create_line(coord1, coord2, width=3, fill=color,stipple='gray25') - self._canvas_select.create_text(coord1[0] - 20 + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 + + self._canvas_select.create_line(coord1, coord2, width=3, fill=color, stipple='gray25') + self._canvas_select.create_text(coord1[0] - 20 + vector[0] / 2 + 5, + coord1[1] + vector[1] / 2 + - 10,text='line' + str(get_num(line)),font="Text 8", fill='black') + 10, text='line' + str(get_num(line)), font="Text 8", + fill='black') if len(self._opt_frames) != 0: - for key,value in self._opt_frames.items(): + for key, value in self._opt_frames.items(): coord1 = self.get_canvas_coord(value[0]) coord2 = self.get_canvas_coord(value[1]) vector = [coord2[0] - coord1[0], coord2[1] - coord1[1]] @@ -1190,7 +1395,7 @@ def draw_select_canvas(self, opt_results = None, save_file = None): else: pass - if len(self._active_points)>1: + if len(self._active_points) > 1: color = 'blue' coord1 = self.get_point_canvas_coord(self._active_points[0]) coord2 = self.get_point_canvas_coord(self._active_points[1]) @@ -1208,46 +1413,47 @@ def draw_select_canvas(self, opt_results = None, save_file = None): # drawing the point dictionary - for key,value in self._point_dict.items(): + for key, value in self._point_dict.items(): pt_size = 6 if key in self._active_points: self._canvas_select.create_oval(self.get_point_canvas_coord(key)[0] - pt_size + 2, - self.get_point_canvas_coord(key)[1] - pt_size + 2, - self.get_point_canvas_coord(key)[0] + pt_size + 2, - self.get_point_canvas_coord(key)[1] + pt_size + 2, fill='blue') + self.get_point_canvas_coord(key)[1] - pt_size + 2, + self.get_point_canvas_coord(key)[0] + pt_size + 2, + self.get_point_canvas_coord(key)[1] + pt_size + 2, fill='blue') if self._active_points.index(key) == 0: self._canvas_select.create_text(self.get_point_canvas_coord(key)[0] - 5, self.get_point_canvas_coord(key)[1] - 14, text='START 1', - font=text_type, fill = 'blue') + font=text_type, fill='blue') elif self._active_points.index(key) == 1: self._canvas_select.create_text(self.get_point_canvas_coord(key)[0] - 5, self.get_point_canvas_coord(key)[1] - 14, - text='STOP 1',font=text_type, fill='blue') + text='STOP 1', font=text_type, fill='blue') elif self._active_points.index(key) == 2: self._canvas_select.create_text(self.get_point_canvas_coord(key)[0] - 5, self.get_point_canvas_coord(key)[1] - 14, - text='START 2',font=text_type, fill='blue') + text='START 2', font=text_type, fill='blue') elif self._active_points.index(key) == 3: self._canvas_select.create_text(self.get_point_canvas_coord(key)[0] - 5, self.get_point_canvas_coord(key)[1] - 14, - text='STOP 2',font=text_type, fill='blue') + text='STOP 2', font=text_type, fill='blue') else: pass else: self._canvas_select.create_oval(self.get_point_canvas_coord(key)[0] - pt_size, - self.get_point_canvas_coord(key)[1] - pt_size, - self.get_point_canvas_coord(key)[0] + pt_size, - self.get_point_canvas_coord(key)[1] + pt_size, fill='red') + self.get_point_canvas_coord(key)[1] - pt_size, + self.get_point_canvas_coord(key)[0] + pt_size, + self.get_point_canvas_coord(key)[1] + pt_size, fill='red') self._canvas_select.create_text(self.get_point_canvas_coord(key)[0] - 5, - self.get_point_canvas_coord(key)[1] - 14, text='pt.'+str(get_num(key)), + self.get_point_canvas_coord(key)[1] - 14, + text='pt.' + str(get_num(key)), font='Verdana 8', fill='blue') else: self._canvas_select.create_text([20, 20], text='Results are presented here. ' 'All results may not fit the screen. ' 'All results are seen in your saved result file.', font='Verdana 12 bold', - fill='red', anchor = 'w') + fill='red', anchor='w') delta, start_x, y_loc = 20, 10, 40 @@ -1262,40 +1468,47 @@ def draw_select_canvas(self, opt_results = None, save_file = None): if save_file is not None: save_file.write('\n') save_file.write('--------------------------------------------------------------------------' + '\n') - save_file.write('Plate fields: '+str(len(values[2]['objects']))+ ' Frames: '+ + save_file.write('Plate fields: ' + str(len(values[2]['objects'])) + ' Frames: ' + str(len(values[2]['frames'])) + '\n') self._canvas_select.create_text([start_x + delta, y_loc], - text=str(len(check_ok))+' panels with weight '+ str(round(values[0],1)), + text=str(len(check_ok)) + ' panels with weight ' + str( + round(values[0], 1)), anchor='w', font=text_type) y_loc += delta item_count, endstring = 0, '' for data_idx, data in enumerate(values[1]): for idx, stuc_info in enumerate(data): - if type(stuc_info) == calc_structure.AllStructure: + if isinstance(stuc_info, AllStructure): if y_loc > 700: y_loc = 120 start_x += 350 if item_count == 0: - endstring = ' START 1'+' OK!\n' if values[1][data_idx][3] else ' START 1'+' NOT OK!\n' - elif item_count > 0 and item_count < len(values[1]) / 2-1 and len(values[1]) != 4: - endstring = ' -------'+' OK!\n' if values[1][data_idx][3] else ' -------'+' NOT OK!\n' - elif item_count == len(values[1])/2-1: - endstring = ' -END 1-'+' OK!\n' if values[1][data_idx][3] else ' -END 1-'+' NOT OK!\n' - elif item_count == len(values[1])/2: - endstring = ' START 2'+' OK!\n' if values[1][data_idx][3] else ' START 2'+' NOT OK!\n' - elif item_count > len(values[1])/2 and item_count < len(values[1])-1: - endstring = ' -------'+' OK!\n' if values[1][data_idx][3] else ' -------'+' NOT OK!\n' - elif item_count == len(values[1])-1: - endstring = ' -END 2-'+' OK!\n' if values[1][data_idx][3] else ' -END 2-'+' NOT OK!\n' + endstring = ' START 1' + ' OK!\n' if values[1][data_idx][ + 3] else ' START 1' + ' NOT OK!\n' + elif item_count > 0 and item_count < len(values[1]) / 2 - 1 and len(values[1]) != 4: + endstring = ' -------' + ' OK!\n' if values[1][data_idx][ + 3] else ' -------' + ' NOT OK!\n' + elif item_count == len(values[1]) / 2 - 1: + endstring = ' -END 1-' + ' OK!\n' if values[1][data_idx][ + 3] else ' -END 1-' + ' NOT OK!\n' + elif item_count == len(values[1]) / 2: + endstring = ' START 2' + ' OK!\n' if values[1][data_idx][ + 3] else ' START 2' + ' NOT OK!\n' + elif item_count > len(values[1]) / 2 and item_count < len(values[1]) - 1: + endstring = ' -------' + ' OK!\n' if values[1][data_idx][ + 3] else ' -------' + ' NOT OK!\n' + elif item_count == len(values[1]) - 1: + endstring = ' -END 2-' + ' OK!\n' if values[1][data_idx][ + 3] else ' -END 2-' + ' NOT OK!\n' self._canvas_select.create_text([start_x + delta, y_loc], - text=stuc_info.get_one_line_string_mixed()+endstring, + text=stuc_info.get_one_line_string_mixed() + endstring, anchor='w', font=text_type) y_loc += 15 if save_file is not None: - save_file.write(stuc_info.get_one_line_string_mixed()+' ' + + save_file.write(stuc_info.get_one_line_string_mixed() + ' ' + stuc_info.get_extended_string_mixed() + ' | ' + stuc_info.Plate.get_report_stresses() + endstring) @@ -1309,13 +1522,14 @@ def draw_select_canvas(self, opt_results = None, save_file = None): for val in values[2]['frames']]) + '\n') save_file.write('Scales used on frames: ' + str([str(round(val, 3)) for val in values[2]['scales']]) + '\n') - save_file.write('----------------------------------------------------------------------------'+'\n') + save_file.write( + '----------------------------------------------------------------------------' + '\n') if save_file is not None: save_file.write('\n ------------- END ---------------') save_file.close() - def draw_result_text(self, geo_opt_obj, save_to_file = None): + def draw_result_text(self, geo_opt_obj, save_to_file=None): ''' Textual version of the results. ''' self._canvas_opt.delete('all') @@ -1334,7 +1548,7 @@ def draw_result_text(self, geo_opt_obj, save_to_file = None): self._canvas_opt.create_text([start_x, y_loc], text='| Plate fields | Fields length | Weight index | All OK? |', - font='Verdana 10 bold', fill='red', anchor = 'w') + font='Verdana 10 bold', fill='red', anchor='w') y_loc += delta / 2 self._canvas_opt.create_text([start_x, y_loc], text='************************************************', anchor='w', @@ -1347,7 +1561,6 @@ def draw_result_text(self, geo_opt_obj, save_to_file = None): if weight != float('inf'): max_weight = weight if weight > max_weight else max_weight - if save_to_file is not None: save_file = open(save_to_file, 'w') save_file.write('| Plate fields | Fields length | Weight index | All OK? |\n') @@ -1358,31 +1571,31 @@ def draw_result_text(self, geo_opt_obj, save_to_file = None): check_ok = [val[2] is True for val in value[1]] - self._canvas_opt.create_text([start_x + 20, y_loc ], text=str(len(check_ok)), + self._canvas_opt.create_text([start_x + 20, y_loc], text=str(len(check_ok)), anchor='w', font=text_type) - self._canvas_opt.create_text([start_x + 120, y_loc ], text=str('No results\n' if - self._geo_results[key][1][0][0] is None else - round(self._geo_results[key][1][0][0]. - Plate.span,4)), + self._canvas_opt.create_text([start_x + 120, y_loc], text=str('No results\n' if + self._geo_results[key][1][0][0] is None else + round(self._geo_results[key][1][0][0]. + Plate.span, 4)), anchor='w', font=text_type) - self._canvas_opt.create_text([start_x + 220, y_loc ], + self._canvas_opt.create_text([start_x + 220, y_loc], text=str(round(self._geo_results[key][0] / max_weight, 3)) if max_weight != 0 else '', anchor='w', font=text_type) - self._canvas_opt.create_text([start_x + 330, y_loc ], text=str(all(check_ok)), + self._canvas_opt.create_text([start_x + 330, y_loc], text=str(all(check_ok)), anchor='w', font=text_type) if save_to_file is not None: - save_file.write(str(len(check_ok))+ ' ' + 'No results\n' if self._geo_results[key][1][0][0] is None - else str(round(self._geo_results[key][1][0][0].Plate.span, - 4)) + ' ' + - str(round(self._geo_results[key][0] / max_weight, 3)) - + '\n' if max_weight != 0 else - '' + ' ' + str(all(check_ok))+'\n') + save_file.write(str(len(check_ok)) + ' ' + 'No results\n' if self._geo_results[key][1][0][0] is None + else str(round(self._geo_results[key][1][0][0].Plate.span, + 4)) + ' ' + + str(round(self._geo_results[key][0] / max_weight, 3)) + + '\n' if max_weight != 0 else + '' + ' ' + str(all(check_ok)) + '\n') if self._geo_results[key][1][0][0] is not None: - xplot.append(round(self._geo_results[key][1][0][0].Plate.span,4)) + xplot.append(round(self._geo_results[key][1][0][0].Plate.span, 4)) yplot.append(round(self._geo_results[key][0] / max_weight, 4)) if save_to_file is not None: @@ -1390,6 +1603,21 @@ def draw_result_text(self, geo_opt_obj, save_to_file = None): else: return None, xplot, yplot + def _line_structure_bundle(self, line): + return self._line_to_struc[line] + + def _line_structure(self, line): + return line_structure.structure(self._line_structure_bundle(line)) + + def _line_structure_type(self, line): + return line_structure.plate(self._line_structure_bundle(line)).get_structure_type() + + def _line_overpressure_side(self, line): + return self._line_structure(line).overpressure_side + + def _copy_line_structure_bundle(self, line): + return line_structure.copy_bundle(self._line_structure_bundle(line)) + def algorithm_info(self): ''' When button is clicked, info is displayed.''' @@ -1459,8 +1687,8 @@ def get_point_canvas_coord(self, point_no): :param point_no: :return: ''' - point_coord_x = self._canvas_draw_origo[0] + self._point_dict[point_no][0]* self._canvas_scale - point_coord_y = self._canvas_draw_origo[1] - self._point_dict[point_no][1]* self._canvas_scale + point_coord_x = self._canvas_draw_origo[0] + self._point_dict[point_no][0] * self._canvas_scale + point_coord_y = self._canvas_draw_origo[1] - self._point_dict[point_no][1] * self._canvas_scale return [point_coord_x, point_coord_y] @@ -1527,14 +1755,14 @@ def button_1_click(self, event): self._active_point = '' for point, coords in self._point_dict.items(): point_coord = self.get_point_canvas_coord(point) - if point_coord[0]-margin < click_x < point_coord[0]+margin and\ - point_coord[1]-margin < click_y < point_coord[1]+margin: + if point_coord[0] - margin < click_x < point_coord[0] + margin and \ + point_coord[1] - margin < click_y < point_coord[1] + margin: self._active_point = point self._point_is_active = True - if len(self._active_points)<4: + if len(self._active_points) < 4: self._active_points.append(self._active_point) - if len(self._active_points)==4: + if len(self._active_points) == 4: self.opt_create_frames(self.opt_get_fractions()) self.draw_select_canvas() @@ -1558,7 +1786,7 @@ def button_2_click(self, event): ''' self._previous_drag_mouse = [event.x, event.y] - if self._opt_resutls =={}: + if self._opt_resutls == {}: return click_x = self._canvas_select.winfo_pointerx() - self._canvas_select.winfo_rootx() click_y = self._canvas_select.winfo_pointery() - self._canvas_select.winfo_rooty() @@ -1586,19 +1814,19 @@ def button_2_click(self, event): self._active_lines = [] self._active_lines.append(key) if key in self._opt_resutls.keys() and self._opt_resutls[key] != None: - self.draw_properties(init_obj=self._line_to_struc[key][0], - opt_obj=self._opt_resutls[key][0], + self.draw_properties(init_obj=self._line_structure(key), + opt_obj=self._opt_resutls[key][0], line=key) else: - self.draw_properties(init_obj=self._line_to_struc[key][0], line=key) + self.draw_properties(init_obj=self._line_structure(key), line=key) break self.draw_select_canvas() self.draw_select_canvas() self.update_running_time() ############################# - self.opt_create_main_structure(self.opt_create_frames(self.opt_get_fractions())[0],self._active_points[0], - self._active_points[1],self._active_points[2],self._active_points[3]) + self.opt_create_main_structure(self.opt_create_frames(self.opt_get_fractions())[0], self._active_points[0], + self._active_points[1], self._active_points[2], self._active_points[3]) def save_and_close(self): ''' @@ -1619,7 +1847,7 @@ def save_and_close(self): return self._frame.destroy() - def toggle(self, found_files = None, obj = None, iterating = False, given_path: str = None): + def toggle(self, found_files=None, obj=None, iterating=False, given_path: str = None): ''' On off button. :param found_files: @@ -1633,20 +1861,20 @@ def toggle(self, found_files = None, obj = None, iterating = False, given_path: predefined_structure = None if self._toggle_btn.config('relief')[-1] == 'sunken': self._toggle_btn.config(relief="raised") - self._toggle_btn.config(bg = 'salmon') - self._ent_spacing_upper.config(bg = 'white') - self._ent_spacing_lower.config(bg = 'white') - self._ent_delta_spacing.config(bg = 'white') + self._toggle_btn.config(bg='salmon') + self._ent_spacing_upper.config(bg='white') + self._ent_spacing_lower.config(bg='white') + self._ent_delta_spacing.config(bg='white') self._filez = None else: self._toggle_btn.config(relief="sunken") self._toggle_btn.config(bg='lightgreen') - self._ent_spacing_upper.config(bg = 'lightgreen') - self._ent_spacing_lower.config(bg = 'lightgreen') - self._ent_delta_spacing.config(bg = 'lightgreen') - self._ent_pl_thk_upper.config(bg = 'lightgreen') - self._ent_pl_thk_lower.config(bg = 'lightgreen') - self._ent_delta_pl_thk.config(bg = 'lightgreen') + self._ent_spacing_upper.config(bg='lightgreen') + self._ent_spacing_lower.config(bg='lightgreen') + self._ent_delta_spacing.config(bg='lightgreen') + self._ent_pl_thk_upper.config(bg='lightgreen') + self._ent_pl_thk_lower.config(bg='lightgreen') + self._ent_delta_pl_thk.config(bg='lightgreen') if given_path is None: self._filez = list(askopenfilenames(parent=self._frame, title='Choose files to open', initialdir=self._root_dir)) @@ -1675,32 +1903,33 @@ def plot_results(self): 'Plotting a selected panel' if self._geo_results is not None \ and type(self._new_option_fraction.get()) == int and type(self._new_option_panel.get()) == int: - op.plot_optimization_results(self._geo_results[int(self._new_option_fraction.get()/2)][1] + op.plot_optimization_results(self._geo_results[int(self._new_option_fraction.get() / 2)][1] [self._new_option_panel.get()]) def get_plate_field_options(self, event): if self._geo_results is not None: self._ent_option_field.destroy() - to_add = tuple([val for val in range(len(self._geo_results[int(self._new_option_fraction.get()/2)][1]))]) + to_add = tuple([val for val in range(len(self._geo_results[int(self._new_option_fraction.get() / 2)][1]))]) self._ent_option_field = tk.OptionMenu(self._frame, self._new_option_panel, *to_add) self._ent_option_field.place(x=self._options_panels_place[0], y=self._options_panels_place[1]) - def mouse_scroll(self,event): - self._canvas_scale += event.delta/50 + def mouse_scroll(self, event): + self._canvas_scale += event.delta / 50 self._canvas_scale = 0 if self._canvas_scale < 0 else self._canvas_scale self.draw_select_canvas() - def button_2_click_and_drag(self,event): + def button_2_click_and_drag(self, event): - self._canvas_draw_origo = (self._canvas_draw_origo[0]-(self._previous_drag_mouse[0]-event.x), - self._canvas_draw_origo[1]-(self._previous_drag_mouse[1]-event.y)) + self._canvas_draw_origo = (self._canvas_draw_origo[0] - (self._previous_drag_mouse[0] - event.x), + self._canvas_draw_origo[1] - (self._previous_drag_mouse[1] - event.y)) - self._previous_drag_mouse = (event.x,event.y) + self._previous_drag_mouse = (event.x, event.y) self.draw_select_canvas() + if __name__ == '__main__': root = tk.Tk() my_app = CreateOptGeoWindow(master=root) diff --git a/anystruct/optimize_multiple_window.py b/anystruct/optimize_multiple_window.py index d301ce36..53277e8c 100644 --- a/anystruct/optimize_multiple_window.py +++ b/anystruct/optimize_multiple_window.py @@ -10,6 +10,7 @@ import anystruct.main_application import anystruct.optimize as op import anystruct.example_data as test + import anystruct.line_structure as line_structure from anystruct.calc_structure import * import anystruct.calc_structure as calc from anystruct.helper import * @@ -18,6 +19,7 @@ import ANYstructure.anystruct.main_application import ANYstructure.anystruct.optimize as op import ANYstructure.anystruct.example_data as test + import ANYstructure.anystruct.line_structure as line_structure from ANYstructure.anystruct.calc_structure import * import ANYstructure.anystruct.calc_structure as calc from ANYstructure.anystruct.helper import * @@ -25,12 +27,19 @@ def helper_harmonizer_multi(iterator): - ''' - :param : - :return: - ''' + """ + Multiprocessing helper for harmonized optimization. + + The ML result payload is selected from the active constraint: + chk[8] -> ML-CL, deactivated + chk[9] -> ML-Numeric result, expected [buckling_uf, ultimate_uf, valid_prediction] + """ this_check, master_x = list(), None + use_semi_analytical = len(iterator['info']['checks']) > 7 and iterator['info']['checks'][7] + use_ml_numeric = len(iterator['info']['checks']) > 9 and iterator['info']['checks'][9] + use_ml_cl = len(iterator['info']['checks']) > 8 and iterator['info']['checks'][8] + for slave_line in iterator['info']['lines']: lateral_press = iterator['info'][slave_line]['lateral pressure'] fat_press = iterator['info'][slave_line]['fatigue pressure'] @@ -38,19 +47,40 @@ def helper_harmonizer_multi(iterator): slamming_pressure = iterator['info'][slave_line]['slamming pressure'] chk_calc_obj = iterator['info'][slave_line]['chk_calc_obj'] master_x = list(iterator['x']) - ml_cl = iterator['info'][slave_line]['ML-CL'] + + if use_semi_analytical: + ml_result = iterator['info'][slave_line].get('SemiAnalytical', [float('inf'), float('inf'), 0, 0.87]) + elif use_ml_numeric: + ml_result = iterator['info'][slave_line].get('ML-Numeric', [float('inf'), float('inf'), 0]) + elif use_ml_cl: + ml_result = [0, 0] + else: + ml_result = None + fup = iterator['info'][slave_line]['fup'] fdwn = iterator['info'][slave_line]['fdwn'] + if iterator['info']['keep spacing']: x = [chk_calc_obj.get_s()] + master_x[1:] + [chk_calc_obj.span, chk_calc_obj.girder_lg] else: x = master_x + [chk_calc_obj.span, chk_calc_obj.girder_lg] - chk_any = op.any_constraints_all(x=x, obj=chk_calc_obj, lat_press=lateral_press, - init_weight=float('inf'), side='p', chk=iterator['info']['checks'], - fat_dict=None if fat_obj == None else fat_obj.get_fatigue_properties(), - fat_press=fat_press, slamming_press=slamming_pressure,PULSrun=None, - print_result=False,fdwn=fdwn, fup=fup, ml_results=ml_cl) + chk_any = op.any_constraints_all( + x=x, + obj=chk_calc_obj, + lat_press=lateral_press, + init_weight=float('inf'), + side='p', + chk=iterator['info']['checks'], + fat_dict=None if fat_obj is None else fat_obj.get_fatigue_properties(), + fat_press=fat_press, + slamming_press=slamming_pressure, + PULSrun=None, + print_result=False, + fdwn=fdwn, + fup=fup, + ml_results=ml_result, + ) this_check.append(chk_any[0]) if all(this_check) and master_x is not None: @@ -58,10 +88,12 @@ def helper_harmonizer_multi(iterator): else: return None + class CreateOptimizeMultipleWindow(): ''' This class initiates the MultiOpt window. ''' + def __init__(self, master, app=None): super(CreateOptimizeMultipleWindow, self).__init__() if __name__ == '__main__': @@ -77,7 +109,7 @@ def __init__(self, master, app=None): self._fatigue_pressure = test.get_fatigue_pressures() self._fatigue_object = test.get_fatigue_object() self._normal_pressure = test.get_random_pressure() - image_dir = os.path.dirname(__file__)+'\\images\\' + image_dir = os.path.dirname(__file__) + '\\images\\' self._active_lines = [] self._ML_buckling = dict() # Buckling machine learning algorithm for name, file_base in zip(['cl SP buc int predictor', 'cl SP buc int scaler', @@ -89,33 +121,33 @@ def __init__(self, master, app=None): 'cl UP buc GLGT predictor', 'cl UP buc GLGT scaler', 'cl UP ult GLGT predictor', 'cl UP ult GLGT scaler' ], - ["ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_UP", - "CL_CSR-Tank_req_cl_predictor", - "CL_CSR-Tank_req_cl_UP_scaler", - "CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", - "CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_SP_scaler"]): - self._ML_buckling[name] = None - if os.path.isfile(file_base + '.pickle'): - file = open(file_base + '.pickle', 'rb') - from sklearn.neural_network import MLPClassifier - from sklearn.preprocessing import StandardScaler - self._ML_buckling[name] = pickle.load(file) - file.close() + ["ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_1_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_SP", + "ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_1_UP", + "ml_files\\CLPIPE_CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_UP", + "ml_files\\CLPIPE_CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_UP", + "CLPIPE_CL_CSR-Tank_req_cl_predictor", + "CLPIPE_CL_CSR-Tank_req_cl_UP_scaler", + "CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", + "CLPIPE_CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_SP_scaler"]): + self._ML_buckling[name] = None + if os.path.isfile(file_base + '.pickle'): + file = open(file_base + '.pickle', 'rb') + from sklearn.neural_network import MLPClassifier + from sklearn.preprocessing import StandardScaler + self._ML_buckling[name] = pickle.load(file) + file.close() else: self.app = app self._load_objects = app._load_dict @@ -155,13 +187,11 @@ def __init__(self, master, app=None): tk.Frame(self._frame, width=770, height=5, bg="grey", colormap="new").place(x=20, y=95) tk.Frame(self._frame, width=770, height=5, bg="grey", colormap="new").place(x=20, y=135) - algorithms = ('anysmart', 'random', 'random_no_delta') tk.Label(self._frame, text='-- Structural optimizer for multiple selections --', font='Verdana 15 bold').place(x=10, y=10) - # upper and lower bounds for optimization # [0.6, 0.012, 0.3, 0.01, 0.1, 0.01] self._new_spacing_upper = tk.DoubleVar() @@ -187,12 +217,12 @@ def __init__(self, master, app=None): self._new_delta_fl_w = tk.DoubleVar() self._new_delta_fl_thk = tk.DoubleVar() self._new_swarm_size = tk.IntVar() - self._new_omega = tk.DoubleVar() - self._new_phip = tk.DoubleVar() - self._new_phig = tk.DoubleVar() + self._new_omega = tk.DoubleVar() + self._new_phip = tk.DoubleVar() + self._new_phig = tk.DoubleVar() self._new_maxiter = tk.IntVar() - self._new_minstep = tk.DoubleVar() - self._new_minfunc = tk.DoubleVar() + self._new_minstep = tk.DoubleVar() + self._new_minfunc = tk.DoubleVar() ent_w = 10 self._ent_spacing_upper = tk.Entry(self._frame, textvariable=self._new_spacing_upper, width=ent_w) @@ -209,7 +239,8 @@ def __init__(self, master, app=None): self._ent_fl_thk_lower = tk.Entry(self._frame, textvariable=self._new_fl_thk_lower, width=ent_w) self._ent_span = tk.Entry(self._frame, textvariable=self._new_span, width=ent_w) self._ent_width_lg = tk.Entry(self._frame, textvariable=self._new_width_lg, width=ent_w) - self._ent_algorithm = tk.OptionMenu(self._frame, self._new_algorithm, command=self.selected_algorithm, *algorithms) + self._ent_algorithm = tk.OptionMenu(self._frame, self._new_algorithm, command=self.selected_algorithm, + *algorithms) self._ent_random_trials = tk.Entry(self._frame, textvariable=self._new_algorithm_random_trials) self._ent_delta_spacing = tk.Entry(self._frame, textvariable=self._new_delta_spacing, width=ent_w) self._ent_delta_pl_thk = tk.Entry(self._frame, textvariable=self._new_delta_pl_thk, width=ent_w) @@ -218,36 +249,34 @@ def __init__(self, master, app=None): self._ent_delta_fl_w = tk.Entry(self._frame, textvariable=self._new_delta_fl_w, width=ent_w) self._ent_delta_fl_thk = tk.Entry(self._frame, textvariable=self._new_delta_fl_thk, width=ent_w) - pso_width = 10 - self._ent_swarm_size = tk.Entry(self._frame,textvariable=self._new_swarm_size, width = pso_width) - self._ent_omega = tk.Entry(self._frame,textvariable=self._new_omega, width = pso_width) - self._ent_phip = tk.Entry(self._frame,textvariable=self._new_phip, width = pso_width) - self._ent_phig = tk.Entry(self._frame,textvariable=self._new_phig, width = pso_width) - self._ent_maxiter = tk.Entry(self._frame,textvariable=self._new_maxiter, width = pso_width) - self._ent_minstep = tk.Entry(self._frame,textvariable=self._new_minstep, width = pso_width) - self._ent_minfunc = tk.Entry(self._frame,textvariable=self._new_minfunc, width = pso_width) + self._ent_swarm_size = tk.Entry(self._frame, textvariable=self._new_swarm_size, width=pso_width) + self._ent_omega = tk.Entry(self._frame, textvariable=self._new_omega, width=pso_width) + self._ent_phip = tk.Entry(self._frame, textvariable=self._new_phip, width=pso_width) + self._ent_phig = tk.Entry(self._frame, textvariable=self._new_phig, width=pso_width) + self._ent_maxiter = tk.Entry(self._frame, textvariable=self._new_maxiter, width=pso_width) + self._ent_minstep = tk.Entry(self._frame, textvariable=self._new_minstep, width=pso_width) + self._ent_minfunc = tk.Entry(self._frame, textvariable=self._new_minfunc, width=pso_width) start_x, start_y, dx, dy = 20, 70, 100, 40 self._new_processes = tk.IntVar() self._new_processes.set(max(cpu_count() - 1, 1)) - tk.Label(self._frame, text='Processes\n (CPUs)', font='Verdana 9 bold', bg = 'silver')\ + tk.Label(self._frame, text='Processes\n (CPUs)', font='Verdana 9 bold', bg='silver') \ .place(x=start_x + 12.3 * dx, y=start_y - 0.2 * dy) - tk.Entry(self._frame, textvariable=self._new_processes, width = 12, bg = 'silver')\ - .place(x=start_x + 12.3 * dx, y=start_y + 0.7* dy) + tk.Entry(self._frame, textvariable=self._new_processes, width=12, bg='silver') \ + .place(x=start_x + 12.3 * dx, y=start_y + 0.7 * dy) self._prop_canvas_dim = (500, 450) self._draw_scale = 500 self._canvas_opt = tk.Canvas(self._frame, width=self._prop_canvas_dim[0], height=self._prop_canvas_dim[1], - background='azure',relief='groove', borderwidth=2) - self._canvas_opt.place(x=start_x+10.5*dx, y=start_y+3.5*dy) + background='azure', relief='groove', borderwidth=2) + self._canvas_opt.place(x=start_x + 10.5 * dx, y=start_y + 3.5 * dy) self._select_canvas_dim = (1000, 720) - self._canvas_select = tk.Canvas(self._frame, width=self._select_canvas_dim[0], height=self._select_canvas_dim[1], - background='azure',relief='groove', borderwidth=2) - self._canvas_select.place(x=start_x+0*dx, y=start_y+3.5*dy) - - + self._canvas_select = tk.Canvas(self._frame, width=self._select_canvas_dim[0], + height=self._select_canvas_dim[1], + background='azure', relief='groove', borderwidth=2) + self._canvas_select.place(x=start_x + 0 * dx, y=start_y + 3.5 * dy) # Labels for the pso self._lb_swarm_size = tk.Label(self._frame, text='swarm size') @@ -262,17 +291,17 @@ def __init__(self, master, app=None): tk.Label(self._frame, text='Iteration delta [mm]', font='Verdana 9').place(x=start_x, y=start_y + dy) tk.Label(self._frame, text='Lower bounds [mm]', font='Verdana 9').place(x=start_x, y=start_y + 2 * dy) tk.Label(self._frame, text='Spacing [mm]', font='Verdana 7 bold').place(x=start_x + 1.97 * dx, - y=start_y - 0.6 * dy) + y=start_y - 0.6 * dy) tk.Label(self._frame, text='Plate thk. [mm]', font='Verdana 7 bold').place(x=start_x + 2.97 * dx, - y=start_y - 0.6 * dy) + y=start_y - 0.6 * dy) tk.Label(self._frame, text='Web height [mm]', font='Verdana 7 bold').place(x=start_x + 3.97 * dx, - y=start_y - 0.6 * dy) + y=start_y - 0.6 * dy) tk.Label(self._frame, text='Web thk. [mm]', font='Verdana 7 bold').place(x=start_x + 4.97 * dx, - y=start_y - 0.6 * dy) + y=start_y - 0.6 * dy) tk.Label(self._frame, text='Flange width [mm]', font='Verdana 7 bold').place(x=start_x + 5.97 * dx, - y=start_y - 0.6 * dy) + y=start_y - 0.6 * dy) tk.Label(self._frame, text='Flange thk. [mm]', font='Verdana 7 bold').place(x=start_x + 6.97 * dx, - y=start_y - 0.6 * dy) + y=start_y - 0.6 * dy) tk.Label(self._frame, text='Estimated running time for algorithm: ', font='Verdana 9 bold').place(x=start_x, y=start_y + 2.8 * dy) self._runnig_time_label = tk.Label(self._frame, text='', font='Verdana 9 bold') @@ -331,11 +360,12 @@ def __init__(self, master, app=None): self._new_check_fatigue = tk.BooleanVar() self._new_check_slamming = tk.BooleanVar() self._new_check_local_buckling = tk.BooleanVar() + self._new_check_semi_analytical_buckling = tk.BooleanVar() self._new_check_ml_buckling = tk.BooleanVar() + self._new_check_ml_numeric_buckling = tk.BooleanVar() self._new_harmonizer = tk.BooleanVar() self._keep_spacing = tk.BooleanVar() - self._new_check_sec_mod.set(True) self._new_check_min_pl_thk.set(True) self._new_check_shear_area.set(True) @@ -345,7 +375,9 @@ def __init__(self, master, app=None): self._new_check_local_buckling.set(True) self._new_harmonizer.set(False) self._keep_spacing.set(False) + self._new_check_semi_analytical_buckling.set(False) self._new_check_ml_buckling.set(False) + self._new_check_ml_numeric_buckling.set(False) self._new_swarm_size.set(100) self._new_omega.set(0.5) @@ -375,69 +407,77 @@ def __init__(self, master, app=None): self._new_fl_thk_lower.trace('w', self.update_running_time) self._new_algorithm_random_trials.trace('w', self.update_running_time) self._new_algorithm.trace('w', self.update_running_time) - self._keep_spacing.trace('w',self.trace_keep_spacing_check) + self._keep_spacing.trace('w', self.trace_keep_spacing_check) + self._new_check_semi_analytical_buckling.trace('w', self.update_running_time) self._new_check_ml_buckling.trace('w', self.update_running_time) + self._new_check_ml_numeric_buckling.trace('w', self.update_running_time) self.running_time_per_item = 1.009943181818182e-5 self._runnig_time_label.config(text=str(self.get_running_time())) tk.Label(self._frame, text='Select algorithm type --->', font='Verdana 8 bold').place(x=start_x + dx * 8, - y=start_y + 1 * dy) + y=start_y + 1 * dy) self._ent_algorithm.place(x=start_x + dx * 10, y=start_y + dy) self.algorithm_random_label = tk.Label(self._frame, text='Number of trials') tk.Button(self._frame, text='algorithm information', command=self.algorithm_info, bg='white') \ - .place(x=start_x + dx * 15, y=start_y + dy *-0.5) + .place(x=start_x + dx * 15, y=start_y + dy * -0.5) self.run_button = tk.Button(self._frame, text='RUN OPTIMIZATION!', command=self.run_optimizaion, bg='red', font='Verdana 10', fg='Yellow') self.run_button.place(x=start_x + dx * 8, y=start_y) - self.run_results = tk.Button(self._frame,text='show calculated', command=self.plot_results, bg='white', - font='Verdana 10',fg='black') - self.run_results.place(x=start_x+dx*8, y=start_y+dy*1.5) + self.run_results = tk.Button(self._frame, text='show calculated', command=self.plot_results, bg='white', + font='Verdana 10', fg='black') + self.run_results.place(x=start_x + dx * 8, y=start_y + dy * 1.5) self._opt_actual_running_time.place(x=start_x + dx * 8, y=start_y - dy * 1.5) self.close_and_save = tk.Button(self._frame, text='Return and replace with selected optimized structure', command=self.save_and_close, bg='green', font='Verdana 10 bold', fg='yellow') self.close_and_save.place(x=start_x + dx * 10, y=10) tk.Button(self._frame, text='Open predefined stiffeners example', - command=self.open_example_file, bg='white', font='Verdana 10')\ - .place(x=start_x+dx*15,y=10) - - - - + command=self.open_example_file, bg='white', font='Verdana 10') \ + .place(x=start_x + dx * 15, y=10) start_y, start_x, dy = 530, 100, 35 - tk.Label(self._frame,text='Check for minimum section modulus').place(x=start_x+dx*9.7,y=start_y+4*dy) - tk.Label(self._frame, text='Check for minimum plate thk.').place(x=start_x+dx*9.7,y=start_y+5*dy) - tk.Label(self._frame, text='Check for minimum shear area').place(x=start_x+dx*9.7,y=start_y+6*dy) - tk.Label(self._frame, text='Check for buckling (RP-C201)').place(x=start_x+dx*9.7,y=start_y+7*dy) + tk.Label(self._frame, text='Check for minimum section modulus').place(x=start_x + dx * 9.7, y=start_y + 4 * dy) + tk.Label(self._frame, text='Check for minimum plate thk.').place(x=start_x + dx * 9.7, y=start_y + 5 * dy) + tk.Label(self._frame, text='Check for minimum shear area').place(x=start_x + dx * 9.7, y=start_y + 6 * dy) + tk.Label(self._frame, text='Check for buckling (RP-C201)').place(x=start_x + dx * 9.7, y=start_y + 7 * dy) tk.Label(self._frame, text='Check for fatigue (RP-C203)').place(x=start_x + dx * 9.7, y=start_y + 8 * dy) tk.Label(self._frame, text='Check for bow slamming').place(x=start_x + dx * 9.7, y=start_y + 9 * dy) tk.Label(self._frame, text='Check for local stf. buckling').place(x=start_x + dx * 9.7, y=start_y + 10 * dy) - tk.Label(self._frame, text='Check for buckling (ML-CL)').place(x=start_x + dx * 9.7, y=start_y + 11 * dy) + tk.Label(self._frame, text='Check for buckling (SemiAnalytical S3/U3)').place(x=start_x + dx * 9.7, + y=start_y + 11 * dy) + tk.Label(self._frame, text='Check for buckling (ML-CL deactivated)').place(x=start_x + dx * 9.7, y=start_y + 12 * dy) + tk.Label(self._frame, text='Check for buckling (ML-Numeric)').place(x=start_x + dx * 9.7, + y=start_y + 13 * dy) tk.Label(self._frame, text='Check to harmonize results. Same stiffener and plate dimensions ' - '(defined by largest in opt).', font='Verdana 10 bold')\ + '(defined by largest in opt).', font='Verdana 10 bold') \ .place(x=start_x + dx * +8.5, y=start_y - 10.5 * dy) tk.Label(self._frame, text='Check to skip iterating over spacing (respective line spacing used).', - font='Verdana 10 bold')\ + font='Verdana 10 bold') \ .place(x=start_x + dx * +8.5, y=start_y - 9.8 * dy) - tk.Checkbutton(self._frame,variable=self._new_check_sec_mod).place(x=start_x+dx*12,y=start_y+4*dy) - tk.Checkbutton(self._frame, variable=self._new_check_min_pl_thk).place(x=start_x+dx*12,y=start_y+5*dy) - tk.Checkbutton(self._frame, variable=self._new_check_shear_area).place(x=start_x+dx*12,y=start_y+6*dy) - tk.Checkbutton(self._frame, variable=self._new_check_buckling).place(x=start_x+dx*12,y=start_y+7*dy) + tk.Checkbutton(self._frame, variable=self._new_check_sec_mod).place(x=start_x + dx * 12, y=start_y + 4 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_min_pl_thk).place(x=start_x + dx * 12, y=start_y + 5 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_shear_area).place(x=start_x + dx * 12, y=start_y + 6 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_buckling).place(x=start_x + dx * 12, y=start_y + 7 * dy) tk.Checkbutton(self._frame, variable=self._new_check_fatigue).place(x=start_x + dx * 12, y=start_y + 8 * dy) tk.Checkbutton(self._frame, variable=self._new_check_slamming).place(x=start_x + dx * 12, y=start_y + 9 * dy) tk.Checkbutton(self._frame, variable=self._new_check_local_buckling).place(x=start_x + dx * 12, y=start_y + 10 * dy) - tk.Checkbutton(self._frame, variable=self._new_check_ml_buckling).place(x=start_x + dx * 12, - y=start_y + 11 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_semi_analytical_buckling).place(x=start_x + dx * 12, + y=start_y + 11 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_ml_buckling, state='disabled').place( + x=start_x + dx * 12, + y=start_y + 12 * dy, + ) + tk.Checkbutton(self._frame, variable=self._new_check_ml_numeric_buckling).place(x=start_x + dx * 12, + y=start_y + 13 * dy) tk.Checkbutton(self._frame, variable=self._new_harmonizer).place(x=start_x + dx * 8, y=start_y - 10.5 * dy) tk.Checkbutton(self._frame, variable=self._keep_spacing).place(x=start_x + dx * +8, y=start_y - 9.8 * dy) self._toggle_btn = tk.Button(self._frame, text="Iterate predefiened stiffeners", relief="raised", - command=self.toggle, bg = 'salmon') + command=self.toggle, bg='salmon') - self._toggle_btn.place(x=start_x+dx*9, y=start_y - dy * 13) + self._toggle_btn.place(x=start_x + dx * 9, y=start_y - dy * 13) self._toggle_object, self._filez = None, None # Stress scaling @@ -446,13 +486,13 @@ def __init__(self, master, app=None): self._new_fdwn = tk.DoubleVar() self._new_fdwn.set(1) - tk.Label(self._frame, text='Factor when scaling stresses up, fup')\ + tk.Label(self._frame, text='Factor when scaling stresses up, fup') \ .place(x=start_x + dx * 13, y=start_y + 5 * dy) - ent_fup = tk.Entry(self._frame, textvariable=self._new_fup, width = 10) + ent_fup = tk.Entry(self._frame, textvariable=self._new_fup, width=10) ent_fup.place(x=start_x + dx * 15.5, y=start_y + 5 * dy) - tk.Label(self._frame, text='Factor when scaling stresses up, fdown')\ + tk.Label(self._frame, text='Factor when scaling stresses up, fdown') \ .place(x=start_x + dx * 13, y=start_y + 6 * dy) - ent_fdwn = tk.Entry(self._frame, textvariable=self._new_fdwn, width = 10) + ent_fdwn = tk.Entry(self._frame, textvariable=self._new_fdwn, width=10) ent_fdwn.place(x=start_x + dx * 15.5, y=start_y + 6 * dy) self.draw_properties() @@ -460,10 +500,9 @@ def __init__(self, master, app=None): # ----------------------------------END OF OPTIMIZE SINGLE COPY----------------------------------------------- self.progress_count = tk.IntVar() self.progress_count.set(0) - self.progress_bar = Progressbar(self._frame, orient="horizontal",length=200, mode="determinate", + self.progress_bar = Progressbar(self._frame, orient="horizontal", length=200, mode="determinate", variable=self.progress_count) - self.progress_bar.place(x=start_x+dx*10.5,y=start_y-dy*11.5) - + self.progress_bar.place(x=start_x + dx * 10.5, y=start_y - dy * 11.5) self.controls() self.draw_select_canvas() @@ -475,7 +514,6 @@ def trace_keep_spacing_check(self, *args): self._ent_delta_spacing.configure({"background": "red"}) self._ent_spacing_upper.configure({"background": "red"}) - def selected_algorithm(self, event): ''' Action when selecting an algorithm in the optionm menu. @@ -526,23 +564,23 @@ def selected_algorithm(self, event): self._ent_random_trials.place_forget() start_x = 150 - self._lb_swarm_size.place(x=start_x + dx*11 , y=start_y - 1 * dy) - self._lb_omega.place(x=start_x + dx*11 , y=start_y - 0 * dy) - self._lb_phip.place(x=start_x + dx*11 , y=start_y + 1 * dy) - self._lb_phig.place(x=start_x + dx*11 , y=start_y + 2 * dy) + self._lb_swarm_size.place(x=start_x + dx * 11, y=start_y - 1 * dy) + self._lb_omega.place(x=start_x + dx * 11, y=start_y - 0 * dy) + self._lb_phip.place(x=start_x + dx * 11, y=start_y + 1 * dy) + self._lb_phig.place(x=start_x + dx * 11, y=start_y + 2 * dy) - self._lb_maxiter.place(x=start_x + dx*14 , y=start_y - 1 * dy) - self._lb_minstep.place(x=start_x + dx*14, y=start_y + 0 * dy) - self._lb_minfunc.place(x=start_x + dx*14, y=start_y + 1 * dy) + self._lb_maxiter.place(x=start_x + dx * 14, y=start_y - 1 * dy) + self._lb_minstep.place(x=start_x + dx * 14, y=start_y + 0 * dy) + self._lb_minfunc.place(x=start_x + dx * 14, y=start_y + 1 * dy) - self._ent_swarm_size.place(x=start_x + dx*12 , y=start_y - 1 * dy) - self._ent_omega.place(x=start_x + dx*12 , y=start_y - 0 * dy) - self._ent_phip.place(x=start_x + dx*12 , y=start_y + 1 * dy) - self._ent_phig.place(x=start_x + dx*12 , y=start_y + 2 * dy) + self._ent_swarm_size.place(x=start_x + dx * 12, y=start_y - 1 * dy) + self._ent_omega.place(x=start_x + dx * 12, y=start_y - 0 * dy) + self._ent_phip.place(x=start_x + dx * 12, y=start_y + 1 * dy) + self._ent_phig.place(x=start_x + dx * 12, y=start_y + 2 * dy) - self._ent_maxiter.place(x=start_x + dx*15 , y=start_y - 1 * dy) - self._ent_minstep.place(x=start_x + dx*15, y=start_y + 0 * dy) - self._ent_minfunc.place(x=start_x + dx*15, y=start_y + 1 * dy) + self._ent_maxiter.place(x=start_x + dx * 15, y=start_y - 1 * dy) + self._ent_minstep.place(x=start_x + dx * 15, y=start_y + 0 * dy) + self._ent_minfunc.place(x=start_x + dx * 15, y=start_y + 1 * dy) def get_pressure_input(self, line): if __name__ == '__main__': @@ -553,7 +591,7 @@ def get_pressure_input(self, line): else: lateral_press = self.app.get_highest_pressure(line)['normal'] / 1e6 - fat_obj = self.app._line_to_struc[line][2] + fat_obj = line_structure.fatigue(self.app._line_to_struc[line]) if fat_obj is not None: try: fat_press = self.app.get_fatigue_pressures(line, fat_obj.get_accelerations()) @@ -583,32 +621,37 @@ def run_optimizaion(self): Function when pressing the optimization botton inside this window. :return: ''' - self.run_button.config(bg = 'white') + self.run_button.config(bg='white') self._opt_results = {} t_start = time.time() self.progress_bar.config(maximum=len(self._active_lines)) self._opt_actual_running_time.config(text='') - + contraints = (self._new_check_sec_mod.get(), self._new_check_min_pl_thk.get(), self._new_check_shear_area.get(), self._new_check_buckling.get(), self._new_check_fatigue.get(), self._new_check_slamming.get(), - self._new_check_local_buckling.get(), False, self._new_check_ml_buckling.get(), False) - - self.pso_parameters = (self._new_swarm_size.get(),self._new_omega.get(),self._new_phip.get(), - self._new_phig.get(),self._new_maxiter.get(),self._new_minstep.get(), + self._new_check_local_buckling.get(), self._new_check_semi_analytical_buckling.get(), + self._new_check_ml_buckling.get(), + self._new_check_ml_numeric_buckling.get()) + + self.pso_parameters = (self._new_swarm_size.get(), self._new_omega.get(), self._new_phip.get(), + self._new_phig.get(), self._new_maxiter.get(), self._new_minstep.get(), self._new_minfunc.get()) max_min_span = None - + self.progress_count.set(0) counter = 0 found_files = self._filez + selected_mat_fac = self._get_selected_material_factor() + for line in self._active_lines: - init_obj = self._line_to_struc[line][0] + init_obj = self._line_structure(line) + self._apply_material_factor_to_structure(init_obj, selected_mat_fac) if __name__ == '__main__': - lateral_press = 200 #for testing + lateral_press = 200 # for testing fat_obj = test.get_fatigue_object() fat_press = test.get_fatigue_pressures() slamming_pressure = test.get_slamming_pressure() @@ -625,27 +668,28 @@ def run_optimizaion(self): fat_obj = input_pressures['fatigue object'] if self._toggle_btn.config('relief')[-1] == 'sunken': - found_files, predefined_stiffener_iter = self.toggle(found_files=found_files, obj = init_obj, + found_files, predefined_stiffener_iter = self.toggle(found_files=found_files, obj=init_obj, iterating=True) else: predefined_stiffener_iter = None self._opt_results[line] = list(op.run_optmizataion(init_obj, self.get_lower_bounds(init_obj), - self.get_upper_bounds(init_obj), - lateral_press,self.get_deltas(), - algorithm=self._new_algorithm.get(), - trials=self._new_algorithm_random_trials.get(), - side=init_obj.Plate.get_side(), - const_chk=contraints, - pso_options=self.pso_parameters, - fatigue_obj=fat_obj, - fat_press_ext_int=fat_press, - slamming_press=slamming_pressure, - predefined_stiffener_iter = predefined_stiffener_iter, - processes=self._new_processes.get(), - min_max_span=max_min_span, use_weight_filter=False, - fdwn = self._new_fdwn.get(), fup = self._new_fdwn.get(), - ml_algo=self._ML_buckling)) + self.get_upper_bounds(init_obj), + lateral_press, self.get_deltas(), + algorithm=self._new_algorithm.get(), + trials=self._new_algorithm_random_trials.get(), + side=init_obj.Plate.get_side(), + const_chk=contraints, + pso_options=self.pso_parameters, + fatigue_obj=fat_obj, + fat_press_ext_int=fat_press, + slamming_press=slamming_pressure, + predefined_stiffener_iter=predefined_stiffener_iter, + processes=self._new_processes.get(), + min_max_span=max_min_span, use_weight_filter=False, + fdwn=self._new_fdwn.get(), fup=self._new_fup.get(), + ml_algo=self._get_selected_ml_algo(), + material_factor=selected_mat_fac)) self._harmonizer_data[line] = {} counter += 1 self.progress_count.set(counter) @@ -653,7 +697,7 @@ def run_optimizaion(self): if self._opt_results[line] != None: self._opt_actual_running_time.config(text='Accumulated running time: \n' - + str(time.time() - t_start) + ' sec') + + str(time.time() - t_start) + ' sec') # print('Runned', line, 'OK') # else: # print('Runned', line, 'NOT OK - no results') @@ -683,23 +727,29 @@ def opt_harmonizer_historic(self): # except TypeError: # [print(val) for val in fail_ok[2]] - all_ok_checks.append(tuple([round(val,10) for val in fail_ok[2][0:6]])) + all_ok_checks.append(tuple([round(val, 10) for val in fail_ok[2][0:6]])) all_ok_checks = set(all_ok_checks) # make iterator for multiprocessing iterator = list() - to_check = (self._new_check_sec_mod.get(), self._new_check_min_pl_thk.get(), + to_check = [self._new_check_sec_mod.get(), self._new_check_min_pl_thk.get(), self._new_check_shear_area.get(), self._new_check_buckling.get(), self._new_check_fatigue.get(), self._new_check_slamming.get(), - self._new_check_local_buckling.get(), False, self._new_check_ml_buckling.get(), False) + self._new_check_local_buckling.get(), self._new_check_semi_analytical_buckling.get(), + False, + self._new_check_ml_numeric_buckling.get()] iter_run_info = dict() + selected_mat_fac = self._get_selected_material_factor() + for slave_line in self._opt_results.keys(): input_pressures = self.get_pressure_input(slave_line) iter_run_info[slave_line] = {'lateral pressure': input_pressures['lateral pressure'], 'fatigue pressure': input_pressures['fatigue pressure'], - 'fatigue object':input_pressures['fatigue object'], - 'slamming pressure':input_pressures['slamming pressure'], - 'chk_calc_obj': self._opt_results[slave_line][0], 'ML-CL': [0,0], + 'fatigue object': input_pressures['fatigue object'], + 'slamming pressure': input_pressures['slamming pressure'], + 'chk_calc_obj': self._opt_results[slave_line][0], + 'SemiAnalytical': [float('inf'), float('inf'), 0, 0.87], + 'ML-Numeric': [float('inf'), float('inf'), 0], 'fup': self._new_fup.get(), 'fdwn': self._new_fdwn.get()} iter_run_info['lines'] = list(self._opt_results.keys()) iter_run_info['checks'] = to_check @@ -708,100 +758,69 @@ def opt_harmonizer_historic(self): for x_check in all_ok_checks: iterator.append({'x': x_check, 'info': iter_run_info}) - if to_check[8]: - # Do ML-CL checks + if to_check[7] or to_check[9]: + # Do SemiAnalytical or ML-Numeric checks for harmonized candidates to_run = list() + to_run_owner = list() + for x_and_info in iterator: for slave_line in x_and_info['info']['lines']: iter_run_info = x_and_info['info'] lateral_press = iter_run_info[slave_line]['lateral pressure'] - fat_press = iter_run_info[slave_line]['fatigue pressure'] fat_obj = iter_run_info[slave_line]['fatigue object'] - slamming_pressure = iter_run_info[slave_line]['slamming pressure'] chk_calc_obj = iter_run_info[slave_line]['chk_calc_obj'] master_x = list(x_and_info['x']) + if iter_run_info['keep spacing']: - x = [chk_calc_obj.Plate.get_s()] + master_x[1:] + [chk_calc_obj.Plate.span, chk_calc_obj.Plate.girder_lg] + x = [chk_calc_obj.Plate.get_s()] + master_x[1:] + [ + chk_calc_obj.Plate.span, + chk_calc_obj.Plate.girder_lg, + ] else: x = master_x + [chk_calc_obj.Plate.span, chk_calc_obj.Plate.girder_lg] + fdwn = self._new_fdwn.get() - fup = self._new_fdwn.get() - calc_object = op.create_new_calc_obj(chk_calc_obj, x, fat_obj.get_fatigue_properties(), fdwn=fdwn, - fup=fup) - - calc_object_stf = op.create_new_calc_obj(chk_calc_obj.Stiffener, x, - fat_obj.get_fatigue_properties(), fdwn=fdwn, fup=fup) - calc_object_pl = op.create_new_calc_obj(chk_calc_obj.Plate, x, fat_obj.get_fatigue_properties(), - fdwn=fdwn, fup=fup) - calc_object = [calc.AllStructure(Plate=calc_object_pl[0], Stiffener=calc_object_stf[0], Girder=None, - main_dict=chk_calc_obj.get_main_properties()['main dict']), - calc_object_pl[1]] + fup = self._new_fup.get() + + calc_object_stf = op.create_new_calc_obj( + chk_calc_obj.Stiffener, + x, + None if fat_obj is None else fat_obj.get_fatigue_properties(), + fdwn=fdwn, + fup=fup, + ) + calc_object_pl = op.create_new_calc_obj( + chk_calc_obj.Plate, + x, + None if fat_obj is None else fat_obj.get_fatigue_properties(), + fdwn=fdwn, + fup=fup, + ) + calc_object = [ + calc.AllStructure( + Plate=calc_object_pl[0], + Stiffener=None if chk_calc_obj.Stiffener is None else calc_object_stf[0], + Girder=None, + main_dict=chk_calc_obj.get_main_properties()['main dict'], + ), + calc_object_pl[1], + ] calc_object[0].lat_press = lateral_press + to_run_owner.append((x_and_info, slave_line)) to_run.append((calc_object, x, lateral_press)) - # ML-CL to be used. - sp_int, sp_gl_gt, up_int, up_gl_gt, \ - sp_int_idx, sp_gl_gt_idx, up_int_idx, up_gl_gt_idx = \ - list(), list(), list(), list(), list(), list(), list(), list() - - # Create iterator - idx_count = 0 - for calc_object, x, lat_press in to_run: - idx_count += 1 + if to_check[7]: + sort_again = self._predict_semi_analytical_for_to_run(to_run) + result_key = 'SemiAnalytical' + else: + sort_again = self._predict_ml_numeric_for_to_run(to_run) + result_key = 'ML-Numeric' - if calc_object[0].Plate.get_puls_sp_or_up() == 'UP': - if calc_object[0].Plate.get_puls_boundary() == 'Int': - up_int.append(calc_object[0].Stiffener.get_buckling_ml_input(lat_press, alone=False)) - up_int_idx.append(idx_count-1) - else: - up_gl_gt_idx.append(idx_count-1) - else: - if calc_object[0].Stiffener.get_puls_boundary() == 'Int': - sp_int.append(calc_object[0].Stiffener.get_buckling_ml_input(lat_press, alone=False)) - sp_int_idx.append(idx_count-1) - else: - sp_gl_gt.append(calc_object[0].Stiffener.get_buckling_ml_input(lat_press, alone=False)) - sp_gl_gt_idx.append(idx_count-1) - - # Predict - sort_again = np.zeros([len(to_run), 2]) - - if len(sp_int) != 0: - sp_int_res = [self._ML_buckling['cl SP buc int predictor'].predict(self._ML_buckling['cl SP buc int scaler'] - .transform(sp_int)), - self._ML_buckling['cl SP ult int predictor'].predict(self._ML_buckling['cl SP buc int scaler'] - .transform(sp_int))] - for idx, res_buc, res_ult in zip(sp_int_idx, sp_int_res[0], sp_int_res[1]): - sort_again[idx] = [res_buc, res_ult] - - if len(sp_gl_gt) != 0: - sp_gl_gt_res = [self._ML_buckling['cl SP buc GLGT predictor'].predict(self._ML_buckling['cl SP buc GLGT scaler'] - .transform(sp_gl_gt)), - self._ML_buckling['cl SP buc GLGT predictor'].predict(self._ML_buckling['cl SP buc GLGT scaler'] - .transform(sp_gl_gt))] - for idx, res_buc, res_ult in zip(sp_gl_gt_idx, sp_gl_gt_res[0], sp_gl_gt_res[1]): - sort_again[idx] = [res_buc, res_ult] - if len(up_int) != 0: - up_int_res = [self._ML_buckling['cl UP buc int predictor'].predict(self._ML_buckling['cl UP buc int scaler'] - .transform(up_int)), - self._ML_buckling['cl UP ult int predictor'].predict(self._ML_buckling['cl UP buc int scaler'] - .transform(up_int))] - for idx, res_buc, res_ult in zip(up_int_idx, up_int_res[0], up_int_res[1]): - sort_again[idx] = [res_buc, res_ult] - if len(up_gl_gt) != 0: - up_gl_gt_res = [self._ML_buckling['cl UP buc GLGT predictor'].predict(self._ML_buckling['cl UP buc GLGT scaler'] - .transform(up_gl_gt)), - self._ML_buckling['cl UP buc GLGT predictor'].predict(self._ML_buckling['cl UP buc GLGT scaler'] - .transform(up_gl_gt))] - for idx, res_buc, res_ult in zip(up_gl_gt_idx, up_gl_gt_res[0], up_gl_gt_res[1]): - sort_again[idx] = [res_buc, res_ult] - - for idx, x_and_info in enumerate(iterator): - for slave_line in x_and_info['info']['lines']: - iterator[idx]['info'][slave_line]['ML-CL'] = sort_again[idx] + for idx, (x_and_info, slave_line) in enumerate(to_run_owner): + x_and_info['info'][slave_line][result_key] = sort_again[idx] - # END ML-CL calc + # END ML calc processes = max(cpu_count() - 1, 1) with Pool(processes) as my_process: @@ -812,36 +831,32 @@ def opt_harmonizer_historic(self): if res is not None: after_multirun_check_ok.append(res) - lowest_area, lowest_x = float('inf'), None + lowest_area, lowest_x = float('inf'), None for ok_chkd in set(after_multirun_check_ok): - if sum(op.get_field_tot_area(ok_chkd )) < lowest_area: + if sum(op.get_field_tot_area(ok_chkd)) < lowest_area: lowest_area = sum(op.get_field_tot_area(ok_chkd)) lowest_x = ok_chkd if lowest_area != float('inf'): for line in self._opt_results.keys(): + line_structure_obj = self._line_structure(line) if self._keep_spacing: - this_x = [self._line_to_struc[line][0].Plate.get_s()] + list(lowest_x)[1:] + \ - [self._line_to_struc[line][0].Plate.span, self._line_to_struc[line][0].Plate.girder_lg] + this_x = [line_structure_obj.Plate.get_s()] + list(lowest_x)[1:] + \ + [line_structure_obj.Plate.span, line_structure_obj.Plate.girder_lg] else: - this_x = list(lowest_x) + [self._line_to_struc[line][0].Plate.span, - self._line_to_struc[line][0].Plate.girder_lg] + this_x = list(lowest_x) + [line_structure_obj.Plate.span, + line_structure_obj.Plate.girder_lg] - calc_object_stf = op.create_new_calc_obj(self._line_to_struc[line][0].Plate, this_x, - fat_obj.get_fatigue_properties(), fdwn=fdwn, fup=fup) - calc_object_pl = op.create_new_calc_obj(self._line_to_struc[line][0].Stiffener, this_x, + calc_object_stf = op.create_new_calc_obj(line_structure_obj.Plate, this_x, fat_obj.get_fatigue_properties(), fdwn=fdwn, fup=fup) - self._opt_results[line][0] = [calc.AllStructure(Plate=calc_object_pl[0], Stiffener=calc_object_stf[0], Girder=None, - main_dict=chk_calc_obj.get_main_properties()['main dict']), - calc_object_pl[1]] - - if self._line_to_struc[line][2] != None: - self._opt_results[line][2] = opt.create_new_calc_obj(init_obj= self._line_to_struc[line][1], - x = this_x, - fat_dict=self._line_to_struc[line] - [2].get_fatigue_properties())[1] - else: - self._line_to_struc[line][2] = None + calc_object_pl = op.create_new_calc_obj(line_structure_obj.Stiffener, this_x, + fat_obj.get_fatigue_properties(), fdwn=fdwn, fup=fup) + self._opt_results[line][0] = [ + calc.AllStructure(Plate=calc_object_pl[0], Stiffener=calc_object_stf[0], Girder=None, + main_dict=chk_calc_obj.get_main_properties()['main dict']), + calc_object_pl[1]] + + self._update_harmonized_fatigue_result(line, this_x) return True else: for line in self._opt_results.keys(): @@ -856,17 +871,17 @@ def opt_harmonizer(self): ''' # Find highest section modulus. - harm_res= {} + harm_res = {} chk = (self._new_check_sec_mod.get(), self._new_check_min_pl_thk.get(), - self._new_check_shear_area.get(), self._new_check_buckling.get(), - self._new_check_fatigue.get(), self._new_check_slamming.get(), - self._new_check_local_buckling.get()) + self._new_check_shear_area.get(), self._new_check_buckling.get(), + self._new_check_fatigue.get(), self._new_check_slamming.get(), + self._new_check_local_buckling.get()) for master_line in self._opt_results.keys(): master_obj = self._opt_results[master_line][0] master_x = [master_obj.Plate.get_s(), master_obj.Plate.get_pl_thk(), master_obj.Stiffener.get_web_h(), master_obj.Stiffener.get_web_thk(), master_obj.Stiffener.get_fl_w(), master_obj.Stiffener.get_fl_thk(), - master_obj.Plate.span,master_obj.Plate.girder_lg] + master_obj.Plate.span, master_obj.Plate.girder_lg] harm_res[master_line] = [] for slave_line in self._opt_results.keys(): input_pressures = self.get_pressure_input(slave_line) @@ -877,31 +892,33 @@ def opt_harmonizer(self): chk_calc_obj = self._opt_results[slave_line][1] chk_result = list(op.run_optmizataion(chk_calc_obj, - master_x[0:6]+[chk_calc_obj.Plate.span, - chk_calc_obj.Plate.girder_lg], - master_x[0:6]+[chk_calc_obj.Plate.span, - chk_calc_obj.Plate.girder_lg], - lateral_press,self.get_deltas(), + master_x[0:6] + [chk_calc_obj.Plate.span, + chk_calc_obj.Plate.girder_lg], + master_x[0:6] + [chk_calc_obj.Plate.span, + chk_calc_obj.Plate.girder_lg], + lateral_press, self.get_deltas(), algorithm=self._new_algorithm.get(), trials=self._new_algorithm_random_trials.get(), side=chk_calc_obj.Plate.get_side(), const_chk=chk, - pso_options=self.pso_parameters,fatigue_obj=fat_obj, + pso_options=self.pso_parameters, fatigue_obj=fat_obj, fat_press_ext_int=fat_press, slamming_press=slamming_pressure, - predefined_stiffener_iter = None, + predefined_stiffener_iter=None, processes=self._new_processes.get(), min_max_span=None, use_weight_filter=True, - fdwn = self._new_fdwn.get(), fup = self._new_fdwn.get()))[0:4] + fdwn=self._new_fdwn.get(), fup=self._new_fup.get()))[0:4] print('Master:', master_line, 'Slave', slave_line, 'Check', chk_result[-1]) harm_res[master_line].append(chk_result) - harmonized_area, harmonized_line =float('inf'), None + harmonized_area, harmonized_line = float('inf'), None for master_line, all_slave_res in harm_res.items(): if all([slave_line_res[-1] for slave_line_res in all_slave_res]): master_obj = self._opt_results[master_line][0] master_area = sum(op.get_field_tot_area([master_obj.Plate.get_s(), master_obj.Plate.get_pl_thk(), - master_obj.Stiffener.get_web_h(), master_obj.Stiffener.get_web_thk(), - master_obj.Stiffener.get_fl_w(), master_obj.Stiffener.get_fl_thk(), - master_obj.Plate.span,master_obj.Plate.girder_lg])) + master_obj.Stiffener.get_web_h(), + master_obj.Stiffener.get_web_thk(), + master_obj.Stiffener.get_fl_w(), + master_obj.Stiffener.get_fl_thk(), + master_obj.Plate.span, master_obj.Plate.girder_lg])) if master_area < harmonized_area: harmonized_area = master_area harmonized_line = master_line @@ -911,21 +928,18 @@ def opt_harmonizer(self): harmonized_x_stf = self._opt_results[harmonized_line][0].Stiffener.get_tuple() harmonized_x = harmonized_x_pl[0:2] + harmonized_x_stf[2:] for line in self._opt_results.keys(): - self._opt_results[line][0] = opt.create_new_structure_obj(self._line_to_struc[line][0], harmonized_x) + line_structure_obj = self._line_structure(line) + self._opt_results[line][0] = opt.create_new_structure_obj(line_structure_obj, harmonized_x) - calc_object_stf = op.create_new_calc_obj(self._line_to_struc[line][0].Plate, harmonized_x) - calc_object_pl = op.create_new_calc_obj(self._line_to_struc[line][0].Stiffener,harmonized_x) + calc_object_stf = op.create_new_calc_obj(line_structure_obj.Plate, harmonized_x) + calc_object_pl = op.create_new_calc_obj(line_structure_obj.Stiffener, harmonized_x) self._opt_results[line][0] = [calc.AllStructure(Plate=calc_object_pl[0], Stiffener=calc_object_stf[0], - Girder=None, main_dict=chk_calc_obj.get_main_properties()['main dict']), - calc_object_pl[1]] - - if self._line_to_struc[line][2] != None: - self._opt_results[line][2] = opt.create_new_calc_obj(init_obj= self._line_to_struc[line][1], - x = harmonized_x, - fat_dict=self._line_to_struc[line] - [2].get_fatigue_properties())[1] - else: - self._line_to_struc[line][2] = None + Girder=None, + main_dict=chk_calc_obj.get_main_properties()[ + 'main dict']), + calc_object_pl[1]] + + self._update_harmonized_fatigue_result(line, harmonized_x) return True else: for line in self._opt_results.keys(): @@ -943,18 +957,20 @@ def get_running_time(self): number_of_combinations = \ max((self._new_spacing_upper.get() - self._new_spacing_lower.get()) / self._new_delta_spacing.get(), 1) * \ - max((self._new_pl_thk_upper.get() - self._new_pl_thk_lower.get()) / self._new_delta_pl_thk.get(), 1) * \ + max((self._new_pl_thk_upper.get() - self._new_pl_thk_lower.get()) / self._new_delta_pl_thk.get(), + 1) * \ max((self._new_web_h_upper.get() - self._new_web_h_lower.get()) / self._new_delta_web_h.get(), 1) * \ max((self._new_web_thk_upper.get() - self._new_web_thk_lower.get()) / self._new_delta_web_thk.get(), 1) * \ max((self._new_fl_w_upper.get() - self._new_fl_w_lower.get()) / self._new_delta_fl_w.get(), 1) * \ max((self._new_fl_thk_upper.get() - self._new_fl_thk_lower.get()) / self._new_delta_fl_thk.get(), 1) - return int(number_of_combinations * self.running_time_per_item)*len(self._active_lines) + return int(number_of_combinations * self.running_time_per_item) * len(self._active_lines) except TclError: return 0 else: try: - return int(self._new_algorithm_random_trials.get() * self.running_time_per_item)*len(self._active_lines) + return int(self._new_algorithm_random_trials.get() * self.running_time_per_item) * len( + self._active_lines) except TclError: return 0 @@ -967,26 +983,318 @@ def get_deltas(self): float(self._new_delta_web_h.get()) / 1000, float(self._new_delta_web_thk.get()) / 1000, float(self._new_delta_fl_w.get()) / 1000, float(self._new_delta_fl_thk.get()) / 1000]) + def _get_selected_material_factor(self): + """ + Return selected material factor from the main application when available. + Fallback is the material factor on the first selected line, and then 1.15. + """ + try: + return float(self.app._new_material_factor.get()) + except Exception: + pass + + try: + if len(self._active_lines) > 0: + first_line = self._active_lines[0] + return float(self._line_structure(first_line).Plate.mat_factor) + except Exception: + pass + + return 1.15 + + def _apply_material_factor_to_structure(self, obj, mat_fac): + """Apply selected material factor to Plate/Stiffener/Girder for optimizer checks.""" + try: + mat_fac = float(mat_fac) + except Exception: + return obj + + for attr_name in ('Plate', 'Stiffener', 'Girder'): + try: + part = getattr(obj, attr_name) + except Exception: + part = None + if part is not None: + try: + part.mat_factor = mat_fac + except Exception: + pass + + return obj + + def _get_selected_ml_algo(self): + """ + Return ML model dictionary for the selected material factor. + + Main app usually stores: + self._ML_buckling[1.10][key] + self._ML_buckling[1.15][key] + + Standalone/test mode may store a flat dictionary: + self._ML_buckling[key] + """ + mat_fac = self._get_selected_material_factor() + + try: + if mat_fac in self._ML_buckling: + return self._ML_buckling[mat_fac] + except Exception: + pass + + return self._ML_buckling + + def _get_ml_input_for_calc_object(self, calc_object, lat_press): + """ + Return the correct ML input row for SP/UP candidate objects. + """ + if calc_object[0].Plate.get_puls_sp_or_up() == 'UP' or calc_object[0].Stiffener is None: + return calc_object[0].Plate.get_buckling_ml_input(lat_press, alone=False) + + return calc_object[0].Stiffener.get_buckling_ml_input(lat_press, alone=False) + + def _predict_ml_cl_for_to_run(self, to_run): + """ + Predict ML-CL class results for harmonizer candidate objects. + + ML-CL is deactivated; this method is retained only for compatibility + with older internal call sites. + + Returns + ------- + np.ndarray + shape = (len(to_run), 2) + columns = [buckling_class, ultimate_class] + """ + ml_algo = self._get_selected_ml_algo() + + sp_int, sp_gl_gt, up_int, up_gl_gt = list(), list(), list(), list() + sp_int_idx, sp_gl_gt_idx, up_int_idx, up_gl_gt_idx = list(), list(), list(), list() + + for idx, (calc_object, x, lat_press) in enumerate(to_run): + ml_input = self._get_ml_input_for_calc_object(calc_object, lat_press) + + if calc_object[0].Plate.get_puls_sp_or_up() == 'UP': + if calc_object[0].Plate.get_puls_boundary() == 'Int': + up_int.append(ml_input) + up_int_idx.append(idx) + else: + up_gl_gt.append(ml_input) + up_gl_gt_idx.append(idx) + else: + if calc_object[0].Stiffener.get_puls_boundary() == 'Int': + sp_int.append(ml_input) + sp_int_idx.append(idx) + else: + sp_gl_gt.append(ml_input) + sp_gl_gt_idx.append(idx) + + sort_again = np.zeros([len(to_run), 2]) + + if len(sp_int) != 0: + x_buc = ml_algo['cl SP buc int scaler'].transform(sp_int) + x_ult = ml_algo['cl SP ult int scaler'].transform(sp_int) + sp_int_res = [ + ml_algo['cl SP buc int predictor'].predict(x_buc), + ml_algo['cl SP ult int predictor'].predict(x_ult), + ] + for idx, res_buc, res_ult in zip(sp_int_idx, sp_int_res[0], sp_int_res[1]): + sort_again[idx] = [res_buc, res_ult] + + if len(sp_gl_gt) != 0: + x_buc = ml_algo['cl SP buc GLGT scaler'].transform(sp_gl_gt) + x_ult = ml_algo['cl SP ult GLGT scaler'].transform(sp_gl_gt) + sp_gl_gt_res = [ + ml_algo['cl SP buc GLGT predictor'].predict(x_buc), + ml_algo['cl SP ult GLGT predictor'].predict(x_ult), + ] + for idx, res_buc, res_ult in zip(sp_gl_gt_idx, sp_gl_gt_res[0], sp_gl_gt_res[1]): + sort_again[idx] = [res_buc, res_ult] + + if len(up_int) != 0: + x_buc = ml_algo['cl UP buc int scaler'].transform(up_int) + x_ult = ml_algo['cl UP ult int scaler'].transform(up_int) + up_int_res = [ + ml_algo['cl UP buc int predictor'].predict(x_buc), + ml_algo['cl UP ult int predictor'].predict(x_ult), + ] + for idx, res_buc, res_ult in zip(up_int_idx, up_int_res[0], up_int_res[1]): + sort_again[idx] = [res_buc, res_ult] + + if len(up_gl_gt) != 0: + x_buc = ml_algo['cl UP buc GLGT scaler'].transform(up_gl_gt) + x_ult = ml_algo['cl UP ult GLGT scaler'].transform(up_gl_gt) + up_gl_gt_res = [ + ml_algo['cl UP buc GLGT predictor'].predict(x_buc), + ml_algo['cl UP ult GLGT predictor'].predict(x_ult), + ] + for idx, res_buc, res_ult in zip(up_gl_gt_idx, up_gl_gt_res[0], up_gl_gt_res[1]): + sort_again[idx] = [res_buc, res_ult] + + return sort_again + + def _predict_semi_analytical_for_to_run(self, to_run): + """ + Predict built-in SemiAnalytical UF results for harmonizer candidate objects. + + Returns columns [buckling_uf, ultimate_uf, valid_prediction, acceptance]. + U3 will reuse this GUI slot when the unstiffened solver branch is added. + """ + sort_again = np.full([len(to_run), 4], float('inf'), dtype=float) + sort_again[:, 2] = 0 + sort_again[:, 3] = 0.87 + + for idx, (calc_object, x, lat_press) in enumerate(to_run): + sort_again[idx, 0:3] = op._predict_semi_analytical_uf(calc_object, lat_press) + + return sort_again + + def _predict_ml_numeric_for_to_run(self, to_run): + """ + Predict ML-Numeric UF results for harmonizer candidate objects. + + The numeric regressor predicts raw UF. The optimization check expects + material-factored UF, so this method applies: + UF = predicted_UF * material_factor + + Returns + ------- + np.ndarray + shape = (len(to_run), 3) + columns = [buckling_uf, ultimate_uf, valid_prediction] + """ + ml_algo = self._get_selected_ml_algo() + mat_fac = self._get_selected_material_factor() + + groups = { + 'sp_int': {'x': [], 'idx': [], 'prefix': 'num SP int'}, + 'sp_glgt': {'x': [], 'idx': [], 'prefix': 'num SP GLGT'}, + 'up_int': {'x': [], 'idx': [], 'prefix': 'num UP int'}, + 'up_glgt': {'x': [], 'idx': [], 'prefix': 'num UP GLGT'}, + } + + for idx, (calc_object, x, lat_press) in enumerate(to_run): + ml_input = self._get_ml_input_for_calc_object(calc_object, lat_press) + + if calc_object[0].Plate.get_puls_sp_or_up() == 'UP': + if calc_object[0].Plate.get_puls_boundary() == 'Int': + key = 'up_int' + else: + key = 'up_glgt' + else: + if calc_object[0].Stiffener.get_puls_boundary() == 'Int': + key = 'sp_int' + else: + key = 'sp_glgt' + + groups[key]['x'].append(ml_input) + groups[key]['idx'].append(idx) + + sort_again = np.zeros([len(to_run), 3]) + sort_again[:, 0] = float('inf') + sort_again[:, 1] = float('inf') + sort_again[:, 2] = 0 + + for group in groups.values(): + if len(group['x']) == 0: + continue + + prefix = group['prefix'] + + valid_predictor_key = f'{prefix} validity predictor' + valid_xscaler_key = f'{prefix} validity xscaler' + reg_predictor_key = f'{prefix} UF reg predictor' + reg_xscaler_key = f'{prefix} UF reg xscaler' + reg_yscaler_key = f'{prefix} UF reg yscaler' + + required_keys = [ + valid_predictor_key, + valid_xscaler_key, + reg_predictor_key, + reg_xscaler_key, + reg_yscaler_key, + ] + + if any(key not in ml_algo or ml_algo[key] is None for key in required_keys): + continue + + x_valid = ml_algo[valid_xscaler_key].transform(group['x']) + valid_pred = ml_algo[valid_predictor_key].predict(x_valid) + + x_reg = ml_algo[reg_xscaler_key].transform(group['x']) + y_scaled = ml_algo[reg_predictor_key].predict(x_reg) + y_numeric_raw = ml_algo[reg_yscaler_key].inverse_transform(y_scaled) + + for local_idx, global_idx in enumerate(group['idx']): + valid_int = int(valid_pred[local_idx]) + if valid_int == 1: + sort_again[global_idx, 0] = float(y_numeric_raw[local_idx, 0]) * mat_fac + sort_again[global_idx, 1] = float(y_numeric_raw[local_idx, 1]) * mat_fac + sort_again[global_idx, 2] = 1 + else: + sort_again[global_idx, 0] = float('inf') + sort_again[global_idx, 1] = float('inf') + sort_again[global_idx, 2] = valid_int + return sort_again def update_running_time(self, *args): - ''' - Estimate the running time of the algorithm. - :return: - ''' + """ + Estimate the running time of the algorithm and keep buckling checks mutually exclusive. + """ try: self._runnig_time_label.config(text=str(self.get_running_time())) except ZeroDivisionError: pass # _tkinter.TclError: pass - if self._new_check_ml_buckling.get() == True: + selected_buckling_checks = [ + self._new_check_buckling.get(), + self._new_check_semi_analytical_buckling.get(), + False, + self._new_check_ml_numeric_buckling.get(), + ] + + if selected_buckling_checks.count(True) > 1: + tk.messagebox.showerror('You can only select one buckling type. Reselect.') + + self._new_check_buckling.set(False) + self._new_check_local_buckling.set(False) + self._new_check_semi_analytical_buckling.set(False) + self._new_check_ml_buckling.set(False) + self._new_check_ml_numeric_buckling.set(False) + + elif (self._new_check_semi_analytical_buckling.get() or self._new_check_ml_numeric_buckling.get()): self._new_check_buckling.set(False) self._new_check_local_buckling.set(False) - def get_upper_bounds(self,obj): + def _line_bundle(self, line): + return self._line_to_struc[line] + + def _line_structure(self, line): + return line_structure.structure(self._line_bundle(line)) + + def _line_stiffener(self, line): + return line_structure.stiffener(self._line_bundle(line)) + + def _line_fatigue(self, line): + return line_structure.fatigue(self._line_bundle(line)) + + def _update_harmonized_fatigue_result(self, line, x): + fatigue_obj = self._line_fatigue(line) + if fatigue_obj is not None: + self._opt_results[line][2] = opt.create_new_calc_obj( + init_obj=self._line_stiffener(line), + x=x, + fat_dict=fatigue_obj.get_fatigue_properties())[1] + else: + self._clear_line_fatigue(line) + + def _clear_line_fatigue(self, line): + self._line_bundle(line)[line_structure.FATIGUE] = None + + def get_upper_bounds(self, obj): ''' Return an numpy array of upper bounds. - :return: + :return: ''' if self._keep_spacing: spacing = obj.Plate.get_s() @@ -997,10 +1305,10 @@ def get_upper_bounds(self,obj): self._new_fl_w_upper.get() / 1000, self._new_fl_thk_upper.get() / 1000, obj.Plate.span, obj.Plate.girder_lg]) - def get_lower_bounds(self,obj): + def get_lower_bounds(self, obj): ''' Return an numpy array of lower bounds. - :return: + :return: ''' if self._keep_spacing: spacing = obj.Plate.get_s() @@ -1014,8 +1322,8 @@ def get_lower_bounds(self,obj): def checkered(self, line_distance): ''' Creates a grid in the properties canvas. - :param line_distance: - :return: + :param line_distance: + :return: ''' # vertical lines at an interval of "line_distance" pixel for x in range(line_distance, self._prop_canvas_dim[0], line_distance): @@ -1024,7 +1332,7 @@ def checkered(self, line_distance): for y in range(line_distance, self._prop_canvas_dim[1], line_distance): self._canvas_opt.create_line(0, y, self._prop_canvas_dim[0], y, fill="grey", stipple='gray50') - def draw_properties(self,init_obj = None, opt_obj=None,line=None): + def draw_properties(self, init_obj=None, opt_obj=None, line=None): ''' Drawing properties in the canvas. :return: @@ -1041,114 +1349,123 @@ def draw_properties(self,init_obj = None, opt_obj=None,line=None): self._canvas_opt.create_rectangle(0, 0, self._prop_canvas_dim[0] + 10, 80, fill='white') self._canvas_opt.create_line(10, 10, 30, 10, fill=init_color, width=5) - self._canvas_opt.create_text(270, 10, text='Initial - Pl.: ' + str(init_obj.Plate.get_s() * 1000) + 'x' + str( - init_obj.Plate.get_pl_thk() * 1000) + - ' Stf.: ' + str(init_obj.Stiffener.get_web_h() * 1000) + 'x' + str( - init_obj.Stiffener.get_web_thk() * 1000) + '+' + - str(init_obj.Stiffener.get_fl_w() * 1000) + 'x' + - str(init_obj.Stiffener.get_fl_thk() * 1000), - font='Verdana 8', - fill=init_color) + self._canvas_opt.create_text(270, 10, + text='Initial - Pl.: ' + str(init_obj.Plate.get_s() * 1000) + 'x' + str( + init_obj.Plate.get_pl_thk() * 1000) + + ' Stf.: ' + str(init_obj.Stiffener.get_web_h() * 1000) + 'x' + str( + init_obj.Stiffener.get_web_thk() * 1000) + '+' + + str(init_obj.Stiffener.get_fl_w() * 1000) + 'x' + + str(init_obj.Stiffener.get_fl_thk() * 1000), + font='Verdana 8', + fill=init_color) self._canvas_opt.create_text(120, 30, text='Weight (per Lg width): ' + - str(int(op.calc_weight([init_obj.Plate.get_s(), - init_obj.Plate.get_pl_thk(), - init_obj.Stiffener.get_web_h(), - init_obj.Stiffener.get_web_thk(), - init_obj.Stiffener.get_fl_w(), - init_obj.Stiffener.get_fl_thk(), - init_obj.Stiffener.span, - init_obj.Stiffener.girder_lg]))), - font='Verdana 8', fill=init_color) - - self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Plate.get_s() / 2, ctr_y, ctr_x + m * init_obj.Plate.get_s() / 2, - ctr_y - m * init_obj.Plate.get_pl_thk(), fill=init_color, stipple=init_stipple) - self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Stiffener.get_web_thk() / 2, ctr_y - m * init_obj.Stiffener.get_pl_thk(), - ctr_x + m * init_obj.Stiffener.get_web_thk() / 2, ctr_y - m * (init_obj.Stiffener.get_web_h() + init_obj.Stiffener.get_pl_thk()) - , fill=init_color, stipple=init_stipple) + str(int(op.calc_weight([init_obj.Plate.get_s(), + init_obj.Plate.get_pl_thk(), + init_obj.Stiffener.get_web_h(), + init_obj.Stiffener.get_web_thk(), + init_obj.Stiffener.get_fl_w(), + init_obj.Stiffener.get_fl_thk(), + init_obj.Stiffener.span, + init_obj.Stiffener.girder_lg]))), + font='Verdana 8', fill=init_color) + + self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Plate.get_s() / 2, ctr_y, + ctr_x + m * init_obj.Plate.get_s() / 2, + ctr_y - m * init_obj.Plate.get_pl_thk(), fill=init_color, + stipple=init_stipple) + self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Stiffener.get_web_thk() / 2, + ctr_y - m * init_obj.Stiffener.get_pl_thk(), + ctr_x + m * init_obj.Stiffener.get_web_thk() / 2, ctr_y - m * ( + init_obj.Stiffener.get_web_h() + init_obj.Stiffener.get_pl_thk()) + , fill=init_color, stipple=init_stipple) if init_obj.Stiffener.get_stiffener_type() not in ['L', 'L-bulb']: - self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Stiffener.get_fl_w() / 2, ctr_y - m * (init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h()), - ctr_x + m * init_obj.Stiffener.get_fl_w() / 2, - ctr_y - m * (init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h() + init_obj.Stiffener.get_fl_thk()), - fill=init_color, stipple=init_stipple) + self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Stiffener.get_fl_w() / 2, ctr_y - m * ( + init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h()), + ctr_x + m * init_obj.Stiffener.get_fl_w() / 2, + ctr_y - m * ( + init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h() + init_obj.Stiffener.get_fl_thk()), + fill=init_color, stipple=init_stipple) else: self._canvas_opt.create_rectangle(ctr_x - m * init_obj.Stiffener.get_web_thk() / 2, - ctr_y - m * (init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h()), - ctr_x + m * init_obj.Stiffener.get_fl_w(), - ctr_y - m * (init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h() + init_obj.Stiffener.get_fl_thk()), - fill=init_color, stipple=init_stipple) - + ctr_y - m * ( + init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h()), + ctr_x + m * init_obj.Stiffener.get_fl_w(), + ctr_y - m * ( + init_obj.Plate.get_pl_thk() + init_obj.Stiffener.get_web_h() + init_obj.Stiffener.get_fl_thk()), + fill=init_color, stipple=init_stipple) + if opt_obj != None: # [0.6, 0.012, 0.25, 0.01, 0.1, 0.01] - self._canvas_opt.config(bg = 'palegreen') + self._canvas_opt.config(bg='palegreen') self._canvas_opt.create_rectangle(ctr_x - m * opt_obj.Plate.get_s() / 2, ctr_y, - ctr_x + m * opt_obj.Plate.get_s() / 2, - ctr_y - m * opt_obj.Plate.get_pl_thk(), fill=opt_color, - stipple=opt_stippe) + ctr_x + m * opt_obj.Plate.get_s() / 2, + ctr_y - m * opt_obj.Plate.get_pl_thk(), fill=opt_color, + stipple=opt_stippe) self._canvas_opt.create_rectangle(ctr_x - m * opt_obj.Stiffener.get_web_thk() / 2, ctr_y - - m * opt_obj.Plate.get_pl_thk(), - ctr_x + m * opt_obj.Stiffener.get_web_thk() / 2, - ctr_y - m * ( - opt_obj.Stiffener.get_web_h() + opt_obj.Plate.get_pl_thk()) - , fill=opt_color, stipple=opt_stippe) + m * opt_obj.Plate.get_pl_thk(), + ctr_x + m * opt_obj.Stiffener.get_web_thk() / 2, + ctr_y - m * ( + opt_obj.Stiffener.get_web_h() + opt_obj.Plate.get_pl_thk()) + , fill=opt_color, stipple=opt_stippe) if init_obj.Stiffener.get_stiffener_type() not in ['L', 'L-bulb']: self._canvas_opt.create_rectangle(ctr_x - m * opt_obj.Stiffener.get_fl_w() / 2, ctr_y - - m * ( - opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h()), - ctr_x + m * opt_obj.Stiffener.get_fl_w() / 2, ctr_y - - m * ( - opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h() + - opt_obj.Stiffener.get_fl_thk()), - fill=opt_color, stipple=opt_stippe) + - m * ( + opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h()), + ctr_x + m * opt_obj.Stiffener.get_fl_w() / 2, ctr_y - + m * ( + opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h() + + opt_obj.Stiffener.get_fl_thk()), + fill=opt_color, stipple=opt_stippe) else: self._canvas_opt.create_rectangle(ctr_x - m * opt_obj.Stiffener.get_web_thk() / 2, ctr_y - - m * ( - opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h()), - ctr_x + m * opt_obj.Stiffener.get_fl_w(), ctr_y - - m * ( - opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h() + - opt_obj.Stiffener.get_fl_thk()), - fill=opt_color, stipple=opt_stippe) + - m * ( + opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h()), + ctr_x + m * opt_obj.Stiffener.get_fl_w(), ctr_y - + m * ( + opt_obj.Plate.get_pl_thk() + opt_obj.Stiffener.get_web_h() + + opt_obj.Stiffener.get_fl_thk()), + fill=opt_color, stipple=opt_stippe) self._canvas_opt.create_line(10, 50, 30, 50, fill=opt_color, width=5) self._canvas_opt.create_text(270, 50, - text='Optimized - Pl.: ' + str(round(opt_obj.Plate.get_s() * 1000,1)) + 'x' + - str(round(opt_obj.Plate.get_pl_thk() * 1000,1)) + ' Stf.: ' - + str(round(opt_obj.Stiffener.get_web_h() * 1000,1)) + - 'x' + str(round(opt_obj.Stiffener.get_web_thk() * 1000,1)) + '+' + - str(round(opt_obj.Stiffener.get_fl_w() * 1000,1)) + - 'x' + str(round(opt_obj.Stiffener.get_fl_thk() * 1000,1)), - font='Verdana 8', fill=opt_color) + text='Optimized - Pl.: ' + str(round(opt_obj.Plate.get_s() * 1000, 1)) + 'x' + + str(round(opt_obj.Plate.get_pl_thk() * 1000, 1)) + ' Stf.: ' + + str(round(opt_obj.Stiffener.get_web_h() * 1000, 1)) + + 'x' + str(round(opt_obj.Stiffener.get_web_thk() * 1000, 1)) + '+' + + str(round(opt_obj.Stiffener.get_fl_w() * 1000, 1)) + + 'x' + str(round(opt_obj.Stiffener.get_fl_thk() * 1000, 1)), + font='Verdana 8', fill=opt_color) self._canvas_opt.create_text(120, 70, text='Weight (per Lg width): ' - + str(int(op.calc_weight([opt_obj.Plate.get_s(), - opt_obj.Plate.get_pl_thk(), - opt_obj.Stiffener.get_web_h(), - opt_obj.Stiffener.get_web_thk(), - opt_obj.Stiffener.get_fl_w(), - opt_obj.Stiffener.get_fl_thk(), - opt_obj.Plate.span, - opt_obj.Plate.girder_lg]))), - font='Verdana 8', fill=opt_color) + + str(int(op.calc_weight([opt_obj.Plate.get_s(), + opt_obj.Plate.get_pl_thk(), + opt_obj.Stiffener.get_web_h(), + opt_obj.Stiffener.get_web_thk(), + opt_obj.Stiffener.get_fl_w(), + opt_obj.Stiffener.get_fl_thk(), + opt_obj.Plate.span, + opt_obj.Plate.girder_lg]))), + font='Verdana 8', fill=opt_color) elif self._opt_results != {}: self._canvas_opt.config(bg='green') self._canvas_opt.create_text(200, 200, text='Optimization results avaliable.\n\n' - 'Middle click orange lines to\n view results.', - font = 'Verdana 14 bold') + 'Middle click orange lines to\n view results.', + font='Verdana 14 bold') else: self._canvas_opt.config(bg='mistyrose') - self._canvas_opt.create_text(200, 60, text='No optimization results found.', font = 'Verdana 14 bold') + self._canvas_opt.create_text(200, 60, text='No optimization results found.', font='Verdana 14 bold') if line != None: if __name__ == '__main__': lateral_press = 200 # for testing else: lateral_press = self.app.get_highest_pressure(line)['normal'] / 1000 - self._canvas_opt.create_text(250, self._prop_canvas_dim[1]-10, - text= line + ' lateral pressure: '+str(lateral_press)+' kPa', - font='Verdana 10 bold',fill='red') - + self._canvas_opt.create_text(250, self._prop_canvas_dim[1] - 10, + text=line + ' lateral pressure: ' + str(lateral_press) + ' kPa', + font='Verdana 10 bold', fill='red') + def draw_select_canvas(self, load_selected=False): ''' Making the lines canvas. @@ -1158,22 +1475,24 @@ def draw_select_canvas(self, load_selected=False): # grid for the canavs - self._canvas_select.create_line(self._canvas_draw_origo[0], 0, self._canvas_draw_origo[0], self._select_canvas_dim[1], - stipple='gray50') - self._canvas_select.create_line(0, self._canvas_draw_origo[1], self._select_canvas_dim[0], self._canvas_draw_origo[1], - stipple='gray50') - self._canvas_select.create_text(self._canvas_draw_origo[0] - 30 , - self._canvas_draw_origo[1] + 20 , text='(0,0)', - font='Text 10') - self._canvas_select.create_text([800 ,60], - text='Mouse left click: select lines to loads\n' - 'Mouse mid click: show properties for one line\n' - 'Mouse right click: clear all selection\n' - 'Shift key press: add selected line\n' - 'Control key press: remove selected line\n\n' - 'NOTE! Select lines you want to return before\n' - 'pressing return button.', font='Verdana 8 bold', - fill='red') + self._canvas_select.create_line(self._canvas_draw_origo[0], 0, self._canvas_draw_origo[0], + self._select_canvas_dim[1], + stipple='gray50') + self._canvas_select.create_line(0, self._canvas_draw_origo[1], self._select_canvas_dim[0], + self._canvas_draw_origo[1], + stipple='gray50') + self._canvas_select.create_text(self._canvas_draw_origo[0] - 30, + self._canvas_draw_origo[1] + 20, text='(0,0)', + font='Text 10') + self._canvas_select.create_text([800, 60], + text='Mouse left click: select lines to loads\n' + 'Mouse mid click: show properties for one line\n' + 'Mouse right click: clear all selection\n' + 'Shift key press: add selected line\n' + 'Control key press: remove selected line\n\n' + 'NOTE! Select lines you want to return before\n' + 'pressing return button.', font='Verdana 8 bold', + fill='red') # drawing the line dictionary. if len(self._line_dict) != 0: for line, value in self._line_dict.items(): @@ -1189,14 +1508,14 @@ def draw_select_canvas(self, load_selected=False): color, width = 'orange', 8 self._canvas_select.create_line(coord1, coord2, width=width, fill=color) self._canvas_select.create_text(coord1[0] + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 + 10, - text='Line ' + str(get_num(line)), font='Verdand 10 bold', - fill='red') + text='Line ' + str(get_num(line)), font='Verdand 10 bold', + fill='red') else: if line in self._opt_results.keys(): color = 'orange' self._canvas_select.create_line(coord1, coord2, width=3, fill=color) self._canvas_select.create_text(coord1[0] - 20 + vector[0] / 2 + 5, coord1[1] + vector[1] / 2 + 10, - text='line' + str(get_num(line)), font="Text 8", fill='black') + text='line' + str(get_num(line)), font="Text 8", fill='black') def algorithm_info(self): ''' When button is clicked, info is displayed.''' @@ -1255,7 +1574,7 @@ def on_closing(self): return mess = tk.messagebox.showwarning('Closed without saving', 'Closing will not save loads you have created', - type = 'okcancel') + type='okcancel') if mess == 'ok': self._frame.grab_release() self._frame.destroy() @@ -1264,11 +1583,11 @@ def on_closing(self): def get_point_canvas_coord(self, point_no): ''' Returning the canvas coordinates of the point. This value will change with slider. - :param point_no: - :return: + :param point_no: + :return: ''' - point_coord_x = self._canvas_draw_origo[0] + self._point_dict[point_no][0]* self._canvas_scale - point_coord_y = self._canvas_draw_origo[1] - self._point_dict[point_no][1]* self._canvas_scale + point_coord_x = self._canvas_draw_origo[0] + self._point_dict[point_no][0] * self._canvas_scale + point_coord_y = self._canvas_draw_origo[1] - self._point_dict[point_no][1] * self._canvas_scale return [point_coord_x, point_coord_y] @@ -1288,14 +1607,14 @@ def controls(self): self._frame.bind("", self.mouse_scroll) self._frame.bind("", self.button_2_click_and_drag) - def shift_pressed(self,event=None): + def shift_pressed(self, event=None): ''' Event is executed when shift key pressed. :return: ''' self._add_to_lines = True - def ctrl_pressed(self,event=None): + def ctrl_pressed(self, event=None): ''' Event when control is pressed. :param event: @@ -1336,7 +1655,7 @@ def left_click(self, event): if self._add_to_lines: if key not in self._active_lines: self._active_lines.append(key) - elif self._add_to_lines== False: + elif self._add_to_lines == False: if key in self._active_lines: self._active_lines.remove(key) self._canvas_select.delete('all') @@ -1344,7 +1663,7 @@ def left_click(self, event): self.draw_select_canvas() self.update_running_time() - def right_click(self,event): + def right_click(self, event): ''' Event when right click. :param evnet: @@ -1357,7 +1676,7 @@ def right_click(self,event): self.draw_select_canvas() self.update_running_time() - def mid_click(self,event): + def mid_click(self, event): ''' Event when right click. :param evnet: @@ -1392,12 +1711,12 @@ def mid_click(self,event): self._canvas_select.delete('all') self._active_lines = [] self._active_lines.append(key) - if key in self._opt_results.keys() and self._opt_results[key]!=None: - self.draw_properties(init_obj=self._line_to_struc[key][0],opt_obj=self._opt_results[key][0], + if key in self._opt_results.keys() and self._opt_results[key] != None: + self.draw_properties(init_obj=self._line_structure(key), opt_obj=self._opt_results[key][0], line=key) self._mid_click_line = key else: - self.draw_properties(init_obj=self._line_to_struc[key][0],line=key) + self.draw_properties(init_obj=self._line_structure(key), line=key) self._mid_click_line = None break self.draw_select_canvas() @@ -1426,12 +1745,12 @@ def save_and_close(self): return self.app.on_close_opt_multiple_window(to_return) - messagebox.showinfo(title='Return info', message='Returning: '+str(list(to_return.keys())) + + messagebox.showinfo(title='Return info', message='Returning: ' + str(list(to_return.keys())) + '\nLines without results are not returned.') self._frame.destroy() - def toggle(self, found_files = None, obj = None, iterating = False): + def toggle(self, found_files=None, obj=None, iterating=False): ''' On off button. :param found_files: @@ -1445,16 +1764,16 @@ def toggle(self, found_files = None, obj = None, iterating = False): predefined_structure = None if self._toggle_btn.config('relief')[-1] == 'sunken': self._toggle_btn.config(relief="raised") - self._toggle_btn.config(bg = 'salmon') - self._ent_spacing_upper.config(bg = 'white') - self._ent_spacing_lower.config(bg = 'white') - self._ent_delta_spacing.config(bg = 'white') + self._toggle_btn.config(bg='salmon') + self._ent_spacing_upper.config(bg='white') + self._ent_spacing_lower.config(bg='white') + self._ent_delta_spacing.config(bg='white') else: self._toggle_btn.config(relief="sunken") self._toggle_btn.config(bg='lightgreen') - self._ent_spacing_upper.config(bg = 'lightgreen') - self._ent_spacing_lower.config(bg = 'lightgreen') - self._ent_delta_spacing.config(bg = 'lightgreen') + self._ent_spacing_upper.config(bg='lightgreen') + self._ent_spacing_lower.config(bg='lightgreen') + self._ent_delta_spacing.config(bg='lightgreen') openfile = list(askopenfilenames(parent=self._frame, title='Choose files to open', initialdir=self._root_dir)) if openfile == []: @@ -1476,18 +1795,18 @@ def plot_results(self): if len(self._opt_results[self._mid_click_line]) != 0: op.plot_optimization_results(self._opt_results[self._mid_click_line]) - def mouse_scroll(self,event): - self._canvas_scale += event.delta/50 + def mouse_scroll(self, event): + self._canvas_scale += event.delta / 50 self._canvas_scale = 0 if self._canvas_scale < 0 else self._canvas_scale self.draw_select_canvas() - def button_2_click_and_drag(self,event): + def button_2_click_and_drag(self, event): - self._canvas_draw_origo = (self._canvas_draw_origo[0]-(self._previous_drag_mouse[0]-event.x), - self._canvas_draw_origo[1]-(self._previous_drag_mouse[1]-event.y)) + self._canvas_draw_origo = (self._canvas_draw_origo[0] - (self._previous_drag_mouse[0] - event.x), + self._canvas_draw_origo[1] - (self._previous_drag_mouse[1] - event.y)) - self._previous_drag_mouse = (event.x,event.y) + self._previous_drag_mouse = (event.x, event.y) self.draw_select_canvas() def open_example_file(self): diff --git a/anystruct/optimize_window.py b/anystruct/optimize_window.py index 9c50b90c..12cb009a 100644 --- a/anystruct/optimize_window.py +++ b/anystruct/optimize_window.py @@ -12,73 +12,126 @@ import anystruct.example_data as test import anystruct.example_data as ex import anystruct.helper as hlp + import anystruct.line_structure as line_structure import anystruct.optimize as op except ModuleNotFoundError: from ANYstructure.anystruct.calc_structure import CalcScantlings, AllStructure import ANYstructure.anystruct.example_data as test import ANYstructure.anystruct.example_data as ex import ANYstructure.anystruct.helper as hlp + import ANYstructure.anystruct.line_structure as line_structure import ANYstructure.anystruct.optimize as op + + class CreateOptimizeWindow(): ''' This class initiates the single optimization window. ''' - def __init__(self,master,app=None): - super(CreateOptimizeWindow,self).__init__() + def __init__(self, master, app=None): + super(CreateOptimizeWindow, self).__init__() if __name__ == '__main__': import pickle Plate = CalcScantlings(ex.obj_dict) - Stiffener = None#CalcScantlings(ex.obj_dict) + Stiffener = None # CalcScantlings(ex.obj_dict) Girder = None # CalcScantlings(ex.obj_dict_heavy) - self._initial_calc_obj = AllStructure(Plate=Plate, Stiffener=Stiffener, Girder=Girder, - main_dict=ex.prescriptive_main_dict) + self._initial_calc_obj = AllStructure(Plate=Plate, Stiffener=Stiffener, Girder=Girder, + main_dict=ex.prescriptive_main_dict) - #self._initial_calc_obj = test.get_structure_calc_object(heavy=True) + # self._initial_calc_obj = test.get_structure_calc_object(heavy=True) self._lateral_pressure = 0.2 self._fatigue_object = test.get_fatigue_object() self._fatigue_pressure = test.get_fatigue_pressures() self._slamming_pressure = test.get_slamming_pressure() - image_dir = os.path.dirname(__file__)+'\\images\\' - self._PULS_object = None - self._puls_acceptance = 0.87 - self._initial_calc_obj.lat_press = self._lateral_pressure/1000 + image_dir = os.path.dirname(__file__) + '\\images\\' + self._initial_calc_obj.lat_press = self._lateral_pressure / 1000 self._ML_buckling = dict() # Buckling machine learning algorithm self._root_dir = '/\\' - for name, file_base in zip(['cl SP buc int predictor', 'cl SP buc int scaler', - 'cl SP ult int predictor', 'cl SP ult int scaler', - 'cl SP buc GLGT predictor', 'cl SP buc GLGT scaler', - 'cl SP ult GLGT predictor', 'cl SP ult GLGT scaler', - 'cl UP buc int predictor', 'cl UP buc int scaler', - 'cl UP ult int predictor', 'cl UP ult int scaler', - 'cl UP buc GLGT predictor', 'cl UP buc GLGT scaler', - 'cl UP ult GLGT predictor', 'cl UP ult GLGT scaler' - ], - ["ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_buc_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_buc_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_ult_scaler_In-plane_support_cl_2,_3_UP", - "CL_CSR-Tank_req_cl_predictor", - "CL_CSR-Tank_req_cl_UP_scaler", - "CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", - "CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_SP_scaler"]): + ml_model_files = [ + # Classification pipeline + ('cl SP buc int predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_SP'), + ('cl SP buc int scaler', 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_SP'), + ('cl SP ult int predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_SP'), + ('cl SP ult int scaler', 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_SP'), + + ('cl SP buc GLGT predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_SP'), + ('cl SP buc GLGT scaler', + 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_SP'), + ('cl SP ult GLGT predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_SP'), + ('cl SP ult GLGT scaler', + 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_SP'), + + ('cl UP buc int predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_UP'), + ('cl UP buc int scaler', 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_UP'), + ('cl UP ult int predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_UP'), + ('cl UP ult int scaler', 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_UP'), + + ('cl UP buc GLGT predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_UP'), + ('cl UP buc GLGT scaler', + 'ml_files\\CLPIPE_CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_UP'), + ('cl UP ult GLGT predictor', + 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_UP'), + ('cl UP ult GLGT scaler', + 'ml_files\\CLPIPE_CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_UP'), + + # Numeric UF pipeline + ('num SP int validity predictor', + 'ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num SP int validity xscaler', + 'ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num SP int UF reg predictor', + 'ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num SP int UF reg xscaler', + 'ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num SP int UF reg yscaler', + 'ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + + ('num SP GLGT validity predictor', + 'ml_files\\NUMPIPE_VALID_predictor_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num SP GLGT validity xscaler', + 'ml_files\\NUMPIPE_VALID_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num SP GLGT UF reg predictor', + 'ml_files\\NUMPIPE_REG_predictor_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num SP GLGT UF reg xscaler', + 'ml_files\\NUMPIPE_REG_xscaler_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num SP GLGT UF reg yscaler', + 'ml_files\\NUMPIPE_REG_yscaler_SP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + + ('num UP int validity predictor', + 'ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num UP int validity xscaler', + 'ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num UP int UF reg predictor', + 'ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num UP int UF reg xscaler', + 'ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + ('num UP int UF reg yscaler', + 'ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_1_In-plane_support_cl_1'), + + ('num UP GLGT validity predictor', + 'ml_files\\NUMPIPE_VALID_predictor_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num UP GLGT validity xscaler', + 'ml_files\\NUMPIPE_VALID_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num UP GLGT UF reg predictor', + 'ml_files\\NUMPIPE_REG_predictor_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num UP GLGT UF reg xscaler', + 'ml_files\\NUMPIPE_REG_xscaler_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ('num UP GLGT UF reg yscaler', + 'ml_files\\NUMPIPE_REG_yscaler_UP_UF_numeric_In-plane_support_cl_2,_3_In-plane_support_cl_2,_3'), + ] + + for name, file_base in ml_model_files: self._ML_buckling[name] = None if os.path.isfile(file_base + '.pickle'): file = open(file_base + '.pickle', 'rb') - from sklearn.neural_network import MLPClassifier - from sklearn.preprocessing import StandardScaler + import pickle self._ML_buckling[name] = pickle.load(file) file.close() @@ -94,8 +147,9 @@ def __init__(self,master,app=None): else: self.app = app - self._initial_calc_obj = app._line_to_struc[app._active_line][0] - self._fatigue_object = app._line_to_struc[app._active_line][2] + active_bundle = app._line_to_struc[app._active_line] + self._initial_calc_obj = line_structure.structure(active_bundle) + self._fatigue_object = line_structure.fatigue(active_bundle) try: self._fatigue_pressure = app.get_fatigue_pressures(app._active_line, self._fatigue_object.get_accelerations()) @@ -113,11 +167,8 @@ def __init__(self,master,app=None): self._slamming_pressure = self.app.get_highest_pressure(self.app._active_line)['slamming'] except KeyError: self._slamming_pressure = 0 - image_dir = app._root_dir +'\\images\\' + image_dir = app._root_dir + '\\images\\' self._root_dir = app._root_dir - self._PULS_object = app._PULS_results - self._puls_acceptance = self.app._new_puls_uf.get() - self._ML_buckling = app._ML_buckling self._predefined_stiffener_iter = None @@ -129,40 +180,39 @@ def __init__(self,master,app=None): self._opt_runned = False self._opt_results = () - self._opt_actual_running_time = tk.Label(self._frame,text='',font='Verdana 12 bold') - + self._opt_actual_running_time = tk.Label(self._frame, text='', font='Verdana 12 bold') self._draw_scale = 500 self._canvas_dim = (500, 450) - self._canvas_opt = tk.Canvas(self._frame,width=self._canvas_dim[0], height=self._canvas_dim[1], - background='azure',relief = 'groove', borderwidth=2) + self._canvas_opt = tk.Canvas(self._frame, width=self._canvas_dim[0], height=self._canvas_dim[1], + background='azure', relief='groove', borderwidth=2) - tk.Frame(self._frame,width=770,height=5, bg="grey", colormap="new").place(x=20,y=127) + tk.Frame(self._frame, width=770, height=5, bg="grey", colormap="new").place(x=20, y=127) tk.Frame(self._frame, width=770, height=5, bg="grey", colormap="new").place(x=20, y=167) - self._canvas_opt.place(x=10,y=300) + self._canvas_opt.place(x=10, y=300) - algorithms = ('anysmart','random','random_no_delta', 'anydetail') + algorithms = ('anysmart', 'random', 'random_no_delta', 'anydetail') - tk.Label(self._frame,text='-- Structural optimizer --',font='Verdana 15 bold').place(x=10,y=10) + tk.Label(self._frame, text='-- Structural optimizer --', font='Verdana 15 bold').place(x=10, y=10) if self._initial_calc_obj.Stiffener is not None: self._spacing = self._initial_calc_obj.Plate.get_s() self._pl_thk = self._initial_calc_obj.Plate.get_pl_thk() self._stf_web_h = self._initial_calc_obj.Stiffener.get_web_h() - self._stf_web_thk =self._initial_calc_obj.Stiffener.get_web_thk() + self._stf_web_thk = self._initial_calc_obj.Stiffener.get_web_thk() self._fl_w = self._initial_calc_obj.Stiffener.get_fl_w() - self._fl_thk =self._initial_calc_obj.Stiffener.get_fl_thk() + self._fl_thk = self._initial_calc_obj.Stiffener.get_fl_thk() else: self._spacing = self._initial_calc_obj.Plate.get_s() self._pl_thk = self._initial_calc_obj.Plate.get_pl_thk() self._stf_web_h = 0 - self._stf_web_thk =0 + self._stf_web_thk = 0 self._fl_w = 0 - self._fl_thk =0 + self._fl_thk = 0 # upper and lower bounds for optimization - #[0.6, 0.012, 0.3, 0.01, 0.1, 0.01] + # [0.6, 0.012, 0.3, 0.01, 0.1, 0.01] self._new_spacing_upper = tk.DoubleVar() self._new_spacing_lower = tk.DoubleVar() self._new_pl_thk_upper = tk.DoubleVar() @@ -180,23 +230,22 @@ def __init__(self,master,app=None): self._new_algorithm = tk.StringVar() self._new_algorithm_random_trials = tk.IntVar() self._new_swarm_size = tk.IntVar() - self._new_omega = tk.DoubleVar() - self._new_phip = tk.DoubleVar() - self._new_phig = tk.DoubleVar() + self._new_omega = tk.DoubleVar() + self._new_phip = tk.DoubleVar() + self._new_phig = tk.DoubleVar() self._new_maxiter = tk.IntVar() - self._new_minstep = tk.DoubleVar() - self._new_minfunc = tk.DoubleVar() + self._new_minstep = tk.DoubleVar() + self._new_minfunc = tk.DoubleVar() self._new_slamming_pressure = tk.DoubleVar() self._new_fatigue_int_press = tk.DoubleVar() self._new_fatigue_ext_press = tk.DoubleVar() - ent_w = 10 - self._ent_spacing_upper = tk.Entry(self._frame, textvariable = self._new_spacing_upper, width = ent_w) + self._ent_spacing_upper = tk.Entry(self._frame, textvariable=self._new_spacing_upper, width=ent_w) self._ent_spacing_lower = tk.Entry(self._frame, textvariable=self._new_spacing_lower, width=ent_w) - self._ent_pl_thk_upper= tk.Entry(self._frame, textvariable=self._new_pl_thk_upper, width=ent_w) - self._ent_pl_thk_lower= tk.Entry(self._frame, textvariable=self._new_pl_thk_lower, width=ent_w) + self._ent_pl_thk_upper = tk.Entry(self._frame, textvariable=self._new_pl_thk_upper, width=ent_w) + self._ent_pl_thk_lower = tk.Entry(self._frame, textvariable=self._new_pl_thk_lower, width=ent_w) self._ent_web_h_upper = tk.Entry(self._frame, textvariable=self._new_web_h_upper, width=ent_w) self._ent_web_h_lower = tk.Entry(self._frame, textvariable=self._new_web_h_lower, width=ent_w) @@ -213,19 +262,20 @@ def __init__(self,master,app=None): self._ent_span = tk.Entry(self._frame, textvariable=self._new_span, width=ent_w) self._ent_width_lg = tk.Entry(self._frame, textvariable=self._new_width_lg, width=ent_w) self._ent_slamming_pressure = tk.Entry(self._frame, textvariable=self._new_slamming_pressure, width=ent_w) - - #additional choices for the random and pso algorithm - self._ent_algorithm = tk.OptionMenu(self._frame,self._new_algorithm,command=self.selected_algorithm,*algorithms) - self._ent_random_trials = tk.Entry(self._frame,textvariable=self._new_algorithm_random_trials) + + # additional choices for the random and pso algorithm + self._ent_algorithm = tk.OptionMenu(self._frame, self._new_algorithm, command=self.selected_algorithm, + *algorithms) + self._ent_random_trials = tk.Entry(self._frame, textvariable=self._new_algorithm_random_trials) pso_width = 10 - self._ent_swarm_size = tk.Entry(self._frame,textvariable=self._new_swarm_size, width = pso_width) - self._ent_omega = tk.Entry(self._frame,textvariable=self._new_omega, width = pso_width) - self._ent_phip = tk.Entry(self._frame,textvariable=self._new_phip, width = pso_width) - self._ent_phig = tk.Entry(self._frame,textvariable=self._new_phig, width = pso_width) - self._ent_maxiter = tk.Entry(self._frame,textvariable=self._new_maxiter, width = pso_width) - self._ent_minstep = tk.Entry(self._frame,textvariable=self._new_minstep, width = pso_width) - self._ent_minfunc = tk.Entry(self._frame,textvariable=self._new_minfunc, width = pso_width) + self._ent_swarm_size = tk.Entry(self._frame, textvariable=self._new_swarm_size, width=pso_width) + self._ent_omega = tk.Entry(self._frame, textvariable=self._new_omega, width=pso_width) + self._ent_phip = tk.Entry(self._frame, textvariable=self._new_phip, width=pso_width) + self._ent_phig = tk.Entry(self._frame, textvariable=self._new_phig, width=pso_width) + self._ent_maxiter = tk.Entry(self._frame, textvariable=self._new_maxiter, width=pso_width) + self._ent_minstep = tk.Entry(self._frame, textvariable=self._new_minstep, width=pso_width) + self._ent_minfunc = tk.Entry(self._frame, textvariable=self._new_minfunc, width=pso_width) self._new_delta_spacing = tk.DoubleVar() self._new_delta_pl_thk = tk.DoubleVar() @@ -233,7 +283,7 @@ def __init__(self,master,app=None): self._new_delta_web_thk = tk.DoubleVar() self._new_delta_fl_w = tk.DoubleVar() self._new_delta_fl_thk = tk.DoubleVar() - + self._new_opt_spacing = tk.DoubleVar() self._new_opt_pl_thk = tk.DoubleVar() self._new_opt_web_h = tk.DoubleVar() @@ -241,22 +291,20 @@ def __init__(self,master,app=None): self._new_opt_fl_w = tk.DoubleVar() self._new_opt_fl_thk = tk.DoubleVar() - self._ent_delta_spacing = tk.Entry(self._frame, textvariable = self._new_delta_spacing, width = ent_w) - self._ent_delta_pl_thk = tk.Entry(self._frame, textvariable = self._new_delta_pl_thk, width = ent_w) - self._ent_delta_web_h = tk.Entry(self._frame, textvariable = self._new_delta_web_h, width = ent_w) - self._ent_delta_web_thk = tk.Entry(self._frame, textvariable = self._new_delta_web_thk, width = ent_w) - self._ent_delta_fl_w = tk.Entry(self._frame, textvariable = self._new_delta_fl_w, width = ent_w) - self._ent_delta_fl_thk = tk.Entry(self._frame, textvariable = self._new_delta_fl_thk, width = ent_w) + self._ent_delta_spacing = tk.Entry(self._frame, textvariable=self._new_delta_spacing, width=ent_w) + self._ent_delta_pl_thk = tk.Entry(self._frame, textvariable=self._new_delta_pl_thk, width=ent_w) + self._ent_delta_web_h = tk.Entry(self._frame, textvariable=self._new_delta_web_h, width=ent_w) + self._ent_delta_web_thk = tk.Entry(self._frame, textvariable=self._new_delta_web_thk, width=ent_w) + self._ent_delta_fl_w = tk.Entry(self._frame, textvariable=self._new_delta_fl_w, width=ent_w) + self._ent_delta_fl_thk = tk.Entry(self._frame, textvariable=self._new_delta_fl_thk, width=ent_w) bg_col = 'pink' - self._ent_opt_spacing = tk.Entry(self._frame, textvariable=self._new_opt_spacing, width=ent_w,bg=bg_col) - self._ent_opt_pl_thk = tk.Entry(self._frame, textvariable=self._new_opt_pl_thk, width=ent_w,bg=bg_col) - self._ent_opt_web_h = tk.Entry(self._frame, textvariable=self._new_opt_web_h, width=ent_w,bg=bg_col) - self._ent_opt_web_thk = tk.Entry(self._frame, textvariable=self._new_opt_web_thk, width=ent_w,bg=bg_col) - self._ent_opt_fl_w = tk.Entry(self._frame, textvariable=self._new_opt_fl_w, width=ent_w,bg=bg_col) - self._ent_opt_fl_thk = tk.Entry(self._frame, textvariable=self._new_opt_fl_thk, width=ent_w,bg=bg_col) - - + self._ent_opt_spacing = tk.Entry(self._frame, textvariable=self._new_opt_spacing, width=ent_w, bg=bg_col) + self._ent_opt_pl_thk = tk.Entry(self._frame, textvariable=self._new_opt_pl_thk, width=ent_w, bg=bg_col) + self._ent_opt_web_h = tk.Entry(self._frame, textvariable=self._new_opt_web_h, width=ent_w, bg=bg_col) + self._ent_opt_web_thk = tk.Entry(self._frame, textvariable=self._new_opt_web_thk, width=ent_w, bg=bg_col) + self._ent_opt_fl_w = tk.Entry(self._frame, textvariable=self._new_opt_fl_w, width=ent_w, bg=bg_col) + self._ent_opt_fl_thk = tk.Entry(self._frame, textvariable=self._new_opt_fl_thk, width=ent_w, bg=bg_col) # stresses in plate and stiffener @@ -267,158 +315,161 @@ def __init__(self,master,app=None): self._new_design_pressure = tk.DoubleVar() self._new_pressure_side = tk.StringVar() - self._ent_trans_stress_high = tk.Entry(self._frame, textvariable=self._new_trans_stress_high, width=ent_w) self._ent_trans_stress_low = tk.Entry(self._frame, textvariable=self._new_trans_stress_low, width=ent_w) self._ent_axial_stress = tk.Entry(self._frame, textvariable=self._new_axial_stress, width=ent_w) self._ent_design_pressure = tk.Entry(self._frame, textvariable=self._new_design_pressure, width=ent_w) - self._ent_design_pressure_side = tk.OptionMenu(self._frame,self._new_pressure_side,*('p','s')) + self._ent_design_pressure_side = tk.OptionMenu(self._frame, self._new_pressure_side, *('p', 's')) self._ent_shear_stress = tk.Entry(self._frame, textvariable=self._new_shear_stress, width=ent_w) - start_x,start_y,dx,dy = 20,100,100,40 + start_x, start_y, dx, dy = 20, 100, 100, 40 self._new_processes = tk.IntVar() self._new_processes.set(max(cpu_count() - 1, 1)) - tk.Label(self._frame, text='Processes\n (CPUs)', font='Verdana 9 bold', bg = 'silver')\ + tk.Label(self._frame, text='Processes\n (CPUs)', font='Verdana 9 bold', bg='silver') \ .place(x=start_x + 8.3 * dx, y=start_y - 1.1 * dy) - tk.Entry(self._frame, textvariable=self._new_processes, width = 12, bg = 'silver')\ + tk.Entry(self._frame, textvariable=self._new_processes, width=12, bg='silver') \ .place(x=start_x + 8.3 * dx, y=start_y - 0.3 * dy) - - tk.Label(self._frame,text='Upper bounds [mm]',font='Verdana 9').place(x=start_x,y=start_y) - tk.Label(self._frame, text='Iteration delta [mm]',font='Verdana 9').place(x=start_x, y=start_y+dy) - tk.Label(self._frame, text='Lower bounds [mm]',font='Verdana 9').place(x=start_x, y=start_y+2*dy) - tk.Label(self._frame, text='Spacing [mm]', font='Verdana 7 bold').place(x=start_x + 1.97 * dx, y=start_y-0.6*dy) - tk.Label(self._frame, text='Plate thk. [mm]', font='Verdana 7 bold').place(x=start_x + 2.97 * dx, y=start_y-0.6*dy) - tk.Label(self._frame, text='Web height [mm]', font='Verdana 7 bold').place(x=start_x + 3.97 * dx, y=start_y-0.6*dy) - tk.Label(self._frame, text='Web thk. [mm]', font='Verdana 7 bold').place(x=start_x + 4.97 * dx, y=start_y-0.6*dy) - tk.Label(self._frame, text='Flange width [mm]', font='Verdana 7 bold').place(x=start_x + 5.97 * dx, y=start_y-0.6*dy) - tk.Label(self._frame, text='Flange thk. [mm]', font='Verdana 7 bold').place(x=start_x + 6.97 * dx, y=start_y-0.6*dy) + tk.Label(self._frame, text='Upper bounds [mm]', font='Verdana 9').place(x=start_x, y=start_y) + tk.Label(self._frame, text='Iteration delta [mm]', font='Verdana 9').place(x=start_x, y=start_y + dy) + tk.Label(self._frame, text='Lower bounds [mm]', font='Verdana 9').place(x=start_x, y=start_y + 2 * dy) + tk.Label(self._frame, text='Spacing [mm]', font='Verdana 7 bold').place(x=start_x + 1.97 * dx, + y=start_y - 0.6 * dy) + tk.Label(self._frame, text='Plate thk. [mm]', font='Verdana 7 bold').place(x=start_x + 2.97 * dx, + y=start_y - 0.6 * dy) + tk.Label(self._frame, text='Web height [mm]', font='Verdana 7 bold').place(x=start_x + 3.97 * dx, + y=start_y - 0.6 * dy) + tk.Label(self._frame, text='Web thk. [mm]', font='Verdana 7 bold').place(x=start_x + 4.97 * dx, + y=start_y - 0.6 * dy) + tk.Label(self._frame, text='Flange width [mm]', font='Verdana 7 bold').place(x=start_x + 5.97 * dx, + y=start_y - 0.6 * dy) + tk.Label(self._frame, text='Flange thk. [mm]', font='Verdana 7 bold').place(x=start_x + 6.97 * dx, + y=start_y - 0.6 * dy) tk.Label(self._frame, text='--------- Number of combinations to run --------->\n' - 'PULS buckling is time consuming, about 0.2 sec. per comb.\n' - 'RP-C203 is much faster and can run many more combinations, 1M+.\n' - 'ML-CL is about as fast as RP-C203.', - font='Verdana 9 bold').place(x=start_x+0.1*dx, y=start_y + 2.8 * dy, anchor = tk.NW) - - self._runnig_time_label = tk.Label(self._frame, text='',font='Verdana 12 bold', fg = 'red') - self._runnig_time_label.place(x=start_x+4.3*dx, y=start_y + 2.8 * dy) - #tk.Label(self._frame, text='seconds ',font='Verdana 9 bold').place(x=start_x+6*dx, y=start_y + 2.8 * dy) - self._result_label = tk.Label(self._frame, text = '',font = 'Verdana 9 bold' ) + 'RP-C203 can run many combinations, 1M+.\n' + 'ML-Numeric is about as fast as RP-C203.', + font='Verdana 9 bold').place(x=start_x + 0.1 * dx, y=start_y + 2.8 * dy, anchor=tk.NW) + + self._runnig_time_label = tk.Label(self._frame, text='', font='Verdana 12 bold', fg='red') + self._runnig_time_label.place(x=start_x + 4.3 * dx, y=start_y + 2.8 * dy) + # tk.Label(self._frame, text='seconds ',font='Verdana 9 bold').place(x=start_x+6*dx, y=start_y + 2.8 * dy) + self._result_label = tk.Label(self._frame, text='', font='Verdana 9 bold') self._result_label.place(x=start_x, y=start_y + 4.2 * dy) - self._ent_spacing_upper.place(x=start_x+dx*2,y=start_y) - self._ent_delta_spacing.place(x=start_x+dx*2,y=start_y+dy) - self._ent_spacing_lower.place(x=start_x+dx*2,y=start_y+2*dy) + self._ent_spacing_upper.place(x=start_x + dx * 2, y=start_y) + self._ent_delta_spacing.place(x=start_x + dx * 2, y=start_y + dy) + self._ent_spacing_lower.place(x=start_x + dx * 2, y=start_y + 2 * dy) + + self._ent_pl_thk_upper.place(x=start_x + dx * 3, y=start_y) + self._ent_delta_pl_thk.place(x=start_x + dx * 3, y=start_y + dy) + self._ent_pl_thk_lower.place(x=start_x + dx * 3, y=start_y + 2 * dy) - self._ent_pl_thk_upper.place(x=start_x+dx*3,y=start_y) - self._ent_delta_pl_thk.place(x=start_x+dx*3,y=start_y+dy) - self._ent_pl_thk_lower.place(x=start_x+dx*3,y=start_y+2*dy) + self._ent_web_h_upper.place(x=start_x + dx * 4, y=start_y) + self._ent_delta_web_h.place(x=start_x + dx * 4, y=start_y + dy) + self._ent_web_h_lower.place(x=start_x + dx * 4, y=start_y + 2 * dy) - self._ent_web_h_upper.place(x=start_x+dx*4,y=start_y) - self._ent_delta_web_h.place(x=start_x+dx*4,y=start_y+dy) - self._ent_web_h_lower.place(x=start_x+dx*4,y=start_y+2*dy) + self._ent_web_thk_upper.place(x=start_x + dx * 5, y=start_y) + self._ent_delta_web_thk.place(x=start_x + dx * 5, y=start_y + dy) + self._ent_web_thk_lower.place(x=start_x + dx * 5, y=start_y + 2 * dy) - self._ent_web_thk_upper.place(x=start_x+dx*5,y=start_y) - self._ent_delta_web_thk.place(x=start_x+dx*5,y=start_y+dy) - self._ent_web_thk_lower.place(x=start_x+dx*5,y=start_y+2*dy) + self._ent_fl_w_upper.place(x=start_x + dx * 6, y=start_y) + self._ent_delta_fl_w.place(x=start_x + dx * 6, y=start_y + dy) + self._ent_fl_w_lower.place(x=start_x + dx * 6, y=start_y + 2 * dy) - self._ent_fl_w_upper.place(x=start_x+dx*6,y=start_y) - self._ent_delta_fl_w.place(x=start_x+dx*6,y=start_y+dy) - self._ent_fl_w_lower.place(x=start_x+dx*6,y=start_y+2*dy) + self._ent_fl_thk_upper.place(x=start_x + dx * 7, y=start_y) + self._ent_delta_fl_thk.place(x=start_x + dx * 7, y=start_y + dy) + self._ent_fl_thk_lower.place(x=start_x + dx * 7, y=start_y + 2 * dy) - self._ent_fl_thk_upper.place(x=start_x+dx*7,y=start_y) - self._ent_delta_fl_thk.place(x=start_x+dx*7,y=start_y+dy) - self._ent_fl_thk_lower.place(x=start_x+dx*7,y=start_y+2*dy) - ### # tk.Label(self._frame,text='Optimized result:\n')\ # .place(x=start_x,y=start_y+ver_mult*dy*0.9) dx_mult = 0.7 - tk.Label(self._frame,text='Optimized values').place(x=start_x,y=start_y+17*dy) + tk.Label(self._frame, text='Optimized values').place(x=start_x, y=start_y + 17 * dy) tk.Label(self._frame, text='s').place(x=start_x, y=start_y + 18 * dy) tk.Label(self._frame, text='pl_thk').place(x=start_x, y=start_y + 19 * dy) - self._ent_opt_spacing.place(x=start_x+dx_mult*dx,y=start_y+18*dy) - self._ent_opt_pl_thk.place(x=start_x+dx_mult*dx,y=start_y+19*dy) - - tk.Label(self._frame, text='web_h').place(x=start_x+2*dx_mult*dx, y=start_y + 18 * dy) - tk.Label(self._frame, text='web_htk').place(x=start_x+2*dx_mult*dx, y=start_y + 19 * dy) - self._ent_opt_web_h.place(x=start_x+3*dx_mult*dx,y=start_y+18*dy) - self._ent_opt_web_thk.place(x=start_x+3*dx_mult*dx,y=start_y+19*dy) - - tk.Label(self._frame, text='fl_thk').place(x=start_x+4*dx_mult*dx, y=start_y + 18 * dy) - tk.Label(self._frame, text='fl_ttk.').place(x=start_x+4*dx_mult*dx, y=start_y + 19 * dy) - self._ent_opt_fl_w.place(x=start_x+5*dx_mult*dx,y=start_y+18*dy) - self._ent_opt_fl_thk.place(x=start_x+5*dx_mult*dx,y=start_y+19*dy) - - #Labels for the pso - - self._lb_swarm_size = tk.Label(self._frame,text='swarm size') - self._lb_omega = tk.Label(self._frame,text='omega') - self._lb_phip = tk.Label(self._frame,text='phip') - self._lb_phig = tk.Label(self._frame,text='phig') - self._lb_maxiter = tk.Label(self._frame,text='maxiter') - self._lb_minstep = tk.Label(self._frame,text='minstep') - self._lb_minfunc = tk.Label(self._frame,text='minfunc') + self._ent_opt_spacing.place(x=start_x + dx_mult * dx, y=start_y + 18 * dy) + self._ent_opt_pl_thk.place(x=start_x + dx_mult * dx, y=start_y + 19 * dy) + + tk.Label(self._frame, text='web_h').place(x=start_x + 2 * dx_mult * dx, y=start_y + 18 * dy) + tk.Label(self._frame, text='web_htk').place(x=start_x + 2 * dx_mult * dx, y=start_y + 19 * dy) + self._ent_opt_web_h.place(x=start_x + 3 * dx_mult * dx, y=start_y + 18 * dy) + self._ent_opt_web_thk.place(x=start_x + 3 * dx_mult * dx, y=start_y + 19 * dy) + + tk.Label(self._frame, text='fl_thk').place(x=start_x + 4 * dx_mult * dx, y=start_y + 18 * dy) + tk.Label(self._frame, text='fl_ttk.').place(x=start_x + 4 * dx_mult * dx, y=start_y + 19 * dy) + self._ent_opt_fl_w.place(x=start_x + 5 * dx_mult * dx, y=start_y + 18 * dy) + self._ent_opt_fl_thk.place(x=start_x + 5 * dx_mult * dx, y=start_y + 19 * dy) + + # Labels for the pso + + self._lb_swarm_size = tk.Label(self._frame, text='swarm size') + self._lb_omega = tk.Label(self._frame, text='omega') + self._lb_phip = tk.Label(self._frame, text='phip') + self._lb_phig = tk.Label(self._frame, text='phig') + self._lb_maxiter = tk.Label(self._frame, text='maxiter') + self._lb_minstep = tk.Label(self._frame, text='minstep') + self._lb_minfunc = tk.Label(self._frame, text='minfunc') ### - tk.Label(self._frame, text='Sigma,y1_Sd - large transversal stress', font='Verdana 9')\ - .place(x=start_x+dx*5,y=start_y+11.5*dy) - tk.Label(self._frame, text='MPa', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+11.5*dy) - - tk.Label(self._frame, text='Sigma,y2_Sd - small transversal stress', font='Verdana 9')\ - .place(x=start_x+dx*5,y=start_y+12.5*dy) - tk.Label(self._frame, text='MPa', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+12.5*dy) - - tk.Label(self._frame, text='Sigma,x_Sd - axial stress', font='Verdana 9')\ - .place(x=start_x+dx*5,y=start_y+13.5*dy) - tk.Label(self._frame, text='MPa', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+13.5*dy) - - tk.Label(self._frame, text='Tau,xy - shear stress', font='Verdana 9')\ - .place(x=start_x+dx*5,y=start_y+14.5*dy) - tk.Label(self._frame, text='MPa', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+14.5*dy) - - tk.Label(self._frame, text='Applied pressure ', font='Verdana 9 bold')\ - .place(x=start_x+dx*5,y=start_y+15.5*dy) - tk.Label(self._frame, text='kPa', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+15.5*dy) - tk.Label(self._frame, text='Plate or stiffener side (p/s): ', font='Verdana 9 bold')\ - .place(x=start_x+dx*9.5,y=start_y+15.5*dy) + tk.Label(self._frame, text='Sigma,y1_Sd - large transversal stress', font='Verdana 9') \ + .place(x=start_x + dx * 5, y=start_y + 11.5 * dy) + tk.Label(self._frame, text='MPa', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 11.5 * dy) + + tk.Label(self._frame, text='Sigma,y2_Sd - small transversal stress', font='Verdana 9') \ + .place(x=start_x + dx * 5, y=start_y + 12.5 * dy) + tk.Label(self._frame, text='MPa', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 12.5 * dy) + + tk.Label(self._frame, text='Sigma,x_Sd - axial stress', font='Verdana 9') \ + .place(x=start_x + dx * 5, y=start_y + 13.5 * dy) + tk.Label(self._frame, text='MPa', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 13.5 * dy) + + tk.Label(self._frame, text='Tau,xy - shear stress', font='Verdana 9') \ + .place(x=start_x + dx * 5, y=start_y + 14.5 * dy) + tk.Label(self._frame, text='MPa', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 14.5 * dy) + + tk.Label(self._frame, text='Applied pressure ', font='Verdana 9 bold') \ + .place(x=start_x + dx * 5, y=start_y + 15.5 * dy) + tk.Label(self._frame, text='kPa', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 15.5 * dy) + tk.Label(self._frame, text='Plate or stiffener side (p/s): ', font='Verdana 9 bold') \ + .place(x=start_x + dx * 9.5, y=start_y + 15.5 * dy) tk.Label(self._frame, text='Span: ', font='Verdana 9') \ .place(x=start_x + dx * 5, y=start_y + 16.5 * dy) - tk.Label(self._frame, text='m', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+16.5*dy) + tk.Label(self._frame, text='m', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 16.5 * dy) tk.Label(self._frame, text='Girder length,Lg: ', font='Verdana 9') \ .place(x=start_x + dx * 5, y=start_y + 17.5 * dy) - tk.Label(self._frame, text='m', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+17.5*dy) + tk.Label(self._frame, text='m', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 17.5 * dy) tk.Label(self._frame, text='Slamming pressure ', font='Verdana 9') \ .place(x=start_x + dx * 5, y=start_y + 18.5 * dy) - tk.Label(self._frame, text='Pa', font='Verdana 9')\ - .place(x=start_x+dx*9,y=start_y+18.5*dy) + tk.Label(self._frame, text='Pa', font='Verdana 9') \ + .place(x=start_x + dx * 9, y=start_y + 18.5 * dy) if self._fatigue_pressure is not None: - tk.Label(self._frame, text='Fatigue pressure: internal= '+str(self._fatigue_pressure['p_int'])+ ' external= ' - +str(self._fatigue_pressure['p_ext']), font='Verdana 7') \ + tk.Label(self._frame, + text='Fatigue pressure: internal= ' + str(self._fatigue_pressure['p_int']) + ' external= ' + + str(self._fatigue_pressure['p_ext']), font='Verdana 7') \ .place(x=start_x + dx * 5, y=start_y + 19.3 * dy) else: - tk.Label(self._frame, text='Fatigue pressure: internal= '+str(0)+ ' external= ' - +str(0), font='Verdana 7') \ + tk.Label(self._frame, text='Fatigue pressure: internal= ' + str(0) + ' external= ' + + str(0), font='Verdana 7') \ .place(x=start_x + dx * 5, y=start_y + 19.3 * dy) - - self._ent_trans_stress_high.place(x=start_x+dx*8,y=start_y+11.5*dy) - self._ent_trans_stress_low.place(x=start_x+dx*8,y=start_y+12.5*dy) - self._ent_axial_stress.place(x=start_x+dx*8,y=start_y+13.5*dy) + self._ent_trans_stress_high.place(x=start_x + dx * 8, y=start_y + 11.5 * dy) + self._ent_trans_stress_low.place(x=start_x + dx * 8, y=start_y + 12.5 * dy) + self._ent_axial_stress.place(x=start_x + dx * 8, y=start_y + 13.5 * dy) self._ent_shear_stress.place(x=start_x + dx * 8, y=start_y + 14.5 * dy) self._ent_design_pressure.place(x=start_x + dx * 8, y=start_y + 15.5 * dy) self._ent_design_pressure_side.place(x=start_x + dx * 12, y=start_y + 15.5 * dy) @@ -426,9 +477,9 @@ def __init__(self,master,app=None): self._ent_width_lg.place(x=start_x + dx * 8, y=start_y + 17.5 * dy) self._ent_slamming_pressure.place(x=start_x + dx * 8, y=start_y + 18.5 * dy) - #setting default values - init_dim = float(10) #mm - init_thk = float(1) #mm + # setting default values + init_dim = float(10) # mm + init_thk = float(1) # mm self._new_delta_spacing.set(init_dim) self._new_delta_pl_thk.set(init_thk) self._new_delta_web_h.set(init_dim) @@ -447,22 +498,22 @@ def __init__(self,master,app=None): self._new_fatigue_ext_press.set(0), self._new_fatigue_int_press.set(0) else: self._new_fatigue_int_press.set(self._fatigue_pressure['p_int']), \ - self._new_fatigue_ext_press.set(self._fatigue_pressure['p_ext']) - - self._new_spacing_upper.set(round(self._spacing*1000,5)) - self._new_spacing_lower.set(round(max(self._spacing*1000,0),5)) - self._new_pl_thk_upper.set(round(self._pl_thk*1000+10,5)) - self._new_pl_thk_lower.set(round(max(self._pl_thk*1000-10,float(10)),5)) - self._new_web_h_upper.set(round(self._stf_web_h*1000+100,5)) - self._new_web_h_lower.set(round(max(self._stf_web_h*1000-100,100),5)) - self._new_web_thk_upper.set(round(self._stf_web_thk*1000+10,5)) - self._new_web_thk_lower.set(round(max(self._stf_web_thk*1000-10,float(10)),5)) + self._new_fatigue_ext_press.set(self._fatigue_pressure['p_ext']) + + self._new_spacing_upper.set(round(self._spacing * 1000, 5)) + self._new_spacing_lower.set(round(max(self._spacing * 1000, 0), 5)) + self._new_pl_thk_upper.set(round(self._pl_thk * 1000 + 10, 5)) + self._new_pl_thk_lower.set(round(max(self._pl_thk * 1000 - 10, float(10)), 5)) + self._new_web_h_upper.set(round(self._stf_web_h * 1000 + 100, 5)) + self._new_web_h_lower.set(round(max(self._stf_web_h * 1000 - 100, 100), 5)) + self._new_web_thk_upper.set(round(self._stf_web_thk * 1000 + 10, 5)) + self._new_web_thk_lower.set(round(max(self._stf_web_thk * 1000 - 10, float(10)), 5)) if self._initial_calc_obj.Stiffener is not None: if self._initial_calc_obj.Stiffener.get_stiffener_type() != 'FB': - self._new_fl_w_upper.set(min(round(self._fl_w*1000+100,5), 200)) - self._new_fl_w_lower.set(round(max(self._fl_w*1000-100,100),5)) - self._new_fl_thk_upper.set(round(self._fl_thk*1000+10,15)) - self._new_fl_thk_lower.set(round(max(self._fl_thk*1000-10,10),15)) + self._new_fl_w_upper.set(min(round(self._fl_w * 1000 + 100, 5), 200)) + self._new_fl_w_lower.set(round(max(self._fl_w * 1000 - 100, 100), 5)) + self._new_fl_thk_upper.set(round(self._fl_thk * 1000 + 10, 15)) + self._new_fl_thk_lower.set(round(max(self._fl_thk * 1000 - 10, 10), 15)) else: self._new_fl_w_upper.set(0) @@ -472,7 +523,7 @@ def __init__(self,master,app=None): self._new_pressure_side.set('p') self._new_width_lg.set(10) - self._new_span.set(round(self._initial_calc_obj.Plate.span,5)) + self._new_span.set(round(self._initial_calc_obj.Plate.span, 5)) self._new_algorithm.set('anysmart') self._new_algorithm_random_trials.set(100000) self._new_swarm_size.set(100) @@ -483,32 +534,30 @@ def __init__(self,master,app=None): self._new_minstep.set(1e-8) self._new_minfunc.set(1e-8) - - self._new_delta_spacing.trace('w',self.update_running_time) - self._new_delta_pl_thk.trace('w',self.update_running_time) - self._new_delta_web_h.trace('w',self.update_running_time) - self._new_delta_web_thk.trace('w',self.update_running_time) - self._new_delta_fl_w.trace('w',self.update_running_time) - self._new_delta_fl_thk.trace('w',self.update_running_time) - self._new_spacing_upper.trace('w',self.update_running_time) - self._new_spacing_lower.trace('w',self.update_running_time) - self._new_pl_thk_upper.trace('w',self.update_running_time) - self._new_pl_thk_lower.trace('w',self.update_running_time) - self._new_web_h_upper.trace('w',self.update_running_time) - self._new_web_h_lower.trace('w',self.update_running_time) - self._new_web_thk_upper.trace('w',self.update_running_time) - self._new_web_thk_lower.trace('w',self.update_running_time) - self._new_fl_w_upper.trace('w',self.update_running_time) - self._new_fl_w_lower.trace('w',self.update_running_time) - self._new_fl_thk_upper.trace('w',self.update_running_time) - self._new_fl_thk_lower.trace('w',self.update_running_time) - self._new_algorithm_random_trials.trace('w',self.update_running_time) - self._new_algorithm.trace('w',self.update_running_time) - - - self.running_time_per_item = {'PULS':0.2489626556016598, 'RP': 1.009943181818182e-5} - self.initial_weight = op.calc_weight([self._spacing,self._pl_thk,self._stf_web_h,self._stf_web_thk, - self._fl_w,self._fl_thk,self._new_span.get(),self._new_width_lg.get()]) + self._new_delta_spacing.trace('w', self.update_running_time) + self._new_delta_pl_thk.trace('w', self.update_running_time) + self._new_delta_web_h.trace('w', self.update_running_time) + self._new_delta_web_thk.trace('w', self.update_running_time) + self._new_delta_fl_w.trace('w', self.update_running_time) + self._new_delta_fl_thk.trace('w', self.update_running_time) + self._new_spacing_upper.trace('w', self.update_running_time) + self._new_spacing_lower.trace('w', self.update_running_time) + self._new_pl_thk_upper.trace('w', self.update_running_time) + self._new_pl_thk_lower.trace('w', self.update_running_time) + self._new_web_h_upper.trace('w', self.update_running_time) + self._new_web_h_lower.trace('w', self.update_running_time) + self._new_web_thk_upper.trace('w', self.update_running_time) + self._new_web_thk_lower.trace('w', self.update_running_time) + self._new_fl_w_upper.trace('w', self.update_running_time) + self._new_fl_w_lower.trace('w', self.update_running_time) + self._new_fl_thk_upper.trace('w', self.update_running_time) + self._new_fl_thk_lower.trace('w', self.update_running_time) + self._new_algorithm_random_trials.trace('w', self.update_running_time) + self._new_algorithm.trace('w', self.update_running_time) + + self.running_time_per_item = {'RP': 1.009943181818182e-5} + self.initial_weight = op.calc_weight([self._spacing, self._pl_thk, self._stf_web_h, self._stf_web_thk, + self._fl_w, self._fl_thk, self._new_span.get(), self._new_width_lg.get()]) img_file_name = 'img_plate_and_stiffener.gif' if os.path.isfile('images/' + img_file_name): @@ -516,40 +565,41 @@ def __init__(self,master,app=None): else: file_path = self._root_dir + '/images/' + img_file_name photo = tk.PhotoImage(file=file_path) - label = tk.Label(self._frame,image=photo) + label = tk.Label(self._frame, image=photo) label.image = photo # keep a reference! label.place(x=550, y=300) - - tk.Label(self._frame,text='Select algorithm', font = 'Verdana 8 bold').place(x=start_x+dx*11, y=start_y+0.5*dy) - self._ent_algorithm.place(x=start_x+dx*11, y=start_y+dy) + tk.Label(self._frame, text='Select algorithm', font='Verdana 8 bold').place(x=start_x + dx * 11, + y=start_y + 0.5 * dy) + self._ent_algorithm.place(x=start_x + dx * 11, y=start_y + dy) self.algorithm_random_label = tk.Label(self._frame, text='Number of trials') - tk.Button(self._frame,text='algorith information',command=self.algorithm_info,bg='white')\ - .place(x=start_x+dx*11, y=start_y+dy*2) - self.run_button = tk.Button(self._frame,text='RUN OPTIMIZATION!', command=self.run_optimizaion, bg='red', - font='Verdana 10 bold',fg='Yellow', relief="raised") - self.run_button.place(x=start_x+dx*8, y=start_y+dy*0.5, relwidth = 0.15) - self.run_results = tk.Button(self._frame,text='show calculated', command=self.plot_results, bg='white', - font='Verdana 10',fg='black') - self.run_results.place(x=start_x+dx*8, y=start_y+dy*1.5) - self._opt_actual_running_time.place(x=start_x+dx*9.5, y=start_y-dy) + tk.Button(self._frame, text='algorith information', command=self.algorithm_info, bg='white') \ + .place(x=start_x + dx * 11, y=start_y + dy * 2) + self.run_button = tk.Button(self._frame, text='RUN OPTIMIZATION!', command=self.run_optimizaion, bg='red', + font='Verdana 10 bold', fg='Yellow', relief="raised") + self.run_button.place(x=start_x + dx * 8, y=start_y + dy * 0.5, relwidth=0.15) + self.run_results = tk.Button(self._frame, text='show calculated', command=self.plot_results, bg='white', + font='Verdana 10', fg='black') + self.run_results.place(x=start_x + dx * 8, y=start_y + dy * 1.5) + self._opt_actual_running_time.place(x=start_x + dx * 9.5, y=start_y - dy) - self.close_and_save =tk.Button(self._frame,text='Return and replace initial structure with optimized', - command=self.save_and_close,bg='green',font='Verdana 10',fg='yellow') - self.close_and_save.place(x=start_x+dx*5,y=10) + self.close_and_save = tk.Button(self._frame, text='Return and replace initial structure with optimized', + command=self.save_and_close, bg='green', font='Verdana 10', fg='yellow') + self.close_and_save.place(x=start_x + dx * 5, y=10) tk.Button(self._frame, text='Open predefined stiffeners example', - command=self.open_example_file, bg='white', font='Verdana 10')\ - .place(x=start_x+dx*10,y=10) + command=self.open_example_file, bg='white', font='Verdana 10') \ + .place(x=start_x + dx * 10, y=10) # Selection of constraints self._new_check_sec_mod = tk.BooleanVar() self._new_check_min_pl_thk = tk.BooleanVar() self._new_check_shear_area = tk.BooleanVar() self._new_check_buckling = tk.BooleanVar() - self._new_check_buckling_puls = tk.BooleanVar() + self._new_check_buckling_semi_analytical = tk.BooleanVar() self._new_check_buckling_ml_cl = tk.BooleanVar() + self._new_check_buckling_ml_numeric = tk.BooleanVar() self._new_check_fatigue = tk.BooleanVar() self._new_check_slamming = tk.BooleanVar() self._new_check_local_buckling = tk.BooleanVar() @@ -562,38 +612,45 @@ def __init__(self,master,app=None): self._new_check_slamming.set(False) self._new_check_local_buckling.set(True) self._new_use_weight_filter.set(True) - self._new_check_buckling_puls.set(False) + self._new_check_buckling_semi_analytical.set(False) self._new_check_buckling_ml_cl.set(False) - self._new_check_buckling_puls.trace('w', self.update_running_time) + self._new_check_buckling_ml_numeric.set(False) + self._new_check_buckling_semi_analytical.trace('w', self.update_running_time) self._new_check_buckling_ml_cl.trace('w', self.update_running_time) - + self._new_check_buckling_ml_numeric.trace('w', self.update_running_time) start_y = 140 - tk.Label(self._frame,text='Check for minimum section mofdulus').place(x=start_x+dx*9.7,y=start_y+4*dy) - tk.Label(self._frame, text='Check for minimum plate thk.').place(x=start_x+dx*9.7,y=start_y+5*dy) - tk.Label(self._frame, text='Check for minimum shear area').place(x=start_x+dx*9.7,y=start_y+6*dy) - tk.Label(self._frame, text='Check for buckling (RP-C201)').place(x=start_x+dx*9.7,y=start_y+7*dy) + tk.Label(self._frame, text='Check for minimum section mofdulus').place(x=start_x + dx * 9.7, y=start_y + 4 * dy) + tk.Label(self._frame, text='Check for minimum plate thk.').place(x=start_x + dx * 9.7, y=start_y + 5 * dy) + tk.Label(self._frame, text='Check for minimum shear area').place(x=start_x + dx * 9.7, y=start_y + 6 * dy) + tk.Label(self._frame, text='Check for buckling (RP-C201)').place(x=start_x + dx * 9.7, y=start_y + 7 * dy) tk.Label(self._frame, text='Check for fatigue (RP-C203)').place(x=start_x + dx * 9.7, y=start_y + 8 * dy) tk.Label(self._frame, text='Check for bow slamming').place(x=start_x + dx * 9.7, y=start_y + 9 * dy) tk.Label(self._frame, text='Check for local stf. buckling').place(x=start_x + dx * 9.7, y=start_y + 10 * dy) tk.Label(self._frame, text='Use weight filter (for speed)').place(x=start_x + dx * 9.7, y=start_y + 11 * dy) - tk.Label(self._frame, text='Check for buckling (PULS)').place(x=start_x + dx * 9.7, y=start_y + 12 * dy) - tk.Label(self._frame, text='Check for buckling (ML-CL)').place(x=start_x + dx * 9.7, y=start_y + 13 * dy) - - tk.Checkbutton(self._frame,variable=self._new_check_sec_mod).place(x=start_x+dx*12,y=start_y+4*dy) - tk.Checkbutton(self._frame, variable=self._new_check_min_pl_thk).place(x=start_x+dx*12,y=start_y+5*dy) - tk.Checkbutton(self._frame, variable=self._new_check_shear_area).place(x=start_x+dx*12,y=start_y+6*dy) - tk.Checkbutton(self._frame, variable=self._new_check_buckling).place(x=start_x+dx*12,y=start_y+7*dy) + tk.Label(self._frame, text='Check for buckling (SemiAnalytical S3/U3)').place(x=start_x + dx * 9.7, + y=start_y + 12 * dy) + tk.Label(self._frame, text='Check for buckling (ML-CL deactivated)').place(x=start_x + dx * 9.7, y=start_y + 13 * dy) + tk.Label(self._frame, text='Check for buckling (ML-Numeric)').place(x=start_x + dx * 9.7, y=start_y + 14 * dy) + + tk.Checkbutton(self._frame, variable=self._new_check_sec_mod).place(x=start_x + dx * 12, y=start_y + 4 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_min_pl_thk).place(x=start_x + dx * 12, y=start_y + 5 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_shear_area).place(x=start_x + dx * 12, y=start_y + 6 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_buckling).place(x=start_x + dx * 12, y=start_y + 7 * dy) tk.Checkbutton(self._frame, variable=self._new_check_fatigue).place(x=start_x + dx * 12, y=start_y + 8 * dy) tk.Checkbutton(self._frame, variable=self._new_check_slamming).place(x=start_x + dx * 12, y=start_y + 9 * dy) tk.Checkbutton(self._frame, variable=self._new_check_local_buckling).place(x=start_x + dx * 12, y=start_y + 10 * dy) tk.Checkbutton(self._frame, variable=self._new_use_weight_filter).place(x=start_x + dx * 12, - y=start_y + 11 * dy) - tk.Checkbutton(self._frame, variable=self._new_check_buckling_puls).place(x=start_x + dx * 12, - y=start_y + 12 * dy) - tk.Checkbutton(self._frame, variable=self._new_check_buckling_ml_cl).place(x=start_x + dx * 12, - y=start_y + 13 * dy) + y=start_y + 11 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_buckling_semi_analytical).place(x=start_x + dx * 12, + y=start_y + 12 * dy) + tk.Checkbutton(self._frame, variable=self._new_check_buckling_ml_cl, state='disabled').place( + x=start_x + dx * 12, + y=start_y + 13 * dy, + ) + tk.Checkbutton(self._frame, variable=self._new_check_buckling_ml_numeric).place(x=start_x + dx * 12, + y=start_y + 14 * dy) # Stress scaling self._new_fup = tk.DoubleVar() @@ -601,31 +658,31 @@ def __init__(self,master,app=None): self._new_fdwn = tk.DoubleVar() self._new_fdwn.set(1) - tk.Label(self._frame, text='Factor when scaling stresses up, fup')\ + tk.Label(self._frame, text='Factor when scaling stresses up, fup') \ .place(x=start_x + dx * 9.7, y=start_y + 16 * dy) - ent_fup = tk.Entry(self._frame, textvariable=self._new_fup, width = 10) + ent_fup = tk.Entry(self._frame, textvariable=self._new_fup, width=10) ent_fup.place(x=start_x + dx * 12, y=start_y + 16 * dy) - tk.Label(self._frame, text='Factor when scaling stresses up, fdown')\ + tk.Label(self._frame, text='Factor when scaling stresses up, fdown') \ .place(x=start_x + dx * 9.7, y=start_y + 17 * dy) - ent_fdwn = tk.Entry(self._frame, textvariable=self._new_fdwn, width = 10) + ent_fdwn = tk.Entry(self._frame, textvariable=self._new_fdwn, width=10) ent_fdwn.place(x=start_x + dx * 12, y=start_y + 17 * dy) # tk.Button(self._frame,text='Iterate predefiened stiffeners',command=self.open_multiple_files ,bg='yellow')\ # .place(x=start_x, y=start_y - dy * 2) self._toggle_btn = tk.Button(self._frame, text="Iterate predefiened stiffeners", relief="raised", - command=self.toggle, bg = 'salmon') + command=self.toggle, bg='salmon') self._toggle_btn.place(x=start_x, y=start_y - dy * 2) self._toggle_object, self._filez = self._initial_calc_obj, None self.draw_properties() self.update_running_time() - def selected_algorithm(self,event): + def selected_algorithm(self, event): ''' Action when selecting an algorithm. :return: ''' start_x, start_y, dx, dy = 20, 100, 100, 40 - if self._new_algorithm.get()=='random' or self._new_algorithm.get()=='random_no_delta': + if self._new_algorithm.get() == 'random' or self._new_algorithm.get() == 'random_no_delta': self._ent_random_trials.place_forget() self.algorithm_random_label.place_forget() self._lb_swarm_size.place_forget() @@ -642,9 +699,9 @@ def selected_algorithm(self,event): self._ent_maxiter.place_forget() self._ent_minstep.place_forget() self._ent_minfunc.place_forget() - self._ent_random_trials.place(x=start_x+dx*11.3, y=start_y+1.2*dy) - self.algorithm_random_label.place(x=start_x+dx*11.3, y=start_y+0.5*dy) - elif self._new_algorithm.get()=='anysmart' or self._new_algorithm.get()=='anydetail': + self._ent_random_trials.place(x=start_x + dx * 11.3, y=start_y + 1.2 * dy) + self.algorithm_random_label.place(x=start_x + dx * 11.3, y=start_y + 0.5 * dy) + elif self._new_algorithm.get() == 'anysmart' or self._new_algorithm.get() == 'anydetail': self._ent_random_trials.place_forget() self.algorithm_random_label.place_forget() self._lb_swarm_size.place_forget() @@ -662,38 +719,107 @@ def selected_algorithm(self,event): self._ent_minstep.place_forget() self._ent_minfunc.place_forget() - elif self._new_algorithm.get()=='pso': - y_place_label =11.2 + elif self._new_algorithm.get() == 'pso': + y_place_label = 11.2 y_place = 12.2 self._ent_random_trials.place_forget() - - self._lb_swarm_size.place(x=start_x+dx*y_place_label, y=start_y-2*dy) - self._lb_omega.place(x=start_x+dx*y_place_label, y=start_y-1*dy) - self._lb_phip.place(x=start_x+dx*y_place_label, y=start_y-0*dy) - self._lb_phig.place(x=start_x+dx*y_place_label, y=start_y+1*dy) - self._lb_maxiter.place(x=start_x+dx*y_place_label, y=start_y+2*dy) - self._lb_minstep.place(x=start_x+dx*y_place_label, y=start_y+3*dy) - self._lb_minfunc.place(x=start_x+dx*y_place_label, y=start_y+4*dy) - - self._ent_swarm_size.place(x=start_x+dx*y_place, y=start_y-2*dy) - self._ent_omega.place(x=start_x+dx*y_place, y=start_y-1*dy) - self._ent_phip.place(x=start_x+dx*y_place, y=start_y+0*dy) - self._ent_phig.place(x=start_x+dx*y_place, y=start_y+1*dy) - self._ent_maxiter.place(x=start_x+dx*y_place, y=start_y+2*dy) - self._ent_minstep.place(x=start_x+dx*y_place, y=start_y+3*dy) - self._ent_minfunc.place(x=start_x+dx*y_place, y=start_y+4*dy) + + self._lb_swarm_size.place(x=start_x + dx * y_place_label, y=start_y - 2 * dy) + self._lb_omega.place(x=start_x + dx * y_place_label, y=start_y - 1 * dy) + self._lb_phip.place(x=start_x + dx * y_place_label, y=start_y - 0 * dy) + self._lb_phig.place(x=start_x + dx * y_place_label, y=start_y + 1 * dy) + self._lb_maxiter.place(x=start_x + dx * y_place_label, y=start_y + 2 * dy) + self._lb_minstep.place(x=start_x + dx * y_place_label, y=start_y + 3 * dy) + self._lb_minfunc.place(x=start_x + dx * y_place_label, y=start_y + 4 * dy) + + self._ent_swarm_size.place(x=start_x + dx * y_place, y=start_y - 2 * dy) + self._ent_omega.place(x=start_x + dx * y_place, y=start_y - 1 * dy) + self._ent_phip.place(x=start_x + dx * y_place, y=start_y + 0 * dy) + self._ent_phig.place(x=start_x + dx * y_place, y=start_y + 1 * dy) + self._ent_maxiter.place(x=start_x + dx * y_place, y=start_y + 2 * dy) + self._ent_minstep.place(x=start_x + dx * y_place, y=start_y + 3 * dy) + self._ent_minfunc.place(x=start_x + dx * y_place, y=start_y + 4 * dy) def modify_structure_object(self): ''' Chaning parameters in the structure object before running. ''' pass + def _get_material_factor_for_optimization(self): + ''' + Returns the material factor used to select ML models and scale numeric UF. + + In normal GUI usage this follows the main application's material-factor selector. + In standalone/test usage it falls back to the current calculation object's + material factor. + ''' + try: + return float(self.app._new_material_factor.get()) + except Exception: + try: + return float(self._initial_calc_obj.Plate.mat_factor) + except Exception: + return 1.15 + + def _get_ml_algo_for_optimization(self): + ''' + The main application may store ML models either as a flat dictionary: + self._ML_buckling['cl SP buc int predictor'] + + or as a material-factor dictionary: + self._ML_buckling[1.15]['cl SP buc int predictor'] + + The optimization code expects the flat dictionary for the selected material + factor, so this helper returns the correct form. + ''' + mat_fac = self._get_material_factor_for_optimization() + + try: + if mat_fac in self._ML_buckling and isinstance(self._ML_buckling[mat_fac], dict): + return self._ML_buckling[mat_fac] + except Exception: + pass + + try: + mat_fac_key = str(mat_fac) + if mat_fac_key in self._ML_buckling and isinstance(self._ML_buckling[mat_fac_key], dict): + return self._ML_buckling[mat_fac_key] + except Exception: + pass + + return self._ML_buckling + + def _ensure_single_buckling_check(self): + ''' + Only one buckling formulation should be active at a time: + RP-C201, SemiAnalytical S3/U3, or ML-Numeric. + ''' + selected = [ + self._new_check_buckling.get(), + self._new_check_buckling_semi_analytical.get(), + False, + self._new_check_buckling_ml_numeric.get(), + ] + + if selected.count(True) > 1: + tk.messagebox.showerror('You can only select one buckling type. Reselect.') + + if self._new_check_buckling.get(): + self._new_check_buckling.set(False) + self._new_check_local_buckling.set(False) + + if self._new_check_buckling_semi_analytical.get(): + self._new_check_buckling_semi_analytical.set(False) + + if self._new_check_buckling_ml_numeric.get(): + self._new_check_buckling_ml_numeric.set(False) + def run_optimizaion(self): ''' function for button :return: ''' - self.run_button.config(bg = 'white') + self.run_button.config(bg='white') self.run_button.config(fg='red') self.run_button.config(text='RUNNING OPTIMIZATION') self.run_button.config(relief="sunken") @@ -701,86 +827,73 @@ def run_optimizaion(self): self._opt_actual_running_time.update() t_start = time.time() self._opt_results, self._opt_runned = (), False - if self._PULS_object is not None: - puls_sheet_location = self._PULS_object.puls_sheet_location - puls_acceptance = self._puls_acceptance - if self._new_check_buckling_puls.get() == True: - if puls_sheet_location is None or not os.path.isfile( - puls_sheet_location): - tk.messagebox.showerror('No PULS excel sheet located', 'Set location of PULS excel sheet.\n' - 'Note that PULS excel may require 32 bit ' - 'office.\n\n' - 'A sheet may be provided but does not exist' - ' in :\n' - + self._PULS_results.puls_sheet_location + - '\n\n Return to main window an run one or more lines in PULS.') - else: - puls_sheet_location = None - puls_acceptance =0.87 - - - self.pso_parameters = (self._new_swarm_size.get(),self._new_omega.get(),self._new_phip.get(), + self.pso_parameters = (self._new_swarm_size.get(), self._new_omega.get(), self._new_phip.get(), self._new_phig.get(), - self._new_maxiter.get(),self._new_minstep.get(),self._new_minfunc.get()) + self._new_maxiter.get(), self._new_minstep.get(), self._new_minfunc.get()) - contraints = (self._new_check_sec_mod.get(),self._new_check_min_pl_thk.get(), + contraints = (self._new_check_sec_mod.get(), self._new_check_min_pl_thk.get(), self._new_check_shear_area.get(), self._new_check_buckling.get(), self._new_check_fatigue.get(), self._new_check_slamming.get(), - self._new_check_local_buckling.get(), self._new_check_buckling_puls.get(), - self._new_check_buckling_ml_cl.get(), False) + self._new_check_local_buckling.get(), self._new_check_buckling_semi_analytical.get(), + False, self._new_check_buckling_ml_numeric.get()) self._initial_calc_obj.Plate.set_span(self._new_span.get()) + selected_mat_fac = self._get_material_factor_for_optimization() + self._initial_calc_obj.Plate.mat_factor = selected_mat_fac + if self._initial_calc_obj.Stiffener is not None: + self._initial_calc_obj.Stiffener.mat_factor = selected_mat_fac + if self._initial_calc_obj.Girder is not None: + self._initial_calc_obj.Girder.mat_factor = selected_mat_fac if self._fatigue_pressure is not None: - fat_press = ((self._fatigue_pressure['p_ext']['loaded'],self._fatigue_pressure['p_ext']['ballast'], + fat_press = ((self._fatigue_pressure['p_ext']['loaded'], self._fatigue_pressure['p_ext']['ballast'], self._fatigue_pressure['p_ext']['part']), - (self._fatigue_pressure['p_int']['loaded'],self._fatigue_pressure['p_int']['ballast'], + (self._fatigue_pressure['p_int']['loaded'], self._fatigue_pressure['p_int']['ballast'], self._fatigue_pressure['p_int']['part'])) else: fat_press = None - self._opt_results= op.run_optmizataion(self._initial_calc_obj,self.get_lower_bounds(), - self.get_upper_bounds(),self._new_design_pressure.get(), - self.get_deltas(),algorithm=self._new_algorithm.get(), - trials=self._new_algorithm_random_trials.get(), - side=self._new_pressure_side.get(), - const_chk=contraints,pso_options = self.pso_parameters, - fatigue_obj=self._fatigue_object, - fat_press_ext_int=fat_press, - slamming_press = self._new_slamming_pressure.get(), - predefined_stiffener_iter=self._predefined_stiffener_iter, - processes=self._new_processes.get(), - use_weight_filter = False if self._new_check_buckling_puls.get() - else self._new_use_weight_filter.get(), - puls_sheet = puls_sheet_location, puls_acceptance = puls_acceptance, - fdwn = self._new_fdwn.get(), fup = self._new_fdwn.get(), - ml_algo= self._ML_buckling) + self._opt_results = op.run_optmizataion(self._initial_calc_obj, self.get_lower_bounds(), + self.get_upper_bounds(), self._new_design_pressure.get(), + self.get_deltas(), algorithm=self._new_algorithm.get(), + trials=self._new_algorithm_random_trials.get(), + side=self._new_pressure_side.get(), + const_chk=contraints, pso_options=self.pso_parameters, + fatigue_obj=self._fatigue_object, + fat_press_ext_int=fat_press, + slamming_press=self._new_slamming_pressure.get(), + predefined_stiffener_iter=self._predefined_stiffener_iter, + processes=self._new_processes.get(), + use_weight_filter=self._new_use_weight_filter.get(), + fdwn=self._new_fdwn.get(), fup=self._new_fup.get(), + ml_algo=self._get_ml_algo_for_optimization(), + material_factor=selected_mat_fac) if self._opt_results is not None and self._opt_results[0] is not None: self._opt_actual_running_time.config(text='Actual running time: \n' - +str(round((time.time()-t_start)/60,4))+' min') + + str(round((time.time() - t_start) / 60, 4)) + ' min') self._opt_actual_running_time.update() self._opt_runned = True if self._opt_results[0].Stiffener is not None: - text = 'Optimization result | Spacing: ' + str(round(self._opt_results[0].Plate.get_s(), 10) * 1000) +\ - ' Plate thickness: ' + str(round(self._opt_results[0].Plate.get_pl_thk() * 1000, 10)) +\ - ' Stiffener - T' + str(round(self._opt_results[0].Stiffener.get_web_h() * 1000, 10)) + 'x'\ - +str(round(self._opt_results[0].Stiffener.get_web_thk() * 1000, 10)) +\ - '+' + str(round(self._opt_results[0].Stiffener.get_fl_w() * 1000, 10)) + 'x'\ - +str(round(self._opt_results[0].Stiffener.get_fl_thk() * 1000, 10)) + text = 'Optimization result | Spacing: ' + str(round(self._opt_results[0].Plate.get_s(), 10) * 1000) + \ + ' Plate thickness: ' + str(round(self._opt_results[0].Plate.get_pl_thk() * 1000, 10)) + \ + ' Stiffener - T' + str(round(self._opt_results[0].Stiffener.get_web_h() * 1000, 10)) + 'x' \ + + str(round(self._opt_results[0].Stiffener.get_web_thk() * 1000, 10)) + \ + '+' + str(round(self._opt_results[0].Stiffener.get_fl_w() * 1000, 10)) + 'x' \ + + str(round(self._opt_results[0].Stiffener.get_fl_thk() * 1000, 10)) else: - text = 'Optimization result | Spacing: ' + str(round(self._opt_results[0].Plate.get_s(), 10) * 1000) +\ - ' Plate thickness: ' + str(round(self._opt_results[0].Plate.get_pl_thk() * 1000, 10)) + text = 'Optimization result | Spacing: ' + str(round(self._opt_results[0].Plate.get_s(), 10) * 1000) + \ + ' Plate thickness: ' + str(round(self._opt_results[0].Plate.get_pl_thk() * 1000, 10)) - self._result_label.config(text= text) + self._result_label.config(text=text) - self._new_opt_spacing.set(round(self._opt_results[0].Plate.get_s(),5)) - self._new_opt_pl_thk.set(round(self._opt_results[0].Plate.get_pl_thk(),5)) + self._new_opt_spacing.set(round(self._opt_results[0].Plate.get_s(), 5)) + self._new_opt_pl_thk.set(round(self._opt_results[0].Plate.get_pl_thk(), 5)) if self._opt_results[0].Stiffener is not None: - self._new_opt_web_h.set(round(self._opt_results[0].Stiffener.get_web_h(),5)) - self._new_opt_web_thk.set(round(self._opt_results[0].Stiffener.get_web_thk(),5)) - self._new_opt_fl_w.set(round(self._opt_results[0].Stiffener.get_fl_w(),5)) - self._new_opt_fl_thk.set(round(self._opt_results[0].Stiffener.get_fl_thk(),5)) + self._new_opt_web_h.set(round(self._opt_results[0].Stiffener.get_web_h(), 5)) + self._new_opt_web_thk.set(round(self._opt_results[0].Stiffener.get_web_thk(), 5)) + self._new_opt_fl_w.set(round(self._opt_results[0].Stiffener.get_fl_w(), 5)) + self._new_opt_fl_thk.set(round(self._opt_results[0].Stiffener.get_fl_thk(), 5)) self.draw_properties() else: messagebox.showinfo(title='Nothing found', message='No better alternatives found. Modify input.\n' @@ -797,104 +910,88 @@ def get_running_time(self): :return: ''' - if self._new_algorithm.get() in ['anysmart','anydetail']: + if self._new_algorithm.get() in ['anysmart', 'anydetail']: all_combs = op.any_get_all_combs(self.get_lower_bounds(), self.get_upper_bounds(), self.get_deltas(), predef_stiffeners=None if self._predefined_stiffener_iter is None else [item.get_tuple() for item in self._predefined_stiffener_iter]) number_of_combinations = len([val for val in all_combs]) - return int(number_of_combinations * self.running_time_per_item['PULS' if self._new_check_buckling_puls.get() - else 'RP']), number_of_combinations + return int(number_of_combinations * self.running_time_per_item['RP']), number_of_combinations - elif self._new_algorithm.get() in ['pso','random','random_no_delta']: + elif self._new_algorithm.get() in ['pso', 'random', 'random_no_delta']: try: number_of_combinations = \ - max((self._new_spacing_upper.get()-self._new_spacing_lower.get())/self._new_delta_spacing.get(),1)* \ - max((self._new_pl_thk_upper.get()-self._new_pl_thk_lower.get())/self._new_delta_pl_thk.get(),1)*\ - max((self._new_web_h_upper.get()-self._new_web_h_lower.get())/self._new_delta_web_h.get(),1)*\ - max((self._new_web_thk_upper.get()-self._new_web_thk_lower.get())/self._new_delta_web_thk.get(),1)*\ - max((self._new_fl_w_upper.get()-self._new_fl_w_lower.get())/self._new_delta_fl_w.get(),1)*\ - max((self._new_fl_thk_upper.get()-self._new_fl_thk_lower.get())/self._new_delta_fl_thk.get(),1) - return int(number_of_combinations*self.running_time_per_item['PULS' if self._new_check_buckling_puls.get() - else 'RP']),number_of_combinations + max((self._new_spacing_upper.get() - self._new_spacing_lower.get()) / self._new_delta_spacing.get(), + 1) * \ + max((self._new_pl_thk_upper.get() - self._new_pl_thk_lower.get()) / self._new_delta_pl_thk.get(), + 1) * \ + max((self._new_web_h_upper.get() - self._new_web_h_lower.get()) / self._new_delta_web_h.get(), 1) * \ + max((self._new_web_thk_upper.get() - self._new_web_thk_lower.get()) / self._new_delta_web_thk.get(), + 1) * \ + max((self._new_fl_w_upper.get() - self._new_fl_w_lower.get()) / self._new_delta_fl_w.get(), 1) * \ + max((self._new_fl_thk_upper.get() - self._new_fl_thk_lower.get()) / self._new_delta_fl_thk.get(), 1) + return int(number_of_combinations * self.running_time_per_item['RP']), number_of_combinations except TclError: - return 0,0 + return 0, 0 else: try: - return int(self._new_algorithm_random_trials.get() * self.running_time_per_item['PULS' if self._new_check_buckling_puls.get() - else 'RP']),\ - self._new_algorithm_random_trials.get() + return int(self._new_algorithm_random_trials.get() * self.running_time_per_item['RP']), \ + self._new_algorithm_random_trials.get() except TclError: - return 0,0 + return 0, 0 def get_deltas(self): ''' Return a numpy array of the deltas. :return: ''' - return np.array([float(self._new_delta_spacing.get())/1000,float(self._new_delta_pl_thk.get())/1000, - float(self._new_delta_web_h.get())/1000,float(self._new_delta_web_thk.get())/1000, - float(self._new_delta_fl_w.get())/1000,float(self._new_delta_fl_thk.get())/1000]) + return np.array([float(self._new_delta_spacing.get()) / 1000, float(self._new_delta_pl_thk.get()) / 1000, + float(self._new_delta_web_h.get()) / 1000, float(self._new_delta_web_thk.get()) / 1000, + float(self._new_delta_fl_w.get()) / 1000, float(self._new_delta_fl_thk.get()) / 1000]) - def update_running_time(self,*args): + def update_running_time(self, *args): ''' Estimate the running time of the algorithm. :return: ''' try: - self._runnig_time_label.config(text=str(int(self.get_running_time()[1])) + ' (about '+ - str(max(round(self.get_running_time()[1]*self.running_time_per_item['PULS' - if self._new_check_buckling_puls.get() - else 'RP']/60,2), 0.1))+ ' min.)') + self._runnig_time_label.config(text=str(int(self.get_running_time()[1])) + ' (about ' + + str(max(round(self.get_running_time()[1] * + self.running_time_per_item['RP'] / 60, 2), 0.1)) + + ' min.)') except (ZeroDivisionError, TclError): - pass# _tkinter.TclError: pass + pass # _tkinter.TclError: pass - if [self._new_check_buckling_ml_cl.get(),self._new_check_buckling_puls.get(), - self._new_check_buckling.get()].count(True) > 1: - tk.messagebox.showerror('You can only select one buckling type. Reselect.') - if self._new_check_buckling_puls.get(): - self._new_check_buckling_puls.set(False) - if self._new_check_buckling_ml_cl.get(): - self._new_check_buckling_ml_cl.set(False) - if self._new_check_buckling.get(): - self._new_check_buckling.set(False) - self._new_check_local_buckling.set(False) + self._ensure_single_buckling_check() - - if self._new_check_buckling_puls.get(): - if self._PULS_object is None or self._PULS_object.puls_sheet_location is None: - tk.messagebox.showerror('Missing PULS sheet', 'Go back to main window and set a PULS sheet location\n' - 'by running one or more lines.') - self._new_check_buckling_puls.set(False) - def get_upper_bounds(self): ''' Return an numpy array of upper bounds. - :return: + :return: ''' - return np.array([self._new_spacing_upper.get()/1000,self._new_pl_thk_upper.get()/1000, - self._new_web_h_upper.get()/1000,self._new_web_thk_upper.get()/1000, - self._new_fl_w_upper.get()/1000,self._new_fl_thk_upper.get()/1000, - self._new_span.get(),self._new_width_lg.get()]) - + return np.array([self._new_spacing_upper.get() / 1000, self._new_pl_thk_upper.get() / 1000, + self._new_web_h_upper.get() / 1000, self._new_web_thk_upper.get() / 1000, + self._new_fl_w_upper.get() / 1000, self._new_fl_thk_upper.get() / 1000, + self._new_span.get(), self._new_width_lg.get()]) + def get_lower_bounds(self): ''' Return an numpy array of lower bounds. - :return: + :return: ''' - return np.array([self._new_spacing_lower.get()/1000,self._new_pl_thk_lower.get()/1000, - self._new_web_h_lower.get()/1000,self._new_web_thk_lower.get()/1000, - self._new_fl_w_lower.get()/1000,self._new_fl_thk_lower.get()/1000, + return np.array([self._new_spacing_lower.get() / 1000, self._new_pl_thk_lower.get() / 1000, + self._new_web_h_lower.get() / 1000, self._new_web_thk_lower.get() / 1000, + self._new_fl_w_lower.get() / 1000, self._new_fl_thk_lower.get() / 1000, self._new_span.get(), self._new_width_lg.get()]) - def checkered(self,line_distance): + def checkered(self, line_distance): # vertical lines at an interval of "line_distance" pixel for x in range(line_distance, self._canvas_dim[0], line_distance): - self._canvas_opt.create_line(x, 0, x, self._canvas_dim[0], fill="grey",stipple='gray50') + self._canvas_opt.create_line(x, 0, x, self._canvas_dim[0], fill="grey", stipple='gray50') # horizontal lines at an interval of "line_distance" pixel for y in range(line_distance, self._canvas_dim[1], line_distance): - self._canvas_opt.create_line(0, y, self._canvas_dim[0], y, fill="grey",stipple='gray50') + self._canvas_opt.create_line(0, y, self._canvas_dim[0], y, fill="grey", stipple='gray50') def draw_properties(self): ''' @@ -903,82 +1000,104 @@ def draw_properties(self): ''' self._canvas_opt.delete('all') self.checkered(10) - ctr_x = self._canvas_dim[0]/2 - ctr_y = self._canvas_dim[1]/2+200 + ctr_x = self._canvas_dim[0] / 2 + ctr_y = self._canvas_dim[1] / 2 + 200 m = self._draw_scale - init_color,init_stipple = 'blue','gray12' - opt_color,opt_stippe = 'red','gray12' - self._canvas_opt.create_rectangle(0,0,self._canvas_dim[0]+10,80,fill='white') - self._canvas_opt.create_line(10,10,30,10,fill = init_color,width=5) - self._canvas_opt.create_text(270,10,text='Initial - Pl.: '+str(self._spacing*1000) +'x'+str(self._pl_thk*1000)+ - ' Stf.: '+str(self._stf_web_h*1000)+'x'+str(self._stf_web_thk*1000)+'+'+ - str(self._fl_w*1000)+'x'+str(self._fl_thk*1000), font = 'Verdana 8', - fill = init_color) - self._canvas_opt.create_text(120,30,text='Weight (per Lg width): '+str(int(self.initial_weight)), - font = 'Verdana 8',fill = init_color) - - self._canvas_opt.create_rectangle(ctr_x-m*self._spacing/2, ctr_y,ctr_x+m*self._spacing/2, - ctr_y-m*self._pl_thk, fill=init_color, stipple=init_stipple ) - self._canvas_opt.create_rectangle(ctr_x - m * self._stf_web_thk / 2, ctr_y-m* self._pl_thk, - ctr_x + m * self._stf_web_thk / 2, ctr_y - m *(self._stf_web_h+self._pl_thk) - , fill=init_color, stipple=init_stipple ) + init_color, init_stipple = 'blue', 'gray12' + opt_color, opt_stippe = 'red', 'gray12' + self._canvas_opt.create_rectangle(0, 0, self._canvas_dim[0] + 10, 80, fill='white') + self._canvas_opt.create_line(10, 10, 30, 10, fill=init_color, width=5) + self._canvas_opt.create_text(270, 10, text='Initial - Pl.: ' + str(self._spacing * 1000) + 'x' + str( + self._pl_thk * 1000) + + ' Stf.: ' + str(self._stf_web_h * 1000) + 'x' + str( + self._stf_web_thk * 1000) + '+' + + str(self._fl_w * 1000) + 'x' + str(self._fl_thk * 1000), + font='Verdana 8', + fill=init_color) + self._canvas_opt.create_text(120, 30, text='Weight (per Lg width): ' + str(int(self.initial_weight)), + font='Verdana 8', fill=init_color) + + self._canvas_opt.create_rectangle(ctr_x - m * self._spacing / 2, ctr_y, ctr_x + m * self._spacing / 2, + ctr_y - m * self._pl_thk, fill=init_color, stipple=init_stipple) + self._canvas_opt.create_rectangle(ctr_x - m * self._stf_web_thk / 2, ctr_y - m * self._pl_thk, + ctr_x + m * self._stf_web_thk / 2, + ctr_y - m * (self._stf_web_h + self._pl_thk) + , fill=init_color, stipple=init_stipple) if self._initial_calc_obj.Stiffener is None: return if self._initial_calc_obj.Stiffener.get_stiffener_type() not in ['L', 'L-bulb']: - self._canvas_opt.create_rectangle(ctr_x-m*self._fl_w/2, ctr_y-m*(self._pl_thk+self._stf_web_h), - ctr_x+m*self._fl_w/2, ctr_y-m*(self._pl_thk+self._stf_web_h+self._fl_thk), - fill=init_color, stipple=init_stipple) + self._canvas_opt.create_rectangle(ctr_x - m * self._fl_w / 2, ctr_y - m * (self._pl_thk + self._stf_web_h), + ctr_x + m * self._fl_w / 2, + ctr_y - m * (self._pl_thk + self._stf_web_h + self._fl_thk), + fill=init_color, stipple=init_stipple) else: - self._canvas_opt.create_rectangle(ctr_x-m*self._stf_web_thk/2, ctr_y-m*(self._pl_thk+self._stf_web_h), - ctr_x+m*self._fl_w, ctr_y-m*(self._pl_thk+self._stf_web_h+self._fl_thk), - fill=init_color, stipple=init_stipple) + self._canvas_opt.create_rectangle(ctr_x - m * self._stf_web_thk / 2, + ctr_y - m * (self._pl_thk + self._stf_web_h), + ctr_x + m * self._fl_w, + ctr_y - m * (self._pl_thk + self._stf_web_h + self._fl_thk), + fill=init_color, stipple=init_stipple) if self._opt_runned: self._canvas_opt.create_rectangle(ctr_x - m * self._opt_results[0].Stiffener.get_s() / 2, ctr_y, - ctr_x + m * self._opt_results[0].Stiffener.get_s() / 2, - ctr_y - m * self._opt_results[0].Stiffener.get_pl_thk(), fill=opt_color, - stipple=opt_stippe) + ctr_x + m * self._opt_results[0].Stiffener.get_s() / 2, + ctr_y - m * self._opt_results[0].Stiffener.get_pl_thk(), fill=opt_color, + stipple=opt_stippe) self._canvas_opt.create_rectangle(ctr_x - m * self._opt_results[0].Stiffener.get_web_thk() / 2, ctr_y - - m * self._opt_results[0].Stiffener.get_pl_thk(), - ctr_x + m * self._opt_results[0].Stiffener.get_web_thk() / 2, - ctr_y - m * (self._opt_results[0].Stiffener.get_web_h() + self._opt_results[0].Stiffener.get_pl_thk()) - , fill=opt_color, stipple=opt_stippe) + m * self._opt_results[0].Stiffener.get_pl_thk(), + ctr_x + m * self._opt_results[0].Stiffener.get_web_thk() / 2, + ctr_y - m * (self._opt_results[0].Stiffener.get_web_h() + + self._opt_results[0].Stiffener.get_pl_thk()) + , fill=opt_color, stipple=opt_stippe) if self._opt_results[0].Stiffener.get_stiffener_type() not in ['L', 'L-bulb']: self._canvas_opt.create_rectangle(ctr_x - m * self._opt_results[0].Stiffener.get_fl_w() / 2, ctr_y - - m * (self._opt_results[0].Stiffener.get_pl_thk()+ self._opt_results[0].Stiffener.get_web_h()), - ctr_x + m * self._opt_results[0].Stiffener.get_fl_w() / 2,ctr_y - - m * (self._opt_results[0].Stiffener.get_pl_thk() + self._opt_results[0].Stiffener.get_web_h() + - self._opt_results[0].Stiffener.get_fl_thk()), - fill=opt_color, stipple=opt_stippe) + - m * (self._opt_results[0].Stiffener.get_pl_thk() + + self._opt_results[0].Stiffener.get_web_h()), + ctr_x + m * self._opt_results[0].Stiffener.get_fl_w() / 2, ctr_y - + m * (self._opt_results[0].Stiffener.get_pl_thk() + self._opt_results[ + 0].Stiffener.get_web_h() + + self._opt_results[0].Stiffener.get_fl_thk()), + fill=opt_color, stipple=opt_stippe) else: self._canvas_opt.create_rectangle(ctr_x - m * self._opt_results[0].Stiffener.get_web_thk() / 2, ctr_y - - m * (self._opt_results[0].Stiffener.get_pl_thk()+ self._opt_results[0].Stiffener.get_web_h()), - ctr_x + m * self._opt_results[0].Stiffener.get_fl_w() ,ctr_y - - m * (self._opt_results[0].Stiffener.get_pl_thk() + self._opt_results[0].Stiffener.get_web_h() + - self._opt_results[0].Stiffener.get_fl_thk()), - fill=opt_color, stipple=opt_stippe) + - m * (self._opt_results[0].Stiffener.get_pl_thk() + + self._opt_results[0].Stiffener.get_web_h()), + ctr_x + m * self._opt_results[0].Stiffener.get_fl_w(), ctr_y - + m * (self._opt_results[0].Stiffener.get_pl_thk() + self._opt_results[ + 0].Stiffener.get_web_h() + + self._opt_results[0].Stiffener.get_fl_thk()), + fill=opt_color, stipple=opt_stippe) self._canvas_opt.create_line(10, 50, 30, 50, fill=opt_color, width=5) - self._canvas_opt.create_text(270,50,text='Optimized - Pl.: '+str(round(self._opt_results[0].Stiffener.get_s()*1000,1)) - +'x'+ str(round(self._opt_results[0].Stiffener.get_pl_thk()*1000,1))+ - ' Stf.: '+str(round(self._opt_results[0].Stiffener.get_web_h()*1000,1))+ - 'x'+str(round(self._opt_results[0].Stiffener.get_web_thk()*1000,1))+'+'+ - str(round(self._opt_results[0].Stiffener.get_fl_w()*1000,1))+ - 'x'+str(round(self._opt_results[0].Stiffener.get_fl_thk()*1000,1)), - font = 'Verdana 8',fill = opt_color) + self._canvas_opt.create_text(270, 50, text='Optimized - Pl.: ' + str( + round(self._opt_results[0].Stiffener.get_s() * 1000, 1)) + + 'x' + str( + round(self._opt_results[0].Stiffener.get_pl_thk() * 1000, 1)) + + ' Stf.: ' + str( + round(self._opt_results[0].Stiffener.get_web_h() * 1000, 1)) + + 'x' + str( + round(self._opt_results[0].Stiffener.get_web_thk() * 1000, 1)) + '+' + + str(round(self._opt_results[0].Stiffener.get_fl_w() * 1000, 1)) + + 'x' + str( + round(self._opt_results[0].Stiffener.get_fl_thk() * 1000, 1)), + font='Verdana 8', fill=opt_color) self._canvas_opt.create_text(120, 70, text='Weight (per Lg width): ' - + str(int(op.calc_weight([self._opt_results[0].Stiffener.get_s(), - self._opt_results[0].Stiffener.get_pl_thk(), - self._opt_results[0].Stiffener.get_web_h(), - self._opt_results[0].Stiffener.get_web_thk(), - self._opt_results[0].Stiffener.get_fl_w(), - self._opt_results[0].Stiffener.get_fl_thk(), - self._new_span.get(), - self._new_width_lg.get()]))), - font='Verdana 8', fill=opt_color) + + str(int(op.calc_weight([self._opt_results[0].Stiffener.get_s(), + self._opt_results[ + 0].Stiffener.get_pl_thk(), + self._opt_results[ + 0].Stiffener.get_web_h(), + self._opt_results[ + 0].Stiffener.get_web_thk(), + self._opt_results[ + 0].Stiffener.get_fl_w(), + self._opt_results[ + 0].Stiffener.get_fl_thk(), + self._new_span.get(), + self._new_width_lg.get()]))), + font='Verdana 8', fill=opt_color) def save_and_close(self): ''' @@ -993,7 +1112,7 @@ def save_and_close(self): try: self.app.on_close_opt_window(self._opt_results) except (IndexError, TypeError): - messagebox.showinfo(title='Nothing to return',message='No results to return.') + messagebox.showinfo(title='Nothing to return', message='No results to return.') return self._frame.destroy() @@ -1033,28 +1152,28 @@ def algorithm_info(self): ' before the search terminates (Default: 1e-8)\n' ' minfunc : The minimum change of swarm’s best objective value\n' ' before the search terminates (Default: 1e-8)\n\n' - + '\n' 'All algorithms calculates local scantling and buckling requirements') def toggle(self): if self._toggle_btn.config('relief')[-1] == 'sunken': self._toggle_btn.config(relief="raised") - self._toggle_btn.config(bg = 'salmon') - self._ent_spacing_upper.config(bg = 'white') - self._ent_spacing_lower.config(bg = 'white') - self._ent_delta_spacing.config(bg = 'white') - predefined_stiffener_iter = [] + self._toggle_btn.config(bg='salmon') + self._ent_spacing_upper.config(bg='white') + self._ent_spacing_lower.config(bg='white') + self._ent_delta_spacing.config(bg='white') + predefined_stiffener_iter = [] else: self._toggle_btn.config(relief="sunken") - self._toggle_btn.config(bg = 'salmon') + self._toggle_btn.config(bg='salmon') self._toggle_btn.config(bg='lightgreen') - self._ent_spacing_upper.config(bg = 'lightgreen') - self._ent_spacing_lower.config(bg = 'lightgreen') - self._ent_delta_spacing.config(bg = 'lightgreen') - self._ent_pl_thk_upper.config(bg = 'lightgreen') - self._ent_pl_thk_lower.config(bg = 'lightgreen') - self._ent_delta_pl_thk.config(bg = 'lightgreen') + self._ent_spacing_upper.config(bg='lightgreen') + self._ent_spacing_lower.config(bg='lightgreen') + self._ent_delta_spacing.config(bg='lightgreen') + self._ent_pl_thk_upper.config(bg='lightgreen') + self._ent_pl_thk_lower.config(bg='lightgreen') + self._ent_delta_pl_thk.config(bg='lightgreen') open_files = askopenfilenames(parent=self._frame, title='Choose files to open', initialdir=self._root_dir) @@ -1066,14 +1185,14 @@ def toggle(self): obj=self._initial_calc_obj.Stiffener) if predefined_stiffener_iter == []: self._toggle_btn.config(relief="raised") - self._toggle_btn.config(bg = 'salmon') - self._ent_spacing_upper.config(bg = 'white') - self._ent_spacing_lower.config(bg = 'white') - self._ent_delta_spacing.config(bg = 'white') - self._ent_pl_thk_upper.config(bg = 'white') - self._ent_pl_thk_lower.config(bg = 'white') - self._ent_delta_pl_thk.config(bg = 'white') - self._predefined_stiffener_iter = None + self._toggle_btn.config(bg='salmon') + self._ent_spacing_upper.config(bg='white') + self._ent_spacing_lower.config(bg='white') + self._ent_delta_spacing.config(bg='white') + self._ent_pl_thk_upper.config(bg='white') + self._ent_pl_thk_lower.config(bg='white') + self._ent_delta_pl_thk.config(bg='white') + self._predefined_stiffener_iter = None else: self._predefined_stiffener_iter = predefined_stiffener_iter self.update_running_time() @@ -1106,7 +1225,6 @@ def receive_progress_info(): print('hi') - if __name__ == '__main__': root = tk.Tk() my_app = CreateOptimizeWindow(master=root) diff --git a/anystruct/project_application.py b/anystruct/project_application.py new file mode 100644 index 00000000..eed21d61 --- /dev/null +++ b/anystruct/project_application.py @@ -0,0 +1,619 @@ +"""Project snapshot, hydration, open-transfer, and persistence services.""" + +import copy +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, MutableMapping + +from . import example_data, line_structure, project_io +from .calc_loads import Loads +from .calc_structure import ( + AllStructure, + CalcFatigue, + CalcScantlings, + CylinderAndCurvedPlate, + Shell, + Structure, +) +from .project_state import ProjectState + + +ProjectFileCodec = project_io.ProjectFileCodec + + +@dataclass(frozen=True) +class LoadCombinationRecord: + """Plain load-combination value captured at the UI boundary.""" + + name: Any + static_factor: Any + dynamic_factor: Any + include: Any + + def to_legacy(self) -> list[Any]: + return [self.name, self.static_factor, self.dynamic_factor, self.include] + + +class ProjectPersistenceError(RuntimeError): + """Failure raised while a project file is loaded or saved.""" + + def __init__(self, action: str, location: Any, error: Exception): + self.action = action + self.location = str(location) + self.error = error + super().__init__(f"Could not {action} project file {self.location!r}: {error}") + + +class ProjectPersistenceService: + """Load and save project state without depending on Tk file objects.""" + + BACKUP_FILE_NAME = "backup.txt" + codec = ProjectFileCodec + + @classmethod + def save_state_to_path(cls, state: ProjectState, path: str | Path) -> Path: + project_path = Path(path) + try: + with project_path.open("w", encoding="utf-8") as project_file: + cls.codec.dump(state, project_file) + except (OSError, TypeError, ValueError) as error: + raise ProjectPersistenceError("save", project_path, error) from error + return project_path + + @classmethod + def load_state_from_path(cls, path: str | Path) -> ProjectState: + project_path = Path(path) + try: + with project_path.open(encoding="utf-8") as project_file: + return cls.codec.load(project_file) + except (OSError, json.JSONDecodeError, TypeError, ValueError) as error: + raise ProjectPersistenceError("load", project_path, error) from error + + @classmethod + def backup_path(cls, root_dir: str | Path) -> Path: + return Path(root_dir).parent / cls.BACKUP_FILE_NAME + + @classmethod + def backup_exists(cls, root_dir: str | Path) -> bool: + return cls.backup_path(root_dir).is_file() + + +@dataclass(frozen=True) +class ProjectFileTarget: + """Resolved project file path plus whether it should become the remembered save path.""" + + path: Path + remember_as_last_save: bool = False + + +class ProjectFileDialogService: + """Resolve project file targets without depending on Tk dialogs.""" + + @staticmethod + def selected_save_target(filename: str | Path, *, backup: bool = False) -> ProjectFileTarget: + return ProjectFileTarget(Path(filename), remember_as_last_save=not backup) + + @staticmethod + def selected_output_target(filename: str | Path | None) -> ProjectFileTarget | None: + if filename in (None, ""): + return None + return ProjectFileTarget(Path(filename), remember_as_last_save=False) + + @staticmethod + def selected_open_target(filename: str | Path | None) -> ProjectFileTarget | None: + if filename in (None, ""): + return None + return ProjectFileTarget(Path(filename), remember_as_last_save=False) + + @staticmethod + def backup_save_target(root_dir: str | Path) -> ProjectFileTarget: + return ProjectFileTarget(ProjectPersistenceService.backup_path(root_dir), remember_as_last_save=False) + + @staticmethod + def remembered_save_target(last_save_file: str | Path | None) -> ProjectFileTarget | None: + if last_save_file is None: + return None + return ProjectFileTarget(Path(last_save_file), remember_as_last_save=False) + + @staticmethod + def restore_target(root_dir: str | Path) -> ProjectFileTarget | None: + backup_path = ProjectPersistenceService.backup_path(root_dir) + if not backup_path.is_file(): + return None + return ProjectFileTarget(backup_path, remember_as_last_save=False) + + @staticmethod + def example_open_target(file_name: str | Path, root_dir: str | Path) -> ProjectFileTarget: + project_path = Path(file_name) + if not project_path.is_file(): + project_path = Path(root_dir) / project_path + return ProjectFileTarget(project_path, remember_as_last_save=False) + + +@dataclass(frozen=True) +class ProjectSaveInput: + """Plain project data captured at the save boundary.""" + + project_information: str + theme: str + points: MutableMapping[str, Any] + lines: MutableMapping[str, Any] + line_bundles: MutableMapping[str, list[Any]] + load_assignments: MutableMapping[str, list[Any]] + accelerations: MutableMapping[str, Any] + load_combinations: tuple[LoadCombinationRecord, ...] + tanks: MutableMapping[str, Any] + tank_grid: Any + tank_search_data: Any + buckling_method: str + shifting: MutableMapping[str, Any] + weight_and_cog: MutableMapping[str, Any] + + +@dataclass(frozen=True) +class ProjectSaveResult: + """Persisted save state and its resolved project path.""" + + path: Path + state: ProjectState + + +class ProjectSnapshotService: + """Create persistence snapshots from domain objects and plain UI values.""" + + @classmethod + def create_state( + cls, + *, + project_information: str, + theme: str, + points: MutableMapping[str, Any], + lines: MutableMapping[str, Any], + line_bundles: MutableMapping[str, list[Any]], + load_assignments: MutableMapping[str, list[Any]], + accelerations: MutableMapping[str, Any], + load_combinations: list[LoadCombinationRecord], + tanks: MutableMapping[str, Any], + tank_grid: Any, + tank_search_data: Any, + buckling_method: str, + shifting: MutableMapping[str, Any], + weight_and_cog: MutableMapping[str, Any], + ) -> ProjectState: + structures, shell_structures, fatigue = cls._serialize_line_bundles(line_bundles) + return ProjectState( + project_information=project_information, + theme=theme, + points=points, + lines=lines, + structures=structures, + shell_structures=shell_structures, + loads=cls._serialize_loads(load_assignments), + accelerations=accelerations, + load_combinations=cls._serialize_load_combinations(load_combinations), + tanks=cls._serialize_tanks(tanks, tank_grid, tank_search_data), + fatigue=fatigue, + buckling_method=buckling_method, + shifting=shifting, + weight_and_cog=weight_and_cog, + ) + + @staticmethod + def _serialize_line_bundles(line_bundles): + structures = {} + shell_structures = {} + fatigue = {} + + for line_name, legacy_bundle in line_bundles.items(): + bundle = line_structure.LineStructureBundle.from_legacy_bundle(legacy_bundle) + structures[line_name] = bundle.line_structure.get_main_properties() + shell_structures[line_name] = ( + None if bundle.cylinder is None else bundle.cylinder.get_all_properties() + ) + if bundle.fatigue is None: + fatigue[line_name] = None + continue + + try: + fatigue[line_name] = bundle.fatigue.get_fatigue_properties() + except AttributeError: + fatigue[line_name] = None + + return structures, shell_structures, fatigue + + @staticmethod + def _serialize_loads(load_assignments): + return { + load_name: [load_data[0].get_load_parmeters(), load_data[1]] + for load_name, load_data in load_assignments.items() + } + + @staticmethod + def _serialize_load_combinations(load_combinations): + return { + combination_number: load_combination.to_legacy() + for combination_number, load_combination in enumerate(load_combinations) + } + + @staticmethod + def _serialize_tanks(tanks, tank_grid, tank_search_data): + tank_properties = {"grid": tank_grid, "search_data": tank_search_data} + for tank_name, tank in tanks.items(): + tank_properties[tank_name] = tank.get_parameters() + return tank_properties + + +class ProjectSaveService: + """Create and persist project snapshots from plain save input.""" + + @staticmethod + def create_state(save_input: ProjectSaveInput) -> ProjectState: + return ProjectSnapshotService.create_state( + project_information=save_input.project_information, + theme=save_input.theme, + points=save_input.points, + lines=save_input.lines, + line_bundles=save_input.line_bundles, + load_assignments=save_input.load_assignments, + accelerations=save_input.accelerations, + load_combinations=list(save_input.load_combinations), + tanks=save_input.tanks, + tank_grid=save_input.tank_grid, + tank_search_data=save_input.tank_search_data, + buckling_method=save_input.buckling_method, + shifting=save_input.shifting, + weight_and_cog=save_input.weight_and_cog, + ) + + @classmethod + def save_path(cls, path: str | Path, save_input: ProjectSaveInput) -> ProjectSaveResult: + state = cls.create_state(save_input) + return ProjectSaveResult( + path=ProjectPersistenceService.save_state_to_path(state, path), + state=state, + ) + + +@dataclass(frozen=True) +class ProjectHydrationDefaults: + """Legacy structure defaults that the UI used to inject while opening files.""" + + structure_types: dict[str, Any] + zstar_optimization: Any = True + puls_buckling_method: Any = "ultimate" + puls_boundary: Any = "Int" + puls_stiffener_end: Any = "Continuous" + puls_sp_or_up: Any = "SP" + puls_up_boundary: Any = "SSSS" + material_factor: Any = 1.15 + + +@dataclass(frozen=True) +class ProjectHydrationResult: + """Domain objects rebuilt from a saved project before Tk widgets are touched.""" + + line_bundles: dict[str, list[Any]] + load_assignments: dict[str, list[Any]] + section_properties: tuple[dict[str, Any], ...] + + +class ProjectHydrationService: + """Rebuild supported domain objects from saved project state.""" + + LOAD_VARIABLES = ( + "poly_third", + "poly_second", + "poly_first", + "poly_const", + "load_condition", + "structure_type", + "man_press", + "static_draft", + "name_of_load", + "limit_state", + "slamming mult pl", + "slamming mult stf", + ) + + @classmethod + def hydrate_objects(cls, state: ProjectState, defaults: ProjectHydrationDefaults): + line_bundles, section_properties = cls._hydrate_line_bundles(state, defaults) + load_assignments = cls._hydrate_loads(state.loads, state.lines, line_bundles) + return ProjectHydrationResult( + line_bundles=line_bundles, + load_assignments=load_assignments, + section_properties=tuple(section_properties), + ) + + @classmethod + def _hydrate_line_bundles(cls, state: ProjectState, defaults: ProjectHydrationDefaults): + line_bundles = {} + section_properties = [] + old_save_file = False + + for line_name, structure_properties in copy.deepcopy(state.structures).items(): + if len(structure_properties) > 10: + old_save_file = True + + line_bundle = line_structure.LineStructureBundle( + loads=[], + load_combinations={}, + ) + + if old_save_file: + structure, fatigue, section_properties_for_line = cls._hydrate_legacy_flat_structure( + structure_properties, + state.fatigue.get(line_name), + defaults, + ) + else: + structure, fatigue, section_properties_for_line = cls._hydrate_structure( + structure_properties, + state.fatigue.get(line_name), + ) + + line_bundle.line_structure = structure + line_bundle.fatigue = fatigue + line_bundle.cylinder = cls._hydrate_cylinder(state.shell_structures.get(line_name)) + line_bundles[line_name] = line_bundle.to_legacy_bundle() + if section_properties_for_line is not None: + section_properties.append(section_properties_for_line) + + return line_bundles, section_properties + + @classmethod + def _hydrate_legacy_flat_structure(cls, structure_properties, fatigue_properties, defaults): + cls._add_legacy_structure_defaults(structure_properties, defaults) + cls._split_legacy_sigma_x(structure_properties) + + main_dict = copy.deepcopy(example_data.prescriptive_main_dict) + end_support_map = {"C": "Continuous", "S": "Sniped"} + stiffener_end = structure_properties["puls stiffener end"][0] + structure_properties["puls stiffener end"] = [ + end_support_map.get(stiffener_end, stiffener_end), + structure_properties["puls stiffener end"][1], + ] + main_dict["material yield"] = [355e6, "Pa"] + main_dict["load factor on stresses"] = [1, ""] + main_dict["load factor on pressure"] = [1, ""] + main_dict["buckling method"] = [structure_properties["puls buckling method"], ""] + main_dict["stiffener end support"] = structure_properties["puls stiffener end"] + main_dict["girder end support"] = ["Continuous", ""] + + domain = ( + "Flat plate, stiffened" + if structure_properties["puls sp or up"][0] == "SP" + else "Flat plate, unstiffened" + ) + main_dict["calculation domain"] = [domain, ""] + + pressure_side_map = {"p": "plate side", "s": "stiffener side"} + if "press_side" in structure_properties: + pressure_side = structure_properties["press_side"][0] + structure_properties["press_side"] = [pressure_side_map[pressure_side], ""] + else: + structure_properties["press_side"] = "both sides" + structure_properties["panel or shell"] = "panel" + + structure = AllStructure( + Plate=CalcScantlings(structure_properties), + Stiffener=None if domain == "Flat plate, unstiffened" else CalcScantlings(structure_properties), + Girder=None, + main_dict=main_dict, + ) + fatigue = ( + None + if fatigue_properties is None + else CalcFatigue(structure_properties, fatigue_properties) + ) + return structure, fatigue, structure_properties + + @staticmethod + def _hydrate_structure(structure_properties, fatigue_properties): + structure = AllStructure( + Plate=( + None + if structure_properties["Plate"] is None + else CalcScantlings(structure_properties["Plate"]) + ), + Stiffener=( + None + if structure_properties["Stiffener"] is None + else CalcScantlings(structure_properties["Stiffener"]) + ), + Girder=( + None + if structure_properties["Girder"] is None + else CalcScantlings(structure_properties["Girder"]) + ), + main_dict=structure_properties["main dict"], + ) + fatigue = ( + None + if fatigue_properties is None + else CalcFatigue(structure_properties["Stiffener"], fatigue_properties) + ) + return structure, fatigue, structure_properties["Stiffener"] + + @classmethod + def _hydrate_cylinder(cls, cylinder_properties): + if cylinder_properties is None: + return None + + cylinder_properties = copy.deepcopy(cylinder_properties) + for structure_type in ("Long. stf.", "Ring stf.", "Ring frame"): + if cylinder_properties[structure_type] is not None: + cls._split_legacy_sigma_x(cylinder_properties[structure_type]) + + return CylinderAndCurvedPlate( + cylinder_properties["Main class"], + shell=( + None + if cylinder_properties["Shell"] is None + else Shell(cylinder_properties["Shell"]) + ), + long_stf=( + None + if cylinder_properties["Long. stf."] is None + else Structure(cylinder_properties["Long. stf."]) + ), + ring_stf=( + None + if cylinder_properties["Ring stf."] is None + else Structure(cylinder_properties["Ring stf."]) + ), + ring_frame=( + None + if cylinder_properties["Ring frame"] is None + else Structure(cylinder_properties["Ring frame"]) + ), + ) + + @classmethod + def _hydrate_loads(cls, load_properties, lines, line_bundles): + load_assignments = {} + for load_name, data in load_properties.items(): + values = list(data[0]) + if len(values) != len(cls.LOAD_VARIABLES): + values.extend([1, 1]) + properties = dict(zip(cls.LOAD_VARIABLES, values)) + load_assignments[load_name] = [Loads(properties), data[1]] + + for line_name in lines: + if line_name in data[1] and line_name in line_bundles: + line_structure.LineStructureBundle.from_legacy_bundle( + line_bundles[line_name] + ).loads.append(load_assignments[load_name][0]) + + return load_assignments + + @staticmethod + def _add_legacy_structure_defaults(structure_properties, defaults): + default_values = { + "structure_types": defaults.structure_types, + "zstar_optimization": defaults.zstar_optimization, + "puls buckling method": defaults.puls_buckling_method, + "puls boundary": defaults.puls_boundary, + "puls stiffener end": defaults.puls_stiffener_end, + "puls sp or up": defaults.puls_sp_or_up, + "puls up boundary": defaults.puls_up_boundary, + "mat_factor": defaults.material_factor, + } + for property_name, property_value in default_values.items(): + if property_name not in structure_properties: + unit = " " if property_name == "structure_types" else "" + structure_properties[property_name] = [property_value, unit] + + @staticmethod + def _split_legacy_sigma_x(properties): + if "sigma_x" in properties: + properties["sigma_x1"] = properties["sigma_x"] + properties["sigma_x2"] = properties["sigma_x"] + properties.pop("sigma_x") + + +@dataclass(frozen=True) +class ProjectOpenResult: + """Assembled project payload ready for the UI open boundary.""" + + state: ProjectState + transfer: "ProjectOpenTransfer" + hydration: ProjectHydrationResult + + +class ProjectOpenService: + """Load and assemble project data before the UI applies it.""" + + @classmethod + def open_path(cls, path: str | Path, defaults: ProjectHydrationDefaults) -> ProjectOpenResult: + return cls.assemble( + ProjectPersistenceService.load_state_from_path(path), + defaults, + ) + + @staticmethod + def assemble(state: ProjectState, defaults: ProjectHydrationDefaults) -> ProjectOpenResult: + return ProjectOpenResult( + state=state, + transfer=ProjectOpenTransferService.create_transfer(state), + hydration=ProjectHydrationService.hydrate_objects(state, defaults), + ) + + +@dataclass(frozen=True) +class OpenLoadCombinationRecord: + """Saved load-combination values ready for Tk variable creation.""" + + name: tuple[Any, ...] + static_factor: Any + dynamic_factor: Any + include: Any = None + + @property + def has_include(self) -> bool: + return self.include is not None + + @classmethod + def from_legacy(cls, data: list[Any]) -> "OpenLoadCombinationRecord": + return cls( + name=tuple(data[0]), + static_factor=data[1], + dynamic_factor=data[2], + include=None if len(data) < 4 else data[3], + ) + + +@dataclass(frozen=True) +class ProjectOpenTransfer: + """Plain project values that `openfile` can apply at the Tk boundary.""" + + project_information: str + theme: str + points: dict[str, Any] + lines: dict[str, Any] + shifting: dict[str, Any] + accelerations: dict[str, Any] + load_combinations: tuple[OpenLoadCombinationRecord, ...] + tank_grid: Any + tank_search_data: dict[int, Any] | None + tank_properties: dict[str, Any] + buckling_method: str + weight_and_cog: dict[str, Any] + + +class ProjectOpenTransferService: + """Normalize saved project values before Tk view state is populated.""" + + DEFAULT_ACCELERATIONS = {"static": 9.81, "dyn_loaded": 0, "dyn_ballast": 0} + + @classmethod + def create_transfer(cls, state: ProjectState) -> ProjectOpenTransfer: + return ProjectOpenTransfer( + project_information=state.project_information, + theme=state.theme, + points=state.points, + lines=state.lines, + shifting=state.shifting, + accelerations={**cls.DEFAULT_ACCELERATIONS, **state.accelerations}, + load_combinations=tuple( + OpenLoadCombinationRecord.from_legacy(data) + for data in state.load_combinations.values() + ), + tank_grid=state.tanks.get("grid"), + tank_search_data=cls._normalize_tank_search_data(state.tanks.get("search_data")), + tank_properties={ + key: value + for key, value in state.tanks.items() + if key not in {"grid", "search_data"} + }, + buckling_method=state.buckling_method, + weight_and_cog=state.weight_and_cog, + ) + + @staticmethod + def _normalize_tank_search_data(search_data): + try: + return {int(key): value for key, value in search_data.items()} + except (AttributeError, TypeError, ValueError): + return None diff --git a/anystruct/project_io.py b/anystruct/project_io.py new file mode 100644 index 00000000..b7d13efe --- /dev/null +++ b/anystruct/project_io.py @@ -0,0 +1,99 @@ +"""Versioned JSON project persistence helpers.""" + +import json +from typing import Any, IO + +from anystruct.project_state import PROJECT_FORMAT_VERSION, ProjectState + + +PROJECT_FORMAT_KEY = "format version" +REMOVED_EXTERNAL_PULS_METHOD = "DNV PULS" +DEACTIVATED_ML_CL_METHODS = ( + "ML-CL (" + "PU" + "LS based)", + "ML-CL (SemiAnalytical based)", +) +ML_NUMERIC_METHOD = "ML-Numeric (PULS based)" +DEFAULT_BUCKLING_METHOD = "DNV-RP-C201 - prescriptive" + + +class ProjectFileCodec: + """Versioned project-file codec and migration entry point.""" + + current_version = PROJECT_FORMAT_VERSION + format_key = PROJECT_FORMAT_KEY + removed_external_puls_method = REMOVED_EXTERNAL_PULS_METHOD + deactivated_ml_cl_methods = DEACTIVATED_ML_CL_METHODS + ml_numeric_method = ML_NUMERIC_METHOD + default_buckling_method = DEFAULT_BUCKLING_METHOD + obsolete_external_puls_keys = ("PULS results",) + + @classmethod + def migrate_mapping(cls, project_data: dict[str, Any]) -> dict[str, Any]: + """Normalize a loaded project mapping into the current supported shape.""" + source_version = cls._source_version(project_data) + if source_version > cls.current_version: + raise ValueError( + f"Project format version {source_version} is newer than supported version {cls.current_version}" + ) + migrated = dict(project_data) + for obsolete_key in cls.obsolete_external_puls_keys: + migrated.pop(obsolete_key, None) + if migrated.get("buckling method") == cls.removed_external_puls_method: + migrated["buckling method"] = cls.default_buckling_method + if migrated.get("buckling method") in cls.deactivated_ml_cl_methods: + migrated["buckling method"] = cls.ml_numeric_method + migrated[cls.format_key] = cls.current_version + return migrated + + @classmethod + def _source_version(cls, project_data: dict[str, Any]) -> int: + if cls.format_key not in project_data: + return 0 + return int(project_data[cls.format_key]) + + @classmethod + def decode_mapping(cls, project_data: dict[str, Any]) -> ProjectState: + """Decode current or legacy project payloads into canonical project state.""" + return ProjectState.from_legacy_mapping(cls.migrate_mapping(project_data)) + + @classmethod + def encode_mapping(cls, project_state: ProjectState) -> dict[str, Any]: + """Encode canonical project state using the compatible JSON project shape.""" + payload = cls.migrate_mapping(project_state.to_legacy_mapping()) + payload[cls.format_key] = cls.current_version + return payload + + @classmethod + def load(cls, project_file: IO[str]) -> ProjectState: + """Load current or legacy project JSON and drop obsolete external PULS state.""" + return cls.decode_mapping(json.load(project_file)) + + @classmethod + def dump(cls, project_state: ProjectState, project_file: IO[str]) -> None: + """Write the canonical state using the compatible JSON project shape.""" + json.dump(cls.encode_mapping(project_state), project_file) + + +def migrate_project_mapping(project_data: dict[str, Any]) -> dict[str, Any]: + """Compatibility wrapper for the versioned project-file codec.""" + return ProjectFileCodec.migrate_mapping(project_data) + + +def decode_project_mapping(project_data: dict[str, Any]) -> ProjectState: + """Compatibility wrapper for the versioned project-file codec.""" + return ProjectFileCodec.decode_mapping(project_data) + + +def encode_project_mapping(project_state: ProjectState) -> dict[str, Any]: + """Compatibility wrapper for the versioned project-file codec.""" + return ProjectFileCodec.encode_mapping(project_state) + + +def load_project_state(project_file: IO[str]) -> ProjectState: + """Compatibility wrapper for the versioned project-file codec.""" + return ProjectFileCodec.load(project_file) + + +def dump_project_state(project_state: ProjectState, project_file: IO[str]) -> None: + """Compatibility wrapper for the versioned project-file codec.""" + ProjectFileCodec.dump(project_state, project_file) diff --git a/anystruct/project_services.py b/anystruct/project_services.py new file mode 100644 index 00000000..b6a1c615 --- /dev/null +++ b/anystruct/project_services.py @@ -0,0 +1,1220 @@ +"""Application services for project edits that do not depend on Tkinter.""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable, MutableMapping + +from . import api_helpers, helper as hlp, line_structure +from .helper import one_load_combination +from .project_application import ( + LoadCombinationRecord, + OpenLoadCombinationRecord, + ProjectHydrationDefaults, + ProjectHydrationResult, + ProjectHydrationService, + ProjectFileDialogService, + ProjectFileTarget, + ProjectFileCodec, + ProjectOpenResult, + ProjectOpenService, + ProjectOpenTransfer, + ProjectOpenTransferService, + ProjectPersistenceError, + ProjectPersistenceService, + ProjectSaveInput, + ProjectSaveResult, + ProjectSaveService, + ProjectSnapshotService, +) + + +POINT_PREFIX = "point" +LINE_PREFIX = "line" + + +def _numbered_name(prefix: str, number: int) -> str: + return f"{prefix}{number}" + + +def _name_number(name: str, prefix: str) -> int: + if not name.startswith(prefix): + raise ValueError(f"{name!r} is not a {prefix} name") + return int(name[len(prefix):]) + + +def _next_available_name(records: MutableMapping[str, Any], prefix: str) -> str: + number = 1 + while _numbered_name(prefix, number) in records: + number += 1 + return _numbered_name(prefix, number) + + +@dataclass(frozen=True) +class PointRecord: + """Named point used at the project editing boundary.""" + + name: str + x: float + y: float + + @classmethod + def from_legacy(cls, name: str, coordinates: list[float]) -> "PointRecord": + return cls(name=name, x=coordinates[0], y=coordinates[1]) + + def to_legacy(self) -> list[float]: + return [self.x, self.y] + + +@dataclass(frozen=True) +class LineRecord: + """Named line whose endpoints use project point names.""" + + name: str + first_point: str + second_point: str + + @classmethod + def from_legacy(cls, name: str, point_numbers: list[int]) -> "LineRecord": + return cls( + name=name, + first_point=_numbered_name(POINT_PREFIX, point_numbers[0]), + second_point=_numbered_name(POINT_PREFIX, point_numbers[1]), + ) + + @property + def point_numbers(self) -> list[int]: + return [ + _name_number(self.first_point, POINT_PREFIX), + _name_number(self.second_point, POINT_PREFIX), + ] + + @property + def endpoint_keys(self) -> tuple[str, str]: + first, second = self.point_numbers + return f"p{first}p{second}", f"p{second}p{first}" + + def to_legacy(self) -> list[int]: + return self.point_numbers + + +class ProjectEditService: + """Mutate legacy geometry dictionaries through typed project records.""" + + def __init__(self, points: MutableMapping[str, list[float]], lines: MutableMapping[str, list[int]]): + self._points = points + self._lines = lines + + def next_point_name(self) -> str: + return _next_available_name(self._points, POINT_PREFIX) + + def next_line_name(self) -> str: + return _next_available_name(self._lines, LINE_PREFIX) + + def add_point(self, name: str, coordinates: tuple[float, float]) -> PointRecord | None: + if list(coordinates) in self._points.values(): + return None + + point = PointRecord(name=name, x=coordinates[0], y=coordinates[1]) + self._points[point.name] = point.to_legacy() + return point + + def move_point(self, name: str, coordinates: tuple[float, float]) -> PointRecord | None: + if name not in self._points or list(coordinates) in self._points.values(): + return None + + point = PointRecord(name=name, x=coordinates[0], y=coordinates[1]) + self._points[name] = point.to_legacy() + return point + + def add_line(self, first_point: str, second_point: str) -> LineRecord | None: + if first_point == second_point or first_point not in self._points or second_point not in self._points: + return None + if self._line_exists(first_point, second_point): + return None + + line = LineRecord(name=self.next_line_name(), first_point=first_point, second_point=second_point) + self._lines[line.name] = line.to_legacy() + return line + + def line(self, name: str) -> LineRecord: + return LineRecord.from_legacy(name, self._lines[name]) + + def connected_line_names(self, point: str) -> list[str]: + point_number = _name_number(point, POINT_PREFIX) + return [name for name, endpoints in self._lines.items() if point_number in endpoints] + + def remove_line(self, name: str) -> LineRecord: + line = self.line(name) + self._lines.pop(name) + return line + + def remove_point(self, name: str) -> PointRecord: + point = PointRecord.from_legacy(name, self._points[name]) + self._points.pop(name) + return point + + def _line_exists(self, first_point: str, second_point: str) -> bool: + requested = {_name_number(first_point, POINT_PREFIX), _name_number(second_point, POINT_PREFIX)} + return any(set(endpoints) == requested for endpoints in self._lines.values()) + + +class LineStructureService: + """Assign and update the legacy line structure bundles behind typed access.""" + + def __init__(self, line_bundles: MutableMapping[str, list[Any]]): + self._line_bundles = line_bundles + + def has_assignment(self, line_name: str) -> bool: + return line_name in self._line_bundles + + def assign_structure(self, line_name: str, structure: Any, cylinder: Any = None): + bundle = line_structure.LineStructureBundle( + line_structure=structure, + loads=[None], + cylinder=cylinder, + ) + self._line_bundles[line_name] = bundle.to_legacy_bundle() + return bundle + + def structure(self, line_name: str): + return self.bundle(line_name).line_structure + + def fatigue(self, line_name: str): + return self.bundle(line_name).fatigue + + def cylinder(self, line_name: str): + return self.bundle(line_name).cylinder + + def replace_structure(self, line_name: str, structure: Any): + bundle = self.bundle(line_name) + bundle.line_structure = structure + self._write_bundle(line_name, bundle) + return structure + + def update_structure_properties(self, line_name: str, properties: dict[str, Any]): + structure = self.structure(line_name) + structure.set_main_properties(properties) + structure.need_recalc = True + return structure + + def set_cylinder(self, line_name: str, cylinder: Any): + bundle = self.bundle(line_name) + bundle.cylinder = cylinder + self._write_bundle(line_name, bundle) + return cylinder + + def sync_fatigue_after_structure_update(self, line_name: str, properties: dict[str, Any]): + bundle = self.bundle(line_name) + if bundle.fatigue is None: + return None + + if bundle.line_structure.calculation_domain == "Flat plate, unstiffened": + bundle.fatigue = None + self._write_bundle(line_name, bundle) + return None + + bundle.fatigue.set_main_properties(properties["Stiffener"]) + return bundle.fatigue + + def replace_loads(self, line_name: str, loads: list[Any]): + bundle = self.bundle(line_name) + bundle.loads = loads + self._write_bundle(line_name, bundle) + return bundle.loads + + def append_load(self, line_name: str, load: Any): + bundle = self.bundle(line_name) + bundle.loads.append(load) + return bundle.loads + + def bundle(self, line_name: str): + return line_structure.LineStructureBundle.from_legacy_bundle(self._line_bundles[line_name]) + + def _write_bundle(self, line_name: str, bundle: line_structure.LineStructureBundle): + self._line_bundles[line_name] = bundle.to_legacy_bundle() + + +@dataclass(frozen=True) +class FlatStructurePropertyRequest: + """Plain values needed to build a flat-plate structure property bundle.""" + + calculation_domain: str + base_values: dict[str, Any] + girder_values: dict[str, Any] + buckling_values: dict[str, Any] + structure_types: dict[str, Any] + + +class FlatStructurePropertyService: + """Build legacy flat-plate property bundles from plain boundary values.""" + + @classmethod + def build(cls, request: FlatStructurePropertyRequest): + values = request.base_values + obj_dict = { + 'mat_yield': [api_helpers.mpa_to_pa(values['material']), 'Pa'], + 'mat_factor': [values['material_factor'], ''], + 'span': [api_helpers.mm_to_m(values['span']), 'm'], + 'spacing': [api_helpers.mm_to_m(values['spacing']), 'm'], + 'plate_thk': [api_helpers.mm_to_m(values['plate_thk']), 'm'], + 'stf_web_height': [api_helpers.mm_to_m(values['stf_web_h']), 'm'], + 'stf_web_thk': [api_helpers.mm_to_m(values['stf_web_t']), 'm'], + 'stf_flange_width': [api_helpers.mm_to_m(values['stf_fl_w']), 'm'], + 'stf_flange_thk': [api_helpers.mm_to_m(values['stf_fl_t']), 'm'], + 'structure_type': [values['structure_type'], ''], + 'stf_type': [values['stf_type'], ''], + 'sigma_y1': [values['sigma_y1'], 'MPa'], + 'sigma_y2': [values['sigma_y2'], 'MPa'], + 'sigma_x1': [values['sigma_x1'], 'MPa'], + 'sigma_x2': [values['sigma_x2'], 'MPa'], + 'tau_xy': [values['tau_xy'], 'MPa'], + 'plate_kpp': [values['plate_kpp'], ''], + 'stf_kps': [values['stf_kps'], ''], + 'stf_km1': [values['stf_km1'], ''], + 'stf_km2': [values['stf_km2'], ''], + 'stf_km3': [values['stf_km3'], ''], + 'press_side': [values['pressure_side'], ''], + 'structure_types': [request.structure_types, ''], + 'zstar_optimization': [values['zstar_optimization'], ''], + 'puls buckling method': [values['puls_method'], ''], + 'puls boundary': [values['puls_boundary'], ''], + 'puls stiffener end': [values['puls_stiffener_end'], ''], + 'puls sp or up': [values['puls_sp_or_up'], ''], + 'puls up boundary': [values['puls_up_boundary'], ''], + 'panel or shell': [values['panel_or_shell'], ''], + 'girder_lg': [api_helpers.mm_to_m(values['girder_lg']), ''], + } + + obj_dict_pl = dict(obj_dict) + obj_dict_stf = dict(obj_dict) + obj_dict_girder = dict(obj_dict) + girder_values = request.girder_values + obj_dict_girder['stf_web_height'] = [api_helpers.mm_to_m(girder_values['web_h']), 'm'] + obj_dict_girder['stf_web_thk'] = [api_helpers.mm_to_m(girder_values['web_t']), 'm'] + obj_dict_girder['stf_flange_width'] = [api_helpers.mm_to_m(girder_values['fl_w']), 'm'] + obj_dict_girder['stf_flange_thk'] = [api_helpers.mm_to_m(girder_values['fl_t']), 'm'] + obj_dict_girder['stf_type'] = [girder_values['type'], ''] + + buckling_values = request.buckling_values + main_dict = { + 'minimum pressure in adjacent spans': [buckling_values['min_pressure_adjacent_spans'], ''], + 'material yield': [api_helpers.mpa_to_pa(values['material']), 'Pa'], + 'load factor on stresses': [buckling_values['load_factor_stresses'], ''], + 'load factor on pressure': [1, ''], + 'buckling method': [values['puls_method'], ''], + 'stiffener end support': [buckling_values['stiffener_end_support'], ''], + 'girder end support': [buckling_values['girder_end_support'], ''], + 'tension field': [buckling_values['tension_field'], ''], + 'plate effective agains sigy': [buckling_values['plate_effective_against_sigy'], ''], + 'buckling length factor stf': [buckling_values['buckling_length_factor_stf'], ''], + 'buckling length factor girder': [buckling_values['buckling_length_factor_girder'], ''], + 'km3': [buckling_values['km3'], ''], + 'km2': [buckling_values['km2'], ''], + 'girder distance between lateral support': [buckling_values['girder_dist_lateral_support'], ''], + 'stiffener distance between lateral support': [buckling_values['stiffener_dist_lateral_support'], ''], + 'panel length, Lp': [buckling_values['panel_length'], ''], + 'pressure side': [values['pressure_side'], ''], + 'fabrication method stiffener': [buckling_values['fabrication_method_stiffener'], ''], + 'fabrication method girder': [buckling_values['fabrication_method_girder'], ''], + 'calculation domain': [request.calculation_domain, ''], + } + + prop_dict = { + 'main dict': main_dict, + 'Plate': obj_dict_pl, + 'Stiffener': None if request.calculation_domain == 'Flat plate, unstiffened' else obj_dict_stf, + 'Girder': None if request.calculation_domain in ['Flat plate, unstiffened', 'Flat plate, stiffened'] + else obj_dict_girder, + } + return prop_dict, obj_dict_stf + + +@dataclass(frozen=True) +class CylinderStructurePropertyRequest: + """Plain values needed to build cylinder property dictionaries.""" + + calculation_domain: str + dummy_values: dict[str, Any] + shell_values: dict[str, Any] + longitudinal_values: dict[str, Any] + ring_stiffener_values: dict[str, Any] + ring_frame_values: dict[str, Any] + load_input: dict[str, Any] + main_values: dict[str, Any] + structure_types: dict[str, Any] + + +@dataclass(frozen=True) +class CylinderStructurePropertyResult: + """Cylinder property dictionaries plus derived stress/force values.""" + + main_dict: dict[str, Any] + shell_dict: dict[str, Any] + longitudinal_dict: dict[str, Any] + ring_stiffener_dict: dict[str, Any] + ring_frame_dict: dict[str, Any] + geometry: int + derived_stresses: tuple[Any, Any, Any, Any, Any] + derived_forces: tuple[Any, Any, Any, Any] + + +class CylinderStructurePropertyService: + """Build legacy cylinder property dictionaries from plain boundary values.""" + + @classmethod + def build(cls, request: CylinderStructurePropertyRequest) -> CylinderStructurePropertyResult: + from .calc_structure import CylinderAndCurvedPlate + + geometry = api_helpers.geometry_id_for_domain(request.calculation_domain) + dummy_data = cls._dummy_data(request) + shell_dict = cls._shell_dict(request) + long_dict = cls._longitudinal_dict(request) + ring_stf_dict = cls._ring_stiffener_dict(request) + ring_frame_dict = cls._ring_frame_dict(request) + derived_stresses, derived_forces = cls._convert_load_input( + request, + geometry, + cylinder_class=CylinderAndCurvedPlate, + ) + sasd, smsd, tTsd, tQsd, shsd = derived_stresses + main_values = request.main_values + main_dict_cyl = { + 'sasd': [api_helpers.mpa_to_pa(sasd), 'Pa'], + 'smsd': [api_helpers.mpa_to_pa(smsd), 'Pa'], + 'tTsd': [api_helpers.mpa_to_pa(tTsd), 'Pa'], + 'tQsd': [api_helpers.mpa_to_pa(tQsd), 'Pa'], + 'psd': [api_helpers.mpa_to_pa(request.load_input['psd']), 'Pa'], + 'shsd': [api_helpers.mpa_to_pa(shsd), 'Pa'], + 'geometry': [geometry, ''], + 'material factor': [main_values['material_factor'], ''], + 'delta0': [0.005, ''], + 'fab method ring stf': [main_values['fab_method_ring_stiffener'], ''], + 'fab method ring girder': [main_values['fab_method_ring_frame'], ''], + 'E-module': [main_values['e_module'], 'Pa'], + 'poisson': [main_values['poisson'], ''], + 'mat_yield': [api_helpers.mpa_to_pa(main_values['yield']), 'Pa'], + 'length between girders': [api_helpers.mm_to_m(main_values['length_between_girders']), 'm'], + 'panel spacing, s': [api_helpers.mm_to_m(main_values['panel_spacing']), 'm'], + 'ring stf excluded': [main_values['ring_stiffener_excluded'], ''], + 'ring frame excluded': [main_values['ring_frame_excluded'], ''], + 'ULS or ALS': [main_values['uls_or_als'], ''], + 'end cap pressure': [main_values['end_cap_pressure'], ''], + } + + for key, value in dummy_data.items(): + long_dict.setdefault(key, value) + ring_stf_dict.setdefault(key, value) + ring_frame_dict.setdefault(key, value) + + return CylinderStructurePropertyResult( + main_dict=main_dict_cyl, + shell_dict=shell_dict, + longitudinal_dict=long_dict, + ring_stiffener_dict=ring_stf_dict, + ring_frame_dict=ring_frame_dict, + geometry=geometry, + derived_stresses=derived_stresses, + derived_forces=derived_forces, + ) + + @staticmethod + def _dummy_data(request): + values = request.dummy_values + return { + 'span': [api_helpers.mm_to_m(values['span']), 'm'], + 'plate_thk': [api_helpers.mm_to_m(values['plate_thk']), 'm'], + 'structure_type': [values['structure_type'], ''], + 'sigma_y1': [values['sigma_y1'], 'MPa'], + 'sigma_y2': [values['sigma_y2'], 'MPa'], + 'sigma_x1': [values['sigma_x1'], 'MPa'], + 'sigma_x2': [values['sigma_x2'], 'MPa'], + 'tau_xy': [values['tau_xy'], 'MPa'], + 'plate_kpp': [values['plate_kpp'], ''], + 'stf_kps': [values['stf_kps'], ''], + 'stf_km1': [values['stf_km1'], ''], + 'stf_km2': [values['stf_km2'], ''], + 'stf_km3': [values['stf_km3'], ''], + 'press_side': [values['pressure_side'], ''], + 'structure_types': [request.structure_types, ''], + 'zstar_optimization': [values['zstar_optimization'], ''], + 'puls buckling method': [values['puls_method'], ''], + 'puls boundary': [values['puls_boundary'], ''], + 'puls stiffener end': [values['puls_stiffener_end'], ''], + 'puls sp or up': [values['puls_sp_or_up'], ''], + 'puls up boundary': [values['puls_up_boundary'], ''], + 'panel or shell': [values['panel_or_shell'], ''], + 'mat_factor': [values['material_factor'], ''], + 'spacing': [api_helpers.mm_to_m(values['spacing']), 'm'], + } + + @staticmethod + def _shell_dict(request): + values = request.shell_values + return { + 'plate_thk': [api_helpers.mm_to_m(values['thickness']), 'm'], + 'radius': [api_helpers.mm_to_m(values['radius']), 'm'], + 'distance between rings, l': [api_helpers.mm_to_m(values['distance_between_rings']), 'm'], + 'length of shell, L': [api_helpers.mm_to_m(values['length']), 'm'], + 'tot cyl length, Lc': [api_helpers.mm_to_m(values['total_length']), 'm'], + 'eff. buckling lenght factor': [values['k_factor'], ''], + 'mat_yield': [api_helpers.mpa_to_pa(request.main_values['yield']), 'Pa'], + } + + @staticmethod + def _longitudinal_dict(request): + values = request.longitudinal_values + return { + 'spacing': [api_helpers.mm_to_m(values['spacing']), 'm'], + 'stf_web_height': [api_helpers.mm_to_m(values['web_h']), 'm'], + 'stf_web_thk': [api_helpers.mm_to_m(values['web_t']), 'm'], + 'stf_flange_width': [api_helpers.mm_to_m(values['fl_w']), 'm'], + 'stf_flange_thk': [api_helpers.mm_to_m(values['fl_t']), 'm'], + 'stf_type': [values['type'], ''], + 'span': [api_helpers.mm_to_m(request.dummy_values['span']), 'm'], + 'mat_yield': [api_helpers.mpa_to_pa(request.main_values['yield']), 'Pa'], + 'panel or shell': ['shell', ''], + } + + @staticmethod + def _ring_stiffener_dict(request): + values = request.ring_stiffener_values + return { + 'stf_web_height': [api_helpers.mm_to_m(values['web_h']), 'm'], + 'stf_web_thk': [api_helpers.mm_to_m(values['web_t']), 'm'], + 'stf_flange_width': [api_helpers.mm_to_m(values['fl_w']), 'm'], + 'stf_flange_thk': [api_helpers.mm_to_m(values['fl_t']), 'm'], + 'stf_type': [values['type'], ''], + 'mat_yield': [api_helpers.mpa_to_pa(request.main_values['yield']), 'Pa'], + 'panel or shell': ['shell', ''], + } + + @staticmethod + def _ring_frame_dict(request): + values = request.ring_frame_values + return { + 'stf_web_height': [api_helpers.mm_to_m(values['web_h']), 'm'], + 'stf_web_thk': [api_helpers.mm_to_m(values['web_t']), 'm'], + 'stf_flange_width': [api_helpers.mm_to_m(values['fl_w']), 'm'], + 'stf_flange_thk': [api_helpers.mm_to_m(values['fl_t']), 'm'], + 'stf_type': [values['type'], ''], + 'span': [api_helpers.mm_to_m(request.dummy_values['span']), 'm'], + 'mat_yield': [api_helpers.mpa_to_pa(request.main_values['yield']), 'Pa'], + 'panel or shell': ['shell', ''], + } + + @staticmethod + def _convert_load_input(request, geometry, cylinder_class): + load_input = request.load_input + shell = request.shell_values + longitudinal = request.longitudinal_values + converter_kwargs = { + 'geometry': geometry, + 'shell_t': shell['thickness'], + 'shell_radius': shell['radius'], + 'shell_spacing': longitudinal['spacing'], + 'hw': longitudinal['web_h'], + 'tw': longitudinal['web_t'], + 'b': longitudinal['fl_w'], + 'tf': longitudinal['fl_t'], + 'CylinderAndCurvedPlate': cylinder_class, + } + if load_input['mode'] == 1: + forces = ( + load_input['Nsd'], + load_input['Msd'], + load_input['Tsd'], + load_input['Qsd'], + ) + stresses = hlp.helper_cylinder_stress_to_force_to_stress( + stresses=None, + forces=forces, + **converter_kwargs, + ) + return stresses, forces + + stresses = ( + load_input['sasd'], + load_input['smsd'], + abs(load_input['tTsd']), + load_input['tQsd'], + load_input['shsd'], + ) + forces_with_shsd = hlp.helper_cylinder_stress_to_force_to_stress( + stresses=stresses, + **converter_kwargs, + ) + return stresses, tuple(forces_with_shsd[:4]) + + +@dataclass(frozen=True) +class LineLoadSyncResult: + """Outcome of rebuilding per-line load assignments.""" + + invalidated_lines: tuple[str, ...] + changed_lines: tuple[str, ...] + + +class LineLoadService: + """Rebuild per-line load references independently of the load window.""" + + def __init__(self, line_bundles: MutableMapping[str, list[Any]]): + self._line_bundles = line_bundles + self._structures = LineStructureService(line_bundles) + + def rebuild_line_loads( + self, + line_names, + load_assignments: MutableMapping[str, list[Any]], + previous_load_assignments: MutableMapping[str, list[Any]] | None, + ) -> LineLoadSyncResult: + for line_name in self._line_bundles: + self._structures.replace_loads(line_name, []) + + invalidated = tuple(mark_lines_for_recalculation(self._line_bundles)) + changed = [] + previous_load_assignments = previous_load_assignments or {} + + for line_name in line_names: + if not self._structures.has_assignment(line_name): + continue + + for load, load_lines in load_assignments.values(): + if self._line_load_changed(line_name, load, load_lines, previous_load_assignments): + if line_name not in changed: + changed.append(line_name) + if line_name in load_lines: + self._structures.append_load(line_name, load) + + return LineLoadSyncResult(invalidated_lines=invalidated, changed_lines=tuple(changed)) + + @staticmethod + def _line_load_changed(line_name, load, load_lines, previous_load_assignments): + previous = previous_load_assignments.get(load.get_name()) + if previous is None: + return line_name in load_lines + + previous_load, previous_lines = previous + load_changed_on_line = str(load) != str(previous_load) and line_name in load_lines + previous_lines + line_assignment_changed = line_name in set(previous_lines).symmetric_difference(load_lines) + return load_changed_on_line or line_assignment_changed + + +def _read_boundary_value(value): + return value.get() if hasattr(value, "get") else value + + +@dataclass(frozen=True) +class LoadFactorRecord: + """Plain load-factor values captured from the load-combination boundary.""" + + name: tuple[Any, ...] + static_factor: Any + dynamic_factor: Any + include: Any + + @classmethod + def from_boundary_values(cls, name, values): + return cls( + name=tuple(name), + static_factor=_read_boundary_value(values[0]), + dynamic_factor=_read_boundary_value(values[1]), + include=_read_boundary_value(values[2]), + ) + + +@dataclass(frozen=True) +class _LegacyLoadFactorValue: + """Small get()-adapter for the legacy helper pressure calculation API.""" + + value: Any + + def get(self): + return self.value + + +def load_factor_records(load_factors) -> dict[tuple[Any, ...], LoadFactorRecord]: + """Capture load factors from either Tk variables or plain boundary values.""" + return { + tuple(name): LoadFactorRecord.from_boundary_values(name, values) + for name, values in load_factors.items() + } + + +@dataclass(frozen=True) +class LinePressureInput: + """Plain application input for one line pressure/load-combination run.""" + + line_name: str + line_bundle: list[Any] + coordinate: tuple[Any, Any] + defined_tanks: tuple[tuple[str, Any], ...] + accelerations: MutableMapping[str, Any] + load_factors: MutableMapping[tuple[Any, ...], LoadFactorRecord] + + +class LinePressureService: + """Calculate line load combinations without depending on Tk variables.""" + + ZERO_PRESSURE_STRUCTURE_TYPES = {"", "FRAME", "GENERAL_INTERNAL_NONWT"} + + @classmethod + def calculate_combinations(cls, line_input: LinePressureInput, limit_state="ULS", get_load_info=False): + if limit_state == "FLS": + return None + + results = {} + load_info = [] + for combination_name in ("dnva", "dnvb"): + results[combination_name] = [] + for load_condition in ("loaded", "ballast"): + returned = cls.calculate_one(line_input, combination_name, load_condition) + if returned is not None: + results[combination_name].append(returned[0]) + load_info.extend(returned[1]) + + for combination_name, load_condition in ( + ("tanktest", "tanktest"), + ("manual", "manual"), + ("slamming", "slamming"), + ): + returned = cls.calculate_one(line_input, combination_name, load_condition) + results[combination_name] = [returned[0]] + load_info.extend(returned[1]) + + return load_info if get_load_info else results + + @classmethod + def calculate_one(cls, line_input: LinePressureInput, combination_name, load_condition): + bundle = line_structure.LineStructureBundle.from_legacy_bundle(line_input.line_bundle) + defined_loads = [ + load + for load in bundle.loads + if load is not None and load.get_limit_state() != "FLS" + ] + + acceleration_key = f"dyn_{load_condition}" + acceleration = ( + line_input.accelerations["static"], + 0 if load_condition in {"tanktest", "manual", "slamming"} + else line_input.accelerations[acceleration_key], + ) + structure = bundle.line_structure + current_line_object = [line_input.line_name, structure.Plate] + if structure.Plate.get_structure_type() in cls.ZERO_PRESSURE_STRUCTURE_TYPES: + return [0, ""] + + return one_load_combination( + current_line_object, + line_input.coordinate, + defined_loads, + load_condition, + list(line_input.defined_tanks), + combination_name, + acceleration, + cls._legacy_load_factor_values(line_input.load_factors), + ) + + @classmethod + def highest_pressure(cls, line_input: LinePressureInput, limit_state="ULS"): + if limit_state != "ULS": + return {"normal": 0, "slamming": 0} + + results = cls.calculate_combinations(line_input) + normal_pressures = [] + slamming_pressure = 0 + slamming_plate_factor = 1 + slamming_stiffener_factor = 1 + + for combination_name, values in results.items(): + if combination_name != "slamming": + normal_pressures.append(max(values)) + continue + if values is not None: + for load in line_structure.loads(line_input.line_bundle): + if load is not None and load.get_load_condition() == "slamming": + slamming_plate_factor = load.get_slamming_reduction_plate() + slamming_stiffener_factor = load.get_slamming_reduction_stf() + slamming_pressure = max(values) + + return { + "normal": max(normal_pressures), + "slamming": slamming_pressure, + "slamming plate reduction factor": slamming_plate_factor, + "slamming stf reduction factor": slamming_stiffener_factor, + } + + @staticmethod + def _legacy_load_factor_values(load_factors): + return { + name: ( + _LegacyLoadFactorValue(record.static_factor), + _LegacyLoadFactorValue(record.dynamic_factor), + _LegacyLoadFactorValue(record.include), + ) + for name, record in load_factors.items() + } + + +@dataclass(frozen=True) +class SesamExportRequest: + """Plain project payload for the supported SESAM JavaScript export.""" + + points: MutableMapping[str, Any] + lines: MutableMapping[str, Any] + sections: Any + line_bundles: MutableMapping[str, list[Any]] + + +class SesamExportService: + """Build the supported SESAM JavaScript export outside GUI callbacks.""" + + @staticmethod + def build_js_lines(request: SesamExportRequest, export_factory=None) -> tuple[str, ...]: + if export_factory is None: + from . import sesam_interface + + export_factory = sesam_interface.JSfile + + export = export_factory( + request.points, + request.lines, + request.sections, + line_to_struc=request.line_bundles, + ) + export.write_points() + export.write_lines() + export.write_sections() + export.write_beams() + return tuple(export.output_lines) + + @classmethod + def write_js_path(cls, request: SesamExportRequest, path, export_factory=None) -> Path: + export_path = Path(path) + export_path.write_text("".join(cls.build_js_lines(request, export_factory)), encoding="utf-8") + return export_path + + +@dataclass(frozen=True) +class ExcelProjectImportData: + """Supported workbook rows read before the Tk import callback applies them.""" + + flat_plate_rows: tuple[Any, ...] + cylinder_rows: tuple[Any, ...] + flat_plate_records: tuple["FlatPlateExcelImportRecord", ...] + cylinder_records: tuple["CylinderExcelImportRecord", ...] + + +@dataclass(frozen=True) +class FlatPlateExcelImportRecord: + """Flat-plate line values decoded from the supported import workbook.""" + + calculation_domain: Any + first_point: tuple[Any, Any] + second_point: tuple[Any, Any] + plate_values: tuple[Any, ...] + stress_values: tuple[Any, ...] + manual_pressure: Any + girder_values: tuple[Any, ...] + buckling_values: tuple[Any, ...] + + @classmethod + def from_row(cls, row: list[Any] | tuple[Any, ...]): + return cls( + calculation_domain=row[0], + first_point=(row[1], row[2]), + second_point=(row[3], row[4]), + plate_values=tuple(row[5:11]), + stress_values=tuple(row[12:17]), + manual_pressure=row[16], + girder_values=tuple(row[18:24]), + buckling_values=tuple(row[24:36]), + ) + + +@dataclass(frozen=True) +class CylinderExcelImportRecord: + """Cylinder line values decoded from the supported import workbook.""" + + calculation_domain: Any + first_point: tuple[Any, Any] + second_point: tuple[Any, Any] + shell_values: tuple[Any, ...] + longitudinal_values: tuple[Any, ...] + ring_stiffener_values: tuple[Any, ...] + ring_frame_values: tuple[Any, ...] + stress_values: tuple[Any, ...] + force_values: tuple[Any, ...] + end_values: tuple[Any, ...] + + @classmethod + def from_row(cls, row: list[Any] | tuple[Any, ...]): + return cls( + calculation_domain=row[0], + first_point=(row[1], row[2]), + second_point=(row[3], row[4]), + shell_values=tuple(row[5:12]), + longitudinal_values=tuple(row[12:18]), + ring_stiffener_values=tuple(row[18:23]), + ring_frame_values=tuple(row[23:29]), + stress_values=tuple(row[29:35]), + force_values=tuple(row[35:39]), + end_values=tuple(row[39:43]), + ) + + +@dataclass(frozen=True) +class CylinderExcelImportDefaults: + """Current project defaults needed when a workbook omits cylinder UI-only values.""" + + plate_thk: Any + structure_type: Any + sigma_y1: Any + sigma_y2: Any + sigma_x1: Any + sigma_x2: Any + tau_xy: Any + plate_kpp: Any + stf_kps: Any + stf_km1: Any + stf_km2: Any + stf_km3: Any + pressure_side: Any + zstar_optimization: Any + puls_method: Any + puls_boundary: Any + puls_stiffener_end: Any + puls_sp_or_up: Any + puls_up_boundary: Any + panel_or_shell: Any + material_factor: Any + design_pressure: Any + shear_stress: Any + e_module: Any + poisson: Any + length_between_girders: Any + fab_method_ring_stiffener: Any + fab_method_ring_frame: Any + end_cap_pressure: Any + structure_types: dict[str, Any] + ring_stiffener_type: Any = "T" + ring_frame_type: Any = "T" + + +class CylinderExcelImportPropertyService: + """Convert supported cylinder workbook records into plain property requests.""" + + @classmethod + def build_request( + cls, + record: CylinderExcelImportRecord, + defaults: CylinderExcelImportDefaults, + ) -> CylinderStructurePropertyRequest: + span = hlp.dist(record.first_point, record.second_point) + shell_thk, shell_radius, dist_rings, shell_length, shell_total_length, shell_k, shell_mat = \ + record.shell_values + long_web_h, long_web_t, long_fl_w, long_fl_t, panel_spacing, long_type = \ + record.longitudinal_values + ring_stf_values, ring_stf_excluded = cls._component_values( + record.ring_stiffener_values, + default_type=defaults.ring_stiffener_type, + has_length=False, + ) + ring_frame_values, ring_frame_excluded = cls._component_values( + record.ring_frame_values, + default_type=defaults.ring_frame_type, + has_length=True, + ) + load_input = cls._load_input(record, defaults) + uls_or_als, shell_yield, _ring_stf_fab, _ring_frame_fab = record.end_values + + return CylinderStructurePropertyRequest( + calculation_domain=record.calculation_domain, + dummy_values={ + "span": span, + "plate_thk": defaults.plate_thk, + "structure_type": defaults.structure_type, + "sigma_y1": defaults.sigma_y1, + "sigma_y2": defaults.sigma_y2, + "sigma_x1": defaults.sigma_x1, + "sigma_x2": defaults.sigma_x2, + "tau_xy": defaults.tau_xy, + "plate_kpp": defaults.plate_kpp, + "stf_kps": defaults.stf_kps, + "stf_km1": defaults.stf_km1, + "stf_km2": defaults.stf_km2, + "stf_km3": defaults.stf_km3, + "pressure_side": defaults.pressure_side, + "zstar_optimization": defaults.zstar_optimization, + "puls_method": defaults.puls_method, + "puls_boundary": defaults.puls_boundary, + "puls_stiffener_end": defaults.puls_stiffener_end, + "puls_sp_or_up": defaults.puls_sp_or_up, + "puls_up_boundary": defaults.puls_up_boundary, + "panel_or_shell": defaults.panel_or_shell, + "material_factor": defaults.material_factor, + "spacing": panel_spacing, + }, + shell_values={ + "thickness": shell_thk, + "radius": shell_radius, + "distance_between_rings": dist_rings, + "length": shell_length, + "total_length": shell_total_length, + "k_factor": shell_k, + }, + longitudinal_values={ + "spacing": panel_spacing, + "web_h": long_web_h, + "web_t": long_web_t, + "fl_w": long_fl_w, + "fl_t": long_fl_t, + "type": long_type, + }, + ring_stiffener_values={ + "web_h": ring_stf_values[0], + "web_t": ring_stf_values[1], + "fl_w": ring_stf_values[2], + "fl_t": ring_stf_values[3], + "type": ring_stf_values[4], + }, + ring_frame_values={ + "web_h": ring_frame_values[0], + "web_t": ring_frame_values[1], + "fl_w": ring_frame_values[2], + "fl_t": ring_frame_values[3], + "type": ring_frame_values[5], + }, + load_input=load_input, + main_values={ + "material_factor": shell_mat, + "fab_method_ring_stiffener": defaults.fab_method_ring_stiffener, + "fab_method_ring_frame": defaults.fab_method_ring_frame, + "e_module": defaults.e_module, + "poisson": defaults.poisson, + "yield": shell_yield, + "length_between_girders": ( + defaults.length_between_girders if ring_frame_excluded else ring_frame_values[4] + ), + "panel_spacing": panel_spacing, + "ring_stiffener_excluded": ring_stf_excluded, + "ring_frame_excluded": ring_frame_excluded, + "uls_or_als": uls_or_als, + "end_cap_pressure": defaults.end_cap_pressure, + }, + structure_types=defaults.structure_types, + ) + + @staticmethod + def _component_values(values, *, default_type, has_length): + if not values or values[0] is None: + if has_length: + return (0, 0, 0, 0, 0, default_type), True + return (0, 0, 0, 0, default_type), True + return values, False + + @staticmethod + def _load_input(record, defaults): + stresses = record.stress_values or () + forces = record.force_values or () + has_stresses = bool(stresses) and stresses[0] is not None + has_forces = bool(forces) and forces[0] is not None + + sasd, smsd, tTsd, tQsd, psd, shsd = ( + stresses if has_stresses else (0, 0, 0, 0, defaults.design_pressure, defaults.shear_stress) + ) + psd = defaults.design_pressure if psd is None else psd + shsd = defaults.shear_stress if shsd is None else shsd + nsd, msd, tsd, qsd = forces if has_forces else (0, 0, 0, 0) + return { + "mode": 1 if has_forces else 2, + "Nsd": nsd, + "Msd": msd, + "Tsd": tsd, + "Qsd": qsd, + "sasd": sasd, + "smsd": smsd, + "tTsd": tTsd, + "tQsd": tQsd, + "psd": psd, + "shsd": shsd, + } + + +@dataclass(frozen=True) +class ExcelImportedLine: + """One workbook record matched to the project line it created.""" + + record: Any + line: LineRecord + + +@dataclass(frozen=True) +class ExcelGeometryImportResult: + """Geometry changes made from workbook records before structures are applied.""" + + created_points: tuple[PointRecord, ...] + imported_lines: tuple[ExcelImportedLine, ...] + + +class ExcelProjectGeometryImportService: + """Apply supported import geometry to the legacy project geometry stores.""" + + @classmethod + def add_records(cls, points, lines, records) -> ExcelGeometryImportResult: + editor = ProjectEditService(points, lines) + created_points = [] + imported_lines = [] + + for record in records: + for workbook_point in (record.first_point, record.second_point): + point = editor.add_point(editor.next_point_name(), cls._point_in_meters(workbook_point)) + if point is not None: + created_points.append(point) + + point_names_by_coordinate = {tuple(coordinates): name for name, coordinates in points.items()} + for record in records: + first_point = point_names_by_coordinate.get(cls._point_in_meters(record.first_point)) + second_point = point_names_by_coordinate.get(cls._point_in_meters(record.second_point)) + if first_point is None or second_point is None: + continue + + line = editor.add_line(first_point, second_point) + if line is not None: + imported_lines.append(ExcelImportedLine(record=record, line=line)) + + return ExcelGeometryImportResult( + created_points=tuple(created_points), + imported_lines=tuple(imported_lines), + ) + + @staticmethod + def _point_in_meters(workbook_point): + return workbook_point[0] / 1000, workbook_point[1] / 1000 + + +class ExcelProjectImportService: + """Read the supported Excel project import workbook through an adapter.""" + + @classmethod + def read_path(cls, path: str | Path, workbook_factory: Callable[..., Any] | None = None): + workbook = cls._open_workbook( + path, + visible=False, + read_only=True, + workbook_factory=workbook_factory, + ) + try: + flat_plate_rows = tuple(workbook.get_sheet_data("flat_plate") or ()) + cylinder_rows = tuple(workbook.get_sheet_data("cylinder") or ()) + return ExcelProjectImportData( + flat_plate_rows=flat_plate_rows, + cylinder_rows=cylinder_rows, + flat_plate_records=tuple( + FlatPlateExcelImportRecord.from_row(row) for row in flat_plate_rows[1:] + ), + cylinder_records=tuple( + CylinderExcelImportRecord.from_row(row) for row in cylinder_rows[1:] + ), + ) + finally: + workbook.close_book() + + @classmethod + def open_example_path(cls, path: str | Path, workbook_factory: Callable[..., Any] | None = None): + """Open the bundled input workbook for inspection in Excel.""" + return cls._open_workbook( + path, + visible=True, + read_only=True, + workbook_factory=workbook_factory, + ) + + @staticmethod + def _open_workbook(path, *, visible, read_only, workbook_factory=None): + if workbook_factory is None: + from .excel_inteface import ExcelInterface + + workbook_factory = ExcelInterface + return workbook_factory(str(path), visible=visible, read_only=read_only) + + +@dataclass(frozen=True) +class ReportRequest: + """Report command values captured before the reporter adapter is called.""" + + filename: str + title: str + seconds: int + source_data: Any + + +@dataclass(frozen=True) +class ReportDataSnapshot: + """Plain report input captured before the Tk application calls a renderer.""" + + project_information: str + buckling_method: str + points: MutableMapping[str, Any] + lines: MutableMapping[str, Any] + line_bundles: MutableMapping[str, list[Any]] + tanks: MutableMapping[str, Any] + loads: MutableMapping[str, Any] + result_state: MutableMapping[str, Any] + highest_pressures: MutableMapping[str, Any] + ml_classes: Any + + +class ReportRequestService: + """Orchestrate supported report requests through an injected adapter.""" + + @staticmethod + def create_pdf(request: ReportRequest, report_factory: Callable[..., Any] | None = None): + if report_factory is None: + from .report_generator import LetterMaker + + report_factory = LetterMaker + + report = report_factory(request.filename, request.title, request.seconds, request.source_data) + report.createDocument() + report.savePDF() + return report + + @staticmethod + def create_table( + request: ReportRequest, + report_factory: Callable[..., Any] | None = None, + document_factory: Callable[[str], Any] | None = None, + ): + if report_factory is None: + from .report_generator import LetterMaker + + report_factory = LetterMaker + if document_factory is None: + from reportlab.lib.pagesizes import landscape, letter + from reportlab.platypus import SimpleDocTemplate + + document_factory = lambda filename: SimpleDocTemplate(filename, pagesize=landscape(letter)) + + report = report_factory(request.filename, request.title, request.seconds, request.source_data) + document = document_factory(request.filename) + document.build(report.createTable()) + return document + + +def mark_line_for_recalculation(line_bundles: MutableMapping[str, list[Any]], line_name: str) -> bool: + """Mark one assigned line dirty and report whether a structure was available.""" + structure = line_structure.structure(line_bundles[line_name]) if line_name in line_bundles else None + if structure is None: + return False + + structure.need_recalc = True + return True + + +def mark_lines_for_recalculation(line_bundles: MutableMapping[str, list[Any]]) -> list[str]: + """Mark legacy line structure bundles dirty through the named bundle adapter.""" + invalidated = [] + for line_name in line_bundles: + if mark_line_for_recalculation(line_bundles, line_name): + invalidated.append(line_name) + return invalidated diff --git a/anystruct/project_state.py b/anystruct/project_state.py new file mode 100644 index 00000000..2032d265 --- /dev/null +++ b/anystruct/project_state.py @@ -0,0 +1,92 @@ +"""Canonical project data used at persistence and application boundaries.""" + +from dataclasses import dataclass, field +from typing import Any + + +PROJECT_FORMAT_VERSION = 1 + + +@dataclass +class ProjectState: + """JSON-safe project snapshot independent of Tkinter widget state.""" + + project_information: str = "" + theme: str = "default" + points: dict[str, Any] = field(default_factory=dict) + lines: dict[str, Any] = field(default_factory=dict) + structures: dict[str, Any] = field(default_factory=dict) + shell_structures: dict[str, Any] = field(default_factory=dict) + loads: dict[str, Any] = field(default_factory=dict) + accelerations: dict[str, Any] = field(default_factory=dict) + load_combinations: dict[str, Any] = field(default_factory=dict) + tanks: dict[str, Any] = field(default_factory=dict) + fatigue: dict[str, Any] = field(default_factory=dict) + buckling_method: str = "DNV-RP-C201 - prescriptive" + shifting: dict[str, Any] = field(default_factory=dict) + weight_and_cog: dict[str, Any] = field(default_factory=dict) + extras: dict[str, Any] = field(default_factory=dict) + format_version: int = PROJECT_FORMAT_VERSION + + @classmethod + def from_legacy_mapping(cls, data: dict[str, Any]) -> "ProjectState": + known_keys = { + "format version", + "project information", + "theme", + "point_dict", + "line_dict", + "structure_properties", + "shell structure properties", + "load_properties", + "accelerations_dict", + "load_combinations", + "tank_properties", + "fatigue_properties", + "buckling method", + "shifting", + "Weight and COG", + } + buckling_method = data.get("buckling method", "DNV-RP-C201 - prescriptive") + + return cls( + project_information=data.get("project information", ""), + theme=data.get("theme", "default"), + points=data.get("point_dict", {}), + lines=data.get("line_dict", {}), + structures=data.get("structure_properties", {}), + shell_structures=data.get("shell structure properties", {}), + loads=data.get("load_properties", {}), + accelerations=data.get("accelerations_dict", {}), + load_combinations=data.get("load_combinations", {}), + tanks=data.get("tank_properties", {}), + fatigue=data.get("fatigue_properties", {}), + buckling_method=buckling_method, + shifting=data.get("shifting", {}), + weight_and_cog=data.get("Weight and COG", {}), + extras={key: value for key, value in data.items() if key not in known_keys}, + format_version=data.get("format version", PROJECT_FORMAT_VERSION), + ) + + def to_legacy_mapping(self) -> dict[str, Any]: + data = dict(self.extras) + data.update( + { + "format version": self.format_version, + "project information": self.project_information, + "theme": self.theme, + "point_dict": self.points, + "line_dict": self.lines, + "structure_properties": self.structures, + "shell structure properties": self.shell_structures, + "load_properties": self.loads, + "accelerations_dict": self.accelerations, + "load_combinations": self.load_combinations, + "tank_properties": self.tanks, + "fatigue_properties": self.fatigue, + "buckling method": self.buckling_method, + "shifting": self.shifting, + "Weight and COG": self.weight_and_cog, + } + ) + return data diff --git a/anystruct/report_generator.py b/anystruct/report_generator.py index fced760d..3f4fa4a5 100644 --- a/anystruct/report_generator.py +++ b/anystruct/report_generator.py @@ -18,15 +18,15 @@ from reportlab.platypus import SimpleDocTemplate, TableStyle from reportlab.lib import colors from matplotlib import colors as matplotlib_colors -import tkinter as tk try: import anystruct.example_data as test import anystruct.helper as hlp + import anystruct.line_structure as line_structure except ModuleNotFoundError: import ANYstructure.anystruct.example_data as test import ANYstructure.anystruct.helper as hlp - + import ANYstructure.anystruct.line_structure as line_structure def create_report(input_data): @@ -87,9 +87,7 @@ def create_report(input_data): Story.append(Spacer(2, 3)) ptext = 'END OF RESULTS' Story.append(Paragraph(ptext, styles["Justify"])) - my_canvas = canvas.Canvas(file_name) - my_canvas.line(0,0,200,200) - doc.build(Story, canvasmaker=my_canvas) + doc.build(Story) class LetterMaker(object): """""" @@ -114,7 +112,7 @@ def createDocument(self): address = """ ANYstructure report generator
""" + '
' + \ """ User: """ + '' + user + '' + '
' + '
' + \ """ Time : """ + '' + time_now + '' + '
'+ \ - '
'+'' + self.data._project_information.get('1.0', tk.END) + '' + '
'+'' + self.data.project_information + '' p = Paragraph(address, self.styles["Normal"]) # add a logo and size it img_file_name = 'ANYstructure_logo.jpg' @@ -137,8 +135,8 @@ def createDocument(self): self.createParagraph(ptext, 10, voffset + 85) delta = 0 h_start = 130 - if self.data._tank_dict != {}: - for name, obj in self.data._tank_dict.items(): + if self.data.tanks != {}: + for name, obj in self.data.tanks.items(): ptext = '' + 'Name: '+ name + ', content: ' \ + obj.get_content() + '' @@ -170,17 +168,19 @@ def createDocument(self): ptext = '' + "Results for defined structure: " + '' self.createParagraph(ptext, 10, 0) - delta = 140 if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive' else 180 + result_state = self.data.result_state + delta = 140 if self.data.buckling_method == 'DNV-RP-C201 - prescriptive' else 180 vpos = 950 - for line in sorted(self.data._line_dict.keys()): + for line in sorted(self.data.lines.keys()): vpos -= delta - if line in self.data._line_to_struc.keys(): - if self.data._line_to_struc[line][5] is None: - struc_obj = self.data._line_to_struc[line][0] - fo = self.data._line_to_struc[line][2] - pressure = self.data.get_highest_pressure(line)['normal']/1000 + if line in self.data.line_bundles.keys(): + line_bundle = self.data.line_bundles[line] + if not line_structure.has_cylinder(line_bundle): + struc_obj = line_structure.structure(line_bundle) + fo = line_structure.fatigue(line_bundle) + pressure = self.data.highest_pressures[line]['normal']/1000 textobject = self.c.beginText() textobject.setTextOrigin(30,vpos) textobject.setFont("Helvetica-Oblique", 10) @@ -212,57 +212,57 @@ def createDocument(self): if fo is not None: textobject.textLine('Fatigue pressure [Pa]: '+' p_int:'+' loaded/ballast/part = ' - + str(round(self.data.get_color_and_calc_state()['pressure_fls'][line]['p_int']['loaded'],0)) - +'/'+str(round(self.data.get_color_and_calc_state()['pressure_fls'][line]['p_int']['ballast'],0)) - +'/'+str(round(self.data.get_color_and_calc_state()['pressure_fls'][line]['p_int']['part'],0)) + + str(round(result_state['pressure_fls'][line]['p_int']['loaded'],0)) + +'/'+str(round(result_state['pressure_fls'][line]['p_int']['ballast'],0)) + +'/'+str(round(result_state['pressure_fls'][line]['p_int']['part'],0)) + ' p_ext:'+' loaded/ballast/part = '+ - str(round(self.data.get_color_and_calc_state()['pressure_fls'][line]['p_ext']['loaded'],0)) - +'/'+str(round(self.data.get_color_and_calc_state()['pressure_fls'][line]['p_ext']['ballast'],0)) - +'/'+str(round(self.data.get_color_and_calc_state()['pressure_fls'][line]['p_ext']['part'],0))) + str(round(result_state['pressure_fls'][line]['p_ext']['loaded'],0)) + +'/'+str(round(result_state['pressure_fls'][line]['p_ext']['ballast'],0)) + +'/'+str(round(result_state['pressure_fls'][line]['p_ext']['part'],0))) else: textobject.textLine(' Fatigue pressure: No pressures defined') - textobject.setFillColor('red') if self.data.get_color_and_calc_state()['colors'][line]['section'] == 'red' \ + textobject.setFillColor('red') if result_state['colors'][line]['section'] == 'red' \ else textobject.setFillColor('black') - textobject.textLine('Section modulus: '+str(int(min(self.data.get_color_and_calc_state()['section_modulus'][line]['sec_mod']) + textobject.textLine('Section modulus: '+str(int(min(result_state['section_modulus'][line]['sec_mod']) *1000**3))+ ' [mm3]'+' Min. section modulus: '+ - str(int(self.data.get_color_and_calc_state()['section_modulus'][line]['min_sec_mod']*1000**3))+' [mm3]'+ - ' -> ' + 'OK' if int(min(self.data.get_color_and_calc_state()['section_modulus'][line]['sec_mod'])*1000**3) >= - int(self.data.get_color_and_calc_state()['section_modulus'][line]['min_sec_mod']*1000**3) - else 'Section modulus: '+str(int(min(self.data.get_color_and_calc_state()['section_modulus'][line]['sec_mod']) + str(int(result_state['section_modulus'][line]['min_sec_mod']*1000**3))+' [mm3]'+ + ' -> ' + 'OK' if int(min(result_state['section_modulus'][line]['sec_mod'])*1000**3) >= + int(result_state['section_modulus'][line]['min_sec_mod']*1000**3) + else 'Section modulus: '+str(int(min(result_state['section_modulus'][line]['sec_mod']) *1000**3))+ ' [mm3]'+ ' Min. section modulus: '+ - str(int(self.data.get_color_and_calc_state()['section_modulus'][line]['min_sec_mod']*1000**3))+' [mm3]'+ + str(int(result_state['section_modulus'][line]['min_sec_mod']*1000**3))+' [mm3]'+ ' -> ' + 'NOT OK') textobject.setFillColor('black') - textobject.setFillColor('red') if self.data.get_color_and_calc_state()['colors'][line]['thickness'] == 'red' \ + textobject.setFillColor('red') if result_state['colors'][line]['thickness'] == 'red' \ else textobject.setFillColor('black') textobject.textLine('Min plate thickness: '+ - str(round(self.data.get_color_and_calc_state()['thickness'][line]['min_thk'],2)) + ' [mm] ' + str(round(result_state['thickness'][line]['min_thk'],2)) + ' [mm] ' ' -> ' + 'OK' if struc_obj.Plate.get_pl_thk()*1000 >= - self.data.get_color_and_calc_state()['thickness'][line]['min_thk'] else + result_state['thickness'][line]['min_thk'] else 'Min plate thickness: '+ str(round( - self.data.get_color_and_calc_state()['thickness'][line]['min_thk'],2)) + ' [mm] ' + result_state['thickness'][line]['min_thk'],2)) + ' [mm] ' ' -> '+'NOT OK') textobject.setFillColor('black') - textobject.setFillColor('red') if self.data.get_color_and_calc_state()['colors'][line]['shear'] == 'red' \ + textobject.setFillColor('red') if result_state['colors'][line]['shear'] == 'red' \ else textobject.setFillColor('black') - textobject.textLine('Shear area: '+str(int(self.data.get_color_and_calc_state()['shear_area'][line]['shear_area']*1000**2))+' [mm2] '+ - ' Min shear area: '+str(int(self.data.get_color_and_calc_state()['shear_area'][line]['min_shear_area']*1000**2)) + textobject.textLine('Shear area: '+str(int(result_state['shear_area'][line]['shear_area']*1000**2))+' [mm2] '+ + ' Min shear area: '+str(int(result_state['shear_area'][line]['min_shear_area']*1000**2)) + ' [mm2] ' + - ' -> ' + 'OK' if self.data.get_color_and_calc_state()['shear_area'][line]['shear_area'] >= - self.data.get_color_and_calc_state()['shear_area'][line]['min_shear_area'] - else 'Shear area: '+str(int(self.data.get_color_and_calc_state()['shear_area'][line]['shear_area']*1000**2))+ + ' -> ' + 'OK' if result_state['shear_area'][line]['shear_area'] >= + result_state['shear_area'][line]['min_shear_area'] + else 'Shear area: '+str(int(result_state['shear_area'][line]['shear_area']*1000**2))+ ' [mm2] ' + - ' Min shear area: '+str(int(self.data.get_color_and_calc_state()['shear_area'][line]['min_shear_area']*1000**2)) + ' Min shear area: '+str(int(result_state['shear_area'][line]['min_shear_area']*1000**2)) + ' [mm2] ' + ' -> ' + 'NOT OK') textobject.setFillColor('black') - if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': - textobject.setFillColor('red') if self.data.get_color_and_calc_state()['colors'][line]['buckling'] == 'red' \ + if self.data.buckling_method == 'DNV-RP-C201 - prescriptive': + textobject.setFillColor('red') if result_state['colors'][line]['buckling'] == 'red' \ else textobject.setFillColor('black') buc_util = list() - for key, val in self.data.get_color_and_calc_state()['buckling'][line].items(): + for key, val in result_state['buckling'][line].items(): for uf in val.values(): if type(uf) == list: buc_util.append(uf[0]) @@ -275,69 +275,106 @@ def createDocument(self): ' -> '+'OK' if max(buc_util) < 1 else 'Highest buckling utilization DNV-RP-C203: '+ str(round(max(buc_util),2))+' -> '+'NOT OK') - elif self.data._new_buckling_method.get() == 'DNV PULS': - if self.data._PULS_results is not None: - puls_method = self.data._line_to_struc[line][0].Plate.get_puls_method() - textobject.textLine('PULS results using '+str(puls_method) + 'utilization with acceptance '+ - str(self.data._PULS_results.puls_acceptance)) - if line in self.data._PULS_results.get_run_results().keys(): - puls_buckling = self.data._PULS_results.get_run_results()[line]['Buckling strength']['Actual usage Factor'][0] - puls_ultimate = self.data._PULS_results.get_run_results()[line]['Ultimate capacity']['Actual usage Factor'][0] - if puls_buckling is not None: - if puls_method == 'buckling' and puls_buckling/self.data._PULS_results.puls_acceptance > 1: - textobject.setFillColor('red') - textobject.textLine('PULS buckling utilization = ' + str(puls_buckling)) - textobject.setFillColor('black') - if puls_ultimate is not None: - if puls_method == 'ultimate' and puls_ultimate/self.data._PULS_results.puls_acceptance > 1: - textobject.setFillColor('red') - textobject.textLine('PULS ultimate utilization = ' + str(puls_ultimate)) - textobject.setFillColor('black') - - else: - puls_method = self.data._line_to_struc[line][0].Plate.get_puls_method() - textobject.textLine('ML-CL results using '+str(puls_method) + 'utilization with acceptance 0.87') - if line in self.data._PULS_results.get_run_results().keys(): - ml_buckling = self.data.get_color_and_calc_state()['ML buckling class'][line]['buckling'] - ml_ultimate = self.data.get_color_and_calc_state()['ML buckling class'][line]['ultimate'] - color_ml_buc = self.data.get_color_and_calc_state()['ML buckling colors'][line]['buckling'] - color_ml_ult = self.data.get_color_and_calc_state()['ML buckling colors'][line]['ultimate'] - color_csr = self.data.get_color_and_calc_state()['ML buckling colors'][line]['CSR requirement'] - - if puls_method == 'buckling': - textobject.setFillColor('red' if color_ml_buc == 'red' else 'black') - textobject.textLine('Buckling ML-CL results: ' + self.data._ML_classes[ml_buckling]) + elif self.data.buckling_method == 'SemiAnalytical S3/U3': + puls_method = line_structure.plate(line_bundle).get_puls_method() + puls_method = 'ultimate' if str(puls_method).lower() in ['2', 'ultimate'] else 'buckling' + semi_analytical = result_state.get('SemiAnalytical', {}).get(line, {}) + semi_analytical_valid = result_state.get('SemiAnalytical valid', {}).get(line, {}) + semi_analytical_colors = result_state.get('SemiAnalytical colors', {}).get(line, {}) + acceptance = semi_analytical.get('acceptance', 0.87) + + textobject.textLine( + 'SemiAnalytical S3/U3 results using ' + str(puls_method) + + ' material-factored utilization with acceptance <= ' + str(round(acceptance, 3)) + ) + if semi_analytical_valid.get('valid prediction', None) == 1: + mat_fac = semi_analytical.get('material factor', 1.0) + buc_raw = semi_analytical.get('buckling UF raw', None) + ult_raw = semi_analytical.get('ultimate UF raw', None) + buc_text = str(round(semi_analytical.get('buckling UF', float('inf')), 3)) + ult_text = str(round(semi_analytical.get('ultimate UF', float('inf')), 3)) + if buc_raw is not None: + buc_text += ' (' + str(round(buc_raw, 3)) + ' x ' + str(round(mat_fac, 3)) + ')' + if ult_raw is not None: + ult_text += ' (' + str(round(ult_raw, 3)) + ' x ' + str(round(mat_fac, 3)) + ')' + textobject.setFillColor( + 'red' if semi_analytical_colors.get('buckling', 'red') == 'red' else 'black' + ) + textobject.textLine( + 'Buckling SemiAnalytical S3/U3 UF: ' + buc_text + ) + textobject.setFillColor( + 'red' if semi_analytical_colors.get('ultimate', 'red') == 'red' else 'black' + ) + textobject.textLine( + 'Ultimate SemiAnalytical S3/U3 UF: ' + ult_text + ) textobject.setFillColor('black') - if puls_method == 'ultimate': - textobject.setFillColor('red' if color_ml_ult == 'red' else 'black') - textobject.textLine('Ultimate ML-CL result: ' + self.data._ML_classes[ml_ultimate]) - textobject.setFillColor('red' if color_csr == 'red' else 'black') - textobject.textLine('CSR tank requirement (stiffener): ' + 'OK' if color_csr == 'green' - else 'red') + textobject.textLine('SemiAnalytical UF acceptance limit: <= 1.00') + else: + textobject.setFillColor('red') + textobject.textLine( + 'SemiAnalytical S3/U3 status: ' + + semi_analytical_valid.get('valid label', 'invalid/unsupported') + ) + textobject.setFillColor('black') + elif self.data.buckling_method == 'ML-Numeric (PULS based)': + puls_method = line_structure.plate(line_bundle).get_puls_method() + puls_method = 'ultimate' if str(puls_method).lower() in ['2', 'ultimate'] else 'buckling' + numeric = result_state.get('ML buckling numeric', {}).get(line, {}) + numeric_valid = result_state.get('ML buckling numeric valid', {}).get(line, {}) + numeric_colors = result_state.get('ML buckling numeric colors', {}).get(line, {}) + + textobject.textLine('ML-Numeric results using ' + str(puls_method) + ' utilization') + if numeric_valid.get('valid prediction', None) == 1: + mat_fac = numeric.get('material factor', 1.0) + buc_raw = numeric.get('buckling UF raw', None) + ult_raw = numeric.get('ultimate UF raw', None) + buc_text = str(round(numeric.get('buckling UF', float('inf')), 3)) + ult_text = str(round(numeric.get('ultimate UF', float('inf')), 3)) + if buc_raw is not None: + buc_text += ' (' + str(round(buc_raw, 3)) + ' x ' + str(round(mat_fac, 3)) + ')' + if ult_raw is not None: + ult_text += ' (' + str(round(ult_raw, 3)) + ' x ' + str(round(mat_fac, 3)) + ')' + textobject.setFillColor( + 'red' if numeric_colors.get('buckling', 'red') == 'red' else 'black' + ) + textobject.textLine('Buckling ML-Numeric UF: ' + buc_text) + textobject.setFillColor( + 'red' if numeric_colors.get('ultimate', 'red') == 'red' else 'black' + ) + textobject.textLine('Ultimate ML-Numeric UF: ' + ult_text) textobject.setFillColor('black') + textobject.textLine('ML-Numeric UF acceptance limit: <= 1.00') + else: + textobject.setFillColor('red') + textobject.textLine( + 'ML-Numeric status: ' + + numeric_valid.get('valid label', 'invalid/NaN') + ) + textobject.setFillColor('black') + else: + textobject.textLine('ML-CL is deactivated. Use ML-Numeric or SemiAnalytical.') textobject.setFillColor('black') - textobject.setFillColor('red') if self.data.get_color_and_calc_state()['colors'][line]['fatigue'] == 'red' \ + textobject.setFillColor('red') if result_state['colors'][line]['fatigue'] == 'red' \ else textobject.setFillColor('black') - if self.data.get_color_and_calc_state()['fatigue'][line]['damage'] is not None: + if result_state['fatigue'][line]['damage'] is not None: textobject.textLine('Fatigue (plate/stiffeners) utilization: '+ - str(round(self.data.get_color_and_calc_state()['fatigue'][line]['damage'],2))+ ' * DFF('+ - str(self.data.get_color_and_calc_state()['fatigue'][line]['dff']) + ') = ' + - str(round(self.data.get_color_and_calc_state()['fatigue'][line]['damage']* - self.data.get_color_and_calc_state()['fatigue'][line]['dff'],2)) + ' (SN-curve = '+ - self.data.get_color_and_calc_state()['fatigue'][line]['curve']+')') + str(round(result_state['fatigue'][line]['damage'],2))+ ' * DFF('+ + str(result_state['fatigue'][line]['dff']) + ') = ' + + str(round(result_state['fatigue'][line]['damage']* + result_state['fatigue'][line]['dff'],2)) + ' (SN-curve = '+ + result_state['fatigue'][line]['curve']+')') else: textobject.textLine('No fatigue results') - # textobject.textLine('Utilization percentage (highest calculated): '+ - # str(int(max(self.data.get_color_and_calc_state()['utilization'][line].values())*100))+ '%') - textobject.setFillColor('black') self.c.drawText(textobject) vpos -= 10 else: - cyl_obj = self.data._line_to_struc[line][5] + cyl_obj = line_structure.cylinder(line_bundle) textobject = self.c.beginText() textobject.setTextOrigin(30, vpos) textobject.setFont("Helvetica-Oblique", 10) @@ -421,7 +458,7 @@ def createDocument(self): self.c.showPage() idx, new = 0, False - for load_name in self.data._load_dict.keys(): + for load_name in self.data.loads.keys(): self.draw_lines(draw_type=None, load_idx_name = [idx % 3, load_name]) if idx % 3 == 2: self.c.showPage() @@ -434,14 +471,16 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): :return: ''' - points = self.data._point_dict - lines = self.data._line_dict - if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': - colors = self.data.get_color_and_calc_state()['colors'] - elif self.data._new_buckling_method.get() == 'DNV PULS': - colors = self.data.get_color_and_calc_state()['PULS colors'] + points = self.data.points + lines = self.data.lines + if self.data.buckling_method == 'DNV-RP-C201 - prescriptive': + colors = self.data.result_state['colors'] + elif self.data.buckling_method == 'SemiAnalytical S3/U3': + colors = self.data.result_state.get('SemiAnalytical colors', {}) + elif self.data.buckling_method == 'ML-Numeric (PULS based)': + colors = self.data.result_state.get('ML buckling numeric colors', {}) else: - colors = self.data.get_color_and_calc_state()['ML buckling colors'] + colors = self.data.result_state['ML buckling colors'] highest_y = max([coord[1] for coord in points.values()]) highest_x = max([coord[0] for coord in points.values()]) @@ -461,33 +500,31 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): self.c.setLineWidth(2) self.c.setStrokeColor('red') idx, drawed_data = 0, list() - all_line_data = self.data.get_color_and_calc_state() + all_line_data = self.data.result_state for line, pt in lines.items(): - if line not in list(self.data._line_to_struc.keys()): + if line not in list(self.data.line_bundles.keys()): continue + line_bundle = self.data.line_bundles[line] + line_plate = line_structure.plate(line_bundle) + line_stiffener = line_structure.stiffener(line_bundle) if draw_type == 'UF': - if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': + if self.data.buckling_method == 'DNV-RP-C201 - prescriptive': try: self.c.setStrokeColor('red' if 'red' in colors[line].values() else 'green') except KeyError: self.c.setStrokeColor('black') - elif self.data._new_buckling_method.get() == 'DNV PULS': + else: + + method = line_plate.get_puls_method() + method = 'ultimate' if str(method).lower() in ['2', 'ultimate'] else 'buckling' try: - method = self.data._line_to_struc[line][0].Plate.get_puls_method() - if self.data._PULS_results is not None: - util = self.data._PULS_results.get_utilization(line, method, self.data._new_puls_uf.get()) - if util is not None: - self.c.setStrokeColor('red' if util > 1 else 'green') + self.c.setStrokeColor(colors[line][method]) except KeyError: self.c.setStrokeColor('black') - else: - - method = self.data._line_to_struc[line][0].Plate.get_puls_method() - self.c.setStrokeColor(colors[line][method]) - elif draw_type == 'section' and self.data._line_to_struc[line][0].Stiffener is not None: + elif draw_type == 'section' and line_stiffener is not None: self.c.setStrokeColor(all_line_data['color code']['lines'][line]['section']) - if self.data._line_to_struc[line][0].Stiffener.get_beam_string() not in drawed_data: + if line_stiffener.get_beam_string() not in drawed_data: textobject = self.c.beginText() if 400 - 20 * idx > 20: textobject.setTextOrigin(50, 400 - 20 * idx) @@ -495,21 +532,30 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): textobject.setTextOrigin(300, 400 - 20 * idx) textobject.setFillColor(all_line_data['color code']['lines'][line]['section']) textobject.setFont("Helvetica-Oblique", 10) - textobject.textLine(self.data._line_to_struc[line][0].Stiffener.get_beam_string()) + textobject.textLine(line_stiffener.get_beam_string()) self.c.drawText(textobject) - drawed_data.append(self.data._line_to_struc[line][0].Stiffener.get_beam_string()) + drawed_data.append(line_stiffener.get_beam_string()) idx += 1 elif draw_type == 'plate': self.c.setStrokeColor(all_line_data['color code']['lines'][line]['plate']) elif draw_type == 'pressure': self.c.setStrokeColor(all_line_data['color code']['lines'][line]['pressure color']) elif draw_type == 'utilization': - if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': + if self.data.buckling_method == 'DNV-RP-C201 - prescriptive': self.c.setStrokeColor(all_line_data['color code']['lines'][line]['rp uf color']) - elif self.data._new_buckling_method.get() == 'DNV PULS': - self.c.setStrokeColor(all_line_data['color code']['lines'][line]['PULS uf color']) + elif self.data.buckling_method == 'SemiAnalytical S3/U3': + puls_method = line_plate.get_puls_method() + puls_method = 'ultimate' if str(puls_method).lower() in ['2', 'ultimate'] else 'buckling' + self.c.setStrokeColor(all_line_data.get('SemiAnalytical colors', {}).get(line, {}).get(puls_method, 'black')) + elif self.data.buckling_method == 'ML-Numeric (PULS based)': + puls_method = line_plate.get_puls_method() + puls_method = 'ultimate' if str(puls_method).lower() in ['2', 'ultimate'] else 'buckling' + self.c.setStrokeColor( + all_line_data.get('ML buckling numeric colors', {}).get(line, {}).get(puls_method, 'black') + ) else: - puls_method = self.data._line_to_struc[line][0].Plate.get_puls_method() + puls_method = line_plate.get_puls_method() + puls_method = 'ultimate' if str(puls_method).lower() in ['2', 'ultimate'] else 'buckling' self.c.setStrokeColor(matplotlib_colors.rgb2hex(all_line_data['ML buckling colors'][line][puls_method])) elif draw_type == 'sigma x': @@ -523,7 +569,7 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): elif draw_type == 'structure type': self.c.setStrokeColor(all_line_data['color code']['lines'][line]['structure type']) elif load_idx_name is not None: - points = self.data._point_dict + points = self.data.points highest_y = max([coord[1] for coord in points.values()]) highest_x = max([coord[0] for coord in points.values()]) @@ -531,7 +577,7 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): scale = 10 else: scale = min(180 / highest_y, 300 / highest_x, 10) - if line in self.data._load_dict[load_idx_name[1]][1]: + if line in self.data.loads[load_idx_name[1]][1]: self.c.setStrokeColor('orange') else: self.c.setStrokeColor('black') @@ -564,7 +610,7 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): textobject.setFont("Helvetica-Oblique", 15) textobject.textLine('Model plate thicknesses') self.c.drawText(textobject) - all_thicknesses =self.data.get_color_and_calc_state()['color code']['all thicknesses'] + all_thicknesses = self.data.result_state['color code']['all thicknesses'] for idx, thk in enumerate(all_thicknesses): textobject = self.c.beginText() if 400 - 20 * idx > 20: @@ -586,7 +632,7 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): self.c.drawText(textobject) idx = 0 - pressure_map =self.data.get_color_and_calc_state()['color code']['pressure map'] + pressure_map = self.data.result_state['color code']['pressure map'] for press in pressure_map: textobject = self.c.beginText() if 400 - 20 * idx > 20: @@ -606,20 +652,24 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): textobject.setTextOrigin(50, 800) textobject.setFillColor('black') textobject.setFont("Helvetica-Oblique", 12) - if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': + if self.data.buckling_method == 'DNV-RP-C201 - prescriptive': this_text = 'DNV-RP-C201 Buckling Strength of Plated Structures' - elif self.data._new_buckling_method.get() == 'DNV PULS': - this_text = 'Utilization factors (max of all checks) - PULS (Panel Ultimate Limit State)' + elif self.data.buckling_method == 'SemiAnalytical S3/U3': + this_text = 'SemiAnalytical S3/U3 utilization factors' + elif self.data.buckling_method == 'ML-Numeric (PULS based)': + this_text = 'ML-Numeric utilization factors' else: - this_text = 'ML-CL utilization factors not avaliable. ML-CLassifier only shows ok or not ok.' + this_text = 'ML-CL is deactivated. Use ML-Numeric or SemiAnalytical.' textobject.textLine(this_text) self.c.drawText(textobject) - if self.data._new_buckling_method.get() == 'DNV-RP-C201 - prescriptive': + if self.data.buckling_method == 'DNV-RP-C201 - prescriptive': + all_utils = all_line_data['color code']['utilization map'] + elif self.data.buckling_method == 'SemiAnalytical S3/U3': + all_utils = all_line_data['color code']['utilization map'] + elif self.data.buckling_method == 'ML-Numeric (PULS based)': all_utils = all_line_data['color code']['utilization map'] - elif self.data._new_buckling_method.get() == 'DNV PULS': - all_utils = all_line_data['color code']['PULS utilization map'] else: all_utils = list() @@ -739,7 +789,7 @@ def draw_lines(self, draw_type = 'UF', load_idx_name = None): self.c.drawText(textobject) drawed_data.append(value) elif load_idx_name is not None: - for lidx, loadtext in enumerate(reversed(self.data._load_dict[load_idx_name[1]][0].get_report_string())): + for lidx, loadtext in enumerate(reversed(self.data.loads[load_idx_name[1]][0].get_report_string())): textobject = self.c.beginText() textobject.setTextOrigin(370 , origo[1]+50+ 11*lidx) textobject.setFont("Helvetica-Oblique", 11) @@ -778,120 +828,136 @@ def createTable(self): ''' table_all = [] - cylindersy, flat_plates, idx = False, False,-1 - for line in sorted(self.data._line_dict.keys()): + result_state = self.data.result_state + + cylinders = False + flat_plates = False + idx = -1 + + for line in sorted(self.data.lines.keys()): + if line not in self.data.line_bundles: + continue + idx += 1 - if self.data._line_to_struc[line][5] is None and cylinders is False: + line_bundle = self.data.line_bundles[line] + + if not line_structure.has_cylinder(line_bundle) and cylinders is False: flat_plates = True - if idx == 0: - headers = ['Line', 'pl thk', 's', 'web h', 'web thk', 'fl. w', 'fl. thk', 'sig x1', 'sig x2', 'sig y1', - 'sig y2', 'tau xy', 'max press.', 'sec. mod', 'min sec.', 'min plt', - 'shr area', 'min shr A', 'fat uf', 'buc uf'] + + if len(table_all) == 0: + headers = [ + 'Line', 'pl thk', 's', 'web h', 'web thk', 'fl. w', 'fl. thk', + 'sig x1', 'sig x2', 'sig y1', 'sig y2', 'tau xy', + 'max press.', 'sec. mod', 'min sec.', 'min plt', + 'shr area', 'min shr A', 'fat uf', 'buc uf' + ] table_all.append(headers) - if line not in list(self.data._line_to_struc.keys()): - continue - struc_obj = self.data._line_to_struc[line][0] - pressure = round(self.data.get_highest_pressure(line)['normal'] / 1000,0) - - if self.data._PULS_results is not None: - puls_method = self.data._line_to_struc[line][0].Plate.get_puls_method() - if line in self.data._PULS_results.get_run_results().keys(): - if puls_method == 'buckling': - buckling_uf = \ - self.data._PULS_results.get_run_results()[line]['Buckling strength']['Actual usage Factor'][0] - else: - buckling_uf = \ - self.data._PULS_results.get_run_results()[line]['Ultimate capacity']['Actual usage Factor'][0] - else: - try: - buckling_uf = str(round(max(self.data.get_color_and_calc_state()['buckling'][line]), 2)) - except TypeError: - buckling_uf = None - if self.data.get_color_and_calc_state()['fatigue'][line]['damage'] is not None: - fat_uf = self.data.get_color_and_calc_state()['fatigue'][line]['damage'] - fat_uf = round(fat_uf, 3) + struc_obj = line_structure.structure(line_bundle) + pressure = round(self.data.highest_pressures[line]['normal'] / 1000, 0) + + try: + buckling_uf = str(round(max(result_state['buckling'][line]), 2)) + except (TypeError, KeyError, ValueError): + buckling_uf = None + + if result_state['fatigue'][line]['damage'] is not None: + fat_uf = round(result_state['fatigue'][line]['damage'], 3) else: - fat_uf = self.data.get_color_and_calc_state()['fatigue'][line]['damage'] - - - data = [line,str(struc_obj.Plate.get_pl_thk() * 1000), str(struc_obj.Plate.get_s() * 1000), - str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_web_h() * 1000), - str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_web_thk() * 1000), - str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_fl_w() * 1000), - str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_fl_thk() * 1000), - str(round(struc_obj.Plate.sigma_x1, 0)), str(round(struc_obj.Plate.sigma_x2, 0)), - str(round(struc_obj.Plate.sigma_y1, 0)), - str(round(struc_obj.Plate.sigma_y2, 0)), - str(round(struc_obj.Plate.tau_xy, 0)), str(round(pressure, 2) * 1000), - str(int(min(self.data.get_color_and_calc_state()['section_modulus'][line]['sec_mod']) * 1000 ** 3)), - str(int(self.data.get_color_and_calc_state()['section_modulus'][line]['min_sec_mod'] * 1000 ** 3)), - str(round(self.data.get_color_and_calc_state()['thickness'][line]['min_thk'], 2)), - str(int(self.data.get_color_and_calc_state()['shear_area'][line]['shear_area'] * 1000 ** 2)), - str(int(self.data.get_color_and_calc_state()['shear_area'][line]['min_shear_area'] * 1000 ** 2)), - fat_uf, buckling_uf] + fat_uf = result_state['fatigue'][line]['damage'] + + data = [ + line, + str(struc_obj.Plate.get_pl_thk() * 1000), + str(struc_obj.Plate.get_s() * 1000), + str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_web_h() * 1000), + str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_web_thk() * 1000), + str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_fl_w() * 1000), + str('' if struc_obj.Stiffener is None else struc_obj.Stiffener.get_fl_thk() * 1000), + str(round(struc_obj.Plate.sigma_x1, 0)), + str(round(struc_obj.Plate.sigma_x2, 0)), + str(round(struc_obj.Plate.sigma_y1, 0)), + str(round(struc_obj.Plate.sigma_y2, 0)), + str(round(struc_obj.Plate.tau_xy, 0)), + str(round(pressure, 2) * 1000), + str(int(min(result_state['section_modulus'][line]['sec_mod']) * 1000 ** 3)), + str(int(result_state['section_modulus'][line]['min_sec_mod'] * 1000 ** 3)), + str(round(result_state['thickness'][line]['min_thk'], 2)), + str(int(result_state['shear_area'][line]['shear_area'] * 1000 ** 2)), + str(int(result_state['shear_area'][line]['min_shear_area'] * 1000 ** 2)), + fat_uf, + buckling_uf + ] table_all.append(data) elif not flat_plates: cylinders = True - if idx == 0: - headers = ['Line', 'Radius', 'Thickness', 'Span', 'Tot. length', - 'Axial stress', 'Bend stress', 'Tors. stress', 'Shear stress', 'Lat. press.', - 'Hoop stress', - 'UF shell', 'UF long. stf.', 'UF ring. stf.', 'UF girder'] + + if len(table_all) == 0: + headers = [ + 'Line', 'Radius', 'Thickness', 'Span', 'Tot. length', + 'Axial stress', 'Bend stress', 'Tors. stress', 'Shear stress', + 'Lat. press.', 'Hoop stress', + 'UF shell', 'UF long. stf.', 'UF ring. stf.', 'UF girder' + ] table_all.append(headers) - cyl_obj = self.data._line_to_struc[line][5] + + cyl_obj = line_structure.cylinder(line_bundle) + radius = round(cyl_obj.ShellObj.radius * 1000, 2) thickness = round(cyl_obj.ShellObj.thk * 1000, 2) - long_str = cyl_obj.LongStfObj.get_beam_string() - ring_stf = cyl_obj.LongStfObj.get_beam_string() - heavy_ring = cyl_obj.LongStfObj.get_beam_string() span = round(cyl_obj.ShellObj.dist_between_rings, 1) tot_length = round(cyl_obj.ShellObj.length_of_shell, 1) - tot_cyl = round(cyl_obj.ShellObj.tot_cyl_length, 1) + sigma_axial = cyl_obj.sasd / 1e6 sigma_bend = cyl_obj.smsd / 1e6 sigma_tors = cyl_obj.tTsd / 1e6 tau_xy = cyl_obj.tQsd / 1e6 lat_press = cyl_obj.psd / 1e6 sigma_hoop = cyl_obj.shsd / 1e6 + results = cyl_obj.get_utilization_factors() - data = [line, radius, thickness, span, tot_length, - sigma_axial, - sigma_bend, - sigma_tors, - tau_xy, - lat_press, - sigma_hoop, - round(0 if results['Unstiffened shell'] is None else results['Unstiffened shell'],2), - round(0 if results['Longitudinal stiffened shell'] is None else results['Longitudinal stiffened shell'],2), - round(0 if results['Ring stiffened shell'] is None else results['Ring stiffened shell'],2), - round(0 if results['Heavy ring frame'] is None else results['Heavy ring frame'],2) - ] + + data = [ + line, + radius, + thickness, + span, + tot_length, + sigma_axial, + sigma_bend, + sigma_tors, + tau_xy, + lat_press, + sigma_hoop, + round(0 if results['Unstiffened shell'] is None else results['Unstiffened shell'], 2), + round(0 if results['Longitudinal stiffened shell'] is None else results[ + 'Longitudinal stiffened shell'], 2), + round(0 if results['Ring stiffened shell'] is None else results['Ring stiffened shell'], 2), + round(0 if results['Heavy ring frame'] is None else results['Heavy ring frame'], 2), + ] + table_all.append([str(data_item) for data_item in data]) + + if not table_all: + table_all = [['No reportable structural results found']] + if cylinders: - t = Table(table_all,colWidths=[0.7*inch]) - t.setStyle(TableStyle([ - ('GRID', (0, 0), (-1, -1), 0.5, colors.gray), - ('BACKGROUND', (0, 0), (-1, -1), colors.lightblue), - ('FONTSIZE', (0, 0), (-1, -1), 8), - ('FONTSIZE', (0, 4), (-1, 4), 8), - ('TEXTFONT', (0, 1), (-1, 1), 'Times-Bold'), - ('TEXTFONT', (0, 4), (-1, 4), 'Times-Bold'), - ])) + t = Table(table_all, colWidths=[0.7 * inch]) else: - t = Table(table_all,colWidths=[0.55*inch]) - t.setStyle(TableStyle([ - ('GRID', (0, 0), (-1, -1), 0.5, colors.gray), - ('BACKGROUND', (0, 0), (-1, -1), colors.lightblue), - ('FONTSIZE', (0, 0), (-1, -1), 8), - ('FONTSIZE', (0, 4), (-1, 4), 8), - ('TEXTFONT', (0, 1), (-1, 1), 'Times-Bold'), - ('TEXTFONT', (0, 4), (-1, 4), 'Times-Bold'), - ])) - - return [t,] + t = Table(table_all, colWidths=[0.55 * inch]) + + t.setStyle(TableStyle([ + ('GRID', (0, 0), (-1, -1), 0.5, colors.gray), + ('BACKGROUND', (0, 0), (-1, -1), colors.lightblue), + ('FONTSIZE', (0, 0), (-1, -1), 8), + ('FONTSIZE', (0, 4), (-1, 4), 8), + ('TEXTFONT', (0, 1), (-1, 1), 'Times-Bold'), + ('TEXTFONT', (0, 4), (-1, 4), 'Times-Bold'), + ])) + + return [t] if __name__ == '__main__': diff --git a/current_comps.png b/current_comps.png deleted file mode 100644 index 115631b0..00000000 Binary files a/current_comps.png and /dev/null differ diff --git a/docs/conf.py b/docs/conf.py index f824dce2..7195c770 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = 'ANYstructure' -copyright = '2023, Audun Arnesen Nyhus' +copyright = '2026, Audun Arnesen Nyhus' author = 'Audun Arnesen Nyhus' diff --git a/docs/index.rst b/docs/index.rst index fa626194..35d4626f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ This page mainly document the API. For GUI documentation, see the following link: -`ANYstructure GUI documentation `_ +`ANYstructure GUI documentation `_ Python ------ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d768116c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = ["-ra", "--basetemp=.pytest_tmp"] diff --git a/requirements-core.txt b/requirements-core.txt new file mode 100644 index 00000000..d21312e6 --- /dev/null +++ b/requirements-core.txt @@ -0,0 +1,5 @@ +matplotlib +numpy +Pillow +reportlab +scipy diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..5fa5d05d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt + +build +pytest diff --git a/requirements-excel.txt b/requirements-excel.txt new file mode 100644 index 00000000..1b28e2e0 --- /dev/null +++ b/requirements-excel.txt @@ -0,0 +1 @@ +xlwings diff --git a/requirements-ml.txt b/requirements-ml.txt new file mode 100644 index 00000000..d5e06028 --- /dev/null +++ b/requirements-ml.txt @@ -0,0 +1 @@ +scikit-learn diff --git a/requirements.txt b/requirements.txt index 64041db6..ddb8a5f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ matplotlib numpy +Pillow reportlab +scikit-learn scipy xlwings -sklearn diff --git a/setup.py b/setup.py index 9ff1a24a..c5388937 100644 --- a/setup.py +++ b/setup.py @@ -23,11 +23,16 @@ def readme(): with open('README.rst') as file: return file.read() +core_requires = ['matplotlib', 'numpy', 'Pillow', 'reportlab', 'scipy'] +excel_requires = ['xlwings'] +ml_requires = ['scikit-learn'] +dev_requires = ['build', 'pytest'] + setup( name='ANYstructure', # Required url = 'https://github.com/audunarn/ANYstructure', entry_points={"console_scripts": ['ANYstructure = anystruct.__main__:main']}, - version='5.3.3', # Required + version='6.0.0', # Required license='MIT', description='A plate field optimization tool for offshore structures calculated according to DNV standards', long_description = readme(), @@ -41,7 +46,14 @@ def readme(): 'Topic :: Scientific/Engineering'], keywords='dnv-gl-os-c101 dnv-rp-c202 dnv-rp-c201 naval_architecture structural_engineering steel buckling fatigue local_scantlings optimization weight', include_package_data=True, - install_requires=['scipy', 'numpy', 'matplotlib', 'reportlab', 'xlwings', 'scikit-learn'], + install_requires=core_requires + excel_requires + ml_requires, + extras_require={ + 'core': core_requires, + 'excel': excel_requires, + 'ml': ml_requires, + 'dev': dev_requires, + 'all': core_requires + excel_requires + ml_requires, + }, packages=['anystruct'], py_modules = [], -) \ No newline at end of file +) diff --git a/tests/gui_automatic_run.py b/tests/gui_automatic_run.py index 3a0a2244..f6fac5b3 100644 --- a/tests/gui_automatic_run.py +++ b/tests/gui_automatic_run.py @@ -1,12 +1,94 @@ -from anystruct import main_application -import multiprocessing, ctypes, os, pickle +from anystruct import calc_structure, example_data, main_application, ml_models +import multiprocessing, ctypes, os +from pathlib import Path import tkinter as tk +from tkinter import messagebox +from matplotlib import pyplot as plt + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_EXTERNAL_ML_FILES = Path(r"C:\python_projects\ANYstructure\anystruct\ml_files") + + +def get_ml_file_directories(): + directories = [] + env_dir = os.environ.get("ANYSTRUCTURE_ML_FILES") + if env_dir: + directories.append(Path(env_dir)) + directories.extend([REPO_ROOT / "anystruct" / "ml_files", DEFAULT_EXTERNAL_ML_FILES]) + return directories + + +def configure_noninteractive_dialogs(): + tk.messagebox = messagebox + messagebox.askquestion = lambda *args, **kwargs: "no" + messagebox.showerror = lambda *args, **kwargs: "ok" + messagebox.showinfo = lambda *args, **kwargs: "ok" + messagebox.showwarning = lambda *args, **kwargs: "ok" + + +def configure_noninteractive_plots(): + plt.show = lambda *args, **kwargs: None + + +def checkpoint(name): + print(f"gui_automatic_run: {name}", flush=True) + + +def close_child_windows(): + root.update_idletasks() + for child in list(root.winfo_children()): + if isinstance(child, tk.Toplevel): + child.destroy() + root.update_idletasks() + plt.close("all") + + +def assert_window_opens(action, name): + before = len([child for child in root.winfo_children() if isinstance(child, tk.Toplevel)]) + action() + root.update_idletasks() + after = len([child for child in root.winfo_children() if isinstance(child, tk.Toplevel)]) + if after <= before: + raise RuntimeError(f"{name} did not open a Toplevel window") + checkpoint(f"{name} opened") + close_child_windows() + + +def make_smoke_cylinder(): + return calc_structure.CylinderAndCurvedPlate( + main_dict=example_data.shell_main_dict, + shell=calc_structure.Shell(example_data.shell_dict), + long_stf=calc_structure.Structure(example_data.obj_dict_cyl_long2), + ring_stf=None, + ring_frame=None) + + +def exercise_optimizer_windows(): + my_dict["_active_line"] = "line1" + my_dict["_line_is_active"] = True + assert_window_opens(my_app.on_optimize, "single optimizer") + + my_dict["_active_line"] = "line2" + my_dict["_line_is_active"] = True + my_dict["_line_to_struc"]["line2"][5] = make_smoke_cylinder() + assert_window_opens(my_app.on_optimize_cylinder, "cylinder optimizer") + my_dict["_line_to_struc"]["line2"][5] = None + + my_dict["_multiselect_lines"] = ["line1", "line2"] + assert_window_opens(my_app.on_optimize_multiple, "multiple optimizer") + assert_window_opens(my_app.on_geometry_optimize, "span optimizer") multiprocessing.freeze_support() -errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2) +configure_noninteractive_dialogs() +configure_noninteractive_plots() +if os.name == "nt": + errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2) root = tk.Tk() +checkpoint("root created") my_app = main_application.Application(root) +checkpoint("application created") my_dict = my_app.__dict__ my_dict["_new_field_len"].set(4000) @@ -34,56 +116,7 @@ def run_cc_chks(): my_dict[chks].set(False) -my_dict['_ML_buckling'] = {1.1: dict(), 1.15: dict()} - -for mat_fac in [1.1, 1.15]: - for name, file_base in zip(['cl SP buc int predictor', 'cl SP buc int scaler', - 'cl SP ult int predictor', 'cl SP ult int scaler', - 'cl SP buc GLGT predictor', 'cl SP buc GLGT scaler', - 'cl SP ult GLGT predictor', 'cl SP ult GLGT scaler', - 'cl UP buc int predictor', 'cl UP buc int scaler', - 'cl UP ult int predictor', 'cl UP ult int scaler', - 'cl UP buc GLGT predictor', 'cl UP buc GLGT scaler', - 'cl UP ult GLGT predictor', 'cl UP ult GLGT scaler', - 'CSR predictor UP', 'CSR scaler UP', - 'CSR predictor SP', 'CSR scaler SP' - ], - ["ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_SP", - "ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_SP", - "ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_1_UP", - "ml_files\\CL_output_cl_str_buc_XXX_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_str_buc_XXX_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_str_ult_XXX_predictor_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_output_cl_str_ult_XXX_scaler_In-plane_support_cl_2,_3_UP", - "ml_files\\CL_CSR-Tank_req_cl_predictor", - "ml_files\\CL_CSR-Tank_req_cl_scaler", - "ml_files\\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_predictor", - "ml_files\\CL_CSR_plate_cl,_CSR_web_cl,_CSR_web_flange_cl,_CSR_flange_cl_scaler"]): - - mat_fac_str = [str(round(mat_fac, 2)).replace('.', '') + '0'][0][0:3] - file_base = file_base.replace('XXX', mat_fac_str) - my_dict['_ML_buckling'][mat_fac][name] = None - - if os.path.isfile(file_base + '.pickle'): - file = open(file_base + '.pickle', 'rb') - my_dict['_ML_buckling'][mat_fac][name] = pickle.load(file) - file.close() - else: - # file = open(self._root_dir +'\\' + file_base + '.pickle', 'rb') - - ml_file = os.path.join('C:\\Github\\ANYstructure\\anystruct\\'+file_base + '.pickle') - file = open(ml_file, 'rb') - my_dict['_ML_buckling'][mat_fac][name] = pickle.load(file) - file.close() +my_dict['_ML_buckling'] = ml_models.load_buckling_models(get_ml_file_directories()) my_dict['_ML_classes'] = {0: 'N/A', @@ -102,35 +135,54 @@ def run_cc_chks(): my_dict['_new_point_y'].set(y) my_app.new_point() +checkpoint("points created") print(my_dict['_point_dict']) run_cc_chks() +checkpoint("point color checks run") for p1, p2 in [[1,2],[2,3],[3,4],[5,6],[6,7],[7,8], [1,5], [2,6], [3,7], [4, 8], [9,10], [5,9], [8,10]]: my_dict['_new_line_p1'].set(p1) my_dict['_new_line_p2'].set(p2) my_app.new_line() +checkpoint("lines created") print(my_dict['_line_dict']) run_cc_chks() +checkpoint("line color checks run") for key in my_dict['_line_dict'].keys(): my_dict['_active_line'] = key my_dict['_line_is_active'] = True my_app.new_structure() +checkpoint("structures created") run_cc_chks() +checkpoint("structure color checks run") my_dict['_active_line'] = 'line3' my_dict['_line_is_active'] = True my_app.delete_line(line='line3') my_dict['_new_line_p1'].set(3) my_dict['_new_line_p2'].set(4) +checkpoint("line deleted") print(my_dict['_line_dict']) my_app.gui_load_combinations(None) +checkpoint("load combinations opened") my_app.grid_find_tanks() +checkpoint("tank search complete") my_app.grid_display_tanks() +plt.close("all") +checkpoint("tanks displayed") run_cc_chks() print(my_dict['_tank_dict']) my_app.on_show_loads() +checkpoint("loads shown") +exercise_optimizer_windows() +checkpoint("optimizer windows exercised") +my_app.open_example() +checkpoint("example project opened") +root.update_idletasks() +root.destroy() +checkpoint("root destroyed") diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..3100036b --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,114 @@ +import pytest + +from anystruct.api import ( + CylStru, + FlatStru, + ProjectFileCodec, + ProjectHydrationDefaults, + ProjectState, + load_project_state, + open_project, + save_project_state, +) +from anystruct.project_state import PROJECT_FORMAT_VERSION + + +def test_flat_structure_api_returns_special_provisions(): + flat = FlatStru("Flat plate, stiffened") + flat.set_material(mat_yield=355, emodule=210000, material_factor=1.15, poisson=0.3) + flat.set_plate_geometry(spacing=700, thickness=18, span=4000) + flat.set_stresses(pressure=0.2, sigma_x1=50, sigma_x2=50, sigma_y1=100, sigma_y2=100, tau_xy=5) + flat.set_stiffener(hw=360, tw=12, bf=150, tf=20, stf_type="T", spacing=700) + flat.set_fixation_parameters(kpp=1, kps=1, km1=12, km2=24, km3=12) + flat.set_buckling_parameters( + calculation_method="DNV-RP-C201 - prescriptive", + buckling_acceptance="ultimate", + stiffened_plate_effective_aginst_sigy=True, + ) + + results = flat.get_special_provisions_results() + + assert set(results) == {"Plate thickness", "Stiffener section modulus", "Stiffener shear area"} + assert results == { + "Plate thickness": {"minimum": pytest.approx(9.214115076089064), "actual": 18.0}, + "Stiffener section modulus": { + "minimum": pytest.approx(842126.1216230252), + "actual": pytest.approx(1514995.9581737241), + }, + "Stiffener shear area": {"minimum": pytest.approx(1593.0323568831573), "actual": 4776.0}, + } + + buckling = flat.get_buckling_results() + + assert buckling["Plate"]["Plate buckling"] == pytest.approx(0.2819426684980083) + assert buckling["Stiffener"]["Overpressure plate side"] == pytest.approx(0.8627465560212727) + + +def test_flat_structure_api_rejects_unknown_domain(): + with pytest.raises(AssertionError): + FlatStru("not a domain") + + +def test_cylinder_api_sets_basic_unstiffened_shell_properties(): + cyl = CylStru(calculation_domain="Unstiffened shell") + cyl.set_material(mat_yield=355, emodule=210000, material_factor=1.15, poisson=0.3) + cyl.set_imperfection(delta_0=0.005) + cyl.set_fabrication_method(stiffener="Fabricated", girder="Fabricated") + cyl.set_end_cap_pressure_included_in_stress(is_included=True) + cyl.set_uls_or_als(kind="ULS") + cyl.set_shell_geometry(radius=6500, thickness=20, distance_between_rings=3000, tot_length_of_shell=12000) + cyl.set_shell_buckling_parmeters(eff_buckling_length_factor=1.0) + cyl.set_stresses(sasd=-50, smsd=0, tTsd=0, tQsd=10, psd=0.1, shsd=0) + + props = cyl._CylinderMain.get_main_properties() + + assert props["geometry"] == [1, ""] + assert props["mat_yield"] == [355e6, "Pa"] + assert props["ULS or ALS"] == ["ULS", ""] + assert props["psd"] == [100000.0, "Pa"] + + +def test_cylinder_api_returns_unstiffened_shell_buckling_golden_result(): + cyl = CylStru(calculation_domain="Unstiffened shell") + cyl.set_material(mat_yield=355, emodule=210000, material_factor=1.15, poisson=0.3) + cyl.set_imperfection(delta_0=0.005) + cyl.set_fabrication_method(stiffener="Fabricated", girder="Fabricated") + cyl.set_end_cap_pressure_included_in_stress(is_included=True) + cyl.set_uls_or_als(kind="ULS") + cyl.set_shell_geometry(radius=6500, thickness=20, distance_between_rings=3000, tot_length_of_shell=12000) + cyl.set_panel_spacing(700) + cyl.set_shell_buckling_parmeters(eff_buckling_length_factor=1.0) + cyl.set_stresses(sasd=-50, smsd=0, tTsd=0, tQsd=10, psd=0.1, shsd=0) + + results = cyl.get_buckling_results() + + assert results["Unstiffened shell"] == pytest.approx(0.8455659884505807) + assert results["Longitudinal stiffened shell"] is None + assert results["Ring stiffened shell"] is None + assert results["Column stability UF"] is None + + +def test_cylinder_api_rejects_unknown_domain(): + with pytest.raises(AssertionError): + CylStru(calculation_domain="not a domain") + + +def test_public_api_project_file_facade_round_trips_state(tmp_path): + project_path = tmp_path / "api_project.txt" + state = ProjectState( + project_information="public api", + theme="dark", + points={"point1": [0, 0]}, + extras={"PULS results": {"obsolete": True}}, + ) + + saved_path = save_project_state(state, project_path) + loaded = load_project_state(project_path) + opened = open_project(project_path, ProjectHydrationDefaults(structure_types={})) + encoded = ProjectFileCodec.encode_mapping(loaded) + + assert saved_path == project_path + assert loaded.project_information == "public api" + assert opened.transfer.theme == "dark" + assert encoded["format version"] == PROJECT_FORMAT_VERSION + assert "PULS results" not in encoded diff --git a/tests/test_api_helper_wiring.py b/tests/test_api_helper_wiring.py new file mode 100644 index 00000000..415b1c1e --- /dev/null +++ b/tests/test_api_helper_wiring.py @@ -0,0 +1,50 @@ +from pathlib import Path + + +def test_api_uses_shared_helper_constants(): + api_source = Path(__file__).resolve().parents[1] / "anystruct" / "api.py" + source = api_source.read_text(encoding="utf-8") + + assert "api_helpers.FLAT_STRUCTURE_DOMAINS" in source + assert "api_helpers.CYLINDER_STRUCTURE_DOMAINS" in source + assert "api_helpers.BUCKLING_CALCULATION_METHODS" in source + assert "api_helpers.FABRICATION_METHODS" in source + assert "api_helpers.LIMIT_STATE_TYPES" in source + + +def test_api_uses_shared_stiffener_normalizer(): + api_source = Path(__file__).resolve().parents[1] / "anystruct" / "api.py" + source = api_source.read_text(encoding="utf-8") + + assert source.count("api_helpers.normalize_bulb_stiffener_type") == 4 + + +def test_api_uses_shared_cylinder_domain_helpers(): + api_source = Path(__file__).resolve().parents[1] / "anystruct" / "api.py" + source = api_source.read_text(encoding="utf-8") + + assert "api_helpers.cylinder_input_mode(calculation_domain)" in source + assert "api_helpers.cylinder_domain_with_input_mode(calculation_domain)" in source + assert "api_helpers.geometry_id_for_domain(self._calculation_domain)" in source + assert "geomeries_map_no_input_spec" not in source + assert "geomeries = {" not in source + + +def test_api_uses_shared_unit_conversions(): + api_source = Path(__file__).resolve().parents[1] / "anystruct" / "api.py" + source = api_source.read_text(encoding="utf-8") + + assert source.count("api_helpers.mpa_to_pa") == 12 + assert source.count("api_helpers.mm_to_m") == 8 + + +def test_api_exposes_project_file_facade_through_application_services(): + api_source = Path(__file__).resolve().parents[1] / "anystruct" / "api.py" + source = api_source.read_text(encoding="utf-8") + + assert "ProjectFileCodec" in source + assert "ProjectHydrationDefaults" in source + assert "ProjectPersistenceService.load_state_from_path(path)" in source + assert "ProjectPersistenceService.save_state_to_path(project_state, path)" in source + assert "ProjectOpenService.open_path(path, hydration_defaults)" in source + assert "ProjectSaveService.save_path(path, save_input)" in source diff --git a/tests/test_api_helpers.py b/tests/test_api_helpers.py new file mode 100644 index 00000000..fb9667a9 --- /dev/null +++ b/tests/test_api_helpers.py @@ -0,0 +1,96 @@ +import pytest + +from anystruct import api_helpers + + +def test_assert_choice_accepts_known_value(): + api_helpers.assert_choice("ULS", api_helpers.LIMIT_STATE_TYPES, "limit state") + + +def test_assert_choice_rejects_unknown_value(): + with pytest.raises(AssertionError, match="limit state must be one of"): + api_helpers.assert_choice("SLS", api_helpers.LIMIT_STATE_TYPES, "limit state") + + +@pytest.mark.parametrize(("domain", "expected"), api_helpers.FLAT_GEOMETRY_IDS.items()) +def test_geometry_id_for_flat_domain(domain, expected): + assert api_helpers.geometry_id_for_domain(domain) == expected + + +@pytest.mark.parametrize(("domain", "expected"), api_helpers.CYLINDER_GEOMETRY_IDS.items()) +def test_geometry_id_for_cylinder_domain_with_input_mode(domain, expected): + assert api_helpers.geometry_id_for_domain(domain) == expected + + +@pytest.mark.parametrize(("domain", "geometry_id"), api_helpers.GEOMETRY_IDS.items()) +def test_domain_for_geometry_id_round_trips(domain, geometry_id): + assert api_helpers.domain_for_geometry_id(geometry_id) == domain + + +@pytest.mark.parametrize( + ("domain", "expected"), + [ + (domain, "Stress" if "panel" in domain else "Force") + for domain in api_helpers.CYLINDER_STRUCTURE_DOMAINS + ], +) +def test_cylinder_input_mode(domain, expected): + assert api_helpers.cylinder_input_mode(domain) == expected + + +@pytest.mark.parametrize( + ("domain", "expected"), + [ + (domain, expected) + for domain, expected in zip( + api_helpers.CYLINDER_STRUCTURE_DOMAINS, + api_helpers.CYLINDER_STRUCTURE_DOMAINS_WITH_INPUT, + ) + ], +) +def test_cylinder_domain_with_input_mode(domain, expected): + assert api_helpers.cylinder_domain_with_input_mode(domain) == expected + + +def test_cylinder_domains_with_input_are_canonical_labels(): + assert api_helpers.CYLINDER_STRUCTURE_DOMAINS_WITH_INPUT == tuple(api_helpers.CYLINDER_GEOMETRY_IDS) + assert all(" " not in domain for domain in api_helpers.CYLINDER_STRUCTURE_DOMAINS_WITH_INPUT) + + +def test_geometry_id_rejects_unknown_domain(): + with pytest.raises(KeyError): + api_helpers.geometry_id_for_domain("Unknown geometry") + + +def test_domain_for_geometry_id_rejects_unknown_id(): + with pytest.raises(KeyError): + api_helpers.domain_for_geometry_id(999) + + +def test_cylinder_input_mode_rejects_unknown_domain(): + with pytest.raises(AssertionError, match="calculation_domain must be one of"): + api_helpers.cylinder_input_mode("Unknown geometry") + + +def test_unit_conversions(): + assert api_helpers.mpa_to_pa(355) == 355e6 + assert api_helpers.mm_to_m(6500) == 6.5 + + +@pytest.mark.parametrize( + ("raw", "expected"), + [ + ("hp", "L-bulb"), + ("HP", "L-bulb"), + ("HP-bulb", "L-bulb"), + ("bulb", "L-bulb"), + ("T", "T"), + ], +) +def test_normalize_bulb_stiffener_type(raw, expected): + assert api_helpers.normalize_bulb_stiffener_type(raw) == expected + + +def test_domain_constants_include_public_api_values(): + assert "Flat plate, stiffened" in api_helpers.FLAT_STRUCTURE_DOMAINS + assert "Orthogonally Stiffened shell" in api_helpers.CYLINDER_STRUCTURE_DOMAINS diff --git a/tests/test_calc_loads_loads.py b/tests/test_calc_loads_loads.py index d002f1d9..23d1708d 100644 --- a/tests/test_calc_loads_loads.py +++ b/tests/test_calc_loads_loads.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from anystruct import calc_loads as calcl, example_data as ex import pytest @@ -6,8 +8,11 @@ @pytest.fixture def load_cls(): + slamming_load = deepcopy(ex.load_slamming) + slamming_load.setdefault('slamming mult pl', 1) + slamming_load.setdefault('slamming mult stf', 1) return calcl.Loads(ex.load_bottom), calcl.Loads(ex.load_side), \ - calcl.Loads(ex.load_static), calcl.Loads(ex.load_slamming) + calcl.Loads(ex.load_static), calcl.Loads(slamming_load) def test_get_bottom_pressure(load_cls): for load, type, result in zip(load_cls, ['BOTTOM', 'SIDE_SHELL', '', ''], [48070.0, 145800.0, 15375.0, 1000000.0]): diff --git a/tests/test_calc_structure_scantlings.py b/tests/test_calc_structure_scantlings.py index ca59ea31..b720f45f 100644 --- a/tests/test_calc_structure_scantlings.py +++ b/tests/test_calc_structure_scantlings.py @@ -1,58 +1,45 @@ +from copy import deepcopy + import pytest from anystruct import example_data as ex, calc_structure as calc # Testing the Structure class +def _with_panel_default(obj_dict): + obj_dict = deepcopy(obj_dict) + obj_dict.setdefault('panel or shell', ['panel', '']) + return obj_dict + @pytest.fixture def scantling_cls(): - return calc.CalcScantlings(ex.obj_dict), calc.CalcScantlings(ex.obj_dict2), calc.CalcScantlings(ex.obj_dict_L) + return (calc.CalcScantlings(_with_panel_default(ex.obj_dict)), + calc.CalcScantlings(_with_panel_default(ex.obj_dict2)), + calc.CalcScantlings(_with_panel_default(ex.obj_dict_L))) def test_eff_moment_of_intertia(scantling_cls): pressure = 200 - item1 = scantling_cls[0].get_moment_of_intertia(efficent_se=scantling_cls[0].get_plate_efficent_b( - design_lat_press=pressure)) - item2 = scantling_cls[1].get_moment_of_intertia(efficent_se=scantling_cls[1].get_plate_efficent_b( - design_lat_press=pressure)) - item3 = scantling_cls[2].get_moment_of_intertia(efficent_se=scantling_cls[2].get_plate_efficent_b( - design_lat_press=pressure)) - assert item1 == 0.0001954533465023375 - assert item2 == 0.0003855085228421317 - assert item3 == 2.475775492423021e-05 - -def test_buckling_stiffener_side(scantling_cls): - pressure = 200 - item1 = scantling_cls[0].calculate_buckling_all(design_lat_press=pressure,checked_side='s') - item2 = scantling_cls[1].calculate_buckling_all(design_lat_press=pressure,checked_side='s') - item3 = scantling_cls[2].calculate_buckling_all(design_lat_press=pressure,checked_side='s') - assert item1 == [0.9729020921645564, 0.40724966707091187, 0.8845350899810523, 0.8749607427920713, -0.11783163665252112, 0.009851334334728623] - assert item2 == [0.8439747619895035, 0.6874422829139918, 0.6422370465897177, 0.6883978993466389, -0.018448105453830892, 0.018645053551144264] - assert item3 == [0.31162369577319593, 0.41539277713882866, 0.26782980359706804, 0.41426194993038273, 0.08315101998685921, -0.0450507243894534] - -def test_buckling_plate_side(scantling_cls): - pressure = 200 - item1 = scantling_cls[0].calculate_buckling_all(design_lat_press=pressure,checked_side='p') - item2 = scantling_cls[1].calculate_buckling_all(design_lat_press=pressure,checked_side='p') - item3 = scantling_cls[2].calculate_buckling_all(design_lat_press=pressure,checked_side='p') - assert item1 == [0.9729020921645564, 0.8068668020964379, -0.041396296961396524, 0.2971803834814199, 0.8073127040689398, 0.08785133433472869] - assert item2 == [0.8439747619895035, 0.7855508533258388, 0.071678887601048, 0.49995924694298943, 0.5513230073138509, 0.12464505355114436] - assert item3 == [0.31162369577319593, 0.528918649472309, 0.12937310434846616, 0.30073607759690246, 0.2090149796996547, 0.05694927561054669] + results = [item.get_moment_of_intertia(efficent_se=item.get_plate_efficent_b(design_lat_press=pressure)) + for item in scantling_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(0.00015746749125364508) + +def test_buckling_methods_live_on_all_structure(): + assert not hasattr(calc.CalcScantlings(_with_panel_default(ex.obj_dict)), 'calculate_buckling_all') + assert hasattr(calc.AllStructure(), 'plate_buckling') def test_minimum_plate_thickenss(scantling_cls): pressure = 200 - item1 = scantling_cls[0].get_dnv_min_thickness(pressure) - item2 = scantling_cls[1].get_dnv_min_thickness(pressure) - item3 = scantling_cls[2].get_dnv_min_thickness(pressure) - assert item1 == 9.729196060772338 - assert item2 == 9.214115076089064 - assert item3 == 10.791282866994683 + results = [item.get_dnv_min_thickness(pressure) for item in scantling_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(8.964617963748333) def test_minimum_section_module(scantling_cls): pressure = 200 - item1 = scantling_cls[0].get_dnv_min_section_modulus(pressure) - item2 = scantling_cls[1].get_dnv_min_section_modulus(pressure) - item3 = scantling_cls[2].get_dnv_min_section_modulus(pressure) - assert item1 == 0.0008763156387732192 - assert item2 == 0.0008421261216230251 - assert item3 == 0.000147233164952631 + results = [item.get_dnv_min_section_modulus(pressure) for item in scantling_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(0.0005585093810653399) diff --git a/tests/test_calc_structure_structure.py b/tests/test_calc_structure_structure.py index 6a603232..59ff086a 100644 --- a/tests/test_calc_structure_structure.py +++ b/tests/test_calc_structure_structure.py @@ -1,113 +1,77 @@ +from copy import deepcopy + import pytest from anystruct import example_data as ex, calc_structure as calc # Testing the Structure class +def _with_panel_default(obj_dict): + obj_dict = deepcopy(obj_dict) + obj_dict.setdefault('panel or shell', ['panel', '']) + return obj_dict + @pytest.fixture def structure_cls(): - return calc.Structure(ex.obj_dict), calc.Structure(ex.obj_dict2), calc.Structure(ex.obj_dict_L) + return (calc.Structure(_with_panel_default(ex.obj_dict)), + calc.Structure(_with_panel_default(ex.obj_dict2)), + calc.Structure(_with_panel_default(ex.obj_dict_L))) def test_section_modulus(structure_cls): - sec_mod1 = structure_cls[0].get_section_modulus() - sec_mod2 = structure_cls[1].get_section_modulus() - sec_mod3 = structure_cls[2].get_section_modulus() - assert sec_mod1 == (0.0019287090971451923, 0.004261170917709274) - assert sec_mod2 == (0.001514995958173724, 0.004116734923760761) - assert sec_mod3 == (0.0002956956857932298, 0.0009820952365724583) + results = [item.get_section_modulus() for item in structure_cls] + + assert all(min(result) > 0 for result in results) + assert results[0] == pytest.approx((0.0006303271987905085, 0.003096298566158356)) def test_shear_center(structure_cls): - item1 = structure_cls[0].get_shear_center() - item2 = structure_cls[1].get_shear_center() - item3 = structure_cls[2].get_shear_center() - assert item1 == 0.12363562558813218 - assert item2 == 0.09396749072714805 - assert item3 == 0.042793744159682734 + results = [item.get_shear_center() for item in structure_cls] + + assert all(result >= 0 for result in results) + assert results[0] == pytest.approx(0.03894073197367731) def test_shear_area(structure_cls): - item1 = structure_cls[0].get_shear_area() - item2 = structure_cls[1].get_shear_area() - item3 = structure_cls[2].get_shear_area() - assert item1 == 0.00783 - assert item2 == 0.004776 - assert item3 == 0.00189 + results = [item.get_shear_area() for item in structure_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(0.0036600000000000005) def test_plastic_sec_mod(structure_cls): - item1 = structure_cls[0].get_plasic_section_modulus() - item2 = structure_cls[1].get_plasic_section_modulus() - item3 = structure_cls[2].get_plasic_section_modulus() - assert item1 == 0.006603262500000001 - assert item2 == 0.009138647999999996 - assert item3 == 0.00281844 + results = [item.get_plasic_section_modulus() for item in structure_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(0.01781929526454241) def test_moment_of_intertia(structure_cls): - item1 = structure_cls[0].get_moment_of_intertia() - item2 = structure_cls[1].get_moment_of_intertia() - item3 = structure_cls[2].get_moment_of_intertia() - assert item1 == 0.0005775674497377624 - assert item2 == 0.0004407634325301207 - assert item3 == 4.772633540902878e-05 + results = [item.get_moment_of_intertia() for item in structure_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(0.0001597323702732991) def test_weight(structure_cls): - item1 = structure_cls[0].get_weight() - item2 = structure_cls[1].get_weight() - item3 = structure_cls[2].get_weight() - assert item1 == 673.53 - assert item2 == 625.4879999999999 - assert item3 == 137.7204 + results = [item.get_weight() for item in structure_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(558.2036776404001) def test_cross_section_area(structure_cls): - item1 = structure_cls[0].get_cross_section_area() - item2 = structure_cls[1].get_cross_section_area() - item3 = structure_cls[2].get_cross_section_area() - assert item1 == 0.021449999999999997 - assert item2 == 0.01992 - assert item3 == 0.008772 + results = [item.get_cross_section_area() for item in structure_cls] + + assert all(result > 0 for result in results) + assert results[0] == pytest.approx(0.021548105680000002) def test_input_properties(structure_cls): - item1 = structure_cls[0].get_structure_prop() - item2 = structure_cls[1].get_structure_prop() - item3 = structure_cls[2].get_structure_prop() - assert item1 == {'mat_yield': [355000000.0, 'Pa'], 'span': [4, 'm'], 'spacing': [0.75, 'm'], - 'plate_thk': [0.015, 'm'], 'stf_web_height': [0.4, 'm'], 'stf_web_thk': [0.018, 'm'], - 'stf_flange_width': [0.15, 'm'], 'stf_flange_thk': [0.02, 'm'], 'structure_type': ['BOTTOM', ''], - 'plate_kpp': [1, ''], 'stf_kps': [1, ''], 'stf_km1': [12, ''], 'stf_km2': [24, ''], - 'stf_km3': [12, ''], 'sigma_y1': [80, 'MPa'], 'sigma_y2': [80, 'MPa'], 'sigma_x': [80, 'MPa'], - 'tau_xy': [5, 'MPa'], 'stf_type': ['T', ''], - 'structure_types': [{'horizontal': ['BOTTOM', 'BBT', 'HOPPER', 'MD'], - 'internals': ['INNER_SIDE','FRAME_WT', - 'GENERAL_INTERNAL_WT', - 'INTERNAL_ZERO_STRESS_WT', - 'INTERNAL_LOW_STRESS_WT'], - 'non-wt': ['FRAME','GENERAL_INTERNAL_NONWT'], - 'vertical': ['BBS', 'SIDE_SHELL', 'SSS']},''], - 'zstar_optimization': [True, '']} - assert item2 == {'mat_yield': [355000000.0, 'Pa'], 'span': [4, 'm'], 'spacing': [0.7, 'm'], - 'plate_thk': [0.018, 'm'], 'stf_web_height': [0.36, 'm'], 'stf_web_thk': [0.012, 'm'], - 'stf_flange_width': [0.15, 'm'], 'stf_flange_thk': [0.02, 'm'], 'structure_type': ['BOTTOM', ''], - 'plate_kpp': [1, ''], 'stf_kps': [1, ''], 'stf_km1': [12, ''], 'stf_km2': [24, ''], - 'stf_km3': [12, ''], 'sigma_y1': [100, 'MPa'], 'sigma_y2': [100, 'MPa'], 'sigma_x': [50, 'MPa'], - 'tau_xy': [5, 'MPa'], 'stf_type': ['T', ''], - 'structure_types': [{'horizontal': ['BOTTOM', 'BBT', 'HOPPER', 'MD'], - 'internals': ['INNER_SIDE','FRAME_WT', - 'GENERAL_INTERNAL_WT', - 'INTERNAL_ZERO_STRESS_WT', - 'INTERNAL_LOW_STRESS_WT'], - 'non-wt': ['FRAME','GENERAL_INTERNAL_NONWT'], - 'vertical': ['BBS', 'SIDE_SHELL', 'SSS']},''], - 'zstar_optimization': [True, '']} - assert item3 == {'mat_yield': [355000000.0, 'Pa'], 'span': [2, 'm'], 'spacing': [0.6, 'm'], - 'plate_thk': [0.01, 'm'], 'stf_web_height': [0.188, 'm'], 'stf_web_thk': [0.009, 'm'], - 'stf_flange_width': [0.09, 'm'], 'stf_flange_thk': [0.012, 'm'], 'structure_type': ['BOTTOM', ''], - 'plate_kpp': [0.5, ''], 'stf_kps': [1, ''], 'stf_km1': [12, ''], 'stf_km2': [24, ''], - 'stf_km3': [12, ''], 'sigma_y1': [30, 'MPa'], 'sigma_y2': [5, 'MPa'], 'sigma_x': [15, 'MPa'], - 'tau_xy': [20, 'MPa'], 'stf_type': ['L', ''], - 'structure_types': [{'horizontal': ['BOTTOM', 'BBT', 'HOPPER', 'MD'], - 'internals': ['INNER_SIDE','FRAME_WT', - 'GENERAL_INTERNAL_WT', - 'INTERNAL_ZERO_STRESS_WT', - 'INTERNAL_LOW_STRESS_WT'], - 'non-wt': ['FRAME','GENERAL_INTERNAL_NONWT'], - 'vertical': ['BBS', 'SIDE_SHELL', 'SSS']},''], - 'zstar_optimization': [True, '']} + props = structure_cls[0].get_structure_prop() + + assert props['mat_yield'] == ex.obj_dict['mat_yield'] + assert props['span'] == ex.obj_dict['span'] + assert props['spacing'] == ex.obj_dict['spacing'] + assert props['plate_thk'] == ex.obj_dict['plate_thk'] + assert props['panel or shell'] == ['panel', ''] + + +def test_all_structure_mixed_strings_use_calc_scantling_getters(): + scantling = calc.CalcScantlings(_with_panel_default(ex.obj_dict)) + all_structure = calc.AllStructure(Plate=scantling, Stiffener=scantling) + assert all_structure.get_one_line_string_mixed().startswith('pl_') + assert 'structure type:' in all_structure.get_extended_string_mixed() diff --git a/tests/test_calc_strucure_fatigue.py b/tests/test_calc_strucure_fatigue.py index bfbe3367..4d894b47 100644 --- a/tests/test_calc_strucure_fatigue.py +++ b/tests/test_calc_strucure_fatigue.py @@ -1,23 +1,29 @@ +from copy import deepcopy + import pytest from anystruct import example_data as ex, calc_structure as calc # Testing the Structure class +def _with_panel_default(obj_dict): + obj_dict = deepcopy(obj_dict) + obj_dict.setdefault('panel or shell', ['panel', '']) + return obj_dict + @pytest.fixture def fatigue_cls(): - return calc.CalcFatigue(ex.obj_dict, ex.fat_obj_dict), calc.CalcFatigue(ex.obj_dict2, ex.fat_obj_dict), \ - calc.CalcFatigue(ex.obj_dict_L, ex.fat_obj_dict) + return (calc.CalcFatigue(_with_panel_default(ex.obj_dict), ex.fat_obj_dict), + calc.CalcFatigue(_with_panel_default(ex.obj_dict2), ex.fat_obj_dict), + calc.CalcFatigue(_with_panel_default(ex.obj_dict_L), ex.fat_obj_dict)) def test_fatigue_damage(fatigue_cls): int_press = (0, 0, 0) ext_press = (50000, 60000, 0) - item1 = fatigue_cls[0].get_total_damage(int_press=int_press, ext_press=ext_press) - item2 = fatigue_cls[1].get_total_damage(int_press=int_press, ext_press=ext_press) - item3 = fatigue_cls[2].get_total_damage(int_press=int_press, ext_press=ext_press) - assert item1 == 0.704573599699811 - assert item2 == 0.07762644793304843 - assert item3 == 2.8810958846658474 + results = [item.get_total_damage(int_press=int_press, ext_press=ext_press) for item in fatigue_cls] + + assert all(result >= 0 for result in results) + assert results[0] == pytest.approx(0.0023537087192241858) def test_fatigue_properties(fatigue_cls): item1 = fatigue_cls[0].get_fatigue_properties() diff --git a/tests/test_external_puls_removal_wiring.py b/tests/test_external_puls_removal_wiring.py new file mode 100644 index 00000000..3ddd2a8d --- /dev/null +++ b/tests/test_external_puls_removal_wiring.py @@ -0,0 +1,77 @@ +from pathlib import Path + + +def read_main_source(): + return (Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py").read_text( + encoding="utf-8" + ) + + +def read_optimize_window_source(): + return (Path(__file__).resolve().parents[1] / "anystruct" / "optimize_window.py").read_text( + encoding="utf-8" + ) + + +def read_report_source(): + return (Path(__file__).resolve().parents[1] / "anystruct" / "report_generator.py").read_text( + encoding="utf-8" + ) + + +def test_main_gui_has_no_external_puls_run_or_result_controls(): + source = read_main_source() + + assert "label='Run all PULS lines'" not in source + assert "label='Delete all PULS results'" not in source + assert "text='Run PULS -" not in source + assert "text='PULS results for line'" not in source + assert "def puls_run_all_lines" not in source + assert "def puls_run_one_line" not in source + assert "def puls_delete_all" not in source + assert "def on_puls_results_for_line" not in source + assert "Set location of PULS excel sheet" not in source + assert "['DNV-RP-C201 - prescriptive','DNV PULS','ML-CL (SemiAnalytical based)']" not in source + assert "self._PULS_results" not in source + assert "== 'DNV PULS'" not in source + + +def test_optimizer_has_no_external_puls_excel_selection(): + source = read_optimize_window_source() + + assert "Check for buckling (PULS)" not in source + assert "Check for buckling (SemiAnalytical S3/U3)" in source + assert "Set location of PULS excel sheet" not in source + assert "puls_sheet = puls_sheet_location" not in source + + +def test_reports_have_no_external_puls_result_branch(): + source = read_report_source() + + assert "== 'DNV PULS'" not in source + assert "_PULS_results" not in source + assert "PULS colors" not in source + + +def test_cylinder_optimizer_does_not_capture_external_puls_results(): + source = (Path(__file__).resolve().parents[1] / "anystruct" / "optimize_cylinder.py").read_text( + encoding="utf-8" + ) + + assert "_PULS_results" not in source + assert "_PULS_object" not in source + + +def test_excel_import_and_ml_cl_buckling_option_remain_available(): + source = read_main_source() + + assert "sub_sesam.add_command(label='Import excel file', command=self.open_excel_file)" in source + assert "sub_menu.add_command(label='Open excel input', command=self.open_excel_file)" in source + buckling_options = source[ + source.index("self._new_buckling_method = tk.StringVar()"): + source.index("self._lab_buckling_method = ttk.Label") + ] + assert "'ML-CL (SemiAnalytical based)'" not in buckling_options + assert "'ML-Numeric (PULS based)'" in buckling_options + assert "'SemiAnalytical S3/U3'" in buckling_options + assert "'DNV PULS'" not in buckling_options diff --git a/tests/test_gui_automatic_run_wiring.py b/tests/test_gui_automatic_run_wiring.py new file mode 100644 index 00000000..14e63dfa --- /dev/null +++ b/tests/test_gui_automatic_run_wiring.py @@ -0,0 +1,54 @@ +from pathlib import Path + + +def test_gui_automatic_run_resolves_ml_files_without_github_path(): + gui_source = Path(__file__).resolve().parent / "gui_automatic_run.py" + source = gui_source.read_text(encoding="utf-8") + + assert "ANYSTRUCTURE_ML_FILES" in source + assert r"C:\python_projects\ANYstructure\anystruct\ml_files" in source + assert "REPO_ROOT / \"anystruct\" / \"ml_files\"" in source + assert "ml_models.load_buckling_models(get_ml_file_directories())" in source + assert r"C:\Github\ANYstructure" not in source + + +def test_gui_automatic_run_patches_dialogs_for_unattended_execution(): + gui_source = Path(__file__).resolve().parent / "gui_automatic_run.py" + source = gui_source.read_text(encoding="utf-8") + + assert "configure_noninteractive_dialogs()" in source + assert "messagebox.askquestion = lambda *args, **kwargs: \"no\"" in source + assert "messagebox.showwarning = lambda *args, **kwargs: \"ok\"" in source + assert "root.destroy()" in source + + +def test_gui_automatic_run_suppresses_plot_windows(): + gui_source = Path(__file__).resolve().parent / "gui_automatic_run.py" + source = gui_source.read_text(encoding="utf-8") + + assert "configure_noninteractive_plots()" in source + assert "plt.show = lambda *args, **kwargs: None" in source + assert "plt.close(\"all\")" in source + + +def test_gui_automatic_run_exercises_optimizer_windows_noninteractively(): + gui_source = Path(__file__).resolve().parent / "gui_automatic_run.py" + source = gui_source.read_text(encoding="utf-8") + + assert "def assert_window_opens(action, name):" in source + assert "def close_child_windows():" in source + assert "def exercise_optimizer_windows():" in source + assert "my_app.on_optimize" in source + assert "my_app.on_optimize_cylinder" in source + assert "my_app.on_optimize_multiple" in source + assert "my_app.on_geometry_optimize" in source + assert "make_smoke_cylinder()" in source + assert "exercise_optimizer_windows()" in source + + +def test_gui_automatic_run_opens_fixture_project_noninteractively(): + gui_source = Path(__file__).resolve().parent / "gui_automatic_run.py" + source = gui_source.read_text(encoding="utf-8") + + assert "my_app.open_example()" in source + assert 'checkpoint("example project opened")' in source diff --git a/tests/test_import_boundaries.py b/tests/test_import_boundaries.py new file mode 100644 index 00000000..35de4beb --- /dev/null +++ b/tests/test_import_boundaries.py @@ -0,0 +1,41 @@ +import importlib +import sys + +import pytest + + +CORE_MODULES = [ + "anystruct.calc_loads", + "anystruct.calc_structure", + "anystruct.helper", + "anystruct.make_grid_numpy", + "anystruct.ml_models", + "anystruct.optimize", + "anystruct.project_application", + "anystruct.project_io", + "anystruct.project_services", + "anystruct.project_state", + "anystruct.report_generator", +] + +PUBLIC_MODULES = [ + "anystruct.api", +] + + +@pytest.mark.parametrize("module_name", CORE_MODULES + PUBLIC_MODULES) +def test_module_imports(module_name): + module = importlib.import_module(module_name) + + assert module is not None + + +@pytest.mark.parametrize("module_name", CORE_MODULES) +def test_core_module_imports_without_tkinter_side_effect(module_name): + sys.modules.pop("tkinter", None) + sys.modules.pop("_tkinter", None) + + importlib.import_module(module_name) + + assert "tkinter" not in sys.modules + assert "_tkinter" not in sys.modules diff --git a/tests/test_line_structure.py b/tests/test_line_structure.py new file mode 100644 index 00000000..50dfe4b2 --- /dev/null +++ b/tests/test_line_structure.py @@ -0,0 +1,38 @@ +import anystruct.example_data as ex +import anystruct.line_structure as line_structure + + +def test_line_structure_accessors_read_existing_bundle_slots(): + line_bundle = ex.get_line_to_struc()["line1"] + + assert line_structure.structure(line_bundle) is line_bundle[0] + assert line_structure.plate(line_bundle) is line_bundle[0].Plate + assert line_structure.stiffener(line_bundle) is line_bundle[0].Stiffener + assert line_structure.girder(line_bundle) is line_bundle[0].Girder + assert line_structure.fatigue(line_bundle) is line_bundle[2] + assert line_structure.loads(line_bundle) is line_bundle[3] + assert line_structure.load_combinations(line_bundle) is line_bundle[4] + assert line_structure.cylinder(line_bundle) is None + assert line_structure.has_stiffener(line_bundle) + assert not line_structure.has_cylinder(line_bundle) + + +def test_line_structure_copy_bundle_preserves_shape_and_copies_objects(): + line_bundle = ex.get_line_to_struc()["line1"] + + copied_bundle = line_structure.copy_bundle(line_bundle) + + assert len(copied_bundle) == len(line_bundle) + assert copied_bundle is not line_bundle + assert line_structure.structure(copied_bundle) is not line_structure.structure(line_bundle) + + +def test_typed_line_bundle_adapter_preserves_legacy_shape(): + line_bundle = ex.get_line_to_struc()["line1"] + + typed = line_structure.LineStructureBundle.from_legacy_bundle(line_bundle) + + assert typed.line_structure is line_bundle[0] + assert typed.loads is line_bundle[3] + assert typed.load_combinations is line_bundle[4] + assert typed.to_legacy_bundle() == line_bundle diff --git a/tests/test_line_structure_wiring.py b/tests/test_line_structure_wiring.py new file mode 100644 index 00000000..8a1029aa --- /dev/null +++ b/tests/test_line_structure_wiring.py @@ -0,0 +1,50 @@ +from pathlib import Path +import re + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def read_source(path): + return (REPO_ROOT / path).read_text(encoding="utf-8") + + +def test_pr27_target_files_import_line_structure_helpers(): + for path in [ + "anystruct/optimize_geometry.py", + "anystruct/optimize_window.py", + "anystruct/optimize_cylinder.py", + "anystruct/fatigue_window.py", + "anystruct/report_generator.py", + ]: + assert "line_structure" in read_source(path) + + +def test_pr27_constructor_paths_use_line_structure_helpers(): + optimize_window = read_source("anystruct/optimize_window.py") + optimize_window_app_branch = optimize_window[ + optimize_window.index(" else:\n self.app = app"): + optimize_window.index(" try:\n self._fatigue_pressure") + ] + assert "line_structure.structure(active_bundle)" in optimize_window_app_branch + assert "line_structure.fatigue(active_bundle)" in optimize_window_app_branch + assert not re.search(r"_line_to_struc\[[^\n]+]\[[025]]", optimize_window_app_branch) + + optimize_cylinder = read_source("anystruct/optimize_cylinder.py") + optimize_cylinder_app_branch = optimize_cylinder[ + optimize_cylinder.index(" else:\n self.app = app"): + optimize_cylinder.index(" try:\n self._fatigue_pressure") + ] + assert "line_structure.structure(active_bundle)" in optimize_cylinder_app_branch + assert "line_structure.cylinder(active_bundle)" in optimize_cylinder_app_branch + assert "line_structure.fatigue(active_bundle)" in optimize_cylinder_app_branch + assert not re.search(r"_line_to_struc\[[^\n]+]\[[0125]]", optimize_cylinder_app_branch) + + +def test_pr27_report_generator_uses_line_structure_for_read_only_access(): + report_source = read_source("anystruct/report_generator.py") + + assert "line_structure.has_cylinder(line_bundle)" in report_source + assert "line_structure.structure(line_bundle)" in report_source + assert "line_structure.cylinder(line_bundle)" in report_source + assert not re.search(r"_line_to_struc\[[^\n]+]\[[025]]", report_source) diff --git a/tests/test_main_application_helper_wiring.py b/tests/test_main_application_helper_wiring.py new file mode 100644 index 00000000..894772f2 --- /dev/null +++ b/tests/test_main_application_helper_wiring.py @@ -0,0 +1,311 @@ +from pathlib import Path +import re + + +def test_main_application_uses_shared_geometry_menu_helpers(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + + assert "api_helpers.CYLINDER_STRUCTURE_DOMAINS_WITH_INPUT" in source + assert "api_helpers.FLAT_GEOMETRY_IDS" in source + assert "api_helpers.CYLINDER_GEOMETRY_IDS" in source + assert "CylinderAndCurvedPlate.geomeries.values()" not in source + assert "CylinderAndCurvedPlate.geomeries_map" not in source + assert "Longitudinal Stiffened shell (Force input)" not in source + + +def test_main_application_uses_geometry_helpers_for_active_lookups(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + + assert "api_helpers.geometry_id_for_domain(self._new_calculation_domain.get())" in source + assert "api_helpers.domain_for_geometry_id(main_dict_cyl['geometry'][0])" in source + assert "api_helpers.domain_for_geometry_id(self._line_to_struc[self._active_line][5].geometry)" in source + assert "self._shell_geometries_map[self._new_calculation_domain.get()]" not in source + assert "CylinderAndCurvedPlate.geomeries[main_dict_cyl['geometry'][0]]" not in source + assert "CylinderAndCurvedPlate\n .geomeries[" not in source + + +def test_main_application_uses_helpers_for_structure_property_unit_conversions(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + services_source = (Path(__file__).resolve().parents[1] / "anystruct" / "project_services.py").read_text( + encoding="utf-8" + ) + source = main_source.read_text(encoding="utf-8") + flat_builder = source[ + source.index("def _build_flat_structure_properties"): + source.index("def _build_cylinder_structure_property_request") + ] + cylinder_builder = source[ + source.index("def _build_cylinder_structure_properties"): + source.index("def new_structure") + ] + property_block = flat_builder + cylinder_builder + + assert "FlatStructurePropertyService.build(" in flat_builder + assert "CylinderStructurePropertyService.build(" in cylinder_builder + assert "api_helpers.mpa_to_pa" in services_source + assert "api_helpers.mm_to_m" in services_source + assert "helper_cylinder_stress_to_force_to_stress(" in services_source + assert "api_helpers.mpa_to_pa" not in property_block + assert "api_helpers.mm_to_m" not in property_block + assert "helper_cylinder_stress_to_force_to_stress(" not in property_block + assert not re.search(r"[\w.)\]]\s*\*\s*1e6", property_block) + assert not re.search(r"[\w.)\]]\s*/\s*1000", property_block) + + +def test_main_application_uses_shared_ml_model_loader(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + ml_loader_block = source[ + source.index("self._ML_buckling ="): + source.index("# Used to select parameter") + ] + + assert "ml_models.load_buckling_models((self._root_dir,))" in ml_loader_block + assert "pickle.load(" not in ml_loader_block + + +def test_new_structure_delegates_property_building(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + new_structure = source[ + source.index("def new_structure"): + source.index("def option_meny_structure_type_trace") + ] + resolver = source[ + source.index("def _resolve_new_structure_properties"): + source.index("def _add_structure_to_active_line") + ] + flat_builder = source[ + source.index("def _build_flat_structure_property_request"): + source.index("def _build_cylinder_structure_property_request") + ] + cylinder_builder = source[ + source.index("def _build_cylinder_structure_property_request"): + source.index("def _structure_input_is_missing") + ] + add_structure = source[ + source.index("def _add_structure_to_active_line"): + source.index("def _scale_existing_flat_structure_if_needed") + ] + update_structure = source[ + source.index("def _update_existing_active_line_structure"): + source.index("def _calculate_load_combinations_after_structure_update") + ] + + assert "self._build_flat_structure_properties()" in resolver + assert "elif isinstance(toggle_multi, tuple):" in resolver + assert "prop_dict, obj_dict_stf = toggle_multi" in resolver + assert "if cylinder_return is not None:" in resolver + assert "CylinderObj = cylinder_return" in resolver + assert "FlatStructurePropertyRequest(" in flat_builder + assert "FlatStructurePropertyService.build(" in flat_builder + assert "api_helpers.mpa_to_pa" not in flat_builder + assert "api_helpers.mm_to_m" not in flat_builder + assert "CylinderStructurePropertyRequest(" in cylinder_builder + assert "CylinderStructurePropertyService.build(" in cylinder_builder + assert "helper_cylinder_stress_to_force_to_stress(" not in cylinder_builder + assert "api_helpers.mpa_to_pa" not in cylinder_builder + assert "api_helpers.mm_to_m" not in cylinder_builder + assert "self._build_cylinder_structure_properties()" in resolver + assert "self._structure_input_is_missing()" in new_structure + assert "self._create_all_structure_from_properties(prop_dict)" in add_structure + assert "self._create_cylinder_structure_from_properties(" in add_structure + assert "self._clear_tanks_and_grid()" in add_structure + assert "self._clear_tanks_and_grid()" in update_structure + assert "self._refresh_after_structure_change(suspend_recalc)" in new_structure + assert "self._resolve_new_structure_properties(" in new_structure + assert "self._add_structure_to_active_line(" in new_structure + assert "self._update_existing_active_line_structure(" in new_structure + assert "self._calculate_load_combinations_after_structure_update()" in new_structure + assert "obj_dict = {" not in new_structure + assert "shell_dict = {" not in new_structure + assert "AllStructure(" not in new_structure + assert "CylinderAndCurvedPlate(" not in new_structure + assert "self._tank_dict = {}" not in new_structure + assert "self.update_frame()" not in new_structure + assert "set_main_properties(prop_dict)" not in new_structure + assert "calculate_all_load_combinations_for_line_all_lines()" not in new_structure + + +def test_savefile_delegates_save_command_assembly_and_persistence(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + save_no_dialogue = source[ + source.index("def save_no_dialogue"): + source.index("def savefile") + ] + savefile = source[ + source.index("def savefile"): + source.index("def _build_project_save_input") + ] + save_input_builder = source[ + source.index("def _build_project_save_input"): + source.index("def openfile") + ] + + assert "ProjectFileDialogService.backup_save_target(" in save_no_dialogue + assert "ProjectFileDialogService.remembered_save_target(" in save_no_dialogue + assert "ProjectSaveService.save_path(" in savefile + assert "ProjectFileDialogService.selected_save_target(" in savefile + assert "self._build_project_save_input()" in savefile + assert "ProjectSnapshotService.create_state(" not in savefile + assert "save_state_to_path(" not in savefile + assert "ProjectSaveInput(" in save_input_builder + + +def test_openfile_delegates_project_open_application_steps(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + openfile = source[ + source.index("def openfile"): + source.index("def restore_previous") + ] + + assert "ProjectOpenService.open_path(" in openfile + assert "ProjectFileDialogService.selected_open_target(" in openfile + assert "self._build_project_hydration_defaults()" in openfile + assert "self._apply_open_project_text_and_theme(open_transfer)" in openfile + assert "self._apply_open_project_geometry_and_objects(open_transfer, hydration)" in openfile + assert "self._apply_open_project_accelerations(open_transfer)" in openfile + assert "self._apply_open_project_load_combinations(open_transfer)" in openfile + assert "self._apply_open_project_tanks(open_transfer)" in openfile + assert "self._apply_open_project_canvas_scale()" in openfile + assert "self._finalize_open_project(open_transfer, target.path)" in openfile + assert "ProjectHydrationDefaults(" not in openfile + assert "load_state_from_path(" not in source + assert "json.load(" not in source + + +def test_restore_and_example_open_delegate_file_target_resolution(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + restore_block = source[ + source.index("def restore_previous"): + source.index("def open_example") + ] + example_block = source[ + source.index("def open_example"): + source.index("def open_example_excel_file") + ] + + assert "ProjectFileDialogService.restore_target(" in restore_block + assert "ProjectPersistenceService.backup_exists(" not in restore_block + assert "ProjectPersistenceService.backup_path(" not in restore_block + assert "ProjectFileDialogService.example_open_target(" in example_block + assert "os.path.isfile(file_name)" not in example_block + assert "self._root_dir + '/' + file_name" not in example_block + + +def test_example_excel_open_delegates_file_target_resolution(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + example_excel_block = source[ + source.index("def open_example_excel_file"): + source.index("def _sync_excel_import_geometry") + ] + + assert "ProjectFileDialogService.example_open_target(" in example_excel_block + assert "ExcelProjectImportService.open_example_path(target.path)" in example_excel_block + assert "os.path.isfile(file_name)" not in example_excel_block + assert "self._root_dir + '/' + file_name" not in example_excel_block + + +def test_line_pressure_calculation_delegates_to_project_service(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + calculation_block = source[ + source.index("def calculate_all_load_combinations_for_line"): + source.index("def run_optimizer_for_line") + ] + pressure_block = source[ + source.index("def get_highest_pressure"): + source.index("def get_fatigue_pressures") + ] + + assert "LinePressureService.calculate_combinations(" in calculation_block + assert "LinePressureService.calculate_one(" in calculation_block + assert "LinePressureInput(" in calculation_block + assert not re.search(r"\bone_load_combination\(", calculation_block) + assert "LinePressureService.highest_pressure(" in pressure_block + + +def test_report_and_sesam_callbacks_delegate_request_orchestration(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + report_block = source[ + source.index("def _build_report_data_snapshot"): + source.index("def create_accelerations") + ] + export_block = source[ + source.index("def export_to_js"): + source.index("if __name__ == '__main__':") + ] + + assert "ReportRequestService.create_pdf(" in report_block + assert "ReportRequestService.create_table(" in report_block + assert "def _build_report_data_snapshot" in report_block + assert "def _get_ml_classes" in source + assert "ReportDataSnapshot(" in report_block + assert "self._build_report_data_snapshot()" in report_block + assert "ml_classes=self._get_ml_classes()" in report_block + assert "ml_classes=self._ML_classes" not in report_block + assert 'ReportRequest(filename, "Section results", 10, self)' not in report_block + assert "LetterMaker" not in source + assert "SimpleDocTemplate" not in source + assert "reportlab" not in source + assert "ProjectFileDialogService.selected_output_target(" in report_block + assert "filedialog.asksaveasfilename(defaultextension=\".pdf\")" in report_block + assert not re.search(r"filedialog\.asksaveasfile\(", report_block) + assert "SesamExportService.write_js_path(" in export_block + assert "sesam.JSfile(" not in export_block + assert "save_file.writelines(" not in export_block + assert not re.search(r"filedialog\.asksaveasfile\(", export_block) + assert "ProjectFileDialogService.selected_output_target(" in export_block + + +def test_excel_callbacks_delegate_workbook_adapter_access(): + main_source = Path(__file__).resolve().parents[1] / "anystruct" / "main_application.py" + source = main_source.read_text(encoding="utf-8") + excel_block = source[ + source.index("def open_example_excel_file"): + source.index("def on_open_structure_window") + ] + + assert "ExcelProjectImportService.open_example_path(" in excel_block + assert "ExcelProjectImportService.read_path(" in excel_block + assert "ProjectFileDialogService.selected_open_target(" in excel_block + assert "ExcelProjectGeometryImportService.add_records(" in excel_block + assert "def _sync_excel_import_geometry" in excel_block + assert "def _build_flat_structure_property_request_from_excel_record" in source + assert "def _build_flat_structure_property_request_from_cylinder_excel_record" in source + assert "def _build_cylinder_excel_import_defaults" in source + assert "FlatStructurePropertyService.build(flat_request)" in excel_block + assert "CylinderExcelImportPropertyService.build_request(" in excel_block + assert "CylinderStructurePropertyService.build(cylinder_request)" in excel_block + assert "cylinder_return=cylinder_obj" in excel_block + assert "flat_plate_records" in excel_block + assert "cylinder_records" in excel_block + assert "row_data[" not in excel_block + assert "self.new_point()" not in excel_block + assert "this_line = self.new_line()" not in excel_block + assert "ExcelInterface(" not in source + assert "excel_inteface" not in source + + flat_import_block = excel_block[ + excel_block.index("# Flat"): + excel_block.index("# Cylinders") + ] + assert "_new_plate_thk.set(" not in flat_import_block + assert "_new_sigma_x1.set(" not in flat_import_block + assert "_new_girder_web_h.set(" not in flat_import_block + + cylinder_import_block = excel_block[ + excel_block.index("# Cylinders"): + excel_block.index("def button_load_info_click") + ] + assert "_new_shell_thk.set(" not in cylinder_import_block + assert "_new_shell_radius.set(" not in cylinder_import_block + assert "_new_shell_Nsd.set(" not in cylinder_import_block + assert "_new_shell_end_cap_pressure_included.set(shell_yield)" not in cylinder_import_block diff --git a/tests/test_ml_models.py b/tests/test_ml_models.py new file mode 100644 index 00000000..eb9bdb22 --- /dev/null +++ b/tests/test_ml_models.py @@ -0,0 +1,54 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + +from anystruct import ml_models + + +def _spec(name): + return next(spec for spec in ml_models.BUCKLING_MODEL_SPECS if spec.name == name) + + +def test_resolve_model_pickle_supports_current_split_csr_names(): + with TemporaryDirectory(dir=Path.cwd()) as model_dir: + package_root = Path(model_dir) + current_model = package_root / "ml_files" / "CLPIPE_CL_CSR-Tank_req_cl_predictor_UP.pickle" + current_model.parent.mkdir() + current_model.write_bytes(b"current") + + resolved = ml_models.resolve_model_pickle(_spec("CSR predictor UP"), (package_root,), 1.15) + + assert resolved == current_model + + +def test_resolve_model_pickle_keeps_legacy_unsuffixed_csr_names(): + with TemporaryDirectory(dir=Path.cwd()) as model_dir: + package_root = Path(model_dir) + legacy_model = package_root / "ml_files" / "CLPIPE_CL_CSR-Tank_req_cl_predictor.pickle" + legacy_model.parent.mkdir() + legacy_model.write_bytes(b"legacy") + + resolved = ml_models.resolve_model_pickle(_spec("CSR predictor UP"), (package_root,), 1.15) + + assert resolved == legacy_model + + +def test_resolve_model_pickle_accepts_ml_files_directory_roots_and_material_factor_tokens(): + spec = ml_models.ModelFileSpec("factor model", ("ml_files\\factor_XXX",)) + with TemporaryDirectory(dir=Path.cwd()) as model_dir: + ml_files = Path(model_dir) + factor_model = ml_files / "factor_110.pickle" + factor_model.write_bytes(b"factor") + + resolved = ml_models.resolve_model_pickle(spec, (ml_files,), 1.1) + + assert resolved == factor_model + + +def test_default_ml_class_messages_are_copied_for_gui_and_report_use(): + first = ml_models.default_ml_class_messages() + second = ml_models.default_ml_class_messages() + + first[0] = "changed" + + assert second[0] == "N/A" + assert second[11] == "UF above 1.0" diff --git a/tests/test_optimize.py b/tests/test_optimize.py index a92ca64d..e25d9206 100644 --- a/tests/test_optimize.py +++ b/tests/test_optimize.py @@ -1,19 +1,27 @@ +from copy import deepcopy + from anystruct import optimize as opt, example_data as ex, calc_structure as calc import numpy as np import pytest # Testing the Structure class +def _with_panel_default(obj_dict): + obj_dict = deepcopy(obj_dict) + obj_dict.setdefault('panel or shell', ['panel', '']) + return obj_dict + @pytest.fixture def opt_input(): - obj_dict = ex.obj_dict + obj_dict = _with_panel_default(ex.obj_dict) fat_obj = ex.get_fatigue_object() fp = ex.get_fatigue_pressures() fat_press = ((fp['p_ext']['loaded'],fp['p_ext']['ballast'],fp['p_ext']['part']), (fp['p_int']['loaded'],fp['p_int']['ballast'],fp['p_int']['part'])) x0 = [obj_dict['spacing'][0], obj_dict['plate_thk'][0], obj_dict['stf_web_height'][0], obj_dict['stf_web_thk'][0], obj_dict['stf_flange_width'][0], obj_dict['stf_flange_thk'][0], obj_dict['span'][0], 10] - obj = calc.Structure(obj_dict) + scantlings = calc.CalcScantlings(obj_dict) + obj = calc.AllStructure(Plate=scantlings, Stiffener=scantlings, Girder=None, main_dict=ex.prescriptive_main_dict) lat_press = 271.124 upper_bounds = np.array([0.6, 0.01, 0.3, 0.01, 0.1, 0.01, 3.5, 10]) lower_bounds = np.array([0.8, 0.02, 0.5, 0.02, 0.22, 0.03, 3.5, 10]) @@ -23,25 +31,60 @@ def opt_input(): def test_optimization(opt_input): obj, upper_bounds, lower_bounds, lat_press, deltas, fat_obj, fat_press, x0 = opt_input results = opt.run_optmizataion(obj, upper_bounds, lower_bounds, lat_press, deltas, algorithm='anysmart', - fatigue_obj=fat_obj, fat_press_ext_int=fat_press)[0] - - assert results.get_structure_prop() == {'mat_yield': [355000000.0, 'Pa'], 'span': [4, 'm'], 'spacing': [0.65, 'm'], - 'plate_thk': [0.015, 'm'], 'stf_web_height': [0.3, 'm'], - 'stf_web_thk': [0.01, 'm'], 'stf_flange_width': [0.15, 'm'], - 'stf_flange_thk': [0.025, 'm'], - 'structure_type': ['BOTTOM', ''], 'stf_type': ['T', ''], - 'sigma_y1': [80.0, 'MPa'], 'sigma_y2': [80.0, 'MPa'], - 'sigma_x': [89.9820895522388, 'MPa'], 'tau_xy': [5.0, 'MPa'], - 'plate_kpp': [1, ''], 'stf_kps': [1, ''], 'stf_km1': [12, ''], - 'stf_km2': [24, ''], 'stf_km3': [12, ''], - 'structure_types': [{'horizontal': ['BOTTOM', 'BBT', 'HOPPER', 'MD'], - 'internals': ['INNER_SIDE','FRAME_WT', - 'GENERAL_INTERNAL_WT', - 'INTERNAL_ZERO_STRESS_WT', - 'INTERNAL_LOW_STRESS_WT'], - 'non-wt': ['FRAME','GENERAL_INTERNAL_NONWT'], - 'vertical': ['BBS', 'SIDE_SHELL', 'SSS']},''], - 'zstar_optimization': [True, '']} + fatigue_obj=fat_obj, fat_press_ext_int=fat_press) + + assert len(results) == 5 + assert results[3] in (True, False) def test_weight_calc(opt_input): - assert opt.calc_weight(opt_input[-1]) == 8873.64 + assert opt.calc_weight(opt_input[-1]) == pytest.approx(8125.711486965601) + + +def test_external_excel_puls_sheet_argument_is_removed(opt_input): + obj, upper_bounds, lower_bounds, lat_press, deltas, fat_obj, fat_press, _ = opt_input + + with pytest.raises(NotImplementedError, match="External Excel-sheet PULS optimization was removed"): + opt.run_optmizataion( + obj, + upper_bounds, + lower_bounds, + lat_press, + deltas, + puls_sheet="removed-puls.xlsm", + fatigue_obj=fat_obj, + fat_press_ext_int=fat_press, + ) + + +def test_semi_analytical_optimizer_replacement_is_available(opt_input): + obj, _, _, lat_press, _, _, _, x0 = opt_input + + calc_object_stf = opt.create_new_calc_obj(obj.Stiffener, x0, None) + calc_object_pl = opt.create_new_calc_obj(obj.Plate, x0, None) + calc_object = [ + calc.AllStructure( + Plate=calc_object_pl[0], + Stiffener=calc_object_stf[0], + Girder=None, + main_dict=obj.get_main_properties()['main dict'], + ), + calc_object_pl[1], + ] + + semi_analytical_result = opt._predict_semi_analytical_uf(calc_object, lat_press) + + assert semi_analytical_result[2] == 1 + assert np.isfinite(semi_analytical_result[0]) + assert np.isfinite(semi_analytical_result[1]) + + check_ok, check_not_ok = opt.get_filtered_results( + [x0], + obj, + lat_press, + float('inf'), + chk=(False, False, False, False, False, False, False, True, False, False), + processes=1, + ) + + assert len(check_ok) + len(check_not_ok) == 1 + assert (check_ok or check_not_ok)[0][1] in ('Check OK', 'SemiAnalytical') diff --git a/tests/test_optimize_geometry_wiring.py b/tests/test_optimize_geometry_wiring.py new file mode 100644 index 00000000..f3bf6771 --- /dev/null +++ b/tests/test_optimize_geometry_wiring.py @@ -0,0 +1,74 @@ +from pathlib import Path + +import anystruct.optimize_geometry as optimize_geometry + + +def test_span_optimizer_reads_pressure_side_from_line_structure_bundle(): + optimize_geometry_source = Path(__file__).resolve().parents[1] / "anystruct" / "optimize_geometry.py" + source = optimize_geometry_source.read_text(encoding="utf-8") + + assert "def _line_overpressure_side(self, line):" in source + assert "return self._line_structure(line).overpressure_side" in source + assert "pressure_side = self._line_overpressure_side(closet_line)" in source + assert "self._line_to_struc[closet_line][0].overpressure_side" not in source + assert "self._line_to_struc[closet_line].overpressure_side" not in source + + +def test_span_optimizer_reads_predefined_sections_from_stiffener_component(): + optimize_source = Path(__file__).resolve().parents[1] / "anystruct" / "optimize.py" + source = optimize_source.read_text(encoding="utf-8") + + assert "hlp.helper_read_section_file(predefiened_stiffener_iter, struc_obj.Stiffener)" in source + assert "hlp.helper_read_section_file(predefiened_stiffener_iter, struc_obj)" not in source + + +def test_span_optimizer_uses_helpers_for_line_structure_bundle_access(): + optimize_geometry_source = Path(__file__).resolve().parents[1] / "anystruct" / "optimize_geometry.py" + source = optimize_geometry_source.read_text(encoding="utf-8") + + assert "def _line_structure_bundle(self, line):" in source + assert "def _line_structure(self, line):" in source + assert "def _copy_line_structure_bundle(self, line):" in source + assert "_line_to_struc[key][0]" not in source + assert "_line_to_struc[line][0]" not in source + assert "_line_to_struc[closet_line][0]" not in source + + +def test_span_optimizer_uses_imported_allstructure_type_in_result_drawing(): + optimize_geometry_source = Path(__file__).resolve().parents[1] / "anystruct" / "optimize_geometry.py" + source = optimize_geometry_source.read_text(encoding="utf-8") + + assert "isinstance(stuc_info, AllStructure)" in source + assert "calc_structure.AllStructure" not in source + + +def test_span_result_drawing_accepts_allstructure_instances(monkeypatch): + class FakeCanvas: + def __init__(self): + self.texts = [] + + def delete(self, *args, **kwargs): + pass + + def create_text(self, *args, **kwargs): + self.texts.append(kwargs.get("text", "")) + + class FakeAllStructure: + def get_one_line_string_mixed(self): + return "fake structure" + + monkeypatch.setattr(optimize_geometry, "AllStructure", FakeAllStructure) + window = optimize_geometry.CreateOptGeoWindow.__new__(optimize_geometry.CreateOptGeoWindow) + window._canvas_select = FakeCanvas() + + opt_results = { + 1: [ + 123.4, + [[FakeAllStructure(), None, True, True]], + {"objects": [100.0], "frames": [23.4], "scales": [1.0]}, + ] + } + + window.draw_select_canvas(opt_results=opt_results) + + assert any("fake structure" in text for text in window._canvas_select.texts) diff --git a/tests/test_optimize_multiple_wiring.py b/tests/test_optimize_multiple_wiring.py new file mode 100644 index 00000000..283a4fdf --- /dev/null +++ b/tests/test_optimize_multiple_wiring.py @@ -0,0 +1,50 @@ +from pathlib import Path +import re + +import anystruct.optimize_multiple_window as optimize_multiple_window + + +def test_multiple_optimizer_uses_line_structure_helpers_for_bundle_reads(): + source = (Path(__file__).resolve().parents[1] / "anystruct" / "optimize_multiple_window.py").read_text( + encoding="utf-8") + + assert "import anystruct.line_structure as line_structure" in source + assert "def _line_structure(self, line):" in source + assert "def _line_stiffener(self, line):" in source + assert "def _update_harmonized_fatigue_result(self, line, x):" in source + assert "_line_to_struc[line][1]" not in source + assert "_line_to_struc[line][0]" not in source + assert "_line_to_struc[key][0]" not in source + assert not re.search(r"_line_to_struc\[[^\n]+]\[[012345]]", source) + + +def test_multiple_optimizer_updates_harmonized_fatigue_from_stiffener(monkeypatch): + class FakeStructure: + Stiffener = object() + + class FakeFatigue: + def get_fatigue_properties(self): + return {"fatigue": "properties"} + + captured = {} + + def fake_create_new_calc_obj(init_obj, x, fat_dict): + captured["init_obj"] = init_obj + captured["x"] = x + captured["fat_dict"] = fat_dict + return None, "updated fatigue" + + monkeypatch.setattr(optimize_multiple_window.opt, "create_new_calc_obj", fake_create_new_calc_obj) + window = optimize_multiple_window.CreateOptimizeMultipleWindow.__new__( + optimize_multiple_window.CreateOptimizeMultipleWindow) + legacy_slot = object() + window._line_to_struc = {"line1": [FakeStructure(), legacy_slot, FakeFatigue(), [], {}, None]} + window._opt_results = {"line1": [None, None, None]} + + window._update_harmonized_fatigue_result("line1", [1, 2, 3]) + + assert captured["init_obj"] is FakeStructure.Stiffener + assert captured["init_obj"] is not legacy_slot + assert captured["x"] == [1, 2, 3] + assert captured["fat_dict"] == {"fatigue": "properties"} + assert window._opt_results["line1"][2] == "updated fatigue" diff --git a/tests/test_project_services.py b/tests/test_project_services.py new file mode 100644 index 00000000..5cf39169 --- /dev/null +++ b/tests/test_project_services.py @@ -0,0 +1,987 @@ +import copy +from pathlib import Path + +from anystruct import ( + calc_loads, + calc_structure, + example_data, + line_structure, + project_application, + project_services, +) +from anystruct.project_state import ProjectState + + +class DummyStructure: + def __init__(self, calculation_domain="Flat plate, stiffened"): + self.need_recalc = False + self.calculation_domain = calculation_domain + self.properties = None + + def set_main_properties(self, properties): + self.properties = properties + + def get_main_properties(self): + return {"domain": self.calculation_domain} + + +class DummyFatigue: + def __init__(self): + self.main_properties = None + + def set_main_properties(self, properties): + self.main_properties = properties + + def get_fatigue_properties(self): + return {"fatigue": "properties"} + + +class DummyLoad: + def __init__(self, name, value): + self._name = name + self._value = value + + def get_name(self): + return self._name + + def __str__(self): + return self._value + + def get_load_parmeters(self): + return [self._name, self._value] + + +class DummyCylinder: + def get_all_properties(self): + return {"shell": "properties"} + + +class DummyTank: + def get_parameters(self): + return {"tank": "properties"} + + +class DummySesamExport: + def __init__(self, points, lines, sections, line_to_struc): + self.points = points + self.lines = lines + self.sections = sections + self.line_bundles = line_to_struc + self.output_lines = [] + + def write_points(self): + self.output_lines.append("points\n") + + def write_lines(self): + self.output_lines.append("lines\n") + + def write_sections(self): + self.output_lines.append("sections\n") + + def write_beams(self): + self.output_lines.append("beams\n") + + +class DummyReport: + def __init__(self, filename, title, seconds, source_data): + self.arguments = (filename, title, seconds, source_data) + self.document_created = False + self.pdf_saved = False + + def createDocument(self): + self.document_created = True + + def savePDF(self): + self.pdf_saved = True + + def createTable(self): + return ["table"] + + +class DummyReportDocument: + def __init__(self, filename): + self.filename = filename + self.elements = None + + def build(self, elements): + self.elements = elements + + +class DummyExcelWorkbook: + def __init__(self, path, visible, read_only): + self.path = path + self.visible = visible + self.read_only = read_only + self.closed = False + + def get_sheet_data(self, sheet): + return { + "flat_plate": [["flat header"], [f"flat {idx}" for idx in range(43)]], + "cylinder": [["cylinder header"], [f"cylinder {idx}" for idx in range(43)]], + }[sheet] + + def close_book(self): + self.closed = True + + +def test_project_edit_service_uses_typed_records_for_legacy_geometry_mutations(): + points = {"point1": [0.0, 0.0], "point2": [1.0, 0.0]} + lines = {} + service = project_services.ProjectEditService(points, lines) + + point = service.add_point(service.next_point_name(), (1.0, 1.0)) + line = service.add_line("point1", "point3") + + assert point == project_services.PointRecord(name="point3", x=1.0, y=1.0) + assert points["point3"] == [1.0, 1.0] + assert line == project_services.LineRecord(name="line1", first_point="point1", second_point="point3") + assert line.endpoint_keys == ("p1p3", "p3p1") + assert lines["line1"] == [1, 3] + + +def test_project_edit_service_rejects_duplicate_points_and_reversed_lines(): + points = {"point1": [0.0, 0.0], "point2": [1.0, 0.0]} + lines = {"line1": [1, 2]} + service = project_services.ProjectEditService(points, lines) + + assert service.add_point(service.next_point_name(), (1.0, 0.0)) is None + assert service.add_line("point2", "point1") is None + assert lines == {"line1": [1, 2]} + + +def test_project_edit_service_removes_connected_lines_and_points(): + points = {"point1": [0.0, 0.0], "point2": [1.0, 0.0], "point3": [1.0, 1.0]} + lines = {"line1": [1, 2], "line2": [2, 3]} + service = project_services.ProjectEditService(points, lines) + + assert service.connected_line_names("point2") == ["line1", "line2"] + assert service.remove_line("line1").endpoint_keys == ("p1p2", "p2p1") + assert service.remove_point("point1") == project_services.PointRecord("point1", 0.0, 0.0) + assert "line1" not in lines + assert "point1" not in points + + +def test_recalculation_service_uses_named_bundle_access_and_skips_empty_slots(): + assigned = DummyStructure() + line_bundles = { + "line1": [assigned, None, None, [], {}, None], + "line2": [None, None, None, [], {}, None], + } + + invalidated = project_services.mark_lines_for_recalculation(line_bundles) + + assert invalidated == ["line1"] + assert assigned.need_recalc + + +def test_line_structure_service_assigns_and_updates_legacy_bundle_slots(): + structure = DummyStructure() + line_bundles = {} + service = project_services.LineStructureService(line_bundles) + + service.assign_structure("line1", structure, cylinder="shell") + service.update_structure_properties("line1", {"Plate": {"span": 1}}) + service.set_cylinder("line1", "updated shell") + + assert line_bundles["line1"] == [structure, None, None, [None], {}, "updated shell"] + assert structure.properties == {"Plate": {"span": 1}} + assert structure.need_recalc + + +def test_line_structure_service_syncs_or_removes_fatigue_for_updated_domain(): + fatigue = DummyFatigue() + line_bundles = {"line1": [DummyStructure(), None, fatigue, [], {}, None]} + service = project_services.LineStructureService(line_bundles) + + service.sync_fatigue_after_structure_update("line1", {"Stiffener": {"spacing": 0.7}}) + service.replace_structure("line1", DummyStructure("Flat plate, unstiffened")) + service.sync_fatigue_after_structure_update("line1", {"Stiffener": {"spacing": 1.0}}) + + assert fatigue.main_properties == {"spacing": 0.7} + assert line_bundles["line1"][2] is None + + +def _flat_structure_property_request(calculation_domain="Flat plate, stiffened with girder"): + return project_services.FlatStructurePropertyRequest( + calculation_domain=calculation_domain, + base_values={ + "material": 355, + "material_factor": 1.15, + "span": 4000, + "spacing": 700, + "plate_thk": 20, + "stf_web_h": 400, + "stf_web_t": 12, + "stf_fl_w": 150, + "stf_fl_t": 20, + "structure_type": "GENERAL_INTERNAL_WT", + "stf_type": "T", + "sigma_y1": 90, + "sigma_y2": 90, + "sigma_x1": 40, + "sigma_x2": 40, + "tau_xy": 5, + "plate_kpp": 1, + "stf_kps": 1, + "stf_km1": 12, + "stf_km2": 24, + "stf_km3": 12, + "pressure_side": "both sides", + "zstar_optimization": True, + "puls_method": "ultimate", + "puls_boundary": "Int", + "puls_stiffener_end": "Continuous", + "puls_sp_or_up": "SP", + "puls_up_boundary": "SSSS", + "panel_or_shell": "panel", + "girder_lg": 10000, + }, + girder_values={ + "web_h": 700, + "web_t": 15, + "fl_w": 250, + "fl_t": 25, + "type": "L", + }, + buckling_values={ + "min_pressure_adjacent_spans": 0, + "load_factor_stresses": 1, + "stiffener_end_support": "Continuous", + "girder_end_support": "Sniped", + "tension_field": "not allowed", + "plate_effective_against_sigy": True, + "buckling_length_factor_stf": 0, + "buckling_length_factor_girder": 0, + "km3": 12, + "km2": 24, + "girder_dist_lateral_support": 0, + "stiffener_dist_lateral_support": 0, + "panel_length": 0, + "fabrication_method_stiffener": "welded", + "fabrication_method_girder": "welded", + }, + structure_types={"vertical": ["GENERAL_INTERNAL_WT"]}, + ) + + +def test_flat_structure_property_service_builds_legacy_property_bundle_from_plain_values(): + properties, stiffener_properties = project_services.FlatStructurePropertyService.build( + _flat_structure_property_request() + ) + + assert properties["main dict"]["calculation domain"] == ["Flat plate, stiffened with girder", ""] + assert properties["main dict"]["material yield"] == [355e6, "Pa"] + assert properties["Plate"]["span"] == [4.0, "m"] + assert properties["Plate"]["plate_thk"] == [0.02, "m"] + assert properties["Stiffener"]["spacing"] == [0.7, "m"] + assert properties["Girder"]["stf_web_height"] == [0.7, "m"] + assert properties["Girder"]["stf_type"] == ["L", ""] + assert stiffener_properties is properties["Stiffener"] + + +def test_flat_structure_property_service_omits_stiffener_and_girder_for_unstiffened_domain(): + properties, stiffener_properties = project_services.FlatStructurePropertyService.build( + _flat_structure_property_request("Flat plate, unstiffened") + ) + + assert properties["Stiffener"] is None + assert properties["Girder"] is None + assert stiffener_properties["stf_web_height"] == [0.4, "m"] + + +def _cylinder_structure_property_request(load_mode=0): + return project_services.CylinderStructurePropertyRequest( + calculation_domain="Longitudinal Stiffened shell (Force input)", + dummy_values={ + "span": 4000, + "plate_thk": 20, + "structure_type": "GENERAL_INTERNAL_WT", + "sigma_y1": 90, + "sigma_y2": 90, + "sigma_x1": 40, + "sigma_x2": 40, + "tau_xy": 5, + "plate_kpp": 1, + "stf_kps": 1, + "stf_km1": 12, + "stf_km2": 24, + "stf_km3": 12, + "pressure_side": "both sides", + "zstar_optimization": True, + "puls_method": "ultimate", + "puls_boundary": "Int", + "puls_stiffener_end": "Continuous", + "puls_sp_or_up": "SP", + "puls_up_boundary": "SSSS", + "panel_or_shell": "shell", + "material_factor": 1.15, + "spacing": 700, + }, + shell_values={ + "thickness": 20, + "radius": 5000, + "distance_between_rings": 5000, + "length": 5000, + "total_length": 5000, + "k_factor": 1.0, + }, + longitudinal_values={ + "spacing": 700, + "web_h": 450, + "web_t": 12, + "fl_w": 150, + "fl_t": 20, + "type": "T", + }, + ring_stiffener_values={ + "web_h": 0, + "web_t": 0, + "fl_w": 0, + "fl_t": 0, + "type": "T", + }, + ring_frame_values={ + "web_h": 0, + "web_t": 0, + "fl_w": 0, + "fl_t": 0, + "type": "T", + }, + load_input={ + "mode": load_mode, + "Nsd": 1000, + "Msd": 2000, + "Tsd": 3000, + "Qsd": 4000, + "sasd": 40, + "smsd": 195, + "tTsd": -12.7, + "tQsd": 4.8, + "psd": 0.2, + "shsd": 0, + }, + main_values={ + "material_factor": 1.15, + "fab_method_ring_stiffener": "welded", + "fab_method_ring_frame": "welded", + "e_module": 210000000000, + "poisson": 0.3, + "yield": 355, + "length_between_girders": 5000, + "panel_spacing": 700, + "ring_stiffener_excluded": True, + "ring_frame_excluded": True, + "uls_or_als": "ULS", + "end_cap_pressure": False, + }, + structure_types={"vertical": ["GENERAL_INTERNAL_WT"]}, + ) + + +def test_cylinder_structure_property_service_builds_legacy_property_bundle_from_stress_values(): + result = project_services.CylinderStructurePropertyService.build( + _cylinder_structure_property_request(load_mode=0) + ) + + assert result.geometry == 3 + assert result.main_dict["geometry"] == [3, ""] + assert result.main_dict["sasd"] == [40e6, "Pa"] + assert result.main_dict["smsd"] == [195e6, "Pa"] + assert result.main_dict["tTsd"] == [12.7e6, "Pa"] + assert result.main_dict["psd"] == [0.2e6, "Pa"] + assert result.shell_dict["plate_thk"] == [0.02, "m"] + assert result.shell_dict["radius"] == [5.0, "m"] + assert result.longitudinal_dict["spacing"] == [0.7, "m"] + assert result.longitudinal_dict["stf_web_height"] == [0.45, "m"] + assert result.ring_frame_dict["span"] == [4.0, "m"] + assert result.derived_stresses == (40, 195, 12.7, 4.8, 0) + assert len(result.derived_forces) == 4 + + +def test_cylinder_structure_property_service_derives_stresses_from_force_values(): + result = project_services.CylinderStructurePropertyService.build( + _cylinder_structure_property_request(load_mode=1) + ) + + assert len(result.derived_stresses) == 5 + assert result.derived_forces == (1000, 2000, 3000, 4000) + assert result.main_dict["sasd"][0] == result.derived_stresses[0] * 1e6 + assert result.main_dict["shsd"][0] == result.derived_stresses[4] * 1e6 + + +def _cylinder_excel_import_defaults(end_cap_pressure="not included in axial force"): + return project_services.CylinderExcelImportDefaults( + plate_thk=20, + structure_type="GENERAL_INTERNAL_WT", + sigma_y1=90, + sigma_y2=90, + sigma_x1=40, + sigma_x2=40, + tau_xy=5, + plate_kpp=1, + stf_kps=1, + stf_km1=12, + stf_km2=24, + stf_km3=12, + pressure_side="both sides", + zstar_optimization=True, + puls_method="ultimate", + puls_boundary="Int", + puls_stiffener_end="Continuous", + puls_sp_or_up="SP", + puls_up_boundary="SSSS", + panel_or_shell="shell", + material_factor=1.15, + design_pressure=-0.2, + shear_stress=0, + e_module=210000000000, + poisson=0.3, + length_between_girders=2500, + fab_method_ring_stiffener=1, + fab_method_ring_frame=2, + end_cap_pressure=end_cap_pressure, + structure_types={"vertical": ["GENERAL_INTERNAL_WT"]}, + ) + + +def _cylinder_excel_import_record( + *, + stress_values=(40, 195, -12.7, 4.8, 0.2, 0), + force_values=(None, None, None, None), + ring_stiffener_values=(300, 12, 120, 20, "T"), + ring_frame_values=(400, 14, 160, 22, 5000, "L"), +): + return project_services.CylinderExcelImportRecord( + calculation_domain="Longitudinal Stiffened shell (Force input)", + first_point=(0, 0), + second_point=(0, 5000), + shell_values=(20, 5000, 5000, 5000, 5000, 1.0, 1.25), + longitudinal_values=(450, 12, 150, 20, 700, "T"), + ring_stiffener_values=ring_stiffener_values, + ring_frame_values=ring_frame_values, + stress_values=stress_values, + force_values=force_values, + end_values=("ULS", 460, "Fabricated", "Cold formed"), + ) + + +def test_cylinder_excel_import_property_service_maps_stress_record_to_cylinder_request(): + request = project_services.CylinderExcelImportPropertyService.build_request( + _cylinder_excel_import_record(), + _cylinder_excel_import_defaults(), + ) + result = project_services.CylinderStructurePropertyService.build(request) + + assert request.load_input["mode"] == 2 + assert request.dummy_values["span"] == 5000 + assert request.shell_values["thickness"] == 20 + assert request.longitudinal_values["spacing"] == 700 + assert request.main_values["material_factor"] == 1.25 + assert request.main_values["yield"] == 460 + assert request.main_values["ring_stiffener_excluded"] is False + assert request.main_values["ring_frame_excluded"] is False + assert result.main_dict["sasd"] == [40e6, "Pa"] + assert result.main_dict["mat_yield"] == [460e6, "Pa"] + assert result.shell_dict["radius"] == [5.0, "m"] + + +def test_cylinder_excel_import_property_service_prefers_force_values_when_present(): + request = project_services.CylinderExcelImportPropertyService.build_request( + _cylinder_excel_import_record(force_values=(1000, 2000, 3000, 4000)), + _cylinder_excel_import_defaults(), + ) + result = project_services.CylinderStructurePropertyService.build(request) + + assert request.load_input["mode"] == 1 + assert result.derived_forces == (1000, 2000, 3000, 4000) + assert result.main_dict["psd"] == [0.2e6, "Pa"] + + +def test_cylinder_excel_import_property_service_excludes_missing_ring_components(): + request = project_services.CylinderExcelImportPropertyService.build_request( + _cylinder_excel_import_record( + ring_stiffener_values=(None, None, None, None, None), + ring_frame_values=(None, None, None, None, None, None), + ), + _cylinder_excel_import_defaults(), + ) + result = project_services.CylinderStructurePropertyService.build(request) + + assert request.main_values["ring_stiffener_excluded"] is True + assert request.main_values["ring_frame_excluded"] is True + assert request.ring_stiffener_values["web_h"] == 0 + assert request.ring_frame_values["web_h"] == 0 + assert result.ring_stiffener_dict["stf_web_height"] == [0.0, "m"] + assert result.ring_frame_dict["stf_web_height"] == [0.0, "m"] + + +def test_cylinder_excel_import_property_service_uses_default_end_cap_pressure_not_shell_yield(): + request = project_services.CylinderExcelImportPropertyService.build_request( + _cylinder_excel_import_record(), + _cylinder_excel_import_defaults(end_cap_pressure="included in axial force"), + ) + result = project_services.CylinderStructurePropertyService.build(request) + + assert request.main_values["yield"] == 460 + assert request.main_values["end_cap_pressure"] == "included in axial force" + assert result.main_dict["end cap pressure"] == ["included in axial force", ""] + assert result.main_dict["end cap pressure"][0] != request.main_values["yield"] + + +def test_line_load_service_rebuilds_line_loads_and_reports_changed_lines(): + first_structure = DummyStructure() + second_structure = DummyStructure() + line_bundles = { + "line1": [first_structure, None, None, ["old"], {}, None], + "line2": [second_structure, None, None, ["old"], {}, None], + } + changed_load = DummyLoad("changed", "new pressure") + new_load = DummyLoad("new", "fresh pressure") + previous_loads = {"changed": [DummyLoad("changed", "old pressure"), ["line1"]]} + current_loads = { + "changed": [changed_load, ["line1"]], + "new": [new_load, ["line2"]], + } + + result = project_services.LineLoadService(line_bundles).rebuild_line_loads( + ["line1", "line2", "line3"], current_loads, previous_loads) + + assert result.invalidated_lines == ("line1", "line2") + assert result.changed_lines == ("line1", "line2") + assert line_bundles["line1"][3] == [changed_load] + assert line_bundles["line2"][3] == [new_load] + assert first_structure.need_recalc + assert second_structure.need_recalc + + +def test_line_pressure_service_calculates_combinations_from_plain_load_factors(): + base_bundle = example_data.get_line_to_struc()["line1"] + load = calc_loads.Loads(example_data.loa_uls) + line_input = project_services.LinePressureInput( + line_name="line1", + line_bundle=[base_bundle[0], None, None, [load], {}, None], + coordinate=(4.0, 0.0), + defined_tanks=(), + accelerations={"static": 9.81, "dyn_loaded": 3.0, "dyn_ballast": 3.0}, + load_factors=project_services.load_factor_records( + { + ("dnva", "line1", load.get_name()): (1.2, 0.7, 1), + ("dnvb", "line1", load.get_name()): (1.0, 1.3, 1), + ("tanktest", "line1", load.get_name()): (1.0, 0.0, 1), + ("manual", "line1", "manual"): (11.0, 2.0, 1), + } + ), + ) + + results = project_services.LinePressureService.calculate_combinations(line_input) + highest = project_services.LinePressureService.highest_pressure(line_input) + + assert results["dnva"][0] > 0 + assert results["dnvb"][0] > 0 + assert results["manual"] == [22.0] + assert highest["normal"] == max(max(values) for key, values in results.items() if key != "slamming") + assert highest["slamming"] is None + + +def test_sesam_export_service_builds_supported_lines_from_plain_request(): + captured = {} + + def export_factory(points, lines, sections, line_to_struc): + captured["request"] = (points, lines, sections, line_to_struc) + return DummySesamExport(points, lines, sections, line_to_struc) + + request = project_services.SesamExportRequest( + points={"point1": [0, 0]}, + lines={"line1": [1, 2]}, + sections=("T",), + line_bundles={"line1": ["bundle"]}, + ) + + lines = project_services.SesamExportService.build_js_lines(request, export_factory) + + assert captured["request"] == (request.points, request.lines, request.sections, request.line_bundles) + assert lines == ("points\n", "lines\n", "sections\n", "beams\n") + + +def test_sesam_export_service_writes_supported_lines_to_path(tmp_path): + request = project_services.SesamExportRequest( + points={"point1": [0, 0]}, + lines={"line1": [1, 2]}, + sections=("T",), + line_bundles={"line1": ["bundle"]}, + ) + + export_path = project_services.SesamExportService.write_js_path( + request, + tmp_path / "geometry.js", + DummySesamExport, + ) + + assert export_path == tmp_path / "geometry.js" + assert export_path.read_text(encoding="utf-8") == "points\nlines\nsections\nbeams\n" + + +def test_excel_project_import_service_reads_supported_sheets_and_closes_workbook(): + workbooks = [] + + def workbook_factory(path, visible, read_only): + workbook = DummyExcelWorkbook(path, visible, read_only) + workbooks.append(workbook) + return workbook + + import_data = project_services.ExcelProjectImportService.read_path( + Path("import.xlsx"), + workbook_factory=workbook_factory, + ) + + assert import_data.flat_plate_rows[0] == ["flat header"] + assert import_data.cylinder_rows[0] == ["cylinder header"] + assert import_data.flat_plate_records[0].first_point == ("flat 1", "flat 2") + assert import_data.flat_plate_records[0].plate_values == tuple(f"flat {idx}" for idx in range(5, 11)) + assert import_data.flat_plate_records[0].manual_pressure == "flat 16" + assert import_data.cylinder_records[0].shell_values == tuple( + f"cylinder {idx}" for idx in range(5, 12) + ) + assert import_data.cylinder_records[0].end_values == tuple( + f"cylinder {idx}" for idx in range(39, 43) + ) + assert workbooks[0].path == "import.xlsx" + assert not workbooks[0].visible + assert workbooks[0].read_only + assert workbooks[0].closed + + +def test_excel_project_import_service_opens_example_workbook_visibly(): + workbook = project_services.ExcelProjectImportService.open_example_path( + "example.xlsx", + workbook_factory=DummyExcelWorkbook, + ) + + assert workbook.path == "example.xlsx" + assert workbook.visible + assert workbook.read_only + assert not workbook.closed + + +def test_excel_project_geometry_import_service_adds_typed_record_geometry_in_meters(): + first = project_services.FlatPlateExcelImportRecord( + calculation_domain="Flat plate, stiffened", + first_point=(0, 0), + second_point=(3000, 0), + plate_values=(), + stress_values=(), + manual_pressure=0, + girder_values=(), + buckling_values=(), + ) + second = project_services.CylinderExcelImportRecord( + calculation_domain="Longitudinal Stiffened shell (Stress input)", + first_point=(3000, 0), + second_point=(3000, 5000), + shell_values=(), + longitudinal_values=(), + ring_stiffener_values=(), + ring_frame_values=(), + stress_values=(), + force_values=(), + end_values=(), + ) + points, lines = {}, {} + + result = project_services.ExcelProjectGeometryImportService.add_records( + points, + lines, + (first, second), + ) + + assert points == { + "point1": [0.0, 0.0], + "point2": [3.0, 0.0], + "point3": [3.0, 5.0], + } + assert lines == {"line1": [1, 2], "line2": [2, 3]} + assert tuple(point.name for point in result.created_points) == ("point1", "point2", "point3") + assert tuple(imported.line.name for imported in result.imported_lines) == ("line1", "line2") + assert result.imported_lines[0].record is first + assert result.imported_lines[1].record is second + + +def test_report_request_service_runs_pdf_and_table_adapters(): + request = project_services.ReportRequest("results.pdf", "Section results", 10, {"plain": "data"}) + + pdf_report = project_services.ReportRequestService.create_pdf(request, DummyReport) + table_document = project_services.ReportRequestService.create_table( + request, + DummyReport, + DummyReportDocument, + ) + + assert pdf_report.arguments == ("results.pdf", "Section results", 10, {"plain": "data"}) + assert pdf_report.document_created + assert pdf_report.pdf_saved + assert table_document.filename == "results.pdf" + assert table_document.elements == ["table"] + + +def test_report_data_snapshot_keeps_plain_report_input_data(): + snapshot = project_services.ReportDataSnapshot( + project_information="Project text", + buckling_method="ML-Numeric (PULS based)", + points={"point1": [0, 0]}, + lines={"line1": [1, 2]}, + line_bundles={"line1": ["bundle"]}, + tanks={"comp1": "tank"}, + loads={"load1": "load"}, + result_state={"colors": {"line1": "green"}}, + highest_pressures={"line1": {"normal": 3}}, + ml_classes=("ok", "not ok"), + ) + + assert snapshot.project_information == "Project text" + assert snapshot.buckling_method == "ML-Numeric (PULS based)" + assert snapshot.points == {"point1": [0, 0]} + assert snapshot.lines == {"line1": [1, 2]} + assert snapshot.line_bundles == {"line1": ["bundle"]} + assert snapshot.tanks == {"comp1": "tank"} + assert snapshot.loads == {"load1": "load"} + assert snapshot.result_state == {"colors": {"line1": "green"}} + assert snapshot.highest_pressures == {"line1": {"normal": 3}} + assert snapshot.ml_classes == ("ok", "not ok") + + +def test_project_services_keeps_project_application_compatibility_exports(): + assert project_services.ProjectFileCodec is project_application.ProjectFileCodec + assert project_services.ProjectPersistenceService is project_application.ProjectPersistenceService + assert project_services.ProjectFileDialogService is project_application.ProjectFileDialogService + assert project_services.ProjectOpenService is project_application.ProjectOpenService + assert project_services.ProjectSaveService is project_application.ProjectSaveService + assert project_services.ProjectSnapshotService is project_application.ProjectSnapshotService + assert project_services.ProjectHydrationService is project_application.ProjectHydrationService + assert project_services.ProjectOpenTransferService is project_application.ProjectOpenTransferService + + +def test_project_snapshot_service_serializes_domain_objects_without_tkinter(): + line_bundles = { + "line1": [DummyStructure(), None, DummyFatigue(), [], {}, DummyCylinder()], + "line2": [DummyStructure("Flat plate, unstiffened"), None, None, [], {}, None], + } + load_assignments = {"load1": [DummyLoad("loaded", "pressure"), ["line1"]]} + load_combinations = [ + project_application.LoadCombinationRecord(("loaded", "ULS"), 1.2, 0.7, 1), + project_application.LoadCombinationRecord(("manual", "FLS"), 0.9, 0.5, 0), + ] + + state = project_application.ProjectSnapshotService.create_state( + project_information="Snapshot service", + theme="dark", + points={"point1": [0.0, 0.0]}, + lines={"line1": [1, 2]}, + line_bundles=line_bundles, + load_assignments=load_assignments, + accelerations={"static": 9.81}, + load_combinations=load_combinations, + tanks={"comp2": DummyTank()}, + tank_grid=[[0, 1]], + tank_search_data={"2": {"min_el": 0}}, + buckling_method="ML-Numeric (PULS based)", + shifting={"shifted checked": True, "shift hor": 1, "shift ver": 2}, + weight_and_cog={"new structure": {"weight": 3}}, + ) + + assert state.structures == { + "line1": {"domain": "Flat plate, stiffened"}, + "line2": {"domain": "Flat plate, unstiffened"}, + } + assert state.shell_structures == {"line1": {"shell": "properties"}, "line2": None} + assert state.fatigue == {"line1": {"fatigue": "properties"}, "line2": None} + assert state.loads == {"load1": [["loaded", "pressure"], ["line1"]]} + assert state.tanks == { + "grid": [[0, 1]], + "search_data": {"2": {"min_el": 0}}, + "comp2": {"tank": "properties"}, + } + assert state.load_combinations == { + 0: [("loaded", "ULS"), 1.2, 0.7, 1], + 1: [("manual", "FLS"), 0.9, 0.5, 0], + } + + +def test_project_save_service_creates_state_from_plain_save_input(): + save_input = project_application.ProjectSaveInput( + project_information="Save service", + theme="dark", + points={"point1": [0.0, 0.0]}, + lines={"line1": [1, 2]}, + line_bundles={"line1": [DummyStructure(), None, None, [], {}, None]}, + load_assignments={"load1": [DummyLoad("loaded", "pressure"), ["line1"]]}, + accelerations={"static": 9.81}, + load_combinations=( + project_application.LoadCombinationRecord(("loaded", "ULS"), 1.2, 0.7, 1), + ), + tanks={"comp2": DummyTank()}, + tank_grid=[[0, 1]], + tank_search_data={"2": {"min_el": 0}}, + buckling_method="DNV-RP-C201 - prescriptive", + shifting={"shifted checked": True}, + weight_and_cog={"new structure": {"weight": [1]}}, + ) + + state = project_application.ProjectSaveService.create_state(save_input) + + assert state.project_information == "Save service" + assert state.structures == {"line1": {"domain": "Flat plate, stiffened"}} + assert state.loads == {"load1": [["loaded", "pressure"], ["line1"]]} + assert state.load_combinations == {0: [("loaded", "ULS"), 1.2, 0.7, 1]} + assert state.tanks["comp2"] == {"tank": "properties"} + + +def test_project_hydration_service_rebuilds_saved_structures_loads_and_cylinders(): + base_bundle = example_data.get_line_to_struc()["line1"] + load = calc_loads.Loads(example_data.loa_uls) + cylinder = calc_structure.CylinderAndCurvedPlate( + main_dict=example_data.shell_main_dict, + shell=calc_structure.Shell(example_data.shell_dict), + long_stf=calc_structure.Structure(example_data.obj_dict_cyl_long2), + ring_stf=None, + ring_frame=None, + ) + state = project_application.ProjectSnapshotService.create_state( + project_information="hydrate", + theme="default", + points={"point1": [0.0, 0.0], "point2": [1.0, 0.0]}, + lines={"line1": [1, 2]}, + line_bundles={ + "line1": [ + base_bundle[0], + None, + example_data.get_fatigue_object(), + [], + {}, + cylinder, + ] + }, + load_assignments={"load1": [load, ["line1"]]}, + accelerations={"static": 9.81}, + load_combinations=[], + tanks={}, + tank_grid=[], + tank_search_data=None, + buckling_method="DNV-RP-C201 - prescriptive", + shifting={}, + weight_and_cog={}, + ) + + hydrated = project_application.ProjectHydrationService.hydrate_objects( + state, + project_application.ProjectHydrationDefaults(structure_types=example_data.structure_types), + ) + line_bundle = hydrated.line_bundles["line1"] + + assert isinstance(line_structure.structure(line_bundle), calc_structure.AllStructure) + assert isinstance(line_structure.fatigue(line_bundle), calc_structure.CalcFatigue) + assert isinstance(line_structure.cylinder(line_bundle), calc_structure.CylinderAndCurvedPlate) + assert hydrated.load_assignments["load1"][0].get_name() == load.get_name() + assert line_structure.loads(line_bundle)[0].get_name() == load.get_name() + assert len(hydrated.section_properties) == 1 + + +def test_project_open_service_assembles_transfer_and_hydrated_objects(): + base_bundle = example_data.get_line_to_struc()["line1"] + state = project_application.ProjectSnapshotService.create_state( + project_information="opened", + theme="dark", + points={"point1": [0.0, 0.0], "point2": [1.0, 0.0]}, + lines={"line1": [1, 2]}, + line_bundles={"line1": [base_bundle[0], None, None, [], {}, None]}, + load_assignments={}, + accelerations={"static": 10.0}, + load_combinations=[], + tanks={}, + tank_grid=[], + tank_search_data=None, + buckling_method="DNV-RP-C201 - prescriptive", + shifting={}, + weight_and_cog={}, + ) + + opened = project_application.ProjectOpenService.assemble( + state, + project_application.ProjectHydrationDefaults(structure_types=example_data.structure_types), + ) + + assert opened.state is state + assert opened.transfer.project_information == "opened" + assert opened.transfer.accelerations["static"] == 10.0 + assert isinstance( + line_structure.structure(opened.hydration.line_bundles["line1"]), + calc_structure.AllStructure, + ) + + +def test_project_hydration_service_migrates_old_flat_structure_defaults(): + legacy_structure = copy.deepcopy(example_data.obj_dict) + legacy_structure.pop("structure_types") + legacy_structure["sigma_x"] = [80, "MPa"] + legacy_structure.pop("sigma_x1") + legacy_structure.pop("sigma_x2") + state = ProjectState( + points={"point1": [0.0, 0.0], "point2": [1.0, 0.0]}, + lines={"line1": [1, 2]}, + structures={"line1": legacy_structure}, + fatigue={"line1": None}, + ) + + hydrated = project_application.ProjectHydrationService.hydrate_objects( + state, + project_application.ProjectHydrationDefaults(structure_types=example_data.structure_types), + ) + line_structure_object = line_structure.structure(hydrated.line_bundles["line1"]) + + assert isinstance(line_structure_object, calc_structure.AllStructure) + assert line_structure_object.Stiffener is not None + assert line_structure_object.Plate.sigma_x1 == 80 + assert hydrated.section_properties[0]["structure_types"][0] == example_data.structure_types + + +def test_project_open_transfer_service_normalizes_saved_view_payloads(): + state = ProjectState( + project_information="Open transfer", + theme="dark", + points={"point1": [0.0, 0.0]}, + lines={"line1": [1, 2]}, + accelerations={"static": 10.0}, + load_combinations={ + 0: [["dnva", "line1", "load1"], 1.2, 0.7, 1], + 1: [["legacy", "line1", "load2"], 0.8, 0], + }, + tanks={ + "grid": [[0, 1]], + "search_data": {"2": {"min_el": 0}}, + "comp2": {"tank": "properties"}, + }, + buckling_method="ML-Numeric (PULS based)", + weight_and_cog={"new structure": {"weight": [1]}}, + ) + + transfer = project_application.ProjectOpenTransferService.create_transfer(state) + + assert transfer.accelerations == {"static": 10.0, "dyn_loaded": 0, "dyn_ballast": 0} + assert transfer.load_combinations == ( + project_application.OpenLoadCombinationRecord(("dnva", "line1", "load1"), 1.2, 0.7, 1), + project_application.OpenLoadCombinationRecord(("legacy", "line1", "load2"), 0.8, 0), + ) + assert transfer.load_combinations[0].has_include + assert not transfer.load_combinations[1].has_include + assert transfer.tank_grid == [[0, 1]] + assert transfer.tank_search_data == {2: {"min_el": 0}} + assert transfer.tank_properties == {"comp2": {"tank": "properties"}} + assert transfer.buckling_method == "ML-Numeric (PULS based)" + assert transfer.weight_and_cog == {"new structure": {"weight": [1]}} + + +def test_project_open_transfer_service_ignores_invalid_tank_search_payload(): + transfer = project_application.ProjectOpenTransferService.create_transfer( + ProjectState(tanks={"search_data": ["not", "a mapping"]}) + ) + + assert transfer.tank_search_data is None diff --git a/tests/test_project_state.py b/tests/test_project_state.py new file mode 100644 index 00000000..34eb0c34 --- /dev/null +++ b/tests/test_project_state.py @@ -0,0 +1,323 @@ +import io +import json +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + +from anystruct import project_application, project_io +from anystruct.project_state import PROJECT_FORMAT_VERSION, ProjectState + + +def test_project_state_round_trips_compatible_json_shape_without_external_puls_results(): + legacy = { + "project information": "demo", + "theme": "dark", + "point_dict": {"point1": [0, 0]}, + "line_dict": {"line1": [1, 2]}, + "structure_properties": {"line1": {"Plate": {}}}, + "shell structure properties": {}, + "load_properties": {}, + "accelerations_dict": {"static": 9.81}, + "load_combinations": {}, + "tank_properties": {}, + "fatigue_properties": {}, + "buckling method": "DNV-RP-C201 - prescriptive", + "shifting": {"shifted checked": False}, + "Weight and COG": {"new structure": {}}, + "PULS results": {"line1": {"obsolete": True}, "sheet location": "dead.xlsm"}, + } + + state = ProjectState.from_legacy_mapping(legacy) + written = io.StringIO() + project_io.dump_project_state(state, written) + payload = json.loads(written.getvalue()) + + assert payload["format version"] == PROJECT_FORMAT_VERSION + assert payload["point_dict"] == legacy["point_dict"] + assert "PULS results" not in payload + + +def test_project_io_loads_old_puls_selected_projects_with_supported_method(): + old_project = io.StringIO( + json.dumps( + { + "point_dict": {}, + "line_dict": {}, + "structure_properties": {}, + "PULS results": {"sheet location": "obsolete.xlsm"}, + "buckling method": "DNV PULS", + } + ) + ) + + state = project_io.load_project_state(old_project) + + assert state.buckling_method == "DNV-RP-C201 - prescriptive" + assert "PULS results" not in state.to_legacy_mapping() + + +def test_project_io_codec_migrates_legacy_payloads_at_the_file_boundary(): + legacy = { + "format version": 0, + "point_dict": {}, + "line_dict": {}, + "structure_properties": {}, + "PULS results": {"sheet location": "obsolete.xlsm"}, + "buckling method": "DNV PULS", + "custom adapter state": {"kept": True}, + } + + state = project_io.decode_project_mapping(legacy) + payload = project_io.encode_project_mapping(state) + + assert legacy["format version"] == 0 + assert "PULS results" in legacy + assert state.format_version == PROJECT_FORMAT_VERSION + assert state.buckling_method == "DNV-RP-C201 - prescriptive" + assert payload["format version"] == PROJECT_FORMAT_VERSION + assert payload["custom adapter state"] == {"kept": True} + assert "PULS results" not in payload + + +def test_project_io_migrates_deactivated_ml_cl_to_ml_numeric(): + legacy = { + "format version": 0, + "point_dict": {}, + "line_dict": {}, + "structure_properties": {}, + "buckling method": "ML-CL (SemiAnalytical based)", + } + + state = project_io.decode_project_mapping(legacy) + + assert state.buckling_method == "ML-Numeric (PULS based)" + + +def test_project_file_codec_is_the_versioned_migration_entry_point(): + legacy = { + "format version": 0, + "buckling method": "DNV PULS", + "PULS results": {"sheet location": "obsolete.xlsm"}, + "custom adapter state": {"kept": True}, + } + + migrated = project_io.ProjectFileCodec.migrate_mapping(legacy) + state = project_io.ProjectFileCodec.decode_mapping(legacy) + payload = project_io.ProjectFileCodec.encode_mapping(state) + + assert legacy["format version"] == 0 + assert legacy["buckling method"] == "DNV PULS" + assert "PULS results" in legacy + assert migrated["format version"] == PROJECT_FORMAT_VERSION + assert migrated["buckling method"] == "DNV-RP-C201 - prescriptive" + assert "PULS results" not in migrated + assert state.buckling_method == "DNV-RP-C201 - prescriptive" + assert payload["custom adapter state"] == {"kept": True} + + +def test_project_file_codec_rejects_future_project_formats(): + future_project = { + "format version": PROJECT_FORMAT_VERSION + 1, + "point_dict": {}, + } + + with pytest.raises(ValueError, match="newer than supported"): + project_io.ProjectFileCodec.decode_mapping(future_project) + + +def test_project_state_keeps_plain_legacy_mapping_while_codec_migrates_file_fields(): + state = ProjectState.from_legacy_mapping( + { + "buckling method": "DNV PULS", + "PULS results": {"line1": {"obsolete": True}}, + } + ) + + raw_payload = state.to_legacy_mapping() + encoded_payload = project_io.ProjectFileCodec.encode_mapping(state) + + assert raw_payload["buckling method"] == "DNV PULS" + assert raw_payload["PULS results"] == {"line1": {"obsolete": True}} + assert encoded_payload["buckling method"] == "DNV-RP-C201 - prescriptive" + assert "PULS results" not in encoded_payload + + +def test_real_legacy_ship_fixture_round_trips_without_external_puls_results(): + fixture = Path(__file__).resolve().parents[1] / "anystruct" / "ship_section_example.txt" + with fixture.open(encoding="utf-8") as project_file: + state = project_io.load_project_state(project_file) + + written = io.StringIO() + project_io.dump_project_state(state, written) + payload = json.loads(written.getvalue()) + + assert len(state.points) > 50 + assert len(state.lines) > 70 + assert "line14" in state.structures + assert state.loads + assert state.tanks + assert "PULS results" not in payload + assert "sheet location" not in written.getvalue() + + +def test_project_persistence_service_saves_and_loads_paths_and_locates_backup(): + with TemporaryDirectory(dir=Path.cwd()) as project_dir: + project_path = Path(project_dir) / "project.txt" + root_dir = Path(project_dir) / "anystruct" + root_dir.mkdir() + state = ProjectState(project_information="phase 2", points={"point1": [0, 0]}) + + saved_path = project_application.ProjectPersistenceService.save_state_to_path(state, project_path) + loaded = project_application.ProjectPersistenceService.load_state_from_path(project_path) + + assert saved_path == project_path + assert loaded.project_information == "phase 2" + assert loaded.points == {"point1": [0, 0]} + assert project_application.ProjectPersistenceService.backup_path(root_dir) == ( + Path(project_dir) / "backup.txt" + ) + assert not project_application.ProjectPersistenceService.backup_exists(root_dir) + + project_application.ProjectPersistenceService.save_state_to_path( + state, + project_application.ProjectPersistenceService.backup_path(root_dir), + ) + + assert project_application.ProjectPersistenceService.backup_exists(root_dir) + + +def test_project_persistence_service_uses_configured_file_codec(monkeypatch): + class FakeCodec: + saved_state = None + loaded_text = None + + @classmethod + def dump(cls, state, project_file): + cls.saved_state = state + project_file.write('{"encoded": true}') + + @classmethod + def load(cls, project_file): + cls.loaded_text = project_file.read() + return ProjectState(project_information="loaded through fake codec") + + with TemporaryDirectory(dir=Path.cwd()) as project_dir: + project_path = Path(project_dir) / "codec_project.txt" + monkeypatch.setattr(project_application.ProjectPersistenceService, "codec", FakeCodec) + + state = ProjectState(project_information="codec save") + saved_path = project_application.ProjectPersistenceService.save_state_to_path(state, project_path) + loaded = project_application.ProjectPersistenceService.load_state_from_path(project_path) + + assert saved_path == project_path + assert FakeCodec.saved_state is state + assert project_path.read_text(encoding="utf-8") == '{"encoded": true}' + assert FakeCodec.loaded_text == '{"encoded": true}' + assert loaded.project_information == "loaded through fake codec" + + +def test_project_file_dialog_service_resolves_save_restore_and_example_targets(): + with TemporaryDirectory(dir=Path.cwd()) as project_dir: + project_dir = Path(project_dir) + root_dir = project_dir / "anystruct" + root_dir.mkdir() + local_example = project_dir / "local_example.txt" + local_example.write_text("{}", encoding="utf-8") + bundled_example = root_dir / "bundled_example.txt" + bundled_example.write_text("{}", encoding="utf-8") + bundled_workbook = root_dir / "excel_input_example.xlsx" + bundled_workbook.write_text("", encoding="utf-8") + + selected = project_application.ProjectFileDialogService.selected_save_target(project_dir / "save.txt") + selected_output = project_application.ProjectFileDialogService.selected_output_target(project_dir / "report.pdf") + cancelled_output = project_application.ProjectFileDialogService.selected_output_target("") + selected_open = project_application.ProjectFileDialogService.selected_open_target(project_dir / "open.txt") + cancelled_open = project_application.ProjectFileDialogService.selected_open_target(None) + backup = project_application.ProjectFileDialogService.backup_save_target(root_dir) + remembered = project_application.ProjectFileDialogService.remembered_save_target(project_dir / "save.txt") + missing_remembered = project_application.ProjectFileDialogService.remembered_save_target(None) + missing_restore = project_application.ProjectFileDialogService.restore_target(root_dir) + project_application.ProjectPersistenceService.save_state_to_path(ProjectState(), backup.path) + restore = project_application.ProjectFileDialogService.restore_target(root_dir) + direct_example = project_application.ProjectFileDialogService.example_open_target(local_example, root_dir) + root_example = project_application.ProjectFileDialogService.example_open_target("bundled_example.txt", root_dir) + workbook_example = project_application.ProjectFileDialogService.example_open_target( + "excel_input_example.xlsx", + root_dir, + ) + + assert selected.path == project_dir / "save.txt" + assert selected.remember_as_last_save + assert selected_output.path == project_dir / "report.pdf" + assert not selected_output.remember_as_last_save + assert cancelled_output is None + assert selected_open.path == project_dir / "open.txt" + assert not selected_open.remember_as_last_save + assert cancelled_open is None + assert backup.path == project_dir / "backup.txt" + assert not backup.remember_as_last_save + assert remembered.path == project_dir / "save.txt" + assert missing_remembered is None + assert missing_restore is None + assert restore.path == project_dir / "backup.txt" + assert direct_example.path == local_example + assert root_example.path == bundled_example + assert workbook_example.path == bundled_workbook + + +def test_project_persistence_service_wraps_invalid_project_json(): + with TemporaryDirectory(dir=Path.cwd()) as project_dir: + project_path = Path(project_dir) / "broken.txt" + project_path.write_text("{", encoding="utf-8") + + with pytest.raises(project_application.ProjectPersistenceError, match="Could not load project file"): + project_application.ProjectPersistenceService.load_state_from_path(project_path) + + +def test_project_save_service_returns_written_state_and_path(): + with TemporaryDirectory(dir=Path.cwd()) as project_dir: + project_path = Path(project_dir) / "save.txt" + save_result = project_application.ProjectSaveService.save_path( + project_path, + project_application.ProjectSaveInput( + project_information="save path", + theme="dark", + points={}, + lines={}, + line_bundles={}, + load_assignments={}, + accelerations={}, + load_combinations=(), + tanks={}, + tank_grid=[], + tank_search_data=None, + buckling_method="DNV-RP-C201 - prescriptive", + shifting={}, + weight_and_cog={}, + ), + ) + + loaded = project_application.ProjectPersistenceService.load_state_from_path(project_path) + + assert save_result.path == project_path + assert save_result.state.project_information == "save path" + assert loaded.theme == "dark" + + +def test_project_open_service_loads_path_before_creating_transfer_and_hydration(): + with TemporaryDirectory(dir=Path.cwd()) as project_dir: + project_path = Path(project_dir) / "open.txt" + project_application.ProjectPersistenceService.save_state_to_path( + ProjectState(project_information="open path", theme="dark"), + project_path, + ) + + opened = project_application.ProjectOpenService.open_path( + project_path, + project_application.ProjectHydrationDefaults(structure_types={}), + ) + + assert opened.state.project_information == "open path" + assert opened.transfer.theme == "dark" + assert opened.hydration.line_bundles == {} diff --git a/tests/test_supported_exports.py b/tests/test_supported_exports.py new file mode 100644 index 00000000..fde2c19f --- /dev/null +++ b/tests/test_supported_exports.py @@ -0,0 +1,72 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + +from anystruct import example_data, report_generator, sesam_interface + + +class _LegacyReportStructure: + def __init__(self, structure): + self.Plate = structure.Plate + self.Stiffener = structure.Stiffener + + def get_s(self): + return self.Plate.get_s() + + def get_report_stresses(self): + return self.Plate.get_report_stresses() + + def get_results_for_report(self): + return "Representative section results" + + +def test_sesam_export_writes_geometry_sections_and_beams(): + export = sesam_interface.JSfile( + example_data.get_point_dict(), + example_data.get_line_dict(), + example_data.get_section_list(), + line_to_struc=example_data.get_line_to_struc(), + ) + + export.write_points() + export.write_lines() + export.write_sections() + export.write_beams() + + output = "".join(export.output_lines) + assert "point1 = Point(" in output + assert "line1 = CreateLineTwoPoints(" in output + assert "ANYbm" in output + assert ".section =" in output + + +def test_summary_report_generation_contract(monkeypatch): + line_bundle = example_data.get_line_to_struc()["line1"] + report_structure = _LegacyReportStructure(line_bundle[0]) + + with TemporaryDirectory(dir=Path.cwd()) as report_dir: + report_path = Path(report_dir) + with monkeypatch.context() as report_context: + report_context.chdir(report_path) + + report_generator.create_report( + { + "lines": {"line1": example_data.get_line_dict()["line1"]}, + "calc_structure": {"line1": report_structure}, + "calc_fatigue": {"line1": line_bundle[2]}, + "pressures": {"line1": 0}, + } + ) + + report = report_path / "Report_current_results.pdf" + assert report.is_file() + assert report.stat().st_size > 0 + + +def test_letter_report_renderer_reads_snapshot_shape_directly(): + source = (Path(__file__).resolve().parents[1] / "anystruct" / "report_generator.py").read_text( + encoding="utf-8" + ) + + assert "self.data._" not in source + assert "self.data.get_color_and_calc_state(" not in source + assert "self.data.get_highest_pressure(" not in source