diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..597da8b --- /dev/null +++ b/.flake8 @@ -0,0 +1,19 @@ +[flake8] +# Use less conservative limit for max. line length, we all have wider screens today. +max-line-length = 127 +max-complexity = 15 +# E741 - allow ambiguous variable names such as 'l'. +# W503 - line breaks should occur before the binary operator (this will become best-practise in the furture) +ignore = E741, W503 + +# Exclude some directories +exclude = + ./virtualenv + +# Deactivate too-complex warning(s) +per-file-ignores = + ./loadskernel/trim_conditions.py:C901 + ./loadskernel/solution_sequences.py:C901 + ./loadskernel/plotting_extra.py:C901 + ./doc/jcl_template.py:E261 + \ No newline at end of file diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml new file mode 100644 index 0000000..ac53f48 --- /dev/null +++ b/.github/workflows/coding-style.yml @@ -0,0 +1,59 @@ +# This workflow will install and then lint the code with Flake8 and Pylint. +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Coding style + +on: + push: + branches: ['master', 'devel'] + pull_request: + branches: '*' + +permissions: + contents: read + +jobs: + Flake8: + runs-on: ubuntu-latest + strategy: + matrix: + # Add multiple Python versions here to run tests on new(er) versions. + python-version: ["3.8"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # The GitHub editor is 127 chars wide + flake8 . --count --statistics + + Pylint: + runs-on: ubuntu-latest + strategy: + matrix: + # Add multiple Python versions here to run tests on new(er) versions. + python-version: ["3.8"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + # Install the package itself to make sure that all imports work. + pip install . + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') --fail-under=7.0 \ No newline at end of file diff --git a/.github/workflows/regression-tests.yml b/.github/workflows/regression-tests.yml new file mode 100644 index 0000000..436a280 --- /dev/null +++ b/.github/workflows/regression-tests.yml @@ -0,0 +1,93 @@ +# This workflow will install and then lint the code with Flake8 and Pylint. +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Regression Tests + +on: + push: + branches: ['master', 'devel'] + pull_request: + branches: '*' + +jobs: + pip-installation: + # This stage only tests if the installation is possible. + # The evironment created herein will be discared and re-created in the test stage. + runs-on: ubuntu-latest + strategy: + matrix: + # Add multiple Python versions here to run tests on new(er) versions. + python-version: ["3.8"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Build and install + run: | + python -m pip install --upgrade pip + # Install with -e (in editable mode) to allow the tracking of the test coverage + pip install -e . + # Check result of installation + which loads-kernel + which model-viewer + which loads-compare + + Jupyter: + runs-on: ubuntu-latest + strategy: + matrix: + # Add multiple Python versions here to run tests on new(er) versions. + python-version: ["3.8"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install jupyter-book + pip install . + - name: Assemble the tutorials to a jupyter book and build htlm pages + run: | + jupyter-book build ./doc/tutorials + # Put the html into a 2nd-level sub-folder and use 1st-level subfolder for uploading + mkdir ./doc/html + mv ./doc/tutorials/_build/html ./doc/html/tutorials + - name: Upload Jupyter book as an artifact + uses: actions/upload-artifact@v3 + with: + name: tutorials + path: ./doc/html + if-no-files-found: ignore + - name: Upload Jupyter book for pages + # This is not a normal artifact but one that can be deployed to the GitHub pages in the next step + uses: actions/upload-pages-artifact@v3 + with: + name: github-pages # This name may not be changed according to the documentation + path: ./doc/html + if-no-files-found: ignore + + deploy-pages: + # Add a dependency to the build job + needs: Jupyter + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Setup GitHub Pages + uses: actions/configure-pages@v4 + - name: Deploy to Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a14cdf..d3f5736 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,7 +24,7 @@ stages: # Check location - pwd -build: +pip-installation: # This stage only tests if the installation is possible. # The evironment created herein will be discared and re-created in the test stage. stage: build @@ -36,26 +36,58 @@ build: # Check result of installation - which loads-kernel - which model-viewer - - which loads-compare + - which loads-compare -test: +Jupyter: + stage: build + tags: + - lk + script: + - *virtualenv + # Assemble the tutorials to a jupyter book and build htlm pages + - jupyter-book build ./doc/tutorials + artifacts: + when: always + paths: + - ./doc/tutorials + +Flake8: + stage: test + tags: + - lk + script: + - *virtualenv + - pip install flake8 + # stop the build if there are Python syntax errors or undefined names + - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # Lint with flake8 + - flake8 . --count --statistics + +Pylint: + stage: test + tags: + - lk + script: + - *virtualenv + - pip install pylint + # Install the package itself to make sure that all imports work. + - pip install . + # Analyse the code with pylint + - pylint $(git ls-files '*.py') --fail-under=7.0 + +Pytest: stage: test timeout: 3 hours coverage: '/^TOTAL.+?(\d+\%)$/' tags: - lk - dependencies: - - build script: # Set-up the environement - *virtualenv # Install with -e (in editable mode) to allow the tracking of the test coverage - pip install -e . - - which loads-kernel # Get the examples repository - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.dlr.de/loads-kernel/loads-kernel-examples.git - # Assemble the tutorials to a jupyter book and build htlm pages - - jupyter-book build ./doc/tutorials # Run the actual testing of the code with pytest - pytest -v --basetemp=./test_tmp --cov=loadskernel --cov=modelviewer --cov=loadscompare --junitxml=testresult.xml # Create some reports @@ -68,19 +100,19 @@ test: - coverage.xml - testresult.xml - coverage - - ./doc/tutorials reports: coverage_report: coverage_format: cobertura path: coverage.xml junit: testresult.xml -pages: +deploy-pages: stage: deploy tags: - lk dependencies: - - test + - Jupyter + - Pytest script: - mkdir public # Publish the coverage htlm results diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..e35bd51 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,9 @@ +[MESSAGES CONTROL] +# Disable the message, report, category or checker with the given id(s). +# C0103 - doesn't conform to snake_case naming style +# C0114/5/6 - Missing docstring +# W0105 - pointless-string-statement, disable it if you're using those strings as documentation, instead of comments. +disable=C0103,C0114,C0115,C0116,W0105 + +# Set same max. line length as with Flake8 +max-line-length=127 \ No newline at end of file diff --git a/doc/efcs_template.py b/doc/efcs_template.py index aae3575..ad23fe7 100644 --- a/doc/efcs_template.py +++ b/doc/efcs_template.py @@ -1,23 +1,24 @@ ''' -This is a template for an EFCS. For each aircraft, an EFCS must be written which maps the pilot -commands to control surface deflections Ux2. This is because every aircraft has different control -surfaces (e.g. one or two elevators, multiple ailerons, etc.) +This is a template for an EFCS. For each aircraft, an EFCS must be written which maps the pilot +commands to control surface deflections Ux2. This is because every aircraft has different control +surfaces (e.g. one or two elevators, multiple ailerons, etc.) ''' import numpy as np -class Efcs: + +class Efcs(): + def __init__(self): self.keys = ['dummy'] self.Ux2 = np.array([0.0]) - + def cs_mapping(self, commands): """ Do nothing in particular, this is just a dummy EFCS. - command_xi = commands[0] + command_xi = commands[0] command_eta = commands[1] command_zeta = commands[2] ... """ - - - return self.Ux2 \ No newline at end of file + + return self.Ux2 diff --git a/doc/jcl_template.py b/doc/jcl_template.py index 000be18..9c04210 100755 --- a/doc/jcl_template.py +++ b/doc/jcl_template.py @@ -1,19 +1,20 @@ """ Job Control File documentation -The Job Control (jcl) is a python class which defines the model and and the simulation and is imported at -the beginning of every simulation. Unlike a conventional parameter file, this allows scripting/programming +The Job Control (jcl) is a python class which defines the model and and the simulation and is imported at +the beginning of every simulation. Unlike a conventional parameter file, this allows scripting/programming of the input, e.g. to convert units, generate mutiple load cases, etc. -Note that this documentation of parameters is comprehensive, but a) not all parameters are necessary for -every kind of simulation and b) some parameters are for experts only --> your JCL might be much smaller. +Note that this documentation of parameters is comprehensive, but a) not all parameters are necessary for +every kind of simulation and b) some parameters are for experts only --> your JCL might be much smaller. """ - import numpy as np -from loadskernel.units import * +from loadskernel.units import ft2m + class jcl: + def __init__(self): - self.general = {# Give your aircraft a name - 'aircraft':'DLR F-19-S', + # Give your aircraft a name and set some general parameters + self.general = {'aircraft': 'DLR F-19-S', # Reference span width (from tip to tip) 'b_ref': 15.375, # Reference chord length @@ -22,56 +23,58 @@ def __init__(self): 'A_ref': 77, # Mean aerodynamic center, also used as moments reference point 'MAC_ref': [6.0, 0.0, 0.0], - } + } """ The electronic flight control system (EFCS) provides the "wireing" of the pilot commands - xi, eta and zeta with the control surface deflections. This is aicraft-specific and needs - to be implemented as a python module. + xi, eta and zeta with the control surface deflections. This is aicraft-specific and needs + to be implemented as a python module. """ - self.efcs = {# Name of the corresponding python module - 'version': 'mephisto', + # Electronic flight control system + self.efcs = {'version': 'mephisto', # Name of the corresponding python module # Path where to find the EFCS module - 'path': '/path/to/EFCS', - } - self.geom = {# Read the structural geometry from ModGen and/or Nastran (mona) BDFs - 'method': 'mona', + 'path': '/path/to/EFCS', + } + # Read the structural geometry + self.geom = {'method': 'mona', # ModGen and/or Nastran (mona) BDFs # bdf file(s) with GRIDs and CORDs (CORD1R and CORD2R) - 'filename_grid':['grids.bdf'], + 'filename_grid': ['grids.bdf'], # bdf file(s) with CQUADs and CTRIAs, for visualization only, e.g. outer skin on the aircraft - 'filename_shell':['shells.bdf'], + 'filename_shell': ['shells.bdf'], # bdf file(s) with MONPNT-cards 'filename_monpnt': 'monpnt.bdf', # Alternative way to define monitoring stations: - # bdf file with GRID-cards, one monitoring station is created at each GRID point, 1st GRID point -> 1st monstation - 'filename_mongrid': 'monstations_grids.bdf', - # additional CORDs for monitoring stations - 'filename_moncoord':'monstations_coords.bdf', + # bdf file with GRID-cards, one monitoring station is created at each GRID point + # 1st GRID point -> 1st monstation + 'filename_mongrid': 'monstations_grids.bdf', + # additional CORDs for monitoring stations + 'filename_moncoord': 'monstations_coords.bdf', # bdf file with GRID-cards, 1st file -> 1st monstation 'filename_monstations': ['monstation_MON1.bdf', 'monstation_MON2.bdf'], - # The following matrices are required for some mass methods. However, the stiffness is geometry + # The following matrices are required for some mass methods. However, the stiffness is geometry # and not mass dependent. Overview: # KGG via DMAP Alter und OP4 - required for mass method = 'modalanalysis', 'guyan' or 'B2000' # USET via DMAP Alter und OP4 - required for mass method = 'modalanalysis', 'guyan' # matrix GM via DMAP Alter und OP4 - required for mass method = 'modalanalysis', 'guyan' # bdf file(s) with ASET1-card - required for mass method = 'guyan' # matrix R_trans frum B2000 - required for mass method = 'B2000' - 'filename_KGG':'KGG.dat', - 'filename_uset': 'uset.op2', - 'filename_GM': 'GM.dat', - 'filename_aset': 'aset.bdf', - 'filename_Rtrans': 'Rtrans.csv', - } - self.aero = {# Settings for the aerodynamic model - # 'mona_steady' - steady trim and quasi-steady time domain simulations + 'filename_KGG': 'KGG.dat', + 'filename_uset': 'uset.op2', + 'filename_GM': 'GM.dat', + 'filename_aset': 'aset.bdf', + 'filename_Rtrans': 'Rtrans.csv', + } + # Settings for the aerodynamic model + self.aero = {'method': 'mona_steady', + # 'mona_steady' - steady trim and quasi-steady time domain simulations # 'mona_unsteady' - unsteady time domain simulation based on the RFA, e.g. for gust # 'freq_dom' - frequency domain simulations, e.g. gust, continuous turbulence, flutter, etc # 'nonlin_steady' - steady trim and quasi-steady time domain simulations with some non-linearities # 'cfd_steady' - steady trim # 'cfd_unsteady' - unsteady time domain simulation, e.g. for gust - 'method': 'mona_steady', + # # True or False, aerodynamic feedback of elastic structure on aerodynamics can be deactivated. # You will still see deformations, but there is no coupling. - 'flex': True, + 'flex': True, # aerogrid is given by CAERO1, CAERO7 or by CQUAD4 cards 'method_caero': 'CAERO1', # bdf file(s) with CAERO1 or CQUAD4-cards for aerogrid. IDs in ascending order. @@ -83,156 +86,153 @@ def __init__(self): # bdf file(s) with AELIST-cards 'filename_aelist': ['filename.AELIST'], # The hingeline of a CS is given by a CORD. Either the y- or the z-axis is taken as hingeline. 'y', 'z' - 'hingeline': 'z', + 'hingeline': 'z', # 'vlm' (panel-aero), 'dlm' (panel-aero) or 'nastran' (external form matrices) 'method_AIC': 'vlm', - 'key':['VC', 'MC'], + 'key': ['VC', 'MC'], 'Ma': [0.8, 0.9], - # provide OP4 files with AICs if method_AIC = 'nastran' + # provide OP4 files with AICs if method_AIC = 'nastran' 'filename_AIC': ['AIC_VC.dat', 'AIC_MC.dat'], # reduced frequencies for DLM, Nastran Definition! - 'k_red': [0.001, 0.01, 0.03, 0.1, 0.3, 0.6, 1.0, 1.5 ], + 'k_red': [0.001, 0.01, 0.03, 0.1, 0.3, 0.6, 1.0, 1.5], # number of poles for rational function approximation (RFA) 'n_poles': 4, # Additional parameters for CFD - 'para_path':'/scratch/tau/', - 'para_file':'para', + 'para_path': '/scratch/tau/', + 'para_file': 'para', # Currently implemented interfaces: 'tau' or 'su2' 'cfd_solver': 'tau', 'tau_solver': 'el', 'tau_cores': 16, # --- Start of experimental section, only for special cases --- # Correction coefficient at CG, negativ = destabilizing - 'Cn_beta_corr': [ -0.012], + 'Cn_beta_corr': [-0.012], # Correction coefficient at CG, positiv = destabilizing - 'Cm_alpha_corr':[ 0.22], - # Correction coefficient at MAC, Cd = Cd0 + dCd/dalpha^2 * alpha^2 + 'Cm_alpha_corr': [0.22], + # Correction coefficient at MAC, Cd = Cd0 + dCd/dalpha^2 * alpha^2 'viscous_drag': 'coefficients', 'Cd_0': [0.005], - 'Cd_alpha^2': [0.018*6.28**2.0], + 'Cd_alpha^2': [0.018 * 6.28 ** 2.0], # True or False, calculates local induced drag e.g. for roll-yaw-coupling 'induced_drag': False, - # Symmetry about xz-plane: Only the right hand side on the aero mesh is give. + # Symmetry about xz-plane: Only the right hand side on the aero mesh is give. # The (missing) left hand side is created virtually by mirroring. 'xz_symmetry': False, # --- End of experimental section --- - } - self.meshdefo = {# General CFD surface mesh information - 'surface': - {# implemented file formats: 'cgns', 'netcdf', 'su2' - 'fileformat': 'netcdf', + } + # General CFD surface mesh information + self.meshdefo = {'surface': {'fileformat': 'netcdf', # implemented file formats: 'cgns', 'netcdf', 'su2' # file name of the CFD mesh - 'filename_grid':'tau.grid', - # list of markers [1, 2, ...] or ['upper', 'lower', ...] of surfaces to be included in deformation - 'markers': [1,3], - }, + 'filename_grid': 'tau.grid', + # list of markers [1, 2, ...] or ['upper', 'lower', ...] of surfaces to be included in + # deformation + 'markers': [1, 3], + }, # Volume mesh information, currently unused - 'volume':{}, - } - self.spline = {# Set the was in which the aerodynamic forces are applied to the structure. - # Options: 'nearest_neighbour', 'rbf' or 'nastran' - 'method': 'nearest_neighbour', + 'volume': {}, + } + # Set the was in which the aerodynamic forces are applied to the structure. + self.spline = {'method': 'nearest_neighbour', # Options: 'nearest_neighbour', 'rbf' or 'nastran' # The nastran spline matrix is written to .f06-file with PARAM OPGTKG 1 'filename_f06': 'filename.f06', # Possibility to use only a subset of the structural grid for splining. True or False 'splinegrid': True, # bdf file(s) with GRIDs to ne used - 'filename_splinegrid': ['splinegrid.bdf'] - } - self.mass = {# Settings for the structural dynamics. - # Currently inplemented interfaces: 'f06', 'modalanalysis', 'guyan', 'CoFE', 'B2000' - 'method': 'modalanalysis', + 'filename_splinegrid': ['splinegrid.bdf'] + } + # Settings for the structural dynamics. + self.mass = {'method': 'modalanalysis', # Inplemented interfaces: 'f06', 'modalanalysis', 'guyan', 'CoFE', 'B2000' 'key': ['M1', 'M2'], # MGG via DMAP Alter and OP4 - always required - 'filename_MGG':['MGG_M1.dat', 'MGG_M2.dat'], + 'filename_MGG': ['MGG_M1.dat', 'MGG_M2.dat'], # eigenvalues and eigenvectors from .f06-file - required for 'mona' - 'filename_S103':['SOL103_M1.f06', 'SOL103_M1.f06'], + 'filename_S103': ['SOL103_M1.f06', 'SOL103_M1.f06'], # eigenvalues and eigenvectors from .f06-file - required for 'mona' - 'filename_CoFE':['M1.mat', 'M2.mat'], + 'filename_CoFE': ['M1.mat', 'M2.mat'], # True or False, omits first six modes - 'omit_rb_modes': False, + 'omit_rb_modes': False, # list(s) of modes to use - 'modes':[np.arange(1,13), np.arange(1,16)], - } - self.damping = {# Modal damping can be applied as a factor of the stiffness matrix. - 'method': 'modal', + 'modes': [np.arange(1, 13), np.arange(1, 16)], + } + # Modal damping can be applied as a factor of the stiffness matrix. + self.damping = {'method': 'modal', 'damping': 0.02, - } - self.atmo = {# The international standard atmosphere (ISA) - 'method':'ISA', - 'key':['FL000','FL055', 'FL075', 'FL200', 'FL300', 'FL450'], + } + # The international standard atmosphere (ISA) + self.atmo = {'method': 'ISA', + 'key': ['FL000', 'FL055', 'FL075', 'FL200', 'FL300', 'FL450'], # Altitude in meters - 'h': ft2m([0, 5500, 7500, 20000, 30000, 45000]), - } - self.eom = {# Setting of the rigid body equations of motion, 'linear' or 'waszak' - 'version': 'linear'} - + 'h': ft2m([0, 5500, 7500, 20000, 30000, 45000]), + } + # Setting of the rigid body equations of motion + self.eom = {'version': 'linear'} # 'linear' or 'waszak' + # --- Start of experimental section, only for special cases --- - para_LG = { # Parameters for generic landing gear - # see PhD Thesis of Wolf Krueger and Sunpeth Cumnuantip - 'stroke_length': 0.3, # m - 'fitting_length': 0.72, # m - 'n': 1.4, + # Parameters for generic landing gear, see PhD Thesis of Wolf Krueger and Sunpeth Cumnuantip + para_LG = {'stroke_length': 0.3, # m + 'fitting_length': 0.72, # m + 'n': 1.4, 'ck': 1.0, - 'd2': 85000.0, # N/(m/s)^2 - 'F_static': 53326.0, # N - 'r_tire': 0.28, # m - 'c1_tire': 832000.0, # N/m - 'd1_tire': 4500.0, # N/(m/s)^2 - 'm_tire': 58.0, # kg - } - self.landinggear = {# Activates a generic landing gear model during time simulation - 'method': 'generic', - 'key': ['MLG1', 'MLG2', 'NLG' ], + 'd2': 85000.0, # N/(m/s)^2 + 'F_static': 53326.0, # N + 'r_tire': 0.28, # m + 'c1_tire': 832000.0, # N/m + 'd1_tire': 4500.0, # N/(m/s)^2 + 'm_tire': 58.0, # kg + } + # Activates a generic landing gear model during time simulation + self.landinggear = {'method': 'generic', + 'key': ['MLG1', 'MLG2', 'NLG'], # IDs of FE attachment nodes - 'attachment_point':[800002, 800003, 800001], + 'attachment_point': [800002, 800003, 800001], # Parameters for generic landing gear module, see above 'para': [para_LG, para_LG, para_LG], - } - self.engine= {# Activates an engine model: 'thrust_only', 'propellerdisk', 'PyPropMat' or 'VLM4Prop' - 'method': 'thrust_only', - # Note: 'PyPropMAt' and 'VLM4Prop' require sensors with the same key to measure the local onflow. - 'key': ['E-P', 'E-S' ], - # IDs of FE attachment nodes - 'attachment_point':[54100003, 64100003], - # Thrust orientation vector in body coordinate system - 'thrust_vector': [[-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]], - # Ratational axis of a propeller in body coordinate system - 'rotation_vector': [[-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]], - # Roational inertia in Nms^2 - 'rotation_inertia': [0.18, 0.18], - # Propeller diamater in m - 'diameter': [1.7, 1.7], - # Number of blades - 'n_blades': [2, 2], - # Mach number for VLM4Prop - 'Ma': [0.25], - # Input-file ('.yaml') for PyPropMAt and VLM4Prop - 'propeller_input_file':'HAP_O6_PROP_pitch.yaml', - } - self.pressure_inlet = {# In case a pressure inlet boundary is modeled in the CFD mesh, the boundary condition will be updated with - # the ambient pressure and temperature. Possible application: engine exhaust without thrust. - 'marker': 'exhaust', + } + # Activates an engine model + self.engine = {'method': 'thrust_only', # Engine models: 'thrust_only', 'propellerdisk', 'PyPropMat' or 'VLM4Prop' + # Note: 'PyPropMAt' and 'VLM4Prop' require sensors with the same key to measure the local onflow. + 'key': ['E-P', 'E-S'], + # IDs of FE attachment nodes + 'attachment_point': [54100003, 64100003], + # Thrust orientation vector in body coordinate system + 'thrust_vector': [[-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]], + # Ratational axis of a propeller in body coordinate system + 'rotation_vector': [[-1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]], + # Roational inertia in Nms^2 + 'rotation_inertia': [0.18, 0.18], + # Propeller diamater in m + 'diameter': [1.7, 1.7], + # Number of blades + 'n_blades': [2, 2], + # Mach number for VLM4Prop + 'Ma': [0.25], + # Input-file ('.yaml') for PyPropMAt and VLM4Prop + 'propeller_input_file': 'HAP_O6_PROP_pitch.yaml', + } + # CFD-specific + # In case a pressure inlet boundary is modeled in the CFD mesh, the boundary condition will be + # updated with the ambient pressure and temperature. Possible application: engine exhaust + # without thrust. + self.pressure_inlet = {'marker': 'exhaust', 'flow_direction': [1.0, 0.0, 0.0], } # --- End of experimental section --- """ - Individual FE nodes can be defiend as sensors, e.g. to "measure" accelerations. - Because the data is calculated during the simulations, these sensors may be used as input for a flight controller or similar. - In case a wind sensor is specified here, this sensor is used to "measure" alpha and beta. + Individual FE nodes can be defiend as sensors, e.g. to "measure" accelerations. + Because the data is calculated during the simulations, these sensors may be used as input for a flight controller or + similar. In case a wind sensor is specified here, this sensor is used to "measure" alpha and beta. """ - self.sensor= {'key': ['wind', 'E-P', 'E-S' ], - # IDs of FE attachment nodes - 'attachment_point':[200013, 54100003, 64100003], - } + self.sensor = {'key': ['wind', 'E-P', 'E-S'], + # IDs of FE attachment nodes + 'attachment_point': [200013, 54100003, 64100003], + } """ - This section controls the automatic plotting and selection of dimensioning load cases. - Simply put a list of names of the monitoring stations (e.g. ['MON1', 'MON2',...]) into the dictionary - of possible load plots listed below. This will generate a pdf document and nastran force and moment + This section controls the automatic plotting and selection of dimensioning load cases. + Simply put a list of names of the monitoring stations (e.g. ['MON1', 'MON2',...]) into the dictionary + of possible load plots listed below. This will generate a pdf document and nastran force and moment cards for the dimensioning load cases. """ - self.loadplots = { - 'potatos_fz_mx': ['MON5'], + self.loadplots = {'potatos_fz_mx': ['MON5'], 'potatos_mx_my': ['MON1', 'MON2', 'MON3', 'MON4', 'MON334'], 'potatos_fz_my': [], 'potatos_fy_mx': [], @@ -242,17 +242,16 @@ def __init__(self): } """ The trimcase defines the maneuver load case, one dictionary per load case. - There may be hundreds or thousands of load cases, so at some point it might be beneficial to script this section or import an excel sheet. + There may be hundreds or thousands of load cases, so at some point it might be beneficial to script this section or + import an excel sheet. """ - self.trimcase = [{# Descriptive string of the maneuver case - # e.g. according to G. Pinho Chiozzotto, "Kriterien fuer die Erstellung eines Lastenkatalogs," Institute of - # Aeroelasticity, iLOADs MS1.2, Feb. 2014. - 'desc': 'CC.BFDM.OVCFL000.Vergleichsfall53', - # Kind of trim condition, blank for trim about all three axes, for more trim conditions see trim_conditions.py + self.trimcase = [{'desc': 'CC.BFDM.OVCFL000.Maneuver_xyz', # Descriptive string of the maneuver case + # Kind of trim condition, blank for trim about all three axes, for more trim conditions see + # trim_conditions.py 'maneuver': '', # Subcase ID number, for Nastran in acending order 'subcase': 53, - # Setting of the operational point + # Setting of the operational point # The flight speed is given by the Mach number 'Ma': 0.8, # Aero key @@ -265,41 +264,39 @@ def __init__(self): 'Nz': 5.0, # Velocities and accelerations given in ISO 9300 coordinate system (right-handed, forward-right-down) # Roll rate in rad/s - 'p': 34.3/180.0*np.pi, + 'p': 34.3 / 180.0 * np.pi, # Pitch rate in rad/s - 'q': 28.6/180.0*np.pi, + 'q': 28.6 / 180.0 * np.pi, # Yaw rate in rad/s 'r': 0.0, # Roll acceleration in rad/s^2 - 'pdot': -286.5/180.0*np.pi, + 'pdot': -286.5 / 180.0 * np.pi, # Pitch acceleration in rad/s^2 'qdot': 0.0, # Yaw acceleration in rad/s^2 'rdot': 0.0, # --- Start of experimental section, only for special cases --- # List of DoF to be constrained - 'support': [0,1,2,3,4,5], + 'support': [0, 1, 2, 3, 4, 5], # Thrust per engine in N or 'balanced' - 'thrust':'balanced', + 'thrust': 'balanced', # Euler angle Phi in rad - 'phi': 0.0/180.0*np.pi, + 'phi': 0.0 / 180.0 * np.pi, # Euler angle Theta in rad - 'theta': 0.0/180.0*np.pi, + 'theta': 0.0 / 180.0 * np.pi, # Pilot command Xi in rad - 'command_xi': 0.0/180.0*np.pi, + 'command_xi': 0.0 / 180.0 * np.pi, # Pilot command Eta in rad - 'command_eta': 0.0/180.0*np.pi, + 'command_eta': 0.0 / 180.0 * np.pi, # Pilot command Zeta in rad - 'command_zeta': 0.0/180.0*np.pi, + 'command_zeta': 0.0 / 180.0 * np.pi, # --- End of experimental section --- - }, - ] + }] """ For every trimcase, a corresponding simcase is required. For maneuvers, it may be empty self.simcase = [{}]. A time simulation is triggered if the simcase contains at least 'dt' and 't_final' """ - self.simcase = [{# Time step size of the output in [s] - 'dt': 0.01, + self.simcase = [{'dt': 0.01, # Time step size of the output in [s] # Time step size for the integration scheme, only applicable in case of unsteady cfd simulation 'dt_integration': 0.001, # Final simulation time in [s] @@ -309,11 +306,12 @@ def __init__(self): # Gust gradient H (half gust length) in [m] 'gust_gradient': 9.0, # Orientation of the gust in [deg], 0/360 = gust from bottom, 180 = gust from top, - # 90 = gust from the right, 270 = gust from the left, arbitrary values possible + # 90 = gust from the right, 270 = gust from the left, arbitrary values possible # (rotation of gust direction vector about Nastran's x-axis pointing backwards) - 'gust_orientation': 0, + 'gust_orientation': 0, # Gust parameters according to CS-25 to calculate the gust velocity - 'gust_para':{'Z_mo': 12500.0, 'MLW': 65949.0, 'MTOW': 73365.0, 'MZFW': 62962.0, 'MD': 0.87, 'T1': 0.00}, + 'gust_para': {'Z_mo': 12500.0, 'MLW': 65949.0, 'MTOW': 73365.0, 'MZFW': 62962.0, 'MD': 0.87, + 'T1': 0.00}, # Alternatively, give gust velocity / Vtas directly 'WG_TAS': 0.1, # True or False, enables continuous turbulence excitation @@ -326,16 +324,16 @@ def __init__(self): 'controller': False, # True or False, enables a generic landing gear 'landinggear': False, - # True or False, enables calculation of rigid and elastic derivatives + # True or False, enables calculation of rigid and elastic derivatives 'derivatives': False, # List of DoF to be constrained - 'support': [0,1,2,3,4,5], + 'support': [0, 1, 2, 3, 4, 5], # True or False, enables flutter check with k, ke or pk method 'flutter': False, # flutter parameters for k and ke method - 'flutter_para':{'method': 'k', 'k_red':np.linspace(2.0, 0.001, 1000)}, + 'flutter_para': {'method': 'k', 'k_red': np.linspace(2.0, 0.001, 1000)}, # flutter parameters for pk method - 'flutter_para':{'method': 'pk', 'Vtas':np.linspace(100.0, 500.0, 100)}, - }, - ] - # End \ No newline at end of file + # 'flutter_para': {'method': 'pk', 'Vtas': np.linspace(100.0, 500.0, 100)}, + }, + ] + # End diff --git a/loadscompare/compare.py b/loadscompare/compare.py index 7e9b004..0350204 100644 --- a/loadscompare/compare.py +++ b/loadscompare/compare.py @@ -1,36 +1,43 @@ # -*- coding: utf-8 -*- -from PyQt5 import QtCore -from PyQt5.QtWidgets import (QApplication, QWidget, QTabWidget, QSizePolicy, QGridLayout, QMainWindow, QAction, QListWidget, QListWidgetItem, - QAbstractItemView, QFileDialog, QComboBox, QCheckBox, QLabel) +import os +from PyQt5 import QtCore +from PyQt5.QtWidgets import (QApplication, QWidget, QTabWidget, QSizePolicy, QGridLayout, QMainWindow, QAction, QListWidget, + QListWidgetItem, QAbstractItemView, QFileDialog, QComboBox, QCheckBox, QLabel) import matplotlib -matplotlib.use('Qt5Agg') from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT from matplotlib.figure import Figure + import numpy as np -import os from loadscompare import plotting -import loadskernel.io_functions as io_functions +from loadskernel import io_functions + + +matplotlib.use('Qt5Agg') + class Compare(): + def __init__(self): - self.datasets = { 'ID':[], - 'dataset':[], - 'desc': [], - 'color':[], - 'n': 0, - } + self.datasets = {'ID': [], + 'dataset': [], + 'desc': [], + 'color': [], + 'n': 0, + } self.common_monstations = np.array([]) - self.colors = ['cornflowerblue', 'limegreen', 'violet', 'darkviolet', 'turquoise', 'orange', 'tomato','darkgrey', 'black'] - self.dof = [ 'Fx [N]', 'Fy [N]', 'Fz [N]', 'Mx [Nm]', 'My [Nm]', 'Mz [Nm]'] - + self.colors = ['cornflowerblue', 'limegreen', 'violet', 'darkviolet', 'turquoise', 'orange', 'tomato', 'darkgrey', + 'black'] + self.dof = ['Fx [N]', 'Fy [N]', 'Fz [N]', 'Mx [Nm]', 'My [Nm]', 'Mz [Nm]'] + # define file options self.file_opt = {} - self.file_opt['filters'] = "HDF5 monitoring station files (monstation*.hdf5);;Pickle monitoring station files (monstation*.pickle);;all files (*.*)" + self.file_opt['filters'] = "HDF5 monitoring station files (monstation*.hdf5);;Pickle monitoring station files \ + (monstation*.pickle);;all files (*.*)" self.file_opt['initialdir'] = os.getcwd() - self.file_opt['title'] = 'Load Monstations' - + self.file_opt['title'] = 'Load Monstations' + def run(self): # Create the app. app = QApplication([]) @@ -38,16 +45,16 @@ def run(self): self.initGUI() # Start the main event loop. app.exec_() - + def test(self): """ - This function is intended for CI testing. - To test at least some parts of the code, the app is initialized, but never started. Instead, all windows are closed again. + This function is intended for CI testing. To test at least some parts of the code, the app is initialized, but never + started. Instead, all windows are closed again. """ app = QApplication([]) self.initGUI() app.closeAllWindows() - + def initGUI(self): # Use one Widget as a main container. self.container = QWidget() @@ -61,7 +68,7 @@ def initGUI(self): layout.addWidget(self.tabs_widget, 0, 0, 2, 1) layout.addWidget(self.canvas, 1, 1) layout.addWidget(self.toolbar, 0, 1) - + def initTabs(self): # Configure tabs widget self.tabs_widget = QTabWidget() @@ -82,52 +89,51 @@ def initLoadsTab(self): self.lb_dataset.setSelectionMode(QAbstractItemView.ExtendedSelection) self.lb_dataset.itemSelectionChanged.connect(self.show_choice) self.lb_dataset.itemChanged.connect(self.update_desc) - + self.lb_mon = QListWidget() self.lb_mon.itemSelectionChanged.connect(self.show_choice) - + self.cb_color = QComboBox() self.cb_color.addItems(self.colors) self.cb_color.activated[str].connect(self.update_color) - + self.cb_xaxis = QComboBox() self.cb_xaxis.addItems(self.dof) self.cb_xaxis.setCurrentIndex(3) self.cb_xaxis.activated.connect(self.show_choice) - + self.cb_yaxis = QComboBox() self.cb_yaxis.addItems(self.dof) self.cb_yaxis.setCurrentIndex(4) self.cb_yaxis.activated.connect(self.show_choice) - + self.cb_hull = QCheckBox("show convex hull") self.cb_hull.setChecked(False) self.cb_hull.stateChanged.connect(self.show_choice) - + self.cb_labels = QCheckBox("show labels") self.cb_labels.setChecked(False) self.cb_labels.stateChanged.connect(self.show_choice) - + self.cb_minmax = QCheckBox("show min/max") self.cb_minmax.setChecked(False) self.cb_minmax.stateChanged.connect(self.show_choice) - + self.label_n_loadcases = QLabel() - - + layout = QGridLayout(tab_loads) # Notation: layout.addWidget(widget, row, column, rowSpan, columnSpan) # side-by-side - layout.addWidget(self.lb_dataset, 0,0,1,1) - layout.addWidget(self.lb_mon, 0,1,1,1) + layout.addWidget(self.lb_dataset, 0, 0, 1, 1) + layout.addWidget(self.lb_mon, 0, 1, 1, 1) # one after the other - layout.addWidget(self.cb_color, 1,0,1,2) - layout.addWidget(self.cb_xaxis, 2,0,1,2) - layout.addWidget(self.cb_yaxis, 3,0,1,2) - layout.addWidget(self.cb_hull, 4,0,1,2) - layout.addWidget(self.cb_labels, 5,0,1,2) - layout.addWidget(self.cb_minmax, 6,0,1,2) - layout.addWidget(self.label_n_loadcases, 7,0,1,2) + layout.addWidget(self.cb_color, 1, 0, 1, 2) + layout.addWidget(self.cb_xaxis, 2, 0, 1, 2) + layout.addWidget(self.cb_yaxis, 3, 0, 1, 2) + layout.addWidget(self.cb_hull, 4, 0, 1, 2) + layout.addWidget(self.cb_labels, 5, 0, 1, 2) + layout.addWidget(self.cb_minmax, 6, 0, 1, 2) + layout.addWidget(self.label_n_loadcases, 7, 0, 1, 2) def initMatplotlibFigure(self): # init Matplotlib Plot @@ -142,12 +148,12 @@ def initMatplotlibFigure(self): self.canvas.setSizePolicy(sizePolicy) self.canvas.setMinimumWidth(800) self.canvas.setMinimumHeight(600) - + self.toolbar = NavigationToolbar2QT(self.canvas, self.container) self.toolbar.update() - + def initWindow(self): - # Set up window and menu + # Set up window and menu self.window = QMainWindow() mainMenu = self.window.menuBar() # Add Menu to window @@ -158,18 +164,18 @@ def initWindow(self): action.triggered.connect(self.load_monstation) fileMenu.addAction(action) - # Add Action buttons + # Add Action buttons action = QAction('Merge Monstations', self.window) action.setShortcut('Ctrl+M') action.triggered.connect(self.merge_monstation) fileMenu.addAction(action) - + action = QAction('Save Monstations', self.window) action.setShortcut('Ctrl+S') action.triggered.connect(self.save_monstation) fileMenu.addAction(action) - + # Add exit button action = QAction('Exit', self.window) action.setShortcut('Ctrl+Q') @@ -179,7 +185,7 @@ def initWindow(self): self.window.setCentralWidget(self.container) self.window.setWindowTitle("Loads Compare") self.window.show() - + def show_choice(self, *args): # called on change in listbox, combobox, etc # discard extra variables @@ -189,40 +195,41 @@ def show_choice(self, *args): else: self.cb_color.setDisabled(True) self.update_plot() - + def update_color(self, color): self.datasets['color'][self.lb_dataset.currentRow()] = color self.update_plot() - + def update_desc(self, *args): self.datasets['desc'][self.lb_dataset.currentRow()] = self.lb_dataset.currentItem().text() self.update_plot() - + def update_plot(self): if self.lb_dataset.currentItem() is not None and self.lb_mon.currentItem() is not None: # Reverse current selection of datasets for plotting. The dataset added/created last is plotted first. - # This is useful for example after merging different datasets. The resulting dataset would obscure the view if plotted last. + # This is useful for example after merging different datasets. The resulting dataset would obscure the view if + # plotted last. current_selection = [item.row() for item in self.lb_dataset.selectedIndexes()] dataset_sel = [self.datasets['dataset'][i] for i in current_selection] - color_sel = [self.datasets['color'][i] for i in current_selection] - desc_sel = [self.datasets['desc'][i] for i in current_selection] - mon_sel = self.common_monstations[self.lb_mon.currentRow()] - n_subcases = [dataset[mon_sel]['subcases'].__len__() for dataset in dataset_sel ] - - self.plotting.potato_plots( dataset_sel, - mon_sel, - desc_sel, - color_sel, - self.cb_xaxis.currentIndex(), - self.cb_yaxis.currentIndex(), - self.cb_xaxis.currentText(), - self.cb_yaxis.currentText(), - self.cb_hull.isChecked(), - self.cb_labels.isChecked(), - self.cb_minmax.isChecked(), - ) + color_sel = [self.datasets['color'][i] for i in current_selection] + desc_sel = [self.datasets['desc'][i] for i in current_selection] + mon_sel = self.common_monstations[self.lb_mon.currentRow()] + n_subcases = [len(dataset[mon_sel]['subcases']) for dataset in dataset_sel] + + self.plotting.potato_plots(dataset_sel, + mon_sel, + desc_sel, + color_sel, + self.cb_xaxis.currentIndex(), + self.cb_yaxis.currentIndex(), + self.cb_xaxis.currentText(), + self.cb_yaxis.currentText(), + self.cb_hull.isChecked(), + self.cb_labels.isChecked(), + self.cb_minmax.isChecked(), + ) self.label_n_loadcases.setText('Selected load case: {}'.format(np.sum(n_subcases))) - else: + else: self.plotting.plot_nothing() self.canvas.draw() @@ -231,7 +238,7 @@ def merge_monstation(self): # Init new dataset. new_dataset = {} for x in [item.row() for item in self.lb_dataset.selectedIndexes()]: - print ('Working on {} ...'.format(self.datasets['desc'][x])) + print('Working on {} ...'.format(self.datasets['desc'][x])) for station in self.common_monstations: if station not in new_dataset.keys(): # create (empty) entries for new monstation @@ -239,43 +246,44 @@ def merge_monstation(self): 'CP': self.datasets['dataset'][x][station]['CP'][()], 'offset': self.datasets['dataset'][x][station]['offset'][()], 'subcases': [], - 'loads':[], - 't':[], + 'loads': [], + 't': [], } - # Merge. - new_dataset[station]['loads'] += list(self.datasets['dataset'][x][station]['loads'][()]) - new_dataset[station]['subcases'] += list(self.datasets['dataset'][x][station]['subcases'][()]) - new_dataset[station]['t'] += list(self.datasets['dataset'][x][station]['t'][()]) - + # Merge. + new_dataset[station]['loads'] += list(self.datasets['dataset'][x][station]['loads'][()]) + new_dataset[station]['subcases'] += list(self.datasets['dataset'][x][station]['subcases'][()]) + new_dataset[station]['t'] += list(self.datasets['dataset'][x][station]['t'][()]) + # Save into data structure. - self.datasets['ID'].append(self.datasets['n']) + self.datasets['ID'].append(self.datasets['n']) self.datasets['dataset'].append(new_dataset) self.datasets['color'].append(self.colors[self.datasets['n']]) - self.datasets['desc'].append('dataset '+ str(self.datasets['n'])) + self.datasets['desc'].append('dataset ' + str(self.datasets['n'])) self.datasets['n'] += 1 # Update fields. self.update_fields() - + def load_monstation(self): # open file dialog - filename = QFileDialog.getOpenFileName(self.window, self.file_opt['title'], self.file_opt['initialdir'], self.file_opt['filters'])[0] + filename = QFileDialog.getOpenFileName(self.window, self.file_opt['title'], self.file_opt['initialdir'], + self.file_opt['filters'])[0] if filename != '': if '.pickle' in filename: with open(filename, 'rb') as f: dataset = io_functions.data_handling.load_pickle(f) elif '.hdf5' in filename: dataset = io_functions.data_handling.load_hdf5(filename) - + # save into data structure - self.datasets['ID'].append(self.datasets['n']) + self.datasets['ID'].append(self.datasets['n']) self.datasets['dataset'].append(dataset) self.datasets['color'].append(self.colors[self.datasets['n']]) - self.datasets['desc'].append('dataset '+ str(self.datasets['n'])) + self.datasets['desc'].append('dataset ' + str(self.datasets['n'])) self.datasets['n'] += 1 # update fields self.update_fields() self.file_opt['initialdir'] = os.path.split(filename)[0] - + def save_monstation(self): """ Saving an HDF5 file to HDF5 does not work (yet). @@ -283,7 +291,8 @@ def save_monstation(self): if self.lb_dataset.currentItem() is not None and len(self.lb_dataset.selectedItems()) == 1: dataset_sel = self.datasets['dataset'][self.lb_dataset.currentRow()] # open file dialog - filename = QFileDialog.getSaveFileName(self.window, self.file_opt['title'], self.file_opt['initialdir'], self.file_opt['filters'])[0] + filename = QFileDialog.getSaveFileName(self.window, self.file_opt['title'], self.file_opt['initialdir'], + self.file_opt['filters'])[0] if filename != '' and '.pickle' in filename: with open(filename, 'wb') as f: io_functions.data_handling.dump_pickle(dataset_sel, f) @@ -303,9 +312,11 @@ def update_fields(self): for x in self.common_monstations: self.lb_mon.addItem(QListWidgetItem(x)) + def command_line_interface(): c = Compare() c.run() - + + if __name__ == "__main__": command_line_interface() diff --git a/loadscompare/plotting.py b/loadscompare/plotting.py index a66f4d3..43e2e25 100644 --- a/loadscompare/plotting.py +++ b/loadscompare/plotting.py @@ -1,45 +1,40 @@ # -*- coding: utf-8 -*- -""" -Created on Fri Feb 10 09:24:10 2017 - -@author: voss_ar -""" - - -import numpy as np import os + import matplotlib.pyplot as plt -from scipy.spatial import ConvexHull from loadskernel import plotting_standard + class Plotting(plotting_standard.LoadPlots): + def __init__(self, fig): plt.rcParams.update({'font.size': 16, - 'svg.fonttype':'none'}) - self.subplot = fig.add_axes([0.2, 0.15, 0.7, 0.75]) # List is [left, bottom, width, height] - im = plt.imread(os.path.dirname(__file__)+'/graphics/LK_logo2.png') + 'svg.fonttype': 'none'}) + self.subplot = fig.add_axes([0.2, 0.15, 0.7, 0.75]) # List is [left, bottom, width, height] + im = plt.imread(os.path.dirname(__file__) + '/graphics/LK_logo2.png') newax = fig.add_axes([0.04, 0.02, 0.10, 0.08]) newax.imshow(im, interpolation='hanning') newax.axis('off') - + def plot_nothing(self): self.subplot.cla() - - def potato_plots(self, dataset_sel, station, descs, colors, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis, show_hull, show_labels, show_minmax): + + def potato_plots(self, dataset_sel, station, descs, colors, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis, + show_hull, show_labels, show_minmax): # This function relies on the potato plotting function in LK imported above to avoid code duplications. # The labels, margins, etc. are adjusted in this function to fit the window space. self.subplot.cla() for i_dataset in range(len(dataset_sel)): self.crit_trimcases = [] self.add_monstations(dataset_sel[i_dataset]) - self.potato_plot(station, descs[i_dataset], colors[i_dataset], dof_xaxis, dof_yaxis, show_hull, show_labels, show_minmax) + self.potato_plot(station, descs[i_dataset], colors[i_dataset], dof_xaxis, dof_yaxis, + show_hull, show_labels, show_minmax) self.subplot.legend(loc='best') - self.subplot.ticklabel_format(style='sci', axis='x', scilimits=(-2,2)) - self.subplot.ticklabel_format(style='sci', axis='y', scilimits=(-2,2)) + self.subplot.ticklabel_format(style='sci', axis='x', scilimits=(-2, 2)) + self.subplot.ticklabel_format(style='sci', axis='y', scilimits=(-2, 2)) self.subplot.grid(True) yax = self.subplot.get_yaxis() yax.set_label_coords(x=-0.18, y=0.5) self.subplot.set_xlabel(var_xaxis) self.subplot.set_ylabel(var_yaxis) - \ No newline at end of file diff --git a/loadskernel/atmosphere.py b/loadskernel/atmosphere.py index d2c2cf8..9c4bb79 100644 --- a/loadskernel/atmosphere.py +++ b/loadskernel/atmosphere.py @@ -1,58 +1,48 @@ # -*- coding: utf-8 -*- -""" -Created on Wed Oct 15 09:23:59 2014 +import math +import logging -@author: voss_ar -""" -import math, logging def isa(h): - # p, rho, T, a = atmo_isa(h) # Calculation of various atmospheric parameters according to the International Standard Atmosphere (ISA) - # This function is in the style of Thiemo Kiers atmo_isa.m, - # but functions for p and T are derived from the US Standard Atmosphere 1976. + # Functions for p and T are derived from the US Standard Atmosphere 1976. # Reference: ISA, ISO 2533, 1975 # US Standard Atmosphere 1976 - # check input if h < -5000 or h > 47000: - logging.error( 'Altitude h = ' + str(h) + ' m, must be -5000 <= h <= 47000 m.') - return - - g0 = 9.80665; # acceleration of gravity (sea level) - # Die US Standard Atmosphere 1976 arbeitet eigentlich mit der universellen - # Gaskonstante und der molaren Masse. + logging.error('Altitude h = ' + str(h) + ' m, must be -5000 <= h <= 47000 m.') + + g0 = 9.80665 # acceleration of gravity (sea level) + # Die US Standard Atmosphere 1976 arbeitet eigentlich mit der universellen + # Gaskonstante und der molaren Masse. # R = R* / M_Luft - R = 287.0531; # spec. gas constant [J/kg/K] - gamma = 1.4; # ratio of specific heats - # beta = 1.458e-6; # constant for calculation mu - # S = 110.4; # Sutherland's constant - - # properties of: troposphere tropopause stratosphere - # 20km=h: - layer = i-1 + hbounds = [-5001.0, 11000.0, 20000.0, 32000.0, 47000.0] + for i, hbound in enumerate(hbounds): + if hbound >= h: + layer = i - 1 break - + # formulas apply up to a height of 86km # differences in intergartion in case of lambda = 0 lead to a distinction between two formulas for p if lambda_ref[layer] == 0: - p = pref[layer] * math.exp( -g0 * (h-href[layer]) / R / Tref[layer] ) + p = pref[layer] * math.exp(-g0 * (h - href[layer]) / R / Tref[layer]) else: - p = pref[layer] * (Tref[layer] / ( Tref[layer]+lambda_ref[layer]*(h-href[layer]) ))**(g0/R/lambda_ref[layer]) - - T = Tref[layer] + lambda_ref[layer]*(h-href[layer]) - rho = p/R/T - a = (gamma*R*T)**0.5 - - return p, rho, T, a + p = pref[layer] * (Tref[layer] / (Tref[layer] + lambda_ref[layer] * (h - href[layer]))) ** (g0 / R / lambda_ref[layer]) + + T = Tref[layer] + lambda_ref[layer] * (h - href[layer]) + rho = p / R / T + a = (gamma * R * T) ** 0.5 + return p, rho, T, a diff --git a/loadskernel/auxiliary_output.py b/loadskernel/auxiliary_output.py index b218ac1..a34365b 100644 --- a/loadskernel/auxiliary_output.py +++ b/loadskernel/auxiliary_output.py @@ -1,193 +1,187 @@ - -import numpy as np -import copy, getpass, platform, time, logging, csv +import logging from collections import OrderedDict +import numpy as np -import loadskernel.io_functions as io_functions -import loadskernel.io_functions.write_mona -import loadskernel.io_functions.data_handling -from loadskernel.grid_trafo import * +from loadskernel.io_functions import write_mona, data_handling from loadskernel.io_functions.data_handling import load_hdf5_dict -class AuxiliaryOutput(object): + +class AuxiliaryOutput(): """ This class provides functions to save data of trim calculations. - """ + """ + def __init__(self, jcl, model, trimcase): self.jcl = jcl self.model = model self.trimcase = trimcase self.responses = [] self.crit_trimcases = [] + self.dyn2stat_data = {} + self.monstations = {} + + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + self.mongrid = load_hdf5_dict(self.model['mongrid']) + self.macgrid = load_hdf5_dict(self.model['macgrid']) + self.coord = load_hdf5_dict(self.model['coord']) + + self.Dkx1 = self.model['Dkx1'][()] - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) - self.mongrid = load_hdf5_dict(self.model['mongrid']) - self.macgrid = load_hdf5_dict(self.model['macgrid']) - self.coord = load_hdf5_dict(self.model['coord']) - - self.Dkx1 = self.model['Dkx1'][()] - - def save_nodaldefo(self, filename): - # deformations are given in 9300 coord - strcgrid_tmp = copy.deepcopy(self.strcgrid) - grid_trafo(strcgrid_tmp, self.coord, 9300) - logging.info( 'saving nodal flexible deformations as dat file...') - with open(filename+'_undeformed.dat', 'w') as fid: - np.savetxt(fid, np.hstack((self.strcgrid['ID'].reshape(-1,1), strcgrid_tmp['offset']))) - - for i_trimcase in range(len(self.jcl.trimcase)): - with open(filename+'_subcase_'+str(self.jcl.trimcase[i_trimcase]['subcase'])+'_Ug.dat', 'w') as fid: - defo = np.hstack((self.strcgrid['ID'].reshape(-1,1), self.strcgrid['offset'] + self.responses[i_trimcase]['Ug_r'][self.strcgrid['set'][:,0:3]] + + self.responses[i_trimcase]['Ug_f'][self.model.strcgrid['set'][:,0:3]] * 500.0)) - np.savetxt(fid, defo) - def write_all_nodalloads(self, filename): - logging.info( 'saving all nodal loads as Nastarn cards...') - with open(filename+'_Pg', 'w') as fid: + logging.info('saving all nodal loads as Nastarn cards...') + with open(filename + '_Pg', 'w') as fid: for i_trimcase in range(len(self.jcl.trimcase)): - io_functions.write_mona.write_force_and_moment_cards(fid, self.strcgrid, self.responses[i_trimcase]['Pg'][0,:], self.jcl.trimcase[i_trimcase]['subcase']) - with open(filename+'_subcases', 'w') as fid: + write_mona.write_force_and_moment_cards(fid, self.strcgrid, + self.responses[i_trimcase]['Pg'][0, :], + self.jcl.trimcase[i_trimcase]['subcase']) + with open(filename + '_subcases', 'w') as fid: for i_trimcase in range(len(self.jcl.trimcase)): - io_functions.write_mona.write_subcases(fid, self.jcl.trimcase[i_trimcase]['subcase'], self.jcl.trimcase[i_trimcase]['desc']) - + write_mona.write_subcases(fid, self.jcl.trimcase[i_trimcase]['subcase'], + self.jcl.trimcase[i_trimcase]['desc']) + def write_trimresults(self, filename_csv): trimresults = [] for response in self.responses: trimresult = self.assemble_trimresult(response) trimresults.append(trimresult) - logging.info('writing trim results to: ' + filename_csv) - io_functions.data_handling.write_list_of_dictionaries(trimresults, filename_csv) - + logging.info('writing trim results to: %s', filename_csv) + data_handling.write_list_of_dictionaries(trimresults, filename_csv) + def assemble_trimresult(self, response): - trimresult = OrderedDict({'subcase': self.jcl.trimcase[response['i'][()]]['subcase'], - 'desc': self.jcl.trimcase[response['i'][()]]['desc'], + trimresult = OrderedDict({'subcase': self.jcl.trimcase[response['i'][()]]['subcase'], + 'desc': self.jcl.trimcase[response['i'][()]]['desc'], }) - self.n_modes = self.model['mass'][self.jcl.trimcase[response['i'][()]]['mass']]['n_modes'][()] + n_modes = self.model['mass'][self.jcl.trimcase[response['i'][()]]['mass']]['n_modes'][()] # get trimmed states - trimresult['x'] = response['X'][0,0] - trimresult['y'] = response['X'][0,1] - trimresult['z'] = response['X'][0,2] - trimresult['phi [deg]'] = response['X'][0,3]/np.pi*180.0 - trimresult['theta [deg]'] = response['X'][0,4]/np.pi*180.0 - trimresult['psi [deg]'] = response['X'][0,5]/np.pi*180.0 - trimresult['dx'] = response['Y'][0,0] - trimresult['dy'] = response['Y'][0,1] - trimresult['dz'] = response['Y'][0,2] - trimresult['u'] = response['X'][0,6] - trimresult['v'] = response['X'][0,7] - trimresult['w'] = response['X'][0,8] - trimresult['p'] = response['X'][0,9] - trimresult['q'] = response['X'][0,10] - trimresult['r'] = response['X'][0,11] - trimresult['du'] = response['Y'][0,6] - trimresult['dv'] = response['Y'][0,7] - trimresult['dw'] = response['Y'][0,8] - trimresult['dp'] = response['Y'][0,9] - trimresult['dq'] = response['Y'][0,10] - trimresult['dr'] = response['Y'][0,11] - trimresult['command_xi [deg]'] = response['X'][0,12+2*self.n_modes]/np.pi*180.0 - trimresult['command_eta [deg]'] = response['X'][0,13+2*self.n_modes]/np.pi*180.0 - trimresult['command_zeta [deg]'] = response['X'][0,14+2*self.n_modes]/np.pi*180.0 - trimresult['thrust per engine [N]'] = response['X'][0,15+2*self.n_modes] - trimresult['stabilizer [deg]'] = response['X'][0,16+2*self.n_modes]/np.pi*180.0 - trimresult['flap setting [deg]'] = response['X'][0,17+2*self.n_modes]/np.pi*180.0 - trimresult['Nz'] = response['Nxyz'][0,2] - trimresult['Vtas'] = response['Y'][0,-2] - trimresult['q_dyn'] = response['q_dyn'][0,0] - trimresult['alpha [deg]'] = response['alpha'][0,0]/np.pi*180.0 - trimresult['beta [deg]'] = response['beta'][0,0]/np.pi*180.0 - + trimresult['x'] = response['X'][0, 0] + trimresult['y'] = response['X'][0, 1] + trimresult['z'] = response['X'][0, 2] + trimresult['phi [deg]'] = response['X'][0, 3] / np.pi * 180.0 + trimresult['theta [deg]'] = response['X'][0, 4] / np.pi * 180.0 + trimresult['psi [deg]'] = response['X'][0, 5] / np.pi * 180.0 + trimresult['dx'] = response['Y'][0, 0] + trimresult['dy'] = response['Y'][0, 1] + trimresult['dz'] = response['Y'][0, 2] + trimresult['u'] = response['X'][0, 6] + trimresult['v'] = response['X'][0, 7] + trimresult['w'] = response['X'][0, 8] + trimresult['p'] = response['X'][0, 9] + trimresult['q'] = response['X'][0, 10] + trimresult['r'] = response['X'][0, 11] + trimresult['du'] = response['Y'][0, 6] + trimresult['dv'] = response['Y'][0, 7] + trimresult['dw'] = response['Y'][0, 8] + trimresult['dp'] = response['Y'][0, 9] + trimresult['dq'] = response['Y'][0, 10] + trimresult['dr'] = response['Y'][0, 11] + trimresult['command_xi [deg]'] = response['X'][0, 12 + 2 * n_modes] / np.pi * 180.0 + trimresult['command_eta [deg]'] = response['X'][0, 13 + 2 * n_modes] / np.pi * 180.0 + trimresult['command_zeta [deg]'] = response['X'][0, 14 + 2 * n_modes] / np.pi * 180.0 + trimresult['thrust per engine [N]'] = response['X'][0, 15 + 2 * n_modes] + trimresult['stabilizer [deg]'] = response['X'][0, 16 + 2 * n_modes] / np.pi * 180.0 + trimresult['flap setting [deg]'] = response['X'][0, 17 + 2 * n_modes] / np.pi * 180.0 + trimresult['Nz'] = response['Nxyz'][0, 2] + trimresult['Vtas'] = response['Y'][0, -2] + trimresult['q_dyn'] = response['q_dyn'][0, 0] + trimresult['alpha [deg]'] = response['alpha'][0, 0] / np.pi * 180.0 + trimresult['beta [deg]'] = response['beta'][0, 0] / np.pi * 180.0 + # calculate additional aero coefficients - Pmac_rbm = np.dot(self.Dkx1.T, response['Pk_rbm'][0,:]) - Pmac_cam = np.dot(self.Dkx1.T, response['Pk_cam'][0,:]) - Pmac_cs = np.dot(self.Dkx1.T, response['Pk_cs'][0,:]) - Pmac_f = np.dot(self.Dkx1.T, response['Pk_f'][0,:]) - Pmac_idrag = np.dot(self.Dkx1.T, response['Pk_idrag'][0,:]) - A = self.jcl.general['A_ref'] #sum(self.model.aerogrid['A'][:]) - AR = self.jcl.general['b_ref']**2.0 / self.jcl.general['A_ref'] - Pmac_c = np.divide(response['Pmac'][0,:],response['q_dyn'][0])/A + Pmac_rbm = np.dot(self.Dkx1.T, response['Pk_rbm'][0, :]) + Pmac_cam = np.dot(self.Dkx1.T, response['Pk_cam'][0, :]) + Pmac_cs = np.dot(self.Dkx1.T, response['Pk_cs'][0, :]) + Pmac_f = np.dot(self.Dkx1.T, response['Pk_f'][0, :]) + Pmac_idrag = np.dot(self.Dkx1.T, response['Pk_idrag'][0, :]) + A = self.jcl.general['A_ref'] # sum(self.model.aerogrid['A'][:]) + AR = self.jcl.general['b_ref'] ** 2.0 / self.jcl.general['A_ref'] + Pmac_c = np.divide(response['Pmac'][0, :], response['q_dyn'][0]) / A # um alpha drehen, um Cl und Cd zu erhalten - Cl = Pmac_c[2]*np.cos(response['alpha'][0,0])+Pmac_c[0]*np.sin(response['alpha'][0,0]) - Cd = Pmac_c[2]*np.sin(response['alpha'][0,0])+Pmac_c[0]*np.cos(response['alpha'][0,0]) - Cd_ind_theo = Cl**2.0/np.pi/AR - - trimresult['Cz_rbm'] = Pmac_rbm[2]/response['q_dyn'][0,0]/A - trimresult['Cz_cam'] = Pmac_cam[2]/response['q_dyn'][0,0]/A - trimresult['Cz_cs'] = Pmac_cs[2]/response['q_dyn'][0,0]/A - trimresult['Cz_f'] = Pmac_f[2]/response['q_dyn'][0,0]/A + Cl = Pmac_c[2] * np.cos(response['alpha'][0, 0]) + Pmac_c[0] * np.sin(response['alpha'][0, 0]) + Cd = Pmac_c[2] * np.sin(response['alpha'][0, 0]) + Pmac_c[0] * np.cos(response['alpha'][0, 0]) + Cd_ind_theo = Cl ** 2.0 / np.pi / AR + + trimresult['Cz_rbm'] = Pmac_rbm[2] / response['q_dyn'][0, 0] / A + trimresult['Cz_cam'] = Pmac_cam[2] / response['q_dyn'][0, 0] / A + trimresult['Cz_cs'] = Pmac_cs[2] / response['q_dyn'][0, 0] / A + trimresult['Cz_f'] = Pmac_f[2] / response['q_dyn'][0, 0] / A trimresult['Cx'] = Pmac_c[0] trimresult['Cy'] = Pmac_c[1] trimresult['Cz'] = Pmac_c[2] - trimresult['Cmx'] = Pmac_c[3]/self.macgrid['b_ref'] - trimresult['Cmy'] = Pmac_c[4]/self.macgrid['c_ref'] - trimresult['Cmz'] = Pmac_c[5]/self.macgrid['b_ref'] + trimresult['Cmx'] = Pmac_c[3] / self.macgrid['b_ref'] + trimresult['Cmy'] = Pmac_c[4] / self.macgrid['c_ref'] + trimresult['Cmz'] = Pmac_c[5] / self.macgrid['b_ref'] trimresult['Cl'] = Cl trimresult['Cd'] = Cd - trimresult['E'] = Cl/Cd - trimresult['Cd_ind'] = Pmac_idrag[0]/response['q_dyn'][0,0]/A - trimresult['Cmz_ind'] = Pmac_idrag[5]/response['q_dyn'][0,0]/A/self.macgrid['b_ref'] - trimresult['e'] = Cd_ind_theo/(Pmac_idrag[0]/response['q_dyn'][0,0]/A) + trimresult['E'] = Cl / Cd + trimresult['Cd_ind'] = Pmac_idrag[0] / response['q_dyn'][0, 0] / A + trimresult['Cmz_ind'] = Pmac_idrag[5] / response['q_dyn'][0, 0] / A / self.macgrid['b_ref'] + trimresult['e'] = Cd_ind_theo / (Pmac_idrag[0] / response['q_dyn'][0, 0] / A) - return trimresult + return trimresult def write_successful_trimcases(self, filename_sucessfull, filename_failed): - # Get the index of all sucessfull responses. This relies on the mechanism, that when loading the responses from HDF5, - # only the successfull ones are returned. + # Get the index of all sucessfull responses. This relies on the mechanism, that when loading the responses from HDF5, + # only the successfull ones are returned. i_cases_sucessfull = [response['i'][()] for response in self.responses] # Init two empty lists trimcases_sucessfull = [] trimcases_failed = [] - # Loop over all trim cases and sort them into the two lists - for i_case in range(len(self.jcl.trimcase)): - trimcase = {'subcase': self.jcl.trimcase[i_case]['subcase'], - 'desc': self.jcl.trimcase[i_case]['desc'],} + # Loop over all trim cases and sort them into the two lists + for i_case, _ in enumerate(self.jcl.trimcase): + trimcase = {'subcase': self.jcl.trimcase[i_case]['subcase'], + 'desc': self.jcl.trimcase[i_case]['desc'], } if i_case in i_cases_sucessfull: trimcases_sucessfull.append(trimcase) else: trimcases_failed.append(trimcase) - logging.info('writing successful trimcases cases to: ' + filename_sucessfull) - io_functions.data_handling.write_list_of_dictionaries(trimcases_sucessfull, filename_sucessfull) - logging.info('writing failed trimcases cases to: ' + filename_failed) - io_functions.data_handling.write_list_of_dictionaries(trimcases_failed, filename_failed) + logging.info('writing successful trimcases cases to: %s', filename_sucessfull) + data_handling.write_list_of_dictionaries(trimcases_sucessfull, filename_sucessfull) + logging.info('writing failed trimcases cases to: %s', filename_failed) + data_handling.write_list_of_dictionaries(trimcases_failed, filename_failed) def write_critical_trimcases(self, filename_csv): # eigentlich gehoert diese Funtion eher zum post-processing als zum # plotten, kann aber erst nach dem plotten ausgefuehrt werden... - crit_trimcases = list(set([crit_trimcase.split('_')[0] for crit_trimcase in self.crit_trimcases])) # extract original subcase number + + # extract original subcase number + crit_trimcases = list(set([crit_trimcase.split('_')[0] for crit_trimcase in self.crit_trimcases])) crit_trimcases_info = [] - for i_case in range(len(self.jcl.trimcase)): + for i_case, _ in enumerate(self.jcl.trimcase): if str(self.jcl.trimcase[i_case]['subcase']) in crit_trimcases: - trimcase = {'subcase': self.jcl.trimcase[i_case]['subcase'], - 'desc': self.jcl.trimcase[i_case]['desc'],} + trimcase = {'subcase': self.jcl.trimcase[i_case]['subcase'], + 'desc': self.jcl.trimcase[i_case]['desc'], } crit_trimcases_info.append(trimcase) - - logging.info('writing critical trimcases cases to: ' + filename_csv) - io_functions.data_handling.write_list_of_dictionaries(crit_trimcases_info, filename_csv) - - def write_critical_nodalloads(self, filename): - logging.info( 'saving critical nodal loads as Nastarn cards...') - # This is quite a complicated sorting because the subcases from dyn2stat may contain non-numeric characters. - # A "normal" sorting returns an undesired sequence, leading IDs in a non-ascending sequence. This a not allowed by Nastran. + + logging.info('writing critical trimcases cases to: %s', filename_csv) + data_handling.write_list_of_dictionaries(crit_trimcases_info, filename_csv) + + def write_critical_nodalloads(self, filename): + logging.info('saving critical nodal loads as Nastarn cards...') + # This is quite a complicated sorting because the subcases from dyn2stat may contain non-numeric characters. + # A "normal" sorting returns an undesired sequence, leading IDs in a non-ascending sequence. This a not + # allowed by Nastran. subcases_IDs = list(self.dyn2stat_data['subcases_ID'][:]) - if type(self.dyn2stat_data['subcases']) == list: - # This is an exception if source is not a hdf5 file. + if isinstance(self.dyn2stat_data['subcases'], list): + # This is an exception if source is not a hdf5 file. # For example, the monstations have been pre-processed by a merge script and are lists already. subcases = self.dyn2stat_data['subcases'] else: # make sure this is a list of strings subcases = list(self.dyn2stat_data['subcases'].asstr()[:]) - crit_ids = [subcases_IDs[subcases.index(str(crit_trimcase))] for crit_trimcase in np.unique(self.crit_trimcases) ] + crit_ids = [subcases_IDs[subcases.index(str(crit_trimcase))] for crit_trimcase in np.unique(self.crit_trimcases)] crit_ids = np.sort(crit_ids) - with open(filename+'_Pg', 'w') as fid: + with open(filename + '_Pg', 'w') as fid: for subcase_ID in crit_ids: idx = subcases_IDs.index(subcase_ID) - io_functions.write_mona.write_force_and_moment_cards(fid, self.strcgrid, self.dyn2stat_data['Pg'][idx][:], subcases_IDs[idx]) - with open(filename+'_subcases', 'w') as fid: + write_mona.write_force_and_moment_cards(fid, self.strcgrid, self.dyn2stat_data['Pg'][idx][:], + subcases_IDs[idx]) + with open(filename + '_subcases', 'w') as fid: for subcase_ID in crit_ids: idx = subcases_IDs.index(subcase_ID) - io_functions.write_mona.write_subcases(fid, subcases_IDs[idx], subcases[idx]) - - def write_critical_sectionloads(self, base_filename): + write_mona.write_subcases(fid, subcases_IDs[idx], subcases[idx]) + + def write_critical_sectionloads(self, base_filename): crit_trimcases = np.unique(self.crit_trimcases) crit_monstations = {} for key, monstation in self.monstations.items(): @@ -208,76 +202,5 @@ def write_critical_sectionloads(self, base_filename): crit_monstations[key]['t'] += [monstation['t'][pos_to_copy]] logging.info('saving critical monstation(s).') with open(base_filename + '.pickle', 'wb') as f: - io_functions.data_handling.dump_pickle(crit_monstations, f) - io_functions.data_handling.dump_hdf5(base_filename + '.hdf5', crit_monstations) - - def save_cpacs_header(self): - - self.cf.add_elem('/cpacs/header', 'name', self.jcl.general['aircraft'], 'text') - self.cf.add_elem('/cpacs/header', 'creator', getpass.getuser() + ' on ' + platform.node() + ' (' + platform.platform() +')', 'text') - self.cf.add_elem('/cpacs/header', 'description', 'This is a file generated by Loads Kernel.', 'text') - self.cf.add_elem('/cpacs/header', 'timestamp', time.strftime("%Y-%m-%d %H:%M", time.localtime()), 'text' ) - - def save_cpacs_flight_load_cases(self): - # create flighLoadCases - self.cf.create_path('/cpacs/vehicles/aircraft/model/analysis', 'loadAnalysis/loadCases/flightLoadCases') - path_flight_load_cases = '/cpacs/vehicles/aircraft/model/analysis/loadAnalysis/loadCases/flightLoadCases' - - # create nodal + cut loads for each trim case - for i_trimcase in range(len(self.jcl.trimcase)): - # info on current load case - self.tixi.createElement(path_flight_load_cases, 'flightLoadCase') - self.cf.add_elem(path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']', 'name', 'subcase ' + str(self.jcl.trimcase[i_trimcase]['subcase']), 'double') - self.cf.add_elem(path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']', 'uID', self.jcl.trimcase[i_trimcase]['desc'], 'text') - desc_string = 'calculated with Loads Kernel on ' + time.strftime("%Y-%m-%d %H:%M", time.localtime()) + ' by ' + getpass.getuser() + ' on ' + platform.node() + ' (' + platform.platform() +')' - self.cf.add_elem(path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']', 'description', desc_string , 'text') - # nodal loads - self.cf.create_path(path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']', 'nodalLoads/wingNodalLoad') - path_nodalLoads = path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']'+'/nodalLoads/wingNodalLoad' - self.cf.add_elem(path_nodalLoads, 'parentUID', 'complete aircraft', 'text') - self.cf.write_cpacs_loadsvector(path_nodalLoads, self.strcgrid, self.responses[i_trimcase]['Pg'] ) - # cut loads - self.cf.create_path(path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']', 'cutLoads/wingCutLoad') - path_cutLoads = path_flight_load_cases+'/flightLoadCase['+str(i_trimcase+1)+']'+'/cutLoads/wingCutLoad' - self.cf.add_elem(path_cutLoads, 'parentUID', 'complete aircraft', 'text') - self.cf.write_cpacs_loadsvector(path_cutLoads, self.mongrid, self.responses[i_trimcase]['Pmon_local']) - - def save_cpacs_dynamic_aircraft_model_points(self): - # save structural grid points to CPACS - self.cf.create_path('/cpacs/vehicles/aircraft/model/wings/wing/dynamicAircraftModel', 'dynamicAircraftModelPoints') - path_dynamic_aircraft_model_points = '/cpacs/vehicles/aircraft/model/wings/wing/dynamicAircraftModel/dynamicAircraftModelPoints' - self.cf.write_cpacs_grid(path_dynamic_aircraft_model_points, self.strcgrid) - - def save_cpacs_cut_load_integration_points(self): - # save monitoring stations to CPACS - self.cf.create_path('/cpacs/vehicles/aircraft/model/wings/wing/dynamicAircraftModel', 'cutLoadIntegrationPoints') - path_cut_load_integration_points = '/cpacs/vehicles/aircraft/model/wings/wing/dynamicAircraftModel/cutLoadIntegrationPoints' - self.cf.write_cpacs_grid(path_cut_load_integration_points, self.mongrid) - #self.cf.write_cpacs_grid_orientation(path_CutLoadIntegrationPoints, self.model.mongrid, self.model.coord) - - def save_cpacs(self, filename): - """ - This function requires the tixiwrapper.py, which is supplied with TIXI. - The file is not part of the repository and needs to be put in a place from where it can be imported. - """ - logging.info( 'saving nodal loads and monitoring stations as CPACS...') - from tixiwrapper import Tixi - self.tixi = Tixi() - self.tixi.create('cpacs') - self.cf = io_functions.cpacs_functions.CpacsFunctions(self.tixi) - - # These paths might already exist when writing into a given CPACS-file... - self.cf.create_path('/cpacs', 'header') - self.save_cpacs_header() - - self.cf.create_path('/cpacs', 'vehicles/aircraft/model/analysis') - self.cf.create_path('/cpacs/vehicles/aircraft/model', 'wings/wing/dynamicAircraftModel') - self.tixi.addTextAttribute('/cpacs/vehicles/aircraft/model/wings/wing', 'uID', 'complete aircraft') - self.tixi.addTextElement('/cpacs/vehicles/aircraft/model/wings/wing', 'description', 'complete aircraft as used in Loads Kernel - without distinction of components' ) - - self.save_cpacs_dynamic_aircraft_model_points() - self.save_cpacs_cut_load_integration_points() - self.save_cpacs_flight_load_cases() - - self.tixi.save(filename) - \ No newline at end of file + data_handling.dump_pickle(crit_monstations, f) + data_handling.dump_hdf5(base_filename + '.hdf5', crit_monstations) diff --git a/loadskernel/build_aero_functions.py b/loadskernel/build_aero_functions.py index 83aad68..03821cb 100644 --- a/loadskernel/build_aero_functions.py +++ b/loadskernel/build_aero_functions.py @@ -1,74 +1,67 @@ # -*- coding: utf-8 -*- -""" -Created on Mon Nov 24 09:07:55 2014 - -@author: voss_ar -""" - -import numpy as np import logging +import numpy as np from matplotlib import pyplot as plt import pandas as pd -import loadskernel.io_functions.read_mona as read_mona -import loadskernel.io_functions.read_bdf as read_bdf -import loadskernel.spline_rules as spline_rules -import loadskernel.spline_functions as spline_functions +from loadskernel.io_functions import read_mona import loadskernel.engine_interfaces.propeller + def build_x2grid(bdf_reader, aerogrid, coord): aesurf = read_mona.add_AESURF(bdf_reader.cards['AESURF']) aelist = read_mona.add_SET1(bdf_reader.cards['AELIST']) # build additional coordinate systems read_mona.add_CORD2R(bdf_reader.cards['CORD2R'], coord) - - x2grid = { 'ID_surf': aesurf['ID'], - 'CID': aesurf['CID'], - 'key': aesurf['key'], - 'eff': aesurf['eff'], - } + + x2grid = {'ID_surf': aesurf['ID'], + 'CID': aesurf['CID'], + 'key': aesurf['key'], + 'eff': aesurf['eff'], + } for i_surf in range(len(aesurf['ID'])): x2grid[i_surf] = {'CD': [], 'CP': [], 'ID': [], 'offset_j': [], 'set_j': []} - for i_panel in aelist['values'][aelist['ID'].index( aesurf['AELIST'][i_surf] )]: - pos_panel = np.where(aerogrid['ID']==i_panel)[0][0] + for i_panel in aelist['values'][aelist['ID'].index(aesurf['AELIST'][i_surf])]: + pos_panel = np.where(aerogrid['ID'] == i_panel)[0][0] x2grid[i_surf]['ID'].append(aerogrid['ID'][pos_panel]) x2grid[i_surf]['CD'].append(aerogrid['CD'][pos_panel]) x2grid[i_surf]['CP'].append(aerogrid['CP'][pos_panel]) x2grid[i_surf]['offset_j'].append(aerogrid['offset_j'][pos_panel]) x2grid[i_surf]['set_j'].append(aerogrid['set_j'][pos_panel]) - - return x2grid, coord + + return x2grid, coord + def build_aerogrid(bdf_reader, filename='', method_caero='CAERO1'): if method_caero == 'CQUAD4': # all corner points are defined as grid points by ModGen caero_grid = read_mona.add_GRIDS(bdf_reader.cards['GRID'].sort_values('ID')) - # four grid points are assembled to one panel, this is expressed as CQUAD4s + # four grid points are assembled to one panel, this is expressed as CQUAD4s caero_panels = read_mona.add_shell_elements(bdf_reader.cards['CQUAD4'].sort_values('ID')) elif method_caero in ['CAERO1', 'CAERO7']: # Adjust the counting of CAERO7 (ZAERO) panels to CAERO1 (Nastran) - bdf_reader.cards['CAERO7']['NSPAN'] -= 1 + bdf_reader.cards['CAERO7']['NSPAN'] -= 1 bdf_reader.cards['CAERO7']['NCHORD'] -= 1 # combine CAERO7 and CAERO1 and use the same function to assemble aerodynamic panels combined_caero = pd.concat([bdf_reader.cards['CAERO1'], bdf_reader.cards['CAERO7']], ignore_index=True) - caero_grid, caero_panels = read_mona.add_panels_from_CAERO(combined_caero.sort_values('ID'), bdf_reader.cards['AEFACT']) + caero_grid, caero_panels = read_mona.add_panels_from_CAERO(combined_caero.sort_values('ID'), + bdf_reader.cards['AEFACT']) elif method_caero in ['VLM4Prop']: caero_grid, caero_panels, cam_rad = loadskernel.engine_interfaces.propeller.read_propeller_input(filename) else: - logging.error( "Error: Method %s not implemented. Available options are 'CQUAD4', 'CAERO1' and 'CAERO7'" % method_caero) - logging.info( ' - from corner points and aero panels, constructing aerogrid') + logging.error("Error: Method %s not implemented. Available options are 'CQUAD4', 'CAERO1' and 'CAERO7'", method_caero) + logging.info(' - from corner points and aero panels, constructing aerogrid') ID = [] - l = [] # length of panel - A = [] # area of one panel - N = [] # unit normal vector - offset_l = [] # 25% point l - offset_k = [] # 50% point k - offset_j = [] # 75% downwash control point j - offset_P1 = [] # Vortex point at 25% chord, 0% span - offset_P3 = [] # Vortex point at 25% chord, 100% span - r = [] # vector P1 to P3, span of panel - - + l = [] # length of panel + A = [] # area of one panel + N = [] # unit normal vector + offset_l = [] # 25% point l + offset_k = [] # 50% point k + offset_j = [] # 75% downwash control point j + offset_P1 = [] # Vortex point at 25% chord, 0% span + offset_P3 = [] # Vortex point at 25% chord, 100% span + r = [] # vector P1 to P3, span of panel + for i_panel in range(len(caero_panels['ID'])): # @@ -82,34 +75,34 @@ def build_aerogrid(bdf_reader, filename='', method_caero='CAERO1'): # | # z.--- x - index_1 = np.where(caero_panels['cornerpoints'][i_panel][0]==caero_grid['ID'])[0][0] - index_2 = np.where(caero_panels['cornerpoints'][i_panel][1]==caero_grid['ID'])[0][0] - index_3 = np.where(caero_panels['cornerpoints'][i_panel][2]==caero_grid['ID'])[0][0] - index_4 = np.where(caero_panels['cornerpoints'][i_panel][3]==caero_grid['ID'])[0][0] - + index_1 = np.where(caero_panels['cornerpoints'][i_panel][0] == caero_grid['ID'])[0][0] + index_2 = np.where(caero_panels['cornerpoints'][i_panel][1] == caero_grid['ID'])[0][0] + index_3 = np.where(caero_panels['cornerpoints'][i_panel][2] == caero_grid['ID'])[0][0] + index_4 = np.where(caero_panels['cornerpoints'][i_panel][3] == caero_grid['ID'])[0][0] + l_1 = caero_grid['offset'][index_2] - caero_grid['offset'][index_1] l_2 = caero_grid['offset'][index_3] - caero_grid['offset'][index_4] b_1 = caero_grid['offset'][index_4] - caero_grid['offset'][index_1] b_2 = caero_grid['offset'][index_3] - caero_grid['offset'][index_2] l_m = (l_1 + l_2) / 2.0 b_m = (b_1 + b_2) / 2.0 - - ID.append(caero_panels['ID'][i_panel]) + + ID.append(caero_panels['ID'][i_panel]) l.append(l_m[0]) # A.append(l_m[0]*b_m[1]) A.append(np.linalg.norm(np.cross(l_m, b_m))) - N.append(np.cross(l_1, b_1)/np.linalg.norm(np.cross(l_1, b_1))) - offset_l.append(caero_grid['offset'][index_1] + 0.25*l_m + 0.50*b_1) - offset_k.append(caero_grid['offset'][index_1] + 0.50*l_m + 0.50*b_1) - offset_j.append(caero_grid['offset'][index_1] + 0.75*l_m + 0.50*b_1) - offset_P1.append(caero_grid['offset'][index_1] + 0.25*l_1) - offset_P3.append(caero_grid['offset'][index_4] + 0.25*l_2) - r.append((caero_grid['offset'][index_4] + 0.25*l_2) - (caero_grid['offset'][index_1] + 0.25*l_1)) - + N.append(np.cross(l_1, b_1) / np.linalg.norm(np.cross(l_1, b_1))) + offset_l.append(caero_grid['offset'][index_1] + 0.25 * l_m + 0.50 * b_1) + offset_k.append(caero_grid['offset'][index_1] + 0.50 * l_m + 0.50 * b_1) + offset_j.append(caero_grid['offset'][index_1] + 0.75 * l_m + 0.50 * b_1) + offset_P1.append(caero_grid['offset'][index_1] + 0.25 * l_1) + offset_P3.append(caero_grid['offset'][index_4] + 0.25 * l_2) + r.append((caero_grid['offset'][index_4] + 0.25 * l_2) - (caero_grid['offset'][index_1] + 0.25 * l_1)) + n = len(ID) - set_l = np.arange(n*6).reshape((n,6)) - set_k = np.arange(n*6).reshape((n,6)) - set_j = np.arange(n*6).reshape((n,6)) + set_l = np.arange(n * 6).reshape((n, 6)) + set_k = np.arange(n * 6).reshape((n, 6)) + set_j = np.arange(n * 6).reshape((n, 6)) aerogrid = {'ID': np.array(ID), 'l': np.array(l), 'A': np.array(A), @@ -128,105 +121,112 @@ def build_aerogrid(bdf_reader, filename='', method_caero='CAERO1'): 'n': n, 'coord_desc': 'bodyfixed', 'cornerpoint_panels': caero_panels['cornerpoints'], - 'cornerpoint_grids': np.hstack((caero_grid['ID'][:,None],caero_grid['offset'])) - } + 'cornerpoint_grids': np.hstack((caero_grid['ID'][:, None], caero_grid['offset'])) + } if method_caero in ['VLM4Prop']: aerogrid['cam_rad'] = cam_rad return aerogrid + def build_macgrid(aerogrid, b_ref): # Assumptions: # - the meam aerodynamic center is the 25% point on the mean aerodynamic choord # - the mean aerodynamic choord runs through the (geometrical) center of all panels A = np.sum(aerogrid['A']) - mean_aero_choord = A/b_ref - geo_center_x = np.dot(aerogrid['offset_k'][:,0], aerogrid['A'])/A - geo_center_y = np.dot(aerogrid['offset_k'][:,1], aerogrid['A'])/A - geo_center_z = np.dot(aerogrid['offset_k'][:,2], aerogrid['A'])/A + mean_aero_choord = A / b_ref + geo_center_x = np.dot(aerogrid['offset_k'][:, 0], aerogrid['A']) / A + geo_center_y = np.dot(aerogrid['offset_k'][:, 1], aerogrid['A']) / A + geo_center_z = np.dot(aerogrid['offset_k'][:, 2], aerogrid['A']) / A macgrid = {'ID': np.array([0]), - 'offset': np.array([[geo_center_x-0.25*mean_aero_choord, geo_center_y, geo_center_z]]), - "set":np.array([[0, 1, 2, 3, 4, 5]]), + 'offset': np.array([[geo_center_x - 0.25 * mean_aero_choord, geo_center_y, geo_center_z]]), + "set": np.array([[0, 1, 2, 3, 4, 5]]), 'CD': np.array([0]), 'CP': np.array([0]), 'coord_desc': 'bodyfixed', 'A_ref': A, 'b_ref': b_ref, 'c_ref': mean_aero_choord, - } + } return macgrid + def rfa(Qjj, k, n_poles=2, filename='rfa.png'): # B = A*x # B ist die gegebene AIC, A die Roger-Approximation, x sind die zu findenden Koeffizienten B0,B1,...B7 - logging.info( 'Performing rational function approximation (RFA) on AIC matrices with {} poles...'.format(n_poles)) + logging.info('Performing rational function approximation (RFA) on AIC matrices with {} poles...'.format(n_poles)) k = np.array(k) n_k = len(k) - ik = k * 1j - betas = np.max(k)/np.arange(1,n_poles+1) # Roger + betas = np.max(k) / np.arange(1, n_poles + 1) # Roger # Alternative proposed by Karpel / ZAERO would be: betas = 1.7*np.max(k)*(np.arange(1,n_poles+1)/(n_poles+1.0))**2.0 option = 2 - if option == 1: # voll - Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), -k**2]) - Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) - elif option == 2: # ohne Beschleunigungsterm - Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) - Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) - elif option == 3: # ohne Beschleunigungsterm und Daempfungsterm - Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) - Ajj_imag = np.array([np.zeros(n_k), np.zeros(n_k), np.zeros(n_k)]) - - for beta in betas: Ajj_real = np.vstack(( Ajj_real, k**2/(k**2+beta**2) )) - for beta in betas: Ajj_imag = np.vstack(( Ajj_imag, k*beta/(k**2+beta**2) )) + if option == 1: # voll + Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), -k ** 2]) + Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) + elif option == 2: # ohne Beschleunigungsterm + Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) + Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) + elif option == 3: # ohne Beschleunigungsterm und Daempfungsterm + Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) + Ajj_imag = np.array([np.zeros(n_k), np.zeros(n_k), np.zeros(n_k)]) + + for beta in betas: + Ajj_real = np.vstack((Ajj_real, k ** 2 / (k ** 2 + beta ** 2))) + Ajj_imag = np.vstack((Ajj_imag, k * beta / (k ** 2 + beta ** 2))) Ajj = np.vstack((Ajj_real.T, Ajj_imag.T)) - - # komplette AIC-Matrix: hierzu wird die AIC mit nj*nj umgeformt in einen Vektor nj**2 - Qjj_reshaped = np.vstack(( np.real(Qjj.reshape(n_k,-1)), np.imag(Qjj.reshape(n_k,-1)) )) + + # komplette AIC-Matrix: hierzu wird die AIC mit nj*nj umgeformt in einen Vektor nj**2 + Qjj_reshaped = np.vstack((np.real(Qjj.reshape(n_k, -1)), np.imag(Qjj.reshape(n_k, -1)))) n_j = Qjj.shape[1] - - logging.info( '- solving B = A*x with least-squares method') - solution, residuals, rank, s = np.linalg.lstsq(Ajj, Qjj_reshaped, rcond=-1) + + logging.info('- solving B = A*x with least-squares method') + solution, _, _, _ = np.linalg.lstsq(Ajj, Qjj_reshaped, rcond=-1) ABCD = solution.reshape(3 + n_poles, n_j, n_j) - + # Kontrolle Qjj_aprox = np.dot(Ajj, solution) RMSE = [] - logging.info( '- root-mean-square error(s): ') + logging.info('- root-mean-square error(s): ') for k_i in range(n_k): - RMSE_real = np.sqrt( ((Qjj_aprox[k_i ,:].reshape(n_j, n_j) - np.real(Qjj[k_i,:,:]))**2).sum(axis=None) / n_j**2 ) - RMSE_imag = np.sqrt( ((Qjj_aprox[k_i+n_k,:].reshape(n_j, n_j) - np.imag(Qjj[k_i,:,:]))**2).sum(axis=None) / n_j**2 ) - RMSE.append([RMSE_real,RMSE_imag ]) - logging.info( ' k = {:<6}, RMSE_real = {:<20}, RMSE_imag = {:<20}'.format(k[k_i], RMSE_real, RMSE_imag)) + RMSE_real = np.sqrt(((Qjj_aprox[k_i, :].reshape(n_j, n_j) + - np.real(Qjj[k_i, :, :])) ** 2).sum(axis=None) / n_j ** 2) + RMSE_imag = np.sqrt(((Qjj_aprox[k_i + n_k, :].reshape(n_j, n_j) + - np.imag(Qjj[k_i, :, :])) ** 2).sum(axis=None) / n_j ** 2) + RMSE.append([RMSE_real, RMSE_imag]) + logging.info(' k = {:<6}, RMSE_real = {:<20}, RMSE_imag = {:<20}'.format(k[k_i], RMSE_real, RMSE_imag)) # Vergroesserung des Frequenzbereichs - k = np.arange(0.0,(k.max()), 0.001) + k = np.arange(0.0, (k.max()), 0.001) n_k = len(k) - - if option == 1: # voll - Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), -k**2]) - Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) - elif option == 2: # ohne Beschleunigungsterm - Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) - Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) - elif option == 3: # ohne Beschleunigungsterm und D��mpfungsterm - Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) - Ajj_imag = np.array([np.zeros(n_k), np.zeros(n_k), np.zeros(n_k)]) - - for beta in betas: Ajj_real = np.vstack(( Ajj_real, k**2/(k**2+beta**2) )) - for beta in betas: Ajj_imag = np.vstack(( Ajj_imag, k*beta/(k**2+beta**2) )) + + if option == 1: # voll + Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), -k ** 2]) + Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) + elif option == 2: # ohne Beschleunigungsterm + Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) + Ajj_imag = np.array([np.zeros(n_k), k, np.zeros(n_k)]) + elif option == 3: # ohne Beschleunigungsterm und Dämpfungsterm + Ajj_real = np.array([np.ones(n_k), np.zeros(n_k), np.zeros(n_k)]) + Ajj_imag = np.array([np.zeros(n_k), np.zeros(n_k), np.zeros(n_k)]) + + for beta in betas: + Ajj_real = np.vstack((Ajj_real, k ** 2 / (k ** 2 + beta ** 2))) + for beta in betas: + Ajj_imag = np.vstack((Ajj_imag, k * beta / (k ** 2 + beta ** 2))) # Plots vom Real- und Imaginaerteil der ersten m_n*n_n Panels first_panel = 0 m_n = 3 n_n = 3 plt.figure() for m_i in range(m_n): - for n_i in range(n_n): - qjj = Qjj[:,first_panel+n_i,first_panel+m_i] - qjj_aprox = np.dot(Ajj_real.T, ABCD[:,first_panel+n_i,first_panel+m_i]) + np.dot(Ajj_imag.T, ABCD[:,first_panel+n_i,first_panel+m_i])*1j - plt.subplot(m_n, n_n, m_n*m_i+n_i+1) + for n_i in range(n_n): + qjj = Qjj[:, first_panel + n_i, first_panel + m_i] + qjj_aprox = np.dot(Ajj_real.T, ABCD[:, first_panel + n_i, first_panel + m_i]) \ + + np.dot(Ajj_imag.T, ABCD[:, first_panel + n_i, first_panel + m_i]) * 1j + plt.subplot(m_n, n_n, m_n * m_i + n_i + 1) plt.plot(np.real(qjj), np.imag(qjj), 'b.-') plt.plot(np.real(qjj_aprox), np.imag(qjj_aprox), 'r-') plt.xlabel('real') plt.ylabel('imag') - + plt.savefig(filename) plt.close() return ABCD, n_poles, betas, np.array(RMSE) diff --git a/loadskernel/build_splinegrid.py b/loadskernel/build_splinegrid.py index cf2f258..4485e69 100644 --- a/loadskernel/build_splinegrid.py +++ b/loadskernel/build_splinegrid.py @@ -1,26 +1,22 @@ # -*- coding: utf-8 -*- -""" -Created on Fri May 8 18:11:26 2015 - -@author: voss_ar -""" - import numpy as np -import loadskernel.io_functions.read_mona as read_mona +from loadskernel.io_functions import read_mona + def build_splinegrid(strcgrid, filenames): - #subgrid = read_mona.Modgen_GRID(filename) - for i_file in range(len(filenames)): - subgrid = read_mona.Modgen_GRID(filenames[i_file]) + # subgrid = read_mona.Modgen_GRID(filename) + for i_file, filename in enumerate(filenames): + subgrid = read_mona.Modgen_GRID(filename) if i_file == 0: subgrid_IDs = subgrid['ID'] else: - subgrid_IDs = np.hstack((subgrid_IDs,subgrid['ID'])) + subgrid_IDs = np.hstack((subgrid_IDs, subgrid['ID'])) return build_subgrid(strcgrid, subgrid_IDs) + def build_subgrid(strcgrid, subgrid_IDs): - splinegrid = {'ID': [], 'CD': [], 'CP': [], 'set': [], 'offset': [], 'n': 0, } + splinegrid = {'ID': [], 'CD': [], 'CP': [], 'set': [], 'offset': [], 'n': 0, } for i_ID in subgrid_IDs: pos = np.where(i_ID == strcgrid['ID'])[0][0] splinegrid['ID'].append(strcgrid['ID'][pos]) @@ -29,42 +25,44 @@ def build_subgrid(strcgrid, subgrid_IDs): splinegrid['n'] += 1 splinegrid['set'].append(strcgrid['set'][pos]) splinegrid['offset'].append(strcgrid['offset'][pos]) - + splinegrid['ID'] = np.array(splinegrid['ID']) splinegrid['CD'] = np.array(splinegrid['CD']) splinegrid['CP'] = np.array(splinegrid['CP']) splinegrid['set'] = np.array(splinegrid['set']) - splinegrid['offset'] = np.array(splinegrid['offset']) + splinegrid['offset'] = np.array(splinegrid['offset']) return splinegrid - + + def grid_thin_out_random(grid, thin_out_factor): randomnumbers = np.random.rand(grid['n']) pos = np.where(randomnumbers < thin_out_factor)[0] - grid_thin= {'ID': grid['ID'][pos], - 'CD': grid['CD'][pos], - 'CP': grid['CP'][pos], - 'set': grid['set'][pos], - 'offset': grid['offset'][pos], - 'n': len(pos), - } + grid_thin = {'ID': grid['ID'][pos], + 'CD': grid['CD'][pos], + 'CP': grid['CP'][pos], + 'set': grid['set'][pos], + 'offset': grid['offset'][pos], + 'n': len(pos), + } return grid_thin - + + def grid_thin_out_radius(grid, radius): pos = list(range(grid['n'])) i = 0 while i < len(pos): - dist = np.sum((grid['offset'][pos] - grid['offset'][pos[i]])**2, axis=1)**0.5 + dist = np.sum((grid['offset'][pos] - grid['offset'] + [pos[i]]) ** 2, axis=1) ** 0.5 dist[i] += radius * 1.1 - pos = np.delete( pos, np.where(dist <= radius)[0] ) + pos = np.delete(pos, np.where(dist <= radius)[0]) i += 1 - - grid_thin= {'ID': grid['ID'][pos], - 'CD': grid['CD'][pos], - 'CP': grid['CP'][pos], - 'set': grid['set'][pos], - 'offset': grid['offset'][pos], - 'n': len(pos), - } - return grid_thin - + + grid_thin = {'ID': grid['ID'][pos], + 'CD': grid['CD'][pos], + 'CP': grid['CP'][pos], + 'set': grid['set'][pos], + 'offset': grid['offset'][pos], + 'n': len(pos), + } + return grid_thin diff --git a/loadskernel/cfd_interfaces/meshdefo.py b/loadskernel/cfd_interfaces/meshdefo.py index a1f794a..12e5fd8 100644 --- a/loadskernel/cfd_interfaces/meshdefo.py +++ b/loadskernel/cfd_interfaces/meshdefo.py @@ -1,46 +1,48 @@ +import logging import numpy as np -import logging -import loadskernel.build_splinegrid as build_splinegrid +from loadskernel import build_splinegrid -class Meshdefo(object): + +class Meshdefo(): """ This is a base class for all CFD interfaces that provides some mesh deformation functionalities. """ - + def Ux2(self, Ux2): logging.info('Apply control surface deflections to cfdgrid.') - Ujx2 = np.zeros(self.aerogrid['n']*6) + Ujx2 = np.zeros(self.aerogrid['n'] * 6) if 'hingeline' in self.jcl.aero and self.jcl.aero['hingeline'] == 'y': hingeline = 'y' elif 'hingeline' in self.jcl.aero and self.jcl.aero['hingeline'] == 'z': hingeline = 'z' - else: # default + else: # default hingeline = 'y' for i_x2 in range(len(self.x2grid['key'])): - logging.debug('Apply deflection of {} for {:0.4f} [deg].'.format(self.x2grid['key'][i_x2], Ux2[i_x2]/np.pi*180.0)) + logging.debug('Apply deflection of {} for {:0.4f} [deg].'.format( + self.x2grid['key'][i_x2], Ux2[i_x2] / np.pi * 180.0)) if hingeline == 'y': - Ujx2 += np.dot(self.Djx2[i_x2],[0,0,0,0,Ux2[i_x2],0]) + Ujx2 += np.dot(self.Djx2[i_x2], [0, 0, 0, 0, Ux2[i_x2], 0]) elif hingeline == 'z': - Ujx2 += np.dot(self.Djx2[i_x2],[0,0,0,0,0,Ux2[i_x2]]) + Ujx2 += np.dot(self.Djx2[i_x2], [0, 0, 0, 0, 0, Ux2[i_x2]]) self.transfer_deformations(self.aerogrid, Ujx2, '_k', rbf_type='wendland2', surface_spline=False, support_radius=1.5) - + def Uf(self, Uf): if 'flex' in self.jcl.aero and self.jcl.aero['flex']: logging.info('Apply flexible deformations to cfdgrid.') # set-up spline grid - if self.jcl.spline['splinegrid'] == True: - # make sure that there are no double points in the spline grid as this would cause a singularity of the spline matrix. + if self.jcl.spline['splinegrid']: + # make sure that there are no double points in the spline grid as this would cause a singularity of the + # spline matrix. splinegrid = build_splinegrid.grid_thin_out_radius(self.splinegrid, 0.01) else: - #splinegrid = build_splinegrid.grid_thin_out_random(model.strcgrid, 0.5) + # splinegrid = build_splinegrid.grid_thin_out_random(model.strcgrid, 0.5) splinegrid = build_splinegrid.grid_thin_out_radius(self.strcgrid, 0.4) - + # get structural deformation - PHIf_strc = self.mass['PHIf_strc'] + PHIf_strc = self.mass['PHIf_strc'] Ug_f_body = np.dot(PHIf_strc.T, Uf.T).T - + self.transfer_deformations(splinegrid, Ug_f_body, '', rbf_type='tps', surface_spline=False) else: logging.info('Apply NO flexible deformations to cfdgrid.') - \ No newline at end of file diff --git a/loadskernel/cfd_interfaces/mpi_helper.py b/loadskernel/cfd_interfaces/mpi_helper.py index 9051775..f20aa1d 100644 --- a/loadskernel/cfd_interfaces/mpi_helper.py +++ b/loadskernel/cfd_interfaces/mpi_helper.py @@ -1,9 +1,13 @@ -import platform, sys, os, time +import platform +import sys +import os +import time try: from mpi4py import MPI -except: +except ImportError: pass - + + def setup_mpi(debug=False): if 'mpi4py.MPI' in sys.modules: have_mpi = True @@ -24,4 +28,4 @@ def setup_mpi(debug=False): comm = None status = None myid = 0 - return have_mpi, comm, status, myid \ No newline at end of file + return have_mpi, comm, status, myid diff --git a/loadskernel/cfd_interfaces/su2_interface.py b/loadskernel/cfd_interfaces/su2_interface.py index 8758c9d..f88d570 100644 --- a/loadskernel/cfd_interfaces/su2_interface.py +++ b/loadskernel/cfd_interfaces/su2_interface.py @@ -1,56 +1,61 @@ - +import copy +import logging +import os +import sys +import time import numpy as np -import logging, os, sys, time, copy -from loadskernel.cfd_interfaces import meshdefo from loadskernel import spline_functions +from loadskernel.cfd_interfaces import meshdefo from loadskernel.cfd_interfaces.mpi_helper import setup_mpi from loadskernel.cfd_interfaces.tau_interface import check_para_path, copy_para_file, check_cfd_folders from loadskernel.grid_trafo import grid_trafo, vector_trafo -from loadskernel.solution_tools import calc_drehmatrix from loadskernel.io_functions.data_handling import load_hdf5_dict +from loadskernel.solution_tools import calc_drehmatrix try: - import SU2, pysu2 -except: + import SU2 + import pysu2 +except ImportError: pass """ There are two ways in which the free stream onflow can be realized: a) using mesh velocities imposed on each grid point (called motion module in Tau) b) using the farfield (the classical way with alpha and beta) -The grid velocity approach (parameter GRID_MOVEMENT= ROTATING_FRAME) allows the simulation of the free-flying aircraft -including roll, pitch and yaw rates by imposing a corresponding velocity field in addition to the flight velocities u, -v and w. The farfield onflow is set to zero in this scenario. As a disadvantage, the grid velocity approach is not -implemented for unsteady simulations because additional source terms become necessary (as discussed with the SU2 developers) +The grid velocity approach (parameter GRID_MOVEMENT= ROTATING_FRAME) allows the simulation of the free-flying aircraft +including roll, pitch and yaw rates by imposing a corresponding velocity field in addition to the flight velocities u, +v and w. The farfield onflow is set to zero in this scenario. As a disadvantage, the grid velocity approach is not +implemented for unsteady simulations because additional source terms become necessary (as discussed with the SU2 developers) and there is an interference with the mesh velocities from the elastic deformations, which make an inplementation tricky, too trick for me. -The farfield onflow is more straight forward and seems to works properly for unsteady simulations. The rigid body motion -can be handled by moving + rotating the whole aircraft in the elastic mesh, which is acceptable for moderate displacements -expected during e.g. a gust encounter. This approach involves more work on the Loads Kernel side, as the surface, the +The farfield onflow is more straight forward and seems to works properly for unsteady simulations. The rigid body motion +can be handled by moving + rotating the whole aircraft in the elastic mesh, which is acceptable for moderate displacements +expected during e.g. a gust encounter. This approach involves more work on the Loads Kernel side, as the surface, the surface deformations and the aerodynamic forces need to be translated back and forth. """ + class SU2InterfaceGridVelocity(meshdefo.Meshdefo): - + def __init__(self, solution): - self.model = solution.model - self.jcl = solution.jcl - self.trimcase = solution.trimcase - self.simcase = solution.simcase + self.model = solution.model + self.jcl = solution.jcl + self.trimcase = solution.trimcase + self.simcase = solution.simcase # load data from HDF5 - self.mass = load_hdf5_dict(self.model['mass'][self.trimcase['mass']]) - self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) - self.cggrid = load_hdf5_dict(self.mass['cggrid']) - self.cfdgrid = load_hdf5_dict(self.model['cfdgrid']) - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + self.mass = load_hdf5_dict(self.model['mass'][self.trimcase['mass']]) + self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) + self.cggrid = load_hdf5_dict(self.mass['cggrid']) + self.cfdgrid = load_hdf5_dict(self.model['cfdgrid']) + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) self.splinegrid = load_hdf5_dict(self.model['splinegrid']) - self.aerogrid = load_hdf5_dict(self.model['aerogrid']) - self.x2grid = load_hdf5_dict(self.model['x2grid']) - self.coord = load_hdf5_dict(self.model['coord']) - - self.Djx2 = self.model['Djx2'][()] - + self.aerogrid = load_hdf5_dict(self.model['aerogrid']) + self.x2grid = load_hdf5_dict(self.model['x2grid']) + self.coord = load_hdf5_dict(self.model['coord']) + + self.Djx2 = self.model['Djx2'][()] + # set switch for first execution self.first_execution = True # stepwidth for time domain simulation @@ -60,32 +65,33 @@ def __init__(self, solution): self.stepwidth = self.simcase['dt'] else: self.stepwidth = None - + # Initialize and check if MPI can be used, SU2 requires MPICH self.have_mpi, self.comm, self.status, self.myid = setup_mpi(debug=False) if not self.have_mpi: logging.error('No MPI detected (SU2 requires MPI)!') - + # Check if pysu2 was imported successfully, see try/except statement in the import section. if "pysu2" in sys.modules and "SU2" in sys.modules: # make sure that all processes are at the same stage self.comm.barrier() logging.info('Init CFD interface of type "{}" on MPI process {}.'.format(self.__class__.__name__, self.myid)) else: - logging.error('pysu2 was/could NOT be imported! Model equations of type "{}" will NOT work.'.format(self.jcl.aero['method'])) + logging.error('pysu2 was/could NOT be imported! Model equations of type "{}" will NOT work.'.format( + self.jcl.aero['method'])) self.FluidSolver = None - + # Set-up file system structure if self.myid == 0: check_cfd_folders(self.jcl) check_para_path(self.jcl) copy_para_file(self.jcl, self.trimcase) - self.para_filename = self.jcl.aero['para_path']+'para_subcase_{}'.format(self.trimcase['subcase']) - + self.para_filename = self.jcl.aero['para_path'] + 'para_subcase_{}'.format(self.trimcase['subcase']) + # Storage for the euler transofmation of the unsteady interface self.XYZ = None self.PhiThetaPsi = None - + def prepare_meshdefo(self, Uf, Ux2): """ In this function, we run all the steps to necessary perform the mesh deformation. @@ -93,7 +99,7 @@ def prepare_meshdefo(self, Uf, Ux2): # There may be CFD partitions which have no deformation markers. In that case, there is nothing to do. if self.local_mesh['n'] > 0: # Initialize the surface deformation vector with zeros - self.Ucfd = np.zeros(self.local_mesh['n']*6) + self.Ucfd = np.zeros(self.local_mesh['n'] * 6) # These two functions are inherited from the original Meshdefo class # Add flexible deformations self.Uf(Uf) @@ -101,7 +107,7 @@ def prepare_meshdefo(self, Uf, Ux2): self.Ux2(Ux2) # Communicate the deformation of the local mesh to the CFD solver self.set_deformations() - + def set_deformations(self): """ Communicate the change of coordinates of the fluid interface to the fluid solver. @@ -109,9 +115,9 @@ def set_deformations(self): """ logging.info('Sending surface deformations to SU2.') for x in range(self.local_mesh['n']): - self.FluidSolver.SetMarkerCustomDisplacement(self.local_mesh['MarkerID'][x], - self.local_mesh['VertexIndex'][x], - self.Ucfd[self.local_mesh['set'][x,:3]]) + self.FluidSolver.SetMarkerCustomDisplacement(self.local_mesh['MarkerID'][x], + self.local_mesh['VertexIndex'][x], + self.Ucfd[self.local_mesh['set'][x, :3]]) def set_grid_velocities(self, uvwpqr): """ @@ -125,7 +131,7 @@ def set_grid_velocities(self, uvwpqr): logging.info('Sending translational and rotational velocities to SU2.') self.FluidSolver.SetTranslationRate(xDot=u, yDot=v, zDot=w) self.FluidSolver.SetRotationRate(rot_x=p, rot_y=q, rot_z=r) - + def update_general_para(self): """ In this section, the parameter file is updated. So far, I haven't found a way to do this via pysu2 for all parameters. @@ -134,44 +140,49 @@ def update_general_para(self): if self.first_execution and self.myid == 0: # read all existing parameters config = SU2.io.Config(self.para_filename) - # set general parameters, which don't change over the course of the CFD simulation, so they are only updated + # set general parameters, which don't change over the course of the CFD simulation, so they are only updated # for the first execution config['MESH_FILENAME'] = self.jcl.meshdefo['surface']['filename_grid'] - config['RESTART_FILENAME'] = self.jcl.aero['para_path']+'sol/restart_subcase_{}.dat'.format(self.trimcase['subcase']) - config['SOLUTION_FILENAME'] = self.jcl.aero['para_path']+'sol/restart_subcase_{}.dat'.format(self.trimcase['subcase']) - config['SURFACE_FILENAME'] = self.jcl.aero['para_path']+'sol/surface_subcase_{}'.format(self.trimcase['subcase']) - config['VOLUME_FILENAME'] = self.jcl.aero['para_path']+'sol/volume_subcase_{}'.format(self.trimcase['subcase']) - config['CONV_FILENAME'] = self.jcl.aero['para_path']+'sol/history_subcase_{}'.format(self.trimcase['subcase']) + config['RESTART_FILENAME'] = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}.dat'.format( + self.trimcase['subcase']) + config['SOLUTION_FILENAME'] = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}.dat'.format( + self.trimcase['subcase']) + config['SURFACE_FILENAME'] = self.jcl.aero['para_path'] + 'sol/surface_subcase_{}'.format( + self.trimcase['subcase']) + config['VOLUME_FILENAME'] = self.jcl.aero['para_path'] + 'sol/volume_subcase_{}'.format( + self.trimcase['subcase']) + config['CONV_FILENAME'] = self.jcl.aero['para_path'] + 'sol/history_subcase_{}'.format( + self.trimcase['subcase']) # free stream definition config['FREESTREAM_TEMPERATURE'] = self.atmo['T'] - config['FREESTREAM_DENSITY'] = self.atmo['rho'] - config['FREESTREAM_PRESSURE'] = self.atmo['p'] + config['FREESTREAM_DENSITY'] = self.atmo['rho'] + config['FREESTREAM_PRESSURE'] = self.atmo['p'] # update the pressure inlet if hasattr(self.jcl, 'pressure_inlet'): - config['MARKER_INLET'] = '( '+', '.join([self.jcl.pressure_inlet['marker'], - str(self.atmo['T']), - str(self.atmo['p']), - str(self.jcl.pressure_inlet['flow_direction'][0]), - str(self.jcl.pressure_inlet['flow_direction'][1]), - str(self.jcl.pressure_inlet['flow_direction'][2]),]) + ' )' + config['MARKER_INLET'] = '( ' + ', '.join([self.jcl.pressure_inlet['marker'], + str(self.atmo['T']), + str(self.atmo['p']), + str(self.jcl.pressure_inlet['flow_direction'][0]), + str(self.jcl.pressure_inlet['flow_direction'][1]), + str(self.jcl.pressure_inlet['flow_direction'][2]), ]) + ' )' # make sure that the free stream onflow is zero config['MACH_NUMBER'] = 0.0 # activate grid deformation config['DEFORM_MESH'] = 'YES' - config['MARKER_DEFORM_MESH'] = '( '+', '.join(self.jcl.meshdefo['surface']['markers'])+' )' + config['MARKER_DEFORM_MESH'] = '( ' + ', '.join(self.jcl.meshdefo['surface']['markers']) + ' )' # activate grid movement config['GRID_MOVEMENT'] = 'ROTATING_FRAME' - config['MOTION_ORIGIN'] = '{} {} {}'.format(self.cggrid['offset'][0,0], - self.cggrid['offset'][0,1], - self.cggrid['offset'][0,2]) + config['MOTION_ORIGIN'] = '{} {} {}'.format(self.cggrid['offset'][0, 0], + self.cggrid['offset'][0, 1], + self.cggrid['offset'][0, 2]) config['MACH_MOTION'] = self.trimcase['Ma'] # there is no restart for the first execution config['RESTART_SOL'] = 'NO' # detrimine cfd solution output - config['OUTPUT_FILES']= ['RESTART', 'RESTART_ASCII', 'TECPLOT', 'SURFACE_TECPLOT'] + config['OUTPUT_FILES'] = ['RESTART', 'RESTART_ASCII', 'TECPLOT', 'SURFACE_TECPLOT'] # set intemediate outputs # config['OUTPUT_WRT_FREQ']= 1 - + # do the update # config.write() maintains the original layout of the file but doesn't add new parameters # config.dump() writes all parameters in a weird order, including default values @@ -186,7 +197,7 @@ def init_solver(self): self.release_memory() self.FluidSolver = pysu2.CSinglezoneDriver(self.para_filename, 1, self.comm) self.get_local_mesh() - + def run_solver(self, i_timestep=0): logging.info('Waiting until all processes are ready to perform a coordinated start...') self.comm.barrier() @@ -194,54 +205,54 @@ def run_solver(self, i_timestep=0): # start timer t_start = time.time() # initialize SU2 if this is the first run. - if self.first_execution == True: + if self.first_execution: self.prepare_initial_solution() # run solver - #self.FluidSolver.StartSolver() + # self.FluidSolver.StartSolver() self.FluidSolver.Preprocess(i_timestep) self.FluidSolver.Run() self.FluidSolver.Postprocess() self.FluidSolver.Update() # write outputs and restart file(s) - self.FluidSolver.Monitor(i_timestep) + self.FluidSolver.Monitor(i_timestep) self.FluidSolver.Output(i_timestep) self.comm.barrier() - + logging.debug('CFD computation performed in {:.2f} seconds.'.format(time.time() - t_start)) - + def get_last_solution(self): return self.Pcfd_global() - + def Pcfd_global(self): logging.debug('Start recovery of nodal loads from SU2') t_start = time.time() - Pcfd_send = np.zeros(self.cfdgrid['n']*6) - Pcfd_rcv = np.zeros(( self.comm.Get_size() , self.cfdgrid['n']*6)) + Pcfd_send = np.zeros(self.cfdgrid['n'] * 6) + Pcfd_rcv = np.zeros((self.comm.Get_size(), self.cfdgrid['n'] * 6)) for x in range(self.local_mesh['n']): fxyz = self.FluidSolver.GetMarkerFlowLoad(self.local_mesh['MarkerID'][x], self.local_mesh['VertexIndex'][x]) - Pcfd_send[self.local_mesh['set_global'][x,:3]] += fxyz + Pcfd_send[self.local_mesh['set_global'][x, :3]] += fxyz self.comm.barrier() self.comm.Allgatherv(Pcfd_send, Pcfd_rcv) Pcfd = Pcfd_rcv.sum(axis=0) logging.debug('All nodal loads recovered, sorted and gathered in {:.2f} sec.'.format(time.time() - t_start)) return Pcfd - + def prepare_initial_solution(self): """ - So far, starting SU2 with different, more robust parameters was not necessary. + So far, starting SU2 with different, more robust parameters was not necessary. """ # Change the first_execution flag self.first_execution = False - + def release_memory(self): # In case SU2 is re-initialized, release the memory taken by the old instance. - if self.FluidSolver != None: + if self.FluidSolver is not None: self.FluidSolver.Finalize() - + def get_local_mesh(self): """ Get the local mesh (the partition of this mpi process) of the fluid solver. - Because the loops over all grid points and the mapping of the local to the gloabl mesh is + Because the loops over all grid points and the mapping of the local to the gloabl mesh is a little time consuming, the information is stored in the following entries: GlobalIndex - the ID of the grid point as defined in the global CFD mesh MarkerID - the ID of the marker the grid point belongs to @@ -255,10 +266,13 @@ def get_local_mesh(self): # Thus, it is necessary to figure out if the partition of the current mpi process has # a node that belongs to a moving surface marker. has_moving_marker = [marker in solver_marker_ids.keys() for marker in solver_all_moving_markers] - + # Set-up some helper variables - tmp_offset = []; tmp_set_global = [] - tmp_global_id = []; tmp_marker_id = []; tmp_vertex_id = [] + tmp_offset = [] + tmp_set_global = [] + tmp_global_id = [] + tmp_marker_id = [] + tmp_vertex_id = [] n = 0 # Loops to get the coordinates of every vertex that belongs the partition of this mpi process for marker in solver_all_moving_markers[has_moving_marker]: @@ -272,17 +286,17 @@ def get_local_mesh(self): tmp_marker_id.append(solver_marker_id) tmp_vertex_id.append(i_vertex) tmp_offset.append(self.FluidSolver.InitialCoordinates().Get(i_point)) - tmp_set_global.append( self.cfdgrid['set'][np.where(GlobalIndex == self.cfdgrid['ID'])[0],:] ) + tmp_set_global.append(self.cfdgrid['set'][np.where(GlobalIndex == self.cfdgrid['ID'])[0], :]) # Store the local mesh, use a pattern similar to a any other grids self.local_mesh = {'GlobalIndex': tmp_global_id, 'MarkerID': tmp_marker_id, 'VertexIndex': tmp_vertex_id, - 'CP': np.repeat(0,n), - 'CD': np.repeat(0,n), + 'CP': np.repeat(0, n), + 'CD': np.repeat(0, n), 'offset': np.array(tmp_offset), - 'set': np.arange(n*6).reshape((n,6)), - 'set_global':np.array(tmp_set_global).squeeze(), + 'set': np.arange(n * 6).reshape((n, 6)), + 'set_global': np.array(tmp_set_global).squeeze(), 'n': n } logging.debug('This is process {} and my local mesh has a size of {}'.format(self.myid, self.local_mesh['n'])) @@ -290,48 +304,50 @@ def get_local_mesh(self): def transfer_deformations(self, grid_i, U_i, set_i, rbf_type, surface_spline, support_radius=2.0): """ This function overwrites the original Meshdefo.transfer_deformations(). - This version works on the local mesh of a mpi partition, making the calculation of the + This version works on the local mesh of a mpi partition, making the calculation of the mesh deformations faster. """ logging.info('Transferring deformations to the local CFD surface with {} nodes.'.format(self.local_mesh['n'])) # build spline matrix - PHIi_d = spline_functions.spline_rbf(grid_i, set_i, self.local_mesh, '', - rbf_type=rbf_type, surface_spline=surface_spline, - support_radius=support_radius, dimensions=[U_i.size, self.local_mesh['n']*6]) + PHIi_d = spline_functions.spline_rbf(grid_i, set_i, self.local_mesh, '', + rbf_type=rbf_type, surface_spline=surface_spline, + support_radius=support_radius, dimensions=[U_i.size, self.local_mesh['n'] * 6]) # store deformation of cfdgrid self.Ucfd += PHIi_d.dot(U_i) - del PHIi_d - + del PHIi_d + def set_euler_transformation(self, XYZ, PhiThetaPsi): self.XYZ = XYZ self.PhiThetaPsi = PhiThetaPsi + class SU2InterfaceFarfieldOnflow(SU2InterfaceGridVelocity): - + def set_grid_velocities(self, uvwpqr): # Do nothing here. pass def get_last_solution(self): return self.Pcfd_body() - + def Pcfd_body(self): Pcfd_global = self.Pcfd_global() - + # translate position and euler angles into body coordinate system PHInorm_cg = self.mass['PHInorm_cg'] - PhiThetaPsi_body = PHInorm_cg[0:3,0:3].dot(self.PhiThetaPsi) - + PhiThetaPsi_body = PHInorm_cg[0:3, 0:3].dot(self.PhiThetaPsi) + # setting up coordinate system coord_tmp = copy.deepcopy(self.coord) - coord_tmp['ID'] = np.append(coord_tmp['ID'], [1000000, 1000001]) - coord_tmp['RID'] = np.append(coord_tmp['RID'], [0, 0]) + coord_tmp['ID'] = np.append(coord_tmp['ID'], [1000000, 1000001]) + coord_tmp['RID'] = np.append(coord_tmp['RID'], [0, 0]) coord_tmp['dircos'] = np.append(coord_tmp['dircos'], [np.eye(3), - calc_drehmatrix(PhiThetaPsi_body[0], PhiThetaPsi_body[1], PhiThetaPsi_body[2])], - axis=0) - coord_tmp['offset'] = np.append(coord_tmp['offset'], [np.array([0.0,0.0,0.0,]), - np.array([0.0,0.0,0.0,])], - axis=0) + calc_drehmatrix(PhiThetaPsi_body[0], + PhiThetaPsi_body[1], + PhiThetaPsi_body[2])], axis=0) + + coord_tmp['offset'] = np.append(coord_tmp['offset'], [np.array([0.0, 0.0, 0.0, ]), + np.array([0.0, 0.0, 0.0, ])], axis=0) cfdgrid_tmp = copy.deepcopy(self.cfdgrid) cfdgrid_tmp['CD'] = np.repeat(1000001, self.cfdgrid['n']) @@ -346,7 +362,7 @@ def prepare_meshdefo(self, Uf, Ux2): # There may be CFD partitions which have no deformation markers. In that case, there is nothing to do. if self.local_mesh['n'] > 0: # Initialize the surface deformation vector with zeros - self.Ucfd = np.zeros(self.local_mesh['n']*6) + self.Ucfd = np.zeros(self.local_mesh['n'] * 6) # These two functions are inherited from the original Meshdefo class # Add flexible deformations self.Uf(Uf) @@ -356,19 +372,21 @@ def prepare_meshdefo(self, Uf, Ux2): self.Ucfd_rbm_transformation(self.XYZ, self.PhiThetaPsi) # Communicate the deformation of the local mesh to the CFD solver self.set_deformations() - + def Ucfd_rbm_transformation(self, XYZ, PhiThetaPsi): # translate position and euler angles into body coordinate system PHInorm_cg = self.mass['PHInorm_cg'] - PhiThetaPsi_body = PHInorm_cg[0:3,0:3].dot(PhiThetaPsi) - XYZ_body = PHInorm_cg[0:3,0:3].dot(XYZ) + PhiThetaPsi_body = PHInorm_cg[0:3, 0:3].dot(PhiThetaPsi) + XYZ_body = PHInorm_cg[0:3, 0:3].dot(XYZ) # remove any translation in x-direction XYZ_body[0] = 0.0 # setting up coordinate system coord_tmp = copy.deepcopy(self.coord) - coord_tmp['ID'] = np.append(coord_tmp['ID'], [1000000, 1000001]) - coord_tmp['RID'] = np.append(coord_tmp['RID'], [0, 0]) - coord_tmp['dircos'] = np.append(coord_tmp['dircos'], [calc_drehmatrix(PhiThetaPsi_body[0], PhiThetaPsi_body[1], PhiThetaPsi_body[2]), + coord_tmp['ID'] = np.append(coord_tmp['ID'], [1000000, 1000001]) + coord_tmp['RID'] = np.append(coord_tmp['RID'], [0, 0]) + coord_tmp['dircos'] = np.append(coord_tmp['dircos'], [calc_drehmatrix(PhiThetaPsi_body[0], + PhiThetaPsi_body[1], + PhiThetaPsi_body[2]), np.eye(3)], axis=0) coord_tmp['offset'] = np.append(coord_tmp['offset'], [self.cggrid['offset'][0] + XYZ_body, -self.cggrid['offset'][0]], axis=0) @@ -377,16 +395,16 @@ def Ucfd_rbm_transformation(self, XYZ, PhiThetaPsi): local_mesh_tmp = copy.deepcopy(self.local_mesh) local_mesh_tmp['CP'] = np.repeat(1000001, self.local_mesh['n']) grid_trafo(local_mesh_tmp, coord_tmp, 1000000) - + # calculate rigid deformation vector from transformed mesh - Ucfd_rigid = np.zeros(self.local_mesh['n']*6) - Ucfd_rigid[self.local_mesh['set'][:,0]] = local_mesh_tmp['offset'][:,0] - self.local_mesh['offset'][:,0] - Ucfd_rigid[self.local_mesh['set'][:,1]] = local_mesh_tmp['offset'][:,1] - self.local_mesh['offset'][:,1] - Ucfd_rigid[self.local_mesh['set'][:,2]] = local_mesh_tmp['offset'][:,2] - self.local_mesh['offset'][:,2] + Ucfd_rigid = np.zeros(self.local_mesh['n'] * 6) + Ucfd_rigid[self.local_mesh['set'][:, 0]] = local_mesh_tmp['offset'][:, 0] - self.local_mesh['offset'][:, 0] + Ucfd_rigid[self.local_mesh['set'][:, 1]] = local_mesh_tmp['offset'][:, 1] - self.local_mesh['offset'][:, 1] + Ucfd_rigid[self.local_mesh['set'][:, 2]] = local_mesh_tmp['offset'][:, 2] - self.local_mesh['offset'][:, 2] # transform existing mesh deformation vector Ucfd_trans = vector_trafo(self.local_mesh, coord_tmp, self.Ucfd, dest_coord=1000000) - + # combine transformation vector self.Ucfd = Ucfd_rigid + Ucfd_trans @@ -398,61 +416,66 @@ def update_general_para(self): if self.first_execution and self.myid == 0: # read all existing parameters config = SU2.io.Config(self.para_filename) - # set general parameters, which don't change over the course of the CFD simulation, so they are only updated + # set general parameters, which don't change over the course of the CFD simulation, so they are only updated # for the first execution config['MESH_FILENAME'] = self.jcl.meshdefo['surface']['filename_grid'] - config['RESTART_FILENAME'] = self.jcl.aero['para_path']+'sol/restart_subcase_{}.dat'.format(self.trimcase['subcase']) - config['SOLUTION_FILENAME'] = self.jcl.aero['para_path']+'sol/restart_subcase_{}.dat'.format(self.trimcase['subcase']) - config['SURFACE_FILENAME'] = self.jcl.aero['para_path']+'sol/surface_subcase_{}'.format(self.trimcase['subcase']) - config['VOLUME_FILENAME'] = self.jcl.aero['para_path']+'sol/volume_subcase_{}'.format(self.trimcase['subcase']) - config['CONV_FILENAME'] = self.jcl.aero['para_path']+'sol/history_subcase_{}'.format(self.trimcase['subcase']) + config['RESTART_FILENAME'] = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}.dat'.format( + self.trimcase['subcase']) + config['SOLUTION_FILENAME'] = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}.dat'.format( + self.trimcase['subcase']) + config['SURFACE_FILENAME'] = self.jcl.aero['para_path'] + 'sol/surface_subcase_{}'.format( + self.trimcase['subcase']) + config['VOLUME_FILENAME'] = self.jcl.aero['para_path'] + 'sol/volume_subcase_{}'.format( + self.trimcase['subcase']) + config['CONV_FILENAME'] = self.jcl.aero['para_path'] + 'sol/history_subcase_{}'.format( + self.trimcase['subcase']) # free stream definition config['FREESTREAM_TEMPERATURE'] = self.atmo['T'] - config['FREESTREAM_DENSITY'] = self.atmo['rho'] - config['FREESTREAM_PRESSURE'] = self.atmo['p'] + config['FREESTREAM_DENSITY'] = self.atmo['rho'] + config['FREESTREAM_PRESSURE'] = self.atmo['p'] # update the pressure inlet if hasattr(self.jcl, 'pressure_inlet'): - config['MARKER_INLET'] = '( '+', '.join([self.jcl.pressure_inlet['marker'], - str(self.atmo['T']), - str(self.atmo['p']), - str(self.jcl.pressure_inlet['flow_direction'][0]), - str(self.jcl.pressure_inlet['flow_direction'][1]), - str(self.jcl.pressure_inlet['flow_direction'][2]),]) + ' )' + config['MARKER_INLET'] = '( ' + ', '.join([self.jcl.pressure_inlet['marker'], + str(self.atmo['T']), + str(self.atmo['p']), + str(self.jcl.pressure_inlet['flow_direction'][0]), + str(self.jcl.pressure_inlet['flow_direction'][1]), + str(self.jcl.pressure_inlet['flow_direction'][2]), ]) + ' )' # set the farfield onflow - config['MACH_NUMBER'] = self.trimcase['Ma'] - config['AOA'] = 0.0 - config['SIDESLIP_ANGLE'] = 0.0 + config['MACH_NUMBER'] = self.trimcase['Ma'] + config['AOA'] = 0.0 + config['SIDESLIP_ANGLE'] = 0.0 # activate grid deformation config['DEFORM_MESH'] = 'YES' - config['MARKER_DEFORM_MESH'] = '( '+', '.join(self.jcl.meshdefo['surface']['markers'])+' )' + config['MARKER_DEFORM_MESH'] = '( ' + ', '.join(self.jcl.meshdefo['surface']['markers']) + ' )' # there is no restart for the first execution config['RESTART_SOL'] = 'NO' # detrimine cfd solution output - config['OUTPUT_FILES']= ['RESTART', 'RESTART_ASCII', 'TECPLOT', 'SURFACE_TECPLOT'] + config['OUTPUT_FILES'] = ['RESTART', 'RESTART_ASCII', 'TECPLOT', 'SURFACE_TECPLOT'] # set intemediate outputs # config['OUTPUT_WRT_FREQ']= 1 - + # do the update # config.write() maintains the original layout of the file but doesn't add new parameters # config.dump() writes all parameters in a weird order, including default values config.dump(self.para_filename) logging.info('SU2 parameter file updated.') - + def update_timedom_para(self): """ - In this section, the time domain-related parameters are updated. + In this section, the time domain-related parameters are updated. """ if self.first_execution and self.myid == 0: config = SU2.io.Config(self.para_filename) - config['TIME_DOMAIN'] = 'YES' + config['TIME_DOMAIN'] = 'YES' config['TIME_MARCHING'] = 'DUAL_TIME_STEPPING-2ND_ORDER' - config['TIME_STEP'] = self.stepwidth + config['TIME_STEP'] = self.stepwidth # estimate the number or steps the integrator will make - timesteps = np.ceil(self.simcase['t_final']/self.simcase['dt']) - iterations_per_timestep = np.ceil(self.simcase['dt']/self.stepwidth) - config['TIME_ITER'] = 2 + timesteps * iterations_per_timestep - config['MAX_TIME'] = (2 + timesteps * iterations_per_timestep) * self.stepwidth - + timesteps = np.ceil(self.simcase['t_final'] / self.simcase['dt']) + iterations_per_timestep = np.ceil(self.simcase['dt'] / self.stepwidth) + config['TIME_ITER'] = 2 + timesteps * iterations_per_timestep + config['MAX_TIME'] = (2 + timesteps * iterations_per_timestep) * self.stepwidth + """ Perform an unsteady restart from a steady solution currently involves the following steps (as discussed here: https://github.com/su2code/SU2/discussions/1964): @@ -460,57 +483,63 @@ def update_timedom_para(self): - Restart the unsteady solution with RESTART_ITER= 2 - Because SU2 requires both the .dat and the .csv file, this involves four file operations. """ - config['RESTART_SOL'] = 'YES' - config['RESTART_ITER'] = 2 + config['RESTART_SOL'] = 'YES' + config['RESTART_ITER'] = 2 # create links for the .dat files... - filename_steady = self.jcl.aero['para_path']+'sol/restart_subcase_{}.dat'.format(self.trimcase['subcase']) - filename_unsteady0 = self.jcl.aero['para_path']+'sol/restart_subcase_{}_00000.dat'.format(self.trimcase['subcase']) - filename_unsteady1 = self.jcl.aero['para_path']+'sol/restart_subcase_{}_00001.dat'.format(self.trimcase['subcase']) + filename_steady = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}.dat'.format( + self.trimcase['subcase']) + filename_unsteady0 = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}_00000.dat'.format( + self.trimcase['subcase']) + filename_unsteady1 = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}_00001.dat'.format( + self.trimcase['subcase']) try: os.symlink(filename_steady, filename_unsteady0) os.symlink(filename_steady, filename_unsteady1) - except: + except FileExistsError: # Do nothing, the most likely cause is that the file already exists. pass # ...and for the .csv files - filename_steady = self.jcl.aero['para_path']+'sol/restart_subcase_{}.csv'.format(self.trimcase['subcase']) - filename_unsteady0 = self.jcl.aero['para_path']+'sol/restart_subcase_{}_00000.csv'.format(self.trimcase['subcase']) - filename_unsteady1 = self.jcl.aero['para_path']+'sol/restart_subcase_{}_00001.csv'.format(self.trimcase['subcase']) + filename_steady = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}.csv'.format( + self.trimcase['subcase']) + filename_unsteady0 = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}_00000.csv'.format( + self.trimcase['subcase']) + filename_unsteady1 = self.jcl.aero['para_path'] + 'sol/restart_subcase_{}_00001.csv'.format( + self.trimcase['subcase']) try: os.symlink(filename_steady, filename_unsteady0) os.symlink(filename_steady, filename_unsteady1) - except: + except FileExistsError: pass - + """ In the time domain, simply rely on the the density residual to establish convergence. - This is because SU2 only needs a few inner iterations per time step, which are too few for a meaningful + This is because SU2 only needs a few inner iterations per time step, which are too few for a meaningful cauchy convergence. """ if 'CONV_FIELD' in config: config.pop('CONV_FIELD') config['INNER_ITER'] = 30 config['CONV_RESIDUAL_MINVAL'] = -6 - - # There is no need for restart solutions, they only take up storage space. Write plotting files only. - config['OUTPUT_FILES']= ['TECPLOT', 'SURFACE_TECPLOT'] - + + # There is no need for restart solutions, they only take up storage space. Write plotting files only. + config['OUTPUT_FILES'] = ['TECPLOT', 'SURFACE_TECPLOT'] + # do the update config.dump(self.para_filename) logging.info('SU2 parameter file updated.') - + def update_gust_para(self, Vtas, Vgust): """ - In this section, the gust-related parameters are updated. + In this section, the gust-related parameters are updated. """ if self.first_execution and self.myid == 0: config = SU2.io.Config(self.para_filename) - config['GRID_MOVEMENT'] = 'GUST' - config['WIND_GUST'] = 'YES' - config['GUST_TYPE'] = 'ONE_M_COSINE' + config['GRID_MOVEMENT'] = 'GUST' + config['WIND_GUST'] = 'YES' + config['GUST_TYPE'] = 'ONE_M_COSINE' """ - Establish the gust direction as SU2 can handle gusts either in x, y or z-direction. Arbitrary gust - orientations are currently not supported. By swithing the sign of the gust velocity, gusts from + Establish the gust direction as SU2 can handle gusts either in x, y or z-direction. Arbitrary gust + orientations are currently not supported. By swithing the sign of the gust velocity, gusts from four different directions are possible. This should cover most of our applications, though. """ if self.simcase['gust_orientation'] in [0.0, 360.0]: @@ -526,16 +555,16 @@ def update_gust_para(self, Vtas, Vgust): config['GUST_DIR'] = 'Y_DIR' config['GUST_AMPL'] = -Vgust else: - logging.error( 'Gust orientation {} currently NOT supported by SU2. \ - Possible values: 0.0, 90.0, 180.0, 270.0 or 360.0 degrees.'.format(self.simcase['gust_orientation'])) + logging.error('Gust orientation {} currently NOT supported by SU2. \ + Possible values: 0.0, 90.0, 180.0, 270.0 or 360.0 degrees.'.format( + self.simcase['gust_orientation'])) # Note: In SU2 this is the full gust length, not the gust gradient H (half gust length). - config['GUST_WAVELENGTH'] = 2.0*self.simcase['gust_gradient'] - config['GUST_PERIODS'] = 1.0 - config['GUST_AMPL'] = Vgust - config['GUST_BEGIN_TIME'] = 0.0 - config['GUST_BEGIN_LOC'] = -2.0*self.simcase['gust_gradient'] - self.simcase['gust_para']['T1']*Vtas + config['GUST_WAVELENGTH'] = 2.0 * self.simcase['gust_gradient'] + config['GUST_PERIODS'] = 1.0 + config['GUST_AMPL'] = Vgust + config['GUST_BEGIN_TIME'] = 0.0 + config['GUST_BEGIN_LOC'] = -2.0 * self.simcase['gust_gradient'] - self.simcase['gust_para']['T1'] * Vtas # do the update config.dump(self.para_filename) logging.info('SU2 parameter file updated.') - diff --git a/loadskernel/cfd_interfaces/tau_interface.py b/loadskernel/cfd_interfaces/tau_interface.py index 8f81728..0a3ebcc 100644 --- a/loadskernel/cfd_interfaces/tau_interface.py +++ b/loadskernel/cfd_interfaces/tau_interface.py @@ -1,17 +1,23 @@ """ -Technically, Tau-Python works with both Python 2 and 3. However, the Tau versions on our -linux cluster and on marvinng are compiled with Python 2 and don't work with Python 3. -With the wrong Python version, an error is raised already during the import, which is +Technically, Tau-Python works with both Python 2 and 3. However, the Tau versions on our +linux cluster and on marvinng are compiled with Python 2 and don't work with Python 3. +With the wrong Python version, an error is raised already during the import, which is handled by the try/except statement below. """ try: import PyPara from tau_python import tau_parallel_end, tau_close -except: +except ImportError: pass +import logging +import os +import platform +import shlex +import subprocess +import sys +import shutil import numpy as np -import logging, os, subprocess, shlex, sys, platform, shutil import scipy.io.netcdf as netcdf from loadskernel.cfd_interfaces import meshdefo @@ -19,15 +25,18 @@ from loadskernel.io_functions.data_handling import check_path from loadskernel.io_functions.data_handling import load_hdf5_dict + def copy_para_file(jcl, trimcase): para_path = check_path(jcl.aero['para_path']) - src = para_path+jcl.aero['para_file'] - dst = para_path+'para_subcase_{}'.format(trimcase['subcase']) + src = para_path + jcl.aero['para_file'] + dst = para_path + 'para_subcase_{}'.format(trimcase['subcase']) shutil.copyfile(src, dst) - + + def check_para_path(jcl): jcl.aero['para_path'] = check_path(jcl.aero['para_path']) + def check_cfd_folders(jcl): para_path = check_path(jcl.aero['para_path']) # check and create default folders for Tau @@ -40,34 +49,36 @@ def check_cfd_folders(jcl): if not os.path.exists(os.path.join(para_path, 'dualgrid')): os.makedirs(os.path.join(para_path, 'dualgrid')) + class TauInterface(meshdefo.Meshdefo): - + def __init__(self, solution): - self.model = solution.model - self.jcl = solution.jcl - self.trimcase = solution.trimcase - self.simcase = solution.simcase + self.model = solution.model + self.jcl = solution.jcl + self.trimcase = solution.trimcase + self.simcase = solution.simcase # load data from HDF5 - self.mass = load_hdf5_dict(self.model['mass'][self.trimcase['mass']]) - self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) - self.cggrid = load_hdf5_dict(self.mass['cggrid']) - self.cfdgrid = load_hdf5_dict(self.model['cfdgrid']) - self.cfdgrids = load_hdf5_dict(self.model['cfdgrids']) - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + self.mass = load_hdf5_dict(self.model['mass'][self.trimcase['mass']]) + self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) + self.cggrid = load_hdf5_dict(self.mass['cggrid']) + self.cfdgrid = load_hdf5_dict(self.model['cfdgrid']) + self.cfdgrids = load_hdf5_dict(self.model['cfdgrids']) + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) self.splinegrid = load_hdf5_dict(self.model['splinegrid']) - self.aerogrid = load_hdf5_dict(self.model['aerogrid']) - self.x2grid = load_hdf5_dict(self.model['x2grid']) - self.coord = load_hdf5_dict(self.model['coord']) - - self.Djx2 = self.model['Djx2'][()] + self.aerogrid = load_hdf5_dict(self.model['aerogrid']) + self.x2grid = load_hdf5_dict(self.model['x2grid']) + self.coord = load_hdf5_dict(self.model['coord']) + + self.Djx2 = self.model['Djx2'][()] # set switch for first execution self.first_execution = True # Check if Tau-Python was imported successfully, see try/except statement in the import section. if "PyPara" in sys.modules: logging.info('Init CFD interface of type "{}"'.format(self.__class__.__name__)) else: - logging.error('Tau-Python was/could NOT be imported! Model equations of type "{}" will NOT work.'.format(self.jcl.aero['method'])) - + logging.error('Tau-Python was/could NOT be imported! Model equations of type "{}" will NOT work.'.format( + self.jcl.aero['method'])) + # Check if the correct MPI implementation is used, Tau requires OpenMPI args_version = shlex.split('mpiexec --help') process = subprocess.Popen(args_version, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -76,17 +87,17 @@ def __init__(self, solution): logging.error('Wrong MPI implementation detected (Tau requires OpenMPI).') # Set-up a list of hosts on which MPI shall be executed. self.setup_mpi_hosts(n_workers=1) - + # Set-up file system structure check_para_path(self.jcl) copy_para_file(self.jcl, self.trimcase) check_cfd_folders(self.jcl) - + def setup_mpi_hosts(self, n_workers): # Set-up a list of hosts on which MPI shall be executed. machinefile = self.jcl.machinefile n_required = self.jcl.aero['tau_cores'] * n_workers - if machinefile == None: + if machinefile is None: # all work is done on this node tau_mpi_hosts = [platform.node()] * n_required else: @@ -97,51 +108,53 @@ def setup_mpi_hosts(self, n_workers): line = line.split(' slots=') tau_mpi_hosts += [line[0]] * int(line[1]) if tau_mpi_hosts.__len__() < n_required: - logging.error('Number of given hosts ({}) smaller than required hosts ({}). Exit.'.format(tau_mpi_hosts.__len__(), n_required)) + logging.error('Number of given hosts ({}) smaller than required hosts ({}). Exit.'.format( + tau_mpi_hosts.__len__(), n_required)) sys.exit() self.tau_mpi_hosts = tau_mpi_hosts - + def prepare_meshdefo(self, Uf, Ux2): self.init_deformations() self.Uf(Uf, self.trimcase) self.Ux2(Ux2) - self.write_deformations(self.jcl.aero['para_path']+'./defo/surface_defo_subcase_' + str(self.trimcase['subcase'])) - - Para = PyPara.Parafile(self.jcl.aero['para_path']+'para_subcase_{}'.format(self.trimcase['subcase'])) + self.write_deformations(self.jcl.aero['para_path'] + './defo/surface_defo_subcase_' + str(self.trimcase['subcase'])) + + Para = PyPara.Parafile(self.jcl.aero['para_path'] + 'para_subcase_{}'.format(self.trimcase['subcase'])) # deformation related parameters # it is important to start the deformation always from the undeformed grid ! para_dict = {'Primary grid filename': self.jcl.meshdefo['surface']['filename_grid'], 'New primary grid prefix': './defo/volume_defo_subcase_{}'.format(self.trimcase['subcase'])} Para.update(para_dict) - para_dict = {'RBF basis coordinates and deflections filename': './defo/surface_defo_subcase_{}.nc'.format(self.trimcase['subcase']),} + para_dict = {'RBF basis coordinates and deflections filename': './defo/surface_defo_subcase_{}.nc'.format( + self.trimcase['subcase']), } Para.update(para_dict, 'group end', 0,) self.pytau_close() - + def set_grid_velocities(self, uvwpqr): - Para = PyPara.Parafile(self.jcl.aero['para_path']+'para_subcase_{}'.format(self.trimcase['subcase'])) + Para = PyPara.Parafile(self.jcl.aero['para_path'] + 'para_subcase_{}'.format(self.trimcase['subcase'])) # set aircraft motion related parameters # given in local, body-fixed reference frame, see Tau User Guide Section 18.1 "Coordinate Systems of the TAU-Code" # rotations in [deg], translations in grid units - para_dict = {'Origin of local coordinate system':'{} {} {}'.format(self.cggrid['offset'][0,0],\ - self.cggrid['offset'][0,1],\ - self.cggrid['offset'][0,2]), + para_dict = {'Origin of local coordinate system': '{} {} {}'.format(self.cggrid['offset'][0, 0], + self.cggrid['offset'][0, 1], + self.cggrid['offset'][0, 2]), 'Polynomial coefficients for translation x': '0 {}'.format(uvwpqr[0]), 'Polynomial coefficients for translation y': '0 {}'.format(uvwpqr[1]), 'Polynomial coefficients for translation z': '0 {}'.format(uvwpqr[2]), - 'Polynomial coefficients for rotation roll': '0 {}'.format(uvwpqr[3]/np.pi*180.0), - 'Polynomial coefficients for rotation pitch':'0 {}'.format(uvwpqr[4]/np.pi*180.0), - 'Polynomial coefficients for rotation yaw': '0 {}'.format(uvwpqr[5]/np.pi*180.0), + 'Polynomial coefficients for rotation roll': '0 {}'.format(uvwpqr[3] / np.pi * 180.0), + 'Polynomial coefficients for rotation pitch': '0 {}'.format(uvwpqr[4] / np.pi * 180.0), + 'Polynomial coefficients for rotation yaw': '0 {}'.format(uvwpqr[5] / np.pi * 180.0), } Para.update(para_dict, 'mdf end', 0,) logging.debug("Parameters updated.") self.pytau_close() - + def update_general_para(self): if self.first_execution: - Para = PyPara.Parafile(self.jcl.aero['para_path']+'para_subcase_{}'.format(self.trimcase['subcase'])) + Para = PyPara.Parafile(self.jcl.aero['para_path'] + 'para_subcase_{}'.format(self.trimcase['subcase'])) # Para.update(para_dict, block_key, block_id, key, key_value, sub_file, para_replace) - - # set general parameters, which don't change over the course of the CFD simulation, so they are only updated + + # set general parameters, which don't change over the course of the CFD simulation, so they are only updated # for the first execution para_dict = {'Reference Mach number': self.trimcase['Ma'], 'Reference temperature': self.atmo['T'], @@ -152,45 +165,50 @@ def update_general_para(self): 'Grid prefix': './dualgrid/subcase_{}'.format(self.trimcase['subcase']), } Para.update(para_dict) - + logging.debug("Parameters updated.") self.pytau_close() - + def update_timedom_para(self): pass - + def update_gust_para(self, simcase, v_gust): pass - + def pytau_close(self): # clean up to avoid trouble at the next run tau_parallel_end() tau_close() - + def init_solver(self): pass - + def run_solver(self): tau_mpi_hosts = ','.join(self.tau_mpi_hosts) - logging.info('Starting Tau deformation, preprocessing and solver on {} hosts ({}).'.format(self.jcl.aero['tau_cores'], tau_mpi_hosts) ) + logging.info('Starting Tau deformation, preprocessing and solver on {} hosts ({}).'.format( + self.jcl.aero['tau_cores'], tau_mpi_hosts)) old_dir = os.getcwd() os.chdir(self.jcl.aero['para_path']) - args_deform = shlex.split('mpiexec -np {} --host {} deformation para_subcase_{} ./log/log_subcase_{} with mpi'.format(self.jcl.aero['tau_cores'], tau_mpi_hosts, self.trimcase['subcase'], self.trimcase['subcase'])) - args_pre = shlex.split('mpiexec -np {} --host {} ptau3d.preprocessing para_subcase_{} ./log/log_subcase_{} with mpi'.format(self.jcl.aero['tau_cores'], tau_mpi_hosts, self.trimcase['subcase'], self.trimcase['subcase'])) - args_solve = shlex.split('mpiexec -np {} --host {} ptau3d.{} para_subcase_{} ./log/log_subcase_{} with mpi'.format(self.jcl.aero['tau_cores'], tau_mpi_hosts, self.jcl.aero['tau_solver'], self.trimcase['subcase'], self.trimcase['subcase'])) - + args_deform = shlex.split('mpiexec -np {} --host {} deformation para_subcase_{} ./log/log_subcase_{} with mpi'.format( + self.jcl.aero['tau_cores'], tau_mpi_hosts, self.trimcase['subcase'], self.trimcase['subcase'])) + args_pre = shlex.split('mpiexec -np {} --host {} ptau3d.preprocessing para_subcase_{} ./log/log_subcase_{} \ + with mpi'.format(self.jcl.aero['tau_cores'], tau_mpi_hosts, self.trimcase['subcase'], self.trimcase['subcase'])) + args_solve = shlex.split('mpiexec -np {} --host {} ptau3d.{} para_subcase_{} ./log/log_subcase_{} with mpi'.format( + self.jcl.aero['tau_cores'], tau_mpi_hosts, self.jcl.aero['tau_solver'], self.trimcase['subcase'], + self.trimcase['subcase'])) + returncode = subprocess.call(args_deform) - if returncode != 0: - raise TauError('Subprocess returned an error from Tau deformation, please see deformation.stdout !') + if returncode != 0: + raise TauError('Subprocess returned an error from Tau deformation, please see deformation.stdout !') returncode = subprocess.call(args_pre) if returncode != 0: raise TauError('Subprocess returned an error from Tau preprocessing, please see preprocessing.stdout !') - + if self.first_execution == 1: self.prepare_initial_solution(args_solve) # We are done with the fist run of Tau, the next execution should be the 'real' run - self.first_execution = False + self.first_execution = False else: returncode = subprocess.call(args_solve) if returncode != 0: @@ -198,23 +216,24 @@ def run_solver(self): logging.info("Tau finished normally.") os.chdir(old_dir) - + def get_last_solution(self): # get filename of surface solution from para file - Para = PyPara.Parafile(self.jcl.aero['para_path']+'para_subcase_{}'.format(self.trimcase['subcase'])) + Para = PyPara.Parafile(self.jcl.aero['para_path'] + 'para_subcase_{}'.format(self.trimcase['subcase'])) filename_surface = self.jcl.aero['para_path'] + Para.get_para_value('Surface output filename') self.pytau_close() # gather from multiple domains old_dir = os.getcwd() os.chdir(self.jcl.aero['para_path']) - # using a individual para file only for gathering allows to gather only the surface, not the volume output, which is faster - with open('gather_subcase_{}.para'.format(self.trimcase['subcase']),'w') as fid: + # using a individual para file only for gathering allows to gather only the surface, not the volume output, which is + # faster + with open('gather_subcase_{}.para'.format(self.trimcase['subcase']), 'w') as fid: fid.write('Restart-data prefix : {}'.format(filename_surface)) subprocess.call(['gather', 'gather_subcase_{}.para'.format(self.trimcase['subcase'])]) os.chdir(old_dir) - - logging.info( 'Reading {}'.format(filename_surface)) + + logging.info('Reading {}'.format(filename_surface)) ncfile_pval = netcdf.NetCDFFile(filename_surface, 'r') global_id = ncfile_pval.variables['global_id'][:].copy() @@ -227,35 +246,32 @@ def get_last_solution(self): pos = range(self.cfdgrid['n']) else: pos = [] - for ID in self.cfdgrid['ID']: - pos.append(np.where(global_id == ID)[0][0]) - # build force vector from cfd solution self.engine(X) - Pcfd = np.zeros(self.cfdgrid['n']*6) - Pcfd[self.cfdgrid['set'][:,0]] = ncfile_pval.variables['x-force'][:][pos].copy() - Pcfd[self.cfdgrid['set'][:,1]] = ncfile_pval.variables['y-force'][:][pos].copy() - Pcfd[self.cfdgrid['set'][:,2]] = ncfile_pval.variables['z-force'][:][pos].copy() + for ID in self.cfdgrid['ID']: + pos.append(np.where(global_id == ID)[0][0]) + # build force vector from cfd solution self.engine(X) + Pcfd = np.zeros(self.cfdgrid['n'] * 6) + Pcfd[self.cfdgrid['set'][:, 0]] = ncfile_pval.variables['x-force'][:][pos].copy() + Pcfd[self.cfdgrid['set'][:, 1]] = ncfile_pval.variables['y-force'][:][pos].copy() + Pcfd[self.cfdgrid['set'][:, 2]] = ncfile_pval.variables['z-force'][:][pos].copy() return Pcfd - - def prepare_initial_solution(self, args_solve): - Para = PyPara.Parafile(self.jcl.aero['para_path']+'para_subcase_{}'.format(self.trimcase['subcase'])) + + def prepare_initial_solution(self, args_solve): + Para = PyPara.Parafile(self.jcl.aero['para_path'] + 'para_subcase_{}'.format(self.trimcase['subcase'])) # set solution parameters for the initial and following solutions - para_dicts = [ - # initial solution - {'Inviscid flux discretization type': 'Upwind', + para_dicts = [{'Inviscid flux discretization type': 'Upwind', 'Order of upwind flux (1-2)': 1.0, - 'Maximal time step number': 300, - }, - # restore parameters for following solutions + 'Maximal time step number': 300, + }, {'Inviscid flux discretization type': Para.get_para_value('Inviscid flux discretization type'), 'Order of upwind flux (1-2)': Para.get_para_value('Order of upwind flux (1-2)'), - 'Maximal time step number': Para.get_para_value('Maximal time step number'), - }] + 'Maximal time step number': Para.get_para_value('Maximal time step number'), + }] for para_dict in para_dicts: Para.update(para_dict) returncode = subprocess.call(args_solve) if returncode != 0: raise TauError('Subprocess returned an error from Tau solver, please see solver.stdout !') - + def release_memory(self): # Because Tau is called via a subprocess, there is no need to release memory manually. pass @@ -264,42 +280,50 @@ def init_deformations(self): # create empty deformation vectors for cfdgrids self.Ucfd = [] for marker in self.cfdgrids: - self.Ucfd.append(np.zeros(self.cfdgrids[marker]['n'][()]*6)) - + self.Ucfd.append(np.zeros(self.cfdgrids[marker]['n'][()] * 6)) + def transfer_deformations(self, grid_i, U_i, set_i, rbf_type, surface_spline, support_radius=2.0): logging.info('Transferring deformations to the CFD surface mesh.') if self.plotting: # set-up plot from mayavi import mlab - p_scale = 0.05 # points + p_scale = 0.05 # points mlab.figure() - mlab.points3d(grid_i['offset'+set_i][:,0], grid_i['offset'+set_i][:,1], grid_i['offset'+set_i][:,2] , scale_factor=p_scale, color=(1,1,1)) - mlab.points3d(grid_i['offset'+set_i][:,0] + U_i[grid_i['set'+set_i][:,0]], grid_i['offset'+set_i][:,1] + U_i[grid_i['set'+set_i][:,1]], grid_i['offset'+set_i][:,2] + U_i[grid_i['set'+set_i][:,2]], scale_factor=p_scale, color=(1,0,0)) + mlab.points3d(grid_i['offset' + set_i][:, 0], grid_i['offset' + set_i][:, 1], grid_i['offset' + set_i][:, 2], + scale_factor=p_scale, color=(1, 1, 1)) + mlab.points3d(grid_i['offset' + set_i][:, 0] + U_i[grid_i['set' + set_i][:, 0]], + grid_i['offset' + set_i][:, 1] + U_i[grid_i['set' + set_i][:, 1]], + grid_i['offset' + set_i][:, 2] + U_i[grid_i['set' + set_i][:, 2]], + scale_factor=p_scale, color=(1, 0, 0)) for marker, Ucfd in zip(self.cfdgrids, self.Ucfd): grid_d = load_hdf5_dict(self.cfdgrids[marker]) logging.debug('Working on marker {}'.format(grid_d['desc'])) # build spline matrix - PHIi_d = spline_functions.spline_rbf(grid_i, set_i, grid_d, '', - rbf_type=rbf_type, surface_spline=surface_spline, - support_radius=support_radius, dimensions=[U_i.size, grid_d['n']*6]) + PHIi_d = spline_functions.spline_rbf(grid_i, set_i, grid_d, '', + rbf_type=rbf_type, surface_spline=surface_spline, + support_radius=support_radius, dimensions=[U_i.size, grid_d['n'] * 6]) # store deformation of cfdgrid Ucfd += PHIi_d.dot(U_i) if self.plotting: U_d = PHIi_d.dot(U_i) - mlab.points3d(grid_d['offset'][:,0], grid_d['offset'][:,1], grid_d['offset'][:,2], color=(0,0,0), mode='point') - mlab.points3d(grid_d['offset'][:,0] + U_d[grid_d['set'][:,0]], grid_d['offset'][:,1] + U_d[grid_d['set'][:,1]], grid_d['offset'][:,2] + U_d[grid_d['set'][:,2]], color=(0,0,1), mode='point') + mlab.points3d(grid_d['offset'][:, 0], grid_d['offset'][:, 1], grid_d['offset'][:, 2], + color=(0, 0, 0), mode='point') + mlab.points3d(grid_d['offset'][:, 0] + U_d[grid_d['set'][:, 0]], + grid_d['offset'][:, 1] + U_d[grid_d['set'][:, 1]], + grid_d['offset'][:, 2] + U_d[grid_d['set'][:, 2]], + color=(0, 0, 1), mode='point') del PHIi_d if self.plotting: mlab.show() - + def write_deformations(self, filename_defo): self.write_defo_netcdf(filename_defo) def write_defo_netcdf(self, filename_defo): - logging.info( 'Writing ' + filename_defo + '.nc') + logging.info('Writing ' + filename_defo + '.nc') f = netcdf.netcdf_file(filename_defo + '.nc', 'w') f.history = 'Surface deformations created by Loads Kernel' - + # Assemble temporary output. One point may belong to multiple markers. tmp_IDs = np.array([], dtype='int') tmp_x = np.array([]) @@ -308,17 +332,17 @@ def write_defo_netcdf(self, filename_defo): tmp_dx = np.array([]) tmp_dy = np.array([]) tmp_dz = np.array([]) - for marker, Ucfd in zip(self.cfdgrids, self.Ucfd): + for marker, Ucfd in zip(self.cfdgrids, self.Ucfd): cfdgrid = load_hdf5_dict(self.cfdgrids[marker]) tmp_IDs = np.concatenate((tmp_IDs, cfdgrid['ID'])) - tmp_x = np.concatenate((tmp_x, cfdgrid['offset'][:,0])) - tmp_y = np.concatenate((tmp_y, cfdgrid['offset'][:,1])) - tmp_z = np.concatenate((tmp_z, cfdgrid['offset'][:,2])) - tmp_dx = np.concatenate((tmp_dx, Ucfd[cfdgrid['set'][:,0]])) - tmp_dy = np.concatenate((tmp_dy, Ucfd[cfdgrid['set'][:,1]])) - tmp_dz = np.concatenate((tmp_dz, Ucfd[cfdgrid['set'][:,2]])) + tmp_x = np.concatenate((tmp_x, cfdgrid['offset'][:, 0])) + tmp_y = np.concatenate((tmp_y, cfdgrid['offset'][:, 1])) + tmp_z = np.concatenate((tmp_z, cfdgrid['offset'][:, 2])) + tmp_dx = np.concatenate((tmp_dx, Ucfd[cfdgrid['set'][:, 0]])) + tmp_dy = np.concatenate((tmp_dy, Ucfd[cfdgrid['set'][:, 1]])) + tmp_dz = np.concatenate((tmp_dz, Ucfd[cfdgrid['set'][:, 2]])) IDs, pos = np.unique(tmp_IDs, return_index=True) - + f.createDimension('no_of_points', IDs.__len__()) # create variables global_id = f.createVariable('global_id', 'i', ('no_of_points',)) @@ -339,5 +363,6 @@ def write_defo_netcdf(self, filename_defo): f.close() + class TauError(Exception): - '''Raise when subprocess yields a returncode != 0 from Tau''' \ No newline at end of file + '''Raise when subprocess yields a returncode != 0 from Tau''' diff --git a/loadskernel/engine_interfaces/engine.py b/loadskernel/engine_interfaces/engine.py index 661f7f9..f77a0d0 100644 --- a/loadskernel/engine_interfaces/engine.py +++ b/loadskernel/engine_interfaces/engine.py @@ -1,38 +1,33 @@ -''' -Created on Jul 4, 2022 - -@author: voss_ar -''' import numpy as np + class EngineLoads(object): def __init__(self): pass - + def torque_moments(self, parameter_dict): """ - This function calculates the torque from a given power and RPM setting. + This function calculates the torque from a given power and RPM setting. """ rot_vec = parameter_dict['rotation_vector'] - RPM = parameter_dict['RPM'] - power = parameter_dict['power'] + RPM = parameter_dict['RPM'] + power = parameter_dict['power'] # initialize empty force vector - P_engine = np.zeros(6) + P_engine = np.zeros(6) # calculate angular velocity rad/s omega = RPM / 60.0 * 2.0 * np.pi # calculate Mxyz - P_engine[3:] = - rot_vec * power / omega + P_engine[3:] = -rot_vec * power / omega return P_engine - + def thrust_forces(self, parameter_dict, thrust): """ This is the most thrust model. The requested thrust (from the trim) is aligned with the thrust orientation vector. """ thrust_vector = parameter_dict['thrust_vector'] # initialize empty force vector - P_engine = np.zeros(6) + P_engine = np.zeros(6) # calculate Fxyz components P_engine[:3] = thrust_vector * thrust return P_engine - \ No newline at end of file diff --git a/loadskernel/engine_interfaces/propeller.py b/loadskernel/engine_interfaces/propeller.py index 9c0d052..be4af5e 100644 --- a/loadskernel/engine_interfaces/propeller.py +++ b/loadskernel/engine_interfaces/propeller.py @@ -1,25 +1,29 @@ # encoding=utf8 - -import logging, sys, yaml, copy +import copy +import logging +import sys +import yaml import numpy as np from panelaero import VLM -from loadskernel import spline_rules -from loadskernel import spline_functions -from loadskernel.solution_tools import * + from loadskernel import equations -import loadskernel.build_aero_functions +from loadskernel import spline_functions +from loadskernel import spline_rules +from loadskernel.solution_tools import calc_drehmatrix +from loadskernel import build_aero_functions try: from pyPropMat.pyPropMat import Prop -except: +except ImportError: pass + class PyPropMat4Loads(object): """ This class calculates the aerodynamic forces and moments of a propeller following equations 1 to 4 in [1]. - The the aerodynamic derivatives are calculated using PyPropMat provided by Christopher Koch, AE-SIM from [2]. - + The the aerodynamic derivatives are calculated using PyPropMat provided by Christopher Koch, AE-SIM from [2]. + Inputs: - filename of the input-file for PyPropMat (*.yaml) - flight_condition_dict (dict): dictionary with flight data @@ -27,26 +31,26 @@ class PyPropMat4Loads(object): Ma (float): flight mach number Rho (float): air density N_rpm (float): propeller revolutions in rpm} - + Sign conventions: - - The derivatives are given in a forward-right-down coordinate system. + - The derivatives are given in a forward-right-down coordinate system. - Theta and q are defined a a positive rotation about the y-axis. - Psi and r are defined a a positive rotation about the z-axis. - - [1] Rodden, W., and Rose, T. Propeller/nacelle whirl flutter addition to MSC/nastran, in + + [1] Rodden, W., and Rose, T. Propeller/nacelle whirl flutter addition to MSC/nastran, in Proceedings of the 1989 MSC World User's Conference, 1989. - + [2] https://phabricator.ae.go.dlr.de/diffusion/180/ """ - + def __init__(self, filename): # Check if pyPropMat was imported successfully, see try/except statement in the import section. if "pyPropMat" in sys.modules: self.prop = Prop(filename) else: - logging.error('pyPropMat was/could NOT be imported!'.format(self.jcl.aero['method'])) + logging.error('pyPropMat was/could NOT be imported!') self.prop_coeffs = None - + def calc_loads(self, parameter_dict): """ Convert signs from structural coordinates (aft-right-up) to propeller coordinates (forward-right-down): @@ -54,67 +58,72 @@ def calc_loads(self, parameter_dict): positive beta = wind from the right side = negative psi positive structural q = pitch-up = positive propeller q positive structural r = yaw to the left = negative proller r - + Convert signs propeller coordinates (forward-right-down) to structural coordinates (aft-right-up): switch Fz and Mz """ - diameter = parameter_dict['diameter'] - Vtas = parameter_dict['Vtas'] - q_dyn = parameter_dict['q_dyn'] - theta = parameter_dict['alpha'] - psi = -parameter_dict['beta'] - q = parameter_dict['pqr'][1] - r = -parameter_dict['pqr'][2] - + diameter = parameter_dict['diameter'] + Vtas = parameter_dict['Vtas'] + q_dyn = parameter_dict['q_dyn'] + theta = parameter_dict['alpha'] + psi = -parameter_dict['beta'] + q = parameter_dict['pqr'][1] + r = -parameter_dict['pqr'][2] + # Calculate the area of the propeller disk with S = pi * r^2 - S = np.pi*(0.5*diameter)**2.0 - - # Calculate the coefficients for the current operational point. It is sufficient to calculate them only once + S = np.pi * (0.5 * diameter) ** 2.0 + + # Calculate the coefficients for the current operational point. It is sufficient to calculate them only once # at the first run. - if self.prop_coeffs == None: - flight_condition_dict = {'V': Vtas, - 'Ma': parameter_dict['Ma'], - 'Rho': parameter_dict['rho'], + if self.prop_coeffs is None: + flight_condition_dict = {'V': Vtas, + 'Ma': parameter_dict['Ma'], + 'Rho': parameter_dict['rho'], 'N_rpm': parameter_dict['RPM']} self.prop_coeffs = self.prop.get_propeller_coefficients(flight_condition_dict, include_lag=False) - + # Get the aerodynamic coefficients - Cz_theta = self.prop_coeffs['z_theta'] - Cz_psi = self.prop_coeffs['z_psi'] - Cz_q = self.prop_coeffs['z_q'] - Cz_r = self.prop_coeffs['z_r'] - Cy_theta = self.prop_coeffs['y_theta'] - Cy_psi = self.prop_coeffs['y_psi'] - Cy_q = self.prop_coeffs['y_q'] - Cy_r = self.prop_coeffs['y_r'] - Cm_theta = self.prop_coeffs['m_theta'] - Cm_psi = self.prop_coeffs['m_psi'] - Cm_q = self.prop_coeffs['m_q'] - Cm_r = self.prop_coeffs['m_r'] - Cn_theta = self.prop_coeffs['n_theta'] - Cn_psi = self.prop_coeffs['n_psi'] - Cn_q = self.prop_coeffs['n_q'] - Cn_r = self.prop_coeffs['n_r'] - + Cz_theta = self.prop_coeffs['z_theta'] + Cz_psi = self.prop_coeffs['z_psi'] + Cz_q = self.prop_coeffs['z_q'] + Cz_r = self.prop_coeffs['z_r'] + Cy_theta = self.prop_coeffs['y_theta'] + Cy_psi = self.prop_coeffs['y_psi'] + Cy_q = self.prop_coeffs['y_q'] + Cy_r = self.prop_coeffs['y_r'] + Cm_theta = self.prop_coeffs['m_theta'] + Cm_psi = self.prop_coeffs['m_psi'] + Cm_q = self.prop_coeffs['m_q'] + Cm_r = self.prop_coeffs['m_r'] + Cn_theta = self.prop_coeffs['n_theta'] + Cn_psi = self.prop_coeffs['n_psi'] + Cn_q = self.prop_coeffs['n_q'] + Cn_r = self.prop_coeffs['n_r'] + # initialize empty force vector P_prop = np.zeros(6) # Side force Fy, equation 3 in [1] - P_prop[1] += q_dyn*S * (Cy_theta*theta + Cy_psi*psi + Cy_q*q*diameter/(2.0*Vtas) + Cy_r*r*diameter/(2.0*Vtas)) + P_prop[1] += q_dyn * S * (Cy_theta * theta + Cy_psi * psi + Cy_q * q * diameter / (2.0 * Vtas) + + Cy_r * r * diameter / (2.0 * Vtas)) # Lift force Fz, equation 1 in [1], convert signs (see above) - P_prop[2] -= q_dyn*S * (Cz_theta*theta + Cz_psi*psi + Cz_q*q*diameter/(2.0*Vtas) + Cz_r*r*diameter/(2.0*Vtas)) + P_prop[2] -= q_dyn * S * (Cz_theta * theta + Cz_psi * psi + Cz_q * q * diameter / (2.0 * Vtas) + + Cz_r * r * diameter / (2.0 * Vtas)) # Pitching moment My, equation 2 in [1] - P_prop[4] += q_dyn*S*diameter * (Cm_theta*theta + Cm_psi*psi + Cm_q*q*diameter/(2.0*Vtas) + Cm_r*r*diameter/(2.0*Vtas)) + P_prop[4] += q_dyn * S * diameter * (Cm_theta * theta + Cm_psi * psi + Cm_q * q * diameter / (2.0 * Vtas) + + Cm_r * r * diameter / (2.0 * Vtas)) # Yawing moment Mz, equation 4 in [1], convert signs (see above) - P_prop[5] -= q_dyn*S*diameter * (Cn_theta*theta + Cn_psi*psi + Cn_q*q*diameter/(2.0*Vtas) + Cn_r*r*diameter/(2.0*Vtas)) - + P_prop[5] -= q_dyn * S * diameter * (Cn_theta * theta + Cn_psi * psi + Cn_q * q * diameter / (2.0 * Vtas) + + Cn_r * r * diameter / (2.0 * Vtas)) + # Check: An onflow from below (pos. alpha = neg. Theta) creates an upward lift (pos. Fz / P_prop[2]) return P_prop + def read_propeller_input(filename): # construct an aerodynamic grid in the y-direction (like a starboard wing) caerocards = [] EID = 1000 - logging.info('Read propeller input from: %s' %filename) + logging.info('Read propeller input from: %s' % filename) with open(filename, 'r') as ymlfile: data = yaml.safe_load(ymlfile) n_span = len(data['tabulatedData']) - 1 @@ -123,63 +132,63 @@ def read_propeller_input(filename): # read first line of CAERO card caerocard = {'EID': EID, 'CP': 0, - 'n_span': 1, # n_boxes - 'n_chord': n_chord, # n_boxes - } + 'n_span': 1, # n_boxes + 'n_chord': n_chord, # n_boxes + } # read second line of CAERO card - caerocard['X1'] = np.array([-0.5*data['tabulatedData'][i_strip][1], data['tabulatedData'][i_strip][0], 0.0]) - caerocard['X2'] = np.array([+0.5*data['tabulatedData'][i_strip][1], data['tabulatedData'][i_strip][0], 0.0]) - caerocard['X3'] = np.array([+0.5*data['tabulatedData'][i_strip+1][1], data['tabulatedData'][i_strip+1][0], 0.0]) - caerocard['X4'] = np.array([-0.5*data['tabulatedData'][i_strip+1][1], data['tabulatedData'][i_strip+1][0], 0.0]) + caerocard['X1'] = np.array([-0.5 * data['tabulatedData'][i_strip][1], data['tabulatedData'][i_strip][0], 0.0]) + caerocard['X2'] = np.array([+0.5 * data['tabulatedData'][i_strip][1], data['tabulatedData'][i_strip][0], 0.0]) + caerocard['X3'] = np.array([+0.5 * data['tabulatedData'][i_strip + 1][1], data['tabulatedData'][i_strip + 1][0], 0.0]) + caerocard['X4'] = np.array([-0.5 * data['tabulatedData'][i_strip + 1][1], data['tabulatedData'][i_strip + 1][0], 0.0]) # read the pitch angel, given in the third column if len(data['tabulatedData'][i_strip]) == 3: - caerocard['cam_rad'] = data['tabulatedData'][i_strip][2]/180.0*np.pi - else: + caerocard['cam_rad'] = data['tabulatedData'][i_strip][2] / 180.0 * np.pi + else: caerocard['cam_rad'] = 0.0 - + caerocards.append(caerocard) EID += 1000 - + # from CAERO cards, construct corner points... ' # then, combine four corner points to one panel - grid_ID = 0 # the file number is used to set a range of grid IDs - caero_grid = {'ID':[], 'offset':[]} - caero_panels = {"ID": [], 'CP':[], 'CD':[], "cornerpoints": []} + grid_ID = 0 # the file number is used to set a range of grid IDs + caero_grid = {'ID': [], 'offset': []} + caero_panels = {"ID": [], 'CP': [], 'CD': [], "cornerpoints": []} cam_rad = [] for caerocard in caerocards: # calculate LE, Root and Tip vectors [x,y,z]^T - LE = caerocard['X4'] - caerocard['X1'] + LE = caerocard['X4'] - caerocard['X1'] Root = caerocard['X2'] - caerocard['X1'] - Tip = caerocard['X3'] - caerocard['X4'] - + Tip = caerocard['X3'] - caerocard['X4'] + # assume equidistant spacing - d_chord = np.linspace(0.0, 1.0, caerocard['n_chord']+1 ) + d_chord = np.linspace(0.0, 1.0, caerocard['n_chord'] + 1) # assume equidistant spacing - d_span = np.linspace(0.0, 1.0, caerocard['n_span']+1 ) - + d_span = np.linspace(0.0, 1.0, caerocard['n_span'] + 1) + # build matrix of corner points # index based on n_divisions - grids_map = np.zeros((caerocard['n_chord']+1,caerocard['n_span']+1), dtype='int') - for i_strip in range(caerocard['n_span']+1): - for i_row in range(caerocard['n_chord']+1): - offset = caerocard['X1'] \ - + LE * d_span[i_strip] \ - + (Root*(1.0-d_span[i_strip]) + Tip*d_span[i_strip]) * d_chord[i_row] + grids_map = np.zeros((caerocard['n_chord'] + 1, caerocard['n_span'] + 1), dtype='int') + for i_strip in range(caerocard['n_span'] + 1): + for i_row in range(caerocard['n_chord'] + 1): + offset = caerocard['X1'] + LE * d_span[i_strip] + (Root * (1.0 - d_span[i_strip]) + + Tip * d_span[i_strip]) * d_chord[i_row] caero_grid['ID'].append(grid_ID) caero_grid['offset'].append(offset) - grids_map[i_row,i_strip ] = grid_ID + grids_map[i_row, i_strip] = grid_ID grid_ID += 1 # build panels from cornerpoints # index based on n_boxes - panel_ID = caerocard['EID'] + panel_ID = caerocard['EID'] for i_strip in range(caerocard['n_span']): for i_row in range(caerocard['n_chord']): caero_panels['ID'].append(panel_ID) - caero_panels['CP'].append(caerocard['CP']) # applying CP of CAERO card to all grids + caero_panels['CP'].append(caerocard['CP']) # applying CP of CAERO card to all grids caero_panels['CD'].append(caerocard['CP']) - caero_panels['cornerpoints'].append([ grids_map[i_row, i_strip], grids_map[i_row+1, i_strip], grids_map[i_row+1, i_strip+1], grids_map[i_row, i_strip+1] ]) + caero_panels['cornerpoints'].append([grids_map[i_row, i_strip], grids_map[i_row + 1, i_strip], + grids_map[i_row + 1, i_strip + 1], grids_map[i_row, i_strip + 1]]) cam_rad.append(caerocard['cam_rad']) - panel_ID += 1 + panel_ID += 1 caero_panels['ID'] = np.array(caero_panels['ID']) caero_panels['CP'] = np.array(caero_panels['CP']) caero_panels['CD'] = np.array(caero_panels['CD']) @@ -189,124 +198,134 @@ def read_propeller_input(filename): cam_rad = np.array(cam_rad) return caero_grid, caero_panels, cam_rad + class VLM4PropModel(object): - + def __init__(self, filename, coord, atmo): self.filename = filename self.coord = coord self.atmo = atmo - + def build_aerogrid(self): - self.aerogrid = loadskernel.build_aero_functions.build_aerogrid(self.filename, method_caero='VLM4Prop') + self.aerogrid = build_aero_functions.build_aerogrid(self.filename, method_caero='VLM4Prop') logging.info('The aerodynamic propeller model consists of {} panels.'.format(self.aerogrid['n'])) - + def build_pacgrid(self): A = np.sum(self.aerogrid['A']) - b_ref = self.aerogrid['offset_P3'][:,1].max() - mean_aero_choord = A/b_ref - self.pacgrid = { 'ID': np.array([0]), - 'offset': np.array([[0.0, 0.0, 0.0]]), - 'set':np.array([[0, 1, 2, 3, 4, 5]]), - 'CD': np.array([0]), - 'CP': np.array([0]), - 'coord_desc': 'bodyfixed', - 'A_ref': A, - 'b_ref': b_ref, - 'c_ref': mean_aero_choord, - } - + b_ref = self.aerogrid['offset_P3'][:, 1].max() + mean_aero_choord = A / b_ref + self.pacgrid = {'ID': np.array([0]), + 'offset': np.array([[0.0, 0.0, 0.0]]), + 'set': np.array([[0, 1, 2, 3, 4, 5]]), + 'CD': np.array([0]), + 'CP': np.array([0]), + 'coord_desc': 'bodyfixed', + 'A_ref': A, + 'b_ref': b_ref, + 'c_ref': mean_aero_choord, + } + rules = spline_rules.rules_aeropanel(self.aerogrid) - self.PHIjk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_j', rules, self.coord, sparse_output=True) - self.PHIlk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_l', rules, self.coord, sparse_output=True) - + self.PHIjk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_j', + rules, self.coord, sparse_output=True) + self.PHIlk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_l', + rules, self.coord, sparse_output=True) + rules = spline_rules.rules_point(self.pacgrid, self.aerogrid) - self.Dkx1 = spline_functions.spline_rb(self.pacgrid, '', self.aerogrid, '_k', rules, self.coord, sparse_output=False) + self.Dkx1 = spline_functions.spline_rb(self.pacgrid, '', self.aerogrid, '_k', + rules, self.coord, sparse_output=False) self.Djx1 = self.PHIjk.dot(self.Dkx1) - + def build_AICs_steady(self, Ma): self.aero = {} - self.aero['Gamma_jj'], __ = VLM.calc_Gammas(aerogrid=copy.deepcopy(self.aerogrid), Ma=Ma, xz_symmetry=True) # dim: Ma,n,n + self.aero['Gamma_jj'], __ = VLM.calc_Gammas(aerogrid=copy.deepcopy(self.aerogrid), Ma=Ma, xz_symmetry=True) + class VLM4PropLoads(object): """ - This class calculates the aerodynamic forces and moments of a propeller using the VLM. + This class calculates the aerodynamic forces and moments of a propeller using the VLM. The propeller blade is modeled as a "starboard wing" in the xy-plane and rotates about the z-axis. Limitations: - This is a quasi-steady approach. - - Modeling of the wake as a horse shoe vortex (straight wake, like for a wing, no spiral shape) and + - Modeling of the wake as a horse shoe vortex (straight wake, like for a wing, no spiral shape) and no interference with the wake of the preceding blade(s). - Modeling of only one blade and the onflow is rotated --> No interaction between neighboring blades. - Only overall Mach number correction possible, no variation in radial direction (influence negligible up to about Ma=0.3) - - Currently, only a positive rotation about the z-axis (clockwise) feasible. A counter-clockwise rotation would + - Currently, only a positive rotation about the z-axis (clockwise) feasible. A counter-clockwise rotation would lead to an onflow of the lifting surface from the back, compromising the VLM assumptions. Sign conventions: - - The forces Pmac are given in the propeller coordinate system (up-right-forward). - - The forces P_prop are given in the aircraft coordinate system (aft-right-up). + - The forces Pmac are given in the propeller coordinate system (up-right-forward). + - The forces P_prop are given in the aircraft coordinate system (aft-right-up). - Alpha positive = w positive, wind from below - Beta positive = v positive, wind from the right side """ - - def __init__(self, model, i_atmo): - + + def __init__(self, model): + self.model = model - self.i_atmo = i_atmo - + # only for compatibility with Common equations class self.Djx1 = self.model.Djx1 - + # rotation matrix from propeller into aircraft system self.Tprop2aircraft = np.array([[0, 0, -1], [0, 1, 0], [1, 0, 0]]) - + def calc_loads(self, parameter_dict): # update the operational point with the input from the parameter_dict - self.Vtas = parameter_dict['Vtas'] - self.alpha = parameter_dict['alpha'] - self.beta = parameter_dict['beta'] - self.RPM = parameter_dict['RPM'] - self.t = parameter_dict['t'] + self.Vtas = parameter_dict['Vtas'] + self.alpha = parameter_dict['alpha'] + self.beta = parameter_dict['beta'] + self.RPM = parameter_dict['RPM'] + self.t = parameter_dict['t'] # set some propeller-related parameters given in the parameter_dict self.n_blades = parameter_dict['n_blades'] # set the initial blade position at t=0.0, which is horizontal - self.phi_blades = np.linspace(0.0, 2.0*np.pi, int(self.n_blades), endpoint=False) + self.phi_blades = np.linspace(0.0, 2.0 * np.pi, int(self.n_blades), endpoint=False) # based on the RPM and the time, calculate the current position of the blades - self.phi_blades += self.RPM/60.0 * self.t * 2.0*np.pi + self.phi_blades += self.RPM / 60.0 * self.t * 2.0 * np.pi # calculate the rotation vector of the propeller in the propeller coordinate system self.rot_vec = self.Tprop2aircraft.T.dot(parameter_dict['rotation_vector']) # select Gamma matrices based on mach number - self.i_aero = 0 + self.i_aero = 0 # calculate the forces Pmac at the propeller hub Pmac = self.calc_Pmac() # initialize empty force vector and convert into aircraft-fixed coordinate system - P_prop = np.zeros(6) + P_prop = np.zeros(6) P_prop[:3] = self.Tprop2aircraft.dot(Pmac[:3]) P_prop[3:] = self.Tprop2aircraft.dot(Pmac[3:]) return P_prop - + def rbm_nonlin(self, dUmac_dt): - wj = np.ones(self.model.aerogrid['n'])*np.sum(dUmac_dt[:3]**2.0)**0.5 + wj = np.ones(self.model.aerogrid['n']) * np.sum(dUmac_dt[:3] ** 2.0) ** 0.5 Pk = equations.common.Common.calc_Pk_nonlin(self, dUmac_dt, wj) return Pk - + def camber_twist_nonlin(self, dUmac_dt): - Ujx1 = np.dot(self.Djx1,dUmac_dt) - Vtas_local = (Ujx1[self.model.aerogrid['set_j'][:,0]]**2.0 + Ujx1[self.model.aerogrid['set_j'][:,1]]**2.0 + Ujx1[self.model.aerogrid['set_j'][:,2]]**2.0)**0.5 - wj = np.sin(self.model.aerogrid['cam_rad']) * Vtas_local * -1.0 + Ujx1 = np.dot(self.Djx1, dUmac_dt) + Vtas_local = (Ujx1[self.model.aerogrid['set_j'][:, 0]] ** 2.0 + + Ujx1[self.model.aerogrid['set_j'][:, 1]] ** 2.0 + + Ujx1[self.model.aerogrid['set_j'][:, 2]] ** 2.0) ** 0.5 + wj = np.sin(self.model.aerogrid['cam_rad']) * Vtas_local * -1.0 Pk = equations.common.Common.calc_Pk_nonlin(self, dUmac_dt, wj) return Pk - + def blade2body(self, phi): - Tblade2body = np.zeros((6,6)) - Tblade2body[0:3,0:3] = calc_drehmatrix(0.0, 0.0, phi) - Tblade2body[3:6,3:6] = calc_drehmatrix(0.0, 0.0, phi) + Tblade2body = np.zeros((6, 6)) + Tblade2body[0:3, 0:3] = calc_drehmatrix(0.0, 0.0, phi) + Tblade2body[3:6, 3:6] = calc_drehmatrix(0.0, 0.0, phi) return Tblade2body, Tblade2body.T - + def calc_Pmac(self): # calculate the motion of the propeller - dUmac_dt_aircraft = np.array([-self.Vtas*np.sin(self.alpha), self.Vtas*np.sin(self.beta), self.Vtas*np.cos(self.alpha)*np.cos(self.beta), 0.0, 0.0, 0.0]) - dUmac_dt_rpm = np.concatenate(([0.0, 0.0, 0.0], self.rot_vec * self.RPM / 60.0 * 2.0 * np.pi )) + dUmac_dt_aircraft = np.array([-self.Vtas * np.sin(self.alpha), + self.Vtas * np.sin(self.beta), + self.Vtas * np.cos(self.alpha) * np.cos(self.beta), 0.0, 0.0, 0.0]) + dUmac_dt_rpm = np.concatenate(([0.0, 0.0, 0.0], self.rot_vec * self.RPM / 60.0 * 2.0 * np.pi)) # loop over all blades - self.Pk = []; self.Pmac = []; self.dUmac_dt = [] + self.Pk = [] + self.Pmac = [] + self.dUmac_dt = [] logging.debug(' Fx Fy Fz Mx My Mz') for i_blade in range(self.n_blades): # calculate the motion seen by the blade @@ -320,32 +339,32 @@ def calc_Pmac(self): self.Pk.append(Pk_rbm + Pk_cam) # sum the forces at the origin and rotate back into aircraft system self.Pmac.append(Tblade2body.dot(self.model.Dkx1.T.dot(Pk_rbm + Pk_cam))) - logging.debug('Forces from blade {} at {:>5.1f} : {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f}'.format( - i_blade, phi_i/np.pi*180.0, - self.Pmac[i_blade][0], self.Pmac[i_blade][1], self.Pmac[i_blade][2], - self.Pmac[i_blade][3], self.Pmac[i_blade][4], self.Pmac[i_blade][5], )) + logging.debug('Forces from blade {} at {:>5.1f} : {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f} \ + {:> 8.1f}'.format(i_blade, phi_i / np.pi * 180.0, + self.Pmac[i_blade][0], self.Pmac[i_blade][1], self.Pmac[i_blade][2], + self.Pmac[i_blade][3], self.Pmac[i_blade][4], self.Pmac[i_blade][5],)) # calculate the sum over all blades Pmac_sum = np.sum(self.Pmac, axis=0) logging.debug(' Sum : {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f} {:> 8.1f}'.format( Pmac_sum[0], Pmac_sum[1], Pmac_sum[2], Pmac_sum[3], Pmac_sum[4], Pmac_sum[5])) return Pmac_sum - + + class PropellerPrecessionLoads(object): - + def __init__(self): pass - + def precession_moments(self, parameter_dict): # get relevant parameters - I = parameter_dict['rotation_inertia'] + I = parameter_dict['rotation_inertia'] rot_vec = parameter_dict['rotation_vector'] - RPM = parameter_dict['RPM'] - pqr = parameter_dict['pqr'] + RPM = parameter_dict['RPM'] + pqr = parameter_dict['pqr'] # initialize empty force vector - P_prop = np.zeros(6) + P_prop = np.zeros(6) # calculate angular velocity vector rad/s - omega = rot_vec * RPM / 60.0 * 2.0 * np.pi + omega = rot_vec * RPM / 60.0 * 2.0 * np.pi # calculate Mxyz - P_prop[3:] = np.cross(omega, pqr) * I + P_prop[3:] = np.cross(omega, pqr) * I return P_prop - \ No newline at end of file diff --git a/loadskernel/equations/cfd.py b/loadskernel/equations/cfd.py index 810e2c8..3a0f665 100644 --- a/loadskernel/equations/cfd.py +++ b/loadskernel/equations/cfd.py @@ -1,29 +1,22 @@ -''' -Created on Aug 2, 2019 - -@author: voss_ar -''' import numpy as np -from loadskernel.solution_tools import * from loadskernel.equations.steady import Steady +from loadskernel.solution_tools import gravitation_on_earth + class CfdSteady(Steady): def equations(self, X, t, modus): self.counter += 1 # recover states - Tgeo2body, Tbody2geo = self.geo2body(X) - dUcg_dt, Uf, dUf_dt = self.recover_states(X) - Vtas, q_dyn = self.recover_Vtas(X) - onflow = self.recover_onflow(X) - alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) - Ux2 = self.get_Ux2(X) - delta_XYZ = np.array([0.0, 0.0, 0.0]) - PhiThetaPsi = X[self.solution.idx_states[3:6]] - # -------------------- - # --- aerodynamics --- - # -------------------- + Tgeo2body, Tbody2geo = self.geo2body(X) + dUcg_dt, Uf, dUf_dt = self.recover_states(X) + Vtas, q_dyn = self.recover_Vtas(X) + alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) + Ux2 = self.get_Ux2(X) + delta_XYZ = np.array([0.0, 0.0, 0.0]) + PhiThetaPsi = X[self.solution.idx_states[3:6]] + # aerodynamics self.cfd_interface.update_general_para() self.cfd_interface.init_solver() self.cfd_interface.set_grid_velocities(X[6:12]) @@ -31,65 +24,56 @@ def equations(self, X, t, modus): self.cfd_interface.prepare_meshdefo(Uf, Ux2) self.cfd_interface.run_solver() Pcfd = self.cfd_interface.get_last_solution() - - Pk_rbm = np.zeros(6*self.aerogrid['n']) - Pk_cam = Pk_rbm*0.0 - Pk_cs = Pk_rbm*0.0 - Pk_f = Pk_rbm*0.0 - Pk_gust = Pk_rbm*0.0 - Pk_idrag = Pk_rbm*0.0 - Pk_unsteady = Pk_rbm*0.0 - - Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn , Uf, dUf_dt, t) - - # ------------------------------- - # --- correction coefficients --- - # ------------------------------- + + Pk_rbm = np.zeros(6 * self.aerogrid['n']) + Pk_cam = Pk_rbm * 0.0 + Pk_cs = Pk_rbm * 0.0 + Pk_f = Pk_rbm * 0.0 + Pk_gust = Pk_rbm * 0.0 + Pk_idrag = Pk_rbm * 0.0 + Pk_unsteady = Pk_rbm * 0.0 + + Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn, Uf, dUf_dt, t) + + # correction coefficients Pb_corr = self.correctioon_coefficients(alpha, beta, q_dyn) - - # --------------------------- - # --- summation of forces --- - # --------------------------- + + # summation of forces Pk_aero = Pk_rbm + Pk_cam + Pk_cs + Pk_f + Pk_gust + Pk_idrag + Pk_unsteady Pmac = np.dot(self.Dkx1.T, Pk_aero) Pb = np.dot(self.PHImac_cg.T, Pmac) + Pb_corr + Pb_ext + np.dot(self.PHIcfd_cg.T, Pcfd) - + g_cg = gravitation_on_earth(self.PHInorm_cg, Tgeo2body) - - # ----------- - # --- EoM --- - # ----------- + + # EoM d2Ucg_dt2, Nxyz = self.rigid_EoM(dUcg_dt, Pb, g_cg, modus) - Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6])) ) + Pf_ext + np.dot(self.PHIcfd_f.T, Pcfd) + Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot(np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6]))) \ + + Pf_ext + np.dot(self.PHIcfd_f.T, Pcfd) d2Uf_dt2 = self.flexible_EoM(dUf_dt, Uf, Pf) - - # ---------------------- - # --- CS derivatives --- - # ---------------------- - dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo,X[6:12])[0:3]) - - # -------------- - # --- output --- - # -------------- - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, + + # CS derivatives + dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo, X[6:12])[0:3]) + + # output + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, Nxyz[2], Vtas, beta, - )) - dy = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, - )) + )) + dy = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, + )) if modus in ['trim']: return Y elif modus in ['trim_full_output']: - response = {'X': X, + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_rbm': Pk_rbm, @@ -106,7 +90,7 @@ def equations(self, X, t, modus): 'Pf': Pf, 'alpha': np.array([alpha]), 'beta': np.array([beta]), - #'Pg_aero': np.dot(PHIk_strc.T, Pk_aero), + # 'Pg_aero': np.dot(PHIk_strc.T, Pk_aero), 'Ux2': Ux2, 'dUcg_dt': dUcg_dt, 'd2Ucg_dt2': d2Ucg_dt2, @@ -118,104 +102,95 @@ def equations(self, X, t, modus): 'Pextra': Pextra, 'Pcfd': Pcfd, 'dy': dy, - } + } return response - + def finalize(self): self.cfd_interface.release_memory() + class CfdUnsteady(CfdSteady): - + def ode_arg_sorter(self, t, X): return self.eval_equations(X, t, 'sim_full_output') def equations(self, X, t, modus): self.counter += 1 # recover states - Tgeo2body, Tbody2geo = self.geo2body(X) - dUcg_dt, Uf, dUf_dt = self.recover_states(X) - Vtas, q_dyn = self.recover_Vtas(X) - onflow = self.recover_onflow(X) - alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) - Ux2 = self.get_Ux2(X) - delta_XYZ = X[self.solution.idx_states[0:3]]-self.X0[self.solution.idx_states[0:3]] - PhiThetaPsi = X[self.solution.idx_states[3:6]] - # -------------------- - # --- aerodynamics --- - # -------------------- + Tgeo2body, Tbody2geo = self.geo2body(X) + dUcg_dt, Uf, dUf_dt = self.recover_states(X) + Vtas, q_dyn = self.recover_Vtas(X) + alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) + Ux2 = self.get_Ux2(X) + delta_XYZ = X[self.solution.idx_states[0:3]] - self.X0[self.solution.idx_states[0:3]] + PhiThetaPsi = X[self.solution.idx_states[3:6]] + + # aerodynamics self.cfd_interface.update_general_para() self.cfd_interface.update_timedom_para() if self.simcase['gust']: - self.cfd_interface.update_gust_para(Vtas, self.WG_TAS*Vtas) + self.cfd_interface.update_gust_para(Vtas, self.WG_TAS * Vtas) self.cfd_interface.init_solver() self.cfd_interface.set_euler_transformation(delta_XYZ, PhiThetaPsi) self.cfd_interface.prepare_meshdefo(Uf, Ux2) # Remember to start SU2 at time step 2, because steps 0 and 1 are taken up by the steady restart solution. # To establish the current time step, we can reuse the existing counter. - self.cfd_interface.run_solver(i_timestep=self.counter+1) + self.cfd_interface.run_solver(i_timestep=self.counter + 1) Pcfd = self.cfd_interface.get_last_solution() - - Pk_rbm = np.zeros(6*self.aerogrid['n']) - Pk_cam = Pk_rbm*0.0 - Pk_cs = Pk_rbm*0.0 - Pk_f = Pk_rbm*0.0 - Pk_gust = Pk_rbm*0.0 - Pk_idrag = Pk_rbm*0.0 - Pk_unsteady = Pk_rbm*0.0 - - Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn , Uf, dUf_dt, t) - - # ------------------------------- - # --- correction coefficients --- - # ------------------------------- + + Pk_rbm = np.zeros(6 * self.aerogrid['n']) + Pk_cam = Pk_rbm * 0.0 + Pk_cs = Pk_rbm * 0.0 + Pk_f = Pk_rbm * 0.0 + Pk_gust = Pk_rbm * 0.0 + Pk_idrag = Pk_rbm * 0.0 + Pk_unsteady = Pk_rbm * 0.0 + + Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn, Uf, dUf_dt, t) + + # correction coefficients Pb_corr = self.correctioon_coefficients(alpha, beta, q_dyn) - - # --------------------------- - # --- summation of forces --- - # --------------------------- + + # summation of forces Pk_aero = Pk_rbm + Pk_cam + Pk_cs + Pk_f + Pk_gust + Pk_idrag + Pk_unsteady Pmac = np.dot(self.Dkx1.T, Pk_aero) Pb = np.dot(self.PHImac_cg.T, Pmac) + Pb_corr + Pb_ext + np.dot(self.PHIcfd_cg.T, Pcfd) - + g_cg = gravitation_on_earth(self.PHInorm_cg, Tgeo2body) - - # ----------- - # --- EoM --- - # ----------- + + # EoM d2Ucg_dt2, Nxyz = self.rigid_EoM(dUcg_dt, Pb, g_cg, modus) - Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6])) ) + Pf_ext + np.dot(self.PHIcfd_f.T, Pcfd) + Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot(np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6]))) \ + + Pf_ext + np.dot(self.PHIcfd_f.T, Pcfd) d2Uf_dt2 = self.flexible_EoM(dUf_dt, Uf, Pf) - - # ---------------------- - # --- CS derivatives --- - # ---------------------- - dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo,X[6:12])[0:3]) - - # -------------- - # --- output --- - # -------------- - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, + + # CS derivatives + dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo, X[6:12])[0:3]) + + # output + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, Nxyz[2], Vtas, beta, - )) - dy = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, - )) - + )) + dy = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, + )) + if modus in ['sim']: return Y - + elif modus in ['sim_full_output']: - # For time domain simulations, typically not all results are required. To reduce the amount of data while maintaining compatibility with the trim, empty arrays are used. - response = {'X': X, + # For time domain simulations, typically not all results are required. To reduce the amount of data while + # maintaining compatibility with the trim, empty arrays are used. + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_aero': Pk_aero, @@ -235,5 +210,5 @@ def equations(self, X, t, modus): 'Pextra': Pextra, 'Pcfd': Pcfd, 'dy': dy, - } - return response \ No newline at end of file + } + return response diff --git a/loadskernel/equations/common.py b/loadskernel/equations/common.py index 792667a..61bfaa3 100644 --- a/loadskernel/equations/common.py +++ b/loadskernel/equations/common.py @@ -1,9 +1,14 @@ -import importlib, os, sys +import importlib +import logging +import os +import numpy as np -from loadskernel.solution_tools import * from loadskernel.cfd_interfaces import tau_interface, su2_interface from loadskernel.engine_interfaces import engine, propeller from loadskernel.io_functions.data_handling import load_hdf5_sparse_matrix, load_hdf5_dict +from loadskernel.solution_tools import calc_drehmatrix, design_gust_cs_25_341, turbulence_cs_25_341, calc_drehmatrix_angular, \ + calc_drehmatrix_angular_inv + class Common(): """ @@ -14,94 +19,112 @@ class Common(): def __init__(self, solution, X0=None): logging.info('Init model equations of type "{}"'.format(self.__class__.__name__)) - self.model = solution.model - self.jcl = solution.jcl - self.trimcase = solution.trimcase - self.X0 = X0 + self.model = solution.model + self.jcl = solution.jcl + self.trimcase = solution.trimcase + self.X0 = X0 # descision/flag if this is a time domain simulation if self.X0 is not None: self.is_sim = True else: self.is_sim = False - self.simcase = solution.simcase + self.simcase = solution.simcase self.trimcond_X = solution.trimcond_X self.trimcond_Y = solution.trimcond_Y - self.solution = solution - self.counter = 0 - + self.solution = solution + # counter for function calls + self.counter = 0 + # convergence parameter for iterative evaluation + self.defo_old = 0.0 + # load data needed for subsequent simulation + self.load_data() + # set-up simulation parameters + self.setup_hingeline() + self.setup_efcs() + self.setup_cfd_interface() + self.setup_gust() + self.setup_controller() + self.setup_aero_matrices() + self.setup_engine_interface() + self.setup_landinggear() + self.setup_sensors() + + def load_data(self): # load data from HDF5 - self.aero = load_hdf5_dict(self.model['aero'][self.trimcase['aero']]) - self.mass = load_hdf5_dict(self.model['mass'][self.trimcase['mass']]) - self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) - - self.aerogrid = load_hdf5_dict(self.model['aerogrid']) - self.macgrid = load_hdf5_dict(self.model['macgrid']) - self.x2grid = load_hdf5_dict(self.model['x2grid']) - self.cggrid = load_hdf5_dict(self.mass['cggrid']) - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) - - self.PHImac_cg = self.mass['PHImac_cg'] - self.PHIcg_mac = self.mass['PHIcg_mac'] + self.aero = load_hdf5_dict(self.model['aero'][self.trimcase['aero']]) + self.mass = load_hdf5_dict(self.model['mass'][self.trimcase['mass']]) + self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) + + self.aerogrid = load_hdf5_dict(self.model['aerogrid']) + self.macgrid = load_hdf5_dict(self.model['macgrid']) + self.x2grid = load_hdf5_dict(self.model['x2grid']) + self.cggrid = load_hdf5_dict(self.mass['cggrid']) + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + + self.PHImac_cg = self.mass['PHImac_cg'] + self.PHIcg_mac = self.mass['PHIcg_mac'] self.PHInorm_cg = self.mass['PHInorm_cg'] self.PHIcg_norm = self.mass['PHIcg_norm'] - self.Mb = self.mass['Mb'] - self.Mff = self.mass['Mff'] - self.Kff = self.mass['Kff'] - self.Dff = self.mass['Dff'] - self.Mhh = self.mass['Mhh'] - self.Khh = self.mass['Khh'] - self.Dhh = self.mass['Dhh'] - self.PHIf_strc = self.mass['PHIf_strc'] + self.Mb = self.mass['Mb'] + self.Mff = self.mass['Mff'] + self.Kff = self.mass['Kff'] + self.Dff = self.mass['Dff'] + self.Mhh = self.mass['Mhh'] + self.Khh = self.mass['Khh'] + self.Dhh = self.mass['Dhh'] + self.PHIf_strc = self.mass['PHIf_strc'] self.PHIstrc_cg = self.mass['PHIstrc_cg'] - self.Mgg = self.mass['MGG'] - self.Mfcg = self.mass['Mfcg'] - self.PHIjf = self.mass['PHIjf'] - self.PHIkf = self.mass['PHIkf'] - self.PHIlf = self.mass['PHIlf'] - self.PHIjh = self.mass['PHIjh'] - self.PHIkh = self.mass['PHIkh'] - self.PHIlh = self.mass['PHIlh'] - self.n_modes = self.mass['n_modes'] - - self.PHIk_strc = load_hdf5_sparse_matrix(self.model['PHIk_strc']) - self.PHIlk = load_hdf5_sparse_matrix(self.model['PHIlk']) - self.Djx1 = self.model['Djx1'][()] - self.Dkx1 = self.model['Dkx1'][()] - self.Djx2 = self.model['Djx2'][()] - self.cam_rad = self.model['camber_twist']['cam_rad'][()] - - self.Qjj = self.aero['Qjj'] - - # set hingeline for cs deflections + self.Mgg = self.mass['MGG'] + self.Mfcg = self.mass['Mfcg'] + self.PHIjf = self.mass['PHIjf'] + self.PHIkf = self.mass['PHIkf'] + self.PHIlf = self.mass['PHIlf'] + self.PHIjh = self.mass['PHIjh'] + self.PHIkh = self.mass['PHIkh'] + self.PHIlh = self.mass['PHIlh'] + self.n_modes = self.mass['n_modes'] + + self.PHIk_strc = load_hdf5_sparse_matrix(self.model['PHIk_strc']) + self.PHIlk = load_hdf5_sparse_matrix(self.model['PHIlk']) + self.Djx1 = self.model['Djx1'][()] + self.Dkx1 = self.model['Dkx1'][()] + self.Djx2 = self.model['Djx2'][()] + self.cam_rad = self.model['camber_twist']['cam_rad'][()] + + self.Qjj = self.aero['Qjj'] + + def setup_hingeline(self): + # set hingeline for cs deflections if 'hingeline' in self.jcl.aero and self.jcl.aero['hingeline'] == 'z': self.hingeline = 'z' - else: # default + else: # default self.hingeline = 'y' - - # import aircraft-specific class from efcs.py dynamically - if 'path' in self.jcl.efcs and sys.version_info[0] >= 3: + + def setup_efcs(self): + # import aircraft-specific class from efcs.py dynamically + if 'path' in self.jcl.efcs: # If a path is specified, import module from that path. - spec = importlib.util.spec_from_file_location(self.jcl.efcs['version'], os.path.join(self.jcl.efcs['path'], self.jcl.efcs['version']+'.py' )) + spec = importlib.util.spec_from_file_location(self.jcl.efcs['version'], + os.path.join(self.jcl.efcs['path'], + self.jcl.efcs['version'] + '.py')) efcs_module = spec.loader.load_module() - elif 'path' in self.jcl.efcs and sys.version_info[0] < 3: - import imp - efcs_module = imp.load_source(self.jcl.efcs['version'], os.path.join(self.jcl.efcs['path'], self.jcl.efcs['version']+'.py' )) - else: + else: logging.error('Please provide an EFCS, see ./doc/jcl_template.py and efcs_template.py for reference.') # init efcs - self.efcs = efcs_module.Efcs() + self.efcs = efcs_module.Efcs() logging.info('Init EFCS "{}"'.format(efcs_module.__name__)) - + # in case we work with cpacs-mona, init the standard mapping if self.jcl.efcs['version'] == 'cpacsmona_standard_efcs': self.efcs.apply_cpasmona_standard_mapping(self.x2grid, self.solution.n_inputs) + def setup_cfd_interface(self): # get cfd splining matrices and cfd solver interface if self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: # get cfd splining matrices self.PHIcfd_strc = load_hdf5_sparse_matrix(self.model['PHIcfd_strc']) - self.PHIcfd_cg = self.mass['PHIcfd_cg'] - self.PHIcfd_f = self.mass['PHIcfd_f'] + self.PHIcfd_cg = self.mass['PHIcfd_cg'] + self.PHIcfd_f = self.mass['PHIcfd_f'] # initialize the interface to a cfd solver if self.jcl.aero['cfd_solver'].lower() == 'tau' and self.jcl.aero['method'] == 'cfd_steady': self.cfd_interface = tau_interface.TauInterface(self.solution) @@ -110,83 +133,90 @@ def __init__(self, solution, X0=None): elif self.jcl.aero['cfd_solver'].lower() == 'su2' and self.jcl.aero['method'] == 'cfd_unsteady': self.cfd_interface = su2_interface.SU2InterfaceFarfieldOnflow(self.solution) else: - logging.error('Interface for CFD solver "{}" and "{}" not implemented!'.format(self.jcl.aero['cfd_solver'], self.jcl.aero['method'])) + logging.error('Interface for CFD solver "{}" and "{}" not implemented!'.format( + self.jcl.aero['cfd_solver'], self.jcl.aero['method'])) - # set-up 1-cos gust + def setup_gust(self): + # set-up 1-cos gust # Vtas aus solution condition berechnen - uvw = np.array(self.trimcond_X[6:9,2], dtype='float') - Vtas = sum(uvw**2)**0.5 + uvw = np.array(self.trimcond_X[6:9, 2], dtype='float') + Vtas = sum(uvw ** 2) ** 0.5 if self.is_sim and 'gust' in self.simcase and self.simcase['gust']: # calculate and set the gust velocities - self.s0 = self.simcase['gust_para']['T1'] * Vtas + self.s0 = self.simcase['gust_para']['T1'] * Vtas if 'WG_TAS' not in self.simcase.keys(): - self.WG_TAS, U_ds, V_gust = design_gust_cs_25_341(self.simcase, self.atmo, Vtas) + self.WG_TAS, _, _ = design_gust_cs_25_341(self.simcase, self.atmo, Vtas) else: self.WG_TAS = self.simcase['WG_TAS'] # write some user information / confirmation - logging.info('Gust set up with initial Vtas = {:.4f}, t1 = {}, WG_tas = {:.4f}'.format(Vtas, self.simcase['gust_para']['T1'], self.WG_TAS)) - - elif self.is_sim and ('turbulence' in self.simcase or 'limit_turbulence' in self.simcase) and (self.simcase['turbulence'] or self.simcase['limit_turbulence']): - self.PHIstrc_mon = load_hdf5_sparse_matrix(self.model['PHIstrc_mon']) - self.mongrid = load_hdf5_dict(self.model['mongrid']) + logging.info('Gust set up with initial Vtas = {:.4f}, t1 = {}, WG_tas = {:.4f}'.format( + Vtas, self.simcase['gust_para']['T1'], self.WG_TAS)) + + elif self.is_sim and ('turbulence' in self.simcase + or 'limit_turbulence' in self.simcase) and (self.simcase['turbulence'] + or self.simcase['limit_turbulence']): + self.PHIstrc_mon = load_hdf5_sparse_matrix(self.model['PHIstrc_mon']) + self.mongrid = load_hdf5_dict(self.model['mongrid']) if 'u_sigma' not in self.simcase.keys(): self.u_sigma = turbulence_cs_25_341(self.simcase, self.atmo, Vtas) else: self.u_sigma = self.simcase['u_sigma'] logging.info('Turbulence set up with initial Vtas = {:.4f} and u_sigma = {:.4f}'.format(Vtas, self.u_sigma)) + def setup_controller(self): # init cs_signal if self.is_sim and 'cs_signal' in self.simcase and self.simcase['cs_signal']: self.efcs.cs_signal_init(self.trimcase['desc']) - + # init controller if self.is_sim and 'controller' in self.simcase and self.simcase['controller']: """ The controller might be set-up in different ways, e.g. to maintain a certain angular acceleration of velocity. Example: self.efcs.controller_init(np.array((0.0,0.0,0.0)), 'angular accelerations') - Example: self.efcs.controller_init(np.dot(self.PHIcg_norm[3:6,3:6],np.dot(calc_drehmatrix_angular(float(self.trimcond_X[3,2]), float(self.trimcond_X[4,2]), float(self.trimcond_X[5,2])), np.array(self.trimcond_X[9:12,2], dtype='float'))), 'angular velocities') """ if self.jcl.efcs['version'] in ['HAP']: - self.efcs.controller_init(command_0=X0[self.solution.idx_inputs], - setpoint_v=float(self.trimcond_Y[np.where(self.trimcond_Y[:,0]=='Vtas')[0][0], 2]), - setpoint_h=-X0[np.where(self.trimcond_X[:,0]=='z')[0][0]], - ) + self.efcs.controller_init(command_0=self.X0[self.solution.idx_inputs], + setpoint_v=float( + self.trimcond_Y[np.where(self.trimcond_Y[:, 0] == 'Vtas')[0][0], 2]), + setpoint_h=-self.X0[np.where(self.trimcond_X[:, 0] == 'z')[0][0]], + ) elif self.jcl.efcs['version'] in ['HAP_FMU']: - setpoint = {'pqr': X0[self.solution.idx_states[9:12]], - 'PhiThetaPsi': X0[self.solution.idx_states[3:6]], - 'z': X0[self.solution.idx_states[2]], - 'Vtas': float(self.trimcond_Y[np.where(self.trimcond_Y[:,0]=='Vtas')[0][0], 2]), - 'commands': X0[self.solution.idx_inputs], - 'Nxyz': np.array([0.0, 0.0, float(self.trimcond_Y[np.where(self.trimcond_Y[:,0]=='Nz')[0][0], 2])]), - 'dz': float(self.trimcond_Y[np.where(self.trimcond_Y[:,0]=='dz')[0][0], 2]), - } - + setpoint = {'pqr': self.X0[self.solution.idx_states[9:12]], + 'PhiThetaPsi': self.X0[self.solution.idx_states[3:6]], + 'z': self.X0[self.solution.idx_states[2]], + 'Vtas': float(self.trimcond_Y[np.where(self.trimcond_Y[:, 0] == 'Vtas')[0][0], 2]), + 'commands': self.X0[self.solution.idx_inputs], + 'Nxyz': np.array([0.0, 0.0, float( + self.trimcond_Y[np.where(self.trimcond_Y[:, 0] == 'Nz')[0][0], 2])]), + 'dz': float(self.trimcond_Y[np.where(self.trimcond_Y[:, 0] == 'dz')[0][0], 2]), + } + self.efcs.fmu_init(filename_fmu=self.jcl.efcs['filename_fmu'], filename_actuator=self.jcl.efcs['filename_actuator'], setpoint=setpoint) elif self.jcl.efcs['version'] in ['FFD']: self.efcs.controller_init( - setpoint={'pqr': X0[self.solution.idx_states[9:12]], - 'PhiThetaPsi': X0[self.solution.idx_states[3:6]], - 'altitude_key': self.trimcase['altitude'], - 'velocity_key': self.trimcase['Ma'], - 'mass_key': self.trimcase['mass'], - 'commands': X0[self.solution.idx_inputs], - 'Nxyz': np.array([0.0, 0.0, float(self.trimcond_Y[np.where(self.trimcond_Y[:, 0] == 'Nz')[0][0], 2])]), + setpoint={'pqr': self.X0[self.solution.idx_states[9:12]], + 'PhiThetaPsi': self.X0[self.solution.idx_states[3:6]], + 'altitude_key': self.trimcase['altitude'], + 'velocity_key': self.trimcase['Ma'], + 'mass_key': self.trimcase['mass'], + 'commands': self.X0[self.solution.idx_inputs], + 'Nxyz': np.array([0.0, 0.0, float( + self.trimcond_Y[np.where(self.trimcond_Y[:, 0] == 'Nz')[0][0], 2])]), }) else: logging.error('Unknown EFCS: {}'.format(self.jcl.efcs['version'])) - - # convergence parameter for iterative evaluation - self.defo_old = 0.0 - + + def setup_aero_matrices(self): if self.jcl.aero['method'] in ['mona_steady', 'mona_unsteady', 'freq_dom']: self.Djf_1 = self.aerogrid['Nmat'].dot(self.aerogrid['Rmat'].dot(self.PHIjf)) - self.Djf_2 = self.aerogrid['Nmat'].dot(self.PHIjf)* -1.0 + self.Djf_2 = self.aerogrid['Nmat'].dot(self.PHIjf) * -1.0 self.Djh_1 = self.aerogrid['Nmat'].dot(self.aerogrid['Rmat'].dot(self.PHIjh)) self.Djh_2 = self.aerogrid['Nmat'].dot(self.PHIjh) * -1.0 - + + def setup_engine_interface(self): if hasattr(self.jcl, 'engine'): self.engine_loads = engine.EngineLoads() if self.jcl.engine['method'] == 'propellerdisk': @@ -195,42 +225,43 @@ def __init__(self, solution, X0=None): self.propeller_aero_loads = propeller.PyPropMat4Loads(self.jcl.engine['propeller_input_file']) self.propeller_precession_loads = propeller.PropellerPrecessionLoads() elif self.jcl.engine['method'] == 'VLM4Prop': - self.propeller_aero_loads = propeller.VLM4PropLoads(self.model.prop, self.i_atmo) + self.propeller_aero_loads = propeller.VLM4PropLoads(self.model.prop) self.propeller_precession_loads = propeller.PropellerPrecessionLoads() - + + def setup_landinggear(self): if hasattr(self.jcl, 'landinggear') or hasattr(self.jcl, 'engine'): self.extragrid = load_hdf5_dict(self.model['extragrid']) self.PHIextra_cg = self.mass['PHIextra_cg'] - self.PHIf_extra = self.mass['PHIf_extra'] - + self.PHIf_extra = self.mass['PHIf_extra'] + + def setup_sensors(self): if hasattr(self.jcl, 'sensor'): self.sensorgrid = load_hdf5_dict(self.model['sensorgrid']) self.PHIsensor_cg = self.mass['PHIsensor_cg'] self.PHIf_sensor = self.mass['PHIf_sensor'] - - + def ode_arg_sorter(self, t, X): return self.eval_equations(X, t, 'sim') - + def calc_Pk_nonlin(self, dUmac_dt, wj): - Ujx1 = np.dot(self.Djx1,dUmac_dt) - q = Ujx1[self.aerogrid['set_j'][:,(0,1,2)]] + Ujx1 = np.dot(self.Djx1, dUmac_dt) + q = Ujx1[self.aerogrid['set_j'][:, (0, 1, 2)]] r = self.aerogrid['r'] rho = self.atmo['rho'] Gamma = self.aero['Gamma_jj'] - Pl = np.zeros(self.aerogrid['n']*6) - Pl[self.aerogrid['set_l'][:,0]] = rho * Gamma.dot(wj) * np.cross(q, r)[:,0] - Pl[self.aerogrid['set_l'][:,1]] = rho * Gamma.dot(wj) * np.cross(q, r)[:,1] - Pl[self.aerogrid['set_l'][:,2]] = rho * Gamma.dot(wj) * np.cross(q, r)[:,2] + Pl = np.zeros(self.aerogrid['n'] * 6) + Pl[self.aerogrid['set_l'][:, 0]] = rho * Gamma.dot(wj) * np.cross(q, r)[:, 0] + Pl[self.aerogrid['set_l'][:, 1]] = rho * Gamma.dot(wj) * np.cross(q, r)[:, 1] + Pl[self.aerogrid['set_l'][:, 2]] = rho * Gamma.dot(wj) * np.cross(q, r)[:, 2] Pk = self.PHIlk.T.dot(Pl) return Pk def calc_Pk(self, q_dyn, wj): - fl = q_dyn * self.aerogrid['N'].T*self.aerogrid['A']*np.dot(self.Qjj, wj) - Pl = np.zeros(self.aerogrid['n']*6) - Pl[self.aerogrid['set_l'][:,0]] = fl[0,:] - Pl[self.aerogrid['set_l'][:,1]] = fl[1,:] - Pl[self.aerogrid['set_l'][:,2]] = fl[2,:] + fl = q_dyn * self.aerogrid['N'].T * self.aerogrid['A'] * np.dot(self.Qjj, wj) + Pl = np.zeros(self.aerogrid['n'] * 6) + Pl[self.aerogrid['set_l'][:, 0]] = fl[0, :] + Pl[self.aerogrid['set_l'][:, 1]] = fl[1, :] + Pl[self.aerogrid['set_l'][:, 2]] = fl[2, :] if self.trimcase['Ma'] >= 1.0: # supersonic aero with NP at 50% (ZONA51) Pk = Pl @@ -240,122 +271,133 @@ def calc_Pk(self, q_dyn, wj): return Pk def rbm_nonlin(self, dUcg_dt, alpha, Vtas): - dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed - Ujx1 = np.dot(self.Djx1,dUmac_dt) - # der downwash wj ist nur die Komponente von Uj, welche senkrecht zum Panel steht! - # --> mit N multiplizieren und danach die Norm bilden - wj = np.sum(self.aerogrid['N'][:] * Ujx1[self.aerogrid['set_j'][:,(0,1,2)]],axis=1) + dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed + Ujx1 = np.dot(self.Djx1, dUmac_dt) + # der downwash wj ist nur die Komponente von Uj, welche senkrecht zum Panel steht! + # --> mit N multiplizieren und danach die Norm bilden + wj = np.sum(self.aerogrid['N'][:] * Ujx1[self.aerogrid['set_j'][:, (0, 1, 2)]], axis=1) Pk = self.calc_Pk_nonlin(dUmac_dt, wj) return Pk, wj - + def rbm(self, dUcg_dt, alpha, q_dyn, Vtas): - dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed - Ujx1 = np.dot(self.Djx1,dUmac_dt) - # der downwash wj ist nur die Komponente von Uj, welche senkrecht zum Panel steht! - # --> mit N multiplizieren und danach die Norm bilden - wj = np.sum(self.aerogrid['N'][:] * Ujx1[self.aerogrid['set_j'][:,(0,1,2)]],axis=1) / Vtas * -1 + dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed + Ujx1 = np.dot(self.Djx1, dUmac_dt) + # der downwash wj ist nur die Komponente von Uj, welche senkrecht zum Panel steht! + # --> mit N multiplizieren und danach die Norm bilden + wj = np.sum(self.aerogrid['N'][:] * Ujx1[self.aerogrid['set_j'][:, (0, 1, 2)]], axis=1) / Vtas * -1 Pk = self.calc_Pk(q_dyn, wj) return Pk, wj - + def camber_twist_nonlin(self, dUcg_dt): - wj = np.sin(self.cam_rad) * -1.0 - dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed + wj = np.sin(self.cam_rad) * -1.0 + dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed Pk = self.calc_Pk_nonlin(dUmac_dt, wj) return Pk, wj - + def camber_twist(self, q_dyn): """ - Multiplication with the sign of the z-direction of the panel normal vector ensures compatibility - with aerodynamic models that are NOT constructed from the left to the right. However, this is not - the ultimate solution, because it will fail for vertical surfaces with camber + twist. Fortunately, I can't - think of any twisted vertical tail or a vertical tail has a cambered airfoil. + Multiplication with the sign of the z-direction of the panel normal vector ensures compatibility + with aerodynamic models that are NOT constructed from the left to the right. However, this is not + the ultimate solution, because it will fail for vertical surfaces with camber + twist. Fortunately, I can't + think of any twisted vertical tail or a vertical tail has a cambered airfoil. """ - wj = np.sin(self.cam_rad) * np.sign(self.aerogrid['N'][:,2]) + wj = np.sin(self.cam_rad) * np.sign(self.aerogrid['N'][:, 2]) Pk = self.calc_Pk(q_dyn, wj) return Pk, wj - + def cs_nonlin(self, dUcg_dt, X, Ux2, Vtas): wj = np.zeros(self.aerogrid['n']) # Hier gibt es zwei Wege und es wird je Steuerflaeche unterschieden: - # a) es liegen Daten in der AeroDB vor -> Kraefte werden interpoliert, dann zu Pk addiert, downwash vector bleibt unveraendert - # b) der downwash der Steuerflaeche wird berechnet, zum downwash vector addiert + # a) es liegen Daten in der AeroDB vor -> Kraefte werden interpoliert, dann zu Pk addiert, downwash vector bleibt + # unveraendert + # b) der downwash der Steuerflaeche wird berechnet, zum downwash vector addiert for i_x2 in range(len(self.efcs.keys)): # b) use DLM solution if self.hingeline == 'y': - Ujx2 = np.dot(self.Djx2[i_x2],[0,0,0,0,Ux2[i_x2],0]) + Ujx2 = np.dot(self.Djx2[i_x2], [0, 0, 0, 0, Ux2[i_x2], 0]) elif self.hingeline == 'z': - Ujx2 = np.dot(self.Djx2[i_x2],[0,0,0,0,0,Ux2[i_x2]]) - # Rotationen ry und rz verursachen Luftkraefte. Rotation rx hat keinen Einfluss, wenn die Stoemung von vorne kommt... + Ujx2 = np.dot(self.Djx2[i_x2], [0, 0, 0, 0, 0, Ux2[i_x2]]) + # Rotationen ry und rz verursachen Luftkraefte. Rotation rx hat keinen Einfluss, wenn die Stoemung von vorne + # kommt... # Mit der Norm von wj geht das Vorzeichen verloren - dies ist aber fuer den Steuerflaechenausschlag wichtig. - wj += self.x2grid['eff'][i_x2] * np.sign(Ux2[i_x2]) * np.sqrt(np.sin(Ujx2[self.aerogrid['set_j'][:,4]])**2.0 + \ - np.sin(Ujx2[self.aerogrid['set_j'][:,5]])**2.0) * -Vtas - dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed + wj += self.x2grid['eff'][i_x2] * np.sign(Ux2[i_x2]) \ + * np.sqrt(np.sin(Ujx2[self.aerogrid['set_j'][:, 4]]) ** 2.0 + + np.sin(Ujx2[self.aerogrid['set_j'][:, 5]]) ** 2.0) * -Vtas + dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # auch bodyfixed Pk = self.calc_Pk_nonlin(dUmac_dt, wj) return Pk, wj - + def cs(self, X, Ux2, q_dyn): wj = np.zeros(self.aerogrid['n']) # Hier gibt es zwei Wege und es wird je Steuerflaeche unterschieden: - # a) es liegen Daten in der AeroDB vor -> Kraefte werden interpoliert, dann zu Pk addiert, downwash vector bleibt unveraendert - # b) der downwash der Steuerflaeche wird berechnet, zum downwash vector addiert + # a) es liegen Daten in der AeroDB vor -> Kraefte werden interpoliert, dann zu Pk addiert, downwash vector bleibt + # unveraendert + # b) der downwash der Steuerflaeche wird berechnet, zum downwash vector addiert for i_x2 in range(len(self.efcs.keys)): # b) use DLM solution if self.hingeline == 'y': - Ujx2 = np.dot(self.Djx2[i_x2],[0,0,0,0,Ux2[i_x2],0]) + Ujx2 = np.dot(self.Djx2[i_x2], [0, 0, 0, 0, Ux2[i_x2], 0]) elif self.hingeline == 'z': - Ujx2 = np.dot(self.Djx2[i_x2],[0,0,0,0,0,Ux2[i_x2]]) - wj += self.x2grid['eff'][i_x2] * np.sum(self.aerogrid['N'][:] * np.cross(Ujx2[self.aerogrid['set_j'][:,(3,4,5)]], np.array([-1.,0.,0.])),axis=1) + Ujx2 = np.dot(self.Djx2[i_x2], [0, 0, 0, 0, 0, Ux2[i_x2]]) + wj += self.x2grid['eff'][i_x2] * np.sum(self.aerogrid['N'][:] + * np.cross(Ujx2[self.aerogrid['set_j'][:, (3, 4, 5)]], + np.array([-1., 0., 0.])), axis=1) Pk = self.calc_Pk(q_dyn, wj) return Pk, wj - + def flexible_nonlin(self, dUcg_dt, Uf, dUf_dt, Vtas): if 'flex' in self.jcl.aero and self.jcl.aero['flex']: dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # modale Verformung - Ujf = np.dot(self.PHIjf, Uf ) - wjf_1 = np.sum(self.aerogrid['N'][:] * np.cross(Ujf[self.aerogrid['set_j'][:,(3,4,5)]], dUmac_dt[0:3]),axis=1) * -1 + Ujf = np.dot(self.PHIjf, Uf) + wjf_1 = np.sum(self.aerogrid['N'][:] * np.cross(Ujf[self.aerogrid['set_j'][:, (3, 4, 5)]], + dUmac_dt[0:3]), axis=1) * -1 # modale Bewegung # zu Ueberpruefen, da Trim bisher in statischer Ruhelage und somit dUf_dt = 0 - dUjf_dt = np.dot(self.PHIjf, dUf_dt ) # viel schneller! - wjf_2 = np.sum(self.aerogrid['N'][:] * dUjf_dt[self.aerogrid['set_j'][:,(0,1,2)]],axis=1) - + dUjf_dt = np.dot(self.PHIjf, dUf_dt) # viel schneller! + wjf_2 = np.sum(self.aerogrid['N'][:] * dUjf_dt[self.aerogrid['set_j'][:, (0, 1, 2)]], axis=1) + wj = wjf_1 + wjf_2 Pk = self.calc_Pk_nonlin(dUmac_dt, wj) else: - Pk = np.zeros(self.aerogrid['n']*6) + Pk = np.zeros(self.aerogrid['n'] * 6) wj = np.zeros(self.aerogrid['n']) return Pk, wj - + def flexible(self, Uf, dUf_dt, dUcg_dt, q_dyn, Vtas): wj = np.zeros(self.aerogrid['n']) if 'flex' in self.jcl.aero and self.jcl.aero['flex']: dUmac_dt = np.dot(self.PHImac_cg, dUcg_dt) # modale Verformung - Ujf = np.dot(self.PHIjf, Uf ) - wjf_1 = np.sum(self.aerogrid['N'][:] * np.cross(Ujf[self.aerogrid['set_j'][:,(3,4,5)]], dUmac_dt[0:3]),axis=1) / Vtas + Ujf = np.dot(self.PHIjf, Uf) + wjf_1 = np.sum(self.aerogrid['N'][:] * np.cross(Ujf[self.aerogrid['set_j'][:, (3, 4, 5)]], + dUmac_dt[0:3]), axis=1) / Vtas # modale Bewegung # zu Ueberpruefen, da Trim bisher in statischer Ruhelage und somit dUf_dt = 0 - dUjf_dt = np.dot(self.PHIjf, dUf_dt ) # viel schneller! - wjf_2 = np.sum(self.aerogrid['N'][:] * dUjf_dt[self.aerogrid['set_j'][:,(0,1,2)]],axis=1) / Vtas * -1 + dUjf_dt = np.dot(self.PHIjf, dUf_dt) # viel schneller! + wjf_2 = np.sum(self.aerogrid['N'][:] * dUjf_dt[self.aerogrid['set_j'][:, (0, 1, 2)]], axis=1) / Vtas * -1 wj = wjf_1 + wjf_2 Pk = self.calc_Pk(q_dyn, wj) return Pk, wj - + def gust(self, X, q_dyn): wj = np.zeros(self.aerogrid['n']) if self.is_sim and 'gust' in self.simcase and self.simcase['gust']: # Eintauchtiefe in die Boe berechnen - s_gust = (X[0] - self.aerogrid['offset_j'][:,0] - self.s0) + s_gust = (X[0] - self.aerogrid['offset_j'][:, 0] - self.s0) # downwash der 1-cos Boe auf ein jedes Panel berechnen - wj_gust = self.WG_TAS * 0.5 * (1-np.cos(np.pi * s_gust / self.simcase['gust_gradient'])) + wj_gust = self.WG_TAS * 0.5 * (1 - np.cos(np.pi * s_gust / self.simcase['gust_gradient'])) wj_gust[np.where(s_gust <= 0.0)] = 0.0 - wj_gust[np.where(s_gust > 2*self.simcase['gust_gradient'])] = 0.0 + wj_gust[np.where(s_gust > 2 * self.simcase['gust_gradient'])] = 0.0 # Ausrichtung der Boe fehlt noch - gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0,0,1]), calc_drehmatrix( self.simcase['gust_orientation']/180.0*np.pi, 0.0, 0.0 )), axis=1) - wj = wj_gust * gust_direction_vector + gust_direction_vector = np.sum(self.aerogrid['N'] + * np.dot(np.array([0, 0, 1]), + calc_drehmatrix(self.simcase['gust_orientation'] + / 180.0 * np.pi, 0.0, 0.0)), axis=1) + wj = wj_gust * gust_direction_vector Pk = self.calc_Pk(q_dyn, wj) return Pk, wj - + def windsensor(self, X, Vtas, Uf, dUf_dt): """ Definitions @@ -368,225 +410,222 @@ def windsensor(self, X, Vtas, Uf, dUf_dt): u, v, w = self.get_sensor_onflow(i_sensor, X, Vtas, Uf, dUf_dt) else: # if no sensors are present, then take only rigid body motion as input - u, v, w = X[6:9] # u v w bodyfixed + u, v, w = X[6:9] # u v w bodyfixed - alpha = np.arctan(w/u) - beta = np.arctan(v/u) - gamma = X[4] - alpha # alpha = theta - gamma + alpha = np.arctan(w / u) + beta = np.arctan(v / u) + gamma = X[4] - alpha # alpha = theta - gamma return alpha, beta, gamma - + def get_sensor_onflow(self, i_sensor, X, Vtas, Uf, dUf_dt): # rigid - u, v, w = self.PHIsensor_cg.dot(X[6:12])[self.sensorgrid['set'][i_sensor,0:3]] # velocity sensor attachment point + u, v, w = self.PHIsensor_cg.dot(X[6:12])[self.sensorgrid['set'][i_sensor, 0:3]] # velocity sensor attachment point # additional wind from flexible deformation - uf_1, vf_1, wf_1 = np.cross(self.PHIf_sensor.T.dot(Uf)[self.sensorgrid['set'][i_sensor,(3,4,5)]], X[6:9]).dot(self.PHIcg_norm[:3,:3]) + _, vf_1, wf_1 = np.cross(self.PHIf_sensor.T.dot(Uf)[self.sensorgrid['set'][i_sensor, (3, 4, 5)]], + X[6:9]).dot(self.PHIcg_norm[:3, :3]) # additional wind from flexible velocity - uf_2, vf_2, wf_2 = self.PHIf_sensor.T.dot(dUf_dt)[self.sensorgrid['set'][i_sensor,0:3]].dot(self.PHIcg_norm[:3,:3]) - + _, vf_2, wf_2 = self.PHIf_sensor.T.dot(dUf_dt)[self.sensorgrid['set'][i_sensor, 0:3]].dot(self.PHIcg_norm[:3, :3]) + v += vf_1 + vf_2 w += wf_1 + wf_2 if self.is_sim and 'gust' in self.simcase and self.simcase['gust']: # Eintauchtiefe in die Boe berechnen, analog zu gust() - s_gust = (X[0] - self.sensorgrid['offset'][i_sensor,0] - self.s0) + s_gust = (X[0] - self.sensorgrid['offset'][i_sensor, 0] - self.s0) # downwash der 1-cos Boe an der Sensorposition, analog zu gust() - wj_gust = self.WG_TAS * 0.5 * (1-np.cos(np.pi * s_gust / self.simcase['gust_gradient'])) - if s_gust <= 0.0: + wj_gust = self.WG_TAS * 0.5 * (1 - np.cos(np.pi * s_gust / self.simcase['gust_gradient'])) + if s_gust <= 0.0: wj_gust = 0.0 - if s_gust > 2*self.simcase['gust_gradient']: + if s_gust > 2 * self.simcase['gust_gradient']: wj_gust = 0.0 # Ausrichtung und Skalierung der Boe - u_gust, v_gust, w_gust = Vtas * wj_gust * np.dot(np.array([0,0,1]), calc_drehmatrix( self.simcase['gust_orientation']/180.0*np.pi, 0.0, 0.0 )) + _, v_gust, w_gust = Vtas * wj_gust * np.dot(np.array([0, 0, 1]), + calc_drehmatrix(self.simcase['gust_orientation'] + / 180.0 * np.pi, 0.0, 0.0)) v -= v_gust w += w_gust return u, v, w - + def idrag(self, wj, q_dyn): - if self.jcl.aero['method_AIC'] in ['vlm', 'dlm', 'ae'] and 'induced_drag' in self.jcl.aero and self.jcl.aero['induced_drag']: - Bjj = self.aero['Bjj'] - cp = np.dot(self.Qjj, wj) # gesammtes cp durch gesammten downwash wj + if self.jcl.aero['method_AIC'] in ['vlm', 'dlm'] and 'induced_drag' in self.jcl.aero and self.jcl.aero['induced_drag']: + Bjj = self.aero['Bjj'] + cp = np.dot(self.Qjj, wj) # gesammtes cp durch gesammten downwash wj wj_ind = np.dot(Bjj, cp) - cf = -wj_ind*cp - fld = q_dyn*self.aerogrid['A']*cf - Pld = np.zeros(self.aerogrid['n']*6) + cf = -wj_ind * cp + fld = q_dyn * self.aerogrid['A'] * cf + Pld = np.zeros(self.aerogrid['n'] * 6) # Der induzierte Widerstand greift in x-Richtung an. Gibt es hierfuer vielleicht eine bessere/generische Loesung? - Pld[self.aerogrid['set_l'][:,0]] = fld - + Pld[self.aerogrid['set_l'][:, 0]] = fld + Pk_idrag = self.PHIlk.T.dot(Pld) else: - Pk_idrag = np.zeros(self.aerogrid['n']*6) - + Pk_idrag = np.zeros(self.aerogrid['n'] * 6) + return Pk_idrag - + def unsteady(self, X, t, wj, Uf, dUf_dt, onflow, q_dyn, Vtas): if 'method_rfa' in self.jcl.aero and self.jcl.aero['method_rfa'] == 'generalized': logging.error('Generalized RFA not yet implemented.') if 'method_rfa' in self.jcl.aero and self.jcl.aero['method_rfa'] == 'halfgeneralized': - Pk_unsteady, Pk_unsteady_B, Pk_unsteady_D, dlag_states_dt = self.unsteady_halfgeneralized(X, t, Uf, dUf_dt, onflow, q_dyn, Vtas) - else: # 'physical' -# dUmac_dt = np.dot(self.PHImac_cg, onflow) -# # modale Verformung -# Ujf = np.dot(self.PHIjf, Uf ) -# wjf_1 = np.sum(self.aerogrid['N'][:] * np.cross(Ujf[self.aerogrid['set_j'][:,(3,4,5)]], dUmac_dt[0:3]),axis=1) / Vtas -# # modale Bewegung -# dUjf_dt = np.dot(self.PHIjf, dUf_dt ) # viel schneller! -# wjf_2 = np.sum(self.aerogrid['N'][:] * dUjf_dt[self.aerogrid['set_j'][:,(0,1,2)]],axis=1) / Vtas * -1.0 -# wjf = wjf_1 + wjf_2 - Pk_unsteady, Pk_unsteady_B, Pk_unsteady_D, dlag_states_dt = self.unsteady_pyhsical(X, t, wj, q_dyn, Vtas) - + Pk_unsteady, _, _, dlag_states_dt = self.unsteady_halfgeneralized(X, t, Uf, dUf_dt, onflow, q_dyn, Vtas) + else: + # 'physical' + Pk_unsteady, _, _, dlag_states_dt = self.unsteady_pyhsical(X, t, wj, q_dyn, Vtas) + return Pk_unsteady, dlag_states_dt def unsteady_halfgeneralized(self, X, t, Uf, dUf_dt, dUcg_dt, q_dyn, Vtas): - n_modes = self.n_modes - n_poles = self.aero['n_poles'] - betas = self.aero['betas'] - ABCD = self.aero['ABCD'] - c_ref = self.jcl.general['c_ref'] + n_modes = self.n_modes + n_poles = self.aero['n_poles'] + betas = self.aero['betas'] + ABCD = self.aero['ABCD'] + c_ref = self.jcl.general['c_ref'] # There are lag states for the rotational motion (_1) and for the translational motion (_2). # This is to separate the lag states as the AIC matrices need to be generalized differently for the two cases. # In addition, the lag states depend on the generalized velocity and the generalized acceleration. - lag_states_1 = X[self.solution.idx_lag_states[:int(self.solution.n_lag_states/2)]].reshape((n_modes,n_poles)) - lag_states_2 = X[self.solution.idx_lag_states[int(self.solution.n_lag_states/2):]].reshape((n_modes,n_poles)) - c_over_Vtas = (0.5*c_ref)/Vtas - if t <= 0.0: # initial step - self.t_old = np.copy(t) + lag_states_1 = X[self.solution.idx_lag_states[:int(self.solution.n_lag_states / 2)]].reshape((n_modes, n_poles)) + lag_states_2 = X[self.solution.idx_lag_states[int(self.solution.n_lag_states / 2):]].reshape((n_modes, n_poles)) + c_over_Vtas = (0.5 * c_ref) / Vtas + if t <= 0.0: # initial step + self.t_old = np.copy(t) self.dUf_dt_old = np.copy(dUf_dt) self.d2Uf_d2t_old = np.zeros(n_modes) - + dt = t - self.t_old # d2Uf_d2t mittels "backward differences" berechnen - if dt > 0.0: # solver laeuft vorwaerts - d2Uf_d2t = (dUf_dt-self.dUf_dt_old) / dt + if dt > 0.0: # solver laeuft vorwaerts + d2Uf_d2t = (dUf_dt - self.dUf_dt_old) / dt self.d2Uf_d2t_old = np.copy(d2Uf_d2t) - - else: # solver bleibt stehen oder laeuft zurueck + + else: # solver bleibt stehen oder laeuft zurueck d2Uf_d2t = self.d2Uf_d2t_old # save for next step - self.t_old = np.copy(t) + self.t_old = np.copy(t) self.dUf_dt_old = np.copy(dUf_dt) # B - Daemfungsterm - cp_unsteady = ABCD[1,:,:].dot(self.Djf_1).dot(dUf_dt) * c_over_Vtas \ - + ABCD[1,:,:].dot(self.Djf_2).dot(d2Uf_d2t) / Vtas * -1.0 * c_over_Vtas - flunsteady = q_dyn * self.aerogrid['N'].T*self.aerogrid['A']*cp_unsteady - Plunsteady = np.zeros((6*self.aerogrid['n'])) - Plunsteady[self.aerogrid['set_l'][:,0]] = flunsteady[0,:] - Plunsteady[self.aerogrid['set_l'][:,1]] = flunsteady[1,:] - Plunsteady[self.aerogrid['set_l'][:,2]] = flunsteady[2,:] + cp_unsteady = ABCD[1, :, :].dot(self.Djf_1).dot(dUf_dt) * c_over_Vtas \ + + ABCD[1, :, :].dot(self.Djf_2).dot(d2Uf_d2t) / Vtas * -1.0 * c_over_Vtas + flunsteady = q_dyn * self.aerogrid['N'].T * self.aerogrid['A'] * cp_unsteady + Plunsteady = np.zeros((6 * self.aerogrid['n'])) + Plunsteady[self.aerogrid['set_l'][:, 0]] = flunsteady[0, :] + Plunsteady[self.aerogrid['set_l'][:, 1]] = flunsteady[1, :] + Plunsteady[self.aerogrid['set_l'][:, 2]] = flunsteady[2, :] Pk_unsteady_B = self.PHIlk.T.dot(Plunsteady) - + # C - Beschleunigungsterm -entfaellt - - + # D1-Dn - lag states dwff_dt_1 = dUf_dt - dlag_states_dt_1 = dwff_dt_1.repeat(n_poles).reshape((n_modes, n_poles)) - betas*lag_states_1/c_over_Vtas + dlag_states_dt_1 = dwff_dt_1.repeat(n_poles).reshape((n_modes, n_poles)) - betas * lag_states_1 / c_over_Vtas dlag_states_dt_1 = dlag_states_dt_1.reshape((-1)) - + dwff_dt_2 = d2Uf_d2t / Vtas * -1.0 - dlag_states_dt_2 = dwff_dt_2.repeat(n_poles).reshape((n_modes, n_poles)) - betas*lag_states_2/c_over_Vtas + dlag_states_dt_2 = dwff_dt_2.repeat(n_poles).reshape((n_modes, n_poles)) - betas * lag_states_2 / c_over_Vtas dlag_states_dt_2 = dlag_states_dt_2.reshape((-1)) - + dlag_states_dt = np.concatenate((dlag_states_dt_1, dlag_states_dt_2)) - + D_dot_lag = np.zeros(self.aerogrid['n']) for i_pole in np.arange(0, n_poles): - D_dot_lag += ABCD[3+i_pole,:,:].dot(self.Djf_1).dot(lag_states_1[:,i_pole]) \ - + ABCD[3+i_pole,:,:].dot(self.Djf_2).dot(lag_states_2[:,i_pole]) - cp_unsteady = D_dot_lag - flunsteady = q_dyn * self.aerogrid['N'].T*self.aerogrid['A']*cp_unsteady - Plunsteady = np.zeros(6*self.aerogrid['n']) - Plunsteady[self.aerogrid['set_l'][:,0]] = flunsteady[0,:] - Plunsteady[self.aerogrid['set_l'][:,1]] = flunsteady[1,:] - Plunsteady[self.aerogrid['set_l'][:,2]] = flunsteady[2,:] + D_dot_lag += ABCD[3 + i_pole, :, :].dot(self.Djf_1).dot(lag_states_1[:, i_pole]) \ + + ABCD[3 + i_pole, :, :].dot(self.Djf_2).dot(lag_states_2[:, i_pole]) + cp_unsteady = D_dot_lag + flunsteady = q_dyn * self.aerogrid['N'].T * self.aerogrid['A'] * cp_unsteady + Plunsteady = np.zeros(6 * self.aerogrid['n']) + Plunsteady[self.aerogrid['set_l'][:, 0]] = flunsteady[0, :] + Plunsteady[self.aerogrid['set_l'][:, 1]] = flunsteady[1, :] + Plunsteady[self.aerogrid['set_l'][:, 2]] = flunsteady[2, :] Pk_unsteady_D = self.PHIlk.T.dot(Plunsteady) - - Pk_unsteady = Pk_unsteady_D + Pk_unsteady_B + + Pk_unsteady = Pk_unsteady_D + Pk_unsteady_B return Pk_unsteady, Pk_unsteady_B, Pk_unsteady_D, dlag_states_dt - + def unsteady_pyhsical(self, X, t, wj, q_dyn, Vtas): - n_j = self.aerogrid['n'] - n_poles = self.aero['n_poles'] - betas = self.aero['betas'] - ABCD = self.aero['ABCD'] - c_ref = self.jcl.general['c_ref'] - - lag_states = X[self.solution.idx_lag_states].reshape((n_j,n_poles)) - c_over_Vtas = (0.5*c_ref)/Vtas - if t <= 0.0: # initial step - self.t_old = np.copy(t) - self.wj_old = np.copy(wj) + n_j = self.aerogrid['n'] + n_poles = self.aero['n_poles'] + betas = self.aero['betas'] + ABCD = self.aero['ABCD'] + c_ref = self.jcl.general['c_ref'] + + lag_states = X[self.solution.idx_lag_states].reshape((n_j, n_poles)) + c_over_Vtas = (0.5 * c_ref) / Vtas + if t <= 0.0: # initial step + self.t_old = np.copy(t) + self.wj_old = np.copy(wj) self.dwj_dt_old = np.zeros(n_j) - self.dlag_states_dt_old = np.zeros(n_j*n_poles) - + self.dlag_states_dt_old = np.zeros(n_j * n_poles) + dt = t - self.t_old # dwj_dt mittels "backward differences" berechnen - if dt > 0.0: # solver laeuft vorwaerts + if dt > 0.0: # solver laeuft vorwaerts dwj_dt = (wj - self.wj_old) / dt self.dwj_dt_old = np.copy(dwj_dt) - else: # solver bleibt stehen oder laeuft zurueck + else: # solver bleibt stehen oder laeuft zurueck dwj_dt = self.dwj_dt_old - - # dwj_dt = (wj - self.wj_old) / dt - # # guard for NaNs and Infs as we divide by dt, which might be zero... - # dwj_dt[np.isnan(dwj_dt)] = 0.0 - # dwj_dt[np.isinf(dwj_dt)] = 0.0 - + # save for next step - self.t_old = np.copy(t) + self.t_old = np.copy(t) self.wj_old = np.copy(wj) - + # B - Daemfungsterm - cp_unsteady = ABCD[1,:,:].dot(dwj_dt) * c_over_Vtas - flunsteady = q_dyn * self.aerogrid['N'].T*self.aerogrid['A']*cp_unsteady - Plunsteady = np.zeros((6*self.aerogrid['n'])) - Plunsteady[self.aerogrid['set_l'][:,0]] = flunsteady[0,:] - Plunsteady[self.aerogrid['set_l'][:,1]] = flunsteady[1,:] - Plunsteady[self.aerogrid['set_l'][:,2]] = flunsteady[2,:] + cp_unsteady = ABCD[1, :, :].dot(dwj_dt) * c_over_Vtas + flunsteady = q_dyn * self.aerogrid['N'].T * self.aerogrid['A'] * cp_unsteady + Plunsteady = np.zeros((6 * self.aerogrid['n'])) + Plunsteady[self.aerogrid['set_l'][:, 0]] = flunsteady[0, :] + Plunsteady[self.aerogrid['set_l'][:, 1]] = flunsteady[1, :] + Plunsteady[self.aerogrid['set_l'][:, 2]] = flunsteady[2, :] Pk_unsteady_B = self.PHIlk.T.dot(Plunsteady) - + # C - Beschleunigungsterm -entfaellt - - + # D1-Dn - lag states - dlag_states_dt = dwj_dt.repeat(n_poles).reshape((n_j, n_poles)) - betas*lag_states/c_over_Vtas + dlag_states_dt = dwj_dt.repeat(n_poles).reshape((n_j, n_poles)) - betas * lag_states / c_over_Vtas dlag_states_dt = dlag_states_dt.reshape((-1)) - + D_dot_lag = np.zeros(n_j) - for i_pole in np.arange(0,n_poles): - D_dot_lag += ABCD[3+i_pole,:,:].dot(lag_states[:,i_pole]) - cp_unsteady = D_dot_lag - flunsteady = q_dyn * self.aerogrid['N'].T*self.aerogrid['A']*cp_unsteady - Plunsteady = np.zeros(6*self.aerogrid['n']) - Plunsteady[self.aerogrid['set_l'][:,0]] = flunsteady[0,:] - Plunsteady[self.aerogrid['set_l'][:,1]] = flunsteady[1,:] - Plunsteady[self.aerogrid['set_l'][:,2]] = flunsteady[2,:] + for i_pole in np.arange(0, n_poles): + D_dot_lag += ABCD[3 + i_pole, :, :].dot(lag_states[:, i_pole]) + cp_unsteady = D_dot_lag + flunsteady = q_dyn * self.aerogrid['N'].T * self.aerogrid['A'] * cp_unsteady + Plunsteady = np.zeros(6 * self.aerogrid['n']) + Plunsteady[self.aerogrid['set_l'][:, 0]] = flunsteady[0, :] + Plunsteady[self.aerogrid['set_l'][:, 1]] = flunsteady[1, :] + Plunsteady[self.aerogrid['set_l'][:, 2]] = flunsteady[2, :] Pk_unsteady_D = self.PHIlk.T.dot(Plunsteady) - Pk_unsteady = Pk_unsteady_D + Pk_unsteady_B + Pk_unsteady = Pk_unsteady_D + Pk_unsteady_B return Pk_unsteady, Pk_unsteady_B, Pk_unsteady_D, dlag_states_dt - + def correctioon_coefficients(self, alpha, beta, q_dyn): Pb_corr = np.zeros(6) if 'Cm_alpha_corr' in self.jcl.aero: - Pb_corr[4] += self.jcl.aero['Cm_alpha_corr'][self.i_aero]*q_dyn*self.jcl.general['A_ref']*self.jcl.general['c_ref']*alpha + Pb_corr[4] += self.aero['Cm_alpha_corr'] * q_dyn \ + * self.jcl.general['A_ref'] * self.jcl.general['c_ref'] * alpha if 'Cn_beta_corr' in self.jcl.aero: - Pb_corr[5] += self.jcl.aero['Cn_beta_corr'][self.i_aero]*q_dyn*self.jcl.general['A_ref']*self.jcl.general['b_ref']*beta - return Pb_corr - + Pb_corr[5] += self.aero['Cn_beta_corr'] * q_dyn \ + * self.jcl.general['A_ref'] * self.jcl.general['b_ref'] * beta + return Pb_corr + def vdrag(self, alpha, q_dyn): Pmac = np.zeros(6) if 'viscous_drag' in self.jcl.aero and self.jcl.aero['viscous_drag'] == 'coefficients': - if 'Cd_0' in self.jcl.aero: Cd0 = self.jcl.aero['Cd_0'][self.i_aero] - else: Cd0 = 0.012 - if 'Cd_0' in self.jcl.aero: Cd_alpha_sq = self.jcl.aero['Cd_alpha^2'][self.i_aero] - else: Cd_alpha_sq = 0.018 - Pmac[0] = q_dyn*self.jcl.general['A_ref'] * (Cd0 + Cd_alpha_sq*alpha**2.0) - return Pmac - + if 'Cd_0' in self.jcl.aero: + Cd0 = self.aero['Cd_0'] + else: + Cd0 = 0.012 + if 'Cd_0' in self.jcl.aero: + Cd_alpha_sq = self.aero['Cd_alpha^2'] + else: + Cd_alpha_sq = 0.018 + Pmac[0] = q_dyn * self.jcl.general['A_ref'] * (Cd0 + Cd_alpha_sq * alpha ** 2.0) + return Pmac + def landinggear(self, X, Tbody2geo): - Pextra = np.zeros(self.extragrid['n']*6) + Pextra = np.zeros(self.extragrid['n'] * 6) F1 = np.zeros(self.extragrid['n']) F2 = np.zeros(self.extragrid['n']) Fx = np.zeros(self.extragrid['n']) @@ -595,86 +634,98 @@ def landinggear(self, X, Tbody2geo): dp2 = np.zeros(self.extragrid['n']) ddp2 = np.zeros(self.extragrid['n']) if self.is_sim and 'landinggear' in self.simcase and self.simcase['landinggear']: - # init - p1 = -self.cggrid['offset'][:,2] + self.extragrid['offset'][:,2] + self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, X[0:6 ]))[self.extragrid['set'][:,2]] + self.PHIf_extra.T.dot(X[12:12+self.n_modes])[self.extragrid['set'][:,2]] # position LG attachment point over ground - dp1 = self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, np.dot(Tbody2geo, X[6:12])))[self.extragrid['set'][:,2]] + self.PHIf_extra.T.dot(X[12+self.n_modes:12+self.n_modes*2])[self.extragrid['set'][:,2]] # velocity LG attachment point - + # position LG attachment point over ground + p1 = -self.cggrid['offset'][:, 2] + self.extragrid['offset'][:, 2] \ + + self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, X[0: 6]))[self.extragrid['set'][:, 2]] \ + + self.PHIf_extra.T.dot(X[12:12 + self.n_modes])[self.extragrid['set'][:, 2]] + # velocity LG attachment point + dp1 = self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, np.dot(Tbody2geo, X[6:12])))[self.extragrid['set'][:, 2]] \ + + self.PHIf_extra.T.dot(X[12 + self.n_modes:12 + self.n_modes * 2])[self.extragrid['set'][:, 2]] + if self.jcl.landinggear['method'] in ['generic']: - p2 = X[self.solution.idx_lg_states[:self.extragrid['n']]] # position Tire center over ground - dp2 = X[self.solution.idx_lg_states[self.extragrid['n']:]] # velocity Tire center + # position Tire center over ground + p2 = X[self.solution.idx_lg_states[:self.extragrid['n']]] + # velocity Tire center + dp2 = X[self.solution.idx_lg_states[self.extragrid['n']:]] # loop over every landing gear for i in range(self.extragrid['n']): # calculate pre-stress F0 in gas spring - # assumption: gas spring is compressed by 2/3 when aircraft on ground - F0 = self.jcl.landinggear['para'][i]['F_static'] / ((1.0-2.0/3.0)**(-self.jcl.landinggear['para'][i]['n']*self.jcl.landinggear['para'][i]['ck'])) # N + # assumption: gas spring is compressed by 2/3 when aircraft on ground + F0 = self.jcl.landinggear['para'][i]['F_static'] \ + / ((1.0 - 2.0 / 3.0) ** (-self.jcl.landinggear['para'][i]['n'] + * self.jcl.landinggear['para'][i]['ck'])) # gas spring and damper - stroke = p2[i] - p1[i] + self.jcl.landinggear['para'][i]['stroke_length'] + self.jcl.landinggear['para'][i]['fitting_length'] + stroke = p2[i] - p1[i] + self.jcl.landinggear['para'][i]['stroke_length'] \ + + self.jcl.landinggear['para'][i]['fitting_length'] if stroke > 0.001: - Ff = F0*(1.0-stroke/self.jcl.landinggear['para'][i]['stroke_length'])**(-self.jcl.landinggear['para'][i]['n']*self.jcl.landinggear['para'][i]['ck']) - Fd = np.sign(dp2[i]-dp1[i])*self.jcl.landinggear['para'][i]['d2']*(dp2[i]-dp1[i])**2.0 + Ff = F0 * (1.0 - stroke / self.jcl.landinggear['para'][i]['stroke_length']) \ + ** (-self.jcl.landinggear['para'][i]['n'] * self.jcl.landinggear['para'][i]['ck']) + Fd = np.sign(dp2[i] - dp1[i]) * self.jcl.landinggear['para'][i]['d2'] * (dp2[i] - dp1[i]) ** 2.0 elif stroke < -0.001: Ff = -F0 - Fd = 0.0 #np.sign(dp2[i]-dp1[i])*self.jcl.landinggear['para'][i]['d2']*(dp2[i]-dp1[i])**2.0 + Fd = 0.0 else: Ff = 0.0 Fd = 0.0 # tire if p2[i] < self.jcl.landinggear['para'][i]['r_tire']: - Fz = self.jcl.landinggear['para'][i]['c1_tire']*(self.jcl.landinggear['para'][i]['r_tire'] - p2[i]) + self.jcl.landinggear['para'][i]['d1_tire']*(-dp2[i]) + Fz = self.jcl.landinggear['para'][i]['c1_tire'] \ + * (self.jcl.landinggear['para'][i]['r_tire'] + - p2[i]) + self.jcl.landinggear['para'][i]['d1_tire'] * (-dp2[i]) else: Fz = 0.0 - Fg_tire = 0.0 #self.jcl.landinggear['para'][i]['m_tire'] * 9.81 - + Fg_tire = 0.0 # self.jcl.landinggear['para'][i]['m_tire'] * 9.81 + # in case of retracted landing gear no forces apply if self.simcase['landinggear_state'][i] == 'extended': - F1[i]=(Ff+Fd) - F2[i]=(-Fg_tire-(Ff+Fd)+Fz) - Fx[i]=(0.25*Fz) # CS 25.479(d)(1) - ddp2[i]=(1.0/self.jcl.landinggear['para'][i]['m_tire']*(-Fg_tire-(Ff+Fd)+Fz)) - else: - F1[i]=(0.0) - F2[i]=(0.0) - Fx[i]=(0.0) - ddp2[i]=(0.0) - + F1[i] = Ff + Fd + F2[i] = -Fg_tire - (Ff + Fd) + Fz + Fx[i] = 0.25 * Fz # CS 25.479(d)(1) + ddp2[i] = 1.0 / self.jcl.landinggear['para'][i]['m_tire'] * (-Fg_tire - (Ff + Fd) + Fz) + else: + F1[i] = 0.0 + F2[i] = 0.0 + Fx[i] = 0.0 + ddp2[i] = 0.0 + elif self.jcl.landinggear['method'] in ['skid']: # loop over every landing gear for i in range(self.extragrid['n']): stroke = self.jcl.landinggear['para'][i]['r_tire'] - p1[i] if (stroke > 0.0) and (self.simcase['landinggear_state'][i] == 'extended'): # Forces only apply with extended landing gear and tire on ground - coeff_friction = 0.4 # landing skid on grass + coeff_friction = 0.4 # landing skid on grass Fz_i = stroke * self.jcl.landinggear['para'][i]['c1_tire'] - Fx_i = coeff_friction*Fz_i - My_i = -p1[i]*Fx_i + Fx_i = coeff_friction * Fz_i + My_i = -p1[i] * Fx_i else: # In case of retracted landing gear or tire is still in the air, no forces apply Fz_i = 0.0 Fx_i = 0.0 My_i = 0.0 - F1[i]=Fz_i - Fx[i]=Fx_i - My[i]=My_i - p2 = [] - dp2 = [] + F1[i] = Fz_i + Fx[i] = Fx_i + My[i] = My_i + p2 = [] + dp2 = [] ddp2 = [] # insert forces in 6dof vector Pextra - Pextra[self.extragrid['set'][:,0]] = Fx - Pextra[self.extragrid['set'][:,2]] = F1 - Pextra[self.extragrid['set'][:,4]] = My - + Pextra[self.extragrid['set'][:, 0]] = Fx + Pextra[self.extragrid['set'][:, 2]] = F1 + Pextra[self.extragrid['set'][:, 4]] = My + return Pextra, p2, dp2, np.array(ddp2), np.array(F1), np.array(F2) - + def apply_support_condition(self, modus, d2Ucg_dt2): # With the support option, the acceleration of the selected DoFs (0,1,2,3,4,5) is set to zero. # Trimcase and simcase can have different support conditions. - + # get support conditions from trimcase or simcase if modus in ['trim', 'trim_full_output'] and 'support' in self.trimcase: support = self.trimcase['support'] - elif modus in ['sim', 'sim_full_output'] and self.simcase!= '' and 'support' in self.simcase: + elif modus in ['sim', 'sim_full_output'] and self.simcase != '' and 'support' in self.simcase: support = self.simcase['support'] else: support = [] @@ -690,119 +741,120 @@ def apply_support_condition(self, modus, d2Ucg_dt2): if 4 in support: d2Ucg_dt2[4] = 0.0 if 5 in support: - d2Ucg_dt2[5] = 0.0 - + d2Ucg_dt2[5] = 0.0 + def geo2body(self, X): - Tgeo2body = np.zeros((6,6)) - Tgeo2body[0:3,0:3] = calc_drehmatrix(X[3], X[4], X[5]) - Tgeo2body[3:6,3:6] = calc_drehmatrix_angular(X[3], X[4], X[5]) - Tbody2geo = np.zeros((6,6)) - Tbody2geo[0:3,0:3] = calc_drehmatrix(X[3], X[4], X[5]).T - Tbody2geo[3:6,3:6] = calc_drehmatrix_angular_inv(X[3], X[4], X[5]) + Tgeo2body = np.zeros((6, 6)) + Tgeo2body[0:3, 0:3] = calc_drehmatrix(X[3], X[4], X[5]) + Tgeo2body[3:6, 3:6] = calc_drehmatrix_angular(X[3], X[4], X[5]) + Tbody2geo = np.zeros((6, 6)) + Tbody2geo[0:3, 0:3] = calc_drehmatrix(X[3], X[4], X[5]).T + Tbody2geo[3:6, 3:6] = calc_drehmatrix_angular_inv(X[3], X[4], X[5]) return Tgeo2body, Tbody2geo - + def recover_states(self, X): - dUcg_dt = np.dot(self.PHInorm_cg, X[6:12]) # u v w p q r bodyfixed - Uf = np.array(X[12:12+self.n_modes]) - dUf_dt = np.array(X[12+self.n_modes:12+self.n_modes*2]) + dUcg_dt = np.dot(self.PHInorm_cg, X[6:12]) # u v w p q r bodyfixed + Uf = np.array(X[12:12 + self.n_modes]) + dUf_dt = np.array(X[12 + self.n_modes:12 + self.n_modes * 2]) return dUcg_dt, Uf, dUf_dt - + def recover_Vtas(self, X): # aktuelle Vtas und q_dyn berechnen dxyz = X[6:9] - Vtas = sum(dxyz**2)**0.5 + Vtas = sum(dxyz ** 2) ** 0.5 rho = self.atmo['rho'] - q_dyn = rho/2.0*Vtas**2 + q_dyn = rho / 2.0 * Vtas ** 2 return Vtas, q_dyn - + def recover_onflow(self, X): - onflow = np.dot(self.PHInorm_cg, X[6:12]) # u v w p q r bodyfixed + onflow = np.dot(self.PHInorm_cg, X[6:12]) # u v w p q r bodyfixed return onflow - + def get_Ux2(self, X): # Steuerflaechenausschlaege vom efcs holen Ux2 = self.efcs.cs_mapping(X[self.solution.idx_inputs]) return Ux2 - + def rigid_EoM(self, dUcg_dt, Pb, g_cg, modus): d2Ucg_dt2 = np.zeros(dUcg_dt.shape) - if hasattr(self.jcl,'eom') and self.jcl.eom['version'] == 'waszak': + if hasattr(self.jcl, 'eom') and self.jcl.eom['version'] == 'waszak': # # non-linear EoM, bodyfixed / Waszak - d2Ucg_dt2[0:3] = np.cross(dUcg_dt[0:3], dUcg_dt[3:6]) + np.dot(np.linalg.inv(self.Mb)[0:3,0:3], Pb[0:3]) + g_cg - d2Ucg_dt2[3:6] = np.dot(np.linalg.inv(self.Mb[3:6,3:6]) , Pb[3:6] - np.cross(dUcg_dt[3:6], np.dot(self.Mb[3:6,3:6], dUcg_dt[3:6])) ) + d2Ucg_dt2[0:3] = np.cross(dUcg_dt[0:3], dUcg_dt[3:6]) + np.dot(np.linalg.inv(self.Mb)[0:3, 0:3], Pb[0:3]) + g_cg + d2Ucg_dt2[3:6] = np.dot(np.linalg.inv(self.Mb[3:6, 3:6]), Pb[3:6] + - np.cross(dUcg_dt[3:6], np.dot(self.Mb[3:6, 3:6], dUcg_dt[3:6]))) self.apply_support_condition(modus, d2Ucg_dt2) - Nxyz = (d2Ucg_dt2[0:3] - g_cg - np.cross(dUcg_dt[0:3], dUcg_dt[3:6]) )/9.8066 + Nxyz = (d2Ucg_dt2[0:3] - g_cg - np.cross(dUcg_dt[0:3], dUcg_dt[3:6])) / 9.8066 else: # linear EoM, bodyfixed / Nastran - d2Ucg_dt2[0:3] = np.dot(np.linalg.inv(self.Mb)[0:3,0:3], Pb[0:3]) + g_cg - d2Ucg_dt2[3:6] = np.dot(np.linalg.inv(self.Mb)[3:6,3:6], Pb[3:6] ) + d2Ucg_dt2[0:3] = np.dot(np.linalg.inv(self.Mb)[0:3, 0:3], Pb[0:3]) + g_cg + d2Ucg_dt2[3:6] = np.dot(np.linalg.inv(self.Mb)[3:6, 3:6], Pb[3:6]) self.apply_support_condition(modus, d2Ucg_dt2) - Nxyz = (d2Ucg_dt2[0:3] - g_cg) /9.8066 + Nxyz = (d2Ucg_dt2[0:3] - g_cg) / 9.8066 return d2Ucg_dt2, Nxyz - + def flexible_EoM(self, dUf_dt, Uf, Pf): - d2Uf_dt2 = np.dot( -np.linalg.inv(self.Mff), ( np.dot(self.Dff, dUf_dt) + np.dot(self.Kff, Uf) - Pf ) ) + d2Uf_dt2 = np.dot(-np.linalg.inv(self.Mff), (np.dot(self.Dff, dUf_dt) + np.dot(self.Kff, Uf) - Pf)) return d2Uf_dt2 - + def get_command_derivatives(self, t, X, Vtas, gamma, alpha, beta, Nxyz, dxyz): if self.is_sim and 'cs_signal' in self.simcase and self.simcase['cs_signal']: dcommand = self.efcs.cs_signal(t) elif self.is_sim and 'controller' in self.simcase and self.simcase['controller']: - feedback = {'pqr': X[self.solution.idx_states[9:12]], - 'PhiThetaPsi': X[self.solution.idx_states[3:6]], - 'z': X[self.solution.idx_states[2]], - 'Vtas': Vtas, - 'gamma': gamma, - 'alpha': alpha, - 'beta': beta, - 'commands': X[self.solution.idx_inputs], - 'Nxyz': Nxyz, - 'dz': dxyz[2], - } + feedback = {'pqr': X[self.solution.idx_states[9:12]], + 'PhiThetaPsi': X[self.solution.idx_states[3:6]], + 'z': X[self.solution.idx_states[2]], + 'Vtas': Vtas, + 'gamma': gamma, + 'alpha': alpha, + 'beta': beta, + 'commands': X[self.solution.idx_inputs], + 'Nxyz': Nxyz, + 'dz': dxyz[2], + } dcommand = self.efcs.controller(t, feedback) else: dcommand = np.zeros(self.solution.n_input_derivatives) return dcommand - + def engine(self, X, Vtas, q_dyn, Uf, dUf_dt, t): if hasattr(self.jcl, 'engine'): # get thrust setting - thrust = X[np.where(self.trimcond_X[:,0]=='thrust')[0][0]] + thrust = X[np.where(self.trimcond_X[:, 0] == 'thrust')[0][0]] dUcg_dt, Uf, dUf_dt = self.recover_states(X) - # calculate velocity at extra / engine attachment point - dUextra_dt = self.PHIextra_cg.dot(dUcg_dt) + self.PHIf_extra.T.dot(dUf_dt) + # calculate velocity at extra / engine attachment point + dUextra_dt = self.PHIextra_cg.dot(dUcg_dt) + self.PHIf_extra.T.dot(dUf_dt) # init an empty force vector for all extra points - Pextra = np.zeros(self.extragrid['n']*6) + Pextra = np.zeros(self.extragrid['n'] * 6) # loop over all engines - for i_engine in range(self.jcl.engine['key'].__len__()): + for i_engine, _ in enumerate(self.jcl.engine['key']): # assemble a dictionary that contains all engine-relevant parameters parameter_dict = {'thrust_vector': np.array(self.jcl.engine['thrust_vector'][i_engine]), 'Vtas': Vtas, 'q_dyn': q_dyn, 'Ma': self.trimcase['Ma'], 'rho': self.atmo['rho'], - } - # get engine thrust vector + } + # get engine thrust vector P_thrust = self.engine_loads.thrust_forces(parameter_dict, thrust) # add thrust to extra point force vector - Pextra[self.extragrid['set'][i_engine,:]] += P_thrust - + Pextra[self.extragrid['set'][i_engine, :]] += P_thrust + if self.jcl.engine['method'] in ['propellerdisk']: # expand the parameter dictionary with the propellerdisk-relevant parameters parameter_dict['RPM'] = self.trimcase['RPM'] parameter_dict['power'] = self.trimcase['power'] - parameter_dict['pqr'] = dUextra_dt[self.extragrid['set'][i_engine,(3,4,5)]] + parameter_dict['pqr'] = dUextra_dt[self.extragrid['set'][i_engine, (3, 4, 5)]] parameter_dict['rotation_inertia'] = self.jcl.engine['rotation_inertia'][i_engine] parameter_dict['rotation_vector'] = np.array(self.jcl.engine['rotation_vector'][i_engine]) - + # get engine load vector(s) - P_engine_torque = self.engine_loads.torque_moments(parameter_dict) - P_precessions = self.propeller_precession_loads.precession_moments(parameter_dict) - + P_engine_torque = self.engine_loads.torque_moments(parameter_dict) + P_precessions = self.propeller_precession_loads.precession_moments(parameter_dict) + # add engine and propeller loads to extra point force vector - Pextra[self.extragrid['set'][i_engine,:]] += P_precessions + P_engine_torque - + Pextra[self.extragrid['set'][i_engine, :]] += P_precessions + P_engine_torque + elif self.jcl.engine['method'] in ['pyPropMat', 'VLM4Prop']: # find the sensor that corresponds to the engine i_sensor = self.jcl.sensor['key'].index(self.jcl.engine['key'][i_engine]) @@ -812,41 +864,42 @@ def engine(self, X, Vtas, q_dyn, Uf, dUf_dt, t): # expand the parameter dictionary with the propellerdisk-relevant parameters parameter_dict['RPM'] = self.trimcase['RPM'] parameter_dict['power'] = self.trimcase['power'] - parameter_dict['alpha'] = np.arctan(w/u) - parameter_dict['beta'] = np.arctan(v/u) - parameter_dict['pqr'] = dUextra_dt[self.extragrid['set'][i_engine,(3,4,5)]] + parameter_dict['alpha'] = np.arctan(w / u) + parameter_dict['beta'] = np.arctan(v / u) + parameter_dict['pqr'] = dUextra_dt[self.extragrid['set'][i_engine, (3, 4, 5)]] parameter_dict['diameter'] = self.jcl.engine['diameter'][i_engine] parameter_dict['n_blades'] = self.jcl.engine['n_blades'][i_engine] parameter_dict['rotation_inertia'] = self.jcl.engine['rotation_inertia'][i_engine] parameter_dict['rotation_vector'] = np.array(self.jcl.engine['rotation_vector'][i_engine]) parameter_dict['t'] = t - + # get engine load vector(s) - P_engine_torque = self.engine_loads.torque_moments(parameter_dict) - P_precessions = self.propeller_precession_loads.precession_moments(parameter_dict) - P_prop = self.propeller_aero_loads.calc_loads(parameter_dict) - + P_engine_torque = self.engine_loads.torque_moments(parameter_dict) + P_precessions = self.propeller_precession_loads.precession_moments(parameter_dict) + P_prop = self.propeller_aero_loads.calc_loads(parameter_dict) + # add engine and propeller loads to extra point force vector - Pextra[self.extragrid['set'][i_engine,:]] += P_precessions + P_engine_torque + P_prop - + Pextra[self.extragrid['set'][i_engine, :]] += P_precessions + P_engine_torque + P_prop + Pb_ext = self.PHIextra_cg.T.dot(Pextra) Pf_ext = self.PHIf_extra.dot(Pextra) else: Pextra = np.array([]) Pb_ext = np.zeros(6) Pf_ext = np.zeros(self.n_modes) - + return Pextra, Pb_ext, Pf_ext def finalize(self): """ This function is called each time a trim is finished and the model equations are no longer used. - The background is that in case the model equations are pure Python code, we can rely on the automatic - memory management. In cases where other, external code is involved, it might become necessary to - release the memory manually, for example with the CFD solver SU2.Of course other, final operations - may be performed as needed. + The background is that in case the model equations are pure Python code, we can rely on the automatic + memory management. In cases where other, external code is involved, it might become necessary to + release the memory manually, for example with the CFD solver SU2.Of course other, final operations + may be performed as needed. """ pass - + + class ConvergenceError(Exception): '''Raise when structural deformation does not converge after xx loops''' diff --git a/loadskernel/equations/frequency_domain.py b/loadskernel/equations/frequency_domain.py index 1e43e8e..58e5dcc 100644 --- a/loadskernel/equations/frequency_domain.py +++ b/loadskernel/equations/frequency_domain.py @@ -1,70 +1,77 @@ +import copy +import logging import numpy as np + from scipy import linalg from scipy.fftpack import fft, ifft, fftfreq -from scipy.interpolate import interp1d +from scipy.interpolate import interp1d from scipy.stats import norm -import logging, copy from loadskernel import solution_tools -from loadskernel.fem_interfaces import fem_helper from loadskernel.equations.common import Common +from loadskernel.fem_interfaces import fem_helper from loadskernel.interpolate import MatrixInterpolation + class GustExcitation(Common): - + def eval_equations(self): self.setup_frequence_parameters() - - logging.info('building transfer functions') - self.build_AIC_interpolators() # unsteady + + logging.info('building transfer functions') + self.build_AIC_interpolators() # unsteady positiv_TFs = self.build_transfer_functions(self.positiv_fftfreqs) TFs = np.zeros((self.n_modes, self.n_modes, self.n_freqs), dtype='complex128') for i_mode in range(self.n_modes): - TFs[:,i_mode,:] = self.mirror_fouriersamples_even(positiv_TFs[:,i_mode,:]) - + TFs[:, i_mode, :] = self.mirror_fouriersamples_even(positiv_TFs[:, i_mode, :]) + logging.info('calculating gust excitation (in physical coordinates, this may take considerable time and memory)') Ph_gust_fourier, Pk_gust_fourier = self.calc_gust_excitation(self.positiv_fftfreqs, self.t) Ph_gust_fourier = self.mirror_fouriersamples_even(Ph_gust_fourier) Pk_gust_fourier = self.mirror_fouriersamples_even(Pk_gust_fourier) - + logging.info('calculating responses') - Uh_fourier = TFs * Ph_gust_fourier # [Antwort, Anregung, Frequenz] - Uh = ifft( np.array((Uh_fourier)*(1j*self.fftomega)**0).sum(axis=1) ) - dUh_dt = ifft( np.array((Uh_fourier)*(1j*self.fftomega)**1).sum(axis=1) ) - d2Uh_dt2 = ifft( np.array((Uh_fourier)*(1j*self.fftomega)**2).sum(axis=1) ) - + Uh_fourier = TFs * Ph_gust_fourier # [Antwort, Anregung, Frequenz] + Uh = ifft(np.array((Uh_fourier) * (1j * self.fftomega) ** 0).sum(axis=1)) + dUh_dt = ifft(np.array((Uh_fourier) * (1j * self.fftomega) ** 1).sum(axis=1)) + d2Uh_dt2 = ifft(np.array((Uh_fourier) * (1j * self.fftomega) ** 2).sum(axis=1)) + logging.info('reconstructing aerodynamic forces (in physical coordinates, this may take considerable time and memory)') - Ph_aero_fourier, Pk_aero_fourier = self.calc_aero_response(self.positiv_fftfreqs, - np.array((Uh_fourier)*(1j*self.fftomega)**0).sum(axis=1)[:,:self.n_freqs//2+1], - np.array((Uh_fourier)*(1j*self.fftomega)**1).sum(axis=1)[:,:self.n_freqs//2+1], ) + Ph_aero_fourier, Pk_aero_fourier = self.calc_aero_response( + self.positiv_fftfreqs, + np.array((Uh_fourier) * (1j * self.fftomega) ** 0).sum(axis=1)[:, :self.n_freqs // 2 + 1], + np.array((Uh_fourier) * (1j * self.fftomega) ** 1).sum(axis=1)[:, :self.n_freqs // 2 + 1],) Ph_aero_fourier = self.mirror_fouriersamples_even(Ph_aero_fourier) Pk_aero_fourier = self.mirror_fouriersamples_even(Pk_aero_fourier) - Pk_aero = np.real(ifft( Pk_gust_fourier ) + ifft( Pk_aero_fourier ))[:,self.t_output] - Pk_gust = np.real(ifft( Pk_gust_fourier ))[:,self.t_output] - + Pk_aero = np.real(ifft(Pk_gust_fourier) + ifft(Pk_aero_fourier))[:, self.t_output] + Pk_gust = np.real(ifft(Pk_gust_fourier))[:, self.t_output] + # split h-set into b- and f-set # remember that the x-component was omitted - Ucg = np.concatenate((np.zeros((len(self.t_output),1)), Uh[:5,self.t_output].T.real - Uh[:5,0].real), axis=1) - dUcg_dt = np.concatenate((np.zeros((len(self.t_output),1)), dUh_dt[:5,self.t_output].T.real - dUh_dt[:5,0].real), axis=1) - d2Ucg_dt2 = np.concatenate((np.zeros((len(self.t_output),1)), d2Uh_dt2[:5,self.t_output].T.real - d2Uh_dt2[:5,0].real), axis=1) - Uf = Uh[5:,self.t_output].T.real - Uh[5:,0].real - dUf_dt = dUh_dt[5:,self.t_output].T.real - dUh_dt[5:,0].real - d2Uf_dt2 = d2Uh_dt2[5:,self.t_output].T.real - d2Uh_dt2[5:,0].real - + Ucg = np.concatenate((np.zeros((len(self.t_output), 1)), Uh[:5, self.t_output].T.real - Uh[:5, 0].real), axis=1) + dUcg_dt = np.concatenate((np.zeros((len(self.t_output), 1)), + dUh_dt[:5, self.t_output].T.real - dUh_dt[:5, 0].real), axis=1) + d2Ucg_dt2 = np.concatenate((np.zeros((len(self.t_output), 1)), + d2Uh_dt2[:5, self.t_output].T.real - d2Uh_dt2[:5, 0].real), axis=1) + Uf = Uh[5:, self.t_output].T.real - Uh[5:, 0].real + dUf_dt = dUh_dt[5:, self.t_output].T.real - dUh_dt[5:, 0].real + d2Uf_dt2 = d2Uh_dt2[5:, self.t_output].T.real - d2Uh_dt2[5:, 0].real + g_cg = np.zeros((len(self.t_output), 3)) commands = np.zeros((len(self.t_output), self.solution.n_inputs)) - - X = np.concatenate((Ucg * np.array([-1.,1.,-1.,-1.,1.,-1.]), # in DIN 9300 body fixed system for flight physics, x, y, z, Phi, Theta, Psi - dUcg_dt * np.array([-1.,1.,-1.,-1.,1.,-1.]), # in DIN 9300 body fixed system for flight physics, u, v, w, p, q, r - Uf, # modal deformations + + # x, y, z, Phi, Theta, Psi, u, v, w, p, q, r in DIN 9300 body fixed system for flight physics + X = np.concatenate((Ucg * np.array([-1., 1., -1., -1., 1., -1.]), + dUcg_dt * np.array([-1., 1., -1., -1., 1., -1.]), + Uf, # modal deformations dUf_dt, # modal velocities commands, ), axis=1) - response = {'X':X, + response = {'X': X, 't': np.array([self.t[self.t_output]]).T, 'Pk_aero': Pk_aero.T, 'Pk_gust': Pk_gust.T, - 'Pk_unsteady':Pk_aero.T*0.0, + 'Pk_unsteady': Pk_aero.T * 0.0, 'dUcg_dt': dUcg_dt, 'd2Ucg_dt2': d2Ucg_dt2, 'Uf': Uf, @@ -72,102 +79,114 @@ def eval_equations(self): 'd2Uf_dt2': d2Uf_dt2, 'g_cg': g_cg, } - return response - + return response + def setup_frequence_parameters(self): self.n_modes = self.model['mass'][self.trimcase['mass']]['n_modes'][()] + 5 self.Vtas, self.q_dyn = self.recover_Vtas(self.X0) # Number of sample points if self.simcase['gust']: - t_factor = 10.0 # increase resolution (df) by extending simulation time + t_factor = 10.0 # increase resolution (df) by extending simulation time else: t_factor = 1.0 dt = self.simcase['dt'] - self.fmax = 1/dt - self.n_freqs = int(t_factor*self.simcase['t_final']*self.fmax) - if self.n_freqs % 2 != 0: # n_freq is odd - self.n_freqs += 1 # make even + self.fmax = 1 / dt + self.n_freqs = int(t_factor * self.simcase['t_final'] * self.fmax) + if self.n_freqs % 2 != 0: # n_freq is odd + self.n_freqs += 1 # make even # sample spacing - self.t = np.linspace(0.0, self.n_freqs*dt, self.n_freqs) - self.t_output = np.where(self.t<=self.simcase['t_final'])[0] # indices of time samples to returned for post-processing - self.freqs = np.linspace(0.0, self.fmax/2.0, self.n_freqs//2) # samples only from zero up to the Nyquist frequency - fftfreqs = fftfreq(self.n_freqs, dt) # whole frequency space including negative frequencies - self.fftomega = 2.0*np.pi*fftfreqs - self.positiv_fftfreqs = np.abs(fftfreqs[:self.n_freqs//2+1]) # positive only frequencies where we need to calculate the TFs and excitations - self.positiv_fftomega = 2.0*np.pi*self.positiv_fftfreqs - - logging.info('Frequency domain solution with tfinal = {}x{} s, nfreq = {}, fmax={} Hz and df = {} Hz'.format(t_factor, self.simcase['t_final'], self.n_freqs//2, self.fmax/2.0, self.fmax/self.n_freqs) ) + self.t = np.linspace(0.0, self.n_freqs * dt, self.n_freqs) + # indices of time samples to returned for post-processing + self.t_output = np.where(self.t <= self.simcase['t_final'])[0] + # samples only from zero up to the Nyquist frequency + self.freqs = np.linspace(0.0, self.fmax / 2.0, self.n_freqs // 2) + # whole frequency space including negative frequencies + fftfreqs = fftfreq(self.n_freqs, dt) + self.fftomega = 2.0 * np.pi * fftfreqs + # positive only frequencies where we need to calculate the TFs and excitations + self.positiv_fftfreqs = np.abs(fftfreqs[:self.n_freqs // 2 + 1]) + self.positiv_fftomega = 2.0 * np.pi * self.positiv_fftfreqs + + logging.info('Frequency domain solution with tfinal = {}x{} s, nfreq = {}, fmax={} Hz and df = {} Hz'.format( + t_factor, self.simcase['t_final'], self.n_freqs // 2, self.fmax / 2.0, self.fmax / self.n_freqs)) if self.f2k(self.freqs.max()) > np.max(self.aero['k_red']): - logging.warning('Required reduced frequency = {:0.3} but AICs given only up to {:0.3}'.format(self.f2k(self.freqs.max()), np.max(self.aero['k_red']))) + logging.warning('Required reduced frequency = {:0.3} but AICs given only up to {:0.3}'.format( + self.f2k(self.freqs.max()), np.max(self.aero['k_red']))) def mirror_fouriersamples_even(self, fouriersamples): mirrored_fourier = np.zeros((fouriersamples.shape[0], self.n_freqs), dtype='complex128') - mirrored_fourier[:,:self.n_freqs//2] = fouriersamples[:,:-1] - mirrored_fourier[:,self.n_freqs//2:] = np.flip(fouriersamples[:,1:], axis=1).conj() + mirrored_fourier[:, :self.n_freqs // 2] = fouriersamples[:, :-1] + mirrored_fourier[:, self.n_freqs // 2:] = np.flip(fouriersamples[:, 1:], axis=1).conj() return mirrored_fourier - + def build_transfer_functions(self, freqs): - TFs = np.zeros((self.n_modes, self.n_modes, len(freqs)), dtype='complex128') # [Antwort, Anregung, Frequenz] - for i_f in range(len(freqs)): - TFs[:,:,i_f] = self.transfer_function(freqs[i_f]) + TFs = np.zeros((self.n_modes, self.n_modes, len(freqs)), dtype='complex128') # [Antwort, Anregung, Frequenz] + for i, f in enumerate(freqs): + TFs[:, :, i] = self.transfer_function(f) return TFs def transfer_function(self, f): - omega = 2.0*np.pi*f + omega = 2.0 * np.pi * f Qhh_1 = self.Qhh_1_interp(self.f2k(f)) Qhh_2 = self.Qhh_2_interp(self.f2k(f)) - TF = np.linalg.inv(-self.Mhh*omega**2 + complex(0,1)*omega*(self.Dhh - Qhh_2) + self.Khh - Qhh_1) + TF = np.linalg.inv(-self.Mhh * omega ** 2 + complex(0, 1) * omega * (self.Dhh - Qhh_2) + self.Khh - Qhh_1) return TF def build_AIC_interpolators(self): # interpolation of physical AIC - self.Qjj_interp = MatrixInterpolation( self.aero['k_red'], self.aero['Qjj_unsteady']) + self.Qjj_interp = MatrixInterpolation(self.aero['k_red'], self.aero['Qjj_unsteady']) # do some pre-multiplications first, then the interpolation - Qhh_1 = []; Qhh_2 = [] + Qhh_1 = [] + Qhh_2 = [] for Qjj_unsteady in self.aero['Qjj_unsteady']: - Qhh_1.append(self.q_dyn * self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_1))) ) - Qhh_2.append(self.q_dyn * self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_2 / self.Vtas ))) ) - self.Qhh_1_interp = MatrixInterpolation( self.aero['k_red'], Qhh_1) - self.Qhh_2_interp = MatrixInterpolation( self.aero['k_red'], Qhh_2) - + Qhh_1.append(self.q_dyn * self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot( + self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_1)))) + Qhh_2.append(self.q_dyn * self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot( + self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_2 / self.Vtas)))) + self.Qhh_1_interp = MatrixInterpolation(self.aero['k_red'], Qhh_1) + self.Qhh_2_interp = MatrixInterpolation(self.aero['k_red'], Qhh_2) + def calc_aero_response(self, freqs, Uh, dUh_dt): # Notation: [n_panels, timesteps] wj_fourier = self.Djh_1.dot(Uh) + self.Djh_2.dot(dUh_dt) / self.Vtas Ph_fourier, Pk_fourier = self.calc_P_fourier(freqs, wj_fourier) return Ph_fourier, Pk_fourier - + def calc_gust_excitation(self, freqs, t): # Notation: [n_panels, timesteps] - wj_gust_f = fft(self.wj_gust(t)) # Eventuell muss wj_gust_f noch skaliert werden mit 2.0/N * np.abs(wj_gust_f[:,0:N//2]) + # Eventuell muss wj_gust_f noch skaliert werden mit 2.0/N * np.abs(wj_gust_f[:,0:N//2]) + wj_gust_f = fft(self.wj_gust(t)) Ph_fourier, Pk_fourier = self.calc_P_fourier(freqs, wj_gust_f) return Ph_fourier, Pk_fourier - + def calc_P_fourier(self, freqs, wj): Ph_fourier = np.zeros((self.n_modes, len(freqs)), dtype='complex128') - Pk_fourier = np.zeros((self.aerogrid['n']*6, len(freqs)), dtype='complex128') - for i_f in range(len(freqs)): - # The interpolation of Qjj is computationally very expensive, especially for a large number of frequencies. - Qjj = self.Qjj_interp(self.f2k(freqs[i_f])) - Pk_fourier[:,i_f] = self.q_dyn * self.PHIlk.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj.dot(wj[:,i_f])))) - Ph_fourier[:,i_f] = self.PHIkh.T.dot(Pk_fourier[:,i_f]) + Pk_fourier = np.zeros((self.aerogrid['n'] * 6, len(freqs)), dtype='complex128') + for i, f in enumerate(freqs): + # The interpolation of Qjj is computationally very expensive, especially for a large number of frequencies. + Qjj = self.Qjj_interp(self.f2k(f)) + Pk_fourier[:, i] = self.q_dyn * self.PHIlk.T.dot(self.aerogrid['Nmat'].T.dot( + self.aerogrid['Amat'].dot(Qjj.dot(wj[:, i])))) + Ph_fourier[:, i] = self.PHIkh.T.dot(Pk_fourier[:, i]) return Ph_fourier, Pk_fourier - + def wj_gust(self, t): - ac_position = np.array([t * self.Vtas]*self.aerogrid['n']) - panel_offset = np.array([self.aerogrid['offset_j'][:,0]]*t.__len__()).T - s_gust = (ac_position - panel_offset - self.s0) + ac_position = np.array([t * self.Vtas] * self.aerogrid['n']) + panel_offset = np.array([self.aerogrid['offset_j'][:, 0]] * len(t)).T + s_gust = ac_position - panel_offset - self.s0 # downwash der 1-cos Boe auf ein jedes Panel berechnen - wj_gust = self.WG_TAS * 0.5 * (1-np.cos(np.pi * s_gust / self.simcase['gust_gradient'])) + wj_gust = self.WG_TAS * 0.5 * (1 - np.cos(np.pi * s_gust / self.simcase['gust_gradient'])) wj_gust[np.where(s_gust <= 0.0)] = 0.0 - wj_gust[np.where(s_gust > 2*self.simcase['gust_gradient'])] = 0.0 + wj_gust[np.where(s_gust > 2 * self.simcase['gust_gradient'])] = 0.0 # Ausrichtung der Boe fehlt noch - gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0,0,1]), solution_tools.calc_drehmatrix( self.simcase['gust_orientation']/180.0*np.pi, 0.0, 0.0 )), axis=1) - wj = wj_gust * np.array([gust_direction_vector]*t.__len__()).T + gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0, 0, 1]), solution_tools.calc_drehmatrix( + self.simcase['gust_orientation'] / 180.0 * np.pi, 0.0, 0.0)), axis=1) + wj = wj_gust * np.array([gust_direction_vector] * len(t)).T return wj - + def f2k(self, f): - return 2.0*np.pi * f * self.macgrid['c_ref']/2.0 / self.Vtas - + return 2.0 * np.pi * f * self.macgrid['c_ref'] / 2.0 / self.Vtas + def k2f(self, k_red): return k_red * self.Vtas / np.pi / self.macgrid['c_ref'] @@ -175,58 +194,66 @@ def k2f(self, k_red): class TurbulenceExcitation(GustExcitation): """ So far, the calculation procedure is identical to the normal 1-cos gust excitation. - All functionalities are inherited from the GustExcitation class, only the excitation itself is replaced. + All functionalities are inherited from the GustExcitation class, only the excitation itself is replaced. """ - + def calc_sigma(self, n_samples): """ Calculate sigma so that U_sigma,ref has the probability to occur once per n_samples. - Formula as given in: https://de.wikipedia.org/wiki/Normalverteilung, compare with values of 'z sigma' in the first column of the table. - Explanations on using the percent point function (ppf) from scipy: https://stackoverflow.com/questions/60699836/how-to-use-norm-ppf + Formula as given in: https://de.wikipedia.org/wiki/Normalverteilung, compare with values of 'z sigma' in the first + column of the table. + Explanations on using the percent point function (ppf) from scipy:https://stackoverflow.com/questions/60699836/ + how-to-use-norm-ppf """ - p = 1.0 - 1.0/n_samples - sigma = norm.ppf((p+1.0)/2.0, loc=0.0, scale=1.0) + p = 1.0 - 1.0 / n_samples + sigma = norm.ppf((p + 1.0) / 2.0, loc=0.0, scale=1.0) return sigma - + def calc_psd_vonKarman(self, freqs): # calculate turbulence excitation by von Karman power spectral density according to CS-25.341 b) - L = 762.0/self.Vtas # normalized turbulence scale [s], 2500.0 ft = 762.0 m - psd_karman = 2.0*L * (1.0+8.0/3.0*(1.339*L*2.0*np.pi*freqs)**2.0)/(1.0+(1.339*L*2.0*np.pi*freqs)**2.0)**(11.0/6.0) - # set psd to zero for f=0.0 to achieve y_mean = 0.0 + L = 762.0 / self.Vtas # normalized turbulence scale [s], 2500.0 ft = 762.0 m + psd_karman = 2.0 * L * (1.0 + 8.0 / 3.0 * (1.339 * L * 2.0 * np.pi * freqs) ** 2.0) \ + / (1.0 + (1.339 * L * 2.0 * np.pi * freqs) ** 2.0) ** (11.0 / 6.0) + # set psd to zero for f=0.0 to achieve y_mean = 0.0 if freqs[0] == 0.0: psd_karman[0] = 0.0 # Calculate the RMS value for cross-checking. Exclude first frequency with f=0.0 from the integral. - logging.info('RMS of PSD input (should approach 1.0): {:.4f}'.format(np.trapz(psd_karman[1:], freqs[1:])**0.5)) + logging.info('RMS of PSD input (should approach 1.0): {:.4f}'.format(np.trapz(psd_karman[1:], freqs[1:]) ** 0.5)) return psd_karman - + def calc_gust_excitation(self, freqs, t): # Calculate turbulence excitation by von Karman power spectral density according to CS-25.341(b) - # For calculation of limit loads in the time domain, scale u_sigma such that it has the probability to occur once during the simulation time. + # For calculation of limit loads in the time domain, scale u_sigma such that it has the probability to occur once + # during the simulation time. sigma = self.calc_sigma(self.n_freqs) - u_sigma = self.u_sigma / sigma # turbulence gust intensity [m/s] + u_sigma = self.u_sigma / sigma # turbulence gust intensity [m/s] logging.info("Using RMS turbulence intensity u_sigma = {:.4f} m/s, sigma = {:.4f}.".format(u_sigma, sigma)) - + psd_karman = self.calc_psd_vonKarman(freqs) # Apply a scaling in the frequency domain to achieve the correct amplitude in the time domain. # Then, CS-25 wants us to take the square root. - psd_scaled = u_sigma * (psd_karman * len(freqs) * self.fmax)**0.5 + psd_scaled = u_sigma * (psd_karman * len(freqs) * self.fmax) ** 0.5 # generate a random phase - np.random.seed() # create a new seed for the random numbers, so that when using multiprocessing, every worker gets a new seed - random_phases = np.random.random_sample(len(freqs)) * 2.0*np.pi - + # create a new seed for the random numbers, so that when using multiprocessing, every worker gets a new seed + np.random.seed() + random_phases = np.random.random_sample(len(freqs)) * 2.0 * np.pi + # apply to all panels with phase delay according to geometrical position - time_delay = self.aerogrid['offset_j'][:,0]/self.Vtas # time delay of every panel in [s] - phase_delay = -np.tile(time_delay, (len(freqs), 1)).T * 2.0*np.pi * freqs # phase delay of every panel and frequency in [rad] + # time delay of every panel in [s] + time_delay = self.aerogrid['offset_j'][:, 0] / self.Vtas + # phase delay of every panel and frequency in [rad] + phase_delay = -np.tile(time_delay, (len(freqs), 1)).T * 2.0 * np.pi * freqs # Ausrichtung der Boe fehlt noch - gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0,0,1]), solution_tools.calc_drehmatrix( self.simcase['gust_orientation']/180.0*np.pi, 0.0, 0.0 )), axis=1) + gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0, 0, 1]), solution_tools.calc_drehmatrix( + self.simcase['gust_orientation'] / 180.0 * np.pi, 0.0, 0.0)), axis=1) # Notation: [n_panels, n_freq] - wj_gust_f = psd_scaled * np.exp(1j*(random_phases+phase_delay)) * gust_direction_vector[:,None] /self.Vtas + wj_gust_f = psd_scaled * np.exp(1j * (random_phases + phase_delay)) * gust_direction_vector[:, None] / self.Vtas Ph_fourier, Pk_fourier = self.calc_P_fourier(freqs, wj_gust_f) - + return Ph_fourier, Pk_fourier - + """ - Checks: + Checks: psd_karman = 2.0*L * (1.0+8.0/3.0*(1.339*L*2.0*np.pi*freqs)**2.0)/(1.0+(1.339*L*2.0*np.pi*freqs)**2.0)**(11.0/6.0) psd_dryden = 2.0*L * (1.0+3.0*(L*2.0*np.pi*freqs)**2.0) /(1.0+(L*2.0*np.pi*freqs)**2.0)**(2.0) from matplotlib import pyplot as plt @@ -238,37 +265,37 @@ def calc_gust_excitation(self, freqs, t): plt.legend(loc='upper right') plt.grid(True) plt.show() - + print('RMS of PSD input (must be close to u_sigma): {:.4f}'.format(np.trapz(psd_karman, freqs)**0.5)) print('RMS in freq. dom: {:.4f}'.format(np.trapz(psd_scaled, freqs)**0.5)) - + w_turb_f = psd_scaled*np.exp(1j*random_phases) print('The amplitude may not change after adding a random phase: {}'.format(np.allclose(np.abs(w_turb_f), psd_scaled))) - + # calculate mean and rms values - fouriersamples_mirrored = self.mirror_fouriersamples_even(w_turb_f[None,:]) - + fouriersamples_mirrored = self.mirror_fouriersamples_even(w_turb_f[None, :]) + print('--- Time signals ---') y = ifft(fouriersamples_mirrored) print('Mean: {:.4f}'.format(np.mean(y))) print('RMS: {:.4f}'.format(np.mean(np.abs(y)**2.0)**0.5)) from matplotlib import pyplot as plt plt.figure() - plt.plot(self.t, y[0,:]) + plt.plot(self.t, y[0, :]) plt.ylabel('$U_{turbulence} [m/s]$') plt.xlabel('Time [s]') plt.grid(True) plt.show() - - + + # compare frequency content y_fourier = fft(y) from matplotlib import pyplot as plt fig, (ax1, ax2) = plt.subplots(2,1, sharex=True) - ax1.scatter(self.fftomega, fouriersamples_mirrored[0,:].real, marker='s', label='original') - ax1.scatter(self.fftomega, y_fourier[0,:].real, marker='.', label='freq -> time -> freq, no delay') - ax2.scatter(self.fftomega, fouriersamples_mirrored[0,:].imag, marker='s', label='original') - ax2.scatter(self.fftomega, y_fourier[0,:].imag, marker='.', label='freq -> time -> freq, no delay') + ax1.scatter(self.fftomega, fouriersamples_mirrored[0, :].real, marker='s', label='original') + ax1.scatter(self.fftomega, y_fourier[0, :].real, marker='.', label='freq -> time -> freq, no delay') + ax2.scatter(self.fftomega, fouriersamples_mirrored[0, :].imag, marker='s', label='original') + ax2.scatter(self.fftomega, y_fourier[0, :].imag, marker='.', label='freq -> time -> freq, no delay') ax1.set_ylabel('real') ax2.set_ylabel('imag') ax2.set_xlabel('freq') @@ -276,172 +303,188 @@ def calc_gust_excitation(self, freqs, t): ax1.grid(True) ax2.grid(True) """ - + + class LimitTurbulence(TurbulenceExcitation): - + def eval_equations(self): self.setup_frequence_parameters() - - logging.info('building transfer functions') - self.build_AIC_interpolators() # unsteady + + logging.info('building transfer functions') + self.build_AIC_interpolators() # unsteady # Notation: [Antwort, Anregung, Frequenz] positiv_TFs = self.build_transfer_functions(self.positiv_fftfreqs) logging.info('calculating gust excitation (in physical coordinates, this may take considerable time and memory)') Ph_gust, Pk_gust = self.calc_white_noise_excitation(self.positiv_fftfreqs) - + psd_karman = self.calc_psd_vonKarman(self.positiv_fftfreqs) - - logging.info('calculating responses') + + logging.info('calculating responses') Hdisp = np.sum(positiv_TFs * Ph_gust, axis=1) - + logging.info('reconstructing aerodynamic forces (in physical coordinates, this may take considerable time and memory)') - # Aerodynamic forces due to the elastic reaction of the aircraft - wj_aero= self.Djh_1.dot(Hdisp) + self.Djh_2.dot(Hdisp*(1j*self.positiv_fftomega)**1) / self.Vtas - Ph_aero, Pk_aero = self.calc_P_fourier(self.positiv_fftfreqs, wj_aero) + # Aerodynamic forces due to the elastic reaction of the aircraft + wj_aero = self.Djh_1.dot(Hdisp) + self.Djh_2.dot(Hdisp * (1j * self.positiv_fftomega) ** 1) / self.Vtas + _, Pk_aero = self.calc_P_fourier(self.positiv_fftfreqs, wj_aero) Haero = self.PHIstrc_mon.T.dot(self.PHIk_strc.T.dot(Pk_aero)) # Aerodynamic forces due to the gust / turbulence Hgust = self.PHIstrc_mon.T.dot(self.PHIk_strc.T.dot(Pk_gust)) # Inertial forces due to the elastic reation of the aircraft - Hiner = self.PHIstrc_mon.T.dot( -self.Mgg.dot( - self.mass['PHIh_strc'].T).dot(Hdisp*(1j*self.positiv_fftomega)**2)) + Hiner = self.PHIstrc_mon.T.dot(-self.Mgg.dot( + self.mass['PHIh_strc'].T).dot(Hdisp * (1j * self.positiv_fftomega) ** 2)) # Force Summation Method: P = Pext + Piner H = Haero + Hgust + Hiner - - A = np.trapz(np.real(H * H.conj())*psd_karman, self.positiv_fftfreqs) ** 0.5 + + A = np.trapz(np.real(H * H.conj()) * psd_karman, self.positiv_fftfreqs) ** 0.5 Pmon = self.u_sigma * A - + logging.info('calculating correlations') - # Using H[:,None,:] * H.conj()[None,:,:] to calculate all coefficients at once would be nice but requires much memory. - # Looping over all rows is more memory efficient and even slightly faster. - # Once the integral is done, the matrix is much smaller. - correlations = np.zeros((self.mongrid['n']*6, self.mongrid['n']*6)) - for i_row in range(6*self.mongrid['n']): - correlations[i_row,:] = np.trapz(np.real(H[i_row,:].conj() * H)*psd_karman, self.positiv_fftfreqs) - correlations /= (A * A[:,None]) + # Using H[:,None, :] * H.conj()[None, :, :] to calculate all coefficients at once would be nice but requires much + # memory. Looping over all rows is more memory efficient and even slightly faster. + # Once the integral is done, the matrix is much smaller. + correlations = np.zeros((self.mongrid['n'] * 6, self.mongrid['n'] * 6)) + for i_row in range(6 * self.mongrid['n']): + correlations[i_row, :] = np.trapz(np.real(H[i_row, :].conj() * H) * psd_karman, self.positiv_fftfreqs) + correlations /= (A * A[:, None]) response = {'Pmon_turb': np.expand_dims(Pmon, axis=0), 'correlations': correlations, } return response - + def calc_white_noise_excitation(self, freqs): # white noise with constant amplitude for all frequencies white_noise = np.ones_like(freqs) # apply to all panels with phase delay according to geometrical position - time_delay = self.aerogrid['offset_j'][:,0]/self.Vtas # time delay of every panel in [s] - phase_delay = -np.tile(time_delay, (len(freqs), 1)).T * 2.0*np.pi * freqs # phase delay of every panel and frequency in [rad] + # time delay of every panel in [s] + time_delay = self.aerogrid['offset_j'][:, 0] / self.Vtas + # phase delay of every panel and frequency in [rad] + phase_delay = -np.tile(time_delay, (len(freqs), 1)).T * 2.0 * np.pi * freqs # Ausrichtung der Boe fehlt noch - gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0,0,1]), solution_tools.calc_drehmatrix( self.simcase['gust_orientation']/180.0*np.pi, 0.0, 0.0 )), axis=1) + gust_direction_vector = np.sum(self.aerogrid['N'] * np.dot(np.array([0, 0, 1]), solution_tools.calc_drehmatrix( + self.simcase['gust_orientation'] / 180.0 * np.pi, 0.0, 0.0)), axis=1) # Notation: [n_panels, n_freq] - wj_gust_f = white_noise * np.exp(1j*(phase_delay)) * gust_direction_vector[:,None] /self.Vtas + wj_gust_f = white_noise * np.exp(1j * (phase_delay)) * gust_direction_vector[:, None] / self.Vtas Ph_fourier, Pk_fourier = self.calc_P_fourier(freqs, wj_gust_f) - + return Ph_fourier, Pk_fourier + class KMethod(GustExcitation): - + def eval_equations(self): self.setup_frequence_parameters() - - logging.info('building systems') - self.build_AIC_interpolators() # unsteady + + logging.info('building systems') + self.build_AIC_interpolators() # unsteady self.build_systems() logging.info('calculating eigenvalues') self.calc_eigenvalues() - - response = {'freqs':self.freqs, - 'damping':self.damping, - 'Vtas':self.Vtas, - } - return response + + response = {'freqs': self.freqs, + 'damping': self.damping, + 'Vtas': self.Vtas, + } + return response def setup_frequence_parameters(self): self.n_modes = self.model['mass'][self.trimcase['mass']]['n_modes'][()] + 5 self.k_reds = self.simcase['flutter_para']['k_red'] self.n_freqs = len(self.k_reds) - + if self.k_reds.max() > np.max(self.aero['k_red']): - logging.warning('Required reduced frequency = {:0.3} but AICs given only up to {:0.3}'.format(self.k_reds.max(), np.max(self.aero['k_red']))) - + logging.warning('Required reduced frequency = {:0.3} but AICs given only up to {:0.3}'.format( + self.k_reds.max(), np.max(self.aero['k_red']))) + def build_AIC_interpolators(self): Qhh = [] for Qjj_unsteady, k_red in zip(self.aero['Qjj_unsteady'], self.aero['k_red']): - Qhh.append(self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_1 + complex(0,1)*k_red/(self.macgrid['c_ref']/2.0)*self.Djh_2))) ) - self.Qhh_interp = interp1d( self.aero['k_red'], Qhh, kind='cubic', axis=0, fill_value="extrapolate") - + Qhh.append(self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj_unsteady).dot( + self.Djh_1 + complex(0, 1) * k_red / (self.macgrid['c_ref'] / 2.0) * self.Djh_2)))) + self.Qhh_interp = interp1d(self.aero['k_red'], Qhh, kind='cubic', axis=0, fill_value="extrapolate") + def build_systems(self): - self.A = np.zeros((self.n_modes, self.n_modes, self.n_freqs), dtype='complex128') # [Antwort, Anregung, Frequenz] - self.B = np.zeros((self.n_modes, self.n_modes, self.n_freqs), dtype='complex128') # [Antwort, Anregung, Frequenz] + self.A = np.zeros((self.n_modes, self.n_modes, self.n_freqs), dtype='complex128') # [Antwort, Anregung, Frequenz] + self.B = np.zeros((self.n_modes, self.n_modes, self.n_freqs), dtype='complex128') # [Antwort, Anregung, Frequenz] for i_f in range(self.n_freqs): - self.A[:,:,i_f], self.B[:,:,i_f] = self.system(self.k_reds[i_f]) - + self.A[:, :, i_f], self.B[:, :, i_f] = self.system(self.k_reds[i_f]) + def system(self, k_red): rho = self.atmo['rho'] Qhh = self.Qhh_interp(k_red) # Schwochow equation (7.10) - A = -self.Mhh - rho/2.0*(self.macgrid['c_ref']/2.0/k_red)**2.0*Qhh + A = -self.Mhh - rho / 2.0 * (self.macgrid['c_ref'] / 2.0 / k_red) ** 2.0 * Qhh B = -self.Khh return A, B - + def calc_eigenvalues(self): - eigenvalues = []; eigenvectors = []; freqs = []; damping = []; Vtas = [] - eigenvalue, eigenvector = linalg.eig(self.A[:,:,0], self.B[:,:,0]) + eigenvalues = [] + eigenvectors = [] + freqs = [] + damping = [] + Vtas = [] + eigenvalue, eigenvector = linalg.eig(self.A[:, :, 0], self.B[:, :, 0]) # sorting idx_pos = np.where(eigenvalue.real >= 0.0)[0] # nur oszillierende Eigenbewegungen - idx_sort = np.argsort(1.0/eigenvalue.real[idx_pos]**0.5) # sort result by eigenvalue - + idx_sort = np.argsort(1.0 / eigenvalue.real[idx_pos] ** 0.5) # sort result by eigenvalue + eigenvalue = eigenvalue[idx_pos][idx_sort] eigenvector = eigenvector[:, idx_pos][:, idx_sort] # store results eigenvalues.append(eigenvalue) eigenvectors.append(eigenvector) - + # calculate frequencies and damping ratios - freqs.append(1.0/eigenvalue.real**0.5 / 2.0/np.pi) - damping.append(eigenvalue.imag/eigenvalue.real) - Vtas.append(self.macgrid['c_ref']/2.0/self.k_reds[0]/eigenvalue.real**0.5) - + freqs.append(1.0 / eigenvalue.real ** 0.5 / 2.0 / np.pi) + damping.append(eigenvalue.imag / eigenvalue.real) + Vtas.append(self.macgrid['c_ref'] / 2.0 / self.k_reds[0] / eigenvalue.real ** 0.5) + for i_f in range(1, self.n_freqs): - eigenvalue, eigenvector = linalg.eig(self.A[:,:,i_f], self.B[:,:,i_f]) + eigenvalue, eigenvector = linalg.eig(self.A[:, :, i_f], self.B[:, :, i_f]) # sorting idx_pos = np.where(eigenvalue.real >= 0.0)[0] MAC = fem_helper.calc_MAC(eigenvectors[-1], eigenvector[:, idx_pos], plot=False) idx_sort = [MAC[x, :].argmax() for x in range(MAC.shape[0])] - + eigenvalue = eigenvalue[idx_pos][idx_sort] eigenvector = eigenvector[:, idx_pos][:, idx_sort] # store results eigenvalues.append(eigenvalue) eigenvectors.append(eigenvector) - freqs.append(1.0/eigenvalue.real**0.5 / 2.0/np.pi) - damping.append(eigenvalue.imag/eigenvalue.real) - Vtas.append(self.macgrid['c_ref']/2.0/self.k_reds[i_f]/eigenvalue.real**0.5) + freqs.append(1.0 / eigenvalue.real ** 0.5 / 2.0 / np.pi) + damping.append(eigenvalue.imag / eigenvalue.real) + Vtas.append(self.macgrid['c_ref'] / 2.0 / self.k_reds[i_f] / eigenvalue.real ** 0.5) self.freqs = np.array(freqs) self.damping = np.array(damping) self.Vtas = np.array(Vtas) - + + class KEMethod(KMethod): - + def system(self, k_red): rho = self.atmo['rho'] Qhh = self.Qhh_interp(k_red) # Nastran equation (2-120) A = self.Khh - B = (k_red/self.macgrid['c_ref']*2.0)**2.0 * self.Mhh + rho/2.0 * Qhh + B = (k_red / self.macgrid['c_ref'] * 2.0) ** 2.0 * self.Mhh + rho / 2.0 * Qhh return A, B - + def calc_eigenvalues(self): - eigenvalues = []; eigenvectors = []; freqs = []; damping = []; Vtas = [] - eigenvalue, eigenvector = linalg.eig(self.A[:,:,0], self.B[:,:,0]) + eigenvalues = [] + eigenvectors = [] + freqs = [] + damping = [] + Vtas = [] + eigenvalue, eigenvector = linalg.eig(self.A[:, :, 0], self.B[:, :, 0]) # sorting idx_pos = range(self.n_modes) - V = ((eigenvalue.real**2 + eigenvalue.imag**2) / eigenvalue.real)**0.5 - freq = self.k_reds[0]*V/np.pi/self.macgrid['c_ref'] + V = ((eigenvalue.real ** 2 + eigenvalue.imag ** 2) / eigenvalue.real) ** 0.5 + freq = self.k_reds[0] * V / np.pi / self.macgrid['c_ref'] idx_sort = np.argsort(freq) - + eigenvalue = eigenvalue[idx_pos][idx_sort] eigenvector = eigenvector[:, idx_pos][:, idx_sort] # store results @@ -449,54 +492,56 @@ def calc_eigenvalues(self): eigenvectors.append(eigenvector) freqs.append(freq[idx_sort]) Vtas.append(V[idx_sort]) - damping.append(np.imag(V[idx_sort]**2 / eigenvalue)) - + damping.append(np.imag(V[idx_sort] ** 2 / eigenvalue)) + for i_f in range(1, self.n_freqs): - eigenvalue, eigenvector = linalg.eig(self.A[:,:,i_f], self.B[:,:,i_f]) + eigenvalue, eigenvector = linalg.eig(self.A[:, :, i_f], self.B[:, :, i_f]) # sorting - if i_f >=2: - pes = eigenvalues[-1] + (self.k_reds[i_f]-self.k_reds[i_f-1])*(eigenvalues[-1] - eigenvalues[-2])/(self.k_reds[i_f-1]-self.k_reds[i_f-2]) + if i_f >= 2: + pes = eigenvalues[-1] + (self.k_reds[i_f] - self.k_reds[i_f - 1]) * (eigenvalues[-1] - eigenvalues[-2]) \ + / (self.k_reds[i_f - 1] - self.k_reds[i_f - 2]) idx_sort = [] for pe in pes: - idx_sort.append(((pe.real-eigenvalue.real)**2 + (pe.imag-eigenvalue.imag)**2).argmin()) - + idx_sort.append(((pe.real - eigenvalue.real) ** 2 + (pe.imag - eigenvalue.imag) ** 2).argmin()) + eigenvalue = eigenvalue[idx_pos][idx_sort] eigenvector = eigenvector[:, idx_pos][:, idx_sort] # store results eigenvalues.append(eigenvalue) eigenvectors.append(eigenvector) - V = ((eigenvalue.real**2 + eigenvalue.imag**2) / eigenvalue.real)**0.5 - freq = self.k_reds[i_f]*V/np.pi/self.macgrid['c_ref'] + V = ((eigenvalue.real ** 2 + eigenvalue.imag ** 2) / eigenvalue.real) ** 0.5 + freq = self.k_reds[i_f] * V / np.pi / self.macgrid['c_ref'] freqs.append(freq) Vtas.append(V) - damping.append(-eigenvalue.imag/eigenvalue.real) - + damping.append(-eigenvalue.imag / eigenvalue.real) + self.freqs = np.array(freqs) self.damping = np.array(damping) self.Vtas = np.array(Vtas) + class PKMethod(KMethod): - + def setup_frequence_parameters(self): self.n_modes_rbm = 5 self.n_modes_f = self.model['mass'][self.trimcase['mass']]['n_modes'][()] self.n_modes = self.n_modes_f + self.n_modes_rbm - - self.states = ["y'", "z'", "$\Phi'$", "$\Theta'$", "$\Psi'$",] - for i_mode in range(1, self.n_modes_f+1): - self.states += ['Uf'+str(i_mode)] + + self.states = ["y'", "z'", "$\Phi'$", "$\Theta'$", "$\Psi'$", ] # noqa: W605 + for i_mode in range(1, self.n_modes_f + 1): + self.states += ['Uf' + str(i_mode)] self.states += ["v'", "w'", "p'", "q'", "r'"] - for i_mode in range(1, self.n_modes_f+1): - self.states += ['$\mathrm{{ \dot Uf{} }}$'.format(str(i_mode))] - - self.Vvec = self.simcase['flutter_para']['Vtas'] - + for i_mode in range(1, self.n_modes_f + 1): + self.states += ['$\mathrm{{ \dot Uf{} }}$'.format(str(i_mode))] # noqa: W605 + + self.Vvec = self.simcase['flutter_para']['Vtas'] + def eval_equations(self): self.setup_frequence_parameters() - - logging.info('building systems') - self.build_AIC_interpolators() # unsteady - logging.info('starting p-k iterations to match k_red with Vtas and omega') + + logging.info('building systems') + self.build_AIC_interpolators() # unsteady + logging.info('starting p-k iterations to match k_red with Vtas and omega') # compute initial guess at k_red=0.0 and first flight speed self.Vtas = self.Vvec[0] eigenvalue, eigenvector = linalg.eig(self.system(k_red=0.0).real) @@ -505,91 +550,100 @@ def eval_equations(self): idx_sort = np.argsort(np.abs(eigenvalue.imag[idx_pos])) # sort result by eigenvalue eigenvalues0 = eigenvalue[idx_pos][idx_sort] eigenvectors0 = eigenvector[:, idx_pos][:, idx_sort] - k0 = eigenvalues0.imag*self.macgrid['c_ref']/2.0/self.Vtas - - eigenvalues = []; eigenvectors = []; freqs = []; damping = []; Vtas = [] + k0 = eigenvalues0.imag * self.macgrid['c_ref'] / 2.0 / self.Vtas + + eigenvalues = [] + eigenvectors = [] + freqs = [] + damping = [] + Vtas = [] # loop over modes for i_mode in range(len(eigenvalues0)): - logging.debug('Mode {}'.format(i_mode+1)) - eigenvalues_per_mode = []; eigenvectors_per_mode = [] + logging.debug('Mode {}'.format(i_mode + 1)) + eigenvalues_per_mode = [] + eigenvectors_per_mode = [] k_old = copy.deepcopy(k0[i_mode]) eigenvectors_old = copy.deepcopy(eigenvectors0) # loop over Vtas - for i_V in range(len(self.Vvec)): - self.Vtas = self.Vvec[i_V] - e = 1.0; n_iter = 0 + for _, V in enumerate(self.Vvec): + self.Vtas = V + e = 1.0 + n_iter = 0 # iteration to match k_red with Vtas and omega of the mode under investigation while e >= 1e-3: eigenvalues_new, eigenvectors_new = self.calc_eigenvalues(self.system(k_old).real, eigenvectors_old) - k_now = np.abs(eigenvalues_new[i_mode].imag)*self.macgrid['c_ref']/2.0/self.Vtas - # Use relaxation for improved convergence, which helps in some cases to avoid oscillations of the iterative solution. - k_new = k_old + 0.8*(k_now-k_old) + k_now = np.abs(eigenvalues_new[i_mode].imag) * self.macgrid['c_ref'] / 2.0 / self.Vtas + # Use relaxation for improved convergence, which helps in some cases to avoid oscillations of the + # iterative solution. + k_new = k_old + 0.8 * (k_now - k_old) e = np.abs(k_new - k_old) k_old = k_new n_iter += 1 if n_iter > 80: - logging.warning('poor convergence for mode {} at Vtas={:.2f} with k_red={:.5f} and e={:.5f}'.format(i_mode+1, self.Vvec[i_V], k_new, e)) + logging.warning('poor convergence for mode {} at Vtas={:.2f} with k_red={:.5f} and e={:.5f}'.format( + i_mode + 1, V, k_new, e)) if n_iter > 100: logging.warning('p-k iteration NOT converged after 100 loops.') break eigenvectors_old = eigenvectors_new eigenvalues_per_mode.append(eigenvalues_new[i_mode]) - eigenvectors_per_mode.append(eigenvectors_new[:,i_mode]) - # store + eigenvectors_per_mode.append(eigenvectors_new[:, i_mode]) + # store eigenvalues_per_mode = np.array(eigenvalues_per_mode) eigenvalues.append(eigenvalues_per_mode) eigenvectors.append(np.array(eigenvectors_per_mode).T) - freqs.append(eigenvalues_per_mode.imag /2.0/np.pi) + freqs.append(eigenvalues_per_mode.imag / 2.0 / np.pi) damping.append(eigenvalues_per_mode.real / np.abs(eigenvalues_per_mode)) Vtas.append(self.Vvec) - - response = {'eigenvalues':np.array(eigenvalues).T, - 'eigenvectors':np.array(eigenvectors).T, - 'freqs':np.array(freqs).T, - 'damping':np.array(damping).T, - 'Vtas':np.array(Vtas).T, + + response = {'eigenvalues': np.array(eigenvalues).T, + 'eigenvectors': np.array(eigenvectors).T, + 'freqs': np.array(freqs).T, + 'damping': np.array(damping).T, + 'Vtas': np.array(Vtas).T, 'states': self.states, - } - return response - + } + return response + def calc_eigenvalues(self, A, eigenvector_old): eigenvalue, eigenvector = linalg.eig(A) - #idx_pos = np.where(eigenvalue.imag >= 0.0)[0] # nur oszillierende Eigenbewegungen - #idx_sort = np.argsort(np.abs(eigenvalue.imag[idx_pos])) # sort result by eigenvalue + # idx_pos = np.where(eigenvalue.imag >= 0.0)[0] # nur oszillierende Eigenbewegungen + # idx_sort = np.argsort(np.abs(eigenvalue.imag[idx_pos])) # sort result by eigenvalue MAC = fem_helper.calc_MAC(eigenvector_old, eigenvector, plot=False) idx_pos = self.get_best_match(MAC) - eigenvalues = eigenvalue[idx_pos]#[idx_sort] - eigenvectors = eigenvector[:,idx_pos]#[:, idx_sort] + eigenvalues = eigenvalue[idx_pos] # [idx_sort] + eigenvectors = eigenvector[:, idx_pos] # [:, idx_sort] return eigenvalues, eigenvectors - - def get_best_match(selfself, MAC): + + def get_best_match(self, MAC): """ - Before: idx_pos = [MAC[x,:].argmax() for x in range(MAC.shape[0])] + Before: idx_pos = [MAC[x, :].argmax() for x in range(MAC.shape[0])] With purely real eigenvalues it happens that the selection (only) by highest MAC value does not work. - The result is that from two different eigenvalues one is take twice. The solution is to keep record + The result is that from two different eigenvalues one is take twice. The solution is to keep record of the matches that are still available so that, if the bets match is already taken, the second best match is selected. """ - possible_matches = [True]*MAC.shape[1] + possible_matches = [True] * MAC.shape[1] possible_idx = np.arange(MAC.shape[1]) idx_pos = [] for x in range(MAC.shape[0]): # the highest MAC value indicates the best match - best_match=MAC[x,possible_matches].argmax() + best_match = MAC[x, possible_matches].argmax() # reconstruct the corresponding index idx_pos.append(possible_idx[possible_matches][best_match]) # remove the best match from the list of candidates - possible_matches[possible_idx[possible_matches][best_match]]=False + possible_matches[possible_idx[possible_matches][best_match]] = False return idx_pos - + def calc_Qhh_1(self, Qjj_unsteady): return self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_1))) - + def calc_Qhh_2(self, Qjj_unsteady): return self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj_unsteady).dot(self.Djh_2))) - + def build_AIC_interpolators(self): # do some pre-multiplications first, then the interpolation - Qhh_1 = []; Qhh_2 = [] + Qhh_1 = [] + Qhh_2 = [] if self.jcl.aero['method'] in ['freq_dom']: for Qjj_unsteady in self.aero['Qjj_unsteady']: Qhh_1.append(self.calc_Qhh_1(Qjj_unsteady)) @@ -598,25 +652,27 @@ def build_AIC_interpolators(self): ABCD = self.aero['ABCD'] for k_red in self.aero['k_red']: D = np.zeros((self.aerogrid['n'], self.aerogrid['n']), dtype='complex') - j = 1j # imaginary number - for i_pole, beta in zip(np.arange(0,self.aero['n_poles']), self.aero['betas']): - D += ABCD[3+i_pole,:,:] * j*k_red / (j*k_red + beta) - Qjj_unsteady = ABCD[0,:,:] + ABCD[1,:,:]*j*k_red + ABCD[2,:,:]*(j*k_red)**2 + D + j = 1j # imaginary number + for i_pole, beta in zip(np.arange(0, self.aero['n_poles']), self.aero['betas']): + D += ABCD[3 + i_pole, :, :] * j * k_red / (j * k_red + beta) + Qjj_unsteady = ABCD[0, :, :] + ABCD[1, :, :] * j * k_red + ABCD[2, :, :] * (j * k_red) ** 2 + D Qhh_1.append(self.calc_Qhh_1(Qjj_unsteady)) Qhh_2.append(self.calc_Qhh_2(Qjj_unsteady)) - - self.Qhh_1_interp = MatrixInterpolation( self.aero['k_red'], Qhh_1) - self.Qhh_2_interp = MatrixInterpolation( self.aero['k_red'], Qhh_2) - + + self.Qhh_1_interp = MatrixInterpolation(self.aero['k_red'], Qhh_1) + self.Qhh_2_interp = MatrixInterpolation(self.aero['k_red'], Qhh_2) + def system(self, k_red): rho = self.atmo['rho'] - + Qhh_1 = self.Qhh_1_interp(k_red) Qhh_2 = self.Qhh_2_interp(k_red) Mhh_inv = np.linalg.inv(self.Mhh) - - upper_part = np.concatenate((np.zeros((self.n_modes, self.n_modes), dtype='complex128'), np.eye(self.n_modes, dtype='complex128')), axis=1) - lower_part = np.concatenate(( -Mhh_inv.dot(self.Khh - rho/2.0*self.Vtas**2.0*Qhh_1), -Mhh_inv.dot(self.Dhh - rho/2.0*self.Vtas*Qhh_2)), axis=1) + + upper_part = np.concatenate((np.zeros((self.n_modes, self.n_modes), dtype='complex128'), + np.eye(self.n_modes, dtype='complex128')), axis=1) + lower_part = np.concatenate((-Mhh_inv.dot(self.Khh - rho / 2.0 * self.Vtas ** 2.0 * Qhh_1), + -Mhh_inv.dot(self.Dhh - rho / 2.0 * self.Vtas * Qhh_2)), axis=1) A = np.concatenate((upper_part, lower_part)) - - return A + + return A diff --git a/loadskernel/equations/landing.py b/loadskernel/equations/landing.py index b136d98..2cb4ab5 100644 --- a/loadskernel/equations/landing.py +++ b/loadskernel/equations/landing.py @@ -1,104 +1,93 @@ -''' -Created on Aug 2, 2019 - -@author: voss_ar -''' import numpy as np - -from loadskernel.solution_tools import * from loadskernel.equations.common import Common +from loadskernel.solution_tools import gravitation_on_earth + class Landing(Common): def equations(self, X, t, modus): self.counter += 1 # recover states - Tgeo2body, Tbody2geo = self.geo2body(X) - dUcg_dt, Uf, dUf_dt = self.recover_states(X) - Vtas, q_dyn = self.recover_Vtas(X) - onflow = self.recover_onflow(X) - alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) - Ux2 = self.get_Ux2(X) - # -------------------- - # --- aerodynamics --- - # -------------------- - Pk_rbm, wj_rbm = self.rbm(onflow, alpha, q_dyn, Vtas) - Pk_cam, wj_cam = self.camber_twist(q_dyn) - Pk_cs, wj_cs = self.cs(X, Ux2, q_dyn) - Pk_f, wj_f = self.flexible(Uf, dUf_dt, onflow, q_dyn, Vtas) + Tgeo2body, Tbody2geo = self.geo2body(X) + dUcg_dt, Uf, dUf_dt = self.recover_states(X) + Vtas, q_dyn = self.recover_Vtas(X) + onflow = self.recover_onflow(X) + alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) + Ux2 = self.get_Ux2(X) + + # aerodynamics + Pk_rbm, wj_rbm = self.rbm(onflow, alpha, q_dyn, Vtas) + Pk_cam, wj_cam = self.camber_twist(q_dyn) + Pk_cs, wj_cs = self.cs(X, Ux2, q_dyn) + Pk_f, wj_f = self.flexible(Uf, dUf_dt, onflow, q_dyn, Vtas) Pk_gust, wj_gust = self.gust(X, q_dyn) - + wj = wj_rbm + wj_cam + wj_cs + wj_f + wj_gust - Pk_idrag = self.idrag(wj, q_dyn) - - Pk_unsteady = Pk_rbm*0.0 - - # ------------------------------- - # --- correction coefficients --- - # ------------------------------- + Pk_idrag = self.idrag(wj, q_dyn) + + Pk_unsteady = Pk_rbm * 0.0 + + # correction coefficients Pb_corr = self.correctioon_coefficients(alpha, beta, q_dyn) - - # -------------------- - # --- landing gear --- - # -------------------- - Pextra, p2, dp2, ddp2, F1, F2 = self.landinggear(X, Tbody2geo) - - # --------------------------- - # --- summation of forces --- - # --------------------------- + + # landing gear + Pextra, _, dp2, ddp2, F1, F2 = self.landinggear(X, Tbody2geo) + + # summation of forces Pk_aero = Pk_rbm + Pk_cam + Pk_cs + Pk_f + Pk_gust + Pk_idrag + Pk_unsteady Pmac = np.dot(self.Dkx1.T, Pk_aero) Pb = np.dot(self.PHImac_cg.T, Pmac) + Pb_corr + np.dot(self.PHIextra_cg.T, Pextra) - + g_cg = gravitation_on_earth(self.PHInorm_cg, Tgeo2body) - - # ----------- - # --- EoM --- - # ----------- + + # EoM d2Ucg_dt2, Nxyz = self.rigid_EoM(dUcg_dt, Pb, g_cg, modus) - Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6])) ) + np.dot(self.PHIf_extra, Pextra) # viel schneller! + Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( + np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6]))) + np.dot(self.PHIf_extra, Pextra) # viel schneller! d2Uf_dt2 = self.flexible_EoM(dUf_dt, Uf, Pf) - - # ---------------------- - # --- CS derivatives --- - # ---------------------- - dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo,X[6:12])[0:3]) - - # -------------- - # --- output --- - # -------------- - + + # CS derivatives + dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo, X[6:12])[0:3]) + + # output if modus in ['trim', 'trim_full_output']: - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, - Nxyz[2], - Vtas, - beta, - )) + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, + Nxyz[2], + Vtas, + beta, + )) elif modus in ['sim', 'sim_full_output']: - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, - np.hstack((dp2, ddp2)), - Nxyz[2], - Vtas, - beta, - )) + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, + np.hstack((dp2, ddp2)), + Nxyz[2], + Vtas, + beta, + )) if modus in ['trim', 'sim']: return Y elif modus in ['trim_full_output', 'sim_full_output']: # calculate translations, velocities and accelerations of some additional points # (might also be used for sensors in a closed-loop system - p1 = -self.cggrid['offset'][:,2] + self.extragrid['offset'][:,2] + self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, X[0:6 ]))[self.extragrid['set'][:,2]] + self.PHIf_extra.T.dot(X[12:12+self.n_modes])[self.extragrid['set'][:,2]] # position LG attachment point over ground - dp1 = self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, np.dot(Tbody2geo, X[6:12])))[self.extragrid['set'][:,2]] + self.PHIf_extra.T.dot(X[12+self.n_modes:12+self.n_modes*2])[self.extragrid['set'][:,2]] # velocity LG attachment point - ddp1 = self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, np.dot(Tbody2geo, Y[6:12])))[self.extragrid['set'][:,2]] + self.PHIf_extra.T.dot(Y[12+self.n_modes:12+self.n_modes*2])[self.extragrid['set'][:,2]] # acceleration LG attachment point + # position LG attachment point over ground + p1 = -self.cggrid['offset'][:, 2] + self.extragrid['offset'][:, 2] \ + + self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, X[0:6]))[self.extragrid['set'][:, 2]] \ + + self.PHIf_extra.T.dot(X[12:12 + self.n_modes])[self.extragrid['set'][:, 2]] + # velocity LG attachment point + dp1 = self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, np.dot(Tbody2geo, X[6:12])))[self.extragrid['set'][:, 2]] \ + + self.PHIf_extra.T.dot(X[12 + self.n_modes:12 + self.n_modes * 2])[self.extragrid['set'][:, 2]] + # acceleration LG attachment point + ddp1 = self.PHIextra_cg.dot(np.dot(self.PHInorm_cg, np.dot(Tbody2geo, Y[6:12])))[self.extragrid['set'][:, 2]] \ + + self.PHIf_extra.T.dot(Y[12 + self.n_modes:12 + self.n_modes * 2])[self.extragrid['set'][:, 2]] - response = {'X': X, + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_rbm': Pk_rbm, @@ -115,7 +104,7 @@ def equations(self, X, t, modus): 'Pf': Pf, 'alpha': np.array([alpha]), 'beta': np.array([beta]), - #'Pg_aero': np.dot(PHIk_strc.T, Pk_aero), + # 'Pg_aero': np.dot(PHIk_strc.T, Pk_aero), 'Ux2': Ux2, 'dUcg_dt': dUcg_dt, 'd2Ucg_dt2': d2Ucg_dt2, @@ -131,35 +120,36 @@ def equations(self, X, t, modus): 'F1': F1, 'F2': F2, } - return response - + return response + def eval_equations(self, X_free, time, modus='trim_full_output'): - + if modus in ['trim', 'trim_full_output']: - # get inputs from trimcond and apply inputs from fsolve - X = np.array(self.trimcond_X[:,2], dtype='float') - X[np.where((self.trimcond_X[:,1] == 'free'))[0]] = X_free - elif modus in[ 'sim', 'sim_full_output']: + # get inputs from trimcond and apply inputs from fsolve + X = np.array(self.trimcond_X[:, 2], dtype='float') + X[np.where((self.trimcond_X[:, 1] == 'free'))[0]] = X_free + elif modus in ['sim', 'sim_full_output']: X = X_free - + # evaluate model equations - if modus=='trim': + if modus == 'trim': Y = self.equations(X, time, 'trim') # get the current values from Y and substract tamlab.figure() # fsolve only finds the roots; Y = 0 - Y_target_ist = Y[np.where((self.trimcond_Y[:,1] == 'target'))[0]] - Y_target_soll = np.array(self.trimcond_Y[:,2], dtype='float')[np.where((self.trimcond_Y[:,1] == 'target'))[0]] + Y_target_ist = Y[np.where((self.trimcond_Y[:, 1] == 'target'))[0]] + Y_target_soll = np.array(self.trimcond_Y[:, 2], dtype='float')[np.where((self.trimcond_Y[:, 1] == 'target'))[0]] out = Y_target_ist - Y_target_soll return out - elif modus=='sim': + elif modus == 'sim': Y = self.equations(X, time, 'sim') - return Y[self.solution.idx_state_derivatives+self.solution.idx_input_derivatives+self.solution.idx_lg_derivatives] # Nz ist eine Rechengroesse und keine Simulationsgroesse! - - elif modus=='sim_full_output': + return Y[self.solution.idx_state_derivatives + self.solution.idx_input_derivatives + + self.solution.idx_lg_derivatives] # Nz ist eine Rechengroesse und keine Simulationsgroesse! + + elif modus == 'sim_full_output': response = self.equations(X, time, 'sim_full_output') return response - - elif modus=='trim_full_output': + + elif modus == 'trim_full_output': response = self.equations(X, time, 'trim_full_output') return response diff --git a/loadskernel/equations/nonlin_steady.py b/loadskernel/equations/nonlin_steady.py index bd0c7cb..5083927 100644 --- a/loadskernel/equations/nonlin_steady.py +++ b/loadskernel/equations/nonlin_steady.py @@ -1,84 +1,67 @@ -''' -Created on Aug 2, 2019 - -@author: voss_ar -''' - import numpy as np -from scipy import linalg -from loadskernel.solution_tools import * from loadskernel.equations.steady import Steady +from loadskernel.solution_tools import gravitation_on_earth + class NonlinSteady(Steady): def equations(self, X, t, modus): self.counter += 1 # recover states - Tgeo2body, Tbody2geo = self.geo2body(X) - dUcg_dt, Uf, dUf_dt = self.recover_states(X) - Vtas, q_dyn = self.recover_Vtas(X) - onflow = self.recover_onflow(X) - alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) - Ux2 = self.get_Ux2(X) - # -------------------- - # --- aerodynamics --- - # -------------------- - Pk_rbm, wj_rbm = self.rbm_nonlin(onflow, alpha, Vtas) - Pk_cs, wj_cs = self.cs_nonlin(onflow, X, Ux2, Vtas) - Pk_f, wj_f = self.flexible_nonlin(onflow, Uf, dUf_dt, Vtas) - Pk_cam, wj_cam = self.camber_twist_nonlin(onflow) - - wj = (wj_rbm + wj_cs + wj_f + wj_cam)/Vtas - Pk_idrag = self.idrag(wj, q_dyn) - - Pk_gust = Pk_rbm*0.0 - Pk_unsteady = Pk_rbm*0.0 - - # ------------------------------- - # --- correction coefficients --- - # ------------------------------- + Tgeo2body, Tbody2geo = self.geo2body(X) + dUcg_dt, Uf, dUf_dt = self.recover_states(X) + Vtas, q_dyn = self.recover_Vtas(X) + onflow = self.recover_onflow(X) + alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) + Ux2 = self.get_Ux2(X) + + # aerodynamics + Pk_rbm, wj_rbm = self.rbm_nonlin(onflow, alpha, Vtas) + Pk_cs, wj_cs = self.cs_nonlin(onflow, X, Ux2, Vtas) + Pk_f, wj_f = self.flexible_nonlin(onflow, Uf, dUf_dt, Vtas) + Pk_cam, wj_cam = self.camber_twist_nonlin(onflow) + + wj = (wj_rbm + wj_cs + wj_f + wj_cam) / Vtas + Pk_idrag = self.idrag(wj, q_dyn) + + Pk_gust = Pk_rbm * 0.0 + Pk_unsteady = Pk_rbm * 0.0 + + # correction coefficients Pb_corr = self.correctioon_coefficients(alpha, beta, q_dyn) Pmac_vdrag = self.vdrag(alpha, q_dyn) - - # --------------------------- - # --- summation of forces --- - # --------------------------- + + # summation of forces Pk_aero = Pk_rbm + Pk_cam + Pk_cs + Pk_f + Pk_gust + Pk_idrag + Pk_unsteady Pmac = np.dot(self.Dkx1.T, Pk_aero) + Pmac_vdrag Pb = np.dot(self.PHImac_cg.T, Pmac) + Pb_corr - + g_cg = gravitation_on_earth(self.PHInorm_cg, Tgeo2body) - - # ----------- - # --- EoM --- - # ----------- + + # EoM d2Ucg_dt2, Nxyz = self.rigid_EoM(dUcg_dt, Pb, g_cg, modus) - Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6])) ) # viel schneller! + Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot(np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6]))) d2Uf_dt2 = self.flexible_EoM(dUf_dt, Uf, Pf) - - # ---------------------- - # --- CS derivatives --- - # ---------------------- - dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo,X[6:12])[0:3]) - # -------------- - # --- output --- - # -------------- - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, + # CS derivatives + dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo, X[6:12])[0:3]) + + # output + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, Nxyz[2], - Vtas, + Vtas, beta, - )) - + )) + if modus in ['trim', 'sim']: return Y elif modus in ['trim_full_output', 'sim_full_output']: - response = {'X': X, + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_rbm': Pk_rbm, @@ -96,7 +79,7 @@ def equations(self, X, t, modus): 'Pf': Pf, 'alpha': np.array([alpha]), 'beta': np.array([beta]), - #'Pg_aero': np.dot(PHIk_strc.T, Pk_aero), + # 'Pg_aero': np.dot(PHIk_strc.T, Pk_aero), 'Ux2': Ux2, 'dUcg_dt': dUcg_dt, 'd2Ucg_dt2': d2Ucg_dt2, @@ -106,7 +89,5 @@ def equations(self, X, t, modus): 'Nxyz': Nxyz, 'g_cg': g_cg, 'Pextra': [], - } - return response - - \ No newline at end of file + } + return response diff --git a/loadskernel/equations/state_space.py b/loadskernel/equations/state_space.py index 00ee6dd..e9dd167 100644 --- a/loadskernel/equations/state_space.py +++ b/loadskernel/equations/state_space.py @@ -1,61 +1,62 @@ +import copy +import logging import numpy as np + from scipy import linalg -import logging, copy -from matplotlib import pyplot as plt -from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable -import itertools -from loadskernel.units import * from loadskernel.equations.frequency_domain import PKMethod +class StateSpaceAnalysis(PKMethod): -class StateSpaceAnalysis(PKMethod): - def eval_equations(self): self.setup_frequence_parameters() - - logging.info('building systems') + + logging.info('building systems') self.build_AICs() eigenvalue, eigenvector = linalg.eig(self.system(self.Vvec[0])) - + bandbreite = eigenvalue.__abs__().max() - eigenvalue.__abs__().min() idx_pos = np.where(eigenvalue.__abs__() / bandbreite >= 1e-3)[0] # no zero eigenvalues idx_sort = np.argsort(np.abs(eigenvalue.imag[idx_pos])) # sort result by eigenvalue - eigenvalues0 = eigenvalue[idx_pos][idx_sort] + # eigenvalues0 = eigenvalue[idx_pos][idx_sort] eigenvectors0 = eigenvector[:, idx_pos][:, idx_sort] - - eigenvalues = []; eigenvectors = []; freqs = []; damping = []; Vtas = [] + + eigenvalues = [] + eigenvectors = [] + freqs = [] + damping = [] + Vtas = [] eigenvectors_old = copy.deepcopy(eigenvectors0) # loop over Vtas - for i_V in range(len(self.Vvec)): - Vtas_i = self.Vvec[i_V] + for _, V in enumerate(self.Vvec): + Vtas_i = V eigenvalues_i, eigenvectors_i = self.calc_eigenvalues(self.system(Vtas_i), eigenvectors_old) - - # store + + # store eigenvalues.append(eigenvalues_i) eigenvectors.append(eigenvectors_i) - freqs.append(eigenvalues_i.imag /2.0/np.pi) + freqs.append(eigenvalues_i.imag / 2.0 / np.pi) damping.append(eigenvalues_i.real / np.abs(eigenvalues_i)) - Vtas.append([Vtas_i]*len(eigenvalues_i)) - + Vtas.append([Vtas_i] * len(eigenvalues_i)) + eigenvectors_old = eigenvectors_i - - response = {'eigenvalues':np.array(eigenvalues), - 'eigenvectors':np.array(eigenvectors), - 'freqs':np.array(freqs), - 'damping':np.array(damping), - 'Vtas':np.array(Vtas), + + response = {'eigenvalues': np.array(eigenvalues), + 'eigenvectors': np.array(eigenvectors), + 'freqs': np.array(freqs), + 'damping': np.array(damping), + 'Vtas': np.array(Vtas), 'states': self.states, - } - return response + } + return response def calc_Qhh_1(self, Qjj): return self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj).dot(self.Djh_1))) - + def calc_Qhh_2(self, Qjj): return self.PHIlh.T.dot(self.aerogrid['Nmat'].T.dot(self.aerogrid['Amat'].dot(Qjj).dot(self.Djh_2))) - + def build_AICs(self): # do some pre-multiplications first, then the interpolation if self.jcl.aero['method'] in ['mona_steady']: @@ -66,47 +67,48 @@ def build_AICs(self): # for k_red in self.aero['k_red']: # D = np.zeros((self.aerogrid['n'], self.aerogrid['n']), dtype='complex') # j = 1j # imaginary number -# for i_pole, beta in zip(np.arange(0,self.aero['n_poles']), self.aero['betas']): -# D += ABCD[3+i_pole,:,:] * j*k_red / (j*k_red + beta) -# Qjj_unsteady = ABCD[0,:,:] + ABCD[1,:,:]*j*k_red + ABCD[2,:,:]*(j*k_red)**2 + D +# for i_pole, beta in zip(np.arange(0,self.aero['n_poles']), self.aero['betas']): +# D += ABCD[3+i_pole, :, :] * j*k_red / (j*k_red + beta) +# Qjj_unsteady = ABCD[0, :, :] + ABCD[1, :, :]*j*k_red + ABCD[2, :, :]*(j*k_red)**2 + D # Qhh_1.append(self.calc_Qhh_1(Qjj_unsteady)) # Qhh_2.append(self.calc_Qhh_2(Qjj_unsteady)) - + def system(self, Vtas): rho = self.atmo['rho'] Mhh_inv = np.linalg.inv(self.Mhh) - - upper_part = np.concatenate((np.zeros((self.n_modes, self.n_modes), dtype='float'), np.eye(self.n_modes, dtype='float')), axis=1) - lower_part = np.concatenate(( -Mhh_inv.dot(self.Khh - rho/2.0*Vtas**2.0*self.Qhh_1), -Mhh_inv.dot(self.Dhh - rho/2.0*Vtas*self.Qhh_2)), axis=1) + + upper_part = np.concatenate((np.zeros((self.n_modes, self.n_modes), dtype='float'), + np.eye(self.n_modes, dtype='float')), axis=1) + lower_part = np.concatenate((-Mhh_inv.dot(self.Khh - rho / 2.0 * Vtas ** 2.0 * self.Qhh_1), + -Mhh_inv.dot(self.Dhh - rho / 2.0 * Vtas * self.Qhh_2)), axis=1) A = np.concatenate((upper_part, lower_part)) - return A + class JacobiAnalysis(PKMethod): - + def __init__(self, response): - self.response = response - + self.response = response + def eval_equations(self): dxyz = self.response['X'][6:9] - Vtas = sum(dxyz**2)**0.5 + Vtas = sum(dxyz ** 2) ** 0.5 eigenvalue, eigenvector = linalg.eig(self.system()) - + bandbreite = eigenvalue.__abs__().max() - eigenvalue.__abs__().min() idx_pos = np.where(eigenvalue.__abs__() / bandbreite >= 1e-3)[0] # no zero eigenvalues idx_sort = np.argsort(np.abs(eigenvalue.imag[idx_pos])) # sort result by eigenvalue eigenvalues = eigenvalue[idx_pos][idx_sort] eigenvectors = eigenvector[:, idx_pos][:, idx_sort] - - # store + + # store self.response['eigenvalues'] = np.array(eigenvalues, ndmin=2) self.response['eigenvectors'] = np.array(eigenvectors, ndmin=3) - self.response['freqs'] = np.array(eigenvalues.imag /2.0/np.pi, ndmin=2) + self.response['freqs'] = np.array(eigenvalues.imag / 2.0 / np.pi, ndmin=2) self.response['damping'] = np.array(eigenvalues.real / np.abs(eigenvalues), ndmin=2) - self.response['Vtas'] = np.array([Vtas]*len(eigenvalues), ndmin=2) - + self.response['Vtas'] = np.array([Vtas] * len(eigenvalues), ndmin=2) + def system(self): - A = self.response['A'] + A = self.response['A'] return A - \ No newline at end of file diff --git a/loadskernel/equations/steady.py b/loadskernel/equations/steady.py index e54a619..e6acb67 100644 --- a/loadskernel/equations/steady.py +++ b/loadskernel/equations/steady.py @@ -1,78 +1,70 @@ +import logging +import numpy as np from scipy import linalg -from loadskernel.solution_tools import * from loadskernel.equations.common import Common, ConvergenceError +from loadskernel.solution_tools import gravitation_on_earth + class Steady(Common): def equations(self, X, t, modus): self.counter += 1 # recover states - Tgeo2body, Tbody2geo = self.geo2body(X) - dUcg_dt, Uf, dUf_dt = self.recover_states(X) - Vtas, q_dyn = self.recover_Vtas(X) - onflow = self.recover_onflow(X) - alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) - Ux2 = self.get_Ux2(X) - # -------------------- - # --- aerodynamics --- - # -------------------- - Pk_rbm, wj_rbm = self.rbm(onflow, alpha, q_dyn, Vtas) - Pk_cam, wj_cam = self.camber_twist(q_dyn) - Pk_cs, wj_cs = self.cs(X, Ux2, q_dyn) - Pk_f, wj_f = self.flexible(Uf, dUf_dt, onflow, q_dyn, Vtas) + Tgeo2body, Tbody2geo = self.geo2body(X) + dUcg_dt, Uf, dUf_dt = self.recover_states(X) + Vtas, q_dyn = self.recover_Vtas(X) + onflow = self.recover_onflow(X) + alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) + Ux2 = self.get_Ux2(X) + + # aerodynamics + Pk_rbm, wj_rbm = self.rbm(onflow, alpha, q_dyn, Vtas) + Pk_cam, wj_cam = self.camber_twist(q_dyn) + Pk_cs, wj_cs = self.cs(X, Ux2, q_dyn) + Pk_f, wj_f = self.flexible(Uf, dUf_dt, onflow, q_dyn, Vtas) Pk_gust, wj_gust = self.gust(X, q_dyn) - + wj = wj_rbm + wj_cam + wj_cs + wj_f + wj_gust - Pk_idrag = self.idrag(wj, q_dyn) - - Pk_unsteady = Pk_rbm*0.0 - - Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn , Uf, dUf_dt, t) - - # ------------------------------- - # --- correction coefficients --- - # ------------------------------- + Pk_idrag = self.idrag(wj, q_dyn) + + Pk_unsteady = Pk_rbm * 0.0 + + Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn, Uf, dUf_dt, t) + + # correction coefficients Pb_corr = self.correctioon_coefficients(alpha, beta, q_dyn) - - # --------------------------- - # --- summation of forces --- - # --------------------------- + + # summation of forces Pk_aero = Pk_rbm + Pk_cam + Pk_cs + Pk_f + Pk_gust + Pk_idrag + Pk_unsteady Pmac = np.dot(self.Dkx1.T, Pk_aero) Pb = np.dot(self.PHImac_cg.T, Pmac) + Pb_corr + Pb_ext - + g_cg = gravitation_on_earth(self.PHInorm_cg, Tgeo2body) - - # ----------- - # --- EoM --- - # ----------- + + # EoM d2Ucg_dt2, Nxyz = self.rigid_EoM(dUcg_dt, Pb, g_cg, modus) - Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6])) ) + Pf_ext # viel schneller! + Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot(np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6]))) + Pf_ext d2Uf_dt2 = self.flexible_EoM(dUf_dt, Uf, Pf) - - # ---------------------- - # --- CS derivatives --- - # ---------------------- - dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo,X[6:12])[0:3]) - - # -------------- - # --- output --- - # -------------- - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, + + # CS derivatives + dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo, X[6:12])[0:3]) + + # output + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, Nxyz[2], Vtas, beta, - )) - + )) + if modus in ['trim', 'sim']: return Y elif modus in ['trim_full_output']: - response = {'X': X, + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_rbm': Pk_rbm, @@ -98,11 +90,12 @@ def equations(self, X, t, modus): 'Nxyz': Nxyz, 'g_cg': g_cg, 'Pextra': Pextra, - } - return response + } + return response elif modus in ['sim_full_output']: - # For time domain simulations, typically not all results are required. To reduce the amount of data while maintaining compatibility with the trim, empty arrays are used. - response = {'X': X, + # For time domain simulations, typically not all results are required. To reduce the amount of data while + # maintaining compatibility with the trim, empty arrays are used. + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_aero': Pk_aero, @@ -120,46 +113,46 @@ def equations(self, X, t, modus): 'Nxyz': Nxyz, 'g_cg': g_cg, 'Pextra': Pextra, - } - return response - + } + return response + def eval_equations(self, X_free, time, modus='trim_full_output'): # this is a wrapper for the model equations 'eqn_basic' if modus in ['trim', 'trim_full_output']: - # get inputs from trimcond and apply inputs from fsolve - X = np.array(self.trimcond_X[:,2], dtype='float') - X[np.where((self.trimcond_X[:,1] == 'free'))[0]] = X_free - elif modus in[ 'sim', 'sim_full_output']: + # get inputs from trimcond and apply inputs from fsolve + X = np.array(self.trimcond_X[:, 2], dtype='float') + X[np.where((self.trimcond_X[:, 1] == 'free'))[0]] = X_free + elif modus in ['sim', 'sim_full_output']: X = X_free - + # evaluate model equations - if modus=='trim': + if modus == 'trim': Y = self.equations(X, time, 'trim') # get the current values from Y and substract tamlab.figure() # fsolve only finds the roots; Y = 0 - Y_target_ist = Y[np.where((self.trimcond_Y[:,1] == 'target'))[0]] - Y_target_soll = np.array(self.trimcond_Y[:,2], dtype='float')[np.where((self.trimcond_Y[:,1] == 'target'))[0]] + Y_target_ist = Y[np.where((self.trimcond_Y[:, 1] == 'target'))[0]] + Y_target_soll = np.array(self.trimcond_Y[:, 2], dtype='float')[np.where((self.trimcond_Y[:, 1] == 'target'))[0]] out = Y_target_ist - Y_target_soll return out - - elif modus=='sim': + + elif modus == 'sim': Y = self.equations(X, time, 'sim') - return Y[self.solution.idx_state_derivatives+self.solution.idx_input_derivatives] # Nz ist eine Rechengroesse und keine Simulationsgroesse! - - elif modus=='sim_full_output': + return Y[self.solution.idx_state_derivatives + self.solution.idx_input_derivatives] + + elif modus == 'sim_full_output': response = self.equations(X, time, 'sim_full_output') return response - - elif modus=='trim_full_output': + + elif modus == 'trim_full_output': response = self.equations(X, time, 'trim_full_output') return response - + def eval_equations_iteratively(self, X_free, time, modus='trim_full_output'): # this is a wrapper for the model equations - - # get inputs from trimcond and apply inputs from fsolve - X = np.array(self.trimcond_X[:,2], dtype='float') - X[np.where((self.trimcond_X[:,1] == 'free'))[0]] = X_free + + # get inputs from trimcond and apply inputs from fsolve + X = np.array(self.trimcond_X[:, 2], dtype='float') + X[np.where((self.trimcond_X[:, 1] == 'free'))[0]] = X_free logging.debug('X_free: {}'.format(X_free)) converged = False inner_loops = 0 @@ -168,7 +161,7 @@ def eval_equations_iteratively(self, X_free, time, modus='trim_full_output'): response = self.equations(X, time, 'trim_full_output') logging.info('Inner iteration {}, calculate structural deformations.'.format(self.counter)) Uf_new = linalg.solve(self.Kff, response['Pf']) - + # Add a relaxation factor between each loop to reduce overshoot and/or oscillations. # In case the solution hasn't converged in a reasonable time (say 8 loops), increase the relaxation. # A low relaxation factor is slower but more robust, f_relax = 1.0 implies no relaxation. @@ -178,41 +171,45 @@ def eval_equations_iteratively(self, X_free, time, modus='trim_full_output'): else: f_relax = 0.5 if inner_loops < 16: - epsilon = self.jcl.general['b_ref']*1.0e-5 + epsilon = self.jcl.general['b_ref'] * 1.0e-5 else: - epsilon = self.jcl.general['b_ref']*1.0e-4 + epsilon = self.jcl.general['b_ref'] * 1.0e-4 logging.info(' - Relaxation factor: {}'.format(f_relax)) logging.info(' - Epsilon: {:0.6g}'.format(epsilon)) - + # recover Uf_old from last step and blend with Uf_now - Uf_old = [self.trimcond_X[np.where((self.trimcond_X[:,0] == 'Uf'+str(i_mode)))[0][0],2] for i_mode in range(1, self.n_modes+1)] + Uf_old = [self.trimcond_X[np.where((self.trimcond_X[:, 0] == 'Uf' + str(i_mode)))[0][0], 2] for i_mode in + range(1, self.n_modes + 1)] Uf_old = np.array(Uf_old, dtype='float') - Uf_new = Uf_new*f_relax + Uf_old*(1.0-f_relax) + Uf_new = Uf_new * f_relax + Uf_old * (1.0 - f_relax) # set new values for Uf in trimcond for next loop and store in response for i_mode in range(self.n_modes): - self.trimcond_X[np.where((self.trimcond_X[:,0] == 'Uf'+str(i_mode+1)))[0][0],2] = '{:g}'.format(Uf_new[i_mode]) - response['X'][12+i_mode] = Uf_new[i_mode] - - # convergence parameter for iterative evaluation + self.trimcond_X[np.where((self.trimcond_X[:, 0] == 'Uf' + str(i_mode + 1)))[0][0], 2] = '{:g}'.format( + Uf_new[i_mode]) + response['X'][12 + i_mode] = Uf_new[i_mode] + + # convergence parameter for iterative evaluation Ug_f_body = np.dot(self.PHIf_strc.T, Uf_new.T).T - defo_new = Ug_f_body[self.strcgrid['set'][:,:3]].max() # Groesste Verformung, meistens Fluegelspitze + defo_new = Ug_f_body[self.strcgrid['set'][:, :3]].max() # Groesste Verformung, meistens Fluegelspitze ddefo = defo_new - self.defo_old self.defo_old = np.copy(defo_new) if np.abs(ddefo) < epsilon: converged = True - logging.info(' - Max. deformation: {:0.6g}, delta: {:0.6g} smaller than epsilon, converged.'.format( defo_new, ddefo)) + logging.info(' - Max. deformation: {:0.6g}, delta: {:0.6g} smaller than epsilon, converged.'.format( + defo_new, ddefo)) else: logging.info(' - Max. deformation: {:0.6g}, delta: {:0.6g}'.format(defo_new, ddefo)) if inner_loops > 20: - raise ConvergenceError('No convergence of structural deformation achieved after {} inner loops. Check convergence of CFD solution and/or convergence criterion "delta".'.format(inner_loops)) + raise ConvergenceError('No convergence of structural deformation achieved after {} inner loops. \ + Check convergence of CFD solution and/or convergence criterion "delta".'.format(inner_loops)) # get the current values from Y and substract tamlab.figure() # fsolve only finds the roots; Y = 0 - Y_target_ist = response['Y'][np.where((self.trimcond_Y[:,1] == 'target'))[0]] - Y_target_soll = np.array(self.trimcond_Y[:,2], dtype='float')[np.where((self.trimcond_Y[:,1] == 'target'))[0]] + Y_target_ist = response['Y'][np.where((self.trimcond_Y[:, 1] == 'target'))[0]] + Y_target_soll = np.array(self.trimcond_Y[:, 2], dtype='float')[np.where((self.trimcond_Y[:, 1] == 'target'))[0]] out = Y_target_ist - Y_target_soll - + if modus in ['trim']: return out - elif modus=='trim_full_output': + elif modus == 'trim_full_output': return response diff --git a/loadskernel/equations/unsteady.py b/loadskernel/equations/unsteady.py index a59fdb9..29b451f 100644 --- a/loadskernel/equations/unsteady.py +++ b/loadskernel/equations/unsteady.py @@ -1,83 +1,68 @@ -''' -Created on Aug 2, 2019 - -@author: voss_ar -''' import numpy as np - -from loadskernel.solution_tools import * from loadskernel.equations.common import Common +from loadskernel.solution_tools import gravitation_on_earth + class Unsteady(Common): def equations(self, X, t, modus): self.counter += 1 # recover states - Tgeo2body, Tbody2geo = self.geo2body(X) - dUcg_dt, Uf, dUf_dt = self.recover_states(X) - Vtas, q_dyn = self.recover_Vtas(self.X0) - onflow = self.recover_onflow(X) - alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) - Ux2 = self.get_Ux2(X) - # -------------------- - # --- aerodynamics --- - # -------------------- - Pk_rbm, wj_rbm = self.rbm(onflow, alpha, q_dyn, Vtas) - Pk_cam, wj_cam = self.camber_twist(q_dyn) - Pk_cs, wj_cs = self.cs(X, Ux2, q_dyn) - Pk_f, wj_f = self.flexible(Uf, dUf_dt, onflow, q_dyn, Vtas) + Tgeo2body, Tbody2geo = self.geo2body(X) + dUcg_dt, Uf, dUf_dt = self.recover_states(X) + Vtas, q_dyn = self.recover_Vtas(self.X0) + onflow = self.recover_onflow(X) + alpha, beta, gamma = self.windsensor(X, Vtas, Uf, dUf_dt) + Ux2 = self.get_Ux2(X) + + # --- aerodynamics --- + Pk_rbm, wj_rbm = self.rbm(onflow, alpha, q_dyn, Vtas) + Pk_cam, wj_cam = self.camber_twist(q_dyn) + Pk_cs, wj_cs = self.cs(X, Ux2, q_dyn) + Pk_f, wj_f = self.flexible(Uf, dUf_dt, onflow, q_dyn, Vtas) Pk_gust, wj_gust = self.gust(X, q_dyn) - + wj = wj_rbm + wj_cam + wj_cs + wj_f + wj_gust - Pk_idrag = self.idrag(wj, q_dyn) + Pk_idrag = self.idrag(wj, q_dyn) Pk_unsteady, dlag_states_dt = self.unsteady(X, t, wj, Uf, dUf_dt, onflow, q_dyn, Vtas) - - Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn , Uf, dUf_dt, t) - - # ------------------------------- - # --- correction coefficients --- - # ------------------------------- + + Pextra, Pb_ext, Pf_ext = self.engine(X, Vtas, q_dyn, Uf, dUf_dt, t) + + # --- correction coefficients --- Pb_corr = self.correctioon_coefficients(alpha, beta, q_dyn) - - # --------------------------- - # --- summation of forces --- - # --------------------------- + + # --- summation of forces --- Pk_aero = Pk_rbm + Pk_cam + Pk_cs + Pk_f + Pk_gust + Pk_idrag + Pk_unsteady Pmac = np.dot(self.Dkx1.T, Pk_aero) Pb = np.dot(self.PHImac_cg.T, Pmac) + Pb_corr + Pb_ext - + g_cg = gravitation_on_earth(self.PHInorm_cg, Tgeo2body) - - # ----------- - # --- EoM --- - # ----------- + + # --- EoM --- d2Ucg_dt2, Nxyz = self.rigid_EoM(dUcg_dt, Pb, g_cg, modus) - Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot( np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6])) ) + Pf_ext # viel schneller! + Pf = np.dot(self.PHIkf.T, Pk_aero) + self.Mfcg.dot(np.hstack((d2Ucg_dt2[0:3] - g_cg, d2Ucg_dt2[3:6]))) + Pf_ext d2Uf_dt2 = self.flexible_EoM(dUf_dt, Uf, Pf) - - # ---------------------- + # --- CS derivatives --- - # ---------------------- - dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo,X[6:12])[0:3]) - - # -------------- - # --- output --- - # -------------- - Y = np.hstack((np.dot(Tbody2geo,X[6:12]), - np.dot(self.PHIcg_norm, d2Ucg_dt2), - dUf_dt, - d2Uf_dt2, - dcommand, + dcommand = self.get_command_derivatives(t, X, Vtas, gamma, alpha, beta, Nxyz, np.dot(Tbody2geo, X[6:12])[0:3]) + + # --- output --- + Y = np.hstack((np.dot(Tbody2geo, X[6:12]), + np.dot(self.PHIcg_norm, d2Ucg_dt2), + dUf_dt, + d2Uf_dt2, + dcommand, dlag_states_dt, Nxyz[2], Vtas, beta, - )) - + )) + if modus in ['trim', 'sim']: return Y + elif modus in ['trim_full_output']: - response = {'X': X, + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_rbm': Pk_rbm, @@ -103,11 +88,13 @@ def equations(self, X, t, modus): 'Nxyz': Nxyz, 'g_cg': g_cg, 'Pextra': Pextra, - } - return response + } + return response + elif modus in ['sim_full_output']: - # For time domain simulations, typically not all results are required. To reduce the amount of data while maintaining compatibility with the trim, empty arrays are used. - response = {'X': X, + # For time domain simulations, typically not all results are required. To reduce the amount of data while + # maintaining compatibility with the trim, empty arrays are used. + response = {'X': X, 'Y': Y, 't': np.array([t]), 'Pk_aero': Pk_aero, @@ -127,19 +114,19 @@ def equations(self, X, t, modus): 'Nxyz': Nxyz, 'g_cg': g_cg, 'Pextra': Pextra, - } - return response - + } + return response + def eval_equations(self, X_free, time, modus='trim_full_output'): - if modus in[ 'sim', 'sim_full_output']: + if modus in ['sim', 'sim_full_output']: X = X_free - + # evaluate model equations - if modus=='sim': + if modus == 'sim': Y = self.equations(X, time, 'sim') - return Y[self.solution.idx_state_derivatives+self.solution.idx_input_derivatives+self.solution.idx_lag_derivatives] # Nz ist eine Rechengroesse und keine Simulationsgroesse! - - elif modus=='sim_full_output': + return Y[self.solution.idx_state_derivatives + self.solution.idx_input_derivatives + + self.solution.idx_lag_derivatives] + + elif modus == 'sim_full_output': response = self.equations(X, time, 'sim_full_output') return response - \ No newline at end of file diff --git a/loadskernel/fem_interfaces/b2000_interface.py b/loadskernel/fem_interfaces/b2000_interface.py index f302392..f98ce45 100644 --- a/loadskernel/fem_interfaces/b2000_interface.py +++ b/loadskernel/fem_interfaces/b2000_interface.py @@ -1,50 +1,52 @@ +import copy +import logging import numpy as np -import copy, logging from loadskernel.fem_interfaces.nastran_interface import NastranInterface from loadskernel.io_functions import read_b2000 + class B2000Interface(NastranInterface): - + def get_stiffness_matrix(self): self.KGG = read_b2000.read_csv(self.jcl.geom['filename_KGG'], sparse_output=True) - self.GM = None + self.GM = None def get_mass_matrix(self, i_mass): self.i_mass = i_mass self.MGG = read_b2000.read_csv(self.jcl.mass['filename_MGG'][self.i_mass], sparse_output=True) return self.MGG - + def get_dofs(self): """ Instead of the u-set, B2000 uses a matrix R to relate the g-set to the f-set. - Example: - Kff = R.T * Kgg * R + Example: + Kff = R.T * Kgg * R ug = R * uf """ - self.Rtrans = read_b2000.read_csv(self.jcl.geom['filename_Rtrans'], sparse_output=True) - + self.Rtrans = read_b2000.read_csv(self.jcl.geom['filename_Rtrans'], sparse_output=True) + def prepare_stiffness_matrices(self): self.KFF = self.Rtrans.dot(self.KGG).dot(self.Rtrans.T) - + def prepare_mass_matrices(self): logging.info('Prepare mass matrices for independent and free DoFs (f-set)') self.MFF = self.Rtrans.dot(self.MGG).dot(self.Rtrans.T) - + def modalanalysis(self): modes_selection = copy.deepcopy(self.jcl.mass['modes'][self.i_mass]) - if self.jcl.mass['omit_rb_modes']: + if self.jcl.mass['omit_rb_modes']: modes_selection += 6 eigenvalue, eigenvector = self.calc_elastic_modes(self.KFF, self.MFF, modes_selection.max()) - logging.info( 'From these {} modes, the following {} modes are selected: {}'.format(modes_selection.max(), len(modes_selection), modes_selection)) + logging.info('From these {} modes, the following {} modes are selected: {}'.format( + modes_selection.max(), len(modes_selection), modes_selection)) self.eigenvalues_f = eigenvalue[modes_selection - 1] # reconstruct modal matrix for g-set / strcgrid - self.PHIstrc_f = np.zeros((6*self.strcgrid['n'], len(modes_selection))) - i = 0 # counter selected modes + self.PHIstrc_f = np.zeros((6 * self.strcgrid['n'], len(modes_selection))) + i = 0 # counter selected modes for i_mode in modes_selection - 1: # deformation of f-set due to i_mode is the ith column of the eigenvector - Uf = eigenvector[:,i_mode].real.reshape((-1,1)) + Uf = eigenvector[:, i_mode].real.reshape((-1, 1)) Ug = self.Rtrans.T.dot(Uf) - self.PHIstrc_f[:,i] = Ug.squeeze() + self.PHIstrc_f[:, i] = Ug.squeeze() i += 1 - return \ No newline at end of file diff --git a/loadskernel/fem_interfaces/cofe_interface.py b/loadskernel/fem_interfaces/cofe_interface.py index ce32b33..288aceb 100644 --- a/loadskernel/fem_interfaces/cofe_interface.py +++ b/loadskernel/fem_interfaces/cofe_interface.py @@ -1,30 +1,33 @@ import scipy + from loadskernel.fem_interfaces.nastran_interface import NastranInterface + class CoFEInterface(NastranInterface): - + def get_stiffness_matrix(self): - with open(self.jcl.geom['filename_CoFE']) as fid: - CoFE_data = scipy.io.loadmat(fid) + with open(self.jcl.geom['filename_CoFE']) as fid: + CoFE_data = scipy.io.loadmat(fid) self.KGG = CoFE_data['KGG'] - self.GM = CoFE_data['GM'].T # convert from CoFE to Nastran - + self.GM = CoFE_data['GM'].T # convert from CoFE to Nastran + def get_mass_matrix(self): - with open(self.jcl.geom['filename_CoFE']) as fid: + with open(self.jcl.geom['filename_CoFE']) as fid: CoFE_data = scipy.io.loadmat(fid) self.MGG = CoFE_data['MGG'] return self.MGG - + def get_dofs(self): - # Prepare some data required for modal analysis which is not mass case dependent. - with open(self.jcl.geom['filename_CoFE']) as fid: CoFE_data = scipy.io.loadmat(fid) - + # Prepare some data required for modal analysis which is not mass case dependent. + with open(self.jcl.geom['filename_CoFE']) as fid: + CoFE_data = scipy.io.loadmat(fid) + # The DoFs of f-, s- and m-set are indexed with respect to g-set # Convert indexing from Matlab to Python - self.pos_f = CoFE_data['nf_g'].squeeze()-1 - self.pos_s = CoFE_data['s'].squeeze()-1 - self.pos_m = CoFE_data['m'].squeeze()-1 - self.pos_n = CoFE_data['n'].squeeze()-1 - + self.pos_f = CoFE_data['nf_g'].squeeze() - 1 + self.pos_s = CoFE_data['s'].squeeze() - 1 + self.pos_m = CoFE_data['m'].squeeze() - 1 + self.pos_n = CoFE_data['n'].squeeze() - 1 + # Free DoFs (f-set) indexed with respect to n-set - self.pos_fn = CoFE_data['nf_n'].squeeze()-1 \ No newline at end of file + self.pos_fn = CoFE_data['nf_n'].squeeze() - 1 diff --git a/loadskernel/fem_interfaces/fem_helper.py b/loadskernel/fem_interfaces/fem_helper.py index ce73949..73719a7 100644 --- a/loadskernel/fem_interfaces/fem_helper.py +++ b/loadskernel/fem_interfaces/fem_helper.py @@ -1,30 +1,32 @@ import numpy as np import matplotlib.pyplot as plt - + + def calc_MAC(X, Y, plot=True): - MAC = np.zeros((X.shape[1],Y.shape[1]),) + MAC = np.zeros((X.shape[1], Y.shape[1]),) for jj in range(Y.shape[1]): for ii in range(X.shape[1]): - q1 = np.dot(X[:,ii].conj().T, X[:,ii]) - q2 = np.dot(Y[:,jj].conj().T, Y[:,jj]) - q3 = np.dot(X[:,ii].conj().T, Y[:,jj]) - MAC[ii,jj] = np.real( np.abs(q3)**2/q1/q2 ) - + q1 = np.dot(X[:, ii].conj().T, X[:, ii]) + q2 = np.dot(Y[:, jj].conj().T, Y[:, jj]) + q3 = np.dot(X[:, ii].conj().T, Y[:, jj]) + MAC[ii, jj] = np.real(np.abs(q3) ** 2 / q1 / q2) + if plot: plt.figure() plt.pcolor(MAC, cmap='hot_r') plt.colorbar() plt.grid('on') - + return MAC, plt - else: - return MAC + return MAC + def force_matrix_symmetry(matrix): - return (matrix + matrix.T)/2.0 + return (matrix + matrix.T) / 2.0 + def check_matrix_symmetry(matrix): # Check if a matrix is symmetric by forcing symmetry and then compare with the original matrix. matrix_sym = force_matrix_symmetry(matrix) - result = (matrix != matrix_sym).nnz==0 - return result \ No newline at end of file + result = (matrix != matrix_sym).nnz == 0 + return result diff --git a/loadskernel/fem_interfaces/nastran_f06_interface.py b/loadskernel/fem_interfaces/nastran_f06_interface.py index cb6003c..610598a 100644 --- a/loadskernel/fem_interfaces/nastran_f06_interface.py +++ b/loadskernel/fem_interfaces/nastran_f06_interface.py @@ -1,58 +1,59 @@ -import numpy as np import copy +import numpy as np -import loadskernel.io_functions.read_mona as read_mona -import loadskernel.io_functions.read_op4 as read_op4 from loadskernel.fem_interfaces.nastran_interface import NastranInterface +from loadskernel.io_functions import read_mona +from loadskernel.io_functions import read_op4 + class Nastranf06Interface(NastranInterface): - + def get_stiffness_matrix(self): - self.KGG = read_op4.load_matrix(self.jcl.geom['filename_KGG'], sparse_output=True, sparse_format=True) - self.GM = None + self.KGG = read_op4.load_matrix(self.jcl.geom['filename_KGG'], sparse_output=True, sparse_format=True) + self.GM = None - def modes_from_SOL103(self): # Mff, Kff and PHIstrc_f - eigenvalues, eigenvectors, node_ids_all = read_mona.NASTRAN_f06_modal(self.jcl.mass['filename_S103'][self.i_mass]) + eigenvalues, eigenvectors, _ = read_mona.NASTRAN_f06_modal(self.jcl.mass['filename_S103'][self.i_mass]) nodes_selection = self.strcgrid['ID'] modes_selection = copy.deepcopy(self.jcl.mass['modes'][self.i_mass]) if self.jcl.mass['omit_rb_modes']: modes_selection += 6 eigenvalues, eigenvectors = read_mona.reduce_modes(eigenvalues, eigenvectors, nodes_selection, modes_selection) - PHIf_strc = np.zeros((len(self.jcl.mass['modes'][self.i_mass]), len(self.strcgrid['ID'])*6)) - for i_mode in range(len(modes_selection)): - eigenvector = eigenvectors[str(modes_selection[i_mode])][:,1:] - PHIf_strc[i_mode,:] = eigenvector.reshape((1,-1))[0] + PHIf_strc = np.zeros((len(self.jcl.mass['modes'][self.i_mass]), len(self.strcgrid['ID']) * 6)) + for i, mode in enumerate(modes_selection): + eigenvector = eigenvectors[str(mode)][:, 1:] + PHIf_strc[i, :] = eigenvector.reshape((1, -1))[0] self.PHIstrc_f = PHIf_strc.T self.eigenvalues_f = np.array(eigenvalues['GeneralizedStiffness']) - - def cg_from_SOL103(self): - massmatrix_0, inertia, offset_cg, CID = read_mona.Nastran_weightgenerator(self.jcl.mass['filename_S103'][self.i_mass]) - cggrid = {"ID": np.array([9000+self.i_mass]), - "offset": np.array([offset_cg]), - "set": np.array([[0, 1, 2, 3, 4, 5]]), + + def cg_from_SOL103(self): + massmatrix_0, inertia, offset_cg, CID = read_mona.Nastran_weightgenerator(self.jcl.mass['filename_S103'][self.i_mass]) + cggrid = {'ID': np.array([9000 + self.i_mass]), + 'offset': np.array([offset_cg]), + 'set': np.array([[0, 1, 2, 3, 4, 5]]), 'CD': np.array([CID]), 'CP': np.array([CID]), 'coord_desc': 'bodyfixed', } - cggrid_norm = {"ID": np.array([9300+self.i_mass]), - "offset": np.array([[-offset_cg[0], offset_cg[1], -offset_cg[2]]]), - "set": np.array([[0, 1, 2, 3, 4, 5]]), - 'CD': np.array([9300]), - 'CP': np.array([9300]), - 'coord_desc': 'bodyfixed_DIN9300', - } + cggrid_norm = {'ID': np.array([9300 + self.i_mass]), + 'offset': np.array([[-offset_cg[0], offset_cg[1], -offset_cg[2]]]), + 'set': np.array([[0, 1, 2, 3, 4, 5]]), + 'CD': np.array([9300]), + 'CP': np.array([9300]), + 'coord_desc': 'bodyfixed_DIN9300', + } # assemble mass matrix about center of gravity, relativ to the axis of the basic coordinate system # DO NOT switch signs for coupling terms of I to suite EoMs, Nastran already did this! - Mb = np.zeros((6,6)) - Mb[0,0] = massmatrix_0[0,0] - Mb[1,1] = massmatrix_0[0,0] - Mb[2,2] = massmatrix_0[0,0] - Mb[3:6,3:6] = inertia #np.array([[1,-1,-1],[-1,1,-1],[-1,-1,1]]) * inertia - - self.cggrid = cggrid # store for later internal use - self.cggrid_norm = cggrid_norm # store for later internal use - - return Mb, cggrid, cggrid_norm + Mb = np.zeros((6, 6)) + Mb[0, 0] = massmatrix_0[0, 0] + Mb[1, 1] = massmatrix_0[0, 0] + Mb[2, 2] = massmatrix_0[0, 0] + Mb[3:6, 3:6] = inertia # np.array([[1,-1,-1],[-1,1,-1],[-1,-1,1]]) * inertia + + # store for later internal use + self.cggrid = cggrid + self.cggrid_norm = cggrid_norm + + return Mb, cggrid, cggrid_norm diff --git a/loadskernel/fem_interfaces/nastran_interface.py b/loadskernel/fem_interfaces/nastran_interface.py index 2426c4b..f9fd0f2 100644 --- a/loadskernel/fem_interfaces/nastran_interface.py +++ b/loadskernel/fem_interfaces/nastran_interface.py @@ -1,61 +1,62 @@ +import copy +import logging + import scipy import numpy as np -import copy, logging -import loadskernel.io_functions.read_mona as read_geom -import loadskernel.spline_rules as spline_rules -import loadskernel.spline_functions as spline_functions -from loadskernel.io_functions import read_op2, read_op4, read_h5 -from loadskernel.fem_interfaces import fem_helper +from loadskernel.io_functions import read_op2, read_op4, read_h5, read_mona +from loadskernel import spline_functions, spline_rules + class NastranInterface(object): - + def __init__(self, jcl, strcgrid, coord): self.jcl = jcl self.strcgrid = strcgrid self.coord = coord - + def get_stiffness_matrix(self): if 'filename_h5' in self.jcl.geom: self.KGG = read_h5.load_matrix(self.jcl.geom['filename_h5'], name='KGG') - self.GM = read_h5.load_matrix(self.jcl.geom['filename_h5'], name='GM').T + self.GM = read_h5.load_matrix(self.jcl.geom['filename_h5'], name='GM').T elif 'filename_KGG' in self.jcl.geom and 'filename_GM' in self.jcl.geom: - self.KGG = read_op4.load_matrix(self.jcl.geom['filename_KGG'], sparse_output=True, sparse_format=True) - self.GM = read_op4.load_matrix(self.jcl.geom['filename_GM'], sparse_output=True, sparse_format=True) + self.KGG = read_op4.load_matrix(self.jcl.geom['filename_KGG'], sparse_output=True, sparse_format=True) + self.GM = read_op4.load_matrix(self.jcl.geom['filename_GM'], sparse_output=True, sparse_format=True) else: logging.error('Please provide filename(s) of .h5 or .OP4 files.') - + def get_mass_matrix(self, i_mass): self.i_mass = i_mass if 'filename_h5' in self.jcl.mass: self.MGG = read_h5.load_matrix(self.jcl.mass['filename_h5'][self.i_mass], name='MGG') elif 'filename_MGG' in self.jcl.mass: self.MGG = read_op4.load_matrix(self.jcl.mass['filename_MGG'][self.i_mass], sparse_output=True, sparse_format=True) - else: + else: logging.error('Please provide filename(s) of .h5 or .OP4 files.') return self.MGG - + def get_sets_from_bitposes(self, x_dec): """ Reference: - National Aeronautics and Space Administration, The Nastran Programmer's Manual, NASA SP-223(01). Washington, D.C.: COSMIC, 1972. + National Aeronautics and Space Administration, The Nastran Programmer's Manual, NASA SP-223(01). Washington, D.C., + COSMIC, 1972. Section 2.3.13.3 USET (TABLE), page 2.3-61 Assumption: There is (only) the f-, s- & m-set - + Bit positons | decimal notation | old sets | new set ------------------------------------------------------------- - 31, 30 and 25 | 2, 4 and 128 | 'S', 'O' and 'A' | g-set - 22 | 1024 | 'SB' | s-set + 31, 30 and 25 | 2, 4 and 128 | 'S', 'O' and 'A' | g-set + 22 | 1024 | 'SB' | s-set 32 | 1 | 'M' | m-set """ - + logging.info('Extracting bit positions from USET to determine DoFs') # The DoFs of f-, s- and m-set are indexed with respect to g-set self.pos_m = [i for i, x in enumerate(x_dec) if x == 1] - self.pos_f = [i for i, x in enumerate(x_dec) if x in [2,4,128]] - # Not (yet) sure if this is a bug, but in some models there are IDs labled with bit positions 22 and 24, + self.pos_f = [i for i, x in enumerate(x_dec) if x in [2, 4, 128]] + # Not (yet) sure if this is a bug, but in some models there are IDs labled with bit positions 22 and 24, # resulting in a decimal notation of 1280. So far, they were treated like the s-set. - self.pos_s = [i for i, x in enumerate(x_dec) if x in [1024,1280]] + self.pos_s = [i for i, x in enumerate(x_dec) if x in [1024, 1280]] # The n-set is the sum of s-set and f-set self.pos_n = self.pos_s + self.pos_f # Sort the n-set by the DoFs @@ -65,27 +66,27 @@ def get_sets_from_bitposes(self, x_dec): self.pos_fn = list(np.where(sorting >= len(self.pos_s))[0]) def get_dofs(self): - # Prepare some data required for modal analysis which is not mass case dependent. + # Prepare some data required for modal analysis which is not mass case dependent. # The uset is actually geometry dependent and should go into the geometry section. # However, it is only required for modal analysis... - logging.info( 'Read USET from OP2-file {} ...'.format( self.jcl.geom['filename_uset'] )) + logging.info('Read USET from OP2-file {} ...'.format(self.jcl.geom['filename_uset'])) op2_data = read_op2.read_post_op2(self.jcl.geom['filename_uset'], verbose=False) if op2_data['uset'] is None: - logging.error( 'No USET found in OP2-file {} !'.format( self.jcl.geom['filename_uset'] )) + logging.error('No USET found in OP2-file {} !'.format(self.jcl.geom['filename_uset'])) self.get_sets_from_bitposes(op2_data['uset']) - + def prepare_stiffness_matrices(self): logging.info('Prepare stiffness matrices for independent and free DoFs (f-set)') # K_Gnn = K_G(n,n) + K_G(n,m)*Gm + Gm.'* K_G(n,m).' + Gm.'* K_G(m,m)*Gm; - Knn = self.KGG[self.pos_n, :][:,self.pos_n] \ - + self.KGG[self.pos_n, :][:,self.pos_m].dot(self.GM.T) \ - + self.GM.dot(self.KGG[self.pos_m, :][:,self.pos_n]) \ - + self.GM.dot(self.KGG[self.pos_m, :][:,self.pos_m].dot(self.GM.T)) - self.KFF = Knn[self.pos_fn, :][:,self.pos_fn] + Knn = self.KGG[self.pos_n, :][:, self.pos_n] \ + + self.KGG[self.pos_n, :][:, self.pos_m].dot(self.GM.T) \ + + self.GM.dot(self.KGG[self.pos_m, :][:, self.pos_n]) \ + + self.GM.dot(self.KGG[self.pos_m, :][:, self.pos_m].dot(self.GM.T)) + self.KFF = Knn[self.pos_fn, :][:, self.pos_fn] def get_aset(self, bdf_reader): logging.info('Preparing a- and o-set...') - asets = read_geom.add_SET1(bdf_reader.cards['ASET1']) + asets = read_mona.add_SET1(bdf_reader.cards['ASET1']) # Simplification: take only those GRIDs where all 6 components belong to the a-set aset_values = asets['values'][asets['ID'].index(123456)] # Then, take the set of the a-set because IDs might be given repeatedly @@ -93,235 +94,243 @@ def get_aset(self, bdf_reader): # Convert to ndarray and then use list comprehension. This is the fastest way of finding indices. gset_grids = np.array(self.strcgrid['ID']) id_g2a = [np.where(gset_grids == x)[0][0] for x in aset_grids] - pos_a = self.strcgrid['set'][id_g2a,:].reshape((1,-1))[0] + pos_a = self.strcgrid['set'][id_g2a, :].reshape((1, -1))[0] # Make sure DoFs of a-set are really in f-set (e.g. due to faulty user input) self.pos_a = pos_a[np.in1d(pos_a, self.pos_f)] # The remainders will be omitted self.pos_o = np.setdiff1d(self.pos_f, self.pos_a) # Convert to ndarray and then use list comprehension. This is the fastest way of finding indices. - pos_f_ndarray = np.array(self.pos_f) + pos_f_ndarray = np.array(self.pos_f) self.pos_f2a = [np.where(pos_f_ndarray == x)[0][0] for x in self.pos_a] self.pos_f2o = [np.where(pos_f_ndarray == x)[0][0] for x in self.pos_o] - logging.info( 'The a-set has {} DoFs and the o-set has {} DoFs'.format(len(self.pos_a), len(self.pos_o) )) - + logging.info('The a-set has {} DoFs and the o-set has {} DoFs'.format(len(self.pos_a), len(self.pos_o))) + def prepare_stiffness_matrices_for_guyan(self): # In a first step, the positions of the a- and o-set DoFs are prepared. # Then the equations are solved for the stiffness matrix. - # References: + # References: # R. Guyan, "Reduction of Stiffness and Mass Matrices," AIAA Journal, vol. 3, no. 2, p. 280, 1964. - # MSC.Software Corporation, "Matrix Operations," in MSC Nastran Linear Static Analysis User's Guide, vol. 2003, D. M. McLean, Ed. 2003, p. 473. - # MSC.Software Corporation, "Theoretical Basis for Reduction Methods," in MSC.Nastran Version 70 Advanced Dynamic Analysis User's Guide, Version 70., H. David N., Ed. p. 63. + # MSC.Software Corporation, "Matrix Operations," in MSC Nastran Linear Static Analysis User's Guide, vol. 2003, + # D. M. McLean, Ed. 2003, p. 473. + # MSC.Software Corporation, "Theoretical Basis for Reduction Methods," in MSC.Nastran Version 70 Advanced Dynamic + # Analysis User's Guide, Version 70., H. David N., Ed. p. 63. - logging.info( "Guyan reduction of stiffness matrix Kff --> Kaa ...") - logging.info( ' - partitioning') + logging.info("Guyan reduction of stiffness matrix Kff --> Kaa ...") + logging.info(' - partitioning') K = {} - K['A'] = self.KFF[self.pos_f2a,:][:,self.pos_f2a] - K['B'] = self.KFF[self.pos_f2a,:][:,self.pos_f2o] - K['B_trans'] = self.KFF[self.pos_f2o,:][:,self.pos_f2a] - K['C'] = self.KFF[self.pos_f2o,:][:,self.pos_f2o] + K['A'] = self.KFF[self.pos_f2a, :][:, self.pos_f2a] + K['B'] = self.KFF[self.pos_f2a, :][:, self.pos_f2o] + K['B_trans'] = self.KFF[self.pos_f2o, :][:, self.pos_f2a] + K['C'] = self.KFF[self.pos_f2o, :][:, self.pos_f2o] # nach Nastran # Anstelle die Inverse zu bilden, wird ein Gleichungssystem geloest. Das ist schneller! - logging.info( " - solving") - self.Goa = - scipy.sparse.linalg.spsolve(K['C'], K['B_trans']) - self.Goa = self.Goa.toarray() # Sparse format is no longer required as Goa is dense anyway! - self.Kaa = K['A'].toarray() + K['B'].dot(self.Goa) # make sure the output is an ndarray - + logging.info(" - solving") + self.Goa = -scipy.sparse.linalg.spsolve(K['C'], K['B_trans']) + self.Goa = self.Goa.toarray() # Sparse format is no longer required as Goa is dense anyway! + self.Kaa = K['A'].toarray() + K['B'].dot(self.Goa) # make sure the output is an ndarray + def prepare_mass_matrices(self): logging.info('Prepare mass matrices for independent and free DoFs (f-set)') - Mnn = self.MGG[self.pos_n, :][:,self.pos_n] \ - + self.MGG[self.pos_n, :][:,self.pos_m].dot(self.GM.T) \ - + self.GM.dot(self.MGG[self.pos_m, :][:,self.pos_n]) \ - + self.GM.dot(self.MGG[self.pos_m, :][:,self.pos_m].dot(self.GM.T)) - self.MFF = Mnn[self.pos_fn, :][:,self.pos_fn] - + Mnn = self.MGG[self.pos_n, :][:, self.pos_n] \ + + self.MGG[self.pos_n, :][:, self.pos_m].dot(self.GM.T) \ + + self.GM.dot(self.MGG[self.pos_m, :][:, self.pos_n]) \ + + self.GM.dot(self.MGG[self.pos_m, :][:, self.pos_m].dot(self.GM.T)) + self.MFF = Mnn[self.pos_fn, :][:, self.pos_fn] + def guyanreduction(self): # First, Guyan's equations are solved for the current mass matrix. - # In a second step, the eigenvalue-eigenvector problem is solved. According to Guyan, the solution is closely but not exactly preserved. + # In a second step, the eigenvalue-eigenvector problem is solved. According to Guyan, the solution is closely but + # not exactly preserved. # Next, the eigenvector for the g-set/strcgrid is reconstructed. # In a final step, the generalized mass and stiffness matrices are calculated. - # The nomenclature might be a little confusing at this point, because the flexible mode shapes and Nastran's free DoFs (f-set) are both labeled with the subscript 'f'... - logging.info( "Guyan reduction of mass matrix Mff --> Maa ...") - logging.info( ' - partitioning') + # The nomenclature might be a little confusing at this point, because the flexible mode shapes and Nastran's free + # DoFs (f-set) are both labeled with the subscript 'f'... + logging.info("Guyan reduction of mass matrix Mff --> Maa ...") + logging.info(' - partitioning') M = {} - M['A'] = self.MFF[self.pos_f2a,:][:,self.pos_f2a] - M['B'] = self.MFF[self.pos_f2a,:][:,self.pos_f2o] - M['B_trans'] = self.MFF[self.pos_f2o,:][:,self.pos_f2a] - M['C'] = self.MFF[self.pos_f2o,:][:,self.pos_f2o] - logging.info( " - solving") + M['A'] = self.MFF[self.pos_f2a, :][:, self.pos_f2a] + M['B'] = self.MFF[self.pos_f2a, :][:, self.pos_f2o] + M['B_trans'] = self.MFF[self.pos_f2o, :][:, self.pos_f2a] + M['C'] = self.MFF[self.pos_f2o, :][:, self.pos_f2o] + logging.info(" - solving") # a) original formulation according to R. Guyan # Maa = M['A'] - M['B'].dot(self.Goa) - self.Goa.T.dot( M['B_trans'] - M['C'].dot(self.Goa) ) - + # b) General Dynamic Reduction as implemented in Nastran (signs are switched!) - Maa = M['A'].toarray() + M['B'].dot(self.Goa) + self.Goa.T.dot( M['B_trans'].toarray() + M['C'].dot(self.Goa) ) # make sure the output is an ndarray - - modes_selection = copy.deepcopy(self.jcl.mass['modes'][self.i_mass]) - if self.jcl.mass['omit_rb_modes']: + Maa = M['A'].toarray() + M['B'].dot(self.Goa) + self.Goa.T.dot(M['B_trans'].toarray() + M['C'].dot(self.Goa)) + + modes_selection = copy.deepcopy(self.jcl.mass['modes'][self.i_mass]) + if self.jcl.mass['omit_rb_modes']: modes_selection += 6 eigenvalue, eigenvector = self.calc_elastic_modes(self.Kaa, Maa, modes_selection.max()) - logging.info( 'From these {} modes, the following {} modes are selected: {}'.format(modes_selection.max(), len(modes_selection), modes_selection)) + logging.info('From these {} modes, the following {} modes are selected: {}'.format( + modes_selection.max(), len(modes_selection), modes_selection)) self.eigenvalues_f = eigenvalue[modes_selection - 1] # reconstruct modal matrix for g-set / strcgrid - self.PHIstrc_f = np.zeros((6*self.strcgrid['n'], len(modes_selection))) - i = 0 # counter selected modes + self.PHIstrc_f = np.zeros((6 * self.strcgrid['n'], len(modes_selection))) + i = 0 # counter selected modes for i_mode in modes_selection - 1: # deformation of a-set due to i_mode is the ith column of the eigenvector - Ua = eigenvector[:,i_mode].real.reshape((-1,1)) + Ua = eigenvector[:, i_mode].real.reshape((-1, 1)) Uf = self.project_aset_to_fset(Ua) Ug = self.project_fset_to_gset(Uf) # store vector in modal matrix - self.PHIstrc_f[:,i] = Ug.squeeze() + self.PHIstrc_f[:, i] = Ug.squeeze() i += 1 return - + def project_aset_to_fset(self, Ua): # calc ommitted grids with Guyan Uo = self.Goa.dot(Ua) # assemble deflections of a and o to f - Uf = np.zeros((len(self.pos_f),1)) + Uf = np.zeros((len(self.pos_f), 1)) Uf[self.pos_f2a] = Ua Uf[self.pos_f2o] = Uo return Uf - + def project_fset_to_gset(self, Uf): # deflection of s-set is zero, because that's the sense of an SPC ... - Us = np.zeros((len(self.pos_s),1)) + Us = np.zeros((len(self.pos_s), 1)) # assemble deflections of s and f to n - Un = np.zeros((6*self.strcgrid['n'],1)) + Un = np.zeros((6 * self.strcgrid['n'], 1)) Un[self.pos_f] = Uf Un[self.pos_s] = Us Un = Un[self.pos_n] # calc remaining deflections with GM Um = self.GM.T.dot(Un) # assemble everything to Ug - Ug = np.zeros((6*self.strcgrid['n'],1)) + Ug = np.zeros((6 * self.strcgrid['n'], 1)) Ug[self.pos_f] = Uf Ug[self.pos_s] = Us Ug[self.pos_m] = Um return Ug - + def modalanalysis(self): modes_selection = copy.deepcopy(self.jcl.mass['modes'][self.i_mass]) - if self.jcl.mass['omit_rb_modes']: + if self.jcl.mass['omit_rb_modes']: modes_selection += 6 eigenvalue, eigenvector = self.calc_elastic_modes(self.KFF, self.MFF, modes_selection.max()) - logging.info( 'From these {} modes, the following {} modes are selected: {}'.format(modes_selection.max(), len(modes_selection), modes_selection)) + logging.info('From these {} modes, the following {} modes are selected: {}'.format( + modes_selection.max(), len(modes_selection), modes_selection)) self.eigenvalues_f = eigenvalue[modes_selection - 1] # reconstruct modal matrix for g-set / strcgrid - self.PHIstrc_f = np.zeros((6*self.strcgrid['n'], len(modes_selection))) - i = 0 # counter selected modes + self.PHIstrc_f = np.zeros((6 * self.strcgrid['n'], len(modes_selection))) + i = 0 # counter selected modes for i_mode in modes_selection - 1: # deformation of f-set due to i_mode is the ith column of the eigenvector - Uf = eigenvector[:,i_mode].real.reshape((-1,1)) + Uf = eigenvector[:, i_mode].real.reshape((-1, 1)) Ug = self.project_fset_to_gset(Uf) # store vector in modal matrix - self.PHIstrc_f[:,i] = Ug.squeeze() + self.PHIstrc_f[:, i] = Ug.squeeze() i += 1 return - + def calc_modal_matrices(self): # calc modal mass and stiffness - logging.info( 'Working on f-set') + logging.info('Working on f-set') Mff = self.PHIstrc_f.T.dot(self.MGG.dot(self.PHIstrc_f)) Kff = self.PHIstrc_f.T.dot(self.KGG.dot(self.PHIstrc_f)) Dff = self.calc_damping(self.eigenvalues_f) - - logging.info( 'Working on h-set') + + logging.info('Working on h-set') # add synthetic modes for rigid body motion eigenvalues_rb, PHIb_strc = self.calc_rbm_modes() PHIstrc_h = np.concatenate((PHIb_strc, self.PHIstrc_f), axis=1) Mhh = PHIstrc_h.T.dot(self.MGG.dot(PHIstrc_h)) # switch signs of off-diagonal terms in rb mass matrix - # Mhh[:6,:6] = self.Mb + # Mhh[:6, :6] = self.Mb Khh = PHIstrc_h.T.dot(self.KGG.dot(PHIstrc_h)) # set rigid body stiffness explicitly to zero Khh[np.diag_indices(len(eigenvalues_rb))] = eigenvalues_rb Dhh = self.calc_damping(np.concatenate((eigenvalues_rb, self.eigenvalues_f))) return Mff, Kff, Dff, self.PHIstrc_f.T, Mhh, Khh, Dhh, PHIstrc_h.T - + def calc_elastic_modes(self, K, M, n_modes): # perform modal analysis on a-set - logging.info( 'Modal analysis for first {} modes...'.format( n_modes )) - eigenvalue, eigenvector = scipy.sparse.linalg.eigs(A=K, M=M , k=n_modes, sigma=0) - idx_sort = np.argsort(eigenvalue) # sort result by eigenvalue + logging.info('Modal analysis for first {} modes...'.format(n_modes)) + eigenvalue, eigenvector = scipy.sparse.linalg.eigs(A=K, M=M, k=n_modes, sigma=0) + idx_sort = np.argsort(eigenvalue) # sort result by eigenvalue eigenvalue = eigenvalue[idx_sort] - eigenvector = eigenvector[:,idx_sort] - frequencies = np.real(eigenvalue)**0.5 /2/np.pi - logging.info( 'Found {} modes with the following frequencies [Hz]:'.format(len(eigenvalue))) - logging.info( frequencies ) + eigenvector = eigenvector[:, idx_sort] + frequencies = np.real(eigenvalue) ** 0.5 / 2 / np.pi + logging.info('Found {} modes with the following frequencies [Hz]:'.format(len(eigenvalue))) + logging.info(frequencies) n_rbm_estimate = np.sum(np.isnan(frequencies) + np.less(frequencies, 0.1)) if all([n_rbm_estimate < 6, self.jcl.mass['omit_rb_modes']]): - logging.warning('There are only {} modes < 0.1 Hz! Is the number of rigid body modes correct ??'.format(n_rbm_estimate)) + logging.warning('There are only {} modes < 0.1 Hz! Is the number of rigid body modes correct ??'.format( + n_rbm_estimate)) return eigenvalue.real, eigenvector.real - + def calc_rbm_modes(self): eigenvalues = np.zeros(5) rules = spline_rules.rules_point(self.cggrid, self.strcgrid) PHIstrc_cg = spline_functions.spline_rb(self.cggrid, '', self.strcgrid, '', rules, self.coord) - return eigenvalues, PHIstrc_cg[:,1:] - + return eigenvalues, PHIstrc_cg[:, 1:] + def calc_damping(self, eigenvalues): # Currently, only modal damping is implemented. See Bianchi et al., # "Using modal damping for full model transient analysis. Application to # pantograph/catenary vibration", presented at the ISMA, 2010. n = len(eigenvalues) - Dff = np.zeros((n,n)) + Dff = np.zeros((n, n)) if hasattr(self.jcl, 'damping') and 'method' in self.jcl.damping and self.jcl.damping['method'] == 'modal': - logging.info( 'Damping: modal damping of {}'.format( self.jcl.damping['damping'] )) - d = eigenvalues**0.5 * 2.0 * self.jcl.damping['damping'] - np.fill_diagonal(Dff, d) # matrix Dff is modified in-place, no return value + logging.info('Damping: modal damping of {}'.format(self.jcl.damping['damping'])) + d = eigenvalues ** 0.5 * 2.0 * self.jcl.damping['damping'] + np.fill_diagonal(Dff, d) # matrix Dff is modified in-place, no return value elif hasattr(self.jcl, 'damping'): - logging.warning( 'Damping method not implemented: {}'.format( str(self.jcl.damping['method']) )) - logging.warning( 'Damping: assuming zero damping.' ) + logging.warning('Damping method not implemented: {}'.format(str(self.jcl.damping['method']))) + logging.warning('Damping: assuming zero damping.') else: - logging.info( 'Damping: assuming zero damping.' ) + logging.info('Damping: assuming zero damping.') return Dff - + def calc_cg(self): - logging.info( 'Calculate center of gravity, mass and inertia (GRDPNT)...') + logging.info('Calculate center of gravity, mass and inertia (GRDPNT)...') # First step: calculate M0 m0grid = {"ID": np.array([999999]), - "offset": np.array([[0,0,0]]), + "offset": np.array([[0, 0, 0]]), "set": np.array([[0, 1, 2, 3, 4, 5]]), 'CD': np.array([0]), 'CP': np.array([0]), - } + } rules = spline_rules.rules_point(m0grid, self.strcgrid) - PHIstrc_m0 = spline_functions.spline_rb(m0grid, '', self.strcgrid, '', rules, self.coord) + PHIstrc_m0 = spline_functions.spline_rb(m0grid, '', self.strcgrid, '', rules, self.coord) m0 = PHIstrc_m0.T.dot(self.MGG.dot(PHIstrc_m0)) - m = m0[0,0] - # Second step: calculate CG location with the help of the inertia moments - offset_cg = [m0[1,5],m0[2,3],m0[0,4]]/m - - cggrid = {"ID": np.array([9000+self.i_mass]), + m = m0[0, 0] + # Second step: calculate CG location with the help of the inertia moments + offset_cg = [m0[1, 5], m0[2, 3], m0[0, 4]] / m + + cggrid = {"ID": np.array([9000 + self.i_mass]), "offset": np.array([offset_cg]), "set": np.array([[0, 1, 2, 3, 4, 5]]), 'CD': np.array([0]), 'CP': np.array([0]), 'coord_desc': 'bodyfixed', - } - cggrid_norm = {"ID": np.array([9300+self.i_mass]), + } + cggrid_norm = {"ID": np.array([9300 + self.i_mass]), "offset": np.array([[-offset_cg[0], offset_cg[1], -offset_cg[2]]]), "set": np.array([[0, 1, 2, 3, 4, 5]]), 'CD': np.array([9300]), 'CP': np.array([9300]), 'coord_desc': 'bodyfixed_DIN9300', - } + } # Third step: calculate Mb rules = spline_rules.rules_point(cggrid, self.strcgrid) - PHIstrc_mb = spline_functions.spline_rb(cggrid, '', self.strcgrid, '', rules, self.coord) + PHIstrc_mb = spline_functions.spline_rb(cggrid, '', self.strcgrid, '', rules, self.coord) mb = PHIstrc_mb.T.dot(self.MGG.dot(PHIstrc_mb)) # switch signs for coupling terms of I to suite EoMs - Mb = np.zeros((6,6)) - Mb[0,0] = m - Mb[1,1] = m - Mb[2,2] = m - Mb[3:6,3:6] = np.array([[1,-1,-1],[-1,1,-1],[-1,-1,1]]) * mb[3:6,3:6] - - logging.info( 'Mass: {}'.format(Mb[0,0])) - logging.info( 'CG at: {}'.format(offset_cg)) - logging.info( 'Inertia: \n{}'.format(Mb[3:6,3:6])) - - self.cggrid = cggrid # store for later internal use - self.cggrid_norm = cggrid_norm # store for later internal use - - return Mb, cggrid, cggrid_norm \ No newline at end of file + Mb = np.zeros((6, 6)) + Mb[0, 0] = m + Mb[1, 1] = m + Mb[2, 2] = m + Mb[3:6, 3:6] = np.array([[1, -1, -1], [-1, 1, -1], [-1, -1, 1]]) * mb[3:6, 3:6] + + logging.info('Mass: {}'.format(Mb[0, 0])) + logging.info('CG at: {}'.format(offset_cg)) + logging.info('Inertia: \n{}'.format(Mb[3:6, 3:6])) + + # store for later internal use + self.cggrid = cggrid + self.cggrid_norm = cggrid_norm + + return Mb, cggrid, cggrid_norm diff --git a/loadskernel/gather_loads.py b/loadskernel/gather_loads.py index 800a7c0..6ff87b1 100644 --- a/loadskernel/gather_loads.py +++ b/loadskernel/gather_loads.py @@ -1,14 +1,16 @@ -import numpy as np import logging +import numpy as np from loadskernel.io_functions.data_handling import load_hdf5_dict + class GatherLoads: """ In this class actually no calculation is done, it merely gathers data. From the response, the monstations are assembled in a more convenient order and format. From the response of a dynamic simulation, the peaks are identified and saved as snapshots (dyn2stat). """ + def __init__(self, jcl, model): self.jcl = jcl self.model = model @@ -22,16 +24,16 @@ def __init__(self, jcl, model): 'CP': self.mongrid['CP'][i_station], 'offset': self.mongrid['offset'][i_station], 'subcases': [], - 'loads':[], - 't':[], - 'turbulence_loads':[], - 'correlations':[], - } - self.dyn2stat = {'Pg': [], + 'loads': [], + 't': [], + 'turbulence_loads': [], + 'correlations': [], + } + self.dyn2stat = {'Pg': [], 'subcases': [], 'subcases_ID': [], - } - + } + def gather_monstations(self, trimcase, response): logging.info('gathering information on monitoring stations from response(s)...') for i_station in range(self.mongrid['n']): @@ -39,13 +41,13 @@ def gather_monstations(self, trimcase, response): subcase = str(trimcase['subcase']) # Unterscheidung zwischen Trim und Zeit-Simulation, da die Dimensionen der response anders sind (n_step x n_value) if len(response['t']) > 1: - loads = response['Pmon_local'][:,self.mongrid['set'][i_station,:]] + loads = response['Pmon_local'][:, self.mongrid['set'][i_station, :]] # save time data per subcase self.monstations[name][str(response['i'])] = {'subcase': subcase, 'loads': loads, - 't': response['t'] } + 't': response['t']} else: - loads = response['Pmon_local'][0,self.mongrid['set'][i_station,:]] + loads = response['Pmon_local'][0, self.mongrid['set'][i_station, :]] self.monstations[name]['subcases'].append(subcase) self.monstations[name]['loads'].append(loads) self.monstations[name]['t'].append(response['t'][0]) @@ -53,60 +55,61 @@ def gather_monstations(self, trimcase, response): if 'Pmon_turb' in response: # Check if there are any limit turbulence loads available in the response. # If yes, map them into the monstations. - loads = response['Pmon_turb'][0,self.mongrid['set'][i_station,:]] - correlations = response['correlations'][self.mongrid['set'][i_station,:],:][:,self.mongrid['set'][i_station,:]] + loads = response['Pmon_turb'][0, self.mongrid['set'][i_station, :]] + correlations = response['correlations'][self.mongrid['set'][i_station, + :], :][:, self.mongrid['set'][i_station, :]] self.monstations[name]['turbulence_loads'].append(loads) - self.monstations[name]['correlations'].append(correlations) - + self.monstations[name]['correlations'].append(correlations) def gather_dyn2stat(self, response): """ Schnittlasten an den Monitoring Stationen raus schreiben (z.B. zum Plotten) Knotenlasten raus schreiben (weiterverarbeitung z.B. als FORCE und MOMENT Karten fuer Nastran) """ - logging.info('searching min/max in time data at {} monitoring stations and gathering loads (dyn2stat)...'.format(len(self.monstations.keys()))) + logging.info('searching min/max in time data at {} monitoring stations ' + 'and gathering loads (dyn2stat)...'.format(len(self.monstations.keys()))) if len(response['t']) > 1: i_case = str(response['i']) - timeslices_dyn2stat = np.array([],dtype=int) - for key in self.monstations.keys(): - pos_max_loads_over_time = np.argmax(self.monstations[key][i_case]['loads'], 0) - pos_min_loads_over_time = np.argmin(self.monstations[key][i_case]['loads'], 0) + timeslices_dyn2stat = np.array([], dtype=int) + for key, monstation in self.monstations.items(): + pos_max_loads_over_time = np.argmax(monstation[i_case]['loads'], 0) + pos_min_loads_over_time = np.argmin(monstation[i_case]['loads'], 0) """ Although the time-based approach considers all DoFs, it might lead to fewer time slices / snapshots compared to Fz,min/max, Mx,min/max, ..., because identical time slices are avoided. """ - timeslices_dyn2stat = np.concatenate((timeslices_dyn2stat, pos_max_loads_over_time, pos_min_loads_over_time)) # remember identified time slices - logging.info('reducing dyn2stat data...') + # Remember identified time slices + timeslices_dyn2stat = np.concatenate((timeslices_dyn2stat, pos_max_loads_over_time, pos_min_loads_over_time)) + logging.info('reducing dyn2stat data...') timeslices_dyn2stat = np.unique(timeslices_dyn2stat) nastran_subcase_running_number = 1 - for pos in timeslices_dyn2stat: + for pos in timeslices_dyn2stat: # save nodal loads Pg for this time slice - self.dyn2stat['Pg'].append(response['Pg'][pos,:]) - subcases_dyn2stat_string = str(self.monstations[key][i_case]['subcase']) + '_t={:.3f}'.format(self.monstations[key][i_case]['t'][pos,0]) + self.dyn2stat['Pg'].append(response['Pg'][pos, :]) + subcases_dyn2stat_string = str(self.monstations[key][i_case]['subcase']) + '_t={:.3f}'.format( + self.monstations[key][i_case]['t'][pos, 0]) self.dyn2stat['subcases'].append(subcases_dyn2stat_string) """ Generate unique IDs for subcases: - Take first digits from original subcase, then add a running number. This is a really stupid approach, - but we are limited to 8 digits and need to make the best out of that... Using 5 digits for the subcase - and 3 digits for the running number appears to work for most cases. This is only important for Nastran - Force and Moment cards as Nastran does not like subcases in the style 'xy_t=0.123' and requires numbers + Take first digits from original subcase, then add a running number. This is a really stupid approach, + but we are limited to 8 digits and need to make the best out of that... Using 5 digits for the subcase + and 3 digits for the running number appears to work for most cases. This is only important for Nastran + Force and Moment cards as Nastran does not like subcases in the style 'xy_t=0.123' and requires numbers in ascending order. """ - self.dyn2stat['subcases_ID'].append( int(self.monstations[key][i_case]['subcase'])*1000 + nastran_subcase_running_number ) + self.dyn2stat['subcases_ID'].append(int(self.monstations[key][i_case]['subcase']) + * 1000 + nastran_subcase_running_number) # save section loads to monstations - for key in self.monstations.keys(): - self.monstations[key]['subcases'].append(subcases_dyn2stat_string) - self.monstations[key]['loads'].append(self.monstations[key][i_case]['loads'][pos,:]) - self.monstations[key]['t'].append(self.monstations[key][i_case]['t'][pos,:]) + for key, monstation in self.monstations.items(): + monstation['subcases'].append(subcases_dyn2stat_string) + monstation['loads'].append(monstation[i_case]['loads'][pos, :]) + monstation['t'].append(monstation[i_case]['t'][pos, :]) # increase counter of running number by 1 nastran_subcase_running_number += 1 else: i_case = response['i'] - self.dyn2stat['Pg'].append(response['Pg'][0,:]) + self.dyn2stat['Pg'].append(response['Pg'][0, :]) self.dyn2stat['subcases'].append(str(self.jcl.trimcase[i_case]['subcase'])) self.dyn2stat['subcases_ID'].append(int(self.jcl.trimcase[i_case]['subcase'])) - - - \ No newline at end of file diff --git a/loadskernel/grid_trafo.py b/loadskernel/grid_trafo.py index 9c5dc63..0d75853 100644 --- a/loadskernel/grid_trafo.py +++ b/loadskernel/grid_trafo.py @@ -1,7 +1,8 @@ +from itertools import groupby import numpy as np import scipy.sparse as sp from loadskernel.utils.sparse_matrices import insert_lil -from itertools import groupby + def all_equal(iterable): # As suggested on stackoverflow, this is the fastest way to check if all element in an array are equal @@ -9,93 +10,97 @@ def all_equal(iterable): g = groupby(iterable) return next(g, True) and not next(g, False) + def grid_trafo(grid, coord, dest_coord): """ This function transforms a grid into a new coordiante system. - The coordiante transformatin can be speed up by using a matrix operation, but only in case all point are - in the same coordinate system. To handle different coorinate systems in one grid (e.g. Nastran GRID points + The coordiante transformatin can be speed up by using a matrix operation, but only in case all point are + in the same coordinate system. To handle different coorinate systems in one grid (e.g. Nastran GRID points given with different CP), the coordinate transformation has to be applied gridpoint-wise. """ - pos_coord_dest = np.where(np.array(coord['ID'])==dest_coord)[0][0] + pos_coord_dest = np.where(np.array(coord['ID']) == dest_coord)[0][0] if all_equal(grid['CP']): # get the right transformation matrices - pos_coord_orig = np.where(np.array(coord['ID'])==grid['CP'][0])[0][0] + pos_coord_orig = np.where(np.array(coord['ID']) == grid['CP'][0])[0][0] # perform transformation - offset_tmp = coord['dircos'][pos_coord_orig].dot(grid['offset'].T).T + coord['offset'][pos_coord_orig] - offset = coord['dircos'][pos_coord_dest].T.dot(offset_tmp.T).T + coord['offset'][pos_coord_dest] + offset_tmp = coord['dircos'][pos_coord_orig].dot(grid['offset'].T).T + coord['offset'][pos_coord_orig] + offset = coord['dircos'][pos_coord_dest].T.dot(offset_tmp.T).T + coord['offset'][pos_coord_dest] # store new offsets in grid grid['offset'] = offset - grid['CP'] = np.array([dest_coord]*grid['n']) + grid['CP'] = np.array([dest_coord] * grid['n']) else: for i_point in range(len(grid['ID'])): - pos_coord_orig = np.where(np.array(coord['ID'])==grid['CP'][i_point])[0][0] - offset_tmp = np.dot(coord['dircos'][pos_coord_orig],grid['offset'][i_point])+coord['offset'][pos_coord_orig] - offset = np.dot(coord['dircos'][pos_coord_dest].T,offset_tmp)+coord['offset'][pos_coord_dest] + pos_coord_orig = np.where(np.array(coord['ID']) == grid['CP'][i_point])[0][0] + offset_tmp = np.dot(coord['dircos'][pos_coord_orig], grid['offset'][i_point]) + coord['offset'][pos_coord_orig] + offset = np.dot(coord['dircos'][pos_coord_dest].T, offset_tmp) + coord['offset'][pos_coord_dest] grid['offset'][i_point] = offset grid['CP'][i_point] = dest_coord + def vector_trafo(grid, coord, forcevector, dest_coord): """ - This function transforms a force (or displacement) vector into a new coordiante system. It is assumed - the force and moments vector is in the coordinate system defined with CD. As above, matrix operations are + This function transforms a force (or displacement) vector into a new coordiante system. It is assumed + the force and moments vector is in the coordinate system defined with CD. As above, matrix operations are applied if all source coord systems are identical. """ - pos_coord_dest = np.where(np.array(coord['ID'])==dest_coord)[0][0] + pos_coord_dest = np.where(np.array(coord['ID']) == dest_coord)[0][0] if all_equal(grid['CD']): # get the right transformation matrices - pos_coord_orig = np.where(np.array(coord['ID'])==grid['CD'][0])[0][0] - # expand for 6 degrees of freedom - dircos_source = np.zeros((6,6)) - dircos_source[0:3,0:3] = coord['dircos'][pos_coord_orig] - dircos_source[3:6,3:6] = coord['dircos'][pos_coord_orig] - dircos_dest = np.zeros((6,6)) - dircos_dest[0:3,0:3] = coord['dircos'][pos_coord_dest] - dircos_dest[3:6,3:6] = coord['dircos'][pos_coord_dest] + pos_coord_orig = np.where(np.array(coord['ID']) == grid['CD'][0])[0][0] + # expand for 6 degrees of freedom + dircos_source = np.zeros((6, 6)) + dircos_source[0:3, 0:3] = coord['dircos'][pos_coord_orig] + dircos_source[3:6, 3:6] = coord['dircos'][pos_coord_orig] + dircos_dest = np.zeros((6, 6)) + dircos_dest[0:3, 0:3] = coord['dircos'][pos_coord_dest] + dircos_dest[3:6, 3:6] = coord['dircos'][pos_coord_dest] # perform transformation - forcevector_trans = dircos_dest.T.dot(dircos_source.dot(forcevector[grid['set']].T)).T.reshape(1,-1).squeeze() + forcevector_trans = dircos_dest.T.dot(dircos_source.dot(forcevector[grid['set']].T)).T.reshape(1, -1).squeeze() else: forcevector_trans = np.zeros(np.shape(forcevector)) for i_station in range(grid['n']): - pos_coord_orig = np.where(np.array(coord['ID'])==grid['CD'][i_station])[0][0] - - dircos_source = np.zeros((6,6)) - dircos_source[0:3,0:3] = coord['dircos'][pos_coord_orig] - dircos_source[3:6,3:6] = coord['dircos'][pos_coord_orig] - dircos_dest = np.zeros((6,6)) - dircos_dest[0:3,0:3] = coord['dircos'][pos_coord_dest] - dircos_dest[3:6,3:6] = coord['dircos'][pos_coord_dest] - - forcevector_trans[grid['set'][i_station]] = dircos_dest.T.dot(dircos_source.dot(forcevector[grid['set'][i_station]])) + pos_coord_orig = np.where(np.array(coord['ID']) == grid['CD'][i_station])[0][0] + + dircos_source = np.zeros((6, 6)) + dircos_source[0:3, 0:3] = coord['dircos'][pos_coord_orig] + dircos_source[3:6, 3:6] = coord['dircos'][pos_coord_orig] + dircos_dest = np.zeros((6, 6)) + dircos_dest[0:3, 0:3] = coord['dircos'][pos_coord_dest] + dircos_dest[3:6, 3:6] = coord['dircos'][pos_coord_dest] + + forcevector_trans[grid['set'][i_station]] = dircos_dest.T.dot(dircos_source.dot( + forcevector[grid['set'][i_station]])) return forcevector_trans + def calc_transformation_matrix(coord, grid_i, set_i, coord_i, grid_d, set_d, coord_d, dimensions=''): # T_i and T_d are the translation matrices that do the projection to the coordinate systems of gird_i and grid_d - # Parameters coord_i and coord_d allow to switch between the coordinate systems CD and CP (compare Nastran User Guide) with + # Parameters coord_i and coord_d allow to switch between the coordinate systems CD and CP (compare Nastran User Guide) with # - CP = coordinate system of grid point offset # - CD = coordinate system of loads vector - # Example of application: + # Example of application: # splinematrix = T_d.T.dot(T_di).dot(T_i) # Pmon_local = T_d.T.dot(T_i).dot(Pmon_global) - + if dimensions != '' and len(dimensions) == 2: dimensions_i = dimensions[0] dimensions_d = dimensions[1] else: - dimensions_i = 6*len(grid_i['set'+set_i]) - dimensions_d = 6*len(grid_d['set'+set_d]) - + dimensions_i = 6 * len(grid_i['set' + set_i]) + dimensions_d = 6 * len(grid_d['set' + set_d]) + # Using sparse matrices is faster and more efficient. - T_i = sp.lil_matrix((dimensions_i,dimensions_i)) + T_i = sp.lil_matrix((dimensions_i, dimensions_i)) for i_i in range(len(grid_i['ID'])): pos_coord_i = coord['ID'].index(grid_i[coord_i][i_i]) - T_i = insert_lil( T_i, coord['dircos'][pos_coord_i], grid_i['set'+set_i][i_i,0:3], grid_i['set'+set_i][i_i,0:3] ) - T_i = insert_lil( T_i, coord['dircos'][pos_coord_i], grid_i['set'+set_i][i_i,3:6], grid_i['set'+set_i][i_i,3:6] ) - - T_d = sp.lil_matrix((dimensions_d,dimensions_d)) + T_i = insert_lil(T_i, coord['dircos'][pos_coord_i], grid_i['set' + set_i][i_i, 0:3], grid_i['set' + set_i][i_i, 0:3]) + T_i = insert_lil(T_i, coord['dircos'][pos_coord_i], grid_i['set' + set_i][i_i, 3:6], grid_i['set' + set_i][i_i, 3:6]) + + T_d = sp.lil_matrix((dimensions_d, dimensions_d)) for i_d in range(len(grid_d['ID'])): pos_coord_d = coord['ID'].index(grid_d[coord_d][i_d]) - T_d = insert_lil( T_d, coord['dircos'][pos_coord_d], grid_d['set'+set_d][i_d,0:3], grid_d['set'+set_d][i_d,0:3] ) - T_d = insert_lil( T_d, coord['dircos'][pos_coord_d], grid_d['set'+set_d][i_d,3:6], grid_d['set'+set_d][i_d,3:6] ) - return T_i.tocsc(), T_d.tocsc() \ No newline at end of file + T_d = insert_lil(T_d, coord['dircos'][pos_coord_d], grid_d['set' + set_d][i_d, 0:3], grid_d['set' + set_d][i_d, 0:3]) + T_d = insert_lil(T_d, coord['dircos'][pos_coord_d], grid_d['set' + set_d][i_d, 3:6], grid_d['set' + set_d][i_d, 3:6]) + return T_i.tocsc(), T_d.tocsc() diff --git a/loadskernel/integrate.py b/loadskernel/integrate.py index 0035d15..5f64581 100644 --- a/loadskernel/integrate.py +++ b/loadskernel/integrate.py @@ -1,5 +1,5 @@ -import numpy as np import logging +import numpy as np """ Achtung - Verwirrungsgefahr! @@ -7,11 +7,14 @@ Nomenklature in den Modellgleichungen: Y = f(t,X) """ + class RungeKutta4(): + # Klassiches Runge-Kutta Verfahren fuer ein Anfangswertprobelm 1. Ordnung - # Implementiert wie beschieben in: https://de.wikipedia.org/wiki/Klassisches_Runge-Kutta-Verfahren + # Implementiert wie beschieben in: https://de.wikipedia.org/wiki/Klassisches_Runge-Kutta-Verfahren def __init__(self, odefun): - self.odefun = odefun # Aufrufbare Funktion einer Differenziagleichung vom Typ dy = odefun(t, y) + # Aufrufbare Funktion einer Differenziagleichung vom Typ dy = odefun(t, y) + self.odefun = odefun self.success = True self.stepwidth = 1.0e-4 self.t = 0.0 @@ -33,16 +36,17 @@ def set_initial_value(self, y0, t0): self.t = t0 def integrate(self, t_end): - self.success = False # uncheck success flag + self.success = False # uncheck success flag # Integration mit fester Schrittweite bis t_end erreicht ist - while self.t < t_end : + while self.t < t_end: self.time_step() # check success flag if np.isnan(self.y).any(): - logging.warning('Encountered NaN during integration at t={}.'.format(self.t)) + logging.warning( + 'Encountered NaN during integration at t={}.'.format(self.t)) else: - self.success = True + self.success = True def time_step(self): # Ausgangswerte bei y(t0) holen @@ -52,18 +56,19 @@ def time_step(self): # Berechnung der Koeffizienten k1 = self.odefun(t0, y0) - k2 = self.odefun(t0 + h/2.0, y0 + h/2.0 * k1) - k3 = self.odefun(t0 + h/2.0, y0 + h/2.0 * k2) - k4 = self.odefun(t0 + h, y0 + h*k3) + k2 = self.odefun(t0 + h / 2.0, y0 + h / 2.0 * k1) + k3 = self.odefun(t0 + h / 2.0, y0 + h / 2.0 * k2) + k4 = self.odefun(t0 + h, y0 + h * k3) # Berechnung der Naeherungsloesung fuer y(t1) t1 = t0 + h - y1 = y0 + h/6.0 * (k1 + 2.0*k2 + 2.0*k3 + k4) + y1 = y0 + h / 6.0 * (k1 + 2.0 * k2 + 2.0 * k3 + k4) # Daten speichern fuer den naechsten Schritt self.t = t1 self.y = y1.copy() + class ExplicitEuler(RungeKutta4): # Explizites Euler Verfahren fuer ein Anfangswertprobelm 1. Ordnung # https://de.wikipedia.org/wiki/Explizites_Euler-Verfahren @@ -76,15 +81,16 @@ def time_step(self): # Berechnung der Koeffizienten k1 = self.odefun(t0, y0) - + # Berechnung der Naeherungsloesung fuer y(t1) t1 = t0 + h - y1 = y0 + h*k1 + y1 = y0 + h * k1 # Daten speichern fuer den naechsten Schritt self.t = t1 self.y = y1.copy() + class AdamsBashforth(RungeKutta4): # Explizites Adams-Bashforth Verfahren fuer ein Anfangswertprobelm 1. Ordnung # https://en.wikipedia.org/wiki/Linear_multistep_method @@ -98,36 +104,37 @@ def time_step(self): # Berechnung der Koeffizienten out = self.odefun(t0, y0) # Handhabung des Outputs, falls es sich um ein Dictionary handelt - if type(out) is dict: + if isinstance(out, dict): self.output_dict = out self.dy.append(out['dy']) else: self.dy.append(out) - + # Berechnung der Naeherungsloesung fuer y(t1) # Je nach dem wie viele zurückliegende Schritte verfügbar sind, wird die Ordnung des Verfahrens erhöht. if self.i == 1: # Dies ist die Euler Methode t1 = t0 + h - y1 = y0 + h*self.dy[-1] + y1 = y0 + h * self.dy[-1] elif self.i == 2: t1 = t0 + h - y1 = y0 + h*(3.0/2.0*self.dy[-1] - 1.0/2.0*self.dy[-2]) + y1 = y0 + h * (3.0 / 2.0 * self.dy[-1] - 1.0 / 2.0 * self.dy[-2]) elif self.i == 3: t1 = t0 + h - y1 = y0 + h*(23.0/12.0*self.dy[-1] - 16.0/12.0*self.dy[-2] + 5.0/12.0*self.dy[-3]) + y1 = y0 + h * (23.0 / 12.0 * self.dy[-1] - 16.0 / 12.0 * self.dy[-2] + 5.0 / 12.0 * self.dy[-3]) elif self.i == 4: t1 = t0 + h - y1 = y0 + h*(55.0/24.0*self.dy[-1] - 59.0/24.0*self.dy[-2] + 37.0/24.0*self.dy[-3] - 9.0/24.0*self.dy[-4]) + y1 = y0 + h * (55.0 / 24.0 * self.dy[-1] - 59.0 / 24.0 * self.dy[-2] + 37.0 / 24.0 * self.dy[-3] + - 9.0 / 24.0 * self.dy[-4]) elif self.i >= 5: t1 = t0 + h - y1 = y0 + h*(1901.0/720.0*self.dy[-1] - 2774.0/720.0*self.dy[-2] + 2616.0/720.0*self.dy[-3] - 1274.0/720.0*self.dy[-4] + 251.0/720.0*self.dy[-5]) + y1 = y0 + h * (1901.0 / 720.0 * self.dy[-1] - 2774.0 / 720.0 * self.dy[-2] + 2616.0 / 720.0 * self.dy[-3] + - 1274.0 / 720.0 * self.dy[-4] + 251.0 / 720.0 * self.dy[-5]) # Daten speichern fuer den naechsten Schritt self.t = t1 self.y = y1.copy() - # Überflüssige zurückliegende Schritte löschen + # Überflüssige zurückliegende Schritte löschen self.dy = self.dy[-4:] # Zeitschrittzähler erhöhen self.i += 1 - \ No newline at end of file diff --git a/loadskernel/interpolate.py b/loadskernel/interpolate.py index a2b8929..5ac8c38 100644 --- a/loadskernel/interpolate.py +++ b/loadskernel/interpolate.py @@ -1,40 +1,41 @@ import numpy as np + class MatrixInterpolation: """ - This is a simple class for linear matrix interpolation. - The intention is to create an interpolation method for AIC matrices, which is similar to but faster than - interp1d from scipy. - A test cases using unsteady AIC matrices of the Allegra configuration resulted in numerically equal results - and a 5-10x faster computation time compared to scipy. Extrapolation is supported. - + This is a simple class for linear matrix interpolation. + The intention is to create an interpolation method for AIC matrices, which is similar to but faster than + interp1d from scipy. + A test cases using unsteady AIC matrices of the Allegra configuration resulted in numerically equal results + and a 5-10x faster computation time compared to scipy. Extrapolation is supported. + Inputs x: 1-D array of n_samples data: 3-D array of matrices to interpolate, Notation [n_samples, n_rows, n_columns] - + Output 2-D array of interpolated matrix values """ - + def __init__(self, x, data): self.x = np.array(x) self.n_samples = len(self.x) self.data = np.array(data) self.gradients = np.zeros_like(self.data) - + self.calc_gradients() - + def __call__(self, i): return self.interpolate(i) - + def calc_gradients(self): # calculate the gradients between all samples - for m in range(self.n_samples-1): - self.gradients[m,:,:] = (self.data[m+1,:,:] - self.data[m,:,:]) / (self.x[m+1] - self.x[m]) + for m in range(self.n_samples - 1): + self.gradients[m, :, :] = (self.data[m + 1, :, :] - self.data[m, :, :]) / (self.x[m + 1] - self.x[m]) # repeat the last set of gradients to make sure there are right-sided gradients available in case of extrapolation - self.gradients[-1,:,:] = self.gradients[-2,:,:] - - def interpolate(self, i): + self.gradients[-1, :, :] = self.gradients[-2, :, :] + + def interpolate(self, i): # find the nearest neighbor pos = np.abs(self.x - i).argmin() # calculate the delta @@ -45,5 +46,4 @@ def interpolate(self, i): else: pos_gradients = pos # perform linear interpolation and return result - return self.data[pos,:,:] + self.gradients[pos_gradients,:,:]*delta - \ No newline at end of file + return self.data[pos, :, :] + self.gradients[pos_gradients, :, :] * delta diff --git a/loadskernel/io_functions/bdf_cards.py b/loadskernel/io_functions/bdf_cards.py index 1d5eee9..132dab8 100644 --- a/loadskernel/io_functions/bdf_cards.py +++ b/loadskernel/io_functions/bdf_cards.py @@ -1,8 +1,17 @@ import logging + from loadskernel.io_functions.read_mona import nastran_number_converter -class SimpleCard(object): - + +class SimpleCard(): + # init empty attributes + expected_lines = 1 + field_names = [] + field_positions = [] + field_types = [] + optional_fields = [] + optional_defaults = [] + @classmethod def parse(cls, card_as_string, width): card = cls.parse_known_fields(cls, card_as_string, width) @@ -12,56 +21,58 @@ def parse_known_fields(self, card_as_string, width): # create an empty dictionary to store the card card = {} # loop over all fields - for name, pos, type in zip(self.field_names, self.field_positions, self.field_types): + for name, pos, field_type in zip(self.field_names, self.field_positions, self.field_types): # look for a default value if name in self.optional_fields: default_value = self.optional_defaults[self.optional_fields.index(name)] else: default_value = None # get the field location in the string - start = pos*width - stop = (pos+1)*width + start = pos * width + stop = (pos + 1) * width """ This is the decision logic to parse fields: Case 1: See if the card is suffiently long --> the field exists - Note that the stop index also works in case the card is too short, it simply take what's there --> this might be the last field + Note that the stop index also works in case the card is too short, it simply take what's there --> this might be + the last field Case 2: The field doesn't exists --> look if the field is optional and take the default Case 3: The field is missing --> issue a warning """ if len(card_as_string) > start: my_field = card_as_string[start:stop] # convert field and store in dictionary - card[name] = nastran_number_converter(my_field, type, default_value) + card[name] = nastran_number_converter(my_field, field_type, default_value) elif name in self.optional_fields: # store default in dictionary card[name] = default_value else: - logging.error('Field {} expected but missing in {} card: {}'.format(name, self.__name__, card_as_string)) + logging.error('Field {} expected but missing in {} card: {}'.format(name, type(self).__name__, card_as_string)) return card + class ListCard(SimpleCard): - + @classmethod def parse(cls, card_as_string, width): """ - For the first fields, re-use the procedure from SimpleCard. + For the first fields, re-use the procedure from SimpleCard. Then, parse further occurences for the last field, which yield the list items. """ card = cls.parse_known_fields(cls, card_as_string, width) card = cls.parse_list_items(cls, card, card_as_string, width) return card - + def parse_list_items(self, card, card_as_string, width): # get the properties of the last field name = self.field_names[-1] - pos = self.field_positions[-1] - type = self.field_types[-1] + pos = self.field_positions[-1] + field_type = self.field_types[-1] # look for a default value if name in self.optional_fields: default_value = self.optional_defaults[self.optional_fields.index(name)] else: default_value = None - # turn the last field into a list + # turn the last field into a list card[name] = [card[name]] """ This is the loop to find more occurences for the last field: @@ -71,140 +82,154 @@ def parse_list_items(self, card, card_as_string, width): i = 1 while True: # get the field location in the string - start = (pos+i)*width - stop = (pos+i+1)*width - + start = (pos + i) * width + stop = (pos + i + 1) * width + if len(card_as_string) > start: my_field = card_as_string[start:stop] # convert field and store in dictionary - card[name].append(nastran_number_converter(my_field, type, default_value)) + card[name].append(nastran_number_converter(my_field, field_type, default_value)) i += 1 else: break return card - + + class GRID(SimpleCard): expected_lines = 1 # field of interest (any other fields are not implemented) - field_names = ['ID', 'CP', 'X1', 'X2', 'X3', 'CD' ] - field_positions = [ 0, 1, 2, 3, 4, 5 ] - field_types = ['int','int','float','float','float','int'] - optional_fields = ['CP', 'CD'] - optional_defaults = [ 0, 0 ] + field_names = ['ID', 'CP', 'X1', 'X2', 'X3', 'CD'] + field_positions = [0, 1, 2, 3, 4, 5] + field_types = ['int', 'int', 'float', 'float', 'float', 'int'] + optional_fields = ['CP', 'CD'] + optional_defaults = [0, 0] + class CQUAD4(SimpleCard): expected_lines = 1 # field of interest (any other fields are not implemented) - field_names = ['ID', 'G1', 'G2', 'G3', 'G4' ] - field_positions = [ 0, 2, 3, 4, 5 ] - field_types = ['int','int','int','int','int'] - optional_fields = [] - optional_defaults = [] + field_names = ['ID', 'G1', 'G2', 'G3', 'G4'] + field_positions = [0, 2, 3, 4, 5] + field_types = ['int', 'int', 'int', 'int', 'int'] + optional_fields = [] + optional_defaults = [] + class CTRIA3(SimpleCard): expected_lines = 1 # field of interest (any other fields are not implemented) - field_names = ['ID', 'G1', 'G2', 'G3' ] - field_positions = [ 0, 2, 3, 4 ] - field_types = ['int','int','int','int'] - optional_fields = [] - optional_defaults = [] + field_names = ['ID', 'G1', 'G2', 'G3'] + field_positions = [0, 2, 3, 4] + field_types = ['int', 'int', 'int', 'int'] + optional_fields = [] + optional_defaults = [] + class CORD2R(SimpleCard): expected_lines = 2 # field of interest (any other fields are not implemented) - field_names = ['ID', 'RID', 'A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3' ] - field_positions = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] - field_types = ['int','int','float','float','float','float','float','float','float','float','float'] - optional_fields = ['RID'] - optional_defaults = [ 0 ] + field_names = ['ID', 'RID', 'A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'] + field_positions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + field_types = ['int', 'int', 'float', 'float', 'float', 'float', 'float', 'float', 'float', 'float', 'float'] + optional_fields = ['RID'] + optional_defaults = [0] + class CORD1R(SimpleCard): expected_lines = 1 # field of interest (any other fields are not implemented) - field_names = ['ID', 'A', 'B', 'C'] - field_positions = [ 0, 1, 2, 3 ] - field_types = ['int','int','int','int'] - optional_fields = [] - optional_defaults = [] + field_names = ['ID', 'A', 'B', 'C'] + field_positions = [0, 1, 2, 3] + field_types = ['int', 'int', 'int', 'int'] + optional_fields = [] + optional_defaults = [] + class MONPNT1(SimpleCard): expected_lines = 2 # field of interest (any other fields are not implemented) - field_names = ['NAME', 'COMP', 'CP', 'X', 'Y', 'Z', 'CD'] - field_positions = [ 0, 9, 10, 11, 12, 13, 14 ] - field_types = [ 'str', 'str','int','float','float','float','int'] - optional_fields = ['CP', 'CD'] - optional_defaults = [ 0, 0 ] + field_names = ['NAME', 'COMP', 'CP', 'X', 'Y', 'Z', 'CD'] + field_positions = [0, 9, 10, 11, 12, 13, 14] + field_types = ['str', 'str', 'int', 'float', 'float', 'float', 'int'] + optional_fields = ['CP', 'CD'] + optional_defaults = [0, 0] + class AECOMP(ListCard): expected_lines = None # field of interest (any other fields are not implemented) - field_names = ['NAME', 'LISTTYPE', 'LISTID'] - field_positions = [ 0, 1, 2 ] - field_types = [ 'str', 'str', 'int'] - optional_fields = ['LISTID'] - optional_defaults = [ None ] + field_names = ['NAME', 'LISTTYPE', 'LISTID'] + field_positions = [0, 1, 2] + field_types = ['str', 'str', 'int'] + optional_fields = ['LISTID'] + optional_defaults = [None] + class SET1(ListCard): expected_lines = None # field of interest (any other fields are not implemented) - field_names = ['ID', 'values' ] - field_positions = [ 0, 1 ] - # Due to the mixture of integers and strings ('THRU') in a SET1 card, all list items are parsed as strings. - field_types = ['int', 'str' ] + field_names = ['ID', 'values'] + field_positions = [0, 1] + # Due to the mixture of integers and strings ('THRU') in a SET1 card, all list items are parsed as strings. + field_types = ['int', 'str'] # Blank strings (e.g. trailing spaces) shall be replaced with None. - optional_fields = ['values'] - optional_defaults = [ None ] + optional_fields = ['values'] + optional_defaults = [None] + class AELIST(SET1): # The AELIST is identical to SET1. pass + class AEFACT(ListCard): expected_lines = None # field of interest (any other fields are not implemented) - field_names = ['ID', 'values' ] - field_positions = [ 0, 1 ] - field_types = ['int', 'float' ] - optional_fields = ['values'] - optional_defaults = [ None ] + field_names = ['ID', 'values'] + field_positions = [0, 1] + field_types = ['int', 'float'] + optional_fields = ['values'] + optional_defaults = [None] + class CAERO1(SimpleCard): expected_lines = 2 # field of interest (any other fields are not implemented) - field_names = ['ID', 'CP', 'NSPAN', 'NCHORD', 'LSPAN', 'LCHORD', 'X1', 'Y1', 'Z1', 'X12', 'X4', 'Y4', 'Z4', 'X43'] - field_positions = [ 0, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15 ] - field_types = ['int','int', 'int', 'int', 'int', 'int','float','float','float','float','float','float','float','float'] - optional_fields = ['CP', 'NSPAN', 'NCHORD', 'LSPAN', 'LCHORD'] - optional_defaults = [ 0, 0, 0, None, None ] + field_names = ['ID', 'CP', 'NSPAN', 'NCHORD', 'LSPAN', 'LCHORD', 'X1', 'Y1', 'Z1', 'X12', 'X4', 'Y4', 'Z4', 'X43'] + field_positions = [0, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15] + field_types = ['int', 'int', 'int', 'int', 'int', 'int', 'float', 'float', 'float', 'float', 'float', 'float', 'float', + 'float'] + optional_fields = ['CP', 'NSPAN', 'NCHORD', 'LSPAN', 'LCHORD'] + optional_defaults = [0, 0, 0, None, None] + class CAERO7(SimpleCard): expected_lines = 3 # field of interest (any other fields are not implemented) - field_names = ['ID', 'CP', 'NSPAN', 'NCHORD', 'X1', 'Y1', 'Z1', 'X12', 'X4', 'Y4', 'Z4', 'X43'] - field_positions = [ 0, 2, 3, 4, 8, 9, 10, 11, 16, 17, 18, 19 ] - field_types = ['int','int', 'int', 'int','float','float','float','float','float','float','float','float'] - optional_fields = ['CP', 'NSPAN', 'NCHORD'] - optional_defaults = [ 0, 0, 0 ] + field_names = ['ID', 'CP', 'NSPAN', 'NCHORD', 'X1', 'Y1', 'Z1', 'X12', 'X4', 'Y4', 'Z4', 'X43'] + field_positions = [0, 2, 3, 4, 8, 9, 10, 11, 16, 17, 18, 19] + field_types = ['int', 'int', 'int', 'int', 'float', 'float', 'float', 'float', 'float', 'float', 'float', 'float'] + optional_fields = ['CP', 'NSPAN', 'NCHORD'] + optional_defaults = [0, 0, 0] + class AESURF(SimpleCard): expected_lines = 1 # field of interest (any other fields are not implemented) - field_names = ['ID', 'LABEL', 'CID', 'AELIST', 'EFF' ] - field_positions = [ 0, 1, 2, 3, 7 ] - field_types = ['int' ,'str', 'int', 'int','float'] - optional_fields = ['EFF'] - optional_defaults = [ 1.0 ] + field_names = ['ID', 'LABEL', 'CID', 'AELIST', 'EFF'] + field_positions = [0, 1, 2, 3, 7] + field_types = ['int', 'str', 'int', 'int', 'float'] + optional_fields = ['EFF'] + optional_defaults = [1.0] + class ASET1(ListCard): expected_lines = None # field of interest (any other fields are not implemented) - field_names = [ 'ID', 'values' ] - field_positions = [ 0, 1 ] - # Due to the mixture of integers and strings ('THRU') in a SET1 card, all list items are parsed as strings. - field_types = ['int', 'str' ] + field_names = ['ID', 'values'] + field_positions = [0, 1] + # Due to the mixture of integers and strings ('THRU') in a SET1 card, all list items are parsed as strings. + field_types = ['int', 'str'] # Blank strings (e.g. trailing spaces) shall be replaced with None. - optional_fields = ['ID', 'values'] - optional_defaults = [ 123456, None ] - + optional_fields = ['ID', 'values'] + optional_defaults = [123456, None] diff --git a/loadskernel/io_functions/cpacs_functions.py b/loadskernel/io_functions/cpacs_functions.py deleted file mode 100644 index 0ae6598..0000000 --- a/loadskernel/io_functions/cpacs_functions.py +++ /dev/null @@ -1,88 +0,0 @@ -''' -Created on Apr 9, 2019 - -@author: voss_ar -''' - -class CpacsFunctions: - def __init__(self, tixi): - self.tixi = tixi - - def write_cpacs_loadsvector(self, parent, grid, Pg, ): - self.add_elem(parent, 'fx', Pg[grid['set'][:,0]], 'vector') - self.add_elem(parent, 'fy', Pg[grid['set'][:,1]], 'vector') - self.add_elem(parent, 'fz', Pg[grid['set'][:,2]], 'vector') - self.add_elem(parent, 'mx', Pg[grid['set'][:,3]], 'vector') - self.add_elem(parent, 'my', Pg[grid['set'][:,4]], 'vector') - self.add_elem(parent, 'mz', Pg[grid['set'][:,5]], 'vector') - - def write_cpacs_grid(self, parent, grid): - self.add_elem(parent, 'uID', grid['ID'], 'vector_int') - self.add_elem(parent, 'x', grid['offset'][:,0], 'vector') - self.add_elem(parent, 'y', grid['offset'][:,1], 'vector') - self.add_elem(parent, 'z', grid['offset'][:,2], 'vector') - - def write_cpacs_grid_orientation(self, parent, grid, coord): - dircos = [coord['dircos'][coord['ID'].index(x)] for x in grid['CD']] - #self.add_elem(parent, 'dircos', dircos, 'vector') - # Wie schreibt man MAtrizen in CPACS ??? - - def create_path(self, parent,path): - # adopted from cps2mn - # Create all elements in CPACS defined by a path string with '/' between elements. - # - # INPUTS - # parent: [string] parent node in CPACS for the elements to be created - # path: [string] path of children elements to be created - # - # Institute of Aeroelasticity - # German Aerospace Center (DLR) - - #tixi, tigl, modelUID = getTixiTiglModelUID() - #Split the path at the '/' creating all the new elements names - tmp = path.split('/') - - #Temporary path containing the name of the parent node - tmp_path = parent - - #Loop over all elements found at 'path' - for i in range(len(tmp)): - - #Create a new element under the current parent node - self.tixi.createElement(tmp_path,tmp[i]) - - #Expands the parent node to include the current element - tmp_path = tmp_path + '/' + tmp[i] - - def add_elem(self, path, elem, data, data_type): - # adopted from cps2mn - # Add element data to cpacs. Can be double, integer, text, vector - # - # INPUTS - # path: [string] path of the parent element in CPACS - # elem: [string] name of the element to be created in CPACS - # data_type [double/integer/text/vector] type of the element to be created - # - # Institute of Aeroelasticity - # German Aerospace Center (DLR) - - #tixi, tigl, modelUID = getTixiTiglModelUID() - #---------------------------------------------------------------------# - #Add data with TIXI - if data_type == 'double': - self.tixi.addTextElement(path, elem, str(data)) - - #if data_type == 'integer': - #error = TIXI.tixiGetIntegerElement( tixiHandle, path, byref(data)) - if data_type == 'text': - self.tixi.addTextElement(path, elem, data) - - #Add float vector - if data_type == 'vector': - format='%f' - self.tixi.addFloatVector(path, elem, data, len(data),format) - - #Add integer vector - if data_type == 'vector_int': - format='%0.0f' - self.tixi.addFloatVector(path, elem, data, len(data),format) \ No newline at end of file diff --git a/loadskernel/io_functions/data_handling.py b/loadskernel/io_functions/data_handling.py index 0e3edb2..7be1a66 100644 --- a/loadskernel/io_functions/data_handling.py +++ b/loadskernel/io_functions/data_handling.py @@ -1,22 +1,32 @@ -import pickle, h5py -import importlib, sys, os, logging, re, csv -import numpy as np +import csv +import h5py +import importlib +import logging +import os +import pickle +import re +import sys + import scipy - +import numpy as np + + def write_list_of_dictionaries(dictionary, filename_csv): with open(filename_csv, 'w') as fid: - if dictionary.__len__() > 0: + if len(dictionary) > 0: w = csv.DictWriter(fid, dictionary[0].keys()) w.writeheader() - w = csv.DictWriter(fid, dictionary[0].keys(), quotechar="'", quoting=csv.QUOTE_NONNUMERIC ) + w = csv.DictWriter(fid, dictionary[0].keys(), quotechar="'", quoting=csv.QUOTE_NONNUMERIC) w.writerows(dictionary) else: fid.write('None') return + def load_pickle(file_object): return pickle.load(file_object) - + + def dump_pickle(data, file_object): if sys.version_info[0] < 3: # This is the compatibility mode for Python 2 @@ -25,60 +35,63 @@ def dump_pickle(data, file_object): # In Python 3, we can specify a more advanced protocol pickle.dump(data, file_object, protocol=4) + def open_hdf5(filename): return h5py.File(filename, 'w') + def write_hdf5(fid, dic, path=''): recursively_save_dict_to_hdf5(fid, dic, path) - return + def close_hdf5(fid): fid.close() - return + def load_hdf5(filename): return h5py.File(filename, 'r') + def dump_hdf5(filename, dic): fid = h5py.File(filename, 'w') recursively_save_dict_to_hdf5(fid, dic, path='') fid.close() - return + def recursively_save_dict_to_hdf5(fid, dic, path=''): for key, item in dic.items(): # make sure that all key are strings (might be integers, for example) key = str(key) if isinstance(item, dict): - recursively_save_dict_to_hdf5(fid, item, path=path+'/'+key) + recursively_save_dict_to_hdf5(fid, item, path=path + '/' + key) elif isinstance(item, (np.ndarray, int, np.number, float)): - fid.create_dataset(path+'/'+key, data=item) + fid.create_dataset(path + '/' + key, data=item) elif isinstance(item, (scipy.sparse.csc_matrix)): - g = fid.create_group(path+'/'+key) - g.create_dataset('data',data=item.data) - g.create_dataset('indptr',data=item.indptr) - g.create_dataset('indices',data=item.indices) + g = fid.create_group(path + '/' + key) + g.create_dataset('data', data=item.data) + g.create_dataset('indptr', data=item.indptr) + g.create_dataset('indices', data=item.indices) g.attrs['shape'] = item.shape g.attrs['is_sparse'] = True - + elif isinstance(item, (str, list)): # In the latest version, h5py handles string and lists of strings correctly. No need for funny conversions :) - fid.create_dataset(path+'/'+key, data=item) + fid.create_dataset(path + '/' + key, data=item) # If the item is a string or if there are strings in a list, then add a label. if isinstance(item, str) or any([isinstance(x, (str)) for x in item]): - fid[path+'/'+key].attrs['is_string'] = True + fid[path + '/' + key].attrs['is_string'] = True else: - raise ValueError('Saving of data type %s not implemented!'%type(item)) + raise ValueError('Saving of data type %s not implemented!', type(item)) - return def load_hdf5_responses(job_name, path_output): - logging.info( '--> Opening response(s).' ) + logging.info('--> Opening response(s).') filename = path_output + 'response_' + job_name + '.hdf5' - fid = load_hdf5(filename) + fid = load_hdf5(filename) response = [fid[key] for key in sorted(fid.keys(), key=int) if fid[key]['successful'][()]] - return response + return response + def load_hdf5_dict(hdf5_object): """ @@ -87,7 +100,9 @@ def load_hdf5_dict(hdf5_object): """ new_dict = {} for key in hdf5_object.keys(): - if isinstance(hdf5_object[key], h5py.Group) and 'is_sparse' in hdf5_object[key].attrs and hdf5_object[key].attrs['is_sparse']: + if isinstance(hdf5_object[key], h5py.Group) and \ + 'is_sparse' in hdf5_object[key].attrs and \ + hdf5_object[key].attrs['is_sparse']: new_dict[key] = load_hdf5_sparse_matrix(hdf5_object[key]) elif 'is_string' in hdf5_object[key].attrs and hdf5_object[key].attrs['is_string']: new_dict[key] = hdf5_object[key].asstr()[()] @@ -95,55 +110,54 @@ def load_hdf5_dict(hdf5_object): new_dict[key] = hdf5_object[key][()] else: new_dict[key] = hdf5_object[key] - + return new_dict + def load_hdf5_sparse_matrix(hdf5_group): """ - This is a convenience function which assembles the sparse matrix. + This is a convenience function which assembles the sparse matrix. Assumption: The matrix is in CSC sparse format. In case the spares-flag is False, try to read it as a normal matrix. """ if 'is_sparse' in hdf5_group.attrs and hdf5_group.attrs['is_sparse']: - M = scipy.sparse.csc_matrix((hdf5_group['data'][()],hdf5_group['indices'][()], + M = scipy.sparse.csc_matrix((hdf5_group['data'][()], hdf5_group['indices'][()], hdf5_group['indptr'][()]), hdf5_group.attrs['shape']) else: M = hdf5_group[()] return M + def load_jcl(job_name, path_input, jcl): - if jcl == None: - logging.info( '--> Reading parameters from JCL.') + if jcl is None: + logging.info('--> Reading parameters from JCL.') # import jcl dynamically by filename - if sys.version_info[0] < 3: - # This is the old way used in Python 2 - import imp - jcl_modul = imp.load_source('jcl', path_input + job_name + '.py') - else: - # this is the newer way used in Python 3 - spec = importlib.util.spec_from_file_location('jcl', os.path.join(path_input, job_name+'.py' )) - jcl_modul = spec.loader.load_module() - jcl = jcl_modul.jcl() + # this is the newer way used in Python 3 + spec = importlib.util.spec_from_file_location('jcl', os.path.join(path_input, job_name + '.py')) + jcl_modul = spec.loader.load_module() + jcl = jcl_modul.jcl() # small check for completeness attributes = ['general', 'efcs', 'geom', 'aero', 'spline', 'mass', 'atmo', 'trimcase', 'simcase'] for attribute in attributes: if not hasattr(jcl, attribute): - logging.critical( 'JCL appears to be incomplete: jcl.{} missing. Exit.'.format(attribute)) + logging.critical('JCL appears to be incomplete: jcl.{} missing. Exit.'.format(attribute)) sys.exit() return jcl + def gather_responses(job_name, path): - logging.info( '--> Gathering response(s).' ) + logging.info('--> Gathering response(s).') filenames = os.listdir(path) filenames.sort() response = [] for filename in filenames: - if re.match('response_{}_subcase_[\d]*.pickle'.format(job_name), filename) is not None: + if re.match(r'response_{}_subcase_[\d]*.pickle'.format(job_name), filename) is not None: logging.debug('loading {}'.format(filename)) with open(os.path.join(path, filename), 'rb') as f: response.append(load_pickle(f)) return response + def check_path(path): if not os.path.exists(path): # Create the directory. Use 'exist_ok=True' in case another process just created the same directory. @@ -151,6 +165,5 @@ def check_path(path): if os.path.isdir(path) and os.access(os.path.dirname(path), os.W_OK): # Make sure the path ends with '/' return os.path.join(path, '') - else: - logging.critical( 'Path ' + str(path) + ' not valid. Exit.') - sys.exit() + logging.critical('Path ' + str(path) + ' not valid. Exit.') + sys.exit() diff --git a/loadskernel/io_functions/matlab_functions.py b/loadskernel/io_functions/matlab_functions.py deleted file mode 100644 index e34be1e..0000000 --- a/loadskernel/io_functions/matlab_functions.py +++ /dev/null @@ -1,14 +0,0 @@ - -import scipy -import scipy.io -import numpy as np - -def save_responses(file_object, responses): - # Lists can not be handles by savemat, thus convert list to stack of objects. - object_stack = np.empty((len(responses),), dtype=np.object) - for i in range(len(responses)): - object_stack[i] = responses[i] - save_mat(file_object, {"responses":object_stack}) - -def save_mat(file_object, data): - scipy.io.savemat(file_object, data) \ No newline at end of file diff --git a/loadskernel/io_functions/read_b2000.py b/loadskernel/io_functions/read_b2000.py index dea8714..944ebb9 100644 --- a/loadskernel/io_functions/read_b2000.py +++ b/loadskernel/io_functions/read_b2000.py @@ -1,10 +1,11 @@ import numpy as np import scipy.sparse as sp + def read_csv(filename, sparse_output=False): - # use numpy to load comma separated data + # use numpy to load comma separated data data = np.loadtxt(filename, comments='#', delimiter=',') # optionally, convert to sparse if sparse_output: data = sp.csc_matrix(data) - return data \ No newline at end of file + return data diff --git a/loadskernel/io_functions/read_bdf.py b/loadskernel/io_functions/read_bdf.py index 7c957b9..1c087eb 100644 --- a/loadskernel/io_functions/read_bdf.py +++ b/loadskernel/io_functions/read_bdf.py @@ -1,32 +1,36 @@ -import copy, os, logging -import pandas as pd - -from loadskernel.io_functions.bdf_cards import * - """ This is a simple and light-weight BDF reader which consist of only two scripts (read_bdf.py & bdf_cards.py), and parses Nastran BDF files to Pandas data frames. It considers only those cards and fields actually needed -for a loads analysis using Loads Kernel, providing maximum compatibility and speed and comes without further +for a loads analysis using Loads Kernel, providing maximum compatibility and speed and comes without further dependencies. Some ideas and concepts are inspired by pyBDF, a comprehensive DLR in-house BDF reader by Markus Zimmer. """ -class Reader(object): +import copy +import logging +import os + +import pandas as pd + +from loadskernel.io_functions import bdf_cards + + +class Reader(): # This is the list (and mapping) of all implemented bdf cards. - card_interpreters = {'GRID' : GRID, - 'CQUAD4' : CQUAD4, - 'CTRIA3' : CTRIA3, - 'CORD2R' : CORD2R, - 'CORD1R' : CORD1R, - 'MONPNT1': MONPNT1, - 'AECOMP' : AECOMP, - 'SET1' : SET1, - 'AEFACT' : AEFACT, - 'CAERO1' : CAERO1, - 'CAERO7' : CAERO7, - 'AESURF' : AESURF, - 'AELIST' : AELIST, - 'ASET1' : ASET1, + card_interpreters = {'GRID': bdf_cards.GRID, + 'CQUAD4': bdf_cards.CQUAD4, + 'CTRIA3': bdf_cards.CTRIA3, + 'CORD2R': bdf_cards.CORD2R, + 'CORD1R': bdf_cards.CORD1R, + 'MONPNT1': bdf_cards.MONPNT1, + 'AECOMP': bdf_cards.AECOMP, + 'SET1': bdf_cards.SET1, + 'AEFACT': bdf_cards.AEFACT, + 'CAERO1': bdf_cards.CAERO1, + 'CAERO7': bdf_cards.CAERO7, + 'AESURF': bdf_cards.AESURF, + 'AELIST': bdf_cards.AELIST, + 'ASET1': bdf_cards.ASET1, } def __init__(self): @@ -38,8 +42,8 @@ def __init__(self): self.includes = [] # This is a list of all known / implemented cards self.known_cards = self.card_interpreters.keys() - # The cards are stored in a Pandas data frames, one frame per type of card. - # The data frames themselfes are stored in a dictionary. + # The cards are stored in a Pandas data frames, one frame per type of card. + # The data frames themselfes are stored in a dictionary. self.cards = {} for card_name in self.known_cards: # Make sure the colums of the data frame have the correct type (int or float) @@ -49,9 +53,9 @@ def __init__(self): df_definition = {} for c, t in zip(card_class.field_names, card_class.field_types): df_definition[c] = pd.Series(dtype=t) - # Create the data frame + # Create the data frame self.cards[card_name] = pd.DataFrame(df_definition) - + def process_deck(self, deck): # Make sure deck is a list of filenames, not a single string if isinstance(deck, str): @@ -63,22 +67,22 @@ def process_deck(self, deck): Step 1: In case include statements are found, move them to filenames. Step 2: Re-run process_deck() Step 3: This loop terminates when the include list is empty, i.e. no more includes are found. - """ + """ self.read_lines_from_files() self.read_cards_from_lines() - + if self.includes: logging.info('Found include(s):') self.filenames = copy.deepcopy(self.includes) self.includes = [] - self.process_deck() - + self.process_deck(self.filenames) + self.aggregate_cards(['ASET1']) self.remove_duplicate_cards() return - + def read_lines_from_files(self): - # reset the line storage before reading new files + # reset the line storage before reading new files self.lines = [] # loop over all filenames and read all lines for filename in self.filenames: @@ -93,7 +97,7 @@ def read_lines_from_files(self): self.processed_files += [filename] else: logging.warning('File NOT found: {}'.format(filename)) - + def read_cards_from_lines(self): if self.lines: logging.info('Read BDF cards from {} lines...'.format(len(self.lines))) @@ -104,15 +108,15 @@ def read_cards_from_lines(self): if card_name in self.known_cards: # get the corresponding interpeter card_class = self.card_interpreters[card_name] - # convert lines to string + # convert lines to string lines_as_string, width = self.convert_lines_to_string(card_class.expected_lines) # parse that string using the proper interpreter card = card_class.parse(lines_as_string, width) # store the card self.store_card(card_name, card) - else: + else: self.lines.pop(0) - + def convert_lines_to_string(self, expected_lines): width = self.get_width_of_fields(self.lines) if expected_lines is not None: @@ -126,8 +130,8 @@ def convert_lines_to_string(self, expected_lines): continuation character is given). """ for i, line in enumerate(self.lines): - if len(self.lines) == i+1 or line[9*width:].strip() != self.lines[i+1][:width].strip(): - n_lines = i+1 + if len(self.lines) == i + 1 or line[9 * width:].strip() != self.lines[i + 1][:width].strip(): + n_lines = i + 1 break # get the line to work with my_lines = self.lines[:n_lines] @@ -138,24 +142,24 @@ def convert_lines_to_string(self, expected_lines): # - remove trailing / continuation characters # - remove first field, this is either the card name (which we no longer need) or the continuation character # - expand a line which is missing spaces at the end, which is important for indexing the fields - # - handle that the last line or a one-line-card might have less than 9 fields + # - handle that the last line or a one-line-card might have less than 9 fields if n_lines > 1: tmp = [] for line in my_lines[:-1]: - n_missing_spaces = (8*width - len(line[width:9*width])) - tmp.append(line[width:9*width]+' '*n_missing_spaces) + n_missing_spaces = 8 * width - len(line[width:9 * width]) + tmp.append(line[width:9 * width] + ' ' * n_missing_spaces) tmp.append(my_lines[-1][width:]) my_lines = tmp else: my_lines = [my_lines[-1][width:]] - + # Join lines to one string lines_as_string = ''.join(my_lines) # Removing consumed lines from list, pop only accepts one index for _ in range(n_lines): self.lines.pop(0) return lines_as_string, width - + def get_width_of_fields(self, lines): # Establish the width of the fields in the Nastran card, which can be 8 or 16 characters. # This is indicated by a '*' at the beginning of a the card. @@ -164,12 +168,12 @@ def get_width_of_fields(self, lines): else: width = 8 return width - + def store_card(self, card_name, card): - # ToDo: It would be nice if the dtype would be conserved. + # It would be nice if the dtype would be conserved, not sure how to do that... new_row = pd.Series(card) self.cards[card_name] = pd.concat([self.cards[card_name], new_row.to_frame().T], ignore_index=True) - + def remove_duplicate_cards(self): # This function looks for duplicates in all data frames. # Duplicates are identified by the first field (typically ID or NAME). @@ -179,8 +183,8 @@ def remove_duplicate_cards(self): self.cards[card_name].drop_duplicates(sort_by_field, inplace=True) new_size = self.cards[card_name].shape[0] if old_size != new_size: - logging.info('Dropping {} duplicate {}s'.format(old_size-new_size, card_name)) - + logging.info('Dropping {} duplicate {}s'.format(old_size - new_size, card_name)) + def aggregate_cards(self, card_names): # This function aggregates selected cards by the first field (typically ID or NAME). for card_name in card_names: @@ -189,10 +193,4 @@ def aggregate_cards(self, card_names): self.cards[card_name] = self.cards[card_name].groupby(by=sort_by_field, as_index=False).agg(sum) new_size = self.cards[card_name].shape[0] if old_size != new_size: - logging.info('Aggregating {} {}s'.format(old_size-new_size, card_name)) - - -# bdf_reader = Reader() -# bdf_reader.process_deck('./allegra-s_c05.bdf') -# bdf_reader.process_deck('./test.bdf') -# print('Done.') + logging.info('Aggregating {} {}s'.format(old_size - new_size, card_name)) diff --git a/loadskernel/io_functions/read_cfdgrids.py b/loadskernel/io_functions/read_cfdgrids.py index 57eea01..8863e42 100644 --- a/loadskernel/io_functions/read_cfdgrids.py +++ b/loadskernel/io_functions/read_cfdgrids.py @@ -1,9 +1,13 @@ -import scipy.io.netcdf as netcdf +import h5py +import logging + import numpy as np -import logging, h5py +import scipy.io.netcdf as netcdf + class ReadCfdgrids: - def __init__(self, jcl): + + def __init__(self, jcl): self.jcl = jcl if 'surface' in jcl.meshdefo: self.filename_grid = self.jcl.meshdefo['surface']['filename_grid'] @@ -12,20 +16,20 @@ def __init__(self, jcl): logging.error('jcl.meshdefo has no key "surface"') def read_surface(self, merge_domains=False): - if 'fileformat' in self.jcl.meshdefo['surface'] and self.jcl.meshdefo['surface']['fileformat'].lower()=='cgns': + if 'fileformat' in self.jcl.meshdefo['surface'] and self.jcl.meshdefo['surface']['fileformat'].lower() == 'cgns': self.read_cfdmesh_cgns(merge_domains) - elif 'fileformat' in self.jcl.meshdefo['surface'] and self.jcl.meshdefo['surface']['fileformat'].lower()=='netcdf': + elif 'fileformat' in self.jcl.meshdefo['surface'] and self.jcl.meshdefo['surface']['fileformat'].lower() == 'netcdf': self.read_cfdmesh_netcdf(merge_domains) - elif 'fileformat' in self.jcl.meshdefo['surface'] and self.jcl.meshdefo['surface']['fileformat'].lower()=='su2': + elif 'fileformat' in self.jcl.meshdefo['surface'] and self.jcl.meshdefo['surface']['fileformat'].lower() == 'su2': self.read_cfdmesh_su2(merge_domains) else: - logging.error('jcl.meshdefo["surface"]["fileformat"] must be "netcdf", "cgns" or "su2"' ) + logging.error('jcl.meshdefo["surface"]["fileformat"] must be "netcdf", "cgns" or "su2"') return - + def read_cfdmesh_cgns(self, merge_domains=False): - logging.info( 'Extracting all points from grid {}'.format(self.filename_grid)) + logging.info('Extracting all points from grid {}'.format(self.filename_grid)) f = h5py.File(self.filename_grid, 'r') - f_scale = 1.0/1000.0 # convert to SI units: 0.001 if mesh is given in [mm], 1.0 if given in [m] + f_scale = 1.0 / 1000.0 # convert to SI units: 0.001 if mesh is given in [mm], 1.0 if given in [m] keys = f['Base'].keys() keys.sort() self.cfdgrids = [] @@ -38,21 +42,21 @@ def read_cfdmesh_cgns(self, merge_domains=False): if key[:4] == 'dom-': logging.info(' - {} included'.format(key)) domain = f['Base'][key] - x = np.concatenate((x, domain['GridCoordinates']['CoordinateX'][' data'][:].reshape(-1)*f_scale)) - y = np.concatenate((y, domain['GridCoordinates']['CoordinateY'][' data'][:].reshape(-1)*f_scale)) - z = np.concatenate((z, domain['GridCoordinates']['CoordinateZ'][' data'][:].reshape(-1)*f_scale)) + x = np.concatenate((x, domain['GridCoordinates']['CoordinateX'][' data'][:].reshape(-1) * f_scale)) + y = np.concatenate((y, domain['GridCoordinates']['CoordinateY'][' data'][:].reshape(-1) * f_scale)) + z = np.concatenate((z, domain['GridCoordinates']['CoordinateZ'][' data'][:].reshape(-1) * f_scale)) else: logging.info(' - {} skipped'.format(key)) f.close() n = len(x) # build cfdgrid cfdgrid = {} - cfdgrid['ID'] = np.arange(n)+1 + cfdgrid['ID'] = np.arange(n) + 1 cfdgrid['CP'] = np.zeros(n) cfdgrid['CD'] = np.zeros(n) - cfdgrid['n'] = n - cfdgrid['offset'] = np.vstack((x,y,z)).T - cfdgrid['set'] = np.arange(6*cfdgrid['n']).reshape(-1,6) + cfdgrid['n'] = n + cfdgrid['offset'] = np.vstack((x, y, z)).T + cfdgrid['set'] = np.arange(6 * cfdgrid['n']).reshape(-1, 6) cfdgrid['desc'] = 'all domains' self.cfdgrids.append(cfdgrid) else: @@ -61,27 +65,27 @@ def read_cfdmesh_cgns(self, merge_domains=False): if key[:4] == 'dom-': logging.info(' - {} included'.format(key)) domain = f['Base'][key] - x = domain['GridCoordinates']['CoordinateX'][' data'][:].reshape(-1)*f_scale - y = domain['GridCoordinates']['CoordinateY'][' data'][:].reshape(-1)*f_scale - z = domain['GridCoordinates']['CoordinateZ'][' data'][:].reshape(-1)*f_scale + x = domain['GridCoordinates']['CoordinateX'][' data'][:].reshape(-1) * f_scale + y = domain['GridCoordinates']['CoordinateY'][' data'][:].reshape(-1) * f_scale + z = domain['GridCoordinates']['CoordinateZ'][' data'][:].reshape(-1) * f_scale n = len(x) # build cfdgrid cfdgrid = {} - cfdgrid['ID'] = np.arange(n)+1 + cfdgrid['ID'] = np.arange(n) + 1 cfdgrid['CP'] = np.zeros(n) cfdgrid['CD'] = np.zeros(n) - cfdgrid['n'] = n - cfdgrid['offset'] = np.vstack((x,y,z)).T - cfdgrid['set'] = np.arange(6*cfdgrid['n']).reshape(-1,6) + cfdgrid['n'] = n + cfdgrid['offset'] = np.vstack((x, y, z)).T + cfdgrid['set'] = np.arange(6 * cfdgrid['n']).reshape(-1, 6) cfdgrid['desc'] = key self.cfdgrids.append(cfdgrid) else: logging.info(' - {} skipped'.format(key)) f.close() - + def read_cfdmesh_netcdf(self, merge_domains=False): markers = self.markers - logging.info( 'Extracting points belonging to marker(s) {} from grid {}'.format(str(markers), self.filename_grid)) + logging.info('Extracting points belonging to marker(s) {} from grid {}'.format(str(markers), self.filename_grid)) # --- get all points on surfaces --- ncfile_grid = netcdf.NetCDFFile(self.filename_grid, 'r') boundarymarker_surfaces = ncfile_grid.variables['boundarymarker_of_surfaces'][:] @@ -91,8 +95,8 @@ def read_cfdmesh_netcdf(self, merge_domains=False): points_of_surface += ncfile_grid.variables['points_of_surfacetriangles'][:].tolist() if 'points_of_surfacequadrilaterals' in ncfile_grid.variables: points_of_surface += ncfile_grid.variables['points_of_surfacequadrilaterals'][:].tolist() - - if merge_domains: + + if merge_domains: # --- get points on surfaces according to marker --- surfaces = np.array([], dtype=int) for marker in markers: @@ -103,9 +107,11 @@ def read_cfdmesh_netcdf(self, merge_domains=False): self.cfdgrid['ID'] = points self.cfdgrid['CP'] = np.zeros(self.cfdgrid['ID'].shape) self.cfdgrid['CD'] = np.zeros(self.cfdgrid['ID'].shape) - self.cfdgrid['n'] = len(self.cfdgrid['ID']) - self.cfdgrid['offset'] = np.vstack((ncfile_grid.variables['points_xc'][:][points].copy(), ncfile_grid.variables['points_yc'][:][points].copy(), ncfile_grid.variables['points_zc'][:][points].copy() )).T - self.cfdgrid['set'] = np.arange(6*self.cfdgrid['n']).reshape(-1,6) + self.cfdgrid['n'] = len(self.cfdgrid['ID']) + self.cfdgrid['offset'] = np.vstack((ncfile_grid.variables['points_xc'][:][points].copy(), + ncfile_grid.variables['points_yc'][:][points].copy(), + ncfile_grid.variables['points_zc'][:][points].copy())).T + self.cfdgrid['set'] = np.arange(6 * self.cfdgrid['n']).reshape(-1, 6) self.cfdgrid['desc'] = markers self.cfdgrid['points_of_surface'] = [points_of_surface[s] for s in surfaces] else: @@ -113,35 +119,37 @@ def read_cfdmesh_netcdf(self, merge_domains=False): for marker in markers: # --- get points on surfaces according to marker --- surfaces = np.where(boundarymarker_surfaces == marker)[0] - points = np.unique([points_of_surface[s] for s in surfaces]) + points = np.unique([points_of_surface[s] for s in surfaces]) # build cfdgrid cfdgrid = {} cfdgrid['ID'] = points cfdgrid['CP'] = np.zeros(cfdgrid['ID'].shape) cfdgrid['CD'] = np.zeros(cfdgrid['ID'].shape) - cfdgrid['n'] = len(cfdgrid['ID']) - cfdgrid['offset'] = np.vstack((ncfile_grid.variables['points_xc'][:][points].copy(), ncfile_grid.variables['points_yc'][:][points].copy(), ncfile_grid.variables['points_zc'][:][points].copy() )).T - cfdgrid['set'] = np.arange(6*cfdgrid['n']).reshape(-1,6) + cfdgrid['n'] = len(cfdgrid['ID']) + cfdgrid['offset'] = np.vstack((ncfile_grid.variables['points_xc'][:][points].copy(), + ncfile_grid.variables['points_yc'][:][points].copy(), + ncfile_grid.variables['points_zc'][:][points].copy())).T + cfdgrid['set'] = np.arange(6 * cfdgrid['n']).reshape(-1, 6) cfdgrid['desc'] = str(marker) cfdgrid['points_of_surface'] = [points_of_surface[s] for s in surfaces] self.cfdgrids.append(cfdgrid) ncfile_grid.close() - + def read_cfdmesh_su2(self, merge_domains=False): """ The description of the SU2 mesh file format is given here: https://su2code.github.io/docs/Mesh-File/ """ - logging.info( 'Extracting points belonging to surface marker(s) from grid {}'.format(self.filename_grid)) + logging.info('Extracting points belonging to surface marker(s) from grid {}'.format(self.filename_grid)) # Open the ascii file and read all lines. with open(self.filename_grid, 'r') as fid: lines = fid.readlines() - # Loop over all lines, if a keyword such as NELEM, NPOIN or MARKER_TAG is found, + # Loop over all lines, if a keyword such as NELEM, NPOIN or MARKER_TAG is found, # this announces a new section in the file. surface_points = {} n_lines = len(lines) i = 0 while i < n_lines: - if str.find(lines[i], 'NELEM') !=- 1: + if str.find(lines[i], 'NELEM') != -1: n_elem = int(lines[i].split()[1]) # There is nothing we need to do with the volume element connectivity, so we can skip this section. # Skipping all lines (at once) saves a lot of time, since there are many volume elements... @@ -154,12 +162,12 @@ def read_cfdmesh_su2(self, merge_domains=False): # Loop over next lines to read all points and their coordinates tmp = [] for x in range(n_points): - tmp.append(lines[i+x].split()) + tmp.append(lines[i + x].split()) points = np.array(tmp, dtype=np.float) i += x elif str.find(lines[i], 'MARKER_TAG') != -1: # New section found. - # Here, all points are listed that belong to one marker. + # Here, all points are listed that belong to one marker. # In addition, the connectivity is given, which we need e.g. for plotting. marker = lines[i].split()[1] i += 1 @@ -169,16 +177,16 @@ def read_cfdmesh_su2(self, merge_domains=False): triangles = [] quadrilaterals = [] for x in range(n_elem): - split_line = lines[i+x].split() + split_line = lines[i + x].split() if split_line[0] == '5': # Triangle elements are identified with a 5 - triangles.append( [int(id) for id in split_line[1:]] ) + triangles.append([int(id) for id in split_line[1:]]) elif split_line[0] == '9': # Quadrilateral elements are identified with a 9 - quadrilaterals.append( [int(id) for id in split_line[1:]]) + quadrilaterals.append([int(id) for id in split_line[1:]]) else: logging.error('Surface elements of type "{}" are not implemented!'.format(split_line[0])) - + i += x # Store everything surface_points[marker] = {} @@ -186,22 +194,22 @@ def read_cfdmesh_su2(self, merge_domains=False): surface_points[marker]['points_of_surface'] = triangles + quadrilaterals i += 1 - if merge_domains: + if merge_domains: # First we need to do some merging... tmp_points = np.array([], dtype=int) tmp_surfaces = [] for marker in self.markers: tmp_points = np.hstack((tmp_points, surface_points[marker]['points'])) tmp_surfaces += surface_points[marker]['points_of_surface'] - + # Assemble the cfdgrid self.cfdgrid = {} self.cfdgrid['ID'] = np.unique(tmp_points) self.cfdgrid['CP'] = np.zeros(self.cfdgrid['ID'].shape) self.cfdgrid['CD'] = np.zeros(self.cfdgrid['ID'].shape) - self.cfdgrid['n'] = len(self.cfdgrid['ID']) - self.cfdgrid['offset'] = points[self.cfdgrid['ID'],:3] - self.cfdgrid['set'] = np.arange(6*self.cfdgrid['n']).reshape(-1,6) + self.cfdgrid['n'] = len(self.cfdgrid['ID']) + self.cfdgrid['offset'] = points[self.cfdgrid['ID'], :3] + self.cfdgrid['set'] = np.arange(6 * self.cfdgrid['n']).reshape(-1, 6) self.cfdgrid['desc'] = self.markers self.cfdgrid['points_of_surface'] = tmp_surfaces else: @@ -213,8 +221,8 @@ def read_cfdmesh_su2(self, merge_domains=False): cfdgrid['CP'] = np.zeros(cfdgrid['ID'].shape) cfdgrid['CD'] = np.zeros(cfdgrid['ID'].shape) cfdgrid['n'] = len(cfdgrid['ID']) - cfdgrid['offset'] = points[cfdgrid['ID'],:3] - cfdgrid['set'] = np.arange(6*cfdgrid['n']).reshape(-1,6) + cfdgrid['offset'] = points[cfdgrid['ID'], :3] + cfdgrid['set'] = np.arange(6 * cfdgrid['n']).reshape(-1, 6) cfdgrid['desc'] = marker cfdgrid['points_of_surface'] = surface_points[marker]['points_of_surface'] self.cfdgrids[marker] = cfdgrid diff --git a/loadskernel/io_functions/read_h5.py b/loadskernel/io_functions/read_h5.py index 602c39b..8b55734 100644 --- a/loadskernel/io_functions/read_h5.py +++ b/loadskernel/io_functions/read_h5.py @@ -1,16 +1,19 @@ -import scipy, logging +import logging +import scipy + from loadskernel.io_functions.data_handling import load_hdf5 + def load_matrix(filename, name): """ - This function reads matrices from Nastran's HDF5 output. + This function reads matrices from Nastran's HDF5 output. Usage of this function: matrix = load_matrix('/path/to/my_bdf_name.mtx.h5', 'KGG') - + The matrices are exported using the following procedure, tested with Nastran 2023.1: In a DMAP alter, any matrices can be send into the HDF5 file with CRDB_MTX MGG//'MGG' $ - Then, in the BULK section, the precision is set to 64, compression is deactivated and the + Then, in the BULK section, the precision is set to 64, compression is deactivated and the matrix output is activated with HDF5OUT PRCISION 64 CMPRMTHD NONE MTX YES """ @@ -23,22 +26,22 @@ def load_matrix(filename, name): # If the matrix is in the file, go ahead, otherwise issue an error message and return an empty 0x0 matrix if name in names: logging.info('Reading matrix {} from {}'.format(name, filename)) - # Get some meta data given by the 'IDENTITY' table. This is important becasue in case multiple matrices - # are stored in the same hdf5 file, the data is simply appended. The meta data identifies which pieces + # Get some meta data given by the 'IDENTITY' table. This is important becasue in case multiple matrices + # are stored in the same hdf5 file, the data is simply appended. The meta data identifies which pieces # of data belong to which matrix. i_matrix = names.index(name) - n_row = hdf5_group['IDENTITY']['ROW'][i_matrix] - n_col = hdf5_group['IDENTITY']['COLUMN'][i_matrix] - col_pos = hdf5_group['IDENTITY']['COLUMN_POS'][i_matrix] - data_pos = hdf5_group['IDENTITY']['DATA_POS'][i_matrix] - non_zero = hdf5_group['IDENTITY']['NON_ZERO'][i_matrix] + n_row = hdf5_group['IDENTITY']['ROW'][i_matrix] + n_col = hdf5_group['IDENTITY']['COLUMN'][i_matrix] + col_pos = hdf5_group['IDENTITY']['COLUMN_POS'][i_matrix] + data_pos = hdf5_group['IDENTITY']['DATA_POS'][i_matrix] + non_zero = hdf5_group['IDENTITY']['NON_ZERO'][i_matrix] # Get pointers, indices and data - indptr = hdf5_group['COLUMN']['POSITION'][col_pos:col_pos+n_col+1]-data_pos - indices = hdf5_group['DATA']['ROW'][data_pos:data_pos+non_zero] - data = hdf5_group['DATA']['VALUE'][data_pos:data_pos+non_zero] + indptr = hdf5_group['COLUMN']['POSITION'][col_pos:col_pos + n_col + 1] - data_pos + indices = hdf5_group['DATA']['ROW'][data_pos:data_pos + non_zero] + data = hdf5_group['DATA']['VALUE'][data_pos:data_pos + non_zero] # Set-up a scipy CSC matrix M = scipy.sparse.csc_matrix((data, indices, indptr), shape=(n_row, n_col)) return M - else: - logging.error('Matrix {} not found in {}'.format(name, filename)) - return None + + logging.error('Matrix {} not found in {}'.format(name, filename)) + return None diff --git a/loadskernel/io_functions/read_mona.py b/loadskernel/io_functions/read_mona.py index 74a5851..ef1a290 100644 --- a/loadskernel/io_functions/read_mona.py +++ b/loadskernel/io_functions/read_mona.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- -""" -Created on Fri Nov 21 16:29:57 2014 +import copy +import logging +import os +import math +from itertools import compress -@author: voss_ar -""" -import string, copy import numpy as np -import scipy.sparse as sp -import math as math -import os, logging import pandas as pd -from itertools import compress +import scipy.sparse as sp + def NASTRAN_f06_modal(filename, modes_selected='all', omitt_rigid_body_modes=False): ''' @@ -18,25 +16,23 @@ def NASTRAN_f06_modal(filename, modes_selected='all', omitt_rigid_body_modes=Fal eigenvectors. (Basierend auf Script von Markus.) ''' filesize = float(os.stat(filename).st_size) - logging.info('Read modal data from f06 file: %s with %.2f MB' %(filename, filesize/1024**2)) - - eigenvalues = {"ModeNo":[], - "ExtractionOrder":[], - "Eigenvalue":[], - "Radians":[], - "Cycles":[], - "GeneralizedMass":[], - "GeneralizedStiffness":[]} - grids = [] - CaseControlEcho = [] + logging.info('Read modal data from f06 file: %s with %.2f MB' % (filename, filesize / 1024 ** 2)) + + eigenvalues = {"ModeNo": [], + "ExtractionOrder": [], + "Eigenvalue": [], + "Radians": [], + "Cycles": [], + "GeneralizedMass": [], + "GeneralizedStiffness": []} node_ids = [] eigenvectors = {} - + with open(filename, 'r') as fid: while True: read_string = fid.readline() - if str.find(str.replace(read_string,' ',''),'REALEIGENVALUES') != -1: - #print ' -> reading eigenvalues...' # Eigenwerte + if str.find(str.replace(read_string, ' ', ''), 'REALEIGENVALUES') != -1: + # print ' -> reading eigenvalues...' # Eigenwerte fid.readline() fid.readline() while True: @@ -51,10 +47,10 @@ def NASTRAN_f06_modal(filename, modes_selected='all', omitt_rigid_body_modes=Fal eigenvalues["GeneralizedStiffness"].append(float(line[6])) else: break - - elif str.find(str.replace(read_string,' ',''),'REALEIGENVECTORNO') != -1 and read_string != '': + + elif str.find(str.replace(read_string, ' ', ''), 'REALEIGENVECTORNO') != -1 and read_string != '': eigenvector_no = int(str.split(read_string)[-1]) - if not str(eigenvector_no) in eigenvectors: + if str(eigenvector_no) not in eigenvectors: eigenvectors[str(eigenvector_no)] = [] fid.readline() fid.readline() @@ -62,37 +58,40 @@ def NASTRAN_f06_modal(filename, modes_selected='all', omitt_rigid_body_modes=Fal line = str.split(fid.readline()) if len(line) == 8 and line[1] == 'G': node_ids.append(int(line[0])) - eigenvectors[str(eigenvector_no)].append([int(line[0]), float(line[2]),float(line[3]),float(line[4]),float(line[5]),float(line[6]),float(line[7])]) + eigenvectors[str(eigenvector_no)].append([int(line[0]), float(line[2]), float(line[3]), float(line[4]), + float(line[5]), float(line[6]), float(line[7])]) else: break - + elif read_string == '': - break + break + + logging.info('Found %i eigenvalues and %i eigenvectors for %i nodes.', len(eigenvalues["ModeNo"]), + len(eigenvectors.keys()), len(node_ids) / len(eigenvalues["ModeNo"])) + return eigenvalues, eigenvectors, node_ids + - logging.info('Found %i eigenvalues and %i eigenvectors for %i nodes.' %(len(eigenvalues["ModeNo"]), len(eigenvectors.keys()), len(node_ids)/len(eigenvalues["ModeNo"]))) - return eigenvalues, eigenvectors, node_ids - def reduce_modes(eigenvalues, eigenvectors, nodes_selection, modes_selection): - logging.info('Reduction of data to %i selected modes and %i nodes.' %(len(modes_selection), len(nodes_selection))) - eigenvalues_new = {"ModeNo":[], - "ExtractionOrder":[], - "Eigenvalue":[], - "Radians":[], - "Cycles":[], - "GeneralizedMass":[], - "GeneralizedStiffness":[]} + logging.info('Reduction of data to %i selected modes and %i nodes.', len(modes_selection), len(nodes_selection)) + eigenvalues_new = {"ModeNo": [], + "ExtractionOrder": [], + "Eigenvalue": [], + "Radians": [], + "Cycles": [], + "GeneralizedMass": [], + "GeneralizedStiffness": []} eigenvectors_new = {} # Searching for the indices of the selected nodes take time # Assumption: nodes have the same sequence in all modes pos_eigenvector = [] - nodes_eigenvector = np.array(eigenvectors[str(modes_selection[0])])[:,0] + nodes_eigenvector = np.array(eigenvectors[str(modes_selection[0])])[:, 0] logging.info(' - working on nodes...') for i_nodes in range(len(nodes_selection)): - pos_eigenvector.append( np.where(nodes_selection[i_nodes] == nodes_eigenvector)[0][0] ) - + pos_eigenvector.append(np.where(nodes_selection[i_nodes] == nodes_eigenvector)[0][0]) + logging.info(' - working on modes...') for i_mode in range(len(modes_selection)): - pos_mode = np.where(modes_selection[i_mode]==np.array(eigenvalues['ModeNo']))[0][0] + pos_mode = np.where(modes_selection[i_mode] == np.array(eigenvalues['ModeNo']))[0][0] eigenvalues_new['ModeNo'].append(eigenvalues['ModeNo'][pos_mode]) eigenvalues_new['ExtractionOrder'].append(eigenvalues['ExtractionOrder'][pos_mode]) eigenvalues_new['Eigenvalue'].append(eigenvalues['Eigenvalue'][pos_mode]) @@ -100,32 +99,33 @@ def reduce_modes(eigenvalues, eigenvectors, nodes_selection, modes_selection): eigenvalues_new['Cycles'].append(eigenvalues['Cycles'][pos_mode]) eigenvalues_new['GeneralizedMass'].append(eigenvalues['GeneralizedMass'][pos_mode]) eigenvalues_new['GeneralizedStiffness'].append(eigenvalues['GeneralizedStiffness'][pos_mode]) - + eigenvectors_new[str(modes_selection[i_mode])] = np.array(eigenvectors[str(modes_selection[i_mode])])[pos_eigenvector] - + return eigenvalues_new, eigenvectors_new + def Nastran_weightgenerator(filename): - logging.info('Read Weight data from f06 file: %s' %filename) - + logging.info('Read Weight data from f06 file: %s', filename) + with open(filename, 'r') as fid: while True: read_string = fid.readline() - if str.find(read_string, 'O U T P U T F R O M G R I D P O I N T W E I G H T G E N E R A T O R') !=-1: + if str.find(read_string, 'O U T P U T F R O M G R I D P O I N T W E I G H T G E N E R A T O R') != -1: read_string = fid.readline() CID = nastran_number_converter(read_string.split()[-1], 'int') read_string = fid.readline() massmatrix_0 = [] - for i in range(6): + for i in range(6): read_string = fid.readline() - massmatrix_0.append([nastran_number_converter(read_string.split()[1], 'float'), \ - nastran_number_converter(read_string.split()[2], 'float'), \ - nastran_number_converter(read_string.split()[3], 'float'), \ - nastran_number_converter(read_string.split()[4], 'float'), \ - nastran_number_converter(read_string.split()[5], 'float'), \ - nastran_number_converter(read_string.split()[6], 'float'), \ - ]) - elif str.find(read_string, 'MASS AXIS SYSTEM (S)') !=-1: + massmatrix_0.append([nastran_number_converter(read_string.split()[1], 'float'), + nastran_number_converter(read_string.split()[2], 'float'), + nastran_number_converter(read_string.split()[3], 'float'), + nastran_number_converter(read_string.split()[4], 'float'), + nastran_number_converter(read_string.split()[5], 'float'), + nastran_number_converter(read_string.split()[6], 'float'), + ]) + elif str.find(read_string, 'MASS AXIS SYSTEM (S)') != -1: read_string = fid.readline() cg_y = nastran_number_converter(read_string.split()[3], 'float') cg_z = nastran_number_converter(read_string.split()[4], 'float') @@ -134,71 +134,77 @@ def Nastran_weightgenerator(filename): offset_cg = np.array([cg_x, cg_y, cg_z]) read_string = fid.readline() read_string = fid.readline() - + inertia = [] - for i in range(3): + for i in range(3): read_string = fid.readline() - inertia.append([nastran_number_converter(read_string.split()[1], 'float'), \ - nastran_number_converter(read_string.split()[2], 'float'), \ - nastran_number_converter(read_string.split()[3], 'float'), \ - ]) + inertia.append([nastran_number_converter(read_string.split()[1], 'float'), + nastran_number_converter(read_string.split()[2], 'float'), + nastran_number_converter(read_string.split()[3], 'float'), + ]) break elif read_string == '': - break - + break + return np.array(massmatrix_0), np.array(inertia), offset_cg, CID - + + def Modgen_GRID(filename): - logging.info('Read GRID data from ModGen file: %s' %filename) + logging.info('Read GRID data from ModGen file: %s' % filename) grids = [] with open(filename, 'r') as fid: lines = fid.readlines() for line in lines: - if line[0] != '$' and str.find(line[:8], 'GRID') !=-1 : - if len(line) <= 48: # if CD is missing, fix with CP + if line[0] != '$' and str.find(line[:8], 'GRID') != -1: + if len(line) <= 48: # if CD is missing, fix with CP line = line + ' ' - grids.append([nastran_number_converter(line[8:16], 'ID'), nastran_number_converter(line[16:24], 'CP'), nastran_number_converter(line[24:32], 'float'), nastran_number_converter(line[32:40], 'float'), nastran_number_converter(line[40:48], 'float'), nastran_number_converter(line[48:56], 'CD')]) - + grids.append([nastran_number_converter(line[8:16], 'ID'), nastran_number_converter(line[16:24], 'CP'), + nastran_number_converter(line[24:32], 'float'), nastran_number_converter(line[32:40], 'float'), + nastran_number_converter(line[40:48], 'float'), nastran_number_converter(line[48:56], 'CD')]) + n = len(grids) grid = {"ID": np.array([grid[0] for grid in grids]), - "offset":np.array([grid[2:5] for grid in grids]), + "offset": np.array([grid[2:5] for grid in grids]), "n": n, "CP": np.array([grid[1] for grid in grids]), "CD": np.array([grid[5] for grid in grids]), - "set": np.arange(n*6).reshape((n,6)), - } + "set": np.arange(n * 6).reshape((n, 6)), + } return grid + def add_GRIDS(pandas_grids): # This functions relies on the Pandas data frames from the bdf reader. n = pandas_grids.shape[0] strcgrid = {} - strcgrid['ID'] = pandas_grids['ID'].to_numpy(dtype='int') - strcgrid['CD'] = pandas_grids['CD'].to_numpy(dtype='int') - strcgrid['CP'] = pandas_grids['CP'].to_numpy(dtype='int') - strcgrid['n'] = n - strcgrid['set'] = np.arange(n*6).reshape((n,6)) + strcgrid['ID'] = pandas_grids['ID'].to_numpy(dtype='int') + strcgrid['CD'] = pandas_grids['CD'].to_numpy(dtype='int') + strcgrid['CP'] = pandas_grids['CP'].to_numpy(dtype='int') + strcgrid['n'] = n + strcgrid['set'] = np.arange(n * 6).reshape((n, 6)) strcgrid['offset'] = pandas_grids[['X1', 'X2', 'X3']].to_numpy(dtype='float') return strcgrid + def add_shell_elements(pandas_panels): # This functions relies on the Pandas data frames from the bdf reader. strcshell = {} n = pandas_panels.shape[0] - strcshell['ID'] = pandas_panels['ID'].to_numpy(dtype='int') - strcshell['cornerpoints'] = np.array(pandas_panels[['G1', 'G2', 'G3', 'G4']]) - strcshell['CD'] = np.zeros(n) # Assumption: panels are given in global coord system - strcshell['CP'] = np.zeros(n) - strcshell['n'] = n + strcshell['ID'] = pandas_panels['ID'].to_numpy(dtype='int') + strcshell['cornerpoints'] = np.array(pandas_panels[['G1', 'G2', 'G3', 'G4']]) + strcshell['CD'] = np.zeros(n) # Assumption: panels are given in global coord system + strcshell['CP'] = np.zeros(n) + strcshell['n'] = n return strcshell - + + def add_panels_from_CAERO(pandas_caero, pandas_aefact): logging.info('Constructing aero panels from CAERO cards') # from CAERO cards, construct corner points... ' # then, combine four corner points to one panel - grid_ID = 1 # the file number is used to set a range of grid IDs - grids = {'ID':[], 'offset':[]} - panels = {"ID": [], 'CP':[], 'CD':[], "cornerpoints": []} + grid_ID = 1 # the file number is used to set a range of grid IDs + grids = {'ID': [], 'offset': []} + panels = {"ID": [], 'CP': [], 'CD': [], "cornerpoints": []} for index, caerocard in pandas_caero.iterrows(): # get the four corner points of the CAERO card X1 = caerocard[['X1', 'Y1', 'Z1']].to_numpy(dtype='float') @@ -206,55 +212,56 @@ def add_panels_from_CAERO(pandas_caero, pandas_aefact): X2 = X1 + np.array([caerocard['X12'], 0.0, 0.0]) X3 = X4 + np.array([caerocard['X43'], 0.0, 0.0]) # calculate LE, Root and Tip vectors [x,y,z]^T - LE = X4 - X1 + LE = X4 - X1 Root = X2 - X1 - Tip = X3 - X4 - n_span = int(caerocard['NSPAN']) + Tip = X3 - X4 + n_span = int(caerocard['NSPAN']) n_chord = int(caerocard['NCHORD']) if caerocard['NCHORD'] == 0: # look in AEFACT cards for the appropriate card and get spacing if pd.notna(caerocard['LCHORD']): - d_chord = [v for v in pandas_aefact.loc[pandas_aefact['ID']==caerocard['LCHORD'],'values'].values[0] if v is not None] - n_chord = len(d_chord)-1 # n_boxes = n_division-1 + d_chord = [v for v in pandas_aefact.loc[pandas_aefact['ID'] == caerocard['LCHORD'], + 'values'].values[0] if v is not None] + n_chord = len(d_chord) - 1 # n_boxes = n_division-1 else: logging.error('Assumption of equal spaced CAERO7 panels is violated!') else: # assume equidistant spacing - d_chord = np.linspace(0.0, 1.0, n_chord+1 ) - + d_chord = np.linspace(0.0, 1.0, n_chord + 1) + if caerocard['NSPAN'] == 0: # look in AEFACT cards for the appropriate card and get spacing if pd.notna(caerocard['LSPAN']): - d_span = [v for v in pandas_aefact.loc[pandas_aefact['ID']==caerocard['LSPAN'],'values'].values[0] if v is not None] - n_span = len(d_span)-1 # n_boxes = n_division-1 + d_span = [v for v in pandas_aefact.loc[pandas_aefact['ID'] == caerocard['LSPAN'], + 'values'].values[0] if v is not None] + n_span = len(d_span) - 1 # n_boxes = n_division-1 else: logging.error('Assumption of equal spaced CAERO7 panels is violated!') else: # assume equidistant spacing - d_span = np.linspace(0.0, 1.0, n_span+1 ) - + d_span = np.linspace(0.0, 1.0, n_span + 1) + # build matrix of corner points # index based on n_divisions - grids_map = np.zeros((n_chord+1,n_span+1), dtype='int') - for i_strip in range(n_span+1): - for i_row in range(n_chord+1): - offset = X1 \ - + LE * d_span[i_strip] \ - + (Root*(1.0-d_span[i_strip]) + Tip*d_span[i_strip]) * d_chord[i_row] + grids_map = np.zeros((n_chord + 1, n_span + 1), dtype='int') + for i_strip in range(n_span + 1): + for i_row in range(n_chord + 1): + offset = X1 + LE * d_span[i_strip] + (Root * (1.0 - d_span[i_strip]) + Tip * d_span[i_strip]) * d_chord[i_row] grids['ID'].append(grid_ID) grids['offset'].append(offset) - grids_map[i_row,i_strip ] = grid_ID + grids_map[i_row, i_strip] = grid_ID grid_ID += 1 # build panels from cornerpoints # index based on n_boxes - panel_ID = int(caerocard['ID']) + panel_ID = int(caerocard['ID']) for i_strip in range(n_span): for i_row in range(n_chord): panels['ID'].append(panel_ID) - panels['CP'].append(caerocard['CP']) # applying CP of CAERO card to all grids + panels['CP'].append(caerocard['CP']) # applying CP of CAERO card to all grids panels['CD'].append(caerocard['CP']) - panels['cornerpoints'].append([ grids_map[i_row, i_strip], grids_map[i_row+1, i_strip], grids_map[i_row+1, i_strip+1], grids_map[i_row, i_strip+1] ]) - panel_ID += 1 + panels['cornerpoints'].append([grids_map[i_row, i_strip], grids_map[i_row + 1, i_strip], + grids_map[i_row + 1, i_strip + 1], grids_map[i_row, i_strip + 1]]) + panel_ID += 1 panels['ID'] = np.array(panels['ID']) panels['CP'] = np.array(panels['CP']) panels['CD'] = np.array(panels['CD']) @@ -263,14 +270,17 @@ def add_panels_from_CAERO(pandas_caero, pandas_aefact): grids['offset'] = np.array(grids['offset']) return grids, panels + def nastran_number_converter(string_in, type, default=0): if type in ['float', 'f']: try: out = float(string_in) - except: + except ValueError: # remove all spaces, which might also occur in between the sign and the number (e.g. in ModGen) string_in = string_in.replace(' ', '') - for c in ['\n', '\r']: string_in = string_in.strip(c) # remove end of line + # remove end of line + for c in ['\n', '\r']: + string_in = string_in.strip(c) if '-' in string_in[1:]: if string_in[0] in ['-', '+']: sign = string_in[0] @@ -284,15 +294,16 @@ def nastran_number_converter(string_in, type, default=0): else: out = float(string_in.replace('+', 'E+')) elif string_in == '': - logging.debug("Could not interpret the following number: '" + string_in + "' -> setting value to "+str(default)) + logging.debug("Could not interpret the following number: '" + string_in + + "' -> setting value to " + str(default)) out = default - else: + else: logging.error("Could not interpret the following number: " + string_in) return - elif type in ['int', 'i','ID', 'CD', 'CP']: + elif type in ['int', 'i', 'ID', 'CD', 'CP']: try: out = int(string_in) - except: + except ValueError: out = default elif type in ['str']: # An dieser Stelle reicht es nicht mehr aus, nur die Leerzeichen zu entfernen... @@ -301,9 +312,10 @@ def nastran_number_converter(string_in, type, default=0): out = string_in.strip('*, ') if out == '': out = default - + return out + def Nastran_DMI(filename): DMI = {} with open(filename, 'r') as fid: @@ -311,14 +323,14 @@ def Nastran_DMI(filename): read_string = fid.readline() if read_string[:3] == 'DMI' and nastran_number_converter(read_string[16:24], 'int') == 0: # this is the header - DMI['NAME'] = str.replace(read_string[8:16],' ','') # matrix name + DMI['NAME'] = str.replace(read_string[8:16], ' ', '') # matrix name DMI['FORM'] = nastran_number_converter(read_string[24:32], 'int') - DMI['TIN'] = nastran_number_converter(read_string[32:40], 'int') - DMI['n_row'] = nastran_number_converter(read_string[56:64], 'int') - DMI['n_col'] = nastran_number_converter(read_string[64:72], 'int') - if DMI['TIN'] in [1,2]: + DMI['TIN'] = nastran_number_converter(read_string[32:40], 'int') + DMI['n_row'] = nastran_number_converter(read_string[56:64], 'int') + DMI['n_col'] = nastran_number_converter(read_string[64:72], 'int') + if DMI['TIN'] in [1, 2]: DMI['data'] = sp.lil_matrix((DMI['n_col'], DMI['n_row']), dtype=float) - elif DMI['TIN'] in [3,4]: + elif DMI['TIN'] in [3, 4]: logging.error('Complex DMI matrix input NOT supported.') logging.info('Read {} data from file: {}'.format(DMI['NAME'], filename)) while True: @@ -329,53 +341,56 @@ def Nastran_DMI(filename): i_col = nastran_number_converter(read_string[16:24], 'int') col = read_string[24:-1] # figure out how many lines the column will have - n_items = DMI['n_row'] # items that go into the column - n_lines = int(math.ceil((n_items-3)/4.0)) - for i_line in range(n_lines): + n_items = DMI['n_row'] # items that go into the column + n_lines = int(math.ceil((n_items - 3) / 4.0)) + for _ in range(n_lines): col += fid.readline()[8:-1] - for i_item in range(n_items): - DMI['data'][i_col-1, nastran_number_converter(col[:8], 'int')-1] = nastran_number_converter(col[8:16], 'float') + for _ in range(n_items): + DMI['data'][i_col - 1, nastran_number_converter(col[:8], 'int') - 1] = \ + nastran_number_converter(col[8:16], 'float') col = col[16:] else: break elif read_string == '': # end of file break - return DMI - + return DMI + + def add_CORD2R(pandas_cord2r, coord): # This functions relies on the Pandas data frames from the bdf reader. - for index, row in pandas_cord2r.iterrows(): + for _, row in pandas_cord2r.iterrows(): ID = int(row['ID']) RID = int(row['RID']) A = row[['A1', 'A2', 'A3']].to_numpy(dtype='float').squeeze() B = row[['B1', 'B2', 'B3']].to_numpy(dtype='float').squeeze() C = row[['C1', 'C2', 'C3']].to_numpy(dtype='float').squeeze() - # build coord + # build coord z = B - A - y = np.cross(B-A, C-A) - x = np.cross(y,z) - dircos = np.vstack((x/np.linalg.norm(x),y/np.linalg.norm(y),z/np.linalg.norm(z))).T + y = np.cross(B - A, C - A) + x = np.cross(y, z) + dircos = np.vstack((x / np.linalg.norm(x), y / np.linalg.norm(y), z / np.linalg.norm(z))).T # save if ID not in coord['ID']: coord['ID'].append(ID) coord['RID'].append(RID) coord['offset'].append(A) coord['dircos'].append(dircos) - + + def add_CORD1R(pandas_cord1r, coord, strcgrid): # This functions relies on the Pandas data frames from the bdf reader. - for index, row in pandas_cord1r.iterrows(): + for _, row in pandas_cord1r.iterrows(): ID = int(row['ID']) RID = 0 A = strcgrid['offset'][np.where(strcgrid['ID'] == row['A'])[0][0]] - B = strcgrid['offset'][np.where(strcgrid['ID'] == row['B'])[0][0]] - C = strcgrid['offset'][np.where(strcgrid['ID'] == row['C'])[0][0]] - # build coord - wie bei CORD2R + B = strcgrid['offset'][np.where(strcgrid['ID'] == row['B'])[0][0]] + C = strcgrid['offset'][np.where(strcgrid['ID'] == row['C'])[0][0]] + # build coord - wie bei CORD2R z = B - A - y = np.cross(B-A, C-A) - x = np.cross(y,z) - dircos = np.vstack((x/np.linalg.norm(x),y/np.linalg.norm(y),z/np.linalg.norm(z))).T + y = np.cross(B - A, C - A) + x = np.cross(y, z) + dircos = np.vstack((x / np.linalg.norm(x), y / np.linalg.norm(y), z / np.linalg.norm(z))).T # save if ID not in coord['ID']: coord['ID'].append(ID) @@ -383,18 +398,20 @@ def add_CORD1R(pandas_cord1r, coord, strcgrid): coord['offset'].append(A) coord['dircos'].append(dircos) + def add_AESURF(pandas_aesurfs): aesurf = {} - aesurf['ID'] = pandas_aesurfs['ID'].to_list() - aesurf['key'] = pandas_aesurfs['LABEL'].to_list() - aesurf['CID'] = pandas_aesurfs['CID'].to_list() - aesurf['AELIST'] = pandas_aesurfs['AELIST'].to_list() - aesurf['eff'] = pandas_aesurfs['EFF'].to_list() + aesurf['ID'] = pandas_aesurfs['ID'].to_list() + aesurf['key'] = pandas_aesurfs['LABEL'].to_list() + aesurf['CID'] = pandas_aesurfs['CID'].to_list() + aesurf['AELIST'] = pandas_aesurfs['AELIST'].to_list() + aesurf['eff'] = pandas_aesurfs['EFF'].to_list() return aesurf + def add_SET1(pandas_sets): # This functions relies on the Pandas data frames from the bdf reader. - # Due to the mixture of integers and strings ('THRU') in a SET1 card, all list items were parsed as strings. + # Due to the mixture of integers and strings ('THRU') in a SET1 card, all list items were parsed as strings. # This function parses the strings to integers and handles the 'THRU' option. set_values = [] for _, row in pandas_sets[['values']].iterrows(): @@ -406,9 +423,9 @@ def add_SET1(pandas_sets): while my_row: if my_row[0] == 'THRU': # Replace 'THRU' with the intermediate values - startvalue = values[-1]+1 + startvalue = values[-1] + 1 stoppvalue = nastran_number_converter(my_row[1], 'int', default=None) - values += list(range(startvalue, stoppvalue+1) ) + values += list(range(startvalue, stoppvalue + 1)) # remove consumed values from row my_row.pop(0) my_row.pop(0) @@ -417,13 +434,14 @@ def add_SET1(pandas_sets): values.append(nastran_number_converter(my_row[0], 'int', default=None)) # remove consumed values from row my_row.pop(0) - set_values.append(np.array(values)) + set_values.append(np.array(values)) sets = {} - sets['ID'] = pandas_sets['ID'].to_list() - sets['values'] = set_values + sets['ID'] = pandas_sets['ID'].to_list() + sets['values'] = set_values return sets - + + def add_AECOMP(pandas_aecomps): # This functions relies on the Pandas data frames from the bdf reader. list_id = [] @@ -432,24 +450,25 @@ def add_AECOMP(pandas_aecomps): for _, row in pandas_aecomps[['LISTID']].iterrows(): is_id = [pd.notna(x) for x in row[0]] list_id.append(list(compress(row[0], is_id))) - + aecomp = {} - aecomp['name'] = pandas_aecomps['NAME'].to_list() + aecomp['name'] = pandas_aecomps['NAME'].to_list() aecomp['list_type'] = pandas_aecomps['LISTTYPE'].to_list() - aecomp['list_id'] = list_id + aecomp['list_id'] = list_id return aecomp + def add_MONPNT1(pandas_monpnts): # This functions relies on the Pandas data frames from the bdf reader. n = pandas_monpnts.shape[0] mongrid = {} # Eigentlich haben MONPNTs keine IDs sondern nur Namen... - mongrid['ID'] = np.arange(1,n+1) - mongrid['name'] = pandas_monpnts['NAME'].to_list() - mongrid['comp'] = pandas_monpnts['COMP'].to_list() - mongrid['CD'] = pandas_monpnts['CD'].to_numpy(dtype='int') - mongrid['CP'] = pandas_monpnts['CP'].to_numpy(dtype='int') - mongrid['n'] = n - mongrid['set'] = np.arange(n*6).reshape((n,6)) + mongrid['ID'] = np.arange(1, n + 1) + mongrid['name'] = pandas_monpnts['NAME'].to_list() + mongrid['comp'] = pandas_monpnts['COMP'].to_list() + mongrid['CD'] = pandas_monpnts['CD'].to_numpy(dtype='int') + mongrid['CP'] = pandas_monpnts['CP'].to_numpy(dtype='int') + mongrid['n'] = n + mongrid['set'] = np.arange(n * 6).reshape((n, 6)) mongrid['offset'] = pandas_monpnts[['X', 'Y', 'Z']].to_numpy(dtype='float') return mongrid diff --git a/loadskernel/io_functions/read_op2.py b/loadskernel/io_functions/read_op2.py index 41c72ac..0e3fac9 100644 --- a/loadskernel/io_functions/read_op2.py +++ b/loadskernel/io_functions/read_op2.py @@ -1,5 +1,5 @@ """ -This OP2 reader is adopted from pyNastran, which is licensed under the +This OP2 reader is adopted from pyNastran, which is licensed under the following conditions. See also https://github.com/SteveDoyle2/pyNastran. @@ -91,12 +91,12 @@ # on, until data block is read in. -class OP2(object): +class OP2(): """Class for reading Nastran op2 files and nas2cam data files.""" def __init__(self, filename=None): self._fileh = None - if isinstance(filename, str) or isinstance(filename, unicode): + if isinstance(filename, str): self._op2_open(filename) def __del__(self): @@ -194,7 +194,7 @@ def _op2_open(self, filename): self._intstru = self._endian + '%dq' self._ibytes = 8 self._Str = struct.Struct(self._endian + 'q') - #print('bit64 = ', self._bit64) + # print('bit64 = ', self._bit64) self._rowsCutoff = 3000 self._int32str = self._endian + 'i4' @@ -212,7 +212,7 @@ def _get_key(self): def _skip_key(self, n): """Skips `n` key triplets ([reclen, key, endrec]).""" - self._fileh.read(n*(8+self._ibytes)) + self._fileh.read(n * (8 + self._ibytes)) def _read_op2_header(self): """ @@ -226,7 +226,7 @@ def _read_op2_header(self): self._fileh.read(4) # reclen frm = self._intstru % key - bytes = self._ibytes*key + bytes = self._ibytes * key self._date = struct.unpack(frm, self._fileh.read(bytes)) # self._date = np.fromfile(self._fileh, self._intstr, key) self._fileh.read(4) # endrec @@ -281,7 +281,7 @@ def _read_op2_name_trailer(self): """ eot, key = self._read_op2_end_of_table() if key == 0: - #print('return None, None, None') + # print('return None, None, None') return None, None, None reclen = self._Str4.unpack(self._fileh.read(4))[0] @@ -303,65 +303,13 @@ def _read_op2_name_trailer(self): self._skip_key(4) reclen = self._Str4.unpack(self._fileh.read(4))[0] - db_binary_name2 = self._fileh.read(reclen) - db_name2 = db_binary_name2.strip().decode('ascii') + self._fileh.read(reclen) self._fileh.read(4) # endrec self._skip_key(2) rec_type = self._get_key() return db_name, trailer, rec_type - def read_op2_matrix(self, name, trailer): - """ - Read and return Nastran op2 matrix at current file position. - - It is assumed that the name has already been read in via - :func:`_read_op2_name_trailer`. - - The size of the matrix is read from trailer: - nrows = trailer[2] - ncols = trailer[1] - """ - dtype = 1 - nrows = trailer[2] - ncols = trailer[1] - print(' %s (%s, %s)' % (name, nrows, ncols)) - matrix = np.zeros((nrows, ncols), order='F') - if self._bit64: - intsize = 8 - else: - intsize = 4 - col = 0 - frm = self._endian + '%dd' - print('frm =', frm) - while dtype > 0: # read in matrix columns - # key is number of elements in next record (row # followed - # by key-1 real numbers) - key = self._get_key() - # read column - while key > 0: - reclen = self._Str4.unpack(self._fileh.read(4))[0] - r = self._Str.unpack(self._fileh.read(self._ibytes))[0]-1 - n = (reclen - intsize) // 8 - if n < self._rowsCutoff: - matrix[r:r+n, col] = struct.unpack( - frm % n, self._fileh.read(n*8)) - else: - matrix[r:r+n, col] = np.fromfile( - self._fileh, np.float64, n) - self._fileh.read(4) # endrec - key = self._get_key() - col += 1 - self._get_key() - dtype = self._get_key() - self._read_op2_end_of_table() - if self._swap: - matrix = matrix.byteswap() - - if name in ['EFMFSMS', 'EFMASSS', 'RBMASSS']: - print(matrix) - return matrix - def skip_op2_matrix(self, trailer): """ Skip Nastran op2 matrix at current position. @@ -390,39 +338,18 @@ def skip_op2_matrix(self, trailer): def skip_op2_table(self): """Skip over Nastran output2 table.""" - eot, key = self._read_op2_end_of_table() + _, key = self._read_op2_end_of_table() if key == 0: return while key > 0: while key > 0: reclen = self._Str4.unpack(self._fileh.read(4))[0] - self._fileh.seek(8+reclen, 1) + self._fileh.seek(8 + reclen, 1) key = self._Str.unpack(self._fileh.read(self._ibytes))[0] self._fileh.read(4) # endrec self._skip_key(2) eot, key = self._read_op2_end_of_table() - def read_op2_matrices(self): - """Read all matrices from Nastran output2 file. - - Returns dictionary containing all matrices in the op2 file: - {'NAME1': matrix1, 'NAME2': matrix2, ...} - - The keys are the names as stored (upper case). - """ - self._fileh.seek(self._postheaderpos) - mats = {} - while 1: - name, trailer, rectype = self._read_op2_name_trailer() - if name is None: - break - if rectype > 0: - print("Reading matrix {}...".format(name)) - mats[name] = self.read_op2_matrix(trailer) - else: - self.skip_op2_table() - return mats - def print_data_block_directory(self): """ Prints op2 data block directory. See also :func:`directory`. @@ -432,7 +359,7 @@ def print_data_block_directory(self): for s in self.dblist: print(s) - def directory(self, verbose=True, redo=False): # TODO: _read_op2_name_trailer + def directory(self, verbose=True, redo=False): # TODO: _read_op2_name_trailer """ Return list of data block names in op2 file. @@ -497,13 +424,13 @@ def directory(self, verbose=True, redo=False): # TODO: _read_op2_name_trailer s = 'Table {0:8}'.format(name) cur = self._fileh.tell() s += (', bytes = {0:10} [{1:10} to {2:10}]'. - format(cur-pos-1, pos, cur)) + format(cur - pos - 1, pos, cur)) if size != [0, 0]: s += (', {0:6} x {1:<}'. format(size[0], size[1])) if name not in dbnames: dbnames[name] = [] - dbnames[name].append([[pos, cur], cur-pos-1, size]) + dbnames[name].append([[pos, cur], cur - pos - 1, size]) dblist.append(s) pos = cur self.dbnames = dbnames @@ -575,9 +502,9 @@ def read_op2_record(self, form=None, N=0): if n < self._rowsCutoff: b = n * bytes_per # print('frmu=%r' % frmu) - data[i:i+n] = struct.unpack(frmu % n, f.read(b)) + data[i:i + n] = struct.unpack(frmu % n, f.read(b)) else: - data[i:i+n] = np.fromfile(f, frm, n) + data[i:i + n] = np.fromfile(f, frm, n) i += n f.read(4) # endrec key = self._get_key() @@ -605,45 +532,10 @@ def skip_op2_record(self): key = self._get_key() while key > 0: reclen = self._Str4.unpack(self._fileh.read(4))[0] - self._fileh.seek(reclen+4, 1) + self._fileh.seek(reclen + 4, 1) key = self._get_key() self._skip_key(2) - def read_op2_table_headers(self, name): - """ - Read op2 table headers and echo them to the screen. - - Parameters - ---------- - name : string - Name of data block that headers are being read for. - - File must be positioned after name and trailer block. For - example, to read the table headers of the last GEOM1S data - block:: - - o2 = op2.OP2('modes.op2') - fpos = o2.dbnames['GEOM1S'][-1][0][0] - o2._fileh.seek(fpos) - name, trailer, dbtype = o2._read_op2_name_trailer() - o2.read_op2_table_headers('GEOM1S') - - """ - key = self._get_key() - print("{0} Headers:".format(name)) - Frm = struct.Struct(self._intstru % 3) - eot = 0 - while not eot: - while key > 0: - reclen = self._Str4.unpack(self._fileh.read(4))[0] - head = Frm.unpack(self._fileh.read(3*self._ibytes)) - print(np.hstack((head, reclen))) - self._fileh.seek((key-3)*self._ibytes, 1) - self._fileh.read(4) - key = self._get_key() - self._skip_key(2) - eot, key = self._read_op2_end_of_table() - def _read_op2_uset(self): """ Read the USET data block. @@ -661,18 +553,6 @@ def _read_op2_uset(self): self._read_op2_end_of_table() return uset - def _read_op2_eqexin(self): - """ - Read the EQEXIN data block. - - Returns (EQEXIN1, EQEXIN) tuple. - - See :func:`read_nas2cam_op2`. - """ - eqexin1 = self.read_op2_record() - eqexin = self.read_op2_record() - self._read_op2_end_of_table() - return eqexin1, eqexin def read_post_op2(op2_filename, verbose=False): """ @@ -684,21 +564,13 @@ def read_post_op2(op2_filename, verbose=False): Name of op2 file. verbose : bool If true, echo names of tables and matrices to screen - getougv1 : bool - If true, read the OUGV1 matrices, if any. Returns dictionary with following members ----------------------------------------- 'uset' : array - 'mats' : dictionary - Dictionary of matrices read from op2 file and indexed by the - name. The 'tload' entry is a typical entry. If `getougv1` is - true, `mats` will contain a list of all 'OUGV1' and 'BOPHIG' - matrices. """ # read op2 file: with OP2(op2_filename) as o2: - mats = {} uset = None o2._fileh.seek(o2._postheaderpos) @@ -713,10 +585,8 @@ def read_post_op2(op2_filename, verbose=False): raise RuntimeError('name=%r' % name) if dbtype > 0: if verbose: - print("Reading matrix {0}...".format(name)) - if name not in mats: - mats[name] = [] - mats[name] += [o2.read_op2_matrix(name, trailer)] + print("Skipping matrix {0}...".format(name)) + o2.skip_op2_matrix(trailer) else: if name.find('USET') == 0: if verbose: @@ -728,9 +598,4 @@ def read_post_op2(op2_filename, verbose=False): print("Skipping table %r..." % name) o2.skip_op2_table() - return {'uset': uset, - 'mats': mats,} - -#data = read_post_op2('/scratch/test/uset.op2', verbose=True) - - \ No newline at end of file + return {'uset': uset} diff --git a/loadskernel/io_functions/read_op4.py b/loadskernel/io_functions/read_op4.py index f7e7b7c..61c7116 100644 --- a/loadskernel/io_functions/read_op4.py +++ b/loadskernel/io_functions/read_op4.py @@ -1,10 +1,13 @@ +import logging +import math +import os + import numpy as np import scipy.sparse as sp -import math as math -import os, logging from loadskernel.io_functions.read_mona import nastran_number_converter + def check_bigmat(n_row): if n_row > 65535: bigmat = True @@ -12,64 +15,70 @@ def check_bigmat(n_row): # L, IROW else: bigmat = False - # Non-BIGMAT matrices have a string header IS, located in record 3, from which the length L and row position i_row are derived by + # Non-BIGMAT matrices have a string header IS, located in record 3, from which the length L and row position i_row + # are derived by # L = INT(IS/65536) - 1 # IROW = IS - 65536(L + 1) return bigmat + def read_op4_header(fid): read_string = fid.readline() """ - Note: some Nastran versions put a minus sign in front of the matrix dimension. - So far, I haven't found an explanation or its meaning in any documentation. - Until we have further information on this, I assume this to be a bug of Nastran, which is ignored but using abs(). + Note: some Nastran versions put a minus sign in front of the matrix dimension. + So far, I haven't found an explanation or its meaning in any documentation. + Until we have further information on this, I assume this to be a bug of Nastran, which is ignored but using abs(). """ n_col = abs(nastran_number_converter(read_string[0:8], 'int')) n_row = abs(nastran_number_converter(read_string[8:16], 'int')) # Assumptions: - # - real values: 16 characters for one value --> five values per line + # - real values: 16 characters for one value --> five values per line # - complex values: 16 characters for real and complex part each --> five values in two line - # According to the manual, NTYPE 1 and 3 are used for single precision while 2 and 4 are used for double precision. + # According to the manual, NTYPE 1 and 3 are used for single precision while 2 and 4 are used for double precision. # However, no change in string length has been observed. - # In addition, the format '1P,5E16.9' has 9 digits after the decimal separator, which is double precision, disregarding the information given by NTYPE. + # In addition, the format '1P,5E16.9' has 9 digits after the decimal separator, which is double precision, disregarding + # the information given by NTYPE. if nastran_number_converter(read_string[24:32], 'int') in [1, 2]: type_real = True elif nastran_number_converter(read_string[24:32], 'int') in [3, 4]: type_real = False else: - logging.error('Unknown format: ' + read_string[24:32] ) - + logging.error('Unknown format: ' + read_string[24:32]) + if nastran_number_converter(read_string[24:32], 'int') in [1, 3]: type_double = False else: type_double = True return n_col, n_row, type_real, type_double + def read_op4_column(fid, data, i_col, i_row, n_lines, n_items, type_real): # read lines of datablock row = '' - for i_line in range(n_lines): + for _ in range(n_lines): row += fid.readline()[:-1] - + for i_item in range(n_items): if type_real: data[i_col, i_row + i_item] = nastran_number_converter(row[:16], 'float') row = row[16:] else: - data[i_col, i_row + i_item] = np.complex(nastran_number_converter(row[:16], 'float'), nastran_number_converter(row[16:32], 'float')) + data[i_col, i_row + i_item] = np.complex(nastran_number_converter(row[:16], 'float'), + nastran_number_converter(row[16:32], 'float')) row = row[32:] return data + def read_op4_sparse(fid, data, n_col, n_row, type_real, type_double): bigmat = check_bigmat(n_row) - while True: + while True: # read header of data block read_string = fid.readline() # end of file reached or the last "dummy column" is reached if read_string == '' or nastran_number_converter(read_string[0:8], 'int') > n_col: break - + i_col = nastran_number_converter(read_string[0:8], 'int') - 1 n_words = nastran_number_converter(read_string[16:24], 'int') while n_words > 0: @@ -77,48 +86,50 @@ def read_op4_sparse(fid, data, n_col, n_row, type_real, type_double): read_string = fid.readline() if bigmat: L = int(nastran_number_converter(read_string[0:8], 'int') - 1) - n_words -= L+2 - i_row = nastran_number_converter(read_string[8:16], 'int') -1 + n_words -= L + 2 + i_row = nastran_number_converter(read_string[8:16], 'int') - 1 else: IS = nastran_number_converter(read_string[0:16], 'int') - L = int(IS/65536 - 1) - n_words -= L+1 - i_row = IS - 65536*(L+1) -1 + L = int(IS / 65536 - 1) + n_words -= L + 1 + i_row = IS - 65536 * (L + 1) - 1 # figure out how many lines the datablock will have - n_items = L # items that go into the column + n_items = L # items that go into the column if type_double: n_items = int(n_items / 2) if type_real: - n_lines = int(math.ceil(n_items/5.0)) + n_lines = int(math.ceil(n_items / 5.0)) else: - n_items = int(n_items / 2) - n_lines = int(math.ceil(n_items/2.5)) + n_items = int(n_items / 2) + n_lines = int(math.ceil(n_items / 2.5)) # now read the data data = read_op4_column(fid, data, i_col, i_row, n_lines, n_items, type_real) return data + def read_op4_dense(fid, data, n_col, type_real): - while True: + while True: # read header of data block read_string = fid.readline() # end of file reached or the last "dummy column" is reached if read_string == '' or nastran_number_converter(read_string[0:8], 'int') > n_col: break - + i_col = nastran_number_converter(read_string[0:8], 'int') - 1 i_row = nastran_number_converter(read_string[8:16], 'int') - 1 - + # figure out how many lines the datablock will have - n_items = nastran_number_converter(read_string[16:24], 'int') # items that go into the column + n_items = nastran_number_converter(read_string[16:24], 'int') # items that go into the column if type_real: - n_lines = int(math.ceil(n_items/5.0)) + n_lines = int(math.ceil(n_items / 5.0)) else: - n_items = int(n_items / 2) - n_lines = int(math.ceil(n_items/2.5)) + n_items = int(n_items / 2) + n_lines = int(math.ceil(n_items / 2.5)) data = read_op4_column(fid, data, i_col, i_row, n_lines, n_items, type_real) - return data - -def load_matrix(filename, sparse_output=False, sparse_format=False ): + return data + + +def load_matrix(filename, sparse_output=False, sparse_format=False): # Assumptions: # - only one matrix is give per file # - often, matrices have sparse characteristics, but might be given in Non-Sparse format. @@ -130,7 +141,7 @@ def load_matrix(filename, sparse_output=False, sparse_format=False ): # False: Non-Sparse and ASCII Format of input file filesize = float(os.stat(filename).st_size) - logging.info('Read matrix from OP4 file: %s with %.2f MB' %(filename, filesize/1024**2)) + logging.info('Read matrix from OP4 file: %s with %.2f MB' % (filename, filesize / 1024 ** 2)) with open(filename, 'r') as fid: # get information from header line @@ -139,19 +150,20 @@ def load_matrix(filename, sparse_output=False, sparse_format=False ): empty_matrix = sp.lil_matrix((n_col, n_row), dtype=float) else: empty_matrix = sp.lil_matrix((n_col, n_row), dtype=complex) - + if sparse_format: - data = read_op4_sparse(fid, empty_matrix, n_col, n_row, type_real, type_double) + data = read_op4_sparse(fid, empty_matrix, n_col, n_row, type_real, type_double) else: data = read_op4_dense(fid, empty_matrix, n_col, type_real) - + if sparse_output: - data = data.tocsc() # better sparse format than lil_matrix + data = data.tocsc() # better sparse format than lil_matrix if not sparse_output: data = data.toarray() - + order = int(np.log10(np.abs(data).max())) if not type_double and order > 10: - logging.warn('OP4 format is single precision and largest value is {} orders of magnitude above numerical precision!'.format(order-10)) - + logging.warning('OP4 format is single precision and largest value is {} orders of magnitude above numerical \ + precision!'.format(order - 10)) + return data diff --git a/loadskernel/io_functions/write_mona.py b/loadskernel/io_functions/write_mona.py index 1caffcd..21ff476 100644 --- a/loadskernel/io_functions/write_mona.py +++ b/loadskernel/io_functions/write_mona.py @@ -1,38 +1,36 @@ -''' -Created on Apr 9, 2019 +import logging -@author: voss_ar -''' import numpy as np -import logging + def number_nastran_converter(number): if np.abs(number) < 1e-8: - number = 0.0 # set tiny number to zero + number = 0.0 # set tiny number to zero if number.is_integer(): number_str = '{:> 7.1f}'.format(number) elif 0.0 <= np.log10(number.__abs__()) < 4.0: - # Here, '{:> 7.6g}' would be nicer, however, trailing zeros '.0' are removed, which leads to an integer, which Nastran doesn't like. + # Here, '{:> 7.6g}' would be nicer, however, trailing zeros '.0' are removed, which leads to an integer, which + # Nastran doesn't like. number_str = '{:> 7.6}'.format(number) elif -3.0 <= np.log10(number.__abs__()) < 0.0: number_str = '{:> 7.5f}'.format(number) else: number_str = '{:> 7.3e}'.format(number) # try normal formatting - if len(number_str)<=8: + if len(number_str) <= 8: return number_str # try to remove 2 characters, works with large numbers - elif len(number_str.replace('e+0', 'e'))<=8: + elif len(number_str.replace('e+0', 'e')) <= 8: return number_str.replace('e+0', 'e') # try smaller precicion and remove 1 character, works with small numbers - elif len('{:> 7.2e}'.format(number).replace('e-0', 'e-'))<=8: + elif len('{:> 7.2e}'.format(number).replace('e-0', 'e-')) <= 8: return '{:> 7.2e}'.format(number).replace('e-0', 'e-') - elif len('{:> 7.1e}'.format(number))<=8: + elif len('{:> 7.1e}'.format(number)) <= 8: return '{:> 7.1e}'.format(number) else: - logging.error('Could not convert number to nastran format: {}'.format(str(number)) ) + logging.error('Could not convert number to nastran format: {}'.format(str(number))) + - def write_SET1(fid, SID, entrys): entries_str = '' for entry in entrys: @@ -40,32 +38,38 @@ def write_SET1(fid, SID, entrys): if len(entries_str) <= 56: line = 'SET1 {:>8d}{:s}\n'.format(SID, entries_str) fid.write(line) - else: + else: line = 'SET1 {:>8d}{:s}+\n'.format(SID, entries_str[:56]) entries_str = entries_str[56:] fid.write(line) - + while len(entries_str) > 64: line = '+ {:s}+\n'.format(entries_str[:64]) entries_str = entries_str[64:] fid.write(line) line = '+ {:s}\n'.format(entries_str) fid.write(line) - + + def write_force_and_moment_cards(fid, grid, Pg, SID): # FORCE and MOMENT cards with all values equal to zero are ommitted to avoid problems when importing to Nastran. for i in range(grid['n']): - if np.any(np.abs(Pg[grid['set'][i,0:3]]) >= 1e-8): - line = 'FORCE ' + '{:>8d}{:>8d}{:>8d}{:>8.7s}{:>8s}{:>8s}{:>8s}\n'.format(SID, int(grid['ID'][i]), int(grid['CD'][i]), str(1.0), number_nastran_converter(Pg[grid['set'][i, 0]]), number_nastran_converter(Pg[grid['set'][i, 1]]), number_nastran_converter(Pg[grid['set'][i, 2]])) + if np.any(np.abs(Pg[grid['set'][i, 0:3]]) >= 1e-8): + line = 'FORCE ' + '{:>8d}{:>8d}{:>8d}{:>8.7s}{:>8s}{:>8s}{:>8s}\n'.format( + SID, int(grid['ID'][i]), int(grid['CD'][i]), str(1.0), number_nastran_converter(Pg[grid['set'][i, 0]]), + number_nastran_converter(Pg[grid['set'][i, 1]]), number_nastran_converter(Pg[grid['set'][i, 2]])) fid.write(line) - if np.any(np.abs(Pg[grid['set'][i,3:6]]) >= 1e-8): - line = 'MOMENT ' + '{:>8d}{:>8d}{:>8d}{:>8.7s}{:>8s}{:>8s}{:>8s}\n'.format(SID, int(grid['ID'][i]), int(grid['CD'][i]), str(1.0), number_nastran_converter(Pg[grid['set'][i, 3]]), number_nastran_converter(Pg[grid['set'][i, 4]]), number_nastran_converter(Pg[grid['set'][i, 5]])) + if np.any(np.abs(Pg[grid['set'][i, 3:6]]) >= 1e-8): + line = 'MOMENT ' + '{:>8d}{:>8d}{:>8d}{:>8.7s}{:>8s}{:>8s}{:>8s}\n'.format( + SID, int(grid['ID'][i]), int(grid['CD'][i]), str(1.0), number_nastran_converter(Pg[grid['set'][i, 3]]), + number_nastran_converter(Pg[grid['set'][i, 4]]), number_nastran_converter(Pg[grid['set'][i, 5]])) fid.write(line) - + + def write_subcases(fid, subcase, desc): line = 'SUBCASE {}\n'.format(int(subcase)) fid.write(line) line = ' SUBT={}\n'.format(str(desc)) fid.write(line) line = ' LOAD={}\n'.format(int(subcase)) - fid.write(line) \ No newline at end of file + fid.write(line) diff --git a/loadskernel/jcl_helper.py b/loadskernel/jcl_helper.py index a8f3c60..47a20fe 100644 --- a/loadskernel/jcl_helper.py +++ b/loadskernel/jcl_helper.py @@ -1,9 +1,7 @@ - -import csv, logging -from collections import OrderedDict -from numpy import array +import csv +import logging import numpy as np -from loadskernel.units import * + def csv2listofdicts(filename_csv): logging.info('Reading list of dicts from: ' + filename_csv) @@ -11,25 +9,28 @@ def csv2listofdicts(filename_csv): with open(filename_csv, 'r') as fid: csv.list_dialects() reader = csv.DictReader(fid, delimiter=',') - for row in reader: + for row in reader: tmp = {} for fieldname in reader.fieldnames: tmp[fieldname] = eval(row[fieldname]) listofdicts.append(tmp) - logging.info('Generated list of {} dicts with the following field names: {}'.format(len(listofdicts), reader.fieldnames)) + logging.info('Generated list of {} dicts with the following field names: {}'.format(len(listofdicts), reader.fieldnames)) return listofdicts - + + def repr2listofdicts(filename): with open(filename, 'r') as fid: trimcase_str = fid.read() trimcase = eval(trimcase_str) - logging.info('Generated list of {} dicts from: {}'.format(len(trimcase), filename) ) + logging.info('Generated list of {} dicts from: {}'.format(len(trimcase), filename)) return trimcase + def generate_empty_listofdicts(trimcase): - empty_listofdicts = [{}]*trimcase.__len__() - logging.info('Generated list of {} empty dicts.'.format(len(empty_listofdicts)) ) + empty_listofdicts = [{}] * len(trimcase) + logging.info('Generated list of {} empty dicts.'.format(len(empty_listofdicts))) return empty_listofdicts + def k_red_sequence(n=8, kmax=2.0): - return (1.0 - np.cos(np.linspace(0.001, np.pi/2.0, n)))*kmax \ No newline at end of file + return (1.0 - np.cos(np.linspace(0.001, np.pi / 2.0, n))) * kmax diff --git a/loadskernel/model.py b/loadskernel/model.py index fbcb330..ffe2937 100644 --- a/loadskernel/model.py +++ b/loadskernel/model.py @@ -1,41 +1,38 @@ # -*- coding: utf-8 -*- -""" -Created on Fri Nov 28 10:53:48 2014 +import numpy as np +import pandas as pd +import scipy.sparse as sp +import scipy.io +import copy +import logging +import time + +from panelaero import VLM, DLM -@author: voss_ar -""" from loadskernel.fem_interfaces import nastran_interface, nastran_f06_interface, cofe_interface, b2000_interface -import loadskernel.build_aero_functions as build_aero_functions -import loadskernel.spline_rules as spline_rules -import loadskernel.spline_functions as spline_functions -import loadskernel.build_splinegrid as build_splinegrid -import loadskernel.io_functions.read_mona as read_mona -import loadskernel.io_functions.read_op4 as read_op4 -import loadskernel.io_functions.read_bdf as read_bdf -import loadskernel.io_functions.read_cfdgrids as read_cfdgrids +from loadskernel import build_aero_functions +from loadskernel import spline_rules +from loadskernel import spline_functions +from loadskernel import build_splinegrid +from loadskernel.io_functions import read_mona, read_op4, read_bdf +from loadskernel.io_functions import read_cfdgrids from loadskernel import grid_trafo from loadskernel.atmosphere import isa as atmo_isa from loadskernel.engine_interfaces import propeller from loadskernel.fem_interfaces import fem_helper -import panelaero.VLM as VLM -import panelaero.DLM as DLM -import numpy as np -import scipy.sparse as sp -import scipy.io -import time, logging, copy -import pandas as pd +class Model(): -class Model: def __init__(self, jcl, path_output): + # store the inputs self.jcl = jcl self.path_output = path_output - - def build_model(self): # init the bdf reader self.bdf_reader = read_bdf.Reader() - # run all build function/stages + + def build_model(self): + # run all build function/stages self.build_coord() self.build_strc() self.build_strcshell() @@ -52,17 +49,16 @@ def build_model(self): delattr(self, 'bdf_reader') delattr(self, 'path_output') delattr(self, 'jcl') - - + def build_coord(self): self.coord = {'ID': [0, 9300], 'RID': [0, 0], 'dircos': [np.eye(3), np.array([[-1, 0, 0], [0, 1, 0], [0, 0, -1]])], - 'offset': [np.array([0,0,0]), np.array([0,0,0])], - } - + 'offset': [np.array([0, 0, 0]), np.array([0, 0, 0])], + } + def build_strc(self): - logging.info( 'Building structural model...') + logging.info('Building structural model...') if self.jcl.geom['method'] == 'mona': # parse given bdf files self.bdf_reader.process_deck(self.jcl.geom['filename_grid']) @@ -71,46 +67,48 @@ def build_strc(self): # build additional coordinate systems read_mona.add_CORD2R(self.bdf_reader.cards['CORD2R'], self.coord) read_mona.add_CORD1R(self.bdf_reader.cards['CORD1R'], self.coord, self.strcgrid) - + elif self.jcl.geom['method'] == 'CoFE': - with open(self.jcl.geom['filename_CoFE']) as fid: + with open(self.jcl.geom['filename_CoFE']) as fid: CoFE_data = scipy.io.loadmat(fid) n = CoFE_data['gnum'].squeeze().__len__() self.strcgrid = {'ID': CoFE_data['gnum'].squeeze(), - 'CD': np.zeros(n), # is this correct? - 'CP': np.zeros(n), # is this correct? - 'n': n, - 'set': CoFE_data['gnum2gdof'].T-1, # convert indexing from Matlab to Python + 'CD': np.zeros(n), # is this correct? + 'CP': np.zeros(n), # is this correct? + 'n': n, + # convert indexing from Matlab to Python + 'set': CoFE_data['gnum2gdof'].T - 1, 'offset': CoFE_data['gcoord'].T, } - + # make sure the strcgrid is in one common coordinate system with ID = 0 (basic system) grid_trafo.grid_trafo(self.strcgrid, self.coord, 0) - # provide a matrix PHIgg for the force and deformation transformation from the grid point coordinate system + # provide a matrix PHIgg for the force and deformation transformation from the grid point coordinate system # specified by CD into the basic system with ID = 0 - _, self.PHIgg = grid_trafo.calc_transformation_matrix(self.coord, - self.strcgrid, '', 'CP', - self.strcgrid, '', 'CD',) + _, self.PHIgg = grid_trafo.calc_transformation_matrix(self.coord, + self.strcgrid, '', 'CP', + self.strcgrid, '', 'CD',) # then set the coorinate sytems CD = 0 self.strcgrid['CD'] = np.zeros(self.strcgrid['n']) - - logging.info('The structural model consists of {} grid points ({} DoFs) and {} coordinate systems.'.format(self.strcgrid['n'], self.strcgrid['n']*6, len(self.coord['ID']) )) - + logging.info('The structural model consists of {} grid points ({} DoFs) and {} coordinate systems.'.format( + self.strcgrid['n'], self.strcgrid['n'] * 6, len(self.coord['ID']))) + def build_strcshell(self): if 'filename_shell' in self.jcl.geom and not self.jcl.geom['filename_shell'] == []: # parse given bdf files self.bdf_reader.process_deck(self.jcl.geom['filename_shell']) # assemble strcshell from CTRIA3 and CQUAD4 elements - self.strcshell = read_mona.add_shell_elements(pd.concat([self.bdf_reader.cards['CQUAD4'], self.bdf_reader.cards['CTRIA3']], ignore_index=True)) + self.strcshell = read_mona.add_shell_elements(pd.concat([self.bdf_reader.cards['CQUAD4'], + self.bdf_reader.cards['CTRIA3']], ignore_index=True)) def build_mongrid(self): if self.jcl.geom['method'] in ['mona', 'CoFE']: if 'filename_mongrid' in self.jcl.geom and not self.jcl.geom['filename_mongrid'] == '': - logging.info( 'Building Monitoring Stations from GRID data...') - self.mongrid = read_mona.Modgen_GRID(self.jcl.geom['filename_mongrid']) + logging.info('Building Monitoring Stations from GRID data...') + self.mongrid = read_mona.Modgen_GRID(self.jcl.geom['filename_mongrid']) # we dont't get names for the monstations from simple grid points, so we make up a name - self.mongrid['name'] = [ 'MON{:s}'.format(str(ID)) for ID in self.mongrid['ID'] ] + self.mongrid['name'] = ['MON{:s}'.format(str(ID)) for ID in self.mongrid['ID']] # build additional coordinate systems self.bdf_reader.process_deck(self.jcl.geom['filename_moncoord']) read_mona.add_CORD2R(self.bdf_reader.cards['CORD2R'], self.coord) @@ -118,7 +116,7 @@ def build_mongrid(self): rules = spline_rules.monstations_from_bdf(self.mongrid, self.jcl.geom['filename_monstations']) self.build_mongrid_matrices(rules) elif 'filename_monpnt' in self.jcl.geom and not self.jcl.geom['filename_monpnt'] == '': - logging.info( 'Reading Monitoring Stations from MONPNTs...') + logging.info('Reading Monitoring Stations from MONPNTs...') # parse given bdf files self.bdf_reader.process_deck(self.jcl.geom['filename_monpnt']) # assemble mongrid @@ -129,65 +127,65 @@ def build_mongrid(self): # get aecomp and sets aecomp = read_mona.add_AECOMP(self.bdf_reader.cards['AECOMP']) sets = read_mona.add_SET1(self.bdf_reader.cards['SET1']) - + rules = spline_rules.monstations_from_aecomp(self.mongrid, aecomp, sets) self.build_mongrid_matrices(rules) - else: - logging.warning( 'No Monitoring Stations are created!') + else: + logging.warning('No Monitoring Stations are created!') """ - This is an empty dummy monitoring stations, which is necessary when no monitoring stations are defined, - because monstations are expected to exist for example for the calculation of cutting forces, which are in - turn expected in the post processing. However, this procedure allows the code to run without any given + This is an empty dummy monitoring stations, which is necessary when no monitoring stations are defined, + because monstations are expected to exist for example for the calculation of cutting forces, which are in + turn expected in the post processing. However, this procedure allows the code to run without any given monitoring stations, which are not available for all models. """ - self.mongrid = {'ID':np.array([0]), + self.mongrid = {'ID': np.array([0]), 'name': ['dummy'], 'label': ['dummy'], - 'CP':np.array([0]), - 'CD':np.array([0]), - 'offset':np.array([0.0, 0.0, 0.0]), - 'set': np.arange(6).reshape((1,6)), - 'n':1, + 'CP': np.array([0]), + 'CD': np.array([0]), + 'offset': np.array([0.0, 0.0, 0.0]), + 'set': np.arange(6).reshape((1, 6)), + 'n': 1, } - self.PHIstrc_mon = np.zeros((self.strcgrid['n']*6, 6)) - + self.PHIstrc_mon = np.zeros((self.strcgrid['n'] * 6, 6)) + def build_mongrid_matrices(self, rules): PHIstrc_mon = spline_functions.spline_rb(self.mongrid, '', self.strcgrid, '', rules, self.coord, sparse_output=True) - # The line above gives the loads coordinate system 'CP' (mostly in global coordinates). + # The line above gives the loads coordinate system 'CP' (mostly in global coordinates). # Next, make sure that PHIstrc_mon maps the loads in the local coordinate system given in 'CD'. # Previously, step this has been accomplished in the post-processing. However, using matrix operations - # and incorporating the coordinate transformation in the pre-processing is much more convenient. - T_i, T_d = grid_trafo.calc_transformation_matrix(self.coord, + # and incorporating the coordinate transformation in the pre-processing is much more convenient. + T_i, T_d = grid_trafo.calc_transformation_matrix(self.coord, self.mongrid, '', 'CP', self.mongrid, '', 'CD',) self.PHIstrc_mon = T_d.T.dot(T_i).dot(PHIstrc_mon.T).T - self.mongrid_rules = rules # save rules for optional writing of MONPNT1 cards - + self.mongrid_rules = rules # save rules for optional writing of MONPNT1 cards + def build_extragrid(self): if hasattr(self.jcl, 'landinggear'): logging.info('Building extragrid from landing gear attachment points...') - self.extragrid = build_splinegrid.build_subgrid(self.strcgrid, self.jcl.landinggear['attachment_point'] ) + self.extragrid = build_splinegrid.build_subgrid(self.strcgrid, self.jcl.landinggear['attachment_point']) elif hasattr(self.jcl, 'engine'): logging.info('Building extragrid from engine attachment points...') - self.extragrid = build_splinegrid.build_subgrid(self.strcgrid, self.jcl.engine['attachment_point'] ) + self.extragrid = build_splinegrid.build_subgrid(self.strcgrid, self.jcl.engine['attachment_point']) else: return self.extragrid['set_strcgrid'] = copy.deepcopy(self.extragrid['set']) - self.extragrid['set'] = np.arange(0,6*self.extragrid['n']).reshape(-1,6) - + self.extragrid['set'] = np.arange(0, 6 * self.extragrid['n']).reshape(-1, 6) + def build_sensorgrid(self): if hasattr(self.jcl, 'sensor'): logging.info('Building sensorgrid from sensor attachment points...') - self.sensorgrid = build_splinegrid.build_subgrid(self.strcgrid, self.jcl.sensor['attachment_point'] ) + self.sensorgrid = build_splinegrid.build_subgrid(self.strcgrid, self.jcl.sensor['attachment_point']) else: return self.sensorgrid['set_strcgrid'] = copy.deepcopy(self.sensorgrid['set']) - self.sensorgrid['set'] = np.arange(0,6*self.sensorgrid['n']).reshape(-1,6) - + self.sensorgrid['set'] = np.arange(0, 6 * self.sensorgrid['n']).reshape(-1, 6) + def build_atmo(self): - logging.info( 'Building atmo model...') + logging.info('Building atmo model...') self.atmo = {} - if self.jcl.atmo['method']=='ISA': + if self.jcl.atmo['method'] == 'ISA': for i_atmo in range(len(self.jcl.atmo['key'])): p, rho, T, a = atmo_isa(self.jcl.atmo['h'][i_atmo]) self.atmo[self.jcl.atmo['key'][i_atmo]] = { @@ -196,13 +194,14 @@ def build_atmo(self): 'rho': rho, 'T': T, 'a': a, - } + } else: - logging.error( 'Unknown atmo method: ' + str(self.jcl.aero['method'])) - + logging.error('Unknown atmo method: ' + str(self.jcl.aero['method'])) + def build_aero(self): - logging.info( 'Building aero model...') - if self.jcl.aero['method'] in [ 'mona_steady', 'mona_unsteady', 'hybrid', 'nonlin_steady', 'cfd_steady', 'cfd_unsteady', 'freq_dom']: + logging.info('Building aero model...') + if self.jcl.aero['method'] in ['mona_steady', 'mona_unsteady', 'hybrid', 'nonlin_steady', + 'cfd_steady', 'cfd_unsteady', 'freq_dom']: self.build_aerogrid() self.build_aero_matrices() self.build_W2GJ() @@ -211,51 +210,52 @@ def build_aero(self): self.build_AICs_steady() self.build_AICs_unsteady() else: - logging.error( 'Unknown aero method: ' + str(self.jcl.aero['method'])) - - logging.info('The aerodynamic model consists of {} panels and {} control surfaces.'.format(self.aerogrid['n'], len(self.x2grid['ID_surf']) )) - + logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) + + logging.info('The aerodynamic model consists of {} panels and {} control surfaces.'.format( + self.aerogrid['n'], len(self.x2grid['ID_surf']))) + def build_aerogrid(self): # To avoid interference with other CQUAD4 cards parsed earlier, clear those dataframes first - if 'method_caero' in self.jcl.aero and self.jcl.aero['method_caero']== 'CQUAD4': + if 'method_caero' in self.jcl.aero and self.jcl.aero['method_caero'] == 'CQUAD4': self.bdf_reader.cards['CQUAD4'].drop(self.bdf_reader.cards['CQUAD4'].index, inplace=True) self.bdf_reader.cards['GRID'].drop(self.bdf_reader.cards['GRID'].index, inplace=True) # Then parse given bdf files self.bdf_reader.process_deck(self.jcl.aero['filename_caero_bdf']) if 'method_caero' in self.jcl.aero: - self.aerogrid = build_aero_functions.build_aerogrid(self.bdf_reader, method_caero=self.jcl.aero['method_caero']) - else: # use default method defined in function - self.aerogrid = build_aero_functions.build_aerogrid(self.bdf_reader) + self.aerogrid = build_aero_functions.build_aerogrid(self.bdf_reader, method_caero=self.jcl.aero['method_caero']) + else: # use default method defined in function + self.aerogrid = build_aero_functions.build_aerogrid(self.bdf_reader) def build_aero_matrices(self): # cast normal vector of panels into a matrix of form (n, n*6) - self.aerogrid['Nmat'] = sp.lil_matrix((self.aerogrid['n'], self.aerogrid['n']*6), dtype=float) + self.aerogrid['Nmat'] = sp.lil_matrix((self.aerogrid['n'], self.aerogrid['n'] * 6), dtype=float) for x in range(self.aerogrid['n']): - self.aerogrid['Nmat'][x,self.aerogrid['set_k'][x,(0,1,2)]] = self.aerogrid['N'][x,(0,1,2)] + self.aerogrid['Nmat'][x, self.aerogrid['set_k'][x, (0, 1, 2)]] = self.aerogrid['N'][x, (0, 1, 2)] self.aerogrid['Nmat'] = self.aerogrid['Nmat'].tocsc() # cast downwash due to rotations of panels into a matrix notation - self.aerogrid['Rmat'] = sp.lil_matrix((self.aerogrid['n']*6, self.aerogrid['n']*6), dtype=float) + self.aerogrid['Rmat'] = sp.lil_matrix((self.aerogrid['n'] * 6, self.aerogrid['n'] * 6), dtype=float) for x in range(self.aerogrid['n']): - self.aerogrid['Rmat'][x*6+1,self.aerogrid['set_k'][x,5]] = -1.0 # rotation about z-axis yields y-downwash - self.aerogrid['Rmat'][x*6+2,self.aerogrid['set_k'][x,4]] = 1.0 # rotation about y-axis yields z-downwash + self.aerogrid['Rmat'][x * 6 + 1, self.aerogrid['set_k'][x, 5]] = -1.0 # rotation about z-axis yields y-downwash + self.aerogrid['Rmat'][x * 6 + 2, self.aerogrid['set_k'][x, 4]] = 1.0 # rotation about y-axis yields z-downwash self.aerogrid['Rmat'] = self.aerogrid['Rmat'].tocsc() # cast areas of panels into matrix notation self.aerogrid['Amat'] = sp.diags(self.aerogrid['A'], format='csc') - + def build_W2GJ(self): # Correctionfor camber and twist, W2GJ if 'filename_DMI_W2GJ' in self.jcl.aero and self.jcl.aero['filename_DMI_W2GJ']: for i_file in range(len(self.jcl.aero['filename_DMI_W2GJ'])): - DMI = read_mona.Nastran_DMI(self.jcl.aero['filename_DMI_W2GJ'][i_file]) + DMI = read_mona.Nastran_DMI(self.jcl.aero['filename_DMI_W2GJ'][i_file]) if i_file == 0: data = DMI['data'].toarray().squeeze() else: data = np.hstack((data, DMI['data'].toarray().squeeze())) - self.camber_twist = {'ID':self.aerogrid['ID'], 'cam_rad':data} + self.camber_twist = {'ID': self.aerogrid['ID'], 'cam_rad': data} else: - logging.info( 'No W2GJ data (correction of camber and twist) given, setting to zero') - self.camber_twist = {'ID':self.aerogrid['ID'], 'cam_rad':np.zeros(self.aerogrid['ID'].shape)} - + logging.info('No W2GJ data (correction of camber and twist) given, setting to zero') + self.camber_twist = {'ID': self.aerogrid['ID'], 'cam_rad': np.zeros(self.aerogrid['ID'].shape)} + def build_macgrid(self): # build mac grid from geometry, except other values are given in general section of jcl self.macgrid = build_aero_functions.build_macgrid(self.aerogrid, self.jcl.general['b_ref']) @@ -267,38 +267,41 @@ def build_macgrid(self): self.macgrid['b_ref'] = self.jcl.general['b_ref'] if 'MAC_ref' in self.jcl.general: self.macgrid['offset'] = np.array([self.jcl.general['MAC_ref']]) - + rules = spline_rules.rules_aeropanel(self.aerogrid) - self.PHIjk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_j', rules, self.coord, sparse_output=True) - self.PHIlk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_l', rules, self.coord, sparse_output=True) - + self.PHIjk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_j', + rules, self.coord, sparse_output=True) + self.PHIlk = spline_functions.spline_rb(self.aerogrid, '_k', self.aerogrid, '_l', + rules, self.coord, sparse_output=True) + rules = spline_rules.rules_point(self.macgrid, self.aerogrid) - self.Dkx1 = spline_functions.spline_rb(self.macgrid, '', self.aerogrid, '_k', rules, self.coord, sparse_output=False) + self.Dkx1 = spline_functions.spline_rb(self.macgrid, '', self.aerogrid, '_k', + rules, self.coord, sparse_output=False) self.Djx1 = self.PHIjk.dot(self.Dkx1) - + def build_cs(self): # control surfaces self.bdf_reader.process_deck(self.jcl.aero['filename_aesurf'] + self.jcl.aero['filename_aelist']) - self.x2grid, self.coord = build_aero_functions.build_x2grid(self.bdf_reader, self.aerogrid, self.coord) + self.x2grid, self.coord = build_aero_functions.build_x2grid(self.bdf_reader, self.aerogrid, self.coord) self.Djx2 = [] for i_surf in range(len(self.x2grid['ID_surf'])): - - hingegrid = {'ID':np.array([self.x2grid['ID_surf'][i_surf]]), - 'offset':np.array([[0,0,0]]), - 'CD':np.array([self.x2grid['CID'][i_surf]]), - 'CP':np.array([self.x2grid['CID'][i_surf]]), - 'set':np.array([[0,1,2,3,4,5]]), + + hingegrid = {'ID': np.array([self.x2grid['ID_surf'][i_surf]]), + 'offset': np.array([[0, 0, 0]]), + 'CD': np.array([self.x2grid['CID'][i_surf]]), + 'CP': np.array([self.x2grid['CID'][i_surf]]), + 'set': np.array([[0, 1, 2, 3, 4, 5]]), + } + surfgrid = {'ID': self.x2grid[i_surf]['ID'], + 'offset_j': np.array(self.x2grid[i_surf]['offset_j']), + 'CD': self.x2grid[i_surf]['CD'], + 'CP': self.x2grid[i_surf]['CP'], + 'set_j': np.array(self.x2grid[i_surf]['set_j']), } - surfgrid = {'ID':self.x2grid[i_surf]['ID'], - 'offset_j':np.array(self.x2grid[i_surf]['offset_j']), - 'CD':self.x2grid[i_surf]['CD'], - 'CP':self.x2grid[i_surf]['CP'], - 'set_j':np.array(self.x2grid[i_surf]['set_j']), - } - dimensions = [6,6*len(self.aerogrid['ID'])] + dimensions = [6, 6 * len(self.aerogrid['ID'])] rules = spline_rules.rules_point(hingegrid, surfgrid) self.Djx2.append(spline_functions.spline_rb(hingegrid, '', surfgrid, '_j', rules, self.coord, dimensions)) - + def build_AICs_steady(self): # set-up empty data structure self.aero = {} @@ -311,28 +314,33 @@ def build_AICs_steady(self): Qjj = np.linalg.inv(np.real(Ajj)) else: Qjj = np.linalg.inv(np.real(Ajj).T) - self.aero[self.jcl.aero['key'][i_aero]]['Qjj'] = Qjj + self.aero[self.jcl.aero['key'][i_aero]]['Qjj'] = Qjj elif self.jcl.aero['method_AIC'] in ['vlm', 'dlm', 'ae']: - logging.info( 'Calculating steady AIC matrices ({} panels, k=0.0) for {} Mach number(s)...'.format( self.aerogrid['n'], len(self.jcl.aero['key']) )) + logging.info('Calculating steady AIC matrices ({} panels, k=0.0) for {} Mach number(s)...'.format( + self.aerogrid['n'], len(self.jcl.aero['key']))) t_start = time.time() - if 'xz_symmetry' in self.jcl.aero: - logging.info( ' - XZ Symmetry: {}'.format( str(self.jcl.aero['xz_symmetry']) ) ) - Qjj, Bjj = VLM.calc_Qjjs(aerogrid=copy.deepcopy(self.aerogrid), Ma=self.jcl.aero['Ma'], xz_symmetry=self.jcl.aero['xz_symmetry']) # dim: Ma,n,n - else: - Qjj, Bjj = VLM.calc_Qjjs(aerogrid=copy.deepcopy(self.aerogrid), Ma=self.jcl.aero['Ma']) # dim: Ma,n,n - Gamma_jj, Q_ind_jj = VLM.calc_Gammas(aerogrid=copy.deepcopy(self.aerogrid), Ma=self.jcl.aero['Ma']) # dim: Ma,n,n + if 'xz_symmetry' in self.jcl.aero: + logging.info( + ' - XZ Symmetry: {}'.format(str(self.jcl.aero['xz_symmetry']))) + Qjj, Bjj = VLM.calc_Qjjs(aerogrid=copy.deepcopy(self.aerogrid), Ma=self.jcl.aero['Ma'], + xz_symmetry=self.jcl.aero['xz_symmetry']) # dim: Ma,n,n + else: + Qjj, Bjj = VLM.calc_Qjjs(aerogrid=copy.deepcopy( + self.aerogrid), Ma=self.jcl.aero['Ma']) # dim: Ma,n,n + Gamma_jj, Q_ind_jj = VLM.calc_Gammas(aerogrid=copy.deepcopy(self.aerogrid), + Ma=self.jcl.aero['Ma']) # dim: Ma,n,n for key, Gamma_jj, Q_ind_jj in zip(self.jcl.aero['key'], Gamma_jj, Q_ind_jj): self.aero[key]['Gamma_jj'] = Gamma_jj self.aero[key]['Q_ind_jj'] = Q_ind_jj - + for key, Qjj, Bjj in zip(self.jcl.aero['key'], Qjj, Bjj): - self.aero[key]['Qjj'] = Qjj - self.aero[key]['Bjj'] = Bjj - - logging.info( 'done in %.2f [sec].' % (time.time() - t_start)) + self.aero[key]['Qjj'] = Qjj + self.aero[key]['Bjj'] = Bjj + + logging.info('done in %.2f [sec].' % (time.time() - t_start)) else: - logging.error( 'Unknown AIC method: ' + str(self.jcl.aero['method_AIC'])) - + logging.error('Unknown AIC method: ' + str(self.jcl.aero['method_AIC'])) + def build_AICs_unsteady(self): if self.jcl.aero['method'] in ['mona_unsteady']: if self.jcl.aero['method_AIC'] == 'dlm': @@ -342,136 +350,160 @@ def build_AICs_unsteady(self): self.build_AICs_Nastran() self.build_rfa() else: - logging.error( 'Unknown AIC method: ' + str(self.jcl.aero['method_AIC'])) + logging.error('Unknown AIC method: ' + str(self.jcl.aero['method_AIC'])) elif self.jcl.aero['method'] in ['freq_dom']: if self.jcl.aero['method_AIC'] == 'dlm': self.build_AICs_DLM() elif self.jcl.aero['method_AIC'] == 'nastran': self.build_AICs_Nastran() self.aero['n_poles'] = 0 - + def build_AICs_DLM(self): - logging.info( 'Calculating unsteady AIC matrices ({} panels, k={} (Nastran Definition!)) for {} Mach number(s)...'.format( self.aerogrid['n'], self.jcl.aero['k_red'], len(self.jcl.aero['key']) )) - if 'xz_symmetry' in self.jcl.aero: - logging.info( ' - XZ Symmetry: {}'.format( str(self.jcl.aero['xz_symmetry']) ) ) - xz_symmetry=self.jcl.aero['xz_symmetry'] + logging.info('Calculating unsteady AIC matrices ({} panels, k={} (Nastran Definition!)) \ + for {} Mach number(s)...'.format(self.aerogrid['n'], self.jcl.aero['k_red'], len(self.jcl.aero['key']))) + if 'xz_symmetry' in self.jcl.aero: + logging.info( + ' - XZ Symmetry: {}'.format(str(self.jcl.aero['xz_symmetry']))) + xz_symmetry = self.jcl.aero['xz_symmetry'] else: - xz_symmetry=False + xz_symmetry = False # Definitions for reduced frequencies: - # ae_getaic: k = omega/U + # ae_getaic: k = omega/U # Nastran: k = 0.5*cref*omega/U t_start = time.time() - Qjj = DLM.calc_Qjjs(aerogrid=copy.deepcopy(self.aerogrid), - Ma=self.jcl.aero['Ma'], - k=np.array(self.jcl.aero['k_red'])/(0.5*self.jcl.general['c_ref']), + Qjj = DLM.calc_Qjjs(aerogrid=copy.deepcopy(self.aerogrid), + Ma=self.jcl.aero['Ma'], + k=np.array(self.jcl.aero['k_red']) / (0.5 * self.jcl.general['c_ref']), xz_symmetry=xz_symmetry) for key, Qjj in zip(self.jcl.aero['key'], Qjj): self.aero[key]['Qjj_unsteady'] = Qjj - self.aero[key]['k_red'] = self.jcl.aero['k_red'] - logging.info( 'done in %.2f [sec].' % (time.time() - t_start)) - + self.aero[key]['k_red'] = self.jcl.aero['k_red'] + logging.info('done in %.2f [sec].' % (time.time() - t_start)) + def build_AICs_Nastran(self): for i_aero in range(len(self.jcl.aero['key'])): - self.aero[self.jcl.aero['key'][i_aero]]['Qjj_unsteady'] = np.zeros((len(self.jcl.aero['k_red']), self.aerogrid['n'], self.aerogrid['n'] ), dtype=complex) + self.aero[self.jcl.aero['key'][i_aero]]['Qjj_unsteady'] = np.zeros((len(self.jcl.aero['k_red']), + self.aerogrid['n'], + self.aerogrid['n']), dtype=complex) for i_k in range(len(self.jcl.aero['k_red'])): - Ajj = read_op4.load_matrix(self.jcl.aero['filename_AIC_unsteady'][i_aero][i_k], sparse_output=False, sparse_format=False) + Ajj = read_op4.load_matrix(self.jcl.aero['filename_AIC_unsteady'][i_aero][i_k], + sparse_output=False, sparse_format=False) if 'given_AIC_is_transposed' in self.jcl.aero and self.jcl.aero['given_AIC_is_transposed']: Qjj = np.linalg.inv(Ajj.T) else: Qjj = np.linalg.inv(Ajj) - self.aero[self.jcl.aero['key'][i_aero]]['Qjj_unsteady'][i_k,:,:] = Qjj - self.aero[self.jcl.aero['key'][i_aero]]['k_red'] = self.jcl.aero['k_red'] - + self.aero[self.jcl.aero['key'][i_aero]]['Qjj_unsteady'][i_k, :, :] = Qjj + self.aero[self.jcl.aero['key'][i_aero]]['k_red'] = self.jcl.aero['k_red'] + def build_rfa(self): for key in self.jcl.aero['key']: - ABCD, n_poles, betas, RMSE = build_aero_functions.rfa(Qjj = self.aero[key]['Qjj_unsteady'], - k = self.jcl.aero['k_red'], - n_poles = self.jcl.aero['n_poles'], - filename=self.path_output+'rfa_{}.png'.format(key)) + ABCD, n_poles, betas, RMSE = build_aero_functions.rfa(Qjj=self.aero[key]['Qjj_unsteady'], + k=self.jcl.aero['k_red'], + n_poles=self.jcl.aero['n_poles'], + filename=self.path_output + 'rfa_{}.png'.format(key)) self.aero[key]['ABCD'] = ABCD self.aero[key]['RMSE'] = RMSE self.aero[key]['n_poles'] = n_poles - self.aero[key]['betas'] = betas + self.aero[key]['betas'] = betas # Remove unsteady AICs to save memory. No longer critical with the HDF5 data format. del self.aero[key]['Qjj_unsteady'] def build_prop(self): if hasattr(self.jcl, 'engine'): if self.jcl.engine['method'] == 'VLM4Prop': - logging.info( 'Building VLM4Prop model...') + logging.info('Building VLM4Prop model...') self.prop = propeller.VLM4PropModel(self.jcl.engine['propeller_input_file'], self.coord, self.atmo) self.prop.build_aerogrid() self.prop.build_pacgrid() self.prop.build_AICs_steady(self.jcl.engine['Ma']) else: - logging.debug( 'Engine method {} has / needs no propulsion properties.'.format(str(self.jcl.engine['method']))) + logging.debug('Engine method {} has / needs no propulsion properties.'.format(str(self.jcl.engine['method']))) def build_splines(self): # ---------------- # ---- splines --- - # ---------------- + # ---------------- # PHIk_strc with 'nearest_neighbour', 'rbf' or 'nastran' if self.jcl.spline['method'] in ['rbf', 'nearest_neighbour']: - if 'splinegrid' in self.jcl.spline and self.jcl.spline['splinegrid'] == True: + if 'splinegrid' in self.jcl.spline and self.jcl.spline['splinegrid'] is True: # this optin is only valid if spline['method'] == 'rbf' or 'rb' - logging.info( 'Coupling aerogrid to strcgrid via splinegrid:') + logging.info('Coupling aerogrid to strcgrid via splinegrid:') self.splinegrid = build_splinegrid.build_splinegrid(self.strcgrid, self.jcl.spline['filename_splinegrid']) - #self.splinegrid = build_splinegrid.grid_thin_out_radius(self.splinegrid, 0.01) + # self.splinegrid = build_splinegrid.grid_thin_out_radius(self.splinegrid, 0.01) else: - logging.info( 'Coupling aerogrid directly. Doing cleanup/thin out of strcgrid to avoid singularities (safety first!)') + logging.info('Coupling aerogrid directly. Doing cleanup/thin out of strcgrid to avoid singularities \ + (safety first!)') self.splinegrid = build_splinegrid.grid_thin_out_radius(self.strcgrid, 0.01) else: self.splinegrid = self.strcgrid - logging.info('The spline model consists of {} grid points.'.format(self.splinegrid['n'])) + logging.info('The spline model consists of {} grid points.'.format( + self.splinegrid['n'])) - if self.jcl.spline['method'] == 'rbf': - self.PHIk_strc = spline_functions.spline_rbf(self.splinegrid, '',self.aerogrid, '_k', 'tps', dimensions=[len(self.strcgrid['ID'])*6, len(self.aerogrid['ID'])*6] ) - # rbf-spline not (yet) stable for translation of forces and moments to structure grid, so use rb-spline with nearest neighbour search instead + if self.jcl.spline['method'] == 'rbf': + self.PHIk_strc = spline_functions.spline_rbf(self.splinegrid, '', self.aerogrid, '_k', 'tps', + dimensions=[len(self.strcgrid['ID']) * 6, + len(self.aerogrid['ID']) * 6]) + # rbf-spline not (yet) stable for translation of forces and moments to structure grid, + # so use rb-spline with nearest neighbour search instead elif self.jcl.spline['method'] == 'nearest_neighbour': - self.coupling_rules = spline_rules.nearest_neighbour(self.splinegrid, '', self.aerogrid, '_k') - self.PHIk_strc = spline_functions.spline_rb(self.splinegrid, '', self.aerogrid, '_k', self.coupling_rules, self.coord, dimensions=[len(self.strcgrid['ID'])*6, len(self.aerogrid['ID'])*6], sparse_output=True) - elif self.jcl.spline['method'] == 'nastran': - self.PHIk_strc = spline_functions.spline_nastran(self.jcl.spline['filename_f06'], self.strcgrid, self.aerogrid) + self.coupling_rules = spline_rules.nearest_neighbour(self.splinegrid, '', self.aerogrid, '_k') + self.PHIk_strc = spline_functions.spline_rb(self.splinegrid, '', self.aerogrid, '_k', + self.coupling_rules, self.coord, + dimensions=[len(self.strcgrid['ID']) * 6, + len(self.aerogrid['ID']) * 6], + sparse_output=True) + elif self.jcl.spline['method'] == 'nastran': + self.PHIk_strc = spline_functions.spline_nastran( + self.jcl.spline['filename_f06'], self.strcgrid, self.aerogrid) else: - logging.error( 'Unknown spline method.') - + logging.error('Unknown spline method.') + def build_cfdgrid(self): # ------------------- # ---- mesh defo --- - # ------------------- - if self.jcl.aero['method'] in [ 'cfd_steady', 'cfd_unsteady']: + # ------------------- + if self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: cfdgrids = read_cfdgrids.ReadCfdgrids(self.jcl) cfdgrids.read_surface(merge_domains=True) cfdgrids.read_surface(merge_domains=False) - self.cfdgrid = cfdgrids.cfdgrid + self.cfdgrid = cfdgrids.cfdgrid self.cfdgrids = cfdgrids.cfdgrids - logging.info('The CFD surface grid consists of {} grid points and {} boundary markers.'.format(self.cfdgrid['n'], self.cfdgrids.__len__()) ) + logging.info('The CFD surface grid consists of {} grid points and {} boundary markers.'.format( + self.cfdgrid['n'], self.cfdgrids.__len__())) - # Option A: CFD forces are transferred to the aerogrid. + # Option A: CFD forces are transferred to the aerogrid. # This allows a direct integration into existing procedures and a comparison to VLM forces. - rules = spline_rules.nearest_neighbour(self.aerogrid, '_k', self.cfdgrid, '') - self.PHIk_cfd = spline_functions.spline_rb(self.aerogrid, '_k', self.cfdgrid, '', rules, self.coord, dimensions=[self.aerogrid['n']*6, self.cfdgrid['n']*6], sparse_output=True) - # Option B: CFD forces are directly transferred to the strcgrid. + rules = spline_rules.nearest_neighbour(self.aerogrid, '_k', self.cfdgrid, '') + self.PHIk_cfd = spline_functions.spline_rb(self.aerogrid, '_k', self.cfdgrid, '', + rules, self.coord, + dimensions=[self.aerogrid['n'] * 6, + self.cfdgrid['n'] * 6], + sparse_output=True) + # Option B: CFD forces are directly transferred to the strcgrid. # This is more physical and for example allows the application of forces on upper and lower side. # The splinegrid from above is re-used. - rules = spline_rules.nearest_neighbour(self.splinegrid, '', self.cfdgrid, '') - self.PHIcfd_strc = spline_functions.spline_rb(self.splinegrid, '', self.cfdgrid, '', rules, self.coord, dimensions=[self.strcgrid['n']*6, self.cfdgrid['n']*6], sparse_output=True) - + rules = spline_rules.nearest_neighbour(self.splinegrid, '', self.cfdgrid, '') + self.PHIcfd_strc = spline_functions.spline_rb(self.splinegrid, '', self.cfdgrid, '', + rules, self.coord, + dimensions=[self.strcgrid['n'] * 6, + self.cfdgrid['n'] * 6], + sparse_output=True) + def build_structural_dynamics(self): - logging.info( 'Building stiffness and mass model...') + logging.info('Building stiffness and mass model...') self.mass = {} if self.jcl.mass['method'] in ['mona', 'f06', 'modalanalysis', 'guyan', 'CoFE', 'B2000']: - + # select the fem interface - if self.jcl.mass['method'] in ['modalanalysis', 'guyan']: + if self.jcl.mass['method'] in ['modalanalysis', 'guyan']: fem_interface = nastran_interface.NastranInterface(self.jcl, self.strcgrid, self.coord) - elif self.jcl.mass['method'] in ['mona', 'f06']: + elif self.jcl.mass['method'] in ['mona', 'f06']: fem_interface = nastran_f06_interface.Nastranf06Interface(self.jcl, self.strcgrid, self.coord) - elif self.jcl.mass['method'] in ['B2000']: + elif self.jcl.mass['method'] in ['B2000']: fem_interface = b2000_interface.B2000Interface(self.jcl, self.strcgrid, self.coord) elif self.jcl.mass['method'] in ['CoFE']: fem_interface = cofe_interface.CoFEInterface(self.jcl, self.strcgrid, self.coord) - + # the stiffness matrix is needed for all methods / fem interfaces fem_interface.get_stiffness_matrix() # the stiffness matrix is needed for the modal discplacement method @@ -479,31 +511,32 @@ def build_structural_dynamics(self): # Check if matrix is symmetric if not fem_helper.check_matrix_symmetry(self.KGG): logging.warning('Stiffness matrix Kgg is NOT symmetric.') - - # do further processing of the stiffness matrix + + # do further processing of the stiffness matrix if self.jcl.mass['method'] in ['modalanalysis', 'guyan', 'CoFE', 'B2000']: fem_interface.get_dofs() fem_interface.prepare_stiffness_matrices() if self.jcl.mass['method'] in ['guyan']: fem_interface.get_aset(self.bdf_reader) fem_interface.prepare_stiffness_matrices_for_guyan() - + # loop over mass configurations for i_mass in range(len(self.jcl.mass['key'])): self.build_mass_matrices(fem_interface, i_mass) self.build_translation_matrices(self.jcl.mass['key'][i_mass]) else: - logging.error( 'Unknown mass method: ' + str(self.jcl.mass['method'])) - + logging.error('Unknown mass method: ' + str(self.jcl.mass['method'])) + def build_mass_matrices(self, fem_interface, i_mass): - logging.info( 'Mass configuration {} of {}: {} '.format(i_mass+1, len(self.jcl.mass['key']), self.jcl.mass['key'][i_mass])) - + logging.info('Mass configuration {} of {}: {} '.format(i_mass + 1, len(self.jcl.mass['key']), + self.jcl.mass['key'][i_mass])) + # the mass matrix is needed for all methods / fem interfaces MGG = fem_interface.get_mass_matrix(i_mass) # Check if matrix is symmetric if not fem_helper.check_matrix_symmetry(MGG): logging.warning('Mass matrix Mgg is NOT symmetric.') - + # getting the eigenvalues and -vectors depends on the method / fem solver if self.jcl.mass['method'] in ['modalanalysis', 'guyan', 'CoFE', 'B2000']: Mb, cggrid, cggrid_norm = fem_interface.calc_cg() @@ -515,7 +548,7 @@ def build_mass_matrices(self, fem_interface, i_mass): elif self.jcl.mass['method'] in ['mona', 'f06']: Mb, cggrid, cggrid_norm = fem_interface.cg_from_SOL103() fem_interface.modes_from_SOL103() - + # calculate all generalized matrices Mff, Kff, Dff, PHIf_strc, Mhh, Khh, Dhh, PHIh_strc = fem_interface.calc_modal_matrices() # apply coodinate transformation so that all deformations are given in the basic coordinate system @@ -536,55 +569,55 @@ def build_mass_matrices(self, fem_interface, i_mass): 'Khh': Khh, 'Dhh': Dhh, 'n_modes': len(self.jcl.mass['modes'][i_mass]), - } - + } + def build_translation_matrices(self, key): """ - In this function, we do a lot of splining. Depending on the intended solution sequence, - different matrices are required. + In this function, we do a lot of splining. Depending on the intended solution sequence, + different matrices are required. """ - - cggrid = self.mass[key]['cggrid'] - cggrid_norm = self.mass[key]['cggrid_norm'] - MGG = self.mass[key]['MGG'] - PHIf_strc = self.mass[key]['PHIf_strc'] - PHIh_strc = self.mass[key]['PHIh_strc'] - + + cggrid = self.mass[key]['cggrid'] + cggrid_norm = self.mass[key]['cggrid_norm'] + MGG = self.mass[key]['MGG'] + PHIf_strc = self.mass[key]['PHIf_strc'] + PHIh_strc = self.mass[key]['PHIh_strc'] + rules = spline_rules.rules_point(cggrid, self.strcgrid) PHIstrc_cg = spline_functions.spline_rb(cggrid, '', self.strcgrid, '', rules, self.coord) - - if self.jcl.aero['method'] in [ 'cfd_steady', 'cfd_unsteady']: + + if self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: rules = spline_rules.rules_point(cggrid, self.cfdgrid) PHIcfd_cg = spline_functions.spline_rb(cggrid, '', self.cfdgrid, '', rules, self.coord) # some pre-multiplications to speed-up main processing PHIcfd_f = self.PHIcfd_strc.dot(PHIf_strc.T) self.mass[key]['PHIcfd_cg'] = PHIcfd_cg - self.mass[key]['PHIcfd_f'] = PHIcfd_f - + self.mass[key]['PHIcfd_f'] = PHIcfd_f + if hasattr(self, 'extragrid'): rules = spline_rules.rules_point(cggrid, self.extragrid) PHIextra_cg = spline_functions.spline_rb(cggrid, '', self.extragrid, '', rules, self.coord) - PHIf_extra = PHIf_strc[:,self.extragrid['set_strcgrid'].reshape(1,-1)[0]] + PHIf_extra = PHIf_strc[:, self.extragrid['set_strcgrid'].reshape(1, -1)[0]] self.mass[key]['PHIextra_cg'] = PHIextra_cg - self.mass[key]['PHIf_extra'] = PHIf_extra - + self.mass[key]['PHIf_extra'] = PHIf_extra + if hasattr(self, 'sensorgrid'): rules = spline_rules.rules_point(cggrid, self.sensorgrid) PHIsensor_cg = spline_functions.spline_rb(cggrid, '', self.sensorgrid, '', rules, self.coord) - PHIf_sensor = PHIf_strc[:,self.sensorgrid['set_strcgrid'].reshape(1,-1)[0]] + PHIf_sensor = PHIf_strc[:, self.sensorgrid['set_strcgrid'].reshape(1, -1)[0]] self.mass[key]['PHIsensor_cg'] = PHIsensor_cg - self.mass[key]['PHIf_sensor'] = PHIf_sensor + self.mass[key]['PHIf_sensor'] = PHIf_sensor rules = spline_rules.rules_point(cggrid, self.macgrid) - PHImac_cg = spline_functions.spline_rb(cggrid, '', self.macgrid, '', rules, self.coord) + PHImac_cg = spline_functions.spline_rb(cggrid, '', self.macgrid, '', rules, self.coord) rules = spline_rules.rules_point(self.macgrid, cggrid) - PHIcg_mac = spline_functions.spline_rb( self.macgrid, '', cggrid, '', rules, self.coord) - + PHIcg_mac = spline_functions.spline_rb(self.macgrid, '', cggrid, '', rules, self.coord) + rules = spline_rules.rules_point(cggrid, cggrid_norm) - PHInorm_cg = spline_functions.spline_rb(cggrid, '', cggrid_norm, '', rules, self.coord) + PHInorm_cg = spline_functions.spline_rb(cggrid, '', cggrid_norm, '', rules, self.coord) rules = spline_rules.rules_point(cggrid_norm, cggrid) - PHIcg_norm = spline_functions.spline_rb(cggrid_norm, '', cggrid, '', rules, self.coord) - + PHIcg_norm = spline_functions.spline_rb(cggrid_norm, '', cggrid, '', rules, self.coord) + # some pre-multiplications to speed-up main processing PHIjf = self.PHIjk.dot(self.PHIk_strc.dot(PHIf_strc.T)) PHIlf = self.PHIlk.dot(self.PHIk_strc.dot(PHIf_strc.T)) @@ -593,7 +626,7 @@ def build_translation_matrices(self, key): PHIlh = self.PHIlk.dot(self.PHIk_strc.dot(PHIh_strc.T)) PHIkh = self.PHIk_strc.dot(PHIh_strc.T) - Mfcg=PHIf_strc.dot(-MGG.dot(PHIstrc_cg)) + Mfcg = PHIf_strc.dot(-MGG.dot(PHIstrc_cg)) # save all matrices to data structure self.mass[key]['Mfcg'] = Mfcg @@ -608,4 +641,3 @@ def build_translation_matrices(self, key): self.mass[key]['PHIjh'] = PHIjh self.mass[key]['PHIlh'] = PHIlh self.mass[key]['PHIkh'] = PHIkh - diff --git a/loadskernel/plotting_extra.py b/loadskernel/plotting_extra.py index 985c797..c530ce0 100755 --- a/loadskernel/plotting_extra.py +++ b/loadskernel/plotting_extra.py @@ -1,40 +1,40 @@ # -*- coding: utf-8 -*- -""" -Created on Tue Oct 27 11:26:27 2015 - -@author: voss_ar -""" +import logging +import os import numpy as np -import os, logging, pickle -from matplotlib import pyplot as plt -plt.rcParams.update({'font.size': 16, - 'svg.fonttype':'none'}) -from matplotlib import animation + +from matplotlib import pyplot as plt + try: from mayavi import mlab from tvtk.api import tvtk -except: +except ImportError: pass from loadskernel import plotting_standard +plt.rcParams.update({'font.size': 16, + 'svg.fonttype': 'none'}) + + class DetailedPlots(plotting_standard.LoadPlots): - + def plot_aerogrid(self, scalars=None, colormap='plasma', value_min=None, value_max=None): - # create the unstructured grid - points = self.aerogrid['cornerpoint_grids'][:,(1,2,3)] + # create the unstructured grid + points = self.aerogrid['cornerpoint_grids'][:, (1, 2, 3)] ug = tvtk.UnstructuredGrid(points=points) shells = [] - for shell in self.aerogrid['cornerpoint_panels']: - shells.append([np.where(self.aerogrid['cornerpoint_grids'][:,0]==id)[0][0] for id in shell]) + for shell in self.aerogrid['cornerpoint_panels']: + shells.append([np.where(self.aerogrid['cornerpoint_grids'][:, 0] == id)[ + 0][0] for id in shell]) shell_type = tvtk.Polygon().cell_type ug.set_cells(shell_type, shells) ug.cell_data.scalars = scalars - + # hand over unstructured grid to mayavi - fig = mlab.figure(bgcolor=(1,1,1)) + mlab.figure(bgcolor=(1, 1, 1)) src_aerogrid = mlab.pipeline.add_dataset(ug) - + # determine if suitable scalar data is given if scalars is not None: # determine an upper and lower limit of the colorbar, if not given @@ -42,109 +42,110 @@ def plot_aerogrid(self, scalars=None, colormap='plasma', value_min=None, value_m value_min = scalars.min() if value_max is None: value_max = scalars.max() - surface = mlab.pipeline.surface(src_aerogrid, opacity=1.0, line_width=0.5, colormap=colormap, vmin=value_min, vmax=value_max) - surface.actor.mapper.scalar_visibility=True - - surface.module_manager.scalar_lut_manager.show_legend=True + surface = mlab.pipeline.surface(src_aerogrid, opacity=1.0, line_width=0.5, + colormap=colormap, vmin=value_min, vmax=value_max) + surface.actor.mapper.scalar_visibility = True + + surface.module_manager.scalar_lut_manager.show_legend = True surface.module_manager.scalar_lut_manager.data_name = 'dCp' - surface.module_manager.scalar_lut_manager.label_text_property.color=(0,0,0) - surface.module_manager.scalar_lut_manager.label_text_property.font_family='times' - surface.module_manager.scalar_lut_manager.label_text_property.bold=False - surface.module_manager.scalar_lut_manager.label_text_property.italic=False - surface.module_manager.scalar_lut_manager.title_text_property.color=(0,0,0) - surface.module_manager.scalar_lut_manager.title_text_property.font_family='times' - surface.module_manager.scalar_lut_manager.title_text_property.bold=False - surface.module_manager.scalar_lut_manager.title_text_property.italic=False - surface.module_manager.scalar_lut_manager.number_of_labels=5 + surface.module_manager.scalar_lut_manager.label_text_property.color = (0, 0, 0) + surface.module_manager.scalar_lut_manager.label_text_property.font_family = 'times' + surface.module_manager.scalar_lut_manager.label_text_property.bold = False + surface.module_manager.scalar_lut_manager.label_text_property.italic = False + surface.module_manager.scalar_lut_manager.title_text_property.color = (0, 0, 0) + surface.module_manager.scalar_lut_manager.title_text_property.font_family = 'times' + surface.module_manager.scalar_lut_manager.title_text_property.bold = False + surface.module_manager.scalar_lut_manager.title_text_property.italic = False + surface.module_manager.scalar_lut_manager.number_of_labels = 5 else: surface = mlab.pipeline.surface(src_aerogrid, opacity=1.0, line_width=0.5) - surface.actor.mapper.scalar_visibility=False - surface.actor.property.edge_visibility=True + surface.actor.mapper.scalar_visibility = False + surface.actor.property.edge_visibility = True mlab.show() def plot_pressure_distribution(self): for response in self.responses: - trimcase = self.jcl.trimcase[response['i'][()]] + trimcase = self.jcl.trimcase[response['i'][()]] logging.info('interactive plotting of resulting pressure distributions for trim {:s}'.format(trimcase['desc'])) - Pk = response['Pk_aero'] #response['Pk_rbm'] + response['Pk_cam'] + Pk = response['Pk_aero'] # response['Pk_rbm'] + response['Pk_cam'] rho = self.model['atmo'][trimcase['altitude']]['rho'] Vtas = trimcase['Ma'] * self.model['atmo'][trimcase['altitude']]['a'] - F = Pk[0,self.aerogrid['set_k'][:,2]] # * -1.0 - cp = F / (rho/2.0*Vtas**2) / self.aerogrid['A'] + F = Pk[0, self.aerogrid['set_k'][:, 2]] # * -1.0 + cp = F / (rho / 2.0 * Vtas ** 2) / self.aerogrid['A'] self.plot_aerogrid(cp) - + def plot_time_data(self): # Create all plots - fig1, (ax11, ax12) = plt.subplots(nrows=2, ncols=1, sharex=True,) - fig2, (ax21, ax22, ax23) = plt.subplots(nrows=3, ncols=1, sharex=True,) - fig3, (ax31, ax32) = plt.subplots(nrows=2, ncols=1, sharex=True,) - fig4, (ax41, ax42) = plt.subplots(nrows=2, ncols=1, sharex=True,) - fig5, (ax51, ax52, ax53) = plt.subplots(nrows=3, ncols=1, sharex=True,) - fig6, (ax61, ax62) = plt.subplots(nrows=2, ncols=1, sharex=True,) + _, (ax11, ax12) = plt.subplots(nrows=2, ncols=1, sharex=True,) + _, (ax21, ax22, ax23) = plt.subplots(nrows=3, ncols=1, sharex=True,) + _, (ax31, ax32) = plt.subplots(nrows=2, ncols=1, sharex=True,) + _, (ax41, ax42) = plt.subplots(nrows=2, ncols=1, sharex=True,) + _, (ax51, ax52, ax53) = plt.subplots(nrows=3, ncols=1, sharex=True,) + _, (ax61, ax62) = plt.subplots(nrows=2, ncols=1, sharex=True,) if hasattr(self.jcl, 'landinggear'): - fig7, (ax71, ax72) = plt.subplots(nrows=2, ncols=1, sharex=True,) + _, (ax71, ax72) = plt.subplots(nrows=2, ncols=1, sharex=True,) Dkx1 = self.model['Dkx1'][()] # Loop over responses and fill plots with data for response in self.responses: - trimcase = self.jcl.trimcase[response['i'][()]] + trimcase = self.jcl.trimcase[response['i'][()]] logging.info('plotting for simulation {:s}'.format(trimcase['desc'])) self.n_modes = self.model['mass'][trimcase['mass']]['n_modes'][()] - - Cl = response['Pmac'][:,2] / response['q_dyn'][:].T / self.jcl.general['A_ref'] - ax11.plot(response['t'], response['Pmac'][:,2], 'b-') + + Cl = response['Pmac'][:, 2] / response['q_dyn'][:].T / self.jcl.general['A_ref'] + ax11.plot(response['t'], response['Pmac'][:, 2], 'b-') ax12.plot(response['t'], Cl.T, 'b-') - + if self.jcl.aero['method'] in ['mona_unsteady']: Pb_gust = [] Pb_unsteady = [] - for i_step in range(len(response['t'])): - Pb_gust.append(np.dot(Dkx1.T, response['Pk_gust'][i_step,:])[2]) - Pb_unsteady.append(np.dot(Dkx1.T, response['Pk_unsteady'][i_step,:])[2]) + for i_step in range(len(response['t'])): + Pb_gust.append(np.dot(Dkx1.T, response['Pk_gust'][i_step, :])[2]) + Pb_unsteady.append(np.dot(Dkx1.T, response['Pk_unsteady'][i_step, :])[2]) ax11.plot(response['t'], Pb_gust, 'k-') ax11.plot(response['t'], Pb_unsteady, 'r-') ax21.plot(response['t'], response['q_dyn'], 'k-') - ax22.plot(response['t'], response['alpha'][:]/np.pi*180.0, 'r-') - ax22.plot(response['t'], response['beta'][:]/np.pi*180.0, 'c-') - ax23.plot(response['t'], response['Nxyz'][:,1], 'g-') - ax23.plot(response['t'], response['Nxyz'][:,2], 'b-') + ax22.plot(response['t'], response['alpha'][:] / np.pi * 180.0, 'r-') + ax22.plot(response['t'], response['beta'][:] / np.pi * 180.0, 'c-') + ax23.plot(response['t'], response['Nxyz'][:, 1], 'g-') + ax23.plot(response['t'], response['Nxyz'][:, 2], 'b-') + + ax32.plot(response['t'], response['X'][:, 3] / np.pi * 180.0, 'b-') + ax32.plot(response['t'], response['X'][:, 4] / np.pi * 180.0, 'g-') + ax32.plot(response['t'], response['X'][:, 5] / np.pi * 180.0, 'r-') - ax31.plot(response['t'], response['X'][:,0], 'b-') - ax31.plot(response['t'], response['X'][:,1], 'g-') - ax31.plot(response['t'], response['X'][:,2], 'r-') + ax41.plot(response['t'], response['X'][:, 6], 'b-') + ax41.plot(response['t'], response['X'][:, 7], 'g-') + ax41.plot(response['t'], response['X'][:, 8], 'r-') - ax32.plot(response['t'], response['X'][:,3]/np.pi*180.0, 'b-') - ax32.plot(response['t'], response['X'][:,4]/np.pi*180.0, 'g-') - ax32.plot(response['t'], response['X'][:,5]/np.pi*180.0, 'r-') + ax41.plot(response['t'], response['X'][:, 6], 'b-') + ax41.plot(response['t'], response['X'][:, 7], 'g-') + ax41.plot(response['t'], response['X'][:, 8], 'r-') - ax41.plot(response['t'], response['X'][:,6], 'b-') - ax41.plot(response['t'], response['X'][:,7], 'g-') - ax41.plot(response['t'], response['X'][:,8], 'r-') + ax42.plot(response['t'], response['X'][:, 9] / np.pi * 180.0, 'b-') + ax42.plot(response['t'], response['X'][:, 10] / np.pi * 180.0, 'g-') + ax42.plot(response['t'], response['X'][:, 11] / np.pi * 180.0, 'r-') - ax42.plot(response['t'], response['X'][:,9]/np.pi*180.0, 'b-') - ax42.plot(response['t'], response['X'][:,10]/np.pi*180.0, 'g-') - ax42.plot(response['t'], response['X'][:,11]/np.pi*180.0, 'r-') + ax51.plot(response['t'], response['X'][:, 12 + 2 * self.n_modes + 0] / np.pi * 180.0, 'b-') + ax51.plot(response['t'], response['X'][:, 12 + 2 * self.n_modes + 1] / np.pi * 180.0, 'g-') + ax51.plot(response['t'], response['X'][:, 12 + 2 * self.n_modes + 2] / np.pi * 180.0, 'r-') - ax51.plot(response['t'], response['X'][:,12+2*self.n_modes+0]/np.pi*180.0, 'b-') - ax51.plot(response['t'], response['X'][:,12+2*self.n_modes+1]/np.pi*180.0, 'g-') - ax51.plot(response['t'], response['X'][:,12+2*self.n_modes+2]/np.pi*180.0, 'r-') + ax52.plot(response['t'], response['X'][:, 12 + 2 * self.n_modes + 3], 'k-') - ax52.plot(response['t'], response['X'][:,12+2*self.n_modes+3], 'k-') - - ax53.plot(response['t'], response['X'][:,12+2*self.n_modes+4], 'b-') - ax53.plot(response['t'], response['X'][:,12+2*self.n_modes+5], 'g-') + ax53.plot(response['t'], response['X'][:, 12 + 2 * self.n_modes + 4], 'b-') + ax53.plot(response['t'], response['X'][:, 12 + 2 * self.n_modes + 5], 'g-') ax61.plot(response['t'], response['Uf'], 'b-') - ax62.plot(response['t'], response['d2Ucg_dt2'][:,0], 'b-') - ax62.plot(response['t'], response['d2Ucg_dt2'][:,1], 'g-') - ax62.plot(response['t'], response['d2Ucg_dt2'][:,2], 'r-') - + ax62.plot(response['t'], response['d2Ucg_dt2'][:, 0], 'b-') + ax62.plot(response['t'], response['d2Ucg_dt2'][:, 1], 'g-') + ax62.plot(response['t'], response['d2Ucg_dt2'][:, 2], 'r-') + if hasattr(self.jcl, 'landinggear'): ax71.plot(response['t'], response['p1']) ax72.plot(response['t'], response['F1']) - + # Make plots nice ax11.set_ylabel('Fz [N]') ax11.grid(True) @@ -154,7 +155,7 @@ def plot_time_data(self): ax12.set_ylabel('Cz [-]') ax12.grid(True) ax12.legend(['Cz']) - + ax21.set_ylabel('[Pa]') ax21.grid(True) ax21.legend(['q_dyn']) @@ -165,7 +166,7 @@ def plot_time_data(self): ax23.legend(['Ny', 'Nz']) ax23.grid(True) ax23.set_ylabel('[-]') - + ax31.set_ylabel('[m]') ax31.grid(True) ax31.legend(['x', 'y', 'z']) @@ -173,7 +174,7 @@ def plot_time_data(self): ax32.set_ylabel('[deg]') ax32.grid(True) ax32.legend(['phi', 'theta', 'psi']) - + ax41.set_ylabel('[m/s]') ax41.grid(True) ax41.legend(['u', 'v', 'w']) @@ -181,7 +182,7 @@ def plot_time_data(self): ax42.set_ylabel('[deg/s]') ax42.grid(True) ax42.legend(['p', 'q', 'r']) - + ax51.set_ylabel('Inputs [deg]') ax51.grid(True) ax51.legend(['Xi', 'Eta', 'Zeta']) @@ -192,14 +193,14 @@ def plot_time_data(self): ax53.set_ylabel('Inputs [deg]') ax53.grid(True) ax53.legend(['stabilizer', 'flap setting']) - + ax61.set_ylabel('Uf') ax61.grid(True) ax62.set_xlabel('t [sec]') ax62.set_ylabel('d2Ucg_dt2 [m/s^2]') ax62.legend(['du', 'dv', 'dw']) ax62.grid(True) - + if hasattr(self.jcl, 'landinggear'): ax71.legend(self.jcl.landinggear['key'], loc='best') ax71.set_ylabel('p1 [m]') @@ -208,170 +209,183 @@ def plot_time_data(self): ax72.set_xlabel('t [s]') ax72.set_ylabel('F1 [N]') ax72.grid(True) - + # Show plots plt.show() -class Animations(plotting_standard.LoadPlots): - + +class Animations(plotting_standard.LoadPlots): + def make_movie(self, path_output, speedup_factor=1.0): for response in self.responses: self.plot_time_animation_3d(response, path_output, speedup_factor=speedup_factor, make_movie=True) - + def make_animation(self, speedup_factor=1.0): for response in self.responses: self.plot_time_animation_3d(response, speedup_factor=speedup_factor) - + def plot_time_animation_3d(self, response, path_output='./', speedup_factor=1.0, make_movie=False): - from mayavi import mlab - from tvtk.api import tvtk - trimcase = self.jcl.trimcase[response['i'][()]] - simcase = self.jcl.simcase[response['i'][()]] - + trimcase = self.jcl.trimcase[response['i'][()]] + simcase = self.jcl.simcase[response['i'][()]] + def update_timestep(self, i): self.fig.scene.disable_render = True points_i = np.array([self.x[i], self.y[i], self.z[i]]).T - scalars_i = self.color_scalar[i,:] + scalars_i = self.color_scalar[i, :] update_strc_display(self, points_i, scalars_i) update_text_display(self, response['t'][i][0]) for ug_vector, ug_cone, data in zip(self.ug_vectors, self.ug_cones, self.vector_data): - vector_data_i = np.vstack((data['u'][i,:], data['v'][i,:], data['w'][i,:])).T + vector_data_i = np.vstack((data['u'][i, :], data['v'][i, :], data['w'][i, :])).T update_vector_display(self, ug_vector, ug_cone, points_i, vector_data_i) # get current view and set new focal point v = mlab.view() r = mlab.roll() - mlab.view(azimuth=v[0], elevation=v[1], roll=r, distance=v[2], focalpoint=points_i.mean(axis=0)) # view from right and above + # view from right and above + mlab.view(azimuth=v[0], elevation=v[1], roll=r, + distance=v[2], focalpoint=points_i.mean(axis=0)) self.fig.scene.disable_render = False - - @mlab.animate(delay=int(simcase['dt']*1000.0/speedup_factor), ui=True) + + @mlab.animate(delay=int(simcase['dt'] * 1000.0 / speedup_factor), ui=True) def anim(self): # internal function that actually updates the animation while True: for i in range(len(response['t'])): update_timestep(self, i) yield - + def movie(self): # internal function that actually updates the animation for i in range(len(response['t'])): update_timestep(self, i) self.fig.scene.render() - self.fig.scene.save_png('{}anim/subcase_{}_frame_{:06d}.png'.format(path_output, trimcase['subcase'], i)) + self.fig.scene.save_png( + '{}anim/subcase_{}_frame_{:06d}.png'.format(path_output, trimcase['subcase'], i)) self.vector_data = [] + def calc_vector_data(self, grid, set='', name='Pg_aero_global', exponent=0.33): Pg = response[name][:] # scaling to enhance small vectors - uvw_t0 = np.linalg.norm(Pg[:,grid['set'+set][:,(0,1,2)]], axis=2) - f_e = uvw_t0**exponent + uvw_t0 = np.linalg.norm(Pg[:, grid['set' + set][:, (0, 1, 2)]], axis=2) + f_e = uvw_t0 ** exponent # apply scaling to Pg - u = Pg[:,grid['set'+set][:,0]] / uvw_t0 * f_e - v = Pg[:,grid['set'+set][:,1]] / uvw_t0 * f_e - w = Pg[:,grid['set'+set][:,2]] / uvw_t0 * f_e + u = Pg[:, grid['set' + set][:, 0]] / uvw_t0 * f_e + v = Pg[:, grid['set' + set][:, 1]] / uvw_t0 * f_e + w = Pg[:, grid['set' + set][:, 2]] / uvw_t0 * f_e # guard for NaNs due to pervious division by uvw u[np.isnan(u)] = 0.0 v[np.isnan(v)] = 0.0 w[np.isnan(w)] = 0.0 # maximale Ist-Laenge eines Vektors - r_max = np.max((u**2.0 + v**2.0 + w**2.0)**0.5) + r_max = np.max((u ** 2.0 + v ** 2.0 + w ** 2.0) ** 0.5) # maximale Soll-Laenge eines Vektors, abgeleitet von der Ausdehnung des Modells - r_scale = 0.5*np.max([grid['offset'+set][:,0].max() - grid['offset'+set][:,0].min(), grid['offset'+set][:,1].max() - grid['offset'+set][:,1].min(), grid['offset'+set][:,2].max() - grid['offset'+set][:,2].min()]) + r_scale = 0.5 * np.max([grid['offset' + set][:, 0].max() - grid['offset' + set][:, 0].min(), + grid['offset' + set][:, 1].max() - grid['offset' + set][:, 1].min(), + grid['offset' + set][:, 2].max() - grid['offset' + set][:, 2].min()]) # skalieren der Vektoren u = u / r_max * r_scale v = v / r_max * r_scale w = w / r_max * r_scale # store - self.vector_data.append({'u':u, 'v':v, 'w':w }) - + self.vector_data.append({'u': u, 'v': v, 'w': w}) + self.ug_vectors = [] - self.ug_cones = [] - def setup_vector_display(self, vector_data, color=(1,0,0), opacity=0.4): - # vectors - ug_vector = tvtk.UnstructuredGrid(points=np.vstack((self.x[0,:], self.y[0,:], self.z[0,:])).T) - ug_vector.point_data.vectors = np.vstack((vector_data['u'][0,:], vector_data['v'][0,:], vector_data['w'][0,:])).T + self.ug_cones = [] + + def setup_vector_display(self, vector_data, color=(1, 0, 0), opacity=0.4): + # vectors + ug_vector = tvtk.UnstructuredGrid(points=np.vstack((self.x[0, :], self.y[0, :], self.z[0, :])).T) + ug_vector.point_data.vectors = np.vstack((vector_data['u'][0, :], + vector_data['v'][0, :], + vector_data['w'][0, :])).T src_vector = mlab.pipeline.add_dataset(ug_vector) - vector = mlab.pipeline.vectors(src_vector, color=color, mode='2ddash', opacity=opacity, scale_mode='vector', scale_factor=1.0) - vector.glyph.glyph.clamping=False + vector = mlab.pipeline.vectors(src_vector, color=color, mode='2ddash', opacity=opacity, + scale_mode='vector', scale_factor=1.0) + vector.glyph.glyph.clamping = False self.ug_vectors.append(ug_vector) # cones for vectors - ug_cone = tvtk.UnstructuredGrid(points=np.vstack((self.x[0,:]+vector_data['u'][0,:], self.y[0,:]+vector_data['v'][0,:], self.z[0,:]+vector_data['w'][0,:])).T) - ug_cone.point_data.vectors = np.vstack((vector_data['u'][0,:], vector_data['v'][0,:], vector_data['w'][0,:])).T + ug_cone = tvtk.UnstructuredGrid(points=np.vstack((self.x[0, :] + vector_data['u'][0, :], + self.y[0, :] + vector_data['v'][0, :], + self.z[0, :] + vector_data['w'][0, :])).T) + ug_cone.point_data.vectors = np.vstack( + (vector_data['u'][0, :], vector_data['v'][0, :], vector_data['w'][0, :])).T src_cone = mlab.pipeline.add_dataset(ug_cone) - cone = mlab.pipeline.vectors(src_cone, color=color, mode='cone', opacity=opacity, scale_mode='vector', scale_factor=0.1, resolution=16) - cone.glyph.glyph.clamping=False + cone = mlab.pipeline.vectors(src_cone, color=color, mode='cone', opacity=opacity, scale_mode='vector', + scale_factor=0.1, resolution=16) + cone.glyph.glyph.clamping = False self.ug_cones.append(ug_cone) - + def update_vector_display(self, ug_vector, ug_cone, points, vector): ug_vector.points.from_array(points) ug_vector.point_data.vectors.from_array(vector) ug_vector.modified() - ug_cone.points.from_array(points+vector) + ug_cone.points.from_array(points + vector) ug_cone.point_data.vectors.from_array(vector) ug_cone.modified() - - def setup_strc_display(self, color=(1,1,1)): - points = np.vstack((self.x[0,:], self.y[0,:], self.z[0,:])).T - scalars = self.color_scalar[0,:] + + def setup_strc_display(self, color=(1, 1, 1)): + points = np.vstack((self.x[0, :], self.y[0, :], self.z[0, :])).T + scalars = self.color_scalar[0, :] self.strc_ug = tvtk.UnstructuredGrid(points=points) self.strc_ug.point_data.scalars = scalars if hasattr(self.model, 'strcshell'): # plot shell as surface shells = [] - for shell in self.strcshell['cornerpoints']: - shells.append([np.where(self.strcgrid['ID']==id)[0][0] for id in shell(np.isfinite(shell))]) + for shell in self.strcshell['cornerpoints']: + shells.append([np.where(self.strcgrid['ID'] == id)[0][0] + for id in shell(np.isfinite(shell))]) shell_type = tvtk.Polygon().cell_type self.strc_ug.set_cells(shell_type, shells) src_points = mlab.pipeline.add_dataset(self.strc_ug) - points = mlab.pipeline.glyph(src_points, colormap='viridis', scale_factor=self.p_scale, scale_mode = 'none') - surface = mlab.pipeline.surface(src_points, colormap='viridis') - else: + mlab.pipeline.glyph(src_points, colormap='viridis', scale_factor=self.p_scale, scale_mode='none') + mlab.pipeline.surface(src_points, colormap='viridis') + else: # plot points as glyphs src_points = mlab.pipeline.add_dataset(self.strc_ug) - points = mlab.pipeline.glyph(src_points, colormap='viridis', scale_factor=self.p_scale, scale_mode = 'none') - + mlab.pipeline.glyph(src_points, colormap='viridis', scale_factor=self.p_scale, scale_mode='none') + def update_strc_display(self, points, scalars): self.strc_ug.points.from_array(points) self.strc_ug.point_data.scalars.from_array(scalars) self.strc_ug.modified() - + def setup_text_display(self): self.scr_text = mlab.text(x=0.1, y=0.8, text='Time', line_width=0.5, width=0.1) - self.scr_text.property.background_color=(1,1,1) - self.scr_text.property.color=(0,0,0) - + self.scr_text.property.background_color = (1, 1, 1) + self.scr_text.property.color = (0, 0, 0) + def update_text_display(self, t): self.scr_text.text = 't = {:>5.3f}s'.format(t) - + def setup_runway(self, length, width, elevation): - x, y = np.mgrid[0:length,-width/2.0:width/2.0+1] - elev = np.ones(x.shape)*elevation - surf = mlab.surf(x, y, elev, warp_scale=1.0, color=(0.9,0.9,0.9)) - + x, y = np.mgrid[0:length, -width / 2.0:width / 2.0 + 1] + elev = np.ones(x.shape) * elevation + mlab.surf(x, y, elev, warp_scale=1.0, color=(0.9, 0.9, 0.9)) + def setup_grid(self, altitude): - spacing=100.0 - x, y = np.mgrid[0:1000+spacing:spacing,-500:500+spacing:spacing] - z = np.ones(x.shape)*altitude - xy = mlab.surf(x, y, z, representation='wireframe', line_width=1.0, color=(0.9,0.9,0.9), opacity=0.4) - #x, z = np.mgrid[0:1000+spacing:spacing,altitude-500:altitude+500+spacing:spacing] - #y = np.zeros(x.shape) - #xz = mlab.surf(x, y, z, representation='wireframe', line_width=1.0, color=(0.9,0.9,0.9), opacity=0.4) - mlab.quiver3d(0.0, 0.0, altitude, 1000.0, 0.0, 0.0, color=(0,0,0), mode='axes', opacity=0.4, scale_mode='vector', scale_factor=1.0) + spacing = 100.0 + x, y = np.mgrid[0:1000 + spacing:spacing, -500:500 + spacing:spacing] + z = np.ones(x.shape) * altitude + mlab.surf(x, y, z, representation='wireframe', line_width=1.0, color=(0.9, 0.9, 0.9), opacity=0.4) + mlab.quiver3d(0.0, 0.0, altitude, 1000.0, 0.0, 0.0, color=(0, 0, 0), mode='axes', opacity=0.4, + scale_mode='vector', scale_factor=1.0) # -------------- - # configure plot - #--------------- + # configure plot + # --------------- grid = self.strcgrid set = '' - + # get deformations - self.x = grid['offset'+set][:,0] + response['Ug'][:][:,grid['set'+set][:,0]] - self.y = grid['offset'+set][:,1] + response['Ug'][:][:,grid['set'+set][:,1]] - self.z = grid['offset'+set][:,2] + response['Ug'][:][:,grid['set'+set][:,2]] - self.color_scalar = np.linalg.norm(response['Ug_f'][:][:,grid['set'+set][:,(0,1,2)]], axis=2) - + self.x = grid['offset' + set][:, 0] + response['Ug'][:][:, grid['set' + set][:, 0]] + self.y = grid['offset' + set][:, 1] + response['Ug'][:][:, grid['set' + set][:, 1]] + self.z = grid['offset' + set][:, 2] + response['Ug'][:][:, grid['set' + set][:, 2]] + self.color_scalar = np.linalg.norm(response['Ug_f'][:][:, grid['set' + set][:, (0, 1, 2)]], axis=2) + # get forces - names = ['Pg_aero_global', 'Pg_iner_global', 'Pg_ext_global', ]# 'Pg_idrag_global', 'Pg_cs_global'] - colors = [(1,0,0), (0,1,1), (0,0,0), (0,0,1)] # red, cyan, black, blue + # 'Pg_idrag_global', 'Pg_cs_global'] + names = ['Pg_aero_global', 'Pg_iner_global', 'Pg_ext_global', ] + colors = [(1, 0, 0), (0, 1, 1), (0, 0, 0), (0, 0, 1)] # red, cyan, black, blue for name in names: calc_vector_data(self, grid=grid, set=set, name=name) @@ -379,57 +393,62 @@ def setup_grid(self, altitude): if make_movie: logging.info('rendering offscreen simulation {:s} ...'.format(trimcase['desc'])) mlab.options.offscreen = True - self.fig = mlab.figure(size=(1920, 1080), bgcolor=(1,1,1)) - else: + self.fig = mlab.figure(size=(1920, 1080), bgcolor=(1, 1, 1)) + else: logging.info('interactive plotting of forces and deformations for simulation {:s}'.format(trimcase['desc'])) - self.fig = mlab.figure(bgcolor=(1,1,1)) - + self.fig = mlab.figure(bgcolor=(1, 1, 1)) + # plot initial position - setup_strc_display(self, color=(0.9,0.9,0.9)) # light grey + setup_strc_display(self, color=(0.9, 0.9, 0.9)) # light grey setup_text_display(self) - - # plot initial forces - opacity=0.4 + + # plot initial forces + opacity = 0.4 for data, color in zip(self.vector_data, colors): setup_vector_display(self, data, color, opacity) - + # plot coordinate system mlab.orientation_axes() - + # --- optional --- - # get earth -# with open('harz.pickle', 'r') as f: -# (x,y,elev) = pickle.load(f) - # plot earth, scale colormap -# surf = mlab.surf(x,y,elev, colormap='terrain', warp_scale=-1.0, vmin = -500.0, vmax=1500.0) #gist_earth terrain summer - #setup_runway(self, length=1000.0, width=30.0, elevation=0.0) + # setup_runway(self, length=1000.0, width=30.0, elevation=0.0) setup_grid(self, 0.0) - - distance = 2.5*((self.x[0,:].max()-self.x[0,:].min())**2 + (self.y[0,:].max()-self.y[0,:].min())**2 + (self.z[0,:].max()-self.z[0,:].min())**2)**0.5 - mlab.view(azimuth=-120.0, elevation=100.0, roll=-75.0, distance=distance, focalpoint=np.array([self.x[0,:].mean(),self.y[0,:].mean(),self.z[0,:].mean()])) # view from left and above + + distance = 2.5 * ((self.x[0, :].max() - self.x[0, :].min()) ** 2 + + (self.y[0, :].max() - self.y[0, :].min()) ** 2 + + (self.z[0, :].max() - self.z[0, :].min()) ** 2) ** 0.5 + # view from left and above + mlab.view(azimuth=-120.0, elevation=100.0, roll=-75.0, distance=distance, + focalpoint=np.array([self.x[0, :].mean(), self.y[0, :].mean(), self.z[0, :].mean()])) if make_movie: if not os.path.exists('{}anim/'.format(path_output)): os.makedirs('{}anim/'.format(path_output)) - movie(self) # launch animation + movie(self) # launch animation mlab.close() # h.246 - cmd = 'ffmpeg -framerate {} -i {}anim/subcase_{}_frame_%06d.png -r 30 -y {}anim/subcase_{}.mov'.format( speedup_factor/simcase['dt'], path_output, trimcase['subcase'], path_output, trimcase['subcase']) + cmd = 'ffmpeg -framerate {} -i {}anim/subcase_{}_frame_%06d.png -r 30 -y {}anim/subcase_{}.mov'.format( + speedup_factor / simcase['dt'], path_output, trimcase['subcase'], path_output, trimcase['subcase']) logging.info(cmd) os.system(cmd) # MPEG-4 - besser geeignet fuer PowerPoint & Co. - cmd = 'ffmpeg -framerate {} -i {}anim/subcase_{}_frame_%06d.png -c:v mpeg4 -q:v 3 -r 30 -y {}anim/subcase_{}.avi'.format( speedup_factor/simcase['dt'], path_output, trimcase['subcase'], path_output, trimcase['subcase']) + cmd = 'ffmpeg -framerate {} -i {}anim/subcase_{}_frame_%06d.png -c:v mpeg4 -q:v 3 -r 30 -y \ + {}anim/subcase_{}.avi'.format(speedup_factor / simcase['dt'], path_output, trimcase['subcase'], + path_output, trimcase['subcase']) logging.info(cmd) os.system(cmd) # GIF als Notloesung. - cmd1 = 'ffmpeg -i {}anim/subcase_{}_frame_000001.png -filter_complex palettegen -y /tmp/palette.png'.format( path_output, trimcase['subcase']) - cmd2 = 'ffmpeg -framerate {} -i {}anim/subcase_{}_frame_%06d.png -i /tmp/palette.png -r 15 -filter_complex paletteuse -y {}anim/subcase_{}.gif'.format( speedup_factor/simcase['dt'], path_output, trimcase['subcase'], path_output, trimcase['subcase']) + cmd1 = 'ffmpeg -i {}anim/subcase_{}_frame_000001.png -filter_complex palettegen -y \ + /tmp/palette.png'.format(path_output, trimcase['subcase']) + cmd2 = 'ffmpeg -framerate {} -i {}anim/subcase_{}_frame_%06d.png -i /tmp/palette.png -r 15 \ + -filter_complex paletteuse -y {}anim/subcase_{}.gif'.format( + speedup_factor / simcase['dt'], path_output, trimcase['subcase'], path_output, trimcase['subcase']) logging.info(cmd1) os.system(cmd1) logging.info(cmd2) os.system(cmd2) - + else: - anim(self) # launch animation + # launch animation + anim(self) mlab.show() - diff --git a/loadskernel/plotting_standard.py b/loadskernel/plotting_standard.py index a97f404..efb1b27 100755 --- a/loadskernel/plotting_standard.py +++ b/loadskernel/plotting_standard.py @@ -1,39 +1,50 @@ # -*- coding: utf-8 -*- - +import logging +import itertools +import os import numpy as np -from matplotlib import pyplot as plt -plt.rcParams.update({'font.size': 16, - 'svg.fonttype':'none', - 'savefig.dpi': 300,}) +from scipy.spatial import ConvexHull + +from matplotlib import pyplot as plt from matplotlib.backends.backend_pdf import PdfPages from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable -from scipy.spatial import ConvexHull -import logging, itertools, os -from loadskernel.units import tas2eas -from loadskernel.units import eas2tas + +try: + from mayavi import mlab +except ImportError: + pass + +from loadskernel.units import tas2eas, eas2tas from loadskernel.io_functions.data_handling import load_hdf5_dict -class LoadPlots(object): +plt.rcParams.update({'font.size': 16, + 'svg.fonttype': 'none', + 'savefig.dpi': 300, }) + + +class LoadPlots(): + def __init__(self, jcl, model): self.jcl = jcl self.model = model self.responses = None self.monstations = None - self.potatos_fz_mx = [] # Wing, HTP - self.potatos_mx_my = [] # Wing, HTP, VTP + self.potatos_fz_mx = [] # Wing, HTP + self.potatos_mx_my = [] # Wing, HTP, VTP self.potatos_fz_my = [] - self.potatos_fy_mx = [] # VTP - self.potatos_mx_mz = [] # VTP - self.potatos_my_mz = [] # FUS + self.potatos_fy_mx = [] # VTP + self.potatos_mx_mz = [] # VTP + self.potatos_my_mz = [] # FUS self.cuttingforces_wing = [] - self.im = plt.imread(os.path.join(os.path.dirname(__file__), 'graphics', 'LK_logo2.png')) - + self.im = plt.imread(os.path.join( + os.path.dirname(__file__), 'graphics', 'LK_logo2.png')) + # load data from HDF5 - self.aerogrid = load_hdf5_dict(self.model['aerogrid']) - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + self.aerogrid = load_hdf5_dict(self.model['aerogrid']) + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) self.splinegrid = load_hdf5_dict(self.model['splinegrid']) self.calc_parameters_from_model_size() - + if hasattr(self.jcl, 'loadplots'): if 'potatos_fz_mx' in self.jcl.loadplots: self.potatos_fz_mx = self.jcl.loadplots['potatos_fz_mx'] @@ -51,99 +62,109 @@ def __init__(self, jcl, model): self.cuttingforces_wing = self.jcl.loadplots['cuttingforces_wing'] else: logging.info('jcl.loadplots not specified in the JCL - no automatic plotting of load envelopes possible.') - + def add_responses(self, responses): self.responses = responses - + def add_monstations(self, monstations): self.monstations = monstations - + def create_axes(self, logo=True): fig = plt.figure() - ax = fig.add_axes([0.2, 0.15, 0.7, 0.75]) # List is [left, bottom, width, height] + # List is [left, bottom, width, height] + ax = fig.add_axes([0.2, 0.15, 0.7, 0.75]) if logo: newax = fig.add_axes([0.04, 0.02, 0.10, 0.08]) newax.imshow(self.im, interpolation='hanning', zorder=-2) newax.axis('off') newax.set_rasterization_zorder(-1) return ax - + def calc_parameters_from_model_size(self): # Calculate the overall size of the model. - self.model_size = ( (self.strcgrid['offset'][:,0].max()-self.strcgrid['offset'][:,0].min())**2 \ - + (self.strcgrid['offset'][:,1].max()-self.strcgrid['offset'][:,1].min())**2 \ - + (self.strcgrid['offset'][:,2].max()-self.strcgrid['offset'][:,2].min())**2 )**0.5 + self.model_size = ((self.strcgrid['offset'][:, 0].max() - self.strcgrid['offset'][:, 0].min()) ** 2 + + (self.strcgrid['offset'][:, 1].max() - self.strcgrid['offset'][:, 1].min()) ** 2 + + (self.strcgrid['offset'][:, 2].max() - self.strcgrid['offset'][:, 2].min()) ** 2) ** 0.5 # Set some parameters which typically give a good view. self.pscale = np.min([self.model_size / 400.0, 0.04]) - + def plot_forces_deformation_interactive(self): - # import mayavi only when needed to long loading times - from mayavi import mlab # loop over all responses for response in self.responses: - trimcase = self.jcl.trimcase[response['i'][()]] + trimcase = self.jcl.trimcase[response['i'][()]] logging.info('interactive plotting of forces and deformations for trim {:s}'.format(trimcase['desc'])) # plot aerodynamic forces - x = self.aerogrid['offset_k'][:,0] - y = self.aerogrid['offset_k'][:,1] - z = self.aerogrid['offset_k'][:,2] - fscale = 0.5 * self.model_size / np.max(np.abs(response['Pk_aero'][0][self.aerogrid['set_k'][:,(0,1,2)]])) + x = self.aerogrid['offset_k'][:, 0] + y = self.aerogrid['offset_k'][:, 1] + z = self.aerogrid['offset_k'][:, 2] + fscale = 0.5 * self.model_size / np.max(np.abs(response['Pk_aero'][0][self.aerogrid['set_k'][:, (0, 1, 2)]])) for name in ['Pk_aero', 'Pk_rbm', 'Pk_cam', 'Pk_cs', 'Pk_f', 'Pk_idrag']: if response[name][0].sum() != 0.0: - fx = response[name][0][self.aerogrid['set_k'][:,0]] - fy = response[name][0][self.aerogrid['set_k'][:,1]] - fz = response[name][0][self.aerogrid['set_k'][:,2]] + fx = response[name][0][self.aerogrid['set_k'][:, 0]] + fy = response[name][0][self.aerogrid['set_k'][:, 1]] + fz = response[name][0][self.aerogrid['set_k'][:, 2]] mlab.figure() mlab.points3d(x, y, z, scale_factor=self.pscale) - mlab.quiver3d(x, y, z, fx, fy, fz, color=(0,1,0), scale_factor=fscale) + mlab.quiver3d(x, y, z, fx, fy, fz, color=(0, 1, 0), scale_factor=fscale) mlab.title(name, size=0.5, height=0.9) else: logging.info('Forces "{}" are zero, skip plotting') - + # plot structural deformations - x = self.strcgrid['offset'][:,0] - y = self.strcgrid['offset'][:,1] - z = self.strcgrid['offset'][:,2] - x_r = self.strcgrid['offset'][:,0] + response['Ug_r'][0][self.strcgrid['set'][:,0]] - y_r = self.strcgrid['offset'][:,1] + response['Ug_r'][0][self.strcgrid['set'][:,1]] - z_r = self.strcgrid['offset'][:,2] + response['Ug_r'][0][self.strcgrid['set'][:,2]] - x_f = self.strcgrid['offset'][:,0] + response['Ug'][0][self.strcgrid['set'][:,0]] - y_f = self.strcgrid['offset'][:,1] + response['Ug'][0][self.strcgrid['set'][:,1]] - z_f = self.strcgrid['offset'][:,2] + response['Ug'][0][self.strcgrid['set'][:,2]] - + x = self.strcgrid['offset'][:, 0] + y = self.strcgrid['offset'][:, 1] + z = self.strcgrid['offset'][:, 2] + x_r = self.strcgrid['offset'][:, 0] + response['Ug_r'][0][self.strcgrid['set'][:, 0]] + y_r = self.strcgrid['offset'][:, 1] + response['Ug_r'][0][self.strcgrid['set'][:, 1]] + z_r = self.strcgrid['offset'][:, 2] + response['Ug_r'][0][self.strcgrid['set'][:, 2]] + x_f = self.strcgrid['offset'][:, 0] + response['Ug'][0][self.strcgrid['set'][:, 0]] + y_f = self.strcgrid['offset'][:, 1] + response['Ug'][0][self.strcgrid['set'][:, 1]] + z_f = self.strcgrid['offset'][:, 2] + response['Ug'][0][self.strcgrid['set'][:, 2]] + mlab.figure() - mlab.points3d(x_r, y_r, z_r, color=(0,1,0), scale_factor=self.pscale) - mlab.points3d(x_f, y_f, z_f, color=(0,0,1), scale_factor=self.pscale) + mlab.points3d(x_r, y_r, z_r, color=(0, 1, 0), scale_factor=self.pscale) + mlab.points3d(x_f, y_f, z_f, color=(0, 0, 1), scale_factor=self.pscale) mlab.title('rbm (green) and flexible deformation (blue, true scale) in 9300 coord', size=0.5, height=0.9) - + # plot structural forces - mlab.figure() + mlab.figure() mlab.points3d(x, y, z, scale_factor=self.pscale) - fscale = 0.5 * self.model_size / np.max(np.abs(response['Pg'][0][self.strcgrid['set'][:,(0,1,2)]])) - fx, fy, fz = response['Pg'][0][self.strcgrid['set'][:,0]], response['Pg'][0][self.strcgrid['set'][:,1]], response['Pg'][0][self.strcgrid['set'][:,2]] - mlab.quiver3d(x, y, z, fx*fscale, fy*fscale, fz*fscale , color=(1,1,0), mode='2ddash', opacity=0.4, scale_mode='vector', scale_factor=1.0) - mlab.quiver3d(x+fx*fscale, y+fy*fscale, z+fz*fscale,fx*fscale, fy*fscale, fz*fscale , color=(1,1,0), mode='cone', scale_mode='vector', scale_factor=0.1, resolution=16) - mlab.points3d(self.splinegrid['offset'][:,0], self.splinegrid['offset'][:,1], self.splinegrid['offset'][:,2], color=(1,1,0), scale_factor=self.pscale*1.5) + fscale = 0.5 * self.model_size / np.max(np.abs(response['Pg'][0][self.strcgrid['set'][:, (0, 1, 2)]])) + fx = response['Pg'][0][self.strcgrid['set'][:, 0]] + fy = response['Pg'][0][self.strcgrid['set'][:, 1]] + fz = response['Pg'][0][self.strcgrid['set'][:, 2]] + mlab.quiver3d(x, y, z, fx * fscale, fy * fscale, fz * fscale, color=(1, 1, 0), mode='2ddash', opacity=0.4, + scale_mode='vector', scale_factor=1.0) + mlab.quiver3d(x + fx * fscale, y + fy * fscale, z + fz * fscale, fx * fscale, fy * fscale, fz * fscale, + color=(1, 1, 0), mode='cone', scale_mode='vector', scale_factor=0.1, resolution=16) + mlab.points3d(self.splinegrid['offset'][:, 0], self.splinegrid['offset'][:, 1], self.splinegrid['offset'][:, 2], + color=(1, 1, 0), scale_factor=self.pscale * 1.5) mlab.title('Pg', size=0.5, height=0.9) - + if response['Pg_cfd'][0].sum() != 0.0: - mlab.figure() + mlab.figure() mlab.points3d(x, y, z, scale_factor=self.pscale) - fscale = 0.5 * self.model_size /np.max(np.abs(response['Pg_cfd'][0][self.strcgrid['set'][:,(0,1,2)]])) - fx, fy, fz = response['Pg_cfd'][0][self.strcgrid['set'][:,0]], response['Pg_cfd'][0][self.strcgrid['set'][:,1]], response['Pg_cfd'][0][self.strcgrid['set'][:,2]] - mlab.quiver3d(x, y, z, fx*fscale, fy*fscale, fz*fscale , color=(1,1,0), mode='2ddash', opacity=0.4, scale_mode='vector', scale_factor=1.0) - mlab.quiver3d(x+fx*fscale, y+fy*fscale, z+fz*fscale,fx*fscale, fy*fscale, fz*fscale , color=(1,1,0), mode='cone', scale_mode='vector', scale_factor=0.1, resolution=16) - mlab.points3d(self.splinegrid['offset'][:,0], self.splinegrid['offset'][:,1], self.splinegrid['offset'][:,2], color=(1,1,0), scale_factor=self.pscale*1.5) + fscale = 0.5 * self.model_size / np.max(np.abs(response['Pg_cfd'][0][self.strcgrid['set'][:, (0, 1, 2)]])) + fx = response['Pg_cfd'][0][self.strcgrid['set'][:, 0]] + fy = response['Pg_cfd'][0][self.strcgrid['set'][:, 1]] + fz = response['Pg_cfd'][0][self.strcgrid['set'][:, 2]] + mlab.quiver3d(x, y, z, fx * fscale, fy * fscale, fz * fscale, color=(1, 1, 0), mode='2ddash', + opacity=0.4, scale_mode='vector', scale_factor=1.0) + mlab.quiver3d(x + fx * fscale, y + fy * fscale, z + fz * fscale, fx * fscale, fy * fscale, fz * fscale, + color=(1, 1, 0), mode='cone', scale_mode='vector', scale_factor=0.1, resolution=16) + mlab.points3d(self.splinegrid['offset'][:, 0], + self.splinegrid['offset'][:, 1], + self.splinegrid['offset'][:, 2], + color=(1, 1, 0), scale_factor=self.pscale * 1.5) mlab.title('Pg_cfd', size=0.5, height=0.9) else: logging.info('CFD forces are zero, skip plotting') - + # Render all plots mlab.show() def plot_monstations(self, filename_pdf): - # launch plotting self.pp = PdfPages(filename_pdf) self.potato_plots() @@ -151,117 +172,120 @@ def plot_monstations(self, filename_pdf): self.cuttingforces_along_wing_plots() self.pp.close() logging.info('plots saved as ' + filename_pdf) - + def potato_plot(self, station, desc, color, dof_xaxis, dof_yaxis, show_hull=True, show_labels=False, show_minmax=False): - loads = np.array(self.monstations[station]['loads']) - if type(self.monstations[station]['subcases']) == list: - # This is an exception if source is not a hdf5 file. + loads = np.array(self.monstations[station]['loads']) + if isinstance(self.monstations[station]['subcases'], list): + # This is an exception if source is not a hdf5 file. # For example, the monstations have been pre-processed by a merge script and are lists already. subcases = self.monstations[station]['subcases'] else: # make sure this is a list of strings - subcases = list(self.monstations[station]['subcases'].asstr()[:]) + subcases = list(self.monstations[station]['subcases'].asstr()[:]) + + points = np.vstack((loads[:, dof_xaxis], loads[:, dof_yaxis])).T + self.subplot.scatter(points[:, 0], points[:, 1], color=color, label=desc, zorder=-2) # plot points - points = np.vstack((loads[:,dof_xaxis], loads[:,dof_yaxis])).T - self.subplot.scatter(points[:,0], points[:,1], color=color, label=desc, zorder=-2) # plot points - if show_hull and points.shape[0] >= 3: try: - hull = ConvexHull(points) # calculated convex hull from scattered points - for simplex in hull.simplices: # plot convex hull - self.subplot.plot(points[simplex,0], points[simplex,1], color=color, linewidth=2.0, linestyle='--') + hull = ConvexHull(points) # calculated convex hull from scattered points + for simplex in hull.simplices: # plot convex hull + self.subplot.plot(points[simplex, 0], points[simplex, 1], color=color, linewidth=2.0, linestyle='--') crit_trimcases = [subcases[i] for i in hull.vertices] - if show_labels: - for i_case in range(crit_trimcases.__len__()): - self.subplot.text(points[hull.vertices[i_case],0], points[hull.vertices[i_case],1], str(subcases[hull.vertices[i_case]]), fontsize=8) - except: + if show_labels: + for i_case in range(crit_trimcases.__len__()): + self.subplot.text(points[hull.vertices[i_case], 0], points[hull.vertices[i_case], 1], + str(subcases[hull.vertices[i_case]]), fontsize=8) + except Exception: crit_trimcases = [] - + elif show_minmax: pos_max_loads = np.argmax(points, 0) pos_min_loads = np.argmin(points, 0) pos_minmax_loads = np.concatenate((pos_min_loads, pos_max_loads)) - self.subplot.scatter(points[pos_minmax_loads,0], points[pos_minmax_loads,1], color=(1,0,0), zorder=-2) # plot points + # plot points + self.subplot.scatter(points[pos_minmax_loads, 0], points[pos_minmax_loads, 1], color=(1, 0, 0), zorder=-2) crit_trimcases = [subcases[i] for i in pos_minmax_loads] else: crit_trimcases = subcases[:] - - if show_labels: + + if show_labels: for crit_trimcase in crit_trimcases: pos = subcases.index(crit_trimcase) - self.subplot.text(points[pos,0], points[pos,1], str(subcases[pos]), fontsize=8) - + self.subplot.text(points[pos, 0], points[pos, 1], str(subcases[pos]), fontsize=8) + self.crit_trimcases += crit_trimcases - + def potato_plot_nicely(self, station, desc, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis): - self.subplot.cla() - self.potato_plot(station, - desc=station, - color='cornflowerblue', - dof_xaxis=dof_xaxis, - dof_yaxis=dof_yaxis, - show_hull=True, - show_labels=True, - show_minmax=False) - - self.subplot.legend(loc='best') - self.subplot.ticklabel_format(style='sci', axis='x', scilimits=(-2,2)) - self.subplot.ticklabel_format(style='sci', axis='y', scilimits=(-2,2)) - self.subplot.grid(visible=True, which='major', axis='both') - self.subplot.minorticks_on() - yax = self.subplot.get_yaxis() - yax.set_label_coords(x=-0.18, y=0.5) - self.subplot.set_xlabel(var_xaxis) - self.subplot.set_ylabel(var_yaxis) - self.subplot.set_rasterization_zorder(-1) - self.pp.savefig() - + self.subplot.cla() + self.potato_plot(station, + desc=station, + color='cornflowerblue', + dof_xaxis=dof_xaxis, + dof_yaxis=dof_yaxis, + show_hull=True, + show_labels=True, + show_minmax=False) + + self.subplot.legend(loc='best') + self.subplot.ticklabel_format(style='sci', axis='x', scilimits=(-2, 2)) + self.subplot.ticklabel_format(style='sci', axis='y', scilimits=(-2, 2)) + self.subplot.grid(visible=True, which='major', axis='both') + self.subplot.minorticks_on() + yax = self.subplot.get_yaxis() + yax.set_label_coords(x=-0.18, y=0.5) + self.subplot.set_xlabel(var_xaxis) + self.subplot.set_ylabel(var_yaxis) + self.subplot.set_rasterization_zorder(-1) + self.pp.savefig() + def potato_plots(self): logging.info('start potato-plotting...') self.subplot = self.create_axes() - - potato = np.sort(np.unique(self.potatos_fz_mx + self.potatos_mx_my + self.potatos_fz_my + self.potatos_fy_mx + self.potatos_mx_mz + self.potatos_my_mz)) + + potato = np.sort(np.unique(self.potatos_fz_mx + self.potatos_mx_my + self.potatos_fz_my + self.potatos_fy_mx + + self.potatos_mx_mz + self.potatos_my_mz)) self.crit_trimcases = [] - for station in potato: + for station in potato: if station in self.potatos_fz_mx: - var_xaxis='Fz [N]' - var_yaxis='Mx [Nm]' - dof_xaxis=2 - dof_yaxis=3 + var_xaxis = 'Fz [N]' + var_yaxis = 'Mx [Nm]' + dof_xaxis = 2 + dof_yaxis = 3 self.potato_plot_nicely(station, station, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis) if station in self.potatos_mx_my: - var_xaxis='Mx [N]' - var_yaxis='My [Nm]' - dof_xaxis=3 - dof_yaxis=4 + var_xaxis = 'Mx [N]' + var_yaxis = 'My [Nm]' + dof_xaxis = 3 + dof_yaxis = 4 self.potato_plot_nicely(station, station, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis) if station in self.potatos_fz_my: - var_xaxis='Fz [N]' - var_yaxis='My [Nm]' - dof_xaxis=2 - dof_yaxis=4 + var_xaxis = 'Fz [N]' + var_yaxis = 'My [Nm]' + dof_xaxis = 2 + dof_yaxis = 4 self.potato_plot_nicely(station, station, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis) if station in self.potatos_fy_mx: - var_xaxis='Fy [N]' - var_yaxis='Mx [Nm]' - dof_xaxis=1 - dof_yaxis=3 + var_xaxis = 'Fy [N]' + var_yaxis = 'Mx [Nm]' + dof_xaxis = 1 + dof_yaxis = 3 self.potato_plot_nicely(station, station, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis) if station in self.potatos_mx_mz: - var_xaxis='Mx [Nm]' - var_yaxis='Mz [Nm]' - dof_xaxis=3 - dof_yaxis=5 + var_xaxis = 'Mx [Nm]' + var_yaxis = 'Mz [Nm]' + dof_xaxis = 3 + dof_yaxis = 5 self.potato_plot_nicely(station, station, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis) if station in self.potatos_my_mz: - var_xaxis='My [Nm]' - var_yaxis='Mz [Nm]' - dof_xaxis=4 - dof_yaxis=5 + var_xaxis = 'My [Nm]' + var_yaxis = 'Mz [Nm]' + dof_xaxis = 4 + dof_yaxis = 5 self.potato_plot_nicely(station, station, dof_xaxis, dof_yaxis, var_xaxis, var_yaxis) plt.close() - + def cuttingforces_along_wing_plots(self): logging.info('start plotting cutting forces along wing...') self.subplot = self.create_axes() @@ -269,30 +293,37 @@ def cuttingforces_along_wing_plots(self): loads = [] offsets = [] for station in self.cuttingforces_wing: - loads.append(list(self.monstations[station]['loads'][:])) # trigger to read the data now with [:] + # trigger to read the data now with [:] + loads.append(list(self.monstations[station]['loads'][:])) offsets.append(list(self.monstations[station]['offset'][:])) loads = np.array(loads) offsets = np.array(offsets) - + for i_cuttingforce in range(len(cuttingforces)): - i_max = np.argmax(loads[:,:,i_cuttingforce], 1) - i_min = np.argmin(loads[:,:,i_cuttingforce], 1) + i_max = np.argmax(loads[:, :, i_cuttingforce], 1) + i_min = np.argmin(loads[:, :, i_cuttingforce], 1) self.subplot.cla() if loads.shape[1] > 50: - logging.debug('plotting of every load case skipped due to large number (>50) of cases') + logging.debug( + 'plotting of every load case skipped due to large number (>50) of cases') else: - self.subplot.plot(offsets[:,1],loads[:,:,i_cuttingforce], color='cornflowerblue', linestyle='-', marker='.', zorder=-2) + self.subplot.plot(offsets[:, 1], loads[:, :, i_cuttingforce], color='cornflowerblue', + linestyle='-', marker='.', zorder=-2) for i_station in range(len(self.cuttingforces_wing)): # verticalalignment or va [ 'center' | 'top' | 'bottom' | 'baseline' ] # max - self.subplot.scatter(offsets[i_station,1],loads[i_station,i_max[i_station],i_cuttingforce], color='r') - self.subplot.text( offsets[i_station,1],loads[i_station,i_max[i_station],i_cuttingforce], str(self.monstations[list(self.monstations)[0]]['subcases'][i_max[i_station]]), fontsize=4, verticalalignment='bottom' ) + self.subplot.scatter(offsets[i_station, 1], loads[i_station, i_max[i_station], i_cuttingforce], color='r') + self.subplot.text(offsets[i_station, 1], loads[i_station, i_max[i_station], i_cuttingforce], + str(self.monstations[list(self.monstations)[0]]['subcases'][i_max[i_station]]), + fontsize=4, verticalalignment='bottom') # min - self.subplot.scatter(offsets[i_station,1],loads[i_station,i_min[i_station],i_cuttingforce], color='r') - self.subplot.text( offsets[i_station,1],loads[i_station,i_min[i_station],i_cuttingforce], str(self.monstations[list(self.monstations)[0]]['subcases'][i_min[i_station]]), fontsize=4, verticalalignment='top' ) + self.subplot.scatter(offsets[i_station, 1], loads[i_station, i_min[i_station], i_cuttingforce], color='r') + self.subplot.text(offsets[i_station, 1], loads[i_station, i_min[i_station], i_cuttingforce], + str(self.monstations[list(self.monstations)[0]]['subcases'][i_min[i_station]]), + fontsize=4, verticalalignment='top') - self.subplot.set_title('Wing') - self.subplot.ticklabel_format(style='sci', axis='y', scilimits=(-2,2)) + self.subplot.set_title('Wing') + self.subplot.ticklabel_format(style='sci', axis='y', scilimits=(-2, 2)) self.subplot.grid(visible=True, which='major', axis='both') self.subplot.minorticks_on() yax = self.subplot.get_yaxis() @@ -302,24 +333,25 @@ def cuttingforces_along_wing_plots(self): self.subplot.set_rasterization_zorder(-1) self.pp.savefig() plt.close() - + def plot_monstations_time(self, filename_pdf): logging.info('start plotting cutting forces over time ...') pp = PdfPages(filename_pdf) - potato = np.sort(np.unique(self.potatos_fz_mx + self.potatos_mx_my + self.potatos_fz_my + self.potatos_fy_mx + self.potatos_mx_mz + self.potatos_my_mz)) + potato = np.sort(np.unique(self.potatos_fz_mx + self.potatos_mx_my + self.potatos_fz_my + self.potatos_fy_mx + + self.potatos_mx_mz + self.potatos_my_mz)) for station in potato: monstation = self.monstations[station] - fig, ax = plt.subplots(6, sharex=True, figsize=(8,10) ) + _, ax = plt.subplots(6, sharex=True, figsize=(8, 10)) # Plot all load cases for which time series are stored in the monstation for i_case in [key for key in monstation.keys() if key.isnumeric()]: loads = monstation[i_case]['loads'] t = monstation[i_case]['t'] - ax[0].plot(t, loads[:,0], 'k', zorder=-2) - ax[1].plot(t, loads[:,1], 'k', zorder=-2) - ax[2].plot(t, loads[:,2], 'k', zorder=-2) - ax[3].plot(t, loads[:,3], 'k', zorder=-2) - ax[4].plot(t, loads[:,4], 'k', zorder=-2) - ax[5].plot(t, loads[:,5], 'k', zorder=-2) + ax[0].plot(t, loads[:, 0], 'k', zorder=-2) + ax[1].plot(t, loads[:, 1], 'k', zorder=-2) + ax[2].plot(t, loads[:, 2], 'k', zorder=-2) + ax[3].plot(t, loads[:, 3], 'k', zorder=-2) + ax[4].plot(t, loads[:, 4], 'k', zorder=-2) + ax[5].plot(t, loads[:, 5], 'k', zorder=-2) # make plots nice ax[0].set_position([0.2, 0.83, 0.7, 0.12]) ax[0].title.set_text(station) @@ -327,108 +359,116 @@ def plot_monstations_time(self, filename_pdf): ax[0].get_yaxis().set_label_coords(x=-0.18, y=0.5) ax[0].grid(visible=True, which='major', axis='both') ax[0].minorticks_on() - ax[0].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) - + ax[0].ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) + ax[1].set_position([0.2, 0.68, 0.7, 0.12]) ax[1].set_ylabel('Fy [N]') ax[1].get_yaxis().set_label_coords(x=-0.18, y=0.5) ax[1].grid(visible=True, which='major', axis='both') ax[1].minorticks_on() - ax[1].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) - + ax[1].ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) + ax[2].set_position([0.2, 0.53, 0.7, 0.12]) ax[2].set_ylabel('Fz [N]') ax[2].get_yaxis().set_label_coords(x=-0.18, y=0.5) ax[2].grid(visible=True, which='major', axis='both') ax[2].minorticks_on() - ax[2].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) - + ax[2].ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) + ax[3].set_position([0.2, 0.38, 0.7, 0.12]) ax[3].set_ylabel('Mx [Nm]') ax[3].get_yaxis().set_label_coords(x=-0.18, y=0.5) ax[3].grid(visible=True, which='major', axis='both') ax[3].minorticks_on() - ax[3].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) - + ax[3].ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) + ax[4].set_position([0.2, 0.23, 0.7, 0.12]) ax[4].set_ylabel('My [Nm]') ax[4].get_yaxis().set_label_coords(x=-0.18, y=0.5) ax[4].grid(visible=True, which='major', axis='both') ax[4].minorticks_on() - ax[4].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) - + ax[4].ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) + ax[5].set_position([0.2, 0.08, 0.7, 0.12]) ax[5].set_ylabel('Mz [Nm]') ax[5].get_yaxis().set_label_coords(x=-0.18, y=0.5) ax[5].grid(visible=True, which='major', axis='both') ax[5].minorticks_on() - ax[5].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) - + ax[5].ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) + ax[5].set_xlabel('t [sec]') - + ax[0].set_rasterization_zorder(-1) ax[1].set_rasterization_zorder(-1) ax[2].set_rasterization_zorder(-1) ax[3].set_rasterization_zorder(-1) ax[4].set_rasterization_zorder(-1) ax[5].set_rasterization_zorder(-1) - + pp.savefig() plt.close() pp.close() logging.info('plots saved as ' + filename_pdf) + class FlutterPlots(LoadPlots): - + def plot_fluttercurves(self): logging.info('start plotting flutter curves...') - fig, ax = plt.subplots(3, sharex=True, figsize=(8,10)) + fig, ax = plt.subplots(3, sharex=True, figsize=(8, 10)) ax_vtas = ax[2].twiny() for response in self.responses: - trimcase = self.jcl.trimcase[response['i'][()]] - h = self.model['atmo'][trimcase['altitude']]['h'][()] - #Plot boundaries + trimcase = self.jcl.trimcase[response['i'][()]] + h = self.model['atmo'][trimcase['altitude']]['h'][()] + # Plot boundaries fmin = 2 * np.floor(response['freqs'][:].min() / 2) - if fmin < -50.0 or np.isnan(fmin): + if fmin < -50.0 or np.isnan(fmin): fmin = -50.0 fmax = 2 * np.ceil(response['freqs'][:].max() / 2) - if fmax > 50.0 or np.isnan(fmax): + if fmax > 50.0 or np.isnan(fmax): fmax = 50.0 Vmin = 0 Vmax = 2 * np.ceil(tas2eas(response['Vtas'][:].max(), h) / 2) - if Vmax > 500.0 or np.isnan(Vmax): + if Vmax > 500.0 or np.isnan(Vmax): Vmax = 500.0 gmin = -0.11 gmax = 0.11 - - colors = itertools.cycle(( plt.cm.tab20c(np.linspace(0, 1, 20)) )) + + colors = itertools.cycle((plt.cm.tab20c(np.linspace(0, 1, 20)))) markers = itertools.cycle(('+', 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'x', 'D',)) - - ax[0].cla(); ax[1].cla(); ax[2].cla(); ax_vtas.cla() - for j in range(response['freqs'].shape[1]): + + ax[0].cla() + ax[1].cla() + ax[2].cla() + ax_vtas.cla() + + for j in range(response['freqs'].shape[1]): marker = next(markers) color = next(colors) - ax[0].plot(tas2eas(response['Vtas'][:, j], h), response['freqs'][:, j], marker=marker, markersize=4.0, linewidth=1.0, color=color) - ax[1].plot(tas2eas(response['Vtas'][:, j], h), response['damping'][:, j], marker=marker, markersize=4.0, linewidth=1.0, color=color) - ax[2].plot(tas2eas(response['Vtas'][:, j], h), response['damping'][:, j], marker=marker, markersize=4.0, linewidth=1.0, color=color) - + ax[0].plot(tas2eas(response['Vtas'][:, j], h), response['freqs'][:, j], + marker=marker, markersize=4.0, linewidth=1.0, color=color) + ax[1].plot(tas2eas(response['Vtas'][:, j], h), response['damping'][:, j], + marker=marker, markersize=4.0, linewidth=1.0, color=color) + ax[2].plot(tas2eas(response['Vtas'][:, j], h), response['damping'][:, j], + marker=marker, markersize=4.0, linewidth=1.0, color=color) + # make plots nice fig.suptitle(trimcase['desc'], fontsize=16) - + ax[0].set_position([0.15, 0.55, 0.75, 0.35]) ax[0].set_ylabel('Frequency [Hz]') ax[0].get_yaxis().set_label_coords(x=-0.13, y=0.5) ax[0].grid(visible=True, which='major', axis='both') ax[0].minorticks_on() ax[0].axis([Vmin, Vmax, fmin, fmax]) - + ax[1].set_position([0.15, 0.35, 0.75, 0.18]) ax[1].set_ylabel('Damping (zoom)') ax[1].get_yaxis().set_label_coords(x=-0.13, y=0.5) ax[1].grid(visible=True, which='major', axis='both') ax[1].minorticks_on() ax[1].axis([Vmin, Vmax, gmin, gmax]) - + ax[2].set_position([0.15, 0.15, 0.75, 0.18]) ax[2].set_ylabel('Damping') ax[2].get_yaxis().set_label_coords(x=-0.13, y=0.5) @@ -436,67 +476,76 @@ def plot_fluttercurves(self): ax[2].minorticks_on() ax[2].axis([Vmin, Vmax, -1.1, 1.1]) ax[2].set_xlabel('$V_{eas} [m/s]$') - + # additional axis for Vtas - + ax_vtas.set_position([0.15, 0.15, 0.75, 0.18]) - ax_vtas.xaxis.set_ticks_position('bottom') # set the position of the second x-axis to bottom - ax_vtas.xaxis.set_label_position('bottom') # set the position of the second x-axis to bottom + # set the position of the second x-axis to bottom + ax_vtas.xaxis.set_ticks_position('bottom') + ax_vtas.xaxis.set_label_position('bottom') ax_vtas.spines['bottom'].set_position(('outward', 60)) x1, x2 = ax[1].get_xlim() - ax_vtas.set_xlim(( eas2tas(x1, h), eas2tas(x2, h) )) + ax_vtas.set_xlim((eas2tas(x1, h), eas2tas(x2, h))) ax_vtas.minorticks_on() ax_vtas.set_xlabel('$V_{tas} [m/s]$') self.pp.savefig() - + def plot_eigenvalues(self): logging.info('start plotting eigenvalues and -vectors...') - fig, ax = plt.subplots(1, 3, figsize=(16,9)) + fig, ax = plt.subplots(1, 3, figsize=(16, 9)) ax_freq = ax[0].twinx() ax_divider = make_axes_locatable(ax[2]) ax_cbar = ax_divider.append_axes("top", size="4%", pad="1%") for response in self.responses: - trimcase = self.jcl.trimcase[response['i'][()]] - simcase = self.jcl.simcase[response['i'][()]] - h = self.model['atmo'][trimcase['altitude']]['h'][()] - + trimcase = self.jcl.trimcase[response['i'][()]] + simcase = self.jcl.simcase[response['i'][()]] + h = self.model['atmo'][trimcase['altitude']]['h'][()] + # this kind of plot is only feasible for methods which iterate over Vtas, e.g. not the K- or KE-methods if 'flutter' in simcase and simcase['flutter_para']['method'] not in ['pk', 'statespace']: - logging.info('skip plotting of eigenvalues and -vectors for {}'.format(trimcase['desc'])) + logging.info( + 'skip plotting of eigenvalues and -vectors for {}'.format(trimcase['desc'])) continue - #Plot boundaries + # Plot boundaries rmax = np.ceil(response['eigenvalues'][:].real.max()) rmin = np.floor(response['eigenvalues'][:].real.min()) imax = np.ceil(response['eigenvalues'][:].imag.max()) imin = np.floor(response['eigenvalues'][:].imag.min()) - - for i in range(response['Vtas'].shape[0]): - colors = itertools.cycle(( plt.cm.tab20c(np.linspace(0, 1, 20)) )) + + for i in range(response['Vtas'].shape[0]): + colors = itertools.cycle((plt.cm.tab20c(np.linspace(0, 1, 20)))) markers = itertools.cycle(('+', 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'x', 'D',)) desc = [str(mode) for mode in range(response['eigenvalues'].shape[1])] - - ax[0].cla(); ax[1].cla(); ax[2].cla(); ax_cbar.cla(), ax_freq.cla() # clear all axes for next plot + + ax[0].cla() + ax[1].cla() + ax[2].cla() + ax_cbar.cla(), ax_freq.cla() # clear all axes for next plot # plot eigenvector - im_eig = ax[2].imshow(response['eigenvectors'][i].__abs__(), cmap='hot_r', aspect='auto', origin='upper', vmin=0.0, vmax=1.0) + im_eig = ax[2].imshow(response['eigenvectors'][i].__abs__(), cmap='hot_r', aspect='auto', + origin='upper', vmin=0.0, vmax=1.0) # add colorbar to plot fig.colorbar(im_eig, cax=ax_cbar, orientation="horizontal") # plot eigenvalues - for j in range(response['eigenvalues'].shape[1]): + for j in range(response['eigenvalues'].shape[1]): marker = next(markers) color = next(colors) - ax[0].plot(response['eigenvalues'][:,j].real, response['eigenvalues'][:,j].imag, color=color, linestyle='--') - ax[0].plot(response['eigenvalues'][i,j].real, response['eigenvalues'][i,j].imag, marker=marker, markersize=8.0, color=color, label=desc[j]) - ax[1].plot(response['eigenvalues'][:,j].real, response['eigenvalues'][:,j].imag, color=color, linestyle='--') - ax[1].plot(response['eigenvalues'][i,j].real, response['eigenvalues'][i,j].imag, marker=marker, markersize=8.0, color=color, label=desc[j]) - ax[2].plot(j,response['states'].__len__(), marker=marker, markersize=8.0, c=color) - + ax[0].plot(response['eigenvalues'][:, j].real, response['eigenvalues'][:, j].imag, + color=color, linestyle='--') + ax[0].plot(response['eigenvalues'][i, j].real, response['eigenvalues'][i, j].imag, + marker=marker, markersize=8.0, color=color, label=desc[j]) + ax[1].plot(response['eigenvalues'][:, j].real, response['eigenvalues'][:, j].imag, + color=color, linestyle='--') + ax[1].plot(response['eigenvalues'][i, j].real, response['eigenvalues'][i, j].imag, + marker=marker, markersize=8.0, color=color, label=desc[j]) + ax[2].plot(j, response['states'].__len__(), + marker=marker, markersize=8.0, c=color) + # make plots nice - fig.suptitle(t='{}, Veas={:.2f} m/s, Vtas={:.2f} m/s'.format(trimcase['desc'], - tas2eas(response['Vtas'][i,0], h), - response['Vtas'][i,0]), - fontsize=16) + fig.suptitle(t='{}, Veas={:.2f} m/s, Vtas={:.2f} m/s'.format( + trimcase['desc'], tas2eas(response['Vtas'][i, 0], h), response['Vtas'][i, 0]), fontsize=16) ax[0].set_position([0.12, 0.1, 0.25, 0.8]) ax[0].set_xlabel('Real') ax[0].set_ylabel('Imag') @@ -504,13 +553,14 @@ def plot_eigenvalues(self): ax[0].grid(visible=True, which='major', axis='both') ax[0].minorticks_on() ax[0].axis([rmin, rmax, imin, imax]) - + # additional axis for frequency - ax_freq.yaxis.set_ticks_position('left') # set the position of the second y-axis to left - ax_freq.yaxis.set_label_position('left') # set the position of the second y-axis to left + # set the position of the second y-axis to left + ax_freq.yaxis.set_ticks_position('left') + ax_freq.yaxis.set_label_position('left') ax_freq.spines['left'].set_position(('outward', 60)) y1, y2 = ax[0].get_ylim() - ax_freq.set_ylim(( y1/2.0/np.pi, y2/2.0/np.pi )) + ax_freq.set_ylim((y1 / 2.0 / np.pi, y2 / 2.0 / np.pi)) ax_freq.minorticks_on() ax_freq.set_ylabel('Frequency [Hz]') @@ -525,33 +575,35 @@ def plot_eigenvalues(self): ax[1].yaxis.offsetText.set_visible(False) # add legend ax[1].legend(bbox_to_anchor=(1.10, 1), loc='upper left', borderaxespad=0.0, fontsize=10) - + ax[2].set_position([0.60, 0.1, 0.35, 0.8]) - ax[2].yaxis.set_ticks(np.arange(0,response['states'].__len__(),1)) + ax[2].yaxis.set_ticks(np.arange(0, response['states'].__len__(), 1)) ax[2].yaxis.set_ticklabels(response['states'].asstr(), fontsize=10) ax[2].yaxis.set_tick_params(rotation=0) - ax[2].xaxis.set_ticks(np.arange(0,response['eigenvalues'].shape[1],1)) - ax[2].xaxis.set_ticklabels(np.arange(0,response['eigenvalues'].shape[1],1), fontsize=10) + ax[2].xaxis.set_ticks(np.arange(0, response['eigenvalues'].shape[1], 1)) + ax[2].xaxis.set_ticklabels(np.arange(0, response['eigenvalues'].shape[1], 1), fontsize=10) ax[2].grid(visible=True, which='major', axis='both') - - ax_cbar.xaxis.set_ticks_position("top") # change tick position to top. Tick position defaults to bottom and overlaps the image. - + + # change tick position to top. Tick position defaults to bottom and overlaps the image. + ax_cbar.xaxis.set_ticks_position("top") + self.pp.savefig() - + def plot_fluttercurves_to_pdf(self, filename_pdf): self.pp = PdfPages(filename_pdf) self.plot_fluttercurves() self.pp.close() logging.info('plots saved as ' + filename_pdf) - + def plot_eigenvalues_to_pdf(self, filename_pdf): self.pp = PdfPages(filename_pdf) self.plot_eigenvalues() self.pp.close() logging.info('plots saved as ' + filename_pdf) + class TurbulencePlots(LoadPlots): - + def plot_monstations(self, filename_pdf): # launch plotting @@ -561,88 +613,84 @@ def plot_monstations(self, filename_pdf): logging.info('plots saved as ' + filename_pdf) def potato_plot(self, station, desc, color, dof_xaxis, dof_yaxis, show_hull=True, show_labels=False, show_minmax=False): - loads = np.array(self.monstations[station]['loads']) - subcases = list(self.monstations[station]['subcases'].asstr()[:]) # make sure this is a list - turbulence_loads = np.array(self.monstations[station]['turbulence_loads']) + loads = np.array(self.monstations[station]['loads']) + subcases = list(self.monstations[station]['subcases'].asstr()[:]) # make sure this is a list + turbulence_loads = np.array(self.monstations[station]['turbulence_loads']) correlations = np.array(self.monstations[station]['correlations']) - - X0 = loads[:,dof_xaxis] - Y0 = loads[:,dof_yaxis] - - tangent_pos = loads.T + turbulence_loads.T * correlations.T - tangent_neg = loads.T - turbulence_loads.T * correlations.T - - AB_pos = loads.T + turbulence_loads.T * ((1.0 - correlations.T)/2.0)**0.5 - AB_neg = loads.T - turbulence_loads.T * ((1.0 - correlations.T)/2.0)**0.5 - - CD_pos = loads.T + turbulence_loads.T * ((1.0 + correlations.T)/2.0)**0.5 - CD_neg = loads.T - turbulence_loads.T * ((1.0 + correlations.T)/2.0)**0.5 - - X = np.vstack(( tangent_pos[dof_xaxis,dof_xaxis, :], - tangent_neg[dof_xaxis,dof_xaxis, :], - tangent_pos[dof_yaxis,dof_xaxis, :], - tangent_neg[dof_yaxis,dof_xaxis, :], - AB_pos[dof_yaxis,dof_xaxis, :], - AB_neg[dof_yaxis,dof_xaxis, :], - CD_pos[dof_yaxis,dof_xaxis, :], - CD_neg[dof_yaxis,dof_xaxis, :], - )).T - Y = np.vstack(( tangent_pos[dof_xaxis,dof_yaxis, :], - tangent_neg[dof_xaxis,dof_yaxis, :], - tangent_pos[dof_yaxis,dof_yaxis, :], - tangent_neg[dof_yaxis,dof_yaxis, :], - AB_neg[dof_xaxis,dof_yaxis, :], - AB_pos[dof_xaxis,dof_yaxis, :], - CD_pos[dof_xaxis,dof_yaxis, :], - CD_neg[dof_xaxis,dof_yaxis, :], - )).T + + X0 = loads[:, dof_xaxis] + Y0 = loads[:, dof_yaxis] + + tangent_pos = loads.T + turbulence_loads.T * correlations.T + tangent_neg = loads.T - turbulence_loads.T * correlations.T + + AB_pos = loads.T + turbulence_loads.T * ((1.0 - correlations.T) / 2.0) ** 0.5 + AB_neg = loads.T - turbulence_loads.T * ((1.0 - correlations.T) / 2.0) ** 0.5 + + CD_pos = loads.T + turbulence_loads.T * ((1.0 + correlations.T) / 2.0) ** 0.5 + CD_neg = loads.T - turbulence_loads.T * ((1.0 + correlations.T) / 2.0) ** 0.5 + + X = np.vstack((tangent_pos[dof_xaxis, dof_xaxis, :], + tangent_neg[dof_xaxis, dof_xaxis, :], + tangent_pos[dof_yaxis, dof_xaxis, :], + tangent_neg[dof_yaxis, dof_xaxis, :], + AB_pos[dof_yaxis, dof_xaxis, :], + AB_neg[dof_yaxis, dof_xaxis, :], + CD_pos[dof_yaxis, dof_xaxis, :], + CD_neg[dof_yaxis, dof_xaxis, :], + )).T + Y = np.vstack((tangent_pos[dof_xaxis, dof_yaxis, :], + tangent_neg[dof_xaxis, dof_yaxis, :], + tangent_pos[dof_yaxis, dof_yaxis, :], + tangent_neg[dof_yaxis, dof_yaxis, :], + AB_neg[dof_xaxis, dof_yaxis, :], + AB_pos[dof_xaxis, dof_yaxis, :], + CD_pos[dof_xaxis, dof_yaxis, :], + CD_neg[dof_xaxis, dof_yaxis, :], + )).T self.subplot.scatter(X0, Y0, color=color, label=desc, zorder=-2) self.subplot.scatter(X.ravel(), Y.ravel(), color=color, zorder=-2) - + if show_hull: for i_subcase in range(len(subcases)): - self.fit_ellipse(X0[i_subcase], Y0[i_subcase], X[i_subcase,:], Y[i_subcase,:], color) -# elif show_minmax: -# pos_max_loads = np.argmax(points, 0) -# pos_min_loads = np.argmin(points, 0) -# pos_minmax_loads = np.concatenate((pos_min_loads, pos_max_loads)) -# self.subplot.scatter(points[pos_minmax_loads,0], points[pos_minmax_loads,1], color=(1,0,0), zorder=-2) # plot points - - if show_labels: + self.fit_ellipse(X0[i_subcase], Y0[i_subcase], X[i_subcase, :], Y[i_subcase, :], color) + + if show_labels: labels = [] for subcase in subcases: for point in ['_T1', '_T2', '_T3', '_T4', '_AB', '_EF', '_CD', '_GH']: - labels.append(subcase+point) + labels.append(subcase + point) for x, y, label in zip(X.ravel(), Y.ravel(), labels): - self.subplot.text(x, y, label, fontsize=8) - + self.subplot.text(x, y, label, fontsize=8) + def fit_ellipse(self, X0, Y0, X, Y, color): # Formulate and solve the least squares problem ||Ax - b ||^2 - A = np.vstack([(X-X0)**2, 2.0*(X-X0)*(Y-Y0), (Y-Y0)**2]).T + A = np.vstack([(X - X0) ** 2, 2.0 * (X - X0) * (Y - Y0), (Y - Y0) ** 2]).T b = np.ones_like(X) x = np.linalg.lstsq(A, b, rcond=None)[0].squeeze() - + # Print the equation of the ellipse in standard form - logging.debug('The ellipse is given by {0:.3}x^2 + {1:.3}*2xy+{2:.3}y^2 = 1'.format(x[0], x[1], x[2])) - - # Calculate the parameters of the ellipse - alpha = -0.5*np.arctan(2*x[1]/(x[2]-x[0])) - eta = x[0]+x[2] - zeta = (x[2]-x[0])/np.cos(2*alpha) - major = (2.0/(eta-zeta))**0.5 - minor = (2.0/(eta+zeta))**0.5 - + logging.debug( + 'The ellipse is given by {0:.3}x^2 + {1:.3}*2xy+{2:.3}y^2 = 1'.format(x[0], x[1], x[2])) + + # Calculate the parameters of the ellipse + alpha = -0.5 * np.arctan(2 * x[1] / (x[2] - x[0])) + eta = x[0] + x[2] + zeta = (x[2] - x[0]) / np.cos(2 * alpha) + major = (2.0 / (eta - zeta)) ** 0.5 + minor = (2.0 / (eta + zeta)) ** 0.5 + logging.debug('Major axis = {:.3f}'.format(major)) logging.debug('Minor axis = {:.3f}'.format(minor)) - logging.debug('Rotation = {:.3f} deg'.format(alpha/np.pi*180.0)) - + logging.debug('Rotation = {:.3f} deg'.format(alpha / np.pi * 180.0)) + # Plot the given samples - #self.subplot.scatter(X, Y, label='Data Points') - + # self.subplot.scatter(X, Y, label='Data Points') + X, Y = self.ellipse_polar(major, minor, alpha) - self.subplot.plot(X+X0, Y+Y0, color=color, linewidth=2.0, linestyle='--') - - def ellipse_polar(self, major, minor, alpha, phi=np.linspace(0, 2.0*np.pi, 360)): - X = 0.0 + major*np.cos(phi)*np.cos(alpha) - minor*np.sin(phi)*np.sin(alpha) - Y = 0.0 + major*np.cos(phi)*np.sin(alpha) + minor*np.sin(phi)*np.cos(alpha) - return X, Y \ No newline at end of file + self.subplot.plot(X + X0, Y + Y0, color=color, linewidth=2.0, linestyle='--') + + def ellipse_polar(self, major, minor, alpha, phi=np.linspace(0, 2.0 * np.pi, 360)): + X = 0.0 + major * np.cos(phi) * np.cos(alpha) - minor * np.sin(phi) * np.sin(alpha) + Y = 0.0 + major * np.cos(phi) * np.sin(alpha) + minor * np.sin(phi) * np.cos(alpha) + return X, Y diff --git a/loadskernel/post_processing.py b/loadskernel/post_processing.py index d3e5d7a..61e5763 100755 --- a/loadskernel/post_processing.py +++ b/loadskernel/post_processing.py @@ -1,109 +1,120 @@ +import copy +import logging import numpy as np -import copy, logging from loadskernel.solution_tools import calc_drehmatrix from loadskernel.grid_trafo import grid_trafo, vector_trafo from loadskernel.io_functions.data_handling import load_hdf5_sparse_matrix, load_hdf5_dict -class PostProcessing(object): + +class PostProcessing(): """ In this class calculations are made that follow up on every simulation. - The functions should be able to handle both trim calculations and time simulations. + The functions should be able to handle both trim calculations and time simulations. """ + def __init__(self, jcl, model, trimcase, response): self.jcl = jcl self.model = model self.trimcase = trimcase self.response = response - - mass = self.model['mass'][trimcase['mass']] - self.n_modes = mass['n_modes'][()] - self.Mgg = load_hdf5_sparse_matrix(mass['MGG']) - self.PHIf_strc = mass['PHIf_strc'][()] + + mass = self.model['mass'][trimcase['mass']] + self.n_modes = mass['n_modes'][()] + self.Mgg = load_hdf5_sparse_matrix(mass['MGG']) + self.PHIf_strc = mass['PHIf_strc'][()] self.PHIstrc_cg = mass['PHIstrc_cg'][()] self.PHIcg_norm = mass['PHIcg_norm'][()] - - self.cggrid = load_hdf5_dict(mass['cggrid']) - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) - self.mongrid = load_hdf5_dict(self.model['mongrid']) - self.coord = load_hdf5_dict(self.model['coord']) - - self.PHIk_strc = load_hdf5_sparse_matrix(self.model['PHIk_strc']) + + self.cggrid = load_hdf5_dict(mass['cggrid']) + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + self.mongrid = load_hdf5_dict(self.model['mongrid']) + self.coord = load_hdf5_dict(self.model['coord']) + + self.PHIk_strc = load_hdf5_sparse_matrix(self.model['PHIk_strc']) self.PHIstrc_mon = load_hdf5_sparse_matrix(self.model['PHIstrc_mon']) - + if hasattr(self.jcl, 'landinggear') or hasattr(self.jcl, 'engine'): self.extragrid = load_hdf5_dict(self.model['extragrid']) - + if self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: # get cfd splining matrices self.PHIcfd_strc = load_hdf5_sparse_matrix(self.model['PHIcfd_strc']) - + def force_summation_method(self): logging.info('calculating forces & moments on structural set (force summation method)...') - response = self.response - - response['Pg_iner'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg_aero'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_unsteady'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_gust'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_cs'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_idrag'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg_ext'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg_cfd'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['d2Ug_dt2'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + response = self.response + + response['Pg_iner'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Pg_aero'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + # response['Pg_unsteady'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + # response['Pg_gust'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + # response['Pg_cs'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + # response['Pg_idrag'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + response['Pg_ext'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Pg_cfd'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Pg'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['d2Ug_dt2'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) for i_step in range(len(response['t'])): - if hasattr(self.jcl,'eom') and self.jcl.eom['version'] == 'waszak': - # Fuer Bewegungsgleichungen z.B. von Waszack muessen die zusaetzlichen Terme hier ebenfalls beruecksichtigt werden! - d2Ug_dt2_r = self.PHIstrc_cg.dot( np.hstack((response['d2Ucg_dt2'][i_step,0:3] - response['g_cg'][i_step,:] - np.cross(response['dUcg_dt'][i_step,0:3], response['dUcg_dt'][i_step,3:6]), - response['d2Ucg_dt2'][i_step,3:6] )) ) + if hasattr(self.jcl, 'eom') and self.jcl.eom['version'] == 'waszak': + # Fuer Bewegungsgleichungen z.B. von Waszack muessen die zusaetzlichen Terme hier ebenfalls beruecksichtigt + # werden! + d2Ug_dt2_r = self.PHIstrc_cg.dot(np.hstack((response['d2Ucg_dt2'][i_step, 0:3] - response['g_cg'][i_step, :] + - np.cross(response['dUcg_dt'][i_step, 0:3], + response['dUcg_dt'][i_step, 3:6]), + response['d2Ucg_dt2'][i_step, 3:6]))) else: - d2Ug_dt2_r = self.PHIstrc_cg.dot( np.hstack((response['d2Ucg_dt2'][i_step,0:3] - response['g_cg'][i_step,:], response['d2Ucg_dt2'][i_step,3:6])) ) # Nastran - - d2Ug_dt2_f = self.PHIf_strc.T.dot(response['d2Uf_dt2'][i_step,:]) - Pg_iner_r = - self.Mgg.dot(d2Ug_dt2_r) - Pg_iner_f = - self.Mgg.dot(d2Ug_dt2_f) - response['Pg_iner'][i_step,:] = Pg_iner_r + Pg_iner_f - response['Pg_aero'][i_step,:] = self.PHIk_strc.T.dot(response['Pk_aero'][i_step,:]) - #response['Pg_gust'][i_step,:] = self.PHIk_strc.T.dot(response['Pk_gust'][i_step,:]) - #response['Pg_unsteady'][i_step,:] = self.PHIk_strc.T.dot(response['Pk_unsteady'][i_step,:]) - #response['Pg_cs'][i_step,:] = self.PHIk_strc.T.dot(response['Pk_cs'][i_step,:]) - #response['Pg_idrag'][i_step,:]= self.PHIk_strc.T.dot(response['Pk_idrag'][i_step,:]) + # Nastran + d2Ug_dt2_r = self.PHIstrc_cg.dot(np.hstack((response['d2Ucg_dt2'][i_step, 0:3] - response['g_cg'][i_step, :], + response['d2Ucg_dt2'][i_step, 3:6]))) + + d2Ug_dt2_f = self.PHIf_strc.T.dot(response['d2Uf_dt2'][i_step, :]) + Pg_iner_r = -self.Mgg.dot(d2Ug_dt2_r) + Pg_iner_f = -self.Mgg.dot(d2Ug_dt2_f) + response['Pg_iner'][i_step, :] = Pg_iner_r + Pg_iner_f + response['Pg_aero'][i_step, :] = self.PHIk_strc.T.dot(response['Pk_aero'][i_step, :]) + # response['Pg_gust'][i_step, :] = self.PHIk_strc.T.dot(response['Pk_gust'][i_step, :]) + # response['Pg_unsteady'][i_step, :] = self.PHIk_strc.T.dot(response['Pk_unsteady'][i_step, :]) + # response['Pg_cs'][i_step, :] = self.PHIk_strc.T.dot(response['Pk_cs'][i_step, :]) + # response['Pg_idrag'][i_step, :]= self.PHIk_strc.T.dot(response['Pk_idrag'][i_step, :]) if self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: - response['Pg_cfd'][i_step,:] = self.PHIcfd_strc.T.dot(response['Pcfd'][i_step,:]) + response['Pg_cfd'][i_step, :] = self.PHIcfd_strc.T.dot(response['Pcfd'][i_step, :]) if hasattr(self.jcl, 'landinggear') or hasattr(self.jcl, 'engine'): - response['Pg_ext'][i_step,self.extragrid['set_strcgrid']] = response['Pextra'][i_step,self.extragrid['set']] - response['Pg'][i_step,:] = response['Pg_aero'][i_step,:] + response['Pg_iner'][i_step,:] + response['Pg_ext'][i_step,:] + response['Pg_cfd'][i_step,:] - response['d2Ug_dt2'][i_step,:] = d2Ug_dt2_r + d2Ug_dt2_f - + response['Pg_ext'][i_step, self.extragrid['set_strcgrid']] = response['Pextra'][i_step, self.extragrid['set']] + response['Pg'][i_step, :] = response['Pg_aero'][i_step, :] + response['Pg_iner'][i_step, :] \ + + response['Pg_ext'][i_step, :] + response['Pg_cfd'][i_step, :] + response['d2Ug_dt2'][i_step, :] = d2Ug_dt2_r + d2Ug_dt2_f + def modal_displacement_method(self): - logging.info('calculating forces & moments on structural set (modal displacement method)...') - logging.warning('using the modal displacement method is not recommended, use force summation method instead.') - response = self.response - Kgg = load_hdf5_sparse_matrix(self.model['KGG']) - - response['Pg'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + logging.info( + 'calculating forces & moments on structural set (modal displacement method)...') + logging.warning( + 'using the modal displacement method is not recommended, use force summation method instead.') + response = self.response + Kgg = load_hdf5_sparse_matrix(self.model['KGG']) + + response['Pg'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) for i_step in range(len(response['t'])): - Uf = response['X'][i_step,:][12:12+self.n_modes] + Uf = response['X'][i_step, :][12:12 + self.n_modes] Ug_f_body = self.PHIf_strc.T.dot(Uf) # MDM: p = K*u - response['Pg'][i_step,:] = Kgg.dot(Ug_f_body) + response['Pg'][i_step, :] = Kgg.dot(Ug_f_body) def euler_transformation(self): logging.info('apply euler angles...') - response = self.response - - response['Pg_iner_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg_aero_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_gust_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_unsteady_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_cs_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - #response['Pg_idrag_global']= np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg_ext_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Pg_cfd_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Ug_r'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Ug_f'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) - response['Ug'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + response = self.response + + response['Pg_iner_global'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Pg_aero_global'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + # response['Pg_gust_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + # response['Pg_unsteady_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + # response['Pg_cs_global'] = np.zeros((len(response['t']), 6*self.strcgrid['n'])) + # response['Pg_idrag_global']= np.zeros((len(response['t']), 6*self.strcgrid['n'])) + response['Pg_ext_global'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Pg_cfd_global'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Ug_r'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Ug_f'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) + response['Ug'] = np.zeros((len(response['t']), 6 * self.strcgrid['n'])) for i_step in range(len(response['t'])): # Including rotation about euler angles in calculation of Ug_r and Ug_f @@ -111,34 +122,42 @@ def euler_transformation(self): # setting up coordinate system coord_tmp = copy.deepcopy(self.coord) - coord_tmp['ID'] = np.append(coord_tmp['ID'], [1000000, 1000001]) - coord_tmp['RID'] = np.append(coord_tmp['RID'], [0, 0]) - coord_tmp['dircos'] = np.append(coord_tmp['dircos'], [self.PHIcg_norm[0:3,0:3].dot(calc_drehmatrix(response['X'][i_step,:][3], response['X'][i_step,:][4], response['X'][i_step,:][5])), - np.eye(3)], axis=0) - coord_tmp['offset'] = np.append(coord_tmp['offset'], [response['X'][i_step,:][0:3], + coord_tmp['ID'] = np.append(coord_tmp['ID'], [1000000, 1000001]) + coord_tmp['RID'] = np.append(coord_tmp['RID'], [0, 0]) + coord_tmp['dircos'] = np.append( + coord_tmp['dircos'], + [self.PHIcg_norm[0:3, 0:3].dot(calc_drehmatrix(response['X'][i_step, :][3], + response['X'][i_step, :][4], + response['X'][i_step, :][5])), + np.eye(3)], axis=0) + coord_tmp['offset'] = np.append(coord_tmp['offset'], [response['X'][i_step, :][0:3], -self.cggrid['offset'][0]], axis=0) - + # apply transformation to strcgrid strcgrid_tmp = copy.deepcopy(self.strcgrid) strcgrid_tmp['CP'] = np.repeat(1000001, self.strcgrid['n']) grid_trafo(strcgrid_tmp, coord_tmp, 1000000) - response['Ug_r'][i_step,self.strcgrid['set'][:,0]] = strcgrid_tmp['offset'][:,0] - self.strcgrid['offset'][:,0] - response['Ug_r'][i_step,self.strcgrid['set'][:,1]] = strcgrid_tmp['offset'][:,1] - self.strcgrid['offset'][:,1] - response['Ug_r'][i_step,self.strcgrid['set'][:,2]] = strcgrid_tmp['offset'][:,2] - self.strcgrid['offset'][:,2] + response['Ug_r'][i_step, self.strcgrid['set'][:, 0]] = strcgrid_tmp['offset'][:, 0] - self.strcgrid['offset'][:, 0] + response['Ug_r'][i_step, self.strcgrid['set'][:, 1]] = strcgrid_tmp['offset'][:, 1] - self.strcgrid['offset'][:, 1] + response['Ug_r'][i_step, self.strcgrid['set'][:, 2]] = strcgrid_tmp['offset'][:, 2] - self.strcgrid['offset'][:, 2] # apply transformation to flexible deformations vector - Uf = response['X'][i_step,:][12:12+self.n_modes] + Uf = response['X'][i_step, :][12:12 + self.n_modes] Ug_f_body = np.dot(self.PHIf_strc.T, Uf.T).T strcgrid_tmp = copy.deepcopy(self.strcgrid) - response['Ug_f'][i_step,:] = vector_trafo(strcgrid_tmp, coord_tmp, Ug_f_body, dest_coord=1000000) - response['Pg_aero_global'][i_step,:] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_aero'][i_step,:], dest_coord=1000000) - response['Pg_iner_global'][i_step,:] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_iner'][i_step,:], dest_coord=1000000) - response['Pg_ext_global'][i_step,:] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_ext'][i_step,:], dest_coord=1000000) - response['Pg_cfd_global'][i_step,:] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_cfd'][i_step,:], dest_coord=1000000) - response['Ug'][i_step,:] = response['Ug_r'][i_step,:] + response['Ug_f'][i_step,:] + response['Ug_f'][i_step, :] = vector_trafo(strcgrid_tmp, coord_tmp, Ug_f_body, dest_coord=1000000) + response['Pg_aero_global'][i_step, :] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_aero'][i_step, :], + dest_coord=1000000) + response['Pg_iner_global'][i_step, :] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_iner'][i_step, :], + dest_coord=1000000) + response['Pg_ext_global'][i_step, :] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_ext'][i_step, :], + dest_coord=1000000) + response['Pg_cfd_global'][i_step, :] = vector_trafo(strcgrid_tmp, coord_tmp, response['Pg_cfd'][i_step, :], + dest_coord=1000000) + response['Ug'][i_step, :] = response['Ug_r'][i_step, :] + response['Ug_f'][i_step, :] def cuttingforces(self): logging.info('calculating cutting forces & moments...') - response = self.response - response['Pmon_local'] = np.zeros((len(response['t']), 6*self.mongrid['n'])) + response = self.response + response['Pmon_local'] = np.zeros((len(response['t']), 6 * self.mongrid['n'])) for i_step in range(len(response['t'])): - response['Pmon_local'][i_step,:] = self.PHIstrc_mon.T.dot(response['Pg'][i_step,:]) + response['Pmon_local'][i_step, :] = self.PHIstrc_mon.T.dot(response['Pg'][i_step, :]) diff --git a/loadskernel/program_flow.py b/loadskernel/program_flow.py index 68a2d80..74fa135 100755 --- a/loadskernel/program_flow.py +++ b/loadskernel/program_flow.py @@ -1,19 +1,23 @@ -import time, getpass, platform, logging, sys, copy, argparse +import argparse +import copy +import getpass +import logging +import platform +import sys +import time + try: from mpi4py import MPI -except: +except ImportError: pass +from loadskernel import solution_sequences, post_processing, gather_loads, auxiliary_output, plotting_standard, plotting_extra from loadskernel.io_functions import data_handling -from loadskernel import solution_sequences -from loadskernel import post_processing -from loadskernel import gather_loads -from loadskernel import auxiliary_output -from loadskernel import plotting_standard import loadskernel.model as model_modul from loadskernel.cfd_interfaces.mpi_helper import setup_mpi -class ProgramFlowHelper(object): + +class ProgramFlowHelper(): def __init__(self, job_name, @@ -24,24 +28,25 @@ def __init__(self, jcl=None, machinefile=None,): # basic options - self.pre = pre # True/False - self.main = main # True/False - self.post = post # True/False + self.pre = pre # True/False + self.main = main # True/False + self.post = post # True/False # debug options - self.debug = False # True/False - self.restart = False # True/False + self.debug = False # True/False + self.restart = False # True/False # advanced options - self.test = test # True/False + self.test = test # True/False # job control options - self.job_name = job_name # string - self.path_input = path_input # path + self.job_name = job_name # string + self.path_input = path_input # path self.path_output = path_output # path - self.jcl = jcl # python class + self.jcl = jcl # python class self.machinefile = machinefile # filename # Initialize some more things self.setup_path() # Initialize MPI interface - self.have_mpi, self.comm, self.status, self.myid = setup_mpi(self.debug) + self.have_mpi, self.comm, self.status, self.myid = setup_mpi( + self.debug) # Establish whether or not to use multiprocessing if self.have_mpi and self.comm.Get_size() > 1: self.use_multiprocessing = True @@ -64,21 +69,22 @@ def print_logo(self): logging.info(' ---------O---------') logging.info('') logging.info('') - + def setup_logger_cluster(self, i): # Generate a separate filename for each subcase - path_log = data_handling.check_path(self.path_output+'log/') - filename = path_log + 'log_' + self.job_name + '_subcase_' + str(self.jcl.trimcase[i]['subcase']) + '.txt.' + str(self.myid) + path_log = data_handling.check_path(self.path_output + 'log/') + filename = path_log + 'log_' + self.job_name + '_subcase_' + str(self.jcl.trimcase[i]['subcase']) \ + + '.txt.' + str(self.myid) # Then create the logger and console output self.create_logfile_and_console_output(filename) def setup_logger(self): - # Generate a generic name for the log file - path_log = data_handling.check_path(self.path_output+'log/') + # Generate a generic name for the log file + path_log = data_handling.check_path(self.path_output + 'log/') filename = path_log + 'log_' + self.job_name + '.txt.' + str(self.myid) # Then create the logger and console output self.create_logfile_and_console_output(filename) - + def create_logfile_and_console_output(self, filename): logger = logging.getLogger() # Set logging level. @@ -96,7 +102,7 @@ def create_logfile_and_console_output(self, filename): logger.removeHandler(hdlr) # Update the list of all existing loggers. existing_handlers = [hdlr.get_name() for hdlr in logger.handlers] - + # Add the following handlers only if they don't exist. This avoid duplicate lines/log entries. if 'lk_logfile' not in existing_handlers: # define a Handler which writes messages to a log file @@ -106,7 +112,7 @@ def create_logfile_and_console_output(self, filename): datefmt='%d/%m/%Y %H:%M:%S') logfile.setFormatter(formatter) logger.addHandler(logfile) - + # For convinience, the first rank writes console outputs, too. if (self.myid == 0) and ('lk_console' not in existing_handlers): # define a Handler which writes messages to the sys.stout @@ -118,9 +124,10 @@ def create_logfile_and_console_output(self, filename): console.setFormatter(formatter) # add the handler(s) to the root logger logger.addHandler(console) - + logger.info('This is the log for process {}.'.format(self.myid)) + class Kernel(ProgramFlowHelper): def run(self): @@ -200,30 +207,28 @@ def run_main_sequential(self): # open response responses = data_handling.load_hdf5_responses(self.job_name, self.path_output) fid = data_handling.open_hdf5(self.path_output + 'response_' + self.job_name + '.hdf5') # open response - + for i in range(len(self.jcl.trimcase)): if self.restart and i in [response['i'][()] for response in responses]: logging.info('Restart option: found existing response.') response = responses[[response['i'][()] for response in responses].index(i)] else: - jcl = copy.deepcopy(self.jcl) + jcl = copy.deepcopy(self.jcl) response = self.main_common(model, jcl, i) if self.myid == 0 and response['successful']: mon.gather_monstations(self.jcl.trimcase[i], response) mon.gather_dyn2stat(response) logging.info('--> Saving response(s).') - data_handling.write_hdf5(fid, response, path='/'+str(response['i'])) + data_handling.write_hdf5(fid, response, path='/' + str(response['i'])) if self.myid == 0: # close response data_handling.close_hdf5(fid) - + logging.info('--> Saving monstation(s).') - data_handling.dump_hdf5(self.path_output + 'monstations_' + self.job_name + '.hdf5', - mon.monstations) - + data_handling.dump_hdf5(self.path_output + 'monstations_' + self.job_name + '.hdf5', mon.monstations) + logging.info('--> Saving dyn2stat.') - data_handling.dump_hdf5(self.path_output + 'dyn2stat_' + self.job_name + '.hdf5', - mon.dyn2stat) + data_handling.dump_hdf5(self.path_output + 'dyn2stat_' + self.job_name + '.hdf5', mon.dyn2stat) logging.info('--> Done in {}.'.format(seconds2string(time.time() - t_start))) def run_main_multiprocessing(self): @@ -231,80 +236,83 @@ def run_main_multiprocessing(self): This function organizes the processing of multiple load cases via MPI. Parallelization is achieved using a worker/master concept. The workers and the master communicate via tags ('ready', 'start', 'done', 'exit'). - This concept is adapted from Jörg Bornschein (see https://github.com/jbornschein/mpi4py-examples/blob/master/09-task-pull.py) + This concept is adapted from Jörg Bornschein (see https://github.com/jbornschein/mpi4py-examples/blob/master/ + 09-task-pull.py) """ - logging.info('--> Starting Main in multiprocessing mode for %d trimcase(s).' % len(self.jcl.trimcase)) + logging.info( + '--> Starting Main in multiprocessing mode for %d trimcase(s).', len(self.jcl.trimcase)) t_start = time.time() - model = data_handling.load_hdf5(self.path_output + 'model_' + self.job_name + '.hdf5') + model = data_handling.load_hdf5( + self.path_output + 'model_' + self.job_name + '.hdf5') # MPI tags can be any integer values tags = {'ready': 0, 'start': 1, - 'done' : 2, - 'exit' : 3, - } + 'done': 2, + 'exit': 3, + } # The master process runs on the first processor if self.myid == 0: n_workers = self.comm.Get_size() - 1 - logging.info('--> I am the master with %d worker(s).' % n_workers) - + logging.info('--> I am the master with %d worker(s).', n_workers) + mon = gather_loads.GatherLoads(self.jcl, model) # open response fid = data_handling.open_hdf5(self.path_output + 'response_' + self.job_name + '.hdf5') - + closed_workers = 0 i_subcase = 0 - + while closed_workers < n_workers: logging.debug('Master is waiting for communication...') data = self.comm.recv(source=MPI.ANY_SOURCE, tag=MPI.ANY_TAG, status=self.status) source = self.status.Get_source() tag = self.status.Get_tag() - + if tag == tags['ready']: # Worker is ready, send out a new subcase. if i_subcase < len(self.jcl.trimcase): self.comm.send(i_subcase, dest=source, tag=tags['start']) - logging.info('--> Sending case %d of %d to worker %d' % (i_subcase+1, len(self.jcl.trimcase), source)) + logging.info('--> Sending case %d of %d to worker %d', i_subcase + 1, len(self.jcl.trimcase), source) i_subcase += 1 else: # No more task to do, send the exit signal. self.comm.send(None, dest=source, tag=tags['exit']) - + elif tag == tags['done']: # The worker has returned a response. response = data if response['successful']: - logging.info("--> Received response ('successful') from worker %d." % source) + logging.info("--> Received response ('successful') from worker %d.", source) mon.gather_monstations(self.jcl.trimcase[response['i']], response) mon.gather_dyn2stat(response) else: # Trim failed, no post processing, save the empty response - logging.info("--> Received response ('failed') from worker %d." % source) + logging.info("--> Received response ('failed') from worker %d.", source) logging.info('--> Saving response(s).') - data_handling.write_hdf5(fid, response, path='/'+str(response['i'])) - + data_handling.write_hdf5(fid, response, path='/' + str(response['i'])) + elif tag == tags['exit']: # The worker confirms the exit. - logging.debug('Worker %d exited.' % source) + logging.debug('Worker %d exited.', source) closed_workers += 1 # close response data_handling.close_hdf5(fid) logging.info('--> Saving monstation(s).') data_handling.dump_hdf5(self.path_output + 'monstations_' + self.job_name + '.hdf5', - mon.monstations) - # with open(path_output + 'monstations_' + job_name + '.mat', 'wb') as f: - # io_matlab.save_mat(f, mon.monstations) + mon.monstations) + logging.info('--> Saving dyn2stat.') data_handling.dump_hdf5(self.path_output + 'dyn2stat_' + self.job_name + '.hdf5', - mon.dyn2stat) + mon.dyn2stat) # The worker process runs on all other processors else: - logging.info('I am worker on process %d.' % self.myid) + logging.info('I am worker on process %d.', self.myid) while True: self.comm.send(None, dest=0, tag=tags['ready']) - i_subcase = self.comm.recv(source=0, tag=MPI.ANY_TAG, status=self.status) + i_subcase = self.comm.recv( + source=0, tag=MPI.ANY_TAG, status=self.status) tag = self.status.Get_tag() - + if tag == tags['start']: # Start a new job response = self.main_common(model, self.jcl, i_subcase) @@ -314,7 +322,7 @@ def run_main_multiprocessing(self): break # Confirm the exit signal. self.comm.send(None, dest=0, tag=tags['exit']) - + logging.info('--> Done in {}.'.format(seconds2string(time.time() - t_start))) def run_post(self): @@ -338,15 +346,17 @@ def run_post(self): elif 'limit_turbulence' in self.jcl.simcase[0] and self.jcl.simcase[0]['limit_turbulence']: plt = plotting_standard.TurbulencePlots(self.jcl, model) plt.add_monstations(monstations) - plt.plot_monstations(self.path_output + 'monstations_turbulence_' + self.job_name + '.pdf') + plt.plot_monstations( + self.path_output + 'monstations_turbulence_' + self.job_name + '.pdf') else: # Here come the loads plots plt = plotting_standard.LoadPlots(self.jcl, model) plt.add_monstations(monstations) plt.add_responses(responses) plt.plot_monstations(self.path_output + 'monstations_' + self.job_name + '.pdf') - if any([x in self.jcl.simcase[0] and self.jcl.simcase[0][x] for x in ['gust', 'turbulence', 'cs_signal', 'controller']]): - plt.plot_monstations_time(self.path_output + 'monstations_time_' + self.job_name + '.pdf') # nur sim + if any([x in self.jcl.simcase[0] and self.jcl.simcase[0][x] for x in ['gust', 'turbulence', + 'cs_signal', 'controller']]): + plt.plot_monstations_time(self.path_output + 'monstations_time_' + self.job_name + '.pdf') # nur sim logging.info('--> Saving auxiliary output data.') aux_out = auxiliary_output.AuxiliaryOutput(self.jcl, model, self.jcl.trimcase) @@ -358,31 +368,27 @@ def run_post(self): aux_out.write_critical_nodalloads(self.path_output + 'nodalloads_' + self.job_name + '.bdf') else: aux_out.write_trimresults(self.path_output + 'trim_results_' + self.job_name + '.csv') - aux_out.write_successful_trimcases(self.path_output + 'successful_trimcases_' + self.job_name + '.csv', + aux_out.write_successful_trimcases(self.path_output + 'successful_trimcases_' + self.job_name + '.csv', self.path_output + 'failed_trimcases_' + self.job_name + '.csv') aux_out.write_critical_trimcases(self.path_output + 'crit_trimcases_' + self.job_name + '.csv') aux_out.write_critical_nodalloads(self.path_output + 'nodalloads_' + self.job_name + '.bdf') # aux_out.write_all_nodalloads(self.path_output + 'nodalloads_all_' + self.job_name + '.bdf') # aux_out.save_nodaldefo(self.path_output + 'nodaldefo_' + self.job_name) # aux_out.save_cpacs(self.path_output + 'cpacs_' + self.job_name + '.xml') - return def run_test(self): """ This section shall be used for code that is not part of standard post-processing procedures. - Below, some useful plotting functions are given that might be helpful, for example - - to identify if a new model is working correctly + Below, some useful plotting functions are given that might be helpful, for example + - to identify if a new model is working correctly - to get extra time data plots - - to animate a time domain simulation + - to animate a time domain simulation """ - # Because the extra plotting functions require a graphical interface (actually mayavi does), - # the import is performed here to avoid unnecessary import failures e.g. on a cluster. - import loadskernel.plotting_extra as plotting_extra # Load the model and the response as usual model = data_handling.load_hdf5(self.path_output + 'model_' + self.job_name + '.hdf5') responses = data_handling.load_hdf5_responses(self.job_name, self.path_output) - - logging.info( '--> Drawing some more detailed plots.') + + logging.info('--> Drawing some more detailed plots.') plt = plotting_extra.DetailedPlots(self.jcl, model) plt.add_responses(responses) if 't_final' and 'dt' in self.jcl.simcase[0].keys(): @@ -390,7 +396,7 @@ def run_test(self): plt.plot_time_data() else: # show some plots of the force vectors, useful to identify model shortcomings - #plt.plot_pressure_distribution() + # plt.plot_pressure_distribution() plt.plot_forces_deformation_interactive() if 't_final' and 'dt' in self.jcl.simcase[0].keys(): @@ -399,7 +405,7 @@ def run_test(self): plt.add_responses(responses) plt.make_animation() # make a video file of the animation - #plt.make_movie(self.path_output, speedup_factor=1.0) + # plt.make_movie(self.path_output, speedup_factor=1.0) """ At the moment, I also use this section for custom analysis scripts. @@ -418,14 +424,13 @@ def run_test(self): # plot = plot_lift_distribution.Liftdistribution(self.jcl, model, responses) # plot.plot_aero_spanwise() - return class ClusterMode(Kernel): """ The cluster mode is similar to the (normal) sequential mode, but only ONE subcase is calculated. - This mode relies on a job scheduler that is able to run an array of jobs, where each the job is + This mode relies on a job scheduler that is able to run an array of jobs, where each the job is accompanied by an index i=0...n and n is the number of all subcases. - In a second step, when all jobs are finished, the results (e.g. one response per subcase) need to + In a second step, when all jobs are finished, the results (e.g. one response per subcase) need to be gathered. Example: k = program_flow.ClusterMode('jcl_name', ...) @@ -443,7 +448,7 @@ def run_cluster(self, i): logging.info('Starting Loads Kernel with job: ' + self.job_name) logging.info('User ' + getpass.getuser() + ' on ' + platform.node() + ' (' + platform.platform() + ')') logging.info('Cluster array mode') - + self.run_main_single(i) logging.info('Loads Kernel finished.') @@ -458,25 +463,27 @@ def run_main_single(self, i): model = data_handling.load_hdf5(self.path_output + 'model_' + self.job_name + '.hdf5') jcl = copy.deepcopy(self.jcl) """ - Before starting the simulation, dump an empty / dummy response. This is a workaround in case SU2 diverges, + Before starting the simulation, dump an empty / dummy response. This is a workaround in case SU2 diverges, which leads to a hard exit (via mpi_abort). In that case, mpi_abort terminates everything and leaves no - possibility to catch and handle an exception in Python. With this workaround, there is at least an empty - response so that the gathering and the post_processing will work properly. + possibility to catch and handle an exception in Python. With this workaround, there is at least an empty + response so that the gathering and the post_processing will work properly. """ # Create an empty response - empty_response = {'i':i, - 'successful':False} + empty_response = {'i': i, + 'successful': False} if self.myid == 0: - path_responses = data_handling.check_path(self.path_output+'responses/') - with open(path_responses + 'response_' + self.job_name + '_subcase_' + str(self.jcl.trimcase[i]['subcase']) + '.pickle', 'wb') as f: + path_responses = data_handling.check_path(self.path_output + 'responses/') + with open(path_responses + 'response_' + self.job_name + '_subcase_' + + str(self.jcl.trimcase[i]['subcase']) + '.pickle', 'wb') as f: data_handling.dump_pickle(empty_response, f) # Start the simulation response = self.main_common(model, jcl, i) # Overwrite the empty response from above if self.myid == 0: logging.info('--> Saving response(s).') - path_responses = data_handling.check_path(self.path_output+'responses/') - with open(path_responses + 'response_' + self.job_name + '_subcase_' + str(self.jcl.trimcase[i]['subcase']) + '.pickle', 'wb') as f: + path_responses = data_handling.check_path(self.path_output + 'responses/') + with open(path_responses + 'response_' + self.job_name + '_subcase_' + + str(self.jcl.trimcase[i]['subcase']) + '.pickle', 'wb') as f: data_handling.dump_pickle(response, f) logging.info('--> Done in {}.'.format(seconds2string(time.time() - t_start))) @@ -488,7 +495,7 @@ def gather_cluster(self): logging.info('cluster gather mode') self.jcl = data_handling.load_jcl(self.job_name, self.path_input, self.jcl) model = data_handling.load_hdf5(self.path_output + 'model_' + self.job_name + '.hdf5') - responses = data_handling.gather_responses(self.job_name, data_handling.check_path(self.path_output+'responses')) + responses = data_handling.gather_responses(self.job_name, data_handling.check_path(self.path_output + 'responses')) mon = gather_loads.GatherLoads(self.jcl, model) fid = data_handling.open_hdf5(self.path_output + 'response_' + self.job_name + '.hdf5') # open response for i in range(len(self.jcl.trimcase)): @@ -498,22 +505,24 @@ def gather_cluster(self): mon.gather_dyn2stat(response) logging.info('--> Saving response(s).') - data_handling.write_hdf5(fid, response, path='/'+str(response['i'])) + data_handling.write_hdf5(fid, response, path='/' + str(response['i'])) # close response data_handling.close_hdf5(fid) logging.info('--> Saving monstation(s).') data_handling.dump_hdf5(self.path_output + 'monstations_' + self.job_name + '.hdf5', - mon.monstations) + mon.monstations) logging.info('--> Saving dyn2stat.') data_handling.dump_hdf5(self.path_output + 'dyn2stat_' + self.job_name + '.hdf5', - mon.dyn2stat) - logging.info('--> Done in {}.'.format(seconds2string(time.time() - t_start))) + mon.dyn2stat) + logging.info( + '--> Done in {}.'.format(seconds2string(time.time() - t_start))) logging.info('Loads Kernel finished.') self.print_logo() + def str2bool(v): # This is a function outside the class to convert strings to boolean. Requirement for parsing command line arguments. if isinstance(v, bool): @@ -524,12 +533,14 @@ def str2bool(v): return False else: raise argparse.ArgumentTypeError('Boolean value expected.') - + + def seconds2string(seconds): m, s = divmod(seconds, 60) h, m = divmod(m, 60) return '{:n}:{:02n}:{:02n} [h:mm:ss]'.format(h, m, round(s)) + def command_line_interface(): parser = argparse.ArgumentParser() # register the most common arguments @@ -543,12 +554,10 @@ def command_line_interface(): args = parser.parse_args() # run the loads kernel with the command line arguments k = Kernel(job_name=args.job_name, pre=args.pre, main=args.main, post=args.post, - path_input=args.path_input, + path_input=args.path_input, path_output=args.path_output) k.run() - -if __name__ == "__main__": - command_line_interface() - +if __name__ == "__main__": + command_line_interface() diff --git a/loadskernel/solution_sequences.py b/loadskernel/solution_sequences.py index a53f0e5..2127db0 100755 --- a/loadskernel/solution_sequences.py +++ b/loadskernel/solution_sequences.py @@ -1,15 +1,16 @@ +import copy +import logging import numpy as np import scipy.optimize as so -import logging, copy from scipy.integrate import ode from loadskernel.integrate import RungeKutta4, ExplicitEuler, AdamsBashforth -from loadskernel.equations.steady import Steady +from loadskernel.equations.steady import Steady from loadskernel.equations.cfd import CfdSteady, CfdUnsteady from loadskernel.equations.nonlin_steady import NonlinSteady -from loadskernel.equations.unsteady import Unsteady -from loadskernel.equations.landing import Landing -from loadskernel.equations.common import ConvergenceError +from loadskernel.equations.unsteady import Unsteady +from loadskernel.equations.landing import Landing +from loadskernel.equations.common import ConvergenceError from loadskernel.equations.frequency_domain import GustExcitation from loadskernel.equations.frequency_domain import TurbulenceExcitation, LimitTurbulence from loadskernel.equations.frequency_domain import KMethod @@ -21,9 +22,10 @@ from loadskernel.cfd_interfaces.tau_interface import TauError from loadskernel.io_functions.data_handling import load_hdf5_dict + class SolutionSequences(TrimConditions): - - def approx_jacobian(self, X0,func,epsilon,dt): + + def approx_jacobian(self, X0, func, epsilon, dt): """ Approximate the Jacobian matrix of callable function func x - The state vector at which the Jacobian matrix is desired @@ -31,66 +33,67 @@ def approx_jacobian(self, X0,func,epsilon,dt): epsilon - The peturbation used to determine the partial derivatives """ X0 = np.asfarray(X0) - jac = np.zeros([len(func(*(X0,0.0, 'sim'))),len(X0)]) + jac = np.zeros([len(func(*(X0, 0.0, 'sim'))), len(X0)]) dX = np.zeros(len(X0)) for i in range(len(X0)): - f0 = func(*(X0,0.0, 'sim')) + f0 = func(*(X0, 0.0, 'sim')) dX[i] = epsilon - fi = func(*(X0+dX,0.0+dt, 'sim')) - jac[:,i] = (fi - f0)/epsilon + fi = func(*(X0 + dX, 0.0 + dt, 'sim')) + jac[:, i] = (fi - f0) / epsilon dX[i] = 0.0 return jac - + def calc_jacobian(self): """ - The Jacobian matrix is computed about the trimmed flight condition. - Alternatively, it may be computed about the trim condition specified in the JCL with: X0 = np.array(self.trimcond_X[:,2], dtype='float') + The Jacobian matrix is computed about the trimmed flight condition. + Alternatively, it may be computed about the trim condition specified in the JCL with + X0 = np.array(self.trimcond_X[:,2], dtype='float') """ - + if self.jcl.aero['method'] in ['mona_steady']: equations = Steady(self) else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) # flight condition - X0 = self.response['X'][0,:] - #X0 = np.array(self.trimcond_X[:,2], dtype='float') + X0 = self.response['X'][0, :] + # X0 = np.array(self.trimcond_X[:,2], dtype='float') logging.info('Calculating jacobian for ' + str(len(X0)) + ' variables...') - jac = self.approx_jacobian(X0=X0, func=equations.equations, epsilon=0.01, dt=1.0) # epsilon sollte klein sein, dt sollte 1.0s sein - self.response['X0'] = X0 # Linearisierungspunkt + # epsilon sollte klein sein, dt sollte 1.0s sein + jac = self.approx_jacobian(X0=X0, func=equations.equations, epsilon=0.01, dt=1.0) + self.response['X0'] = X0 # Linearisierungspunkt self.response['Y0'] = equations.equations(X0, t=0.0, modus='trim') self.response['jac'] = jac - self.response['states'] = self.states[:,0].tolist() - self.response['state_derivatives'] = self.state_derivatives[:,0].tolist() - self.response['inputs'] = self.inputs[:,0].tolist() - self.response['outputs'] = self.outputs[:,0].tolist() + self.response['states'] = self.states[:, 0].tolist() + self.response['state_derivatives'] = self.state_derivatives[:, 0].tolist() + self.response['inputs'] = self.inputs[:, 0].tolist() + self.response['outputs'] = self.outputs[:, 0].tolist() # States need to be reordered into ABCD matrices! - # X = [ rbm, flex, command_cs, lag_states ] + # X = [rbm, flex, command_cs, lag_states ] # Y = [drbm, dflex, dcommand_cs, dlag_states, outputs] # [Y] = [A B] * [X] - # [C D] + # [C D] idx_9dof = self.idx_states[3:12] idx_A = self.idx_states idx_B = self.idx_inputs idx_C = self.idx_outputs - self.response['9DOF'] = jac[idx_9dof,:][:,idx_9dof] # rigid body motion only - self.response['A'] = jac[idx_A,:][:,idx_A] # aircraft itself, including elastic states - self.response['B'] = jac[idx_A,:][:,idx_B] # reaction of aircraft on external excitation - self.response['C'] = jac[idx_C,:][:,idx_A] # sensors - self.response['D'] = jac[idx_C,:][:,idx_B] # reaction of sensors on external excitation + self.response['9DOF'] = jac[idx_9dof, :][:, idx_9dof] # rigid body motion only + self.response['A'] = jac[idx_A, :][:, idx_A] # aircraft itself, including elastic states + self.response['B'] = jac[idx_A, :][:, idx_B] # reaction of aircraft on external excitation + self.response['C'] = jac[idx_C, :][:, idx_A] # sensors + self.response['D'] = jac[idx_C, :][:, idx_B] # reaction of sensors on external excitation self.response['idx_A'] = idx_A self.response['idx_B'] = idx_B self.response['idx_C'] = idx_C self.response['desc'] = self.trimcase['desc'] - + # perform analysis on jacobian matrix equations = JacobiAnalysis(self.response) equations.eval_equations() - + def calc_derivatives(self): self.macgrid = load_hdf5_dict(self.model['macgrid']) - - + self.calc_flexible_derivatives() self.calc_rigid_derivatives() self.calc_additional_derivatives('rigid') @@ -100,19 +103,19 @@ def calc_derivatives(self): self.calc_NP() self.calc_cs_effectiveness() logging.info('--------------------------------------------------------------------------------------') - - def calc_rigid_derivatives(self): - if self.jcl.aero['method'] in [ 'mona_steady', 'mona_unsteady', 'hybrid']: + + def calc_rigid_derivatives(self): + if self.jcl.aero['method'] in ['mona_steady', 'mona_unsteady', 'hybrid']: equations = Steady(self) - elif self.jcl.aero['method'] in [ 'nonlin_steady']: + elif self.jcl.aero['method'] in ['nonlin_steady']: equations = NonlinSteady(self) else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) - + A = self.jcl.general['A_ref'] - delta = 0.01 - - X0 = np.array(self.trimcond_X[:,2], dtype='float') + delta = 0.01 + + X0 = np.array(self.trimcond_X[:, 2], dtype='float') response0 = equations.equations(X0, 0.0, 'trim_full_output') derivatives = [] logging.info('Calculating rigid derivatives...') @@ -120,63 +123,68 @@ def calc_rigid_derivatives(self): xi = copy.deepcopy(X0) xi[i] += delta response = equations.equations(xi, 0.0, 'trim_full_output') - Pmac_c = (response['Pmac']-response0['Pmac'])/response['q_dyn']/A/delta - derivatives.append([Pmac_c[0], Pmac_c[1], Pmac_c[2], Pmac_c[3]/self.macgrid['b_ref'], Pmac_c[4]/self.macgrid['c_ref'], Pmac_c[5]/self.macgrid['b_ref']]) + Pmac_c = (response['Pmac'] - response0['Pmac']) / response['q_dyn'] / A / delta + derivatives.append([Pmac_c[0], Pmac_c[1], Pmac_c[2], Pmac_c[3] / self.macgrid['b_ref'], + Pmac_c[4] / self.macgrid['c_ref'], Pmac_c[5] / self.macgrid['b_ref']]) # write back original response and store results - self.response['rigid_parameters'] = self.trimcond_X[:,0].tolist() + self.response['rigid_parameters'] = self.trimcond_X[:, 0].tolist() self.response['rigid_derivatives'] = derivatives - + def calc_flexible_derivatives(self): """ - The calculation of flexible derivatives is based on adding an increment (delta) to selected trim parameters + The calculation of flexible derivatives is based on adding an increment (delta) to selected trim parameters in the trim condition. Then, the trim solution is calculated for the modified parameters and subtracted from a baseline calculation (response0), leading to the flexible derivatives with respect to the modified parameter. """ - + if not self.trimcase['maneuver'] == 'derivatives': logging.warning("Please set 'maneuver' to 'derivatives' in your trimcase.") # save response a baseline response0 = self.response trimcond_X0 = copy.deepcopy(self.trimcond_X) - + vtas = self.trimcase['Ma'] * self.model['atmo'][self.trimcase['altitude']]['a'][()] A = self.jcl.general['A_ref'] - - delta = 0.01 + + delta = 0.01 parameters = ['theta', 'psi', 'p', 'q', 'r', 'command_xi', 'command_eta', 'command_zeta'] derivatives = [] logging.info('Calculating flexible derivatives...') for parameter in parameters: # modify selected parameter in trim conditions - self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == parameter))[0][0],2] += delta + self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == parameter))[0][0], 2] += delta if parameter == 'theta': - theta = self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == parameter))[0][0],2] - self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == 'u'))[0][0],2] = vtas*np.cos(theta) - self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == 'w'))[0][0],2] = vtas*np.sin(theta) + theta = self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == parameter))[0][0], 2] + self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == 'u'))[0][0], 2] = vtas * np.cos(theta) + self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == 'w'))[0][0], 2] = vtas * np.sin(theta) elif parameter == 'psi': - psi = self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == parameter))[0][0],2] - self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == 'u'))[0][0],2] = vtas*np.cos(psi) - self.trimcond_X[np.where((np.vstack((self.states , self.inputs))[:,0] == 'v'))[0][0],2] = vtas*np.sin(psi) + psi = self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == parameter))[0][0], 2] + self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == 'u'))[0][0], 2] = vtas * np.cos(psi) + self.trimcond_X[np.where((np.vstack((self.states, self.inputs))[:, 0] == 'v'))[0][0], 2] = vtas * np.sin(psi) # re-calculate new trim self.exec_trim() - Pmac_c = (self.response['Pmac']-response0['Pmac'])/response0['q_dyn']/A/delta - derivatives.append([Pmac_c[0,0], Pmac_c[0,1], Pmac_c[0,2], Pmac_c[0,3]/self.macgrid['b_ref'], Pmac_c[0,4]/self.macgrid['c_ref'], Pmac_c[0,5]/self.macgrid['b_ref']]) - # restore trim condition for next loop + Pmac_c = (self.response['Pmac'] - response0['Pmac']) / response0['q_dyn'] / A / delta + derivatives.append([Pmac_c[0, 0], Pmac_c[0, 1], Pmac_c[0, 2], Pmac_c[0, 3] / self.macgrid['b_ref'], + Pmac_c[0, 4] / self.macgrid['c_ref'], Pmac_c[0, 5] / self.macgrid['b_ref']]) + # restore trim condition for next loop self.trimcond_X = copy.deepcopy(trimcond_X0) # write back original response and store results self.response = response0 self.response['flexible_parameters'] = parameters self.response['flexible_derivatives'] = derivatives - + def calc_NP(self): pos = self.response['flexible_parameters'].index('theta') self.response['NP_flex'] = np.zeros(3) - self.response['NP_flex'][0] = self.macgrid['offset'][0,0] - self.jcl.general['c_ref'] * self.response['flexible_derivatives'][pos][4] / self.response['flexible_derivatives'][pos][2] - self.response['NP_flex'][1] = self.macgrid['offset'][0,1] + self.jcl.general['b_ref'] * self.response['flexible_derivatives'][pos][3] / self.response['flexible_derivatives'][pos][2] + self.response['NP_flex'][0] = self.macgrid['offset'][0, 0] - self.jcl.general['c_ref'] \ + * self.response['flexible_derivatives'][pos][4] / self.response['flexible_derivatives'][pos][2] + self.response['NP_flex'][1] = self.macgrid['offset'][0, 1] + self.jcl.general['b_ref'] \ + * self.response['flexible_derivatives'][pos][3] / self.response['flexible_derivatives'][pos][2] logging.info('--------------------------------------------------------------------------------------') logging.info('Aeroelastic neutral point / aerodynamic center:') - logging.info('NP_flex (x,y) = {:0.4g},{:0.4g}'.format(self.response['NP_flex'][0], self.response['NP_flex'][1])) - + logging.info('NP_flex (x,y) = {:0.4g},{:0.4g}'.format( + self.response['NP_flex'][0], self.response['NP_flex'][1])) + def calc_cs_effectiveness(self): logging.info('--------------------------------------------------------------------------------------') logging.info('Aeroelastic control surface effectiveness:') @@ -184,14 +192,16 @@ def calc_cs_effectiveness(self): for p in ['command_xi', 'command_eta', 'command_zeta']: pos_rigid = self.response['rigid_parameters'].index(p) pos_flex = self.response['flexible_parameters'].index(p) - d = np.array(self.response['flexible_derivatives'][pos_flex]) / np.array(self.response['rigid_derivatives'][pos_rigid]) - tmp = '{:>20} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g}'.format( p, d[0], d[1], d[2], d[3], d[4], d[5] ) + d = np.array(self.response['flexible_derivatives'][pos_flex]) \ + / np.array(self.response['rigid_derivatives'][pos_rigid]) + tmp = '{:>20} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g}'.format( + p, d[0], d[1], d[2], d[3], d[4], d[5]) logging.info(tmp) def calc_additional_derivatives(self, key): # key: 'rigid' or 'flexible' """ - Achtung beim Vergleichen mit Nastran: Bei Nick-, Roll- und Gierderivativa ist die Skalierung der Raten + Achtung beim Vergleichen mit Nastran: Bei Nick-, Roll- und Gierderivativa ist die Skalierung der Raten von Nastran sehr gewöhnungsbedürftig! Zum Beispiel: q * c_ref / (2 * V) = PITCH p * b_ref / (2 * V) = ROLL @@ -199,18 +209,24 @@ def calc_additional_derivatives(self, key): """ vtas = self.trimcase['Ma'] * self.model['atmo'][self.trimcase['altitude']]['a'][()] - self.response[key+'_parameters'] += ['p*', 'q*', 'r*'] - self.response[key+'_derivatives'].append(list(np.array(self.response[key+'_derivatives'][self.response[key+'_parameters'].index('p')]) / self.jcl.general['b_ref'] * 2.0 * vtas)) - self.response[key+'_derivatives'].append(list(np.array(self.response[key+'_derivatives'][self.response[key+'_parameters'].index('q')]) / self.jcl.general['c_ref'] * 2.0 * vtas)) - self.response[key+'_derivatives'].append(list(np.array(self.response[key+'_derivatives'][self.response[key+'_parameters'].index('r')]) / self.jcl.general['b_ref'] * 2.0 * vtas)) - + self.response[key + '_parameters'] += ['p*', 'q*', 'r*'] + self.response[key + '_derivatives'].append(list(np.array( + self.response[key + '_derivatives'][self.response[key + '_parameters'].index('p')]) + / self.jcl.general['b_ref'] * 2.0 * vtas)) + self.response[key + '_derivatives'].append(list(np.array( + self.response[key + '_derivatives'][self.response[key + '_parameters'].index('q')]) + / self.jcl.general['c_ref'] * 2.0 * vtas)) + self.response[key + '_derivatives'].append(list(np.array( + self.response[key + '_derivatives'][self.response[key + '_parameters'].index('r')]) + / self.jcl.general['b_ref'] * 2.0 * vtas)) + def print_derivatives(self, key): # print some information into log file # key: 'rigid' or 'flexible' - parameters = self.response[key+'_parameters'] - derivatives = self.response[key+'_derivatives'] + parameters = self.response[key + '_parameters'] + derivatives = self.response[key + '_derivatives'] logging.info('--------------------------------------------------------------------------------------') - logging.info('Calculated ' + key +' derivatives for ' + str(len(parameters)) + ' variables.') + logging.info('Calculated ' + key + ' derivatives for ' + str(len(parameters)) + ' variables.') logging.info('MAC_ref = {}'.format(self.jcl.general['MAC_ref'])) logging.info('A_ref = {}'.format(self.jcl.general['A_ref'])) logging.info('b_ref = {}'.format(self.jcl.general['b_ref'])) @@ -219,23 +235,24 @@ def print_derivatives(self, key): logging.info('Derivatives given in body axis (aft-right-up):') logging.info(' Cx Cy Cz Cmx Cmy Cmz') for p, d in zip(parameters, derivatives): - tmp = '{:>20} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g}'.format( p, d[0], d[1], d[2], d[3], d[4], d[5] ) + tmp = '{:>20} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g} {:< 10.4g}'.format( + p, d[0], d[1], d[2], d[3], d[4], d[5]) logging.info(tmp) - + def exec_trim(self): - if self.jcl.aero['method'] in [ 'mona_steady', 'mona_unsteady', 'hybrid', 'nonlin_steady', 'freq_dom']: + if self.jcl.aero['method'] in ['mona_steady', 'mona_unsteady', 'hybrid', 'nonlin_steady', 'freq_dom']: self.direct_trim() - elif self.jcl.aero['method'] in [ 'cfd_steady', 'cfd_unsteady']: + elif self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: self.iterative_trim() else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) if self.successful: - # To align the trim results with the time/frequency simulations, we expand the response by one dimension. - # Notation: (n_timesteps, n_dof) --> the trim results can be considered as the solution at time step zero. + # To align the trim results with the time/frequency simulations, we expand the response by one dimension. + # Notation: (n_timesteps, n_dof) --> the trim results can be considered as the solution at time step zero. # This saves a significant amount of lines of additional code in the post processing. for key in self.response.keys(): self.response[key] = np.expand_dims(self.response[key], axis=0) - + def direct_trim(self): # The purpose of HYBRD is to find a zero of a system of N non- # linear functions in N variables by a modification of the Powell @@ -243,28 +260,29 @@ def direct_trim(self): # lates the functions. The Jacobian is then calculated by a for- # ward-difference approximation. # http://www.math.utah.edu/software/minpack/minpack/hybrd.html - - if self.jcl.aero['method'] in [ 'mona_steady', 'mona_unsteady', 'hybrid', 'freq_dom'] and not hasattr(self.jcl, 'landinggear'): + + if self.jcl.aero['method'] in ['mona_steady', 'mona_unsteady', 'hybrid', + 'freq_dom'] and not hasattr(self.jcl, 'landinggear'): equations = Steady(self) - elif self.jcl.aero['method'] in [ 'nonlin_steady']: + elif self.jcl.aero['method'] in ['nonlin_steady']: equations = NonlinSteady(self) elif self.simcase['landinggear'] and self.jcl.landinggear['method'] in ['generic', 'skid']: equations = Landing(self) else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) - - xfree_0 = np.array(self.trimcond_X[:,2], dtype='float')[np.where((self.trimcond_X[:,1] == 'free'))[0]] - + + xfree_0 = np.array(self.trimcond_X[:, 2], dtype='float')[np.where((self.trimcond_X[:, 1] == 'free'))[0]] + if self.trimcase['maneuver'] == 'bypass': logging.info('Running bypass...') self.response = equations.eval_equations(xfree_0, time=0.0, modus='trim_full_output') self.successful = True else: logging.info('Running trim for ' + str(len(xfree_0)) + ' variables...') - xfree, info, status, msg= so.fsolve(equations.eval_equations, xfree_0, args=(0.0, 'trim'), full_output=True) + xfree, info, status, msg = so.fsolve(equations.eval_equations, xfree_0, args=(0.0, 'trim'), full_output=True) logging.info(msg) logging.debug('Function evaluations: ' + str(info['nfev'])) - + # no errors, check trim status for success if status == 1: # if trim was successful, then do one last evaluation with the final parameters. @@ -273,59 +291,65 @@ def direct_trim(self): else: self.response = {} self.successful = False - logging.warning('SolutionSequences failed for subcase {}. The SolutionSequences solver reports: {}'.format(self.trimcase['subcase'], msg)) + logging.warning('SolutionSequences failed for subcase {}. The SolutionSequences solver reports: {}'.format( + self.trimcase['subcase'], msg)) equations.finalize() return def iterative_trim(self): - if self.jcl.aero['method'] in [ 'mona_steady', 'mona_unsteady', 'hybrid']: + if self.jcl.aero['method'] in ['mona_steady', 'mona_unsteady', 'hybrid']: equations = Steady(self) - elif self.jcl.aero['method'] in [ 'cfd_steady', 'cfd_unsteady']: + elif self.jcl.aero['method'] in ['cfd_steady', 'cfd_unsteady']: equations = CfdSteady(self) else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) - + self.set_modal_states_fix() - xfree_0 = np.array(self.trimcond_X[:,2], dtype='float')[np.where((self.trimcond_X[:,1] == 'free'))[0]] # start trim from scratch - + # start trim from scratch + xfree_0 = np.array(self.trimcond_X[:, 2], dtype='float')[np.where((self.trimcond_X[:, 1] == 'free'))[0]] + if self.trimcase['maneuver'] == 'bypass': logging.info('running bypass...') self.response = equations.eval_equations(xfree_0, time=0.0, modus='trim_full_output') self.successful = True else: logging.info('Running trim for ' + str(len(xfree_0)) + ' variables...') + """ + Because the iterative trim is typically used in combination with CFD, some solver settings need to be modified. + - The jacobian matrix is constructed using finite differences. With CFD, a sufficiently large step size should + be used to obtain meaningful gradients (signal-to-noise ratio). This is controlled with parameter 'epsfcn=1.0e-3'. + - Because both the aerodynamic solution and the aero-structural coupling are iterative procedures, the residuals + add up and the tolerance of the trim solution has to be increased. This is controlled with parameter 'xtol=1.0e-3'. + - Approaching the trim point in small steps improves the robustness of the CFD solution. This is controlled with + parameter 'factor=0.1'. + """ try: - """ - Because the iterative trim is typically used in combination with CFD, some solver settings need to be modified. - - The jacobian matrix is constructed using finite differences. With CFD, a sufficiently large step size should - be used to obtain meaningful gradients (signal-to-noise ratio). This is controlled with parameter 'epsfcn=1.0e-3'. - - Because both the aerodynamic solution and the aero-structural coupling are iterative procedures, the residuals - add up and the tolerance of the trim solution has to be increased. This is controlled with parameter 'xtol=1.0e-3'. - - Approaching the trim point in small steps improves the robustness of the CFD solution. This is controlled with - parameter 'factor=0.1'. - """ - xfree, info, status, msg= so.fsolve(equations.eval_equations_iteratively, xfree_0, args=(0.0, 'trim'), - full_output=True, epsfcn=1.0e-3, xtol=1.0e-3, factor=0.1 ) + xfree, info, status, msg = so.fsolve(equations.eval_equations_iteratively, xfree_0, args=(0.0, 'trim'), + full_output=True, epsfcn=1.0e-3, xtol=1.0e-3, factor=0.1) except TauError as e: self.response = {} self.successful = False - logging.warning('SolutionSequences failed for subcase {} due to CFDError: {}'.format(self.trimcase['subcase'], e)) + logging.warning('SolutionSequences failed for subcase {} due to CFDError: {}'.format( + self.trimcase['subcase'], e)) except ConvergenceError as e: self.response = {} self.successful = False - logging.warning('SolutionSequences failed for subcase {} due to ConvergenceError: {}'.format(self.trimcase['subcase'], e)) + logging.warning('SolutionSequences failed for subcase {} due to ConvergenceError: {}'.format( + self.trimcase['subcase'], e)) else: logging.info(msg) logging.info('function evaluations: ' + str(info['nfev'])) # no errors, check trim status for success if status == 1: # if trim was successful, then do one last evaluation with the final parameters. - self.response = equations.eval_equations_iteratively(xfree, time=0.0, modus='trim_full_output') + self.response = equations.eval_equations_iteratively( + xfree, time=0.0, modus='trim_full_output') self.successful = True else: self.response = {} self.successful = False - logging.warning('SolutionSequences failed for subcase {}. The SolutionSequences solver reports: {}'.format(self.trimcase['subcase'], msg)) + logging.warning('SolutionSequences failed for subcase {}. The SolutionSequences solver reports: {}'.format( + self.trimcase['subcase'], msg)) equations.finalize() return @@ -337,59 +361,59 @@ def exec_sim(self): self.exec_sim_freq_dom() else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) - + def exec_sim_time_dom(self): """ - Select the right set of equations. + Select the right set of equations. If required, add new states, e.g. for the landing gear or unsteady aerodynamics. """ # get initial solution from trim - X0 = self.response['X'][0,:] + X0 = self.response['X'][0, :] # select solution sequence if self.jcl.aero['method'] in ['mona_steady', 'hybrid'] and not hasattr(self.jcl, 'landinggear'): equations = Steady(self, X0) - elif self.jcl.aero['method'] in [ 'nonlin_steady']: + elif self.jcl.aero['method'] in ['nonlin_steady']: equations = NonlinSteady(self, X0) elif self.simcase['landinggear'] and self.jcl.landinggear['method'] in ['generic', 'skid']: # add landing gear to system self.add_landinggear() # reset initial solution including new states - X0 = self.response['X'][0,:] + X0 = self.response['X'][0, :] equations = Landing(self, X0) elif self.jcl.aero['method'] in ['mona_unsteady']: if 'disturbance' in self.simcase.keys(): logging.info('Adding disturbance of {} to state(s) '.format(self.simcase['disturbance'])) - self.response['X'][0,11+self.simcase['disturbance_mode']] += self.simcase['disturbance'] + self.response['X'][0, 11 + self.simcase['disturbance_mode']] += self.simcase['disturbance'] # add lag states to system self.add_lagstates() # reset initial solution including new states - X0 = self.response['X'][0,:] + X0 = self.response['X'][0, :] equations = Unsteady(self, X0) - elif self.jcl.aero['method'] in [ 'cfd_unsteady']: + elif self.jcl.aero['method'] in ['cfd_unsteady']: equations = CfdUnsteady(self, X0) else: logging.error('Unknown aero method: ' + str(self.jcl.aero['method'])) - + """ - There are two ways of time intergartion. - + There are two ways of time intergartion. + In most cases, the Adams Bashforth method provided by scipy.integrate.ode is used: Advantages + Good accuracy controll by adaptive time step size + Tested by continuous integration chain with a long history of numerically equivalet results - Disadvantages - - Accepts only the derivative 'dy' and doesn't handle any additional outputs (like the response at the given time step). - This requires a second run at the selected time steps to obatin the full outputs / response dictionary. + Disadvantages + - Accepts only the derivative 'dy' and doesn't handle any additional outputs (like the response at the given time + step). This requires a second run at the selected time steps to obatin the full outputs / response dictionary. - Adaptive step size not suitable for CFD applications - + Self-implemented Adams Bashforth integration sheme: Advantages + Fixed time step size + Handles dictionary outputs of the ode functions - Disadvantages + Disadvantages - Not fully tested """ - + if 'dt_integration' in self.simcase: dt_integration = self.simcase['dt_integration'] else: @@ -398,39 +422,39 @@ def exec_sim_time_dom(self): t_final = self.simcase['t_final'] xt = [] t = [] - + logging.info('Running time simulation for ' + str(t_final) + ' sec...') if self.jcl.aero['method'] in ['cfd_unsteady']: integrator = self.select_integrator(equations, 'AdamsBashforth_FixedTimeStep', dt_integration) integrator.set_initial_value(X0, 0.0) - + while integrator.successful() and integrator.t < t_final: - integrator.integrate(integrator.t+dt) + integrator.integrate(integrator.t + dt) xt.append(integrator.y) t.append(integrator.t) - # To avoid an excessive amount of data, e.g. during unsteady cfd simulations, + # To avoid an excessive amount of data, e.g. during unsteady cfd simulations, # keep only the response data on the first mpi process (id = 0). if self.myid == 0: for key in integrator.output_dict.keys(): - self.response[key] = np.vstack((self.response[key],integrator.output_dict[key])) - - else: + self.response[key] = np.vstack((self.response[key], integrator.output_dict[key])) + + else: integrator = self.select_integrator(equations, 'AdamsBashforth') integrator.set_initial_value(X0, 0.0) - - while integrator.successful() and integrator.t < t_final: - integrator.integrate(integrator.t+dt) + + while integrator.successful() and integrator.t < t_final: + integrator.integrate(integrator.t + dt) xt.append(integrator.y) t.append(integrator.t) - + if integrator.successful(): logging.info('Simulation finished. Running (again) with full outputs at selected time steps...') equations.eval_equations(X0, 0.0, modus='sim_full_output') - for i_step in np.arange(0,len(t)): + for i_step in np.arange(0, len(t)): response_step = equations.eval_equations(xt[i_step], t[i_step], modus='sim_full_output') for key in response_step.keys(): - self.response[key] = np.vstack((self.response[key],response_step[key])) - + self.response[key] = np.vstack((self.response[key], response_step[key])) + # Handle unsucessful time integration if integrator.successful(): self.successful = True @@ -439,12 +463,12 @@ def exec_sim_time_dom(self): self.successful = False logging.warning('Integration failed!') return - + def select_integrator(self, equations, integration_scheme='AdamsBashforth', stepwidth=1e-4): """ Select an ode integration scheme: - - two methods from scipy.integrate.ode (Adams-Bashforth and RK45) with variable time step size and - - three own implementations (RK4, Euler and AdamsBashforth) with fixed time step size + - two methods from scipy.integrate.ode (Adams-Bashforth and RK45) with variable time step size and + - three own implementations (RK4, Euler and AdamsBashforth) with fixed time step size are available. Recommended: 'Adams-Bashforth' """ @@ -455,14 +479,16 @@ def select_integrator(self, equations, integration_scheme='AdamsBashforth', step elif integration_scheme == 'AdamsBashforth_FixedTimeStep': integrator = AdamsBashforth(equations.ode_arg_sorter).set_integrator(stepwidth) elif integration_scheme == 'AdamsBashforth': - integrator = ode(equations.ode_arg_sorter).set_integrator('vode', method='adams', nsteps=2000, rtol=1e-4, atol=1e-4, max_step=5e-4) # non-stiff: 'adams', stiff: 'bdf' + integrator = ode(equations.ode_arg_sorter).set_integrator('vode', method='adams', nsteps=2000, + rtol=1e-4, atol=1e-4, max_step=5e-4) elif integration_scheme == 'RK45': - integrator = ode(equations.ode_arg_sorter).set_integrator('dopri5', nsteps=2000, rtol=1e-2, atol=1e-8, max_step=1e-4) + integrator = ode(equations.ode_arg_sorter).set_integrator('dopri5', nsteps=2000, + rtol=1e-2, atol=1e-8, max_step=1e-4) return integrator - + def exec_sim_freq_dom(self): # get initial solution from trim - X0 = self.response['X'][0,:] + X0 = self.response['X'][0, :] # select solution sequence if self.simcase['gust']: equations = GustExcitation(self, X0) @@ -477,10 +503,10 @@ def exec_sim_freq_dom(self): self.response[key] = response_sim[key] + self.response[key] logging.info('Frequency domain simulation finished.') self.successful = True - + def exec_flutter(self): # get initial solution from trim - X0 = self.response['X'][0,:] + X0 = self.response['X'][0, :] # select solution sequence if self.simcase['flutter_para']['method'] == 'k': equations = KMethod(self, X0) @@ -495,4 +521,3 @@ def exec_flutter(self): for key in response_flutter.keys(): self.response[key] = response_flutter[key] self.successful = True - \ No newline at end of file diff --git a/loadskernel/solution_tools.py b/loadskernel/solution_tools.py index 3d65b1e..ce5ec92 100644 --- a/loadskernel/solution_tools.py +++ b/loadskernel/solution_tools.py @@ -1,127 +1,136 @@ # -*- coding: utf-8 -*- -""" -Created on Thu Nov 27 17:44:58 2014 - -@author: voss_ar -""" +import logging import numpy as np from loadskernel.atmosphere import isa as atmo_isa -import logging -def calc_drehmatrix_angular( phi=0.0, theta=0.0, psi=0.0 ): + +def calc_drehmatrix_angular(phi=0.0, theta=0.0, psi=0.0): # Alle Winkel in [rad] ! # geo to body - drehmatrix = np.array(([1., 0. , -np.sin(theta)], [0., np.cos(phi), np.sin(phi)*np.cos(theta)], [0., -np.sin(phi), np.cos(phi)*np.cos(theta)])) + drehmatrix = np.array(([1., 0., -np.sin(theta)], + [0., np.cos(phi), np.sin(phi) * np.cos(theta)], + [0., -np.sin(phi), np.cos(phi) * np.cos(theta)])) return drehmatrix -def calc_drehmatrix_angular_inv( phi=0.0, theta=0.0, psi=0.0 ): + +def calc_drehmatrix_angular_inv(phi=0.0, theta=0.0, psi=0.0): # Alle Winkel in [rad] ! # body to geo - drehmatrix = np.array(([1., np.sin(phi)*np.tan(theta), np.cos(phi)*np.tan(theta)], [0., np.cos(phi), -np.sin(phi)], [0., np.sin(phi)/np.cos(theta), np.cos(phi)/np.cos(theta)])) + drehmatrix = np.array(([1., np.sin(phi) * np.tan(theta), np.cos(phi) * np.tan(theta)], + [0., np.cos(phi), -np.sin(phi)], + [0., np.sin(phi) / np.cos(theta), np.cos(phi) / np.cos(theta)])) return drehmatrix -def calc_drehmatrix( phi=0.0, theta=0.0, psi=0.0 ): + +def calc_drehmatrix(phi=0.0, theta=0.0, psi=0.0): # Alle Winkel in [rad] ! - drehmatrix_phi = np.array(([1., 0. , 0.], [0., np.cos(phi), np.sin(phi)], [0., -np.sin(phi), np.cos(phi)])) - drehmatrix_theta = np.array(([np.cos(theta), 0, -np.sin(theta)], [0, 1, 0], [np.sin(theta), 0, np.cos(theta)])) - drehematrix_psi = np.array(([np.cos(psi), np.sin(psi), 0], [-np.sin(psi), np.cos(psi), 0],[0, 0, 1])) - drehmatrix = np.dot(np.dot(drehmatrix_phi, drehmatrix_theta),drehematrix_psi) + drehmatrix_phi = np.array(([1., 0., 0.], [0., np.cos(phi), np.sin(phi)], [0., -np.sin(phi), np.cos(phi)])) + drehmatrix_theta = np.array(([np.cos(theta), 0, -np.sin(theta)], [0, 1, 0], [np.sin(theta), 0, np.cos(theta)])) + drehematrix_psi = np.array(([np.cos(psi), np.sin(psi), 0], [-np.sin(psi), np.cos(psi), 0], [0, 0, 1])) + drehmatrix = np.dot(np.dot(drehmatrix_phi, drehmatrix_theta), drehematrix_psi) return drehmatrix + def gravitation_on_earth(PHInorm_cg, Tgeo2body): - g = np.array([0.0, 0.0, 9.8066]) # erdfest, geodetic - g_cg = np.dot(PHInorm_cg[0:3,0:3], np.dot(Tgeo2body[0:3,0:3],g)) # bodyfixed + g = np.array([0.0, 0.0, 9.8066]) # erdfest, geodetic + g_cg = np.dot(PHInorm_cg[0:3, 0:3], np.dot(Tgeo2body[0:3, 0:3], g)) # bodyfixed return g_cg + def design_gust_cs_25_341(simcase, atmo, V): # Gust Calculation from CS 25.341 (a) # adapted from matlab-script by Vega Handojo, DLR-AE-LAE, 2015 # convert (possible) integer to float - gust_gradient = float(simcase['gust_gradient']) # Gust gradient / half length - altitude = float(atmo['h']) # Altitude - rho = float(atmo['rho']) # Air density - V = float(V) # Speed - V_D = float(atmo['a'] * simcase['gust_para']['MD']) # Design Dive speed - + gust_gradient = float(simcase['gust_gradient']) # Gust gradient / half length + altitude = float(atmo['h']) # Altitude + rho = float(atmo['rho']) # Air density + V = float(V) # Speed + V_D = float(atmo['a'] * simcase['gust_para']['MD']) # Design Dive speed + _, rho0, _, _ = atmo_isa(0.0) # Check if flight alleviation factor fg is provided by user as input, else calculate fg according to CS 25.341(a)(6) if 'Fg' in simcase['gust_para']: fg = float(simcase['gust_para']['Fg']) else: - Z_mo = float(simcase['gust_para']['Z_mo']) # Maximum operating altitude - MLW = float(simcase['gust_para']['MLW']) # Maximum Landing Weight - MTOW = float(simcase['gust_para']['MTOW']) # Maximum Take-Off Weight - MZFW = float(simcase['gust_para']['MZFW']) # Maximum Zero Fuel Weight + Z_mo = float(simcase['gust_para']['Z_mo']) # Maximum operating altitude + MLW = float(simcase['gust_para']['MLW']) # Maximum Landing Weight + MTOW = float(simcase['gust_para']['MTOW']) # Maximum Take-Off Weight + MZFW = float(simcase['gust_para']['MZFW']) # Maximum Zero Fuel Weight fg = calc_fg(altitude, Z_mo, MLW, MTOW, MZFW) - logging.info('CS25_Uds is set up with flight profile alleviation factor Fg = {}'.format(fg)) - + logging.info( + 'CS25_Uds is set up with flight profile alleviation factor Fg = {}'.format(fg)) + # reference gust velocity (EAS) [m/s] if altitude <= 4572: - u_ref = 17.07-(17.07-13.41)*altitude/4572.0 + u_ref = 17.07 - (17.07 - 13.41) * altitude / 4572.0 else: - u_ref = 13.41-(13.41-6.36)*((altitude-4572.0)/(18288.0-4572.0)) + u_ref = 13.41 - (13.41 - 6.36) * ((altitude - 4572.0) / (18288.0 - 4572.0)) if V == V_D: - u_ref = u_ref/2.0 + u_ref = u_ref / 2.0 # design gust velocity (EAS) - u_ds = u_ref * fg * (gust_gradient/107.0)**(1.0/6.0) - v_gust = u_ds * (rho0/rho)**0.5 #in TAS - + u_ds = u_ref * fg * (gust_gradient / 107.0) ** (1.0 / 6.0) + v_gust = u_ds * (rho0 / rho) ** 0.5 # in TAS + # parameters for Nastran cards - WG_TAS = v_gust/V #TAS/TAS - + WG_TAS = v_gust / V # TAS/TAS + return WG_TAS, u_ds, v_gust + def turbulence_cs_25_341(simcase, atmo, V): # Turbulence Calculation from CS 25.341 (b) - + # convert (possible) integers to floats - altitude = float(atmo['h']) # Altitude - Z_mo = float(simcase['gust_para']['Z_mo']) # Maximum operating altitude - V = float(V) # Speed - V_C = float(atmo['a'] * simcase['gust_para']['MC']) # Design Cruise speed - V_D = float(atmo['a'] * simcase['gust_para']['MD']) # Design Dive speed - MLW = float(simcase['gust_para']['MLW']) # Maximum Landing Weight - MTOW = float(simcase['gust_para']['MTOW']) # Maximum Take-Off Weight - MZFW = float(simcase['gust_para']['MZFW']) # Maximum Zero Fuel Weight - + altitude = float(atmo['h']) # Altitude + Z_mo = float(simcase['gust_para']['Z_mo']) # Maximum operating altitude + V = float(V) # Speed + V_C = float(atmo['a'] * simcase['gust_para']['MC']) # Design Cruise speed + V_D = float(atmo['a'] * simcase['gust_para']['MD']) # Design Dive speed + MLW = float(simcase['gust_para']['MLW']) # Maximum Landing Weight + MTOW = float(simcase['gust_para']['MTOW']) # Maximum Take-Off Weight + MZFW = float(simcase['gust_para']['MZFW']) # Maximum Zero Fuel Weight + fg = calc_fg(altitude, Z_mo, MLW, MTOW, MZFW) - + # reference turbulence intensity (TAS) [m/s] # CS 25.341 (b)(3): U_sigma_ref is the reference turbulence intensity that... if altitude <= 7315.0: - # ...varies linearly with altitude from 27.43m/s (90 ft/s) (TAS) at sea level to 24.08 m/s (79 ft/s) (TAS) at 7315 m (24000 ft) - u_ref = 27.43-(27.43-24.08)*altitude/7315.0 + # ...varies linearly with altitude from 27.43m/s (90 ft/s) (TAS) at sea level + # to 24.08 m/s (79 ft/s) (TAS) at 7315 m (24000 ft) + u_ref = 27.43 - (27.43 - 24.08) * altitude / 7315.0 else: # ...and is then constant at 24.08 m/s (79 ft/s) (TAS) up to the altitude of 18288 m (60000 ft) u_ref = 24.08 - + # limit turbulence intensity (TAS) [m/s] - u_sigma = u_ref * fg - + u_sigma = u_ref * fg + # At speed VD: U_sigma is equal to 1/2 the values and # At speeds between VC and VD: U_sigma is equal to a value obtained by linear interpolation. if V > V_C: - u_sigma = u_sigma * (1.0 - 0.5*(V-V_C)/(V_D-V_C)) - + u_sigma = u_sigma * (1.0 - 0.5 * (V - V_C) / (V_D - V_C)) + return u_sigma + def calc_fg(altitude, Z_mo, MLW, MTOW, MZFW): # calculate the flight profile alleviation factor as given in CS 25.341 (a)(6) - - R1 = MLW/MTOW - R2 = MZFW/MTOW - f_gm = (R2*np.tan(np.pi*R1/4))**0.5 - f_gz = 1.0-Z_mo/76200.0 + + R1 = MLW / MTOW + R2 = MZFW / MTOW + f_gm = (R2 * np.tan(np.pi * R1 / 4)) ** 0.5 + f_gz = 1.0 - Z_mo / 76200.0 # At sea level, the flight profile alleviation factor is determined by - fg_sl = 0.5*(f_gz+f_gm) - # The flight profile alleviation factor, Fg, must be increased linearly from the sea level value to a value of 1.0 at the maximum operating altitude + fg_sl = 0.5 * (f_gz + f_gm) + # The flight profile alleviation factor, Fg, must be increased linearly from the sea level value + # to a value of 1.0 at the maximum operating altitude if altitude == 0.0: fg = fg_sl elif altitude == Z_mo: fg = 1.0 else: - fg = fg_sl + (1.0-fg_sl)*altitude/Z_mo + fg = fg_sl + (1.0 - fg_sl) * altitude / Z_mo return fg diff --git a/loadskernel/spline_functions.py b/loadskernel/spline_functions.py index 3fef85a..16360e3 100644 --- a/loadskernel/spline_functions.py +++ b/loadskernel/spline_functions.py @@ -1,26 +1,29 @@ +import logging +import time import numpy as np import scipy from scipy import sparse as sp -import time, logging from loadskernel.io_functions.read_mona import nastran_number_converter from loadskernel import grid_trafo from loadskernel.utils.sparse_matrices import insert_coo, insert_lil + def spline_nastran(filename, strcgrid, aerogrid): - logging.info('Read Nastran spline (PARAM OPGTKG 1) from {}'.format(filename)) + logging.info( + 'Read Nastran spline (PARAM OPGTKG 1) from {}'.format(filename)) with open(filename, 'r') as fid: lines = fid.readlines() i_line = 0 - PHI = np.zeros((aerogrid['n']*6, strcgrid['n']*6)) + PHI = np.zeros((aerogrid['n'] * 6, strcgrid['n'] * 6)) for line in lines: i_line += 1 - if str.find(str.replace(line,' ',''),'GPJK') != -1: + if str.find(str.replace(line, ' ', ''), 'GPJK') != -1: i_line += 3 break - - while str.find(str.replace(lines[i_line],' ',''),'COLUMN') != -1: - #print lines[i_line] + + while str.find(str.replace(lines[i_line], ' ', ''), 'COLUMN') != -1: + # print lines[i_line] line_split = lines[i_line].split() if line_split[3].split('-')[1][:2] == 'T3': tmp = 2 @@ -28,14 +31,14 @@ def spline_nastran(filename, strcgrid, aerogrid): tmp = 4 else: logging.error('DOF not implemented!') - col = aerogrid['set_k'][np.where(int(line_split[3].split('-')[0]) == aerogrid['ID'])[0][0],tmp] - + col = aerogrid['set_k'][np.where(int(line_split[3].split('-')[0]) == aerogrid['ID'])[0][0], tmp] + i_line += 1 while True: if lines[i_line] == '\n' or lines[i_line][0] == '1': i_line += 1 break - + line_split = lines[i_line].split() while len(line_split) >= 3: if line_split[1] == 'T1': @@ -51,28 +54,30 @@ def spline_nastran(filename, strcgrid, aerogrid): elif line_split[1] == 'R3': tmp = 5 else: - logging.error('DOF not implemented!') - row = strcgrid['set'][np.where(int(line_split[0]) == strcgrid['ID'])[0][0],tmp] - PHI[col,row] = nastran_number_converter(line_split[2], 'float') - + logging.error('DOF not implemented!') + row = strcgrid['set'][np.where(int(line_split[0]) == strcgrid['ID'])[0][0], tmp] + PHI[col, row] = nastran_number_converter(line_split[2], 'float') + line_split = line_split[3:] i_line += 1 - + return PHI -def spline_rbf(grid_i, set_i, grid_d, set_d, + +def spline_rbf(grid_i, set_i, grid_d, set_d, rbf_type='tps', surface_spline=False, dimensions='', support_radius=2.0): """ This is a convenience function that wraps the Spline_rbf class and returns the spline matrix PHI. """ - rbf = Spline_rbf(grid_i['offset'+set_i].T, grid_d['offset'+set_d].T, rbf_type, surface_spline, support_radius) + rbf = Spline_rbf(grid_i['offset' + set_i].T, grid_d['offset' + set_d].T, rbf_type, surface_spline, support_radius) rbf.build_M() rbf.build_BC() rbf.build_splinematrix() - rbf.expand_splinematrix(grid_i, set_i, grid_d, set_d, dimensions) + rbf.expand_splinematrix(grid_i, set_i, grid_d, set_d, dimensions) return rbf.PHI_expanded + class Spline_rbf: def __init__(self, nodes_fe, nodes_cfd, rbf_type, surface_spline, support_radius): @@ -89,8 +94,8 @@ def __init__(self, nodes_fe, nodes_cfd, rbf_type, surface_spline, support_radius self.R = support_radius if self.surface_spline: logging.debug('Using surface formulation (2D xy surface)') - self.nodes_cfd = nodes_cfd[0:2,:] - self.nodes_fe = nodes_fe[0:2,:] + self.nodes_cfd = nodes_cfd[0:2, :] + self.nodes_fe = nodes_fe[0:2, :] self.n = 3 else: logging.debug('Using volume formulation (3D)') @@ -99,52 +104,51 @@ def __init__(self, nodes_fe, nodes_cfd, rbf_type, surface_spline, support_radius self.n = 4 self.n_fe = nodes_fe.shape[1] self.n_cfd = self.nodes_cfd.shape[1] - logging.debug('Splining (rbf) of {:.0f} points to {:.0f} points...'.format(self.n_cfd , self.n_fe)) + logging.debug('Splining (rbf) of {:.0f} points to {:.0f} points...'.format(self.n_cfd, self.n_fe)) def build_M(self): # Nomenklatur nach Neumann & Krueger logging.debug(' - building M') - self.A = np.vstack((np.ones(self.n_fe),self.nodes_fe)) + self.A = np.vstack((np.ones(self.n_fe), self.nodes_fe)) phi = np.zeros((self.n_fe, self.n_fe)) for i in range(self.n_fe): - #for j in range(i+1): - #r_ij = np.linalg.norm(self.nodes_fe[:,i] - self.nodes_fe[:,j]) - r_ii_vec = self.nodes_fe[:,:i+1] - np.tile(self.nodes_fe[:,i],(i+1,1)).T - r_ii = np.sum(r_ii_vec**2, axis=0)**0.5 + r_ii_vec = self.nodes_fe[:, :i + 1] - np.tile(self.nodes_fe[:, i], (i + 1, 1)).T + r_ii = np.sum(r_ii_vec ** 2, axis=0) ** 0.5 rbf_values = self.eval_rbf(r_ii) - phi[i,:i+1] = rbf_values - phi[:i+1,i] = rbf_values - + phi[i, :i + 1] = rbf_values + phi[:i + 1, i] = rbf_values + # Gleichungssystem aufstellen und loesen # M * ab = rechte_seite # 0 A * a = 0 # A' phi b y - + # linke Seite basteln - M1 = np.hstack((np.zeros((self.n,self.n)) , self.A)) - M2 = np.hstack(( self.A.transpose(), phi)) + M1 = np.hstack((np.zeros((self.n, self.n)), self.A)) + M2 = np.hstack((self.A.transpose(), phi)) self.M = np.vstack((M1, M2)) def build_BC(self): # Nomenklatur nach Neumann & Krueger logging.debug(' - building B and C') - B = np.vstack((np.ones(self.n_cfd),self.nodes_cfd)) + B = np.vstack((np.ones(self.n_cfd), self.nodes_cfd)) C = np.zeros((self.n_fe, self.n_cfd)) for i in range(self.n_fe): - r_ij_vec = np.tile(self.nodes_fe[:,i], (self.n_cfd,1)).T - self.nodes_cfd[:,:] - r_ij = np.sum(r_ij_vec**2, axis=0)**0.5 + r_ij_vec = np.tile( + self.nodes_fe[:, i], (self.n_cfd, 1)).T - self.nodes_cfd[:, :] + r_ij = np.sum(r_ij_vec ** 2, axis=0) ** 0.5 rbf_values = self.eval_rbf(r_ij) - C[i,:] = rbf_values + C[i, :] = rbf_values self.BC = np.vstack((B, C)) - + def build_splinematrix(self): """ Now that the system of equations is set-up, we can work on the solution. - Note 1: Instead of calculating the inverse of M, we solve the system, which + Note 1: Instead of calculating the inverse of M, we solve the system, which can be faster. - Note 2: In PyCSM_CouplingMatrix_3D.py by Jens Neumann a slightly different - approach is used by first reducing the system size (M * E) and the solving - the system. However, this requires two more matrix multiplications and showed + Note 2: In PyCSM_CouplingMatrix_3D.py by Jens Neumann a slightly different + approach is used by first reducing the system size (M * E) and the solving + the system. However, this requires two more matrix multiplications and showed no advantages in terms of speed, so I decided to stay with my formulation. Note 3: The results are numerically equivalent (using np.allclose(self.PHI, self.G)) to results obtained with PyCSM_CouplingMatrix_3D.py. @@ -152,133 +156,137 @@ def build_splinematrix(self): # solve the system t_start = time.time() logging.debug(' - solving M*H=BC for H') - H = scipy.linalg.solve(self.M, self.BC).T + H = scipy.linalg.solve(self.M, self.BC).T logging.debug(' - done in {:.2f} sec'.format(time.time() - t_start)) # finally, extract the complete splining matrix PHI from H - self.PHI = H[:,self.n:] - - def expand_splinematrix(self, grid_i, set_i, grid_d, set_d, dimensions): + self.PHI = H[:, self.n:] + + def expand_splinematrix(self, grid_i, set_i, grid_d, set_d, dimensions): """ This functions does three things: a) The spline matrix applies to all three translational degrees of freedom. - b) The size of the splining matrix is expanded as one might want the matrix to be - bigger than actually needed. One example might be the multiplication of the (smaller) + b) The size of the splining matrix is expanded as one might want the matrix to be + bigger than actually needed. One example might be the multiplication of the (smaller) x2grid with the (larger) AIC matrix. - c) Because the spline matrix can contain many zeros, a sparse matrix might be a better - choice compared to a full numpy array. + c) Because the spline matrix can contain many zeros, a sparse matrix might be a better + choice compared to a full numpy array. """ if dimensions != '' and len(dimensions) == 2: dimensions_i = dimensions[0] dimensions_d = dimensions[1] else: - dimensions_i = 6*len(grid_i['set'+set_i]) - dimensions_d = 6*len(grid_d['set'+set_d]) + dimensions_i = 6 * len(grid_i['set' + set_i]) + dimensions_d = 6 * len(grid_d['set' + set_d]) t_start = time.time() - logging.debug(' - expanding spline matrix to {:.0f} DOFs and {:.0f} DOFs...'.format(dimensions_d , dimensions_i)) + logging.debug( + ' - expanding spline matrix to {:.0f} DOFs and {:.0f} DOFs...'.format(dimensions_d, dimensions_i)) # coo sparse matrices are good for inserting new data - PHI_coo = sp.coo_matrix((dimensions_d , dimensions_i)) - PHI_coo = insert_coo(PHI_coo, self.PHI, grid_d['set'+set_d][:,0], grid_i['set'+set_i][:,0]) - PHI_coo = insert_coo(PHI_coo, self.PHI, grid_d['set'+set_d][:,1], grid_i['set'+set_i][:,1]) - PHI_coo = insert_coo(PHI_coo, self.PHI, grid_d['set'+set_d][:,2], grid_i['set'+set_i][:,2]) + PHI_coo = sp.coo_matrix((dimensions_d, dimensions_i)) + PHI_coo = insert_coo(PHI_coo, self.PHI, grid_d['set' + set_d][:, 0], grid_i['set' + set_i][:, 0]) + PHI_coo = insert_coo(PHI_coo, self.PHI, grid_d['set' + set_d][:, 1], grid_i['set' + set_i][:, 1]) + PHI_coo = insert_coo(PHI_coo, self.PHI, grid_d['set' + set_d][:, 2], grid_i['set' + set_i][:, 2]) # better sparse format than coo - self.PHI_expanded = PHI_coo.tocsc() + self.PHI_expanded = PHI_coo.tocsc() logging.debug(' - done in {:.2f} sec'.format(time.time() - t_start)) - + def eval_rbf(self, r): - + if self.rbf_type == 'linear': return r - - if self.rbf_type == 'tps': + + elif self.rbf_type == 'tps': """ - See Harder, R. L., and Desmarais, R. N., “Interpolation using - surface splines.,” Journal of Aircraft, vol. 9, no. 2, pp. 189–191, + See Harder, R. L., and Desmarais, R. N., “Interpolation using + surface splines.,” Journal of Aircraft, vol. 9, no. 2, pp. 189–191, Feb. 1972, https://doi.org/10.2514/3.44330. """ - rbf = r**2 * np.log(r**2) + rbf = r ** 2 * np.log(r ** 2) # Fix singularity when r = 0.0 - rbf[r==0.0] = 0.0 + rbf[r == 0.0] = 0.0 return rbf - - if self.rbf_type == 'wendland1': + + elif self.rbf_type == 'wendland1': """ - See section 4.1.2 in Neumann, J., “Identifikation radialer Basisfunktionen zur - räumlichen Strömungs-Struktur-Kopplung unter Berücksichtigung des Interpolations- - und des Lasttransformationsfehlers,” Institute of Aeroelasticity, Göttingen, Germany, + See section 4.1.2 in Neumann, J., “Identifikation radialer Basisfunktionen zur + räumlichen Strömungs-Struktur-Kopplung unter Berücksichtigung des Interpolations- + und des Lasttransformationsfehlers,” Institute of Aeroelasticity, Göttingen, Germany, Internal Report DLR IB 232-2008 J 01, 2008. """ rbf = np.zeros(r.shape) pos = r <= self.R - rbf[pos] = (1.0-r[pos]/self.R)**2 + rbf[pos] = (1.0 - r[pos] / self.R) ** 2 return rbf - - if self.rbf_type == 'wendland2': + + elif self.rbf_type == 'wendland2': rbf = np.zeros(r.shape) pos = r <= self.R - rbf[pos] = (1.0-r[pos]/self.R)**4 * (4.0*r[pos]/self.R + 1.0) + rbf[pos] = (1.0 - r[pos] / self.R) ** 4 * (4.0 * r[pos] / self.R + 1.0) return rbf - + else: logging.error('Unkown Radial Basis Function!') -def spline_rb(grid_i, set_i, grid_d, set_d, splinerules, coord, dimensions='', sparse_output=False): - + +def spline_rb(grid_i, set_i, grid_d, set_d, splinerules, coord, dimensions='', sparse_output=False): + # Here, the size of the splining matrix is determined. One might want the matrix to be bigger than actually needed. # One example might be the multiplication of the (smaller) x2grid with the (larger) AIC matrix. if dimensions != '' and len(dimensions) == 2: dimensions_i = dimensions[0] dimensions_d = dimensions[1] else: - dimensions_i = 6*len(grid_i['set'+set_i]) - dimensions_d = 6*len(grid_d['set'+set_d]) - logging.info('Splining (rigid body) of {:.0f} DOFs to {:.0f} DOFs...'.format(dimensions_d , dimensions_i)) - + dimensions_i = 6 * len(grid_i['set' + set_i]) + dimensions_d = 6 * len(grid_d['set' + set_d]) + logging.info('Splining (rigid body) of {:.0f} DOFs to {:.0f} DOFs...'.format(dimensions_d, dimensions_i)) + # transfer points into common coord offset_dest_i = [] for i_point in range(len(grid_i['ID'])): pos_coord = coord['ID'].index(grid_i['CP'][i_point]) - offset_dest_i.append(np.dot(coord['dircos'][pos_coord],grid_i['offset'+set_i][i_point])+coord['offset'][pos_coord]) + offset_dest_i.append(np.dot(coord['dircos'][pos_coord], + grid_i['offset' + set_i][i_point]) + coord['offset'][pos_coord]) offset_dest_i = np.array(offset_dest_i) - + offset_dest_d = [] for i_point in range(len(grid_d['ID'])): pos_coord = coord['ID'].index(grid_d['CP'][i_point]) - offset_dest_d.append(np.dot(coord['dircos'][pos_coord],grid_d['offset'+set_d][i_point])+coord['offset'][pos_coord]) + offset_dest_d.append(np.dot(coord['dircos'][pos_coord], + grid_d['offset' + set_d][i_point]) + coord['offset'][pos_coord]) offset_dest_d = np.array(offset_dest_d) - + # T_i and T_d are the translation matrices that do the projection to the coordinate systems of gird_i and grid_d - T_i, T_d = grid_trafo.calc_transformation_matrix(coord, + T_i, T_d = grid_trafo.calc_transformation_matrix(coord, grid_i, set_i, 'CP', - grid_d, set_d, 'CP', + grid_d, set_d, 'CP', dimensions) - + # In matrix T_di the actual splining of gird_d to grid_i according as defined in splinerules is done. # Actually, this is the part that implements the rigid body spline - # The part above should be generic for different splines and could/should be moved to a different function - T_di = sp.lil_matrix( (dimensions_d, dimensions_i) ) + # The part above should be generic for different splines and could/should be moved to a different function + T_di = sp.lil_matrix((dimensions_d, dimensions_i)) for ID_i in splinerules: for ID_d in splinerules[ID_i]: try: - position_i = np.where(grid_i['ID']==ID_i)[0][0] - position_d = np.where(grid_d['ID']==ID_d)[0][0] - except: - raise AssertionError('There is a problem at monitoring station {}, grid {}'.format(ID_i, ID_d)) - + position_i = np.where(grid_i['ID'] == ID_i)[0][0] + position_d = np.where(grid_d['ID'] == ID_d)[0][0] + except Exception as exc: + raise AssertionError('There is a problem at monitoring station {}, grid {}'.format(ID_i, ID_d)) from exc + T_sub = np.eye(6) # Kraefte erzeugen zusaetzliche Momente durch Hebelarm 'r' - # M = r cross F = sk(r)*F - r = offset_dest_d[position_d] - offset_dest_i[position_i] - T_sub[0,4] = r[2] - T_sub[0,5] = -r[1] - T_sub[1,3] = -r[2] - T_sub[1,5] = r[0] - T_sub[2,3] = r[1] - T_sub[2,4] = -r[0] - T_di = insert_lil(T_di, T_sub, grid_d['set'+set_d][position_d,0:6], grid_i['set'+set_i][position_i,0:6]) - + # M = r cross F = sk(r)*F + r = offset_dest_d[position_d] - offset_dest_i[position_i] + T_sub[0, 4] = r[2] + T_sub[0, 5] = -r[1] + T_sub[1, 3] = -r[2] + T_sub[1, 5] = r[0] + T_sub[2, 3] = r[1] + T_sub[2, 4] = -r[0] + T_di = insert_lil(T_di, T_sub, grid_d['set' + set_d][position_d, 0:6], grid_i['set' + set_i][position_i, 0:6]) + splinematrix = T_d.transpose().dot(T_di).dot(T_i) if sparse_output: - return splinematrix.tocsc() # better sparse format than coo_matrix - else: - return splinematrix.toarray() + # better sparse format than coo_matrix + return splinematrix.tocsc() + return splinematrix.toarray() diff --git a/loadskernel/spline_rules.py b/loadskernel/spline_rules.py index f8f82e4..91cf485 100644 --- a/loadskernel/spline_rules.py +++ b/loadskernel/spline_rules.py @@ -5,32 +5,37 @@ import loadskernel.io_functions.read_mona as read_mona """ -Spline rules describe how a rigid body spline is constructed. The mapping of dependent to +Spline rules describe how a rigid body spline is constructed. The mapping of dependent to independent grids is stored in a dictionary with the following nomenclature: splinerules = {'ID of independent grid': [list of dependent grid IDs]} """ -def nearest_neighbour(grid_i, set_i, grid_d, set_d): - logging.info('Searching nearest neighbour of {:.0f} dependent nodes in {:.0f} independent nodes...'.format(len(grid_d['ID']) , len(grid_i['ID']))) + +def nearest_neighbour(grid_i, set_i, grid_d, set_d): + logging.info('Searching nearest neighbour of {:.0f} dependent nodes in {:.0f} independent nodes...'.format( + len(grid_d['ID']), len(grid_i['ID']))) splinerules = {} for i_d in range(grid_d['n']): - dist = np.sum((grid_i['offset'+set_i] - grid_d['offset'+set_d][i_d])**2, axis=1)**0.5 + dist = np.sum( + (grid_i['offset' + set_i] - grid_d['offset' + set_d][i_d]) ** 2, axis=1) ** 0.5 neighbour = grid_i['ID'][dist.argmin()] if neighbour not in splinerules: splinerules[neighbour] = [grid_d['ID'][i_d]] else: splinerules[neighbour] += [grid_d['ID'][i_d]] - + return splinerules + def rules_point(grid_i, grid_d): # All dependent grids are mapped to one grid point, which might be CG or MAC # Assumption: the relevant point is expected to be the only/first point in the independet grid splinerules = {} splinerules[int(grid_i['ID'])] = list(grid_d['ID']) return splinerules - + + def rules_aeropanel(aerogrid): # Map every point k to its corresponding point j splinerules = {} @@ -38,21 +43,26 @@ def rules_aeropanel(aerogrid): splinerules[id] = [id] return splinerules + def monstations_from_bdf(mongrid, filenames): if mongrid['n'] != len(filenames): - logging.error('Number of Stations in mongrid ({:.0f}) and number of bdfs ({:.0f}) unequal!'.format(mongrid['n'], len(filenames))) + logging.error('Number of Stations in mongrid ({:.0f}) and number of bdfs ({:.0f}) unequal!'.format( + mongrid['n'], len(filenames))) splinerules = {} for i_station in range(mongrid['n']): - tmp = read_mona.Modgen_GRID(filenames[i_station]) + tmp = read_mona.Modgen_GRID(filenames[i_station]) splinerules[mongrid['ID'][i_station]] = list(tmp['ID']) return splinerules + def monstations_from_aecomp(mongrid, aecomp, sets): splinerules = {} for i_station in range(mongrid['n']): - i_aecomp = aecomp['name'].index( mongrid['comp'][i_station]) - i_sets = [sets['values'][sets['ID'].index(x)] for x in aecomp['list_id'][i_aecomp]] + i_aecomp = aecomp['name'].index(mongrid['comp'][i_station]) + i_sets = [sets['values'][sets['ID'].index( + x)] for x in aecomp['list_id'][i_aecomp]] # combine the IDs in case multiple sets are given by one AECOMP card - splinerules[mongrid['ID'][i_station]] = np.unique(np.concatenate(i_sets).ravel()).tolist() + splinerules[mongrid['ID'][i_station]] = np.unique( + np.concatenate(i_sets).ravel()).tolist() return splinerules diff --git a/loadskernel/trim_conditions.py b/loadskernel/trim_conditions.py index 30718b7..099cda8 100644 --- a/loadskernel/trim_conditions.py +++ b/loadskernel/trim_conditions.py @@ -3,16 +3,18 @@ from loadskernel.io_functions.data_handling import load_hdf5_dict from loadskernel.cfd_interfaces.mpi_helper import setup_mpi + class TrimConditions: + def __init__(self, model, jcl, trimcase, simcase): self.model = model self.jcl = jcl self.trimcase = trimcase - self.simcase = simcase - + self.simcase = simcase + self.response = None self.successful = None - + self.states = None self.inputs = None self.state_derivatives = None @@ -21,7 +23,7 @@ def __init__(self, model, jcl, trimcase, simcase): self.lg_states = None self.lg_derivatives = None self.lag_states = None - + self.trimcond_X = None self.trimcond_Y = None @@ -32,7 +34,7 @@ def __init__(self, model, jcl, trimcase, simcase): self.n_outputs = None self.n_lg_states = None self.n_lag_states = None - + self.idx_states = None self.idx_inputs = None self.idx_state_derivatives = None @@ -42,9 +44,9 @@ def __init__(self, model, jcl, trimcase, simcase): self.idx_lg_derivatives = None self.idx_lag_states = None self.idx_lag_derivatives = None - + # Initialize MPI interface - self.have_mpi, self.comm, self.status, self.myid = setup_mpi() + self.have_mpi, self.comm, self.status, self.myid = setup_mpi() def set_trimcond(self): # set states, derivatives, inputs and output parameters according to requested maneuver @@ -54,27 +56,30 @@ def set_trimcond(self): self.add_stabilizer_setting() self.add_flap_setting() # append inputs to X vector... - self.trimcond_X = np.vstack((self.states , self.inputs)) - self.n_states = self.states.__len__() - self.n_inputs = self.inputs.__len__() - self.idx_states = list(range(0,self.n_states)) - self.idx_inputs = list(range(self.n_states, self.n_states+self.n_inputs)) - + self.trimcond_X = np.vstack((self.states, self.inputs)) + self.n_states = len(self.states) + self.n_inputs = len(self.inputs) + self.idx_states = list(range(0, self.n_states)) + self.idx_inputs = list( + range(self.n_states, self.n_states + self.n_inputs)) + # ... and input derivatives and outputs to Y vector self.trimcond_Y = np.vstack((self.state_derivatives, self.input_derivatives, self.outputs)) - self.n_state_derivatives = self.state_derivatives.__len__() - self.n_input_derivatives = self.input_derivatives.__len__() - self.n_outputs = self.outputs.__len__() - self.idx_state_derivatives = list(range(0,self.n_state_derivatives)) - self.idx_input_derivatives = list(range(self.n_state_derivatives, self.n_state_derivatives+self.n_input_derivatives)) - self.idx_outputs = list(range(self.n_state_derivatives+self.n_input_derivatives, self.n_state_derivatives+self.n_input_derivatives+self.n_outputs)) + self.n_state_derivatives = len(self.state_derivatives) + self.n_input_derivatives = len(self.input_derivatives) + self.n_outputs = len(self.outputs) + self.idx_state_derivatives = list(range(0, self.n_state_derivatives)) + self.idx_input_derivatives = list(range( + self.n_state_derivatives, self.n_state_derivatives + self.n_input_derivatives)) + self.idx_outputs = list(range(self.n_state_derivatives + self.n_input_derivatives, self.n_state_derivatives + + self.n_input_derivatives + self.n_outputs)) def set_defaults(self): """ The trim condition is a mix of strings and numeric values. This is possible with np.array of 'object' type. - However, object-arrays have some disadvantages, such as lack of compatibility with mathematical operations - such as np.cross() or HDF5 files, but present the trim conditions in a nice and compact form. For the model - equations, the numerical values are converted into np.arrays of 'float' type. + However, object-arrays have some disadvantages, such as lack of compatibility with mathematical operations + such as np.cross() or HDF5 files, but present the trim conditions in a nice and compact form. For the model + equations, the numerical values are converted into np.arrays of 'float' type. (This is much better than the old methods of converting the numerical values to 'string' type arrays.) Requirement for a determined trim condition: 'free' parameters in trimcond_X == 'target' parameters in trimcond_Y @@ -83,347 +88,362 @@ def set_defaults(self): self.atmo = load_hdf5_dict(self.model['atmo'][self.trimcase['altitude']]) self.n_modes = self.model['mass'][self.trimcase['mass']]['n_modes'][()] vtas = self.trimcase['Ma'] * self.atmo['a'] - theta = 0.0/180.0*np.pi # starting with a small angle of attack increases the performance and convergence of the CFD solution - u = vtas*np.cos(theta) - w = vtas*np.sin(theta) + # starting with a small angle of attack increases the performance and convergence of the CFD solution + theta = 0.0 / 180.0 * np.pi + u = vtas * np.cos(theta) + w = vtas * np.sin(theta) z = -self.atmo['h'] - + # Default trim conditions in case maneuver = ''. # right hand side - self.states = np.array([ - ['x', 'fix', 0.0,], - ['y', 'fix', 0.0,], - ['z', 'fix', z ,], - ['phi', 'fix', 0.0,], - ['theta', 'free', theta,], # dependent on u and w if dz = 0 - ['psi', 'fix', 0.0,], - ['u', 'free', u, ], - ['v', 'fix', 0.0,], - ['w', 'free', w,], - ['p', 'fix', self.trimcase['p'],], - ['q', 'fix', self.trimcase['q'],], - ['r', 'fix', self.trimcase['r'],], - ], dtype=object) - for i_mode in range(1, self.n_modes+1): - self.states = np.vstack((self.states , np.array(['Uf'+str(i_mode), 'free', 0.0], dtype=object))) - for i_mode in range(1, self.n_modes+1): - self.states = np.vstack((self.states , np.array(['dUf_dt'+str(i_mode), 'fix', 0.0], dtype=object))) - - self.inputs = np.array([ - ['command_xi', 'free', 0.0], - ['command_eta', 'free', 0.0], - ['command_zeta', 'free', 0.0], - ['thrust', 'fix', 0.0], - ['stabilizer', 'fix', 0.0], - ['flap_setting', 'fix', 0.0], - ], dtype=object) - + self.states = np.array( + [['x', 'fix', 0.0], + ['y', 'fix', 0.0], + ['z', 'fix', z], + ['phi', 'fix', 0.0], + ['theta', 'free', theta], # dependent on u and w if dz = 0 + ['psi', 'fix', 0.0], + ['u', 'free', u], + ['v', 'fix', 0.0], + ['w', 'free', w], + ['p', 'fix', self.trimcase['p']], + ['q', 'fix', self.trimcase['q']], + ['r', 'fix', self.trimcase['r']], + ], dtype=object) + for i_mode in range(1, self.n_modes + 1): + self.states = np.vstack((self.states, np.array( + ['Uf' + str(i_mode), 'free', 0.0], dtype=object))) + for i_mode in range(1, self.n_modes + 1): + self.states = np.vstack((self.states, np.array( + ['dUf_dt' + str(i_mode), 'fix', 0.0], dtype=object))) + + self.inputs = np.array( + [['command_xi', 'free', 0.0], + ['command_eta', 'free', 0.0], + ['command_zeta', 'free', 0.0], + ['thrust', 'fix', 0.0], + ['stabilizer', 'fix', 0.0], + ['flap_setting', 'fix', 0.0], + ], dtype=object) + # left hand side - self.state_derivatives = np.array([ - ['dx', 'target', vtas,], # dx = vtas if dz = 0 - ['dy', 'free', 0.0,], - ['dz', 'target', 0.0,], - ['dphi', 'free', 0.0,], - ['dtheta', 'free', 0.0,], - ['dpsi', 'free', 0.0,], - ['du', 'free', 0.0,], - ['dv', 'free', 0.0,], - ['dw', 'free', 0.0,], - ['dp', 'target', self.trimcase['pdot'],], - ['dq', 'target', self.trimcase['qdot'],], - ['dr', 'target', self.trimcase['rdot'],], - ], dtype=object) - - for i_mode in range(1, self.n_modes+1): - self.state_derivatives = np.vstack((self.state_derivatives , np.array(['dUf_dt'+str(i_mode), 'fix', 0.0], dtype=object))) - for i_mode in range(1, self.n_modes+1): - self.state_derivatives = np.vstack((self.state_derivatives , np.array(['d2Uf_d2t'+str(i_mode), 'target', 0.0], dtype=object))) - - self.input_derivatives = np.array([ - ['dcommand_xi', 'fix', 0.0], - ['dcommand_eta', 'fix', 0.0], - ['dcommand_zeta', 'fix', 0.0], - ['dthrust', 'fix', 0.0], - ['dstabilizer', 'fix', 0.0], - ['dflap_setting', 'fix', 0.0], - ], dtype=object) - - self.outputs = np.array([ - ['Nz', 'target', self.trimcase['Nz']], - ['Vtas', 'free', vtas,], - ['beta', 'free', 0.0] - ], dtype=object) - + self.state_derivatives = np.array( + [['dx', 'target', vtas], # dx = vtas if dz = 0 + ['dy', 'free', 0.0], + ['dz', 'target', 0.0], + ['dphi', 'free', 0.0], + ['dtheta', 'free', 0.0], + ['dpsi', 'free', 0.0], + ['du', 'free', 0.0], + ['dv', 'free', 0.0], + ['dw', 'free', 0.0], + ['dp', 'target', self.trimcase['pdot']], + ['dq', 'target', self.trimcase['qdot']], + ['dr', 'target', self.trimcase['rdot']], + ], dtype=object) + + for i_mode in range(1, self.n_modes + 1): + self.state_derivatives = np.vstack((self.state_derivatives, + np.array(['dUf_dt' + str(i_mode), 'fix', 0.0], dtype=object))) + for i_mode in range(1, self.n_modes + 1): + self.state_derivatives = np.vstack((self.state_derivatives, + np.array(['d2Uf_d2t' + str(i_mode), 'target', 0.0], dtype=object))) + + self.input_derivatives = np.array( + [['dcommand_xi', 'fix', 0.0], + ['dcommand_eta', 'fix', 0.0], + ['dcommand_zeta', 'fix', 0.0], + ['dthrust', 'fix', 0.0], + ['dstabilizer', 'fix', 0.0], + ['dflap_setting', 'fix', 0.0], + ], dtype=object) + + self.outputs = np.array( + [['Nz', 'target', self.trimcase['Nz']], + ['Vtas', 'free', vtas, ], + ['beta', 'free', 0.0] + ], dtype=object) + def set_maneuver(self): - # Trim about pitch axis only + """ + Long list of if-statements can be replace by match-case in the future (with python 3.10), see + https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching + Until then, the Flake8 warning C901 'TrimConditions.set_maneuver' is too complex (16) is squelched because I see no + other way to do the selection. + """ + + # Trim about pitch axis only if self.trimcase['maneuver'] in ['pitch', 'elevator']: logging.info('Setting trim conditions to "pitch"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim about pitch axis only, but with stabilizer elif self.trimcase['maneuver'] in ['stabilizer']: logging.info('Setting trim conditions to "stabilizer"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'stabilizer'))[0][0],1] = 'free' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'stabilizer'))[0][0], 1] = 'free' # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim about pitch and roll axis, no yaw elif self.trimcase['maneuver'] == 'pitch&roll': logging.info('Setting trim conditions to "pitch&roll"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim about pitch and yaw axis, no roll elif self.trimcase['maneuver'] == 'pitch&yaw': logging.info('Setting trim conditions to "pitch&yaw"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + # Trim conditin for level landing at a given sink rate 'dz' elif self.trimcase['maneuver'] in ['L1wheel', 'L2wheel']: logging.info('Setting trim conditions to "level landing"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dx'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dz'))[0][0],2] = self.trimcase['dz'] - self.outputs[np.where((self.outputs[:,0] == 'Vtas'))[0][0],1] = 'target' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dx'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dz'))[0][0], 2] = self.trimcase['dz'] + self.outputs[np.where((self.outputs[:, 0] == 'Vtas'))[0][0], 1] = 'target' # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim condition with prescribed angle theta, e.g. for 3 wheel landing elif self.trimcase['maneuver'] in ['L3wheel']: logging.info('Setting trim conditions to "3 wheel landing"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dx'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dz'))[0][0],2] = self.trimcase['dz'] - self.states[np.where((self.states[:,0] == 'theta'))[0][0],1] = 'target' - self.states[np.where((self.states[:,0] == 'theta'))[0][0],2] = self.trimcase['theta'] + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dx'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dz'))[0][0], 2] = self.trimcase['dz'] + self.states[np.where((self.states[:, 0] == 'theta'))[0][0], 1] = 'target' + self.states[np.where((self.states[:, 0] == 'theta'))[0][0], 2] = self.trimcase['theta'] # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - self.outputs[np.where((self.outputs[:,0] == 'Nz'))[0][0],1] = 'free' - self.outputs[np.where((self.outputs[:,0] == 'Vtas'))[0][0],1] = 'target' - - # Trim condition for a glider. Sink rate 'w' is allowed so that the velocity remains constant (du=0.0) + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + self.outputs[np.where((self.outputs[:, 0] == 'Nz'))[0][0], 1] = 'free' + self.outputs[np.where((self.outputs[:, 0] == 'Vtas'))[0][0], 1] = 'target' + + # Trim condition for a glider. Sink rate 'w' is allowed so that the velocity remains constant (du=0.0) elif self.trimcase['maneuver'] == 'segelflug': logging.info('Setting trim conditions to "segelflug"') - # inputs + # inputs # without changes # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dx'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dz'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'du'))[0][0],1] = 'target' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dw'))[0][0],1] = 'target' - self.outputs[np.where((self.outputs[:,0] == 'Nz'))[0][0],1] = 'free' - self.outputs[np.where((self.outputs[:,0] == 'Vtas'))[0][0],1] = 'target' - + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dx'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dz'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'du'))[0][0], 1] = 'target' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dw'))[0][0], 1] = 'target' + self.outputs[np.where((self.outputs[:, 0] == 'Nz'))[0][0], 1] = 'free' + self.outputs[np.where((self.outputs[:, 0] == 'Vtas'))[0][0], 1] = 'target' + # Trim conditions with alpha only, e.g. for Pratt formula elif self.trimcase['maneuver'] == 'pratt': logging.info('Setting trim conditions to "pratt"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dq'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - - # Trim condition with prescribed control surface deflections Xi and Zeta + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dq'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + + # Trim condition with prescribed control surface deflections Xi and Zeta elif self.trimcase['maneuver'] == 'Xi&Zeta-fixed': logging.info('Setting trim conditions to "Xi&Zeta-fixed"') - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],2] = self.trimcase['command_xi'] - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],2] = self.trimcase['command_zeta'] - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 2] = self.trimcase['command_xi'] + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 2] = self.trimcase['command_zeta'] + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim condition with prescribed control surface deflections, accelerations free elif self.trimcase['maneuver'] == 'CS-fixed': logging.info('Setting trim conditions to "CS-fixed"') - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],2] = self.trimcase['command_xi'] - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],2] = self.trimcase['command_eta'] - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],2] = self.trimcase['command_zeta'] - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dq'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 2] = self.trimcase['command_xi'] + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 2] = self.trimcase['command_eta'] + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 2] = self.trimcase['command_zeta'] + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dq'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim condition with prescribed control surface deflections, roll, pitch and yaw rates free elif self.trimcase['maneuver'] == 'CS&Acc-fixed': logging.info('Setting trim conditions to "CS&Acc-fixed"') - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],2] = self.trimcase['command_xi'] - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],2] = self.trimcase['command_eta'] - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],2] = self.trimcase['command_zeta'] - self.states[np.where((self.states[:,0] == 'p'))[0][0],1] = 'free' - self.states[np.where((self.states[:,0] == 'q'))[0][0],1] = 'free' - self.states[np.where((self.states[:,0] == 'r'))[0][0],1] = 'free' - + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 2] = self.trimcase['command_xi'] + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 2] = self.trimcase['command_eta'] + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 2] = self.trimcase['command_zeta'] + self.states[np.where((self.states[:, 0] == 'p'))[0][0], 1] = 'free' + self.states[np.where((self.states[:, 0] == 'q'))[0][0], 1] = 'free' + self.states[np.where((self.states[:, 0] == 'r'))[0][0], 1] = 'free' + # Trim condition that allows sideslip elif self.trimcase['maneuver'] == 'sideslip': logging.info('Setting trim conditions to "sideslip"') # fixed roll and yaw control - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],2] = self.trimcase['command_xi'] - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],2] = self.trimcase['command_zeta'] - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 2] = self.trimcase['command_xi'] + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 2] = self.trimcase['command_zeta'] + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # set sideslip condition - self.states[np.where((self.states[:,0] == 'psi'))[0][0],1] = 'free' - self.states[np.where((self.states[:,0] == 'v'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dy'))[0][0],1] = 'target' - self.outputs[np.where((self.outputs[:,0] == 'beta'))[0][0],1] = 'target' - self.outputs[np.where((self.outputs[:,0] == 'beta'))[0][0],2] = self.trimcase['beta'] - + self.states[np.where((self.states[:, 0] == 'psi'))[0][0], 1] = 'free' + self.states[np.where((self.states[:, 0] == 'v'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dy'))[0][0], 1] = 'target' + self.outputs[np.where((self.outputs[:, 0] == 'beta'))[0][0], 1] = 'target' + self.outputs[np.where((self.outputs[:, 0] == 'beta'))[0][0], 2] = self.trimcase['beta'] + # Trim condition for a coordinated sideslip at a given angle beta elif self.trimcase['maneuver'] == 'coordinated_sideslip': logging.info('Setting trim conditions to "sideslip"') - + # set sideslip condition - self.states[np.where((self.states[:,0] == 'psi'))[0][0],1] = 'free' - self.states[np.where((self.states[:,0] == 'v'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dy'))[0][0],1] = 'target' - self.outputs[np.where((self.outputs[:,0] == 'beta'))[0][0],1] = 'target' - self.outputs[np.where((self.outputs[:,0] == 'beta'))[0][0],2] = self.trimcase['beta'] - + self.states[np.where((self.states[:, 0] == 'psi'))[0][0], 1] = 'free' + self.states[np.where((self.states[:, 0] == 'v'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dy'))[0][0], 1] = 'target' + self.outputs[np.where((self.outputs[:, 0] == 'beta'))[0][0], 1] = 'target' + self.outputs[np.where((self.outputs[:, 0] == 'beta'))[0][0], 2] = self.trimcase['beta'] + # Trim condition for no trim / bypass with prescribed euler angles and control surface deflections. - # Used for bypass analyses and for debugging. + # Used for bypass analyses and for debugging. elif self.trimcase['maneuver'] in ['bypass', 'derivatives']: logging.info('Setting trim conditions to "bypass"') vtas = self.trimcase['Ma'] * self.atmo['a'] theta = self.trimcase['theta'] - u = vtas*np.cos(theta) - w = vtas*np.sin(theta) + u = vtas * np.cos(theta) + w = vtas * np.sin(theta) # inputs - self.states[np.where((self.states[:,0] == 'phi'))[0][0],1] = 'fix' - self.states[np.where((self.states[:,0] == 'phi'))[0][0],2] = self.trimcase['phi'] - self.states[np.where((self.states[:,0] == 'theta'))[0][0],1] = 'fix' - self.states[np.where((self.states[:,0] == 'theta'))[0][0],2] = theta - self.states[np.where((self.states[:,0] == 'u'))[0][0],2] = u - self.states[np.where((self.states[:,0] == 'w'))[0][0],2] = w - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],2] = self.trimcase['command_xi'] - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],2] = self.trimcase['command_eta'] - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],2] = self.trimcase['command_zeta'] + self.states[np.where((self.states[:, 0] == 'phi'))[0][0], 1] = 'fix' + self.states[np.where((self.states[:, 0] == 'phi'))[0][0], 2] = self.trimcase['phi'] + self.states[np.where((self.states[:, 0] == 'theta'))[0][0], 1] = 'fix' + self.states[np.where((self.states[:, 0] == 'theta'))[0][0], 2] = theta + self.states[np.where((self.states[:, 0] == 'u'))[0][0], 2] = u + self.states[np.where((self.states[:, 0] == 'w'))[0][0], 2] = w + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 2] = self.trimcase['command_xi'] + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 2] = self.trimcase['command_eta'] + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 2] = self.trimcase['command_zeta'] # outputs - self.outputs[np.where((self.outputs[:,0] == 'Nz'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dq'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.outputs[np.where((self.outputs[:, 0] == 'Nz'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dq'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + # Trim condtions for a windtunnel-like setting. # All remaining trim conditions are given, only the structural flexibility is calculated. elif self.trimcase['maneuver'] in ['windtunnel']: logging.info('Setting trim conditions to "windtunnel"') vtas = self.trimcase['Ma'] * self.atmo['a'] theta = self.trimcase['theta'] - u = vtas*np.cos(theta) - w = vtas*np.sin(theta) + u = vtas * np.cos(theta) + w = vtas * np.sin(theta) # inputs - self.states[np.where((self.states[:,0] == 'phi'))[0][0],1] = 'fix' - self.states[np.where((self.states[:,0] == 'phi'))[0][0],2] = self.trimcase['phi'] - self.states[np.where((self.states[:,0] == 'theta'))[0][0],1] = 'fix' - self.states[np.where((self.states[:,0] == 'theta'))[0][0],2] = theta - self.states[np.where((self.states[:,0] == 'u'))[0][0],1] = 'fix' - self.states[np.where((self.states[:,0] == 'u'))[0][0],2] = u - self.states[np.where((self.states[:,0] == 'w'))[0][0],1] = 'fix' - self.states[np.where((self.states[:,0] == 'w'))[0][0],2] = w - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_xi'))[0][0],2] = self.trimcase['command_xi'] - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_eta'))[0][0],2] = self.trimcase['command_eta'] - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],1] = 'fix' - self.inputs[np.where((self.inputs[:,0] == 'command_zeta'))[0][0],2] = self.trimcase['command_zeta'] + self.states[np.where((self.states[:, 0] == 'phi'))[0][0], 1] = 'fix' + self.states[np.where((self.states[:, 0] == 'phi'))[0][0], 2] = self.trimcase['phi'] + self.states[np.where((self.states[:, 0] == 'theta'))[0][0], 1] = 'fix' + self.states[np.where((self.states[:, 0] == 'theta'))[0][0], 2] = theta + self.states[np.where((self.states[:, 0] == 'u'))[0][0], 1] = 'fix' + self.states[np.where((self.states[:, 0] == 'u'))[0][0], 2] = u + self.states[np.where((self.states[:, 0] == 'w'))[0][0], 1] = 'fix' + self.states[np.where((self.states[:, 0] == 'w'))[0][0], 2] = w + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_xi'))[0][0], 2] = self.trimcase['command_xi'] + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_eta'))[0][0], 2] = self.trimcase['command_eta'] + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 1] = 'fix' + self.inputs[np.where((self.inputs[:, 0] == 'command_zeta'))[0][0], 2] = self.trimcase['command_zeta'] # outputs - self.outputs[np.where((self.outputs[:,0] == 'Nz'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dx'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dz'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dp'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dq'))[0][0],1] = 'free' - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'dr'))[0][0],1] = 'free' - + self.outputs[np.where((self.outputs[:, 0] == 'Nz'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dx'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dz'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dp'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dq'))[0][0], 1] = 'free' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'dr'))[0][0], 1] = 'free' + else: logging.info('Setting trim conditions to "default"') def add_stabilizer_setting(self): if 'stabilizer' in self.trimcase: - self.inputs[np.where((self.inputs[:,0] == 'stabilizer'))[0][0],2] = self.trimcase['stabilizer'] - + self.inputs[np.where((self.inputs[:, 0] == 'stabilizer'))[0][0], 2] = self.trimcase['stabilizer'] + def add_flap_setting(self): if 'flap_setting' in self.trimcase: - self.inputs[np.where((self.inputs[:,0] == 'flap_setting'))[0][0],2] = self.trimcase['flap_setting'] - - + self.inputs[np.where((self.inputs[:, 0] == 'flap_setting'))[0][0], 2] = self.trimcase['flap_setting'] + def add_engine(self): if hasattr(self.jcl, 'engine'): if 'thrust' in self.trimcase and self.trimcase['thrust'] in ['free', 'balanced']: logging.info('Setting trim conditions to "balanced thrust"') # inputs - self.inputs[np.where((self.inputs[:,0] == 'thrust'))[0][0],1] = 'free' + self.inputs[np.where((self.inputs[:, 0] == 'thrust'))[0][0], 1] = 'free' # outputs - self.state_derivatives[np.where((self.state_derivatives[:,0] == 'du'))[0][0],1] = 'target' + self.state_derivatives[np.where((self.state_derivatives[:, 0] == 'du'))[0][0], 1] = 'target' elif 'thrust' in self.trimcase: logging.info('Setting trim conditions to {} [N] thrust per engine'.format(self.trimcase['thrust'])) # inputs - self.inputs[np.where((self.inputs[:,0] == 'thrust'))[0][0],2] = self.trimcase['thrust'] + self.inputs[np.where((self.inputs[:, 0] == 'thrust'))[0][0], 2] = self.trimcase['thrust'] def add_landinggear(self): self.lg_states = [] self.lg_derivatives = [] - + if self.jcl.landinggear['method'] in ['generic']: logging.info('Adding 2 x {} states for landing gear'.format(self.model.extragrid['n'])) for i in range(self.model.extragrid['n']): - self.lg_states.append(self.response['p1'][i] - self.jcl.landinggear['para'][i]['stroke_length'] - self.jcl.landinggear['para'][i]['fitting_length']) + self.lg_states.append(self.response['p1'][i] - self.jcl.landinggear['para'][i]['stroke_length'] + - self.jcl.landinggear['para'][i]['fitting_length']) self.lg_derivatives.append(self.response['dp1'][i]) for i in range(self.model.extragrid['n']): self.lg_states.append(self.response['dp1'][i]) self.lg_derivatives.append(self.response['ddp1'][i]) - elif self.jcl.landinggear['method'] in ['skid']: + elif self.jcl.landinggear['method'] in ['skid']: pass - + # expand 1d list to 2d array self.lg_states = np.expand_dims(self.lg_states, axis=0) self.lg_derivatives = np.expand_dims(self.lg_derivatives, axis=0) # update response with landing gear states self.response['X'] = np.append(self.response['X'], self.lg_states, axis=1) - self.response['Y'] = np.hstack((self.response['Y'][:, self.idx_state_derivatives + self.idx_input_derivatives], self.lg_derivatives, self.response['Y'][:,self.idx_outputs] )) + self.response['Y'] = np.hstack((self.response['Y'][:, self.idx_state_derivatives + self.idx_input_derivatives], + self.lg_derivatives, self.response['Y'][:, self.idx_outputs])) # set indices self.n_lg_states = self.lg_states.shape[1] - self.idx_lg_states = list(range(self.n_states+self.n_inputs, self.n_states+self.n_inputs+self.n_lg_states)) - self.idx_lg_derivatives = list(range(self.n_state_derivatives+self.n_input_derivatives, self.n_state_derivatives+self.n_input_derivatives+self.n_lg_states)) - self.idx_outputs = list(range(self.n_state_derivatives+self.n_input_derivatives+self.n_lg_states, self.n_state_derivatives+self.n_input_derivatives+self.n_lg_states+self.n_outputs)) - + self.idx_lg_states = list(range(self.n_states + self.n_inputs, self.n_states + self.n_inputs + self.n_lg_states)) + self.idx_lg_derivatives = list(range(self.n_state_derivatives + self.n_input_derivatives, self.n_state_derivatives + + self.n_input_derivatives + self.n_lg_states)) + self.idx_outputs = list(range(self.n_state_derivatives + self.n_input_derivatives + self.n_lg_states, + self.n_state_derivatives + self.n_input_derivatives + self.n_lg_states + self.n_outputs)) + def add_lagstates(self): # Initialize lag states with zero and extend steady response vectors X and Y # Distinguish between pyhsical rfa on panel level and generalized rfa. This influences the number of lag states. @@ -432,23 +452,27 @@ def add_lagstates(self): logging.error('Generalized RFA not yet implemented.') elif 'method_rfa' in self.jcl.aero and self.jcl.aero['method_rfa'] == 'halfgeneralized': logging.info('Adding {} x {} unsteady lag states to the system'.format(2 * self.n_modes, n_poles)) - self.lag_states = np.zeros((1, 2 * self.n_modes * n_poles)) + self.lag_states = np.zeros((1, 2 * self.n_modes * n_poles)) else: logging.info('Adding {} x {} unsteady lag states to the system'.format(self.model['aerogrid']['n'][()], n_poles)) - self.lag_states = np.zeros((1,self.model['aerogrid']['n'][()] * n_poles)) + self.lag_states = np.zeros((1, self.model['aerogrid']['n'][()] * n_poles)) # update response with lag states self.response['X'] = np.append(self.response['X'], self.lag_states, axis=1) - self.response['Y'] = np.hstack((self.response['Y'][:,self.idx_state_derivatives + self.idx_input_derivatives], self.lag_states, self.response['Y'][:, self.idx_outputs] )) + self.response['Y'] = np.hstack((self.response['Y'][:, self.idx_state_derivatives + self.idx_input_derivatives], + self.lag_states, self.response['Y'][:, self.idx_outputs])) # set indices self.n_lag_states = self.lag_states.shape[1] - self.idx_lag_states = list(range(self.n_states+self.n_inputs, self.n_states+self.n_inputs+self.n_lag_states)) - self.idx_lag_derivatives = list(range(self.n_state_derivatives+self.n_input_derivatives, self.n_state_derivatives+self.n_input_derivatives+self.n_lag_states)) - self.idx_outputs = list(range(self.n_state_derivatives+self.n_input_derivatives+self.n_lag_states, self.n_state_derivatives+self.n_input_derivatives+self.n_lag_states+self.n_outputs)) - + self.idx_lag_states = list(range(self.n_states + self.n_inputs, self.n_states + self.n_inputs + self.n_lag_states)) + self.idx_lag_derivatives = list(range(self.n_state_derivatives + self.n_input_derivatives, self.n_state_derivatives + + self.n_input_derivatives + self.n_lag_states)) + self.idx_outputs = list(range(self.n_state_derivatives + self.n_input_derivatives + self.n_lag_states, + self.n_state_derivatives + self.n_input_derivatives + self.n_lag_states + + self.n_outputs)) + def set_modal_states_fix(self): # remove modes from trimcond_Y and _Y - for i_mode in range(1, self.n_modes+1): - self.trimcond_X[np.where((self.trimcond_X[:,0] == 'Uf'+str(i_mode)))[0][0],1] = 'fix' - self.trimcond_X[np.where((self.trimcond_X[:,0] == 'dUf_dt'+str(i_mode)))[0][0],1] = 'fix' - self.trimcond_Y[np.where((self.trimcond_Y[:,0] == 'dUf_dt'+str(i_mode)))[0][0],1] = 'fix' - self.trimcond_Y[np.where((self.trimcond_Y[:,0] == 'd2Uf_d2t'+str(i_mode)))[0][0],1] = 'fix' + for i_mode in range(1, self.n_modes + 1): + self.trimcond_X[np.where((self.trimcond_X[:, 0] == 'Uf' + str(i_mode)))[0][0], 1] = 'fix' + self.trimcond_X[np.where((self.trimcond_X[:, 0] == 'dUf_dt' + str(i_mode)))[0][0], 1] = 'fix' + self.trimcond_Y[np.where((self.trimcond_Y[:, 0] == 'dUf_dt' + str(i_mode)))[0][0], 1] = 'fix' + self.trimcond_Y[np.where((self.trimcond_Y[:, 0] == 'd2Uf_d2t' + str(i_mode)))[0][0], 1] = 'fix' diff --git a/loadskernel/units.py b/loadskernel/units.py index 7b56734..c1e45b6 100644 --- a/loadskernel/units.py +++ b/loadskernel/units.py @@ -1,71 +1,77 @@ # -*- coding: utf-8 -*- -""" -Created on Tue Oct 21 16:42:14 2014 - -@author: voss_ar -""" import numpy as np from loadskernel.atmosphere import isa as atmo_isa + def ft2m(length_ft): # exactly - return np.array(length_ft)*0.3048 - + return np.array(length_ft) * 0.3048 + + def m2ft(length_m): # exactly - return np.array(length_m)/0.3048 - + return np.array(length_m) / 0.3048 + + def kn2ms(speed_kn): # reference: SI Brochure: The International System of Units (SI) [8th edition, 2006; updated in 2014] - return np.array(speed_kn)*1852./3600. + return np.array(speed_kn) * 1852. / 3600. + def ms2kn(speed_ms): - return np.array(speed_ms)/1852.*3600. - -def eas2tas(eas,h): - p0, rho0, T0, a0 = atmo_isa(0) - p, rho, T, a = atmo_isa(h) - return eas*(rho0/rho)**0.5 - -def tas2eas(tas,h): - p0, rho0, T0, a0 = atmo_isa(0) - p, rho, T, a = atmo_isa(h) - return tas/(rho0/rho)**0.5 - -def tas2cas(tas,h): + return np.array(speed_ms) / 1852. * 3600. + + +def eas2tas(eas, h): + _, rho0, _, _ = atmo_isa(0) + _, rho, _, _ = atmo_isa(h) + return eas * (rho0 / rho) ** 0.5 + + +def tas2eas(tas, h): + _, rho0, _, _ = atmo_isa(0) + _, rho, _, _ = atmo_isa(h) + return tas / (rho0 / rho) ** 0.5 + + +def tas2cas(tas, h): # Like cas2tas but with different procedure to calculate qc. # For double-checking, see interactive chart at # http://walter.bislins.ch/blog/index.asp?page=Compressibility+Correction+Chart%2C+Verwendung+und+Berechnung # In addition, the round-trip Vtas --> Vcas --> Vtas must result in the same Vtas. - p, rho, T, a = atmo_isa(h) - p0, rho0, T0, a0 = atmo_isa(0) + p, rho, _, a = atmo_isa(h) + p0, rho0, _, _ = atmo_isa(0) gamma = 1.4 - qc = p*( (1 + (gamma-1)/2 * (tas/a)**2)**(gamma/(gamma-1)) - 1 ) - f = ( gamma/(gamma-1)*p/qc*( (qc/p+1)**((gamma-1)/gamma) - 1 ) )**0.5 - f0 = ( gamma/(gamma-1)*p0/qc*( (qc/p0+1)**((gamma-1)/gamma) - 1 ) )**0.5 - cas = tas * f0/f / (rho0/rho)**0.5 + qc = p * ((1 + (gamma - 1) / 2 * (tas / a) ** 2) ** (gamma / (gamma - 1)) - 1) + f = (gamma / (gamma - 1) * p / qc * ((qc / p + 1) ** ((gamma - 1) / gamma) - 1)) ** 0.5 + f0 = (gamma / (gamma - 1) * p0 / qc * ((qc / p0 + 1) ** ((gamma - 1) / gamma) - 1)) ** 0.5 + cas = tas * f0 / f / (rho0 / rho) ** 0.5 return cas - -def cas2tas(cas,h): + + +def cas2tas(cas, h): # Reference: NASA RP 1046,Measurement of Aircraft Speed and Altitude, William Gracey, 1980 - p, rho, T, a = atmo_isa(h) - p0, rho0, T0, a0 = atmo_isa(0) + p, rho, _, _ = atmo_isa(h) + p0, rho0, _, _ = atmo_isa(0) gamma = 1.4 - qc = p0*( (1+(gamma-1)/(2*gamma)*rho0/p0*cas**2)**(gamma/(gamma-1)) - 1 ) - f = ( gamma/(gamma-1)*p/qc*( (qc/p+1)**((gamma-1)/gamma) - 1 ) )**0.5 - f0 = ( gamma/(gamma-1)*p0/qc*( (qc/p0+1)**((gamma-1)/gamma) - 1 ) )**0.5 - tas = cas * f/f0 * (rho0/rho)**0.5 + qc = p0 * ((1 + (gamma - 1) / (2 * gamma) * rho0 / p0 * cas ** 2) ** (gamma / (gamma - 1)) - 1) + f = (gamma / (gamma - 1) * p / qc * ((qc / p + 1) ** ((gamma - 1) / gamma) - 1)) ** 0.5 + f0 = (gamma / (gamma - 1) * p0 / qc * ((qc / p0 + 1) ** ((gamma - 1) / gamma) - 1)) ** 0.5 + tas = cas * f / f0 * (rho0 / rho) ** 0.5 return tas -def cas2Ma(cas,h): - tas = cas2tas(cas,h) - p, rho, T, a = atmo_isa(h) - return tas/a + +def cas2Ma(cas, h): + tas = cas2tas(cas, h) + _, _, _, a = atmo_isa(h) + return tas / a + def tas2Ma(tas, h): - p, rho, T, a = atmo_isa(h) - return tas/a + _, _, _, a = atmo_isa(h) + return tas / a + -def eas2Ma(eas,h): +def eas2Ma(eas, h): tas = eas2tas(eas, h) return tas2Ma(tas, h) diff --git a/loadskernel/utils/PID.py b/loadskernel/utils/PID.py index d8da3ed..7a98646 100644 --- a/loadskernel/utils/PID.py +++ b/loadskernel/utils/PID.py @@ -1,12 +1,14 @@ - """ -The ideal PID-controller is not suitable for direct field interaction, therefore it is called the non-interactive PID-controller. -It is highly responsive to electrical noise on the PV input if the derivative function is enabled. +The ideal PID-controller is not suitable for direct field interaction, therefore it is called the non-interactive +PID-controller. It is highly responsive to electrical noise on the PV input if the derivative function is enabled. (Practical Process Control for Engineers and Technicians, Wolfgang Altmann, 2005, Chapter 7) -> If the PV signal contains no noise and/or derivative gain Kd is not used, the ideal PID controller can be used """ + + class PID_ideal(): + def __init__(self, Kp=0.2, Ki=0.0, Kd=0.0, t=0.0): self.Kp = Kp self.Ki = Ki @@ -29,25 +31,25 @@ def clear(self): self.windup_guard = 20.0 self.output = 0.0 - + def setSetPoint(self, SetPoint): self.SetPoint = SetPoint - + def update_PID_Terms(self, t, feedback_value): - + error = self.SetPoint - feedback_value self.current_time = t delta_time = self.current_time - self.last_time delta_error = error - self.last_error - if (delta_time >= self.sample_time): + if delta_time >= self.sample_time: self.PTerm = error self.ITerm += error * delta_time - if (self.ITerm < -self.windup_guard): + if self.ITerm < -self.windup_guard: self.ITerm = -self.windup_guard - elif (self.ITerm > self.windup_guard): + elif self.ITerm > self.windup_guard: self.ITerm = self.windup_guard self.DTerm = 0.0 @@ -57,25 +59,28 @@ def update_PID_Terms(self, t, feedback_value): # Remember last time and last error for next calculation self.last_time = self.current_time self.last_error = error - + def update(self, t, feedback_value): self.update_PID_Terms(t, feedback_value) self.update_output() - + def update_output(self): """Calculates PID value for given reference feedback .. math:: - u(t) = K_p e(t) + K_i \int_{0}^{t} e(t)dt + K_d {de}/{dt} + u(t) = K_p e(t) + K_i integral_{0}^{t} e(t)dt + K_d {de}/{dt} """ self.output = (self.Kp * self.PTerm) + (self.Ki * self.ITerm) + (self.Kd * self.DTerm) - + + """ -The standart PID-controller is especially designed for direct field interaction and is therefore called the interactive PID-controller. -Due to internal filtering in the derivative block the effects of electrical noise on the PV input is greatly reduced. -(Practical Process Control for Engineers and Technicians, Wolfgang Altmann, 2005, Chapter 7) +The standart PID-controller is especially designed for direct field interaction and is therefore called the interactive +PID-controller. Due to internal filtering in the derivative block the effects of electrical noise on the PV input is greatly +reduced. (Practical Process Control for Engineers and Technicians, Wolfgang Altmann, 2005, Chapter 7) """ + + class PID_standart(PID_ideal): - + def __init__(self, Kp=0.2, Ti=0.0, Td=0.0, t=0.0): self.Kp = Kp self.Ti = Ti @@ -84,17 +89,18 @@ def __init__(self, Kp=0.2, Ti=0.0, Td=0.0, t=0.0): self.current_time = t self.last_time = self.current_time self.clear() - + def update_output(self): """Calculates PID value for given reference feedback .. math:: - u(t) = K_p * [ e(t) + 1/T_i \int_{0}^{t} e(t)dt + T_d {de}/{dt} ] + u(t) = K_p * [ e(t) + 1/T_i integral_{0}^{t} e(t)dt + T_d {de}/{dt} ] """ - self.output = self.Kp * (self.PTerm + (1.0/self.Ti * self.ITerm) + (self.Td * self.DTerm)) + self.output = self.Kp * \ + (self.PTerm + (1.0 / self.Ti * self.ITerm) + (self.Td * self.DTerm)) class PID_T1(PID_ideal): - + def __init__(self, Kp=0.2, Ti=0.0, Td=0.0, T1=0.0, t=0.0): self.Kp = Kp self.Ti = Ti @@ -104,10 +110,7 @@ def __init__(self, Kp=0.2, Ti=0.0, Td=0.0, T1=0.0, t=0.0): self.current_time = t self.last_time = self.current_time self.clear() - + def update_output(self): - self.output = self.Kp * ((self.T1+self.Ti)/self.Ti*self.PTerm + (1.0/self.Ti * self.ITerm) + (self.T1 + self.Td) * self.DTerm) - - - - \ No newline at end of file + self.output = self.Kp * ((self.T1 + self.Ti) / self.Ti * self.PTerm + (1.0 / self.Ti * self.ITerm) + + (self.T1 + self.Td) * self.DTerm) diff --git a/loadskernel/utils/filter.py b/loadskernel/utils/filter.py index a946ca2..5507baf 100644 --- a/loadskernel/utils/filter.py +++ b/loadskernel/utils/filter.py @@ -1,6 +1,6 @@ - from scipy import signal + class Butter: # Application of Butterworth filter to a time signal # fs = sample rate, Hz @@ -8,20 +8,19 @@ class Butter: # Usage: # butter = filter.Butter(fs=40.0, cutoff=2.0) # data_filtered = butter.butter_lowpass_filter(data) - + def __init__(self, cutoff, fs, order=5): self.order = order - self.fs = fs # sample rate, Hz - self.cutoff = cutoff # desired cutoff frequency of the filter, Hz - pass + self.fs = fs # sample rate, Hz + self.cutoff = cutoff # desired cutoff frequency of the filter, Hz def butter_lowpass(self): - nyq = 0.5 * self.fs - normal_cutoff = self.cutoff / nyq - b, a = signal.butter(self.order, normal_cutoff, btype='low', analog=False) - return b, a - + nyq = 0.5 * self.fs + normal_cutoff = self.cutoff / nyq + b, a = signal.butter(self.order, normal_cutoff, btype='low', analog=False) + return b, a + def butter_lowpass_filter(self, data): - b, a = self.butter_lowpass() - y = signal.filtfilt(b, a, data) - return y \ No newline at end of file + b, a = self.butter_lowpass() + y = signal.filtfilt(b, a, data) + return y diff --git a/loadskernel/utils/sparse_matrices.py b/loadskernel/utils/sparse_matrices.py index bbb4ca5..4d935c4 100644 --- a/loadskernel/utils/sparse_matrices.py +++ b/loadskernel/utils/sparse_matrices.py @@ -1,23 +1,25 @@ import numpy as np + def insert_coo(sparsematrix, submatrix, idx1, idx2): """ For sparse matrices, "fancy indexing" is not supported / not implemented as of 2017 In case of a coo matrix, row and column based indexing is supported and used below. - Observation: For sparse matrices where data is continously inserted, the COO format - becomes slower and slower the bigger the matrix becomes. This is possibly due to the + Observation: For sparse matrices where data is continously inserted, the COO format + becomes slower and slower the bigger the matrix becomes. This is possibly due to the resizing of the numpy arrays. """ row, col = np.meshgrid(idx1, idx2, indexing='ij') - sparsematrix.row = np.concatenate(( sparsematrix.row, row.reshape(-1) )) - sparsematrix.col = np.concatenate(( sparsematrix.col, col.reshape(-1) )) - sparsematrix.data = np.concatenate(( sparsematrix.data, submatrix.reshape(-1) )) + sparsematrix.row = np.concatenate((sparsematrix.row, row.reshape(-1))) + sparsematrix.col = np.concatenate((sparsematrix.col, col.reshape(-1))) + sparsematrix.data = np.concatenate((sparsematrix.data, submatrix.reshape(-1))) return sparsematrix + def insert_lil(sparsematrix, submatrix, idx1, idx2): """ - In contrast to COO, the LIL format is based on lists, which are faster to expand. - Since my first implementation in 2014, the interface to the LIL matrix has improved and + In contrast to COO, the LIL format is based on lists, which are faster to expand. + Since my first implementation in 2014, the interface to the LIL matrix has improved and allows the direct assignment of data to matrix items, accessed by their index. """ row, col = np.meshgrid(idx1, idx2, indexing='ij') diff --git a/modelviewer/cfdgrid.py b/modelviewer/cfdgrid.py index 7ee0cdf..6423233 100644 --- a/modelviewer/cfdgrid.py +++ b/modelviewer/cfdgrid.py @@ -1,25 +1,27 @@ - - -import scipy.io.netcdf as netcdf +from scipy.io import netcdf import loadskernel.io_functions.read_cfdgrids + class TauGrid(loadskernel.io_functions.read_cfdgrids.ReadCfdgrids): - def __init__(self): + + def __init__(self): pass def load_file(self, filename): - self.filename_grid = filename + self.filename_grid = filename self.get_markers() self.read_cfdmesh_netcdf() - + def get_markers(self): ncfile_grid = netcdf.NetCDFFile(self.filename_grid, 'r') self.markers = ncfile_grid.variables['marker'][:].tolist() + class SU2Grid(loadskernel.io_functions.read_cfdgrids.ReadCfdgrids): - def __init__(self): + + def __init__(self): pass def load_file(self, filename): - self.filename_grid = filename + self.filename_grid = filename self.read_cfdmesh_su2() diff --git a/modelviewer/iges.py b/modelviewer/iges.py index 6eb64f7..2af42e2 100644 --- a/modelviewer/iges.py +++ b/modelviewer/iges.py @@ -1,33 +1,35 @@ - -import sys, os +import os +import sys from tvtk.api import tvtk try: import pyiges -except: +except ImportError: pass -class IgesMesh(object): - def __init__(self): + +class IgesMesh(): + + def __init__(self): self.meshes = [] - pass def load_file(self, filename): - self.filename = filename + self.filename = filename self.read_iges() - + def read_iges(self): """ - The Pyvista package provides a nice reader for iges files (pyiges) and has a converter to VTK. - This VTK object can be plotted with mayavi natively so that we don't need to switch everything + The Pyvista package provides a nice reader for iges files (pyiges) and has a converter to VTK. + This VTK object can be plotted with mayavi natively so that we don't need to switch everything to pyvista just for the iges plotting. """ if 'pyiges' in sys.modules: iges_object = pyiges.read(self.filename) - pyvista_mb = iges_object.to_vtk(lines=False, bsplines=False, surfaces=True, points=False, merge=False) + pyvista_mb = iges_object.to_vtk( + lines=False, bsplines=False, surfaces=True, points=False, merge=False) """ Since version 0.32, Pyvista returns a MultiBlockDataSet, with is not hashable anymore. According to a discussion on github, this is a feature, not a bug (https://github.com/pyvista/pyvista/issues/1751) - The following lines do the conversion to a "true" VTK MultiBlockDataSet, which is hashable and can be + The following lines do the conversion to a "true" VTK MultiBlockDataSet, which is hashable and can be plotted in mayavi without any further issues. """ # Create a new, empty vtk dataset @@ -40,4 +42,3 @@ def read_iges(self): 'vtk': vtk_mb}) else: print('Pyiges modul not found, can not read iges file.') - diff --git a/modelviewer/plotting.py b/modelviewer/plotting.py index 9a45ff3..16546b6 100644 --- a/modelviewer/plotting.py +++ b/modelviewer/plotting.py @@ -4,108 +4,112 @@ from loadskernel.io_functions.data_handling import load_hdf5_dict + class Plotting: + def __init__(self): pass def plot_nothing(self): mlab.clf(self.fig) - self.show_masses=False - self.show_strc=False - self.show_mode=False - self.show_aero=False - self.show_cfdgrids=False - self.show_coupling=False - self.show_cs=False - self.show_cell=False - self.show_monstations=False - self.show_iges=False - + self.show_masses = False + self.show_strc = False + self.show_mode = False + self.show_aero = False + self.show_cfdgrids = False + self.show_coupling = False + self.show_cs = False + self.show_cell = False + self.show_monstations = False + self.show_iges = False + def add_figure(self, fig): self.fig = fig - self.fig.scene.background = (1.,1.,1.) + self.fig.scene.background = (1., 1., 1.) self.plot_nothing() - + def add_model(self, model): self.model = model - self.strcgrid = load_hdf5_dict(self.model['strcgrid']) + self.strcgrid = load_hdf5_dict(self.model['strcgrid']) self.splinegrid = load_hdf5_dict(self.model['splinegrid']) - self.aerogrid = load_hdf5_dict(self.model['aerogrid']) - self.x2grid = load_hdf5_dict(self.model['x2grid']) - self.mongrid = load_hdf5_dict(self.model['mongrid']) - self.coord = load_hdf5_dict(self.model['coord']) - self.Djx2 = self.model['Djx2'][()] + self.aerogrid = load_hdf5_dict(self.model['aerogrid']) + self.x2grid = load_hdf5_dict(self.model['x2grid']) + self.mongrid = load_hdf5_dict(self.model['mongrid']) + self.coord = load_hdf5_dict(self.model['coord']) + self.Djx2 = self.model['Djx2'][()] self.calc_parameters_from_model_size() self.calc_focalpoint() - + def add_cfdgrids(self, cfdgrids): self.cfdgrids = cfdgrids - + def add_iges_meshes(self, meshes): self.iges_meshes = meshes def calc_parameters_from_model_size(self): # Calculate the overall size of the model. - model_size = ( (self.strcgrid['offset'][:,0].max()-self.strcgrid['offset'][:,0].min())**2 \ - + (self.strcgrid['offset'][:,1].max()-self.strcgrid['offset'][:,1].min())**2 \ - + (self.strcgrid['offset'][:,2].max()-self.strcgrid['offset'][:,2].min())**2 )**0.5 + model_size = ((self.strcgrid['offset'][:, 0].max() - self.strcgrid['offset'][:, 0].min()) ** 2 + + (self.strcgrid['offset'][:, 1].max() - self.strcgrid['offset'][:, 1].min()) ** 2 + + (self.strcgrid['offset'][:, 2].max() - self.strcgrid['offset'][:, 2].min()) ** 2) ** 0.5 # Set some parameters which typically give a good view. - self.distance = model_size * 1.5 - self.pscale = np.min([model_size / 400.0, 0.04]) - self.macscale = np.min([model_size / 10.0, 1.0]) - + self.distance = model_size * 1.5 + self.pscale = np.min([model_size / 400.0, 0.04]) + self.macscale = np.min([model_size / 10.0, 1.0]) + def calc_focalpoint(self): - self.focalpoint = (self.strcgrid['offset'].min(axis=0) + self.strcgrid['offset'].max(axis=0))/2.0 - + self.focalpoint = (self.strcgrid['offset'].min( + axis=0) + self.strcgrid['offset'].max(axis=0)) / 2.0 + def set_view_left_above(self): - self.azimuth = 60.0 + self.azimuth = 60.0 self.elevation = -65.0 - self.roll = 55.0 + self.roll = 55.0 self.set_view() - + def set_view_back(self): - self.azimuth = 180.0 + self.azimuth = 180.0 self.elevation = -90.0 - self.roll = -90.0 + self.roll = -90.0 self.set_view() - + def set_view_side(self): - self.azimuth = 90.0 + self.azimuth = 90.0 self.elevation = -90.0 - self.roll = 0.0 + self.roll = 0.0 self.set_view() - + def set_view_top(self): - self.azimuth = 180.0 + self.azimuth = 180.0 self.elevation = 0.0 - self.roll = 0.0 - self.distance *= 1.5 # zoom out more + self.roll = 0.0 + self.distance *= 1.5 # zoom out more self.set_view() - self.calc_parameters_from_model_size() # rest zoom + self.calc_parameters_from_model_size() # rest zoom def set_view(self): - mlab.view(azimuth=self.azimuth, elevation=self.elevation, roll=self.roll, distance=self.distance, focalpoint=self.focalpoint) - #mlab.orientation_axes() - + mlab.view(azimuth=self.azimuth, elevation=self.elevation, roll=self.roll, distance=self.distance, + focalpoint=self.focalpoint) + # mlab.orientation_axes() + def hide_masses(self): self.src_masses.remove() self.src_mass_cg.remove() - self.show_masses=False + self.show_masses = False mlab.draw(self.fig) - + def plot_masses(self, MGG, Mb, cggrid, rho=2700.0): # get nodal masses - m_cg = Mb[0,0] + m_cg = Mb[0, 0] m = MGG.diagonal()[0::6] - - radius_mass_cg = ((m_cg*3.)/(4.*rho*np.pi))**(1./3.) - radius_masses = ((m*3.)/(4.*rho*np.pi))**(1./3.) + + radius_mass_cg = ((m_cg * 3.) / (4. * rho * np.pi)) ** (1. / 3.) + radius_masses = ((m * 3.) / (4. * rho * np.pi)) ** (1. / 3.) if self.show_masses: self.update_mass_display(radius_masses, radius_mass_cg, cggrid) else: self.setup_mass_display(radius_masses, radius_mass_cg, cggrid) - self.show_masses=True + self.show_masses = True mlab.draw(self.fig) def setup_mass_display(self, radius_masses, radius_mass_cg, cggrid): @@ -113,14 +117,16 @@ def setup_mass_display(self, radius_masses, radius_mass_cg, cggrid): self.ug1_mass.point_data.scalars = radius_masses # plot points as glyphs self.src_masses = mlab.pipeline.add_dataset(self.ug1_mass) - points = mlab.pipeline.glyph(self.src_masses, scale_mode='scalar', scale_factor = 1.0, color=(1,0.7,0)) + points = mlab.pipeline.glyph( + self.src_masses, scale_mode='scalar', scale_factor=1.0, color=(1, 0.7, 0)) points.glyph.glyph.clamping = False - + self.ug2_mass = tvtk.UnstructuredGrid(points=cggrid['offset']) self.ug2_mass.point_data.scalars = np.array([radius_mass_cg]) # plot points as glyphs self.src_mass_cg = mlab.pipeline.add_dataset(self.ug2_mass) - points = mlab.pipeline.glyph(self.src_mass_cg, scale_mode='scalar', scale_factor = 1.0, color=(1,1,0), opacity=0.3, resolution=64) + points = mlab.pipeline.glyph(self.src_mass_cg, scale_mode='scalar', scale_factor=1.0, color=(1, 1, 0), + opacity=0.3, resolution=64) points.glyph.glyph.clamping = False def update_mass_display(self, radius_masses, radius_mass_cg, cggrid): @@ -133,48 +139,53 @@ def update_mass_display(self, radius_masses, radius_mass_cg, cggrid): def hide_strc(self): self.src_strc.remove() - self.show_strc=False + self.show_strc = False mlab.draw(self.fig) - + def plot_strc(self): - self.src_strc = self.setup_strc_display(offsets=self.strcgrid['offset'], color=(0,0,1), p_scale=self.pscale) - self.show_strc=True + self.src_strc = self.setup_strc_display( + offsets=self.strcgrid['offset'], color=(0, 0, 1), p_scale=self.pscale) + self.show_strc = True mlab.draw(self.fig) - + def hide_mode(self): self.src_mode.remove() - self.show_mode=False + self.show_mode = False mlab.draw(self.fig) - + def plot_mode(self, offsets): if self.show_mode: self.update_mode_display(offsets=offsets) else: - self.src_mode = self.setup_strc_display(offsets=offsets, color=(0,1,0), p_scale=self.pscale) - self.show_mode=True + self.src_mode = self.setup_strc_display( + offsets=offsets, color=(0, 1, 0), p_scale=self.pscale) + self.show_mode = True mlab.draw(self.fig) - + def setup_strc_display(self, offsets, color, p_scale): self.ug_strc = tvtk.UnstructuredGrid(points=offsets) if 'strcshell' in self.model: # plot shell as surface shells = [] - for shell in self.model['strcshell']['cornerpoints'][()]: - shells.append([np.where(self.strcgrid['ID']==id)[0][0] for id in shell[np.isfinite(shell)]]) + for shell in self.model['strcshell']['cornerpoints'][()]: + shells.append([np.where(self.strcgrid['ID'] == id)[0][0] + for id in shell[np.isfinite(shell)]]) shell_type = tvtk.Polygon().cell_type self.ug_strc.set_cells(shell_type, shells) src_strc = mlab.pipeline.add_dataset(self.ug_strc) - points = mlab.pipeline.glyph(src_strc, color=color, scale_factor=p_scale) + points = mlab.pipeline.glyph( + src_strc, color=color, scale_factor=p_scale) surface = mlab.pipeline.surface(src_strc, opacity=0.4, color=color) - surface.actor.property.edge_visibility=True - surface.actor.property.line_width = 0.5 - else: + surface.actor.property.edge_visibility = True + surface.actor.property.line_width = 0.5 + else: # plot points as glyphs src_strc = mlab.pipeline.add_dataset(self.ug_strc) - points = mlab.pipeline.glyph(src_strc, color=color, scale_factor=p_scale) + points = mlab.pipeline.glyph( + src_strc, color=color, scale_factor=p_scale) points.glyph.glyph.scale_mode = 'data_scaling_off' return src_strc - + def update_mode_display(self, offsets): self.ug_strc.points.from_array(offsets) self.ug_strc.modified() @@ -182,19 +193,21 @@ def update_mode_display(self, offsets): def hide_aero(self): self.src_aerogrid.remove() self.src_MAC.remove() - self.show_aero=False + self.show_aero = False mlab.draw(self.fig) - + def plot_aero(self, scalars=None, vminmax=[-10.0, 10.0]): self.setup_aero_display(scalars, vminmax) - self.show_aero=True + self.show_aero = True mlab.draw(self.fig) - + def setup_aero_display(self, scalars, vminmax): - ug1 = tvtk.UnstructuredGrid(points=self.aerogrid['cornerpoint_grids'][:,(1,2,3)]) + ug1 = tvtk.UnstructuredGrid( + points=self.aerogrid['cornerpoint_grids'][:, (1, 2, 3)]) shells = [] - for shell in self.aerogrid['cornerpoint_panels']: - shells.append([np.where(self.aerogrid['cornerpoint_grids'][:,0]==id)[0][0] for id in shell]) + for shell in self.aerogrid['cornerpoint_panels']: + shells.append([np.where(self.aerogrid['cornerpoint_grids'][:, 0] == id)[ + 0][0] for id in shell]) shell_type = tvtk.Polygon().cell_type ug1.set_cells(shell_type, shells) if scalars is not None: @@ -203,151 +216,183 @@ def setup_aero_display(self, scalars, vminmax): ug2 = tvtk.UnstructuredGrid(points=np.array([self.MAC])) self.src_MAC = mlab.pipeline.add_dataset(ug2) - points = mlab.pipeline.glyph(self.src_MAC, scale_mode='scalar', scale_factor = self.macscale, color=(1,0,0), opacity=0.4, resolution=64) + points = mlab.pipeline.glyph(self.src_MAC, scale_mode='scalar', scale_factor=self.macscale, color=(1, 0, 0), + opacity=0.4, resolution=64) points.glyph.glyph.clamping = False - + if scalars is not None: - surface = mlab.pipeline.surface(self.src_aerogrid, colormap='coolwarm', vmin=vminmax[0], vmax=vminmax[1]) - surface.module_manager.scalar_lut_manager.show_legend=True - surface.module_manager.scalar_lut_manager.label_text_property.color=(0,0,0) - surface.module_manager.scalar_lut_manager.label_text_property.font_family='times' - surface.module_manager.scalar_lut_manager.label_text_property.bold=False - surface.module_manager.scalar_lut_manager.label_text_property.italic=False - surface.module_manager.scalar_lut_manager.number_of_labels=5 - + surface = mlab.pipeline.surface( + self.src_aerogrid, colormap='coolwarm', vmin=vminmax[0], vmax=vminmax[1]) + surface.module_manager.scalar_lut_manager.show_legend = True + surface.module_manager.scalar_lut_manager.label_text_property.color = ( + 0, 0, 0) + surface.module_manager.scalar_lut_manager.label_text_property.font_family = 'times' + surface.module_manager.scalar_lut_manager.label_text_property.bold = False + surface.module_manager.scalar_lut_manager.label_text_property.italic = False + surface.module_manager.scalar_lut_manager.number_of_labels = 5 + else: - surface = mlab.pipeline.surface(self.src_aerogrid, color=(1,1,1)) - surface.actor.property.edge_visibility=True - surface.actor.property.edge_color=(0,0,0) - surface.actor.property.line_width=0.5 - + surface = mlab.pipeline.surface(self.src_aerogrid, color=(1, 1, 1)) + surface.actor.property.edge_visibility = True + surface.actor.property.edge_color = (0, 0, 0) + surface.actor.property.line_width = 0.5 + def hide_cfdgrids(self): for src in self.src_cfdgrids: src.remove() - self.show_cfdgrids=False + self.show_cfdgrids = False mlab.draw(self.fig) - + def plot_cfdgrids(self, markers): self.src_cfdgrids = [] for cfdgrid in self.cfdgrids: if cfdgrid['desc'] in markers: - self.setup_cfdgrid_display(grid=cfdgrid, color=(1,1,1), scalars=None) - self.show_cfdgrids=True + self.setup_cfdgrid_display( + grid=cfdgrid, color=(1, 1, 1), scalars=None) + self.show_cfdgrids = True mlab.draw(self.fig) def setup_cfdgrid_display(self, grid, color, scalars): ug = tvtk.UnstructuredGrid(points=grid['offset']) - #ug.point_data.scalars = scalars + # ug.point_data.scalars = scalars shells = [] - for shell in grid['points_of_surface']: - shells.append([np.where(grid['ID']==id)[0][0] for id in shell]) + for shell in grid['points_of_surface']: + shells.append([np.where(grid['ID'] == id)[0][0] for id in shell]) shell_type = tvtk.Polygon().cell_type ug.set_cells(shell_type, shells) src_cfdgrid = mlab.pipeline.add_dataset(ug) self.src_cfdgrids.append(src_cfdgrid) - - surface = mlab.pipeline.surface(src_cfdgrid, opacity=1.0, line_width=0.5, color=color) - surface.actor.property.edge_visibility=True + + surface = mlab.pipeline.surface( + src_cfdgrid, opacity=1.0, line_width=0.5, color=color) + surface.actor.property.edge_visibility = True def hide_aero_strc_coupling(self): self.src_grid_i.remove() self.src_grid_d.remove() if 'coupling_rules' in self.model: self.src_splinerules.remove() - self.show_coupling=False + self.show_coupling = False mlab.draw(self.fig) - + def plot_aero_strc_coupling(self): if 'coupling_rules' in self.model: coupling_rules = load_hdf5_dict(self.model['coupling_rules']) - self.src_grid_i, self.src_grid_d, self.src_splinerules = self.plot_splinerules(self.splinegrid, '', self.aerogrid, '_k', coupling_rules, self.coord) + self.src_grid_i, self.src_grid_d, self.src_splinerules = self.plot_splinerules(self.splinegrid, '', + self.aerogrid, '_k', + coupling_rules, self.coord) else: - self.src_grid_i, self.src_grid_d = self.plot_splinegrids(self.splinegrid, '', self.aerogrid, '_k') - self.show_coupling=True - - def plot_splinegrids(self, grid_i, set_i, grid_d, set_d): - src_grid_i = mlab.points3d(grid_i['offset'+set_i][:,0], grid_i['offset'+set_i][:,1], grid_i['offset'+set_i][:,2], scale_factor=self.pscale*2, color=(0,1,0)) - src_grid_d = mlab.points3d(grid_d['offset'+set_d][:,0], grid_d['offset'+set_d][:,1], grid_d['offset'+set_d][:,2], scale_factor=self.pscale, color=(1,0,0)) + self.src_grid_i, self.src_grid_d = self.plot_splinegrids( + self.splinegrid, '', self.aerogrid, '_k') + self.show_coupling = True + + def plot_splinegrids(self, grid_i, set_i, grid_d, set_d): + src_grid_i = mlab.points3d(grid_i['offset' + set_i][:, 0], + grid_i['offset' + set_i][:, 1], + grid_i['offset' + set_i][:, 2], + scale_factor=self.pscale * 2, color=(0, 1, 0)) + src_grid_d = mlab.points3d(grid_d['offset' + set_d][:, 0], + grid_d['offset' + set_d][:, 1], + grid_d['offset' + set_d][:, 2], + scale_factor=self.pscale, color=(1, 0, 0)) return src_grid_i, src_grid_d - - def plot_splinerules(self, grid_i, set_i, grid_d, set_d, splinerules, coord): + + def plot_splinerules(self, grid_i, set_i, grid_d, set_d, splinerules, coord): # transfer points into common coord offset_dest_i = [] for i_point in range(len(grid_i['ID'])): pos_coord = np.where(coord['ID'] == grid_i['CP'][i_point])[0][0] - offset_dest_i.append(np.dot(coord['dircos'][pos_coord],grid_i['offset'+set_i][i_point])+coord['offset'][pos_coord]) + offset_dest_i.append(np.dot(coord['dircos'][pos_coord], grid_i['offset' + set_i][i_point]) + + coord['offset'][pos_coord]) offset_dest_i = np.array(offset_dest_i) - + offset_dest_d = [] for i_point in range(len(grid_d['ID'])): pos_coord = np.where(coord['ID'] == grid_d['CP'][i_point])[0][0] - offset_dest_d.append(np.dot(coord['dircos'][pos_coord],grid_d['offset'+set_d][i_point])+coord['offset'][pos_coord]) + offset_dest_d.append(np.dot(coord['dircos'][pos_coord], grid_d['offset' + set_d][i_point]) + + coord['offset'][pos_coord]) offset_dest_d = np.array(offset_dest_d) - - position_i = []; position_d = [] - + + position_i = [] + position_d = [] + for ID_i in splinerules: for ID_d in splinerules[ID_i]: - position_i.append( np.where(grid_i['ID'] == int(ID_i))[0][0] ) - position_d.append( np.where(grid_d['ID'] == int(ID_d))[0][0] ) - - x = offset_dest_i[position_i,0] - y = offset_dest_i[position_i,1] - z = offset_dest_i[position_i,2] - u = offset_dest_d[position_d,0] - x - v = offset_dest_d[position_d,1] - y - w = offset_dest_d[position_d,2] - z - - src_grid_i = mlab.points3d(grid_i['offset'+set_i][:,0], grid_i['offset'+set_i][:,1], grid_i['offset'+set_i][:,2], scale_factor=self.pscale*2, color=(0,1,0)) - src_grid_d = mlab.points3d(grid_d['offset'+set_d][:,0], grid_d['offset'+set_d][:,1], grid_d['offset'+set_d][:,2], scale_factor=self.pscale, color=(1,0,0)) - src_spilerules = mlab.quiver3d(x,y,z,u,v,w, mode='2ddash', scale_factor=1.0, color=(0,0,0), opacity=0.4) + position_i.append(np.where(grid_i['ID'] == int(ID_i))[0][0]) + position_d.append(np.where(grid_d['ID'] == int(ID_d))[0][0]) + + x = offset_dest_i[position_i, 0] + y = offset_dest_i[position_i, 1] + z = offset_dest_i[position_i, 2] + u = offset_dest_d[position_d, 0] - x + v = offset_dest_d[position_d, 1] - y + w = offset_dest_d[position_d, 2] - z + + src_grid_i = mlab.points3d(grid_i['offset' + set_i][:, 0], + grid_i['offset' + set_i][:, 1], + grid_i['offset' + set_i][:, 2], + scale_factor=self.pscale * 2, color=(0, 1, 0)) + src_grid_d = mlab.points3d(grid_d['offset' + set_d][:, 0], + grid_d['offset' + set_d][:, 1], + grid_d['offset' + set_d][:, 2], + scale_factor=self.pscale, color=(1, 0, 0)) + src_spilerules = mlab.quiver3d( + x, y, z, u, v, w, mode='2ddash', scale_factor=1.0, color=(0, 0, 0), opacity=0.4) return src_grid_i, src_grid_d, src_spilerules - + def hide_monstations(self): self.src_mongrid_i.remove() self.src_mongrid_d.remove() self.src_mongrid_rules.remove() - self.show_monstations=False + self.show_monstations = False mlab.draw(self.fig) - + def plot_monstations(self, monstation_id): if self.show_monstations: self.hide_monstations() # create a sub-set from all mongrid_rules - rules = {monstation_id: self.model['mongrid_rules'][str(monstation_id)][()]} - self.src_mongrid_i, self.src_mongrid_d, self.src_mongrid_rules = self.plot_splinerules(self.mongrid, '', self.strcgrid, '', rules, self.coord) - self.show_monstations=True + rules = { + monstation_id: self.model['mongrid_rules'][str(monstation_id)][()]} + self.src_mongrid_i, self.src_mongrid_d, self.src_mongrid_rules = self.plot_splinerules(self.mongrid, '', + self.strcgrid, '', + rules, self.coord) + self.show_monstations = True def hide_cs(self): self.src_cs.remove() - self.show_cs=False + self.show_cs = False mlab.draw(self.fig) - + def plot_cs(self, i_surf, axis, deg): # determine deflections if axis == 'y-axis': - Uj = np.dot(self.Djx2[i_surf],[0,0,0,0,deg/180.0*np.pi,0]) + Uj = np.dot(self.Djx2[i_surf], [ + 0, 0, 0, 0, deg / 180.0 * np.pi, 0]) elif axis == 'z-axis': - Uj = np.dot(self.Djx2[i_surf],[0,0,0,0,0,deg/180.0*np.pi]) + Uj = np.dot(self.Djx2[i_surf], [ + 0, 0, 0, 0, 0, deg / 180.0 * np.pi]) else: - Uj = np.dot(self.Djx2[i_surf],[0,0,0,0,0,0]) + Uj = np.dot(self.Djx2[i_surf], [0, 0, 0, 0, 0, 0]) # find those panels belonging to the current control surface i_surf - members_of_i_surf = [np.where(self.aerogrid['ID']==x)[0][0] for x in self.x2grid[str(i_surf)]['ID'][()]] - points = self.aerogrid['offset_k'][members_of_i_surf,:]+Uj[self.aerogrid['set_k'][members_of_i_surf,:][:,(0,1,2)]] - + members_of_i_surf = [np.where(self.aerogrid['ID'] == x)[ + 0][0] for x in self.x2grid[str(i_surf)]['ID'][()]] + points = self.aerogrid['offset_k'][members_of_i_surf, :] \ + + Uj[self.aerogrid['set_k'][members_of_i_surf, :][:, (0, 1, 2)]] + if self.show_cs: self.update_cs_display(points) else: self.setup_cs_display(points) - self.show_cs=True + self.show_cs = True mlab.draw(self.fig) - def setup_cs_display(self, points): + def setup_cs_display(self, points): self.ug1_cs = tvtk.UnstructuredGrid(points=points) # plot points as glyphs self.src_cs = mlab.pipeline.add_dataset(self.ug1_cs) - points = mlab.pipeline.glyph(self.src_cs, scale_mode='scalar', scale_factor=self.pscale, color=(1,0,0)) + points = mlab.pipeline.glyph( + self.src_cs, scale_mode='scalar', scale_factor=self.pscale, color=(1, 0, 0)) points.glyph.glyph.scale_mode = 'data_scaling_off' def update_cs_display(self, points): @@ -356,70 +401,76 @@ def update_cs_display(self, points): def hide_cell(self): self.src_cell.remove() - self.show_cell=False + self.show_cell = False mlab.draw(self.fig) - + def plot_cell(self, cell_data, show_cells): if self.show_cell: # self.update_cell_display(cell_data=cell_data) # The pure update doesn't work in case different shells are selected. self.hide_cell() - self.setup_cell_display(offsets=self.strcgrid['offset'], color=(0,0,1), p_scale=self.pscale, cell_data=cell_data, show_cells=show_cells) - self.show_cell=True + self.setup_cell_display(offsets=self.strcgrid['offset'], color=(0, 0, 1), p_scale=self.pscale, + cell_data=cell_data, show_cells=show_cells) + self.show_cell = True mlab.draw(self.fig) - + def setup_cell_display(self, offsets, color, p_scale, cell_data, show_cells): ug = tvtk.UnstructuredGrid(points=offsets) - #ug.point_data.scalars = scalars + # ug.point_data.scalars = scalars if 'strcshell' in self.model: # plot shell as surface - shells = []; data = [] + shells = [] + data = [] for i_shell in range(self.model['strcshell']['n'][()]): shell = self.model['strcshell']['cornerpoints'][i_shell] if shell in show_cells: data.append(cell_data[i_shell]) - shells.append([np.where(self.strcgrid['ID']==id)[0][0] for id in shell[np.isfinite(shell)]]) + shells.append([np.where(self.strcgrid['ID'] == id)[0][0] + for id in shell[np.isfinite(shell)]]) shell_type = tvtk.Polygon().cell_type ug.set_cells(shell_type, shells) ug.cell_data.scalars = data self.src_cell = mlab.pipeline.add_dataset(ug) - #points = mlab.pipeline.glyph(self.src_cell, color=color, scale_factor=p_scale) - surface = mlab.pipeline.surface(self.src_cell, opacity=1.0, line_width=0.5, colormap='plasma', vmin=cell_data.min(), vmax=cell_data.max()) - surface.actor.property.edge_visibility=True - - surface.module_manager.scalar_lut_manager.show_legend=True - surface.module_manager.scalar_lut_manager.label_text_property.color=(0,0,0) - surface.module_manager.scalar_lut_manager.label_text_property.font_family='times' - surface.module_manager.scalar_lut_manager.label_text_property.bold=False - surface.module_manager.scalar_lut_manager.label_text_property.italic=False - surface.module_manager.scalar_lut_manager.number_of_labels=5 - - else: + # points = mlab.pipeline.glyph(self.src_cell, color=color, scale_factor=p_scale) + surface = mlab.pipeline.surface(self.src_cell, opacity=1.0, line_width=0.5, colormap='plasma', + vmin=cell_data.min(), vmax=cell_data.max()) + surface.actor.property.edge_visibility = True + + surface.module_manager.scalar_lut_manager.show_legend = True + surface.module_manager.scalar_lut_manager.label_text_property.color = ( + 0, 0, 0) + surface.module_manager.scalar_lut_manager.label_text_property.font_family = 'times' + surface.module_manager.scalar_lut_manager.label_text_property.bold = False + surface.module_manager.scalar_lut_manager.label_text_property.italic = False + surface.module_manager.scalar_lut_manager.number_of_labels = 5 + + else: # plot points as glyphs self.src_cell = mlab.pipeline.add_dataset(ug) - points = mlab.pipeline.glyph(self.src_cell, color=color, scale_factor=p_scale) + points = mlab.pipeline.glyph( + self.src_cell, color=color, scale_factor=p_scale) points.glyph.glyph.scale_mode = 'data_scaling_off' def update_cell_display(self, cell_data): self.src_cell.outputs[0].cell_data.scalars.from_array(cell_data) self.src_cell.update() - + def hide_iges(self): for src in self.src_iges: src.remove() - self.show_iges=False + self.show_iges = False mlab.draw(self.fig) - + def plot_iges(self, selected_meshes): self.src_iges = [] for mesh in self.iges_meshes: if mesh['desc'] in selected_meshes: self.setup_iges_display(mesh['vtk']) - self.show_iges=True + self.show_iges = True mlab.draw(self.fig) def setup_iges_display(self, vtk_object): src = mlab.pipeline.add_dataset(vtk_object) self.src_iges.append(src) - surface = mlab.pipeline.surface(src, opacity=0.4, line_width=0.5, color=(0.5, 0.5, 0.5)) - \ No newline at end of file + mlab.pipeline.surface( + src, opacity=0.4, line_width=0.5, color=(0.5, 0.5, 0.5)) diff --git a/modelviewer/pytran.py b/modelviewer/pytran.py index 0a89b55..e07db4b 100644 --- a/modelviewer/pytran.py +++ b/modelviewer/pytran.py @@ -1,171 +1,76 @@ - -import numpy as np import tables - -class NastranSOL101: +import numpy as np + + +class NastranSOL101(): + def __init__(self): self.celldata = {} - pass - + def load_file(self, filename): - self.filename = filename - self.file= tables.open_file(self.filename) + self.filename = filename + self.file = tables.open_file(self.filename) self.read_data() - self.file.close() - + self.file.close() + def add_model(self, model): self.model = model - self.strcgrid = model.strcgrid - self.strcshell = model.strcshell - + self.strcgrid = model.strcgrid + self.strcshell = model.strcshell + def prepare_celldata(self): self.merge_shells() self.map_nastran2strcgrid() self.shell_thickness() - #self.shell_stress_faster() - #self.set_material_properties() - #self.shell_FI() - + def read_bin(self, bin_stream, components): # create empty data structure - dict = {} + data = {} for component in components: - dict[component] = [] + data[component] = [] # read binary data stream and sort into python data structure for row in bin_stream: for component in components: - dict[component].append(row[component]) - return dict - + data[component].append(row[component]) + return data + def read_data(self): - # INPUTs - if 'CQUAD4' in self.file.root.NASTRAN.INPUT.ELEMENT._v_children: + # INPUTs + if 'CQUAD4' in self.file.root.NASTRAN.INPUT.ELEMENT._v_children: bin_stream = self.file.root.NASTRAN.INPUT.ELEMENT.CQUAD4 components = ["EID", "PID", "T"] self.cquad4 = self.read_bin(bin_stream, components) self.cquad4['n'] = self.cquad4['EID'].__len__() else: - self.cquad4 = {"EID":[], "PID":[], "T":[]} - - if 'CTRIA3' in self.file.root.NASTRAN.INPUT.ELEMENT._v_children: + self.cquad4 = {"EID": [], "PID": [], "T": []} + + if 'CTRIA3' in self.file.root.NASTRAN.INPUT.ELEMENT._v_children: bin_stream = self.file.root.NASTRAN.INPUT.ELEMENT.CTRIA3 components = ["EID", "PID", "T"] self.ctria3 = self.read_bin(bin_stream, components) self.ctria3['n'] = self.ctria3['EID'].__len__() else: - self.ctria3 = {"EID":[], "PID":[], "T":[]} - - if 'PSHELL' in self.file.root.NASTRAN.INPUT.PROPERTY._v_children: + self.ctria3 = {"EID": [], "PID": [], "T": []} + + if 'PSHELL' in self.file.root.NASTRAN.INPUT.PROPERTY._v_children: bin_stream = self.file.root.NASTRAN.INPUT.PROPERTY.PSHELL components = ['PID', 'MID1', 'MID2', 'MID3', 'T'] self.pshell = self.read_bin(bin_stream, components) - -# # RESULTs for Shells -# bin_stream = self.file.root.NASTRAN.RESULT.ELEMENTAL.STRESS.QUAD4 -# components = ["EID", "X1", "Y1", "XY1", "X2", "Y2", "XY2", "DOMAIN_ID"] -# self.stress_quad4 = self.read_bin(bin_stream, components) - - - if 'PCOMP' in self.file.root.NASTRAN.INPUT.PROPERTY._v_children: - bin_stream = self.file.root.NASTRAN.INPUT.PROPERTY.PCOMP.IDENTITY - components = ['PID', 'NPLIES'] - self.pcomp = self.read_bin(bin_stream, components) - - bin_stream = self.file.root.NASTRAN.INPUT.PROPERTY.PCOMP.PLY - components = ['MID', 'T', 'THETA'] - plies = self.read_bin(bin_stream, components) - self.pcomp['plies'] = plies - -# # RESULTs for Composite -# bin_stream = self.file.root.NASTRAN.RESULT.ELEMENTAL.STRESS.QUAD4_COMP -# components = ["EID", "PLY", "X1", "Y1", "T1", "DOMAIN_ID"] -# self.stress_quad4_comp = self.read_bin(bin_stream, components) -# -# bin_stream = self.file.root.NASTRAN.RESULT.ELEMENTAL.STRESS.TRIA3_COMP -# components = ["EID", "PLY", "X1", "Y1", "T1", "DOMAIN_ID"] -# self.stress_tria3_comp = self.read_bin(bin_stream, components) -# -# bin_stream = self.file.root.NASTRAN.RESULT.ELEMENTAL.STRAIN.QUAD4_COMP -# components = ["EID", "PLY", "X1", "Y1", "T1", "DOMAIN_ID"] -# self.strain_quad4_comp = self.read_bin(bin_stream, components) -# -# bin_stream = self.file.root.NASTRAN.RESULT.ELEMENTAL.STRAIN.TRIA3_COMP -# components = ["EID", "PLY", "X1", "Y1", "T1", "DOMAIN_ID"] -# self.strain_tria3_comp = self.read_bin(bin_stream, components) - -# bin_stream = self.file.root.NASTRAN.RESULT.DOMAINS -# components = ["ID", 'SUBCASE'] -# self.domains = self.read_bin(bin_stream, components) -# self.domains['n'] = self.domains['ID'].__len__() - + def merge_shells(self): self.shells = {} components = ["EID", "PID", "T"] for component in components: - self.shells[component] = np.array(self.cquad4[component] + self.ctria3[component]) - self.shells['n'] = self.shells['EID'].__len__() - -# self.results_shells_comp = {} -# components = ["EID", "PLY", "X1", "Y1", "T1", "DOMAIN_ID"] -# for component in components: -# self.results_shells_comp[component] = np.array(self.result_quad4_comp[component] + self.result_tria3_comp[component]) - + self.shells[component] = np.array( + self.cquad4[component] + self.ctria3[component]) + self.shells['n'] = len(self.shells['EID']) + def map_nastran2strcgrid(self): - self.map_nastran2strcgrid = [np.where(self.shells['EID']==ID)[0][0] for ID in self.strcshell['ID']] - + self.nastran2strcgrid = [np.where(self.shells['EID'] == ID)[ + 0][0] for ID in self.strcshell['ID']] + def shell_thickness(self): # map elements and properties from model and results - PIDs = self.shells['PID'][self.map_nastran2strcgrid] + PIDs = self.shells['PID'][self.nastran2strcgrid] pos_PID = [self.pshell['PID'].index(ID) for ID in PIDs] self.celldata['thickness'] = np.array(self.pshell['T'])[pos_PID] - - def shell_stress_faster(self): - sigma1_quad4 = np.array(self.result_quad4_comp['X1']).reshape(self.domains['n'], self.cquad4['n'],-1) - sigma2_quad4 = np.array(self.result_quad4_comp['Y1']).reshape(self.domains['n'], self.cquad4['n'],-1) - tau12_quad4 = np.array(self.result_quad4_comp['T1']).reshape(self.domains['n'], self.cquad4['n'],-1) - sigma1_tria3 = np.array(self.result_tria3_comp['X1']).reshape(self.domains['n'], self.ctria3['n'],-1) - sigma2_tria3 = np.array(self.result_tria3_comp['Y1']).reshape(self.domains['n'], self.ctria3['n'],-1) - tau12_tria3 = np.array(self.result_tria3_comp['T1']).reshape(self.domains['n'], self.ctria3['n'],-1) - self.celldata['sigma1'] = np.concatenate((sigma1_quad4, sigma1_tria3), axis=1)[:,self.map_nastran2strcgrid,:] - self.celldata['sigma2'] = np.concatenate((sigma2_quad4, sigma2_tria3), axis=1)[:,self.map_nastran2strcgrid,:] - self.celldata['tau12'] = np.concatenate((tau12_quad4, tau12_tria3), axis=1)[:,self.map_nastran2strcgrid,:] - - def set_material_properties(self): - #MAT8 11000001 1.55+11 8.5+9 .3 3.7+9 3.7+9 3.7+9 1510.+ - #+ 0.0 0.0 0.0 0.833+9 0.25+9 16.66+6 66.66+6 25.E6+ - #+ 0.0 0.0 - self.Xt = 0.833e+9 - self.Xc = 0.25e+9 - self.Yt = 16.66e+6 - self.Yc = 66.66e+6 - self.S = 25.0e+6 - - def shell_FI(self): - self.celldata['TsaiWu'] = np.zeros(self.celldata['sigma1'].shape) - self.celldata['TsaiHill'] = np.zeros(self.celldata['sigma1'].shape) - - F11 = (self.Xt*self.Xc)**-1 - F22 = (self.Yt*self.Yc)**-1 - Fss = (self.S**2)**-1 - F1 = 1.0/self.Xt - 1.0/self.Xc - F2 = 1.0/self.Yt - 1.0/self.Yc - F12 = 0.0 #-0.5*(F11*F22)**0.5 - - for i_subcase in range(self.domains['n']): - for i_shell in range(self.shells['n']): - nplies = self.celldata['sigma1'][i_subcase,i_shell].__len__() - for i_ply in range(nplies): - sigma1 = self.celldata['sigma1'][i_subcase,i_shell,i_ply] - sigma2 = self.celldata['sigma2'][i_subcase,i_shell,i_ply] - tau12 = self.celldata['tau12'][i_subcase,i_shell,i_ply] - FI_tsaiwu = F11*sigma1**2 + 2*F12*sigma1*sigma2 + F22*sigma2**2 + Fss*tau12**2 + F1*sigma1 + F2*sigma2 - # distinguish tension and compression for Tsai-Hill - if sigma1 > 0.0: X = self.Xt - else: X = self.Xc - if sigma2 > 0.0: Y = self.Yt - else: Y = self.Yc - S = self.S - FI_tsaihill = (sigma1/X)**2 + (sigma2/Y)**2 + (tau12/S)**2 - sigma1*sigma2/X**2 - - self.celldata['TsaiWu'][i_subcase,i_shell,i_ply] = FI_tsaiwu - self.celldata['TsaiHill'][i_subcase,i_shell,i_ply] = FI_tsaihill diff --git a/modelviewer/view.py b/modelviewer/view.py index a146a9d..8b7c6a7 100644 --- a/modelviewer/view.py +++ b/modelviewer/view.py @@ -16,8 +16,10 @@ from modelviewer.cfdgrid import TauGrid, SU2Grid from modelviewer.iges import IgesMesh + class Visualization(HasTraits): scene = Instance(MlabSceneModel, ()) + @on_trait_change('scene.activated') def update_plot(self): # This function is called when the view is opened. We don't @@ -32,7 +34,8 @@ def update_plot(self): height=600, width=600, show_label=False), resizable=True # We need this to resize with the parent widget ) - + + class MayaviQWidget(QtGui.QWidget): def __init__(self, parent=None): @@ -47,6 +50,7 @@ def __init__(self, parent=None): layout.addWidget(self.ui) self.ui.setParent(self) + class Modelviewer(): def __init__(self): @@ -68,7 +72,7 @@ def __init__(self): self.nc_opt['filters'] = "all files (*.*)" self.nc_opt['initialdir'] = os.getcwd() self.nc_opt['title'] = 'Open a Grid File' - + # define file options self.iges_opt = {} self.iges_opt['filters'] = "IGES files (*.igs *.iges);;all files (*.*)" @@ -89,11 +93,11 @@ def run(self): self.initGUI() # Start the main event loop. app.exec_() - + def test(self): """ - This function is intended for CI testing. - To test at least some parts of the code, the app is initialized, but never started. Instead, all windows are closed again. + This function is intended for CI testing. To test at least some parts of the code, the app is initialized, but + never started. Instead, all windows are closed again. """ app = QtGui.QApplication.instance() self.initGUI() @@ -114,7 +118,8 @@ def initGUI(self): def initTabs(self): # Configure tabs widget self.tabs_widget = QtGui.QTabWidget() - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy = QtGui.QSizePolicy( + QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) self.tabs_widget.setSizePolicy(sizePolicy) self.tabs_widget.setMinimumWidth(300) self.tabs_widget.setMaximumWidth(450) @@ -144,7 +149,8 @@ def initStrcTab(self): self.list_modes_mass = QtGui.QListWidget() self.list_modes_mass.itemSelectionChanged.connect(self.update_modes) self.list_modes_number = QtGui.QListWidget() - self.list_modes_number.itemSelectionChanged.connect(self.get_mode_data_for_plotting) + self.list_modes_number.itemSelectionChanged.connect( + self.get_mode_data_for_plotting) self.lb_freq = QtGui.QLabel('Frequency: {:0.4f} Hz'.format(0.0)) self.lb_uf = QtGui.QLabel('Scaling: 1.0') # slider for generalized coordinate magnification factor @@ -162,13 +168,13 @@ def initStrcTab(self): layout_strc = QtGui.QGridLayout(tab_strc) layout_strc.addWidget(lb_undeformed, 0, 0, 1, -1) layout_strc.addWidget(bt_strc_show, 1, 0, 1, -1) - layout_strc.addWidget(bt_strc_hide, 2, 0, 1, -1) + layout_strc.addWidget(bt_strc_hide, 2, 0, 1, -1) layout_strc.addWidget(lb_modes_mass, 3, 0, 1, 1) layout_strc.addWidget(lb_modes_number, 3, 1, 1, 1) layout_strc.addWidget(self.list_modes_mass, 4, 0, 1, 1) layout_strc.addWidget(self.list_modes_number, 4, 1, 1, 1) - layout_strc.addWidget(self.lb_freq, 5, 0, 1, -1) - layout_strc.addWidget(self.lb_uf, 6, 0, 1, -1) + layout_strc.addWidget(self.lb_freq, 5, 0, 1, -1) + layout_strc.addWidget(self.lb_uf, 6, 0, 1, -1) layout_strc.addWidget(self.sl_uf, 7, 0, 1, -1) layout_strc.addWidget(bt_mode_hide, 8, 0, 1, -1) @@ -177,7 +183,8 @@ def initMassTab(self): self.tabs_widget.addTab(tab_mass, "mass") # Elements of mass tab self.list_mass = QtGui.QListWidget() - self.list_mass.itemSelectionChanged.connect(self.get_mass_data_for_plotting) + self.list_mass.itemSelectionChanged.connect( + self.get_mass_data_for_plotting) self.lb_rho = QtGui.QLabel('Rho: 2700 kg/m^3') # slider for generalized coordinate magnification factor self.sl_rho = QtGui.QSlider(QtCore.Qt.Horizontal) @@ -188,12 +195,13 @@ def initMassTab(self): self.sl_rho.setTickPosition(QtGui.QSlider.TicksBelow) self.sl_rho.setTickInterval(500) self.sl_rho.valueChanged.connect(self.get_mass_data_for_plotting) - self.lb_cg = QtGui.QLabel('CG: x={:0.4f}, y={:0.4f}, z={:0.4f} m'.format(0.0, 0.0, 0.0)) + self.lb_cg = QtGui.QLabel( + 'CG: x={:0.4f}, y={:0.4f}, z={:0.4f} m'.format(0.0, 0.0, 0.0)) self.lb_cg_mac = QtGui.QLabel('CG: x={:0.4f} % MAC'.format(0.0)) self.lb_mass = QtGui.QLabel('Mass: {:0.2f} kg'.format(0.0)) - self.lb_Ixx = QtGui.QLabel('Ixx: {:0.4g} kg m^2'.format(0.0)) - self.lb_Iyy = QtGui.QLabel('Iyy: {:0.4g} kg m^2'.format(0.0)) - self.lb_Izz = QtGui.QLabel('Izz: {:0.4g} kg m^2'.format(0.0)) + self.lb_Ixx = QtGui.QLabel('Ixx: {:0.4g} kg m^2'.format(0.0)) + self.lb_Iyy = QtGui.QLabel('Iyy: {:0.4g} kg m^2'.format(0.0)) + self.lb_Izz = QtGui.QLabel('Izz: {:0.4g} kg m^2'.format(0.0)) bt_mass_hide = QtGui.QPushButton('Hide') bt_mass_hide.clicked.connect(self.plotting.hide_masses) @@ -220,12 +228,15 @@ def initAeroTab(self): self.cb_w2gj.stateChanged.connect(self.get_aero_for_plotting) bt_aero_hide = QtGui.QPushButton('Hide') bt_aero_hide.clicked.connect(self.plotting.hide_aero) - self.lb_MAC = QtGui.QLabel('MAC: x={:0.4f}, y={:0.4f} m'.format(0.0, 0.0)) + self.lb_MAC = QtGui.QLabel( + 'MAC: x={:0.4f}, y={:0.4f} m'.format(0.0, 0.0)) self.lb_MAC2 = QtGui.QLabel('') self.list_markers = QtGui.QListWidget() - self.list_markers.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) # allow multiple selections - self.list_markers.itemSelectionChanged.connect(self.get_new_markers_for_plotting) + self.list_markers.setSelectionMode( + QtGui.QAbstractItemView.ExtendedSelection) # allow multiple selections + self.list_markers.itemSelectionChanged.connect( + self.get_new_markers_for_plotting) bt_cfdgrid_hide = QtGui.QPushButton('Hide CFD Grids') bt_cfdgrid_hide.clicked.connect(self.plotting.hide_cfdgrids) @@ -240,7 +251,7 @@ def initAeroTab(self): def initCouplingTab(self): tab_coupling = QtGui.QWidget() - self.tabs_widget.addTab(tab_coupling,"coupling") + self.tabs_widget.addTab(tab_coupling, "coupling") # Elements of coupling tab bt_coupling_show = QtGui.QPushButton('Show') bt_coupling_show.clicked.connect(self.plotting.plot_aero_strc_coupling) @@ -256,8 +267,9 @@ def initMonstationsTab(self): tab_monstations = QtGui.QWidget() self.tabs_widget.addTab(tab_monstations, "monstations") # Elements of monstations tab - self.list_monstations = QtGui.QListWidget() - self.list_monstations.itemSelectionChanged.connect(self.get_monstation_for_plotting) + self.list_monstations = QtGui.QListWidget() + self.list_monstations.itemSelectionChanged.connect( + self.get_monstation_for_plotting) self.lb_monstation_coord = QtGui.QLabel('Coord:') bt_monstations_hide = QtGui.QPushButton('Hide') bt_monstations_hide.clicked.connect(self.plotting.hide_monstations) @@ -301,25 +313,29 @@ def initPytranTab(self): self.tabs_widget.addTab(tab_pytran, "pytran") # Elements of results tab self.list_celldata = QtGui.QListWidget() - self.list_celldata.itemSelectionChanged.connect(self.get_new_cell_data_for_plotting) + self.list_celldata.itemSelectionChanged.connect( + self.get_new_cell_data_for_plotting) self.list_show_cells = QtGui.QListWidget() - self.list_show_cells.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) - self.list_show_cells.itemSelectionChanged.connect(self.get_new_cell_data_for_plotting) - + self.list_show_cells.setSelectionMode( + QtGui.QAbstractItemView.ExtendedSelection) + self.list_show_cells.itemSelectionChanged.connect( + self.get_new_cell_data_for_plotting) + bt_cell_hide = QtGui.QPushButton('Hide Nastran results') bt_cell_hide.clicked.connect(self.plotting.hide_cell) - + layout_pytran = QtGui.QGridLayout(tab_pytran) layout_pytran.addWidget(self.list_celldata, 0, 0, 1, 1) layout_pytran.addWidget(self.list_show_cells, 0, 1, 1, 1) layout_pytran.addWidget(bt_cell_hide, 1, 0, 1, -1) - + def initIgesTab(self): tab_iges = QtGui.QWidget() self.tabs_widget.addTab(tab_iges, "iges") - + self.list_iges = QtGui.QListWidget() - self.list_iges.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) # allow multiple selections + self.list_iges.setSelectionMode( + QtGui.QAbstractItemView.ExtendedSelection) # allow multiple selections self.list_iges.itemSelectionChanged.connect(self.get_iges_for_plotting) bt_iges_hide = QtGui.QPushButton('Hide IGES') bt_iges_hide.clicked.connect(self.plotting.hide_iges) @@ -333,7 +349,8 @@ def initMayaviFigure(self): # --- set up Mayavi Figure --- # ---------------------------- self.mayavi_widget = MayaviQWidget(self.container) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy = QtGui.QSizePolicy( + QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.mayavi_widget.setSizePolicy(sizePolicy) fig = self.mayavi_widget.visualization.update_plot() self.plotting.add_figure(fig) @@ -351,7 +368,8 @@ def initWindow(self): loadButtonModel.triggered.connect(self.load_model) fileMenu.addAction(loadButtonModel) - self.loadButtonNastran = QtGui.QAction('Load Nastran results', self.window) + self.loadButtonNastran = QtGui.QAction( + 'Load Nastran results', self.window) self.loadButtonNastran.setShortcut('Ctrl+R') self.loadButtonNastran.setDisabled(True) self.loadButtonNastran.triggered.connect(self.load_nastran_results) @@ -361,12 +379,12 @@ def initWindow(self): self.loadButtonTauGrid.setShortcut('Ctrl+T') self.loadButtonTauGrid.triggered.connect(self.load_tau_grid) fileMenu.addAction(self.loadButtonTauGrid) - + self.loadButtonSU2Grid = QtGui.QAction('Load SU2 Grid', self.window) self.loadButtonSU2Grid.setShortcut('Ctrl+S') self.loadButtonSU2Grid.triggered.connect(self.load_su2_grid) fileMenu.addAction(self.loadButtonSU2Grid) - + self.loadButtonIges = QtGui.QAction('Load IGES', self.window) self.loadButtonIges.setShortcut('Ctrl+I') self.loadButtonIges.triggered.connect(self.load_iges) @@ -409,7 +427,7 @@ def update_modes(self): if tmp is not None: old_mode = tmp.data(0) self.list_modes_number.clear() - for mode in range(1, self.model['mass'][key]['n_modes'][()]+1): + for mode in range(1, self.model['mass'][key]['n_modes'][()] + 1): item = QtGui.QListWidgetItem(str(mode)) self.list_modes_number.addItem(item) if tmp is not None and int(old_mode) == mode: @@ -417,20 +435,21 @@ def update_modes(self): self.get_mode_data_for_plotting() def get_mode_data_for_plotting(self): - uf_i = np.sign(self.sl_uf.value()) * (self.sl_uf.value()/5.0)**2.0 + uf_i = np.sign(self.sl_uf.value()) * (self.sl_uf.value() / 5.0) ** 2.0 self.lb_uf.setText('Scaling: {:0.2f}'.format(uf_i)) if self.list_modes_mass.currentItem() is not None and self.list_modes_number.currentItem() is not None: key = self.list_modes_mass.currentItem().data(0) mass = self.model['mass'][key] - i_mode = int(self.list_modes_number.currentItem().data(0))-1 - uf = np.zeros((mass['n_modes'][()],1)) + i_mode = int(self.list_modes_number.currentItem().data(0)) - 1 + uf = np.zeros((mass['n_modes'][()], 1)) uf[i_mode] = uf_i ug = mass['PHIf_strc'][()].T.dot(uf) - offset_f = ug[self.model['strcgrid']['set'][:,:3]].squeeze() - self.plotting.plot_mode(self.model['strcgrid']['offset'][()]+offset_f) + offset_f = ug[self.model['strcgrid']['set'][:, :3]].squeeze() + self.plotting.plot_mode( + self.model['strcgrid']['offset'][()] + offset_f) # the eigenvalue directly corresponds to the generalized stiffness if Mass is scaled to 1.0 eigenvalue = mass['Kff'][()].diagonal()[i_mode] - freq = np.real(eigenvalue)**0.5 /2/np.pi + freq = np.real(eigenvalue) ** 0.5 / 2 / np.pi self.lb_freq.setText('Frequency: {:0.4f} Hz'.format(freq)) def get_mass_data_for_plotting(self, *args): @@ -441,24 +460,26 @@ def get_mass_data_for_plotting(self, *args): Mgg = load_hdf5_sparse_matrix(self.model['mass'][key]['MGG']) Mb = self.model['mass'][key]['Mb'][()] cggrid = load_hdf5_dict(self.model['mass'][key]['cggrid']) - self.plotting.plot_masses(Mgg, Mb , cggrid, rho) - self.lb_cg.setText('CG: x={:0.4f}, y={:0.4f}, z={:0.4f} m'.format(cggrid['offset'][0,0], - cggrid['offset'][0,1], - cggrid['offset'][0,2])) + self.plotting.plot_masses(Mgg, Mb, cggrid, rho) + self.lb_cg.setText('CG: x={:0.4f}, y={:0.4f}, z={:0.4f} m'.format(cggrid['offset'][0, 0], + cggrid['offset'][0, 1], + cggrid['offset'][0, 2])) # cg_mac = (x_cg - x_mac)*c_ref * 100 [%] # negativ bedeutet Vorlage --> stabil - cg_mac = (cggrid['offset'][0,0]-self.MAC[0])/self.model['macgrid']['c_ref'][()]*100.0 + cg_mac = (cggrid['offset'][0, 0] - self.MAC[0]) / \ + self.model['macgrid']['c_ref'][()] * 100.0 if cg_mac == 0.0: rating = 'indifferent' elif cg_mac < 0.0: rating = 'stable' elif cg_mac > 0.0: rating = 'unstable' - self.lb_cg_mac.setText('CG: x={:0.4f} % MAC, {}'.format(cg_mac, rating)) - self.lb_mass.setText('Mass: {:0.2f} kg'.format(Mb[0,0])) - self.lb_Ixx.setText('Ixx: {:0.4g} kg m^2'.format(Mb[3,3])) - self.lb_Iyy.setText('Iyy: {:0.4g} kg m^2'.format(Mb[4,4])) - self.lb_Izz.setText('Izz: {:0.4g} kg m^2'.format(Mb[5,5])) + self.lb_cg_mac.setText( + 'CG: x={:0.4f} % MAC, {}'.format(cg_mac, rating)) + self.lb_mass.setText('Mass: {:0.2f} kg'.format(Mb[0, 0])) + self.lb_Ixx.setText('Ixx: {:0.4g} kg m^2'.format(Mb[3, 3])) + self.lb_Iyy.setText('Iyy: {:0.4g} kg m^2'.format(Mb[4, 4])) + self.lb_Izz.setText('Izz: {:0.4g} kg m^2'.format(Mb[5, 5])) def get_monstation_for_plotting(self, *args): if self.list_monstations.currentItem() is not None: @@ -466,40 +487,45 @@ def get_monstation_for_plotting(self, *args): pos = list(self.model['mongrid']['name'].asstr()).index(key) monstation_id = self.model['mongrid']['ID'][pos] self.plotting.plot_monstations(monstation_id) - self.lb_monstation_coord.setText('Coord: {}'.format(self.model['mongrid']['CD'][pos])) - + self.lb_monstation_coord.setText( + 'Coord: {}'.format(self.model['mongrid']['CD'][pos])) + def calc_MAC(self, key): # The mean aerodynamic center is calculated from the aerodynamics. # This approach includes also the downwash from wing on HTP. - Qjj = self.model['aero'][key]['Qjj'][()] - PHIlk = load_hdf5_sparse_matrix(self.model['PHIlk']) - Dkx1 = self.model['Dkx1'][()] - aerogrid = load_hdf5_dict(self.model['aerogrid']) - macgrid = load_hdf5_dict(self.model['macgrid']) + Qjj = self.model['aero'][key]['Qjj'][()] + PHIlk = load_hdf5_sparse_matrix(self.model['PHIlk']) + Dkx1 = self.model['Dkx1'][()] + aerogrid = load_hdf5_dict(self.model['aerogrid']) + macgrid = load_hdf5_dict(self.model['macgrid']) # assume unit downwash - Ujx1 = np.dot(self.model['Djx1'][()],[0,0,1.0,0,0,0]) - wj = np.sum(aerogrid['N'][:] * Ujx1[aerogrid['set_j'][:,(0,1,2)]],axis=1) - fl = aerogrid['N'].T*aerogrid['A']*np.dot(Qjj, wj) - Pl = np.zeros(aerogrid['n']*6) - Pl[aerogrid['set_l'][:,0]] = fl[0,:] - Pl[aerogrid['set_l'][:,1]] = fl[1,:] - Pl[aerogrid['set_l'][:,2]] = fl[2,:] + Ujx1 = np.dot(self.model['Djx1'][()], [0, 0, 1.0, 0, 0, 0]) + wj = np.sum(aerogrid['N'][:] + * Ujx1[aerogrid['set_j'][:, (0, 1, 2)]], axis=1) + fl = aerogrid['N'].T * aerogrid['A'] * np.dot(Qjj, wj) + Pl = np.zeros(aerogrid['n'] * 6) + Pl[aerogrid['set_l'][:, 0]] = fl[0, :] + Pl[aerogrid['set_l'][:, 1]] = fl[1, :] + Pl[aerogrid['set_l'][:, 2]] = fl[2, :] Pmac = Dkx1.T.dot(PHIlk.T.dot(Pl)) self.MAC = np.zeros(3) - self.MAC[0] = macgrid['offset'][0,0] - Pmac[4] / Pmac[2] - self.MAC[1] = macgrid['offset'][0,1] + Pmac[3] / Pmac[2] + self.MAC[0] = macgrid['offset'][0, 0] - Pmac[4] / Pmac[2] + self.MAC[1] = macgrid['offset'][0, 1] + Pmac[3] / Pmac[2] self.plotting.MAC = self.MAC - + def get_aero_for_plotting(self): if self.list_aero.currentItem() is not None: key = self.list_aero.currentItem().data(0) self.calc_MAC(key) - self.lb_MAC.setText('MAC: x={:0.4f}, y={:0.4f} m'.format(self.MAC[0], self.MAC[1])) - self.lb_MAC2.setText('(based on AIC from "{}", rigid, subsonic)'.format(key)) + self.lb_MAC.setText( + 'MAC: x={:0.4f}, y={:0.4f} m'.format(self.MAC[0], self.MAC[1])) + self.lb_MAC2.setText( + '(based on AIC from "{}", rigid, subsonic)'.format(key)) if self.cb_w2gj.isChecked(): if self.plotting.show_aero: self.plotting.hide_aero() - self.plotting.plot_aero(self.model['camber_twist']['cam_rad'][()]/np.pi*180.0) + self.plotting.plot_aero( + self.model['camber_twist']['cam_rad'][()] / np.pi * 180.0) else: if self.plotting.show_aero: self.plotting.hide_aero() @@ -519,7 +545,8 @@ def get_cs_data_for_plotting(self, *args): if self.list_cs.currentItem() is not None: # determine cs key = self.list_cs.currentItem().data(0) - i_surf = np.where(self.model['x2grid']['key'].asstr()[:] == key)[0][0] + i_surf = np.where(self.model['x2grid'] + ['key'].asstr()[:] == key)[0][0] axis = self.cb_axis.currentText() # hand over for plotting self.plotting.plot_cs(i_surf, axis, deg) @@ -529,7 +556,7 @@ def get_new_cell_data_for_plotting(self, *args): items = self.list_show_cells.selectedItems() show_cells = [int(item.text()) for item in items] key = self.list_celldata.currentItem().data(0) - celldata = self.nastran.celldata[key] + celldata = self.nastran.celldata[key] self.plotting.plot_cell(celldata, show_cells) def get_new_markers_for_plotting(self, *args): @@ -544,7 +571,7 @@ def get_markers_for_plotting(self, *args): items = self.list_markers.selectedItems() selected_markers = [item.text() for item in items] self.plotting.plot_cfdgrids(selected_markers) - + def get_iges_for_plotting(self, *args): if self.list_iges.currentItem() is not None: # determine marker @@ -553,10 +580,11 @@ def get_iges_for_plotting(self, *args): if self.plotting.show_iges: self.plotting.hide_iges() self.plotting.plot_iges(selected_meshes) - + def load_model(self): # open file dialog - filename = QtGui.QFileDialog.getOpenFileName(self.window, self.file_opt['title'], self.file_opt['initialdir'], self.file_opt['filters'])[0] + filename = QtGui.QFileDialog.getOpenFileName(self.window, self.file_opt['title'], + self.file_opt['initialdir'], self.file_opt['filters'])[0] if filename != '': # load model self.model = data_handling.load_hdf5(filename) @@ -574,13 +602,13 @@ def update_fields(self): for key in self.model['mass'].keys(): self.list_mass.addItem(QtGui.QListWidgetItem(key)) self.list_modes_mass.addItem(QtGui.QListWidgetItem(key)) - + self.list_aero.clear() for key in self.model['aero'].keys(): self.list_aero.addItem(QtGui.QListWidgetItem(key)) self.list_cs.clear() - for key in self.model['x2grid']['key'].asstr() : + for key in self.model['x2grid']['key'].asstr(): self.list_cs.addItem(QtGui.QListWidgetItem(key)) self.list_monstations.clear() @@ -589,7 +617,8 @@ def update_fields(self): self.list_monstations.addItem(QtGui.QListWidgetItem(str(name))) def load_nastran_results(self): - filename = QtGui.QFileDialog.getOpenFileName(self.window, self.hdf5_opt['title'], self.hdf5_opt['initialdir'], self.hdf5_opt['filters'])[0] + filename = QtGui.QFileDialog.getOpenFileName(self.window, self.hdf5_opt['title'], + self.hdf5_opt['initialdir'], self.hdf5_opt['filters'])[0] if filename != '': self.nastran.load_file(filename) self.nastran.add_model(self.model) @@ -606,7 +635,8 @@ def update_celldata(self): self.list_show_cells.addItem(QtGui.QListWidgetItem(str(key))) def load_tau_grid(self): - filename = QtGui.QFileDialog.getOpenFileName(self.window, self.nc_opt['title'], self.nc_opt['initialdir'], self.nc_opt['filters'])[0] + filename = QtGui.QFileDialog.getOpenFileName(self.window, self.nc_opt['title'], + self.nc_opt['initialdir'], self.nc_opt['filters'])[0] if filename != '': self.tabs_widget.setCurrentIndex(2) self.cfdgrid = TauGrid() @@ -614,9 +644,10 @@ def load_tau_grid(self): self.plotting.add_cfdgrids(self.cfdgrid.cfdgrids) self.update_markers() self.nc_opt['initialdir'] = os.path.split(filename)[0] - + def load_su2_grid(self): - filename = QtGui.QFileDialog.getOpenFileName(self.window, self.nc_opt['title'], self.nc_opt['initialdir'], self.nc_opt['filters'])[0] + filename = QtGui.QFileDialog.getOpenFileName(self.window, self.nc_opt['title'], + self.nc_opt['initialdir'], self.nc_opt['filters'])[0] if filename != '': self.tabs_widget.setCurrentIndex(2) self.cfdgrid = SU2Grid() @@ -629,24 +660,27 @@ def update_markers(self): self.list_markers.clear() for cfdgrid in self.cfdgrid.cfdgrids: self.list_markers.addItem(QtGui.QListWidgetItem(cfdgrid['desc'])) - + def load_iges(self): - filename = QtGui.QFileDialog.getOpenFileName(self.window, self.iges_opt['title'], self.iges_opt['initialdir'], self.iges_opt['filters'])[0] + filename = QtGui.QFileDialog.getOpenFileName(self.window, self.iges_opt['title'], + self.iges_opt['initialdir'], self.iges_opt['filters'])[0] if filename != '': self.tabs_widget.setCurrentIndex(6) self.iges.load_file(filename) self.plotting.add_iges_meshes(self.iges.meshes) self.update_list_iges() self.iges_opt['initialdir'] = os.path.split(filename)[0] - + def update_list_iges(self): self.list_iges.clear() for mesh in self.iges.meshes: self.list_iges.addItem(QtGui.QListWidgetItem(mesh['desc'])) + def command_line_interface(): m = Modelviewer() m.run() - + + if __name__ == "__main__": command_line_interface() diff --git a/scripts/launch.py b/scripts/launch.py index b83191a..32dd0c3 100644 --- a/scripts/launch.py +++ b/scripts/launch.py @@ -2,6 +2,6 @@ # Here you launch the Loads Kernel with your job k = program_flow.Kernel('jcl_Discus2c', pre=True, main=True, post=True, test=False, - path_input='../../loads-kernel-examples/Discus2c/JCLs', - path_output='../../loads-kernel-examples/Discus2c/output') + path_input='../../loads-kernel-examples/Discus2c/JCLs', + path_output='../../loads-kernel-examples/Discus2c/output') k.run() diff --git a/scripts/merge.py b/scripts/merge.py index abd9235..e896be9 100755 --- a/scripts/merge.py +++ b/scripts/merge.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- -""" -Created on Thu Nov 27 14:00:31 2014 - -@author: voss_ar -""" -import getpass, platform, logging, sys, copy +import copy +import getpass +import logging +import platform import numpy as np from loadskernel.io_functions import data_handling @@ -12,88 +10,99 @@ from loadskernel import plotting_standard from loadskernel import program_flow + class Merge: + def __init__(self, path_input, path_output): - self.datasets = { 'ID':[], - 'jcl':[], - 'monstations':[], - 'response':[], - 'dyn2stat':[], - 'desc': [], - 'color':[], - 'n': 0, - } + self.datasets = {'ID': [], + 'jcl': [], + 'monstations': [], + 'response': [], + 'dyn2stat': [], + 'desc': [], + 'color': [], + 'n': 0, + } self.common_monstations = np.array([]) - - self.path_input = data_handling.check_path(path_input) - self.path_output = data_handling.check_path(path_output) - + + self.path_input = data_handling.check_path(path_input) + self.path_output = data_handling.check_path(path_output) + def load_job(self, job_name): # load jcl jcl = data_handling.load_jcl(job_name, self.path_input, jcl=None) - - logging.info( '--> Loading monstations(s).' ) - monstations = data_handling.load_hdf5(self.path_output + 'monstations_' + job_name + '.hdf5') - logging.info( '--> Loading dyn2stat.' ) - dyn2stat_data = data_handling.load_hdf5(self.path_output + 'dyn2stat_' + job_name + '.hdf5') - + logging.info('--> Loading monstations(s).') + monstations = data_handling.load_hdf5( + self.path_output + 'monstations_' + job_name + '.hdf5') + + logging.info('--> Loading dyn2stat.') + dyn2stat_data = data_handling.load_hdf5( + self.path_output + 'dyn2stat_' + job_name + '.hdf5') + # save into data structure - self.datasets['ID'].append(self.datasets['n']) + self.datasets['ID'].append(self.datasets['n']) self.datasets['jcl'].append(jcl) self.datasets['monstations'].append(monstations) self.datasets['dyn2stat'].append(dyn2stat_data) - self.datasets['desc'].append('dataset '+ str(self.datasets['n'])) + self.datasets['desc'].append('dataset ' + str(self.datasets['n'])) self.datasets['n'] += 1 - - def load_jobs(self, jobs_to_merge): - for job_name in jobs_to_merge: + + def load_jobs(self, job_names): + for job_name in job_names: logging.info('job:' + job_name) self.load_job(job_name) self.find_common_monstations() - + def find_common_monstations(self): - keys = [monstations.keys() for monstations in self.datasets['monstations']] - self.common_monstations = sorted(list(set(keys[0]).intersection(*keys[1:]))) - - def run_merge(self, job_name, jobs_to_merge): - - k = program_flow.Kernel(job_name, path_input=self.path_input, path_output=self.path_output) + keys = [monstations.keys() + for monstations in self.datasets['monstations']] + self.common_monstations = sorted( + list(set(keys[0]).intersection(*keys[1:]))) + + def run_merge(self, job_name, job_names): + + k = program_flow.Kernel( + job_name, path_input=self.path_input, path_output=self.path_output) k.setup_path() k.setup_logger() - logging.info( 'Starting Loads Merge') - logging.info( 'user ' + getpass.getuser() + ' on ' + platform.node() + ' (' + platform.platform() +')') - self.model = data_handling.load_hdf5(self.path_output + 'model_' + jobs_to_merge[0] + '.hdf5') - self.load_jobs(jobs_to_merge) + logging.info('Starting Loads Merge') + logging.info('user ' + getpass.getuser() + ' on ' + + platform.node() + ' (' + platform.platform() + ')') + self.model = data_handling.load_hdf5( + self.path_output + 'model_' + job_names[0] + '.hdf5') + self.load_jobs(job_names) self.build_new_dataset() self.plot_monstations(job_name) self.build_auxiliary_output(job_name) logging.info('Loads Merge finished.') k.print_logo() - + def build_new_dataset(self): # Init new datastructure new_monstations = {} - new_dyn2stat = {'Pg':[], - 'subcases':[], - 'subcases_ID':[]} + new_dyn2stat = {'Pg': [], + 'subcases': [], + 'subcases_ID': []} # Take first jcl as baseline, clear out trim- and simcases new_jcl = copy.deepcopy(self.datasets['jcl'][0]) - new_jcl.trimcase=[] - new_jcl.simcase=[] - + new_jcl.trimcase = [] + new_jcl.simcase = [] + # Merge datasets for x in range(self.datasets['n']): logging.info('Working on {} ...'.format(self.datasets['desc'][x])) # Append trimcases new_jcl.trimcase += self.datasets['jcl'][x].trimcase - new_jcl.simcase += self.datasets['jcl'][x].simcase + new_jcl.simcase += self.datasets['jcl'][x].simcase # Append dyn2stat - new_dyn2stat['Pg'] += list(self.datasets['dyn2stat'][x]['Pg'][()]) - new_dyn2stat['subcases'] += list(self.datasets['dyn2stat'][x]['subcases'].asstr()[:]) - new_dyn2stat['subcases_ID'] += list(self.datasets['dyn2stat'][x]['subcases_ID'][()]) - + new_dyn2stat['Pg'] += list(self.datasets['dyn2stat'][x]['Pg'][()]) + new_dyn2stat['subcases'] += list(self.datasets['dyn2stat'] + [x]['subcases'].asstr()[:]) + new_dyn2stat['subcases_ID'] += list( + self.datasets['dyn2stat'][x]['subcases_ID'][()]) + # Handle monstations for station in self.common_monstations: if station not in new_monstations: @@ -102,51 +111,58 @@ def build_new_dataset(self): 'CP': self.datasets['monstations'][x][station]['CP'][()], 'offset': self.datasets['monstations'][x][station]['offset'][()], 'subcases': [], - 'loads':[], - 't':[], + 'loads': [], + 't': [], } - # Merge. - new_monstations[station]['loads'] += list(self.datasets['monstations'][x][station]['loads'][()]) - new_monstations[station]['subcases'] += list(self.datasets['monstations'][x][station]['subcases'].asstr()[:]) - new_monstations[station]['t'] += list(self.datasets['monstations'][x][station]['t'][()]) + # Merge. + new_monstations[station]['loads'] += list( + self.datasets['monstations'][x][station]['loads'][()]) + new_monstations[station]['subcases'] += list( + self.datasets['monstations'][x][station]['subcases'].asstr()[:]) + new_monstations[station]['t'] += list( + self.datasets['monstations'][x][station]['t'][()]) # Save into existing data structure. self.new_dataset_id = self.datasets['n'] - self.datasets['ID'].append(self.new_dataset_id) + self.datasets['ID'].append(self.new_dataset_id) self.datasets['monstations'].append(new_monstations) self.datasets['dyn2stat'].append(new_dyn2stat) self.datasets['jcl'].append(new_jcl) - self.datasets['desc'].append('dataset '+ str(self.datasets['n'])) + self.datasets['desc'].append('dataset ' + str(self.datasets['n'])) self.datasets['n'] += 1 def plot_monstations(self, job_name): - logging.info( '--> Drawing some plots.' ) - jcl = self.datasets['jcl'][self.new_dataset_id] - monstations = self.datasets['monstations'][self.new_dataset_id] + logging.info('--> Drawing some plots.') + jcl = self.datasets['jcl'][self.new_dataset_id] + monstations = self.datasets['monstations'][self.new_dataset_id] plt = plotting_standard.LoadPlots(jcl, model=self.model) # determine crit trimcases graphically plt.add_monstations(monstations) - plt.plot_monstations(self.path_output + 'monstations_' + job_name + '.pdf') + plt.plot_monstations(self.path_output + + 'monstations_' + job_name + '.pdf') # store crit trimcases self.crit_trimcases = plt.crit_trimcases - + def build_auxiliary_output(self, job_name): - logging.info( '--> Saving auxiliary output data.') - jcl = self.datasets['jcl'][self.new_dataset_id] + logging.info('--> Saving auxiliary output data.') + jcl = self.datasets['jcl'][self.new_dataset_id] dyn2stat_data = self.datasets['dyn2stat'][self.new_dataset_id] - monstations = self.datasets['monstations'][self.new_dataset_id] - - aux_out = auxiliary_output.AuxiliaryOutput(jcl=jcl, model=self.model, trimcase=jcl.trimcase) + monstations = self.datasets['monstations'][self.new_dataset_id] + + aux_out = auxiliary_output.AuxiliaryOutput( + jcl=jcl, model=self.model, trimcase=jcl.trimcase) aux_out.crit_trimcases = self.crit_trimcases aux_out.dyn2stat_data = dyn2stat_data aux_out.monstations = monstations - - aux_out.write_critical_sectionloads(self.path_output + 'monstations_' + job_name) - aux_out.write_critical_trimcases(self.path_output + 'crit_trimcases_' + job_name + '.csv') - aux_out.write_critical_nodalloads(self.path_output + 'nodalloads_' + job_name + '.bdf') - + + aux_out.write_critical_sectionloads( + self.path_output + 'monstations_' + job_name) + aux_out.write_critical_trimcases( + self.path_output + 'crit_trimcases_' + job_name + '.csv') + aux_out.write_critical_nodalloads( + self.path_output + 'nodalloads_' + job_name + '.bdf') + + if __name__ == "__main__": - jobs_to_merge = ['jcl_HAP-O6-loop7_maneuver', 'jcl_HAP-O6-loop7_gust_unsteady_FMU', 'jcl_HAP-O6-loop7_landing', 'jcl_HAP-O6-loop7_prop'] - m = Merge(path_input='/scratch/HAP_workingcopy/JCLs', path_output='/scratch/HAP_LoadsKernel') - m.run_merge('jcl_HAP-O6_merged_loop7', jobs_to_merge) - \ No newline at end of file + m = Merge(path_input='/path/to/JCLs', path_output='/path/to/output') + m.run_merge('jcl_merged', ['jcl_a', 'jcl_b', 'jcl_c']) diff --git a/setup.py b/setup.py index 5b5ce67..d28ed84 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,20 @@ """ -Setup file -Install Loads Kernel via: +Setup file +Install Loads Kernel via: - pip install --user -e -In case Panel-Aero is not yet installed: +In case Panel-Aero is not yet installed: - pip install git+https://gitlab.dlr.de/loads-kernel/panel-aero.git """ from setuptools import setup, find_packages + def my_setup(): setup(name='Loads-Kernel', version='2023.08', - description='The Loads Kernel Software allows for the calculation of quasi-steady and dynamic maneuver loads, unsteady gust loads in the time and frequency domain as well as dynamic landing loads based on a generic landing gear module.', + description="""The Loads Kernel Software allows for the calculation of quasi-steady and dynamic maneuver loads, + unsteady gust loads in the time and frequency domain as well as dynamic landing loads based on a generic landing + gear module.""", url='https://github.com/DLR-AE/LoadsKernel', author='Arne Voß', author_email='arne.voss@dlr.de', @@ -22,22 +25,20 @@ def my_setup(): 'loads-compare=loadscompare.compare:command_line_interface']}, include_package_data=True, package_data={'loadskernel': ['graphics/*.*'], - 'loadscompare': ['graphics/*.*'],}, + 'loadscompare': ['graphics/*.*'], }, python_requires='>=3.8', - install_requires=[ - 'Panel-Aero @ git+https://github.com/DLR-AE/PanelAero.git', + install_requires=['Panel-Aero @ git+https://github.com/DLR-AE/PanelAero.git', 'matplotlib', 'mayavi', - 'traits', - 'traitsui', - 'pyface', + 'traits', + 'traitsui', + 'pyface', 'pyiges @ git+https://github.com/pyvista/pyiges.git', 'numpy', 'scipy', 'psutil', 'h5py', 'tables', - 'mpi4py', 'pytest', 'pytest-cov', 'pyyaml', @@ -45,8 +46,9 @@ def my_setup(): 'jupyter', 'jupyter-book', ], - extras_require={'FMI': ['pyfmi']}, + extras_require={'FMI': ['pyfmi', 'mpi4py']}, ) + if __name__ == '__main__': my_setup() diff --git a/tests/helper_functions.py b/tests/helper_functions.py index f2b0063..353e4d4 100644 --- a/tests/helper_functions.py +++ b/tests/helper_functions.py @@ -1,28 +1,30 @@ +import logging +import h5py import numpy as np from scipy.sparse import issparse -import logging, h5py -class HelperFunctions(object): - + +class HelperFunctions(): + # List of items that are skipped. - # This makes the addidtion of new stuff easier and compatible with older reference results. + # This makes the addidtion of new stuff easier and compatible with older reference results. list_skip = [] # List of items where the sign shall be ignored. - # This is usefull for the comparison of matrices related to eigenvalues and eigenvectors. - list_ignore_sign = ['Mff', 'Mhh', 'Kff', 'Khh', 'Mfcg', - 'PHIf_strc', 'PHIh_strc', 'PHIjf', 'PHIlf', 'PHIkf', 'PHIjh', 'PHIlh', 'PHIkh', 'PHIf_extra', 'PHIf_sensor', + # This is usefull for the comparison of matrices related to eigenvalues and eigenvectors. + list_ignore_sign = ['Mff', 'Mhh', 'Kff', 'Khh', 'Mfcg', + 'PHIf_strc', 'PHIh_strc', 'PHIjf', 'PHIlf', 'PHIkf', 'PHIjh', 'PHIlh', 'PHIkh', 'PHIf_extra', + 'PHIf_sensor', 'Uf', 'dUf_dt', 'd2Uf_dt2', 'Pf', 'X', 'Y', 'eigenvalues', 'eigenvectors', 'freqs', 'jac', 'A', 'B', 'C', 'D', 'X0', 'rigid_derivatives'] - def compare_lists(self, list_a, list_b, key=''): is_equal = [] for item_a, item_b in zip(list_a, list_b): - if type(item_a) == list: + if isinstance(item_a, list): is_equal += [self.compare_lists(item_a, item_b, key)] - elif type(item_a) in [dict, h5py.Group]: + elif isinstance(item_a, (dict, h5py.Group)): is_equal += [self.compare_dictionaries(item_a, item_b)] - elif type(item_a) == h5py.Dataset: + elif isinstance(item_a, h5py.Dataset): is_equal += [self.compare_hdf5_datasets(item_a, item_b, key)] else: is_equal += [self.compare_items(item_a, item_b, key)] @@ -33,21 +35,21 @@ def compare_dictionaries(self, dict_a, dict_b): for key in dict_a: if key in self.list_skip: logging.info('Skipping {}'.format(key)) - else: + else: logging.info('Comparing {}'.format(key)) - if type(dict_a[key]) in [dict, h5py.Group]: + if isinstance(dict_a[key], (dict, h5py.Group)): # dive deeper into the dicionary this_dict_is_equal = [self.compare_dictionaries(dict_a[key], dict_b[key])] - elif type(dict_a[key]) == list: + elif isinstance(dict_a[key], list): # dive deeper into list this_dict_is_equal = [self.compare_lists(dict_a[key], dict_b[key], key)] - elif type(dict_a[key]) == h5py.Dataset: + elif isinstance(dict_a[key], h5py.Dataset): # dive deeper into the HDF5 file this_dict_is_equal = [self.compare_hdf5_datasets(dict_a[key], dict_b[key], key)] else: # compare items this_dict_is_equal = [self.compare_items(dict_a[key], dict_b[key], key)] - + if not np.all(this_dict_is_equal): logging.warning("{} does NOT match reference".format(key)) is_equal += this_dict_is_equal @@ -57,7 +59,7 @@ def compare_hdf5_datasets(self, item_a, item_b, key): # for hdf5 files, there are two way to access the data, either with [()] or with [:] try: return self.compare_items(item_a[()], item_b[()], key) - except: + except Exception: return self.compare_items(item_a[()], item_b, key) def compare_items(self, item_a, item_b, key): @@ -68,8 +70,8 @@ def compare_items(self, item_a, item_b, key): if issparse(item_a): # sparse efficiency, compare != instead of == - result = np.all((item_a != item_b).toarray() == False) - elif type(item_a) == np.ndarray: + result = np.all(np.invert((item_a != item_b).toarray())) + elif isinstance(item_a, np.ndarray): if item_a.dtype == 'object': # numpy objects can be compare with np.equal result = np.all(np.equal(item_a, item_b)) diff --git a/tests/test_dummy.py b/tests/test_dummy.py deleted file mode 100644 index 3313ff5..0000000 --- a/tests/test_dummy.py +++ /dev/null @@ -1,6 +0,0 @@ -import platform - -def test_dummy(): - print('The dummy test is executed.') - print('Running on python version {}'.format(platform.python_version())) - pass \ No newline at end of file diff --git a/tests/test_gui.py b/tests/test_gui.py index a39051e..905a1d4 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -3,16 +3,18 @@ from loadscompare import compare from modelviewer import view + class TestLoadsCompare(): - + def test_gui(self): logging.info('Testing Loads Compare') c = compare.Compare() c.test() + class TestModelViewer(): - + def test_gui(self): logging.info('Testing Model Viewer') m = view.Modelviewer() - m.test() \ No newline at end of file + m.test() diff --git a/tests/test_integration.py b/tests/test_integration.py index 865c284..e5ffc5f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,74 +1,83 @@ -import logging, pytest, subprocess, shlex, os - -from loadskernel import program_flow, io_functions -from tests.helper_functions import HelperFunctions - """ -For the following tests, the loads-kernel-examples and the loads-kernel-reference-results are +For the following tests, the loads-kernel-examples and the loads-kernel-reference-results are used, which are located in dedictaed repositories. The examples are cloned by the CI-Pipeline. -The reference results are not updated automatically during testing, this has to be done manually. -Finally, a tempory directory is used for the outputs in order to avoid pollution of the +The reference results are not updated automatically during testing, this has to be done manually. +Finally, a tempory directory is used for the outputs in order to avoid pollution of the repositories. """ -path_examples = './loads-kernel-examples/' + +import logging +import os +import shlex +import subprocess + +import pytest + +from loadskernel import program_flow, io_functions +from tests.helper_functions import HelperFunctions + + +path_examples = './loads-kernel-examples/' path_reference = '/work/voss_ar/loads-kernel-reference-results/' + @pytest.fixture(scope='class') def get_test_dir(tmpdir_factory): test_dir = tmpdir_factory.mktemp('output') test_dir = io_functions.data_handling.check_path(test_dir) return str(test_dir) + class TestDiscus2c(HelperFunctions): job_name = 'jcl_Discus2c' path_input = os.path.join(path_examples, 'Discus2c', 'JCLs') - + def test_preprocessing_functional(self, get_test_dir): # Here you launch the Loads Kernel with your job k = program_flow.Kernel(self.job_name, pre=True, main=False, post=False, - path_input=self.path_input, - path_output=get_test_dir) + path_input=self.path_input, + path_output=get_test_dir) k.run() - + def test_mainprocessing_functional(self, get_test_dir): # Here you launch the Loads Kernel with your job k = program_flow.Kernel(self.job_name, pre=False, main=True, post=False, - path_input=self.path_input, - path_output=get_test_dir) + path_input=self.path_input, + path_output=get_test_dir) k.run() - + def test_postprocessing_functional(self, get_test_dir): # Here you launch the Loads Kernel with your job k = program_flow.Kernel(self.job_name, pre=False, main=False, post=True, - path_input=self.path_input, - path_output=get_test_dir) + path_input=self.path_input, + path_output=get_test_dir) k.run() - + def test_preprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing model with reference') - model = io_functions.data_handling.load_hdf5(get_test_dir + 'model_' + self.job_name + '.hdf5') - reference_model = io_functions.data_handling.load_hdf5(path_reference + 'model_' + self.job_name + '.hdf5') + model = io_functions.data_handling.load_hdf5(get_test_dir + 'model_' + self.job_name + '.hdf5') + reference_model = io_functions.data_handling.load_hdf5(path_reference + 'model_' + self.job_name + '.hdf5') assert self.compare_dictionaries(model, reference_model), "model does NOT match reference" - + def test_mainprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing response with reference') - responses = io_functions.data_handling.load_hdf5_responses(self.job_name, get_test_dir) - reference_responses = io_functions.data_handling.load_hdf5_responses(self.job_name, path_reference) + responses = io_functions.data_handling.load_hdf5_responses(self.job_name, get_test_dir) + reference_responses = io_functions.data_handling.load_hdf5_responses(self.job_name, path_reference) assert self.compare_lists(responses, reference_responses), "response does NOT match reference" - + logging.info('Comparing monstations with reference') - monstations = io_functions.data_handling.load_hdf5(get_test_dir + 'monstations_' + self.job_name + '.hdf5') - reference_monstations = io_functions.data_handling.load_hdf5(path_reference + 'monstations_' + self.job_name + '.hdf5') + monstations = io_functions.data_handling.load_hdf5(get_test_dir + 'monstations_' + self.job_name + '.hdf5') + reference_monstations = io_functions.data_handling.load_hdf5(path_reference + 'monstations_' + self.job_name + '.hdf5') assert self.compare_dictionaries(monstations, reference_monstations), "monstations do NOT match reference" - + # do comparisons logging.info('Comparing dyn2stat with reference') dyn2stat_data = io_functions.data_handling.load_hdf5(get_test_dir + 'dyn2stat_' + self.job_name + '.hdf5') reference_dyn2stat_data = io_functions.data_handling.load_hdf5(path_reference + 'dyn2stat_' + self.job_name + '.hdf5') assert self.compare_dictionaries(dyn2stat_data, reference_dyn2stat_data), "dyn2stat does NOT match reference" - + def test_postprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing crit_trimcases with reference') @@ -77,18 +86,20 @@ def test_postprocessing_results(self, get_test_dir): with open(path_reference + 'crit_trimcases_' + self.job_name + '.csv', 'r') as f: reference_lines = f.readlines() assert self.compare_lists(lines, reference_lines), "crit_trimcases do NOT match reference" - + logging.info('Comparing subcases with reference') with open(get_test_dir + 'nodalloads_' + self.job_name + '.bdf_subcases', 'r') as f: lines = f.readlines() with open(path_reference + 'nodalloads_' + self.job_name + '.bdf_subcases', 'r') as f: reference_lines = f.readlines() - assert self.compare_lists(lines, reference_lines), "subcases do NOT match reference" + assert self.compare_lists( + lines, reference_lines), "subcases do NOT match reference" + class TestDiscus2cParallelProcessing(HelperFunctions): job_name = 'jcl_Discus2c_parallelprocessing' path_input = os.path.join(path_examples, 'Discus2c', 'JCLs') - + def test_preprocessing_functional_via_command_line_interface(self, get_test_dir): # Here we us the command line interface args = shlex.split('loads-kernel --job_name %s \ @@ -104,103 +115,113 @@ def test_mainprocessing_functional_via_command_line_interface(self, get_test_dir --path_input %s --path_output %s' % (self.job_name, self.path_input, get_test_dir)) out = subprocess.run(args, env=os.environ) assert out.returncode == 0, "subprocess failed: " + str(args) - + def test_preprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing model with reference') - model = io_functions.data_handling.load_hdf5(get_test_dir + 'model_' + self.job_name + '.hdf5') - reference_model = io_functions.data_handling.load_hdf5(path_reference + 'model_' + self.job_name + '.hdf5') + model = io_functions.data_handling.load_hdf5(get_test_dir + 'model_' + self.job_name + '.hdf5') + reference_model = io_functions.data_handling.load_hdf5(path_reference + 'model_' + self.job_name + '.hdf5') assert self.compare_dictionaries(model, reference_model), "model does NOT match reference" - + def test_mainprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing response with reference') - responses = io_functions.data_handling.load_hdf5_responses(self.job_name, get_test_dir) - reference_responses = io_functions.data_handling.load_hdf5_responses(self.job_name, path_reference) + responses = io_functions.data_handling.load_hdf5_responses(self.job_name, get_test_dir) + reference_responses = io_functions.data_handling.load_hdf5_responses(self.job_name, path_reference) assert self.compare_lists(responses, reference_responses), "response does NOT match reference" logging.info('Comparing monstations with reference') - monstations = io_functions.data_handling.load_hdf5(get_test_dir + 'monstations_' + self.job_name + '.hdf5') - reference_monstations = io_functions.data_handling.load_hdf5(path_reference + 'monstations_' + self.job_name + '.hdf5') + monstations = io_functions.data_handling.load_hdf5(get_test_dir + 'monstations_' + self.job_name + '.hdf5') + reference_monstations = io_functions.data_handling.load_hdf5(path_reference + 'monstations_' + self.job_name + '.hdf5') assert self.compare_dictionaries(monstations, reference_monstations), "monstations do NOT match reference" # do comparisons logging.info('Comparing dyn2stat with reference') - dyn2stat_data = io_functions.data_handling.load_hdf5(get_test_dir + 'dyn2stat_' + self.job_name + '.hdf5') + dyn2stat_data = io_functions.data_handling.load_hdf5(get_test_dir + 'dyn2stat_' + self.job_name + '.hdf5') reference_dyn2stat_data = io_functions.data_handling.load_hdf5(path_reference + 'dyn2stat_' + self.job_name + '.hdf5') assert self.compare_dictionaries(dyn2stat_data, reference_dyn2stat_data), "dyn2stat does NOT match reference" + class TestDiscus2cNonlinSteady(TestDiscus2c): job_name = 'jcl_Discus2c_nonlin_steady' path_input = os.path.join(path_examples, 'Discus2c', 'JCLs') - + + class TestDiscus2cTimedom(TestDiscus2c): job_name = 'jcl_Discus2c_timedom' path_input = os.path.join(path_examples, 'Discus2c', 'JCLs') - + + class TestDiscus2cB2000(TestDiscus2c): job_name = 'jcl_Discus2c_B2000' path_input = os.path.join(path_examples, 'Discus2c', 'JCLs') - + + class TestAllegraTimedom(TestDiscus2c): job_name = 'jcl_ALLEGRA_timedom' path_input = os.path.join(path_examples, 'Allegra', 'JCLs') - + + class TestAllegraFreqdom(TestDiscus2c): job_name = 'jcl_ALLEGRA_freqdom' path_input = os.path.join(path_examples, 'Allegra', 'JCLs') - + + class TestAllegraFlutter(HelperFunctions): job_name = 'jcl_ALLEGRA_flutter' path_input = os.path.join(path_examples, 'Allegra', 'JCLs') - + def test_preprocessing_functional(self, get_test_dir): # Here you launch the Loads Kernel with your job k = program_flow.Kernel(self.job_name, pre=True, main=False, post=False, - path_input=self.path_input, - path_output=get_test_dir) + path_input=self.path_input, + path_output=get_test_dir) k.run() - + def test_mainprocessing_functional(self, get_test_dir): # Here you launch the Loads Kernel with your job k = program_flow.Kernel(self.job_name, pre=False, main=True, post=False, - path_input=self.path_input, - path_output=get_test_dir) + path_input=self.path_input, + path_output=get_test_dir) k.run() - + def test_postprocessing_functional(self, get_test_dir): # Here you launch the Loads Kernel with your job k = program_flow.Kernel(self.job_name, pre=False, main=False, post=True, - path_input=self.path_input, - path_output=get_test_dir) + path_input=self.path_input, + path_output=get_test_dir) k.run() - + def test_preprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing model with reference') - model = io_functions.data_handling.load_hdf5(get_test_dir + 'model_' + self.job_name + '.hdf5') - reference_model = io_functions.data_handling.load_hdf5(path_reference + 'model_' + self.job_name + '.hdf5') + model = io_functions.data_handling.load_hdf5(get_test_dir + 'model_' + self.job_name + '.hdf5') + reference_model = io_functions.data_handling.load_hdf5(path_reference + 'model_' + self.job_name + '.hdf5') assert self.compare_dictionaries(model, reference_model), "model does NOT match reference" - + def test_mainprocessing_results(self, get_test_dir): # do comparisons logging.info('Comparing response with reference') - responses = io_functions.data_handling.load_hdf5_responses(self.job_name, get_test_dir) - reference_responses = io_functions.data_handling.load_hdf5_responses(self.job_name, path_reference) + responses = io_functions.data_handling.load_hdf5_responses(self.job_name, get_test_dir) + reference_responses = io_functions.data_handling.load_hdf5_responses(self.job_name, path_reference) assert self.compare_lists(responses, reference_responses), "response does NOT match reference" - + + class TestAllegraLimitTurbulence(TestAllegraFlutter): job_name = 'jcl_ALLEGRA_limitturbulence' path_input = os.path.join(path_examples, 'Allegra', 'JCLs') - + + class TestHAPO6Trim(TestDiscus2c): job_name = 'jcl_HAP-O6' path_input = os.path.join(path_examples, 'HAP-O6', 'JCLs') - + + class TestHAPO6Derivatives(TestAllegraFlutter): job_name = 'jcl_HAP-O6_derivatives' path_input = os.path.join(path_examples, 'HAP-O6', 'JCLs') - + + class TestHAPO6StateSpaceSystem(TestAllegraFlutter): job_name = 'jcl_HAP-O6_sss' path_input = os.path.join(path_examples, 'HAP-O6', 'JCLs')