├── drlfoam ├── tests │ ├── __init__.py │ └── test_utils.py ├── agent │ ├── tests │ │ ├── __init__.py │ │ ├── test_ppo_agent.py │ │ └── test_agent.py │ ├── __init__.py │ ├── agent.py │ └── ppo_agent.py ├── environment │ ├── tests │ │ ├── __init__.py │ │ ├── test_environment.py │ │ ├── test_rotating_cylinder.py │ │ └── test_rotating_pinball.py │ ├── __init__.py │ ├── environment.py │ ├── rotating_cylinder.py │ └── rotating_pinball.py ├── execution │ ├── tests │ │ ├── __init__.py │ │ ├── test_local.py │ │ ├── test_manager.py │ │ └── test_slurm.py │ ├── __init__.py │ ├── local.py │ ├── manager.py │ ├── buffer.py │ ├── slurm.py │ └── setup.py ├── __init__.py ├── version.py ├── constants.py └── utils.py ├── requirements.txt ├── openfoam ├── src │ ├── agentRotatingWallVelocity │ │ ├── Make │ │ │ ├── files │ │ │ └── options │ │ ├── agentRotatingWallVelocityFvPatchVectorField.H │ │ └── agentRotatingWallVelocityFvPatchVectorField.C │ └── pinballRotatingWallVelocity │ │ ├── Make │ │ ├── files │ │ └── options │ │ └── pinballRotatingWallVelocityFvPatchVectorField.H └── test_cases │ ├── rotatingPinball2D │ ├── policy.pt │ ├── Allclean │ ├── Allrun │ ├── Allrun.pre │ ├── constant │ │ ├── turbulenceProperties │ │ └── transportProperties │ ├── system │ │ ├── decomposeParDict │ │ ├── setExprBoundaryFieldsDict │ │ ├── fvSchemes │ │ ├── fvSolution │ │ ├── topoSetDict │ │ ├── controlDict │ │ └── blockMeshDict │ └── 0.org │ │ ├── p │ │ └── U │ └── rotatingCylinder2D │ ├── policy.pt │ ├── Allclean │ ├── Allrun │ ├── Allrun.pre │ ├── constant │ ├── turbulenceProperties │ └── transportProperties │ ├── system │ ├── decomposeParDict │ ├── setExprBoundaryFieldsDict │ ├── fvSchemes │ ├── fvSolution │ ├── controlDict │ └── blockMeshDict │ └── 0.org │ ├── p │ └── U ├── create_test_data ├── Allwmake ├── Allwclean ├── examples ├── jobscript ├── create_dummy_policy.py ├── config_orig.yml └── run_training.py ├── references.md ├── setup-env ├── .gitignore └── README.md /drlfoam/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /drlfoam/agent/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /drlfoam/environment/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /drlfoam/execution/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /drlfoam/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import * -------------------------------------------------------------------------------- /drlfoam/version.py: -------------------------------------------------------------------------------- 1 | """drlfoam package version.""" 2 | 3 | __version__ = "0.1" -------------------------------------------------------------------------------- /drlfoam/agent/__init__.py: -------------------------------------------------------------------------------- 1 | from .agent import FCPolicy, FCValue 2 | from .ppo_agent import PPOAgent 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | --find-links https://download.pytorch.org/whl/torch_stable.html 2 | torch==1.13.1 3 | pandas 4 | numpy 5 | PyYAML -------------------------------------------------------------------------------- /drlfoam/execution/__init__.py: -------------------------------------------------------------------------------- 1 | from .buffer import Buffer 2 | from .local import LocalBuffer 3 | from .slurm import SlurmConfig, SlurmBuffer -------------------------------------------------------------------------------- /openfoam/src/agentRotatingWallVelocity/Make/files: -------------------------------------------------------------------------------- 1 | agentRotatingWallVelocityFvPatchVectorField.C 2 | LIB = $(DRL_LIBBIN)/libAgentRotatingWallVelocity -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/policy.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OFDataCommittee/drlfoam/HEAD/openfoam/test_cases/rotatingPinball2D/policy.pt -------------------------------------------------------------------------------- /openfoam/src/pinballRotatingWallVelocity/Make/files: -------------------------------------------------------------------------------- 1 | pinballRotatingWallVelocityFvPatchVectorField.C 2 | LIB = $(DRL_LIBBIN)/libPinballRotatingWallVelocity 3 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/policy.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OFDataCommittee/drlfoam/HEAD/openfoam/test_cases/rotatingCylinder2D/policy.pt -------------------------------------------------------------------------------- /drlfoam/environment/__init__.py: -------------------------------------------------------------------------------- 1 | from .environment import Environment 2 | from .rotating_cylinder import RotatingCylinder2D 3 | from .rotating_pinball import RotatingPinball2D -------------------------------------------------------------------------------- /drlfoam/environment/tests/test_environment.py: -------------------------------------------------------------------------------- 1 | 2 | from pytest import raises 3 | from ..environment import Environment 4 | 5 | 6 | def test_environment(): 7 | with raises(TypeError): 8 | env = Environment("", "", "", "", "", 1, 1, 1) 9 | 10 | -------------------------------------------------------------------------------- /create_test_data: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # create test data for unittests 4 | test_case_path="openfoam/test_cases" 5 | test_data_path="test_data" 6 | env_name="rotatingCylinder2D" 7 | 8 | mkdir -p $test_data_path 9 | cp -r "${test_case_path}/${env_name}" "${test_data_path}/" 10 | cd "${test_data_path}/${env_name}" && ./Allrun.pre -------------------------------------------------------------------------------- /Allwmake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cmd="wmake all openfoam/src" 4 | 5 | if [[ $* == *--container* ]] 6 | then 7 | # compile with container 8 | source setup-env --container 9 | singularity exec $DRL_IMAGE bash -c "source $DRL_BASHRC && $cmd" 10 | else 11 | # compile without container 12 | source setup-env && $cmd 13 | fi 14 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/Allclean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "${0%/*}" || exit 3 | . "${DRL_BASE:?}"/openfoam/RunFunctions 4 | #------------------------------------------------------------------------------ 5 | 6 | cleanCase 7 | rm -rf 0 trajectory.csv 8 | 9 | #------------------------------------------------------------------------------ 10 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/Allrun: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "${0%/*}" || exit 3 | . "${DRL_BASE:?}"/openfoam/RunFunctions 4 | #------------------------------------------------------------------------------ 5 | 6 | # run case 7 | runParallel pimpleFoam 8 | 9 | #------------------------------------------------------------------------------ 10 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/Allclean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "${0%/*}" || exit 3 | . "${DRL_BASE:?}"/openfoam/RunFunctions 4 | #------------------------------------------------------------------------------ 5 | 6 | cleanCase 7 | rm -rf 0 trajectory.csv 8 | 9 | #------------------------------------------------------------------------------ 10 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/Allrun: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "${0%/*}" || exit 3 | . "${DRL_BASE:?}"/openfoam/RunFunctions 4 | #------------------------------------------------------------------------------ 5 | 6 | # run case 7 | runParallel pimpleFoam 8 | 9 | #------------------------------------------------------------------------------ 10 | -------------------------------------------------------------------------------- /drlfoam/constants.py: -------------------------------------------------------------------------------- 1 | 2 | from os import environ 3 | from os.path import join 4 | from torch import DoubleTensor 5 | 6 | 7 | DEFAULT_TENSOR_TYPE = DoubleTensor 8 | EPS_SP = 1.0e-6 9 | EPS_DP = 1.0e-14 10 | BASE_PATH = environ.get("DRL_BASE", "") 11 | TESTCASE_PATH = join(BASE_PATH, "openfoam", "test_cases") 12 | TESTDATA_PATH = join(BASE_PATH, "test_data") -------------------------------------------------------------------------------- /Allwclean: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cmd="wclean all openfoam/src" 4 | 5 | if [[ $* == *--container* ]] 6 | then 7 | # compile with container 8 | source setup-env --container 9 | singularity exec $DRL_IMAGE bash -c "source $DRL_BASHRC && $cmd" 10 | else 11 | # compile without container 12 | source setup-env && $cmd 13 | fi 14 | 15 | rm -vf ${DRL_LIBBIN}/* -------------------------------------------------------------------------------- /examples/jobscript: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #SBATCH --job-name=lr2_pinball_main 3 | #SBATCH --ntasks=6 4 | #SBATCH --output=%x_%j.out 5 | #SBATCH --partition=queue-1 6 | #SBATCH --constraint=c5a.24xlarge 7 | 8 | source /fsx/OpenFOAM/OpenFOAM-v2206/etc/bashrc 9 | source /fsx/drlfoam_main/setup-env 10 | source /fsx/drlfoam_main/pydrl/bin/activate 11 | 12 | python3 run_training.py -o test_pinball_lr2 -i 100 -r 10 -b 10 -f 300 -s rotatingPinball2D -e slurm &> log.pinball_main_lr2 13 | -------------------------------------------------------------------------------- /references.md: -------------------------------------------------------------------------------- 1 | ## Student projects 2 | 3 | - J. Geise, “Learning of optimized multigrid solver settings for CFD applications”, Zenodo, 2023. doi: 10.5281/zenodo.10361535 4 | - Janis Geise, “Robust model-based deep reinforcement learning for flow control”, Zenodo, 2023. doi: 10.5281/zenodo.7642927 5 | - Tom Krogmann, “Optimal sensor placement for active flow control with deep reinforcement learning”, Zenodo, 2023. doi: 10.5281/zenodo.7636959 6 | - E. Schulze, “Model-based Reinforcement Learning for Accelerated Learning From CFD Simulations”, Zenodo, 2022. doi: 10.5281/zenodo.6375575 -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/Allrun.pre: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "${0%/*}" || exit 3 | . "${DRL_BASE:?}"/openfoam/RunFunctions 4 | #------------------------------------------------------------------------------ 5 | 6 | # create mesh 7 | runApplication blockMesh 8 | runApplication topoSet 9 | 10 | # decompose and run case 11 | cp -r 0.org 0 12 | runApplication setExprBoundaryFields 13 | runApplication decomposePar 14 | runParallel renumberMesh -overwrite 15 | runParallel -s "pre" pimpleFoam 16 | 17 | #------------------------------------------------------------------------------ 18 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/Allrun.pre: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "${0%/*}" || exit 3 | . "${DRL_BASE:?}"/openfoam/RunFunctions 4 | #------------------------------------------------------------------------------ 5 | 6 | # create mesh 7 | runApplication blockMesh 8 | 9 | # set inlet velocity 10 | cp -r 0.org 0 11 | runApplication setExprBoundaryFields 12 | 13 | # decompose and run case 14 | runApplication decomposePar 15 | runParallel renumberMesh -overwrite 16 | runParallel -s "pre" pimpleFoam 17 | 18 | #------------------------------------------------------------------------------ 19 | -------------------------------------------------------------------------------- /openfoam/src/agentRotatingWallVelocity/Make/options: -------------------------------------------------------------------------------- 1 | EXE_INC = \ 2 | -std=c++17 \ 3 | -Wno-deprecated-declarations -Wno-old-style-cast -Wno-redundant-move \ 4 | -I$(LIB_SRC)/finiteVolume/lnInclude \ 5 | -I$(LIB_SRC)/sampling/lnInclude \ 6 | -I$(LIB_SRC)/OpenFOAM/lnInclude \ 7 | -I$(DRL_TORCH)/include \ 8 | -I$(DRL_TORCH)/include/torch/csrc/api/include 9 | 10 | LIB_LIBS = \ 11 | -lfiniteVolume \ 12 | -rdynamic \ 13 | -Wl,-rpath,$(DRL_TORCH)/lib $(DRL_TORCH)/lib/libtorch.so $(DRL_TORCH)/lib/libc10.so \ 14 | -Wl,--no-as-needed,$(DRL_TORCH)/lib/libtorch_cpu.so \ 15 | -Wl,--as-needed $(DRL_TORCH)/lib/libc10.so \ 16 | -Wl,--no-as-needed,$(DRL_TORCH)/lib/libtorch.so 17 | -------------------------------------------------------------------------------- /openfoam/src/pinballRotatingWallVelocity/Make/options: -------------------------------------------------------------------------------- 1 | EXE_INC = \ 2 | -std=c++17 \ 3 | -Wno-deprecated-declarations -Wno-old-style-cast -Wno-redundant-move \ 4 | -I$(LIB_SRC)/finiteVolume/lnInclude \ 5 | -I$(LIB_SRC)/sampling/lnInclude \ 6 | -I$(LIB_SRC)/OpenFOAM/lnInclude \ 7 | -I$(DRL_TORCH)/include \ 8 | -I$(DRL_TORCH)/include/torch/csrc/api/include 9 | 10 | LIB_LIBS = \ 11 | -lfiniteVolume \ 12 | -rdynamic \ 13 | -Wl,-rpath,$(DRL_TORCH)/lib $(DRL_TORCH)/lib/libtorch.so $(DRL_TORCH)/lib/libc10.so \ 14 | -Wl,--no-as-needed,$(DRL_TORCH)/lib/libtorch_cpu.so \ 15 | -Wl,--as-needed $(DRL_TORCH)/lib/libc10.so \ 16 | -Wl,--no-as-needed,$(DRL_TORCH)/lib/libtorch.so 17 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/constant/turbulenceProperties: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "constant"; 14 | object turbulenceProperties; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | simulationType laminar; 19 | 20 | // ************************************************************************* // 21 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/constant/turbulenceProperties: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "constant"; 14 | object turbulenceProperties; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | simulationType laminar; 19 | 20 | 21 | // ************************************************************************* // 22 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/constant/transportProperties: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "constant"; 14 | object transportProperties; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | transportModel Newtonian; 19 | 20 | nu 0.01; 21 | 22 | // ************************************************************************* // 23 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/constant/transportProperties: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "constant"; 14 | object transportProperties; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | transportModel Newtonian; 19 | 20 | nu 1.0e-3; 21 | 22 | 23 | // ************************************************************************* // 24 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/system/decomposeParDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object decomposeParDict; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | numberOfSubdomains 2; 19 | 20 | method hierarchical; 21 | 22 | coeffs 23 | { 24 | n (2 1 1); 25 | } 26 | 27 | 28 | // ************************************************************************* // 29 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/system/decomposeParDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object decomposeParDict; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | numberOfSubdomains 8; 19 | 20 | method hierarchical; 21 | 22 | coeffs 23 | { 24 | n (4 2 1); 25 | } 26 | 27 | 28 | // ************************************************************************* // 29 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/0.org/p: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class volScalarField; 13 | object p; 14 | } 15 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 16 | 17 | dimensions [0 2 -2 0 0 0 0]; 18 | 19 | internalField uniform 0; 20 | 21 | boundaryField 22 | { 23 | "(inlet|cylinders|outlet)" 24 | { 25 | type zeroGradient; 26 | } 27 | 28 | "(front|back)" 29 | { 30 | type empty; 31 | } 32 | } 33 | 34 | 35 | // ************************************************************************* // 36 | -------------------------------------------------------------------------------- /examples/create_dummy_policy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create a randomly initialized policy network. 3 | """ 4 | import sys 5 | import torch as pt 6 | from os import environ 7 | from os.path import join 8 | from typing import Union 9 | 10 | BASE_PATH = environ.get("DRL_BASE", "") 11 | sys.path.insert(0, BASE_PATH) 12 | 13 | from drlfoam.agent import FCPolicy 14 | 15 | 16 | def create_dummy_policy(n_probes: int, n_actions: int, target_dir: str, 17 | abs_action: Union[int, float, pt.Tensor]) -> None: 18 | """ 19 | initializes new policy 20 | 21 | :param n_probes: number of probes placed in the flow field 22 | :param n_actions: number of actions 23 | :param target_dir: path to the training directory 24 | :param abs_action: absolute value of the action boundaries 25 | :return: None 26 | """ 27 | policy = FCPolicy(n_probes, n_actions, -abs_action, abs_action) 28 | script = pt.jit.script(policy) 29 | script.save(join(target_dir, "policy.pt")) 30 | 31 | 32 | if __name__ == "__main__": 33 | # rotatingCylinder2D 34 | create_dummy_policy(12, 1, join("..", "openfoam", "test_cases", "rotatingCylinder2D"), 5) 35 | # rotatingPinball2D 36 | create_dummy_policy(14, 3, join("..", "openfoam", "test_cases", "rotatingCylinder2D"), 5) 37 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/system/setExprBoundaryFieldsDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v2006 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | object setExprBoundaryFieldsDict; 14 | } 15 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 16 | 17 | U_inlet 18 | { 19 | field U; 20 | 21 | expressions 22 | ( 23 | { 24 | patch inlet; 25 | target value; 26 | vel { dir (1 0 0); } 27 | expression #{ (pos().y() < 0.0) ? 1.01*$[(vector)vel.dir] : 0.99*$[(vector)vel.dir] #}; 28 | } 29 | ); 30 | } 31 | 32 | // ************************************************************************* // 33 | 34 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/0.org/p: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class volScalarField; 13 | object p; 14 | } 15 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 16 | 17 | dimensions [0 2 -2 0 0 0 0]; 18 | 19 | internalField uniform 0; 20 | 21 | boundaryField 22 | { 23 | "(inlet|cylinder|top|bottom)" 24 | { 25 | type zeroGradient; 26 | } 27 | 28 | outlet 29 | { 30 | type fixedValue; 31 | value $internalField; 32 | } 33 | 34 | "(front|back)" 35 | { 36 | type empty; 37 | } 38 | } 39 | 40 | 41 | // ************************************************************************* // 42 | -------------------------------------------------------------------------------- /drlfoam/execution/local.py: -------------------------------------------------------------------------------- 1 | """ 2 | implements a local buffer 3 | """ 4 | from typing import Union 5 | from subprocess import Popen 6 | 7 | from .buffer import Buffer 8 | from ..environment import Environment 9 | 10 | 11 | def submit_and_wait(cmd: str, cwd: str, timeout: int = 1e15): 12 | proc = Popen([cmd], cwd=cwd) 13 | proc.wait(timeout) 14 | 15 | 16 | class LocalBuffer(Buffer): 17 | def __init__( 18 | self, 19 | path: str, 20 | base_env: Environment, 21 | buffer_size: int, 22 | n_runners_max: int, 23 | keep_trajectories: bool = True, 24 | timeout: Union[int, float] = 1e15, 25 | ): 26 | super(LocalBuffer, self).__init__( 27 | path, base_env, buffer_size, n_runners_max, keep_trajectories, timeout 28 | ) 29 | 30 | def prepare(self) -> None: 31 | cmd = f"./{self._base_env.initializer_script}" 32 | cwd = self._base_env.path 33 | self._manager.add(submit_and_wait, cmd, cwd, self._timeout) 34 | self._manager.run() 35 | self._base_env.initialized = True 36 | 37 | def fill(self) -> None: 38 | for env in self.envs: 39 | self._manager.add(submit_and_wait, f"./{env.run_script}", env.path, self._timeout) 40 | self._manager.run() 41 | if self._keep_trajectories: 42 | self.save_trajectories() 43 | self._n_fills += 1 -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/system/setExprBoundaryFieldsDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v2006 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | object setExprBoundaryFieldsDict; 14 | } 15 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 16 | 17 | U_inlet 18 | { 19 | field U; 20 | 21 | expressions 22 | ( 23 | { 24 | patch inlet; 25 | target value; 26 | //Um 1.5; - maximum velocity at the center of the channel 27 | //H 0.41; - channel height 28 | vel { dir (1 0 0); } 29 | expression #{ 4*1.5*pos().y()*(0.41-pos().y())/(0.41*0.41)*$[(vector)vel.dir] #}; 30 | } 31 | ); 32 | } 33 | 34 | // ************************************************************************* // 35 | 36 | -------------------------------------------------------------------------------- /setup-env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # location of repository 4 | export DRL_BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | # location of compiled libraries 6 | export DRL_LIBBIN="${DRL_BASE}/openfoam/libs" 7 | mkdir -p $DRL_LIBBIN 8 | 9 | if [[ $* == *--container* ]] 10 | then 11 | # workflow with Singularity container 12 | echo "Setting environment variables for execution with Singularity" 13 | # image location 14 | export DRL_IMAGE="${DRL_BASE}/of2206-py1.12.1-cpu.sif" 15 | # location of OpenFOAM bashrc file inside the container 16 | export DRL_BASHRC="/usr/lib/openfoam/openfoam2206/etc/bashrc" 17 | # path to libTorch inside the container 18 | export DRL_TORCH="/opt/libtorch" 19 | else 20 | # workflow with local installation of dependencies 21 | echo "Setting environment variables for execution without containers" 22 | unset DRL_IMAGE DRL_BASHRC 23 | # path to libTorch 24 | export DRL_TORCH="${DRL_BASE}/libtorch" 25 | # download libTorch if necessary 26 | if [ ! -d $DRL_TORCH ]; 27 | then 28 | echo "Could not find libTorch dependencies. Downloading libTorch to ${DRL_TORCH}" 29 | wget https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-1.12.1%2Bcpu.zip && \ 30 | unzip libtorch-cxx11-abi-shared-with-deps-1.12.1+cpu.zip 31 | rm libtorch-cxx11-abi-shared-with-deps-1.12.1+cpu.zip 32 | fi 33 | fi 34 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/system/fvSchemes: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object fvSchemes; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | ddtSchemes 19 | { 20 | default Euler; 21 | } 22 | 23 | gradSchemes 24 | { 25 | default Gauss linear; 26 | } 27 | 28 | divSchemes 29 | { 30 | default Gauss linear; 31 | div((nuEff*dev(T(grad(U))))) Gauss linear; 32 | } 33 | 34 | laplacianSchemes 35 | { 36 | default Gauss linear corrected; 37 | } 38 | 39 | interpolationSchemes 40 | { 41 | default linear; 42 | } 43 | 44 | snGradSchemes 45 | { 46 | default corrected; 47 | } 48 | 49 | fluxRequired 50 | { 51 | default no; 52 | p ; 53 | } 54 | 55 | 56 | // ************************************************************************* // 57 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/system/fvSchemes: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object fvSchemes; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | ddtSchemes 19 | { 20 | default Euler; 21 | } 22 | 23 | gradSchemes 24 | { 25 | default Gauss linear; 26 | } 27 | 28 | divSchemes 29 | { 30 | default Gauss linear; 31 | div(phi,U) Gauss limitedLinear 1.0; 32 | div((nuEff*dev(T(grad(U))))) Gauss linear; 33 | } 34 | 35 | laplacianSchemes 36 | { 37 | default Gauss linear corrected; 38 | } 39 | 40 | interpolationSchemes 41 | { 42 | default linear; 43 | } 44 | 45 | snGradSchemes 46 | { 47 | default corrected; 48 | } 49 | 50 | fluxRequired 51 | { 52 | default no; 53 | p ; 54 | } 55 | 56 | 57 | // ************************************************************************* // 58 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/0.org/U: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: com | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class volVectorField; 13 | arch "LSB;label=32;scalar=64"; 14 | location "0"; 15 | object U; 16 | } 17 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 18 | 19 | dimensions [ 0 1 -1 0 0 0 0 ]; 20 | 21 | internalField uniform ( 0 0 0 ); 22 | 23 | boundaryField 24 | { 25 | inlet 26 | { 27 | type fixedValue; 28 | value uniform (1 0 0); 29 | } 30 | 31 | outlet 32 | { 33 | type zeroGradient; 34 | } 35 | 36 | cylinder 37 | { 38 | type agentRotatingWallVelocity; 39 | origin (0.2 0.2 0.0); 40 | axis (0 0 1); 41 | policy "policy.pt"; 42 | probesDict "probes"; 43 | startTime 4.0; 44 | interval 20; 45 | train true; 46 | seed 0; 47 | absOmegaMax 5.0; 48 | value uniform (0 0 0); 49 | } 50 | 51 | "(top|bottom)" 52 | { 53 | type noSlip; 54 | } 55 | 56 | "(front|back)" 57 | { 58 | type empty; 59 | } 60 | } 61 | 62 | 63 | // ************************************************************************* // 64 | -------------------------------------------------------------------------------- /drlfoam/agent/tests/test_ppo_agent.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from os import remove 4 | from os.path import join, isfile 5 | import torch as pt 6 | from ..ppo_agent import PPOAgent 7 | from ...constants import DEFAULT_TENSOR_TYPE 8 | 9 | pt.set_default_tensor_type(DEFAULT_TENSOR_TYPE) 10 | 11 | 12 | class TestPPOAgent(): 13 | def test_update(self): 14 | states = [pt.rand((10, 100)) for _ in range(5)] 15 | rewards = [pt.rand(10) for _ in range(5)] 16 | actions = [pt.rand(10) for _ in range(5)] 17 | agent = PPOAgent(100, 1, pt.tensor(-10), pt.tensor(10)) 18 | agent.update(states, actions, rewards) 19 | hist = agent.history 20 | assert "policy_loss" in hist 21 | assert len(hist["policy_loss"]) == 1 22 | assert "value_loss" in hist 23 | assert len(hist["value_loss"]) == 1 24 | # test multiple actions 25 | actions = [pt.rand((10, 3)) for _ in range(5)] 26 | agent = PPOAgent(100, 3, pt.tensor(-10), pt.tensor(10)) 27 | agent.update(states, actions, rewards) 28 | assert "policy_loss" in hist 29 | assert len(hist["policy_loss"]) == 1 30 | 31 | 32 | def test_save_load(self): 33 | states = pt.rand((5, 100)) 34 | agent = PPOAgent(100, 1, pt.tensor(-10), pt.tensor(10)) 35 | p_out_ref = agent._policy(states) 36 | v_out_ref = agent._value(states) 37 | checkpoint_path = join("/tmp", "checkpoint.pt") 38 | agent.save_state(checkpoint_path) 39 | assert isfile(checkpoint_path) 40 | agent.load_state(checkpoint_path) 41 | p_out = agent._policy(states) 42 | v_out = agent._value(states) 43 | assert pt.allclose(p_out_ref, p_out) 44 | assert pt.allclose(v_out_ref, v_out) 45 | 46 | def test_trace_policy(self): 47 | states = pt.rand((5, 100)) 48 | agent = PPOAgent(100, 1, pt.tensor(-10), pt.tensor(10)) 49 | p_out_ref = agent._policy(states) 50 | trace = agent.trace_policy() 51 | p_out = trace(states) 52 | assert pt.allclose(p_out_ref, p_out) 53 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/system/fvSolution: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object fvSolution; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | solvers 19 | { 20 | p 21 | { 22 | solver GAMG; 23 | smoother DICGaussSeidel; 24 | tolerance 1e-6; 25 | relTol 0.01; 26 | } 27 | 28 | pFinal 29 | { 30 | $p; 31 | relTol 0; 32 | } 33 | 34 | U 35 | { 36 | solver smoothSolver; 37 | smoother symGaussSeidel; 38 | tolerance 1e-05; 39 | relTol 0.1; 40 | } 41 | 42 | UFinal 43 | { 44 | $U; 45 | relTol 0; 46 | } 47 | 48 | } 49 | 50 | PIMPLE 51 | { 52 | momentumPredictor yes; 53 | transonic no; 54 | nOuterCorrectors 20; 55 | nCorrectors 3; 56 | nNonOrthogonalCorrectors 1; 57 | consistent yes; 58 | pRefCell 0; 59 | pRefValue 0; 60 | 61 | residualControl 62 | { 63 | U 64 | { 65 | relTol 0; 66 | tolerance 0.0001; 67 | } 68 | p 69 | { 70 | relTol 0; 71 | tolerance 0.001; 72 | } 73 | } 74 | } 75 | 76 | 77 | // ************************************************************************* // 78 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/0.org/U: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: com | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class volVectorField; 13 | arch "LSB;label=32;scalar=64"; 14 | location "0"; 15 | object U; 16 | } 17 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 18 | 19 | dimensions [ 0 1 -1 0 0 0 0 ]; 20 | 21 | internalField uniform ( 0 0 0 ); 22 | 23 | boundaryField 24 | { 25 | inlet // Note that the inlet patch consists of the boundary surface area at the inlet, top and bottom of the mesh 26 | { 27 | type fixedValue; 28 | value uniform (1 0 0); 29 | } 30 | 31 | outlet 32 | { 33 | type inletOutlet; 34 | inletValue uniform (0 0 0); 35 | value $internalField; 36 | } 37 | 38 | cylinders 39 | { 40 | type pinballRotatingWallVelocity; 41 | origin_a (-1.299 0.0 0.0); 42 | origin_b (0.0 0.75 0.0); 43 | origin_c (0.0 -0.75 0.0); 44 | axis (0 0 1); 45 | policy "policy.pt"; 46 | probesDict "probes"; 47 | train true; 48 | seed 0; 49 | absOmegaMax 5.0; 50 | value uniform (0 0 0); 51 | } 52 | 53 | "(front|back)" 54 | { 55 | type empty; 56 | } 57 | } 58 | 59 | 60 | // ************************************************************************* // 61 | -------------------------------------------------------------------------------- /drlfoam/execution/manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | implements a class for handling the execution of runner for filling the buffer 3 | """ 4 | import logging 5 | 6 | from queue import Queue 7 | from threading import Thread 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def string_args(args: list, kwargs: dict) -> str: 13 | args_str = ", ".join([str(arg) for arg in args]) 14 | kwargs_str = ", ".join(f"{key}={str(value)}" for key, value in kwargs.items()) 15 | if args_str and kwargs_str: 16 | return args_str + ", " + kwargs_str 17 | elif args_str and not kwargs_str: 18 | return args_str 19 | elif not args_str and kwargs_str: 20 | return kwargs_str 21 | else: 22 | return "" 23 | 24 | 25 | class Runner(Thread): 26 | def __init__(self, tasks: Queue, name: str): 27 | super(Runner, self).__init__() 28 | self._tasks = tasks 29 | self._name = name 30 | self.daemon = True 31 | self.start() 32 | 33 | def run(self) -> None: 34 | while not self._tasks.empty(): 35 | try: 36 | func, args, kwargs = self._tasks.get() 37 | logger.info(f"{self._name}: {func.__name__}({string_args(args, kwargs)})") 38 | func(*args, **kwargs) 39 | except Exception as e: 40 | logger.warning(f"{self._name}: " + str(e)) 41 | finally: 42 | self._tasks.task_done() 43 | 44 | logger.info(f"{self._name}: all tasks done") 45 | 46 | 47 | class TaskManager(Queue): 48 | def __init__(self, n_runners_max: int): 49 | super(TaskManager, self).__init__() 50 | self._n_runners_max = n_runners_max 51 | self._runners = None 52 | 53 | def add(self, task, *args, **kwargs) -> None: 54 | self.put((task, args, kwargs)) 55 | 56 | def run(self, wait: bool = True) -> None: 57 | n_runners = min(self._n_runners_max, self.qsize()) 58 | self._runners = [Runner(self, f"Runner {i}") for i in range(n_runners)] 59 | if wait: 60 | self.wait() 61 | 62 | def wait(self) -> None: 63 | self.join() 64 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/system/fvSolution: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object fvSolution; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | solvers 19 | { 20 | p 21 | { 22 | solver GAMG; 23 | smoother DICGaussSeidel; 24 | tolerance 1e-6; 25 | relTol 0.01; 26 | } 27 | 28 | pFinal 29 | { 30 | $p; 31 | relTol 0; 32 | } 33 | 34 | U 35 | { 36 | solver smoothSolver; 37 | smoother symGaussSeidel; 38 | tolerance 1e-05; 39 | relTol 0.1; 40 | } 41 | 42 | UFinal 43 | { 44 | $U; 45 | relTol 0; 46 | } 47 | 48 | "cellDisplacement.*" 49 | { 50 | solver GAMG; 51 | tolerance 1e-14; 52 | relTol 0; 53 | smoother GaussSeidel; 54 | } 55 | } 56 | 57 | PIMPLE 58 | { 59 | momentumPredictor yes; 60 | transonic no; 61 | nOuterCorrectors 50; 62 | nCorrectors 1; 63 | nNonOrthogonalCorrectors 0; 64 | consistent yes; 65 | 66 | residualControl 67 | { 68 | "(U|p)" 69 | { 70 | relTol 0; 71 | tolerance 0.0001; 72 | } 73 | } 74 | } 75 | 76 | 77 | // ************************************************************************* // 78 | -------------------------------------------------------------------------------- /drlfoam/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | 2 | from os import remove, makedirs 3 | from os.path import join 4 | from shutil import rmtree 5 | from ..utils import (get_latest_time, replace_line_in_file, 6 | replace_line_latest, fetch_line_from_file) 7 | 8 | 9 | def test_get_latest_time(): 10 | test_folder = join("/tmp", "test_get_latest_time") 11 | folders = ["constant", "system", "0.org", "1e-6", "0", "1", "1.01"] 12 | for f in folders: 13 | makedirs(join(test_folder, f), exist_ok=True) 14 | latest = get_latest_time(test_folder) 15 | rmtree(test_folder) 16 | assert latest == "1.01" 17 | 18 | 19 | def test_fetch_line_from_file(): 20 | testfile = "/tmp/test_replace_line_in_file.txt" 21 | with open(testfile, "w+") as tf: 22 | tf.write( 23 | """ 24 | policy model; 25 | seed 1; 26 | train true; 27 | """ 28 | ) 29 | line = fetch_line_from_file(testfile, "train") 30 | remove(testfile) 31 | assert "true" in line 32 | 33 | 34 | def test_replace_line_in_file(): 35 | testfile = "/tmp/test_replace_line_in_file.txt" 36 | with open(testfile, "w+") as tf: 37 | tf.write( 38 | """ 39 | policy model; 40 | seed 1; 41 | train true; 42 | """ 43 | ) 44 | keyword, new = "seed", "seed 0;" 45 | replace_line_in_file(testfile, keyword, new) 46 | with open(testfile, "r") as tf: 47 | found_new_line = False 48 | for line in tf.readlines(): 49 | if new in line: 50 | found_new_line = True 51 | remove(testfile) 52 | assert found_new_line 53 | 54 | 55 | def test_replace_line_latest(): 56 | test_folder = join("/tmp", "test_replace_latest") 57 | for p in ["processor0", "processor1"]: 58 | makedirs(join(test_folder, p, "2"), exist_ok=True) 59 | with open(join(test_folder, p, "2", "U"), "w+") as tf: 60 | tf.write("seed 0;") 61 | replace_line_latest(test_folder, "U", "seed", "seed 1;") 62 | found = 0 63 | for p in ["processor0", "processor1"]: 64 | with open(join(test_folder, p, "2", "U"), "r") as tf: 65 | for line in tf.readlines(): 66 | if "seed 1;" in line: 67 | found += 1 68 | rmtree(test_folder) 69 | assert found == 2 70 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/system/topoSetDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v2106 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | object topoSetDict; 14 | } 15 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 16 | 17 | actions 18 | ( 19 | { 20 | name cylinder_a; 21 | type faceSet; 22 | action new; 23 | source cylinderToFace; 24 | p1 (-1.299 0 -1e3); 25 | p2 (-1.299 0 1e3); 26 | radius 0.5; 27 | } 28 | 29 | { 30 | name cylinder_b; 31 | type faceSet; 32 | action new; 33 | source cylinderToFace; 34 | p1 (0 0.75 -1e3); 35 | p2 (0 0.75 1e3); 36 | radius 0.5; 37 | } 38 | 39 | { 40 | name cylinder_c; 41 | type faceSet; 42 | action new; 43 | source cylinderToFace; 44 | p1 (0 -0.75 -1e3); 45 | p2 (0 -0.75 1e3); 46 | radius 0.5; 47 | } 48 | 49 | { 50 | name faceZone_a; 51 | type faceZoneSet; 52 | action new; 53 | source setToFaceZone; 54 | faceSet cylinder_a; 55 | } 56 | 57 | { 58 | name faceZone_b; 59 | type faceZoneSet; 60 | action new; 61 | source setToFaceZone; 62 | faceSet cylinder_b; 63 | } 64 | 65 | { 66 | name faceZone_c; 67 | type faceZoneSet; 68 | action new; 69 | source setToFaceZone; 70 | faceSet cylinder_c; 71 | } 72 | 73 | ); 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # repository-specific 2 | libtorch 3 | lnInclude 4 | linux64GccDPInt32Opt 5 | .vscode 6 | test_data 7 | test_training 8 | openfoam/libs 9 | *.sif 10 | pydrl 11 | examples/training* 12 | examples/visual 13 | examples/analyze_training.ipynb 14 | examples/test* 15 | *.out 16 | log.* 17 | 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | *.py,cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | local_settings.py 78 | db.sqlite3 79 | db.sqlite3-journal 80 | 81 | # Flask stuff: 82 | instance/ 83 | .webassets-cache 84 | 85 | # Scrapy stuff: 86 | .scrapy 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | 91 | # PyBuilder 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | post_processing/ 148 | run/ 149 | -------------------------------------------------------------------------------- /drlfoam/agent/tests/test_agent.py: -------------------------------------------------------------------------------- 1 | 2 | from os import remove 3 | from os.path import join, isfile 4 | import torch as pt 5 | from ..agent import compute_returns, compute_gae, FCPolicy, FCValue 6 | from ...constants import DEFAULT_TENSOR_TYPE 7 | 8 | 9 | pt.set_default_tensor_type(DEFAULT_TENSOR_TYPE) 10 | 11 | 12 | def test_compute_rewards(): 13 | gamma = 0.99 14 | rewards = pt.tensor([1.0, 0.1, 2.0]) 15 | r0 = 0.99**0*1.0 + 0.99**1*0.1 + 0.99**2*2.0 16 | r1 = 0.99**0*0.1 + 0.99**1*2.0 17 | r2 = 0.99**0*2.0 18 | expected = pt.tensor([r0, r1, r2]) 19 | assert pt.allclose(expected, compute_returns(rewards, 0.99)) 20 | 21 | 22 | def test_compute_gae(): 23 | values = pt.tensor([0.1, 0.5, 0.2, 1.0]) 24 | rewards = pt.tensor([1.0, 0.1, 2.0, 0.2]) 25 | d0 = 1.0 + 0.99 * 0.5 - 0.1 26 | d1 = 0.1 + 0.99 * 0.2 - 0.5 27 | d2 = 2.0 + 0.99 * 1.0 - 0.2 28 | t0, t1, t2 = (0.99*0.97)**0, (0.99*0.97)**1, (0.99*0.97)**2 29 | A0 = t0*d0 + t1*d1 + t2*d2 30 | A1 = t0*d1 + t1*d2 31 | A2 = t0*d2 32 | expected = pt.tensor([A0, A1, A2]) 33 | assert pt.allclose(expected, compute_gae(rewards, values, 0.99, 0.97)) 34 | 35 | 36 | class TestFCPolicy(): 37 | def test_scale(self): 38 | policy = FCPolicy(100, 1, pt.tensor(-10), pt.tensor(10)) 39 | scaled = policy._scale(pt.tensor([-10.0, 0.0, 10.0])) 40 | expected = pt.tensor([0.0, 0.5, 1.0]) 41 | assert pt.allclose(scaled, expected) 42 | 43 | def test_forward(self): 44 | policy = FCPolicy(100, 1, pt.tensor(-10), pt.tensor(10)) 45 | states = pt.rand((20, 100)) 46 | out = policy(states) 47 | assert out.shape == (20, 2) 48 | assert (out > 1.0).sum().item() == 40 49 | 50 | def test_predict(self): 51 | states, actions = pt.rand((20, 100)), pt.rand(20) 52 | policy = FCPolicy(100, 1, pt.tensor(-10), pt.tensor(10)) 53 | logp, entropy = policy.predict(states, actions) 54 | assert logp.shape == (20,) 55 | assert entropy.shape == (20,) 56 | states, actions = pt.rand((20, 100)), pt.rand((20, 2)) 57 | policy = FCPolicy(100, 2, pt.tensor(-10), pt.tensor(10)) 58 | logp, entropy = policy.predict(states, actions) 59 | assert logp.shape == (20*2,) 60 | assert entropy.shape == (20*2,) 61 | 62 | def test_tracing(self): 63 | policy = FCPolicy(100, 1, pt.tensor(-10), pt.tensor(10)) 64 | test_in = pt.rand((2, 100)) 65 | out_ref = policy(test_in) 66 | trace = pt.jit.script(policy) 67 | dest = join("/tmp/traced_test_model.pt") 68 | trace.save(dest) 69 | assert isfile(dest) 70 | policy_tr = pt.jit.load(dest) 71 | out = policy_tr(test_in) 72 | assert pt.allclose(out_ref, out) 73 | remove(dest) 74 | 75 | 76 | def test_FCValue(): 77 | value = FCValue(100) 78 | states = pt.rand((10, 100)) 79 | out = value(states) 80 | assert out.shape == (10,) 81 | -------------------------------------------------------------------------------- /drlfoam/execution/tests/test_local.py: -------------------------------------------------------------------------------- 1 | 2 | from os import makedirs, remove 3 | from os.path import join, isdir, isfile 4 | from shutil import copytree, rmtree 5 | from copy import deepcopy 6 | from pytest import fixture, raises 7 | import torch as pt 8 | from ..local import LocalBuffer 9 | from ...environment import RotatingCylinder2D 10 | from ...agent import FCPolicy 11 | from ...constants import TESTDATA_PATH, TESTCASE_PATH 12 | 13 | 14 | @fixture() 15 | def temp_training(): 16 | training = join("/tmp", "test_training") 17 | makedirs(training, exist_ok=True) 18 | case = "rotatingCylinder2D" 19 | source = join(TESTCASE_PATH, case) 20 | dest = join(training, case) 21 | copytree(source, dest, dirs_exist_ok=True) 22 | env = RotatingCylinder2D() 23 | env.path = dest 24 | yield (training, env) 25 | rmtree(training) 26 | 27 | 28 | class TestLocalBuffer(): 29 | def test_create_copies(self, temp_training): 30 | path, base_env = temp_training 31 | buffer = LocalBuffer(path, base_env, 2, 2) 32 | envs = buffer.envs 33 | assert isdir(join(path, "copy_0")) 34 | assert isdir(join(path, "copy_1")) 35 | assert envs[0].path == join(path, "copy_0") 36 | assert envs[1].path == join(path, "copy_1") 37 | 38 | def test_update_policy(self, temp_training): 39 | path, base_env = temp_training 40 | buffer = LocalBuffer(path, base_env, 1, 1) 41 | envs = buffer.envs 42 | remove(join(envs[0].path, envs[0].policy)) 43 | policy = FCPolicy(1, 1, -1, 1) 44 | buffer.update_policy(pt.jit.script(policy)) 45 | assert isfile(join(envs[0].path, envs[0].policy)) 46 | 47 | def test_prepare_fill_reset_clean(self, temp_training): 48 | path, base_env = temp_training 49 | base_env.start_time = 0.0 50 | base_env.end_time = 0.015 51 | buffer = LocalBuffer(path, base_env, 2, 2) 52 | buffer.prepare() 53 | assert isfile(join(path, base_env.path, "log.blockMesh")) 54 | assert isfile(join(path, base_env.path, "trajectory.csv")) 55 | assert isdir(join(path, base_env.path, "postProcessing")) 56 | buffer.reset() 57 | assert not isfile(join(path, "copy_0", "trajectory.csv")) 58 | assert not isdir(join(path, "copy_0", "postProcessing")) 59 | buffer.fill() 60 | assert isfile(join(path, "copy_0", "trajectory.csv")) 61 | assert isfile(join(path, "copy_0", "log.pimpleFoam")) 62 | assert isdir(join(path, "copy_0", "postProcessing")) 63 | # implicit test of save_trajectory() 64 | assert isfile(join(path, "observations_0.pt")) 65 | assert buffer._n_fills == 1 66 | buffer.clean() 67 | assert not isfile(join(path, "copy_0", "log.blockMesh")) 68 | 69 | def test_timeout(self, temp_training): 70 | path, base_env = temp_training 71 | base_env.start_time = 0.0 72 | base_env.end_time = 0.015 73 | buffer = LocalBuffer(path, base_env, 2, 2, timeout=1) 74 | assert not isfile(join(path, base_env.path, "log.pimpleFoam.pre")) 75 | -------------------------------------------------------------------------------- /examples/config_orig.yml: -------------------------------------------------------------------------------- 1 | # drlfoam Setup Configuration; any mandatory entries, which are not present here will be replaced with default values 2 | 3 | # general settings for the training 4 | training: 5 | executer: "local" # executer, either 'local' or 'slurm' 6 | simulation: "rotatingCylinder2D" # simulation environment either 'rotatingCylinder2D' or 'rotatingPinball2D' 7 | training_path: "test_training" # path to the directory in which the training should be executed 8 | n_runner: 2 # number of runners for parallel execution 9 | buffer_size: 4 # buffer size 10 | end_time: 8 # finish time of the simulation 11 | seed: 0 # seed value 12 | episodes: 20 # number of episodes to run the training 13 | checkpoint: null # start fresh or from a checkpoint.pt file null means no checkpoint provided 14 | timeout: 1e15 # execution time before a job gets killed, only relevant if executer is slurm 15 | 16 | # settings for the policy network 17 | policy_network: 18 | n_layers: 2 # number of hidden layers 19 | n_neurons: 64 # number of neurons per layer 20 | activation: "relu" # activation function 21 | 22 | # settings for training the policy network 23 | policy_training: 24 | epochs: 100 # max. number of epochs to run 25 | lr: 4e-4 # initial learning rate for the policy network 26 | clip: 0.1 # value for clipping the update of the policy network 27 | grad_norm: "inf" # clipping value for the gradient of the policy network 28 | kl_stop: 0.2 # value for KL-divergence criteria for stopping the training 29 | 30 | # settings for the value network 31 | value_network: 32 | n_layers: 2 # number of hidden layers 33 | n_neurons: 64 # number of neurons per layer 34 | activation: "relu" # activation function 35 | 36 | # settings for training the value network 37 | value_training: 38 | epochs: 100 # max. number of epochs to run 39 | lr: 5e-4 # initial learning rate for the value network 40 | clip: 0.1 # value for clipping the update of the value network 41 | grad_norm: "inf" # clipping value for the gradient of the value network 42 | mse_stop: 25.0 # value for MSE-divergence criteria for stopping the training 43 | 44 | # settings for the PPO hyperparameter 45 | ppo_settings: 46 | gamma: 0.99 # discount factor 47 | lambda: 0.97 # hyperparameter lambda for computing the GAE 48 | entropy_weight: 0.01 # value for weighing the entropy 49 | -------------------------------------------------------------------------------- /drlfoam/execution/buffer.py: -------------------------------------------------------------------------------- 1 | """ 2 | implements a base class for storing the buffer 3 | """ 4 | import logging 5 | import torch as pt 6 | 7 | from copy import deepcopy 8 | from shutil import copytree 9 | from subprocess import Popen 10 | from typing import Tuple, List 11 | from os.path import join, exists 12 | from abc import ABC, abstractmethod 13 | 14 | from ..agent import FCPolicy 15 | from .manager import TaskManager 16 | from ..environment import Environment 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class Buffer(ABC): 22 | def __init__( 23 | self, 24 | path: str, 25 | base_env: Environment, 26 | buffer_size: int, 27 | n_runners_max: int, 28 | keep_trajectories: bool, 29 | timeout: int, 30 | ): 31 | self._path = path 32 | self._base_env = base_env 33 | self._buffer_size = buffer_size 34 | self._n_runners_max = n_runners_max 35 | self._keep_trajectories = keep_trajectories 36 | self._timeout = timeout 37 | self._manager = TaskManager(self._n_runners_max) 38 | self._envs = None 39 | self._n_fills = 0 40 | 41 | @abstractmethod 42 | def prepare(self): 43 | pass 44 | 45 | @abstractmethod 46 | def fill(self): 47 | pass 48 | 49 | def create_copies(self): 50 | envs = [] 51 | for i in range(self._buffer_size): 52 | dest = join(self._path, f"copy_{i}") 53 | if not exists(dest): 54 | copytree(self._base_env.path, dest, dirs_exist_ok=True) 55 | envs.append(deepcopy(self._base_env)) 56 | envs[-1].path = dest 57 | envs[-1].end_time = envs[-1].end_time 58 | envs[-1].start_time = envs[-1].start_time 59 | envs[-1].seed = i 60 | self._envs = envs 61 | 62 | def update_policy(self, policy: FCPolicy): 63 | for env in self.envs: 64 | policy.save(join(env.path, env.policy)) 65 | policy.save(join(self.base_env.path, self.base_env.policy)) 66 | 67 | def reset(self): 68 | for env in self.envs: 69 | env.reset() 70 | 71 | def clean(self): 72 | for env in self.envs: 73 | proc = Popen([f"./{env.clean_script}"], cwd=env.path) 74 | proc.wait() 75 | 76 | def save_trajectories(self): 77 | pt.save( 78 | [env.observations for env in self.envs], 79 | join(self._path, f"observations_{self._n_fills}.pt") 80 | ) 81 | 82 | @property 83 | def base_env(self) -> Environment: 84 | return self._base_env 85 | 86 | @property 87 | def envs(self): 88 | if self._envs is None: 89 | self.create_copies() 90 | return self._envs 91 | 92 | @property 93 | def observations(self) -> Tuple[List[pt.Tensor], List[pt.Tensor], List[pt.Tensor]]: 94 | states, actions, rewards = [], [], [] 95 | for env in self.envs: 96 | obs = env.observations 97 | if all([key in obs for key in ("states", "actions", "rewards")]): 98 | states.append(obs["states"]) 99 | actions.append(obs["actions"]) 100 | rewards.append(obs["rewards"]) 101 | else: 102 | logger.warning(f"Warning: environment {env.path} returned empty observations") 103 | return states, actions, rewards 104 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/system/controlDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object controlDict; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | libs ("${DRL_LIBBIN}/libAgentRotatingWallVelocity.so"); 19 | 20 | application pimpleFoam; 21 | 22 | startFrom latestTime; 23 | 24 | startTime 0; 25 | 26 | stopAt endTime; 27 | 28 | endTime 4.0; 29 | 30 | deltaT 5.0e-4; 31 | 32 | writeControl runTime; 33 | 34 | writeInterval 1.0; 35 | 36 | purgeWrite 1; 37 | 38 | writeFormat ascii; 39 | 40 | writePrecision 8; 41 | 42 | timeFormat general; 43 | 44 | timePrecision 8; 45 | 46 | runTimeModifiable false; 47 | 48 | functions 49 | { 50 | forces 51 | { 52 | type forceCoeffs; 53 | libs (forces); 54 | executeControl runTime; 55 | executeInterval 0.01; 56 | writeControl runTime; 57 | writeInterval 0.01; 58 | timeStart 4.0; 59 | log false; 60 | patches 61 | ( 62 | cylinder 63 | ); 64 | coefficients (Cd Cl); 65 | rhoInf 1; 66 | rho rhoInf; 67 | CofR (0.2 0.2 0.005); 68 | liftDir (0 1 0); 69 | dragDir (1 0 0); 70 | magUInf 1.0; 71 | lRef 0.1; 72 | Aref 0.001; 73 | } 74 | 75 | probes 76 | { 77 | type probes; 78 | libs (sampling); 79 | 80 | // Name of the directory for probe data 81 | name probes; 82 | 83 | // Write at same frequency as fields 84 | executeControl runTime; 85 | executeInterval 0.01; 86 | writeControl runTime; 87 | writeInterval 0.01; 88 | timeStart 4.0; 89 | 90 | // Fields to be probed 91 | fields (p); 92 | 93 | // Optional: do not recalculate cells if mesh moves 94 | fixedLocations true; 95 | 96 | // Optional: interpolation scheme to use (default is cell) 97 | interpolationScheme cell; 98 | 99 | probeLocations 100 | ( 101 | (0.3 0.15 0.005) 102 | (0.3 0.2 0.005) 103 | (0.3 0.25 0.005) 104 | (0.4 0.15 0.005) 105 | (0.4 0.2 0.005) 106 | (0.4 0.25 0.005) 107 | (0.5 0.15 0.005) 108 | (0.5 0.2 0.005) 109 | (0.5 0.25 0.005) 110 | (0.6 0.15 0.005) 111 | (0.6 0.2 0.005) 112 | (0.6 0.25 0.005) 113 | ); 114 | 115 | // Optional: filter out points that haven't been found. Default 116 | // is to include them (with value -VGREAT) 117 | includeOutOfBounds false; 118 | } 119 | 120 | } 121 | 122 | // ************************************************************************* // 123 | -------------------------------------------------------------------------------- /drlfoam/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | helper functions 3 | """ 4 | import sys 5 | import logging 6 | import fileinput 7 | 8 | from glob import glob 9 | from typing import Any, Union 10 | from os.path import isdir, isfile, basename, join 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def get_time_folders(path: str) -> list: 16 | def is_float(element: Any) -> bool: 17 | # taken from: 18 | # https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float-in-python 19 | try: 20 | float(element) 21 | return True 22 | except ValueError: 23 | return False 24 | folders = [basename(x) for x in glob(join(path, "[0-9]*")) 25 | if isdir(x) and is_float(basename(x))] 26 | return folders 27 | 28 | 29 | def get_latest_time(path: str) -> str: 30 | folders = get_time_folders(path) 31 | if not folders: 32 | if isdir(join(path, "0.org")): 33 | return "0.org" 34 | else: 35 | raise ValueError(f"Could not find time folder in {path}") 36 | return sorted(folders, key=float)[-1] 37 | 38 | 39 | def fetch_line_from_file(path: str, keyword: str) -> str: 40 | with open(path) as f: 41 | lines = [] 42 | for line in f.readlines(): 43 | if keyword in line: 44 | lines.append(line) 45 | return lines if len(lines) > 1 else lines[0] 46 | 47 | 48 | def replace_line_in_file(path: str, keyword: str, new: str) -> None: 49 | """Keyword-based replacement of one or more lines in a file. 50 | 51 | :param path: file location 52 | :type path: str 53 | :param keyword: keyword based on which lines are selected 54 | :type keyword: str 55 | :param new: the new line replacing the old one 56 | :type new: str 57 | """ 58 | new = new + "\n" if not new.endswith("\n") else new 59 | fin = fileinput.input(path, inplace=True) 60 | for line in fin: 61 | if keyword in line: 62 | line = new 63 | sys.stdout.write(line) 64 | fin.close() 65 | 66 | 67 | def replace_line_latest(path: str, filename: str, keyword: str, new: str, 68 | processor: bool = True) -> None: 69 | search_path = join(path, "processor0") if processor else path 70 | latest_time = get_latest_time(search_path) 71 | if processor: 72 | for p in glob(join(path, "processor*")): 73 | replace_line_in_file( 74 | join(p, latest_time, filename), keyword, new 75 | ) 76 | else: 77 | replace_line_in_file( 78 | join(path, latest_time, filename), keyword, new 79 | ) 80 | 81 | 82 | def check_path(path: str) -> None: 83 | if not isdir(path): 84 | raise ValueError(f"Could not find path {path}") 85 | 86 | 87 | def check_file(file_path: str) -> None: 88 | if not isfile(file_path): 89 | raise ValueError(f"Could not find file {file_path}") 90 | 91 | 92 | def check_pos_int(value: int, name: str, with_zero=False) -> None: 93 | message = f"Argument {name} must be a positive integer; got {value}" 94 | if not isinstance(value, int): 95 | raise ValueError(message) 96 | lb = 0 if with_zero else 1 97 | if value < lb: 98 | raise ValueError(message) 99 | 100 | 101 | def check_pos_float(value: float, name: str, with_zero=False) -> None: 102 | message = f"Argument {name} must be a positive float; got {value}" 103 | if not isinstance(value, (float, int)): 104 | raise ValueError(message) 105 | if with_zero and value < 0.0: 106 | raise ValueError(message) 107 | if not with_zero and value <= 0.0: 108 | raise ValueError(message) 109 | -------------------------------------------------------------------------------- /drlfoam/execution/tests/test_manager.py: -------------------------------------------------------------------------------- 1 | 2 | from os import remove 3 | from os.path import join, isfile 4 | from queue import Queue 5 | from pathlib import Path 6 | from time import sleep 7 | from ..manager import Runner, TaskManager 8 | 9 | 10 | class TestRunner(): 11 | def test_run_no_args(self): 12 | queue = Queue() 13 | path = join("/tmp", "runner_testfile_no_args.txt") 14 | def create_file(): 15 | if isfile(path): 16 | remove(path) 17 | Path(path).touch() 18 | queue.put((create_file, [], {})) 19 | runner = Runner(queue, "Runner 0") 20 | runner.run() 21 | runner.join() 22 | assert isfile(path) 23 | remove(path) 24 | 25 | def test_run_args(self): 26 | queue = Queue() 27 | def create_file(path): 28 | if isfile(path): 29 | remove(path) 30 | Path(path).touch() 31 | path = join("/tmp", "runner_testfile_args.txt") 32 | queue.put((create_file, [path], {})) 33 | runner = Runner(queue, "Runner 0") 34 | runner.run() 35 | runner.join() 36 | assert isfile(path) 37 | remove(path) 38 | 39 | def test_run_kwargs(self): 40 | queue = Queue() 41 | def create_file(path): 42 | if isfile(path): 43 | remove(path) 44 | Path(path).touch() 45 | path = join("/tmp", "runner_testfile_kwargs.txt") 46 | queue.put((create_file, [], {"path": path})) 47 | runner = Runner(queue, "Runner 0") 48 | runner.run() 49 | runner.join() 50 | assert isfile(path) 51 | remove(path) 52 | 53 | def test_run_args_kwargs(self): 54 | queue = Queue() 55 | def create_file(path_1, path_2): 56 | for p in (path_1, path_2): 57 | if isfile(p): 58 | remove(p) 59 | Path(p).touch() 60 | path_1 = join("/tmp", "runner_testfile_1.txt") 61 | path_2 = join("/tmp", "runner_testfile_2.txt") 62 | queue.put((create_file, [path_1], {"path_2": path_2})) 63 | runner = Runner(queue, "Runner 0") 64 | runner.run() 65 | runner.join() 66 | for p in (path_1, path_2): 67 | assert isfile(p) 68 | remove(p) 69 | 70 | 71 | class TestTaskManager(): 72 | def test_add_and_run_no_args(self): 73 | manager = TaskManager(1) 74 | path = join("/tmp", "runner_testfile_no_args.txt") 75 | def create_file(): 76 | if isfile(path): 77 | remove(path) 78 | Path(path).touch() 79 | manager.add(create_file) 80 | assert manager.qsize() == 1 81 | manager.run() 82 | assert isfile(path) 83 | remove(path) 84 | 85 | 86 | def test_add_and_run_args(self): 87 | manager = TaskManager(1) 88 | def create_file(path_1, path_2): 89 | for p in (path_1, path_2): 90 | if isfile(p): 91 | remove(p) 92 | Path(p).touch() 93 | path_1 = join("/tmp", "runner_testfile_1.txt") 94 | path_2 = join("/tmp", "runner_testfile_2.txt") 95 | manager.add(create_file, path_1, path_2) 96 | manager.run() 97 | for p in (path_1, path_2): 98 | assert isfile(p) 99 | remove(p) 100 | 101 | def test_add_and_run_args_kwargs(self): 102 | manager = TaskManager(1) 103 | def create_file(path_1, path_2): 104 | for p in (path_1, path_2): 105 | if isfile(p): 106 | remove(p) 107 | Path(p).touch() 108 | path_1 = join("/tmp", "runner_testfile_1.txt") 109 | path_2 = join("/tmp", "runner_testfile_2.txt") 110 | manager.add(create_file, path_1, path_2=path_2) 111 | manager.run() 112 | for p in (path_1, path_2): 113 | assert isfile(p) 114 | remove(p) -------------------------------------------------------------------------------- /examples/run_training.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example training script. 3 | """ 4 | import sys 5 | import logging 6 | 7 | from time import time 8 | from os import environ 9 | from os.path import join 10 | 11 | BASE_PATH = environ.get("DRL_BASE", "") 12 | sys.path.insert(0, BASE_PATH) 13 | 14 | from drlfoam.agent import PPOAgent 15 | # from examples.debug import DebugTraining 16 | from drlfoam.execution.setup import ParseSetup 17 | from examples.create_dummy_policy import create_dummy_policy 18 | from drlfoam.execution import LocalBuffer, SlurmBuffer, SlurmConfig 19 | 20 | logging.basicConfig(level=logging.INFO) 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def print_statistics(actions, rewards): 25 | rt = [r.mean().item() for r in rewards] 26 | at_mean = [a.mean().item() for a in actions] 27 | at_std = [a.std().item() for a in actions] 28 | reward_msg = f"Reward mean/min/max: {sum(rt) / len(rt):2.4f}/{min(rt):2.4f}/{max(rt):2.4f}" 29 | action_mean_msg = f"Mean action mean/min/max: {sum(at_mean) / len(at_mean):2.4f}/{min(at_mean):2.4f}/{max(at_mean):2.4f}" 30 | action_std_msg = f"Std. action mean/min/max: {sum(at_std) / len(at_std):2.4f}/{min(at_std):2.4f}/{max(at_std):2.4f}" 31 | logger.info("\n".join((reward_msg, action_mean_msg, action_std_msg))) 32 | 33 | 34 | def main(): 35 | # if we want to debug from an IDE, we need to set all required paths first 36 | # if DEBUG: 37 | # debug = DebugTraining() 38 | 39 | # load the setup 40 | setup = ParseSetup(BASE_PATH) 41 | training_path = str(join(BASE_PATH, setup.buffer["training_path"])) 42 | setup.env.path = join(training_path, "base") 43 | 44 | # add the path to openfoam to the Allrun scripts 45 | # if DEBUG: 46 | # debug.set_openfoam_bashrc(training_path=setup.env.path) 47 | 48 | # create buffer 49 | if setup.training["executer"] == "local": 50 | buffer = LocalBuffer(training_path, setup.env, setup.buffer["buffer_size"], setup.buffer["n_runner"]) 51 | elif setup.training["executer"] == "slurm": 52 | # Typical Slurm configs for TU Dresden cluster 53 | config = SlurmConfig( 54 | n_tasks_per_node=setup.env.mpi_ranks, n_nodes=1, time="03:00:00", job_name="drl_train", 55 | modules=["development/24.04 GCC/12.3.0", "OpenMPI/4.1.5", "OpenFOAM/v2312"], 56 | commands_pre=["source $FOAM_BASH", f"source {BASE_PATH}/setup-env"] 57 | ) 58 | buffer = SlurmBuffer(training_path, setup.env, setup.buffer["buffer_size"], 59 | setup.buffer["n_runner"], config, timeout=setup.buffer["timeout"]) 60 | else: 61 | raise ValueError( 62 | f"Unknown executer {setup.training['executer']}; available options are 'local' and 'slurm'.") 63 | 64 | # create PPO agent 65 | agent = PPOAgent(setup.env.n_states, setup.env.n_actions, -setup.env.action_bounds, setup.env.action_bounds, 66 | **setup.agent) 67 | 68 | # load checkpoint if provided 69 | if setup.training["checkpoint"] is not None: 70 | logging.info(f"Loading checkpoint from file {setup.training['checkpoint']}") 71 | agent.load_state(join(training_path, setup.training["checkpoint"])) 72 | starting_episode = agent.history["episode"][-1] + 1 73 | buffer._n_fills = starting_episode 74 | else: 75 | starting_episode = 0 76 | 77 | # create fresh random policy and execute the base case 78 | create_dummy_policy(setup.env.n_states, setup.env.n_actions, setup.env.path, setup.env.action_bounds) 79 | 80 | # execute the base simulation 81 | buffer.prepare() 82 | 83 | buffer.base_env.start_time = buffer.base_env.end_time 84 | buffer.base_env.end_time = setup.training["end_time"] 85 | buffer.reset() 86 | 87 | # begin training 88 | start_time = time() 89 | for e in range(starting_episode, setup.training["episodes"]): 90 | logger.info(f"Start of episode {e}") 91 | buffer.fill() 92 | states, actions, rewards = buffer.observations 93 | print_statistics(actions, rewards) 94 | agent.update(states, actions, rewards) 95 | agent.save_state(join(training_path, f"checkpoint_{e}.pt")) 96 | current_policy = agent.trace_policy() 97 | buffer.update_policy(current_policy) 98 | current_policy.save(join(training_path, f"policy_trace_{e}.pt")) 99 | if not e == setup.training["episodes"] - 1: 100 | buffer.reset() 101 | logger.info(f"Training time (s): {time() - start_time}") 102 | 103 | 104 | if __name__ == "__main__": 105 | # option for running the training in IDE, e.g. in debugger 106 | # DEBUG = False 107 | main() 108 | -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingCylinder2D/system/blockMeshDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | object blockMeshDict; 14 | } 15 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 16 | 17 | lengthX 2.2; 18 | lengthY 0.41; 19 | cylinderX 0.2; 20 | cylinderY 0.2; 21 | radius 0.05; 22 | xPartOfR #eval{ sqrt(2)/2 * $radius}; 23 | blocks 25; 24 | blocks2 #eval{ round((($lengthX-$cylinderX * 2)) / ($cylinderX * 2) * $blocks)}; 25 | 26 | thickness 0.01; 27 | 28 | 29 | scale 1; 30 | 31 | vertices #codeStream 32 | { 33 | codeInclude 34 | #{ 35 | #include "pointField.H" 36 | #}; 37 | 38 | code 39 | #{ 40 | pointField points({ 41 | /* 0*/ {0, 0, 0}, 42 | /* 1*/ {$cylinderX * 2, 0, 0}, 43 | /* 2*/ {$lengthX, 0, 0}, 44 | /* 3*/ {$lengthX, $lengthY, 0}, 45 | /* 4*/ {$cylinderX * 2, $lengthY, 0}, 46 | /* 5*/ {0, $lengthY, 0}, 47 | /* 6*/ {$cylinderX - $xPartOfR, $cylinderY - $xPartOfR, 0}, 48 | /* 7*/ {$cylinderX + $xPartOfR, $cylinderY - $xPartOfR, 0}, 49 | /* 8*/ {$cylinderX - $xPartOfR, $cylinderY + $xPartOfR, 0}, 50 | /* 9*/ {$cylinderX + $xPartOfR, $cylinderY + $xPartOfR, 0} 51 | }); 52 | 53 | // Duplicate z points for thickness 54 | const label sz = points.size(); 55 | points.resize(2*sz); 56 | for (label i = 0; i < sz; ++i) 57 | { 58 | const point& pt = points[i]; 59 | points[i + sz] = point(pt.x(), pt.y(), $thickness); 60 | } 61 | 62 | os << points; 63 | #}; 64 | }; 65 | 66 | blocks 67 | ( 68 | hex (0 1 7 6 10 11 17 16) ($blocks $blocks 1) simpleGrading (1 0.25 1) 69 | hex (1 4 9 7 11 14 19 17) ($blocks $blocks 1) simpleGrading (1 0.25 1) 70 | hex (4 5 8 9 14 15 18 19) ($blocks $blocks 1) simpleGrading (1 0.25 1) 71 | hex (5 0 6 8 15 10 16 18) ($blocks $blocks 1) simpleGrading (1 0.25 1) 72 | hex (1 2 3 4 11 12 13 14) ($blocks2 $blocks 1) simpleGrading (4 1 1) 73 | ); 74 | 75 | edges 76 | ( 77 | arc 6 7 ($cylinderX #eval{$cylinderY-$radius} 0) 78 | arc 7 9 (#eval{$cylinderX+$radius} $cylinderY 0) 79 | arc 9 8 ($cylinderX #eval{$cylinderY+$radius} 0) 80 | arc 8 6 (#eval{$cylinderX-$radius} $cylinderY 0) 81 | arc 16 17 ($cylinderX #eval{$cylinderY-$radius} $thickness) 82 | arc 17 19 (#eval{$cylinderX+$radius} $cylinderY $thickness) 83 | arc 19 18 ($cylinderX #eval{$cylinderY+$radius} $thickness) 84 | arc 18 16 (#eval{$cylinderX-$radius} $cylinderY $thickness) 85 | ); 86 | 87 | boundary 88 | ( 89 | top 90 | { 91 | type patch; 92 | faces 93 | ( 94 | (4 5 15 14) 95 | (3 4 14 13) 96 | ); 97 | } 98 | bottom 99 | { 100 | type patch; 101 | faces 102 | ( 103 | (0 1 11 10) 104 | (1 2 12 11) 105 | ); 106 | } 107 | inlet 108 | { 109 | type patch; 110 | faces 111 | ( 112 | (5 0 10 15) 113 | ); 114 | } 115 | outlet 116 | { 117 | type patch; 118 | faces 119 | ( 120 | (2 3 13 12) 121 | ); 122 | } 123 | back 124 | { 125 | type empty; 126 | faces 127 | ( 128 | (0 1 7 6) 129 | (1 4 9 7) 130 | (4 5 8 9) 131 | (5 0 6 8) 132 | (1 2 3 4) 133 | ); 134 | } 135 | front 136 | { 137 | type empty; 138 | faces 139 | ( 140 | (10 11 17 16) 141 | (11 14 19 17) 142 | (14 15 18 19) 143 | (15 10 16 18) 144 | (11 12 13 14) 145 | ); 146 | } 147 | cylinder 148 | { 149 | type patch; 150 | faces 151 | ( 152 | (6 7 17 16) 153 | (7 9 19 17) 154 | (9 8 18 19) 155 | (8 6 16 18) 156 | ); 157 | } 158 | ); 159 | 160 | mergePatchPairs 161 | ( 162 | ); 163 | 164 | // ************************************************************************* // 165 | -------------------------------------------------------------------------------- /drlfoam/environment/tests/test_rotating_cylinder.py: -------------------------------------------------------------------------------- 1 | 2 | from os.path import join, exists 3 | from shutil import copytree, rmtree 4 | from pytest import raises, fixture 5 | from ..rotating_cylinder import (RotatingCylinder2D, _parse_forces, 6 | _parse_trajectory, _parse_probes) 7 | from ...constants import TESTDATA_PATH 8 | from ...utils import fetch_line_from_file 9 | 10 | 11 | @fixture() 12 | def temp_case(): 13 | case = "rotatingCylinder2D" 14 | source = join(TESTDATA_PATH, case) 15 | dest = join("/tmp", case) 16 | yield copytree(source, dest, dirs_exist_ok=True) 17 | rmtree(dest) 18 | 19 | 20 | def test_parse_forces(temp_case): 21 | path = join(temp_case, "postProcessing", "forces", "0", "coefficient.dat") 22 | forces = _parse_forces(path) 23 | assert all(forces.columns == ["t", "cd", "cl"]) 24 | assert len(forces) == 1 25 | 26 | 27 | def test_parse_probes(temp_case): 28 | path = join(temp_case, "postProcessing", "probes", "0", "p") 29 | n_probes = 12 30 | probes = _parse_probes(path, n_probes) 31 | assert len(probes.columns) == n_probes + 1 32 | assert len(probes) == 1 33 | 34 | 35 | def test_parse_trajectory(temp_case): 36 | path = join(temp_case, "trajectory.csv") 37 | tr = _parse_trajectory(path) 38 | assert all(tr.columns == ["t", "omega", "alpha", "beta"]) 39 | assert len(tr) == 1 40 | 41 | 42 | class TestRotatingCylinder2D(object): 43 | def test_common(self, temp_case): 44 | env = RotatingCylinder2D() 45 | assert True 46 | 47 | def test_start_time(self, temp_case): 48 | env = RotatingCylinder2D() 49 | env.initialized = True 50 | env.path = temp_case 51 | env.start_time = 2.0 52 | lines = fetch_line_from_file( 53 | join(env.path, "system", "controlDict"), 54 | "timeStart" 55 | ) 56 | assert all(["2.0" in line for line in lines]) 57 | 58 | def test_end_time(self, temp_case): 59 | env = RotatingCylinder2D() 60 | env.initialized = True 61 | env.path = temp_case 62 | env.end_time = 8.0 63 | line = fetch_line_from_file( 64 | join(env.path, "system", "controlDict"), 65 | "endTime " 66 | ) 67 | assert "8.0" in line 68 | 69 | def test_control_interval(self, temp_case): 70 | env = RotatingCylinder2D() 71 | env.initialized = True 72 | env.path = temp_case 73 | env.control_interval = 0.001 74 | lines = fetch_line_from_file( 75 | join(env.path, "system", "controlDict"), 76 | "executeInterval" 77 | ) 78 | assert all(["0.001" in line for line in lines]) 79 | lines = fetch_line_from_file( 80 | join(env.path, "system", "controlDict"), 81 | "writeInterval" 82 | ) 83 | assert all(["0.001" in line for line in lines]) 84 | 85 | def test_action_bounds(self, temp_case): 86 | env = RotatingCylinder2D() 87 | env.initialized = True 88 | env.path = temp_case 89 | env.action_bounds = 10.0 90 | line = fetch_line_from_file( 91 | join(env.path, "processor0", "4", "U"), 92 | "absOmegaMax" 93 | ) 94 | assert "10.0" in line 95 | 96 | def test_seed(self, temp_case): 97 | env = RotatingCylinder2D() 98 | env.initialized = True 99 | env.path = temp_case 100 | env.seed = 10 101 | line = fetch_line_from_file( 102 | join(env.path, "processor0", "4", "U"), 103 | "seed" 104 | ) 105 | assert "10" in line 106 | 107 | def test_policy(self, temp_case): 108 | env = RotatingCylinder2D() 109 | env.initialized = True 110 | env.path = temp_case 111 | env.policy = "model.pt" 112 | line = fetch_line_from_file( 113 | join(env.path, "processor0", "4", "U"), 114 | "policy" 115 | ) 116 | assert "model.pt" in line 117 | 118 | def test_train(self, temp_case): 119 | env = RotatingCylinder2D() 120 | env.initialized = True 121 | env.path = temp_case 122 | env.train = False 123 | line = fetch_line_from_file( 124 | join(env.path, "processor0", "4", "U"), 125 | "train" 126 | ) 127 | assert "false" in line 128 | 129 | def test_reset(self, temp_case): 130 | env = RotatingCylinder2D() 131 | env.initialized = True 132 | env.path = temp_case 133 | env.start_time = 4.0 134 | env.reset() 135 | assert not exists(join(env.path, "log.pimpleFoam")) 136 | assert not exists(join(env.path, "trajectory.csv")) 137 | assert not exists(join(env.path, "postProcessing")) 138 | 139 | def test_observations(self, temp_case): 140 | env = RotatingCylinder2D() 141 | env.initialized = True 142 | env.path = temp_case 143 | obs = env.observations 144 | assert len(obs.keys()) == 7 145 | assert all([obs[key].shape[0] == 1 for key in obs]) 146 | -------------------------------------------------------------------------------- /drlfoam/environment/environment.py: -------------------------------------------------------------------------------- 1 | """Base class for all environments. 2 | 3 | The base class provides a common interface for all derived environments 4 | and implements shared functionality. New environments should be derived 5 | from this class. 6 | """ 7 | from abc import ABC, abstractmethod 8 | from os.path import join 9 | from typing import Union, Tuple 10 | from torch import Tensor 11 | 12 | from ..utils import check_path, check_file, check_pos_int 13 | 14 | 15 | class Environment(ABC): 16 | def __init__(self, path: str, initializer_script: str, run_script: str, 17 | clean_script: str, mpi_ranks: int, n_states: int, 18 | n_actions: int): 19 | """ 20 | implements a base class for environments 21 | 22 | :param path: path to the current test case inside the 'openfoam' directory 23 | :type path: str 24 | :param initializer_script: name of the script which should be executed for the base case 25 | :type initializer_script: str 26 | :param run_script: name of the script which should be executed for the simulations 27 | :type run_script: str 28 | :param clean_script: name of the script which should be executed for resetting the simulations 29 | :type clean_script: str 30 | :param mpi_ranks: number of MPI ranks for executing the simulation 31 | :type mpi_ranks: int 32 | :param n_states: number of states 33 | :type n_states: int 34 | :param n_actions: number of actions 35 | :type n_actions: int 36 | """ 37 | self.path = path 38 | self.initializer_script = initializer_script 39 | self.run_script = run_script 40 | self.clean_script = clean_script 41 | self.mpi_ranks = mpi_ranks 42 | self.n_states = n_states 43 | self.n_actions = n_actions 44 | self._initialized = False 45 | self._start_time = None 46 | self._end_time = None 47 | self._control_interval = None 48 | self._action_bounds = None 49 | self._seed = None 50 | self._policy = None 51 | self._train = None 52 | self._observations = None 53 | 54 | @abstractmethod 55 | def reset(self): 56 | pass 57 | 58 | @property 59 | def path(self) -> str: 60 | return self._path 61 | 62 | @path.setter 63 | def path(self, value: str): 64 | check_path(value) 65 | self._path = value 66 | 67 | @property 68 | def initializer_script(self) -> str: 69 | return self._initializer_script 70 | 71 | @initializer_script.setter 72 | def initializer_script(self, value: str): 73 | check_file(join(self.path, value)) 74 | self._initializer_script = value 75 | 76 | @property 77 | def run_script(self) -> str: 78 | return self._run_script 79 | 80 | @run_script.setter 81 | def run_script(self, value: str): 82 | check_file(join(self.path, value)) 83 | self._run_script = value 84 | 85 | @property 86 | def clean_script(self) -> str: 87 | return self._clean_script 88 | 89 | @clean_script.setter 90 | def clean_script(self, value: str): 91 | check_file(join(self.path, value)) 92 | self._clean_script = value 93 | 94 | @property 95 | def mpi_ranks(self) -> int: 96 | return self._mpi_ranks 97 | 98 | @mpi_ranks.setter 99 | def mpi_ranks(self, value: int): 100 | check_pos_int(value, "mpi_ranks") 101 | self._mpi_ranks = value 102 | 103 | @property 104 | def n_states(self) -> int: 105 | return self._n_states 106 | 107 | @n_states.setter 108 | def n_states(self, value: int): 109 | check_pos_int(value, "n_states") 110 | self._n_states = value 111 | 112 | @property 113 | def n_actions(self) -> int: 114 | return self._n_actions 115 | 116 | @n_actions.setter 117 | def n_actions(self, value: int): 118 | check_pos_int(value, "n_actions") 119 | self._n_actions = value 120 | 121 | @property 122 | def initialized(self): 123 | return self._initialized 124 | 125 | @initialized.setter 126 | def initialized(self, _): 127 | self._initialized = True 128 | 129 | @property 130 | @abstractmethod 131 | def start_time(self) -> float: 132 | pass 133 | 134 | @property 135 | @abstractmethod 136 | def end_time(self) -> float: 137 | pass 138 | 139 | @property 140 | @abstractmethod 141 | def control_interval(self) -> int: 142 | pass 143 | 144 | @property 145 | @abstractmethod 146 | def action_bounds(self) -> Union[Tensor, float]: 147 | pass 148 | 149 | @property 150 | @abstractmethod 151 | def seed(self) -> int: 152 | pass 153 | 154 | @property 155 | @abstractmethod 156 | def policy(self) -> str: 157 | pass 158 | 159 | @property 160 | @abstractmethod 161 | def train(self) -> bool: 162 | pass 163 | 164 | @property 165 | @abstractmethod 166 | def observations(self) -> Tuple[Tensor]: 167 | pass 168 | 169 | @start_time.setter 170 | def start_time(self, value): 171 | self._start_time = value 172 | 173 | @end_time.setter 174 | def end_time(self, value): 175 | self._end_time = value 176 | 177 | @seed.setter 178 | def seed(self, value): 179 | self._seed = value -------------------------------------------------------------------------------- /openfoam/test_cases/rotatingPinball2D/system/controlDict: -------------------------------------------------------------------------------- 1 | /*--------------------------------*- C++ -*----------------------------------*\ 2 | | ========= | | 3 | | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox | 4 | | \\ / O peration | Version: v1912 | 5 | | \\ / A nd | Website: www.openfoam.com | 6 | | \\/ M anipulation | | 7 | \*---------------------------------------------------------------------------*/ 8 | FoamFile 9 | { 10 | version 2.0; 11 | format ascii; 12 | class dictionary; 13 | location "system"; 14 | object controlDict; 15 | } 16 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 17 | 18 | libs ("${DRL_LIBBIN}/libPinballRotatingWallVelocity.so"); 19 | 20 | application pimpleFoam; 21 | 22 | startFrom latestTime; 23 | 24 | startTime 0; 25 | 26 | stopAt endTime; 27 | 28 | endTime 200.0; 29 | 30 | deltaT 1.0e-2; 31 | 32 | writeControl runTime; 33 | 34 | writeInterval 0.5; 35 | 36 | purgeWrite 1; 37 | 38 | writeFormat ascii; 39 | 40 | writePrecision 8; 41 | 42 | timeFormat general; 43 | 44 | timePrecision 8; 45 | 46 | runTimeModifiable false; 47 | 48 | functions 49 | { 50 | forces 51 | { 52 | type forceCoeffs; 53 | libs (forces); 54 | executeControl runTime; 55 | executeInterval 0.5; 56 | writeControl runTime; 57 | writeInterval 0.5; 58 | timeStart 200; 59 | log yes; 60 | writeFields yes; 61 | 62 | patches 63 | ( 64 | cylinders 65 | ); 66 | coefficients (Cd Cl); 67 | rhoInf 1; 68 | rho rhoInf; 69 | CofR (-0.433 0.0 0.0); 70 | liftDir (0 1 0); 71 | dragDir (1 0 0); 72 | magUInf 1.0; 73 | lRef 2.5; 74 | Aref 0.05; 75 | } 76 | 77 | field_cylinder_a 78 | { 79 | type surfaceFieldValue; 80 | libs (fieldFunctionObjects); 81 | fields (forceCoeff); 82 | executeControl runTime; 83 | executeInterval 0.5; 84 | writeControl runTime; 85 | writeInterval 0.5; 86 | timeStart 200; 87 | operation sum; 88 | regionType faceZone; 89 | name faceZone_a; 90 | weightField none; 91 | writeFields yes; 92 | surfaceFormat raw; 93 | } 94 | 95 | field_cylinder_b 96 | { 97 | type surfaceFieldValue; 98 | libs (fieldFunctionObjects); 99 | fields (forceCoeff); 100 | executeControl runTime; 101 | executeInterval 0.5; 102 | writeControl runTime; 103 | writeInterval 0.5; 104 | timeStart 200; 105 | operation sum; 106 | regionType faceZone; 107 | name faceZone_b; 108 | weightField none; 109 | writeFields yes; 110 | surfaceFormat raw; 111 | } 112 | 113 | field_cylinder_c 114 | { 115 | 116 | type surfaceFieldValue; 117 | libs (fieldFunctionObjects); 118 | fields (forceCoeff); 119 | executeControl runTime; 120 | executeInterval 0.5; 121 | writeControl runTime; 122 | writeInterval 0.5; 123 | timeStart 200; 124 | operation sum; 125 | regionType faceZone; 126 | name faceZone_c; 127 | weightField none; 128 | writeFields yes; 129 | surfaceFormat raw; 130 | } 131 | 132 | probes 133 | { 134 | type probes; 135 | libs (sampling); 136 | 137 | // Name of the directory for probe data 138 | name probes; 139 | 140 | // Write at same frequency as fields 141 | executeControl runTime; 142 | executeInterval 0.5; 143 | writeControl runTime; 144 | writeInterval 0.5; 145 | timeStart 200; 146 | 147 | // Fields to be probed 148 | fields (p); 149 | 150 | // Optional: do not recalculate cells if mesh moves 151 | fixedLocations true; 152 | 153 | // Optional: interpolation scheme to use (default is cell) 154 | interpolationScheme cell; 155 | 156 | probeLocations 157 | ( 158 | (-0.55 0.75 0.005) 159 | (-0.55 -0.75 0.005) 160 | (0.0 1.33 0.005) 161 | (0.0 -1.33 0.005) 162 | (0.45 1.1 0.005) 163 | (0.45 -1.1 0.005) 164 | (1.35 1.55 0.005) 165 | (1.35 -1.55 0.005) 166 | (1.35 0.8 0.005) 167 | (1.35 -0.8 0.005) 168 | (2.35 1.55 0.005) 169 | (2.35 -1.55 0.005) 170 | (2.35 0.8 0.005) 171 | (2.35 -0.8 0.005) 172 | ); 173 | 174 | // Optional: filter out points that haven't been found. Default 175 | // is to include them (with value -VGREAT) 176 | includeOutOfBounds false; 177 | } 178 | 179 | } 180 | 181 | // ************************************************************************* // 182 | -------------------------------------------------------------------------------- /drlfoam/execution/tests/test_slurm.py: -------------------------------------------------------------------------------- 1 | 2 | from os import remove 3 | from os.path import join, isfile, isdir 4 | from re import findall 5 | from subprocess import Popen, PIPE 6 | from shutil import which 7 | import pytest 8 | from .test_local import temp_training 9 | from ..slurm import (SlurmBuffer, SlurmConfig, submit_job, 10 | get_job_status, submit_and_wait) 11 | 12 | 13 | slurm_available = pytest.mark.skipif( 14 | which("sbatch") is None, reason="Slurm workload manager not available" 15 | ) 16 | 17 | 18 | @slurm_available 19 | def test_submit_job(): 20 | config = SlurmConfig( 21 | commands=["sleep 1"], job_name="testjob", n_tasks=1, mem_per_cpu=1 22 | ) 23 | path = join("/tmp", "testjob.sh") 24 | config.write(path) 25 | job_id = submit_job(path) 26 | assert isinstance(job_id, int) 27 | if isfile(path): 28 | remove(path) 29 | 30 | 31 | @slurm_available 32 | def test_get_job_status(): 33 | config = SlurmConfig( 34 | commands=["sleep 1"], job_name="testjob", n_tasks=1, mem_per_cpu=1 35 | ) 36 | path = join("/tmp", "testjob.sh") 37 | config.write(path) 38 | job_id = submit_job(path) 39 | status = get_job_status(job_id) 40 | assert status in ["R", "CF", "PD"] 41 | if isfile(path): 42 | remove(path) 43 | with pytest.raises(Exception): 44 | get_job_status(0) 45 | 46 | 47 | @slurm_available 48 | def test_submit_and_wait(): 49 | config = SlurmConfig( 50 | commands=["sleep 2"], job_name="testjob", n_tasks=1, mem_per_cpu=1 51 | ) 52 | path = join("/tmp", "testjob.sh") 53 | config.write(path) 54 | submit_and_wait(path, 1) 55 | assert True 56 | 57 | 58 | class TestSlurmConfig(): 59 | def test_empty_write(self): 60 | config = SlurmConfig() 61 | path = join("/tmp", "emptyjob.sh") 62 | config.write(path) 63 | assert isfile(path) 64 | with open(path, "r") as job: 65 | content = job.read() 66 | assert "bash" in content 67 | remove(path) 68 | 69 | def test_write_options(self): 70 | config = SlurmConfig( 71 | job_name="testjob", 72 | n_tasks=8, 73 | mail_user="name@mail.com", 74 | n_nodes=1, 75 | time="120:00:00", 76 | partition="debug" 77 | ) 78 | path = join("/tmp", "testjob.sh") 79 | config.write(path) 80 | assert isfile(path) 81 | with open(path, "r") as job: 82 | content = job.read() 83 | assert "testjob" in content 84 | assert "name@mail.com" in content 85 | assert "120:00:00" in content 86 | assert "debug" in content 87 | remove(path) 88 | 89 | def test_write_modules(self): 90 | modules = [ 91 | "singularity/3.6.0rc2", 92 | "mpi/openmpi/4.1.1/gcc" 93 | ] 94 | config = SlurmConfig(job_name="testjob") 95 | config.modules = modules 96 | path = join("/tmp", "testjob.sh") 97 | config.write(path) 98 | assert isfile(path) 99 | with open(path, "r") as job: 100 | content = job.read() 101 | for m in modules: 102 | assert f"module load {m}" in content 103 | remove(path) 104 | 105 | def test_write_commands(self): 106 | modules = [ 107 | "singularity/3.6.0rc2", 108 | "mpi/openmpi/4.1.1/gcc" 109 | ] 110 | commands_pre = [ 111 | "source environment.sh", 112 | "source OpenFOAM commands" 113 | ] 114 | commands = [ 115 | "cd where/to/run", 116 | "./Allrun" 117 | ] 118 | config = SlurmConfig(job_name="testjob") 119 | config.modules = modules 120 | config.commands_pre = commands_pre 121 | config.commands = commands 122 | path = join("/tmp", "testjob.sh") 123 | config.write(path) 124 | assert isfile(path) 125 | with open(path, "r") as job: 126 | content = job.read() 127 | for c in commands_pre + commands: 128 | assert c in content 129 | remove(path) 130 | 131 | 132 | @slurm_available 133 | class TestSlurmBuffer(): 134 | def test_prepare_reset_fill_clean(self, temp_training): 135 | path, base_env = temp_training 136 | base_env.start_time = 0.0 137 | base_env.end_time = 0.015 138 | config = SlurmConfig(n_tasks=2, mem_per_cpu=2) 139 | buffer = SlurmBuffer(path, base_env, 2, 2, config) 140 | buffer.prepare() 141 | base_env.end_time = 0.03 142 | assert isfile(join(path, base_env.path, "log.blockMesh")) 143 | assert isfile(join(path, base_env.path, "trajectory.csv")) 144 | assert isdir(join(path, base_env.path, "postProcessing")) 145 | envs = buffer.envs 146 | buffer.reset() 147 | assert not isfile(join(path, "copy_0", "trajectory.csv")) 148 | assert not isdir(join(path, "copy_0", "postProcessing")) 149 | buffer.fill() 150 | assert isfile(join(path, "copy_0", "trajectory.csv")) 151 | assert isfile(join(path, "copy_0", "log.pimpleFoam")) 152 | assert isdir(join(path, "copy_0", "postProcessing")) 153 | # implicit test of save_trajectory() 154 | assert isfile(join(path, "observations_0.pt")) 155 | assert buffer._n_fills == 1 156 | buffer.clean() 157 | assert not isfile(join(path, "copy_0", "log.blockMesh")) 158 | 159 | def test_timeout(self, temp_training): 160 | path, base_env = temp_training 161 | base_env.start_time = 0.0 162 | base_env.end_time = 0.015 163 | config = SlurmConfig(n_tasks=2, mem_per_cpu=2) 164 | buffer = SlurmBuffer(path, base_env, 2, 2, config, timeout=1, wait=1) 165 | buffer.prepare() 166 | assert not isfile(join(path, base_env.path, "log.pimpleFoam.pre")) 167 | -------------------------------------------------------------------------------- /drlfoam/environment/tests/test_rotating_pinball.py: -------------------------------------------------------------------------------- 1 | from os.path import join, exists 2 | from shutil import copytree, rmtree 3 | from pytest import raises, fixture 4 | from ..rotating_pinball import (RotatingPinball2D, 5 | _parse_surface_field_sum, 6 | _parse_forces, _parse_probes, 7 | _parse_trajectory) 8 | from ...constants import TESTDATA_PATH 9 | from ...utils import fetch_line_from_file 10 | 11 | 12 | @fixture() 13 | def temp_case(): 14 | case = "rotatingPinball2D" 15 | source = join(TESTDATA_PATH, case) 16 | dest = join("/tmp", case) 17 | yield copytree(source, dest, dirs_exist_ok=True) 18 | rmtree(dest) 19 | 20 | 21 | def test_parse_surface_field_sum(temp_case): 22 | path = join(temp_case, "postProcessing", "field_cylinder_a", 23 | "0", "surfaceFieldValue.dat") 24 | sfs = _parse_surface_field_sum(path) 25 | assert all(sfs.columns == ["t", "cx", "cy", "cz"]) 26 | assert len(sfs) == 3 27 | assert not sfs.isnull().values.any() 28 | 29 | 30 | def test_parse_forces(temp_case): 31 | forces = _parse_forces(temp_case) 32 | columns = ["t_a", "cx_a", "cy_a", "cz_a", 33 | "t_b", "cx_b", "cy_b", "cz_b", 34 | "t_c", "cx_c", "cy_c", "cz_c" 35 | ] 36 | assert all(forces.columns == columns) 37 | assert len(forces) == 3 38 | assert not forces.isnull().values.any() 39 | 40 | 41 | def test_parse_probes(temp_case): 42 | path = join(temp_case, "postProcessing", "probes", "0", "p") 43 | probes = _parse_probes(path, 14) 44 | columns = ["t"] + [f"p{i}" for i in range(14)] 45 | assert all(probes.columns == columns) 46 | assert len(probes) == 3 47 | assert not probes.isnull().values.any() 48 | 49 | 50 | def test_parse_trajectory(temp_case): 51 | path = join(temp_case, "trajectory.csv") 52 | tr = _parse_trajectory(path) 53 | columns = [ 54 | "t", 55 | "omega_a", "alpha_a", "beta_a", 56 | "omega_b", "alpha_b", "beta_b", 57 | "omega_c", "alpha_c", "beta_c" 58 | ] 59 | assert all(tr.columns == columns) 60 | assert len(tr) == 3 61 | assert not tr.isnull().values.any() 62 | 63 | 64 | class TestRotatingPinball(object): 65 | def test_common(self, temp_case): 66 | _ = RotatingPinball2D() 67 | assert True 68 | 69 | def test_start_time(self, temp_case): 70 | env = RotatingPinball2D() 71 | env.initialized = True 72 | env.path = temp_case 73 | env.start_time = 3.0 74 | lines = fetch_line_from_file( 75 | join(env.path, "system", "controlDict"), 76 | "timeStart" 77 | ) 78 | assert len(lines) == 5 79 | assert all(["3.0" in line for line in lines]) 80 | 81 | def test_end_time(self, temp_case): 82 | env = RotatingPinball2D() 83 | env.initialized = True 84 | env.path = temp_case 85 | env.end_time = 300.0 86 | line = fetch_line_from_file( 87 | join(env.path, "system", "controlDict"), 88 | "endTime " 89 | ) 90 | assert "300.0" in line 91 | 92 | def test_control_interval(self, temp_case): 93 | env = RotatingPinball2D() 94 | env.initialized = True 95 | env.path = temp_case 96 | env.control_interval = 2.0 97 | lines = fetch_line_from_file( 98 | join(env.path, "system", "controlDict"), 99 | "executeInterval" 100 | ) 101 | assert len(lines) == 5 102 | assert all(["2.0" in line for line in lines]) 103 | lines = fetch_line_from_file( 104 | join(env.path, "system", "controlDict"), 105 | "writeInterval" 106 | ) 107 | assert len(lines) == 6 108 | assert all(["2.0" in line for line in lines]) 109 | 110 | def test_action_bounds(self, temp_case): 111 | env = RotatingPinball2D() 112 | env.initialized = True 113 | env.path = temp_case 114 | env.action_bounds = 10.0 115 | line = fetch_line_from_file( 116 | join(env.path, "processor1", "2", "U"), 117 | "absOmegaMax" 118 | ) 119 | assert "10.0" in line 120 | 121 | def test_seed(self, temp_case): 122 | env = RotatingPinball2D() 123 | env.initialized = True 124 | env.path = temp_case 125 | env.seed = 10 126 | line = fetch_line_from_file( 127 | join(env.path, "processor2", "2", "U"), 128 | "seed" 129 | ) 130 | assert "10" in line 131 | 132 | def test_policy(self, temp_case): 133 | env = RotatingPinball2D() 134 | env.initialized = True 135 | env.path = temp_case 136 | env.policy = "model.pt" 137 | line = fetch_line_from_file( 138 | join(env.path, "processor3", "2", "U"), 139 | "policy" 140 | ) 141 | assert "model.pt" in line 142 | 143 | def test_train(self, temp_case): 144 | env = RotatingPinball2D() 145 | env.initialized = True 146 | env.path = temp_case 147 | env.train = False 148 | line = fetch_line_from_file( 149 | join(env.path, "processor0", "2", "U"), 150 | "train" 151 | ) 152 | assert "false" in line 153 | 154 | def test_reset(self, temp_case): 155 | env = RotatingPinball2D() 156 | env.initialized = True 157 | env.path = temp_case 158 | env.start_time = 2.0 159 | env.reset() 160 | assert not exists(join(env.path, "log.pimpleFoam")) 161 | assert not exists(join(env.path, "trajectory.csv")) 162 | assert not exists(join(env.path, "postProcessing")) 163 | 164 | def test_observations(self, temp_case): 165 | env = RotatingPinball2D() 166 | env.initialized = True 167 | env.path = temp_case 168 | obs = env.observations 169 | assert len(obs.keys()) == 15 170 | assert all([obs[key].shape[0] == 3 for key in obs]) 171 | assert obs["states"].shape == (3, 14) 172 | assert obs["actions"].shape == (3, 3) 173 | assert obs["rewards"].shape == (3,) 174 | 175 | -------------------------------------------------------------------------------- /drlfoam/agent/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implements functions for computing the returns and GAE as well as classes for policy - and value network. 3 | Provides further a base class for all agents. 4 | """ 5 | from typing import Callable, Tuple, Union 6 | from abc import ABC, abstractmethod 7 | import torch as pt 8 | from ..constants import DEFAULT_TENSOR_TYPE 9 | 10 | 11 | pt.set_default_tensor_type(DEFAULT_TENSOR_TYPE) 12 | 13 | 14 | def compute_returns(rewards: pt.Tensor, gamma: Union[int, float] = 0.99) -> pt.Tensor: 15 | """ 16 | compute the returns based on given rewards and discount factor 17 | 18 | :param rewards: rewards 19 | :type rewards: pt.Tensor 20 | :param gamma: discount factor 21 | :type gamma: Union[int, float] 22 | :return: returns 23 | :rtype: pt.Tensor 24 | """ 25 | n_steps = len(rewards) 26 | discounts = pt.logspace(0, n_steps-1, n_steps, gamma) 27 | returns = [(discounts[:n_steps-t] * rewards[t:]).sum() 28 | for t in range(n_steps)] 29 | return pt.tensor(returns) 30 | 31 | 32 | def compute_gae(rewards: pt.Tensor, values: pt.Tensor, gamma: Union[int, float] = 0.99, 33 | lam: Union[int, float] = 0.97) -> pt.Tensor: 34 | """ 35 | Compute the generalized advantage estimate (GAE) based on 36 | 37 | 'High-Dimensional Continuous Control Using Generalized Advantage Estimation', https://arxiv.org/abs/1506.02438 38 | 39 | :param rewards: rewards 40 | :type rewards: pt.Tensor 41 | :param values: values of the states (output of value network) 42 | :type values: pt.Tensor 43 | :param gamma: discount factor 44 | :type gamma: Union[int, float] 45 | :param lam: hyperparameter lambda 46 | :type lam: Union[int, float] 47 | :return: GAE 48 | :rtype: pt.Tensor 49 | """ 50 | n_steps = len(rewards) 51 | factor = pt.logspace(0, n_steps-1, n_steps, gamma*lam) 52 | delta = rewards[:-1] + gamma * values[1:] - values[:-1] 53 | gae = [(factor[:n_steps-t-1] * delta[t:]).sum() 54 | for t in range(n_steps - 1)] 55 | return pt.tensor(gae) 56 | 57 | 58 | class FCPolicy(pt.nn.Module): 59 | def __init__(self, n_states: int, n_actions: int, action_min: Union[int, float, pt.Tensor], 60 | action_max: Union[int, float, pt.Tensor], n_layers: int = 2, n_neurons: int = 64, 61 | activation: Callable = pt.nn.functional.relu): 62 | """ 63 | implements policy network 64 | 65 | :param n_states: number of states 66 | :type n_states: int 67 | :param n_actions: number of actions 68 | :type n_actions: int 69 | :param action_min: lower bound of the actions 70 | :type action_min: Union[int, float, pt.Tensor] 71 | :param action_max: upper bound of the actions 72 | :type action_max: Union[int, float, pt.Tensor] 73 | :param n_layers: number of hidden layers 74 | :type n_layers: int 75 | :param n_neurons: number of neurons per layer 76 | :type n_neurons: int 77 | :param activation: activation function 78 | :type activation: pt.Callable 79 | """ 80 | super(FCPolicy, self).__init__() 81 | self._n_states = n_states 82 | self._n_actions = n_actions 83 | self._action_min = action_min 84 | self._action_max = action_max 85 | self._n_layers = n_layers 86 | self._n_neurons = n_neurons 87 | self._activation = activation 88 | 89 | # set up policy network 90 | self._layers = pt.nn.ModuleList() 91 | self._layers.append(pt.nn.Linear(self._n_states, self._n_neurons)) 92 | if self._n_layers > 1: 93 | for hidden in range(self._n_layers - 1): 94 | self._layers.append(pt.nn.Linear(self._n_neurons, self._n_neurons)) 95 | self._layers.append(pt.nn.LayerNorm(self._n_neurons)) 96 | self._last_layer = pt.nn.Linear(self._n_neurons, 2*self._n_actions) 97 | 98 | @pt.jit.ignore 99 | def _scale(self, actions: pt.Tensor) -> pt.Tensor: 100 | """ 101 | perform min-max-scaling of the actions 102 | 103 | :param actions: unscaled actions 104 | :type actions: pt.Tensor 105 | :return: actions scaled to an interval of [0, 1] 106 | :rtype pt.Tensor 107 | """ 108 | return (actions - self._action_min) / (self._action_max - self._action_min) 109 | 110 | def forward(self, x: pt.Tensor) -> pt.Tensor: 111 | for layer in self._layers: 112 | x = self._activation(layer(x)) 113 | return 1.0 + pt.nn.functional.softplus(self._last_layer(x)) 114 | 115 | @pt.jit.ignore 116 | def predict(self, states: pt.Tensor, actions: pt.Tensor) -> Tuple[pt.Tensor, pt.Tensor]: 117 | """ 118 | predict log-probability and associated entropy based on given states and actions based on a beta distribution 119 | for each action 120 | 121 | :param states: unscaled states 122 | :type states: pt.Tensor 123 | :param actions: unscaled actions 124 | :type actions: pt.Tensor 125 | :return: log-probability and entropy of the beta distribution(s); in case of multiple distributions, the sum 126 | is taken over the second axis 127 | :rtype Tuple[pt.Tensor, pt.Tensor] 128 | """ 129 | out = self.forward(states) 130 | c0 = out[:, :self._n_actions] 131 | c1 = out[:, self._n_actions:] 132 | beta = pt.distributions.Beta(c0, c1) 133 | if len(actions.shape) == 1: 134 | scaled_actions = self._scale(actions.unsqueeze(-1)) 135 | else: 136 | scaled_actions = self._scale(actions) 137 | log_p = beta.log_prob(scaled_actions) 138 | if len(actions.shape) == 1: 139 | return log_p.squeeze(), beta.entropy().squeeze() 140 | else: 141 | return log_p.sum(dim=1), beta.entropy().sum(dim=1) 142 | 143 | 144 | class FCValue(pt.nn.Module): 145 | def __init__(self, n_states: int, n_layers: int = 2, n_neurons: int = 64, 146 | activation: Callable = pt.nn.functional.relu): 147 | """ 148 | implements value network 149 | 150 | :param n_states: number of states 151 | :type n_states: int 152 | :param n_layers: number of hidden layers 153 | :type n_layers: int 154 | :param n_neurons: number of neurons per layer 155 | :type n_neurons: int 156 | :param activation: activation function 157 | :type activation: pt.Callable 158 | """ 159 | super(FCValue, self).__init__() 160 | self._n_states = n_states 161 | self._n_layers = n_layers 162 | self._n_neurons = n_neurons 163 | self._activation = activation 164 | 165 | # set up value network 166 | self._layers = pt.nn.ModuleList() 167 | self._layers.append(pt.nn.Linear(self._n_states, self._n_neurons)) 168 | if self._n_layers > 1: 169 | for hidden in range(self._n_layers - 1): 170 | self._layers.append(pt.nn.Linear(self._n_neurons, self._n_neurons)) 171 | self._layers.append(pt.nn.LayerNorm(self._n_neurons)) 172 | self._layers.append(pt.nn.Linear(self._n_neurons, 1)) 173 | 174 | def forward(self, x: pt.Tensor) -> pt.Tensor: 175 | for i_layer in range(len(self._layers) - 1): 176 | x = self._activation(self._layers[i_layer](x)) 177 | return self._layers[-1](x).squeeze() 178 | 179 | 180 | class Agent(ABC): 181 | """Common interface for all agents. 182 | """ 183 | 184 | @abstractmethod 185 | def update(self, states, actions, rewards): 186 | pass 187 | 188 | @abstractmethod 189 | def save_state(self, path: str): 190 | pass 191 | 192 | @abstractmethod 193 | def load_state(self, state: Union[str, dict]): 194 | pass 195 | 196 | @abstractmethod 197 | def trace_policy(self): 198 | pass 199 | 200 | @property 201 | @abstractmethod 202 | def history(self): 203 | pass 204 | 205 | @property 206 | @abstractmethod 207 | def state(self): 208 | pass -------------------------------------------------------------------------------- /openfoam/src/agentRotatingWallVelocity/agentRotatingWallVelocityFvPatchVectorField.H: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | ========= | 3 | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox 4 | \\ / O peration | 5 | \\ / A nd | www.openfoam.com 6 | \\/ M anipulation | 7 | ------------------------------------------------------------------------------- 8 | Copyright (C) 2011-2016 OpenFOAM Foundation 9 | ------------------------------------------------------------------------------- 10 | License 11 | This file is part of OpenFOAM. 12 | 13 | OpenFOAM is free software: you can redistribute it and/or modify it 14 | under the terms of the GNU General Public License as published by 15 | the Free Software Foundation, either version 3 of the License, or 16 | (at your option) any later version. 17 | 18 | OpenFOAM is distributed in the hope that it will be useful, but WITHOUT 19 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 20 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 21 | for more details. 22 | 23 | You should have received a copy of the GNU General Public License 24 | along with OpenFOAM. If not, see . 25 | 26 | Class 27 | Foam::agentRotatingWallVelocityFvPatchVectorField 28 | 29 | Group 30 | grpWallBoundaryConditions grpGenericBoundaryConditions 31 | 32 | Description 33 | This boundary condition provides a rotational velocity condition. 34 | 35 | Usage 36 | \table 37 | Property | Description | Required | Default value 38 | origin | origin of rotation in Cartesian coordinates | yes| 39 | axis | axis of rotation | yes | 40 | \endtable 41 | 42 | Example of the boundary condition specification: 43 | \verbatim 44 | 45 | { 46 | type agentRotatingWallVelocity; 47 | origin (0 0 0); 48 | axis (0 0 1); 49 | policy "policy.pt" 50 | seed 0; 51 | train true; 52 | absOmegaMax 0.05; 53 | } 54 | \endverbatim 55 | 56 | 57 | See also 58 | Foam::fixedValueFvPatchField 59 | 60 | SourceFiles 61 | agentRotatingWallVelocityFvPatchVectorField.C 62 | 63 | \*---------------------------------------------------------------------------*/ 64 | 65 | #ifndef agentRotatingWallVelocityFvPatchVectorField_H 66 | #define agentRotatingWallVelocityFvPatchVectorField_H 67 | 68 | #include 69 | #include 70 | #include "fixedValueFvPatchFields.H" 71 | #include "probes.H" 72 | #include "timeControl.H" 73 | 74 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 75 | 76 | namespace Foam 77 | { 78 | 79 | /*---------------------------------------------------------------------------*\ 80 | Class agentRotatingWallVelocityFvPatchVectorField Declaration 81 | \*---------------------------------------------------------------------------*/ 82 | 83 | class agentRotatingWallVelocityFvPatchVectorField 84 | : 85 | public fixedValueFvPatchVectorField 86 | { 87 | // Private data 88 | 89 | //- origin of the rotation 90 | vector origin_; 91 | 92 | //- axis of the rotation 93 | vector axis_; 94 | 95 | //- training or evaluation mode 96 | bool train_; 97 | 98 | //- name of the PyTorch angular velocity model 99 | word policy_name_; 100 | 101 | //- PyTorch model of angular velocity 102 | torch::jit::script::Module policy_; 103 | 104 | //- largest allowed absolute value of angular velocity 105 | scalar abs_omega_max_; 106 | 107 | //- seed for random sampling 108 | int seed_; 109 | 110 | //- name of the probes function object dictionary 111 | word probes_name_; 112 | 113 | //- random number generator 114 | std::mt19937 gen_; 115 | 116 | //- next predicted angular velocity 117 | scalar omega_; 118 | 119 | //- previously predicted angular velocity 120 | scalar omega_old_; 121 | 122 | //- last time at which angular velocity was computed 123 | scalar control_time_; 124 | 125 | //- update of next angular velocity value if true 126 | bool update_omega_; 127 | 128 | //- when to start controlling 129 | scalar start_time_; 130 | 131 | //- time increment between control actions 132 | scalar dt_control_; 133 | 134 | //- probes function object 135 | Foam::probes probes_; 136 | 137 | //- timeControls consistent with function objects 138 | Foam::timeControl control_; 139 | 140 | public: 141 | 142 | //- Runtime type information 143 | TypeName("agentRotatingWallVelocity"); 144 | 145 | 146 | // Constructors 147 | 148 | //- Construct from patch and internal field 149 | agentRotatingWallVelocityFvPatchVectorField 150 | ( 151 | const fvPatch&, 152 | const DimensionedField& 153 | ); 154 | 155 | //- Construct from patch, internal field and dictionary 156 | agentRotatingWallVelocityFvPatchVectorField 157 | ( 158 | const fvPatch&, 159 | const DimensionedField&, 160 | const dictionary& 161 | ); 162 | 163 | //- Construct by mapping given agentRotatingWallVelocityFvPatchVectorField 164 | // onto a new patch 165 | agentRotatingWallVelocityFvPatchVectorField 166 | ( 167 | const agentRotatingWallVelocityFvPatchVectorField&, 168 | const fvPatch&, 169 | const DimensionedField&, 170 | const fvPatchFieldMapper& 171 | ); 172 | 173 | //- Construct as copy 174 | agentRotatingWallVelocityFvPatchVectorField 175 | ( 176 | const agentRotatingWallVelocityFvPatchVectorField& 177 | ); 178 | 179 | //- Construct and return a clone 180 | virtual tmp clone() const 181 | { 182 | return tmp 183 | ( 184 | new agentRotatingWallVelocityFvPatchVectorField(*this) 185 | ); 186 | } 187 | 188 | //- Construct as copy setting internal field reference 189 | agentRotatingWallVelocityFvPatchVectorField 190 | ( 191 | const agentRotatingWallVelocityFvPatchVectorField&, 192 | const DimensionedField& 193 | ); 194 | 195 | //- Construct and return a clone setting internal field reference 196 | virtual tmp clone 197 | ( 198 | const DimensionedField& iF 199 | ) const 200 | { 201 | return tmp 202 | ( 203 | new agentRotatingWallVelocityFvPatchVectorField(*this, iF) 204 | ); 205 | } 206 | 207 | 208 | 209 | // Member functions 210 | 211 | //- Update the coefficients associated with the patch field 212 | virtual void updateCoeffs(); 213 | 214 | //- Write 215 | virtual void write(Ostream&) const; 216 | 217 | //- Save trajectory to file 218 | void saveTrajectory(scalar alpha, scalar beta) const; 219 | 220 | //- Get probes function object dictionary 221 | const Foam::dictionary& getProbesDict(); 222 | 223 | //- initialize probes function object 224 | Foam::probes initializeProbes(); 225 | 226 | //- initialize control 227 | Foam::timeControl initializeControl(); 228 | 229 | }; 230 | 231 | 232 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 233 | 234 | } // End namespace Foam 235 | 236 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 237 | 238 | #endif 239 | 240 | // ************************************************************************* // 241 | -------------------------------------------------------------------------------- /drlfoam/environment/rotating_cylinder.py: -------------------------------------------------------------------------------- 1 | """ 2 | implements the environment for the rotatingCylinder2D 3 | """ 4 | import logging 5 | import torch as pt 6 | 7 | from re import sub 8 | from os import remove 9 | from glob import glob 10 | from io import StringIO 11 | from typing import Union 12 | from shutil import rmtree 13 | from pandas import read_csv, DataFrame 14 | from os.path import join, isfile, isdir 15 | 16 | from .environment import Environment 17 | from ..constants import TESTCASE_PATH, DEFAULT_TENSOR_TYPE 18 | from ..utils import (check_pos_int, check_pos_float, replace_line_in_file, 19 | get_time_folders, replace_line_latest) 20 | 21 | 22 | pt.set_default_tensor_type(DEFAULT_TENSOR_TYPE) 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def _parse_forces(path: str) -> DataFrame: 27 | return read_csv(path, sep="\t", comment="#", header=None, names=["t", "cx_a", "cy_a"]) 28 | 29 | 30 | def _parse_probes(path: str, n_probes: int) -> DataFrame: 31 | with open(path, "r") as pfile: 32 | pdata = sub("[()]", "", pfile.read()) 33 | names = ["t"] + [f"p{i}" for i in range(n_probes)] 34 | return read_csv(StringIO(pdata), header=None, names=names, comment="#", delim_whitespace=True) 35 | 36 | 37 | def _parse_trajectory(path: str) -> DataFrame: 38 | return read_csv(path, sep=",", header=0, names=["t", "omega_a", "alpha_a", "beta_a"]) 39 | 40 | 41 | class RotatingCylinder2D(Environment): 42 | def __init__(self, r1: Union[int, float] = 3.0, r2: Union[int, float] = 1.0, r3: Union[int, float] = 0.1): 43 | """ 44 | implements the RotatingCylinder2D environment 45 | 46 | :param r1: offset in reward function 47 | :type r1: Union[int, float] 48 | :param r2: weighing factor for cd in reward function 49 | :type r2: Union[int, float] 50 | :param r3: weighing factor for cl in reward function 51 | :type r3: Union[int, float] 52 | """ 53 | super(RotatingCylinder2D, self).__init__( 54 | join(TESTCASE_PATH, "rotatingCylinder2D"), "Allrun.pre", 55 | "Allrun", "Allclean", 2, 12, 1 56 | ) 57 | self._r1 = r1 58 | self._r2 = r2 59 | self._r3 = r3 60 | self._initialized = False 61 | self._start_time = 0 62 | self._end_time = 4 63 | self._control_interval = 0.01 64 | self._train = True 65 | self._seed = 0 66 | self._action_bounds = 5.0 67 | self._policy = "policy.pt" 68 | 69 | def reward(self, data: dict) -> pt.Tensor: 70 | return self._r1 - (self._r2 * data["cx_a"] + self._r3 * data["cy_a"].abs()) 71 | 72 | @property 73 | def start_time(self) -> float: 74 | return self._start_time 75 | 76 | @start_time.setter 77 | def start_time(self, value: float) -> None: 78 | check_pos_float(value, "start_time", with_zero=True) 79 | replace_line_in_file( 80 | join(self.path, "system", "controlDict"), 81 | "timeStart", 82 | f" timeStart {value};" 83 | ) 84 | self._start_time = value 85 | 86 | @property 87 | def end_time(self) -> float: 88 | return self._end_time 89 | 90 | @end_time.setter 91 | def end_time(self, value: float) -> None: 92 | check_pos_float(value, "end_time", with_zero=True) 93 | replace_line_in_file( 94 | join(self.path, "system", "controlDict"), 95 | "endTime ", 96 | f"endTime {value};" 97 | ) 98 | self._end_time = value 99 | 100 | @property 101 | def control_interval(self) -> Union[float, int]: 102 | return self._control_interval 103 | 104 | @control_interval.setter 105 | def control_interval(self, value: int) -> None: 106 | check_pos_float(value, "control_interval") 107 | replace_line_in_file( 108 | join(self.path, "system", "controlDict"), 109 | "executeInterval", 110 | f" executeInterval {value};", 111 | ) 112 | replace_line_in_file( 113 | join(self.path, "system", "controlDict"), 114 | "writeInterval", 115 | f" writeInterval {value};", 116 | ) 117 | self._control_interval = value 118 | 119 | @property 120 | def action_bounds(self) -> float: 121 | return self._action_bounds 122 | 123 | @action_bounds.setter 124 | def action_bounds(self, value: float) -> None: 125 | proc = True if self.initialized else False 126 | new = f" absOmegaMax {value:2.4f};" 127 | replace_line_latest(self.path, "U", "absOmegaMax", new, proc) 128 | self._action_bounds = value 129 | 130 | @property 131 | def seed(self) -> int: 132 | return self._seed 133 | 134 | @seed.setter 135 | def seed(self, value: int) -> None: 136 | check_pos_int(value, "seed", with_zero=True) 137 | proc = True if self.initialized else False 138 | new = f" seed {value};" 139 | replace_line_latest(self.path, "U", "seed", new, proc) 140 | self._seed = value 141 | 142 | @property 143 | def policy(self) -> str: 144 | return self._policy 145 | 146 | @policy.setter 147 | def policy(self, value: str) -> None: 148 | proc = True if self.initialized else False 149 | new = f" policy {value};" 150 | replace_line_latest(self.path, "U", "policy", new, proc) 151 | self._policy = value 152 | 153 | @property 154 | def train(self) -> bool: 155 | return self._train 156 | 157 | @train.setter 158 | def train(self, value: bool) -> None: 159 | proc = True if self.initialized else False 160 | value_cpp = "true" if value else "false" 161 | new = f" train {value_cpp};" 162 | replace_line_latest(self.path, "U", "train", new, proc) 163 | self._train = value 164 | 165 | @property 166 | def observations(self) -> dict: 167 | obs = {} 168 | try: 169 | times_folder_forces = glob(join(self.path, "postProcessing", "forces", "*")) 170 | force_path = join(times_folder_forces[0], "coefficient.dat") 171 | forces = _parse_forces(force_path) 172 | tr_path = join(self.path, "trajectory.csv") 173 | tr = _parse_trajectory(tr_path) 174 | times_folder_probes = glob(join(self.path, "postProcessing", "probes", "*")) 175 | probes_path = join(times_folder_probes[0], "p") 176 | probes = _parse_probes(probes_path, self._n_states) 177 | p_names = ["p{:d}".format(i) for i in range(self._n_states)] 178 | obs["states"] = pt.from_numpy(probes[p_names].values) 179 | obs["actions"] = pt.from_numpy(tr["omega_a"].values) 180 | obs["cx_a"] = pt.from_numpy(forces["cx_a"].values) 181 | obs["cy_a"] = pt.from_numpy(forces["cy_a"].values) 182 | obs["rewards"] = self.reward(obs) 183 | obs["alpha"] = pt.from_numpy(tr["alpha_a"].values) 184 | obs["beta"] = pt.from_numpy(tr["beta_a"].values) 185 | except Exception as e: 186 | logger.warning("Could not parse observations: ", e) 187 | finally: 188 | return obs 189 | 190 | def reset(self) -> None: 191 | # if we are not in base case, then there should be a log-file from the solver used (e.g. interFoam / pimpleFoam) 192 | solver_log = glob(join(self.path, "log.*Foam")) 193 | if solver_log: 194 | files = [f"log.{solver_log[0].split('.')[-1]}", "finished.txt", "trajectory.csv"] 195 | else: 196 | # otherwise we are in the base case and have only a log.*Foam.pre, which we don't want to remove 197 | files = ["finished.txt", "trajectory.csv"] 198 | for f in files: 199 | f_path = join(self.path, f) 200 | if isfile(f_path): 201 | remove(f_path) 202 | post = join(self.path, "postProcessing") 203 | if isdir(post): 204 | rmtree(post) 205 | times = get_time_folders(join(self.path, "processor0")) 206 | times = [t for t in times if float(t) > self.start_time] 207 | for p in glob(join(self.path, "processor*")): 208 | for t in times: 209 | rmtree(join(p, t)) 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deep Reinforcement Learning with OpenFOAM 2 | 3 | ## Overview 4 | 5 | ### Repository structure 6 | 7 | - **docs**: Sphinx documentation sources (work in progress) 8 | - **drlfoam**: Python library for DRL with OpenFOAM 9 | - **examples**: annotated scripts for performing DRL trainings and tests 10 | - **openfoam**: OpenFOAM simulation setups (*test_cases*) and source files for additional OpenFOAM library components, e.g., boundary conditions (*src*) 11 | 12 | For a list of research projects employing drlFoam, refer to the [references](./references.md). 13 | 14 | ### drlFoam package 15 | 16 | - **drlfoam.environment**: wrapper classes for manipulating and parsing OpenFOAM test cases 17 | - **drlfoam.agent**: policy and value networks, agent logic and training 18 | - **drlfoam.execution**: trajectory buffer, local and cluster (Slurm) execution 19 | 20 | ## Installation 21 | 22 | ### Python environment 23 | 24 | A miss-match between the installed Python-frontend of PyTorch and the C++ frontend (libTorch) can lead to unexpected behavior. Therefore, it is recommended to create a virtual environment using [venv](https://docs.python.org/3/library/venv.html) or [conda](https://docs.conda.io/en/latest/miniconda.html). Moreover, the currently used PyTorch version (1.12.1) requires a Python interpreter **>=3.8**. The following listing shows all steps to set up a suitable virtual environment on Ubuntu 20.04 or newer. 25 | ``` 26 | # install venv 27 | sudo apt update && sudo apt install python3.8-venv 28 | # if not yet cloned, get drlfoam 29 | git clone https://github.com/OFDataCommittee/drlfoam.git 30 | cd drlfoam 31 | # create a new environment called pydrl 32 | python3 -m venv pydrl 33 | # start the environment and install dependencies 34 | source pydrl/bin/activate 35 | pip install -r requirements.txt 36 | # once the work is done, leave the environment 37 | deactivate 38 | ``` 39 | 40 | ### OpenFOAM library components 41 | 42 | Source code and test cases are only compatible with **OpenFOAM-v2206**; [installation instructions](https://develop.openfoam.com/Development/openfoam/-/wikis/precompiled). You can use the pre-compiled binaries. Make sure that the OpenFOAM environment variables are available, e.g., by adding `source /usr/lib/openfoam/openfoam2206/etc/bashrc` to `$HOME/.bashrc`. The additional OpenFOAM library components are compiled as follows: 43 | ``` 44 | # at the top-level of this repository 45 | # when executing for the first time, libTorch is downloaded by Allwmake 46 | source setup-env 47 | ./Allwmake 48 | ``` 49 | In case you want to re-compile starting from a clean state: 50 | ``` 51 | # assuming you are at the repository's top folder 52 | ./Allwclean 53 | ``` 54 | 55 | ### Working with Singularity containers 56 | 57 | Instead of installing dependencies manually, you can also employ the provided Singularity container. Singularity simplifies execution on HPC infrastructures, because no dependencies except for Singularity itself and OpenMPI are required. To build the container locally, run: 58 | ``` 59 | sudo singularity build of2206-py1.12.1-cpu.sif docker://andreweiner/of_pytorch:of2206-py1.12.1-cpu 60 | ``` 61 | By default, the container is expected to be located at the repository's top level. The default location may be changed by adjusting the `DRL_IMAGE` variable in *setup-env*. To build the OpenFOAM library components, provide the `--container` flag: 62 | ``` 63 | ./Allwmake --container 64 | ``` 65 | Similarly, for cleaning up the build: 66 | ``` 67 | ./Allwclean --container 68 | ``` 69 | 70 | ## Running a training 71 | 72 | Currently, there are two examples of assembling a DRL training with drlFoam: 73 | 1. the *rotatingCylinder2D* test case 74 | 2. the *rotatingPinball2D* test case 75 | 76 | To perform the training locally, execute the following steps: 77 | ``` 78 | # from the top-level of this repository 79 | source pydrl/bin/activate 80 | source setup-env 81 | cd examples 82 | # see config_orig.yml for all available options 83 | # defaults to: training saved in test_training; buffer size 4; 2 runners 84 | # this training requires 4 MPI ranks on average and two loops 85 | # of each runner to fill the buffer 86 | python3 run_training.py 87 | ``` 88 | The settings can be adjusted in the `config_orig.yml`, located in the `examples` directory. 89 | To run the training with the Apptainer container, pass the `--container` flag to *setup-env*: 90 | ``` 91 | source setup-env --container 92 | python3 run_training.py 93 | ``` 94 | 95 | ## Running a training with SLURM 96 | 97 | This section describes how to run a training on a HPC with SLURM. The workflow was tested on TU Braunschweig's [Pheonix cluster](https://www.tu-braunschweig.de/en/it/dienste/21/phoenix) and might need small adjustments for other HPC configurations. The cluster should provide the following modules/packages: 98 | - Apptainer 99 | - Python 3.8 100 | - OpenMPI v4.1 (minor difference might be OK) 101 | - SLURM 102 | 103 | After logging in, the following steps set up all dependencies. These steps have to be executed only once in a new environment: 104 | ``` 105 | # load git and clone repository 106 | module load comp/git/latest 107 | git clone https://github.com/OFDataCommittee/drlfoam.git 108 | # copy the Singularity image to the drlfoam folder 109 | cp /path/to/of2206-py1.12.1-cpu.sif drlfoam/ 110 | # set up the virtual Python environment 111 | module load python/3.8.2 112 | python3 -m venv pydrl 113 | source pydrl/bin/activate 114 | pip install --upgrade pip 115 | pip install -r requirements.txt 116 | # compile the OpenFOAM library components 117 | module load singularity/latest 118 | ./Allwmake --container 119 | ``` 120 | The *examples/run_training.py* scripts support SLURM-based. To run a new training on the cluster, navigate to the *examples* folder and create a new dedicated jobscript, e.g., *training_jobscript*. A suitable jobscript looks as follows: 121 | ``` 122 | #SBATCH --partition=standard 123 | #SBATCH --nodes=1 124 | #SBATCH --time=7-00:00:00 125 | #SBATCH --job-name=drl_train 126 | #SBATCH --ntasks-per-node=1 127 | 128 | module load python/3.8.2 129 | 130 | # adjust path if necessary 131 | source ~/drlfoam/pydrl/bin/activate 132 | source ~/drlfoam/setup-env --container 133 | 134 | # start a training with a buffer size of 8 and 8 runners; 135 | # save output to log.test_training 136 | python3 run_training.py &> log.test_training 137 | ``` 138 | Submitting, inspecting, and canceling of trainings works as follows: 139 | ``` 140 | # start the training 141 | sbatch training_jobscript 142 | # check the SLURM queue 143 | squeue -u $USER 144 | # canceling a job 145 | scancel job_id 146 | ``` 147 | To detect potential errors in case of failure, inspect *all* log files: 148 | - the log file of the driver script, e.g., *log.test_training* 149 | - the output of individual simulation jobs, e.g., slurm-######.out 150 | - the standard OpenFOAM log files located in *test_training/copy_#* 151 | 152 | ## Development 153 | 154 | Unittests are implemented with [PyTest](https://docs.pytest.org/en/7.1.x/). Some tests require a [Slurm](https://slurm.schedmd.com/documentation.html) installation on the test machine. Instructions for a minimal Slurm setup on Ubuntu are available [here](https://gist.github.com/ckandoth/2acef6310041244a690e4c08d2610423). If Slurm is not available, related tests are ignored. Some test require additional test data, which can be created with the *create_test_data* script. 155 | ``` 156 | # examples for running all or selected tests 157 | # starting from the repository top-level 158 | # run all available tests with additional output (-s) 159 | source setup-env 160 | pytest -s drlfoam 161 | # run all tests in the agent sub-package 162 | pytest -s drlfoam/agent 163 | # run all tests for the ppo_agent.py module 164 | pytest -s drlfoam/agent/tests/test_ppo_agent.py 165 | ``` 166 | 167 | ## Contributions 168 | 169 | drlFoam is currently developed and maintained by Andre Weiner (@AndreWeiner). Significant contributions to usability, bug fixes, and tests have been made during the first [OpenFOAM-Machine Learning Hackathon](https://github.com/OFDataCommittee/OFMLHackathon) by (in alphabetical order): 170 | 171 | - Ajay Navilarekal Rajgopal (@ajaynr) 172 | - Darshan Thummar (@darshan315) 173 | - Guilherme Lindner (@guilindner) 174 | - Julian Bissantz (@jbissantz) 175 | - Morgan Kerhouant 176 | - Mosayeb Shams (@mosayebshams) 177 | - Tomislav Marić (@tmaric) 178 | 179 | The foundation of the drlFoam implementation are the student projects by [Darshan Thummar](https://github.com/darshan315/flow_past_cylinder_by_DRL) and [Fabian Gabriel](https://github.com/FabianGabriel/Active_flow_control_past_cylinder_using_DRL). 180 | 181 | ## License 182 | 183 | drlFoam is [GPLv3](https://en.wikipedia.org/wiki/GNU_General_Public_License)-licensed; refer to the [LICENSE](https://github.com/OFDataCommittee/drlfoam/blob/main/LICENSE) file for more information. -------------------------------------------------------------------------------- /openfoam/src/pinballRotatingWallVelocity/pinballRotatingWallVelocityFvPatchVectorField.H: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | ========= | 3 | \\ / F ield | OpenFOAM: The Open Source CFD Toolbox 4 | \\ / O peration | 5 | \\ / A nd | www.openfoam.com 6 | \\/ M anipulation | 7 | ------------------------------------------------------------------------------- 8 | Copyright (C) 2011-2016 OpenFOAM Foundation 9 | ------------------------------------------------------------------------------- 10 | License 11 | This file is part of OpenFOAM. 12 | 13 | OpenFOAM is free software: you can redistribute it and/or modify it 14 | under the terms of the GNU General Public License as published by 15 | the Free Software Foundation, either version 3 of the License, or 16 | (at your option) any later version. 17 | 18 | OpenFOAM is distributed in the hope that it will be useful, but WITHOUT 19 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 20 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 21 | for more details. 22 | 23 | You should have received a copy of the GNU General Public License 24 | along with OpenFOAM. If not, see . 25 | 26 | Class 27 | Foam::pinballRotatingWallVelocityFvPatchVectorField 28 | 29 | Group 30 | grpWallBoundaryConditions grpGenericBoundaryConditions 31 | 32 | Description 33 | This boundary condition provides a rotational velocity condition. 34 | The boundary condition was originally implemented by Tom Krogmann 35 | during his student project at TU Braunschweig. For more information 36 | refer to: 37 | GitHub: https://github.com/TomKrogmann/Optimal_Sensor_Placement_for_Active_Flow_Control_in_Deep_Reinforcement_Learning 38 | report: https://zenodo.org/record/7636959#.Y-qTLcbMJkg 39 | 40 | Usage 41 | \table 42 | Property | Description | Required | Default value 43 | origin | origin of rotation in Cartesian coordinates | yes| 44 | axis | axis of rotation | yes | 45 | \endtable 46 | 47 | Example of the boundary condition specification: 48 | \verbatim 49 | 50 | { 51 | type pinballRotatingWallVelocity; 52 | origin_a (1 0 0); 53 | origin_b (0 1 0); 54 | origin_c (0 -1 0); 55 | axis (0 0 1); 56 | policy "policy.pt" 57 | seed 0; 58 | train true; 59 | absOmegaMax 5.0; 60 | } 61 | \endverbatim 62 | 63 | See also 64 | Foam::fixedValueFvPatchField 65 | 66 | SourceFiles 67 | pinballRotatingWallVelocityFvPatchVectorField.C 68 | 69 | \*---------------------------------------------------------------------------*/ 70 | 71 | #ifndef pinballRotatingWallVelocityFvPatchVectorField_H 72 | #define pinballRotatingWallVelocityFvPatchVectorField_H 73 | 74 | #include 75 | #include 76 | #include "fixedValueFvPatchFields.H" 77 | #include "probes.H" 78 | #include "timeControl.H" 79 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 80 | 81 | namespace Foam 82 | { 83 | 84 | /*---------------------------------------------------------------------------*\ 85 | Class pinballRotatingWallVelocityFvPatchVectorField Declaration 86 | \*---------------------------------------------------------------------------*/ 87 | 88 | class pinballRotatingWallVelocityFvPatchVectorField 89 | : 90 | public fixedValueFvPatchVectorField 91 | { 92 | // Private data 93 | 94 | //- origin of cylinder a 95 | vector origin_a_; 96 | 97 | //- origin of cylinder b 98 | vector origin_b_; 99 | 100 | //- origin of cylinder c 101 | vector origin_c_; 102 | 103 | //- axis of the rotation 104 | vector axis_; 105 | 106 | //- training or evaluation mode 107 | bool train_; 108 | 109 | //- name of the PyTorch angular velocity model 110 | word policy_name_; 111 | 112 | //- PyTorch model predicting mean and log(std) of angular velocity 113 | torch::jit::script::Module policy_; 114 | 115 | //- largest allowed absolute value of angular velocity 116 | scalar abs_omega_max_; 117 | 118 | //- seed for random sampling 119 | int seed_; 120 | 121 | //- name of the probes function object dictionary 122 | word probes_name_; 123 | 124 | //- random number generator 125 | std::mt19937 gen_; 126 | 127 | //- next predicted angular velocity of cylinder a 128 | scalar omega_a_; 129 | 130 | //- previously predicted angular velocity of cylinder a 131 | scalar omega_old_a_; 132 | 133 | //- next predicted angular velocity of cylinder b 134 | scalar omega_b_; 135 | 136 | //- previously predicted angular velocity of cylinder b 137 | scalar omega_old_b_; 138 | 139 | //- next predicted angular velocity of cylinder c 140 | scalar omega_c_; 141 | 142 | //- previously predicted angular velocity of cylinder c 143 | scalar omega_old_c_; 144 | 145 | //- last time at which angular velocity was computed 146 | scalar control_time_; 147 | 148 | //- update of next angular velocity value if true 149 | bool update_omega_; 150 | 151 | //- when to start controlling 152 | scalar start_time_; 153 | 154 | //- time increment between control actions 155 | scalar dt_control_; 156 | 157 | //- probes function object 158 | Foam::probes probes_; 159 | 160 | //- timeControls consistent with function objects 161 | Foam::timeControl control_; 162 | 163 | //- lists of face centers, normals, and face IDs 164 | DynamicList centers_a_; 165 | DynamicList centers_b_; 166 | DynamicList centers_c_; 167 | DynamicList normals_a_; 168 | DynamicList normals_b_; 169 | DynamicList normals_c_; 170 | DynamicList