From 822cb19f87b91a104b88fb482a3e2ba28b00ea47 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 26 Feb 2024 08:37:16 +0100 Subject: [PATCH 01/40] Create tutorial structure --- muscle-tendon-complex/README.md | 120 ++++++++ muscle-tendon-complex/clean-tutorial.sh | 1 + ...s-multiple-perpendicular-flaps-results.png | Bin 0 -> 89727 bytes .../tutorials-muscle-tendon-complex-setup.png | Bin 0 -> 34433 bytes muscle-tendon-complex/precice-config.xml | 266 ++++++++++++++++++ 5 files changed, 387 insertions(+) create mode 100644 muscle-tendon-complex/README.md create mode 120000 muscle-tendon-complex/clean-tutorial.sh create mode 100644 muscle-tendon-complex/images/tutorials-multiple-perpendicular-flaps-results.png create mode 100644 muscle-tendon-complex/images/tutorials-muscle-tendon-complex-setup.png create mode 100644 muscle-tendon-complex/precice-config.xml diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md new file mode 100644 index 000000000..857ffea61 --- /dev/null +++ b/muscle-tendon-complex/README.md @@ -0,0 +1,120 @@ +--- +title: Muscle-tendon complex +permalink: tutorials-muscle-tendon-complex.html +keywords: multi-coupling, OpenDiHu, deal.II, skeletal muscle +summary: In this case, an skeletal muscle (biceps) and three tendons are coupled together using a fully-implicit multi-coupling scheme. +--- + +{% note %} +Get the [case files of this tutorial](https://github.com/precice/tutorials/tree/master/muscle-tendon-complex). Read how in the [tutorials introduction](https://www.precice.org/tutorials.html). +{% endnote %} + +## Case Setup + +In the following tutorial we model the contraction of a muscle, in particular, the biceps. The biceps is attached to the bones by three tendons (one at the bottom and two at the top). We enforce an activation in the muscle which results in its contraction. The tendons move as a result of the muscle contraction. In this case, a muscle and three tendons are coupled together using a fully-implicit multi-coupling scheme. The case setup is shown here: + +![Setup](images/tutorials-muscle-tendon-complex-setup.png) + +The muscle participant (in red), is connected to three tendons. The muscle sends traction values to the tendons, which send displacement and velocity values back to the muscle. The end of each tendon which is not attached to the muscle is fixed by a dirichlet boundary condition (in reality, it would be fixed to the bones). + +The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not match perfectly. + + +TODO: how is the muscle activated! + +TODO: Add related case? multiple-perpendicular flap? + +## Why multi-coupling? + +This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant to participant manner. However, such a composition is not suited for combining multiple strong fluid-structure interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. + +We can set this in our `precice-config.xml`: + +```xml + + + + + + +``` + +The participant that has the control is the one that it is connected to all other participants. This is why we have chosen the muscle participant for this task. + +## About the Solvers + +For the fluid participant we use OpenFOAM. In particular, we use the application `pimpleFoam`. The geometry of the Fluid participant is defined in the file `Fluid/system/blockMeshDict`. Besides, we must specify where are we exchanging data with the other participants. The interfaces are set in the file `Fluid/system/preciceDict`. In this file, we set to exchange stress and displacement on the surface of each flap. + +Most of the coupling details are specified in the file `precice-config.xml`. Here we estipulate the order in which we read/write data from one participant to another or how we map from the fluid to the solid's mesh. In particular, we have chosen the nearest-neighbor mapping scheme. + +For the simulation of the solid participants we use the deal.II adapter. In deal.II, the geometry of the domain is specified directly on the solver. The two flaps in our case are essentially the same but for the x-coordinate. The flap geometry is given to the solver when we select the scenario in the '.prm' file. + +```text +set Scenario = PF +``` + +But to specify the position of the flap along the x-axis, we must specify it in the `solid-upstream-dealii/parameters.prm` file as follows: + +```text +set Flap location = -1.0 +``` + +While in case of `solid-downstream-dealii/parameters.prm` we write: + +```text +set Flap location = 1.0 +``` + +The scenario settings are implemented similarly for the nonlinear case. + +## Running the Simulation + +1. Preparation: + To run the coupled simulation, copy the deal.II executable `elasticity` into the main folder. To learn how to obtain the deal.II executable take a look at the description on the [deal.II-adapter page](https://www.precice.org/adapter-dealii-overview.html). +2. Starting: + + We are going to run each solver in a different terminal. It is important that first we navigate to the simulation directory so that all solvers start in the same directory. + To start the `Fluid` participant, run: + + ```bash + cd fluid-openfoam + ./run.sh + ``` + + to start OpenFOAM in serial or + + ```bash + cd fluid-openfoam + ./run.sh -parallel + ``` + + for a parallel run. + + The solid participants are only designed for serial runs. To run the `Solid-Upstream` participant, execute the corresponding deal.II binary file e.g. by: + + ```bash + cd solid-upstream-dealii + ./run.sh + ``` + + Finally, in the third terminal we will run the solver for the `Solid-Downstream` participant by: + + ```bash + cd solid-downstream-dealii + ./run.sh + ``` + +## Postprocessing + +After the simulation has finished, you can visualize your results using e.g. ParaView. Fluid results are in the OpenFOAM format and you may load the `fluid-openfoam.foam` file. Looking at the fluid results is enough to obtain information about the behaviour of the flaps. You can also visualize the solid participants' vtks though. + +![Example visualization](images/tutorials-multiple-perpendicular-flaps-results.png) + +## References + + +[1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. _Comput Mech_ **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 + +{% disclaimer %} +This offering is not approved or endorsed by OpenCFD Limited, producer and distributor of the OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and OpenCFD® trade marks. +{% enddisclaimer %} diff --git a/muscle-tendon-complex/clean-tutorial.sh b/muscle-tendon-complex/clean-tutorial.sh new file mode 120000 index 000000000..4713f5092 --- /dev/null +++ b/muscle-tendon-complex/clean-tutorial.sh @@ -0,0 +1 @@ +../tools/clean-tutorial-base.sh \ No newline at end of file diff --git a/muscle-tendon-complex/images/tutorials-multiple-perpendicular-flaps-results.png b/muscle-tendon-complex/images/tutorials-multiple-perpendicular-flaps-results.png new file mode 100644 index 0000000000000000000000000000000000000000..aedc406dba2d6e1f2436416193829ffbab82dd6b GIT binary patch literal 89727 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

Cw>WJAZS@23c@-#LoNk?#+ir!QV3d_vTNx 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

NeEiXyM!!^}GenYz`tKD8^n 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 CHQXNn literal 0 HcmV?d00001 diff --git a/muscle-tendon-complex/images/tutorials-muscle-tendon-complex-setup.png b/muscle-tendon-complex/images/tutorials-muscle-tendon-complex-setup.png new file mode 100644 index 0000000000000000000000000000000000000000..d744e1f2488080dee730ccddccc347c2d5f0ea98 GIT binary patch literal 34433 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 RR~AChJymUFk+RM6{{tGW^)&zh literal 0 HcmV?d00001 diff --git a/muscle-tendon-complex/precice-config.xml b/muscle-tendon-complex/precice-config.xml new file mode 100644 index 000000000..0e2871fe6 --- /dev/null +++ b/muscle-tendon-complex/precice-config.xml @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From cc8b43cf875c7fd74be0ca12cb4cb366bf247404 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 26 Feb 2024 08:56:42 +0100 Subject: [PATCH 02/40] Add sample structure for an opendihu participant --- muscle-tendon-complex/README.md | 8 +- .../muscle-opendihu/SConstruct | 18 + .../muscle-opendihu/Sconscript | 14 + .../muscle-opendihu/settings-muscle.py | 562 ++++++++++++++++++ .../src/muscle_electrophysiology_precice.cpp | 47 ++ .../muscle-opendihu/variables/variables.py | 159 +++++ 6 files changed, 804 insertions(+), 4 deletions(-) create mode 100644 muscle-tendon-complex/muscle-opendihu/SConstruct create mode 100644 muscle-tendon-complex/muscle-opendihu/Sconscript create mode 100644 muscle-tendon-complex/muscle-opendihu/settings-muscle.py create mode 100644 muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp create mode 100644 muscle-tendon-complex/muscle-opendihu/variables/variables.py diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 857ffea61..77dc5090e 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -41,7 +41,7 @@ We can set this in our `precice-config.xml`: The participant that has the control is the one that it is connected to all other participants. This is why we have chosen the muscle participant for this task. -## About the Solvers +## About the Solvers TODO For the fluid participant we use OpenFOAM. In particular, we use the application `pimpleFoam`. The geometry of the Fluid participant is defined in the file `Fluid/system/blockMeshDict`. Besides, we must specify where are we exchanging data with the other participants. The interfaces are set in the file `Fluid/system/preciceDict`. In this file, we set to exchange stress and displacement on the surface of each flap. @@ -67,7 +67,7 @@ set Flap location = 1.0 The scenario settings are implemented similarly for the nonlinear case. -## Running the Simulation +## Running the Simulation TODO 1. Preparation: To run the coupled simulation, copy the deal.II executable `elasticity` into the main folder. To learn how to obtain the deal.II executable take a look at the description on the [deal.II-adapter page](https://www.precice.org/adapter-dealii-overview.html). @@ -104,13 +104,13 @@ The scenario settings are implemented similarly for the nonlinear case. ./run.sh ``` -## Postprocessing +## Postprocessing TODO After the simulation has finished, you can visualize your results using e.g. ParaView. Fluid results are in the OpenFOAM format and you may load the `fluid-openfoam.foam` file. Looking at the fluid results is enough to obtain information about the behaviour of the flaps. You can also visualize the solid participants' vtks though. ![Example visualization](images/tutorials-multiple-perpendicular-flaps-results.png) -## References +## References TODO [1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. _Comput Mech_ **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 diff --git a/muscle-tendon-complex/muscle-opendihu/SConstruct b/muscle-tendon-complex/muscle-opendihu/SConstruct new file mode 100644 index 000000000..e416fac61 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/SConstruct @@ -0,0 +1,18 @@ +# SConstruct file for a single example. +# +# Usage: `scons BUILD_TYPE=debug` will build debug version, `scons` will build release version. + +# Call the generic `SConstructGeneral` script that will configure everything. It is located at the top level directory of opendihu. +# That script will then call a `SConscript` file that defines which sources to use. + +import os + +# get the directory where opendihu is installed (the top level directory of opendihu) +opendihu_home = os.environ.get('OPENDIHU_HOME') or "../../../../.." + +# set path where the "SConscript" file is located (set to current path) +path_where_to_call_sconscript = Dir('.').srcnode().abspath + +# call general SConstruct that will configure everything and then call SConscript at the given path +SConscript(os.path.join(opendihu_home,'SConstructGeneral'), + exports={"path": path_where_to_call_sconscript}) \ No newline at end of file diff --git a/muscle-tendon-complex/muscle-opendihu/Sconscript b/muscle-tendon-complex/muscle-opendihu/Sconscript new file mode 100644 index 000000000..aa29b9b32 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/Sconscript @@ -0,0 +1,14 @@ +# This script declares to SCons how to compile the example. +# It has to be called from a SConstruct file. +# The 'env' object is passed from there and contains further specification like directory and debug/release flags. +# +# Note: If you're creating a new example and copied this file, adjust the desired name of the executable in the 'target' parameter of env.Program. + + +Import('env') # import Environment object from calling SConstruct + +# if the option no_tests was given, quit the script +if not env['no_examples']: + + # create the main executable + env.Program(target = 'muscle_electrophysiology_precice', source = "src/muscle_electrophysiology_precice.cpp") diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py new file mode 100644 index 000000000..ff4b37d04 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -0,0 +1,562 @@ +# Multiple 1D fibers (monodomain) with 3D dynamic mooney rivlin with active contraction term, on biceps geometry + +import sys, os +import timeit +import argparse +import importlib +import distutils.util + +# set title of terminal +title = "muscle" +print('\33]0;{}\a'.format(title), end='', flush=True) + +# parse rank arguments +rank_no = (int)(sys.argv[-2]) +n_ranks = (int)(sys.argv[-1]) + +# add variables subfolder to python path where the variables script is located +script_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_path) +sys.path.insert(0, os.path.join(script_path,'variables')) + +import variables # file variables.py, defined default values for all parameters, you can set the parameters there +from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain + +# if first argument contains "*.py", it is a custom variable definition file, load these values +if ".py" in sys.argv[0]: + variables_path_and_filename = sys.argv[0] + variables_path,variables_filename = os.path.split(variables_path_and_filename) # get path and filename + sys.path.insert(0, os.path.join(script_path,variables_path)) # add the directory of the variables file to python path + variables_module,_ = os.path.splitext(variables_filename) # remove the ".py" extension to get the name of the module + + if rank_no == 0: + print("Loading variables from \"{}\".".format(variables_path_and_filename)) + + custom_variables = importlib.import_module(variables_module, package=variables_filename) # import variables module + variables.__dict__.update(custom_variables.__dict__) + sys.argv = sys.argv[1:] # remove first argument, which now has already been parsed +else: + if rank_no == 0: + print("Error: no variables file was specified, e.g:\n ./biceps_contraction ../settings_biceps_contraction.py ramp.py") + exit(0) + +# -------------- begin user parameters ---------------- +variables.output_timestep_3D = 50 #[ms] output timestep of mechanics +variables.output_timestep_fibers = 50 # [ms] output timestep of fibers +# -------------- end user parameters ---------------- + +# define command line arguments +mbool = lambda x:bool(distutils.util.strtobool(x)) # function to parse bool arguments +parser = argparse.ArgumentParser(description='muscle') +parser.add_argument('--scenario_name', help='The name to identify this run in the log.', default=variables.scenario_name) +parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) +parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) +parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) +parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) +parser.add_argument('--diffusion_solver_type', help='The solver for the diffusion.', default=variables.diffusion_solver_type, choices=["gmres","cg","lu","gamg","richardson","chebyshev","cholesky","jacobi","sor","preonly"]) +parser.add_argument('--diffusion_preconditioner_type', help='The preconditioner for the diffusion.', default=variables.diffusion_preconditioner_type, choices=["jacobi","sor","lu","ilu","gamg","none"]) +parser.add_argument('--potential_flow_solver_type', help='The solver for the potential flow (non-spd matrix).', default=variables.potential_flow_solver_type, choices=["gmres","cg","lu","gamg","richardson","chebyshev","cholesky","jacobi","sor","preonly"]) +parser.add_argument('--potential_flow_preconditioner_type', help='The preconditioner for the potential flow.', default=variables.potential_flow_preconditioner_type, choices=["jacobi","sor","lu","ilu","gamg","none"]) +parser.add_argument('--paraview_output', help='Enable the paraview output writer.', default=variables.paraview_output, action='store_true') +parser.add_argument('--adios_output', help='Enable the MegaMol/ADIOS output writer.', default=variables.adios_output, action='store_true') +parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) +parser.add_argument('--fiber_distribution_file', help='The filename of the file that contains the MU firing times.', default=variables.fiber_distribution_file) +parser.add_argument('--firing_times_file', help='The filename of the file that contains the cellml model.', default=variables.firing_times_file) +parser.add_argument('--end_time', '--tend', '-t', help='The end simulation time.', type=float, default=variables.end_time) +parser.add_argument('--output_timestep', help='The timestep for writing outputs.', type=float, default=variables.output_timestep) +parser.add_argument('--dt_0D', help='The timestep for the 0D model.', type=float, default=variables.dt_0D) +parser.add_argument('--dt_1D', help='The timestep for the 1D model.', type=float, default=variables.dt_1D) +parser.add_argument('--dt_splitting', help='The timestep for the splitting.', type=float, default=variables.dt_splitting) +parser.add_argument('--dt_3D', help='The timestep for the 3D model, i.e. dynamic solid mechanics.', type=float, default=variables.dt_3D) +parser.add_argument('--disable_firing_output', help='Disables the initial list of fiber firings.', default=variables.disable_firing_output, action='store_true') +parser.add_argument('--enable_coupling', help='Enables the precice coupling.', type=mbool, default=variables.enable_coupling) +parser.add_argument('--v', help='Enable full verbosity in c++ code') +parser.add_argument('-v', help='Enable verbosity level in c++ code', action="store_true") +parser.add_argument('-vmodule', help='Enable verbosity level for given file in c++ code') +parser.add_argument('-pause', help='Stop at parallel debugging barrier', action="store_true") + +# parse command line arguments and assign values to variables module +args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) +if len(other_args) != 0 and rank_no == 0: + print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) + +# initialize some dependend variables +if variables.n_subdomains is not None: + variables.n_subdomains_x = variables.n_subdomains[0] + variables.n_subdomains_y = variables.n_subdomains[1] + variables.n_subdomains_z = variables.n_subdomains[2] + +variables.n_subdomains = variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z + +# automatically initialize partitioning if it has not been set +if n_ranks != variables.n_subdomains: + + # create all possible partitionings to the given number of ranks + optimal_value = n_ranks**(1/3) + possible_partitionings = [] + for i in range(1,n_ranks+1): + for j in range(1,n_ranks+1): + if i*j <= n_ranks and n_ranks % (i*j) == 0: + k = (int)(n_ranks / (i*j)) + performance = (k-optimal_value)**2 + (j-optimal_value)**2 + 1.1*(i-optimal_value)**2 + possible_partitionings.append([i,j,k,performance]) + + # if no possible partitioning was found + if len(possible_partitionings) == 0: + if rank_no == 0: + print("\n\n\033[0;31mError! Number of ranks {} does not match given partitioning {} x {} x {} = {} and no automatic partitioning could be done.\n\n\033[0m".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) + quit() + + # select the partitioning with the lowest value of performance which is the best + lowest_performance = possible_partitionings[0][3]+1 + for i in range(len(possible_partitionings)): + if possible_partitionings[i][3] < lowest_performance: + lowest_performance = possible_partitionings[i][3] + variables.n_subdomains_x = possible_partitionings[i][0] + variables.n_subdomains_y = possible_partitionings[i][1] + variables.n_subdomains_z = possible_partitionings[i][2] + +# output information of run +if rank_no == 0: + print("scenario_name: {}, n_subdomains: {} {} {}, n_ranks: {}, end_time: {}".format(variables.scenario_name, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, n_ranks, variables.end_time)) + print("dt_0D: {:0.0e}, diffusion_solver_type: {}".format(variables.dt_0D, variables.diffusion_solver_type)) + print("dt_1D: {:0.0e}, potential_flow_solver_type: {}".format(variables.dt_1D, variables.potential_flow_solver_type)) + print("dt_splitting: {:0.0e}, emg_solver_type: {}, emg_initial_guess_nonzero: {}".format(variables.dt_splitting, variables.emg_solver_type, variables.emg_initial_guess_nonzero)) + print("dt_3D: {:0.0e}, paraview_output: {}".format(variables.dt_3D, variables.paraview_output)) + print("output_timestep: {:0.0e} stimulation_frequency: {} 1/ms = {} Hz".format(variables.output_timestep, variables.stimulation_frequency, variables.stimulation_frequency*1e3)) + print("fiber_file: {}".format(variables.fiber_file)) + print("cellml_file: {}".format(variables.cellml_file)) + print("fiber_distribution_file: {}".format(variables.fiber_distribution_file)) + print("firing_times_file: {}".format(variables.firing_times_file)) + print("********************************************************************************") + + print("prefactor: sigma_eff/(Am*Cm) = {} = {} / ({}*{})".format(variables.Conductivity/(variables.Am*variables.Cm), variables.Conductivity, variables.Am, variables.Cm)) + + # start timer to measure duration of parsing of this script + t_start_script = timeit.default_timer() + +# initialize all helper variables +from helper import * + +variables.n_subdomains_xy = variables.n_subdomains_x * variables.n_subdomains_y +variables.n_fibers_total = variables.n_fibers_x * variables.n_fibers_y + +if False: + for subdomain_coordinate_y in range(variables.n_subdomains_y): + for subdomain_coordinate_x in range(variables.n_subdomains_x): + + print("subdomain (x{},y{}) ranks: {} n fibers in subdomain: x{},y{}".format(subdomain_coordinate_x, subdomain_coordinate_y, + list(range(subdomain_coordinate_y*variables.n_subdomains_x + subdomain_coordinate_x, n_ranks, variables.n_subdomains_x*variables.n_subdomains_y)), + n_fibers_in_subdomain_x(subdomain_coordinate_x), n_fibers_in_subdomain_y(subdomain_coordinate_y))) + + for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)): + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)): + print("({},{}) n instances: {}".format(fiber_in_subdomain_coordinate_x,fiber_in_subdomain_coordinate_y, + n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y))) + + +# define the config dict +config = { + "scenarioName": variables.scenario_name, + "logFormat": "csv", + "solverStructureDiagramFile": "out/muscle_solver_structure.txt", # output file of a diagram that shows data connection between solvers + "mappingsBetweenMeshesLogFile": "out/muscle_mappings_between_meshes.txt", # log file of when mappings between meshes occur + "Meshes": variables.meshes, + "MappingsBetweenMeshes": variables.mappings_between_meshes, + "Solvers": { + "diffusionTermSolver": {# solver for the implicit timestepping scheme of the diffusion time step + "maxIterations": 1e4, + "relativeTolerance": 1e-10, + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual + "solverType": variables.diffusion_solver_type, + "preconditionerType": variables.diffusion_preconditioner_type, + "dumpFilename": "", # "out/dump_" + "dumpFormat": "matlab", + }, + "potentialFlowSolver": {# solver for the initial potential flow, that is needed to estimate fiber directions for the bidomain equation + "relativeTolerance": 1e-10, + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual + "maxIterations": 1e4, + "solverType": variables.potential_flow_solver_type, + "preconditionerType": variables.potential_flow_preconditioner_type, + "dumpFilename": "", + "dumpFormat": "matlab", + }, + "mechanicsSolver": { # solver for the dynamic mechanics problem + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 140, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-5, # relative tolerance of the nonlinear solver + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesRebuildJacobianFrequency": 3, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again + "dumpFilename": "", # dump system matrix and right hand side after every solve + "dumpFormat": "matlab", # default, ascii, matlab + } + }, + "PreciceAdapter": { # precice adapter for muscle + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + "couplingEnabled": variables.enable_coupling, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file + "preciceParticipantName": "MuscleSolver", # name of the own precice participant, has to match the name given in the precice xml config file + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "meshName": "MuscleMeshBottom", # precice name of the 2D coupling mesh + "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + }, + { + "meshName": "MuscleMeshTopA", # precice name of the 2D coupling mesh + "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + }, + { + "meshName": "MuscleMeshTopB", # precice name of the 2D coupling mesh + "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + }, + ], + "preciceData": [ + { + "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "MuscleMeshBottom", # name of the precice coupling surface mesh, as given in the precice xml settings file + "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "MuscleMeshTopA", # name of the precice coupling surface mesh, as given in the precice xml settings file + "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "MuscleMeshTopB", # name of the precice coupling surface mesh, as given in the precice xml settings file + "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "MuscleMeshBottom", # name of the precice coupling surface mesh, as given in the precice xml settings + "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "MuscleMeshTopA", # name of the precice coupling surface mesh, as given in the precice xml settings + "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "MuscleMeshTopB", # name of the precice coupling surface mesh, as given in the precice xml settings + "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file + }, + ], + + "Coupling": { + "timeStepWidth": variables.dt_3D, # 1e-1 + "logTimeStepWidthAsKey": "dt_3D", + "durationLogKey": "duration_total", + "timeStepOutputInterval": 1, + "endTime": variables.end_time, + "connectedSlotsTerm1To2": {1:2}, # transfer gamma to MuscleContractionSolver, the receiving slots are λ, λdot, γ + "connectedSlotsTerm2To1": None, # transfer nothing back + "Term1": { # monodomain, fibers + "MultipleInstances": { + "logKey": "duration_subdomains_xy", + "ranksAllComputedInstances": list(range(n_ranks)), + "nInstances": variables.n_subdomains_xy, + "instances": + [{ + "ranks": list(range(subdomain_coordinate_y*variables.n_subdomains_x + subdomain_coordinate_x, n_ranks, variables.n_subdomains_x*variables.n_subdomains_y)), + + # this is for the actual model with fibers + "StrangSplitting": { + #"numberTimeSteps": 1, + "timeStepWidth": variables.dt_splitting, # 1e-1 + "logTimeStepWidthAsKey": "dt_splitting", + "durationLogKey": "duration_monodomain", + "timeStepOutputInterval": 100, + "endTime": variables.dt_splitting, + "connectedSlotsTerm1To2": [0,1,2], # transfer slot 0 = state Vm from Term1 (CellML) to Term2 (Diffusion) + "connectedSlotsTerm2To1": [0,None,2], # transfer the same back, this avoids data copy + + "Term1": { # CellML, i.e. reaction term of Monodomain equation + "MultipleInstances": { + "logKey": "duration_subdomains_z", + "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y), + "instances": + [{ + "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction + "Heun" : { + "timeStepWidth": variables.dt_0D, # timestep width of 0D problem + "logTimeStepWidthAsKey": "dt_0D", # key under which the time step width will be written to the log file + "durationLogKey": "duration_0D", # log key of duration for this solver + "timeStepOutputInterval": 1e4, # how often to print the current timestep + "initialValues": [], # no initial values are specified + "dirichletBoundaryConditions": {}, # no Dirichlet boundary conditions are specified + "dirichletOutputFilename": None, # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable + "inputMeshIsGlobal": True, # the boundary conditions and initial values would be given as global numbers + "checkForNanInf": True, # abort execution if the solution contains nan or inf values + "nAdditionalFieldVariables": 0, # number of additional field variables + "additionalSlotNames": "", + + "CellML" : { + "modelFilename": variables.cellml_file, # input C++ source file or cellml XML file + #"statesInitialValues": [], # if given, the initial values for the the states of one instance + "statesInitialValues": variables.states_initial_values, # initial values for new_slow_TK + "initializeStatesToEquilibrium": False, # if the equilibrium values of the states should be computed before the simulation starts + "initializeStatesToEquilibriumTimestepWidth": 1e-4, # if initializeStatesToEquilibrium is enable, the timestep width to use to solve the equilibrium equation + + # optimization parameters + "optimizationType": "vc", # "vc", "simd", "openmp" type of generated optimizated source file + "approximateExponentialFunction": True, # if optimizationType is "vc", whether the exponential function exp(x) should be approximate by (1+x/n)^n with n=1024 + "compilerFlags": "-fPIC -O3 -march=native -Wno-deprecated-declarations -shared ", # compiler flags used to compile the optimized model code + "maximumNumberOfThreads": 0, # if optimizationType is "openmp", the maximum number of threads to use. Default value 0 means no restriction. + + # stimulation callbacks + #"libraryFilename": "cellml_simd_lib.so", # compiled library + #"setSpecificParametersFunction": set_specific_parameters, # callback function that sets parameters like stimulation current + #"setSpecificParametersCallInterval": int(1./variables.stimulation_frequency/variables.dt_0D), # set_specific_parameters should be called every 0.1, 5e-5 * 1e3 = 5e-2 = 0.05 + "setSpecificStatesFunction": set_specific_states, # callback function that sets states like Vm, activation can be implemented by using this method and directly setting Vm values, or by using setParameters/setSpecificParameters + #"setSpecificStatesCallInterval": 2*int(1./variables.stimulation_frequency/variables.dt_0D), # set_specific_states should be called variables.stimulation_frequency times per ms, the factor 2 is needed because every Heun step includes two calls to rhs + "setSpecificStatesCallInterval": 0, # 0 means disabled + "setSpecificStatesCallFrequency": variables.get_specific_states_call_frequency(fiber_no, motor_unit_no), # set_specific_states should be called variables.stimulation_frequency times per ms + "setSpecificStatesFrequencyJitter": variables.get_specific_states_frequency_jitter(fiber_no, motor_unit_no), # random value to add or substract to setSpecificStatesCallFrequency every stimulation, this is to add random jitter to the frequency + "setSpecificStatesRepeatAfterFirstCall": 0.01, # [ms] simulation time span for which the setSpecificStates callback will be called after a call was triggered + "setSpecificStatesCallEnableBegin": variables.get_specific_states_call_enable_begin(fiber_no, motor_unit_no),# [ms] first time when to call setSpecificStates + "additionalArgument": fiber_no, # last argument that will be passed to the callback functions set_specific_states, set_specific_parameters, etc. + + # parameters to the cellml model + "mappings": variables.mappings, # mappings between parameters and algebraics/constants and between outputConnectorSlots and states, algebraics or parameters, they are defined in helper.py + "parametersInitialValues": variables.parameters_initial_values, #[0.0, 1.0], # initial values for the parameters: I_Stim, l_hs + + "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh + "stimulationLogFilename": "out/stimulation.log", # a file that will contain the times of stimulations + }, + "OutputWriter" : [ + {"format": "Paraview", "outputInterval": 1, "filename": "out/" + variables.scenario_name + "/0D_states({},{})".format(fiber_in_subdomain_coordinate_x,fiber_in_subdomain_coordinate_y), "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} + ] if variables.states_output else [] + + }, + } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ + for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ + for motor_unit_no in [get_motor_unit_no(fiber_no)]], + } + }, + "Term2": { # Diffusion + "MultipleInstances": { + "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y), + "instances": + [{ + "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction + "CrankNicolson" : { + "initialValues": [], # no initial values are given + #"numberTimeSteps": 1, + "timeStepWidth": variables.dt_1D, # timestep width for the diffusion problem + "timeStepWidthRelativeTolerance": 1e-10, + "logTimeStepWidthAsKey": "dt_1D", # key under which the time step width will be written to the log file + "durationLogKey": "duration_1D", # log key of duration for this solver + "timeStepOutputInterval": 1e4, # how often to print the current timestep + "dirichletBoundaryConditions": {}, # old Dirichlet BC that are not used in FastMonodomainSolver: {0: -75.0036, -1: -75.0036}, + "dirichletOutputFilename": None, # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable + "inputMeshIsGlobal": True, # initial values would be given as global numbers + "solverName": "diffusionTermSolver", # reference to the linear solver + "nAdditionalFieldVariables": 2, # number of additional field variables that will be written to the output file, here for stress + "additionalSlotNames": ["stress", "activation"], + "checkForNanInf": True, # abort execution if the solution contains nan or inf values + + "FiniteElementMethod" : { + "inputMeshIsGlobal": True, + "meshName": "MeshFiber_{}".format(fiber_no), + "solverName": "diffusionTermSolver", + "prefactor": get_diffusion_prefactor(fiber_no, motor_unit_no), # resolves to Conductivity / (Am * Cm) + "slotName": None, + }, + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": int(1./variables.dt_1D*variables.output_timestep), "filename": "out/fiber_"+str(fiber_no), "binary": True, "fixedFormat": False, "combineFiles": True}, + #{"format": "Paraview", "outputInterval": 1./variables.dt_1D*variables.output_timestep, "filename": "out/fiber_"+str(i)+"_txt", "binary": False, "fixedFormat": False}, + #{"format": "ExFile", "filename": "out/fiber_"+str(i), "outputInterval": 1./variables.dt_1D*variables.output_timestep, "sphereSize": "0.02*0.02*0.02"}, + #{"format": "PythonFile", "filename": "out/fiber_"+str(i), "outputInterval": 1./variables.dt_1D*variables.output_timestep, "binary":True, "onlyNodalValues":True}, + ] + }, + } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ + for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ + for motor_unit_no in [get_motor_unit_no(fiber_no)]], + "OutputWriter": [ + {"format": "Paraview", "outputInterval": int(1/variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} + ], + }, + }, + }, + + # this is for biceps_contraction_no_cell, i.e. PrescribedValues instead of fibers + "GodunovSplitting": { # this splitting scheme is only needed to replicate the solver structure as with the fibers + "timeStepWidth": variables.dt_3D, + "logTimeStepWidthAsKey": "dt_splitting", + "durationLogKey": "duration_prescribed_values", + "timeStepOutputInterval": 100, + "endTime": variables.dt_3D, + "connectedSlotsTerm1To2": [], + "connectedSlotsTerm2To1": [], # transfer the same back, this avoids data copy + + "Term1": { + "MultipleInstances": { + "logKey": "duration_subdomains_z", + "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y), + "instances": + [{ + "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction + "PrescribedValues": { + "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh + "numberTimeSteps": 1, # number of timesteps to call the callback functions subsequently, this is usually 1 for prescribed values, because it is enough to set the reaction term only once per time step + "timeStepOutputInterval": 20, # if the time step should be written to console, a value > 10 produces no output + "slotNames": [], # names of the data connector slots + + # a list of field variables that will get values assigned in every timestep, by the provided callback function + "fieldVariables1": [ + {"name": "Vm", "callback": None}, + {"name": "stress", "callback": set_stress_values}, + ], + "fieldVariables2": [], + "additionalArgument": fiber_no, # a custom argument to the fieldVariables callback functions, this will be passed on as the last argument + + "OutputWriter" : [ + {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_fibers), "filename": "out/prescribed_fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} + ] + }, + } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ + for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ + for motor_unit_no in [get_motor_unit_no(fiber_no)]], + + #"OutputWriter" : variables.output_writer_fibers, + "OutputWriter": [ + {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_fibers), "filename": "out/fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} + ] + } + }, + + # term2 is unused, it is needed to be similar to the actual fiber solver structure + "Term2": {} + } + + } if (subdomain_coordinate_x,subdomain_coordinate_y) == (variables.own_subdomain_coordinate_x,variables.own_subdomain_coordinate_y) else None + for subdomain_coordinate_y in range(variables.n_subdomains_y) + for subdomain_coordinate_x in range(variables.n_subdomains_x)] + }, + "fiberDistributionFile": variables.fiber_distribution_file, # for FastMonodomainSolver, e.g. MU_fibre_distribution_3780.txt + "firingTimesFile": variables.firing_times_file, # for FastMonodomainSolver, e.g. MU_firing_times_real.txt + "onlyComputeIfHasBeenStimulated": True, # only compute fibers after they have been stimulated for the first time + "disableComputationWhenStatesAreCloseToEquilibrium": True, # optimization where states that are close to their equilibrium will not be computed again + "valueForStimulatedPoint": variables.vm_value_stimulated, # to which value of Vm the stimulated node should be set + "neuromuscularJunctionRelativeSize": 0.1, # range where the neuromuscular junction is located around the center, relative to fiber length. The actual position is draws randomly from the interval [0.5-s/2, 0.5+s/2) with s being this option. 0 means sharply at the center, 0.1 means located approximately at the center, but it can vary 10% in total between all fibers. + }, + "Term2": { # solid mechanics + "MuscleContractionSolver": { + "numberTimeSteps": 1, # only use 1 timestep per interval + "timeStepOutputInterval": 100, # do not output time steps + "Pmax": variables.pmax, # maximum PK2 active stress + "enableForceLengthRelation": variables.enable_force_length_relation, # if the factor f_l(λ_f) modeling the force-length relation (as in Heidlauf2013) should be multiplied. Set to false if this relation is already considered in the CellML model. + "lambdaDotScalingFactor": variables.lambda_dot_scaling_factor, # scaling factor for the output of the lambda dot slot, i.e. the contraction velocity. Use this to scale the unit-less quantity to, e.g., micrometers per millisecond for the subcellular model. + "slotNames": ["lambda", "ldot", "gamma", "T"], # names of the data connector slots + "OutputWriter" : [ + {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/muscle_3D", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + "mapGeometryToMeshes": [], # the mesh names of the meshes that will get the geometry transferred + "dynamic": True, # if the dynamic solid mechanics solver should be used, else it computes the quasi-static problem + + # the actual solid mechanics solver, this is either "DynamicHyperelasticitySolver" or "HyperelasticitySolver", depending on the value of "dynamic" + "DynamicHyperelasticitySolver": { + "timeStepWidth": variables.dt_3D, # time step width + "durationLogKey": "nonlinear", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements + "residualNormLogFilename": "muscle_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written + "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian + + "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian + + # mesh + "inputMeshIsGlobal": True, # the mesh is given locally + "meshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config + "fiberMeshNames": variables.fiber_mesh_names, # fiber meshes that will be used to determine the fiber direction, for multidomain there are no fibers so this would be empty list + #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + + # solving + "solverName": "mechanicsSolver", # name of the nonlinear solver configuration, it is defined under "Solvers" at the beginning of this config + #"loadFactors": [0.25, 0.66, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + "loadFactorGiveUpThreshold": 4e-2, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the progression between two subsequent load factors gets smaller than this value, the solution is aborted. + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be repeated + + # boundary and initial conditions + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements + "divideNeumannBoundaryConditionValuesByTotalArea": True, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied + "updateDirichletBoundaryConditionsFunction": None, # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step + + "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "extrapolateInitialGuess": True, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + "dirichletOutputFilename": "out/muscle_dirichlet_boundary_conditions", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable + "totalForceLogFilename": "out/muscle_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal + + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. + "OutputWriter" : [ + + # Paraview files + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/u", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + #{"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, + ], + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter" : [ # output files for displacements function space (quadratic elements) + #{"format": "Paraview", "outputInterval": int(output_interval/dt), "filename": "out/dynamic", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/muscle_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + } + } + } + } + } +} + + +# stop timer and calculate how long parsing lasted +if rank_no == 0: + t_stop_script = timeit.default_timer() + print("Python config parsed in {:.1f}s.".format(t_stop_script - t_start_script)) \ No newline at end of file diff --git a/muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp b/muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp new file mode 100644 index 000000000..de4897bb6 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp @@ -0,0 +1,47 @@ +#include +#include +#include + +#include +#include "easylogging++.h" + +#include "opendihu.h" + +int main(int argc, char *argv[]) { + // multiple fibers in arbitrary partitioning, coupled to dynamic nonlinear + // elasticity + + // initialize everything, handle arguments and parse settings from input file + DihuContext settings(argc, argv); + + // define problem + Control::PreciceAdapter, + BasisFunction::LagrangeOfOrder<1>>>>>, + Control::MultipleInstances< + TimeSteppingScheme::CrankNicolson< // fiber diffusion + SpatialDiscretization::FiniteElementMethod< + Mesh::StructuredDeformableOfDimension<1>, + BasisFunction::LagrangeOfOrder<1>, + Quadrature::Gauss<2>, + Equation::Dynamic::IsotropicDiffusion>>>>>>, + MuscleContractionSolver<>>> + problem(settings); + + // run problem + problem.run(); + + return EXIT_SUCCESS; +} diff --git a/muscle-tendon-complex/muscle-opendihu/variables/variables.py b/muscle-tendon-complex/muscle-opendihu/variables/variables.py new file mode 100644 index 000000000..346098aa8 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/variables/variables.py @@ -0,0 +1,159 @@ +case_name = "default" +precice_config_file = "default" + +# scenario name for log file +scenario_name = "muscle" + +# Fixed units in cellMl models: +# These define the unit system. +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 uA = 1e-6 A +# 1 uF = 1e-6 F +# +# derived units: +# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 +# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg + +# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN +# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS +# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV +# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz +# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 +# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa + +# Hodgkin-Huxley +# t: ms +# STATES[0], Vm: mV +# CONSTANTS[1], Cm: uF*cm^-2 +# CONSTANTS[2], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Shorten +# t: ms +# CONSTANTS[0], Cm: uF*cm^-2 +# STATES[0], Vm: mV +# ALGEBRAIC[32], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Fixed units in mechanics system +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 N +# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa +# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg +# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 +# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 + +# material parameters +# -------------------- +# quantities in mechanics unit system +rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) + +# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) +# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 +# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 + +c1 = 3.176e-10 # [N/cm^2] +c2 = 1.813 # [N/cm^2] +b = 1.075e-2 # [N/cm^2] anisotropy parameter +d = 9.1733 # [-] anisotropy parameter + +material_parameters = [c1, c2, b, d] # material parameters +pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) +#pmax = 0.73 + +# load +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +bottom_traction = [0.0,0.0,0.0] # [N] + +# Monodomain parameters +# -------------------- +# quantities in CellML unit system +Conductivity = 3.828 # [mS/cm] sigma, conductivity +Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) +Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) +# diffusion prefactor = Conductivity/(Am*Cm) + +# timing and activation parameters +# ----------------- +# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" +import random +random.seed(0) # ensure that random numbers are the same on every rank +# radius: [μm], stimulation frequency [Hz], jitter [-] +motor_units = [ + {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers + {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers +] + +# timing parameters +# ----------------- +end_time = 20000.0 # [ms] end time of the simulation +stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. +stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation +dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) +dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) +dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) +dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 +output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D + + +# input files +fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" +fat_mesh_file = fiber_file + "_fat.bin" +firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" +cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" + +# stride for sampling the 3D elements from the fiber data +# a higher number leads to less 3D elements +sampling_stride_x = 2 +sampling_stride_y = 2 +sampling_stride_z = 74 + +# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 +# when a fiber point is still considered part of the element. +# Try to increase this such that all mappings have all points. +mapping_tolerance = 0.5 + +# other options +paraview_output = True +adios_output = False +exfile_output = False +python_output = False +disable_firing_output = False + +# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's +def get_am(fiber_no, mu_no): + # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm + r = motor_units[mu_no]["radius"]*1e-4 + # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r + return 2./r + #return Am + +def get_cm(fiber_no, mu_no): + return Cm + +def get_conductivity(fiber_no, mu_no): + return Conductivity + +def get_specific_states_call_frequency(fiber_no, mu_no): + stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] + return stimulation_frequency*1e-3 + +def get_specific_states_frequency_jitter(fiber_no, mu_no): + #return 0 + return motor_units[mu_no % len(motor_units)]["jitter"] + +def get_specific_states_call_enable_begin(fiber_no, mu_no): + return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file From feef2d0b74f7c8f04e42e6748eec1b27ab3f038a Mon Sep 17 00:00:00 2001 From: carme-hp Date: Tue, 27 Feb 2024 23:06:55 +0100 Subject: [PATCH 03/40] Move src code to opendihu-solver folder --- .../muscle-opendihu/Sconscript | 14 ------------ .../opendihu-solver/SConscript | 8 +++++++ .../SConstruct | 10 +-------- .../opendihu-solver/clean.sh | 3 +++ .../src/muscle-solver.cpp} | 21 +++++------------- .../opendihu-solver/src/tendon-solver.cpp | 22 +++++++++++++++++++ 6 files changed, 40 insertions(+), 38 deletions(-) delete mode 100644 muscle-tendon-complex/muscle-opendihu/Sconscript create mode 100644 muscle-tendon-complex/opendihu-solver/SConscript rename muscle-tendon-complex/{muscle-opendihu => opendihu-solver}/SConstruct (51%) create mode 100644 muscle-tendon-complex/opendihu-solver/clean.sh rename muscle-tendon-complex/{muscle-opendihu/src/muscle_electrophysiology_precice.cpp => opendihu-solver/src/muscle-solver.cpp} (57%) create mode 100644 muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp diff --git a/muscle-tendon-complex/muscle-opendihu/Sconscript b/muscle-tendon-complex/muscle-opendihu/Sconscript deleted file mode 100644 index aa29b9b32..000000000 --- a/muscle-tendon-complex/muscle-opendihu/Sconscript +++ /dev/null @@ -1,14 +0,0 @@ -# This script declares to SCons how to compile the example. -# It has to be called from a SConstruct file. -# The 'env' object is passed from there and contains further specification like directory and debug/release flags. -# -# Note: If you're creating a new example and copied this file, adjust the desired name of the executable in the 'target' parameter of env.Program. - - -Import('env') # import Environment object from calling SConstruct - -# if the option no_tests was given, quit the script -if not env['no_examples']: - - # create the main executable - env.Program(target = 'muscle_electrophysiology_precice', source = "src/muscle_electrophysiology_precice.cpp") diff --git a/muscle-tendon-complex/opendihu-solver/SConscript b/muscle-tendon-complex/opendihu-solver/SConscript new file mode 100644 index 000000000..e70812192 --- /dev/null +++ b/muscle-tendon-complex/opendihu-solver/SConscript @@ -0,0 +1,8 @@ +# This script declares to SCons how to compile the example. +# It has to be called from a SConstruct file. +# The 'env' object is passed from there and contains further specification like directory and debug/release flags. + +Import('env') + +env.Program(target = 'muscle-solver', source = "src/muscle-solver.cpp") +env.Program(target = 'tendon-solver', source = "src/tendon-solver.cpp") diff --git a/muscle-tendon-complex/muscle-opendihu/SConstruct b/muscle-tendon-complex/opendihu-solver/SConstruct similarity index 51% rename from muscle-tendon-complex/muscle-opendihu/SConstruct rename to muscle-tendon-complex/opendihu-solver/SConstruct index e416fac61..78c806c09 100644 --- a/muscle-tendon-complex/muscle-opendihu/SConstruct +++ b/muscle-tendon-complex/opendihu-solver/SConstruct @@ -1,15 +1,7 @@ -# SConstruct file for a single example. -# -# Usage: `scons BUILD_TYPE=debug` will build debug version, `scons` will build release version. - -# Call the generic `SConstructGeneral` script that will configure everything. It is located at the top level directory of opendihu. -# That script will then call a `SConscript` file that defines which sources to use. - import os # get the directory where opendihu is installed (the top level directory of opendihu) -opendihu_home = os.environ.get('OPENDIHU_HOME') or "../../../../.." - +opendihu_home = os.environ.get('OPENDIHU_HOME') # set path where the "SConscript" file is located (set to current path) path_where_to_call_sconscript = Dir('.').srcnode().abspath diff --git a/muscle-tendon-complex/opendihu-solver/clean.sh b/muscle-tendon-complex/opendihu-solver/clean.sh new file mode 100644 index 000000000..2d9bf2ff9 --- /dev/null +++ b/muscle-tendon-complex/opendihu-solver/clean.sh @@ -0,0 +1,3 @@ +rm -r .* *.log +rm -r build_release +rm -r precice-profiling diff --git a/muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp b/muscle-tendon-complex/opendihu-solver/src/muscle-solver.cpp similarity index 57% rename from muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp rename to muscle-tendon-complex/opendihu-solver/src/muscle-solver.cpp index de4897bb6..0e6bdf82b 100644 --- a/muscle-tendon-complex/muscle-opendihu/src/muscle_electrophysiology_precice.cpp +++ b/muscle-tendon-complex/opendihu-solver/src/muscle-solver.cpp @@ -8,30 +8,22 @@ #include "opendihu.h" int main(int argc, char *argv[]) { - // multiple fibers in arbitrary partitioning, coupled to dynamic nonlinear - // elasticity - - // initialize everything, handle arguments and parse settings from input file + DihuContext settings(argc, argv); - // define problem Control::PreciceAdapter, BasisFunction::LagrangeOfOrder<1>>>>>, Control::MultipleInstances< - TimeSteppingScheme::CrankNicolson< // fiber diffusion + TimeSteppingScheme::CrankNicolson< SpatialDiscretization::FiniteElementMethod< Mesh::StructuredDeformableOfDimension<1>, BasisFunction::LagrangeOfOrder<1>, @@ -40,7 +32,6 @@ int main(int argc, char *argv[]) { MuscleContractionSolver<>>> problem(settings); - // run problem problem.run(); return EXIT_SUCCESS; diff --git a/muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp b/muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp new file mode 100644 index 000000000..72af7afd8 --- /dev/null +++ b/muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +#include +#include "easylogging++.h" + +#include "opendihu.h" + +int main(int argc, char *argv[]) { + + DihuContext settings(argc, argv); + + Control::PreciceAdapter> + problem(settings); + + problem.run(); + + return EXIT_SUCCESS; +} From cfbe7182f768b74523eb65941fcde9dc6054cdc6 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Tue, 27 Feb 2024 23:26:16 +0100 Subject: [PATCH 04/40] Complete opendihu participants --- .../muscle-opendihu/settings-muscle.py | 18 +- muscle-tendon-complex/precice-config.xml | 172 +++--- .../settings-tendon-bottom.py | 489 ++++++++++++++++++ .../variables/variables.py | 159 ++++++ .../settings-tendon-top-A.py | 260 ++++++++++ .../variables/variables.py | 159 ++++++ .../settings-tendon-top-B.py | 259 ++++++++++ .../variables/variables.py | 159 ++++++ 8 files changed, 1580 insertions(+), 95 deletions(-) create mode 100644 muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py create mode 100644 muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py create mode 100644 muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py create mode 100644 muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py create mode 100644 muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py create mode 100644 muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py index ff4b37d04..d305446c2 100644 --- a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -208,50 +208,50 @@ "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver { - "meshName": "MuscleMeshBottom", # precice name of the 2D coupling mesh + "meshName": "Muscle-Bottom-Mesh", # precice name of the 2D coupling mesh "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top }, { - "meshName": "MuscleMeshTopA", # precice name of the 2D coupling mesh + "meshName": "Muscle-Top-A-Mesh", # precice name of the 2D coupling mesh "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top }, { - "meshName": "MuscleMeshTopB", # precice name of the 2D coupling mesh + "meshName": "Muscle-Top-B-Mesh", # precice name of the 2D coupling mesh "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top }, ], "preciceData": [ { "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "MuscleMeshBottom", # name of the precice coupling surface mesh, as given in the precice xml settings file + "meshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "MuscleMeshTopA", # name of the precice coupling surface mesh, as given in the precice xml settings file + "meshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "MuscleMeshTopB", # name of the precice coupling surface mesh, as given in the precice xml settings file + "meshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "MuscleMeshBottom", # name of the precice coupling surface mesh, as given in the precice xml settings + "meshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "MuscleMeshTopA", # name of the precice coupling surface mesh, as given in the precice xml settings + "meshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "MuscleMeshTopB", # name of the precice coupling surface mesh, as given in the precice xml settings + "meshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file }, ], diff --git a/muscle-tendon-complex/precice-config.xml b/muscle-tendon-complex/precice-config.xml index 0e2871fe6..7b7677c55 100644 --- a/muscle-tendon-complex/precice-config.xml +++ b/muscle-tendon-complex/precice-config.xml @@ -14,37 +14,37 @@ - + - + - + - + - + - + @@ -58,29 +58,29 @@ - - - - - - + + + + + + - - - + + + - - - + + + - - - + + + - + @@ -135,20 +135,20 @@ - - + + - - - + + + - + @@ -157,20 +157,20 @@ - - + + - - - + + + - + @@ -179,20 +179,20 @@ - - + + - - - + + + - + @@ -216,30 +216,30 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + @@ -251,16 +251,16 @@ --> - - - + + + - - - + + + - - - + + + \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py new file mode 100644 index 000000000..0ff101778 --- /dev/null +++ b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py @@ -0,0 +1,489 @@ +# Transversely-isotropic Mooney Rivlin on a tendon geometry +# Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. +import sys, os +import numpy as np +import pickle +import argparse +import sys +sys.path.insert(0, '.') +import variables # file variables.py, defines default values for all parameters, you can set the parameters there +from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain +import stl +from stl import mesh +import json + +# set title of terminal +title = "tendon_bottom" +print('\33]0;{}\a'.format(title), end='', flush=True) + +# material parameters +# -------------------- +# quantities in mechanics unit system +variables.rho = 10 # [1e-4 kg/cm^3] 10 = density of the muscle (density of water) + +# material parameters for Saint Venant-Kirchhoff material +# https://www.researchgate.net/publication/230248067_Bulk_Modulus + +youngs_modulus = 7e4 # [N/cm^2 = 10kPa] +shear_modulus = 3e4 + +#youngs_modulus*=1e-3 +#shear_modulus*=1e-3 + +lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda +mu = shear_modulus # Lamé parameter mu or G (shear modulus) + +variables.material_parameters = [lambd, mu] + +variables.constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +variables.force = 100.0 # [N] pulling force to the bottom + +variables.dt_elasticity = 1 # [ms] time step width for elasticity +variables.end_time = 20000 # [ms] simulation time +variables.scenario_name = "tendon_bottom" +variables.is_bottom_tendon = True # whether the tendon is at the bottom (negative z-direction), this is important for the boundary conditions +variables.output_timestep_3D = 50 # [ms] output timestep + +# input mesh file +fiber_file = "../../../../input/left_biceps_brachii_tendon1.bin" # bottom tendon +#fiber_file = "../../../../input/left_biceps_brachii_tendon2a.bin" +#fiber_file = "../../../../input/left_biceps_brachii_tendon2b.bin" +#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" + +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. + +# parse arguments +rank_no = (int)(sys.argv[-2]) +n_ranks = (int)(sys.argv[-1]) + +# define command line arguments +parser = argparse.ArgumentParser(description='tendon') +parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) +parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) +parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) +parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) +parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) +parser.add_argument('-vmodule', help='ignore') + +# parse command line arguments and assign values to variables module +args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) +if len(other_args) != 0 and rank_no == 0: + print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) + +# partitioning +# ------------ +# this has to match the total number of processes +if variables.n_subdomains is not None: + variables.n_subdomains_x = variables.n_subdomains[0] + variables.n_subdomains_y = variables.n_subdomains[1] + variables.n_subdomains_z = variables.n_subdomains[2] + +# compute partitioning +if rank_no == 0: + if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: + print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) + sys.exit(-1) + +# stride for sampling the 3D elements from the fiber data +# here any number is possible +sampling_stride_x = 1 +sampling_stride_y = 1 +sampling_stride_z = 2 + +# create the partitioning using the script in create_partitioned_meshes_for_settings.py +result = create_partitioned_meshes_for_settings( + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + fiber_file, load_fiber_data, + sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) + +[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result + +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z +nx = n_points_3D_mesh_linear_global_x-1 +ny = n_points_3D_mesh_linear_global_y-1 +nz = n_points_3D_mesh_linear_global_z-1 + +node_positions = variables.meshes["3Dmesh_quadratic"]["nodePositions"] + +# boundary conditions (for quadratic elements) +# -------------------------------------------- +[mx, my, mz] = variables.meshes["3Dmesh_quadratic"]["nPointsGlobal"] +[nx, ny, nz] = variables.meshes["3Dmesh_quadratic"]["nElements"] + +# set Dirichlet BC, fix x and y coordinates of the end of tendon that is attached to the bone +variables.elasticity_dirichlet_bc = {} +k = 0 + +# fix z value on the whole x-y-plane +for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,None,None,None,None] + +# set Neumann BC, set traction at the end of the tendon that is attached to the bone +k = 0 +# start with 0 BC +variables.elasticity_neumann_bc = [{"element": k*nx*ny + j*nx + i, "constantVector": [0,0,0], "face": "2-"} for j in range(ny) for i in range(nx)] + +def update_neumann_bc(t): + + # set new Neumann boundary conditions + k = 0 + factor = min(1, t/100) # for t ∈ [0,100] from 0 to 1 + elasticity_neumann_bc = [{ + "element": k*nx*ny + j*nx + i, + "constantVector": [0,0,-variables.force*factor], # force pointing to bottom + "face": "2-", + "isInReferenceConfiguration": True + } for j in range(ny) for i in range(nx)] + + config = { + "inputMeshIsGlobal": True, + "divideNeumannBoundaryConditionValuesByTotalArea": True, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied + "neumannBoundaryConditions": elasticity_neumann_bc, + } + print("prescribed pulling force to bottom: {}".format(variables.force*factor)) + return config + +# update dirichlet boundary conditions to account for movement of humerus + +current_ulna_force = 0 +current_ulna_angle = 0 + +# global coordinates of tendon bottom +# global coordinates of elbow hinge +elbow_hinge_point = np.array([3.54436, 11.4571, -58.5607]) +bottom_tendon_insertion_point = np.array([4.30, 14.81, -63.41]) + +vec = -elbow_hinge_point + bottom_tendon_insertion_point +angle_offset = np.arctan((bottom_tendon_insertion_point[2] - elbow_hinge_point[2]) / np.linalg.norm(vec)) +rotation_axis = np.array([-1.5, 1, 0]) +rotation_axis = rotation_axis / np.linalg.norm(rotation_axis) # normalize rotation axis + +def rotation_matrix(angle): + axis_x = rotation_axis[0] + axis_y = rotation_axis[1] + axis_z = rotation_axis[2] + + # compute rotation matrix + rotation_matrix = np.array([ + [np.cos(angle) + axis_x**2*(1 - np.cos(angle)), + axis_x*axis_y*(1 - np.cos(angle)) - axis_z*np.sin(angle), + axis_x*axis_z*(1 - np.cos(angle)) + axis_y*np.sin(angle)], + [axis_y*axis_x*(1 - np.cos(angle)) + axis_z*np.sin(angle), + np.cos(angle) + axis_y**2*(1 - np.cos(angle)), + axis_y*axis_z*(1 - np.cos(angle)) - axis_x*np.sin(angle)], + [axis_z*axis_x*(1 - np.cos(angle)) - axis_y*np.sin(angle), + axis_z*axis_y*(1 - np.cos(angle)) + axis_x*np.sin(angle), + np.cos(angle) + axis_z**2*(1 - np.cos(angle))]]) + + return rotation_matrix + +call_count = 0 +ulna_series_files = [] +ulna_stl_filename = os.path.join(os.getcwd(),"cm_left_ulna.stl") +stl_mesh = None +try: + stl_mesh = mesh.Mesh.from_file(ulna_stl_filename) +except: + print("Could not open file {} in directory {}".format(ulna_stl_filename, os.getcwd())) + sys.exit(0) + +# Function to update dirichlet boundary conditions over time, t. +# Only those entries can be updated that were also initially set. +def update_dirichlet_bc(t): + global current_ulna_angle + + # determine parameter for the rotation of the tendon insertion point + angle = current_ulna_angle + rotation_point = elbow_hinge_point + rotation_mat = rotation_matrix(angle) + vertex = bottom_tendon_insertion_point + + # rotation vertex about rotation_axis by angle + vertex = vertex - rotation_point + vertex = rotation_mat.dot(vertex) + vertex = vertex + rotation_point + + new_insertion_point = vertex + offset = -bottom_tendon_insertion_point + new_insertion_point + print("angle: {} deg, rot matrix: {}".format(angle*180/np.pi,rotation_matrix(angle))) + print("old insertion point: {}, new insertion point: {}, offset: {}".format(bottom_tendon_insertion_point,new_insertion_point,offset)) + #offset[0] = 0 + #offset[1] = 0 + #offset[2] = 0 + + # update dirichlet boundary conditions, set prescribed value to offset, do not constrain velocity + for key in variables.elasticity_dirichlet_bc.keys(): + variables.elasticity_dirichlet_bc[key] = [offset[0],offset[1],offset[2],None,None,None] + return variables.elasticity_dirichlet_bc + +def store_rotated_ulna(t): + global current_ulna_angle, call_count, stl_mesh, ulna_series_files + + if np.isnan(current_ulna_angle): + return + + # store rotated ulna + call_count += 1 + output_interval = 50 # [ms] (because coupling timestep is 1ms) + if call_count % output_interval == 0: + out_triangles = [] + + rotation_point = elbow_hinge_point + rotation_mat = rotation_matrix(current_ulna_angle) + + for p in stl_mesh.points: + # p contains the 9 entries [p1x p1y p1z p2x p2y p2z p3x p3y p3z] of the triangle with corner points (p1,p2,p3) + + # transform vertices + vertex_list = [] + + # apply rotation + for vertex in [np.array(p[0:3]), np.array(p[3:6]), np.array(p[6:9])]: + vertex = vertex - rotation_point + vertex = rotation_mat.dot(vertex) + vertex = vertex + rotation_point + vertex_list.append(vertex) + + out_triangles += [vertex_list] + + # Create the mesh + out_mesh = mesh.Mesh(np.zeros(len(out_triangles), dtype=mesh.Mesh.dtype)) + for i, f in enumerate(out_triangles): + out_mesh.vectors[i] = f + + out_mesh.update_normals() + ulna_output_filename = "ulna_{:04d}.stl".format((int)(call_count/output_interval)) + ulna_output_path = os.path.join("out",ulna_output_filename) + out_mesh.save(ulna_output_path) + print("Saved file {}".format(ulna_output_path)) + ulna_series_files.append({"name": ulna_output_filename, "time": t}) + + # save json series file + ulna_series_filename = "out/ulna.stl.series" + with open(ulna_series_filename, "w") as f: + data = {"file-series-version" : "1.0", "files" : ulna_series_files} + json.dump(data, f, indent='\t') + +forces = [] +def callback_total_force(t, bearing_force_bottom, bearing_moment_bottom, bearing_force_top, bearing_moment_top): + global current_ulna_angle + # this callback functions gets the current total forces that are exerted by the muscle + + current_ulna_force = bearing_force_bottom[2] + + # compute average of last 10 values + forces.append(current_ulna_force) + + if len(forces) > 10: + forces.pop(0) + current_ulna_force = np.mean(forces) + print("callback_total_force, t: {}, force: {}".format(t, current_ulna_force)) + + # compute relation between force and angle of ulna + if False: + # positive angle = elbow flexion (ulna move downwards) + min_ulna_angle = 0 # [deg] + max_ulna_angle = -45 # [deg] + force_factor = -current_ulna_force / 500 + force_factor = min(1.5, max(-0.5, force_factor)) + + current_ulna_angle = min_ulna_angle + force_factor * (max_ulna_angle-min_ulna_angle) + print("callback_total_force, t: {}, force: {}, factor: {}, angle: {}".format(t, current_ulna_force, force_factor,current_ulna_angle)) + current_ulna_angle *= np.pi/180 # convert from deg to rad + + + +# Function to postprocess the output +# This function gets periodically called by the running simulation. +# It provides all current variables for each node: geometry (position), u, v, stress, etc. +def postprocess(result): + global current_ulna_angle + + result = result[0] + # print result for debugging + #print(result) + + # get current time + current_time = result["currentTime"] + timestep_no = result["timeStepNo"] + + # get number of nodes + nx = result["nElementsLocal"][0] # number of elements + ny = result["nElementsLocal"][1] # number of elements + nz = result["nElementsLocal"][2] # number of elements + mx = 2*nx + 1 # number of nodes for quadratic elements + my = 2*ny + 1 + mz = 2*nz + 1 + + # parse variables + field_variables = result["data"] + + #for f in field_variables: + # print(f["name"]) + + # field_variables[0] is the geometry + # field_variables[1] is the displacements u + # etc., uncomment the above to see all field variables + + displacement_components = field_variables[1]["components"] + + # traction values contains the traction vector in reference configuration + u1_values = displacement_components[0]["values"] # displacement in x-direction + u2_values = displacement_components[1]["values"] # displacement in y-direction + u3_values = displacement_components[2]["values"] # displacement in z-direction + u3_values_bottom = [u3_values[j*mx+i] for j in range(my) for i in range(mx)] + z_displacement = np.mean(u3_values) + + #print("nx,ny: {},{}, mx,my: {},{}, {}={}".format(nx,ny,mx,my,mx*my*mz,len(u3_values))) + + # compute elbow angle from displacement of muscle in z direction + vec = -elbow_hinge_point + bottom_tendon_insertion_point + current_ulna_angle = -np.arcsin(z_displacement / np.linalg.norm(vec)) + + print("z displacement: {} ({}), current_ulna_angle: {} deg".format(z_displacement, np.linalg.norm(vec), current_ulna_angle*180/np.pi)) + + store_rotated_ulna(current_time) + +config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" + "timeStepWidth": variables.dt_elasticity, # time step width + "endTime": variables.end_time, # end time of the simulation time span + "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements + "residualNormLogFilename": "out/tendon_bottom_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written + "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian + + "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian + + # mesh + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings + + "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction + #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + "fiberDirectionInElement": [0,0,1], # if fiberMeshNames and fiberDirections are empty, directly set the constant fiber direction, in element coordinate system + + # nonlinear solver + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver + "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + "snesRebuildJacobianFrequency": 5, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again + + #"dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve + "dumpFilename": "", # dump disabled + "dumpFormat": "matlab", # default, ascii, matlab + + #"loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep + #"loadFactors": [0.5, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + "loadFactorGiveUpThreshold": 1e-3, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the load factors get too small, it aborts the solve. + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called + + # boundary and initial conditions + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements + "divideNeumannBoundaryConditionValuesByTotalArea": True, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied + "updateDirichletBoundaryConditionsFunction": None, #update_dirichlet_bc, # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # stide every which step the update function should be called, 1 means every time step + "updateNeumannBoundaryConditionsFunction": update_neumann_bc, # a callback function to periodically update the Neumann boundary conditions + "updateNeumannBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step + + "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "extrapolateInitialGuess": True, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + "dirichletOutputFilename": "out/tendon_bottom_dirichlet_boundary_conditions", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable + "totalForceLogFilename": "out/tendon_bottom_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal + "totalForceFunction": None, #callback_total_force, # callback function that gets the total force at bottom and top of the domain + "totalForceFunctionCallInterval": 1, # how often the "totalForceFunction" is called + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. + "OutputWriter" : [ + + # Paraview files + {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_bottom", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + {"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": "", "fileNumbering": "incremental"}, + ], + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter" : [ # output files for displacements function space (quadratic elements) + {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_bottom_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, +} + +config = { + "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file + "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files + "solverStructureDiagramFile": "out/tendon_bottom_solver_structure.txt", # output file of a diagram that shows data connection between solvers + "mappingsBetweenMeshesLogFile": "out/tendon_bottom_mappings_between_meshes_log.txt", # log file for mappings + "Meshes": variables.meshes, + + "PreciceAdapter": { # precice adapter for bottom tendon + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file + "preciceParticipantName": "TendonSolverBottom", # name of the own precice participant, has to match the name given in the precice xml config file + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "meshName": "Tendon-Bottom-Mesh", # precice name of the 2D coupling mesh + "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + } + ], + "preciceData": [ + { + "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file + } + ], + "HyperelasticitySolver": config_hyperelasticity, + "DynamicHyperelasticitySolver": config_hyperelasticity, + } +} diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py new file mode 100644 index 000000000..346098aa8 --- /dev/null +++ b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py @@ -0,0 +1,159 @@ +case_name = "default" +precice_config_file = "default" + +# scenario name for log file +scenario_name = "muscle" + +# Fixed units in cellMl models: +# These define the unit system. +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 uA = 1e-6 A +# 1 uF = 1e-6 F +# +# derived units: +# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 +# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg + +# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN +# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS +# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV +# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz +# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 +# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa + +# Hodgkin-Huxley +# t: ms +# STATES[0], Vm: mV +# CONSTANTS[1], Cm: uF*cm^-2 +# CONSTANTS[2], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Shorten +# t: ms +# CONSTANTS[0], Cm: uF*cm^-2 +# STATES[0], Vm: mV +# ALGEBRAIC[32], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Fixed units in mechanics system +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 N +# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa +# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg +# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 +# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 + +# material parameters +# -------------------- +# quantities in mechanics unit system +rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) + +# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) +# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 +# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 + +c1 = 3.176e-10 # [N/cm^2] +c2 = 1.813 # [N/cm^2] +b = 1.075e-2 # [N/cm^2] anisotropy parameter +d = 9.1733 # [-] anisotropy parameter + +material_parameters = [c1, c2, b, d] # material parameters +pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) +#pmax = 0.73 + +# load +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +bottom_traction = [0.0,0.0,0.0] # [N] + +# Monodomain parameters +# -------------------- +# quantities in CellML unit system +Conductivity = 3.828 # [mS/cm] sigma, conductivity +Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) +Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) +# diffusion prefactor = Conductivity/(Am*Cm) + +# timing and activation parameters +# ----------------- +# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" +import random +random.seed(0) # ensure that random numbers are the same on every rank +# radius: [μm], stimulation frequency [Hz], jitter [-] +motor_units = [ + {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers + {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers +] + +# timing parameters +# ----------------- +end_time = 20000.0 # [ms] end time of the simulation +stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. +stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation +dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) +dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) +dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) +dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 +output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D + + +# input files +fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" +fat_mesh_file = fiber_file + "_fat.bin" +firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" +cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" + +# stride for sampling the 3D elements from the fiber data +# a higher number leads to less 3D elements +sampling_stride_x = 2 +sampling_stride_y = 2 +sampling_stride_z = 74 + +# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 +# when a fiber point is still considered part of the element. +# Try to increase this such that all mappings have all points. +mapping_tolerance = 0.5 + +# other options +paraview_output = True +adios_output = False +exfile_output = False +python_output = False +disable_firing_output = False + +# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's +def get_am(fiber_no, mu_no): + # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm + r = motor_units[mu_no]["radius"]*1e-4 + # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r + return 2./r + #return Am + +def get_cm(fiber_no, mu_no): + return Cm + +def get_conductivity(fiber_no, mu_no): + return Conductivity + +def get_specific_states_call_frequency(fiber_no, mu_no): + stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] + return stimulation_frequency*1e-3 + +def get_specific_states_frequency_jitter(fiber_no, mu_no): + #return 0 + return motor_units[mu_no % len(motor_units)]["jitter"] + +def get_specific_states_call_enable_begin(fiber_no, mu_no): + return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py new file mode 100644 index 000000000..4452481fb --- /dev/null +++ b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py @@ -0,0 +1,260 @@ +# Transversely-isotropic Mooney Rivlin on a tendon geometry +# Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. +import sys, os +import numpy as np +import pickle +import argparse +import sys +sys.path.insert(0, '.') +import variables # file variables.py, defines default values for all parameters, you can set the parameters there +from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain + +# set title of terminal +title = "tendon_top_a" +print('\33]0;{}\a'.format(title), end='', flush=True) + +# material parameters +# -------------------- +# quantities in mechanics unit system +variables.rho = 10 # [1e-4 kg/cm^3] 10 = density of the muscle (density of water) + +# material parameters for Saint Venant-Kirchhoff material +# https://www.researchgate.net/publication/230248067_Bulk_Modulus + +youngs_modulus = 7e4 # [N/cm^2 = 10kPa] +shear_modulus = 3e4 + +#youngs_modulus*=1e-3 +#shear_modulus*=1e-3 + +lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda +mu = shear_modulus # Lamé parameter mu or G (shear modulus) + +variables.material_parameters = [lambd, mu] + +variables.constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +variables.force = 1.0 # [N] + +variables.dt_elasticity = 1 # [ms] time step width for elasticity +variables.end_time = 20000 # [ms] simulation time +variables.scenario_name = "tendon_top_a" +variables.is_bottom_tendon = False # whether the tendon is at the bottom (negative z-direction), this is important for the boundary conditions +variables.output_timestep_3D = 50 # [ms] output timestep + +# input mesh file +#fiber_file = "../../../../input/left_biceps_brachii_tendon1.bin" # bottom tendon +fiber_file = "../../../../input/left_biceps_brachii_tendon2a.bin" # top tendon +#fiber_file = "../../../../input/left_biceps_brachii_tendon2b.bin" +#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" + +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. + +# parse arguments +rank_no = (int)(sys.argv[-2]) +n_ranks = (int)(sys.argv[-1]) + +# define command line arguments +parser = argparse.ArgumentParser(description='tendon') +parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) +parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) +parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) +parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) +parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) +parser.add_argument('-vmodule', help='ignore') + +# parse command line arguments and assign values to variables module +args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) +if len(other_args) != 0 and rank_no == 0: + print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) + +# partitioning +# ------------ +# this has to match the total number of processes +if variables.n_subdomains is not None: + variables.n_subdomains_x = variables.n_subdomains[0] + variables.n_subdomains_y = variables.n_subdomains[1] + variables.n_subdomains_z = variables.n_subdomains[2] + +# compute partitioning +if rank_no == 0: + if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: + print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) + sys.exit(-1) + +# stride for sampling the 3D elements from the fiber data +# here any number is possible +sampling_stride_x = 1 +sampling_stride_y = 1 +sampling_stride_z = 2 + +# create the partitioning using the script in create_partitioned_meshes_for_settings.py +result = create_partitioned_meshes_for_settings( + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + fiber_file, load_fiber_data, + sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) + +[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result + +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z +nx = n_points_3D_mesh_linear_global_x-1 +ny = n_points_3D_mesh_linear_global_y-1 +nz = n_points_3D_mesh_linear_global_z-1 + +node_positions = variables.meshes["3Dmesh_quadratic"]["nodePositions"] + +# boundary conditions (for quadratic elements) +# -------------------------------------------- +[mx, my, mz] = variables.meshes["3Dmesh_quadratic"]["nPointsGlobal"] +[nx, ny, nz] = variables.meshes["3Dmesh_quadratic"]["nElements"] + +# set Dirichlet BC, fix top end of tendon that is attached to the bone +variables.elasticity_dirichlet_bc = {} +k = mz-1 + +# fix the whole x-y plane +for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,0.0,None,None,None] + +# set no Neumann BC +variables.elasticity_neumann_bc = [] + + +config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" + "timeStepWidth": variables.dt_elasticity, # time step width + "endTime": variables.end_time, # end time of the simulation time span + "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements + "residualNormLogFilename": "out/tendon_top_a_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written + "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian + + "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian + + # mesh + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings + + "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction + #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + "fiberDirectionInElement": [0,0,1], # if fiberMeshNames and fiberDirections are empty, directly set the constant fiber direction, in element coordinate system + + # nonlinear solver + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver + "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + "snesRebuildJacobianFrequency": 5, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again + + #"dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve + "dumpFilename": "", # dump disabled + "dumpFormat": "matlab", # default, ascii, matlab + + #"loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep + #"loadFactors": [0.5, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + "loadFactorGiveUpThreshold": 1e-3, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the load factors get too small, it aborts the solve. + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called + + # boundary and initial conditions + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements + "divideNeumannBoundaryConditionValuesByTotalArea": False, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied + "updateDirichletBoundaryConditionsFunction": None, # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step + + "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "extrapolateInitialGuess": False, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + "dirichletOutputFilename": "out/tendon_top_a_dirichlet_boundary_conditions_tendon_top_a", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable + "totalForceLogFilename": "out/tendon_top_a_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. + "OutputWriter" : [ + + # Paraview files + {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_a", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + #{"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, + ], + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter" : [ # output files for displacements function space (quadratic elements) + {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_a_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, +} + +config = { + "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file + "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files + "solverStructureDiagramFile": "out/tendon_top_a_solver_structure.txt", # output file of a diagram that shows data connection between solvers + "mappingsBetweenMeshesLogFile": "out/tendon_top_a_mappings_between_meshes_log.txt", # log file for mappings + "Meshes": variables.meshes, + + "PreciceAdapter": { # precice adapter for bottom tendon + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file + "preciceParticipantName": "TendonSolverTopA", # name of the own precice participant, has to match the name given in the precice xml config file + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "meshName": "Tendon-Top-A-Mesh", # precice name of the 2D coupling mesh + "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + } + ], + "preciceData": [ + { + "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file + } + ], + "HyperelasticitySolver": config_hyperelasticity, + "DynamicHyperelasticitySolver": config_hyperelasticity, + } +} diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py new file mode 100644 index 000000000..346098aa8 --- /dev/null +++ b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py @@ -0,0 +1,159 @@ +case_name = "default" +precice_config_file = "default" + +# scenario name for log file +scenario_name = "muscle" + +# Fixed units in cellMl models: +# These define the unit system. +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 uA = 1e-6 A +# 1 uF = 1e-6 F +# +# derived units: +# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 +# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg + +# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN +# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS +# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV +# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz +# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 +# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa + +# Hodgkin-Huxley +# t: ms +# STATES[0], Vm: mV +# CONSTANTS[1], Cm: uF*cm^-2 +# CONSTANTS[2], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Shorten +# t: ms +# CONSTANTS[0], Cm: uF*cm^-2 +# STATES[0], Vm: mV +# ALGEBRAIC[32], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Fixed units in mechanics system +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 N +# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa +# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg +# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 +# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 + +# material parameters +# -------------------- +# quantities in mechanics unit system +rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) + +# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) +# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 +# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 + +c1 = 3.176e-10 # [N/cm^2] +c2 = 1.813 # [N/cm^2] +b = 1.075e-2 # [N/cm^2] anisotropy parameter +d = 9.1733 # [-] anisotropy parameter + +material_parameters = [c1, c2, b, d] # material parameters +pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) +#pmax = 0.73 + +# load +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +bottom_traction = [0.0,0.0,0.0] # [N] + +# Monodomain parameters +# -------------------- +# quantities in CellML unit system +Conductivity = 3.828 # [mS/cm] sigma, conductivity +Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) +Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) +# diffusion prefactor = Conductivity/(Am*Cm) + +# timing and activation parameters +# ----------------- +# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" +import random +random.seed(0) # ensure that random numbers are the same on every rank +# radius: [μm], stimulation frequency [Hz], jitter [-] +motor_units = [ + {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers + {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers +] + +# timing parameters +# ----------------- +end_time = 20000.0 # [ms] end time of the simulation +stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. +stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation +dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) +dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) +dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) +dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 +output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D + + +# input files +fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" +fat_mesh_file = fiber_file + "_fat.bin" +firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" +cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" + +# stride for sampling the 3D elements from the fiber data +# a higher number leads to less 3D elements +sampling_stride_x = 2 +sampling_stride_y = 2 +sampling_stride_z = 74 + +# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 +# when a fiber point is still considered part of the element. +# Try to increase this such that all mappings have all points. +mapping_tolerance = 0.5 + +# other options +paraview_output = True +adios_output = False +exfile_output = False +python_output = False +disable_firing_output = False + +# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's +def get_am(fiber_no, mu_no): + # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm + r = motor_units[mu_no]["radius"]*1e-4 + # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r + return 2./r + #return Am + +def get_cm(fiber_no, mu_no): + return Cm + +def get_conductivity(fiber_no, mu_no): + return Conductivity + +def get_specific_states_call_frequency(fiber_no, mu_no): + stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] + return stimulation_frequency*1e-3 + +def get_specific_states_frequency_jitter(fiber_no, mu_no): + #return 0 + return motor_units[mu_no % len(motor_units)]["jitter"] + +def get_specific_states_call_enable_begin(fiber_no, mu_no): + return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py new file mode 100644 index 000000000..59ae52eec --- /dev/null +++ b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py @@ -0,0 +1,259 @@ +# Transversely-isotropic Mooney Rivlin on a tendon geometry +# Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. +import sys, os +import numpy as np +import pickle +import argparse +import sys +sys.path.insert(0, '.') +import variables # file variables.py, defines default values for all parameters, you can set the parameters there +from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain + +# set title of terminal +title = "tendon_top_b" +print('\33]0;{}\a'.format(title), end='', flush=True) + +# material parameters +# -------------------- +# quantities in mechanics unit system +variables.rho = 10 # [1e-4 kg/cm^3] 10 = density of the muscle (density of water) + +# material parameters for Saint Venant-Kirchhoff material +# https://www.researchgate.net/publication/230248067_Bulk_Modulus + +youngs_modulus = 7e4 # [N/cm^2 = 10kPa] +shear_modulus = 3e4 + +#youngs_modulus*=1e-3 +#shear_modulus*=1e-3 + +lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda +mu = shear_modulus # Lamé parameter mu or G (shear modulus) + +variables.material_parameters = [lambd, mu] + +variables.constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +variables.force = 1.0 # [N] + +variables.dt_elasticity = 1 # [ms] time step width for elasticity +variables.end_time = 20000 # [ms] simulation time +variables.scenario_name = "tendon_top_b" +variables.is_bottom_tendon = False # whether the tendon is at the bottom (negative z-direction), this is important for the boundary conditions +variables.output_timestep_3D = 50 # [ms] output timestep + +# input mesh file +#fiber_file = "../../../../input/left_biceps_brachii_tendon1.bin" # bottom tendon +#fiber_file = "../../../../input/left_biceps_brachii_tendon2a.bin" # top tendon +fiber_file = "../../../../input/left_biceps_brachii_tendon2b.bin" +#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" + +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. + +# parse arguments +rank_no = (int)(sys.argv[-2]) +n_ranks = (int)(sys.argv[-1]) + +# define command line arguments +parser = argparse.ArgumentParser(description='tendon') +parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) +parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) +parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) +parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) +parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) +parser.add_argument('-vmodule', help='ignore') + +# parse command line arguments and assign values to variables module +args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) +if len(other_args) != 0 and rank_no == 0: + print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) + +# partitioning +# ------------ +# this has to match the total number of processes +if variables.n_subdomains is not None: + variables.n_subdomains_x = variables.n_subdomains[0] + variables.n_subdomains_y = variables.n_subdomains[1] + variables.n_subdomains_z = variables.n_subdomains[2] + +# compute partitioning +if rank_no == 0: + if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: + print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) + sys.exit(-1) + +# stride for sampling the 3D elements from the fiber data +# here any number is possible +sampling_stride_x = 1 +sampling_stride_y = 1 +sampling_stride_z = 2 + +# create the partitioning using the script in create_partitioned_meshes_for_settings.py +result = create_partitioned_meshes_for_settings( + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + fiber_file, load_fiber_data, + sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) + +[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result + +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z +nx = n_points_3D_mesh_linear_global_x-1 +ny = n_points_3D_mesh_linear_global_y-1 +nz = n_points_3D_mesh_linear_global_z-1 + +node_positions = variables.meshes["3Dmesh_quadratic"]["nodePositions"] + +# boundary conditions (for quadratic elements) +# -------------------------------------------- +[mx, my, mz] = variables.meshes["3Dmesh_quadratic"]["nPointsGlobal"] +[nx, ny, nz] = variables.meshes["3Dmesh_quadratic"]["nElements"] + +# set Dirichlet BC, fix top end of tendon that is attached to the bone +variables.elasticity_dirichlet_bc = {} +k = mz-1 + +# fix the whole x-y plane +for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,0.0,None,None,None] + +# set no Neumann BC +variables.elasticity_neumann_bc = [] + +config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" + "timeStepWidth": variables.dt_elasticity, # time step width + "endTime": variables.end_time, # end time of the simulation time span + "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements + "residualNormLogFilename": "out/tendon_top_b_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written + "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian + + "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian + + # mesh + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings + + "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction + #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + "fiberDirectionInElement": [0,0,1], # if fiberMeshNames and fiberDirections are empty, directly set the constant fiber direction, in element coordinate system + + # nonlinear solver + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver + "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + "snesRebuildJacobianFrequency": 5, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again + + #"dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve + "dumpFilename": "", # dump disabled + "dumpFormat": "matlab", # default, ascii, matlab + + #"loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep + #"loadFactors": [0.5, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + "loadFactorGiveUpThreshold": 1e-3, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the load factors get too small, it aborts the solve. + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called + + # boundary and initial conditions + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements + "divideNeumannBoundaryConditionValuesByTotalArea": False, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied + "updateDirichletBoundaryConditionsFunction": None, # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step + + "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "extrapolateInitialGuess": False, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + "dirichletOutputFilename": "out/tendon_top_b_dirichlet_boundary_conditions", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable + "totalForceLogFilename": "out/tendon_top_b_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. + "OutputWriter" : [ + + # Paraview files + {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_b", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + #{"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, + ], + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter" : [ # output files for displacements function space (quadratic elements) + {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_b_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter" : [ + #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, +} + +config = { + "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file + "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files + "solverStructureDiagramFile": "out/tendon_top_b_solver_structure.txt", # output file of a diagram that shows data connection between solvers + "mappingsBetweenMeshesLogFile": "out/tendon_top_b_mappings_between_meshes_log.txt", # log file for mappings + "Meshes": variables.meshes, + + "PreciceAdapter": { # precice adapter for bottom tendon + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file + "preciceParticipantName": "TendonSolverTopB", # name of the own precice participant, has to match the name given in the precice xml config file + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "meshName": "Tendon-Top-B-Mesh", # precice name of the 2D coupling mesh + "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + } + ], + "preciceData": [ + { + "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + }, + { + "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" + "meshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file + } + ], + "HyperelasticitySolver": config_hyperelasticity, + "DynamicHyperelasticitySolver": config_hyperelasticity, + } +} diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py new file mode 100644 index 000000000..346098aa8 --- /dev/null +++ b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py @@ -0,0 +1,159 @@ +case_name = "default" +precice_config_file = "default" + +# scenario name for log file +scenario_name = "muscle" + +# Fixed units in cellMl models: +# These define the unit system. +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 uA = 1e-6 A +# 1 uF = 1e-6 F +# +# derived units: +# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 +# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg + +# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN +# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS +# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV +# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz +# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 +# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa + +# Hodgkin-Huxley +# t: ms +# STATES[0], Vm: mV +# CONSTANTS[1], Cm: uF*cm^-2 +# CONSTANTS[2], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Shorten +# t: ms +# CONSTANTS[0], Cm: uF*cm^-2 +# STATES[0], Vm: mV +# ALGEBRAIC[32], I_Stim: uA*cm^-2 +# -> all units are consistent + +# Fixed units in mechanics system +# 1 cm = 1e-2 m +# 1 ms = 1e-3 s +# 1 N +# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa +# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg +# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 +# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 + +# material parameters +# -------------------- +# quantities in mechanics unit system +rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) + +# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) +# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 +# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 + +c1 = 3.176e-10 # [N/cm^2] +c2 = 1.813 # [N/cm^2] +b = 1.075e-2 # [N/cm^2] anisotropy parameter +d = 9.1733 # [-] anisotropy parameter + +material_parameters = [c1, c2, b, d] # material parameters +pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) +#pmax = 0.73 + +# load +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +bottom_traction = [0.0,0.0,0.0] # [N] + +# Monodomain parameters +# -------------------- +# quantities in CellML unit system +Conductivity = 3.828 # [mS/cm] sigma, conductivity +Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) +Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) +# diffusion prefactor = Conductivity/(Am*Cm) + +# timing and activation parameters +# ----------------- +# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" +import random +random.seed(0) # ensure that random numbers are the same on every rank +# radius: [μm], stimulation frequency [Hz], jitter [-] +motor_units = [ + {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers + {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, + {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers +] + +# timing parameters +# ----------------- +end_time = 20000.0 # [ms] end time of the simulation +stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. +stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation +dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) +dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) +dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) +dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 +output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D + + +# input files +fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" +#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" +fat_mesh_file = fiber_file + "_fat.bin" +firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" +cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" + +# stride for sampling the 3D elements from the fiber data +# a higher number leads to less 3D elements +sampling_stride_x = 2 +sampling_stride_y = 2 +sampling_stride_z = 74 + +# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 +# when a fiber point is still considered part of the element. +# Try to increase this such that all mappings have all points. +mapping_tolerance = 0.5 + +# other options +paraview_output = True +adios_output = False +exfile_output = False +python_output = False +disable_firing_output = False + +# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's +def get_am(fiber_no, mu_no): + # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm + r = motor_units[mu_no]["radius"]*1e-4 + # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r + return 2./r + #return Am + +def get_cm(fiber_no, mu_no): + return Cm + +def get_conductivity(fiber_no, mu_no): + return Conductivity + +def get_specific_states_call_frequency(fiber_no, mu_no): + stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] + return stimulation_frequency*1e-3 + +def get_specific_states_frequency_jitter(fiber_no, mu_no): + #return 0 + return motor_units[mu_no % len(motor_units)]["jitter"] + +def get_specific_states_call_enable_begin(fiber_no, mu_no): + return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file From 3eb5bb08430ed743221dd78c85e0da5841f368b1 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Tue, 27 Feb 2024 23:45:53 +0100 Subject: [PATCH 05/40] Update README --- muscle-tendon-complex/README.md | 78 +++++++++++++-------------------- 1 file changed, 30 insertions(+), 48 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 77dc5090e..12140e7c4 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -1,7 +1,7 @@ --- title: Muscle-tendon complex permalink: tutorials-muscle-tendon-complex.html -keywords: multi-coupling, OpenDiHu, deal.II, skeletal muscle +keywords: multi-coupling, OpenDiHu, skeletal muscle summary: In this case, an skeletal muscle (biceps) and three tendons are coupled together using a fully-implicit multi-coupling scheme. --- @@ -19,14 +19,11 @@ The muscle participant (in red), is connected to three tendons. The muscle sends The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not match perfectly. - -TODO: how is the muscle activated! - -TODO: Add related case? multiple-perpendicular flap? +TODO: Explain how is the muscle activated! ## Why multi-coupling? -This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant to participant manner. However, such a composition is not suited for combining multiple strong fluid-structure interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. +This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant to participant manner. However, such a composition is not suited for combining multiple strong fluid-structure interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. For another multi-coupling tutorial, you can refer to the [multiple perpendicular flaps tutorial](http://precice.org/tutorials-multiple-perpendicular-flaps.html). We can set this in our `precice-config.xml`: @@ -41,74 +38,59 @@ We can set this in our `precice-config.xml`: The participant that has the control is the one that it is connected to all other participants. This is why we have chosen the muscle participant for this task. -## About the Solvers TODO - -For the fluid participant we use OpenFOAM. In particular, we use the application `pimpleFoam`. The geometry of the Fluid participant is defined in the file `Fluid/system/blockMeshDict`. Besides, we must specify where are we exchanging data with the other participants. The interfaces are set in the file `Fluid/system/preciceDict`. In this file, we set to exchange stress and displacement on the surface of each flap. - -Most of the coupling details are specified in the file `precice-config.xml`. Here we estipulate the order in which we read/write data from one participant to another or how we map from the fluid to the solid's mesh. In particular, we have chosen the nearest-neighbor mapping scheme. - -For the simulation of the solid participants we use the deal.II adapter. In deal.II, the geometry of the domain is specified directly on the solver. The two flaps in our case are essentially the same but for the x-coordinate. The flap geometry is given to the solver when we select the scenario in the '.prm' file. - -```text -set Scenario = PF -``` - -But to specify the position of the flap along the x-axis, we must specify it in the `solid-upstream-dealii/parameters.prm` file as follows: - -```text -set Flap location = -1.0 -``` +## About the solvers -While in case of `solid-downstream-dealii/parameters.prm` we write: +OpenDiHu is used for the muscle and each tendon participants. +The muscle solver consists of a ... TODO +The tendon solver consists of a ... TODO -```text -set Flap location = 1.0 -``` +## Running the Simulation -The scenario settings are implemented similarly for the nonlinear case. +1. Preparation: ... TODO + - Install OpenDiHu + - Download input files for OpenDiHu + - Setup `$OPENDIHU_HOME` to your `.bashrc` file + - Compile muscle and tendon solvers -## Running the Simulation TODO + ```bash + cd opendihu-solver + ./build.sh + ``` + - Move executables to participants directory -1. Preparation: - To run the coupled simulation, copy the deal.II executable `elasticity` into the main folder. To learn how to obtain the deal.II executable take a look at the description on the [deal.II-adapter page](https://www.precice.org/adapter-dealii-overview.html). 2. Starting: We are going to run each solver in a different terminal. It is important that first we navigate to the simulation directory so that all solvers start in the same directory. - To start the `Fluid` participant, run: + To start the `Muscle` participant, run: ```bash - cd fluid-openfoam + cd muscle-opendihu ./run.sh ``` - - to start OpenFOAM in serial or + To start the `Tendon-Bottom` participant, run: ```bash - cd fluid-openfoam - ./run.sh -parallel + cd tendon-bottom-opendihu + ./run.sh ``` - for a parallel run. - - The solid participants are only designed for serial runs. To run the `Solid-Upstream` participant, execute the corresponding deal.II binary file e.g. by: + To start the `Tendon-Top-A` participant, run: ```bash - cd solid-upstream-dealii + cd tendon-top-A-opendihu ./run.sh ``` - Finally, in the third terminal we will run the solver for the `Solid-Downstream` participant by: + Finally, to start the `Tendon-Top-B` participant, run: - ```bash - cd solid-downstream-dealii + ```bash + cd tendon-top-B-opendihu ./run.sh ``` -## Postprocessing TODO - -After the simulation has finished, you can visualize your results using e.g. ParaView. Fluid results are in the OpenFOAM format and you may load the `fluid-openfoam.foam` file. Looking at the fluid results is enough to obtain information about the behaviour of the flaps. You can also visualize the solid participants' vtks though. +## Postprocessing... TODO -![Example visualization](images/tutorials-multiple-perpendicular-flaps-results.png) +After the simulation has finished, you can visualize your results using e.g. ParaView. ## References TODO From b559c85c2977c8f349fdefe57b1abef6af078fce Mon Sep 17 00:00:00 2001 From: carme-hp Date: Thu, 29 Feb 2024 20:16:25 +0100 Subject: [PATCH 06/40] Get muscle participant to run --- .../muscle-opendihu/settings-muscle.py | 11 +- .../muscle-opendihu/variables/variables.py | 238 +++++++++--------- 2 files changed, 118 insertions(+), 131 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py index d305446c2..ad8630d58 100644 --- a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -80,11 +80,6 @@ if len(other_args) != 0 and rank_no == 0: print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) -# initialize some dependend variables -if variables.n_subdomains is not None: - variables.n_subdomains_x = variables.n_subdomains[0] - variables.n_subdomains_y = variables.n_subdomains[1] - variables.n_subdomains_z = variables.n_subdomains[2] variables.n_subdomains = variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z @@ -202,8 +197,8 @@ "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console "timestepWidth": 1, # coupling time step width, must match the value in the precice config "couplingEnabled": variables.enable_coupling, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file - "preciceParticipantName": "MuscleSolver", # name of the own precice participant, has to match the name given in the precice xml config file + "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file + "preciceParticipantName": "Muscle", # name of the own precice participant, has to match the name given in the precice xml config file "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver @@ -559,4 +554,4 @@ # stop timer and calculate how long parsing lasted if rank_no == 0: t_stop_script = timeit.default_timer() - print("Python config parsed in {:.1f}s.".format(t_stop_script - t_start_script)) \ No newline at end of file + print("Python config parsed in {:.1f}s.".format(t_stop_script - t_start_script)) diff --git a/muscle-tendon-complex/muscle-opendihu/variables/variables.py b/muscle-tendon-complex/muscle-opendihu/variables/variables.py index 346098aa8..75740c47d 100644 --- a/muscle-tendon-complex/muscle-opendihu/variables/variables.py +++ b/muscle-tendon-complex/muscle-opendihu/variables/variables.py @@ -1,145 +1,83 @@ -case_name = "default" -precice_config_file = "default" - -# scenario name for log file -scenario_name = "muscle" - -# Fixed units in cellMl models: -# These define the unit system. -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 uA = 1e-6 A -# 1 uF = 1e-6 F -# -# derived units: -# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 -# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg - -# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN -# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS -# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV -# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz -# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 -# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa - -# Hodgkin-Huxley -# t: ms -# STATES[0], Vm: mV -# CONSTANTS[1], Cm: uF*cm^-2 -# CONSTANTS[2], I_Stim: uA*cm^-2 -# -> all units are consistent - -# Shorten -# t: ms -# CONSTANTS[0], Cm: uF*cm^-2 -# STATES[0], Vm: mV -# ALGEBRAIC[32], I_Stim: uA*cm^-2 -# -> all units are consistent - -# Fixed units in mechanics system -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 N -# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa -# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg -# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 -# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 - # material parameters # -------------------- -# quantities in mechanics unit system -rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) - -# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) -# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 -# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 c1 = 3.176e-10 # [N/cm^2] c2 = 1.813 # [N/cm^2] b = 1.075e-2 # [N/cm^2] anisotropy parameter d = 9.1733 # [-] anisotropy parameter - material_parameters = [c1, c2, b, d] # material parameters -pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) -#pmax = 0.73 - -# load -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -bottom_traction = [0.0,0.0,0.0] # [N] - -# Monodomain parameters -# -------------------- -# quantities in CellML unit system -Conductivity = 3.828 # [mS/cm] sigma, conductivity -Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) -Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) -# diffusion prefactor = Conductivity/(Am*Cm) - -# timing and activation parameters -# ----------------- -# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" -import random -random.seed(0) # ensure that random numbers are the same on every rank -# radius: [μm], stimulation frequency [Hz], jitter [-] -motor_units = [ - {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers - {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers -] +pmax = 7.3 # maximum stress [N/cm^2] +Conductivity = 3.828 # sigma, conductivity [mS/cm] +Am = 500.0 # surface area to volume ratio [cm^-1] +Cm = 0.58 # membrane capacitance [uF/cm^2] +innervation_zone_width = 0. # not used [cm], this will later be used to specify a variance of positions of the innervation point at the fibers +rho = 10 +# solvers +# ------- +diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation +diffusion_preconditioner_type = "none" # preconditioner +potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_preconditioner_type = "none" # preconditioner +emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_preconditioner_type = "none" # preconditioner +emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution # timing parameters # ----------------- -end_time = 20000.0 # [ms] end time of the simulation +end_time = 1000.0 # [ms] end time of the simulation stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. -stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation -dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) -dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) -dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) -dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG -output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 -output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D - +dt_0D = 1e-3 # [ms] timestep width of ODEs +dt_1D = 1.5e-3 # [ms] timestep width of diffusion +dt_splitting = 3e-3 # [ms] overall timestep width of strang splitting +dt_3D = 1e0 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +output_timestep = 1e0 # [ms] timestep for output files +output_timestep_3D_emg = 1e0 # [ms] timestep for output files +output_timestep_3D = 1e0 # [ms] timestep for output files +output_timestep_fibers = 1e0 # [ms] timestep for output files +activation_start_time = 0 # [ms] time when to start checking for stimulation # input files -fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" +# ----------- + +import os +opendihu_home = os.environ.get('OPENDIHU_HOME') +fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_9x9fibers.bin" fat_mesh_file = fiber_file + "_fat.bin" -firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency -fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" -cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +fiber_distribution_file = opendihu_home + "/examples/electrophysiology/input/MU_fibre_distribution_10MUs.txt" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_real.txt" +precice_config_file = "../precice-config.xml" +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +debug_output = False # verbose output in this python script, for debugging the domain decomposition +disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space +paraview_output = False # If the paraview output writer should be enabled +adios_output = False # If the MegaMol/ADIOS output writer should be enabled +python_output = False # If the Python output writer should be enabled +exfile_output = False # If the Exfile output writer should be enabled + +# partitioning +# ------------ +# this has to match the total number of processes +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 # stride for sampling the 3D elements from the fiber data -# a higher number leads to less 3D elements +# here any number is possible sampling_stride_x = 2 sampling_stride_y = 2 -sampling_stride_z = 74 +sampling_stride_z = 50 -# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 -# when a fiber point is still considered part of the element. -# Try to increase this such that all mappings have all points. -mapping_tolerance = 0.5 +mapping_tolerance = 0.1 -# other options -paraview_output = True -adios_output = False -exfile_output = False -python_output = False -disable_firing_output = False +# scenario name for log file +scenario_name = "" # functions, here, Am, Cm and Conductivity are constant for all fibers and MU's +# These functions can be redefined differently in a custom variables script def get_am(fiber_no, mu_no): - # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm - r = motor_units[mu_no]["radius"]*1e-4 - # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r - return 2./r - #return Am + return Am def get_cm(fiber_no, mu_no): return Cm @@ -148,12 +86,66 @@ def get_conductivity(fiber_no, mu_no): return Conductivity def get_specific_states_call_frequency(fiber_no, mu_no): - stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] - return stimulation_frequency*1e-3 + return stimulation_frequency def get_specific_states_frequency_jitter(fiber_no, mu_no): - #return 0 - return motor_units[mu_no % len(motor_units)]["jitter"] + return [0] def get_specific_states_call_enable_begin(fiber_no, mu_no): - return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file + return activation_start_time + + +# further internal variables that will be set by the helper.py script and used in the config in settings_fibers_emg.py +n_fibers_total = None +n_subdomains_xy = None +own_subdomain_coordinate_x = None +own_subdomain_coordinate_y = None +own_subdomain_coordinate_z = None +n_fibers_x = None +n_fibers_y = None +n_points_whole_fiber = None +n_points_3D_mesh_global_x = None +n_points_3D_mesh_global_y = None +n_points_3D_mesh_global_z = None +output_writer_fibers = None +output_writer_emg = None +output_writer_0D_states = None +states_output = False +parameters_used_as_algebraic = None +parameters_used_as_constant = None +parameters_initial_values = None +output_algebraic_index = None +output_state_index = None +nodal_stimulation_current = None +fiber_file_handle = None +fibers = None +fiber_distribution = None +firing_times = None +n_fibers_per_subdomain_x = None +n_fibers_per_subdomain_y = None +n_points_per_subdomain_z = None +z_point_index_start = None +z_point_index_end = None +meshes = None +potential_flow_dirichlet_bc = None +elasticity_dirichlet_bc = None +elasticity_neumann_bc = None +fibers_on_own_rank = None +n_fiber_nodes_on_subdomain = None +fiber_start_node_no = None +generate_linear_3d_mesh = False +generate_quadratic_3d_mesh = True +nx = None +ny = None +nz = None +constant_body_force = None +bottom_traction = None +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 +states_initial_values = [] +enable_coupling = True +enable_force_length_relation = True +lambda_dot_scaling_factor = 1 +mappings = None +vm_value_stimulated = None \ No newline at end of file From 1b4f1fe240bc3e51a19bc29a243a825696933807 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 4 Mar 2024 20:47:36 +0100 Subject: [PATCH 07/40] Add check for OPENDIHU_HOME --- muscle-tendon-complex/opendihu-solver/build.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 muscle-tendon-complex/opendihu-solver/build.sh diff --git a/muscle-tendon-complex/opendihu-solver/build.sh b/muscle-tendon-complex/opendihu-solver/build.sh new file mode 100644 index 000000000..7d1a1ea13 --- /dev/null +++ b/muscle-tendon-complex/opendihu-solver/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +if [ -n $OPENDIHU_HOME ] +then + alias sr='$OPENDIHU_HOME/scripts/shortcuts/sr.sh' + alias mkorn='$OPENDIHU_HOME/scripts/shortcuts/mkorn.sh' + mkorn && sr + # copy executables to partipant folders + cp build_release/muscle-solver ../muscle-opendihu/ + cp build_release/tendon-solver ../tendon-bottom-opendihu/ + cp build_release/tendon-solver ../tendon-top-A-opendihu/ + cp build_release/tendon-solver ../tendon-top-B-opendihu/ +else + echo "OPENDIHU_HOME is not defined" +fi \ No newline at end of file From 0cb1947ea9a8b52fa88db1a4b5f22c35e653b6fc Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 4 Mar 2024 21:02:11 +0100 Subject: [PATCH 08/40] Rename meshName to preciceMeshName --- .../muscle-opendihu/settings-muscle.py | 26 +++++++++---------- .../settings-tendon-bottom.py | 8 +++--- .../settings-tendon-top-A.py | 8 +++--- .../settings-tendon-top-B.py | 8 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py index ad8630d58..7d696bddd 100644 --- a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -203,50 +203,50 @@ "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver { - "meshName": "Muscle-Bottom-Mesh", # precice name of the 2D coupling mesh + "preciceMeshName": "Muscle-Bottom-Mesh", # precice name of the 2D coupling mesh "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top }, { - "meshName": "Muscle-Top-A-Mesh", # precice name of the 2D coupling mesh + "preciceMeshName": "Muscle-Top-A-Mesh", # precice name of the 2D coupling mesh "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top }, { - "meshName": "Muscle-Top-B-Mesh", # precice name of the 2D coupling mesh + "preciceMeshName": "Muscle-Top-B-Mesh", # precice name of the 2D coupling mesh "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top }, ], "preciceData": [ { "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file }, ], @@ -329,7 +329,7 @@ "mappings": variables.mappings, # mappings between parameters and algebraics/constants and between outputConnectorSlots and states, algebraics or parameters, they are defined in helper.py "parametersInitialValues": variables.parameters_initial_values, #[0.0, 1.0], # initial values for the parameters: I_Stim, l_hs - "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh + "preciceMeshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh "stimulationLogFilename": "out/stimulation.log", # a file that will contain the times of stimulations }, "OutputWriter" : [ @@ -367,7 +367,7 @@ "FiniteElementMethod" : { "inputMeshIsGlobal": True, - "meshName": "MeshFiber_{}".format(fiber_no), + "preciceMeshName": "MeshFiber_{}".format(fiber_no), "solverName": "diffusionTermSolver", "prefactor": get_diffusion_prefactor(fiber_no, motor_unit_no), # resolves to Conductivity / (Am * Cm) "slotName": None, @@ -408,7 +408,7 @@ [{ "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction "PrescribedValues": { - "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh + "preciceMeshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh "numberTimeSteps": 1, # number of timesteps to call the callback functions subsequently, this is usually 1 for prescribed values, because it is enough to set the reaction term only once per time step "timeStepOutputInterval": 20, # if the time step should be written to console, a value > 10 produces no output "slotNames": [], # names of the data connector slots @@ -484,7 +484,7 @@ # mesh "inputMeshIsGlobal": True, # the mesh is given locally - "meshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config + "preciceMeshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config "fiberMeshNames": variables.fiber_mesh_names, # fiber meshes that will be used to determine the fiber direction, for multidomain there are no fibers so this would be empty list #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py index 0ff101778..5c4ac4ea0 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py @@ -366,7 +366,7 @@ def postprocess(result): # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian # mesh - "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "preciceMeshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction @@ -466,20 +466,20 @@ def postprocess(result): "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver { - "meshName": "Tendon-Bottom-Mesh", # precice name of the 2D coupling mesh + "preciceMeshName": "Tendon-Bottom-Mesh", # precice name of the 2D coupling mesh "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top } ], "preciceData": [ { "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file } ], diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py index 4452481fb..fc9f56694 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py @@ -141,7 +141,7 @@ # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian # mesh - "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "preciceMeshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction @@ -237,20 +237,20 @@ "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver { - "meshName": "Tendon-Top-A-Mesh", # precice name of the 2D coupling mesh + "preciceMeshName": "Tendon-Top-A-Mesh", # precice name of the 2D coupling mesh "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top } ], "preciceData": [ { "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file } ], diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py index 59ae52eec..dafd77ff9 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py @@ -140,7 +140,7 @@ # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian # mesh - "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "preciceMeshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction @@ -236,20 +236,20 @@ "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver { - "meshName": "Tendon-Top-B-Mesh", # precice name of the 2D coupling mesh + "preciceMeshName": "Tendon-Top-B-Mesh", # precice name of the 2D coupling mesh "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top } ], "preciceData": [ { "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file }, { "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "meshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file } ], From bcbc6fd286d0d055d3a9fba99fbcb21423b17fa7 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 4 Mar 2024 21:35:03 +0100 Subject: [PATCH 09/40] Fix rename --- muscle-tendon-complex/muscle-opendihu/settings-muscle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py index 7d696bddd..26606ce47 100644 --- a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -329,7 +329,7 @@ "mappings": variables.mappings, # mappings between parameters and algebraics/constants and between outputConnectorSlots and states, algebraics or parameters, they are defined in helper.py "parametersInitialValues": variables.parameters_initial_values, #[0.0, 1.0], # initial values for the parameters: I_Stim, l_hs - "preciceMeshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh + "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh "stimulationLogFilename": "out/stimulation.log", # a file that will contain the times of stimulations }, "OutputWriter" : [ @@ -367,7 +367,7 @@ "FiniteElementMethod" : { "inputMeshIsGlobal": True, - "preciceMeshName": "MeshFiber_{}".format(fiber_no), + "meshName": "MeshFiber_{}".format(fiber_no), "solverName": "diffusionTermSolver", "prefactor": get_diffusion_prefactor(fiber_no, motor_unit_no), # resolves to Conductivity / (Am * Cm) "slotName": None, @@ -408,7 +408,7 @@ [{ "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction "PrescribedValues": { - "preciceMeshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh + "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh "numberTimeSteps": 1, # number of timesteps to call the callback functions subsequently, this is usually 1 for prescribed values, because it is enough to set the reaction term only once per time step "timeStepOutputInterval": 20, # if the time step should be written to console, a value > 10 produces no output "slotNames": [], # names of the data connector slots @@ -484,7 +484,7 @@ # mesh "inputMeshIsGlobal": True, # the mesh is given locally - "preciceMeshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config + "meshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config "fiberMeshNames": variables.fiber_mesh_names, # fiber meshes that will be used to determine the fiber direction, for multidomain there are no fibers so this would be empty list #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system From ff5ed786bc6adb7cfb3d5771769b458876540989 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 4 Mar 2024 23:00:05 +0100 Subject: [PATCH 10/40] Fix paths, variables and add scripts --- .../muscle-opendihu/clean.sh | 4 + .../muscle-opendihu/helper.py | 457 ++++++++++++++++++ muscle-tendon-complex/muscle-opendihu/run.sh | 2 + .../opendihu-solver/clean.sh | 8 + .../tendon-bottom-opendihu/clean.sh | 4 + .../tendon-bottom-opendihu/run.sh | 2 + .../settings-tendon-bottom.py | 269 ++--------- .../variables/variables.py | 251 ++++------ .../tendon-top-A-opendihu/clean.sh | 4 + .../tendon-top-A-opendihu/run.sh | 2 + .../settings-tendon-top-A.py | 87 ++-- .../variables/variables.py | 251 ++++------ .../tendon-top-B-opendihu/clean.sh | 4 + .../tendon-top-B-opendihu/run.sh | 2 + .../settings-tendon-top-B.py | 84 ++-- .../variables/variables.py | 251 ++++------ 16 files changed, 889 insertions(+), 793 deletions(-) create mode 100644 muscle-tendon-complex/muscle-opendihu/clean.sh create mode 100644 muscle-tendon-complex/muscle-opendihu/helper.py create mode 100644 muscle-tendon-complex/muscle-opendihu/run.sh create mode 100644 muscle-tendon-complex/tendon-bottom-opendihu/clean.sh create mode 100644 muscle-tendon-complex/tendon-bottom-opendihu/run.sh create mode 100644 muscle-tendon-complex/tendon-top-A-opendihu/clean.sh create mode 100644 muscle-tendon-complex/tendon-top-A-opendihu/run.sh create mode 100644 muscle-tendon-complex/tendon-top-B-opendihu/clean.sh create mode 100644 muscle-tendon-complex/tendon-top-B-opendihu/run.sh diff --git a/muscle-tendon-complex/muscle-opendihu/clean.sh b/muscle-tendon-complex/muscle-opendihu/clean.sh new file mode 100644 index 000000000..fafa9d5e0 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/clean.sh @@ -0,0 +1,4 @@ +#!/bin/sh +rm -r precice-profiling +rm -r __pycache__ +rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/muscle-opendihu/helper.py b/muscle-tendon-complex/muscle-opendihu/helper.py new file mode 100644 index 000000000..3dc1a5c35 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/helper.py @@ -0,0 +1,457 @@ +# Multiple 1D fibers (monodomain) with 3D contraction, biceps geometry +# This is a helper script that sets a lot of the internal variables which are all defined in variables.py +# +# if variables.fiber_file=cuboid.bin, it uses a small cuboid test example + +import numpy as np +import pickle +import sys,os +import struct +import argparse +#sys.path.insert(0, '..') +import variables # file variables.py +from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings + +# parse arguments +rank_no = (int)(sys.argv[-2]) +n_ranks = (int)(sys.argv[-1]) + +# generate cuboid fiber file +if "cuboid.bin" in variables.fiber_file: + + if variables.n_fibers_y is None: + variables.n_fibers_x = 4 + variables.n_fibers_y = variables.n_fibers_x + variables.n_points_whole_fiber = 20 + + size_x = variables.n_fibers_x * 0.1 + size_y = variables.n_fibers_y * 0.1 + size_z = variables.n_points_whole_fiber / 100. + + if rank_no == 0: + print("create cuboid.bin with size [{},{},{}], n points [{},{},{}]".format(size_x, size_y, size_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber)) + + # write header + with open(variables.fiber_file, "wb") as outfile: + + # write header + header_str = "opendihu self-generated cuboid " + outfile.write(struct.pack('32s',bytes(header_str, 'utf-8'))) # 32 bytes + outfile.write(struct.pack('i', 40)) # header length + outfile.write(struct.pack('i', variables.n_fibers_x*variables.n_fibers_y)) # n_fibers + outfile.write(struct.pack('i', variables.n_points_whole_fiber)) # variables.n_points_whole_fiber + outfile.write(struct.pack('i', 0)) # nBoundaryPointsXNew + outfile.write(struct.pack('i', 0)) # nBoundaryPointsZNew + outfile.write(struct.pack('i', 0)) # nFineGridFibers_ + outfile.write(struct.pack('i', 1)) # nRanks + outfile.write(struct.pack('i', 1)) # nRanksZ + outfile.write(struct.pack('i', 0)) # nFibersPerRank + outfile.write(struct.pack('i', 0)) # date + + # loop over points + for y in range(variables.n_fibers_y): + for x in range(variables.n_fibers_x): + for z in range(variables.n_points_whole_fiber): + point = [x*(float)(size_x)/(variables.n_fibers_x), y*(float)(size_y)/(variables.n_fibers_y), z*(float)(size_z)/(variables.n_points_whole_fiber)] + outfile.write(struct.pack('3d', point[0], point[1], point[2])) # data point + +# output diffusion solver type +if rank_no == 0: + print("diffusion solver type: {}".format(variables.diffusion_solver_type)) + +variables.load_fiber_data = False # load all local node positions from fiber_file, in order to infer partitioning for fat_layer mesh + +# create the partitioning using the script in create_partitioned_meshes_for_settings.py +result = create_partitioned_meshes_for_settings( + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + variables.fiber_file, variables.load_fiber_data, + variables.sampling_stride_x, variables.sampling_stride_y, variables.sampling_stride_z, variables.generate_linear_3d_mesh, variables.generate_quadratic_3d_mesh) +[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result + +variables.n_subdomains_xy = variables.n_subdomains_x * variables.n_subdomains_y +variables.n_fibers_total = variables.n_fibers_x * variables.n_fibers_y + +# create mappings between meshes +#variables.mappings_between_meshes = {"MeshFiber_{}".format(i) : "3Dmesh" for i in range(variables.n_fibers_total)} +variables.mappings_between_meshes = {"MeshFiber_{}".format(i) : {"name": "3Dmesh", "xiTolerance": 1e-3} for i in range(variables.n_fibers_total)} + +# a higher tolerance includes more fiber dofs that may be almost out of the 3D mesh +variables.mappings_between_meshes = { + "MeshFiber_{}".format(i) : { + "name": "3Dmesh_quadratic", + "xiTolerance": variables.mapping_tolerance, + "enableWarnings": False, + "compositeUseOnlyInitializedMappings": False, + "fixUnmappedDofs": True, + "defaultValue": 0, + } for i in range(variables.n_fibers_total) +} +# set output writer +variables.output_writer_fibers = [] +variables.output_writer_elasticity = [] +variables.output_writer_emg = [] +variables.output_writer_0D_states = [] + +subfolder = "" +if variables.paraview_output: + if variables.adios_output: + subfolder = "paraview/" + variables.output_writer_emg.append({"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "Paraview", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) + if variables.states_output: + variables.output_writer_0D_states.append({"format": "Paraview", "outputInterval": 1, "filename": "out/" + subfolder + variables.scenario_name + "/0D_states", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) + +if variables.adios_output: + if variables.paraview_output: + subfolder = "adios/" + variables.output_writer_emg.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "useFrontBackBuffer": False, "combineNInstances": 1, "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "useFrontBackBuffer": False, "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "combineNInstances": variables.n_subdomains_xy, "useFrontBackBuffer": False, "fileNumbering": "incremental"}) + #variables.output_writer_fibers.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + variables.scenario_name + "/fibers", "combineNInstances": 1, "useFrontBackBuffer": False, "fileNumbering": "incremental"} + +if variables.python_output: + if variables.adios_output: + subfolder = "python/" + variables.output_writer_emg.append({"format": "PythonFile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "binary": True, "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "PythonFile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "binary": True, "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "PythonFile", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "binary": True, "fileNumbering": "incremental"}) + +if variables.exfile_output: + if variables.adios_output: + subfolder = "exfile/" + variables.output_writer_emg.append({"format": "Exfile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "Exfile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "Exfile", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "fileNumbering": "incremental"}) + +# set variable mappings for cellml model +if "hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" in variables.cellml_file: # hodgkin huxley membrane with fatigue from shorten + # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} + variables.mappings = { + ("parameter", 0): ("constant", "membrane/i_Stim"), # parameter 0 is I_stim + ("parameter", 1): ("constant", "razumova/l_hs"), # parameter 1 is fiber stretch λ + ("parameter", 2): ("constant", "razumova/velocity"), # parameter 2 is fiber contraction velocity \dot{λ} + ("connectorSlot", "vm"): ("state", "membrane/V"), # expose state Vm to the operator splitting + ("connectorSlot", "stress"):("algebraic", "razumova/activestress"), # expose algebraic γ to the operator splitting + ("connectorSlot", "alpha"): ("algebraic", "razumova/activation"), # expose algebraic α to the operator splitting + + ("connectorSlot", "lambda"):"razumova/l_hs", # connect output "lamda" of mechanics solver to parameter 1 (l_hs) + ("connectorSlot", "ldot"): "razumova/velocity", # connect output "ldot" of mechanics solver to parameter 2 (rel_velo) + } + variables.parameters_initial_values = [0, 1, 0] # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/rel_velo = \dot{λ} + variables.nodal_stimulation_current = 40. # not used + variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.enable_force_length_relation = False # disable computation of force-length relation in opendihu, as it is carried out in CellML model + variables.lambda_dot_scaling_factor = 7.815e-05 # scaling factor to convert dimensionless contraction velocity to shortening velocity, velocity = factor*\dot{lambda} + +elif "hodgkin_huxley" in variables.cellml_file: + # parameters: I_stim + variables.mappings = { + ("parameter", 0): ("constant", "membrane/i_Stim"), # parameter 0 is constant 2 = I_stim + ("connectorSlot", "vm"): "membrane/V", # expose state 0 = Vm to the operator splitting + } + variables.parameters_initial_values = [0.0] # initial value for stimulation current + variables.nodal_stimulation_current = 40. # not used + variables.vm_value_stimulated = 20. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + +elif "shorten" in variables.cellml_file: + # parameters: stimulation current I_stim, fiber stretch λ + variables.mappings = { + ("parameter", 0): ("algebraic", "wal_environment/I_HH"), # parameter is algebraic 32 + ("parameter", 1): ("constant", "razumova/L_x"), # parameter is constant 65, fiber stretch λ, this indicates how much the fiber has stretched, 1 means no extension + ("connectorSlot", "vm"): "wal_environment/vS", # expose state 0 = Vm to the operator splitting + } + variables.parameters_initial_values = [0.0, 1.0] # stimulation current I_stim, fiber stretch λ + variables.nodal_stimulation_current = 1200. # not used + variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + +elif "slow_TK_2014" in variables.cellml_file: # this is (3a, "MultiPhysStrain", old tomo mechanics) in OpenCMISS + # parameters: I_stim, fiber stretch λ + variables.mappings = { + ("parameter", 0): ("constant", "wal_environment/I_HH"), # parameter 0 is constant 54 = I_stim + ("parameter", 1): ("constant", "razumova/L_S"), # parameter 1 is constant 67 = fiber stretch λ + ("connectorSlot", "vm"): "wal_environment/vS", # expose state 0 = Vm to the operator splitting + ("connectorSlot", "stress"):"razumova/stress", # expose algebraic 12 = γ to the operator splitting + } + variables.parameters_initial_values = [0.0, 1.0] # wal_environment/I_HH = I_stim, razumova/L_S = λ + variables.nodal_stimulation_current = 40. # not used + variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + +elif "Aliev_Panfilov_Razumova_2016_08_22" in variables.cellml_file : # this is (3, "MultiPhysStrain", numerically more stable) in OpenCMISS, this only computes A1,A2,x1,x2 not the stress + # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} + variables.mappings = { + ("parameter", 0): ("constant", "Aliev_Panfilov/I_HH"), # parameter 0 is constant 0 = I_stim + ("parameter", 1): ("constant", "Razumova/l_hs"), # parameter 1 is constant 8 = fiber stretch λ + ("parameter", 2): ("constant", "Razumova/velo"), # parameter 2 is constant 9 = fiber contraction velocity \dot{λ} + ("connectorSlot", "vm"): "Aliev_Panfilov/V_m", # expose state 0 = Vm to the operator splitting + ("connectorSlot", "stress"):"Razumova/sigma", # expose algebraic 0 = γ to the operator splitting + } + variables.parameters_initial_values = [0, 1, 0] # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/velo = \dot{λ} + variables.nodal_stimulation_current = 40. # not used + variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + +elif "Aliev_Panfilov_Razumova_Titin" in variables.cellml_file: # this is (4, "Titin") in OpenCMISS + # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} + variables.mappings = { + ("parameter", 0): ("constant", "Aliev_Panfilov/I_HH"), # parameter 0 is constant 0 = I_stim + ("parameter", 1): ("constant", "Razumova/l_hs"), # parameter 1 is constant 11 = fiber stretch λ + ("parameter", 2): ("constant", "Razumova/rel_velo"), # parameter 2 is constant 12 = fiber contraction velocity \dot{λ} + ("connectorSlot", "vm"): ("state", "Aliev_Panfilov/V_m"), # expose state 0 = Vm to the operator splitting + ("connectorSlot", "stress"):("algebraic", "Razumova/ActiveStress"), # expose algebraic 4 = γ to the operator splitting + ("connectorSlot", "alpha"): ("algebraic", "Razumova/Activation"), # expose algebraic 5 = α to the operator splitting + } + variables.parameters_initial_values = [0, 1, 0] # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/rel_velo = \dot{λ} + variables.nodal_stimulation_current = 40. # not used + variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + + +# callback functions +# -------------------------- +def get_motor_unit_no(fiber_no): + return int(variables.fiber_distribution[fiber_no % len(variables.fiber_distribution)]-1) + +def get_diffusion_prefactor(fiber_no, mu_no): + diffusion_prefactor = variables.get_conductivity(fiber_no, mu_no) / (variables.get_am(fiber_no, mu_no) * variables.get_cm(fiber_no, mu_no)) + #print("diffusion_prefactor: {}/({}*{}) = {}".format(variables.get_conductivity(fiber_no, mu_no), variables.get_am(fiber_no, mu_no), variables.get_cm(fiber_no, mu_no), diffusion_prefactor)) + return diffusion_prefactor + +def fiber_gets_stimulated(fiber_no, frequency, current_time): + """ + determine if fiber fiber_no gets stimulated at simulation time current_time + """ + + # determine motor unit + alpha = 1.0 # 0.8 + mu_no = (int)(get_motor_unit_no(fiber_no)*alpha) + + # determine if fiber fires now + index = int(np.round(current_time * frequency)) + n_firing_times = np.size(variables.firing_times,0) + + #if variables.firing_times[index % n_firing_times, mu_no] == 1: + #print("{}: fiber {} is mu {}, t = {}, row: {}, stimulated: {} {}".format(rank_no, fiber_no, mu_no, current_time, (index % n_firing_times), variables.firing_times[index % n_firing_times, mu_no], "true" if variables.firing_times[index % n_firing_times, mu_no] == 1 else "false")) + + return variables.firing_times[index % n_firing_times, mu_no] == 1 + +def set_parameters(n_nodes_global, time_step_no, current_time, parameters, dof_nos_global, fiber_no): + + # determine if fiber gets stimulated at the current time + is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) + + # determine nodes to stimulate (center node, left and right neighbour) + innervation_zone_width_n_nodes = variables.innervation_zone_width*100 # 100 nodes per cm + innervation_node_global = int(n_nodes_global / 2) # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + nodes_to_stimulate_global = [innervation_node_global] + if innervation_node_global > 0: + nodes_to_stimulate_global.insert(0, innervation_node_global-1) + if innervation_node_global < n_nodes_global-1: + nodes_to_stimulate_global.append(innervation_node_global+1) + + # stimulation value + if is_fiber_gets_stimulated: + stimulation_current = variables.nodal_stimulation_current + else: + stimulation_current = 0. + + first_dof_global = dof_nos_global[0] + last_dof_global = dof_nos_global[-1] + + for node_no_global in nodes_to_stimulate_global: + if first_dof_global <= node_no_global <= last_dof_global: + # get local no for global no (1D) + dof_no_local = node_no_global - first_dof_global + parameters[dof_no_local] = stimulation_current + +# callback function that can set parameters, i.e. stimulation current +def set_specific_parameters(n_nodes_global, time_step_no, current_time, parameters, fiber_no): + + # determine if fiber gets stimulated at the current time + is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) + + # determine nodes to stimulate (center node, left and right neighbour) + innervation_zone_width_n_nodes = variables.innervation_zone_width*100 # 100 nodes per cm + innervation_node_global = int(n_nodes_global / 2) # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + nodes_to_stimulate_global = [innervation_node_global] + + for k in range(10): + if innervation_node_global-k >= 0: + nodes_to_stimulate_global.insert(0, innervation_node_global-k) + if innervation_node_global+k <= n_nodes_global-1: + nodes_to_stimulate_global.append(innervation_node_global+k) + + # stimulation value + if is_fiber_gets_stimulated: + stimulation_current = 40. + else: + stimulation_current = 0. + + for node_no_global in nodes_to_stimulate_global: + parameters[(node_no_global,0)] = stimulation_current # key: ((x,y,z),nodal_dof_index) + +# callback function that can set states, i.e. prescribed values for stimulation +def set_specific_states(n_nodes_global, time_step_no, current_time, states, fiber_no): + + #print("call set_specific_states at time {}".format(current_time)) + + # determine if fiber gets stimulated at the current time + is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) + + if is_fiber_gets_stimulated: + # determine nodes to stimulate (center node, left and right neighbour) + innervation_zone_width_n_nodes = variables.innervation_zone_width*100 # 100 nodes per cm + innervation_node_global = int(n_nodes_global / 2) # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + nodes_to_stimulate_global = [innervation_node_global] + if innervation_node_global > 0: + nodes_to_stimulate_global.insert(0, innervation_node_global-1) + if innervation_node_global < n_nodes_global-1: + nodes_to_stimulate_global.append(innervation_node_global+1) + #if rank_no == 0: + # print("t: {}, stimulate fiber {} at nodes {}".format(current_time, fiber_no, nodes_to_stimulate_global)) + + for node_no_global in nodes_to_stimulate_global: + states[(node_no_global,0,0)] = variables.vm_value_stimulated # key: ((x,y,z),nodal_dof_index,state_no) + +# callback function for artifical stress values, instead of monodomain +def set_stress_values(n_dofs_global, n_nodes_global_per_coordinate_direction, time_step_no, current_time, values, global_natural_dofs, fiber_no): + # n_dofs_global: (int) global number of dofs in the mesh where to set the values + # n_nodes_global_per_coordinate_direction (list of ints) [mx, my, mz] number of global nodes in each coordinate direction. + # For composite meshes, the values are only for the first submesh, for other meshes sum(...) equals n_dofs_global + # time_step_no: (int) current time step number + # current_time: (float) the current simulation time + # values: (list of floats) all current local values of the field variable, if there are multiple components, they are stored in struct-of-array memory layout + # i.e. [point0_component0, point0_component1, ... pointN_component0, point1_component0, point1_component1, ...] + # After the call, these values will be assigned to the field variable. + # global_natural_dofs (list of ints) for every local dof no. the dof no. in global natural ordering + # additional_argument: The value of the option "additionalArgument", can be any Python object. + + # loop over nodes in fiber + for local_dof_no in range(len(values)): + # get the global no. of the current dof + global_dof_no = global_natural_dofs[local_dof_no] + + n_nodes_per_fiber = n_nodes_global_per_coordinate_direction[0] + + k = global_dof_no + N = n_nodes_per_fiber + + if k > N/2: + k = N/2 - k + else: + k = k - N/2 + + values[local_dof_no] = 0.1*np.sin((current_time/100 + 0.2*k/N + 0.1*fiber_no/variables.n_fibers_total) * 2*np.pi) ** 2 + + +# load MU distribution and firing times +variables.fiber_distribution = np.genfromtxt(variables.fiber_distribution_file, delimiter=" ") +variables.firing_times = np.genfromtxt(variables.firing_times_file) + +# for debugging output show when the first 20 fibers will fire +if rank_no == 0 and not variables.disable_firing_output: + print("Debugging output about fiber firing: Taking input from file \"{}\"".format(variables.firing_times_file)) + import timeit + t_start = timeit.default_timer() + + first_stimulation_info = [] + + n_firing_times = np.size(variables.firing_times,0) + for fiber_no_index in range(variables.n_fibers_total): + if fiber_no_index % 100 == 0: + t_algebraic = timeit.default_timer() + if t_algebraic - t_start > 100: + print("Note: break after {}/{} fibers ({:.0f}%) because it already took {:.3f}s".format(fiber_no_index,variables.n_fibers_total,100.0*fiber_no_index/(variables.n_fibers_total-1.),t_algebraic - t_start)) + break + + first_stimulation = None + for current_time in np.linspace(0,1./variables.stimulation_frequency*n_firing_times,n_firing_times): + if fiber_gets_stimulated(fiber_no_index, variables.stimulation_frequency, current_time): + first_stimulation = current_time + break + mu_no = get_motor_unit_no(fiber_no_index) + first_stimulation_info.append([fiber_no_index,mu_no,first_stimulation]) + + first_stimulation_info.sort(key=lambda x: 1e6+1e-6*x[1]+1e-12*x[0] if x[2] is None else x[2]+1e-6*x[1]+1e-12*x[0]) + + print("First stimulation times") + print(" Time MU fibers") + n_stimulated_mus = 0 + n_not_stimulated_mus = 0 + stimulated_fibers = [] + last_time = 0 + last_mu_no = first_stimulation_info[0][1] + for stimulation_info in first_stimulation_info: + mu_no = stimulation_info[1] + fiber_no = stimulation_info[0] + if mu_no == last_mu_no: + stimulated_fibers.append(fiber_no) + else: + if last_time is not None: + if len(stimulated_fibers) > 10: + print("{:8.2f} {:3} {} (only showing first 10, {} total)".format(last_time,last_mu_no,str(stimulated_fibers[0:10]),len(stimulated_fibers))) + else: + print("{:8.2f} {:3} {}".format(last_time,last_mu_no,str(stimulated_fibers))) + n_stimulated_mus += 1 + else: + if len(stimulated_fibers) > 10: + print(" never stimulated: MU {:3}, fibers {} (only showing first 10, {} total)".format(last_mu_no,str(stimulated_fibers[0:10]),len(stimulated_fibers))) + else: + print(" never stimulated: MU {:3}, fibers {}".format(last_mu_no,str(stimulated_fibers))) + n_not_stimulated_mus += 1 + stimulated_fibers = [fiber_no] + + last_time = stimulation_info[2] + last_mu_no = mu_no + + print("stimulated MUs: {}, not stimulated MUs: {}".format(n_stimulated_mus,n_not_stimulated_mus)) + + t_end = timeit.default_timer() + print("duration of assembling this list: {:.3f} s\n".format(t_end-t_start)) + +# compute partitioning +if rank_no == 0: + if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: + print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) + quit() + +# n_fibers_per_subdomain_* is already set + +#################################### +# set Dirichlet BC for the flow problem + +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z + +n_points_3D_mesh_quadratic_global_x = 2*n_points_3D_mesh_linear_global_x - 1 +n_points_3D_mesh_quadratic_global_y = 2*n_points_3D_mesh_linear_global_y - 1 +n_points_3D_mesh_quadratic_global_z = 2*n_points_3D_mesh_linear_global_z - 1 + +# set boundary conditions for the elasticity +[mx, my, mz] = variables.meshes["3Dmesh_quadratic"]["nPointsGlobal"] +[nx, ny, nz] = variables.meshes["3Dmesh_quadratic"]["nElements"] + +variables.fiber_mesh_names = [mesh_name for mesh_name in variables.meshes.keys() if "MeshFiber" in mesh_name] + +# set Dirichlet BC at top nodes for linear elasticity problem, fix muscle at top +variables.elasticity_dirichlet_bc = {} +if False: + for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[(mz-1)*mx*my + j*mx + i] = [0.0,0.0,0.0,0.0,0.0,0.0] + +# fix muscle at bottom +if False: + k = 0 + for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,0.0,0.0,0.0,0.0] + +# Neumann BC at top nodes, traction upwards +#k = nz-1 +#variables.elasticity_neumann_bc = [{"element": k*nx*ny + j*nx + i, "constantVector": [0.0,0.0,10.0], "face": "2+"} for j in range(ny) for i in range(nx)] +variables.elasticity_neumann_bc = [] + +#with open("mesh","w") as f: +# f.write(str(variables.meshes["3Dmesh_quadratic"])) + diff --git a/muscle-tendon-complex/muscle-opendihu/run.sh b/muscle-tendon-complex/muscle-opendihu/run.sh new file mode 100644 index 000000000..3248c0a62 --- /dev/null +++ b/muscle-tendon-complex/muscle-opendihu/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./muscle-solver settings-muscle.py variables.py \ No newline at end of file diff --git a/muscle-tendon-complex/opendihu-solver/clean.sh b/muscle-tendon-complex/opendihu-solver/clean.sh index 2d9bf2ff9..0a24e263a 100644 --- a/muscle-tendon-complex/opendihu-solver/clean.sh +++ b/muscle-tendon-complex/opendihu-solver/clean.sh @@ -1,3 +1,11 @@ +#!/bin/bash + rm -r .* *.log rm -r build_release rm -r precice-profiling + +# remove executables from partipant folders +rm ../muscle-opendihu/muscle-solver +rm ../tendon-bottom-opendihu/tendon-solver +rm ../tendon-top-A-opendihu/tendon-solver +rm ../tendon-top-B-opendihu/tendon-solver \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh new file mode 100644 index 000000000..fafa9d5e0 --- /dev/null +++ b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh @@ -0,0 +1,4 @@ +#!/bin/sh +rm -r precice-profiling +rm -r __pycache__ +rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/run.sh b/muscle-tendon-complex/tendon-bottom-opendihu/run.sh new file mode 100644 index 000000000..d5c7a5968 --- /dev/null +++ b/muscle-tendon-complex/tendon-bottom-opendihu/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./tendon-solver settings-tendon-bottom.py \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py index 5c4ac4ea0..493e22626 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py @@ -2,54 +2,48 @@ # Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. import sys, os import numpy as np -import pickle import argparse import sys -sys.path.insert(0, '.') -import variables # file variables.py, defines default values for all parameters, you can set the parameters there -from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain -import stl from stl import mesh import json # set title of terminal -title = "tendon_bottom" +title = "tendon-bottom" print('\33]0;{}\a'.format(title), end='', flush=True) -# material parameters -# -------------------- -# quantities in mechanics unit system -variables.rho = 10 # [1e-4 kg/cm^3] 10 = density of the muscle (density of water) +#add variables subfolder to python path where the variables script is located +script_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_path) +sys.path.insert(0, os.path.join(script_path,'variables')) -# material parameters for Saint Venant-Kirchhoff material -# https://www.researchgate.net/publication/230248067_Bulk_Modulus +import variables +from create_partitioned_meshes_for_settings import * -youngs_modulus = 7e4 # [N/cm^2 = 10kPa] -shear_modulus = 3e4 +# update material parameters +if (variables.tendon_material == "nonLinear"): + c = 9.98 # [N/cm^2=kPa] + ca = 14.92 # [-] + ct = 14.7 # [-] + cat = 9.64 # [-] + ctt = 11.24 # [-] + mu = 3.76 # [N/cm^2=kPa] + k1 = 42.217e3 # [N/cm^2=kPa] + k2 = 411.360e3 # [N/cm^2=kPa] -#youngs_modulus*=1e-3 -#shear_modulus*=1e-3 + variables.material_parameters = [c, ca, ct, cat, ctt, mu, k1, k2] -lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda -mu = shear_modulus # Lamé parameter mu or G (shear modulus) +if (variables.tendon_material == "SaintVenantKirchoff"): + # material parameters for Saint Venant-Kirchhoff material + # https://www.researchgate.net/publication/230248067_Bulk_Modulus -variables.material_parameters = [lambd, mu] + youngs_modulus = 7e4 # [N/cm^2 = 10kPa] + shear_modulus = 3e4 -variables.constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -variables.force = 100.0 # [N] pulling force to the bottom + lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda + mu = shear_modulus # Lamé parameter mu or G (shear modulus) -variables.dt_elasticity = 1 # [ms] time step width for elasticity -variables.end_time = 20000 # [ms] simulation time -variables.scenario_name = "tendon_bottom" -variables.is_bottom_tendon = True # whether the tendon is at the bottom (negative z-direction), this is important for the boundary conditions -variables.output_timestep_3D = 50 # [ms] output timestep + variables.material_parameters = [lambd, mu] -# input mesh file -fiber_file = "../../../../input/left_biceps_brachii_tendon1.bin" # bottom tendon -#fiber_file = "../../../../input/left_biceps_brachii_tendon2a.bin" -#fiber_file = "../../../../input/left_biceps_brachii_tendon2b.bin" -#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. @@ -94,7 +88,7 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, - fiber_file, load_fiber_data, + variables.fiber_file, load_fiber_data, sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) [variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result @@ -148,206 +142,6 @@ def update_neumann_bc(t): print("prescribed pulling force to bottom: {}".format(variables.force*factor)) return config -# update dirichlet boundary conditions to account for movement of humerus - -current_ulna_force = 0 -current_ulna_angle = 0 - -# global coordinates of tendon bottom -# global coordinates of elbow hinge -elbow_hinge_point = np.array([3.54436, 11.4571, -58.5607]) -bottom_tendon_insertion_point = np.array([4.30, 14.81, -63.41]) - -vec = -elbow_hinge_point + bottom_tendon_insertion_point -angle_offset = np.arctan((bottom_tendon_insertion_point[2] - elbow_hinge_point[2]) / np.linalg.norm(vec)) -rotation_axis = np.array([-1.5, 1, 0]) -rotation_axis = rotation_axis / np.linalg.norm(rotation_axis) # normalize rotation axis - -def rotation_matrix(angle): - axis_x = rotation_axis[0] - axis_y = rotation_axis[1] - axis_z = rotation_axis[2] - - # compute rotation matrix - rotation_matrix = np.array([ - [np.cos(angle) + axis_x**2*(1 - np.cos(angle)), - axis_x*axis_y*(1 - np.cos(angle)) - axis_z*np.sin(angle), - axis_x*axis_z*(1 - np.cos(angle)) + axis_y*np.sin(angle)], - [axis_y*axis_x*(1 - np.cos(angle)) + axis_z*np.sin(angle), - np.cos(angle) + axis_y**2*(1 - np.cos(angle)), - axis_y*axis_z*(1 - np.cos(angle)) - axis_x*np.sin(angle)], - [axis_z*axis_x*(1 - np.cos(angle)) - axis_y*np.sin(angle), - axis_z*axis_y*(1 - np.cos(angle)) + axis_x*np.sin(angle), - np.cos(angle) + axis_z**2*(1 - np.cos(angle))]]) - - return rotation_matrix - -call_count = 0 -ulna_series_files = [] -ulna_stl_filename = os.path.join(os.getcwd(),"cm_left_ulna.stl") -stl_mesh = None -try: - stl_mesh = mesh.Mesh.from_file(ulna_stl_filename) -except: - print("Could not open file {} in directory {}".format(ulna_stl_filename, os.getcwd())) - sys.exit(0) - -# Function to update dirichlet boundary conditions over time, t. -# Only those entries can be updated that were also initially set. -def update_dirichlet_bc(t): - global current_ulna_angle - - # determine parameter for the rotation of the tendon insertion point - angle = current_ulna_angle - rotation_point = elbow_hinge_point - rotation_mat = rotation_matrix(angle) - vertex = bottom_tendon_insertion_point - - # rotation vertex about rotation_axis by angle - vertex = vertex - rotation_point - vertex = rotation_mat.dot(vertex) - vertex = vertex + rotation_point - - new_insertion_point = vertex - offset = -bottom_tendon_insertion_point + new_insertion_point - print("angle: {} deg, rot matrix: {}".format(angle*180/np.pi,rotation_matrix(angle))) - print("old insertion point: {}, new insertion point: {}, offset: {}".format(bottom_tendon_insertion_point,new_insertion_point,offset)) - #offset[0] = 0 - #offset[1] = 0 - #offset[2] = 0 - - # update dirichlet boundary conditions, set prescribed value to offset, do not constrain velocity - for key in variables.elasticity_dirichlet_bc.keys(): - variables.elasticity_dirichlet_bc[key] = [offset[0],offset[1],offset[2],None,None,None] - return variables.elasticity_dirichlet_bc - -def store_rotated_ulna(t): - global current_ulna_angle, call_count, stl_mesh, ulna_series_files - - if np.isnan(current_ulna_angle): - return - - # store rotated ulna - call_count += 1 - output_interval = 50 # [ms] (because coupling timestep is 1ms) - if call_count % output_interval == 0: - out_triangles = [] - - rotation_point = elbow_hinge_point - rotation_mat = rotation_matrix(current_ulna_angle) - - for p in stl_mesh.points: - # p contains the 9 entries [p1x p1y p1z p2x p2y p2z p3x p3y p3z] of the triangle with corner points (p1,p2,p3) - - # transform vertices - vertex_list = [] - - # apply rotation - for vertex in [np.array(p[0:3]), np.array(p[3:6]), np.array(p[6:9])]: - vertex = vertex - rotation_point - vertex = rotation_mat.dot(vertex) - vertex = vertex + rotation_point - vertex_list.append(vertex) - - out_triangles += [vertex_list] - - # Create the mesh - out_mesh = mesh.Mesh(np.zeros(len(out_triangles), dtype=mesh.Mesh.dtype)) - for i, f in enumerate(out_triangles): - out_mesh.vectors[i] = f - - out_mesh.update_normals() - ulna_output_filename = "ulna_{:04d}.stl".format((int)(call_count/output_interval)) - ulna_output_path = os.path.join("out",ulna_output_filename) - out_mesh.save(ulna_output_path) - print("Saved file {}".format(ulna_output_path)) - ulna_series_files.append({"name": ulna_output_filename, "time": t}) - - # save json series file - ulna_series_filename = "out/ulna.stl.series" - with open(ulna_series_filename, "w") as f: - data = {"file-series-version" : "1.0", "files" : ulna_series_files} - json.dump(data, f, indent='\t') - -forces = [] -def callback_total_force(t, bearing_force_bottom, bearing_moment_bottom, bearing_force_top, bearing_moment_top): - global current_ulna_angle - # this callback functions gets the current total forces that are exerted by the muscle - - current_ulna_force = bearing_force_bottom[2] - - # compute average of last 10 values - forces.append(current_ulna_force) - - if len(forces) > 10: - forces.pop(0) - current_ulna_force = np.mean(forces) - print("callback_total_force, t: {}, force: {}".format(t, current_ulna_force)) - - # compute relation between force and angle of ulna - if False: - # positive angle = elbow flexion (ulna move downwards) - min_ulna_angle = 0 # [deg] - max_ulna_angle = -45 # [deg] - force_factor = -current_ulna_force / 500 - force_factor = min(1.5, max(-0.5, force_factor)) - - current_ulna_angle = min_ulna_angle + force_factor * (max_ulna_angle-min_ulna_angle) - print("callback_total_force, t: {}, force: {}, factor: {}, angle: {}".format(t, current_ulna_force, force_factor,current_ulna_angle)) - current_ulna_angle *= np.pi/180 # convert from deg to rad - - - -# Function to postprocess the output -# This function gets periodically called by the running simulation. -# It provides all current variables for each node: geometry (position), u, v, stress, etc. -def postprocess(result): - global current_ulna_angle - - result = result[0] - # print result for debugging - #print(result) - - # get current time - current_time = result["currentTime"] - timestep_no = result["timeStepNo"] - - # get number of nodes - nx = result["nElementsLocal"][0] # number of elements - ny = result["nElementsLocal"][1] # number of elements - nz = result["nElementsLocal"][2] # number of elements - mx = 2*nx + 1 # number of nodes for quadratic elements - my = 2*ny + 1 - mz = 2*nz + 1 - - # parse variables - field_variables = result["data"] - - #for f in field_variables: - # print(f["name"]) - - # field_variables[0] is the geometry - # field_variables[1] is the displacements u - # etc., uncomment the above to see all field variables - - displacement_components = field_variables[1]["components"] - - # traction values contains the traction vector in reference configuration - u1_values = displacement_components[0]["values"] # displacement in x-direction - u2_values = displacement_components[1]["values"] # displacement in y-direction - u3_values = displacement_components[2]["values"] # displacement in z-direction - u3_values_bottom = [u3_values[j*mx+i] for j in range(my) for i in range(mx)] - z_displacement = np.mean(u3_values) - - #print("nx,ny: {},{}, mx,my: {},{}, {}={}".format(nx,ny,mx,my,mx*my*mz,len(u3_values))) - - # compute elbow angle from displacement of muscle in z direction - vec = -elbow_hinge_point + bottom_tendon_insertion_point - current_ulna_angle = -np.arcsin(z_displacement / np.linalg.norm(vec)) - - print("z displacement: {} ({}), current_ulna_angle: {} deg".format(z_displacement, np.linalg.norm(vec), current_ulna_angle*180/np.pi)) - - store_rotated_ulna(current_time) config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" "timeStepWidth": variables.dt_elasticity, # time step width @@ -424,10 +218,7 @@ def postprocess(result): # Paraview files {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_bottom", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - - # Python callback function "postprocess" - {"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": "", "fileNumbering": "incremental"}, - ], + ], # 2. additional output writer that writes also the hydrostatic pressure "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities "OutputWriter" : [ @@ -460,8 +251,8 @@ def postprocess(result): "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console "timestepWidth": 1, # coupling time step width, must match the value in the precice config "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file - "preciceParticipantName": "TendonSolverBottom", # name of the own precice participant, has to match the name given in the precice xml config file + "preciceConfigFilename": variables.precice_config_file, + "preciceParticipantName": "Tendon-Bottom", # name of the own precice participant, has to match the name given in the precice xml config file "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py index 346098aa8..3aff9e6ba 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py @@ -1,159 +1,116 @@ -case_name = "default" -precice_config_file = "default" +scenario_name = "tendon-bottom" -# scenario name for log file -scenario_name = "muscle" +# time parameters +# --------------- +dt_elasticity = 1 # [ms] time step width for elasticity +end_time = 20000 # [ms] simulation time +output_timestep_3D = 50 # [ms] output timestep -# Fixed units in cellMl models: -# These define the unit system. -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 uA = 1e-6 A -# 1 uF = 1e-6 F -# -# derived units: -# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 -# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg - -# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN -# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS -# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV -# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz -# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 -# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa +# setup +# ----- +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +force = 100.0 # [N] pulling force to the bottom -# Hodgkin-Huxley -# t: ms -# STATES[0], Vm: mV -# CONSTANTS[1], Cm: uF*cm^-2 -# CONSTANTS[2], I_Stim: uA*cm^-2 -# -> all units are consistent -# Shorten -# t: ms -# CONSTANTS[0], Cm: uF*cm^-2 -# STATES[0], Vm: mV -# ALGEBRAIC[32], I_Stim: uA*cm^-2 -# -> all units are consistent -# Fixed units in mechanics system -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 N -# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa -# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg -# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 -# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 +# input files +# ----------- + +import os +opendihu_home = os.environ.get('OPENDIHU_HOME') +fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon1.bin" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +precice_config_file = "../precice-config.xml" +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +debug_output = False # verbose output in this python script, for debugging the domain decomposition +disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space +paraview_output = False # If the paraview output writer should be enabled +adios_output = False # If the MegaMol/ADIOS output writer should be enabled +python_output = False # If the Python output writer should be enabled +exfile_output = False # If the Exfile output writer should be enabled# material parameters # material parameters # -------------------- -# quantities in mechanics unit system -rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) - -# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) -# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 -# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 - -c1 = 3.176e-10 # [N/cm^2] -c2 = 1.813 # [N/cm^2] -b = 1.075e-2 # [N/cm^2] anisotropy parameter -d = 9.1733 # [-] anisotropy parameter - -material_parameters = [c1, c2, b, d] # material parameters -pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) -#pmax = 0.73 - -# load -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -bottom_traction = [0.0,0.0,0.0] # [N] - -# Monodomain parameters -# -------------------- -# quantities in CellML unit system -Conductivity = 3.828 # [mS/cm] sigma, conductivity -Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) -Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) -# diffusion prefactor = Conductivity/(Am*Cm) - -# timing and activation parameters -# ----------------- -# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" -import random -random.seed(0) # ensure that random numbers are the same on every rank -# radius: [μm], stimulation frequency [Hz], jitter [-] -motor_units = [ - {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers - {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers -] - -# timing parameters -# ----------------- -end_time = 20000.0 # [ms] end time of the simulation -stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. -stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation -dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) -dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) -dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) -dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG -output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 -output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D - - -# input files -fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" -fat_mesh_file = fiber_file + "_fat.bin" -firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency -fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" -cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +tendon_material = "nonLinear" +rho = 10 + +# solvers +# ------- +diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation +diffusion_preconditioner_type = "none" # preconditioner +potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_preconditioner_type = "none" # preconditioner +emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_preconditioner_type = "none" # preconditioner +emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution + +# partitioning +# ------------ +# this has to match the total number of processes +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 # stride for sampling the 3D elements from the fiber data -# a higher number leads to less 3D elements +# here any number is possible sampling_stride_x = 2 sampling_stride_y = 2 -sampling_stride_z = 74 - -# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 -# when a fiber point is still considered part of the element. -# Try to increase this such that all mappings have all points. -mapping_tolerance = 0.5 - -# other options -paraview_output = True -adios_output = False -exfile_output = False -python_output = False -disable_firing_output = False - -# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's -def get_am(fiber_no, mu_no): - # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm - r = motor_units[mu_no]["radius"]*1e-4 - # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r - return 2./r - #return Am - -def get_cm(fiber_no, mu_no): - return Cm - -def get_conductivity(fiber_no, mu_no): - return Conductivity - -def get_specific_states_call_frequency(fiber_no, mu_no): - stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] - return stimulation_frequency*1e-3 - -def get_specific_states_frequency_jitter(fiber_no, mu_no): - #return 0 - return motor_units[mu_no % len(motor_units)]["jitter"] - -def get_specific_states_call_enable_begin(fiber_no, mu_no): - return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file +sampling_stride_z = 50 + +mapping_tolerance = 0.1 + + +# further internal variables that will be set by the helper.py script and used in the config in settings_fibers_emg.py +n_fibers_total = None +n_subdomains_xy = None +own_subdomain_coordinate_x = None +own_subdomain_coordinate_y = None +own_subdomain_coordinate_z = None +n_fibers_x = None +n_fibers_y = None +n_points_whole_fiber = None +n_points_3D_mesh_global_x = None +n_points_3D_mesh_global_y = None +n_points_3D_mesh_global_z = None +output_writer_fibers = None +output_writer_emg = None +output_writer_0D_states = None +states_output = False +parameters_used_as_algebraic = None +parameters_used_as_constant = None +parameters_initial_values = None +output_algebraic_index = None +output_state_index = None +nodal_stimulation_current = None +fiber_file_handle = None +fibers = None +fiber_distribution = None +firing_times = None +n_fibers_per_subdomain_x = None +n_fibers_per_subdomain_y = None +n_points_per_subdomain_z = None +z_point_index_start = None +z_point_index_end = None +meshes = None +potential_flow_dirichlet_bc = None +elasticity_dirichlet_bc = None +elasticity_neumann_bc = None +fibers_on_own_rank = None +n_fiber_nodes_on_subdomain = None +fiber_start_node_no = None +generate_linear_3d_mesh = False +generate_quadratic_3d_mesh = True +nx = None +ny = None +nz = None +constant_body_force = None +bottom_traction = None +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 +states_initial_values = [] +enable_coupling = True +enable_force_length_relation = True +lambda_dot_scaling_factor = 1 +mappings = None +vm_value_stimulated = None \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh new file mode 100644 index 000000000..fafa9d5e0 --- /dev/null +++ b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh @@ -0,0 +1,4 @@ +#!/bin/sh +rm -r precice-profiling +rm -r __pycache__ +rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/run.sh b/muscle-tendon-complex/tendon-top-A-opendihu/run.sh new file mode 100644 index 000000000..14e873864 --- /dev/null +++ b/muscle-tendon-complex/tendon-top-A-opendihu/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./tendon-solver settings-tendon-top-A.py \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py index fc9f56694..66f51ccc1 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py @@ -2,51 +2,44 @@ # Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. import sys, os import numpy as np -import pickle -import argparse import sys -sys.path.insert(0, '.') -import variables # file variables.py, defines default values for all parameters, you can set the parameters there -from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain # set title of terminal -title = "tendon_top_a" +title = "tendon-top-a" print('\33]0;{}\a'.format(title), end='', flush=True) -# material parameters -# -------------------- -# quantities in mechanics unit system -variables.rho = 10 # [1e-4 kg/cm^3] 10 = density of the muscle (density of water) +#add variables subfolder to python path where the variables script is located +script_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_path) +sys.path.insert(0, os.path.join(script_path,'variables')) -# material parameters for Saint Venant-Kirchhoff material -# https://www.researchgate.net/publication/230248067_Bulk_Modulus +import variables +from create_partitioned_meshes_for_settings import * -youngs_modulus = 7e4 # [N/cm^2 = 10kPa] -shear_modulus = 3e4 +# update material parameters +if (variables.tendon_material == "nonLinear"): + c = 9.98 # [N/cm^2=kPa] + ca = 14.92 # [-] + ct = 14.7 # [-] + cat = 9.64 # [-] + ctt = 11.24 # [-] + mu = 3.76 # [N/cm^2=kPa] + k1 = 42.217e3 # [N/cm^2=kPa] + k2 = 411.360e3 # [N/cm^2=kPa] -#youngs_modulus*=1e-3 -#shear_modulus*=1e-3 + variables.material_parameters = [c, ca, ct, cat, ctt, mu, k1, k2] -lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda -mu = shear_modulus # Lamé parameter mu or G (shear modulus) +if (variables.tendon_material == "SaintVenantKirchoff"): + # material parameters for Saint Venant-Kirchhoff material + # https://www.researchgate.net/publication/230248067_Bulk_Modulus -variables.material_parameters = [lambd, mu] + youngs_modulus = 7e4 # [N/cm^2 = 10kPa] + shear_modulus = 3e4 -variables.constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -variables.force = 1.0 # [N] + lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda + mu = shear_modulus # Lamé parameter mu or G (shear modulus) -variables.dt_elasticity = 1 # [ms] time step width for elasticity -variables.end_time = 20000 # [ms] simulation time -variables.scenario_name = "tendon_top_a" -variables.is_bottom_tendon = False # whether the tendon is at the bottom (negative z-direction), this is important for the boundary conditions -variables.output_timestep_3D = 50 # [ms] output timestep - -# input mesh file -#fiber_file = "../../../../input/left_biceps_brachii_tendon1.bin" # bottom tendon -fiber_file = "../../../../input/left_biceps_brachii_tendon2a.bin" # top tendon -#fiber_file = "../../../../input/left_biceps_brachii_tendon2b.bin" -#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" + variables.material_parameters = [lambd, mu] load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. @@ -54,28 +47,6 @@ rank_no = (int)(sys.argv[-2]) n_ranks = (int)(sys.argv[-1]) -# define command line arguments -parser = argparse.ArgumentParser(description='tendon') -parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) -parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) -parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) -parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) -parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) -parser.add_argument('-vmodule', help='ignore') - -# parse command line arguments and assign values to variables module -args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) -if len(other_args) != 0 and rank_no == 0: - print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) - -# partitioning -# ------------ -# this has to match the total number of processes -if variables.n_subdomains is not None: - variables.n_subdomains_x = variables.n_subdomains[0] - variables.n_subdomains_y = variables.n_subdomains[1] - variables.n_subdomains_z = variables.n_subdomains[2] - # compute partitioning if rank_no == 0: if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: @@ -91,7 +62,7 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, - fiber_file, load_fiber_data, + variables.fiber_file, load_fiber_data, sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) [variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result @@ -231,8 +202,8 @@ "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console "timestepWidth": 1, # coupling time step width, must match the value in the precice config "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file - "preciceParticipantName": "TendonSolverTopA", # name of the own precice participant, has to match the name given in the precice xml config file + "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file + "preciceParticipantName": "Tendon-Top-A", # name of the own precice participant, has to match the name given in the precice xml config file "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py index 346098aa8..70d0aee9b 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py @@ -1,159 +1,116 @@ -case_name = "default" -precice_config_file = "default" +scenario_name = "tendon-top-A" -# scenario name for log file -scenario_name = "muscle" +# time parameters +# --------------- +dt_elasticity = 1 # [ms] time step width for elasticity +end_time = 20000 # [ms] simulation time +output_timestep_3D = 50 # [ms] output timestep -# Fixed units in cellMl models: -# These define the unit system. -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 uA = 1e-6 A -# 1 uF = 1e-6 F -# -# derived units: -# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 -# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg - -# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN -# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS -# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV -# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz -# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 -# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa +# setup +# ----- +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +force = 100.0 # [N] pulling force to the bottom -# Hodgkin-Huxley -# t: ms -# STATES[0], Vm: mV -# CONSTANTS[1], Cm: uF*cm^-2 -# CONSTANTS[2], I_Stim: uA*cm^-2 -# -> all units are consistent -# Shorten -# t: ms -# CONSTANTS[0], Cm: uF*cm^-2 -# STATES[0], Vm: mV -# ALGEBRAIC[32], I_Stim: uA*cm^-2 -# -> all units are consistent -# Fixed units in mechanics system -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 N -# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa -# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg -# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 -# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 +# input files +# ----------- + +import os +opendihu_home = os.environ.get('OPENDIHU_HOME') +fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon2a.bin" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +precice_config_file = "../precice-config.xml" +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +debug_output = False # verbose output in this python script, for debugging the domain decomposition +disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space +paraview_output = False # If the paraview output writer should be enabled +adios_output = False # If the MegaMol/ADIOS output writer should be enabled +python_output = False # If the Python output writer should be enabled +exfile_output = False # If the Exfile output writer should be enabled# material parameters # material parameters # -------------------- -# quantities in mechanics unit system -rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) - -# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) -# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 -# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 - -c1 = 3.176e-10 # [N/cm^2] -c2 = 1.813 # [N/cm^2] -b = 1.075e-2 # [N/cm^2] anisotropy parameter -d = 9.1733 # [-] anisotropy parameter - -material_parameters = [c1, c2, b, d] # material parameters -pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) -#pmax = 0.73 - -# load -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -bottom_traction = [0.0,0.0,0.0] # [N] - -# Monodomain parameters -# -------------------- -# quantities in CellML unit system -Conductivity = 3.828 # [mS/cm] sigma, conductivity -Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) -Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) -# diffusion prefactor = Conductivity/(Am*Cm) - -# timing and activation parameters -# ----------------- -# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" -import random -random.seed(0) # ensure that random numbers are the same on every rank -# radius: [μm], stimulation frequency [Hz], jitter [-] -motor_units = [ - {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers - {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers -] - -# timing parameters -# ----------------- -end_time = 20000.0 # [ms] end time of the simulation -stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. -stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation -dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) -dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) -dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) -dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG -output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 -output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D - - -# input files -fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" -fat_mesh_file = fiber_file + "_fat.bin" -firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency -fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" -cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +tendon_material = "nonLinear" +rho = 10 + +# solvers +# ------- +diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation +diffusion_preconditioner_type = "none" # preconditioner +potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_preconditioner_type = "none" # preconditioner +emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_preconditioner_type = "none" # preconditioner +emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution + +# partitioning +# ------------ +# this has to match the total number of processes +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 # stride for sampling the 3D elements from the fiber data -# a higher number leads to less 3D elements +# here any number is possible sampling_stride_x = 2 sampling_stride_y = 2 -sampling_stride_z = 74 - -# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 -# when a fiber point is still considered part of the element. -# Try to increase this such that all mappings have all points. -mapping_tolerance = 0.5 - -# other options -paraview_output = True -adios_output = False -exfile_output = False -python_output = False -disable_firing_output = False - -# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's -def get_am(fiber_no, mu_no): - # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm - r = motor_units[mu_no]["radius"]*1e-4 - # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r - return 2./r - #return Am - -def get_cm(fiber_no, mu_no): - return Cm - -def get_conductivity(fiber_no, mu_no): - return Conductivity - -def get_specific_states_call_frequency(fiber_no, mu_no): - stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] - return stimulation_frequency*1e-3 - -def get_specific_states_frequency_jitter(fiber_no, mu_no): - #return 0 - return motor_units[mu_no % len(motor_units)]["jitter"] - -def get_specific_states_call_enable_begin(fiber_no, mu_no): - return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file +sampling_stride_z = 50 + +mapping_tolerance = 0.1 + + +# further internal variables that will be set by the helper.py script and used in the config in settings_fibers_emg.py +n_fibers_total = None +n_subdomains_xy = None +own_subdomain_coordinate_x = None +own_subdomain_coordinate_y = None +own_subdomain_coordinate_z = None +n_fibers_x = None +n_fibers_y = None +n_points_whole_fiber = None +n_points_3D_mesh_global_x = None +n_points_3D_mesh_global_y = None +n_points_3D_mesh_global_z = None +output_writer_fibers = None +output_writer_emg = None +output_writer_0D_states = None +states_output = False +parameters_used_as_algebraic = None +parameters_used_as_constant = None +parameters_initial_values = None +output_algebraic_index = None +output_state_index = None +nodal_stimulation_current = None +fiber_file_handle = None +fibers = None +fiber_distribution = None +firing_times = None +n_fibers_per_subdomain_x = None +n_fibers_per_subdomain_y = None +n_points_per_subdomain_z = None +z_point_index_start = None +z_point_index_end = None +meshes = None +potential_flow_dirichlet_bc = None +elasticity_dirichlet_bc = None +elasticity_neumann_bc = None +fibers_on_own_rank = None +n_fiber_nodes_on_subdomain = None +fiber_start_node_no = None +generate_linear_3d_mesh = False +generate_quadratic_3d_mesh = True +nx = None +ny = None +nz = None +constant_body_force = None +bottom_traction = None +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 +states_initial_values = [] +enable_coupling = True +enable_force_length_relation = True +lambda_dot_scaling_factor = 1 +mappings = None +vm_value_stimulated = None \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh new file mode 100644 index 000000000..fafa9d5e0 --- /dev/null +++ b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh @@ -0,0 +1,4 @@ +#!/bin/sh +rm -r precice-profiling +rm -r __pycache__ +rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/run.sh b/muscle-tendon-complex/tendon-top-B-opendihu/run.sh new file mode 100644 index 000000000..117e658e5 --- /dev/null +++ b/muscle-tendon-complex/tendon-top-B-opendihu/run.sh @@ -0,0 +1,2 @@ +#!/bin/sh +./tendon-solver settings-tendon-top-B.py \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py index dafd77ff9..443fa7b18 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py @@ -2,51 +2,44 @@ # Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. import sys, os import numpy as np -import pickle -import argparse import sys -sys.path.insert(0, '.') -import variables # file variables.py, defines default values for all parameters, you can set the parameters there -from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain # set title of terminal -title = "tendon_top_b" +title = "tendon-top-b" print('\33]0;{}\a'.format(title), end='', flush=True) -# material parameters -# -------------------- -# quantities in mechanics unit system -variables.rho = 10 # [1e-4 kg/cm^3] 10 = density of the muscle (density of water) +#add variables subfolder to python path where the variables script is located +script_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_path) +sys.path.insert(0, os.path.join(script_path,'variables')) -# material parameters for Saint Venant-Kirchhoff material -# https://www.researchgate.net/publication/230248067_Bulk_Modulus +import variables +from create_partitioned_meshes_for_settings import * -youngs_modulus = 7e4 # [N/cm^2 = 10kPa] -shear_modulus = 3e4 +# update material parameters +if (variables.tendon_material == "nonLinear"): + c = 9.98 # [N/cm^2=kPa] + ca = 14.92 # [-] + ct = 14.7 # [-] + cat = 9.64 # [-] + ctt = 11.24 # [-] + mu = 3.76 # [N/cm^2=kPa] + k1 = 42.217e3 # [N/cm^2=kPa] + k2 = 411.360e3 # [N/cm^2=kPa] -#youngs_modulus*=1e-3 -#shear_modulus*=1e-3 + variables.material_parameters = [c, ca, ct, cat, ctt, mu, k1, k2] -lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda -mu = shear_modulus # Lamé parameter mu or G (shear modulus) +if (variables.tendon_material == "SaintVenantKirchoff"): + # material parameters for Saint Venant-Kirchhoff material + # https://www.researchgate.net/publication/230248067_Bulk_Modulus -variables.material_parameters = [lambd, mu] + youngs_modulus = 7e4 # [N/cm^2 = 10kPa] + shear_modulus = 3e4 -variables.constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -variables.force = 1.0 # [N] + lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda + mu = shear_modulus # Lamé parameter mu or G (shear modulus) -variables.dt_elasticity = 1 # [ms] time step width for elasticity -variables.end_time = 20000 # [ms] simulation time -variables.scenario_name = "tendon_top_b" -variables.is_bottom_tendon = False # whether the tendon is at the bottom (negative z-direction), this is important for the boundary conditions -variables.output_timestep_3D = 50 # [ms] output timestep - -# input mesh file -#fiber_file = "../../../../input/left_biceps_brachii_tendon1.bin" # bottom tendon -#fiber_file = "../../../../input/left_biceps_brachii_tendon2a.bin" # top tendon -fiber_file = "../../../../input/left_biceps_brachii_tendon2b.bin" -#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_7x7fibers.bin" + variables.material_parameters = [lambd, mu] load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. @@ -54,27 +47,8 @@ rank_no = (int)(sys.argv[-2]) n_ranks = (int)(sys.argv[-1]) -# define command line arguments -parser = argparse.ArgumentParser(description='tendon') -parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) -parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) -parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) -parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) -parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) -parser.add_argument('-vmodule', help='ignore') - -# parse command line arguments and assign values to variables module -args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) -if len(other_args) != 0 and rank_no == 0: - print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) - # partitioning # ------------ -# this has to match the total number of processes -if variables.n_subdomains is not None: - variables.n_subdomains_x = variables.n_subdomains[0] - variables.n_subdomains_y = variables.n_subdomains[1] - variables.n_subdomains_z = variables.n_subdomains[2] # compute partitioning if rank_no == 0: @@ -91,7 +65,7 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, - fiber_file, load_fiber_data, + variables.fiber_file, load_fiber_data, sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) [variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result @@ -230,8 +204,8 @@ "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console "timestepWidth": 1, # coupling time step width, must match the value in the precice config "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": "precice_config_muscle_dirichlet_tendon_neumann_implicit_coupling_multiple_tendons.xml", # the preCICE configuration file - "preciceParticipantName": "TendonSolverTopB", # name of the own precice participant, has to match the name given in the precice xml config file + "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file + "preciceParticipantName": "Tendon-Top-B", # name of the own precice participant, has to match the name given in the precice xml config file "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py index 346098aa8..5aa58b6a9 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py @@ -1,159 +1,116 @@ -case_name = "default" -precice_config_file = "default" +scenario_name = "tendon-top-B" -# scenario name for log file -scenario_name = "muscle" +# time parameters +# --------------- +dt_elasticity = 1 # [ms] time step width for elasticity +end_time = 20000 # [ms] simulation time +output_timestep_3D = 50 # [ms] output timestep -# Fixed units in cellMl models: -# These define the unit system. -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 uA = 1e-6 A -# 1 uF = 1e-6 F -# -# derived units: -# (F=s^4*A^2*m^-2*kg^-1) => 1 ms^4*uA^2*cm^-2*x*kg^-1 = (1e-3)^4 s^4 * (1e-6)^2 A^2 * (1e-2)^-2 m^-2 * (x)^-1 kg^-1 = 1e-12 * 1e-12 * 1e4 F = 1e-20 * x^-1 F := 1e-6 F => x = 1e-14 -# 1e-14 kg = 10e-15 kg = 10e-12 g = 10 pg - -# (N=kg*m*s^-2) => 1 10pg*cm*ms^2 = 1e-14 kg * 1e-2 m * (1e-3)^-2 s^-2 = 1e-14 * 1e-2 * 1e6 N = 1e-10 N = 10 nN -# (S=kg^-1*m^-2*s^3*A^2, Siemens not Sievert!) => (1e-14*kg)^-1*cm^-2*ms^3*uA^2 = (1e-14)^-1 kg^-1 * (1e-2)^-2 m^-2 * (1e-3)^3 s^3 * (1e-6)^2 A^2 = 1e14 * 1e4 * 1e-9 * 1e-12 S = 1e-3 S = 1 mS -# (V=kg*m^2*s^-3*A^-1) => 1 10pg*cm^2*ms^-3*uA^-1 = (1e-14) kg * (1e-2)^2 m^2 * (1e-3)^-3 s^-3 * (1e-6)^-1 A^-1 = 1e-14 * 1e-4 * 1e6 * 1e6 V = 1e-6 V = 1mV -# (Hz=s^-1) => 1 ms^-1 = (1e-3)^-1 s^-1 = 1e3 Hz -# (kg/m^3) => 1 10 pg/cm^3 = 1e-14 kg / (1e-2 m)^3 = 1e-14 * 1e6 kg/m^3 = 1e-8 kg/m^3 -# (Pa=kg/(m*s^2)) => 1e-14 kg / (1e-2 m * 1e-3^2 s^2) = 1e-14 / (1e-8) Pa = 1e-6 Pa +# setup +# ----- +constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force +force = 100.0 # [N] pulling force to the bottom -# Hodgkin-Huxley -# t: ms -# STATES[0], Vm: mV -# CONSTANTS[1], Cm: uF*cm^-2 -# CONSTANTS[2], I_Stim: uA*cm^-2 -# -> all units are consistent -# Shorten -# t: ms -# CONSTANTS[0], Cm: uF*cm^-2 -# STATES[0], Vm: mV -# ALGEBRAIC[32], I_Stim: uA*cm^-2 -# -> all units are consistent -# Fixed units in mechanics system -# 1 cm = 1e-2 m -# 1 ms = 1e-3 s -# 1 N -# 1 N/cm^2 = (kg*m*s^-2) / (1e-2 m)^2 = 1e4 kg*m^-1*s^-2 = 10 kPa -# (kg = N*s^2*m^-1) => N*ms^2*cm^-1 = N*(1e-3 s)^2 * (1e-2 m)^-1 = 1e-4 N*s^2*m^-1 = 1e-4 kg -# (kg/m^3) => 1 * 1e-4 kg * (1e-2 m)^-3 = 1e2 kg/m^3 -# (m/s^2) => 1 cm/ms^2 = 1e-2 m * (1e-3 s)^-2 = 1e4 m*s^-2 +# input files +# ----------- + +import os +opendihu_home = os.environ.get('OPENDIHU_HOME') +fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon2b.bin" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +precice_config_file = "../precice-config.xml" +load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +debug_output = False # verbose output in this python script, for debugging the domain decomposition +disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space +paraview_output = False # If the paraview output writer should be enabled +adios_output = False # If the MegaMol/ADIOS output writer should be enabled +python_output = False # If the Python output writer should be enabled +exfile_output = False # If the Exfile output writer should be enabled# material parameters # material parameters # -------------------- -# quantities in mechanics unit system -rho = 10 # [1e-4 kg/cm^3] density of the muscle (density of water) - -# Mooney-Rivlin parameters [c1,c2,b,d] of c1*(Ibar1 - 3) + c2*(Ibar2 - 3) + b/d (λ - 1) - b*ln(λ) -# Heidlauf13: [6.352e-10 kPa, 3.627 kPa, 2.756e-5 kPa, 43.373] = [6.352e-11 N/cm^2, 3.627e-1 N/cm^2, 2.756e-6 N/cm^2, 43.373], pmax = 73 kPa = 7.3 N/cm^2 -# Heidlauf16: [3.176e-10 N/cm^2, 1.813 N/cm^2, 1.075e-2 N/cm^2, 9.1733], pmax = 7.3 N/cm^2 - -c1 = 3.176e-10 # [N/cm^2] -c2 = 1.813 # [N/cm^2] -b = 1.075e-2 # [N/cm^2] anisotropy parameter -d = 9.1733 # [-] anisotropy parameter - -material_parameters = [c1, c2, b, d] # material parameters -pmax = 7.3 # [N/cm^2] maximum isometric active stress (30-40) -#pmax = 0.73 - -# load -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -bottom_traction = [0.0,0.0,0.0] # [N] - -# Monodomain parameters -# -------------------- -# quantities in CellML unit system -Conductivity = 3.828 # [mS/cm] sigma, conductivity -Am = 500.0 # [cm^-1] surface area to volume ratio (this is not used, instead values of motor_units are used) -Cm = 0.58 # [uF/cm^2] membrane capacitance, (1 = fast twitch, 0.58 = slow twitch) -# diffusion prefactor = Conductivity/(Am*Cm) - -# timing and activation parameters -# ----------------- -# motor units from paper Klotz2019 "Modelling the electrical activity of skeletal muscle tissue using a multi‐domain approach" -import random -random.seed(0) # ensure that random numbers are the same on every rank -# radius: [μm], stimulation frequency [Hz], jitter [-] -motor_units = [ - {"radius": 40.00, "activation_start_time": 0.0, "stimulation_frequency": 23.92, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # low number of fibers - {"radius": 42.35, "activation_start_time": 0.2, "stimulation_frequency": 23.36, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 45.00, "activation_start_time": 0.4, "stimulation_frequency": 23.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 48.00, "activation_start_time": 0.6, "stimulation_frequency": 22.46, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 51.42, "activation_start_time": 0.8, "stimulation_frequency": 20.28, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 55.38, "activation_start_time": 1.0, "stimulation_frequency": 16.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 60.00, "activation_start_time": 1.2, "stimulation_frequency": 12.05, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 65.45, "activation_start_time": 1.4, "stimulation_frequency": 10.03, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 72.00, "activation_start_time": 1.6, "stimulation_frequency": 8.32, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, - {"radius": 80.00, "activation_start_time": 1.8, "stimulation_frequency": 7.66, "jitter": [0.1*random.uniform(-1,1) for i in range(100)]}, # high number of fibers -] - -# timing parameters -# ----------------- -end_time = 20000.0 # [ms] end time of the simulation -stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. -stimulation_frequency_jitter = 0 # [-] jitter in percent of the frequency, added and substracted to the stimulation_frequency after each stimulation -dt_0D = 2e-4 # [ms] timestep width of ODEs (1e-3) -dt_1D = 2e-4 # [ms] timestep width of diffusion (1e-3) -dt_splitting = 2e-4 # [ms] overall timestep width of strang splitting (1e-3) -dt_3D = 1 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG -output_timestep_fibers = 4e0 # [ms] timestep for fiber output, 0.5 -output_timestep_3D = dt_3D # [ms] timestep for output of fibers and mechanics, should be a multiple of dt_3D - - -# input files -fiber_file = "../../../../input/left_biceps_brachii_9x9fibers.bin" -#fiber_file = "../../../../input/left_biceps_brachii_31x31fibers.bin" -fat_mesh_file = fiber_file + "_fat.bin" -firing_times_file = "../../../../input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency -fiber_distribution_file = "../../../../input/MU_fibre_distribution_10MUs.txt" -cellml_file = "../../../../input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +tendon_material = "nonLinear" +rho = 10 + +# solvers +# ------- +diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation +diffusion_preconditioner_type = "none" # preconditioner +potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_preconditioner_type = "none" # preconditioner +emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_preconditioner_type = "none" # preconditioner +emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution + +# partitioning +# ------------ +# this has to match the total number of processes +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 # stride for sampling the 3D elements from the fiber data -# a higher number leads to less 3D elements +# here any number is possible sampling_stride_x = 2 sampling_stride_y = 2 -sampling_stride_z = 74 - -# Tolerance value in the element coordinate system of the 3D elements, [0,1]^3 -# when a fiber point is still considered part of the element. -# Try to increase this such that all mappings have all points. -mapping_tolerance = 0.5 - -# other options -paraview_output = True -adios_output = False -exfile_output = False -python_output = False -disable_firing_output = False - -# functions, here, Am, Cm and Conductivity are constant for all fibers and MU's -def get_am(fiber_no, mu_no): - # get radius in cm, 1 μm = 1e-6 m = 1e-4*1e-2 m = 1e-4 cm - r = motor_units[mu_no]["radius"]*1e-4 - # cylinder surface: A = 2*π*r*l, V = cylinder volume: π*r^2*l, Am = A/V = 2*π*r*l / (π*r^2*l) = 2/r - return 2./r - #return Am - -def get_cm(fiber_no, mu_no): - return Cm - -def get_conductivity(fiber_no, mu_no): - return Conductivity - -def get_specific_states_call_frequency(fiber_no, mu_no): - stimulation_frequency = motor_units[mu_no % len(motor_units)]["stimulation_frequency"] - return stimulation_frequency*1e-3 - -def get_specific_states_frequency_jitter(fiber_no, mu_no): - #return 0 - return motor_units[mu_no % len(motor_units)]["jitter"] - -def get_specific_states_call_enable_begin(fiber_no, mu_no): - return motor_units[mu_no % len(motor_units)]["activation_start_time"]*1e3 \ No newline at end of file +sampling_stride_z = 50 + +mapping_tolerance = 0.1 + + +# further internal variables that will be set by the helper.py script and used in the config in settings_fibers_emg.py +n_fibers_total = None +n_subdomains_xy = None +own_subdomain_coordinate_x = None +own_subdomain_coordinate_y = None +own_subdomain_coordinate_z = None +n_fibers_x = None +n_fibers_y = None +n_points_whole_fiber = None +n_points_3D_mesh_global_x = None +n_points_3D_mesh_global_y = None +n_points_3D_mesh_global_z = None +output_writer_fibers = None +output_writer_emg = None +output_writer_0D_states = None +states_output = False +parameters_used_as_algebraic = None +parameters_used_as_constant = None +parameters_initial_values = None +output_algebraic_index = None +output_state_index = None +nodal_stimulation_current = None +fiber_file_handle = None +fibers = None +fiber_distribution = None +firing_times = None +n_fibers_per_subdomain_x = None +n_fibers_per_subdomain_y = None +n_points_per_subdomain_z = None +z_point_index_start = None +z_point_index_end = None +meshes = None +potential_flow_dirichlet_bc = None +elasticity_dirichlet_bc = None +elasticity_neumann_bc = None +fibers_on_own_rank = None +n_fiber_nodes_on_subdomain = None +fiber_start_node_no = None +generate_linear_3d_mesh = False +generate_quadratic_3d_mesh = True +nx = None +ny = None +nz = None +constant_body_force = None +bottom_traction = None +n_subdomains_x = 1 +n_subdomains_y = 1 +n_subdomains_z = 1 +states_initial_values = [] +enable_coupling = True +enable_force_length_relation = True +lambda_dot_scaling_factor = 1 +mappings = None +vm_value_stimulated = None \ No newline at end of file From a80cfaac153d9a708a0cf93e3b57ec77ebc8e464 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Tue, 5 Mar 2024 16:25:19 +0100 Subject: [PATCH 11/40] Add remove precice-run to clean.sh --- muscle-tendon-complex/muscle-opendihu/clean.sh | 2 +- muscle-tendon-complex/tendon-bottom-opendihu/clean.sh | 2 +- muscle-tendon-complex/tendon-top-A-opendihu/clean.sh | 2 +- muscle-tendon-complex/tendon-top-B-opendihu/clean.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/clean.sh b/muscle-tendon-complex/muscle-opendihu/clean.sh index fafa9d5e0..467b38c90 100644 --- a/muscle-tendon-complex/muscle-opendihu/clean.sh +++ b/muscle-tendon-complex/muscle-opendihu/clean.sh @@ -1,4 +1,4 @@ #!/bin/sh -rm -r precice-profiling +rm -r precice-* rm -r __pycache__ rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh index fafa9d5e0..467b38c90 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh +++ b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh @@ -1,4 +1,4 @@ #!/bin/sh -rm -r precice-profiling +rm -r precice-* rm -r __pycache__ rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh index fafa9d5e0..467b38c90 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh +++ b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh @@ -1,4 +1,4 @@ #!/bin/sh -rm -r precice-profiling +rm -r precice-* rm -r __pycache__ rm -r lib logs out \ No newline at end of file diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh index fafa9d5e0..467b38c90 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh +++ b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh @@ -1,4 +1,4 @@ #!/bin/sh -rm -r precice-profiling +rm -r precice-* rm -r __pycache__ rm -r lib logs out \ No newline at end of file From b38378235a8bfb97bbb3ce8da8acc4d4446d7d0f Mon Sep 17 00:00:00 2001 From: carme-hp Date: Tue, 5 Mar 2024 16:27:12 +0100 Subject: [PATCH 12/40] Fix bc errors --- .../tendon-bottom-opendihu/settings-tendon-bottom.py | 2 +- .../tendon-top-A-opendihu/settings-tendon-top-A.py | 2 +- .../tendon-top-B-opendihu/settings-tendon-top-B.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py index 493e22626..93a24736c 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py @@ -160,7 +160,7 @@ def update_neumann_bc(t): # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian # mesh - "preciceMeshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py index 66f51ccc1..b0d546c52 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py @@ -112,7 +112,7 @@ # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian # mesh - "preciceMeshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py index 443fa7b18..0deb3cd91 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py @@ -114,7 +114,7 @@ # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian # mesh - "preciceMeshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction From 0bdcb05ecdd63ce6c5d9eaa631d561b3433d1324 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Tue, 5 Mar 2024 16:52:28 +0100 Subject: [PATCH 13/40] Setup exchange directory and remove comments --- muscle-tendon-complex/precice-config.xml | 79 +++--------------------- 1 file changed, 7 insertions(+), 72 deletions(-) diff --git a/muscle-tendon-complex/precice-config.xml b/muscle-tendon-complex/precice-config.xml index 7b7677c55..cb7d07af3 100644 --- a/muscle-tendon-complex/precice-config.xml +++ b/muscle-tendon-complex/precice-config.xml @@ -1,19 +1,14 @@ - - - - - @@ -49,15 +44,8 @@ - - - - - @@ -65,7 +53,6 @@ - @@ -76,36 +63,8 @@ - - - - - - - - + + - - - - - - - - - - - - - - - - + + + - - - - - + + @@ -225,10 +168,6 @@ - - - - @@ -247,10 +186,6 @@ - - From c6091ae75e17c395a4a9a8eeb2db8f00cf70e3fa Mon Sep 17 00:00:00 2001 From: carme-hp Date: Sun, 10 Mar 2024 11:18:42 +0100 Subject: [PATCH 14/40] Add information about OpenDiHu --- muscle-tendon-complex/README.md | 39 ++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 12140e7c4..7be19abed 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -40,25 +40,44 @@ The participant that has the control is the one that it is connected to all othe ## About the solvers -OpenDiHu is used for the muscle and each tendon participants. -The muscle solver consists of a ... TODO -The tendon solver consists of a ... TODO +OpenDiHu is used for the muscle and each tendon participants. + +**The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. + + - The [*FastMonodomainSolver*](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers, i.e, how an electrical signal propagates from the center to the extremes of the muscle fibers. The electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. The solver solves the so called "monodomain equation" independently for each fiber. The equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e. the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellm`. + + - The [*MuscleContractionSolver*](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the active paramter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) + +**The tendon solver** is a dynamic FEM mechanical solver. It models an hyperelastic passive material. The material parameters are chosen as in [Carniel et al.](https://pubmed.ncbi.nlm.nih.gov/28238424/) ## Running the Simulation -1. Preparation: ... TODO +1. Preparation: - Install OpenDiHu + + In the OpenDiHu website you can find detailed [installation instructions](https://opendihu.readthedocs.io/en/latest/user/installation.html). + + We recommend to download the code from the [GitHub repository](https://github.com/opendihu/opendihu) and to run `make release_without_tests` in the parent directory. + + > **Note:** OpenDiHu automatically downloads dependencies and installs them in the `opendihu/dependencies/` folder. You can avoid that by setting e.g., `PRECICE_DOWNLOAD = False` in the [user-variables.scons.py](https://github.com/opendihu/opendihu/blob/develop/user-variables.scons.py) before building OpenDiHu. + - Download input files for OpenDiHu + + OpenDiHu requires of input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading this files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [click here](https://zenodo.org/record/4705982/files/input.tgz?download=1) to download the necessary files. Please extract the files and place them on `opendihu/examples/electrophysiology/` directory. + - Setup `$OPENDIHU_HOME` to your `.bashrc` file + ``` + export OPENDIHU_HOME=/path/to/opendihu + + ``` - Compile muscle and tendon solvers - ```bash - cd opendihu-solver - ./build.sh - ``` - - Move executables to participants directory + ```bash + cd opendihu-solver + ./build.sh + ``` -2. Starting: +2. Starting the simulation: We are going to run each solver in a different terminal. It is important that first we navigate to the simulation directory so that all solvers start in the same directory. To start the `Muscle` participant, run: From 1d31091f434963257f87f192b3250571bf59b037 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Sun, 10 Mar 2024 13:40:43 +0100 Subject: [PATCH 15/40] Add .gitignore --- muscle-tendon-complex/.gitignore | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 muscle-tendon-complex/.gitignore diff --git a/muscle-tendon-complex/.gitignore b/muscle-tendon-complex/.gitignore new file mode 100644 index 000000000..e8ff11487 --- /dev/null +++ b/muscle-tendon-complex/.gitignore @@ -0,0 +1,12 @@ +**/__pycache__ +**/lib +**/logs +**/out +**/*.log +**/*.txt +**/precice-profiling +**/build_release +**/muscle-solver +**/tendon-solver +**/.sconf_temp +**/.scons* From 6bfa0a9187941afa667bbcbd7f82400052b57ed7 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:03:54 +0100 Subject: [PATCH 16/40] Fix Markdown linting issues in muscle tutorial README --- muscle-tendon-complex/README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 7be19abed..eb2bf4799 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -13,11 +13,11 @@ Get the [case files of this tutorial](https://github.com/precice/tutorials/tree/ In the following tutorial we model the contraction of a muscle, in particular, the biceps. The biceps is attached to the bones by three tendons (one at the bottom and two at the top). We enforce an activation in the muscle which results in its contraction. The tendons move as a result of the muscle contraction. In this case, a muscle and three tendons are coupled together using a fully-implicit multi-coupling scheme. The case setup is shown here: -![Setup](images/tutorials-muscle-tendon-complex-setup.png) +![Setup](images/tutorials-muscle-tendon-complex-setup.png) The muscle participant (in red), is connected to three tendons. The muscle sends traction values to the tendons, which send displacement and velocity values back to the muscle. The end of each tendon which is not attached to the muscle is fixed by a dirichlet boundary condition (in reality, it would be fixed to the bones). -The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not match perfectly. +The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not match perfectly. TODO: Explain how is the muscle activated! @@ -42,34 +42,34 @@ The participant that has the control is the one that it is connected to all othe OpenDiHu is used for the muscle and each tendon participants. -**The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. +**The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. - - The [*FastMonodomainSolver*](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers, i.e, how an electrical signal propagates from the center to the extremes of the muscle fibers. The electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. The solver solves the so called "monodomain equation" independently for each fiber. The equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e. the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellm`. +- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers, i.e, how an electrical signal propagates from the center to the extremes of the muscle fibers. The electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. The solver solves the so called "monodomain equation" independently for each fiber. The equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e. the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellm`. - - The [*MuscleContractionSolver*](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the active paramter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) +- The [MuscleContractionSolver](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the active paramter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) **The tendon solver** is a dynamic FEM mechanical solver. It models an hyperelastic passive material. The material parameters are chosen as in [Carniel et al.](https://pubmed.ncbi.nlm.nih.gov/28238424/) -## Running the Simulation +## Running the Simulation 1. Preparation: - Install OpenDiHu - In the OpenDiHu website you can find detailed [installation instructions](https://opendihu.readthedocs.io/en/latest/user/installation.html). - - We recommend to download the code from the [GitHub repository](https://github.com/opendihu/opendihu) and to run `make release_without_tests` in the parent directory. + In the OpenDiHu website you can find detailed [installation instructions](https://opendihu.readthedocs.io/en/latest/user/installation.html). + We recommend to download the code from the [GitHub repository](https://github.com/opendihu/opendihu) and to run `make release_without_tests` in the parent directory. > **Note:** OpenDiHu automatically downloads dependencies and installs them in the `opendihu/dependencies/` folder. You can avoid that by setting e.g., `PRECICE_DOWNLOAD = False` in the [user-variables.scons.py](https://github.com/opendihu/opendihu/blob/develop/user-variables.scons.py) before building OpenDiHu. - - - Download input files for OpenDiHu - OpenDiHu requires of input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading this files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [click here](https://zenodo.org/record/4705982/files/input.tgz?download=1) to download the necessary files. Please extract the files and place them on `opendihu/examples/electrophysiology/` directory. - + - Download input files for OpenDiHu + + OpenDiHu requires of input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading this files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [click here](https://zenodo.org/record/4705982/files/input.tgz?download=1) to download the necessary files. Please extract the files and place them on `opendihu/examples/electrophysiology/` directory. + - Setup `$OPENDIHU_HOME` to your `.bashrc` file - ``` - export OPENDIHU_HOME=/path/to/opendihu + ```bash + export OPENDIHU_HOME=/path/to/opendihu ``` + - Compile muscle and tendon solvers ```bash @@ -86,6 +86,7 @@ OpenDiHu is used for the muscle and each tendon participants. cd muscle-opendihu ./run.sh ``` + To start the `Tendon-Bottom` participant, run: ```bash From 5459862cb1ce921feb222bdf08f7309c47a878d1 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:06:45 +0100 Subject: [PATCH 17/40] Fix Markdown linting issues in muscle tutorial README --- muscle-tendon-complex/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index eb2bf4799..83c58a932 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -115,7 +115,7 @@ After the simulation has finished, you can visualize your results using e.g. Par ## References TODO -[1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. _Comput Mech_ **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 +[1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. *Comput Mech* **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 {% disclaimer %} This offering is not approved or endorsed by OpenCFD Limited, producer and distributor of the OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and OpenCFD® trade marks. From c3b4db8aa442cb0060f9ae6e0315333f025596ee Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:18:05 +0100 Subject: [PATCH 18/40] Remove unrelated results image - fix check.sh --- ...als-multiple-perpendicular-flaps-results.png | Bin 89727 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 muscle-tendon-complex/images/tutorials-multiple-perpendicular-flaps-results.png diff --git a/muscle-tendon-complex/images/tutorials-multiple-perpendicular-flaps-results.png b/muscle-tendon-complex/images/tutorials-multiple-perpendicular-flaps-results.png deleted file mode 100644 index aedc406dba2d6e1f2436416193829ffbab82dd6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89727 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

Cw>WJAZS@23c@-#LoNk?#+ir!QV3d_vTNx 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

NeEiXyM!!^}GenYz`tKD8^n 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 CHQXNn From b24c63d21c810ec875de812df846131f4b05c16e Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:22:21 +0100 Subject: [PATCH 19/40] Mark shell scripts as executable --- muscle-tendon-complex/muscle-opendihu/clean.sh | 0 muscle-tendon-complex/muscle-opendihu/run.sh | 0 muscle-tendon-complex/opendihu-solver/build.sh | 0 muscle-tendon-complex/opendihu-solver/clean.sh | 0 muscle-tendon-complex/tendon-bottom-opendihu/clean.sh | 0 muscle-tendon-complex/tendon-bottom-opendihu/run.sh | 0 muscle-tendon-complex/tendon-top-A-opendihu/clean.sh | 0 muscle-tendon-complex/tendon-top-A-opendihu/run.sh | 0 muscle-tendon-complex/tendon-top-B-opendihu/clean.sh | 0 muscle-tendon-complex/tendon-top-B-opendihu/run.sh | 0 10 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 muscle-tendon-complex/muscle-opendihu/clean.sh mode change 100644 => 100755 muscle-tendon-complex/muscle-opendihu/run.sh mode change 100644 => 100755 muscle-tendon-complex/opendihu-solver/build.sh mode change 100644 => 100755 muscle-tendon-complex/opendihu-solver/clean.sh mode change 100644 => 100755 muscle-tendon-complex/tendon-bottom-opendihu/clean.sh mode change 100644 => 100755 muscle-tendon-complex/tendon-bottom-opendihu/run.sh mode change 100644 => 100755 muscle-tendon-complex/tendon-top-A-opendihu/clean.sh mode change 100644 => 100755 muscle-tendon-complex/tendon-top-A-opendihu/run.sh mode change 100644 => 100755 muscle-tendon-complex/tendon-top-B-opendihu/clean.sh mode change 100644 => 100755 muscle-tendon-complex/tendon-top-B-opendihu/run.sh diff --git a/muscle-tendon-complex/muscle-opendihu/clean.sh b/muscle-tendon-complex/muscle-opendihu/clean.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/muscle-opendihu/run.sh b/muscle-tendon-complex/muscle-opendihu/run.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/opendihu-solver/build.sh b/muscle-tendon-complex/opendihu-solver/build.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/opendihu-solver/clean.sh b/muscle-tendon-complex/opendihu-solver/clean.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/run.sh b/muscle-tendon-complex/tendon-bottom-opendihu/run.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/run.sh b/muscle-tendon-complex/tendon-top-A-opendihu/run.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh old mode 100644 new mode 100755 diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/run.sh b/muscle-tendon-complex/tendon-top-B-opendihu/run.sh old mode 100644 new mode 100755 From 50333efec7abf7ab9e933e8a6069ce4964da6b24 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:29:50 +0100 Subject: [PATCH 20/40] Fix shellcheck errors --- muscle-tendon-complex/opendihu-solver/build.sh | 8 +++----- muscle-tendon-complex/opendihu-solver/clean.sh | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/muscle-tendon-complex/opendihu-solver/build.sh b/muscle-tendon-complex/opendihu-solver/build.sh index 7d1a1ea13..d39dddc11 100755 --- a/muscle-tendon-complex/opendihu-solver/build.sh +++ b/muscle-tendon-complex/opendihu-solver/build.sh @@ -1,10 +1,8 @@ #!/bin/bash -if [ -n $OPENDIHU_HOME ] +if [ -n "$OPENDIHU_HOME" ] then - alias sr='$OPENDIHU_HOME/scripts/shortcuts/sr.sh' - alias mkorn='$OPENDIHU_HOME/scripts/shortcuts/mkorn.sh' - mkorn && sr + "$OPENDIHU_HOME/scripts/shortcuts/sr.sh" && "$OPENDIHU_HOME/scripts/shortcuts/mkorn.sh" # copy executables to partipant folders cp build_release/muscle-solver ../muscle-opendihu/ cp build_release/tendon-solver ../tendon-bottom-opendihu/ @@ -12,4 +10,4 @@ then cp build_release/tendon-solver ../tendon-top-B-opendihu/ else echo "OPENDIHU_HOME is not defined" -fi \ No newline at end of file +fi diff --git a/muscle-tendon-complex/opendihu-solver/clean.sh b/muscle-tendon-complex/opendihu-solver/clean.sh index 0a24e263a..619121b2f 100755 --- a/muscle-tendon-complex/opendihu-solver/clean.sh +++ b/muscle-tendon-complex/opendihu-solver/clean.sh @@ -1,6 +1,6 @@ #!/bin/bash -rm -r .* *.log +rm -r .* ./*.log rm -r build_release rm -r precice-profiling @@ -8,4 +8,4 @@ rm -r precice-profiling rm ../muscle-opendihu/muscle-solver rm ../tendon-bottom-opendihu/tendon-solver rm ../tendon-top-A-opendihu/tendon-solver -rm ../tendon-top-B-opendihu/tendon-solver \ No newline at end of file +rm ../tendon-top-B-opendihu/tendon-solver From 0a5858a39d9b7a2032b0b4cbf3e93d227dc75f70 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:49:01 +0100 Subject: [PATCH 21/40] Format with autopep8 autopep8 --in-place --exit-code --aggressive --ignore=E402 --max-line-length=120 file.py --- .../muscle-opendihu/helper.py | 818 ++++++----- .../muscle-opendihu/settings-muscle.py | 1299 ++++++++++------- .../settings-tendon-bottom.py | 486 +++--- .../settings-tendon-top-A.py | 407 ++++-- .../settings-tendon-top-B.py | 407 ++++-- 5 files changed, 2100 insertions(+), 1317 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/helper.py b/muscle-tendon-complex/muscle-opendihu/helper.py index 3dc1a5c35..ecc712d96 100644 --- a/muscle-tendon-complex/muscle-opendihu/helper.py +++ b/muscle-tendon-complex/muscle-opendihu/helper.py @@ -5,10 +5,11 @@ import numpy as np import pickle -import sys,os +import sys +import os import struct import argparse -#sys.path.insert(0, '..') +# sys.path.insert(0, '..') import variables # file variables.py from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings @@ -18,75 +19,90 @@ # generate cuboid fiber file if "cuboid.bin" in variables.fiber_file: - - if variables.n_fibers_y is None: - variables.n_fibers_x = 4 - variables.n_fibers_y = variables.n_fibers_x - variables.n_points_whole_fiber = 20 - - size_x = variables.n_fibers_x * 0.1 - size_y = variables.n_fibers_y * 0.1 - size_z = variables.n_points_whole_fiber / 100. - - if rank_no == 0: - print("create cuboid.bin with size [{},{},{}], n points [{},{},{}]".format(size_x, size_y, size_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber)) - - # write header - with open(variables.fiber_file, "wb") as outfile: - - # write header - header_str = "opendihu self-generated cuboid " - outfile.write(struct.pack('32s',bytes(header_str, 'utf-8'))) # 32 bytes - outfile.write(struct.pack('i', 40)) # header length - outfile.write(struct.pack('i', variables.n_fibers_x*variables.n_fibers_y)) # n_fibers - outfile.write(struct.pack('i', variables.n_points_whole_fiber)) # variables.n_points_whole_fiber - outfile.write(struct.pack('i', 0)) # nBoundaryPointsXNew - outfile.write(struct.pack('i', 0)) # nBoundaryPointsZNew - outfile.write(struct.pack('i', 0)) # nFineGridFibers_ - outfile.write(struct.pack('i', 1)) # nRanks - outfile.write(struct.pack('i', 1)) # nRanksZ - outfile.write(struct.pack('i', 0)) # nFibersPerRank - outfile.write(struct.pack('i', 0)) # date - - # loop over points - for y in range(variables.n_fibers_y): - for x in range(variables.n_fibers_x): - for z in range(variables.n_points_whole_fiber): - point = [x*(float)(size_x)/(variables.n_fibers_x), y*(float)(size_y)/(variables.n_fibers_y), z*(float)(size_z)/(variables.n_points_whole_fiber)] - outfile.write(struct.pack('3d', point[0], point[1], point[2])) # data point + + if variables.n_fibers_y is None: + variables.n_fibers_x = 4 + variables.n_fibers_y = variables.n_fibers_x + variables.n_points_whole_fiber = 20 + + size_x = variables.n_fibers_x * 0.1 + size_y = variables.n_fibers_y * 0.1 + size_z = variables.n_points_whole_fiber / 100. + + if rank_no == 0: + print( + "create cuboid.bin with size [{},{},{}], n points [{},{},{}]".format( + size_x, + size_y, + size_z, + variables.n_fibers_x, + variables.n_fibers_y, + variables.n_points_whole_fiber)) + + # write header + with open(variables.fiber_file, "wb") as outfile: + + # write header + header_str = "opendihu self-generated cuboid " + outfile.write(struct.pack('32s', bytes(header_str, 'utf-8'))) # 32 bytes + outfile.write(struct.pack('i', 40)) # header length + outfile.write(struct.pack('i', variables.n_fibers_x * variables.n_fibers_y)) # n_fibers + outfile.write(struct.pack('i', variables.n_points_whole_fiber)) # variables.n_points_whole_fiber + outfile.write(struct.pack('i', 0)) # nBoundaryPointsXNew + outfile.write(struct.pack('i', 0)) # nBoundaryPointsZNew + outfile.write(struct.pack('i', 0)) # nFineGridFibers_ + outfile.write(struct.pack('i', 1)) # nRanks + outfile.write(struct.pack('i', 1)) # nRanksZ + outfile.write(struct.pack('i', 0)) # nFibersPerRank + outfile.write(struct.pack('i', 0)) # date + + # loop over points + for y in range(variables.n_fibers_y): + for x in range(variables.n_fibers_x): + for z in range(variables.n_points_whole_fiber): + point = [x * (float)(size_x) / (variables.n_fibers_x), y * (float)(size_y) / + (variables.n_fibers_y), z * (float)(size_z) / (variables.n_points_whole_fiber)] + outfile.write(struct.pack('3d', point[0], point[1], point[2])) # data point # output diffusion solver type if rank_no == 0: - print("diffusion solver type: {}".format(variables.diffusion_solver_type)) + print("diffusion solver type: {}".format(variables.diffusion_solver_type)) variables.load_fiber_data = False # load all local node positions from fiber_file, in order to infer partitioning for fat_layer mesh # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( - variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.fiber_file, variables.load_fiber_data, variables.sampling_stride_x, variables.sampling_stride_y, variables.sampling_stride_z, variables.generate_linear_3d_mesh, variables.generate_quadratic_3d_mesh) -[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result - +[variables.meshes, + variables.own_subdomain_coordinate_x, + variables.own_subdomain_coordinate_y, + variables.own_subdomain_coordinate_z, + variables.n_fibers_x, + variables.n_fibers_y, + variables.n_points_whole_fiber] = result + variables.n_subdomains_xy = variables.n_subdomains_x * variables.n_subdomains_y variables.n_fibers_total = variables.n_fibers_x * variables.n_fibers_y # create mappings between meshes -#variables.mappings_between_meshes = {"MeshFiber_{}".format(i) : "3Dmesh" for i in range(variables.n_fibers_total)} -variables.mappings_between_meshes = {"MeshFiber_{}".format(i) : {"name": "3Dmesh", "xiTolerance": 1e-3} for i in range(variables.n_fibers_total)} +# variables.mappings_between_meshes = {"MeshFiber_{}".format(i) : "3Dmesh" for i in range(variables.n_fibers_total)} +variables.mappings_between_meshes = {"MeshFiber_{}".format( + i): {"name": "3Dmesh", "xiTolerance": 1e-3} for i in range(variables.n_fibers_total)} # a higher tolerance includes more fiber dofs that may be almost out of the 3D mesh variables.mappings_between_meshes = { - "MeshFiber_{}".format(i) : { - "name": "3Dmesh_quadratic", - "xiTolerance": variables.mapping_tolerance, - "enableWarnings": False, - "compositeUseOnlyInitializedMappings": False, - "fixUnmappedDofs": True, - "defaultValue": 0, - } for i in range(variables.n_fibers_total) + "MeshFiber_{}".format(i): { + "name": "3Dmesh_quadratic", + "xiTolerance": variables.mapping_tolerance, + "enableWarnings": False, + "compositeUseOnlyInitializedMappings": False, + "fixUnmappedDofs": True, + "defaultValue": 0, + } for i in range(variables.n_fibers_total) } -# set output writer +# set output writer variables.output_writer_fibers = [] variables.output_writer_elasticity = [] variables.output_writer_emg = [] @@ -94,253 +110,356 @@ subfolder = "" if variables.paraview_output: - if variables.adios_output: - subfolder = "paraview/" - variables.output_writer_emg.append({"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) - variables.output_writer_elasticity.append({"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) - variables.output_writer_fibers.append({"format": "Paraview", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) - if variables.states_output: - variables.output_writer_0D_states.append({"format": "Paraview", "outputInterval": 1, "filename": "out/" + subfolder + variables.scenario_name + "/0D_states", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"}) + if variables.adios_output: + subfolder = "paraview/" + variables.output_writer_emg.append({"format": "Paraview", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D_emg), + "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "Paraview", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D), + "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "Paraview", + "outputInterval": int(1. / variables.dt_splitting * variables.output_timestep_fibers), + "filename": "out/" + subfolder + variables.scenario_name + "/fibers", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"}) + if variables.states_output: + variables.output_writer_0D_states.append({"format": "Paraview", + "outputInterval": 1, + "filename": "out/" + subfolder + variables.scenario_name + "/0D_states", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"}) if variables.adios_output: - if variables.paraview_output: - subfolder = "adios/" - variables.output_writer_emg.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "useFrontBackBuffer": False, "combineNInstances": 1, "fileNumbering": "incremental"}) - variables.output_writer_elasticity.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "useFrontBackBuffer": False, "fileNumbering": "incremental"}) - variables.output_writer_fibers.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "combineNInstances": variables.n_subdomains_xy, "useFrontBackBuffer": False, "fileNumbering": "incremental"}) - #variables.output_writer_fibers.append({"format": "MegaMol", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + variables.scenario_name + "/fibers", "combineNInstances": 1, "useFrontBackBuffer": False, "fileNumbering": "incremental"} + if variables.paraview_output: + subfolder = "adios/" + variables.output_writer_emg.append({"format": "MegaMol", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D_emg), + "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", + "useFrontBackBuffer": False, + "combineNInstances": 1, + "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "MegaMol", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D), + "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", + "useFrontBackBuffer": False, + "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "MegaMol", + "outputInterval": int(1. / variables.dt_splitting * variables.output_timestep_fibers), + "filename": "out/" + subfolder + variables.scenario_name + "/fibers", + "combineNInstances": variables.n_subdomains_xy, + "useFrontBackBuffer": False, + "fileNumbering": "incremental"}) + # variables.output_writer_fibers.append({"format": "MegaMol", + # "outputInterval": + # int(1./variables.dt_splitting*variables.output_timestep_fibers), + # "filename": "out/" + variables.scenario_name + "/fibers", + # "combineNInstances": 1, "useFrontBackBuffer": False, "fileNumbering": + # "incremental"} if variables.python_output: - if variables.adios_output: - subfolder = "python/" - variables.output_writer_emg.append({"format": "PythonFile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "binary": True, "fileNumbering": "incremental"}) - variables.output_writer_elasticity.append({"format": "PythonFile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "binary": True, "fileNumbering": "incremental"}) - variables.output_writer_fibers.append({"format": "PythonFile", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "binary": True, "fileNumbering": "incremental"}) + if variables.adios_output: + subfolder = "python/" + variables.output_writer_emg.append({"format": "PythonFile", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D_emg), + "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", + "binary": True, + "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "PythonFile", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D), + "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", + "binary": True, + "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "PythonFile", + "outputInterval": int(1. / variables.dt_splitting * variables.output_timestep_fibers), + "filename": "out/" + subfolder + variables.scenario_name + "/fibers", + "binary": True, + "fileNumbering": "incremental"}) if variables.exfile_output: - if variables.adios_output: - subfolder = "exfile/" - variables.output_writer_emg.append({"format": "Exfile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D_emg), "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", "fileNumbering": "incremental"}) - variables.output_writer_elasticity.append({"format": "Exfile", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", "fileNumbering": "incremental"}) - variables.output_writer_fibers.append({"format": "Exfile", "outputInterval": int(1./variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/" + subfolder + variables.scenario_name + "/fibers", "fileNumbering": "incremental"}) + if variables.adios_output: + subfolder = "exfile/" + variables.output_writer_emg.append({"format": "Exfile", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D_emg), + "filename": "out/" + subfolder + variables.scenario_name + "/hd_emg", + "fileNumbering": "incremental"}) + variables.output_writer_elasticity.append({"format": "Exfile", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D), + "filename": "out/" + subfolder + variables.scenario_name + "/elasticity", + "fileNumbering": "incremental"}) + variables.output_writer_fibers.append({"format": "Exfile", + "outputInterval": int(1. / variables.dt_splitting * variables.output_timestep_fibers), + "filename": "out/" + subfolder + variables.scenario_name + "/fibers", + "fileNumbering": "incremental"}) # set variable mappings for cellml model if "hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" in variables.cellml_file: # hodgkin huxley membrane with fatigue from shorten - # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} - variables.mappings = { - ("parameter", 0): ("constant", "membrane/i_Stim"), # parameter 0 is I_stim - ("parameter", 1): ("constant", "razumova/l_hs"), # parameter 1 is fiber stretch λ - ("parameter", 2): ("constant", "razumova/velocity"), # parameter 2 is fiber contraction velocity \dot{λ} - ("connectorSlot", "vm"): ("state", "membrane/V"), # expose state Vm to the operator splitting - ("connectorSlot", "stress"):("algebraic", "razumova/activestress"), # expose algebraic γ to the operator splitting - ("connectorSlot", "alpha"): ("algebraic", "razumova/activation"), # expose algebraic α to the operator splitting - - ("connectorSlot", "lambda"):"razumova/l_hs", # connect output "lamda" of mechanics solver to parameter 1 (l_hs) - ("connectorSlot", "ldot"): "razumova/velocity", # connect output "ldot" of mechanics solver to parameter 2 (rel_velo) - } - variables.parameters_initial_values = [0, 1, 0] # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/rel_velo = \dot{λ} - variables.nodal_stimulation_current = 40. # not used - variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) - variables.enable_force_length_relation = False # disable computation of force-length relation in opendihu, as it is carried out in CellML model - variables.lambda_dot_scaling_factor = 7.815e-05 # scaling factor to convert dimensionless contraction velocity to shortening velocity, velocity = factor*\dot{lambda} + # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} + variables.mappings = { + ("parameter", 0): ("constant", "membrane/i_Stim"), # parameter 0 is I_stim + ("parameter", 1): ("constant", "razumova/l_hs"), # parameter 1 is fiber stretch λ + ("parameter", 2): ("constant", "razumova/velocity"), # parameter 2 is fiber contraction velocity \dot{λ} + ("connectorSlot", "vm"): ("state", "membrane/V"), # expose state Vm to the operator splitting + ("connectorSlot", "stress"): ("algebraic", "razumova/activestress"), # expose algebraic γ to the operator splitting + ("connectorSlot", "alpha"): ("algebraic", "razumova/activation"), # expose algebraic α to the operator splitting + + # connect output "lamda" of mechanics solver to parameter 1 (l_hs) + ("connectorSlot", "lambda"): "razumova/l_hs", + # connect output "ldot" of mechanics solver to parameter 2 (rel_velo) + ("connectorSlot", "ldot"): "razumova/velocity", + } + # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/rel_velo = \dot{λ} + variables.parameters_initial_values = [0, 1, 0] + variables.nodal_stimulation_current = 40. # not used + # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.vm_value_stimulated = 40. + # disable computation of force-length relation in opendihu, as it is carried out in CellML model + variables.enable_force_length_relation = False + # scaling factor to convert dimensionless contraction velocity to + # shortening velocity, velocity = factor*\dot{lambda} + variables.lambda_dot_scaling_factor = 7.815e-05 elif "hodgkin_huxley" in variables.cellml_file: - # parameters: I_stim - variables.mappings = { - ("parameter", 0): ("constant", "membrane/i_Stim"), # parameter 0 is constant 2 = I_stim - ("connectorSlot", "vm"): "membrane/V", # expose state 0 = Vm to the operator splitting - } - variables.parameters_initial_values = [0.0] # initial value for stimulation current - variables.nodal_stimulation_current = 40. # not used - variables.vm_value_stimulated = 20. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + # parameters: I_stim + variables.mappings = { + ("parameter", 0): ("constant", "membrane/i_Stim"), # parameter 0 is constant 2 = I_stim + ("connectorSlot", "vm"): "membrane/V", # expose state 0 = Vm to the operator splitting + } + variables.parameters_initial_values = [0.0] # initial value for stimulation current + variables.nodal_stimulation_current = 40. # not used + # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.vm_value_stimulated = 20. elif "shorten" in variables.cellml_file: - # parameters: stimulation current I_stim, fiber stretch λ - variables.mappings = { - ("parameter", 0): ("algebraic", "wal_environment/I_HH"), # parameter is algebraic 32 - ("parameter", 1): ("constant", "razumova/L_x"), # parameter is constant 65, fiber stretch λ, this indicates how much the fiber has stretched, 1 means no extension - ("connectorSlot", "vm"): "wal_environment/vS", # expose state 0 = Vm to the operator splitting - } - variables.parameters_initial_values = [0.0, 1.0] # stimulation current I_stim, fiber stretch λ - variables.nodal_stimulation_current = 1200. # not used - variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) - + # parameters: stimulation current I_stim, fiber stretch λ + variables.mappings = { + ("parameter", 0): ("algebraic", "wal_environment/I_HH"), # parameter is algebraic 32 + # parameter is constant 65, fiber stretch λ, this indicates how much the + # fiber has stretched, 1 means no extension + ("parameter", 1): ("constant", "razumova/L_x"), + ("connectorSlot", "vm"): "wal_environment/vS", # expose state 0 = Vm to the operator splitting + } + variables.parameters_initial_values = [0.0, 1.0] # stimulation current I_stim, fiber stretch λ + variables.nodal_stimulation_current = 1200. # not used + # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.vm_value_stimulated = 40. + elif "slow_TK_2014" in variables.cellml_file: # this is (3a, "MultiPhysStrain", old tomo mechanics) in OpenCMISS - # parameters: I_stim, fiber stretch λ - variables.mappings = { - ("parameter", 0): ("constant", "wal_environment/I_HH"), # parameter 0 is constant 54 = I_stim - ("parameter", 1): ("constant", "razumova/L_S"), # parameter 1 is constant 67 = fiber stretch λ - ("connectorSlot", "vm"): "wal_environment/vS", # expose state 0 = Vm to the operator splitting - ("connectorSlot", "stress"):"razumova/stress", # expose algebraic 12 = γ to the operator splitting - } - variables.parameters_initial_values = [0.0, 1.0] # wal_environment/I_HH = I_stim, razumova/L_S = λ - variables.nodal_stimulation_current = 40. # not used - variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) - -elif "Aliev_Panfilov_Razumova_2016_08_22" in variables.cellml_file : # this is (3, "MultiPhysStrain", numerically more stable) in OpenCMISS, this only computes A1,A2,x1,x2 not the stress - # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} - variables.mappings = { - ("parameter", 0): ("constant", "Aliev_Panfilov/I_HH"), # parameter 0 is constant 0 = I_stim - ("parameter", 1): ("constant", "Razumova/l_hs"), # parameter 1 is constant 8 = fiber stretch λ - ("parameter", 2): ("constant", "Razumova/velo"), # parameter 2 is constant 9 = fiber contraction velocity \dot{λ} - ("connectorSlot", "vm"): "Aliev_Panfilov/V_m", # expose state 0 = Vm to the operator splitting - ("connectorSlot", "stress"):"Razumova/sigma", # expose algebraic 0 = γ to the operator splitting - } - variables.parameters_initial_values = [0, 1, 0] # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/velo = \dot{λ} - variables.nodal_stimulation_current = 40. # not used - variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) - + # parameters: I_stim, fiber stretch λ + variables.mappings = { + ("parameter", 0): ("constant", "wal_environment/I_HH"), # parameter 0 is constant 54 = I_stim + ("parameter", 1): ("constant", "razumova/L_S"), # parameter 1 is constant 67 = fiber stretch λ + ("connectorSlot", "vm"): "wal_environment/vS", # expose state 0 = Vm to the operator splitting + # expose algebraic 12 = γ to the operator splitting + ("connectorSlot", "stress"): "razumova/stress", + } + # wal_environment/I_HH = I_stim, razumova/L_S = λ + variables.parameters_initial_values = [0.0, 1.0] + variables.nodal_stimulation_current = 40. # not used + # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.vm_value_stimulated = 40. + +# this is (3, "MultiPhysStrain", numerically more stable) in OpenCMISS, this only computes A1,A2,x1,x2 not the stress +elif "Aliev_Panfilov_Razumova_2016_08_22" in variables.cellml_file: + # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} + variables.mappings = { + ("parameter", 0): ("constant", "Aliev_Panfilov/I_HH"), # parameter 0 is constant 0 = I_stim + ("parameter", 1): ("constant", "Razumova/l_hs"), # parameter 1 is constant 8 = fiber stretch λ + # parameter 2 is constant 9 = fiber contraction velocity \dot{λ} + ("parameter", 2): ("constant", "Razumova/velo"), + ("connectorSlot", "vm"): "Aliev_Panfilov/V_m", # expose state 0 = Vm to the operator splitting + # expose algebraic 0 = γ to the operator splitting + ("connectorSlot", "stress"): "Razumova/sigma", + } + # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/velo = \dot{λ} + variables.parameters_initial_values = [0, 1, 0] + variables.nodal_stimulation_current = 40. # not used + # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.vm_value_stimulated = 40. + elif "Aliev_Panfilov_Razumova_Titin" in variables.cellml_file: # this is (4, "Titin") in OpenCMISS - # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} - variables.mappings = { - ("parameter", 0): ("constant", "Aliev_Panfilov/I_HH"), # parameter 0 is constant 0 = I_stim - ("parameter", 1): ("constant", "Razumova/l_hs"), # parameter 1 is constant 11 = fiber stretch λ - ("parameter", 2): ("constant", "Razumova/rel_velo"), # parameter 2 is constant 12 = fiber contraction velocity \dot{λ} - ("connectorSlot", "vm"): ("state", "Aliev_Panfilov/V_m"), # expose state 0 = Vm to the operator splitting - ("connectorSlot", "stress"):("algebraic", "Razumova/ActiveStress"), # expose algebraic 4 = γ to the operator splitting - ("connectorSlot", "alpha"): ("algebraic", "Razumova/Activation"), # expose algebraic 5 = α to the operator splitting - } - variables.parameters_initial_values = [0, 1, 0] # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/rel_velo = \dot{λ} - variables.nodal_stimulation_current = 40. # not used - variables.vm_value_stimulated = 40. # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + # parameters: I_stim, fiber stretch λ, fiber contraction velocity \dot{λ} + variables.mappings = { + ("parameter", 0): ("constant", "Aliev_Panfilov/I_HH"), # parameter 0 is constant 0 = I_stim + ("parameter", 1): ("constant", "Razumova/l_hs"), # parameter 1 is constant 11 = fiber stretch λ + # parameter 2 is constant 12 = fiber contraction velocity \dot{λ} + ("parameter", 2): ("constant", "Razumova/rel_velo"), + ("connectorSlot", "vm"): ("state", "Aliev_Panfilov/V_m"), # expose state 0 = Vm to the operator splitting + # expose algebraic 4 = γ to the operator splitting + ("connectorSlot", "stress"): ("algebraic", "Razumova/ActiveStress"), + # expose algebraic 5 = α to the operator splitting + ("connectorSlot", "alpha"): ("algebraic", "Razumova/Activation"), + } + # Aliev_Panfilov/I_HH = I_stim, Razumova/l_hs = λ, Razumova/rel_velo = \dot{λ} + variables.parameters_initial_values = [0, 1, 0] + variables.nodal_stimulation_current = 40. # not used + # to which value of Vm the stimulated node should be set (option "valueForStimulatedPoint" of FastMonodomainSolver) + variables.vm_value_stimulated = 40. # callback functions # -------------------------- def get_motor_unit_no(fiber_no): - return int(variables.fiber_distribution[fiber_no % len(variables.fiber_distribution)]-1) + return int(variables.fiber_distribution[fiber_no % len(variables.fiber_distribution)] - 1) + def get_diffusion_prefactor(fiber_no, mu_no): - diffusion_prefactor = variables.get_conductivity(fiber_no, mu_no) / (variables.get_am(fiber_no, mu_no) * variables.get_cm(fiber_no, mu_no)) - #print("diffusion_prefactor: {}/({}*{}) = {}".format(variables.get_conductivity(fiber_no, mu_no), variables.get_am(fiber_no, mu_no), variables.get_cm(fiber_no, mu_no), diffusion_prefactor)) - return diffusion_prefactor + diffusion_prefactor = variables.get_conductivity( + fiber_no, mu_no) / (variables.get_am(fiber_no, mu_no) * variables.get_cm(fiber_no, mu_no)) + # print("diffusion_prefactor: {}/({}*{}) = {}".format(variables.get_conductivity(fiber_no, mu_no), variables.get_am(fiber_no, mu_no), variables.get_cm(fiber_no, mu_no), diffusion_prefactor)) + return diffusion_prefactor + def fiber_gets_stimulated(fiber_no, frequency, current_time): - """ - determine if fiber fiber_no gets stimulated at simulation time current_time - """ - - # determine motor unit - alpha = 1.0 # 0.8 - mu_no = (int)(get_motor_unit_no(fiber_no)*alpha) - - # determine if fiber fires now - index = int(np.round(current_time * frequency)) - n_firing_times = np.size(variables.firing_times,0) - - #if variables.firing_times[index % n_firing_times, mu_no] == 1: - #print("{}: fiber {} is mu {}, t = {}, row: {}, stimulated: {} {}".format(rank_no, fiber_no, mu_no, current_time, (index % n_firing_times), variables.firing_times[index % n_firing_times, mu_no], "true" if variables.firing_times[index % n_firing_times, mu_no] == 1 else "false")) - - return variables.firing_times[index % n_firing_times, mu_no] == 1 - + """ + determine if fiber fiber_no gets stimulated at simulation time current_time + """ + + # determine motor unit + alpha = 1.0 # 0.8 + mu_no = (int)(get_motor_unit_no(fiber_no) * alpha) + + # determine if fiber fires now + index = int(np.round(current_time * frequency)) + n_firing_times = np.size(variables.firing_times, 0) + + # if variables.firing_times[index % n_firing_times, mu_no] == 1: + # print("{}: fiber {} is mu {}, t = {}, row: {}, stimulated: {} {}".format(rank_no, fiber_no, mu_no, current_time, (index % n_firing_times), variables.firing_times[index % n_firing_times, mu_no], "true" if variables.firing_times[index % n_firing_times, mu_no] == 1 else "false")) + + return variables.firing_times[index % n_firing_times, mu_no] == 1 + + def set_parameters(n_nodes_global, time_step_no, current_time, parameters, dof_nos_global, fiber_no): - - # determine if fiber gets stimulated at the current time - is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) - - # determine nodes to stimulate (center node, left and right neighbour) - innervation_zone_width_n_nodes = variables.innervation_zone_width*100 # 100 nodes per cm - innervation_node_global = int(n_nodes_global / 2) # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) - nodes_to_stimulate_global = [innervation_node_global] - if innervation_node_global > 0: - nodes_to_stimulate_global.insert(0, innervation_node_global-1) - if innervation_node_global < n_nodes_global-1: - nodes_to_stimulate_global.append(innervation_node_global+1) - - # stimulation value - if is_fiber_gets_stimulated: - stimulation_current = variables.nodal_stimulation_current - else: - stimulation_current = 0. - - first_dof_global = dof_nos_global[0] - last_dof_global = dof_nos_global[-1] - - for node_no_global in nodes_to_stimulate_global: - if first_dof_global <= node_no_global <= last_dof_global: - # get local no for global no (1D) - dof_no_local = node_no_global - first_dof_global - parameters[dof_no_local] = stimulation_current - + + # determine if fiber gets stimulated at the current time + is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) + + # determine nodes to stimulate (center node, left and right neighbour) + innervation_zone_width_n_nodes = variables.innervation_zone_width * 100 # 100 nodes per cm + # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + innervation_node_global = int(n_nodes_global / 2) + nodes_to_stimulate_global = [innervation_node_global] + if innervation_node_global > 0: + nodes_to_stimulate_global.insert(0, innervation_node_global - 1) + if innervation_node_global < n_nodes_global - 1: + nodes_to_stimulate_global.append(innervation_node_global + 1) + + # stimulation value + if is_fiber_gets_stimulated: + stimulation_current = variables.nodal_stimulation_current + else: + stimulation_current = 0. + + first_dof_global = dof_nos_global[0] + last_dof_global = dof_nos_global[-1] + + for node_no_global in nodes_to_stimulate_global: + if first_dof_global <= node_no_global <= last_dof_global: + # get local no for global no (1D) + dof_no_local = node_no_global - first_dof_global + parameters[dof_no_local] = stimulation_current + # callback function that can set parameters, i.e. stimulation current -def set_specific_parameters(n_nodes_global, time_step_no, current_time, parameters, fiber_no): - - # determine if fiber gets stimulated at the current time - is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) - - # determine nodes to stimulate (center node, left and right neighbour) - innervation_zone_width_n_nodes = variables.innervation_zone_width*100 # 100 nodes per cm - innervation_node_global = int(n_nodes_global / 2) # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) - nodes_to_stimulate_global = [innervation_node_global] - - for k in range(10): - if innervation_node_global-k >= 0: - nodes_to_stimulate_global.insert(0, innervation_node_global-k) - if innervation_node_global+k <= n_nodes_global-1: - nodes_to_stimulate_global.append(innervation_node_global+k) - - # stimulation value - if is_fiber_gets_stimulated: - stimulation_current = 40. - else: - stimulation_current = 0. - - for node_no_global in nodes_to_stimulate_global: - parameters[(node_no_global,0)] = stimulation_current # key: ((x,y,z),nodal_dof_index) -# callback function that can set states, i.e. prescribed values for stimulation -def set_specific_states(n_nodes_global, time_step_no, current_time, states, fiber_no): - #print("call set_specific_states at time {}".format(current_time)) - - # determine if fiber gets stimulated at the current time - is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) +def set_specific_parameters(n_nodes_global, time_step_no, current_time, parameters, fiber_no): + + # determine if fiber gets stimulated at the current time + is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) - if is_fiber_gets_stimulated: # determine nodes to stimulate (center node, left and right neighbour) - innervation_zone_width_n_nodes = variables.innervation_zone_width*100 # 100 nodes per cm - innervation_node_global = int(n_nodes_global / 2) # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + innervation_zone_width_n_nodes = variables.innervation_zone_width * 100 # 100 nodes per cm + # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + innervation_node_global = int(n_nodes_global / 2) nodes_to_stimulate_global = [innervation_node_global] - if innervation_node_global > 0: - nodes_to_stimulate_global.insert(0, innervation_node_global-1) - if innervation_node_global < n_nodes_global-1: - nodes_to_stimulate_global.append(innervation_node_global+1) - #if rank_no == 0: - # print("t: {}, stimulate fiber {} at nodes {}".format(current_time, fiber_no, nodes_to_stimulate_global)) + + for k in range(10): + if innervation_node_global - k >= 0: + nodes_to_stimulate_global.insert(0, innervation_node_global - k) + if innervation_node_global + k <= n_nodes_global - 1: + nodes_to_stimulate_global.append(innervation_node_global + k) + + # stimulation value + if is_fiber_gets_stimulated: + stimulation_current = 40. + else: + stimulation_current = 0. for node_no_global in nodes_to_stimulate_global: - states[(node_no_global,0,0)] = variables.vm_value_stimulated # key: ((x,y,z),nodal_dof_index,state_no) + parameters[(node_no_global, 0)] = stimulation_current # key: ((x,y,z),nodal_dof_index) + +# callback function that can set states, i.e. prescribed values for stimulation + + +def set_specific_states(n_nodes_global, time_step_no, current_time, states, fiber_no): + + # print("call set_specific_states at time {}".format(current_time)) + + # determine if fiber gets stimulated at the current time + is_fiber_gets_stimulated = fiber_gets_stimulated(fiber_no, variables.stimulation_frequency, current_time) + + if is_fiber_gets_stimulated: + # determine nodes to stimulate (center node, left and right neighbour) + innervation_zone_width_n_nodes = variables.innervation_zone_width * 100 # 100 nodes per cm + # + np.random.randint(-innervation_zone_width_n_nodes/2,innervation_zone_width_n_nodes/2+1) + innervation_node_global = int(n_nodes_global / 2) + nodes_to_stimulate_global = [innervation_node_global] + if innervation_node_global > 0: + nodes_to_stimulate_global.insert(0, innervation_node_global - 1) + if innervation_node_global < n_nodes_global - 1: + nodes_to_stimulate_global.append(innervation_node_global + 1) + # if rank_no == 0: + # print("t: {}, stimulate fiber {} at nodes {}".format(current_time, fiber_no, nodes_to_stimulate_global)) + + for node_no_global in nodes_to_stimulate_global: + states[(node_no_global, 0, 0)] = variables.vm_value_stimulated # key: ((x,y,z),nodal_dof_index,state_no) # callback function for artifical stress values, instead of monodomain -def set_stress_values(n_dofs_global, n_nodes_global_per_coordinate_direction, time_step_no, current_time, values, global_natural_dofs, fiber_no): + + +def set_stress_values(n_dofs_global, n_nodes_global_per_coordinate_direction, + time_step_no, current_time, values, global_natural_dofs, fiber_no): # n_dofs_global: (int) global number of dofs in the mesh where to set the values - # n_nodes_global_per_coordinate_direction (list of ints) [mx, my, mz] number of global nodes in each coordinate direction. + # n_nodes_global_per_coordinate_direction (list of ints) [mx, my, mz] number of global nodes in each coordinate direction. # For composite meshes, the values are only for the first submesh, for other meshes sum(...) equals n_dofs_global # time_step_no: (int) current time step number # current_time: (float) the current simulation time - # values: (list of floats) all current local values of the field variable, if there are multiple components, they are stored in struct-of-array memory layout + # values: (list of floats) all current local values of the field variable, if there are multiple components, they are stored in struct-of-array memory layout # i.e. [point0_component0, point0_component1, ... pointN_component0, point1_component0, point1_component1, ...] # After the call, these values will be assigned to the field variable. # global_natural_dofs (list of ints) for every local dof no. the dof no. in global natural ordering # additional_argument: The value of the option "additionalArgument", can be any Python object. - + # loop over nodes in fiber for local_dof_no in range(len(values)): - # get the global no. of the current dof - global_dof_no = global_natural_dofs[local_dof_no] - - n_nodes_per_fiber = n_nodes_global_per_coordinate_direction[0] - - k = global_dof_no - N = n_nodes_per_fiber - - if k > N/2: - k = N/2 - k - else: - k = k - N/2 - - values[local_dof_no] = 0.1*np.sin((current_time/100 + 0.2*k/N + 0.1*fiber_no/variables.n_fibers_total) * 2*np.pi) ** 2 - + # get the global no. of the current dof + global_dof_no = global_natural_dofs[local_dof_no] + + n_nodes_per_fiber = n_nodes_global_per_coordinate_direction[0] + + k = global_dof_no + N = n_nodes_per_fiber + + if k > N / 2: + k = N / 2 - k + else: + k = k - N / 2 + + values[local_dof_no] = 0.1 * np.sin((current_time / 100 + 0.2 * k / N + 0.1 * + fiber_no / variables.n_fibers_total) * 2 * np.pi) ** 2 + # load MU distribution and firing times variables.fiber_distribution = np.genfromtxt(variables.fiber_distribution_file, delimiter=" ") @@ -348,85 +467,109 @@ def set_stress_values(n_dofs_global, n_nodes_global_per_coordinate_direction, ti # for debugging output show when the first 20 fibers will fire if rank_no == 0 and not variables.disable_firing_output: - print("Debugging output about fiber firing: Taking input from file \"{}\"".format(variables.firing_times_file)) - import timeit - t_start = timeit.default_timer() - - first_stimulation_info = [] - - n_firing_times = np.size(variables.firing_times,0) - for fiber_no_index in range(variables.n_fibers_total): - if fiber_no_index % 100 == 0: - t_algebraic = timeit.default_timer() - if t_algebraic - t_start > 100: - print("Note: break after {}/{} fibers ({:.0f}%) because it already took {:.3f}s".format(fiber_no_index,variables.n_fibers_total,100.0*fiber_no_index/(variables.n_fibers_total-1.),t_algebraic - t_start)) - break - - first_stimulation = None - for current_time in np.linspace(0,1./variables.stimulation_frequency*n_firing_times,n_firing_times): - if fiber_gets_stimulated(fiber_no_index, variables.stimulation_frequency, current_time): - first_stimulation = current_time - break - mu_no = get_motor_unit_no(fiber_no_index) - first_stimulation_info.append([fiber_no_index,mu_no,first_stimulation]) - - first_stimulation_info.sort(key=lambda x: 1e6+1e-6*x[1]+1e-12*x[0] if x[2] is None else x[2]+1e-6*x[1]+1e-12*x[0]) - - print("First stimulation times") - print(" Time MU fibers") - n_stimulated_mus = 0 - n_not_stimulated_mus = 0 - stimulated_fibers = [] - last_time = 0 - last_mu_no = first_stimulation_info[0][1] - for stimulation_info in first_stimulation_info: - mu_no = stimulation_info[1] - fiber_no = stimulation_info[0] - if mu_no == last_mu_no: - stimulated_fibers.append(fiber_no) - else: - if last_time is not None: - if len(stimulated_fibers) > 10: - print("{:8.2f} {:3} {} (only showing first 10, {} total)".format(last_time,last_mu_no,str(stimulated_fibers[0:10]),len(stimulated_fibers))) - else: - print("{:8.2f} {:3} {}".format(last_time,last_mu_no,str(stimulated_fibers))) - n_stimulated_mus += 1 - else: - if len(stimulated_fibers) > 10: - print(" never stimulated: MU {:3}, fibers {} (only showing first 10, {} total)".format(last_mu_no,str(stimulated_fibers[0:10]),len(stimulated_fibers))) + print("Debugging output about fiber firing: Taking input from file \"{}\"".format(variables.firing_times_file)) + import timeit + t_start = timeit.default_timer() + + first_stimulation_info = [] + + n_firing_times = np.size(variables.firing_times, 0) + for fiber_no_index in range(variables.n_fibers_total): + if fiber_no_index % 100 == 0: + t_algebraic = timeit.default_timer() + if t_algebraic - t_start > 100: + print("Note: break after {}/{} fibers ({:.0f}%) because it already took {:.3f}s".format(fiber_no_index, + variables.n_fibers_total, 100.0 * fiber_no_index / (variables.n_fibers_total - 1.), t_algebraic - t_start)) + break + + first_stimulation = None + for current_time in np.linspace(0, 1. / variables.stimulation_frequency * n_firing_times, n_firing_times): + if fiber_gets_stimulated(fiber_no_index, variables.stimulation_frequency, current_time): + first_stimulation = current_time + break + mu_no = get_motor_unit_no(fiber_no_index) + first_stimulation_info.append([fiber_no_index, mu_no, first_stimulation]) + + first_stimulation_info.sort( + key=lambda x: 1e6 + + 1e-6 * + x[1] + + 1e-12 * + x[0] if x[2] is None else x[2] + + 1e-6 * + x[1] + + 1e-12 * + x[0]) + + print("First stimulation times") + print(" Time MU fibers") + n_stimulated_mus = 0 + n_not_stimulated_mus = 0 + stimulated_fibers = [] + last_time = 0 + last_mu_no = first_stimulation_info[0][1] + for stimulation_info in first_stimulation_info: + mu_no = stimulation_info[1] + fiber_no = stimulation_info[0] + if mu_no == last_mu_no: + stimulated_fibers.append(fiber_no) else: - print(" never stimulated: MU {:3}, fibers {}".format(last_mu_no,str(stimulated_fibers))) - n_not_stimulated_mus += 1 - stimulated_fibers = [fiber_no] - - last_time = stimulation_info[2] - last_mu_no = mu_no - - print("stimulated MUs: {}, not stimulated MUs: {}".format(n_stimulated_mus,n_not_stimulated_mus)) - - t_end = timeit.default_timer() - print("duration of assembling this list: {:.3f} s\n".format(t_end-t_start)) - + if last_time is not None: + if len(stimulated_fibers) > 10: + print("{:8.2f} {:3} {} (only showing first 10, {} total)".format( + last_time, last_mu_no, str(stimulated_fibers[0:10]), len(stimulated_fibers))) + else: + print("{:8.2f} {:3} {}".format(last_time, last_mu_no, str(stimulated_fibers))) + n_stimulated_mus += 1 + else: + if len(stimulated_fibers) > 10: + print(" never stimulated: MU {:3}, fibers {} (only showing first 10, {} total)".format( + last_mu_no, str(stimulated_fibers[0:10]), len(stimulated_fibers))) + else: + print(" never stimulated: MU {:3}, fibers {}".format(last_mu_no, str(stimulated_fibers))) + n_not_stimulated_mus += 1 + stimulated_fibers = [fiber_no] + + last_time = stimulation_info[2] + last_mu_no = mu_no + + print("stimulated MUs: {}, not stimulated MUs: {}".format(n_stimulated_mus, n_not_stimulated_mus)) + + t_end = timeit.default_timer() + print("duration of assembling this list: {:.3f} s\n".format(t_end - t_start)) + # compute partitioning if rank_no == 0: - if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: - print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) - quit() - + if n_ranks != variables.n_subdomains_x * variables.n_subdomains_y * variables.n_subdomains_z: + print( + "\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format( + n_ranks, + variables.n_subdomains_x, + variables.n_subdomains_y, + variables.n_subdomains_z, + variables.n_subdomains_x * + variables.n_subdomains_y * + variables.n_subdomains_z)) + quit() + # n_fibers_per_subdomain_* is already set #################################### # set Dirichlet BC for the flow problem -n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) -n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) -n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) -n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) + for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) + for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) + for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x * \ + n_points_3D_mesh_linear_global_y * n_points_3D_mesh_linear_global_z + +n_points_3D_mesh_quadratic_global_x = 2 * n_points_3D_mesh_linear_global_x - 1 +n_points_3D_mesh_quadratic_global_y = 2 * n_points_3D_mesh_linear_global_y - 1 +n_points_3D_mesh_quadratic_global_z = 2 * n_points_3D_mesh_linear_global_z - 1 -n_points_3D_mesh_quadratic_global_x = 2*n_points_3D_mesh_linear_global_x - 1 -n_points_3D_mesh_quadratic_global_y = 2*n_points_3D_mesh_linear_global_y - 1 -n_points_3D_mesh_quadratic_global_z = 2*n_points_3D_mesh_linear_global_z - 1 - # set boundary conditions for the elasticity [mx, my, mz] = variables.meshes["3Dmesh_quadratic"]["nPointsGlobal"] [nx, ny, nz] = variables.meshes["3Dmesh_quadratic"]["nElements"] @@ -436,22 +579,21 @@ def set_stress_values(n_dofs_global, n_nodes_global_per_coordinate_direction, ti # set Dirichlet BC at top nodes for linear elasticity problem, fix muscle at top variables.elasticity_dirichlet_bc = {} if False: - for j in range(my): - for i in range(mx): - variables.elasticity_dirichlet_bc[(mz-1)*mx*my + j*mx + i] = [0.0,0.0,0.0,0.0,0.0,0.0] - + for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[(mz - 1) * mx * my + j * mx + i] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + # fix muscle at bottom if False: - k = 0 - for j in range(my): - for i in range(mx): - variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,0.0,0.0,0.0,0.0] - + k = 0 + for j in range(my): + for i in range(mx): + variables.elasticity_dirichlet_bc[k * mx * my + j * mx + i] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + # Neumann BC at top nodes, traction upwards -#k = nz-1 -#variables.elasticity_neumann_bc = [{"element": k*nx*ny + j*nx + i, "constantVector": [0.0,0.0,10.0], "face": "2+"} for j in range(ny) for i in range(nx)] +# k = nz-1 +# variables.elasticity_neumann_bc = [{"element": k*nx*ny + j*nx + i, "constantVector": [0.0,0.0,10.0], "face": "2+"} for j in range(ny) for i in range(nx)] variables.elasticity_neumann_bc = [] -#with open("mesh","w") as f: +# with open("mesh","w") as f: # f.write(str(variables.meshes["3Dmesh_quadratic"])) - diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py index 26606ce47..7dce41893 100644 --- a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -1,6 +1,7 @@ # Multiple 1D fibers (monodomain) with 3D dynamic mooney rivlin with active contraction term, on biceps geometry -import sys, os +import sys +import os import timeit import argparse import importlib @@ -17,119 +18,243 @@ # add variables subfolder to python path where the variables script is located script_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, script_path) -sys.path.insert(0, os.path.join(script_path,'variables')) +sys.path.insert(0, os.path.join(script_path, 'variables')) -import variables # file variables.py, defined default values for all parameters, you can set the parameters there -from create_partitioned_meshes_for_settings import * # file create_partitioned_meshes_for_settings with helper functions about own subdomain +import variables # file variables.py, defined default values for all parameters, you can set the parameters there +# file create_partitioned_meshes_for_settings with helper functions about own subdomain +from create_partitioned_meshes_for_settings import * # if first argument contains "*.py", it is a custom variable definition file, load these values if ".py" in sys.argv[0]: - variables_path_and_filename = sys.argv[0] - variables_path,variables_filename = os.path.split(variables_path_and_filename) # get path and filename - sys.path.insert(0, os.path.join(script_path,variables_path)) # add the directory of the variables file to python path - variables_module,_ = os.path.splitext(variables_filename) # remove the ".py" extension to get the name of the module - - if rank_no == 0: - print("Loading variables from \"{}\".".format(variables_path_and_filename)) - - custom_variables = importlib.import_module(variables_module, package=variables_filename) # import variables module - variables.__dict__.update(custom_variables.__dict__) - sys.argv = sys.argv[1:] # remove first argument, which now has already been parsed + variables_path_and_filename = sys.argv[0] + variables_path, variables_filename = os.path.split(variables_path_and_filename) # get path and filename + # add the directory of the variables file to python path + sys.path.insert(0, os.path.join(script_path, variables_path)) + # remove the ".py" extension to get the name of the module + variables_module, _ = os.path.splitext(variables_filename) + + if rank_no == 0: + print("Loading variables from \"{}\".".format(variables_path_and_filename)) + + custom_variables = importlib.import_module(variables_module, + package=variables_filename) # import variables module + variables.__dict__.update(custom_variables.__dict__) + sys.argv = sys.argv[1:] # remove first argument, which now has already been parsed else: - if rank_no == 0: - print("Error: no variables file was specified, e.g:\n ./biceps_contraction ../settings_biceps_contraction.py ramp.py") - exit(0) + if rank_no == 0: + print("Error: no variables file was specified, e.g:\n ./biceps_contraction ../settings_biceps_contraction.py ramp.py") + exit(0) # -------------- begin user parameters ---------------- -variables.output_timestep_3D = 50 #[ms] output timestep of mechanics -variables.output_timestep_fibers = 50 # [ms] output timestep of fibers +variables.output_timestep_3D = 50 # [ms] output timestep of mechanics +variables.output_timestep_fibers = 50 # [ms] output timestep of fibers # -------------- end user parameters ---------------- # define command line arguments -mbool = lambda x:bool(distutils.util.strtobool(x)) # function to parse bool arguments + + +def mbool(x): return bool(distutils.util.strtobool(x)) # function to parse bool arguments + + parser = argparse.ArgumentParser(description='muscle') -parser.add_argument('--scenario_name', help='The name to identify this run in the log.', default=variables.scenario_name) -parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) -parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) -parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) -parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) -parser.add_argument('--diffusion_solver_type', help='The solver for the diffusion.', default=variables.diffusion_solver_type, choices=["gmres","cg","lu","gamg","richardson","chebyshev","cholesky","jacobi","sor","preonly"]) -parser.add_argument('--diffusion_preconditioner_type', help='The preconditioner for the diffusion.', default=variables.diffusion_preconditioner_type, choices=["jacobi","sor","lu","ilu","gamg","none"]) -parser.add_argument('--potential_flow_solver_type', help='The solver for the potential flow (non-spd matrix).', default=variables.potential_flow_solver_type, choices=["gmres","cg","lu","gamg","richardson","chebyshev","cholesky","jacobi","sor","preonly"]) -parser.add_argument('--potential_flow_preconditioner_type', help='The preconditioner for the potential flow.', default=variables.potential_flow_preconditioner_type, choices=["jacobi","sor","lu","ilu","gamg","none"]) -parser.add_argument('--paraview_output', help='Enable the paraview output writer.', default=variables.paraview_output, action='store_true') -parser.add_argument('--adios_output', help='Enable the MegaMol/ADIOS output writer.', default=variables.adios_output, action='store_true') -parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) -parser.add_argument('--fiber_distribution_file', help='The filename of the file that contains the MU firing times.', default=variables.fiber_distribution_file) -parser.add_argument('--firing_times_file', help='The filename of the file that contains the cellml model.', default=variables.firing_times_file) -parser.add_argument('--end_time', '--tend', '-t', help='The end simulation time.', type=float, default=variables.end_time) -parser.add_argument('--output_timestep', help='The timestep for writing outputs.', type=float, default=variables.output_timestep) -parser.add_argument('--dt_0D', help='The timestep for the 0D model.', type=float, default=variables.dt_0D) -parser.add_argument('--dt_1D', help='The timestep for the 1D model.', type=float, default=variables.dt_1D) -parser.add_argument('--dt_splitting', help='The timestep for the splitting.', type=float, default=variables.dt_splitting) -parser.add_argument('--dt_3D', help='The timestep for the 3D model, i.e. dynamic solid mechanics.', type=float, default=variables.dt_3D) -parser.add_argument('--disable_firing_output', help='Disables the initial list of fiber firings.', default=variables.disable_firing_output, action='store_true') -parser.add_argument('--enable_coupling', help='Enables the precice coupling.', type=mbool, default=variables.enable_coupling) -parser.add_argument('--v', help='Enable full verbosity in c++ code') -parser.add_argument('-v', help='Enable verbosity level in c++ code', action="store_true") -parser.add_argument('-vmodule', help='Enable verbosity level for given file in c++ code') -parser.add_argument('-pause', help='Stop at parallel debugging barrier', action="store_true") +parser.add_argument( + '--scenario_name', + help='The name to identify this run in the log.', + default=variables.scenario_name) +parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) +parser.add_argument( + '--n_subdomains_x', + '-x', + help='Number of subdomains in x direction.', + type=int, + default=variables.n_subdomains_x) +parser.add_argument( + '--n_subdomains_y', + '-y', + help='Number of subdomains in y direction.', + type=int, + default=variables.n_subdomains_y) +parser.add_argument( + '--n_subdomains_z', + '-z', + help='Number of subdomains in z direction.', + type=int, + default=variables.n_subdomains_z) +parser.add_argument( + '--diffusion_solver_type', + help='The solver for the diffusion.', + default=variables.diffusion_solver_type, + choices=[ + "gmres", + "cg", + "lu", + "gamg", + "richardson", + "chebyshev", + "cholesky", + "jacobi", + "sor", + "preonly"]) +parser.add_argument( + '--diffusion_preconditioner_type', + help='The preconditioner for the diffusion.', + default=variables.diffusion_preconditioner_type, + choices=[ + "jacobi", + "sor", + "lu", + "ilu", + "gamg", + "none"]) +parser.add_argument( + '--potential_flow_solver_type', + help='The solver for the potential flow (non-spd matrix).', + default=variables.potential_flow_solver_type, + choices=[ + "gmres", + "cg", + "lu", + "gamg", + "richardson", + "chebyshev", + "cholesky", + "jacobi", + "sor", + "preonly"]) +parser.add_argument( + '--potential_flow_preconditioner_type', + help='The preconditioner for the potential flow.', + default=variables.potential_flow_preconditioner_type, + choices=[ + "jacobi", + "sor", + "lu", + "ilu", + "gamg", + "none"]) +parser.add_argument( + '--paraview_output', + help='Enable the paraview output writer.', + default=variables.paraview_output, + action='store_true') +parser.add_argument( + '--adios_output', + help='Enable the MegaMol/ADIOS output writer.', + default=variables.adios_output, + action='store_true') +parser.add_argument( + '--fiber_file', + help='The filename of the file that contains the fiber data.', + default=variables.fiber_file) +parser.add_argument( + '--fiber_distribution_file', + help='The filename of the file that contains the MU firing times.', + default=variables.fiber_distribution_file) +parser.add_argument( + '--firing_times_file', + help='The filename of the file that contains the cellml model.', + default=variables.firing_times_file) +parser.add_argument( + '--end_time', + '--tend', + '-t', + help='The end simulation time.', + type=float, + default=variables.end_time) +parser.add_argument( + '--output_timestep', + help='The timestep for writing outputs.', + type=float, + default=variables.output_timestep) +parser.add_argument('--dt_0D', help='The timestep for the 0D model.', type=float, default=variables.dt_0D) +parser.add_argument('--dt_1D', help='The timestep for the 1D model.', type=float, default=variables.dt_1D) +parser.add_argument( + '--dt_splitting', + help='The timestep for the splitting.', + type=float, + default=variables.dt_splitting) +parser.add_argument( + '--dt_3D', + help='The timestep for the 3D model, i.e. dynamic solid mechanics.', + type=float, + default=variables.dt_3D) +parser.add_argument( + '--disable_firing_output', + help='Disables the initial list of fiber firings.', + default=variables.disable_firing_output, + action='store_true') +parser.add_argument( + '--enable_coupling', + help='Enables the precice coupling.', + type=mbool, + default=variables.enable_coupling) +parser.add_argument('--v', help='Enable full verbosity in c++ code') +parser.add_argument('-v', help='Enable verbosity level in c++ code', action="store_true") +parser.add_argument('-vmodule', help='Enable verbosity level for given file in c++ code') +parser.add_argument('-pause', help='Stop at parallel debugging barrier', action="store_true") # parse command line arguments and assign values to variables module args, other_args = parser.parse_known_args(args=sys.argv[:-2], namespace=variables) if len(other_args) != 0 and rank_no == 0: print("Warning: These arguments were not parsed by the settings python file\n " + "\n ".join(other_args), file=sys.stderr) - -variables.n_subdomains = variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z + +variables.n_subdomains = variables.n_subdomains_x * variables.n_subdomains_y * variables.n_subdomains_z # automatically initialize partitioning if it has not been set if n_ranks != variables.n_subdomains: - - # create all possible partitionings to the given number of ranks - optimal_value = n_ranks**(1/3) - possible_partitionings = [] - for i in range(1,n_ranks+1): - for j in range(1,n_ranks+1): - if i*j <= n_ranks and n_ranks % (i*j) == 0: - k = (int)(n_ranks / (i*j)) - performance = (k-optimal_value)**2 + (j-optimal_value)**2 + 1.1*(i-optimal_value)**2 - possible_partitionings.append([i,j,k,performance]) - - # if no possible partitioning was found - if len(possible_partitionings) == 0: - if rank_no == 0: - print("\n\n\033[0;31mError! Number of ranks {} does not match given partitioning {} x {} x {} = {} and no automatic partitioning could be done.\n\n\033[0m".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) - quit() - - # select the partitioning with the lowest value of performance which is the best - lowest_performance = possible_partitionings[0][3]+1 - for i in range(len(possible_partitionings)): - if possible_partitionings[i][3] < lowest_performance: - lowest_performance = possible_partitionings[i][3] - variables.n_subdomains_x = possible_partitionings[i][0] - variables.n_subdomains_y = possible_partitionings[i][1] - variables.n_subdomains_z = possible_partitionings[i][2] + + # create all possible partitionings to the given number of ranks + optimal_value = n_ranks**(1 / 3) + possible_partitionings = [] + for i in range(1, n_ranks + 1): + for j in range(1, n_ranks + 1): + if i * j <= n_ranks and n_ranks % (i * j) == 0: + k = (int)(n_ranks / (i * j)) + performance = (k - optimal_value)**2 + (j - optimal_value)**2 + 1.1 * (i - optimal_value)**2 + possible_partitionings.append([i, j, k, performance]) + + # if no possible partitioning was found + if len(possible_partitionings) == 0: + if rank_no == 0: + print("\n\n\033[0;31mError! Number of ranks {} does not match given partitioning {} x {} x {} = {} and no automatic partitioning could be done.\n\n\033[0m".format( + n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x * variables.n_subdomains_y * variables.n_subdomains_z)) + quit() + + # select the partitioning with the lowest value of performance which is the best + lowest_performance = possible_partitionings[0][3] + 1 + for i in range(len(possible_partitionings)): + if possible_partitionings[i][3] < lowest_performance: + lowest_performance = possible_partitionings[i][3] + variables.n_subdomains_x = possible_partitionings[i][0] + variables.n_subdomains_y = possible_partitionings[i][1] + variables.n_subdomains_z = possible_partitionings[i][2] # output information of run if rank_no == 0: - print("scenario_name: {}, n_subdomains: {} {} {}, n_ranks: {}, end_time: {}".format(variables.scenario_name, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, n_ranks, variables.end_time)) - print("dt_0D: {:0.0e}, diffusion_solver_type: {}".format(variables.dt_0D, variables.diffusion_solver_type)) - print("dt_1D: {:0.0e}, potential_flow_solver_type: {}".format(variables.dt_1D, variables.potential_flow_solver_type)) - print("dt_splitting: {:0.0e}, emg_solver_type: {}, emg_initial_guess_nonzero: {}".format(variables.dt_splitting, variables.emg_solver_type, variables.emg_initial_guess_nonzero)) - print("dt_3D: {:0.0e}, paraview_output: {}".format(variables.dt_3D, variables.paraview_output)) - print("output_timestep: {:0.0e} stimulation_frequency: {} 1/ms = {} Hz".format(variables.output_timestep, variables.stimulation_frequency, variables.stimulation_frequency*1e3)) - print("fiber_file: {}".format(variables.fiber_file)) - print("cellml_file: {}".format(variables.cellml_file)) - print("fiber_distribution_file: {}".format(variables.fiber_distribution_file)) - print("firing_times_file: {}".format(variables.firing_times_file)) - print("********************************************************************************") - - print("prefactor: sigma_eff/(Am*Cm) = {} = {} / ({}*{})".format(variables.Conductivity/(variables.Am*variables.Cm), variables.Conductivity, variables.Am, variables.Cm)) - - # start timer to measure duration of parsing of this script - t_start_script = timeit.default_timer() - + print("scenario_name: {}, n_subdomains: {} {} {}, n_ranks: {}, end_time: {}".format(variables.scenario_name, + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, n_ranks, variables.end_time)) + print("dt_0D: {:0.0e}, diffusion_solver_type: {}".format( + variables.dt_0D, variables.diffusion_solver_type)) + print("dt_1D: {:0.0e}, potential_flow_solver_type: {}".format( + variables.dt_1D, variables.potential_flow_solver_type)) + print("dt_splitting: {:0.0e}, emg_solver_type: {}, emg_initial_guess_nonzero: {}".format( + variables.dt_splitting, variables.emg_solver_type, variables.emg_initial_guess_nonzero)) + print("dt_3D: {:0.0e}, paraview_output: {}".format(variables.dt_3D, variables.paraview_output)) + print("output_timestep: {:0.0e} stimulation_frequency: {} 1/ms = {} Hz".format(variables.output_timestep, + variables.stimulation_frequency, variables.stimulation_frequency * 1e3)) + print("fiber_file: {}".format(variables.fiber_file)) + print("cellml_file: {}".format(variables.cellml_file)) + print("fiber_distribution_file: {}".format(variables.fiber_distribution_file)) + print("firing_times_file: {}".format(variables.firing_times_file)) + print("********************************************************************************") + + print("prefactor: sigma_eff/(Am*Cm) = {} = {} / ({}*{})".format(variables.Conductivity / + (variables.Am * variables.Cm), variables.Conductivity, variables.Am, variables.Cm)) + + # start timer to measure duration of parsing of this script + t_start_script = timeit.default_timer() + # initialize all helper variables from helper import * @@ -137,421 +262,613 @@ variables.n_fibers_total = variables.n_fibers_x * variables.n_fibers_y if False: - for subdomain_coordinate_y in range(variables.n_subdomains_y): - for subdomain_coordinate_x in range(variables.n_subdomains_x): - - print("subdomain (x{},y{}) ranks: {} n fibers in subdomain: x{},y{}".format(subdomain_coordinate_x, subdomain_coordinate_y, - list(range(subdomain_coordinate_y*variables.n_subdomains_x + subdomain_coordinate_x, n_ranks, variables.n_subdomains_x*variables.n_subdomains_y)), - n_fibers_in_subdomain_x(subdomain_coordinate_x), n_fibers_in_subdomain_y(subdomain_coordinate_y))) + for subdomain_coordinate_y in range(variables.n_subdomains_y): + for subdomain_coordinate_x in range(variables.n_subdomains_x): + + print("subdomain (x{},y{}) ranks: {} n fibers in subdomain: x{},y{}".format(subdomain_coordinate_x, subdomain_coordinate_y, + list( + range( + subdomain_coordinate_y * + variables.n_subdomains_x + + subdomain_coordinate_x, + n_ranks, + variables.n_subdomains_x * + variables.n_subdomains_y)), + n_fibers_in_subdomain_x(subdomain_coordinate_x), n_fibers_in_subdomain_y(subdomain_coordinate_y))) - for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)): - for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)): - print("({},{}) n instances: {}".format(fiber_in_subdomain_coordinate_x,fiber_in_subdomain_coordinate_y, - n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y))) + for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)): + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)): + print("({},{}) n instances: {}".format(fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y, + n_fibers_in_subdomain_x(subdomain_coordinate_x) * n_fibers_in_subdomain_y(subdomain_coordinate_y))) # define the config dict config = { - "scenarioName": variables.scenario_name, - "logFormat": "csv", - "solverStructureDiagramFile": "out/muscle_solver_structure.txt", # output file of a diagram that shows data connection between solvers - "mappingsBetweenMeshesLogFile": "out/muscle_mappings_between_meshes.txt", # log file of when mappings between meshes occur - "Meshes": variables.meshes, - "MappingsBetweenMeshes": variables.mappings_between_meshes, - "Solvers": { - "diffusionTermSolver": {# solver for the implicit timestepping scheme of the diffusion time step - "maxIterations": 1e4, - "relativeTolerance": 1e-10, - "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual - "solverType": variables.diffusion_solver_type, - "preconditionerType": variables.diffusion_preconditioner_type, - "dumpFilename": "", # "out/dump_" - "dumpFormat": "matlab", - }, - "potentialFlowSolver": {# solver for the initial potential flow, that is needed to estimate fiber directions for the bidomain equation - "relativeTolerance": 1e-10, - "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual - "maxIterations": 1e4, - "solverType": variables.potential_flow_solver_type, - "preconditionerType": variables.potential_flow_preconditioner_type, - "dumpFilename": "", - "dumpFormat": "matlab", + "scenarioName": variables.scenario_name, + "logFormat": "csv", + # output file of a diagram that shows data connection between solvers + "solverStructureDiagramFile": "out/muscle_solver_structure.txt", + "mappingsBetweenMeshesLogFile": "out/muscle_mappings_between_meshes.txt", # log file of when mappings between meshes occur + "Meshes": variables.meshes, + "MappingsBetweenMeshes": variables.mappings_between_meshes, + "Solvers": { + "diffusionTermSolver": { # solver for the implicit timestepping scheme of the diffusion time step + "maxIterations": 1e4, + "relativeTolerance": 1e-10, + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual + "solverType": variables.diffusion_solver_type, + "preconditionerType": variables.diffusion_preconditioner_type, + "dumpFilename": "", # "out/dump_" + "dumpFormat": "matlab", + }, + "potentialFlowSolver": { # solver for the initial potential flow, that is needed to estimate fiber directions for the bidomain equation + "relativeTolerance": 1e-10, + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual + "maxIterations": 1e4, + "solverType": variables.potential_flow_solver_type, + "preconditionerType": variables.potential_flow_preconditioner_type, + "dumpFilename": "", + "dumpFormat": "matlab", + }, + "mechanicsSolver": { # solver for the dynamic mechanics problem + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 140, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-5, # relative tolerance of the nonlinear solver + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesRebuildJacobianFrequency": 3, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again + "dumpFilename": "", # dump system matrix and right hand side after every solve + "dumpFormat": "matlab", # default, ascii, matlab + } }, - "mechanicsSolver": { # solver for the dynamic mechanics problem - "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver - "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver - "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls - "preconditionerType": "lu", # type of the preconditioner - "maxIterations": 1e4, # maximum number of iterations in the linear solver - "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations - "snesMaxIterations": 140, # maximum number of iterations in the nonlinear solver - "snesRelativeTolerance": 1e-5, # relative tolerance of the nonlinear solver - "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver - "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" - "snesRebuildJacobianFrequency": 3, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again - "dumpFilename": "", # dump system matrix and right hand side after every solve - "dumpFormat": "matlab", # default, ascii, matlab - } - }, - "PreciceAdapter": { # precice adapter for muscle - "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console - "timestepWidth": 1, # coupling time step width, must match the value in the precice config - "couplingEnabled": variables.enable_coupling, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file - "preciceParticipantName": "Muscle", # name of the own precice participant, has to match the name given in the precice xml config file - "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication - "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged - "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver - { - "preciceMeshName": "Muscle-Bottom-Mesh", # precice name of the 2D coupling mesh - "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top - }, - { - "preciceMeshName": "Muscle-Top-A-Mesh", # precice name of the 2D coupling mesh - "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top - }, - { - "preciceMeshName": "Muscle-Top-B-Mesh", # precice name of the 2D coupling mesh - "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top - }, - ], - "preciceData": [ - { - "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file - "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file - "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file - "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file - "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "read-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file - "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file - "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Muscle-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings - "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Muscle-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings - "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "write-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Muscle-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings - "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file - }, - ], - - "Coupling": { - "timeStepWidth": variables.dt_3D, # 1e-1 - "logTimeStepWidthAsKey": "dt_3D", - "durationLogKey": "duration_total", - "timeStepOutputInterval": 1, - "endTime": variables.end_time, - "connectedSlotsTerm1To2": {1:2}, # transfer gamma to MuscleContractionSolver, the receiving slots are λ, λdot, γ - "connectedSlotsTerm2To1": None, # transfer nothing back - "Term1": { # monodomain, fibers - "MultipleInstances": { - "logKey": "duration_subdomains_xy", - "ranksAllComputedInstances": list(range(n_ranks)), - "nInstances": variables.n_subdomains_xy, - "instances": - [{ - "ranks": list(range(subdomain_coordinate_y*variables.n_subdomains_x + subdomain_coordinate_x, n_ranks, variables.n_subdomains_x*variables.n_subdomains_y)), - - # this is for the actual model with fibers - "StrangSplitting": { - #"numberTimeSteps": 1, - "timeStepWidth": variables.dt_splitting, # 1e-1 - "logTimeStepWidthAsKey": "dt_splitting", - "durationLogKey": "duration_monodomain", - "timeStepOutputInterval": 100, - "endTime": variables.dt_splitting, - "connectedSlotsTerm1To2": [0,1,2], # transfer slot 0 = state Vm from Term1 (CellML) to Term2 (Diffusion) - "connectedSlotsTerm2To1": [0,None,2], # transfer the same back, this avoids data copy - - "Term1": { # CellML, i.e. reaction term of Monodomain equation - "MultipleInstances": { - "logKey": "duration_subdomains_z", - "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y), - "instances": - [{ - "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction - "Heun" : { - "timeStepWidth": variables.dt_0D, # timestep width of 0D problem - "logTimeStepWidthAsKey": "dt_0D", # key under which the time step width will be written to the log file - "durationLogKey": "duration_0D", # log key of duration for this solver - "timeStepOutputInterval": 1e4, # how often to print the current timestep - "initialValues": [], # no initial values are specified - "dirichletBoundaryConditions": {}, # no Dirichlet boundary conditions are specified - "dirichletOutputFilename": None, # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable - "inputMeshIsGlobal": True, # the boundary conditions and initial values would be given as global numbers - "checkForNanInf": True, # abort execution if the solution contains nan or inf values - "nAdditionalFieldVariables": 0, # number of additional field variables - "additionalSlotNames": "", - - "CellML" : { - "modelFilename": variables.cellml_file, # input C++ source file or cellml XML file - #"statesInitialValues": [], # if given, the initial values for the the states of one instance - "statesInitialValues": variables.states_initial_values, # initial values for new_slow_TK - "initializeStatesToEquilibrium": False, # if the equilibrium values of the states should be computed before the simulation starts - "initializeStatesToEquilibriumTimestepWidth": 1e-4, # if initializeStatesToEquilibrium is enable, the timestep width to use to solve the equilibrium equation - - # optimization parameters - "optimizationType": "vc", # "vc", "simd", "openmp" type of generated optimizated source file - "approximateExponentialFunction": True, # if optimizationType is "vc", whether the exponential function exp(x) should be approximate by (1+x/n)^n with n=1024 - "compilerFlags": "-fPIC -O3 -march=native -Wno-deprecated-declarations -shared ", # compiler flags used to compile the optimized model code - "maximumNumberOfThreads": 0, # if optimizationType is "openmp", the maximum number of threads to use. Default value 0 means no restriction. - - # stimulation callbacks - #"libraryFilename": "cellml_simd_lib.so", # compiled library - #"setSpecificParametersFunction": set_specific_parameters, # callback function that sets parameters like stimulation current - #"setSpecificParametersCallInterval": int(1./variables.stimulation_frequency/variables.dt_0D), # set_specific_parameters should be called every 0.1, 5e-5 * 1e3 = 5e-2 = 0.05 - "setSpecificStatesFunction": set_specific_states, # callback function that sets states like Vm, activation can be implemented by using this method and directly setting Vm values, or by using setParameters/setSpecificParameters - #"setSpecificStatesCallInterval": 2*int(1./variables.stimulation_frequency/variables.dt_0D), # set_specific_states should be called variables.stimulation_frequency times per ms, the factor 2 is needed because every Heun step includes two calls to rhs - "setSpecificStatesCallInterval": 0, # 0 means disabled - "setSpecificStatesCallFrequency": variables.get_specific_states_call_frequency(fiber_no, motor_unit_no), # set_specific_states should be called variables.stimulation_frequency times per ms - "setSpecificStatesFrequencyJitter": variables.get_specific_states_frequency_jitter(fiber_no, motor_unit_no), # random value to add or substract to setSpecificStatesCallFrequency every stimulation, this is to add random jitter to the frequency - "setSpecificStatesRepeatAfterFirstCall": 0.01, # [ms] simulation time span for which the setSpecificStates callback will be called after a call was triggered - "setSpecificStatesCallEnableBegin": variables.get_specific_states_call_enable_begin(fiber_no, motor_unit_no),# [ms] first time when to call setSpecificStates - "additionalArgument": fiber_no, # last argument that will be passed to the callback functions set_specific_states, set_specific_parameters, etc. - - # parameters to the cellml model - "mappings": variables.mappings, # mappings between parameters and algebraics/constants and between outputConnectorSlots and states, algebraics or parameters, they are defined in helper.py - "parametersInitialValues": variables.parameters_initial_values, #[0.0, 1.0], # initial values for the parameters: I_Stim, l_hs - - "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh - "stimulationLogFilename": "out/stimulation.log", # a file that will contain the times of stimulations - }, - "OutputWriter" : [ - {"format": "Paraview", "outputInterval": 1, "filename": "out/" + variables.scenario_name + "/0D_states({},{})".format(fiber_in_subdomain_coordinate_x,fiber_in_subdomain_coordinate_y), "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} - ] if variables.states_output else [] - - }, - } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ - for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ - for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ - for motor_unit_no in [get_motor_unit_no(fiber_no)]], - } - }, - "Term2": { # Diffusion + "PreciceAdapter": { # precice adapter for muscle + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "couplingEnabled": variables.enable_coupling, + "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file + # name of the own precice participant, has to match the name given in the precice xml config file + "preciceParticipantName": "Muscle", + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + # if the output writers should be called only after a time window of + # precice is complete, this means the timestep has converged + "outputOnlyConvergedTimeSteps": True, + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "preciceMeshName": "Muscle-Bottom-Mesh", # precice name of the 2D coupling mesh + "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + }, + { + "preciceMeshName": "Muscle-Top-A-Mesh", # precice name of the 2D coupling mesh + "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + }, + { + "preciceMeshName": "Muscle-Top-B-Mesh", # precice name of the 2D coupling mesh + "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + }, + ], + "preciceData": [ + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "read-displacements-velocities", + # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Muscle-Bottom-Mesh", + # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "displacementsName": "Displacement", + # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "read-displacements-velocities", + # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Muscle-Top-A-Mesh", + # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "displacementsName": "Displacement", + # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "read-displacements-velocities", + # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Muscle-Top-B-Mesh", + # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "displacementsName": "Displacement", + # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "write-traction", + # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Muscle-Bottom-Mesh", + # name of the traction "data", i.e. field variable, as given in the precice xml settings file + "tractionName": "Traction", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "write-traction", + # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Muscle-Top-A-Mesh", + # name of the traction "data", i.e. field variable, as given in the precice xml settings file + "tractionName": "Traction", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "write-traction", + # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Muscle-Top-B-Mesh", + # name of the traction "data", i.e. field variable, as given in the precice xml settings file + "tractionName": "Traction", + }, + ], + + "Coupling": { + "timeStepWidth": variables.dt_3D, # 1e-1 + "logTimeStepWidthAsKey": "dt_3D", + "durationLogKey": "duration_total", + "timeStepOutputInterval": 1, + "endTime": variables.end_time, + # transfer gamma to MuscleContractionSolver, the receiving slots are λ, λdot, γ + "connectedSlotsTerm1To2": {1: 2}, + "connectedSlotsTerm2To1": None, # transfer nothing back + "Term1": { # monodomain, fibers "MultipleInstances": { - "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y), - "instances": - [{ - "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction - "CrankNicolson" : { - "initialValues": [], # no initial values are given - #"numberTimeSteps": 1, - "timeStepWidth": variables.dt_1D, # timestep width for the diffusion problem - "timeStepWidthRelativeTolerance": 1e-10, - "logTimeStepWidthAsKey": "dt_1D", # key under which the time step width will be written to the log file - "durationLogKey": "duration_1D", # log key of duration for this solver - "timeStepOutputInterval": 1e4, # how often to print the current timestep - "dirichletBoundaryConditions": {}, # old Dirichlet BC that are not used in FastMonodomainSolver: {0: -75.0036, -1: -75.0036}, - "dirichletOutputFilename": None, # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable - "inputMeshIsGlobal": True, # initial values would be given as global numbers - "solverName": "diffusionTermSolver", # reference to the linear solver - "nAdditionalFieldVariables": 2, # number of additional field variables that will be written to the output file, here for stress - "additionalSlotNames": ["stress", "activation"], - "checkForNanInf": True, # abort execution if the solution contains nan or inf values - - "FiniteElementMethod" : { - "inputMeshIsGlobal": True, - "meshName": "MeshFiber_{}".format(fiber_no), - "solverName": "diffusionTermSolver", - "prefactor": get_diffusion_prefactor(fiber_no, motor_unit_no), # resolves to Conductivity / (Am * Cm) - "slotName": None, - }, - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": int(1./variables.dt_1D*variables.output_timestep), "filename": "out/fiber_"+str(fiber_no), "binary": True, "fixedFormat": False, "combineFiles": True}, - #{"format": "Paraview", "outputInterval": 1./variables.dt_1D*variables.output_timestep, "filename": "out/fiber_"+str(i)+"_txt", "binary": False, "fixedFormat": False}, - #{"format": "ExFile", "filename": "out/fiber_"+str(i), "outputInterval": 1./variables.dt_1D*variables.output_timestep, "sphereSize": "0.02*0.02*0.02"}, - #{"format": "PythonFile", "filename": "out/fiber_"+str(i), "outputInterval": 1./variables.dt_1D*variables.output_timestep, "binary":True, "onlyNodalValues":True}, - ] - }, - } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ - for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ - for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ - for motor_unit_no in [get_motor_unit_no(fiber_no)]], - "OutputWriter": [ - {"format": "Paraview", "outputInterval": int(1/variables.dt_splitting*variables.output_timestep_fibers), "filename": "out/fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} - ], + "logKey": "duration_subdomains_xy", + "ranksAllComputedInstances": list(range(n_ranks)), + "nInstances": variables.n_subdomains_xy, + "instances": + [{ + "ranks": list(range(subdomain_coordinate_y * variables.n_subdomains_x + subdomain_coordinate_x, n_ranks, variables.n_subdomains_x * variables.n_subdomains_y)), + + # this is for the actual model with fibers + "StrangSplitting": { + # "numberTimeSteps": 1, + "timeStepWidth": variables.dt_splitting, # 1e-1 + "logTimeStepWidthAsKey": "dt_splitting", + "durationLogKey": "duration_monodomain", + "timeStepOutputInterval": 100, + "endTime": variables.dt_splitting, + # transfer slot 0 = state Vm from Term1 (CellML) to Term2 (Diffusion) + "connectedSlotsTerm1To2": [0, 1, 2], + "connectedSlotsTerm2To1": [0, None, 2], # transfer the same back, this avoids data copy + + "Term1": { # CellML, i.e. reaction term of Monodomain equation + "MultipleInstances": { + "logKey": "duration_subdomains_z", + "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x) * n_fibers_in_subdomain_y(subdomain_coordinate_y), + "instances": + [{ + # these rank nos are local nos to the outer instance of MultipleInstances, + # i.e. from 0 to number of ranks in z direction + "ranks": list(range(variables.n_subdomains_z)), + "Heun": { + "timeStepWidth": variables.dt_0D, # timestep width of 0D problem + # key under which the time step width will be written to the log file + "logTimeStepWidthAsKey": "dt_0D", + "durationLogKey": "duration_0D", # log key of duration for this solver + "timeStepOutputInterval": 1e4, # how often to print the current timestep + "initialValues": [], # no initial values are specified + "dirichletBoundaryConditions": {}, # no Dirichlet boundary conditions are specified + # filename for a vtp file that contains the Dirichlet boundary condition + # nodes and their values, set to None to disable + "dirichletOutputFilename": None, + # the boundary conditions and initial values would be given as global + # numbers + "inputMeshIsGlobal": True, + "checkForNanInf": True, # abort execution if the solution contains nan or inf values + "nAdditionalFieldVariables": 0, # number of additional field variables + "additionalSlotNames": "", + + "CellML": { + "modelFilename": variables.cellml_file, # input C++ source file or cellml XML file + # "statesInitialValues": [], # if given, the initial values for the the states of one instance + "statesInitialValues": variables.states_initial_values, # initial values for new_slow_TK + # if the equilibrium values of the states should be computed before the + # simulation starts + "initializeStatesToEquilibrium": False, + # if initializeStatesToEquilibrium is enable, the timestep width to use to + # solve the equilibrium equation + "initializeStatesToEquilibriumTimestepWidth": 1e-4, + + # optimization parameters + # "vc", "simd", "openmp" type of generated optimizated source file + "optimizationType": "vc", + # if optimizationType is "vc", whether the exponential function exp(x) + # should be approximate by (1+x/n)^n with n=1024 + "approximateExponentialFunction": True, + # compiler flags used to compile the optimized model code + "compilerFlags": "-fPIC -O3 -march=native -Wno-deprecated-declarations -shared ", + # if optimizationType is "openmp", the maximum number of threads to use. + # Default value 0 means no restriction. + "maximumNumberOfThreads": 0, + + # stimulation callbacks + # "libraryFilename": "cellml_simd_lib.so", # compiled library + # "setSpecificParametersFunction": set_specific_parameters, # callback function that sets parameters like stimulation current + # "setSpecificParametersCallInterval": int(1./variables.stimulation_frequency/variables.dt_0D), # set_specific_parameters should be called every 0.1, 5e-5 * 1e3 = 5e-2 = 0.05 + # callback function that sets states like Vm, activation can be + # implemented by using this method and directly setting Vm values, or by + # using setParameters/setSpecificParameters + "setSpecificStatesFunction": set_specific_states, + # "setSpecificStatesCallInterval": 2*int(1./variables.stimulation_frequency/variables.dt_0D), # set_specific_states should be called variables.stimulation_frequency times per ms, the factor 2 is needed because every Heun step includes two calls to rhs + "setSpecificStatesCallInterval": 0, # 0 means disabled + # set_specific_states should be called variables.stimulation_frequency + # times per ms + "setSpecificStatesCallFrequency": variables.get_specific_states_call_frequency(fiber_no, motor_unit_no), + # random value to add or substract to setSpecificStatesCallFrequency every + # stimulation, this is to add random jitter to the frequency + "setSpecificStatesFrequencyJitter": variables.get_specific_states_frequency_jitter(fiber_no, motor_unit_no), + # [ms] simulation time span for which the setSpecificStates callback will be called after a call was triggered + "setSpecificStatesRepeatAfterFirstCall": 0.01, + # [ms] first time when to call setSpecificStates + "setSpecificStatesCallEnableBegin": variables.get_specific_states_call_enable_begin(fiber_no, motor_unit_no), + # last argument that will be passed to the callback functions + # set_specific_states, set_specific_parameters, etc. + "additionalArgument": fiber_no, + + # parameters to the cellml model + # mappings between parameters and algebraics/constants and between + # outputConnectorSlots and states, algebraics or parameters, they are + # defined in helper.py + "mappings": variables.mappings, + # [0.0, 1.0], # initial values for the parameters: I_Stim, l_hs + "parametersInitialValues": variables.parameters_initial_values, + + # reference to the fiber mesh + "meshName": "MeshFiber_{}".format(fiber_no), + # a file that will contain the times of stimulations + "stimulationLogFilename": "out/stimulation.log", + }, + "OutputWriter": [ + {"format": "Paraview", + "outputInterval": 1, + "filename": "out/" + variables.scenario_name + "/0D_states({},{})".format(fiber_in_subdomain_coordinate_x, + fiber_in_subdomain_coordinate_y), + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"} + ] if variables.states_output else [] + + }, + } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ + for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ + for motor_unit_no in [get_motor_unit_no(fiber_no)]], + } + }, + "Term2": { # Diffusion + "MultipleInstances": { + "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x) * n_fibers_in_subdomain_y(subdomain_coordinate_y), + "instances": + [{ + # these rank nos are local nos to the outer instance of MultipleInstances, + # i.e. from 0 to number of ranks in z direction + "ranks": list(range(variables.n_subdomains_z)), + "CrankNicolson": { + "initialValues": [], # no initial values are given + # "numberTimeSteps": 1, + "timeStepWidth": variables.dt_1D, # timestep width for the diffusion problem + "timeStepWidthRelativeTolerance": 1e-10, + # key under which the time step width will be written to the log file + "logTimeStepWidthAsKey": "dt_1D", + "durationLogKey": "duration_1D", # log key of duration for this solver + "timeStepOutputInterval": 1e4, # how often to print the current timestep + # old Dirichlet BC that are not used in FastMonodomainSolver: {0: + # -75.0036, -1: -75.0036}, + "dirichletBoundaryConditions": {}, + # filename for a vtp file that contains the Dirichlet boundary condition + # nodes and their values, set to None to disable + "dirichletOutputFilename": None, + "inputMeshIsGlobal": True, # initial values would be given as global numbers + "solverName": "diffusionTermSolver", # reference to the linear solver + # number of additional field variables that will be written to the output + # file, here for stress + "nAdditionalFieldVariables": 2, + "additionalSlotNames": ["stress", "activation"], + "checkForNanInf": True, # abort execution if the solution contains nan or inf values + + "FiniteElementMethod": { + "inputMeshIsGlobal": True, + "meshName": "MeshFiber_{}".format(fiber_no), + "solverName": "diffusionTermSolver", + # resolves to Conductivity / (Am * Cm) + "prefactor": get_diffusion_prefactor(fiber_no, motor_unit_no), + "slotName": None, + }, + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": int(1./variables.dt_1D*variables.output_timestep), "filename": "out/fiber_"+str(fiber_no), "binary": True, "fixedFormat": False, "combineFiles": True}, + # {"format": "Paraview", "outputInterval": 1./variables.dt_1D*variables.output_timestep, "filename": "out/fiber_"+str(i)+"_txt", "binary": False, "fixedFormat": False}, + # {"format": "ExFile", "filename": "out/fiber_"+str(i), "outputInterval": 1./variables.dt_1D*variables.output_timestep, "sphereSize": "0.02*0.02*0.02"}, + # {"format": "PythonFile", "filename": "out/fiber_"+str(i), "outputInterval": 1./variables.dt_1D*variables.output_timestep, "binary":True, "onlyNodalValues":True}, + ] + }, + } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ + for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ + for motor_unit_no in [get_motor_unit_no(fiber_no)]], + "OutputWriter": [ + {"format": "Paraview", + "outputInterval": int(1 / variables.dt_splitting * variables.output_timestep_fibers), + "filename": "out/fibers", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"} + ], + }, + }, + }, + + # this is for biceps_contraction_no_cell, i.e. PrescribedValues instead of fibers + "GodunovSplitting": { # this splitting scheme is only needed to replicate the solver structure as with the fibers + "timeStepWidth": variables.dt_3D, + "logTimeStepWidthAsKey": "dt_splitting", + "durationLogKey": "duration_prescribed_values", + "timeStepOutputInterval": 100, + "endTime": variables.dt_3D, + "connectedSlotsTerm1To2": [], + "connectedSlotsTerm2To1": [], # transfer the same back, this avoids data copy + + "Term1": { + "MultipleInstances": { + "logKey": "duration_subdomains_z", + "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x) * n_fibers_in_subdomain_y(subdomain_coordinate_y), + "instances": + [{ + # these rank nos are local nos to the outer instance of MultipleInstances, + # i.e. from 0 to number of ranks in z direction + "ranks": list(range(variables.n_subdomains_z)), + "PrescribedValues": { + # reference to the fiber mesh + "meshName": "MeshFiber_{}".format(fiber_no), + "numberTimeSteps": 1, # number of timesteps to call the callback functions subsequently, this is usually 1 for prescribed values, because it is enough to set the reaction term only once per time step + "timeStepOutputInterval": 20, # if the time step should be written to console, a value > 10 produces no output + "slotNames": [], # names of the data connector slots + + # a list of field variables that will get values assigned in every + # timestep, by the provided callback function + "fieldVariables1": [ + {"name": "Vm", "callback": None}, + {"name": "stress", "callback": set_stress_values}, + ], + "fieldVariables2": [], + # a custom argument to the fieldVariables callback functions, this will be + # passed on as the last argument + "additionalArgument": fiber_no, + + "OutputWriter": [ + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_fibers), + "filename": "out/prescribed_fibers", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"} + ] + }, + } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ + for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ + for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ + for motor_unit_no in [get_motor_unit_no(fiber_no)]], + + # "OutputWriter" : variables.output_writer_fibers, + "OutputWriter": [ + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_fibers), + "filename": "out/fibers", + "binary": True, + "fixedFormat": False, + "combineFiles": True, + "fileNumbering": "incremental"} + ] + } + }, + + # term2 is unused, it is needed to be similar to the actual fiber solver structure + "Term2": {} + } + + } if (subdomain_coordinate_x, subdomain_coordinate_y) == (variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y) else None + for subdomain_coordinate_y in range(variables.n_subdomains_y) + for subdomain_coordinate_x in range(variables.n_subdomains_x)] }, - }, + # for FastMonodomainSolver, e.g. MU_fibre_distribution_3780.txt + "fiberDistributionFile": variables.fiber_distribution_file, + "firingTimesFile": variables.firing_times_file, # for FastMonodomainSolver, e.g. MU_firing_times_real.txt + # only compute fibers after they have been stimulated for the first time + "onlyComputeIfHasBeenStimulated": True, + # optimization where states that are close to their equilibrium will not be computed again + "disableComputationWhenStatesAreCloseToEquilibrium": True, + "valueForStimulatedPoint": variables.vm_value_stimulated, # to which value of Vm the stimulated node should be set + # range where the neuromuscular junction is located around the center, + # relative to fiber length. The actual position is draws randomly from the + # interval [0.5-s/2, 0.5+s/2) with s being this option. 0 means sharply at + # the center, 0.1 means located approximately at the center, but it can + # vary 10% in total between all fibers. + "neuromuscularJunctionRelativeSize": 0.1, }, - - # this is for biceps_contraction_no_cell, i.e. PrescribedValues instead of fibers - "GodunovSplitting": { # this splitting scheme is only needed to replicate the solver structure as with the fibers - "timeStepWidth": variables.dt_3D, - "logTimeStepWidthAsKey": "dt_splitting", - "durationLogKey": "duration_prescribed_values", - "timeStepOutputInterval": 100, - "endTime": variables.dt_3D, - "connectedSlotsTerm1To2": [], - "connectedSlotsTerm2To1": [], # transfer the same back, this avoids data copy - - "Term1": { - "MultipleInstances": { - "logKey": "duration_subdomains_z", - "nInstances": n_fibers_in_subdomain_x(subdomain_coordinate_x)*n_fibers_in_subdomain_y(subdomain_coordinate_y), - "instances": - [{ - "ranks": list(range(variables.n_subdomains_z)), # these rank nos are local nos to the outer instance of MultipleInstances, i.e. from 0 to number of ranks in z direction - "PrescribedValues": { - "meshName": "MeshFiber_{}".format(fiber_no), # reference to the fiber mesh - "numberTimeSteps": 1, # number of timesteps to call the callback functions subsequently, this is usually 1 for prescribed values, because it is enough to set the reaction term only once per time step - "timeStepOutputInterval": 20, # if the time step should be written to console, a value > 10 produces no output - "slotNames": [], # names of the data connector slots - - # a list of field variables that will get values assigned in every timestep, by the provided callback function - "fieldVariables1": [ - {"name": "Vm", "callback": None}, - {"name": "stress", "callback": set_stress_values}, - ], - "fieldVariables2": [], - "additionalArgument": fiber_no, # a custom argument to the fieldVariables callback functions, this will be passed on as the last argument - - "OutputWriter" : [ - {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_fibers), "filename": "out/prescribed_fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} - ] - }, - } for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)) \ - for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)) \ - for fiber_no in [get_fiber_no(subdomain_coordinate_x, subdomain_coordinate_y, fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y)] \ - for motor_unit_no in [get_motor_unit_no(fiber_no)]], - - #"OutputWriter" : variables.output_writer_fibers, - "OutputWriter": [ - {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_fibers), "filename": "out/fibers", "binary": True, "fixedFormat": False, "combineFiles": True, "fileNumbering": "incremental"} - ] + "Term2": { # solid mechanics + "MuscleContractionSolver": { + "numberTimeSteps": 1, # only use 1 timestep per interval + "timeStepOutputInterval": 100, # do not output time steps + "Pmax": variables.pmax, # maximum PK2 active stress + # if the factor f_l(λ_f) modeling the force-length relation (as in + # Heidlauf2013) should be multiplied. Set to false if this relation is + # already considered in the CellML model. + "enableForceLengthRelation": variables.enable_force_length_relation, + # scaling factor for the output of the lambda dot slot, i.e. the + # contraction velocity. Use this to scale the unit-less quantity to, e.g., + # micrometers per millisecond for the subcellular model. + "lambdaDotScalingFactor": variables.lambda_dot_scaling_factor, + "slotNames": ["lambda", "ldot", "gamma", "T"], # names of the data connector slots + "OutputWriter": [ + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D), + "filename": "out/muscle_3D", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + ], + "mapGeometryToMeshes": [], # the mesh names of the meshes that will get the geometry transferred + "dynamic": True, # if the dynamic solid mechanics solver should be used, else it computes the quasi-static problem + + # the actual solid mechanics solver, this is either + # "DynamicHyperelasticitySolver" or "HyperelasticitySolver", depending on + # the value of "dynamic" + "DynamicHyperelasticitySolver": { + "timeStepWidth": variables.dt_3D, # time step width + "durationLogKey": "nonlinear", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + # scaling factor for displacements, only set to sth. other than 1 only to + # increase visual appearance for very small displacements + "displacementsScalingFactor": 1.0, + # log file where residual norm values of the nonlinear solver will be written + "residualNormLogFilename": "muscle_log_residual_norm.txt", + # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useAnalyticJacobian": True, + # whether to use the numerically computed jacobian matrix in the nonlinear + # solver (slow), only works with non-nested matrices, if both numeric and + # analytic are enable, it uses the analytic for the preconditioner and the + # numeric as normal jacobian + "useNumericJacobian": False, + + # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + "dumpDenseMatlabVariables": False, + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables + # all all three true, the analytic and numeric jacobian matrices will get + # compared to see if there are programming errors for the analytic + # jacobian + + # mesh + "inputMeshIsGlobal": True, # the mesh is given locally + "meshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config + # fiber meshes that will be used to determine the fiber direction, for + # multidomain there are no fibers so this would be empty list + "fiberMeshNames": variables.fiber_mesh_names, + # "fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + + # solving + # name of the nonlinear solver configuration, it is defined under + # "Solvers" at the beginning of this config + "solverName": "mechanicsSolver", + # "loadFactors": [0.25, 0.66, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + # a threshold for the load factor, when to abort the solve of the current + # time step. The load factors are adjusted automatically if the nonlinear + # solver diverged. If the progression between two subsequent load factors + # gets smaller than this value, the solution is aborted. + "loadFactorGiveUpThreshold": 4e-2, + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be repeated + + # boundary and initial conditions + # the initial Dirichlet boundary conditions that define values for + # displacements u and velocity v + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, + # Neumann boundary conditions that define traction forces on surfaces of elements + "neumannBoundaryConditions": variables.elasticity_neumann_bc, + # if the given Neumann boundary condition values under + # "neumannBoundaryConditions" are total forces instead of surface loads + # and therefore should be scaled by the surface area of all elements where + # Neumann BC are applied + "divideNeumannBoundaryConditionValuesByTotalArea": True, + # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunction": None, + # every which step the update function should be called, 1 means every time step + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, + + # the initial values for the displacements, vector of values for every + # node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesDisplacements": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # the initial values for the velocities, vector of values for every node + # [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # if the initial values for the dynamic nonlinear problem should be + # computed by extrapolating the previous displacements and velocities + "extrapolateInitialGuess": True, + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + # filename for a vtp file that contains the Dirichlet boundary condition + # nodes and their values, set to None to disable + "dirichletOutputFilename": "out/muscle_dirichlet_boundary_conditions", + # filename of a log file that will contain the total (bearing) forces and + # moments at the top and bottom of the volume + "totalForceLogFilename": "out/muscle_force.csv", + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + # global element nos of the bottom elements used to compute the total + # forces in the log file totalForceLogFilename + "totalForceBottomElementNosGlobal": [j * nx + i for j in range(ny) for i in range(nx)], + # global element nos of the top elements used to compute the total forces + # in the log file totalForceTopElementsGlobal + "totalForceTopElementNosGlobal": [(nz - 1) * ny * nx + j * nx + i for j in range(ny) for i in range(nx)], + + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic + # elements function space. Writes displacements, velocities and PK2 + # stresses. + "OutputWriter": [ + + # Paraview files + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/u", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + # {"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, + ], + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter": [ # output files for displacements function space (quadratic elements) + # {"format": "Paraview", "outputInterval": int(output_interval/dt), "filename": "out/dynamic", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_3D * variables.output_timestep_3D), + "filename": "out/muscle_virtual_work", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, + # the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + } } - }, - - # term2 is unused, it is needed to be similar to the actual fiber solver structure - "Term2": {} } - - } if (subdomain_coordinate_x,subdomain_coordinate_y) == (variables.own_subdomain_coordinate_x,variables.own_subdomain_coordinate_y) else None - for subdomain_coordinate_y in range(variables.n_subdomains_y) - for subdomain_coordinate_x in range(variables.n_subdomains_x)] - }, - "fiberDistributionFile": variables.fiber_distribution_file, # for FastMonodomainSolver, e.g. MU_fibre_distribution_3780.txt - "firingTimesFile": variables.firing_times_file, # for FastMonodomainSolver, e.g. MU_firing_times_real.txt - "onlyComputeIfHasBeenStimulated": True, # only compute fibers after they have been stimulated for the first time - "disableComputationWhenStatesAreCloseToEquilibrium": True, # optimization where states that are close to their equilibrium will not be computed again - "valueForStimulatedPoint": variables.vm_value_stimulated, # to which value of Vm the stimulated node should be set - "neuromuscularJunctionRelativeSize": 0.1, # range where the neuromuscular junction is located around the center, relative to fiber length. The actual position is draws randomly from the interval [0.5-s/2, 0.5+s/2) with s being this option. 0 means sharply at the center, 0.1 means located approximately at the center, but it can vary 10% in total between all fibers. - }, - "Term2": { # solid mechanics - "MuscleContractionSolver": { - "numberTimeSteps": 1, # only use 1 timestep per interval - "timeStepOutputInterval": 100, # do not output time steps - "Pmax": variables.pmax, # maximum PK2 active stress - "enableForceLengthRelation": variables.enable_force_length_relation, # if the factor f_l(λ_f) modeling the force-length relation (as in Heidlauf2013) should be multiplied. Set to false if this relation is already considered in the CellML model. - "lambdaDotScalingFactor": variables.lambda_dot_scaling_factor, # scaling factor for the output of the lambda dot slot, i.e. the contraction velocity. Use this to scale the unit-less quantity to, e.g., micrometers per millisecond for the subcellular model. - "slotNames": ["lambda", "ldot", "gamma", "T"], # names of the data connector slots - "OutputWriter" : [ - {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/muscle_3D", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ], - "mapGeometryToMeshes": [], # the mesh names of the meshes that will get the geometry transferred - "dynamic": True, # if the dynamic solid mechanics solver should be used, else it computes the quasi-static problem - - # the actual solid mechanics solver, this is either "DynamicHyperelasticitySolver" or "HyperelasticitySolver", depending on the value of "dynamic" - "DynamicHyperelasticitySolver": { - "timeStepWidth": variables.dt_3D, # time step width - "durationLogKey": "nonlinear", # key to find duration of this solver in the log file - "timeStepOutputInterval": 1, # how often the current time step should be printed to console - - "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material - "density": variables.rho, # density of the material - "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements - "residualNormLogFilename": "muscle_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written - "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) - "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian - - "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) - # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian - - # mesh - "inputMeshIsGlobal": True, # the mesh is given locally - "meshName": "3Dmesh_quadratic", # name of the 3D mesh, it is defined under "Meshes" at the beginning of this config - "fiberMeshNames": variables.fiber_mesh_names, # fiber meshes that will be used to determine the fiber direction, for multidomain there are no fibers so this would be empty list - #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system - - # solving - "solverName": "mechanicsSolver", # name of the nonlinear solver configuration, it is defined under "Solvers" at the beginning of this config - #"loadFactors": [0.25, 0.66, 1.0], # load factors for every timestep - "loadFactors": [], # no load factors, solve problem directly - "loadFactorGiveUpThreshold": 4e-2, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the progression between two subsequent load factors gets smaller than this value, the solution is aborted. - "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be repeated - - # boundary and initial conditions - "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v - "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements - "divideNeumannBoundaryConditionValuesByTotalArea": True, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied - "updateDirichletBoundaryConditionsFunction": None, # function that updates the dirichlet BCs while the simulation is running - "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step - - "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "extrapolateInitialGuess": True, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities - "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity - - "dirichletOutputFilename": "out/muscle_dirichlet_boundary_conditions", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable - "totalForceLogFilename": "out/muscle_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume - "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file - "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename - "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal - - - # define which file formats should be written - # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. - "OutputWriter" : [ - - # Paraview files - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/u", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - - # Python callback function "postprocess" - #{"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, - ], - # 2. additional output writer that writes also the hydrostatic pressure - "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, - # 3. additional output writer that writes virtual work terms - "dynamic": { # output of the dynamic solver, has additional virtual work values - "OutputWriter" : [ # output files for displacements function space (quadratic elements) - #{"format": "Paraview", "outputInterval": int(output_interval/dt), "filename": "out/dynamic", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - {"format": "Paraview", "outputInterval": int(1./variables.dt_3D*variables.output_timestep_3D), "filename": "out/muscle_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ], - }, - # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written - "LoadIncrements": { - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, - } } - } } - } } # stop timer and calculate how long parsing lasted if rank_no == 0: - t_stop_script = timeit.default_timer() - print("Python config parsed in {:.1f}s.".format(t_stop_script - t_start_script)) + t_stop_script = timeit.default_timer() + print("Python config parsed in {:.1f}s.".format(t_stop_script - t_start_script)) diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py index 93a24736c..c27771d6d 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/settings-tendon-bottom.py @@ -1,6 +1,8 @@ # Transversely-isotropic Mooney Rivlin on a tendon geometry -# Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. -import sys, os +# Note, this is not possible to be run in parallel because the fibers +# cannot be initialized without MultipleInstances class. +import sys +import os import numpy as np import argparse import sys @@ -11,12 +13,12 @@ title = "tendon-bottom" print('\33]0;{}\a'.format(title), end='', flush=True) -#add variables subfolder to python path where the variables script is located +# add variables subfolder to python path where the variables script is located script_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, script_path) -sys.path.insert(0, os.path.join(script_path,'variables')) +sys.path.insert(0, os.path.join(script_path, 'variables')) -import variables +import variables from create_partitioned_meshes_for_settings import * # update material parameters @@ -36,16 +38,22 @@ # material parameters for Saint Venant-Kirchhoff material # https://www.researchgate.net/publication/230248067_Bulk_Modulus - youngs_modulus = 7e4 # [N/cm^2 = 10kPa] + youngs_modulus = 7e4 # [N/cm^2 = 10kPa] shear_modulus = 3e4 - lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda + lambd = shear_modulus * (youngs_modulus - 2 * shear_modulus) / \ + (3 * shear_modulus - youngs_modulus) # Lamé parameter lambda mu = shear_modulus # Lamé parameter mu or G (shear modulus) variables.material_parameters = [lambd, mu] -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False # parse arguments rank_no = (int)(sys.argv[-2]) @@ -53,11 +61,29 @@ # define command line arguments parser = argparse.ArgumentParser(description='tendon') -parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) -parser.add_argument('--n_subdomains_x', '-x', help='Number of subdomains in x direction.', type=int, default=variables.n_subdomains_x) -parser.add_argument('--n_subdomains_y', '-y', help='Number of subdomains in y direction.', type=int, default=variables.n_subdomains_y) -parser.add_argument('--n_subdomains_z', '-z', help='Number of subdomains in z direction.', type=int, default=variables.n_subdomains_z) -parser.add_argument('--fiber_file', help='The filename of the file that contains the fiber data.', default=variables.fiber_file) +parser.add_argument('--n_subdomains', nargs=3, help='Number of subdomains in x,y,z direction.', type=int) +parser.add_argument( + '--n_subdomains_x', + '-x', + help='Number of subdomains in x direction.', + type=int, + default=variables.n_subdomains_x) +parser.add_argument( + '--n_subdomains_y', + '-y', + help='Number of subdomains in y direction.', + type=int, + default=variables.n_subdomains_y) +parser.add_argument( + '--n_subdomains_z', + '-z', + help='Number of subdomains in z direction.', + type=int, + default=variables.n_subdomains_z) +parser.add_argument( + '--fiber_file', + help='The filename of the file that contains the fiber data.', + default=variables.fiber_file) parser.add_argument('-vmodule', help='ignore') # parse command line arguments and assign values to variables module @@ -69,16 +95,24 @@ # ------------ # this has to match the total number of processes if variables.n_subdomains is not None: - variables.n_subdomains_x = variables.n_subdomains[0] - variables.n_subdomains_y = variables.n_subdomains[1] - variables.n_subdomains_z = variables.n_subdomains[2] + variables.n_subdomains_x = variables.n_subdomains[0] + variables.n_subdomains_y = variables.n_subdomains[1] + variables.n_subdomains_z = variables.n_subdomains[2] # compute partitioning if rank_no == 0: - if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: - print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) - sys.exit(-1) - + if n_ranks != variables.n_subdomains_x * variables.n_subdomains_y * variables.n_subdomains_z: + print( + "\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format( + n_ranks, + variables.n_subdomains_x, + variables.n_subdomains_y, + variables.n_subdomains_z, + variables.n_subdomains_x * + variables.n_subdomains_y * + variables.n_subdomains_z)) + sys.exit(-1) + # stride for sampling the 3D elements from the fiber data # here any number is possible sampling_stride_x = 1 @@ -87,19 +121,29 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( - variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.fiber_file, load_fiber_data, sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) -[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result +[variables.meshes, + variables.own_subdomain_coordinate_x, + variables.own_subdomain_coordinate_y, + variables.own_subdomain_coordinate_z, + variables.n_fibers_x, + variables.n_fibers_y, + variables.n_points_whole_fiber] = result -n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) -n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) -n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) -n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z -nx = n_points_3D_mesh_linear_global_x-1 -ny = n_points_3D_mesh_linear_global_y-1 -nz = n_points_3D_mesh_linear_global_z-1 +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) + for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) + for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) + for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x * \ + n_points_3D_mesh_linear_global_y * n_points_3D_mesh_linear_global_z +nx = n_points_3D_mesh_linear_global_x - 1 +ny = n_points_3D_mesh_linear_global_y - 1 +nz = n_points_3D_mesh_linear_global_z - 1 node_positions = variables.meshes["3Dmesh_quadratic"]["nodePositions"] @@ -111,170 +155,252 @@ # set Dirichlet BC, fix x and y coordinates of the end of tendon that is attached to the bone variables.elasticity_dirichlet_bc = {} k = 0 - + # fix z value on the whole x-y-plane for j in range(my): - for i in range(mx): - variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,None,None,None,None] - + for i in range(mx): + variables.elasticity_dirichlet_bc[k * mx * my + j * mx + i] = [0.0, 0.0, None, None, None, None] + # set Neumann BC, set traction at the end of the tendon that is attached to the bone k = 0 # start with 0 BC -variables.elasticity_neumann_bc = [{"element": k*nx*ny + j*nx + i, "constantVector": [0,0,0], "face": "2-"} for j in range(ny) for i in range(nx)] +variables.elasticity_neumann_bc = [{"element": k * nx * ny + j * nx + i, + "constantVector": [0, 0, 0], "face": "2-"} for j in range(ny) for i in range(nx)] + def update_neumann_bc(t): - # set new Neumann boundary conditions - k = 0 - factor = min(1, t/100) # for t ∈ [0,100] from 0 to 1 - elasticity_neumann_bc = [{ - "element": k*nx*ny + j*nx + i, - "constantVector": [0,0,-variables.force*factor], # force pointing to bottom - "face": "2-", - "isInReferenceConfiguration": True - } for j in range(ny) for i in range(nx)] - - config = { - "inputMeshIsGlobal": True, - "divideNeumannBoundaryConditionValuesByTotalArea": True, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied - "neumannBoundaryConditions": elasticity_neumann_bc, - } - print("prescribed pulling force to bottom: {}".format(variables.force*factor)) - return config + # set new Neumann boundary conditions + k = 0 + factor = min(1, t / 100) # for t ∈ [0,100] from 0 to 1 + elasticity_neumann_bc = [{ + "element": k * nx * ny + j * nx + i, + "constantVector": [0, 0, -variables.force * factor], # force pointing to bottom + "face": "2-", + "isInReferenceConfiguration": True + } for j in range(ny) for i in range(nx)] + + config = { + "inputMeshIsGlobal": True, + # if the given Neumann boundary condition values under + # "neumannBoundaryConditions" are total forces instead of surface loads + # and therefore should be scaled by the surface area of all elements where + # Neumann BC are applied + "divideNeumannBoundaryConditionValuesByTotalArea": True, + "neumannBoundaryConditions": elasticity_neumann_bc, + } + print("prescribed pulling force to bottom: {}".format(variables.force * factor)) + return config config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" - "timeStepWidth": variables.dt_elasticity, # time step width - "endTime": variables.end_time, # end time of the simulation time span - "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file - "timeStepOutputInterval": 1, # how often the current time step should be printed to console - - "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material - "density": variables.rho, # density of the material - "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements - "residualNormLogFilename": "out/tendon_bottom_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written - "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) - "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian - - "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) - # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian - - # mesh - "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions - "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings - - "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction - #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system - "fiberDirectionInElement": [0,0,1], # if fiberMeshNames and fiberDirections are empty, directly set the constant fiber direction, in element coordinate system - - # nonlinear solver - "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver - "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver - "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls - "preconditionerType": "lu", # type of the preconditioner - "maxIterations": 1e4, # maximum number of iterations in the linear solver - "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations - "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver - "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver - "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" - "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver - "snesRebuildJacobianFrequency": 5, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again - - #"dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve - "dumpFilename": "", # dump disabled - "dumpFormat": "matlab", # default, ascii, matlab - - #"loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep - #"loadFactors": [0.5, 1.0], # load factors for every timestep - "loadFactors": [], # no load factors, solve problem directly - "loadFactorGiveUpThreshold": 1e-3, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the load factors get too small, it aborts the solve. - "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called - - # boundary and initial conditions - "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v - "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements - "divideNeumannBoundaryConditionValuesByTotalArea": True, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied - "updateDirichletBoundaryConditionsFunction": None, #update_dirichlet_bc, # function that updates the dirichlet BCs while the simulation is running - "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # stide every which step the update function should be called, 1 means every time step - "updateNeumannBoundaryConditionsFunction": update_neumann_bc, # a callback function to periodically update the Neumann boundary conditions - "updateNeumannBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step - - "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "extrapolateInitialGuess": True, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities - "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity - - "dirichletOutputFilename": "out/tendon_bottom_dirichlet_boundary_conditions", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable - "totalForceLogFilename": "out/tendon_bottom_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume - "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file - "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename - "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal - "totalForceFunction": None, #callback_total_force, # callback function that gets the total force at bottom and top of the domain - "totalForceFunctionCallInterval": 1, # how often the "totalForceFunction" is called - - # define which file formats should be written - # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. - "OutputWriter" : [ - - # Paraview files - {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_bottom", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ], - # 2. additional output writer that writes also the hydrostatic pressure - "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, - # 3. additional output writer that writes virtual work terms - "dynamic": { # output of the dynamic solver, has additional virtual work values - "OutputWriter" : [ # output files for displacements function space (quadratic elements) - {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_bottom_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + "timeStepWidth": variables.dt_elasticity, # time step width + "endTime": variables.end_time, # end time of the simulation time span + "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + # scaling factor for displacements, only set to sth. other than 1 only to + # increase visual appearance for very small displacements + "displacementsScalingFactor": 1.0, + # log file where residual norm values of the nonlinear solver will be written + "residualNormLogFilename": "out/tendon_bottom_log_residual_norm.txt", + # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useAnalyticJacobian": True, + # whether to use the numerically computed jacobian matrix in the nonlinear + # solver (slow), only works with non-nested matrices, if both numeric and + # analytic are enable, it uses the analytic for the preconditioner and the + # numeric as normal jacobian + "useNumericJacobian": False, + + # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + "dumpDenseMatlabVariables": False, + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables + # all all three true, the analytic and numeric jacobian matrices will get + # compared to see if there are programming errors for the analytic + # jacobian + + # mesh + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings + "inputMeshIsGlobal": True, + + "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction + # "fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + # if fiberMeshNames and fiberDirections are empty, directly set the + # constant fiber direction, in element coordinate system + "fiberDirectionInElement": [0, 0, 1], + + # nonlinear solver + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver + # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesLineSearchType": "l2", + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, + # 1 means rebuild every time the Jacobian is computed within a single + # nonlinear solve, 2 means every second time the Jacobian is built etc. -2 + # means rebuild at next chance but then never again + "snesRebuildJacobianFrequency": 5, + + # "dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve + "dumpFilename": "", # dump disabled + "dumpFormat": "matlab", # default, ascii, matlab + + # "loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep + # "loadFactors": [0.5, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + # a threshold for the load factor, when to abort the solve of the current + # time step. The load factors are adjusted automatically if the nonlinear + # solver diverged. If the load factors get too small, it aborts the solve. + "loadFactorGiveUpThreshold": 1e-3, + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called + + # boundary and initial conditions + # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, + # Neumann boundary conditions that define traction forces on surfaces of elements + "neumannBoundaryConditions": variables.elasticity_neumann_bc, + # if the given Neumann boundary condition values under + # "neumannBoundaryConditions" are total forces instead of surface loads + # and therefore should be scaled by the surface area of all elements where + # Neumann BC are applied + "divideNeumannBoundaryConditionValuesByTotalArea": True, + # update_dirichlet_bc, # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunction": None, + # stide every which step the update function should be called, 1 means every time step + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, + # a callback function to periodically update the Neumann boundary conditions + "updateNeumannBoundaryConditionsFunction": update_neumann_bc, + # every which step the update function should be called, 1 means every time step + "updateNeumannBoundaryConditionsFunctionCallInterval": 1, + + # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesDisplacements": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # if the initial values for the dynamic nonlinear problem should be + # computed by extrapolating the previous displacements and velocities + "extrapolateInitialGuess": True, + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + # filename for a vtp file that contains the Dirichlet boundary condition + # nodes and their values, set to None to disable + "dirichletOutputFilename": "out/tendon_bottom_dirichlet_boundary_conditions", + # filename of a log file that will contain the total (bearing) forces and + # moments at the top and bottom of the volume + "totalForceLogFilename": "out/tendon_bottom_force.csv", + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceBottomElementNosGlobal": [j * nx + i for j in range(ny) for i in range(nx)], + # global element nos of the top elements used to compute the total forces + # in the log file totalForceTopElementsGlobal + "totalForceTopElementNosGlobal": [(nz - 1) * ny * nx + j * nx + i for j in range(ny) for i in range(nx)], + # callback_total_force, # callback function that gets the total force at bottom and top of the domain + "totalForceFunction": None, + "totalForceFunctionCallInterval": 1, # how often the "totalForceFunction" is called + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic + # elements function space. Writes displacements, velocities and PK2 + # stresses. + "OutputWriter": [ + + # Paraview files + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_elasticity * variables.output_timestep_3D), + "filename": "out/tendon_bottom", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, ], - }, - # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written - "LoadIncrements": { - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter": [ # output files for displacements function space (quadratic elements) + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_elasticity * variables.output_timestep_3D), + "filename": "out/tendon_bottom_virtual_work", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, + # the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, } config = { - "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file - "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files - "solverStructureDiagramFile": "out/tendon_bottom_solver_structure.txt", # output file of a diagram that shows data connection between solvers - "mappingsBetweenMeshesLogFile": "out/tendon_bottom_mappings_between_meshes_log.txt", # log file for mappings - "Meshes": variables.meshes, - - "PreciceAdapter": { # precice adapter for bottom tendon - "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console - "timestepWidth": 1, # coupling time step width, must match the value in the precice config - "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": variables.precice_config_file, - "preciceParticipantName": "Tendon-Bottom", # name of the own precice participant, has to match the name given in the precice xml config file - "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication - "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged - "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver - { - "preciceMeshName": "Tendon-Bottom-Mesh", # precice name of the 2D coupling mesh - "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top - } - ], - "preciceData": [ - { - "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file - "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file - "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Tendon-Bottom-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings - "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file - } - ], - "HyperelasticitySolver": config_hyperelasticity, - "DynamicHyperelasticitySolver": config_hyperelasticity, - } + "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file + "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files + # output file of a diagram that shows data connection between solvers + "solverStructureDiagramFile": "out/tendon_bottom_solver_structure.txt", + "mappingsBetweenMeshesLogFile": "out/tendon_bottom_mappings_between_meshes_log.txt", # log file for mappings + "Meshes": variables.meshes, + + "PreciceAdapter": { # precice adapter for bottom tendon + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "couplingEnabled": True, + "preciceConfigFilename": variables.precice_config_file, + # name of the own precice participant, has to match the name given in the precice xml config file + "preciceParticipantName": "Tendon-Bottom", + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + # if the output writers should be called only after a time window of + # precice is complete, this means the timestep has converged + "outputOnlyConvergedTimeSteps": True, + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "preciceMeshName": "Tendon-Bottom-Mesh", # precice name of the 2D coupling mesh + "face": "2+", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + } + ], + "preciceData": [ + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "write-displacements-velocities", + # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Tendon-Bottom-Mesh", + # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "displacementsName": "Displacement", + # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "read-traction", + # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Tendon-Bottom-Mesh", + # name of the traction "data", i.e. field variable, as given in the precice xml settings file + "tractionName": "Traction", + } + ], + "HyperelasticitySolver": config_hyperelasticity, + "DynamicHyperelasticitySolver": config_hyperelasticity, + } } diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py index b0d546c52..68e17404a 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/settings-tendon-top-A.py @@ -1,6 +1,8 @@ # Transversely-isotropic Mooney Rivlin on a tendon geometry -# Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. -import sys, os +# Note, this is not possible to be run in parallel because the fibers +# cannot be initialized without MultipleInstances class. +import sys +import os import numpy as np import sys @@ -8,12 +10,12 @@ title = "tendon-top-a" print('\33]0;{}\a'.format(title), end='', flush=True) -#add variables subfolder to python path where the variables script is located +# add variables subfolder to python path where the variables script is located script_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, script_path) -sys.path.insert(0, os.path.join(script_path,'variables')) +sys.path.insert(0, os.path.join(script_path, 'variables')) -import variables +import variables from create_partitioned_meshes_for_settings import * # update material parameters @@ -33,15 +35,21 @@ # material parameters for Saint Venant-Kirchhoff material # https://www.researchgate.net/publication/230248067_Bulk_Modulus - youngs_modulus = 7e4 # [N/cm^2 = 10kPa] + youngs_modulus = 7e4 # [N/cm^2 = 10kPa] shear_modulus = 3e4 - lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda + lambd = shear_modulus * (youngs_modulus - 2 * shear_modulus) / \ + (3 * shear_modulus - youngs_modulus) # Lamé parameter lambda mu = shear_modulus # Lamé parameter mu or G (shear modulus) variables.material_parameters = [lambd, mu] -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False # parse arguments rank_no = (int)(sys.argv[-2]) @@ -49,10 +57,18 @@ # compute partitioning if rank_no == 0: - if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: - print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) - sys.exit(-1) - + if n_ranks != variables.n_subdomains_x * variables.n_subdomains_y * variables.n_subdomains_z: + print( + "\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format( + n_ranks, + variables.n_subdomains_x, + variables.n_subdomains_y, + variables.n_subdomains_z, + variables.n_subdomains_x * + variables.n_subdomains_y * + variables.n_subdomains_z)) + sys.exit(-1) + # stride for sampling the 3D elements from the fiber data # here any number is possible sampling_stride_x = 1 @@ -61,19 +77,29 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( - variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.fiber_file, load_fiber_data, sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) -[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result +[variables.meshes, + variables.own_subdomain_coordinate_x, + variables.own_subdomain_coordinate_y, + variables.own_subdomain_coordinate_z, + variables.n_fibers_x, + variables.n_fibers_y, + variables.n_points_whole_fiber] = result -n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) -n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) -n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) -n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z -nx = n_points_3D_mesh_linear_global_x-1 -ny = n_points_3D_mesh_linear_global_y-1 -nz = n_points_3D_mesh_linear_global_z-1 +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) + for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) + for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) + for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x * \ + n_points_3D_mesh_linear_global_y * n_points_3D_mesh_linear_global_z +nx = n_points_3D_mesh_linear_global_x - 1 +ny = n_points_3D_mesh_linear_global_y - 1 +nz = n_points_3D_mesh_linear_global_z - 1 node_positions = variables.meshes["3Dmesh_quadratic"]["nodePositions"] @@ -84,148 +110,221 @@ # set Dirichlet BC, fix top end of tendon that is attached to the bone variables.elasticity_dirichlet_bc = {} -k = mz-1 - +k = mz - 1 + # fix the whole x-y plane for j in range(my): - for i in range(mx): - variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,0.0,None,None,None] - + for i in range(mx): + variables.elasticity_dirichlet_bc[k * mx * my + j * mx + i] = [0.0, 0.0, 0.0, None, None, None] + # set no Neumann BC variables.elasticity_neumann_bc = [] config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" - "timeStepWidth": variables.dt_elasticity, # time step width - "endTime": variables.end_time, # end time of the simulation time span - "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file - "timeStepOutputInterval": 1, # how often the current time step should be printed to console - - "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material - "density": variables.rho, # density of the material - "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements - "residualNormLogFilename": "out/tendon_top_a_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written - "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) - "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian - - "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) - # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian - - # mesh - "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions - "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings - - "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction - #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system - "fiberDirectionInElement": [0,0,1], # if fiberMeshNames and fiberDirections are empty, directly set the constant fiber direction, in element coordinate system - - # nonlinear solver - "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver - "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver - "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls - "preconditionerType": "lu", # type of the preconditioner - "maxIterations": 1e4, # maximum number of iterations in the linear solver - "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations - "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver - "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver - "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" - "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver - "snesRebuildJacobianFrequency": 5, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again - - #"dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve - "dumpFilename": "", # dump disabled - "dumpFormat": "matlab", # default, ascii, matlab - - #"loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep - #"loadFactors": [0.5, 1.0], # load factors for every timestep - "loadFactors": [], # no load factors, solve problem directly - "loadFactorGiveUpThreshold": 1e-3, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the load factors get too small, it aborts the solve. - "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called - - # boundary and initial conditions - "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v - "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements - "divideNeumannBoundaryConditionValuesByTotalArea": False, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied - "updateDirichletBoundaryConditionsFunction": None, # function that updates the dirichlet BCs while the simulation is running - "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step - - "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "extrapolateInitialGuess": False, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities - "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity - - "dirichletOutputFilename": "out/tendon_top_a_dirichlet_boundary_conditions_tendon_top_a", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable - "totalForceLogFilename": "out/tendon_top_a_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume - "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file - "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename - "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal - - # define which file formats should be written - # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. - "OutputWriter" : [ - - # Paraview files - {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_a", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - - # Python callback function "postprocess" - #{"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, - ], - # 2. additional output writer that writes also the hydrostatic pressure - "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, - # 3. additional output writer that writes virtual work terms - "dynamic": { # output of the dynamic solver, has additional virtual work values - "OutputWriter" : [ # output files for displacements function space (quadratic elements) - {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_a_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + "timeStepWidth": variables.dt_elasticity, # time step width + "endTime": variables.end_time, # end time of the simulation time span + "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + # scaling factor for displacements, only set to sth. other than 1 only to + # increase visual appearance for very small displacements + "displacementsScalingFactor": 1.0, + # log file where residual norm values of the nonlinear solver will be written + "residualNormLogFilename": "out/tendon_top_a_log_residual_norm.txt", + # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useAnalyticJacobian": True, + # whether to use the numerically computed jacobian matrix in the nonlinear + # solver (slow), only works with non-nested matrices, if both numeric and + # analytic are enable, it uses the analytic for the preconditioner and the + # numeric as normal jacobian + "useNumericJacobian": False, + + # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + "dumpDenseMatlabVariables": False, + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables + # all all three true, the analytic and numeric jacobian matrices will get + # compared to see if there are programming errors for the analytic + # jacobian + + # mesh + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings + "inputMeshIsGlobal": True, + + "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction + # "fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + # if fiberMeshNames and fiberDirections are empty, directly set the + # constant fiber direction, in element coordinate system + "fiberDirectionInElement": [0, 0, 1], + + # nonlinear solver + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver + # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesLineSearchType": "l2", + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, + # 1 means rebuild every time the Jacobian is computed within a single + # nonlinear solve, 2 means every second time the Jacobian is built etc. -2 + # means rebuild at next chance but then never again + "snesRebuildJacobianFrequency": 5, + + # "dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve + "dumpFilename": "", # dump disabled + "dumpFormat": "matlab", # default, ascii, matlab + + # "loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep + # "loadFactors": [0.5, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + # a threshold for the load factor, when to abort the solve of the current + # time step. The load factors are adjusted automatically if the nonlinear + # solver diverged. If the load factors get too small, it aborts the solve. + "loadFactorGiveUpThreshold": 1e-3, + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called + + # boundary and initial conditions + # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, + # Neumann boundary conditions that define traction forces on surfaces of elements + "neumannBoundaryConditions": variables.elasticity_neumann_bc, + # if the given Neumann boundary condition values under + # "neumannBoundaryConditions" are total forces instead of surface loads + # and therefore should be scaled by the surface area of all elements where + # Neumann BC are applied + "divideNeumannBoundaryConditionValuesByTotalArea": False, + # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunction": None, + # every which step the update function should be called, 1 means every time step + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, + + # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesDisplacements": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # if the initial values for the dynamic nonlinear problem should be + # computed by extrapolating the previous displacements and velocities + "extrapolateInitialGuess": False, + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + # filename for a vtp file that contains the Dirichlet boundary condition + # nodes and their values, set to None to disable + "dirichletOutputFilename": "out/tendon_top_a_dirichlet_boundary_conditions_tendon_top_a", + # filename of a log file that will contain the total (bearing) forces and + # moments at the top and bottom of the volume + "totalForceLogFilename": "out/tendon_top_a_force.csv", + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceBottomElementNosGlobal": [j * nx + i for j in range(ny) for i in range(nx)], + # global element nos of the top elements used to compute the total forces + # in the log file totalForceTopElementsGlobal + "totalForceTopElementNosGlobal": [(nz - 1) * ny * nx + j * nx + i for j in range(ny) for i in range(nx)], + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic + # elements function space. Writes displacements, velocities and PK2 + # stresses. + "OutputWriter": [ + + # Paraview files + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_elasticity * variables.output_timestep_3D), + "filename": "out/tendon_top_a", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + # {"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, ], - }, - # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written - "LoadIncrements": { - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter": [ # output files for displacements function space (quadratic elements) + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_elasticity * variables.output_timestep_3D), + "filename": "out/tendon_top_a_virtual_work", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, + # the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, } config = { - "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file - "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files - "solverStructureDiagramFile": "out/tendon_top_a_solver_structure.txt", # output file of a diagram that shows data connection between solvers - "mappingsBetweenMeshesLogFile": "out/tendon_top_a_mappings_between_meshes_log.txt", # log file for mappings - "Meshes": variables.meshes, - - "PreciceAdapter": { # precice adapter for bottom tendon - "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console - "timestepWidth": 1, # coupling time step width, must match the value in the precice config - "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file - "preciceParticipantName": "Tendon-Top-A", # name of the own precice participant, has to match the name given in the precice xml config file - "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication - "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged - "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver - { - "preciceMeshName": "Tendon-Top-A-Mesh", # precice name of the 2D coupling mesh - "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top - } - ], - "preciceData": [ - { - "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file - "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file - "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Tendon-Top-A-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings - "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file - } - ], - "HyperelasticitySolver": config_hyperelasticity, - "DynamicHyperelasticitySolver": config_hyperelasticity, - } + "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file + "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files + # output file of a diagram that shows data connection between solvers + "solverStructureDiagramFile": "out/tendon_top_a_solver_structure.txt", + "mappingsBetweenMeshesLogFile": "out/tendon_top_a_mappings_between_meshes_log.txt", # log file for mappings + "Meshes": variables.meshes, + + "PreciceAdapter": { # precice adapter for bottom tendon + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "couplingEnabled": True, + "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file + # name of the own precice participant, has to match the name given in the precice xml config file + "preciceParticipantName": "Tendon-Top-A", + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + # if the output writers should be called only after a time window of + # precice is complete, this means the timestep has converged + "outputOnlyConvergedTimeSteps": True, + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "preciceMeshName": "Tendon-Top-A-Mesh", # precice name of the 2D coupling mesh + "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + } + ], + "preciceData": [ + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "write-displacements-velocities", + # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Tendon-Top-A-Mesh", + # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "displacementsName": "Displacement", + # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "read-traction", + # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Tendon-Top-A-Mesh", + # name of the traction "data", i.e. field variable, as given in the precice xml settings file + "tractionName": "Traction", + } + ], + "HyperelasticitySolver": config_hyperelasticity, + "DynamicHyperelasticitySolver": config_hyperelasticity, + } } diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py index 0deb3cd91..8a6a63b7f 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/settings-tendon-top-B.py @@ -1,6 +1,8 @@ # Transversely-isotropic Mooney Rivlin on a tendon geometry -# Note, this is not possible to be run in parallel because the fibers cannot be initialized without MultipleInstances class. -import sys, os +# Note, this is not possible to be run in parallel because the fibers +# cannot be initialized without MultipleInstances class. +import sys +import os import numpy as np import sys @@ -8,12 +10,12 @@ title = "tendon-top-b" print('\33]0;{}\a'.format(title), end='', flush=True) -#add variables subfolder to python path where the variables script is located +# add variables subfolder to python path where the variables script is located script_path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, script_path) -sys.path.insert(0, os.path.join(script_path,'variables')) +sys.path.insert(0, os.path.join(script_path, 'variables')) -import variables +import variables from create_partitioned_meshes_for_settings import * # update material parameters @@ -33,15 +35,21 @@ # material parameters for Saint Venant-Kirchhoff material # https://www.researchgate.net/publication/230248067_Bulk_Modulus - youngs_modulus = 7e4 # [N/cm^2 = 10kPa] + youngs_modulus = 7e4 # [N/cm^2 = 10kPa] shear_modulus = 3e4 - lambd = shear_modulus*(youngs_modulus - 2*shear_modulus) / (3*shear_modulus - youngs_modulus) # Lamé parameter lambda + lambd = shear_modulus * (youngs_modulus - 2 * shear_modulus) / \ + (3 * shear_modulus - youngs_modulus) # Lamé parameter lambda mu = shear_modulus # Lamé parameter mu or G (shear modulus) variables.material_parameters = [lambd, mu] -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False # parse arguments rank_no = (int)(sys.argv[-2]) @@ -52,10 +60,18 @@ # compute partitioning if rank_no == 0: - if n_ranks != variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z: - print("\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format(n_ranks, variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.n_subdomains_x*variables.n_subdomains_y*variables.n_subdomains_z)) - sys.exit(-1) - + if n_ranks != variables.n_subdomains_x * variables.n_subdomains_y * variables.n_subdomains_z: + print( + "\n\nError! Number of ranks {} does not match given partitioning {} x {} x {} = {}.\n\n".format( + n_ranks, + variables.n_subdomains_x, + variables.n_subdomains_y, + variables.n_subdomains_z, + variables.n_subdomains_x * + variables.n_subdomains_y * + variables.n_subdomains_z)) + sys.exit(-1) + # stride for sampling the 3D elements from the fiber data # here any number is possible sampling_stride_x = 1 @@ -64,19 +80,29 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( - variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, + variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, variables.fiber_file, load_fiber_data, sampling_stride_x, sampling_stride_y, sampling_stride_z, True, True) -[variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, variables.own_subdomain_coordinate_z, variables.n_fibers_x, variables.n_fibers_y, variables.n_points_whole_fiber] = result +[variables.meshes, + variables.own_subdomain_coordinate_x, + variables.own_subdomain_coordinate_y, + variables.own_subdomain_coordinate_z, + variables.n_fibers_x, + variables.n_fibers_y, + variables.n_points_whole_fiber] = result -n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) for subdomain_coordinate_x in range(variables.n_subdomains_x)]) -n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) for subdomain_coordinate_y in range(variables.n_subdomains_y)]) -n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) for subdomain_coordinate_z in range(variables.n_subdomains_z)]) -n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x*n_points_3D_mesh_linear_global_y*n_points_3D_mesh_linear_global_z -nx = n_points_3D_mesh_linear_global_x-1 -ny = n_points_3D_mesh_linear_global_y-1 -nz = n_points_3D_mesh_linear_global_z-1 +n_points_3D_mesh_linear_global_x = sum([n_sampled_points_in_subdomain_x(subdomain_coordinate_x) + for subdomain_coordinate_x in range(variables.n_subdomains_x)]) +n_points_3D_mesh_linear_global_y = sum([n_sampled_points_in_subdomain_y(subdomain_coordinate_y) + for subdomain_coordinate_y in range(variables.n_subdomains_y)]) +n_points_3D_mesh_linear_global_z = sum([n_sampled_points_in_subdomain_z(subdomain_coordinate_z) + for subdomain_coordinate_z in range(variables.n_subdomains_z)]) +n_points_3D_mesh_linear_global = n_points_3D_mesh_linear_global_x * \ + n_points_3D_mesh_linear_global_y * n_points_3D_mesh_linear_global_z +nx = n_points_3D_mesh_linear_global_x - 1 +ny = n_points_3D_mesh_linear_global_y - 1 +nz = n_points_3D_mesh_linear_global_z - 1 node_positions = variables.meshes["3Dmesh_quadratic"]["nodePositions"] @@ -87,147 +113,220 @@ # set Dirichlet BC, fix top end of tendon that is attached to the bone variables.elasticity_dirichlet_bc = {} -k = mz-1 - +k = mz - 1 + # fix the whole x-y plane for j in range(my): - for i in range(mx): - variables.elasticity_dirichlet_bc[k*mx*my + j*mx + i] = [0.0,0.0,0.0,None,None,None] - + for i in range(mx): + variables.elasticity_dirichlet_bc[k * mx * my + j * mx + i] = [0.0, 0.0, 0.0, None, None, None] + # set no Neumann BC variables.elasticity_neumann_bc = [] config_hyperelasticity = { # for both "HyperelasticitySolver" and "DynamicHyperelasticitySolver" - "timeStepWidth": variables.dt_elasticity, # time step width - "endTime": variables.end_time, # end time of the simulation time span - "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file - "timeStepOutputInterval": 1, # how often the current time step should be printed to console - - "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material - "density": variables.rho, # density of the material - "displacementsScalingFactor": 1.0, # scaling factor for displacements, only set to sth. other than 1 only to increase visual appearance for very small displacements - "residualNormLogFilename": "out/tendon_top_b_log_residual_norm.txt", # log file where residual norm values of the nonlinear solver will be written - "useAnalyticJacobian": True, # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) - "useNumericJacobian": False, # whether to use the numerically computed jacobian matrix in the nonlinear solver (slow), only works with non-nested matrices, if both numeric and analytic are enable, it uses the analytic for the preconditioner and the numeric as normal jacobian - - "dumpDenseMatlabVariables": False, # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) - # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables all all three true, the analytic and numeric jacobian matrices will get compared to see if there are programming errors for the analytic jacobian - - # mesh - "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions - "inputMeshIsGlobal": True, # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings - - "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction - #"fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system - "fiberDirectionInElement": [0,0,1], # if fiberMeshNames and fiberDirections are empty, directly set the constant fiber direction, in element coordinate system - - # nonlinear solver - "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver - "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver - "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls - "preconditionerType": "lu", # type of the preconditioner - "maxIterations": 1e4, # maximum number of iterations in the linear solver - "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations - "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver - "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver - "snesLineSearchType": "l2", # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" - "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver - "snesRebuildJacobianFrequency": 5, # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, 1 means rebuild every time the Jacobian is computed within a single nonlinear solve, 2 means every second time the Jacobian is built etc. -2 means rebuild at next chance but then never again - - #"dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve - "dumpFilename": "", # dump disabled - "dumpFormat": "matlab", # default, ascii, matlab - - #"loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep - #"loadFactors": [0.5, 1.0], # load factors for every timestep - "loadFactors": [], # no load factors, solve problem directly - "loadFactorGiveUpThreshold": 1e-3, # a threshold for the load factor, when to abort the solve of the current time step. The load factors are adjusted automatically if the nonlinear solver diverged. If the load factors get too small, it aborts the solve. - "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called - - # boundary and initial conditions - "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, # the initial Dirichlet boundary conditions that define values for displacements u and velocity v - "neumannBoundaryConditions": variables.elasticity_neumann_bc, # Neumann boundary conditions that define traction forces on surfaces of elements - "divideNeumannBoundaryConditionValuesByTotalArea": False, # if the given Neumann boundary condition values under "neumannBoundaryConditions" are total forces instead of surface loads and therefore should be scaled by the surface area of all elements where Neumann BC are applied - "updateDirichletBoundaryConditionsFunction": None, # function that updates the dirichlet BCs while the simulation is running - "updateDirichletBoundaryConditionsFunctionCallInterval": 1, # every which step the update function should be called, 1 means every time step - - "initialValuesDisplacements": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "initialValuesVelocities": [[0.0,0.0,0.0] for _ in range(mx*my*mz)], # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] - "extrapolateInitialGuess": False, # if the initial values for the dynamic nonlinear problem should be computed by extrapolating the previous displacements and velocities - "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity - - "dirichletOutputFilename": "out/tendon_top_b_dirichlet_boundary_conditions", # filename for a vtp file that contains the Dirichlet boundary condition nodes and their values, set to None to disable - "totalForceLogFilename": "out/tendon_top_b_force.csv", # filename of a log file that will contain the total (bearing) forces and moments at the top and bottom of the volume - "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file - "totalForceBottomElementNosGlobal": [j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename - "totalForceTopElementNosGlobal": [(nz-1)*ny*nx + j*nx + i for j in range(ny) for i in range(nx)], # global element nos of the top elements used to compute the total forces in the log file totalForceTopElementsGlobal - - # define which file formats should be written - # 1. main output writer that writes output files using the quadratic elements function space. Writes displacements, velocities and PK2 stresses. - "OutputWriter" : [ - - # Paraview files - {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_b", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - - # Python callback function "postprocess" - #{"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, - ], - # 2. additional output writer that writes also the hydrostatic pressure - "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, - # 3. additional output writer that writes virtual work terms - "dynamic": { # output of the dynamic solver, has additional virtual work values - "OutputWriter" : [ # output files for displacements function space (quadratic elements) - {"format": "Paraview", "outputInterval": int(1./variables.dt_elasticity*variables.output_timestep_3D), "filename": "out/tendon_top_b_virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - #{"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + "timeStepWidth": variables.dt_elasticity, # time step width + "endTime": variables.end_time, # end time of the simulation time span + "durationLogKey": "duration_mechanics", # key to find duration of this solver in the log file + "timeStepOutputInterval": 1, # how often the current time step should be printed to console + + "materialParameters": variables.material_parameters, # material parameters of the Mooney-Rivlin material + "density": variables.rho, # density of the material + # scaling factor for displacements, only set to sth. other than 1 only to + # increase visual appearance for very small displacements + "displacementsScalingFactor": 1.0, + # log file where residual norm values of the nonlinear solver will be written + "residualNormLogFilename": "out/tendon_top_b_log_residual_norm.txt", + # whether to use the analytically computed jacobian matrix in the nonlinear solver (fast) + "useAnalyticJacobian": True, + # whether to use the numerically computed jacobian matrix in the nonlinear + # solver (slow), only works with non-nested matrices, if both numeric and + # analytic are enable, it uses the analytic for the preconditioner and the + # numeric as normal jacobian + "useNumericJacobian": False, + + # whether to have extra output of matlab vectors, x,r, jacobian matrix (very slow) + "dumpDenseMatlabVariables": False, + # if useAnalyticJacobian,useNumericJacobian and dumpDenseMatlabVariables + # all all three true, the analytic and numeric jacobian matrices will get + # compared to see if there are programming errors for the analytic + # jacobian + + # mesh + "meshName": "3Dmesh_quadratic", # mesh with quadratic Lagrange ansatz functions + # boundary conditions are specified in global numberings, whereas the mesh is given in local numberings + "inputMeshIsGlobal": True, + + "fiberMeshNames": [], # fiber meshes that will be used to determine the fiber direction + # "fiberDirection": [0,0,1], # if fiberMeshNames is empty, directly set the constant fiber direction, in element coordinate system + # if fiberMeshNames and fiberDirections are empty, directly set the + # constant fiber direction, in element coordinate system + "fiberDirectionInElement": [0, 0, 1], + + # nonlinear solver + "relativeTolerance": 1e-10, # 1e-10 relative tolerance of the linear solver + "absoluteTolerance": 1e-10, # 1e-10 absolute tolerance of the residual of the linear solver + "solverType": "preonly", # type of the linear solver: cg groppcg pipecg pipecgrr cgne nash stcg gltr richardson chebyshev gmres tcqmr fcg pipefcg bcgs ibcgs fbcgs fbcgsr bcgsl cgs tfqmr cr pipecr lsqr preonly qcg bicg fgmres pipefgmres minres symmlq lgmres lcd gcr pipegcr pgmres dgmres tsirm cgls + "preconditionerType": "lu", # type of the preconditioner + "maxIterations": 1e4, # maximum number of iterations in the linear solver + "snesMaxFunctionEvaluations": 1e8, # maximum number of function iterations + "snesMaxIterations": 240, # maximum number of iterations in the nonlinear solver + "snesRelativeTolerance": 1e-2, # relative tolerance of the nonlinear solver + # type of linesearch, possible values: "bt" "nleqerr" "basic" "l2" "cp" "ncglinear" + "snesLineSearchType": "l2", + "snesAbsoluteTolerance": 1e-5, # absolute tolerance of the nonlinear solver + # how often the jacobian should be recomputed, -1 indicates NEVER rebuild, + # 1 means rebuild every time the Jacobian is computed within a single + # nonlinear solve, 2 means every second time the Jacobian is built etc. -2 + # means rebuild at next chance but then never again + "snesRebuildJacobianFrequency": 5, + + # "dumpFilename": "out/r{}/m".format(sys.argv[-1]), # dump system matrix and right hand side after every solve + "dumpFilename": "", # dump disabled + "dumpFormat": "matlab", # default, ascii, matlab + + # "loadFactors": [0.1, 0.2, 0.35, 0.5, 1.0], # load factors for every timestep + # "loadFactors": [0.5, 1.0], # load factors for every timestep + "loadFactors": [], # no load factors, solve problem directly + # a threshold for the load factor, when to abort the solve of the current + # time step. The load factors are adjusted automatically if the nonlinear + # solver diverged. If the load factors get too small, it aborts the solve. + "loadFactorGiveUpThreshold": 1e-3, + "nNonlinearSolveCalls": 1, # how often the nonlinear solve should be called + + # boundary and initial conditions + # the initial Dirichlet boundary conditions that define values for displacements u and velocity v + "dirichletBoundaryConditions": variables.elasticity_dirichlet_bc, + # Neumann boundary conditions that define traction forces on surfaces of elements + "neumannBoundaryConditions": variables.elasticity_neumann_bc, + # if the given Neumann boundary condition values under + # "neumannBoundaryConditions" are total forces instead of surface loads + # and therefore should be scaled by the surface area of all elements where + # Neumann BC are applied + "divideNeumannBoundaryConditionValuesByTotalArea": False, + # function that updates the dirichlet BCs while the simulation is running + "updateDirichletBoundaryConditionsFunction": None, + # every which step the update function should be called, 1 means every time step + "updateDirichletBoundaryConditionsFunctionCallInterval": 1, + + # the initial values for the displacements, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesDisplacements": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # the initial values for the velocities, vector of values for every node [[node1-x,y,z], [node2-x,y,z], ...] + "initialValuesVelocities": [[0.0, 0.0, 0.0] for _ in range(mx * my * mz)], + # if the initial values for the dynamic nonlinear problem should be + # computed by extrapolating the previous displacements and velocities + "extrapolateInitialGuess": False, + "constantBodyForce": variables.constant_body_force, # a constant force that acts on the whole body, e.g. for gravity + + # filename for a vtp file that contains the Dirichlet boundary condition + # nodes and their values, set to None to disable + "dirichletOutputFilename": "out/tendon_top_b_dirichlet_boundary_conditions", + # filename of a log file that will contain the total (bearing) forces and + # moments at the top and bottom of the volume + "totalForceLogFilename": "out/tendon_top_b_force.csv", + "totalForceLogOutputInterval": 10, # output interval when to write the totalForceLog file + # global element nos of the bottom elements used to compute the total forces in the log file totalForceLogFilename + "totalForceBottomElementNosGlobal": [j * nx + i for j in range(ny) for i in range(nx)], + # global element nos of the top elements used to compute the total forces + # in the log file totalForceTopElementsGlobal + "totalForceTopElementNosGlobal": [(nz - 1) * ny * nx + j * nx + i for j in range(ny) for i in range(nx)], + + # define which file formats should be written + # 1. main output writer that writes output files using the quadratic + # elements function space. Writes displacements, velocities and PK2 + # stresses. + "OutputWriter": [ + + # Paraview files + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_elasticity * variables.output_timestep_3D), + "filename": "out/tendon_top_b", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + + # Python callback function "postprocess" + # {"format": "PythonCallback", "outputInterval": 1, "callback": postprocess, "onlyNodalValues":True, "filename": ""}, ], - }, - # 4. output writer for debugging, outputs files after each load increment, the geometry is not changed but u and v are written - "LoadIncrements": { - "OutputWriter" : [ - #{"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, - ] - }, + # 2. additional output writer that writes also the hydrostatic pressure + "pressure": { # output files for pressure function space (linear elements), contains pressure values, as well as displacements and velocities + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/p", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, + # 3. additional output writer that writes virtual work terms + "dynamic": { # output of the dynamic solver, has additional virtual work values + "OutputWriter": [ # output files for displacements function space (quadratic elements) + {"format": "Paraview", + "outputInterval": int(1. / variables.dt_elasticity * variables.output_timestep_3D), + "filename": "out/tendon_top_b_virtual_work", + "binary": True, + "fixedFormat": False, + "onlyNodalValues": True, + "combineFiles": True, + "fileNumbering": "incremental"}, + # {"format": "Paraview", "outputInterval": 1, "filename": "out/"+variables.scenario_name+"/virtual_work", "binary": True, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ], + }, + # 4. output writer for debugging, outputs files after each load increment, + # the geometry is not changed but u and v are written + "LoadIncrements": { + "OutputWriter": [ + # {"format": "Paraview", "outputInterval": 1, "filename": "out/load_increments", "binary": False, "fixedFormat": False, "onlyNodalValues":True, "combineFiles":True, "fileNumbering": "incremental"}, + ] + }, } config = { - "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file - "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files - "solverStructureDiagramFile": "out/tendon_top_b_solver_structure.txt", # output file of a diagram that shows data connection between solvers - "mappingsBetweenMeshesLogFile": "out/tendon_top_b_mappings_between_meshes_log.txt", # log file for mappings - "Meshes": variables.meshes, - - "PreciceAdapter": { # precice adapter for bottom tendon - "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console - "timestepWidth": 1, # coupling time step width, must match the value in the precice config - "couplingEnabled": True, # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging - "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file - "preciceParticipantName": "Tendon-Top-B", # name of the own precice participant, has to match the name given in the precice xml config file - "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication - "outputOnlyConvergedTimeSteps": True, # if the output writers should be called only after a time window of precice is complete, this means the timestep has converged - "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver - { - "preciceMeshName": "Tendon-Top-B-Mesh", # precice name of the 2D coupling mesh - "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top - } - ], - "preciceData": [ - { - "mode": "write-displacements-velocities", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings file - "displacementsName": "Displacement", # name of the displacements "data", i.e. field variable, as given in the precice xml settings file - "velocitiesName": "Velocity", # name of the velocity "data", i.e. field variable, as given in the precice xml settings file - }, - { - "mode": "read-traction", # mode is one of "read-displacements-velocities", "read-traction", "write-displacements-velocities", "write-traction" - "preciceMeshName": "Tendon-Top-B-Mesh", # name of the precice coupling surface mesh, as given in the precice xml settings - "tractionName": "Traction", # name of the traction "data", i.e. field variable, as given in the precice xml settings file - } - ], - "HyperelasticitySolver": config_hyperelasticity, - "DynamicHyperelasticitySolver": config_hyperelasticity, - } + "scenarioName": variables.scenario_name, # scenario name to identify the simulation runs in the log file + "logFormat": "csv", # "csv" or "json", format of the lines in the log file, csv gives smaller files + # output file of a diagram that shows data connection between solvers + "solverStructureDiagramFile": "out/tendon_top_b_solver_structure.txt", + "mappingsBetweenMeshesLogFile": "out/tendon_top_b_mappings_between_meshes_log.txt", # log file for mappings + "Meshes": variables.meshes, + + "PreciceAdapter": { # precice adapter for bottom tendon + "timeStepOutputInterval": 100, # interval in which to display current timestep and time in console + "timestepWidth": 1, # coupling time step width, must match the value in the precice config + # if the precice coupling is enabled, if not, it simply calls the nested solver, for debugging + "couplingEnabled": True, + "preciceConfigFilename": variables.precice_config_file, # the preCICE configuration file + # name of the own precice participant, has to match the name given in the precice xml config file + "preciceParticipantName": "Tendon-Top-B", + "scalingFactor": 1, # a factor to scale the exchanged data, prior to communication + # if the output writers should be called only after a time window of + # precice is complete, this means the timestep has converged + "outputOnlyConvergedTimeSteps": True, + "preciceMeshes": [ # the precice meshes get created as the top or bottom surface of the main geometry mesh of the nested solver + { + "preciceMeshName": "Tendon-Top-B-Mesh", # precice name of the 2D coupling mesh + "face": "2-", # face of the 3D mesh where the 2D mesh is located, "2-" = bottom, "2+" = top + } + ], + "preciceData": [ + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "write-displacements-velocities", + # name of the precice coupling surface mesh, as given in the precice xml settings file + "preciceMeshName": "Tendon-Top-B-Mesh", + # name of the displacements "data", i.e. field variable, as given in the precice xml settings file + "displacementsName": "Displacement", + # name of the velocity "data", i.e. field variable, as given in the precice xml settings file + "velocitiesName": "Velocity", + }, + { + # mode is one of "read-displacements-velocities", "read-traction", + # "write-displacements-velocities", "write-traction" + "mode": "read-traction", + # name of the precice coupling surface mesh, as given in the precice xml settings + "preciceMeshName": "Tendon-Top-B-Mesh", + # name of the traction "data", i.e. field variable, as given in the precice xml settings file + "tractionName": "Traction", + } + ], + "HyperelasticitySolver": config_hyperelasticity, + "DynamicHyperelasticitySolver": config_hyperelasticity, + } } From e3bd5e299b44c1001e25e4ab2115c3a77218ded7 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 09:53:36 +0100 Subject: [PATCH 22/40] Bump GItHub Action autopep8 from v1 to v2 --- .github/workflows/check-pep8.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-pep8.yml b/.github/workflows/check-pep8.yml index e89bbbbae..0bd0d44c9 100644 --- a/.github/workflows/check-pep8.yml +++ b/.github/workflows/check-pep8.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v2 - name: autopep8 id: autopep8 - uses: peter-evans/autopep8@v1 + uses: peter-evans/autopep8@v2 with: args: --recursive --diff --aggressive --aggressive --exit-code --ignore E402 --max-line-length 120 . - name: Fail if autopep8 made changes From 83f7597a1810eb3a3ebccbbcf2fd9696f9dca420 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 10:07:09 +0100 Subject: [PATCH 23/40] Format more files with autopep8 pip3 install --user autopep8 autopep8 --recursive --in-place --aggressive --exit-code --ignore E402 --max-line-length 120 . --- .../muscle-opendihu/variables/variables.py | 58 ++++++++++++------- .../variables/variables.py | 28 +++++---- .../variables/variables.py | 28 +++++---- .../variables/variables.py | 28 +++++---- 4 files changed, 89 insertions(+), 53 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/variables/variables.py b/muscle-tendon-complex/muscle-opendihu/variables/variables.py index 75740c47d..105d0d9c7 100644 --- a/muscle-tendon-complex/muscle-opendihu/variables/variables.py +++ b/muscle-tendon-complex/muscle-opendihu/variables/variables.py @@ -3,33 +3,38 @@ c1 = 3.176e-10 # [N/cm^2] c2 = 1.813 # [N/cm^2] -b = 1.075e-2 # [N/cm^2] anisotropy parameter -d = 9.1733 # [-] anisotropy parameter +b = 1.075e-2 # [N/cm^2] anisotropy parameter +d = 9.1733 # [-] anisotropy parameter material_parameters = [c1, c2, b, d] # material parameters pmax = 7.3 # maximum stress [N/cm^2] Conductivity = 3.828 # sigma, conductivity [mS/cm] Am = 500.0 # surface area to volume ratio [cm^-1] Cm = 0.58 # membrane capacitance [uF/cm^2] -innervation_zone_width = 0. # not used [cm], this will later be used to specify a variance of positions of the innervation point at the fibers +# not used [cm], this will later be used to specify a variance of positions of the innervation point at the fibers +innervation_zone_width = 0. rho = 10 # solvers # ------- diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation diffusion_preconditioner_type = "none" # preconditioner -potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined -potential_flow_preconditioner_type = "none" # preconditioner -emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +# solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_solver_type = "gmres" +potential_flow_preconditioner_type = "none" # preconditioner +# solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_solver_type = "cg" emg_preconditioner_type = "none" # preconditioner -emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution +emg_initial_guess_nonzero = False # < If the initial guess for the emg linear system should be set to the previous solution # timing parameters # ----------------- end_time = 1000.0 # [ms] end time of the simulation -stimulation_frequency = 100*1e-3 # [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. +# [ms^-1] sampling frequency of stimuli in firing_times_file, in stimulations per ms, number before 1e-3 factor is in Hertz. +stimulation_frequency = 100 * 1e-3 dt_0D = 1e-3 # [ms] timestep width of ODEs dt_1D = 1.5e-3 # [ms] timestep width of diffusion dt_splitting = 3e-3 # [ms] overall timestep width of strang splitting -dt_3D = 1e0 # [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +# [ms] time step width of coupling, when 3D should be performed, also sampling time of monopolar EMG +dt_3D = 1e0 output_timestep = 1e0 # [ms] timestep for output files output_timestep_3D_emg = 1e0 # [ms] timestep for output files output_timestep_3D = 1e0 # [ms] timestep for output files @@ -43,12 +48,18 @@ opendihu_home = os.environ.get('OPENDIHU_HOME') fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_9x9fibers.bin" fat_mesh_file = fiber_file + "_fat.bin" -firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_always.txt" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +# use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency +firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_always.txt" fiber_distribution_file = opendihu_home + "/examples/electrophysiology/input/MU_fibre_distribution_10MUs.txt" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_real.txt" precice_config_file = "../precice-config.xml" -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False debug_output = False # verbose output in this python script, for debugging the domain decomposition disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space paraview_output = False # If the paraview output writer should be enabled @@ -76,23 +87,30 @@ # functions, here, Am, Cm and Conductivity are constant for all fibers and MU's # These functions can be redefined differently in a custom variables script + + def get_am(fiber_no, mu_no): - return Am + return Am + def get_cm(fiber_no, mu_no): - return Cm - + return Cm + + def get_conductivity(fiber_no, mu_no): - return Conductivity + return Conductivity + def get_specific_states_call_frequency(fiber_no, mu_no): - return stimulation_frequency + return stimulation_frequency + def get_specific_states_frequency_jitter(fiber_no, mu_no): - return [0] + return [0] + def get_specific_states_call_enable_begin(fiber_no, mu_no): - return activation_start_time + return activation_start_time # further internal variables that will be set by the helper.py script and used in the config in settings_fibers_emg.py @@ -148,4 +166,4 @@ def get_specific_states_call_enable_begin(fiber_no, mu_no): enable_force_length_relation = True lambda_dot_scaling_factor = 1 mappings = None -vm_value_stimulated = None \ No newline at end of file +vm_value_stimulated = None diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py index 3aff9e6ba..34a7672ab 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py @@ -3,14 +3,13 @@ # time parameters # --------------- dt_elasticity = 1 # [ms] time step width for elasticity -end_time = 20000 # [ms] simulation time +end_time = 20000 # [ms] simulation time output_timestep_3D = 50 # [ms] output timestep # setup # ----- -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -force = 100.0 # [N] pulling force to the bottom - +constant_body_force = (0, 0, -9.81e-4) # [cm/ms^2], gravity constant for the body force +force = 100.0 # [N] pulling force to the bottom # input files @@ -19,9 +18,14 @@ import os opendihu_home = os.environ.get('OPENDIHU_HOME') fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon1.bin" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" precice_config_file = "../precice-config.xml" -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False debug_output = False # verbose output in this python script, for debugging the domain decomposition disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space paraview_output = False # If the paraview output writer should be enabled @@ -38,11 +42,13 @@ # ------- diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation diffusion_preconditioner_type = "none" # preconditioner -potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined -potential_flow_preconditioner_type = "none" # preconditioner -emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +# solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_solver_type = "gmres" +potential_flow_preconditioner_type = "none" # preconditioner +# solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_solver_type = "cg" emg_preconditioner_type = "none" # preconditioner -emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution +emg_initial_guess_nonzero = False # < If the initial guess for the emg linear system should be set to the previous solution # partitioning # ------------ @@ -113,4 +119,4 @@ enable_force_length_relation = True lambda_dot_scaling_factor = 1 mappings = None -vm_value_stimulated = None \ No newline at end of file +vm_value_stimulated = None diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py index 70d0aee9b..b53686c0f 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py @@ -3,14 +3,13 @@ # time parameters # --------------- dt_elasticity = 1 # [ms] time step width for elasticity -end_time = 20000 # [ms] simulation time +end_time = 20000 # [ms] simulation time output_timestep_3D = 50 # [ms] output timestep # setup # ----- -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -force = 100.0 # [N] pulling force to the bottom - +constant_body_force = (0, 0, -9.81e-4) # [cm/ms^2], gravity constant for the body force +force = 100.0 # [N] pulling force to the bottom # input files @@ -19,9 +18,14 @@ import os opendihu_home = os.environ.get('OPENDIHU_HOME') fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon2a.bin" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" precice_config_file = "../precice-config.xml" -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False debug_output = False # verbose output in this python script, for debugging the domain decomposition disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space paraview_output = False # If the paraview output writer should be enabled @@ -38,11 +42,13 @@ # ------- diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation diffusion_preconditioner_type = "none" # preconditioner -potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined -potential_flow_preconditioner_type = "none" # preconditioner -emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +# solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_solver_type = "gmres" +potential_flow_preconditioner_type = "none" # preconditioner +# solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_solver_type = "cg" emg_preconditioner_type = "none" # preconditioner -emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution +emg_initial_guess_nonzero = False # < If the initial guess for the emg linear system should be set to the previous solution # partitioning # ------------ @@ -113,4 +119,4 @@ enable_force_length_relation = True lambda_dot_scaling_factor = 1 mappings = None -vm_value_stimulated = None \ No newline at end of file +vm_value_stimulated = None diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py index 5aa58b6a9..eb7be320f 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py @@ -3,14 +3,13 @@ # time parameters # --------------- dt_elasticity = 1 # [ms] time step width for elasticity -end_time = 20000 # [ms] simulation time +end_time = 20000 # [ms] simulation time output_timestep_3D = 50 # [ms] output timestep # setup # ----- -constant_body_force = (0,0,-9.81e-4) # [cm/ms^2], gravity constant for the body force -force = 100.0 # [N] pulling force to the bottom - +constant_body_force = (0, 0, -9.81e-4) # [cm/ms^2], gravity constant for the body force +force = 100.0 # [N] pulling force to the bottom # input files @@ -19,9 +18,14 @@ import os opendihu_home = os.environ.get('OPENDIHU_HOME') fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon2b.bin" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" precice_config_file = "../precice-config.xml" -load_fiber_data = False # If the fiber geometry data should be loaded completely in the python script. If True, this reads the binary file and assigns the node positions in the config. If False, the C++ code will read the binary file and only extract the local node positions. This is more performant for highly parallel runs. +# If the fiber geometry data should be loaded completely in the python +# script. If True, this reads the binary file and assigns the node +# positions in the config. If False, the C++ code will read the binary +# file and only extract the local node positions. This is more performant +# for highly parallel runs. +load_fiber_data = False debug_output = False # verbose output in this python script, for debugging the domain decomposition disable_firing_output = True # Disables the initial list of fiber firings on the console to save some console space paraview_output = False # If the paraview output writer should be enabled @@ -38,11 +42,13 @@ # ------- diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation diffusion_preconditioner_type = "none" # preconditioner -potential_flow_solver_type = "gmres" # solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined -potential_flow_preconditioner_type = "none" # preconditioner -emg_solver_type = "cg" # solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +# solver and preconditioner for an initial Laplace flow on the domain, from which fiber directions are determined +potential_flow_solver_type = "gmres" +potential_flow_preconditioner_type = "none" # preconditioner +# solver and preconditioner for the 3D static Bidomain equation that solves the intra-muscular EMG signal +emg_solver_type = "cg" emg_preconditioner_type = "none" # preconditioner -emg_initial_guess_nonzero = False #< If the initial guess for the emg linear system should be set to the previous solution +emg_initial_guess_nonzero = False # < If the initial guess for the emg linear system should be set to the previous solution # partitioning # ------------ @@ -113,4 +119,4 @@ enable_force_length_relation = True lambda_dot_scaling_factor = 1 mappings = None -vm_value_stimulated = None \ No newline at end of file +vm_value_stimulated = None From dcd7a3f97037a3e6cb3e535c0405f1bf3d1dbcd2 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 10:10:53 +0100 Subject: [PATCH 24/40] Double --aggressive autopep8 --- .../muscle-opendihu/settings-muscle.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py index 7dce41893..e5675cb94 100644 --- a/muscle-tendon-complex/muscle-opendihu/settings-muscle.py +++ b/muscle-tendon-complex/muscle-opendihu/settings-muscle.py @@ -265,21 +265,29 @@ def mbool(x): return bool(distutils.util.strtobool(x)) # function to parse boo for subdomain_coordinate_y in range(variables.n_subdomains_y): for subdomain_coordinate_x in range(variables.n_subdomains_x): - print("subdomain (x{},y{}) ranks: {} n fibers in subdomain: x{},y{}".format(subdomain_coordinate_x, subdomain_coordinate_y, - list( - range( - subdomain_coordinate_y * - variables.n_subdomains_x + - subdomain_coordinate_x, - n_ranks, - variables.n_subdomains_x * - variables.n_subdomains_y)), - n_fibers_in_subdomain_x(subdomain_coordinate_x), n_fibers_in_subdomain_y(subdomain_coordinate_y))) + print( + "subdomain (x{},y{}) ranks: {} n fibers in subdomain: x{},y{}".format( + subdomain_coordinate_x, + subdomain_coordinate_y, + list( + range( + subdomain_coordinate_y * + variables.n_subdomains_x + + subdomain_coordinate_x, + n_ranks, + variables.n_subdomains_x * + variables.n_subdomains_y)), + n_fibers_in_subdomain_x(subdomain_coordinate_x), + n_fibers_in_subdomain_y(subdomain_coordinate_y))) for fiber_in_subdomain_coordinate_y in range(n_fibers_in_subdomain_y(subdomain_coordinate_y)): for fiber_in_subdomain_coordinate_x in range(n_fibers_in_subdomain_x(subdomain_coordinate_x)): - print("({},{}) n instances: {}".format(fiber_in_subdomain_coordinate_x, fiber_in_subdomain_coordinate_y, - n_fibers_in_subdomain_x(subdomain_coordinate_x) * n_fibers_in_subdomain_y(subdomain_coordinate_y))) + print( + "({},{}) n instances: {}".format( + fiber_in_subdomain_coordinate_x, + fiber_in_subdomain_coordinate_y, + n_fibers_in_subdomain_x(subdomain_coordinate_x) * + n_fibers_in_subdomain_y(subdomain_coordinate_y))) # define the config dict From f9325f3bacbe4bb5f4019932740549ef7711912f Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 10:11:43 +0100 Subject: [PATCH 25/40] Double --aggressive autopep8 --- muscle-tendon-complex/muscle-opendihu/helper.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/helper.py b/muscle-tendon-complex/muscle-opendihu/helper.py index ecc712d96..c01343ca1 100644 --- a/muscle-tendon-complex/muscle-opendihu/helper.py +++ b/muscle-tendon-complex/muscle-opendihu/helper.py @@ -72,9 +72,16 @@ # create the partitioning using the script in create_partitioned_meshes_for_settings.py result = create_partitioned_meshes_for_settings( - variables.n_subdomains_x, variables.n_subdomains_y, variables.n_subdomains_z, - variables.fiber_file, variables.load_fiber_data, - variables.sampling_stride_x, variables.sampling_stride_y, variables.sampling_stride_z, variables.generate_linear_3d_mesh, variables.generate_quadratic_3d_mesh) + variables.n_subdomains_x, + variables.n_subdomains_y, + variables.n_subdomains_z, + variables.fiber_file, + variables.load_fiber_data, + variables.sampling_stride_x, + variables.sampling_stride_y, + variables.sampling_stride_z, + variables.generate_linear_3d_mesh, + variables.generate_quadratic_3d_mesh) [variables.meshes, variables.own_subdomain_coordinate_x, variables.own_subdomain_coordinate_y, From bbfa73e3de4d48cb40152466b49f43f7a0b5320f Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:10:44 +0100 Subject: [PATCH 26/40] Minor fixes in muscle tutorial README --- muscle-tendon-complex/README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 83c58a932..35e8cbe5d 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -2,7 +2,7 @@ title: Muscle-tendon complex permalink: tutorials-muscle-tendon-complex.html keywords: multi-coupling, OpenDiHu, skeletal muscle -summary: In this case, an skeletal muscle (biceps) and three tendons are coupled together using a fully-implicit multi-coupling scheme. +summary: In this case, a skeletal muscle (biceps) and three tendons are coupled together using a fully-implicit multi-coupling scheme. --- {% note %} @@ -11,19 +11,19 @@ Get the [case files of this tutorial](https://github.com/precice/tutorials/tree/ ## Case Setup -In the following tutorial we model the contraction of a muscle, in particular, the biceps. The biceps is attached to the bones by three tendons (one at the bottom and two at the top). We enforce an activation in the muscle which results in its contraction. The tendons move as a result of the muscle contraction. In this case, a muscle and three tendons are coupled together using a fully-implicit multi-coupling scheme. The case setup is shown here: +In the following tutorial, we model the contraction of a muscle (the biceps). The biceps is attached to the bones by three tendons (one at the bottom and two at the top). We enforce an activation in the muscle which results in its contraction. The tendons move as a result of the muscle contraction. In this case, a muscle and three tendons are coupled together using a fully-implicit multi-coupling scheme. The case setup is shown in the following figure: ![Setup](images/tutorials-muscle-tendon-complex-setup.png) -The muscle participant (in red), is connected to three tendons. The muscle sends traction values to the tendons, which send displacement and velocity values back to the muscle. The end of each tendon which is not attached to the muscle is fixed by a dirichlet boundary condition (in reality, it would be fixed to the bones). +The muscle participant (in red) is connected to three tendons. The muscle sends traction values to the tendons, which send displacement and velocity values back to the muscle. The end of each tendon which is not attached to the muscle is fixed by a dirichlet boundary condition (in reality, it would be fixed to the bones). -The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not match perfectly. +The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not perfectly match. TODO: Explain how is the muscle activated! ## Why multi-coupling? -This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant to participant manner. However, such a composition is not suited for combining multiple strong fluid-structure interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. For another multi-coupling tutorial, you can refer to the [multiple perpendicular flaps tutorial](http://precice.org/tutorials-multiple-perpendicular-flaps.html). +This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant-to-participant manner. However, such a composition is not suited for combining multiple strong fluid-structure interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. For another multi-coupling tutorial, you can refer to the [multiple perpendicular flaps tutorial](http://precice.org/tutorials-multiple-perpendicular-flaps.html). We can set this in our `precice-config.xml`: @@ -33,18 +33,17 @@ We can set this in our `precice-config.xml`: - ``` The participant that has the control is the one that it is connected to all other participants. This is why we have chosen the muscle participant for this task. ## About the solvers -OpenDiHu is used for the muscle and each tendon participants. +We use solvers based on [OpenDiHu](https://github.com/opendihu/opendihu) for all participants. **The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. -- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers, i.e, how an electrical signal propagates from the center to the extremes of the muscle fibers. The electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. The solver solves the so called "monodomain equation" independently for each fiber. The equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e. the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellm`. +- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers, i.e, how an electrical signal propagates from the center to the extremes of the muscle fibers. The electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. The solver solves the so called "monodomain equation" independently for each fiber. The equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellm`. - The [MuscleContractionSolver](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the active paramter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) @@ -58,11 +57,13 @@ OpenDiHu is used for the muscle and each tendon participants. In the OpenDiHu website you can find detailed [installation instructions](https://opendihu.readthedocs.io/en/latest/user/installation.html). We recommend to download the code from the [GitHub repository](https://github.com/opendihu/opendihu) and to run `make release_without_tests` in the parent directory. - > **Note:** OpenDiHu automatically downloads dependencies and installs them in the `opendihu/dependencies/` folder. You can avoid that by setting e.g., `PRECICE_DOWNLOAD = False` in the [user-variables.scons.py](https://github.com/opendihu/opendihu/blob/develop/user-variables.scons.py) before building OpenDiHu. + {% note %} + OpenDiHu automatically downloads dependencies and installs them in the `opendihu/dependencies/` folder. You can avoid that by setting e.g., `PRECICE_DOWNLOAD = False` in the [user-variables.scons.py](https://github.com/opendihu/opendihu/blob/develop/user-variables.scons.py) before building OpenDiHu. + {% endnote %} - Download input files for OpenDiHu - OpenDiHu requires of input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading this files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [click here](https://zenodo.org/record/4705982/files/input.tgz?download=1) to download the necessary files. Please extract the files and place them on `opendihu/examples/electrophysiology/` directory. + OpenDiHu requires input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading these files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [directly download the necessary files](https://zenodo.org/record/4705982/files/input.tgz?download=1). Extract the files and place them in the `opendihu/examples/electrophysiology/` directory. - Setup `$OPENDIHU_HOME` to your `.bashrc` file @@ -79,7 +80,7 @@ OpenDiHu is used for the muscle and each tendon participants. 2. Starting the simulation: - We are going to run each solver in a different terminal. It is important that first we navigate to the simulation directory so that all solvers start in the same directory. + We are going to run each solver in a different terminal. It is important that first we navigate to the respective case directory, so that all solvers start from directories with same parent directory (see `exchange-directory` in the `precice-config.xml`). To start the `Muscle` participant, run: ```bash @@ -103,7 +104,7 @@ OpenDiHu is used for the muscle and each tendon participants. Finally, to start the `Tendon-Top-B` participant, run: - ```bash + ```bash cd tendon-top-B-opendihu ./run.sh ``` From 9660161d256e7950b977037fddba2d3bb67d0d5b Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:30:16 +0100 Subject: [PATCH 27/40] Add a clean_opendihu function, adjust cleaning scripts --- muscle-tendon-complex/muscle-opendihu/clean.sh | 8 +++++--- muscle-tendon-complex/tendon-bottom-opendihu/clean.sh | 8 +++++--- muscle-tendon-complex/tendon-top-A-opendihu/clean.sh | 8 +++++--- muscle-tendon-complex/tendon-top-B-opendihu/clean.sh | 8 +++++--- tools/cleaning-tools.sh | 10 ++++++++++ 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/clean.sh b/muscle-tendon-complex/muscle-opendihu/clean.sh index 467b38c90..56411170b 100755 --- a/muscle-tendon-complex/muscle-opendihu/clean.sh +++ b/muscle-tendon-complex/muscle-opendihu/clean.sh @@ -1,4 +1,6 @@ #!/bin/sh -rm -r precice-* -rm -r __pycache__ -rm -r lib logs out \ No newline at end of file +set -e -u + +. ../../tools/cleaning-tools.sh + +clean_opendihu . diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh index 467b38c90..56411170b 100755 --- a/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh +++ b/muscle-tendon-complex/tendon-bottom-opendihu/clean.sh @@ -1,4 +1,6 @@ #!/bin/sh -rm -r precice-* -rm -r __pycache__ -rm -r lib logs out \ No newline at end of file +set -e -u + +. ../../tools/cleaning-tools.sh + +clean_opendihu . diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh index 467b38c90..56411170b 100755 --- a/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh +++ b/muscle-tendon-complex/tendon-top-A-opendihu/clean.sh @@ -1,4 +1,6 @@ #!/bin/sh -rm -r precice-* -rm -r __pycache__ -rm -r lib logs out \ No newline at end of file +set -e -u + +. ../../tools/cleaning-tools.sh + +clean_opendihu . diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh index 467b38c90..56411170b 100755 --- a/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh +++ b/muscle-tendon-complex/tendon-top-B-opendihu/clean.sh @@ -1,4 +1,6 @@ #!/bin/sh -rm -r precice-* -rm -r __pycache__ -rm -r lib logs out \ No newline at end of file +set -e -u + +. ../../tools/cleaning-tools.sh + +clean_opendihu . diff --git a/tools/cleaning-tools.sh b/tools/cleaning-tools.sh index 6edaf3804..2f7c948ad 100755 --- a/tools/cleaning-tools.sh +++ b/tools/cleaning-tools.sh @@ -153,3 +153,13 @@ clean_dumux() { ) } + +clean_opendihu() { + ( + set -e -u + cd "$1" + echo "- Cleaning up OpenDiHu case in $(pwd)" + rm -rfv ./logs/ ./out/ + clean_precice_logs + ) +} From 13ccd83eca078fe364a65a0ca25d8738e289abd8 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:34:07 +0100 Subject: [PATCH 28/40] Rename opendihu-solver to solver-opendihu --- muscle-tendon-complex/README.md | 2 +- .../SConscript | 0 .../SConstruct | 0 .../build.sh | 0 .../clean.sh | 0 .../src/muscle-solver.cpp | 17 +++++++++-------- .../src/tendon-solver.cpp | 8 ++++---- 7 files changed, 14 insertions(+), 13 deletions(-) rename muscle-tendon-complex/{opendihu-solver => solver-opendihu}/SConscript (100%) rename muscle-tendon-complex/{opendihu-solver => solver-opendihu}/SConstruct (100%) rename muscle-tendon-complex/{opendihu-solver => solver-opendihu}/build.sh (100%) rename muscle-tendon-complex/{opendihu-solver => solver-opendihu}/clean.sh (100%) rename muscle-tendon-complex/{opendihu-solver => solver-opendihu}/src/muscle-solver.cpp (79%) rename muscle-tendon-complex/{opendihu-solver => solver-opendihu}/src/tendon-solver.cpp (76%) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 35e8cbe5d..42f71b878 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -74,7 +74,7 @@ We use solvers based on [OpenDiHu](https://github.com/opendihu/opendihu) for all - Compile muscle and tendon solvers ```bash - cd opendihu-solver + cd solver-opendihu ./build.sh ``` diff --git a/muscle-tendon-complex/opendihu-solver/SConscript b/muscle-tendon-complex/solver-opendihu/SConscript similarity index 100% rename from muscle-tendon-complex/opendihu-solver/SConscript rename to muscle-tendon-complex/solver-opendihu/SConscript diff --git a/muscle-tendon-complex/opendihu-solver/SConstruct b/muscle-tendon-complex/solver-opendihu/SConstruct similarity index 100% rename from muscle-tendon-complex/opendihu-solver/SConstruct rename to muscle-tendon-complex/solver-opendihu/SConstruct diff --git a/muscle-tendon-complex/opendihu-solver/build.sh b/muscle-tendon-complex/solver-opendihu/build.sh similarity index 100% rename from muscle-tendon-complex/opendihu-solver/build.sh rename to muscle-tendon-complex/solver-opendihu/build.sh diff --git a/muscle-tendon-complex/opendihu-solver/clean.sh b/muscle-tendon-complex/solver-opendihu/clean.sh similarity index 100% rename from muscle-tendon-complex/opendihu-solver/clean.sh rename to muscle-tendon-complex/solver-opendihu/clean.sh diff --git a/muscle-tendon-complex/opendihu-solver/src/muscle-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp similarity index 79% rename from muscle-tendon-complex/opendihu-solver/src/muscle-solver.cpp rename to muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp index 0e6bdf82b..7b8778e0d 100644 --- a/muscle-tendon-complex/opendihu-solver/src/muscle-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp @@ -1,29 +1,30 @@ #include -#include #include +#include #include #include "easylogging++.h" #include "opendihu.h" -int main(int argc, char *argv[]) { - +int main(int argc, char *argv[]) +{ + DihuContext settings(argc, argv); Control::PreciceAdapter, BasisFunction::LagrangeOfOrder<1>>>>>, Control::MultipleInstances< - TimeSteppingScheme::CrankNicolson< + TimeSteppingScheme::CrankNicolson< SpatialDiscretization::FiniteElementMethod< Mesh::StructuredDeformableOfDimension<1>, BasisFunction::LagrangeOfOrder<1>, diff --git a/muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp similarity index 76% rename from muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp rename to muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp index 72af7afd8..a2dfcf81d 100644 --- a/muscle-tendon-complex/opendihu-solver/src/tendon-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp @@ -1,19 +1,19 @@ #include -#include #include +#include #include #include "easylogging++.h" #include "opendihu.h" -int main(int argc, char *argv[]) { +int main(int argc, char *argv[]) +{ DihuContext settings(argc, argv); Control::PreciceAdapter> + Equation::SolidMechanics::HyperelasticTendon>> problem(settings); problem.run(); From d9141f5adf7d1f89fabc4346bc9fd5d467e8a6a9 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:36:50 +0100 Subject: [PATCH 29/40] solver-opendihu scripts: set -e -u - Exit on errors - Complain for undefined variables --- muscle-tendon-complex/solver-opendihu/build.sh | 1 + muscle-tendon-complex/solver-opendihu/clean.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/muscle-tendon-complex/solver-opendihu/build.sh b/muscle-tendon-complex/solver-opendihu/build.sh index d39dddc11..6eaf7927e 100755 --- a/muscle-tendon-complex/solver-opendihu/build.sh +++ b/muscle-tendon-complex/solver-opendihu/build.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e -u if [ -n "$OPENDIHU_HOME" ] then diff --git a/muscle-tendon-complex/solver-opendihu/clean.sh b/muscle-tendon-complex/solver-opendihu/clean.sh index 619121b2f..d2740911f 100755 --- a/muscle-tendon-complex/solver-opendihu/clean.sh +++ b/muscle-tendon-complex/solver-opendihu/clean.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e -u rm -r .* ./*.log rm -r build_release From 9ec1cdbe0c1c0148bc8422ab0a00a529a5a79ea5 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:41:58 +0100 Subject: [PATCH 30/40] Fix clean_opendihu call to clean_precice_logs --- tools/cleaning-tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cleaning-tools.sh b/tools/cleaning-tools.sh index 2f7c948ad..80db82242 100755 --- a/tools/cleaning-tools.sh +++ b/tools/cleaning-tools.sh @@ -160,6 +160,6 @@ clean_opendihu() { cd "$1" echo "- Cleaning up OpenDiHu case in $(pwd)" rm -rfv ./logs/ ./out/ - clean_precice_logs + clean_precice_logs . ) } From b3602f0df7f71a0f6fe3ac9e7b0a60ab018f4338 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:44:35 +0100 Subject: [PATCH 31/40] Further fixes in solver-opendihu/clean.sh --- muscle-tendon-complex/solver-opendihu/clean.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/muscle-tendon-complex/solver-opendihu/clean.sh b/muscle-tendon-complex/solver-opendihu/clean.sh index d2740911f..ebfabac17 100755 --- a/muscle-tendon-complex/solver-opendihu/clean.sh +++ b/muscle-tendon-complex/solver-opendihu/clean.sh @@ -1,12 +1,12 @@ #!/bin/bash set -e -u -rm -r .* ./*.log -rm -r build_release -rm -r precice-profiling +rm -rf ./*.log +rm -rf build_release +rm -rf precice-profiling # remove executables from partipant folders -rm ../muscle-opendihu/muscle-solver -rm ../tendon-bottom-opendihu/tendon-solver -rm ../tendon-top-A-opendihu/tendon-solver -rm ../tendon-top-B-opendihu/tendon-solver +rm -f ../muscle-opendihu/muscle-solver +rm -f ../tendon-bottom-opendihu/tendon-solver +rm -f ../tendon-top-A-opendihu/tendon-solver +rm -f ../tendon-top-B-opendihu/tendon-solver From e908415518229b1d27ba34b15911814d2afbcfc1 Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 11:48:40 +0100 Subject: [PATCH 32/40] Apply set -e -u to more shell scripts --- muscle-tendon-complex/muscle-opendihu/run.sh | 4 +++- muscle-tendon-complex/tendon-bottom-opendihu/run.sh | 4 +++- muscle-tendon-complex/tendon-top-A-opendihu/run.sh | 4 +++- muscle-tendon-complex/tendon-top-B-opendihu/run.sh | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/muscle-tendon-complex/muscle-opendihu/run.sh b/muscle-tendon-complex/muscle-opendihu/run.sh index 3248c0a62..d09eff9c4 100755 --- a/muscle-tendon-complex/muscle-opendihu/run.sh +++ b/muscle-tendon-complex/muscle-opendihu/run.sh @@ -1,2 +1,4 @@ #!/bin/sh -./muscle-solver settings-muscle.py variables.py \ No newline at end of file +set -e -u + +./muscle-solver settings-muscle.py variables.py diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/run.sh b/muscle-tendon-complex/tendon-bottom-opendihu/run.sh index d5c7a5968..4de786bf9 100755 --- a/muscle-tendon-complex/tendon-bottom-opendihu/run.sh +++ b/muscle-tendon-complex/tendon-bottom-opendihu/run.sh @@ -1,2 +1,4 @@ #!/bin/sh -./tendon-solver settings-tendon-bottom.py \ No newline at end of file +set -e -u + +./tendon-solver settings-tendon-bottom.py diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/run.sh b/muscle-tendon-complex/tendon-top-A-opendihu/run.sh index 14e873864..c7b70e79a 100755 --- a/muscle-tendon-complex/tendon-top-A-opendihu/run.sh +++ b/muscle-tendon-complex/tendon-top-A-opendihu/run.sh @@ -1,2 +1,4 @@ #!/bin/sh -./tendon-solver settings-tendon-top-A.py \ No newline at end of file +set -e -u + +./tendon-solver settings-tendon-top-A.py diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/run.sh b/muscle-tendon-complex/tendon-top-B-opendihu/run.sh index 117e658e5..ad204d701 100755 --- a/muscle-tendon-complex/tendon-top-B-opendihu/run.sh +++ b/muscle-tendon-complex/tendon-top-B-opendihu/run.sh @@ -1,2 +1,4 @@ #!/bin/sh -./tendon-solver settings-tendon-top-B.py \ No newline at end of file +set -e -u + +./tendon-solver settings-tendon-top-B.py From b7e9272123c9a3eb17409cd036b672249c4ae8aa Mon Sep 17 00:00:00 2001 From: Gerasimos Chourdakis Date: Mon, 11 Mar 2024 12:01:22 +0100 Subject: [PATCH 33/40] Format precice-config.xml --- muscle-tendon-complex/precice-config.xml | 266 +++++++++++------------ 1 file changed, 123 insertions(+), 143 deletions(-) diff --git a/muscle-tendon-complex/precice-config.xml b/muscle-tendon-complex/precice-config.xml index cb7d07af3..2c641bbb6 100644 --- a/muscle-tendon-complex/precice-config.xml +++ b/muscle-tendon-complex/precice-config.xml @@ -1,164 +1,148 @@ - - + - + - - - - + + + + - - - + + + - + - - - + + + - + - - - + + + - - - + + + - + - - - + + + - - - + + + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + - + - - - - - - - - - - + + + + + + - + - - - - - - - - - - + + + + + + - + - - - - + + + + - - - - - - - + + + + + + - @@ -168,34 +152,30 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + - \ No newline at end of file + From 2e6dc39e90174bb3551fa2ef1592c064e715fcd8 Mon Sep 17 00:00:00 2001 From: carme-hp <71499004+carme-hp@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:46:56 +0100 Subject: [PATCH 34/40] Update muscle-tendon-complex/README.md Co-authored-by: Gerasimos Chourdakis --- muscle-tendon-complex/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 42f71b878..3e8320bf8 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -23,7 +23,7 @@ TODO: Explain how is the muscle activated! ## Why multi-coupling? -This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant-to-participant manner. However, such a composition is not suited for combining multiple strong fluid-structure interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. For another multi-coupling tutorial, you can refer to the [multiple perpendicular flaps tutorial](http://precice.org/tutorials-multiple-perpendicular-flaps.html). +This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant-to-participant manner, limited to primarily explicit coupling schemes. However, such a composition is not suited for combining multiple strong interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. For another multi-coupling tutorial, you can refer to the [multiple perpendicular flaps tutorial](http://precice.org/tutorials-multiple-perpendicular-flaps.html). We can set this in our `precice-config.xml`: From 09d53d65ee0844d6475110d50f4a0eb86038eb98 Mon Sep 17 00:00:00 2001 From: carme-hp <71499004+carme-hp@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:52:26 +0100 Subject: [PATCH 35/40] Update muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp Co-authored-by: Gerasimos Chourdakis --- .../solver-opendihu/src/tendon-solver.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp index a2dfcf81d..c0c7589f4 100644 --- a/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp @@ -1,11 +1,8 @@ #include #include #include - -#include -#include "easylogging++.h" - -#include "opendihu.h" +#include +#include int main(int argc, char *argv[]) { From 37233da056c9983fb70bf8b77a9704cf3e5f0242 Mon Sep 17 00:00:00 2001 From: carme-hp <71499004+carme-hp@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:53:05 +0100 Subject: [PATCH 36/40] Update muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp Co-authored-by: Gerasimos Chourdakis --- .../solver-opendihu/src/muscle-solver.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp index 7b8778e0d..bedfa82d6 100644 --- a/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp @@ -1,11 +1,8 @@ #include #include #include - -#include -#include "easylogging++.h" - -#include "opendihu.h" +#include +#include int main(int argc, char *argv[]) { From 76b5a0fe6e150ba616e3140ee56c06d510b31c73 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 11 Mar 2024 13:54:25 +0100 Subject: [PATCH 37/40] Explain activation and add citation --- muscle-tendon-complex/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 3e8320bf8..41832d89b 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -19,8 +19,6 @@ The muscle participant (in red) is connected to three tendons. The muscle sends The muscle and tendon meshes are obtained from patient imaging. The interfaces of the tendons and the muscle do not perfectly match, which is a quite common issue due to the limitations of imaging methods and postprocessing tools. Nonetheless, preCICE coupling methods are robust and can handle meshes that do not perfectly match. -TODO: Explain how is the muscle activated! - ## Why multi-coupling? This is a case with four participants: the muscle and each tendon. In preCICE, there are two options to [couple more than two participants](https://www.precice.org/configuration-coupling-multi.html). The first option is a composition of bi-coupling schemes, in which we must specify the exchange of data in a participant-to-participant manner, limited to primarily explicit coupling schemes. However, such a composition is not suited for combining multiple strong interactions [1]. Thus, in this case, we use the second option, fully-implicit multi-coupling. For another multi-coupling tutorial, you can refer to the [multiple perpendicular flaps tutorial](http://precice.org/tutorials-multiple-perpendicular-flaps.html). @@ -43,9 +41,10 @@ We use solvers based on [OpenDiHu](https://github.com/opendihu/opendihu) for all **The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. -- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers, i.e, how an electrical signal propagates from the center to the extremes of the muscle fibers. The electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. The solver solves the so called "monodomain equation" independently for each fiber. The equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellm`. +- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers. Motor neurons fire electrical signals that propagate from the neuromuscular junctions. i.e. the center of the muscle fibers, to the extremes of the muscle fibers. The propagation of the electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. +The solver solves the so called "monodomain equation" independently for each fiber [2]. The modonomain equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml`. The firing of the motor neurons is modelled by the `opendihu/examples/electrophysiology/input/MU_firing_times_always.txt` file. -- The [MuscleContractionSolver](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the active paramter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) +- The [MuscleContractionSolver](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the activation parameter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) **The tendon solver** is a dynamic FEM mechanical solver. It models an hyperelastic passive material. The material parameters are chosen as in [Carniel et al.](https://pubmed.ncbi.nlm.nih.gov/28238424/) @@ -118,6 +117,8 @@ After the simulation has finished, you can visualize your results using e.g. Par [1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. *Comput Mech* **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 +[2] T. Heidlauf, O. Röhrle. A multiscale chemo-electro-mechanical skeletal muscle model to analyze muscle contraction and force generation for different muscle fiber arrangements. *Front. Physiology* **5** (2014). https://doi.org/10.3389/fphys.2014.00498 + {% disclaimer %} This offering is not approved or endorsed by OpenCFD Limited, producer and distributor of the OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and OpenCFD® trade marks. {% enddisclaimer %} From db6820efa61855f55a339ac534c66e6053098862 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 11 Mar 2024 13:59:56 +0100 Subject: [PATCH 38/40] Use pre-commit --- muscle-tendon-complex/README.md | 6 +++--- muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp | 2 +- muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 41832d89b..23d779f83 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -41,8 +41,8 @@ We use solvers based on [OpenDiHu](https://github.com/opendihu/opendihu) for all **The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. -- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers. Motor neurons fire electrical signals that propagate from the neuromuscular junctions. i.e. the center of the muscle fibers, to the extremes of the muscle fibers. The propagation of the electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. -The solver solves the so called "monodomain equation" independently for each fiber [2]. The modonomain equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml`. The firing of the motor neurons is modelled by the `opendihu/examples/electrophysiology/input/MU_firing_times_always.txt` file. +- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers. Motor neurons fire electrical signals that propagate from the neuromuscular junctions. i.e. the center of the muscle fibers, to the extremes of the muscle fibers. The propagation of the electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. +The solver solves the so called "monodomain equation" independently for each fiber [2]. The modonomain equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml`. The firing of the motor neurons is modelled by the `opendihu/examples/electrophysiology/input/MU_firing_times_always.txt` file. - The [MuscleContractionSolver](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the activation parameter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) @@ -117,7 +117,7 @@ After the simulation has finished, you can visualize your results using e.g. Par [1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. *Comput Mech* **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 -[2] T. Heidlauf, O. Röhrle. A multiscale chemo-electro-mechanical skeletal muscle model to analyze muscle contraction and force generation for different muscle fiber arrangements. *Front. Physiology* **5** (2014). https://doi.org/10.3389/fphys.2014.00498 +[2] T. Heidlauf, O. Röhrle. A multiscale chemo-electro-mechanical skeletal muscle model to analyze muscle contraction and force generation for different muscle fiber arrangements. *Front. Physiology* **5** (2014). https://doi.org/10.3389/fphys.2014.00498 {% disclaimer %} This offering is not approved or endorsed by OpenCFD Limited, producer and distributor of the OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and OpenCFD® trade marks. diff --git a/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp index bedfa82d6..27eca51f7 100644 --- a/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include int main(int argc, char *argv[]) diff --git a/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp index c0c7589f4..8a729edfa 100644 --- a/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include int main(int argc, char *argv[]) From 535342f44a94d3dd9e5c7b550ae75daa910cdd0b Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 11 Mar 2024 13:59:56 +0100 Subject: [PATCH 39/40] Use pre-commit --- muscle-tendon-complex/README.md | 11 ++++++----- .../muscle-opendihu/variables/variables.py | 13 +++++++------ .../solver-opendihu/src/muscle-solver.cpp | 2 +- .../solver-opendihu/src/tendon-solver.cpp | 2 +- .../tendon-bottom-opendihu/variables/variables.py | 6 +++--- .../tendon-top-A-opendihu/variables/variables.py | 6 +++--- .../tendon-top-B-opendihu/variables/variables.py | 6 +++--- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/muscle-tendon-complex/README.md b/muscle-tendon-complex/README.md index 41832d89b..685027e15 100644 --- a/muscle-tendon-complex/README.md +++ b/muscle-tendon-complex/README.md @@ -41,8 +41,8 @@ We use solvers based on [OpenDiHu](https://github.com/opendihu/opendihu) for all **The muscle solver** consists of a multi-physcis multi-scale solver itself. It combines two OpenDiHu solvers in one: the *FastMonodomainSolver* and the *MuscleContractionSolver*. The two solvers are coupled using the OpenDiHu coupling tool for weak coupling. -- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers. Motor neurons fire electrical signals that propagate from the neuromuscular junctions. i.e. the center of the muscle fibers, to the extremes of the muscle fibers. The propagation of the electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. -The solver solves the so called "monodomain equation" independently for each fiber [2]. The modonomain equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml`. The firing of the motor neurons is modelled by the `opendihu/examples/electrophysiology/input/MU_firing_times_always.txt` file. +- The [FastMonodomainSolver](https://opendihu.readthedocs.io/en/latest/settings/fast_monodomain_solver.html) models the electrochemical processes that take place in the muscle fibers. Motor neurons fire electrical signals that propagate from the neuromuscular junctions. i.e. the center of the muscle fibers, to the extremes of the muscle fibers. The propagation of the electrical signal triggers chemical reactions which lead to the contraction of sarcomeres, the smallest contraction unit in the muscle. +The solver solves the so called "monodomain equation" independently for each fiber [2]. The modonomain equation has a reaction term (small time scale) and a diffusion term (large time scale) and is solved using Strang splitting. The sarcomeres, i.e., the reaction term, are modelled using a variant of the Shorten model, specified by the CellML file `opendihu/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml`. The firing of the motor neurons is modelled by the `opendihu/examples/electrophysiology/input/MU_firing_times_always.txt` file. - The [MuscleContractionSolver](https://opendihu.readthedocs.io/en/latest/settings/muscle_contraction_solver.html) models the mechanics of the muscle. It consists of a dynamic FEM solver that models an hyperelastic active material. The active component is calculated from the activation parameter $\gamma$, which ranges from 0 (no activation) to 1 (maximum activation) and is calculated in the *FastMonodomainSolver*. The material parameters are chosen as in [Heidlauf et al.](https://link.springer.com/article/10.1007/s10237-016-0772-7) @@ -62,12 +62,13 @@ The solver solves the so called "monodomain equation" independently for each fib - Download input files for OpenDiHu - OpenDiHu requires input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading these files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [directly download the necessary files](https://zenodo.org/record/4705982/files/input.tgz?download=1). Extract the files and place them in the `opendihu/examples/electrophysiology/` directory. + OpenDiHu requires input files hosted in [Zenodo](https://zenodo.org/records/4705982) which include CellML files (containing model equations) and mesh files. Downloading these files is necessary to simulate muscles and/or tendons with OpenDiHu. You can [directly download the necessary files](https://zenodo.org/record/4705982/files/input.tgz?download=1). - - Setup `$OPENDIHU_HOME` to your `.bashrc` file + - Setup `$OPENDIHU_HOME` and `$OPENDIHU_INPUT_DIR` in your `.bashrc` file ```bash export OPENDIHU_HOME=/path/to/opendihu + export OPENDIHU_INPUT_DIR=/path/to/input ``` - Compile muscle and tendon solvers @@ -117,7 +118,7 @@ After the simulation has finished, you can visualize your results using e.g. Par [1] H. Bungartz, F. Linder, M. Mehl, B. Uekermann. A plug-and-play coupling approach for parallel multi-field simulations. *Comput Mech* **55**, 1119-1129 (2015). https://doi.org/10.1007/s00466-014-1113-2 -[2] T. Heidlauf, O. Röhrle. A multiscale chemo-electro-mechanical skeletal muscle model to analyze muscle contraction and force generation for different muscle fiber arrangements. *Front. Physiology* **5** (2014). https://doi.org/10.3389/fphys.2014.00498 +[2] T. Heidlauf, O. Röhrle. A multiscale chemo-electro-mechanical skeletal muscle model to analyze muscle contraction and force generation for different muscle fiber arrangements. *Front. Physiology* **5** (2014). https://doi.org/10.3389/fphys.2014.00498 {% disclaimer %} This offering is not approved or endorsed by OpenCFD Limited, producer and distributor of the OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and OpenCFD® trade marks. diff --git a/muscle-tendon-complex/muscle-opendihu/variables/variables.py b/muscle-tendon-complex/muscle-opendihu/variables/variables.py index 105d0d9c7..b75aebcf6 100644 --- a/muscle-tendon-complex/muscle-opendihu/variables/variables.py +++ b/muscle-tendon-complex/muscle-opendihu/variables/variables.py @@ -13,6 +13,7 @@ # not used [cm], this will later be used to specify a variance of positions of the innervation point at the fibers innervation_zone_width = 0. rho = 10 + # solvers # ------- diffusion_solver_type = "cg" # solver and preconditioner for the diffusion part of the Monodomain equation @@ -45,14 +46,14 @@ # ----------- import os -opendihu_home = os.environ.get('OPENDIHU_HOME') -fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_9x9fibers.bin" +input_dir = os.environ.get('OPENDIHU_INPUT_DIR') +fiber_file = input_dir + "/left_biceps_brachii_9x9fibers.bin" fat_mesh_file = fiber_file + "_fat.bin" # use setSpecificStatesCallEnableBegin and setSpecificStatesCallFrequency -firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_always.txt" -fiber_distribution_file = opendihu_home + "/examples/electrophysiology/input/MU_fibre_distribution_10MUs.txt" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" -firing_times_file = opendihu_home + "/examples/electrophysiology/input/MU_firing_times_real.txt" +firing_times_file = input_dir + "/MU_firing_times_always.txt" +fiber_distribution_file = input_dir + "/MU_fibre_distribution_10MUs.txt" +cellml_file = input_dir + "/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +firing_times_file = input_dir + "/MU_firing_times_real.txt" precice_config_file = "../precice-config.xml" # If the fiber geometry data should be loaded completely in the python # script. If True, this reads the binary file and assigns the node diff --git a/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp index bedfa82d6..27eca51f7 100644 --- a/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/muscle-solver.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include int main(int argc, char *argv[]) diff --git a/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp index c0c7589f4..8a729edfa 100644 --- a/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp +++ b/muscle-tendon-complex/solver-opendihu/src/tendon-solver.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include int main(int argc, char *argv[]) diff --git a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py index 34a7672ab..91eb72a1e 100644 --- a/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-bottom-opendihu/variables/variables.py @@ -16,9 +16,9 @@ # ----------- import os -opendihu_home = os.environ.get('OPENDIHU_HOME') -fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon1.bin" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +input_dir = os.environ.get('OPENDIHU_INPUT_DIR') +fiber_file = input_dir + "/left_biceps_brachii_tendon1.bin" +cellml_file = input_dir + "/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" precice_config_file = "../precice-config.xml" # If the fiber geometry data should be loaded completely in the python # script. If True, this reads the binary file and assigns the node diff --git a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py index b53686c0f..433abb485 100644 --- a/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-top-A-opendihu/variables/variables.py @@ -16,9 +16,9 @@ # ----------- import os -opendihu_home = os.environ.get('OPENDIHU_HOME') -fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon2a.bin" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +input_dir = os.environ.get('OPENDIHU_INPUT_DIR') +fiber_file = input_dir + "/left_biceps_brachii_tendon2a.bin" +cellml_file = input_dir + "/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" precice_config_file = "../precice-config.xml" # If the fiber geometry data should be loaded completely in the python # script. If True, this reads the binary file and assigns the node diff --git a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py index eb7be320f..eca352d9d 100644 --- a/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py +++ b/muscle-tendon-complex/tendon-top-B-opendihu/variables/variables.py @@ -16,9 +16,9 @@ # ----------- import os -opendihu_home = os.environ.get('OPENDIHU_HOME') -fiber_file = opendihu_home + "/examples/electrophysiology/input/left_biceps_brachii_tendon2b.bin" -cellml_file = opendihu_home + "/examples/electrophysiology/input/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" +input_dir = os.environ.get('OPENDIHU_INPUT_DIR') +fiber_file = input_dir + "/left_biceps_brachii_tendon2b.bin" +cellml_file = input_dir + "/2020_06_03_hodgkin-huxley_shorten_ocallaghan_davidson_soboleva_2007.cellml" precice_config_file = "../precice-config.xml" # If the fiber geometry data should be loaded completely in the python # script. If True, this reads the binary file and assigns the node From 755c3e483eb691228afacf153713a84cc6a67e06 Mon Sep 17 00:00:00 2001 From: carme-hp Date: Mon, 11 Mar 2024 18:22:18 +0100 Subject: [PATCH 40/40] Add automatically generated folder to gitignore --- muscle-tendon-complex/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/muscle-tendon-complex/.gitignore b/muscle-tendon-complex/.gitignore index e8ff11487..03904b82a 100644 --- a/muscle-tendon-complex/.gitignore +++ b/muscle-tendon-complex/.gitignore @@ -10,3 +10,4 @@ **/tendon-solver **/.sconf_temp **/.scons* +muscle-opendihu/src