From 6fb466e4186c78fec90e36f325a6ede5703cb3ca Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Wed, 28 Jan 2026 14:26:49 +0100 Subject: [PATCH 01/33] added readthedocs and integration of luscle3 plugins --- .gitattributes | 2 +- .readthedocs.yaml | 23 + MANIFEST.in | 4 +- Makefile | 35 +- README.md | 41 + ci/muscle3/build-macro-model.sh | 7 + ci/muscle3/make-docs.sh | 30 + ci/muscle3/setup-test-env.sh | 54 + ci/muscle3/st00-defs.sh | 19 + ci/muscle3/st01-system-env.sh | 46 + ci/muscle3/st02-iwrap-load.sh | 17 + ci/muscle3/st03-m3plugins-local.sh | 4 + ci/muscle3/static-code-analysis.sh | 10 + ci/muscle3/test-runner.sh | 9 + docs/_toc.yml | 8 + docs/documentation/actor_types.rst | 237 ++ .../installation_guide/iwrap_installation.rst | 69 + docs/documentation/muscle3_actors.rst | 328 +++ .../documentation/muscle3_migration_guide.rst | 318 +++ .../muscle3_plugins/code_wrapping.rst | 458 ++++ .../muscle3_plugins/example/Makefile | 19 + .../example/code_description.yaml | 22 + .../muscle3_plugins/example/example.ymmsl | 30 + .../muscle3_plugins/example/macro.f90 | 83 + .../muscle3_plugins/example/standalone.f90 | 62 + .../muscle3_plugins/example/wrapped_code.f90 | 71 + .../muscle3_plugins/examples.rst | 338 +++ docs/documentation/muscle3_plugins/index.rst | 215 ++ .../muscle3_plugins/installation.rst | 287 +++ docs/documentation/quickstart.rst | 386 +++ docs/iWrap_introduction.md | 10 +- docs/images/muscle3/actor-ports.png | Bin 0 -> 35487 bytes docs/images/muscle3/macro-actor.png | Bin 0 -> 61738 bytes iwrap/__init__.py | 13 +- iwrap/_version.py | 665 ----- .../muscle3_common/__init__.py | 0 .../actor_generators/muscle3_common/dumper.py | 27 + .../muscle3_common/m3_utils.py | 99 + .../actor_generators/muscle3_cpp/__init__.py | 0 .../muscle3_cpp/m3_cpp_actor.py | 120 + .../muscle3_cpp/resources/Makefile.jinja2 | 76 + .../muscle3_cpp/resources/macros/flags.jinja2 | 5 + .../resources/macros/legacy_ids.jinja2 | 26 + .../resources/src/muscle3_tools.cpp.jinja2 | 356 +++ .../resources/src/muscle3_tools.h.jinja2 | 55 + .../resources/src/standalone.cpp.jinja2 | 217 ++ .../muscle3_fortran/__init__.py | 0 .../muscle3_fortran/m3_fortran_actor.py | 119 + .../muscle3_fortran/resources/Makefile.jinja2 | 94 + .../resources/macros/flags.jinja2 | 5 + .../resources/macros/legacy_ids.jinja2 | 26 + .../resources/src/fortrantools.f90 | 145 ++ .../resources/src/muscle3_tools.f90.jinja2 | 359 +++ .../resources/src/standalone.f90.jinja2 | 243 ++ .../muscle3_python/__init__.py | 3 + .../muscle3_python/m3_python_actor.py | 103 + .../resources/__init__.py.jinja2 | 0 .../resources/macros/flags.jinja2 | 5 + .../resources/macros/legacy_ids.jinja2 | 12 + .../resources/muscle3_tools.py.jinja2 | 158 ++ .../resources/standalone.py.jinja2 | 162 ++ pyproject.toml | 11 +- requirements_muscle3.txt | 3 + setup.cfg | 11 +- setup.py | 11 +- test_muscle3_integration.py | 34 + tests/README.md | 422 ++++ tests/conftest.py | 396 +++ tests/integration/test_muscle3_integration.py | 88 + tests/muscle3/actors/cpp/basic_cpp/Makefile | 10 + .../actors/cpp/basic_cpp/basic_cpp.yaml | 31 + .../muscle3/actors/cpp/basic_cpp/expected.out | 3 + .../actors/cpp/basic_cpp/input_physics.xml | 31 + .../actors/cpp/basic_cpp/input_physics.xsd | 120 + .../actors/cpp/basic_cpp/test_micro.ymmsl | 13 + .../muscle3/actors/cpp/basic_mpi_cpp/Makefile | 11 + .../cpp/basic_mpi_cpp/basic_mpi_cpp.yaml | 34 + .../actors/cpp/basic_mpi_cpp/expected.out | 3 + .../actors/cpp/basic_mpi_cpp/test_micro.ymmsl | 15 + tests/muscle3/actors/cpp/restart_cpp/Makefile | 12 + .../actors/cpp/restart_cpp/expected.out | 3 + .../actors/cpp/restart_cpp/restart_cpp.yaml | 34 + .../actors/cpp/restart_cpp/test_micro.ymmsl | 16 + .../actors/cpp/restart_mpi_cpp/Makefile | 12 + .../actors/cpp/restart_mpi_cpp/expected.out | 3 + .../cpp/restart_mpi_cpp/restart_mpi_cpp.yaml | 34 + .../cpp/restart_mpi_cpp/test_micro.ymmsl | 18 + .../muscle3/actors/fortran/basic_mpi/Makefile | 9 + .../actors/fortran/basic_mpi/basic_mpi.yaml | 33 + .../actors/fortran/basic_mpi/expected.out | 3 + .../actors/fortran/basic_mpi/test_micro.ymmsl | 15 + .../actors/fortran/code_lifecycle/Makefile | 9 + .../code_lifecycle/code_lifecycle.yaml | 34 + .../fortran/code_lifecycle/expected.out | 3 + .../fortran/code_lifecycle/input_physics.xml | 31 + .../fortran/code_lifecycle/input_physics.xsd | 120 + .../fortran/code_lifecycle/test_micro.ymmsl | 13 + .../actors/fortran/code_restart/Makefile | 11 + .../fortran/code_restart/code_restart.yaml | 33 + .../actors/fortran/code_restart/expected.out | 3 + .../fortran/code_restart/test_micro.ymmsl | 16 + .../actors/fortran/code_restart_mpi/Makefile | 11 + .../code_restart_mpi/code_restart_mpi.yaml | 33 + .../fortran/code_restart_mpi/expected.out | 3 + .../fortran/code_restart_mpi/test_micro.ymmsl | 18 + .../actors/python/basic_python/Makefile | 9 + .../python/basic_python/basic_python.yaml | 25 + .../actors/python/basic_python/expected.out | 3 + .../python/basic_python/test_micro.ymmsl | 13 + .../actors/python/restart_python/Makefile | 12 + .../actors/python/restart_python/expected.out | 3 + .../python/restart_python/restart_python.yaml | 28 + .../python/restart_python/test_micro.ymmsl | 17 + .../xml/input/input_physics.xml | 31 + .../xml/input/input_physics.xsd | 120 + tests/muscle3/macro/Makefile | 27 + tests/muscle3/macro/test_macro.f90 | 144 ++ tests/muscle3/workflow/test.ymmsl | 16 + tests/muscle3/workflow/test_macro.ymmsl | 9 + .../muscle3/wrapped_codes/cpp/basic/Makefile | 44 + .../muscle3/wrapped_codes/cpp/basic/basic.cpp | 134 + tests/muscle3/wrapped_codes/cpp/basic/basic.h | 27 + .../wrapped_codes/cpp/parallel_mpi/Makefile | 27 + .../cpp/parallel_mpi/parallel_mpi.cpp | 154 ++ .../cpp/parallel_mpi/parallel_mpi.h | 27 + .../wrapped_codes/fortran/basic/Makefile | 30 + .../wrapped_codes/fortran/basic/basic.f90 | 381 +++ .../fortran/basic_parametrizable/Makefile | 18 + .../fortran/basic_parametrizable/basic.f90 | 172 ++ .../fortran/parallel_mpi/Makefile | 27 + .../fortran/parallel_mpi/parallel_mpi.f90 | 213 ++ .../wrapped_codes/python/basic/Makefile | 7 + .../wrapped_codes/python/basic/basic.py | 94 + tests/pytest.ini | 58 + tests/run-all-tests-master.sh | 394 +++ tests/run-all-tests.sh | 185 ++ tests/run-muscle3-tests.sh | 46 + tests/unit/generators/test_muscle3_cpp.py | 61 + tests/unit/generators/test_muscle3_fortran.py | 61 + tests/unit/generators/test_muscle3_python.py | 137 + versioneer.py | 2220 ----------------- 141 files changed, 10167 insertions(+), 2919 deletions(-) create mode 100644 .readthedocs.yaml create mode 100755 ci/muscle3/build-macro-model.sh create mode 100755 ci/muscle3/make-docs.sh create mode 100755 ci/muscle3/setup-test-env.sh create mode 100755 ci/muscle3/st00-defs.sh create mode 100755 ci/muscle3/st01-system-env.sh create mode 100755 ci/muscle3/st02-iwrap-load.sh create mode 100755 ci/muscle3/st03-m3plugins-local.sh create mode 100755 ci/muscle3/static-code-analysis.sh create mode 100755 ci/muscle3/test-runner.sh create mode 100644 docs/documentation/actor_types.rst create mode 100644 docs/documentation/muscle3_actors.rst create mode 100644 docs/documentation/muscle3_migration_guide.rst create mode 100644 docs/documentation/muscle3_plugins/code_wrapping.rst create mode 100644 docs/documentation/muscle3_plugins/example/Makefile create mode 100644 docs/documentation/muscle3_plugins/example/code_description.yaml create mode 100644 docs/documentation/muscle3_plugins/example/example.ymmsl create mode 100644 docs/documentation/muscle3_plugins/example/macro.f90 create mode 100644 docs/documentation/muscle3_plugins/example/standalone.f90 create mode 100644 docs/documentation/muscle3_plugins/example/wrapped_code.f90 create mode 100644 docs/documentation/muscle3_plugins/examples.rst create mode 100644 docs/documentation/muscle3_plugins/index.rst create mode 100644 docs/documentation/muscle3_plugins/installation.rst create mode 100644 docs/documentation/quickstart.rst create mode 100644 docs/images/muscle3/actor-ports.png create mode 100644 docs/images/muscle3/macro-actor.png delete mode 100644 iwrap/_version.py create mode 100644 iwrap/generators/actor_generators/muscle3_common/__init__.py create mode 100644 iwrap/generators/actor_generators/muscle3_common/dumper.py create mode 100644 iwrap/generators/actor_generators/muscle3_common/m3_utils.py create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/__init__.py create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/m3_cpp_actor.py create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/resources/Makefile.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/resources/macros/flags.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/resources/macros/legacy_ids.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.cpp.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.h.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_cpp/resources/src/standalone.cpp.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/__init__.py create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/m3_fortran_actor.py create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/resources/Makefile.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/resources/macros/flags.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/resources/macros/legacy_ids.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/resources/src/fortrantools.f90 create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/resources/src/muscle3_tools.f90.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_fortran/resources/src/standalone.f90.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_python/__init__.py create mode 100644 iwrap/generators/actor_generators/muscle3_python/m3_python_actor.py create mode 100644 iwrap/generators/actor_generators/muscle3_python/resources/__init__.py.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_python/resources/macros/flags.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 create mode 100644 iwrap/generators/actor_generators/muscle3_python/resources/standalone.py.jinja2 create mode 100644 requirements_muscle3.txt create mode 100644 test_muscle3_integration.py create mode 100644 tests/README.md create mode 100644 tests/conftest.py create mode 100644 tests/integration/test_muscle3_integration.py create mode 100644 tests/muscle3/actors/cpp/basic_cpp/Makefile create mode 100644 tests/muscle3/actors/cpp/basic_cpp/basic_cpp.yaml create mode 100644 tests/muscle3/actors/cpp/basic_cpp/expected.out create mode 100644 tests/muscle3/actors/cpp/basic_cpp/input_physics.xml create mode 100644 tests/muscle3/actors/cpp/basic_cpp/input_physics.xsd create mode 100644 tests/muscle3/actors/cpp/basic_cpp/test_micro.ymmsl create mode 100644 tests/muscle3/actors/cpp/basic_mpi_cpp/Makefile create mode 100644 tests/muscle3/actors/cpp/basic_mpi_cpp/basic_mpi_cpp.yaml create mode 100644 tests/muscle3/actors/cpp/basic_mpi_cpp/expected.out create mode 100644 tests/muscle3/actors/cpp/basic_mpi_cpp/test_micro.ymmsl create mode 100644 tests/muscle3/actors/cpp/restart_cpp/Makefile create mode 100644 tests/muscle3/actors/cpp/restart_cpp/expected.out create mode 100644 tests/muscle3/actors/cpp/restart_cpp/restart_cpp.yaml create mode 100644 tests/muscle3/actors/cpp/restart_cpp/test_micro.ymmsl create mode 100644 tests/muscle3/actors/cpp/restart_mpi_cpp/Makefile create mode 100644 tests/muscle3/actors/cpp/restart_mpi_cpp/expected.out create mode 100644 tests/muscle3/actors/cpp/restart_mpi_cpp/restart_mpi_cpp.yaml create mode 100644 tests/muscle3/actors/cpp/restart_mpi_cpp/test_micro.ymmsl create mode 100644 tests/muscle3/actors/fortran/basic_mpi/Makefile create mode 100644 tests/muscle3/actors/fortran/basic_mpi/basic_mpi.yaml create mode 100644 tests/muscle3/actors/fortran/basic_mpi/expected.out create mode 100644 tests/muscle3/actors/fortran/basic_mpi/test_micro.ymmsl create mode 100644 tests/muscle3/actors/fortran/code_lifecycle/Makefile create mode 100644 tests/muscle3/actors/fortran/code_lifecycle/code_lifecycle.yaml create mode 100644 tests/muscle3/actors/fortran/code_lifecycle/expected.out create mode 100644 tests/muscle3/actors/fortran/code_lifecycle/input_physics.xml create mode 100644 tests/muscle3/actors/fortran/code_lifecycle/input_physics.xsd create mode 100644 tests/muscle3/actors/fortran/code_lifecycle/test_micro.ymmsl create mode 100644 tests/muscle3/actors/fortran/code_restart/Makefile create mode 100644 tests/muscle3/actors/fortran/code_restart/code_restart.yaml create mode 100644 tests/muscle3/actors/fortran/code_restart/expected.out create mode 100644 tests/muscle3/actors/fortran/code_restart/test_micro.ymmsl create mode 100644 tests/muscle3/actors/fortran/code_restart_mpi/Makefile create mode 100644 tests/muscle3/actors/fortran/code_restart_mpi/code_restart_mpi.yaml create mode 100644 tests/muscle3/actors/fortran/code_restart_mpi/expected.out create mode 100644 tests/muscle3/actors/fortran/code_restart_mpi/test_micro.ymmsl create mode 100644 tests/muscle3/actors/python/basic_python/Makefile create mode 100644 tests/muscle3/actors/python/basic_python/basic_python.yaml create mode 100644 tests/muscle3/actors/python/basic_python/expected.out create mode 100644 tests/muscle3/actors/python/basic_python/test_micro.ymmsl create mode 100644 tests/muscle3/actors/python/restart_python/Makefile create mode 100644 tests/muscle3/actors/python/restart_python/expected.out create mode 100644 tests/muscle3/actors/python/restart_python/restart_python.yaml create mode 100644 tests/muscle3/actors/python/restart_python/test_micro.ymmsl create mode 100644 tests/muscle3/code_parameters/xml/input/input_physics.xml create mode 100644 tests/muscle3/code_parameters/xml/input/input_physics.xsd create mode 100644 tests/muscle3/macro/Makefile create mode 100644 tests/muscle3/macro/test_macro.f90 create mode 100644 tests/muscle3/workflow/test.ymmsl create mode 100644 tests/muscle3/workflow/test_macro.ymmsl create mode 100644 tests/muscle3/wrapped_codes/cpp/basic/Makefile create mode 100644 tests/muscle3/wrapped_codes/cpp/basic/basic.cpp create mode 100644 tests/muscle3/wrapped_codes/cpp/basic/basic.h create mode 100644 tests/muscle3/wrapped_codes/cpp/parallel_mpi/Makefile create mode 100644 tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.cpp create mode 100644 tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.h create mode 100644 tests/muscle3/wrapped_codes/fortran/basic/Makefile create mode 100644 tests/muscle3/wrapped_codes/fortran/basic/basic.f90 create mode 100644 tests/muscle3/wrapped_codes/fortran/basic_parametrizable/Makefile create mode 100644 tests/muscle3/wrapped_codes/fortran/basic_parametrizable/basic.f90 create mode 100644 tests/muscle3/wrapped_codes/fortran/parallel_mpi/Makefile create mode 100644 tests/muscle3/wrapped_codes/fortran/parallel_mpi/parallel_mpi.f90 create mode 100644 tests/muscle3/wrapped_codes/python/basic/Makefile create mode 100644 tests/muscle3/wrapped_codes/python/basic/basic.py create mode 100644 tests/pytest.ini create mode 100755 tests/run-all-tests-master.sh create mode 100755 tests/run-all-tests.sh create mode 100755 tests/run-muscle3-tests.sh create mode 100644 tests/unit/generators/test_muscle3_cpp.py create mode 100644 tests/unit/generators/test_muscle3_fortran.py create mode 100644 tests/unit/generators/test_muscle3_python.py delete mode 100644 versioneer.py diff --git a/.gitattributes b/.gitattributes index da75c11..8b13789 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -iwrap/_version.py export-subst + diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..0f66308 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,23 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/_config.yml + fail_on_warning: false + +# Optionally set the Python requirements used to build documentation +python: + install: + - requirements: docs/requirements.txt + +# Explicitly specify submodules +submodules: + include: all diff --git a/MANIFEST.in b/MANIFEST.in index 83f20f2..f5b84e2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,4 @@ recursive-include bin * recursive-include iwrap * include requirements* include README.md -include LICENSE.md -include versioneer.py -include iwrap/_version.py \ No newline at end of file +include LICENSE.md \ No newline at end of file diff --git a/Makefile b/Makefile index 68bb2ec..4e94638 100755 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ all: iwrap_build install: install_dir install_iwrap install_module docs uninstall: uninstall_module uninstall_iwrap -.PHONY: build/module/$(MODULEFILE) install_iwrap update_iwrap iwrap_build build_deps build_deps_clear help clean docs +.PHONY: build/module/$(MODULEFILE) install_iwrap update_iwrap iwrap_build build_deps build_deps_clear help clean docs test-muscle3 build-muscle3-macro clean-muscle3 check_already_installed: @@ -104,6 +104,10 @@ help: install_dir @echo -e "\tINSTALL_MOD: [$(INSTALL_MOD)]" @echo -e "Version of the package - iWrap version:" @echo -e "\tVERSION: [$(VERSION)]" + @echo -e "\n- MUSCLE3-specific targets:" + @echo -e "\ttest-muscle3 : Run MUSCLE3 integration tests" + @echo -e "\tbuild-muscle3-macro : Build MUSCLE3 macro model for tests" + @echo -e "\tclean-muscle3 : Clean MUSCLE3 test artifacts" docs: @$(SHELL_SCRIPT) @@ -111,6 +115,33 @@ docs: code-check: pylint -E ./iwrap -clean: +# MUSCLE3-specific targets +test-muscle3: + @echo "Running MUSCLE3 integration tests..." + @if [ -f tests/run-muscle3-tests.sh ]; then \ + cd tests && ./run-muscle3-tests.sh; \ + else \ + echo "MUSCLE3 test script not found. Running pytest..."; \ + $(PY_CMD) -m pytest tests/muscle3/ -v -m muscle3 || echo "pytest not available or tests failed"; \ + fi + +build-muscle3-macro: + @echo "Building MUSCLE3 macro model for tests..." + @if [ -d tests/muscle3/macro ]; then \ + $(MAKE) -C tests/muscle3/macro; \ + else \ + echo "MUSCLE3 macro directory not found at tests/muscle3/macro"; \ + fi + +clean-muscle3: + @echo "Cleaning MUSCLE3 test artifacts..." + @$(RM) -rf tests/muscle3/run_* + @$(RM) -rf tests/muscle3/*/m3_actor + @$(RM) -rf tests/muscle3/actors/*/run_* + @$(RM) -rf tests/muscle3/actors/*/*/m3_actor + @$(RM) -rf tests/integration/muscle3/run_* + @echo "MUSCLE3 test artifacts cleaned" + +clean: clean-muscle3 rm -rf dist rm -rf build diff --git a/README.md b/README.md index 9e005d4..b028e92 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,21 @@ # iWrap +[![Read the Docs](https://img.shields.io/badge/docs-readthedocs-blue)](https://iwrap.readthedocs.io) + iWrap is a modular component generator, implemented in Python, used for creating IMAS actors from physics models. This mechanism allows to integrate physics codes written in one language (Fortran, CPP) within complex computing scenarios designed in other language (e.g. Python). It's plug-in based modular design with clear separation of concerns allows to generate various types of actors and easily change data access paradigm (from dataset descriptor for AL to direct HDC data for instance) +## Available Actor Types + +iWrap includes the following built-in actor generators: + +- **Python Actor**: Standard Python actors for straightforward Python integrations +- **MUSCLE3 Actors** (optional): High-performance multiscale coupling framework + - MUSCLE3-Python: Python code with MUSCLE3 coupling + - MUSCLE3-CPP: C++ code with MUSCLE3 coupling + - MUSCLE3-Fortran: Fortran code with MUSCLE3 coupling + For user conveniency it provides two kinds of interfaces: * user friendly graphical interface that allows non-experienced users to define an actor in intuitive way * command line interface foreseen for more advanced users that may want to e.g. automatise actor generation process using scripts. @@ -14,6 +26,35 @@ To learn about iWrap, please follow `tutorials/README.md` to generate an interac We have also prepared HTML and Docker versions, so you can choose the one that suits you best. +# Installation + +## Quick Install + +### Basic Installation (Core Only) +```bash +pip install iwrap +``` +This installs iWrap with the standard Python actor generator. + +### Installation with MUSCLE3 Support +```bash +pip install iwrap[muscle3] +``` +This enables MUSCLE3 actor generators (MUSCLE3-Python, MUSCLE3-CPP, MUSCLE3-Fortran). + +### Development Installation +```bash +pip install iwrap[all] +``` +Installs all optional dependencies including MUSCLE3. + +## Verify Installation + +List available actor types: +```bash +iwrap --list-actor-types +``` + # Configuration of working environment ## Downloading software diff --git a/ci/muscle3/build-macro-model.sh b/ci/muscle3/build-macro-model.sh new file mode 100755 index 0000000..1bdb4e5 --- /dev/null +++ b/ci/muscle3/build-macro-model.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +source ci/muscle3/setup-test-env.sh $* || exit 1 + +cd ./tests/macro +make all + diff --git a/ci/muscle3/make-docs.sh b/ci/muscle3/make-docs.sh new file mode 100755 index 0000000..1a0a7e0 --- /dev/null +++ b/ci/muscle3/make-docs.sh @@ -0,0 +1,30 @@ +#!/bin/bash + + +source ci/muscle3/setup-test-env.sh $@ || exit 1 + +echo "-----------Create Python Virtual ENV-------------" +# Remove Virtual env if already exists +rm -rf venv +python -m venv --system-site-packages venv +#python -m venv venv + +echo "-----------Activate Python Virtual ENV-------------" +source `pwd`/venv/bin/activate + +echo "-----------------PIP sphinx-rtd-theme------------" +pip install sphinx-rtd-theme + + +echo "--------------Report Python version--------------" +echo "Python version: `python --version`" +echo "Using python from: `which python`" + + +echo "--------------Build documentation--------------" +# PYTHONPATH set to allow sphinx find 'Read The Docs' theme +PYTHONPATH=`pwd`/venv/lib//python3.11/site-packages/:$PYTHONPATH make docs +# make docs + +echo "-----------Remove Python Virtual ENV-------------" +rm -rf venv \ No newline at end of file diff --git a/ci/muscle3/setup-test-env.sh b/ci/muscle3/setup-test-env.sh new file mode 100755 index 0000000..b20ddd2 --- /dev/null +++ b/ci/muscle3/setup-test-env.sh @@ -0,0 +1,54 @@ +#!/bin/bash + + +print_help() { + echo -e "Usage:" + echo -e "\t $0 [--use-installed|-i] [--help|-h]" + echo + echo -e "optional arguments:" + echo -e "\t -i, --use-installed use PIP installed or already loaded instance of plugins" + echo -e "\t -h, --help show this help message and exit" +} + + +use_installed=false + +while [ $# -gt 0 ]; do + case "$1" in + --use-installed|-i) + echo "WARNING: Using pre-loaded instance of plugins!" + use_installed=true + ;; + --help|-h) + print_help + exit 0 + ;; + *) + printf "ERROR: Incorrect parameter: \"$1\"\n" + print_help + # exit 1 + esac + shift +done + +# setup system environment +source ci/muscle3/st01-system-env.sh "$@" || exit 1 + +# look if iWrap has been already loaded / configured +if `which iwrap &> /dev/null` ; then + echo "WARNING: Using pre-loaded 'iWrap' from `which iwrap`!" + +else + echo "INFO: iWrap was not loaded! Loading 'iWrap' module!" + source ci/muscle3/st02-iwrap-load.sh "$@" || exit 1 +fi + +# check if 'local' or installed version should be used +if $use_installed ; then + echo "WARNING: Using pre-loaded instance of plugins!" +else + echo "INFO: Setting up plugins from local directory!" + source ci/muscle3/st03-m3plugins-local.sh "$@" || exit 1 +fi + + diff --git a/ci/muscle3/st00-defs.sh b/ci/muscle3/st00-defs.sh new file mode 100755 index 0000000..5523801 --- /dev/null +++ b/ci/muscle3/st00-defs.sh @@ -0,0 +1,19 @@ +# auxiliary functions definitions + + +function yell () +{ + echo "$0: $*" >&2 +} + +function die () +{ + yell "$*"; exit 1 +} + +function try () +{ + "$@" || die "cannot $*" +} + + diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh new file mode 100755 index 0000000..b580b5d --- /dev/null +++ b/ci/muscle3/st01-system-env.sh @@ -0,0 +1,46 @@ +# Set up environment +source ci-build/st00-defs.sh + +# Set up environment +source /usr/share/Modules/init/sh +module use /work/imas/etc/modules/all + +# Set up environment +echo "--------------Module load IMAS--------------" +if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL + + + try module load XMLlib/3.3.1-intel-compilers-2023.2.1 + + try module load MUSCLE3/0.7.1-intel-2023b + try module load IMAS/3.41.0-4.11.10-intel-2020b # <= No IMAS for intel 2023b + + + export CXX="icpc" + export FC="ifort" + export MPICXX="mpiicpc" + export MPIFC="mpiifort" + +else + + # GFORTRAN + try module load XMLlib/3.3.1-GCC-13.2.0 + try module load MUSCLE3/0.7.1-foss-2023b + try module load IMAS/3.41.0-4.11.10-foss-2023b + + export CXX="g++" + export FC="gfortran" + export MPICXX="mpicxx" + export MPIFC="mpifort" +fi + +if [[ ! -n $AL_VERSION ]]; then + export AL_VERSION=$UAL_VERSION +fi + +export AL_MAJOR="${AL_VERSION%.*.*}" + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../tests" &> /dev/null && pwd ) + +echo TESTS_DIR: $TESTS_DIR diff --git a/ci/muscle3/st02-iwrap-load.sh b/ci/muscle3/st02-iwrap-load.sh new file mode 100755 index 0000000..8d6503d --- /dev/null +++ b/ci/muscle3/st02-iwrap-load.sh @@ -0,0 +1,17 @@ +# Set up environment +source ci-build/st00-defs.sh + +# Set up environment +echo "--------------Module load iWrap--------------" +if [ "$COMPILER_VENDOR" == "intel" ]; then + + # INTEL + try module load iWrap/0.10.0-intel-2023b + +else + + # INTEL + try module load iWrap/0.10.0-GCCcore-13.2.0 + +fi + diff --git a/ci/muscle3/st03-m3plugins-local.sh b/ci/muscle3/st03-m3plugins-local.sh new file mode 100755 index 0000000..f96fe36 --- /dev/null +++ b/ci/muscle3/st03-m3plugins-local.sh @@ -0,0 +1,4 @@ + +export PYTHONPATH=${PWD}/iwrap_plugins:${PYTHONPATH} + + diff --git a/ci/muscle3/static-code-analysis.sh b/ci/muscle3/static-code-analysis.sh new file mode 100755 index 0000000..1783333 --- /dev/null +++ b/ci/muscle3/static-code-analysis.sh @@ -0,0 +1,10 @@ +#!/bin/sh --login +# Task: Setup Environment and Run Pylint code check. Publish report artifacts with python modules and imas env modules. +set -e + +source ci/muscle3/setup-test-env.sh "$@" || exit 1 +echo "--------------Module load Pylint--------------" +module load Pylint/3.2.5-GCCcore-13.2.0 +python -m pylint -E ./iwrap_plugins > pylint.log + + diff --git a/ci/muscle3/test-runner.sh b/ci/muscle3/test-runner.sh new file mode 100755 index 0000000..fa251d0 --- /dev/null +++ b/ci/muscle3/test-runner.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +source ci/muscle3/setup-test-env.sh $* || exit 1 + +cd $TEST_DIR + +echo "- - - - - TEST CASE: " $TEST_DIR " - - - - - - - - - - - - " +make test + diff --git a/docs/_toc.yml b/docs/_toc.yml index 9d5d78e..28cfa1c 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -7,9 +7,17 @@ root: iWrap_introduction.md parts: - caption: Welcome to Documentation chapters: + - file: documentation/quickstart.rst + title: Quick Start Guide - file: documentation/iWrap_intro.rst title: Users Manual sections: + - file: documentation/actor_types.rst + title: Actor Types Overview + - file: documentation/muscle3_actors.rst + title: MUSCLE3 Actor Generators + - file: documentation/muscle3_migration_guide.rst + title: MUSCLE3 Migration Guide - file: documentation/code_standardization.rst sections: - file: documentation/code_standardization/code_standardization_fortran.rst diff --git a/docs/documentation/actor_types.rst b/docs/documentation/actor_types.rst new file mode 100644 index 0000000..b2b5d7d --- /dev/null +++ b/docs/documentation/actor_types.rst @@ -0,0 +1,237 @@ +======================================== +Actor Types Overview +======================================== + +iWrap supports multiple actor generator types, each designed for specific use cases and integration scenarios. + +Available Actor Types +======================================== + +Built-in Generators +------------------- + +iWrap includes the following built-in actor generators: + +Standard Python Actor +~~~~~~~~~~~~~~~~~~~~~ + +**Type ID:** ``python`` + +**Description:** Standard Python actors for straightforward Python integrations with IMAS workflows. + +**Use Cases:** +- Simple Python-based physics codes +- Prototyping and development +- Direct IMAS IDS data access +- Python-to-Python workflows + +**Installation:** Included in core iWrap (no extra dependencies) + +**Documentation:** :doc:`project_description` + +MUSCLE3 Actors (Optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +MUSCLE3 (Multiscale Coupling Library and Environment) actors enable high-performance multiscale and multiphysics coupling scenarios. + +**Installation Required:** ``pip install iwrap[muscle3]`` + +Available MUSCLE3 Generators: + +**MUSCLE3-Python** + :Type ID: ``MUSCLE3-Python`` + :Language: Python + :Use Case: Python physics codes in MUSCLE3 workflows + :Documentation: :doc:`muscle3_actors` + +**MUSCLE3-CPP** + :Type ID: ``MUSCLE3-CPP`` + :Language: C++ + :Use Case: High-performance C++ codes with MUSCLE3 coupling + :Documentation: :doc:`muscle3_actors` + +**MUSCLE3-Fortran** + :Type ID: ``MUSCLE3-Fortran`` + :Language: Fortran + :Use Case: Fortran codes or legacy codes with MUSCLE3 integration + :Documentation: :doc:`muscle3_actors` + +Choosing an Actor Type +======================================== + +Decision Matrix +--------------- + +Use this matrix to help choose the appropriate actor type: + ++----------------------------+------------------+------------------------+ +| Requirement | Standard Python | MUSCLE3 Actors | ++============================+==================+========================+ +| Simple Python workflow | ✅ Best choice | ❌ Overkill | ++----------------------------+------------------+------------------------+ +| Multiscale coupling | ❌ Manual | ✅ Built-in | ++----------------------------+------------------+------------------------+ +| Multi-language workflow | ✅ Supported | ✅ Optimized | ++----------------------------+------------------+------------------------+ +| High-performance coupling | ⚠️ Manual setup | ✅ Built-in | ++----------------------------+------------------+------------------------+ +| Complex time scales | ❌ Manual | ✅ Built-in | ++----------------------------+------------------+------------------------+ +| Learning curve | ✅ Low | ⚠️ Medium | ++----------------------------+------------------+------------------------+ +| External dependencies | ✅ None | ⚠️ MUSCLE3 required | ++----------------------------+------------------+------------------------+ + +Recommendation Guidelines +-------------------------- + +**Choose Standard Python Actor if:** +- You have a simple Python-based physics code +- You're building a straightforward sequential workflow +- You don't need complex multiscale coupling +- You want minimal dependencies + +**Choose MUSCLE3 Actor if:** +- You need multiscale or multiphysics coupling +- Your workflow involves multiple time scales +- You need high-performance parallel coupling +- You're building complex coupled simulations +- You need standardized coupling interfaces + +Listing Available Actor Types +======================================== + +To see which actor types are available in your iWrap installation: + +.. code-block:: bash + + iwrap --list-actor-types + +Example output (core only): + +.. code-block:: text + + Id : Name : Description + ---------------------------------------------------------------------- + python : python : python + +Example output (with MUSCLE3): + +.. code-block:: text + + Id : Name : Description + ---------------------------------------------------------------------- + MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model + MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + python : python : python + +Getting Details About an Actor Type +==================================== + +Get detailed information about a specific actor type: + +.. code-block:: bash + + iwrap --list-actor-details python + iwrap --list-actor-details MUSCLE3-Python + +This will show: +- Supported code languages +- Supported data types +- API version +- Additional requirements + +Comparison Table +======================================== + +Feature Comparison +------------------ + ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| Feature | Python Actor | MUSCLE3-Python | MUSCLE3-CPP | MUSCLE3-Fortran | ++===========================+===================+===================+===================+===================+ +| **Actor Language** | Python | Python | Python | Python | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Code Language** | Python, | Python | C++ | Fortran | +| | Fortran, C++, Java| | | | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Coupling Framework** | Direct calls | MUSCLE3 | MUSCLE3 | MUSCLE3 | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Multiscale Support** | Manual | Built-in | Built-in | Built-in | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Init/Finalize IDS** | ✅ Allowed | ❌ Not allowed | ❌ Not allowed | ❌ Not allowed | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Workflow Description** | Python script | yMMSL | yMMSL | yMMSL | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Extra Dependencies** | None | MUSCLE3 | MUSCLE3 | MUSCLE3 | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ +| **Performance** | Good | Excellent | Excellent | Excellent | ++---------------------------+-------------------+-------------------+-------------------+-------------------+ + +Usage Examples +======================================== + +Generating Different Actor Types +--------------------------------- + +**Standard Python Actor:** + +.. code-block:: bash + + iwrap -a my_actor -t python -f code_description.yaml + +**MUSCLE3 Python Actor:** + +.. code-block:: bash + + iwrap -a my_actor -t MUSCLE3-Python -f code_description.yaml + +**MUSCLE3 C++ Actor:** + +.. code-block:: bash + + iwrap -a my_actor -t MUSCLE3-CPP -f code_description.yaml + +**MUSCLE3 Fortran Actor:** + +.. code-block:: bash + + iwrap -a my_actor -t MUSCLE3-Fortran -f code_description.yaml + +External Plugin Support +======================================== + +iWrap's plugin architecture allows for external actor generators to be developed and distributed separately. + +How External Plugins Work +-------------------------- + +1. External plugins are Python packages with the namespace ``iwrap_actor_generator`` +2. They are automatically discovered when installed +3. They appear alongside built-in generators + +Creating External Plugins +-------------------------- + +See :doc:`developers_manual/04_adding_generators` for details on creating custom actor generators. + +Installing External Plugins +---------------------------- + +External plugins can be installed via pip: + +.. code-block:: bash + + pip install your-custom-iwrap-plugin + +After installation, they will appear in the list of available actor types. + +See Also +======================================== + +- :doc:`muscle3_actors` - MUSCLE3 actor generators documentation +- :doc:`project_description` - Creating actor descriptions +- :doc:`actor_generation_cmdln` - Command-line usage +- :doc:`iwrap_gui` - Graphical interface +- :doc:`developers_manual/04_adding_generators` - Creating custom generators diff --git a/docs/documentation/installation_guide/iwrap_installation.rst b/docs/documentation/installation_guide/iwrap_installation.rst index b9622d7..3c79617 100644 --- a/docs/documentation/installation_guide/iwrap_installation.rst +++ b/docs/documentation/installation_guide/iwrap_installation.rst @@ -147,3 +147,72 @@ Load the module into the environment: iwrap-gui +iWrap Installation +================== + +Installation Options +-------------------- + +Basic Installation (Core Only) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Install iWrap core without MUSCLE3 support: + +.. code-block:: bash + + pip install iwrap + +This installs the basic iWrap functionality with the standard Python actor generator. + +Installation with MUSCLE3 Support +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To enable MUSCLE3 actor generators, install with the MUSCLE3 extra: + +.. code-block:: bash + + pip install iwrap[muscle3] + +Or alternatively: + +.. code-block:: bash + + pip install iwrap + pip install -r requirements_muscle3.txt + +This adds the following actor generators: +- MUSCLE3-Python: Generate Python actors with MUSCLE3 coupling +- MUSCLE3-CPP: Generate C++ actors with MUSCLE3 coupling +- MUSCLE3-Fortran: Generate Fortran actors with MUSCLE3 coupling + +Full Installation (All Features) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For development or to install all optional features: + +.. code-block:: bash + + pip install iwrap[all] + +This includes MUSCLE3 support and all other optional dependencies. + +Verifying Installation +~~~~~~~~~~~~~~~~~~~~~~ + +To verify which actor generators are available: + +.. code-block:: bash + + iwrap --list-actor-types + +Expected output with MUSCLE3 installed: + +.. code-block:: text + + Id : Name : Description + ---------------------------------------------------------------------- + MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model + MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + python : python : python + diff --git a/docs/documentation/muscle3_actors.rst b/docs/documentation/muscle3_actors.rst new file mode 100644 index 0000000..b99a7c2 --- /dev/null +++ b/docs/documentation/muscle3_actors.rst @@ -0,0 +1,328 @@ +======================================== +MUSCLE3 Actor Generators +======================================== + +Overview +======================================== + +iWrap includes built-in support for generating MUSCLE3 actors, enabling multiscale and multiphysics coupling scenarios. MUSCLE3 (Multiscale Coupling Library and Environment) is a high-performance coupling framework designed for complex scientific simulations. + +Available MUSCLE3 Generators +======================================== + +iWrap provides three MUSCLE3 actor generators: + +MUSCLE3-Python +-------------- +Generate Python actors with MUSCLE3 coupling capabilities. + +**Use case:** Python-based physics codes that need to participate in MUSCLE3 workflows. + +**Actor Type ID:** ``MUSCLE3-Python`` + +MUSCLE3-CPP +----------- +Generate C++ actors with MUSCLE3 coupling capabilities. + +**Use case:** High-performance C++ physics codes requiring MUSCLE3 integration. + +**Actor Type ID:** ``MUSCLE3-CPP`` + +MUSCLE3-Fortran +--------------- +Generate Fortran actors with MUSCLE3 coupling capabilities. + +**Use case:** Legacy Fortran codes or performance-critical Fortran implementations needing MUSCLE3 coupling. + +**Actor Type ID:** ``MUSCLE3-Fortran`` + +Installation +======================================== + +MUSCLE3 support requires the MUSCLE3 library to be installed. + +Installing iWrap with MUSCLE3 Support +-------------------------------------- + +.. code-block:: bash + + pip install iwrap[muscle3] + +This will install: +- iWrap core +- MUSCLE3 Python library (>= 0.7.0) +- Enable all three MUSCLE3 actor generators + +Verifying Installation +---------------------- + +Check that MUSCLE3 generators are available: + +.. code-block:: bash + + iwrap --list-actor-types + +Expected output: + +.. code-block:: text + + Id : Name : Description + ---------------------------------------------------------------------- + MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model + MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + python : python : python + +Usage +======================================== + +Generating MUSCLE3 Actors +-------------------------- + +Command Line +~~~~~~~~~~~~ + +Generate a MUSCLE3 Python actor: + +.. code-block:: bash + + iwrap -a my_actor -t MUSCLE3-Python -f code_description.yaml + +Generate a MUSCLE3 C++ actor: + +.. code-block:: bash + + iwrap -a my_actor -t MUSCLE3-CPP -f code_description.yaml + +Generate a MUSCLE3 Fortran actor: + +.. code-block:: bash + + iwrap -a my_actor -t MUSCLE3-Fortran -f code_description.yaml + +Graphical Interface +~~~~~~~~~~~~~~~~~~~ + +Launch iWrap GUI and select MUSCLE3 actor type from the dropdown: + +.. code-block:: bash + + iwrap-gui + +Then: +1. Select actor type: ``MUSCLE3-Python``, ``MUSCLE3-CPP``, or ``MUSCLE3-Fortran`` +2. Fill in code description +3. Generate actor + +Code Description Requirements +------------------------------ + +MUSCLE3 actors have specific requirements for code descriptions: + +**Restrictions:** +- INIT and FINALIZE methods **cannot** have IDS arguments +- Only the main STEP/RUN method can have IDS arguments + +**Example Code Description (YAML):** + +.. code-block:: yaml + + actor_description: + actor_name: my_muscle3_actor + actor_type: MUSCLE3-Python + + code_description: + implementation: + programming_language: python + code_path: /path/to/my_code.py + + subroutines: + init: + name: initialize + arguments: [] # No IDS arguments allowed + + step: + name: run_step + arguments: + - name: equilibrium + type: input + - name: core_profiles + type: output + + finalize: + name: cleanup + arguments: [] # No IDS arguments allowed + +Running MUSCLE3 Workflows +-------------------------- + +After generating MUSCLE3 actors, they need to be integrated into a MUSCLE3 workflow using yMMSL (YAML-based MUSCLE Simulation Language). + +**Example yMMSL Workflow:** + +.. code-block:: yaml + + ymmsl_version: v0.1 + + model: + name: my_simulation + components: + macro: + implementation: macro_model + ports: + o_i: core_profiles + s_o: equilibrium + + micro: + implementation: my_muscle3_actor # Generated by iWrap + ports: + f_init: equilibrium + o_f: core_profiles + + settings: + micro.time_step: 0.1 + macro.iterations: 100 + +Running the workflow: + +.. code-block:: bash + + muscle_manager workflow.ymmsl + +Differences from Standard Python Actors +======================================== + +MUSCLE3 actors differ from standard Python actors in several ways: + ++---------------------------+-------------------------+-------------------------+ +| Feature | Standard Python Actor | MUSCLE3 Actor | ++===========================+=========================+=========================+ +| Coupling Framework | Direct Python calls | MUSCLE3 messaging | ++---------------------------+-------------------------+-------------------------+ +| Workflow Description | Python script | yMMSL file | ++---------------------------+-------------------------+-------------------------+ +| Data Exchange | Direct IDS passing | MUSCLE3 ports | ++---------------------------+-------------------------+-------------------------+ +| Execution Model | Sequential/MPI | MUSCLE3 managed | ++---------------------------+-------------------------+-------------------------+ +| Init/Finalize Arguments | Can have IDS | No IDS allowed | ++---------------------------+-------------------------+-------------------------+ +| Multiscale Coupling | Manual | Built-in | ++---------------------------+-------------------------+-------------------------+ + +Best Practices +======================================== + +1. **Use MUSCLE3 for Multiscale Simulations** + + MUSCLE3 is designed for complex multiscale and multiphysics scenarios where different components run at different time scales. + +2. **Keep Init/Finalize Simple** + + Avoid complex data operations in init/finalize methods. Use them only for setup and cleanup. + +3. **Design Clear Port Interfaces** + + Plan your MUSCLE3 ports carefully to ensure clear data flow between components. + +4. **Test Components Independently** + + Before integrating into a MUSCLE3 workflow, test individual actors to ensure they work correctly. + +5. **Use yMMSL Validation** + + Validate your yMMSL files before running to catch configuration errors early. + +Troubleshooting +======================================== + +MUSCLE3 Not Found +----------------- + +**Error:** +:: + + ImportError: No module named 'muscle3' + +**Solution:** + +.. code-block:: bash + + pip install iwrap[muscle3] + +Or install MUSCLE3 separately: + +.. code-block:: bash + + pip install muscle3>=0.7.0 + +MUSCLE3 Generators Not Listed +------------------------------ + +**Problem:** MUSCLE3 generators don't appear in ``iwrap --list-actor-types`` + +**Possible Causes:** +1. MUSCLE3 library not installed +2. Wrong iWrap version (MUSCLE3 built-in since version X.X.X) + +**Solution:** + +.. code-block:: bash + + # Verify MUSCLE3 is installed + python -c "import muscle3; print(muscle3.__version__)" + + # Reinstall iWrap with MUSCLE3 + pip install --upgrade iwrap[muscle3] + +Init/Finalize IDS Argument Error +--------------------------------- + +**Error:** +:: + + ValueError: MUSCLE3 actor generator cannot handle INIT/FINALIZE methods with IDS arguments + +**Solution:** + +Remove IDS arguments from init and finalize methods in your code description YAML file. Only the main step/run method can have IDS arguments. + +Resources +======================================== + +- `MUSCLE3 Documentation `_ +- `MUSCLE3 GitHub Repository `_ +- `iWrap Examples with MUSCLE3 <../tutorial/>`_ + +Migration from External Plugin +======================================== + +If you were previously using ``iwrap-plugins-muscle3`` as an external package: + +1. **Uninstall old plugin:** + + .. code-block:: bash + + pip uninstall iwrap-plugins-muscle3 + +2. **Update iWrap installation:** + + .. code-block:: bash + + pip install --upgrade iwrap[muscle3] + +3. **Verify:** + + .. code-block:: bash + + iwrap --list-actor-types + +All existing YAML files and workflows remain compatible. No changes to your code descriptions are needed. + +See Also +======================================== + +- :doc:`../project_description` - Actor definition and generation +- :doc:`../actor_generation_cmdln` - Command-line interface +- :doc:`../iwrap_gui` - Graphical user interface +- :doc:`../developers_manual/04_adding_generators` - Creating custom generators diff --git a/docs/documentation/muscle3_migration_guide.rst b/docs/documentation/muscle3_migration_guide.rst new file mode 100644 index 0000000..6059796 --- /dev/null +++ b/docs/documentation/muscle3_migration_guide.rst @@ -0,0 +1,318 @@ +======================================== +Migration Guide: MUSCLE3 Plugin Integration +======================================== + +This guide helps users and developers migrate from the external ``iwrap-plugins-muscle3`` package to the new built-in MUSCLE3 generators in iWrap core. + +What Changed? +======================================== + +MUSCLE3 Actor Generators are Now Built-in +------------------------------------------ + +Previously, MUSCLE3 support was provided through a separate ``iwrap-plugins-muscle3`` package. +Now, MUSCLE3 generators are **built directly into iWrap core**, making installation and usage simpler. + +**Before:** + +.. code-block:: bash + + # Old installation (two packages) + pip install iwrap + pip install iwrap-plugins-muscle3 + +**After:** + +.. code-block:: bash + + # New installation (single package with optional feature) + pip install iwrap[muscle3] + +Benefits +-------- + +✅ **Simpler Installation:** One command instead of two packages + +✅ **Unified Version Management:** Core and MUSCLE3 generators always compatible + +✅ **Better Integration:** MUSCLE3 generators maintained alongside core + +✅ **Easier Updates:** Update both core and MUSCLE3 features together + +Migration Steps +======================================== + +For End Users +------------- + +**Step 1: Uninstall Old Plugin** + +.. code-block:: bash + + pip uninstall iwrap-plugins-muscle3 + +**Step 2: Update iWrap** + +.. code-block:: bash + + pip install --upgrade iwrap[muscle3] + +**Step 3: Verify** + +.. code-block:: bash + + iwrap --list-actor-types + +You should see all MUSCLE3 generators listed. + +**Step 4: Test Your Workflows** + +Your existing YAML files and workflows should work without changes: + +.. code-block:: bash + + # This should work exactly as before + iwrap -a my_actor -t MUSCLE3-Python -f my_code.yaml + +For Developers +-------------- + +**Step 1: Update Development Environment** + +.. code-block:: bash + + # Remove old plugin + pip uninstall iwrap-plugins-muscle3 + + # Install new version in development mode + cd /path/to/iwrap + pip install -e .[all] + +**Step 2: Update Import Statements (if you extended generators)** + +If you created custom generators based on MUSCLE3, update imports: + +**Before:** + +.. code-block:: python + + from iwrap_plugins.iwrap_actor_generator.muscle3_common import m3_utils + +**After:** + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_common import m3_utils + +**Step 3: Update Tests** + +Test discovery may need updates if you had custom tests: + +.. code-block:: python + + # Old test path + from iwrap_plugins.iwrap_actor_generator.muscle3_python import PythonActorGenerator + + # New test path + from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator + +For CI/CD Pipelines +------------------- + +**Update CI Configuration** + +**Before (.gitlab-ci.yml / .github/workflows):** + +.. code-block:: yaml + + install: + script: + - pip install iwrap + - pip install iwrap-plugins-muscle3 + +**After:** + +.. code-block:: yaml + + install: + script: + - pip install iwrap[muscle3] + +**Update Test Matrix** + +Consider testing with and without MUSCLE3: + +.. code-block:: yaml + + test-core: + script: + - pip install iwrap # Core only + - pytest -m "not muscle3" + + test-with-muscle3: + script: + - pip install iwrap[muscle3] + - pytest # All tests including MUSCLE3 + +Compatibility +======================================== + +Backward Compatibility +---------------------- + +✅ **YAML Files:** All existing code description YAML files work without changes + +✅ **Actor Types:** Same actor type IDs (``MUSCLE3-Python``, ``MUSCLE3-CPP``, ``MUSCLE3-Fortran``) + +✅ **Command Line:** Same CLI arguments and options + +✅ **Workflows:** Existing yMMSL workflow files work without changes + +✅ **Generated Actors:** Same actor structure and behavior + +API Version +----------- + +MUSCLE3 generators updated from API 2.0 to API 2.1 to match iWrap core. + +This is an internal change and should not affect end users. + +Breaking Changes +---------------- + +⚠️ **None for end users** + +⚠️ **For developers:** Import paths changed (see above) + +Troubleshooting +======================================== + +Issue: Both Old and New Installed +---------------------------------- + +**Symptom:** Conflicts or duplicate generators listed + +**Solution:** + +.. code-block:: bash + + # Remove both + pip uninstall iwrap iwrap-plugins-muscle3 + + # Reinstall fresh + pip install iwrap[muscle3] + +Issue: MUSCLE3 Generators Not Found +------------------------------------ + +**Symptom:** Only ``python`` generator listed, no MUSCLE3 + +**Check:** + +.. code-block:: bash + + # Verify MUSCLE3 extra was installed + pip show iwrap + + # Look for: Requires: muscle3>=0.7.0 + +**Solution:** + +.. code-block:: bash + + pip install muscle3>=0.7.0 + # OR + pip install --upgrade iwrap[muscle3] + +Issue: Import Errors in Custom Code +------------------------------------ + +**Symptom:** ``ModuleNotFoundError: No module named 'iwrap_plugins'`` + +**Cause:** Custom code still using old import paths + +**Solution:** Update imports as shown in "For Developers" section + +Frequently Asked Questions +======================================== + +Can I still use the old plugin? +-------------------------------- + +No. The ``iwrap-plugins-muscle3`` package is deprecated and will not receive updates. +Please migrate to the built-in MUSCLE3 generators. + +Do I need to change my YAML files? +----------------------------------- + +No. All existing code description YAML files work without any changes. + +Will my old workflows break? +----------------------------- + +No. Your yMMSL workflow files and Python scripts work without changes. + +What if I don't need MUSCLE3? +------------------------------ + +Simply install iWrap without the ``[muscle3]`` extra: + +.. code-block:: bash + + pip install iwrap + +MUSCLE3 generators won't be available, but core functionality works perfectly. + +Can I install MUSCLE3 support later? +------------------------------------- + +Yes: + +.. code-block:: bash + + # Install core first + pip install iwrap + + # Add MUSCLE3 support later + pip install muscle3>=0.7.0 + +What about future plugins? +--------------------------- + +iWrap still supports external plugins! The plugin discovery mechanism is unchanged. +Future plugin developers can still create ``iwrap_actor_generator`` namespace packages. + +Timeline and Support +======================================== + +Release Timeline +---------------- + +- **iWrap v0.7.x and earlier:** External ``iwrap-plugins-muscle3`` required +- **iWrap v0.8.0+:** MUSCLE3 built-in, plugin deprecated +- **Future:** Plugin package will be archived (read-only) + +Support Policy +-------------- + +- **Built-in MUSCLE3:** Fully supported, actively maintained +- **External plugin:** Deprecated, no new features, critical bugs only +- **After 6 months:** External plugin unsupported + +Getting Help +======================================== + +If you encounter issues during migration: + +1. Check this migration guide +2. Review the :doc:`muscle3_actors` documentation +3. Check existing issues on the iWrap repository +4. Contact: iWrap Development Team + +Additional Resources +======================================== + +- :doc:`muscle3_actors` - Complete MUSCLE3 documentation +- :doc:`installation_guide/iwrap_installation` - Installation instructions +- :doc:`developers_manual/04_adding_generators` - Creating custom generators +- `MUSCLE3 Documentation `_ diff --git a/docs/documentation/muscle3_plugins/code_wrapping.rst b/docs/documentation/muscle3_plugins/code_wrapping.rst new file mode 100644 index 0000000..8425e14 --- /dev/null +++ b/docs/documentation/muscle3_plugins/code_wrapping.rst @@ -0,0 +1,458 @@ +.. sectnum:: + +.. toctree:: + :numbered: + :maxdepth: 10 + +MUSCLE3 actor generators requirements +####################################################################################################################### +Following software must be available to wrap the code into MUSCLE3 models: + +- *iWrap* - MUSCLE3 actors generators are provided as iWrap plugins, thus they cannot be used without iWrap +- *MUSCLE3 libraries* - indispensable for building actors - should be available via ``pkg-config`` mechanism +- *MUSCLE3 actor generator plugins* - all paths to generator packages must be listed on the ``PYTHONPATH`` + +.. note:: + Usually operation system can be configured using ``modules`` toolkit, so in most cases loading ``iwrap4paf`` + is enough to configure user working environment. + + **Warning**: Module name(s) may vary depending on platform! + +The code to be wrapped +####################################################################################################################### +Physics codes, to be used for generation of an actor can be of arbitrary content but must follow the standardized interface (API) +introduced with iWrap: a mandatory main function and optional ones for the initialization, +the finalization, for saving/restoring internal state and for getting the current timestamp. + +.. warning:: + The INIT and FINALIZE methods of the provided code cannot have IDS arguments, to be wrapped to MUSCLE3 model. + +.. note:: + To keep compatibility of the native code with other kinds of generators, the code itself should be ‘MUSCLE3 agnostic’: + no MUSCLE3 methods should be called from within a wrapped code. + +Actor generation +####################################################################################################################### + +To generate MUSCLE3 actor, `muscle3-` needs to be chosen out of all available actor types. +it can be done using either iwrap command line, YAML describing an actor or iWrap GUI. + +.. code-block:: console + + shell> iwrap --actor-type muscle3- -f code_description.yaml + +.. code-block:: YAML + :caption: Actor description + :emphasize-lines: 3 + + actor_description: + ... + actor_type: muscle3- # Actor type set to MUSCLE3 actor + ... + + code_description: + ... + +.. tip:: + To help developers write MUSCLE3-compliant code, the iWrap MUSCLE3 plug-in allows you + to generate so-called **actor's skeleton**. + The **actor's skeleton** is a source code for a given actor without embedding actual calls to native code. + All places where subroutines from the code could be introduced are clearly marked with comments + in the generated source code. + To generate this kind of the source code, simply leave out (or comment out the `programming_language` entry + of code description + +MUSCLE3 actor +####################################################################################################################### + +Created actor contains a native code placed into a standalone executable `.exe` that acts +as a MUSCLE3 function. + +A generated code intermediates between a wrapped code and MUSCLE3 library. It's main functionality includes: + +- Management of inter-model communication: + + - deserialization of IDSes received from MUSCLE3 and passing them to wrapped code + - serialization of IDSes returned by wrapped code and sending them using MUSCLE3 +- Handling actor checkpointing and restarting + +Physical time handling +========================================================================================= + +MUSCLE3 supports sending and receiving timestamped messages, which can help support consistent physical time handling +throughout the coupled simulation. Providing timestamps is optional, however, for checkpoint support it is required. +IDSes are sent in separate messages, using dedicated channels: + +- If code provides the GET_TIMESTAMP method, (single) timestamp obtained from this method will be used + for all output IDS messages (this is the time of the model, time in individual IDS may be slightly smaller, + but never larger). + +- If GET_TIMESTAMP method is not available, while sending particular IDS: + + - if IDS/time is allocated - IDS/time[last_index] will be used as message timestamp + (note: heterogeneous can also have that filled as the "common" time for this IDS) + - if IDS/time is not allocated - timestamp will be set to invalid value + + + +iWrap MUSCLE3 actor control flow +========================================================================================= +Basic MUSCLE3 control flow could be briefly described as follows: once the MUSCLE3 +and the wrapped code are initialized, at every iteration of the MUSCLE3 reuse loop, +the actor will receive data from other models, call the ‘main’ method of the wrapped model +and send data computed by mode. Computations finish with the clean up operations. + +#. Initialisation: + + a. MUSCLE3 system initialisation + #. Model initialisation - a model INIT method is called (if provided) + +#. A main ‘MUSCLE3 controlled’ loop: + + a. The actor receives input IDSes from interoperating MUSCLE3 models + #. The MAIN method of the wrapped code is called + #. Sending output IDSes to interoperating MUSCLE3 models + +#. Finalisation + + a. Model finalization: a model finish method is called (if provided) + #. MUSCLE3 system cleanup + +.. code-block:: fortran + :caption: MUSCLE3 actor structure (simplified Fortran-like pseudo code) + + > > > ENVIRONMENT INITIALIZATION < < < + CALL init_muscle(instance) + CALL wrapped_code_init([code_params]) ! called if init provided + + ! > > > MUSCLE3 LOOP. < < < + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + + ! > > > RECEIVING INPUT IDSes < < < + CALL receive_input_idses(instance, ids1_in, ids2_in, ...) + + ! > > > WRAPPED CODE < < < + CALL wrapped_code(ids1_in, ids2_in, ..., ids1_out, ids2_out, ...[, code_params]) + + ! > > > SENDING OUTPUT IDSes < < < + ! if GET_TIMESTAMP method provided + timestamp = get_timestamp_from_wrapped() + ! else obtained from IDS time vector, while sending particular IDS + + CALL send_output_idses(instance, ids1_out, ids2_out, ..., timestamp) + + end do ! MUSCLE3 loop + + ! > > > FINALIZATION < < < + CALL wrapped_code_finalize() ! called if finalize provided + CALL finalize_muscle(instance) + + +Wrapping of MPI codes +========================================================================================= + +Since the actor doesn’t know how the model distributes its work over the processes, +it is the wrapped code duty to properly communicate the inputs to the other MPI processes, +run computations, collect the results, and return them in the root process. + +* The MAIN function (see iWrap model basic API) is called on all MPI processes and all processes + received valid IDSes. In contrary, the result of the main function is ignored in all non-root processes. + +* The INIT and FINALIZE functions are called on all MPI processes. + +* The GET_STATE and SET_STATE functions are called on all MPI processes. However, + only the root process will receive a non-empty input for SET_STATE. Similarly, + the result of the GET_STATE function is ignored in all non-root processes. + +Note that, even though each process in the template below calls `receive_idses`, +only the root process actually receives data. The other processes receive a dummy message, +thereby acting as a barrier without relying on a spinloop. For a detailed explanation of the MUSCLE3 API with MPI, +we refer to the `MUSCLE3 documentation `_ +To overcome this issue a wrapping code broadcasts every IDS from root to all non-root processes. + +.. warning:: + MUSCLE3 provides MPI support only for C++ and Fortran, so the iWrap generator handles MPI only in these languages. + +.. code-block:: fortran + :caption: Generated MUSCLE3 MPI actor structure (simplified Fortran-like pseudo code) + + > > > ENVIRONMENT INITIALIZATION < < < + CALL mpi_initialization(mpi_rank) + CALL init_muscle(instance) + CALL wrapped_code_init([code_params]) ! called if init provided + + ! > > > MUSCLE3 LOOP. < < < + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + + ! > > > RECEIVING INPUT IDSes < < < + CALL receive_idses(instance, ids1_in, ids2_in, ...) + + if (mpi_rank == 0) then + ! Broadcast all IDSes to non-root processes + endif + + ! > > > WRAPPED CODE < < < + CALL wrapped_code(ids1_in, ids2_in, ..., ids1_out, ids2_out, ..., [, code_params]) + + if (mpi_rank == 0) then + ! > > > SENDING OUTPUT IDSes < < < + ! if GET_TIMESTAMP method provided + timestamp = get_timestamp_from_wrapped() + ! ELSE obtained from IDS time vector, while sending particular IDS + + CALL send_ids(instance, ids1_out, ids2_out, ..., timestamp) + endif + + end do ! MUSCLE3 loop + + ! > > > FINALIZATION < < < + CALL wrapped_code_finalize() ! called if finalize provided + CALL finalize_muscle(instance) + CALL mpi_finalization() + + +The restart feature +========================================================================================= +The wrapped code is run ‘atomically’, so no interaction between an actor and native method is possible +(the actor cannot force the model method to save a checkpoint at an arbitrary time, while it is executed). +Additionally, the wrapped methods are ‘MUSCLE3 agnostic’ - no calls of MUSCLE3 checkpointing methods +are available within the wrapped code. Nevertheless, to support stateful, compute demanding codes, +iWrap generated actors offers the possibility of a smooth restart of the model without losing +results obtained before error/crash occurred, based on MUSCLE3 checkpointing mechanism. + +To enable this feature, the wrapped code MUST provide both a method `GET_STATE` returning information +describing the code internal state, and a method `SET_STATE` restoring the state. + + +.. code-block:: fortran + :caption: Generated MUSCLE3 actor structure with restart enabled (simplified Fortran-like pseudo code) + + logical :: initial_run = .TRUE. + + > > > ENVIRONMENT INITIALIZATION < < < + CALL mpi_initialization(mpi_rank) + CALL init_muscle(instance) + + ! > > > MUSCLE3 LOOP. < < < + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + + ! - - - WRAPPED CODE INITIALIZATION - - - # # # # + if ( LIBMUSCLE_Instance_resuming(instance)) then + ! > > > RESTART : the code is restored from the saved snapshot < < < + if (process_rank == MPI_ROOT_RANK) then + CALL restore_wrapped_code_state(instance) + endif + else ! m3_instance is not resuming + ! > > > INITIALIZATION by calling INIT method of the code < < < + if (initial_run) then + CALL wrapped_code_init([code_params]) ! called if init provided + endif + endif ! LIBMUSCLE_Instance_resuming + initial_run = .FALSE. + + if( LIBMUSCLE_Instance_should_init(instance)) then + ! > > > RECEIVING INPUT IDSes < < < + CALL receive_idses(instance, ids1_in, ids2_in, ...) + endif ! LIBMUSCLE_Instance_should_init + + ! > > > WRAPPED CODE < < < + CALL wrapped_code(ids1_in, ids2_in, ..., ids1_out, ids2_out, ..., [, code_params]) + + if (mpi_rank == MPI_ROOT_RANK) then + ! > > > SENDING OUTPUT IDSes < < < + ! if GET_TIMESTAMP method provided + timestamp = get_timestamp_from_wrapped() + ! ELSE obtained from IDS time vector, while sending particular IDS + + CALL send_ids(instance, ids1_out, ids2_out, ..., timestamp) + endif + + ! > > > SAVE SNAPSHOT < < < + if (LIBMUSCLE_Instance_should_save_final_snapshot(instance)) then + if (process_rank == MPI_ROOT_RANK ) then + CALL save_wrapped_code_state(instance) + endif + endif + + end do ! < < < MUSCLE3 LOOP < < < + + ! > > > FINALIZATION < < < + CALL wrapped_code_finalize() ! called if finalize provided + CALL finalize_muscle(instance) + CALL mpi_finalization() + +MUSCLE3 models coupling and running +####################################################################################################################### +Models, either wrapped by iWrap or prepared by user in any other way, have to be coupled manually +by workflow/scenario designer following MUSCLE3 standard guidelines and rules. + + +MUSCLE3 models coupling +========================================================================================= +MUSCLE3 uses the Multiscale Modelling and Simulation Language (MMSL) to describe +the structure of a multiscale model. MMSL can be expressed as a YAML file (yMMSL). +The MMSL lets one describe which components (submodels, scale bridges, data converters, UQ components, etc.) +a multiscale model consist of, how many instances of each we need, and how they are wired together. + +Following asumptions and limitations should be taken into consideration while designing a MUSCLE3 scenario: + + - Communication is managed (only) by a wrapper + - Actor communicates with other model(s) by sending/receiving IDSes + - Every message contains only one IDS + - IDSes are received via port of `F_INIT` type + - IDSes are sent via port of `O_F` type + - Every port handles only one in/out argument (IDS) + - Port number and names correspond to argument of wrapped code + - MPI: data are sent and received only by the root MPI process + +Changing code parameters of an actor +========================================================================================= +Setting ``.parameters_file`` property in yMMLS file (see example below) +forces an actor to read parameters from the pointed file and not from the default one. + +.. warning:: + - Only absolute path to code parameters file can be specified + - No system variables can be a part of specified path + - The code parameters read from the file must conform a code parameter schema provided by a code developer at the actor generation stage. + +.. code-block:: YAML + :caption: An example of a MUSCLE3 yMMLS workflow description + :emphasize-lines: 7,8 + + model: + name: helloworld + components: + macro: macro + micro: micro + ... + settings: + micro.parameters_file: /path/to/parameters.xml + +Provenance info +========================================================================================= + +YAML description +--------------------- +In order to provide additional information about actor configuration, actor/code description YAML file is being copied into actor's directory during generation process. + +Code parameters +--------------------- +Setting ``copy_xml_parameters`` property in yMMLS file (see example below) +forces an actor to save code parameters XML file in workdir during execution. If not set, parameters won't be saved. + +.. code-block:: YAML + :caption: An example of a MUSCLE3 yMMLS workflow description + :emphasize-lines: 7,8 + + model: + name: helloworld + components: + macro: macro + micro: micro + ... + settings: + copy_xml_parameters: true + + +Running MUSCLE3 scenario +========================================================================================= +An yMMSL description of the computing scenario needs to be run using `muscle_manager` +- the central run-time component of MUSCLE3. It is started together with the component +intances, and provides a central coordination point that the instances use to find each other. +The manager also collects log messages from the individual instances to aid in debugging, +and some profiling information to aid in scheduling and performance optimisation. + +.. note:: Further reading: + + - `yMMSL overview `_ + - `MUSCLE3 tutorial `_ + +Example +####################################################################################################################### +This example shows how to prepare and run an MUSCLE3 computing scenario, consisting of two codes +(aka. MUSCLE3 models), communicating with each other by sending/receiving IDSes: + +- prepared manually *MUSCLE3 macro model* +- a native code wrapped automatically by iWrap into *MUSCLE3 function* + +.. image:: ../../images/muscle3/macro-actor.png + + +Wrapping code into MUSCLE3 function +========================================================================================= + +A code to be wrapped is a very simple Fortran subroutine, obtaining `core_profiles` IDS as input +and returning `distribution_sources` IDS. To be compatible with 'iWrap standardized API' argument list +contains also `status_code` and `status_message`. + +.. literalinclude:: /resources/example/wrapped_code.f90 + :language: fortran + :caption: wrapped_code.f90 + +The wrapped code has to be compiled and packed into a static library. + +.. code-block:: console + :caption: Building wrapped code + + gfortran -c -o wrapped_code.o wrapped_code.f90 `pkg-config --cflags imas-gfortran` + @ar -rcs libwrapped_code.a wrapped_code.o + +Once a static library is built, the code description has to to be prepared, +to provide iWrap with all information required to generate an actor. + +.. literalinclude:: /resources/example/code_description.yaml + :language: YAML + :caption: code_description.yaml + +To generate an actor, one has to call `iwrap` command providing an actor type (`muscle3`), +an arbitrary actor name and YAML file containing the code description. + +.. code-block:: console + :caption: Actor generation + + iwrap --actor-type muscle3 --actor-name m3_actor -f ./code_description.yaml + +If generation was successful, an executable `m3_actor.exe` containing user provided subroutine +wrapped into MUSCLE3 function, is created in `/m3_actor/bin/` directory. + +.. image:: ../../images/muscle3/actor-ports.png +.. note:: + Please notice, that ports of generated actor have exactly the same names + as they have been defined in code description YAML + +.. literalinclude:: /resources/example/standalone.f90 + :language: fortran + :caption: Autogenerated code + +Macro model +========================================================================================= +A `macro model` code has to be prepared manually. This code "triggers" a generated actor +sending to it `core_profiles` IDS and receiving back computed `distribution_sources` IDS + +.. literalinclude:: /resources/example/macro.f90 + :language: fortran + :caption: macro.f90 + +.. code-block:: console + :caption: Building macro model + + gfortran -o macro.exe macro.f90 `pkg-config --cflags --libs imas-gfortran ymmsl libmuscle_fortran` + + +Scenario description +========================================================================================= + + +.. literalinclude:: /resources/example/example.ymmsl + :language: YAML + :caption: example.ymmsl + + +Launching an example +========================================================================================= + +.. code-block:: console + :caption: Running an example + + muscle_manager --start-all example.ymmsl + diff --git a/docs/documentation/muscle3_plugins/example/Makefile b/docs/documentation/muscle3_plugins/example/Makefile new file mode 100644 index 0000000..5aa450b --- /dev/null +++ b/docs/documentation/muscle3_plugins/example/Makefile @@ -0,0 +1,19 @@ +.PHONY: native + +native: + gfortran -g -c -o wrapped_code.o wrapped_code.f90 `pkg-config --cflags imas-gfortran` + @ar -rcs libwrapped_code.a wrapped_code.o + +actor: + iwrap --actor-type muscle3 --actor-name m3_actor -f ./code_description.yaml + +macro: + gfortran -g -o macro.exe macro.f90 `pkg-config --cflags --libs imas-gfortran ymmsl libmuscle_fortran` + + +run: + EXAMPLE_DIR=`pwd` muscle_manager --start-all example.ymmsl + +clean: + rm -rf *.o *.exe *.mod *.a run_* + diff --git a/docs/documentation/muscle3_plugins/example/code_description.yaml b/docs/documentation/muscle3_plugins/example/code_description.yaml new file mode 100644 index 0000000..e746873 --- /dev/null +++ b/docs/documentation/muscle3_plugins/example/code_description.yaml @@ -0,0 +1,22 @@ +code_description: + implementation: + subroutines: + init: wrapped_code_init + main: wrapped_code_main + finalize: wrapped_code_finalise + programming_language: Fortran + data_type: legacy + code_path: ./libwrapped_code.a + include_path: ./mod_wrapped_code.mod + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + settings: + compiler_cmd: gfortran + + + diff --git a/docs/documentation/muscle3_plugins/example/example.ymmsl b/docs/documentation/muscle3_plugins/example/example.ymmsl new file mode 100644 index 0000000..aa9d914 --- /dev/null +++ b/docs/documentation/muscle3_plugins/example/example.ymmsl @@ -0,0 +1,30 @@ +ymmsl_version: v0.1 + +model: + name: example + components: + macro: macro + micro: micro + conduits: + macro.core_profiles_out: micro.core_profiles_in + micro.distribution_sources_out: macro.distribution_sources_in +settings: + t_max: 2.0 + dt: 1.0 + muscle_remote_log_level: DEBUG + +implementations: + micro: + executable: ${IWRAP_ACTORS_DIR}/m3_actor/bin/m3_actor.exe + + macro: + executable: ${EXAMPLE_DIR}/macro.exe + +resources: + macro: + threads: 1 + + micro: + threads: 1 + + diff --git a/docs/documentation/muscle3_plugins/example/macro.f90 b/docs/documentation/muscle3_plugins/example/macro.f90 new file mode 100644 index 0000000..96e4f7d --- /dev/null +++ b/docs/documentation/muscle3_plugins/example/macro.f90 @@ -0,0 +1,83 @@ +program macro + use ids_routines + use ymmsl + use libmuscle + implicit none + + type (ids_core_profiles) :: core_profiles_out + type (ids_distribution_sources) :: distribution_sources_in + + real (selected_real_kind(15)) :: t_cur, t_next, t_max, dt + character(len=1), dimension(:), allocatable :: serialized_ids + + type(LIBMUSCLE_PortsDescription) :: ports + type(LIBMUSCLE_Instance) :: instance + + type(LIBMUSCLE_Message) :: rmsg + type(LIBMUSCLE_DataConstRef) :: ritem + + type(LIBMUSCLE_Message) :: smsg + type(LIBMUSCLE_Data) :: sitem + + + ! > > > - - - INITIALISATION - - - < < < + ports = LIBMUSCLE_PortsDescription_create() + call LIBMUSCLE_PortsDescription_add(ports, YMMSL_OPERATOR_O_I, 'core_profiles_out') + call LIBMUSCLE_PortsDescription_add(ports, YMMSL_OPERATOR_S, 'distribution_sources_in') + instance = LIBMUSCLE_Instance_create(ports) + call LIBMUSCLE_PortsDescription_free(ports) + + ! > > > - - - MAIN MUSCLE3 LOOP - - - < < < + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + ! F_INIT + t_max = LIBMUSCLE_Instance_get_setting_as_real8(instance, 't_max') + dt = LIBMUSCLE_Instance_get_setting_as_real8(instance, 'dt') + + t_cur = 0.0 + + ! > > > - - - INTERNAL LOOP - - - < < < + do while (t_cur + dt < t_max) + ! O_I + t_next = t_cur + dt + + + ! > > > - - - MODEL COMPUTATIONS - - - < < < + + core_profiles_out%ids_properties%homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS + allocate(core_profiles_out%time(1)) + core_profiles_out%time(1) = 1.0 + + ! . . . + ! > > > - - - - - - - - - - - - - - - < < < + + ! > > > - - - SENDING DATA - - - < < < + call ids_serialize(core_profiles_out, serialized_ids) + call ids_deallocate(core_profiles_out) + + sitem = LIBMUSCLE_Data_create_byte_array(serialized_ids) + smsg = LIBMUSCLE_Message_create(t_cur, sitem) + + call LIBMUSCLE_Instance_send(instance, 'core_profiles_out', smsg) + call LIBMUSCLE_Message_free(smsg) + call LIBMUSCLE_Data_free(sitem) + deallocate(serialized_ids) + + + ! > > > - - - RECEIVING DATA - - - < < < + rmsg = LIBMUSCLE_Instance_receive(instance, 'distribution_sources_in') + ritem = LIBMUSCLE_Message_get_data(rmsg); + + allocate(serialized_ids(LIBMUSCLE_DataConstRef_size(ritem))) + call LIBMUSCLE_DataConstRef_as_byte_array(ritem, serialized_ids) + call LIBMUSCLE_DataConstRef_free(ritem) + call LIBMUSCLE_Message_free(rmsg) + + call ids_deserialize(serialized_ids, distribution_sources_in) + deallocate(serialized_ids) + call ids_deallocate(distribution_sources_in) + + t_cur = t_cur + dt + end do + end do + +end program macro \ No newline at end of file diff --git a/docs/documentation/muscle3_plugins/example/standalone.f90 b/docs/documentation/muscle3_plugins/example/standalone.f90 new file mode 100644 index 0000000..825f063 --- /dev/null +++ b/docs/documentation/muscle3_plugins/example/standalone.f90 @@ -0,0 +1,62 @@ +program standalone + + use iwrap_tools + use muscle3_tools + use mod_wrapped_code + + implicit none + + !--------------------------------------------------------- + character(len=*), parameter :: ACTOR_NAME = "m3_actor" + type(LIBMUSCLE_Instance) :: instance + integer :: process_rank + + + !---- Status info ---- + integer :: status_code = 0 + character(len=:), pointer :: status_message + + !---- Code parameters ---- + character(len=:), allocatable :: xml_string + type(ids_parameters_input) :: imas_code_params + + !---- IN/OUT IDSes ---- + ! core_profiles_in + type (ids_core_profiles) :: core_profiles_in + ! distribution_sources_out + type (ids_distribution_sources) :: distribution_sources_out + + ! > > > - - - INITIALISATION - - - < < < + CALL init_muscle(instance) + + ! > > > - - - WRAPPED CODE CALL - INIT SBRT - - - < < < + CALL wrapped_code_init( status_code, status_message) + CALL check_status(instance, status_code, status_message, ACTOR_NAME) + + ! > > > MUSCLE3 LOOP. < < < + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + + ! > > > RECEIVING INPUT IDSes < < < + CALL recveive_input_idses(core_profiles_in, instance ) + + ! > > > - - - WRAPPED CODE CALL - MAIN SBRT - - - < < < + CALL wrapped_code_main( core_profiles_in, distribution_sources_out, status_code, status_message) + + !-----------Check status info --------------------- + CALL check_status(instance, status_code, status_message, ACTOR_NAME) + + ! > > > SENDING OUTPUT IDSes < < < + CALL send_output_idses(distribution_sources_out, instance) + + ! > > > CLEAN UP < < < + CALL ids_deallocate(core_profiles_in) + CALL ids_deallocate(distribution_sources_out) + + end do ! < < < MUSCLE3 LOOP < < < + + ! > > > - - - - - - - - - - - - - WRAPPED CODE CALL - FINALISE SBRT - - - - - - - - - - - - - - - - - - < < < + CALL wrapped_code_finalise(status_code, status_message) + !-----------Check status info --------------------- + CALL check_status(instance, status_code, status_message, ACTOR_NAME) + +end program \ No newline at end of file diff --git a/docs/documentation/muscle3_plugins/example/wrapped_code.f90 b/docs/documentation/muscle3_plugins/example/wrapped_code.f90 new file mode 100644 index 0000000..dc2f540 --- /dev/null +++ b/docs/documentation/muscle3_plugins/example/wrapped_code.f90 @@ -0,0 +1,71 @@ +module mod_wrapped_code +contains + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine wrapped_code_init(status_code, status_message) + use ids_schemas, only: ids_parameters_input + implicit none + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + write(*,*) '=======================================' + write(*,*) 'Wrapped code: INITIALISATION called' + write(*,*) '=======================================' + + end subroutine wrapped_code_init + + + ! + ! MAIN SUBROUTINE + ! + subroutine wrapped_code_main(coreprofilesin, distsourceout, error_flag, error_message) + use ids_schemas + implicit none + + type (ids_core_profiles) :: coreprofilesin + type (ids_distribution_sources) :: distsourceout + integer, intent(out) :: error_flag + character(len=:), pointer, intent(out) :: error_message + + write(*,*) '=======================================' + write(*,*) 'Wrapped code: MAIN called' + write(*,*) '=======================================' + + !COMPUTATIONS + allocate(distsourceout%time(1)) + distsourceout%time(1) = coreprofilesin%time(1) + 1 + distsourceout%ids_properties%homogeneous_time = 1 + + return + end subroutine wrapped_code_main + + ! + ! FINALISATION SUBROUTINE + ! + subroutine wrapped_code_finalise(status_code, status_message) + implicit none + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + write(*,*) '=======================================' + write(*,*) 'Wrapped code: FINALISATION called' + write(*,*) '=======================================' + + end subroutine wrapped_code_finalise + +end module mod_wrapped_code + + diff --git a/docs/documentation/muscle3_plugins/examples.rst b/docs/documentation/muscle3_plugins/examples.rst new file mode 100644 index 0000000..79adcdd --- /dev/null +++ b/docs/documentation/muscle3_plugins/examples.rst @@ -0,0 +1,338 @@ +MUSCLE3 Examples +================ + +This section provides complete working examples of MUSCLE3 actor generation +and usage with iWrap. + +Basic Fortran Example +--------------------- + +This example demonstrates wrapping a simple Fortran code as a MUSCLE3 actor. +The example shows a complete workflow from code description to running a +coupled simulation. + +Example Files +~~~~~~~~~~~~~ + +All example files are located in ``docs/documentation/muscle3_plugins/example/``: + +* ``code_description.yaml`` - Actor description for iWrap +* ``example.ymmsl`` - MUSCLE3 workflow configuration +* ``macro.f90`` - Macro model (orchestrator) +* ``standalone.f90`` - Original standalone code +* ``wrapped_code.f90`` - Code prepared for wrapping +* ``Makefile`` - Build instructions + +Code Description +~~~~~~~~~~~~~~~~ + +The code description YAML file defines the actor interface: + +.. literalinclude:: example/code_description.yaml + :language: yaml + :caption: code_description.yaml + +The code description defines: + +* **Actor name and type**: Identifies the actor and generator to use +* **Input/output ports**: Defines data exchange interfaces +* **Code parameters**: Configuration options for the wrapped code +* **Implementation details**: Language, source files, build options + +Workflow Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +The yMMSL file defines the coupled simulation workflow: + +.. literalinclude:: example/example.ymmsl + :language: yaml + :caption: example.ymmsl + +The yMMSL file defines: + +* **Workflow components**: Macro and micro models +* **Port connections**: How actors exchange data +* **Simulation parameters**: Runtime configuration +* **Resources**: Computational resources for each component + +Wrapped Code +~~~~~~~~~~~~ + +The Fortran code to be wrapped: + +.. literalinclude:: example/wrapped_code.f90 + :language: fortran + :caption: wrapped_code.f90 + +This code implements the physics model that will be wrapped as a MUSCLE3 actor. + +Macro Model +~~~~~~~~~~~ + +The macro model orchestrates the coupled simulation: + +.. literalinclude:: example/macro.f90 + :language: fortran + :caption: macro.f90 + +The macro model: + +* Initializes the simulation +* Sends data to the micro model (wrapped code) +* Receives results from the micro model +* Manages the simulation loop + +Building the Example +~~~~~~~~~~~~~~~~~~~~ + +Step 1: Generate the MUSCLE3 Actor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + cd docs/documentation/muscle3_plugins/example + + # Generate the MUSCLE3 actor + iwrap --actor-type muscle3_fortran \ + --file code_description.yaml \ + --install-dir ./m3_actor + +This command: + +* Reads the code description +* Generates MUSCLE3 wrapper code +* Creates build system (Makefile) +* Sets up the actor directory structure + +Step 2: Build the Actor +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + cd m3_actor + make + +This compiles: + +* The wrapped physics code +* The MUSCLE3 wrapper +* All dependencies + +Step 3: Build the Macro Model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + cd ../ + make macro + +This compiles the macro model that will orchestrate the simulation. + +Step 4: Run the Coupled Simulation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + muscle_manager --start-all example.ymmsl + +This: + +* Starts the MUSCLE3 manager +* Launches both macro and micro models +* Manages data exchange +* Runs the coupled simulation + +Expected Output +~~~~~~~~~~~~~~~ + +The simulation should produce output showing: + +.. code-block:: text + + [MUSCLE3] Starting simulation... + [Macro] Initializing macro model + [Macro] Sending data to micro model + [Micro] Received data from macro + [Micro] Processing... + [Micro] Sending results to macro + [Macro] Received results from micro + [MUSCLE3] Simulation complete + +Output files will be created in a ``run_*`` directory containing: + +* Simulation logs +* Output data +* Performance metrics +* Snapshots (if checkpointing enabled) + +More Examples +------------- + +The test suite contains additional examples for different scenarios: + +Python Examples +~~~~~~~~~~~~~~~ + +Located in ``tests/muscle3/actors/python/``: + +* **basic_python**: Simple Python actor +* **restart_python**: Python actor with restart support + +C++ Examples +~~~~~~~~~~~~ + +Located in ``tests/muscle3/actors/cpp/``: + +* **basic_cpp**: Simple C++ actor +* **basic_mpi_cpp**: MPI-parallel C++ actor +* **restart_cpp**: C++ actor with restart support +* **restart_mpi_cpp**: MPI-parallel C++ actor with restart + +Fortran Examples +~~~~~~~~~~~~~~~~ + +Located in ``tests/muscle3/actors/fortran/``: + +* **code_lifecycle**: Complete lifecycle example +* **basic_mpi**: MPI-parallel Fortran actor +* **code_restart**: Fortran actor with restart support +* **code_restart_mpi**: MPI-parallel Fortran actor with restart + +Running Test Examples +~~~~~~~~~~~~~~~~~~~~~ + +Each test example can be run using its Makefile: + +.. code-block:: bash + + cd tests/muscle3/actors// + make test + +This will: + +1. Build the wrapped code +2. Generate the MUSCLE3 actor +3. Run the coupled simulation +4. Validate the output + +Example: Running the Python Basic Test +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + cd tests/muscle3/actors/python/basic_python + make test + +Advanced Features +----------------- + +MPI Parallelization +~~~~~~~~~~~~~~~~~~~ + +For parallel codes, the MUSCLE3 generators support MPI: + +.. code-block:: yaml + + # In code_description.yaml + implementation: + language: fortran + parallel: mpi + mpi_ranks: 4 + +See the ``basic_mpi`` examples for complete implementations. + +Restart/Checkpoint Support +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Enable restart support in your code description: + +.. code-block:: yaml + + # In code_description.yaml + features: + restart: true + checkpoint_interval: 10 + +See the ``restart_*`` examples for complete implementations. + +Custom Parameters +~~~~~~~~~~~~~~~~~ + +Define custom parameters for your code: + +.. code-block:: yaml + + # In code_description.yaml + parameters: + - name: timestep + type: float + default: 0.01 + - name: max_iterations + type: integer + default: 100 + +IMAS Integration +~~~~~~~~~~~~~~~~ + +For IMAS-based codes, specify IDS usage: + +.. code-block:: yaml + + # In code_description.yaml + ports: + input: + - name: equilibrium_in + ids: equilibrium + output: + - name: equilibrium_out + ids: equilibrium + +Troubleshooting +--------------- + +Actor Generation Fails +~~~~~~~~~~~~~~~~~~~~~~~ + +If ``iwrap`` fails to generate the actor: + +1. Check the code description YAML syntax +2. Verify all required fields are present +3. Check iWrap logs for detailed error messages +4. Ensure MUSCLE3 plugins are installed: ``iwrap --list-actor-types`` + +Build Fails +~~~~~~~~~~~ + +If the actor fails to build: + +1. Verify MUSCLE3 libraries are available: ``pkg-config --modversion muscle3`` +2. Check compiler is available: ``gfortran --version`` or ``g++ --version`` +3. Review build logs in the actor directory +4. Ensure all source files are present + +Runtime Errors +~~~~~~~~~~~~~~ + +If the simulation fails at runtime: + +1. Check MUSCLE3 manager logs +2. Verify yMMSL workflow configuration +3. Ensure port connections are correct +4. Check for MPI configuration issues (if using MPI) + +Next Steps +---------- + +* Adapt the basic example to your own code +* Explore advanced features in the :doc:`code_wrapping` guide +* Review the :doc:`../muscle3_actors` documentation +* Check the test suite for more examples +* Consult the `MUSCLE3 documentation `_ + +See Also +-------- + +* :doc:`installation` - Installation instructions +* :doc:`code_wrapping` - Detailed code wrapping guide +* :doc:`../muscle3_actors` - Overview of MUSCLE3 actors +* :doc:`../muscle3_migration_guide` - Migration guide diff --git a/docs/documentation/muscle3_plugins/index.rst b/docs/documentation/muscle3_plugins/index.rst new file mode 100644 index 0000000..f3aa0fd --- /dev/null +++ b/docs/documentation/muscle3_plugins/index.rst @@ -0,0 +1,215 @@ +MUSCLE3 Plugins Documentation +============================== + +This section provides detailed documentation for the MUSCLE3 actor generators +integrated into iWrap. + +.. note:: + As of iWrap version 0.8.0, MUSCLE3 plugins have been integrated into the + main iWrap package. The separate ``iwrap-plugins-muscle3`` repository has + been archived. + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + installation + code_wrapping + examples + +Overview +-------- + +The MUSCLE3 plugins provide actor generators for three programming languages: + +* **Python** - ``iwrap.generators.actor_generators.muscle3_python`` +* **C++** - ``iwrap.generators.actor_generators.muscle3_cpp`` +* **Fortran** - ``iwrap.generators.actor_generators.muscle3_fortran`` + +These generators create MUSCLE3-compatible actors from your physics codes, +enabling multiscale coupling through the MUSCLE3 framework. + +What are MUSCLE3 Actors? +------------------------- + +MUSCLE3 (Multiscale Coupling Library and Environment 3) is a framework for +building multiscale coupled simulations. A MUSCLE3 actor is a component in +a coupled simulation that: + +* Communicates with other actors through well-defined ports +* Exchanges data using the MUSCLE3 communication infrastructure +* Can be written in Python, C++, or Fortran +* Follows the MUSCLE3 API conventions + +The iWrap MUSCLE3 plugins automate the process of wrapping your existing +physics codes into MUSCLE3 actors, handling: + +* Port definitions and data exchange +* MUSCLE3 API integration +* Build system generation +* Parameter handling +* Restart/checkpoint support + +Quick Start +----------- + +1. **Install iWrap with MUSCLE3 support:** + + .. code-block:: bash + + pip install iwrap[muscle3] + +2. **Prepare your code description YAML file:** + + Define your code's interface, parameters, and implementation details. + See :doc:`code_wrapping` for detailed instructions. + +3. **Generate a MUSCLE3 actor:** + + .. code-block:: bash + + iwrap --actor-type muscle3_python --file my_code.yaml --install-dir ./actor + + Replace ``muscle3_python`` with ``muscle3_cpp`` or ``muscle3_fortran`` as needed. + +4. **Build the generated actor:** + + .. code-block:: bash + + cd actor + make + +5. **Run your coupled simulation:** + + Create a yMMSL workflow file and run with MUSCLE3: + + .. code-block:: bash + + muscle_manager --start-all my_workflow.ymmsl + +For a complete working example, see :doc:`examples`. + +Supported Languages +------------------- + +Python +~~~~~~ + +**Actor Type:** ``muscle3_python`` + +**Features:** + +* Pure Python implementation +* Easy to debug and modify +* Supports all MUSCLE3 features +* Ideal for prototyping and Python-based codes + +**Import:** + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_python import M3PythonActor + +C++ +~~~ + +**Actor Type:** ``muscle3_cpp`` + +**Features:** + +* High performance +* Direct integration with C++ physics codes +* Supports MPI parallelization +* Automatic build system generation + +**Import:** + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_cpp import M3CppActor + +Fortran +~~~~~~~ + +**Actor Type:** ``muscle3_fortran`` + +**Features:** + +* Native Fortran support +* Optimized for HPC environments +* Supports MPI parallelization +* Compatible with legacy Fortran codes + +**Import:** + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_fortran import M3FortranActor + +Common Utilities +~~~~~~~~~~~~~~~~ + +All generators share common utilities for MUSCLE3 integration: + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_common import m3_utils + +Key Features +------------ + +Automatic Code Generation +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The plugins automatically generate: + +* MUSCLE3 actor wrapper code +* Build system (Makefile) +* Port definitions and data exchange logic +* Parameter handling +* Initialization and finalization code + +IMAS Integration +~~~~~~~~~~~~~~~~ + +Full support for IMAS (Integrated Modelling & Analysis Suite): + +* Automatic IDS (Interface Data Structure) handling +* Data conversion between IMAS and MUSCLE3 formats +* Support for IMAS Access Layer + +MPI Support +~~~~~~~~~~~ + +For parallel codes: + +* MPI initialization and finalization +* Rank-aware data distribution +* Collective operations support + +Restart/Checkpoint +~~~~~~~~~~~~~~~~~~ + +Built-in support for: + +* Saving simulation state +* Restarting from checkpoints +* Snapshot management + +See Also +-------- + +* :doc:`../muscle3_actors` - Overview of MUSCLE3 actors in iWrap +* :doc:`../muscle3_migration_guide` - Migration guide for existing users +* :doc:`installation` - Installation instructions +* :doc:`code_wrapping` - Detailed code wrapping guide +* :doc:`examples` - Complete working examples + +External Resources +------------------ + +* `MUSCLE3 Documentation `_ +* `IMAS Documentation `_ +* `iWrap Repository `_ diff --git a/docs/documentation/muscle3_plugins/installation.rst b/docs/documentation/muscle3_plugins/installation.rst new file mode 100644 index 0000000..109bacc --- /dev/null +++ b/docs/documentation/muscle3_plugins/installation.rst @@ -0,0 +1,287 @@ +.. sectnum:: + +.. toctree:: + +Installation +####################################################################################################################### + +.. note:: + As of iWrap version 0.8.0, MUSCLE3 plugins have been integrated into the main iWrap package. + The separate ``iwrap-plugins-muscle3`` repository has been archived. + +MUSCLE3 support is now included in the main iWrap package and can be installed using pip with the ``muscle3`` extra. + +Requirements +####################################################################################################################### + +Software required to use MUSCLE3 plugins: + +- `Python` >= 3.6 +- `iWrap` >= 0.7.0 +- `MUSCLE3` >= 0.7.0 +- `IMAS` Access Layer >= 4.11 (for IMAS-based codes) + +Software required to build documentation: + +- `make` >= 3.82 +- `Python` >= 3.8.6 +- Python packages: + + - `Sphinx` >= 3.2.1 + - `sphinx-bootstrap-theme` >= 0.7.1 + - `sphinx-rtd-theme` >= 1.0.0 + +Installation via pip +####################################################################################################################### + +Standard Installation +========================================================================================= + +To install iWrap with MUSCLE3 support: + +.. code-block:: console + + pip install iwrap[muscle3] + +This will install: + +- iWrap core package +- MUSCLE3 actor generators (Python, C++, Fortran) +- MUSCLE3 dependencies (muscle3, ymmsl) + +Development Installation +========================================================================================= + +For development, install in editable mode: + +.. code-block:: console + + git clone https://git.iter.org/projects/IMEX/repos/iwrap + cd iwrap + pip install -e .[muscle3] + +Installation from Source +####################################################################################################################### + +Using Makefile +========================================================================================= + +The iWrap repository includes a Makefile for building and installing: + +.. code-block:: console + + make all # Build iWrap + make install # Install iWrap + make install_module # Install environment module + +To check all available targets and configuration options: + +.. code-block:: console + + shell> make help + +Variables that can be configured: + +- ``PYTHON_CMD`` - Python interpreter to use (default: python) +- ``INSTALL_PREFIX`` - Installation directory prefix +- ``INSTALL_MOD`` - Module file installation directory +- ``VERSION`` - iWrap version (auto-detected from git) + +Verification +####################################################################################################################### + +Verification of MUSCLE3 Plugins Installation +========================================================================================= + +Once iWrap is installed with MUSCLE3 support, verify that the MUSCLE3 actor generators are available: + +.. code-block:: console + + shell> iwrap --list-actor-types + + Id : Name : Description + ---------------------------------------------------------------------- + python : Simple Python actor : Simple Python actor + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model + MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model + +If the MUSCLE3 actor types are listed, the installation was successful. + +Verify Python Imports +========================================================================================= + +You can also verify that the MUSCLE3 generators can be imported: + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_python import M3PythonActor + from iwrap.generators.actor_generators.muscle3_cpp import M3CppActor + from iwrap.generators.actor_generators.muscle3_fortran import M3FortranActor + from iwrap.generators.actor_generators.muscle3_common import m3_utils + +If these imports succeed without errors, the MUSCLE3 plugins are correctly installed. + +Environment Setup +####################################################################################################################### + +ITER SDCC +========================================================================================= + +For the ITER Organisation computing cluster (SDCC), use the provided configuration script: + +.. code-block:: console + + source set-iter.sh + +This script will: + +- Load required modules (iWrap, MUSCLE3, IMAS) +- Set up environment variables +- Configure PYTHONPATH + +EUROfusion Gateway +========================================================================================= + +For the EUROfusion Gateway, use: + +.. code-block:: console + + source set-gw.sh + +This script provides similar functionality for the Gateway environment. + +Manual Environment Setup +========================================================================================= + +If you're not using the provided scripts, ensure the following are available: + +1. **MUSCLE3 Libraries**: Available via pkg-config + + .. code-block:: console + + pkg-config --modversion muscle3 + +2. **IMAS Access Layer**: Properly configured + + .. code-block:: console + + module load imas + +3. **Python Path**: iWrap should be in your PYTHONPATH (automatically handled by pip install) + +Migration from Separate Plugin Package +####################################################################################################################### + +If you were previously using the separate ``iwrap-plugins-muscle3`` package: + +Old Installation +========================================================================================= + +.. code-block:: console + + pip install iwrap + pip install iwrap-plugins-muscle3 + +New Installation +========================================================================================= + +.. code-block:: console + + pip install iwrap[muscle3] + +Import Path Changes +========================================================================================= + +Update your code to use the new import paths: + +**Old (separate plugin):** + +.. code-block:: python + + from iwrap_plugins.iwrap_actor_generator.muscle3_python import M3PythonActor + +**New (integrated):** + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_python import M3PythonActor + +See the :doc:`../muscle3_migration_guide` for more details on migrating from the separate plugin package. + +Uninstall +####################################################################################################################### + +To uninstall iWrap: + +.. code-block:: console + + pip uninstall iwrap + +This will remove both the core iWrap package and the integrated MUSCLE3 plugins. + +Troubleshooting +####################################################################################################################### + +MUSCLE3 Actor Types Not Listed +========================================================================================= + +If ``iwrap --list-actor-types`` doesn't show MUSCLE3 actor types: + +1. Verify MUSCLE3 extra was installed: + + .. code-block:: console + + pip show iwrap | grep muscle3 + +2. Reinstall with MUSCLE3 support: + + .. code-block:: console + + pip install --force-reinstall iwrap[muscle3] + +Import Errors +========================================================================================= + +If you get import errors when trying to use MUSCLE3 generators: + +1. Verify iWrap is installed: + + .. code-block:: console + + python -c "import iwrap; print(iwrap.__version__)" + +2. Verify MUSCLE3 dependencies are installed: + + .. code-block:: console + + python -c "import muscle3; print(muscle3.__version__)" + +3. Check your Python environment is correct: + + .. code-block:: console + + which python + which iwrap + +MUSCLE3 Library Not Found +========================================================================================= + +If you get errors about MUSCLE3 libraries not being found during actor compilation: + +1. Verify MUSCLE3 is available via pkg-config: + + .. code-block:: console + + pkg-config --modversion muscle3 + pkg-config --cflags muscle3 + pkg-config --libs muscle3 + +2. Load the MUSCLE3 module (if using environment modules): + + .. code-block:: console + + module load muscle3 + +3. Check the MUSCLE3 installation documentation for your platform. + diff --git a/docs/documentation/quickstart.rst b/docs/documentation/quickstart.rst new file mode 100644 index 0000000..d2a002e --- /dev/null +++ b/docs/documentation/quickstart.rst @@ -0,0 +1,386 @@ +======================================== +Quick Start Guide +======================================== + +This guide will help you get started with iWrap quickly, covering both standard Python actors and MUSCLE3 actors. + +Installation +======================================== + +Choose Your Installation +------------------------ + +**Option 1: Core Only (Minimal)** + +.. code-block:: bash + + pip install iwrap + +Includes: Standard Python actor generator + +**Option 2: With MUSCLE3 Support (Recommended)** + +.. code-block:: bash + + pip install iwrap[muscle3] + +Includes: Python actor + MUSCLE3 actors (Python, C++, Fortran) + +**Option 3: Development (All Features)** + +.. code-block:: bash + + pip install iwrap[all] + +Includes: All features and development tools + +Verify Installation +------------------- + +.. code-block:: bash + + iwrap --version + iwrap --list-actor-types + +Your First Actor (Standard Python) +======================================== + +Step 1: Prepare Your Code +-------------------------- + +Create a simple Python physics code ``my_code.py``: + +.. code-block:: python + + # my_code.py + def init(): + """Initialize the code""" + print("Initializing...") + + def run(equilibrium_in, core_profiles_out): + """Main computation""" + print("Running step...") + # Your physics calculations here + return equilibrium_in, core_profiles_out + + def finalize(): + """Cleanup""" + print("Finalizing...") + +Step 2: Create Code Description +-------------------------------- + +Create ``code_description.yaml``: + +.. code-block:: yaml + + actor_description: + actor_name: my_first_actor + actor_type: python + + code_description: + implementation: + programming_language: python + code_path: ./my_code.py + + subroutines: + init: + name: init + arguments: [] + + step: + name: run + arguments: + - name: equilibrium + type: input + - name: core_profiles + type: output + + finalize: + name: finalize + arguments: [] + +Step 3: Generate Actor +----------------------- + +.. code-block:: bash + + iwrap -a my_first_actor -t python -f code_description.yaml + +Step 4: Use the Actor +---------------------- + +Create a workflow ``workflow.py``: + +.. code-block:: python + + import imas + from my_first_actor import MyFirstActor + + # Create IDS objects + equilibrium = imas.equilibrium() + core_profiles = imas.core_profiles() + + # Initialize and run actor + actor = MyFirstActor() + actor.init() + actor.run(equilibrium, core_profiles) + actor.finalize() + +Run it: + +.. code-block:: bash + + python workflow.py + +Your First MUSCLE3 Actor +======================================== + +Prerequisites +------------- + +Ensure MUSCLE3 is installed: + +.. code-block:: bash + + pip install iwrap[muscle3] + # Verify + python -c "import muscle3; print(muscle3.__version__)" + +Step 1: Prepare Your Code +-------------------------- + +Create a MUSCLE3-compatible code ``my_muscle3_code.py``: + +.. code-block:: python + + # my_muscle3_code.py + def init(): + """Initialize - no IDS arguments allowed""" + print("MUSCLE3 actor initializing...") + + def run(equilibrium_in, core_profiles_out): + """Main computation with IDS""" + print("MUSCLE3 step running...") + # Your physics here + return equilibrium_in, core_profiles_out + + def finalize(): + """Cleanup - no IDS arguments allowed""" + print("MUSCLE3 actor finalizing...") + +Step 2: Create Code Description +-------------------------------- + +Create ``muscle3_code_description.yaml``: + +.. code-block:: yaml + + actor_description: + actor_name: my_muscle3_actor + actor_type: MUSCLE3-Python + + code_description: + implementation: + programming_language: python + code_path: ./my_muscle3_code.py + + subroutines: + init: + name: init + arguments: [] # IMPORTANT: No IDS for MUSCLE3 init + + step: + name: run + arguments: + - name: equilibrium + type: input + - name: core_profiles + type: output + + finalize: + name: finalize + arguments: [] # IMPORTANT: No IDS for MUSCLE3 finalize + +Step 3: Generate MUSCLE3 Actor +------------------------------- + +.. code-block:: bash + + iwrap -a my_muscle3_actor -t MUSCLE3-Python -f muscle3_code_description.yaml + +Step 4: Create MUSCLE3 Workflow +-------------------------------- + +Create ``workflow.ymmsl``: + +.. code-block:: yaml + + ymmsl_version: v0.1 + + model: + name: my_first_muscle3_simulation + components: + macro: + implementation: macro_model + ports: + o_i: core_profiles + s_o: equilibrium + + micro: + implementation: my_muscle3_actor + ports: + f_init: equilibrium + o_f: core_profiles + + settings: + micro.time_step: 0.1 + macro.iterations: 10 + +Step 5: Run MUSCLE3 Workflow +----------------------------- + +.. code-block:: bash + + muscle_manager workflow.ymmsl + +Using the GUI +======================================== + +iWrap provides a graphical interface for easy actor generation. + +Launch GUI +---------- + +.. code-block:: bash + + iwrap-gui + +Or load an existing description: + +.. code-block:: bash + + iwrap-gui -f code_description.yaml + +GUI Steps +--------- + +1. **Select Actor Type:** Choose from dropdown (python, MUSCLE3-Python, etc.) +2. **Enter Actor Name:** Specify your actor name +3. **Code Details:** Browse and select your code file +4. **Define Methods:** Add init, step, finalize methods +5. **Specify IDS Arguments:** Define input/output IDS for each method +6. **Generate:** Click generate button + +The GUI will create the YAML file and generate the actor. + +Common Tasks +======================================== + +Listing Available Actor Types +------------------------------ + +.. code-block:: bash + + iwrap --list-actor-types + +Getting Help +------------ + +.. code-block:: bash + + iwrap -h + iwrap-gui -h + +Checking Version +---------------- + +.. code-block:: bash + + iwrap --version + +Viewing Actor Generator Details +-------------------------------- + +.. code-block:: bash + + iwrap --list-actor-details python + iwrap --list-actor-details MUSCLE3-Python + +Common Issues and Solutions +======================================== + +Issue: MUSCLE3 Generators Not Available +---------------------------------------- + +**Symptom:** Only ``python`` appears in ``--list-actor-types`` + +**Solution:** + +.. code-block:: bash + + pip install iwrap[muscle3] + +Issue: Import Error +------------------- + +**Symptom:** ``ModuleNotFoundError: No module named 'iwrap'`` + +**Solution:** Ensure iWrap is installed and PYTHONPATH is set: + +.. code-block:: bash + + pip install iwrap + # Or use environment setup script + source set-iter.sh + +Issue: MUSCLE3 Init/Finalize Error +----------------------------------- + +**Symptom:** ``ValueError: MUSCLE3 actor generator cannot handle INIT/FINALIZE methods with IDS arguments`` + +**Solution:** Remove IDS arguments from init and finalize methods in YAML: + +.. code-block:: yaml + + subroutines: + init: + name: init + arguments: [] # Empty - no IDS allowed + finalize: + name: finalize + arguments: [] # Empty - no IDS allowed + +Next Steps +======================================== + +Now that you've created your first actor, explore: + +📚 **Documentation** + - :doc:`actor_types` - Learn about all actor types + - :doc:`muscle3_actors` - Deep dive into MUSCLE3 + - :doc:`project_description` - Complete actor description reference + +🎓 **Tutorials** + - Follow interactive Jupyter tutorials in ``docs/tutorial/`` + - Try example actors in ``examples/`` + +🔧 **Advanced Topics** + - :doc:`code_standardization` - Code requirements and best practices + - :doc:`developers_manual/index` - Extending iWrap + - :doc:`actor_usage` - Using generated actors in workflows + +💡 **Examples** + - Browse ``examples/`` directory for complete working examples + - Check plugin tests for advanced usage patterns + +Getting Help +======================================== + +If you need assistance: + +1. Check the documentation: :doc:`../index` +2. Review examples in the repository +3. Contact: iWrap Development Team + +Happy coding with iWrap! 🚀 diff --git a/docs/iWrap_introduction.md b/docs/iWrap_introduction.md index a99b0f2..9944f86 100644 --- a/docs/iWrap_introduction.md +++ b/docs/iWrap_introduction.md @@ -74,11 +74,17 @@ Source: https://en.wikipedia.org/wiki/Actor_model iWrap is built around a plug-in based modular design, making it very flexible and versatile. It enables you to generate various types of actors and switch between different data representation and access methods. -```{admonition} MUSCLE3 Integration +```{admonition} Available Actor Types :class: important iWrap is engineered with modularity in mind, enabling it to generate a variety of actor types to suit your specific needs. -Currently, it supports the creation of `Python` actors for straightforward Python integrations and `Muscle3` actors for more complex, high-performance computing scenarios. + +**Built-in Actor Generators:** +- **Python Actor**: Standard Python actors for straightforward Python integrations +- **MUSCLE3 Actors**: High-performance coupling framework actors (requires `pip install iwrap[muscle3]`) + - MUSCLE3-Python: Python code with MUSCLE3 coupling + - MUSCLE3-CPP: C++ code with MUSCLE3 coupling + - MUSCLE3-Fortran: Fortran code with MUSCLE3 coupling ``` diff --git a/docs/images/muscle3/actor-ports.png b/docs/images/muscle3/actor-ports.png new file mode 100644 index 0000000000000000000000000000000000000000..48e51ac52176f61501a57776611d661cf012377d GIT binary patch literal 35487 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 literal 0 HcmV?d00001 diff --git a/docs/images/muscle3/macro-actor.png b/docs/images/muscle3/macro-actor.png new file mode 100644 index 0000000000000000000000000000000000000000..cd4508e013c3613aa5d2d21f62bf443b8212a807 GIT binary patch literal 61738 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�e^ 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 Q|HQd0uUMN^Uh;_jKW-hMu>b%7 literal 0 HcmV?d00001 diff --git a/iwrap/__init__.py b/iwrap/__init__.py index 97cccb7..e549516 100644 --- a/iwrap/__init__.py +++ b/iwrap/__init__.py @@ -2,5 +2,14 @@ IWRAP_DIR = os.path.dirname(os.path.realpath(__file__)) -from . import _version -__version__ = _version.get_versions()['version'] +try: + from importlib.metadata import version, PackageNotFoundError +except ImportError: + # Python < 3.8 + from importlib_metadata import version, PackageNotFoundError + +try: + __version__ = version("iwrap") +except PackageNotFoundError: + # Package is not installed + __version__ = "unknown" diff --git a/iwrap/_version.py b/iwrap/_version.py deleted file mode 100644 index ed0a4d5..0000000 --- a/iwrap/_version.py +++ /dev/null @@ -1,665 +0,0 @@ - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. -# Generated by versioneer-0.28 -# https://github.com/python-versioneer/python-versioneer - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys -from typing import Callable, Dict -import functools - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_describe_output = "$Format:%(describe)$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" - mo_tag_in_git_refnames = re.search(r'(\d+\.)(\d+\.)(\d+)', git_refnames) - if not mo_tag_in_git_refnames: - mo_tag_in_git_describe = re.search(r'(\d+\.)(\d+\.)(\d+)', git_describe_output) - if mo_tag_in_git_describe: - mo_describe = '(, tag: '+ git_describe_output +',,'+ git_refnames + ')' - git_refnames = mo_describe - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440" - cfg.tag_prefix = "" - cfg.parentdir_prefix = "" - cfg.versionfile_source = "iwrap/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs, method): # decorator - """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - - popen_kwargs = {} - if sys.platform == "win32": - # This hides the console window if pythonw.exe is used - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - popen_kwargs["startupinfo"] = startupinfo - - for command in commands: - dispcmd = str([command] + args) - try: - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) - break - except OSError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, process.returncode - return stdout, process.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r'\d', r): - continue - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - # GIT_DIR can interfere with correct operation of Versioneer. - # It may be intended to be passed to the Versioneer-versioned project, - # but that should not change where we get our version from. - env = os.environ.copy() - env.pop("GIT_DIR", None) - runner = functools.partial(runner, env=env) - - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) - pieces["distance"] = len(out.split()) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces): - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver): - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces): - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post(pieces["closest-tag"]) - rendered = tag_version - if post_version is not None: - rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) - else: - rendered += ".post0.dev%d" % (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} diff --git a/iwrap/generators/actor_generators/muscle3_common/__init__.py b/iwrap/generators/actor_generators/muscle3_common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iwrap/generators/actor_generators/muscle3_common/dumper.py b/iwrap/generators/actor_generators/muscle3_common/dumper.py new file mode 100644 index 0000000..fe2f18b --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_common/dumper.py @@ -0,0 +1,27 @@ +import yaml + + +class CustomDumper(yaml.SafeDumper): + def ignore_aliases(self, data): + return True + + #dump empty sequence as: ' ' (nothing) + def represent_sequence(self, tag, sequence, flow_style=None): + if not sequence: # Check if the sequence is empty + return super().represent_scalar(u'tag:yaml.org,2002:null', u'') + return super().represent_sequence(tag, sequence, flow_style) + + # dump empty dict as: ' ' (nothing) + def represent_mapping(self, tag, mapping, flow_style=None): + if not mapping: # Check if the sequence is empty + return super().represent_scalar(u'tag:yaml.org,2002:null', u'') + return super().represent_mapping(tag, mapping, flow_style) + + def represent_none(self, data): + return super().represent_scalar('tag:yaml.org,2002:null', u'') + + # dump empty dict as: ' ' (nothing) + def represent_scalar(self, tag, value, style=None): + if not value or value.lower() == 'null': # Check if value is None or 'null' + return super().represent_scalar(u'tag:yaml.org,2002:null', u'') + return super().represent_scalar(tag, value, style) diff --git a/iwrap/generators/actor_generators/muscle3_common/m3_utils.py b/iwrap/generators/actor_generators/muscle3_common/m3_utils.py new file mode 100644 index 0000000..cef8795 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_common/m3_utils.py @@ -0,0 +1,99 @@ +import os +import shutil +from typing import List + +import yaml +from .dumper import CustomDumper + + +def template_filter_func(templates_list: List[str]) -> bool: + if "__pycache__" in templates_list: + return False + + if "macros" in templates_list: + return False + + return True + +def copy_native_lib(install_dir: str, project_settings:dict, destination_dir:str): + + native_lib_path = project_settings['code_description'].get('implementation', {}).get('code_path') + if not native_lib_path: + return + + destination_dir = os.path.join( install_dir, destination_dir) + if not os.path.isdir( destination_dir ): + os.makedirs( destination_dir ) + + shutil.copy( native_lib_path, destination_dir ) + +def copy_include(install_dir: str, project_settings:dict): + + include_path = project_settings['code_description'].get('implementation', {}).get('include_path') + if not include_path: + return + + destination_dir = os.path.join( install_dir, 'include' ) + if not os.path.isdir( destination_dir ): + os.makedirs( destination_dir ) + + shutil.copy( include_path, destination_dir ) + +def copy_extra_libs(install_dir: str, project_settings: dict): + + libraries = project_settings['code_description'].get('settings', {}).get('extra_libraries', {}).get('path_defined') + + if not libraries: + return + + destination_dir = os.path.join( install_dir, 'extra-libs' ) + if not os.path.isdir( destination_dir ): + os.makedirs( destination_dir ) + + for library_path in libraries: + shutil.copy( library_path, destination_dir ) + + +def copy_code_params_files(install_dir: str, project_settings:dict): + code_parameters = project_settings['code_description'].get('implementation', {}).get('code_parameters') + if not code_parameters: + return + + parameters_file = code_parameters.get('parameters') + schema_file = code_parameters.get('schema') + + if not parameters_file: + return + + if parameters_file and not schema_file: + raise Exception('Error! Code parameters schema file (XSD) is missing!') + + destination_dir = os.path.join(install_dir, 'input') + if not os.path.isdir( destination_dir ): + os.makedirs( destination_dir ) + + shutil.copy( parameters_file, destination_dir ) + shutil.copy( schema_file, destination_dir ) + +def copy_build_info(install_dir: str, project_settings:dict): + output_dict = dict((key, project_settings[key]) for key in project_settings if key in ['actor_description','code_description','build_info']) + with open(f'{install_dir}/{project_settings["actor_description"]["actor_name"]}.yaml', "w") as file: + yaml.dump(output_dict, file, Dumper=CustomDumper) + + +def validate(project_settings: dict = None): + + init_arguments = project_settings['code_description'] \ + .get('implementation', {}) \ + .get('subroutines', {}) \ + .get('init', {})\ + .get('arguments') + + finalize_arguments = project_settings['code_description'] \ + .get('implementation', {}) \ + .get('subroutines', {}) \ + .get('finalize', {})\ + .get('arguments') + + if init_arguments or finalize_arguments: + raise ValueError( 'MUSCLE3 actor generator cannot handle INIT/FINALIZE methods with IDS arguments' ) \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_cpp/__init__.py b/iwrap/generators/actor_generators/muscle3_cpp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iwrap/generators/actor_generators/muscle3_cpp/m3_cpp_actor.py b/iwrap/generators/actor_generators/muscle3_cpp/m3_cpp_actor.py new file mode 100644 index 0000000..d977ef9 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/m3_cpp_actor.py @@ -0,0 +1,120 @@ +import logging +import os +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Set, List + + +from iwrap.generation_engine.utils.jinja2_template_processing import process_template_dir +from iwrap.generators.actor_generators import ActorGenerator +from iwrap.settings.project import ProjectSettings + +import jinja2 +import sys + +from iwrap.settings.platform.platform_settings import PlatformSettings + + +from iwrap.generators.actor_generators.muscle3_common import m3_utils + + +class CppActorGenerator(ActorGenerator): + # Class logger + __logger = logging.getLogger(__name__ + "." + __qualname__) + + COMPLIANT_API = '2.1' + + @property + def type(self) -> str: + return 'MUSCLE3-CPP' + + @property + def name(self) -> str: + return 'MUSCLE3 (C++)' + + @property + def description(self) -> str: + return 'Wrapping C++ code into MUSCLE3 micro model' + + @property + def actor_language(self) -> List[str]: + return 'cpp' + + @property + def actor_data_types(self) -> List[str]: + return ['legacy'] + + @property + def code_data_types(self) -> List[str]: + return ['legacy'] + + @property + def code_languages(self) -> Set[str]: + return {'cpp'} + + def __init__(self): + + self.__info_output_stream = None + self.temp_dir: tempfile.TemporaryDirectory = None + self.jinja_env: jinja2.Environment = None + self.install_dir: str = None + self.skeleton_generation: bool = False + + def configure(self, info_output_stream=sys.stdout): + self.__info_output_stream = info_output_stream + + def validate(self, project_settings: dict = None): + m3_utils.validate( project_settings ) + + def initialize(self, project_settings: dict = None): + install_dir = None + + if project_settings: + install_dir = project_settings['actor_description'].get( 'install_dir' ) + programming_language = project_settings['code_description'].get('implementation', {}).get('programming_language') + if not programming_language: + self.skeleton_generation = True + + if not install_dir: + install_dir = PlatformSettings().directories.actor_install_dir + self.install_dir: str = str(Path(install_dir, ProjectSettings.get_settings().actor_description.actor_name)) + + def generate(self, project_settings: dict): + self.cleanup( project_settings ) + os.makedirs( self.install_dir, exist_ok=True ) + + current_path = os.path.dirname(os.path.realpath(__file__)) + process_template_dir( None, current_path + '/resources', self.install_dir, project_settings, + filter_func=m3_utils.template_filter_func, + output_stream= self.__info_output_stream, ) + + m3_utils.copy_native_lib( self.install_dir, project_settings, "lib" ) + m3_utils.copy_include( self.install_dir, project_settings ) + m3_utils.copy_extra_libs( self.install_dir, project_settings ) + m3_utils.copy_code_params_files( self.install_dir, project_settings ) + m3_utils.copy_build_info( self.install_dir, project_settings ) + + def build(self, project_settings: dict): + + if self.skeleton_generation: + return + + proc = subprocess.Popen( [], executable = "make", cwd=self.install_dir, + encoding='utf-8', text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) + + for line in proc.stdout: + print( line, file=self.__info_output_stream, end='' ) + + return_code = proc.wait() + if return_code: + raise subprocess.CalledProcessError( return_code, 'make' ) + + def install(self, project_settings: dict): + ... + + def cleanup(self, project_settings: dict): + shutil.rmtree(self.install_dir, ignore_errors=True) + + diff --git a/iwrap/generators/actor_generators/muscle3_cpp/resources/Makefile.jinja2 b/iwrap/generators/actor_generators/muscle3_cpp/resources/Makefile.jinja2 new file mode 100644 index 0000000..53c9769 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/resources/Makefile.jinja2 @@ -0,0 +1,76 @@ +# actor name +ACTOR_NAME={{actor_description.actor_name}} + + +{% if code_description.settings.mpi_compiler_cmd %} +CXX={{ code_description.settings.mpi_compiler_cmd }} + +{% elif code_description.settings.compiler_cmd %} +CXX={{ code_description.settings.compiler_cmd }} +{% endif %} + + +WRAPPER_DIR=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) + +COMPILER_FLAGS={{ code_description.settings.compiler_flags or '' }} + +CXXFLAGS= -g -fpic -pthread $(COMPILER_FLAGS) + +CODE_LIB_DEF= +{%- if code_description.implementation.code_path -%} +./lib/{{code_description.implementation.code_path | basename}} +{% endif %} + +{% if code_description.settings.mpi_compiler_cmd %} +MUSCLE3_PKG=libmuscle_mpi +{% else %} +MUSCLE3_PKG=libmuscle +{% endif %} + +# main IMAS libs to be used +{% if build_info.al_version.startswith('4.') %} +IMAS_LIB_NAME=imas-cpp +{% else %} +IMAS_LIB_NAME=al-cpp +{% endif %} +CORE_LIBS_FLAGS= $(shell pkg-config --cflags $(IMAS_LIB_NAME) ymmsl $(MUSCLE3_PKG)) +CORE_LIBS=$(shell pkg-config --libs $(IMAS_LIB_NAME) ymmsl $(MUSCLE3_PKG)) + +# required libs published using pkg-config mechanism +{% if code_description.settings.extra_libraries and code_description.settings.extra_libraries.pkg_config_defined %} +LIBS_PKGCONFIG_DEFINED_NAMES={{ (code_description.settings.extra_libraries.pkg_config_defined or []) | join(' ')}} +LIBS_PKGCONFIG_DEFINED_FLAGS=$(shell pkg-config --cflags $(LIBS_PKGCONFIG_DEFINED_NAMES)) +LIBS_PKGCONFIG_DEFINED=$(shell pkg-config --libs $(LIBS_PKGCONFIG_DEFINED_NAMES)) +{% else %} +LIBS_PKGCONFIG_DEFINED_NAMES= +LIBS_PKGCONFIG_DEFINED_FLAGS= +LIBS_PKGCONFIG_DEFINED= +{% endif %} + + +# required libs provided by user as a path +LIBS_PATH_DEFINED= +{%- if code_description.settings %} +{%- for lib_path in code_description.settings.extra_libraries.path_defined or [] %} + ./extra-libs/{{ lib_path | basename }} +{%- endfor %} +{% endif %} + +INCLUDES=-I./include $(CORE_LIBS_FLAGS) $(LIBS_PKGCONFIG_DEFINED_FLAGS) +LIBS=$(CORE_LIBS) $(LIBS_PKGCONFIG_DEFINED) $(LIBS_PATH_DEFINED) $(CODE_LIB_DEF) + + +all: ./bin/$(ACTOR_NAME).exe + +./bin/$(ACTOR_NAME).exe: src/standalone.cpp build/muscle3_tools.o + @echo " * $^ -> $@" + @mkdir -p ./bin + $(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ -Wl,--start-group $^ $(LIBS) -Wl,--end-group -Wl,-rpath,$(WRAPPER_DIR)/extra-libs/ + +build/%.o: src/%.cpp + @echo " * $^ -> $@" + @mkdir -p build + $(CXX) $(CXXFLAGS) -c -w $^ -o $@ $(INCLUDES) + +clean: + -rm -f *.o build/* ./bin/* diff --git a/iwrap/generators/actor_generators/muscle3_cpp/resources/macros/flags.jinja2 b/iwrap/generators/actor_generators/muscle3_cpp/resources/macros/flags.jinja2 new file mode 100644 index 0000000..feb9158 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/resources/macros/flags.jinja2 @@ -0,0 +1,5 @@ +{%- set checkpointable = code_description.implementation.subroutines.get_state and code_description.implementation.subroutines.set_state %} +{%- set code_parameters_provided = code_description.implementation.code_parameters.parameters %} +{%- set mpi_code = code_description.settings.mpi_compiler_cmd -%} + +{%- set skeleton_only = not code_description.implementation.programming_language -%} diff --git a/iwrap/generators/actor_generators/muscle3_cpp/resources/macros/legacy_ids.jinja2 b/iwrap/generators/actor_generators/muscle3_cpp/resources/macros/legacy_ids.jinja2 new file mode 100644 index 0000000..fea7695 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/resources/macros/legacy_ids.jinja2 @@ -0,0 +1,26 @@ +{% macro imports(al_version) -%} + +{% if al_version.startswith('4.') %} +#include "UALClasses.h" +{% else %} +#include "ALClasses.h" +{% endif %} +{%- endmacro -%} + +{% macro declare(ids_name, ids_var_name) -%} + IdsNs::IDS::{{ ids_name }} {{ ids_var_name }}; +{%- endmacro -%} + +{%- macro argument(ids_name, ids_var_name) -%} + IdsNs::IDS::{{ ids_name }}& {{ ids_var_name }} +{%- endmacro -%} + +{%- macro provenance(ids_var_name, sbrt_name) -%} + {{ ids_var_name }}.code.name = "{{sbrt_name}}"; + {{ ids_var_name }}.code.version = ""; +{%- endmacro -%} + +{% macro deallocate(ids_var_name) -%} + // Should not be called until IMAS-3937 will be resolved + // {{ ids_var_name }}.clear(); +{%- endmacro -%} \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.cpp.jinja2 b/iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.cpp.jinja2 new file mode 100644 index 0000000..e2cf310 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.cpp.jinja2 @@ -0,0 +1,356 @@ +{% import './macros/%s_ids.jinja2' % code_description.implementation.data_type as ids_macro %} + +{%- import './macros/flags.jinja2' as flags with context-%} + +#include +#include +#include +#include + +//for code_parameters copying +#include +#include +#include +#include +#include + +{{ ids_macro.imports(build_info.al_version) }} + +#include "muscle3_tools.h" + +using libmuscle::Data; +using libmuscle::DataConstRef; +using libmuscle::Message; +using ymmsl::Operator; + +{% if flags.mpi_code %} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// INIT MPI +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +int init_mpi() +{ + int mpi_rank; + int was_mpi_initialized; + + MPI_Initialized(&was_mpi_initialized); + if (!was_mpi_initialized) + MPI_Init(NULL, NULL); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + return mpi_rank; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// FINISH MPI +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void finish_mpi() +{ + int was_mpi_finalized; + //---- MPI Finalization ---- + MPI_Finalized(&was_mpi_finalized); + if (!was_mpi_finalized) + MPI_Finalize(); +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// BROADCAST IDS +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void broadcast_ids(char** serialized_ids, int *serialized_ids_size) +{ + // Share information among all processors + MPI_Bcast(serialized_ids_size, 1, MPI_INTEGER, MPI_ROOT_RANK, MPI_COMM_WORLD); + + if( *serialized_ids == NULL && *serialized_ids_size > 0) // Not a MPI root + { + *serialized_ids = (char*) malloc(*serialized_ids_size); + } + + MPI_Bcast( *serialized_ids, *serialized_ids_size, MPI_CHARACTER, MPI_ROOT_RANK, MPI_COMM_WORLD); +} +{% endif %} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// INIT MUSCLE3 +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +libmuscle::Instance* init_muscle3() +{ + libmuscle::Instance* m3_instance; + std::vector input_ports; + std::vector output_ports; + {% if flags.checkpointable %} + libmuscle::InstanceFlags flags = libmuscle::InstanceFlags::USES_CHECKPOINT_API; + {% else %} + libmuscle::InstanceFlags flags = libmuscle::InstanceFlags::NONE; + {% endif %} + + /* * * * - - - INPUT ('F_INIT') ports - - - * * * */ +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + // {{ argument.name }} + input_ports.push_back("{{ argument.name }}"); + +{% endfor %} + + /* * * * - - - OUTPUT ('O_F') ports - - - * * * */ +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + // {{ argument.name }} + output_ports.push_back("{{ argument.name }}"); + + {% endfor %} + + + {% if flags.mpi_code %} + m3_instance = new libmuscle::Instance(0, NULL, + {% raw %}{{Operator::F_INIT, input_ports}, {Operator::O_F, output_ports}}, {% endraw %} + flags, + MPI_COMM_WORLD, MPI_ROOT_RANK); + {% else %} + m3_instance = new libmuscle::Instance(0, NULL, + {% raw %} {{Operator::F_INIT, input_ports}, {Operator::O_F, output_ports}}, {% endraw %} + flags); + {% endif %} + + return m3_instance; +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// FINISH MUSCLE3 +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void finish_muscle3(libmuscle::Instance* m3_instance) +{ + + delete(m3_instance); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// RECEIVE IDS +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void receive_ids(libmuscle::Instance* m3_instance, std::string argument_name, IdsNs::Ids* ids) +{ + char* serialized_ids = NULL; + int serialized_ids_size = 0; + + auto msg = m3_instance->receive(argument_name); + libmuscle::DataConstRef data = msg.data(); + + if(! data.is_nil() ) // MPI root or a single process + { + serialized_ids_size = data.size(); + serialized_ids = (char*)data.as_byte_array(); + } + + {% if flags.mpi_code %} + broadcast_ids(&serialized_ids, &serialized_ids_size); + {% endif %} + + std::string ids_string(serialized_ids, serialized_ids_size); + + ids->deserialize(ids_string); + + if( data.is_nil() ) // should be called for MPI run (not a root process) + free(serialized_ids); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// GET INPUT IDSES +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void receive_input_idses( +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ ids_macro.argument(argument.type, argument.name) }}, +{% endfor %} + libmuscle::Instance* m3_instance) +{ + + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + //--------- RECEIVE IDS : {{ argument.name }} ------------------------ + receive_ids(m3_instance, "{{ argument.name }}", &{{ argument.name }}); + +{% endfor %} +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// SEND IDS +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void send_ids(libmuscle::Instance* m3_instance, std::string argument_name, IdsNs::Ids* ids, double timestamp) +{ + std::string ids_str; + + ids_str = ids->serialize(DEFAULT_SERIALIZER_PROTOCOL); + auto ids_bytes = libmuscle::Data::byte_array(ids_str.data(), ids_str.size()); + libmuscle::Message msg( timestamp, ids_bytes); + + m3_instance->send(argument_name, msg); +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// SEND OUTPUT IDSES +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +void send_output_idses(double timestamp, +{%- for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' -%} + {{ ids_macro.argument(argument.type, argument.name) }}, +{% endfor %} + libmuscle::Instance* m3_instance) +{ + int time_size = -1.0; + + + + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + //--------- SEND IDS : {{ argument.name }} ------------------------ + {% if not code_description.implementation.subroutines.get_timestamp %} + timestamp = -1.0; // resetting value + time_size = {{ argument.name }}.time.size(); + if (time_size > 0) + { + timestamp = {{ argument.name }}.time(time_size - 1); + } + {% endif %} + send_ids(m3_instance, "{{ argument.name }}", &{{ argument.name }}, timestamp); + +{% endfor %} +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// READ CODE PARAMETERS +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +void read_code_parameters(libmuscle::Instance* m3_instance, char* exec_path, IdsNs::codeparam_t &imas_code_params) +{ + std::string code_parameters_file_path; + std::string exec_path_str(exec_path); + bool copy_xml_parameters = false; + std::string default_file_name = "{{ (code_description.implementation.code_parameters.parameters or "") | basename }}"; + + try + { + // checking if property 'parameters_file' can be read + code_parameters_file_path = m3_instance->get_setting_as("parameters_file"); + } + catch (std::out_of_range& exc) + { + //Value for setting parameters_file was not set. Using default file. + string directory; + const size_t last_slash_idx = exec_path_str.rfind('/'); + directory = exec_path_str.substr(0, last_slash_idx); + code_parameters_file_path = directory + "/../input/" + default_file_name; + } + + read_code_parameters_file(code_parameters_file_path, imas_code_params); + + try + { + // checking if property 'copy_code_parameters' can be read + copy_xml_parameters = m3_instance->get_setting_as("copy_xml_parameters"); + + if(copy_xml_parameters) + { + // copy code parameters into workdir + // create file descriptors + int fd_in = open(code_parameters_file_path.c_str(),O_RDONLY); + int fd_out = open(default_file_name.c_str(),O_RDWR); + struct stat stat_buf ; + + fstat(fd_in,&stat_buf); + ssize_t size = sendfile(fd_out,fd_in,0,stat_buf.st_size); + + close(fd_in); + close(fd_out); + } + } + catch (std::out_of_range& exc) + { + //just do not copy parameters file + } +} + +void read_code_parameters_file(std::string file_path, IdsNs::codeparam_t &imas_code_params) +{ + std::string code_parameters; + char* cstr; + + code_parameters = read_file( file_path); + + cstr = (char*) malloc(code_parameters.size() + 1); + strcpy(cstr, code_parameters.c_str()); + + *(imas_code_params.parameters) = cstr; + imas_code_params.default_param = NULL; + imas_code_params.schema = NULL; +} + +std::string read_file(std::string file_path) +{ + std::ifstream file; + std::stringstream buffer; + std::string file_content; + + file = std::ifstream (file_path); + buffer << file.rdbuf(); + file_content = buffer.str(); + return file_content; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// XXX XXX +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +void handle_status_info(int status_code, std::string status_message, const std::string actor_name) +{ + std::string message = ""; + if(status_message.empty()) + { + message = status_message; + } + + if(status_code !=0) { + printf("---Diagnostic information returned from *** %s ***:---\n", actor_name); + printf("-------Status code : %d\n", status_code); + printf("-------Status message : %s\n", message); + printf("---------------------------------------------------------\n"); + } +} + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// RESTORING CODE STATE +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +{% if flags.checkpointable %} +std::string receive_code_state(libmuscle::Instance* m3_instance) +{ + std::string state_str; + + //---- Status info ---- + int status_code = 0; + std::string status_message; + + libmuscle::Message m3_message = m3_instance->load_snapshot(); + libmuscle::DataConstRef m3_data = m3_message.data(); + + if (!m3_data.is_nil()) { + state_str = m3_data.as(); + + return state_str; + + } +} // receive_code_state + + +void save_code_state(libmuscle::Instance* m3_instance, std::string state_str) +{ + double timestamp = -1.0; + + //---- Status info ---- + int status_code = 0; + std::string status_message; + + libmuscle::DataConstRef m3_data = libmuscle::DataConstRef(state_str); + libmuscle::Message m3_message(timestamp, m3_data); + m3_instance->save_final_snapshot(m3_message); + + } // save_code_state +{% else %} +// Subroutines not generated - no GET_STATE/SET_STATE methods provided +{% endif %} \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.h.jinja2 b/iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.h.jinja2 new file mode 100644 index 0000000..9ac7f49 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/resources/src/muscle3_tools.h.jinja2 @@ -0,0 +1,55 @@ +{% import './macros/%s_ids.jinja2' % code_description.implementation.data_type as ids_macro %} +{%- import './macros/flags.jinja2' as flags with context-%} + + +#ifndef _IWRAP_TOOLS +#define _IWRAP_TOOLS + +#include +{{ ids_macro.imports(build_info.al_version) }} + + +const int MPI_ROOT_RANK = 0; + +{% if flags.mpi_code %} +int init_mpi(); +void finish_mpi(); +{% endif %} + +libmuscle::Instance* init_muscle3(); +void finish_muscle3(libmuscle::Instance* m3_instance); + +void send_ids(libmuscle::Instance* m3_instance, std::string argument_name, IdsNs::Ids* ids, double timestamp); +void send_output_idses(double timestamp, +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + {{ ids_macro.argument(argument.type, argument.name) }}, +{% endfor %} + libmuscle::Instance* m3_instance); + +void receive_ids(libmuscle::Instance* m3_instance, std::string argument_name, IdsNs::Ids* ids); +void receive_input_idses( +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ ids_macro.argument(argument.type, argument.name) }}, +{% endfor %} + libmuscle::Instance* m3_instance); + +void read_code_parameters(libmuscle::Instance* m3_instance, char* exec_path, IdsNs::codeparam_t &imas_code_params); + +void read_code_parameters_file(std::string file_path, IdsNs::codeparam_t &imas_code_params); + +std::string read_file(std::string file_path); + +void handle_status_info(int status_code, std::string status_message, const std::string actor_name); + +void release_status_info(char* status_message); + +// RESTORING CODE STATE +{% if flags.checkpointable %} +std::string receive_code_state(libmuscle::Instance* m3_instance); +void save_code_state(libmuscle::Instance* m3_instance, std::string state_str); +{% else %} +// Subroutines not generated - no GET_STATE/SET_STATE methods provided +{% endif %} + + +#endif // _IWRAP_TOOLS \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_cpp/resources/src/standalone.cpp.jinja2 b/iwrap/generators/actor_generators/muscle3_cpp/resources/src/standalone.cpp.jinja2 new file mode 100644 index 0000000..3228bea --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_cpp/resources/src/standalone.cpp.jinja2 @@ -0,0 +1,217 @@ +{% import './macros/%s_ids.jinja2' % code_description.implementation.data_type as ids_macro -%} + +{%- import './macros/flags.jinja2' as flags with context -%} +{%- if flags.skeleton_only %} + {% set comment_out = '//' -%} +{% endif %} + +{%- macro skeleton_call(method_name) -%} + // >>> Subroutine {{ method_name }} should be called here <<< +{%- endmacro -%} + +#include +{% if flags.mpi_code %} + +#include +{% endif %} +#include "muscle3_tools.h" +{{ ids_macro.imports(build_info.al_version) }} +{%if code_description.implementation.include_path %} +#include "{{code_description.implementation.include_path | basename }}" +{% else %} +// Put '#include ".h"' here +{% endif %} + + +int main(int argc, char **argv) +{ + libmuscle::Instance* m3_instance; + double timestamp = -1.0; + + int mpi_rank = 0; + //---- Status info ---- + int status_code = 0; + std::string status_message; + //---- Code parameters ---- + IdsNs::codeparam_t imas_code_params; +{% if flags.checkpointable %} + std::string state_str; +{% endif %} + +{% if code_description.implementation.subroutines.init.name or flags.skeleton_only %} + bool initial_run = true; +{% endif %} + + //---- IN/OUT IDSes ---- +{% for argument in code_description.implementation.subroutines.main.arguments %} + {{ ids_macro.declare(argument.type, argument.name) }} +{% endfor %} + + /* * * * - - - INITIALISATION - - - * * * */ +{% if flags.mpi_code %} + mpi_rank = init_mpi(); +{% endif %} + m3_instance = init_muscle3(); + + {% if flags.code_parameters_provided %} + //---- Code parameters ---- + read_code_parameters(m3_instance, argv[0], "{{code_description.implementation.code_parameters.parameters | basename }}", imas_code_params); + {% endif %} + + /* * * * - - - MUSCLE3 LOOP - - - * * * */ + while (m3_instance->reuse_instance()) { + + // - - - WRAPPED CODE INITIALIZATION - - - +{% if flags.checkpointable %} + // > > > RESTART : the code is restored from the saved snapshot < < < + if (m3_instance->resuming()) { + if (mpi_rank == MPI_ROOT_RANK){ + state_str = receive_code_state(m3_instance); + // Calling SET_STATE + {% if flags.skeleton_only %} + // + // >>> Subroutine "SET_STATE" should be called here <<< + // + {% endif %} + {{ comment_out }} {{code_description.implementation.subroutines.set_state}}(state_str, status_code, status_message); + + } + } +{% endif %} + +{%- if code_description.implementation.subroutines.init.name %} + {% if flags.checkpointable %} + else // m3_instance is not resuming + { // > > > INITIALIZATION by calling INIT method of the code < < < + {% endif %} + + /* * * * - - - WRAPPED CODE CALL - INIT SBRT - - - * * * */ + if (initial_run) { + {% if flags.skeleton_only %} + // + // >>> Subroutine "INIT" should be called here <<< + // + {% endif %} + {{ comment_out }} {{code_description.implementation.subroutines.init.name}} ( + {% if flags.code_parameters_provided %} + {{ comment_out }} imas_code_params, + {% endif %} + {{ comment_out }} status_code, status_message); + } + {% if flags.checkpointable %} + } // m3_instance->resuming() + {% endif %} + initial_run = false; + {% endif %} + +{% if flags.checkpointable %} + if(m3_instance->should_init()) { +{% endif %} + + /* * * * - - - RECEIVING INPUT IDSes - - - * * * */ + receive_input_idses( + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ argument.name }}, + {% endfor %} + m3_instance ); + +{% if flags.checkpointable %} + } // m3_instance->should_init() +{% endif %} + + /* * * * - - - WRAPPED CODE CALL - MAIN SBRT - - - * * * */ + {% if flags.skeleton_only %} + // + // >>> Subroutine "MAIN" should be called here <<< + // + {% endif %} + {{ comment_out }} {{code_description.implementation.subroutines.main.name}}( + {% for argument in code_description.implementation.subroutines.main.arguments %} + {{ comment_out }} {{ argument.name }}, + {% endfor %} + {% if flags.code_parameters_provided %} + {{ comment_out }} imas_code_params, + {% endif %} + {{ comment_out }} status_code, status_message ); + + handle_status_info(status_code, status_message, "{{actor_description.actor_name}}"); + + + {% if flags.mpi_code %} + if (mpi_rank == MPI_ROOT_RANK) + { + // --- called only for RANK 0 process +{% endif %} + + /* * * * - - - SENDING OUTPUT IDSes - - - * * * */ + {% if code_description.implementation.subroutines.get_timestamp %} + // > > > GET TIMESTAMP < < < + {% if flags.skeleton_only %} + // + // >>> Subroutine "GET_TIMESTAMP" should be called here <<< + // + {% endif %} + {{ comment_out }} {{code_description.implementation.subroutines.get_timestamp}}(timestamp, status_code, status_message); + handle_status_info(status_code, status_message, "{{actor_description.actor_name}}"); + + {% endif %} + send_output_idses(timestamp, + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + {{ argument.name }}, + {% endfor %} + m3_instance); + +{% if flags.mpi_code %} + } //The end of section called only for RANK 0 process +{% endif %} + + + +{% if flags.checkpointable %} + // > > > SAVE SNAPSHOT < < < + if (m3_instance->should_save_final_snapshot()) { + if (mpi_rank == MPI_ROOT_RANK ) { + // Calling GET_STATE + {% if flags.skeleton_only %} + // + // >>> Subroutine "GET_STATE" should be called here <<< + // + {% endif %} + {{ comment_out }} {{code_description.implementation.subroutines.get_state}}(state_str, status_code, status_message); + + save_code_state(m3_instance, state_str); + } + } +{% endif %} + + /* * * * - - - CLEAN UP - - - * * * */ + {% for argument in code_description.implementation.subroutines.main.arguments %} + {{ ids_macro.deallocate( argument.name) }} + {% endfor %} + + } // > > > > THE END OF MUSCLE3 LOOP + + /* * * * - - - FINALIZATION - - - * * * */ + {% if code_description.implementation.subroutines.finalize.name %} + + /* * * * - - - WRAPPED CODE CALL - FINISH SBRT - - - * * * */ + {% if flags.skeleton_only %} + // + // >>> Subroutine "FINALIZE" should be called here <<< + // + {% endif %} + {{ comment_out }} {{code_description.implementation.subroutines.finalize.name}} (status_code, status_message); + + handle_status_info(status_code, status_message, "{{actor_description.actor_name}}"); + {% endif %} + + {% if flags.code_parameters_provided %} + // ------Deallocating code parameters ---------------------------- + {% endif %} + + {% if flags.mpi_code %} + finish_mpi(); + {% endif %} + + finish_muscle3(m3_instance); +} diff --git a/iwrap/generators/actor_generators/muscle3_fortran/__init__.py b/iwrap/generators/actor_generators/muscle3_fortran/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iwrap/generators/actor_generators/muscle3_fortran/m3_fortran_actor.py b/iwrap/generators/actor_generators/muscle3_fortran/m3_fortran_actor.py new file mode 100644 index 0000000..8895537 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/m3_fortran_actor.py @@ -0,0 +1,119 @@ +import logging +import os +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Set, List + +from iwrap.generation_engine.utils.jinja2_template_processing import process_template_dir +from iwrap.generators.actor_generators import ActorGenerator +from iwrap.settings.project import ProjectSettings + + +import jinja2 +import sys + +from iwrap.settings.platform.platform_settings import PlatformSettings + +from iwrap.generators.actor_generators.muscle3_common import m3_utils + + +class FortranPAFActorGenerator(ActorGenerator): + # Class logger + __logger = logging.getLogger(__name__ + "." + __qualname__) + + COMPLIANT_API = '2.1' + + @property + def type(self) -> str: + return 'MUSCLE3-Fortran' + + @property + def name(self) -> str: + return 'MUSCLE3 (Fortran)' + + @property + def description(self) -> str: + return 'Wrapping Fortran code into MUSCLE3 micro model' + + @property + def actor_language(self) -> List[str]: + return 'fortran' + + @property + def actor_data_types(self) -> List[str]: + return ['legacy'] + + @property + def code_data_types(self) -> List[str]: + return ['legacy'] + + @property + def code_languages(self) -> Set[str]: + return {'fortran'} + + def __init__(self): + + self.__info_output_stream = None + self.temp_dir: tempfile.TemporaryDirectory = None + self.jinja_env: jinja2.Environment = None + self.install_dir: str = None + self.skeleton_generation = False + + def configure(self, info_output_stream=sys.stdout): + self.__info_output_stream = info_output_stream + + def validate(self, project_settings: dict = None): + m3_utils.validate( project_settings ) + + def initialize(self, project_settings: dict = None): + install_dir = None + + if project_settings: + install_dir = project_settings['actor_description'].get( 'install_dir' ) + programming_language = project_settings['code_description'].get('implementation', {}).get('programming_language') + if not programming_language: + self.skeleton_generation = True + + if not install_dir: + install_dir = PlatformSettings().directories.actor_install_dir + self.install_dir: str = str(Path(install_dir, ProjectSettings.get_settings().actor_description.actor_name)) + + def generate(self, project_settings: dict): + self.cleanup(project_settings) + os.makedirs( self.install_dir, exist_ok=True ) + + current_path = os.path.dirname(os.path.realpath(__file__)) + process_template_dir( None, current_path + '/resources', self.install_dir, project_settings, + filter_func=m3_utils.template_filter_func, + output_stream= self.__info_output_stream, ) + + m3_utils.copy_native_lib( self.install_dir, project_settings, "lib" ) + m3_utils.copy_include( self.install_dir, project_settings ) + m3_utils.copy_extra_libs( self.install_dir, project_settings ) + m3_utils.copy_code_params_files( self.install_dir, project_settings ) + m3_utils.copy_build_info( self.install_dir, project_settings ) + + def build(self, project_settings: dict): + + if self.skeleton_generation: + return + + proc = subprocess.Popen( [], executable = "make", cwd=self.install_dir, + encoding='utf-8', text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) + + for line in proc.stdout: + print( line, file=self.__info_output_stream, end='' ) + + return_code = proc.wait() + if return_code: + raise subprocess.CalledProcessError( return_code, 'make' ) + + def install(self, project_settings: dict): + ... + + def cleanup(self, project_settings: dict): + shutil.rmtree(self.install_dir, ignore_errors=True) + + diff --git a/iwrap/generators/actor_generators/muscle3_fortran/resources/Makefile.jinja2 b/iwrap/generators/actor_generators/muscle3_fortran/resources/Makefile.jinja2 new file mode 100644 index 0000000..ce39209 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/resources/Makefile.jinja2 @@ -0,0 +1,94 @@ +# actor name +ACTOR_NAME={{actor_description.actor_name}} + +{% if code_description.settings.compiler_cmd %} +# Fortran compiler name +FC={{ code_description.settings.compiler_cmd }} +{% endif %} + +{% if code_description.settings.mpi_compiler_cmd %} +# MPI Fortran compiler - can be empty for non-MPI code +MPIFC={{ code_description.settings.mpi_compiler_cmd or '' }} +{% endif %} + +WRAPPER_DIR=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) + +# Open MP Flag if the code requires it +COMPILER_FLAGS={{ code_description.settings.compiler_flags or '' }} + +MOD_DIR=./build/ + +#TO DO - any clever way to do this? +ifeq ($(FC),$(filter $(FC),ifort ifx)) +# INTEL + FLAGS=-g -fPIC -module $(MOD_DIR) +else +# GFORTRAN + FLAGS=-g -fPIC -J$(MOD_DIR) +endif + +FCLAGS=$(FLAGS) $(COMPILER_FLAGS) + +# the name of static library file containing wrapped code +CODE_LIB_DEF= +{%- if code_description.implementation.code_path -%} +./lib/{{code_description.implementation.code_path | basename}} +{% endif %} + +{% if code_description.settings.mpi_compiler_cmd %} +MUSCLE3_PKG=libmuscle_mpi_fortran +{% else %} +MUSCLE3_PKG=libmuscle_fortran +{% endif %} + +# main IMAS libs to be used +{% if build_info.al_version.startswith('4.') %} +IMAS_LIB_NAME=imas-$(FC) +{% else %} +IMAS_LIB_NAME=al-fortran +{% endif %} +CORE_LIBS_FLAGS:= $(shell pkg-config --cflags "$(IMAS_LIB_NAME) ymmsl $(MUSCLE3_PKG)") +CORE_LIBS:=$(shell pkg-config --libs "$(IMAS_LIB_NAME)" ymmsl $(MUSCLE3_PKG)) + +# required libs published using pkg-config mechanism +{% if code_description.settings and code_description.settings.extra_libraries.pkg_config_defined %} +LIBS_PKGCONFIG_DEFINED_NAMES={{ (code_description.settings.extra_libraries.pkg_config_defined or []) | join(' ')}} +LIBS_PKGCONFIG_DEFINED_FLAGS=$(shell pkg-config --cflags $(LIBS_PKGCONFIG_DEFINED_NAMES)) +LIBS_PKGCONFIG_DEFINED=$(shell pkg-config --libs $(LIBS_PKGCONFIG_DEFINED_NAMES)) +{% else %} +LIBS_PKGCONFIG_DEFINED_NAMES= +LIBS_PKGCONFIG_DEFINED_FLAGS= +LIBS_PKGCONFIG_DEFINED= +{% endif %} + +# required libs provided by user as a path +LIBS_PATH_DEFINED= +{%- if code_description.settings %} +{%- for lib_path in code_description.settings.extra_libraries.path_defined or [] %} + ./extra-libs/{{ lib_path | basename }} +{%- endfor %} +{% endif %} + +INCLUDES=-I./include $(CORE_LIBS_FLAGS) $(LIBS_PKGCONFIG_DEFINED_FLAGS) +LIBS=$(CORE_LIBS) $(LIBS_PKGCONFIG_DEFINED) $(LIBS_PATH_DEFINED) $(CODE_LIB_DEF) + +{% if code_description.settings.mpi_compiler_cmd %} +# MPI compilation wrapper will be used +FC=$(MPIFC) + +{% endif %} +all: ./bin/$(ACTOR_NAME).exe + +./bin/$(ACTOR_NAME).exe: src/standalone.f90 build/fortrantools.o build/muscle3_tools.o + @echo " * $^ -> $@" + @mkdir -p ./bin + $(FC) $(FCLAGS) -o $@ -Wl,--start-group $^ $(INCLUDES) -I$(MOD_DIR) $(LIBS) -Wl,--end-group -Wl,-rpath,$(WRAPPER_DIR)/extra-libs/ + +build/%.o: src/%.f90 + @echo $(PWD) + @echo " * $< -> $@" + @mkdir -p ./build + $(FC) $(FCLAGS) -c -w $^ -o $@ $(INCLUDES) + +clean: + -rm -f *.o build/* ../bin/* diff --git a/iwrap/generators/actor_generators/muscle3_fortran/resources/macros/flags.jinja2 b/iwrap/generators/actor_generators/muscle3_fortran/resources/macros/flags.jinja2 new file mode 100644 index 0000000..7c727ce --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/resources/macros/flags.jinja2 @@ -0,0 +1,5 @@ +{%- set checkpointable = code_description.implementation.subroutines.get_state and code_description.implementation.subroutines.set_state %} +{%- set code_parameters_provided = code_description.implementation.code_parameters.parameters %} +{%- set mpi_code = code_description.settings.mpi_compiler_cmd -%} + +{%- set skeleton_only = not code_description.implementation.programming_language -%} \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_fortran/resources/macros/legacy_ids.jinja2 b/iwrap/generators/actor_generators/muscle3_fortran/resources/macros/legacy_ids.jinja2 new file mode 100644 index 0000000..76f1fcc --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/resources/macros/legacy_ids.jinja2 @@ -0,0 +1,26 @@ +{% macro imports() -%} + use ids_schemas + use ids_routines +{%- endmacro -%} + +{% macro declare(ids_name, ids_var_name) -%} + ! {{ ids_var_name }} + type (ids_{{ ids_name }}) :: {{ ids_var_name }} +{%- endmacro -%} + +{%- macro provenance(ids_var_name, sbrt_name) -%} + if (associated({{ ids_var_name }}%code%name)) then + deallocate({{ ids_var_name }}%code%name) + endif + if (associated({{ ids_var_name }}%code%version)) then + deallocate({{ ids_var_name }}%code%version) + endif + allocate({{ ids_var_name }}%code%name(1)) + allocate({{ ids_var_name }}%code%version(1)) + {{ ids_var_name }}%code%name(1) = "{{sbrt_name}}" + {{ ids_var_name }}%code%version(1) = "" +{%- endmacro -%} + +{% macro deallocate(ids_var_name) -%} + call ids_deallocate({{ ids_var_name }}) +{%- endmacro -%} \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_fortran/resources/src/fortrantools.f90 b/iwrap/generators/actor_generators/muscle3_fortran/resources/src/fortrantools.f90 new file mode 100644 index 0000000..95572dc --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/resources/src/fortrantools.f90 @@ -0,0 +1,145 @@ + +{%- import './macros/flags.jinja2' as flags with context-%} + +module iwrap_tools + {% if flags.mpi_code %} + use libmuscle_mpi + {% else %} + use libmuscle + {% endif %} + + implicit none + +contains + + SUBROUTINE check_status(instance, status_code, status_message, actor_name, actor_method) + + type(LIBMUSCLE_Instance), INTENT(IN) :: instance + integer :: status_code + character(*) :: actor_name + character(*) :: actor_method + character(:), pointer :: status_message + character(:), allocatable :: status_info + + if(associated(status_message)) then + status_info = status_message + deallocate(status_message) + else + status_info = "" + end if + + ! NO ERROR + if(status_code == 0) return + + ! WARNING + if(status_code > 0) then + print *, "---WARNING returned from *** ", actor_name, "/", actor_method, " ***:---" + print *, "-------Output flag : ", status_code + print *, "-------Status info: ", status_info + print *, "---------------------------------------------------------" + + end if + + ! ERROR + if(status_code < 0) then + print *, "---WARNING returned from *** ", actor_name, "/", actor_method, " ***:---" + print *, "-------Output flag : ", status_code + print *, "-------Status info: ", status_info + print *, "---------------------------------------------------------" + CALL LIBMUSCLE_Instance_error_shutdown(instance, "*"//actor_name//"/"//actor_method//"* error: "//status_info) + CALL exit(status_code) + end if + + END SUBROUTINE check_status + + + FUNCTION get_exec_dir() RESULT(exec_path) + CHARACTER(len=2055) :: exec_path + integer :: idx + + CALL get_command_argument(0, exec_path) + + idx = index(trim(exec_path), '/', .True.) + exec_path = exec_path(:idx) + + END FUNCTION get_exec_dir + + + FUNCTION read_code_parameters_file(file_name, imas_code_params) RESULT(status) + use ids_schemas, ONLY: ids_parameters_input + character(len=:),allocatable, intent(in) :: file_name + type(ids_parameters_input), intent(OUT) :: imas_code_params + character(len=:), allocatable :: code_parameters_str + integer :: status + integer :: iloopmax, string_size, err_code + + status = read_file(file_name, code_parameters_str) + if (status /= 0) return + + string_size = LEN_TRIM(code_parameters_str) + + iloopmax=string_size/132 + if (mod(string_size,132)/=0) then + iloopmax = iloopmax + 1 + endif + allocate(imas_code_params%parameters_value(iloopmax)) + + imas_code_params%parameters_value = transfer(code_parameters_str(1:string_size), imas_code_params%parameters_value) + + if(mod(string_size,132)/=0) then + imas_code_params%parameters_value(iloopmax)(mod(string_size,132)+1:132) = ' ' + endif + + END FUNCTION read_code_parameters_file + + !--------------------------------------------------- + FUNCTION read_file(filename, str) RESULT(status) + implicit none + + character(len=*),intent(in) :: filename + character(len=:),allocatable, intent(out) :: str + + !local variables: + integer :: iunit,istat,filesize, status + character(len=1) :: c + + status = 0 + + open(newunit=iunit,file=filename,status='OLD',& + form='UNFORMATTED',access='STREAM',iostat=istat) + + if (istat /=0) then + write(*,*) 'Error opening file: ', filename + status = -1 + return + end if + + !how many characters are in the file: + inquire(file=filename, size=filesize) + if (filesize < 1) then + write(*,*) 'Error getting file size: ', filename + status = -1 + return + end if + + !read the file all at once: + allocate( character(len=filesize) :: str ) + read(iunit,pos=1,iostat=istat) str + + if (istat /=0 ) then + write(*,*) 'Error reading file: ', filename + status = -1 + return + end if + + !make sure it was all read by trying to read more: + read(iunit,pos=filesize+1,iostat=istat) c + if (.not. IS_IOSTAT_END(istat)) & + write(*,*) 'Error: file was not completely read.' + + close(iunit, iostat=istat) + end function read_file + +end module iwrap_tools + + diff --git a/iwrap/generators/actor_generators/muscle3_fortran/resources/src/muscle3_tools.f90.jinja2 b/iwrap/generators/actor_generators/muscle3_fortran/resources/src/muscle3_tools.f90.jinja2 new file mode 100644 index 0000000..a347bed --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/resources/src/muscle3_tools.f90.jinja2 @@ -0,0 +1,359 @@ +{% import './macros/%s_ids.jinja2' % code_description.implementation.data_type as ids_macro %} + +{%- import './macros/flags.jinja2' as flags with context-%} + +MODULE muscle3_tools + use ymmsl + {% if flags.mpi_code %} + use mpi + use libmuscle_mpi + {% else %} + use libmuscle + {% endif %} + use ids_schemas + use ids_routines + use iwrap_tools + implicit none + + integer, parameter :: MPI_ROOT_RANK = 0 +contains + + +{% if flags.mpi_code %} + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! INIT MPI + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE init_mpi(process_rank) + + integer, intent(OUT) :: process_rank + integer :: status + logical :: was_initialized + + call MPI_Init(status) + call MPI_COMM_RANK(MPI_COMM_WORLD, process_rank, status) + + END SUBROUTINE init_mpi + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! FINISH MPI + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE finish_mpi() + + integer :: status + logical :: was_mpi_finalized + + call MPI_finalized(was_mpi_finalized, status) + if (.not. was_mpi_finalized) call MPI_Finalize(status) + + END SUBROUTINE finish_mpi + + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! BROADCAST IDS + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE broadcast_ids(serialized_ids) + + character(len=1), dimension(:), allocatable :: serialized_ids + integer :: ierror + integer :: serialized_ids_size = 0 + + ! Share information among all processors + if(allocated(serialized_ids)) serialized_ids_size = SIZE(serialized_ids) + + CALL mpi_bcast(serialized_ids_size, 1, MPI_INTEGER, MPI_ROOT_RANK, MPI_COMM_WORLD, ierror) + if(.not. allocated(serialized_ids) .and. serialized_ids_size > 0) allocate(serialized_ids(serialized_ids_size)) + CALL mpi_bcast(serialized_ids, serialized_ids_size, MPI_CHARACTER, MPI_ROOT_RANK, MPI_COMM_WORLD, ierror) + + END SUBROUTINE broadcast_ids + +{% endif %} + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! INIT MUSCLE3 + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE init_muscle(instance) + + type(LIBMUSCLE_Instance), INTENT(OUT) :: instance + type(LIBMUSCLE_PortsDescription) :: ports + + logical, parameter :: CHECKPOINT_API_ENABLED = {% if flags.checkpointable %}.TRUE.{% else %}.FALSE.{% endif %} + + + ports = LIBMUSCLE_PortsDescription_create() + + ! Creation of INPUT ('F_INIT') ports + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + ! {{ argument.name }} + CALL LIBMUSCLE_PortsDescription_add(ports, YMMSL_OPERATOR_F_INIT, '{{ argument.name }}') + {% endfor %} + + ! Creation of OUTPUT ('O_F') ports + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + ! {{ argument.name }} + CALL LIBMUSCLE_PortsDescription_add(ports, YMMSL_OPERATOR_O_F, '{{ argument.name }}') + {% endfor %} + + {% if flags.mpi_code %} + instance = LIBMUSCLE_Instance_create(ports, LIBMUSCLE_InstanceFlags(USES_CHECKPOINT_API=CHECKPOINT_API_ENABLED), & + MPI_COMM_WORLD, MPI_ROOT_RANK) + {% else %} + instance = LIBMUSCLE_Instance_create(ports, LIBMUSCLE_InstanceFlags( USES_CHECKPOINT_API=CHECKPOINT_API_ENABLED)) + {% endif %} + + call LIBMUSCLE_PortsDescription_free(ports) + + END SUBROUTINE init_muscle + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! SEND IDS + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE send_ids(instance, serialized_ids, ids_name, time_stamp) + + type(LIBMUSCLE_Instance) :: instance + character(len=1), dimension(:), allocatable :: serialized_ids + character(len=*) :: ids_name + real (kind=LIBMUSCLE_int8) :: time_stamp + real (kind=LIBMUSCLE_int8) :: next_time_stamp + + type(LIBMUSCLE_Message) :: m3_message + type(LIBMUSCLE_Data) :: m3_data + + m3_data = LIBMUSCLE_Data_create_byte_array(serialized_ids) + m3_message = LIBMUSCLE_Message_create(time_stamp, m3_data) + + call LIBMUSCLE_Instance_send(instance, ids_name, m3_message) + call LIBMUSCLE_Message_free(m3_message) + + call LIBMUSCLE_Data_free(m3_data) + + END SUBROUTINE send_ids + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! RECEIVE IDS + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE receive_ids(instance, ids_name, serialized_ids) + + type(LIBMUSCLE_Instance), INTENT(IN) :: instance + character(len=*), INTENT(IN) :: ids_name + character(len=1), dimension(:), allocatable, INTENT(OUT) :: serialized_ids + + type(LIBMUSCLE_Message) :: m3_message + type(LIBMUSCLE_DataConstRef) :: m3_data + + m3_message = LIBMUSCLE_Instance_receive(instance, ids_name) + m3_data = LIBMUSCLE_Message_get_data(m3_message) + + if ( .not. LIBMUSCLE_DataConstRef_is_nil(m3_data)) then + allocate(serialized_ids(LIBMUSCLE_DataConstRef_size(m3_data))) + call LIBMUSCLE_DataConstRef_as_byte_array(m3_data, serialized_ids) + call LIBMUSCLE_DataConstRef_free(m3_data) + end if + call LIBMUSCLE_Message_free(m3_message) + + + END SUBROUTINE receive_ids + + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! RECEIVE INPUT IDSES + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE receive_input_idses(& + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ argument.name }}, & + {% endfor %} + instance) + + type(LIBMUSCLE_Instance), INTENT(IN) :: instance + character(len=1), dimension(:), allocatable :: serialized_ids + + {% if flags.mpi_code %} + ! workaround for fixed random seed and MPI (to be removed if IMAS-4524 solved) + integer, allocatable :: wa_seed(:) + integer :: wa_n + {% endif %} + + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ ids_macro.declare(argument.type, argument.name) }} + {% endfor %} + + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + !--------- RECEIVE IDS : {{ argument.name }} ------------------------ + CALL receive_ids(instance, '{{ argument.name }}', serialized_ids) + + {% if flags.mpi_code %} + CALL broadcast_ids(serialized_ids) ! MUSCLE3 sends data to root process only + + ! Workaround: get current random state (to be removed if IMAS-4524 solved) + call random_seed(size = wa_n) + allocate(wa_seed(wa_n)) + call random_seed(get = wa_seed) + ! replace with a unique random seed (hopefully unique across different MPI processes) + call random_seed() + {% endif %} + + CALL ids_deserialize(serialized_ids, {{ argument.name }}) + deallocate(serialized_ids) + + {% if flags.mpi_code %} + ! restore random seed (to be removed if IMAS-4524 solved) + call random_seed(put = wa_seed) + deallocate(wa_seed) + {% endif %} + + {% endfor %} + + END SUBROUTINE receive_input_idses + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! RETURN OUTPUT IDSES + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE send_output_idses(instance, & + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + {{ argument.name }}, & + {% endfor %} + in_timestamp) + + type(LIBMUSCLE_Instance), INTENT(IN) :: instance + real (kind=LIBMUSCLE_real8), INTENT(IN) :: in_timestamp + real (kind=LIBMUSCLE_real8) :: timestamp ! = -1.0_LIBMUSCLE_real8 + integer :: time_size + !---- Status info ---- + integer :: status_code = 0 + character(len=:), pointer :: status_message + + character(len=1), dimension(:), allocatable :: serialized_ids + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + {{ ids_macro.declare(argument.type, argument.name) }} + {% endfor %} + + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + !--------- SEND IDS : {{ argument.name }} ------------------------ + CALL ids_serialize({{ argument.name }}, serialized_ids) + if ( in_timestamp .GE. 0) then + timestamp = in_timestamp + else + timestamp = -1.0_LIBMUSCLE_real8 ! resetting a value + if ( ASSOCIATED({{ argument.name }}%time) .AND. SIZE({{ argument.name }}%time) > 0) then + time_size = SIZE({{ argument.name }}%time) + timestamp = {{ argument.name }}%time(time_size) + endif + endif + CALL send_ids(instance, serialized_ids, '{{ argument.name }}', timestamp ) + deallocate(serialized_ids) + + {% endfor %} + + END SUBROUTINE send_output_idses + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! RESTORING CODE STATE + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +{% if flags.checkpointable %} + SUBROUTINE receive_wrapped_code_state(instance, state_str) + type(LIBMUSCLE_Instance), intent(IN) :: instance + character(len=:), allocatable, intent(OUT) :: state_str + type(LIBMUSCLE_Message) :: m3_message + type(LIBMUSCLE_DataConstRef) :: m3_data + integer :: string_size + !---- Status info ---- + integer :: status_code = 0 + character(len=:), pointer :: status_message + + + m3_message = LIBMUSCLE_Instance_load_snapshot(instance) + m3_data = LIBMUSCLE_Message_get_data(m3_message) + + if ( .not. LIBMUSCLE_DataConstRef_is_nil(m3_data)) then + state_str = LIBMUSCLE_DataConstRef_as_character(m3_data ) + call LIBMUSCLE_DataConstRef_free(m3_data) + end if + call LIBMUSCLE_Message_free(m3_message) + END SUBROUTINE receive_wrapped_code_state + + SUBROUTINE save_wrapped_code_state(instance, state_str) + + type(LIBMUSCLE_Instance), intent(IN) :: instance + character(len=:), allocatable, intent(IN) :: state_str + type(LIBMUSCLE_Message) :: m3_message + type(LIBMUSCLE_Data) :: m3_data + integer :: string_size + real (kind=LIBMUSCLE_real8) :: timestamp ! = -1.0_LIBMUSCLE_real8 + !---- Status info ---- + integer :: status_code = 0 + character(len=:), pointer :: status_message + + m3_data = LIBMUSCLE_Data_create(state_str) + m3_message = LIBMUSCLE_Message_create(timestamp, m3_data) + CALL LIBMUSCLE_Instance_save_final_snapshot(instance, m3_message) + + call LIBMUSCLE_Data_free(m3_data) + call LIBMUSCLE_Message_free(m3_message) + END SUBROUTINE save_wrapped_code_state +{% else %} + ! Subroutines not generated - no GET_STATE/SET_STATE methods provided +{% endif %} + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! CHECK CONNECTION + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + SUBROUTINE check_connection(instance, port_name) + + type(LIBMUSCLE_Instance) :: instance + character(len=*) :: port_name + + if ( LIBMUSCLE_Instance_is_connected(instance, port_name)) return + + ! any error to report? + write(*,*) "ERROR: Port " // port_name //" not connected!" + call LIBMUSCLE_Instance_error_shutdown(instance, "ERROR: Port " // port_name //" not connected!") + call exit(1) + + END SUBROUTINE check_connection + + FUNCTION read_code_parameters(instance, imas_code_params) RESULT(status) + use ids_schemas, ONLY: ids_parameters_input + type(LIBMUSCLE_Instance), INTENT(IN) :: instance + character(len=:), allocatable :: default_file_name + character(len=:), allocatable :: exec_dir, file_path, err_msg + type(ids_parameters_input), intent(OUT) :: imas_code_params + integer :: status + logical :: copy_xml_parameters + copy_xml_parameters = .false. + + default_file_name = "{{ (code_description.implementation.code_parameters.parameters or "") | basename }}" + file_path = LIBMUSCLE_Instance_get_setting_as_character(instance, "parameters_file", status, err_msg) + + ! checking if property was read or not + if (status .ne. LIBMUSCLE_success) then + ! checking if an error occured while reading property + if (status .ne. LIBMUSCLE_out_of_range) then + write(*,*) " ERROR: Reading 'parameters_file' property failed! ", err_msg + return + end if + + ! reading default code parameters file + exec_dir = get_exec_dir() + exec_dir = trim(exec_dir) + file_path = exec_dir//"/../input/"//default_file_name + end if + + status = read_code_parameters_file(file_path, imas_code_params) + if (status /= 0) return + + copy_xml_parameters = LIBMUSCLE_Instance_get_setting_as_logical(instance, "copy_xml_parameters", status, err_msg) + + if (status .eq. LIBMUSCLE_success .and. & + copy_xml_parameters .eqv. .true. .and. & + associated(imas_code_params%parameters_value)) then + OPEN(UNIT=1, FILE=default_file_name, STATUS="REPLACE") + write(1, *) imas_code_params%parameters_value + CLOSE(UNIT=1) + else + !copy_xml_parameters setting is not mandatory so don't propagate error + status = 0 + return + end if + + END FUNCTION read_code_parameters + +END MODULE muscle3_tools \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_fortran/resources/src/standalone.f90.jinja2 b/iwrap/generators/actor_generators/muscle3_fortran/resources/src/standalone.f90.jinja2 new file mode 100644 index 0000000..43250e8 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_fortran/resources/src/standalone.f90.jinja2 @@ -0,0 +1,243 @@ +{% import './macros/%s_ids.jinja2' % code_description.implementation.data_type as ids_macro %} + +{%- import './macros/flags.jinja2' as flags with context-%} + +{%- if flags.skeleton_only %} + {% set comment_out = '!' -%} +{% endif %} + +! = = = = = = = ACTOR: {{actor_description.actor_name}} = = = = = = +program standalone + +{% if flags.mpi_code %} + use mpi +{% endif %} + use iwrap_tools + use muscle3_tools + {% if code_description.implementation.include_path %} + use {{code_description.implementation.include_path | stemname}} + {% else %} + ! Put "use " here + {% endif %} + implicit none + + !--------------------------------------------------------- + character(len=*), parameter :: ACTOR_NAME = "{{actor_description.actor_name}}" + type(LIBMUSCLE_Instance) :: instance + integer :: process_rank = MPI_ROOT_RANK + + !---- code state ---- + character(len=:), allocatable :: state_str + + !---- timestamp ---- + real (kind=LIBMUSCLE_real8) :: timestamp = -1.0_LIBMUSCLE_real8 + + !---- Status info ---- + integer :: status_code = 0 + character(len=:), pointer :: status_message + + !---- Code parameters ---- + character(len=:), allocatable :: xml_string + type(ids_parameters_input) :: imas_code_params + + !---- IN/OUT IDSes ---- +{% for argument in code_description.implementation.subroutines.main.arguments %} + {{ ids_macro.declare(argument.type, argument.name) }} +{% endfor %} + +{% if code_description.implementation.subroutines.init.name %} + logical :: initial_run + initial_run = .TRUE. +{% endif %} + + + ! > > > - - - INITIALISATION - - - < < < +{% if flags.mpi_code %} + CALL init_mpi(process_rank) +{% endif %} + + CALL init_muscle(instance) + +{% if flags.code_parameters_provided %} + ! ------------------ code parameters ---------------------------- + status_code = read_code_parameters(instance, imas_code_params) + if (status_code /= 0) CALL exit (status_code) + +{% endif %} + + ! > > > MUSCLE3 LOOP. < < < + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + + ! - - - WRAPPED CODE INITIALIZATION - - - # # # # +{% if flags.checkpointable %} + if ( LIBMUSCLE_Instance_resuming(instance)) then + ! > > > RESTART : the code is restored from the saved snapshot < < < + if (process_rank == MPI_ROOT_RANK) then + CALL receive_wrapped_code_state(instance, state_str) + ! Calling SET_STATE + {% if flags.skeleton_only %} + ! + ! >>> Subroutine "SET_STATE" should be called here <<< + ! + {% endif %} + {{ comment_out }} CALL {{code_description.implementation.subroutines.set_state}}(state_str, status_code, status_message) + + endif +{% endif %} + +{% if code_description.implementation.subroutines.init.name %} + {% if flags.checkpointable %} + else ! m3_instance is not resuming + ! > > > INITIALIZATION by calling INIT method of the code < < < + {% endif %} + + if (initial_run) then + !---- RESET status info ---- + nullify(status_message) + status_code = 0 + ! Calls the wrapped INIT subroutine + {% if flags.skeleton_only %} + ! + ! >>> Subroutine "INIT" should be called here <<< + ! + {% endif %} + {{ comment_out }} CALL {{code_description.implementation.subroutines.init.name}}( + {%- if flags.code_parameters_provided -%} + imas_code_params, + {%- endif -%} + status_code, status_message) + CALL check_status(instance, status_code, status_message, ACTOR_NAME, "INIT") + initial_run = .FALSE. + endif + {% endif %} + + {% if flags.checkpointable %} + endif ! LIBMUSCLE_Instance_resuming + + if( LIBMUSCLE_Instance_should_init(instance)) then +{% endif %} + + ! > > > RECEIVING INPUT IDSes < < < + CALL receive_input_idses(& + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ argument.name }}, & + {% endfor %} + instance ) + +{% if flags.checkpointable %} + endif ! LIBMUSCLE_Instance_should_init +{% endif %} + + !---- RESET status info ---- + nullify(status_message) + status_code = 0 + + ! > > > - - - WRAPPED CODE CALL - MAIN SBRT - - - < < < + {% if flags.skeleton_only %} + ! + ! >>> Subroutine "MAIN" should be called here <<< + ! + {% endif %} + {{ comment_out }} CALL {{code_description.implementation.subroutines.main.name}}( & + {% for argument in code_description.implementation.subroutines.main.arguments %} + {{ comment_out }} {{ argument.name }}, & + {% endfor %} + {% if flags.code_parameters_provided %} + {{ comment_out }} imas_code_params, & + {% endif %} + {{ comment_out }} status_code, status_message) + + + !-----------Check status info --------------------- + CALL check_status(instance, status_code, status_message, ACTOR_NAME, "MAIN") + +{% if flags.mpi_code %} + if (process_rank == MPI_ROOT_RANK) then +{% endif %} + + ! > > > RETURNING OUTPUT IDSes < < < + {% if code_description.implementation.subroutines.get_timestamp %} + ! > > > GET TIMESTAMP < < < + nullify(status_message) + status_code = 0 + {% if flags.skeleton_only %} + ! + ! >>> Subroutine "GET_TIMESTAMP" should be called here <<< + ! + {% endif %} + {{ comment_out }} CALL {{code_description.implementation.subroutines.get_timestamp}}(timestamp, status_code, status_message) + + CALL check_status(instance, status_code, status_message, "{{actor_description.actor_name}}", "GET_TIMESTAMP") + + {% endif %} + CALL send_output_idses(instance, & + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + {{ argument.name }}, & + {% endfor %} + timestamp) + +{% if flags.mpi_code %} + endif ! --- The end of section called only for RANK 0 process +{% endif %} + + +{% if flags.checkpointable %} + ! > > > SAVE SNAPSHOT < < < + if (LIBMUSCLE_Instance_should_save_final_snapshot(instance)) then + if (process_rank == MPI_ROOT_RANK ) then ! Always .TRUE. for non-MPI code + ! Calling GET_STATE + nullify(status_message) + status_code = 0 + {% if flags.skeleton_only %} + ! + ! >>> Subroutine "GET_STATE" should be called here <<< + ! + {% endif %} + + {{ comment_out }} CALL {{code_description.implementation.subroutines.get_state}}(state_str, status_code, status_message) + + CALL check_status(instance, status_code, status_message, ACTOR_NAME, "GET_STATE") + CALL save_wrapped_code_state(instance, state_str) + endif + endif + +{% endif %} + + ! > > > CLEAN UP < < < + {% for argument in code_description.implementation.subroutines.main.arguments %} + {{ ids_macro.deallocate( argument.name) }} + {% endfor %} + + end do ! < < < MUSCLE3 LOOP < < < + + {% if code_description.implementation.subroutines.finalize.name %} + ! > > > - - - - - - - - - - - - - WRAPPED CODE CALL - FINALISE SBRT - - - - - - - - - - - - - - - - - - < < < + !---- RESET status info ---- + nullify(status_message) + status_code = 0 + {% if flags.skeleton_only %} + ! + ! >>> Subroutine "FINALIZE" should be called here <<< + ! + {% endif %} + {{ comment_out }} CALL {{code_description.implementation.subroutines.finalize.name}}(status_code, status_message) + + !-----------Check status info --------------------- + CALL check_status(instance, status_code, status_message, ACTOR_NAME, "FINALIZE") + +{% endif %} + + {% if flags.code_parameters_provided %} + ! ------Deallocating code parameters ---------------------------- + if (associated(imas_code_params%parameters_value)) deallocate(imas_code_params%parameters_value) + {% endif %} + + !---- MUSCLE3 Finalization ---- + call LIBMUSCLE_Instance_free(instance) + + {% if flags.mpi_code %} + !---- MPI Finalization ---- + call finish_mpi() + + {% endif %} +end program diff --git a/iwrap/generators/actor_generators/muscle3_python/__init__.py b/iwrap/generators/actor_generators/muscle3_python/__init__.py new file mode 100644 index 0000000..802dd39 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_python/__init__.py @@ -0,0 +1,3 @@ +from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator + +__all__ = ['PythonActorGenerator'] \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_python/m3_python_actor.py b/iwrap/generators/actor_generators/muscle3_python/m3_python_actor.py new file mode 100644 index 0000000..d90daec --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_python/m3_python_actor.py @@ -0,0 +1,103 @@ +import logging +import os +import shutil +import tempfile +from pathlib import Path +from typing import Set, List + + +from iwrap.generation_engine.utils.jinja2_template_processing import process_template_dir +from iwrap.generators.actor_generators import ActorGenerator +from iwrap.settings.project import ProjectSettings + +import jinja2 +import sys + +from iwrap.settings.platform.platform_settings import PlatformSettings + +from iwrap.generators.actor_generators.muscle3_common import m3_utils + + +class PythonActorGenerator(ActorGenerator): + # Class logger + __logger = logging.getLogger(__name__ + "." + __qualname__) + + COMPLIANT_API = '2.1' + + @property + def type(self) -> str: + return 'MUSCLE3-Python' + + @property + def name(self) -> str: + return 'MUSCLE3 (Python)' + + @property + def description(self) -> str: + return 'Wrapping Python code into MUSCLE3 micro model' + + @property + def actor_language(self) -> List[str]: + return 'python' + + @property + def actor_data_types(self) -> List[str]: + return ['legacy'] + + @property + def code_data_types(self) -> List[str]: + return ['legacy'] + + @property + def code_languages(self) -> Set[str]: + return {'python'} + + def __init__(self): + + self.__info_output_stream = None + self.temp_dir: tempfile.TemporaryDirectory = None + self.jinja_env: jinja2.Environment = None + self.install_dir: str = None + self.wrapper_dir = 'wrapper' + + def configure(self, info_output_stream=sys.stdout): + self.__info_output_stream = info_output_stream + + def validate(self, project_settings: dict = None): + m3_utils.validate( project_settings ) + + def initialize(self, project_settings: dict = None): + install_dir = None + + if project_settings: + install_dir = project_settings['actor_description'].get( 'install_dir' ) + + if not install_dir: + install_dir = PlatformSettings().directories.actor_install_dir + self.install_dir: str = str(Path(install_dir, ProjectSettings.get_settings().actor_description.actor_name)) + + def generate(self, project_settings: dict): + self.cleanup(project_settings) + os.makedirs( self.install_dir, exist_ok=True ) + current_path = os.path.dirname(os.path.realpath(__file__)) + process_template_dir( None, current_path + '/resources', self.install_dir, project_settings, + filter_func=m3_utils.template_filter_func, + output_stream= self.__info_output_stream, ) + + m3_utils.copy_native_lib( self.install_dir, project_settings, "wrapped_code" ) + m3_utils.copy_code_params_files( self.install_dir, project_settings ) + m3_utils.copy_build_info( self.install_dir, project_settings ) + + + def build(self, project_settings: dict): + ... + + def install(self, project_settings: dict): + ... + + def cleanup(self, project_settings: dict): + shutil.rmtree(self.install_dir, ignore_errors=True) + + + + diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/__init__.py.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/__init__.py.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/macros/flags.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/macros/flags.jinja2 new file mode 100644 index 0000000..7c727ce --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_python/resources/macros/flags.jinja2 @@ -0,0 +1,5 @@ +{%- set checkpointable = code_description.implementation.subroutines.get_state and code_description.implementation.subroutines.set_state %} +{%- set code_parameters_provided = code_description.implementation.code_parameters.parameters %} +{%- set mpi_code = code_description.settings.mpi_compiler_cmd -%} + +{%- set skeleton_only = not code_description.implementation.programming_language -%} \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 new file mode 100644 index 0000000..f2f2ed0 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 @@ -0,0 +1,12 @@ +{% macro imports() -%} + import imas +{%- endmacro -%} + +{% macro declare(ids_name, ids_var_name) -%} + {{ ids_var_name }} = imas.{{ ids_name }}() +{%- endmacro -%} + +{%- macro provenance(ids_var_name, sbrt_name) -%} + {{ ids_var_name }}.code.name = "{{sbrt_name}}" + {{ ids_var_name }}.code.version = "" +{%- endmacro -%} diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 new file mode 100644 index 0000000..725c73c --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 @@ -0,0 +1,158 @@ +{%- import './macros/flags.jinja2' as flags with context -%} + +import os +import ymmsl +from shutil import copy +import libmuscle +from libmuscle import Instance, USES_CHECKPOINT_API + +import imas +from imas import imasdef + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# INIT MUSCLE3 +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def init_muscle3(): + + input_ports = [] + output_ports = [] + + # # # # - - - INPUT ('F_INIT') ports - - - # # # # +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + # {{ argument.name }} + input_ports.append("{{ argument.name }}") + +{% endfor %} + # # # # - - - OUTPUT ('O_F') ports - - - # # # # +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + # {{ argument.name }} + output_ports.append("{{ argument.name }}") + + {% endfor %} + ports = {ymmsl.Operator.F_INIT : input_ports, ymmsl.Operator.O_F : output_ports} +{% if flags.checkpointable %} + m3_instance = libmuscle.Instance(ports, libmuscle.USES_CHECKPOINT_API) +{% else %} + m3_instance = libmuscle.Instance(ports) +{% endif %} + + return m3_instance + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# FINISH MUSCLE3 +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def finish_muscle3(m3_instance): + ... + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# RECEIVE IDS +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def receive_ids(m3_instance: libmuscle.Instance, port_name, ids): + m3_message = m3_instance.receive(port_name) + + ids_bytes = m3_message.data + ids.deserialize(ids_bytes) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# SEND IDS +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def send_ids(m3_instance: libmuscle.Instance, port_name, ids, timestamp): + + ids_bytes = ids.serialize(imasdef.DEFAULT_SERIALIZER_PROTOCOL) + m3_message = libmuscle.Message( timestamp, None, ids_bytes) + + m3_instance.send(port_name, m3_message) + + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# RECEIVE INPUT IDSES +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def receive_input_idses(m3_instance: libmuscle.Instance, +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + {{ argument.name }}, +{%- endfor -%} + ): + +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' %} + # {{ argument.name }} + receive_ids(m3_instance, "{{ argument.name }}", {{ argument.name }}) + +{% endfor %} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# SEND OUTPUT IDSES +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def send_output_idses(m3_instance: libmuscle.Instance, timestamp, +{% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + {{ argument.name }}, +{% endfor -%} + ): + + {% for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' %} + #--------- SEND IDS : {{ argument.name }} ------------------------ + {% if not code_description.implementation.subroutines.get_timestamp %} + timestamp = -1.0 # resetting value + time_size = len({{ argument.name }}.time) + if time_size > 0: + time_stamp = {{ argument.name }}.time[time_size - 1] + + {% endif %} + send_ids(m3_instance, "{{ argument.name }}", {{ argument.name }}, timestamp) + +{% endfor %} + +{% if code_description.implementation.code_parameters.parameters %} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# XXX XXX +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def read_code_parameters(m3_instance: libmuscle.Instance): + + try: + # checking if property 'parameters_file' can be read + code_parameters_file_path = m3_instance.get_setting("parameters_file", "str") + except KeyError: + + # Value for setting parameters_file was not set. Using default file. + directory = os.path.realpath(os.path.dirname(__file__)) + code_parameters_file_path = directory + "/input/{{code_description.implementation.code_parameters.parameters | basename }}" + + with open(code_parameters_file_path, 'r') as file: + code_parameters = file.read() + + try: + # checking if property 'copy_xml_parameters' can be read + copy_xml_parameters = m3_instance.get_setting("copy_xml_parameters", "bool") + if copy_xml_parameters: + #shutil.copy() + copy( code_parameters_file_path, "./" ) + except KeyError: + ... + #just don't copy parameters + + return code_parameters + +{% endif %} +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# RESTORING CODE STATE +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +{% if flags.checkpointable %} +def receive_code_state(m3_instance: libmuscle.Instance): + m3_message = m3_instance.load_snapshot() + if m3_message is None: + return None + + return m3_message.data + +def save_code_state(m3_instance: libmuscle.Instance, state_str): + + timestamp = -1.0 + m3_message = libmuscle.Message( timestamp, None, state_str) + m3_instance.save_final_snapshot(m3_message) + +{% else %} +# Methods not generated - no GET_STATE/SET_STATE methods provided +{% endif %} \ No newline at end of file diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/standalone.py.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/standalone.py.jinja2 new file mode 100644 index 0000000..20f1371 --- /dev/null +++ b/iwrap/generators/actor_generators/muscle3_python/resources/standalone.py.jinja2 @@ -0,0 +1,162 @@ +{% import './macros/%s_ids.jinja2' % code_description.implementation.data_type as ids_macro %} + +{%- import './macros/flags.jinja2' as flags with context -%} + +{% set ARG_INDENT = "\n"~20*" " %} + +{%- if flags.checkpointable %} + {% set chkp_indent = 4 %} +{% else %} + {% set chkp_indent = 0 %} +{% endif %} + +{%- if code_description.implementation.code_path %} + {% set python_module = code_description.implementation.code_path | stemname %} +{% else %} + {% set python_module = "" %} +{% endif %} + +{%- if flags.skeleton_only %} + {% set comment_out = '# ' -%} +{% endif %} + +{{ ids_macro.imports() }} + +from {{actor_description.actor_name}} import muscle3_tools +{{ comment_out }}from {{actor_description.actor_name}}.wrapped_code import {{ python_module }} + +if __name__ == "__main__": + + timestamp = -1.0 +{% if code_description.implementation.subroutines.init.name %} + initial_run = True +{% endif %} + + #---- IN/OUT IDSes ---- +{% for argument in code_description.implementation.subroutines.main.arguments %} + {{ ids_macro.declare(argument.type, argument.name) }} +{% endfor %} + + # # # # - - - M3 INITIALISATION - - - # # # # + m3_instance = muscle3_tools.init_muscle3() + + {% if flags.code_parameters_provided %} + #---- Code parameters ---- + imas_code_params = muscle3_tools.read_code_parameters(m3_instance) + + {% endif %} + + # # # # - - - MUSCLE3 LOOP - - - # # # # + while m3_instance.reuse_instance(): + + # # # # - - - WRAPPED CODE INITIALIZATION - - - # # # # +{% if flags.checkpointable %} + if m3_instance.resuming(): + # > > > RESTART : the code is restored from the saved snapshot < < < + state_str = muscle3_tools.receive_code_state(m3_instance) + # Calling SET_STATE + {% if flags.skeleton_only %} + # + # >>> Subroutine "SET_STATE" should be called here <<< + # + {% endif %} + {{ comment_out }}{{ python_module }}.{{code_description.implementation.subroutines.set_state}}(state_str) +{% endif %} + {% if code_description.implementation.subroutines.init.name %} + {% if flags.checkpointable %} + else: # m3_instance is not resuming + # > > > INITIALIZATION by calling INIT method of the code < < < + {% endif %} + {% filter indent(width=chkp_indent, first=True) %} + if initial_run: + {% if flags.skeleton_only %} + ... + # + # >>> Subroutine "INIT" should be called here <<< + # + {% endif %} + {{ comment_out }}{{ python_module }}.{{code_description.implementation.subroutines.init.name}}( + {%- if flags.code_parameters_provided -%} + imas_code_params + {%- endif -%} + ) + + {% endfilter %} + initial_run = False + {% endif %} + +{% if flags.checkpointable %} + if m3_instance.should_init(): +{% endif %} + {% filter indent(width=chkp_indent) %} + # # # # - - - RECEIVING INPUT IDSes - - - # # # # + muscle3_tools.receive_input_idses( + {{- ARG_INDENT }} m3_instance, + {%- for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'IN' -%} + {{ ARG_INDENT }} {{ argument.name }}, + {%- endfor -%} ) + + {% endfilter %} + + # # # # - - - WRAPPED CODE CALL - MAIN SBRT - - - # # # # + {% if flags.skeleton_only %} + # + # >>> Subroutine "MAIN" should be called here <<< + # + {% endif %} + {{ comment_out }}{{ python_module }}.{{code_description.implementation.subroutines.main.name}}( + {%- for argument in code_description.implementation.subroutines.main.arguments -%} + {{ ARG_INDENT }}{{ comment_out }} {{ argument.name }}, + {%- endfor -%} + {% if flags.code_parameters_provided -%} + {{ ARG_INDENT }}{{ comment_out }} imas_code_params + {% endif -%} ) + + + # # # # - - - SENDING OUTPUT IDSes - - - # # # # + {% if code_description.implementation.subroutines.get_timestamp %} + # > > > GET TIMESTAMP < < < + {% if flags.skeleton_only %} + # + # >>> Subroutine "GET_TIMESTAMP" should be called here <<< + # + {% endif %} + {{ comment_out }}timestamp = {{ python_module }}.{{code_description.implementation.subroutines.get_timestamp}}() + + + {% endif %} + muscle3_tools.send_output_idses( + {{- ARG_INDENT }} m3_instance, timestamp, + {%- for argument in code_description.implementation.subroutines.main.arguments if argument.intent == 'OUT' -%} + {{ ARG_INDENT }} {{ argument.name }}, + {%- endfor -%} ) + +{% if flags.checkpointable %} + # > > > SAVE SNAPSHOT < < < + if m3_instance.should_save_final_snapshot(): + # Calling GET_STATE + {% if flags.skeleton_only %} + # + # >>> Subroutine "GET_STATE" should be called here <<< + # + {% endif %} + {{ comment_out }}state_str = {{ python_module }}.{{code_description.implementation.subroutines.get_state}}() + + muscle3_tools.save_code_state(m3_instance, state_str) +{% endif %} + + #> > > > THE END OF MUSCLE3 LOOP + + # # # # - - - FINALIZATION - - - # # # # + {% if code_description.implementation.subroutines.finalize.name %} + + # # # # - - - WRAPPED CODE CALL - FINISH SBRT - - - # # # # + {% if flags.skeleton_only %} + # + # >>> Subroutine "FINALIZATION" should be called here <<< + # + {% endif %} + {{ comment_out }}{{ python_module }}.{{code_description.implementation.subroutines.finalize.name}} () + {% endif %} + + muscle3_tools.finish_muscle3(m3_instance) diff --git a/pyproject.toml b/pyproject.toml index 0444ef6..69b09f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,8 @@ [build-system] # Minimum requirements for the build system to execute. -requires = ["setuptools>=43", "wheel", "tomli", "versioneer[toml]"] +requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" -[tool.versioneer] -VCS = "git" -style = "pep440" -versionfile_source = "iwrap/_version.py" -versionfile_build = "iwrap/_version.py" -tag_prefix = "" -parentdir_prefix = "" +[tool.setuptools_scm] +write_to = "iwrap/_version.py" diff --git a/requirements_muscle3.txt b/requirements_muscle3.txt new file mode 100644 index 0000000..a1f732f --- /dev/null +++ b/requirements_muscle3.txt @@ -0,0 +1,3 @@ +# Optional dependencies for MUSCLE3 actor generators +# Install with: pip install -r requirements_muscle3.txt +muscle3>=0.7.0 diff --git a/setup.cfg b/setup.cfg index 2cf0468..0786337 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,13 +19,4 @@ package_dir = iwrap max-line-length = 88 [aliases] -test = pytest - - -[versioneer] -VCS = git -style = pep440 -versionfile_source = iwrap/_version.py -versionfile_build = iwrap/_version.py -tag_prefix = -parentdir_prefix = \ No newline at end of file +test = pytest \ No newline at end of file diff --git a/setup.py b/setup.py index 5cd43b4..ee0dd48 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ import sys import subprocess from glob import glob -import versioneer # Import other stdlib packages from itertools import chain @@ -73,7 +72,7 @@ def list_docs_data_files(path_to_docs: str): pyproject_data = tomli.loads(pyproject_text) optional_reqs = {} -for req in ["core"]: +for req in ["core", "muscle3"]: optional_reqs[req] = DistTextFile(this_dir / f"requirements_{req}.txt").readlines() install_requires = optional_reqs.pop("core") # collect all optional dependencies in a "all" target @@ -89,12 +88,12 @@ def list_docs_data_files(path_to_docs: str): # For allowed version strings, see: # https://packaging.python.org/specifications/core-metadata/ for allow version strings - cmdclassdict = versioneer.get_cmdclass() - cmdclassdict['clean'] = CleanCommand - setup( - version=versioneer.get_version(), + use_scm_version={ + 'write_to': 'iwrap/_version.py', + }, packages=find_packages(exclude=('tests*', 'testing*', 'test_suite*')), + cmdclass={'clean': CleanCommand}, setup_requires=pyproject_data["build-system"]["requires"], include_package_data=True, install_requires=install_requires, diff --git a/test_muscle3_integration.py b/test_muscle3_integration.py new file mode 100644 index 0000000..2a9fa20 --- /dev/null +++ b/test_muscle3_integration.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +""" +Test script to verify MUSCLE3 integration into iWrap core +""" + +import sys +sys.path.insert(0, '/home/ITER/sawantp1/github/iWrap') + +from iwrap.generation_engine.engine import Engine + +print('🔧 Testing iWrap with MUSCLE3 Built-in Generators') +print('=' * 60) + +# Initialize engine +Engine().startup() +generators = Engine().registered_generators + +print(f'\n✓ Engine initialized successfully!') +print(f'✓ Found {len(generators)} actor generators:\n') + +for gen in generators: + print(f' Type: {gen.type:<20} | Name: {gen.name:<30}') + print(f' API Version: {gen.COMPLIANT_API}') + if hasattr(gen, 'code_languages'): + print(f' Code Languages: {gen.code_languages}') + print() + +print('=' * 60) +print('✅ MUSCLE3 Integration Successful!') +print('\nNext steps:') +print(' 1. Test actor generation') +print(' 2. Integrate tests') +print(' 3. Update documentation') +print(' 4. Configure CI/CD') diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..e123824 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,422 @@ +# iWrap Test Suite + +This directory contains the comprehensive test suite for iWrap, including unit tests, integration tests, and IMAS integration tests. + +## Test Structure + +``` +tests/ +├── conftest.py # Pytest configuration and shared fixtures +├── pytest.ini # Pytest markers and settings +│ +├── run-all-tests-master.sh # Master orchestrator for all tests +├── run-all-tests.sh # Comprehensive pytest-based tests +├── run-muscle3-tests.sh # MUSCLE3-specific tests only +├── run-tests.sh # Legacy IMAS integration tests +│ +├── unit/ # Fast, isolated component tests +│ ├── core/ # Core functionality tests +│ └── generators/ # Generator-specific tests +│ ├── test_muscle3_python.py +│ ├── test_muscle3_cpp.py +│ └── test_muscle3_fortran.py +│ +├── integration/ # Cross-component workflow tests +│ └── test_muscle3_integration.py +│ +├── muscle3/ # MUSCLE3-specific test cases +│ ├── actors/ +│ ├── wrapped_codes/ +│ ├── macro/ +│ ├── workflow/ +│ └── code_parameters/ +│ +└── test_cases/ # IMAS integration test cases + └── ... +``` + +## Running Tests + +### Quick Start + +Run all pytest-based tests (unit + integration): +```bash +./tests/run-all-tests-master.sh +``` + +### Test Runners + +#### Master Orchestrator (Recommended) +The master orchestrator provides fine-grained control over which test suites to run: + +```bash +# Run all tests including IMAS (slow) +./tests/run-all-tests-master.sh --all + +# Run only unit tests (fast) +./tests/run-all-tests-master.sh --unit-only + +# Run only integration tests +./tests/run-all-tests-master.sh --integration-only + +# Run only MUSCLE3 tests +./tests/run-all-tests-master.sh --muscle3-only + +# Include IMAS tests +./tests/run-all-tests-master.sh --imas + +# Skip MUSCLE3 tests +./tests/run-all-tests-master.sh --skip-muscle3 + +# Verbose output +./tests/run-all-tests-master.sh -v + +# Stop on first failure +./tests/run-all-tests-master.sh --fail-fast +``` + +#### Comprehensive Pytest Tests +Run all pytest-based tests with detailed reporting: +```bash +./tests/run-all-tests.sh +``` + +This script: +- Checks for MUSCLE3 availability +- Runs core unit tests +- Runs generator unit tests +- Runs MUSCLE3 tests (if available) +- Runs integration tests +- Provides color-coded summary + +#### MUSCLE3-Only Tests +Run only MUSCLE3 tests (requires MUSCLE3 installation): +```bash +./tests/run-muscle3-tests.sh +``` + +#### IMAS Integration Tests +Run legacy IMAS integration tests (slow, requires IMAS): +```bash +./tests/run-tests.sh +``` + +### Direct Pytest Usage + +You can also use pytest directly for more control: + +```bash +# Run all unit tests +pytest tests/unit/ -v + +# Run all integration tests +pytest tests/integration/ -v + +# Run only MUSCLE3 tests +pytest tests/ -v -m muscle3 + +# Run tests excluding MUSCLE3 +pytest tests/ -v -m "not muscle3" + +# Run specific test file +pytest tests/unit/generators/test_muscle3_python.py -v + +# Run with coverage +pytest tests/ --cov=iwrap --cov-report=html + +# Run tests in parallel (requires pytest-xdist) +pytest tests/ -n auto +``` + +## Test Markers + +Tests are organized using pytest markers: + +- `@pytest.mark.unit` - Fast, isolated unit tests +- `@pytest.mark.integration` - Cross-component integration tests +- `@pytest.mark.muscle3` - Tests requiring MUSCLE3 library +- `@pytest.mark.imas` - Tests requiring IMAS +- `@pytest.mark.slow` - Slow-running tests +- `@pytest.mark.core` - Core functionality (no optional deps) +- `@pytest.mark.generators` - Generator-specific tests + +### Running Tests by Marker + +```bash +# Run only unit tests +pytest -m unit + +# Run only integration tests +pytest -m integration + +# Run only MUSCLE3 tests +pytest -m muscle3 + +# Run core tests (no optional dependencies) +pytest -m core + +# Combine markers +pytest -m "unit and not muscle3" +pytest -m "integration or muscle3" +``` + +## MUSCLE3 Tests + +MUSCLE3 tests are **optional** and will be automatically skipped if MUSCLE3 is not installed. + +### Installing MUSCLE3 + +```bash +# Install iWrap with MUSCLE3 support +pip install -e .[muscle3] + +# Or install MUSCLE3 separately +pip install muscle3>=0.7.0 +``` + +### MUSCLE3 Test Categories + +1. **Unit Tests** (`tests/unit/generators/`) + - Generator instantiation + - API version verification + - Property validation + - Discovery mechanism + +2. **Integration Tests** (`tests/integration/`) + - Full workflow testing + - Generator selection + - Cross-generator compatibility + +3. **MUSCLE3-Specific Tests** (`tests/muscle3/`) + - Actor generation and compilation + - Wrapped code integration + - Macro execution + - Workflow validation + - Code parameter handling + +## Test Fixtures + +Common fixtures are provided in `conftest.py`: + +### Environment Fixtures +- `muscle3_available` - Check if MUSCLE3 is installed +- `skip_if_no_muscle3` - Skip test if MUSCLE3 unavailable +- `clean_environment` - Clean environment variables +- `mock_imas_environment` - Mock IMAS environment + +### Directory Fixtures +- `temp_dir` - Temporary directory for tests +- `temp_project_dir` - Temporary project structure +- `temp_actor_dir` - Temporary actor generation directory + +### Settings Fixtures +- `mock_settings` - Mock iWrap settings +- `mock_muscle3_settings` - Mock MUSCLE3 settings + +### Generator Fixtures +- `generator_registry` - All discovered generators +- `core_generators` - Only core generators +- `muscle3_generators` - Only MUSCLE3 generators + +### Data Fixtures +- `sample_actor_yaml` - Sample actor configuration +- `sample_muscle3_yaml` - Sample MUSCLE3 actor configuration +- `sample_python_file` - Sample Python file +- `sample_yaml_file` - Sample YAML file + +## Continuous Integration + +### GitHub Actions / GitLab CI + +Example CI configuration: + +```yaml +test: + matrix: + include: + - name: "Core Tests" + env: + MUSCLE3: "false" + - name: "Core + MUSCLE3 Tests" + env: + MUSCLE3: "true" + + script: + - pip install -e . + - if [ "$MUSCLE3" = "true" ]; then pip install -e .[muscle3]; fi + - ./tests/run-all-tests-master.sh +``` + +### Test Reports + +Test logs are saved to `/tmp/`: +- `/tmp/iwrap_core_tests.log` - Core unit test output +- `/tmp/iwrap_generator_tests.log` - Generator test output +- `/tmp/iwrap_muscle3_tests.log` - MUSCLE3 test output +- `/tmp/iwrap_integration_tests.log` - Integration test output + +### JUnit XML Reports + +Generate JUnit XML reports for CI integration: + +```bash +pytest tests/ --junitxml=test-results.xml +``` + +## Writing New Tests + +### Unit Test Template + +```python +import pytest +from iwrap.generators.actor_generators.your_generator import YourGenerator + +@pytest.mark.unit +@pytest.mark.generators +def test_generator_instantiation(): + """Test that generator can be instantiated.""" + generator = YourGenerator() + assert generator is not None + +@pytest.mark.unit +@pytest.mark.muscle3 # If requires MUSCLE3 +def test_muscle3_feature(skip_if_no_muscle3): + """Test MUSCLE3-specific feature.""" + # Test code here + pass +``` + +### Integration Test Template + +```python +import pytest +from iwrap.generation_engine import Engine + +@pytest.mark.integration +def test_full_workflow(temp_dir, mock_settings): + """Test complete actor generation workflow.""" + engine = Engine(mock_settings) + # Test workflow here + pass +``` + +### Using Fixtures + +```python +@pytest.mark.unit +def test_with_fixtures(temp_dir, sample_yaml_file, mock_settings): + """Test using multiple fixtures.""" + # Fixtures are automatically provided + assert temp_dir.exists() + assert sample_yaml_file.exists() + assert mock_settings.work_dir == str(temp_dir) +``` + +## Test Coverage + +Generate coverage report: + +```bash +# HTML report +pytest tests/ --cov=iwrap --cov-report=html +open htmlcov/index.html + +# Terminal report +pytest tests/ --cov=iwrap --cov-report=term-missing + +# XML report (for CI) +pytest tests/ --cov=iwrap --cov-report=xml +``` + +## Debugging Tests + +### Run Single Test +```bash +pytest tests/unit/generators/test_muscle3_python.py::test_generator_import -v +``` + +### Drop into PDB on Failure +```bash +pytest tests/ --pdb +``` + +### Print Output +```bash +pytest tests/ -s # Show print statements +``` + +### Verbose Output +```bash +pytest tests/ -vv # Very verbose +``` + +## Performance + +### Test Execution Time + +- **Unit Tests**: < 5 seconds +- **Integration Tests**: < 30 seconds +- **MUSCLE3 Tests**: < 1 minute +- **IMAS Tests**: 5-30 minutes (depending on configuration) + +### Parallel Execution + +Install pytest-xdist for parallel test execution: + +```bash +pip install pytest-xdist + +# Run tests in parallel +pytest tests/ -n auto # Use all CPU cores +pytest tests/ -n 4 # Use 4 workers +``` + +## Troubleshooting + +### MUSCLE3 Tests Always Skipped + +Ensure MUSCLE3 is installed: +```bash +python3 -c "import muscle3; print(muscle3.__version__)" +``` + +Install if missing: +```bash +pip install iwrap[muscle3] +``` + +### Import Errors + +Ensure PYTHONPATH is set: +```bash +export PYTHONPATH=$(pwd):$PYTHONPATH +pytest tests/ +``` + +### Tests Pass Locally but Fail in CI + +Check: +1. Python version compatibility +2. Dependency versions +3. Environment variables +4. File permissions +5. PYTHONPATH configuration + +## Contributing + +When adding new features: + +1. **Write tests first** (TDD approach) +2. **Add appropriate markers** (`@pytest.mark.unit`, etc.) +3. **Use existing fixtures** when possible +4. **Document new fixtures** in conftest.py +5. **Update this README** if adding new test categories +6. **Ensure tests pass** with and without MUSCLE3 + +## Questions? + +For questions about the test suite: +- Check existing test files for examples +- Review `conftest.py` for available fixtures +- Consult the [iWrap Documentation](../docs/) +- Open an issue on GitHub/GitLab diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..fb52ca6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,396 @@ +""" +Pytest configuration and shared fixtures for iWrap test suite. + +This module provides common fixtures and configuration for all iWrap tests, +including support for conditional MUSCLE3 testing. +""" + +import os +import sys +import tempfile +import shutil +from pathlib import Path +import pytest + +# Add the project root to the Python path +PROJECT_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + + +####################################################################################################################### +# MUSCLE3 Availability Check +####################################################################################################################### + +def pytest_configure(config): + """Configure pytest with custom markers.""" + config.addinivalue_line("markers", "muscle3: marks tests as requiring MUSCLE3 (skipped if not installed)") + config.addinivalue_line("markers", "unit: marks tests as unit tests") + config.addinivalue_line("markers", "integration: marks tests as integration tests") + config.addinivalue_line("markers", "imas: marks tests as requiring IMAS") + config.addinivalue_line("markers", "slow: marks tests as slow running") + config.addinivalue_line("markers", "core: marks tests as core functionality (no optional deps)") + config.addinivalue_line("markers", "generators: marks tests for generator modules") + + +@pytest.fixture(scope="session") +def muscle3_available(): + """Check if MUSCLE3 is available.""" + try: + import muscle3 + return True + except ImportError: + return False + + +@pytest.fixture(scope="session") +def skip_if_no_muscle3(muscle3_available): + """Skip test if MUSCLE3 is not available.""" + if not muscle3_available: + pytest.skip("MUSCLE3 is not installed") + + +####################################################################################################################### +# Temporary Directory Fixtures +####################################################################################################################### + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for test files.""" + temp_path = tempfile.mkdtemp() + yield Path(temp_path) + shutil.rmtree(temp_path, ignore_errors=True) + + +@pytest.fixture +def temp_project_dir(temp_dir): + """Create a temporary project directory with standard structure.""" + project_dir = temp_dir / "test_project" + project_dir.mkdir(parents=True, exist_ok=True) + + # Create standard subdirectories + (project_dir / "src").mkdir() + (project_dir / "tests").mkdir() + (project_dir / "docs").mkdir() + + yield project_dir + + # Cleanup happens automatically via temp_dir cleanup + + +@pytest.fixture +def temp_actor_dir(temp_dir): + """Create a temporary directory for actor generation.""" + actor_dir = temp_dir / "actors" + actor_dir.mkdir(parents=True, exist_ok=True) + yield actor_dir + + +####################################################################################################################### +# Mock Settings Fixtures +####################################################################################################################### + +@pytest.fixture +def mock_settings(temp_dir): + """Create mock settings for testing.""" + from iwrap.settings.settings import Settings + + settings = Settings() + settings.work_dir = str(temp_dir) + settings.actor_name = "test_actor" + settings.actor_language = "python" + settings.actor_type = "python" + + return settings + + +@pytest.fixture +def mock_muscle3_settings(temp_dir, muscle3_available): + """Create mock MUSCLE3 settings for testing.""" + if not muscle3_available: + pytest.skip("MUSCLE3 is not installed") + + from iwrap.settings.settings import Settings + + settings = Settings() + settings.work_dir = str(temp_dir) + settings.actor_name = "test_muscle3_actor" + settings.actor_language = "python" + settings.actor_type = "MUSCLE3-Python" + + return settings + + +####################################################################################################################### +# Sample Data Fixtures +####################################################################################################################### + +@pytest.fixture +def sample_actor_yaml(): + """Provide sample actor YAML configuration.""" + return """ +actor: + name: test_actor + description: Test actor for unit testing + language: python + type: python + +code_parameters: + - name: test_param + type: float + default: 1.0 + description: Test parameter + +ports: + inputs: + - name: input_port + description: Test input port + outputs: + - name: output_port + description: Test output port +""" + + +@pytest.fixture +def sample_muscle3_yaml(): + """Provide sample MUSCLE3 actor YAML configuration.""" + return """ +actor: + name: test_muscle3_actor + description: Test MUSCLE3 actor + language: python + type: MUSCLE3-Python + +code_parameters: + - name: test_param + type: float + default: 1.0 + description: Test parameter + +ports: + inputs: + - name: input_port + description: Test input port + operator: F_INIT + outputs: + - name: output_port + description: Test output port + operator: O_F +""" + + +####################################################################################################################### +# Generator Registry Fixtures +####################################################################################################################### + +@pytest.fixture +def generator_registry(): + """Get the generator registry.""" + from iwrap.generators.actor_generators.base_actor_generator import ActorGenerator + + # Force discovery of all generators + from iwrap.generation_engine import _engine + + # Return all discovered generators + return {gen.get_type(): gen for gen in ActorGenerator.__subclasses__()} + + +@pytest.fixture +def core_generators(generator_registry): + """Get only core (non-MUSCLE3) generators.""" + return { + name: gen for name, gen in generator_registry.items() + if "MUSCLE3" not in name + } + + +@pytest.fixture +def muscle3_generators(generator_registry, muscle3_available): + """Get only MUSCLE3 generators.""" + if not muscle3_available: + pytest.skip("MUSCLE3 is not installed") + + return { + name: gen for name, gen in generator_registry.items() + if "MUSCLE3" in name + } + + +####################################################################################################################### +# File System Fixtures +####################################################################################################################### + +@pytest.fixture +def sample_python_file(temp_dir): + """Create a sample Python file for testing.""" + py_file = temp_dir / "test_script.py" + py_file.write_text(""" +def test_function(): + return "Hello, World!" + +if __name__ == "__main__": + print(test_function()) +""") + return py_file + + +@pytest.fixture +def sample_yaml_file(temp_dir, sample_actor_yaml): + """Create a sample YAML file for testing.""" + yaml_file = temp_dir / "test_actor.yaml" + yaml_file.write_text(sample_actor_yaml) + return yaml_file + + +@pytest.fixture +def sample_muscle3_yaml_file(temp_dir, sample_muscle3_yaml): + """Create a sample MUSCLE3 YAML file for testing.""" + yaml_file = temp_dir / "test_muscle3_actor.yaml" + yaml_file.write_text(sample_muscle3_yaml) + return yaml_file + + +####################################################################################################################### +# Environment Fixtures +####################################################################################################################### + +@pytest.fixture +def clean_environment(monkeypatch): + """Provide a clean environment without IMAS/iWrap specific variables.""" + env_vars_to_remove = [ + 'IMAS_PREFIX', + 'IMAS_HOME', + 'MUSCLE3_HOME', + 'IWRAP_HOME', + ] + + for var in env_vars_to_remove: + monkeypatch.delenv(var, raising=False) + + yield + + +@pytest.fixture +def mock_imas_environment(monkeypatch, temp_dir): + """Mock IMAS environment variables.""" + imas_prefix = temp_dir / "imas" + imas_prefix.mkdir(parents=True, exist_ok=True) + + monkeypatch.setenv('IMAS_PREFIX', str(imas_prefix)) + monkeypatch.setenv('IMAS_HOME', str(imas_prefix)) + + yield imas_prefix + + +####################################################################################################################### +# Utility Fixtures +####################################################################################################################### + +@pytest.fixture +def caplog_debug(caplog): + """Set log level to DEBUG for capturing all logs.""" + import logging + caplog.set_level(logging.DEBUG) + return caplog + + +@pytest.fixture +def isolated_filesystem(): + """Create an isolated filesystem for testing.""" + cwd = os.getcwd() + temp_path = tempfile.mkdtemp() + os.chdir(temp_path) + + yield Path(temp_path) + + os.chdir(cwd) + shutil.rmtree(temp_path, ignore_errors=True) + + +####################################################################################################################### +# Session Scoped Fixtures +####################################################################################################################### + +@pytest.fixture(scope="session") +def project_root(): + """Get the project root directory.""" + return PROJECT_ROOT + + +@pytest.fixture(scope="session") +def iwrap_version(): + """Get the iWrap version.""" + try: + from iwrap._version import __version__ + return __version__ + except ImportError: + return "unknown" + + +####################################################################################################################### +# Parametrize Helpers +####################################################################################################################### + +def pytest_generate_tests(metafunc): + """Custom test parametrization.""" + # Parametrize actor languages if requested + if "actor_language" in metafunc.fixturenames: + languages = ["python"] # Add more as needed: ["python", "cpp", "fortran"] + metafunc.parametrize("actor_language", languages) + + # Parametrize generator types + if "generator_type" in metafunc.fixturenames: + from iwrap.generators.actor_generators.base_actor_generator import ActorGenerator + + # Get all generator types + generator_types = [gen.get_type() for gen in ActorGenerator.__subclasses__()] + + # Filter based on markers + if metafunc.definition.get_closest_marker("muscle3"): + # Only MUSCLE3 generators + generator_types = [gt for gt in generator_types if "MUSCLE3" in gt] + elif metafunc.definition.get_closest_marker("core"): + # Only core generators + generator_types = [gt for gt in generator_types if "MUSCLE3" not in gt] + + metafunc.parametrize("generator_type", generator_types) + + +####################################################################################################################### +# Pytest Hooks +####################################################################################################################### + +def pytest_collection_modifyitems(config, items): + """Modify test collection to handle MUSCLE3 markers.""" + try: + import muscle3 + muscle3_available = True + except ImportError: + muscle3_available = False + + if not muscle3_available: + skip_muscle3 = pytest.mark.skip(reason="MUSCLE3 is not installed") + for item in items: + if "muscle3" in item.keywords: + item.add_marker(skip_muscle3) + + +def pytest_report_header(config): + """Add custom information to pytest report header.""" + try: + from iwrap._version import __version__ + version = __version__ + except ImportError: + version = "unknown" + + try: + import muscle3 + muscle3_version = muscle3.__version__ + muscle3_status = f"installed (v{muscle3_version})" + except ImportError: + muscle3_status = "NOT installed" + + return [ + f"iWrap version: {version}", + f"MUSCLE3 status: {muscle3_status}", + f"Project root: {PROJECT_ROOT}", + ] diff --git a/tests/integration/test_muscle3_integration.py b/tests/integration/test_muscle3_integration.py new file mode 100644 index 0000000..9624a33 --- /dev/null +++ b/tests/integration/test_muscle3_integration.py @@ -0,0 +1,88 @@ +""" +Integration tests for MUSCLE3 actor generators +Tests the full workflow of MUSCLE3 actor generation +""" +import pytest +import sys +import tempfile +import shutil +from pathlib import Path + +# Check if MUSCLE3 is available +muscle3_available = pytest.importorskip("muscle3", reason="MUSCLE3 not installed") + + +class TestMuscle3Integration: + """Integration tests for MUSCLE3 functionality""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup test environment""" + iwrap_root = Path(__file__).parent.parent + if str(iwrap_root) not in sys.path: + sys.path.insert(0, str(iwrap_root)) + + # Create temporary directory for test outputs + self.temp_dir = tempfile.mkdtemp(prefix='iwrap_muscle3_test_') + yield + # Cleanup + if Path(self.temp_dir).exists(): + shutil.rmtree(self.temp_dir) + + @pytest.mark.muscle3 + @pytest.mark.integration + @pytest.mark.slow + def test_all_muscle3_generators_registered(self): + """Test that all MUSCLE3 generators are properly registered""" + from iwrap.generation_engine.engine import Engine + + engine = Engine() + engine.startup() + + generators = {g.type: g for g in engine.registered_generators} + + # Check all MUSCLE3 generators are present + assert 'MUSCLE3-Python' in generators + assert 'MUSCLE3-CPP' in generators + assert 'MUSCLE3-Fortran' in generators + + # Check they have correct API version + for gen_type in ['MUSCLE3-Python', 'MUSCLE3-CPP', 'MUSCLE3-Fortran']: + assert generators[gen_type].COMPLIANT_API == '2.1' + + @pytest.mark.muscle3 + @pytest.mark.integration + def test_muscle3_generator_selection(self): + """Test selecting different MUSCLE3 generators""" + from iwrap.generation_engine.engine import Engine + + engine = Engine() + engine.startup() + + # Test getting each generator type + for gen_type in ['MUSCLE3-Python', 'MUSCLE3-CPP', 'MUSCLE3-Fortran']: + generator = Engine.get_generator(gen_type) + assert generator is not None + assert generator.type == gen_type + + @pytest.mark.muscle3 + @pytest.mark.integration + def test_core_without_muscle3_still_works(self): + """Test that core iWrap works even if MUSCLE3 generators fail""" + from iwrap.generation_engine.engine import Engine + + engine = Engine() + engine.startup() + + generators = engine.registered_generators + + # At least the core python generator should be available + generator_types = [g.type for g in generators] + assert 'python' in generator_types + + # And ideally MUSCLE3 ones too + assert len(generator_types) >= 1 + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/tests/muscle3/actors/cpp/basic_cpp/Makefile b/tests/muscle3/actors/cpp/basic_cpp/Makefile new file mode 100644 index 0000000..db80faf --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_cpp/Makefile @@ -0,0 +1,10 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=basic_cpp +ACTOR_TYPE=muscle3-cpp +CODE_LANGUAGE=cpp +WRAPPED_CODE_NAME=basic + +clean wrapped actor actor-gui wf-run check-output test: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ + diff --git a/tests/muscle3/actors/cpp/basic_cpp/basic_cpp.yaml b/tests/muscle3/actors/cpp/basic_cpp/basic_cpp.yaml new file mode 100644 index 0000000..68e0f26 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_cpp/basic_cpp.yaml @@ -0,0 +1,31 @@ +code_description: + implementation: + subroutines: + init: init_code + main: code_step + finalize: clean_up + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/cpp/basic/libbasic.a + include_path: $TESTS_DIR/wrapped_codes/cpp/basic/basic.h + programming_language: cpp + data_dictionary_compliant: 3.37.0 + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: g++ + mpi_compiler_cmd: + open_mp_switch: -DAL_MAJOR=$(AL_MAJOR) + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/cpp/basic_cpp/expected.out b/tests/muscle3/actors/cpp/basic_cpp/expected.out new file mode 100644 index 0000000..39f4a47 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_cpp/expected.out @@ -0,0 +1,3 @@ + 20001.000000000000 + 40002.000000000000 + 60003.000000000000 diff --git a/tests/muscle3/actors/cpp/basic_cpp/input_physics.xml b/tests/muscle3/actors/cpp/basic_cpp/input_physics.xml new file mode 100644 index 0000000..362c4bf --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_cpp/input_physics.xml @@ -0,0 +1,31 @@ + + + + 131024 + + + 1 + + + 10 + + + 2 + + + 1.5 + + + public + + + iter + + + user_environment_variable + + + iter + + + diff --git a/tests/muscle3/actors/cpp/basic_cpp/input_physics.xsd b/tests/muscle3/actors/cpp/basic_cpp/input_physics.xsd new file mode 100644 index 0000000..3102010 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_cpp/input_physics.xsd @@ -0,0 +1,120 @@ + + + Code parameters for Physics Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Input/output shot number + + + + + + + Input run number + + + + + + + Output run number + + + + + + + Number of times the multiplication factor is applied + + + + + + + Multiplication factor for the major radius + + + + + + + Input database name to read from external database + + + + + + + Input machine name to read from external database + + + + + + + Local database name to write into local IMAS database + + + + + + + Local machine name to write into local IMAS database + + + + + diff --git a/tests/muscle3/actors/cpp/basic_cpp/test_micro.ymmsl b/tests/muscle3/actors/cpp/basic_cpp/test_micro.ymmsl new file mode 100644 index 0000000..bd9c8a0 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_cpp/test_micro.ymmsl @@ -0,0 +1,13 @@ +ymmsl_version: v0.1 + +model: + name: basic_cpp + +implementations: + micro: + executable: ${ACTOR_DIR}/basic_cpp/bin/basic_cpp.exe + + +resources: + micro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/actors/cpp/basic_mpi_cpp/Makefile b/tests/muscle3/actors/cpp/basic_mpi_cpp/Makefile new file mode 100644 index 0000000..35143f4 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_mpi_cpp/Makefile @@ -0,0 +1,11 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=basic_mpi_cpp +ACTOR_TYPE=muscle3-cpp +CODE_LANGUAGE=cpp +WRAPPED_CODE_NAME=parallel_mpi + +clean wrapped actor actor-gui wf-run check-output test: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ + + diff --git a/tests/muscle3/actors/cpp/basic_mpi_cpp/basic_mpi_cpp.yaml b/tests/muscle3/actors/cpp/basic_mpi_cpp/basic_mpi_cpp.yaml new file mode 100644 index 0000000..b243f6f --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_mpi_cpp/basic_mpi_cpp.yaml @@ -0,0 +1,34 @@ +code_description: + implementation: + subroutines: + init: init_code + finalize: clean_up + main: code_step + data_type: legacy + programming_language: cpp + data_dictionary_compliant: 3.37.0 + code_path: $TESTS_DIR/wrapped_codes/cpp/parallel_mpi/libparallel_mpi.a + include_path: $TESTS_DIR/wrapped_codes/cpp/parallel_mpi/parallel_mpi.h + code_parameters: + parameters: + schema: + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: g++ + mpi_compiler_cmd: mpicxx + open_mp_switch: -DAL_MAJOR=$(AL_MAJOR) + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/cpp/basic_mpi_cpp/expected.out b/tests/muscle3/actors/cpp/basic_mpi_cpp/expected.out new file mode 100644 index 0000000..9ab02f0 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_mpi_cpp/expected.out @@ -0,0 +1,3 @@ + 2001.0000000000000 + 4002.0000000000000 + 6003.0000000000000 diff --git a/tests/muscle3/actors/cpp/basic_mpi_cpp/test_micro.ymmsl b/tests/muscle3/actors/cpp/basic_mpi_cpp/test_micro.ymmsl new file mode 100644 index 0000000..5d3fde8 --- /dev/null +++ b/tests/muscle3/actors/cpp/basic_mpi_cpp/test_micro.ymmsl @@ -0,0 +1,15 @@ +ymmsl_version: v0.1 + +model: + name: basic_mpi_cpp + +implementations: + micro: + execution_model: openmpi + executable: bash + args: ['-c', '${ACTOR_DIR}/basic_mpi_cpp/bin/basic_mpi_cpp.exe'] + +resources: + micro: + mpi_processes: 2 + diff --git a/tests/muscle3/actors/cpp/restart_cpp/Makefile b/tests/muscle3/actors/cpp/restart_cpp/Makefile new file mode 100644 index 0000000..8ac0ea4 --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_cpp/Makefile @@ -0,0 +1,12 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=restart_cpp +ACTOR_TYPE=muscle3-cpp + +CODE_LANGUAGE=cpp +WRAPPED_CODE_NAME=basic + +test: | clean wrapped actor wf-run wf-restart check-output + +clean wrapped actor actor-gui wf-run wf-restart check-output: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ \ No newline at end of file diff --git a/tests/muscle3/actors/cpp/restart_cpp/expected.out b/tests/muscle3/actors/cpp/restart_cpp/expected.out new file mode 100644 index 0000000..cb3ad6a --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_cpp/expected.out @@ -0,0 +1,3 @@ + 80001.000000000000 + 100002.00000000000 + 120003.00000000000 diff --git a/tests/muscle3/actors/cpp/restart_cpp/restart_cpp.yaml b/tests/muscle3/actors/cpp/restart_cpp/restart_cpp.yaml new file mode 100644 index 0000000..d30b9ed --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_cpp/restart_cpp.yaml @@ -0,0 +1,34 @@ +code_description: + implementation: + subroutines: + init: init_code + finalize: clean_up + main: code_step + get_state: get_code_state + set_state: restore_code_state + get_timestamp: get_timestamp_cpp + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/cpp/basic/libbasic.a + include_path: $TESTS_DIR/wrapped_codes/cpp/basic/basic.h + programming_language: cpp + data_dictionary_compliant: 3.37.0 + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: g++ + mpi_compiler_cmd: + open_mp_switch: -DAL_MAJOR=$(AL_MAJOR) + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/cpp/restart_cpp/test_micro.ymmsl b/tests/muscle3/actors/cpp/restart_cpp/test_micro.ymmsl new file mode 100644 index 0000000..91d077c --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_cpp/test_micro.ymmsl @@ -0,0 +1,16 @@ +ymmsl_version: v0.1 + +model: + name: restart_cpp + +checkpoints: + at_end: true + +implementations: + micro: + executable: ${ACTOR_DIR}/restart_cpp/bin/restart_cpp.exe + + +resources: + micro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/actors/cpp/restart_mpi_cpp/Makefile b/tests/muscle3/actors/cpp/restart_mpi_cpp/Makefile new file mode 100644 index 0000000..8cc0cb7 --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_mpi_cpp/Makefile @@ -0,0 +1,12 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=restart_mpi_cpp +ACTOR_TYPE=muscle3-cpp + +CODE_LANGUAGE=cpp +WRAPPED_CODE_NAME=parallel_mpi + +test: | clean wrapped actor wf-run wf-restart check-output + +clean wrapped actor actor-gui wf-run wf-restart check-output: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ \ No newline at end of file diff --git a/tests/muscle3/actors/cpp/restart_mpi_cpp/expected.out b/tests/muscle3/actors/cpp/restart_mpi_cpp/expected.out new file mode 100644 index 0000000..c63cb63 --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_mpi_cpp/expected.out @@ -0,0 +1,3 @@ + 8001.0000000000000 + 10002.000000000000 + 12003.000000000000 diff --git a/tests/muscle3/actors/cpp/restart_mpi_cpp/restart_mpi_cpp.yaml b/tests/muscle3/actors/cpp/restart_mpi_cpp/restart_mpi_cpp.yaml new file mode 100644 index 0000000..59f42c6 --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_mpi_cpp/restart_mpi_cpp.yaml @@ -0,0 +1,34 @@ +code_description: + implementation: + subroutines: + init: init_code + finalize: clean_up + main: code_step + get_state: get_code_state + set_state: restore_code_state + get_timestamp: get_timestamp_cpp + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/cpp/parallel_mpi/libparallel_mpi.a + include_path: $TESTS_DIR/wrapped_codes/cpp/parallel_mpi/parallel_mpi.h + programming_language: cpp + data_dictionary_compliant: 3.37.0 + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: g++ + mpi_compiler_cmd: mpicxx + open_mp_switch: -DAL_MAJOR=$(AL_MAJOR) + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/cpp/restart_mpi_cpp/test_micro.ymmsl b/tests/muscle3/actors/cpp/restart_mpi_cpp/test_micro.ymmsl new file mode 100644 index 0000000..5206a57 --- /dev/null +++ b/tests/muscle3/actors/cpp/restart_mpi_cpp/test_micro.ymmsl @@ -0,0 +1,18 @@ +ymmsl_version: v0.1 + +model: + name: restart_mpi_cpp + +checkpoints: + at_end: true + +implementations: + micro: + execution_model: openmpi + executable: bash + args: ['-c', '${ACTOR_DIR}/restart_mpi_cpp/bin/restart_mpi_cpp.exe'] + + +resources: + micro: + mpi_processes: 2 \ No newline at end of file diff --git a/tests/muscle3/actors/fortran/basic_mpi/Makefile b/tests/muscle3/actors/fortran/basic_mpi/Makefile new file mode 100644 index 0000000..7ab0274 --- /dev/null +++ b/tests/muscle3/actors/fortran/basic_mpi/Makefile @@ -0,0 +1,9 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=basic_mpi +ACTOR_TYPE=muscle3-fortran +CODE_LANGUAGE=fortran +WRAPPED_CODE_NAME=parallel_mpi + +clean wrapped actor actor-gui wf-run wf-restart check-output test: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ diff --git a/tests/muscle3/actors/fortran/basic_mpi/basic_mpi.yaml b/tests/muscle3/actors/fortran/basic_mpi/basic_mpi.yaml new file mode 100644 index 0000000..3aef354 --- /dev/null +++ b/tests/muscle3/actors/fortran/basic_mpi/basic_mpi.yaml @@ -0,0 +1,33 @@ +code_description: + implementation: + subroutines: + init: init_code + finalize: clean_up + main: code_step + programming_language: Fortran + data_dictionary_compliant: 3.37.0 + code_path: $TESTS_DIR/wrapped_codes/fortran/parallel_mpi/libparallel_mpi.a + include_path: $TESTS_DIR/wrapped_codes/fortran/parallel_mpi/mod_parallel_mpi.mod + data_type: legacy + code_parameters: + parameters: + schema: + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: gfortran + mpi_compiler_cmd: mpifort + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/fortran/basic_mpi/expected.out b/tests/muscle3/actors/fortran/basic_mpi/expected.out new file mode 100644 index 0000000..e4373b0 --- /dev/null +++ b/tests/muscle3/actors/fortran/basic_mpi/expected.out @@ -0,0 +1,3 @@ + 2002.0000000000000 + 4003.0000000000000 + 6004.0000000000000 diff --git a/tests/muscle3/actors/fortran/basic_mpi/test_micro.ymmsl b/tests/muscle3/actors/fortran/basic_mpi/test_micro.ymmsl new file mode 100644 index 0000000..c704a1a --- /dev/null +++ b/tests/muscle3/actors/fortran/basic_mpi/test_micro.ymmsl @@ -0,0 +1,15 @@ +ymmsl_version: v0.1 + +model: + name: basic_mpi + +implementations: + micro: + execution_model: openmpi + executable: bash + args: ['-c', '${ACTOR_DIR}/basic_mpi/bin/basic_mpi.exe'] + +resources: + micro: + mpi_processes: 2 + diff --git a/tests/muscle3/actors/fortran/code_lifecycle/Makefile b/tests/muscle3/actors/fortran/code_lifecycle/Makefile new file mode 100644 index 0000000..b7de788 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_lifecycle/Makefile @@ -0,0 +1,9 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=code_lifecycle +ACTOR_TYPE=muscle3-fortran +CODE_LANGUAGE=fortran +WRAPPED_CODE_NAME=basic + +clean wrapped actor actor-gui wf-run check-output test: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ \ No newline at end of file diff --git a/tests/muscle3/actors/fortran/code_lifecycle/code_lifecycle.yaml b/tests/muscle3/actors/fortran/code_lifecycle/code_lifecycle.yaml new file mode 100644 index 0000000..ae6a9ac --- /dev/null +++ b/tests/muscle3/actors/fortran/code_lifecycle/code_lifecycle.yaml @@ -0,0 +1,34 @@ +code_description: + implementation: + subroutines: + init: init_code + main: code_step + finalize: clean_up + programming_language: Fortran + data_dictionary_compliant: 3.37.0 + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/fortran/basic/libbasic.a + include_path: $TESTS_DIR/wrapped_codes/fortran/basic/mod_basic.mod + code_parameters: + parameters: $TESTS_DIR/code_parameters/xml/input/input_physics.xml + schema: $TESTS_DIR/code_parameters/xml/input/input_physics.xsd + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: gfortran + mpi_compiler_cmd: + extra_libraries: + pkg_config_defined: + - xmllib + path_defined: + + diff --git a/tests/muscle3/actors/fortran/code_lifecycle/expected.out b/tests/muscle3/actors/fortran/code_lifecycle/expected.out new file mode 100644 index 0000000..39f4a47 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_lifecycle/expected.out @@ -0,0 +1,3 @@ + 20001.000000000000 + 40002.000000000000 + 60003.000000000000 diff --git a/tests/muscle3/actors/fortran/code_lifecycle/input_physics.xml b/tests/muscle3/actors/fortran/code_lifecycle/input_physics.xml new file mode 100644 index 0000000..362c4bf --- /dev/null +++ b/tests/muscle3/actors/fortran/code_lifecycle/input_physics.xml @@ -0,0 +1,31 @@ + + + + 131024 + + + 1 + + + 10 + + + 2 + + + 1.5 + + + public + + + iter + + + user_environment_variable + + + iter + + + diff --git a/tests/muscle3/actors/fortran/code_lifecycle/input_physics.xsd b/tests/muscle3/actors/fortran/code_lifecycle/input_physics.xsd new file mode 100644 index 0000000..3102010 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_lifecycle/input_physics.xsd @@ -0,0 +1,120 @@ + + + Code parameters for Physics Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Input/output shot number + + + + + + + Input run number + + + + + + + Output run number + + + + + + + Number of times the multiplication factor is applied + + + + + + + Multiplication factor for the major radius + + + + + + + Input database name to read from external database + + + + + + + Input machine name to read from external database + + + + + + + Local database name to write into local IMAS database + + + + + + + Local machine name to write into local IMAS database + + + + + diff --git a/tests/muscle3/actors/fortran/code_lifecycle/test_micro.ymmsl b/tests/muscle3/actors/fortran/code_lifecycle/test_micro.ymmsl new file mode 100644 index 0000000..d49d603 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_lifecycle/test_micro.ymmsl @@ -0,0 +1,13 @@ +ymmsl_version: v0.1 + +model: + name: code_lifecycle + +implementations: + micro: + executable: ${ACTOR_DIR}/code_lifecycle/bin/code_lifecycle.exe + + +resources: + micro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/actors/fortran/code_restart/Makefile b/tests/muscle3/actors/fortran/code_restart/Makefile new file mode 100644 index 0000000..dc4b38c --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart/Makefile @@ -0,0 +1,11 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=code_restart +ACTOR_TYPE=muscle3-fortran +CODE_LANGUAGE=fortran +WRAPPED_CODE_NAME=basic + +test: | clean wrapped actor wf-run wf-restart check-output + +clean wrapped actor actor-gui wf-run wf-restart check-output: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ \ No newline at end of file diff --git a/tests/muscle3/actors/fortran/code_restart/code_restart.yaml b/tests/muscle3/actors/fortran/code_restart/code_restart.yaml new file mode 100644 index 0000000..e8968f7 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart/code_restart.yaml @@ -0,0 +1,33 @@ +code_description: + implementation: + subroutines: + init: init_code + finalize: clean_up + main: code_step + get_state: get_code_state + set_state: restore_code_state + get_timestamp: get_timestamp + programming_language: Fortran + data_dictionary_compliant: 3.37.0 + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/fortran/basic/libbasic.a + include_path: $TESTS_DIR/wrapped_codes/fortran/basic/mod_basic.mod + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: gfortran + mpi_compiler_cmd: + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/fortran/code_restart/expected.out b/tests/muscle3/actors/fortran/code_restart/expected.out new file mode 100644 index 0000000..cb3ad6a --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart/expected.out @@ -0,0 +1,3 @@ + 80001.000000000000 + 100002.00000000000 + 120003.00000000000 diff --git a/tests/muscle3/actors/fortran/code_restart/test_micro.ymmsl b/tests/muscle3/actors/fortran/code_restart/test_micro.ymmsl new file mode 100644 index 0000000..94ac2ed --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart/test_micro.ymmsl @@ -0,0 +1,16 @@ +ymmsl_version: v0.1 + +model: + name: code_restart + +checkpoints: + at_end: true + +implementations: + micro: + executable: ${ACTOR_DIR}/code_restart/bin/code_restart.exe + + +resources: + micro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/actors/fortran/code_restart_mpi/Makefile b/tests/muscle3/actors/fortran/code_restart_mpi/Makefile new file mode 100644 index 0000000..ea8da26 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart_mpi/Makefile @@ -0,0 +1,11 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=code_restart_mpi +ACTOR_TYPE=muscle3-fortran +CODE_LANGUAGE=fortran +WRAPPED_CODE_NAME=parallel_mpi + +test: | clean wrapped actor wf-run wf-restart check-output + +clean wrapped actor actor-gui wf-run wf-restart check-output: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ \ No newline at end of file diff --git a/tests/muscle3/actors/fortran/code_restart_mpi/code_restart_mpi.yaml b/tests/muscle3/actors/fortran/code_restart_mpi/code_restart_mpi.yaml new file mode 100644 index 0000000..1c66e29 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart_mpi/code_restart_mpi.yaml @@ -0,0 +1,33 @@ +code_description: + implementation: + subroutines: + init: init_code + finalize: clean_up + main: code_step + get_state: get_code_state + set_state: restore_code_state + get_timestamp: get_timestamp + programming_language: Fortran + data_dictionary_compliant: 3.37.0 + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/fortran/parallel_mpi/libparallel_mpi.a + include_path: $TESTS_DIR/wrapped_codes/fortran/parallel_mpi/mod_parallel_mpi.mod + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + settings: + compiler_cmd: gfortran + mpi_compiler_cmd: mpifort + extra_libraries: + pkg_config_defined: + path_defined: + + diff --git a/tests/muscle3/actors/fortran/code_restart_mpi/expected.out b/tests/muscle3/actors/fortran/code_restart_mpi/expected.out new file mode 100644 index 0000000..261d3b4 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart_mpi/expected.out @@ -0,0 +1,3 @@ + 8002.0000000000000 + 10003.000000000000 + 12004.000000000000 diff --git a/tests/muscle3/actors/fortran/code_restart_mpi/test_micro.ymmsl b/tests/muscle3/actors/fortran/code_restart_mpi/test_micro.ymmsl new file mode 100644 index 0000000..f1a7d67 --- /dev/null +++ b/tests/muscle3/actors/fortran/code_restart_mpi/test_micro.ymmsl @@ -0,0 +1,18 @@ +ymmsl_version: v0.1 + +model: + name: code_restart_mpi + +checkpoints: + at_end: true + +implementations: + micro: + execution_model: openmpi + executable: bash + args: ['-c', '${ACTOR_DIR}/code_restart_mpi/bin/code_restart_mpi.exe'] + + +resources: + micro: + mpi_processes: 2 \ No newline at end of file diff --git a/tests/muscle3/actors/python/basic_python/Makefile b/tests/muscle3/actors/python/basic_python/Makefile new file mode 100644 index 0000000..87f868e --- /dev/null +++ b/tests/muscle3/actors/python/basic_python/Makefile @@ -0,0 +1,9 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=basic_python +ACTOR_TYPE=muscle3-python +CODE_LANGUAGE=python +WRAPPED_CODE_NAME=basic + +clean wrapped actor actor-gui wf-run check-output test: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ diff --git a/tests/muscle3/actors/python/basic_python/basic_python.yaml b/tests/muscle3/actors/python/basic_python/basic_python.yaml new file mode 100644 index 0000000..0ca85b2 --- /dev/null +++ b/tests/muscle3/actors/python/basic_python/basic_python.yaml @@ -0,0 +1,25 @@ +code_description: + implementation: + subroutines: + init: init_code + main: code_step + finalize: clean_up + programming_language: Python + data_dictionary_compliant: 3.37.0 + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/python/basic/basic.py + include_path: $TESTS_DIR/wrapped_codes/python/basic/basic.py + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + + + diff --git a/tests/muscle3/actors/python/basic_python/expected.out b/tests/muscle3/actors/python/basic_python/expected.out new file mode 100644 index 0000000..b0e1d76 --- /dev/null +++ b/tests/muscle3/actors/python/basic_python/expected.out @@ -0,0 +1,3 @@ + 19.000000000000000 + 38.000000000000000 + 57.000000000000000 diff --git a/tests/muscle3/actors/python/basic_python/test_micro.ymmsl b/tests/muscle3/actors/python/basic_python/test_micro.ymmsl new file mode 100644 index 0000000..b529e3a --- /dev/null +++ b/tests/muscle3/actors/python/basic_python/test_micro.ymmsl @@ -0,0 +1,13 @@ +ymmsl_version: v0.1 + +model: + name: basic_python + +implementations: + micro: + executable: python + args: ${ACTOR_DIR}/basic_python/standalone.py + +resources: + micro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/actors/python/restart_python/Makefile b/tests/muscle3/actors/python/restart_python/Makefile new file mode 100644 index 0000000..c7253ab --- /dev/null +++ b/tests/muscle3/actors/python/restart_python/Makefile @@ -0,0 +1,12 @@ +export # export all variables to 'sub-Makefiles' + +ACTOR_NAME=restart_python +ACTOR_TYPE=muscle3-python + +CODE_LANGUAGE=python +WRAPPED_CODE_NAME=basic + +test: | clean wrapped actor wf-run wf-restart check-output + +clean wrapped actor actor-gui wf-run wf-restart check-output: + $(MAKE) -f $(TESTS_DIR)/Makefile $@ diff --git a/tests/muscle3/actors/python/restart_python/expected.out b/tests/muscle3/actors/python/restart_python/expected.out new file mode 100644 index 0000000..772ced3 --- /dev/null +++ b/tests/muscle3/actors/python/restart_python/expected.out @@ -0,0 +1,3 @@ + 76.000000000000000 + 95.000000000000000 + 114.00000000000000 diff --git a/tests/muscle3/actors/python/restart_python/restart_python.yaml b/tests/muscle3/actors/python/restart_python/restart_python.yaml new file mode 100644 index 0000000..d64f286 --- /dev/null +++ b/tests/muscle3/actors/python/restart_python/restart_python.yaml @@ -0,0 +1,28 @@ +code_description: + implementation: + subroutines: + init: init_code + main: code_step + finalize: clean_up + get_state: get_code_state + set_state: restore_code_state + get_timestamp: get_timestamp + programming_language: Python + data_dictionary_compliant: 3.37.0 + data_type: legacy + code_path: $TESTS_DIR/wrapped_codes/python/basic/basic.py + include_path: $TESTS_DIR/wrapped_codes/python/basic/basic.py + arguments: + - name: core_profiles_in + type: core_profiles + intent: IN + - name: distribution_sources_out + type: distribution_sources + intent: OUT + documentation: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim + veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. ' + + + diff --git a/tests/muscle3/actors/python/restart_python/test_micro.ymmsl b/tests/muscle3/actors/python/restart_python/test_micro.ymmsl new file mode 100644 index 0000000..588920e --- /dev/null +++ b/tests/muscle3/actors/python/restart_python/test_micro.ymmsl @@ -0,0 +1,17 @@ +ymmsl_version: v0.1 + +model: + name: restart_python + +checkpoints: + at_end: true + +implementations: + micro: + executable: python + args: ${ACTOR_DIR}/restart_python/standalone.py + + +resources: + micro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/code_parameters/xml/input/input_physics.xml b/tests/muscle3/code_parameters/xml/input/input_physics.xml new file mode 100644 index 0000000..362c4bf --- /dev/null +++ b/tests/muscle3/code_parameters/xml/input/input_physics.xml @@ -0,0 +1,31 @@ + + + + 131024 + + + 1 + + + 10 + + + 2 + + + 1.5 + + + public + + + iter + + + user_environment_variable + + + iter + + + diff --git a/tests/muscle3/code_parameters/xml/input/input_physics.xsd b/tests/muscle3/code_parameters/xml/input/input_physics.xsd new file mode 100644 index 0000000..3102010 --- /dev/null +++ b/tests/muscle3/code_parameters/xml/input/input_physics.xsd @@ -0,0 +1,120 @@ + + + Code parameters for Physics Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Input/output shot number + + + + + + + Input run number + + + + + + + Output run number + + + + + + + Number of times the multiplication factor is applied + + + + + + + Multiplication factor for the major radius + + + + + + + Input database name to read from external database + + + + + + + Input machine name to read from external database + + + + + + + Local database name to write into local IMAS database + + + + + + + Local machine name to write into local IMAS database + + + + + diff --git a/tests/muscle3/macro/Makefile b/tests/muscle3/macro/Makefile new file mode 100644 index 0000000..bc4dede --- /dev/null +++ b/tests/muscle3/macro/Makefile @@ -0,0 +1,27 @@ +AL_VERSION ?= $(UAL_VERSION) +AL_MAJOR := $(firstword $(subst ., ,$(AL_VERSION))) + +FLAGS= -g -fPIC # FPIC AND PREPROCESSING OPTIONS + +ifeq ($(AL_MAJOR),) + $(error Could not extract major AL version from UAL_VERSION/AL_VERSION system variable. Is your IMAS module loaded?) +else ifeq ($(AL_MAJOR), 5) + IMAS_LIB_NAME=al-fortran +else ifeq ($(AL_MAJOR), 4) + IMAS_LIB_NAME=imas-$(FC) +endif + +LIBS = `pkg-config --libs $(IMAS_LIB_NAME) ymmsl libmuscle_fortran` +INCLUDES = `pkg-config --cflags $(IMAS_LIB_NAME) ymmsl libmuscle_fortran` + +all: test_macro.exe + +%.o: %.f90 + $(FC) $(FLAGS) -c -o $@ $^ ${INCLUDES} + +%.exe: %.o + $(FC) $(FLAGS) -o $@ $^ $(LIBS) + +clean: + @$(RM) -f *.o *.exe + diff --git a/tests/muscle3/macro/test_macro.f90 b/tests/muscle3/macro/test_macro.f90 new file mode 100644 index 0000000..a8ce709 --- /dev/null +++ b/tests/muscle3/macro/test_macro.f90 @@ -0,0 +1,144 @@ +program helloworld + use ids_routines + use ymmsl + use libmuscle + implicit none + + + + + + type (ids_core_profiles) :: core_profiles_out + type (ids_distribution_sources) :: distribution_sources_in + + real (selected_real_kind(15)) :: t_cur, t_next, t_max, dt + character(len=1), dimension(:), allocatable :: serialized_ids + real :: time + + type(LIBMUSCLE_PortsDescription) :: ports + type(LIBMUSCLE_Instance) :: instance + + type(LIBMUSCLE_Message) :: rmsg + type(LIBMUSCLE_DataConstRef) :: ritem + + type(LIBMUSCLE_Message) :: smsg + type(LIBMUSCLE_Data) :: sitem + + character(:), allocatable :: test_dir_path + + write(*,*) "Starting Fortran M3 macro" + + test_dir_path = get_test_dir() + + open(10, file=test_dir_path//"/test.out", status="NEW") + + ports = LIBMUSCLE_PortsDescription_create() + call LIBMUSCLE_PortsDescription_add(ports, YMMSL_OPERATOR_O_I, 'core_profiles_out') + call LIBMUSCLE_PortsDescription_add(ports, YMMSL_OPERATOR_S, 'distribution_sources_in') + instance = LIBMUSCLE_Instance_create(ports, & + LIBMUSCLE_InstanceFlags(KEEPS_NO_STATE_FOR_NEXT_USE=.true.)) + call LIBMUSCLE_PortsDescription_free(ports) + + do while (LIBMUSCLE_Instance_reuse_instance(instance)) + ! F_INIT + t_max = LIBMUSCLE_Instance_get_setting_as_real8(instance, 't_max') + dt = LIBMUSCLE_Instance_get_setting_as_real8(instance, 'dt') + + time = 1 + + t_cur = 0.0 + do while (t_cur + dt < t_max) + ! O_I + t_next = t_cur + dt + + + ! SENDING DATA + allocate(core_profiles_out%ids_properties%comment(1)) + core_profiles_out%ids_properties%comment(1) = 'Hello world!' + core_profiles_out%ids_properties%homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS + allocate(core_profiles_out%code%name(1)) + core_profiles_out%code%name = 'HelloWorld Actor in Fortran' + + allocate(core_profiles_out%time(1)) + core_profiles_out%time(1) = t_next + + + call ids_serialize(core_profiles_out, serialized_ids) + call ids_deallocate(core_profiles_out) + + sitem = LIBMUSCLE_Data_create_byte_array(serialized_ids) + smsg = LIBMUSCLE_Message_create(t_cur, sitem) + + if (t_next + dt <= t_max) then + call LIBMUSCLE_Message_set_next_timestamp(smsg, t_next) + end if + call LIBMUSCLE_Instance_send(instance, 'core_profiles_out', smsg) + call LIBMUSCLE_Message_free(smsg) + call LIBMUSCLE_Data_free(sitem) + deallocate(serialized_ids) + + + ! > > > - - - RECEIVING DATA - - - < < < + + rmsg = LIBMUSCLE_Instance_receive(instance, 'distribution_sources_in') + ritem = LIBMUSCLE_Message_get_data(rmsg); + + allocate(serialized_ids(LIBMUSCLE_DataConstRef_size(ritem))) + call LIBMUSCLE_DataConstRef_as_byte_array(ritem, serialized_ids) + call LIBMUSCLE_DataConstRef_free(ritem) + + + ! deserialize and verify received IDS + call ids_deserialize(serialized_ids, distribution_sources_in) + deallocate(serialized_ids) + + if (.not. associated(distribution_sources_in%time)) then + call LIBMUSCLE_Instance_error_shutdown(instance, "Received TIME is not associated") + call exit(1) + end if + + write(*,*) "TIME received ", distribution_sources_in%time + write(10,*) distribution_sources_in%time + + time = distribution_sources_in%time(1) + + call ids_deallocate(distribution_sources_in) + + if (LIBMUSCLE_Message_has_next_timestamp(rmsg)) then + t_max = LIBMUSCLE_Message_next_timestamp(rmsg) + end if + + call LIBMUSCLE_Message_free(rmsg) + + ! a simulation would actually update something here, but we're just saying + ! hi to connected actors and don't do anything else + t_cur = t_cur + dt + end do + + + + end do + + close(10) + +contains + + FUNCTION get_test_dir() RESULT(test_dir_path) + + CHARACTER(:), allocatable :: test_dir_path + CHARACTER(len=1000) :: tmp_str + INTEGER :: var_size, status + + CALL get_environment_variable ("TEST_DIR", value=tmp_str, length=var_size, status=status) + + if (status .ne. 0) then + write (*,*) 'Getting environment variable "TEST_DIR" failed: status = ', status + stop + end if + + ALLOCATE(character(len=var_size) :: test_dir_path) + + test_dir_path =tmp_str(1:var_size) + + END FUNCTION get_test_dir +end program helloworld diff --git a/tests/muscle3/workflow/test.ymmsl b/tests/muscle3/workflow/test.ymmsl new file mode 100644 index 0000000..fc031f6 --- /dev/null +++ b/tests/muscle3/workflow/test.ymmsl @@ -0,0 +1,16 @@ +ymmsl_version: v0.1 + +model: + name: helloworld + components: + macro: macro + micro: micro + conduits: + macro.core_profiles_out: micro.core_profiles_in + micro.distribution_sources_out: macro.distribution_sources_in +settings: + t_max: 4.0 + dt: 1.0 + muscle_remote_log_level: DEBUG + + diff --git a/tests/muscle3/workflow/test_macro.ymmsl b/tests/muscle3/workflow/test_macro.ymmsl new file mode 100644 index 0000000..cd41116 --- /dev/null +++ b/tests/muscle3/workflow/test_macro.ymmsl @@ -0,0 +1,9 @@ +ymmsl_version: v0.1 + +implementations: + macro: + executable: ${TESTS_DIR}/macro/test_macro.exe + +resources: + macro: + threads: 1 \ No newline at end of file diff --git a/tests/muscle3/wrapped_codes/cpp/basic/Makefile b/tests/muscle3/wrapped_codes/cpp/basic/Makefile new file mode 100644 index 0000000..c9c051f --- /dev/null +++ b/tests/muscle3/wrapped_codes/cpp/basic/Makefile @@ -0,0 +1,44 @@ +AL_VERSION ?= $(UAL_VERSION) +AL_MAJOR := $(firstword $(subst ., ,$(AL_VERSION))) + +CFLAGS = -pthread -g -O0 -D__USE_XOPEN2K8 -fPIC -DAL_MAJOR=$(AL_MAJOR) + +ifeq ($(AL_MAJOR),) + $(error Could not extract major AL version from UAL_VERSION/AL_VERSION system variable. Is your IMAS module loaded?) +else ifeq ($(AL_MAJOR), 5) + IMAS_LIB_NAME=al-cpp +else ifeq ($(AL_MAJOR), 4) + IMAS_LIB_NAME=imas-cpp +endif + +LIBS = `pkg-config --libs $(IMAS_LIB_NAME)` +INCLUDES = `pkg-config --cflags $(IMAS_LIB_NAME)` + +all: libbasic.a + +# LIBRARY COMPILATION +lib%.a: %.o + $(AR) -rcs $@ $^ + +# RULES FOR ALL OBJECT FILES +%.o: %.cpp + $(CXX) $(CFLAGS) -c -o $@ $^ $(INCLUDES) $(LIBS) + + +# CLEAN DIRECTORY +clean: + $(RM) *.o *.a + + + + + + + + + + + + + + diff --git a/tests/muscle3/wrapped_codes/cpp/basic/basic.cpp b/tests/muscle3/wrapped_codes/cpp/basic/basic.cpp new file mode 100644 index 0000000..73bca36 --- /dev/null +++ b/tests/muscle3/wrapped_codes/cpp/basic/basic.cpp @@ -0,0 +1,134 @@ +#include + +#include "basic.h" + +// ======================================= +// GET STATE +//======================================= + +int code_state = 0; + +void get_code_state( std::string& state_out, int& status_code, std::string& status_message) +{ + status_code = 0; + + status_message = "INITIALISATION: OK"; + state_out = std::to_string(code_state); + + std::cout << "=======================================================" << std::endl; + std::cout << "Code restart CPP: GET STATE called" << std::endl; + std::cout << "STATE is : " << state_out << std::endl; + std::cout << "=======================================================" << std::endl; +} + +// ======================================= +// SET STATE +//======================================= +void restore_code_state( std::string state, int& status_code, std::string& status_message) +{ + status_code = 0; + status_message = "FINALISATION: OK"; + + code_state = std::stoi( state ); + std::cout << "=======================================================" << std::endl; + std::cout << "Code lifecycle CPP: RESTORE STATE called" << std::endl; + std::cout << "STATE TO BE RESTORED : " << code_state << std::endl; + std::cout << "=======================================================" << std::endl; +} + +// ======================================= +// GET TIMESTAMP +//======================================= +void get_timestamp_cpp(double& timestamp_out, int& status_code, std::string& status_message) +{ + timestamp_out = (double) code_state; +} + +// ======================================= +// INITIALISATION +//======================================= + +void init_code (int& status_code, std::string& status_message) +{ + status_code = 0; + + status_message = "INITIALISATION: OK"; + + printf("=======================================================\n"); + printf("Code lifecycle CPP: INITIALISATION called\n"); + printf("=======================================================\n"); + // printf( "%s\n", *(codeparam.parameters)); + printf( "\n=======================================\n"); +} + +// ======================================= +// FINALISATION +//======================================= +void clean_up( int& status_code, std::string& status_message) +{ + status_code = 0; + status_message = "FINALISATION: OK"; + + printf("=======================================================\n"); + printf("Code lifecycle CPP: FINALISATION called\n"); + printf("=======================================================\n"); +} + +// ======================================= +// MAIN +//======================================= +void code_step(const IdsNs::IDS::core_profiles& core_profiles_in, + IdsNs::IDS::distribution_sources& distribution_sources_out, + int& status_code, std::string& status_message) +{ + int idsSize = 0; + + // INITIALISATION OF ERROR FLAG + status_code = 0; + + // INITIAL DISPLAY + std::cout << "=======================================" << std::endl; + std::cout << "START OF PHYSICS CODE" << std::endl; + + + std::cout << "Starting from: " << code_state << std::endl; + std::cout << "PWD: " << get_current_dir_name() << std::endl; + + + + for (int i = 0; i < 20; i++) + { + // COMPUTATIONS + code_state++; + } + + + std::cout << "Counting to : " << code_state << std::endl; + + distribution_sources_out.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS; + idsSize = core_profiles_in.time.extent(0); + std::cout << "Size of input IDS: " < 0) { + distribution_sources_out.time.resize(idsSize); + // Fill in the output IDS (Physical data) + for(int i=0; i < idsSize; i++) + { + // Time : copy from input IDS + distribution_sources_out.time(i) = 1000 * code_state + core_profiles_in.time(i); + } + } + else { + distribution_sources_out.time.resize(1); + distribution_sources_out.time(1) = 1000 * code_state; + } + + // INITIALISATION OF STATUS INFO + status_message = "Status info of code_restart CPP"; + + + // FINAL DISPLAY + std::cout << "END OF PHYSICS CODE" << std::endl; + std::cout << "=======================================" << std::endl; + std::cout << " " << std::endl; +} diff --git a/tests/muscle3/wrapped_codes/cpp/basic/basic.h b/tests/muscle3/wrapped_codes/cpp/basic/basic.h new file mode 100644 index 0000000..976203e --- /dev/null +++ b/tests/muscle3/wrapped_codes/cpp/basic/basic.h @@ -0,0 +1,27 @@ +#ifndef _CODE_RESTART_CPP +#define _CODE_RESTART_CPP + +#if 5 == AL_MAJOR + #include "ALClasses.h" +#elif AL_MAJOR == 4 + #include "UALClasses.h" +#else + #warning Could not find AL_MAJOR variable. Assuming AL version = 5.x.x + #include "ALClasses.h" +#endif + +void get_code_state( std::string& state_out, int& status_code, std::string& status_message); + +void restore_code_state( std::string state, int& status_code, std::string& status_message); + +void get_timestamp_cpp(double& timestamp_out, int& status_code, std::string& status_message); + + +void init_code (int& status_code, std::string& status_message); +void clean_up( int& status_code, std::string& status_message); + +void code_step(const IdsNs::IDS::core_profiles& core_profiles_in, + IdsNs::IDS::distribution_sources& distribution_sources_out, + int& status_code, std::string& status_message); + +#endif // _CODE_RESTART_CPP \ No newline at end of file diff --git a/tests/muscle3/wrapped_codes/cpp/parallel_mpi/Makefile b/tests/muscle3/wrapped_codes/cpp/parallel_mpi/Makefile new file mode 100644 index 0000000..99d61c7 --- /dev/null +++ b/tests/muscle3/wrapped_codes/cpp/parallel_mpi/Makefile @@ -0,0 +1,27 @@ +AL_VERSION ?= $(UAL_VERSION) +AL_MAJOR := $(firstword $(subst ., ,$(AL_VERSION))) + +FLAGS = -pthread -g -O0 -D__USE_XOPEN2K8 -fPIC -DAL_MAJOR=$(AL_MAJOR) + +ifeq ($(AL_MAJOR),) + $(error Could not extract major AL version from UAL_VERSION/AL_VERSION system variable. Is your IMAS module loaded?) +else ifeq ($(AL_MAJOR), 5) + IMAS_LIB_NAME=al-cpp +else ifeq ($(AL_MAJOR), 4) + IMAS_LIB_NAME=imas-cpp +endif + +LIBS = `pkg-config --libs $(IMAS_LIB_NAME)` +INCLUDES = `pkg-config --cflags $(IMAS_LIB_NAME)` + + +all: libparallel_mpi.a + +lib%.a: %.o + @$(AR) -rvcs $@ $^ + +%.o: %.cpp + $(MPICXX) $(FLAGS) -c -o $@ $^ $(INCLUDES) $(LIBS) + +clean: + @$(RM) -f *.o *.a diff --git a/tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.cpp b/tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.cpp new file mode 100644 index 0000000..c946f59 --- /dev/null +++ b/tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.cpp @@ -0,0 +1,154 @@ +#include +#include "parallel_mpi.h" + +int code_state = 0; + +// ======================================= +// INITIALISATION +//======================================= + +void init_code (int& status_code, std::string& status_message) +{ + int mpi_size, mpi_rank; + + status_code = 0; + status_message = "INITIALISATION: OK"; + + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + std::cout << "=======================================================" << std::endl; + std::cout << "CPP MPI: INITIALISATION called <" << mpi_rank << "/" << mpi_size << ">" << std::endl; + std::cout << "=======================================================" << std::endl; +} + +// ======================================= +// FINALISATION +//======================================= +void clean_up( int& status_code, std::string& status_message) +{ + int mpi_size, mpi_rank; + + status_code = 0; + status_message = "FINALISATION: OK"; + + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + std::cout << "=======================================================" << std::endl; + std::cout << "CPP MPI: FINALISATION called <" << mpi_rank << "/" << mpi_size << ">" << std::endl; + std::cout << "=======================================================" << std::endl; +} + +// ======================================= +// MAIN +//======================================= +void code_step(const IdsNs::IDS::core_profiles& in_core_profiles, + IdsNs::IDS::distribution_sources& out_distribution_sources, + int& status_code, std::string& status_message) +{ + int mpi_size, mpi_rank; + int idsSize = -1; + + status_code = 0; + status_message = "STEP OK"; + + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + std::cout << "=======================================" << std::endl; + std::cout << "START OF PHYSICS CODE<" << mpi_rank << "/" << mpi_size << ">" << std::endl; + std::cout << "Starting from: " << code_state << std::endl; + + for (int i = 0; i < 20; i++) + { + // ANY COMPUTATIONS + code_state++; + } + + std::cout << "Counting to : " << code_state << std::endl; + + idsSize = in_core_profiles.time.extent(0); + std::cout << "Size of input IDS: " <" << std::endl; + + if (idsSize > 0) { + out_distribution_sources.time.resize(idsSize); + // Fill in the output IDS (Physical data) + for(int i=0; i < idsSize; i++) + { + // Time : copy from input IDS + out_distribution_sources.time(i) = 1000000 * mpi_rank + 100 * code_state + in_core_profiles.time(i); + } + } + else { + out_distribution_sources.time.resize(1); + out_distribution_sources.time(1) = 1000000 * mpi_rank + 100 * code_state; + } + out_distribution_sources.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS; + out_distribution_sources.code.name = "cp2ds_mpi_cpp"; + out_distribution_sources.code.version = "1.0"; + out_distribution_sources.code.output_flag = 0 ; + + + std::cout << "END OF PHYSICS CODE" << std::endl; + std::cout << "=======================================" << std::endl; + +} + +// ======================================= +// GET STATE +//======================================= + + +void get_code_state( std::string& state_out, int& status_code, std::string& status_message) +{ + int mpi_size, mpi_rank; + status_code = 0; + + status_message = "INITIALISATION: OK"; + state_out = std::to_string(code_state); + + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + std::cout << "=======================================================" << std::endl; + std::cout << "CPP MPI: GET STATE called<" << mpi_rank << "/" << mpi_size << ">" << std::endl; + std::cout << "STATE is : " << state_out << std::endl; + std::cout << "=======================================================" << std::endl; +} + +// ======================================= +// SET STATE +//======================================= +void restore_code_state( std::string state, int& status_code, std::string& status_message) +{ + int mpi_size, mpi_rank; + status_code = 0; + status_message = "FINALISATION: OK"; + + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + code_state = std::stoi( state ); + std::cout << "=======================================================" << std::endl; + std::cout << "CPP MPI: RESTORE STATE called<" << mpi_rank << "/" << mpi_size << ">" << std::endl; + std::cout << "STATE TO BE RESTORED : " << code_state << std::endl; + std::cout << "=======================================================" << std::endl; +} + +// ======================================= +// GET TIMESTAMP +//======================================= +void get_timestamp_cpp(double& timestamp_out, int& status_code, std::string& status_message) +{ + int mpi_size, mpi_rank; + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + + timestamp_out = (double) code_state; + + std::cout << "=======================================================" << std::endl; + std::cout << "CPP MPI: GET TIMESTAMP called<" << mpi_rank << "/" << mpi_size << ">" << std::endl; + std::cout << "TIMESTAMP : " << timestamp_out << std::endl; + std::cout << "=======================================================" << std::endl; +} \ No newline at end of file diff --git a/tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.h b/tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.h new file mode 100644 index 0000000..1154bec --- /dev/null +++ b/tests/muscle3/wrapped_codes/cpp/parallel_mpi/parallel_mpi.h @@ -0,0 +1,27 @@ +#ifndef _PARALLEL_MPI_CPP +#define _PARALLEL_MPI_CPP + +#if 5 == AL_MAJOR + #include "ALClasses.h" +#elif AL_MAJOR == 4 + #include "UALClasses.h" +#else + #warning Could not find AL_MAJOR variable. Assuming AL version = 5.x.x + #include "ALClasses.h" +#endif + +void init_code (int& status_code, std::string& status_message); + +void clean_up( int& status_code, std::string& status_message); + +void code_step(const IdsNs::IDS::core_profiles& core_profiles_in, + IdsNs::IDS::distribution_sources& distribution_sources_out, + int& status_code, std::string& status_message); + +void get_code_state( std::string& state_out, int& status_code, std::string& status_message); + +void restore_code_state( std::string state, int& status_code, std::string& status_message); + +void get_timestamp_cpp(double& timestamp_out, int& status_code, std::string& status_message); + +#endif // _PARALLEL_MPI_CPP \ No newline at end of file diff --git a/tests/muscle3/wrapped_codes/fortran/basic/Makefile b/tests/muscle3/wrapped_codes/fortran/basic/Makefile new file mode 100644 index 0000000..d2979e3 --- /dev/null +++ b/tests/muscle3/wrapped_codes/fortran/basic/Makefile @@ -0,0 +1,30 @@ +AL_VERSION ?= $(UAL_VERSION) +AL_MAJOR := $(firstword $(subst ., ,$(AL_VERSION))) + +FLAGS= -g -fPIC # FPIC AND PREPROCESSING OPTIONS + +ifeq ($(AL_MAJOR),) + $(error Could not extract major AL version from UAL_VERSION/AL_VERSION system variable. Is your IMAS module loaded?) +else ifeq ($(AL_MAJOR), 5) + IMAS_LIB_NAME=al-fortran +else ifeq ($(AL_MAJOR), 4) + IMAS_LIB_NAME=imas-$(FC) +endif + +LIBS = `pkg-config --libs $(IMAS_LIB_NAME)` +INCLUDES = `pkg-config --cflags $(IMAS_LIB_NAME)` + +all: libbasic.a + +lib%.a: %.o + @$(AR) -rcs $@ $^ + +%.o: %.f90 + $(FC) $(FLAGS) -c -o $@ $^ $(INCLUDES) $(LIBS) + +clean: + @$(RM) -f *.exe *.a *.mod *.o *~ + + + + diff --git a/tests/muscle3/wrapped_codes/fortran/basic/basic.f90 b/tests/muscle3/wrapped_codes/fortran/basic/basic.f90 new file mode 100644 index 0000000..8aae928 --- /dev/null +++ b/tests/muscle3/wrapped_codes/fortran/basic/basic.f90 @@ -0,0 +1,381 @@ +module mod_basic + +implicit none + + type type_codeparam_physics_data + integer :: ntimes + double precision:: multiplication_factor + end type type_codeparam_physics_data + + integer :: code_state = 0 + + interface code_step + module procedure codeStepIdsNoParameters, codeStepIdsParameters + end interface code_step + + interface init_code + module procedure initNoIdsNoParameters, initOnlyParameters, initIdsOnly, initIdsParameters + end interface init_code + + interface clean_up + module procedure cleanUpNoIds, cleanUpIds + end interface clean_up + +contains + ! + ! RESTORE_CODE_STATE SUBROUTINE + ! + subroutine restore_code_state (state_str, status_code, status_message) + + implicit none + character(len=:), allocatable, intent(in) :: state_str + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + read(state_str , *) code_state + write(*,*) '=======================================' + write(*,*) 'Basic: RESTORE STATE called' + write(*,*) 'STATE TO BE RESTORED :', code_state + write(*,*) '=======================================' + + end subroutine restore_code_state + + ! + ! GET_CODE_STATE SUBROUTINE + ! + subroutine get_code_state (state_str, status_code, status_message) + + implicit none + character(len=:), allocatable, intent(out) :: state_str + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + allocate(character(50):: state_str) + write(state_str,*) code_state + + + + write(*,*) '=======================================' + write(*,*) 'Basic: GET CODE STATE called' + write(*,*) 'STATE is :', state_str + write(*,*) '=======================================' + + end subroutine get_code_state + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! GET TIMESTAMP SUBROUTINE + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + subroutine get_timestamp(timestamp_out, error_flag, error_message) + + integer,parameter :: DP=kind(1.0D0) + real(kind=DP), intent(out) :: timestamp_out + !---- Status info ---- + integer, intent(out) :: error_flag + character(len=:), pointer, intent(out) :: error_message + + error_flag = 0 + timestamp_out = code_state + + end subroutine get_timestamp + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine initNoIdsNoParameters(status_code, status_message) + use ids_schemas, only: ids_parameters_input + implicit none + type(ids_parameters_input) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + call initOnlyParameters (xml_parameters, status_code, status_message) + + end subroutine initNoIdsNoParameters + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine initOnlyParameters(xml_parameters, status_code, status_message) + use ids_schemas, only: ids_parameters_input, ids_core_profiles, ids_distribution_sources + implicit none + type (ids_core_profiles) :: core_profiles_in + type (ids_distribution_sources) :: distribution_sources_out + type(ids_parameters_input), intent(in) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + call initIdsParameters (core_profiles_in, distribution_sources_out, xml_parameters, status_code, status_message) + + end subroutine initOnlyParameters + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine initIdsOnly (core_profiles_in, distribution_sources_out, status_code, status_message) + use ids_schemas, only: ids_parameters_input, ids_core_profiles, ids_distribution_sources + implicit none + type (ids_core_profiles), intent(in) :: core_profiles_in + type (ids_distribution_sources), intent(out) :: distribution_sources_out + type (ids_parameters_input) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + call initIdsParameters (core_profiles_in, distribution_sources_out, xml_parameters, status_code, status_message) + + end subroutine initIdsOnly + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine initIdsParameters (core_profiles_in, distribution_sources_out, xml_parameters, status_code, status_message) + use ids_schemas, only: ids_parameters_input, ids_core_profiles, ids_distribution_sources + use ids_routines, only: IDS_TIME_MODE_UNKNOWN + implicit none + type (ids_core_profiles), intent(in) :: core_profiles_in + type (ids_distribution_sources), intent(out) :: distribution_sources_out + type (ids_parameters_input), intent(in) :: xml_parameters + integer, intent(inout) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: i + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + write(*,*) '=======================================' + write(*,*) 'Code lifecycle: INITIALISATION called' + write(*,*) '---------------------------------------' + + + distribution_sources_out%ids_properties%homogeneous_time = 1 + if (core_profiles_in%ids_properties%homogeneous_time /= IDS_TIME_MODE_UNKNOWN) then + + if (associated(core_profiles_in%time)) then + allocate(distribution_sources_out%time(size(core_profiles_in%time))) + + write(0,*) 'Received size of input time from core_profiles_in : ', SIZE(core_profiles_in%time) + + ! Fill in the output IDS (Physical data) + do i=1,size(core_profiles_in%time) + ! Time : copy from input IDS + distribution_sources_out%time(i) = core_profiles_in%time(i) + 1.0 + enddo + + else + allocate(distribution_sources_out%time(1)) + distribution_sources_out%time(1) = 1000 * code_state + end if + + allocate(distribution_sources_out%code%name(1)) ! For a string of 132 characters max. + distribution_sources_out%code%name(1) = 'basic methods: INIT' + allocate(distribution_sources_out%code%output_flag(1)) + distribution_sources_out%code%output_flag(1) = 0 + + end if + + write(*,*) '---------------------------------------' + write(*,*) 'Basic methods: INITIALISATION ends' + write(*,*) '=======================================' + + end subroutine initIdsParameters + + ! + ! FINALISATION SUBROUTINE + ! + subroutine cleanUpNoIds(status_code, status_message) + use ids_schemas, only: ids_core_profiles, ids_distribution_sources + implicit none + + type (ids_distribution_sources) :: distribution_sources_in + type (ids_core_profiles) :: core_profiles_out + + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + call cleanUpIds(distribution_sources_in, core_profiles_out, status_code, status_message) + + end subroutine cleanUpNoIds + + ! + ! FINALISATION SUBROUTINE + ! + subroutine cleanUpIds(distribution_sources_in, core_profiles_out, status_code, status_message) + use ids_schemas, only: ids_core_profiles, ids_distribution_sources + use ids_routines, only: IDS_TIME_MODE_UNKNOWN + + implicit none + + type (ids_distribution_sources), intent(IN) :: distribution_sources_in + type (ids_core_profiles), intent(OUT) :: core_profiles_out + + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: i + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + write(*,*) '=======================================' + write(*,*) 'Basic methods: FINALISATION called' + write(*,*) '---------------------------------------' + + + + if (distribution_sources_in%ids_properties%homogeneous_time /= IDS_TIME_MODE_UNKNOWN) then + core_profiles_out%ids_properties%homogeneous_time = distribution_sources_in%ids_properties%homogeneous_time + + allocate(core_profiles_out%time(size(distribution_sources_in%time))) + + write(0,*) 'Received size of input time from core_profiles_in : ', SIZE(distribution_sources_in%time) + + ! Fill in the output IDS (Physical data) + do i=1,size(distribution_sources_in%time) + ! Time : copy from input IDS + core_profiles_out%time(i) = distribution_sources_in%time(i) + 1.0 + enddo + + allocate(core_profiles_out%code%name(1)) ! For a string of 132 characters max. + core_profiles_out%code%name(1) = 'basic methods: FINALIZE' + allocate(core_profiles_out%code%output_flag(1)) + core_profiles_out%code%output_flag(1) = 0 + + end if + + write(*,*) '---------------------------------------' + write(*,*) 'Basic methods: FINALISATION ends' + write(*,*) '=======================================' + + end subroutine cleanUpIds + + ! + ! MAIN SUBROUTINE + ! + subroutine codeStepNoIdsNoParameters(status_code, status_message) + use ids_schemas, only: ids_parameters_input + implicit none + + type(ids_parameters_input) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + call codeStepNoIdsParameters(xml_parameters, status_code, status_message) + end subroutine codeStepNoIdsNoParameters + + ! + ! MAIN SUBROUTINE + ! + subroutine codeStepNoIdsParameters(xml_parameters, status_code, status_message) + use ids_schemas, only: ids_core_profiles, ids_distribution_sources, ids_parameters_input + implicit none + + type(ids_core_profiles):: core_profiles_in + type(ids_distribution_sources):: distribution_sources_out + type(ids_parameters_input) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + call codeStepIdsParameters(core_profiles_in, distribution_sources_out, xml_parameters, status_code, status_message) + end subroutine codeStepNoIdsParameters + + ! + ! MAIN SUBROUTINE + ! + subroutine codeStepIdsNoParameters(core_profiles_in, distribution_sources_out, status_code, status_message) + use ids_schemas, only: ids_core_profiles, ids_distribution_sources, ids_parameters_input + implicit none + + type(ids_core_profiles):: core_profiles_in + type(ids_distribution_sources):: distribution_sources_out + type(ids_parameters_input) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + call codeStepIdsParameters(core_profiles_in, distribution_sources_out, xml_parameters, status_code, status_message) + end subroutine codeStepIdsNoParameters + + ! + ! MAIN SUBROUTINE + ! + subroutine codeStepIdsParameters(core_profiles_in, distribution_sources_out, xml_parameters, status_code, status_message) + use ids_schemas, only: ids_core_profiles, ids_distribution_sources, ids_parameters_input + use ids_routines, only: IDS_TIME_MODE_UNKNOWN + + implicit none + + type(ids_core_profiles), intent(IN) :: core_profiles_in + type(ids_distribution_sources), intent(OUT) :: distribution_sources_out + type(ids_parameters_input) :: xml_parameters + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: i + + ! INITIALISATION OF ERROR FLAG + status_code = 0 + allocate(character(50):: status_message) + + ! INITIAL DISPLAY + write(*,*) '=======================================' + write(*,*) 'START OF PHYSICS CODE' + write(*,*) '---------------------------------------' + + write(*,*) 'Starting from: ', code_state + do i = 1, 20 + ! COMPUTATIONS + code_state = code_state + 1 + end do + + write(*,*) 'Counting to: ', code_state + + ! CHECK IF INPUT IDS IS VALID + if ( core_profiles_in%ids_properties%homogeneous_time /= IDS_TIME_MODE_UNKNOWN & + .and.size(core_profiles_in%time)>0) then + + + ! MANDATORY FLAG (UNIFORM TIME HERE) + distribution_sources_out%ids_properties%homogeneous_time = 1 + allocate(distribution_sources_out%code%name(1)) ! For a string of 132 characters max. + + distribution_sources_out%code%name(1) = 'EXAMPLE: STEP method' + allocate(distribution_sources_out%code%version(1)) ! For a string of 132 characters max. + + distribution_sources_out%code%version(1) = '1.0' + allocate(distribution_sources_out%code%output_flag(1)) + distribution_sources_out%code%output_flag(1) = 0 ! Integer output flag, 0 means the run was successful and can be used in the rest of the workflow, <0 means failure + + ! Fill in the output IDS (Physical data) + allocate(distribution_sources_out%time(size(core_profiles_in%time))) + do i=1,size(core_profiles_in%time) + ! Time : copy from input IDS + distribution_sources_out%time(i) = 1000 * code_state + core_profiles_in%time(i) + enddo + + endif + + status_message = 'STEP: OK' + + ! FINAL DISPLAY + write(*,*) '---------------------------------------' + write(*,*) 'END OF PHYSICS CODE' + write(*,*) '=======================================' + + end subroutine codeStepIdsParameters + +end module mod_basic \ No newline at end of file diff --git a/tests/muscle3/wrapped_codes/fortran/basic_parametrizable/Makefile b/tests/muscle3/wrapped_codes/fortran/basic_parametrizable/Makefile new file mode 100644 index 0000000..a97fcd9 --- /dev/null +++ b/tests/muscle3/wrapped_codes/fortran/basic_parametrizable/Makefile @@ -0,0 +1,18 @@ +FLAGS = -g -fPIC +LIBS = `pkg-config --libs imas-$(FC)` +INCLUDES = `pkg-config --cflags imas-$(FC)` + +all: libbasic_parametrizable.a + +lib%.a: %.o + @$(AR) -rcs $@ $^ + +%.o: %.f90 + $(FC) $(FLAGS) -c -o $@ $^ $(INCLUDES) $(LIBS) + +clean: + @$(RM) -f *.exe *.a *.mod *.o *~ + + + + diff --git a/tests/muscle3/wrapped_codes/fortran/basic_parametrizable/basic.f90 b/tests/muscle3/wrapped_codes/fortran/basic_parametrizable/basic.f90 new file mode 100644 index 0000000..e050d72 --- /dev/null +++ b/tests/muscle3/wrapped_codes/fortran/basic_parametrizable/basic.f90 @@ -0,0 +1,172 @@ +module mod_basic + +integer :: code_state = 0 + +contains + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine init_code (status_code, status_message) + use ids_schemas, only: ids_parameters_input + implicit none + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + write(*,*) '=======================================' + write(*,*) 'Code lifecycle: INITIALISATION called' + write(*,*) '=======================================' + + end subroutine init_code + + + ! + ! FINALISATION SUBROUTINE + ! + subroutine clean_up(status_code, status_message) + implicit none + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + write(*,*) '=======================================' + write(*,*) 'Code lifecycle: FINALISATION called' + write(*,*) '=======================================' + + end subroutine clean_up + + ! + ! MAIN SUBROUTINE + ! + subroutine code_step(core_profiles_in, distribution_sources_out, status_code, status_message) + use ids_schemas, only: ids_core_profiles, ids_distribution_sources, ids_is_valid + + implicit none + + type(ids_core_profiles):: core_profiles_in + type(ids_distribution_sources):: distribution_sources_out + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: i + + ! INITIAL DISPLAY + write(*,*) '=======================================' + write(*,*) 'START OF PHYSICS CODE' + + ! INITIALISATION OF ERROR FLAG + status_code = 0 + allocate(character(50):: status_message) + status_message = 'code_restart: OK' + + write(*,*) 'Starting from: ', code_state + + do i = 1, 20 + ! COMPUTATIONS + code_state = code_state + 1 + end do + + write(*,*) 'Counting to: ', code_state + + distribution_sources_out%ids_properties%homogeneous_time = 1 + if (associated(core_profiles_in%time)) then + + allocate(distribution_sources_out%time(size(core_profiles_in%time))) + write(0,*) 'Received size of input time from coreprofilesin : ', SIZE(core_profiles_in%time) + + ! Fill in the output IDS (Physical data) + do i=1,size(core_profiles_in%time) + ! Time : copy from input IDS + distribution_sources_out%time(i) = 1000 * code_state + core_profiles_in%time(i) + ! THE TIME FIELD MUST BE FILLED (MANDATORY) in case of multiple time slice mode for the IDS; + + enddo + else + allocate(distribution_sources_out%time(1)) + distribution_sources_out%time(1) = 1000 * code_state + end if + + ! FINAL DISPLAY + write(*,*) 'END OF PHYSICS CODE' + write(*,*) '=======================================' + write(*,*) ' ' + + end subroutine code_step + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! GET TIMESTAMP SUBROUTINE + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + subroutine get_timestamp(timestamp_out, error_flag, error_message) + + integer,parameter :: DP=kind(1.0D0) + real(kind=DP), intent(out) :: timestamp_out + !---- Status info ---- + integer, intent(out) :: error_flag + character(len=:), pointer, intent(out) :: error_message + + error_flag = 0 + timestamp_out = code_state + + end subroutine get_timestamp + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine get_code_state (state_str, status_code, status_message) + + implicit none + character(len=:), allocatable, intent(out) :: state_str + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + allocate(character(50):: state_str) + write(state_str,*) code_state + + + + write(*,*) '=======================================' + write(*,*) 'Code lifecycle: GET CODE STATE called' + write(*,*) 'STATE is :', state_str + write(*,*) '=======================================' + + end subroutine get_code_state + + subroutine restore_code_state (state_str, status_code, status_message) + ! + ! INITIALISATION SUBROUTINE + ! + implicit none + character(len=:), allocatable, intent(in) :: state_str + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + read(state_str , *) code_state + write(*,*) '=======================================' + write(*,*) 'Code lifecycle: RESTORE STATE called' + write(*,*) 'STATE TO BE RESTORED :', code_state + write(*,*) '=======================================' + + end subroutine restore_code_state + +end module mod_basic diff --git a/tests/muscle3/wrapped_codes/fortran/parallel_mpi/Makefile b/tests/muscle3/wrapped_codes/fortran/parallel_mpi/Makefile new file mode 100644 index 0000000..e4ff8fc --- /dev/null +++ b/tests/muscle3/wrapped_codes/fortran/parallel_mpi/Makefile @@ -0,0 +1,27 @@ +AL_VERSION ?= $(UAL_VERSION) +AL_MAJOR := $(firstword $(subst ., ,$(AL_VERSION))) + +FLAGS= -g -fPIC # FPIC AND PREPROCESSING OPTIONS + +ifeq ($(AL_MAJOR),) + $(error Could not extract major AL version from UAL_VERSION/AL_VERSION system variable. Is your IMAS module loaded?) +else ifeq ($(AL_MAJOR), 5) + IMAS_LIB_NAME=al-fortran +else ifeq ($(AL_MAJOR), 4) + IMAS_LIB_NAME=imas-$(FC) +endif + +LIBS = `pkg-config --libs $(IMAS_LIB_NAME)` +INCLUDES = `pkg-config --cflags $(IMAS_LIB_NAME)` + +all: libparallel_mpi.a + +lib%.a: %.o + @$(AR) -rcs $@ $^ + +%.o: %.f90 + $(MPIFC) $(FLAGS) -c -o $@ $^ $(INCLUDES) $(LIBS) + +clean: + @$(RM) -f *.o *.a *.mod *~ + diff --git a/tests/muscle3/wrapped_codes/fortran/parallel_mpi/parallel_mpi.f90 b/tests/muscle3/wrapped_codes/fortran/parallel_mpi/parallel_mpi.f90 new file mode 100644 index 0000000..63062d4 --- /dev/null +++ b/tests/muscle3/wrapped_codes/fortran/parallel_mpi/parallel_mpi.f90 @@ -0,0 +1,213 @@ +module mod_parallel_mpi + + use mpi + use ids_schemas + + integer :: code_state = 0 + +contains + + + ! + ! INITIALISATION SUBROUTINE + ! + subroutine init_code (status_code, status_message) + use ids_schemas, only: ids_parameters_input + implicit none + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: mpi_world_size, mpi_rank, error + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + call MPI_Comm_size ( MPI_COMM_WORLD, mpi_world_size, error ) + call MPI_Comm_rank ( MPI_COMM_WORLD, mpi_rank, error ) + + write(*,*) '=======================================' + write(*,*) 'Parallel MPI:: INITIALISATION called' + write(*,*) 'Process <', mpi_rank, '> out of: ', mpi_world_size + write(*,*) '=======================================' + + end subroutine init_code + + + ! + ! FINALISATION SUBROUTINE + ! + subroutine clean_up(status_code, status_message) + implicit none + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: mpi_world_size, mpi_rank, error + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + call MPI_Comm_size ( MPI_COMM_WORLD, mpi_world_size, error ) + call MPI_Comm_rank ( MPI_COMM_WORLD, mpi_rank, error ) + + write(*,*) '=======================================' + write(*,*) 'Parallel MPI: FINALISATION called' + write(*,*) 'Process <', mpi_rank, '> out of: ', mpi_world_size + write(*,*) '=======================================' + + end subroutine clean_up + + subroutine code_step(coreprofilesin, distsourceout, error_flag, error_message) + + implicit none + + integer,parameter :: DP=kind(1.0D0) + + type (ids_core_profiles) :: coreprofilesin + type (ids_distribution_sources) :: distsourceout + integer, intent(out) :: error_flag + character(len=:), pointer, intent(out) :: error_message + + integer :: i + integer :: mpi_world_size, mpi_rank, error + + write(0,*) 'Entering subroutine parallel MPI: code_step' + + ! INITIALISATION OF ERROR FLAG + error_flag = 0 + allocate(character(50):: error_message) + error_message = 'Status info of parallel_mpi' + + call MPI_Comm_size ( MPI_COMM_WORLD, mpi_world_size, error ) + call MPI_Comm_rank ( MPI_COMM_WORLD, mpi_rank, error ) + + ! The output IDS must be allocated with its number of time slices (1 for a single time slice physics module) + ! Here we allocate the output IDS to the same size as the input IDS (but this is not a general rule) + + write(*,*) '=======================================' + write(*,*) 'Parallel MPI: STEP called' + write(*,*) 'Process <', mpi_rank, '> out of: ', mpi_world_size + write(*,*) '=======================================' + + write(*,*) 'Starting from: ', code_state + + do i = 1, 20 + ! COMPUTATIONS + code_state = code_state + 1 + end do + + write(*,*) 'Counting to: ', code_state + + if (associated(coreprofilesin%time)) then + + allocate(distsourceout%time(size(coreprofilesin%time))) + write(0,*) 'Received size of input time from coreprofilesin : ', SIZE(coreprofilesin%time) + + ! Fill in the output IDS (Physical data) + do i=1,size(coreprofilesin%time) + ! Time : copy from input IDS + distsourceout%time(i) = 1000000 * mpi_rank + 100 * code_state + coreprofilesin%time(i) + 1 + ! THE TIME FIELD MUST BE FILLED (MANDATORY) in case of multiple time slice mode for the IDS; + + enddo + else + allocate(distsourceout%time(1)) + distsourceout%time(1) = 1000000 * mpi_rank + 100 * code_state + end if + + + distsourceout%ids_properties%homogeneous_time = 1 + + allocate(distsourceout%code%name(1)) ! For a string of 132 characters max. + distsourceout%code%name(1) = 'parallel_mpi' + allocate(distsourceout%code%version(1)) ! For a string of 132 characters max. + distsourceout%code%version(1) = '1.0' + allocate(distsourceout%code%output_flag(1)) + distsourceout%code%output_flag(1) = 0 ! Integer output flag, 0 means the run was successful and can be used in the rest of the workflow, <0 means failure + + write(*,*) 'END OF PHYSICS CODE' + write(*,*) '=======================================' + + return + end subroutine + + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! GET TIMESTAMP SUBROUTINE + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + subroutine get_timestamp(timestamp_out, error_flag, error_message) + + integer,parameter :: DP=kind(1.0D0) + real(kind=DP), intent(out) :: timestamp_out + !---- Status info ---- + integer, intent(out) :: error_flag + character(len=:), pointer, intent(out) :: error_message + integer :: mpi_world_size, mpi_rank, error + + error_flag = 0 + timestamp_out = code_state + + end subroutine get_timestamp + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! GET STATE SUBROUTINE + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + subroutine get_code_state (state_str, status_code, status_message) + + implicit none + character(len=:), allocatable, intent(out) :: state_str + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: mpi_world_size, mpi_rank, error + + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + allocate(character(50):: state_str) + write(state_str,*) code_state + + call MPI_Comm_size ( MPI_COMM_WORLD, mpi_world_size, error ) + call MPI_Comm_rank ( MPI_COMM_WORLD, mpi_rank, error ) + + write(*,*) '=======================================' + write(*,*) 'Parallel MPI:: GET CODE STATE called' + write(*,*) 'Process <', mpi_rank, '> out of: ', mpi_world_size + write(*,*) 'STATE is :', state_str + write(*,*) '=======================================' + + end subroutine get_code_state + + + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ! SET STATE SUBROUTINE + ! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + subroutine restore_code_state (state_str, status_code, status_message) + + implicit none + character(len=:), allocatable, intent(in) :: state_str + integer, intent(out) :: status_code + character(len=:), pointer, intent(out) :: status_message + integer :: mpi_world_size, mpi_rank, error + + ! Setting status to SUCCESS + status_code = 0 + allocate(character(50):: status_message) + status_message = 'OK' + + call MPI_Comm_size ( MPI_COMM_WORLD, mpi_world_size, error ) + call MPI_Comm_rank ( MPI_COMM_WORLD, mpi_rank, error ) + + read(state_str , *) code_state + write(*,*) '=======================================' + write(*,*) 'Parallel MPI: RESTORE STATE called' + write(*,*) 'STATE TO BE RESTORED :', code_state + write(*,*) 'Process <', mpi_rank, '> out of: ', mpi_world_size + write(*,*) '=======================================' + + end subroutine restore_code_state +end module mod_parallel_mpi diff --git a/tests/muscle3/wrapped_codes/python/basic/Makefile b/tests/muscle3/wrapped_codes/python/basic/Makefile new file mode 100644 index 0000000..3d83c36 --- /dev/null +++ b/tests/muscle3/wrapped_codes/python/basic/Makefile @@ -0,0 +1,7 @@ + +all: + #do nothing + +clean: + #do nothing + diff --git a/tests/muscle3/wrapped_codes/python/basic/basic.py b/tests/muscle3/wrapped_codes/python/basic/basic.py new file mode 100644 index 0000000..12c9daa --- /dev/null +++ b/tests/muscle3/wrapped_codes/python/basic/basic.py @@ -0,0 +1,94 @@ +code_state = 0 + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# GET CODE STATE +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def get_code_state(): + state_str = code_state + + print( '=======================================' ) + print( 'Code lifecycle: GET CODE STATE called' ) + print( 'STATE is :', state_str ) + print( '=======================================' ) + + return state_str + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# SET CODE STATE +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def restore_code_state(state_str): + global code_state + + code_state = float( state_str ) + print( '=======================================' ) + print( 'Code lifecycle: RESTORE STATE called' ) + print( 'STATE TO BE RESTORED :', code_state ) + print( '=======================================' ) + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# GET TIMESTAMP +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def get_timestamp(): + + timestamp_out = code_state + + return timestamp_out + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# INITIALISATION +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def init_code (): + + print('=======================================') + print('Code lifecycle: INITIALISATION called') + print('=======================================') + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# FINALISATION +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def clean_up(): + + print('=======================================') + print('Code lifecycle: FINALISATION called') + print('=======================================') + + + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# MAIN +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +def code_step(core_profiles_in, distribution_sources_out): + global code_state + print('=======================================') + print('START OF PHYSICS CODE') + print('=======================================') + print('Starting from: ', code_state) + + for i in range (1, 20): + # COMPUTATIONS + code_state = code_state + 1 + + print('Counting to: ', code_state) + + print('=======================================') + + # MANDATORY FLAG (UNIFORM TIME HERE) + distribution_sources_out.ids_properties.homogeneous_time = 1 + + distribution_sources_out.code.name = 'EXAMPLE: code_restart' + distribution_sources_out.code.version = '1.0' + + distribution_sources_out.time.resize(1) + distribution_sources_out.time[0] = code_state + + # FINAL DISPLAY + print('END OF PHYSICS CODE') + print('=======================================') + + + diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..f46fde8 --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,58 @@ +# pytest configuration for iWrap +# This file configures pytest for the iWrap test suite including MUSCLE3 tests + +[pytest] +# Test discovery patterns +python_files = test_*.py *_test.py pytests.py +python_classes = Test* +python_functions = test_* + +# Directories to search for tests +testpaths = tests + +# Markers for categorizing tests +markers = + unit: Unit tests (fast, isolated) + integration: Integration tests (slower, may require external dependencies) + muscle3: Tests requiring MUSCLE3 library + imas: Tests requiring IMAS library + slow: Slow running tests + core: Core iWrap functionality tests + generators: Generator tests + gui: GUI related tests + +# Minimum log level +log_level = INFO + +# Output options +addopts = + -v + --strict-markers + --tb=short + --durations=10 + +# Coverage options (if pytest-cov is installed) +# Uncomment to enable coverage reporting +# addopts = +# --cov=iwrap +# --cov-report=html +# --cov-report=term-missing + +# Ignore directories +norecursedirs = + .git + .tox + dist + build + *.egg + __pycache__ + docs + envs + +# Timeout for tests (in seconds) - requires pytest-timeout +# timeout = 300 + +# Filterwarnings +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning diff --git a/tests/run-all-tests-master.sh b/tests/run-all-tests-master.sh new file mode 100755 index 0000000..ff1d8a2 --- /dev/null +++ b/tests/run-all-tests-master.sh @@ -0,0 +1,394 @@ +#!/bin/bash +####################################################################################################################### +# Master Test Orchestrator for iWrap +# Runs both pytest-based tests (unit/integration) and IMAS integration tests +####################################################################################################################### + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Default configuration +RUN_UNIT_TESTS=1 +RUN_INTEGRATION_TESTS=1 +RUN_IMAS_TESTS=0 # IMAS tests are optional and slow +RUN_MUSCLE3_TESTS=1 # Run if MUSCLE3 is available +VERBOSE=0 +FAIL_FAST=0 + +# Results tracking +UNIT_TESTS_RESULT=0 +INTEGRATION_TESTS_RESULT=0 +IMAS_TESTS_RESULT=0 +MUSCLE3_TESTS_RESULT=0 + +####################################################################################################################### +# Help Function +####################################################################################################################### +print_help() { + cat << EOF +${BLUE}iWrap Master Test Suite${NC} + +Usage: $0 [OPTIONS] + +Options: + --unit-only Run only unit tests + --integration-only Run only integration tests + --imas Run IMAS integration tests (slow) + --muscle3-only Run only MUSCLE3 tests + --all Run all tests including IMAS + --skip-muscle3 Skip MUSCLE3 tests even if available + --fail-fast Stop on first failure + -v, --verbose Verbose output + -h, --help Show this help message + +Test Types: + Unit Tests: Fast, isolated component tests (pytest) + Integration Tests: Cross-component workflow tests (pytest) + MUSCLE3 Tests: Tests requiring MUSCLE3 library (pytest) + IMAS Tests: Full IMAS integration tests (legacy test suite) + +Examples: + $0 # Run unit + integration tests + $0 --all # Run all tests including IMAS + $0 --unit-only # Run only unit tests + $0 --muscle3-only # Run only MUSCLE3 tests + $0 --imas # Include IMAS tests + $0 --skip-muscle3 # Skip MUSCLE3 tests + +EOF +} + +####################################################################################################################### +# Parse Command Line Arguments +####################################################################################################################### +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --unit-only) + RUN_INTEGRATION_TESTS=0 + RUN_IMAS_TESTS=0 + shift + ;; + --integration-only) + RUN_UNIT_TESTS=0 + RUN_IMAS_TESTS=0 + shift + ;; + --muscle3-only) + RUN_UNIT_TESTS=0 + RUN_INTEGRATION_TESTS=0 + RUN_IMAS_TESTS=0 + shift + ;; + --imas) + RUN_IMAS_TESTS=1 + shift + ;; + --all) + RUN_UNIT_TESTS=1 + RUN_INTEGRATION_TESTS=1 + RUN_IMAS_TESTS=1 + shift + ;; + --skip-muscle3) + RUN_MUSCLE3_TESTS=0 + shift + ;; + --fail-fast) + FAIL_FAST=1 + shift + ;; + -v|--verbose) + VERBOSE=1 + shift + ;; + -h|--help) + print_help + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + print_help + exit 1 + ;; + esac + done +} + +####################################################################################################################### +# Environment Setup +####################################################################################################################### +setup_environment() { + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Environment Setup${NC}" + echo -e "${BLUE}========================================${NC}" + + # Set PYTHONPATH + export PYTHONPATH=$(pwd):${PYTHONPATH} + echo -e "PYTHONPATH: ${PYTHONPATH}" + + # Check pytest + if ! command -v pytest &> /dev/null; then + echo -e "${RED}Error: pytest is not installed${NC}" + echo "Install with: pip install pytest" + exit 1 + fi + echo -e "${GREEN}✓ pytest is available${NC}" + + # Check MUSCLE3 + if python3 -c "import muscle3" 2>/dev/null; then + echo -e "${GREEN}✓ MUSCLE3 is available${NC}" + MUSCLE3_AVAILABLE=1 + else + echo -e "${YELLOW}⚠ MUSCLE3 is not available (tests will be skipped)${NC}" + MUSCLE3_AVAILABLE=0 + if [ $RUN_MUSCLE3_TESTS -eq 1 ]; then + RUN_MUSCLE3_TESTS=0 + fi + fi + + echo "" +} + +####################################################################################################################### +# Run Unit Tests +####################################################################################################################### +run_unit_tests() { + if [ $RUN_UNIT_TESTS -eq 0 ]; then + return 0 + fi + + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Running Unit Tests${NC}" + echo -e "${BLUE}========================================${NC}" + + local pytest_args="-v -m unit" + if [ $VERBOSE -eq 1 ]; then + pytest_args="$pytest_args -vv" + fi + + if [ $FAIL_FAST -eq 1 ]; then + pytest_args="$pytest_args -x" + fi + + # Run unit tests, excluding MUSCLE3 if not available + if [ $MUSCLE3_AVAILABLE -eq 0 ]; then + pytest_args="$pytest_args and not muscle3" + fi + + if pytest tests/unit/ $pytest_args 2>&1 | tee /tmp/iwrap_unit_tests.log; then + echo -e "${GREEN}✓ Unit tests PASSED${NC}" + UNIT_TESTS_RESULT=0 + else + echo -e "${RED}✗ Unit tests FAILED${NC}" + UNIT_TESTS_RESULT=1 + if [ $FAIL_FAST -eq 1 ]; then + return 1 + fi + fi + echo "" + + return $UNIT_TESTS_RESULT +} + +####################################################################################################################### +# Run Integration Tests +####################################################################################################################### +run_integration_tests() { + if [ $RUN_INTEGRATION_TESTS -eq 0 ]; then + return 0 + fi + + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Running Integration Tests${NC}" + echo -e "${BLUE}========================================${NC}" + + local pytest_args="-v -m integration" + if [ $VERBOSE -eq 1 ]; then + pytest_args="$pytest_args -vv" + fi + + if [ $FAIL_FAST -eq 1 ]; then + pytest_args="$pytest_args -x" + fi + + # Run integration tests, excluding MUSCLE3 if not available + if [ $MUSCLE3_AVAILABLE -eq 0 ]; then + pytest_args="$pytest_args and not muscle3" + fi + + if pytest tests/integration/ $pytest_args 2>&1 | tee /tmp/iwrap_integration_tests.log; then + echo -e "${GREEN}✓ Integration tests PASSED${NC}" + INTEGRATION_TESTS_RESULT=0 + else + echo -e "${RED}✗ Integration tests FAILED${NC}" + INTEGRATION_TESTS_RESULT=1 + if [ $FAIL_FAST -eq 1 ]; then + return 1 + fi + fi + echo "" + + return $INTEGRATION_TESTS_RESULT +} + +####################################################################################################################### +# Run MUSCLE3 Tests +####################################################################################################################### +run_muscle3_tests() { + if [ $RUN_MUSCLE3_TESTS -eq 0 ] || [ $MUSCLE3_AVAILABLE -eq 0 ]; then + return 0 + fi + + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Running MUSCLE3 Tests${NC}" + echo -e "${BLUE}========================================${NC}" + + local pytest_args="-v -m muscle3" + if [ $VERBOSE -eq 1 ]; then + pytest_args="$pytest_args -vv" + fi + + if [ $FAIL_FAST -eq 1 ]; then + pytest_args="$pytest_args -x" + fi + + if pytest tests/ $pytest_args 2>&1 | tee /tmp/iwrap_muscle3_tests.log; then + echo -e "${GREEN}✓ MUSCLE3 tests PASSED${NC}" + MUSCLE3_TESTS_RESULT=0 + else + echo -e "${RED}✗ MUSCLE3 tests FAILED${NC}" + MUSCLE3_TESTS_RESULT=1 + if [ $FAIL_FAST -eq 1 ]; then + return 1 + fi + fi + echo "" + + return $MUSCLE3_TESTS_RESULT +} + +####################################################################################################################### +# Run IMAS Tests +####################################################################################################################### +run_imas_tests() { + if [ $RUN_IMAS_TESTS -eq 0 ]; then + return 0 + fi + + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Running IMAS Integration Tests${NC}" + echo -e "${BLUE}========================================${NC}" + echo -e "${YELLOW}Note: IMAS tests are slow and require IMAS environment${NC}" + echo "" + + if [ -f "./tests/run-tests.sh" ]; then + if ./tests/run-tests.sh 2>&1 | tee /tmp/iwrap_imas_tests.log; then + echo -e "${GREEN}✓ IMAS tests PASSED${NC}" + IMAS_TESTS_RESULT=0 + else + echo -e "${RED}✗ IMAS tests FAILED${NC}" + IMAS_TESTS_RESULT=1 + fi + else + echo -e "${YELLOW}⚠ IMAS test script not found${NC}" + IMAS_TESTS_RESULT=0 + fi + echo "" + + return $IMAS_TESTS_RESULT +} + +####################################################################################################################### +# Print Summary +####################################################################################################################### +print_summary() { + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Test Summary${NC}" + echo -e "${BLUE}========================================${NC}" + + local total_failed=0 + + if [ $RUN_UNIT_TESTS -eq 1 ]; then + if [ $UNIT_TESTS_RESULT -eq 0 ]; then + echo -e "${GREEN}✓ Unit Tests: PASSED${NC}" + else + echo -e "${RED}✗ Unit Tests: FAILED${NC}" + total_failed=$((total_failed + 1)) + fi + fi + + if [ $RUN_INTEGRATION_TESTS -eq 1 ]; then + if [ $INTEGRATION_TESTS_RESULT -eq 0 ]; then + echo -e "${GREEN}✓ Integration Tests: PASSED${NC}" + else + echo -e "${RED}✗ Integration Tests: FAILED${NC}" + total_failed=$((total_failed + 1)) + fi + fi + + if [ $MUSCLE3_AVAILABLE -eq 1 ] && [ $RUN_MUSCLE3_TESTS -eq 1 ]; then + if [ $MUSCLE3_TESTS_RESULT -eq 0 ]; then + echo -e "${GREEN}✓ MUSCLE3 Tests: PASSED${NC}" + else + echo -e "${RED}✗ MUSCLE3 Tests: FAILED${NC}" + total_failed=$((total_failed + 1)) + fi + elif [ $RUN_MUSCLE3_TESTS -eq 1 ]; then + echo -e "${YELLOW}⚠ MUSCLE3 Tests: SKIPPED (not installed)${NC}" + fi + + if [ $RUN_IMAS_TESTS -eq 1 ]; then + if [ $IMAS_TESTS_RESULT -eq 0 ]; then + echo -e "${GREEN}✓ IMAS Tests: PASSED${NC}" + else + echo -e "${RED}✗ IMAS Tests: FAILED${NC}" + total_failed=$((total_failed + 1)) + fi + fi + + echo -e "${BLUE}========================================${NC}" + + if [ $total_failed -eq 0 ]; then + echo -e "${GREEN}✅ All tests PASSED!${NC}" + return 0 + else + echo -e "${RED}❌ $total_failed test suite(s) FAILED${NC}" + return 1 + fi +} + +####################################################################################################################### +# Main Execution +####################################################################################################################### +main() { + echo -e "${CYAN}" + echo "╔════════════════════════════════════════╗" + echo "║ iWrap Master Test Suite ║" + echo "╚════════════════════════════════════════╝" + echo -e "${NC}" + echo "" + + parse_args "$@" + setup_environment + + # Run tests + run_unit_tests + run_integration_tests + run_muscle3_tests + run_imas_tests + + # Print summary and exit + print_summary + exit $? +} + +# Run main +main "$@" diff --git a/tests/run-all-tests.sh b/tests/run-all-tests.sh new file mode 100755 index 0000000..448e00f --- /dev/null +++ b/tests/run-all-tests.sh @@ -0,0 +1,185 @@ +#!/bin/bash +####################################################################################################################### +# iWrap Test Runner +# Runs all tests including optional MUSCLE3 tests +####################################################################################################################### + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test results +CORE_TESTS_PASSED=0 +MUSCLE3_TESTS_PASSED=0 +INTEGRATION_TESTS_PASSED=0 + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}iWrap Test Suite${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# Check if pytest is available +if ! command -v pytest &> /dev/null; then + echo -e "${RED}Error: pytest is not installed${NC}" + echo "Please install pytest: pip install pytest" + exit 1 +fi + +# Check Python path +if [ -z "$PYTHONPATH" ]; then + export PYTHONPATH=$(pwd) +else + export PYTHONPATH=$(pwd):$PYTHONPATH +fi + +echo -e "${BLUE}PYTHONPATH set to: $PYTHONPATH${NC}" +echo "" + +####################################################################################################################### +# Run Core Unit Tests +####################################################################################################################### +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Running Core Unit Tests${NC}" +echo -e "${BLUE}========================================${NC}" + +if pytest tests/unit/core -v -m "not muscle3" 2>&1 | tee /tmp/iwrap_core_tests.log; then + CORE_TESTS_PASSED=1 + echo -e "${GREEN}✓ Core unit tests passed${NC}" +else + echo -e "${RED}✗ Core unit tests failed${NC}" +fi +echo "" + +####################################################################################################################### +# Run Generator Unit Tests (excluding MUSCLE3) +####################################################################################################################### +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Running Generator Unit Tests (Core)${NC}" +echo -e "${BLUE}========================================${NC}" + +if [ -d "tests/unit/generators" ]; then + if pytest tests/unit/generators -v -m "not muscle3" 2>&1 | tee /tmp/iwrap_generator_tests.log; then + echo -e "${GREEN}✓ Core generator tests passed${NC}" + else + echo -e "${YELLOW}⚠ Some core generator tests failed${NC}" + fi +else + echo -e "${YELLOW}⚠ No generator tests directory found${NC}" +fi +echo "" + +####################################################################################################################### +# Check if MUSCLE3 is available +####################################################################################################################### +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Checking MUSCLE3 Availability${NC}" +echo -e "${BLUE}========================================${NC}" + +if python3 -c "import muscle3" 2>/dev/null; then + echo -e "${GREEN}✓ MUSCLE3 library is installed${NC}" + MUSCLE3_AVAILABLE=1 +else + echo -e "${YELLOW}⚠ MUSCLE3 library is NOT installed${NC}" + echo -e "${YELLOW} MUSCLE3 tests will be skipped${NC}" + echo -e "${YELLOW} To run MUSCLE3 tests, install with: pip install iwrap[muscle3]${NC}" + MUSCLE3_AVAILABLE=0 +fi +echo "" + +####################################################################################################################### +# Run MUSCLE3 Tests (if available) +####################################################################################################################### +if [ $MUSCLE3_AVAILABLE -eq 1 ]; then + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE}Running MUSCLE3 Unit Tests${NC}" + echo -e "${BLUE}========================================${NC}" + + if pytest tests/unit/generators -v -m "muscle3" 2>&1 | tee /tmp/iwrap_muscle3_tests.log; then + MUSCLE3_TESTS_PASSED=1 + echo -e "${GREEN}✓ MUSCLE3 unit tests passed${NC}" + else + echo -e "${RED}✗ MUSCLE3 unit tests failed${NC}" + fi + echo "" +fi + +####################################################################################################################### +# Run Integration Tests +####################################################################################################################### +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Running Integration Tests${NC}" +echo -e "${BLUE}========================================${NC}" + +if [ -d "tests/integration" ]; then + if [ $MUSCLE3_AVAILABLE -eq 1 ]; then + # Run all integration tests + if pytest tests/integration -v 2>&1 | tee /tmp/iwrap_integration_tests.log; then + INTEGRATION_TESTS_PASSED=1 + echo -e "${GREEN}✓ Integration tests passed${NC}" + else + echo -e "${RED}✗ Integration tests failed${NC}" + fi + else + # Run only non-MUSCLE3 integration tests + if pytest tests/integration -v -m "not muscle3" 2>&1 | tee /tmp/iwrap_integration_tests.log; then + INTEGRATION_TESTS_PASSED=1 + echo -e "${GREEN}✓ Core integration tests passed${NC}" + else + echo -e "${RED}✗ Core integration tests failed${NC}" + fi + fi +else + echo -e "${YELLOW}⚠ No integration tests directory found${NC}" +fi +echo "" + +####################################################################################################################### +# Test Summary +####################################################################################################################### +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}Test Summary${NC}" +echo -e "${BLUE}========================================${NC}" + +if [ $CORE_TESTS_PASSED -eq 1 ]; then + echo -e "${GREEN}✓ Core Tests: PASSED${NC}" +else + echo -e "${RED}✗ Core Tests: FAILED${NC}" +fi + +if [ $MUSCLE3_AVAILABLE -eq 1 ]; then + if [ $MUSCLE3_TESTS_PASSED -eq 1 ]; then + echo -e "${GREEN}✓ MUSCLE3 Tests: PASSED${NC}" + else + echo -e "${RED}✗ MUSCLE3 Tests: FAILED${NC}" + fi +else + echo -e "${YELLOW}⚠ MUSCLE3 Tests: SKIPPED (MUSCLE3 not installed)${NC}" +fi + +if [ $INTEGRATION_TESTS_PASSED -eq 1 ]; then + echo -e "${GREEN}✓ Integration Tests: PASSED${NC}" +else + echo -e "${RED}✗ Integration Tests: FAILED${NC}" +fi + +echo "" +echo -e "${BLUE}========================================${NC}" + +# Exit with error if any required tests failed +if [ $CORE_TESTS_PASSED -eq 0 ]; then + echo -e "${RED}Test suite FAILED - Core tests failed${NC}" + exit 1 +fi + +if [ $MUSCLE3_AVAILABLE -eq 1 ] && [ $MUSCLE3_TESTS_PASSED -eq 0 ]; then + echo -e "${RED}Test suite FAILED - MUSCLE3 tests failed${NC}" + exit 1 +fi + +echo -e "${GREEN}✅ All tests PASSED!${NC}" +exit 0 diff --git a/tests/run-muscle3-tests.sh b/tests/run-muscle3-tests.sh new file mode 100755 index 0000000..bb2e1ea --- /dev/null +++ b/tests/run-muscle3-tests.sh @@ -0,0 +1,46 @@ +#!/bin/bash +####################################################################################################################### +# MUSCLE3 Test Runner +# Runs only MUSCLE3-specific tests +####################################################################################################################### + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}MUSCLE3 Test Suite${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# Check if MUSCLE3 is installed +if ! python3 -c "import muscle3" 2>/dev/null; then + echo -e "${RED}Error: MUSCLE3 is not installed${NC}" + echo -e "${YELLOW}Install with: pip install iwrap[muscle3]${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ MUSCLE3 is installed${NC}" +echo "" + +# Set PYTHONPATH +export PYTHONPATH=$(pwd):$PYTHONPATH + +# Run MUSCLE3 tests +echo -e "${BLUE}Running MUSCLE3 tests...${NC}" +pytest tests/ -v -m "muscle3" --tb=short + +if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}✅ All MUSCLE3 tests PASSED!${NC}" + exit 0 +else + echo "" + echo -e "${RED}❌ MUSCLE3 tests FAILED${NC}" + exit 1 +fi diff --git a/tests/unit/generators/test_muscle3_cpp.py b/tests/unit/generators/test_muscle3_cpp.py new file mode 100644 index 0000000..427dec3 --- /dev/null +++ b/tests/unit/generators/test_muscle3_cpp.py @@ -0,0 +1,61 @@ +""" +Unit tests for MUSCLE3 C++ actor generator +""" +import pytest +import sys +from pathlib import Path + +# Check if MUSCLE3 is available +muscle3_available = pytest.importorskip("muscle3", reason="MUSCLE3 not installed") + + +class TestMuscle3CppGenerator: + """Test suite for MUSCLE3 C++ actor generator""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup test environment""" + iwrap_root = Path(__file__).parent.parent.parent + if str(iwrap_root) not in sys.path: + sys.path.insert(0, str(iwrap_root)) + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_import(self): + """Test that MUSCLE3 C++ generator can be imported""" + from iwrap.generators.actor_generators.muscle3_cpp.m3_cpp_actor import CppActorGenerator + assert CppActorGenerator is not None + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_instantiation(self): + """Test MUSCLE3 C++ generator instantiation""" + from iwrap.generators.actor_generators.muscle3_cpp.m3_cpp_actor import CppActorGenerator + generator = CppActorGenerator() + assert generator is not None + assert generator.type == 'MUSCLE3-CPP' + assert generator.name == 'MUSCLE3 (C++)' + assert generator.actor_language == 'python' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_api_version(self): + """Test that generator has correct API version""" + from iwrap.generators.actor_generators.muscle3_cpp.m3_cpp_actor import CppActorGenerator + generator = CppActorGenerator() + assert generator.COMPLIANT_API == '2.1' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_properties(self): + """Test generator properties""" + from iwrap.generators.actor_generators.muscle3_cpp.m3_cpp_actor import CppActorGenerator + generator = CppActorGenerator() + + assert 'cpp' in generator.code_languages + assert 'legacy' in generator.actor_data_types + assert 'legacy' in generator.code_data_types + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/tests/unit/generators/test_muscle3_fortran.py b/tests/unit/generators/test_muscle3_fortran.py new file mode 100644 index 0000000..f18de82 --- /dev/null +++ b/tests/unit/generators/test_muscle3_fortran.py @@ -0,0 +1,61 @@ +""" +Unit tests for MUSCLE3 Fortran actor generator +""" +import pytest +import sys +from pathlib import Path + +# Check if MUSCLE3 is available +muscle3_available = pytest.importorskip("muscle3", reason="MUSCLE3 not installed") + + +class TestMuscle3FortranGenerator: + """Test suite for MUSCLE3 Fortran actor generator""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup test environment""" + iwrap_root = Path(__file__).parent.parent.parent + if str(iwrap_root) not in sys.path: + sys.path.insert(0, str(iwrap_root)) + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_import(self): + """Test that MUSCLE3 Fortran generator can be imported""" + from iwrap.generators.actor_generators.muscle3_fortran.m3_fortran_actor import FortranPAFActorGenerator + assert FortranPAFActorGenerator is not None + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_instantiation(self): + """Test MUSCLE3 Fortran generator instantiation""" + from iwrap.generators.actor_generators.muscle3_fortran.m3_fortran_actor import FortranPAFActorGenerator + generator = FortranPAFActorGenerator() + assert generator is not None + assert generator.type == 'MUSCLE3-Fortran' + assert generator.name == 'MUSCLE3 (Fortran)' + assert generator.actor_language == 'python' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_api_version(self): + """Test that generator has correct API version""" + from iwrap.generators.actor_generators.muscle3_fortran.m3_fortran_actor import FortranPAFActorGenerator + generator = FortranPAFActorGenerator() + assert generator.COMPLIANT_API == '2.1' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_properties(self): + """Test generator properties""" + from iwrap.generators.actor_generators.muscle3_fortran.m3_fortran_actor import FortranPAFActorGenerator + generator = FortranPAFActorGenerator() + + assert 'fortran' in generator.code_languages + assert 'legacy' in generator.actor_data_types + assert 'legacy' in generator.code_data_types + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/tests/unit/generators/test_muscle3_python.py b/tests/unit/generators/test_muscle3_python.py new file mode 100644 index 0000000..bdf571d --- /dev/null +++ b/tests/unit/generators/test_muscle3_python.py @@ -0,0 +1,137 @@ +""" +Unit tests for MUSCLE3 Python actor generator +""" +import pytest +import sys +from pathlib import Path + +# Check if MUSCLE3 is available +muscle3_available = pytest.importorskip("muscle3", reason="MUSCLE3 not installed") + + +class TestMuscle3PythonGenerator: + """Test suite for MUSCLE3 Python actor generator""" + + @pytest.fixture(autouse=True) + def setup(self): + """Setup test environment""" + # Add iWrap to path if needed + iwrap_root = Path(__file__).parent.parent.parent + if str(iwrap_root) not in sys.path: + sys.path.insert(0, str(iwrap_root)) + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_import(self): + """Test that MUSCLE3 Python generator can be imported""" + from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator + assert PythonActorGenerator is not None + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_instantiation(self): + """Test MUSCLE3 Python generator instantiation""" + from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator + generator = PythonActorGenerator() + assert generator is not None + assert generator.type == 'MUSCLE3-Python' + assert generator.name == 'MUSCLE3 (Python)' + assert generator.actor_language == 'python' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_api_version(self): + """Test that generator has correct API version""" + from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator + generator = PythonActorGenerator() + assert generator.COMPLIANT_API == '2.1' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_properties(self): + """Test generator properties""" + from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator + generator = PythonActorGenerator() + + assert 'python' in generator.code_languages + assert 'legacy' in generator.actor_data_types + assert 'legacy' in generator.code_data_types + assert generator.description == 'Wrapping Python code into MUSCLE3 micro model' + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_generator_discovery(self): + """Test that MUSCLE3 generator is discovered by engine""" + from iwrap.generation_engine.engine import Engine + + engine = Engine() + engine.startup() + + generators = engine.registered_generators + generator_types = [g.type for g in generators] + + assert 'MUSCLE3-Python' in generator_types + assert 'MUSCLE3-CPP' in generator_types + assert 'MUSCLE3-Fortran' in generator_types + + +class TestMuscle3CommonUtils: + """Test suite for MUSCLE3 common utilities""" + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_m3_utils_import(self): + """Test that m3_utils can be imported""" + from iwrap.generators.actor_generators.muscle3_common import m3_utils + assert m3_utils is not None + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_template_filter_func(self): + """Test template filter function""" + from iwrap.generators.actor_generators.muscle3_common.m3_utils import template_filter_func + + assert template_filter_func(['normal_file.py']) == True + assert template_filter_func(['__pycache__/file.pyc']) == False + assert template_filter_func(['macros/macro.j2']) == False + + @pytest.mark.muscle3 + @pytest.mark.unit + def test_validate_function(self): + """Test validation function for MUSCLE3 constraints""" + from iwrap.generators.actor_generators.muscle3_common.m3_utils import validate + + # Valid case: no IDS arguments in init/finalize + valid_settings = { + 'code_description': { + 'implementation': { + 'subroutines': { + 'init': {'arguments': []}, + 'finalize': {'arguments': []} + } + } + } + } + + # Should not raise + validate(valid_settings) + + # Invalid case: IDS arguments in init + invalid_settings = { + 'code_description': { + 'implementation': { + 'subroutines': { + 'init': {'arguments': [{'name': 'equilibrium', 'type': 'input'}]}, + 'finalize': {'arguments': []} + } + } + } + } + + # Should raise ValueError + with pytest.raises(ValueError, match='MUSCLE3 actor generator cannot handle INIT/FINALIZE'): + validate(invalid_settings) + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/versioneer.py b/versioneer.py deleted file mode 100644 index 84f29c6..0000000 --- a/versioneer.py +++ /dev/null @@ -1,2220 +0,0 @@ - -# Version: 0.28 - -"""The Versioneer - like a rocketeer, but for versions. - -The Versioneer -============== - -* like a rocketeer, but for versions! -* https://github.com/python-versioneer/python-versioneer -* Brian Warner -* License: Public Domain (Unlicense) -* Compatible with: Python 3.7, 3.8, 3.9, 3.10 and pypy3 -* [![Latest Version][pypi-image]][pypi-url] -* [![Build Status][travis-image]][travis-url] - -This is a tool for managing a recorded version number in setuptools-based -python projects. The goal is to remove the tedious and error-prone "update -the embedded version string" step from your release process. Making a new -release should be as easy as recording a new tag in your version-control -system, and maybe making new tarballs. - - -## Quick Install - -Versioneer provides two installation modes. The "classic" vendored mode installs -a copy of versioneer into your repository. The experimental build-time dependency mode -is intended to allow you to skip this step and simplify the process of upgrading. - -### Vendored mode - -* `pip install versioneer` to somewhere in your $PATH - * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is - available, so you can also use `conda install -c conda-forge versioneer` -* add a `[tool.versioneer]` section to your `pyproject.toml` or a - `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) - * Note that you will need to add `tomli; python_version < "3.11"` to your - build-time dependencies if you use `pyproject.toml` -* run `versioneer install --vendor` in your source tree, commit the results -* verify version information with `python setup.py version` - -### Build-time dependency mode - -* `pip install versioneer` to somewhere in your $PATH - * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is - available, so you can also use `conda install -c conda-forge versioneer` -* add a `[tool.versioneer]` section to your `pyproject.toml` or a - `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) -* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) - to the `requires` key of the `build-system` table in `pyproject.toml`: - ```toml - [build-system] - requires = ["setuptools", "versioneer[toml]"] - build-backend = "setuptools.build_meta" - ``` -* run `versioneer install --no-vendor` in your source tree, commit the results -* verify version information with `python setup.py version` - -## Version Identifiers - -Source trees come from a variety of places: - -* a version-control system checkout (mostly used by developers) -* a nightly tarball, produced by build automation -* a snapshot tarball, produced by a web-based VCS browser, like github's - "tarball from tag" feature -* a release tarball, produced by "setup.py sdist", distributed through PyPI - -Within each source tree, the version identifier (either a string or a number, -this tool is format-agnostic) can come from a variety of places: - -* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows - about recent "tags" and an absolute revision-id -* the name of the directory into which the tarball was unpacked -* an expanded VCS keyword ($Id$, etc) -* a `_version.py` created by some earlier build step - -For released software, the version identifier is closely related to a VCS -tag. Some projects use tag names that include more than just the version -string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool -needs to strip the tag prefix to extract the version identifier. For -unreleased software (between tags), the version identifier should provide -enough information to help developers recreate the same tree, while also -giving them an idea of roughly how old the tree is (after version 1.2, before -version 1.3). Many VCS systems can report a description that captures this, -for example `git describe --tags --dirty --always` reports things like -"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the -0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes). - -The version identifier is used for multiple purposes: - -* to allow the module to self-identify its version: `myproject.__version__` -* to choose a name and prefix for a 'setup.py sdist' tarball - -## Theory of Operation - -Versioneer works by adding a special `_version.py` file into your source -tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. - -`_version.py` also contains `$Revision$` markers, and the installation -process marks `_version.py` to have this marker rewritten with a tag name -during the `git archive` command. As a result, generated tarballs will -contain enough information to get the proper version. - -To allow `setup.py` to compute a version too, a `versioneer.py` is added to -the top level of your source tree, next to `setup.py` and the `setup.cfg` -that configures it. This overrides several distutils/setuptools commands to -compute the version when invoked, and changes `setup.py build` and `setup.py -sdist` to replace `_version.py` with a small static file that contains just -the generated version data. - -## Installation - -See [INSTALL.md](./INSTALL.md) for detailed installation instructions. - -## Version-String Flavors - -Code which uses Versioneer can learn about its version string at runtime by -importing `_version` from your main `__init__.py` file and running the -`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can -import the top-level `versioneer.py` and run `get_versions()`. - -Both functions return a dictionary with different flavors of version -information: - -* `['version']`: A condensed version string, rendered using the selected - style. This is the most commonly used value for the project's version - string. The default "pep440" style yields strings like `0.11`, - `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section - below for alternative styles. - -* `['full-revisionid']`: detailed revision identifier. For Git, this is the - full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". - -* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the - commit date in ISO 8601 format. This will be None if the date is not - available. - -* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that - this is only accurate if run in a VCS checkout, otherwise it is likely to - be False or None - -* `['error']`: if the version string could not be computed, this will be set - to a string describing the problem, otherwise it will be None. It may be - useful to throw an exception in setup.py if this is set, to avoid e.g. - creating tarballs with a version string of "unknown". - -Some variants are more useful than others. Including `full-revisionid` in a -bug report should allow developers to reconstruct the exact code being tested -(or indicate the presence of local changes that should be shared with the -developers). `version` is suitable for display in an "about" box or a CLI -`--version` output: it can be easily compared against release notes and lists -of bugs fixed in various releases. - -The installer adds the following text to your `__init__.py` to place a basic -version in `YOURPROJECT.__version__`: - - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions - -## Styles - -The setup.cfg `style=` configuration controls how the VCS information is -rendered into a version string. - -The default style, "pep440", produces a PEP440-compliant string, equal to the -un-prefixed tag name for actual releases, and containing an additional "local -version" section with more detail for in-between builds. For Git, this is -TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags ---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the -tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and -that this commit is two revisions ("+2") beyond the "0.11" tag. For released -software (exactly equal to a known tag), the identifier will only contain the -stripped tag, e.g. "0.11". - -Other styles are available. See [details.md](details.md) in the Versioneer -source tree for descriptions. - -## Debugging - -Versioneer tries to avoid fatal errors: if something goes wrong, it will tend -to return a version of "0+unknown". To investigate the problem, run `setup.py -version`, which will run the version-lookup code in a verbose mode, and will -display the full contents of `get_versions()` (including the `error` string, -which may help identify what went wrong). - -## Known Limitations - -Some situations are known to cause problems for Versioneer. This details the -most significant ones. More can be found on Github -[issues page](https://github.com/python-versioneer/python-versioneer/issues). - -### Subprojects - -Versioneer has limited support for source trees in which `setup.py` is not in -the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are -two common reasons why `setup.py` might not be in the root: - -* Source trees which contain multiple subprojects, such as - [Buildbot](https://github.com/buildbot/buildbot), which contains both - "master" and "slave" subprojects, each with their own `setup.py`, - `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI - distributions (and upload multiple independently-installable tarballs). -* Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other languages) in subdirectories. - -Versioneer will look for `.git` in parent directories, and most operations -should get the right version string. However `pip` and `setuptools` have bugs -and implementation details which frequently cause `pip install .` from a -subproject directory to fail to find a correct version string (so it usually -defaults to `0+unknown`). - -`pip install --editable .` should work correctly. `setup.py install` might -work too. - -Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in -some later version. - -[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking -this issue. The discussion in -[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the -issue from the Versioneer side in more detail. -[pip PR#3176](https://github.com/pypa/pip/pull/3176) and -[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve -pip to let Versioneer work correctly. - -Versioneer-0.16 and earlier only looked for a `.git` directory next to the -`setup.cfg`, so subprojects were completely unsupported with those releases. - -### Editable installs with setuptools <= 18.5 - -`setup.py develop` and `pip install --editable .` allow you to install a -project into a virtualenv once, then continue editing the source code (and -test) without re-installing after every change. - -"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a -convenient way to specify executable scripts that should be installed along -with the python package. - -These both work as expected when using modern setuptools. When using -setuptools-18.5 or earlier, however, certain operations will cause -`pkg_resources.DistributionNotFound` errors when running the entrypoint -script, which must be resolved by re-installing the package. This happens -when the install happens with one version, then the egg_info data is -regenerated while a different version is checked out. Many setup.py commands -cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into -a different virtualenv), so this can be surprising. - -[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes -this one, but upgrading to a newer version of setuptools should probably -resolve it. - - -## Updating Versioneer - -To upgrade your project to a new release of Versioneer, do the following: - -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg` and `pyproject.toml`, if necessary, - to include any new configuration settings indicated by the release notes. - See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install --[no-]vendor` in your source tree, to replace - `SRC/_version.py` -* commit any changed files - -## Future Directions - -This tool is designed to make it easily extended to other version-control -systems: all VCS-specific components are in separate directories like -src/git/ . The top-level `versioneer.py` script is assembled from these -components by running make-versioneer.py . In the future, make-versioneer.py -will take a VCS name as an argument, and will construct a version of -`versioneer.py` that is specific to the given VCS. It might also take the -configuration arguments that are currently provided manually during -installation by editing setup.py . Alternatively, it might go the other -direction and include code from all supported VCS systems, reducing the -number of intermediate scripts. - -## Similar projects - -* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time - dependency -* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of - versioneer -* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools - plugin - -## License - -To make Versioneer easier to embed, all its code is dedicated to the public -domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the "Unlicense", as described in -https://unlicense.org/. - -[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg -[pypi-url]: https://pypi.python.org/pypi/versioneer/ -[travis-image]: -https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg -[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer - -""" -# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring -# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements -# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error -# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with -# pylint:disable=attribute-defined-outside-init,too-many-arguments - -import configparser -import errno -import json -import os -import re -import subprocess -import sys -from pathlib import Path -from typing import Callable, Dict -import functools - -have_tomllib = True -if sys.version_info >= (3, 11): - import tomllib -else: - try: - import tomli as tomllib - except ImportError: - have_tomllib = False - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_root(): - """Get the project root directory. - - We require that all commands are run from the project root, i.e. the - directory that contains setup.py, setup.cfg, and versioneer.py . - """ - root = os.path.realpath(os.path.abspath(os.getcwd())) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - # allow 'python path/to/setup.py COMMAND' - root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") - raise VersioneerBadRootError(err) - try: - # Certain runtime workflows (setup.py install/develop in a setuptools - # tree) execute all dependencies in a single python process, so - # "versioneer" may be imported multiple times, and python's shared - # module-import table will cache the first one. So we can't use - # os.path.dirname(__file__), as that will find whichever - # versioneer.py was first imported, even in later projects. - my_path = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(my_path)[0]) - vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(my_path), versioneer_py)) - except NameError: - pass - return root - - -def get_config_from_root(root): - """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise OSError (if setup.cfg is missing), or - # configparser.NoSectionError (if it lacks a [versioneer] section), or - # configparser.NoOptionError (if it lacks "VCS="). See the docstring at - # the top of versioneer.py for instructions on writing your setup.cfg . - root = Path(root) - pyproject_toml = root / "pyproject.toml" - setup_cfg = root / "setup.cfg" - section = None - if pyproject_toml.exists() and have_tomllib: - try: - with open(pyproject_toml, 'rb') as fobj: - pp = tomllib.load(fobj) - section = pp['tool']['versioneer'] - except (tomllib.TOMLDecodeError, KeyError): - pass - if not section: - parser = configparser.ConfigParser() - with open(setup_cfg) as cfg_file: - parser.read_file(cfg_file) - parser.get("versioneer", "VCS") # raise error if missing - - section = parser["versioneer"] - - cfg = VersioneerConfig() - cfg.VCS = section['VCS'] - cfg.style = section.get("style", "") - cfg.versionfile_source = section.get("versionfile_source") - cfg.versionfile_build = section.get("versionfile_build") - cfg.tag_prefix = section.get("tag_prefix") - if cfg.tag_prefix in ("''", '""', None): - cfg.tag_prefix = "" - cfg.parentdir_prefix = section.get("parentdir_prefix") - cfg.verbose = section.get("verbose") - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -# these dictionaries contain VCS-specific tools -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs, method): # decorator - """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - HANDLERS.setdefault(vcs, {})[method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - - popen_kwargs = {} - if sys.platform == "win32": - # This hides the console window if pythonw.exe is used - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - popen_kwargs["startupinfo"] = startupinfo - - for command in commands: - try: - dispcmd = str([command] + args) - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) - break - except OSError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, process.returncode - return stdout, process.returncode - - -LONG_VERSION_PY['git'] = r''' -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. -# Generated by versioneer-0.28 -# https://github.com/python-versioneer/python-versioneer - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys -from typing import Callable, Dict -import functools - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" - git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "%(STYLE)s" - cfg.tag_prefix = "%(TAG_PREFIX)s" - cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" - cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs, method): # decorator - """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - - popen_kwargs = {} - if sys.platform == "win32": - # This hides the console window if pythonw.exe is used - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - popen_kwargs["startupinfo"] = startupinfo - - for command in commands: - try: - dispcmd = str([command] + args) - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) - break - except OSError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %%s" %% dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %%s" %% (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %%s (error)" %% dispcmd) - print("stdout was %%s" %% stdout) - return None, process.returncode - return stdout, process.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %%s but none started with prefix %%s" %% - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %%d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} - if verbose: - print("discarding '%%s', no digits" %% ",".join(refs - tags)) - if verbose: - print("likely tags: %%s" %% ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r'\d', r): - continue - if verbose: - print("picking %%s" %% r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - # GIT_DIR can interfere with correct operation of Versioneer. - # It may be intended to be passed to the Versioneer-versioned project, - # but that should not change where we get our version from. - env = os.environ.copy() - env.pop("GIT_DIR", None) - runner = functools.partial(runner, env=env) - - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) - if rc != 0: - if verbose: - print("Directory %%s not under git control" %% root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%%s'" - %% describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%%s' doesn't start with prefix '%%s'" - print(fmt %% (full_tag, tag_prefix)) - pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" - %% (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) - pieces["distance"] = len(out.split()) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces): - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver): - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces): - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post(pieces["closest-tag"]) - rendered = tag_version - if post_version is not None: - rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) - else: - rendered += ".post0.dev%%d" %% (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%%d" %% pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%%s'" %% style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} -''' - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - mo_tag_in_git_refnames = None - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - mo_tag_in_git_refnames = re.search(r'(\d+\.)(\d+\.)(\d+)', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_describe_output ="): - if not mo_tag_in_git_refnames: - # print("Tag version not found, Getting from git describe method") - mo_tag_in_git_describe = re.search(r'(\d+\.)(\d+\.)(\d+)', line) - if mo_tag_in_git_describe: - # print('Version found :', mo_tag_in_git_describe.group(0)) - words = line.split("=") - version_string = "" - if len(words)==2: - version_string = words[1].replace('"','').strip() - # print('Version found :', version_string) - mo_describe = '(, tag: '+ version_string +',,'+ keywords["refnames"] + ')' - keywords["refnames"] = mo_describe - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r'\d', r): - continue - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - # GIT_DIR can interfere with correct operation of Versioneer. - # It may be intended to be passed to the Versioneer-versioned project, - # but that should not change where we get our version from. - env = os.environ.copy() - env.pop("GIT_DIR", None) - runner = functools.partial(runner, env=env) - - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) - pieces["distance"] = len(out.split()) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def do_vcs_install(versionfile_source, ipy): - """Git-specific installation logic for Versioneer. - - For Git, this means creating/changing .gitattributes to mark _version.py - for export-subst keyword substitution. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - files = [versionfile_source] - if ipy: - files.append(ipy) - if "VERSIONEER_PEP518" not in globals(): - try: - my_path = __file__ - if my_path.endswith((".pyc", ".pyo")): - my_path = os.path.splitext(my_path)[0] + ".py" - versioneer_file = os.path.relpath(my_path) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) - present = False - try: - with open(".gitattributes", "r") as fobj: - for line in fobj: - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - break - except OSError: - pass - if not present: - with open(".gitattributes", "a+") as fobj: - fobj.write(f"{versionfile_source} export-subst\n") - files.append(".gitattributes") - run_command(GITS, ["add", "--"] + files) - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.28) from -# revision-control system data, or from the parent directory name of an -# unpacked source archive. Distribution tarballs contain a pre-generated copy -# of this file. - -import json - -version_json = ''' -%s -''' # END VERSION_JSON - - -def get_versions(): - return json.loads(version_json) -""" - - -def versions_from_file(filename): - """Try to determine the version from _version.py if present.""" - try: - with open(filename) as f: - contents = f.read() - except OSError: - raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - raise NotThisMethod("no version_json in _version.py") - return json.loads(mo.group(1)) - - -def write_to_version_file(filename, versions): - """Write the given version number to the given _version.py file.""" - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) - with open(filename, "w") as f: - f.write(SHORT_VERSION_PY % contents) - - print("set %s to '%s'" % (filename, versions["version"])) - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces): - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver): - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces): - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post(pieces["closest-tag"]) - rendered = tag_version - if post_version is not None: - rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) - else: - rendered += ".post0.dev%d" % (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -class VersioneerBadRootError(Exception): - """The project root directory is unknown or missing key files.""" - - -def get_versions(verbose=False): - """Get the project version from whatever source is available. - - Returns dict with two keys: 'version' and 'full'. - """ - if "versioneer" in sys.modules: - # see the discussion in cmdclass.py:get_cmdclass() - del sys.modules["versioneer"] - - root = get_root() - cfg = get_config_from_root(root) - - assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" - handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" - assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" - - versionfile_abs = os.path.join(root, cfg.versionfile_source) - - # extract version from first of: _version.py, VCS command (e.g. 'git - # describe'), parentdir. This is meant to work for developers using a - # source checkout, for users of a tarball created by 'setup.py sdist', - # and for users of a tarball/zipball created by 'git archive' or github's - # download-from-tag feature or the equivalent in other VCSes. - - get_keywords_f = handlers.get("get_keywords") - from_keywords_f = handlers.get("keywords") - if get_keywords_f and from_keywords_f: - try: - keywords = get_keywords_f(versionfile_abs) - ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) - if verbose: - print("got version from expanded keyword %s" % ver) - return ver - except NotThisMethod: - pass - - try: - ver = versions_from_file(versionfile_abs) - if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) - return ver - except NotThisMethod: - pass - - from_vcs_f = handlers.get("pieces_from_vcs") - if from_vcs_f: - try: - pieces = from_vcs_f(cfg.tag_prefix, root, verbose) - ver = render(pieces, cfg.style) - if verbose: - print("got version from VCS %s" % ver) - return ver - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - if verbose: - print("got version from parentdir %s" % ver) - return ver - except NotThisMethod: - pass - - if verbose: - print("unable to compute version") - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} - - -def get_version(): - """Get the short version string for this project.""" - return get_versions()["version"] - - -def get_cmdclass(cmdclass=None): - """Get the custom setuptools subclasses used by Versioneer. - - If the package uses a different cmdclass (e.g. one from numpy), it - should be provide as an argument. - """ - if "versioneer" in sys.modules: - del sys.modules["versioneer"] - # this fixes the "python setup.py develop" case (also 'install' and - # 'easy_install .'), in which subdependencies of the main project are - # built (using setup.py bdist_egg) in the same python process. Assume - # a main project A and a dependency B, which use different versions - # of Versioneer. A's setup.py imports A's Versioneer, leaving it in - # sys.modules by the time B's setup.py is executed, causing B to run - # with the wrong versioneer. Setuptools wraps the sub-dep builds in a - # sandbox that restores sys.modules to it's pre-build state, so the - # parent is protected against the child's "import versioneer". By - # removing ourselves from sys.modules here, before the child build - # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/python-versioneer/python-versioneer/issues/52 - - cmds = {} if cmdclass is None else cmdclass.copy() - - # we add "version" to setuptools - from setuptools import Command - - class cmd_version(Command): - description = "report generated version string" - user_options = [] - boolean_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - print(" date: %s" % vers.get("date")) - if vers["error"]: - print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - - # we override "build_py" in setuptools - # - # most invocation pathways end up running build_py: - # distutils/build -> build_py - # distutils/install -> distutils/build ->.. - # setuptools/bdist_wheel -> distutils/install ->.. - # setuptools/bdist_egg -> distutils/install_lib -> build_py - # setuptools/install -> bdist_egg ->.. - # setuptools/develop -> ? - # pip install: - # copies source tree to a tempdir before running egg_info/etc - # if .git isn't copied too, 'git describe' will fail - # then does setup.py bdist_wheel, or sometimes setup.py install - # setup.py egg_info -> ? - - # pip install -e . and setuptool/editable_wheel will invoke build_py - # but the build_py command is not expected to copy any files. - - # we override different "build_py" commands for both environments - if 'build_py' in cmds: - _build_py = cmds['build_py'] - else: - from setuptools.command.build_py import build_py as _build_py - - class cmd_build_py(_build_py): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_py.run(self) - if getattr(self, "editable_mode", False): - # During editable installs `.py` and data files are - # not copied to build_lib - return - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - - if 'build_ext' in cmds: - _build_ext = cmds['build_ext'] - else: - from setuptools.command.build_ext import build_ext as _build_ext - - class cmd_build_ext(_build_ext): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_ext.run(self) - if self.inplace: - # build_ext --inplace will only build extensions in - # build/lib<..> dir with no _version.py to write to. - # As in place builds will already have a _version.py - # in the module dir, we do not need to write one. - return - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if not cfg.versionfile_build: - return - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - if not os.path.exists(target_versionfile): - print(f"Warning: {target_versionfile} does not exist, skipping " - "version update. This can happen if you are running build_ext " - "without first running build_py.") - return - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_ext"] = cmd_build_ext - - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe - # nczeczulin reports that py2exe won't like the pep440-style string - # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. - # setup(console=[{ - # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION - # "product_version": versioneer.get_version(), - # ... - - class cmd_build_exe(_build_exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _build_exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["build_exe"] = cmd_build_exe - del cmds["build_py"] - - if 'py2exe' in sys.modules: # py2exe enabled? - try: - from py2exe.setuptools_buildexe import py2exe as _py2exe - except ImportError: - from py2exe.distutils_buildexe import py2exe as _py2exe - - class cmd_py2exe(_py2exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _py2exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["py2exe"] = cmd_py2exe - - # sdist farms its file list building out to egg_info - if 'egg_info' in cmds: - _egg_info = cmds['egg_info'] - else: - from setuptools.command.egg_info import egg_info as _egg_info - - class cmd_egg_info(_egg_info): - def find_sources(self): - # egg_info.find_sources builds the manifest list and writes it - # in one shot - super().find_sources() - - # Modify the filelist and normalize it - root = get_root() - cfg = get_config_from_root(root) - self.filelist.append('versioneer.py') - if cfg.versionfile_source: - # There are rare cases where versionfile_source might not be - # included by default, so we must be explicit - self.filelist.append(cfg.versionfile_source) - self.filelist.sort() - self.filelist.remove_duplicates() - - # The write method is hidden in the manifest_maker instance that - # generated the filelist and was thrown away - # We will instead replicate their final normalization (to unicode, - # and POSIX-style paths) - from setuptools import unicode_utils - normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') - for f in self.filelist.files] - - manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') - with open(manifest_filename, 'w') as fobj: - fobj.write('\n'.join(normalized)) - - cmds['egg_info'] = cmd_egg_info - - # we override different "sdist" commands for both environments - if 'sdist' in cmds: - _sdist = cmds['sdist'] - else: - from setuptools.command.sdist import sdist as _sdist - - class cmd_sdist(_sdist): - def run(self): - versions = get_versions() - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old - # version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir, files): - root = get_root() - cfg = get_config_from_root(root) - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory - # (remembering that it may be a hardlink) and replace it with an - # updated value - target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist - - return cmds - - -CONFIG_ERROR = """ -setup.cfg is missing the necessary Versioneer configuration. You need -a section like: - - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = - parentdir_prefix = myproject- - -You will also need to edit your setup.py to use the results: - - import versioneer - setup(version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), ...) - -Please read the docstring in ./versioneer.py for configuration instructions, -edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. -""" - -SAMPLE_CONFIG = """ -# See the docstring in versioneer.py for instructions. Note that you must -# re-run 'versioneer.py setup' after changing this section, and commit the -# resulting files. - -[versioneer] -#VCS = git -#style = pep440 -#versionfile_source = -#versionfile_build = -#tag_prefix = -#parentdir_prefix = - -""" - -OLD_SNIPPET = """ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -""" - -INIT_PY_SNIPPET = """ -from . import {0} -__version__ = {0}.get_versions()['version'] -""" - - -def do_setup(): - """Do main VCS-independent setup function for installing Versioneer.""" - root = get_root() - try: - cfg = get_config_from_root(root) - except (OSError, configparser.NoSectionError, - configparser.NoOptionError) as e: - if isinstance(e, (OSError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) - with open(os.path.join(root, "setup.cfg"), "a") as f: - f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) - return 1 - - print(" creating %s" % cfg.versionfile_source) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") - if os.path.exists(ipy): - try: - with open(ipy, "r") as f: - old = f.read() - except OSError: - old = "" - module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] - snippet = INIT_PY_SNIPPET.format(module) - if OLD_SNIPPET in old: - print(" replacing boilerplate in %s" % ipy) - with open(ipy, "w") as f: - f.write(old.replace(OLD_SNIPPET, snippet)) - elif snippet not in old: - print(" appending to %s" % ipy) - with open(ipy, "a") as f: - f.write(snippet) - else: - print(" %s unmodified" % ipy) - else: - print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-subst keyword - # substitution. - do_vcs_install(cfg.versionfile_source, ipy) - return 0 - - -def scan_setup_py(): - """Validate the contents of setup.py against Versioneer's expectations.""" - found = set() - setters = False - errors = 0 - with open("setup.py", "r") as f: - for line in f.readlines(): - if "import versioneer" in line: - found.add("import") - if "versioneer.get_cmdclass()" in line: - found.add("cmdclass") - if "versioneer.get_version()" in line: - found.add("get_version") - if "versioneer.VCS" in line: - setters = True - if "versioneer.versionfile_source" in line: - setters = True - if len(found) != 3: - print("") - print("Your setup.py appears to be missing some important items") - print("(but I might be wrong). Please make sure it has something") - print("roughly like the following:") - print("") - print(" import versioneer") - print(" setup( version=versioneer.get_version(),") - print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") - errors += 1 - if setters: - print("You should remove lines like 'versioneer.VCS = ' and") - print("'versioneer.versionfile_source = ' . This configuration") - print("now lives in setup.cfg, and should be removed from setup.py") - print("") - errors += 1 - return errors - - -def setup_command(): - """Set up Versioneer and exit with appropriate error code.""" - errors = do_setup() - errors += scan_setup_py() - sys.exit(1 if errors else 0) - - -if __name__ == "__main__": - cmd = sys.argv[1] - if cmd == "setup": - setup_command() From 5e842dce2fefd76713872155e32f58bb1512df5c Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Wed, 28 Jan 2026 17:11:44 +0100 Subject: [PATCH 02/33] removed link and replaced with imports as it is causing not a regular file issue when pip install --- .../python_actor/resources/common/code_parameters.py | 2 +- .../python_actor/resources/common/code_parameters_handlers | 1 - iwrap/gui/settings/code_parameters_pane.py | 4 ++-- setup.cfg | 2 +- setup.py | 2 -- 5 files changed, 4 insertions(+), 7 deletions(-) delete mode 120000 iwrap/generators/actor_generators/python_actor/resources/common/code_parameters_handlers diff --git a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py index c61332c..751d87a 100644 --- a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py +++ b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py @@ -1,5 +1,5 @@ import logging -from .code_parameters_handlers.handler_factory import HandlerFactory +from iwrap.settings.code_parameters_handlers.handler_factory import HandlerFactory class CodeParameters: # Class logger diff --git a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters_handlers b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters_handlers deleted file mode 120000 index 183ba09..0000000 --- a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters_handlers +++ /dev/null @@ -1 +0,0 @@ -../../../../../settings/code_parameters_handlers/ \ No newline at end of file diff --git a/iwrap/gui/settings/code_parameters_pane.py b/iwrap/gui/settings/code_parameters_pane.py index ebda2ff..a4bad2e 100644 --- a/iwrap/gui/settings/code_parameters_pane.py +++ b/iwrap/gui/settings/code_parameters_pane.py @@ -8,8 +8,8 @@ from iwrap.settings.project import ProjectSettings from iwrap.gui.settings.tooltip import ToolTip -from iwrap.generators.actor_generators.python_actor.resources.common.code_parameters_handlers.handler_factory import HandlerFactory -from iwrap.generators.actor_generators.python_actor.resources.common.code_parameters_handlers.parameters_handler_interface import ParametersHandlerInterface +from iwrap.settings.code_parameters_handlers.handler_factory import HandlerFactory +from iwrap.settings.code_parameters_handlers.parameters_handler_interface import ParametersHandlerInterface class CodeParametersPane(ttk.Frame, IWrapPane): # Class logger diff --git a/setup.cfg b/setup.cfg index 0786337..8542445 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ author_email = olivier.hoenen@iter.org license = ITER license description = A modular component generator, implemented in Python, used for creating IMAS actors from physics models. long_description_content_type=text/x-rst -long_description = file: Readme.md +long_description = file: README.md url = https://git.iter.org/projects/IMEX/repos/iwrap [flake8] diff --git a/setup.py b/setup.py index ee0dd48..f8bc351 100644 --- a/setup.py +++ b/setup.py @@ -93,12 +93,10 @@ def list_docs_data_files(path_to_docs: str): 'write_to': 'iwrap/_version.py', }, packages=find_packages(exclude=('tests*', 'testing*', 'test_suite*')), - cmdclass={'clean': CleanCommand}, setup_requires=pyproject_data["build-system"]["requires"], include_package_data=True, install_requires=install_requires, extras_require=optional_reqs, - cmdclass=cmdclassdict, entry_points={ 'console_scripts': [ 'iwrap-gui = bin.run:gui', From 9066df1c382bda4e07175c276f0c5e614546966d Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 09:43:59 +0100 Subject: [PATCH 03/33] updated CI files --- ci/muscle3/st01-system-env.sh | 35 +++++++++++++++++++++++------------ ci/muscle3/st02-iwrap-load.sh | 16 +++++----------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index b580b5d..2100ad5 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -1,19 +1,29 @@ # Set up environment -source ci-build/st00-defs.sh +source ci/muscle3/st00-defs.sh +# Set up environment such that module files can be loaded +if test -f /etc/profile.d/modules.sh ;then +. /etc/profile.d/modules.sh +else +. /usr/share/Modules/init/sh +fi +module purge # Set up environment -source /usr/share/Modules/init/sh -module use /work/imas/etc/modules/all +# module use /work/imas/etc/modules/all # Set up environment echo "--------------Module load IMAS--------------" if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL - try module load XMLlib/3.3.1-intel-compilers-2023.2.1 + try module load XMLlib/3.3.1-intel-compilers-2023.2.1 - try module load MUSCLE3/0.7.1-intel-2023b - try module load IMAS/3.41.0-4.11.10-intel-2020b # <= No IMAS for intel 2023b + try module load MUSCLE3/0.7.1-intel-2023b + try module load IMAS-AL-Fortran/5.4.0-intel-2023b-DD-4.0.0 + try module load IMAS-AL-Java/5.4.0-intel-2023b-DD-4.0.0 + try module load IMAS-AL-Cpp/5.4.0-intel-2023b-DD-4.0.0 + try module load IMAS-AL-Matlab/5.4.0-intel-2023b-DD-4.0.0 + try module load IMAS-Python/2.0.1-intel-2023b export CXX="icpc" @@ -24,9 +34,13 @@ if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL else # GFORTRAN + try module load IMAS-AL-Fortran/5.4.0-foss-2023b-DD-4.0.0 + try module load IMAS-AL-Java/5.4.0-foss-2023b-DD-4.0.0 + try module load IMAS-AL-Cpp/5.4.0-foss-2023b-DD-4.0.0 + try module load IMAS-AL-Matlab/5.4.0-foss-2023b-DD-4.0.0 + try module load IMAS-Python/2.0.1-foss-2023b try module load XMLlib/3.3.1-GCC-13.2.0 try module load MUSCLE3/0.7.1-foss-2023b - try module load IMAS/3.41.0-4.11.10-foss-2023b export CXX="g++" export FC="gfortran" @@ -34,13 +48,10 @@ else export MPIFC="mpifort" fi -if [[ ! -n $AL_VERSION ]]; then - export AL_VERSION=$UAL_VERSION -fi - export AL_MAJOR="${AL_VERSION%.*.*}" SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../tests" &> /dev/null && pwd ) + +export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests" &> /dev/null && pwd ) echo TESTS_DIR: $TESTS_DIR diff --git a/ci/muscle3/st02-iwrap-load.sh b/ci/muscle3/st02-iwrap-load.sh index 8d6503d..a71ffdb 100755 --- a/ci/muscle3/st02-iwrap-load.sh +++ b/ci/muscle3/st02-iwrap-load.sh @@ -1,17 +1,11 @@ # Set up environment -source ci-build/st00-defs.sh +source ci/muscle3/st00-defs.sh -# Set up environment -echo "--------------Module load iWrap--------------" -if [ "$COMPILER_VENDOR" == "intel" ]; then - - # INTEL - try module load iWrap/0.10.0-intel-2023b +export IWRAP_HOME=$(realpath "$(dirname ${BASH_SOURCE})/../..") -else +export PATH=${IWRAP_HOME}/bin:${PATH} - # INTEL - try module load iWrap/0.10.0-GCCcore-13.2.0 +export PYTHONPATH=${IWRAP_HOME}:${PYTHONPATH} +export TESTS_DIR="${IWRAP_HOME}/tests" -fi From fe79c2cc6cbdb48ec00d1648a202a3c82f4906e0 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 10:06:05 +0100 Subject: [PATCH 04/33] fixed CI scripts --- ci/muscle3/build-macro-model.sh | 2 +- ci/muscle3/setup-test-env.sh | 12 ++++++------ ci/muscle3/st00-defs.sh | 2 +- ci/muscle3/st02-iwrap-load.sh | 5 +++++ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ci/muscle3/build-macro-model.sh b/ci/muscle3/build-macro-model.sh index 1bdb4e5..cc06701 100755 --- a/ci/muscle3/build-macro-model.sh +++ b/ci/muscle3/build-macro-model.sh @@ -2,6 +2,6 @@ source ci/muscle3/setup-test-env.sh $* || exit 1 -cd ./tests/macro +cd ./tests/muscle3/macro make all diff --git a/ci/muscle3/setup-test-env.sh b/ci/muscle3/setup-test-env.sh index b20ddd2..477302b 100755 --- a/ci/muscle3/setup-test-env.sh +++ b/ci/muscle3/setup-test-env.sh @@ -44,11 +44,11 @@ else fi # check if 'local' or installed version should be used -if $use_installed ; then - echo "WARNING: Using pre-loaded instance of plugins!" -else - echo "INFO: Setting up plugins from local directory!" - source ci/muscle3/st03-m3plugins-local.sh "$@" || exit 1 -fi +# if $use_installed ; then +# echo "WARNING: Using pre-loaded instance of plugins!" +# else +# echo "INFO: Setting up plugins from local directory!" +# source ci/muscle3/st03-m3plugins-local.sh "$@" || exit 1 +# fi diff --git a/ci/muscle3/st00-defs.sh b/ci/muscle3/st00-defs.sh index 5523801..506b666 100755 --- a/ci/muscle3/st00-defs.sh +++ b/ci/muscle3/st00-defs.sh @@ -1,6 +1,6 @@ # auxiliary functions definitions - +echo "executing $(basename "$0")" function yell () { echo "$0: $*" >&2 diff --git a/ci/muscle3/st02-iwrap-load.sh b/ci/muscle3/st02-iwrap-load.sh index a71ffdb..e1be470 100755 --- a/ci/muscle3/st02-iwrap-load.sh +++ b/ci/muscle3/st02-iwrap-load.sh @@ -8,4 +8,9 @@ export PATH=${IWRAP_HOME}/bin:${PATH} export PYTHONPATH=${IWRAP_HOME}:${PYTHONPATH} export TESTS_DIR="${IWRAP_HOME}/tests" +echo "IWRAP_HOME: $IWRAP_HOME" +echo "PATH: $PATH" +echo "PYTHONPATH: $PYTHONPATH" +echo "TESTS_DIR: $TESTS_DIR" +echo "IWRAP setup completed successfully" From a9c80d305ca9dcb71833d23a87d1ea7e23eade7f Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 10:38:49 +0100 Subject: [PATCH 05/33] fixed muscle3 test cases --- ci/muscle3/build-macro-model.sh | 2 +- ci/muscle3/setup-test-env.sh | 2 +- ci/muscle3/st01-system-env.sh | 6 +----- ci/muscle3/st02-iwrap-load.sh | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ci/muscle3/build-macro-model.sh b/ci/muscle3/build-macro-model.sh index cc06701..1faca37 100755 --- a/ci/muscle3/build-macro-model.sh +++ b/ci/muscle3/build-macro-model.sh @@ -1,7 +1,7 @@ #!/bin/bash source ci/muscle3/setup-test-env.sh $* || exit 1 - +echo "executing $(basename "$0")" cd ./tests/muscle3/macro make all diff --git a/ci/muscle3/setup-test-env.sh b/ci/muscle3/setup-test-env.sh index 477302b..bc727a1 100755 --- a/ci/muscle3/setup-test-env.sh +++ b/ci/muscle3/setup-test-env.sh @@ -1,5 +1,5 @@ #!/bin/bash - +echo "executing $(basename "$0")" print_help() { echo -e "Usage:" diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index 2100ad5..d02b7d1 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -1,6 +1,6 @@ # Set up environment source ci/muscle3/st00-defs.sh - +echo "executing $(basename "$0")" # Set up environment such that module files can be loaded if test -f /etc/profile.d/modules.sh ;then . /etc/profile.d/modules.sh @@ -8,14 +8,11 @@ else . /usr/share/Modules/init/sh fi module purge -# Set up environment -# module use /work/imas/etc/modules/all # Set up environment echo "--------------Module load IMAS--------------" if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL - try module load XMLlib/3.3.1-intel-compilers-2023.2.1 try module load MUSCLE3/0.7.1-intel-2023b @@ -25,7 +22,6 @@ if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL try module load IMAS-AL-Matlab/5.4.0-intel-2023b-DD-4.0.0 try module load IMAS-Python/2.0.1-intel-2023b - export CXX="icpc" export FC="ifort" export MPICXX="mpiicpc" diff --git a/ci/muscle3/st02-iwrap-load.sh b/ci/muscle3/st02-iwrap-load.sh index e1be470..c558187 100755 --- a/ci/muscle3/st02-iwrap-load.sh +++ b/ci/muscle3/st02-iwrap-load.sh @@ -1,6 +1,6 @@ # Set up environment source ci/muscle3/st00-defs.sh - +echo "executing $(basename "$0")" export IWRAP_HOME=$(realpath "$(dirname ${BASH_SOURCE})/../..") export PATH=${IWRAP_HOME}/bin:${PATH} From 44916d4fc3128b7ab80f65e98f54361c0e9b61ef Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 10:47:29 +0100 Subject: [PATCH 06/33] removed setting TEST_DIR --- ci/muscle3/st02-iwrap-load.sh | 4 ++-- envs/common/01_set_iwrap_env.sh | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ci/muscle3/st02-iwrap-load.sh b/ci/muscle3/st02-iwrap-load.sh index c558187..4af1713 100755 --- a/ci/muscle3/st02-iwrap-load.sh +++ b/ci/muscle3/st02-iwrap-load.sh @@ -6,11 +6,11 @@ export IWRAP_HOME=$(realpath "$(dirname ${BASH_SOURCE})/../..") export PATH=${IWRAP_HOME}/bin:${PATH} export PYTHONPATH=${IWRAP_HOME}:${PYTHONPATH} -export TESTS_DIR="${IWRAP_HOME}/tests" + echo "IWRAP_HOME: $IWRAP_HOME" echo "PATH: $PATH" echo "PYTHONPATH: $PYTHONPATH" -echo "TESTS_DIR: $TESTS_DIR" + echo "IWRAP setup completed successfully" diff --git a/envs/common/01_set_iwrap_env.sh b/envs/common/01_set_iwrap_env.sh index 16a4946..62d12ba 100644 --- a/envs/common/01_set_iwrap_env.sh +++ b/envs/common/01_set_iwrap_env.sh @@ -2,10 +2,6 @@ export PATH=${IWRAP_HOME}/bin:${PATH} -if [[ ! -n $AL_VERSION ]]; then - export AL_VERSION=$UAL_VERSION -fi - export PYTHONPATH=${IWRAP_HOME}:${PYTHONPATH} export TESTS_DIR="${IWRAP_HOME}/tests" From 1558387080e99274a68d6ac76381e633b40af881 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 10:52:33 +0100 Subject: [PATCH 07/33] fixed issues with TEST_DIR --- ci/muscle3/st01-system-env.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index d02b7d1..1e6246e 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -46,8 +46,8 @@ fi export AL_MAJOR="${AL_VERSION%.*.*}" -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests" &> /dev/null && pwd ) +# export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests" &> /dev/null && pwd ) -echo TESTS_DIR: $TESTS_DIR +# echo TESTS_DIR: $TESTS_DIR From f3ef70292b7f8c00d236d1e33c455a363aa6468b Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 11:10:59 +0100 Subject: [PATCH 08/33] check testdir --- tests/muscle3/actors/cpp/basic_cpp/Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/muscle3/actors/cpp/basic_cpp/Makefile b/tests/muscle3/actors/cpp/basic_cpp/Makefile index db80faf..1e275a2 100644 --- a/tests/muscle3/actors/cpp/basic_cpp/Makefile +++ b/tests/muscle3/actors/cpp/basic_cpp/Makefile @@ -6,5 +6,15 @@ CODE_LANGUAGE=cpp WRAPPED_CODE_NAME=basic clean wrapped actor actor-gui wf-run check-output test: +@if [ -z "$(TESTS_DIR)" ]; then \ + echo "Error: TESTS_DIR is not set"; \ + exit 1; \ +fi +@echo "TESTS_DIR: $(TESTS_DIR)" + +@if [ ! -f "$(TESTS_DIR)/Makefile" ]; then \ + echo "Error: $(TESTS_DIR)/Makefile not found"; \ + exit 1; \ +fi $(MAKE) -f $(TESTS_DIR)/Makefile $@ From 43e546473bf6182cb3917b6900ddcabd5429917c Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 11:15:30 +0100 Subject: [PATCH 09/33] fix makefile --- tests/muscle3/actors/cpp/basic_cpp/Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/muscle3/actors/cpp/basic_cpp/Makefile b/tests/muscle3/actors/cpp/basic_cpp/Makefile index 1e275a2..565bd25 100644 --- a/tests/muscle3/actors/cpp/basic_cpp/Makefile +++ b/tests/muscle3/actors/cpp/basic_cpp/Makefile @@ -6,11 +6,11 @@ CODE_LANGUAGE=cpp WRAPPED_CODE_NAME=basic clean wrapped actor actor-gui wf-run check-output test: -@if [ -z "$(TESTS_DIR)" ]; then \ - echo "Error: TESTS_DIR is not set"; \ - exit 1; \ -fi -@echo "TESTS_DIR: $(TESTS_DIR)" + @if [ -z "$(TESTS_DIR)" ]; then \ + echo "Error: TESTS_DIR is not set"; \ + exit 1; \ + fi + @echo "TESTS_DIR: $(TESTS_DIR)" @if [ ! -f "$(TESTS_DIR)/Makefile" ]; then \ echo "Error: $(TESTS_DIR)/Makefile not found"; \ From 5739b7df2e7f7799df18bd1cba62f7d027232aa8 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 11:18:33 +0100 Subject: [PATCH 10/33] fixed missing separator --- tests/muscle3/actors/cpp/basic_cpp/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/muscle3/actors/cpp/basic_cpp/Makefile b/tests/muscle3/actors/cpp/basic_cpp/Makefile index 565bd25..8ea9a7d 100644 --- a/tests/muscle3/actors/cpp/basic_cpp/Makefile +++ b/tests/muscle3/actors/cpp/basic_cpp/Makefile @@ -12,9 +12,9 @@ clean wrapped actor actor-gui wf-run check-output test: fi @echo "TESTS_DIR: $(TESTS_DIR)" -@if [ ! -f "$(TESTS_DIR)/Makefile" ]; then \ - echo "Error: $(TESTS_DIR)/Makefile not found"; \ - exit 1; \ -fi + @if [ ! -f "$(TESTS_DIR)/Makefile" ]; then \ + echo "Error: $(TESTS_DIR)/Makefile not found"; \ + exit 1; \ + fi $(MAKE) -f $(TESTS_DIR)/Makefile $@ From 42e9068e41e23555ec61939984f3203d4dbb9095 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 11:33:32 +0100 Subject: [PATCH 11/33] added makefile --- ci/muscle3/st01-system-env.sh | 6 ++-- tests/muscle3/Makefile | 54 +++++++++++++++++++++++++++++++++++ tests/muscle3/pytest.ini | 11 +++++++ tests/muscle3/pytests.py | 4 +++ tests/muscle3/run-tests.sh | 5 ++++ 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 tests/muscle3/Makefile create mode 100644 tests/muscle3/pytest.ini create mode 100755 tests/muscle3/pytests.py create mode 100755 tests/muscle3/run-tests.sh diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index 1e6246e..d02b7d1 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -46,8 +46,8 @@ fi export AL_MAJOR="${AL_VERSION%.*.*}" -# SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests" &> /dev/null && pwd ) +export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests" &> /dev/null && pwd ) -# echo TESTS_DIR: $TESTS_DIR +echo TESTS_DIR: $TESTS_DIR diff --git a/tests/muscle3/Makefile b/tests/muscle3/Makefile new file mode 100644 index 0000000..b6812db --- /dev/null +++ b/tests/muscle3/Makefile @@ -0,0 +1,54 @@ +.PHONY: wrapped actor actor-gui wf-run check-output test +export + +export TEST_DIR:=$(TESTS_DIR)/actors/$(CODE_LANGUAGE)/$(ACTOR_NAME) +LAST_SNAPSHOT:=$(lastword $(sort $(wildcard $(TEST_DIR)/run_$(ACTOR_NAME)_*/snapshots/snapshot_*.ymmsl))) + + +ACTOR_DIR:=$(TEST_DIR)/m3_actor +export PYTHONPATH:=$(ACTOR_DIR):$(PYTHONPATH) + +test: | clean wrapped actor wf-run check-output + + +check-output: + @echo "================ CHECKING TEST OUTPUT =========================" + @cmp expected.out test.out &> /dev/null || \ + (echo 'ERROR: expected and real workflow outcome differs!' && exit 1 ) + @echo 'SUCCESS: Test PASSED' + + +wf-run: + @echo "================ TEST FIRST RUN =========================" + + @$(RM) -f test.out + muscle_manager --start-all --log-level DEBUG\ + $(TESTS_DIR)/workflow/test.ymmsl \ + $(TESTS_DIR)/workflow/test_macro.ymmsl \ + $(TESTS_DIR)/actors/$(CODE_LANGUAGE)/$(ACTOR_NAME)/test_micro.ymmsl + +wf-restart: + @echo "================ TEST RESTART =========================" + @$(RM) -f test.out + muscle_manager --start-all --log-level DEBUG\ + $(TESTS_DIR)/workflow/test.ymmsl \ + $(TESTS_DIR)/workflow/test_macro.ymmsl \ + $(TESTS_DIR)/actors/$(CODE_LANGUAGE)/$(ACTOR_NAME)/test_micro.ymmsl \ + $(LAST_SNAPSHOT) + +actor: + @echo "================ ACTOR BUILD =========================" + iwrap --actor-type $(ACTOR_TYPE) --actor-name $(ACTOR_NAME) --install-dir $(ACTOR_DIR) --file $(TEST_DIR)/$(ACTOR_NAME).yaml + +actor-gui: + iwrap-gui --actor-type $(ACTOR_TYPE) --actor-name $(ACTOR_NAME) --install-dir $(ACTOR_DIR) --file $(TEST_DIR)/$(ACTOR_NAME).yaml + +wrapped: + @echo "================ WRAPPED CODE BUILD =========================" + @$(MAKE) -C $(TESTS_DIR)/wrapped_codes/$(CODE_LANGUAGE)/$(WRAPPED_CODE_NAME) clean + @$(MAKE) -C $(TESTS_DIR)/wrapped_codes/$(CODE_LANGUAGE)/$(WRAPPED_CODE_NAME) + +clean: + #@$(RM) -rf ./reports + @$(RM) -rf $(ACTOR_DIR) + @$(RM) -rf ./run_$(ACTOR_NAME)_* diff --git a/tests/muscle3/pytest.ini b/tests/muscle3/pytest.ini new file mode 100644 index 0000000..8176c20 --- /dev/null +++ b/tests/muscle3/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +markers = + slow: Mark test as slow + fast: Mark test as fast + +log_cli = True +log_cli_level = INFO +log_cli_format = [%(levelname)s] + %(message)s + +addopts = -v --junitxml=test_results.xml \ No newline at end of file diff --git a/tests/muscle3/pytests.py b/tests/muscle3/pytests.py new file mode 100755 index 0000000..9da8c49 --- /dev/null +++ b/tests/muscle3/pytests.py @@ -0,0 +1,4 @@ +#!/bin/env python3 + + +print("I am placeholder for a future tests!") diff --git a/tests/muscle3/run-tests.sh b/tests/muscle3/run-tests.sh new file mode 100755 index 0000000..cbe4f67 --- /dev/null +++ b/tests/muscle3/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + + +echo "I am placeholder for a future tests!" + From a34e20c219c8cf876c47ab47b1ee20820b4cdbbe Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 11:37:02 +0100 Subject: [PATCH 12/33] updated paths --- ci/muscle3/st01-system-env.sh | 2 +- tests/muscle3/actors/cpp/basic_cpp/Makefile | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index d02b7d1..eb05fc9 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -48,6 +48,6 @@ export AL_MAJOR="${AL_VERSION%.*.*}" SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests" &> /dev/null && pwd ) +export TESTS_DIR=$( cd -- "${SCRIPT_DIR}/../../tests/muscle3" &> /dev/null && pwd ) echo TESTS_DIR: $TESTS_DIR diff --git a/tests/muscle3/actors/cpp/basic_cpp/Makefile b/tests/muscle3/actors/cpp/basic_cpp/Makefile index 8ea9a7d..db80faf 100644 --- a/tests/muscle3/actors/cpp/basic_cpp/Makefile +++ b/tests/muscle3/actors/cpp/basic_cpp/Makefile @@ -6,15 +6,5 @@ CODE_LANGUAGE=cpp WRAPPED_CODE_NAME=basic clean wrapped actor actor-gui wf-run check-output test: - @if [ -z "$(TESTS_DIR)" ]; then \ - echo "Error: TESTS_DIR is not set"; \ - exit 1; \ - fi - @echo "TESTS_DIR: $(TESTS_DIR)" - - @if [ ! -f "$(TESTS_DIR)/Makefile" ]; then \ - echo "Error: $(TESTS_DIR)/Makefile not found"; \ - exit 1; \ - fi $(MAKE) -f $(TESTS_DIR)/Makefile $@ From a35c3a5ed92e634e02bc497c0823c3b3e0337ca4 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 12:06:13 +0100 Subject: [PATCH 13/33] added pyyaml --- ci/muscle3/st01-system-env.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index eb05fc9..cdb94a8 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -21,6 +21,7 @@ if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL try module load IMAS-AL-Cpp/5.4.0-intel-2023b-DD-4.0.0 try module load IMAS-AL-Matlab/5.4.0-intel-2023b-DD-4.0.0 try module load IMAS-Python/2.0.1-intel-2023b + try module load PyYAML/6.0.1-GCCcore-13.2.0 export CXX="icpc" export FC="ifort" @@ -37,6 +38,7 @@ else try module load IMAS-Python/2.0.1-foss-2023b try module load XMLlib/3.3.1-GCC-13.2.0 try module load MUSCLE3/0.7.1-foss-2023b + try module load PyYAML/6.0.1-GCCcore-13.2.0 export CXX="g++" export FC="gfortran" From 3d69065259b13a891f02d8389adbd5f7094fa305 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 12:50:07 +0100 Subject: [PATCH 14/33] added dependency and fixed converter issue --- ci/muscle3/st01-system-env.sh | 2 ++ iwrap/settings/compatibility/converter.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/ci/muscle3/st01-system-env.sh b/ci/muscle3/st01-system-env.sh index cdb94a8..5093ac5 100755 --- a/ci/muscle3/st01-system-env.sh +++ b/ci/muscle3/st01-system-env.sh @@ -22,6 +22,7 @@ if [ "$COMPILER_VENDOR" == "intel" ]; then # INTEL try module load IMAS-AL-Matlab/5.4.0-intel-2023b-DD-4.0.0 try module load IMAS-Python/2.0.1-intel-2023b try module load PyYAML/6.0.1-GCCcore-13.2.0 + try module load lxml/4.9.3-GCCcore-13.2.0 export CXX="icpc" export FC="ifort" @@ -39,6 +40,7 @@ else try module load XMLlib/3.3.1-GCC-13.2.0 try module load MUSCLE3/0.7.1-foss-2023b try module load PyYAML/6.0.1-GCCcore-13.2.0 + try module load lxml/4.9.3-GCCcore-13.2.0 export CXX="g++" export FC="gfortran" diff --git a/iwrap/settings/compatibility/converter.py b/iwrap/settings/compatibility/converter.py index 63807d8..353678a 100644 --- a/iwrap/settings/compatibility/converter.py +++ b/iwrap/settings/compatibility/converter.py @@ -63,6 +63,10 @@ def __delete(self, splitted_path, condition, current_node=None): return False return True else: + # Check if the key exists before trying to access it + if splitted_path[0] not in current_node: + return False + if isinstance(current_node[splitted_path[0]], list): condition = condition.replace("$TARGET", "current_node[splitted_path[0]][x]") del_indexes = [] From e0e40be7be3b55a57661c5b7362cc901be31a8ca Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 13:36:17 +0100 Subject: [PATCH 15/33] imas-al-python to imas-python --- .../muscle3_python/resources/macros/legacy_ids.jinja2 | 3 ++- .../muscle3_python/resources/muscle3_tools.py.jinja2 | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 index f2f2ed0..f5ad592 100644 --- a/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 +++ b/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 @@ -3,7 +3,8 @@ {%- endmacro -%} {% macro declare(ids_name, ids_var_name) -%} - {{ ids_var_name }} = imas.{{ ids_name }}() + _factory = imas.IDSFactory() + {{ ids_var_name }} = _factory.{{ ids_name }}() {%- endmacro -%} {%- macro provenance(ids_var_name, sbrt_name) -%} diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 index 725c73c..3a4d210 100644 --- a/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 +++ b/iwrap/generators/actor_generators/muscle3_python/resources/muscle3_tools.py.jinja2 @@ -7,7 +7,6 @@ import libmuscle from libmuscle import Instance, USES_CHECKPOINT_API import imas -from imas import imasdef # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -60,7 +59,7 @@ def receive_ids(m3_instance: libmuscle.Instance, port_name, ids): # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - def send_ids(m3_instance: libmuscle.Instance, port_name, ids, timestamp): - ids_bytes = ids.serialize(imasdef.DEFAULT_SERIALIZER_PROTOCOL) + ids_bytes = ids.serialize(imas.ids_defs.DEFAULT_SERIALIZER_PROTOCOL) m3_message = libmuscle.Message( timestamp, None, ids_bytes) m3_instance.send(port_name, m3_message) From 0a49e047d9ead7273a1234bc0a0caec66eebdc9c Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 13:40:36 +0100 Subject: [PATCH 16/33] fixed indentation --- .../muscle3_python/resources/macros/legacy_ids.jinja2 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 b/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 index f5ad592..9a780e7 100644 --- a/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 +++ b/iwrap/generators/actor_generators/muscle3_python/resources/macros/legacy_ids.jinja2 @@ -3,11 +3,11 @@ {%- endmacro -%} {% macro declare(ids_name, ids_var_name) -%} - _factory = imas.IDSFactory() - {{ ids_var_name }} = _factory.{{ ids_name }}() + _factory = imas.IDSFactory() + {{ ids_var_name }} = _factory.{{ ids_name }}() {%- endmacro -%} {%- macro provenance(ids_var_name, sbrt_name) -%} - {{ ids_var_name }}.code.name = "{{sbrt_name}}" - {{ ids_var_name }}.code.version = "" + {{ ids_var_name }}.code.name = "{{sbrt_name}}" + {{ ids_var_name }}.code.version = "" {%- endmacro -%} From 36c5d0c5fb82f886a055e511dcc869e3ece06da5 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 13:50:55 +0100 Subject: [PATCH 17/33] fixed documentation --- envs/iter/gcc/al5/configure_env.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/envs/iter/gcc/al5/configure_env.sh b/envs/iter/gcc/al5/configure_env.sh index c500193..54e72b6 100755 --- a/envs/iter/gcc/al5/configure_env.sh +++ b/envs/iter/gcc/al5/configure_env.sh @@ -15,6 +15,7 @@ module load JPype/1.5.0-gfbf-2023b module load json-fortran/8.5.2-GCC-13.2.0 module load JsonCpp/1.9.5-GCCcore-13.2.0 module load f90nml/1.4.4-GCCcore-13.2.0 +module load nodejs/20.9.0-GCCcore-13.2.0 export CXX="g++" export FC="gfortran" From df5df3a650efaf4ac69840c2027419dc36bafbfa Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 17:13:52 +0100 Subject: [PATCH 18/33] fixed jupyter versions --- docs/requirements.txt | 34 +++++++++++++++--------------- envs/iter/gcc/al5/configure_env.sh | 1 - 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 26c6c33..a973efd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,18 +1,18 @@ -urllib3 -jupyterlab -jupyter-book -docutils -jupyterlab_myst -lxml -markdown_it_py -mdit_py_plugins -myst-nb -myst-parser -sphinx -sphinx_inline_tabs -tomli -typing-extensions -pylint -pyparsing +urllib3==2.5.0 +jupyterlab==4.4.5 +jupyter-book==1.0.4.post1 +docutils==0.21.2 +jupyterlab_myst==2.4.2 +lxml==6.0.0 +markdown_it_py==3.0.0 +mdit_py_plugins==0.4.2 +myst-nb==1.3.0 +myst-parser==3.0.1 +sphinx==7.4.7 +sphinx_inline_tabs==2023.4.21 +tomli==2.2.1 +typing-extensions==4.14.1 +pylint==3.3.7 +pyparsing==3.2.3 numpy==1.26.4 -sphinx_immaterial \ No newline at end of file +sphinx_immaterial==0.13.5 \ No newline at end of file diff --git a/envs/iter/gcc/al5/configure_env.sh b/envs/iter/gcc/al5/configure_env.sh index 54e72b6..c500193 100755 --- a/envs/iter/gcc/al5/configure_env.sh +++ b/envs/iter/gcc/al5/configure_env.sh @@ -15,7 +15,6 @@ module load JPype/1.5.0-gfbf-2023b module load json-fortran/8.5.2-GCC-13.2.0 module load JsonCpp/1.9.5-GCCcore-13.2.0 module load f90nml/1.4.4-GCCcore-13.2.0 -module load nodejs/20.9.0-GCCcore-13.2.0 export CXX="g++" export FC="gfortran" From f979b6be4dea9720a4195cdf1f2563c72efd3542 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 17:37:42 +0100 Subject: [PATCH 19/33] updated documentation --- docs/documentation/actor_generation_cmdln.rst | 2 +- docs/documentation/quickstart.rst | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/documentation/actor_generation_cmdln.rst b/docs/documentation/actor_generation_cmdln.rst index 92d30b9..7835fe0 100644 --- a/docs/documentation/actor_generation_cmdln.rst +++ b/docs/documentation/actor_generation_cmdln.rst @@ -35,7 +35,7 @@ Once YAML file is prepared it can be used for generation of an actor using iWrap lists details of given actor type generator -v, --version show program's version number and exit - For more information, visit . + For more information, visit . If YAML file contains both code description and actor description parts, no additional switches are required. diff --git a/docs/documentation/quickstart.rst b/docs/documentation/quickstart.rst index d2a002e..5a67762 100644 --- a/docs/documentation/quickstart.rst +++ b/docs/documentation/quickstart.rst @@ -118,8 +118,9 @@ Create a workflow ``workflow.py``: from my_first_actor import MyFirstActor # Create IDS objects - equilibrium = imas.equilibrium() - core_profiles = imas.core_profiles() + _factory = imas.IDSFactory() + equilibrium = _factory.equilibrium() + core_profiles = _factory.core_profiles() # Initialize and run actor actor = MyFirstActor() From 53e580aa8b9893e60eec49ee390e9a6881cff89d Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 29 Jan 2026 17:50:34 +0100 Subject: [PATCH 20/33] fixed documentation and add work pypi workflow --- .github/workflows/publish-to-pypi.yml | 102 ++++++++++++++++++ docs/documentation/muscle3_actors.rst | 22 ++-- .../documentation/muscle3_migration_guide.rst | 4 +- docs/documentation/quickstart.rst | 4 +- 4 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/publish-to-pypi.yml diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..130f073 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,102 @@ +name: Publish Python Package to PyPI and TestPyPI + +on: + release: + types: [published] + workflow_dispatch: + inputs: + publish_to: + description: 'Where to publish (testpypi, pypi, or both)' + required: true + default: 'testpypi' + type: choice + options: + - testpypi + - pypi + - both + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for setuptools_scm + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check distribution + run: twine check dist/* + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-testpypi: + name: Publish to TestPyPI + needs: [build] + runs-on: ubuntu-latest + if: | + (github.event_name == 'workflow_dispatch' && + (github.event.inputs.publish_to == 'testpypi' || github.event.inputs.publish_to == 'both')) || + (github.event_name == 'release' && github.event.action == 'published') + + environment: + name: testpypi + url: https://test.pypi.org/project/iwrap/ + + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + publish-to-pypi: + name: Publish to PyPI + needs: [build] + runs-on: ubuntu-latest + if: | + (github.event_name == 'workflow_dispatch' && + (github.event.inputs.publish_to == 'pypi' || github.event.inputs.publish_to == 'both')) || + (github.event_name == 'release' && github.event.action == 'published') + + environment: + name: pypi + url: https://pypi.org/project/iwrap/ + + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/docs/documentation/muscle3_actors.rst b/docs/documentation/muscle3_actors.rst index b99a7c2..24b9360 100644 --- a/docs/documentation/muscle3_actors.rst +++ b/docs/documentation/muscle3_actors.rst @@ -195,19 +195,19 @@ Differences from Standard Python Actors MUSCLE3 actors differ from standard Python actors in several ways: +---------------------------+-------------------------+-------------------------+ -| Feature | Standard Python Actor | MUSCLE3 Actor | +| Feature | Standard Python Actor | MUSCLE3 Actor | +===========================+=========================+=========================+ -| Coupling Framework | Direct Python calls | MUSCLE3 messaging | +| Coupling Framework | Direct Python calls | MUSCLE3 messaging | +---------------------------+-------------------------+-------------------------+ -| Workflow Description | Python script | yMMSL file | +| Workflow Description | Python script | yMMSL file | +---------------------------+-------------------------+-------------------------+ -| Data Exchange | Direct IDS passing | MUSCLE3 ports | +| Data Exchange | Direct IDS passing | MUSCLE3 ports | +---------------------------+-------------------------+-------------------------+ -| Execution Model | Sequential/MPI | MUSCLE3 managed | +| Execution Model | Sequential/MPI | MUSCLE3 managed | +---------------------------+-------------------------+-------------------------+ -| Init/Finalize Arguments | Can have IDS | No IDS allowed | +| Init/Finalize Arguments | Can have IDS | No IDS allowed | +---------------------------+-------------------------+-------------------------+ -| Multiscale Coupling | Manual | Built-in | +| Multiscale Coupling | Manual | Built-in | +---------------------------+-------------------------+-------------------------+ Best Practices @@ -322,7 +322,7 @@ All existing YAML files and workflows remain compatible. No changes to your code See Also ======================================== -- :doc:`../project_description` - Actor definition and generation -- :doc:`../actor_generation_cmdln` - Command-line interface -- :doc:`../iwrap_gui` - Graphical user interface -- :doc:`../developers_manual/04_adding_generators` - Creating custom generators +- :doc:`project_description` - Actor definition and generation +- :doc:`actor_generation_cmdln` - Command-line interface +- :doc:`iwrap_gui` - Graphical user interface +- :doc:`developers_manual/04_adding_generators` - Creating custom generators diff --git a/docs/documentation/muscle3_migration_guide.rst b/docs/documentation/muscle3_migration_guide.rst index 6059796..b42a590 100644 --- a/docs/documentation/muscle3_migration_guide.rst +++ b/docs/documentation/muscle3_migration_guide.rst @@ -1,6 +1,6 @@ -======================================== +==================================================== Migration Guide: MUSCLE3 Plugin Integration -======================================== +==================================================== This guide helps users and developers migrate from the external ``iwrap-plugins-muscle3`` package to the new built-in MUSCLE3 generators in iWrap core. diff --git a/docs/documentation/quickstart.rst b/docs/documentation/quickstart.rst index 5a67762..98aaa1e 100644 --- a/docs/documentation/quickstart.rst +++ b/docs/documentation/quickstart.rst @@ -368,7 +368,7 @@ Now that you've created your first actor, explore: 🔧 **Advanced Topics** - :doc:`code_standardization` - Code requirements and best practices - - :doc:`developers_manual/index` - Extending iWrap + - :doc:`developers_manual` - Extending iWrap - :doc:`actor_usage` - Using generated actors in workflows 💡 **Examples** @@ -380,7 +380,7 @@ Getting Help If you need assistance: -1. Check the documentation: :doc:`../index` +1. Check the documentation: :doc:`iWrap_intro` 2. Review examples in the repository 3. Contact: iWrap Development Team From 2f156636654a2ef4ba65e7dafedd995c6b34f094 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 30 Jan 2026 08:06:32 +0100 Subject: [PATCH 21/33] check if this cpp issue resolves --- .../generic_handler.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/iwrap/settings/code_parameters_handlers/generic_handler.py b/iwrap/settings/code_parameters_handlers/generic_handler.py index b267182..909cbf1 100644 --- a/iwrap/settings/code_parameters_handlers/generic_handler.py +++ b/iwrap/settings/code_parameters_handlers/generic_handler.py @@ -105,15 +105,28 @@ def initialize(self, parameters_path: str, schema_path: str): self._parameters_path = parameters_path self._schema_path = schema_path - # Read XSD (if not yet loaded) + # Read schema (if not yet loaded) if not self._schema_str: - schema_path = Path( self._default_params_dir, Path(self._schema_path)) - self._schema_str = self._read_file( schema_path ) + # Resolve schema path - make absolute if relative + if schema_path and not Path(schema_path).is_absolute(): + full_schema_path = Path(self._default_params_dir, schema_path).resolve() + else: + full_schema_path = Path(schema_path).resolve() if schema_path else None + + if full_schema_path: + self.__logger.debug(f"Reading schema from: {full_schema_path}") + self._schema_str = self._read_file(full_schema_path) if self._new_path_set: if self._parameters_path: - parameters_path = Path( self._default_params_dir, Path(self._parameters_path)) - self._parameters_str = self._read_file(parameters_path) + # Resolve parameters path - make absolute if relative + if not Path(self._parameters_path).is_absolute(): + full_params_path = Path(self._default_params_dir, self._parameters_path).resolve() + else: + full_params_path = Path(self._parameters_path).resolve() + + self.__logger.debug(f"Reading parameters from: {full_params_path}") + self._parameters_str = self._read_file(full_params_path) self._new_path_set = False def restore_default_parameters_path(self): From d37cb99ec0a8a00a3a5e17ad857a6fe22a193088 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 30 Jan 2026 08:38:23 +0100 Subject: [PATCH 22/33] check if this cpp issue resolves --- .../generic_handler.py | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/iwrap/settings/code_parameters_handlers/generic_handler.py b/iwrap/settings/code_parameters_handlers/generic_handler.py index 909cbf1..03604ce 100644 --- a/iwrap/settings/code_parameters_handlers/generic_handler.py +++ b/iwrap/settings/code_parameters_handlers/generic_handler.py @@ -107,25 +107,40 @@ def initialize(self, parameters_path: str, schema_path: str): # Read schema (if not yet loaded) if not self._schema_str: - # Resolve schema path - make absolute if relative - if schema_path and not Path(schema_path).is_absolute(): - full_schema_path = Path(self._default_params_dir, schema_path).resolve() - else: - full_schema_path = Path(schema_path).resolve() if schema_path else None - - if full_schema_path: + # Resolve schema path + if schema_path: + schema_path_obj = Path(schema_path) + if schema_path_obj.is_absolute(): + full_schema_path = schema_path_obj + else: + # Relative paths are resolved from current working directory (actor location) + # not from iwrap code location + full_schema_path = Path.cwd() / schema_path_obj + + full_schema_path = full_schema_path.resolve() self.__logger.debug(f"Reading schema from: {full_schema_path}") + + if not full_schema_path.exists(): + raise FileNotFoundError(f"Schema file not found: {full_schema_path}") + self._schema_str = self._read_file(full_schema_path) if self._new_path_set: if self._parameters_path: - # Resolve parameters path - make absolute if relative - if not Path(self._parameters_path).is_absolute(): - full_params_path = Path(self._default_params_dir, self._parameters_path).resolve() + # Resolve parameters path + params_path_obj = Path(self._parameters_path) + if params_path_obj.is_absolute(): + full_params_path = params_path_obj else: - full_params_path = Path(self._parameters_path).resolve() + # Relative paths are resolved from current working directory (actor location) + full_params_path = Path.cwd() / params_path_obj + full_params_path = full_params_path.resolve() self.__logger.debug(f"Reading parameters from: {full_params_path}") + + if not full_params_path.exists(): + raise FileNotFoundError(f"Parameters file not found: {full_params_path}") + self._parameters_str = self._read_file(full_params_path) self._new_path_set = False From a83513ccc64b270b89f2b917ab63425026e058d8 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 30 Jan 2026 08:57:17 +0100 Subject: [PATCH 23/33] check if this cpp issue resolves --- .../resources/common/code_parameters.py | 18 +++++++ .../generic_handler.py | 54 ++++++------------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py index 751d87a..9dda706 100644 --- a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py +++ b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py @@ -36,6 +36,24 @@ def set_parameter(self, path_to_node:str, value) -> None: self.__handler.set_parameter(path_to_node, value) def __init__(self, default_parameters_path:str, schema_path:str, parameters_format="xml"): + from pathlib import Path as PathLib + + # Resolve paths relative to actor's input directory + # Actor structure: actor_name/common/code_parameters.py and actor_name/input/ + actor_dir = PathLib(__file__).parent.parent # Go up from common/ to actor root + input_dir = actor_dir / 'input' + + # Convert relative paths (filenames) to absolute paths in actor's input directory + if default_parameters_path: + params_path = PathLib(default_parameters_path) + if not params_path.is_absolute(): + default_parameters_path = str(input_dir / params_path) + + if schema_path: + schema_path_obj = PathLib(schema_path) + if not schema_path_obj.is_absolute(): + schema_path = str(input_dir / schema_path_obj) + self._default_parameters_path = default_parameters_path self._schema_path = schema_path diff --git a/iwrap/settings/code_parameters_handlers/generic_handler.py b/iwrap/settings/code_parameters_handlers/generic_handler.py index 03604ce..f273f57 100644 --- a/iwrap/settings/code_parameters_handlers/generic_handler.py +++ b/iwrap/settings/code_parameters_handlers/generic_handler.py @@ -10,7 +10,6 @@ class GenericHandler(ParametersHandlerInterface, ABC): __logger = logging.getLogger(__name__ + "." + __qualname__) def __init__(self): - self._default_params_dir = Path(Path(__file__).parent, '../../input/') self._new_path_set: bool = True self._parameters_path: str = None self._default_parameters_path: str = None @@ -106,43 +105,24 @@ def initialize(self, parameters_path: str, schema_path: str): self._schema_path = schema_path # Read schema (if not yet loaded) - if not self._schema_str: - # Resolve schema path - if schema_path: - schema_path_obj = Path(schema_path) - if schema_path_obj.is_absolute(): - full_schema_path = schema_path_obj - else: - # Relative paths are resolved from current working directory (actor location) - # not from iwrap code location - full_schema_path = Path.cwd() / schema_path_obj - - full_schema_path = full_schema_path.resolve() - self.__logger.debug(f"Reading schema from: {full_schema_path}") - - if not full_schema_path.exists(): - raise FileNotFoundError(f"Schema file not found: {full_schema_path}") - - self._schema_str = self._read_file(full_schema_path) - - if self._new_path_set: - if self._parameters_path: - # Resolve parameters path - params_path_obj = Path(self._parameters_path) - if params_path_obj.is_absolute(): - full_params_path = params_path_obj - else: - # Relative paths are resolved from current working directory (actor location) - full_params_path = Path.cwd() / params_path_obj - - full_params_path = full_params_path.resolve() - self.__logger.debug(f"Reading parameters from: {full_params_path}") - - if not full_params_path.exists(): - raise FileNotFoundError(f"Parameters file not found: {full_params_path}") + if not self._schema_str and schema_path: + schema_file = Path(schema_path).resolve() + self.__logger.debug(f"Reading schema from: {schema_file}") + + if not schema_file.exists(): + raise FileNotFoundError(f"Schema file not found: {schema_file}") - self._parameters_str = self._read_file(full_params_path) - self._new_path_set = False + self._schema_str = self._read_file(schema_file) + + if self._new_path_set and self._parameters_path: + params_file = Path(self._parameters_path).resolve() + self.__logger.debug(f"Reading parameters from: {params_file}") + + if not params_file.exists(): + raise FileNotFoundError(f"Parameters file not found: {params_file}") + + self._parameters_str = self._read_file(params_file) + self._new_path_set = False def restore_default_parameters_path(self): self._parameters_path = self._default_parameters_path From d03009ef59092b0981a7561c5e7e63a25d863512 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 30 Jan 2026 09:41:35 +0100 Subject: [PATCH 24/33] debug the cpp issue --- .../python_actor/resources/common/code_parameters.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py index 9dda706..4898e8c 100644 --- a/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py +++ b/iwrap/generators/actor_generators/python_actor/resources/common/code_parameters.py @@ -40,19 +40,27 @@ def __init__(self, default_parameters_path:str, schema_path:str, parameters_form # Resolve paths relative to actor's input directory # Actor structure: actor_name/common/code_parameters.py and actor_name/input/ - actor_dir = PathLib(__file__).parent.parent # Go up from common/ to actor root + # Use resolve() to get the absolute real path, handling symlinks + actor_dir = PathLib(__file__).resolve().parent.parent # Go up from common/ to actor root input_dir = actor_dir / 'input' + self.__logger.debug(f"Actor directory: {actor_dir}") + self.__logger.debug(f"Input directory: {input_dir}") + self.__logger.debug(f"Input params path: {default_parameters_path}") + self.__logger.debug(f"Input schema path: {schema_path}") + # Convert relative paths (filenames) to absolute paths in actor's input directory if default_parameters_path: params_path = PathLib(default_parameters_path) if not params_path.is_absolute(): default_parameters_path = str(input_dir / params_path) + self.__logger.debug(f"Resolved params path: {default_parameters_path}") if schema_path: schema_path_obj = PathLib(schema_path) if not schema_path_obj.is_absolute(): schema_path = str(input_dir / schema_path_obj) + self.__logger.debug(f"Resolved schema path: {schema_path}") self._default_parameters_path = default_parameters_path self._schema_path = schema_path From a8d1d601da911ab0ba53167da582de2a1bba62b5 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Fri, 30 Jan 2026 18:21:38 +0100 Subject: [PATCH 25/33] added workflow --- .github/workflows/publish-to-pypi.yml | 102 -------------------------- .github/workflows/publish.yml | 74 +++++++++++++++++++ README.md | 2 +- 3 files changed, 75 insertions(+), 103 deletions(-) delete mode 100644 .github/workflows/publish-to-pypi.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml deleted file mode 100644 index 130f073..0000000 --- a/.github/workflows/publish-to-pypi.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: Publish Python Package to PyPI and TestPyPI - -on: - release: - types: [published] - workflow_dispatch: - inputs: - publish_to: - description: 'Where to publish (testpypi, pypi, or both)' - required: true - default: 'testpypi' - type: choice - options: - - testpypi - - pypi - - both - -jobs: - build: - name: Build distribution - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for setuptools_scm - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Build package - run: python -m build - - - name: Check distribution - run: twine check dist/* - - - name: Store the distribution packages - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - publish-to-testpypi: - name: Publish to TestPyPI - needs: [build] - runs-on: ubuntu-latest - if: | - (github.event_name == 'workflow_dispatch' && - (github.event.inputs.publish_to == 'testpypi' || github.event.inputs.publish_to == 'both')) || - (github.event_name == 'release' && github.event.action == 'published') - - environment: - name: testpypi - url: https://test.pypi.org/project/iwrap/ - - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ - - publish-to-pypi: - name: Publish to PyPI - needs: [build] - runs-on: ubuntu-latest - if: | - (github.event_name == 'workflow_dispatch' && - (github.event.inputs.publish_to == 'pypi' || github.event.inputs.publish_to == 'both')) || - (github.event_name == 'release' && github.event.action == 'published') - - environment: - name: pypi - url: https://pypi.org/project/iwrap/ - - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..57f8862 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,74 @@ +name: build-wheel-and-publish-test-pypi + +on: + push: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + name: Build distribution + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + # until saxonche is available in 3.13 + # https://saxonica.plan.io/issues/6561 + python-version: "<3.13" + - name: Install pypa/build + run: >- + python3 -m pip install pip setuptools wheel build + - name: Build a binary wheel and a source tarball + run: python3 -m build . + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish iWrap distribution to PyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + needs: + - build + runs-on: ubuntu-22.04 + environment: + name: pypi + url: https://pypi.org/p/iwrap + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + publish-to-testpypi: + name: Publish iWrap distribution to TestPyPI + if: github.event_name == 'push' + needs: + - build + runs-on: ubuntu-22.04 + environment: + name: testpypi + url: https://test.pypi.org/p/iwrap + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution to TestPyPI + uses: pypa/gh-action-pypi-publish@unstable/v1 + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true \ No newline at end of file diff --git a/README.md b/README.md index b028e92..71bc72b 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ iwrap --list-actor-types # Configuration of working environment ## Downloading software - git clone ssh://git@git.iter.org/imex/iwrap.git + git clone https://github.com/iterorganization/iWrap.git cd iwrap git checkout From d9f92ee03e276febd9e4cb77cd1933891f847dfe Mon Sep 17 00:00:00 2001 From: Prasad Date: Fri, 6 Feb 2026 11:00:53 +0100 Subject: [PATCH 26/33] Update docs/documentation/installation_guide/iwrap_installation.rst Co-authored-by: Olivier Hoenen --- docs/documentation/installation_guide/iwrap_installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/installation_guide/iwrap_installation.rst b/docs/documentation/installation_guide/iwrap_installation.rst index 3c79617..d01d5cf 100644 --- a/docs/documentation/installation_guide/iwrap_installation.rst +++ b/docs/documentation/installation_guide/iwrap_installation.rst @@ -153,7 +153,7 @@ iWrap Installation Installation Options -------------------- -Basic Installation (Core Only) +Basic Installation (for Python actors only) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Install iWrap core without MUSCLE3 support: From e3ca4c016d1a0c6f0e6899ba54ce3c35487656e3 Mon Sep 17 00:00:00 2001 From: Prasad Date: Fri, 6 Feb 2026 11:01:03 +0100 Subject: [PATCH 27/33] Update docs/documentation/installation_guide/iwrap_installation.rst Co-authored-by: Olivier Hoenen --- docs/documentation/installation_guide/iwrap_installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/installation_guide/iwrap_installation.rst b/docs/documentation/installation_guide/iwrap_installation.rst index d01d5cf..af53ca0 100644 --- a/docs/documentation/installation_guide/iwrap_installation.rst +++ b/docs/documentation/installation_guide/iwrap_installation.rst @@ -156,7 +156,7 @@ Installation Options Basic Installation (for Python actors only) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Install iWrap core without MUSCLE3 support: +Install iWrap without MUSCLE3 support: .. code-block:: bash From ae1f553acec4ebf685580e423b24988e68d4fdc4 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Wed, 11 Feb 2026 17:00:32 +0100 Subject: [PATCH 28/33] fixed documentation --- .github/workflows/publish.yml | 4 +- .readthedocs.yaml | 8 +- docs/_config.yml | 2 - docs/_toc.yml | 17 +- docs/documentation/actor_generation_cmdln.rst | 2 +- docs/documentation/actor_types.rst | 7 +- .../installation_guide/iwrap_installation.rst | 261 +++++++++++++- docs/documentation/muscle3.rst | 52 +++ docs/documentation/muscle3_actors.rst | 42 +-- .../documentation/muscle3_migration_guide.rst | 318 ---------------- .../muscle3_plugins/examples.rst | 338 ------------------ docs/documentation/muscle3_plugins/index.rst | 215 ----------- .../muscle3_plugins/installation.rst | 287 --------------- .../muscle3_resources/actor-ports.png | Bin 0 -> 35487 bytes .../code_wrapping.rst | 14 +- .../example/Makefile | 0 .../example/code_description.yaml | 0 .../example/example.ymmsl | 0 .../example/macro.f90 | 0 .../example/standalone.f90 | 0 .../example/wrapped_code.f90 | 0 .../muscle3_resources/installation.rst | 175 +++++++++ .../muscle3_resources/macro-actor.png | Bin 0 -> 61738 bytes iwrap/iwrap_main.py | 2 +- setup.cfg | 2 +- 25 files changed, 504 insertions(+), 1242 deletions(-) create mode 100644 docs/documentation/muscle3.rst delete mode 100644 docs/documentation/muscle3_migration_guide.rst delete mode 100644 docs/documentation/muscle3_plugins/examples.rst delete mode 100644 docs/documentation/muscle3_plugins/index.rst delete mode 100644 docs/documentation/muscle3_plugins/installation.rst create mode 100644 docs/documentation/muscle3_resources/actor-ports.png rename docs/documentation/{muscle3_plugins => muscle3_resources}/code_wrapping.rst (98%) rename docs/documentation/{muscle3_plugins => muscle3_resources}/example/Makefile (100%) rename docs/documentation/{muscle3_plugins => muscle3_resources}/example/code_description.yaml (100%) rename docs/documentation/{muscle3_plugins => muscle3_resources}/example/example.ymmsl (100%) rename docs/documentation/{muscle3_plugins => muscle3_resources}/example/macro.f90 (100%) rename docs/documentation/{muscle3_plugins => muscle3_resources}/example/standalone.f90 (100%) rename docs/documentation/{muscle3_plugins => muscle3_resources}/example/wrapped_code.f90 (100%) create mode 100644 docs/documentation/muscle3_resources/installation.rst create mode 100644 docs/documentation/muscle3_resources/macro-actor.png diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57f8862..97d6def 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,9 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - # until saxonche is available in 3.13 - # https://saxonica.plan.io/issues/6561 - python-version: "<3.13" + python-version: "3.x" - name: Install pypa/build run: >- python3 -m pip install pip setuptools wheel build diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0f66308..cbca5c3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,11 +7,9 @@ build: os: ubuntu-22.04 tools: python: "3.11" - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/_config.yml - fail_on_warning: false + commands: + - pip install -r docs/requirements.txt + - cd docs && jupyter-book build . # Optionally set the Python requirements used to build documentation python: diff --git a/docs/_config.yml b/docs/_config.yml index 3c02dd1..e1366c8 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -33,8 +33,6 @@ sphinx: html_theme_options: show_navbar_depth: 3 #left sidebar show_toc_level: 3 #rigth sidebar - repo_url: "https://git.iter.org/projects/IMEX/repos/iwrap" - repo_name: "iWrap" suppress_warnings: - "etoc.toctree" diff --git a/docs/_toc.yml b/docs/_toc.yml index 28cfa1c..1b17e40 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -9,15 +9,13 @@ parts: chapters: - file: documentation/quickstart.rst title: Quick Start Guide + - file: documentation/actor_types.rst + title: Actor Types Overview + - file: documentation/muscle3_actors.rst + title: MUSCLE3 Actor Generators - file: documentation/iWrap_intro.rst title: Users Manual sections: - - file: documentation/actor_types.rst - title: Actor Types Overview - - file: documentation/muscle3_actors.rst - title: MUSCLE3 Actor Generators - - file: documentation/muscle3_migration_guide.rst - title: MUSCLE3 Migration Guide - file: documentation/code_standardization.rst sections: - file: documentation/code_standardization/code_standardization_fortran.rst @@ -40,6 +38,7 @@ parts: - file: documentation/developers_manual/05_plugins_compatibility.rst - file: documentation/developers_manual/06_code_desc_compatibility.rst - file: documentation/developers_manual/07_code_parameters_handling.rst + - file: documentation/developers_manual/08_ids_handling.rst - file: documentation/developers_manual/09_system_tests.rst title: System tests sections: @@ -51,6 +50,12 @@ parts: sections: - file: documentation/installation_guide/iwrap_requirements.rst - file: documentation/installation_guide/iwrap_configuration.rst + + - file: documentation/muscle3.rst + title: MUSCLE3 Actors + sections: + - file: documentation/muscle3_resources/installation.rst + - file: documentation/muscle3_resources/code_wrapping.rst - caption: Welcome to Tutorial chapters: diff --git a/docs/documentation/actor_generation_cmdln.rst b/docs/documentation/actor_generation_cmdln.rst index 7835fe0..20436e1 100644 --- a/docs/documentation/actor_generation_cmdln.rst +++ b/docs/documentation/actor_generation_cmdln.rst @@ -35,7 +35,7 @@ Once YAML file is prepared it can be used for generation of an actor using iWrap lists details of given actor type generator -v, --version show program's version number and exit - For more information, visit . + For more information, visit . If YAML file contains both code description and actor description parts, no additional switches are required. diff --git a/docs/documentation/actor_types.rst b/docs/documentation/actor_types.rst index b2b5d7d..f3652b4 100644 --- a/docs/documentation/actor_types.rst +++ b/docs/documentation/actor_types.rst @@ -162,13 +162,10 @@ Feature Comparison +---------------------------+-------------------+-------------------+-------------------+-------------------+ | **Init/Finalize IDS** | ✅ Allowed | ❌ Not allowed | ❌ Not allowed | ❌ Not allowed | +---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Workflow Description** | Python script | yMMSL | yMMSL | yMMSL | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Extra Dependencies** | None | MUSCLE3 | MUSCLE3 | MUSCLE3 | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Performance** | Good | Excellent | Excellent | Excellent | +| **Workflow Description** | XML | yMMSL | yMMSL | yMMSL | +---------------------------+-------------------+-------------------+-------------------+-------------------+ + Usage Examples ======================================== diff --git a/docs/documentation/installation_guide/iwrap_installation.rst b/docs/documentation/installation_guide/iwrap_installation.rst index af53ca0..0f00ca0 100644 --- a/docs/documentation/installation_guide/iwrap_installation.rst +++ b/docs/documentation/installation_guide/iwrap_installation.rst @@ -147,14 +147,40 @@ Load the module into the environment: iwrap-gui -iWrap Installation -================== +iWrap Python Installation +####################################################################################################################### + +MUSCLE3 support is now included in the main iWrap package and can be installed using pip with the ``muscle3`` extra. + + +Requirements +####################################################################################################################### + +Software required to use MUSCLE3 plugins: + +- `Python` +- `iWrap` +- `MUSCLE3` +- `IMAS-Fortran` (if using Fortran actors) +- `IMAS-Cpp` (if using C++ actors) +- `IMAS-Java` (if using Java actors) +- `XMLLib` (for XML parsing) -Installation Options --------------------- +Software required to build documentation: -Basic Installation (for Python actors only) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- `make` +- `Python` +- Python packages: + + - `Sphinx` + - `sphinx-bootstrap-theme` + - `sphinx-rtd-theme` + + +`C and Fortran libmuscle installation `_ + +Overview +================ Install iWrap without MUSCLE3 support: @@ -180,10 +206,27 @@ Or alternatively: pip install iwrap pip install -r requirements_muscle3.txt -This adds the following actor generators: -- MUSCLE3-Python: Generate Python actors with MUSCLE3 coupling -- MUSCLE3-CPP: Generate C++ actors with MUSCLE3 coupling -- MUSCLE3-Fortran: Generate Fortran actors with MUSCLE3 coupling +This will install: + +- iWrap core package +- MUSCLE3-Python actor generator +- MUSCLE3 dependencies (muscle3, ymmsl) + +If you need MUSCLE3-Cpp or MUSCLE3-Fortran actors, install libmuscle separately: + +`C and Fortran libmuscle installation `_ + + +Development Installation +~~~~~~~~~~~~~~~~~~~~~~~~ + +For development, install in editable mode: + +.. code-block:: bash + + git clone https://github.com/iterorganization/iWrap.git + cd iwrap + pip install -e .[muscle3] Full Installation (All Features) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -196,6 +239,99 @@ For development or to install all optional features: This includes MUSCLE3 support and all other optional dependencies. +Installation from Source +####################################################################################################################### + +Using Makefile +========================================================================================= + +The iWrap repository includes a Makefile for building and installing: + +.. code-block:: console + + make all # Build iWrap + make install # Install iWrap + make install_module # Install environment module + +To check all available targets and configuration options: + +.. code-block:: console + + shell> make help + +Variables that can be configured: + +- ``PYTHON_CMD`` - Python interpreter to use (default: python) +- ``INSTALL_PREFIX`` - Installation directory prefix +- ``INSTALL_MOD`` - Module file installation directory +- ``VERSION`` - iWrap version (auto-detected from git) + +Verification +####################################################################################################################### + +Verification of MUSCLE3 Plugins Installation +========================================================================================= + +Once iWrap is installed with MUSCLE3 support, verify that the MUSCLE3 actor generators are available: + +.. code-block:: console + + shell> iwrap --list-actor-types + + Id : Name : Description + ---------------------------------------------------------------------- + python : Simple Python actor : Simple Python actor + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + +If the MUSCLE3 actor types are listed, the installation was successful. + +Verify Python Imports +========================================================================================= + +You can also verify that the MUSCLE3 generators can be imported: + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_python import M3PythonActor + from iwrap.generators.actor_generators.muscle3_common import m3_utils + +If these imports succeed without errors, the MUSCLE3 plugins are correctly installed. + +Manual Environment Setup +======================== + +If you're not using the provided scripts, ensure the following are available: + +1. **MUSCLE3 Libraries**: Available via pkg-config + + .. code-block:: bash + + pkg-config --modversion muscle3 + + * libmuscle (CPP + Fortran) — `MUSCLE3 `_ (if using MUSCLE3 actors) + +2. **IMAS Access Layer**: Properly configured + + .. code-block:: bash + + module load IMAS-Matlab + module load IMAS-Fortran + module load IMAS-Cpp + module load IMAS-Java + + See: `IMAS-Matlab `_, + `IMAS-Fortran `_, + `IMAS-Cpp `_, + `IMAS-Java `_ + +3. **Python Path**: iWrap should be in your PYTHONPATH (automatically handled by pip install) + +4. **Extras**: Properly configured + + .. code-block:: bash + + module load XMLLib + Verifying Installation ~~~~~~~~~~~~~~~~~~~~~~ @@ -211,8 +347,105 @@ Expected output with MUSCLE3 installed: Id : Name : Description ---------------------------------------------------------------------- - MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model - MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model - MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model - python : python : python + python : Simple Python actor : Simple Python actor + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + +You can also verify that the MUSCLE3 generators can be imported: + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_python import M3PythonActor + from iwrap.generators.actor_generators.muscle3_common import m3_utils + +If these imports succeed without errors, the MUSCLE3 plugins are correctly installed. + +Troubleshooting +####################################################################################################################### + +MUSCLE3 Actor Types Not Listed +=============================== + +If ``iwrap --list-actor-types`` doesn't show MUSCLE3 actor types: + +1. Verify MUSCLE3 extra was installed: + + .. code-block:: bash + + pip show iwrap | grep muscle3 + +2. Reinstall with MUSCLE3 support: + + .. code-block:: bash + + pip install --force-reinstall iwrap[muscle3] + +Import Errors +============= + +If you get import errors when trying to use MUSCLE3 generators: + +1. Verify iWrap is installed: + + .. code-block:: bash + + python -c "import iwrap; print(iwrap.__version__)" + +2. Verify MUSCLE3 dependencies are installed: + + .. code-block:: bash + + python -c "import muscle3; print(muscle3.__version__)" + +3. Check your Python environment is correct: + + .. code-block:: bash + + which python + which iwrap + +MUSCLE3 Library Not Found +========================= + +If you get errors about MUSCLE3 libraries not being found during actor compilation: + +1. Verify MUSCLE3 is available via pkg-config: + + .. code-block:: bash + + pkg-config --modversion muscle3 + pkg-config --cflags muscle3 + pkg-config --libs muscle3 + +2. Load the MUSCLE3 module (if using environment modules): + + .. code-block:: bash + + module load muscle3 + +3. Check the MUSCLE3 installation documentation for your platform. + + +Migration from Separate Plugin Package +####################################################################################################################### + +If you were previously using the separate ``iwrap-plugins-muscle3`` package: + +Import Path Changes +========================================================================================= + +Update your code to use the new import paths: + +**Old (separate plugin):** + +.. code-block:: python + + from iwrap_plugins.iwrap_actor_generator.muscle3_python import M3PythonActor + +**New (integrated):** + +.. code-block:: python + + from iwrap.generators.actor_generators.muscle3_python import M3PythonActor + + diff --git a/docs/documentation/muscle3.rst b/docs/documentation/muscle3.rst new file mode 100644 index 0000000..696d79e --- /dev/null +++ b/docs/documentation/muscle3.rst @@ -0,0 +1,52 @@ +####################################################################################################################### +MUSCLE3 Actors +####################################################################################################################### + +.. toctree:: + :maxdepth: 10 + :caption: MUSCLE3 Actors + +.. note:: + + MUSCLE3 + MUSCLE3 is a coupling library for building multiscale simulations out of single-scale models. + It is the third incarnation of the MUSCLE Multiscale Coupling Library and Environment, + developed by the e-MUSC project of the University of Amsterdam and the Netherlands eScience Center. + For any details related to MUSCLE3, please take a look at documentation + available `here `_ . + + + MUSCLE3 Actor + A standalone program, generated by iWrap, containing native code calls wrapped into MUSCLE3 function instance + + +In most cases, MUSCLE3 actors are implemented manually, through calls to the MUSCLE3 API in an ad-hoc manner. +However, for simpler cases it is possible to generate actors automatically using iWrap component generator. + +The iWrap is extended to generate MUSCLE3 actors from any code that follows iWrap standardized code specification, +without changes of the code API. The MUSCLE3 wrapper is one of ‘actor generators’ provided by the iWrap. + +The iWrap is able to wrap native code provided in Python, C++ and Fortran, producing either a standalone binary +executable (C++ and Fortran) or a wrapping script (Python). + +.. toctree:: + :maxdepth: 10 + :caption: Code wrapping + + muscle3_resources/code_wrapping.rst + +.. toctree:: + :maxdepth: 10 + :caption: Installation guide + + muscle3_resources/installation.rst + +.. note:: Further reading + + - `MUSCLE3 documentation `_ + - `MUSCLE3 repository `_ + + + + + diff --git a/docs/documentation/muscle3_actors.rst b/docs/documentation/muscle3_actors.rst index 24b9360..fc08076 100644 --- a/docs/documentation/muscle3_actors.rst +++ b/docs/documentation/muscle3_actors.rst @@ -5,7 +5,8 @@ MUSCLE3 Actor Generators Overview ======================================== -iWrap includes built-in support for generating MUSCLE3 actors, enabling multiscale and multiphysics coupling scenarios. MUSCLE3 (Multiscale Coupling Library and Environment) is a high-performance coupling framework designed for complex scientific simulations. +iWrap includes built-in support for generating MUSCLE3 actors, enabling multiscale and multiphysics coupling scenarios. +MUSCLE3 (Multiscale Coupling Library and Environment) is a high-performance coupling framework designed for complex scientific simulations. Available MUSCLE3 Generators ======================================== @@ -36,43 +37,6 @@ Generate Fortran actors with MUSCLE3 coupling capabilities. **Actor Type ID:** ``MUSCLE3-Fortran`` -Installation -======================================== - -MUSCLE3 support requires the MUSCLE3 library to be installed. - -Installing iWrap with MUSCLE3 Support --------------------------------------- - -.. code-block:: bash - - pip install iwrap[muscle3] - -This will install: -- iWrap core -- MUSCLE3 Python library (>= 0.7.0) -- Enable all three MUSCLE3 actor generators - -Verifying Installation ----------------------- - -Check that MUSCLE3 generators are available: - -.. code-block:: bash - - iwrap --list-actor-types - -Expected output: - -.. code-block:: text - - Id : Name : Description - ---------------------------------------------------------------------- - MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model - MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model - MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model - python : python : python - Usage ======================================== @@ -199,7 +163,7 @@ MUSCLE3 actors differ from standard Python actors in several ways: +===========================+=========================+=========================+ | Coupling Framework | Direct Python calls | MUSCLE3 messaging | +---------------------------+-------------------------+-------------------------+ -| Workflow Description | Python script | yMMSL file | +| Workflow Description | XML file | yMMSL file | +---------------------------+-------------------------+-------------------------+ | Data Exchange | Direct IDS passing | MUSCLE3 ports | +---------------------------+-------------------------+-------------------------+ diff --git a/docs/documentation/muscle3_migration_guide.rst b/docs/documentation/muscle3_migration_guide.rst deleted file mode 100644 index b42a590..0000000 --- a/docs/documentation/muscle3_migration_guide.rst +++ /dev/null @@ -1,318 +0,0 @@ -==================================================== -Migration Guide: MUSCLE3 Plugin Integration -==================================================== - -This guide helps users and developers migrate from the external ``iwrap-plugins-muscle3`` package to the new built-in MUSCLE3 generators in iWrap core. - -What Changed? -======================================== - -MUSCLE3 Actor Generators are Now Built-in ------------------------------------------- - -Previously, MUSCLE3 support was provided through a separate ``iwrap-plugins-muscle3`` package. -Now, MUSCLE3 generators are **built directly into iWrap core**, making installation and usage simpler. - -**Before:** - -.. code-block:: bash - - # Old installation (two packages) - pip install iwrap - pip install iwrap-plugins-muscle3 - -**After:** - -.. code-block:: bash - - # New installation (single package with optional feature) - pip install iwrap[muscle3] - -Benefits --------- - -✅ **Simpler Installation:** One command instead of two packages - -✅ **Unified Version Management:** Core and MUSCLE3 generators always compatible - -✅ **Better Integration:** MUSCLE3 generators maintained alongside core - -✅ **Easier Updates:** Update both core and MUSCLE3 features together - -Migration Steps -======================================== - -For End Users -------------- - -**Step 1: Uninstall Old Plugin** - -.. code-block:: bash - - pip uninstall iwrap-plugins-muscle3 - -**Step 2: Update iWrap** - -.. code-block:: bash - - pip install --upgrade iwrap[muscle3] - -**Step 3: Verify** - -.. code-block:: bash - - iwrap --list-actor-types - -You should see all MUSCLE3 generators listed. - -**Step 4: Test Your Workflows** - -Your existing YAML files and workflows should work without changes: - -.. code-block:: bash - - # This should work exactly as before - iwrap -a my_actor -t MUSCLE3-Python -f my_code.yaml - -For Developers --------------- - -**Step 1: Update Development Environment** - -.. code-block:: bash - - # Remove old plugin - pip uninstall iwrap-plugins-muscle3 - - # Install new version in development mode - cd /path/to/iwrap - pip install -e .[all] - -**Step 2: Update Import Statements (if you extended generators)** - -If you created custom generators based on MUSCLE3, update imports: - -**Before:** - -.. code-block:: python - - from iwrap_plugins.iwrap_actor_generator.muscle3_common import m3_utils - -**After:** - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_common import m3_utils - -**Step 3: Update Tests** - -Test discovery may need updates if you had custom tests: - -.. code-block:: python - - # Old test path - from iwrap_plugins.iwrap_actor_generator.muscle3_python import PythonActorGenerator - - # New test path - from iwrap.generators.actor_generators.muscle3_python.m3_python_actor import PythonActorGenerator - -For CI/CD Pipelines -------------------- - -**Update CI Configuration** - -**Before (.gitlab-ci.yml / .github/workflows):** - -.. code-block:: yaml - - install: - script: - - pip install iwrap - - pip install iwrap-plugins-muscle3 - -**After:** - -.. code-block:: yaml - - install: - script: - - pip install iwrap[muscle3] - -**Update Test Matrix** - -Consider testing with and without MUSCLE3: - -.. code-block:: yaml - - test-core: - script: - - pip install iwrap # Core only - - pytest -m "not muscle3" - - test-with-muscle3: - script: - - pip install iwrap[muscle3] - - pytest # All tests including MUSCLE3 - -Compatibility -======================================== - -Backward Compatibility ----------------------- - -✅ **YAML Files:** All existing code description YAML files work without changes - -✅ **Actor Types:** Same actor type IDs (``MUSCLE3-Python``, ``MUSCLE3-CPP``, ``MUSCLE3-Fortran``) - -✅ **Command Line:** Same CLI arguments and options - -✅ **Workflows:** Existing yMMSL workflow files work without changes - -✅ **Generated Actors:** Same actor structure and behavior - -API Version ------------ - -MUSCLE3 generators updated from API 2.0 to API 2.1 to match iWrap core. - -This is an internal change and should not affect end users. - -Breaking Changes ----------------- - -⚠️ **None for end users** - -⚠️ **For developers:** Import paths changed (see above) - -Troubleshooting -======================================== - -Issue: Both Old and New Installed ----------------------------------- - -**Symptom:** Conflicts or duplicate generators listed - -**Solution:** - -.. code-block:: bash - - # Remove both - pip uninstall iwrap iwrap-plugins-muscle3 - - # Reinstall fresh - pip install iwrap[muscle3] - -Issue: MUSCLE3 Generators Not Found ------------------------------------- - -**Symptom:** Only ``python`` generator listed, no MUSCLE3 - -**Check:** - -.. code-block:: bash - - # Verify MUSCLE3 extra was installed - pip show iwrap - - # Look for: Requires: muscle3>=0.7.0 - -**Solution:** - -.. code-block:: bash - - pip install muscle3>=0.7.0 - # OR - pip install --upgrade iwrap[muscle3] - -Issue: Import Errors in Custom Code ------------------------------------- - -**Symptom:** ``ModuleNotFoundError: No module named 'iwrap_plugins'`` - -**Cause:** Custom code still using old import paths - -**Solution:** Update imports as shown in "For Developers" section - -Frequently Asked Questions -======================================== - -Can I still use the old plugin? --------------------------------- - -No. The ``iwrap-plugins-muscle3`` package is deprecated and will not receive updates. -Please migrate to the built-in MUSCLE3 generators. - -Do I need to change my YAML files? ------------------------------------ - -No. All existing code description YAML files work without any changes. - -Will my old workflows break? ------------------------------ - -No. Your yMMSL workflow files and Python scripts work without changes. - -What if I don't need MUSCLE3? ------------------------------- - -Simply install iWrap without the ``[muscle3]`` extra: - -.. code-block:: bash - - pip install iwrap - -MUSCLE3 generators won't be available, but core functionality works perfectly. - -Can I install MUSCLE3 support later? -------------------------------------- - -Yes: - -.. code-block:: bash - - # Install core first - pip install iwrap - - # Add MUSCLE3 support later - pip install muscle3>=0.7.0 - -What about future plugins? ---------------------------- - -iWrap still supports external plugins! The plugin discovery mechanism is unchanged. -Future plugin developers can still create ``iwrap_actor_generator`` namespace packages. - -Timeline and Support -======================================== - -Release Timeline ----------------- - -- **iWrap v0.7.x and earlier:** External ``iwrap-plugins-muscle3`` required -- **iWrap v0.8.0+:** MUSCLE3 built-in, plugin deprecated -- **Future:** Plugin package will be archived (read-only) - -Support Policy --------------- - -- **Built-in MUSCLE3:** Fully supported, actively maintained -- **External plugin:** Deprecated, no new features, critical bugs only -- **After 6 months:** External plugin unsupported - -Getting Help -======================================== - -If you encounter issues during migration: - -1. Check this migration guide -2. Review the :doc:`muscle3_actors` documentation -3. Check existing issues on the iWrap repository -4. Contact: iWrap Development Team - -Additional Resources -======================================== - -- :doc:`muscle3_actors` - Complete MUSCLE3 documentation -- :doc:`installation_guide/iwrap_installation` - Installation instructions -- :doc:`developers_manual/04_adding_generators` - Creating custom generators -- `MUSCLE3 Documentation `_ diff --git a/docs/documentation/muscle3_plugins/examples.rst b/docs/documentation/muscle3_plugins/examples.rst deleted file mode 100644 index 79adcdd..0000000 --- a/docs/documentation/muscle3_plugins/examples.rst +++ /dev/null @@ -1,338 +0,0 @@ -MUSCLE3 Examples -================ - -This section provides complete working examples of MUSCLE3 actor generation -and usage with iWrap. - -Basic Fortran Example ---------------------- - -This example demonstrates wrapping a simple Fortran code as a MUSCLE3 actor. -The example shows a complete workflow from code description to running a -coupled simulation. - -Example Files -~~~~~~~~~~~~~ - -All example files are located in ``docs/documentation/muscle3_plugins/example/``: - -* ``code_description.yaml`` - Actor description for iWrap -* ``example.ymmsl`` - MUSCLE3 workflow configuration -* ``macro.f90`` - Macro model (orchestrator) -* ``standalone.f90`` - Original standalone code -* ``wrapped_code.f90`` - Code prepared for wrapping -* ``Makefile`` - Build instructions - -Code Description -~~~~~~~~~~~~~~~~ - -The code description YAML file defines the actor interface: - -.. literalinclude:: example/code_description.yaml - :language: yaml - :caption: code_description.yaml - -The code description defines: - -* **Actor name and type**: Identifies the actor and generator to use -* **Input/output ports**: Defines data exchange interfaces -* **Code parameters**: Configuration options for the wrapped code -* **Implementation details**: Language, source files, build options - -Workflow Configuration -~~~~~~~~~~~~~~~~~~~~~~ - -The yMMSL file defines the coupled simulation workflow: - -.. literalinclude:: example/example.ymmsl - :language: yaml - :caption: example.ymmsl - -The yMMSL file defines: - -* **Workflow components**: Macro and micro models -* **Port connections**: How actors exchange data -* **Simulation parameters**: Runtime configuration -* **Resources**: Computational resources for each component - -Wrapped Code -~~~~~~~~~~~~ - -The Fortran code to be wrapped: - -.. literalinclude:: example/wrapped_code.f90 - :language: fortran - :caption: wrapped_code.f90 - -This code implements the physics model that will be wrapped as a MUSCLE3 actor. - -Macro Model -~~~~~~~~~~~ - -The macro model orchestrates the coupled simulation: - -.. literalinclude:: example/macro.f90 - :language: fortran - :caption: macro.f90 - -The macro model: - -* Initializes the simulation -* Sends data to the micro model (wrapped code) -* Receives results from the micro model -* Manages the simulation loop - -Building the Example -~~~~~~~~~~~~~~~~~~~~ - -Step 1: Generate the MUSCLE3 Actor -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash - - cd docs/documentation/muscle3_plugins/example - - # Generate the MUSCLE3 actor - iwrap --actor-type muscle3_fortran \ - --file code_description.yaml \ - --install-dir ./m3_actor - -This command: - -* Reads the code description -* Generates MUSCLE3 wrapper code -* Creates build system (Makefile) -* Sets up the actor directory structure - -Step 2: Build the Actor -^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash - - cd m3_actor - make - -This compiles: - -* The wrapped physics code -* The MUSCLE3 wrapper -* All dependencies - -Step 3: Build the Macro Model -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash - - cd ../ - make macro - -This compiles the macro model that will orchestrate the simulation. - -Step 4: Run the Coupled Simulation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash - - muscle_manager --start-all example.ymmsl - -This: - -* Starts the MUSCLE3 manager -* Launches both macro and micro models -* Manages data exchange -* Runs the coupled simulation - -Expected Output -~~~~~~~~~~~~~~~ - -The simulation should produce output showing: - -.. code-block:: text - - [MUSCLE3] Starting simulation... - [Macro] Initializing macro model - [Macro] Sending data to micro model - [Micro] Received data from macro - [Micro] Processing... - [Micro] Sending results to macro - [Macro] Received results from micro - [MUSCLE3] Simulation complete - -Output files will be created in a ``run_*`` directory containing: - -* Simulation logs -* Output data -* Performance metrics -* Snapshots (if checkpointing enabled) - -More Examples -------------- - -The test suite contains additional examples for different scenarios: - -Python Examples -~~~~~~~~~~~~~~~ - -Located in ``tests/muscle3/actors/python/``: - -* **basic_python**: Simple Python actor -* **restart_python**: Python actor with restart support - -C++ Examples -~~~~~~~~~~~~ - -Located in ``tests/muscle3/actors/cpp/``: - -* **basic_cpp**: Simple C++ actor -* **basic_mpi_cpp**: MPI-parallel C++ actor -* **restart_cpp**: C++ actor with restart support -* **restart_mpi_cpp**: MPI-parallel C++ actor with restart - -Fortran Examples -~~~~~~~~~~~~~~~~ - -Located in ``tests/muscle3/actors/fortran/``: - -* **code_lifecycle**: Complete lifecycle example -* **basic_mpi**: MPI-parallel Fortran actor -* **code_restart**: Fortran actor with restart support -* **code_restart_mpi**: MPI-parallel Fortran actor with restart - -Running Test Examples -~~~~~~~~~~~~~~~~~~~~~ - -Each test example can be run using its Makefile: - -.. code-block:: bash - - cd tests/muscle3/actors// - make test - -This will: - -1. Build the wrapped code -2. Generate the MUSCLE3 actor -3. Run the coupled simulation -4. Validate the output - -Example: Running the Python Basic Test -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash - - cd tests/muscle3/actors/python/basic_python - make test - -Advanced Features ------------------ - -MPI Parallelization -~~~~~~~~~~~~~~~~~~~ - -For parallel codes, the MUSCLE3 generators support MPI: - -.. code-block:: yaml - - # In code_description.yaml - implementation: - language: fortran - parallel: mpi - mpi_ranks: 4 - -See the ``basic_mpi`` examples for complete implementations. - -Restart/Checkpoint Support -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Enable restart support in your code description: - -.. code-block:: yaml - - # In code_description.yaml - features: - restart: true - checkpoint_interval: 10 - -See the ``restart_*`` examples for complete implementations. - -Custom Parameters -~~~~~~~~~~~~~~~~~ - -Define custom parameters for your code: - -.. code-block:: yaml - - # In code_description.yaml - parameters: - - name: timestep - type: float - default: 0.01 - - name: max_iterations - type: integer - default: 100 - -IMAS Integration -~~~~~~~~~~~~~~~~ - -For IMAS-based codes, specify IDS usage: - -.. code-block:: yaml - - # In code_description.yaml - ports: - input: - - name: equilibrium_in - ids: equilibrium - output: - - name: equilibrium_out - ids: equilibrium - -Troubleshooting ---------------- - -Actor Generation Fails -~~~~~~~~~~~~~~~~~~~~~~~ - -If ``iwrap`` fails to generate the actor: - -1. Check the code description YAML syntax -2. Verify all required fields are present -3. Check iWrap logs for detailed error messages -4. Ensure MUSCLE3 plugins are installed: ``iwrap --list-actor-types`` - -Build Fails -~~~~~~~~~~~ - -If the actor fails to build: - -1. Verify MUSCLE3 libraries are available: ``pkg-config --modversion muscle3`` -2. Check compiler is available: ``gfortran --version`` or ``g++ --version`` -3. Review build logs in the actor directory -4. Ensure all source files are present - -Runtime Errors -~~~~~~~~~~~~~~ - -If the simulation fails at runtime: - -1. Check MUSCLE3 manager logs -2. Verify yMMSL workflow configuration -3. Ensure port connections are correct -4. Check for MPI configuration issues (if using MPI) - -Next Steps ----------- - -* Adapt the basic example to your own code -* Explore advanced features in the :doc:`code_wrapping` guide -* Review the :doc:`../muscle3_actors` documentation -* Check the test suite for more examples -* Consult the `MUSCLE3 documentation `_ - -See Also --------- - -* :doc:`installation` - Installation instructions -* :doc:`code_wrapping` - Detailed code wrapping guide -* :doc:`../muscle3_actors` - Overview of MUSCLE3 actors -* :doc:`../muscle3_migration_guide` - Migration guide diff --git a/docs/documentation/muscle3_plugins/index.rst b/docs/documentation/muscle3_plugins/index.rst deleted file mode 100644 index f3aa0fd..0000000 --- a/docs/documentation/muscle3_plugins/index.rst +++ /dev/null @@ -1,215 +0,0 @@ -MUSCLE3 Plugins Documentation -============================== - -This section provides detailed documentation for the MUSCLE3 actor generators -integrated into iWrap. - -.. note:: - As of iWrap version 0.8.0, MUSCLE3 plugins have been integrated into the - main iWrap package. The separate ``iwrap-plugins-muscle3`` repository has - been archived. - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - installation - code_wrapping - examples - -Overview --------- - -The MUSCLE3 plugins provide actor generators for three programming languages: - -* **Python** - ``iwrap.generators.actor_generators.muscle3_python`` -* **C++** - ``iwrap.generators.actor_generators.muscle3_cpp`` -* **Fortran** - ``iwrap.generators.actor_generators.muscle3_fortran`` - -These generators create MUSCLE3-compatible actors from your physics codes, -enabling multiscale coupling through the MUSCLE3 framework. - -What are MUSCLE3 Actors? -------------------------- - -MUSCLE3 (Multiscale Coupling Library and Environment 3) is a framework for -building multiscale coupled simulations. A MUSCLE3 actor is a component in -a coupled simulation that: - -* Communicates with other actors through well-defined ports -* Exchanges data using the MUSCLE3 communication infrastructure -* Can be written in Python, C++, or Fortran -* Follows the MUSCLE3 API conventions - -The iWrap MUSCLE3 plugins automate the process of wrapping your existing -physics codes into MUSCLE3 actors, handling: - -* Port definitions and data exchange -* MUSCLE3 API integration -* Build system generation -* Parameter handling -* Restart/checkpoint support - -Quick Start ------------ - -1. **Install iWrap with MUSCLE3 support:** - - .. code-block:: bash - - pip install iwrap[muscle3] - -2. **Prepare your code description YAML file:** - - Define your code's interface, parameters, and implementation details. - See :doc:`code_wrapping` for detailed instructions. - -3. **Generate a MUSCLE3 actor:** - - .. code-block:: bash - - iwrap --actor-type muscle3_python --file my_code.yaml --install-dir ./actor - - Replace ``muscle3_python`` with ``muscle3_cpp`` or ``muscle3_fortran`` as needed. - -4. **Build the generated actor:** - - .. code-block:: bash - - cd actor - make - -5. **Run your coupled simulation:** - - Create a yMMSL workflow file and run with MUSCLE3: - - .. code-block:: bash - - muscle_manager --start-all my_workflow.ymmsl - -For a complete working example, see :doc:`examples`. - -Supported Languages -------------------- - -Python -~~~~~~ - -**Actor Type:** ``muscle3_python`` - -**Features:** - -* Pure Python implementation -* Easy to debug and modify -* Supports all MUSCLE3 features -* Ideal for prototyping and Python-based codes - -**Import:** - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_python import M3PythonActor - -C++ -~~~ - -**Actor Type:** ``muscle3_cpp`` - -**Features:** - -* High performance -* Direct integration with C++ physics codes -* Supports MPI parallelization -* Automatic build system generation - -**Import:** - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_cpp import M3CppActor - -Fortran -~~~~~~~ - -**Actor Type:** ``muscle3_fortran`` - -**Features:** - -* Native Fortran support -* Optimized for HPC environments -* Supports MPI parallelization -* Compatible with legacy Fortran codes - -**Import:** - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_fortran import M3FortranActor - -Common Utilities -~~~~~~~~~~~~~~~~ - -All generators share common utilities for MUSCLE3 integration: - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_common import m3_utils - -Key Features ------------- - -Automatic Code Generation -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The plugins automatically generate: - -* MUSCLE3 actor wrapper code -* Build system (Makefile) -* Port definitions and data exchange logic -* Parameter handling -* Initialization and finalization code - -IMAS Integration -~~~~~~~~~~~~~~~~ - -Full support for IMAS (Integrated Modelling & Analysis Suite): - -* Automatic IDS (Interface Data Structure) handling -* Data conversion between IMAS and MUSCLE3 formats -* Support for IMAS Access Layer - -MPI Support -~~~~~~~~~~~ - -For parallel codes: - -* MPI initialization and finalization -* Rank-aware data distribution -* Collective operations support - -Restart/Checkpoint -~~~~~~~~~~~~~~~~~~ - -Built-in support for: - -* Saving simulation state -* Restarting from checkpoints -* Snapshot management - -See Also --------- - -* :doc:`../muscle3_actors` - Overview of MUSCLE3 actors in iWrap -* :doc:`../muscle3_migration_guide` - Migration guide for existing users -* :doc:`installation` - Installation instructions -* :doc:`code_wrapping` - Detailed code wrapping guide -* :doc:`examples` - Complete working examples - -External Resources ------------------- - -* `MUSCLE3 Documentation `_ -* `IMAS Documentation `_ -* `iWrap Repository `_ diff --git a/docs/documentation/muscle3_plugins/installation.rst b/docs/documentation/muscle3_plugins/installation.rst deleted file mode 100644 index 109bacc..0000000 --- a/docs/documentation/muscle3_plugins/installation.rst +++ /dev/null @@ -1,287 +0,0 @@ -.. sectnum:: - -.. toctree:: - -Installation -####################################################################################################################### - -.. note:: - As of iWrap version 0.8.0, MUSCLE3 plugins have been integrated into the main iWrap package. - The separate ``iwrap-plugins-muscle3`` repository has been archived. - -MUSCLE3 support is now included in the main iWrap package and can be installed using pip with the ``muscle3`` extra. - -Requirements -####################################################################################################################### - -Software required to use MUSCLE3 plugins: - -- `Python` >= 3.6 -- `iWrap` >= 0.7.0 -- `MUSCLE3` >= 0.7.0 -- `IMAS` Access Layer >= 4.11 (for IMAS-based codes) - -Software required to build documentation: - -- `make` >= 3.82 -- `Python` >= 3.8.6 -- Python packages: - - - `Sphinx` >= 3.2.1 - - `sphinx-bootstrap-theme` >= 0.7.1 - - `sphinx-rtd-theme` >= 1.0.0 - -Installation via pip -####################################################################################################################### - -Standard Installation -========================================================================================= - -To install iWrap with MUSCLE3 support: - -.. code-block:: console - - pip install iwrap[muscle3] - -This will install: - -- iWrap core package -- MUSCLE3 actor generators (Python, C++, Fortran) -- MUSCLE3 dependencies (muscle3, ymmsl) - -Development Installation -========================================================================================= - -For development, install in editable mode: - -.. code-block:: console - - git clone https://git.iter.org/projects/IMEX/repos/iwrap - cd iwrap - pip install -e .[muscle3] - -Installation from Source -####################################################################################################################### - -Using Makefile -========================================================================================= - -The iWrap repository includes a Makefile for building and installing: - -.. code-block:: console - - make all # Build iWrap - make install # Install iWrap - make install_module # Install environment module - -To check all available targets and configuration options: - -.. code-block:: console - - shell> make help - -Variables that can be configured: - -- ``PYTHON_CMD`` - Python interpreter to use (default: python) -- ``INSTALL_PREFIX`` - Installation directory prefix -- ``INSTALL_MOD`` - Module file installation directory -- ``VERSION`` - iWrap version (auto-detected from git) - -Verification -####################################################################################################################### - -Verification of MUSCLE3 Plugins Installation -========================================================================================= - -Once iWrap is installed with MUSCLE3 support, verify that the MUSCLE3 actor generators are available: - -.. code-block:: console - - shell> iwrap --list-actor-types - - Id : Name : Description - ---------------------------------------------------------------------- - python : Simple Python actor : Simple Python actor - MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model - MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model - MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model - -If the MUSCLE3 actor types are listed, the installation was successful. - -Verify Python Imports -========================================================================================= - -You can also verify that the MUSCLE3 generators can be imported: - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_python import M3PythonActor - from iwrap.generators.actor_generators.muscle3_cpp import M3CppActor - from iwrap.generators.actor_generators.muscle3_fortran import M3FortranActor - from iwrap.generators.actor_generators.muscle3_common import m3_utils - -If these imports succeed without errors, the MUSCLE3 plugins are correctly installed. - -Environment Setup -####################################################################################################################### - -ITER SDCC -========================================================================================= - -For the ITER Organisation computing cluster (SDCC), use the provided configuration script: - -.. code-block:: console - - source set-iter.sh - -This script will: - -- Load required modules (iWrap, MUSCLE3, IMAS) -- Set up environment variables -- Configure PYTHONPATH - -EUROfusion Gateway -========================================================================================= - -For the EUROfusion Gateway, use: - -.. code-block:: console - - source set-gw.sh - -This script provides similar functionality for the Gateway environment. - -Manual Environment Setup -========================================================================================= - -If you're not using the provided scripts, ensure the following are available: - -1. **MUSCLE3 Libraries**: Available via pkg-config - - .. code-block:: console - - pkg-config --modversion muscle3 - -2. **IMAS Access Layer**: Properly configured - - .. code-block:: console - - module load imas - -3. **Python Path**: iWrap should be in your PYTHONPATH (automatically handled by pip install) - -Migration from Separate Plugin Package -####################################################################################################################### - -If you were previously using the separate ``iwrap-plugins-muscle3`` package: - -Old Installation -========================================================================================= - -.. code-block:: console - - pip install iwrap - pip install iwrap-plugins-muscle3 - -New Installation -========================================================================================= - -.. code-block:: console - - pip install iwrap[muscle3] - -Import Path Changes -========================================================================================= - -Update your code to use the new import paths: - -**Old (separate plugin):** - -.. code-block:: python - - from iwrap_plugins.iwrap_actor_generator.muscle3_python import M3PythonActor - -**New (integrated):** - -.. code-block:: python - - from iwrap.generators.actor_generators.muscle3_python import M3PythonActor - -See the :doc:`../muscle3_migration_guide` for more details on migrating from the separate plugin package. - -Uninstall -####################################################################################################################### - -To uninstall iWrap: - -.. code-block:: console - - pip uninstall iwrap - -This will remove both the core iWrap package and the integrated MUSCLE3 plugins. - -Troubleshooting -####################################################################################################################### - -MUSCLE3 Actor Types Not Listed -========================================================================================= - -If ``iwrap --list-actor-types`` doesn't show MUSCLE3 actor types: - -1. Verify MUSCLE3 extra was installed: - - .. code-block:: console - - pip show iwrap | grep muscle3 - -2. Reinstall with MUSCLE3 support: - - .. code-block:: console - - pip install --force-reinstall iwrap[muscle3] - -Import Errors -========================================================================================= - -If you get import errors when trying to use MUSCLE3 generators: - -1. Verify iWrap is installed: - - .. code-block:: console - - python -c "import iwrap; print(iwrap.__version__)" - -2. Verify MUSCLE3 dependencies are installed: - - .. code-block:: console - - python -c "import muscle3; print(muscle3.__version__)" - -3. Check your Python environment is correct: - - .. code-block:: console - - which python - which iwrap - -MUSCLE3 Library Not Found -========================================================================================= - -If you get errors about MUSCLE3 libraries not being found during actor compilation: - -1. Verify MUSCLE3 is available via pkg-config: - - .. code-block:: console - - pkg-config --modversion muscle3 - pkg-config --cflags muscle3 - pkg-config --libs muscle3 - -2. Load the MUSCLE3 module (if using environment modules): - - .. code-block:: console - - module load muscle3 - -3. Check the MUSCLE3 installation documentation for your platform. - diff --git a/docs/documentation/muscle3_resources/actor-ports.png b/docs/documentation/muscle3_resources/actor-ports.png new file mode 100644 index 0000000000000000000000000000000000000000..48e51ac52176f61501a57776611d661cf012377d GIT binary patch literal 35487 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 literal 0 HcmV?d00001 diff --git a/docs/documentation/muscle3_plugins/code_wrapping.rst b/docs/documentation/muscle3_resources/code_wrapping.rst similarity index 98% rename from docs/documentation/muscle3_plugins/code_wrapping.rst rename to docs/documentation/muscle3_resources/code_wrapping.rst index 8425e14..662b4e4 100644 --- a/docs/documentation/muscle3_plugins/code_wrapping.rst +++ b/docs/documentation/muscle3_resources/code_wrapping.rst @@ -375,7 +375,7 @@ This example shows how to prepare and run an MUSCLE3 computing scenario, consist - prepared manually *MUSCLE3 macro model* - a native code wrapped automatically by iWrap into *MUSCLE3 function* -.. image:: ../../images/muscle3/macro-actor.png +.. image:: macro-actor.png Wrapping code into MUSCLE3 function @@ -385,7 +385,7 @@ A code to be wrapped is a very simple Fortran subroutine, obtaining `core_profil and returning `distribution_sources` IDS. To be compatible with 'iWrap standardized API' argument list contains also `status_code` and `status_message`. -.. literalinclude:: /resources/example/wrapped_code.f90 +.. literalinclude:: example/wrapped_code.f90 :language: fortran :caption: wrapped_code.f90 @@ -400,7 +400,7 @@ The wrapped code has to be compiled and packed into a static library. Once a static library is built, the code description has to to be prepared, to provide iWrap with all information required to generate an actor. -.. literalinclude:: /resources/example/code_description.yaml +.. literalinclude:: example/code_description.yaml :language: YAML :caption: code_description.yaml @@ -415,12 +415,12 @@ an arbitrary actor name and YAML file containing the code description. If generation was successful, an executable `m3_actor.exe` containing user provided subroutine wrapped into MUSCLE3 function, is created in `/m3_actor/bin/` directory. -.. image:: ../../images/muscle3/actor-ports.png +.. image:: actor-ports.png .. note:: Please notice, that ports of generated actor have exactly the same names as they have been defined in code description YAML -.. literalinclude:: /resources/example/standalone.f90 +.. literalinclude:: example/standalone.f90 :language: fortran :caption: Autogenerated code @@ -429,7 +429,7 @@ Macro model A `macro model` code has to be prepared manually. This code "triggers" a generated actor sending to it `core_profiles` IDS and receiving back computed `distribution_sources` IDS -.. literalinclude:: /resources/example/macro.f90 +.. literalinclude:: example/macro.f90 :language: fortran :caption: macro.f90 @@ -443,7 +443,7 @@ Scenario description ========================================================================================= -.. literalinclude:: /resources/example/example.ymmsl +.. literalinclude:: example/example.ymmsl :language: YAML :caption: example.ymmsl diff --git a/docs/documentation/muscle3_plugins/example/Makefile b/docs/documentation/muscle3_resources/example/Makefile similarity index 100% rename from docs/documentation/muscle3_plugins/example/Makefile rename to docs/documentation/muscle3_resources/example/Makefile diff --git a/docs/documentation/muscle3_plugins/example/code_description.yaml b/docs/documentation/muscle3_resources/example/code_description.yaml similarity index 100% rename from docs/documentation/muscle3_plugins/example/code_description.yaml rename to docs/documentation/muscle3_resources/example/code_description.yaml diff --git a/docs/documentation/muscle3_plugins/example/example.ymmsl b/docs/documentation/muscle3_resources/example/example.ymmsl similarity index 100% rename from docs/documentation/muscle3_plugins/example/example.ymmsl rename to docs/documentation/muscle3_resources/example/example.ymmsl diff --git a/docs/documentation/muscle3_plugins/example/macro.f90 b/docs/documentation/muscle3_resources/example/macro.f90 similarity index 100% rename from docs/documentation/muscle3_plugins/example/macro.f90 rename to docs/documentation/muscle3_resources/example/macro.f90 diff --git a/docs/documentation/muscle3_plugins/example/standalone.f90 b/docs/documentation/muscle3_resources/example/standalone.f90 similarity index 100% rename from docs/documentation/muscle3_plugins/example/standalone.f90 rename to docs/documentation/muscle3_resources/example/standalone.f90 diff --git a/docs/documentation/muscle3_plugins/example/wrapped_code.f90 b/docs/documentation/muscle3_resources/example/wrapped_code.f90 similarity index 100% rename from docs/documentation/muscle3_plugins/example/wrapped_code.f90 rename to docs/documentation/muscle3_resources/example/wrapped_code.f90 diff --git a/docs/documentation/muscle3_resources/installation.rst b/docs/documentation/muscle3_resources/installation.rst new file mode 100644 index 0000000..78debf1 --- /dev/null +++ b/docs/documentation/muscle3_resources/installation.rst @@ -0,0 +1,175 @@ +.. sectnum:: + +.. toctree:: + +Installer functionality +####################################################################################################################### + +iWrap MUSCLE3 plugins installer: + +- Builds WWW pages with manuals from ReStructured Text files using Sphinx +- Copies manuals to chosen installation folder +- Copies plugins (actor generators with set of templates) to chosen installation folder +- Builds module file from template and install it in selected directory + +Requirements +####################################################################################################################### + +Software required to install plugins: + +- `make` >= 3.82 + +Software required to build and install manuals: + +- `make` >= 3.82 +- `Python` >= 3.8.6 +- Python packages: + + - `Sphinx` >= 3.2.1 + - `sphinx-bootstrap-theme` >= 0.7.1 + - `sphinx-rtd-theme` >= 1.0.0 + +Installation +####################################################################################################################### + +iWrap MUSCLE3 plugins installer is based on `make` utility. To check all available targets +and configuration options, `make help` could be run from the console. + +.. code-block:: console + + shell> make help + + Available targets: + install : install plugins and iWrap4paf module (default) + uninstall : uninstall iWrap4paf module + install_plugins : install iWrap4paf plugins + install_module : install iWrap4paf module + clean : remove installation directories + help : display this help message + + Variables: + IWRAP_MODULE_NAME : name of iWrap module [ iwrap ] + MUSCLE3_MODULE_NAME : name of MUSCLE3 module [ muscle3 ] + VERSION : version of iWrap4paf [ 0.1.0 ] + NAME : name of iWrap4paf module [ iwrap4paf ] + INSTALL_PREFIX : path to software installation directory [ $HOME/IWRAP4PAF/install ] + INSTALL_DIR : full path to iWrap4paf installation directory [ $HOME/IWRAP4PAF/install/iwrap4paf/0.1.0 ] + MODULE_NAME : name of module file [ iwrap4paf ] + MODULE_INSTALL_PREFIX : path to module installation directory [ $HOME/IWRAP4PAF/modules ] + MODULE_INSTALL_DIR : full path to module installation directory [ $HOME/IWRAP4PAF/modules/iwrap4paf/0.1.0 ] + + +.. note:: + A command ``make help`` shows not only systems variables that may be configured, + but also their current values. It is strongly recommended to check before installation, + if all variables have correct (wanted) values. + + +Plugins installation +========================================================================================= +To install the plugins a `make install_plugins` command should be executed. + +.. code-block:: console + + shell> make install_plugins + +The installer copies plugins files to a directory defined as: + +.. code-block:: console + + $INSTALL_PREFIX/$NAME/$VERSION + +As it could be easily spotted, the target installation directory could be changed +by setting following system variables: + +- ``INSTALL_PREFIX`` - software install direcotry +- ``NAME`` - name of the plugins pack (may differ on various platforms) +- ``VERSION`` - plugins version (default value is set automatically using ``git describe``) + +Module installation +========================================================================================= +The module could be installed by launching `make install_module` command. + +.. code-block:: console + + shell> make install_module + +The installer builds a module file from the template based on a current values of following system variables: + +- ``IWRAP_MODULE_NAME`` - name of iWrap module (may differ on different platforms) +- ``MUSCLE3_MODULE_NAME`` - name of MSUCLE3 module (may differ on different platforms) +- ``VERSION`` - plugins version (default value is set automatically using ``git describe``) +- ``MODULE_NAME`` - name of module (may differ on different platforms) +- ``MODULE_INSTALL_PREFIX``- path to software modules directory +- ``MODULE_INSTALL_DIR`` - full path to module installation directory + +Verification +####################################################################################################################### + +Verification of the plugins installation +========================================================================================= + +Once plugins are installed, correctness of the installation can be verified by listing +available actor types registered in iWrap: + +.. code-block:: console + + shell> iwrap --list-actor-types + + Id : Name : Description + ---------------------------------------------------------------------- + python : Simple Python actor : Simple Python actor + MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model + MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model + MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model + +.. note:: + Please make sure that paths to all installed plugins are added to ``PYTHONPATH`` including directory containing + common functionality (it is usually done by loading module `iwrap-plugins-muscle3`). Typical configuration usually + looks like follows: + + .. code-block:: shell + + # is the directory where plugins were installed + export PYTHONPATH=/generators/common:${PYTHONPATH} + export PYTHONPATH=/generators/fortran:${PYTHONPATH} + export PYTHONPATH=/generators/cpp:${PYTHONPATH} + export PYTHONPATH=/generators/python:${PYTHONPATH} + + +Verification of the module installation +========================================================================================= + +After installing the module and adding it to MODULEPATH, its availability and correctness +could be checked by listing and loading `iwrap-plugins-muscle3` module + +.. code-block:: shell + + # is the name chosen for the module at the installation stage (default: iwrap-plugins-muscle3) + module available + module load + + + +Uninstall +####################################################################################################################### + +To uninstall software following commands could be run: + +.. code-block:: shell + + make unistall # to uninstall plugins and module + # OR + make uninstall_plugins # to uninstall plugins + make uninstall_module # to uninstall module + + +.. warning:: + + Proper uninstallation of plugins (including their distribution and module file) requires the use + of exactly (!) the same system variables as for the installation process. + + + + + diff --git a/docs/documentation/muscle3_resources/macro-actor.png b/docs/documentation/muscle3_resources/macro-actor.png new file mode 100644 index 0000000000000000000000000000000000000000..cd4508e013c3613aa5d2d21f62bf443b8212a807 GIT binary patch literal 61738 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�e^ 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 Q|HQd0uUMN^Uh;_jKW-hMu>b%7 literal 0 HcmV?d00001 diff --git a/iwrap/iwrap_main.py b/iwrap/iwrap_main.py index af3d02c..b500c14 100644 --- a/iwrap/iwrap_main.py +++ b/iwrap/iwrap_main.py @@ -17,7 +17,7 @@ def get_parser(is_commandline_mode: bool) -> argparse.ArgumentParser: # Management of input arguments parser = argparse.ArgumentParser( description='iWrap - a modular IMAS actor generator, used for creating ' 'standardized actors from a code interfaced with IDSs.', - epilog='For more information, visit .', + epilog='For more information, visit .', # formatter_class=argparse.ArgumentDefaultsHelpFormatter ) diff --git a/setup.cfg b/setup.cfg index 8542445..771cb0d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ license = ITER license description = A modular component generator, implemented in Python, used for creating IMAS actors from physics models. long_description_content_type=text/x-rst long_description = file: README.md -url = https://git.iter.org/projects/IMEX/repos/iwrap +url = https://github.com/iterorganization/iWrap.git [flake8] ignore = From 3c0e1a4ea2bba63a86cf24dea3b19ba3592262e0 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 12 Feb 2026 13:24:08 +0100 Subject: [PATCH 29/33] removed generated documents and kept original documents from muscle3 plugin repo --- docs/_toc.yml | 5 - docs/documentation/actor_types.rst | 234 -------------- docs/documentation/muscle3.rst | 4 - docs/documentation/muscle3_actors.rst | 292 ------------------ .../muscle3_resources/installation.rst | 175 ----------- docs/documentation/quickstart.rst | 137 +------- iwrap/iwrap_main.py | 2 +- 7 files changed, 4 insertions(+), 845 deletions(-) delete mode 100644 docs/documentation/actor_types.rst delete mode 100644 docs/documentation/muscle3_actors.rst delete mode 100644 docs/documentation/muscle3_resources/installation.rst diff --git a/docs/_toc.yml b/docs/_toc.yml index 1b17e40..3c909ce 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -9,10 +9,6 @@ parts: chapters: - file: documentation/quickstart.rst title: Quick Start Guide - - file: documentation/actor_types.rst - title: Actor Types Overview - - file: documentation/muscle3_actors.rst - title: MUSCLE3 Actor Generators - file: documentation/iWrap_intro.rst title: Users Manual sections: @@ -54,7 +50,6 @@ parts: - file: documentation/muscle3.rst title: MUSCLE3 Actors sections: - - file: documentation/muscle3_resources/installation.rst - file: documentation/muscle3_resources/code_wrapping.rst - caption: Welcome to Tutorial diff --git a/docs/documentation/actor_types.rst b/docs/documentation/actor_types.rst deleted file mode 100644 index f3652b4..0000000 --- a/docs/documentation/actor_types.rst +++ /dev/null @@ -1,234 +0,0 @@ -======================================== -Actor Types Overview -======================================== - -iWrap supports multiple actor generator types, each designed for specific use cases and integration scenarios. - -Available Actor Types -======================================== - -Built-in Generators -------------------- - -iWrap includes the following built-in actor generators: - -Standard Python Actor -~~~~~~~~~~~~~~~~~~~~~ - -**Type ID:** ``python`` - -**Description:** Standard Python actors for straightforward Python integrations with IMAS workflows. - -**Use Cases:** -- Simple Python-based physics codes -- Prototyping and development -- Direct IMAS IDS data access -- Python-to-Python workflows - -**Installation:** Included in core iWrap (no extra dependencies) - -**Documentation:** :doc:`project_description` - -MUSCLE3 Actors (Optional) -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -MUSCLE3 (Multiscale Coupling Library and Environment) actors enable high-performance multiscale and multiphysics coupling scenarios. - -**Installation Required:** ``pip install iwrap[muscle3]`` - -Available MUSCLE3 Generators: - -**MUSCLE3-Python** - :Type ID: ``MUSCLE3-Python`` - :Language: Python - :Use Case: Python physics codes in MUSCLE3 workflows - :Documentation: :doc:`muscle3_actors` - -**MUSCLE3-CPP** - :Type ID: ``MUSCLE3-CPP`` - :Language: C++ - :Use Case: High-performance C++ codes with MUSCLE3 coupling - :Documentation: :doc:`muscle3_actors` - -**MUSCLE3-Fortran** - :Type ID: ``MUSCLE3-Fortran`` - :Language: Fortran - :Use Case: Fortran codes or legacy codes with MUSCLE3 integration - :Documentation: :doc:`muscle3_actors` - -Choosing an Actor Type -======================================== - -Decision Matrix ---------------- - -Use this matrix to help choose the appropriate actor type: - -+----------------------------+------------------+------------------------+ -| Requirement | Standard Python | MUSCLE3 Actors | -+============================+==================+========================+ -| Simple Python workflow | ✅ Best choice | ❌ Overkill | -+----------------------------+------------------+------------------------+ -| Multiscale coupling | ❌ Manual | ✅ Built-in | -+----------------------------+------------------+------------------------+ -| Multi-language workflow | ✅ Supported | ✅ Optimized | -+----------------------------+------------------+------------------------+ -| High-performance coupling | ⚠️ Manual setup | ✅ Built-in | -+----------------------------+------------------+------------------------+ -| Complex time scales | ❌ Manual | ✅ Built-in | -+----------------------------+------------------+------------------------+ -| Learning curve | ✅ Low | ⚠️ Medium | -+----------------------------+------------------+------------------------+ -| External dependencies | ✅ None | ⚠️ MUSCLE3 required | -+----------------------------+------------------+------------------------+ - -Recommendation Guidelines --------------------------- - -**Choose Standard Python Actor if:** -- You have a simple Python-based physics code -- You're building a straightforward sequential workflow -- You don't need complex multiscale coupling -- You want minimal dependencies - -**Choose MUSCLE3 Actor if:** -- You need multiscale or multiphysics coupling -- Your workflow involves multiple time scales -- You need high-performance parallel coupling -- You're building complex coupled simulations -- You need standardized coupling interfaces - -Listing Available Actor Types -======================================== - -To see which actor types are available in your iWrap installation: - -.. code-block:: bash - - iwrap --list-actor-types - -Example output (core only): - -.. code-block:: text - - Id : Name : Description - ---------------------------------------------------------------------- - python : python : python - -Example output (with MUSCLE3): - -.. code-block:: text - - Id : Name : Description - ---------------------------------------------------------------------- - MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model - MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model - MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model - python : python : python - -Getting Details About an Actor Type -==================================== - -Get detailed information about a specific actor type: - -.. code-block:: bash - - iwrap --list-actor-details python - iwrap --list-actor-details MUSCLE3-Python - -This will show: -- Supported code languages -- Supported data types -- API version -- Additional requirements - -Comparison Table -======================================== - -Feature Comparison ------------------- - -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| Feature | Python Actor | MUSCLE3-Python | MUSCLE3-CPP | MUSCLE3-Fortran | -+===========================+===================+===================+===================+===================+ -| **Actor Language** | Python | Python | Python | Python | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Code Language** | Python, | Python | C++ | Fortran | -| | Fortran, C++, Java| | | | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Coupling Framework** | Direct calls | MUSCLE3 | MUSCLE3 | MUSCLE3 | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Multiscale Support** | Manual | Built-in | Built-in | Built-in | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Init/Finalize IDS** | ✅ Allowed | ❌ Not allowed | ❌ Not allowed | ❌ Not allowed | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ -| **Workflow Description** | XML | yMMSL | yMMSL | yMMSL | -+---------------------------+-------------------+-------------------+-------------------+-------------------+ - - -Usage Examples -======================================== - -Generating Different Actor Types ---------------------------------- - -**Standard Python Actor:** - -.. code-block:: bash - - iwrap -a my_actor -t python -f code_description.yaml - -**MUSCLE3 Python Actor:** - -.. code-block:: bash - - iwrap -a my_actor -t MUSCLE3-Python -f code_description.yaml - -**MUSCLE3 C++ Actor:** - -.. code-block:: bash - - iwrap -a my_actor -t MUSCLE3-CPP -f code_description.yaml - -**MUSCLE3 Fortran Actor:** - -.. code-block:: bash - - iwrap -a my_actor -t MUSCLE3-Fortran -f code_description.yaml - -External Plugin Support -======================================== - -iWrap's plugin architecture allows for external actor generators to be developed and distributed separately. - -How External Plugins Work --------------------------- - -1. External plugins are Python packages with the namespace ``iwrap_actor_generator`` -2. They are automatically discovered when installed -3. They appear alongside built-in generators - -Creating External Plugins --------------------------- - -See :doc:`developers_manual/04_adding_generators` for details on creating custom actor generators. - -Installing External Plugins ----------------------------- - -External plugins can be installed via pip: - -.. code-block:: bash - - pip install your-custom-iwrap-plugin - -After installation, they will appear in the list of available actor types. - -See Also -======================================== - -- :doc:`muscle3_actors` - MUSCLE3 actor generators documentation -- :doc:`project_description` - Creating actor descriptions -- :doc:`actor_generation_cmdln` - Command-line usage -- :doc:`iwrap_gui` - Graphical interface -- :doc:`developers_manual/04_adding_generators` - Creating custom generators diff --git a/docs/documentation/muscle3.rst b/docs/documentation/muscle3.rst index 696d79e..34fbabd 100644 --- a/docs/documentation/muscle3.rst +++ b/docs/documentation/muscle3.rst @@ -35,11 +35,7 @@ executable (C++ and Fortran) or a wrapping script (Python). muscle3_resources/code_wrapping.rst -.. toctree:: - :maxdepth: 10 - :caption: Installation guide - muscle3_resources/installation.rst .. note:: Further reading diff --git a/docs/documentation/muscle3_actors.rst b/docs/documentation/muscle3_actors.rst deleted file mode 100644 index fc08076..0000000 --- a/docs/documentation/muscle3_actors.rst +++ /dev/null @@ -1,292 +0,0 @@ -======================================== -MUSCLE3 Actor Generators -======================================== - -Overview -======================================== - -iWrap includes built-in support for generating MUSCLE3 actors, enabling multiscale and multiphysics coupling scenarios. -MUSCLE3 (Multiscale Coupling Library and Environment) is a high-performance coupling framework designed for complex scientific simulations. - -Available MUSCLE3 Generators -======================================== - -iWrap provides three MUSCLE3 actor generators: - -MUSCLE3-Python --------------- -Generate Python actors with MUSCLE3 coupling capabilities. - -**Use case:** Python-based physics codes that need to participate in MUSCLE3 workflows. - -**Actor Type ID:** ``MUSCLE3-Python`` - -MUSCLE3-CPP ------------ -Generate C++ actors with MUSCLE3 coupling capabilities. - -**Use case:** High-performance C++ physics codes requiring MUSCLE3 integration. - -**Actor Type ID:** ``MUSCLE3-CPP`` - -MUSCLE3-Fortran ---------------- -Generate Fortran actors with MUSCLE3 coupling capabilities. - -**Use case:** Legacy Fortran codes or performance-critical Fortran implementations needing MUSCLE3 coupling. - -**Actor Type ID:** ``MUSCLE3-Fortran`` - -Usage -======================================== - -Generating MUSCLE3 Actors --------------------------- - -Command Line -~~~~~~~~~~~~ - -Generate a MUSCLE3 Python actor: - -.. code-block:: bash - - iwrap -a my_actor -t MUSCLE3-Python -f code_description.yaml - -Generate a MUSCLE3 C++ actor: - -.. code-block:: bash - - iwrap -a my_actor -t MUSCLE3-CPP -f code_description.yaml - -Generate a MUSCLE3 Fortran actor: - -.. code-block:: bash - - iwrap -a my_actor -t MUSCLE3-Fortran -f code_description.yaml - -Graphical Interface -~~~~~~~~~~~~~~~~~~~ - -Launch iWrap GUI and select MUSCLE3 actor type from the dropdown: - -.. code-block:: bash - - iwrap-gui - -Then: -1. Select actor type: ``MUSCLE3-Python``, ``MUSCLE3-CPP``, or ``MUSCLE3-Fortran`` -2. Fill in code description -3. Generate actor - -Code Description Requirements ------------------------------- - -MUSCLE3 actors have specific requirements for code descriptions: - -**Restrictions:** -- INIT and FINALIZE methods **cannot** have IDS arguments -- Only the main STEP/RUN method can have IDS arguments - -**Example Code Description (YAML):** - -.. code-block:: yaml - - actor_description: - actor_name: my_muscle3_actor - actor_type: MUSCLE3-Python - - code_description: - implementation: - programming_language: python - code_path: /path/to/my_code.py - - subroutines: - init: - name: initialize - arguments: [] # No IDS arguments allowed - - step: - name: run_step - arguments: - - name: equilibrium - type: input - - name: core_profiles - type: output - - finalize: - name: cleanup - arguments: [] # No IDS arguments allowed - -Running MUSCLE3 Workflows --------------------------- - -After generating MUSCLE3 actors, they need to be integrated into a MUSCLE3 workflow using yMMSL (YAML-based MUSCLE Simulation Language). - -**Example yMMSL Workflow:** - -.. code-block:: yaml - - ymmsl_version: v0.1 - - model: - name: my_simulation - components: - macro: - implementation: macro_model - ports: - o_i: core_profiles - s_o: equilibrium - - micro: - implementation: my_muscle3_actor # Generated by iWrap - ports: - f_init: equilibrium - o_f: core_profiles - - settings: - micro.time_step: 0.1 - macro.iterations: 100 - -Running the workflow: - -.. code-block:: bash - - muscle_manager workflow.ymmsl - -Differences from Standard Python Actors -======================================== - -MUSCLE3 actors differ from standard Python actors in several ways: - -+---------------------------+-------------------------+-------------------------+ -| Feature | Standard Python Actor | MUSCLE3 Actor | -+===========================+=========================+=========================+ -| Coupling Framework | Direct Python calls | MUSCLE3 messaging | -+---------------------------+-------------------------+-------------------------+ -| Workflow Description | XML file | yMMSL file | -+---------------------------+-------------------------+-------------------------+ -| Data Exchange | Direct IDS passing | MUSCLE3 ports | -+---------------------------+-------------------------+-------------------------+ -| Execution Model | Sequential/MPI | MUSCLE3 managed | -+---------------------------+-------------------------+-------------------------+ -| Init/Finalize Arguments | Can have IDS | No IDS allowed | -+---------------------------+-------------------------+-------------------------+ -| Multiscale Coupling | Manual | Built-in | -+---------------------------+-------------------------+-------------------------+ - -Best Practices -======================================== - -1. **Use MUSCLE3 for Multiscale Simulations** - - MUSCLE3 is designed for complex multiscale and multiphysics scenarios where different components run at different time scales. - -2. **Keep Init/Finalize Simple** - - Avoid complex data operations in init/finalize methods. Use them only for setup and cleanup. - -3. **Design Clear Port Interfaces** - - Plan your MUSCLE3 ports carefully to ensure clear data flow between components. - -4. **Test Components Independently** - - Before integrating into a MUSCLE3 workflow, test individual actors to ensure they work correctly. - -5. **Use yMMSL Validation** - - Validate your yMMSL files before running to catch configuration errors early. - -Troubleshooting -======================================== - -MUSCLE3 Not Found ------------------ - -**Error:** -:: - - ImportError: No module named 'muscle3' - -**Solution:** - -.. code-block:: bash - - pip install iwrap[muscle3] - -Or install MUSCLE3 separately: - -.. code-block:: bash - - pip install muscle3>=0.7.0 - -MUSCLE3 Generators Not Listed ------------------------------- - -**Problem:** MUSCLE3 generators don't appear in ``iwrap --list-actor-types`` - -**Possible Causes:** -1. MUSCLE3 library not installed -2. Wrong iWrap version (MUSCLE3 built-in since version X.X.X) - -**Solution:** - -.. code-block:: bash - - # Verify MUSCLE3 is installed - python -c "import muscle3; print(muscle3.__version__)" - - # Reinstall iWrap with MUSCLE3 - pip install --upgrade iwrap[muscle3] - -Init/Finalize IDS Argument Error ---------------------------------- - -**Error:** -:: - - ValueError: MUSCLE3 actor generator cannot handle INIT/FINALIZE methods with IDS arguments - -**Solution:** - -Remove IDS arguments from init and finalize methods in your code description YAML file. Only the main step/run method can have IDS arguments. - -Resources -======================================== - -- `MUSCLE3 Documentation `_ -- `MUSCLE3 GitHub Repository `_ -- `iWrap Examples with MUSCLE3 <../tutorial/>`_ - -Migration from External Plugin -======================================== - -If you were previously using ``iwrap-plugins-muscle3`` as an external package: - -1. **Uninstall old plugin:** - - .. code-block:: bash - - pip uninstall iwrap-plugins-muscle3 - -2. **Update iWrap installation:** - - .. code-block:: bash - - pip install --upgrade iwrap[muscle3] - -3. **Verify:** - - .. code-block:: bash - - iwrap --list-actor-types - -All existing YAML files and workflows remain compatible. No changes to your code descriptions are needed. - -See Also -======================================== - -- :doc:`project_description` - Actor definition and generation -- :doc:`actor_generation_cmdln` - Command-line interface -- :doc:`iwrap_gui` - Graphical user interface -- :doc:`developers_manual/04_adding_generators` - Creating custom generators diff --git a/docs/documentation/muscle3_resources/installation.rst b/docs/documentation/muscle3_resources/installation.rst deleted file mode 100644 index 78debf1..0000000 --- a/docs/documentation/muscle3_resources/installation.rst +++ /dev/null @@ -1,175 +0,0 @@ -.. sectnum:: - -.. toctree:: - -Installer functionality -####################################################################################################################### - -iWrap MUSCLE3 plugins installer: - -- Builds WWW pages with manuals from ReStructured Text files using Sphinx -- Copies manuals to chosen installation folder -- Copies plugins (actor generators with set of templates) to chosen installation folder -- Builds module file from template and install it in selected directory - -Requirements -####################################################################################################################### - -Software required to install plugins: - -- `make` >= 3.82 - -Software required to build and install manuals: - -- `make` >= 3.82 -- `Python` >= 3.8.6 -- Python packages: - - - `Sphinx` >= 3.2.1 - - `sphinx-bootstrap-theme` >= 0.7.1 - - `sphinx-rtd-theme` >= 1.0.0 - -Installation -####################################################################################################################### - -iWrap MUSCLE3 plugins installer is based on `make` utility. To check all available targets -and configuration options, `make help` could be run from the console. - -.. code-block:: console - - shell> make help - - Available targets: - install : install plugins and iWrap4paf module (default) - uninstall : uninstall iWrap4paf module - install_plugins : install iWrap4paf plugins - install_module : install iWrap4paf module - clean : remove installation directories - help : display this help message - - Variables: - IWRAP_MODULE_NAME : name of iWrap module [ iwrap ] - MUSCLE3_MODULE_NAME : name of MUSCLE3 module [ muscle3 ] - VERSION : version of iWrap4paf [ 0.1.0 ] - NAME : name of iWrap4paf module [ iwrap4paf ] - INSTALL_PREFIX : path to software installation directory [ $HOME/IWRAP4PAF/install ] - INSTALL_DIR : full path to iWrap4paf installation directory [ $HOME/IWRAP4PAF/install/iwrap4paf/0.1.0 ] - MODULE_NAME : name of module file [ iwrap4paf ] - MODULE_INSTALL_PREFIX : path to module installation directory [ $HOME/IWRAP4PAF/modules ] - MODULE_INSTALL_DIR : full path to module installation directory [ $HOME/IWRAP4PAF/modules/iwrap4paf/0.1.0 ] - - -.. note:: - A command ``make help`` shows not only systems variables that may be configured, - but also their current values. It is strongly recommended to check before installation, - if all variables have correct (wanted) values. - - -Plugins installation -========================================================================================= -To install the plugins a `make install_plugins` command should be executed. - -.. code-block:: console - - shell> make install_plugins - -The installer copies plugins files to a directory defined as: - -.. code-block:: console - - $INSTALL_PREFIX/$NAME/$VERSION - -As it could be easily spotted, the target installation directory could be changed -by setting following system variables: - -- ``INSTALL_PREFIX`` - software install direcotry -- ``NAME`` - name of the plugins pack (may differ on various platforms) -- ``VERSION`` - plugins version (default value is set automatically using ``git describe``) - -Module installation -========================================================================================= -The module could be installed by launching `make install_module` command. - -.. code-block:: console - - shell> make install_module - -The installer builds a module file from the template based on a current values of following system variables: - -- ``IWRAP_MODULE_NAME`` - name of iWrap module (may differ on different platforms) -- ``MUSCLE3_MODULE_NAME`` - name of MSUCLE3 module (may differ on different platforms) -- ``VERSION`` - plugins version (default value is set automatically using ``git describe``) -- ``MODULE_NAME`` - name of module (may differ on different platforms) -- ``MODULE_INSTALL_PREFIX``- path to software modules directory -- ``MODULE_INSTALL_DIR`` - full path to module installation directory - -Verification -####################################################################################################################### - -Verification of the plugins installation -========================================================================================= - -Once plugins are installed, correctness of the installation can be verified by listing -available actor types registered in iWrap: - -.. code-block:: console - - shell> iwrap --list-actor-types - - Id : Name : Description - ---------------------------------------------------------------------- - python : Simple Python actor : Simple Python actor - MUSCLE3-Python : MUSCLE3 (Python) : Wrapping Python code into MUSCLE3 micro model - MUSCLE3-CPP : MUSCLE3 (C++) : Wrapping C++ code into MUSCLE3 micro model - MUSCLE3-Fortran : MUSCLE3 (Fortran) : Wrapping Fortran code into MUSCLE3 micro model - -.. note:: - Please make sure that paths to all installed plugins are added to ``PYTHONPATH`` including directory containing - common functionality (it is usually done by loading module `iwrap-plugins-muscle3`). Typical configuration usually - looks like follows: - - .. code-block:: shell - - # is the directory where plugins were installed - export PYTHONPATH=/generators/common:${PYTHONPATH} - export PYTHONPATH=/generators/fortran:${PYTHONPATH} - export PYTHONPATH=/generators/cpp:${PYTHONPATH} - export PYTHONPATH=/generators/python:${PYTHONPATH} - - -Verification of the module installation -========================================================================================= - -After installing the module and adding it to MODULEPATH, its availability and correctness -could be checked by listing and loading `iwrap-plugins-muscle3` module - -.. code-block:: shell - - # is the name chosen for the module at the installation stage (default: iwrap-plugins-muscle3) - module available - module load - - - -Uninstall -####################################################################################################################### - -To uninstall software following commands could be run: - -.. code-block:: shell - - make unistall # to uninstall plugins and module - # OR - make uninstall_plugins # to uninstall plugins - make uninstall_module # to uninstall module - - -.. warning:: - - Proper uninstallation of plugins (including their distribution and module file) requires the use - of exactly (!) the same system variables as for the installation process. - - - - - diff --git a/docs/documentation/quickstart.rst b/docs/documentation/quickstart.rst index 98aaa1e..966b0ed 100644 --- a/docs/documentation/quickstart.rst +++ b/docs/documentation/quickstart.rst @@ -5,45 +5,9 @@ Quick Start Guide This guide will help you get started with iWrap quickly, covering both standard Python actors and MUSCLE3 actors. Installation -======================================== - -Choose Your Installation ------------------------- - -**Option 1: Core Only (Minimal)** - -.. code-block:: bash - - pip install iwrap - -Includes: Standard Python actor generator - -**Option 2: With MUSCLE3 Support (Recommended)** - -.. code-block:: bash - - pip install iwrap[muscle3] - -Includes: Python actor + MUSCLE3 actors (Python, C++, Fortran) - -**Option 3: Development (All Features)** - -.. code-block:: bash - - pip install iwrap[all] +============ -Includes: All features and development tools - -Verify Installation -------------------- - -.. code-block:: bash - - iwrap --version - iwrap --list-actor-types - -Your First Actor (Standard Python) -======================================== +For detailed installation instructions, see :doc:`installation_guide/iwrap_installation`. Step 1: Prepare Your Code -------------------------- @@ -141,12 +105,7 @@ Prerequisites ------------- Ensure MUSCLE3 is installed: - -.. code-block:: bash - - pip install iwrap[muscle3] - # Verify - python -c "import muscle3; print(muscle3.__version__)" +For detailed installation instructions, see :doc:`installation_guide/iwrap_installation`. Step 1: Prepare Your Code -------------------------- @@ -263,18 +222,6 @@ Or load an existing description: iwrap-gui -f code_description.yaml -GUI Steps ---------- - -1. **Select Actor Type:** Choose from dropdown (python, MUSCLE3-Python, etc.) -2. **Enter Actor Name:** Specify your actor name -3. **Code Details:** Browse and select your code file -4. **Define Methods:** Add init, step, finalize methods -5. **Specify IDS Arguments:** Define input/output IDS for each method -6. **Generate:** Click generate button - -The GUI will create the YAML file and generate the actor. - Common Tasks ======================================== @@ -307,81 +254,3 @@ Viewing Actor Generator Details iwrap --list-actor-details python iwrap --list-actor-details MUSCLE3-Python - -Common Issues and Solutions -======================================== - -Issue: MUSCLE3 Generators Not Available ----------------------------------------- - -**Symptom:** Only ``python`` appears in ``--list-actor-types`` - -**Solution:** - -.. code-block:: bash - - pip install iwrap[muscle3] - -Issue: Import Error -------------------- - -**Symptom:** ``ModuleNotFoundError: No module named 'iwrap'`` - -**Solution:** Ensure iWrap is installed and PYTHONPATH is set: - -.. code-block:: bash - - pip install iwrap - # Or use environment setup script - source set-iter.sh - -Issue: MUSCLE3 Init/Finalize Error ------------------------------------ - -**Symptom:** ``ValueError: MUSCLE3 actor generator cannot handle INIT/FINALIZE methods with IDS arguments`` - -**Solution:** Remove IDS arguments from init and finalize methods in YAML: - -.. code-block:: yaml - - subroutines: - init: - name: init - arguments: [] # Empty - no IDS allowed - finalize: - name: finalize - arguments: [] # Empty - no IDS allowed - -Next Steps -======================================== - -Now that you've created your first actor, explore: - -📚 **Documentation** - - :doc:`actor_types` - Learn about all actor types - - :doc:`muscle3_actors` - Deep dive into MUSCLE3 - - :doc:`project_description` - Complete actor description reference - -🎓 **Tutorials** - - Follow interactive Jupyter tutorials in ``docs/tutorial/`` - - Try example actors in ``examples/`` - -🔧 **Advanced Topics** - - :doc:`code_standardization` - Code requirements and best practices - - :doc:`developers_manual` - Extending iWrap - - :doc:`actor_usage` - Using generated actors in workflows - -💡 **Examples** - - Browse ``examples/`` directory for complete working examples - - Check plugin tests for advanced usage patterns - -Getting Help -======================================== - -If you need assistance: - -1. Check the documentation: :doc:`iWrap_intro` -2. Review examples in the repository -3. Contact: iWrap Development Team - -Happy coding with iWrap! 🚀 diff --git a/iwrap/iwrap_main.py b/iwrap/iwrap_main.py index b500c14..33b08ed 100644 --- a/iwrap/iwrap_main.py +++ b/iwrap/iwrap_main.py @@ -17,7 +17,7 @@ def get_parser(is_commandline_mode: bool) -> argparse.ArgumentParser: # Management of input arguments parser = argparse.ArgumentParser( description='iWrap - a modular IMAS actor generator, used for creating ' 'standardized actors from a code interfaced with IDSs.', - epilog='For more information, visit .', + epilog='For more information, visit .', # formatter_class=argparse.ArgumentDefaultsHelpFormatter ) From 775837cd6369b7badfc46022096422fa671d0339 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 12 Feb 2026 13:57:07 +0100 Subject: [PATCH 30/33] copy files to correct directory --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index cbca5c3..c1e3a12 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,6 +10,9 @@ build: commands: - pip install -r docs/requirements.txt - cd docs && jupyter-book build . + # RTD expects the built HTML at: $READTHEDOCS_OUTPUT/html + - mkdir -p $READTHEDOCS_OUTPUT/html + - cp -a docs/_build/html/. $READTHEDOCS_OUTPUT/html/ # Optionally set the Python requirements used to build documentation python: From d2abdbae8ea903e7c4825a1f9658c62af3fefefc Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 12 Feb 2026 15:22:25 +0100 Subject: [PATCH 31/33] removed plugin wordings and fix commands in jupyter notebook --- .readthedocs.yaml | 1 + .../installation_guide/iwrap_installation.rst | 6 +++--- docs/documentation/muscle3_resources/code_wrapping.rst | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c1e3a12..1322a80 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,6 +8,7 @@ build: tools: python: "3.11" commands: + - pip install -e . - pip install -r docs/requirements.txt - cd docs && jupyter-book build . # RTD expects the built HTML at: $READTHEDOCS_OUTPUT/html diff --git a/docs/documentation/installation_guide/iwrap_installation.rst b/docs/documentation/installation_guide/iwrap_installation.rst index 0f00ca0..ccff134 100644 --- a/docs/documentation/installation_guide/iwrap_installation.rst +++ b/docs/documentation/installation_guide/iwrap_installation.rst @@ -156,7 +156,7 @@ MUSCLE3 support is now included in the main iWrap package and can be installed u Requirements ####################################################################################################################### -Software required to use MUSCLE3 plugins: +Software required to use MUSCLE3 actor generators: - `Python` - `iWrap` @@ -269,7 +269,7 @@ Variables that can be configured: Verification ####################################################################################################################### -Verification of MUSCLE3 Plugins Installation +Verification of MUSCLE3 Generators Installation ========================================================================================= Once iWrap is installed with MUSCLE3 support, verify that the MUSCLE3 actor generators are available: @@ -295,7 +295,7 @@ You can also verify that the MUSCLE3 generators can be imported: from iwrap.generators.actor_generators.muscle3_python import M3PythonActor from iwrap.generators.actor_generators.muscle3_common import m3_utils -If these imports succeed without errors, the MUSCLE3 plugins are correctly installed. +If these imports succeed without errors, the MUSCLE3 generators are correctly installed. Manual Environment Setup ======================== diff --git a/docs/documentation/muscle3_resources/code_wrapping.rst b/docs/documentation/muscle3_resources/code_wrapping.rst index 662b4e4..8f01fb8 100644 --- a/docs/documentation/muscle3_resources/code_wrapping.rst +++ b/docs/documentation/muscle3_resources/code_wrapping.rst @@ -8,13 +8,12 @@ MUSCLE3 actor generators requirements ####################################################################################################################### Following software must be available to wrap the code into MUSCLE3 models: -- *iWrap* - MUSCLE3 actors generators are provided as iWrap plugins, thus they cannot be used without iWrap +- *iWrap* - MUSCLE3 actors generators are built-in and included in the main iWrap package (v0.8.0+) - *MUSCLE3 libraries* - indispensable for building actors - should be available via ``pkg-config`` mechanism -- *MUSCLE3 actor generator plugins* - all paths to generator packages must be listed on the ``PYTHONPATH`` .. note:: - Usually operation system can be configured using ``modules`` toolkit, so in most cases loading ``iwrap4paf`` - is enough to configure user working environment. + Usually operation system can be configured using ``modules`` toolkit, so in most cases loading ``iwrap`` + module is enough to configure user working environment. MUSCLE3 libraries must also be available. **Warning**: Module name(s) may vary depending on platform! @@ -54,7 +53,7 @@ it can be done using either iwrap command line, YAML describing an actor or iWra ... .. tip:: - To help developers write MUSCLE3-compliant code, the iWrap MUSCLE3 plug-in allows you + To help developers write MUSCLE3-compliant code, the iWrap MUSCLE3 actor generators allow you to generate so-called **actor's skeleton**. The **actor's skeleton** is a source code for a given actor without embedding actual calls to native code. All places where subroutines from the code could be introduced are clearly marked with comments From 454ddb1460f852fde3e6a483c15c126826faa8e8 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 12 Feb 2026 15:26:26 +0100 Subject: [PATCH 32/33] install noneditable --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1322a80..387e99a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,7 +8,7 @@ build: tools: python: "3.11" commands: - - pip install -e . + - pip install . - pip install -r docs/requirements.txt - cd docs && jupyter-book build . # RTD expects the built HTML at: $READTHEDOCS_OUTPUT/html From 815bb2419ff676198c400cf8627a7019aa28a9b0 Mon Sep 17 00:00:00 2001 From: prasad-sawantdesai Date: Thu, 12 Feb 2026 15:41:18 +0100 Subject: [PATCH 33/33] fixed double submenu for muscle3 actors --- docs/documentation/muscle3.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/documentation/muscle3.rst b/docs/documentation/muscle3.rst index 34fbabd..96e6da0 100644 --- a/docs/documentation/muscle3.rst +++ b/docs/documentation/muscle3.rst @@ -29,13 +29,6 @@ without changes of the code API. The MUSCLE3 wrapper is one of ‘actor generato The iWrap is able to wrap native code provided in Python, C++ and Fortran, producing either a standalone binary executable (C++ and Fortran) or a wrapping script (Python). -.. toctree:: - :maxdepth: 10 - :caption: Code wrapping - - muscle3_resources/code_wrapping.rst - - .. note:: Further reading