├── .gitignore ├── .travis.yml ├── Doxyfile ├── FenicsSolver ├── CoupledNavierStokesSolver.py ├── FSISolver.py ├── LargeDeformationSolver.py ├── LinearElasticitySolver.py ├── NonlinearElasticitySolver.py ├── ScalarTransportDGSolver.py ├── ScalarTransportSolver.py ├── SolverBase.py ├── __init__.py └── main.py ├── FenicsSolver_FreeCAD.png ├── LICENSE ├── Readme.md ├── data ├── TestHeatTransfer.json ├── mesh.xml ├── mesh_facet_region.xml └── mesh_physical_region.xml ├── doc ├── Fenics18_Xia.pdf ├── fsi_velmag.mp4 └── upgrade your script from Fenics 2017 to 2019.py ├── examples ├── config.py ├── run_all_tests.py ├── test_cfd_solver.py ├── test_customized_case_settings.py ├── test_electrostatics.py ├── test_flow_pass_cylinder.py ├── test_heat_transfer.py ├── test_large_deformation.py ├── test_linear_elasticity.py └── test_nonlinear_elasticity.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # specific to this project 7 | examples/navier_stokes_cylinder/* 8 | 9 | #doxygen 10 | docs/* 11 | 12 | # C extensions 13 | *.so 14 | .hidden 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # dotenv 90 | .env 91 | 92 | # virtualenv 93 | .venv 94 | venv/ 95 | ENV/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | #dist: bionic 2 | #dist: xenial is not supported before Nov 2018, python2 is the default for trusty 3 | #dist: bionic is now available 4 | # https://blog.travis-ci.com/2018-11-08-xenial-release 5 | # docker is another way to test out this FenicsSolver 6 | 7 | 8 | sudo: required 9 | language: python 10 | jobs: 11 | include: 12 | - os: linux 13 | dist: bionic 14 | python: 3.6 15 | env: TOXENV=DOLFIN 16 | 17 | - os: linux 18 | dist: xenial 19 | python: 2.7 # 20 | env: TOXENV=DOLFIN # later using python 3.7 for dolfinX 21 | 22 | #services: 23 | # - docker 24 | 25 | #branches: 26 | # only: 27 | # - master 28 | 29 | # To add unlisted APT sources, follow instructions in https://docs.travis-ci.com/user/installing-dependencies#Installing-Packages-with-the-APT-Addon 30 | before_install: 31 | - sudo add-apt-repository -y ppa:fenics-packages/fenics 32 | - sudo apt-get -q update 33 | 34 | # shared by all env matrix 35 | install: 36 | - sudo apt-get update -q 37 | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then travis_retry sudo apt-get install 38 | -y python-matplotlib python-numpy git doxygen python-dolfin fenics; fi 39 | - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then travis_retry sudo apt-get install 40 | -y python3-matplotlib python3-numpy git doxygen python3-dolfin fenics; fi 41 | - echo "current working dir during install stage is $(pwd)" && ls 42 | 43 | # already git clone this repo: git clone --depth=50 --branch=master https://github.com/qingfengxia/FenicsSolver.git qingfengxia/FenicsSolver 44 | # cd into this repo 45 | 46 | 47 | # Use the YAML block scalar header (|) to allow easier multiline script coding. 48 | 49 | # #docker services 50 | #- docker pull quay.io/fenicsproject/stable:current 51 | #- docker run -v $(pwd):/home/fenics/shared -w /home/fenics/shared quay.io/fenicsproject/stable:current /bin/bash -c "echo $(pwd) && export BATCH && export PYTHONPATH=$(pwd):$PYTHONPATH && cd examples && python3 run_all_tests.py" 52 | ## output of docker, show the log 53 | #- docker logs 54 | 55 | # in travis, python will be set to version you specified above 56 | script: 57 | # test the installed fenics packages 58 | - if [ ${TOXENV} = 'DOLFIN' ]; then python -c 'import dolfin; print("Fenics version", dolfin.__version__)'; fi 59 | - if [ ${TOXENV} = 'DOLFINX' ]; then python -c 'import dolfin; print("Fenics version", dolfin.__version__)'; fi 60 | - echo "current working dir is $(pwd)" 61 | # test without installation 62 | - export PYTHONPATH=$(pwd):$PYTHONPATH 63 | - if [ -d 'examples' ] ; then cd examples && python run_all_tests.py && cd .. ; fi 64 | - if [ -d 'data' ] ; then cd data && python -m FenicsSolver TestHeatTransfer.json && cd .. ; fi 65 | #- doxygen Doxyfile 66 | 67 | # build the wheel, skip_cleanup: true 68 | - python setup.py bdist_wheel 69 | # TODO: github can host package 70 | 71 | # deploy to multiple providers are possible, make a list 72 | #~ deploy: 73 | #~ - provider: pages 74 | #~ skip_cleanup: true 75 | #~ local_dir: docs/html 76 | #~ github_token: $GH_REPO_TOKEN 77 | #~ on: 78 | #~ branch: hg_pages 79 | -------------------------------------------------------------------------------- /FenicsSolver/CoupledNavierStokesSolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2017 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | 25 | from __future__ import print_function, division 26 | #import math may cause error 27 | import numbers 28 | import numpy as np 29 | import os.path 30 | import copy 31 | 32 | from dolfin import * 33 | 34 | 35 | """ 36 | Feature: coupled velocity and pressure laminar flow with G2 stabilisaton 37 | 38 | General Galerkin (G2) stabilisaton (reference paper: ), but it still does not work for Re>10. 39 | Turbulent flow will not be implemented: use third-party solvers like Oasis: a high-level/high-performance open source Navier-Stokes solver 40 | OpenFOAM solver. 41 | 42 | TODO: 43 | 1. temperature induced natural convection, need solve thermal, constant thermal expansion coeffi 44 | see paper: 45 | 46 | 2. noninertial frame of reference 47 | linear accelation can be merged to body source, just as body 48 | + ALE: mesh moving velocity, for FSI solver 49 | + SRF: for centrifugal pump, add item of centrifugal and coriolis forces 50 | + MRF: for turbine blade and stator, should be done in a coupling way 51 | 52 | reference_frame_settings = {'type': 'ALE', 'mesh_velocity': vel} 53 | reference_frame_settings = {'type': 'SRF', 'center_point': (0, 0, 0), 'omega': -1} 54 | omega: angular velocity rad/s, minus sign mean ?? direction 55 | omega*omega*x_vector + cross(2*omega, u_vector) 56 | stab for coriolis force item? 57 | 58 | 3. nonlinear viscosity model, nu(T), nu(U, p, T), curerntly diverging ! 59 | """ 60 | 61 | from .SolverBase import SolverBase 62 | class CoupledNavierStokesSolver(SolverBase): 63 | """ incompressible and laminar flow only with G2 stabilisaton 64 | """ 65 | def __init__(self, case_input): 66 | 67 | if 'solving_temperature' in case_input: 68 | self.solving_temperature = case_input['solving_temperature'] 69 | else: 70 | self.solving_temperature = False 71 | SolverBase.__init__(self, case_input) 72 | self.compressible = False 73 | # init and reference must be provided by case setup 74 | 75 | self.using_nonlinear_solver = True 76 | #self.settings['solver_name'] = "CoupledNavierStokesSolver" 77 | if self.solving_temperature: 78 | self.settings['mixed_variable'] = ('velocity', 'pressure', 'temperature') 79 | else: 80 | self.settings['mixed_variable'] = ('velocity', 'pressure') 81 | ## Define solver parameters, underreleax, tolerance, max_iter 82 | # solver_parameters 83 | 84 | def generate_function_space(self, periodic_boundary): 85 | self.vel_degree = self.settings['fe_degree'] + 1 # order 3 is working for 2D elbow testing 86 | self.pressure_degree = self.settings['fe_degree'] 87 | self.is_mixed_function_space = True # FIXME: how to detect it is mixed, if function_space is provided 88 | self._update_function_space(periodic_boundary) 89 | 90 | def _update_function_space(self, periodic_boundary=None): 91 | V = VectorElement(self.settings['fe_family'], self.mesh.ufl_cell(), self.vel_degree) # degree 2, must be higher than pressure 92 | Q = FiniteElement(self.settings['fe_family'], self.mesh.ufl_cell(), self.pressure_degree) 93 | #T = FiniteElement("CG", self.mesh.ufl_cell(), 1) # temperature subspace, or just use Q 94 | if self.solving_temperature: 95 | mixed_element = MixedElement([V, Q, Q]) 96 | else: 97 | mixed_element = V * Q # MixedFunctionSpace has been removed from 2016.2, this API works only for 2 sub 98 | if periodic_boundary: 99 | self.function_space = FunctionSpace(self.mesh, mixed_element, constrained_domain=periodic_boundary) 100 | else: 101 | self.function_space = FunctionSpace(self.mesh, mixed_element) 102 | self.velocity_subfunction_space = self.function_space.sub(0) 103 | 104 | def update_solver_function_space(self, periodic_boundary=None): 105 | #used by FSI solver, after mesh moving 106 | self._update_function_space(periodic_boundary) 107 | 108 | self.trial_function = TrialFunction(self.function_space) 109 | self.test_function = TestFunction(self.function_space) 110 | # Define functions for new function space 111 | _w_current = Function(self.function_space) 112 | _w_current.vector()[:] = self.w_current.vector()[:] # assuming no topo change 113 | self.w_current = _w_current 114 | _w_prev = Function(self.function_space) # on previous time-step mesh 115 | _w_prev.vector()[:] = self.w_prev.vector()[:] # assuming no topo change 116 | self.w_prev = _w_prev # 117 | 118 | def get_body_source(self): 119 | # FIXME: source term type, centrifugal force is possible 120 | if 'body_source' in self.settings and self.settings['body_source']: 121 | body_force = self.translate_value(self.settings['body_source']) 122 | else: # default gravity 123 | if self.dimension == 2: 124 | body_force = Constant((0, -9.8)) 125 | else: 126 | body_force = Constant((0, 0, -9.8)) 127 | return body_force 128 | 129 | def get_initial_field(self): 130 | # assume: velocity is a tupe of constant, or string expression, or a function of the mixed functionspace 131 | print("self.initial_values = ", self.initial_values) 132 | 133 | if isinstance(self.initial_values, (Function,)): 134 | try: 135 | up0 = Function(self.initial_values) # same mesh and function space 136 | except: 137 | up0 = project(self.initial_values, self.function_space) 138 | return up0 139 | 140 | _initial_values = list(self.initial_values['velocity']) 141 | _initial_values.append(self.initial_values['pressure']) 142 | if self.solving_temperature: 143 | _initial_values.append(self.initial_values['temperature']) 144 | #self.function_space.ufl_element(), wht not working 145 | _initial_values_expr = Expression( tuple([str(v) for v in _initial_values]), degree = self.settings['fe_degree']) 146 | up0 = interpolate(_initial_values_expr, self.function_space) 147 | return up0 148 | 149 | def viscous_stress(self, up, T_space): 150 | u, p = split(up) # TODO: not general split 151 | if not T_space: 152 | T_space = TensorFunctionSpace(self.function_space.mesh(), 'CG', 1) 153 | sigma = project(self.viscosity()*(grad(u) + grad(u).T) - p*Identity(self.dimension), T_space, \ 154 | solver_type = 'mumps') 155 | return sigma 156 | 157 | def boundary_traction(self, up, target_space = None): 158 | # https://www.openfoam.com/documentation/cpp-guide/html/classFoam_1_1functionObjects_1_1externalCoupled.html#a1063d7a675858ee0e647e36abbefe463 159 | sigma = self.viscous_stress(up) # already included pressure 160 | n = FacetNormal(self.mesh) 161 | # stress_normal = dot(n, dot(sigma, n)) #scalar, pressure 162 | # (tangential stress vector) = dot(sigma, n) - stress_normal*n 163 | #traction = dot(sigma, n) # may need as_vector, can not project? 164 | traction = sigma[i,j]*n[j] # ufl.log.UFLException: Shapes do not match 165 | #traction = Constant((1e2*self.current_step, 0)) # TODO: tmp bypass for test 166 | #print('before traction project ...') 167 | if target_space: 168 | traction = interpolate(traction, target_space) # project does not work 169 | return traction 170 | 171 | def calc_drag_and_lift(self, up, drag_axis_index, lift_axis_index, boundary_index_list): 172 | # Compute force on cylinder, axis_index: 0 for x-axis, 1 for y_axis 173 | T = self.viscous_stress(up) 174 | n = FacetNormal(self.mesh) 175 | if (boundary_index_list and len(boundary_index_list)): 176 | drag = -T[drag_axis_index,j]*n[j]*self.ds(boundary_index_list[0]) 177 | lift = -T[lift_axis_index,j]*n[j]*self.ds(boundary_index_list[0]) 178 | for _i in boundary_index_list[1:]: 179 | drag -= T[drag_axis_index,j]*n[j]*self.ds(_i) # index j means summnation 180 | lift -= T[lift_axis_index,j]*n[j]*self.ds(_i) 181 | drag = assemble(drag) 182 | lift = assemble(lift) 183 | return drag, lift 184 | else: 185 | raise SolverError('Error: boundary_index_list must be specified to calc drag and lift forces') 186 | 187 | def viscous_heat(self, u, p): 188 | # shear heating power, FIXME: not tested code 189 | V_space = self.function_space.sub(0) 190 | return project(inner(self.viscosity()*(grad(u) + grad(u).T) - p*Identity(self.dimension), grad(u)) , V_space, 191 | solver_type = 'mumps', form_compilder_parameters = \ 192 | {'cpp_optimize': True, "representation": 'quadrature', "quadrature_degree": 2}) 193 | 194 | def viscosity(self, current_w=None): 195 | # TODO: define rheology_model = {}, strain_rate dep viscosity is not implemented 196 | if 'Newtonian' in self.material and (not self.material['Newtonian']): 197 | if not current_w: 198 | current_w = self.w_current # newton nonliner solver will replace self.trial_function to self.w_current 199 | if self.solving_temperature: 200 | u,p,T = split(current_w) 201 | _nu = self.material['kinematic_viscosity'] 202 | _nu = _nu * (1 + (p/self.reference_values['pressure']) * 0.1) 203 | _nu = _nu * (1 - (T/self.reference_values['temperature']) * 0.2) 204 | else: 205 | u,p = split(current_w) 206 | _nu = self.material['kinematic_viscosity'] 207 | _nu = _nu * pow(p/self.reference_values['pressure'], 0.1) 208 | else: 209 | _nu = self.material['kinematic_viscosity'] 210 | #if isinstance(_nu, (Constant, numbers.Number)): 211 | # nu = Constant(_nu) 212 | #else: #if nu is nu is string or function 213 | return _nu # nonlinear, nonNewtonian 214 | 215 | def generate_form(self, time_iter_, trial_function, test_function, up_current, up_prev): 216 | W = self.function_space 217 | ## boundary setup and update for each time step 218 | 219 | print("Updating boundary at time iter = {}".format(time_iter_)) 220 | ds = Measure("ds", subdomain_data=self.boundary_facets) 221 | if time_iter_ == 0: 222 | plot(self.boundary_facets, title ="boundary colored by ID") # diff color do visual diff boundary 223 | 224 | # Define unknown and test function(s) 225 | 226 | ## weak form 227 | if self.transient_settings['transient']: # temporal scheme 228 | F = self.F_transient(time_iter_, trial_function, test_function, up_current, up_prev) 229 | else: 230 | F = self.F_static(trial_function, test_function, up_current) 231 | # added boundary to F_static() or here to make Crank-Nicolson temporal scheme 232 | Dirichlet_bcs_up, F_bc = self.update_boundary_conditions(time_iter_, trial_function, test_function, ds) 233 | for it in F_bc: 234 | F += it 235 | 236 | if self.solving_temperature: # test not passed! 237 | F_T, T_bc = self.generate_thermal_form(time_iter_, trial_function, test_function, up_current, up_prev) 238 | F += F_T 239 | Dirichlet_bcs_up += T_bc 240 | 241 | if self.using_nonlinear_solver: 242 | F = action(F, up_current) 243 | self.J = derivative(F, up_current, trial_function) # 244 | 245 | return F, Dirichlet_bcs_up 246 | 247 | def generate_thermal_form(self, time_iter_, trial_function, test_function, up_current, up_prev): 248 | # temperature related items for the coupled vel, pressure and temperature form 249 | assert not self.compressible 250 | u, p, T = split(trial_function) 251 | v, q, Tq = split(test_function) 252 | u_current, p_current, T_current = split(up_current) 253 | u_prev, p_prev, T_prev = split(up_prev) 254 | 255 | #translate_setting, set the FunctionSpace 256 | Tsettings = copy.copy(self.settings) 257 | Tsettings['scalar_name'] = 'temperature' 258 | Tsettings['mesh'] = None 259 | Tsettings['function_space'] = self.function_space.sub(2) 260 | #Tsettings['convective_velocity'] = u_current # can not use u_trial_function 261 | #Tsettings['advection_settings'] = {'stabilization_method': 'SPUG', 'Pe': 1.0/(15.0/(4200*1000))} 262 | # it is possible to use SPUG (not rotating velocity), with and without body source 263 | Tsettings['advection_settings'] = {'stabilization_method': 'IP', 'alpha': 0.1} 264 | #Tsettings['body_source'] = # incomplate form? 265 | import pprint 266 | pprint.pprint(Tsettings) 267 | from FenicsSolver import ScalarTransportSolver 268 | Tsolver = ScalarTransportSolver.ScalarTransportSolver(Tsettings) 269 | #Tsover.is_mixed_function_space = False 270 | 271 | #print('type(u_current)', u_current, type(u_current)) # ufl.tensors.ListTensor 272 | #print('type(u)', u, type(u)) 273 | Tsolver.convective_velocity = u_current 274 | #ds should be passed to Tsolver? but they are actually same 275 | #convection stab can be a problem! 276 | F_T, T_bc = Tsolver.generate_form(time_iter_, T, Tq, T_current, T_prev) 277 | #inner() ufl.log.UFLException: Shapes do not match: and . 278 | tau = self.viscosity()*(grad(u) + grad(u).T) 279 | #sigma = tau- p*Identity(self.dimension) # u_current for both nonlinear and picard loop 280 | epsdot = 0.5 * (grad(u) + grad(u).T) 281 | viscous_heating = inner(epsdot, tau) # method 1 282 | #viscous_heating = 2 * self.viscosity(up_current) * tr(dot(epsdot, epsdot)) 283 | print('type of viscous heating:', type(viscous_heating)) 284 | ## sigma * u: heat flux, sigma * grad(u) heat source 285 | #F_T -= viscous_heating *Tq*dx # need sum to scalar! 286 | return F_T, T_bc 287 | 288 | def F_static(self, trial_function, test_function, up_0): 289 | if self.solving_temperature: 290 | u, p, T = split(trial_function) 291 | v, q, Tq = split(test_function) 292 | u_0, p_0, T_0 = split(up_0) 293 | else: 294 | u, p = split(trial_function) 295 | v, q = split(test_function) 296 | u_0, p_0 = split(up_0) 297 | def epsilon(u): 298 | """Return the symmetric gradient.""" 299 | return 0.5 * (grad(u) + grad(u).T) 300 | ''' 301 | if self.using_nonlinear_solver: 302 | advection_velocity = u 303 | nu = self.viscosity(trial_function) 304 | else: # using Picard loop FIXME: crank-nicolis scheme 305 | ''' 306 | advection_velocity = u_0 # u_0 is the current value to be solved for steady case 307 | nu = self.viscosity(up_0) # 308 | mu = nu*self.material['density'] 309 | rho = self.material['density'] 310 | #plot(nu, title = 'viscosity') # all zero 311 | #import matplotlib.pyplot as plt 312 | #plt.show() 313 | 314 | # Define Form for the static Stokes Coupled equation, divided by rho on both sides 315 | F = nu * 2.0*inner(epsilon(u), epsilon(v))*dx \ 316 | - (p/rho)*div(v)*dx \ 317 | + div(u)*(q/rho)*dx # comes from incomperssible flow equation, diveded by rho? or not divided by rho, does that make difference? 318 | if self.settings['body_source']: # just gravity, without * rho 319 | F -= inner(self.get_body_source(), v)*dx 320 | 321 | if 'reference_frame_settings' in self.settings: 322 | rfs = self.settings['reference_frame_settings'] 323 | if rfs['type'] == 'ALE': # also used in FSI mesh moving 324 | advection_velocity -= self.translate_value(rfs['mesh_velocity']) 325 | # treated mesh_velocity as part of advection, so it can be stabilized 326 | #elif rfs['type'] == 'SRF': 327 | # pass 328 | else: 329 | raise SolverError('reference_frame_settings type `{}` is not supported'.format(rfs['type'])) 330 | 331 | # Add advective term, technically, convection means 332 | F += inner(dot(grad(u), advection_velocity), v)*dx 333 | 334 | if 'advection_settings' in self.settings: 335 | ads = self.settings['advection_settings'] # a very big panelty factor can stabalize, but leading to diffusion error 336 | else: 337 | ads = {'stabilization_method': None} # default none 338 | 339 | if ads['stabilization_method'] and ads['stabilization_method'] == 'G2': 340 | """ 341 | ref: 342 | dt is uniform time step, and u^ ; T^ are values of velocity and temperature from previous time 343 | delta1 and delta2 are stablisation parameter defined in DG 344 | Johan Hoffman and Claes Johnson. Computational Turbulent Incompressible Flow, 345 | volume 4 of Applied Mathematics: Body and Soul. Springer, 2007. 346 | URL http://dx.doi.org/10.1007/978-3-540-46533-1. 347 | """ 348 | h = 2*Circumradius(self.mesh) # cell size 349 | if ads['Re']<=1: 350 | delta1 = ads['kappa1'] * h*h 351 | delta2 = ads['kappa2'] * h*h 352 | else: # convection dominant, test_f=0: 476 | # flux is a general flux density, heatFlux: W/m2 is not a general flux name 477 | g = self.translate_value(bc['value']) 478 | integrals_N.append(g*Tq*ds(i)) 479 | elif bc['type'] == 'HTC': # FIXME: HTC is not a general name or general type, only for thermal analysis 480 | #Robin, how to get the boundary value, T as the first, HTC as the second, does not apply to nonlinear PDE 481 | Ta = self.translate_value(bc['ambient']) 482 | htc = self.translate_value(bc['value']) # must be specified in Constant or Expressed in setup dict 483 | integrals_N.append( htc*(Ta-T)*Tq*ds(i)) 484 | else: 485 | print('temperature boundary type`{}` is not supported thus ignored'.format(bc['type'])) 486 | ''' 487 | else: 488 | print('boundary setup is done in scalar transport for incompressible flow'.format(bc['variable'])) 489 | ## end of boundary setup 490 | return Dirichlet_bcs_up, F_bc 491 | 492 | def solve_form(self, F, up_, Dirichlet_bcs_up): 493 | # only for static case? 494 | if self.using_nonlinear_solver: 495 | return self.solve_nonlinear_problem(F, up_, Dirichlet_bcs_up, self.J) 496 | else: # Solve Navier-Stokes problem with Picard method 497 | iter_ = 0 498 | max_iter = 50 499 | eps = 1.0 500 | tol = 1E-4 501 | under_relax_ratio = 0.7 502 | up_temp = Function(self.function_space) # a temporal to save value in the Picard loop 503 | 504 | timer_solver = Timer("TimerSolveStatic") 505 | timer_solver.start() 506 | while (iter_ < max_iter and eps > tol): 507 | # solve the linear stokes flow to avoid up_s = 0 508 | 509 | up_temp.assign(up_) 510 | # other solving methods 511 | up_ = self.solve_linear_problem(F, up_, Dirichlet_bcs_up) 512 | # AMG is not working with mixed function space 513 | 514 | #limiting result value, if the problem is highly nonlinear 515 | diff_up = up_.vector().get_local() - up_temp.vector().get_local() 516 | eps = np.linalg.norm(diff_up, ord=np.Inf) 517 | 518 | print("iter = {:d}; eps_up = {:e}; time elapsed = {}\n".format(iter_, eps, timer_solver.elapsed())) 519 | 520 | ## underreleax should be defined here, Courant number, 521 | up_.vector()[:] = up_temp.vector().get_local() + diff_up * under_relax_ratio 522 | 523 | iter_ += 1 524 | ## end of Picard loop 525 | timer_solver.stop() 526 | print("*" * 10 + " end of Navier-Stokes Picard iteration" + "*" * 10) 527 | 528 | return up_ 529 | 530 | def plot_result(self): 531 | if self.solving_temperature: 532 | u,p,T= split(self.result) 533 | if self.using_matplotlib: 534 | import matplotlib.pyplot as plt 535 | plt.figure() 536 | plot(u ,title = "velocity") 537 | plt.figure() 538 | plot(p, title = "pressure", scalarbar = True) 539 | plt.gca().legend(loc = 'best') 540 | plt.figure() 541 | plot(T, title = "temperature", scalarbar = True) 542 | plt.gca().legend(loc = 'best') 543 | else: 544 | plot(u) 545 | plot(p) 546 | plot(T) 547 | else: 548 | u,p= split(self.result) 549 | plot(u) 550 | plot(p) 551 | -------------------------------------------------------------------------------- /FenicsSolver/FSISolver.py: -------------------------------------------------------------------------------- 1 | # not yet fully tested code, 2 | # copyright qingfeng Xia 2018 3 | 4 | """ 5 | 6 | #How it works: 7 | 1. provide mesh with fluid and solid submeshes, 8 | 2. coupled solver will detect matching interfaces, modify solver setting (boundary condition) 9 | 3. create participant solvers (as a ordered list) 10 | 4. solve the fluid first, , setup solid boundary condition (traction force) for solid solver, no need to move mesh for solid solver 11 | 5. move mesh for fluid solver, w_current, w_prev need to be in different function space, in order to save deformed mesh and data 12 | 13 | Limitations: 14 | - serial mapping only 15 | - no movement relaxation 16 | - no higher Re fluid solver 17 | - will not support multiple frictional contact 18 | 19 | """ 20 | 21 | from __future__ import print_function, division, absolute_import 22 | from FenicsSolver.CoupledNavierStokesSolver import CoupledNavierStokesSolver 23 | from FenicsSolver.LargeDeformationSolver import LargeDeformationSolver 24 | from FenicsSolver.LinearElasticitySolver import LinearElasticitySolver 25 | from FenicsSolver.SolverBase import SolverBase, SolverError 26 | from dolfin import * 27 | import math, copy 28 | import numpy as np 29 | 30 | _debug = False 31 | 32 | class CoupledSolver(): 33 | """ This CoupledSolver class provide a skeleton for coupling sovler 34 | it contains a list of solver, and coordiate solvers in overrided `solve_current_step()` 35 | it can be used for sequential coupling, FSI, mixed-dimension solver 36 | """ 37 | def __init__(self, solver_input): 38 | self.settings = solver_input 39 | 40 | def solve_transient(self): 41 | # 42 | self.init_solver() 43 | file = File("pressure_output.pvd", "compressed") 44 | 45 | # Define a parameters for a stationary loop 46 | self.transient_settings = self.settings['transient_settings'] 47 | ts = self.settings['transient_settings'] 48 | self.current_time = ts['starting_time'] 49 | self.current_step = 0 50 | if ts['transient']: 51 | t_end = ts['ending_time'] 52 | else: 53 | t_end = self.current_time+ 1 54 | 55 | cs = self.settings['coupling_settings'] 56 | #print(ts, self.current_time, t_end) 57 | # Transient loop also works for steady, by set `t_end = self.time_step` 58 | timer_solver_all = Timer("TimerSolveAll") # 2017.2 Ubuntu Python2 errors 59 | timer_solver_all.start() 60 | while (self.current_time < t_end): 61 | if ts['transient']: 62 | dt = self.get_time_step(self.current_step) 63 | else: 64 | dt = 1 65 | 66 | # in the first step, initial flow field will be calc in a steady way, since up_current == up_prev 67 | for s in self.solver_list: 68 | s.current_step = self.current_step 69 | ## overloaded by derived classes, maybe move out of temporal loop if boundary does not change form 70 | self.solve_current_step() 71 | p_result = self.fluid_solver.w_current.split()[1] 72 | p_result.rename('pressure', 'label') 73 | file << (p_result, self.current_time) # todo: moved to fluid_solver 74 | u_result = self.fluid_solver.w_current.split()[0] 75 | u_result.rename('velocity', 'label') 76 | file << (u_result, self.current_time) # todo: moved to fluid_solver 77 | 78 | print("Current time = ", self.current_time, " TimerSolveAll = ", timer_solver_all.elapsed()) 79 | # stop for steady case, or update time 80 | 81 | if not self.transient_settings['transient']: 82 | break 83 | #quasi-static, check value change is small enough! 84 | 85 | self.current_step += 1 86 | self.current_time += dt 87 | ## end of time loop 88 | timer_solver_all.stop() 89 | self.plot_result() 90 | 91 | return [solver.result for solver in self.solver_list] 92 | 93 | def init_solver(self): 94 | for solver in self.solver_list: 95 | solver.init_solver() 96 | 97 | def solve(self): 98 | self.result = self.solve_transient() 99 | return self.result 100 | 101 | def plot_result(self): 102 | for solver in self.solver_list: 103 | solver.plot() 104 | 105 | def save(self): 106 | pass 107 | 108 | def get_time_step(self, time_iter_): 109 | ## fixed step, but could be supplied with an np.array/list 110 | try: 111 | dt = float(self.transient_settings['time_step']) 112 | except: 113 | ts = self.transient_settings['time_series'] 114 | if len(ts) >= time_iter_: 115 | dt = ts[time_iter_] - ts[time_iter_] 116 | else: 117 | print('time step can only be a sequence or scalar') 118 | #self.mesh.hmin() # Compute minimum cell diameter. courant number 119 | return dt 120 | 121 | def get_current_time(self, time_iter_): 122 | try: 123 | dt = float(self.transient_settings['time_step']) 124 | tp = self.transient_settings['starting_time'] + dt * (time_iter_ - 1) 125 | except: 126 | if len(self.transient_settings['time_series']) >= time_iter_: 127 | tp = self.transient_settings['time_series'][time_iter_] 128 | else: 129 | print('time point can only be a sequence of time series or derived from constant time step') 130 | return tp 131 | 132 | 133 | class FSISolver(CoupledSolver): 134 | """ 135 | """ 136 | def __init__(self, solver_input): 137 | self.settings = solver_input 138 | for s in self.settings['participants']: 139 | if s['solver_domain'] == "fluidic": 140 | self.fluid_solver = CoupledNavierStokesSolver(s['settings']) 141 | elif s['solver_domain'] == "elastic": 142 | #self.solid_solver = LargeDeformationSolver(s['settings']) 143 | self.solid_solver =LinearElasticitySolver(s['settings']) 144 | else: 145 | raise SolverError("unsupported subdomain solver: {}".format(s['solver_name'])) 146 | self.solver_list = [self.fluid_solver, self.solid_solver] 147 | self.detect_interfaces() 148 | self.original_solid_mesh = copy.copy(self.solid_solver.mesh) 149 | self.original_fluid_mesh = copy.copy(self.fluid_solver.mesh) 150 | 151 | self.using_submesh = True #submesh method, topo does NOT change with mesh moving? 152 | self.parent_mesh = self.settings['parent_mesh'] 153 | assert self.fluid_solver.settings['fe_degree']+1 == self.solid_solver.settings['fe_degree'] 154 | self.detect_interface_mapping() 155 | 156 | #self.parent_vector_function_space = VectorFunctionSpace(self.parent, 157 | # self.fluid_solver.settings['fe_family'], self.fluid_solver.settings['fe_degree']) 158 | #self.parent_facet_function = MeshFunction('double', self.parent_vector_function_space.mesh(), mesh.topology().dim() - 1) 159 | 160 | #self.solid_facet_function = FacetFunction('double', self.solid_vector_function_space) 161 | #self.fluid_facet_function = FacetFunction('double', self.fluid_vector_function_space) 162 | 163 | 164 | #self.solid_boundary = BoundaryMesh(self.solid_solver.mesh, "exterior") # instead of FacetMesh for better performance 165 | #self.fluid_boundary = BoundaryMesh(self.fluid_solver.mesh, "exterior") 166 | #from domain to boundary solid_boundary_f.interpolate(cellFunction) 167 | self.original_fb_vector_fs = VectorFunctionSpace(self.original_fluid_mesh, 168 | self.fluid_solver.settings['fe_family'], self.fluid_solver.settings['fe_degree']) 169 | self.original_sb_vector_fs = VectorFunctionSpace(self.original_solid_mesh, 170 | self.solid_solver.settings['fe_family'], self.solid_solver.settings['fe_degree']) 171 | 172 | self.mesh_offset = Function(self.original_fb_vector_fs) 173 | self.previous_fluid_mesh_disp = Function(self.original_fb_vector_fs) 174 | 175 | #################### SubmeshMapper ################# 176 | def detect_interface_mapping(self): 177 | #"For mixed FunctionSpaces vertex index is offset with the number of dofs per vertex" (this also applies for VectorFunctionSpaces). 178 | #dof mapping 179 | self.solid_parent_vi = self.solid_solver.mesh.data().array('parent_vertex_indices', 0) 180 | self.fluid_parent_vi = self.fluid_solver.mesh.data().array('parent_vertex_indices', 0) 181 | #print(self.solid_parent_vi) 182 | #print(len(self.fluid_parent_vi), type(self.fluid_parent_vi)) # find out shared vertex 183 | self.interface_parent_vi = np.intersect1d(self.solid_parent_vi, self.fluid_parent_vi) 184 | vl = [] 185 | for vi in self.interface_parent_vi: 186 | vl.append((np.nonzero(self.fluid_parent_vi == vi)[0][0], np.nonzero(self.solid_parent_vi == vi)[0][0])) 187 | self.interface_fluid_solid_vi = vl 188 | #print(self.interface_parent_vi, self.interface_fluid_solid_vi) 189 | 190 | # can be split into 2 function from here 191 | self.fluid_V1 = VectorFunctionSpace(self.fluid_solver.mesh, 192 | self.fluid_solver.settings['fe_family'], 1) # msut be 1 for vertex -> DoF mapping 193 | self.solid_V1 = VectorFunctionSpace(self.original_solid_mesh, 194 | self.solid_solver.settings['fe_family'], 1) # msut be 1 for vertex -> DoF mapping 195 | v2d = vertex_to_dof_map(self.solid_V1) 196 | self.solid_v2d = v2d.reshape((-1, self.solid_solver.dimension)) 197 | #print('self.solid_v2d = ', self.solid_v2d) 198 | 199 | # for scalar, vertex = numpy array 200 | v2d = vertex_to_dof_map(self.fluid_V1) #vector degree 1 201 | self.fluid_v2d = v2d.reshape((-1, self.fluid_solver.dimension)) 202 | #print('self.fluid_v2d = ', self.fluid_v2d) 203 | 204 | self.fluid_T1 = TensorFunctionSpace(self.fluid_solver.mesh, 205 | self.fluid_solver.settings['fe_family'], 1) # msut be 1 for vertex -> DoF mapping 206 | self.solid_T1 = TensorFunctionSpace(self.original_solid_mesh, 207 | self.solid_solver.settings['fe_family'], 1) # msut be 1 for vertex -> DoF mapping 208 | v2d = vertex_to_dof_map(self.fluid_T1) 209 | self.fluid_v2d_tensor = v2d.reshape((-1, self.fluid_solver.dimension* self.fluid_solver.dimension)) 210 | 211 | v2d = vertex_to_dof_map(self.solid_T1) 212 | #print('vertex_to_dof_map(self.solid_T1) = ', v2d.shape, v2d[:12]) 213 | self.solid_v2d_tensor = v2d.reshape((-1, self.solid_solver.dimension*self.solid_solver.dimension)) 214 | #print('self.solid_v2d_tensor = ', self.solid_v2d_tensor.shape, self.solid_v2d_tensor) 215 | 216 | #set(self.solid_parent_vi).intersection(set(self.fluid_parent_vi)); np.array(list( a_py_set)) # not efficient 217 | 218 | def map_solid_to_fluid_vector(self, solid_f, target_space): 219 | assert self.using_submesh 220 | # 221 | solid_V1_temp = project(solid_f, self.solid_V1) 222 | fluid_V1_temp = Function(self.fluid_V1) # set all DOF to zero? 223 | for fi, si in self.interface_fluid_solid_vi: 224 | fluid_V1_temp.vector()[self.fluid_v2d[fi]] = solid_V1_temp.vector()[self.solid_v2d[si]] 225 | return project(fluid_V1_temp, target_space) 226 | 227 | def map_fluid_to_solid_vector(self, fluid_f, target_space): 228 | assert self.using_submesh 229 | #rank 1 vector function 230 | fluid_V1_temp = project(fluid_f, self.fluid_V1) 231 | solid_V1_temp = Function(self.solid_V1) 232 | #print(self.fluid_v2d[0], fluid_V1_temp.vector()[self.fluid_v2d[0]]) 233 | for fi, si in self.interface_fluid_solid_vi: 234 | #print(fi, fluid_V1_temp.vector()[self.fluid_v2d[fi]]) 235 | solid_V1_temp.vector()[self.solid_v2d[si]] = fluid_V1_temp.vector()[self.fluid_v2d[fi]] 236 | return project(solid_V1_temp, target_space) 237 | 238 | def map_fluid_to_solid_tensor(self, sigma): 239 | #print( sigma.vector().get_local().shape) 1D Petsc vector, size = npoint * dim * dim 240 | boundary_stress = Function(self.solid_T1) # set all DOF to zero? 241 | for fi, si in self.interface_fluid_solid_vi: 242 | #print(fi, type(sigma.vector()[self.fluid_v2d_tensor[fi]]), sigma.vector()[self.fluid_v2d_tensor[fi]]) 243 | # reverse stress sensor from fluid to solid 244 | boundary_stress.vector()[self.solid_v2d_tensor[si]] = - sigma.vector()[self.fluid_v2d_tensor[fi]] 245 | return boundary_stress 246 | 247 | 248 | def solve_current_step(self): 249 | # only NS equation needs current value to build form 250 | self.fluid_solver.solve_current_step() 251 | if _debug: 252 | plot(self.fluid_solver.boundary_facets, title = "fluid boundary") 253 | plot(self.solid_solver.boundary_facets, title = "solid boundary") 254 | self.fluid_solver.plot() 255 | 256 | # self.up_trial_function, self.up_test_function, self.up_current, self.up_prev 257 | self.update_solid_interface(self.fluid_solver.w_current) # set solid boundary_conditions 258 | 259 | self.solid_solver.solve_current_step() 260 | if _debug: 261 | self.solid_solver.plot() 262 | 263 | #move fluid mesh and set the boundary 264 | mesh_disp = self.update_fluid_interface(self.solid_solver.w_current) # set solid boundary_conditions, 265 | self.move_fluid_interface(mesh_disp) 266 | # self.move_solid_interface() # not necessary for submeshing no interpolation 267 | 268 | def detect_interfaces(self, specific_type = 'FSI'): 269 | # matching by boundary name, not by coordinate coincidence, also comes from setting dict 270 | self.interfaces = {} # list of tuple of dict 271 | for key, bc in self.fluid_solver.settings['boundary_conditions'].items(): 272 | if 'coupling' in bc and bc['coupling'] == specific_type: 273 | if key in self.solid_solver.settings['boundary_conditions']: 274 | self.interfaces[key] = (bc, self.solid_solver.settings['boundary_conditions'][key]) 275 | else: 276 | raise SolverError('couplng boundary named `{}` in fluid_solver has no corresponding in solid_solver'.format(key)) 277 | assert self.interfaces, 'interfaces dict should not be empty' 278 | 279 | def update_solid_interface(self, up_current): 280 | # setup stress boundary on the solid domain 281 | sigma = self.fluid_solver.viscous_stress(up_current, self.fluid_T1) 282 | boundary_stress = self.map_fluid_to_solid_tensor(sigma) # reverse direction 283 | #boundary_stress = Constant(((0, 0), (100, 0))) # test passed 284 | for iface in self.interfaces: 285 | #bc_values = {'type': 'stress', 'value': boundary_stress} 286 | self.solid_solver.settings['boundary_conditions'][iface]['value'] = boundary_stress 287 | self.solid_solver.settings['boundary_conditions'][iface]['type'] = 'stress' 288 | print("updated interface:", self.solid_solver.settings['boundary_conditions'][iface]) 289 | 290 | def move_fluid_interface(self, mesh_disp): 291 | # assing no fluid mesh topo change 292 | self.previous_fluid_mesh = copy.copy(self.fluid_solver.mesh) 293 | self.mesh_offset.vector()[:] = mesh_disp.vector().get_local() - self.previous_fluid_mesh_disp.vector().get_local() 294 | ALE.move(self.fluid_solver.mesh, self.mesh_offset) 295 | self.previous_fluid_mesh_disp = mesh_disp 296 | 297 | #no need to fluid redefine function space, but mesh will not move in result file 298 | self.fluid_solver.update_solver_function_space(None) 299 | # 300 | #no need to move the solid mesh, for spatial interpolation if not using submesh mapping 301 | 302 | def generate_mesh_deformation_bc(self, V, bfunc): 303 | # does not mater for displacement velocity 304 | Dirichlet_bcs = [] 305 | zero_vector = Constant(self.fluid_solver.dimension*(0.0,)) 306 | for key, boundary in self.fluid_solver.boundary_conditions.items(): 307 | if 'coupling' in boundary and boundary['coupling'] == 'FSI': 308 | dbc = DirichletBC(V, bfunc, self.fluid_solver.boundary_facets, boundary['boundary_id']) 309 | else: 310 | dbc = DirichletBC(V, zero_vector, self.fluid_solver.boundary_facets, boundary['boundary_id']) 311 | Dirichlet_bcs.append(dbc) 312 | return Dirichlet_bcs 313 | 314 | def update_fluid_interface(self, uv_current): 315 | # can NOT be shared between tume step 316 | deforming_from_original_mesh = True 317 | if deforming_from_original_mesh: 318 | disp, vel = self.solid_solver.displacement(), self.solid_solver.velocity() 319 | # move to __init__ 320 | Vf = VectorFunctionSpace(self.original_fluid_mesh, self.fluid_solver.settings['fe_family'], self.fluid_solver.settings['fe_degree']) 321 | Vs = VectorFunctionSpace(self.original_solid_mesh, self.solid_solver.settings['fe_family'], self.solid_solver.settings['fe_degree']) 322 | 323 | #disp_bfunc = Function(Vf)#interpolate(disp, self.original_fb_vector_fs) # 324 | #vel_bfunc = Function(Vf) #interpolate(vel, self.original_fb_vector_fs) 325 | disp_bfunc = self.map_solid_to_fluid_vector(disp, Vf) 326 | vel_bfunc = self.map_solid_to_fluid_vector(vel, Vf) 327 | if _debug: 328 | plot(vel, title = 'velocity_solid') 329 | plot(disp_bfunc, title = 'disp_bfunc_mapped_from_solid') 330 | plot(vel_bfunc, title = 'vel_bfunc_mapped_from_solid') 331 | 332 | #mapping to original mesh, from index matching diff subdomains? 333 | #build a single FacetMesh function, and set all the value 334 | bcs_displacement = self.generate_mesh_deformation_bc(Vf, disp_bfunc) 335 | # at the interface, solid_disp_current ; otherwise, zero 336 | bcs_velocity = self.generate_mesh_deformation_bc(Vf, vel_bfunc) 337 | # at the interface, (solid_disp_current - solid_disp_prev) / dt, otherwise, zero 338 | mesh_disp, mesh_velocity = get_mesh_moving_displacement_and_velocity(Vf, self.original_fluid_mesh, bcs_displacement, bcs_velocity) 339 | 340 | else: # incremental deformation from previous mesh 341 | Vs = VectorFunctionSpace(self.current_solid_mesh, self.solid_solver.settings['fe_family'], self.solid_solver.settings['fe_degree']) 342 | nDisp, nVel = Function(Vs), Function(Vs) 343 | nDisp.vector()[:] = disp.vector().get_local() 344 | nDisp.vector()[:] = disp.vector().get_local() 345 | disp_bfunc = project(nDisp, Vf) 346 | vel_bfunc = project(nVel, Vf) 347 | 348 | # array assignment from fluid solver current mesh to original mesh, if no topo change 349 | 350 | # mapping to original mesh, from index matching diff subdomains? 351 | #build a single FacetMesh function, and set all the value 352 | bcs_displacement = self.generate_mesh_deformation_bc(Vf, disp_bfunc) 353 | # at the interface, solid_disp_current ; otherwise, zero 354 | bcs_velocity = self.generate_mesh_deformation_bc(Vf, vel_bfunc) 355 | # at the interface, (solid_disp_current - solid_disp_prev) / dt, otherwise, zero 356 | 357 | # deform from original mesh is only for small deformation 358 | # for large deformation, incremental mesh vel and disp 359 | mesh_disp, mesh_velocity = get_mesh_moving_displacement_and_velocity(V, self.original_fluid_mesh, bcs_displacement, bcs_velocity) 360 | 361 | # mesh after get mesh velocity? # fluid mesh velocity, cna be get from du/dt 362 | self.previous_fluid_mesh = copy.copy(self.current_fluid_mesh) 363 | 364 | # set solid boundary_conditions, Dirichlet boundary for interface 365 | if _debug: plot(mesh_velocity, title = 'fluid_mesh_velocity') 366 | #interactive() 367 | self.fluid_solver.settings['reference_frame_settings']['mesh_velocity'] = mesh_velocity 368 | for iface in self.interfaces: 369 | #boundary_velocity = Constant((0.0001,0)) # FIXME: tmp test, from cell to facet 370 | boundary_velocity = mesh_velocity 371 | bc_values = [{'variable': "velocity",'type': 'Dirichlet', 'value': boundary_velocity}] 372 | self.fluid_solver.settings['boundary_conditions'][iface]['value'] = bc_values 373 | 374 | print('max mesh disp', np.max(mesh_disp.vector().get_local())) 375 | return mesh_disp 376 | 377 | def move_solid_interface(self): 378 | disp = self.solid_solver.displacement() 379 | new_solid_mesh = copy.copy(self.original_solid_mesh) 380 | ALE.move(new_solid_mesh, project(disp, self.solid_V1)) # must match geometry degree 1 381 | self.current_solid_mesh = new_solid_mesh 382 | 383 | 384 | ################### 385 | def get_mesh_moving_displacement_and_velocity(V, mesh, bcs_displacement, bcs_velocity): 386 | # see: chapter 4, 'Coupled Fluid-Structure Simulation of Flapping Wings', 2012 387 | # bcs: Dirichlet conditions for displacements 388 | # bcsp: Dirichlet conditions for velocities 389 | # return: internal displacement and velocity 390 | #bfc = mesh.data().mesh_function('bfc') # why, error 391 | 392 | u = TrialFunction(V) 393 | v = TestFunction(V) 394 | f = Constant(mesh.geometry().dim()*(0.0, )) # source 395 | DG = FunctionSpace(mesh, "DG", 0) 396 | 397 | E = Function(DG) 398 | for c in cells(mesh): 399 | E.vector()[c.index()]=1./c.volume() # set elastic modulus 400 | 401 | nu = 0.0 # invisicid 402 | mu = E /(2.0*(1.0 + nu)) 403 | lmbda = E*nu /((1.0 + nu)*(1 - 2*nu)) 404 | 405 | #domains = CellFunction("size_t", mesh) 406 | dx = Measure('dx', domain = mesh) 407 | #dx: Multiple domains found, making the choice of integration domain ambiguous. 408 | def sigma(v): 409 | return 2.0*mu*sym(grad(v)) + lmbda * tr(sym(grad(v)))* Identity(mesh.geometry().dim()) 410 | a = inner(sigma(u), sym(grad(v)))*dx 411 | L = inner(f, v)*dx 412 | 413 | A = assemble(a) 414 | b = assemble(L) 415 | # set Dirichlet conditions for displacements 416 | [bc.apply(A, b) for bc in bcs_displacement] 417 | u = Function(V) 418 | # generalized minimal residual method(gmres) with ILU preconditioning(ilu) 419 | solve(A, u.vector(), b, 'gmres', 'ilu') 420 | 421 | # set Dirichlet conditions for velocities 422 | u_v = Function(V) 423 | [bc.apply(A, b) for bc in bcs_velocity] 424 | solve(A, u_v.vector(), b, 'gmres', 'ilu') 425 | 426 | return u, u_v -------------------------------------------------------------------------------- /FenicsSolver/LargeDeformationSolver.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2018 - Qingfeng Xia * * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | from __future__ import print_function, division 24 | import math 25 | import numpy as np 26 | 27 | from dolfin import * 28 | from .NonlinearElasticitySolver import NonlinearElasticitySolver 29 | from .SolverBase import SolverBase, SolverError 30 | 31 | class LargeDeformationSolver(NonlinearElasticitySolver): 32 | """ 33 | adapted from: http://www.karlin.mff.cuni.cz/~blechta/fenics-tutorial/elasticity/doc.html 34 | velocity is not vertex velocity! 35 | kinematic energy is not added 36 | """ 37 | def __init__(self, case_settings): 38 | NonlinearElasticitySolver.__init__(self, case_settings) 39 | 40 | case_settings['vector_name'] = 'displacement' 41 | # Use UFLACS to speed-up assembly and limit quadrature degree 42 | parameters['form_compiler']['representation'] = 'uflacs' 43 | parameters['form_compiler']['optimize'] = True 44 | parameters['form_compiler']['quadrature_degree'] = 4 45 | 46 | def generate_function_space(self, periodic_boundary): 47 | self.is_mixed_function_space = True 48 | print('self.is_mixed_function_space in the solver', self.is_mixed_function_space) 49 | V = VectorElement(self.settings['fe_family'], self.mesh.ufl_cell(), self.settings['fe_degree']) 50 | Q = FiniteElement(self.settings['fe_family'], self.mesh.ufl_cell(), self.settings['fe_degree']) 51 | mixed_element = MixedElement([V, V, Q]) # displacement, velocity, pressure 52 | 53 | if periodic_boundary: 54 | self.function_space = FunctionSpace(self.mesh, mixed_element, constrained_domain=periodic_boundary) 55 | else: 56 | self.function_space = FunctionSpace(self.mesh, mixed_element) 57 | 58 | ''' 59 | def get_initial_field(self): 60 | # assume: all constant, velocity is a tupe of constant 61 | #_initial_values = [] 62 | #_initial_values.append(self.dimension*(0.0,)) 63 | #_initial_values.append(self.dimension*(0.0,)) # self.initial_values['velocity'] 64 | # _initial_values.append(0.0) 65 | _initial_values = (self.dimension*2+1)*(0.0,) 66 | _expr = tuple([str(v) for v in _initial_values]) 67 | print(_expr) 68 | _initial_values_expr = Expression( _expr, degree = self.settings['fe_degree']) 69 | up0 = interpolate(_initial_values_expr, self.function_space) 70 | return up0 71 | ''' 72 | 73 | def get_flux(self, u, mag_vector): 74 | F = Identity(self.dimension) + grad(u) 75 | print("mag_vector", mag_vector) 76 | return det(F)*dot(inv(F).T, mag_vector) 77 | 78 | def generate_form(self, time_iter_, w_trial, w_test, w_current, w_prev): 79 | 80 | dx= Measure("dx", subdomain_data=self.subdomains) # in case material property depends on subdomains 81 | #material property 82 | E = self.material['elastic_modulus'] 83 | nu = self.material['poisson_ratio'] 84 | mu = Constant(E/(2.0*(1.0 + nu))) # shear modulus 85 | 86 | (u, v, p) = split(w_current) # displacement, velocity, pressure 87 | (u0, v0, p0) = split(w_prev) 88 | (_u, _v, _p) = split(w_test) 89 | 90 | I = Identity(self.dimension) 91 | 92 | # Define constitutive law for large deformation, coordinates in deformed configuration 93 | def stress(u, p): 94 | """Define constitutive law for large deformation, coordinates in deformed configuration 95 | Returns 1st Piola-Kirchhoff stress and (local) mass balance for given u, p. 96 | see: """ 97 | 98 | F = I + grad(u) # deformation gradient 99 | J = det(F) 100 | B = F * F.T # left cauchy green deformation tensor, unsymmetric tensor, while cauchy stress is symmetric 101 | T = -p*I + mu*(B-I) # Cauchy stress 102 | S = J*T*inv(F).T # 1st Piola-Kirchhoff stress 103 | if nu == 0.5: 104 | # Incompressible material, singularity problem 105 | pp = J-1.0 # volumetric change 106 | else: 107 | # Compressible 108 | lmbd = Constant(E*nu/((1.0 + nu)*(1.0 - 2.0*nu))) # volumetric modulus 109 | pp = 1.0/lmbd*p + (J*J-1.0) # 110 | return S, pp 111 | 112 | if self.transient_settings['transient']: 113 | dt = self.get_time_step(time_iter_) 114 | q = 0.5 # time fwd scheme 0.5: crank-niklas 115 | else: 116 | raise SolverError("large deformation solver must be solved in a transient way") 117 | 118 | # Balance of momentum, using velocity test function here 119 | S, pp = stress(u, p) 120 | S0, pp0 = stress(u0, p0) 121 | F1 = (1.0/dt)*inner(u-u0, _u)*dx \ 122 | - ( q*inner(v, _u)*dx + (1.0-q)*inner(v0, _u)*dx ) # q is the time stepping constant 123 | F2a = inner(S, grad(_v))*dx + pp*_p*dx 124 | F2b = inner(S0, grad(_v))*dx + pp0*_p*dx 125 | F2 = (1.0/dt)*inner(v-v0, _v)*dx + q*F2a + (1.0-q)*F2b 126 | 127 | F = F1 + F2 128 | ds= Measure("ds", subdomain_data=self.boundary_facets) # if later marking updating in this ds? 129 | # note: why u and _v (test function for velocity) are passed to the function below? 130 | bcs, integrals_F = self.update_boundary_conditions(time_iter_, u, _v, ds) 131 | if time_iter_==0: 132 | plot(self.boundary_facets, title = "boundary facets colored by ID") 133 | #interactive() 134 | 135 | if self.body_source: 136 | integrals_F.append(inner(self.body_source, _v)*dx ) # 137 | 138 | F += sum(integrals_F) # FIXME: in original tutorial, boundary flux is added to F 139 | #to do: direction_vector if it is not a tuple, constant of vector 140 | #nitsche for directly 141 | # def get_source_item(): generalised, done 142 | # Traction at boundary, stress or force? 143 | 144 | # Whole system and its Jacobian 145 | 146 | self.J = derivative(F, w_current) 147 | return F, bcs 148 | 149 | def solve_form(self, F, u_, bcs): 150 | solve(F == 0, u_, bcs, J=self.J, 151 | solver_parameters={"newton_solver":{"linear_solver":"mumps","absolute_tolerance":1e-9,"relative_tolerance":1e-7}}) 152 | return u_ 153 | 154 | def displacement(self): 155 | if self.is_mixed_function_space: 156 | u_, v_, p_ = split(self.w_current) 157 | return u_ # large deformation function space: disp, vel, pressure 158 | 159 | def velocity(self): 160 | dt = self.get_time_step(self.current_step) 161 | if self.is_mixed_function_space: 162 | u_, v_, p_ = split(self.w_current) 163 | #return v_ # large deformation function space: disp, vel, pressure, velocity not correct 164 | u0_, v0_, p0_ = split(self.w_prev) 165 | return (u_ - u0_)/Constant(dt) 166 | 167 | def plot_result(self): 168 | # Extract solution components and rename 169 | (u, v, p) = split(self.result) 170 | #v.rename("v", "velocity") 171 | #p.rename("p", "pressure") 172 | plot(u, mode="displacement", wireframe=True) 173 | 174 | def save(self, result_filename): 175 | print("Error: save function has error, using SolverBase save()") 176 | print("Suppress this error by provide this dummy save() in the subclass") 177 | """ 178 | suffix = '.pvd' 179 | assert result_filename[-4:] == '.pvd' 180 | result_filename_root = result_filename[:-4] 181 | ret = split(self.result) 182 | for i, var in enumerate(ret): 183 | var_name = self.settings['mixed_variable'][i] 184 | # AttributeError: 'ListTensor' object has no attribute 'rename', for LargeDeformationSolver 185 | var.rename(var_name, "label") # why renaming does not show in vtu file? 186 | var_result_filename = result_filename_root + '_' + var_name + suffix 187 | result_stream = File(var_result_filename) 188 | result_stream << (var, self.current_time) 189 | """ 190 | pass 191 | #################################### 192 | 193 | -------------------------------------------------------------------------------- /FenicsSolver/LinearElasticitySolver.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2017 - Qingfeng Xia * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | """ 24 | Features: 25 | - transient: support very slow boundary change, Elastostatics 26 | - thermal stress are implemented but with very basic example 27 | - modal analysis, not tested yet 28 | - boundary conditions: see member funtion `update_boundary_conditions()` 29 | - support 2D and 3D with nullspace accel 30 | 31 | Todo: 32 | - Elastodynamics - or dynamics (acceleration * density can not be ignored) vibration, damping, 33 | - point source, as nodal constraint, is not supported yet 34 | - contact/frictional boundary condition, not yet implemented 35 | - nonhomogenous meterial property like elastic modulus, not yet tested 36 | - anisotropy needs rank 4 tensor, not yet tested 37 | 38 | plasticity will be implemented in PlasticitySolver, 39 | and other nonliearity by NonlinearElasticitySolver 40 | """ 41 | 42 | from __future__ import print_function, division 43 | import math 44 | import collections 45 | import numbers 46 | import numpy as np 47 | 48 | 49 | ##################################### 50 | from dolfin import * 51 | 52 | from .SolverBase import SolverBase, SolverError 53 | class LinearElasticitySolver(SolverBase): 54 | # static and dynamic 55 | def __init__(self, case_settings): 56 | case_settings['vector_name'] = 'displacement' 57 | SolverBase.__init__(self, case_settings) 58 | # solver specific setting 59 | self.solving_modal = False 60 | self.solving_dynamics = False # not quasi-static, structure's acceleration make impact 61 | 62 | def sigma(self, u): 63 | # Stress computation for linear elasticity 64 | elasticity = self.material['elastic_modulus'] 65 | nu = self.material['poisson_ratio'] 66 | mu = elasticity/(2.0*(1.0 + nu)) 67 | lmbda = elasticity*nu/((1.0 + nu)*(1.0 - 2.0*nu)) 68 | #return 2.0*mu*sym(grad(u)) + lmbda*tr(sym(grad(u)))*Identity(len(u)) # div(u) == tr(sym(grad(u)))? 69 | return 2.0*mu*sym(grad(u)) + lmbda*div(u)*Identity(len(u)) 70 | 71 | def von_Mises(self, u): 72 | s = self.sigma(u) - (1./3)*tr(self.sigma(u))*Identity(self.dimension) # deviatoric stress 73 | von_Mises = sqrt(3./2*inner(s, s)) 74 | 75 | V = FunctionSpace(self.mesh, 'P', 1) # correct, but why using another function space 76 | return project(von_Mises, V) 77 | 78 | def thermal_stress(self, T): 79 | elasticity = self.material['elastic_modulus'] 80 | nu = self.material['poisson_ratio'] 81 | tec = self.material['thermal_expansion_coefficient'] 82 | thermal_strain = tec * ( T - Constant(self.reference_values['temperature'])) 83 | #(lmbda*tr(eps(v)) - kappa*dT)*Identity(2) + 2*mu*eps(v) # coupled 84 | #alpha*(3*lmbda+2*mu)*dT)*Identity(2) # weak coupled 85 | return elasticity/(1.0 - 2.0*nu) * thermal_strain * Identity(self.dimension) 86 | 87 | def strain_energy(self, u): 88 | # Strain energy or the plastic heat generation 89 | elasticity = self.material['elastic_modulus'] 90 | nu = self.material['poisson_ratio'] 91 | mu = elasticity/(2.0*(1.0 + nu)) 92 | lmbda = elasticity*nu/((1.0 + nu)*(1.0 - 2.0*nu)) 93 | return lmbda/2.0*(tr(eps(v)))^2 + mu*tr(eps(v)**2) 94 | 95 | def get_flux(self, u, mag_vector): 96 | # pass-through , to be overloaded in large deformation solver 97 | return mag_vector 98 | 99 | def update_boundary_conditions(self, time_iter_, u, v, ds): 100 | V = self.function_space 101 | bcs = [] 102 | integrals_N = [] 103 | mesh_normal = FacetNormal(self.mesh) # n is predefined as outward as positive 104 | 105 | if 'point_source' in self.settings and self.settings['point_source']: 106 | ps = self.settings['surface_source'] 107 | #assume it s PointSource type, or a list of PointSource 108 | bcs.append(ps) 109 | 110 | if 'surface_source' in self.settings and self.settings['surface_source']: 111 | gS = self.get_flux(u, self.settings['surface_source']['value']) 112 | if 'direction' in self.settings['surface_source'] and self.settings['surface_source']['direction']: 113 | direction_vector = self.settings['surface_source']['direction'] 114 | else: 115 | integrals_N.append(dot(mesh_normal*gS, v)*ds) 116 | 117 | for name, bc_settings in self.boundary_conditions.items(): 118 | i = bc_settings['boundary_id'] 119 | bc = self.get_boundary_variable(bc_settings) 120 | 121 | print(bc) 122 | if bc['type'] =='Dirichlet' or bc['type'] =='displacement': 123 | if not self.is_mixed_function_space: 124 | bv = bc['value'] # translate_value() is not supported for value types: [1e-3, None, None] 125 | if isinstance(bv, (tuple, list)) and len(bv) == self.dimension: 126 | axis_i=0 127 | for disp in bv: 128 | if not disp is None: # None means free of constraint, but zero is kind of constraint 129 | dbc = DirichletBC(V.sub(axis_i), self.translate_value(disp), self.boundary_facets, i) 130 | bcs.append(dbc) 131 | axis_i += 1 132 | else: 133 | dbc = DirichletBC(V, self.translate_value(bv), self.boundary_facets, i) 134 | bcs.append(dbc) 135 | else: # mixed_function_space for LargeDeformationSolver 136 | disp_i, vel_i, pressure_i = 0, 1, 2 137 | if 'value' in bc: # only displacement is provided, set the velocity as zero? 138 | bv = bc['value'] 139 | if bc['variable'] == 'displacement': 140 | if isinstance(bv, (tuple, list)) and len(bv) == self.dimension and not all(bv): 141 | axis_i=0 142 | for disp in bv: 143 | if not disp is None: # None means free of constraint, but zero is kind of constraint 144 | dbc = DirichletBC(V.sub(disp_i).sub(axis_i), self.translate_value(disp), self.boundary_facets, i) 145 | bcs.append(dbc) 146 | axis_i += 1 147 | else: 148 | var_i = disp_i 149 | dbc = DirichletBC(V.sub(var_i), self.translate_value(bv), self.boundary_facets, i) 150 | bcs.append(dbc) 151 | elif bc['variable'] == 'velocity': 152 | var_i = vel_i 153 | dbc = DirichletBC(V.sub(var_i), self.translate_value(bv), self.boundary_facets, i) 154 | bcs.append(dbc) 155 | elif bc['variable'] == 'all': 156 | dbc = DirichletBC(V, self.translate_value(bv), self.boundary_facets, i) 157 | bcs.append(dbc) 158 | else: 159 | print(bc) 160 | raise SolverError('Error, boundary setting is not supported: ') 161 | 162 | else: # bc['values'] =[ {'variable': displacement' , 'value': dvalue}, { 'variable':'velocity', 'value': vvalue} 163 | pass # not yet needed 164 | 165 | elif bc['type'] == 'force': 166 | if isinstance(bc['value'], (tuple, list)) and len(bc['value']) == self.dimension: 167 | force_vector = Constant((bc['value'])) 168 | else: 169 | bc_force = self.translate_value(bc['value']) 170 | # calc the surface area and calc stress, normal and tangential? 171 | bc_area = assemble(Constant(1)*ds(bc['boundary_id'], domain=self.mesh)) 172 | print('boundary area (m2) for force boundary is', bc_area) 173 | g = bc_force / bc_area 174 | # FIXME: assuming all force are normal to mesh boundary 175 | if 'direction' in bc and bc['direction']: 176 | direction_vector = bc['direction'] 177 | else: 178 | direction_vector = mesh_normal 179 | force_vector = direction_vector*g 180 | integrals_N.append(dot(self.get_flux(u, force_vector), v)*ds(i)) 181 | elif bc['type'] == 'pressure': 182 | # scalar, normal to boundary surface, or by a given direction vector 183 | if 'direction' in bc and bc['direction']: 184 | direction_vector = bc['direction'] 185 | else: 186 | direction_vector = mesh_normal 187 | g = direction_vector * self.translate_value(bc['value']) 188 | #FIXME: assuming all force are normal to mesh boundary 189 | integrals_N.append(dot(self.get_flux(u, g),v)*ds(i)) 190 | elif bc['type'] == 'stress': # must be normal stress vector, or stress tensor 191 | g = self.translate_value(bc['value']) 192 | if isinstance(g, (Constant, )): 193 | pass 194 | else: 195 | g = dot(g, mesh_normal) # FIXME: FSI reverse direction before get here 196 | integrals_N.append(dot(self.get_flux(u, g),v)*ds(i)) 197 | elif bc['type'] == 'Neumann': # Neumann is the strain: du/dx then how to make a surface stress? 198 | raise SolverError('Neumann boundary type`{}` is not supported'.format(bc['type'])) 199 | elif bc['type'] == 'symmetry': 200 | raise SolverError('symmetry boundary type`{}` is not supported'.format(bc['type'])) 201 | else: 202 | raise SolverError('boundary type`{}` is not supported'.format(bc['type'])) 203 | ## nodal constraint is not yet supported, try make it a small surface load instead 204 | return bcs, integrals_N 205 | 206 | def generate_form(self, time_iter_, u, v, u_current, u_prev): 207 | # todo: transient 208 | V = self.function_space 209 | 210 | elasticity = self.material['elastic_modulus'] 211 | nu = self.material['poisson_ratio'] 212 | mu = elasticity/(2.0*(1.0 + nu)) 213 | lmbda = elasticity*nu/((1.0 + nu)*(1.0 - 2.0*nu)) 214 | 215 | F = inner(self.sigma(u), grad(v))*dx 216 | if self.transient_settings['transient'] and self.solving_dynamics: 217 | if time_iter_>=1: 218 | accel = self.get_acceleration(time_iter_) 219 | #mesh_velocity in FSI 220 | F -= self.material['density'] * inner(accel, v) * dx 221 | 222 | ds= Measure("ds", subdomain_data=self.boundary_facets) # if later marking updating in this ds? 223 | bcs, integrals_F = self.update_boundary_conditions(time_iter_, u, v, ds) 224 | if time_iter_==0: 225 | plot(self.boundary_facets, title = "boundary facets colored by ID") 226 | 227 | if self.body_source: 228 | integrals_F.append( inner(self.body_source, v)*dx ) 229 | 230 | # thermal stress 231 | if not hasattr(self, 'temperature_distribution'): 232 | if 'temperature_distribution' in self.settings and self.settings['temperature_distribution']: 233 | self.temperature_distribution = self.translate_value(self.settings['temperature_distribution']) 234 | if hasattr(self, 'temperature_distribution') and self.temperature_distribution: 235 | T = self.translate_value(self.temperature_distribution) # interpolate 236 | stress_t = self.thermal_stress(self.settings['temperature_distribution']) 237 | if stress_t: 238 | F -= inner(stress_t, grad(v)) * dx 239 | # sym(grad(v)) == epislon(v), it does not matter for multiply identity matrix 240 | 241 | # Assemble system, applying boundary conditions and extra items 242 | if len(integrals_F): 243 | for item in integrals_F: F += item # L side 244 | 245 | return F, bcs 246 | 247 | def solve_form(self, F, u_, bcs): 248 | if self.dimension == 3: 249 | u_ = self.solve_amg(F, u_, bcs) 250 | else: 251 | u_ = self.solve_linear_problem(F, u_, bcs) 252 | # calc boundingbox to make sure no large deformation? 253 | return u_ 254 | 255 | def displacement(self): 256 | if self.is_mixed_function_space: 257 | raise SolverError('subclass with mixed_function_space must override this function') 258 | else: 259 | return self.w_current 260 | 261 | def velocity(self): 262 | dt = self.get_time_step(self.current_step) 263 | if self.is_mixed_function_space: 264 | raise SolverError('subclass with mixed_function_space must override this function') 265 | else: 266 | u_ = self.w_current 267 | u0_ = self.w_prev 268 | return (u_ - u0_)/Constant(dt) 269 | 270 | def solve_modal(self): 271 | # FIXME: not yet complete code, mass matrix is not added into form, also only for linear form 272 | trial_function = TrialFunction(self.function_space) 273 | test_function = TestFunction(self.function_space) 274 | # Define functions for transient loop 275 | u_current = self.get_initial_field() # init to default or user provided constant 276 | u_prev = Function(self.function_space) 277 | u_prev.assign(u_current) 278 | 279 | current_step = 0 280 | F, bcs = self.generate_form(current_step, trial_function, test_function, u_current, u_prev) 281 | return self.solve_modal_form(F, bcs) 282 | 283 | def solve_modal_form(self, F, bcs): 284 | # Test for PETSc 285 | if not has_linear_algebra_backend("PETSc"): 286 | print("DOLFIN has not been configured with PETSc. Exiting.") 287 | exit() 288 | # Set backend to PETSC 289 | parameters["linear_algebra_backend"] = "PETSc" 290 | 291 | # todo: Assemble stiffness form, it is not fully tested yet 292 | A = PETScMatrix() 293 | b = PETScVector() 294 | assemble_system(lhs(F), rhs(F), bcs, A_tensor=A, b_tensor=b) # preserve symmetry 295 | 296 | # Create eigensolver 297 | eigensolver = SLEPcEigenSolver(A) 298 | 299 | # Compute all eigenvalues of A x = \lambda x 300 | print("Computing eigenvalues. This can take a minute.") 301 | eigensolver.solve() 302 | 303 | # Extract largest (first) eigenpair 304 | r, c, rx, cx = eigensolver.get_eigenpair(0) 305 | 306 | print("Largest eigenvalue: ", r) 307 | 308 | # Initialize function and assign eigenvector 309 | ev = Function(self.function_space) 310 | ev.vector()[:] = rx 311 | 312 | return ev 313 | -------------------------------------------------------------------------------- /FenicsSolver/NonlinearElasticitySolver.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2017 - Qingfeng Xia * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | from __future__ import print_function, division 24 | import math 25 | import collections 26 | import numbers 27 | import numpy as np 28 | 29 | 30 | ##################################### 31 | from dolfin import * 32 | 33 | from .SolverBase import SolverBase, SolverError 34 | from .LinearElasticitySolver import LinearElasticitySolver 35 | 36 | class NonlinearElasticitySolver(LinearElasticitySolver): 37 | """ share code as much as possible withe the linear version, especially boundary settings 38 | supported: nonlinear material property, hyperelastic material 39 | hyperelasticity: adapted from official tutorial: 40 | https://github.com/FEniCS/dolfin/blob/master/demo/documented/hyperelasticity/python/demo_hyperelasticity.py.rst 41 | """ 42 | def __init__(self, s): 43 | LinearElasticitySolver.__init__(self, s) 44 | #nonlinear special setting 45 | self.settings['mixed_variable'] = ('displacement', 'velocity', 'pressure') 46 | 47 | def generate_form(self, time_iter_, u, v, u_current, u_prev): 48 | # todo: transient 49 | # Optimization options for the form compiler 50 | parameters["form_compiler"]["cpp_optimize"] = True 51 | parameters["form_compiler"]["representation"] = "uflacs" 52 | V = self.function_space 53 | 54 | elasticity = self.material['elastic_modulus'] 55 | nu = self.material['poisson_ratio'] 56 | mu = elasticity/(2.0*(1.0 + nu)) 57 | lmbda = elasticity*nu/((1.0 + nu)*(1.0 - 2.0*nu)) 58 | 59 | I = Identity(self.dimension) # Identity tensor 60 | F = I + grad(u_current) # Deformation gradient 61 | C = F.T*F # Right Cauchy-Green tensor 62 | # Invariants of deformation tensors 63 | Ic = tr(C) 64 | J = det(F) 65 | 66 | # Stored strain energy density (compressible neo-Hookean model) 67 | psi = (mu/2)*(Ic - 3) - mu*ln(J) + (lmbda/2)*(ln(J))**2 68 | 69 | # Total potential energy 70 | Pi = psi*dx 71 | #- dot(T, u_current)*ds # Traction force on the boundary 72 | # Dirichlet boundary, just as LinearElasticitySolver 73 | 74 | # how about kinematic energy, only for dynamic process vibration 75 | if self.transient_settings['transient']: 76 | vel = (u_current - u_prev) /dt 77 | Pi += 0.5*vel*vel*dx # not yet tested code! 78 | 79 | if self.body_source: 80 | Pi -= dot(self.body_source, u_current)*dx 81 | 82 | ds= Measure("ds", subdomain_data=self.boundary_facets) # if later marking updating in this ds? 83 | # hack solution: u_current as testfunction v 84 | bcs, integrals_F = self.update_boundary_conditions(time_iter_, u, u_current, ds) 85 | if time_iter_==0: 86 | plot(self.boundary_facets, title = "boundary facets colored by ID") 87 | # Assemble system, applying boundary conditions and extra items 88 | if len(integrals_F): 89 | for item in integrals_F: Pi -= item 90 | 91 | # Compute first variation of Pi (directional derivative about u in the direction of v) 92 | F = derivative(Pi, u_current, v) 93 | self.J = derivative(F, u_current, u) 94 | return F, bcs 95 | 96 | def solve_form(self, F, u_, bcs): 97 | solve(F == 0, u_, bcs, J=self.J) 98 | return u_ 99 | 100 | -------------------------------------------------------------------------------- /FenicsSolver/ScalarTransportDGSolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2017 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | 25 | # this solver has not passed testing, all result is NAN 26 | 27 | from __future__ import print_function, division 28 | import math 29 | import numpy as np 30 | 31 | from dolfin import * 32 | 33 | supported_scalars = {'temperature', 'electric_potential', 'species_concentration'} 34 | 35 | 36 | from .ScalarTransportSolver import ScalarTransportSolver 37 | from .SolverBase import SolverBase, SolverError 38 | class ScalarTransportDGSolver(ScalarTransportSolver): 39 | """ share code as much as possible withe the CG version, 40 | adapted from official tutorial: 2D, no source, no Neumann boundary 41 | https://github.com/FEniCS/dolfin/tree/master/demo/undocumented/dg-advection-diffusion 42 | """ 43 | def __init__(self, s): 44 | ScalarTransportSolver.__init__(self, s) 45 | self.using_diffusion_form = True 46 | 47 | def generate_function_space(self, periodic_boundary): 48 | self.is_mixed_function_space = False 49 | if periodic_boundary: 50 | self.function_space_CG = FunctionSpace(self.mesh, "CG", self.settings['fe_degree'], constrained_domain=periodic_boundary) 51 | self.function_space = FunctionSpace(self.mesh, "DG", self.settings['fe_degree'], constrained_domain=periodic_boundary) 52 | self.vector_function_space = VectorFunctionSpace(self.mesh, 'CG', self.settings['fe_degree']+1, constrained_domain=periodic_boundary) 53 | # the group and degree of the FE element. 54 | else: 55 | self.function_space_CG = FunctionSpace(self.mesh, "CG", self.settings['fe_degree']) 56 | self.function_space = FunctionSpace(self.mesh, "DG", self.settings['fe_degree']) 57 | self.vector_function_space = VectorFunctionSpace(self.mesh, 'CG', self.settings['fe_degree']+1) 58 | 59 | def get_convective_velocity_function(self, convective_velocity): 60 | # fixme: rename ! 61 | #vel = self.translate_value(convective_velocity, self.vector_function_space) 62 | vel = convective_velocity 63 | #vel = Constant((1.0, 1.0)) 64 | #print("vel.ufl_shape", vel.ufl_shape) 65 | return vel 66 | 67 | def generate_form(self, time_iter_, T, Tq, T_current, T_prev): 68 | parameters["ghost_mode"] = "shared_facet" 69 | # T, Tq can be shared between time steps, form is unified diffussion coefficient 70 | n = FacetNormal(self.mesh) 71 | h = CellSize(self.mesh) # cell size 72 | h_avg = (h('+') + h('-'))/2 73 | # Penalty term 74 | 75 | dx= Measure("dx", subdomain_data=self.subdomains) # subdomain (MeshFunction) does not distinguish DG CG? 76 | ds= Measure("ds", subdomain_data=self.boundary_facets) 77 | 78 | conductivity = self.conductivity() # constant, experssion or tensor 79 | capacity = self.capacity() # density * specific capacity -> volumetrical capacity 80 | diffusivity = self.diffusivity() # diffusivity 81 | #print(conductivity, capacity, diffusivity) 82 | 83 | bcs, integrals_N = self.update_boundary_conditions(time_iter_, T, Tq, ds) 84 | 85 | def F_convective(): 86 | velocity = self.get_convective_velocity_function(self.convective_velocity) 87 | vel_n = (dot(velocity, n) + abs(dot(velocity, n)))/2.0 88 | 89 | if False: 90 | #http://www.karlin.mff.cuni.cz/~hron/fenics-tutorial/discontinuous_galerkin/doc.html 91 | # the only difference between official DG advection tutorial is `alpha/avg(h)` 92 | Pe= 1.0/diffusivity 93 | alpha = 1 94 | theta = 0.5 95 | 96 | def a(u,v) : 97 | # Bilinear form 98 | a_int = dot(grad(v), (1.0/Pe)*grad(u) - velocity*u)*dx 99 | 100 | a_fac = (1.0/Pe)*(alpha/avg(h))*dot(jump(u, n), jump(v, n))*dS \ 101 | - (1.0/Pe)*dot(avg(grad(u)), jump(v, n))*dS \ 102 | - (1.0/Pe)*dot(jump(u, n), avg(grad(v)))*dS 103 | 104 | a_vel = dot(jump(v), vel_n('+')*u('+') - vel_n('-')*u('-') )*dS + dot(v, vel_n*u)*ds 105 | 106 | a = a_int + a_fac + a_vel 107 | return a 108 | 109 | if self.transient_settings['transient']: 110 | # Define variational forms 111 | a0=a(T_prev,Tq) 112 | a1=a(T,Tq) 113 | 114 | F = (1/dt)*inner(T, Tq)*dx - (1/dt)*inner(T_prev,Tq)*dx + theta*a1 + (1-theta)*a0 115 | else: 116 | F = theta*a(T,Tq) + (1-theta)*a(T_prev,Tq) 117 | F = F * Constant(capacity) 118 | 119 | else: 120 | if self.dimension == 2: 121 | alpha = Constant(5.0) # default 5 for 2D, but it needs to be higher for 3D case 122 | else: 123 | alpha = Constant(500) 124 | v = Tq 125 | phi = T 126 | kappa = diffusivity 127 | # ( dot(v, n) + |dot(v, n)| )/2.0 128 | 129 | # Bilinear form 130 | a_int = dot(grad(v), kappa*grad(phi) - velocity*phi)*dx 131 | 132 | a_fac = kappa*(alpha/h('+'))*dot(jump(v, n), jump(phi, n))*dS \ 133 | - kappa*dot(avg(grad(v)), jump(phi, n))*dS \ 134 | - kappa*dot(jump(v, n), avg(grad(phi)))*dS 135 | 136 | #`Numerical simulations of advection-dominated scalar mixing with applications to spinal CSF flow and drug transport` 137 | a_vel = dot(jump(v), vel_n('+')*phi('+') - vel_n('-')*phi('-') )*dS + dot(v, vel_n*phi)*ds 138 | 139 | F = (a_int + a_fac + a_vel) * Constant(capacity) 140 | 141 | if integrals_N: 142 | print("integrals_N", integrals_N) 143 | F -= sum(integrals_N) # FIXME: DG may need distinct newmann boundary flux 144 | # Linear form 145 | if self.body_source: 146 | F -= Tq * self.body_source * dx 147 | return F 148 | 149 | if not hasattr(self, 'convective_velocity'): # if velocity is not directly assigned to the solver 150 | if 'convective_velocity' in self.settings and self.settings['convective_velocity']: 151 | self.convective_velocity = self.settings['convective_velocity'] 152 | else: 153 | self.convective_velocity = None 154 | 155 | if self.convective_velocity: # convective heat conduction 156 | F = F_convective() 157 | print('Discrete Galerkin method solves only advection-diffusion equation') 158 | else: 159 | F = None 160 | raise SolverError('Error: Discrete Galerkin method should be used with advection velocity') 161 | 162 | return F, bcs 163 | 164 | def solve_form(self, F, T_current, bcs): 165 | if False: 166 | self.solve_linear_problem(F, T_current, bcs) 167 | else: 168 | problem = LinearVariationalProblem(lhs(F), rhs(F), T_current, bcs) 169 | solver = LinearVariationalSolver(problem) 170 | solver.solve() 171 | """ 172 | a, L = lhs(F), rhs(F) 173 | print(a, L) 174 | 175 | A = PETScMatrix() 176 | assemble(a, tensor=A) 177 | b = assemble(L) 178 | 179 | #for bc in bcs: bc.apply(A, b) 180 | # Set up boundary condition (apply strong BCs) 181 | class DirichletBoundary(SubDomain): 182 | def inside(self, x, on_boundary): 183 | return on_boundary 184 | g = Expression("300", degree=1) 185 | bc = DirichletBC(self.function_space, g, DirichletBoundary(), "geometric") 186 | bc.apply(A, b) 187 | #bc.apply(A) #error here: 188 | 189 | # Solve system 190 | solve(A, T_current.vector(), b) 191 | """ 192 | return T_current 193 | 194 | def solve(self): 195 | _result = self.solve_transient() 196 | # Project solution to a continuous function space 197 | self.result = project(_result, V=self.function_space_CG) 198 | return self.result -------------------------------------------------------------------------------- /FenicsSolver/ScalarTransportSolver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2017 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | 25 | from __future__ import print_function, division 26 | import math 27 | import numpy as np 28 | 29 | from dolfin import * 30 | 31 | supported_scalars = {'temperature', 'electric_potential', 'species_concentration'} 32 | electric_permittivity_in_vacumm = 8.854187817e-12 33 | # For small species factor, diffusivity = primary species, such as dye in water, always with convective velocity 34 | # electric_potential, only for dieletric material electrostatics (permittivity/conductivity << 1) 35 | # magnetic_potential is a vector, magnetostatics (static current) is solved in MaxwellEMSolver (permittivity/conductivity >> 1) 36 | # porous pressure, e.g. underground water pressure 37 | 38 | # thermal diffusivity = (thermal conductivity) / (density * specific heat) 39 | # thermal volumetric capacity = density * specific heat 40 | 41 | from .SolverBase import SolverBase, SolverError 42 | class ScalarTransportSolver(SolverBase): 43 | """ general scalar transportation (diffusion and advection) solver, exampled by Heat Transfer 44 | # 4 types of boundaries supported: math, physical 45 | # body source unit: W/m^3, apply to whole body/domain 46 | # surface source unit: W/m^2, apply to whole boundary, 47 | # convective velocity: m/s, stablization is controlled by advection_settings 48 | # Thermal Conductivity: w/(K m) 49 | # Specific Heat Capacity, Cp: J/(kg K) 50 | # thermal specific: 51 | # shear_heating: common in lubrication scinario, high viscosity and high shear speed, one kind of volume/body source 52 | # radiation: radiation_settings {} 53 | """ 54 | def __init__(self, s): 55 | SolverBase.__init__(self, s) 56 | 57 | if 'scalar_name' in self.settings: 58 | self.scalar_name = self.settings['scalar_name'].lower() 59 | else: 60 | self.scalar_name = "temperature" 61 | self.using_diffusion_form = False # diffusion form is simple in math, but not easy to deal with nonlinear material property 62 | 63 | self.nonlinear = False 64 | self.nonlinear_material = False 65 | for v in self.material.values(): 66 | if callable(v): # fixedme: if other material properties are functions, it will be regarded as nonlinear 67 | self.nonlinear = True 68 | 69 | if self.scalar_name == "eletric_potential": 70 | assert self.settings['transient_settings']['transient'] == False 71 | #delay the convective velocity and radiation setting detection in geneate_form() 72 | 73 | def capacity(self, T=None): 74 | # to calc diffusion coeff : conductivity/capacity, it must be number only for 75 | if 'capacity' in self.material: 76 | c = self.material['capacity'] 77 | # if not found, calc it, otherwise, it is 78 | elif self.scalar_name == "temperature": 79 | cp = self.material['specific_heat_capacity'] 80 | c = self.material['density'] * cp 81 | elif self.scalar_name == "electric_potential": 82 | c = electric_permittivity_in_vacumm 83 | elif self.scalar_name == "spicies_concentration": 84 | c = 1 85 | else: 86 | raise SolverError('material capacity property is not found for {}'.format(self.scalar_name)) 87 | #print(type(c)) 88 | from inspect import isfunction 89 | if isfunction(c): # accept only function or lambda, ulf.algebra.Product is also callable 90 | self.nonlinear_material = True 91 | return c(T) 92 | return self.get_material_value(c) # todo: deal with nonlinear material 93 | 94 | def diffusivity(self, T=None): 95 | if 'diffusivity' in self.material: 96 | c = self.material['diffusivity'] 97 | elif self.scalar_name == "temperature": 98 | c = self.material['thermal_conductivity'] / self.capacity() 99 | elif self.scalar_name == "electric_potential": 100 | c = self.material['relative_electric_permittivity'] 101 | elif self.scalar_name == "spicies_concentration": 102 | c = self.material['diffusivity'] 103 | else: 104 | raise SolverError('conductivity material property is not found for {}'.format(self.scalar_name)) 105 | 106 | from inspect import isfunction # dolfin.Funciton is also callable 107 | if isfunction(c): 108 | self.nonlinear_material = True 109 | return c(T) 110 | return self.get_material_value(c) # todo: deal with nonlinear material 111 | 112 | def conductivity(self, T=None): 113 | # nonlinear material: c = function(T) 114 | if 'conductivity' in self.material: 115 | c = self.material['conductivity'] 116 | elif self.scalar_name == "temperature": 117 | c = self.material['thermal_conductivity'] 118 | elif self.scalar_name == "electric_potential": 119 | c = self.material['relative_electric_permittivity'] * electric_permittivity_in_vacumm 120 | elif self.scalar_name == "spicies_concentration": 121 | c = self.material['diffusivity'] 122 | else: 123 | c = self.diffusivity() * self.capacity() 124 | #print('conductivity', c) 125 | from inspect import isfunction 126 | if isfunction(c): 127 | self.nonlinear_material = True 128 | return c(T) 129 | return self.get_material_value(c) # todo: deal with nonlinear material 130 | 131 | def get_convective_velocity_function(self, convective_velocity): 132 | import ufl.tensors # used in coupled NS and energy form 133 | if isinstance(convective_velocity, ufl.tensors.ListTensor): 134 | return convective_velocity 135 | else: 136 | self.vector_function_space = VectorFunctionSpace(self.mesh, 'CG', self.settings['fe_degree']+1) 137 | vel = self.translate_value(convective_velocity, self.vector_function_space) 138 | #print('type of convective_velocity', type(convective_velocity), type(vel)) 139 | #print("vel.ufl_shape", vel.ufl_shape) 140 | return vel 141 | 142 | def update_boundary_conditions(self, time_iter_, T, Tq, ds): 143 | # test_function is removed from integrals_N items, so SPUG residual can include boundary condition 144 | capacity = self.capacity(T) # constant, experssion or tensor 145 | 146 | bcs = [] 147 | integrals_N = [] 148 | if 'point_source' in self.settings and self.settings['point_source']: 149 | ps = self.settings['point_source'] 150 | # FIXME: not test yet, assuming PointSource type, or a list of PointSource(value, position) 151 | if isinstance(ps, PointSource): 152 | bcs.append(ps) 153 | else: # a list of tuple (point, density) 154 | ps_list =[PointSource(self.function_space, Point(si[0]), si[1]) for si in ps] 155 | bcs += ps_list 156 | 157 | mesh_normal = FacetNormal(self.mesh) # n is predefined as outward as positive 158 | if 'surface_source' in self.settings and self.settings['surface_source']: 159 | gS = self.get_flux(self.settings['surface_source']['value']) 160 | if 'direction' in self.settings['surface_source'] and self.settings['surface_source']['direction']: 161 | direction_vector = self.settings['surface_source']['direction'] 162 | else: 163 | integrals_N.append(dot(mesh_normal*gS, v)*ds) 164 | 165 | for name, bc_settings in self.boundary_conditions.items(): 166 | i = bc_settings['boundary_id'] 167 | bc = self.get_boundary_variable(bc_settings) # should deal with 'value' and 'values = []' 168 | 169 | if bc['type'] == 'Dirichlet' or bc['type'] == 'fixedValue': 170 | if not isinstance(bc['value'], DirichletBC): 171 | T_bc = self.translate_value(bc['value']) 172 | dbc = DirichletBC(self.function_space, T_bc, self.boundary_facets, i) 173 | bcs.append(dbc) 174 | else: 175 | bcs.append(bc['value']) 176 | elif bc['type'] == 'Neumann' or bc['type'] =='fixedGradient': # unit: K/m 177 | g = self.translate_value(bc['value']) 178 | if self.using_diffusion_form: 179 | integrals_N.append(g*Tq*ds(i)) 180 | else: 181 | integrals_N.append(capacity*g*Tq*ds(i)) 182 | #integrals_N.append(inner(capacity * inner(normal, g), Tq)*ds(i)) # if g is a grad vector 183 | elif bc['type'] == 'symmetry': 184 | pass # zero gradient 185 | elif bc['type'] == 'mixed' or bc['type'] == 'Robin': 186 | T_bc = self.translate_value(bc['value']) 187 | g = self.translate_value(bc['gradient']) 188 | if self.using_diffusion_form: # solve T 189 | integrals_N.append(g*Tq*ds(i)) 190 | else: # solver flux 191 | integrals_N.append(capacity*g*Tq*ds(i)) 192 | dbc = DirichletBC(self.function_space, T_bc, self.boundary_facets, i) 193 | bcs.append(dbc) 194 | elif bc['type'].lower().find('flux')>=0 or bc['type'] == 'electric_current': 195 | # flux is a general flux density, heatFlux: W/m2 is not a general flux name 196 | g = self.translate_value(bc['value']) 197 | if self.using_diffusion_form: 198 | integrals_N.append(g/capacity*Tq*ds(i)) 199 | else: 200 | integrals_N.append(g*Tq*ds(i)) 201 | elif bc['type'] == 'HTC': # FIXME: HTC is not a general name or general type, only for thermal analysis 202 | #Robin, how to get the boundary value, T as the first, HTC as the second, does not apply to nonlinear PDE 203 | Ta = self.translate_value(bc['ambient']) 204 | htc = self.translate_value(bc['value']) # must be specified in Constant or Expressed in setup dict 205 | if self.using_diffusion_form: 206 | integrals_N.append( htc/capacity*(Ta-T)*Tq*ds(i)) 207 | else: 208 | integrals_N.append( htc*(Ta-T)*Tq*ds(i)) 209 | else: 210 | raise SolverError('boundary type`{}` is not supported'.format(bc['type'])) 211 | return bcs, integrals_N 212 | 213 | def get_body_source_items(self, time_iter_, T, Tq, dx): 214 | bs = self.get_body_source() # defined in base solver, has already translated value 215 | print("body source: ", bs) 216 | if bs and isinstance(bs, dict): 217 | S = [] 218 | for k,v in bs.items(): 219 | # it is good to using DG for multi-scale meshing, subdomain marking double 220 | S.append(v['value']*Tq*dx(v['subdomain_id'])) 221 | return S 222 | else: 223 | if bs: 224 | return [bs*Tq*dx] 225 | else: 226 | return None 227 | 228 | def generate_form(self, time_iter_, T, T_test, T_current, T_prev): 229 | # T, Tq can be shared between time steps, form is unified diffussion coefficient 230 | normal = FacetNormal(self.mesh) 231 | 232 | dx= Measure("dx", subdomain_data=self.subdomains) # cells 233 | ds= Measure("ds", subdomain_data=self.boundary_facets) #boundary cells 234 | #dS = Measure("dS", subdomain_data=self.boundary_facets) 235 | 236 | conductivity = self.conductivity(T) # constant, experssion or tensor, function of T for nonlinear 237 | #print('conductivity = ', conductivity) 238 | capacity = self.capacity(T) # density * specific capacity -> volumetrical capacity 239 | #print('capacity = ', capacity) 240 | #diffusivity = self.diffusivity(T) # diffusivity not in used for this conductivity form 241 | #print("diffusivity = ", diffusivity) 242 | 243 | # detection here, to deal with assigned velocity after solver intialization 244 | if not hasattr(self, 'convective_velocity'): # if velocity is not directly assigned to the solver 245 | if 'convective_velocity' in self.settings and self.settings['convective_velocity']: 246 | self.convective_velocity = self.settings['convective_velocity'] 247 | else: 248 | self.convective_velocity = None 249 | 250 | if self.convective_velocity: 251 | if 'advection_settings' in self.settings: 252 | ads = self.settings['advection_settings'] # a very big panelty factor can stabalize, but leading to diffusion error 253 | else: 254 | ads = {'stabilization_method': None} # default none 255 | 256 | velocity = self.get_convective_velocity_function(self.convective_velocity) 257 | h = 2*Circumradius(self.mesh) # cell size 258 | 259 | if ads['stabilization_method'] == 'SPUG': 260 | # Add SUPG stabilisation terms 261 | print('solving convection by SPUG stablization') 262 | #`Numerical simulations of advection-dominated scalar mixing with applications to spinal CSF flow and drug transport` page 20 263 | # SPUG_method == 2, ref: 264 | vnorm = sqrt(dot(velocity, velocity)) 265 | Pe = ads['Pe'] # Peclet number: 266 | tau = 0.5*h*pow(4.0/(Pe*h)+2.0*vnorm,-1.0) # this user-chosen value 267 | delta = h/(2*vnorm) 268 | SPUG_method = 2 269 | if SPUG_method == 2: 270 | Tq = (T_test + tau*inner(velocity, grad(T_test))) # residual and variatonal form has diff sign for the diffusion item 271 | elif SPUG_method == 1: 272 | Tq = T_test 273 | else: 274 | raise SolverError('SPUG only has only 2 variants') 275 | else: 276 | Tq = T_test 277 | else: 278 | Tq = T_test 279 | 280 | # poission equation, unified for all kind of variables 281 | # it apply to nonlinear conductivity if solved in nonlinear way, which is a function of temperature 282 | # if using external nonlinear looping: d(conductivity)/ dT * div(grad(T)) * Tq * dx 283 | # div(grad(T)) is difficult to calc, so this item is ignored here, it works as capacity/dT is more important 284 | def F_static(T, Tq): 285 | return inner(conductivity * grad(T), grad(Tq))*dx 286 | 287 | if self.transient_settings['transient']: 288 | dt = self.get_time_step(time_iter_) 289 | theta = Constant(0.5) # Crank-Nicolson time scheme 290 | # Define time discretized equation, it depends on scalar type: Energy, Species, 291 | # FIXME: nonlinear capacity is not supported 292 | F = (1.0/dt)*inner(T-T_prev, Tq)*capacity*dx \ 293 | + theta*F_static(T, Tq) + (1.0-theta)*F_static(T_prev, Tq) # FIXME: check using T_0 or T_prev ? 294 | else: 295 | F = F_static(T, Tq) 296 | 297 | bcs, integrals_N = self.update_boundary_conditions(time_iter_, T, Tq, ds) 298 | if integrals_N: 299 | F -= sum(integrals_N) 300 | 301 | bs_items = self.get_body_source_items(time_iter_,T, Tq, dx) 302 | if bs_items: 303 | F -= sum(bs_items) 304 | 305 | if self.convective_velocity: 306 | if self.nonlinear_material: # those 2 expr are equal 307 | F += inner(velocity, grad(T*capacity))*Tq*dx 308 | if 'dc_dT' in self.material and isinstance(capacity, Function): # external nonlinear looping 309 | F += inner(velocity, grad(T))*Tq*self.material['dc_dT']*T*dx 310 | else: 311 | F += inner(velocity, grad(T))*Tq*capacity*dx 312 | if ads['stabilization_method'] and ads['stabilization_method'] == 'IP': 313 | print('solving convection by interior penalty stablization') 314 | alpha = Constant(ads['alpha']) 315 | F += alpha*avg(h)**2*inner(jump(grad(T),normal), jump(grad(Tq),normal))*capacity*dS 316 | # http://www.karlin.mff.cuni.cz/~hron/fenics-tutorial/convection_diffusion/doc.html 317 | if ads['stabilization_method'] and ads['stabilization_method'] == 'SPUG' and SPUG_method == 1: 318 | #https://fenicsproject.org/qa/6951/help-on-supg-method-in-the-advection-diffusion-demo/ 319 | if self.transient_settings['transient']: 320 | residual = dot(velocity, grad(T)) - theta*conductivity*div(grad(T)) - (1.0-theta)*conductivity*div(grad(T_prev)) \ 321 | + (1.0/dt)*inner(T-T_prev, Tq)*capacity # FIXME: 322 | else: 323 | residual = dot(velocity, grad(T)) - conductivity*div(grad(T)) # diffusion item sign is different from variational form 324 | F_residual = residual * delta*dot(velocity, grad(Tq)) * dx 325 | bs_r_items = self.get_body_source_items(time_iter_,T, delta*dot(velocity, grad(Tq)), dx) 326 | if bs_r_items: 327 | F_residual -= sum(bs_r_items) 328 | F += F_residual 329 | 330 | using_mass_conservation = False # not well tested, Nitsche boundary 331 | if using_mass_conservation: 332 | print('mass conservation compensation for zero mass flux on the curved boundary') 333 | sigma = Constant(2) # penalty parameter 334 | #he = self.mesh.ufl_cell().max_facet_edge_length, T - Constant(300) 335 | #F -= inner(dot(velocity, normal), dot(grad(T), normal))*Tq*capacity*ds # (1.0/ h**sigma) * 336 | F -= dot(dot(velocity, normal), T)*capacity*ds*Tq 337 | 338 | if self.scalar_name == "temperature": 339 | if ('radiation_settings' in self.settings and self.settings['radiation_settings']): 340 | self.radiation_settings = self.settings['radiation_settings'] 341 | self.has_radiation = True 342 | elif hasattr(self, 'radiation_settings') and self.radiation_settings: 343 | self.has_radiation = True 344 | else: 345 | self.has_radiation = False 346 | 347 | if self.has_radiation: 348 | #print(m_, radiation_flux, F) 349 | self.nonlinear = True 350 | F -= self.radiation_flux(T)*Tq*ds # for all surface, without considering view angle 351 | 352 | #print(F) 353 | if self.nonlinear_material: 354 | self.nonlinear = True 355 | if self.nonlinear: 356 | F = action(F, T_current) # API 1.0 still working ; newer API , replacing TrialFunction with Function for nonlinear 357 | self.J = derivative(F, T_current, T) # Gateaux derivative 358 | 359 | return F, bcs 360 | 361 | def radiation_flux(self, T): 362 | Stefan_constant = 5.670367e-8 # W/m-2/K-4 363 | if 'emissivity' in self.material: 364 | emissivity = self.material['emissivity'] # self.settings['radiation_settings']['emissivity'] 365 | elif 'emissivity' in self.radiation_settings: 366 | emissivity = self.radiation_settings['emissivity'] 367 | else: 368 | emissivity = 1.0 369 | if 'ambient_temperature' in self.radiation_settings: 370 | T_ambient_radiaton = self.radiation_settings['ambient_temperature'] 371 | else: 372 | T_ambient_radiaton = self.reference_values['temperature'] 373 | 374 | m_ = emissivity * Stefan_constant 375 | radiation_flux = m_*(T_ambient_radiaton**4 - pow(T, 4)) # it is nonlinear item 376 | return radiation_flux 377 | 378 | def solve_form(self, F, T_current, bcs): 379 | if self.nonlinear: 380 | print('solving by nonlinear solver') 381 | return self.solve_nonlinear_problem(F, T_current, bcs, self.J) 382 | else: 383 | return self.solve_linear_problem(F, T_current, bcs) 384 | 385 | ############## public API ########################## 386 | 387 | def export(self): 388 | #save and return save file name, also timestamp 389 | result_filename = self.settings['case_folder'] + os.path.sep + self.get_variable_name() + "_time0" + ".vtk" 390 | return result_filename 391 | 392 | -------------------------------------------------------------------------------- /FenicsSolver/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1" 2 | __author__ = 'Qingfeng Xia' 3 | 4 | #__all__ = [] 5 | 6 | # a function can be execute here to enable 7 | # python3 -m FenicsSolver config.json 8 | 9 | import sys 10 | from .main import main 11 | 12 | if len(sys.argv) >= 2: 13 | main(sys.argv) -------------------------------------------------------------------------------- /FenicsSolver/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2017 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | from __future__ import print_function, division 25 | import json 26 | import sys 27 | import os.path 28 | 29 | """To cope with different python versions for FreeCAD and Fenics, running as an external program 30 | python2 and python3 compatible 31 | """ 32 | 33 | if sys.version_info[0] >= 3: 34 | unicode = str 35 | 36 | _encoding = 'utf8' 37 | def _decode_list(data): 38 | rv = [] 39 | for item in data: 40 | if isinstance(item, unicode): 41 | item = item.encode(_encoding) 42 | elif isinstance(item, list): 43 | item = _decode_list(item) 44 | elif isinstance(item, dict): 45 | item = _decode_dict(item) 46 | rv.append(item) 47 | return rv 48 | 49 | def _decode_dict(data): 50 | rv = {} 51 | for key in data: 52 | value = data[key] 53 | if isinstance(key, unicode): 54 | key = key.encode(_encoding) 55 | if isinstance(value, unicode): 56 | value = value.encode(_encoding) 57 | elif isinstance(value, list): 58 | value = _decode_list(value) 59 | elif isinstance(value, dict): 60 | value = _decode_dict(value) 61 | rv[key] = value 62 | return rv 63 | #obj = json.loads(s, object_hook=_decode_dict) 64 | 65 | def load_settings(case_input): 66 | # Order of key in dict is lost 67 | if isinstance(case_input, (dict)): 68 | settings = case_input 69 | elif os.path.exists(case_input): 70 | s = open(case_input, 'r').read() 71 | settings = json.loads(s) # object_hook=_decode_dict 72 | print(settings) 73 | else: 74 | raise TypeError('{} is not supported by Fenics as case input, only path string or dict'.format(type(case_input))) 75 | return settings 76 | 77 | def main(case_input): 78 | settings = load_settings(case_input) 79 | solver_name = settings['solver_name'] 80 | if solver_name == "CoupledNavierStokesSolver": 81 | from . import CoupledNavierStokesSolver 82 | solver = CoupledNavierStokesSolver.CoupledNavierStokesSolver(settings) 83 | solver.solve() 84 | elif solver_name == "ScalarTransportSolver": 85 | from . import ScalarTransportSolver 86 | solver = ScalarTransportSolver.ScalarTransportSolver(settings) 87 | solver.solve() 88 | elif solver_name == "LinearElasticitySolver": 89 | from . import LinearElasticitySolver 90 | solver = LinearElasticitySolver.LinearElasticitySolver(settings) 91 | solver.solve() 92 | else: 93 | raise NameError('Solver name : {} is not supported by Fenics'.format(solver_name)) 94 | #plot may be done by ParaView or by fenics solver.plot() 95 | solver.plot() 96 | 97 | if __name__ == "__main__": 98 | # will mpirun also affect argv? No, the first is always the one following `python`, i.e. `main.py` 99 | print(sys.argv) 100 | if len(sys.argv) < 2: 101 | print("Not enough input argument, Usage: `python main.py case_input` \n run testing instead") 102 | # must start this solver in FenicsSolver folder 103 | 104 | else: 105 | config_file = sys.argv[1] 106 | print("run FenicsSolver with config file", config_file) 107 | main(config_file) 108 | -------------------------------------------------------------------------------- /FenicsSolver_FreeCAD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingfengxia/FenicsSolver/134466a0b01b884d176f48e34c5f5d8960450b30/FenicsSolver_FreeCAD.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Multiphysics FEM solver based on Fenics 2 | 3 | [![Build Status](https://travis-ci.org/qingfengxia/FenicsSolver.svg?branch=master)](https://travis-ci.org/qingfengxia/FenicsSolver.svg) [![Coverage Status](https://coveralls.io/repos/github/qingfengxia/FenicsSolver.svg/badge.svg?branch=master)](https://coveralls.io/github/qingfengxia/FenicsSolver.svg?branch=master) 4 | 5 | This is a sub-projects of [intelligient engineering design](https://qingfengxia.github.io/) 6 | 7 | by Qingfeng Xia, 2017~ 8 | 9 | This is derived from my personal independent research, although I am not yet financially independent in the University of Oxford. This solver has features beyond commercial commercial solvers, in its capability to solve multibody, multi-physics, multiscale and reduced-order nonlinear problems. 10 | 11 | This software project is an essential part of my research ambition in *Measurement and modelling at extreme conditions* and *Automated and intelligent engineering design*. 12 | 13 | ![Schematic of automated engineering design pipeline](https://forum.freecadweb.org/download/file.php?id=73587) 14 | 15 | > **Note** Fenics-X, the next generation Fenics, is expected in 2020. The API change in FenicsX should not affect FenicsSolver's json input format. If the mesh input format can support complicate boundary mesh, the previous effort of GUI inside FreeCAD would be more useful to solve more realistic engineering problem. I am investigating a standard solver input format, to enable coupling with other multi-physics solver. 16 | 17 | ## License 18 | 19 | LGPL licensed as [FreeCAD](https://github.com/FreeCAD/FreeCAD) and [fenics-project](https://fenicsproject.org/) 20 | 21 | ## Screenshot 22 | 23 | There has been some effort to give a GUI for setup FenicsSolver in [FreeCAD's CFD module](https://github.com/qingfengxia/Cfd) Features include boundary condition selection and setup in GUI, mesh and FenicsSolver input generation, but it is not a top priority in regarding research. 24 | 25 | ![FenicsSolver as a FEM solver in CfdWorkbench of FreeCAD](FenicsSolver_FreeCAD.png?raw=true "FenicsSolver as a CFD solver in CfdWorkbench of FreeCAD") 26 | 27 | ## Description 28 | 29 | A set of multi-physics FEM solvers based on Fenics with GUI support(via integration Fenics into FreeCAD FemWorkbench and CfdWorkbench), focusing on multi-body, reduced-order nonlinear problem and mutlti-solver coupling.It functions like COMSOL or Moose, but it is free and it is made of Python. 30 | 31 | + Solvers implemented: 32 | - Scalar Transport (heat transfer, mass transfer, electric potential, etc) 33 | - Naiver Stokes incompressible laminar flow, 34 | - linear elasticity, nonlinear (hyper-elastic) elasticity, large deformation, plasticity 35 | 36 | + Solvers under development: 37 | - scalar transport using DG 38 | - viscoelastic 39 | - Naiver Stokes compressible laminar flow 40 | - Maxwell electromagnetics 41 | - drift-diffusion (plasma and semiconductor) 42 | - wave propagation 43 | 44 | + coupling of above solvers 45 | - flow-structure interaction (coded but not yet tested) 46 | - thermal, chemical, electrical, structure process (yet completed) 47 | 48 | + Coupling to external solvers: 49 | - turbulent flow and multiphase flow will be implemented by coupled to external CFD solver, OpenFOAM. 50 | - see the sister project OpenFOAM preprocessor within FreeCAD Cfd workbench 51 | 52 | 53 | ## Installation 54 | 55 | It is python2 and python3 compatible, just as fenics itself. For Fenics version 2017.2 on ubuntu, Python3 is recommended, since there is some binary string/unicode problem in Python 2. Fenics 2017.2 also remove VTK plotting, and most of plotting in FenicsSolver examples are ignored. 56 | 57 | general installation guide on Linux: 58 | link to install lastest Fenics via PPA on ubuntu: 59 | ``` 60 | sudo add-apt-repository ppa:fenics-packages/fenics 61 | sudo apt-get update 62 | sudo apt-get install fenics python-dolfin 63 | #Fenics 2018.1 has only python3 64 | #sudo apt-get install fenics python3-dolfin 65 | ``` 66 | 67 | copy this folder to any place on the python search path (PYTHON_PATH), assuming fenics has been installed. 68 | 69 | ``` 70 | git clone https://github.com/qingfengxia/FenicsSolver.git 71 | ``` 72 | 73 | installation via PIP will be implemented later once API is stable, but an early preview v0.1 could be privided 74 | ``` 75 | #make sure you have install Fenics, then install by pip(python) or pip3(python3) 76 | sudo pip install FenicsSolver # not working 77 | ``` 78 | 79 | to install the latest version for python3 directly form github, this will not install test files 80 | ``` 81 | pip3 install git+https://github.com/qingfengxia/FenicsSolver.git#FenicsSolver 82 | # Ubuntu16.04 pip3 seems too old to install matplotlib. 83 | # fenics 2019.1 can be installed from pip 84 | ``` 85 | 86 | 87 | 88 | ## To cite this code 89 | 90 | My journal papers using this code: 91 | 92 | 1. [Quasi-static finite element modelling of thermal distribution and heat partitioning for the multi-component system of high speed metal cutting](https://scholar.google.co.uk/scholar?oi=bibs&cluster=14068789234387025355&btnI=1&hl=en), Q Xia, DRH Gillespie - Journal of Materials Processing Technology, 2019 93 | 94 | 2. [Quasi-Static Thermal Modeling of Multiscale Sliding Contact for Unlubricated Brush Seal Materials](https://scholar.google.co.uk/scholar?oi=bibs&cluster=17170664792619422119&btnI=1&hl=en) 95 | 96 | Q Xia, DRH Gillespie, AK Owen, G Franceschini - Journal of Engineering for Gas Turbines and Power, 2019 97 | 98 | ## Acknowledgement 99 | 100 | Thanks for my family members' (esp, Mrs J Wang) understanding and support, so I can work at home. 101 | 102 | 103 | 104 | ## Tested version 105 | 106 | This package is under heavy refactoring, considered alpha. 107 | 108 | This package is python 2 and python 3 compatibe. 109 | Fenics version 2017.1 tested is on Ubuntu16.04 and python 2.7 with/without FreeCAD 0.17 dev. 110 | Fenics version 2017.2 tested is on Ubuntu16.04 and Python 3, without FreeCAD GUI. 111 | Fenics version 2019.1 tested is on Ubuntu18.04 and Python 3, with FreeCAD 0.19 dev. 112 | 113 | Run the python script files with "test_" suffix, which are gtest compatible. 114 | 115 | ## Documentation: 116 | 117 | **doxygen/sphinx** generated document is planned, yet completed 118 | 119 | ## How to contribute 120 | 121 | There are lots of places to be improve: 122 | + Code review, esp. json solver setup input data structure, naming, It should be well-design and stable 123 | + testing on different Linux platform 124 | 125 | new features: 126 | + limits for variable 127 | + a general higher order temporal integral 128 | + DG scalar transportation, currently, the DG solver does not work with 3D geometry. 129 | + Maxwell equation 130 | 131 | ## Roadmap and progress 132 | 133 | see also my presentation at Fenics 18: [Automated Mechanical Engineering Design using Open Source CAE Software Packages](doc/Fenics18_Xia.pdf) 134 | 135 | ### Initial demonstration (Sep 2017) 136 | 137 | A series of object oriented solvers: *ScalerEquationSolver*, *CoupledNavierStokesSolver* and *LinearElasticitySolver*, derived from *BaseSolver*, while a few other are under active development. 138 | 139 | Case setup: json file format is the text case setup file, mapping directly to and from python dict data structure. 140 | 141 | ### FreeCAD GUI integration (Nov 2017) 142 | 143 | 2D and 3D xml mesh and case setup writing has been implemented within [CfdWorkbench](https://github.com/qingfengxia/Cfd), this feature has yet been push to FreeCAD master. 144 | 145 | Meanwhile, FreeCAD developer *joha2* has added mesh export function in FemWorkbench, once the boundary mesh can be exported, case setup for fenics solver will be write in FreeCAD workbench. 146 | 147 | 148 | ### Coupling of multiple solvers (implemented in 2018, not yet fully tested) 149 | 150 | Fluid-structure interaction coupling has a initial implementation in segregate coupling mode, see engine seal FSI simulation: 151 | ![2D FSI simulation of labyrinth seal](doc/fsi_velmag.mp4) 152 | 153 | Tight coupling of all physical fields is under design, target on thermal modelling of complicated systems like bearing, motor, etc. 154 | 155 | ### Coupling with external solvers OpenFOAM (some work has been done, but yet completed, scheduled to 2019) 156 | 157 | VTK is the data exchange format for one-way coupling from OpenFOAM to FenicsSolver, from Foam to VTK and VTK to Foam (mesh and internal field data files) 158 | Two-way coupling should be implemented with the multiphysics coupling library [preCICE](https://github.com/precice/precice) 159 | 160 | a video/presentation of my *13th OpenFOAM Workshop* presentation can be found here: 161 | [Coupling OpenFOAM with FeniCS for multiphysis simulation](https://www.iesensor.com/blog/2018/06/25/coupling-openfoam-with-fenics-for-multiphysis-simulation-openfoam-workshop-13-presentation/) 162 | 163 | ### Update code to be compatible with Fenics 2019.1 on Python3 (2019) 164 | 165 | almost done 166 | 167 | ### Coupling with electomagnetic, structural and thermal simulation (2019) 168 | 169 | #### thermal-elastic-plastic coupling 170 | #### pip packaging and installation guide 171 | #### Travis CI integration 172 | 173 | ### Update API for Fenics-X (2020) 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /data/TestHeatTransfer.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "SolverBase has the default solver settings", 3 | "solver_name": "ScalarTransportSolver", 4 | "scalar_name": "temperature", 5 | "case_folder": "/tmp/", 6 | "case_name": "TestHT", 7 | "mesh": "../data/mesh.xml", 8 | "case_file": "../data/TestHeatTransfer.json", 9 | "fe_degree": 1, 10 | "fe_family": "CG", 11 | "periodic_boundary": null, 12 | "material": { 13 | "kinematic_viscosity": 1, 14 | "name": "oil", "density": 1000, 15 | "specific_heat_capacity": 500, 16 | "thermal_conductivity": 20 17 | }, 18 | "boundary_conditions": 19 | { 20 | "inlet":{"name": "Inlet", "variable": "temperature", "value": 350, "boundary_id": 1, "type": "Dirichlet"}, 21 | "outlet": {"name": "Outlet", "variable": "temperature", "value": 300, "boundary_id": 2, "type": "Dirichlet"} 22 | }, 23 | "initial_values": {"temperature": 293}, 24 | "solver_settings": 25 | { 26 | "transient_settings": {"maximum_interation": 100, "transient": false, "starting_time": 0, "time_step": 0.01, "ending_time": 0.03}, 27 | "reference_values": {"temperature": 293}, 28 | "solver_parameters": {"relative_tolerance": 1e-7, "maximum_iterations": 500, "monitor_convergence": true} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /doc/Fenics18_Xia.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingfengxia/FenicsSolver/134466a0b01b884d176f48e34c5f5d8960450b30/doc/Fenics18_Xia.pdf -------------------------------------------------------------------------------- /doc/fsi_velmag.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingfengxia/FenicsSolver/134466a0b01b884d176f48e34c5f5d8960450b30/doc/fsi_velmag.mp4 -------------------------------------------------------------------------------- /doc/upgrade your script from Fenics 2017 to 2019.py: -------------------------------------------------------------------------------- 1 | **upgrade from Fenics 2017.1 to 2019** 2 | 3 | 4 | 5 | 6 | To enjoy 7 | 8 | + python2 is gone in Fenics 2018.1 9 | but it should be easy to upgrade, tool `2to3` 10 | `from __future__ import print_function, division` 11 | 12 | + SWIG wrapping is replaced by pybind11 in 2018.1 13 | 14 | + dolfin.dolfin_version() is gone, using dolfin.__version__ 15 | the output is the same, string of `2019.1.0` 16 | `ver = [int(s) for s in dolfin.__version__.split('.')]` 17 | 18 | + renaming in 2018.1 19 | > Rename mpi_comm_world() to MPI.comm_world. 20 | > Removed from fenics import *, use from dolfin import * 21 | > Rename ERROR, CRITICAL etc. to LogLevel.ERROR, LogLevel.CRITICAL. 22 | 23 | ```python 24 | try: 25 | mpi_comm_world_size = MPI.size(mpi_comm_world()) 26 | except: 27 | mpi_comm_world_size = MPI.size(MPI.comm_world) 28 | if mpi_comm_world_size >1: 29 | self.parallel = True 30 | ``` 31 | 32 | ## 33 | 34 | + VTK plot is gone, using matplotlib 35 | 36 | ```python 37 | if int(ver[0]) <= 2017 and int(ver[1])<2: 38 | using_matplotlib = False # using VTK 39 | else: 40 | using_matplotlib = True 41 | 42 | if not using_matplotlib: 43 | interactive() 44 | else: 45 | import matplotlib.pyplot as plt 46 | plt.show() 47 | ``` 48 | 49 | + FacetFunction is gone 50 | 51 | `boundary_facets = MeshFunction('size_t', mesh, mesh.topology().dim() - 1)` 52 | 53 | 54 | + UserExpression instead of Expression 55 | ```python 56 | if ver[0]<2018: 57 | UserExpression = Expression 58 | ``` 59 | 60 | 61 | + Function.vector().get_local() 62 | This function is available in 2017.1, just replace `vector().array()` 63 | 64 | 65 | + "Remove UnitQuadMesh and UnitHexMesh. Now use UnitSquareMesh and UnitCubeMesh with cell type qualifiers." -------------------------------------------------------------------------------- /examples/config.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import os.path 4 | 5 | # make test_*.py work for developer, without install this package 6 | try: 7 | import FenicsSolver 8 | except: 9 | parent_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) 10 | sys.path.append(parent_dir) 11 | 12 | 13 | def is_interactive(): 14 | return not is_batch() 15 | 16 | # run all the test under example folder: pytest BATCH=1 17 | def is_batch(): 18 | if 'pytest' in sys.argv: 19 | return True 20 | if 'BATCH' in os.environ: 21 | return True 22 | if os.environ.get('FENICSSOLVER_BATCH', False): # an env var set in run_all_tests.py 23 | return True 24 | return False -------------------------------------------------------------------------------- /examples/run_all_tests.py: -------------------------------------------------------------------------------- 1 | """local test before package generation and push to pypi 2 | """ 3 | 4 | from __future__ import print_function, division 5 | 6 | import os.path, os, sys 7 | 8 | import glob 9 | test_files = glob.glob("test_*.py") 10 | 11 | this_dir = os.path.dirname(os.path.realpath(__file__)) 12 | #currently, not all test can pass the test, solvers are under development 13 | test_files = """test_cfd_solver.py, test_heat_transfer.py, test_large_deformation.py, test_nonlinear_elasticity.py, test_electrostatics.py""" 14 | #test_plasticity.py, test_flow_pass_cylinder.py, test_customized_case_settings.py 15 | #test_linear_elasticity.py, takes long time to complete 16 | test_files = test_files.split(', ') 17 | 18 | os.environ['FENICSSOLVER_BATCH'] = 'TRUE' 19 | 20 | for tf in test_files: 21 | print("run the test file:", tf) 22 | exec(open(this_dir + os.path.sep + tf).read()) # compatible for both py2 and py3 23 | 24 | # unset env var, os.unsetenv() does not work on OSX 25 | del os.environ['FENICSSOLVER_BATCH'] -------------------------------------------------------------------------------- /examples/test_cfd_solver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2017 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | 25 | from __future__ import print_function, division 26 | import math 27 | import copy 28 | import numpy as np 29 | 30 | from config import is_interactive 31 | interactively = is_interactive() 32 | 33 | from dolfin import * 34 | from FenicsSolver import SolverBase 35 | 36 | 37 | import dolfin 38 | ver = [int(s) for s in dolfin.__version__.split('.')] 39 | print("Test CFD soler with dolfin version()", dolfin.__version__) 40 | if ver[0]<2018: 41 | UserExpression = Expression 42 | from mshr import Box, Rectangle, generate_mesh 43 | else: 44 | print('mshr module is not included in fenics version: ', dolfin.__version__) 45 | print('skip the test since elbow mesh can not been made') 46 | exit() 47 | 48 | 49 | transient = False 50 | T_ambient =300 51 | T_wall = 350 52 | p_inlet = 1.1e5 53 | p_outlet = 1e5 54 | 55 | length_scale = 1 56 | max_vel=1 * length_scale 57 | 58 | def setup(using_elbow = True, using_3D = False, compressible=False): 59 | zero_vel = Constant((0,0)) 60 | if using_elbow: 61 | x_min, x_max = 0 * length_scale, 2*length_scale 62 | y_min, y_max = 0 * length_scale, 2*length_scale 63 | x_mid, y_mid = 1*length_scale, 1*length_scale 64 | if using_3D: 65 | dim = 3 66 | z_min, z_max = 0 * length_scale, 1*length_scale 67 | elbow = Box(Point(x_min, y_min, z_min), Point(x_mid,y_max, z_max)) + Box(Point(x_mid, y_mid, z_min), Point(x_max, y_max, z_max)) 68 | mesh = generate_mesh(elbow, 20) 69 | front_back_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary \ 70 | and (near(x[2], z_min) or near(x[2], z_max))) 71 | zero_vel = Constant((0, 0, 0)) 72 | else: 73 | dim = 2 74 | elbow = Rectangle(Point(x_min, y_min), Point(x_mid,y_max)) + Rectangle(Point(x_mid, y_mid), Point(x_max, y_max)) 75 | mesh = generate_mesh(elbow, 20) 76 | 77 | static_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary \ 78 | and (near(x[0], x_min) or near(x[1], y_max) or near(x[0], x_mid) or near(x[1], y_mid))) 79 | bottom = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], y_min) ) 80 | outlet = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[0], x_max)) 81 | else: 82 | #length_scale = 1 83 | mesh = UnitSquareMesh(40, 100) 84 | static_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary and (near(x[0], 0) or near(x[0], 1))) 85 | 86 | bottom = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], 0) ) 87 | outlet = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], 1)) 88 | #moving_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], 1) ) 89 | #bcs_u["moving"] = {'boundary': top, 'boundary_id': 2, 'variable': "velocity", 'type': 'Dirichlet', 'value': Constant((1,0))} 90 | 91 | from collections import OrderedDict 92 | bcs_u = OrderedDict() 93 | 94 | bcs_u["static"] = {'boundary': static_boundary, 'boundary_id': 1, 95 | 'values':[{'variable': "velocity",'type': 'Dirichlet', 'value': zero_vel, 'unit': 'm/s'}, 96 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_wall} ]} 97 | if using_3D: 98 | bcs_u["front_back"] = {'boundary': front_back_boundary, 'boundary_id': 5, 99 | 'values':[{'variable': "velocity",'type': 'Dirichlet', 'value': zero_vel}, 100 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_wall} ]} 101 | 102 | bcs_p = OrderedDict() 103 | bcs_p["outlet"] = {'boundary': outlet, 'boundary_id': 3, 104 | 'values':[{'variable': "pressure", 'type': 'Dirichlet', 'value': p_outlet}, 105 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_ambient} ]} 106 | 107 | #inlet_vel_expr = Expression('max_vel * (1.0 - pow(abs(x[0]-x_mid)/x_mid, 2))', max_vel=10, x_mid=0.5) 108 | x_c=0.5 * length_scale 109 | if using_3D: 110 | _init_vel = (0, max_vel*0.2, 0) 111 | class InletVelcotyExpression(UserExpression): 112 | def eval(self, value, x): 113 | value[0] = 0 114 | value[1] = max_vel * (1.0 - (abs(x[0]-x_c)/x_c)**2) 115 | value[2] = 0 116 | def value_shape(self): 117 | return (3,) 118 | else: 119 | _init_vel = (0, max_vel*0.2) 120 | class InletVelcotyExpression(UserExpression): 121 | def eval(self, value, x): 122 | value[0] = 0 123 | value[1] = max_vel * (1.0 - (abs(x[0]-x_c)/x_c)**2) 124 | def value_shape(self): 125 | return (2,) 126 | 127 | bcs_u["inlet"] = {'boundary': bottom, 'boundary_id': 2, 128 | 'values':[{'variable': "temperature",'type': 'Dirichlet', 'value': T_ambient} ]} 129 | if transient: 130 | vels = [Constant((0, max_vel)), InletVelcotyExpression(degree=1), InletVelcotyExpression(degree=1)] 131 | bcs_u["inlet"] ['values'].append({'variable': "velocity", 'type': 'Dirichlet', 'value': vels}) 132 | else: 133 | vel = InletVelcotyExpression(degree=1) # degree = 1, may not general enough, fixme 134 | #if compressible: 135 | #bcs_u["inlet"] ['values'].append({'variable': "pressure", 'type': 'Dirichlet', 'value': p_inlet}) 136 | #else: 137 | bcs_u["inlet"] ['values'].append({'variable': "velocity", 'type': 'Dirichlet', 'value': vel}) 138 | 139 | bcs = bcs_p.copy() 140 | for k in bcs_u: 141 | bcs[k] = bcs_u[k] 142 | 143 | s = copy.copy(SolverBase.default_case_settings) 144 | ''' 145 | dt = 0.001 146 | t_end = 0.005 147 | transient_settings = {'transient': True, 'starting_time': 0.0, 'time_step': dt, 'ending_time': t_end} 148 | s['solver_settings']['transient_settings'] = transient_settings 149 | ''' 150 | 151 | s['mesh'] = mesh 152 | print(info(mesh)) 153 | s['boundary_conditions'] = bcs 154 | s['initial_values'] = {'velocity': _init_vel, "temperature": T_ambient, "pressure": 1e5} 155 | s['solver_settings']['reference_values'] = {'velocity': (1, 1), "temperature": T_ambient, "pressure": 1e5} 156 | 157 | return s 158 | 159 | def test_compressible(): 160 | s = setup(using_elbow = True, using_3D = True, compressible = True) 161 | fluid = {'name': 'ideal gas', 'kinematic_viscosity': 1e-2, 'density': 1.3} 162 | s['material'] = fluid 163 | 164 | from FenicsSolver import CompressibleNSSolver 165 | solver = CompressibleNSSolver.CompressibleNSSolver(s) # set a very large viscosity for the large inlet width 166 | #solver.init_values = Expression(('1', '0', '1e-5'), degree=1) 167 | if interactively: 168 | solver.plot() 169 | 170 | def test_incompressible(using_elbow = True, coupling_energy_equation = True, Newtonian = True ): 171 | s = setup(using_elbow, using_3D = False, compressible = False) 172 | s['solving_temperature'] = coupling_energy_equation 173 | # nonNewtonian diverges! 174 | 175 | if using_elbow: 176 | Re = 1e0 # see pressure change, # stabilization_method seems make no difference 177 | else: 178 | s['fe_degree'] = 1 # test failed, can not finish JIT 179 | Re = 10 # mesh is good enough to simulate higher Re 180 | 181 | fluid = {'name': 'gas', 'kinematic_viscosity': (length_scale * max_vel)/Re, 'density': 1, 182 | 'specific_heat_capacity': 420, 'thermal_conductivity': 0.1, 'Newtonian': Newtonian} 183 | s['material'] = fluid 184 | 185 | # Re=10 is working without G2, when Re=1e-3 got NaN error, it is not clear why 186 | #s['advection_settings'] = {'Re': Re, 'stabilization_method': 'G2' , 'kappa1': 4, 'kappa2': 2} 187 | print("Reynolds number = ", Re) 188 | 189 | from FenicsSolver import CoupledNavierStokesSolver 190 | solver = CoupledNavierStokesSolver.CoupledNavierStokesSolver(s) # set a very large viscosity for the large inlet width 191 | #solver.using_nonlinear_solver = False 192 | if coupling_energy_equation: 193 | u,p,T= split(solver.solve()) 194 | else: 195 | u,p= split(solver.solve()) 196 | 197 | if interactively: 198 | solver.plot() 199 | 200 | if not coupling_energy_equation and False: # not fully tested 201 | from FenicsSolver import ScalarTransportSolver 202 | solver_T = ScalarTransportSolver.ScalarTransportSolver(s) 203 | #Q = solver.function_space.sub(1) # seem it is hard to share vel between. u is vectorFunction 204 | Q = solver_T.function_space 205 | cvel = VectorFunction(Q) 206 | cvel.interpolate(u) # degree ? 207 | selver_T.convective_velocity = cvel 208 | T = solver_T.solve() 209 | 210 | 211 | if __name__ == '__main__': 212 | test_incompressible(using_elbow = True, coupling_energy_equation = True, Newtonian = True) 213 | #test_incompressible(using_elbow = True, coupling_energy_equation = False, Newtonian = False) 214 | #test_incompressible(False, False, True) # driven cavity failed 215 | #test_incompressible(False, True) # Elbow 3D is slow but possible 216 | 217 | # manually call test function, if discovered by google test, it will not plot in interactive mode 218 | #test_compressible(True, True) -------------------------------------------------------------------------------- /examples/test_customized_case_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2017 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | from __future__ import print_function, division 25 | 26 | """ 27 | This is outdated, served solver for FreeCAD CFD GUI 28 | """ 29 | 30 | from config import is_interactive 31 | interactively = is_interactive() # manual set it False to debug solver 32 | 33 | from FenicsSolver.main import load_settings 34 | from FenicsSolver import ScalarTransportSolver 35 | 36 | def test_elasticity(): 37 | pass 38 | 39 | def test_CFD(): 40 | # 41 | raise NotImplementedError("currently, CFD test is not usable, try CFD test in examples folder") 42 | df = '../data/TestCFD.json' 43 | settings = load_settings(df) # force python2 load ascii string 44 | #settings['case_folder'] = os.path.abspath(os.path.dirname(__file__)) + os.path.sep + "data" 45 | # 46 | from FenicsSolver import CoupledNavierStokesSolver 47 | solver = CoupledNavierStokesSolver.CoupledNavierStokesSolver(settings) 48 | solver.print() 49 | solver.solve() 50 | solver.plot() 51 | 52 | def test_heat_transfer(): 53 | #print(os.path.dirname(__file__)) # __file__ is absolute path for python 3.4+ 54 | df = '../data/TestHeatTransfer.json' 55 | settings = load_settings(df) # load FreeCAD GUI generated json data 56 | #settings['case_folder'] = os.path.abspath(os.path.dirname(__file__)) + os.path.sep + "data" 57 | ########################################### 58 | """ here lot of customization can be done, e.g. 59 | settings['body_source'] = dolfin.Expression('', degree=1) 60 | anisotropic material, convective velociyt, see examples/test_*.py for more details 61 | """ 62 | ########################################### 63 | solver = ScalarTransportSolver.ScalarTransportSolver(settings) 64 | solver.print() 65 | solver.solve() 66 | solver.plot() 67 | 68 | if __name__ == "__main__": 69 | test_heat_transfer() -------------------------------------------------------------------------------- /examples/test_electrostatics.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2017 - Qingfeng Xia * * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | from __future__ import print_function, division 24 | import math 25 | import numpy as np 26 | 27 | from config import is_interactive 28 | interactively = is_interactive() # manual set it False to debug solver 29 | 30 | from dolfin import * 31 | from FenicsSolver.ScalarTransportSolver import ScalarTransportSolver 32 | 33 | 34 | #mesh = UnitCubeMesh(20, 20, 20) 35 | mesh = UnitSquareMesh(40, 40) 36 | Q = FunctionSpace(mesh, "CG", 1) 37 | 38 | cx_min, cy_min, cx_max, cy_max = 0,0,1,1 39 | # no need for on_boundary, it should capture the boundary 40 | top = AutoSubDomain(lambda x: near(x[1], cy_max) ) 41 | bottom = AutoSubDomain(lambda x: near(x[1],cy_min)) 42 | left = AutoSubDomain(lambda x: near(x[0], cx_min) ) 43 | right = AutoSubDomain(lambda x: near(x[0],cx_max)) 44 | 45 | V_high = 360 46 | V_low = 300 47 | V_ground = 300 48 | resistivity = 2300 # ohm.m 49 | # source item: unit: C/m3 50 | # flux boundary condition (surface charge): C/m2 51 | # Neumann boundary, unit V/m 52 | # PointSource (Dirac delta function) is supported, uint? 53 | # API: PointSource(V, p, magnitude=1.0), Create point source at given coordinates point of given magnitude 54 | 55 | 56 | #polarity is not considered by this solver 57 | 58 | magnetic_permeability_0 = 4 * pi * 1e-7 59 | electric_permittivity_0 = 8.854187817e-12 60 | K_anisotropic = Expression((('exp(x[0])','sin(x[1])'), ('sin(x[0])','tan(x[1])')), degree=0) 61 | 62 | # dielectric or conducting 63 | material = {'name': "silicon", 'thermal_conductivity': 149, 'specific_heat_capacity': 1000, \ 64 | 'density': 2500, 'sound_speed': 8433, 'Poisson_ratio': 0.2, 'elastic_modulus': 1.5e11, \ 65 | 'magnetic_permeability': magnetic_permeability_0*1, \ 66 | 'relative_electric_permittivity': 11.7, 'electric_conductivity': 1.0/resistivity} # Tref set in reference_values 67 | 68 | 69 | length = cy_max - cy_min 70 | epsilon = material['relative_electric_permittivity']*electric_permittivity_0 71 | electric_displacement = (V_high-V_low)/length * epsilon # divided by length scale which is unity 1 -> heat flux W/m^2 72 | 73 | bcs = { 74 | "hot": {'boundary': top, 'boundary_id': 1, 'type': 'Dirichlet', 'value': Constant(V_high)}, 75 | "left": {'boundary': left, 'boundary_id': 3, 'type': 'flux', 'value': Constant(0)}, 76 | "right": {'boundary': right, 'boundary_id': 4, 'type': 'flux', 'value': Constant(0)}, 77 | #back and front is zero gradient, need not set, it is default 78 | } 79 | 80 | settings = {'solver_name': 'ScalarTransportSolver', 81 | 'mesh': None, 'function_space': Q, 'periodic_boundary': None, 'element_degree': 1, 82 | 'boundary_conditions': bcs, 'body_source': None, 83 | 'initial_values': {'electric_potential': V_ground}, 84 | 'material': material, 85 | 'solver_settings': { 86 | 'transient_settings': {'transient': False, 'starting_time': 0, 'time_step': 0.1, 'ending_time': 1}, 87 | 'reference_values': {'temperature': 300, 'electric_potential': V_ground}, 88 | 'solver_parameters': {"relative_tolerance": 1e-9, # mapping to solver.parameters of Fenics 89 | "maximum_iterations": 500, 90 | "monitor_convergence": True, # print to console 91 | }, 92 | }, 93 | # solver specific settings 94 | 'scalar_name': 'electric_potential', 95 | } 96 | 97 | def test(): 98 | using_convective_velocity = False 99 | 100 | using_anisotropic_material = True 101 | if using_anisotropic_material: 102 | #tensor-weighted-poisson/python/demo_tensor-weighted-poisson.py 103 | settings['material']['relative_elelectric_permittivity'] = K_anisotropic 104 | else: 105 | print("analytical current density [A/m^2] = ", electric_displacement) 106 | 107 | #if using_convective_velocity: 108 | bcs["cold"] = {'boundary': bottom, 'boundary_id': 2, 'type': 'Dirichlet', 'value': Constant(V_low)} 109 | #bcs["cold"] = {'boundary': bottom, 'boundary_id': 2, 'type': 'flux', 'value': Constant(electric_displacement)} 110 | 111 | if using_convective_velocity: 112 | settings['convective_velocity'] = Constant((0.5, -0.5)) 113 | else: 114 | settings['convective_velocity'] = None 115 | solver = ScalarTransportSolver(settings) 116 | #debugging: show boundary selection 117 | plot(solver.boundary_facets, "boundary facets colored by ID") 118 | plot(solver.subdomains, "subdomain cells colored by ID") 119 | 120 | T = solver.solve() 121 | post_process(T) 122 | if interactively: 123 | solver.plot() 124 | 125 | def post_process(T): 126 | # Report flux, they should match 127 | normal = FacetNormal(mesh) 128 | boundary_facets = MeshFunction('size_t', mesh, mesh.topology().dim() - 1) 129 | boundary_facets.set_all(0) 130 | id=1 131 | bottom.mark(boundary_facets, id) 132 | ds= Measure("ds", subdomain_data=boundary_facets) 133 | 134 | flux = assemble(epsilon * dot(grad(T), normal)*ds(id)) 135 | print("integral on the top surface(A)", flux) 136 | 137 | plot(T, title='electric Potential (V)') 138 | 139 | if __name__ == '__main__': 140 | test() 141 | -------------------------------------------------------------------------------- /examples/test_flow_pass_cylinder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2018 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | """ 25 | FEniCS tutorial demo program: Incompressible Navier-Stokes equations 26 | for flow around a cylinder using the Incremental Pressure Correction 27 | Scheme (IPCS). 28 | 29 | u' + u . nabla(u)) - div(sigma(u, p)) = f 30 | div(u) = 0 31 | """ 32 | 33 | from __future__ import print_function, division 34 | import math 35 | import copy 36 | import numpy as np 37 | 38 | from config import is_interactive 39 | interactively = is_interactive() # manual set it False to debug solver 40 | 41 | from dolfin import * 42 | from mshr import * 43 | from FenicsSolver import SolverBase 44 | 45 | 46 | using_fenics_solver = True 47 | using_segegrate_solver = not using_fenics_solver 48 | 49 | t_end = 5.0 # final time 50 | num_steps = 5000 # number of time steps 51 | dt = t_end / num_steps # time step size 52 | mu = 0.01 # dynamic viscosity 53 | rho = 1 # density 54 | transient_settings = {'transient': False, 'starting_time': 0.0, 'time_step': dt, 'ending_time': t_end} 55 | 56 | # Create mesh 57 | x_min, x_max = 0, 2.2 58 | y_min, y_max = 0, 0.41 59 | c_x, c_y = 0.2, 0.2 60 | c_radius = 0.05 61 | channel = Rectangle(Point(x_min, y_min), Point(x_max, y_max)) 62 | cylinder = Circle(Point(c_x, c_y), c_radius) 63 | domain = channel - cylinder 64 | mesh = generate_mesh(domain, 64) 65 | 66 | vel_mean = 4.0 67 | # Define inflow profile 68 | inflow_profile = ('{}*1.5*x[1]*(0.41 - x[1]) / pow(0.41, 2)'.format(vel_mean), '0') 69 | 70 | if using_fenics_solver: 71 | coupling_energy_equation = False 72 | mu = 3 73 | rho = 100 74 | #vel_mean = 0.4 75 | 76 | length_scale = (y_max - y_min) 77 | Re = length_scale * vel_mean / (mu/rho) 78 | print("Reynolds number = ", Re) 79 | 80 | gdim = 2 81 | zero_vel = Constant(gdim*(0,)) 82 | p_outlet = 10000 83 | T_ambient = 300 84 | 85 | _inlet_vel = Constant((vel_mean, 0)) # Expression(inflow_profile, degree = 2) 86 | 87 | inlet_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[0], x_min)) 88 | outlet_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[0], x_max)) 89 | static_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], y_min) or near(x[1], y_max)) 90 | cylinder_boundary = AutoSubDomain(lambda x, on_boundary: on_boundary and x[0]>0.1 and x[0]<0.3 and x[1]>0.1 and x[1]<0.3) 91 | 92 | from collections import OrderedDict 93 | bcs = OrderedDict() 94 | 95 | bcs["wall"] = {'boundary': static_boundary, 'boundary_id': 0, 96 | 'values':[{'variable': "velocity",'type': 'Dirichlet', 'value': zero_vel, 'unit': 'm/s'}, 97 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_ambient} ]} 98 | bcs["cylinder"] = {'boundary': cylinder_boundary, 'boundary_id': 1, 99 | 'values':[{'variable': "velocity",'type': 'Dirichlet', 'value': zero_vel, 'unit': 'm/s'}, 100 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_ambient} ]} 101 | bcs["inlet"] = {'boundary': inlet_boundary, 'boundary_id': 2, 102 | 'values':[{'variable': "velocity", 'type': 'Dirichlet', 'value': _inlet_vel}, 103 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_ambient} ]} 104 | bcs["outlet"] = {'boundary': outlet_boundary, 'boundary_id': 3, 105 | 'values':[{'variable': "pressure", 'type': 'Dirichlet', 'value': p_outlet}, 106 | {'variable': "temperature",'type': 'Dirichlet', 'value': T_ambient} ]} 107 | 108 | 109 | s = copy.copy(SolverBase.default_case_settings) 110 | s['mesh'] = mesh 111 | s['fe_family'] = 'CG' # 'P' 112 | print(info(mesh)) 113 | s['boundary_conditions'] = bcs 114 | s['initial_values'] = {'velocity': (0, 0), "temperature": T_ambient, "pressure": p_outlet} 115 | s['solver_settings']['reference_values'] = {'velocity': (1, 1), "temperature": T_ambient, "pressure": p_outlet} 116 | s['solver_settings']['solver_parameters'] = { 117 | "relative_tolerance": 1e-9, # mapping to solver.parameters of Fenics 118 | "maximum_iterations": 50, 119 | "relaximation_parameter": 0.001, 120 | "monitor_convergence": True, # print to console 121 | }, 122 | s['solving_temperature'] = coupling_energy_equation 123 | fluid = {'name': 'oil', 'kinematic_viscosity': mu/rho, 'density': rho, 124 | 'specific_heat_capacity': 4200, 'thermal_conductivity': 0.1, 'Newtonian': True} 125 | s['material'] = fluid 126 | 127 | #s['advection_settings'] = {'Re': Re, 'stabilization_method': 'G2' , 'kappa1': 4, 'kappa2': 2} 128 | 129 | from FenicsSolver import CoupledNavierStokesSolver 130 | solver = CoupledNavierStokesSolver.CoupledNavierStokesSolver(s) # set a very large viscosity for the large inlet width 131 | solver.using_nonlinear_solver = False 132 | solver.transient_settings['transient'] = False 133 | from pprint import pprint 134 | #pprint(solver.settings) 135 | plot(solver.boundary_facets) 136 | up0 = solver.solve() # get a realistic intial valve from static simulation 137 | solver.initial_values = up0 138 | solver.transient_settings['transient'] = transient_settings 139 | solver.solve() # no need to transform static bc into transient bc 140 | 141 | if interactively: 142 | solver.plot() 143 | 144 | if using_segegrate_solver: 145 | # Define expressions used in variational forms 146 | f = Constant((0, 0)) 147 | k = Constant(dt) 148 | mu = Constant(mu) 149 | rho = Constant(rho) 150 | 151 | # Define boundaries 152 | inflow = 'near(x[0], {})'.format(x_min) 153 | outflow = 'near(x[0], {})'.format(x_max) 154 | walls = 'near(x[1], {}) || near(x[1], {})'.format(y_min, y_max) 155 | cylinder = 'on_boundary && x[0]>0.1 && x[0]<0.3 && x[1]>0.1 && x[1]<0.3' 156 | #AutoSubDomain 157 | 158 | # Define inflow profile 159 | inflow_profile = ('4.0*1.5*x[1]*(0.41 - x[1]) / pow(0.41, 2)', '0') 160 | 161 | # Define function spaces, why not CG ? 162 | V = VectorFunctionSpace(mesh, 'P', 2) 163 | Q = FunctionSpace(mesh, 'P', 1) 164 | 165 | # Define boundary conditions 166 | bcu_inflow = DirichletBC(V, Expression(inflow_profile, degree=2), inflow) 167 | bcu_walls = DirichletBC(V, Constant((0, 0)), walls) 168 | bcu_cylinder = DirichletBC(V, Constant((0, 0)), cylinder) 169 | bcp_outflow = DirichletBC(Q, Constant(0), outflow) 170 | 171 | 172 | bcu = [bcu_inflow, bcu_walls, bcu_cylinder] 173 | bcp = [bcp_outflow] 174 | 175 | # Define trial and test functions 176 | u = TrialFunction(V) 177 | v = TestFunction(V) 178 | p = TrialFunction(Q) 179 | q = TestFunction(Q) 180 | 181 | # Define functions for solutions at previous and current time steps 182 | u_n = Function(V) 183 | u_ = Function(V) 184 | p_n = Function(Q) 185 | p_ = Function(Q) 186 | 187 | U = 0.5*(u_n + u) 188 | n = FacetNormal(mesh) 189 | 190 | # Define symmetric gradient 191 | def epsilon(u): 192 | return sym(nabla_grad(u)) 193 | 194 | # Define stress tensor 195 | def sigma(u, p): 196 | return 2*mu*epsilon(u) - p*Identity(len(u)) 197 | 198 | # Define variational problem for step 1 199 | F1 = rho*dot((u - u_n) / k, v)*dx \ 200 | + rho*dot(dot(u_n, nabla_grad(u_n)), v)*dx \ 201 | + inner(sigma(U, p_n), epsilon(v))*dx \ 202 | + dot(p_n*n, v)*ds - dot(mu*nabla_grad(U)*n, v)*ds \ 203 | - dot(f, v)*dx 204 | a1 = lhs(F1) 205 | L1 = rhs(F1) 206 | 207 | # Define variational problem for step 2 208 | a2 = dot(nabla_grad(p), nabla_grad(q))*dx 209 | L2 = dot(nabla_grad(p_n), nabla_grad(q))*dx - (1/k)*div(u_)*q*dx 210 | 211 | # Define variational problem for step 3 212 | a3 = dot(u, v)*dx 213 | L3 = dot(u_, v)*dx - k*dot(nabla_grad(p_ - p_n), v)*dx 214 | 215 | # Assemble matrices 216 | A1 = assemble(a1) 217 | A2 = assemble(a2) 218 | A3 = assemble(a3) 219 | 220 | # Apply boundary conditions to matrices 221 | [bc.apply(A1) for bc in bcu] 222 | [bc.apply(A2) for bc in bcp] 223 | 224 | # Create XDMF files for visualization output 225 | xdmffile_u = XDMFFile('navier_stokes_cylinder/velocity.xdmf') 226 | xdmffile_p = XDMFFile('navier_stokes_cylinder/pressure.xdmf') 227 | 228 | # Create time series (for use in reaction_system.py) 229 | timeseries_u = TimeSeries('navier_stokes_cylinder/velocity_series') 230 | timeseries_p = TimeSeries('navier_stokes_cylinder/pressure_series') 231 | 232 | # Save mesh to file (for use in reaction_system.py) 233 | File('navier_stokes_cylinder/cylinder.xml.gz') << mesh 234 | 235 | # Create progress bar 236 | progress = Progress('Time-stepping') 237 | set_log_level(PROGRESS) 238 | 239 | # Time-stepping 240 | t = 0 241 | for n in range(num_steps): 242 | 243 | # Update current time 244 | t += dt 245 | 246 | # Step 1: Tentative velocity step 247 | b1 = assemble(L1) 248 | [bc.apply(b1) for bc in bcu] 249 | solve(A1, u_.vector(), b1, 'bicgstab', 'hypre_amg') 250 | 251 | # Step 2: Pressure correction step 252 | b2 = assemble(L2) 253 | [bc.apply(b2) for bc in bcp] 254 | solve(A2, p_.vector(), b2, 'bicgstab', 'hypre_amg') 255 | 256 | # Step 3: Velocity correction step 257 | b3 = assemble(L3) 258 | solve(A3, u_.vector(), b3, 'cg', 'sor') 259 | 260 | # Plot solution 261 | plot(u_, title='Velocity') 262 | plot(p_, title='Pressure') 263 | 264 | # Save solution to file (XDMF/HDF5) 265 | xdmffile_u.write(u_, t) 266 | xdmffile_p.write(p_, t) 267 | 268 | # Save nodal values to file 269 | timeseries_u.store(u_.vector(), t) 270 | timeseries_p.store(p_.vector(), t) 271 | 272 | # Update previous solution 273 | u_n.assign(u_) 274 | p_n.assign(p_) 275 | 276 | # Update progress bar 277 | progress.update(t / t_end) 278 | print('u max:', u_.vector().array().max()) 279 | 280 | # Hold plot 281 | interactive() 282 | -------------------------------------------------------------------------------- /examples/test_heat_transfer.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2017 - Qingfeng Xia * * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | from __future__ import print_function, division 24 | import math 25 | import numpy as np 26 | 27 | from config import is_interactive 28 | interactively = is_interactive() 29 | 30 | from dolfin import * 31 | from FenicsSolver.ScalarTransportSolver import ScalarTransportSolver 32 | 33 | #mesh = UnitCubeMesh(20, 20, 20) 34 | mesh = UnitSquareMesh(40, 40) 35 | Q = FunctionSpace(mesh, "CG", 1) 36 | #print(dir(Q)) 37 | #print(dir(Q._ufl_element)) 38 | #print(Q._ufl_element.degree()) 39 | 40 | cx_min, cy_min, cx_max, cy_max = 0,0,1,1 41 | # no need for on_boundary, it should capture the boundary 42 | top = AutoSubDomain(lambda x: near(x[1], cy_max) ) 43 | bottom = AutoSubDomain(lambda x: near(x[1],cy_min)) 44 | left = AutoSubDomain(lambda x: near(x[0], cx_min) ) 45 | right = AutoSubDomain(lambda x: near(x[0],cx_max)) 46 | 47 | T_hot = 360 48 | T_cold = 300 49 | T_ambient = 300 50 | 51 | nonlinear = False 52 | if not nonlinear: 53 | conductivity = 0.6 54 | else: 55 | conductivity = lambda T: (T-T_ambient)/T_ambient * 0.6 56 | length = cy_max - cy_min 57 | heat_flux = (T_hot-T_cold)/length*conductivity # divided by length scale which is unity 1 -> heat flux W/m^2 58 | 59 | bcs = { 60 | "hot": {'boundary': top, 'boundary_id': 1, 'values': { 61 | 'temperature': {'variable': 'temperature', 'type': 'Dirichlet', 'value': Constant(T_hot)} 62 | } }, 63 | "left": {'boundary': left, 'boundary_id': 3, 'values': { 64 | 'temperature': {'variable': 'temperature', 'type': 'heatFlux', 'value': Constant(0)} 65 | } }, # unit: K/m 66 | "right": {'boundary': right, 'boundary_id': 4, 'values': { 67 | #'temperature': {'variable': 'temperature', 'type': 'heatFlux', 'value': Constant(0)} 68 | 'temperature': {'variable': 'temperature', 'type': 'symmetry', 'value': None} 69 | } } 70 | #back and front is zero gradient, need not set, it is default 71 | } 72 | 73 | settings = {'solver_name': 'ScalarEquationSolver', 74 | 'mesh': None, 'function_space': Q, 'periodic_boundary': None, 'fe_degree': 1, 75 | 'boundary_conditions': bcs, 76 | 'body_source': None, 77 | 'initial_values': {'temperature': T_ambient}, 78 | 'material':{'density': 1000, 'specific_heat_capacity': 4200, 'thermal_conductivity': 0.1}, 79 | 'solver_settings': { 80 | 'transient_settings': {'transient': False, 'starting_time': 0, 'time_step': 0.1, 'ending_time': 1}, 81 | 'reference_values': {'temperature': T_ambient}, 82 | 'solver_parameters': {"relative_tolerance": 1e-9, # mapping to solver.parameters of Fenics 83 | "maximum_iterations": 500, 84 | "monitor_convergence": True, # print to console 85 | }, 86 | }, 87 | # solver specific settings 88 | 'scalar_name': 'temperature', 89 | } 90 | 91 | K_anisotropic = Expression((('exp(x[0])','sin(x[1])'), ('sin(x[0])','tan(x[1])')), degree=0) #works! 92 | """ 93 | # Create mesh functions for c00, c01, c11 94 | c00 = MeshFunction("double", mesh, 2) # degree/dim == 2? 95 | c11 = MeshFunction("double", mesh, 2) 96 | c22 = MeshFunction("double", mesh, 2) 97 | for cell in cells(mesh): 98 | c00[cell] = conductivity 99 | c11[cell] = conductivity*0.1 100 | c22[cell] = conductivity*0.1 101 | # Code for C++ evaluation of conductivity 102 | conductivity_code = ''' 103 | 104 | class Conductivity : public Expression 105 | { 106 | public: 107 | 108 | // Create expression with 3 components 109 | Conductivity() : Expression(3) {} 110 | 111 | // Function for evaluating expression on each cell 112 | void eval(Array& values, const Array& x, const ufc::cell& cell) const 113 | { 114 | const uint D = cell.topological_dimension; 115 | const uint cell_index = cell.index; 116 | values[0] = (*c00)[cell_index]; 117 | values[1] = (*c11)[cell_index]; 118 | values[2] = (*c22)[cell_index]; 119 | } 120 | 121 | // The data stored in mesh functions 122 | std::shared_ptr > c00; 123 | std::shared_ptr > c11; 124 | std::shared_ptr > c22; 125 | 126 | }; 127 | ''' 128 | 129 | c = Expression(cppcode=conductivity_code, degree=0) 130 | c.c00 = c00 131 | c.c11 = c11 132 | c.c22 = c22 133 | K = as_matrix(((c[0], c[1]), (c[1], c[2]))) 134 | """ 135 | 136 | def setup(using_anisotropic_conductivity, using_convective_velocity, using_DG_solver, using_HTC): 137 | 138 | if using_anisotropic_conductivity: 139 | #tensor-weighted-poisson/python/demo_tensor-weighted-poisson.py 140 | K = K_anisotropic 141 | else: 142 | K = conductivity 143 | print("analytical heat flux [w/m^2] = ", heat_flux) 144 | 145 | if not using_HTC: 146 | if False: #using_convective_velocity: 147 | bcs["cold"] = {'boundary': bottom, 'boundary_id': 2, 'values': { 148 | 'temperature': {'variable': 'temperature', 'type': 'Dirichlet', 'value': Constant(T_cold)} 149 | } } 150 | else: 151 | bcs["cold"] = {'boundary': bottom, 'boundary_id': 2, 'values': { 152 | 'temperature': {'variable': 'temperature', 'type': 'heatFlux', 'value': Constant(heat_flux)} 153 | } } 154 | else: 155 | htc = 100 156 | bcs["hot"] = {'boundary': top, 'boundary_id': 1, 'values': { 157 | 'temperature': {'variable': 'temperature', 'type': 'heatFlux', 'value': Constant(heat_flux)} 158 | } } 159 | bcs["cold"] = {'boundary': bottom, 'boundary_id': 2, 'values': { 160 | 'temperature': {'variable': 'temperature', 'type': 'HTC', 'value': Constant(htc), 'ambient': Constant(T_ambient)} 161 | } } 162 | 163 | if using_convective_velocity: 164 | settings['convective_velocity'] = Constant((0.005, -0.005)) 165 | else: 166 | settings['convective_velocity'] = None 167 | 168 | solver = ScalarTransportSolver(settings) 169 | 170 | solver.material['conductivity'] = K 171 | #debugging: show boundary selection 172 | plot(solver.boundary_facets, title="boundary facets colored by ID") # matplotlib can not plot 1D 173 | plot(solver.subdomains, title="subdomain cells colored by ID") 174 | 175 | T = solver.solve() 176 | post_process(T) 177 | if interactively: 178 | solver.plot() 179 | 180 | def post_process(T): 181 | # Report flux, they should match 182 | normal = FacetNormal(mesh) 183 | boundary_facets = MeshFunction('size_t', mesh, mesh.topology().dim() - 1) 184 | boundary_facets.set_all(0) 185 | id=1 186 | bottom.mark(boundary_facets, id) 187 | ds= Measure("ds", subdomain_data=boundary_facets) 188 | 189 | flux = assemble(conductivity * dot(grad(T), normal)*ds(id)) 190 | print("heat flux rate integral on the surface(w/m^2)", flux) 191 | 192 | plot(T, title='Temperature') 193 | 194 | 195 | def test_radiation(): 196 | using_anisotropic_conductivity = False 197 | if using_anisotropic_conductivity: 198 | #tensor-weighted-poisson/python/demo_tensor-weighted-poisson.py 199 | K = K_anisotropic 200 | else: 201 | K = conductivity 202 | htc = heat_flux / (T_hot - T_cold) 203 | print("analytical heat flux [w/m^2] = ", heat_flux) 204 | 205 | #if using_convective_velocity: 206 | bcs["cold"] = {'boundary': bottom, 'boundary_id': 2, 'values': { 207 | 'temperature': {'variable': 'temperature', 'type': 'Dirichlet', 'value': Constant(T_cold)} 208 | } } 209 | settings['radiation_settings'] = {'ambient_temperature': T_ambient-20, 'emissivity': 0.9} 210 | settings['convective_velocity'] = None 211 | solver = ScalarTransportSolver(settings) 212 | solver.material['conductivity'] = K 213 | solver.material['emissivity'] = 0.9 214 | 215 | T = solver.solve() 216 | post_process(T) 217 | if interactively: 218 | solver.plot() 219 | 220 | def test(): 221 | #setup(using_anisotropic_conductivity = True, using_convective_velocity = False, using_DG_solver = False, using_HTC = False) 222 | #setup(using_anisotropic_conductivity = False, using_convective_velocity = False, using_DG_solver = False, using_HTC = True) 223 | #DG is not test here 224 | setup(using_anisotropic_conductivity = False, using_convective_velocity = True, using_DG_solver = True, using_HTC = True) 225 | 226 | if __name__ == '__main__': 227 | test() 228 | test_radiation() -------------------------------------------------------------------------------- /examples/test_large_deformation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # *************************************************************************** 3 | # * * 4 | # * Copyright (c) 2018 - Qingfeng Xia * 5 | # * * 6 | # * This program is free software; you can redistribute it and/or modify * 7 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 8 | # * as published by the Free Software Foundation; either version 2 of * 9 | # * the License, or (at your option) any later version. * 10 | # * for detail see the LICENCE text file. * 11 | # * * 12 | # * This program is distributed in the hope that it will be useful, * 13 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 14 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 15 | # * GNU Library General Public License for more details. * 16 | # * * 17 | # * You should have received a copy of the GNU Library General Public * 18 | # * License along with this program; if not, write to the Free Software * 19 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 20 | # * USA * 21 | # * * 22 | # *************************************************************************** 23 | 24 | from __future__ import print_function, division 25 | import math 26 | import numpy as np 27 | 28 | """ 29 | error: Error: Unable to create Dirichlet boundary condition. 30 | """ 31 | 32 | from config import is_interactive 33 | interactively = is_interactive() # manual set it False to debug solver 34 | 35 | from dolfin import * 36 | from FenicsSolver.LargeDeformationSolver import LargeDeformationSolver 37 | from FenicsSolver import SolverBase 38 | 39 | 40 | def solve_elasticity(using_2d, length, E, nu, dt, t_end, dirname): 41 | """Prepares 2D geometry. Returns facet function with 1, 2 on parts of the boundary.""" 42 | if using_2d: 43 | n = 4 44 | x0 = 0.0 45 | x1 = x0 + length 46 | y0 = 0.0 47 | y1 = 1.0 48 | mesh = RectangleMesh(Point(x0, y0), Point(x1, y1), int((x1-x0)*n), int((y1-y0)*n), 'crossed') 49 | gdim = 2 50 | bF_direction = Constant((0.0, 1.0)) 51 | else: 52 | mesh = Mesh('lego_beam.xml') 53 | gdim = mesh.geometry().dim() 54 | x0 = mesh.coordinates()[:, 0].min() 55 | x1 = mesh.coordinates()[:, 0].max() 56 | gdim = 3 57 | bF_direction = Constant((0.0, 0.0, 1.0)) 58 | 59 | left = AutoSubDomain(lambda x: near(x[0], x0)) 60 | right = AutoSubDomain(lambda x: near(x[0], x1)) 61 | 62 | from collections import OrderedDict 63 | bcs = OrderedDict() 64 | # not supported value type: V.sub(0).sub(0) 65 | bcs["fixed"] = {'boundary': left, 'boundary_id': 1, 'type': 'Dirichlet', 'variable': "displacement", \ 66 | 'value': gdim*(0.0, )} 67 | bcs["fixed_velocity"] = {'boundary': left, 'boundary_id': 1, 'type': 'Dirichlet', 'variable': "velocity", \ 68 | 'value': gdim*(0.0, )} 69 | bfunc = lambda t: 100*t # it should be a functon of time 70 | #bcs["displ"] = {'boundary': right, 'boundary_id': 2, 'type': 'pressure', 'value': bfunc, 'direction': bF_direction} 71 | bcs["stress_b"] = {'boundary': right, 'boundary_id': 2, 'type': 'force', 'value': (0, 5)} 72 | 73 | import copy 74 | s = copy.copy(SolverBase.default_case_settings) 75 | s['material'] = {'name': 'steel', 'elastic_modulus': E, 'poisson_ratio': nu, 'density': 1000, 76 | 'thermal_expansion_coefficient': 2e-6} # 77 | 78 | s['mesh'] = mesh 79 | s['boundary_conditions'] = bcs 80 | #s['temperature_distribution']=None 81 | #s['vector_name'] = ['displacement', 'velocity'] 82 | s['solver_settings'] = { 83 | 'transient_settings': {'transient': True, 'starting_time': 0, 'time_step': dt, 'ending_time': t_end}, 84 | 'reference_values': {'temperature': 293 }, 85 | } 86 | 87 | # solver specific setting 88 | # 89 | solver = LargeDeformationSolver(s) 90 | w = solver.solve() 91 | if interactively: 92 | solver.plot() 93 | 94 | if __name__ == '__main__': 95 | solve_elasticity(True, 20, 1e5, 0.3, 0.25, 5, 'results_2d_comp') 96 | solve_elasticity(True, 20, 1e5, 0.5, 0.25, 5, 'results_2d_incomp') 97 | #solve_elasticity(geometry_2d(80.0), 1e5, 0.3, 0.25, 5.0, 'results_2d_long_comp') 98 | #solve_elasticity(False, 20, 1e5, 0.3, 0.50, 5.0, 'results_3d_comp') 99 | -------------------------------------------------------------------------------- /examples/test_linear_elasticity.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2017 - Qingfeng Xia * * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | from __future__ import print_function, division 24 | import math 25 | import numpy as np 26 | 27 | from config import is_interactive 28 | interactively = is_interactive() # manual set it False to debug solver 29 | 30 | from dolfin import * 31 | set_log_level(ERROR) 32 | from FenicsSolver import LinearElasticitySolver 33 | from FenicsSolver import SolverBase 34 | 35 | def test(has_thermal_stress, has_body_source, transient = False, boundary_type = 1): 36 | #has_body_source to test gravity as body source 37 | # 38 | xmin, xmax = 0, 10 39 | ymin, ymax = 0, 1 40 | zmin, zmax = 0, 1 41 | nx,ny,nz = 40, 10, 10 42 | mesh = BoxMesh(Point(xmin, ymin, zmin), Point(xmax, ymax,zmax), nx,ny,nz) 43 | geo_dim = mesh.geometry().dim() 44 | 45 | #################################### 46 | # Left boundary 47 | class Left(SubDomain): 48 | def inside(self, x, on_boundary): 49 | return near(x[0], xmin) 50 | 51 | # Right boundary 52 | class Right(SubDomain): 53 | def inside(self, x, on_boundary): 54 | return near(x[0], xmax) 55 | 56 | top = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], ymax) ) 57 | bottom = AutoSubDomain(lambda x, on_boundary: on_boundary and near(x[1], ymin) ) 58 | # Point (8,0.5,0.5) at which load is added 59 | #DirichletBC(V, g_T, pt, method="pointwise")] 60 | class point(SubDomain): 61 | def inside(self, x, on_boundary): 62 | return near(x[0], xmax) and near(x[1],0.5,1e-2) and near(x[2],0.5,1e-2) 63 | 64 | omega = 100 # rad/s, axis, origin are other parameters 65 | rho = 7800 # kg/m3, density 66 | # body force: Loading due to centripetal acceleration (rho*omega^2*x_i) or gravity 67 | #bf = Expression(("rho*omega*omega*x[0]", "rho*omega*omega*x[1]", "0.0"), omega=omega, rho=rho, degree=2) 68 | bf = Expression(("10*rho", "0", "0.0"), omega=omega, rho=rho, degree=2) # gravity but in x-axis 69 | 70 | from collections import OrderedDict 71 | bcs = OrderedDict() 72 | # 73 | #bcs["fixed"] = {'boundary': Left(), 'boundary_id': 1, 'type': 'Dirichlet', 'value': Constant((0,0,0))} 74 | bcs["fixed"] = {'boundary': Left(), 'boundary_id': 1, 'type': 'Dirichlet', 'value': (Constant(0), None, None)} 75 | 76 | if boundary_type == 1: 77 | bcs["displ"] = {'boundary': Right(), 'boundary_id': 2, 'type': 'Dirichlet', 'value': Constant((0, 0, zmax*1e-3))} 78 | #bcs["displ"] = {'boundary': Right(), 'boundary_id': 2, 'type': 'Dirichlet', 'value': (Constant(0), None, None)} 79 | # constraint only one direction displacement 80 | #bcs["displ"] = {'boundary': Right(), 'boundary_id': 2, 'type': 'Dirichlet', 'value': (None, None, Constant(0.01))} 81 | elif boundary_type == 2: 82 | bcs["tensile"] = {'boundary': Right(), 'boundary_id': 2, 'type': 'stress', 'value': Constant((1e8,0, 0))} #correct, normal stress 83 | # force on boundary surface is converted into tangential and normal stress and apply onto boundary facets 84 | #bcs["pressing"] = {'boundary': top, 'boundary_id': 3, 'type': 'stress', 'value': Constant((0, 1e6, 0)), 'direction': None} 85 | elif boundary_type == 3: 86 | bcs["bending"] = {'boundary': Right(), 'boundary_id': 2, 'type': 'force', 'value': Constant((0, 1e6, 0))} # correct, shearing force 87 | else: 88 | raise NotImplementedError() 89 | 90 | # TODO: 91 | #bcs["sym"] = {'boundary': bottom, 'boundary_id': 4, 'type': 'symmetry', 'value': None} 92 | #bcs["antisym"] = {'boundary': bottom, 'boundary_id': 4, 'type': 'antisymmetry', 'value': None} 93 | 94 | # TODO: spring is an analog to heat transfer coefficient 95 | #bcs["spring"] = {'boundary': bottom, 'boundary_id': 4, 'type': 'spring', 96 | # 'K': Constant(), 'fixed_position': Point(xmax,4,(zmin+xmax)*0.5))} 97 | # for dynamic system, damping boundary 98 | 99 | dt = 0.001 100 | f =100 101 | t_end = 0.005 102 | transient_settings = {'transient': True, 'starting_time': 0.0, 'time_step': dt, 'ending_time': t_end} 103 | 104 | # Create function space 105 | fe_degree = 2 106 | V = VectorFunctionSpace(mesh, "Lagrange", fe_degree) 107 | #external Temperature distribution can be solved by the same function space, then pass to elastic solver 108 | 109 | import copy 110 | s = copy.copy(SolverBase.default_case_settings) # deep copy? 111 | s['material'] = {'name': 'steel', 'elastic_modulus': 2e11, 'poisson_ratio': 0.27, 'density': 7800, 112 | 'thermal_expansion_coefficient': 2e-6} #default to steel 113 | s['function_space'] = V 114 | s['boundary_conditions'] = bcs 115 | s['temperature_distribution']=None 116 | #s['vector_name'] = 'displacement' 117 | s['solver_settings']['reference_values'] = {'temperature':293 } # solver specific setting 118 | if transient: 119 | s['solver_settings']['transient_settings'] = transient_settings 120 | dynamic_stress = lambda t: Constant((1e8*math.sin(f*math.pi*2*t), 0, 0)) 121 | bcs["tensile"] = {'boundary': Right(), 'boundary_id': 2, 'type': 'stress', 'value': dynamic_stress} #correct, normal stress 122 | 123 | if has_thermal_stress: 124 | print('test thermal stress') 125 | s['temperature_distribution'] = Expression("343", degree=fe_degree) 126 | #Expression("dT * x[1]/ymax", dT = 100, ymax=ymax, degree=fe_degree) 127 | # need a better expression to test thermal. 128 | if has_body_source: 129 | s['body_source'] = bf 130 | solver = LinearElasticitySolver.LinearElasticitySolver(s) # body force test passed 131 | 132 | ''' 133 | X0 = FunctionSpace(mesh, "RT", 2) 134 | X = VectorFunctionSpace(mesh, "RT", 2) 135 | u = interpolate(Expression(('1', '2')), X0) 136 | project(grad(u), X) 137 | ''' 138 | 139 | #ev = solver.solve_modal() # need proper boundary condition to test? 140 | u = solver.solve() 141 | plot(solver.von_Mises(u), title='Stress von Mises') 142 | #if not run by pytest 143 | if interactively: 144 | solver.plot() 145 | 146 | ################################### 147 | if False: 148 | # Save solution to VTK format 149 | File("elastic_displacement.pvd", "compressed") << u 150 | 151 | # Save colored mesh partitions in VTK format if running in parallel 152 | if MPI.size(mesh.mpi_comm()) > 1: 153 | File("partitions.pvd") << CellFunction("size_t", mesh, \ 154 | MPI.rank(mesh.mpi_comm())) 155 | 156 | # Project and write stress field to post-processing file 157 | W = TensorFunctionSpace(mesh, "Discontinuous Lagrange", 0) 158 | stress = project(solver.sigma(u), V=W) 159 | File("stress.pvd") << stress 160 | ##################################### 161 | 162 | if __name__ == '__main__': 163 | #test(has_thermal_stress = True, has_body_source=True, transient = False, boundary_type =2) 164 | test(has_thermal_stress = True, has_body_source=True, transient = True) 165 | test(has_thermal_stress = True, has_body_source=True) 166 | test(has_thermal_stress = False, has_body_source=True) 167 | test(has_thermal_stress = True, has_body_source=False) 168 | test(has_thermal_stress = False, has_body_source=False) #failed! Error: Unable to creating dolfin.Form. -------------------------------------------------------------------------------- /examples/test_nonlinear_elasticity.py: -------------------------------------------------------------------------------- 1 | # *************************************************************************** 2 | # * * 3 | # * Copyright (c) 2017 - Qingfeng Xia * * 4 | # * * 5 | # * This program is free software; you can redistribute it and/or modify * 6 | # * it under the terms of the GNU Lesser General Public License (LGPL) * 7 | # * as published by the Free Software Foundation; either version 2 of * 8 | # * the License, or (at your option) any later version. * 9 | # * for detail see the LICENCE text file. * 10 | # * * 11 | # * This program is distributed in the hope that it will be useful, * 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of * 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 14 | # * GNU Library General Public License for more details. * 15 | # * * 16 | # * You should have received a copy of the GNU Library General Public * 17 | # * License along with this program; if not, write to the Free Software * 18 | # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * 19 | # * USA * 20 | # * * 21 | # *************************************************************************** 22 | 23 | from __future__ import print_function, division 24 | import math 25 | import numpy as np 26 | 27 | from config import is_interactive 28 | interactively = is_interactive() # manual set it False to debug solver 29 | 30 | from dolfin import * 31 | from FenicsSolver import LinearElasticitySolver 32 | from FenicsSolver import NonlinearElasticitySolver 33 | from FenicsSolver import SolverBase 34 | 35 | set_log_level(ERROR) 36 | 37 | def test(): 38 | 39 | # Create mesh and define function space 40 | mesh = UnitCubeMesh(24, 16, 16) 41 | V = VectorFunctionSpace(mesh, "Lagrange", 1) 42 | 43 | B = Constant((0.0, -0.5, 0.0)) # Body force per unit volume 44 | T = Constant((0.1, 0.0, 0.0)) # Traction force on the boundary, for all? 45 | 46 | # Mark boundary subdomians 47 | left = CompiledSubDomain("near(x[0], side) && on_boundary", side = 0.0) 48 | right = CompiledSubDomain("near(x[0], side) && on_boundary", side = 1.0) 49 | # Define Dirichlet boundary (x = 0 or x = 1) 50 | c = Constant((0.0, 0.0, 0.0)) 51 | r = Expression(("scale*0.0", 52 | "scale*(y0 + (x[1] - y0)*cos(theta) - (x[2] - z0)*sin(theta) - x[1])", 53 | "scale*(z0 + (x[1] - y0)*sin(theta) + (x[2] - z0)*cos(theta) - x[2])"), 54 | scale = 0.5, y0 = 0.5, z0 = 0.5, theta = pi/3, degree=2) 55 | 56 | bcl = DirichletBC(V, c, left) 57 | bcr = DirichletBC(V, r, right) 58 | 59 | from collections import OrderedDict 60 | bcs = OrderedDict() 61 | # 62 | bcs["left"] = {'boundary': left, 'boundary_id': 1, 'type': 'Dirichlet', 'value': c} 63 | bcs["right"] = {'boundary': right, 'boundary_id': 2, 'type': 'Dirichlet', 'value': r} 64 | 65 | import copy 66 | s = copy.copy(SolverBase.default_case_settings) # deep copy? 67 | s['material'] = {'name': 'rubber', 'elastic_modulus': 10, 'poisson_ratio': 0.3, 68 | 'density': 800, 'thermal_expansion_coefficient': 2e-6} # fixme, unknown properties value 69 | s['function_space'] = V 70 | s['boundary_conditions'] = bcs 71 | s['body_source'] = B 72 | s['surface_source'] = {'value': Constant(0.1), 'direction': Constant((1, 0.0, 0.0)) } #T # apply to all boundaries, 73 | solver = NonlinearElasticitySolver.NonlinearElasticitySolver(s) # body force test passed 74 | #lsolver = LinearElasticitySolver.LinearElasticitySolver(s) 75 | #lu = lsolver.solve() 76 | 77 | u = solver.solve() 78 | ## Plot solution of displacement 79 | # Plot stress, fixme: von_Mises may not apply 80 | #plot(solver.von_Mises(u), title='Stress von Mises') 81 | 82 | #if not run by pytest 83 | if interactively: 84 | solver.plot() 85 | 86 | if __name__ == '__main__': 87 | test() -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | uploading to pypi is delayed until API is stable 3 | https://packaging.python.org/tutorials/distributing-packages/#setup-py 4 | #python setup.py bdist_wheel --universal 5 | """ 6 | 7 | # Always prefer setuptools over distutils 8 | from setuptools import setup, find_packages 9 | # To use a consistent encoding 10 | from codecs import open 11 | import os 12 | from os import path 13 | 14 | here = path.abspath(path.dirname(__file__)) 15 | 16 | # Get the long description from the README file 17 | with open(path.join(here, 'Readme.md'), encoding='utf-8') as f: 18 | long_description = f.read() 19 | 20 | setup( 21 | name='FenicsSolver', 22 | 23 | # Versions should comply with PEP440. For a discussion on single-sourcing 24 | # the version across setup.py and the project code, see 25 | # https://packaging.python.org/en/latest/single_source_version.html 26 | version='0.1.0', 27 | 28 | description='A multi-physics FEA solver based on Fenics', 29 | long_description=long_description, 30 | 31 | # The project's main homepage. 32 | url='https://github.com/qingfengxia/FenicsSolver', 33 | 34 | # Author details 35 | author='Qingfeng Xia', 36 | author_email='qingfeng.xia@eng-ox-ac-uk', 37 | 38 | # Choose your license 39 | license='LGPL', 40 | 41 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 42 | classifiers=[ 43 | # How mature is this project? Common values are 44 | # 3 - Alpha 45 | # 4 - Beta 46 | # 5 - Production/Stable 47 | 'Development Status :: 3 - Alpha', 48 | 49 | # Indicate who your project is intended for 50 | 'Intended Audience :: Science\Research', 51 | 'Topic :: Scientific/Engineering :: Physics', 52 | 'Environment :: Console' 53 | 54 | # Pick your license as you wish (should match "license" above) 55 | 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 56 | 57 | # Specify the Python versions you support here. In particular, ensure 58 | # that you indicate whether you support Python 2, Python 3 or both. 59 | 'Programming Language :: Python :: 2', 60 | 'Programming Language :: Python :: 2.7', 61 | 'Programming Language :: Python :: 3', 62 | ], 63 | 64 | # What does your project relate to? 65 | keywords=['Dolfin', 'Fenics', 'FEA', 'FEM', 'CFD'], 66 | 67 | # You can just specify the packages manually here if your project is simple. Or you can use find_packages(). 68 | # there must be a subfolder in the git repo root 69 | packages=find_packages(where='.', exclude=['contrib', 'docs', 'tests']), 70 | # Alternatively, if you want to distribute just a my_module.py, uncomment this: 71 | 72 | # List run-time dependencies here. These will be installed by pip when 73 | # your project is installed. For an analysis of "install_requires" vs pip's 74 | # requirements files see: 75 | # https://packaging.python.org/en/latest/requirements.html 76 | #install_requires=['fenics', 'numpy', 'matplotlib'], # let user to install a proper Fenics version 77 | install_requires=[], 78 | 79 | # List additional groups of dependencies here (e.g. development 80 | # dependencies). You can install these using the following syntax, 81 | # for example: 82 | # $ pip install -e .[dev,test] 83 | #extras_require={ 84 | # 'dev': [], # dolfin requires a lot of packages, this package does not introduce any new dependencies 85 | # 'test': ['unittest'], 86 | #}, 87 | 88 | # If there are data files included in your packages that need to be 89 | # installed, specify them here. If using Python 2.6 or less, then these 90 | # have to be included in MANIFEST.in as well. 91 | package_data={ 92 | #'sample': ['package_data.dat'], # todo later for testing data 93 | }, 94 | 95 | # Although 'package_data' is the preferred approach, in some case you may 96 | # need to place data files outside of your packages. See: 97 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 98 | # In this case, 'data_file' will be installed into '/my_data' 99 | data_files=[('readme', ['Readme.md', 'FenicsSolver_FreeCAD.png'])], 100 | 101 | # To provide executable scripts, use entry points in preference to the 102 | # "scripts" keyword. Entry points provide cross-platform support and allow 103 | # pip to create the appropriate form of executable for the target platform. 104 | entry_points={ 105 | 'console_scripts': [ 106 | 'main=main.py', 107 | ], 108 | }, 109 | ) 110 | --------------------------------------------------------------------------------