diff --git a/.flake8 b/.flake8 index 02acfdcd..a3d38ae0 100644 --- a/.flake8 +++ b/.flake8 @@ -19,6 +19,7 @@ extend-exclude = tests .git studies + examples # <<< UPDATE ACCORDING WITH YOUR PYTHON PROJECT diff --git a/.github/workflows/pr_to_main.yml b/.github/workflows/pr_to_main.yml index c8e9ceef..f064c755 100644 --- a/.github/workflows/pr_to_main.yml +++ b/.github/workflows/pr_to_main.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -54,7 +54,7 @@ jobs: - name: Install all dependencies run: | python -m pip install --upgrade pip - pip install -r docs/requirements.txt + pip install -r requirements_dev.txt - name: Build documentation run: | sphinx-build -b html ./docs/source ./docs/build/html @@ -62,7 +62,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Checkout diff --git a/.github/workflows/pr_to_pr.yml b/.github/workflows/pr_to_pr.yml index 1861700f..87a109a9 100644 --- a/.github/workflows/pr_to_pr.yml +++ b/.github/workflows/pr_to_pr.yml @@ -65,7 +65,7 @@ jobs: run: python -m pip install -U sphinx - name: Install Sphinx requirements run: | - pip install -r docs/requirements.txt + pip install -r requirements_dev.txt - name: Build documentation run: | sphinx-build -b html ./docs/source ./docs/build/html diff --git a/.github/workflows/push_to_main.yml b/.github/workflows/push_to_main.yml index 6983132f..33109f17 100644 --- a/.github/workflows/push_to_main.yml +++ b/.github/workflows/push_to_main.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -45,7 +45,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Checkout diff --git a/.gitignore b/.gitignore index 5a2294e0..481e66c2 100644 --- a/.gitignore +++ b/.gitignore @@ -126,4 +126,9 @@ notebooks/tutorials/Data/ # Exclude obj files in tests folder !tests/*/*.obj -*.lock \ No newline at end of file +*.lock + +# Exclude sphinx gallery files +docs/source/auto_examples/ +docs/source/gen_modules/ +docs/source/sg_execution_times.rst \ No newline at end of file diff --git a/README.md b/README.md index dd0cf7d4..24f88f20 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,38 @@ f3dasm | [**Installation**](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/general/gettingstarted.html) | [**GitHub**](https://github.com/bessagroup/f3dasm) | [**PyPI**](https://pypi.org/project/f3dasm/) -| [**Practical sessions**](https://github.com/mpvanderschelling/f3dasm_teach) ## Summary -Welcome to `f3dasm`, a Python package for data-driven design and analysis of structures and materials. +Welcome to `f3dasm`, a **f**ramework for **d**ata-**d**riven **d**esign and **a**nalysis of **s**tructures and **m**aterials. +`f3dasm` introduces a general and user-friendly data-driven Python package for researchers and practitioners working on design and analysis of materials and structures. Some of the key features include: + +- **Modular design** + - The framework introduces flexible interfaces, allowing users to easily integrate their own models and algorithms. + +- **Automatic data management** + - The framework automatically manages I/O processes, saving you time and effort implementing these common procedures. + +- **Easy parallelization** + - The framework manages parallelization of experiments, and is compatible with both local and high-performance cluster computing. + +- **Built-in defaults** + - The framework includes a collection of benchmark functions, optimization algorithms and sampling strategies to get you started right away! + +- **Hydra integration** + - The framework is supports the [hydra](https://hydra.cc/) configuration manager, to easily manage and run experiments. + +## Getting started + +The best way to get started is to follow the [installation instructions](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/general/gettingstarted.html). + +## Illustrative benchmarks + +This package includes a collection of illustrative benchmark studies that demonstrate the capabilities of the framework. These studies are available in the `/studies` folder, and include the following studies: + +- Benchmarking optimization algorithms against well-known benchmark functions +- 'Fragile Becomes Supercompressible' ([Bessa et al. (2019)](https://onlinelibrary.wiley.com/doi/full/10.1002/adma.201904845)) ## Authorship @@ -26,9 +52,7 @@ Welcome to `f3dasm`, a Python package for data-driven design and analysis of str The Bessa research group at TU Delft is small... At the moment, we have limited availability to help future users/developers adapting the code to new problems, but we will do our best to help! -## Getting started -The best way to get started is to follow the [installation instructions](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/general/gettingstarted.html). ## Referencing @@ -48,7 +72,7 @@ If you find any **issues, bugs or problems** with this template, please use the ## License -Copyright 2023, Martin van der Schelling +Copyright 2024, Martin van der Schelling All rights reserved. diff --git a/VERSION b/VERSION index 5c1c82e4..721b9931 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.71 \ No newline at end of file +1.4.8 \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index 3e9cc925..15e9dcd1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -44,7 +44,8 @@ help: clean: -rm -rf $(BUILDDIR)/* - + -rm -rf $(SOURCEDIR)/auto_examples/* + -rm -rf $(SOURCEDIR)/gen_modules/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(SOURCEDIR) $(BUILDDIR)/html @echo diff --git a/docs/requirements.txt b/docs/requirements.txt index 97e45e6b..5d481e61 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,5 @@ sphinx sphinx_rtd_theme sphinxcontrib-bibtex sphinx_autodoc_typehints -sphinx-tabs==3.4.4 \ No newline at end of file +sphinx-tabs==3.4.4 +sphinx-gallery \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 55603adc..7789eb74 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,6 +7,8 @@ import os import sys +from sphinx_gallery.sorting import FileNameSortKey + # -- Search path for extensions and modules ----------------------------------- # If extensions or Python modules are in a different directory than this file, # then add these directories to sys.path so that Sphinx can search for them @@ -24,9 +26,9 @@ project = 'f3dasm' author = 'Martin van der Schelling' -copyright = '2022, Martin van der Schelling' -version = '1.4.71' -release = '1.4.71' +copyright = '2024, Martin van der Schelling' +version = '1.4.8' +release = '1.4.8' # -- General configuration ---------------------------------------------------- @@ -43,7 +45,18 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx_autodoc_typehints', - 'sphinx_tabs.tabs'] + 'sphinx_tabs.tabs', + 'sphinx_gallery.gen_gallery',] + +sphinx_gallery_conf = { + 'examples_dirs': ['../../examples'], # path to your example scripts + 'gallery_dirs': ['auto_examples'], + 'reference_url': {'sphinx_gallery': None, }, + 'backreferences_dir': 'gen_modules/backreferences', + 'doc_module': ('f3dasm',), + "filename_pattern": r"/*\.py", + "within_subsection_order": FileNameSortKey, +} # Source: https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-source_suffix source_suffix = {'.rst': 'restructuredtext', } diff --git a/docs/source/img/reaction-braking-stopping.png b/docs/source/img/reaction-braking-stopping.png new file mode 100644 index 00000000..915c20db Binary files /dev/null and b/docs/source/img/reaction-braking-stopping.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index 9bb3db19..f501388c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,76 +7,16 @@ f3dasm ====== .. toctree:: - :maxdepth: 3 - :caption: General + :maxdepth: 1 :hidden: - :glob: - rst_doc_files/general/gettingstarted rst_doc_files/general/overview + rst_doc_files/general/installation + rst_doc_files/defaults + auto_examples/index + API reference <_autosummary/f3dasm> .. toctree:: - :maxdepth: 2 - :caption: Design of Experiments :hidden: - :glob: - - rst_doc_files/classes/design/domain - rst_doc_files/classes/sampling/sampling - -.. toctree:: - :maxdepth: 2 - :caption: Data - :hidden: - :glob: - - rst_doc_files/classes/design/experimentdata - rst_doc_files/classes/design/experimentsample - -.. toctree:: - :maxdepth: 2 - :caption: Data Generation - :hidden: - :glob: - - rst_doc_files/classes/datageneration/datagenerator - rst_doc_files/classes/datageneration/functions - rst_doc_files/classes/datageneration/f3dasm-simulate - - -.. toctree:: - :maxdepth: 2 - :caption: Machine Learning - :hidden: - :glob: - - rst_doc_files/classes/machinelearning/machinelearning - -.. toctree:: - :maxdepth: 2 - :caption: Optimization - :hidden: - :glob: - - rst_doc_files/classes/optimization/optimizers - rst_doc_files/classes/optimization/f3dasm-optimize - -.. toctree:: - :maxdepth: 2 - :caption: Data-driven process - :hidden: - :glob: - - rst_doc_files/classes/workflow/workflow - rst_doc_files/classes/workflow/hydra - rst_doc_files/classes/workflow/cluster - -.. toctree:: - :name: apitoc - :caption: API - :hidden: - - rst_doc_files/reference/index.rst - Code <_autosummary/f3dasm> .. include:: readme.rst \ No newline at end of file diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 21af36dc..f823774b 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1,13 +1,49 @@ -.. image:: img/f3dasm-logo.png - :width: 70% - :alt: f3dasm logo - :align: center +Welcome to the documentation page of the 'Framework for Data-Driven Design and Analysis of Structures and Materials'. +Here you will find all information on installing, using and contributing to the Python package. -Summary -------- +Basic Concepts +-------------- -Welcome to the documentation page of the 'Framework for Data-Driven Design and Analysis of Structures and Materials' (:mod:`f3dasm`). -Here you will find all information on installing, using and contributing to the Python package. +**f3dasm** introduces a general and user-friendly data-driven Python package for researchers and practitioners working on design and analysis of materials and structures. +Some of the key features of are: + +- Modular design + + - The framework introduces flexible interfaces, allowing users to easily integrate their own models and algorithms. + +- Automatic data management + + - the framework automatically manages I/O processes, saving you time and effort implementing these common procedures. + +- :doc:`Easy parallelization ` + + - the framework manages parallelization of experiments, and is compatible with both local and high-performance cluster computing. + +- :doc:`Built-in defaults ` + + - The framework includes a collection of :ref:`benchmark functions `, :ref:`optimization algorithms ` and :ref:`sampling strategies ` to get you started right away! + +- :doc:`Hydra integration ` + + - The framework is integrated with `hydra `_ configuration manager, to easily manage and run experiments. + + +.. .. include:: auto_examples/001_domain/index.rst + +.. .. include:: auto_examples/002_experimentdata/index.rst + +.. .. include:: auto_examples/003_datageneration/index.rst + +.. .. include:: auto_examples/004_optimization/index.rst + +Getting started +--------------- + +The best way to get started is to: + +* Read the :ref:`overview` section, containing a brief introduction to the framework and a statement of need. +* Follow the :ref:`installation-instructions` to get going! +* Check out the :ref:`examples` section, containing a collection of examples to get you familiar with the framework. Authorship & Citation --------------------- @@ -16,21 +52,21 @@ Authorship & Citation .. [1] PhD Candiate, Delft University of Technology, `Website `_ , `GitHub `_ -If you use :mod:`f3dasm` in your research or in a scientific publication, it is appreciated that you cite the paper below: +.. If you use :mod:`f3dasm` in your research or in a scientific publication, it is appreciated that you cite the paper below: -**Computer Methods in Applied Mechanics and Engineering** (`paper `_): +.. **Computer Methods in Applied Mechanics and Engineering** (`paper `_): -.. code-block:: tex +.. .. code-block:: tex - @article{Bessa2017, - title={A framework for data-driven analysis of materials under uncertainty: Countering the curse of dimensionality}, - author={Bessa, Miguel A and Bostanabad, Ramin and Liu, Zeliang and Hu, Anqi and Apley, Daniel W and Brinson, Catherine and Chen, Wei and Liu, Wing Kam}, - journal={Computer Methods in Applied Mechanics and Engineering}, - volume={320}, - pages={633--667}, - year={2017}, - publisher={Elsevier} - } +.. @article{Bessa2017, +.. title={A framework for data-driven analysis of materials under uncertainty: Countering the curse of dimensionality}, +.. author={Bessa, Miguel A and Bostanabad, Ramin and Liu, Zeliang and Hu, Anqi and Apley, Daniel W and Brinson, Catherine and Chen, Wei and Liu, Wing Kam}, +.. journal={Computer Methods in Applied Mechanics and Engineering}, +.. volume={320}, +.. pages={633--667}, +.. year={2017}, +.. publisher={Elsevier} +.. } .. Statement of Need @@ -63,14 +99,6 @@ If you use :mod:`f3dasm` in your research or in a scientific publication, it is .. .. [5] Shin, D., Cupertino, A., de Jong, M. H., Steeneken, P. G., Bessa, M. A., & Norte, R. A. (2022). .. *Spiderweb nanomechanical resonators via bayesian optimization: inspired by nature and guided by machine learning*. Advanced Materials, 34(3), 2106248. -Getting started ---------------- - - -The best way to get started is to: - -* Read the :ref:`overview` section, containting a brief introduction to the framework and a statement of need. -* Follow the :ref:`installation-instructions` section, containing a step-by-step guide on how to install the package. Contribute ---------- @@ -88,12 +116,12 @@ Useful links Related extension libraries --------------------------- * `f3dasm_optimize `_: Optimization algorithms for the :mod:`f3dasm` package. -* `f3dasm_simulate `_: Simulators for the :mod:`f3dasm` package. -* `f3dasm_teach `_: Hub for practical session and educational material on using :mod:`f3dasm`. +.. * `f3dasm_simulate `_: Simulators for the :mod:`f3dasm` package. +.. * `f3dasm_teach `_: Hub for practical session and educational material on using :mod:`f3dasm`. License ------- -Copyright 2023, Martin van der Schelling +Copyright 2024, Martin van der Schelling All rights reserved. diff --git a/docs/source/rst_doc_files/classes/datageneration/datagenerator.rst b/docs/source/rst_doc_files/classes/datageneration/datagenerator.rst index 02b90ff9..0d82c3e2 100644 --- a/docs/source/rst_doc_files/classes/datageneration/datagenerator.rst +++ b/docs/source/rst_doc_files/classes/datageneration/datagenerator.rst @@ -33,7 +33,7 @@ We provide the datagenerator to the :meth:`~f3dasm.ExperimentData.evaluate` func Any key-word arguments that need to be passed down to the :class:`~f3dasm.datageneration.DataGenerator` can be passed in the :code:`kwargs` argument of the :meth:`~f3dasm.ExperimentData.evaluate` function. -There are three methods available of handeling the :class:`~f3dasm.ExperimentSample` objects: +There are three methods available of handling the :class:`~f3dasm.ExperimentSample` objects: * :code:`sequential`: regular for-loop over each of the :class:`~f3dasm.ExperimentSample` objects in order * :code:`parallel`: utilizing the multiprocessing capabilities (with the `pathos `_ multiprocessing library), each :class:`~f3dasm.ExperimentSample` object is run in a separate core @@ -105,3 +105,5 @@ You can add the ``post-process-function`` to the :class:`~f3dasm.datageneration. .. code-block:: python experimentdata.add_post_process(pre_process_function) + +.. include:: ../../../auto_examples/003_datageneration/index.rst \ No newline at end of file diff --git a/docs/source/rst_doc_files/classes/datageneration/functions.rst b/docs/source/rst_doc_files/classes/datageneration/functions.rst index 193a4f33..1858ca4b 100644 --- a/docs/source/rst_doc_files/classes/datageneration/functions.rst +++ b/docs/source/rst_doc_files/classes/datageneration/functions.rst @@ -35,6 +35,8 @@ Benchmark functions can substitute the expensive simulation in the experiment_data.evaluate('Ackley', kwargs={'noise': 0.1, 'scale_bounds': [[0., 1.], [-1., 1.]], 'offset': True, 'seed': 42}) +.. _implemented-benchmark-functions: + Implemented benchmark functions ------------------------------- diff --git a/docs/source/rst_doc_files/classes/design/domain.rst b/docs/source/rst_doc_files/classes/design/domain.rst index 14bf3961..add74685 100644 --- a/docs/source/rst_doc_files/classes/design/domain.rst +++ b/docs/source/rst_doc_files/classes/design/domain.rst @@ -19,7 +19,7 @@ The :class:`~f3dasm.design.Domain` is a set of parameter instances that make up | -To start, we instantiate an empty domain object: +To start, we create an empty domain object: .. code-block:: python @@ -28,14 +28,14 @@ To start, we instantiate an empty domain object: domain = Domain() -Now we can gradually add some parameters! +Now we can add some parameters! .. _parameters: -Parameters ----------- +Input parameters +---------------- -Parameters are singular features of the input search space. They are used to define the search space of the design. +Input parameters are singular features of the input search space. They are used to define the search space of the design. .. image:: ../../../img/f3dasm-parameter.png :width: 50% @@ -51,31 +51,35 @@ There are four types of parameters that can be created: :ref:`float ` + Domain from a `hydra `_ configuration file ------------------------------------------------------------- @@ -132,13 +151,21 @@ The domain can now be created by calling the :func:`~f3dasm.design.Domain.from_y def my_app(cfg): domain = Domain.from_yaml(cfg.domain) -Helper function for single-objective, n-dimensional continuous Domains +Helper function for single-objective, n-dimensional continuous domains ---------------------------------------------------------------------- We can make easily make a :math:`n`-dimensional continous domain with the helper function :func:`~f3dasm.design.make_nd_continuous_domain`. We have to specify the boundaries (``bounds``) for each of the dimensions with a list of lists or numpy :class:`~numpy.ndarray`: .. code-block:: python - + + from f3dasm.design import make_nd_continuous_domain + import numpy as np bounds = np.array([[-1.0, 1.0], [-1.0, 1.0]]) - domain = f3dasm.make_nd_continuous_domain(bounds=bounds, dimensionality=2) + domain = make_nd_continuous_domain(bounds=bounds, dimensionality=2) + + +.. .. minigallery:: f3dasm.design.Domain +.. :add-heading: Examples using the `Domain` object +.. :heading-level: - + diff --git a/docs/source/rst_doc_files/classes/design/experimentdata.rst b/docs/source/rst_doc_files/classes/design/experimentdata.rst index 522db82d..2da768a1 100644 --- a/docs/source/rst_doc_files/classes/design/experimentdata.rst +++ b/docs/source/rst_doc_files/classes/design/experimentdata.rst @@ -4,7 +4,7 @@ Experiment Data The :class:`~f3dasm.ExperimentData` object is the main object used to store implementations of a design-of-experiments, keep track of results, perform optimization and extract data for machine learning purposes. -All other processses of f3dasm use this object to manipulate and access data about your experiments. +All other processses of :mod:`f3dasm` use this object to manipulate and access data about your experiments. The :class:`~f3dasm.ExperimentData` object consists of the following attributes: @@ -42,7 +42,8 @@ You can construct a :class:`~f3dasm.ExperimentData` object by providing it :ref: .. code-block:: python >>> from f3dasm import ExperimentData - >>> data = ExperimentData(domain, input_data, output_data) + >>> data = ExperimentData( + domain=domain, input_data=input_data, output_data=output_data) The following sections will explain how to construct a :class:`~f3dasm.ExperimentData` object from your own data. @@ -62,7 +63,7 @@ Learn more about the :class:`~f3dasm.design.Domain` object in the :ref:`domain < >>> domain = Domain() >>> domain.add_float('x0', 0., 1.) >>> domain.add_float('x1', 0., 1.) - >>> data = ExperimentData(domain) + >>> data = ExperimentData(domain=domain) .. warning :: @@ -89,9 +90,9 @@ Several datatypes are supported for the ``input_data`` argument: >>> from f3dasm import ExperimentData >>> from f3dasm.design import Domain - >>> df = pd.DataFrame(...) # your data in a pandas DataFrame - >>> domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)}) - >>> data = ExperimentData.from_dataframe(df, domain) + >>> domain.add_float('x0', 0., 1.) + >>> domain.add_float('x1', 0., 1.) + >>> data = ExperimentData(domain=domain, input_data=df) * A two-dimensional :class:`~numpy.ndarray` object with shape (, ) @@ -100,9 +101,10 @@ Several datatypes are supported for the ``input_data`` argument: >>> from f3dasm import ExperimentData >>> from f3dasm.design import Domain >>> import numpy as np - >>> input_data = np.array([[0.1, 0.2], [0.3, 0.4]]) - >>> domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)}) - >>> data = ExperimentData.from_array(input_data, domain) + >>> input_array = np.array([[0.1, 0.2], [0.3, 0.4]]) + >>> domain.add_float('x0', 0., 1.) + >>> domain.add_float('x1', 0., 1.) + >>> data = ExperimentData(domain=domain, input_data=input_array) .. note:: @@ -116,8 +118,9 @@ Several datatypes are supported for the ``input_data`` argument: >>> from f3dasm import ExperimentData >>> from f3dasm.design import Domain - >>> domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)}) - >>> data = ExperimentData.from_csv("my_experiment_data.csv", domain) + >>> domain.add_float('x0', 0., 1.) + >>> domain.add_float('x1', 0., 1.) + >>> data = ExperimentData(domain=doman, input_data="my_experiment_data.csv") .. _output-data-format: @@ -135,24 +138,27 @@ Several datatypes are supported for the ``output_data`` argument: >>> from f3dasm import ExperimentData >>> from f3dasm.design import Domain >>> df = pd.DataFrame(...) # your data in a pandas DataFrame - >>> domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)}) - >>> data = ExperimentData(input_data=df, domain=domain) + >>> domain.add_output('x0') + >>> domain.add_output('x1') + >>> data = ExperimentData(domain=domain, output_data=df) * A two-dimensional :class:`~numpy.ndarray` object with shape (, ) >>> from f3dasm import ExperimentData >>> from f3dasm.design import Domain >>> import numpy as np - >>> input_array = np.array([[0.1, 0.2], [0.3, 0.4]]) - >>> domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)}) - >>> data = ExperimentData(input_data=input_array, domain=domain) + >>> output_array = np.array([[0.1, 0.2], [0.3, 0.4]]) + >>> domain.add_output('x0') + >>> domain.add_output('x1') + >>> data = ExperimentData(domain=domain, output_array=output_array) * A string or path to a ``.csv`` file containing the output data. The ``.csv`` file should contain a header row with the names of the output variables and the first column should be indices for the experiments. >>> from f3dasm import ExperimentData >>> from f3dasm.design import Domain - >>> domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)}) - >>> data = ExperimentData(input_data="my_experiment_data.csv", domain=domain) + >>> domain.add_output('x0') + >>> domain.add_output('x1') + >>> data = ExperimentData(domain=domain, output_data="my_experiment_data.csv") If you don't have output data yet, you can also construct an :class:`~f3dasm.ExperimentData` object without providing output data. @@ -163,7 +169,8 @@ project directory ^^^^^^^^^^^^^^^^^ The ``project_dir`` argument is used to :ref:`store the ExperimentData to disk ` -You can provide a string or a path to a directory. If the directory does not exist, it will be created. +You can provide a string or a path to a directory. This can either be a relative or absolute path. +If the directory does not exist, it will be created. .. code-block:: python @@ -172,12 +179,11 @@ You can provide a string or a path to a directory. If the directory does not exi >>> project_dir = "folder/to/my_project_directory" >>> data = ExperimentData(project_dir=project_dir) -You can also set the project directoy manually after creation with the :meth:`~f3dasm.ExperimentData.set_project_dir` method" +You can also set the project directory manually after creation with the :meth:`~f3dasm.ExperimentData.set_project_dir` method" .. code-block:: python >>> from f3dasm import ExperimentData - >>> from f3dasm.design import Domain >>> data = ExperimentData() >>> data.set_project_dir("folder/to/my_project_directory") @@ -197,39 +203,32 @@ classmethod with the path of project directory: .. _experimentdata-sampling: -ExperimentData from a sampling ------------------------------- +ExperimentData from sampling +---------------------------- You can directly construct an :class:`~f3dasm.ExperimentData` object from a sampling strategy by using the :meth:`~f3dasm.ExperimentData.from_sampling` method. You have to provide the following arguments: -* A sampling function. To learn more about integrating your sampling function, please refer to :ref:`this ` section. +* A sampling function. To learn more about integrating your sampling function, please refer to the :ref:`this ` section. * A :class:`~f3dasm.design.Domain` object describing the input variables of the sampling function. * The number of samples to generate. * An optional seed for the random number generator. .. code-block:: python - from f3dasm import ExperimentData, Domain, ContinuousParameter + from f3dasm import ExperimentData, Domain def your_sampling_function(domain, n_samples, seed): # your sampling function # ... return samples - domain = Domain({'x0': ContinuousParameter(0., 1.)}, 'x1': ContinuousParameter(0., 1.)} - sampler = RandomUniform(domain, 10) + domain = Domain() + domain.add_float('x0', 0., 1.) + domain.add_float('x1', 0., 1.) data = ExperimentData.from_sampling(sampler=your_sampling_function, domain=domain, n_samples=10, seed=42) -You can use the built-in samplers from the sampling module by providing one of the following strings as the ``sampler`` argument: - -======================== ====================================================================== =========================================================================================================== -Name Method Reference -======================== ====================================================================== =========================================================================================================== -``"random"`` Random Uniform sampling `numpy.random.uniform `_ -``"latin"`` Latin Hypercube sampling `SALib.latin `_ -``"sobol"`` Sobol Sequence sampling `SALib.sobol_sequence `_ -======================== ====================================================================== =========================================================================================================== +You can use :ref:`built-in samplers ` by providing one of the following strings as the ``sampler`` argument: .. code-block:: python @@ -269,7 +268,7 @@ You can create an experimentdata :class:`~f3dasm.ExperimentData` object in the s experimentdata: input_data: path/to/input_data.csv output_data: - domain: ${domain} + domain: ${domain} .. note:: @@ -285,11 +284,8 @@ Inside your python script, you can then create the :class:`~f3dasm.ExperimentDat >>> @hydra.main(config_path="conf", config_name="config") >>> def my_app(config): - >>> data = ExperimentData.from_yaml(config) + >>> data = ExperimentData.from_yaml(config.experimentdata) -.. note:: - - Make sure to pass the full :code:`config` to the :meth:`~f3dasm.ExperimentData.from_yaml` constructor! To create the :class:`~f3dasm.ExperimentData` object with the :meth:`~f3dasm.ExperimentData.from_sampling` method, you can use the following configuration: @@ -316,7 +312,7 @@ To create the :class:`~f3dasm.ExperimentData` object with the :meth:`~f3dasm.Exp .. note:: - The :class:`~f3dasm.sampling.Sampler` object will be constructed using the :class:`~f3dasm.design.Domain` object from the config file. Make sure you have the :code:`domain` key in your :code:`config.yaml`! + Make sure you have the :code:`domain` key in your :code:`config.yaml`! To see how to configure the :class:`~f3dasm.design.Domain` object with hydra, see :ref:`this ` section. @@ -357,12 +353,13 @@ Storing the ExperimentData object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :class:`~f3dasm.ExperimentData` object can be exported to a collection of files using the :meth:`~f3dasm.ExperimentData.store` method. +You can provide a path to a directory where the files will be stored, or if not provided, the files will be stored in the directory provided in the :attr:`~f3dasm.design.ExperimentData.project_dir` attribute: .. code-block:: python >>> data.store("path/to/project_dir") -Inside the project directory, a subfolder `experiment_data` will be created containing the following files: +Inside the project directory, a subfolder `experiment_data` will be created with the following files: - :code:`domain.pkl`: The :class:`~f3dasm.design.Domain` object - :code:`input.csv`: The :attr:`~f3dasm.design.ExperimentData.input_data` table @@ -397,3 +394,8 @@ Alternatively, you can convert the input- and outputdata of your data-driven pro * :class:`~numpy.ndarray` (:meth:`~f3dasm.ExperimentData.to_numpy`); creates a tuple of two :class:`~numpy.ndarray` objects containing the input- and outputdata. * :class:`~xarray.Dataset` (:meth:`~f3dasm.ExperimentData.to_xarray`); creates a :class:`~xarray.Dataset` object containing the input- and outputdata. * :class:`~pd.DataFrame` (:meth:`~f3dasm.ExperimentData.to_pandas`); creates a tuple of two :class:`~pd.DataFrame` object containing the input- and outputdata. + +.. .. minigallery:: f3dasm.ExperimentData +.. :add-heading: Examples using the `ExperimentData` object +.. :heading-level: - + diff --git a/docs/source/rst_doc_files/classes/design/experimentsample.rst b/docs/source/rst_doc_files/classes/design/experimentsample.rst index 5e6013d1..4c2e0b05 100644 --- a/docs/source/rst_doc_files/classes/design/experimentsample.rst +++ b/docs/source/rst_doc_files/classes/design/experimentsample.rst @@ -13,20 +13,20 @@ A :class:`~f3dasm.ExperimentSample` object contains a single realization of the .. warning:: A :class:`~f3dasm.ExperimentSample` is not constructed manually, but created inside the :class:`~f3dasm.ExperimentData` when it is required by internal processes. The main use of the :class:`~f3dasm.ExperimentSample` is in the context of the :class:`~f3dasm.datageneration.DataGenerator` in order to extract design variables and store output variables. - Learn more about the :class:~`f3dasm.datageneration.DataGenerator` in the :ref:`Data Generation ` section. + Learn more about the :class:`~f3dasm.datageneration.DataGenerator` in the :ref:`Data Generation ` section. For each of the experiments in the :class:`~f3dasm.ExperimentData`, an :class:`~f3dasm.ExperimentSample` object can be created. -This object contains: +This object contains the following attributes: -* the input parameters of the sample: :attr:`~f3dasm.design.ExperimentSample.input_data` +* the input parameters of the experiment, :attr:`~f3dasm.design.ExperimentSample.input_data`, as a dictionary .. code-block:: python >>> experiment_sample.input_data {'param_1': 0.0249, 'param_2': 0.034, 'param_3': 0.1} -* the output parameters of the sample: :attr:`~f3dasm.design.ExperimentSample.output_data` +* the output parameters of the experiment, :attr:`~f3dasm.design.ExperimentSample.output_data`, as a dictionary .. code-block:: python @@ -36,18 +36,18 @@ This object contains: .. note:: - If you have `stored your output to disk `, the :attr:`~f3dasm.design.ExperimentSample.output_data` will contain a reference to the stored output instead of the actual output. + If you have :ref:`stored your output to disk `, the :attr:`~f3dasm.design.ExperimentSample.output_data` will contain a reference to the stored output instead of the actual output. If you want to load the objects from disk, use the :attr:`~f3dasm.design.ExperimentSample.output_data_loaded` attribute. -* the index number of the experiment: :attr:`~f3dasm.design.ExperimentSample.job_number` +* the index number of the experiment: :attr:`~f3dasm.design.ExperimentSample.job_number`, as an integer .. code-block:: python >>> experiment_sample.job_number 0 -Input parameters of an experiment sample can be accessed using the :attr:`~f3dasm.design.ExperimentSample.get` attribute, with the name of the parameter as the key. -An KeyError will be raised if the key is not found. +Input and output parameters of an experiment sample can be accessed using the :attr:`~f3dasm.design.ExperimentSample.get` attribute, with the name of the parameter as the key. +An error will be raised if the key is not found. .. code-block:: python @@ -67,10 +67,12 @@ The :class:`~f3dasm.ExperimentData` object can be manually iterated over to get ExperimentSample(1 : {'x0': 0.7203461491873061, 'x1': 0.7320604457665572, 'x2': 0.2524387342272223} - {}) ExperimentSample(2 : {'x0': 0.35449352388104904, 'x1': 0.11413412225748525, 'x2': 0.1467895592274866} - {}) +.. _storing-output-experiment-sample: + Storing output parameters to the experiment sample -------------------------------------------------- -After running your simulation, you can store the result back into the :class:`~f3dasm.ExperimentSample` with the :meth:`f3dasm.design.ExperimentSample.store` method. +After running your simulation, you can store the result back into the :class:`~f3dasm.ExperimentSample` with the :meth:`~f3dasm.design.ExperimentSample.store` method. There are two ways of storing your output: * Singular values can be stored directly to the :attr:`~f3dasm.design.ExperimentData.output_data` @@ -129,7 +131,7 @@ A reference (:code:`Path`) will be saved to the :attr:`~f3dasm.design.Experiment In the output data of the :class:`~f3dasm.ExperimentData` object, a reference path (e.g. :code:`/output_numpy/0.npy`) to the stored object will be saved. -:mod:`f3dasm` has built-in storing functions for numpy :class:`~numpy.ndarray`, pandas :class:`~pandas.DataFrame` and xarray :class:`~xarray.DataArray` and :class:`~xarray.Dataset`. +:mod:`f3dasm` has built-in storing functions for numpy :class:`~numpy.ndarray`, pandas :class:`~pandas.DataFrame` and xarray :class:`~xarray.DataArray` and :class:`~xarray.Dataset` objects. For any other type of object, the object will be stored in the `pickle `_ format diff --git a/docs/source/rst_doc_files/classes/optimization/f3dasm-optimize.rst b/docs/source/rst_doc_files/classes/optimization/f3dasm-optimize.rst index 2c676a97..1ea01164 100644 --- a/docs/source/rst_doc_files/classes/optimization/f3dasm-optimize.rst +++ b/docs/source/rst_doc_files/classes/optimization/f3dasm-optimize.rst @@ -4,10 +4,8 @@ ======================= -The :mod:`f3dasm.datageneration` module is designed to be easily extended by third-party libraries. -In order to not bloat the main :mod:`f3dasm` package, these extensions are provided as separate package: `f3dasm_optimize `_. - -More ports to optimization algorithms are available in the `f3dasm_optimize `_ package, which can be installed via pip: +The :mod:`f3dasm.optimization` module is designed to be easily extended by third-party libraries. +These extensions are provided as separate package: `f3dasm_optimize `_, which can be installed via pip: .. code-block:: bash diff --git a/docs/source/rst_doc_files/classes/sampling/sampling.rst b/docs/source/rst_doc_files/classes/sampling/sampling.rst index 96e46baa..4c902a38 100644 --- a/docs/source/rst_doc_files/classes/sampling/sampling.rst +++ b/docs/source/rst_doc_files/classes/sampling/sampling.rst @@ -3,30 +3,58 @@ Sampling ======== -Samplers take the :class:`~f3dasm.design.Domain` object and return input data based on the sampling strategy. +In the context of the data-driven process, samplers play a crucial role in generating input data for experiments or analyses. +A sampler takes a :class:`~f3dasm.design.Domain` object, and applies a specific strategy to produce samples. +These samples serve as input for further analysis, experimentation, or modeling. + +This section describes how you can implement your sampling strategy or use the built-in samplers in a data-driven process. .. _integrating-samplers: Implement your sampling strategy -------------------------------- -To integrate your sampling strategy in in the data-driven process, you should create a function that takes the following arguments: +When integrating your sampling strategy into the data-driven process, you have to create a function that will take several arguments: + +* :code:`domain`: A :class:`~f3dasm.design.Domain` object that represents the design-of-experiments +* :code:`n_samples`: The number of samples you wish to generate. It's not always necessary to define this upfront, as some sampling methods might inherently determine the number of samples based on certain criteria or properties of the domain. +* :code:`seed`: A seed for the random number generator to replicate the sampling process. This enables you to control the randomness of the sampling process [1]_. By setting a seed, you ensure reproducibility, meaning that if you run the sampling function with the same seed multiple times, you'll get the same set of samples. -* A :class:`~f3dasm.design.Domain` object -* The number of samples to create -* A random seed (optional) +.. [1] If no seed is provided, the function should use a random seed. The function should return the samples (``input_data``) in one of the following formats: * A :class:`~pandas.DataFrame` object * A :class:`~numpy.ndarray` object +.. code-block:: python + def your_sampling_method(domain: Domain, n_samples: int, seed: Optional[int]): + # Your sampling logic here + ... + return your_samples -.. _implemented samplers: +An example: implementing a sobol sequence sampler +------------------------------------------------- -Use the sampler in the data-driven process -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +For example, the following code defines a sampler based on a `Sobol sequence `_: + +.. code-block:: python + + from f3dasm.design import Domain + from SALib.sample import sobol_sequence + + def sample_sobol_sequence(domain: Domain, n_samples: int, **kwargs): + samples = sobol_sequence.sample(n_samples, len(domain)) + + # stretch samples + for dim, param in enumerate(domain.space.values()): + samples[:, dim] = ( + samples[:, dim] * ( + param.upper_bound - param.lower_bound + ) + param.lower_bound + ) + return samples To use the sampler in the data-driven process, you should pass the function to the :class:`~f3dasm.ExperimentData` object as follows: @@ -41,17 +69,12 @@ To use the sampler in the data-driven process, you should pass the function to t # Generate samples experiment_data.sample(sampler=your_sampling_method, n_samples=10, seed=42) - -.. note:: - - This method will throw an error if you do not have any prior ``input_data`` in the :class:`~f3dasm.ExperimentData` - object before sampling **and** you do not provide a :class:`~f3dasm.design.Domain` object in the initializer. +.. _implemented samplers: Implemented samplers -------------------- The following built-in implementations of samplers can be used in the data-driven process. -To use these samplers ======================== ====================================================================== =========================================================================================================== Name Method Reference diff --git a/docs/source/rst_doc_files/classes/workflow/cluster.rst b/docs/source/rst_doc_files/cluster.rst similarity index 100% rename from docs/source/rst_doc_files/classes/workflow/cluster.rst rename to docs/source/rst_doc_files/cluster.rst diff --git a/docs/source/rst_doc_files/defaults.rst b/docs/source/rst_doc_files/defaults.rst new file mode 100644 index 00000000..76d5b9a6 --- /dev/null +++ b/docs/source/rst_doc_files/defaults.rst @@ -0,0 +1,176 @@ +Built-in functionalities +======================== + +.. _implemented samplers: + +Implemented samplers +-------------------- + +The following built-in implementations of samplers can be used in the data-driven process. + +======================== ====================================================================== =========================================================================================================== +Name Method Reference +======================== ====================================================================== =========================================================================================================== +``"random"`` Random Uniform sampling `numpy.random.uniform `_ +``"latin"`` Latin Hypercube sampling `SALib.latin `_ +``"sobol"`` Sobol Sequence sampling `SALib.sobol_sequence `_ +``"grid"`` Grid Search sampling `itertools.product `_ +======================== ====================================================================== =========================================================================================================== + +.. _implemented-benchmark-functions: + +Implemented benchmark functions +------------------------------- + +These benchmark functions are taken and modified from the `Python Benchmark Test Optimization Function Single Objective `_ github repository. +The following implementations of benchmark functions can instantiated with the name in the 'Data-generator argument' column. + +.. note:: + + Not all benchmark functions are implemented for all dimensions. + If you want to use a benchmark function for a dimension that is not implemented, you will get a :class:`~NotImplementedError`. + +Convex functions +^^^^^^^^^^^^^^^^ + +======================== ====================================================== =========================== +Name Docs of the Python class Data-generator argument +======================== ====================================================== =========================== +Ackley N. 2 :class:`~f3dasm.datageneration.AckleyN2` ``"Ackley N. 2"`` +Bohachevsky N. 1 :class:`~f3dasm.datageneration.BohachevskyN1` ``"Bohachevsky N. 1"`` +Booth :class:`~f3dasm.datageneration.Booth` ``"Booth"`` +Brent :class:`~f3dasm.datageneration.Brent` ``"Brent"`` +Brown :class:`~f3dasm.datageneration.Brown` ``"Brown"`` +Bukin N. 6 :class:`~f3dasm.datageneration.BukinN6` ``"Bukin N. 6"`` +Dixon Price :class:`~f3dasm.datageneration.DixonPrice` ``"Dixon Price"`` +Exponential :class:`~f3dasm.datageneration.Exponential` ``"Exponential"`` +Matyas :class:`~f3dasm.datageneration.Matyas` ``"Matyas"`` +McCormick :class:`~f3dasm.datageneration.McCormick` ``"McCormick"`` +Perm 0, d, beta :class:`~f3dasm.datageneration.PermZeroDBeta` ``"Perm 0, d, beta"`` +Powell :class:`~f3dasm.datageneration.Powell` ``"Powell"`` +Rotated Hyper-Ellipsoid :class:`~f3dasm.datageneration.RotatedHyperEllipsoid` ``"Rotated Hyper-Ellipsoid"`` +Schwefel 2.20 :class:`~f3dasm.datageneration.Schwefel2_20` ``"Schwefel 2.20"`` +Schwefel 2.21 :class:`~f3dasm.datageneration.Schwefel2_21` ``"Schwefel 2.21"`` +Schwefel 2.22 :class:`~f3dasm.datageneration.Schwefel2_22` ``"Schwefel 2.22"`` +Schwefel 2.23 :class:`~f3dasm.datageneration.Schwefel2_23` ``"Schwefel 2.23"`` +Sphere :class:`~f3dasm.datageneration.Sphere` ``"Sphere"`` +Sum Squares :class:`~f3dasm.datageneration.SumSquares` ``"Sum Squares"`` +Thevenot :class:`~f3dasm.datageneration.Thevenot` ``"Thevenot"`` +Trid :class:`~f3dasm.datageneration.Trid` ``"Trid"`` +Xin She Yang N.3 :class:`~f3dasm.datageneration.XinSheYangN3` ``"Xin She Yang N.3"`` +Xin-She Yang N.4 :class:`~f3dasm.datageneration.XinSheYangN4` ``"Xin-She Yang N.4"`` +======================== ====================================================== =========================== + + + +Seperable functions +^^^^^^^^^^^^^^^^^^^ + +======================== ============================================== ============================ +Name Docs of the Python class Data-generator argument +======================== ============================================== ============================ +Ackley :class:`~f3dasm.datageneration.Ackley` ``"Ackley"`` +Bohachevsky N. 1 :class:`~f3dasm.datageneration.BohachevskyN1` ``"Bohachevsky N. 1"`` +Easom :class:`~f3dasm.datageneration.Easom` ``"Easom"`` +Egg Crate :class:`~f3dasm.datageneration.EggCrate` ``"Egg Crate"`` +Exponential :class:`~f3dasm.datageneration.Exponential` ``"Exponential"`` +Griewank :class:`~f3dasm.datageneration.Griewank` ``"Griewank"`` +Michalewicz :class:`~f3dasm.datageneration.Michalewicz` ``"Michalewicz"`` +Powell :class:`~f3dasm.datageneration.Powell` ``"Powell"`` +Qing :class:`~f3dasm.datageneration.Qing` ``"Qing"`` +Quartic :class:`~f3dasm.datageneration.Quartic` ``"Quartic"`` +Rastrigin :class:`~f3dasm.datageneration.Rastrigin` ``"Rastrigin"`` +Schwefel :class:`~f3dasm.datageneration.Schwefel` ``"Schwefel"`` +Schwefel 2.20 :class:`~f3dasm.datageneration.Schwefel2_20` ``"Schwefel 2.20"`` +Schwefel 2.21 :class:`~f3dasm.datageneration.Schwefel2_21` ``"Schwefel 2.21"`` +Schwefel 2.22 :class:`~f3dasm.datageneration.Schwefel2_22` ``"Schwefel 2.22"`` +Schwefel 2.23 :class:`~f3dasm.datageneration.Schwefel2_23` ``"Schwefel 2.23"`` +Sphere :class:`~f3dasm.datageneration.Sphere` ``"Sphere"`` +Styblinski Tank :class:`~f3dasm.datageneration.StyblinskiTank` ``"Styblinski Tank"`` +Sum Squares :class:`~f3dasm.datageneration.SumSquares` ``"Sum Squares"`` +Thevenot :class:`~f3dasm.datageneration.Thevenot` ``"Thevenot"`` +Xin She Yang :class:`~f3dasm.datageneration.XinSheYang` ``"Xin She Yang"`` +======================== ============================================== ============================ + +Multimodal functions +^^^^^^^^^^^^^^^^^^^^ + +======================== ================================================ ========================== +Name Docs of the Python class Data-generator argument +======================== ================================================ ========================== +Ackley :class:`~f3dasm.datageneration.Ackley` ``"Ackley"`` +Ackley N. 3 :class:`~f3dasm.datageneration.AckleyN3` ``"Ackley N. 3"`` +Ackley N. 4 :class:`~f3dasm.datageneration.AckleyN4` ``"Ackley N. 4"`` +Adjiman :class:`~f3dasm.datageneration.Adjiman` ``"Adjiman"`` +Bartels :class:`~f3dasm.datageneration.Bartels` ``"Bartels"`` +Beale :class:`~f3dasm.datageneration.Beale` ``"Beale"`` +Bird :class:`~f3dasm.datageneration.Bird` ``"Bird"`` +Bohachevsky N. 2 :class:`~f3dasm.datageneration.BohachevskyN2` ``"Bohachevsky N. 2"`` +Bohachevsky N. 3 :class:`~f3dasm.datageneration.BohachevskyN3` ``"Bohachevsky N. 3"`` +Branin :class:`~f3dasm.datageneration.Branin` ``"Branin"`` +Bukin N. 6 :class:`~f3dasm.datageneration.BukinN6` ``"Bukin N. 6"`` +Colville :class:`~f3dasm.datageneration.Colville` ``"Colville"`` +Cross-in-Tray :class:`~f3dasm.datageneration.CrossInTray` ``"Cross-in-Tray"`` +De Jong N. 5 :class:`~f3dasm.datageneration.DeJongN5` ``"De Jong N. 5"`` +Deckkers-Aarts :class:`~f3dasm.datageneration.DeckkersAarts` ``"Deckkers-Aarts"`` +Easom :class:`~f3dasm.datageneration.Easom` ``"Easom"`` +Egg Crate :class:`~f3dasm.datageneration.EggCrate` ``"Egg Crate"`` +Egg Holder :class:`~f3dasm.datageneration.EggHolder` ``"Egg Holder"`` +Goldstein-Price :class:`~f3dasm.datageneration.GoldsteinPrice` ``"Goldstein-Price"`` +Happy Cat :class:`~f3dasm.datageneration.HappyCat` ``"Happy Cat"`` +Himmelblau :class:`~f3dasm.datageneration.Himmelblau` ``"Himmelblau"`` +Holder-Table :class:`~f3dasm.datageneration.HolderTable` ``"Holder-Table"`` +Keane :class:`~f3dasm.datageneration.Keane` ``"Keane"`` +Langermann :class:`~f3dasm.datageneration.Langermann` ``"Langermann"`` +Levy :class:`~f3dasm.datageneration.Levy` ``"Levy"`` +Levy N. 13 :class:`~f3dasm.datageneration.LevyN13` ``"Levy N. 13"`` +McCormick :class:`~f3dasm.datageneration.McCormick` ``"McCormick"`` +Michalewicz :class:`~f3dasm.datageneration.Michalewicz` ``"Michalewicz"`` +Periodic :class:`~f3dasm.datageneration.Periodic` ``"Periodic"`` +Perm d, beta :class:`~f3dasm.datageneration.PermDBeta` ``"Perm d, beta"`` +Qing :class:`~f3dasm.datageneration.Qing` ``"Qing"`` +Quartic :class:`~f3dasm.datageneration.Quartic` ``"Quartic"`` +Rastrigin :class:`~f3dasm.datageneration.Rastrigin` ``"Rastrigin"`` +Rosenbrock :class:`~f3dasm.datageneration.Rosenbrock` ``"Rosenbrock"`` +Salomon :class:`~f3dasm.datageneration.Salomon` ``"Salomon"`` +Schwefel :class:`~f3dasm.datageneration.Schwefel` ``"Schwefel"`` +Shekel :class:`~f3dasm.datageneration.Shekel` ``"Shekel"`` +Shubert :class:`~f3dasm.datageneration.Shubert` ``"Shubert"`` +Shubert N. 3 :class:`~f3dasm.datageneration.ShubertN3` ``"Shubert N. 3"`` +Shubert N. 4 :class:`~f3dasm.datageneration.ShubertN4` ``"Shubert N. 4"`` +Styblinski Tank :class:`~f3dasm.datageneration.StyblinskiTank` ``"Styblinski Tank"`` +Thevenot :class:`~f3dasm.datageneration.Thevenot` ``"Thevenot"`` +Xin She Yang :class:`~f3dasm.datageneration.XinSheYang` ``"Xin She Yang"`` +Xin She Yang N.2 :class:`~f3dasm.datageneration.XinSheYangN2` ``"Xin She Yang N.2"`` +======================== ================================================ ========================== + +.. _implemented optimizers: + +Implemented optimizers +---------------------- + +The following implementations of optimizers can found under the :mod:`f3dasm.optimization` module: +These are ported from `scipy-optimize `_ + +======================== ========================================================================= =============================================================================================== +Name Key-word Reference +======================== ========================================================================= =============================================================================================== +Conjugate Gradient ``"CG"`` `scipy.minimize CG `_ +L-BFGS-B ``"LBFGSB"`` `scipy.minimize L-BFGS-B `_ +Nelder Mead ``"NelderMead"`` `scipy.minimize NelderMead `_ +Random search ``"RandomSearch"`` `numpy `_ +======================== ========================================================================= =============================================================================================== + +.. _f3dasm-optimize: + +:code:`f3dasm-optimize` +^^^^^^^^^^^^^^^^^^^^^^^ + +The :mod:`f3dasm.optimization` module is designed to be easily extended by third-party libraries. +These extensions are provided as separate package: `f3dasm_optimize `_, which can be installed via pip: + +.. code-block:: bash + + pip install f3dasm_optimize + +More information about this extension can be found in the `f3dasm_optimize documentation page `_ \ No newline at end of file diff --git a/docs/source/rst_doc_files/general/gettingstarted.rst b/docs/source/rst_doc_files/general/installation.rst similarity index 96% rename from docs/source/rst_doc_files/general/gettingstarted.rst rename to docs/source/rst_doc_files/general/installation.rst index 23c6d876..53f00b9d 100644 --- a/docs/source/rst_doc_files/general/gettingstarted.rst +++ b/docs/source/rst_doc_files/general/installation.rst @@ -1,10 +1,9 @@ .. _installation-instructions: -=============== -Getting Started -=============== +Installation instructions +========================= -There are different ways to install f3dasm: +There are different ways to install :mod:`f3dasm`: * :ref:`Install the latest official release `. This is the best approach for most users that want to use the f3dasm package. @@ -18,11 +17,11 @@ There are different ways to install f3dasm: .. _install_official_release: Installing the latest release -============================= +----------------------------- :mod:`f3dasm` is purely Python code and compatible with: -1. Python 3.7 or higher. +1. Python 3.8 or higher. 2. the three major operations system (Linux, MacOS, Ubuntu). 3. the `pip `_ package manager system. @@ -101,7 +100,7 @@ This will show the installed version of f3dasm. .. _install_from_source: Installing from source -====================== +---------------------- - The Python PyPI package (:code:`pip install f3dasm`) contains the code that is used when installing the package as a **user**. It contains only the :code:`main` branch version. diff --git a/docs/source/rst_doc_files/classes/workflow/hydra.rst b/docs/source/rst_doc_files/hydra.rst similarity index 100% rename from docs/source/rst_doc_files/classes/workflow/hydra.rst rename to docs/source/rst_doc_files/hydra.rst diff --git a/docs/source/rst_doc_files/supercompressible.rst b/docs/source/rst_doc_files/supercompressible.rst new file mode 100644 index 00000000..e78a255d --- /dev/null +++ b/docs/source/rst_doc_files/supercompressible.rst @@ -0,0 +1,316 @@ +Fragile becomes supercompressible +################################# + +.. raw:: html + +
+ Image 1 + Image 2 + Image 3 +
+ +This study is based on the work of `Bessa et al. (2019) `_ +and aims to reproduce the results of the paper using the ``f3dasm`` package. + +Summary +======= + +For years, creating new materials has been a time-consuming effort that requires significant resources because we have +followed a trial-and-error design process. Now, a new paradigm is emerging where machine learning is used to design new +materials and structures with unprecedented properties. Using this data-driven process, a new super-compressible +meta-material was discovered despite being made of a fragile polymer. + +The above figure shows the newly developed meta-material prototype that was designed with the above-mentioned computational +data-driven approach and where experiments were used for validation, not discovery. This enabled the design and additive +manufacturing of a lightweight, recoverable and super-compressible meta-material achieving more than 90% compressive strain +when using a brittle base material that breaks at around 4% strain. Within minutes, the machine learning model was used to +optimize designs for different choices of base material, length-scales, and manufacturing process. Current results show that +super-compressibility is possible for optimized designs reaching stresses on the order of 1 kPa using brittle polymers, or +stresses on the order of 20 MPa using carbon-like materials. + +Design of experiments +===================== + +The supercompressible meta-material is parameterized by 5 geometric parameters and 2 material parameters. The geometry is +defined by the top and bottom diameters, :math:`D_1` and :math:`D_2`, the height :math:`P` and the cross-section parameters of the +vertical longerons: the cross-sectional area :math:`A`, moments of inertia :math:`I_x` and :math:`I_y`, and torsional constant :math:`J`. +The isotropic material is defined by its elastic constants: Young's modulus :math:`E` and shear modulus :math:`G`. + +.. raw:: html + +
+ drawing +
+ +Due to the principle of superposition, both the geometric and material parameters can be scaled by one of its dimensions/properties +(here :math:`D_1` and :math:`E`). Therefore, the variables that you will find in the dataset are: + +.. math:: + + \frac{D_1-D_2}{D_1},\ \frac{P}{D_1},\ \frac{I_x}{D_1^4},\ \frac{I_y}{D_1^4},\ \frac{J}{D_1^4},\ \frac{A}{D_1^2}, \frac{G}{E} + ++-------------------------------+-------------------------+ +| expression | parameter name | ++===============================+=========================+ +| :math:`\frac{D_1-D_2}{D_1}` | ``ratio_top_diameter`` | ++-------------------------------+-------------------------+ +| :math:`\frac{P}{D_1}` | ``ratio_pitch`` | ++-------------------------------+-------------------------+ +| :math:`\frac{I_x}{D_1^4}` | ``ratio_Ixx`` | ++-------------------------------+-------------------------+ +| :math:`\frac{I_y}{D_1^4}` | ``ratio_Iyy`` | ++-------------------------------+-------------------------+ +| :math:`\frac{J}{D_1^4}` | ``ratio_J`` | ++-------------------------------+-------------------------+ +| :math:`\frac{A}{D_1^2}` | ``ratio_area`` | ++-------------------------------+-------------------------+ +| :math:`\frac{G}{E}` | ``ratio_shear_modulus`` | ++-------------------------------+-------------------------+ + +This is a 7-dimensional problem and learning the response surface may require a significant amount of training points [#]_. +Therefore, you will also consider a simpler version of the problem in 3 dimensions, defined by constraining the longerons' +cross-section to be circular with diameter :math:`d`, and choosing a particular material, leading to the following 3 features: + +.. math:: + + \frac{d}{D_1}, \frac{D_2-D_1}{D_1},\ \frac{P}{D_1} + ++-------------------------------+-------------------------+ +| expression | parameter name | ++===============================+=========================+ +| :math:`\frac{D_1-D_2}{D_1}` | ``ratio_top_diameter`` | ++-------------------------------+-------------------------+ +| :math:`\frac{P}{D_1}` | ``ratio_pitch`` | ++-------------------------------+-------------------------+ +| :math:`\frac{d}{D_1}` | ``ratio_d`` | ++-------------------------------+-------------------------+ + +.. [#] Remember the "curse of dimensionality"! + +Contents of this folder +======================= + ++----------------+----------------------------------------------+ +| File/Folder | Description | ++================+==============================================+ +| ``main.py`` | Main script to run the experiment | ++----------------+----------------------------------------------+ +| ``config.yaml``| Configuration file for the experiment | ++----------------+----------------------------------------------+ +| ``README.md`` | Explanation of this experiment | ++----------------+----------------------------------------------+ +| ``img/`` | Folder with images used in this file | ++----------------+----------------------------------------------+ +| ``pbsjob.sh`` | TORQUE job file to run the experiment in a | +| | cluster | ++----------------+----------------------------------------------+ +| ``outputs/`` | Folder with the results of running this | +| | experiment | ++----------------+----------------------------------------------+ + +.. note:: + + The ``outputs/`` folder is created when the experiment has been run for the first time. + +Usage +===== + +Before running the experiment +----------------------------- + +1. Install the ``abaqus2py`` package. See `here `_ for instructions. +2. Change the ``config.yaml`` file to your liking. See `here <#explanation-of-configyaml-parameters>`_ for an explanation of the parameters. + +Running the experiment on your local machine +-------------------------------------------- + +1. Navigate to this folder and run ``python main.py`` + +Running the experiment on a TORQUE cluster +------------------------------------------ + +1. Make sure you have an ``conda`` environment named ``f3dasm_env`` with the packages installed in the first step. +2. Navigate to this folder and submit the job with i.e. 2 nodes: ``qsub pbsjob.sh -t 0-2`` + +Results +======= + +Results are stored in a newly created ``outputs`` folder, with a subdirectory indicating the current date (e.g. ``2023-11-06``). + +* When running on a local machine, the output will be saved in a directory indicating the current time (e.g. ``13-50-14``). +* When running on a cluster, the output will be saved in a directory indicating the current job ID (e.g. ``538734.hpc06.hpc``). + +The following subdirectories are created: + +* ``experiment_data``: Contains the input, output, domain and jobs to construct the ``f3dasm.ExperimentData`` object. +* ``.hydra``: Contains the ``config.yaml`` file used to run the experiment. +* ``lin_buckle`` and ``riks``: Contain the ABAQUS simulation results for the linear buckling and Riks analysis, respectively. + +Lastly, a log file ``main.log`` is created. + +The folder structure is as follows: + +.. code-block:: text + + outputs/ + └── 2023-11-06/ + └── 13-50-14/ + ├── .hydra/ + ├── experiment_data/ + │ ├── domain.pkl + │ ├── input.csv + │ ├── output.csv + │ └── jobs.pkl + ├── lin_buckle/ + │ ├── 0/ + │ ├── 1/ + │ └── 2/ + ├── riks/ + │ ├── 0/ + │ ├── 1/ + │ └── 2/ + ├── loads/ + │ ├── 0.npy + │ ├── 1.npy + │ └── 2.npy + ├── max_disps/ + │ ├── 0.npy + │ ├── 1.npy + │ └── 2.npy + └── main.log + +Explanation of ``config.yaml`` parameters +========================================= + +There are two different configurations for this experiment: +- The full 7-dimensional problem as defined in the paper. +- The 3-dimensional problem, defined by constraining the longerons' cross-section to be circular with diameter :math:`d` and + choosing a fixed material. + +Common problem domain +--------------------- + +``young_modulus`` + Name Type Description + ------------- --------- --------------------------------- + ``value`` ``float`` Young's modulus value + +``n_longerons`` + Name Type Description + ------------- --------- --------------------------------- + ``value`` ``float`` Number of longerons in the design + +``bottom_diameter`` (:math:`D_2`) + Name Type Description + ------------- --------- --------------------------------- + ``value`` ``float`` Bottom diameter of the design + +``ratio_top_diameter`` (:math:`\frac{D_1-D_2}{D_1}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of top diameter ratio + ``high`` ``float`` Upper bound of top diameter ratio + +``ratio_pitch`` (:math:`\frac{P}{D_1}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of the pitch ratio + ``high`` ``float`` Upper bound of the pitch ratio + +3-dimensional problem domain +---------------------------- + +``ratio_d`` (:math:`\frac{d}{D_1}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of longerons cross-section + ``high`` ``float`` Upper bound of longerons cross-section + +``ratio_shear_modulus`` (:math:`\frac{G}{E}`) + Name Type Description + ------------- --------- --------------------------------- + ``value`` ``float`` Lower bound of shear modulus ratio + +7-dimensional problem domain +---------------------------- + +``ratio_area`` (:math:`\frac{A}{D_1^2}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of the area ratio + ``high`` ``float`` Upper bound of the area ratio + +``ratio_Ixx`` (:math:`\frac{I_x}{D_1^4}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of the :math:`I_{xx}` ratio + ``high`` ``float`` Upper bound of the :math:`I_{xx}` ratio + +``ratio_Iyy`` (:math:`\frac{I_y}{D_1^4}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of the :math:`I_{yy}` ratio + ``high`` ``float`` Upper bound of the :math:`I_{yy}` ratio + +``ratio_J`` (:math:`\frac{J}{D_1^4}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of the :math:`J` ratio + ``high`` ``float`` Upper bound of the :math:`J` ratio + +``ratio_shear_modulus`` (:math:`\frac{G}{E}`) + Name Type Description + ------------- --------- --------------------------------- + ``low`` ``float`` Lower bound of shear modulus ratio + ``high`` ``float`` Upper bound of shear modulus ratio + +``circular`` + Name Type Description + ------------- --------- --------------------------------- + ``value`` ``bool`` If the design is simplified or not + +Experiment Data +--------------- + +``from Sampling`` + Name Type Description + ------------- -------------------- --------------------------------- + ``sampler`` ``str`` Sampler name + ``seed`` ``int`` Seed value + ``n_samples`` ``int`` Number of samples + ``domain`` ``f3dasm.Domain`` ``f3dasm`` Domain object + +``mode`` + Name Type Description + ------------- --------- --------------------------------- + ``mode`` ``string`` Evaluation mode of ``f3dasm`` + +``hpc`` + Name Type Description + ------------- --------- --------------------------------- + ``jobid`` ``int`` Job ID of the array-job, automatically + overwritten by scheduler bash script + +.. [#] When running on a local machine, this value will be left as the default: -1. + +``imperfection`` + Name Type Description + ------------- --------- --------------------------------- + ``mean`` ``float`` Mean value of lognormal distribution + ``std`` ``float`` Standard deviation value of lognormal distribution + +``scripts`` + Name Type Description + ------------------ --------- --------------------------------------- + ``lin_buckle_pre`` ``str`` Absolute path of linear buckling script + ``lin_buckle_post````str`` Absolute path of linear buckling post-processing script + ``riks_pre`` ``str`` Absolute path of RIKS analysis script + ``riks_post`` ``str`` Absolute path of RIKS analysis post-processing script + +Logging +------- + +``log_level`` + Name Type Description + ------------- --------- --------------------------------- + ``log_level`` ``int`` Log level value diff --git a/examples/001_domain/001_domain_creation.py b/examples/001_domain/001_domain_creation.py new file mode 100644 index 00000000..5e6d6b32 --- /dev/null +++ b/examples/001_domain/001_domain_creation.py @@ -0,0 +1,89 @@ +""" +Introduction to domain and parameters +===================================== + +This section will give you information on how to set up your search space with the :ref:`domain ` class and the :ref:`parameters ` +The :class:`~f3dasm.design.Domain` is a set of parameter instances that make up the feasible search space. +""" +############################################################################### +# To start, we create an empty domain object: +import numpy as np +from hydra import compose, initialize + +from f3dasm.design import Domain, make_nd_continuous_domain + +domain = Domain() + +############################################################################### +# Input parameters +# ---------------- +# +# Now we well add some input parameters: +# There are four types of parameters that can be created: +# +# - :ref:`floating point ` parameters + +domain.add_float(name='x1', low=0.0, high=100.0) +domain.add_float(name='x2', low=0.0, high=4.0) + +############################################################################### +# - :ref:`discrete integer pramaters ` + +domain.add_int(name='x3', low=2, high=4) +domain.add_int(name='x4', low=74, high=99) + +############################################################################### +# - :ref:`categorical parameters ` + +domain.add_category(name='x5', categories=['test1', 'test2', 'test3', 'test4']) +domain.add_category(name='x6', categories=[0.9, 0.2, 0.1, -2]) + +############################################################################### +# - :ref:`constant parameters` + +domain.add_constant(name='x7', value=0.9) + +############################################################################### +# We can print the domain object to see the parameters that have been added: + +print(domain) + +############################################################################### +# Output parameters +# ----------------- +# +# Output parameters are the results of evaluating the input design with a data generation model. +# Output parameters can hold any type of data, e.g. a scalar value, a vector, a matrix, etc. +# Normally, you would not need to define output parameters, as they are created automatically when you store a variable to the :class:`~f3dasm.ExperimentData` object. + +domain.add_output(name='y', to_disk=False) + +############################################################################### +# The :code:`to_disk` argument can be set to :code:`True` to store the output parameter on disk. A reference to the file is stored in the :class:`~f3dasm.ExperimentData` object. +# This is useful when the output data is very large, or when the output data is an array-like object. +# More information on storing output can be found in :ref:`this section ` + +############################################################################### +# Filtering the domain +# -------------------- +# +# The domain object can be filtered to only include certain types of parameters. +# This might be useful when you want to create a design of experiments with only continuous parameters, for example. +# The attributes :attr:`~f3dasm.design.Domain.continuous`, :attr:`~f3dasm.design.Domain.discrete`, :attr:`~f3dasm.design.Domain.categorical`, and :attr:`~f3dasm.design.Domain.constant` can be used to filter the domain object. + +print(f"Continuous domain: {domain.continuous}") +print(f"Discrete domain: {domain.discrete}") +print(f"Categorical domain: {domain.categorical}") +print(f"Constant domain: {domain.constant}") + +############################################################################### +# Helper function for single-objective, n-dimensional continuous domains +# ---------------------------------------------------------------------- +# +# We can make easily make a :math:`n`-dimensional continous domain with the helper function :func:`~f3dasm.design.make_nd_continuous_domain`. +# We have to specify the boundaries (``bounds``) for each of the dimensions with a list of lists or numpy :class:`~numpy.ndarray`: + +bounds = np.array([[-1.0, 1.0], [-1.0, 1.0]]) +domain = make_nd_continuous_domain(bounds=bounds) + +print(domain) diff --git a/examples/001_domain/002_own_sampler.py b/examples/001_domain/002_own_sampler.py new file mode 100644 index 00000000..f5cc2265 --- /dev/null +++ b/examples/001_domain/002_own_sampler.py @@ -0,0 +1,103 @@ +""" +Implementing a grid search sampler from scratch +=============================================== + +In this example, we will implement a `grid search sampler `_ from scratch. +The grid search sampler is a simple sampler that evaluates all possible combinations of the parameters in the domain. This is useful for small domains, but it can become computationally expensive for larger domains. +We will show how to create this sampler and use it in a :mod:`f3dasm` data-driven experiment. +""" + +from __future__ import annotations + +from itertools import product +from typing import Dict, Optional + +import numpy as np +import pandas as pd + +from f3dasm import ExperimentData +from f3dasm.design import Domain + +############################################################################### +# When integrating your sampling strategy into the data-driven process, you have to create a function that will take the domain as argument: +# Several other optional arguments can be passed to the function as well, such as: +# +# * :code:`n_samples`: The number of samples you wish to generate. It's not always necessary to define this upfront, as some sampling methods might inherently determine the number of samples based on certain criteria or properties of the domain. +# * :code:`seed`: A seed for the random number generator to replicate the sampling process. This enables you to control the randomness of the sampling process [1]_. By setting a seed, you ensure reproducibility, meaning that if you run the sampling function with the same seed multiple times, you'll get the same set of samples. +# +# .. [1] If no seed is provided, the function should use a random seed. +# +# Additionally, the function can accept any other keyword arguments that you might need to pass to the sampling function. +# This also means that you shoud handle arbitrary keyword arguments in your function with the `**kwargs` syntax. +# The function should return the samples (``input_data``) in one of the following formats: +# +# * A :class:`~pandas.DataFrame` object +# * A :class:`~numpy.ndarray` object +# +# For our implementation of the grid-search sampler, we need to handle the case of continous parameters. +# We require the user to pass down a dictionary with the discretization stepsize for each continuous parameter. + + +def grid( + domain: Domain, stepsize_continuous_parameters: + Optional[Dict[str, float] | float] = None, **kwargs) -> pd.DataFrame: + + # Extract the continous part of the domain + continuous = domain.continuous + + # If therei s no continuos space, we can return an empty dictionary + if not continuous.space: + discrete_space = {} + + else: + discrete_space = {key: continuous.space[key].to_discrete( + step=value) for key, + value in stepsize_continuous_parameters.items()} + + continuous_to_discrete = Domain(discrete_space) + + _iterdict = {} + + # For all the categorical parameters, we will iterate over the categories + for k, v in domain.categorical.space.items(): + _iterdict[k] = v.categories + + # For all the discrete parameters, we will iterate over the range of values + for k, v, in domain.discrete.space.items(): + _iterdict[k] = range(v.lower_bound, v.upper_bound+1, v.step) + + # For all the continuous parameters, we will iterate over the range of values + # based on the stepsize provided + for k, v, in continuous_to_discrete.space.items(): + _iterdict[k] = np.arange( + start=v.lower_bound, stop=v.upper_bound, step=v.step) + + # We will create a dataframe with all the possible combinations using + # the itertools.product function + df = pd.DataFrame(list(product(*_iterdict.values())), + columns=_iterdict, dtype=object)[domain.names] + + # return the samples + return df + +############################################################################### +# To test our implementation, we will create a domain with a mix of continuous, discrete, and categorical parameters. + + +domain = Domain() +domain.add_float("param_1", -1.0, 1.0) +domain.add_int("param_2", 1, 5) +domain.add_category("param_3", ["red", "blue", "green", "yellow", "purple"]) + +############################################################################### +# We will now sample the domain using the grid sampler we implemented. +# We can create an empty ExperimentData object and call the :meth:`~f3dasm.data.ExperimentData.sample` method to add the samples to the object: + +experiment_data = ExperimentData(domain=domain) +experiment_data.sample( + sampler=grid, stepsize_continuous_parameters={"param_1": 0.2}) + +############################################################################### +# We can print the samples to see the results: + +print(experiment_data) diff --git a/examples/001_domain/003_builtin_sampler.py b/examples/001_domain/003_builtin_sampler.py new file mode 100644 index 00000000..67dfcf79 --- /dev/null +++ b/examples/001_domain/003_builtin_sampler.py @@ -0,0 +1,51 @@ +""" +Use the built-in sampling strategies +==================================== + +In this example, we will use the built-in sampling strategies provided by :mod:`f3dasm` to generate samples for a data-driven experiment. +""" + +from matplotlib import pyplot as plt + +from f3dasm import ExperimentData +from f3dasm.design import make_nd_continuous_domain + +############################################################################### +# We create 2D continuous input domain with the :func:`~f3dasm.design.make_nd_continuous_domain` helper function: + +domain = make_nd_continuous_domain(bounds=[[0., 1.], [0., 1.]]) +print(domain) + +############################################################################### +# You can create an :class:`~f3dasm.ExperimentData` object with the :meth:`~f3dasm.ExperimentData.from_sampling` constructor directly: + +data_random = ExperimentData.from_sampling( + domain=domain, n_samples=10, sampler='random', seed=42) + +fig, ax = plt.subplots(figsize=(4, 4)) + +print(data_random) + +df_random, _ = data_random.to_pandas() +ax.scatter(df_random.iloc[:, 0], df_random.iloc[:, 1]) +ax.set_xlabel(domain.names[0]) +ax.set_ylabel(domain.names[1]) + +############################################################################### +# :mod:`f3dasm` provides several built-in samplers. +# The example below shows how to use the Latin Hypercube Sampling (LHS) sampler: + +data_lhs = ExperimentData.from_sampling( + domain=domain, n_samples=10, sampler='latin', seed=42) + +fig, ax = plt.subplots(figsize=(4, 4)) + +print(data_lhs) + +df_lhs, _ = data_lhs.to_pandas() +ax.scatter(df_lhs.iloc[:, 0], df_lhs.iloc[:, 1]) +ax.set_xlabel(domain.names[0]) +ax.set_ylabel(domain.names[1]) + +############################################################################### +# More information all the available samplers can be found in :ref:`here `. diff --git a/examples/001_domain/README.rst b/examples/001_domain/README.rst new file mode 100644 index 00000000..26d0da6a --- /dev/null +++ b/examples/001_domain/README.rst @@ -0,0 +1,4 @@ +Design-of-experiments +^^^^^^^^^^^^^^^^^^^^^ + +The submodule :mod:`f3dasm.design` contains the :class:`~f3dasm.design.Domain` object that makes up your feasible search space. diff --git a/examples/002_experimentdata/001_experimentdata.py b/examples/002_experimentdata/001_experimentdata.py new file mode 100644 index 00000000..e86088eb --- /dev/null +++ b/examples/002_experimentdata/001_experimentdata.py @@ -0,0 +1,138 @@ +""" +Creating an ExperimentData object from various external sources +=============================================================== + +The :class:`~f3dasm.ExperimentData` object is the main object used to store implementations of a design-of-experiments, +keep track of results, perform optimization and extract data for machine learning purposes. + +All other processses of :mod:`f3dasm` use this object to manipulate and access data about your experiments. + +The :class:`~f3dasm.ExperimentData` object consists of the following attributes: + +- :ref:`domain `: The feasible :class:`~f3dasm.design.Domain` of the Experiment. Used for sampling and optimization. +- :ref:`input_data `: Tabular data containing the input variables of the experiments as column and the experiments as rows. +- :ref:`output_data `: Tabular data containing the tracked outputs of the experiments. +- :ref:`project_dir `: A user-defined project directory where all files related to your data-driven process will be stored. +""" + +############################################################################### +# The :class:`~f3dasm.ExperimentData` object can be constructed in several ways: +# +# You can construct a :class:`~f3dasm.ExperimentData` object by providing it :ref:`input_data `, +# :ref:`output_data `, a :ref:`domain ` object and a :ref:`project_dir `. + +import numpy as np +import pandas as pd + +from f3dasm import ExperimentData +from f3dasm.design import Domain + +############################################################################### +# domain +# ^^^^^^ +# The domain object is used to define the feasible space of the experiments. + +domain = Domain() +domain.add_float('x0', 0., 1.) +domain.add_float('x1', 0., 1.) + +############################################################################### +# input_data +# ^^^^^^^^^^ +# +# Input data describes the input variables of the experiments. +# The input data is provided in a tabular manner, with the number of rows equal to the number of experiments and the number of columns equal to the number of input variables. +# +# Single parameter values can have any of the basic built-in types: ``int``, ``float``, ``str``, ``bool``. Lists, tuples or array-like structures are not allowed. +# +# We can give the input data as a :class:`~pandas.DataFrame` object with the input variable names as columns and the experiments as rows. + +input_data = pd.DataFrame({ + 'x0': [0.1, 0.2, 0.3], + 'x1': [0.4, 0.5, 0.6] +}) + +experimentdata = ExperimentData(domain=domain, input_data=input_data) +print(experimentdata) + +############################################################################### +# or a two-dimensional :class:`~numpy.ndarray` object with shape (, ): + +input_data = np.array([ + [0.1, 0.4], + [0.2, 0.5], + [0.3, 0.6] +]) + +experimentdata = ExperimentData(domain=domain, input_data=input_data) +print(experimentdata) + +############################################################################### +# .. note:: +# +# When providing a :class:`~numpy.ndarray` object, you need to provide a :class:`~f3dasm.design.Domain` object as well. +# Also, the order of the input variables is inferred from the order of the columns in the :class:`~f3dasm.design.Domain` object. + +############################################################################### +# Another option is a path to a ``.csv`` file containing the input data. +# The ``.csv`` file should contain a header row with the names of the input variables +# and the first column should be indices for the experiments. +# +# output_data +# ^^^^^^^^^^^ +# +# Output data describes the output variables of the experiments. +# The output data is provided in a tabular manner, with the number of rows equal to the number of experiments and the number of columns equal to the number of output variables. +# +# The same rules apply for the output data as for the input data: + +output_data = pd.DataFrame({ + 'y': [1.1, 1.2, 1.3], +}) + +experimentdata = ExperimentData(domain=domain, input_data=input_data, + output_data=output_data) + +print(experimentdata) + +############################################################################### +# .. note:: +# +# When the output to an ExperimentData object is provided, the job will be set to finished, +# as the output data is considerd the result of the experiment. +# +# Adding data after constructing +# ------------------------------ +# +# If you have constructed your :class:`~f3dasm.ExperimentData` object, +# you can add ``input_data``, ``output_data``, a ``domain`` or the ``project_dir`` using the :meth:`~f3dasm.ExperimentData.add` method: + +new_data = pd.DataFrame({ + 'x0': [1.5, 1.7], + 'x1': [1.3, 1.9] +}) +experimentdata.add(input_data=new_data, domain=domain) +print(experimentdata) + +############################################################################### +# Exporting the data to various formats +# ------------------------------------- +# +# You can convert the input- and outputdata of your data-driven process to other well-known datatypes: +# +# * :meth:`~f3dasm.ExperimentData.to_numpy`; creates a tuple of two :class:`~numpy.ndarray` objects containing the input- and outputdata. + +arr_input, arr_output = experimentdata.to_numpy() +print(arr_input) + +############################################################################### +# * :meth:`~f3dasm.ExperimentData.to_xarray`; creates a :class:`~xarray.Dataset` object containing the input- and outputdata. + +ds = experimentdata.to_xarray() +print(ds) + +############################################################################### +# * :meth:`~f3dasm.ExperimentData.to_pandas`; creates a tuple of two :class:`~pd.DataFrame` object containing the input- and outputdata. + +df_input, df_output = experimentdata.to_pandas() +print(df_input) diff --git a/examples/002_experimentdata/002_experimentdata_storing.py b/examples/002_experimentdata/002_experimentdata_storing.py new file mode 100644 index 00000000..40279fa2 --- /dev/null +++ b/examples/002_experimentdata/002_experimentdata_storing.py @@ -0,0 +1,50 @@ +""" +Storing experiment data to disk +=============================== + +In this example, we will show how to store the experiment data to disk using the :meth:`~f3dasm.ExperimentData.store` method and +how to load the stored data using the :meth:`~f3dasm.ExperimentData.from_file` method. +""" + +############################################################################### +# project directory +# ^^^^^^^^^^^^^^^^^ +# +# The ``project_dir`` argument is used to :ref:`store the ExperimentData to disk ` +# You can provide a string or a path to a directory. This can either be a relative or absolute path. +# If the directory does not exist, it will be created. + +from f3dasm import ExperimentData + +data = ExperimentData() +data.set_project_dir("./example_project_dir") + +print(data.project_dir) + +############################################################################### +# Storing the data +# ^^^^^^^^^^^^^^^^^ +# +# The :meth:`~f3dasm.ExperimentData.store` method is used to store the experiment data to disk. + +data.store() + +############################################################################### +# The data is stored in several files in an 'experiment_data' subfolder in the provided project directory: +# +# .. code-block:: none +# :caption: Directory Structure +# +# my_project/ +# ├── my_script.py +# └── experiment_data +# ├── domain.pkl +# ├── input_data.csv +# ├── output_data.csv +# └── jobs.pkl +# +# In order to load the data, you can use the :meth:`~f3dasm.ExperimentData.from_file` method. + +data_loaded = ExperimentData.from_file(project_dir="./example_project_dir") + +print(data_loaded) diff --git a/examples/002_experimentdata/README.rst b/examples/002_experimentdata/README.rst new file mode 100644 index 00000000..4da7ded9 --- /dev/null +++ b/examples/002_experimentdata/README.rst @@ -0,0 +1,7 @@ +Managing experiments with the ExperimentData object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`~f3dasm.ExperimentData` object is the main object used to store implementations of a design-of-experiments, +keep track of results, perform optimization and extract data for machine learning purposes. + +All other processses of :mod:`f3dasm` use this object to manipulate and access data about your experiments. diff --git a/examples/002_experimentdata/example_project_dir/experiment_data/domain.pkl b/examples/002_experimentdata/example_project_dir/experiment_data/domain.pkl new file mode 100644 index 00000000..6b319cb7 Binary files /dev/null and b/examples/002_experimentdata/example_project_dir/experiment_data/domain.pkl differ diff --git a/examples/002_experimentdata/example_project_dir/experiment_data/input.csv b/examples/002_experimentdata/example_project_dir/experiment_data/input.csv new file mode 100644 index 00000000..e16c76df --- /dev/null +++ b/examples/002_experimentdata/example_project_dir/experiment_data/input.csv @@ -0,0 +1 @@ +"" diff --git a/examples/002_experimentdata/example_project_dir/experiment_data/jobs.pkl b/examples/002_experimentdata/example_project_dir/experiment_data/jobs.pkl new file mode 100644 index 00000000..1884af77 Binary files /dev/null and b/examples/002_experimentdata/example_project_dir/experiment_data/jobs.pkl differ diff --git a/examples/002_experimentdata/example_project_dir/experiment_data/output.csv b/examples/002_experimentdata/example_project_dir/experiment_data/output.csv new file mode 100644 index 00000000..e16c76df --- /dev/null +++ b/examples/002_experimentdata/example_project_dir/experiment_data/output.csv @@ -0,0 +1 @@ +"" diff --git a/examples/003_datageneration/001_own_datagenerator.py b/examples/003_datageneration/001_own_datagenerator.py new file mode 100644 index 00000000..0a004427 --- /dev/null +++ b/examples/003_datageneration/001_own_datagenerator.py @@ -0,0 +1,194 @@ +""" +Implement your own datagenerator: car stopping distance problem +=============================================================== + +In this example, we will implement a custom data generator that generates output for a data-driven experiment. +We will use the 'car stopping distance' problem as an example. +""" + +import matplotlib.pyplot as plt +import numpy as np +from scipy.stats import norm + +from f3dasm import ExperimentData +from f3dasm.datageneration import DataGenerator +from f3dasm.design import Domain + +############################################################################### +# +# Car stopping distance problem +# ----------------------------- +# +# .. image:: ../../img/reaction-braking-stopping.png +# :width: 70% +# :align: center +# :alt: Workflow +# +# Car stopping distance :math:`y` as a function of its velocity :math:`x` before it starts braking: +# +# .. math:: +# +# y = z x + \frac{1}{2 \mu g} x^2 = z x + 0.1 x^2 +# +# +# - :math:`z` is the driver's reaction time (in seconds) +# - :math:`\mu` is the road/tires coefficient of friction (we assume :math:`\mu=0.5`) +# - :math:`g` is the acceleration of gravity (assume :math:`g=10 m/s^2`). +# +# .. math:: +# +# y = d_r + d_{b} +# +# where :math:`d_r` is the reaction distance, and :math:`d_b` is the braking distance. +# +# Reaction distance :math:`d_r` +# +# .. math:: +# +# d_r = z x +# +# with :math:`z` being the driver's reaction time, and :math:`x` being the velocity of the car at the start of braking. +# +# Kinetic energy of moving car: +# +# .. math:: +# +# E = \frac{1}{2}m x^2 +# +# where :math:`m` is the car mass. +# +# Work done by braking: +# +# .. math:: +# +# W = \mu m g d_b +# +# +# where :math:`\mu` is the coefficient of friction between the road and the tire, :math:`g` is the acceleration of gravity, and :math:`d_b` is the car braking distance. +# +# The braking distance follows from :math:`E=W`: +# +# .. math:: +# +# d_b = \frac{1}{2\mu g}x^2 +# +# Therefore, if we add the reacting distance :math:`d_r` to the braking distance :math:`d_b` we get the stopping distance :math:`y`: +# +# .. math:: +# +# y = d_r + d_b = z x + \frac{1}{2\mu g} x^2 +# +# +# Every driver has its own reaction time :math:`z` +# Assume the distribution associated to :math:`z` is Gaussian with mean :math:`\mu_z=1.5` seconds and variance :math:`\sigma_z^2=0.5^2`` seconds\ :sup:`2`: +# +# .. math:: +# +# z \sim \mathcal{N}(\mu_z=1.5,\sigma_z^2=0.5^2) +# +# +# We create a function that generates the stopping distance :math:`y` given the velocity :math:`x` and the reaction time :math:`z`: + + +def y(x): + z = norm.rvs(1.5, 0.5, size=1) + y = z*x + 0.1*x**2 + return y + + +############################################################################### +# Implementing the data generator +# ------------------------------- +# +# Implementing this relationship in :mod:`f3dasm` can be done in two ways: +# +# +# 1. Directly using a function +# 2. Providing an object from a custom class that inherits from the :class:`~f3dasm.datageneration.DataGenerator` class. +# +# Using a function directly +# ^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# We can use the function :func:`y(x)` directly as the data generator. We will demonstrate this in the following example code: +# +# +# In order to create an :class:`~f3dasm.ExperimentData` object, we have to first create a domain +domain = Domain() +domain.add_float('x', low=0., high=100.) + +############################################################################### +# For demonstration purposes, we will generate a dataset of stopping distances for velocities between 3 and 83 m/s. + +N = 33 # number of points to generate +Data_x = np.linspace(3, 83, 100) + +############################################################################### +# We can construct an :class:`~f3dasm.ExperimentData` object with the :class:`~f3dasm.design.Domain` and the numpy array: + +experiment_data = ExperimentData(input_data=Data_x, domain=domain) +print(experiment_data) + +############################################################################### +# As you can see, the ExperimentData object has been created successfully and the jobs have the label 'open'. +# This means that the output has not been generated yet. We can now compute the stopping distance by calling the :meth:`~f3dasm.ExperimentData.evaluate` method: +# We have to provide the function as the ``data_generator`` argument and provide name of the return value as the ``output_names`` argument: + +experiment_data.evaluate(data_generator=y, output_names=['y']) + +arr_in, arr_out = experiment_data.to_numpy() + +fig, ax = plt.subplots() +ax.scatter(arr_in, arr_out.flatten(), s=2) +_ = ax.set_xlabel('Car velocity ($m/s$)') +_ = ax.set_ylabel('Stopping distance ($m$)') + +############################################################################### +# The experiments have been evaluated and the jobs value has been set to 'finished' + +print(experiment_data) + +############################################################################### +# +# Using the DataGenerator class +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# We can also implement the data generator as a class that inherits from the :class:`~f3dasm.datageneration.DataGenerator` class. +# This allows for more flexibility and control over the data generation process. + +experiment_data_class = ExperimentData(input_data=Data_x, domain=domain) + +############################################################################### +# The custom data generator class should have an :meth:`~f3dasm.datageneration.DataGenerator.execute` method. +# In this method, we can access the experiment using the :attr:`~f3dasm.datageneration.DataGenerator.experiment_sample` attribute. +# We can store the output of the data generation process using the :meth:`~f3dasm.datageneration.DataGenerator.experiment_sample.store` method. + + +class CarStoppingDistance(DataGenerator): + def __init__(self, mu_z: float, sigma_z: float): + self.mu_z = mu_z + self.sigma_z = sigma_z + + def execute(self): + x = self.experiment_sample.get('x') + z = norm.rvs(self.mu_z, self.sigma_z, size=1) + y = z*x + 0.1*x**2 + self.experiment_sample.store(object=y, name='y', to_disk=False) + +############################################################################### +# We create an object of the :class:`~CarStoppingDistance` class and pass it to the :meth:`~f3dasm.ExperimentData.evaluate` method: + + +car_stopping_distance = CarStoppingDistance(mu_z=1.5, sigma_z=0.5) +experiment_data_class.evaluate( + data_generator=car_stopping_distance, mode='sequential') + +print(experiment_data_class) + +############################################################################### +# +# There are three methods available of evaluating the experiments: +# +# * :code:`sequential`: regular for-loop over each of the experiments in order +# * :code:`parallel`: utilizing the multiprocessing capabilities (with the `pathos `_ multiprocessing library), each experiment is run in a separate core +# * :code:`cluster`: each experiment is run in a seperate node. This is especially useful on a high-performance computation cluster where you have multiple worker nodes and a commonly accessible resource folder. After completion of an experiment, the node will automatically pick the next available open experiment. +# * :code:`cluster_parallel`: Combination of the :code:`cluster` and :code:`parallel` mode. Each node will run multiple samples in parallel. diff --git a/examples/003_datageneration/002_builtin_benchmarkfunctions.py b/examples/003_datageneration/002_builtin_benchmarkfunctions.py new file mode 100644 index 00000000..3d1d6b3d --- /dev/null +++ b/examples/003_datageneration/002_builtin_benchmarkfunctions.py @@ -0,0 +1,62 @@ +""" +Use the built-in benchmark functions +==================================== + +In this example, we will use the built-in benchmark functions provided by :mod:`f3dasm.datageneration.functions` to generate output for a data-driven experiment. +""" + +import matplotlib.pyplot as plt + +from f3dasm import ExperimentData +from f3dasm.design import make_nd_continuous_domain + +############################################################################### +# :mod:`f3dasm` ships with a set of benchmark functions that can be used to test the performance of +# optimization algorithms or to mock some expensive simulation in order to test the data-driven process. +# These benchmark functions are taken and modified from the `Python Benchmark Test Optimization Function Single Objective `_ github repository. +# +# Let's start by creating a continuous domain +# with 2 input variables, each ranging from -1.0 to 1.0 + +domain = make_nd_continuous_domain([[-1., 1.], [-1., 1.]]) + +############################################################################### +# We generate the input data by sampling the domain equally spaced with the grid sampler and create the :class:`~f3dasm.ExperimentData` object: + +experiment_data = ExperimentData.from_sampling( + 'grid', domain=domain, stepsize_continuous_parameters=0.1) + +print(experiment_data) + +############################################################################### +# Evaluating a 2D version of the Ackley function is as simple as +# calling the :meth:`~f3dasm.ExperimentData.evaluate` method with the function name as the ``data_generator`` argument. +# +# In addition, you can provide a dictionary (``kwargs``) with the followinging keywords to the :class:`~f3dasm.design.ExperimentData.evaluate` method: +# +# * ``scale_bounds``: A 2D list of floats that define the scaling lower and upper boundaries for each dimension. The normal benchmark function box-constraints will be scaled to these boundaries. +# * ``noise``: A float that defines the standard deviation of the Gaussian noise that is added to the objective value. +# * ``offset``: A boolean value. If ``True``, the benchmark function will be offset by a constant vector that will be randomly generated [1]_. +# * ``seed``: Seed for the random number generator for the ``noise`` and ``offset`` calculations. +# +# .. [1] As benchmark functions usually have their minimum at the origin, the offset is used to test the robustness of the optimization algorithm. + +experiment_data.evaluate(data_generator='Ackley', kwargs={ + 'scale_bounds': domain.get_bounds(), 'offset': False}) + +############################################################################### +# The function values are stored in the ``y`` variable of the output data: + +print(experiment_data) + +############################################################################### + +arr_in, arr_out = experiment_data.to_numpy() +fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) +ax.scatter(arr_in[:, 0], arr_in[:, 1], arr_out.ravel()) +_ = ax.set_xlabel('$x_0$') +_ = ax.set_ylabel('$x_1$') +_ = ax.set_zlabel('$f(x)$') + +############################################################################### +# A complete list of all the implemented benchmark functions can be found :ref:`here ` diff --git a/examples/003_datageneration/003_storing.py b/examples/003_datageneration/003_storing.py new file mode 100644 index 00000000..3b2d0477 --- /dev/null +++ b/examples/003_datageneration/003_storing.py @@ -0,0 +1,142 @@ +""" +Storing data generation output to disk +====================================== + +After running your simulation, you can store the result back into the :class:`~f3dasm.ExperimentSample` with the :meth:`~f3dasm.ExperimentSample.store` method. +There are two ways of storing your output: + +* Singular values can be stored directly to the :attr:`~f3dasm.ExperimentData.output_data` +* Large objects can be stored to disk and a reference path will be stored to the :attr:`~f3dasm.ExperimentData.output_data`. +""" + +import numpy as np + +from f3dasm import ExperimentData, StoreProtocol +from f3dasm.datageneration import DataGenerator +from f3dasm.design import make_nd_continuous_domain + +############################################################################### +# For this example we create a 3 dimensional continuous domain and generate 10 random samples. + +domain = make_nd_continuous_domain([[0., 1.], [0., 1.], [0., 1.]]) +experiment_data = ExperimentData.from_sampling( + sampler='random', domain=domain, n_samples=10, seed=42) + +############################################################################### +# Single values +# ------------- + +# Single values or small lists can be stored to the :class:`~f3dasm.ExperimentData` using the ``to_disk=False`` argument, with the name of the parameter as the key. +# This will create a new output parameter if the parameter name is not found in :attr:`~f3dasm.ExperimentData.output_data` of the :class:`~f3dasm.ExperimentData` object: +# This is especially useful if you want to get a quick overview of some loss or design metric of your sample. +# +# We create a custom datagenerator that sums the input features and stores the result back to the :class:`~f3dasm.ExperimentData` object: + + +class MyDataGenerator_SumInput(DataGenerator): + def execute(self): + input_, _ = self.experiment_sample.to_numpy() + y = float(sum(input_)) + self.experiment_sample.store(object=y, name='y', to_disk=False) + +############################################################################### +# We pass the custom data generator to the :meth:`~f3dasm.ExperimentData.evaluate` method and inspect the experimentdata after completion: + + +my_data_generator_single = MyDataGenerator_SumInput() + +experiment_data.evaluate(data_generator=my_data_generator_single) +print(experiment_data) + +############################################################################### +# +# All built-in singular types are supported for storing to the :class:`~f3dasm.ExperimentData` this way. Array-like data such as numpy arrays and pandas dataframes are **not** supported and will raise an error. +# +# .. note:: +# +# Outputs stored directly to the :attr:`~f3dasm.ExperimentData.output_data` will be stored within the :class:`~f3dasm.ExperimentData` object. +# This means that the output will be loaded into memory everytime this object is accessed. For large outputs, it is recommended to store the output to disk. +# +# Large objects and array-like data +# --------------------------------- +# +# In order to store large objects or array-like data, the :meth:`~f3dasm.ExperimentSample.store` method using the ``to_disk=True`` argument, can be used. +# A reference (:code:`Path`) will be saved to the :attr:`~f3dasm.ExperimentData.output_data`. +# +# We create a another custom datagenerator that doubles the input features, but leaves them as an array: + +experiment_data = ExperimentData.from_sampling( + sampler='random', domain=domain, n_samples=10, seed=42) + + +class MyDataGenerator_DoubleInputs(DataGenerator): + def execute(self): + input_, output_ = self.experiment_sample.to_numpy() + y = input_ * 2 + self.experiment_sample.store( + object=y, name='output_numpy', to_disk=True) + + +my_data_generator = MyDataGenerator_DoubleInputs() + +experiment_data.evaluate(data_generator=my_data_generator) +print(experiment_data) + +############################################################################### +# :mod:`f3dasm` will automatically create a new directory in the project directory for each output parameter and store the object with a generated filename referencing the :attr:`~f3dasm.design.ExperimentSample.job_number` of the design. +# +# .. code-block:: none +# :caption: Directory Structure +# +# project_dir/ +# ├── output_numpy/ +# │ ├── 0.npy +# │ ├── 1.npy +# │ ├── 2.npy +# │ └── 3.npy +# │ +# └── experiment_data/ +# ├── domain.pkl +# ├── input.csv +# ├── output.csv +# └── jobs.pkl +# +# +# In the output data of the :class:`~f3dasm.ExperimentData` object, a reference path (e.g. :code:`/output_numpy/0.npy`) to the stored object will be saved. +# +# Create a custom storage method +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# :mod:`f3dasm` has built-in storing functions for numpy :class:`~numpy.ndarray`, pandas :class:`~pandas.DataFrame` and xarray :class:`~xarray.DataArray` and :class:`~xarray.Dataset` objects. +# For any other type of object, the object will be stored in the `pickle `_ format +# +# You can provide your own storing class to the :class:`~f3dasm.ExperimentSample.store` method call: +# +# * a ``store`` method should store an ``self.object`` to disk at the location of ``self.path`` +# * a ``load`` method should load the object from disk at the location of ``self.path`` and return it +# * a class variable ``suffix`` should be defined, which is the file extension of the stored object as a string. +# * the class should inherit from the :class:`~f3dasm.StoreProtocol` class +# +# You can take the following class for a :class:`~numpy.ndarray` object as an example: + + +class NumpyStore(StoreProtocol): + suffix: int = '.npy' + + def store(self) -> None: + np.save(file=self.path.with_suffix(self.suffix), arr=self.object) + + def load(self) -> np.ndarray: + return np.load(file=self.path.with_suffix(self.suffix)) + +############################################################################### +# After defining the storing function, it can be used as an additional argument in the :meth:`~f3dasm.ExperimentSample.store` method: + + +class MyDataGenerator_DoubleInputs(DataGenerator): + def execute(self): + input_, output_ = self.experiment_sample.to_numpy() + y = input_ * 2 + self.experiment_sample.store( + object=y, name='output_numpy', + to_disk=True, store_method=NumpyStore) diff --git a/examples/003_datageneration/README.rst b/examples/003_datageneration/README.rst new file mode 100644 index 00000000..8726d463 --- /dev/null +++ b/examples/003_datageneration/README.rst @@ -0,0 +1,8 @@ +Generating output using the Data Generator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`~f3dasm.datageneration.DataGenerator` class is the main class of the :mod:`~f3dasm.datageneration` module. +It is used to generate :attr:`~f3dasm.ExperimentData.output_data` for the :class:`~f3dasm.ExperimentData`. + +The :class:`~f3dasm.datageneration.DataGenerator` can serve as the interface between the +:class:`~f3dasm.ExperimentData` object and any third-party simulation software. \ No newline at end of file diff --git a/examples/003_datageneration/output_numpy/0.npy b/examples/003_datageneration/output_numpy/0.npy new file mode 100644 index 00000000..3759655a Binary files /dev/null and b/examples/003_datageneration/output_numpy/0.npy differ diff --git a/examples/003_datageneration/output_numpy/1.npy b/examples/003_datageneration/output_numpy/1.npy new file mode 100644 index 00000000..4a77a22a Binary files /dev/null and b/examples/003_datageneration/output_numpy/1.npy differ diff --git a/examples/003_datageneration/output_numpy/2.npy b/examples/003_datageneration/output_numpy/2.npy new file mode 100644 index 00000000..0abee086 Binary files /dev/null and b/examples/003_datageneration/output_numpy/2.npy differ diff --git a/examples/003_datageneration/output_numpy/3.npy b/examples/003_datageneration/output_numpy/3.npy new file mode 100644 index 00000000..39c452a0 Binary files /dev/null and b/examples/003_datageneration/output_numpy/3.npy differ diff --git a/examples/003_datageneration/output_numpy/4.npy b/examples/003_datageneration/output_numpy/4.npy new file mode 100644 index 00000000..c89c9782 Binary files /dev/null and b/examples/003_datageneration/output_numpy/4.npy differ diff --git a/examples/003_datageneration/output_numpy/5.npy b/examples/003_datageneration/output_numpy/5.npy new file mode 100644 index 00000000..f7073b2a Binary files /dev/null and b/examples/003_datageneration/output_numpy/5.npy differ diff --git a/examples/003_datageneration/output_numpy/6.npy b/examples/003_datageneration/output_numpy/6.npy new file mode 100644 index 00000000..b2fa3491 Binary files /dev/null and b/examples/003_datageneration/output_numpy/6.npy differ diff --git a/examples/003_datageneration/output_numpy/7.npy b/examples/003_datageneration/output_numpy/7.npy new file mode 100644 index 00000000..8a2172a5 Binary files /dev/null and b/examples/003_datageneration/output_numpy/7.npy differ diff --git a/examples/003_datageneration/output_numpy/8.npy b/examples/003_datageneration/output_numpy/8.npy new file mode 100644 index 00000000..5246aa5d Binary files /dev/null and b/examples/003_datageneration/output_numpy/8.npy differ diff --git a/examples/003_datageneration/output_numpy/9.npy b/examples/003_datageneration/output_numpy/9.npy new file mode 100644 index 00000000..47f2dcd4 Binary files /dev/null and b/examples/003_datageneration/output_numpy/9.npy differ diff --git a/examples/004_optimization/001_builtin_optimizers.py b/examples/004_optimization/001_builtin_optimizers.py new file mode 100644 index 00000000..ec615dea --- /dev/null +++ b/examples/004_optimization/001_builtin_optimizers.py @@ -0,0 +1,62 @@ +""" +Use the built-in optimization algorithms +======================================== + +In this example, we will use the built-in optimization algorithms provided by the :mod:`f3dasm.optimization` submodule to optimize the Rosenbrock benchmark function. +""" + +import matplotlib.pyplot as plt + +from f3dasm import ExperimentData +from f3dasm.design import make_nd_continuous_domain +from f3dasm.optimization import OPTIMIZERS + +############################################################################### +# We create a 3D continous domain and sample one point from it. + +domain = make_nd_continuous_domain([[-1., 1.], [-1., 1.], [-1., 1.]]) + +experimentdata = ExperimentData.from_sampling( + domain=domain, sampler="random", seed=42, n_samples=1) + +print(experimentdata) + +############################################################################### +# We evaluate the sample point on the Rosenbrock benchmark function: + +experimentdata.evaluate(data_generator='Rosenbrock', kwargs={ + 'scale_bounds': domain.get_bounds(), 'offset': False}) + +print(experimentdata) + +############################################################################### +# We call the :meth:`~f3dasm.ExperimentData.optimize` method with ``optimizer='CG'`` +# and ``data_generator='Rosenbrock'`` to optimize the Rosenbrock benchmark function with the +# Conjugate Gradient Optimizer: + +experimentdata.optimize(optimizer='CG', data_generator='Rosenbrock', kwargs={ + 'scale_bounds': domain.get_bounds(), 'offset': False}, + iterations=50) + +print(experimentdata) + +############################################################################### +# We plot the convergence of the optimization process: + +_, df_output = experimentdata.to_pandas() + +fig, ax = plt.subplots() +ax.plot(df_output) +_ = ax.set_xlabel('number of function evaluations') +_ = ax.set_ylabel('$f(x)$') +ax.set_yscale('log') + +############################################################################### +# Hyper-parameters of the optimizer can be passed as dictionary to the :meth:`~f3dasm.ExperimentData.optimize` method. +# If none are provided, default hyper-parameters are used. The hyper-parameters are specific to the optimizer used, and can be found in the corresponding documentation. +# +# An overview of the available optimizers can be found in :ref:`this section ` of the documentation +# Access to more off-the-shelf optimizers requires the installation of the `f3dasm_optimize `_ package and its corresponding dependencies. +# You can check which optimizers can be used by inspecting the ``f3dasm.optimization.OPTIMIZERS`` variable: + +print(OPTIMIZERS) diff --git a/examples/004_optimization/README.rst b/examples/004_optimization/README.rst new file mode 100644 index 00000000..9c060fee --- /dev/null +++ b/examples/004_optimization/README.rst @@ -0,0 +1,4 @@ +Optimizing your design +^^^^^^^^^^^^^^^^^^^^^^ + +Optimize your design with your optimization algorithm or use the built-in optimizers. \ No newline at end of file diff --git a/examples/005_workflow/001_cluster_computing.py b/examples/005_workflow/001_cluster_computing.py new file mode 100644 index 00000000..54910178 --- /dev/null +++ b/examples/005_workflow/001_cluster_computing.py @@ -0,0 +1,185 @@ +""" +Using f3dasm on a high-performance cluster computer +=================================================== + +Your :mod:`f3dasm` workflow can be seemlessly translated to a high-performance computing cluster. +The advantage is that you can parallelize the total number of experiments among the nodes of the cluster. +This is especially useful when you have a large number of experiments to run. +""" + +############################################################################### +# .. note:: +# This example has been tested on the following high-performance computing cluster systems: +# +# * The `hpc06 cluster of Delft University of Technology `_ , using the `TORQUE resource manager `_. +# * The `DelftBlue: TU Delft supercomputer `_, using the `SLURM resource manager `_. +# * The `OSCAR compute cluster from Brown University `_, using the `SLURM resource manager `_. + +from time import sleep + +import numpy as np + +from f3dasm import HPC_JOBID, ExperimentData +from f3dasm.design import make_nd_continuous_domain + +############################################################################### +# The following example is the same as in section :ref:`workflow`; only now we are omiting the optimization part and only parallelize the data generation: +# +# * Create a 20D continuous :class:`~f3dasm.design.Domain` +# * Sample from the domain using a the Latin-hypercube sampler +# * With multiple nodes; use a data generation function, which will be the ``"Ackley"`` function a from the :ref:`benchmark-functions` +# +# +# .. image:: ../../img/f3dasm-workflow-example-cluster.png +# :width: 70% +# :align: center +# :alt: Workflow + +############################################################################### +# We want to make sure that the sampling is done only once, and that the data generation is done in parallel. +# Therefore we can divide the different nodes into two categories: +# +# * The first node (:code:`f3dasm.HPC_JOBID == 0`) will be the **master** node, which will be responsible for creating the design-of-experiments and sampling (the ``create_experimentdata`` function). + + +def create_experimentdata(): + """Design of Experiment""" + # Create a domain object + domain = make_nd_continuous_domain( + bounds=np.tile([0.0, 1.0], (20, 1)), dimensionality=20) + + # Create the ExperimentData object + data = ExperimentData(domain=domain) + + # Sampling from the domain + data.sample(sampler='latin', n_samples=10) + + # Store the data to disk + data.store() + +############################################################################### +# * All the other nodes (:code:`f3dasm.HPC_JOBID > 0`) will be **process** nodes, which will retrieve the :class:`~f3dasm.ExperimentData` from disk and go straight to the data generation function. +# +# .. image:: ../../img/f3dasm-workflow-cluster-roles.png +# :width: 100% +# :align: center +# :alt: Cluster roles + + +def worker_node(): + # Extract the experimentdata from disk + data = ExperimentData.from_file(project_dir='.') + + """Data Generation""" + # Use the data-generator to evaluate the initial samples + data.evaluate(data_generator='Ackley', mode='cluster') + +############################################################################### +# The entrypoint of the script can now check the jobid of the current node and decide whether to create the experiment data or to run the data generation function: + + +if __name__ == '__main__': + # Check the jobid of the current node + if HPC_JOBID is None: + # If the jobid is none, we are not running anything now + pass + + elif HPC_JOBID == 0: + create_experimentdata() + worker_node() + elif HPC_JOBID > 0: + # Asynchronize the jobs in order to omit racing conditions + sleep(HPC_JOBID) + worker_node() + +############################################################################### +# +# Running the program +# ------------------- +# +# You can run the workflow by submitting the bash script to the HPC queue: +# Make sure you have `miniconda3 `_ installed on the cluster, and that you have created a conda environment (in this example named ``f3dasm_env``) with the necessary packages: +# +# .. tabs:: +# +# .. group-tab:: TORQUE +# +# .. code-block:: bash +# +# #!/bin/bash +# # Torque directives (#PBS) must always be at the start of a job script! +# #PBS -N ExampleScript +# #PBS -q mse +# #PBS -l nodes=1:ppn=12,walltime=12:00:00 +# +# # Make sure I'm the only one that can read my output +# umask 0077 +# +# +# # The PBS_JOBID looks like 1234566[0]. +# # With the following line, we extract the PBS_ARRAYID, the part in the brackets []: +# PBS_ARRAYID=$(echo "${PBS_JOBID}" | sed 's/\[[^][]*\]//g') +# +# module load use.own +# module load miniconda3 +# cd $PBS_O_WORKDIR +# +# # Here is where the application is started on the node +# # activating my conda environment: +# +# source activate f3dasm_env +# +# # limiting number of threads +# OMP_NUM_THREADS=12 +# export OMP_NUM_THREADS=12 +# +# +# # If the PBS_ARRAYID is not set, set it to None +# if ! [ -n "${PBS_ARRAYID+1}" ]; then +# PBS_ARRAYID=None +# fi +# +# # Executing my python program with the jobid flag +# python main.py --jobid=${PBS_ARRAYID} +# +# .. group-tab:: SLURM +# +# .. code-block:: bash +# +# #!/bin/bash -l +# +# #SBATCH -J "ExmpleScript" # name of the job (can be change to whichever name you like) +# #SBATCH --get-user-env # to set environment variables +# +# #SBATCH --partition=compute +# #SBATCH --time=12:00:00 +# #SBATCH --nodes=1 +# #SBATCH --ntasks-per-node=12 +# #SBATCH --cpus-per-task=1 +# #SBATCH --mem=0 +# #SBATCH --account=research-eemcs-me +# #SBATCH --array=0-2 +# +# source activate f3dasm_env +# +# # Executing my python program with the jobid flag +# python3 main.py --jobid=${SLURM_ARRAY_TASK_ID} +# +# +# You can run the workflow by submitting the bash script to the HPC queue. +# the following command submits an array job with 3 jobs with :code:`f3dasm.HPC_JOBID` of 0, 1 and 2. +# +# .. tabs:: +# +# .. group-tab:: TORQUE +# +# .. code-block:: bash +# +# qsub pbsjob.sh -t 0-2 +# +# .. group-tab:: SLURM +# +# .. code-block:: bash +# +# sbatch --array 0-2 pbsjob.sh +# diff --git a/examples/005_workflow/README.rst b/examples/005_workflow/README.rst new file mode 100644 index 00000000..256635b3 --- /dev/null +++ b/examples/005_workflow/README.rst @@ -0,0 +1,2 @@ +Combining everything in a data-driven workflow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \ No newline at end of file diff --git a/examples/006_hydra/001_hydra_usage.py b/examples/006_hydra/001_hydra_usage.py new file mode 100644 index 00000000..7799bf7f --- /dev/null +++ b/examples/006_hydra/001_hydra_usage.py @@ -0,0 +1,165 @@ +""" +Combine hydra configurations with f3dasm +======================================== + +.. _hydra: https://hydra.cc/ + +`hydra `_ is an open-source configuration management framework that is widely used in machine learning and other software development domains. +It is designed to help developers manage and organize complex configuration settings for their projects, +making it easier to experiment with different configurations, manage multiple environments, and maintain reproducibility in their work. + +`hydra `_ can be seamlessly integrated with the worfklows in :mod:`f3dasm` to manage the configuration settings for the project. +""" + +from hydra import compose, initialize + +from f3dasm import ExperimentData +from f3dasm.design import Domain + +############################################################################### +# Domain from a `hydra `_ configuration file +# ------------------------------------------------------------- +# +# If you are using `hydra `_ to manage your configuration files, you can create a domain from a configuration file. +# Your config needs to have a key (e.g. :code:`domain`) that has a dictionary with the parameter names (e.g. :code:`param_1`) as keys +# and a dictionary with the parameter type (:code:`type`) and the corresponding arguments as values: +# +# .. code-block:: yaml +# :caption: config.yaml +# +# domain: +# param_1: +# type: float +# low: -1.0 +# high: 1.0 +# param_2: +# type: int +# low: 1 +# high: 10 +# param_3: +# type: category +# categories: ['red', 'blue', 'green', 'yellow', 'purple'] +# param_4: +# type: constant +# value: some_value +# +# In order to run the following code snippet, you need to have a configuration file named :code:`config.yaml` in the current working directory. + + +with initialize(version_base=None, config_path="."): + config = compose(config_name="config") + +domain = Domain.from_yaml(config.domain) +print(domain) + +############################################################################### +# ExperimentData from a `hydra `_ configuration file +# --------------------------------------------------------------------- +# +# If you are using `hydra `_ for configuring your experiments, you can use it to construct +# an :class:`~f3dasm.ExperimentData` object from the information in the :code:`config.yaml` file with the :meth:`~f3dasm.ExperimentData.from_yaml` method. +# +# ExperimentData from file with hydra +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# You can create an experimentdata :class:`~f3dasm.ExperimentData` object in the same way as the :meth:`~f3dasm.design.Domain.from_file` method, but with the :code:`from_file` key in the :code:`config.yaml` file: +# +# .. code-block:: yaml +# :caption: config_from_file.yaml +# +# domain: +# x0: +# type: float +# lower_bound: 0. +# upper_bound: 1. +# x1: +# type: float +# lower_bound: 0. +# upper_bound: 1. +# +# experimentdata: +# from_file: ./example_project_dir +# +# .. note:: +# +# The :class:`~f3dasm.design.Domain` object will be constructed using the :code:`domain` key in the :code:`config.yaml` file. Make sure you have the :code:`domain` key in your :code:`config.yaml`! +# +# +# Inside your python script, you can then create the :class:`~f3dasm.ExperimentData` object with the :meth:`~f3dasm.ExperimentData.from_yaml` method: + +with initialize(version_base=None, config_path="."): + config_from_file = compose(config_name="config_from_file") + +data_from_file = ExperimentData.from_yaml(config_from_file.experimentdata) +print(data_from_file) + +############################################################################### +# ExperimentData from sampling with hydra +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# To create the :class:`~f3dasm.ExperimentData` object with the :meth:`~f3dasm.ExperimentData.from_sampling` method, +# you can use the following configuration: +# +# .. code-block:: yaml +# :caption: config_from_sampling.yaml +# +# domain: +# x0: +# type: float +# lower_bound: 0. +# upper_bound: 1. +# x1: +# type: float +# lower_bound: 0. +# upper_bound: 1. +# +# experimentdata: +# from_sampling: +# domain: ${domain} +# sampler: random +# seed: 1 +# n_samples: 10 +# +# In order to run the following code snippet, you need to have a configuration file named :code:`config_from_sampling.yaml` in the current working directory. + +with initialize(version_base=None, config_path="."): + config_sampling = compose(config_name="config_from_sampling") + +data_from_sampling = ExperimentData.from_yaml(config_sampling.experimentdata) +print(data_from_sampling) + +############################################################################### +# Combining both approaches +# ^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# You can also combine both approaches to create the :class:`~f3dasm.ExperimentData` object by +# continuing an existing experiment with new samples. This can be done by providing both keys: +# +# .. code-block:: yaml +# :caption: config_combining.yaml +# +# domain: +# x0: +# type: float +# lower_bound: 0. +# upper_bound: 1. +# x1: +# type: float +# lower_bound: 0. +# upper_bound: 1. +# +# experimentdata: +# from_file: ./example_project_dir +# from_sampling: +# domain: ${domain} +# sampler: random +# seed: 1 +# n_samples: 10 +# +# In order to run the following code snippet, you need to have a configuration file named :code:`config_combining.yaml` in the current working directory. + +with initialize(version_base=None, config_path="."): + config_sampling = compose(config_name="config_combining") + +data_combining = ExperimentData.from_yaml(config_sampling.experimentdata) +print(data_combining) diff --git a/examples/006_hydra/002_cluster_hydra.py b/examples/006_hydra/002_cluster_hydra.py new file mode 100644 index 00000000..114862a2 --- /dev/null +++ b/examples/006_hydra/002_cluster_hydra.py @@ -0,0 +1,209 @@ +""" +Using hydra on the high-performance cluster computer +==================================================== + +`hydra `_ can be seamlessly integrated with the worfklows in :mod:`f3dasm` to manage the configuration settings for the project. +""" +############################################################################### +# +# The following example is the same as in section :ref:`workflow`; we will create a workflow for the following data-driven process: +# +# * Create a 2D continuous :class:`~f3dasm.design.Domain` +# * Sample from the domain using a the Latin-hypercube sampler +# * Use a data generation function, which will be the ``"Ackley"`` function a from the :ref:`benchmark-functions` +# +# .. image:: ../../img/f3dasm-workflow-example-cluster.png +# :width: 70% +# :align: center +# :alt: Workflow + +from time import sleep + +import hydra + +from f3dasm import ExperimentData + +############################################################################### +# Directory Structure +# ^^^^^^^^^^^^^^^^^^^ +# +# The directory structure for the project is as follows: +# +# - `my_project/` is the current working directory. +# - `config.yaml` is a hydra YAML configuration file. +# - `main.py` is the main entry point of the project, governed by :mod:`f3dasm`. +# +# +# .. code-block:: none +# :caption: Directory Structure +# +# my_project/ +# ├── my_script.py +# └── config.yaml +# └── main.py +# +# The `config_from_sampling.yaml` file contains the configuration settings for the project: +# +# .. code-block:: yaml +# :caption: config_from_sampling.yaml +# +# domain: +# x0: +# type: float +# low: 0. +# high: 1. +# x1: +# type: float +# low: 0. +# high: 1. +# +# experimentdata: +# from_sampling: +# domain: ${domain} +# sampler: random +# seed: 1 +# n_samples: 10 +# +# mode: sequential +# +# hpc: +# jobid: -1 +# +# It specifies the search-space domain, sampler settings, and the execution mode (`sequential` in this case). +# The domain is defined with `x0` and `x1` as continuous parameters with their corresponding lower and upper bounds. +# +# We want to make sure that the sampling is done only once, and that the data generation is done in parallel. +# Therefore we can divide the different nodes into two categories: +# +# * The first node (:code:`f3dasm.HPC_JOBID == 0`) will be the **master** node, which will be responsible for creating the design-of-experiments and sampling (the ``create_experimentdata`` function). + + +def create_experimentdata(config): + """Design of Experiment""" + # Create the ExperimentData object + data = ExperimentData.from_yaml(config.experimentdata) + + # Store the data to disk + data.store() + + +def worker_node(config): + # Extract the experimentdata from disk + data = ExperimentData.from_file(project_dir='.') + + """Data Generation""" + # Use the data-generator to evaluate the initial samples + data.evaluate(data_generator='Ackley', mode=config.mode) + + +############################################################################### +# The entrypoint of the script can now check the jobid of the current node and decide whether to create the experiment data or to run the data generation function: + +@hydra.main(config_path=".", config_name="config_from_sampling") +def main(config): + # Check the jobid of the current node + if config.hpc.jobid == 0: + create_experimentdata() + worker_node() + elif config.hpc.jobid == -1: # Sequential + create_experimentdata() + worker_node() + elif config.hpc.jobid > 0: + # Asynchronize the jobs in order to omit racing conditions + sleep(config.hpc.jobid) + worker_node() + + +############################################################################### +# +# Running the program +# ------------------- +# +# You can run the workflow by submitting the bash script to the HPC queue: +# Make sure you have `miniconda3 `_ installed on the cluster, and that you have created a conda environment (in this example named ``f3dasm_env``) with the necessary packages: +# +# .. tabs:: +# +# .. group-tab:: TORQUE +# +# .. code-block:: bash +# +# #!/bin/bash +# # Torque directives (#PBS) must always be at the start of a job script! +# #PBS -N ExampleScript +# #PBS -q mse +# #PBS -l nodes=1:ppn=12,walltime=12:00:00 +# +# # Make sure I'm the only one that can read my output +# umask 0077 +# +# +# # The PBS_JOBID looks like 1234566[0]. +# # With the following line, we extract the PBS_ARRAYID, the part in the brackets []: +# PBS_ARRAYID=$(echo "${PBS_JOBID}" | sed 's/\[[^][]*\]//g') +# +# module load use.own +# module load miniconda3 +# cd $PBS_O_WORKDIR +# +# # Here is where the application is started on the node +# # activating my conda environment: +# +# source activate f3dasm_env +# +# # limiting number of threads +# OMP_NUM_THREADS=12 +# export OMP_NUM_THREADS=12 +# +# +# # If the PBS_ARRAYID is not set, set it to None +# if ! [ -n "${PBS_ARRAYID+1}" ]; then +# PBS_ARRAYID=None +# fi +# +# # Executing my python program with the jobid flag +# python main.py ++hpc.jobid=${PBS_ARRAYID} hydra.run.dir=outputs/${now:%Y-%m-%d}/${JOB_ID} +# +# .. group-tab:: SLURM +# +# .. code-block:: bash +# +# #!/bin/bash -l +# +# #SBATCH -J "ExmpleScript" # name of the job (can be change to whichever name you like) +# #SBATCH --get-user-env # to set environment variables +# +# #SBATCH --partition=compute +# #SBATCH --time=12:00:00 +# #SBATCH --nodes=1 +# #SBATCH --ntasks-per-node=12 +# #SBATCH --cpus-per-task=1 +# #SBATCH --mem=0 +# #SBATCH --account=research-eemcs-me +# #SBATCH --array=0-2 +# +# source activate f3dasm_env +# +# # Executing my python program with the jobid flag +# python main.py ++hpc.jobid=${SLURM_ARRAY_TASK_ID} hydra.run.dir=/scratch/${USER}/${projectfolder}/${SLURM_ARRAY_JOB_ID} +# +# .. warning:: +# Make sure you set the ``hydra.run.dir`` argument in the jobscript to the location where you want to store the output of the hydra runs! +# +# You can run the workflow by submitting the bash script to the HPC queue. +# the following command submits an array job with 3 jobs with :code:`f3dasm.HPC_JOBID` of 0, 1 and 2. +# +# .. tabs:: +# +# .. group-tab:: TORQUE +# +# .. code-block:: bash +# +# qsub pbsjob.sh -t 0-2 +# +# .. group-tab:: SLURM +# +# .. code-block:: bash +# +# sbatch --array 0-2 pbsjob.sh +# diff --git a/examples/006_hydra/README.rst b/examples/006_hydra/README.rst new file mode 100644 index 00000000..07dfdd1c --- /dev/null +++ b/examples/006_hydra/README.rst @@ -0,0 +1,4 @@ +Integration with hydra +^^^^^^^^^^^^^^^^^^^^^^ + +Examples that integrate the :mod:`f3dasm` package with the configuration manager `hydra `_ diff --git a/examples/006_hydra/config.yaml b/examples/006_hydra/config.yaml new file mode 100644 index 00000000..fd99800d --- /dev/null +++ b/examples/006_hydra/config.yaml @@ -0,0 +1,15 @@ +domain: + param_1: + type: float + low: -1.0 + high: 1.0 + param_2: + type: int + low: 1 + high: 10 + param_3: + type: category + categories: ['red', 'blue', 'green', 'yellow', 'purple'] + param_4: + type: constant + value: some_value diff --git a/examples/006_hydra/config_combining.yaml b/examples/006_hydra/config_combining.yaml new file mode 100644 index 00000000..bcea14ee --- /dev/null +++ b/examples/006_hydra/config_combining.yaml @@ -0,0 +1,23 @@ +domain: + param_1: + type: float + low: -1.0 + high: 1.0 + param_2: + type: int + low: 1 + high: 10 + param_3: + type: category + categories: ['red', 'blue', 'green', 'yellow', 'purple'] + param_4: + type: constant + value: some_value + +experimentdata: + from_file: ./example_project_dir + from_sampling: + domain: ${domain} + sampler: random + seed: 1 + n_samples: 10 diff --git a/examples/006_hydra/config_from_file.yaml b/examples/006_hydra/config_from_file.yaml new file mode 100644 index 00000000..282de482 --- /dev/null +++ b/examples/006_hydra/config_from_file.yaml @@ -0,0 +1,12 @@ +domain: + x0: + type: float + low: 0. + high: 1. + x1: + type: float + low: 0. + high: 1. + +experimentdata: + from_file: ./example_project_dir \ No newline at end of file diff --git a/examples/006_hydra/config_from_sampling.yaml b/examples/006_hydra/config_from_sampling.yaml new file mode 100644 index 00000000..096d03c4 --- /dev/null +++ b/examples/006_hydra/config_from_sampling.yaml @@ -0,0 +1,21 @@ +domain: + x0: + type: float + low: 0. + high: 1. + x1: + type: float + low: 0. + high: 1. + +experimentdata: + from_sampling: + domain: ${domain} + sampler: random + seed: 1 + n_samples: 10 + +mode: sequential + +hpc: + jobid: -1 \ No newline at end of file diff --git a/examples/006_hydra/example_project_dir/experiment_data/domain.pkl b/examples/006_hydra/example_project_dir/experiment_data/domain.pkl new file mode 100644 index 00000000..6b319cb7 Binary files /dev/null and b/examples/006_hydra/example_project_dir/experiment_data/domain.pkl differ diff --git a/examples/006_hydra/example_project_dir/experiment_data/input.csv b/examples/006_hydra/example_project_dir/experiment_data/input.csv new file mode 100644 index 00000000..e16c76df --- /dev/null +++ b/examples/006_hydra/example_project_dir/experiment_data/input.csv @@ -0,0 +1 @@ +"" diff --git a/examples/006_hydra/example_project_dir/experiment_data/jobs.pkl b/examples/006_hydra/example_project_dir/experiment_data/jobs.pkl new file mode 100644 index 00000000..1884af77 Binary files /dev/null and b/examples/006_hydra/example_project_dir/experiment_data/jobs.pkl differ diff --git a/examples/006_hydra/example_project_dir/experiment_data/output.csv b/examples/006_hydra/example_project_dir/experiment_data/output.csv new file mode 100644 index 00000000..e16c76df --- /dev/null +++ b/examples/006_hydra/example_project_dir/experiment_data/output.csv @@ -0,0 +1 @@ +"" diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 00000000..341c60a6 --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,9 @@ +.. _examples: + +Tutorials +========= + +.. toctree:: + :hidden: + +Below is a gallery of tutorials that use various part of the :mod:`f3dasm` package diff --git a/setup.cfg b/setup.cfg index 4ae03cd9..8a2f9446 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,6 @@ classifiers = Intended Audience :: Science/Research Topic :: Scientific/Engineering :: Artificial Intelligence Topic :: Software Development :: Libraries :: Python Modules - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -30,12 +29,15 @@ classifiers = Operating System :: MacOS [options] -python_requires = >=3.7 +python_requires = >=3.8 package_dir = = src packages = find: install_requires = file: requirements.txt +[options.extras_require] +benchmark = abaqus2py ==1.0.0; f3dasm_optimize + [options.packages.find] where = src exclude = diff --git a/src/f3dasm/__init__.py b/src/f3dasm/__init__.py index 12d6b7ec..fe9c2c53 100644 --- a/src/f3dasm/__init__.py +++ b/src/f3dasm/__init__.py @@ -6,17 +6,8 @@ functions for data analysis, design of experiments, machine learning, optimization, sampling, and simulation. -Usage ------ - ->>> import f3dasm - -Links ------ - - Documentation: https://f3dasm.readthedocs.io - -Author: Martin van der Schelling (M.P.vanderSchelling@tudelft.nl) +- Author: Martin van der Schelling (M.P.vanderSchelling@tudelft.nl) """ # Modules @@ -24,6 +15,7 @@ from .__version__ import __version__ from ._src._argparser import HPC_JOBID +from ._src.experimentdata._io import StoreProtocol from ._src.experimentdata.experimentdata import ExperimentData from ._src.experimentdata.experimentsample import ExperimentSample from ._src.logger import DistributedFileHandler, logger @@ -53,4 +45,5 @@ 'run_optimization', 'HPC_JOBID', 'calculate_mean_std', + 'StoreProtocol', ] diff --git a/src/f3dasm/__version__.py b/src/f3dasm/__version__.py index 6f46e5a9..2d411c18 100644 --- a/src/f3dasm/__version__.py +++ b/src/f3dasm/__version__.py @@ -1 +1 @@ -__version__: str = "1.4.71" +__version__: str = "1.4.8" diff --git a/src/f3dasm/_src/datageneration/datagenerator.py b/src/f3dasm/_src/datageneration/datagenerator.py index acd5fa02..83adcb78 100644 --- a/src/f3dasm/_src/datageneration/datagenerator.py +++ b/src/f3dasm/_src/datageneration/datagenerator.py @@ -9,22 +9,19 @@ from __future__ import annotations # Standard -import sys +import inspect from abc import abstractmethod from functools import partial from typing import Any, Callable, Dict, List, Optional -if sys.version_info < (3, 8): # NOQA - from typing_extensions import Protocol # NOQA -else: - from typing import Protocol - # Third-party import numpy as np # Local from ..design.domain import Domain -from ..experimentdata.experimentsample import _experimentsample_factory +# from ..experimentdata._io import StoreProtocol +from ..experimentdata.experimentsample import (ExperimentSample, + _experimentsample_factory) from ..logger import time_and_log # Authorship & Credits @@ -37,18 +34,6 @@ # ============================================================================= -class ExperimentSample(Protocol): - def get(self, key: str) -> Any: - ... - - def store(self, object: Any, name: str, to_disk: bool) -> None: - ... - - @property - def job_number(self) -> int: - ... - - class DataGenerator: """Base class for a data generator""" @@ -190,7 +175,6 @@ def add_post_process(self, func: Callable, **kwargs): def convert_function(f: Callable, - input: List[str], output: Optional[List[str]] = None, kwargs: Optional[Dict[str, Any]] = None, to_disk: Optional[List[str]] = None) -> DataGenerator: @@ -201,8 +185,6 @@ def convert_function(f: Callable, ---------- f : Callable The function to be converted. - input : List[str] - A list of argument names required by the function. output : Optional[List[str]], optional A list of names for the return values of the function. Defaults to None. @@ -224,7 +206,8 @@ def convert_function(f: Callable, as long as they are consistent with the `input` and `output` arguments that are given to this function. """ - + signature = inspect.signature(f) + input = list(signature.parameters) kwargs = kwargs if kwargs is not None else {} to_disk = to_disk if to_disk is not None else [] output = output if output is not None else [] @@ -232,7 +215,7 @@ def convert_function(f: Callable, class TempDataGenerator(DataGenerator): def execute(self, **_kwargs) -> None: _input = {input_name: self.experiment_sample.get(input_name) - for input_name in input} + for input_name in input if input_name not in kwargs} _output = f(**_input, **kwargs) # check if output is empty diff --git a/src/f3dasm/_src/design/domain.py b/src/f3dasm/_src/design/domain.py index a66dcf16..ea97d4e7 100644 --- a/src/f3dasm/_src/design/domain.py +++ b/src/f3dasm/_src/design/domain.py @@ -14,7 +14,8 @@ import sys from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, List, Sequence, Type +from typing import (Any, Dict, Iterable, Iterator, List, Optional, Sequence, + Type) if sys.version_info < (3, 8): # NOQA from typing_extensions import Literal # NOQA @@ -262,21 +263,6 @@ def _cast_types_dataframe(self) -> dict: return {name: parameter._type for name, parameter in self.space.items()} - def _create_empty_dataframe(self) -> pd.DataFrame: - """Create an empty DataFrame with input columns. - - Returns - ------- - pd.DataFrame - DataFrame containing "input" columns. - """ - # input columns - input_columns = [name for name in self.space.keys()] - - return pd.DataFrame(columns=input_columns).astype( - self._cast_types_dataframe() - ) - # Append and remove parameters # ============================================================================= @@ -390,23 +376,6 @@ def add_constant(self, name: str, value: Any): """ self._add(name, _ConstantParameter(value)) - def add_parameter(self, name: str): - """Add a new parameter to the domain. - - Parameters - ---------- - name : str - Name of the input parameter. - - Example - ------- - >>> domain = Domain() - >>> domain.add_parameter('param1') - >>> domain.space - {'param1': Parameter()} - """ - self._add(name, _Parameter()) - def add(self, name: str, type: Literal['float', 'int', 'category', 'constant'], **kwargs): @@ -476,186 +445,6 @@ def add_output(self, name: str, to_disk: bool, exist_ok=False): # Getters # ============================================================================= - def get_continuous_parameters(self) -> Dict[str, _ContinuousParameter]: - """Get all continuous input parameters. - - Returns - ------- - Dict[str, _ContinuousParameter] - Space of continuous input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': _ContinuousParameter(lower_bound=0., upper_bound=1.), - ... 'param2': CategoricalParameter(categories=['A', 'B', 'C']), - ... 'param3': _ContinuousParameter(lower_bound=2., upper_bound=5.) - ... } - >>> continuous_input_params = domain.get_continuous_input_parameters() - >>> continuous_input_params - {'param1': _ContinuousParameter(lower_bound=0., upper_bound=1.), - 'param3': _ContinuousParameter(lower_bound=2., upper_bound=5.)} - """ - return self._filter(_ContinuousParameter).space - - def get_continuous_names(self) -> List[str]: - """Get the names of continuous input parameters in the input space. - - Returns - ------- - List[str] - List of names of continuous input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': _ContinuousParameter(lower_bound=0., upper_bound=1.), - ... 'param2': _DiscreteParameter(lower_bound=1, upper_bound=3), - ... 'param3': _ContinuousParameter(lower_bound=2., upper_bound=5.) - ... } - >>> continuous_input_names = domain.get_continuous_input_names() - >>> continuous_input_names - ['param1', 'param3'] - """ - return self._filter(_ContinuousParameter).names - - def get_discrete_parameters(self) -> Dict[str, _DiscreteParameter]: - """Retrieve all discrete input parameters. - - Returns - ------- - Dict[str, _DiscreteParameter] - Space of discrete input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': _DiscreteParameter(lower_bound=1, upperBound=4), - ... 'param2': CategoricalParameter(categories=['A', 'B', 'C']), - ... 'param3': _DiscreteParameter(lower_bound=4, upperBound=6) - ... } - >>> discrete_input_params = domain.get_discrete_input_parameters() - >>> discrete_input_params - {'param1': _DiscreteParameter(lower_bound=1, upperBound=4)), - 'param3': _DiscreteParameter(lower_bound=4, upperBound=6)} - """ - return self._filter(_DiscreteParameter).space - - def get_discrete_names(self) -> List[str]: - """Retrieve the names of all discrete input parameters. - - Returns - ------- - List[str] - List of names of discrete input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': _DiscreteParameter(lower_bound=1, upperBound=4), - ... 'param2': _ContinuousParameter(lower_bound=0, upper_bound=1), - ... 'param3': _DiscreteParameter(lower_bound=4, upperBound=6) - ... } - >>> discrete_input_names = domain.get_discrete_input_names() - >>> discrete_input_names - ['param1', 'param3'] - """ - return self._filter(_DiscreteParameter).names - - def get_categorical_parameters(self) -> Dict[str, _CategoricalParameter]: - """Retrieve all categorical input parameters. - - Returns - ------- - Dict[str, CategoricalParameter] - Space of categorical input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': CategoricalParameter(categories=['A', 'B', 'C']), - ... 'param2': _ContinuousParameter(lower_bound=0, upper_bound=1), - ... 'param3': CategoricalParameter(categories=['X', 'Y', 'Z']) - ... } - >>> categorical_input_params = - domain.get_categorical_input_parameters() - >>> categorical_input_params - {'param1': CategoricalParameter(categories=['A', 'B', 'C']), - 'param3': CategoricalParameter(categories=['X', 'Y', 'Z'])} - """ - return self._filter(_CategoricalParameter).space - - def get_categorical_names(self) -> List[str]: - """Retrieve the names of categorical input parameters. - - Returns - ------- - List[str] - List of names of categorical input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': CategoricalParameter(categories=['A', 'B', 'C']), - ... 'param2': _ContinuousParameter(lower_bound=0, upper_bound=1), - ... 'param3': CategoricalParameter(categories=['X', 'Y', 'Z']) - ... } - >>> categorical_input_names = domain.get_categorical_input_names() - >>> categorical_input_names - ['param1', 'param3'] - """ - return self._filter(_CategoricalParameter).names - - def get_constant_parameters(self) -> Dict[str, _ConstantParameter]: - """Retrieve all constant input parameters. - - Returns - ------- - Dict[str, ConstantParameter] - Space of constant input parameters. - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': ConstantParameter(value=0), - ... 'param2': CategoricalParameter(categories=['A', 'B', 'C']), - ... 'param3': ConstantParameter(value=1) - ... } - >>> constant_input_params = domain.get_constant_input_parameters() - >>> constant_input_params - {'param1': ConstantParameter(value=0), - 'param3': ConstantParameter(value=1)} - """ - return self._filter(_ConstantParameter).space - - def get_constant_names(self) -> List[str]: - """Receive the names of the constant input parameters - - Returns - ------- - list of names of constant input parameters - - Example - ------- - >>> domain = Domain() - >>> domain.space = { - ... 'param1': ConstantParameter(value=0), - ... 'param2': ConstantParameter(value=1), - ... 'param3': _ContinuousParameter(lower_bound=0, upper_bound=1) - ... } - >>> constant_input_names = domain.get_constant_input_names() - >>> constant_input_names - ['param1', 'param2'] - """ - return self._filter(_ConstantParameter).names - def get_bounds(self) -> np.ndarray: """Return the boundary constraints of the continuous input parameters @@ -680,7 +469,7 @@ def get_bounds(self) -> np.ndarray: """ return np.array( [[parameter.lower_bound, parameter.upper_bound] - for _, parameter in self.get_continuous_parameters().items()] + for _, parameter in self.continuous.space.items()] ) def _filter(self, type: Type[_Parameter]) -> Domain: @@ -788,30 +577,6 @@ def _all_input_continuous(self) -> bool: """Check if all input parameters are continuous""" return len(self) == len(self._filter(_ContinuousParameter)) - def _check_output(self, names: List[str]): - """Check if output is in the domain and add it if not - - Parameters - ---------- - - names : list of str - Names of the outputs to be checked - - Example - ------- - >>> domain = Domain() - >>> domain.add_output('output1') - >>> domain.add_output('output2') - >>> domain._check_output(['output1', 'output2', 'output3']) - >>> domain.output_space - {'output1': _ContinuousParameter(lower_bound=-inf, upper_bound=inf), - 'output2': _ContinuousParameter(lower_bound=-inf, upper_bound=inf), - 'output3': _ContinuousParameter(lower_bound=-inf, upper_bound=inf)} - """ - for output_name in names: - if not self.is_in_output(output_name): - self.add_output(output_name, to_disk=False) - def is_in_output(self, output_name: str) -> bool: """Check if output is in the domain @@ -838,16 +603,18 @@ def is_in_output(self, output_name: str) -> bool: def make_nd_continuous_domain(bounds: np.ndarray | List[List[float]], - dimensionality: int) -> Domain: + dimensionality: Optional[int] = None) -> Domain: """Create a continuous domain. Parameters ---------- bounds : numpy.ndarray - A 2D numpy array of shape (dimensionality, 2) specifying the lower \ + A 2D numpy array of shape (dimensionality, 2) specifying the lower and upper bounds of every dimension. dimensionality : int - The number of dimensions. + The number of dimensions, optional. If not given, it is inferred + from the shape of the bounds. Argument is still present for legacy + reasons. Returns ------- @@ -875,6 +642,8 @@ def make_nd_continuous_domain(bounds: np.ndarray | List[List[float]], # bounds is a list of lists, convert to numpy array: bounds = np.array(bounds) + dimensionality = bounds.shape[0] + for dim in range(dimensionality): space[f"x{dim}"] = _ContinuousParameter( lower_bound=bounds[dim, 0], upper_bound=bounds[dim, 1]) @@ -886,7 +655,6 @@ def _domain_factory(domain: Domain | DictConfig | None, input_data: pd.DataFrame, output_data: pd.DataFrame) -> Domain: if isinstance(domain, Domain): - # domain._check_output(output_data.columns) return domain elif isinstance(domain, (Path, str)): diff --git a/src/f3dasm/_src/design/parameter.py b/src/f3dasm/_src/design/parameter.py index 93fedecd..7d21bc70 100644 --- a/src/f3dasm/_src/design/parameter.py +++ b/src/f3dasm/_src/design/parameter.py @@ -190,6 +190,34 @@ def _check_range(self): f"(lower_bound={self.lower_bound}, \ higher_bound={self.upper_bound}") + def to_discrete(self, step: int = 1) -> _DiscreteParameter: + """Convert the continuous parameter to a discrete parameter. + + Parameters + ---------- + step : int + The step size of the discrete search space, which defaults to 1. + + Returns + ------- + DiscreteParameter + The discrete parameter. + + Raises + ------ + ValueError + If the step size is less than or equal to 0. + + """ + if step <= 0: + raise ValueError("The step size must be larger than 0.") + + return _DiscreteParameter( + lower_bound=int(self.lower_bound), + upper_bound=int(self.upper_bound), + step=step + ) + @dataclass class _DiscreteParameter(_Parameter): @@ -251,7 +279,7 @@ def _check_range(self): raise ValueError("step size must be larger than 0!") -@dataclass +@ dataclass class _CategoricalParameter(_Parameter): """Create a search space parameter that is categorical diff --git a/src/f3dasm/_src/design/samplers.py b/src/f3dasm/_src/design/samplers.py deleted file mode 100644 index e5f5016f..00000000 --- a/src/f3dasm/_src/design/samplers.py +++ /dev/null @@ -1,352 +0,0 @@ -"""Base class for sampling methods""" - -# Modules -# ============================================================================= - -from __future__ import annotations - -# Standard -import sys -from itertools import product - -if sys.version_info < (3, 8): # NOQA - from typing_extensions import Literal # NOQA -else: - from typing import Literal - -from typing import Optional - -# Third-party -import numpy as np -import pandas as pd -from SALib.sample import latin, sobol_sequence - -# Locals -from .domain import Domain - -# Authorship & Credits -# ============================================================================= -__author__ = 'Martin van der Schelling (M.P.vanderSchelling@tudelft.nl)' -__credits__ = ['Martin van der Schelling'] -__status__ = 'Stable' -# ============================================================================= -# -# ============================================================================= - -SamplerNames = Literal['random', 'latin', 'sobol', 'grid'] - -# Factory function -# ============================================================================= - - -def _sampler_factory(sampler: str, domain: Domain) -> Sampler: - if sampler.lower() == 'random': - return RandomUniform(domain) - - elif sampler.lower() == 'latin': - return LatinHypercube(domain) - - elif sampler.lower() == 'sobol': - return SobolSequence(domain) - - elif sampler.lower() == 'grid': - return GridSampler(domain) - - else: - raise KeyError(f"Sampler {sampler} not found!" - f"Available built-in samplers are: 'random'," - f"'latin' and 'sobol'") - - -# Base Class -# ============================================================================= - - -class Sampler: - def __init__(self, domain: Domain, seed: Optional[int] = None, - number_of_samples: Optional[int] = None): - """Interface for sampling method - - Parameters - ---------- - domain : Domain - domain object - seed : int - seed for sampling - number_of_samples : Optional[int] - number of samples to be generated, defaults to None - """ - self.domain = domain - self.seed = seed - self.number_of_samples = number_of_samples - if seed: - np.random.seed(seed) - - def set_seed(self, seed: int): - """Set the seed of the sampler - - Parameters - ---------- - seed - the seed to be used - """ - np.random.seed(seed) - self.seed = seed - - def sample_continuous(self, numsamples: int) -> np.ndarray: - """Create N samples within the search space - - Parameters - ---------- - numsamples - number of samples - - Returns - ------- - samples - """ - raise NotImplementedError("Subclasses should implement this method.") - - def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame: - """Receive samples of the search space - - Parameters - ---------- - numsamples - number of samples - - Returns - ------- - Data objects with the samples - """ - - self.set_seed(self.seed) - - # If numsamples is None, take the object attribute number_of_samples - if numsamples is None: - numsamples = self.number_of_samples - - # First sample the continuous parameters - samples_continuous = self.sample_continuous(numsamples=numsamples) - - # Sample discrete parameters - samples_discrete = self._sample_discrete(numsamples=numsamples) - - # Sample categorical parameters - samples_categorical = self._sample_categorical(numsamples=numsamples) - - # Sample constant parameters - samples_constant = self._sample_constant(numsamples=numsamples) - - # Merge samples into array - samples = np.hstack( - (samples_continuous, samples_discrete, - samples_categorical, samples_constant)) - - # TODO #60 : Fix this ordering issue - # Get the column names in this particular order - columnnames = [ - name - for name in self.domain.get_continuous_names( - ) + self.domain.get_discrete_names( - ) + self.domain.get_categorical_names( - ) + self.domain.get_constant_names() - ] - - # First get an empty reference frame from the DoE - empty_frame = self.domain._create_empty_dataframe() - - # Then, create a new frame from the samples and columnnames - samples_frame = pd.DataFrame( - data=samples, columns=columnnames, dtype=object) - df = pd.concat([empty_frame, samples_frame], sort=True) - - return df - - def __call__(self, domain: Domain, n_samples: int, seed: int): - """Call the sampler""" - self.domain = domain - self.number_of_samples = n_samples - self.seed = seed - return self.get_samples() - - def _sample_constant(self, numsamples: int): - constant = self.domain.get_constant_parameters() - samples = np.empty(shape=(numsamples, len(constant))) - for dim, param in enumerate(constant.values()): - samples[:, dim] = param.value - - return samples - - def _sample_discrete(self, numsamples: int): - """Sample the descrete parameters, default randomly uniform""" - discrete = self.domain.get_discrete_parameters() - samples = np.empty(shape=(numsamples, len(discrete)), dtype=np.int32) - for dim, param in enumerate(discrete.values()): - samples[:, dim] = np.random.choice( - range(param.lower_bound, - param.upper_bound + 1, param.step), - size=numsamples, - ) - - return samples - - def _sample_categorical(self, numsamples: int): - """Sample the categorical parameters, default randomly uniform""" - categorical = self.domain.get_categorical_parameters() - samples = np.empty(shape=(numsamples, len(categorical)), dtype=object) - for dim, param in enumerate(categorical.values()): - samples[:, dim] = np.random.choice( - param.categories, size=numsamples) - - return samples - - def _stretch_samples(self, samples: np.ndarray) -> np.ndarray: - """Stretch samples to their boundaries""" - continuous = self.domain.get_continuous_parameters() - for dim, param in enumerate(continuous.values()): - samples[:, dim] = ( - samples[:, dim] * ( - param.upper_bound - param.lower_bound - ) + param.lower_bound - ) - - # If param.log is True, take the 10** of the samples - if param.log: - samples[:, dim] = 10**samples[:, dim] - - return samples - - -# Built-in samplers -# ============================================================================= - -class LatinHypercube(Sampler): - """Sampling via Latin Hypercube Sampling""" - - def sample_continuous(self, numsamples: int) -> np.ndarray: - """Sample from continuous space - - Parameters - ---------- - numsamples - number of samples - - Returns - ------- - samples - """ - continuous = self.domain.continuous - problem = { - "num_vars": len(continuous), - "names": continuous.names, - "bounds": [[s.lower_bound, s.upper_bound] - for s in continuous.values()], - } - - samples = latin.sample(problem, N=numsamples, seed=self.seed) - return samples - - -class RandomUniform(Sampler): - """ - Sampling via random uniform sampling - """ - - def sample_continuous(self, numsamples: int) -> np.ndarray: - """Sample from continuous space - - Parameters - ---------- - numsamples - number of samples - - Returns - ------- - samples - """ - continuous = self.domain.continuous - samples = np.random.uniform(size=(numsamples, len(continuous))) - - # stretch samples - samples = self._stretch_samples(samples) - return samples - - -class SobolSequence(Sampler): - """Sampling via Sobol Sequencing with SALib - - Reference: `SALib `_""" - - def sample_continuous(self, numsamples: int) -> np.ndarray: - """Sample from continuous space - - Parameters - ---------- - numsamples - number of samples - - Returns - ------- - samples - """ - continuous = self.domain.continuous - - samples = sobol_sequence.sample(numsamples, len(continuous)) - - # stretch samples - samples = self._stretch_samples(samples) - return samples - - -class GridSampler(Sampler): - """Sampling via Grid Sampling - - All the combination of the discrete and categorical parameters are - sampled. The argument number_of_samples is ignored. - Notes - ----- - This sampler is at the moment only applicable for - discrete and categorical parameters. - - """ - - def get_samples(self, numsamples: Optional[int] = None) -> pd.DataFrame: - """Receive samples of the search space - - Parameters - ---------- - numsamples - number of samples - - Returns - ------- - Data objects with the samples - """ - - self.set_seed(self.seed) - - # If numsamples is None, take the object attribute number_of_samples - if numsamples is None: - numsamples = self.number_of_samples - - continuous = self.domain.get_continuous_parameters() - - if continuous: - raise ValueError("Grid sampling is only possible for domains \ - strictly with only discrete and \ - categorical parameters") - - discrete = self.domain.get_discrete_parameters() - categorical = self.domain.get_categorical_parameters() - - _iterdict = {} - - for k, v in categorical.items(): - _iterdict[k] = v.categories - - for k, v, in discrete.items(): - _iterdict[k] = range(v.lower_bound, v.upper_bound+1) - - return pd.DataFrame(list(product(*_iterdict.values())), - columns=_iterdict, dtype=object) diff --git a/src/f3dasm/_src/experimentdata/_columns.py b/src/f3dasm/_src/experimentdata/_columns.py index c58bd621..76a3f474 100644 --- a/src/f3dasm/_src/experimentdata/_columns.py +++ b/src/f3dasm/_src/experimentdata/_columns.py @@ -55,6 +55,21 @@ def __repr__(self) -> str: """Representation of the _Columns object.""" return self.columns.keys().__repr__() + def __add__(self, __o: _Columns) -> _Columns: + """Add two _Columns objects. + + Parameters + ---------- + __o: _Columns + _Columns object to add + + Returns + ------- + _Columns + _Columns object with the columns of both _Columns objects + """ + return _Columns({**self.columns, **__o.columns}) + @property def names(self) -> List[str]: """List of the names of the columns. diff --git a/src/f3dasm/_src/experimentdata/_data.py b/src/f3dasm/_src/experimentdata/_data.py index e6d0cc55..b75bf379 100644 --- a/src/f3dasm/_src/experimentdata/_data.py +++ b/src/f3dasm/_src/experimentdata/_data.py @@ -162,7 +162,7 @@ def from_domain(cls, domain: Domain) -> _Data: # Set the categories tot the categorical parameters for index, (name, categorical_input) in enumerate( - domain.get_categorical_parameters().items()): + domain.categorical.space.items()): df[index] = pd.Categorical( df[index], categories=categorical_input.categories) @@ -436,6 +436,22 @@ def overwrite(self, indices: Iterable[int], other: _Data | Dict[str, Any]): self.data.update(other.data.set_index(pd.Index(indices))) + def join(self, __o: _Data) -> _Data: + """Join two Data objects together. + + Parameters + ---------- + __o : Data + The Data object to join. + + Returns + ------- + The joined Data object. + """ + return _Data( + pd.concat([self.data, __o.data], axis=1, ignore_index=True), + columns=self.columns + __o.columns) + # Getters and setters # ============================================================================= diff --git a/src/f3dasm/_src/experimentdata/_io.py b/src/f3dasm/_src/experimentdata/_io.py index 388edac9..f602dbac 100644 --- a/src/f3dasm/_src/experimentdata/_io.py +++ b/src/f3dasm/_src/experimentdata/_io.py @@ -47,7 +47,7 @@ # ============================================================================= -class _Store: +class StoreProtocol: """Base class for storing and loading output data from disk""" suffix: int @@ -93,7 +93,7 @@ def load(self) -> Any: raise NotImplementedError() -class PickleStore(_Store): +class PickleStore(StoreProtocol): """Class to store and load objects using the pickle protocol""" suffix: str = '.pkl' @@ -118,7 +118,7 @@ def load(self) -> Any: return pickle.load(file) -class NumpyStore(_Store): +class NumpyStore(StoreProtocol): """Class to store and load objects using the numpy protocol""" suffix: str = '.npy' @@ -140,7 +140,7 @@ def load(self) -> np.ndarray: return np.load(file=self.path.with_suffix(self.suffix)) -class PandasStore(_Store): +class PandasStore(StoreProtocol): """Class to store and load objects using the pandas protocol""" suffix: str = '.csv' @@ -162,7 +162,7 @@ def load(self) -> pd.DataFrame: return pd.read_csv(self.path.with_suffix(self.suffix)) -class XarrayStore(_Store): +class XarrayStore(StoreProtocol): """Class to store and load objects using the xarray protocol""" suffix: str = '.nc' @@ -184,7 +184,7 @@ def load(self) -> xr.DataArray | xr.Dataset: return xr.open_dataset(self.path.with_suffix(self.suffix)) -class FigureStore(_Store): +class FigureStore(StoreProtocol): """Class to store and load objects using the matplotlib protocol""" suffix: str = '.png' @@ -223,7 +223,7 @@ def load(self) -> np.ndarray: return plt.imread(self.path.with_suffix(self.suffix)) -STORE_TYPE_MAPPING: Mapping[Type, _Store] = { +STORE_TYPE_MAPPING: Mapping[Type, StoreProtocol] = { np.ndarray: NumpyStore, pd.DataFrame: PandasStore, pd.Series: PandasStore, @@ -237,7 +237,7 @@ def load(self) -> np.ndarray: def load_object(path: Path, experimentdata_directory: Path, - store_method: Type[_Store] = PickleStore) -> Any: + store_method: Type[StoreProtocol] = PickleStore) -> Any: """ Load an object from disk from a given path and storing method @@ -281,7 +281,7 @@ def load_object(path: Path, experimentdata_directory: Path, # Use a generator expression to find the first matching store type, # or None if no match is found - matched_store_type: _Store = next( + matched_store_type: StoreProtocol = next( (store_type for store_type in STORE_TYPE_MAPPING.values() if store_type.suffix == item_suffix), PickleStore) @@ -294,7 +294,7 @@ def load_object(path: Path, experimentdata_directory: Path, def save_object(object: Any, path: Path, experimentdata_directory: Path, - store_method: Optional[Type[_Store]] = None) -> str: + store_method: Optional[Type[StoreProtocol]] = None) -> str: """Function to save the object to path, with the appropriate storing method. @@ -328,12 +328,12 @@ def save_object(object: Any, path: Path, experimentdata_directory: Path, object_type = type(object) if object_type not in STORE_TYPE_MAPPING: - storage: _Store = PickleStore(object, _path) + storage: StoreProtocol = PickleStore(object, _path) logger.debug(f"Object type {object_type} is not natively supported. " f"The default pickle storage method will be used.") else: - storage: _Store = STORE_TYPE_MAPPING[object_type](object, _path) + storage: StoreProtocol = STORE_TYPE_MAPPING[object_type](object, _path) # Store object storage.store() return storage.suffix diff --git a/src/f3dasm/_src/experimentdata/experimentdata.py b/src/f3dasm/_src/experimentdata/experimentdata.py index f0974c7c..97584f24 100644 --- a/src/f3dasm/_src/experimentdata/experimentdata.py +++ b/src/f3dasm/_src/experimentdata/experimentdata.py @@ -9,8 +9,9 @@ from __future__ import annotations -import sys +import inspect # Standard +import sys import traceback from functools import wraps from pathlib import Path @@ -33,10 +34,9 @@ from pathos.helpers import mp # Local -from ..datageneration.datagenerator import DataGenerator +from ..datageneration.datagenerator import DataGenerator, convert_function from ..datageneration.functions.function_factory import _datagenerator_factory from ..design.domain import Domain, _domain_factory -from ..design.samplers import Sampler, SamplerNames, _sampler_factory from ..logger import logger from ..optimization import Optimizer from ..optimization.optimizer_factory import _optimizer_factory @@ -46,6 +46,7 @@ OUTPUT_DATA_FILENAME, _project_dir_factory) from ._jobqueue import NoOpenJobsError, Status, _jobs_factory from .experimentsample import ExperimentSample +from .samplers import Sampler, SamplerNames, _sampler_factory from .utils import number_of_overiterations, number_of_updates # Authorship & Credits @@ -293,7 +294,8 @@ def from_file(cls: Type[ExperimentData], @classmethod def from_sampling(cls, sampler: Sampler | str, domain: Domain | DictConfig, n_samples: int = 1, - seed: Optional[int] = None) -> ExperimentData: + seed: Optional[int] = None, + **kwargs) -> ExperimentData: """Create an ExperimentData object from a sampler. Parameters @@ -324,9 +326,12 @@ def from_sampling(cls, sampler: Sampler | str, domain: Domain | DictConfig, * 'latin' : Latin Hypercube Sampling * 'sobol' : Sobol Sequence Sampling * 'grid' : Grid Search Sampling + + Any additional keyword arguments are passed to the sampler. """ experimentdata = cls(domain=domain) - experimentdata.sample(sampler=sampler, n_samples=n_samples, seed=seed) + experimentdata.sample( + sampler=sampler, n_samples=n_samples, seed=seed, **kwargs) return experimentdata @classmethod @@ -868,6 +873,25 @@ def _reset_index(self) -> None: self._output_data.reset_index(self._input_data.indices) self._jobs.reset_index() + def join(self, other: ExperimentData) -> ExperimentData: + """Join two ExperimentData objects. + + Parameters + ---------- + other : ExperimentData + The other ExperimentData object to join with. + + Returns + ------- + ExperimentData + The joined ExperimentData object. + """ + return ExperimentData( + input_data=self._input_data.join(other._input_data), + output_data=self._output_data.join(other._output_data), + jobs=self._jobs, + domain=self.domain + other.domain, + project_dir=self.project_dir) # ExperimentSample # ============================================================================= @@ -1091,13 +1115,14 @@ def mark_all_nan_open(self) -> None: def evaluate(self, data_generator: DataGenerator, mode: Literal['sequential', 'parallel', 'cluster', 'cluster_parallel'] = 'sequential', - kwargs: Optional[dict] = None) -> None: + kwargs: Optional[dict] = None, + output_names: Optional[List[str]] = None) -> None: """Run any function over the entirety of the experiments Parameters ---------- data_generator : DataGenerator - data grenerator to use + data generator to use mode : str, optional operational mode, by default 'sequential'. Choose between: @@ -1109,6 +1134,10 @@ def evaluate(self, data_generator: DataGenerator, kwargs, optional Any keyword arguments that need to be supplied to the function, by default None + output_names : List[str], optional + If you provide a function as data generator, you have to provide + the names of all the output parameters that are in the return + statement, in order of appearance. Raises ------ @@ -1118,7 +1147,16 @@ def evaluate(self, data_generator: DataGenerator, if kwargs is None: kwargs = {} - if isinstance(data_generator, str): + if inspect.isfunction(data_generator): + if output_names is None: + raise TypeError( + ("If you provide a function as data generator, you have to" + "provide the names of the return arguments with the" + "output_names attribute.")) + data_generator = convert_function( + f=data_generator, output=output_names) + + elif isinstance(data_generator, str): data_generator = _datagenerator_factory( data_generator, self.domain, kwargs) @@ -1708,7 +1746,7 @@ def _iterate_scipy(self, optimizer: Optimizer, # ========================================================================= def sample(self, sampler: Sampler | SamplerNames, n_samples: int = 1, - seed: Optional[int] = None) -> None: + seed: Optional[int] = None, **kwargs) -> None: """Sample data from the domain providing the sampler strategy Parameters @@ -1726,6 +1764,17 @@ def sample(self, sampler: Sampler | SamplerNames, n_samples: int = 1, seed : Optional[int], optional Seed to use for the sampler, by default None + Note + ---- + When using the 'grid' sampler, an optional argument + 'stepsize_continuous_parameters' can be passed to specify the stepsize + to cast continuous parameters to discrete parameters. + + - The stepsize should be a dictionary with the parameter names as keys\ + and the stepsize as values. + - Alternatively, a single stepsize can be passed for all continuous\ + parameters. + Raises ------ ValueError @@ -1736,7 +1785,7 @@ def sample(self, sampler: Sampler | SamplerNames, n_samples: int = 1, sampler = _sampler_factory(sampler, self.domain) sample_data: DataTypes = sampler( - domain=self.domain, n_samples=n_samples, seed=seed) + domain=self.domain, n_samples=n_samples, seed=seed, **kwargs) self.add(input_data=sample_data, domain=self.domain) # Project directory diff --git a/src/f3dasm/_src/experimentdata/experimentsample.py b/src/f3dasm/_src/experimentdata/experimentsample.py index 1bc21d8a..cb0e550d 100644 --- a/src/f3dasm/_src/experimentdata/experimentsample.py +++ b/src/f3dasm/_src/experimentdata/experimentsample.py @@ -25,7 +25,7 @@ # Local from ..design.domain import Domain from ..logger import logger -from ._io import _Store, load_object, save_object +from ._io import StoreProtocol, load_object, save_object # Authorship & Credits # ============================================================================= @@ -148,7 +148,7 @@ def _from_numpy_without_domain( return dict_input, dict_output def get(self, item: str, - load_method: Optional[Type[_Store]] = None) -> Any: + load_method: Optional[Type[StoreProtocol]] = None) -> Any: """Retrieve a sample parameter by its name. Parameters @@ -312,7 +312,7 @@ def to_dict(self) -> Dict[str, Any]: 'job_number': self.job_number} def store(self, name: str, object: Any, to_disk: bool = False, - store_method: Optional[Type[_Store]] = None) -> None: + store_method: Optional[Type[StoreProtocol]] = None) -> None: """Store an object to disk. Parameters @@ -340,8 +340,9 @@ def store(self, name: str, object: Any, to_disk: bool = False, else: self._store_to_experimentdata(object=object, name=name) - def _store_to_disk(self, object: Any, name: str, - store_method: Optional[Type[_Store]] = None) -> None: + def _store_to_disk( + self, object: Any, name: str, + store_method: Optional[Type[StoreProtocol]] = None) -> None: file_path = Path(name) / str(self.job_number) # Check if the file_dir exists diff --git a/src/f3dasm/_src/experimentdata/samplers.py b/src/f3dasm/_src/experimentdata/samplers.py new file mode 100644 index 00000000..4347d615 --- /dev/null +++ b/src/f3dasm/_src/experimentdata/samplers.py @@ -0,0 +1,472 @@ +"""Base class for sampling methods""" + +# Modules +# ============================================================================= + +from __future__ import annotations + +# Standard +import sys +from itertools import product + +if sys.version_info < (3, 8): # NOQA + from typing_extensions import Literal, Protocol # NOQA +else: + from typing import Literal, Protocol + +from typing import Dict, Optional + +# Third-party +import numpy as np +import pandas as pd +from SALib.sample import latin as salib_latin +from SALib.sample import sobol_sequence + +# Locals +from ..design.domain import Domain +from ._data import DataTypes + +# Authorship & Credits +# ============================================================================= +__author__ = 'Martin van der Schelling (M.P.vanderSchelling@tudelft.nl)' +__credits__ = ['Martin van der Schelling'] +__status__ = 'Stable' +# ============================================================================= +# +# ============================================================================= + +SamplerNames = Literal['random', 'latin', 'sobol', 'grid'] + + +class Sampler(Protocol): + """ + Interface class for samplers + """ + def __call__(domain: Domain, **kwargs) -> DataTypes: + ... + +# Factory function +# ============================================================================= + + +def _sampler_factory(sampler: str, domain: Domain) -> Sampler: + """ + Factory function for samplers + + Parameters + ---------- + sampler : str + name of the sampler + domain : Domain + domain object + + Returns + ------- + Sampler + sampler object + """ + if sampler.lower() == 'random': + return randomuniform + + elif sampler.lower() == 'latin': + return latin + + elif sampler.lower() == 'sobol': + return sobol + + elif sampler.lower() == 'grid': + return grid + + else: + raise KeyError(f"Sampler {sampler} not found!" + f"Available built-in samplers are: 'random'," + f"'latin' and 'sobol'") + + +# Utility functions +# ============================================================================= + +def _stretch_samples(domain: Domain, samples: np.ndarray) -> np.ndarray: + """Stretch samples to their boundaries + + Parameters + ---------- + domain : Domain + domain object + samples : np.ndarray + samples to stretch + + Returns + ------- + np.ndarray + stretched samples + """ + for dim, param in enumerate(domain.space.values()): + samples[:, dim] = ( + samples[:, dim] * ( + param.upper_bound - param.lower_bound + ) + param.lower_bound + ) + + # If param.log is True, take the 10** of the samples + if param.log: + samples[:, dim] = 10**samples[:, dim] + + return samples + + +def sample_constant(domain: Domain, n_samples: int): + """Sample the constant parameters + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + + Returns + ------- + np.ndarray + samples + """ + samples = np.array([param.value for param in domain.space.values()]) + return np.tile(samples, (n_samples, 1)) + + +def sample_np_random_choice( + domain: Domain, n_samples: int, + seed: Optional[int] = None, **kwargs): + """Sample with np random choice + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : Optional[int], optional + random seed, by default None + + Returns + ------- + np.ndarray + samples + """ + rng = np.random.default_rng(seed) + samples = np.empty(shape=(n_samples, len(domain)), dtype=object) + for dim, param in enumerate(domain.space.values()): + samples[:, dim] = rng.choice( + param.categories, size=n_samples) + + return samples + + +def sample_np_random_choice_range( + domain: Domain, n_samples: int, + seed: Optional[int] = None, **kwargs): + """Samples with np random choice with a range of values + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : Optional[int], optional + random seed, by default None + + Returns + ------- + np.ndarray + samples + """ + samples = np.empty(shape=(n_samples, len(domain)), dtype=np.int32) + rng = np.random.default_rng(seed) + for dim, param in enumerate(domain.space.values()): + samples[:, dim] = rng.choice( + range(param.lower_bound, + param.upper_bound + 1, param.step), + size=n_samples, + ) + + return samples + + +def sample_np_random_uniform( + domain: Domain, n_samples: int, + seed: Optional[int] = None, **kwargs) -> np.ndarray: + """Sample with numpy random uniform + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : Optional[int], optional + random seed, by default None + + Returns + ------- + np.ndarray + samples + """ + rng = np.random.default_rng(seed) + samples = rng.uniform(low=0.0, high=1.0, size=(n_samples, len(domain))) + + # stretch samples + samples = _stretch_samples(domain, samples) + return samples + + +def sample_latin_hypercube( + domain: Domain, n_samples: int, + seed: Optional[int] = None, **kwargs) -> np.ndarray: + """Sample with Latin Hypercube sampling + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : Optional[int], optional + random seed, by default None + + Returns + ------- + np.ndarray + samples + """ + problem = { + "num_vars": len(domain), + "names": domain.names, + "bounds": [[s.lower_bound, s.upper_bound] + for s in domain.space.values()], + } + + samples = salib_latin.sample(problem, N=n_samples, seed=seed) + return samples + + +def sample_sobol_sequence( + domain: Domain, n_samples: int, **kwargs) -> np.ndarray: + """Sample with Sobol sequence sampling + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + + Returns + ------- + np.ndarray + samples + """ + samples = sobol_sequence.sample(n_samples, len(domain)) + + # stretch samples + samples = _stretch_samples(domain, samples) + return samples + + +# Built-in samplers +# ============================================================================= + + +def randomuniform( + domain: Domain, n_samples: int, seed: int, **kwargs) -> DataTypes: + """ + Random uniform sampling + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : int + random seed for reproducibility + + Returns + ------- + DataTypes + input samples in one of the supported data types for the ExperimentData + input data. + """ + _continuous = sample_np_random_uniform( + domain=domain.continuous, n_samples=n_samples, + seed=seed) + + _discrete = sample_np_random_choice_range( + domain=domain.discrete, n_samples=n_samples, + seed=seed) + + _categorical = sample_np_random_choice( + domain=domain.categorical, n_samples=n_samples, + seed=seed) + + _constant = sample_constant(domain.constant, n_samples) + + df = pd.concat( + [pd.DataFrame(_continuous, columns=domain.continuous.names), + pd.DataFrame(_discrete, columns=domain.discrete.names), + pd.DataFrame( + _categorical, columns=domain.categorical.names), + pd.DataFrame(_constant, columns=domain.constant.names)], axis=1 + )[domain.names] + + return df + + +def grid( + domain: Domain, stepsize_continuous_parameters: + Optional[Dict[str, float] | float] = None, **kwargs) -> DataTypes: + """Receive samples of the search space + + Parameters + ---------- + n_samples : int + number of samples + stepsize_continuous_parameters : Dict[str, float] | float, optional + stepsize for the continuous parameters, by default None. + If a float is given, all continuous parameters are sampled with + the same stepsize. If a dictionary is given, the stepsize for each + continuous parameter can be specified. + + Returns + ------- + DataTypes + input samples in one of the supported data types for the ExperimentData + input data. + + Raises + ------ + ValueError + If the stepsize_continuous_parameters is given as a dictionary + and not specified for all continuous parameters. + """ + continuous = domain.continuous + + if not continuous.space: + discrete_space = continuous.space + + elif isinstance(stepsize_continuous_parameters, (float, int)): + discrete_space = {name: param.to_discrete( + step=stepsize_continuous_parameters) + for name, param in continuous.space.items()} + + elif isinstance(stepsize_continuous_parameters, dict): + discrete_space = {key: continuous.space[key].to_discrete( + step=value) for key, + value in stepsize_continuous_parameters.items()} + + if len(discrete_space) != len(domain.continuous): + raise ValueError( + "If you specify the stepsize for continuous parameters, \ + the stepsize_continuous_parameters should \ + contain all continuous parameters") + + continuous_to_discrete = Domain(discrete_space) + + _iterdict = {} + + for k, v in domain.categorical.space.items(): + _iterdict[k] = v.categories + + for k, v, in domain.discrete.space.items(): + _iterdict[k] = range(v.lower_bound, v.upper_bound+1, v.step) + + for k, v, in continuous_to_discrete.space.items(): + _iterdict[k] = np.arange( + start=v.lower_bound, stop=v.upper_bound, step=v.step) + + df = pd.DataFrame(list(product(*_iterdict.values())), + columns=_iterdict, dtype=object)[domain.names] + + return df + + +def sobol(domain: Domain, n_samples: int, seed: int, **kwargs) -> DataTypes: + """ + Sobol sequence sampling + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : int + random seed for reproducibility + + Returns + ------- + DataTypes + input samples in one of the supported data types for the ExperimentData + input data. + """ + _continuous = sample_sobol_sequence( + domain=domain.continuous, n_samples=n_samples) + + _discrete = sample_np_random_choice_range( + domain=domain.discrete, n_samples=n_samples, seed=seed) + + _categorical = sample_np_random_choice( + domain=domain.categorical, n_samples=n_samples, seed=seed) + + _constant = sample_constant(domain=domain.constant, n_samples=n_samples) + + df = pd.concat( + [pd.DataFrame(_continuous, columns=domain.continuous.names), + pd.DataFrame(_discrete, columns=domain.discrete.names), + pd.DataFrame( + _categorical, columns=domain.categorical.names), + pd.DataFrame(_constant, columns=domain.constant.names)], axis=1 + )[domain.names] + + return df + + +def latin(domain: Domain, n_samples: int, seed: int, **kwargs) -> DataTypes: + """ + Latin Hypercube sampling + + Parameters + ---------- + domain : Domain + domain object + n_samples : int + number of samples + seed : int + random seed for reproducibility + + Returns + ------- + DataTypes + input samples in one of the supported data types for the ExperimentData + input data. + """ + _continuous = sample_latin_hypercube( + domain=domain.continuous, n_samples=n_samples, seed=seed) + + _discrete = sample_np_random_choice_range( + domain=domain.discrete, n_samples=n_samples, seed=seed) + + _categorical = sample_np_random_choice( + domain=domain.categorical, n_samples=n_samples, seed=seed) + + _constant = sample_constant(domain=domain.constant, n_samples=n_samples) + + df = pd.concat( + [pd.DataFrame(_continuous, columns=domain.continuous.names), + pd.DataFrame(_discrete, columns=domain.discrete.names), + pd.DataFrame( + _categorical, columns=domain.categorical.names), + pd.DataFrame(_constant, columns=domain.constant.names)], axis=1 + )[domain.names] + + return df diff --git a/src/f3dasm/design.py b/src/f3dasm/design.py index 420c6328..cfd2b6c0 100644 --- a/src/f3dasm/design.py +++ b/src/f3dasm/design.py @@ -9,7 +9,6 @@ from ._src.design.parameter import (PARAMETERS, _CategoricalParameter, _ConstantParameter, _ContinuousParameter, _DiscreteParameter, _Parameter) -from ._src.design.samplers import Sampler, SamplerNames from ._src.experimentdata._data import _Data from ._src.experimentdata._jobqueue import NoOpenJobsError, Status, _JobQueue @@ -35,6 +34,4 @@ 'Status', '_Data', '_JobQueue', - 'Sampler', - 'SamplerNames', ] diff --git a/studies/README.md b/studies/README.md index 6117ec94..0d344965 100644 --- a/studies/README.md +++ b/studies/README.md @@ -1,144 +1,36 @@ Studies -=========== +======= -This folder denotes studies. +This folder denotes benchmark studies that can be run with the `f3dasm` package. +In order to run a study, you need to have the `f3dasm[benchmark]` extra requirements installed: -## Folder structure and files +``` +pip install f3dasm[benchmark] +``` +## Folder structure and files of a study ``` -├── studies +├── . │ └── my_study -│ ├── custom_module -│ │ ├── custom_script.py -│ │ └── __init__.py │ ├── main.py -│ ├── config.py │ ├── config.yaml │ ├── pbsjob.sh │ └── README.md └── src/f3dasm ``` -* Each study is put in a separate folder, in this case `my_study` +* Each study is put in a separate folder * The `README.md` file gives a description, author and optionally citable source. * The main script that has to be called should be named `main.py` -* Additional scripts or modules can be placed inside the `my_studys` folder. -* `pbsjob.sh` is a [`TORQUE`](https://adaptivecomputing.com/cherry-services/torque-resource-manager/) file that will submit the `main.py` file to a high-performance queuing system. -* The `config.py` and `config.yaml` are [`hydra`](https://hydra.cc/docs/intro/) configuration files. More on that in the next section. - -## Hydra - -Configurations and data-storage for the studys is handled by the [`hydra`](https://hydra.cc/docs/intro/) package. - -* `config.py` denotes the types of all of the configurable parameters: - -```python -from dataclasses import dataclass -from typing import Any, List - -@dataclass -class SubConfig: - parameter1: float - parameter2: List[str] - parameter3: int - -@dataclass -class Config: - subconfig: SubConfig - parameter4: int -``` - -This will help you with type-hinting and write cleaner code. - -* `config.yaml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file containing the values of the configuration parameters: +* `pbsjob.sh` is a batchscript file that will submit the `main.py` file to a [`TORQUE`](https://adaptivecomputing.com/cherry-services/torque-resource-manager/) high-performance queuing system. +* The `config.yaml` are [`hydra`](https://hydra.cc/docs/intro/) configuration files. -```yaml -subconfig: - parameter1: -1.0 - parameter2: ['banana','apple', 'pear'] +## Available studies -parameter4: 3 -``` - -* A minimal `main.py` file will look something like this: - - -```python -import hydra -from config import Config -from hydra.core.config_store import ConfigStore - -import f3dasm - -@hydra.main(config_path=".", config_name="config") -def main(cfg: Config): - ... - - -cs = ConfigStore.instance() -cs.store(name="f3dasm_config", node=Config) - -if __name__ == "__main__": - main() - -``` +There are two benchmark studies available: -The configurations are given in the custom `Config` class type imported from `config.py` as input to the `main(cfg: Config)` function. This is done by the `@hydra.main()` decorater. - -## Executing an study - -Scripts can be run in two ways: - -* Locally on your computer, by running the `main.py` file: - -```bash -$ python3 main.py -``` - -> Make sure you run the file in an environment where `f3dasm` and its dependencies are installed correctly! - -* On a high-performance computer by submitting the `pbsjob.sh` to the queue: - -```bash -$ qsub pbshjob.sh -``` - -> You can create array jobs easily in the commandline with the `-t` flag. - -From the location that you executed/submitted the script, an `/outputs/` folder will be created, if not present. - -In this `/outputs/` folder, a new folder will be created named: - -* `/%year-%month-%day/%hour-%minute-%seconds/` locally -* `$PBS_JOBID/` on the HPC - -The output-data, `hydra` configurations (`/.hyra/`) and logfile (`main.log`) will be automatically put in this folder - -This will look something like this: - - -### Locally -``` -├── outputs - └── 2022-11-30 - └── 13-27-47 - ├── .hydra - | ├── config.yaml - | ├── hydra.yaml - | └── overrides.yaml - ├── main.log - └── data.obj -``` - -### HPC -``` -├── outputs - └── 448990 - ├── .hydra - | ├── config.yaml - | ├── hydra.yaml - | └── overrides.yaml - ├── main.log - └── data.obj -``` \ No newline at end of file +| Study | Description | +| :-- | :-- | +| Fragile becomes supercompressible | A benchmark study that compares the performance of the `f3dasm` package with other packages. | +| Comparing optimization algorithms on benchmark functions | A benchmark study that compares the performance of the `f3dasm` package with other packages. | \ No newline at end of file diff --git a/studies/benchmark_optimizers/README.md b/studies/benchmark_optimizers/README.md new file mode 100644 index 00000000..40f99200 --- /dev/null +++ b/studies/benchmark_optimizers/README.md @@ -0,0 +1,150 @@ +# Comparing optimization algorithms on benchmark functions + +
+drawing +
+ +## Summary + +We create a dataset with the performance of different optimization algorithms on different optimization problems. + +The dataset is created by solving a set of parametrized benchmark functions with a set of optimization algorithms. The benchmark functions are parametrized by their analytical formula, dimensionality, noise, and seed: + +
+drawing +
+ +The benchmark function is optimized for a given number of iterations and repeated with different initial conditions (realizations). The results are stored in a [NetCDF](https://docs.xarray.dev/en/stable/user-guide/io.html#netcdf) (`.nc`) file. + +## Contents of this folder + +| File/Folder | Description | +|-------------|-------------| +| `main.py` | Main script to run the experiment | +| `config.yaml` | Configuration file for the experiment | +| `README.md` | Explanation of this experiment | +| `img/` | Folder with images used in this file | +| `pbsjob.sh` | TORQUE job file to run the experiment in a cluster | +| `outputs/` | Folder with the results of running this experiment | + +> The `outputs/` folder is created when the experiment has been run for the first time. + +## Usage + +### Before running the experiment + +1. Install `f3dasm_optimize` in your environment. See [here](https://bessagroup.github.io/f3dasm_optimize/rst_doc_files/getting_started.html) for instructions. +3. Change the `config.yaml` file to your liking. See [here](#explanation-of-configyaml-parameters) for an explanation of the parameters. + +### Running the experiment on your local machine + +1. Navigate to this folder and run `python main.py` + +### Running the experiment on a TORQUE cluster + +1. Make sure you have an `conda` environment named `f3dasm_env` with the packages installed in the first step +2. Navigate to this folder and submit the job with i.e. 2 nodes: `qsub pbsjob.sh -t 0-2` + + +## Results + +Results are stored in a newly created `outputs` folder, with a subdirectory +indicating the current date (e.g. `2023-11-06`). + +* When running on a local machine, the output will be saved in a directory indicating the current time (e.g. `13-50-14`). +* When running on a cluster, the output will be saved in a directory indicating the current job ID (e.g. `538734.hpc06.hpc`). + +The following subdirectories are created: + +* `experiment_data`: Contains the input, output, domain and jobs to construct the [`f3dasm.ExperimentData`](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/design/experimentdata.html) object. +* ``: Contains a [NetCDF](https://docs.xarray.dev/en/stable/user-guide/io.html#netcdf) (`.nc`) file with the optimization results for each optimization problem. The folder name is the concatenation of the two optimization algorithms used. +* `.hydra`: Contains the `config.yaml` file used to run the experiment. + +Lastly, a log file `main.log` is created. + +The folder structure is as follows: + +``` +outputs/ +└── 2023-11-06/ + └── 13-50-14/ + ├── .hydra/ + ├── PSO/ + ├── LBFGSB/ + ├── CMAES/ + │ ├── 0.nc + │ ├── 1.nc + │ └── 2.nc + ├── experiment_data/ + │ ├── domain.pkl + │ ├── input.csv + │ ├── output.csv + │ └── jobs.pkl + └── main.log +``` + + +## Explanation of `config.yaml` parameters + +### Domain +#### Function Name +| Name | Type | Description | +|------------|----------|----------------------------| +| categories | `List[str]` | List of benchmark functions ([reference](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/datageneration/functions.html)) | + +#### Dimensionality +| Name | Type | Description | +|------------|----------|----------------------------| +| categories | `List[int]` | List of possible values of the function dimensionality | + +#### Noise +| Name | Type | Description | +|------------|----------|----------------------------| +| categories | `List[float]` | List of possible values of the function noise standard deviation | + +#### Seed +| Name | Type | Description | +|------|------|---------------------| +| low | `int` | Lower bound value of random seeds | +| high | `int` | Upper bound value of random seeds | + +#### Budget +| Name | Type | Description | +|-------|----------|-----------------------| +| value | `int` | Maximum number of iterations | + +### Experiment Data +#### From Sampling +| Name | Type | Description | +|--------------|--------|------------------------| +| seed | `int` | Seed value | +| n_samples | `int` | Number of samples | +| domain | `f3dasm.Domain` | `f3dasm` Domain object ([reference](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/design/domain.html)) | + +### Mode +| Name | Type | Description | +|-------|--------|-------------| +| mode | string | Evaluation mode of `f3dasm` ([reference](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/datageneration/datagenerator.html#)) | + +### Optimization +| Name | Type | Description | +|----------------------------|----------|---------------------------| +| lower_bound | `float` | Box-constraint lower bound for every dimension | +| upper_bound | `float` | Box-constraint upper bound for every dimension | +| sampler_name | `str` | Name of the sampling strategy for the first iterations ([reference](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/sampling/sampling.html#id2)) | +| number_of_samples | `int` | Number of initial samples ($\vec{x}_0$) | +| realizations | `int` | Number of realizations with different initial conditions | +| optimizers | `List[str]` | List of dictionaries. Each dictionary contains a key ``name`` with the optimizer name ([from `f3dasm`](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/optimization/optimizers.html#implemented-optimizers), [and `f3dasm_optimize`](https://bessagroup.github.io/f3dasm_optimize/rst_doc_files/optimizers.html#implemented-optimizers)) and a (optionally) a ``hyperparameters`` key to overwrite hyper-parameters of that specific optimzier. | + + +### HPC +| Name | Type | Description | +|--------|------|--------------| +| jobid[^2] | `int` | Job ID of the array-job, automatically overwritten by scheduler bash script | + +[^2]: When running on a local machine, this value will be left as the default: -1. + +### Log Level +| Name | Type | Description | +|-----------|------|-----------------| +| log_level | `int` | Log level value ([see `logging` module for more info](https://docs.python.org/3/library/logging.html#logging-levels)) | diff --git a/studies/benchmark_optimizers/config.yaml b/studies/benchmark_optimizers/config.yaml new file mode 100644 index 00000000..455806b4 --- /dev/null +++ b/studies/benchmark_optimizers/config.yaml @@ -0,0 +1,117 @@ +domain: + function_name: + type: category + categories: ['Ackley', + 'Ackley N. 2', + 'Ackley N. 3', + 'Ackley N. 4', + 'Adjiman', + 'Bartels', + 'Beale', + 'Bird', + 'Bohachevsky N. 1', + 'Bohachevsky N. 2', + 'Bohachevsky N. 3', + 'Booth', + 'Branin', + 'Brent', + 'Brown', + 'Bukin N. 6', + 'Colville', + 'Cross-in-Tray', + 'De Jong N. 5', + 'Deckkers-Aarts', + 'Dixon Price', + 'Drop-Wave', + 'Easom', + 'Egg Crate', + 'Egg Holder', + 'Exponential', + 'Goldstein-Price', + 'Griewank', + 'Happy Cat', + 'Himmelblau', + 'Holder-Table', + 'Keane', + 'Langermann', + 'Leon', + 'Levy', + 'Levy N. 13', + 'Matyas', + 'McCormick', + 'Michalewicz', + 'Periodic', + 'Powell', + 'Qing', + 'Quartic', + 'Rastrigin', + 'Ridge', + 'Rosenbrock', + 'Rotated Hyper-Ellipsoid', + 'Salomon', + 'Schaffel N. 1', + 'Schaffel N. 2', + 'Schaffel N. 3', + 'Schaffel N. 4', + 'Schwefel', + 'Schwefel 2.20', + 'Schwefel 2.21', + 'Schwefel 2.22', + 'Schwefel 2.23', + 'Shekel', + 'Shubert', + 'Shubert N. 3', + 'Shubert N. 4', + 'Sphere', + 'Styblinski Tang', + 'Sum Squares', + 'Thevenot', + 'Three-Hump', + 'Trid', + 'Wolfe', + 'Xin She Yang', + 'Xin She Yang N.2', + 'Xin She Yang N.3', + 'Xin-She Yang N.4', + 'Zakharov'] + dimensionality: + type: category + categories: [2, 10, 20, 50, 100] + noise: + type: category + categories: [0.0, 0.1] + seed: + type: int + low: 0 + high: 1000000 + budget: + type: constant + value: 200 + +experimentdata: + # from_file: /home/martin/Documents/GitHub/L2CO/experiments/create_trainingdata_two_optimizers/outputs/2024-03-06/16-11-02 + from_sampling: + seed: 2036 + n_samples: 20 + domain: ${domain} + +mode: cluster + +optimization: + lower_bound: 0.0 + upper_bound: 1.0 + sampler_name: "latin" + number_of_samples: 30 + realizations: 3 + optimizers: + - name: CMAES + - name: PSO + - name: Adam + hyperparameters: + learning_rate: 0.04 + - name: LBFGSB + +hpc: + jobid: -1 + +log_level: 20 diff --git a/studies/benchmark_optimizers/img/data-driven-process.png b/studies/benchmark_optimizers/img/data-driven-process.png new file mode 100644 index 00000000..90a4741c Binary files /dev/null and b/studies/benchmark_optimizers/img/data-driven-process.png differ diff --git a/studies/benchmark_optimizers/img/problems.png b/studies/benchmark_optimizers/img/problems.png new file mode 100644 index 00000000..08a465d2 Binary files /dev/null and b/studies/benchmark_optimizers/img/problems.png differ diff --git a/studies/benchmark_optimizers/main.py b/studies/benchmark_optimizers/main.py new file mode 100644 index 00000000..c7294355 --- /dev/null +++ b/studies/benchmark_optimizers/main.py @@ -0,0 +1,207 @@ + + +""" +Main entrypoint of the experiment + +Functions +--------- + +main + Main script to call +pre_processing + Pre-processing steps +process + Main process to execute +""" + +# +# Modules +# ============================================================================= + +# Standard +from pathlib import Path +from time import sleep + +# Third-party +import hydra +import numpy as np +import pandas as pd +import xarray as xr + +# Local +from f3dasm import ExperimentData +from f3dasm.datageneration import DataGenerator +from f3dasm.datageneration.functions import get_functions +from f3dasm.design import Domain, make_nd_continuous_domain + +# Authorship & Credits +# ============================================================================= +__author__ = 'Martin van der Schelling (M.P.vanderSchelling@tudelft.nl)' +__credits__ = ['Martin van der Schelling'] +__status__ = 'Stable' +# ============================================================================= +# +# ============================================================================= + + +# Custom sampler method +# ============================================================================= + +def sample_if_compatible_function( + domain: Domain, n_samples: int, seed: int) -> pd.DataFrame: + rng = np.random.default_rng(seed) + samples = [] + + for i in range(n_samples): + dim = rng.choice(domain.space['dimensionality'].categories) + + available_functions = list(set(get_functions(d=int(dim))) & set( + domain.space['function_name'].categories)) + function_name = rng.choice(available_functions) + + noise = rng.choice(domain.space['noise'].categories) + seed = rng.integers( + low=domain.space['seed'].lower_bound, + high=domain.space['seed'].upper_bound) + budget = domain.space['budget'].value + + samples.append([function_name, dim, noise, seed, budget]) + + return pd.DataFrame(samples, columns=domain.names)[domain.names] + +# Custom datagenerator +# ============================================================================= + + +class BenchmarkOptimizer(DataGenerator): + def __init__(self, config): + self.config = config + + def optimize_function(self, optimizer: dict) -> xr.Dataset: + seed = self.experiment_sample.get('seed') + function_name = self.experiment_sample.get('function_name') + dimensionality = self.experiment_sample.get('dimensionality') + noise = self.experiment_sample.get('noise') + budget = self.experiment_sample.get('budget') + + hyperparameters = optimizer['hyperparameters'] \ + if 'hyperparameters' in optimizer else {} + + # inside loop + data_list = [] + for r in range(self.config.optimization.realizations): + + domain = make_nd_continuous_domain( + bounds=np.tile( + [self.config.optimization.lower_bound, + self.config.optimization.upper_bound], + (dimensionality, 1))) + data = ExperimentData.from_sampling( + sampler=self.config.optimization.sampler_name, domain=domain, + n_samples=self.config.optimization.number_of_samples, + seed=seed + r) + + data.evaluate( + data_generator=function_name, + kwargs={'scale_bounds': domain.get_bounds(), 'offset': True, + 'noise': noise, 'seed': seed}, + mode='sequential') + + data.optimize( + optimizer=optimizer['name'], data_generator=function_name, + kwargs={'scale_bounds': domain.get_bounds( + ), 'offset': True, 'noise': noise, 'seed': seed}, + iterations=budget, x0_selection='best', + hyperparameters={'seed': seed + r, + **hyperparameters}) + + data_list.append(data.to_xarray()) + + return xr.concat(data_list, dim=xr.DataArray( + range(self.config.optimization.realizations), dims='realization')) + + def execute(self): + for optimizer in self.config.optimization.optimizers: + opt_results = self.optimize_function(optimizer) + + self.experiment_sample.store( + object=opt_results, name=optimizer['name'], to_disk=True) + +# Data-driven workflow +# ============================================================================= + + +def pre_processing(config): + + if 'from_sampling' in config.experimentdata: + experimentdata = ExperimentData.from_sampling( + sampler=sample_if_compatible_function, + domain=Domain.from_yaml(config.domain), + n_samples=config.experimentdata.from_sampling.n_samples, + seed=config.experimentdata.from_sampling.seed) + + else: + experimentdata = ExperimentData.from_yaml(config.experimentdata) + + experimentdata.store(Path.cwd()) + + +def process(config): + """Main script that handles the execution of open jobs + + Parameters + ---------- + config + Hydra configuration file object + """ + project_dir = Path().cwd() + + # Retrieve the ExperimentData object + max_tries = 500 + tries = 0 + + while tries < max_tries: + try: + data = ExperimentData.from_file(project_dir) + break # Break out of the loop if successful + except FileNotFoundError: + tries += 1 + sleep(10) + + if tries == max_tries: + raise FileNotFoundError(f"Could not open ExperimentData after " + f"{max_tries} attempts.") + + benchmark_optimizer = BenchmarkOptimizer(config) + + data.evaluate(data_generator=benchmark_optimizer, mode=config.mode) + + if config.mode == 'sequential': + # Store the ExperimentData to a csv file + data.store() + + +@hydra.main(config_path=".", config_name="config") +def main(config): + """Main script to call + + Parameters + ---------- + config + Configuration parameters defined in config.yaml + """ + # Execute the initial_script for the first job + if config.hpc.jobid == 0: + pre_processing(config) + + elif config.hpc.jobid == -1: # Sequential + pre_processing(config) + process(config) + + else: + sleep(3*config.hpc.jobid) # To asynchronize the jobs + process(config) + + +if __name__ == "__main__": + main() diff --git a/studies/benchmark_optimizers/pbsjob.sh b/studies/benchmark_optimizers/pbsjob.sh new file mode 100644 index 00000000..f302b8e4 --- /dev/null +++ b/studies/benchmark_optimizers/pbsjob.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Torque directives (#PBS) must always be at the start of a job script! +#PBS -N CreateTrainingData +#PBS -q mse +#PBS -l nodes=1:ppn=12,walltime=12:00:00 +# Set the name of the job +# +# Set the rerunable flag, 'n' is not rerunable, default is 'y' + +# Make sure I'm the only one that can read my output +umask 0077 + +# #################################################### +# # OPTIONAL: creating temporary directory on the node +# # for details see: +# # https://hpcwiki.tudelft.nl/index.php/More_about_queues_and_nodes + +# # create a temporary directory in /var/tmp +# TMP=/var/tmp/${PBS_JOBID} +# mkdir -p ${TMP} +# echo "Temporary work dir: ${TMP}" +# if [ ! -d "${TMP}" ]; then +# echo "Cannot create temporary directory. Disk probably full." +# exit 1 +# fi + +# # copy the input files to ${TMP} +# echo "Copying from ${PBS_O_WORKDIR}/ to ${TMP}/" +# /usr/bin/rsync -vax "${PBS_O_WORKDIR}/" ${TMP}/ +# cd ${TMP} +# #################################################### + +JOB_ID=$(echo "${PBS_JOBID}" | sed 's/\[[^][]*\]//g') + +module load use.own +module load miniconda3 +cd $PBS_O_WORKDIR + +# Here is where the application is started on the node +# activating my conda environment: + +source activate f3dasm_env + +# limiting number of threads -> see hpcwiki + +OMP_NUM_THREADS=12 +export OMP_NUM_THREADS=12 + + +# Check if PBS_ARRAYID exists, else set to -1 +if ! [ -n "${PBS_ARRAYID+1}" ]; then + PBS_ARRAYID=-1 +fi + +#Executing my python program + +python main.py ++hpc.jobid=${PBS_ARRAYID} hydra.run.dir=outputs/${now:%Y-%m-%d}/${JOB_ID} + +# job done, copy everything back +# echo "Copying from ${TMP}/ to ${PBS_O_WORKDIR}/" +# /usr/bin/rsync -vax ${TMP}/ "${PBS_O_WORKDIR}/" + +# # delete my temporary files +# [ $? -eq 0 ] && /bin/rm -rf ${TMP} \ No newline at end of file diff --git a/studies/fragile_becomes_supercompressible/3d_domain.yaml b/studies/fragile_becomes_supercompressible/3d_domain.yaml new file mode 100644 index 00000000..0f3cddc5 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/3d_domain.yaml @@ -0,0 +1,28 @@ +domain: + young_modulus: + type: constant + value: 3500.0 + n_longerons: + type: constant + value: 3 + bottom_diameter: + type: constant + value: 100.0 + ratio_top_diameter: + type: float + low: 0.0 + high: 0.8 + ratio_pitch: + type: float + low: 0.25 + high: 1.50 + ratio_d: + type: float + low: 0.004 + high: 0.073 + ratio_shear_modulus: + type: constant + value: 0.3677 + circular: + type: constant + value: true \ No newline at end of file diff --git a/studies/fragile_becomes_supercompressible/7d_domain.yaml b/studies/fragile_becomes_supercompressible/7d_domain.yaml new file mode 100644 index 00000000..fb8a32de --- /dev/null +++ b/studies/fragile_becomes_supercompressible/7d_domain.yaml @@ -0,0 +1,41 @@ +domain: + young_modulus: + type: constant + value: 3500.0 + n_longerons: + type: constant + value: 3 + bottom_diameter: + type: constant + value: 100.0 + ratio_top_diameter: + type: float + low: 0.0 + high: 0.8 + ratio_pitch: + type: float + low: 0.25 + high: 1.50 + ratio_shear_modulus: + type: float + low: 0.035 + high: 0.45 + ratio_area: + type: float + low: 0.0000117 + high: 0.0041 + ratio_Ixx: + type: float + low: 0.00000000001128 + high: 0.0000014 + ratio_Iyy: + type: float + low: 0.00000000001128 + high: 0.0000014 + ratio_J: + type: float + low: 0.00000000001353 + high: 0.00000777 + circular: + type: constant + value: false \ No newline at end of file diff --git a/studies/fragile_becomes_supercompressible/README.md b/studies/fragile_becomes_supercompressible/README.md new file mode 100644 index 00000000..da3ccc84 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/README.md @@ -0,0 +1,272 @@ +# Fragile becomes supercompressible + +
+ Image 1 + Image 2 + Image 3 +
+ +
+ +This study is based on the work of [Bessa et al. (2019)](https://onlinelibrary.wiley.com/doi/full/10.1002/adma.201904845) and aims to reproduce the results of the paper using the `f3dasm` package. +## Summary + +For years, creating new materials has been a time consuming effort that requires significant resources because we have followed a trial-and-error design process. Now, a new paradigm is emerging where machine learning is used to design new materials and structures with unprecedented properties. Using this data-driven process, a new super-compressible meta-material was discovered despite being made of a fragile polymer. + +The above figure shows the newly developed meta-material prototype that was designed with the above-mentioned computational data-driven approach and where experiments were used for validation, not discovery. This enabled the design and additive manufacturing of a lightweight, recoverable and super-compressible meta-material achieving more than 90% compressive strain when using a brittle base material that breaks at around 4% strain. Within minutes, the machine learning model was used to optimize designs for different choices of base material, length-scales and manufacturing process. Current results show that super-compressibility is possible for optimized designs reaching stresses on the order of 1 kPa using brittle polymers, or stresses on the order of 20 MPa using carbon like materials. + +#### Design of experiments + +The supercompressible meta-material is parameterized by 5 geometric parameters and 2 material parameters. The geometry is defined by the top and bottom diameters, $D_1$ and $D_2$, the height $P$ and the cross-section parameters of the vertical longerons: the cross-sectional area $A$, moments of inertial $I_x$ and $I_y$, and torsional constant $J$. The isotropic material is defined by its elastic constants: Young's modulus $E$ and shear modulus $G$. + +
+drawing +
+ +
+ +Due to the principle of superposition both the geometric and material parameters can be scaled by one of its dimensions/properties (here $D_1$ and $E$). Therefore, the variables that you will find in the dataset are: + +$$ +\frac{D_1-D_2}{D_1},\ \frac{P}{D_1},\ \frac{I_x}{D_1^4},\ \frac{I_y}{D_1^4},\ \frac{J}{D_1^4},\ \frac{A}{D_1^2}, \frac{G}{E} +$$ + +| expression | parameter name | +| ----------- | --------------- | +| $\frac{D_1-D_2}{D_1}$ | `ratio_top_diameter` +|$\frac{P}{D_1}$| `ratio_pitch` +|$\frac{I_x}{D_1^4}$| `ratio_Ixx` +|$\frac{I_y}{D_1^4}$| `ratio_Iyy` +|$\frac{J}{D_1^4}$| `ratio_J` +|$\frac{A}{D_1^2}$| `ratio_area` +|$\frac{G}{E}$| `ratio_shear_modulus` + +This is a 7-dimensional problem and learning the response surface may require a significant amount of training points[^1]. Therefore, you will also consider a simpler version of the problem in 3 dimensions, defined by constraining the longerons' cross-section to be circular with diameter $d$, and choosing a particular material, leading to the following 3 features: + +$$ +\frac{d}{D_1}, \frac{D_2-D_1}{D_1},\ \frac{P}{D_1} +$$ + +| expression | parameter name | +| ----------- | --------------- | +$\frac{D_1-D_2}{D_1}$| `ratio_top_diameter` +$\frac{P}{D_1}$ |`ratio_pitch` +$\frac{d}{D_1}$ |`ratio_d` + +[^1]: Remember the "curse of dimensionality"! + + + + + + +## Contents of this folder + +| File/Folder | Description | +|-------------|-------------| +| `main.py` | Main script to run the experiment | +| `config.yaml` | Configuration file for the experiment | +| `README.md` | Explanation of this experiment | +| `img/` | Folder with images used in this file | +| `pbsjob.sh` | TORQUE job file to run the experiment in a cluster | +| `outputs/` | Folder with the results of running this experiment | + +> The `outputs/` folder is created when the experiment has been run for the first time. + +## Usage + +### Before running the experiment + +1. Install the `abaqus2py` package. See [here](https://github.com/bessagroup/abaqus2py) for instructions. +2. Change the `config.yaml` file to your liking. See [here](#explanation-of-configyaml-parameters) for an explanation of the parameters. + +### Running the experiment on your local machine + +1. Navigate to this folder and run `python main.py` + +### Running the experiment on a TORQUE cluster + +1. Make sure you have an `conda` environment named `f3dasm_env` with the packages installed in the first step +2. Navigate to this folder and submit the job with i.e. 2 nodes: `qsub pbsjob.sh -t 0-2` + + +## Results + +Results are stored in a newly created `outputs` folder, with a subdirectory +indicating the current date (e.g. `2023-11-06`). + +* When running on a local machine, the output will be saved in a directory indicating the current time (e.g. `13-50-14`). +* When running on a cluster, the output will be saved in a directory indicating the current job ID (e.g. `538734.hpc06.hpc`). + +The following subdirectories are created: + +* `experiment_data`: Contains the input, output, domain and jobs to construct the [`f3dasm.ExperimentData`](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/design/experimentdata.html) object. +* `.hydra`: Contains the `config.yaml` file used to run the experiment. +* `lin_buckle` and `riks`: Contain the ABAQUS simulation results for the linear buckling and Riks analysis, respectively. + + +Lastly, a log file `main.log` is created. + +The folder structure is as follows: + +``` +outputs/ +└── 2023-11-06/ + └── 13-50-14/ + ├── .hydra/ + ├── experiment_data/ + │ ├── domain.pkl + │ ├── input.csv + │ ├── output.csv + │ └── jobs.pkl + ├── lin_buckle/ + │ ├── 0/ + │ ├── 1/ + │ └── 2/ + ├── riks/ + │ ├── 0/ + │ ├── 1/ + │ └── 2/ + ├── loads/ + │ ├── 0.npy + │ ├── 1.npy + │ └── 2.npy + ├── max_disps/ + │ ├── 0.ny + │ ├── 1.npy + │ └── 2.npy + └── main.log +``` + +## Explanation of `config.yaml` parameters + +There are two different configurations for this experiment: + - The full 7-dimensional problem as defined in the paper +- The 3-dimensional problem, defined by constraining the longerons' cross-section to be circular with diameter $d$ and choosing a fixed material. + +### Common problem domain + +#### young_modulus +| Name | Type | Description | +|------------|----------|----------------------------| +| value | `float` | Young's modulus value | + +#### n_longerons +| Name | Type | Description | +|------------|----------|----------------------------| +| value | `float` | Number of longerons in the design | + +#### bottom_diameter ($D_2$) +| Name | Type | Description | +|------------|----------|----------------------------| +| value | `float` | Bottom diameter of the design | + +#### ratio_top_diameter ($\frac{D_1-D_2}{D_1}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of top diamater ratio | +| high | `float` | Upper bound of top diamater ratio | + +#### ratio_pitch ($\frac{P}{D_1}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of the pitch ratio | +| high | `float` | Upper bound of the pitch ratio | + +### 3 dimensional problem domain + + +#### ratio_d ($\frac{d}{D_1}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of longerons cross-section | +| high | `float` | Upper bound of longerons cross-section | + +#### ratio_shear_modulus ($\frac{G}{E}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| value | `float` | Lower bound of shear modulus ratio | + +### 7 dimensional problem domain + +#### ratio_area ($\frac{A}{D_1^2}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of the area ratio | +| high | `float` | Upper bound of the area ratio | + +#### ratio_Ixx ($\frac{I_x}{D_1^4}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of the $I_{xx}$ ratio | +| high | `float` | Upper bound of the $I_{xx}$ ratio | + +#### ratio_Iyy ($\frac{I_y}{D_1^4}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of the $I_{yy}$ ratio | +| high | `float` | Upper bound of the $I_{yy}$ ratio | + +#### ratio_J ($\frac{J}{D_1^4}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of the $J$ ratio | +| high | `float` | Upper bound of the $J$ ratio | + +#### ratio_shear_modulus ($\frac{G}{E}$) +| Name | Type | Description | +|------------|----------|----------------------------| +| low | `float` | Lower bound of shear modulus ratio | +| high | `float` | Upper bound of shear modulus ratio | + + +#### circular +| Name | Type | Description | +|------------|----------|----------------------------| +| value | `bool` | If the design is simplified or not | + +... + +### Experiment Data +#### from Sampling +| Name | Type | Description | +|--------------|--------|------------------------| +| sampler | `str` | Sampler name | +| seed | `int` | Seed value | +| n_samples | `int` | Number of samples | +| domain | `f3dasm.Domain` | `f3dasm` Domain object ([reference](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/design/domain.html)) | + +### mode +| Name | Type | Description | +|-------|--------|-------------| +| mode | string | Evaluation mode of `f3dasm` ([reference](https://f3dasm.readthedocs.io/en/latest/rst_doc_files/classes/datageneration/datagenerator.html#)) | + + +### hpc +| Name | Type | Description | +|--------|------|--------------| +| jobid[^2] | `int` | Job ID of the array-job, automatically overwritten by scheduler bash script | + +[^2]: When running on a local machine, this value will be left as the default: -1. + +### imperfection + +| Name | Type | Description | +|--------------|--------|------------------------| +| mean | `float` | Mean value of lognormal distribution | +| std | `float` | Standard deviation value of lognormal distribution | + +### scripts + +| Name | Type | Description | +|--------------|--------|------------------------| +| lin_buckle_pre | `str` | Absolute path of linear buckling script | +| lin_buckle_post | `str` | Absolute path of linear buckling post-processing script | +| riks_pre | `str` | Absolute path of RIKS analysis script | +| riks_post | `str` | Absolute path of RIKS analysis post-processing script | + + +### Logging +| Name | Type | Description | +|-----------|------|-----------------| +| log_level | `int` | Log level value ([see `logging` module for more info](https://docs.python.org/3/library/logging.html#logging-levels)) | diff --git a/studies/fragile_becomes_supercompressible/config.yaml b/studies/fragile_becomes_supercompressible/config.yaml new file mode 100644 index 00000000..ee448a2d --- /dev/null +++ b/studies/fragile_becomes_supercompressible/config.yaml @@ -0,0 +1,35 @@ +defaults: + - 7d_domain + - override hydra/job_logging: custom + +experimentdata: + from_file: ./example_design + + # from_sampling: + # sampler: latin + # seed: 42 + # n_samples: 3 + # domain: ${domain} + +mode: sequential + +hpc: + jobid: -1 + +imperfection: + mean: -2.705021452041446 + sigma: 0.293560379208524 + domain: + imperfection: + type: float + low: 0.0 + high: 1.0 + +scripts: + lin_buckle_pre: /home/martin/Documents/GitHub/F3DASM/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle.py + lin_buckle_post: /home/martin/Documents/GitHub/F3DASM/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle_pp.py + riks_pre: /home/martin/Documents/GitHub/F3DASM/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks.py + riks_post: /home/martin/Documents/GitHub/F3DASM/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks_pp.py + + +log_level: 20 \ No newline at end of file diff --git a/studies/fragile_becomes_supercompressible/example_design/experiment_data/domain.pkl b/studies/fragile_becomes_supercompressible/example_design/experiment_data/domain.pkl new file mode 100644 index 00000000..195a1995 Binary files /dev/null and b/studies/fragile_becomes_supercompressible/example_design/experiment_data/domain.pkl differ diff --git a/studies/fragile_becomes_supercompressible/example_design/experiment_data/input.csv b/studies/fragile_becomes_supercompressible/example_design/experiment_data/input.csv new file mode 100644 index 00000000..0878f0a3 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/example_design/experiment_data/input.csv @@ -0,0 +1,2 @@ +,bottom_diameter,circular,imperfection,n_longerons,ratio_Ixx,ratio_Iyy,ratio_J,ratio_area,ratio_pitch,ratio_shear_modulus,ratio_top_diameter,young_modulus +0,100,0,0.2,3,0.0000001697758,0.0000003717950,1e-06,0.003624,0.75,0.367714286,0.4,3500.0 diff --git a/studies/fragile_becomes_supercompressible/example_design/experiment_data/jobs.pkl b/studies/fragile_becomes_supercompressible/example_design/experiment_data/jobs.pkl new file mode 100644 index 00000000..68654e40 Binary files /dev/null and b/studies/fragile_becomes_supercompressible/example_design/experiment_data/jobs.pkl differ diff --git a/studies/fragile_becomes_supercompressible/example_design/experiment_data/output.csv b/studies/fragile_becomes_supercompressible/example_design/experiment_data/output.csv new file mode 100644 index 00000000..a35ab0cb --- /dev/null +++ b/studies/fragile_becomes_supercompressible/example_design/experiment_data/output.csv @@ -0,0 +1,2 @@ +"" +0 diff --git a/studies/fragile_becomes_supercompressible/hydra/job_logging/custom.yaml b/studies/fragile_becomes_supercompressible/hydra/job_logging/custom.yaml new file mode 100644 index 00000000..fc2dc776 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/hydra/job_logging/custom.yaml @@ -0,0 +1,20 @@ +# python logging configuration for tasks +version: 1 +formatters: + simple: + format: "[%(asctime)s][%(name)s][%(levelname)s] - %(message)s" +handlers: + console: + class: logging.StreamHandler + formatter: simple + stream: ext://sys.stdout + file: + class: f3dasm.DistributedFileHandler + formatter: simple + # absolute file path + filename: ${hydra.runtime.output_dir}/${hydra.job.name}.log +root: + level: INFO + handlers: [console, file] + +disable_existing_loggers: false diff --git a/studies/fragile_becomes_supercompressible/img/fifty.png b/studies/fragile_becomes_supercompressible/img/fifty.png new file mode 100644 index 00000000..14b77020 Binary files /dev/null and b/studies/fragile_becomes_supercompressible/img/fifty.png differ diff --git a/studies/fragile_becomes_supercompressible/img/ninety.png b/studies/fragile_becomes_supercompressible/img/ninety.png new file mode 100644 index 00000000..287bbd81 Binary files /dev/null and b/studies/fragile_becomes_supercompressible/img/ninety.png differ diff --git a/studies/fragile_becomes_supercompressible/img/supercompressible_metamaterial.png b/studies/fragile_becomes_supercompressible/img/supercompressible_metamaterial.png new file mode 100644 index 00000000..78371f7e Binary files /dev/null and b/studies/fragile_becomes_supercompressible/img/supercompressible_metamaterial.png differ diff --git a/studies/fragile_becomes_supercompressible/img/undeformed.png b/studies/fragile_becomes_supercompressible/img/undeformed.png new file mode 100644 index 00000000..1b3a808c Binary files /dev/null and b/studies/fragile_becomes_supercompressible/img/undeformed.png differ diff --git a/studies/fragile_becomes_supercompressible/main.py b/studies/fragile_becomes_supercompressible/main.py new file mode 100644 index 00000000..2f608819 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/main.py @@ -0,0 +1,187 @@ + + +""" +Main entrypoint of the experiment + +Functions +--------- + +main + Main script to call +pre_processing + Pre-processing steps +process + Main process to execute +""" + +# +# Modules +# ============================================================================= + +# Standard +from pathlib import Path +from time import sleep +from typing import Optional + +# Third-party +import hydra +import numpy as np +import pandas as pd +from f3dasm import ExperimentData +from f3dasm import logger as f3dasm_logger +from f3dasm.design import Domain + +from abaqus2py import F3DASMAbaqusSimulator + +# Authorship & Credits +# ============================================================================= +__author__ = 'Martin van der Schelling (M.P.vanderSchelling@tudelft.nl)' +__credits__ = ['Martin van der Schelling'] +__status__ = 'Stable' +# ============================================================================= +# +# ============================================================================= + + +# Custom sampler method +# ============================================================================= + +def log_normal_sampler(domain: Domain, n_samples: int, + mean: float, sigma: float, seed: Optional[int] = None): + """Sampler function for lognormal distribution + + Parameters + ---------- + domain + Domain object + n_samples + Number of samples to generate + mean + Mean of the lognormal distribution + sigma + Standard deviation of the lognormal distribution + seed + Seed for the random number generator + + Returns + ------- + DataFrame + pandas DataFrame with the samples + """ + rng = np.random.default_rng(seed) + sampled_imperfections = rng.lognormal( + mean=mean, sigma=sigma, size=n_samples) + return pd.DataFrame(sampled_imperfections, columns=domain.names) + +# ============================================================================= + + +def pre_processing(config): + experimentdata = ExperimentData.from_yaml(config.experimentdata) + + if 'from_sampling' in config.imperfection: + domain_imperfections = Domain.from_yaml(config.imperfection.domain) + + imperfections = ExperimentData.from_sampling( + sampler=log_normal_sampler, + domain=domain_imperfections, + n_samples=config.experimentdata.from_sampling.n_samples, + mean=config.imperfection.mean, + sigma=config.imperfection.sigma, + seed=config.experimentdata.from_sampling.seed) + + experimentdata = experimentdata.join(imperfections) + + experimentdata.store(Path.cwd()) + + # Create directories for ABAQUS results + (Path.cwd() / 'lin_buckle').mkdir(exist_ok=True) + (Path.cwd() / 'riks').mkdir(exist_ok=True) + + +def post_processing(config): + ... + + +def process(config): + """Main script that handles the execution of open jobs + + Parameters + ---------- + config + Hydra configuration file object + """ + # if 'from_file' in config.experimentdata: + # project_dir = config.experimentdata.from_file + + # else: + project_dir = Path().cwd() + + # Retrieve the ExperimentData object + max_tries = 500 + tries = 0 + + while tries < max_tries: + try: + data = ExperimentData.from_file(project_dir) + break # Break out of the loop if successful + except FileNotFoundError: + tries += 1 + sleep(10) + + if tries == max_tries: + raise FileNotFoundError(f"Could not open ExperimentData after " + f"{max_tries} attempts.") + + simulator_lin_buckle = F3DASMAbaqusSimulator( + py_file=config.scripts.lin_buckle_pre, + post_py_file=config.scripts.lin_buckle_post, + working_directory=Path.cwd() / 'lin_buckle', + max_waiting_time=60) + simulator_riks = F3DASMAbaqusSimulator( + py_file=config.scripts.riks_pre, + post_py_file=config.scripts.riks_post, + working_directory=Path.cwd() / 'riks', + max_waiting_time=120) + + data.evaluate(data_generator=simulator_lin_buckle, mode=config.mode) + + data.store() + + data.mark_all('open') + + data.evaluate(data_generator=simulator_riks, mode=config.mode) + + if config.mode == 'sequential': + # Store the ExperimentData to a csv file + data.store() + + +@hydra.main(config_path=".", config_name="config") +def main(config): + """Main script to call + + Parameters + ---------- + config + Configuration parameters defined in config.yaml + """ + + f3dasm_logger.setLevel(config.log_level) + # Execute the initial_script for the first job + if config.hpc.jobid == 0: + pre_processing(config) + post_processing(config) + + elif config.hpc.jobid == -1: # Sequential + pre_processing(config) + process(config) + post_processing(config) + + else: + sleep(3*config.hpc.jobid) # To asynchronize the jobs + process(config) + + +if __name__ == "__main__": + main() diff --git a/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle.py b/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle.py new file mode 100644 index 00000000..b2c4d180 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle.py @@ -0,0 +1,351 @@ +''' +Created on 2020-09-22 12:07:10 +Last modified on 2020-09-30 07:59:40 + +@author: L. F. Pereira (lfpereira@fe.up.pt)) +''' + +# imports + +from __future__ import division + +# standard library +import itertools + +import mesh +# third-party +import numpy as np +from abaqus import backwardCompatibility, mdb +from abaqusConstants import (ANALYTIC_RIGID_SURFACE, B31, BEFORE_ANALYSIS, + BUCKLING_MODES, CARTESIAN, CONSTANT, + DEFORMABLE_BODY, DURING_ANALYSIS, FINER, + FROM_SECTION, IMPRINT, ISOTROPIC, KINEMATIC, + LINEAR, MIDDLE_SURFACE, N1_COSINES, OFF, ON, + THREE_D, WHOLE_SURFACE) +# abaqus +from caeModules import * # allow noGui # NOQA +from part import EdgeArray + +# n_longerons = 3 +# bottom_diameter = 100. +# young_modulus = 3.5e3 +# shear_modulus = 1287. +# ratio_d = 0.02000 +# ratio_pitch = 0.750000 +# ratio_top_diameter = 0.40 + + +def main(dict): + + # variable definition + model_name = 'SUPERCOMPRESSIBLE_LIN_BUCKLE' + job_number = 'SUPERCOMPRESSIBLE_LIN_BUCKLE' # int(dict['job_number']) + + # Flag + circular = bool(dict['circular']) # True or False + + # Constant parameters DoE + n_longerons = int(dict['n_longerons']) + young_modulus = dict['young_modulus'] + bottom_diameter = dict['bottom_diameter'] + + # Variables from DoE + ratio_pitch = dict['ratio_pitch'] + ratio_top_diameter = dict['ratio_top_diameter'] + ratio_shear_modulus = dict['ratio_shear_modulus'] + + # calculate the shear_modulus: + shear_modulus = ratio_shear_modulus * young_modulus + + if circular: + ratio_d = dict['ratio_d'] + d = ratio_d * bottom_diameter + + # 7D + if not circular: + ratio_area = dict['ratio_area'] + ratio_Ixx = dict['ratio_Ixx'] + ratio_Iyy = dict['ratio_Iyy'] + ratio_J = dict['ratio_J'] + area = ratio_area * bottom_diameter ** 2 + Ixx = ratio_Ixx * bottom_diameter ** 4 + Iyy = ratio_Iyy * bottom_diameter ** 4 + J = ratio_J * bottom_diameter ** 4 + + # variables from ratios + pitch = ratio_pitch * bottom_diameter + top_diameter = bottom_diameter * (1 - ratio_top_diameter) + + # variables with defaults + n_storeys = 1 + twist_angle = 0. + transition_length_ratio = 1. + + # compute variables + mast_radius = bottom_diameter / 2. + mast_height = n_storeys * pitch + cone_slope = (bottom_diameter - top_diameter) / bottom_diameter + + # create abaqus model + model = mdb.Model(name=model_name) + backwardCompatibility.setValues(reportDeprecated=False) + if 'Model-1' in mdb.models.keys(): + del mdb.models['Model-1'] + + # create joints + joints = np.zeros((n_storeys + 1, n_longerons, 3)) + for i_storey in range(0, n_storeys + 1, 1): + zcoord = mast_height / n_storeys * i_storey + aux1 = 2.0 * np.pi / n_longerons + aux2 = twist_angle * min( + zcoord / mast_height / transition_length_ratio, 1.0) + for i_vertex in range(0, n_longerons): + aux3 = aux1 * i_vertex + aux2 + xcoord = mast_radius * np.cos(aux3) + ycoord = mast_radius * np.sin(aux3) + joints[i_storey, i_vertex, :] = ( + xcoord * (1.0 - min( + zcoord, transition_length_ratio * mast_height + ) / mast_height * cone_slope), + ycoord * (1.0 - min( + zcoord, transition_length_ratio * mast_height + ) / mast_height * cone_slope), + zcoord) + + # create geometry longerons + longerons_name = 'LONGERONS' + part_longerons = model.Part(longerons_name, dimensionality=THREE_D, + type=DEFORMABLE_BODY) + longeron_points = [] + for i_vertex in range(0, n_longerons): + # get required points + longeron_points.append([joints[i_storey, i_vertex, :] + for i_storey in range(0, n_storeys + 1)]) + # create wires + part_longerons.WirePolyLine(points=longeron_points[-1], + mergeType=IMPRINT, meshable=ON) + + # create surface + surface_name = 'ANALYTICAL_SURF' + s = model.ConstrainedSketch(name='SURFACE_SKETCH', + sheetSize=mast_radius * 3.0) + s.Line(point1=(0.0, -mast_radius * 1.1), + point2=(0.0, mast_radius * 1.1)) + part_surf = model.Part(name=surface_name, dimensionality=THREE_D, + type=ANALYTIC_RIGID_SURFACE) + part_surf.AnalyticRigidSurfExtrude(sketch=s, + depth=mast_radius * 2.2) + + # create required sets and surfaces + # surface + part_surf.Surface(side1Faces=part_surf.faces, + name=surface_name) + + # longeron + edges = part_longerons.edges + vertices = part_longerons.vertices + + # individual sets + all_edges = [] + for i_vertex, long_pts in enumerate(longeron_points): + # get vertices and edges + selected_vertices = [vertices.findAt((pt,)) for pt in long_pts] + all_edges.append(EdgeArray([edges.findAt(pt) for pt in long_pts])) + # individual sets + long_name = 'LONGERON-{}'.format(i_vertex) + part_longerons.Set(edges=all_edges[-1], name=long_name) + # joints + for i_storey, vertex in enumerate(selected_vertices): + joint_name = 'JOINT-{}-{}'.format(i_storey, i_vertex) + part_longerons.Set(vertices=vertex, name=joint_name) + + name = 'ALL_LONGERONS' + part_longerons.Set(edges=all_edges, name=name) + name = 'ALL_LONGERONS_SURF' + part_longerons.Surface(circumEdges=all_edges, name=name) + + # joint sets + selected_vertices = [] + for i_storey in range(0, n_storeys + 1): + selected_vertices.append([]) + for i_vertex in range(0, n_longerons): + name = 'JOINT-{}-{}'.format(i_storey, i_vertex) + selected_vertices[-1].append(part_longerons.sets[name].vertices) + + name = 'BOTTOM_JOINTS' + part_longerons.Set(name=name, vertices=selected_vertices[0]) + name = 'TOP_JOINTS' + part_longerons.Set(name=name, vertices=selected_vertices[-1]) + name = 'ALL_JOINTS' + all_vertices = list(itertools.chain(*selected_vertices)) + part_longerons.Set(name=name, vertices=all_vertices) + + # create beam section + # create section material + material_name = 'LONGERON_MATERIAL' + nu = young_modulus / (2 * shear_modulus) - 1 + abaqusMaterial = model.Material(name=material_name) + abaqusMaterial.Elastic(type=ISOTROPIC, table=((young_modulus, nu),)) + + # create profile + profile_name = 'LONGERONS_PROFILE' + section_name = 'LONGERONS_SECTION' + + if circular: + r = d / 2. + model.CircularProfile(name=profile_name, r=r) + model.BeamSection( + consistentMassMatrix=False, integration=DURING_ANALYSIS, + material=material_name, name=section_name, + poissonRatio=0.31, profile=profile_name, + temperatureVar=LINEAR) + + if not circular: + model.GeneralizedProfile(name=profile_name, + area=area, i11=Ixx, + i12=0., i22=Iyy, j=J, gammaO=0., + gammaW=0.) + model.BeamSection( + name=section_name, integration=BEFORE_ANALYSIS, + beamShape=CONSTANT, profile=profile_name, thermalExpansion=OFF, + temperatureDependency=OFF, dependencies=0, + table=((young_modulus, shear_modulus),), + poissonRatio=.31, + alphaDamping=0.0, betaDamping=0.0, compositeDamping=0.0, + centroid=(0.0, 0.0), shearCenter=(0.0, 0.0), + consistentMassMatrix=False) + + # section assignment + part_longerons.SectionAssignment( + offset=0.0, offsetField='', offsetType=MIDDLE_SURFACE, + region=part_longerons.sets['ALL_LONGERONS'], + sectionName=section_name, thicknessAssignment=FROM_SECTION) + # section orientation + for i_vertex, pts in enumerate(longeron_points): + dir_vec_n1 = np.array(pts[0]) - (0., 0., 0.) + longeron_name = 'LONGERON-{}'.format(i_vertex) + region = part_longerons.sets[longeron_name] + part_longerons.assignBeamSectionOrientation( + region=region, method=N1_COSINES, n1=dir_vec_n1) + + # generate mesh + # seed part + mesh_size = min(mast_radius, pitch) / 300. + mesh_deviation_factor = .04 + mesh_min_size_factor = .001 + element_code = B31 + part_longerons.seedPart( + size=mesh_size, deviationFactor=mesh_deviation_factor, + minSizeFactor=mesh_min_size_factor, constraint=FINER) + # assign element type + elem_type_longerons = mesh.ElemType(elemCode=element_code) + part_longerons.setElementType(regions=(part_longerons.edges,), + elemTypes=(elem_type_longerons,)) + # generate mesh + part_longerons.generateMesh() + + # create instances + modelAssembly = model.rootAssembly + part_surf = model.parts[surface_name] + modelAssembly.Instance(name=longerons_name, + part=part_longerons, dependent=ON) + modelAssembly.Instance(name=surface_name, + part=part_surf, dependent=ON) + # rotate surface + modelAssembly.rotate(instanceList=(surface_name, ), + axisPoint=(0., 0., 0.), + axisDirection=(0., 1., 0.), angle=90.) + + # create reference points for boundary conditions + ref_point_positions = ['BOTTOM', 'TOP'] + for i, position in enumerate(ref_point_positions): + sign = 1 if i else -1 + rp = modelAssembly.ReferencePoint( + point=( + 0., 0., i * mast_height + sign * 1.1 * mast_radius)) + modelAssembly.Set( + referencePoints=(modelAssembly.referencePoints[rp.id],), + name='Z{}_REF_POINT'.format(position)) + + # add constraints for loading + instance_longerons = modelAssembly.instances[longerons_name] + instance_surf = modelAssembly.instances[surface_name] + ref_points = [modelAssembly.sets['Z{}_REF_POINT'.format(position)] + for position in ref_point_positions] + + # bottom point and analytic surface + surf = instance_surf.surfaces[surface_name] + model.RigidBody( + 'CONSTRAINT-RIGID_BODY-BOTTOM', refPointRegion=ref_points[0], + surfaceRegion=surf) + + # create local datums + datums = [] + for i_vertex in range(0, n_longerons): + origin = joints[0, i_vertex, :] + point2 = joints[0, i_vertex - 1, :] + name = 'LOCAL_DATUM_{}'.format(i_vertex) + datums.append(part_longerons.DatumCsysByThreePoints( + origin=origin, point2=point2, name=name, coordSysType=CARTESIAN, + point1=(0.0, 0.0, 0.0))) + + # create coupling constraints + for i_vertex in range(n_longerons): + datum = instance_longerons.datums[datums[i_vertex].id] + for i, i_storey in enumerate([0, n_storeys]): + joint_name = 'JOINT-{}-{}'.format(i_storey, i_vertex) + slave_region = instance_longerons.sets[joint_name] + master_region = ref_points[i] + constraint_name = 'CONSTRAINT-%s-%i-%i' % ( + 'Z{}_REF_POINT'.format(ref_point_positions[i]), + i_storey, i_vertex) + model.Coupling(name=constraint_name, controlPoint=master_region, + surface=slave_region, influenceRadius=WHOLE_SURFACE, + couplingType=KINEMATIC, localCsys=datum, u1=ON, + u2=ON, u3=ON, ur1=OFF, ur2=ON, ur3=ON) + + # from now on, there's differences between linear buckle and riks + + # create step + step_name = 'BUCKLE_STEP' + model.BuckleStep(step_name, numEigen=20, previous='Initial', minEigen=0.) + + # set bcs (displacement) + region_name = 'Z{}_REF_POINT'.format(ref_point_positions[0]) + loaded_region = modelAssembly.sets[region_name] + model.DisplacementBC('BC_FIX', createStepName=step_name, + region=loaded_region, u1=0., u2=0., u3=0., + ur1=0., ur2=0., ur3=0., buckleCase=BUCKLING_MODES) + + # set bcs (load) + applied_load = -1. + region_name = 'Z{}_REF_POINT'.format(ref_point_positions[-1]) + loaded_region = modelAssembly.sets[region_name] + model.ConcentratedForce('APPLIED_FORCE', createStepName=step_name, + region=loaded_region, cf3=applied_load) + + # create provisory inp + modelJob = mdb.Job(model=model_name, name=str(job_number)) + modelJob.writeInput(consistencyChecking=OFF) + + # ask for node file + with open('{}.inp'.format(job_number), 'r') as file: + lines = file.readlines() + + line_cmp = '** {}\n'.format('OUTPUT REQUESTS') + for i, line in reversed(list(enumerate(lines))): + if line == line_cmp: + break + + insert_line = i + 2 + for line in reversed(['*NODE FILE, frequency=1', 'U']): + lines.insert(insert_line, '{}\n'.format(line)) + + with open('{}.inp'.format(job_number), 'w') as file: + file.writelines(lines) + + # # create job + modelJob = mdb.JobFromInputFile(inputFileName='{}.inp'.format(job_number), + name=job_number) + # modelJob.submit(consistencyChecking=OFF) + # modelJob.waitForCompletion() diff --git a/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle_pp.py b/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle_pp.py new file mode 100644 index 00000000..9d160bc6 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/scripts/supercompressible_lin_buckle_pp.py @@ -0,0 +1,85 @@ +''' +Created on 2020-09-22 15:35:10 +Last modified on 2020-09-29 14:42:51 + +@author: L. F. Pereira (lfpereira@fe.up.pt)) +''' + +import pickle + +# imports +# from abaqus import session +import numpy as np + +# # variable initialization and odb opening + + +def main(odb): + step = odb.steps[odb.steps.keys()[-1]] + frames = step.frames + + # get maximum displacements + variable = 'UR' + directions = (1, 2, 3) + nodeSet = odb.rootAssembly.nodeSets[' ALL NODES'] + values = [] + for frame in frames: + varFieldOutputs = frame.fieldOutputs[variable] + outputs = varFieldOutputs.getSubset(region=nodeSet).values + output_frame = [] + for direction in directions: + output_frame.append([output.data[direction - 1] + for output in outputs]) + values.append(output_frame) + + max_disps = [] + for value in values: + max_disp = np.max(np.abs(np.array(value))) + max_disps.append(max_disp) + + # get loads + eigenvalues = [float(frame.description.split('EigenValue =')[1]) + for frame in list(frames)[1:]] + + # is coilable + # get top ref point info + ztop_set_name = 'ZTOP_REF_POINT' + nodeSet = odb.rootAssembly.nodeSets[ztop_set_name] + + # get info + directions = (3,) + variable = 'UR' + ztop_ur = [] + for frame in list(frames)[1:]: + varFieldOutputs = frame.fieldOutputs[variable] + outputs = varFieldOutputs.getSubset(region=nodeSet).values + output_frame = [] + for direction in directions: + output_frame.append([output.data[direction - 1] + for output in outputs]) + ztop_ur.append(output_frame) + + directions = (1, 2,) + variable = 'U' + ztop_u = [] + for frame in list(frames)[1:]: + varFieldOutputs = frame.fieldOutputs[variable] + outputs = varFieldOutputs.getSubset(region=nodeSet).values + output_frame = [] + for direction in directions: + output_frame.append( + [output.data[direction - 1] for output in outputs]) + ztop_u.append(output_frame) + + ur = ztop_ur[0] + u = ztop_u[0] + coilable = int(abs(ur[0][0]) > 1.0e-4 and abs(u[0][0]) + < 1.0e-4 and abs(u[1][0]) < 1.0e-4) + + buckling_results = {'max_disps': np.array(max_disps), + 'loads': np.array(eigenvalues), + 'coilable': coilable, + 'lin_buckle_odb': odb.name} + + with open('results.pkl', 'wb') as file: + pickle.dump(buckling_results, file) diff --git a/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks.py b/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks.py new file mode 100644 index 00000000..6bae3302 --- /dev/null +++ b/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks.py @@ -0,0 +1,388 @@ +# imports + +from __future__ import division + +# standard library +import itertools + +import mesh +# third-party +import numpy as np +from abaqus import backwardCompatibility, mdb +from abaqusConstants import (ANALYTIC_RIGID_SURFACE, B31, BEFORE_ANALYSIS, + BUCKLING_MODES, CARTESIAN, CONSTANT, + DEFORMABLE_BODY, DURING_ANALYSIS, FINER, FINITE, + FROM_SECTION, HARD, IMPRINT, ISOTROPIC, KINEMATIC, + LINEAR, MIDDLE_SURFACE, N1_COSINES, NONE, OFF, + OMIT, ON, THREE_D, WHOLE_SURFACE) +# abaqus +from caeModules import * # allow noGui # NOQA +from part import EdgeArray + + +def main(dict): # = 'lin_buckle'): + model_name = 'SUPERCOMPRESSIBLE_RIKS' + job_number = 'SUPERCOMPRESSIBLE_RIKS' # int(dict['job_number']) + + # Flag + circular = dict['circular'] # True or False + + # Constant parameters DoE + n_longerons = int(dict['n_longerons']) + young_modulus = dict['young_modulus'] + bottom_diameter = dict['bottom_diameter'] + + # Variables from DoE + ratio_pitch = dict['ratio_pitch'] + ratio_top_diameter = dict['ratio_top_diameter'] + ratio_shear_modulus = dict['ratio_shear_modulus'] + + # Output from linear buckling analysis + # coilable = dict['coilable'] + lin_bckl_max_disp = dict['max_disps'] + lin_buckle_odb = dict['lin_buckle_odb'] + + # Imperfection + imperfection = dict['imperfection'] + + if circular: + ratio_d = dict['ratio_d'] + d = ratio_d * bottom_diameter + + # 7D + if not circular: + ratio_area = dict['ratio_area'] + ratio_Ixx = dict['ratio_Ixx'] + ratio_Iyy = dict['ratio_Iyy'] + ratio_J = dict['ratio_J'] + # initialization + area = ratio_area * bottom_diameter ** 2 + Ixx = ratio_Ixx * bottom_diameter ** 4 + Iyy = ratio_Iyy * bottom_diameter ** 4 + J = ratio_J * bottom_diameter ** 4 + + # variables from ratios + shear_modulus = ratio_shear_modulus * young_modulus + pitch = ratio_pitch * bottom_diameter + top_diameter = bottom_diameter * (1 - ratio_top_diameter) + + # variables with defaults + n_storeys = 1 + twist_angle = 0. + transition_length_ratio = 1. + + # compute variables + mast_radius = bottom_diameter / 2. + mast_height = n_storeys * pitch + cone_slope = (bottom_diameter - top_diameter) / bottom_diameter + + # create abaqus model + model = mdb.Model(name=model_name) + backwardCompatibility.setValues(reportDeprecated=False) + if 'Model-1' in mdb.models.keys(): + del mdb.models['Model-1'] + + # create joints + joints = np.zeros((n_storeys + 1, n_longerons, 3)) + for i_storey in range(0, n_storeys + 1, 1): + zcoord = mast_height / n_storeys * i_storey + aux1 = 2.0 * np.pi / n_longerons + aux2 = twist_angle * min( + zcoord / mast_height / transition_length_ratio, 1.0) + for i_vertex in range(0, n_longerons): + aux3 = aux1 * i_vertex + aux2 + xcoord = mast_radius * np.cos(aux3) + ycoord = mast_radius * np.sin(aux3) + joints[i_storey, i_vertex, :] = ( + xcoord * ( + 1.0 - min(zcoord, transition_length_ratio * mast_height + ) / mast_height * cone_slope), ycoord * ( + 1.0 - min(zcoord, transition_length_ratio * mast_height + ) / mast_height * cone_slope), zcoord) + + # create geometry longerons + longerons_name = 'LONGERONS' + part_longerons = model.Part(longerons_name, dimensionality=THREE_D, + type=DEFORMABLE_BODY) + longeron_points = [] + for i_vertex in range(0, n_longerons): + # get required points + longeron_points.append([joints[i_storey, i_vertex, :] + for i_storey in range(0, n_storeys + 1)]) + # create wires + part_longerons.WirePolyLine(points=longeron_points[-1], + mergeType=IMPRINT, meshable=ON) + + # create surface + surface_name = 'ANALYTICAL_SURF' + s = model.ConstrainedSketch(name='SURFACE_SKETCH', + sheetSize=mast_radius * 3.0) + s.Line(point1=(0.0, -mast_radius * 1.1), + point2=(0.0, mast_radius * 1.1)) + part_surf = model.Part(name=surface_name, dimensionality=THREE_D, + type=ANALYTIC_RIGID_SURFACE) + part_surf.AnalyticRigidSurfExtrude(sketch=s, + depth=mast_radius * 2.2) + + # create required sets and surfaces + # surface + part_surf.Surface(side1Faces=part_surf.faces, + name=surface_name) + + # longeron + edges = part_longerons.edges + vertices = part_longerons.vertices + + # individual sets + all_edges = [] + for i_vertex, long_pts in enumerate(longeron_points): + # get vertices and edges + selected_vertices = [vertices.findAt((pt,)) for pt in long_pts] + all_edges.append(EdgeArray([edges.findAt(pt) for pt in long_pts])) + # individual sets + long_name = 'LONGERON-{}'.format(i_vertex) + part_longerons.Set(edges=all_edges[-1], name=long_name) + # joints + for i_storey, vertex in enumerate(selected_vertices): + joint_name = 'JOINT-{}-{}'.format(i_storey, i_vertex) + part_longerons.Set(vertices=vertex, name=joint_name) + + name = 'ALL_LONGERONS' + part_longerons.Set(edges=all_edges, name=name) + name = 'ALL_LONGERONS_SURF' + part_longerons.Surface(circumEdges=all_edges, name=name) + + # joint sets + selected_vertices = [] + for i_storey in range(0, n_storeys + 1): + selected_vertices.append([]) + for i_vertex in range(0, n_longerons): + name = 'JOINT-{}-{}'.format(i_storey, i_vertex) + selected_vertices[-1].append(part_longerons.sets[name].vertices) + + name = 'BOTTOM_JOINTS' + part_longerons.Set(name=name, vertices=selected_vertices[0]) + name = 'TOP_JOINTS' + part_longerons.Set(name=name, vertices=selected_vertices[-1]) + name = 'ALL_JOINTS' + all_vertices = list(itertools.chain(*selected_vertices)) + part_longerons.Set(name=name, vertices=all_vertices) + + # create beam section + # create section material + material_name = 'LONGERON_MATERIAL' + nu = young_modulus / (2 * shear_modulus) - 1 + abaqusMaterial = model.Material(name=material_name) + abaqusMaterial.Elastic(type=ISOTROPIC, table=((young_modulus, nu),)) + + # create profile + profile_name = 'LONGERONS_PROFILE' + section_name = 'LONGERONS_SECTION' + + if circular: + # 3D simplified model + r = d / 2. + model.CircularProfile(name=profile_name, r=r) + # create profile + model.BeamSection( + consistentMassMatrix=False, integration=DURING_ANALYSIS, + material=material_name, name=section_name, + poissonRatio=0.31, profile=profile_name, + temperatureVar=LINEAR) + + if not circular: + # 7D full model + # create profile + model.GeneralizedProfile(name=profile_name, + area=area, i11=Ixx, + i12=0., i22=Iyy, j=J, gammaO=0., + gammaW=0.) + # create section + model.BeamSection( + name=section_name, integration=BEFORE_ANALYSIS, + beamShape=CONSTANT, profile=profile_name, thermalExpansion=OFF, + temperatureDependency=OFF, dependencies=0, + table=((young_modulus, shear_modulus),), + poissonRatio=.31, + alphaDamping=0.0, betaDamping=0.0, compositeDamping=0.0, + centroid=(0.0, 0.0), shearCenter=(0.0, 0.0), + consistentMassMatrix=False) + + # section assignment + part_longerons.SectionAssignment( + offset=0.0, offsetField='', offsetType=MIDDLE_SURFACE, + region=part_longerons.sets['ALL_LONGERONS'], + sectionName=section_name, thicknessAssignment=FROM_SECTION) + # section orientation + for i_vertex, pts in enumerate(longeron_points): + dir_vec_n1 = np.array(pts[0]) - (0., 0., 0.) + longeron_name = 'LONGERON-{}'.format(i_vertex) + region = part_longerons.sets[longeron_name] + part_longerons.assignBeamSectionOrientation( + region=region, method=N1_COSINES, n1=dir_vec_n1) + + # generate mesh + # seed part + mesh_size = min(mast_radius, pitch) / 300. + mesh_deviation_factor = .04 + mesh_min_size_factor = .001 + element_code = B31 + part_longerons.seedPart( + size=mesh_size, deviationFactor=mesh_deviation_factor, + minSizeFactor=mesh_min_size_factor, constraint=FINER) + # assign element type + elem_type_longerons = mesh.ElemType(elemCode=element_code) + part_longerons.setElementType(regions=(part_longerons.edges,), + elemTypes=(elem_type_longerons,)) + # generate mesh + part_longerons.generateMesh() + + # create instances + modelAssembly = model.rootAssembly + part_surf = model.parts[surface_name] + modelAssembly.Instance(name=longerons_name, + part=part_longerons, dependent=ON) + modelAssembly.Instance(name=surface_name, + part=part_surf, dependent=ON) + # rotate surface + modelAssembly.rotate(instanceList=(surface_name, ), + axisPoint=(0., 0., 0.), + axisDirection=(0., 1., 0.), angle=90.) + + # create reference points for boundary conditions + ref_point_positions = ['BOTTOM', 'TOP'] + for i, position in enumerate(ref_point_positions): + sign = 1 if i else -1 + rp = modelAssembly.ReferencePoint( + point=(0., 0., i * mast_height + sign * 1.1 * mast_radius)) + modelAssembly.Set( + referencePoints=(modelAssembly.referencePoints[rp.id],), + name='Z{}_REF_POINT'.format(position)) + + # add constraints for loading + instance_longerons = modelAssembly.instances[longerons_name] + instance_surf = modelAssembly.instances[surface_name] + ref_points = [modelAssembly.sets['Z{}_REF_POINT'.format(position)] + for position in ref_point_positions] + + # bottom point and analytic surface + surf = instance_surf.surfaces[surface_name] + model.RigidBody( + 'CONSTRAINT-RIGID_BODY-BOTTOM', refPointRegion=ref_points[0], + surfaceRegion=surf) + + # create local datums + datums = [] + for i_vertex in range(0, n_longerons): + origin = joints[0, i_vertex, :] + point2 = joints[0, i_vertex - 1, :] + name = 'LOCAL_DATUM_{}'.format(i_vertex) + datums.append(part_longerons.DatumCsysByThreePoints( + origin=origin, point2=point2, name=name, coordSysType=CARTESIAN, + point1=(0.0, 0.0, 0.0))) + + # create coupling constraints + for i_vertex in range(n_longerons): + datum = instance_longerons.datums[datums[i_vertex].id] + for i, i_storey in enumerate([0, n_storeys]): + joint_name = 'JOINT-{}-{}'.format(i_storey, i_vertex) + slave_region = instance_longerons.sets[joint_name] + master_region = ref_points[i] + constraint_name = 'CONSTRAINT-%s-%i-%i' % ( + 'Z{}_REF_POINT'.format(ref_point_positions[i]), + i_storey, i_vertex) + model.Coupling(name=constraint_name, controlPoint=master_region, + surface=slave_region, influenceRadius=WHOLE_SURFACE, + couplingType=KINEMATIC, localCsys=datum, u1=ON, + u2=ON, u3=ON, ur1=OFF, ur2=ON, ur3=ON) + + # from now on, there's differences between linear buckle and riks + + # create step + step_name = 'RIKS_STEP' + model.StaticRiksStep(step_name, nlgeom=ON, maxNumInc=400, + initialArcInc=5e-2, maxArcInc=0.5, previous='Initial') + + # set bcs (displacement) - shared with linear buckling + region_name = 'Z{}_REF_POINT'.format(ref_point_positions[0]) + loaded_region = modelAssembly.sets[region_name] + model.DisplacementBC('BC_FIX', createStepName=step_name, + region=loaded_region, u1=0., u2=0., u3=0., + ur1=0., ur2=0., ur3=0., buckleCase=BUCKLING_MODES) + + # set bcs (displacement) + vert_disp = - pitch + region_name = 'Z{}_REF_POINT'.format(ref_point_positions[-1]) + loaded_region = modelAssembly.sets[region_name] + model.DisplacementBC('DISPLACEMENT', createStepName=step_name, + region=loaded_region, u3=vert_disp, + buckleCase=BUCKLING_MODES) + + # set contact between longerons + # add contact properties + # contact property + contact = model.ContactProperty('IMP_TARG') + # contact behaviour + contact.NormalBehavior(allowSeparation=OFF, pressureOverclosure=HARD) + contact.GeometricProperties(contactArea=1., padThickness=None) + # create interaction + master = modelAssembly.instances[surface_name].surfaces[surface_name] + slave = modelAssembly.instances[ + longerons_name].surfaces['ALL_LONGERONS_SURF'] + # model.SurfaceToSurfaceContactStd(name='IMP_TARG', + # createStepName='Initial', master=master, + # slave=slave, sliding=FINITE, + # interactionProperty=contact.name, thickness=OFF) + + # model.SurfaceToSurfaceContactStd( + # name='IMP_TARG', createStepName='Initial', master=master, + # slave=slave, sliding=FINITE, interactionProperty=contact.name, + # thickness=OFF) + model.SurfaceToSurfaceContactStd( + name='IMP_TARG', adjustMethod=NONE, clearanceRegion=None, + createStepName='Initial', + datumAxis=None, initialClearance=OMIT, + interactionProperty=contact.name, main=master, + secondary=slave, sliding=FINITE, thickness=OFF) + + # outputs + # energy outputs + model.HistoryOutputRequest( + name='ENERGIES', createStepName=step_name, variables=('ALLEN',)) + # load-disp outputs + position = ref_point_positions[-1] + region = model.rootAssembly.sets['Z{}_REF_POINT'.format(position)] + model.HistoryOutputRequest( + name='RP_{}'.format(position), createStepName=step_name, + region=region, variables=('U', 'RF')) + + # create provisory inp + modelJob = mdb.Job(model=model_name, name=str(job_number)) + modelJob.writeInput(consistencyChecking=OFF) + + # add imperfections to inp + # previous_model_results['max_disps'][1] + amp_factor = imperfection / lin_bckl_max_disp[1] + # TODO: deal with previous_model_job_name + text = ['*IMPERFECTION, FILE={}, STEP=1'.format(lin_buckle_odb), + '{}, {}'.format(1, amp_factor)] + with open('{}.inp'.format(job_number), 'r') as file: + lines = file.readlines() + + line_cmp = '** {}\n'.format('INTERACTIONS') + for i, line in reversed(list(enumerate(lines))): + if line == line_cmp: + break + + insert_line = i + 2 + for line in reversed(text): + lines.insert(insert_line, '{}\n'.format(line)) + + with open('{}.inp'.format(job_number), 'w') as file: + file.writelines(lines) + + # # create job + # modelJob = mdb.JobFromInputFile(inputFileName='{}.inp'.format(job_name), + # name=job_name) + # modelJob.submit(consistencyChecking=OFF) + # modelJob.waitForCompletion() diff --git a/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks_pp.py b/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks_pp.py new file mode 100644 index 00000000..f3099f4a --- /dev/null +++ b/studies/fragile_becomes_supercompressible/scripts/supercompressible_riks_pp.py @@ -0,0 +1,68 @@ +''' +Created on 2020-09-22 16:07:04 +Last modified on 2020-09-23 07:10:39 + +@author: L. F. Pereira (lfpereira@fe.up.pt)) +''' + +import pickle + +import numpy as np +# imports +from abaqus import session # NOQA + +# # variable initialization and odb opening +# job_name = 'Simul_SUPERCOMPRESSIBLE_RIKS' +# odb_name = '{}.odb'.format(job_name) +# odb = session.openOdb(name=odb_name) + + +def main(odb): + # variable initialization and odb opening + # odb_name = '{}.odb'.format(job_name) + # odb = session.openOdb(name=odb_name) + + riks_results = {} + + # reference point data + variables = ['U', 'UR', 'RF', 'RM'] + set_name = 'ZTOP_REF_POINT' + step_name = 'RIKS_STEP' + step = odb.steps[step_name] + directions = (1, 2, 3) + nodes = odb.rootAssembly.nodeSets[set_name].nodes[0] + # get variables + for variable in variables: + y = [] + for node in nodes: + instance_name = node.instanceName if \ + node.instanceName else 'ASSEMBLY' + name = 'Node ' + instance_name + '.' + str(node.label) + historyOutputs = step.historyRegions[name].historyOutputs + node_data = [] + for direction in directions: + node_data.append( + [data[1] for data in historyOutputs['%s%i' % ( + variable, direction)].data]) + y.append(node_data) + riks_results[variable] = np.array(y[0]) + + # # deformation + # frames = step.frames + # nodeSet = odb.rootAssembly.elementSets[' ALL ELEMENTS'] + # directions = (1, 3,) + # variable = 'E' + # values = [] + # for frame in frames: + # varFieldOutputs = frame.fieldOutputs[variable] + # outputs = varFieldOutputs.getSubset(region=nodeSet).values + # output_frame = [] + # for direction in directions: + # output_frame.append([output.data[direction - 1] + # for output in outputs]) + # values.append(output_frame) + + # riks_results[variable] = np.array(values) + + with open('results.pkl', 'wb') as file: + pickle.dump(riks_results, file) diff --git a/tests/datageneration/test_datagenerator.py b/tests/datageneration/test_datagenerator.py index dadd361b..beb2bd08 100644 --- a/tests/datageneration/test_datagenerator.py +++ b/tests/datageneration/test_datagenerator.py @@ -10,7 +10,7 @@ def test_convert_function( experiment_data: ExperimentData, function_1: Callable): - data_generator = convert_function(f=function_1, input=['x'], output=[ + data_generator = convert_function(f=function_1, output=[ 'y0', 'y1'], kwargs={'s': 103}) assert isinstance(data_generator, DataGenerator) @@ -20,7 +20,7 @@ def test_convert_function( def test_convert_function2( experiment_data: ExperimentData, function_2: Callable): - data_generator = convert_function(f=function_2, input=['x'], output=[ + data_generator = convert_function(f=function_2, output=[ 'y0', 'y1']) assert isinstance(data_generator, DataGenerator) diff --git a/tests/design/test_designofexperiments.py b/tests/design/test_designofexperiments.py index 00a9b1cb..8641b6f7 100644 --- a/tests/design/test_designofexperiments.py +++ b/tests/design/test_designofexperiments.py @@ -21,30 +21,30 @@ def test_correct_doe(doe): def test_get_continuous_parameters(doe: Domain): design = {'x1': _ContinuousParameter(lower_bound=2.4, upper_bound=10.3), 'x3': _ContinuousParameter(lower_bound=10.0, upper_bound=380.3)} - assert doe.get_continuous_parameters() == design + assert doe.continuous.space == design def test_get_discrete_parameters(doe: Domain): design = {'x2': _DiscreteParameter(lower_bound=5, upper_bound=80), 'x5': _DiscreteParameter(lower_bound=2, upper_bound=3)} - assert doe.get_discrete_parameters() == design + assert doe.discrete.space == design def test_get_categorical_parameters(doe: Domain): - assert doe.get_categorical_parameters() == {'x4': _CategoricalParameter( + assert doe.categorical.space == {'x4': _CategoricalParameter( categories=["test1", "test2", "test3"])} def test_get_continuous_names(doe: Domain): - assert doe.get_continuous_names() == ["x1", "x3"] + assert doe.continuous.names == ["x1", "x3"] def test_get_discrete_names(doe: Domain): - assert doe.get_discrete_names() == ["x2", "x5"] + assert doe.discrete.names == ["x2", "x5"] def test_get_categorical_names(doe: Domain): - assert doe.get_categorical_names() == ["x4"] + assert doe.categorical.names == ["x4"] def test_add_arbitrary_list_as_categorical_parameter(): diff --git a/tests/design/test_space.py b/tests/design/test_space.py index 76e7850b..47df8f37 100644 --- a/tests/design/test_space.py +++ b/tests/design/test_space.py @@ -177,5 +177,34 @@ def test_add_combination(args): assert a + b == expected +def test_to_discrete(): + a = _ContinuousParameter(0., 5.) + c = _DiscreteParameter(0, 5, 0.2) + b = a.to_discrete(0.2) + assert isinstance(b, _DiscreteParameter) + assert b.lower_bound == 0 + assert b.upper_bound == 5 + assert b.step == 0.2 + assert b == c + + +def test_to_discrete_negative_stepsize(): + a = _ContinuousParameter(0., 5.) + with pytest.raises(ValueError): + a.to_discrete(-0.2) + + +def test_default_stepsize_to_discrete(): + default_stepsize = 1 + a = _ContinuousParameter(0., 5.) + c = _DiscreteParameter(0, 5, default_stepsize) + b = a.to_discrete() + assert isinstance(b, _DiscreteParameter) + assert b.lower_bound == 0 + assert b.upper_bound == 5 + assert b.step == default_stepsize + assert b == c + + if __name__ == "__main__": # pragma: no cover pytest.main() diff --git a/tests/experimentdata/conftest.py b/tests/experimentdata/conftest.py index 948ff792..b6d59d67 100644 --- a/tests/experimentdata/conftest.py +++ b/tests/experimentdata/conftest.py @@ -97,8 +97,8 @@ def experimentdata_expected_only_domain() -> ExperimentData: @pytest.fixture(scope="package") def numpy_array(domain_continuous: Domain) -> np.ndarray: - np.random.seed(SEED) - return np.random.rand(10, len(domain_continuous)) + rng = np.random.default_rng(SEED) + return rng.random((10, len(domain_continuous))) @pytest.fixture(scope="package") @@ -108,8 +108,9 @@ def numpy_output_array(domain_continuous: Domain) -> np.ndarray: @pytest.fixture(scope="package") def xarray_dataset(domain_continuous: Domain) -> xr.Dataset: - np.random.seed(SEED) - input_data = np.random.rand(10, len(domain_continuous)) + rng = np.random.default_rng(SEED) + # np.random.seed(SEED) + input_data = rng.random((10, len(domain_continuous))) input_names = domain_continuous.names output_data = pd.DataFrame() @@ -123,8 +124,9 @@ def xarray_dataset(domain_continuous: Domain) -> xr.Dataset: @pytest.fixture(scope="package") def pandas_dataframe(domain_continuous: Domain) -> pd.DataFrame: - np.random.seed(SEED) - return pd.DataFrame(np.random.rand(10, len(domain_continuous)), columns=domain_continuous.names) + # np.random.seed(SEED) + rng = np.random.default_rng(SEED) + return pd.DataFrame(rng.random((10, len(domain_continuous))), columns=domain_continuous.names) @pytest.fixture(scope="package") diff --git a/tests/experimentdata/test_experimentdata.py b/tests/experimentdata/test_experimentdata.py index 10142ffd..582aa401 100644 --- a/tests/experimentdata/test_experimentdata.py +++ b/tests/experimentdata/test_experimentdata.py @@ -184,16 +184,16 @@ def test_set_error(experimentdata_continuous: ExperimentData): def create_sample_csv_input(file_path): data = [ ["x0", "x1", "x2"], - [0.374540, 0.950714, 0.731994], - [0.598658, 0.156019, 0.155995], - [0.058084, 0.866176, 0.601115], - [0.708073, 0.020584, 0.969910], - [0.832443, 0.212339, 0.181825], - [0.183405, 0.304242, 0.524756], - [0.431945, 0.291229, 0.611853], - [0.139494, 0.292145, 0.366362], - [0.456070, 0.785176, 0.199674], - [0.514234, 0.592415, 0.046450], + [0.77395605, 0.43887844, 0.85859792], + [0.69736803, 0.09417735, 0.97562235], + [0.7611397, 0.78606431, 0.12811363], + [0.45038594, 0.37079802, 0.92676499], + [0.64386512, 0.82276161, 0.4434142], + [0.22723872, 0.55458479, 0.06381726], + [0.82763117, 0.6316644, 0.75808774], + [0.35452597, 0.97069802, 0.89312112], + [0.7783835, 0.19463871, 0.466721], + [0.04380377, 0.15428949, 0.68304895], [0.000000, 0.000000, 0.000000], [1.000000, 1.000000, 1.000000], ] @@ -316,16 +316,16 @@ def str_output(tmp_path: Path): def numpy_input(): return np.array([ - [0.374540, 0.950714, 0.731994], - [0.598658, 0.156019, 0.155995], - [0.058084, 0.866176, 0.601115], - [0.708073, 0.020584, 0.969910], - [0.832443, 0.212339, 0.181825], - [0.183405, 0.304242, 0.524756], - [0.431945, 0.291229, 0.611853], - [0.139494, 0.292145, 0.366362], - [0.456070, 0.785176, 0.199674], - [0.514234, 0.592415, 0.046450], + [0.77395605, 0.43887844, 0.85859792], + [0.69736803, 0.09417735, 0.97562235], + [0.7611397, 0.78606431, 0.12811363], + [0.45038594, 0.37079802, 0.92676499], + [0.64386512, 0.82276161, 0.4434142], + [0.22723872, 0.55458479, 0.06381726], + [0.82763117, 0.6316644, 0.75808774], + [0.35452597, 0.97069802, 0.89312112], + [0.7783835, 0.19463871, 0.466721], + [0.04380377, 0.15428949, 0.68304895], [0.000000, 0.000000, 0.000000], [1.000000, 1.000000, 1.000000], ]) @@ -351,16 +351,16 @@ def numpy_output(): def pd_input(): return pd.DataFrame([ - [0.374540, 0.950714, 0.731994], - [0.598658, 0.156019, 0.155995], - [0.058084, 0.866176, 0.601115], - [0.708073, 0.020584, 0.969910], - [0.832443, 0.212339, 0.181825], - [0.183405, 0.304242, 0.524756], - [0.431945, 0.291229, 0.611853], - [0.139494, 0.292145, 0.366362], - [0.456070, 0.785176, 0.199674], - [0.514234, 0.592415, 0.046450], + [0.77395605, 0.43887844, 0.85859792], + [0.69736803, 0.09417735, 0.97562235], + [0.7611397, 0.78606431, 0.12811363], + [0.45038594, 0.37079802, 0.92676499], + [0.64386512, 0.82276161, 0.4434142], + [0.22723872, 0.55458479, 0.06381726], + [0.82763117, 0.6316644, 0.75808774], + [0.35452597, 0.97069802, 0.89312112], + [0.7783835, 0.19463871, 0.466721], + [0.04380377, 0.15428949, 0.68304895], [0.000000, 0.000000, 0.000000], [1.000000, 1.000000, 1.000000], ], columns=["x0", "x1", "x2"]) diff --git a/tests/sampling/test_sampling.py b/tests/sampling/test_sampling.py index 42e2b19c..419e8169 100644 --- a/tests/sampling/test_sampling.py +++ b/tests/sampling/test_sampling.py @@ -26,15 +26,13 @@ def test_correct_sampling_ran(design3: Domain): seed = 42 numsamples = 5 - ground_truth_samples = np.array( - [ - [5.358867, 25, 362.049508, "test1", 5.504359], - [7.129402, 37, 67.773703, "test1", 1.645163], - [2.858861, 80, 330.745027, "test1", 4.627471], - [7.993773, 62, 17.622438, "test3", 7.098396], - [8.976297, 26, 88.629173, "test3", 1.818227], - ] - ) + ground_truth_samples = np.array([ + [8.514253, 11, 172.516686, 'test1', 6.352606], + [7.909207, 63, 44.873872, 'test3', 7.13667], + [8.413004, 54, 301.079612, 'test2', 1.458361], + [5.958049, 38, 147.306508, 'test2', 6.809325], + [7.486534, 37, 314.668625, 'test2', 3.570875] + ]) df_ground_truth = pd.DataFrame(data=ground_truth_samples) df_ground_truth = df_ground_truth.astype( @@ -65,11 +63,11 @@ def test_correct_sampling_sobol(design3: Domain): ground_truth_samples = np.array( [ - [2.4000, 56, 10.0000, "test3", 0.6000], - [6.3500, 19, 195.1500, "test2", 3.9500], - [8.3250, 76, 102.5750, "test3", 2.2750], - [4.3750, 65, 287.7250, "test3", 5.6250], - [5.3625, 25, 148.8625, "test3", 4.7875], + [2.4000, 11, 10.0000, "test1", 0.6000], + [6.3500, 63, 195.1500, "test3", 3.9500], + [8.3250, 54, 102.5750, "test2", 2.2750], + [4.3750, 38, 287.7250, "test2", 5.6250], + [5.3625, 37, 148.8625, "test2", 4.7875], ] ) @@ -99,11 +97,11 @@ def test_correct_sampling_lhs(design3: Domain): ground_truth_samples = np.array( [ - [8.258755, 64, 95.614741, "test2", 1.580872], - [5.651772, 19, 222.269005, "test3", 2.149033], - [4.925880, 66, 80.409902, "test3", 5.919679], - [2.991773, 66, 233.704488, "test1", 6.203645], - [10.035259, 51, 321.965835, "test3", 4.085494], + [8.258755, 11, 95.614741, "test1", 1.580872], + [5.651772, 63, 222.269005, "test3", 2.149033], + [4.925880, 54, 80.409902, "test2", 5.919679], + [2.991773, 38, 233.704488, "test2", 6.203645], + [10.035259, 37, 321.965835, "test2", 4.085494], ] ) diff --git a/tests/sampling/test_sampling_categorical.py b/tests/sampling/test_sampling_categorical.py index 41348a99..91cdf6e0 100644 --- a/tests/sampling/test_sampling_categorical.py +++ b/tests/sampling/test_sampling_categorical.py @@ -1,54 +1,56 @@ -import numpy as np -import pytest +# import numpy as np +# import pytest -from f3dasm._src.design.samplers import RandomUniform +# from f3dasm._src.experimentdata.samplers import RandomUniform, randomuniform -pytestmark = pytest.mark.smoke +# pytestmark = pytest.mark.smoke -def test_correct_discrete_sampling_1(design): - seed = 42 +# def test_correct_discrete_sampling_1(design): +# seed = 42 - # Construct sampler - random_uniform = RandomUniform(domain=design, seed=seed) +# numsamples = 5 +# # Construct sampler +# random_uniform = randomuniform( +# domain=design, seed=seed, n_samples=numsamples) - numsamples = 5 +# random_uniform = random_uniform.data.to_numpy() - ground_truth_samples = np.array( - [ - ["test3", "material1"], - ["test1", "material3"], - ["test3", "material2"], - ["test3", "material3"], - ["test1", "material3"], - ] - ) - samples = random_uniform._sample_categorical(numsamples=numsamples) +# ground_truth_samples = np.array( +# [ +# ["test3", "material1"], +# ["test1", "material3"], +# ["test3", "material2"], +# ["test3", "material3"], +# ["test1", "material3"], +# ] +# ) +# # samples = random_uniform._sample_categorical(numsamples=numsamples) - assert (samples == ground_truth_samples).all() +# assert random_uniform == ground_truth_samples -def test_correct_discrete_sampling_2(design2): - seed = 42 +# def test_correct_discrete_sampling_2(design2): +# seed = 42 - # Construct sampler - random_uniform = RandomUniform(domain=design2, seed=seed) +# # Construct sampler +# random_uniform = RandomUniform(domain=design2, seed=seed) - numsamples = 5 +# numsamples = 5 - ground_truth_samples = np.array( - [ - ["main", "test51", "material6"], - ["main", "test14", "material18"], - ["main", "test71", "material10"], - ["main", "test60", "material10"], - ["main", "test20", "material3"], - ] - ) - samples = random_uniform._sample_categorical(numsamples=numsamples) +# ground_truth_samples = np.array( +# [ +# ["main", "test51", "material6"], +# ["main", "test14", "material18"], +# ["main", "test71", "material10"], +# ["main", "test60", "material10"], +# ["main", "test20", "material3"], +# ] +# ) +# samples = random_uniform._sample_categorical(numsamples=numsamples) - assert (samples == ground_truth_samples).all() +# assert (samples == ground_truth_samples).all() -if __name__ == "__main__": # pragma: no cover - pytest.main() +# if __name__ == "__main__": # pragma: no cover +# pytest.main() diff --git a/tests/sampling/test_sampling_continuous.py b/tests/sampling/test_sampling_continuous.py deleted file mode 100644 index 715ff6f3..00000000 --- a/tests/sampling/test_sampling_continuous.py +++ /dev/null @@ -1,81 +0,0 @@ -# # -*- coding: utf-8 -*- - -import numpy as np -import pytest - -from f3dasm._src.design.samplers import (LatinHypercube, RandomUniform, - SobolSequence) -from f3dasm.design import Domain - -pytestmark = pytest.mark.smoke - - -# Random Uniform Sampling - - -def test_correct_randomuniform_sampling(design3: Domain): - seed = 42 - - # Construct sampler - random_uniform = RandomUniform(domain=design3, seed=seed) - - numsamples = 5 - - ground_truth_samples = np.array( - [ - [5.35886694, 362.04950766, 5.50435941], - [7.12940203, 67.77370256, 1.64516329], - [2.85886054, 330.74502678, 4.62747058], - [7.99377336, 17.62243824, 7.09839601], - [8.97629686, 88.62917268, 1.81822728], - ] - ) - samples = random_uniform.sample_continuous(numsamples=numsamples) - - assert samples == pytest.approx(ground_truth_samples) - - -def test_correct_latinhypercube_sampling(design3: Domain): - seed = 42 - - # Construct sampler - latin_hypercube = LatinHypercube(domain=design3, seed=seed) - - numsamples = 5 - - ground_truth_samples = np.array( - [ - [8.25875467, 95.61474051, 1.58087188], - [5.65177211, 222.26900536, 2.14903266], - [4.92588041, 80.40990153, 5.9196792], - [2.99177339, 233.70448765, 6.20364546], - [10.03525937, 321.96583454, 4.08549412], - ] - ) - samples = latin_hypercube.sample_continuous(numsamples=numsamples) - assert samples == pytest.approx(ground_truth_samples) - - -def test_correct_sobolsequence_sampling(design3): - seed = 42 - - # Construct sampler - sobol_sequencing = SobolSequence(domain=design3, seed=seed) - - numsamples = 5 - - ground_truth_samples = np.array( - [ - [2.4, 10.0, 0.6], - [6.35, 195.15, 3.95], - [8.325, 102.575, 2.275], - [4.375, 287.725, 5.625], - [5.3625, 148.8625, 4.7875], - ] - ) - samples = sobol_sequencing.sample_continuous(numsamples=numsamples) - assert samples == pytest.approx(ground_truth_samples) - - -if __name__ == "__main__": # pragma: no cover - pytest.main() diff --git a/tests/sampling/test_sampling_discrete.py b/tests/sampling/test_sampling_discrete.py index 84635c03..2db08216 100644 --- a/tests/sampling/test_sampling_discrete.py +++ b/tests/sampling/test_sampling_discrete.py @@ -1,45 +1,46 @@ -import numpy as np -import pytest +# import numpy as np +# import pytest -from f3dasm._src.design.samplers import RandomUniform +# from f3dasm._src.experimentdata.samplers import RandomUniform -pytestmark = pytest.mark.smoke +# pytestmark = pytest.mark.smoke -def test_correct_discrete_sampling_1(design4): - seed = 42 +# def test_correct_discrete_sampling_1(design4): +# seed = 42 - # Construct sampler - random_uniform = RandomUniform(domain=design4, seed=seed) +# # Construct sampler +# random_uniform = RandomUniform(domain=design4, seed=seed) - numsamples = 5 +# numsamples = 5 - ground_truth_samples = np.array([[56, 5], [19, 4], [76, 5], [65, 5], [25, 5]]) - samples = random_uniform._sample_discrete(numsamples=numsamples) +# ground_truth_samples = np.array( +# [[56, 5], [19, 4], [76, 5], [65, 5], [25, 5]]) +# samples = random_uniform._sample_discrete(numsamples=numsamples) - assert samples == pytest.approx(ground_truth_samples) +# assert samples == pytest.approx(ground_truth_samples) -def test_correct_discrete_sampling_2(design5): - seed = 42 +# def test_correct_discrete_sampling_2(design5): +# seed = 42 - # Construct sampler - random_uniform = RandomUniform(domain=design5, seed=seed) +# # Construct sampler +# random_uniform = RandomUniform(domain=design5, seed=seed) - numsamples = 5 +# numsamples = 5 - ground_truth_samples = np.array( - [ - [56, 5, 510], - [19, 4, 523], - [76, 5, 523], - [65, 5, 502], - [25, 5, 521], - ] - ) - samples = random_uniform._sample_discrete(numsamples=numsamples) - assert samples == pytest.approx(ground_truth_samples) +# ground_truth_samples = np.array( +# [ +# [56, 5, 510], +# [19, 4, 523], +# [76, 5, 523], +# [65, 5, 502], +# [25, 5, 521], +# ] +# ) +# samples = random_uniform._sample_discrete(numsamples=numsamples) +# assert samples == pytest.approx(ground_truth_samples) -if __name__ == "__main__": # pragma: no cover - pytest.main() +# if __name__ == "__main__": # pragma: no cover +# pytest.main()