From 03c6acba7bcb99a0c11617521e8e733697ba1e56 Mon Sep 17 00:00:00 2001
From: Juncheng E
Date: Mon, 28 Feb 2022 12:13:27 +0100
Subject: [PATCH 01/10] New release 0.1.0 (#52)
* Fixing html footer.
* Adding more py versions to be supported.
* Update gitignore
egg files are now ignored.
* Added new ci file for github actions.
* Removed obsolet travis ci file.
* Fixing html footer.
* Adding more py versions to be supported.
* Update gitignore
egg files are now ignored.
* Added new ci file for github actions.
* Removed obsolet travis ci file.
* Carsten (#30)
* Fixing version issue in CI setup.
* Update ci file.
libpyvinyl was not installed into testing environment, this caused ImportError.
* Update gitignor
Ignore emacs swap files.
* Small cosmetic fixes.
Co-authored-by: Carsten Fortmann-Grote
* Add readthedocs.yaml
Configuration of basic rtd settings now live in this file as recommended by
readthedocs.
* Update readthedocs.yaml
Fixed ubuntu version
* Update readthedocs.yaml
Fixing path to rtd python conf file.
* Update readthedocs.yaml
Add global requirements and project to install step
* Update readthedocs.yaml
Trying to fix package install.
* Remove readthecods.yaml from git.
* Update refman.rst
Replace autoclass by automodule statements.
Include Collection and Instrument modules.
* Update authors.
* Update gitignore.
json dumps, h5 and tmp files produced by examples and tests are now ignored.
* Updated parameters container to Ordered_dict to preserve parameter order.
Moved value from an attribute to a property. In connection to this, I made the consequence of entering an illegal value an error instead of it just being ignored. This was chosen as the setter function is now hidden from the user, so they wouldnt necessarily know a check is being made behind the scences.
Updated tests to account for removal of set_value function.
* using json_tricks in order to support dump and load of numpy arrays in json
* instructions to run tests: suggested pytest package for developers
* more tests for the Parameter class
* - changed logic of interval
- modified tests to accomodate new logic for intervals
- add_interval and add_option now have an additional argument
* small fix to ParameterTest
* Suggested changes by Junchen: setup.py taking requirements from requirements.txt file
* Replace interval and option with _interval and _option
* Using closed intervals on both sides
* Fixing logic of is_legal to simplify the reading of the code
* Fixing and adding more tests for checking validity of values
* More meaningful error message when looking for a parameter that does not exist
* Addressing Carsten comments
* Added __iter__ and __next__ methods to CalculatorParameters which
use the underlying parameters.values() __iter__ and __next__ methods.
If we have a CalculatorParameters object called A, these two are eqvivalent:
for parameter in A.parameters.values():
print(parameter)
for parameter in A:
print(parameter)
It is also possible to get all the parameter objects contained in
a CalculatorParameters object into a list with just:
parameter_list = list(A)
* set_parameters method in base calculator
* Updated test to check set_parameters.
* removed specialized calculator and removed useless numpy dependency from BaseCalculator
* Specializedcalculator moved into the Tests
* forgotten SpecializedCalculator in tests/
* Split requirements in prod.txt and dev.txt
* github workflow using black also added to the dev requirements
* Update ci.yml
* Fix CI
* reformatted by black
* Several changes:
- Introduced type checks for values, intervals and options.
- Using pint.Quantity for floats and physical quantities with units
- Encode and decode pint.Quantity in json
* Adding test for using units.
* Removed print
* Code cleaning and improvements plus more tests
* respecting flake8 suggestion as reported by Junchen
* fixing typos
* Add BaseData.py
* add DiffractionData examples
* merge
* Update ci file.
libpyvinyl was not installed into testing environment, this caused ImportError.
* Update data and format base classes and example.
In this way, we can avoid dynamic module loading.
* Adding specs
* Update api_spec.py
Tried to combine Jun's and my earlier prototype into one. Not entirely happy
yet, need to add parameters / collections to get the whole story.
* add pseudocode
* add pseudocode
* modified based on the first interation of reviews.
* update BaseData and BaseFormat examples
* Add module docstring.
* comply snake case naming style
* cleaned up the pseudo code for discussion
* nice examples
* add BaseDataTest.py
* clean up unnecessary test and add integration test
* NumberCalculatorsFinished
* update PlusCalculator
* Add test_ArrayCalculators.py
* add test_Instrument.py
* Update BaseCalculator in plusminus
* the new proposed BaseCalculator
* No need input_keys for calculator init
* Format: Predicble return object from write
* add initialization check
* minor: error raise info updated
* fix errors for test_ArrayCalculators
* Black format
* improve BaseDataTest
* add list format test
* make mapping_type() consistent
* fix else in __get_dict_data and __get_file_data
* Add docstrings to BaseData
* add DataCollection to_list test
* Update plusminus test
* Update BaseFormat convert example
* Update BaseFormat convert example
* Temporary RandomImageCalculator.py for CI fixing
* implement Optional hint
* update BaseFormat placeholder functions and correct typos
* Fix typos and tests for duplicating file output
* Update format return style
* Remove unnecessary tests
More comprehensive tests are added in PR #47
* Format correction
* Update ci.yml to include testing pytest scripts
* update docstrings of basecalculator
* clean up BaseCalculator
* finish BaseCalcualtor, working on Test
* fix base_dir
* cleaned up BaseCalculatorTest
* set DataCollection setitem
* fixed all tests
* add pytest for integration
* uncomment missed calculator test
* remove SpecializedCalculator.py
* add edit parameter tests in BaseCalculatorTest.py
* fix ci
* add | to ci.yml
* Parameters.to_dict(): Deepcopy to not modify the original parameters
* Update README
Add installation and testing instructions.
* Update BaseCalculatorTest
* Renamed 'pointer' to 'reference' (there are no pointers in python)
* Removed commented code
* Update Test.
Remove unused modules
Remove travis check
pep8 fixes
* Syntax fix
* Update BaseCalculatorTest
Removed logging since not used.
* Update BaseCalculatorTest
Adding class docstring to class.
* Update BaseCalculator
Typo in authors list
Remove implementation of init_parameters(), it's an abstract class, so raise
NotImplementedError.
TODO: test init_parameters()
* Update README
Make summary more concise.
* Update BaseCalculator
black reformatting.
* Update example notebook
Adjust to new API (libpyvinyl)
TODO: fix BaseCalculator, see error in cell 7)
* Add Parameter class to module.
* Seperate unit tests and integration tests
* update tests in README.md
* update ci.yml with the new test file structure
* Fix BaseCalculator output consistensy check
* check expected_data can be reached, then allow extra data to be read
* expose BaseData in libpyvinyl __init__
* update setup.py toward 0.1.0
* bump version in setup.py
* Update CI badge in README.md
Replace the outdated Travis badge with current github CI badge.
* Add Parameters test corresponding to #51
* Method to retrieve the parameter value as a pint Quantity
* Support for numpy floats as input with conversion to pint.Quantity
* numpy now required by Parameter class in order to accept numpy floats as value
* Allow BaseCalculator to accept None input.
* Fix #50 by increasing ljust and adding a space
To deal with very long parameter names, we increasted the ljust and
added a space after the name, value and unit.
* Remove example calculators, which are not compatible with the new BaseCalculator class
* Remove unnecessary docs of the integration test
- Remove the license, history.rst and contributing
- Add graphs explaining the plusminus integration test
* Fix setup.py author format
Co-authored-by: Carsten Fortmann-Grote
Co-authored-by: Carsten Fortmann-Grote
Co-authored-by: Shervin Nourbakhsh
Co-authored-by: Carsten Fortmann-Grote
Co-authored-by: Mads Bertelsen
Co-authored-by: mads-bertelsen
Co-authored-by: shervin86
---
.github/workflows/ci.yml | 27 +
.gitignore | 13 +-
.travis.yml | 31 -
DEVEL.md | 22 +
README.md | 102 ++-
doc/source/_templates/footer.html | 2 +-
doc/source/conf.py | 4 +-
doc/source/include/notebooks/example-01.ipynb | 440 +++++++------
doc/source/include/refman.rst | 16 +-
libpyvinyl/AbstractBaseClass.py | 2 +-
libpyvinyl/BaseCalculator.py | 473 ++++++++------
libpyvinyl/BaseData.py | 489 ++++++++++++++
libpyvinyl/BaseFormat.py | 109 ++++
libpyvinyl/BeamlinePropagator.py | 47 --
libpyvinyl/Detector.py | 47 --
libpyvinyl/Instrument.py | 50 +-
libpyvinyl/Parameters/Collections.py | 128 +++-
libpyvinyl/Parameters/Parameter.py | 485 ++++++++++----
libpyvinyl/Parameters/__init__.py | 2 +-
libpyvinyl/SignalGenerator.py | 47 --
libpyvinyl/__init__.py | 6 +
requirements.txt | 7 +-
requirements/dev.txt | 4 +
requirements/prod.txt | 7 +
setup.cfg | 4 +-
setup.py | 56 +-
tests/BaseCalculatorTest.py | 381 -----------
tests/BeamlinePropagatorTest.py | 85 ---
tests/DetectorTest.py | 49 --
tests/InstrumentTest.py | 119 ----
tests/ParametersTest.py | 266 --------
tests/RadiationSampleInteractorTest.py | 49 --
tests/RandomImageCalculator.py | 38 --
tests/SignalGeneratorTest.py | 50 --
tests/Test.py | 69 --
.../plusminus/.github/ISSUE_TEMPLATE.md | 15 +
tests/integration/plusminus/.gitignore | 105 +++
tests/integration/plusminus/README.rst | 18 +
.../plusminus/docs/01-data_structure.png | Bin 0 -> 218354 bytes
.../plusminus/docs/02-instrument_example.png | Bin 0 -> 171726 bytes
.../ArrayCalculators/ArrayCalculator.py | 59 ++
.../plusminus/ArrayCalculators/__init__.py | 1 +
.../plusminus/ArrayData/ArrayData.py | 36 ++
.../plusminus/plusminus/ArrayData/H5Format.py | 47 ++
.../plusminus/ArrayData/TXTFormat.py | 45 ++
.../plusminus/plusminus/ArrayData/__init__.py | 3 +
.../plusminus/plusminus/BaseCalculator.py | 243 +++++++
.../NumberCalculators/MinusCalculator.py | 55 ++
.../NumberCalculators/PlusCalculator.py | 52 ++
.../plusminus/NumberCalculators/__init__.py | 2 +
.../plusminus/NumberData/H5Format.py | 47 ++
.../plusminus/NumberData/NumberData.py | 52 ++
.../plusminus/NumberData/TXTFormat.py | 45 ++
.../plusminus/NumberData/__init__.py | 3 +
.../plusminus/plusminus/__init__.py | 8 +
.../plusminus/plusminus/plusminus.py | 1 +
.../plusminus/requirements.txt} | 0
.../plusminus/requirements_dev.txt | 13 +
tests/integration/plusminus/setup.cfg | 23 +
tests/integration/plusminus/setup.py | 53 ++
tests/integration/plusminus/tests/__init__.py | 1 +
.../plusminus/tests/test_ArrayCalculators.py | 29 +
.../plusminus/tests/test_Instrument.py | 38 ++
.../plusminus/tests/test_NumberCalculators.py | 55 ++
.../plusminus/tests/test_NumberData.py | 106 ++++
tests/integration/plusminus/tox.ini | 26 +
.../unit/Test.py | 47 +-
tests/unit/test_BaseCalculator.py | 304 +++++++++
tests/unit/test_BaseData.py | 422 +++++++++++++
tests/unit/test_Instrument.py | 119 ++++
tests/unit/test_Parameters.py | 596 ++++++++++++++++++
71 files changed, 4531 insertions(+), 1864 deletions(-)
create mode 100644 .github/workflows/ci.yml
delete mode 100644 .travis.yml
create mode 100644 DEVEL.md
create mode 100644 libpyvinyl/BaseData.py
create mode 100644 libpyvinyl/BaseFormat.py
delete mode 100644 libpyvinyl/BeamlinePropagator.py
delete mode 100644 libpyvinyl/Detector.py
delete mode 100644 libpyvinyl/SignalGenerator.py
create mode 100644 requirements/dev.txt
create mode 100644 requirements/prod.txt
delete mode 100644 tests/BaseCalculatorTest.py
delete mode 100644 tests/BeamlinePropagatorTest.py
delete mode 100644 tests/DetectorTest.py
delete mode 100644 tests/InstrumentTest.py
delete mode 100644 tests/ParametersTest.py
delete mode 100644 tests/RadiationSampleInteractorTest.py
delete mode 100644 tests/RandomImageCalculator.py
delete mode 100644 tests/SignalGeneratorTest.py
delete mode 100644 tests/Test.py
create mode 100644 tests/integration/plusminus/.github/ISSUE_TEMPLATE.md
create mode 100644 tests/integration/plusminus/.gitignore
create mode 100644 tests/integration/plusminus/README.rst
create mode 100644 tests/integration/plusminus/docs/01-data_structure.png
create mode 100644 tests/integration/plusminus/docs/02-instrument_example.png
create mode 100644 tests/integration/plusminus/plusminus/ArrayCalculators/ArrayCalculator.py
create mode 100644 tests/integration/plusminus/plusminus/ArrayCalculators/__init__.py
create mode 100644 tests/integration/plusminus/plusminus/ArrayData/ArrayData.py
create mode 100644 tests/integration/plusminus/plusminus/ArrayData/H5Format.py
create mode 100644 tests/integration/plusminus/plusminus/ArrayData/TXTFormat.py
create mode 100644 tests/integration/plusminus/plusminus/ArrayData/__init__.py
create mode 100644 tests/integration/plusminus/plusminus/BaseCalculator.py
create mode 100644 tests/integration/plusminus/plusminus/NumberCalculators/MinusCalculator.py
create mode 100644 tests/integration/plusminus/plusminus/NumberCalculators/PlusCalculator.py
create mode 100644 tests/integration/plusminus/plusminus/NumberCalculators/__init__.py
create mode 100644 tests/integration/plusminus/plusminus/NumberData/H5Format.py
create mode 100644 tests/integration/plusminus/plusminus/NumberData/NumberData.py
create mode 100644 tests/integration/plusminus/plusminus/NumberData/TXTFormat.py
create mode 100644 tests/integration/plusminus/plusminus/NumberData/__init__.py
create mode 100644 tests/integration/plusminus/plusminus/__init__.py
create mode 100644 tests/integration/plusminus/plusminus/plusminus.py
rename tests/{__init__Test.py => integration/plusminus/requirements.txt} (100%)
create mode 100644 tests/integration/plusminus/requirements_dev.txt
create mode 100644 tests/integration/plusminus/setup.cfg
create mode 100644 tests/integration/plusminus/setup.py
create mode 100644 tests/integration/plusminus/tests/__init__.py
create mode 100644 tests/integration/plusminus/tests/test_ArrayCalculators.py
create mode 100644 tests/integration/plusminus/tests/test_Instrument.py
create mode 100644 tests/integration/plusminus/tests/test_NumberCalculators.py
create mode 100644 tests/integration/plusminus/tests/test_NumberData.py
create mode 100644 tests/integration/plusminus/tox.ini
rename libpyvinyl/RadiationSampleInteractor.py => tests/unit/Test.py (63%)
create mode 100644 tests/unit/test_BaseCalculator.py
create mode 100644 tests/unit/test_BaseData.py
create mode 100644 tests/unit/test_Instrument.py
create mode 100644 tests/unit/test_Parameters.py
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a94a800
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: CI
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements/dev.txt
+ pip install .
+ - name: unit testing
+ run: |
+ cd tests
+ pytest .
+ - name: Code formatting
+ run: |
+ black --check libpyvinyl/
diff --git a/.gitignore b/.gitignore
index 7c5019f..fd798ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,16 @@
*.code-workspace
.ropeproject
tags
-pyvinyl.egg-info/
doc/build
+libpyvinyl.egg-info
+.#*.*
+.readthedocs.yaml
+**/notebooks/*.json
+**/notebooks/*.h5
+**/notebooks/tmp*
+**/notebooks/.ipynb_checkpoints/
+tests/*.json
+tests/*.h5
+tests/tmp*
+dist/
+build/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b7a0d4f..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-language: python
-python:
- - "3.5"
- - "3.6"
- - "3.7"
- - "3.8"
- - "nightly"
-
-sudo: required
-dist: xenial
-
-matrix:
- allow_failures:
- - python: "3.5"
- - python: "3.6"
- - python: "nightly"
-
-
-cache:
- apt: false
- directories:
- - $HOME/.cache/pip
- - $HOME/lib
-
-install:
- - cd $TRAVIS_BUILD_DIR
- - pip install -r requirements.txt
- - pip install .
-
-script:
- - python tests/Test.py
diff --git a/DEVEL.md b/DEVEL.md
new file mode 100644
index 0000000..f72c24d
--- /dev/null
+++ b/DEVEL.md
@@ -0,0 +1,22 @@
+How to test
+------------------------------
+
+Minimally needed:
+```
+pip install -e ./
+cd tests/unit
+python Test.py
+```
+
+Recommended:
+```
+pip install --user pytest
+pip install -e ./
+cd tests
+# Test all
+pytest ./
+# Unit test only
+pytest ./unit
+# Integration test only
+pytest ./integration
+```
diff --git a/README.md b/README.md
index 48bc2af..9d01fb4 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,110 @@
# libpyvinyl - The python APIs for Virtual Neutron and x-raY Laboratory
-[![Build Status](https://travis-ci.com/PaNOSC-ViNYL/libpyvinyl.svg?branch=master)](https://travis-ci.com/PaNOSC-ViNYL/libpyvinyl)
+[![CI](https://github.com/PaNOSC-ViNYL/libpyvinyl/actions/workflows/ci.yml/badge.svg)](https://github.com/PaNOSC-ViNYL/libpyvinyl/actions/workflows/ci.yml)
[![Documentation Status](https://readthedocs.org/projects/libpyvinyl/badge/?version=latest)](https://libpyvinyl.readthedocs.io/en/latest/?badge=latest)
-
+
## Summary
+
The python package `libpyvinyl` exposes the high level API for simulation codes under
-the umbrella of the Virtual Neutron and x-raY Laboratory (ViNYL).
+the umbrella of the Virtual Neutron and x-raY Laboratory (ViNYL).
The fundamental class is the `BaseCalculator` and its sister class `Parameters`.
While `Parameters` is a pure state engine, i.e. it's sole purpose is to encapsulate
the physical, numerical, and computational parameters of a simulation, the `BaseCalculator`
-exposes the interface to
+exposes the interface to
-- Configure a simulation (through the corresponding `Parameters` instance)
-- Launch the simulation run
-- Collect the simulation output data and make it queriable as a class attribute
-- Snapshoot a simulation by dumping the object to disk (using the `dill` library).
+- Configure a simulation.
+- Launch the simulation run.
+- Collect the simulation output data.
+- Construct a `Data` instance that represents the simulation output data.
+- Snapshoot a simulation by dumping the object to disk.
- Reload a simulation run from disk and continue the run with optionally modified parameters.
-The `BaseCalculaton` is an abstract base class, it shall not be instantiated as such.
+The `BaseCalculator` is an abstract base class, it shall not be instantiated as such.
The anticipated use is to inherit specialised `Calculators` from `BaseCalculator` and to
implement the core functionality in the derived class. In particular, this is required
-for the methods responsible to launch a simulation (`run()`) .
+for the methods responsible to launch a simulation through the `backengine()` method.
As an example, we demonstrate in an [accompanying notebook](https://github.com/PaNOSC-ViNYL/libpyvinyl/blob/master/doc/source/include/notebooks/example-01.ipynb)
how to declare a derived `Calculator` and implement a `backengine` method. The example then
-shows how to run the simulation, store the results in a `hdf5` file, snapshot the simulation
+shows how to run the simulation, store the results in a `hdf5` file, snapshot the simulation
and reload the simulation into memory.
-## Acknowledgement
-This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852.
+## Installation
+
+We recommend installation in a virtual environment, either `conda` or `pyenv`.
+
+### Create a `conda` environment
+
+```
+$> conda create -n libpyvinyl
+```
+
+### Common users
+
+```
+$> pip install libpyvinyl
+```
+
+### Developers
+
+We provide a requirements file for developers in _requirements/dev.txt_.
+
+```
+$> cd requirements
+$> pip install -r dev.txt
+```
+
+`conda install` is currently not supported.
+
+Then, install `libpyvinyl` into the same environment. The `-e` flag links the installed library to
+the source code in the repository, such that changes in the latter are immediately effective in the installed version.
+```
+$> cd ..
+$> pip install -e .
+```
+## Testing
+
+We recommend to run the unittests and integration tests.
+
+```
+$> pytest tests
+```
+
+You should see a test report similar to this:
+
+```
+=============================================================== test session starts ================================================================
+platform linux -- Python 3.8.10, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
+rootdir: /home/juncheng/Projects/libpyvinyl
+collected 100 items
+
+integration/plusminus/tests/test_ArrayCalculators.py . [ 1%]
+integration/plusminus/tests/test_Instrument.py . [ 2%]
+integration/plusminus/tests/test_NumberCalculators.py ... [ 5%]
+integration/plusminus/tests/test_NumberData.py ........... [ 16%]
+unit/test_BaseCalculator.py .......... [ 26%]
+unit/test_BaseData.py ........................... [ 53%]
+unit/test_Instrument.py ....... [ 60%]
+unit/test_Parameters.py ........................................ [100%]
+
+=============================================================== 100 passed in 0.56s ================================================================
+```
+
+You can also run unittests only:
+
+```
+pytest tests/unit
+```
+
+Or to run integration tests only:
+
+```
+pytest tests/integration
+```
+
+## Acknowledgement
+
+This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852.
diff --git a/doc/source/_templates/footer.html b/doc/source/_templates/footer.html
index ccb00fa..af67528 100644
--- a/doc/source/_templates/footer.html
+++ b/doc/source/_templates/footer.html
@@ -10,7 +10,7 @@
The software
- libpyvinyl is licensed under the LGPL version 3 or later.
+ libpyvinyl is licensed under the LGPL version 3 or later.
This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852
diff --git a/doc/source/conf.py b/doc/source/conf.py
index d7951ea..3e09a77 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -23,8 +23,8 @@
# -- Project information -----------------------------------------------------
project = 'libpyvinyl'
-copyright = '2020, Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E'
-author = 'Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E'
+copyright = '2020-2021, Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E, Shervin Nourbakhsh'
+author = 'Carsten Fortmann-Grote, Mads Bertelsen, Juncheng E, Shervin Nourbakhsh'
# The short X.Y version
version = '0.0.2'
diff --git a/doc/source/include/notebooks/example-01.ipynb b/doc/source/include/notebooks/example-01.ipynb
index 8709a7e..6a95225 100644
--- a/doc/source/include/notebooks/example-01.ipynb
+++ b/doc/source/include/notebooks/example-01.ipynb
@@ -9,107 +9,93 @@
},
{
"cell_type": "markdown",
+ "metadata": {},
"source": [
"## Imports "
- ],
- "metadata": {
- "collapsed": false
- }
+ ]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
+ "metadata": {},
"outputs": [],
"source": [
- "from libpyvinyl.BaseCalculator import BaseCalculator, Parameters\n",
- "import os\n",
- "import h5py\n",
- "import numpy"
- ],
+ "%load_ext autoreload\n",
+ "%autoreload 2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "from libpyvinyl import BaseCalculator, CalculatorParameters, Parameter\n",
+ "import os\n",
+ "import h5py\n",
+ "import numpy"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "## Implement a Calculator that derives from `BaseCalculator`."
- ],
"metadata": {
- "collapsed": false,
"pycharm": {
"name": "#%% md\n"
}
- }
- },
- {
- "cell_type": "code",
- "execution_count": 27,
- "outputs": [],
+ },
"source": [
- "from libpyvinyl.BaseCalculator import BaseCalculator, Parameters\n",
- "import numpy\n",
- "import h5py\n",
- "import sys"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
+ "## Implement a Calculator that derives from `BaseCalculator`."
+ ]
},
{
"cell_type": "code",
- "execution_count": 30,
- "outputs": [],
- "source": [
- "sys.path.insert(0,'../../../../tests')\n",
- "\n",
- "from RandomImageCalculator import RandomImageCalculator"
- ],
+ "execution_count": 3,
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
- },
- {
- "cell_type": "code",
- "execution_count": null,
+ },
"outputs": [],
"source": [
- "class RandomImageCalculatorNB(BaseCalculator):\n",
+ "class RandomImageCalculator(BaseCalculator):\n",
" \"\"\" class: Implements simulation of a rondom image for demonstration purposes. \"\"\"\n",
- " def __init__(self, parameters=None, dumpfile=None, input_path=None, output_path=None):\n",
+ " def __init__(self, \n",
+ " *args,\n",
+ " **kwargs\n",
+ " ):\n",
" \"\"\" Constructor of the RandomImageCalculator class. \"\"\"\n",
- " super().__init__(parameters=parameters, dumpfile=dumpfile, output_path=output_path)\n",
+ " super().__init__(*args, **kwargs)\n",
+ " \n",
+ " self.__data = None\n",
+ " \n",
"\n",
" def backengine(self):\n",
" \"\"\" Method to do the actual calculation.\"\"\"\n",
- " tmpdata = numpy.random.random((self.parameters.grid_size_x, self.parameters.grid_size_y))\n",
- "\n",
- " self._set_data(tmpdata)\n",
- " return 0\n",
+ " self.__data = [numpy.random.random((self.parameters.grid_size_x, self.parameters.grid_size_y))]\n",
+ " \n",
+ " self.saveH5()\n",
"\n",
" def saveH5(self, openpmd=False):\n",
" \"\"\" Save image to hdf5 file 'output_path'. \"\"\"\n",
- " with h5py.File(self.output_path, \"w\") as h5:\n",
- " ds = h5.create_dataset(\"/data\", data=self.data)\n",
- "\n",
- " h5.close()\n",
- "\n"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
+ " for i,fname in enumerate(self.output_filenames):\n",
+ " with h5py.File(fname, \"w\") as h5:\n",
+ " ds = h5.create_dataset(\"/data\", data=self.__data[i])\n",
+ " \n",
+ " def init_parameters():\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -120,52 +106,101 @@
},
{
"cell_type": "code",
- "execution_count": 31,
+ "execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
- "parameters = Parameters(photon_energy=6e3, pulse_energy=1.0e-6, grid_size_x=128, grid_size_y=128)"
+ "photon_energy = Parameter(name='photon_energy', unit='keV', comment=\"The photon energy in units of kilo electronvolt (keV)\")\n",
+ "pulse_energy = Parameter(name='pulse_energy', unit='J')\n",
+ "grid_size_x = Parameter(name='grid_size_x', unit=\"\")\n",
+ "grid_size_y = Parameter(name='grid_size_y', unit=\"\")\n"
]
},
{
- "cell_type": "markdown",
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
"source": [
- "### Setup the calculator"
- ],
- "metadata": {
- "collapsed": false
- }
+ "photon_energy.value = 6.0\n",
+ "pulse_energy.value = 5.0e-6\n",
+ "grid_size_x.value = 128\n",
+ "grid_size_y.value = 256\n"
+ ]
},
{
"cell_type": "code",
- "execution_count": 32,
+ "execution_count": 6,
+ "metadata": {},
"outputs": [],
"source": [
- "calculator = RandomImageCalculator(parameters, output_path=\"out.h5\")"
- ],
+ "parameters = CalculatorParameters([photon_energy, pulse_energy, grid_size_x, grid_size_y])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Setup the calculator"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [
+ {
+ "ename": "ValueError",
+ "evalue": "len(output_keys) = 1 is not equal to len(output_data_types) = 1",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m/tmp/ipykernel_3697194/3811637111.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcalculator\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mRandomImageCalculator\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparameters\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mparameters\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'RandomImageCalculator'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moutput_keys\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m'random_image.h5'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moutput_data_types\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;32m/tmp/ipykernel_3697194/1285912060.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 6\u001b[0m ):\n\u001b[1;32m 7\u001b[0m \u001b[0;34m\"\"\" Constructor of the RandomImageCalculator class. \"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 8\u001b[0;31m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__init__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 9\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/Repositories/libpyvinyl/libpyvinyl/BaseCalculator.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, name, input, output_keys, output_data_types, output_filenames, instrument_base_dir, calculator_base_dir, parameters)\u001b[0m\n\u001b[1;32m 127\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mparameters\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mparameters\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 128\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 129\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__check_consistency\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 130\u001b[0m \u001b[0;31m# Create output data objects according to the output_data_classes\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 131\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__init_output\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+ "\u001b[0;32m~/Repositories/libpyvinyl/libpyvinyl/BaseCalculator.py\u001b[0m in \u001b[0;36m__check_consistency\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 134\u001b[0m \u001b[0;34m\"\"\"Check the consistency of the input parameters\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 135\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moutput_keys\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moutput_data_types\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 136\u001b[0;31m raise ValueError(\n\u001b[0m\u001b[1;32m 137\u001b[0m \u001b[0;34mf\"len(output_keys) = {len(self.output_keys)} is not equal to len(output_data_types) = {len(self.output_keys)}\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 138\u001b[0m )\n",
+ "\u001b[0;31mValueError\u001b[0m: len(output_keys) = 1 is not equal to len(output_data_types) = 1"
+ ]
+ }
+ ],
+ "source": [
+ "calculator = RandomImageCalculator(parameters=parameters, name='RandomImageCalculator', output_keys='random_image.h5', output_data_types=[], input=[] )"
+ ]
},
{
"cell_type": "markdown",
+ "metadata": {},
"source": [
"### Run the backengine"
- ],
- "metadata": {
- "collapsed": false
- }
+ ]
},
{
"cell_type": "code",
"execution_count": 33,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"data": {
- "text/plain": "0"
+ "text/plain": [
+ "0"
+ ]
},
"execution_count": 33,
"metadata": {},
@@ -174,30 +209,45 @@
],
"source": [
"calculator.backengine()"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
+ ]
},
{
"cell_type": "markdown",
+ "metadata": {},
"source": [
"### Look at the data and store as hdf5"
- ],
- "metadata": {
- "collapsed": false
- }
+ ]
},
{
"cell_type": "code",
"execution_count": 34,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"data": {
- "text/plain": "array([[0.07471297, 0.00703423, 0.92835525, ..., 0.83050226, 0.93011749,\n 0.10575778],\n [0.30465388, 0.36400513, 0.48903381, ..., 0.0438568 , 0.39087367,\n 0.97940832],\n [0.61994805, 0.84566645, 0.42535347, ..., 0.94919735, 0.17939005,\n 0.74872113],\n ...,\n [0.22103305, 0.07844426, 0.8127275 , ..., 0.4273249 , 0.78210725,\n 0.59653636],\n [0.00889755, 0.40566176, 0.33960702, ..., 0.2634355 , 0.34068678,\n 0.99275201],\n [0.99495603, 0.18621833, 0.25057866, ..., 0.33598942, 0.10660242,\n 0.20565293]])"
+ "text/plain": [
+ "array([[0.07471297, 0.00703423, 0.92835525, ..., 0.83050226, 0.93011749,\n",
+ " 0.10575778],\n",
+ " [0.30465388, 0.36400513, 0.48903381, ..., 0.0438568 , 0.39087367,\n",
+ " 0.97940832],\n",
+ " [0.61994805, 0.84566645, 0.42535347, ..., 0.94919735, 0.17939005,\n",
+ " 0.74872113],\n",
+ " ...,\n",
+ " [0.22103305, 0.07844426, 0.8127275 , ..., 0.4273249 , 0.78210725,\n",
+ " 0.59653636],\n",
+ " [0.00889755, 0.40566176, 0.33960702, ..., 0.2634355 , 0.34068678,\n",
+ " 0.99275201],\n",
+ " [0.99495603, 0.18621833, 0.25057866, ..., 0.33598942, 0.10660242,\n",
+ " 0.20565293]])"
+ ]
},
"execution_count": 34,
"metadata": {},
@@ -206,90 +256,98 @@
],
"source": [
"calculator.data"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
+ ]
},
{
"cell_type": "code",
"execution_count": 35,
- "outputs": [],
- "source": [
- "calculator.saveH5(calculator.output_path)"
- ],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "calculator.saveH5(calculator.output_path)"
+ ]
},
{
"cell_type": "markdown",
+ "metadata": {},
"source": [
"### Save the parameters to a human readable json file."
- ],
- "metadata": {
- "collapsed": false
- }
+ ]
},
{
"cell_type": "code",
"execution_count": 36,
- "outputs": [],
- "source": [
- "parameters.to_json(\"my_parameters.json\")"
- ],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "parameters.to_json(\"my_parameters.json\")"
+ ]
},
{
"cell_type": "markdown",
+ "metadata": {},
"source": [
"### Save calculator to binary dump."
- ],
- "metadata": {
- "collapsed": false
- }
+ ]
},
{
"cell_type": "code",
"execution_count": 37,
- "outputs": [],
- "source": [
- "dumpfile = calculator.dump()"
- ],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "dumpfile = calculator.dump()"
+ ]
},
{
"cell_type": "markdown",
+ "metadata": {},
"source": [
"### Load back parameters"
- ],
- "metadata": {
- "collapsed": false
- }
+ ]
},
{
"cell_type": "code",
"execution_count": 38,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"data": {
- "text/plain": "6000.0"
+ "text/plain": [
+ "6000.0"
+ ]
},
"execution_count": 38,
"metadata": {},
@@ -298,111 +356,121 @@
],
"source": [
"new_parameters = Parameters.from_json(\"my_parameters.json\")"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "### Look ath the photon energy of the restored parameters."
- ],
"metadata": {
- "collapsed": false,
"pycharm": {
"name": "#%% md\n"
}
- }
+ },
+ "source": [
+ "### Look ath the photon energy of the restored parameters."
+ ]
},
{
"cell_type": "code",
"execution_count": null,
- "outputs": [],
- "source": [
- "new_parameters.photon_energy"
- ],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "new_parameters.photon_energy"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "### Reconstruct the dumped calculator."
- ],
"metadata": {
- "collapsed": false,
"pycharm": {
"name": "#%% md\n"
}
- }
+ },
+ "source": [
+ "### Reconstruct the dumped calculator."
+ ]
},
{
"cell_type": "code",
"execution_count": null,
- "outputs": [],
- "source": [
- "reloaded_calculator = RandomImageCalculator(dumpfile=dumpfile)"
- ],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "reloaded_calculator = RandomImageCalculator(dumpfile=dumpfile)"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "### Query the data from the reconstructed calculator."
- ],
"metadata": {
- "collapsed": false,
"pycharm": {
"name": "#%% md\n"
}
- }
+ },
+ "source": [
+ "### Query the data from the reconstructed calculator."
+ ]
},
{
"cell_type": "code",
"execution_count": null,
- "outputs": [],
- "source": [
- "reloaded_calculator.data"
- ],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": [
+ "reloaded_calculator.data"
+ ]
},
{
"cell_type": "markdown",
- "source": [
- "### Look at the photon energy of the reconstructed calculator."
- ],
"metadata": {
- "collapsed": false,
"pycharm": {
"name": "#%% md\n"
}
- }
+ },
+ "source": [
+ "### Look at the photon energy of the reconstructed calculator."
+ ]
},
{
"cell_type": "code",
"execution_count": 39,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [
{
"data": {
- "text/plain": "6000.0"
+ "text/plain": [
+ "6000.0"
+ ]
},
"execution_count": 39,
"metadata": {},
@@ -411,32 +479,29 @@
],
"source": [
"reloaded_calculator.parameters.photon_energy\n"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
+ ]
},
{
"cell_type": "code",
"execution_count": null,
- "outputs": [],
- "source": [],
"metadata": {
"collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ },
"pycharm": {
"name": "#%%\n"
}
- }
+ },
+ "outputs": [],
+ "source": []
}
],
"metadata": {
"kernelspec": {
- "name": "pycharm-44f7cfec",
+ "display_name": "pyvinyl",
"language": "python",
- "display_name": "PyCharm (libpyvinyl)"
+ "name": "pyvinyl"
},
"language_info": {
"codemirror_mode": {
@@ -448,9 +513,16 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.5"
+ "version": "3.9.7"
+ },
+ "widgets": {
+ "application/vnd.jupyter.widget-state+json": {
+ "state": {},
+ "version_major": 2,
+ "version_minor": 0
+ }
}
},
"nbformat": 4,
"nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/doc/source/include/refman.rst b/doc/source/include/refman.rst
index 38cf3be..9f293e0 100644
--- a/doc/source/include/refman.rst
+++ b/doc/source/include/refman.rst
@@ -1,12 +1,22 @@
API Reference Manual
====================
-.. autoclass:: libpyvinyl.BaseCalculator
+.. automodule:: libpyvinyl.BaseCalculator
:members:
:undoc-members:
:show-inheritance:
-.. autoclass:: libpyvinyl.Parameters
+.. automodule:: libpyvinyl.Parameters.Collection
:members:
:undoc-members:
- :show-inheritance:
\ No newline at end of file
+ :show-inheritance:
+
+.. automodule:: libpyvinyl.Parameters.Parameter
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. automodule:: libpyvinyl.Instrument
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/libpyvinyl/AbstractBaseClass.py b/libpyvinyl/AbstractBaseClass.py
index 475ff98..c5bf935 100644
--- a/libpyvinyl/AbstractBaseClass.py
+++ b/libpyvinyl/AbstractBaseClass.py
@@ -25,6 +25,7 @@
####################################################################################
from abc import ABCMeta, abstractmethod
+
class AbstractBaseClass(object, metaclass=ABCMeta):
"""
:class AbstractBaseClass: Base class of libpyvinyl
@@ -33,4 +34,3 @@ class AbstractBaseClass(object, metaclass=ABCMeta):
@abstractmethod
def __init__(self):
pass
-
diff --git a/libpyvinyl/BaseCalculator.py b/libpyvinyl/BaseCalculator.py
index 12d5716..b914fa9 100644
--- a/libpyvinyl/BaseCalculator.py
+++ b/libpyvinyl/BaseCalculator.py
@@ -1,14 +1,14 @@
"""
-:module BaseCalculator: Module hosting the BaseCalculator and Parameters classes.
+:module BaseCalculator: Module hosting the BaseCalculator class.
"""
####################################################################################
# #
-# This file is part of libpyvinyl - The APIs for Virtual Neutron and x-raY #
+# This file is part of libpyvinyl - The APIs for Virtual Neutron and x-raY #
# Laboratory. #
# #
-# Copyright (C) 2020 Carsten Fortmann-Grote #
+# Copyright (C) 2021 Carsten Fortmann-Grote, Juncheng E #
# #
# This program is free software: you can redistribute it and/or modify it under #
# the terms of the GNU Lesser General Public License as published by the Free #
@@ -25,19 +25,22 @@
####################################################################################
from abc import abstractmethod
-from libpyvinyl.AbstractBaseClass import AbstractBaseClass
-from libpyvinyl.Parameters import CalculatorParameters
+from typing import Union, Optional
from tempfile import mkstemp
import copy
import dill
-import h5py
-import sys
+from pathlib import Path
import logging
-import numpy
import os
-logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s',
- level=logging.WARNING)
+from libpyvinyl.AbstractBaseClass import AbstractBaseClass
+from libpyvinyl.BaseData import BaseData, DataCollection
+from libpyvinyl.Parameters import CalculatorParameters
+
+
+logging.basicConfig(
+ format="%(asctime)s %(levelname)s:%(message)s", level=logging.WARNING
+)
class BaseCalculator(AbstractBaseClass):
@@ -50,77 +53,293 @@ class BaseCalculator(AbstractBaseClass):
This class is to be used as a base class for all calculators that implement
a special simulation module, such as a photon diffraction calculator. Such a
- specialized Calculator than has the same interface to the simulation
- backengine as all other ViNyL Calculators.
+ specialized Calculator has the same interface to the simulation
+ backengine as all other ViNYL Calculators.
+
+ A Complete example including a instrument and calculators can be found in
+ `test/integration/plusminus`
"""
- @abstractmethod
- def __init__(self, name: str, parameters=None, dumpfile=None, **kwargs):
+
+ def __init__(
+ self,
+ name: str,
+ input: Union[DataCollection, list, BaseData],
+ output_keys: Union[list, str],
+ output_data_types: list,
+ output_filenames: Union[list, str, None] = None,
+ instrument_base_dir: str = "./",
+ calculator_base_dir: str = "BaseCalculator",
+ parameters: CalculatorParameters = None,
+ ):
"""
- :param name: The name for this calculator.
- :type name: str
+ :param name: The name of this calculator.
+ :type name: str
+
+ :param name: The input of this calculator. It can be a `DataCollection`,
+ a list of `DataCollection`s or a single Data Object.
+ :type name: DataCollection, list or BaseData
+
+ :param output_keys: The key(s) of this calculator's output data. It's a list of `str`s or
+ a single str.
+ :type output_keys: list or str
+
+ :param output_data_types: The data type(s), i.e., classes, of each output. It's a list of the
+ data classes or a single data class. The available data classes are based on `BaseData`.
+ :type output_data_types: list or DataClass
+
+ :param output_filenames: The name(s) of the output file(s). It can be a str of a filename or
+ a list of filenames. If the mapping is dict mapping, the name is `None`. Defaults to None.
+ :type output_filenames: list or str
+
+ :param instrument_base_dir: The base directory for the instrument to which this calculator
+ belongs. Defaults to "./". The final exact output file path depends on `instrument_base_dir`
+ and `calculator_base_dir`: `instrument_base_dir`/`calculator_base_dir`/filename
+ :type instrument_base_dir: str
+
+ :param calculator_base_dir: The base directory for this calculator. Defaults to "./". The final
+ exact output file path depends on `instrument_base_dir` and
+ `calculator_base_dir`: `instrument_base_dir`/`calculator_base_dir`/filename
+ :type instrument_base_dir: str
:param parameters: The parameters for this calculator.
:type parameters: Parameters
- :param dumpfile: If given, load a previously dumped (aka pickled) calculator.
+ """
+ # Initialize the variables
+ self.__name = None
+ self.__instrument_base_dir = None
+ self.__calculator_base_dir = None
+ self.__input = None
+ self.__output_keys = None
+ self.__output_data_types = None
+ self.__output_filenames = None
+ self.__parameters = None
+
+ self.name = name
+ self.input = input
+ self.output_keys = output_keys
+ self.output_data_types = output_data_types
+ self.output_filenames = output_filenames
+ self.instrument_base_dir = instrument_base_dir
+ self.calculator_base_dir = calculator_base_dir
+ self.parameters = parameters
+
+ self.__check_consistency()
+ # Create output data objects according to the output_data_classes
+ self.__init_output()
+
+ def __check_consistency(self):
+ """Check the consistency of the input parameters"""
+ if len(self.output_keys) != len(self.output_data_types):
+ raise ValueError(
+ f"len(output_keys) = {len(self.output_keys)} is not equal to len(output_data_types) = {len(self.output_data_types)}"
+ )
+
+ def __check_output_filenames(self):
+ """Since output_filenames can be None for output in dict mapping, only check output_files when necessary"""
+ if len(self.output_data_types) != len(self.output_filenames):
+ raise ValueError(
+ f"len(output_filenames) = {len(self.output_filenames)} is not equal to len(output_data_types) = {len(self.output_data_types)}"
+ )
+
+ @property
+ def name(self) -> str:
+ return self.__name
+
+ @name.setter
+ def name(self, value):
+ if isinstance(value, str):
+ self.__name = value
+ else:
+ raise TypeError(
+ f"Calculator: `name` is expected to be a str, not {type(value)}"
+ )
- :param kwargs: (key, value) pairs of further arguments to the calculator, e.g input_path, output_path.
+ @property
+ def parameters(self) -> CalculatorParameters:
+ return self.__parameters
+
+ @parameters.setter
+ def parameters(self, value: CalculatorParameters):
+ self.set_parameters(value)
+
+ def set_parameters(self, value: CalculatorParameters):
+ """Set the calculator parameters"""
+ if isinstance(value, CalculatorParameters):
+ self.__parameters = value
+ elif value is None:
+ self.init_parameters()
+ else:
+ raise TypeError(
+ f"Calculator: `parameters` is expected to be CalculatorParameters, not {type(value)}"
+ )
- If both 'parameters' and 'dumpfile' are given, the dumpfile is loaded
- first. Passing a parameters object may be used to update some
- parameters.
+ @property
+ def instrument_base_dir(self) -> str:
+ return self.__instrument_base_dir
- Example:
- ```
- # Define a specialized calculator.
- class MyCalculator(BaseCalculator):
+ @instrument_base_dir.setter
+ def instrument_base_dir(self, value):
+ self.set_instrument_base_dir(value)
- def __init__(self, parameters=None, dumpfile=None, **kwargs):
- super()__init__(parameters, dumpfile, **kwargs)
+ def set_instrument_base_dir(self, value: str):
+ """Set the instrument base directory"""
+ if isinstance(value, str):
+ self.__instrument_base_dir = value
+ else:
+ raise TypeError(
+ f"Calculator: `instrument_base_dir` is expected to be a str, not {type(value)}"
+ )
- def backengine(self):
- os.system("my_simulation_backengine_call")
+ @property
+ def calculator_base_dir(self) -> str:
+ return self.__calculator_base_dir
- def saveH5(self):
- # Format output into openpmd hdf5 format.
+ @calculator_base_dir.setter
+ def calculator_base_dir(self, value):
+ self.set_calculator_base_dir(value)
- class MyParameters(Parameters):
- pass
+ def set_calculator_base_dir(self, value: str):
+ """Set the calculator base directory"""
+ if isinstance(value, str):
+ self.__calculator_base_dir = value
+ else:
+ raise TypeError(
+ f"Calculator: `calculator_base_dir` is expected to be a str, not {type(value)}"
+ )
- my_calculator = MyCalculator(my_parameters)
+ @property
+ def input(self) -> DataCollection:
+ return self.__input
+
+ @input.setter
+ def input(self, value):
+ self.set_input(value)
+
+ def set_input(self, value: Union[DataCollection, list, BaseData, None]):
+ """Set the calculator input data. It can be a DataCollection, list or BaseData object."""
+ if isinstance(value, (DataCollection, type(None))):
+ self.__input = value
+ elif isinstance(value, list):
+ self.__input = DataCollection(*value)
+ elif isinstance(value, BaseData):
+ self.__input = DataCollection(value)
+ else:
+ raise TypeError(
+ f"Calculator: `input` can be a DataCollection, list or BaseData object, and will be treated as a DataCollection. Your input type: {type(value)} is not accepted."
+ )
- my_calculator.backengine()
+ @property
+ def output_keys(self) -> list:
+ return self.__output_keys
- my_calculator.saveH5("my_sim_output.h5")
- my_calculater.dump("my_calculator.dill")
- ```
+ @output_keys.setter
+ def output_keys(self, value):
+ self.set_output_keys(value)
- """
+ @property
+ def base_dir(self):
+ """The base path for the output files of this calculator in consideration of instrument_base_dir and calculator_base_dir"""
+ base_dir = Path(self.instrument_base_dir) / self.calculator_base_dir
+ return str(base_dir)
- if isinstance(name, str):
- self.name = name
+ @property
+ def output_file_paths(self):
+ """The final output file paths considering base_dir"""
+ self.__check_output_filenames()
+ paths = []
+
+ for filename in self.output_filenames:
+ path = Path(self.base_dir) / filename
+ # Make sure the file directory exists
+ path.parent.mkdir(parents=True, exist_ok=True)
+ paths.append(str(path))
+ return paths
+
+ def set_output_keys(self, value: Union[list, str]):
+ """Set the calculator output keys. It can be a list of str or a single str."""
+ if isinstance(value, list):
+ for item in value:
+ assert type(item) is str
+ self.__output_keys = value
+ elif isinstance(value, str):
+ self.__output_keys = [value]
else:
- raise TypeError("name should be in str type.")
- # Set data
- self.__data = None
+ raise TypeError(
+ f"Calculator: `output_keys` can be a list or str, and will be treated as a list. Your input type: {type(value)} is not accepted."
+ )
- if isinstance(parameters, (type(None), CalculatorParameters)):
- self.parameters = parameters
+ @property
+ def output_data_types(self) -> list:
+ return self.__output_data_types
+
+ @output_data_types.setter
+ def output_data_types(self, value):
+ self.set_output_data_types(value)
+
+ def set_output_data_types(self, value: Union[list, BaseData]):
+ """Set the calculator output data type. It can be a list of DataClass or a single DataClass."""
+ if isinstance(value, list):
+ for item in value:
+ assert issubclass(item, BaseData)
+ self.__output_data_types = value
+ elif issubclass(value, BaseData):
+ self.__output_data_types = [value]
else:
raise TypeError(
- "parameters should be in CalculatorParameters type.")
+ f"Calculator: `output_data_types` can be a list or a subclass of BaseData, and will be treated as a list. Your input type: {type(value)} is not accepted."
+ )
- # Must load after setting paramters to avoid being overrode by empty parameters.
- if dumpfile is not None:
- self.__load_from_dump(dumpfile)
+ @property
+ def output_filenames(self) -> list:
+ return self.__output_filenames
+
+ @output_filenames.setter
+ def output_filenames(self, value):
+ self.set_output_filenames(value)
+
+ def set_output_filenames(self, value: Union[list, str, None]):
+ """Set the calculator output filenames. It can be a list of filenames or just a single str."""
+ if isinstance(value, list):
+ for item in value:
+ assert type(item) is str or type(None)
+ self.__output_filenames = value
+ elif isinstance(value, (str, type(None))):
+ self.__output_filenames = [value]
+ else:
+ raise TypeError(
+ f"Calculator: `output_filenames` can be a list or just a str or None, and will be treated as a list. Your input type: {type(value)} is not accepted."
+ )
- if "output_path" in kwargs:
- self.output_path = kwargs["output_path"]
+ @property
+ def output(self):
+ """The output of this calculator"""
+ return self.__output
+
+ @property
+ def data(self):
+ """The alias of output. It's not recommended to use this variable name due to it's ambiguity."""
+ return self.__output
+
+ @abstractmethod
+ def init_parameters(self):
+ """Virtual method to initialize all parameters. Must be implemented on the
+ specialized class."""
+
+ raise NotImplementedError
+
+ def __init_output(self):
+ """Create output data objects according to the output_data_types"""
+ output = DataCollection()
+ for i, key in enumerate(self.output_keys):
+ output_data = self.output_data_types[i](key)
+ output.add_data(output_data)
+ self.__output = output
def __call__(self, parameters=None, **kwargs):
- """ The copy constructor
+ """The copy constructor
:param parameters: The parameters for the new calculator.
:type parameters: CalculatorParameters
@@ -135,51 +354,42 @@ def __call__(self, parameters=None, **kwargs):
new.__dict__.update(kwargs)
- if parameters is not None:
+ if parameters is None:
+ new.parameters = copy.deepcopy(new.parameters)
+ else:
new.parameters = parameters
-
return new
- def __load_from_dump(self, dumpfile):
- """ """
- """
- Load a dill dump and initialize self's internals.
+ @classmethod
+ def from_dump(cls, dumpfile: str):
+ """Load a dill dump from a dumpfile.
+ :param dumpfile: The file name of the dumpfile.
+ :type dumpfile: str
+ :return: The calculator object restored from the dumpfile.
+ :rtype: CalcualtorClass
"""
- with open(dumpfile, 'rb') as fhandle:
+ with open(dumpfile, "rb") as fhandle:
try:
tmp = dill.load(fhandle)
except:
- raise IOError(
- "Cannot load calculator from {}.".format(dumpfile))
+ raise IOError("Cannot load calculator from {}.".format(dumpfile))
- self.__dict__ = copy.deepcopy(tmp.__dict__)
+ if not isinstance(tmp, cls):
+ raise TypeError(f"The object in the file {dumpfile} is not a {cls}")
- del tmp
-
- @property
- def parameters(self):
- """ The parameters of this calculator. """
+ return tmp
- return self.__parameters
-
- @parameters.setter
- def parameters(self, val):
-
- if not isinstance(val, (type(None), CalculatorParameters)):
- raise TypeError(
- """Passed argument 'parameters' has wrong type. Expected CalculatorParameters, found {}."""
- .format(type(val)))
-
- self.__parameters = val
-
- def dump(self, fname=None):
+ def dump(self, fname: Optional[str] = None) -> str:
"""
Dump class instance to file.
:param fname: Filename (path) of the file to write.
+ :type fname: str
+ :return: The filename of the dumpfile
+ :rtype: str
"""
if fname is None:
@@ -188,109 +398,14 @@ def dump(self, fname=None):
prefix=self.__class__.__name__[-1],
dir=os.getcwd(),
)
- try:
- with open(fname, "wb") as file_handle:
- dill.dump(self, file_handle)
- except:
- raise
+ with open(fname, "wb") as file_handle:
+ dill.dump(self, file_handle)
return fname
- @abstractmethod
- def saveH5(self, fname: str, openpmd: bool = True):
- """ Save the simulation data to hdf5 file.
-
- :param fname: The filename (path) of the file to write the data to.
- :type fname: str
-
- :param openpmd: Flag that controls whether the data is to be written in according to the openpmd metadata standard. Default is True.
-
- """
-
- @property
- def data(self):
- return self.__data
-
- @data.setter
- def data(self, val):
- raise AttributeError("Attribute 'data' is read-only.")
-
@abstractmethod
def backengine(self):
- pass
-
- @classmethod
- def run_from_cli(cls):
- """
- Method to start calculator computations from command line.
-
- :return: exit with status code
-
- """
- if len(sys.argv) == 2:
- fname = sys.argv[1]
- calculator = cls(fname)
- status = calculator._run()
- sys.exit(status)
-
- def _run(self):
- """
- Method to do computations. By default starts backengine.
-
- :return: status code.
-
- """
- result = self.backengine()
-
- if result is None:
- result = 0
-
- return result
-
- def _set_data(self, data):
- """ """
- """ Private method to store the data on the object.
-
- :param data: The data to store.
-
- """
-
- self.__data = data
-
-
-# Mocks for testing. Have to be here to work around bug in dill that does not
-# like classes to be defined outside of __main__.
-class SpecializedCalculator(BaseCalculator):
- def __init__(self, name, parameters=None, dumpfile=None, **kwargs):
-
- super().__init__(name, parameters, dumpfile, **kwargs)
-
- def setParams(self, photon_energy: float = 10, pulse_energy: float = 1e-3):
- if not isinstance(self.parameters, CalculatorParameters):
- self.parameters = CalculatorParameters()
- self.parameters.new_parameter("photon_energy",
- unit="eV",
- comment="Photon energy")
- self.parameters['photon_energy'].set_value(photon_energy)
-
- self.parameters.new_parameter("pulse_energy",
- unit="joule",
- comment="Pulse energy")
- self.parameters['pulse_energy'].set_value(pulse_energy)
-
- def backengine(self):
- self._BaseCalculator__data = numpy.random.normal(
- loc=self.parameters['photon_energy'].value,
- scale=0.001 * self.parameters['photon_energy'].value,
- size=(100, ))
-
- return 0
-
- def saveH5(self, openpmd=False):
- with h5py.File(self.output_path, "w") as h5:
- ds = h5.create_dataset("/data", data=self.data)
-
- h5.close()
+ raise NotImplementedError
# This project has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreement No. 823852.
diff --git a/libpyvinyl/BaseData.py b/libpyvinyl/BaseData.py
new file mode 100644
index 0000000..cb06948
--- /dev/null
+++ b/libpyvinyl/BaseData.py
@@ -0,0 +1,489 @@
+""" :module BaseData: Module hosts the BaseData class."""
+from typing import Union, Optional
+from abc import abstractmethod, ABCMeta
+from libpyvinyl.AbstractBaseClass import AbstractBaseClass
+
+
+class BaseData(AbstractBaseClass):
+ """The abstract data class. Inheriting classes represent simulation input and/or output
+ data and provide a harmonized user interface to simulation data of various kinds rather than a data format.
+ Their purpose is to provide a harmonized user interface to common data operations such as reading/writing from/to disk.
+
+ :param key: The key to identify the Data Object.
+ :type key: str
+ :param expected_data: A placeholder dict for expected data. The keys of this dict are expected to be found during the execution of `get_data()`.
+ The value for each key can be `None`.
+ :type expected_data: dict
+ :param data_dict: The dict to map by this DataClass. It has to be `None` if a file mapping was already set, defaults to None.
+ :type data_dict: dict, optional
+ :param filename: The filename of the file to map by this DataClass. It has to be `None` if a dict mapping was already set, defaults to None.
+ :type filename: str, optional
+ :param file_format_class: The FormatClass to map the file by this DataClass, It has to be `None` if a dict mapping was already set, defaults to None
+ :type file_format_class: class, optional
+ :param file_format_kwargs: The kwargs needed to map the file, defaults to None.
+ :type file_format_kwargs: dict, optional
+ """
+
+ def __init__(
+ self,
+ key: str,
+ expected_data: dict,
+ data_dict: Optional[dict] = None,
+ filename: Optional[str] = None,
+ file_format_class=None,
+ file_format_kwargs: Optional[dict] = None,
+ ):
+ self.__key = None
+ self.__expected_data = None
+ self.__data_dict = None
+ self.__filename = None
+ self.__file_format_class = None
+ self.__file_format_kwargs = None
+
+ self.key = key
+ # Expected_data is checked when `self.get_data()`
+ self.expected_data = expected_data
+ # This will be always be None if the data class is mapped to a file
+ self.data_dict = data_dict
+ # These will be always be None if the data class is mapped to a python data dict object
+ self.filename = filename
+ self.file_format_class = file_format_class
+ self.file_format_kwargs = file_format_kwargs
+
+ self.__check_consistency()
+
+ @property
+ def key(self) -> str:
+ """The key of the class instance for calculator usage"""
+ return self.__key
+
+ @key.setter
+ def key(self, value: str):
+ if isinstance(value, str):
+ self.__key = value
+ else:
+ raise TypeError(f"Data Class: key should be a str, not {type(value)}")
+
+ @property
+ def expected_data(self):
+ """The expected_data of the class instance for calculator usage"""
+ return self.__expected_data
+
+ @expected_data.setter
+ def expected_data(self, value):
+ if isinstance(value, dict):
+ self.__expected_data = value
+ else:
+ raise TypeError(
+ f"Data Class: expected_data should be a dict, not {type(value)}"
+ )
+
+ @property
+ def data_dict(self):
+ """The data_dict of the class instance for calculator usage"""
+ return self.__data_dict
+
+ @data_dict.setter
+ def data_dict(self, value):
+ if isinstance(value, dict):
+ self.__data_dict = value
+ elif value is None:
+ self.__data_dict = None
+ else:
+ raise TypeError(
+ f"Data Class: data_dict should be None or a dict, not {type(value)}"
+ )
+ self.__check_consistency()
+
+ def set_dict(self, data_dict: dict):
+ """Set a mapping dict for this DataClass.
+
+ :param data_dict: The data dict to map
+ :type data_dict: dict
+ """
+ self.data_dict = data_dict
+
+ def set_file(self, filename: str, format_class, **kwargs):
+ """Set a mapping file for this DataClass.
+
+ :param filename: The filename of the file to map.
+ :type filename: str
+ :param format_class: The FormatClass to map the file
+ :type format_class: class
+ """
+ self.filename = filename
+ self.file_format_class = format_class
+ self.file_format_kwargs = kwargs
+ self.__check_consistency()
+
+ @property
+ def filename(self):
+ """The filename of the file to map by this DataClass."""
+ return self.__filename
+
+ @filename.setter
+ def filename(self, value):
+ if isinstance(value, str):
+ self.__filename = value
+ elif value is None:
+ self.__filename = None
+ else:
+ raise TypeError(
+ f"Data Class: filename should be None or a str, not {type(value)}"
+ )
+
+ @property
+ def file_format_class(self):
+ """The FormatClass to map the file by this DataClass"""
+ return self.__file_format_class
+
+ @file_format_class.setter
+ def file_format_class(self, value):
+ if isinstance(value, ABCMeta):
+ self.__file_format_class = value
+ elif value is None:
+ self.__file_format_class = None
+ else:
+ raise TypeError(
+ f"Data Class: format_class should be None or a format class, not {type(value)}"
+ )
+
+ @property
+ def file_format_kwargs(self):
+ """The kwargs needed to map the file"""
+ return self.__file_format_kwargs
+
+ @file_format_kwargs.setter
+ def file_format_kwargs(self, value):
+ if isinstance(value, dict):
+ self.__file_format_kwargs = value
+ elif value is None:
+ self.__file_format_kwargs = None
+ else:
+ raise TypeError(
+ f"Data Class: file_format_kwargs should be None or a dict, not {type(value)}"
+ )
+
+ @property
+ def mapping_type(self):
+ """If this data class is a file mapping or python dict mapping."""
+ return self.__check_mapping_type()
+
+ def __check_mapping_type(self):
+ """Check the mapping_type of this class."""
+ if self.data_dict is not None:
+ return dict
+ elif self.filename is not None:
+ return self.file_format_class
+ else:
+ raise TypeError("Neither self.__data_dict or self.__filename was found.")
+
+ @property
+ def mapping_content(self):
+ """Returns an overview of the keys of the mapped dict or the filename of the mapped file"""
+ if self.mapping_type == dict:
+ return self.data_dict.keys()
+ else:
+ return self.filename
+
+ @staticmethod
+ def _add_ioformat(format_dict, format_class):
+ """Register an ioformat to a `format_dict` listing the formats supported by this DataClass.
+
+ :param format_dict: The dict listing the supported formats.
+ :type format_dict: dict
+ :param format_class: The FormatClass to add.
+ :type format_class: class
+ """
+ register = format_class.format_register()
+ for key, val in register.items():
+ if key == "key":
+ this_format = val
+ format_dict[val] = {}
+ else:
+ format_dict[this_format][key] = val
+
+ @classmethod
+ @abstractmethod
+ def supported_formats(self):
+ format_dict = {}
+ # Add the supported format classes when creating a concrete class.
+ # See the example at `tests/BaseDataTest.py`
+ self._add_ioformat(format_dict, FormatClass)
+ return format_dict
+
+ @classmethod
+ def list_formats(self):
+ """Print supported formats"""
+ out_string = ""
+ supported_formats = self.supported_formats()
+ for key in supported_formats:
+ dicts = supported_formats[key]
+ format_class = dicts["format_class"]
+ if format_class:
+ out_string += "Format class: {}\n".format(format_class)
+ out_string += "Key: {}\n".format(key)
+ out_string += "Description: {}\n".format(dicts["description"])
+ ext = dicts["ext"]
+ if ext != "":
+ out_string += "File extension: {}\n".format(ext)
+ kwargs = dicts["read_kwargs"]
+ if kwargs != [""]:
+ out_string += "Extra reading keywords: {}\n".format(kwargs)
+ kwargs = dicts["write_kwargs"]
+ if kwargs != [""]:
+ out_string += "Extra writing keywords: {}\n".format(kwargs)
+ out_string += "\n"
+ print(out_string)
+
+ def __check_consistency(self):
+ # If all of the file-related parameters are set:
+ if all([self.filename, self.file_format_class]):
+ # If the data_dict is also set:
+ if self.data_dict is not None:
+ raise RuntimeError(
+ "self.data_dict and self.filename can not be set for one data class at the same time."
+ )
+ else:
+ pass
+ # If any one of the file-related parameters is None:
+ elif (
+ self.filename is None
+ and self.file_format_class is None
+ and self.file_format_kwargs is None
+ ):
+ pass
+ # If some of the file-related parameters is None and some is not None:
+ else:
+ raise RuntimeError(
+ "self.filename, self.file_format_class, self.file_format_kwargs are not consistent."
+ )
+
+ @classmethod
+ def from_file(cls, filename: str, format_class, key: dict, **kwargs):
+ """Create a Data Object mapping a file.
+
+ :param filename: The filename of the file to map by this DataClass. It has to be `None` if a dict mapping was already set, defaults to None.
+ :type filename: str, optional
+ :param file_format_class: The FormatClass to map the file by this DataClass, It has to be `None` if a dict mapping was already set, defaults to None
+ :type file_format_class: class, optional
+ :param file_format_kwargs: The kwargs needed to map the file, defaults to None.
+ :type file_format_kwargs: dict, optional
+ :param key: The key to identify the Data Object.
+ :type key: str
+
+ :return: A Data Object
+ :rtype: BaseData
+ """
+ return cls(
+ key,
+ filename=filename,
+ file_format_class=format_class,
+ file_format_kwargs=kwargs,
+ )
+
+ @classmethod
+ def from_dict(cls, data_dict: dict, key: str):
+ """Create a Data Object mapping a data dict.
+
+ :param data_dict: The dict to map by this DataClass. It has to be `None` if a file mapping was already set, defaults to None.
+ :type data_dict: dict
+ :param key: The key to identify the Data Object.
+ :type key: str
+ :return: A Data Object
+ :rtype: BaseData
+ """
+ return cls(key, data_dict=data_dict)
+
+ def write(self, filename: str, format_class, key: str = None, **kwargs):
+ """Write the data mapped by the Data Object into a file and return a Data Object
+ mapping the file. It converts either a file or a python object to a file
+ The behavior related to a file will always be handled by the format class.
+ If it's a python dictionary mapping, write with the specified format_class
+ directly.
+
+ :param filename: The filename of the file to be written.
+ :type filename: str
+ :param file_format_class: The FormatClass to write the file.
+ :type file_format_class: class
+ :param key: The identification key of the new Data Object. When it's `None`, a new key will
+ be generated with a suffix added to the previous identification key by the FormatClass. Defaults to None.
+ :type key: str, optional
+ :return: A Data Object
+ :rtype: BaseData
+ """
+ if self.mapping_type == dict:
+ return format_class.write(self, filename, key, **kwargs)
+ elif format_class in self.file_format_class.direct_convert_formats():
+ return self.file_format_class.convert(
+ self, filename, format_class, key, **kwargs
+ )
+ # If it's a file mapping and would like to write in the same file format of the
+ # mapping, it will let the user know that a file containing the data in the same format already existed.
+ elif format_class == self.file_format_class:
+ print(
+ f"Hint: This data already existed in the file {self.__filename} in format {self.__file_format_class}. `cp {self.__filename} {filename}` could be faster."
+ )
+ print(
+ f"Will still write the data into the file {filename} in format {format_class}"
+ )
+ return format_class.write(self, filename, key, **kwargs)
+ else:
+ return format_class.write(self, filename, key, **kwargs)
+
+ def __check_for_expected_data(self, data_to_read):
+ """Check if the `data_to_read` contains the data we have"""
+ for key in self.expected_data.keys():
+ try:
+ data_to_read[key]
+ except KeyError:
+ raise KeyError(
+ f"Expected data dict key '{key}' is not found."
+ ) from None
+
+ def __get_dict_data(self):
+ """Get the data dict from a dict mapping"""
+ if self.__data_dict is not None:
+ # It will automatically check the data needed to be extracted.
+ self.__check_for_expected_data(self.__data_dict)
+ return self.data_dict
+ else:
+ raise RuntimeError(
+ "__get_dict_data() should not be called when self.__data_dict is None"
+ )
+
+ def __get_file_data(self):
+ """Get the data dict from a file mapping"""
+ if self.__filename is not None:
+ data_to_read = self.__file_format_class.read(
+ self.__filename, **self.__file_format_kwargs
+ )
+ # It will automatically check the data needed to be extracted.
+ self.__check_for_expected_data(data_to_read)
+ data_to_return = {}
+ for key in data_to_read.keys():
+ data_to_return[key] = data_to_read[key]
+ return data_to_return
+ else:
+ raise RuntimeError(
+ "__get_file_data() should not be called when self.__filename is None"
+ )
+
+ def get_data(self):
+ """Return the data in a dictionary"""
+ # From either a file or a python object to a python object
+ if self.__data_dict is not None:
+ return self.__get_dict_data()
+ elif self.__filename is not None:
+ return self.__get_file_data()
+
+ def __str__(self):
+ """Returns strings of Data objects info"""
+ string = f"key = {self.key}\n"
+ string += f"mapping = {self.mapping_type}: {self.mapping_content}"
+ return string
+
+
+# DataCollection class
+class DataCollection:
+ """A collection of Data Objects"""
+
+ def __init__(self, *args):
+ self.data_object_dict = {}
+ self.add_data(*args)
+
+ def __len__(self):
+ return len(self.data_object_dict)
+
+ def __setitem__(self, key, value):
+ if key != value.key:
+ print(
+ f"Warning: the key '{key}' of this DataCollection will be replaced by the key '{value.key}' set in the input data."
+ )
+ del self.data_object_dict[key]
+ self.add_data(value)
+
+ def __getitem__(self, keys):
+ if isinstance(keys, str):
+ return self.get_data_object(keys)
+ elif isinstance(keys, list):
+ subset = []
+ for key in keys:
+ subset.append(self.get_data_object(key))
+ return DataCollection(*subset)
+
+ def add_data(self, *args):
+ """Add data objects to the data colletion"""
+ for data in args:
+ assert isinstance(data, BaseData)
+ self.data_object_dict[data.key] = data
+
+ def get_data(self):
+ """Get the data of the data object(s).
+ When there is only one item in the DataCollection, it returns the data dict,
+ When there are more then one items, it returns a dictionary of the data dicts"""
+ if len(self.data_object_dict) == 1:
+ return next(iter(self.data_object_dict.values())).get_data()
+ else:
+ data_dicts = {}
+ for key, obj in self.data_object_dict.items():
+ data_dicts[key] = obj.get_data()
+ return data_dicts
+
+ def write(
+ self,
+ filename: Union[str, dict],
+ format_class,
+ key: Union[str, dict] = None,
+ **kwargs,
+ ):
+ """Write the data object(s) to the file(s).
+ When there is only one item in the DataCollection, it returns the data object mapping the file which was wirttern,
+ When there are more then one items, it returns a dictionary of the data objects.
+
+ :param filename: The name(s) of the file(s) to write. When there are multiple items, they are expected in
+ a dict where the keys corresponding to the data in this collection.
+ :type filename: str or dict
+ :param format_class: The format class of the file(s). When there are multiple items, they are expected in
+ a dict where the keys corresponding to the data in this collection.
+ :type format_class: class or dict
+ :param key: The key(s) of the data object(s) mapping the written file(s), defaults to None.
+ :type key: str or dict, optional
+ :return: A data object or a dict of data objects.
+ :rtype: DataClass or dict
+ """
+
+ if len(self.data_object_dict) == 1:
+ obj = next(iter(self.data_object_dict.values()))
+ return obj.write(filename, format_class, key, **kwargs)
+ else:
+ assert isinstance(key, dict)
+ data_dicts = {}
+ for col_key, obj in self.data_object_dict.items():
+ written_data = obj.write(
+ filename[col_key], format_class[col_key], key[col_key], **kwargs
+ )
+ data_dicts[written_data.key] = written_data
+ return data_dicts
+
+ def get_data_object(self, key: str):
+ """Get one data object by its key
+
+ :param key: The key of the data object to get.
+ :type key: str
+ :return: A data object
+ :rtype: DataClass
+ """
+ return self.data_object_dict[key]
+
+ def to_list(self):
+ """Return a list of the data objects in the data collection"""
+ return [value for value in self.data_object_dict.values()]
+
+ def __str__(self):
+ """Returns strings of the data object info"""
+ string = "Data collection:\n"
+ string += "key - mapping\n\n"
+ for data_object in self.data_object_dict.values():
+ string += f"{data_object.key} - {data_object.mapping_type}: {data_object.mapping_content}\n"
+ return string
diff --git a/libpyvinyl/BaseFormat.py b/libpyvinyl/BaseFormat.py
new file mode 100644
index 0000000..abdaac3
--- /dev/null
+++ b/libpyvinyl/BaseFormat.py
@@ -0,0 +1,109 @@
+from abc import abstractmethod
+from libpyvinyl.AbstractBaseClass import AbstractBaseClass
+from libpyvinyl.BaseData import BaseData
+
+
+class BaseFormat(AbstractBaseClass):
+ """The abstract format class. It's the interface of a certain data format."""
+
+ def __init__(self):
+ # Nothing needs to be done here.
+ pass
+
+ @classmethod
+ @abstractmethod
+ def format_register(self):
+ # Override this `format_register` method in a concrete format class.
+ key = "Base"
+ desciption = "Base data format"
+ file_extension = "base"
+ read_kwargs = [""]
+ write_kwargs = [""]
+ return self._create_format_register(
+ key, desciption, file_extension, read_kwargs, write_kwargs
+ )
+
+ @classmethod
+ def _create_format_register(
+ cls,
+ key: str,
+ desciption: str,
+ file_extension: str,
+ read_kwargs=[""],
+ write_kwargs=[""],
+ ):
+ format_register = {
+ "key": key, # FORMAT KEY
+ "description": desciption, # FORMAT DESCRIPTION
+ "ext": file_extension, # FORMAT EXTENSION
+ "format_class": cls, # CLASS NAME OF THE FORMAT
+ "read_kwargs": read_kwargs, # KEYWORDS LIST NEEDED TO READ
+ "write_kwargs": write_kwargs, # KEYWORDS LIST NEEDED TO WRITE
+ }
+ return format_register
+
+ @classmethod
+ @abstractmethod
+ def read(self, filename: str, **kwargs) -> dict:
+ """Read the data from the file with the `filename` to a dictionary. The dictionary will
+ be used by its corresponding data class."""
+ # Example codes. Override this function in a concrete class.
+ data_dict = {}
+ with h5py.File(filename, "r") as h5:
+ for key, val in h5.items():
+ data_dict[key] = val[()]
+ return data_dict
+
+ @classmethod
+ @abstractmethod
+ def write(cls, object: BaseData, filename: str, key: str, **kwargs):
+ """Save the data with the `filename`."""
+ # Example codes. Override this function in a concrete class.
+ data_dict = object.get_data()
+ arr = np.array([data_dict["number"]])
+ np.savetxt(filename, arr, fmt="%.3f")
+ if key is None:
+ original_key = object.key
+ key = original_key + "_to_TXTFormat"
+ return object.from_file(filename, cls, key)
+ else:
+ return object.from_file(filename, cls, key)
+
+ @staticmethod
+ @abstractmethod
+ def direct_convert_formats():
+ # Assume the format can be converted directly to the formats supported by these classes:
+ # AFormat, BFormat
+ # Override this `direct_convert_formats` in a concrete format class
+ return [Aformat, BFormat]
+
+ @classmethod
+ @abstractmethod
+ def convert(
+ cls, obj: BaseData, output: str, output_format_class: str, key, **kwargs
+ ):
+ """Direct convert method, if the default converting would be too slow or not suitable for the output_format"""
+ # If there is no direct converting supported:
+ raise NotImplementedError
+ if output_format_class is AFormat:
+ return cls.convert_to_AFormat(obj.filename, output)
+ else:
+ raise TypeError(
+ "Direct converting to format {} is not supported".format(
+ output_format_class
+ )
+ )
+ # Set the key of the returned object
+ if key is None:
+ original_key = obj.key
+ key = original_key + "_from_BaseFormat"
+ return obj.from_file(output, output_format_class, key)
+
+ # Example convert_to_AFormat()
+ # @classmethod
+ # def convert_to_AFormat(cls, input: str, output: str):
+ # """The engine of convert method."""
+ # print("Directly converting BaseFormat to AFormat")
+ # number = float(np.loadtxt(input))
+ # with h5py.File(output, "w") as h5:
+ # h5["number"] = number
diff --git a/libpyvinyl/BeamlinePropagator.py b/libpyvinyl/BeamlinePropagator.py
deleted file mode 100644
index b5f3193..0000000
--- a/libpyvinyl/BeamlinePropagator.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""
-:module BeamlinePropagator: Module hosting the BeamlinePropagator and BeamlinePropagatorParameters
-abstract classes.
-"""
-
-
-####################################################################################
-# #
-# This file is part of libpyvinyl - The APIs for Virtual Neutron and x-raY #
-# Laboratory. #
-# #
-# Copyright (C) 2020 Carsten Fortmann-Grote #
-# #
-# This program is free software: you can redistribute it and/or modify it under #
-# the terms of the GNU Lesser General Public License as published by the Free #
-# Software Foundation, either version 3 of the License, or (at your option) any #
-# later version. #
-# #
-# This program is distributed in the hope that it will be useful, but WITHOUT ANY #
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A #
-# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. #
-# #
-# You should have received a copy of the GNU Lesser General Public License along #
-# with this program. If not, see str:
+ """Returning the units as a string"""
+ return str(self.__unit)
+
+ @unit.setter
+ def unit(self, uni):
"""
- Sets a legal interval for this parameter, None for infinite
+ Assignment of the units
+
+ A pint.Unit is used if the string is recognized as a valid unit in the registry.
+ It is stored as a string otherwise.
"""
- if min_value is None:
- min_value = -math.inf
- if max_value is None:
- max_value = math.inf
+ try:
+ self.__unit = Unit(uni)
+ except pint.errors.UndefinedUnitError:
+ self.__unit = uni
+
+ @property
+ def value_no_conversion(self):
+ """
+ Returning the object stored in value with no conversions
+ """
+ return self.__value
+
+ @property
+ def pint_value(self):
+ """Returning the value as a pint object if available, an error otherwise"""
+ if not isinstance(self.__value, Quantity):
+ raise TypeError("The parameter value is not of pint.Quantity type")
+ return self.__value
+
+ @property
+ def value(self):
+ if isinstance(self.__value, Quantity):
+ return self.__value.m_as(self.__unit)
+ else:
+ return self.__value
+
+ @staticmethod
+ def __is_type_compatible(t1: type, t2: Union[None, type]) -> bool:
+ """
+ Check type compatibility
+
+ :param t1: first type
+ :type t1: type
+
+ :param t2: second type
+ :type t2: type
+
+ :return: bool
+
+ True if t1 and t2 are of the same type or if one is int and the other float
+ False otherwise
+ """
+ if t1 == type(None) or t2 == type(None):
+ return True
+ if t1 == None or t2 == None:
+ return True
+
+ # promote any int or float to pint.Quantity
+ if t1 == float or t1 == int or t1 == numpy.float64:
+ t1 = Quantity
+ if t2 == float or t2 == int or t2 == numpy.float64:
+ t2 = Quantity
+
+ if "quantity" in str(t1):
+ t1 = Quantity
+ if "quantity" in str(t2):
+ t2 = Quantity
+
+ if t1 == t2:
+ return True
+
+ return False
+
+ def __to_quantity(self, value: Any) -> Union[Quantity, Any]:
+ """
+ Converts value into a pint.Quantity if this Parameter is defined to be a Quantity.
+ It returns value unaltered otherwise.
+ """
+
+ if self.__value_type == Quantity and not isinstance(value, Quantity):
+ return Quantity(value, self.__unit)
+
+ return value
+
+ def __set_value_type(self, value: Any) -> None:
+ """
+ Sets the type for the parameter.
+ It should always be preceded by a __check_compatibility to avoid chaning the type for the Parameter
- self.legal_intervals.append([min_value, max_value])
+ :param value: a value that might be assigned as Parameter value or in an interval or option
+ :type value: any type
- def add_illegal_interval(self, min_value, max_value):
+ It will raise an exception if the type is not coherent to what previously is declared.
"""
- Sets an illegal interval for this parameter, None for infinite
+ if (
+ hasattr(value, "__iter__")
+ and not isinstance(value, str)
+ and not isinstance(value, Quantity)
+ ):
+ value = value[0]
+
+ # if an integer has units, then it is a quantity -> promotion
+ if isinstance(value, int) and self.__unit != "":
+ self.__value_type = Quantity
+ # if value is a float, than can be used as a quantity -> promotion
+ elif isinstance(value, float):
+ self.__value_type = Quantity
+ else: # cannot be treated as a quantity
+ self.__value_type = type(value)
+
+ def __check_compatibility(self, value: Any) -> None:
"""
+ Raises an error if this parameter and the given value are not of the same type or compatible
+ :param value: a value that might be assigned as Parameter value or in an interval or option
+ :type value: any type
+
+ It will raise an exception if the type is not coherent to what previously is declared.
+ """
+
+ vtype = type(value)
+ assert vtype != None
+ v = value
+ # First case: value is a list, it might be good to double check
+ # that all the members are of the same type
+ if isinstance(value, list):
+ vtype = type(value[0])
+ for v in value:
+ if not self.__is_type_compatible(vtype, type(v)):
+ raise TypeError(
+ "Iterable object passed as value for the parameter, but it is made of inhomogeneous types: ",
+ vtype,
+ type(v),
+ )
+ elif isinstance(value, dict):
+ raise NotImplementedError("Dictionaries are not accepted")
+
+ # check that the value is compatible with what previously defined
+ if not self.__is_type_compatible(vtype, self.__value_type):
+ raise TypeError(
+ "New value of type {} is different from {} previously defined".format(
+ type(value), self.__value_type
+ )
+ )
+
+ @value.setter
+ def value(self, value):
+ """
+ Sets value of this parameter if value is legal,
+ an exception is raised otherwise.
+
+ :param value: value
+ :type value: str | boolean | int | float | object | pint.Quantity
+ If value is a float, it is internally converted to a pint.Quantity
+ """
+ self.__check_compatibility(value)
+ self.__set_value_type(value)
+ value = self.__to_quantity(value)
+
+ if self.is_legal(value):
+ self.__value = value
+ else:
+ raise ValueError("Value of parameter '" + self.name + "' illegal.")
+
+ def add_interval(self, min_value, max_value, intervals_are_legal):
+ """
+ Sets an interval for this parameter: [min_value, max_value]
+ The interval is closed on both sides: min_value and and max_value are included.
+
+ :param min_value: minimum value of the interval
+ :type min_value: float or None for infinite
+
+ :param max_value: maximum value of the interval
+ :type max_value: float or None for infinite
+
+ :param intervals_are_legal: if not done previously, it defines if all the intervals of this parameter should be considered as allowed or forbidden intervals.
+ :type intervals_are_legal: boolean
+
+ """
+
if min_value is None:
min_value = -math.inf
if max_value is None:
max_value = math.inf
- self.illegal_intervals.append([min_value, max_value])
+ self.__check_compatibility(min_value)
+ self.__check_compatibility(max_value)
+
+ self.__set_value_type(min_value) # it could have been max_value
- def add_option(self, option):
+ if self.__intervals_are_legal is None:
+ self.__intervals_are_legal = intervals_are_legal
+ else:
+ if self.__intervals_are_legal != intervals_are_legal:
+ print("WARNING: All intervals should be either legal or illegal.")
+ print(
+ " Interval: ["
+ + str(min_value)
+ + ":"
+ + str(max_value)
+ + "] is declared differently w.r.t. to the previous intervals"
+ )
+ # should it throw an expection?
+ raise ValueError("Parameter", "interval", "multiple validities")
+
+ self.__intervals.append(
+ [self.__to_quantity(min_value), self.__to_quantity(max_value)]
+ )
+
+ # if the interval has been added after assignement of the value of the parameter,
+ # the latter should be checked
+ if not self.value_no_conversion is None:
+ if self.is_legal(self.value) is False:
+ raise ValueError(
+ "Value "
+ + str(self.value)
+ + " is now illegal based on the newly added interval"
+ )
+
+ def add_option(self, option, options_are_legal):
"""
Sets allowed values for this parameter
"""
- if isinstance(option, list):
- self.options += option
+
+ if self.__options_are_legal is None:
+ self.__options_are_legal = options_are_legal
else:
- self.options.append(option)
+ if self.__options_are_legal != options_are_legal:
+ print("ERROR: All options should be either legal or illegal.")
+ print(
+ " This option is declared differently w.r.t. to the previous ones"
+ )
+ # should it throw an expection?
+ raise ValueError("Parameter", "options", "multiple validities")
- def set_value(self, value):
- """
- Sets value of this parameter if value is legal, otherwise warning is shown
+ self.__check_compatibility(option)
+ self.__set_value_type(option) # it could have been max_value
- This could be expanded to raise an exception, or such could be in is_legal
- """
- if self.is_legal(value):
- self.value = value
+ if isinstance(option, list):
+ for op in option:
+ self.__options.append(self.__to_quantity(op))
else:
- print("WARNING: Value of parameter '" + self.name
- + "' illegal, ignored.")
-
- def is_legal(self, value=None):
+ self.__options.append(self.__to_quantity(option))
+
+ # if the option has been added after assignement of the value of the parameter,
+ # the latter should be checked
+ if not self.value_no_conversion is None:
+ if self.is_legal(self.value) is False:
+ raise ValueError(
+ "Value "
+ + str(self.value)
+ + " is now illegal based on the newly added option"
+ )
+
+ def is_legal(self, values=None):
"""
Checks whether or not given or contained value is legal given constraints.
- Illegal intervals have the highest priority to be checked. Then it will check the
- legal intervals and options. The overlaps among the constrains will be overridden by
- the constrain of higher priority.
"""
- if value is None:
- value = self.value
- # Check illegal intervals
- for illegal_interval in self.illegal_intervals:
- if illegal_interval[0] < value < illegal_interval[1]:
- return False
+ if values is None:
+ values = self.__value
- # Check legal intervals
- is_inside_a_legal_interval = False
- for legal_interval in self.legal_intervals:
- if legal_interval[0] < value < legal_interval[1]:
- is_inside_a_legal_interval = True
+ if (
+ not hasattr(values, "__iter__")
+ or isinstance(values, str)
+ or isinstance(values, Quantity)
+ ):
+ # print(str(hasattr(values, "__iter__")) + str(values))
+ # first if types are compatible
- if not is_inside_a_legal_interval and len(self.legal_intervals) > 0:
- return False
+ if self.__is_type_compatible(type(values), self.__value_type) is False:
+ return False
- # checked intervals, can return if options not used (frequent case)
- if len(self.options) == 0:
- return True
+ value = self.__to_quantity(values)
- for option in self.options:
- if option == value:
- # If the value matches any option, it is legal
+ # obvious, if no conditions are defined, the value is always legal
+ if len(self.__options) == 0 and len(self.__intervals) == 0:
return True
- # Since no options matched the parameter, it is illegal
- return False
+ # first check if the value is in any defined discrete value
+ for option in self.__options:
+ if option == value:
+ return self.__options_are_legal
+
+ # secondly check if it is in any defined interval
+ for interval in self.__intervals:
+ if interval[0] <= value <= interval[1]:
+ return self.__intervals_are_legal
+
+ # at this point the value has not been found in any interval
+ # if intervals where defined and were forbidden intervals, the value should be accepted
+ if len(self.__intervals) > 0:
+ return not self.__intervals_are_legal
+
+ # if there where no intervals defined, then it depends if the discrete values were forbidden or allowed
+ return not self.__options_are_legal
+
+ # else
+ # all values have to be True
+
+ for value in values:
+ if not self.is_legal(value):
+ return False
+
+ return True
def print_paramter_constraints(self):
"""
- Print the legal and illegal intervals of this parameter.
+ Print the legal and illegal intervals of this parameter. FIXME
"""
print(self.name)
- print('illegal intervals:', self.illegal_intervals)
- print('legal intervals:', self.legal_intervals)
- print('options', self.options)
+ print("intervals:", self.__intervals)
+ print("intervals are legal:", self.__intervals_are_legal)
+ print("options", self.__options)
+ print("options are legal:", self.__options_are_legal)
- def clear_legal_intervals(self):
+ def clear_intervals(self):
"""
- Clear the legal intervals of this parameter.
+ Clear the intervals of this parameter.
"""
- self.legal_intervals = []
-
- def clear_illegal_intervals(self):
- """
- Clear the illegal intervals of this parameter.
- """
- self.illegal_intervals = []
+ self.__intervals = []
def clear_options(self):
"""
Clear the option values of this parameter.
"""
- self.options = []
+ self.__options = []
def print_line(self):
"""
returns string with one line description of parameter
"""
- if self.unit is None:
+ if self.__unit is None or self.__unit == Unit(""):
unit_string = ""
else:
- unit_string = "[" + self.unit + "]"
+ unit_string = "[" + str(self.__unit) + "] "
- if self.value is None:
- string = self.name.ljust(20)
+ if self.value_no_conversion is None:
+ string = self.name.ljust(40) + " "
else:
- string = self.name.ljust(15)
- string += str(self.value).ljust(5)
+ string = self.name.ljust(35) + " "
+ string += str(self.value).ljust(10) + " "
- string += unit_string.ljust(10)
+ string += unit_string.ljust(20) + " "
if self.comment is not None:
string += self.comment
string += 3 * " "
- for legal_interval in self.legal_intervals:
- interval = "L[" + str(legal_interval[0]) + ", " + str(
- legal_interval[1]) + "]"
- string += interval.ljust(10)
-
- for illegal_interval in self.illegal_intervals:
- interval = "I[" + str(illegal_interval[0]) + ", " + str(
- illegal_interval[1]) + "]"
+ for interval in self.__intervals:
+ legal = "L" if self.__intervals_are_legal else "I"
+ interval = legal + "[" + str(interval[0]) + ", " + str(interval[1]) + "]"
string += interval.ljust(10)
- if len(self.options) > 0:
+ if len(self.__options) > 0:
values = "("
- for option in self.options:
+ for option in self.__options:
values += str(option) + ", "
values = values.strip(", ")
values += ")"
@@ -180,32 +454,29 @@ def __repr__(self):
Returns string with thorough description of parameter
"""
string = "Parameter named: '" + self.name + "'"
- if self.value is None:
+ if self.value_no_conversion is None:
string += " without set value.\n"
else:
string += " with value: " + str(self.value) + "\n"
- if self.unit is not None:
- string += " [" + self.unit + "]\n"
+ if self.__unit is not None:
+ string += " [" + str(self.__unit) + "]\n"
if self.comment is not None:
string += " " + self.comment + "\n"
- if len(self.legal_intervals) > 0:
- string += " Legal intervals:\n"
- for legal_interval in self.legal_intervals:
- string += " [" + str(legal_interval[0]) + "," + str(
- legal_interval[1]) + "]\n"
-
- if len(self.illegal_intervals) > 0:
- string += " Illegal intervals:\n"
- for illegal_interval in self.illegal_intervals:
- string += " [" + str(illegal_interval[0]) + ", " + str(
- illegal_interval[1]) + "]\n"
-
- if len(self.options) > 0:
- string += " Allowed values:\n"
- for option in self.options:
+ if len(self.__intervals) > 0:
+ string += (
+ " Legal intervals:\n"
+ if self.__intervals_are_legal
+ else " Illegal intervals:\n"
+ )
+ for interval in self.__intervals:
+ string += " [" + str(interval[0]) + "," + str(interval[1]) + "]\n"
+
+ if len(self.__options) > 0:
+ string += " Allowed values:\n" # FIXME
+ for option in self.__options:
string += " " + str(option) + "\n"
return string
diff --git a/libpyvinyl/Parameters/__init__.py b/libpyvinyl/Parameters/__init__.py
index b6f1c9a..3f66957 100644
--- a/libpyvinyl/Parameters/__init__.py
+++ b/libpyvinyl/Parameters/__init__.py
@@ -1,2 +1,2 @@
from .Parameter import Parameter
-from .Collections import InstrumentParameters, CalculatorParameters, MasterParameter
\ No newline at end of file
+from .Collections import InstrumentParameters, CalculatorParameters, MasterParameter
diff --git a/libpyvinyl/SignalGenerator.py b/libpyvinyl/SignalGenerator.py
deleted file mode 100644
index 2a5304b..0000000
--- a/libpyvinyl/SignalGenerator.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""
-:module SignalGenerator: Module hosting the SignalGenerator and SignalGeneratorParameters
-abstract classes.
-"""
-
-
-####################################################################################
-# #
-# This file is part of libpyvinyl - The APIs for Virtual Neutron and x-raY #
-# Laboratory. #
-# #
-# Copyright (C) 2020 Carsten Fortmann-Grote #
-# #
-# This program is free software: you can redistribute it and/or modify it under #
-# the terms of the GNU Lesser General Public License as published by the Free #
-# Software Foundation, either version 3 of the License, or (at your option) any #
-# later version. #
-# #
-# This program is distributed in the hope that it will be useful, but WITHOUT ANY #
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A #
-# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. #
-# #
-# You should have received a copy of the GNU Lesser General Public License along #
-# with this program. If not, see OK <---')
- sys.exit(0)
-
- sys.exit(1)
diff --git a/tests/integration/plusminus/.github/ISSUE_TEMPLATE.md b/tests/integration/plusminus/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..92fcec9
--- /dev/null
+++ b/tests/integration/plusminus/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,15 @@
+* PlusMinus version:
+* Python version:
+* Operating System:
+
+### Description
+
+Describe what you were trying to get done.
+Tell us what happened, what went wrong, and what you expected to happen.
+
+### What I Did
+
+```
+Paste the command(s) you ran and the output.
+If there was a crash, please include the traceback here.
+```
diff --git a/tests/integration/plusminus/.gitignore b/tests/integration/plusminus/.gitignore
new file mode 100644
index 0000000..43091aa
--- /dev/null
+++ b/tests/integration/plusminus/.gitignore
@@ -0,0 +1,105 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# dotenv
+.env
+
+# virtualenv
+.venv
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+# IDE settings
+.vscode/
\ No newline at end of file
diff --git a/tests/integration/plusminus/README.rst b/tests/integration/plusminus/README.rst
new file mode 100644
index 0000000..f362d3b
--- /dev/null
+++ b/tests/integration/plusminus/README.rst
@@ -0,0 +1,18 @@
+=========
+PlusMinus
+=========
+
+An example of a small platform implementing libpyvinyl.
+
+Data structure
+##############
+.. image:: ./docs/01-data_structure.png
+
+Instrument example
+##################
+.. image:: ./docs/02-instrument_example.png
+
+This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
+
+.. _Cookiecutter: https://github.com/audreyr/cookiecutter
+.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
diff --git a/tests/integration/plusminus/docs/01-data_structure.png b/tests/integration/plusminus/docs/01-data_structure.png
new file mode 100644
index 0000000000000000000000000000000000000000..39778312df45756357840344dc5859542a6c8dc4
GIT binary patch
literal 218354
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