diff --git a/.github/workflows/test_pyfans.yaml b/.github/workflows/test_pyfans.yaml new file mode 100644 index 0000000..9fb7c5f --- /dev/null +++ b/.github/workflows/test_pyfans.yaml @@ -0,0 +1,37 @@ +name: Test PyFans + +on: [push, pull_request] + +jobs: + pyfans-run-test: + runs-on: ubuntu-latest + container: unistuttgartdae/fans-ci:noble + defaults: + run: + shell: "bash --login -eo pipefail {0}" + env: + FANS_BUILD_DIR: build + FANS_MPI_USER: fans + steps: + + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Generate build directory + run: mkdir -p ${{ env.FANS_BUILD_DIR }} + + - name: Install dependencies + run: | + apt update + apt install -y cmake make g++ python3 python3-numpy python3.12-dev + + - name: Configure + working-directory: ${{ env.FANS_BUILD_DIR }} + run: | + cmake .. -DFANS_LIBRARY_FOR_MICRO_MANAGER=ON + make + + - name: Run FANS as a library via a Python script + run: | + cd test/test_pyfans + python3 run_fans_as_library.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3656e..3d696c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # FANS Changelog +## latest + +- Build FANS as a library to be coupled to a macro-scale simulation via preCICE and the Micro Manager https://github.com/DataAnalyticsEngineering/FANS/pull/23 + ## v0.3.0 - Added Linear thermal and mechanical triclinic material models https://github.com/DataAnalyticsEngineering/FANS/pull/32 diff --git a/CMakeLists.txt b/CMakeLists.txt index c652e7b..ea22f70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,17 @@ find_package(MPI REQUIRED) find_package(FFTW3 REQUIRED COMPONENTS DOUBLE MPI) +option(FANS_LIBRARY_FOR_MICRO_MANAGER "Building FANS as a library to be used by the Micro Manager." OFF) + +if (FANS_LIBRARY_FOR_MICRO_MANAGER) + include(FetchContent) + FetchContent_Declare( + pybind11 GIT_REPOSITORY https://github.com/pybind/pybind11.git + GIT_TAG v2.12.0 + ) + FetchContent_MakeAvailable(pybind11) +endif() + # ############################################################################## # TARGETS # ############################################################################## @@ -115,6 +126,10 @@ add_custom_command( COMMENT "Create a symlink for FANS executable to ${CMAKE_CURRENT_SOURCE_DIR}/test/" ) +if (FANS_LIBRARY_FOR_MICRO_MANAGER) + add_subdirectory(pyfans) +endif () + # ############################################################################## # HEADERS # ############################################################################## diff --git a/pyfans/CMakeLists.txt b/pyfans/CMakeLists.txt new file mode 100644 index 0000000..fd6ad8b --- /dev/null +++ b/pyfans/CMakeLists.txt @@ -0,0 +1,11 @@ +pybind11_add_module(PyFANS micro.hpp micro.cpp) +target_link_libraries(PyFANS PRIVATE FANS::FANS) + +add_custom_command( + TARGET PyFANS + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink + $ + ${CMAKE_CURRENT_SOURCE_DIR}/../test/test_pyfans/$ + COMMENT "Create a symlink for FANS python bindings to ${CMAKE_CURRENT_SOURCE_DIR}/../test/" +) diff --git a/pyfans/README.md b/pyfans/README.md new file mode 100644 index 0000000..475f005 --- /dev/null +++ b/pyfans/README.md @@ -0,0 +1,15 @@ +# pyFANS + +pyFANS is a Python-wrapped library to control FANS via the [Micro Manager](https://precice.org/tooling-micro-manager-overview.html). The main idea is to create a large number of FANS simulations, and couple them to one macro-scale simulation typically in Abaqus, CalculiX, etc. The library follows the [API of the Micro Manager](https://precice.org/tooling-micro-manager-prepare-micro-simulation.html). + +## Dependencies + +- [pybind11](https://pybind11.readthedocs.io/en/stable/index.html) + +## Building + +To build FANS as a Micro Manager compatible Python library, set the CMake variable `FANS_LIB` to `ON`. The CMake command to compile FANS would then be `cmake .. -DFANS_LIBRARY_FOR_MICRO_MANAGER=ON`. + +## Usage + +pyFANS is intended to be used with the Micro Manager and preCICE for two-scale coupled simulations. However, standalone use of the library is not restricted per se. Look at the [test_pyfans](../test/test_pyfans/) example to see how the library is used in a Python script. diff --git a/pyfans/micro.cpp b/pyfans/micro.cpp new file mode 100644 index 0000000..e4c075c --- /dev/null +++ b/pyfans/micro.cpp @@ -0,0 +1,100 @@ +// Micro simulation for mechanical problems +// In this file we solve a micro problem with FANS which is controlled by the Micro Manager +// This file is compiled with pybind11 to be available as a python module +// +// To check if python is able to import it, run: +// python3 -c "import micro; micro.MicroSimulation(1)" +// from the same directory + +#include "micro.hpp" +#include "setup.h" +#include "matmodel.h" + +py::array_t merge_arrays(py::array_t array1, py::array_t array2) +{ + // Ensure arrays are contiguous for efficient merging + array1 = array1.attr("copy")(); + array2 = array2.attr("copy")(); + + // Get numpy concatenate function + py::object np = py::module::import("numpy"); + py::object concatenate = np.attr("concatenate"); + + // Concatenate the two arrays + py::tuple arrays = py::make_tuple(array1, array2); + py::array_t result = concatenate(arrays, py::int_(0)).cast>(); + + return result; +} + +MicroSimulation::MicroSimulation(int sim_id, char *input_file) +{ + MPI_Init(NULL, NULL); + + // initialize fftw mpi + fftw_mpi_init(); + + // Input file name is hardcoded. TODO: Make it configurable + reader.ReadInputFile(input_file); + + reader.ReadMS(3); + matmodel = createMatmodel<3>(reader); + solver = createSolver<3>(reader, matmodel); +} + +py::dict MicroSimulation::solve(py::dict macro_data, double dt) +{ + // Time step value dt is not used currently, but is available for future use + + // Create a pybind style Numpy array from macro_write_data["micro_vector_data"], which is a Numpy array + py::array_t strain1 = macro_data["strains1to3"].cast>(); + py::array_t strain2 = macro_data["strains4to6"].cast>(); + + py::array_t strain = merge_arrays(strain1, strain2); + std::vector g0 = std::vector(strain.data(), strain.data() + strain.size()); // convert numpy array to std::vector. + + VectorXd homogenized_stress; + + matmodel->setGradient(g0); + + solver->solve(); + + homogenized_stress = solver->get_homogenized_stress(); + + auto C = solver->get_homogenized_tangent(pert_param); + + // Convert data to a py::dict again to send it back to the Micro Manager + py::dict micro_write_data; + + // Add stress and stiffness matrix data to Python dict to be returned + std::vector stress13 = {homogenized_stress[0], homogenized_stress[1], homogenized_stress[2]}; + micro_write_data["stresses1to3"] = stress13; + std::vector stress46 = {homogenized_stress[3], homogenized_stress[4], homogenized_stress[5]}; + micro_write_data["stresses4to6"] = stress46; + std::vector C_1 = {C(0, 0), C(0, 1), C(0, 2)}; + micro_write_data["cmat1"] = C_1; + std::vector C_2 = {C(0, 3), C(0, 4), C(0, 5)}; + micro_write_data["cmat2"] = C_2; + std::vector C_3 = {C(1, 1), C(1, 2), C(1, 3)}; + micro_write_data["cmat3"] = C_3; + std::vector C_4 = {C(1, 4), C(1, 5), C(2, 2)}; + micro_write_data["cmat4"] = C_4; + std::vector C_5 = {C(2, 3), C(2, 4), C(2, 5)}; + micro_write_data["cmat5"] = C_5; + std::vector C_6 = {C(3, 3), C(3, 4), C(3, 5)}; + micro_write_data["cmat6"] = C_6; + std::vector C_7 = {C(4, 4), C(4, 5), C(5, 5)}; + micro_write_data["cmat7"] = C_7; + + return micro_write_data; +} + +PYBIND11_MODULE(PyFANS, m) +{ + // optional docstring + m.doc() = "FANS for Micro Manager"; + + py::class_(m, "MicroSimulation") + .def(py::init()) + .def("solve", &MicroSimulation::solve); +} diff --git a/pyfans/micro.hpp b/pyfans/micro.hpp new file mode 100644 index 0000000..80a292f --- /dev/null +++ b/pyfans/micro.hpp @@ -0,0 +1,31 @@ +// This is the header file for the micro simulation class. +// It is included in the micro_cpp_dummy.cpp file and the micro_cpp_dummy.cpp file is compiled with pybind11 to create a python module. +// The python module is then imported in the Micro Manager. + +#pragma once +#include +#include + +#include "pybind11/pybind11.h" +#include "pybind11/numpy.h" // numpy arrays +#include "pybind11/stl.h" // std::vector conversion + +#include "general.h" +#include "matmodel.h" +#include "solver.h" + +namespace py = pybind11; + +class MicroSimulation { + public: + MicroSimulation(int sim_id, char *input_file = "input.json"); + py::dict solve(py::dict macro_write_data, double dt); + + private: + int _sim_id; + Reader reader; + // Hardcoding mechanical models because these definitions need information from the input file. + Matmodel<3> *matmodel; + Solver<3> *solver; + double pert_param = 1e-6; // scalar strain perturbation parameter +}; diff --git a/test/test_pyfans/README.md b/test/test_pyfans/README.md new file mode 100644 index 0000000..e3b0e2a --- /dev/null +++ b/test/test_pyfans/README.md @@ -0,0 +1,17 @@ +# Test pyFANS + +Test pyFANS as standalone library called from a Python script. + +## Build pyFANS + +Configure the FANS CMake build with the variable `FANS_LIB` set to `ON`. + +## Run the test + +Run + +```bash +python3 run_fans_as_library.py +``` + +The script creates a pyFANS object and calls the `solve()` method. The script only checks if the pyFANS object is created and the solve function is callable. The result is not checked for correctness. diff --git a/test/test_pyfans/input.json b/test/test_pyfans/input.json new file mode 100644 index 0000000..bb72604 --- /dev/null +++ b/test/test_pyfans/input.json @@ -0,0 +1,25 @@ +{ + "ms_filename": "../microstructures/sphere32.h5", + "ms_datasetname": "/sphere/32x32x32/ms", + "ms_L": [1.0, 1.0, 1.0], + + "problem_type": "mechanical", + "matmodel": "LinearElasticIsotropic", + "material_properties":{ + "bulk_modulus": [62.5000, 222.222], + "shear_modulus": [28.8462, 166.6667] + }, + + "method": "cg", + "error_parameters":{ + "measure": "Linfinity", + "type": "absolute", + "tolerance": 1e-10 + }, + "n_it": 100, + "macroscale_loading": [ + [[]] + ], + + "results": [] +} diff --git a/test/test_pyfans/run_fans_as_library.py b/test/test_pyfans/run_fans_as_library.py new file mode 100644 index 0000000..706ab85 --- /dev/null +++ b/test/test_pyfans/run_fans_as_library.py @@ -0,0 +1,24 @@ +""" +Run FANS as a Python callable library. +""" +import PyFANS as fans +import numpy as np + +# from mpi4py import MPI + +# MPI.Init() + +micro = fans.MicroSimulation(1) + +macro_data = dict() + +macro_data["strains1to3"] = np.random.rand(3) +macro_data["strains4to6"] = np.random.rand(3) + +dt = 0.0001 + +output = micro.solve(macro_data, dt) # solve FANS for one load vector g0 + +print(output) + +# MPI.Finalize()